diff options
917 files changed, 12974 insertions, 7389 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 19540e4331e..d35fd28c766 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,4 +1,4 @@ -image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.5-golang-1.8-git-2.13-chrome-62.0-node-8.x-yarn-1.2-postgresql-9.6" +image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.5-golang-1.8-git-2.14-chrome-63.0-node-8.x-yarn-1.2-postgresql-9.6" .default-cache: &default-cache key: "ruby-235-with-yarn" @@ -76,10 +76,15 @@ stages: except: - /(^docs[\/-].*|.*-docs$)/ +.except-qa: &except-qa + except: + - /(^qa[\/-].*|.*-qa$)/ + .rspec-metadata: &rspec-metadata <<: *dedicated-runner <<: *pull-cache <<: *except-docs + <<: *except-qa stage: test script: - JOB_NAME=( $CI_JOB_NAME ) @@ -118,6 +123,7 @@ stages: <<: *dedicated-runner <<: *pull-cache <<: *except-docs + <<: *except-qa stage: test script: - JOB_NAME=( $CI_JOB_NAME ) @@ -169,6 +175,7 @@ package-qa: # Review docs base .review-docs: &review-docs + <<: *except-qa image: ruby:2.4-alpine before_script: - gem install gitlab --no-doc @@ -214,6 +221,7 @@ review-docs-cleanup: retrieve-tests-metadata: <<: *tests-metadata-state <<: *except-docs + <<: *except-qa stage: prepare cache: key: tests_metadata @@ -265,6 +273,7 @@ flaky-examples-check: except: - master - /(^docs[\/-].*|.*-docs$)/ + - /(^qa[\/-].*|.*-qa$)/ artifacts: expire_in: 30d paths: @@ -369,6 +378,7 @@ spinach-mysql 3 4: *spinach-metadata-mysql <<: *ruby-static-analysis <<: *dedicated-runner <<: *except-docs + <<: *except-qa <<: *pull-cache stage: test script: @@ -387,6 +397,7 @@ static-analysis: # - Make sure cURL examples in API docs use the full switches docs lint: <<: *dedicated-runner + <<: *except-qa image: "registry.gitlab.com/gitlab-org/gitlab-build-images:nanoc-bootstrap-ruby-2.4-alpine" stage: test cache: {} @@ -409,6 +420,7 @@ downtime_check: - tags - /^[\d-]+-stable(-ee)?$/ - /(^docs[\/-].*|.*-docs$)/ + - /(^qa[\/-].*|.*-qa$)/ ee_compat_check: <<: *rake-exec @@ -430,6 +442,7 @@ ee_compat_check: .db-migrate-reset: &db-migrate-reset <<: *dedicated-runner <<: *except-docs + <<: *except-qa <<: *pull-cache stage: test script: @@ -447,6 +460,7 @@ db:migrate:reset-mysql: <<: *dedicated-runner <<: *pull-cache <<: *except-docs + <<: *except-qa stage: test variables: SETUP_DB: "false" @@ -473,6 +487,7 @@ migration:path-mysql: .db-rollback: &db-rollback <<: *dedicated-runner <<: *except-docs + <<: *except-qa <<: *pull-cache stage: test script: @@ -490,6 +505,7 @@ db:rollback-mysql: .db-seed_fu: &db-seed_fu <<: *dedicated-runner <<: *except-docs + <<: *except-qa <<: *pull-cache stage: test variables: @@ -524,6 +540,7 @@ db:check-schema-pg: gitlab:assets:compile: <<: *dedicated-runner <<: *except-docs + <<: *except-qa <<: *pull-cache stage: test dependencies: [] @@ -547,6 +564,7 @@ karma: <<: *use-pg <<: *dedicated-runner <<: *except-docs + <<: *except-qa <<: *pull-cache stage: test variables: @@ -599,6 +617,7 @@ qa:internal: coverage: <<: *dedicated-runner <<: *except-docs + <<: *except-qa <<: *pull-cache stage: post-test services: [] @@ -618,6 +637,7 @@ coverage: lint:javascript:report: <<: *dedicated-runner <<: *except-docs + <<: *except-qa <<: *pull-cache stage: post-test dependencies: @@ -677,6 +697,7 @@ cache gems: gitlab_git_test: <<: *pull-cache <<: *except-docs + <<: *except-qa variables: SETUP_DB: "false" script: diff --git a/.rubocop.yml b/.rubocop.yml index c427f219a0d..7721cfaf850 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1185,7 +1185,20 @@ RSpec/SubjectStub: RSpec/VerifiedDoubles: Enabled: false -# GitlabSecurity ############################################################## +# Gitlab ################################################################### + +Gitlab/ModuleWithInstanceVariables: + Enable: true + Exclude: + # We ignore Rails helpers right now because it's hard to workaround it + - app/helpers/**/*_helper.rb + # We ignore Rails mailers right now because it's hard to workaround it + - app/mailers/emails/**/*.rb + # We ignore spec helpers because it usually doesn't matter + - spec/support/**/*.rb + - features/steps/**/*.rb + +# GitlabSecurity ########################################################### GitlabSecurity/DeepMunge: Enabled: true diff --git a/CHANGELOG.md b/CHANGELOG.md index 6088a1b3515..fd7825b5f82 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,37 @@ documentation](doc/development/changelog.md) for instructions on adding your own entry. +## 10.2.5 (2017-12-15) + +### Fixed (8 changes) + +- Create a fork network for forks with a deleted source. !15595 +- Correctly link to a forked project from the new fork page. !15653 +- Fix the fork project functionality for projects with hashed storage. !15671 +- Fix updateEndpoint undefined error for issue_show app root. !15698 +- Fix broken illustration images for monitoring page empty states. !15889 +- Fix related branches/Merge requests failing to load when the hostname setting is changed. +- Fix gitlab:import:repos Rake task moving repositories into the wrong location. +- Gracefully handle case when repository's root ref does not exist. + +### Performance (3 changes) + +- Keep track of all circuitbreaker keys in a set. !15613 +- Only load branch names for protected branch checks. +- Optimize API /groups/:id/projects by preloading associations. + + +## 10.2.4 (2017-12-07) + +### Security (5 changes) + +- Fix e-mail address disclosure through member search fields +- Prevent creating issues through API when user does not have permissions +- Prevent an information disclosure in the Groups API +- Fix user without access to private Wiki being able to see it on the project page +- Fix Cross-Site Scripting (XSS) vulnerability while editing a comment + + ## 10.2.3 (2017-11-30) ### Fixed (7 changes) @@ -237,6 +268,17 @@ entry. - Add Gitaly metrics to the performance bar. +## 10.1.5 (2017-12-07) + +### Security (5 changes) + +- Fix e-mail address disclosure through member search fields +- Prevent creating issues through API when user does not have permissions +- Prevent an information disclosure in the Groups API +- Fix user without access to private Wiki being able to see it on the project page +- Fix Cross-Site Scripting (XSS) vulnerability while editing a comment + + ## 10.1.4 (2017-11-14) ### Fixed (4 changes) @@ -485,6 +527,17 @@ entry. - creation of keys moved to services. !13331 (haseebeqx) - Add username as GL_USERNAME in hooks. +## 10.0.7 (2017-12-07) + +### Security (5 changes) + +- Fix e-mail address disclosure through member search fields +- Prevent creating issues through API when user does not have permissions +- Prevent an information disclosure in the Groups API +- Fix user without access to private Wiki being able to see it on the project page +- Fix Cross-Site Scripting (XSS) vulnerability while editing a comment + + ## 10.0.5 (2017-11-03) - [FIXED] Fix incorrect X-axis labels in Prometheus graphs. !14258 diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index cb6b534abe1..7e750b4ebf3 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -0.59.0 +0.60.0 diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION index 4e32c7b1caf..e030a0157c9 100644 --- a/GITLAB_SHELL_VERSION +++ b/GITLAB_SHELL_VERSION @@ -1 +1 @@ -5.10.1 +5.10.3 @@ -283,7 +283,7 @@ group :metrics do gem 'influxdb', '~> 0.2', require: false # Prometheus - gem 'prometheus-client-mmap', '~> 0.7.0.beta39' + gem 'prometheus-client-mmap', '~> 0.7.0.beta43' gem 'raindrops', '~> 0.18' end @@ -311,7 +311,7 @@ group :development, :test do gem 'fuubar', '~> 2.2.0' gem 'database_cleaner', '~> 1.5.0' - gem 'factory_girl_rails', '~> 4.7.0' + gem 'factory_bot_rails', '~> 4.8.2' gem 'rspec-rails', '~> 3.6.0' gem 'rspec-retry', '~> 0.4.5' gem 'spinach-rails', '~> 0.2.1' @@ -400,14 +400,18 @@ group :ed25519 do end # Gitaly GRPC client -gem 'gitaly-proto', '~> 0.59.0', require: 'gitaly' +gem 'gitaly-proto', '~> 0.61.0', require: 'gitaly' gem 'toml-rb', '~> 0.3.15', require: false # Feature toggles -gem 'flipper', '~> 0.10.2' -gem 'flipper-active_record', '~> 0.10.2' +gem 'flipper', '~> 0.11.0' +gem 'flipper-active_record', '~> 0.11.0' +gem 'flipper-active_support_cache_store', '~> 0.11.0' # Structured logging gem 'lograge', '~> 0.5' gem 'grape_logging', '~> 1.7' + +# Asset synchronization +gem 'asset_sync', '~> 2.2.0' diff --git a/Gemfile.lock b/Gemfile.lock index 6213167ae0b..11040fab805 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -58,6 +58,11 @@ GEM asciidoctor (1.5.3) asciidoctor-plantuml (0.0.7) asciidoctor (~> 1.5) + asset_sync (2.2.0) + activemodel (>= 4.1.0) + fog-core + mime-types (>= 2.99) + unf ast (2.3.0) atomic (1.1.99) attr_encrypted (3.0.3) @@ -190,10 +195,10 @@ GEM excon (0.57.1) execjs (2.6.0) expression_parser (0.9.0) - factory_girl (4.7.0) + factory_bot (4.8.2) activesupport (>= 3.0.0) - factory_girl_rails (4.7.0) - factory_girl (~> 4.7.0) + factory_bot_rails (4.8.2) + factory_bot (~> 4.8.2) railties (>= 3.0.0) faraday (0.12.2) multipart-post (>= 1.2, < 3) @@ -210,10 +215,13 @@ GEM path_expander (~> 1.0) ruby_parser (~> 3.0) sexp_processor (~> 4.0) - flipper (0.10.2) - flipper-active_record (0.10.2) + flipper (0.11.0) + flipper-active_record (0.11.0) activerecord (>= 3.2, < 6) - flipper (~> 0.10.2) + flipper (~> 0.11.0) + flipper-active_support_cache_store (0.11.0) + activesupport (>= 3.2, < 6) + flipper (~> 0.11.0) flowdock (0.7.1) httparty (~> 0.7) multi_json @@ -276,7 +284,7 @@ GEM po_to_json (>= 1.0.0) rails (>= 3.2.0) gherkin-ruby (0.3.2) - gitaly-proto (0.59.0) + gitaly-proto (0.61.0) google-protobuf (~> 3.1) grpc (~> 1.0) github-linguist (4.7.6) @@ -975,6 +983,7 @@ DEPENDENCIES asana (~> 0.6.0) asciidoctor (~> 1.5.2) asciidoctor-plantuml (= 0.0.7) + asset_sync (~> 2.2.0) attr_encrypted (~> 3.0.0) awesome_print (~> 1.2.0) babosa (~> 1.0.2) @@ -1011,12 +1020,13 @@ DEPENDENCIES dropzonejs-rails (~> 0.7.1) email_reply_trimmer (~> 0.1) email_spec (~> 1.6.0) - factory_girl_rails (~> 4.7.0) + factory_bot_rails (~> 4.8.2) faraday (~> 0.12) ffaker (~> 2.4) flay (~> 2.8.0) - flipper (~> 0.10.2) - flipper-active_record (~> 0.10.2) + flipper (~> 0.11.0) + flipper-active_record (~> 0.11.0) + flipper-active_support_cache_store (~> 0.11.0) fog-aliyun (~> 0.2.0) fog-aws (~> 1.4) fog-core (~> 1.44) @@ -1032,7 +1042,7 @@ DEPENDENCIES gettext (~> 3.2.2) gettext_i18n_rails (~> 1.8.0) gettext_i18n_rails_js (~> 1.2.0) - gitaly-proto (~> 0.59.0) + gitaly-proto (~> 0.61.0) github-linguist (~> 4.7.0) gitlab-flowdock-git-hook (~> 1.0.1) gitlab-markup (~> 1.6.2) @@ -1107,7 +1117,7 @@ DEPENDENCIES peek-sidekiq (~> 1.0.3) pg (~> 0.18.2) premailer-rails (~> 1.9.7) - prometheus-client-mmap (~> 0.7.0.beta39) + prometheus-client-mmap (~> 0.7.0.beta43) pry-byebug (~> 3.4.1) pry-rails (~> 0.3.4) rack-attack (~> 4.4.1) diff --git a/PROCESS.md b/PROCESS.md index 7c8db689256..3fcf676b302 100644 --- a/PROCESS.md +++ b/PROCESS.md @@ -130,7 +130,8 @@ freeze date (the 7th) should have a corresponding Enterprise Edition merge request, even if there are no conflicts. This is to reduce the size of the subsequent EE merge, as we often merge a lot to CE on the release date. For more information, see -[limit conflicts with EE when developing on CE][limit_ee_conflicts]. +[Automatic CE->EE merge][automatic_ce_ee_merge] and +[Guidelines for implementing Enterprise Edition features][ee_features]. ### After the 7th @@ -281,4 +282,5 @@ still an issue I encourage you to open it on the [GitLab.com issue tracker](http ["Implement design & UI elements" guidelines]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#implement-design-ui-elements [Thoughtbot code review guide]: https://github.com/thoughtbot/guides/tree/master/code-review [done]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#definition-of-done -[limit_ee_conflicts]: https://docs.gitlab.com/ce/development/limit_ee_conflicts.html +[automatic_ce_ee_merge]: https://docs.gitlab.com/ce/development/automatic_ce_ee_merge.html +[ee_features]: https://docs.gitlab.com/ce/development/ee_features.html diff --git a/app/assets/images/icon_image_comment.svg b/app/assets/images/icon_image_comment.svg deleted file mode 100644 index cf6cb972940..00000000000 --- a/app/assets/images/icon_image_comment.svg +++ /dev/null @@ -1 +0,0 @@ -<svg width="24" height="30" viewBox="0 0 24 30" xmlns="http://www.w3.org/2000/svg"><title>cursor</title><g fill="none" fill-rule="evenodd"><path d="M24 12.105c0 6.686-5.74 11.58-12 17.895C5.74 23.684 0 18.79 0 12.105 0 5.42 5.373 0 12 0s12 5.42 12 12.105z" fill="#1F78D1" fill-rule="nonzero"/><path d="M15.28 25.249c1.458-1.475 2.539-2.635 3.474-3.747 2.851-3.394 4.203-6.265 4.203-9.397 0-6.111-4.908-11.062-10.957-11.062-6.05 0-10.957 4.951-10.957 11.062 0 3.132 1.352 6.003 4.203 9.397.935 1.112 2.016 2.272 3.474 3.747.511.517 2.216 2.213 3.28 3.275 1.064-1.062 2.769-2.758 3.28-3.275z" fill="#FFF"/><path d="M14.551 8.256A6.874 6.874 0 0 0 12 7.787c-.91 0-1.763.156-2.558.469-.79.308-1.42.725-1.888 1.252-.465.527-.697 1.096-.697 1.708 0 .5.159.977.476 1.433.321.45.772.841 1.352 1.172l.583.334-.181.643c-.107.407-.263.79-.469 1.152a6.604 6.604 0 0 0 1.842-1.145l.288-.254.381.04c.309.035.599.053.871.053.91 0 1.761-.154 2.551-.462.795-.312 1.424-.732 1.889-1.259.468-.526.703-1.096.703-1.707 0-.612-.235-1.181-.703-1.708-.465-.527-1.094-.944-1.889-1.252zm2.645.81c.536.656.804 1.373.804 2.15 0 .776-.268 1.495-.804 2.156-.535.656-1.263 1.176-2.183 1.56-.92.38-1.924.57-3.013.57a9.16 9.16 0 0 1-.971-.054 7.32 7.32 0 0 1-3.08 1.62 5.044 5.044 0 0 1-.764.148h-.033a.26.26 0 0 1-.181-.074.324.324 0 0 1-.107-.18v-.007c-.014-.018-.016-.045-.007-.08.014-.037.018-.059.014-.068 0-.009.01-.031.033-.067a.645.645 0 0 0 .04-.06 1.73 1.73 0 0 0 .047-.054l.054-.06a53.034 53.034 0 0 1 .435-.489c.049-.049.118-.136.207-.26.094-.126.168-.24.221-.342.054-.103.114-.235.181-.395.067-.161.125-.33.174-.51-.7-.397-1.254-.888-1.66-1.473A3.261 3.261 0 0 1 6 11.216c0-.777.268-1.494.804-2.15.535-.66 1.263-1.18 2.183-1.56.92-.384 1.924-.576 3.013-.576 1.09 0 2.094.192 3.013.576.92.38 1.648.9 2.183 1.56z" fill="#1F78D1" fill-rule="nonzero"/></g></svg> diff --git a/app/assets/images/icon_image_comment@2x.svg b/app/assets/images/icon_image_comment@2x.svg deleted file mode 100644 index 83be91d3705..00000000000 --- a/app/assets/images/icon_image_comment@2x.svg +++ /dev/null @@ -1 +0,0 @@ -<svg width="48" height="60" viewBox="0 0 48 60" xmlns="http://www.w3.org/2000/svg"><title>cursor_2x</title><g fill="none" fill-rule="evenodd"><path d="M48 24.21C48 37.583 36.522 47.369 24 60 11.478 47.368 0 37.582 0 24.21 0 10.84 10.745 0 24 0s24 10.84 24 24.21z" fill="#1F78D1" fill-rule="nonzero"/><path d="M30.56 50.497c2.915-2.95 5.078-5.268 6.947-7.493 5.703-6.788 8.406-12.53 8.406-18.793 0-12.223-9.815-22.124-21.913-22.124S2.087 11.988 2.087 24.211c0 6.263 2.703 12.005 8.406 18.793 1.87 2.225 4.032 4.544 6.947 7.493 1.022 1.035 4.432 4.426 6.56 6.55 2.128-2.124 5.538-5.515 6.56-6.55z" fill="#FFF"/><path d="M29.103 16.512c-1.58-.625-3.282-.938-5.103-.938-1.821 0-3.527.313-5.116.938-1.58.616-2.84 1.45-3.777 2.504-.928 1.054-1.393 2.192-1.393 3.415 0 1 .317 1.956.951 2.866.643.902 1.545 1.684 2.706 2.344l1.165.67-.362 1.286a9.603 9.603 0 0 1-.937 2.303 13.208 13.208 0 0 0 3.683-2.29l.576-.509.763.08c.616.072 1.196.108 1.741.108 1.821 0 3.522-.308 5.103-.925 1.589-.625 2.848-1.464 3.776-2.517.938-1.054 1.407-2.192 1.407-3.416 0-1.223-.469-2.361-1.407-3.415-.928-1.053-2.187-1.888-3.776-2.504zm5.29 1.62c1.071 1.313 1.607 2.746 1.607 4.3 0 1.553-.536 2.99-1.607 4.312-1.072 1.312-2.527 2.353-4.366 3.12-1.84.76-3.848 1.139-6.027 1.139a18.32 18.32 0 0 1-1.942-.107c-1.768 1.562-3.821 2.643-6.16 3.24-.438.126-.947.224-1.527.295h-.067a.521.521 0 0 1-.362-.147.649.649 0 0 1-.214-.362v-.013c-.027-.036-.032-.09-.014-.16.027-.072.036-.117.027-.135 0-.017.022-.062.067-.133a1.29 1.29 0 0 0 .08-.121c.01-.009.04-.045.094-.107a106.068 106.068 0 0 1 .522-.59c.215-.232.367-.401.456-.508.098-.099.236-.273.415-.523.188-.25.335-.477.442-.683.107-.205.228-.468.362-.79.134-.321.25-.66.348-1.018-1.402-.794-2.51-1.777-3.322-2.946C12.402 25.025 12 23.77 12 22.43c0-1.553.536-2.986 1.607-4.299 1.072-1.321 2.527-2.361 4.366-3.12 1.84-.768 3.848-1.152 6.027-1.152 2.179 0 4.188.384 6.027 1.152 1.84.759 3.294 1.799 4.366 3.12z" fill="#1F78D1" fill-rule="nonzero"/></g></svg> diff --git a/app/assets/images/icons.json b/app/assets/images/icons.json index 4e967936ce0..68d6528758b 100644 --- a/app/assets/images/icons.json +++ b/app/assets/images/icons.json @@ -1 +1 @@ -{"iconCount":180,"spriteSize":82176,"icons":["abuse","account","admin","angle-double-left","angle-double-right","angle-down","angle-left","angle-right","angle-up","appearance","applications","approval","arrow-down","arrow-right","assignee","bold","book","branch","bullhorn","calendar","cancel","chart","chevron-down","chevron-left","chevron-right","chevron-up","clock","close","code","collapse","comment-dots","comment-next","comment","comments","commit","credit-card","cut","dashboard","disk","doc_code","doc_image","doc_text","double-headed-arrow","download","duplicate","earth","ellipsis_v","emoji_slightly_smiling_face","emoji_smile","emoji_smiley","epic","external-link","eye-slash","eye","file-addition","file-deletion","file-modified","filter","folder","fork","geo-nodes","git-merge","group","history","home","hook","hourglass","image-comment-dark","import","issue-block","issue-child","issue-close","issue-duplicate","issue-new","issue-open-m","issue-open","issue-parent","issues","italic","key-2","key","label","labels","leave","level-up","license","link","list-bulleted","list-numbered","location-dot","location","lock-open","lock","log","mail","menu","merge-request-close","messages","mobile-issue-close","monitor","more","notifications-off","notifications","overview","pencil-square","pencil","pipeline","play","plus-square-o","plus-square","plus","preferences","profile","project","push-rules","question-o","question","quote","redo","remove","repeat","retry","scale","screen-full","screen-normal","scroll_down","scroll_up","search","settings","shield","slight-frown","slight-smile","smile","smiley","snippet","spam","spinner","star-o","star","status_canceled_borderless","status_canceled","status_closed","status_created_borderless","status_created","status_failed_borderless","status_failed","status_manual_borderless","status_manual","status_notfound_borderless","status_open","status_pending_borderless","status_pending","status_running_borderless","status_running","status_skipped_borderless","status_skipped","status_success_borderless","status_success_solid","status_success","status_warning_borderless","status_warning","stop","task-done","template","terminal","thumb-down","thumb-up","thumbtack","timer","todo-add","todo-done","token","unapproval","unassignee","unlink","user","users","volume-up","warning","work"]}
\ No newline at end of file +{"iconCount":181,"spriteSize":81482,"icons":["abuse","account","admin","angle-double-left","angle-double-right","angle-down","angle-left","angle-right","angle-up","appearance","applications","approval","arrow-down","arrow-right","assignee","bold","book","branch","bullhorn","calendar","cancel","chart","chevron-down","chevron-left","chevron-right","chevron-up","clock","close","code","collapse","comment-dots","comment-next","comment","comments","commit","credit-card","cut","dashboard","disk","doc_code","doc_image","doc_text","double-headed-arrow","download","duplicate","earth","ellipsis_v","emoji_slightly_smiling_face","emoji_smile","emoji_smiley","epic","external-link","eye-slash","eye","file-addition","file-deletion","file-modified","filter","folder","fork","geo-nodes","git-merge","group","history","home","hook","hourglass","image-comment-dark","image-comment-light","import","issue-block","issue-child","issue-close","issue-duplicate","issue-new","issue-open-m","issue-open","issue-parent","issues","italic","key-2","key","label","labels","leave","level-up","license","link","list-bulleted","list-numbered","location-dot","location","lock-open","lock","log","mail","menu","merge-request-close","messages","mobile-issue-close","monitor","more","notifications-off","notifications","overview","pencil-square","pencil","pipeline","play","plus-square-o","plus-square","plus","preferences","profile","project","push-rules","question-o","question","quote","redo","remove","repeat","retry","scale","screen-full","screen-normal","scroll_down","scroll_up","search","settings","shield","slight-frown","slight-smile","smile","smiley","snippet","spam","spinner","star-o","star","status_canceled_borderless","status_canceled","status_closed","status_created_borderless","status_created","status_failed_borderless","status_failed","status_manual_borderless","status_manual","status_notfound_borderless","status_open","status_pending_borderless","status_pending","status_running_borderless","status_running","status_skipped_borderless","status_skipped","status_success_borderless","status_success_solid","status_success","status_warning_borderless","status_warning","stop","task-done","template","terminal","thumb-down","thumb-up","thumbtack","timer","todo-add","todo-done","token","unapproval","unassignee","unlink","user","users","volume-up","warning","work"]}
\ No newline at end of file diff --git a/app/assets/images/icons.svg b/app/assets/images/icons.svg index 77ce6b2d89f..fd8f7862911 100644 --- a/app/assets/images/icons.svg +++ b/app/assets/images/icons.svg @@ -1 +1 @@ -<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><symbol viewBox="0 0 16 16" id="abuse" xmlns="http://www.w3.org/2000/svg"><path d="M11.408.328l4.029 3.222A1.5 1.5 0 0 1 16 4.72v6.555a1.5 1.5 0 0 1-.563 1.171l-4.026 3.224a1.5 1.5 0 0 1-.937.329H5.529a1.5 1.5 0 0 1-.937-.328L.563 12.45A1.5 1.5 0 0 1 0 11.28V4.724a1.5 1.5 0 0 1 .563-1.171L4.589.329A1.5 1.5 0 0 1 5.526 0h4.945c.34 0 .67.116.937.328zM10.296 2H5.702L2 4.964v6.074L5.704 14h4.594L14 11.036V4.962L10.296 2zM8 4a1 1 0 0 1 1 1v3a1 1 0 1 1-2 0V5a1 1 0 0 1 1-1zm0 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="account" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M9.195 9.965l-.568-.875a.25.25 0 0 1 .015-.294l.405-.5a.25.25 0 0 1 .283-.075l.938.36c.257-.183.543-.325.851-.42l.322-.988A.25.25 0 0 1 11.679 7h.642a.25.25 0 0 1 .238.173l.322.988c.308.095.594.237.851.42l.938-.36a.25.25 0 0 1 .283.076l.405.5a.25.25 0 0 1 .015.293l-.568.875c.113.297.18.616.193.95l.898.54a.25.25 0 0 1 .115.27l-.144.626a.25.25 0 0 1-.222.193l-1.115.098a3.015 3.015 0 0 1-.512.608l.165 1.18a.25.25 0 0 1-.138.259l-.577.281a.25.25 0 0 1-.29-.05l-.874-.905a3.035 3.035 0 0 1-.608 0l-.875.904a.25.25 0 0 1-.289.051l-.577-.281a.25.25 0 0 1-.138-.26l.165-1.18a3.015 3.015 0 0 1-.512-.607l-1.115-.098a.25.25 0 0 1-.222-.193l-.144-.626a.25.25 0 0 1 .115-.27l.898-.54c.013-.334.08-.653.193-.95zM6.789 8.023A12.845 12.845 0 0 0 6 8c-5.036 0-6 2.74-6 4.48C0 14.22.076 15 6 15c.553 0 1.055-.006 1.51-.02A5.977 5.977 0 0 1 6 11c0-1.083.287-2.1.79-2.977zM5.976 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6zM12 12a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="admin" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M13.162 2.5a3.5 3.5 0 0 1-3.163 5.479L6.08 14.766a1.5 1.5 0 0 1-2.598-1.5L7.4 6.479A3.5 3.5 0 0 1 10.564 1L8.9 3.88l2.599 1.5 1.663-2.88zm-8.63 11.949a.5.5 0 1 0 .5-.866.5.5 0 0 0-.5.866z"/></symbol><symbol viewBox="0 0 16 16" id="angle-double-left" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10.414 7.95l4.243-4.243a1 1 0 0 0-1.414-1.414l-4.95 4.95a.997.997 0 0 0 0 1.414l4.95 4.95a1 1 0 1 0 1.414-1.415L10.414 7.95zm-7 0l4.243-4.243a1 1 0 0 0-1.414-1.414l-4.95 4.95a.997.997 0 0 0 0 1.414l4.95 4.95a1 1 0 0 0 1.414-1.415L3.414 7.95z"/></symbol><symbol viewBox="0 0 16 16" id="angle-double-right" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M5.536 7.95L1.293 3.707a1 1 0 0 1 1.414-1.414l4.95 4.95a.997.997 0 0 1 0 1.414l-4.95 4.95a1 1 0 1 1-1.414-1.415L5.536 7.95zm7 0L8.293 3.707a1 1 0 0 1 1.414-1.414l4.95 4.95a.997.997 0 0 1 0 1.414l-4.95 4.95a1 1 0 0 1-1.414-1.415l4.243-4.242z"/></symbol><symbol viewBox="0 0 16 16" id="angle-down" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8 10.243l-4.95-4.95a1 1 0 0 0-1.414 1.414l5.657 5.657a.997.997 0 0 0 1.414 0l5.657-5.657a1 1 0 0 0-1.414-1.414L8 10.243z"/></symbol><symbol viewBox="0 0 16 16" id="angle-left" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M5.757 8l4.95-4.95a1 1 0 1 0-1.414-1.414L3.636 7.293a.997.997 0 0 0 0 1.414l5.657 5.657a1 1 0 0 0 1.414-1.414L5.757 8z"/></symbol><symbol viewBox="0 0 16 16" id="angle-right" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10.243 8l-4.95-4.95a1 1 0 0 1 1.414-1.414l5.657 5.657a.997.997 0 0 1 0 1.414l-5.657 5.657a1 1 0 0 1-1.414-1.414L10.243 8z"/></symbol><symbol viewBox="0 0 16 16" id="angle-up" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8 6.757l-4.95 4.95a1 1 0 1 1-1.414-1.414l5.657-5.657a.997.997 0 0 1 1.414 0l5.657 5.657a1 1 0 0 1-1.414 1.414L8 6.757z"/></symbol><symbol viewBox="0 0 16 16" id="appearance" xmlns="http://www.w3.org/2000/svg"><path d="M11.161 12.456l.232.121c.1.053.175.094.249.137.53.318.844.75.857 1.402.012 1.397-1.116 1.756-3.12 1.858a23.85 23.85 0 0 1-1.38.026A8 8 0 0 1 0 8a8 8 0 0 1 8-8c4.417 0 7.998 3.582 7.998 7.977.06 2.621-1.312 3.586-4.48 3.648-.602.008-1.068.043-1.4.104.228.192.598.47 1.043.727zm-3.287-.943c-.019-1.495 1.228-1.856 3.611-1.888C13.67 9.582 14.028 9.33 13.998 8A6 6 0 1 0 8 14c.603 0 .91-.004 1.277-.023a9.7 9.7 0 0 0 .478-.035c-1.172-.738-1.868-1.47-1.88-2.43zM6 5a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm6 3a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm-2-3a1 1 0 1 1 0-2 1 1 0 0 1 0 2zM4 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="applications" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M1 0h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H1a1 1 0 0 1-1-1V1a1 1 0 0 1 1-1zm0 6h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H1a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1zm6-6h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1V1a1 1 0 0 1 1-1zm0 1v2h2V1H7zm0 5h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1zm6-6h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1h-2a1 1 0 0 1-1-1V1a1 1 0 0 1 1-1zm0 6h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1h-2a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1zm0 1v2h2V7h-2zM1 12h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H1a1 1 0 0 1-1-1v-2a1 1 0 0 1 1-1zm0 1v2h2v-2H1zm6-1h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1v-2a1 1 0 0 1 1-1zm6 0h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1h-2a1 1 0 0 1-1-1v-2a1 1 0 0 1 1-1z"/></symbol><symbol viewBox="0 0 16 16" id="approval" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10.536 10.657l2.828-2.829a1 1 0 0 1 1.414 1.415l-3.535 3.535a.997.997 0 0 1-1.415 0l-2.12-2.121A1 1 0 1 1 9.12 9.243l1.415 1.414zM7.632 8.109A2 2 0 0 0 7 11.364l2.121 2.121a1.996 1.996 0 0 0 2.807.021C11.686 14.554 10.627 15 6 15c-5.924 0-6-.78-6-2.52S.964 8 6 8c.6 0 1.142.038 1.632.109zM5.976 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6z"/></symbol><symbol viewBox="0 0 16 16" id="arrow-down" xmlns="http://www.w3.org/2000/svg"><path d="M10.472 7.282a.862.862 0 0 1 1.26-.006c.357.364.357.958 0 1.285L8.627 11.73A.886.886 0 0 1 8 12a.849.849 0 0 1-.627-.27L4.275 8.561a.904.904 0 0 1-.013-1.285.861.861 0 0 1 1.26-.007l2.486 2.527z"/></symbol><symbol viewBox="0 0 16 16" id="arrow-right" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M9 6H2a2 2 0 1 0 0 4h7v2.586a1 1 0 0 0 1.707.707l4.586-4.586a1 1 0 0 0 0-1.414l-4.586-4.586A1 1 0 0 0 9 3.414V6z"/></symbol><symbol viewBox="0 0 16 16" id="assignee" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M12 5V4a1 1 0 0 1 2 0v1h1a1 1 0 0 1 0 2h-1v1a1 1 0 0 1-2 0V7h-1a1 1 0 0 1 0-2h1zM5.976 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6zM6 15c-5.924 0-6-.78-6-2.52S.964 8 6 8s6 2.692 6 4.48c0 1.788-.076 2.52-6 2.52z"/></symbol><symbol viewBox="0 0 16 16" id="bold" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M4 12.5v-9A1.5 1.5 0 0 1 5.5 2h2.104c2.182 0 3.879.681 3.879 2.982 0 1.067-.517 2.227-1.374 2.595v.073C11.176 7.963 12 8.865 12 10.466 12 12.914 10.19 14 7.911 14H5.5A1.5 1.5 0 0 1 4 12.5zm2.376-5.696H7.49c1.164 0 1.665-.552 1.665-1.417 0-.94-.534-1.289-1.649-1.289h-1.13v2.706zm0 5.098h1.341c1.293 0 1.956-.515 1.956-1.62 0-1.049-.647-1.472-1.956-1.472H6.376v3.092z"/></symbol><symbol viewBox="0 0 16 16" id="book" xmlns="http://www.w3.org/2000/svg"><path d="M7 2H5a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2v4.191a.5.5 0 0 1-.724.447l-1.052-.526a.5.5 0 0 0-.448 0l-1.052.526A.5.5 0 0 1 7 6.191V2zM5 0h6a4 4 0 0 1 4 4v8a4 4 0 0 1-4 4H5a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4z"/></symbol><symbol viewBox="0 0 16 16" id="branch" xmlns="http://www.w3.org/2000/svg"><path d="M6 11.978v.29a2 2 0 1 1-2 0V3.732a2 2 0 1 1 2 0v3.849c.592-.491 1.31-.854 2.15-1.081 1.308-.353 1.875-.882 1.893-1.743a2 2 0 1 1 2.002-.051C12.053 6.54 10.857 7.84 8.67 8.43 7.056 8.867 6.195 9.98 6 11.978zM5 3a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm6 1a1 1 0 1 0 0-2 1 1 0 0 0 0 2zM5 15a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="bullhorn" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M6.143 10H7V4H3a3 3 0 1 0 0 6h.143l.734 5.141a1 1 0 0 0 .99.859h1.556a.5.5 0 0 0 .495-.57L6.143 10zM8 4c1.034.02 2.039-.274 3.014-.883.727-.455 1.836-1.334 3.328-2.637A1 1 0 0 1 16 1.233v10.764a1 1 0 0 1-1.595.803c-1.658-1.227-2.788-1.992-3.392-2.294-.781-.39-1.785-.559-3.013-.506V4z"/></symbol><symbol viewBox="0 0 16 16" id="calendar" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M12 2h2a2 2 0 0 1 2 2H0a2 2 0 0 1 2-2h2V1a1 1 0 1 1 2 0v1h4V1a1 1 0 1 1 2 0v1zM0 4h16v9a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V4zm2 2.5V13a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V6.5a.5.5 0 0 0-.5-.5h-11a.5.5 0 0 0-.5.5zM5 8h2a1 1 0 1 1 0 2H5a1 1 0 1 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="cancel" xmlns="http://www.w3.org/2000/svg"><path d="M3.11 4.523a6 6 0 0 0 8.367 8.367L3.109 4.524zM4.522 3.11l8.368 8.368A6 6 0 0 0 4.524 3.11zM8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16z"/></symbol><symbol viewBox="0 0 16 16" id="chart" xmlns="http://www.w3.org/2000/svg"><path d="M15 14a1 1 0 0 1 0 2H2a2 2 0 0 1-2-2V1a1 1 0 1 1 2 0v13h13zM3.142 8.735l2.502-2.561a.5.5 0 0 1 .714-.003L8 7.833l3.592-4.553a.5.5 0 0 1 .796.015l2.516 3.454a.5.5 0 0 1 .096.295V12.5a.5.5 0 0 1-.5.5h-11a.5.5 0 0 1-.5-.5V9.085a.5.5 0 0 1 .142-.35z"/></symbol><symbol viewBox="0 0 16 16" id="chevron-down" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8.078 8.2l3.535-3.536a2 2 0 0 1 2.828 2.828l-4.949 4.95c-.39.39-.902.586-1.414.586a1.994 1.994 0 0 1-1.414-.586l-4.95-4.95a2 2 0 1 1 2.828-2.828l3.536 3.535z"/></symbol><symbol viewBox="0 0 16 16" id="chevron-left" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M7.977 7.998l3.535-3.535a2 2 0 1 0-2.828-2.828l-4.95 4.949c-.39.39-.586.902-.586 1.414 0 .512.196 1.024.586 1.414l4.95 4.95a2 2 0 1 0 2.828-2.828L7.977 7.998z"/></symbol><symbol viewBox="0 0 16 16" id="chevron-right" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8.22 7.998L4.683 4.463a2 2 0 0 1 2.828-2.828l4.95 4.949c.39.39.586.902.586 1.414a1.99 1.99 0 0 1-.586 1.414l-4.95 4.95a2 2 0 0 1-2.828-2.828l3.535-3.536z"/></symbol><symbol viewBox="0 0 16 16" id="chevron-up" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M7.778 8.957l3.535 3.535a2 2 0 1 0 2.828-2.828l-4.949-4.95a1.994 1.994 0 0 0-1.414-.586c-.512 0-1.024.196-1.414.586l-4.95 4.95a2 2 0 1 0 2.828 2.828l3.536-3.535z"/></symbol><symbol viewBox="0 0 16 16" id="clock" xmlns="http://www.w3.org/2000/svg"><path d="M9 7h1a1 1 0 0 1 0 2H8a.997.997 0 0 1-1-1V5a1 1 0 1 1 2 0v2zm-1 9A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12z"/></symbol><symbol viewBox="0 0 16 16" id="close" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M9.414 8l4.95-4.95a1 1 0 0 0-1.414-1.414L8 6.586l-4.95-4.95A1 1 0 0 0 1.636 3.05L6.586 8l-4.95 4.95a1 1 0 1 0 1.414 1.414L8 9.414l4.95 4.95a1 1 0 1 0 1.414-1.414L9.414 8z"/></symbol><symbol viewBox="0 0 16 16" id="code" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M15.871 8.243a.997.997 0 0 0-.293-.707L12.75 4.707a1 1 0 0 0-1.414 1.414l2.12 2.122-2.12 2.121a1 1 0 0 0 1.414 1.414l2.828-2.828a.997.997 0 0 0 .293-.707zm-13.243 0L4.75 6.12a1 1 0 1 0-1.414-1.414L.507 7.536a.997.997 0 0 0 0 1.414l2.829 2.828a1 1 0 1 0 1.414-1.414L2.628 8.243zm6.407-4.107a1 1 0 0 1 .707 1.225L8.19 11.157a1 1 0 1 1-1.931-.518L7.81 4.843a1 1 0 0 1 1.224-.707z"/></symbol><symbol viewBox="0 0 9 13" id="collapse"><path d="M.084.25C.01.18-.015.12.008.071.031.024.093 0 .194 0h8.521c.1 0 .162.024.185.072.023.048-.002.107-.075.177l-4.11 3.935a.372.372 0 0 1-.11.072h-.301a.508.508 0 0 1-.11-.072L.084.249zM.377 6.88a.364.364 0 0 1-.26-.105.334.334 0 0 1-.11-.25v-.709c0-.096.036-.179.11-.249a.364.364 0 0 1 .26-.105h8.15c.101 0 .188.035.261.105.074.07.11.153.11.25v.709c0 .096-.036.179-.11.249a.364.364 0 0 1-.26.105H.377zM.084 12.132c-.074.07-.099.129-.076.177.023.048.085.072.186.072h8.521c.1 0 .162-.024.185-.072.023-.048-.002-.107-.075-.177l-4.11-3.935a.372.372 0 0 0-.11-.072h-.301a.508.508 0 0 0-.11.072l-4.11 3.935z"/></symbol><symbol viewBox="0 0 16 16" id="comment" xmlns="http://www.w3.org/2000/svg"><path d="M1.707 15.707C1.077 16.337 0 15.891 0 15V3a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H5.414l-3.707 3.707zM2 12.586l2.293-2.293A1 1 0 0 1 5 10h8a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v9.586z"/></symbol><symbol viewBox="0 0 16 16" id="comment-dots" xmlns="http://www.w3.org/2000/svg"><path d="M1.707 15.707C1.077 16.337 0 15.891 0 15V3a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H5.414l-3.707 3.707zM2 12.586l2.293-2.293A1 1 0 0 1 5 10h8a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v9.586zM5 7a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm3 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm3 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="comment-next" xmlns="http://www.w3.org/2000/svg"><path d="M8 5V4a.5.5 0 0 1 .8-.4l2.667 2a.5.5 0 0 1 0 .8L8.8 8.4A.5.5 0 0 1 8 8V7H6a1 1 0 1 1 0-2h2zM1.707 15.707C1.077 16.337 0 15.891 0 15V3a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H5.414l-3.707 3.707zM2 12.586l2.293-2.293A1 1 0 0 1 5 10h8a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v9.586z"/></symbol><symbol viewBox="0 0 16 16" id="comments" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M3.75 10L0 13V3a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2H3.75zM13 5h1a2 2 0 0 1 2 2v8l-2.667-2H8a2 2 0 0 1-2-2h4a3 3 0 0 0 3-3V5z"/></symbol><symbol viewBox="0 0 16 16" id="commit" xmlns="http://www.w3.org/2000/svg"><path d="M8 10a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm3.876-1.008a4.002 4.002 0 0 1-7.752 0A1.01 1.01 0 0 1 4 9H1a1 1 0 1 1 0-2h3c.042 0 .083.003.124.008a4.002 4.002 0 0 1 7.752 0A1.01 1.01 0 0 1 12 7h3a1 1 0 0 1 0 2h-3a1.01 1.01 0 0 1-.124-.008z"/></symbol><symbol viewBox="0 0 16 16" id="credit-card" xmlns="http://www.w3.org/2000/svg"><path d="M14 5a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1h12zm0 3H2v3a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V8zM3 2h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3zm6.5 8h3a.5.5 0 1 1 0 1h-3a.5.5 0 1 1 0-1z"/></symbol><symbol viewBox="0 0 16 16" id="cut" xmlns="http://www.w3.org/2000/svg"><rect width="16" height="2" y="7" fill-rule="evenodd" rx="1"/></symbol><symbol viewBox="0 0 16 16" id="dashboard" xmlns="http://www.w3.org/2000/svg"><path d="M7.709 10.021l.696-2.6a.5.5 0 0 1 .966.26l-.657 2.45A2 2 0 0 1 10 12H6a2 2 0 0 1 1.709-1.979zM0 8.9a8 8 0 0 1 15.998 0H16v3.6a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5V8.9zM14 9A6 6 0 1 0 2 9v3.5a.5.5 0 0 0 .5.5h11a.5.5 0 0 0 .5-.5V9zM3.5 9a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm9 0a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm-7-3a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm5 0a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1z"/></symbol><symbol viewBox="0 0 16 16" id="disk" xmlns="http://www.w3.org/2000/svg"><path d="M16 11.764V3a3 3 0 0 0-3-3H3a3 3 0 0 0-3 3v8.764A2.989 2.989 0 0 1 2 11V3a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v8c.768 0 1.47.289 2 .764zM2 12h12a2 2 0 1 1 0 4H2a2 2 0 1 1 0-4zm10 1a1 1 0 1 0 0 2 1 1 0 0 0 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="doc_code" xmlns="http://www.w3.org/2000/svg"><path d="M8 2H5a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V7h-3a2 2 0 0 1-2-2V2zm2 .414V5h2.586L10 2.414zm1.036 7.607a.498.498 0 0 1-.147.354l-1.414 1.414a.5.5 0 0 1-.707-.707l1.06-1.06-1.06-1.061a.5.5 0 0 1 .707-.707l1.414 1.414a.498.498 0 0 1 .147.353zm-4.822 0l1.06 1.061a.5.5 0 0 1-.706.707l-1.414-1.414a.498.498 0 0 1 0-.707l1.414-1.414a.5.5 0 1 1 .707.707l-1.06 1.06zM5 0h4.586A2 2 0 0 1 11 .586L14.414 4A2 2 0 0 1 15 5.414V12a4 4 0 0 1-4 4H5a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4z"/></symbol><symbol viewBox="0 0 16 16" id="doc_image" xmlns="http://www.w3.org/2000/svg"><path d="M8 2H5a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V7h-3a2 2 0 0 1-2-2V2zm2 .414V5h2.586L10 2.414zM7.333 9.667l1.313-1.313a.5.5 0 0 1 .708 0L12 11H4l2.188-1.75a.5.5 0 0 1 .624 0l.521.417zM5 0h4.586A2 2 0 0 1 11 .586L14.414 4A2 2 0 0 1 15 5.414V12a4 4 0 0 1-4 4H5a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4zm.5 8a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zM4 11h8v.7a.3.3 0 0 1-.3.3H4.3a.3.3 0 0 1-.3-.3V11z"/></symbol><symbol viewBox="0 0 16 16" id="doc_text" xmlns="http://www.w3.org/2000/svg"><path d="M8 2H5a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V7h-3a2 2 0 0 1-2-2V2zm2 .414V5h2.586L10 2.414zM5 0h4.586A2 2 0 0 1 11 .586L14.414 4A2 2 0 0 1 15 5.414V12a4 4 0 0 1-4 4H5a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4zm.5 11h5a.5.5 0 1 1 0 1h-5a.5.5 0 1 1 0-1zm0-2h5a.5.5 0 1 1 0 1h-5a.5.5 0 0 1 0-1zm0-2h2a.5.5 0 0 1 0 1h-2a.5.5 0 0 1 0-1z"/></symbol><symbol viewBox="0 0 105 26" id="double-headed-arrow" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M1.018 11.089L15.138.614c1.23-.911 3.086-.795 4.147.26.461.46.715 1.045.715 1.651v20.95C20 24.869 18.684 26 17.06 26a3.238 3.238 0 0 1-1.921-.614L1.019 14.911C-.212 14-.347 12.405.714 11.35c.094-.094.195-.18.303-.261zm102.964 0c.108.08.21.167.303.26 1.061 1.056.925 2.65-.303 3.562l-14.12 10.475A3.238 3.238 0 0 1 87.94 26C86.316 26 85 24.87 85 23.475V2.525c0-.606.254-1.192.715-1.65 1.061-1.056 2.917-1.172 4.146-.26l14.12 10.474zM35 17a4 4 0 1 1 0-8 4 4 0 0 1 0 8zm18 0a4 4 0 1 1 0-8 4 4 0 0 1 0 8zm18 0a4 4 0 1 1 0-8 4 4 0 0 1 0 8z"/></symbol><symbol viewBox="0 0 16 16" id="download" xmlns="http://www.w3.org/2000/svg"><path d="M9 12h1a.5.5 0 0 1 .4.8l-2 2.667a.5.5 0 0 1-.8 0l-2-2.667A.5.5 0 0 1 6 12h1V8a1 1 0 1 1 2 0v4zM4 9a1 1 0 1 1 0 2 4 4 0 0 1-1.971-7.481 4 4 0 0 1 6.633-2.505 3.999 3.999 0 0 1 3.82 2.014A4 4 0 0 1 12 11a1 1 0 0 1 0-2 2 2 0 1 0 0-4h-1a2 2 0 0 0-3.112-1.662A2 2 0 1 0 4.268 5H4a2 2 0 1 0 0 4z"/></symbol><symbol viewBox="0 0 16 16" id="duplicate" xmlns="http://www.w3.org/2000/svg"><path d="M14 10h-3a1 1 0 0 1-1-1V6H8.527A.527.527 0 0 0 8 6.527V13a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1v-3zm-4-7H8.527c-.18 0-.355.013-.527.04V3a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h2v2H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3h4a3 3 0 0 1 3 3zM8.527 4h2.323a.5.5 0 0 1 .35.143l4.65 4.551a.5.5 0 0 1 .15.357V13a3 3 0 0 1-3 3H9a3 3 0 0 1-3-3V6.527A2.527 2.527 0 0 1 8.527 4z"/></symbol><symbol viewBox="0 0 16 16" id="earth" xmlns="http://www.w3.org/2000/svg"><path d="M8.7 2.04l-.082.177c.283.223.422.413.417.571-.008.237-.311.057-.444.274-.133.218.038.542-.112.637-.15.096-.398-.386-.479-.46-.054-.049-.166-.257-.336-.625l-.216-.225a.844.844 0 0 0-.418-.035c-.177.038-.075.1-.035.132.04.032.32.037.452.2.132.164.03.224-.05.298-.054.05-.157.062-.31.035H5.952l-.402.398.03.325.229.455.324-.463c.008-.206.058-.342.15-.41.14-.1.342-.15.534-.085.191.066-.057.218.011.271.068.053.204-.098.313-.02.11.08.07.155.104.322.036.167.254.114.398.328.144.215.19.29.147.483-.043.195-.168.26-.305.232-.138-.028-.107-.246-.275-.348-.168-.102-.266-.114-.386-.054-.12.06-.016.129.023.235.04.106.274.321.224.43-.05.107-.108.116-.42 0-.21-.077-.414-.007-.615.212l-.76.722c-.153.715-.3 1.13-.44 1.243-.211.17-.177-.483-.483-.656-.306-.174-.494-.047-.8-.07-.307-.023-.42.65-.38.873a.434.434 0 0 0 .221.321c.236-.141.39-.184.465-.128.11.084-.144.267-.074.425.07.158.314.069.386.283.073.213.084.48-.05.706-.135.227-.275.178-.4.053-.127-.126-.033-.375-.255-.704-.223-.329-.381-.337-.63-.787-.158-.287-.35-.743-.575-1.366a6 6 0 0 0 3.21 7.198l.001-.075c0-.577-.004-.944-.012-1.102-.011-.236-.95-.945-1.104-1.2-.154-.256-.34-.595-.355-.746-.016-.151.185-.232.344-.325.16-.093-.11-.367.028-.626.137-.258.395-.438.496-.356.101.081.058.228.267.333.209.104.077-.213.456-.178.38.035.143.201.252.216.11.016.113-.127.299-.143.186-.015.282.445.471.622.19.178.452.008.611.043.159.034.267.09.402.255.136.166-.03.352.073.557.103.205 1.07.22 1.433.255.364.034.371.011.371.324s-.166.314-.453.507c-.286.193-.166.462-.38.762-.212.3-.316.062-.622.14-.306.077-.413.382-.452.568-.039.186-.386.094-.877.232-.29.082-.429.144-.569.204a6.002 6.002 0 0 0 7.682-4.3c-.094-.384-.18-.63-.258-.74-.213-.297-.36.21-.924.49-.564.278-.57-.288-.81-.49-.16-.133-.212-.44-.158-.92-.005-.478.02-.828.077-1.049.057-.221.126-.543.207-.965.351-.373.606-.572.764-.595.237-.034.336.374.658.3a.315.315 0 0 0 .035-.01 5.993 5.993 0 0 0-.475-.824l-.309-.043a.646.646 0 0 0-.332-.117c-.205-.02-.025.128-.089.24-.064.112-.235.724-.437.685-.201-.039-.204-.374-.17-.668.036-.294-.077-.35-.2-.412-.124-.062-.325-.213-.556-.295-.232-.082-.123-.175-.093-.274.03-.1.208-.015.193-.058-.014-.044-.313-.135-.266-.167.03-.02.2-.02.506.003l.216-.012.293-.163a.58.58 0 0 0-.376-.22c-.233-.036-.513-.034-.73-.142-.205-.103-.458-.36-.643-.638A5.965 5.965 0 0 0 8.7 2.04zM8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16z"/></symbol><symbol viewBox="0 0 1600 1600" id="ellipsis_v" xmlns="http://www.w3.org/2000/svg"><path d="M1088 1248v192q0 40-28 68t-68 28H800q-40 0-68-28t-28-68v-192q0-40 28-68t68-28h192q40 0 68 28t28 68zm0-512v192q0 40-28 68t-68 28H800q-40 0-68-28t-28-68V736q0-40 28-68t68-28h192q40 0 68 28t28 68zm0-512v192q0 40-28 68t-68 28H800q-40 0-68-28t-28-68V224q0-40 28-68t68-28h192q40 0 68 28t28 68z"/></symbol><symbol viewBox="0 0 18 18" id="emoji_slightly_smiling_face" xmlns="http://www.w3.org/2000/svg"><path d="M13.29 11.098a4.328 4.328 0 0 1-1.618 2.285c-.79.578-1.68.867-2.672.867-.992 0-1.883-.29-2.672-.867a4.328 4.328 0 0 1-1.617-2.285.721.721 0 0 1 .047-.569.715.715 0 0 1 .445-.369.721.721 0 0 1 .568.047.715.715 0 0 1 .37.445 2.91 2.91 0 0 0 1.084 1.518A2.93 2.93 0 0 0 9 12.75a2.93 2.93 0 0 0 1.775-.58 2.913 2.913 0 0 0 1.084-1.518.711.711 0 0 1 .375-.445.737.737 0 0 1 .575-.047c.195.063.34.186.433.37.094.183.11.372.047.568zM7.5 6c0 .414-.146.768-.44 1.06A1.44 1.44 0 0 1 6 7.5a1.44 1.44 0 0 1-1.06-.44A1.445 1.445 0 0 1 4.5 6c0-.414.146-.768.44-1.06A1.44 1.44 0 0 1 6 4.5c.414 0 .768.146 1.06.44.294.292.44.646.44 1.06zm6 0c0 .414-.146.768-.44 1.06A1.44 1.44 0 0 1 12 7.5a1.44 1.44 0 0 1-1.06-.44A1.445 1.445 0 0 1 10.5 6c0-.414.146-.768.44-1.06A1.44 1.44 0 0 1 12 4.5c.414 0 .768.146 1.06.44.294.292.44.646.44 1.06zm3 3a7.29 7.29 0 0 0-.598-2.912 7.574 7.574 0 0 0-1.6-2.39 7.574 7.574 0 0 0-2.39-1.6A7.29 7.29 0 0 0 9 1.5a7.29 7.29 0 0 0-2.912.598 7.574 7.574 0 0 0-2.39 1.6 7.574 7.574 0 0 0-1.6 2.39A7.29 7.29 0 0 0 1.5 9c0 1.016.2 1.986.598 2.912a7.574 7.574 0 0 0 1.6 2.39 7.574 7.574 0 0 0 2.39 1.6A7.29 7.29 0 0 0 9 16.5a7.29 7.29 0 0 0 2.912-.598 7.574 7.574 0 0 0 2.39-1.6 7.574 7.574 0 0 0 1.6-2.39A7.29 7.29 0 0 0 16.5 9zM18 9a8.804 8.804 0 0 1-1.207 4.518 8.96 8.96 0 0 1-3.275 3.275A8.804 8.804 0 0 1 9 18a8.804 8.804 0 0 1-4.518-1.207 8.96 8.96 0 0 1-3.275-3.275A8.804 8.804 0 0 1 0 9c0-1.633.402-3.139 1.207-4.518a8.96 8.96 0 0 1 3.275-3.275A8.804 8.804 0 0 1 9 0c1.633 0 3.139.402 4.518 1.207a8.96 8.96 0 0 1 3.275 3.275A8.804 8.804 0 0 1 18 9z" fill-rule="evenodd"/></symbol><symbol viewBox="0 0 18 18" id="emoji_smile" xmlns="http://www.w3.org/2000/svg"><path d="M13.29 11.098a4.328 4.328 0 0 1-1.618 2.285c-.79.578-1.68.867-2.672.867-.992 0-1.883-.29-2.672-.867a4.328 4.328 0 0 1-1.617-2.285.721.721 0 0 1 .047-.569.715.715 0 0 1 .445-.369c.195-.062 7.41-.062 7.606 0 .195.063.34.186.433.37.094.183.11.372.047.568zM14 6.37c0 .398-.04.755-.513.755-.473 0-.498-.272-1.237-.272-.74 0-.74.215-1.165.215-.425 0-.585-.3-.585-.698 0-.397.17-.736.513-1.017.341-.281.754-.422 1.237-.422.483 0 .896.14 1.237.422.342.28.513.62.513 1.017zm-6.5 0c0 .398-.04.755-.513.755-.473 0-.498-.272-1.237-.272-.74 0-.74.215-1.165.215-.425 0-.585-.3-.585-.698 0-.397.17-.736.513-1.017.341-.281.754-.422 1.237-.422.483 0 .896.14 1.237.422.342.28.513.62.513 1.017zm9 2.63a7.29 7.29 0 0 0-.598-2.912 7.574 7.574 0 0 0-1.6-2.39 7.574 7.574 0 0 0-2.39-1.6A7.29 7.29 0 0 0 9 1.5a7.29 7.29 0 0 0-2.912.598 7.574 7.574 0 0 0-2.39 1.6 7.574 7.574 0 0 0-1.6 2.39A7.29 7.29 0 0 0 1.5 9c0 1.016.2 1.986.598 2.912a7.574 7.574 0 0 0 1.6 2.39 7.574 7.574 0 0 0 2.39 1.6A7.29 7.29 0 0 0 9 16.5a7.29 7.29 0 0 0 2.912-.598 7.574 7.574 0 0 0 2.39-1.6 7.574 7.574 0 0 0 1.6-2.39A7.29 7.29 0 0 0 16.5 9zM18 9a8.804 8.804 0 0 1-1.207 4.518 8.96 8.96 0 0 1-3.275 3.275A8.804 8.804 0 0 1 9 18a8.804 8.804 0 0 1-4.518-1.207 8.96 8.96 0 0 1-3.275-3.275A8.804 8.804 0 0 1 0 9c0-1.633.402-3.139 1.207-4.518a8.96 8.96 0 0 1 3.275-3.275A8.804 8.804 0 0 1 9 0c1.633 0 3.139.402 4.518 1.207a8.96 8.96 0 0 1 3.275 3.275A8.804 8.804 0 0 1 18 9z" fill-rule="evenodd"/></symbol><symbol viewBox="0 0 18 18" id="emoji_smiley" xmlns="http://www.w3.org/2000/svg"><path d="M13.29 11.098a4.328 4.328 0 0 1-1.618 2.285c-.79.578-1.68.867-2.672.867-.992 0-1.883-.29-2.672-.867a4.328 4.328 0 0 1-1.617-2.285.721.721 0 0 1 .047-.569.715.715 0 0 1 .445-.369c.195-.062 7.41-.062 7.606 0 .195.063.34.186.433.37.094.183.11.372.047.568h.001zM7.5 6c0 .414-.146.768-.44 1.06A1.44 1.44 0 0 1 6 7.5a1.44 1.44 0 0 1-1.06-.44A1.445 1.445 0 0 1 4.5 6c0-.414.146-.768.44-1.06A1.44 1.44 0 0 1 6 4.5c.414 0 .768.146 1.06.44.294.292.44.646.44 1.06zm6 0c0 .414-.146.768-.44 1.06A1.44 1.44 0 0 1 12 7.5a1.44 1.44 0 0 1-1.06-.44A1.445 1.445 0 0 1 10.5 6c0-.414.146-.768.44-1.06A1.44 1.44 0 0 1 12 4.5c.414 0 .768.146 1.06.44.294.292.44.646.44 1.06zm3 3a7.29 7.29 0 0 0-.598-2.912 7.574 7.574 0 0 0-1.6-2.39 7.574 7.574 0 0 0-2.39-1.6A7.29 7.29 0 0 0 9 1.5a7.29 7.29 0 0 0-2.912.598 7.574 7.574 0 0 0-2.39 1.6 7.574 7.574 0 0 0-1.6 2.39A7.29 7.29 0 0 0 1.5 9c0 1.016.2 1.986.598 2.912a7.574 7.574 0 0 0 1.6 2.39 7.574 7.574 0 0 0 2.39 1.6c.92.397 1.91.6 2.912.598a7.29 7.29 0 0 0 2.912-.598 7.574 7.574 0 0 0 2.39-1.6 7.574 7.574 0 0 0 1.6-2.39c.397-.92.6-1.91.598-2.912zM18 9a8.804 8.804 0 0 1-1.207 4.518 8.96 8.96 0 0 1-3.275 3.275A8.804 8.804 0 0 1 9 18a8.804 8.804 0 0 1-4.518-1.207 8.96 8.96 0 0 1-3.275-3.275A8.804 8.804 0 0 1 0 9c0-1.633.402-3.139 1.207-4.518a8.96 8.96 0 0 1 3.275-3.275A8.804 8.804 0 0 1 9 0c1.633 0 3.139.402 4.518 1.207a8.96 8.96 0 0 1 3.275 3.275A8.804 8.804 0 0 1 18 9z"/></symbol><symbol viewBox="0 0 16 16" id="epic" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M14.985 8.044l-.757 2.272a1 1 0 0 1-.949.684H1.618a1 1 0 0 1-.894-1.447l.318-.637A2 2 0 0 0 1.618 9h11.661a2 2 0 0 0 1.706-.956zm0 3l-.757 2.272a1 1 0 0 1-.949.684H1.618a1 1 0 0 1-.894-1.447l.318-.637a2 2 0 0 0 .576.084h11.661a2 2 0 0 0 1.706-.956zM3.618 2h10.995a1 1 0 0 1 .948 1.316l-1.333 4a1 1 0 0 1-.949.684H1.618a1 1 0 0 1-.894-1.447l2-4A1 1 0 0 1 3.618 2zm-.382 4h9.322l.667-2H4.236l-1 2z"/></symbol><symbol viewBox="0 0 16 16" id="external-link" xmlns="http://www.w3.org/2000/svg"><path d="M13.121 4.177l-4.95 4.95a1 1 0 1 1-1.414-1.414l4.95-4.95-1.386-1.386a.5.5 0 0 1 .299-.85l4.709-.524a.5.5 0 0 1 .552.552l-.523 4.71a.5.5 0 0 1-.851.297l-1.386-1.385zM12 8.884a1 1 0 0 1 2 0v4a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3v-8a3 3 0 0 1 3-3h4a1 1 0 1 1 0 2H3a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1v-4z"/></symbol><symbol viewBox="0 0 16 16" id="eye" xmlns="http://www.w3.org/2000/svg"><path d="M8 14C4.816 14 2.253 12.284.393 8.981a2 2 0 0 1 0-1.962C2.253 3.716 4.816 2 8 2s5.747 1.716 7.607 5.019a2 2 0 0 1 0 1.962C13.747 12.284 11.184 14 8 14zm0-2c2.41 0 4.338-1.29 5.864-4C12.338 5.29 10.411 4 8 4 5.59 4 3.662 5.29 2.136 8 3.662 10.71 5.589 12 8 12zm0-1a3 3 0 1 1 0-6 3 3 0 0 1 0 6zm1-3a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="eye-slash" xmlns="http://www.w3.org/2000/svg"><path d="M13.618 2.62L1.62 14.619a1 1 0 0 1-.985-1.668l1.525-1.526C1.516 10.742.926 9.927.393 8.981a2 2 0 0 1 0-1.962C2.253 3.716 4.816 2 8 2c1.074 0 2.076.195 3.006.58l.944-.944a1 1 0 0 1 1.668.985zM8.068 11a3 3 0 0 0 2.931-2.932l-2.931 2.931zm-3.02-2.462a3 3 0 0 1 3.49-3.49l.884-.884A6.044 6.044 0 0 0 8 4C5.59 4 3.662 5.29 2.136 8c.445.79.924 1.46 1.439 2.011l1.473-1.473zm.421 5.06l1.658-1.658c.283.04.575.06.873.06 2.41 0 4.338-1.29 5.864-4a11.023 11.023 0 0 0-1.133-1.664l1.418-1.418a12.799 12.799 0 0 1 1.458 2.1 2 2 0 0 1 0 1.963C13.747 12.284 11.184 14 8 14a7.883 7.883 0 0 1-2.53-.402z"/></symbol><symbol viewBox="0 0 16 16" id="file-addition" xmlns="http://www.w3.org/2000/svg"><path d="M7 7V5a1 1 0 1 1 2 0v2h2a1 1 0 0 1 0 2H9v2a1 1 0 0 1-2 0V9H5a1 1 0 1 1 0-2h2zM3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 1a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3a2 2 0 0 0-2-2H3z"/></symbol><symbol viewBox="0 0 16 16" id="file-deletion" xmlns="http://www.w3.org/2000/svg"><path d="M3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 1a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3a2 2 0 0 0-2-2H3zm2 6h6a1 1 0 0 1 0 2H5a1 1 0 1 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="file-modified" xmlns="http://www.w3.org/2000/svg"><path d="M3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 1a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3a2 2 0 0 0-2-2H3zm5 4a3 3 0 1 1 0 6 3 3 0 0 1 0-6z"/></symbol><symbol viewBox="0 0 16 16" id="filter" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10 6v9l-3.724-1.862A.5.5 0 0 1 6 12.691V6L1.854 1.854A.5.5 0 0 1 2.207 1h11.586a.5.5 0 0 1 .353.854L10 6z"/></symbol><symbol viewBox="0 0 16 16" id="folder" xmlns="http://www.w3.org/2000/svg"><path d="M7.228 5l-.475-1.335A1 1 0 0 0 5.81 3H2v9a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V6a1 1 0 0 0-1-1H7.228zM13 3a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a2 2 0 0 1 2-2h3.81a3 3 0 0 1 2.827 1.995L13 3z"/></symbol><symbol viewBox="0 0 16 16" id="fork" xmlns="http://www.w3.org/2000/svg"><path d="M9 12.268a2 2 0 1 1-2 0V8.874A4.002 4.002 0 0 1 4 5V3.732a2 2 0 1 1 2 0V5a2 2 0 1 0 4 0V3.732a2 2 0 1 1 2 0V5a4.002 4.002 0 0 1-3 3.874v3.394zM11 3a1 1 0 1 0 0-2 1 1 0 0 0 0 2zM5 3a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm3 12a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="geo-nodes" xmlns="http://www.w3.org/2000/svg"><path d="M9.7 13.1l-.2.2c-.7.8-2 .9-2.8.1-.1 0-.1-.1-.1-.1l-.2-.2c-2 .2-3.4.7-3.4 1.4 0 .8 2.2 1.5 5 1.5s5-.7 5-1.5c0-.7-1.4-1.2-3.3-1.4M7.3 12.7c.4.4 1 .3 1.4-.1C11.6 9.5 13 7 13 5.3 13 2.4 10.8 0 8 0S3 2.4 3 5.3C3 7 4.4 9.5 7.3 12.7M8 2c1.6 0 3 1.4 3 3.3 0 1-1 2.8-3 5.2-2-2.4-3-4.2-3-5.2C5 3.4 6.4 2 8 2"/><circle cx="8" cy="5" r="1"/></symbol><symbol viewBox="0 0 16 16" id="git-merge" xmlns="http://www.w3.org/2000/svg"><path d="M11 12.268V5a1 1 0 0 0-1-1v1a.5.5 0 0 1-.8.4l-2.667-2a.5.5 0 0 1 0-.8L9.2.6a.5.5 0 0 1 .8.4v1a3 3 0 0 1 3 3v7.268a2 2 0 1 1-2 0zm-6 0a2 2 0 1 1-2 0V4.732a2 2 0 1 1 2 0v7.536zM4 4a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm0 11a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm8 0a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="group" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M3.048 11.997C-.377 11.975.013 11.782.013 10.56.013 9.235.653 8 4 8c.444 0 .84.022 1.194.062.164.435.426.82.76 1.132-1.786.389-2.721 1.353-2.906 2.803zm2.94-7.222a2.993 2.993 0 0 0-.976 1.95 2 2 0 1 1 .975-1.95zm6.964 7.222c-.185-1.45-1.12-2.414-2.906-2.803.334-.311.596-.697.76-1.132C11.16 8.022 11.556 8 12 8c3.346 0 3.987 1.235 3.987 2.56 0 1.222.39 1.415-3.035 1.437zm-1.964-5.272a2.993 2.993 0 0 0-.976-1.95 2 2 0 1 1 .976 1.95zM8 9a2 2 0 1 1 0-4 2 2 0 0 1 0 4zm0 5c-2.177 0-3.987-.115-3.987-1.44S4.653 10 8 10c3.346 0 3.987 1.235 3.987 2.56S10.177 14 8 14z"/></symbol><symbol viewBox="0 0 16 16" id="history" xmlns="http://www.w3.org/2000/svg"><path d="M2.868 3.24a7 7 0 1 1-.043 9.475 1 1 0 0 1 1.478-1.348 5 5 0 1 0 .124-6.865l.796.645a.5.5 0 0 1-.193.873l-3.232.814a.5.5 0 0 1-.622-.504L1.3 3a.5.5 0 0 1 .814-.37l.754.61zM9 8h1a1 1 0 0 1 0 2H8a.997.997 0 0 1-1-1V6a1 1 0 1 1 2 0v2z"/></symbol><symbol viewBox="0 0 16 16" id="home" xmlns="http://www.w3.org/2000/svg"><path d="M8.462 2.177a.505.505 0 0 1-.038.044l.038-.044zm-.787 0l.038.043a.5.5 0 0 1-.038-.043zM3.706 7h8.725L8.069 2.585 3.706 7zM7 13.369V12a1 1 0 0 1 2 0v1.369h3V9H4v4.369h3zM14 9v4.836c0 .833-.657 1.533-1.5 1.533h-9c-.843 0-1.5-.7-1.5-1.533V9h-.448a1.1 1.1 0 0 1-.783-1.873L6.934.887a1.5 1.5 0 0 1 2.269 0l6.165 6.24A1.1 1.1 0 0 1 14.585 9H14z"/></symbol><symbol viewBox="0 0 16 16" id="hook" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10 3a1 1 0 0 0-1-1H7a1 1 0 0 0-1 1h4zm0 1H6v1a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1V4zM7 8a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3h2a3 3 0 0 1 3 3v2a3 3 0 0 1-3 3v4a2 2 0 1 0 4 0h-.44a.3.3 0 0 1-.25-.466l1.44-2.16a.3.3 0 0 1 .5 0l1.44 2.16a.3.3 0 0 1-.25.466H15a4 4 0 0 1-7 2.646A4 4 0 0 1 1 12H.56a.3.3 0 0 1-.25-.466l1.44-2.16a.3.3 0 0 1 .5 0l1.44 2.16a.3.3 0 0 1-.25.466H3a2 2 0 1 0 4 0V8z"/></symbol><symbol viewBox="0 0 16 16" id="hourglass" xmlns="http://www.w3.org/2000/svg"><path d="M10.331 4.889A2.988 2.988 0 0 0 11 3V2H5v1c0 .362.064.709.182 1.03l5.15.859zM3 14v-1c0-1.78.93-3.342 2.33-4.228.447-.327.67-.582.67-.764 0-.19-.242-.46-.725-.815A4.996 4.996 0 0 1 3 3V2H2a1 1 0 1 1 0-2h12a1 1 0 0 1 0 2h-1v1a4.997 4.997 0 0 1-2.39 4.266c-.407.3-.61.545-.61.734 0 .19.203.434.61.734A4.997 4.997 0 0 1 13 13v1h1a1 1 0 0 1 0 2H2a1 1 0 0 1 0-2h1zm8 0v-1a3 3 0 0 0-6 0v1h6z"/></symbol><symbol viewBox="0 0 24 30" id="image-comment-dark" xmlns="http://www.w3.org/2000/svg"><title>cursor_active</title><g fill="none" fill-rule="evenodd"><path d="M24 12.105c0 6.686-5.74 11.58-12 17.895C5.74 23.684 0 18.79 0 12.105 0 5.42 5.373 0 12 0s12 5.42 12 12.105z" fill="#FFF" fill-rule="nonzero"/><path d="M15.28 25.249c1.458-1.475 2.539-2.635 3.474-3.747 2.851-3.394 4.203-6.265 4.203-9.397 0-6.111-4.908-11.062-10.957-11.062-6.05 0-10.957 4.951-10.957 11.062 0 3.132 1.352 6.003 4.203 9.397.935 1.112 2.016 2.272 3.474 3.747.511.517 2.216 2.213 3.28 3.275 1.064-1.062 2.769-2.758 3.28-3.275z" fill="#1F78D1"/><path d="M14.551 8.256A6.874 6.874 0 0 0 12 7.787a6.92 6.92 0 0 0-2.558.469c-.79.308-1.42.725-1.888 1.252-.465.527-.697 1.096-.697 1.708 0 .5.159.977.476 1.433.321.45.772.841 1.352 1.172l.583.334-.181.643c-.107.407-.263.79-.469 1.152a6.604 6.604 0 0 0 1.842-1.145l.288-.254.381.04c.309.035.599.053.871.053.91 0 1.761-.154 2.551-.462.795-.312 1.424-.732 1.889-1.259.468-.526.703-1.096.703-1.707 0-.612-.235-1.181-.703-1.708-.465-.527-1.094-.944-1.889-1.252zm2.645.81c.536.656.804 1.373.804 2.15 0 .776-.268 1.495-.804 2.156-.535.656-1.263 1.176-2.183 1.56-.92.38-1.924.57-3.013.57a9.16 9.16 0 0 1-.971-.054 7.32 7.32 0 0 1-3.08 1.62 5.044 5.044 0 0 1-.764.148h-.033a.26.26 0 0 1-.181-.074.324.324 0 0 1-.107-.18v-.007c-.014-.018-.016-.045-.007-.08.014-.037.018-.059.014-.068a.19.19 0 0 1 .033-.067.645.645 0 0 0 .04-.06 1.73 1.73 0 0 0 .047-.054l.054-.06a53.034 53.034 0 0 1 .435-.489c.049-.049.118-.136.207-.26a2.57 2.57 0 0 0 .221-.342c.054-.103.114-.235.181-.395a4.18 4.18 0 0 0 .174-.51c-.7-.397-1.254-.888-1.66-1.473A3.261 3.261 0 0 1 6 11.216c0-.777.268-1.494.804-2.15.535-.66 1.263-1.18 2.183-1.56.92-.384 1.924-.576 3.013-.576 1.09 0 2.094.192 3.013.576.92.38 1.648.9 2.183 1.56z" fill="#FFF" fill-rule="nonzero"/></g></symbol><symbol viewBox="0 0 16 16" id="import" xmlns="http://www.w3.org/2000/svg"><path d="M9 8h1a.5.5 0 0 1 .4.8l-2 2.667a.5.5 0 0 1-.8 0L5.6 8.8A.5.5 0 0 1 6 8h1V1a1 1 0 1 1 2 0v7zM0 8a1 1 0 1 1 2 0 6 6 0 1 0 12 0 1 1 0 0 1 2 0A8 8 0 1 1 0 8z"/></symbol><symbol viewBox="0 0 16 16" id="issue-block" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M5.803 8a5.97 5.97 0 0 0-.462 1H4.5a.5.5 0 0 1 0-1h1.303zM4.5 5h3a.5.5 0 0 1 0 1h-3a.5.5 0 0 1 0-1zm7.5.083a6.04 6.04 0 0 0-2 0V3a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h2.083a5.96 5.96 0 0 0 .72 2H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3h6a3 3 0 0 1 3 3v2.083zm1.121 3.796zM11 16a5 5 0 1 1 0-10 5 5 0 0 1 0 10zm-1.293-2.292a3 3 0 0 0 4.001-4.001l-4.001 4zm-1.415-1.415l4.001-4a3 3 0 0 0-4.001 4.001z"/></symbol><symbol viewBox="0 0 16 16" id="issue-child" xmlns="http://www.w3.org/2000/svg"><path d="M11 8H5v1h1a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1H1a1 1 0 0 1-1-1v-4a1 1 0 0 1 1-1h2V7a.997.997 0 0 1 1-1h3V4H4.5a.5.5 0 0 1-.5-.5v-2a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.5.5H9v2h3a.997.997 0 0 1 1 1v2h2a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1h-5a1 1 0 0 1-1-1v-4a1 1 0 0 1 1-1h1V8zm-9 3v2h3v-2H2zm9 0v2h3v-2h-3z"/></symbol><symbol viewBox="0 0 16 16" id="issue-close" xmlns="http://www.w3.org/2000/svg"><path d="M7.536 8.657l2.828-2.829a1 1 0 0 1 1.414 1.415l-3.535 3.535a.997.997 0 0 1-1.415 0l-2.12-2.121A1 1 0 0 1 6.12 7.243l1.415 1.414zM8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12z"/></symbol><symbol viewBox="0 0 16 16" id="issue-duplicate" xmlns="http://www.w3.org/2000/svg"><path d="M10.874 2H12a3 3 0 0 1 3 3v8a3 3 0 0 1-3 3h-2c-.918 0-1.74-.413-2.29-1.063a3.987 3.987 0 0 0 1.988-.984A1 1 0 0 0 10 14h2a1 1 0 0 0 1-1V5a1 1 0 0 0-1-1h-1V3c0-.345-.044-.68-.126-1zM4 0h3a3 3 0 0 1 3 3v8a3 3 0 0 1-3 3H4a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h3a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H4z"/></symbol><symbol viewBox="0 0 16 16" id="issue-new" xmlns="http://www.w3.org/2000/svg"><path d="M10 2V1a1 1 0 0 1 2 0v1h1a1 1 0 0 1 0 2h-1v1a1 1 0 0 1-2 0V4H9a1 1 0 1 1 0-2h1zm0 6a1 1 0 0 1 2 0v5a3 3 0 0 1-3 3H5a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3h1a1 1 0 1 1 0 2H5a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1V8z"/></symbol><symbol viewBox="0 0 16 16" id="issue-open" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zm0-2a4 4 0 1 1 0-8 4 4 0 0 1 0 8zm0-2a2 2 0 1 0 0-4 2 2 0 0 0 0 4z"/></symbol><symbol viewBox="0 0 16 16" id="issue-open-m" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12z"/></symbol><symbol viewBox="0 0 16 16" id="issue-parent" xmlns="http://www.w3.org/2000/svg"><path d="M11 11H5v1h1.5a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.5.5h-6a.5.5 0 0 1-.5-.5v-2a.5.5 0 0 1 .5-.5H3v-2a.997.997 0 0 1 1-1h3V7H5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1H9v2h3a.997.997 0 0 1 1 1v2h2.5a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.5.5h-6a.5.5 0 0 1-.5-.5v-2a.5.5 0 0 1 .5-.5H11v-1zM6 3v2h4V3H6z"/></symbol><symbol viewBox="0 0 16 16" id="issues" xmlns="http://www.w3.org/2000/svg"><path d="M10.458 15.012l.311.055a3 3 0 0 0 3.476-2.433l1.389-7.879A3 3 0 0 0 13.2 1.28L11.23.933a3.002 3.002 0 0 0-.824-.031c.364.59.58 1.28.593 2.02l1.854.328a1 1 0 0 1 .811 1.158l-1.389 7.879a1 1 0 0 1-1.158.81l-.118-.02a3.98 3.98 0 0 1-.541 1.935zM3 0h4a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3z"/></symbol><symbol viewBox="0 0 16 16" id="italic" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M5.5 12l2-8H6a1 1 0 1 1 0-2h6a1 1 0 0 1 0 2h-1.5l-2 8H10a1 1 0 0 1 0 2H4a1 1 0 0 1 0-2h1.5z"/></symbol><symbol viewBox="0 0 16 16" id="key" xmlns="http://www.w3.org/2000/svg"><path d="M7.575 6.689a4.002 4.002 0 0 1 6.274-4.86 4 4 0 0 1-4.86 6.274l-2.21 2.21.706.708a1 1 0 1 1-1.414 1.414l-.707-.707-.707.707.707.707a1 1 0 1 1-1.414 1.414l-.707-.707a1 1 0 0 1-1.414-1.414l5.746-5.746zm2.032-.618a2 2 0 1 0 2.828-2.828A2 2 0 0 0 9.607 6.07z"/></symbol><symbol viewBox="0 0 16 16" id="key-2" xmlns="http://www.w3.org/2000/svg"><path d="M5.172 14.157l-.344.344-2.485.133a.462.462 0 0 1-.497-.503l.14-2.24a.599.599 0 0 1 .177-.382l5.155-5.155a4 4 0 1 1 2.828 2.828l-1.439 1.44-1.06-.354-.708.707.354 1.06-.707.708-1.06-.354-.708.707.354 1.06zm6.01-8.839a1 1 0 1 0 1.414-1.414 1 1 0 0 0-1.414 1.414z"/></symbol><symbol viewBox="0 0 16 16" id="label" xmlns="http://www.w3.org/2000/svg"><path d="M11.782 14.718a3 3 0 0 1-4.242 0L1.652 8.829a2 2 0 0 1-.565-1.702l.54-3.703a2 2 0 0 1 1.69-1.69l3.703-.54a2 2 0 0 1 1.703.564l5.888 5.888a3 3 0 0 1 0 4.243l-2.829 2.829zm1.415-5.657L7.309 3.173l-3.703.54-.54 3.702 5.888 5.888a1 1 0 0 0 1.414 0l2.829-2.828a1 1 0 0 0 0-1.414zM5.732 5.525A1 1 0 1 1 7.146 6.94a1 1 0 0 1-1.414-1.414z"/></symbol><symbol viewBox="0 0 16 16" id="labels" xmlns="http://www.w3.org/2000/svg"><path d="M9.424 2.254l2.08-.905a1 1 0 0 1 1.206.326l3.013 4.12a1 1 0 0 1 .16.849l-1.947 7.264a3 3 0 0 1-3.675 2.122l-.5-.135a3.999 3.999 0 0 0 1.082-1.782 1 1 0 0 0 1.16-.722l1.823-6.802-2.258-3.087-.687.299a2 2 0 0 0-.628-.88l-.829-.667zM.377 3.7L4.4.498a1 1 0 0 1 1.25.003L9.627 3.7a1 1 0 0 1 .373.78V13a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V4.482A1 1 0 0 1 .377 3.7zM2 13a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1V4.958L5.02 2.561 2 4.964V13zm3-6a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="leave" xmlns="http://www.w3.org/2000/svg"><path d="M11 7V5.883a.5.5 0 0 1 .757-.429l3.528 2.117a.5.5 0 0 1 0 .858l-3.528 2.117a.5.5 0 0 1-.757-.43V9H7a1 1 0 1 1 0-2h4zm-2 6.256a1 1 0 0 1 2 0A2.744 2.744 0 0 1 8.256 16H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3h5.19A2.81 2.81 0 0 1 11 2.81a1 1 0 0 1-2 0A.81.81 0 0 0 8.19 2H3a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h5.256c.41 0 .744-.333.744-.744z"/></symbol><symbol viewBox="0 0 16 16" id="level-up" xmlns="http://www.w3.org/2000/svg"><path fill="#2E2E2E" fill-rule="evenodd" d="M7 6h3.489a.5.5 0 0 0 .373-.832L6.374.117a.5.5 0 0 0-.748 0l-4.488 5.05A.5.5 0 0 0 1.51 6H5v7a3 3 0 0 0 3 3h6a1 1 0 0 0 0-2H8a1 1 0 0 1-1-1V6z"/></symbol><symbol viewBox="0 0 16 16" id="license" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M12.56 8.9l2.66 4.606a.3.3 0 0 1-.243.45l-1.678.094a.1.1 0 0 0-.078.044l-.953 1.432a.3.3 0 0 1-.51-.016L9.097 10.9a5.994 5.994 0 0 0 3.464-2zm-5.23 2.063L4.707 15.51a.3.3 0 0 1-.51.016l-.953-1.432a.1.1 0 0 0-.078-.044l-1.678-.094a.3.3 0 0 1-.243-.45l2.48-4.297a5.983 5.983 0 0 0 3.607 1.754zM8 10A5 5 0 1 1 8 0a5 5 0 0 1 0 10zm0-2a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm0-1a2 2 0 1 1 0-4 2 2 0 0 1 0 4z"/></symbol><symbol viewBox="0 0 16 16" id="link" xmlns="http://www.w3.org/2000/svg"><path d="M6.986 3.35l2.12-2.122a4 4 0 0 1 5.657 5.657l-2.828 2.829a4 4 0 0 1-5.657 0 1 1 0 0 1 1.414-1.415 2 2 0 0 0 2.829 0l2.828-2.828a2 2 0 1 0-2.828-2.828l-1.001 1a5.018 5.018 0 0 0-2.534-.294zm2.12 9.192l-2.12 2.121a4 4 0 1 1-5.658-5.656l2.829-2.829a4 4 0 0 1 5.657 0 1 1 0 1 1-1.415 1.414 2 2 0 0 0-2.828 0l-2.828 2.829a2 2 0 1 0 2.828 2.828l1.001-1.001a5.018 5.018 0 0 0 2.534.294z"/></symbol><symbol viewBox="0 0 16 16" id="list-bulleted" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M1 4a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm0 5a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm4-7h10a1 1 0 0 1 0 2H5a1 1 0 1 1 0-2zm0 5h10a1 1 0 0 1 0 2H5a1 1 0 1 1 0-2zm-4 7a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm4-2h10a1 1 0 0 1 0 2H5a1 1 0 0 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="list-numbered" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M6 2h8a1 1 0 0 1 0 2H6a1 1 0 1 1 0-2zm0 5h8a1 1 0 0 1 0 2H6a1 1 0 1 1 0-2zm0 5h8a1 1 0 0 1 0 2H6a1 1 0 0 1 0-2zM1.156 5v-.828h.816V2.204h-.72v-.636c.432-.084.708-.192.996-.372h.756v2.976h.684V5H1.156zm-.18 5v-.588c.9-.828 1.596-1.464 1.596-1.98 0-.342-.192-.504-.468-.504-.252 0-.444.18-.624.36l-.552-.552c.396-.42.756-.612 1.32-.612.768 0 1.308.492 1.308 1.248 0 .612-.576 1.284-1.092 1.812.192-.024.468-.048.636-.048h.636V10H.976zm1.26 5.072c-.618 0-1.068-.204-1.356-.54l.468-.648c.234.216.51.36.78.36.336 0 .552-.12.552-.36 0-.288-.15-.456-.948-.456v-.72c.636 0 .828-.168.828-.432 0-.228-.138-.348-.396-.348-.252 0-.432.108-.672.312l-.516-.624c.372-.312.768-.492 1.236-.492.84 0 1.38.384 1.38 1.074 0 .366-.204.642-.612.822v.024c.432.132.732.432.732.912 0 .72-.684 1.116-1.476 1.116z"/></symbol><symbol viewBox="0 0 16 16" id="location" xmlns="http://www.w3.org/2000/svg"><path d="M8.755 15.144a1 1 0 0 1-1.51 0C3.748 11.114 2 8.065 2 6a6 6 0 1 1 12 0c0 2.065-1.748 5.113-5.245 9.144zM12 6a4 4 0 1 0-8 0c0 1.314 1.312 3.71 4 6.944C10.688 9.71 12 7.314 12 6zM8 8a2 2 0 1 1 0-4 2 2 0 0 1 0 4z"/></symbol><symbol viewBox="0 0 16 16" id="location-dot" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M6.314 13.087C4.382 13.295 3 13.85 3 14.5c0 .828 2.239 1.5 5 1.5s5-.672 5-1.5c0-.65-1.382-1.205-3.314-1.413l-.202.225a2 2 0 0 1-2.968 0l-.202-.225zm2.428-.445a1 1 0 0 1-1.484 0C4.419 9.5 3 7.037 3 5.252 3 2.353 5.239 0 8 0s5 2.352 5 5.253c0 1.784-1.42 4.247-4.258 7.389zM11 5.252C11 3.436 9.634 2 8 2S5 3.435 5 5.253c0 1.027.974 2.824 3 5.203 2.026-2.38 3-4.176 3-5.203zM8 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="lock" xmlns="http://www.w3.org/2000/svg"><path d="M10 5V4h2v1a3 3 0 0 1 3 3v5a3 3 0 0 1-3 3H4a3 3 0 0 1-3-3V8a3 3 0 0 1 3-3V4h2v1h4zM4 7a1 1 0 0 0-1 1v5a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V8a1 1 0 0 0-1-1H4zm0-3a4 4 0 1 1 8 0h-2a2 2 0 1 0-4 0H4z"/></symbol><symbol viewBox="0 0 16 16" id="lock-open" xmlns="http://www.w3.org/2000/svg"><path d="M4.044 4a4 4 0 0 1 6.99-2.658 1 1 0 1 1-1.495 1.33A2 2 0 0 0 6.044 4a.998.998 0 0 1-.07.367v.701H12a3 3 0 0 1 3 3v5a3 3 0 0 1-3 3H4a3 3 0 0 1-3-3v-5a3 3 0 0 1 2.974-3V4h.07zM4 7.07a1 1 0 0 0-1 1v5a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1v-5a1 1 0 0 0-1-1H4z"/></symbol><symbol viewBox="0 0 16 16" id="log" xmlns="http://www.w3.org/2000/svg"><path d="M4 0h8a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H4a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H4zm1 4a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm0 3a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm3-5h3a1 1 0 0 1 0 2H8a1 1 0 1 1 0-2zm0 3h3a1 1 0 0 1 0 2H8a1 1 0 1 1 0-2zm-3 5a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm3-2h3a1 1 0 0 1 0 2H8a1 1 0 0 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="mail" xmlns="http://www.w3.org/2000/svg"><path d="M14 5.6L9.338 9.796a2 2 0 0 1-2.676 0L2 5.6V11a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V5.6zM3 2h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3zm.212 2L8 8.31 12.788 4H3.212z"/></symbol><symbol viewBox="0 0 16 16" id="menu" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M1.143 2h13.714C15.488 2 16 2.448 16 3s-.512 1-1.143 1H1.143C.512 4 0 3.552 0 3s.512-1 1.143-1zm0 5h13.714C15.488 7 16 7.448 16 8s-.512 1-1.143 1H1.143C.512 9 0 8.552 0 8s.512-1 1.143-1zm0 5h13.714c.631 0 1.143.448 1.143 1s-.512 1-1.143 1H1.143C.512 14 0 13.552 0 13s.512-1 1.143-1z"/></symbol><symbol viewBox="0 0 16 16" id="merge-request-close" xmlns="http://www.w3.org/2000/svg"><path d="M9.414 8l1.414 1.414a1 1 0 1 1-1.414 1.414L8 9.414l-1.414 1.414a1 1 0 1 1-1.414-1.414L6.586 8 5.172 6.586a1 1 0 1 1 1.414-1.414L8 6.586l1.414-1.414a1 1 0 1 1 1.414 1.414L9.414 8zM8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12z"/></symbol><symbol viewBox="0 0 16 16" id="messages" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8.588 8.942l1.173 5.862A1 1 0 0 1 8.78 16H7.22a1 1 0 0 1-.98-1.196l1.172-5.862a3.014 3.014 0 0 0 1.176 0zM8 8a2 2 0 1 1 0-4 2 2 0 0 1 0 4zM4.464 2.464L5.88 3.88a3 3 0 0 0 0 4.242L4.464 9.536a5 5 0 0 1 0-7.072zm7.072 7.072L10.12 8.12a3 3 0 0 0 0-4.242l1.415-1.415a5 5 0 0 1 0 7.072zM2.343.343l1.414 1.414a6 6 0 0 0 0 8.486l-1.414 1.414a8 8 0 0 1 0-11.314zm11.314 11.314l-1.414-1.414a6 6 0 0 0 0-8.486L13.657.343a8 8 0 0 1 0 11.314z"/></symbol><symbol viewBox="0 0 16 16" id="mobile-issue-close" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M5.657 10.728L2.12 7.192A1 1 0 1 0 .707 8.607l4.243 4.242a.997.997 0 0 0 1.414 0l8.485-8.485a1 1 0 1 0-1.414-1.414l-7.778 7.778z"/></symbol><symbol viewBox="0 0 16 16" id="monitor" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10 13v1h3a1 1 0 0 1 0 2H3a1 1 0 0 1 0-2h3v-1H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v7a3 3 0 0 1-3 3h-3zM3 2a1 1 0 0 0-1 1v7a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3zm5.723 6.416l-2.66-1.773-1.71 1.71a.5.5 0 1 1-.707-.707l2-2a.5.5 0 0 1 .631-.062l2.66 1.773 2.71-2.71a.5.5 0 0 1 .707.707l-3 3a.5.5 0 0 1-.631.062z"/></symbol><symbol viewBox="0 0 16 16" id="more" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8 4a2 2 0 1 1 0-4 2 2 0 0 1 0 4zm0 6a2 2 0 1 1 0-4 2 2 0 0 1 0 4zm0 6a2 2 0 1 1 0-4 2 2 0 0 1 0 4z"/></symbol><symbol viewBox="0 0 16 16" id="notifications" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M6 14H2.435a2 2 0 0 1-1.761-2.947c.962-1.788 1.521-3.065 1.68-3.832.322-1.566.947-5.501 4.65-6.134a1 1 0 1 1 1.994-.024c3.755.528 4.375 4.27 4.761 6.043.188.86.742 2.188 1.661 3.982A2 2 0 0 1 13.64 14H10a2 2 0 1 1-4 0zm5.805-6.468c-.325-1.492-.37-1.674-.61-2.288C10.6 3.716 9.742 3 8.07 3c-1.608 0-2.49.718-3.103 2.197-.28.676-.356.982-.654 2.428-.208 1.012-.827 2.424-1.877 4.375H13.64c-.993-1.937-1.6-3.396-1.835-4.468z"/></symbol><symbol viewBox="0 0 16 16" id="notifications-off" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M13.26 5.089c.243.757.382 1.478.5 2.017.187.86.74 2.188 1.66 3.982A2 2 0 0 1 13.64 14H10a2 2 0 1 1-4 0H4.35l2-2h7.29c-.993-1.937-1.6-3.396-1.835-4.468-.07-.326-.129-.59-.178-.81l1.634-1.633zM10.943 1.75l-1.48 1.48C9.07 3.076 8.612 3 8.069 3c-1.608 0-2.49.718-3.103 2.197-.28.676-.356.982-.654 2.428-.065.317-.17.673-.317 1.073L.45 12.242a1.99 1.99 0 0 1 .224-1.19c.962-1.787 1.521-3.064 1.68-3.831.322-1.566.947-5.501 4.65-6.134a1 1 0 1 1 1.994-.024 4.867 4.867 0 0 1 1.944.688zm2.932-.105a1 1 0 0 1 0 1.415L2.561 14.374a1 1 0 1 1-1.415-1.414L12.46 1.646a1 1 0 0 1 1.414 0z"/></symbol><symbol viewBox="0 0 16 16" id="overview" xmlns="http://www.w3.org/2000/svg"><path d="M2 0h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2zm0 2v3h3V2H2zm9-2h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2h-3a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2zm0 2v3h3V2h-3zM2 9h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2zm0 2v3h3v-3H2zm9-2h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2h-3a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2zm0 2v3h3v-3h-3z"/></symbol><symbol viewBox="0 0 16 16" id="pencil" xmlns="http://www.w3.org/2000/svg"><path d="M13.02 1.293l1.414 1.414a1 1 0 0 1 0 1.414L4.119 14.436a1 1 0 0 1-.704.293l-2.407.008L1 12.316a1 1 0 0 1 .293-.71L11.605 1.292a1 1 0 0 1 1.414 0zm-1.416 1.415l-.707.707L12.31 4.83l.707-.707-1.414-1.415zM3.411 13.73l1.123-1.122H3.12v-1.415L2 12.312l.005 1.422 1.406-.005z"/></symbol><symbol viewBox="0 0 16 16" id="pencil-square" xmlns="http://www.w3.org/2000/svg"><path d="M12 9a1 1 0 0 1 2 0v4a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3h4a1 1 0 1 1 0 2H3a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V9zm.778-7.179l1.414 1.415-6.476 6.476a1 1 0 0 1-.498.27l-1.51.325.323-1.512a1 1 0 0 1 .27-.497l6.477-6.477zM15.607.407a1 1 0 0 1 0 1.414l-.708.707-1.414-1.414.707-.707a1 1 0 0 1 1.415 0z"/></symbol><symbol viewBox="0 0 16 16" id="pipeline" xmlns="http://www.w3.org/2000/svg"><path d="M8.969 7.25a2 2 0 1 1-1.938 0A1.002 1.002 0 0 1 7 7V5.083a.2.2 0 0 1 .06-.142l.877-.87a.1.1 0 0 1 .141 0l.864.87A.2.2 0 0 1 9 5.083V7c0 .086-.01.17-.031.25zM8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zm4.5-4a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm0-3a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm-2 6a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm0-9a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm-5 9a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm0-9a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm-2 6a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm0-3a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zM8 10a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="play" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M2.765 15.835c-.545.321-1.258.159-1.593-.363A1.075 1.075 0 0 1 1 14.89V1.11C1 .496 1.518 0 2.158 0c.214 0 .424.057.607.165l11.684 6.89c.544.321.714 1.005.38 1.526a1.135 1.135 0 0 1-.38.364l-11.684 6.89z"/></symbol><symbol viewBox="0 0 16 16" id="plus" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M7 7V1a1 1 0 1 1 2 0v6h6a1 1 0 0 1 0 2H9v6a1 1 0 0 1-2 0V9H1a1 1 0 1 1 0-2h6z"/></symbol><symbol viewBox="0 0 16 16" id="plus-square" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M9 7V4a1 1 0 1 0-2 0v3H4a1 1 0 1 0 0 2h3v3a1 1 0 0 0 2 0V9h3a1 1 0 0 0 0-2H9zM3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3z"/></symbol><symbol viewBox="0 0 16 16" id="plus-square-o" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M7 7V5a1 1 0 1 1 2 0v2h2a1 1 0 0 1 0 2H9v2a1 1 0 0 1-2 0V9H5a1 1 0 1 1 0-2h2zM3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3z"/></symbol><symbol viewBox="0 0 16 16" id="preferences" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M5 12h10a1 1 0 0 1 0 2H5a1 1 0 0 1-2 0v-2a1 1 0 0 1 2 0zm-3 0H1a1 1 0 0 0 0 2h1v-2zm11-5h2a1 1 0 0 1 0 2h-2a1 1 0 0 1-2 0V7a1 1 0 0 1 2 0zm-3 0H1a1 1 0 1 0 0 2h9V7zM6 2h9a1 1 0 0 1 0 2H6a1 1 0 1 1-2 0V2a1 1 0 1 1 2 0zM3 2H1a1 1 0 1 0 0 2h2V2z"/></symbol><symbol viewBox="0 0 16 16" id="profile" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zm-4.274-3.404C4.412 9.709 5.694 9 8 9c2.313 0 3.595.7 4.28 1.586A4.997 4.997 0 0 1 8 13a4.997 4.997 0 0 1-4.274-2.404zM8 8a2 2 0 1 1 0-4 2 2 0 0 1 0 4z"/></symbol><symbol viewBox="0 0 16 16" id="project" xmlns="http://www.w3.org/2000/svg"><path d="M8.462 2.177l-.038.044a.505.505 0 0 0 .038-.044zm-.787 0a.5.5 0 0 0 .038.043l-.038-.043zM3.706 7h8.725L8.069 2.585 3.706 7zM7 13.369V12a1 1 0 0 1 2 0v1.369h3V9H4v4.369h3zM14 9v4.836c0 .833-.657 1.533-1.5 1.533h-9c-.843 0-1.5-.7-1.5-1.533V9h-.448a1.1 1.1 0 0 1-.783-1.873L6.934.887a1.5 1.5 0 0 1 2.269 0l6.165 6.24A1.1 1.1 0 0 1 14.585 9H14z"/></symbol><symbol viewBox="0 0 16 16" id="push-rules" xmlns="http://www.w3.org/2000/svg"><path d="M6.268 9a2 2 0 0 1 3.464 0H11a1 1 0 0 1 0 2H9.732a2 2 0 0 1-3.464 0H5a1 1 0 0 1 0-2h1.268zM7 2H4a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1h-1v3.515a.3.3 0 0 1-.434.268l-1.432-.716a.3.3 0 0 0-.268 0l-1.432.716A.3.3 0 0 1 7 5.515V2zM4 0h8a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H4a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm4 11a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="question" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm-1.46-5.602h2.233a3.97 3.97 0 0 1 .051-.558c.029-.17.073-.326.133-.469.06-.143.14-.28.242-.41.102-.13.228-.263.38-.399.26-.24.504-.467.733-.683a5.03 5.03 0 0 0 .598-.668c.17-.23.302-.477.399-.742a2.66 2.66 0 0 0 .144-.907c0-.505-.083-.95-.25-1.335a2.55 2.55 0 0 0-.723-.97 3.2 3.2 0 0 0-1.152-.589 5.441 5.441 0 0 0-1.531-.2c-.516 0-.998.063-1.445.188a3.19 3.19 0 0 0-1.168.59c-.331.268-.594.61-.79 1.027-.195.417-.295.917-.3 1.5h2.64c.006-.224.04-.416.102-.578.062-.161.142-.293.238-.394a.921.921 0 0 1 .332-.227 1.04 1.04 0 0 1 .39-.074c.34 0 .593.095.763.285.169.19.254.488.254.895 0 .328-.106.63-.317.906-.21.276-.499.565-.863.867-.214.182-.39.374-.531.574-.141.2-.253.42-.336.657a3.656 3.656 0 0 0-.176.777 7.89 7.89 0 0 0-.05.937zm-.321 2.375c0 .188.035.362.105.524.07.161.17.3.301.418.13.117.284.21.46.277.178.068.376.102.595.102.218 0 .416-.034.593-.102.178-.068.331-.16.461-.277a1.2 1.2 0 0 0 .301-.418c.07-.162.106-.336.106-.524a1.3 1.3 0 0 0-.106-.523 1.2 1.2 0 0 0-.3-.418 1.461 1.461 0 0 0-.462-.277 1.651 1.651 0 0 0-.593-.102c-.22 0-.417.034-.594.102a1.46 1.46 0 0 0-.461.277 1.2 1.2 0 0 0-.3.418 1.284 1.284 0 0 0-.106.523z"/></symbol><symbol viewBox="0 0 16 16" id="question-o" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zm-.778-4.151c0-.301.014-.575.044-.82a3.2 3.2 0 0 1 .154-.68c.073-.208.17-.4.294-.575.123-.176.278-.343.465-.503a4.81 4.81 0 0 0 .755-.758c.185-.242.277-.506.277-.793 0-.356-.074-.617-.222-.783-.148-.166-.37-.25-.667-.25a.92.92 0 0 0-.342.065.806.806 0 0 0-.29.199 1.04 1.04 0 0 0-.209.345 1.5 1.5 0 0 0-.088.506H5.082c.005-.51.092-.948.263-1.313.171-.364.401-.664.69-.899.29-.234.63-.406 1.023-.516a4.66 4.66 0 0 1 1.264-.164c.497 0 .944.058 1.34.174.397.117.733.289 1.008.517.276.227.487.51.633.847.146.337.218.727.218 1.17 0 .295-.042.56-.126.792a2.52 2.52 0 0 1-.349.65 4.4 4.4 0 0 1-.523.584c-.2.19-.414.389-.642.598a2.73 2.73 0 0 0-.332.349c-.089.114-.16.233-.212.359a1.868 1.868 0 0 0-.116.41 3.39 3.39 0 0 0-.044.489H7.222zm-.28 2.078c0-.164.03-.317.092-.458a1.05 1.05 0 0 1 .263-.366c.114-.103.248-.183.403-.243a1.45 1.45 0 0 1 .52-.089c.191 0 .364.03.52.09.154.059.289.14.403.242.114.103.201.224.263.366.061.141.092.294.092.458 0 .164-.03.316-.092.458a1.05 1.05 0 0 1-.263.365 1.278 1.278 0 0 1-.404.243 1.43 1.43 0 0 1-.52.089c-.19 0-.364-.03-.519-.089-.155-.06-.29-.14-.403-.243a1.05 1.05 0 0 1-.263-.365 1.135 1.135 0 0 1-.093-.458z"/></symbol><symbol viewBox="0 0 16 16" id="quote" xmlns="http://www.w3.org/2000/svg"><path d="M15 3v8a3 3 0 0 1-3 3 1 1 0 0 1 0-2 1 1 0 0 0 1-1V9h-2a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h3a1 1 0 0 1 1 1zM7 3v8a3 3 0 0 1-3 3 1 1 0 0 1 0-2 1 1 0 0 0 1-1V9H3a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h3a1 1 0 0 1 1 1z"/></symbol><symbol viewBox="0 0 16 16" id="redo" xmlns="http://www.w3.org/2000/svg"><path d="M4.666 4.423a5 5 0 1 1-.203 6.944 1 1 0 1 0-1.478 1.347 7 7 0 1 0 .12-9.556L1.842 2.137a.5.5 0 0 0-.815.385L1 7.26a.5.5 0 0 0 .607.492l4.629-1.013a.5.5 0 0 0 .207-.877L4.666 4.423z"/></symbol><symbol viewBox="0 0 16 16" id="remove" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M2 3a1 1 0 1 1 0-2h12a1 1 0 0 1 0 2v10a3 3 0 0 1-3 3H5a3 3 0 0 1-3-3V3zm3-2a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1H5zM4 3v10a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V3H4zm2.5 2a.5.5 0 0 1 .5.5v6a.5.5 0 1 1-1 0v-6a.5.5 0 0 1 .5-.5zm3 0a.5.5 0 0 1 .5.5v6a.5.5 0 1 1-1 0v-6a.5.5 0 0 1 .5-.5z"/></symbol><symbol viewBox="0 0 16 16" id="repeat" xmlns="http://www.w3.org/2000/svg"><path d="M11.494 4.423a5 5 0 1 0 .203 6.944 1 1 0 1 1 1.478 1.347 7 7 0 1 1-.12-9.556l1.262-1.021a.5.5 0 0 1 .815.385l.028 4.738a.5.5 0 0 1-.607.492L9.924 6.739a.5.5 0 0 1-.207-.877l1.777-1.439z"/></symbol><symbol viewBox="0 0 16 16" id="retry" xmlns="http://www.w3.org/2000/svg"><path d="M4.114 6.958a4 4 0 0 0 5.283 4.775 1 1 0 1 1 .712 1.87A6 6 0 0 1 2.182 6.44l-.741-.2a.5.5 0 0 1-.12-.915l2.195-1.268a.5.5 0 0 1 .683.183l1.268 2.196a.5.5 0 0 1-.563.733l-.79-.212zm7.777 2.084a4 4 0 0 0-5.284-4.775 1 1 0 0 1-.712-1.87 6 6 0 0 1 7.927 7.162l.742.2a.5.5 0 0 1 .12.915l-2.196 1.268a.5.5 0 0 1-.683-.183l-1.267-2.196a.5.5 0 0 1 .562-.733l.79.212z"/></symbol><symbol viewBox="0 0 16 16" id="scale" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M13.99 9a.792.792 0 0 0-.078-.231L13 7l-.912 1.769a.791.791 0 0 0-.077.231h1.978zm-10 0a.792.792 0 0 0-.078-.231L3 7l-.912 1.769A.791.791 0 0 0 2.011 9h1.978zM2 0h12a1 1 0 0 1 0 2H2a1 1 0 1 1 0-2zm3 14h6a1 1 0 0 1 0 2H5a1 1 0 0 1 0-2zM8 4a1 1 0 0 1 1 1v9H7V5a1 1 0 0 1 1-1zm-4.53-.714l2.265 4.735c.68 1.42.006 3.091-1.504 3.73A3.161 3.161 0 0 1 3 12c-1.657 0-3-1.263-3-2.821 0-.4.09-.794.264-1.158L2.53 3.286a.53.53 0 0 1 .94 0zm10 0l2.265 4.735c.68 1.42.006 3.091-1.504 3.73A3.161 3.161 0 0 1 13 12c-1.657 0-3-1.263-3-2.821 0-.4.09-.794.264-1.158l2.266-4.735a.53.53 0 0 1 .94 0z"/></symbol><symbol viewBox="0 0 16 16" id="screen-full" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M14 14v-2a1 1 0 0 1 2 0v3a.997.997 0 0 1-1 1h-3a1 1 0 0 1 0-2h2zM2 14v-2a1 1 0 0 0-2 0v3a1 1 0 0 0 1 1h3a1 1 0 0 0 0-2H2zM15.707.293A.997.997 0 0 1 16 1v3a1 1 0 0 1-2 0V2h-2a1 1 0 0 1 0-2h3c.276 0 .526.112.707.293zM2 2v2a1 1 0 1 1-2 0V1a.997.997 0 0 1 1-1h3a1 1 0 1 1 0 2H2zm4 4h4a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1z"/></symbol><symbol viewBox="0 0 16 16" id="screen-normal" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M3 3V1a1 1 0 1 1 2 0v3a.997.997 0 0 1-1 1H1a1 1 0 1 1 0-2h2zm10 0h2a1 1 0 0 1 0 2h-3a.997.997 0 0 1-1-1V1a1 1 0 0 1 2 0v2zM3 13H1a1 1 0 0 1 0-2h3a.997.997 0 0 1 1 1v3a1 1 0 0 1-2 0v-2zm10 0v2a1 1 0 0 1-2 0v-3a.997.997 0 0 1 1-1h3a1 1 0 0 1 0 2h-2zM6.5 7h3a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5v-1a.5.5 0 0 1 .5-.5z"/></symbol><symbol viewBox="0 0 12 16" id="scroll_down" xmlns="http://www.w3.org/2000/svg"><path class="evfirst-triangle" d="M1.048 14.155a.508.508 0 0 0-.32.105c-.091.07-.136.154-.136.25v.71c0 .095.045.178.135.249.09.07.197.105.321.105h10.043a.51.51 0 0 0 .321-.105c.09-.07.136-.154.136-.25v-.71c0-.095-.045-.178-.136-.249a.508.508 0 0 0-.32-.105"/><path class="evsecond-triangle" d="M.687 8.027c-.09-.087-.122-.16-.093-.22.028-.06.104-.09.228-.09h10.5c.123 0 .2.03.228.09.029.06-.002.133-.093.22L6.393 12.91a.458.458 0 0 1-.136.089h-.37a.626.626 0 0 1-.136-.09"/><path class="evthird-triangle" d="M.687 1.027C.597.94.565.867.594.807c.028-.06.104-.09.228-.09h10.5c.123 0 .2.03.228.09.029.06-.002.133-.093.22L6.393 5.91a.458.458 0 0 1-.136.09h-.37a.626.626 0 0 1-.136-.09"/></symbol><symbol viewBox="0 0 12 16" id="scroll_up" xmlns="http://www.w3.org/2000/svg"><path d="M1.048 1.845a.508.508 0 0 1-.32-.105c-.091-.07-.136-.154-.136-.25V.78c0-.095.045-.178.135-.249a.508.508 0 0 1 .321-.105h10.043a.51.51 0 0 1 .321.105c.09.07.136.154.136.25v.71c0 .095-.045.178-.136.249a.508.508 0 0 1-.32.105M.687 7.973c-.09.087-.122.16-.093.22.028.06.104.09.228.09h10.5c.123 0 .2-.03.228-.09.029-.06-.002-.133-.093-.22L6.393 3.09A.458.458 0 0 0 6.257 3h-.37a.626.626 0 0 0-.136.09M.687 14.973c-.09.087-.122.16-.093.22.028.06.104.09.228.09h10.5c.123 0 .2-.03.228-.09.029-.06-.002-.133-.093-.22L6.393 10.09a.458.458 0 0 0-.136-.09h-.37a.626.626 0 0 0-.136.09"/></symbol><symbol viewBox="0 0 16 16" id="search" xmlns="http://www.w3.org/2000/svg"><path d="M8.853 8.854a3.5 3.5 0 1 0-4.95-4.95 3.5 3.5 0 0 0 4.95 4.95zm.207 2.328a5.5 5.5 0 1 1 2.121-2.121l3.329 3.328a1.5 1.5 0 0 1-2.121 2.121L9.06 11.182z"/></symbol><symbol viewBox="0 0 16 16" id="settings" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M2.415 5.803L1.317 4.084A.5.5 0 0 1 1.35 3.5l.805-.994a.5.5 0 0 1 .564-.153l1.878.704a5.975 5.975 0 0 1 1.65-.797L6.885.342A.5.5 0 0 1 7.36 0h1.28a.5.5 0 0 1 .474.342l.639 1.918a5.97 5.97 0 0 1 1.65.797l1.877-.704a.5.5 0 0 1 .565.153l.805.994a.5.5 0 0 1 .032.584l-1.097 1.719c.217.551.354 1.143.399 1.76l1.731 1.058a.5.5 0 0 1 .227.54l-.288 1.246a.5.5 0 0 1-.44.385l-2.008.19a6.026 6.026 0 0 1-1.142 1.431l.265 1.995a.5.5 0 0 1-.277.516l-1.15.56a.5.5 0 0 1-.576-.1l-1.424-1.452a6.047 6.047 0 0 1-1.804 0l-1.425 1.453a.5.5 0 0 1-.576.1l-1.15-.561a.5.5 0 0 1-.276-.516l.265-1.995a6.026 6.026 0 0 1-1.143-1.43l-2.008-.191a.5.5 0 0 1-.44-.385L.058 9.16a.5.5 0 0 1 .226-.539l1.732-1.058a5.968 5.968 0 0 1 .399-1.76zM8 11a3 3 0 1 0 0-6 3 3 0 0 0 0 6z"/></symbol><symbol viewBox="0 0 16 16" id="shield" xmlns="http://www.w3.org/2000/svg"><path d="M4 0h8a3 3 0 0 1 3 3v7.186a3 3 0 0 1-1.426 2.554l-4 2.465a3 3 0 0 1-3.148 0l-4-2.465A3 3 0 0 1 1 10.186V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v7.186a1 1 0 0 0 .475.852l4 2.464a1 1 0 0 0 1.05 0l4-2.464a1 1 0 0 0 .475-.852V3a1 1 0 0 0-1-1H4zm0 1.5a.5.5 0 0 1 .5-.5h4v8.837a.5.5 0 0 1-.753.431l-3.5-2.052A.5.5 0 0 1 4 9.785V3.5z"/></symbol><symbol viewBox="0 0 16 16" id="slight-frown" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zm-2.163-3.275a2.499 2.499 0 0 1 4.343.03.5.5 0 0 1-.871.49 1.5 1.5 0 0 0-2.607-.018.5.5 0 1 1-.865-.502zM5 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm6 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="slight-smile" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zM5 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm6 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm-5.163 2.254a.5.5 0 1 1 .865-.502 1.499 1.499 0 0 0 2.607-.018.5.5 0 1 1 .871.49 2.499 2.499 0 0 1-4.343.03z"/></symbol><symbol viewBox="0 0 16 16" id="smile" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zM6.18 6.27a.5.5 0 0 1-.873.487.5.5 0 0 0-.872-.003.5.5 0 1 1-.87-.495 1.5 1.5 0 0 1 2.616.012zm6 0a.5.5 0 1 1-.873.487.5.5 0 0 0-.872-.003.5.5 0 1 1-.87-.495 1.5 1.5 0 0 1 2.616.012zM5 9a3 3 0 0 0 6 0H5z"/></symbol><symbol viewBox="0 0 16 16" id="smiley" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zM5 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm6 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2zM5 9h6a3 3 0 0 1-6 0z"/></symbol><symbol viewBox="0 0 16 16" id="snippet" xmlns="http://www.w3.org/2000/svg"><path d="M10.67 9.31a3.001 3.001 0 0 1 2.062 5.546 3 3 0 0 1-3.771-4.559 1.007 1.007 0 0 1-.095-.137l-4.5-7.794a1 1 0 0 1 1.732-1l4.5 7.794c.028.05.052.1.071.15zm-3.283.35l-.289.5c-.028.05-.06.095-.095.137a3.001 3.001 0 0 1-3.77 4.56A3 3 0 0 1 5.294 9.31c.02-.051.043-.102.071-.15l.866-1.5 1.155 2zm2.31-4l-1.156-2 1.325-2.294a1 1 0 0 1 1.732 1L9.696 5.66zm-5.465 7.464a1 1 0 1 0 1-1.732 1 1 0 0 0-1 1.732zm7.5 0a1 1 0 1 0-1-1.732 1 1 0 0 0 1 1.732z"/></symbol><symbol viewBox="0 0 16 16" id="spam" xmlns="http://www.w3.org/2000/svg"><path d="M8.75.433l5.428 3.134a1.5 1.5 0 0 1 .75 1.299v6.268a1.5 1.5 0 0 1-.75 1.299L8.75 15.567a1.5 1.5 0 0 1-1.5 0l-5.428-3.134a1.5 1.5 0 0 1-.75-1.299V4.866a1.5 1.5 0 0 1 .75-1.299L7.25.433a1.5 1.5 0 0 1 1.5 0zM3.072 5.155v5.69L8 13.691l4.928-2.846v-5.69L8 2.309 3.072 5.155zM8 4a1 1 0 0 1 1 1v3a1 1 0 1 1-2 0V5a1 1 0 0 1 1-1zm0 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 14 14" id="spinner" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><circle cx="7" cy="7" r="6" stroke="#000" stroke-opacity=".1" stroke-width="2"/><path fill="#000" fill-opacity=".1" fill-rule="nonzero" d="M7 0a7 7 0 0 1 7 7h-2a5 5 0 0 0-5-5V0z"/></g></symbol><symbol viewBox="0 0 16 16" id="star" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M7.609 14.394l-3.465 1.473a1 1 0 0 1-1.39-.989l.276-4.024a1 1 0 0 0-.219-.694L.303 7.037A1 1 0 0 1 .83 5.443l3.715-.964a1 1 0 0 0 .609-.457L7.14.682a1 1 0 0 1 1.72 0l1.985 3.34a1 1 0 0 0 .609.457l3.715.964a1 1 0 0 1 .528 1.594L13.19 10.16a1 1 0 0 0-.219.694l.275 4.024a1 1 0 0 1-1.389.989l-3.465-1.473a1 1 0 0 0-.782 0z"/></symbol><symbol viewBox="0 0 16 16" id="star-o" xmlns="http://www.w3.org/2000/svg"><path d="M10.975 10.99a3 3 0 0 1 .655-2.083l1.54-1.916-2.219-.576a3 3 0 0 1-1.825-1.37L8 3.15 6.874 5.044a3 3 0 0 1-1.825 1.371l-2.218.576 1.54 1.916a3 3 0 0 1 .654 2.083l-.165 2.4 1.965-.836a3 3 0 0 1 2.348 0l1.965.836-.164-2.399zM7.61 14.394l-3.465 1.473a1 1 0 0 1-1.39-.989l.276-4.024a1 1 0 0 0-.219-.694L.303 7.037A1 1 0 0 1 .83 5.443l3.715-.964a1 1 0 0 0 .609-.457L7.14.682a1 1 0 0 1 1.72 0l1.985 3.34a1 1 0 0 0 .609.457l3.715.964a1 1 0 0 1 .528 1.594L13.19 10.16a1 1 0 0 0-.219.694l.275 4.024a1 1 0 0 1-1.389.989l-3.465-1.473a1 1 0 0 0-.782 0z"/></symbol><symbol viewBox="0 0 14 14" id="status_canceled" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M5.2 3.8l4.9 4.9c.2.2.2.5 0 .7l-.7.7c-.2.2-.5.2-.7 0L3.8 5.2c-.2-.2-.2-.5 0-.7l.7-.7c.2-.2.5-.2.7 0"/></g></symbol><symbol viewBox="0 0 22 22" id="status_canceled_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M8.171 5.971l7.7 7.7a.76.76 0 0 1 0 1.1l-1.1 1.1a.76.76 0 0 1-1.1 0l-7.7-7.7a.76.76 0 0 1 0-1.1l1.1-1.1a.76.76 0 0 1 1.1 0"/></symbol><symbol viewBox="0 0 16 16" id="status_closed" xmlns="http://www.w3.org/2000/svg"><path d="M7.536 8.657l2.828-2.83a1 1 0 0 1 1.414 1.416l-3.535 3.535a1 1 0 0 1-1.415.001l-2.12-2.12a1 1 0 1 1 1.413-1.415zM8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12z"/></symbol><symbol viewBox="0 0 14 14" id="status_created" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><circle cx="7" cy="7" r="3.25"/></g></symbol><symbol viewBox="0 0 22 22" id="status_created_borderless" xmlns="http://www.w3.org/2000/svg"><circle cx="11" cy="11" r="5.107"/></symbol><symbol viewBox="0 0 14 14" id="status_failed" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M7 5.969L5.599 4.568a.29.29 0 0 0-.413.004l-.614.614a.294.294 0 0 0-.004.413L5.968 7l-1.4 1.401a.29.29 0 0 0 .004.413l.614.614c.113.114.3.117.413.004L7 8.032l1.401 1.4a.29.29 0 0 0 .413-.004l.614-.614a.294.294 0 0 0 .004-.413L8.032 7l1.4-1.401a.29.29 0 0 0-.004-.413l-.614-.614a.294.294 0 0 0-.413-.004L7 5.968z"/></g></symbol><symbol viewBox="0 0 22 22" id="status_failed_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M11 9.38L8.798 7.178a.455.455 0 0 0-.65.006l-.964.965a.462.462 0 0 0-.006.65L9.38 11l-2.202 2.202a.455.455 0 0 0 .006.65l.965.964a.462.462 0 0 0 .65.006L11 12.62l2.202 2.202a.455.455 0 0 0 .65-.006l.964-.965a.462.462 0 0 0 .006-.65L12.62 11l2.202-2.202a.455.455 0 0 0-.006-.65l-.965-.964a.462.462 0 0 0-.65-.006L11 9.38z"/></symbol><symbol viewBox="0 0 14 14" id="status_manual" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M10.5 7.63V6.37l-.787-.13c-.044-.175-.132-.349-.263-.61l.481-.652-.918-.913-.657.478a2.346 2.346 0 0 0-.612-.26L7.656 3.5H6.388l-.132.783c-.219.043-.394.13-.612.26l-.657-.478-.918.913.437.652c-.131.218-.175.392-.262.61l-.744.086v1.261l.787.13c.044.218.132.392.263.61l-.438.651.92.913.655-.434c.175.086.394.173.613.26l.131.783h1.313l.131-.783c.219-.043.394-.13.613-.26l.656.478.918-.913-.48-.652c.13-.218.218-.435.262-.61l.656-.13zM7 8.283a1.285 1.285 0 0 1-1.313-1.305c0-.739.57-1.304 1.313-1.304.744 0 1.313.565 1.313 1.304 0 .74-.57 1.305-1.313 1.305z"/></g></symbol><symbol viewBox="0 0 22 22" id="status_manual_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M16.5 11.99v-1.98l-1.238-.206c-.068-.273-.206-.546-.412-.956l.756-1.025-1.444-1.435-1.03.752a3.686 3.686 0 0 0-.963-.41L12.03 5.5h-1.994l-.206 1.23c-.343.068-.618.205-.962.41l-1.031-.752-1.444 1.435.687 1.025c-.206.341-.275.615-.412.956L5.5 9.941v1.981l1.237.205c.07.342.207.615.413.957l-.688 1.025 1.444 1.434 1.032-.683c.274.137.618.274.962.41l.206 1.23h2.063l.206-1.23c.344-.068.619-.205.963-.41l1.03.752 1.444-1.435-.756-1.025c.207-.341.344-.683.413-.956l1.031-.205zM11 13.017c-1.169 0-2.063-.889-2.063-2.05 0-1.162.894-2.05 2.063-2.05s2.063.888 2.063 2.05c0 1.161-.894 2.05-2.063 2.05z"/></symbol><symbol viewBox="0 0 22 22" id="status_notfound_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M12.822 11.29c.816-.581 1.421-1.348 1.683-2.322.603-2.243-.973-4.553-3.53-4.553-1.15 0-2.085.41-2.775 1.089-.42.413-.672.835-.8 1.167a1.179 1.179 0 0 0 2.2.847c.016-.043.1-.184.252-.334.264-.259.613-.412 1.123-.412.938 0 1.47.78 1.254 1.584-.105.39-.37.726-.773 1.012a3.25 3.25 0 0 1-.945.47 1.179 1.179 0 0 0-.874 1.138v2.234a1.179 1.179 0 1 0 2.358 0v-1.43a5.9 5.9 0 0 0 .827-.492z"/><ellipse cx="10.825" cy="16.711" rx="1.275" ry="1.322"/></symbol><symbol viewBox="0 0 14 14" id="status_open" xmlns="http://www.w3.org/2000/svg"><path d="M0 7c0-3.866 3.142-7 7-7 3.866 0 7 3.142 7 7 0 3.866-3.142 7-7 7-3.866 0-7-3.142-7-7z"/><path d="M1 7c0 3.309 2.69 6 6 6 3.309 0 6-2.69 6-6 0-3.309-2.69-6-6-6-3.309 0-6 2.69-6 6z" fill="#FFF"/><path d="M7 9.219a2.218 2.218 0 1 0 0-4.436A2.218 2.218 0 0 0 7 9.22zm0 1.12a3.338 3.338 0 1 1 0-6.676 3.338 3.338 0 0 1 0 6.676z"/></symbol><symbol viewBox="0 0 14 14" id="status_pending" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M4.7 5.3c0-.2.1-.3.3-.3h.9c.2 0 .3.1.3.3v3.4c0 .2-.1.3-.3.3H5c-.2 0-.3-.1-.3-.3V5.3m3 0c0-.2.1-.3.3-.3h.9c.2 0 .3.1.3.3v3.4c0 .2-.1.3-.3.3H8c-.2 0-.3-.1-.3-.3V5.3"/></g></symbol><symbol viewBox="0 0 22 22" id="status_pending_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M7.386 8.329c0-.315.157-.472.471-.472h1.414c.315 0 .472.157.472.472v5.342c0 .315-.157.472-.472.472H7.857c-.314 0-.471-.157-.471-.472V8.33m4.714 0c0-.315.157-.472.471-.472h1.415c.314 0 .471.157.471.472v5.342c0 .315-.157.472-.471.472H12.57c-.314 0-.471-.157-.471-.472V8.33"/></symbol><symbol viewBox="0 0 14 14" id="status_running" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M7 3c2.2 0 4 1.8 4 4s-1.8 4-4 4c-1.3 0-2.5-.7-3.3-1.7L7 7V3"/></g></symbol><symbol viewBox="0 0 22 22" id="status_running_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M11 4.714c3.457 0 6.286 2.829 6.286 6.286 0 3.457-2.829 6.286-6.286 6.286-2.043 0-3.929-1.1-5.186-2.672L11 11V4.714"/></symbol><symbol viewBox="0 0 14 14" id="status_skipped" xmlns="http://www.w3.org/2000/svg"><path d="M7 14A7 7 0 1 1 7 0a7 7 0 0 1 0 14z"/><path d="M7 13A6 6 0 1 0 7 1a6 6 0 0 0 0 12z" fill="#FFF"/><path d="M6.415 7.04L4.579 5.203a.295.295 0 0 1 .004-.416l.349-.349a.29.29 0 0 1 .416-.004l2.214 2.214a.289.289 0 0 1 .019.021l.132.133c.11.11.108.291 0 .398L5.341 9.573a.282.282 0 0 1-.398 0l-.331-.331a.285.285 0 0 1 0-.399L6.415 7.04zm2.54 0L7.119 5.203a.295.295 0 0 1 .004-.416l.349-.349a.29.29 0 0 1 .416-.004l2.214 2.214a.289.289 0 0 1 .019.021l.132.133c.11.11.108.291 0 .398L7.881 9.573a.282.282 0 0 1-.398 0l-.331-.331a.285.285 0 0 1 0-.399L8.955 7.04z"/></symbol><symbol viewBox="0 0 22 22" id="status_skipped_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M14.072 11.063l-2.82 2.82a.46.46 0 0 0-.001.652l.495.495a.457.457 0 0 0 .653-.001l3.7-3.7a.46.46 0 0 0 .001-.653l-.196-.196a.453.453 0 0 0-.03-.033l-3.479-3.479a.464.464 0 0 0-.654.007l-.548.548a.463.463 0 0 0-.007.654l2.886 2.886z"/><path d="M10.08 11.063l-2.819 2.82a.46.46 0 0 0-.002.652l.496.495a.457.457 0 0 0 .652-.001l3.7-3.7a.46.46 0 0 0 .002-.653l-.196-.196a.453.453 0 0 0-.03-.033l-3.48-3.479a.464.464 0 0 0-.653.007l-.548.548a.463.463 0 0 0-.007.654l2.886 2.886z"/></symbol><symbol viewBox="0 0 14 14" id="status_success" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M6.278 7.697L5.045 6.464a.296.296 0 0 0-.42-.002l-.613.614a.298.298 0 0 0 .002.42l1.91 1.909a.5.5 0 0 0 .703.005l.265-.265L9.997 6.04a.291.291 0 0 0-.009-.408l-.614-.614a.29.29 0 0 0-.408-.009L6.278 7.697z"/></g></symbol><symbol viewBox="0 0 22 22" id="status_success_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M9.866 12.095l-1.95-1.95a.462.462 0 0 0-.647.01l-.964.964a.46.46 0 0 0-.01.646l3.013 3.014a.787.787 0 0 0 1.106.008l.425-.425 4.854-4.853a.462.462 0 0 0 .002-.659l-.964-.964a.468.468 0 0 0-.658.002l-4.207 4.207z"/></symbol><symbol viewBox="0 0 14 14" id="status_success_solid" xmlns="http://www.w3.org/2000/svg"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7zm6.278.697L5.045 6.464a.296.296 0 0 0-.42-.002l-.613.614a.298.298 0 0 0 .002.42l1.91 1.909a.5.5 0 0 0 .703.005l.265-.265L9.997 6.04a.291.291 0 0 0-.009-.408l-.614-.614a.29.29 0 0 0-.408-.009L6.278 7.697z" fill-rule="evenodd"/></symbol><symbol viewBox="0 0 14 14" id="status_warning" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M6 3.5c0-.3.2-.5.5-.5h1c.3 0 .5.2.5.5v4c0 .3-.2.5-.5.5h-1c-.3 0-.5-.2-.5-.5v-4m0 6c0-.3.2-.5.5-.5h1c.3 0 .5.2.5.5v1c0 .3-.2.5-.5.5h-1c-.3 0-.5-.2-.5-.5v-1"/></g></symbol><symbol viewBox="0 0 22 22" id="status_warning_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M9.429 5.5c0-.471.314-.786.785-.786h1.572c.471 0 .785.315.785.786v6.286c0 .471-.314.785-.785.785h-1.572c-.471 0-.785-.314-.785-.785V5.5m0 9.429c0-.472.314-.786.785-.786h1.572c.471 0 .785.314.785.786V16.5c0 .471-.314.786-.785.786h-1.572c-.471 0-.785-.315-.785-.786v-1.571"/></symbol><symbol viewBox="0 0 16 16" id="stop" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M2 0h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2z"/></symbol><symbol viewBox="0 0 16 16" id="task-done" xmlns="http://www.w3.org/2000/svg"><path d="M7.536 8.657l2.828-2.829a1 1 0 0 1 1.414 1.415l-3.535 3.535a.997.997 0 0 1-1.415 0l-2.12-2.121A1 1 0 0 1 6.12 7.243l1.415 1.414zM3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3z"/></symbol><symbol viewBox="0 0 16 16" id="template" xmlns="http://www.w3.org/2000/svg"><path d="M3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3zm.8 2h2.4a.8.8 0 0 1 .8.8v1.4a.8.8 0 0 1-.8.8H3.8a.8.8 0 0 1-.8-.8V4.8a.8.8 0 0 1 .8-.8zm4.7 0h4a.5.5 0 1 1 0 1h-4a.5.5 0 0 1 0-1zm0 2h4a.5.5 0 1 1 0 1h-4a.5.5 0 0 1 0-1zm-5 3h9a.5.5 0 1 1 0 1h-9a.5.5 0 0 1 0-1zm0 2h9a.5.5 0 1 1 0 1h-9a.5.5 0 1 1 0-1z"/></symbol><symbol viewBox="0 0 16 16" id="terminal" xmlns="http://www.w3.org/2000/svg"><path d="M7 8a.997.997 0 0 1-.293.707l-1.414 1.414a1 1 0 1 1-1.414-1.414L4.586 8l-.707-.707a1 1 0 1 1 1.414-1.414l1.414 1.414A.997.997 0 0 1 7 8zM4 0h8a4 4 0 0 1 4 4v8a4 4 0 0 1-4 4H4a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4zm0 2a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2H4zm5 7h2a1 1 0 0 1 0 2H9a1 1 0 0 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="thumb-down" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8.33 11h5.282a2 2 0 0 0 1.963-2.38l-.563-2.905a3 3 0 0 0-.243-.732l-1.103-2.286A3 3 0 0 0 10.964 1H7a3 3 0 0 0-3 3v6.3a2 2 0 0 0 .436 1.247l3.11 3.9a.632.632 0 0 0 .941.053l.137-.137a1 1 0 0 0 .28-.87L8.329 11zM1 10h2V3H1a1 1 0 0 0-1 1v5a1 1 0 0 0 1 1z"/></symbol><symbol viewBox="0 0 16 16" id="thumb-up" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8.33 5h5.282a2 2 0 0 1 1.963 2.38l-.563 2.905a3 3 0 0 1-.243.732l-1.103 2.286A3 3 0 0 1 10.964 15H7a3 3 0 0 1-3-3V5.7a2 2 0 0 1 .436-1.247l3.11-3.9A.632.632 0 0 1 8.487.5l.137.137a1 1 0 0 1 .28.87L8.329 5zM1 6h2v7H1a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1z"/></symbol><symbol viewBox="0 0 16 16" id="thumbtack" xmlns="http://www.w3.org/2000/svg"><path d="M7.125 9h-2.19a.5.5 0 0 1-.417-.777L6 6V2L5.362.724A.5.5 0 0 1 5.809 0h4.382a.5.5 0 0 1 .447.724L10 2v4l1.482 2.223a.5.5 0 0 1-.416.777H8.875L8 16l-.875-7z" fill-rule="evenodd"/></symbol><symbol viewBox="0 0 16 16" id="timer" xmlns="http://www.w3.org/2000/svg"><path d="M12.022 3.27l.77-.77a1 1 0 0 1 1.415 1.414l-.728.729a7 7 0 1 1-1.456-1.372zM8 14A5 5 0 1 0 8 4a5 5 0 0 0 0 10zm0-9a1 1 0 0 1 1 1v2a1 1 0 1 1-2 0V6a1 1 0 0 1 1-1zM6 0h4a1 1 0 0 1 0 2H6a1 1 0 1 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="todo-add" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10 4V2a1 1 0 0 1 2 0v2h2a1 1 0 0 1 0 2h-2v2a1 1 0 0 1-2 0V6H8a1 1 0 1 1 0-2h2zm2 7a1 1 0 0 1 2 0v2a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3h2a1 1 0 1 1 0 2H3a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1v-2z"/></symbol><symbol viewBox="0 0 16 16" id="todo-done" xmlns="http://www.w3.org/2000/svg"><path d="M8.243 7.485l4.95-4.95a1 1 0 1 1 1.414 1.415L8.95 9.607a.997.997 0 0 1-1.414 0L4.707 6.778a1 1 0 0 1 1.414-1.414l2.122 2.121zM12 11a1 1 0 0 1 2 0v2a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3h2a1 1 0 1 1 0 2H3a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1v-2z"/></symbol><symbol viewBox="0 0 16 16" id="token" xmlns="http://www.w3.org/2000/svg"><path d="M3 2h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V5a1 1 0 0 0-1-1H3zm1 5a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm4 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm4 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="unapproval" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M11.95 8.536l1.06-1.061a1 1 0 0 1 1.415 1.414l-1.061 1.06 1.06 1.061a1 1 0 0 1-1.414 1.415l-1.06-1.061-1.06 1.06a1 1 0 1 1-1.415-1.414l1.06-1.06-1.06-1.06a1 1 0 0 1 1.414-1.415l1.06 1.06zm-3.768-.33c.006.503.201 1.006.586 1.39l.353.354-.353.353a2 2 0 1 0 2.828 2.829l.354-.354.047.048C11.964 14.363 11.527 15 6 15c-5.924 0-6-.78-6-2.52S.964 8 6 8c.834 0 1.557.074 2.182.205zM5.976 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6z"/></symbol><symbol viewBox="0 0 16 16" id="unassignee" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M11 5h4a1 1 0 0 1 0 2h-4a1 1 0 0 1 0-2zM5.976 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6zM6 15c-5.924 0-6-.78-6-2.52S.964 8 6 8s6 2.692 6 4.48c0 1.788-.076 2.52-6 2.52z"/></symbol><symbol viewBox="0 0 16 16" id="unlink" xmlns="http://www.w3.org/2000/svg"><path d="M11.295 8.845l-.659-1.664a1.78 1.78 0 0 0 .04-.04l1.415-1.414c.586-.586.654-1.468.152-1.97s-1.384-.434-1.97.152L8.859 5.323a1.781 1.781 0 0 0-.04.04l-1.664-.658c.141-.208.305-.408.491-.594l1.415-1.414c1.366-1.367 3.424-1.525 4.596-.354 1.171 1.172 1.013 3.23-.354 4.596L11.89 8.354c-.186.186-.386.35-.594.491zm-2.45 2.45a4.075 4.075 0 0 1-.491.594l-1.415 1.414c-1.366 1.367-3.424 1.525-4.596.354-1.171-1.172-1.013-3.23.354-4.596L4.11 7.646c.186-.186.386-.35.594-.491l.659 1.664a1.781 1.781 0 0 0-.04.04l-1.415 1.414c-.586.586-.654 1.468-.152 1.97s1.384.434 1.97-.152l1.414-1.414a1.78 1.78 0 0 0 .04-.04l1.664.658zm3.812-2.088h2a.5.5 0 0 1 .5.5v.05a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1-.5-.5v-.05a.5.5 0 0 1 .5-.5zm-.384 2.116l1.415 1.414a.5.5 0 0 1 0 .708l-.037.036a.5.5 0 0 1-.707 0l-1.414-1.414a.5.5 0 0 1 0-.707l.036-.037a.5.5 0 0 1 .707 0zm-2.823 1.09a.5.5 0 0 1 .5-.5h.052a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.5.5H9.95a.5.5 0 0 1-.5-.5v-2zm-2.748-9.16a.5.5 0 0 1-.5.5h-.05a.5.5 0 0 1-.5-.5v-2a.5.5 0 0 1 .5-.5h.05a.5.5 0 0 1 .5.5v2zm-2.116.383a.5.5 0 0 1 0 .707l-.036.036a.5.5 0 0 1-.707 0L2.428 2.965a.5.5 0 0 1 0-.707l.037-.036a.5.5 0 0 1 .707 0l1.414 1.414zm-1.09 2.823h-2a.5.5 0 0 1-.5-.5v-.051a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 .5.5v.05a.5.5 0 0 1-.5.5z"/></symbol><symbol viewBox="0 0 16 16" id="user" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6zm0 8c-6.888 0-6.976-.78-6.976-2.52S2.144 8 8 8s6.976 2.692 6.976 4.48c0 1.788-.088 2.52-6.976 2.52z"/></symbol><symbol viewBox="0 0 16 16" id="users" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10.521 8.01C15.103 8.19 16 10.755 16 12.48c0 1.533-.056 2.29-3.808 2.475.609-.54.808-1.331.808-2.475 0-1.911-.804-3.503-2.479-4.47zm-1.67-1.228A3.987 3.987 0 0 0 9.976 4a3.987 3.987 0 0 0-1.125-2.782 3 3 0 1 1 0 5.563zM5.976 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6zM6 15c-5.924 0-6-.78-6-2.52S.964 8 6 8s6 2.692 6 4.48c0 1.788-.076 2.52-6 2.52z"/></symbol><symbol viewBox="0 0 16 16" id="volume-up" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M1 5h1v6H1a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1zm2 0l4.445-2.964A1 1 0 0 1 9 2.87v10.26a1 1 0 0 1-1.555.833L3 11V5zm10.283 7.89a.5.5 0 0 1-.66-.752A5.485 5.485 0 0 0 14.5 8c0-1.601-.687-3.09-1.865-4.128a.5.5 0 0 1 .661-.75A6.484 6.484 0 0 1 15.5 8a6.485 6.485 0 0 1-2.217 4.89zm-2.002-2.236a.5.5 0 1 1-.652-.758c.55-.472.871-1.157.871-1.896 0-.732-.315-1.411-.856-1.883a.5.5 0 0 1 .658-.753A3.492 3.492 0 0 1 12.5 8c0 1.033-.45 1.994-1.219 2.654z"/></symbol><symbol viewBox="0 0 16 16" id="warning" xmlns="http://www.w3.org/2000/svg"><path d="M15.34 10.479A3 3 0 0 1 12.756 15h-9.51A3 3 0 0 1 .66 10.479l4.755-8.083a3 3 0 0 1 5.172 0l4.755 8.083zm-6.478-7.07a1 1 0 0 0-1.724 0l-4.755 8.084A1 1 0 0 0 3.245 13h9.51a1 1 0 0 0 .862-1.507L8.862 3.41zM8 5a1 1 0 0 1 1 1v2a1 1 0 1 1-2 0V6a1 1 0 0 1 1-1zm0 7a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="work" xmlns="http://www.w3.org/2000/svg"><path d="M12 3h1a3 3 0 0 1 3 3v7a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V6a3 3 0 0 1 3-3h1V2a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v1zM6 2v1h4V2H6zM3 5a1 1 0 0 0-1 1v7a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V6a1 1 0 0 0-1-1H3zm1.5 1a.5.5 0 0 1 .5.5v6a.5.5 0 1 1-1 0v-6a.5.5 0 0 1 .5-.5zm7 0a.5.5 0 0 1 .5.5v6a.5.5 0 1 1-1 0v-6a.5.5 0 0 1 .5-.5z"/></symbol></svg>
\ No newline at end of file +<?xml version="1.0" encoding="utf-8"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><symbol viewBox="0 0 16 16" id="abuse" xmlns="http://www.w3.org/2000/svg"><path d="M11.408.328l4.029 3.222A1.5 1.5 0 0 1 16 4.72v6.555a1.5 1.5 0 0 1-.563 1.171l-4.026 3.224a1.5 1.5 0 0 1-.937.329H5.529a1.5 1.5 0 0 1-.937-.328L.563 12.45A1.5 1.5 0 0 1 0 11.28V4.724a1.5 1.5 0 0 1 .563-1.171L4.589.329A1.5 1.5 0 0 1 5.526 0h4.945c.34 0 .67.116.937.328zM10.296 2H5.702L2 4.964v6.074L5.704 14h4.594L14 11.036V4.962L10.296 2zM8 4a1 1 0 0 1 1 1v3a1 1 0 1 1-2 0V5a1 1 0 0 1 1-1zm0 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="account" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M9.195 9.965l-.568-.875a.25.25 0 0 1 .015-.294l.405-.5a.25.25 0 0 1 .283-.075l.938.36c.257-.183.543-.325.851-.42l.322-.988A.25.25 0 0 1 11.679 7h.642a.25.25 0 0 1 .238.173l.322.988c.308.095.594.237.851.42l.938-.36a.25.25 0 0 1 .283.076l.405.5a.25.25 0 0 1 .015.293l-.568.875c.113.297.18.616.193.95l.898.54a.25.25 0 0 1 .115.27l-.144.626a.25.25 0 0 1-.222.193l-1.115.098a3.015 3.015 0 0 1-.512.608l.165 1.18a.25.25 0 0 1-.138.259l-.577.281a.25.25 0 0 1-.29-.05l-.874-.905a3.035 3.035 0 0 1-.608 0l-.875.904a.25.25 0 0 1-.289.051l-.577-.281a.25.25 0 0 1-.138-.26l.165-1.18a3.015 3.015 0 0 1-.512-.607l-1.115-.098a.25.25 0 0 1-.222-.193l-.144-.626a.25.25 0 0 1 .115-.27l.898-.54c.013-.334.08-.653.193-.95zM6.789 8.023A12.845 12.845 0 0 0 6 8c-5.036 0-6 2.74-6 4.48C0 14.22.076 15 6 15c.553 0 1.055-.006 1.51-.02A5.977 5.977 0 0 1 6 11c0-1.083.287-2.1.79-2.977zM5.976 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6zM12 12a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="admin" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M13.162 2.5a3.5 3.5 0 0 1-3.163 5.479L6.08 14.766a1.5 1.5 0 0 1-2.598-1.5L7.4 6.479A3.5 3.5 0 0 1 10.564 1L8.9 3.88l2.599 1.5 1.663-2.88zm-8.63 11.949a.5.5 0 1 0 .5-.866.5.5 0 0 0-.5.866z"/></symbol><symbol viewBox="0 0 16 16" id="angle-double-left" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10.414 7.95l4.243-4.243a1 1 0 0 0-1.414-1.414l-4.95 4.95a.997.997 0 0 0 0 1.414l4.95 4.95a1 1 0 1 0 1.414-1.415L10.414 7.95zm-7 0l4.243-4.243a1 1 0 0 0-1.414-1.414l-4.95 4.95a.997.997 0 0 0 0 1.414l4.95 4.95a1 1 0 0 0 1.414-1.415L3.414 7.95z"/></symbol><symbol viewBox="0 0 16 16" id="angle-double-right" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M5.536 7.95L1.293 3.707a1 1 0 0 1 1.414-1.414l4.95 4.95a.997.997 0 0 1 0 1.414l-4.95 4.95a1 1 0 1 1-1.414-1.415L5.536 7.95zm7 0L8.293 3.707a1 1 0 0 1 1.414-1.414l4.95 4.95a.997.997 0 0 1 0 1.414l-4.95 4.95a1 1 0 0 1-1.414-1.415l4.243-4.242z"/></symbol><symbol viewBox="0 0 16 16" id="angle-down" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8 10.243l-4.95-4.95a1 1 0 0 0-1.414 1.414l5.657 5.657a.997.997 0 0 0 1.414 0l5.657-5.657a1 1 0 0 0-1.414-1.414L8 10.243z"/></symbol><symbol viewBox="0 0 16 16" id="angle-left" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M5.757 8l4.95-4.95a1 1 0 1 0-1.414-1.414L3.636 7.293a.997.997 0 0 0 0 1.414l5.657 5.657a1 1 0 0 0 1.414-1.414L5.757 8z"/></symbol><symbol viewBox="0 0 16 16" id="angle-right" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10.243 8l-4.95-4.95a1 1 0 0 1 1.414-1.414l5.657 5.657a.997.997 0 0 1 0 1.414l-5.657 5.657a1 1 0 0 1-1.414-1.414L10.243 8z"/></symbol><symbol viewBox="0 0 16 16" id="angle-up" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8 6.757l-4.95 4.95a1 1 0 1 1-1.414-1.414l5.657-5.657a.997.997 0 0 1 1.414 0l5.657 5.657a1 1 0 0 1-1.414 1.414L8 6.757z"/></symbol><symbol viewBox="0 0 16 16" id="appearance" xmlns="http://www.w3.org/2000/svg"><path d="M11.161 12.456l.232.121c.1.053.175.094.249.137.53.318.844.75.857 1.402.012 1.397-1.116 1.756-3.12 1.858a23.85 23.85 0 0 1-1.38.026A8 8 0 0 1 0 8a8 8 0 0 1 8-8c4.417 0 7.998 3.582 7.998 7.977.06 2.621-1.312 3.586-4.48 3.648-.602.008-1.068.043-1.4.104.228.192.598.47 1.043.727zm-3.287-.943c-.019-1.495 1.228-1.856 3.611-1.888C13.67 9.582 14.028 9.33 13.998 8A6 6 0 1 0 8 14c.603 0 .91-.004 1.277-.023a9.7 9.7 0 0 0 .478-.035c-1.172-.738-1.868-1.47-1.88-2.43zM6 5a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm6 3a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm-2-3a1 1 0 1 1 0-2 1 1 0 0 1 0 2zM4 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="applications" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M1 0h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H1a1 1 0 0 1-1-1V1a1 1 0 0 1 1-1zm0 6h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H1a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1zm6-6h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1V1a1 1 0 0 1 1-1zm0 1v2h2V1H7zm0 5h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1zm6-6h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1h-2a1 1 0 0 1-1-1V1a1 1 0 0 1 1-1zm0 6h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1h-2a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1zm0 1v2h2V7h-2zM1 12h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H1a1 1 0 0 1-1-1v-2a1 1 0 0 1 1-1zm0 1v2h2v-2H1zm6-1h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1v-2a1 1 0 0 1 1-1zm6 0h2a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1h-2a1 1 0 0 1-1-1v-2a1 1 0 0 1 1-1z"/></symbol><symbol viewBox="0 0 16 16" id="approval" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10.536 10.657l2.828-2.829a1 1 0 0 1 1.414 1.415l-3.535 3.535a.997.997 0 0 1-1.415 0l-2.12-2.121A1 1 0 1 1 9.12 9.243l1.415 1.414zM7.632 8.109A2 2 0 0 0 7 11.364l2.121 2.121a1.996 1.996 0 0 0 2.807.021C11.686 14.554 10.627 15 6 15c-5.924 0-6-.78-6-2.52S.964 8 6 8c.6 0 1.142.038 1.632.109zM5.976 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6z"/></symbol><symbol viewBox="0 0 16 16" id="arrow-down" xmlns="http://www.w3.org/2000/svg"><path d="M10.472 7.282a.862.862 0 0 1 1.26-.006c.357.364.357.958 0 1.285L8.627 11.73A.886.886 0 0 1 8 12a.849.849 0 0 1-.627-.27L4.275 8.561a.904.904 0 0 1-.013-1.285.861.861 0 0 1 1.26-.007l2.486 2.527z"/></symbol><symbol viewBox="0 0 16 16" id="arrow-right" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M9 6H2a2 2 0 1 0 0 4h7v2.586a1 1 0 0 0 1.707.707l4.586-4.586a1 1 0 0 0 0-1.414l-4.586-4.586A1 1 0 0 0 9 3.414V6z"/></symbol><symbol viewBox="0 0 16 16" id="assignee" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M12 5V4a1 1 0 0 1 2 0v1h1a1 1 0 0 1 0 2h-1v1a1 1 0 0 1-2 0V7h-1a1 1 0 0 1 0-2h1zM5.976 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6zM6 15c-5.924 0-6-.78-6-2.52S.964 8 6 8s6 2.692 6 4.48c0 1.788-.076 2.52-6 2.52z"/></symbol><symbol viewBox="0 0 16 16" id="bold" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M4 12.5v-9A1.5 1.5 0 0 1 5.5 2h2.104c2.182 0 3.879.681 3.879 2.982 0 1.067-.517 2.227-1.374 2.595v.073C11.176 7.963 12 8.865 12 10.466 12 12.914 10.19 14 7.911 14H5.5A1.5 1.5 0 0 1 4 12.5zm2.376-5.696H7.49c1.164 0 1.665-.552 1.665-1.417 0-.94-.534-1.289-1.649-1.289h-1.13v2.706zm0 5.098h1.341c1.293 0 1.956-.515 1.956-1.62 0-1.049-.647-1.472-1.956-1.472H6.376v3.092z"/></symbol><symbol viewBox="0 0 16 16" id="book" xmlns="http://www.w3.org/2000/svg"><path d="M7 2H5a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2v4.191a.5.5 0 0 1-.724.447l-1.052-.526a.5.5 0 0 0-.448 0l-1.052.526A.5.5 0 0 1 7 6.191V2zM5 0h6a4 4 0 0 1 4 4v8a4 4 0 0 1-4 4H5a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4z"/></symbol><symbol viewBox="0 0 16 16" id="branch" xmlns="http://www.w3.org/2000/svg"><path d="M6 11.978v.29a2 2 0 1 1-2 0V3.732a2 2 0 1 1 2 0v3.849c.592-.491 1.31-.854 2.15-1.081 1.308-.353 1.875-.882 1.893-1.743a2 2 0 1 1 2.002-.051C12.053 6.54 10.857 7.84 8.67 8.43 7.056 8.867 6.195 9.98 6 11.978zM5 3a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm6 1a1 1 0 1 0 0-2 1 1 0 0 0 0 2zM5 15a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="bullhorn" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M6.143 10H7V4H3a3 3 0 1 0 0 6h.143l.734 5.141a1 1 0 0 0 .99.859h1.556a.5.5 0 0 0 .495-.57L6.143 10zM8 4c1.034.02 2.039-.274 3.014-.883.727-.455 1.836-1.334 3.328-2.637A1 1 0 0 1 16 1.233v10.764a1 1 0 0 1-1.595.803c-1.658-1.227-2.788-1.992-3.392-2.294-.781-.39-1.785-.559-3.013-.506V4z"/></symbol><symbol viewBox="0 0 16 16" id="calendar" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M12 2h2a2 2 0 0 1 2 2H0a2 2 0 0 1 2-2h2V1a1 1 0 1 1 2 0v1h4V1a1 1 0 1 1 2 0v1zM0 4h16v9a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V4zm2 2.5V13a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V6.5a.5.5 0 0 0-.5-.5h-11a.5.5 0 0 0-.5.5zM5 8h2a1 1 0 1 1 0 2H5a1 1 0 1 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="cancel" xmlns="http://www.w3.org/2000/svg"><path d="M3.11 4.523a6 6 0 0 0 8.367 8.367L3.109 4.524zM4.522 3.11l8.368 8.368A6 6 0 0 0 4.524 3.11zM8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16z"/></symbol><symbol viewBox="0 0 16 16" id="chart" xmlns="http://www.w3.org/2000/svg"><path d="M15 14a1 1 0 0 1 0 2H2a2 2 0 0 1-2-2V1a1 1 0 1 1 2 0v13h13zM3.142 8.735l2.502-2.561a.5.5 0 0 1 .714-.003L8 7.833l3.592-4.553a.5.5 0 0 1 .796.015l2.516 3.454a.5.5 0 0 1 .096.295V12.5a.5.5 0 0 1-.5.5h-11a.5.5 0 0 1-.5-.5V9.085a.5.5 0 0 1 .142-.35z"/></symbol><symbol viewBox="0 0 16 16" id="chevron-down" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8.078 8.2l3.535-3.536a2 2 0 0 1 2.828 2.828l-4.949 4.95c-.39.39-.902.586-1.414.586a1.994 1.994 0 0 1-1.414-.586l-4.95-4.95a2 2 0 1 1 2.828-2.828l3.536 3.535z"/></symbol><symbol viewBox="0 0 16 16" id="chevron-left" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M7.977 7.998l3.535-3.535a2 2 0 1 0-2.828-2.828l-4.95 4.949c-.39.39-.586.902-.586 1.414 0 .512.196 1.024.586 1.414l4.95 4.95a2 2 0 1 0 2.828-2.828L7.977 7.998z"/></symbol><symbol viewBox="0 0 16 16" id="chevron-right" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8.22 7.998L4.683 4.463a2 2 0 0 1 2.828-2.828l4.95 4.949c.39.39.586.902.586 1.414a1.99 1.99 0 0 1-.586 1.414l-4.95 4.95a2 2 0 0 1-2.828-2.828l3.535-3.536z"/></symbol><symbol viewBox="0 0 16 16" id="chevron-up" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M7.778 8.957l3.535 3.535a2 2 0 1 0 2.828-2.828l-4.949-4.95a1.994 1.994 0 0 0-1.414-.586c-.512 0-1.024.196-1.414.586l-4.95 4.95a2 2 0 1 0 2.828 2.828l3.536-3.535z"/></symbol><symbol viewBox="0 0 16 16" id="clock" xmlns="http://www.w3.org/2000/svg"><path d="M9 7h1a1 1 0 0 1 0 2H8a.997.997 0 0 1-1-1V5a1 1 0 1 1 2 0v2zm-1 9A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12z"/></symbol><symbol viewBox="0 0 16 16" id="close" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M9.414 8l4.95-4.95a1 1 0 0 0-1.414-1.414L8 6.586l-4.95-4.95A1 1 0 0 0 1.636 3.05L6.586 8l-4.95 4.95a1 1 0 1 0 1.414 1.414L8 9.414l4.95 4.95a1 1 0 1 0 1.414-1.414L9.414 8z"/></symbol><symbol viewBox="0 0 16 16" id="code" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M15.871 8.243a.997.997 0 0 0-.293-.707L12.75 4.707a1 1 0 0 0-1.414 1.414l2.12 2.122-2.12 2.121a1 1 0 0 0 1.414 1.414l2.828-2.828a.997.997 0 0 0 .293-.707zm-13.243 0L4.75 6.12a1 1 0 1 0-1.414-1.414L.507 7.536a.997.997 0 0 0 0 1.414l2.829 2.828a1 1 0 1 0 1.414-1.414L2.628 8.243zm6.407-4.107a1 1 0 0 1 .707 1.225L8.19 11.157a1 1 0 1 1-1.931-.518L7.81 4.843a1 1 0 0 1 1.224-.707z"/></symbol><symbol viewBox="0 0 9 13" id="collapse"><path d="M.084.25C.01.18-.015.12.008.071.031.024.093 0 .194 0h8.521c.1 0 .162.024.185.072.023.048-.002.107-.075.177l-4.11 3.935a.372.372 0 0 1-.11.072h-.301a.508.508 0 0 1-.11-.072L.084.249zM.377 6.88a.364.364 0 0 1-.26-.105.334.334 0 0 1-.11-.25v-.709c0-.096.036-.179.11-.249a.364.364 0 0 1 .26-.105h8.15c.101 0 .188.035.261.105.074.07.11.153.11.25v.709c0 .096-.036.179-.11.249a.364.364 0 0 1-.26.105H.377zM.084 12.132c-.074.07-.099.129-.076.177.023.048.085.072.186.072h8.521c.1 0 .162-.024.185-.072.023-.048-.002-.107-.075-.177l-4.11-3.935a.372.372 0 0 0-.11-.072h-.301a.508.508 0 0 0-.11.072l-4.11 3.935z"/></symbol><symbol viewBox="0 0 16 16" id="comment" xmlns="http://www.w3.org/2000/svg"><path d="M1.707 15.707C1.077 16.337 0 15.891 0 15V3a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H5.414l-3.707 3.707zM2 12.586l2.293-2.293A1 1 0 0 1 5 10h8a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v9.586z"/></symbol><symbol viewBox="0 0 16 16" id="comment-dots" xmlns="http://www.w3.org/2000/svg"><path d="M1.707 15.707C1.077 16.337 0 15.891 0 15V3a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H5.414l-3.707 3.707zM2 12.586l2.293-2.293A1 1 0 0 1 5 10h8a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v9.586zM5 7a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm3 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm3 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="comment-next" xmlns="http://www.w3.org/2000/svg"><path d="M8 5V4a.5.5 0 0 1 .8-.4l2.667 2a.5.5 0 0 1 0 .8L8.8 8.4A.5.5 0 0 1 8 8V7H6a1 1 0 1 1 0-2h2zM1.707 15.707C1.077 16.337 0 15.891 0 15V3a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H5.414l-3.707 3.707zM2 12.586l2.293-2.293A1 1 0 0 1 5 10h8a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v9.586z"/></symbol><symbol viewBox="0 0 16 16" id="comments" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M3.75 10L0 13V3a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2H3.75zM13 5h1a2 2 0 0 1 2 2v8l-2.667-2H8a2 2 0 0 1-2-2h4a3 3 0 0 0 3-3V5z"/></symbol><symbol viewBox="0 0 16 16" id="commit" xmlns="http://www.w3.org/2000/svg"><path d="M8 10a2 2 0 1 0 0-4 2 2 0 0 0 0 4zm3.876-1.008a4.002 4.002 0 0 1-7.752 0A1.01 1.01 0 0 1 4 9H1a1 1 0 1 1 0-2h3c.042 0 .083.003.124.008a4.002 4.002 0 0 1 7.752 0A1.01 1.01 0 0 1 12 7h3a1 1 0 0 1 0 2h-3a1.01 1.01 0 0 1-.124-.008z"/></symbol><symbol viewBox="0 0 16 16" id="credit-card" xmlns="http://www.w3.org/2000/svg"><path d="M14 5a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1h12zm0 3H2v3a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V8zM3 2h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3zm6.5 8h3a.5.5 0 1 1 0 1h-3a.5.5 0 1 1 0-1z"/></symbol><symbol viewBox="0 0 16 16" id="cut" xmlns="http://www.w3.org/2000/svg"><rect width="16" height="2" y="7" fill-rule="evenodd" rx="1"/></symbol><symbol viewBox="0 0 16 16" id="dashboard" xmlns="http://www.w3.org/2000/svg"><path d="M7.709 10.021l.696-2.6a.5.5 0 0 1 .966.26l-.657 2.45A2 2 0 0 1 10 12H6a2 2 0 0 1 1.709-1.979zM0 8.9a8 8 0 0 1 15.998 0H16v3.6a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5V8.9zM14 9A6 6 0 1 0 2 9v3.5a.5.5 0 0 0 .5.5h11a.5.5 0 0 0 .5-.5V9zM3.5 9a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm9 0a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm-7-3a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm5 0a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1z"/></symbol><symbol viewBox="0 0 16 16" id="disk" xmlns="http://www.w3.org/2000/svg"><path d="M16 11.764V3a3 3 0 0 0-3-3H3a3 3 0 0 0-3 3v8.764A2.989 2.989 0 0 1 2 11V3a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v8c.768 0 1.47.289 2 .764zM2 12h12a2 2 0 1 1 0 4H2a2 2 0 1 1 0-4zm10 1a1 1 0 1 0 0 2 1 1 0 0 0 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="doc_code" xmlns="http://www.w3.org/2000/svg"><path d="M8 2H5a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V7h-3a2 2 0 0 1-2-2V2zm2 .414V5h2.586L10 2.414zm1.036 7.607a.498.498 0 0 1-.147.354l-1.414 1.414a.5.5 0 0 1-.707-.707l1.06-1.06-1.06-1.061a.5.5 0 0 1 .707-.707l1.414 1.414a.498.498 0 0 1 .147.353zm-4.822 0l1.06 1.061a.5.5 0 0 1-.706.707l-1.414-1.414a.498.498 0 0 1 0-.707l1.414-1.414a.5.5 0 1 1 .707.707l-1.06 1.06zM5 0h4.586A2 2 0 0 1 11 .586L14.414 4A2 2 0 0 1 15 5.414V12a4 4 0 0 1-4 4H5a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4z"/></symbol><symbol viewBox="0 0 16 16" id="doc_image" xmlns="http://www.w3.org/2000/svg"><path d="M8 2H5a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V7h-3a2 2 0 0 1-2-2V2zm2 .414V5h2.586L10 2.414zM7.333 9.667l1.313-1.313a.5.5 0 0 1 .708 0L12 11H4l2.188-1.75a.5.5 0 0 1 .624 0l.521.417zM5 0h4.586A2 2 0 0 1 11 .586L14.414 4A2 2 0 0 1 15 5.414V12a4 4 0 0 1-4 4H5a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4zm.5 8a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zM4 11h8v.7a.3.3 0 0 1-.3.3H4.3a.3.3 0 0 1-.3-.3V11z"/></symbol><symbol viewBox="0 0 16 16" id="doc_text" xmlns="http://www.w3.org/2000/svg"><path d="M8 2H5a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V7h-3a2 2 0 0 1-2-2V2zm2 .414V5h2.586L10 2.414zM5 0h4.586A2 2 0 0 1 11 .586L14.414 4A2 2 0 0 1 15 5.414V12a4 4 0 0 1-4 4H5a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4zm.5 11h5a.5.5 0 1 1 0 1h-5a.5.5 0 1 1 0-1zm0-2h5a.5.5 0 1 1 0 1h-5a.5.5 0 0 1 0-1zm0-2h2a.5.5 0 0 1 0 1h-2a.5.5 0 0 1 0-1z"/></symbol><symbol viewBox="0 0 105 26" id="double-headed-arrow" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M1.018 11.089L15.138.614c1.23-.911 3.086-.795 4.147.26.461.46.715 1.045.715 1.651v20.95C20 24.869 18.684 26 17.06 26a3.238 3.238 0 0 1-1.921-.614L1.019 14.911C-.212 14-.347 12.405.714 11.35c.094-.094.195-.18.303-.261zm102.964 0c.108.08.21.167.303.26 1.061 1.056.925 2.65-.303 3.562l-14.12 10.475A3.238 3.238 0 0 1 87.94 26C86.316 26 85 24.87 85 23.475V2.525c0-.606.254-1.192.715-1.65 1.061-1.056 2.917-1.172 4.146-.26l14.12 10.474zM35 17a4 4 0 1 1 0-8 4 4 0 0 1 0 8zm18 0a4 4 0 1 1 0-8 4 4 0 0 1 0 8zm18 0a4 4 0 1 1 0-8 4 4 0 0 1 0 8z"/></symbol><symbol viewBox="0 0 16 16" id="download" xmlns="http://www.w3.org/2000/svg"><path d="M9 12h1a.5.5 0 0 1 .4.8l-2 2.667a.5.5 0 0 1-.8 0l-2-2.667A.5.5 0 0 1 6 12h1V8a1 1 0 1 1 2 0v4zM4 9a1 1 0 1 1 0 2 4 4 0 0 1-1.971-7.481 4 4 0 0 1 6.633-2.505 3.999 3.999 0 0 1 3.82 2.014A4 4 0 0 1 12 11a1 1 0 0 1 0-2 2 2 0 1 0 0-4h-1a2 2 0 0 0-3.112-1.662A2 2 0 1 0 4.268 5H4a2 2 0 1 0 0 4z"/></symbol><symbol viewBox="0 0 16 16" id="duplicate" xmlns="http://www.w3.org/2000/svg"><path d="M14 10h-3a1 1 0 0 1-1-1V6H8.527A.527.527 0 0 0 8 6.527V13a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1v-3zm-4-7H8.527c-.18 0-.355.013-.527.04V3a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h2v2H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3h4a3 3 0 0 1 3 3zM8.527 4h2.323a.5.5 0 0 1 .35.143l4.65 4.551a.5.5 0 0 1 .15.357V13a3 3 0 0 1-3 3H9a3 3 0 0 1-3-3V6.527A2.527 2.527 0 0 1 8.527 4z"/></symbol><symbol viewBox="0 0 16 16" id="earth" xmlns="http://www.w3.org/2000/svg"><path d="M8.7 2.04l-.082.177c.283.223.422.413.417.571-.008.237-.311.057-.444.274-.133.218.038.542-.112.637-.15.096-.398-.386-.479-.46-.054-.049-.166-.257-.336-.625l-.216-.225a.844.844 0 0 0-.418-.035c-.177.038-.075.1-.035.132.04.032.32.037.452.2.132.164.03.224-.05.298-.054.05-.157.062-.31.035H5.952l-.402.398.03.325.229.455.324-.463c.008-.206.058-.342.15-.41.14-.1.342-.15.534-.085.191.066-.057.218.011.271.068.053.204-.098.313-.02.11.08.07.155.104.322.036.167.254.114.398.328.144.215.19.29.147.483-.043.195-.168.26-.305.232-.138-.028-.107-.246-.275-.348-.168-.102-.266-.114-.386-.054-.12.06-.016.129.023.235.04.106.274.321.224.43-.05.107-.108.116-.42 0-.21-.077-.414-.007-.615.212l-.76.722c-.153.715-.3 1.13-.44 1.243-.211.17-.177-.483-.483-.656-.306-.174-.494-.047-.8-.07-.307-.023-.42.65-.38.873a.434.434 0 0 0 .221.321c.236-.141.39-.184.465-.128.11.084-.144.267-.074.425.07.158.314.069.386.283.073.213.084.48-.05.706-.135.227-.275.178-.4.053-.127-.126-.033-.375-.255-.704-.223-.329-.381-.337-.63-.787-.158-.287-.35-.743-.575-1.366a6 6 0 0 0 3.21 7.198l.001-.075c0-.577-.004-.944-.012-1.102-.011-.236-.95-.945-1.104-1.2-.154-.256-.34-.595-.355-.746-.016-.151.185-.232.344-.325.16-.093-.11-.367.028-.626.137-.258.395-.438.496-.356.101.081.058.228.267.333.209.104.077-.213.456-.178.38.035.143.201.252.216.11.016.113-.127.299-.143.186-.015.282.445.471.622.19.178.452.008.611.043.159.034.267.09.402.255.136.166-.03.352.073.557.103.205 1.07.22 1.433.255.364.034.371.011.371.324s-.166.314-.453.507c-.286.193-.166.462-.38.762-.212.3-.316.062-.622.14-.306.077-.413.382-.452.568-.039.186-.386.094-.877.232-.29.082-.429.144-.569.204a6.002 6.002 0 0 0 7.682-4.3c-.094-.384-.18-.63-.258-.74-.213-.297-.36.21-.924.49-.564.278-.57-.288-.81-.49-.16-.133-.212-.44-.158-.92-.005-.478.02-.828.077-1.049.057-.221.126-.543.207-.965.351-.373.606-.572.764-.595.237-.034.336.374.658.3a.315.315 0 0 0 .035-.01 5.993 5.993 0 0 0-.475-.824l-.309-.043a.646.646 0 0 0-.332-.117c-.205-.02-.025.128-.089.24-.064.112-.235.724-.437.685-.201-.039-.204-.374-.17-.668.036-.294-.077-.35-.2-.412-.124-.062-.325-.213-.556-.295-.232-.082-.123-.175-.093-.274.03-.1.208-.015.193-.058-.014-.044-.313-.135-.266-.167.03-.02.2-.02.506.003l.216-.012.293-.163a.58.58 0 0 0-.376-.22c-.233-.036-.513-.034-.73-.142-.205-.103-.458-.36-.643-.638A5.965 5.965 0 0 0 8.7 2.04zM8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16z"/></symbol><symbol viewBox="0 0 1600 1600" id="ellipsis_v" xmlns="http://www.w3.org/2000/svg"><path d="M1088 1248v192q0 40-28 68t-68 28H800q-40 0-68-28t-28-68v-192q0-40 28-68t68-28h192q40 0 68 28t28 68zm0-512v192q0 40-28 68t-68 28H800q-40 0-68-28t-28-68V736q0-40 28-68t68-28h192q40 0 68 28t28 68zm0-512v192q0 40-28 68t-68 28H800q-40 0-68-28t-28-68V224q0-40 28-68t68-28h192q40 0 68 28t28 68z"/></symbol><symbol viewBox="0 0 18 18" id="emoji_slightly_smiling_face" xmlns="http://www.w3.org/2000/svg"><path d="M13.29 11.098a4.328 4.328 0 0 1-1.618 2.285c-.79.578-1.68.867-2.672.867-.992 0-1.883-.29-2.672-.867a4.328 4.328 0 0 1-1.617-2.285.721.721 0 0 1 .047-.569.715.715 0 0 1 .445-.369.721.721 0 0 1 .568.047.715.715 0 0 1 .37.445 2.91 2.91 0 0 0 1.084 1.518A2.93 2.93 0 0 0 9 12.75a2.93 2.93 0 0 0 1.775-.58 2.913 2.913 0 0 0 1.084-1.518.711.711 0 0 1 .375-.445.737.737 0 0 1 .575-.047c.195.063.34.186.433.37.094.183.11.372.047.568zM7.5 6c0 .414-.146.768-.44 1.06A1.44 1.44 0 0 1 6 7.5a1.44 1.44 0 0 1-1.06-.44A1.445 1.445 0 0 1 4.5 6c0-.414.146-.768.44-1.06A1.44 1.44 0 0 1 6 4.5c.414 0 .768.146 1.06.44.294.292.44.646.44 1.06zm6 0c0 .414-.146.768-.44 1.06A1.44 1.44 0 0 1 12 7.5a1.44 1.44 0 0 1-1.06-.44A1.445 1.445 0 0 1 10.5 6c0-.414.146-.768.44-1.06A1.44 1.44 0 0 1 12 4.5c.414 0 .768.146 1.06.44.294.292.44.646.44 1.06zm3 3a7.29 7.29 0 0 0-.598-2.912 7.574 7.574 0 0 0-1.6-2.39 7.574 7.574 0 0 0-2.39-1.6A7.29 7.29 0 0 0 9 1.5a7.29 7.29 0 0 0-2.912.598 7.574 7.574 0 0 0-2.39 1.6 7.574 7.574 0 0 0-1.6 2.39A7.29 7.29 0 0 0 1.5 9c0 1.016.2 1.986.598 2.912a7.574 7.574 0 0 0 1.6 2.39 7.574 7.574 0 0 0 2.39 1.6A7.29 7.29 0 0 0 9 16.5a7.29 7.29 0 0 0 2.912-.598 7.574 7.574 0 0 0 2.39-1.6 7.574 7.574 0 0 0 1.6-2.39A7.29 7.29 0 0 0 16.5 9zM18 9a8.804 8.804 0 0 1-1.207 4.518 8.96 8.96 0 0 1-3.275 3.275A8.804 8.804 0 0 1 9 18a8.804 8.804 0 0 1-4.518-1.207 8.96 8.96 0 0 1-3.275-3.275A8.804 8.804 0 0 1 0 9c0-1.633.402-3.139 1.207-4.518a8.96 8.96 0 0 1 3.275-3.275A8.804 8.804 0 0 1 9 0c1.633 0 3.139.402 4.518 1.207a8.96 8.96 0 0 1 3.275 3.275A8.804 8.804 0 0 1 18 9z" fill-rule="evenodd"/></symbol><symbol viewBox="0 0 18 18" id="emoji_smile" xmlns="http://www.w3.org/2000/svg"><path d="M13.29 11.098a4.328 4.328 0 0 1-1.618 2.285c-.79.578-1.68.867-2.672.867-.992 0-1.883-.29-2.672-.867a4.328 4.328 0 0 1-1.617-2.285.721.721 0 0 1 .047-.569.715.715 0 0 1 .445-.369c.195-.062 7.41-.062 7.606 0 .195.063.34.186.433.37.094.183.11.372.047.568zM14 6.37c0 .398-.04.755-.513.755-.473 0-.498-.272-1.237-.272-.74 0-.74.215-1.165.215-.425 0-.585-.3-.585-.698 0-.397.17-.736.513-1.017.341-.281.754-.422 1.237-.422.483 0 .896.14 1.237.422.342.28.513.62.513 1.017zm-6.5 0c0 .398-.04.755-.513.755-.473 0-.498-.272-1.237-.272-.74 0-.74.215-1.165.215-.425 0-.585-.3-.585-.698 0-.397.17-.736.513-1.017.341-.281.754-.422 1.237-.422.483 0 .896.14 1.237.422.342.28.513.62.513 1.017zm9 2.63a7.29 7.29 0 0 0-.598-2.912 7.574 7.574 0 0 0-1.6-2.39 7.574 7.574 0 0 0-2.39-1.6A7.29 7.29 0 0 0 9 1.5a7.29 7.29 0 0 0-2.912.598 7.574 7.574 0 0 0-2.39 1.6 7.574 7.574 0 0 0-1.6 2.39A7.29 7.29 0 0 0 1.5 9c0 1.016.2 1.986.598 2.912a7.574 7.574 0 0 0 1.6 2.39 7.574 7.574 0 0 0 2.39 1.6A7.29 7.29 0 0 0 9 16.5a7.29 7.29 0 0 0 2.912-.598 7.574 7.574 0 0 0 2.39-1.6 7.574 7.574 0 0 0 1.6-2.39A7.29 7.29 0 0 0 16.5 9zM18 9a8.804 8.804 0 0 1-1.207 4.518 8.96 8.96 0 0 1-3.275 3.275A8.804 8.804 0 0 1 9 18a8.804 8.804 0 0 1-4.518-1.207 8.96 8.96 0 0 1-3.275-3.275A8.804 8.804 0 0 1 0 9c0-1.633.402-3.139 1.207-4.518a8.96 8.96 0 0 1 3.275-3.275A8.804 8.804 0 0 1 9 0c1.633 0 3.139.402 4.518 1.207a8.96 8.96 0 0 1 3.275 3.275A8.804 8.804 0 0 1 18 9z" fill-rule="evenodd"/></symbol><symbol viewBox="0 0 18 18" id="emoji_smiley" xmlns="http://www.w3.org/2000/svg"><path d="M13.29 11.098a4.328 4.328 0 0 1-1.618 2.285c-.79.578-1.68.867-2.672.867-.992 0-1.883-.29-2.672-.867a4.328 4.328 0 0 1-1.617-2.285.721.721 0 0 1 .047-.569.715.715 0 0 1 .445-.369c.195-.062 7.41-.062 7.606 0 .195.063.34.186.433.37.094.183.11.372.047.568h.001zM7.5 6c0 .414-.146.768-.44 1.06A1.44 1.44 0 0 1 6 7.5a1.44 1.44 0 0 1-1.06-.44A1.445 1.445 0 0 1 4.5 6c0-.414.146-.768.44-1.06A1.44 1.44 0 0 1 6 4.5c.414 0 .768.146 1.06.44.294.292.44.646.44 1.06zm6 0c0 .414-.146.768-.44 1.06A1.44 1.44 0 0 1 12 7.5a1.44 1.44 0 0 1-1.06-.44A1.445 1.445 0 0 1 10.5 6c0-.414.146-.768.44-1.06A1.44 1.44 0 0 1 12 4.5c.414 0 .768.146 1.06.44.294.292.44.646.44 1.06zm3 3a7.29 7.29 0 0 0-.598-2.912 7.574 7.574 0 0 0-1.6-2.39 7.574 7.574 0 0 0-2.39-1.6A7.29 7.29 0 0 0 9 1.5a7.29 7.29 0 0 0-2.912.598 7.574 7.574 0 0 0-2.39 1.6 7.574 7.574 0 0 0-1.6 2.39A7.29 7.29 0 0 0 1.5 9c0 1.016.2 1.986.598 2.912a7.574 7.574 0 0 0 1.6 2.39 7.574 7.574 0 0 0 2.39 1.6c.92.397 1.91.6 2.912.598a7.29 7.29 0 0 0 2.912-.598 7.574 7.574 0 0 0 2.39-1.6 7.574 7.574 0 0 0 1.6-2.39c.397-.92.6-1.91.598-2.912zM18 9a8.804 8.804 0 0 1-1.207 4.518 8.96 8.96 0 0 1-3.275 3.275A8.804 8.804 0 0 1 9 18a8.804 8.804 0 0 1-4.518-1.207 8.96 8.96 0 0 1-3.275-3.275A8.804 8.804 0 0 1 0 9c0-1.633.402-3.139 1.207-4.518a8.96 8.96 0 0 1 3.275-3.275A8.804 8.804 0 0 1 9 0c1.633 0 3.139.402 4.518 1.207a8.96 8.96 0 0 1 3.275 3.275A8.804 8.804 0 0 1 18 9z"/></symbol><symbol viewBox="0 0 16 16" id="epic" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M14.985 8.044l-.757 2.272a1 1 0 0 1-.949.684H1.618a1 1 0 0 1-.894-1.447l.318-.637A2 2 0 0 0 1.618 9h11.661a2 2 0 0 0 1.706-.956zm0 3l-.757 2.272a1 1 0 0 1-.949.684H1.618a1 1 0 0 1-.894-1.447l.318-.637a2 2 0 0 0 .576.084h11.661a2 2 0 0 0 1.706-.956zM3.618 2h10.995a1 1 0 0 1 .948 1.316l-1.333 4a1 1 0 0 1-.949.684H1.618a1 1 0 0 1-.894-1.447l2-4A1 1 0 0 1 3.618 2zm-.382 4h9.322l.667-2H4.236l-1 2z"/></symbol><symbol viewBox="0 0 16 16" id="external-link" xmlns="http://www.w3.org/2000/svg"><path d="M13.121 4.177l-4.95 4.95a1 1 0 1 1-1.414-1.414l4.95-4.95-1.386-1.386a.5.5 0 0 1 .299-.85l4.709-.524a.5.5 0 0 1 .552.552l-.523 4.71a.5.5 0 0 1-.851.297l-1.386-1.385zM12 8.884a1 1 0 0 1 2 0v4a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3v-8a3 3 0 0 1 3-3h4a1 1 0 1 1 0 2H3a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1v-4z"/></symbol><symbol viewBox="0 0 16 16" id="eye" xmlns="http://www.w3.org/2000/svg"><path d="M8 14C4.816 14 2.253 12.284.393 8.981a2 2 0 0 1 0-1.962C2.253 3.716 4.816 2 8 2s5.747 1.716 7.607 5.019a2 2 0 0 1 0 1.962C13.747 12.284 11.184 14 8 14zm0-2c2.41 0 4.338-1.29 5.864-4C12.338 5.29 10.411 4 8 4 5.59 4 3.662 5.29 2.136 8 3.662 10.71 5.589 12 8 12zm0-1a3 3 0 1 1 0-6 3 3 0 0 1 0 6zm1-3a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="eye-slash" xmlns="http://www.w3.org/2000/svg"><path d="M13.618 2.62L1.62 14.619a1 1 0 0 1-.985-1.668l1.525-1.526C1.516 10.742.926 9.927.393 8.981a2 2 0 0 1 0-1.962C2.253 3.716 4.816 2 8 2c1.074 0 2.076.195 3.006.58l.944-.944a1 1 0 0 1 1.668.985zM8.068 11a3 3 0 0 0 2.931-2.932l-2.931 2.931zm-3.02-2.462a3 3 0 0 1 3.49-3.49l.884-.884A6.044 6.044 0 0 0 8 4C5.59 4 3.662 5.29 2.136 8c.445.79.924 1.46 1.439 2.011l1.473-1.473zm.421 5.06l1.658-1.658c.283.04.575.06.873.06 2.41 0 4.338-1.29 5.864-4a11.023 11.023 0 0 0-1.133-1.664l1.418-1.418a12.799 12.799 0 0 1 1.458 2.1 2 2 0 0 1 0 1.963C13.747 12.284 11.184 14 8 14a7.883 7.883 0 0 1-2.53-.402z"/></symbol><symbol viewBox="0 0 16 16" id="file-addition" xmlns="http://www.w3.org/2000/svg"><path d="M7 7V5a1 1 0 1 1 2 0v2h2a1 1 0 0 1 0 2H9v2a1 1 0 0 1-2 0V9H5a1 1 0 1 1 0-2h2zM3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 1a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3a2 2 0 0 0-2-2H3z"/></symbol><symbol viewBox="0 0 16 16" id="file-deletion" xmlns="http://www.w3.org/2000/svg"><path d="M3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 1a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3a2 2 0 0 0-2-2H3zm2 6h6a1 1 0 0 1 0 2H5a1 1 0 1 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="file-modified" xmlns="http://www.w3.org/2000/svg"><path d="M3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 1a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3a2 2 0 0 0-2-2H3zm5 4a3 3 0 1 1 0 6 3 3 0 0 1 0-6z"/></symbol><symbol viewBox="0 0 16 16" id="filter" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10 6v9l-3.724-1.862A.5.5 0 0 1 6 12.691V6L1.854 1.854A.5.5 0 0 1 2.207 1h11.586a.5.5 0 0 1 .353.854L10 6z"/></symbol><symbol viewBox="0 0 16 16" id="folder" xmlns="http://www.w3.org/2000/svg"><path d="M7.228 5l-.475-1.335A1 1 0 0 0 5.81 3H2v9a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V6a1 1 0 0 0-1-1H7.228zM13 3a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a2 2 0 0 1 2-2h3.81a3 3 0 0 1 2.827 1.995L13 3z"/></symbol><symbol viewBox="0 0 16 16" id="fork" xmlns="http://www.w3.org/2000/svg"><path d="M9 12.268a2 2 0 1 1-2 0V8.874A4.002 4.002 0 0 1 4 5V3.732a2 2 0 1 1 2 0V5a2 2 0 1 0 4 0V3.732a2 2 0 1 1 2 0V5a4.002 4.002 0 0 1-3 3.874v3.394zM11 3a1 1 0 1 0 0-2 1 1 0 0 0 0 2zM5 3a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm3 12a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="geo-nodes" xmlns="http://www.w3.org/2000/svg"><path d="M9.7 13.1l-.2.2c-.7.8-2 .9-2.8.1-.1 0-.1-.1-.1-.1l-.2-.2c-2 .2-3.4.7-3.4 1.4 0 .8 2.2 1.5 5 1.5s5-.7 5-1.5c0-.7-1.4-1.2-3.3-1.4M7.3 12.7c.4.4 1 .3 1.4-.1C11.6 9.5 13 7 13 5.3 13 2.4 10.8 0 8 0S3 2.4 3 5.3C3 7 4.4 9.5 7.3 12.7M8 2c1.6 0 3 1.4 3 3.3 0 1-1 2.8-3 5.2-2-2.4-3-4.2-3-5.2C5 3.4 6.4 2 8 2"/><circle cx="8" cy="5" r="1"/></symbol><symbol viewBox="0 0 16 16" id="git-merge" xmlns="http://www.w3.org/2000/svg"><path d="M11 12.268V5a1 1 0 0 0-1-1v1a.5.5 0 0 1-.8.4l-2.667-2a.5.5 0 0 1 0-.8L9.2.6a.5.5 0 0 1 .8.4v1a3 3 0 0 1 3 3v7.268a2 2 0 1 1-2 0zm-6 0a2 2 0 1 1-2 0V4.732a2 2 0 1 1 2 0v7.536zM4 4a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm0 11a1 1 0 1 0 0-2 1 1 0 0 0 0 2zm8 0a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="group" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M3.048 11.997C-.377 11.975.013 11.782.013 10.56.013 9.235.653 8 4 8c.444 0 .84.022 1.194.062.164.435.426.82.76 1.132-1.786.389-2.721 1.353-2.906 2.803zm2.94-7.222a2.993 2.993 0 0 0-.976 1.95 2 2 0 1 1 .975-1.95zm6.964 7.222c-.185-1.45-1.12-2.414-2.906-2.803.334-.311.596-.697.76-1.132C11.16 8.022 11.556 8 12 8c3.346 0 3.987 1.235 3.987 2.56 0 1.222.39 1.415-3.035 1.437zm-1.964-5.272a2.993 2.993 0 0 0-.976-1.95 2 2 0 1 1 .976 1.95zM8 9a2 2 0 1 1 0-4 2 2 0 0 1 0 4zm0 5c-2.177 0-3.987-.115-3.987-1.44S4.653 10 8 10c3.346 0 3.987 1.235 3.987 2.56S10.177 14 8 14z"/></symbol><symbol viewBox="0 0 16 16" id="history" xmlns="http://www.w3.org/2000/svg"><path d="M2.868 3.24a7 7 0 1 1-.043 9.475 1 1 0 0 1 1.478-1.348 5 5 0 1 0 .124-6.865l.796.645a.5.5 0 0 1-.193.873l-3.232.814a.5.5 0 0 1-.622-.504L1.3 3a.5.5 0 0 1 .814-.37l.754.61zM9 8h1a1 1 0 0 1 0 2H8a.997.997 0 0 1-1-1V6a1 1 0 1 1 2 0v2z"/></symbol><symbol viewBox="0 0 16 16" id="home" xmlns="http://www.w3.org/2000/svg"><path d="M8.462 2.177a.505.505 0 0 1-.038.044l.038-.044zm-.787 0l.038.043a.5.5 0 0 1-.038-.043zM3.706 7h8.725L8.069 2.585 3.706 7zM7 13.369V12a1 1 0 0 1 2 0v1.369h3V9H4v4.369h3zM14 9v4.836c0 .833-.657 1.533-1.5 1.533h-9c-.843 0-1.5-.7-1.5-1.533V9h-.448a1.1 1.1 0 0 1-.783-1.873L6.934.887a1.5 1.5 0 0 1 2.269 0l6.165 6.24A1.1 1.1 0 0 1 14.585 9H14z"/></symbol><symbol viewBox="0 0 16 16" id="hook" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10 3a1 1 0 0 0-1-1H7a1 1 0 0 0-1 1h4zm0 1H6v1a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1V4zM7 8a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3h2a3 3 0 0 1 3 3v2a3 3 0 0 1-3 3v4a2 2 0 1 0 4 0h-.44a.3.3 0 0 1-.25-.466l1.44-2.16a.3.3 0 0 1 .5 0l1.44 2.16a.3.3 0 0 1-.25.466H15a4 4 0 0 1-7 2.646A4 4 0 0 1 1 12H.56a.3.3 0 0 1-.25-.466l1.44-2.16a.3.3 0 0 1 .5 0l1.44 2.16a.3.3 0 0 1-.25.466H3a2 2 0 1 0 4 0V8z"/></symbol><symbol viewBox="0 0 16 16" id="hourglass" xmlns="http://www.w3.org/2000/svg"><path d="M10.331 4.889A2.988 2.988 0 0 0 11 3V2H5v1c0 .362.064.709.182 1.03l5.15.859zM3 14v-1c0-1.78.93-3.342 2.33-4.228.447-.327.67-.582.67-.764 0-.19-.242-.46-.725-.815A4.996 4.996 0 0 1 3 3V2H2a1 1 0 1 1 0-2h12a1 1 0 0 1 0 2h-1v1a4.997 4.997 0 0 1-2.39 4.266c-.407.3-.61.545-.61.734 0 .19.203.434.61.734A4.997 4.997 0 0 1 13 13v1h1a1 1 0 0 1 0 2H2a1 1 0 0 1 0-2h1zm8 0v-1a3 3 0 0 0-6 0v1h6z"/></symbol><symbol viewBox="0 0 38 38" id="image-comment-dark" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><circle cx="19" cy="19" r="18" fill="#1F78D1"/><path fill="#FFF" fill-rule="nonzero" d="M19 38C8.507 38 0 29.493 0 19S8.507 0 19 0s19 8.507 19 19-8.507 19-19 19zm0-2c9.389 0 17-7.611 17-17S28.389 2 19 2 2 9.611 2 19s7.611 17 17 17zm-6.293-8.293c-.63.63-1.707.184-1.707-.707V15a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3h-7.586l-3.707 3.707zM13 24.586l2.293-2.293A1 1 0 0 1 16 22h8a1 1 0 0 0 1-1v-6a1 1 0 0 0-1-1H14a1 1 0 0 0-1 1v9.586z"/></g></symbol><symbol viewBox="0 0 38 38" id="image-comment-light" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><circle cx="19" cy="19" r="18" fill="#FFF"/><path fill="#1F78D1" fill-rule="nonzero" d="M19 38C8.507 38 0 29.493 0 19S8.507 0 19 0s19 8.507 19 19-8.507 19-19 19zm0-2c9.389 0 17-7.611 17-17S28.389 2 19 2 2 9.611 2 19s7.611 17 17 17zm-6.293-8.293c-.63.63-1.707.184-1.707-.707V15a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3h-7.586l-3.707 3.707zM13 24.586l2.293-2.293A1 1 0 0 1 16 22h8a1 1 0 0 0 1-1v-6a1 1 0 0 0-1-1H14a1 1 0 0 0-1 1v9.586z"/></g></symbol><symbol viewBox="0 0 16 16" id="import" xmlns="http://www.w3.org/2000/svg"><path d="M9 8h1a.5.5 0 0 1 .4.8l-2 2.667a.5.5 0 0 1-.8 0L5.6 8.8A.5.5 0 0 1 6 8h1V1a1 1 0 1 1 2 0v7zM0 8a1 1 0 1 1 2 0 6 6 0 1 0 12 0 1 1 0 0 1 2 0A8 8 0 1 1 0 8z"/></symbol><symbol viewBox="0 0 16 16" id="issue-block" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M5.803 8a5.97 5.97 0 0 0-.462 1H4.5a.5.5 0 0 1 0-1h1.303zM4.5 5h3a.5.5 0 0 1 0 1h-3a.5.5 0 0 1 0-1zm7.5.083a6.04 6.04 0 0 0-2 0V3a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h2.083a5.96 5.96 0 0 0 .72 2H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3h6a3 3 0 0 1 3 3v2.083zm1.121 3.796zM11 16a5 5 0 1 1 0-10 5 5 0 0 1 0 10zm-1.293-2.292a3 3 0 0 0 4.001-4.001l-4.001 4zm-1.415-1.415l4.001-4a3 3 0 0 0-4.001 4.001z"/></symbol><symbol viewBox="0 0 16 16" id="issue-child" xmlns="http://www.w3.org/2000/svg"><path d="M11 8H5v1h1a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1H1a1 1 0 0 1-1-1v-4a1 1 0 0 1 1-1h2V7a.997.997 0 0 1 1-1h3V4H4.5a.5.5 0 0 1-.5-.5v-2a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.5.5H9v2h3a.997.997 0 0 1 1 1v2h2a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1h-5a1 1 0 0 1-1-1v-4a1 1 0 0 1 1-1h1V8zm-9 3v2h3v-2H2zm9 0v2h3v-2h-3z"/></symbol><symbol viewBox="0 0 16 16" id="issue-close" xmlns="http://www.w3.org/2000/svg"><path d="M7.536 8.657l2.828-2.829a1 1 0 0 1 1.414 1.415l-3.535 3.535a.997.997 0 0 1-1.415 0l-2.12-2.121A1 1 0 0 1 6.12 7.243l1.415 1.414zM8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12z"/></symbol><symbol viewBox="0 0 16 16" id="issue-duplicate" xmlns="http://www.w3.org/2000/svg"><path d="M10.874 2H12a3 3 0 0 1 3 3v8a3 3 0 0 1-3 3h-2c-.918 0-1.74-.413-2.29-1.063a3.987 3.987 0 0 0 1.988-.984A1 1 0 0 0 10 14h2a1 1 0 0 0 1-1V5a1 1 0 0 0-1-1h-1V3c0-.345-.044-.68-.126-1zM4 0h3a3 3 0 0 1 3 3v8a3 3 0 0 1-3 3H4a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h3a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H4z"/></symbol><symbol viewBox="0 0 16 16" id="issue-new" xmlns="http://www.w3.org/2000/svg"><path d="M10 2V1a1 1 0 0 1 2 0v1h1a1 1 0 0 1 0 2h-1v1a1 1 0 0 1-2 0V4H9a1 1 0 1 1 0-2h1zm0 6a1 1 0 0 1 2 0v5a3 3 0 0 1-3 3H5a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3h1a1 1 0 1 1 0 2H5a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1V8z"/></symbol><symbol viewBox="0 0 16 16" id="issue-open" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zm0-2a4 4 0 1 1 0-8 4 4 0 0 1 0 8zm0-2a2 2 0 1 0 0-4 2 2 0 0 0 0 4z"/></symbol><symbol viewBox="0 0 16 16" id="issue-open-m" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12z"/></symbol><symbol viewBox="0 0 16 16" id="issue-parent" xmlns="http://www.w3.org/2000/svg"><path d="M11 11H5v1h1.5a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.5.5h-6a.5.5 0 0 1-.5-.5v-2a.5.5 0 0 1 .5-.5H3v-2a.997.997 0 0 1 1-1h3V7H5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1H9v2h3a.997.997 0 0 1 1 1v2h2.5a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.5.5h-6a.5.5 0 0 1-.5-.5v-2a.5.5 0 0 1 .5-.5H11v-1zM6 3v2h4V3H6z"/></symbol><symbol viewBox="0 0 16 16" id="issues" xmlns="http://www.w3.org/2000/svg"><path d="M10.458 15.012l.311.055a3 3 0 0 0 3.476-2.433l1.389-7.879A3 3 0 0 0 13.2 1.28L11.23.933a3.002 3.002 0 0 0-.824-.031c.364.59.58 1.28.593 2.02l1.854.328a1 1 0 0 1 .811 1.158l-1.389 7.879a1 1 0 0 1-1.158.81l-.118-.02a3.98 3.98 0 0 1-.541 1.935zM3 0h4a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3z"/></symbol><symbol viewBox="0 0 16 16" id="italic" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M5.5 12l2-8H6a1 1 0 1 1 0-2h6a1 1 0 0 1 0 2h-1.5l-2 8H10a1 1 0 0 1 0 2H4a1 1 0 0 1 0-2h1.5z"/></symbol><symbol viewBox="0 0 16 16" id="key" xmlns="http://www.w3.org/2000/svg"><path d="M7.575 6.689a4.002 4.002 0 0 1 6.274-4.86 4 4 0 0 1-4.86 6.274l-2.21 2.21.706.708a1 1 0 1 1-1.414 1.414l-.707-.707-.707.707.707.707a1 1 0 1 1-1.414 1.414l-.707-.707a1 1 0 0 1-1.414-1.414l5.746-5.746zm2.032-.618a2 2 0 1 0 2.828-2.828A2 2 0 0 0 9.607 6.07z"/></symbol><symbol viewBox="0 0 16 16" id="key-2" xmlns="http://www.w3.org/2000/svg"><path d="M5.172 14.157l-.344.344-2.485.133a.462.462 0 0 1-.497-.503l.14-2.24a.599.599 0 0 1 .177-.382l5.155-5.155a4 4 0 1 1 2.828 2.828l-1.439 1.44-1.06-.354-.708.707.354 1.06-.707.708-1.06-.354-.708.707.354 1.06zm6.01-8.839a1 1 0 1 0 1.414-1.414 1 1 0 0 0-1.414 1.414z"/></symbol><symbol viewBox="0 0 16 16" id="label" xmlns="http://www.w3.org/2000/svg"><path d="M11.782 14.718a3 3 0 0 1-4.242 0L1.652 8.829a2 2 0 0 1-.565-1.702l.54-3.703a2 2 0 0 1 1.69-1.69l3.703-.54a2 2 0 0 1 1.703.564l5.888 5.888a3 3 0 0 1 0 4.243l-2.829 2.829zm1.415-5.657L7.309 3.173l-3.703.54-.54 3.702 5.888 5.888a1 1 0 0 0 1.414 0l2.829-2.828a1 1 0 0 0 0-1.414zM5.732 5.525A1 1 0 1 1 7.146 6.94a1 1 0 0 1-1.414-1.414z"/></symbol><symbol viewBox="0 0 16 16" id="labels" xmlns="http://www.w3.org/2000/svg"><path d="M9.424 2.254l2.08-.905a1 1 0 0 1 1.206.326l3.013 4.12a1 1 0 0 1 .16.849l-1.947 7.264a3 3 0 0 1-3.675 2.122l-.5-.135a3.999 3.999 0 0 0 1.082-1.782 1 1 0 0 0 1.16-.722l1.823-6.802-2.258-3.087-.687.299a2 2 0 0 0-.628-.88l-.829-.667zM.377 3.7L4.4.498a1 1 0 0 1 1.25.003L9.627 3.7a1 1 0 0 1 .373.78V13a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V4.482A1 1 0 0 1 .377 3.7zM2 13a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1V4.958L5.02 2.561 2 4.964V13zm3-6a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="leave" xmlns="http://www.w3.org/2000/svg"><path d="M11 7V5.883a.5.5 0 0 1 .757-.429l3.528 2.117a.5.5 0 0 1 0 .858l-3.528 2.117a.5.5 0 0 1-.757-.43V9H7a1 1 0 1 1 0-2h4zm-2 6.256a1 1 0 0 1 2 0A2.744 2.744 0 0 1 8.256 16H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3h5.19A2.81 2.81 0 0 1 11 2.81a1 1 0 0 1-2 0A.81.81 0 0 0 8.19 2H3a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h5.256c.41 0 .744-.333.744-.744z"/></symbol><symbol viewBox="0 0 16 16" id="level-up" xmlns="http://www.w3.org/2000/svg"><path fill="#2E2E2E" fill-rule="evenodd" d="M7 6h3.489a.5.5 0 0 0 .373-.832L6.374.117a.5.5 0 0 0-.748 0l-4.488 5.05A.5.5 0 0 0 1.51 6H5v7a3 3 0 0 0 3 3h6a1 1 0 0 0 0-2H8a1 1 0 0 1-1-1V6z"/></symbol><symbol viewBox="0 0 16 16" id="license" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M12.56 8.9l2.66 4.606a.3.3 0 0 1-.243.45l-1.678.094a.1.1 0 0 0-.078.044l-.953 1.432a.3.3 0 0 1-.51-.016L9.097 10.9a5.994 5.994 0 0 0 3.464-2zm-5.23 2.063L4.707 15.51a.3.3 0 0 1-.51.016l-.953-1.432a.1.1 0 0 0-.078-.044l-1.678-.094a.3.3 0 0 1-.243-.45l2.48-4.297a5.983 5.983 0 0 0 3.607 1.754zM8 10A5 5 0 1 1 8 0a5 5 0 0 1 0 10zm0-2a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm0-1a2 2 0 1 1 0-4 2 2 0 0 1 0 4z"/></symbol><symbol viewBox="0 0 16 16" id="link" xmlns="http://www.w3.org/2000/svg"><path d="M6.986 3.35l2.12-2.122a4 4 0 0 1 5.657 5.657l-2.828 2.829a4 4 0 0 1-5.657 0 1 1 0 0 1 1.414-1.415 2 2 0 0 0 2.829 0l2.828-2.828a2 2 0 1 0-2.828-2.828l-1.001 1a5.018 5.018 0 0 0-2.534-.294zm2.12 9.192l-2.12 2.121a4 4 0 1 1-5.658-5.656l2.829-2.829a4 4 0 0 1 5.657 0 1 1 0 1 1-1.415 1.414 2 2 0 0 0-2.828 0l-2.828 2.829a2 2 0 1 0 2.828 2.828l1.001-1.001a5.018 5.018 0 0 0 2.534.294z"/></symbol><symbol viewBox="0 0 16 16" id="list-bulleted" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M1 4a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm0 5a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm4-7h10a1 1 0 0 1 0 2H5a1 1 0 1 1 0-2zm0 5h10a1 1 0 0 1 0 2H5a1 1 0 1 1 0-2zm-4 7a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm4-2h10a1 1 0 0 1 0 2H5a1 1 0 0 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="list-numbered" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M6 2h8a1 1 0 0 1 0 2H6a1 1 0 1 1 0-2zm0 5h8a1 1 0 0 1 0 2H6a1 1 0 1 1 0-2zm0 5h8a1 1 0 0 1 0 2H6a1 1 0 0 1 0-2zM1.156 5v-.828h.816V2.204h-.72v-.636c.432-.084.708-.192.996-.372h.756v2.976h.684V5H1.156zm-.18 5v-.588c.9-.828 1.596-1.464 1.596-1.98 0-.342-.192-.504-.468-.504-.252 0-.444.18-.624.36l-.552-.552c.396-.42.756-.612 1.32-.612.768 0 1.308.492 1.308 1.248 0 .612-.576 1.284-1.092 1.812.192-.024.468-.048.636-.048h.636V10H.976zm1.26 5.072c-.618 0-1.068-.204-1.356-.54l.468-.648c.234.216.51.36.78.36.336 0 .552-.12.552-.36 0-.288-.15-.456-.948-.456v-.72c.636 0 .828-.168.828-.432 0-.228-.138-.348-.396-.348-.252 0-.432.108-.672.312l-.516-.624c.372-.312.768-.492 1.236-.492.84 0 1.38.384 1.38 1.074 0 .366-.204.642-.612.822v.024c.432.132.732.432.732.912 0 .72-.684 1.116-1.476 1.116z"/></symbol><symbol viewBox="0 0 16 16" id="location" xmlns="http://www.w3.org/2000/svg"><path d="M8.755 15.144a1 1 0 0 1-1.51 0C3.748 11.114 2 8.065 2 6a6 6 0 1 1 12 0c0 2.065-1.748 5.113-5.245 9.144zM12 6a4 4 0 1 0-8 0c0 1.314 1.312 3.71 4 6.944C10.688 9.71 12 7.314 12 6zM8 8a2 2 0 1 1 0-4 2 2 0 0 1 0 4z"/></symbol><symbol viewBox="0 0 16 16" id="location-dot" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M6.314 13.087C4.382 13.295 3 13.85 3 14.5c0 .828 2.239 1.5 5 1.5s5-.672 5-1.5c0-.65-1.382-1.205-3.314-1.413l-.202.225a2 2 0 0 1-2.968 0l-.202-.225zm2.428-.445a1 1 0 0 1-1.484 0C4.419 9.5 3 7.037 3 5.252 3 2.353 5.239 0 8 0s5 2.352 5 5.253c0 1.784-1.42 4.247-4.258 7.389zM11 5.252C11 3.436 9.634 2 8 2S5 3.435 5 5.253c0 1.027.974 2.824 3 5.203 2.026-2.38 3-4.176 3-5.203zM8 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="lock" xmlns="http://www.w3.org/2000/svg"><path d="M10 5V4h2v1a3 3 0 0 1 3 3v5a3 3 0 0 1-3 3H4a3 3 0 0 1-3-3V8a3 3 0 0 1 3-3V4h2v1h4zM4 7a1 1 0 0 0-1 1v5a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V8a1 1 0 0 0-1-1H4zm0-3a4 4 0 1 1 8 0h-2a2 2 0 1 0-4 0H4z"/></symbol><symbol viewBox="0 0 16 16" id="lock-open" xmlns="http://www.w3.org/2000/svg"><path d="M4.044 4a4 4 0 0 1 6.99-2.658 1 1 0 1 1-1.495 1.33A2 2 0 0 0 6.044 4a.998.998 0 0 1-.07.367v.701H12a3 3 0 0 1 3 3v5a3 3 0 0 1-3 3H4a3 3 0 0 1-3-3v-5a3 3 0 0 1 2.974-3V4h.07zM4 7.07a1 1 0 0 0-1 1v5a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1v-5a1 1 0 0 0-1-1H4z"/></symbol><symbol viewBox="0 0 16 16" id="log" xmlns="http://www.w3.org/2000/svg"><path d="M4 0h8a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H4a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H4zm1 4a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm0 3a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm3-5h3a1 1 0 0 1 0 2H8a1 1 0 1 1 0-2zm0 3h3a1 1 0 0 1 0 2H8a1 1 0 1 1 0-2zm-3 5a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm3-2h3a1 1 0 0 1 0 2H8a1 1 0 0 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="mail" xmlns="http://www.w3.org/2000/svg"><path d="M14 5.6L9.338 9.796a2 2 0 0 1-2.676 0L2 5.6V11a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V5.6zM3 2h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3zm.212 2L8 8.31 12.788 4H3.212z"/></symbol><symbol viewBox="0 0 16 16" id="menu" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M1.143 2h13.714C15.488 2 16 2.448 16 3s-.512 1-1.143 1H1.143C.512 4 0 3.552 0 3s.512-1 1.143-1zm0 5h13.714C15.488 7 16 7.448 16 8s-.512 1-1.143 1H1.143C.512 9 0 8.552 0 8s.512-1 1.143-1zm0 5h13.714c.631 0 1.143.448 1.143 1s-.512 1-1.143 1H1.143C.512 14 0 13.552 0 13s.512-1 1.143-1z"/></symbol><symbol viewBox="0 0 16 16" id="merge-request-close" xmlns="http://www.w3.org/2000/svg"><path d="M9.414 8l1.414 1.414a1 1 0 1 1-1.414 1.414L8 9.414l-1.414 1.414a1 1 0 1 1-1.414-1.414L6.586 8 5.172 6.586a1 1 0 1 1 1.414-1.414L8 6.586l1.414-1.414a1 1 0 1 1 1.414 1.414L9.414 8zM8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12z"/></symbol><symbol viewBox="0 0 16 16" id="messages" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8.588 8.942l1.173 5.862A1 1 0 0 1 8.78 16H7.22a1 1 0 0 1-.98-1.196l1.172-5.862a3.014 3.014 0 0 0 1.176 0zM8 8a2 2 0 1 1 0-4 2 2 0 0 1 0 4zM4.464 2.464L5.88 3.88a3 3 0 0 0 0 4.242L4.464 9.536a5 5 0 0 1 0-7.072zm7.072 7.072L10.12 8.12a3 3 0 0 0 0-4.242l1.415-1.415a5 5 0 0 1 0 7.072zM2.343.343l1.414 1.414a6 6 0 0 0 0 8.486l-1.414 1.414a8 8 0 0 1 0-11.314zm11.314 11.314l-1.414-1.414a6 6 0 0 0 0-8.486L13.657.343a8 8 0 0 1 0 11.314z"/></symbol><symbol viewBox="0 0 16 16" id="mobile-issue-close" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M5.657 10.728L2.12 7.192A1 1 0 1 0 .707 8.607l4.243 4.242a.997.997 0 0 0 1.414 0l8.485-8.485a1 1 0 1 0-1.414-1.414l-7.778 7.778z"/></symbol><symbol viewBox="0 0 16 16" id="monitor" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10 13v1h3a1 1 0 0 1 0 2H3a1 1 0 0 1 0-2h3v-1H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v7a3 3 0 0 1-3 3h-3zM3 2a1 1 0 0 0-1 1v7a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3zm5.723 6.416l-2.66-1.773-1.71 1.71a.5.5 0 1 1-.707-.707l2-2a.5.5 0 0 1 .631-.062l2.66 1.773 2.71-2.71a.5.5 0 0 1 .707.707l-3 3a.5.5 0 0 1-.631.062z"/></symbol><symbol viewBox="0 0 16 16" id="more" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8 4a2 2 0 1 1 0-4 2 2 0 0 1 0 4zm0 6a2 2 0 1 1 0-4 2 2 0 0 1 0 4zm0 6a2 2 0 1 1 0-4 2 2 0 0 1 0 4z"/></symbol><symbol viewBox="0 0 16 16" id="notifications" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M6 14H2.435a2 2 0 0 1-1.761-2.947c.962-1.788 1.521-3.065 1.68-3.832.322-1.566.947-5.501 4.65-6.134a1 1 0 1 1 1.994-.024c3.755.528 4.375 4.27 4.761 6.043.188.86.742 2.188 1.661 3.982A2 2 0 0 1 13.64 14H10a2 2 0 1 1-4 0zm5.805-6.468c-.325-1.492-.37-1.674-.61-2.288C10.6 3.716 9.742 3 8.07 3c-1.608 0-2.49.718-3.103 2.197-.28.676-.356.982-.654 2.428-.208 1.012-.827 2.424-1.877 4.375H13.64c-.993-1.937-1.6-3.396-1.835-4.468z"/></symbol><symbol viewBox="0 0 16 16" id="notifications-off" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M13.26 5.089c.243.757.382 1.478.5 2.017.187.86.74 2.188 1.66 3.982A2 2 0 0 1 13.64 14H10a2 2 0 1 1-4 0H4.35l2-2h7.29c-.993-1.937-1.6-3.396-1.835-4.468-.07-.326-.129-.59-.178-.81l1.634-1.633zM10.943 1.75l-1.48 1.48C9.07 3.076 8.612 3 8.069 3c-1.608 0-2.49.718-3.103 2.197-.28.676-.356.982-.654 2.428-.065.317-.17.673-.317 1.073L.45 12.242a1.99 1.99 0 0 1 .224-1.19c.962-1.787 1.521-3.064 1.68-3.831.322-1.566.947-5.501 4.65-6.134a1 1 0 1 1 1.994-.024 4.867 4.867 0 0 1 1.944.688zm2.932-.105a1 1 0 0 1 0 1.415L2.561 14.374a1 1 0 1 1-1.415-1.414L12.46 1.646a1 1 0 0 1 1.414 0z"/></symbol><symbol viewBox="0 0 16 16" id="overview" xmlns="http://www.w3.org/2000/svg"><path d="M2 0h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2zm0 2v3h3V2H2zm9-2h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2h-3a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2zm0 2v3h3V2h-3zM2 9h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2zm0 2v3h3v-3H2zm9-2h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2h-3a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2zm0 2v3h3v-3h-3z"/></symbol><symbol viewBox="0 0 16 16" id="pencil" xmlns="http://www.w3.org/2000/svg"><path d="M13.02 1.293l1.414 1.414a1 1 0 0 1 0 1.414L4.119 14.436a1 1 0 0 1-.704.293l-2.407.008L1 12.316a1 1 0 0 1 .293-.71L11.605 1.292a1 1 0 0 1 1.414 0zm-1.416 1.415l-.707.707L12.31 4.83l.707-.707-1.414-1.415zM3.411 13.73l1.123-1.122H3.12v-1.415L2 12.312l.005 1.422 1.406-.005z"/></symbol><symbol viewBox="0 0 16 16" id="pencil-square" xmlns="http://www.w3.org/2000/svg"><path d="M12 9a1 1 0 0 1 2 0v4a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3h4a1 1 0 1 1 0 2H3a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V9zm.778-7.179l1.414 1.415-6.476 6.476a1 1 0 0 1-.498.27l-1.51.325.323-1.512a1 1 0 0 1 .27-.497l6.477-6.477zM15.607.407a1 1 0 0 1 0 1.414l-.708.707-1.414-1.414.707-.707a1 1 0 0 1 1.415 0z"/></symbol><symbol viewBox="0 0 16 16" id="pipeline" xmlns="http://www.w3.org/2000/svg"><path d="M8.969 7.25a2 2 0 1 1-1.938 0A1.002 1.002 0 0 1 7 7V5.083a.2.2 0 0 1 .06-.142l.877-.87a.1.1 0 0 1 .141 0l.864.87A.2.2 0 0 1 9 5.083V7c0 .086-.01.17-.031.25zM8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zm4.5-4a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm0-3a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm-2 6a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm0-9a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm-5 9a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm0-9a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm-2 6a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zm0-3a.5.5 0 1 1 0-1 .5.5 0 0 1 0 1zM8 10a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="play" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M2.765 15.835c-.545.321-1.258.159-1.593-.363A1.075 1.075 0 0 1 1 14.89V1.11C1 .496 1.518 0 2.158 0c.214 0 .424.057.607.165l11.684 6.89c.544.321.714 1.005.38 1.526a1.135 1.135 0 0 1-.38.364l-11.684 6.89z"/></symbol><symbol viewBox="0 0 16 16" id="plus" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M7 7V1a1 1 0 1 1 2 0v6h6a1 1 0 0 1 0 2H9v6a1 1 0 0 1-2 0V9H1a1 1 0 1 1 0-2h6z"/></symbol><symbol viewBox="0 0 16 16" id="plus-square" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M9 7V4a1 1 0 1 0-2 0v3H4a1 1 0 1 0 0 2h3v3a1 1 0 0 0 2 0V9h3a1 1 0 0 0 0-2H9zM3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3z"/></symbol><symbol viewBox="0 0 16 16" id="plus-square-o" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M7 7V5a1 1 0 1 1 2 0v2h2a1 1 0 0 1 0 2H9v2a1 1 0 0 1-2 0V9H5a1 1 0 1 1 0-2h2zM3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3z"/></symbol><symbol viewBox="0 0 16 16" id="preferences" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M5 12h10a1 1 0 0 1 0 2H5a1 1 0 0 1-2 0v-2a1 1 0 0 1 2 0zm-3 0H1a1 1 0 0 0 0 2h1v-2zm11-5h2a1 1 0 0 1 0 2h-2a1 1 0 0 1-2 0V7a1 1 0 0 1 2 0zm-3 0H1a1 1 0 1 0 0 2h9V7zM6 2h9a1 1 0 0 1 0 2H6a1 1 0 1 1-2 0V2a1 1 0 1 1 2 0zM3 2H1a1 1 0 1 0 0 2h2V2z"/></symbol><symbol viewBox="0 0 16 16" id="profile" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zm-4.274-3.404C4.412 9.709 5.694 9 8 9c2.313 0 3.595.7 4.28 1.586A4.997 4.997 0 0 1 8 13a4.997 4.997 0 0 1-4.274-2.404zM8 8a2 2 0 1 1 0-4 2 2 0 0 1 0 4z"/></symbol><symbol viewBox="0 0 16 16" id="project" xmlns="http://www.w3.org/2000/svg"><path d="M8.462 2.177l-.038.044a.505.505 0 0 0 .038-.044zm-.787 0a.5.5 0 0 0 .038.043l-.038-.043zM3.706 7h8.725L8.069 2.585 3.706 7zM7 13.369V12a1 1 0 0 1 2 0v1.369h3V9H4v4.369h3zM14 9v4.836c0 .833-.657 1.533-1.5 1.533h-9c-.843 0-1.5-.7-1.5-1.533V9h-.448a1.1 1.1 0 0 1-.783-1.873L6.934.887a1.5 1.5 0 0 1 2.269 0l6.165 6.24A1.1 1.1 0 0 1 14.585 9H14z"/></symbol><symbol viewBox="0 0 16 16" id="push-rules" xmlns="http://www.w3.org/2000/svg"><path d="M6.268 9a2 2 0 0 1 3.464 0H11a1 1 0 0 1 0 2H9.732a2 2 0 0 1-3.464 0H5a1 1 0 0 1 0-2h1.268zM7 2H4a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1h-1v3.515a.3.3 0 0 1-.434.268l-1.432-.716a.3.3 0 0 0-.268 0l-1.432.716A.3.3 0 0 1 7 5.515V2zM4 0h8a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H4a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm4 11a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="question" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm-1.46-5.602h2.233a3.97 3.97 0 0 1 .051-.558c.029-.17.073-.326.133-.469.06-.143.14-.28.242-.41.102-.13.228-.263.38-.399.26-.24.504-.467.733-.683a5.03 5.03 0 0 0 .598-.668c.17-.23.302-.477.399-.742a2.66 2.66 0 0 0 .144-.907c0-.505-.083-.95-.25-1.335a2.55 2.55 0 0 0-.723-.97 3.2 3.2 0 0 0-1.152-.589 5.441 5.441 0 0 0-1.531-.2c-.516 0-.998.063-1.445.188a3.19 3.19 0 0 0-1.168.59c-.331.268-.594.61-.79 1.027-.195.417-.295.917-.3 1.5h2.64c.006-.224.04-.416.102-.578.062-.161.142-.293.238-.394a.921.921 0 0 1 .332-.227 1.04 1.04 0 0 1 .39-.074c.34 0 .593.095.763.285.169.19.254.488.254.895 0 .328-.106.63-.317.906-.21.276-.499.565-.863.867-.214.182-.39.374-.531.574-.141.2-.253.42-.336.657a3.656 3.656 0 0 0-.176.777 7.89 7.89 0 0 0-.05.937zm-.321 2.375c0 .188.035.362.105.524.07.161.17.3.301.418.13.117.284.21.46.277.178.068.376.102.595.102.218 0 .416-.034.593-.102.178-.068.331-.16.461-.277a1.2 1.2 0 0 0 .301-.418c.07-.162.106-.336.106-.524a1.3 1.3 0 0 0-.106-.523 1.2 1.2 0 0 0-.3-.418 1.461 1.461 0 0 0-.462-.277 1.651 1.651 0 0 0-.593-.102c-.22 0-.417.034-.594.102a1.46 1.46 0 0 0-.461.277 1.2 1.2 0 0 0-.3.418 1.284 1.284 0 0 0-.106.523z"/></symbol><symbol viewBox="0 0 16 16" id="question-o" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zm-.778-4.151c0-.301.014-.575.044-.82a3.2 3.2 0 0 1 .154-.68c.073-.208.17-.4.294-.575.123-.176.278-.343.465-.503a4.81 4.81 0 0 0 .755-.758c.185-.242.277-.506.277-.793 0-.356-.074-.617-.222-.783-.148-.166-.37-.25-.667-.25a.92.92 0 0 0-.342.065.806.806 0 0 0-.29.199 1.04 1.04 0 0 0-.209.345 1.5 1.5 0 0 0-.088.506H5.082c.005-.51.092-.948.263-1.313.171-.364.401-.664.69-.899.29-.234.63-.406 1.023-.516a4.66 4.66 0 0 1 1.264-.164c.497 0 .944.058 1.34.174.397.117.733.289 1.008.517.276.227.487.51.633.847.146.337.218.727.218 1.17 0 .295-.042.56-.126.792a2.52 2.52 0 0 1-.349.65 4.4 4.4 0 0 1-.523.584c-.2.19-.414.389-.642.598a2.73 2.73 0 0 0-.332.349c-.089.114-.16.233-.212.359a1.868 1.868 0 0 0-.116.41 3.39 3.39 0 0 0-.044.489H7.222zm-.28 2.078c0-.164.03-.317.092-.458a1.05 1.05 0 0 1 .263-.366c.114-.103.248-.183.403-.243a1.45 1.45 0 0 1 .52-.089c.191 0 .364.03.52.09.154.059.289.14.403.242.114.103.201.224.263.366.061.141.092.294.092.458 0 .164-.03.316-.092.458a1.05 1.05 0 0 1-.263.365 1.278 1.278 0 0 1-.404.243 1.43 1.43 0 0 1-.52.089c-.19 0-.364-.03-.519-.089-.155-.06-.29-.14-.403-.243a1.05 1.05 0 0 1-.263-.365 1.135 1.135 0 0 1-.093-.458z"/></symbol><symbol viewBox="0 0 16 16" id="quote" xmlns="http://www.w3.org/2000/svg"><path d="M15 3v8a3 3 0 0 1-3 3 1 1 0 0 1 0-2 1 1 0 0 0 1-1V9h-2a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h3a1 1 0 0 1 1 1zM7 3v8a3 3 0 0 1-3 3 1 1 0 0 1 0-2 1 1 0 0 0 1-1V9H3a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h3a1 1 0 0 1 1 1z"/></symbol><symbol viewBox="0 0 16 16" id="redo" xmlns="http://www.w3.org/2000/svg"><path d="M4.666 4.423a5 5 0 1 1-.203 6.944 1 1 0 1 0-1.478 1.347 7 7 0 1 0 .12-9.556L1.842 2.137a.5.5 0 0 0-.815.385L1 7.26a.5.5 0 0 0 .607.492l4.629-1.013a.5.5 0 0 0 .207-.877L4.666 4.423z"/></symbol><symbol viewBox="0 0 16 16" id="remove" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M2 3a1 1 0 1 1 0-2h12a1 1 0 0 1 0 2v10a3 3 0 0 1-3 3H5a3 3 0 0 1-3-3V3zm3-2a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1H5zM4 3v10a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V3H4zm2.5 2a.5.5 0 0 1 .5.5v6a.5.5 0 1 1-1 0v-6a.5.5 0 0 1 .5-.5zm3 0a.5.5 0 0 1 .5.5v6a.5.5 0 1 1-1 0v-6a.5.5 0 0 1 .5-.5z"/></symbol><symbol viewBox="0 0 16 16" id="repeat" xmlns="http://www.w3.org/2000/svg"><path d="M11.494 4.423a5 5 0 1 0 .203 6.944 1 1 0 1 1 1.478 1.347 7 7 0 1 1-.12-9.556l1.262-1.021a.5.5 0 0 1 .815.385l.028 4.738a.5.5 0 0 1-.607.492L9.924 6.739a.5.5 0 0 1-.207-.877l1.777-1.439z"/></symbol><symbol viewBox="0 0 16 16" id="retry" xmlns="http://www.w3.org/2000/svg"><path d="M4.114 6.958a4 4 0 0 0 5.283 4.775 1 1 0 1 1 .712 1.87A6 6 0 0 1 2.182 6.44l-.741-.2a.5.5 0 0 1-.12-.915l2.195-1.268a.5.5 0 0 1 .683.183l1.268 2.196a.5.5 0 0 1-.563.733l-.79-.212zm7.777 2.084a4 4 0 0 0-5.284-4.775 1 1 0 0 1-.712-1.87 6 6 0 0 1 7.927 7.162l.742.2a.5.5 0 0 1 .12.915l-2.196 1.268a.5.5 0 0 1-.683-.183l-1.267-2.196a.5.5 0 0 1 .562-.733l.79.212z"/></symbol><symbol viewBox="0 0 16 16" id="scale" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M13.99 9a.792.792 0 0 0-.078-.231L13 7l-.912 1.769a.791.791 0 0 0-.077.231h1.978zm-10 0a.792.792 0 0 0-.078-.231L3 7l-.912 1.769A.791.791 0 0 0 2.011 9h1.978zM2 0h12a1 1 0 0 1 0 2H2a1 1 0 1 1 0-2zm3 14h6a1 1 0 0 1 0 2H5a1 1 0 0 1 0-2zM8 4a1 1 0 0 1 1 1v9H7V5a1 1 0 0 1 1-1zm-4.53-.714l2.265 4.735c.68 1.42.006 3.091-1.504 3.73A3.161 3.161 0 0 1 3 12c-1.657 0-3-1.263-3-2.821 0-.4.09-.794.264-1.158L2.53 3.286a.53.53 0 0 1 .94 0zm10 0l2.265 4.735c.68 1.42.006 3.091-1.504 3.73A3.161 3.161 0 0 1 13 12c-1.657 0-3-1.263-3-2.821 0-.4.09-.794.264-1.158l2.266-4.735a.53.53 0 0 1 .94 0z"/></symbol><symbol viewBox="0 0 16 16" id="screen-full" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M14 14v-2a1 1 0 0 1 2 0v3a.997.997 0 0 1-1 1h-3a1 1 0 0 1 0-2h2zM2 14v-2a1 1 0 0 0-2 0v3a1 1 0 0 0 1 1h3a1 1 0 0 0 0-2H2zM15.707.293A.997.997 0 0 1 16 1v3a1 1 0 0 1-2 0V2h-2a1 1 0 0 1 0-2h3c.276 0 .526.112.707.293zM2 2v2a1 1 0 1 1-2 0V1a.997.997 0 0 1 1-1h3a1 1 0 1 1 0 2H2zm4 4h4a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1z"/></symbol><symbol viewBox="0 0 16 16" id="screen-normal" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M3 3V1a1 1 0 1 1 2 0v3a.997.997 0 0 1-1 1H1a1 1 0 1 1 0-2h2zm10 0h2a1 1 0 0 1 0 2h-3a.997.997 0 0 1-1-1V1a1 1 0 0 1 2 0v2zM3 13H1a1 1 0 0 1 0-2h3a.997.997 0 0 1 1 1v3a1 1 0 0 1-2 0v-2zm10 0v2a1 1 0 0 1-2 0v-3a.997.997 0 0 1 1-1h3a1 1 0 0 1 0 2h-2zM6.5 7h3a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5v-1a.5.5 0 0 1 .5-.5z"/></symbol><symbol viewBox="0 0 12 16" id="scroll_down" xmlns="http://www.w3.org/2000/svg"><path class="ewfirst-triangle" d="M1.048 14.155a.508.508 0 0 0-.32.105c-.091.07-.136.154-.136.25v.71c0 .095.045.178.135.249.09.07.197.105.321.105h10.043a.51.51 0 0 0 .321-.105c.09-.07.136-.154.136-.25v-.71c0-.095-.045-.178-.136-.249a.508.508 0 0 0-.32-.105"/><path class="ewsecond-triangle" d="M.687 8.027c-.09-.087-.122-.16-.093-.22.028-.06.104-.09.228-.09h10.5c.123 0 .2.03.228.09.029.06-.002.133-.093.22L6.393 12.91a.458.458 0 0 1-.136.089h-.37a.626.626 0 0 1-.136-.09"/><path class="ewthird-triangle" d="M.687 1.027C.597.94.565.867.594.807c.028-.06.104-.09.228-.09h10.5c.123 0 .2.03.228.09.029.06-.002.133-.093.22L6.393 5.91a.458.458 0 0 1-.136.09h-.37a.626.626 0 0 1-.136-.09"/></symbol><symbol viewBox="0 0 12 16" id="scroll_up" xmlns="http://www.w3.org/2000/svg"><path d="M1.048 1.845a.508.508 0 0 1-.32-.105c-.091-.07-.136-.154-.136-.25V.78c0-.095.045-.178.135-.249a.508.508 0 0 1 .321-.105h10.043a.51.51 0 0 1 .321.105c.09.07.136.154.136.25v.71c0 .095-.045.178-.136.249a.508.508 0 0 1-.32.105M.687 7.973c-.09.087-.122.16-.093.22.028.06.104.09.228.09h10.5c.123 0 .2-.03.228-.09.029-.06-.002-.133-.093-.22L6.393 3.09A.458.458 0 0 0 6.257 3h-.37a.626.626 0 0 0-.136.09M.687 14.973c-.09.087-.122.16-.093.22.028.06.104.09.228.09h10.5c.123 0 .2-.03.228-.09.029-.06-.002-.133-.093-.22L6.393 10.09a.458.458 0 0 0-.136-.09h-.37a.626.626 0 0 0-.136.09"/></symbol><symbol viewBox="0 0 16 16" id="search" xmlns="http://www.w3.org/2000/svg"><path d="M8.853 8.854a3.5 3.5 0 1 0-4.95-4.95 3.5 3.5 0 0 0 4.95 4.95zm.207 2.328a5.5 5.5 0 1 1 2.121-2.121l3.329 3.328a1.5 1.5 0 0 1-2.121 2.121L9.06 11.182z"/></symbol><symbol viewBox="0 0 16 16" id="settings" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M2.415 5.803L1.317 4.084A.5.5 0 0 1 1.35 3.5l.805-.994a.5.5 0 0 1 .564-.153l1.878.704a5.975 5.975 0 0 1 1.65-.797L6.885.342A.5.5 0 0 1 7.36 0h1.28a.5.5 0 0 1 .474.342l.639 1.918a5.97 5.97 0 0 1 1.65.797l1.877-.704a.5.5 0 0 1 .565.153l.805.994a.5.5 0 0 1 .032.584l-1.097 1.719c.217.551.354 1.143.399 1.76l1.731 1.058a.5.5 0 0 1 .227.54l-.288 1.246a.5.5 0 0 1-.44.385l-2.008.19a6.026 6.026 0 0 1-1.142 1.431l.265 1.995a.5.5 0 0 1-.277.516l-1.15.56a.5.5 0 0 1-.576-.1l-1.424-1.452a6.047 6.047 0 0 1-1.804 0l-1.425 1.453a.5.5 0 0 1-.576.1l-1.15-.561a.5.5 0 0 1-.276-.516l.265-1.995a6.026 6.026 0 0 1-1.143-1.43l-2.008-.191a.5.5 0 0 1-.44-.385L.058 9.16a.5.5 0 0 1 .226-.539l1.732-1.058a5.968 5.968 0 0 1 .399-1.76zM8 11a3 3 0 1 0 0-6 3 3 0 0 0 0 6z"/></symbol><symbol viewBox="0 0 16 16" id="shield" xmlns="http://www.w3.org/2000/svg"><path d="M4 0h8a3 3 0 0 1 3 3v7.186a3 3 0 0 1-1.426 2.554l-4 2.465a3 3 0 0 1-3.148 0l-4-2.465A3 3 0 0 1 1 10.186V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v7.186a1 1 0 0 0 .475.852l4 2.464a1 1 0 0 0 1.05 0l4-2.464a1 1 0 0 0 .475-.852V3a1 1 0 0 0-1-1H4zm0 1.5a.5.5 0 0 1 .5-.5h4v8.837a.5.5 0 0 1-.753.431l-3.5-2.052A.5.5 0 0 1 4 9.785V3.5z"/></symbol><symbol viewBox="0 0 16 16" id="slight-frown" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zm-2.163-3.275a2.499 2.499 0 0 1 4.343.03.5.5 0 0 1-.871.49 1.5 1.5 0 0 0-2.607-.018.5.5 0 1 1-.865-.502zM5 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm6 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="slight-smile" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zM5 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm6 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm-5.163 2.254a.5.5 0 1 1 .865-.502 1.499 1.499 0 0 0 2.607-.018.5.5 0 1 1 .871.49 2.499 2.499 0 0 1-4.343.03z"/></symbol><symbol viewBox="0 0 16 16" id="smile" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zM6.18 6.27a.5.5 0 0 1-.873.487.5.5 0 0 0-.872-.003.5.5 0 1 1-.87-.495 1.5 1.5 0 0 1 2.616.012zm6 0a.5.5 0 1 1-.873.487.5.5 0 0 0-.872-.003.5.5 0 1 1-.87-.495 1.5 1.5 0 0 1 2.616.012zM5 9a3 3 0 0 0 6 0H5z"/></symbol><symbol viewBox="0 0 16 16" id="smiley" xmlns="http://www.w3.org/2000/svg"><path d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12zM5 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm6 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2zM5 9h6a3 3 0 0 1-6 0z"/></symbol><symbol viewBox="0 0 16 16" id="snippet" xmlns="http://www.w3.org/2000/svg"><path d="M10.67 9.31a3.001 3.001 0 0 1 2.062 5.546 3 3 0 0 1-3.771-4.559 1.007 1.007 0 0 1-.095-.137l-4.5-7.794a1 1 0 0 1 1.732-1l4.5 7.794c.028.05.052.1.071.15zm-3.283.35l-.289.5c-.028.05-.06.095-.095.137a3.001 3.001 0 0 1-3.77 4.56A3 3 0 0 1 5.294 9.31c.02-.051.043-.102.071-.15l.866-1.5 1.155 2zm2.31-4l-1.156-2 1.325-2.294a1 1 0 0 1 1.732 1L9.696 5.66zm-5.465 7.464a1 1 0 1 0 1-1.732 1 1 0 0 0-1 1.732zm7.5 0a1 1 0 1 0-1-1.732 1 1 0 0 0 1 1.732z"/></symbol><symbol viewBox="0 0 16 16" id="spam" xmlns="http://www.w3.org/2000/svg"><path d="M8.75.433l5.428 3.134a1.5 1.5 0 0 1 .75 1.299v6.268a1.5 1.5 0 0 1-.75 1.299L8.75 15.567a1.5 1.5 0 0 1-1.5 0l-5.428-3.134a1.5 1.5 0 0 1-.75-1.299V4.866a1.5 1.5 0 0 1 .75-1.299L7.25.433a1.5 1.5 0 0 1 1.5 0zM3.072 5.155v5.69L8 13.691l4.928-2.846v-5.69L8 2.309 3.072 5.155zM8 4a1 1 0 0 1 1 1v3a1 1 0 1 1-2 0V5a1 1 0 0 1 1-1zm0 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 14 14" id="spinner" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><circle cx="7" cy="7" r="6" stroke="#000" stroke-opacity=".1" stroke-width="2"/><path fill="#000" fill-opacity=".1" fill-rule="nonzero" d="M7 0a7 7 0 0 1 7 7h-2a5 5 0 0 0-5-5V0z"/></g></symbol><symbol viewBox="0 0 16 16" id="star" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M7.609 14.394l-3.465 1.473a1 1 0 0 1-1.39-.989l.276-4.024a1 1 0 0 0-.219-.694L.303 7.037A1 1 0 0 1 .83 5.443l3.715-.964a1 1 0 0 0 .609-.457L7.14.682a1 1 0 0 1 1.72 0l1.985 3.34a1 1 0 0 0 .609.457l3.715.964a1 1 0 0 1 .528 1.594L13.19 10.16a1 1 0 0 0-.219.694l.275 4.024a1 1 0 0 1-1.389.989l-3.465-1.473a1 1 0 0 0-.782 0z"/></symbol><symbol viewBox="0 0 16 16" id="star-o" xmlns="http://www.w3.org/2000/svg"><path d="M10.975 10.99a3 3 0 0 1 .655-2.083l1.54-1.916-2.219-.576a3 3 0 0 1-1.825-1.37L8 3.15 6.874 5.044a3 3 0 0 1-1.825 1.371l-2.218.576 1.54 1.916a3 3 0 0 1 .654 2.083l-.165 2.4 1.965-.836a3 3 0 0 1 2.348 0l1.965.836-.164-2.399zM7.61 14.394l-3.465 1.473a1 1 0 0 1-1.39-.989l.276-4.024a1 1 0 0 0-.219-.694L.303 7.037A1 1 0 0 1 .83 5.443l3.715-.964a1 1 0 0 0 .609-.457L7.14.682a1 1 0 0 1 1.72 0l1.985 3.34a1 1 0 0 0 .609.457l3.715.964a1 1 0 0 1 .528 1.594L13.19 10.16a1 1 0 0 0-.219.694l.275 4.024a1 1 0 0 1-1.389.989l-3.465-1.473a1 1 0 0 0-.782 0z"/></symbol><symbol viewBox="0 0 14 14" id="status_canceled" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M5.2 3.8l4.9 4.9c.2.2.2.5 0 .7l-.7.7c-.2.2-.5.2-.7 0L3.8 5.2c-.2-.2-.2-.5 0-.7l.7-.7c.2-.2.5-.2.7 0"/></g></symbol><symbol viewBox="0 0 22 22" id="status_canceled_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M8.171 5.971l7.7 7.7a.76.76 0 0 1 0 1.1l-1.1 1.1a.76.76 0 0 1-1.1 0l-7.7-7.7a.76.76 0 0 1 0-1.1l1.1-1.1a.76.76 0 0 1 1.1 0"/></symbol><symbol viewBox="0 0 16 16" id="status_closed" xmlns="http://www.w3.org/2000/svg"><path d="M7.536 8.657l2.828-2.83a1 1 0 0 1 1.414 1.416l-3.535 3.535a1 1 0 0 1-1.415.001l-2.12-2.12a1 1 0 1 1 1.413-1.415zM8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm0-2A6 6 0 1 0 8 2a6 6 0 0 0 0 12z"/></symbol><symbol viewBox="0 0 14 14" id="status_created" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><circle cx="7" cy="7" r="3.25"/></g></symbol><symbol viewBox="0 0 22 22" id="status_created_borderless" xmlns="http://www.w3.org/2000/svg"><circle cx="11" cy="11" r="5.107"/></symbol><symbol viewBox="0 0 14 14" id="status_failed" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M7 5.969L5.599 4.568a.29.29 0 0 0-.413.004l-.614.614a.294.294 0 0 0-.004.413L5.968 7l-1.4 1.401a.29.29 0 0 0 .004.413l.614.614c.113.114.3.117.413.004L7 8.032l1.401 1.4a.29.29 0 0 0 .413-.004l.614-.614a.294.294 0 0 0 .004-.413L8.032 7l1.4-1.401a.29.29 0 0 0-.004-.413l-.614-.614a.294.294 0 0 0-.413-.004L7 5.968z"/></g></symbol><symbol viewBox="0 0 22 22" id="status_failed_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M11 9.38L8.798 7.178a.455.455 0 0 0-.65.006l-.964.965a.462.462 0 0 0-.006.65L9.38 11l-2.202 2.202a.455.455 0 0 0 .006.65l.965.964a.462.462 0 0 0 .65.006L11 12.62l2.202 2.202a.455.455 0 0 0 .65-.006l.964-.965a.462.462 0 0 0 .006-.65L12.62 11l2.202-2.202a.455.455 0 0 0-.006-.65l-.965-.964a.462.462 0 0 0-.65-.006L11 9.38z"/></symbol><symbol viewBox="0 0 14 14" id="status_manual" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M10.5 7.63V6.37l-.787-.13c-.044-.175-.132-.349-.263-.61l.481-.652-.918-.913-.657.478a2.346 2.346 0 0 0-.612-.26L7.656 3.5H6.388l-.132.783c-.219.043-.394.13-.612.26l-.657-.478-.918.913.437.652c-.131.218-.175.392-.262.61l-.744.086v1.261l.787.13c.044.218.132.392.263.61l-.438.651.92.913.655-.434c.175.086.394.173.613.26l.131.783h1.313l.131-.783c.219-.043.394-.13.613-.26l.656.478.918-.913-.48-.652c.13-.218.218-.435.262-.61l.656-.13zM7 8.283a1.285 1.285 0 0 1-1.313-1.305c0-.739.57-1.304 1.313-1.304.744 0 1.313.565 1.313 1.304 0 .74-.57 1.305-1.313 1.305z"/></g></symbol><symbol viewBox="0 0 22 22" id="status_manual_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M16.5 11.99v-1.98l-1.238-.206c-.068-.273-.206-.546-.412-.956l.756-1.025-1.444-1.435-1.03.752a3.686 3.686 0 0 0-.963-.41L12.03 5.5h-1.994l-.206 1.23c-.343.068-.618.205-.962.41l-1.031-.752-1.444 1.435.687 1.025c-.206.341-.275.615-.412.956L5.5 9.941v1.981l1.237.205c.07.342.207.615.413.957l-.688 1.025 1.444 1.434 1.032-.683c.274.137.618.274.962.41l.206 1.23h2.063l.206-1.23c.344-.068.619-.205.963-.41l1.03.752 1.444-1.435-.756-1.025c.207-.341.344-.683.413-.956l1.031-.205zM11 13.017c-1.169 0-2.063-.889-2.063-2.05 0-1.162.894-2.05 2.063-2.05s2.063.888 2.063 2.05c0 1.161-.894 2.05-2.063 2.05z"/></symbol><symbol viewBox="0 0 22 22" id="status_notfound_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M12.822 11.29c.816-.581 1.421-1.348 1.683-2.322.603-2.243-.973-4.553-3.53-4.553-1.15 0-2.085.41-2.775 1.089-.42.413-.672.835-.8 1.167a1.179 1.179 0 0 0 2.2.847c.016-.043.1-.184.252-.334.264-.259.613-.412 1.123-.412.938 0 1.47.78 1.254 1.584-.105.39-.37.726-.773 1.012a3.25 3.25 0 0 1-.945.47 1.179 1.179 0 0 0-.874 1.138v2.234a1.179 1.179 0 1 0 2.358 0v-1.43a5.9 5.9 0 0 0 .827-.492z"/><ellipse cx="10.825" cy="16.711" rx="1.275" ry="1.322"/></symbol><symbol viewBox="0 0 14 14" id="status_open" xmlns="http://www.w3.org/2000/svg"><path d="M0 7c0-3.866 3.142-7 7-7 3.866 0 7 3.142 7 7 0 3.866-3.142 7-7 7-3.866 0-7-3.142-7-7z"/><path d="M1 7c0 3.309 2.69 6 6 6 3.309 0 6-2.69 6-6 0-3.309-2.69-6-6-6-3.309 0-6 2.69-6 6z" fill="#FFF"/><path d="M7 9.219a2.218 2.218 0 1 0 0-4.436A2.218 2.218 0 0 0 7 9.22zm0 1.12a3.338 3.338 0 1 1 0-6.676 3.338 3.338 0 0 1 0 6.676z"/></symbol><symbol viewBox="0 0 14 14" id="status_pending" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M4.7 5.3c0-.2.1-.3.3-.3h.9c.2 0 .3.1.3.3v3.4c0 .2-.1.3-.3.3H5c-.2 0-.3-.1-.3-.3V5.3m3 0c0-.2.1-.3.3-.3h.9c.2 0 .3.1.3.3v3.4c0 .2-.1.3-.3.3H8c-.2 0-.3-.1-.3-.3V5.3"/></g></symbol><symbol viewBox="0 0 22 22" id="status_pending_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M7.386 8.329c0-.315.157-.472.471-.472h1.414c.315 0 .472.157.472.472v5.342c0 .315-.157.472-.472.472H7.857c-.314 0-.471-.157-.471-.472V8.33m4.714 0c0-.315.157-.472.471-.472h1.415c.314 0 .471.157.471.472v5.342c0 .315-.157.472-.471.472H12.57c-.314 0-.471-.157-.471-.472V8.33"/></symbol><symbol viewBox="0 0 14 14" id="status_running" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M7 3c2.2 0 4 1.8 4 4s-1.8 4-4 4c-1.3 0-2.5-.7-3.3-1.7L7 7V3"/></g></symbol><symbol viewBox="0 0 22 22" id="status_running_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M11 4.714c3.457 0 6.286 2.829 6.286 6.286 0 3.457-2.829 6.286-6.286 6.286-2.043 0-3.929-1.1-5.186-2.672L11 11V4.714"/></symbol><symbol viewBox="0 0 14 14" id="status_skipped" xmlns="http://www.w3.org/2000/svg"><path d="M7 14A7 7 0 1 1 7 0a7 7 0 0 1 0 14z"/><path d="M7 13A6 6 0 1 0 7 1a6 6 0 0 0 0 12z" fill="#FFF"/><path d="M6.415 7.04L4.579 5.203a.295.295 0 0 1 .004-.416l.349-.349a.29.29 0 0 1 .416-.004l2.214 2.214a.289.289 0 0 1 .019.021l.132.133c.11.11.108.291 0 .398L5.341 9.573a.282.282 0 0 1-.398 0l-.331-.331a.285.285 0 0 1 0-.399L6.415 7.04zm2.54 0L7.119 5.203a.295.295 0 0 1 .004-.416l.349-.349a.29.29 0 0 1 .416-.004l2.214 2.214a.289.289 0 0 1 .019.021l.132.133c.11.11.108.291 0 .398L7.881 9.573a.282.282 0 0 1-.398 0l-.331-.331a.285.285 0 0 1 0-.399L8.955 7.04z"/></symbol><symbol viewBox="0 0 22 22" id="status_skipped_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M14.072 11.063l-2.82 2.82a.46.46 0 0 0-.001.652l.495.495a.457.457 0 0 0 .653-.001l3.7-3.7a.46.46 0 0 0 .001-.653l-.196-.196a.453.453 0 0 0-.03-.033l-3.479-3.479a.464.464 0 0 0-.654.007l-.548.548a.463.463 0 0 0-.007.654l2.886 2.886z"/><path d="M10.08 11.063l-2.819 2.82a.46.46 0 0 0-.002.652l.496.495a.457.457 0 0 0 .652-.001l3.7-3.7a.46.46 0 0 0 .002-.653l-.196-.196a.453.453 0 0 0-.03-.033l-3.48-3.479a.464.464 0 0 0-.653.007l-.548.548a.463.463 0 0 0-.007.654l2.886 2.886z"/></symbol><symbol viewBox="0 0 14 14" id="status_success" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M6.278 7.697L5.045 6.464a.296.296 0 0 0-.42-.002l-.613.614a.298.298 0 0 0 .002.42l1.91 1.909a.5.5 0 0 0 .703.005l.265-.265L9.997 6.04a.291.291 0 0 0-.009-.408l-.614-.614a.29.29 0 0 0-.408-.009L6.278 7.697z"/></g></symbol><symbol viewBox="0 0 22 22" id="status_success_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M9.866 12.095l-1.95-1.95a.462.462 0 0 0-.647.01l-.964.964a.46.46 0 0 0-.01.646l3.013 3.014a.787.787 0 0 0 1.106.008l.425-.425 4.854-4.853a.462.462 0 0 0 .002-.659l-.964-.964a.468.468 0 0 0-.658.002l-4.207 4.207z"/></symbol><symbol viewBox="0 0 14 14" id="status_success_solid" xmlns="http://www.w3.org/2000/svg"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7zm6.278.697L5.045 6.464a.296.296 0 0 0-.42-.002l-.613.614a.298.298 0 0 0 .002.42l1.91 1.909a.5.5 0 0 0 .703.005l.265-.265L9.997 6.04a.291.291 0 0 0-.009-.408l-.614-.614a.29.29 0 0 0-.408-.009L6.278 7.697z" fill-rule="evenodd"/></symbol><symbol viewBox="0 0 14 14" id="status_warning" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M6 3.5c0-.3.2-.5.5-.5h1c.3 0 .5.2.5.5v4c0 .3-.2.5-.5.5h-1c-.3 0-.5-.2-.5-.5v-4m0 6c0-.3.2-.5.5-.5h1c.3 0 .5.2.5.5v1c0 .3-.2.5-.5.5h-1c-.3 0-.5-.2-.5-.5v-1"/></g></symbol><symbol viewBox="0 0 22 22" id="status_warning_borderless" xmlns="http://www.w3.org/2000/svg"><path d="M9.429 5.5c0-.471.314-.786.785-.786h1.572c.471 0 .785.315.785.786v6.286c0 .471-.314.785-.785.785h-1.572c-.471 0-.785-.314-.785-.785V5.5m0 9.429c0-.472.314-.786.785-.786h1.572c.471 0 .785.314.785.786V16.5c0 .471-.314.786-.785.786h-1.572c-.471 0-.785-.315-.785-.786v-1.571"/></symbol><symbol viewBox="0 0 16 16" id="stop" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M2 0h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2z"/></symbol><symbol viewBox="0 0 16 16" id="task-done" xmlns="http://www.w3.org/2000/svg"><path d="M7.536 8.657l2.828-2.829a1 1 0 0 1 1.414 1.415l-3.535 3.535a.997.997 0 0 1-1.415 0l-2.12-2.121A1 1 0 0 1 6.12 7.243l1.415 1.414zM3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3z"/></symbol><symbol viewBox="0 0 16 16" id="template" xmlns="http://www.w3.org/2000/svg"><path d="M3 0h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V3a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H3zm.8 2h2.4a.8.8 0 0 1 .8.8v1.4a.8.8 0 0 1-.8.8H3.8a.8.8 0 0 1-.8-.8V4.8a.8.8 0 0 1 .8-.8zm4.7 0h4a.5.5 0 1 1 0 1h-4a.5.5 0 0 1 0-1zm0 2h4a.5.5 0 1 1 0 1h-4a.5.5 0 0 1 0-1zm-5 3h9a.5.5 0 1 1 0 1h-9a.5.5 0 0 1 0-1zm0 2h9a.5.5 0 1 1 0 1h-9a.5.5 0 1 1 0-1z"/></symbol><symbol viewBox="0 0 16 16" id="terminal" xmlns="http://www.w3.org/2000/svg"><path d="M7 8a.997.997 0 0 1-.293.707l-1.414 1.414a1 1 0 1 1-1.414-1.414L4.586 8l-.707-.707a1 1 0 1 1 1.414-1.414l1.414 1.414A.997.997 0 0 1 7 8zM4 0h8a4 4 0 0 1 4 4v8a4 4 0 0 1-4 4H4a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4zm0 2a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2H4zm5 7h2a1 1 0 0 1 0 2H9a1 1 0 0 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="thumb-down" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8.33 11h5.282a2 2 0 0 0 1.963-2.38l-.563-2.905a3 3 0 0 0-.243-.732l-1.103-2.286A3 3 0 0 0 10.964 1H7a3 3 0 0 0-3 3v6.3a2 2 0 0 0 .436 1.247l3.11 3.9a.632.632 0 0 0 .941.053l.137-.137a1 1 0 0 0 .28-.87L8.329 11zM1 10h2V3H1a1 1 0 0 0-1 1v5a1 1 0 0 0 1 1z"/></symbol><symbol viewBox="0 0 16 16" id="thumb-up" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8.33 5h5.282a2 2 0 0 1 1.963 2.38l-.563 2.905a3 3 0 0 1-.243.732l-1.103 2.286A3 3 0 0 1 10.964 15H7a3 3 0 0 1-3-3V5.7a2 2 0 0 1 .436-1.247l3.11-3.9A.632.632 0 0 1 8.487.5l.137.137a1 1 0 0 1 .28.87L8.329 5zM1 6h2v7H1a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1z"/></symbol><symbol viewBox="0 0 16 16" id="thumbtack" xmlns="http://www.w3.org/2000/svg"><path d="M7.125 9h-2.19a.5.5 0 0 1-.417-.777L6 6V2L5.362.724A.5.5 0 0 1 5.809 0h4.382a.5.5 0 0 1 .447.724L10 2v4l1.482 2.223a.5.5 0 0 1-.416.777H8.875L8 16l-.875-7z" fill-rule="evenodd"/></symbol><symbol viewBox="0 0 16 16" id="timer" xmlns="http://www.w3.org/2000/svg"><path d="M12.022 3.27l.77-.77a1 1 0 0 1 1.415 1.414l-.728.729a7 7 0 1 1-1.456-1.372zM8 14A5 5 0 1 0 8 4a5 5 0 0 0 0 10zm0-9a1 1 0 0 1 1 1v2a1 1 0 1 1-2 0V6a1 1 0 0 1 1-1zM6 0h4a1 1 0 0 1 0 2H6a1 1 0 1 1 0-2z"/></symbol><symbol viewBox="0 0 16 16" id="todo-add" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10 4V2a1 1 0 0 1 2 0v2h2a1 1 0 0 1 0 2h-2v2a1 1 0 0 1-2 0V6H8a1 1 0 1 1 0-2h2zm2 7a1 1 0 0 1 2 0v2a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3h2a1 1 0 1 1 0 2H3a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1v-2z"/></symbol><symbol viewBox="0 0 16 16" id="todo-done" xmlns="http://www.w3.org/2000/svg"><path d="M8.243 7.485l4.95-4.95a1 1 0 1 1 1.414 1.415L8.95 9.607a.997.997 0 0 1-1.414 0L4.707 6.778a1 1 0 0 1 1.414-1.414l2.122 2.121zM12 11a1 1 0 0 1 2 0v2a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3h2a1 1 0 1 1 0 2H3a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1v-2z"/></symbol><symbol viewBox="0 0 16 16" id="token" xmlns="http://www.w3.org/2000/svg"><path d="M3 2h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V5a3 3 0 0 1 3-3zm0 2a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V5a1 1 0 0 0-1-1H3zm1 5a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm4 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2zm4 0a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="unapproval" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M11.95 8.536l1.06-1.061a1 1 0 0 1 1.415 1.414l-1.061 1.06 1.06 1.061a1 1 0 0 1-1.414 1.415l-1.06-1.061-1.06 1.06a1 1 0 1 1-1.415-1.414l1.06-1.06-1.06-1.06a1 1 0 0 1 1.414-1.415l1.06 1.06zm-3.768-.33c.006.503.201 1.006.586 1.39l.353.354-.353.353a2 2 0 1 0 2.828 2.829l.354-.354.047.048C11.964 14.363 11.527 15 6 15c-5.924 0-6-.78-6-2.52S.964 8 6 8c.834 0 1.557.074 2.182.205zM5.976 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6z"/></symbol><symbol viewBox="0 0 16 16" id="unassignee" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M11 5h4a1 1 0 0 1 0 2h-4a1 1 0 0 1 0-2zM5.976 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6zM6 15c-5.924 0-6-.78-6-2.52S.964 8 6 8s6 2.692 6 4.48c0 1.788-.076 2.52-6 2.52z"/></symbol><symbol viewBox="0 0 16 16" id="unlink" xmlns="http://www.w3.org/2000/svg"><path d="M11.295 8.845l-.659-1.664a1.78 1.78 0 0 0 .04-.04l1.415-1.414c.586-.586.654-1.468.152-1.97s-1.384-.434-1.97.152L8.859 5.323a1.781 1.781 0 0 0-.04.04l-1.664-.658c.141-.208.305-.408.491-.594l1.415-1.414c1.366-1.367 3.424-1.525 4.596-.354 1.171 1.172 1.013 3.23-.354 4.596L11.89 8.354c-.186.186-.386.35-.594.491zm-2.45 2.45a4.075 4.075 0 0 1-.491.594l-1.415 1.414c-1.366 1.367-3.424 1.525-4.596.354-1.171-1.172-1.013-3.23.354-4.596L4.11 7.646c.186-.186.386-.35.594-.491l.659 1.664a1.781 1.781 0 0 0-.04.04l-1.415 1.414c-.586.586-.654 1.468-.152 1.97s1.384.434 1.97-.152l1.414-1.414a1.78 1.78 0 0 0 .04-.04l1.664.658zm3.812-2.088h2a.5.5 0 0 1 .5.5v.05a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1-.5-.5v-.05a.5.5 0 0 1 .5-.5zm-.384 2.116l1.415 1.414a.5.5 0 0 1 0 .708l-.037.036a.5.5 0 0 1-.707 0l-1.414-1.414a.5.5 0 0 1 0-.707l.036-.037a.5.5 0 0 1 .707 0zm-2.823 1.09a.5.5 0 0 1 .5-.5h.052a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.5.5H9.95a.5.5 0 0 1-.5-.5v-2zm-2.748-9.16a.5.5 0 0 1-.5.5h-.05a.5.5 0 0 1-.5-.5v-2a.5.5 0 0 1 .5-.5h.05a.5.5 0 0 1 .5.5v2zm-2.116.383a.5.5 0 0 1 0 .707l-.036.036a.5.5 0 0 1-.707 0L2.428 2.965a.5.5 0 0 1 0-.707l.037-.036a.5.5 0 0 1 .707 0l1.414 1.414zm-1.09 2.823h-2a.5.5 0 0 1-.5-.5v-.051a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 .5.5v.05a.5.5 0 0 1-.5.5z"/></symbol><symbol viewBox="0 0 16 16" id="user" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6zm0 8c-6.888 0-6.976-.78-6.976-2.52S2.144 8 8 8s6.976 2.692 6.976 4.48c0 1.788-.088 2.52-6.976 2.52z"/></symbol><symbol viewBox="0 0 16 16" id="users" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M10.521 8.01C15.103 8.19 16 10.755 16 12.48c0 1.533-.056 2.29-3.808 2.475.609-.54.808-1.331.808-2.475 0-1.911-.804-3.503-2.479-4.47zm-1.67-1.228A3.987 3.987 0 0 0 9.976 4a3.987 3.987 0 0 0-1.125-2.782 3 3 0 1 1 0 5.563zM5.976 7a3 3 0 1 1 0-6 3 3 0 0 1 0 6zM6 15c-5.924 0-6-.78-6-2.52S.964 8 6 8s6 2.692 6 4.48c0 1.788-.076 2.52-6 2.52z"/></symbol><symbol viewBox="0 0 16 16" id="volume-up" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M1 5h1v6H1a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1zm2 0l4.445-2.964A1 1 0 0 1 9 2.87v10.26a1 1 0 0 1-1.555.833L3 11V5zm10.283 7.89a.5.5 0 0 1-.66-.752A5.485 5.485 0 0 0 14.5 8c0-1.601-.687-3.09-1.865-4.128a.5.5 0 0 1 .661-.75A6.484 6.484 0 0 1 15.5 8a6.485 6.485 0 0 1-2.217 4.89zm-2.002-2.236a.5.5 0 1 1-.652-.758c.55-.472.871-1.157.871-1.896 0-.732-.315-1.411-.856-1.883a.5.5 0 0 1 .658-.753A3.492 3.492 0 0 1 12.5 8c0 1.033-.45 1.994-1.219 2.654z"/></symbol><symbol viewBox="0 0 16 16" id="warning" xmlns="http://www.w3.org/2000/svg"><path d="M15.34 10.479A3 3 0 0 1 12.756 15h-9.51A3 3 0 0 1 .66 10.479l4.755-8.083a3 3 0 0 1 5.172 0l4.755 8.083zm-6.478-7.07a1 1 0 0 0-1.724 0l-4.755 8.084A1 1 0 0 0 3.245 13h9.51a1 1 0 0 0 .862-1.507L8.862 3.41zM8 5a1 1 0 0 1 1 1v2a1 1 0 1 1-2 0V6a1 1 0 0 1 1-1zm0 7a1 1 0 1 1 0-2 1 1 0 0 1 0 2z"/></symbol><symbol viewBox="0 0 16 16" id="work" xmlns="http://www.w3.org/2000/svg"><path d="M12 3h1a3 3 0 0 1 3 3v7a3 3 0 0 1-3 3H3a3 3 0 0 1-3-3V6a3 3 0 0 1 3-3h1V2a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v1zM6 2v1h4V2H6zM3 5a1 1 0 0 0-1 1v7a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V6a1 1 0 0 0-1-1H3zm1.5 1a.5.5 0 0 1 .5.5v6a.5.5 0 1 1-1 0v-6a.5.5 0 0 1 .5-.5zm7 0a.5.5 0 0 1 .5.5v6a.5.5 0 1 1-1 0v-6a.5.5 0 0 1 .5-.5z"/></symbol></svg>
\ No newline at end of file diff --git a/app/assets/images/illustrations/image_comment_light_cursor.svg b/app/assets/images/illustrations/image_comment_light_cursor.svg new file mode 100644 index 00000000000..ac712ea0c96 --- /dev/null +++ b/app/assets/images/illustrations/image_comment_light_cursor.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 38 38"><g fill="none" fill-rule="evenodd"><circle cx="19" cy="19" r="18" fill="#FFF"/><path fill="#1F78D1" fill-rule="nonzero" d="M19 38C8.507 38 0 29.493 0 19S8.507 0 19 0s19 8.507 19 19-8.507 19-19 19zm0-2c9.389 0 17-7.611 17-17S28.389 2 19 2 2 9.611 2 19s7.611 17 17 17zm-6.293-8.293c-.63.63-1.707.184-1.707-.707V15a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3h-7.586l-3.707 3.707zM13 24.586l2.293-2.293A1 1 0 0 1 16 22h8a1 1 0 0 0 1-1v-6a1 1 0 0 0-1-1H14a1 1 0 0 0-1 1v9.586z"/></g></svg>
\ No newline at end of file diff --git a/app/assets/images/illustrations/image_comment_light_cursor@2x.svg b/app/assets/images/illustrations/image_comment_light_cursor@2x.svg new file mode 100644 index 00000000000..02943acd9d7 --- /dev/null +++ b/app/assets/images/illustrations/image_comment_light_cursor@2x.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 38 38"><g fill="none" fill-rule="evenodd"><circle cx="19" cy="19" r="18" fill="#FFF"/><path fill="#1F78D1" fill-rule="nonzero" d="M19 38C8.507 38 0 29.493 0 19S8.507 0 19 0s19 8.507 19 19-8.507 19-19 19zm0-2c9.389 0 17-7.611 17-17S28.389 2 19 2 2 9.611 2 19s7.611 17 17 17zm-6.293-8.293c-.63.63-1.707.184-1.707-.707V15a3 3 0 0 1 3-3h10a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3h-7.586l-3.707 3.707zM13 24.586l2.293-2.293A1 1 0 0 1 16 22h8a1 1 0 0 0 1-1v-6a1 1 0 0 0-1-1H14a1 1 0 0 0-1 1v9.586z"/></g></svg>
\ No newline at end of file diff --git a/app/assets/javascripts/activities.js b/app/assets/javascripts/activities.js index 5d060165f4b..6a0662ba903 100644 --- a/app/assets/javascripts/activities.js +++ b/app/assets/javascripts/activities.js @@ -1,9 +1,10 @@ /* eslint-disable no-param-reassign, class-methods-use-this */ -/* global Pager */ import Cookies from 'js-cookie'; +import Pager from './pager'; +import { localTimeAgo } from './lib/utils/datetime_utility'; -class Activities { +export default class Activities { constructor() { Pager.init(20, true, false, data => data, this.updateTooltips); @@ -15,7 +16,7 @@ class Activities { } updateTooltips() { - gl.utils.localTimeAgo($('.js-timeago', '.content_list')); + localTimeAgo($('.js-timeago', '.content_list')); } reloadActivities() { @@ -33,6 +34,3 @@ class Activities { $sender.closest('li').toggleClass('active'); } } - -window.gl = window.gl || {}; -window.gl.Activities = Activities; diff --git a/app/assets/javascripts/admin.js b/app/assets/javascripts/admin.js index 34669dd13d6..c1f7fa2aced 100644 --- a/app/assets/javascripts/admin.js +++ b/app/assets/javascripts/admin.js @@ -1,62 +1,59 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, one-var-declaration-per-line, no-unused-vars, no-else-return, prefer-arrow-callback, camelcase, quotes, comma-dangle, max-len */ - -window.Admin = (function() { - function Admin() { - var modal, showBlacklistType; - $('input#user_force_random_password').on('change', function(elem) { - var elems; - elems = $('#user_password, #user_password_confirmation'); - if ($(this).attr('checked')) { - return elems.val('').attr('disabled', true); - } else { - return elems.removeAttr('disabled'); - } - }); - $('body').on('click', '.js-toggle-colors-link', function(e) { - e.preventDefault(); - return $('.js-toggle-colors-container').toggle(); - }); - $('.log-tabs a').click(function(e) { - e.preventDefault(); - return $(this).tab('show'); - }); - $('.log-bottom').click(function(e) { - var visible_log; - e.preventDefault(); - visible_log = $(".file-content:visible"); - return visible_log.animate({ - scrollTop: visible_log.find('ol').height() - }, "fast"); - }); - modal = $('.change-owner-holder'); - $('.change-owner-link').bind("click", function(e) { - e.preventDefault(); - $(this).hide(); - return modal.show(); - }); - $('.change-owner-cancel-link').bind("click", function(e) { - e.preventDefault(); - modal.hide(); - return $('.change-owner-link').show(); - }); - $('li.project_member').bind('ajax:success', function() { - return gl.utils.refreshCurrentPage(); - }); - $('li.group_member').bind('ajax:success', function() { - return gl.utils.refreshCurrentPage(); - }); - showBlacklistType = function() { - if ($("input[name='blacklist_type']:checked").val() === 'file') { - $('.blacklist-file').show(); - return $('.blacklist-raw').hide(); - } else { - $('.blacklist-file').hide(); - return $('.blacklist-raw').show(); - } - }; - $("input[name='blacklist_type']").click(showBlacklistType); - showBlacklistType(); +import { refreshCurrentPage } from './lib/utils/url_utility'; + +function showBlacklistType() { + if ($('input[name="blacklist_type"]:checked').val() === 'file') { + $('.blacklist-file').show(); + $('.blacklist-raw').hide(); + } else { + $('.blacklist-file').hide(); + $('.blacklist-raw').show(); } +} + +export default function adminInit() { + const modal = $('.change-owner-holder'); + + $('input#user_force_random_password').on('change', function randomPasswordClick() { + const $elems = $('#user_password, #user_password_confirmation'); + if ($(this).attr('checked')) { + $elems.val('').attr('disabled', true); + } else { + $elems.removeAttr('disabled'); + } + }); + + $('body').on('click', '.js-toggle-colors-link', (e) => { + e.preventDefault(); + $('.js-toggle-colors-container').toggle(); + }); + + $('.log-tabs a').on('click', function logTabsClick(e) { + e.preventDefault(); + $(this).tab('show'); + }); + + $('.log-bottom').on('click', (e) => { + e.preventDefault(); + const $visibleLog = $('.file-content:visible'); + $visibleLog.animate({ + scrollTop: $visibleLog.find('ol').height(), + }, 'fast'); + }); + + $('.change-owner-link').on('click', function changeOwnerLinkClick(e) { + e.preventDefault(); + $(this).hide(); + modal.show(); + }); + + $('.change-owner-cancel-link').on('click', (e) => { + e.preventDefault(); + modal.hide(); + $('.change-owner-link').show(); + }); + + $('li.project_member, li.group_member').on('ajax:success', refreshCurrentPage); - return Admin; -})(); + $("input[name='blacklist_type']").on('click', showBlacklistType); + showBlacklistType(); +} diff --git a/app/assets/javascripts/aside.js b/app/assets/javascripts/aside.js deleted file mode 100644 index 88756884d16..00000000000 --- a/app/assets/javascripts/aside.js +++ /dev/null @@ -1,24 +0,0 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, quotes, prefer-arrow-callback, no-var, one-var, one-var-declaration-per-line, no-else-return, max-len */ - -window.Aside = (function() { - function Aside() { - $(document).off("click", "a.show-aside"); - $(document).on("click", 'a.show-aside', function(e) { - var btn, icon; - e.preventDefault(); - btn = $(e.currentTarget); - icon = btn.find('i'); - if (icon.hasClass('fa-angle-left')) { - btn.parent().find('section').hide(); - btn.parent().find('aside').fadeIn(); - return icon.removeClass('fa-angle-left').addClass('fa-angle-right'); - } else { - btn.parent().find('aside').hide(); - btn.parent().find('section').fadeIn(); - return icon.removeClass('fa-angle-right').addClass('fa-angle-left'); - } - }); - } - - return Aside; -})(); diff --git a/app/assets/javascripts/behaviors/secret_values.js b/app/assets/javascripts/behaviors/secret_values.js new file mode 100644 index 00000000000..1cf0b960eb0 --- /dev/null +++ b/app/assets/javascripts/behaviors/secret_values.js @@ -0,0 +1,42 @@ +import { n__ } from '../locale'; +import { convertPermissionToBoolean } from '../lib/utils/common_utils'; + +export default class SecretValues { + constructor(container) { + this.container = container; + } + + init() { + this.values = this.container.querySelectorAll('.js-secret-value'); + this.placeholders = this.container.querySelectorAll('.js-secret-value-placeholder'); + this.revealButton = this.container.querySelector('.js-secret-value-reveal-button'); + + this.revealText = n__('Reveal value', 'Reveal values', this.values.length); + this.hideText = n__('Hide value', 'Hide values', this.values.length); + + const isRevealed = convertPermissionToBoolean(this.revealButton.dataset.secretRevealStatus); + this.updateDom(isRevealed); + + this.revealButton.addEventListener('click', this.onRevealButtonClicked.bind(this)); + } + + onRevealButtonClicked() { + const previousIsRevealed = convertPermissionToBoolean( + this.revealButton.dataset.secretRevealStatus, + ); + this.updateDom(!previousIsRevealed); + } + + updateDom(isRevealed) { + this.values.forEach((value) => { + value.classList.toggle('hide', !isRevealed); + }); + + this.placeholders.forEach((placeholder) => { + placeholder.classList.toggle('hide', isRevealed); + }); + + this.revealButton.textContent = isRevealed ? this.hideText : this.revealText; + this.revealButton.dataset.secretRevealStatus = isRevealed; + } +} diff --git a/app/assets/javascripts/behaviors/toggler_behavior.js b/app/assets/javascripts/behaviors/toggler_behavior.js index b70b0a9bbf8..417ac31fc86 100644 --- a/app/assets/javascripts/behaviors/toggler_behavior.js +++ b/app/assets/javascripts/behaviors/toggler_behavior.js @@ -5,6 +5,7 @@ // %button.js-toggle-button // %div.js-toggle-content // +import { getLocationHash } from '../lib/utils/url_utility'; $(() => { function toggleContainer(container, toggleState) { @@ -32,7 +33,7 @@ $(() => { // If we're accessing a permalink, ensure it is not inside a // closed js-toggle-container! - const hash = window.gl.utils.getLocationHash(); + const hash = getLocationHash(); const anchor = hash && document.getElementById(hash); const container = anchor && $(anchor).closest('.js-toggle-container'); diff --git a/app/assets/javascripts/blob/blob_file_dropzone.js b/app/assets/javascripts/blob/blob_file_dropzone.js index 0d590a9dbc4..f7ae6f1cd12 100644 --- a/app/assets/javascripts/blob/blob_file_dropzone.js +++ b/app/assets/javascripts/blob/blob_file_dropzone.js @@ -1,6 +1,6 @@ /* eslint-disable func-names, object-shorthand, prefer-arrow-callback */ import Dropzone from 'dropzone'; -import '../lib/utils/url_utility'; +import { visitUrl } from '../lib/utils/url_utility'; import { HIDDEN_CLASS } from '../lib/utils/constants'; import csrf from '../lib/utils/csrf'; @@ -49,7 +49,7 @@ export default class BlobFileDropzone { }); this.on('success', function (header, response) { $('#modal-upload-blob').modal('hide'); - window.gl.utils.visitUrl(response.filePath); + visitUrl(response.filePath); }); this.on('maxfilesexceeded', function (file) { dropzoneMessage.addClass(HIDDEN_CLASS); diff --git a/app/assets/javascripts/blob/blob_line_permalink_updater.js b/app/assets/javascripts/blob/blob_line_permalink_updater.js index c8f68860fbd..d36d9f0de2d 100644 --- a/app/assets/javascripts/blob/blob_line_permalink_updater.js +++ b/app/assets/javascripts/blob/blob_line_permalink_updater.js @@ -1,7 +1,9 @@ +import { getLocationHash } from '../lib/utils/url_utility'; + const lineNumberRe = /^L[0-9]+/; const updateLineNumbersOnBlobPermalinks = (linksToUpdate) => { - const hash = gl.utils.getLocationHash(); + const hash = getLocationHash(); if (hash && lineNumberRe.test(hash)) { const hashUrlString = `#${hash}`; diff --git a/app/assets/javascripts/boards/components/board_sidebar.js b/app/assets/javascripts/boards/components/board_sidebar.js index faa76da964f..616de2347e1 100644 --- a/app/assets/javascripts/boards/components/board_sidebar.js +++ b/app/assets/javascripts/boards/components/board_sidebar.js @@ -1,9 +1,9 @@ /* eslint-disable comma-dangle, space-before-function-paren, no-new */ /* global MilestoneSelect */ -/* global Sidebar */ import Vue from 'vue'; import Flash from '../../flash'; +import Sidebar from '../../right_sidebar'; import eventHub from '../../sidebar/event_hub'; import assigneeTitle from '../../sidebar/components/assignees/assignee_title'; import assignees from '../../sidebar/components/assignees/assignees'; diff --git a/app/assets/javascripts/commit/image_file.js b/app/assets/javascripts/commit/image_file.js index 50d0cb5c86d..5662802525e 100644 --- a/app/assets/javascripts/commit/image_file.js +++ b/app/assets/javascripts/commit/image_file.js @@ -121,7 +121,7 @@ export default class ImageFile { return $('.swipe.view', this.file).each((function(_this) { return function(index, view) { var $swipeWrap, $swipeBar, $swipeFrame, wrapPadding, ref; - ref = this.prepareFrames(view), maxWidth = ref[0], maxHeight = ref[1]; + ref = _this.prepareFrames(view), maxWidth = ref[0], maxHeight = ref[1]; $swipeFrame = $('.swipe-frame', view); $swipeWrap = $('.swipe-wrap', view); $swipeBar = $('.swipe-bar', view); @@ -158,7 +158,7 @@ export default class ImageFile { return $('.onion-skin.view', this.file).each((function(_this) { return function(index, view) { var $frame, $track, $dragger, $frameAdded, framePadding, ref, dragging = false; - ref = this.prepareFrames(view), maxWidth = ref[0], maxHeight = ref[1]; + ref = _this.prepareFrames(view), maxWidth = ref[0], maxHeight = ref[1]; $frame = $('.onion-skin-frame', view); $frameAdded = $('.frame.added', view); $track = $('.drag-track', view); diff --git a/app/assets/javascripts/commits.js b/app/assets/javascripts/commits.js index 9b952ea7b60..3a03cbf6b90 100644 --- a/app/assets/javascripts/commits.js +++ b/app/assets/javascripts/commits.js @@ -1,9 +1,10 @@ /* eslint-disable func-names, wrap-iife, consistent-return, no-return-assign, no-param-reassign, one-var-declaration-per-line, no-unused-vars, prefer-template, object-shorthand, prefer-arrow-callback */ -/* global Pager */ import { pluralize } from './lib/utils/text_utility'; +import { localTimeAgo } from './lib/utils/datetime_utility'; +import Pager from './pager'; export default (function () { const CommitsList = {}; @@ -91,7 +92,7 @@ export default (function () { $commitsHeadersLast.find('span.commits-count').text(`${commitsCount} ${pluralize('commit', commitsCount)}`); } - gl.utils.localTimeAgo($processedData.find('.js-timeago')); + localTimeAgo($processedData.find('.js-timeago')); return processedData; }; diff --git a/app/assets/javascripts/compare.js b/app/assets/javascripts/compare.js index 9e5dbd64a7e..144caf1d278 100644 --- a/app/assets/javascripts/compare.js +++ b/app/assets/javascripts/compare.js @@ -1,7 +1,8 @@ /* eslint-disable func-names, space-before-function-paren, wrap-iife, quotes, no-var, object-shorthand, consistent-return, no-unused-vars, comma-dangle, vars-on-top, prefer-template, max-len */ +import { localTimeAgo } from './lib/utils/datetime_utility'; -window.Compare = (function() { - function Compare(opts) { +export default class Compare { + constructor(opts) { this.opts = opts; this.source_loading = $(".js-source-loading"); this.target_loading = $(".js-target-loading"); @@ -34,12 +35,12 @@ window.Compare = (function() { this.initialState(); } - Compare.prototype.initialState = function() { + initialState() { this.getSourceHtml(); - return this.getTargetHtml(); - }; + this.getTargetHtml(); + } - Compare.prototype.getTargetProject = function() { + getTargetProject() { return $.ajax({ url: this.opts.targetProjectUrl, data: { @@ -52,22 +53,22 @@ window.Compare = (function() { return $('.js-target-branch-dropdown .dropdown-content').html(html); } }); - }; + } - Compare.prototype.getSourceHtml = function() { - return this.sendAjax(this.opts.sourceBranchUrl, this.source_loading, '.mr_source_commit', { + getSourceHtml() { + return this.constructor.sendAjax(this.opts.sourceBranchUrl, this.source_loading, '.mr_source_commit', { ref: $("input[name='merge_request[source_branch]']").val() }); - }; + } - Compare.prototype.getTargetHtml = function() { - return this.sendAjax(this.opts.targetBranchUrl, this.target_loading, '.mr_target_commit', { + getTargetHtml() { + return this.constructor.sendAjax(this.opts.targetBranchUrl, this.target_loading, '.mr_target_commit', { target_project_id: $("input[name='merge_request[target_project_id]']").val(), ref: $("input[name='merge_request[target_branch]']").val() }); - }; + } - Compare.prototype.sendAjax = function(url, loading, target, data) { + static sendAjax(url, loading, target, data) { var $target; $target = $(target); return $.ajax({ @@ -81,10 +82,8 @@ window.Compare = (function() { loading.hide(); $target.html(html); var className = '.' + $target[0].className.replace(' ', '.'); - gl.utils.localTimeAgo($('.js-timeago', className)); + localTimeAgo($('.js-timeago', className)); } }); - }; - - return Compare; -})(); + } +} diff --git a/app/assets/javascripts/compare_autocomplete.js b/app/assets/javascripts/compare_autocomplete.js index 72c0d98d47c..e633ef8a29e 100644 --- a/app/assets/javascripts/compare_autocomplete.js +++ b/app/assets/javascripts/compare_autocomplete.js @@ -1,68 +1,60 @@ /* eslint-disable func-names, space-before-function-paren, one-var, no-var, one-var-declaration-per-line, object-shorthand, comma-dangle, prefer-arrow-callback, no-else-return, newline-per-chained-call, wrap-iife, max-len */ -window.CompareAutocomplete = (function() { - function CompareAutocomplete() { - this.initDropdown(); - } - - CompareAutocomplete.prototype.initDropdown = function() { - return $('.js-compare-dropdown').each(function() { - var $dropdown, selected; - $dropdown = $(this); - selected = $dropdown.data('selected'); - const $dropdownContainer = $dropdown.closest('.dropdown'); - const $fieldInput = $(`input[name="${$dropdown.data('field-name')}"]`, $dropdownContainer); - const $filterInput = $('input[type="search"]', $dropdownContainer); - $dropdown.glDropdown({ - data: function(term, callback) { - return $.ajax({ - url: $dropdown.data('refs-url'), - data: { - ref: $dropdown.data('ref'), - search: term, - } - }).done(function(refs) { - return callback(refs); - }); - }, - selectable: true, - filterable: true, - filterRemote: true, - fieldName: $dropdown.data('field-name'), - filterInput: 'input[type="search"]', - renderRow: function(ref) { - var link; - if (ref.header != null) { - return $('<li />').addClass('dropdown-header').text(ref.header); - } else { - link = $('<a />').attr('href', '#').addClass(ref === selected ? 'is-active' : '').text(ref).attr('data-ref', escape(ref)); - return $('<li />').append(link); +export default function initCompareAutocomplete() { + $('.js-compare-dropdown').each(function() { + var $dropdown, selected; + $dropdown = $(this); + selected = $dropdown.data('selected'); + const $dropdownContainer = $dropdown.closest('.dropdown'); + const $fieldInput = $(`input[name="${$dropdown.data('field-name')}"]`, $dropdownContainer); + const $filterInput = $('input[type="search"]', $dropdownContainer); + $dropdown.glDropdown({ + data: function(term, callback) { + return $.ajax({ + url: $dropdown.data('refs-url'), + data: { + ref: $dropdown.data('ref'), + search: term, } - }, - id: function(obj, $el) { - return $el.attr('data-ref'); - }, - toggleLabel: function(obj, $el) { - return $el.text().trim(); - } - }); - $filterInput.on('keyup', (e) => { - const keyCode = e.keyCode || e.which; - if (keyCode !== 13) return; - const text = $filterInput.val(); - $fieldInput.val(text); - $('.dropdown-toggle-text', $dropdown).text(text); - $dropdownContainer.removeClass('open'); - }); - - $dropdownContainer.on('click', '.dropdown-content a', (e) => { - $dropdown.prop('title', e.target.text.replace(/_+?/g, '-')); - if ($dropdown.hasClass('has-tooltip')) { - $dropdown.tooltip('fixTitle'); + }).done(function(refs) { + return callback(refs); + }); + }, + selectable: true, + filterable: true, + filterRemote: true, + fieldName: $dropdown.data('field-name'), + filterInput: 'input[type="search"]', + renderRow: function(ref) { + var link; + if (ref.header != null) { + return $('<li />').addClass('dropdown-header').text(ref.header); + } else { + link = $('<a />').attr('href', '#').addClass(ref === selected ? 'is-active' : '').text(ref).attr('data-ref', escape(ref)); + return $('<li />').append(link); } - }); + }, + id: function(obj, $el) { + return $el.attr('data-ref'); + }, + toggleLabel: function(obj, $el) { + return $el.text().trim(); + } + }); + $filterInput.on('keyup', (e) => { + const keyCode = e.keyCode || e.which; + if (keyCode !== 13) return; + const text = $filterInput.val(); + $fieldInput.val(text); + $('.dropdown-toggle-text', $dropdown).text(text); + $dropdownContainer.removeClass('open'); }); - }; - return CompareAutocomplete; -})(); + $dropdownContainer.on('click', '.dropdown-content a', (e) => { + $dropdown.prop('title', e.target.text.replace(/_+?/g, '-')); + if ($dropdown.hasClass('has-tooltip')) { + $dropdown.tooltip('fixTitle'); + } + }); + }); +} diff --git a/app/assets/javascripts/contextual_sidebar.js b/app/assets/javascripts/contextual_sidebar.js index 46b68ebe158..74520675a7c 100644 --- a/app/assets/javascripts/contextual_sidebar.js +++ b/app/assets/javascripts/contextual_sidebar.js @@ -9,7 +9,7 @@ export default class ContextualSidebar { } initDomElements() { - this.$page = $('.page-with-sidebar'); + this.$page = $('.layout-page'); this.$sidebar = $('.nav-sidebar'); this.$innerScroll = $('.nav-sidebar-inner-scroll', this.$sidebar); this.$overlay = $('.mobile-overlay'); @@ -28,7 +28,7 @@ export default class ContextualSidebar { this.$closeSidebar.on('click', () => this.toggleSidebarNav(false)); this.$overlay.on('click', () => this.toggleSidebarNav(false)); this.$sidebarToggle.on('click', () => { - const value = !this.$sidebar.hasClass('sidebar-icons-only'); + const value = !this.$sidebar.hasClass('sidebar-collapsed-desktop'); this.toggleCollapsedSidebar(value); }); @@ -43,16 +43,16 @@ export default class ContextualSidebar { } toggleSidebarNav(show) { - this.$sidebar.toggleClass('nav-sidebar-expanded', show); + this.$sidebar.toggleClass('sidebar-expanded-mobile', show); this.$overlay.toggleClass('mobile-nav-open', show); - this.$sidebar.removeClass('sidebar-icons-only'); + this.$sidebar.removeClass('sidebar-collapsed-desktop'); } toggleCollapsedSidebar(collapsed) { const breakpoint = bp.getBreakpointSize(); if (this.$sidebar.length) { - this.$sidebar.toggleClass('sidebar-icons-only', collapsed); + this.$sidebar.toggleClass('sidebar-collapsed-desktop', collapsed); this.$page.toggleClass('page-with-icon-sidebar', breakpoint === 'sm' ? true : collapsed); } ContextualSidebar.setCollapsedCookie(collapsed); diff --git a/app/assets/javascripts/deploy_keys/components/key.vue b/app/assets/javascripts/deploy_keys/components/key.vue index b41d464475f..2a05c6f001e 100644 --- a/app/assets/javascripts/deploy_keys/components/key.vue +++ b/app/assets/javascripts/deploy_keys/components/key.vue @@ -1,5 +1,6 @@ <script> import actionBtn from './action_btn.vue'; + import { getTimeago } from '../../lib/utils/datetime_utility'; export default { props: { @@ -21,7 +22,7 @@ }, computed: { timeagoDate() { - return gl.utils.getTimeago().format(this.deployKey.created_at); + return getTimeago().format(this.deployKey.created_at); }, editDeployKeyPath() { return `${this.endpoint}/${this.deployKey.id}/edit`; diff --git a/app/assets/javascripts/diff.js b/app/assets/javascripts/diff.js index c8874e48c09..a162424b3cf 100644 --- a/app/assets/javascripts/diff.js +++ b/app/assets/javascripts/diff.js @@ -1,4 +1,4 @@ -import './lib/utils/url_utility'; +import { getLocationHash } from './lib/utils/url_utility'; import FilesCommentButton from './files_comment_button'; import SingleFileDiff from './single_file_diff'; import imageDiffHelper from './image_diff/helpers/index'; @@ -31,7 +31,7 @@ export default class Diff { isBound = true; } - if (gl.utils.getLocationHash()) { + if (getLocationHash()) { this.highlightSelectedLine(); } @@ -73,7 +73,7 @@ export default class Diff { } openAnchoredDiff(cb) { - const locationHash = gl.utils.getLocationHash(); + const locationHash = getLocationHash(); const anchoredDiff = locationHash && locationHash.split('_')[0]; if (!anchoredDiff) return; @@ -128,7 +128,7 @@ export default class Diff { } // eslint-disable-next-line class-methods-use-this highlightSelectedLine() { - const hash = gl.utils.getLocationHash(); + const hash = getLocationHash(); const $diffFiles = $('.diff-file'); $diffFiles.find('.hll').removeClass('hll'); diff --git a/app/assets/javascripts/diff_notes/models/discussion.js b/app/assets/javascripts/diff_notes/models/discussion.js index dc43e4b2cc7..1b8a9af9390 100644 --- a/app/assets/javascripts/diff_notes/models/discussion.js +++ b/app/assets/javascripts/diff_notes/models/discussion.js @@ -2,6 +2,7 @@ /* global NoteModel */ import Vue from 'vue'; +import { localTimeAgo } from '../../lib/utils/datetime_utility'; class DiscussionModel { constructor (discussionId) { @@ -71,7 +72,7 @@ class DiscussionModel { $(`${discussionSelector} .discussion-header`).append(data.discussion_headline_html); } - gl.utils.localTimeAgo($('.js-timeago', `${discussionSelector}`)); + localTimeAgo($('.js-timeago', `${discussionSelector}`)); } else { $discussionHeadline.remove(); } diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index 678af8f7b7a..62867c56214 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -7,34 +7,35 @@ import IssuableForm from './issuable_form'; import LabelsSelect from './labels_select'; /* global MilestoneSelect */ import NewBranchForm from './new_branch_form'; -/* global NotificationsForm */ -/* global NotificationsDropdown */ +import NotificationsForm from './notifications_form'; +import notificationsDropdown from './notifications_dropdown'; import groupAvatar from './group_avatar'; import GroupLabelSubscription from './group_label_subscription'; -/* global LineHighlighter */ +import LineHighlighter from './line_highlighter'; import BuildArtifacts from './build_artifacts'; import CILintEditor from './ci_lint_editor'; import groupsSelect from './groups_select'; -/* global Search */ -/* global Admin */ +import Search from './search'; +import initAdmin from './admin'; import NamespaceSelect from './namespace_select'; import NewCommitForm from './new_commit_form'; import Project from './project'; import projectAvatar from './project_avatar'; -/* global MergeRequest */ -/* global Compare */ -/* global CompareAutocomplete */ -/* global ProjectFindFile */ +import MergeRequest from './merge_request'; +import Compare from './compare'; +import initCompareAutocomplete from './compare_autocomplete'; +import ProjectFindFile from './project_find_file'; import ProjectNew from './project_new'; import projectImport from './project_import'; import Labels from './labels'; import LabelManager from './label_manager'; -/* global Sidebar */ +import Sidebar from './right_sidebar'; import IssuableTemplateSelectors from './templates/issuable_template_selectors'; import Flash from './flash'; import CommitsList from './commits'; import Issue from './issue'; import BindInOut from './behaviors/bind_in_out'; +import SecretValues from './behaviors/secret_values'; import DeleteModal from './branches/branches_delete_modal'; import Group from './group'; import GroupsList from './groups_list'; @@ -90,7 +91,8 @@ import memberExpirationDate from './member_expiration_date'; import DueDateSelectors from './due_date_select'; import Diff from './diff'; import ProjectLabelSubscription from './project_label_subscription'; -import ProjectVariables from './project_variables'; +import SearchAutocomplete from './search_autocomplete'; +import Activities from './activities'; (function() { var Dispatcher; @@ -333,7 +335,7 @@ import ProjectVariables from './project_variables'; shortcut_handler = new ShortcutsIssuable(true); break; case 'dashboard:activity': - new gl.Activities(); + new Activities(); break; case 'projects:commit:show': new Diff(); @@ -354,7 +356,7 @@ import ProjectVariables from './project_variables'; $('.commit-info.branches').load(document.querySelector('.js-commit-box').dataset.commitPath); break; case 'projects:activity': - new gl.Activities(); + new Activities(); shortcut_handler = new ShortcutsNavigation(); break; case 'projects:commits:show': @@ -372,7 +374,7 @@ import ProjectVariables from './project_variables'; if ($('#tree-slider').length) new TreeView(); if ($('.blob-viewer').length) new BlobViewer(); - if ($('.project-show-activity').length) new gl.Activities(); + if ($('.project-show-activity').length) new Activities(); $('#tree-slider').waitForImages(function() { ajaxGet(document.querySelector('.js-tree-content').dataset.logsPath); }); @@ -406,13 +408,13 @@ import ProjectVariables from './project_variables'; }); break; case 'groups:activity': - new gl.Activities(); + new Activities(); break; case 'groups:show': const newGroupChildWrapper = document.querySelector('.js-new-project-subgroup'); shortcut_handler = new ShortcutsNavigation(); new NotificationsForm(); - new NotificationsDropdown(); + notificationsDropdown(); new ProjectsList(); if (newGroupChildWrapper) { @@ -525,8 +527,18 @@ import ProjectVariables from './project_variables'; case 'projects:settings:ci_cd:show': // Initialize expandable settings panels initSettingsPanels(); + + const runnerToken = document.querySelector('.js-secret-runner-token'); + if (runnerToken) { + const runnerTokenSecretValue = new SecretValues(runnerToken); + runnerTokenSecretValue.init(); + } case 'groups:settings:ci_cd:show': - new ProjectVariables(); + const secretVariableTable = document.querySelector('.js-secret-variable-table'); + if (secretVariableTable) { + const secretVariableTableValues = new SecretValues(secretVariableTable); + secretVariableTableValues.init(); + } break; case 'ci:lints:create': case 'ci:lints:show': @@ -583,7 +595,7 @@ import ProjectVariables from './project_variables'; // needed in rspec gl.u2fAuthenticate = u2fAuthenticate; case 'admin': - new Admin(); + initAdmin(); switch (path[1]) { case 'broadcast_messages': initBroadcastMessagesForm(); @@ -615,14 +627,14 @@ import ProjectVariables from './project_variables'; break; case 'profiles': new NotificationsForm(); - new NotificationsDropdown(); + notificationsDropdown(); break; case 'projects': new Project(); projectAvatar(); switch (path[1]) { case 'compare': - new CompareAutocomplete(); + initCompareAutocomplete(); break; case 'edit': shortcut_handler = new ShortcutsNavigation(); @@ -638,7 +650,7 @@ import ProjectVariables from './project_variables'; case 'show': new Star(); new ProjectNew(); - new NotificationsDropdown(); + notificationsDropdown(); break; case 'wikis': new Wikis(); @@ -683,7 +695,7 @@ import ProjectVariables from './project_variables'; Dispatcher.prototype.initSearch = function() { // Only when search form is present if ($('.search').length) { - return new gl.SearchAutocomplete(); + return new SearchAutocomplete(); } }; diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js index 69c57f923b6..2ba85c7da97 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js @@ -1,3 +1,4 @@ +import { visitUrl } from '../lib/utils/url_utility'; import Flash from '../flash'; import FilteredSearchContainer from './container'; import RecentSearchesRoot from './recent_searches_root'; @@ -566,7 +567,7 @@ class FilteredSearchManager { if (this.updateObject) { this.updateObject(parameterizedUrl); } else { - gl.utils.visitUrl(parameterizedUrl); + visitUrl(parameterizedUrl); } } diff --git a/app/assets/javascripts/fly_out_nav.js b/app/assets/javascripts/fly_out_nav.js index 98837c3b2a0..6110d961609 100644 --- a/app/assets/javascripts/fly_out_nav.js +++ b/app/assets/javascripts/fly_out_nav.js @@ -21,7 +21,7 @@ let headerHeight = 50; export const getHeaderHeight = () => headerHeight; -export const isSidebarCollapsed = () => sidebar && sidebar.classList.contains('sidebar-icons-only'); +export const isSidebarCollapsed = () => sidebar && sidebar.classList.contains('sidebar-collapsed-desktop'); export const canShowActiveSubItems = (el) => { if (el.classList.contains('active') && !isSidebarCollapsed()) { diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js index 7ca783d3af6..cf4a70e321e 100644 --- a/app/assets/javascripts/gl_dropdown.js +++ b/app/assets/javascripts/gl_dropdown.js @@ -2,6 +2,7 @@ /* global fuzzaldrinPlus */ import _ from 'underscore'; import fuzzaldrinPlus from 'fuzzaldrin-plus'; +import { visitUrl } from './lib/utils/url_utility'; import { isObject } from './lib/utils/type_utility'; var GitLabDropdown, GitLabDropdownFilter, GitLabDropdownRemote, GitLabDropdownInput; @@ -852,7 +853,7 @@ GitLabDropdown = (function() { if ($el.length) { var href = $el.attr('href'); if (href && href !== '#') { - gl.utils.visitUrl(href); + visitUrl(href); } else { $el.trigger('click'); } diff --git a/app/assets/javascripts/graphs/stat_graph_contributors.js b/app/assets/javascripts/graphs/stat_graph_contributors.js index e7232ca3712..743c049e9fb 100644 --- a/app/assets/javascripts/graphs/stat_graph_contributors.js +++ b/app/assets/javascripts/graphs/stat_graph_contributors.js @@ -1,13 +1,14 @@ /* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, camelcase, one-var-declaration-per-line, quotes, no-param-reassign, quote-props, comma-dangle, prefer-template, max-len, no-return-assign, no-shadow */ import _ from 'underscore'; -import d3 from 'd3'; import { ContributorsGraph, ContributorsAuthorGraph, ContributorsMasterGraph } from './stat_graph_contributors_graph'; import ContributorsStatGraphUtil from './stat_graph_contributors_util'; -import { n__ } from '../locale'; +import { n__, s__, createDateTimeFormat, sprintf } from '../locale'; export default (function() { - function ContributorsStatGraph() {} + function ContributorsStatGraph() { + this.dateFormat = createDateTimeFormat({ year: 'numeric', month: 'long', day: 'numeric' }); + } ContributorsStatGraph.prototype.init = function(log) { var author_commits, total_commits; @@ -95,11 +96,15 @@ export default (function() { }; ContributorsStatGraph.prototype.change_date_header = function() { - var print, print_date_format, x_domain; - x_domain = ContributorsGraph.prototype.x_domain; - print_date_format = d3.time.format("%B %e %Y"); - print = print_date_format(x_domain[0]) + " - " + print_date_format(x_domain[1]); - return $("#date_header").text(print); + const x_domain = ContributorsGraph.prototype.x_domain; + const formattedDateRange = sprintf( + s__('ContributorsPage|%{startDate} – %{endDate}'), + { + startDate: this.dateFormat.format(new Date(x_domain[0])), + endDate: this.dateFormat.format(new Date(x_domain[1])), + }, + ); + return $('#date_header').text(formattedDateRange); }; ContributorsStatGraph.prototype.redraw_author_commit_info = function(author) { diff --git a/app/assets/javascripts/graphs/stat_graph_contributors_graph.js b/app/assets/javascripts/graphs/stat_graph_contributors_graph.js index f64b4638485..187f3c008e8 100644 --- a/app/assets/javascripts/graphs/stat_graph_contributors_graph.js +++ b/app/assets/javascripts/graphs/stat_graph_contributors_graph.js @@ -1,6 +1,7 @@ /* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, max-len, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, comma-dangle, no-return-assign, prefer-arrow-callback, quotes, prefer-template, newline-per-chained-call, no-else-return, no-shadow */ import _ from 'underscore'; import d3 from 'd3'; +import { dateTickFormat } from '../lib/utils/tick_formats'; const extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }; const hasProp = {}.hasOwnProperty; @@ -93,9 +94,12 @@ export const ContributorsMasterGraph = (function(superClass) { extend(ContributorsMasterGraph, superClass); function ContributorsMasterGraph(data1) { + const $parentElement = $('#contributors-master'); + const parentPadding = parseFloat($parentElement.css('padding-left')) + parseFloat($parentElement.css('padding-right')); + this.data = data1; this.update_content = this.update_content.bind(this); - this.width = $('.content').width() - 70; + this.width = $('.content').width() - parentPadding - (this.MARGIN.left + this.MARGIN.right); this.height = 200; this.x = null; this.y = null; @@ -131,7 +135,10 @@ export const ContributorsMasterGraph = (function(superClass) { }; ContributorsMasterGraph.prototype.create_axes = function() { - this.x_axis = d3.svg.axis().scale(this.x).orient("bottom"); + this.x_axis = d3.svg.axis() + .scale(this.x) + .orient('bottom') + .tickFormat(dateTickFormat); return this.y_axis = d3.svg.axis().scale(this.y).orient("left").ticks(5); }; @@ -219,7 +226,11 @@ export const ContributorsAuthorGraph = (function(superClass) { }; ContributorsAuthorGraph.prototype.create_axes = function() { - this.x_axis = d3.svg.axis().scale(this.x).orient("bottom").ticks(8); + this.x_axis = d3.svg.axis() + .scale(this.x) + .orient('bottom') + .ticks(8) + .tickFormat(dateTickFormat); return this.y_axis = d3.svg.axis().scale(this.y).orient("left").ticks(5); }; diff --git a/app/assets/javascripts/groups/components/app.vue b/app/assets/javascripts/groups/components/app.vue index 2c0b6ab4ea8..241e026b84c 100644 --- a/app/assets/javascripts/groups/components/app.vue +++ b/app/assets/javascripts/groups/components/app.vue @@ -5,7 +5,7 @@ import eventHub from '../event_hub'; import { getParameterByName } from '../../lib/utils/common_utils'; import loadingIcon from '../../vue_shared/components/loading_icon.vue'; import { COMMON_STR } from '../constants'; - +import { mergeUrlParams } from '../../lib/utils/url_utility'; import groupsComponent from './groups.vue'; export default { @@ -93,7 +93,7 @@ export default { this.isLoading = false; $.scrollTo(0); - const currentPath = gl.utils.mergeUrlParams({ page }, window.location.href); + const currentPath = mergeUrlParams({ page }, window.location.href); window.history.replaceState({ page: currentPath, }, document.title, currentPath); diff --git a/app/assets/javascripts/groups/components/group_item.vue b/app/assets/javascripts/groups/components/group_item.vue index c76ce762b54..6421547bbde 100644 --- a/app/assets/javascripts/groups/components/group_item.vue +++ b/app/assets/javascripts/groups/components/group_item.vue @@ -1,4 +1,5 @@ <script> +import { visitUrl } from '../../lib/utils/url_utility'; import tooltip from '../../vue_shared/directives/tooltip'; import identicon from '../../vue_shared/components/identicon.vue'; import eventHub from '../event_hub'; @@ -60,7 +61,7 @@ export default { if (this.hasChildren) { eventHub.$emit('toggleChildren', this.group); } else { - gl.utils.visitUrl(this.group.relativePath); + visitUrl(this.group.relativePath); } } }, diff --git a/app/assets/javascripts/groups/components/item_actions.vue b/app/assets/javascripts/groups/components/item_actions.vue index 09cb79c1afd..58ba5aff7cf 100644 --- a/app/assets/javascripts/groups/components/item_actions.vue +++ b/app/assets/javascripts/groups/components/item_actions.vue @@ -1,7 +1,7 @@ <script> import { s__ } from '../../locale'; import tooltip from '../../vue_shared/directives/tooltip'; -import PopupDialog from '../../vue_shared/components/popup_dialog.vue'; +import modal from '../../vue_shared/components/modal.vue'; import eventHub from '../event_hub'; import { COMMON_STR } from '../constants'; import Icon from '../../vue_shared/components/icon.vue'; @@ -9,7 +9,7 @@ import Icon from '../../vue_shared/components/icon.vue'; export default { components: { Icon, - PopupDialog, + modal, }, directives: { tooltip, @@ -27,7 +27,7 @@ export default { }, data() { return { - dialogStatus: false, + modalStatus: false, }; }, computed: { @@ -43,10 +43,10 @@ export default { }, methods: { onLeaveGroup() { - this.dialogStatus = true; + this.modalStatus = true; }, leaveGroup(leaveConfirmed) { - this.dialogStatus = false; + this.modalStatus = false; if (leaveConfirmed) { eventHub.$emit('leaveGroup', this.group, this.parentGroup); } @@ -82,8 +82,8 @@ export default { class="fa fa-sign-out" aria-hidden="true"/> </a> - <popup-dialog - v-show="dialogStatus" + <modal + v-show="modalStatus" :primary-button-label="__('Leave')" kind="warning" :title="__('Are you sure?')" diff --git a/app/assets/javascripts/groups/new_group_child.js b/app/assets/javascripts/groups/new_group_child.js index 8e273579aae..a120d501e35 100644 --- a/app/assets/javascripts/groups/new_group_child.js +++ b/app/assets/javascripts/groups/new_group_child.js @@ -1,3 +1,4 @@ +import { visitUrl } from '../lib/utils/url_utility'; import DropLab from '../droplab/drop_lab'; import ISetter from '../droplab/plugins/input_setter'; @@ -54,9 +55,9 @@ export default class NewGroupChild { onClickNewGroupChildButton(e) { if (e.target.dataset.action === NEW_PROJECT) { - gl.utils.visitUrl(this.newGroupPath); + visitUrl(this.newGroupPath); } else if (e.target.dataset.action === NEW_SUBGROUP) { - gl.utils.visitUrl(this.subgroupPath); + visitUrl(this.subgroupPath); } } } diff --git a/app/assets/javascripts/image_diff/helpers/badge_helper.js b/app/assets/javascripts/image_diff/helpers/badge_helper.js index 6a6a668308d..eddaeda9578 100644 --- a/app/assets/javascripts/image_diff/helpers/badge_helper.js +++ b/app/assets/javascripts/image_diff/helpers/badge_helper.js @@ -19,12 +19,9 @@ export function addImageBadge(containerEl, { coordinate, badgeText, noteId }) { } export function addImageCommentBadge(containerEl, { coordinate, noteId }) { - const buttonEl = createImageBadge(noteId, coordinate, ['image-comment-badge', 'inverted']); - const iconEl = document.createElement('i'); - iconEl.className = 'fa fa-comment-o'; - iconEl.setAttribute('aria-label', 'comment'); + const buttonEl = createImageBadge(noteId, coordinate, ['image-comment-badge']); + buttonEl.innerHTML = gl.utils.spriteIcon('image-comment-dark'); - buttonEl.appendChild(iconEl); containerEl.appendChild(buttonEl); } diff --git a/app/assets/javascripts/init_issuable_sidebar.js b/app/assets/javascripts/init_issuable_sidebar.js index ada693afc46..5d4c1851fe5 100644 --- a/app/assets/javascripts/init_issuable_sidebar.js +++ b/app/assets/javascripts/init_issuable_sidebar.js @@ -2,7 +2,7 @@ /* global MilestoneSelect */ import LabelsSelect from './labels_select'; import IssuableContext from './issuable_context'; -/* global Sidebar */ +import Sidebar from './right_sidebar'; import DueDateSelectors from './due_date_select'; @@ -15,5 +15,5 @@ export default () => { new LabelsSelect(); new IssuableContext(sidebarOptions.currentUser); new DueDateSelectors(); - window.sidebar = new Sidebar(); + Sidebar.initialize(); }; diff --git a/app/assets/javascripts/issuable_bulk_update_sidebar.js b/app/assets/javascripts/issuable_bulk_update_sidebar.js index ba2b6737988..bf77b93b643 100644 --- a/app/assets/javascripts/issuable_bulk_update_sidebar.js +++ b/app/assets/javascripts/issuable_bulk_update_sidebar.js @@ -21,7 +21,7 @@ export default class IssuableBulkUpdateSidebar { } initDomElements() { - this.$page = $('.page-with-sidebar'); + this.$page = $('.layout-page'); this.$sidebar = $('.right-sidebar'); this.$sidebarInnerContainer = this.$sidebar.find('.issuable-sidebar'); this.$bulkEditCancelBtn = $('.js-bulk-update-menu-hide'); diff --git a/app/assets/javascripts/issue.js b/app/assets/javascripts/issue.js index 7de07e9403d..411c820cc43 100644 --- a/app/assets/javascripts/issue.js +++ b/app/assets/javascripts/issue.js @@ -8,18 +8,7 @@ import IssuablesHelper from './helpers/issuables_helper'; export default class Issue { constructor() { - if ($('a.btn-close').length) { - this.taskList = new TaskList({ - dataType: 'issue', - fieldName: 'description', - selector: '.detail-page-description', - onSuccess: (result) => { - document.querySelector('#task_status').innerText = result.task_status; - document.querySelector('#task_status_short').innerText = result.task_status_short; - } - }); - this.initIssueBtnEventListeners(); - } + if ($('a.btn-close').length) this.initIssueBtnEventListeners(); Issue.$btnNewBranch = $('#new-branch'); Issue.createMrDropdownWrap = document.querySelector('.create-mr-dropdown-wrap'); @@ -59,7 +48,7 @@ export default class Issue { }) .fail(() => new Flash(issueFailMessage)) .done((data) => { - const isClosedBadge = $('div.status-box-closed'); + const isClosedBadge = $('div.status-box-issue-closed'); const isOpenBadge = $('div.status-box-open'); const projectIssuesCounter = $('.issue_counter'); diff --git a/app/assets/javascripts/issue_show/components/app.vue b/app/assets/javascripts/issue_show/components/app.vue index 5bdc7c99503..25ebe5314e0 100644 --- a/app/assets/javascripts/issue_show/components/app.vue +++ b/app/assets/javascripts/issue_show/components/app.vue @@ -1,5 +1,6 @@ <script> import Visibility from 'visibilityjs'; +import { visitUrl } from '../../lib/utils/url_utility'; import Poll from '../../lib/utils/poll'; import eventHub from '../event_hub'; import Service from '../services/index'; @@ -8,7 +9,7 @@ import titleComponent from './title.vue'; import descriptionComponent from './description.vue'; import editedComponent from './edited.vue'; import formComponent from './form.vue'; -import '../../lib/utils/url_utility'; +import recaptchaModalImplementor from '../../vue_shared/mixins/recaptcha_modal_implementor'; export default { props: { @@ -149,6 +150,11 @@ export default { editedComponent, formComponent, }, + + mixins: [ + recaptchaModalImplementor, + ], + methods: { openForm() { if (!this.showForm) { @@ -164,12 +170,14 @@ export default { closeForm() { this.showForm = false; }, + updateIssuable() { this.service.updateIssuable(this.store.formState) .then(res => res.json()) + .then(data => this.checkForSpam(data)) .then((data) => { if (location.pathname !== data.web_url) { - gl.utils.visitUrl(data.web_url); + visitUrl(data.web_url); } return this.service.getData(); @@ -179,11 +187,24 @@ export default { this.store.updateState(data); eventHub.$emit('close.form'); }) - .catch(() => { - eventHub.$emit('close.form'); - window.Flash(`Error updating ${this.issuableType}`); + .catch((error) => { + if (error && error.name === 'SpamError') { + this.openRecaptcha(); + } else { + eventHub.$emit('close.form'); + window.Flash(`Error updating ${this.issuableType}`); + } }); }, + + closeRecaptchaModal() { + this.store.setFormState({ + updateLoading: false, + }); + + this.closeRecaptcha(); + }, + deleteIssuable() { this.service.deleteIssuable() .then(res => res.json()) @@ -191,7 +212,7 @@ export default { // Stop the poll so we don't get 404's with the issuable not existing this.poll.stop(); - gl.utils.visitUrl(data.web_url); + visitUrl(data.web_url); }) .catch(() => { eventHub.$emit('close.form'); @@ -237,9 +258,9 @@ export default { </script> <template> - <div> +<div> + <div v-if="canUpdate && showForm"> <form-component - v-if="canUpdate && showForm" :form-state="formState" :can-destroy="canDestroy" :issuable-templates="issuableTemplates" @@ -251,30 +272,37 @@ export default { :can-attach-file="canAttachFile" :enable-autocomplete="enableAutocomplete" /> - <div v-else> - <title-component - :issuable-ref="issuableRef" - :can-update="canUpdate" - :title-html="state.titleHtml" - :title-text="state.titleText" - :show-inline-edit-button="showInlineEditButton" - /> - <description-component - v-if="state.descriptionHtml" - :can-update="canUpdate" - :description-html="state.descriptionHtml" - :description-text="state.descriptionText" - :updated-at="state.updatedAt" - :task-status="state.taskStatus" - :issuable-type="issuableType" - :update-url="updateEndpoint" - /> - <edited-component - v-if="hasUpdated" - :updated-at="state.updatedAt" - :updated-by-name="state.updatedByName" - :updated-by-path="state.updatedByPath" - /> - </div> + + <recaptcha-modal + v-show="showRecaptcha" + :html="recaptchaHTML" + @close="closeRecaptchaModal" + /> + </div> + <div v-else> + <title-component + :issuable-ref="issuableRef" + :can-update="canUpdate" + :title-html="state.titleHtml" + :title-text="state.titleText" + :show-inline-edit-button="showInlineEditButton" + /> + <description-component + v-if="state.descriptionHtml" + :can-update="canUpdate" + :description-html="state.descriptionHtml" + :description-text="state.descriptionText" + :updated-at="state.updatedAt" + :task-status="state.taskStatus" + :issuable-type="issuableType" + :update-url="updateEndpoint" + /> + <edited-component + v-if="hasUpdated" + :updated-at="state.updatedAt" + :updated-by-name="state.updatedByName" + :updated-by-path="state.updatedByPath" + /> </div> +</div> </template> diff --git a/app/assets/javascripts/issue_show/components/description.vue b/app/assets/javascripts/issue_show/components/description.vue index b7559ced946..c3f2bf130bb 100644 --- a/app/assets/javascripts/issue_show/components/description.vue +++ b/app/assets/javascripts/issue_show/components/description.vue @@ -1,9 +1,14 @@ <script> import animateMixin from '../mixins/animate'; import TaskList from '../../task_list'; + import recaptchaModalImplementor from '../../vue_shared/mixins/recaptcha_modal_implementor'; export default { - mixins: [animateMixin], + mixins: [ + animateMixin, + recaptchaModalImplementor, + ], + props: { canUpdate: { type: Boolean, @@ -51,6 +56,7 @@ this.updateTaskStatusText(); }, }, + methods: { renderGFM() { $(this.$refs['gfm-content']).renderGFM(); @@ -61,9 +67,19 @@ dataType: this.issuableType, fieldName: 'description', selector: '.detail-page-description', + onSuccess: this.taskListUpdateSuccess.bind(this), }); } }, + + taskListUpdateSuccess(data) { + try { + this.checkForSpam(data); + } catch (error) { + if (error && error.name === 'SpamError') this.openRecaptcha(); + } + }, + updateTaskStatusText() { const taskRegexMatches = this.taskStatus.match(/(\d+) of ((?!0)\d+)/); const $issuableHeader = $('.issuable-meta'); @@ -109,5 +125,11 @@ :data-update-url="updateUrl" > </textarea> + + <recaptcha-modal + v-show="showRecaptcha" + :html="recaptchaHTML" + @close="closeRecaptcha" + /> </div> </template> diff --git a/app/assets/javascripts/issue_show/components/fields/description.vue b/app/assets/javascripts/issue_show/components/fields/description.vue index 52fe4ecd08b..4e577546551 100644 --- a/app/assets/javascripts/issue_show/components/fields/description.vue +++ b/app/assets/javascripts/issue_show/components/fields/description.vue @@ -53,7 +53,7 @@ <textarea id="issue-description" class="note-textarea js-gfm-input js-autosize markdown-area" - data-supports-quick-actionss="false" + data-supports-quick-actions="false" aria-label="Description" v-model="formState.description" ref="textarea" diff --git a/app/assets/javascripts/issue_show/index.js b/app/assets/javascripts/issue_show/index.js index a21ce41e65e..7b762496ba5 100644 --- a/app/assets/javascripts/issue_show/index.js +++ b/app/assets/javascripts/issue_show/index.js @@ -7,7 +7,7 @@ document.addEventListener('DOMContentLoaded', () => { const initialDataEl = document.getElementById('js-issuable-app-initial-data'); const props = JSON.parse(initialDataEl.innerHTML.replace(/"/g, '"')); - $('.issuable-edit').on('click', (e) => { + $('.js-issuable-edit').on('click', (e) => { e.preventDefault(); eventHub.$emit('open.form'); diff --git a/app/assets/javascripts/job.js b/app/assets/javascripts/job.js index cf8fda9a4fa..198a7823381 100644 --- a/app/assets/javascripts/job.js +++ b/app/assets/javascripts/job.js @@ -1,7 +1,9 @@ import _ from 'underscore'; +import { visitUrl } from './lib/utils/url_utility'; import bp from './breakpoints'; import { bytesToKiB } from './lib/utils/number_utils'; import { setCiStatusFavicon } from './lib/utils/common_utils'; +import { timeFor } from './lib/utils/datetime_utility'; export default class Job { constructor(options) { @@ -9,7 +11,7 @@ export default class Job { this.state = null; this.options = options || $('.js-build-options').data(); - this.pageUrl = this.options.pageUrl; + this.pagePath = this.options.pagePath; this.buildStatus = this.options.buildStatus; this.state = this.options.logState; this.buildStage = this.options.buildStage; @@ -167,11 +169,11 @@ export default class Job { getBuildTrace() { return $.ajax({ - url: `${this.pageUrl}/trace.json`, + url: `${this.pagePath}/trace.json`, data: { state: this.state }, }) .done((log) => { - setCiStatusFavicon(`${this.pageUrl}/status.json`); + setCiStatusFavicon(`${this.pagePath}/status.json`); if (log.state) { this.state = log.state; @@ -209,7 +211,7 @@ export default class Job { } if (log.status !== this.buildStatus) { - gl.utils.visitUrl(this.pageUrl); + visitUrl(this.pagePath); } }) .fail(() => { @@ -260,7 +262,7 @@ export default class Job { if ($date.length) { const date = $date.text(); return $date.text( - gl.utils.timeFor(new Date(date.replace(/([0-9]+)-([0-9]+)-([0-9]+)/g, '$1/$2/$3')), ' '), + timeFor(new Date(date.replace(/([0-9]+)-([0-9]+)-([0-9]+)/g, '$1/$2/$3'))), ); } } diff --git a/app/assets/javascripts/lib/utils/cache.js b/app/assets/javascripts/lib/utils/cache.js index 3141f1eeafc..596bd1e388a 100644 --- a/app/assets/javascripts/lib/utils/cache.js +++ b/app/assets/javascripts/lib/utils/cache.js @@ -1,4 +1,4 @@ -class Cache { +export default class Cache { constructor() { this.internalStorage = { }; } @@ -15,5 +15,3 @@ class Cache { delete this.internalStorage[key]; } } - -export default Cache; diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js index 33cc807912c..b5328c77b25 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js +++ b/app/assets/javascripts/lib/utils/common_utils.js @@ -1,3 +1,4 @@ +import { getLocationHash } from './url_utility'; export const getPagePath = (index = 0) => $('body').attr('data-page').split(':')[index]; @@ -65,7 +66,7 @@ export const disableButtonIfEmptyField = (fieldSelector, buttonSelector, eventNa // automatically adjust scroll position for hash urls taking the height of the navbar into account // https://github.com/twitter/bootstrap/issues/1768 export const handleLocationHash = () => { - let hash = window.gl.utils.getLocationHash(); + let hash = getLocationHash(); if (!hash) return; // This is required to handle non-unicode characters in hash diff --git a/app/assets/javascripts/lib/utils/constants.js b/app/assets/javascripts/lib/utils/constants.js index 7a72509d234..9a61003ef30 100644 --- a/app/assets/javascripts/lib/utils/constants.js +++ b/app/assets/javascripts/lib/utils/constants.js @@ -1,3 +1,2 @@ -/* eslint-disable import/prefer-default-export */ export const BYTES_IN_KIB = 1024; export const HIDDEN_CLASS = 'hidden'; diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js index d0578b230b1..198b5164c92 100644 --- a/app/assets/javascripts/lib/utils/datetime_utility.js +++ b/app/assets/javascripts/lib/utils/datetime_utility.js @@ -1,9 +1,6 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-param-reassign, no-cond-assign, comma-dangle, no-unused-expressions, prefer-template, max-len */ - import timeago from 'timeago.js'; import dateFormat from 'vendor/date.format'; import { pluralize } from './text_utility'; - import { lang, s__, @@ -12,121 +9,125 @@ import { window.timeago = timeago; window.dateFormat = dateFormat; -(function() { - (function(w) { - var base; - var timeagoInstance; +/** + * Given a date object returns the day of the week in English + * @param {date} date + * @returns {String} + */ +export const getDayName = date => ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'][date.getDay()]; - if (w.gl == null) { - w.gl = {}; - } - if ((base = w.gl).utils == null) { - base.utils = {}; - } - w.gl.utils.days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']; +/** + * @example + * dateFormat('2017-12-05','mmm d, yyyy h:MMtt Z' ) -> "Dec 5, 2017 12:00am GMT+0000" + * @param {date} datetime + * @returns {String} + */ +export const formatDate = datetime => dateFormat(datetime, 'mmm d, yyyy h:MMtt Z'); - w.gl.utils.formatDate = function(datetime) { - return dateFormat(datetime, 'mmm d, yyyy h:MMtt Z'); +let timeagoInstance; +/** + * Sets a timeago Instance + */ +export function getTimeago() { + if (!timeagoInstance) { + const localeRemaining = function getLocaleRemaining(number, index) { + return [ + [s__('Timeago|less than a minute ago'), s__('Timeago|in a while')], + [s__('Timeago|less than a minute ago'), s__('Timeago|%s seconds remaining')], + [s__('Timeago|about a minute ago'), s__('Timeago|1 minute remaining')], + [s__('Timeago|%s minutes ago'), s__('Timeago|%s minutes remaining')], + [s__('Timeago|about an hour ago'), s__('Timeago|1 hour remaining')], + [s__('Timeago|about %s hours ago'), s__('Timeago|%s hours remaining')], + [s__('Timeago|a day ago'), s__('Timeago|1 day remaining')], + [s__('Timeago|%s days ago'), s__('Timeago|%s days remaining')], + [s__('Timeago|a week ago'), s__('Timeago|1 week remaining')], + [s__('Timeago|%s weeks ago'), s__('Timeago|%s weeks remaining')], + [s__('Timeago|a month ago'), s__('Timeago|1 month remaining')], + [s__('Timeago|%s months ago'), s__('Timeago|%s months remaining')], + [s__('Timeago|a year ago'), s__('Timeago|1 year remaining')], + [s__('Timeago|%s years ago'), s__('Timeago|%s years remaining')], + ][index]; }; - - w.gl.utils.getDayName = function(date) { - return this.days[date.getDay()]; + const locale = function getLocale(number, index) { + return [ + [s__('Timeago|less than a minute ago'), s__('Timeago|in a while')], + [s__('Timeago|less than a minute ago'), s__('Timeago|in %s seconds')], + [s__('Timeago|about a minute ago'), s__('Timeago|in 1 minute')], + [s__('Timeago|%s minutes ago'), s__('Timeago|in %s minutes')], + [s__('Timeago|about an hour ago'), s__('Timeago|in 1 hour')], + [s__('Timeago|about %s hours ago'), s__('Timeago|in %s hours')], + [s__('Timeago|a day ago'), s__('Timeago|in 1 day')], + [s__('Timeago|%s days ago'), s__('Timeago|in %s days')], + [s__('Timeago|a week ago'), s__('Timeago|in 1 week')], + [s__('Timeago|%s weeks ago'), s__('Timeago|in %s weeks')], + [s__('Timeago|a month ago'), s__('Timeago|in 1 month')], + [s__('Timeago|%s months ago'), s__('Timeago|in %s months')], + [s__('Timeago|a year ago'), s__('Timeago|in 1 year')], + [s__('Timeago|%s years ago'), s__('Timeago|in %s years')], + ][index]; }; - w.gl.utils.localTimeAgo = function($timeagoEls, setTimeago = true) { - $timeagoEls.each((i, el) => { - if (setTimeago) { - // Recreate with custom template - $(el).tooltip({ - template: '<div class="tooltip local-timeago" role="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>' - }); - } + timeago.register(lang, locale); + timeago.register(`${lang}-remaining`, localeRemaining); + timeagoInstance = timeago(); + } - el.classList.add('js-timeago-render'); - }); + return timeagoInstance; +} - gl.utils.renderTimeago($timeagoEls); - }; +/** + * For the given element, renders a timeago instance. + * @param {jQuery} $els + */ +export const renderTimeago = ($els) => { + const timeagoEls = $els || document.querySelectorAll('.js-timeago-render'); - w.gl.utils.getTimeago = function() { - var locale; - - if (!timeagoInstance) { - const localeRemaining = function(number, index) { - return [ - [s__('Timeago|less than a minute ago'), s__('Timeago|in a while')], - [s__('Timeago|less than a minute ago'), s__('Timeago|%s seconds remaining')], - [s__('Timeago|about a minute ago'), s__('Timeago|1 minute remaining')], - [s__('Timeago|%s minutes ago'), s__('Timeago|%s minutes remaining')], - [s__('Timeago|about an hour ago'), s__('Timeago|1 hour remaining')], - [s__('Timeago|about %s hours ago'), s__('Timeago|%s hours remaining')], - [s__('Timeago|a day ago'), s__('Timeago|1 day remaining')], - [s__('Timeago|%s days ago'), s__('Timeago|%s days remaining')], - [s__('Timeago|a week ago'), s__('Timeago|1 week remaining')], - [s__('Timeago|%s weeks ago'), s__('Timeago|%s weeks remaining')], - [s__('Timeago|a month ago'), s__('Timeago|1 month remaining')], - [s__('Timeago|%s months ago'), s__('Timeago|%s months remaining')], - [s__('Timeago|a year ago'), s__('Timeago|1 year remaining')], - [s__('Timeago|%s years ago'), s__('Timeago|%s years remaining')] - ][index]; - }; - locale = function(number, index) { - return [ - [s__('Timeago|less than a minute ago'), s__('Timeago|in a while')], - [s__('Timeago|less than a minute ago'), s__('Timeago|in %s seconds')], - [s__('Timeago|about a minute ago'), s__('Timeago|in 1 minute')], - [s__('Timeago|%s minutes ago'), s__('Timeago|in %s minutes')], - [s__('Timeago|about an hour ago'), s__('Timeago|in 1 hour')], - [s__('Timeago|about %s hours ago'), s__('Timeago|in %s hours')], - [s__('Timeago|a day ago'), s__('Timeago|in 1 day')], - [s__('Timeago|%s days ago'), s__('Timeago|in %s days')], - [s__('Timeago|a week ago'), s__('Timeago|in 1 week')], - [s__('Timeago|%s weeks ago'), s__('Timeago|in %s weeks')], - [s__('Timeago|a month ago'), s__('Timeago|in 1 month')], - [s__('Timeago|%s months ago'), s__('Timeago|in %s months')], - [s__('Timeago|a year ago'), s__('Timeago|in 1 year')], - [s__('Timeago|%s years ago'), s__('Timeago|in %s years')] - ][index]; - }; - - timeago.register(lang, locale); - timeago.register(`${lang}-remaining`, localeRemaining); - timeagoInstance = timeago(); - } - - return timeagoInstance; - }; + // timeago.js sets timeouts internally for each timeago value to be updated in real time + getTimeago().render(timeagoEls, lang); +}; - w.gl.utils.timeFor = function(time, suffix, expiredLabel) { - var timefor; - if (!time) { - return ''; - } - if (new Date(time) < new Date()) { - expiredLabel || (expiredLabel = s__('Timeago|Past due')); - timefor = expiredLabel; - } else { - timefor = gl.utils.getTimeago().format(time, `${lang}-remaining`).trim(); - } - return timefor; - }; +/** + * For the given elements, sets a tooltip with a formatted date. + * @param {jQuery} + * @param {Boolean} setTimeago + */ +export const localTimeAgo = ($timeagoEls, setTimeago = true) => { + $timeagoEls.each((i, el) => { + if (setTimeago) { + // Recreate with custom template + $(el).tooltip({ + template: '<div class="tooltip local-timeago" role="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>', + }); + } - w.gl.utils.renderTimeago = function($els) { - const timeagoEls = $els || document.querySelectorAll('.js-timeago-render'); + el.classList.add('js-timeago-render'); + }); - // timeago.js sets timeouts internally for each timeago value to be updated in real time - gl.utils.getTimeago().render(timeagoEls, lang); - }; + renderTimeago($timeagoEls); +}; - w.gl.utils.getDayDifference = function(a, b) { - var millisecondsPerDay = 1000 * 60 * 60 * 24; - var date1 = Date.UTC(a.getFullYear(), a.getMonth(), a.getDate()); - var date2 = Date.UTC(b.getFullYear(), b.getMonth(), b.getDate()); +/** + * Returns remaining or passed time over the given time. + * @param {*} time + * @param {*} expiredLabel + */ +export const timeFor = (time, expiredLabel) => { + if (!time) { + return ''; + } + if (new Date(time) < new Date()) { + return expiredLabel || s__('Timeago|Past due'); + } + return getTimeago().format(time, `${lang}-remaining`).trim(); +}; - return Math.floor((date2 - date1) / millisecondsPerDay); - }; - })(window); -}).call(window); +export const getDayDifference = (a, b) => { + const millisecondsPerDay = 1000 * 60 * 60 * 24; + const date1 = Date.UTC(a.getFullYear(), a.getMonth(), a.getDate()); + const date2 = Date.UTC(b.getFullYear(), b.getMonth(), b.getDate()); + + return Math.floor((date2 - date1) / millisecondsPerDay); +}; /** * Port of ruby helper time_interval_in_words. @@ -162,3 +163,10 @@ export function dateInWords(date, abbreviated = false) { return `${monthName} ${date.getDate()}, ${year}`; } + +window.gl = window.gl || {}; +window.gl.utils = { + ...(window.gl.utils || {}), + getTimeago, + localTimeAgo, +}; diff --git a/app/assets/javascripts/lib/utils/tick_formats.js b/app/assets/javascripts/lib/utils/tick_formats.js new file mode 100644 index 00000000000..0c10a85e336 --- /dev/null +++ b/app/assets/javascripts/lib/utils/tick_formats.js @@ -0,0 +1,39 @@ +import { createDateTimeFormat } from '../../locale'; + +let dateTimeFormats; + +export const initDateFormats = () => { + const dayFormat = createDateTimeFormat({ month: 'short', day: 'numeric' }); + const monthFormat = createDateTimeFormat({ month: 'long' }); + const yearFormat = createDateTimeFormat({ year: 'numeric' }); + + dateTimeFormats = { + dayFormat, + monthFormat, + yearFormat, + }; +}; + +initDateFormats(); + +/** + Formats a localized date in way that it can be used for d3.js axis.tickFormat(). + + That is, it displays + - 4-digit for first of January + - full month name for first of every month + - day and abbreviated month otherwise + + see also https://github.com/d3/d3-3.x-api-reference/blob/master/SVG-Axes.md#tickFormat + */ +export const dateTickFormat = (date) => { + if (date.getDate() !== 1) { + return dateTimeFormats.dayFormat.format(date); + } + + if (date.getMonth() > 0) { + return dateTimeFormats.monthFormat.format(date); + } + + return dateTimeFormats.yearFormat.format(date); +}; diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js index 17236c91490..f1ee9c8f2e5 100644 --- a/app/assets/javascripts/lib/utils/url_utility.js +++ b/app/assets/javascripts/lib/utils/url_utility.js @@ -1,93 +1,69 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-param-reassign, no-cond-assign, one-var, one-var-declaration-per-line, no-void, guard-for-in, no-restricted-syntax, prefer-template, quotes, max-len */ - -var base; -var w = window; -if (w.gl == null) { - w.gl = {}; -} -if ((base = w.gl).utils == null) { - base.utils = {}; -} // Returns an array containing the value(s) of the // of the key passed as an argument -w.gl.utils.getParameterValues = function(sParam) { - var i, sPageURL, sParameterName, sURLVariables, values; - sPageURL = decodeURIComponent(window.location.search.substring(1)); - sURLVariables = sPageURL.split('&'); - sParameterName = void 0; - values = []; - i = 0; - while (i < sURLVariables.length) { - sParameterName = sURLVariables[i].split('='); +export function getParameterValues(sParam) { + const sPageURL = decodeURIComponent(window.location.search.substring(1)); + + return sPageURL.split('&').reduce((acc, urlParam) => { + const sParameterName = urlParam.split('='); + if (sParameterName[0] === sParam) { - values.push(sParameterName[1].replace(/\+/g, ' ')); + acc.push(sParameterName[1].replace(/\+/g, ' ')); } - i += 1; - } - return values; -}; + + return acc; + }, []); +} + // @param {Object} params - url keys and value to merge // @param {String} url -w.gl.utils.mergeUrlParams = function(params, url) { - var lastChar, newUrl, paramName, paramValue, pattern; - newUrl = decodeURIComponent(url); - for (paramName in params) { - paramValue = params[paramName]; - pattern = new RegExp("\\b(" + paramName + "=).*?(&|$)"); - if (paramValue == null) { - newUrl = newUrl.replace(pattern, ''); +export function mergeUrlParams(params, url) { + let newUrl = Object.keys(params).reduce((acc, paramName) => { + const paramValue = params[paramName]; + const pattern = new RegExp(`\\b(${paramName}=).*?(&|$)`); + + if (paramValue === null) { + return acc.replace(pattern, ''); } else if (url.search(pattern) !== -1) { - newUrl = newUrl.replace(pattern, "$1" + paramValue + "$2"); - } else { - newUrl = "" + newUrl + (newUrl.indexOf('?') > 0 ? '&' : '?') + paramName + "=" + paramValue; + return acc.replace(pattern, `$1${paramValue}$2`); } - } + + return `${acc}${acc.indexOf('?') > 0 ? '&' : '?'}${paramName}=${paramValue}`; + }, decodeURIComponent(url)); + // Remove a trailing ampersand - lastChar = newUrl[newUrl.length - 1]; + const lastChar = newUrl[newUrl.length - 1]; + if (lastChar === '&') { newUrl = newUrl.slice(0, -1); } + return newUrl; -}; -// removes parameter query string from url. returns the modified url -w.gl.utils.removeParamQueryString = function(url, param) { - var urlVariables, variables; - url = decodeURIComponent(url); - urlVariables = url.split('&'); - return ((function() { - var j, len, results; - results = []; - for (j = 0, len = urlVariables.length; j < len; j += 1) { - variables = urlVariables[j]; - if (variables.indexOf(param) === -1) { - results.push(variables); - } - } - return results; - })()).join('&'); -}; -w.gl.utils.removeParams = (params) => { +} + +export function removeParamQueryString(url, param) { + const decodedUrl = decodeURIComponent(url); + const urlVariables = decodedUrl.split('&'); + + return urlVariables.filter(variable => variable.indexOf(param) === -1).join('&'); +} + +export function removeParams(params) { const url = document.createElement('a'); url.href = window.location.href; + params.forEach((param) => { - url.search = w.gl.utils.removeParamQueryString(url.search, param); + url.search = removeParamQueryString(url.search, param); }); + return url.href; -}; -w.gl.utils.getLocationHash = function(url) { - var hashIndex; - if (typeof url === 'undefined') { - // Note: We can't use window.location.hash here because it's - // not consistent across browsers - Firefox will pre-decode it - url = window.location.href; - } - hashIndex = url.indexOf('#'); - return hashIndex === -1 ? null : url.substring(hashIndex + 1); -}; +} -w.gl.utils.refreshCurrentPage = () => gl.utils.visitUrl(window.location.href); +export function getLocationHash(url = window.location.href) { + const hashIndex = url.indexOf('#'); + + return hashIndex === -1 ? null : url.substring(hashIndex + 1); +} -// eslint-disable-next-line import/prefer-default-export export function visitUrl(url, external = false) { if (external) { // Simulate `target="blank" rel="noopener noreferrer"` @@ -100,12 +76,10 @@ export function visitUrl(url, external = false) { } } +export function refreshCurrentPage() { + visitUrl(window.location.href); +} + export function redirectTo(url) { return window.location.assign(url); } - -window.gl = window.gl || {}; -window.gl.utils = { - ...(window.gl.utils || {}), - visitUrl, -}; diff --git a/app/assets/javascripts/line_highlighter.js b/app/assets/javascripts/line_highlighter.js index a75d1a4b8d0..fbd381d8ff7 100644 --- a/app/assets/javascripts/line_highlighter.js +++ b/app/assets/javascripts/line_highlighter.js @@ -175,4 +175,4 @@ LineHighlighter.prototype.__setLocationHash__ = function(value) { }, document.title, value); }; -window.LineHighlighter = LineHighlighter; +export default LineHighlighter; diff --git a/app/assets/javascripts/locale/index.js b/app/assets/javascripts/locale/index.js index 1003b9ba0af..2f4328b56e1 100644 --- a/app/assets/javascripts/locale/index.js +++ b/app/assets/javascripts/locale/index.js @@ -1,8 +1,7 @@ import Jed from 'jed'; import sprintf from './sprintf'; -const langAttribute = document.querySelector('html').getAttribute('lang'); -const lang = (langAttribute || 'en').replace(/-/g, '_'); +const languageCode = () => document.querySelector('html').getAttribute('lang') || 'en'; const locale = new Jed(window.translations || {}); delete window.translations; @@ -47,9 +46,19 @@ const pgettext = (keyOrContext, key) => { return translated[translated.length - 1]; }; -export { lang }; +/** + Creates an instance of Intl.DateTimeFormat for the current locale. + + @param formatOptions for available options, please see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DateTimeFormat + @returns {Intl.DateTimeFormat} +*/ +const createDateTimeFormat = + formatOptions => Intl.DateTimeFormat(languageCode(), formatOptions); + +export { languageCode }; export { gettext as __ }; export { ngettext as n__ }; export { pgettext as s__ }; export { sprintf }; +export { createDateTimeFormat }; export default locale; diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index dcc0fa63b63..b984914ad68 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -1,6 +1,5 @@ /* eslint-disable func-names, space-before-function-paren, no-var, quotes, consistent-return, prefer-arrow-callback, comma-dangle, object-shorthand, no-new, max-len, no-multi-spaces, import/newline-after-import, import/first */ /* global ConfirmDangerModal */ -/* global Aside */ import jQuery from 'jquery'; import _ from 'underscore'; @@ -28,50 +27,30 @@ import './commit/image_file'; // lib/utils import { handleLocationHash } from './lib/utils/common_utils'; -import './lib/utils/datetime_utility'; -import './lib/utils/url_utility'; +import { localTimeAgo, renderTimeago } from './lib/utils/datetime_utility'; +import { getLocationHash, visitUrl } from './lib/utils/url_utility'; // behaviors import './behaviors/'; // everything else -import './activities'; -import './admin'; -import './aside'; import loadAwardsHandler from './awards_handler'; import bp from './breakpoints'; -import './commits'; -import './compare'; -import './compare_autocomplete'; import './confirm_danger_modal'; import Flash, { removeFlashClickListener } from './flash'; import './gl_dropdown'; -import './gl_field_error'; -import './gl_field_errors'; -import './gl_form'; import initTodoToggle from './header'; import initImporterStatus from './importer_status'; import './layout_nav'; import LazyLoader from './lazy_loader'; import './line_highlighter'; import initLogoAnimation from './logo'; -import './merge_request'; import './merge_request_tabs'; import './milestone_select'; import './notes'; -import './notifications_dropdown'; -import './notifications_form'; -import './pager'; import './preview_markdown'; -import './project_find_file'; -import './project_import'; import './projects_dropdown'; -import './projects_list'; -import './syntax_highlight'; import './render_gfm'; -import './right_sidebar'; -import './search'; -import './search_autocomplete'; import initBreadcrumbs from './breadcrumb'; import './dispatcher'; @@ -122,13 +101,13 @@ $(function () { // `hashchange` is not triggered when link target is already in window.location $body.on('click', 'a[href^="#"]', function() { var href = this.getAttribute('href'); - if (href.substr(1) === gl.utils.getLocationHash()) { + if (href.substr(1) === getLocationHash()) { setTimeout(handleLocationHash, 1); } }); if (bootstrapBreakpoint === 'xs') { - const $rightSidebar = $('aside.right-sidebar, .page-with-sidebar'); + const $rightSidebar = $('aside.right-sidebar, .layout-page'); $rightSidebar .removeClass('right-sidebar-expanded') @@ -188,13 +167,13 @@ $(function () { trigger: 'focus', // set the viewport to the main content, excluding the navigation bar, so // the navigation can't overlap the popover - viewport: '.page-with-sidebar' + viewport: '.layout-page' }); $('.trigger-submit').on('change', function () { return $(this).parents('form').submit(); // Form submitter }); - gl.utils.localTimeAgo($('abbr.timeago, .js-timeago'), true); + localTimeAgo($('abbr.timeago, .js-timeago'), true); // Disable form buttons while a form is submitting $body.on('ajax:complete, ajax:beforeSend, submit', 'form', function (e) { var buttons; @@ -281,9 +260,8 @@ $(function () { return fitSidebarForSize(); }); loadAwardsHandler(); - new Aside(); - gl.utils.renderTimeago(); + renderTimeago(); $(document).trigger('init.scrolling-tabs'); @@ -294,7 +272,7 @@ $(function () { const action = `${this.action}${link.search === '' ? '?' : '&'}`; event.preventDefault(); - gl.utils.visitUrl(`${action}${$(this).serialize()}`); + visitUrl(`${action}${$(this).serialize()}`); }); const flashContainer = document.querySelector('.flash-container'); diff --git a/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js index 17591829b76..94561d6b7c3 100644 --- a/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js +++ b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js @@ -10,6 +10,7 @@ import './mixins/line_conflict_actions'; import './components/diff_file_editor'; import './components/inline_conflict_lines'; import './components/parallel_conflict_lines'; +import syntaxHighlight from '../syntax_highlight'; $(() => { const INTERACTIVE_RESOLVE_MODE = 'interactive'; @@ -53,7 +54,7 @@ $(() => { mergeConflictsStore.setLoadingState(false); this.$nextTick(() => { - $('.js-syntax-highlight').syntaxHighlight(); + syntaxHighlight($('.js-syntax-highlight')); }); }); }, diff --git a/app/assets/javascripts/merge_request.js b/app/assets/javascripts/merge_request.js index a9c08df4f93..cb3cdea8111 100644 --- a/app/assets/javascripts/merge_request.js +++ b/app/assets/javascripts/merge_request.js @@ -1,148 +1,143 @@ /* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, quotes, no-underscore-dangle, one-var, one-var-declaration-per-line, consistent-return, dot-notation, quote-props, comma-dangle, object-shorthand, max-len, prefer-arrow-callback */ -/* global MergeRequestTabs */ import 'vendor/jquery.waitforimages'; import TaskList from './task_list'; -import './merge_request_tabs'; +import MergeRequestTabs from './merge_request_tabs'; import IssuablesHelper from './helpers/issuables_helper'; import { addDelimiter } from './lib/utils/text_utility'; -(function() { - this.MergeRequest = (function() { - function MergeRequest(opts) { - // Initialize MergeRequest behavior - // - // Options: - // action - String, current controller action - // - this.opts = opts != null ? opts : {}; - this.submitNoteForm = this.submitNoteForm.bind(this); - this.$el = $('.merge-request'); - this.$('.show-all-commits').on('click', (function(_this) { - return function() { - return _this.showAllCommits(); - }; - })(this)); - - this.initTabs(); - this.initMRBtnListeners(); - this.initCommitMessageListeners(); - this.closeReopenReportToggle = IssuablesHelper.initCloseReopenReport(); - - if ($("a.btn-close").length) { - this.taskList = new TaskList({ - dataType: 'merge_request', - fieldName: 'description', - selector: '.detail-page-description', - onSuccess: (result) => { - document.querySelector('#task_status').innerText = result.task_status; - document.querySelector('#task_status_short').innerText = result.task_status_short; - } - }); - } - } - - // Local jQuery finder - MergeRequest.prototype.$ = function(selector) { - return this.$el.find(selector); +function MergeRequest(opts) { + // Initialize MergeRequest behavior + // + // Options: + // action - String, current controller action + // + this.opts = opts != null ? opts : {}; + this.submitNoteForm = this.submitNoteForm.bind(this); + this.$el = $('.merge-request'); + this.$('.show-all-commits').on('click', (function(_this) { + return function() { + return _this.showAllCommits(); }; - - MergeRequest.prototype.initTabs = function() { - if (window.mrTabs) { - window.mrTabs.unbindEvents(); + })(this)); + + this.initTabs(); + this.initMRBtnListeners(); + this.initCommitMessageListeners(); + this.closeReopenReportToggle = IssuablesHelper.initCloseReopenReport(); + + if ($("a.btn-close").length) { + this.taskList = new TaskList({ + dataType: 'merge_request', + fieldName: 'description', + selector: '.detail-page-description', + onSuccess: (result) => { + document.querySelector('#task_status').innerText = result.task_status; + document.querySelector('#task_status_short').innerText = result.task_status_short; } - window.mrTabs = new gl.MergeRequestTabs(this.opts); - }; - - MergeRequest.prototype.showAllCommits = function() { - this.$('.first-commits').remove(); - return this.$('.all-commits').removeClass('hide'); - }; - - MergeRequest.prototype.initMRBtnListeners = function() { - var _this; - _this = this; - return $('a.btn-close, a.btn-reopen').on('click', function(e) { - var $this, shouldSubmit; - $this = $(this); - shouldSubmit = $this.hasClass('btn-comment'); - if (shouldSubmit && $this.data('submitted')) { - return; - } - - if (this.closeReopenReportToggle) this.closeReopenReportToggle.setDisable(); - - if (shouldSubmit) { - if ($this.hasClass('btn-comment-and-close') || $this.hasClass('btn-comment-and-reopen')) { - e.preventDefault(); - e.stopImmediatePropagation(); - - _this.submitNoteForm($this.closest('form'), $this); - } - } - }); - }; - - MergeRequest.prototype.submitNoteForm = function(form, $button) { - var noteText; - noteText = form.find("textarea.js-note-text").val(); - if (noteText.trim().length > 0) { - form.submit(); - $button.data('submitted', true); - return $button.trigger('click'); - } - }; - - MergeRequest.prototype.initCommitMessageListeners = function() { - $(document).on('click', 'a.js-with-description-link', function(e) { - var textarea = $('textarea.js-commit-message'); - e.preventDefault(); + }); + } +} + +// Local jQuery finder +MergeRequest.prototype.$ = function(selector) { + return this.$el.find(selector); +}; + +MergeRequest.prototype.initTabs = function() { + if (window.mrTabs) { + window.mrTabs.unbindEvents(); + } + window.mrTabs = new MergeRequestTabs(this.opts); +}; + +MergeRequest.prototype.showAllCommits = function() { + this.$('.first-commits').remove(); + return this.$('.all-commits').removeClass('hide'); +}; + +MergeRequest.prototype.initMRBtnListeners = function() { + var _this; + _this = this; + return $('a.btn-close, a.btn-reopen').on('click', function(e) { + var $this, shouldSubmit; + $this = $(this); + shouldSubmit = $this.hasClass('btn-comment'); + if (shouldSubmit && $this.data('submitted')) { + return; + } - textarea.val(textarea.data('messageWithDescription')); - $('.js-with-description-hint').hide(); - $('.js-without-description-hint').show(); - }); + if (this.closeReopenReportToggle) this.closeReopenReportToggle.setDisable(); - $(document).on('click', 'a.js-without-description-link', function(e) { - var textarea = $('textarea.js-commit-message'); + if (shouldSubmit) { + if ($this.hasClass('btn-comment-and-close') || $this.hasClass('btn-comment-and-reopen')) { e.preventDefault(); + e.stopImmediatePropagation(); - textarea.val(textarea.data('messageWithoutDescription')); - $('.js-with-description-hint').show(); - $('.js-without-description-hint').hide(); - }); - }; - - MergeRequest.prototype.updateStatusText = function(classToRemove, classToAdd, newStatusText) { - $('.detail-page-header .status-box') - .removeClass(classToRemove) - .addClass(classToAdd) - .find('span') - .text(newStatusText); - }; - - MergeRequest.prototype.decreaseCounter = function(by = 1) { - const $el = $('.nav-links .js-merge-counter'); - const count = Math.max((parseInt($el.text().replace(/[^\d]/, ''), 10) - by), 0); - - $el.text(addDelimiter(count)); - }; - - MergeRequest.prototype.hideCloseButton = function() { - const el = document.querySelector('.merge-request .js-issuable-actions'); - const closeDropdownItem = el.querySelector('li.close-item'); - if (closeDropdownItem) { - closeDropdownItem.classList.add('hidden'); - // Selects the next dropdown item - el.querySelector('li.report-item').click(); - } else { - // No dropdown just hide the Close button - el.querySelector('.btn-close').classList.add('hidden'); + _this.submitNoteForm($this.closest('form'), $this); } - // Dropdown for mobile screen - el.querySelector('li.js-close-item').classList.add('hidden'); - }; - - return MergeRequest; - })(); -}).call(window); + } + }); +}; + +MergeRequest.prototype.submitNoteForm = function(form, $button) { + var noteText; + noteText = form.find("textarea.js-note-text").val(); + if (noteText.trim().length > 0) { + form.submit(); + $button.data('submitted', true); + return $button.trigger('click'); + } +}; + +MergeRequest.prototype.initCommitMessageListeners = function() { + $(document).on('click', 'a.js-with-description-link', function(e) { + var textarea = $('textarea.js-commit-message'); + e.preventDefault(); + + textarea.val(textarea.data('messageWithDescription')); + $('.js-with-description-hint').hide(); + $('.js-without-description-hint').show(); + }); + + $(document).on('click', 'a.js-without-description-link', function(e) { + var textarea = $('textarea.js-commit-message'); + e.preventDefault(); + + textarea.val(textarea.data('messageWithoutDescription')); + $('.js-with-description-hint').show(); + $('.js-without-description-hint').hide(); + }); +}; + +MergeRequest.prototype.updateStatusText = function(classToRemove, classToAdd, newStatusText) { + $('.detail-page-header .status-box') + .removeClass(classToRemove) + .addClass(classToAdd) + .find('span') + .text(newStatusText); +}; + +MergeRequest.prototype.decreaseCounter = function(by = 1) { + const $el = $('.nav-links .js-merge-counter'); + const count = Math.max((parseInt($el.text().replace(/[^\d]/, ''), 10) - by), 0); + + $el.text(addDelimiter(count)); +}; + +MergeRequest.prototype.hideCloseButton = function() { + const el = document.querySelector('.merge-request .js-issuable-actions'); + const closeDropdownItem = el.querySelector('li.close-item'); + if (closeDropdownItem) { + closeDropdownItem.classList.add('hidden'); + // Selects the next dropdown item + el.querySelector('li.report-item').click(); + } else { + // No dropdown just hide the Close button + el.querySelector('.btn-close').classList.add('hidden'); + } + // Dropdown for mobile screen + el.querySelector('li.js-close-item').classList.add('hidden'); +}; + +export default MergeRequest; diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js index 54c1b7a268e..cacca35ca98 100644 --- a/app/assets/javascripts/merge_request_tabs.js +++ b/app/assets/javascripts/merge_request_tabs.js @@ -11,8 +11,11 @@ import { handleLocationHash, isMetaClick, } from './lib/utils/common_utils'; +import { getLocationHash } from './lib/utils/url_utility'; import initDiscussionTab from './image_diff/init_discussion_tab'; import Diff from './diff'; +import { localTimeAgo } from './lib/utils/datetime_utility'; +import syntaxHighlight from './syntax_highlight'; /* eslint-disable max-len */ // MergeRequestTabs @@ -60,387 +63,382 @@ import Diff from './diff'; // /* eslint-enable max-len */ -(() => { - // Store the `location` object, allowing for easier stubbing in tests - let location = window.location; +// Store the `location` object, allowing for easier stubbing in tests +let location = window.location; - class MergeRequestTabs { +export default class MergeRequestTabs { - constructor({ action, setUrl, stubLocation } = {}) { - const mergeRequestTabs = document.querySelector('.js-tabs-affix'); - const navbar = document.querySelector('.navbar-gitlab'); - const paddingTop = 16; + constructor({ action, setUrl, stubLocation } = {}) { + const mergeRequestTabs = document.querySelector('.js-tabs-affix'); + const navbar = document.querySelector('.navbar-gitlab'); + const paddingTop = 16; - this.diffsLoaded = false; - this.pipelinesLoaded = false; - this.commitsLoaded = false; - this.fixedLayoutPref = null; + this.diffsLoaded = false; + this.pipelinesLoaded = false; + this.commitsLoaded = false; + this.fixedLayoutPref = null; - this.setUrl = setUrl !== undefined ? setUrl : true; - this.setCurrentAction = this.setCurrentAction.bind(this); - this.tabShown = this.tabShown.bind(this); - this.showTab = this.showTab.bind(this); - this.stickyTop = navbar ? navbar.offsetHeight - paddingTop : 0; + this.setUrl = setUrl !== undefined ? setUrl : true; + this.setCurrentAction = this.setCurrentAction.bind(this); + this.tabShown = this.tabShown.bind(this); + this.showTab = this.showTab.bind(this); + this.stickyTop = navbar ? navbar.offsetHeight - paddingTop : 0; - if (mergeRequestTabs) { - this.stickyTop += mergeRequestTabs.offsetHeight; - } - - if (stubLocation) { - location = stubLocation; - } + if (mergeRequestTabs) { + this.stickyTop += mergeRequestTabs.offsetHeight; + } - this.bindEvents(); - this.activateTab(action); - this.initAffix(); + if (stubLocation) { + location = stubLocation; } - bindEvents() { - $(document) - .on('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown) - .on('click', '.js-show-tab', this.showTab); + this.bindEvents(); + this.activateTab(action); + this.initAffix(); + } - $('.merge-request-tabs a[data-toggle="tab"]') - .on('click', this.clickTab); - } + bindEvents() { + $(document) + .on('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown) + .on('click', '.js-show-tab', this.showTab); - // Used in tests - unbindEvents() { - $(document) - .off('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown) - .off('click', '.js-show-tab', this.showTab); + $('.merge-request-tabs a[data-toggle="tab"]') + .on('click', this.clickTab); + } - $('.merge-request-tabs a[data-toggle="tab"]') - .off('click', this.clickTab); - } + // Used in tests + unbindEvents() { + $(document) + .off('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown) + .off('click', '.js-show-tab', this.showTab); - destroyPipelinesView() { - if (this.commitPipelinesTable) { - this.commitPipelinesTable.$destroy(); - this.commitPipelinesTable = null; + $('.merge-request-tabs a[data-toggle="tab"]') + .off('click', this.clickTab); + } - document.querySelector('#commit-pipeline-table-view').innerHTML = ''; - } + destroyPipelinesView() { + if (this.commitPipelinesTable) { + this.commitPipelinesTable.$destroy(); + this.commitPipelinesTable = null; + + document.querySelector('#commit-pipeline-table-view').innerHTML = ''; } + } - showTab(e) { + showTab(e) { + e.preventDefault(); + this.activateTab($(e.target).data('action')); + } + + clickTab(e) { + if (e.currentTarget && isMetaClick(e)) { + const targetLink = e.currentTarget.getAttribute('href'); + e.stopImmediatePropagation(); e.preventDefault(); - this.activateTab($(e.target).data('action')); + window.open(targetLink, '_blank'); } + } - clickTab(e) { - if (e.currentTarget && isMetaClick(e)) { - const targetLink = e.currentTarget.getAttribute('href'); - e.stopImmediatePropagation(); - e.preventDefault(); - window.open(targetLink, '_blank'); + tabShown(e) { + const $target = $(e.target); + const action = $target.data('action'); + + if (action === 'commits') { + this.loadCommits($target.attr('href')); + this.expandView(); + this.resetViewContainer(); + this.destroyPipelinesView(); + } else if (this.isDiffAction(action)) { + this.loadDiff($target.attr('href')); + if (bp.getBreakpointSize() !== 'lg') { + this.shrinkView(); } - } - - tabShown(e) { - const $target = $(e.target); - const action = $target.data('action'); - - if (action === 'commits') { - this.loadCommits($target.attr('href')); - this.expandView(); - this.resetViewContainer(); - this.destroyPipelinesView(); - } else if (this.isDiffAction(action)) { - this.loadDiff($target.attr('href')); - if (bp.getBreakpointSize() !== 'lg') { - this.shrinkView(); - } - if (this.diffViewType() === 'parallel') { - this.expandViewContainer(); - } - this.destroyPipelinesView(); - } else if (action === 'pipelines') { - this.resetViewContainer(); - this.mountPipelinesView(); - } else { - if (bp.getBreakpointSize() !== 'xs') { - this.expandView(); - } - this.resetViewContainer(); - this.destroyPipelinesView(); - - initDiscussionTab(); + if (this.diffViewType() === 'parallel') { + this.expandViewContainer(); } - if (this.setUrl) { - this.setCurrentAction(action); + this.destroyPipelinesView(); + } else if (action === 'pipelines') { + this.resetViewContainer(); + this.mountPipelinesView(); + } else { + if (bp.getBreakpointSize() !== 'xs') { + this.expandView(); } + this.resetViewContainer(); + this.destroyPipelinesView(); + + initDiscussionTab(); + } + if (this.setUrl) { + this.setCurrentAction(action); } + } - scrollToElement(container) { - if (location.hash) { - const offset = 0 - ( - $('.navbar-gitlab').outerHeight() + - $('.js-tabs-affix').outerHeight() - ); - const $el = $(`${container} ${location.hash}:not(.match)`); - if ($el.length) { - $.scrollTo($el[0], { offset }); - } + scrollToElement(container) { + if (location.hash) { + const offset = 0 - ( + $('.navbar-gitlab').outerHeight() + + $('.js-tabs-affix').outerHeight() + ); + const $el = $(`${container} ${location.hash}:not(.match)`); + if ($el.length) { + $.scrollTo($el[0], { offset }); } } + } - // Activate a tab based on the current action - activateTab(action) { - // important note: the .tab('show') method triggers 'shown.bs.tab' event itself - $(`.merge-request-tabs a[data-action='${action}']`).tab('show'); + // Activate a tab based on the current action + activateTab(action) { + // important note: the .tab('show') method triggers 'shown.bs.tab' event itself + $(`.merge-request-tabs a[data-action='${action}']`).tab('show'); + } + + // Replaces the current Merge Request-specific action in the URL with a new one + // + // If the action is "notes", the URL is reset to the standard + // `MergeRequests#show` route. + // + // Examples: + // + // location.pathname # => "/namespace/project/merge_requests/1" + // setCurrentAction('diffs') + // location.pathname # => "/namespace/project/merge_requests/1/diffs" + // + // location.pathname # => "/namespace/project/merge_requests/1/diffs" + // setCurrentAction('show') + // location.pathname # => "/namespace/project/merge_requests/1" + // + // location.pathname # => "/namespace/project/merge_requests/1/diffs" + // setCurrentAction('commits') + // location.pathname # => "/namespace/project/merge_requests/1/commits" + // + // Returns the new URL String + setCurrentAction(action) { + this.currentAction = action; + + // Remove a trailing '/commits' '/diffs' '/pipelines' + let newState = location.pathname.replace(/\/(commits|diffs|pipelines)(\.html)?\/?$/, ''); + + // Append the new action if we're on a tab other than 'notes' + if (this.currentAction !== 'show' && this.currentAction !== 'new') { + newState += `/${this.currentAction}`; } - // Replaces the current Merge Request-specific action in the URL with a new one - // - // If the action is "notes", the URL is reset to the standard - // `MergeRequests#show` route. - // - // Examples: - // - // location.pathname # => "/namespace/project/merge_requests/1" - // setCurrentAction('diffs') - // location.pathname # => "/namespace/project/merge_requests/1/diffs" - // - // location.pathname # => "/namespace/project/merge_requests/1/diffs" - // setCurrentAction('show') - // location.pathname # => "/namespace/project/merge_requests/1" - // - // location.pathname # => "/namespace/project/merge_requests/1/diffs" - // setCurrentAction('commits') - // location.pathname # => "/namespace/project/merge_requests/1/commits" - // - // Returns the new URL String - setCurrentAction(action) { - this.currentAction = action; + // Ensure parameters and hash come along for the ride + newState += location.search + location.hash; - // Remove a trailing '/commits' '/diffs' '/pipelines' - let newState = location.pathname.replace(/\/(commits|diffs|pipelines)(\.html)?\/?$/, ''); + // TODO: Consider refactoring in light of turbolinks removal. - // Append the new action if we're on a tab other than 'notes' - if (this.currentAction !== 'show' && this.currentAction !== 'new') { - newState += `/${this.currentAction}`; - } + // Replace the current history state with the new one without breaking + // Turbolinks' history. + // + // See https://github.com/rails/turbolinks/issues/363 + window.history.replaceState({ + url: newState, + }, document.title, newState); - // Ensure parameters and hash come along for the ride - newState += location.search + location.hash; + return newState; + } - // TODO: Consider refactoring in light of turbolinks removal. + loadCommits(source) { + if (this.commitsLoaded) { + return; + } + this.ajaxGet({ + url: `${source}.json`, + success: (data) => { + document.querySelector('div#commits').innerHTML = data.html; + localTimeAgo($('.js-timeago', 'div#commits')); + this.commitsLoaded = true; + this.scrollToElement('#commits'); + }, + }); + } - // Replace the current history state with the new one without breaking - // Turbolinks' history. - // - // See https://github.com/rails/turbolinks/issues/363 - window.history.replaceState({ - url: newState, - }, document.title, newState); + mountPipelinesView() { + const pipelineTableViewEl = document.querySelector('#commit-pipeline-table-view'); + const CommitPipelinesTable = gl.CommitPipelinesTable; + this.commitPipelinesTable = new CommitPipelinesTable({ + propsData: { + endpoint: pipelineTableViewEl.dataset.endpoint, + helpPagePath: pipelineTableViewEl.dataset.helpPagePath, + emptyStateSvgPath: pipelineTableViewEl.dataset.emptyStateSvgPath, + errorStateSvgPath: pipelineTableViewEl.dataset.errorStateSvgPath, + autoDevopsHelpPath: pipelineTableViewEl.dataset.helpAutoDevopsPath, + }, + }).$mount(); + + // $mount(el) replaces the el with the new rendered component. We need it in order to mount + // it everytime this tab is clicked - https://vuejs.org/v2/api/#vm-mount + pipelineTableViewEl.appendChild(this.commitPipelinesTable.$el); + } - return newState; + loadDiff(source) { + if (this.diffsLoaded) { + document.dispatchEvent(new CustomEvent('scroll')); + return; } - loadCommits(source) { - if (this.commitsLoaded) { - return; - } - this.ajaxGet({ - url: `${source}.json`, - success: (data) => { - document.querySelector('div#commits').innerHTML = data.html; - gl.utils.localTimeAgo($('.js-timeago', 'div#commits')); - this.commitsLoaded = true; - this.scrollToElement('#commits'); - }, - }); - } + // We extract pathname for the current Changes tab anchor href + // some pages like MergeRequestsController#new has query parameters on that anchor + const urlPathname = parseUrlPathname(source); - mountPipelinesView() { - const pipelineTableViewEl = document.querySelector('#commit-pipeline-table-view'); - const CommitPipelinesTable = gl.CommitPipelinesTable; - this.commitPipelinesTable = new CommitPipelinesTable({ - propsData: { - endpoint: pipelineTableViewEl.dataset.endpoint, - helpPagePath: pipelineTableViewEl.dataset.helpPagePath, - emptyStateSvgPath: pipelineTableViewEl.dataset.emptyStateSvgPath, - errorStateSvgPath: pipelineTableViewEl.dataset.errorStateSvgPath, - autoDevopsHelpPath: pipelineTableViewEl.dataset.helpAutoDevopsPath, - }, - }).$mount(); + this.ajaxGet({ + url: `${urlPathname}.json${location.search}`, + success: (data) => { + const $container = $('#diffs'); + $container.html(data.html); - // $mount(el) replaces the el with the new rendered component. We need it in order to mount - // it everytime this tab is clicked - https://vuejs.org/v2/api/#vm-mount - pipelineTableViewEl.appendChild(this.commitPipelinesTable.$el); - } + initChangesDropdown(this.stickyTop); - loadDiff(source) { - if (this.diffsLoaded) { - document.dispatchEvent(new CustomEvent('scroll')); - return; - } + if (typeof gl.diffNotesCompileComponents !== 'undefined') { + gl.diffNotesCompileComponents(); + } + + localTimeAgo($('.js-timeago', 'div#diffs')); + syntaxHighlight($('#diffs .js-syntax-highlight')); - // We extract pathname for the current Changes tab anchor href - // some pages like MergeRequestsController#new has query parameters on that anchor - const urlPathname = parseUrlPathname(source); - - this.ajaxGet({ - url: `${urlPathname}.json${location.search}`, - success: (data) => { - const $container = $('#diffs'); - $container.html(data.html); - - initChangesDropdown(this.stickyTop); - - if (typeof gl.diffNotesCompileComponents !== 'undefined') { - gl.diffNotesCompileComponents(); - } - - gl.utils.localTimeAgo($('.js-timeago', 'div#diffs')); - $('#diffs .js-syntax-highlight').syntaxHighlight(); - - if (this.diffViewType() === 'parallel' && this.isDiffAction(this.currentAction)) { - this.expandViewContainer(); - } - this.diffsLoaded = true; - - new Diff(); - this.scrollToElement('#diffs'); - - $('.diff-file').each((i, el) => { - new BlobForkSuggestion({ - openButtons: $(el).find('.js-edit-blob-link-fork-toggler'), - forkButtons: $(el).find('.js-fork-suggestion-button'), - cancelButtons: $(el).find('.js-cancel-fork-suggestion-button'), - suggestionSections: $(el).find('.js-file-fork-suggestion-section'), - actionTextPieces: $(el).find('.js-file-fork-suggestion-section-action'), - }) - .init(); + if (this.diffViewType() === 'parallel' && this.isDiffAction(this.currentAction)) { + this.expandViewContainer(); + } + this.diffsLoaded = true; + + new Diff(); + this.scrollToElement('#diffs'); + + $('.diff-file').each((i, el) => { + new BlobForkSuggestion({ + openButtons: $(el).find('.js-edit-blob-link-fork-toggler'), + forkButtons: $(el).find('.js-fork-suggestion-button'), + cancelButtons: $(el).find('.js-cancel-fork-suggestion-button'), + suggestionSections: $(el).find('.js-file-fork-suggestion-section'), + actionTextPieces: $(el).find('.js-file-fork-suggestion-section-action'), + }) + .init(); + }); + + // Scroll any linked note into view + // Similar to `toggler_behavior` in the discussion tab + const hash = getLocationHash(); + const anchor = hash && $container.find(`.note[id="${hash}"]`); + if (anchor && anchor.length > 0) { + const notesContent = anchor.closest('.notes_content'); + const lineType = notesContent.hasClass('new') ? 'new' : 'old'; + notes.toggleDiffNote({ + target: anchor, + lineType, + forceShow: true, }); + anchor[0].scrollIntoView(); + handleLocationHash(); + // We have multiple elements on the page with `#note_xxx` + // (discussion and diff tabs) and `:target` only applies to the first + anchor.addClass('target'); + } + }, + }); + } - // Scroll any linked note into view - // Similar to `toggler_behavior` in the discussion tab - const hash = window.gl.utils.getLocationHash(); - const anchor = hash && $container.find(`.note[id="${hash}"]`); - if (anchor && anchor.length > 0) { - const notesContent = anchor.closest('.notes_content'); - const lineType = notesContent.hasClass('new') ? 'new' : 'old'; - notes.toggleDiffNote({ - target: anchor, - lineType, - forceShow: true, - }); - anchor[0].scrollIntoView(); - handleLocationHash(); - // We have multiple elements on the page with `#note_xxx` - // (discussion and diff tabs) and `:target` only applies to the first - anchor.addClass('target'); - } - }, - }); - } + // Show or hide the loading spinner + // + // status - Boolean, true to show, false to hide + toggleLoading(status) { + $('.mr-loading-status .loading').toggle(status); + } - // Show or hide the loading spinner - // - // status - Boolean, true to show, false to hide - toggleLoading(status) { - $('.mr-loading-status .loading').toggle(status); - } + ajaxGet(options) { + const defaults = { + beforeSend: () => this.toggleLoading(true), + error: () => new Flash('An error occurred while fetching this tab.', 'alert'), + complete: () => this.toggleLoading(false), + dataType: 'json', + type: 'GET', + }; + $.ajax($.extend({}, defaults, options)); + } - ajaxGet(options) { - const defaults = { - beforeSend: () => this.toggleLoading(true), - error: () => new Flash('An error occurred while fetching this tab.', 'alert'), - complete: () => this.toggleLoading(false), - dataType: 'json', - type: 'GET', - }; - $.ajax($.extend({}, defaults, options)); - } + diffViewType() { + return $('.inline-parallel-buttons a.active').data('view-type'); + } - diffViewType() { - return $('.inline-parallel-buttons a.active').data('view-type'); - } + isDiffAction(action) { + return action === 'diffs' || action === 'new/diffs'; + } - isDiffAction(action) { - return action === 'diffs' || action === 'new/diffs'; + expandViewContainer() { + const $wrapper = $('.content-wrapper .container-fluid').not('.breadcrumbs'); + if (this.fixedLayoutPref === null) { + this.fixedLayoutPref = $wrapper.hasClass('container-limited'); } + $wrapper.removeClass('container-limited'); + } - expandViewContainer() { - const $wrapper = $('.content-wrapper .container-fluid').not('.breadcrumbs'); - if (this.fixedLayoutPref === null) { - this.fixedLayoutPref = $wrapper.hasClass('container-limited'); - } - $wrapper.removeClass('container-limited'); + resetViewContainer() { + if (this.fixedLayoutPref !== null) { + $('.content-wrapper .container-fluid') + .toggleClass('container-limited', this.fixedLayoutPref); } + } - resetViewContainer() { - if (this.fixedLayoutPref !== null) { - $('.content-wrapper .container-fluid') - .toggleClass('container-limited', this.fixedLayoutPref); - } - } + shrinkView() { + const $gutterIcon = $('.js-sidebar-toggle i:visible'); - shrinkView() { - const $gutterIcon = $('.js-sidebar-toggle i:visible'); + // Wait until listeners are set + setTimeout(() => { + // Only when sidebar is expanded + if ($gutterIcon.is('.fa-angle-double-right')) { + $gutterIcon.closest('a').trigger('click', [true]); + } + }, 0); + } - // Wait until listeners are set - setTimeout(() => { - // Only when sidebar is expanded - if ($gutterIcon.is('.fa-angle-double-right')) { - $gutterIcon.closest('a').trigger('click', [true]); - } - }, 0); + // Expand the issuable sidebar unless the user explicitly collapsed it + expandView() { + if (Cookies.get('collapsed_gutter') === 'true') { + return; } + const $gutterIcon = $('.js-sidebar-toggle i:visible'); - // Expand the issuable sidebar unless the user explicitly collapsed it - expandView() { - if (Cookies.get('collapsed_gutter') === 'true') { - return; + // Wait until listeners are set + setTimeout(() => { + // Only when sidebar is collapsed + if ($gutterIcon.is('.fa-angle-double-left')) { + $gutterIcon.closest('a').trigger('click', [true]); } - const $gutterIcon = $('.js-sidebar-toggle i:visible'); + }, 0); + } - // Wait until listeners are set - setTimeout(() => { - // Only when sidebar is collapsed - if ($gutterIcon.is('.fa-angle-double-left')) { - $gutterIcon.closest('a').trigger('click', [true]); - } - }, 0); - } + initAffix() { + const $tabs = $('.js-tabs-affix'); + const $fixedNav = $('.navbar-gitlab'); + + // Screen space on small screens is usually very sparse + // So we dont affix the tabs on these + if (bp.getBreakpointSize() === 'xs' || !$tabs.length) return; + + /** + If the browser does not support position sticky, it returns the position as static. + If the browser does support sticky, then we allow the browser to handle it, if not + then we default back to Bootstraps affix + **/ + if ($tabs.css('position') !== 'static') return; + + const $diffTabs = $('#diff-notes-app'); + + $tabs.off('affix.bs.affix affix-top.bs.affix') + .affix({ + offset: { + top: () => ( + $diffTabs.offset().top - $tabs.height() - $fixedNav.height() + ), + }, + }) + .on('affix.bs.affix', () => $diffTabs.css({ marginTop: $tabs.height() })) + .on('affix-top.bs.affix', () => $diffTabs.css({ marginTop: '' })); - initAffix() { - const $tabs = $('.js-tabs-affix'); - const $fixedNav = $('.navbar-gitlab'); - - // Screen space on small screens is usually very sparse - // So we dont affix the tabs on these - if (bp.getBreakpointSize() === 'xs' || !$tabs.length) return; - - /** - If the browser does not support position sticky, it returns the position as static. - If the browser does support sticky, then we allow the browser to handle it, if not - then we default back to Bootstraps affix - **/ - if ($tabs.css('position') !== 'static') return; - - const $diffTabs = $('#diff-notes-app'); - - $tabs.off('affix.bs.affix affix-top.bs.affix') - .affix({ - offset: { - top: () => ( - $diffTabs.offset().top - $tabs.height() - $fixedNav.height() - ), - }, - }) - .on('affix.bs.affix', () => $diffTabs.css({ marginTop: $tabs.height() })) - .on('affix-top.bs.affix', () => $diffTabs.css({ marginTop: '' })); - - // Fix bug when reloading the page already scrolling - if ($tabs.hasClass('affix')) { - $tabs.trigger('affix.bs.affix'); - } + // Fix bug when reloading the page already scrolling + if ($tabs.hasClass('affix')) { + $tabs.trigger('affix.bs.affix'); } } - - window.gl = window.gl || {}; - window.gl.MergeRequestTabs = MergeRequestTabs; -})(); +} diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js index 74e5a4f1cea..2e5e818d61d 100644 --- a/app/assets/javascripts/milestone_select.js +++ b/app/assets/javascripts/milestone_select.js @@ -2,6 +2,7 @@ /* global Issuable */ /* global ListMilestone */ import _ from 'underscore'; +import { timeFor } from './lib/utils/datetime_utility'; (function() { this.MilestoneSelect = (function() { @@ -216,7 +217,7 @@ import _ from 'underscore'; $value.css('display', ''); if (data.milestone != null) { data.milestone.full_path = _this.currentProject.full_path; - data.milestone.remaining = gl.utils.timeFor(data.milestone.due_date); + data.milestone.remaining = timeFor(data.milestone.due_date); data.milestone.name = data.milestone.title; $value.html(milestoneLinkTemplate(data.milestone)); return $sidebarCollapsedValue.find('span').html(collapsedSidebarLabelTemplate(data.milestone)); diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue index cbe24c0915b..8da723ced03 100644 --- a/app/assets/javascripts/monitoring/components/dashboard.vue +++ b/app/assets/javascripts/monitoring/components/dashboard.vue @@ -21,6 +21,8 @@ hasMetrics: convertPermissionToBoolean(metricsData.hasMetrics), documentationPath: metricsData.documentationPath, settingsPath: metricsData.settingsPath, + tagsPath: metricsData.tagsPath, + projectPath: metricsData.projectPath, metricsEndpoint: metricsData.additionalMetrics, deploymentEndpoint: metricsData.deploymentEndpoint, emptyGettingStartedSvgPath: metricsData.emptyGettingStartedSvgPath, @@ -112,6 +114,8 @@ :hover-data="hoverData" :update-aspect-ratio="updateAspectRatio" :deployment-data="store.deploymentData" + :project-path="projectPath" + :tags-path="tagsPath" /> </graph-group> </div> diff --git a/app/assets/javascripts/monitoring/components/graph.vue b/app/assets/javascripts/monitoring/components/graph.vue index f8782fde927..cdae287658b 100644 --- a/app/assets/javascripts/monitoring/components/graph.vue +++ b/app/assets/javascripts/monitoring/components/graph.vue @@ -30,6 +30,14 @@ required: false, default: () => ({}), }, + projectPath: { + type: String, + required: true, + }, + tagsPath: { + type: String, + required: true, + }, }, mixins: [MonitoringMixin], @@ -251,6 +259,14 @@ :line-color="path.lineColor" :area-color="path.areaColor" /> + <rect + class="prometheus-graph-overlay" + :width="(graphWidth - 70)" + :height="(graphHeight - 100)" + transform="translate(-5, 20)" + ref="graphOverlay" + @mousemove="handleMouseOverGraph($event)"> + </rect> <graph-deployment :show-deploy-info="showDeployInfo" :deployment-data="reducedDeploymentData" @@ -267,14 +283,6 @@ :graph-height-offset="graphHeightOffset" :show-flag-content="showFlagContent" /> - <rect - class="prometheus-graph-overlay" - :width="(graphWidth - 70)" - :height="(graphHeight - 100)" - transform="translate(-5, 20)" - ref="graphOverlay" - @mousemove="handleMouseOverGraph($event)"> - </rect> </svg> </svg> </div> diff --git a/app/assets/javascripts/monitoring/components/graph/deployment.vue b/app/assets/javascripts/monitoring/components/graph/deployment.vue index e3b8be0c7fb..026e2fd0c49 100644 --- a/app/assets/javascripts/monitoring/components/graph/deployment.vue +++ b/app/assets/javascripts/monitoring/components/graph/deployment.vue @@ -1,5 +1,6 @@ <script> - import { dateFormat, timeFormat } from '../../utils/date_time_formatters'; + import { dateFormatWithName, timeFormat } from '../../utils/date_time_formatters'; + import Icon from '../../../vue_shared/components/icon.vue'; export default { props: { @@ -25,6 +26,10 @@ }, }, + components: { + Icon, + }, + computed: { calculatedHeight() { return this.graphHeight - this.graphHeightOffset; @@ -33,7 +38,7 @@ methods: { refText(d) { - return d.tag ? d.ref : d.sha.slice(0, 6); + return d.tag ? d.ref : d.sha.slice(0, 8); }, formatTime(deploymentTime) { @@ -41,7 +46,7 @@ }, formatDate(deploymentTime) { - return dateFormat(deploymentTime); + return dateFormatWithName(deploymentTime); }, nameDeploymentClass(deployment) { @@ -54,11 +59,19 @@ positionFlag(deployment) { let xPosition = 3; - if (deployment.xPos > (this.graphWidth - 200)) { - xPosition = -97; + if (deployment.xPos > (this.graphWidth - 225)) { + xPosition = -142; } return xPosition; }, + + svgContainerHeight(tag) { + let svgHeight = 80; + if (!tag) { + svgHeight -= 20; + } + return svgHeight; + }, }, }; </script> @@ -91,35 +104,75 @@ class="js-deploy-info-box" :x="positionFlag(deployment)" y="0" - width="92" - height="60"> + width="134" + :height="svgContainerHeight(deployment.tag)"> <rect class="rect-text-metric deploy-info-rect rect-metric" x="1" y="1" rx="2" - width="90" - height="58"> + width="132" + :height="svgContainerHeight(deployment.tag) - 2"> </rect> - <g - transform="translate(5, 2)"> - <text - class="deploy-info-text text-metric-bold"> - {{refText(deployment)}} - </text> - </g> - <text - class="deploy-info-text" - y="18" - transform="translate(5, 2)"> - {{formatDate(deployment.time)}} - </text> <text class="deploy-info-text text-metric-bold" - y="38" transform="translate(5, 2)"> - {{formatTime(deployment.time)}} + Deployed </text> + <!--The date info--> + <g transform="translate(5, 20)"> + <text class="deploy-info-text"> + {{formatDate(deployment.time)}} + </text> + <text + class="deploy-info-text text-metric-bold" + x="62"> + {{formatTime(deployment.time)}} + </text> + </g> + <line + class="divider-line" + x1="0" + y1="38" + x2="132" + :y2="38" + stroke="#000"> + </line> + <!--Commit information--> + <g transform="translate(5, 40)"> + <icon + name="commit" + :width="12" + :height="12" + :y="3"> + </icon> + <a :xlink:href="deployment.commitUrl"> + <text + class="deploy-info-text deploy-info-text-link" + transform="translate(20, 2)"> + {{refText(deployment)}} + </text> + </a> + </g> + <!--Tag information--> + <g + transform="translate(5, 55)" + v-if="deployment.tag"> + <icon + name="label" + :width="12" + :height="12" + :y="5"> + </icon> + <a :xlink:href="deployment.tagUrl"> + <text + class="deploy-info-text deploy-info-text-link" + transform="translate(20, 2)" + y="2"> + {{deployment.tag}} + </text> + </a> + </g> </svg> </g> <svg diff --git a/app/assets/javascripts/monitoring/mixins/monitoring_mixins.js b/app/assets/javascripts/monitoring/mixins/monitoring_mixins.js index 31f38aca5d6..cbca14ede02 100644 --- a/app/assets/javascripts/monitoring/mixins/monitoring_mixins.js +++ b/app/assets/javascripts/monitoring/mixins/monitoring_mixins.js @@ -33,7 +33,9 @@ const mixins = { id: deployment.id, time, sha: deployment.sha, + commitUrl: `${this.projectPath}/commit/${deployment.sha}`, tag: deployment.tag, + tagUrl: `${this.tagsPath}/${deployment.tag}`, ref: deployment.ref.name, xPos, showDeploymentFlag: false, diff --git a/app/assets/javascripts/monitoring/utils/date_time_formatters.js b/app/assets/javascripts/monitoring/utils/date_time_formatters.js index c4c6b1ac1f5..ad07a8465e2 100644 --- a/app/assets/javascripts/monitoring/utils/date_time_formatters.js +++ b/app/assets/javascripts/monitoring/utils/date_time_formatters.js @@ -1,6 +1,7 @@ import d3 from 'd3'; export const dateFormat = d3.time.format('%b %-d, %Y'); +export const dateFormatWithName = d3.time.format('%a, %b %-d'); export const timeFormat = d3.time.format('%-I:%M%p'); export const bisectDate = d3.bisector(d => d.time).left; diff --git a/app/assets/javascripts/namespace_select.js b/app/assets/javascripts/namespace_select.js index 1d496c64e53..aa377327107 100644 --- a/app/assets/javascripts/namespace_select.js +++ b/app/assets/javascripts/namespace_select.js @@ -1,6 +1,6 @@ /* eslint-disable func-names, space-before-function-paren, no-var, comma-dangle, object-shorthand, no-else-return, prefer-template, quotes, prefer-arrow-callback, max-len */ import Api from './api'; -import './lib/utils/url_utility'; +import { mergeUrlParams } from './lib/utils/url_utility'; export default class NamespaceSelect { constructor(opts) { @@ -50,7 +50,7 @@ export default class NamespaceSelect { } }, url(namespace) { - return gl.utils.mergeUrlParams({ [fieldName]: namespace.id }, window.location.href); + return mergeUrlParams({ [fieldName]: namespace.id }, window.location.href); }, }); } diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js index e1ab28978e8..042fe44e1c6 100644 --- a/app/assets/javascripts/notes.js +++ b/app/assets/javascripts/notes.js @@ -16,6 +16,7 @@ import Autosize from 'autosize'; import 'vendor/jquery.caret'; // required by jquery.atwho import 'vendor/jquery.atwho'; import AjaxCache from '~/lib/utils/ajax_cache'; +import { getLocationHash } from './lib/utils/url_utility'; import Flash from './flash'; import CommentTypeToggle from './comment_type_toggle'; import GLForm from './gl_form'; @@ -24,6 +25,7 @@ import Autosave from './autosave'; import TaskList from './task_list'; import { ajaxPost, isInViewport, getPagePath, scrollToElement, isMetaKey } from './lib/utils/common_utils'; import imageDiffHelper from './image_diff/helpers/index'; +import { localTimeAgo } from './lib/utils/datetime_utility'; window.autosize = Autosize; @@ -310,7 +312,7 @@ export default class Notes { setupNewNote($note) { // Update datetime format on the recent note - gl.utils.localTimeAgo($note.find('.js-timeago'), false); + localTimeAgo($note.find('.js-timeago'), false); this.collapseLongCommitList(); this.taskList.init(); @@ -330,7 +332,7 @@ export default class Notes { } static updateNoteTargetSelector($note) { - const hash = gl.utils.getLocationHash(); + const hash = getLocationHash(); // Needs to be an explicit true/false for the jQuery `toggleClass(force)` const addTargetClass = Boolean(hash && $note.filter(`#${hash}`).length > 0); $note.toggleClass('target', addTargetClass); @@ -462,7 +464,7 @@ export default class Notes { this.renderDiscussionAvatar(diffAvatarContainer, noteEntity); } - gl.utils.localTimeAgo($('.js-timeago'), false); + localTimeAgo($('.js-timeago'), false); Notes.checkMergeRequestStatus(); return this.updateNotesCount(1); } diff --git a/app/assets/javascripts/notes/components/issue_comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue index 78986a450c2..e594377bc40 100644 --- a/app/assets/javascripts/notes/components/issue_comment_form.vue +++ b/app/assets/javascripts/notes/components/comment_form.vue @@ -15,7 +15,7 @@ import issuableStateMixin from '../mixins/issuable_state'; export default { - name: 'issueCommentForm', + name: 'commentForm', data() { return { note: '', diff --git a/app/assets/javascripts/notes/components/issue_note_body.vue b/app/assets/javascripts/notes/components/note_body.vue index a16c5f6a785..ac4e1ffe53a 100644 --- a/app/assets/javascripts/notes/components/issue_note_body.vue +++ b/app/assets/javascripts/notes/components/note_body.vue @@ -2,7 +2,7 @@ import noteEditedText from './note_edited_text.vue'; import noteAwardsList from './note_awards_list.vue'; import noteAttachment from './note_attachment.vue'; - import issueNoteForm from './issue_note_form.vue'; + import noteForm from './note_form.vue'; import TaskList from '../../task_list'; import autosave from '../mixins/autosave'; @@ -29,7 +29,7 @@ noteEditedText, noteAwardsList, noteAttachment, - issueNoteForm, + noteForm, }, computed: { noteBody() { @@ -87,7 +87,7 @@ <div v-html="note.note_html" class="note-text md"></div> - <issue-note-form + <note-form v-if="isEditing" ref="noteForm" @handleFormUpdate="handleFormUpdate" diff --git a/app/assets/javascripts/notes/components/issue_note_form.vue b/app/assets/javascripts/notes/components/note_form.vue index 4d527cb6643..4d527cb6643 100644 --- a/app/assets/javascripts/notes/components/issue_note_form.vue +++ b/app/assets/javascripts/notes/components/note_form.vue diff --git a/app/assets/javascripts/notes/components/issue_discussion.vue b/app/assets/javascripts/notes/components/noteable_discussion.vue index 460fde9b62a..11e8f805635 100644 --- a/app/assets/javascripts/notes/components/issue_discussion.vue +++ b/app/assets/javascripts/notes/components/noteable_discussion.vue @@ -2,12 +2,12 @@ import { mapActions, mapGetters } from 'vuex'; import Flash from '../../flash'; import { SYSTEM_NOTE } from '../constants'; - import issueNote from './issue_note.vue'; import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; + import noteableNote from './noteable_note.vue'; import noteHeader from './note_header.vue'; import noteSignedOutWidget from './note_signed_out_widget.vue'; import noteEditedText from './note_edited_text.vue'; - import issueNoteForm from './issue_note_form.vue'; + import noteForm from './note_form.vue'; import placeholderNote from '../../vue_shared/components/notes/placeholder_note.vue'; import placeholderSystemNote from '../../vue_shared/components/notes/placeholder_system_note.vue'; import autosave from '../mixins/autosave'; @@ -25,12 +25,12 @@ }; }, components: { - issueNote, + noteableNote, userAvatarLink, noteHeader, noteSignedOutWidget, noteEditedText, - issueNoteForm, + noteForm, placeholderNote, placeholderSystemNote, }, @@ -86,7 +86,7 @@ return placeholderNote; } - return issueNote; + return noteableNote; }, componentData(note) { return note.isPlaceholderNote ? note.notes[0] : note; @@ -209,7 +209,7 @@ type="button" class="js-vue-discussion-reply btn btn-text-field" title="Add a reply">Reply...</button> - <issue-note-form + <note-form v-if="isReplying" save-button-title="Comment" :discussion="note" diff --git a/app/assets/javascripts/notes/components/issue_note.vue b/app/assets/javascripts/notes/components/noteable_note.vue index 8c81c5d6df3..9186d6ff64a 100644 --- a/app/assets/javascripts/notes/components/issue_note.vue +++ b/app/assets/javascripts/notes/components/noteable_note.vue @@ -1,10 +1,11 @@ <script> import { mapGetters, mapActions } from 'vuex'; + import { escape } from 'underscore'; import Flash from '../../flash'; import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; import noteHeader from './note_header.vue'; import noteActions from './note_actions.vue'; - import issueNoteBody from './issue_note_body.vue'; + import noteBody from './note_body.vue'; import eventHub from '../event_hub'; export default { @@ -25,7 +26,7 @@ userAvatarLink, noteHeader, noteActions, - issueNoteBody, + noteBody, }, computed: { ...mapGetters([ @@ -85,7 +86,7 @@ }; this.isRequesting = true; this.oldContent = this.note.note_html; - this.note.note_html = noteText; + this.note.note_html = escape(noteText); this.updateNote(data) .then(() => { @@ -122,9 +123,7 @@ // we need to do this to prevent noteForm inconsistent content warning // this is something we intentionally do so we need to recover the content this.note.note = noteText; - if (this.$refs.noteBody) { - this.$refs.noteBody.$refs.noteForm.note = noteText; // TODO: This could be better - } + this.$refs.noteBody.$refs.noteForm.note = noteText; }, }, created() { @@ -173,7 +172,7 @@ @handleDelete="deleteHandler" /> </div> - <issue-note-body + <note-body :note="note" :can-edit="note.current_user.can_edit" :is-editing="isEditing" diff --git a/app/assets/javascripts/notes/components/issue_notes_app.vue b/app/assets/javascripts/notes/components/notes_app.vue index 4cfcffa2391..c4cae4b3b6f 100644 --- a/app/assets/javascripts/notes/components/issue_notes_app.vue +++ b/app/assets/javascripts/notes/components/notes_app.vue @@ -1,18 +1,19 @@ <script> import { mapGetters, mapActions } from 'vuex'; + import { getLocationHash } from '../../lib/utils/url_utility'; import Flash from '../../flash'; import store from '../stores/'; import * as constants from '../constants'; - import issueNote from './issue_note.vue'; - import issueDiscussion from './issue_discussion.vue'; + import noteableNote from './noteable_note.vue'; + import noteableDiscussion from './noteable_discussion.vue'; import systemNote from '../../vue_shared/components/notes/system_note.vue'; - import issueCommentForm from './issue_comment_form.vue'; + import commentForm from './comment_form.vue'; import placeholderNote from '../../vue_shared/components/notes/placeholder_note.vue'; import placeholderSystemNote from '../../vue_shared/components/notes/placeholder_system_note.vue'; import loadingIcon from '../../vue_shared/components/loading_icon.vue'; export default { - name: 'issueNotesApp', + name: 'notesApp', props: { noteableData: { type: Object, @@ -35,10 +36,10 @@ }; }, components: { - issueNote, - issueDiscussion, + noteableNote, + noteableDiscussion, systemNote, - issueCommentForm, + commentForm, loadingIcon, placeholderNote, placeholderSystemNote, @@ -68,10 +69,10 @@ } return placeholderNote; } else if (note.individual_note) { - return note.notes[0].system ? systemNote : issueNote; + return note.notes[0].system ? systemNote : noteableNote; } - return issueDiscussion; + return noteableDiscussion; }, getComponentData(note) { return note.individual_note ? note.notes[0] : note; @@ -86,7 +87,7 @@ .then(() => this.checkLocationHash()) .catch(() => { this.isLoading = false; - Flash('Something went wrong while fetching issue comments. Please try again.'); + Flash('Something went wrong while fetching comments. Please try again.'); }); }, initPolling() { @@ -95,7 +96,7 @@ this.poll(); }, checkLocationHash() { - const hash = gl.utils.getLocationHash(); + const hash = getLocationHash(); const element = document.getElementById(hash); if (hash && element) { @@ -146,6 +147,6 @@ /> </ul> - <issue-comment-form /> + <comment-form /> </div> </template> diff --git a/app/assets/javascripts/notes/index.js b/app/assets/javascripts/notes/index.js index 8d74c5de5cf..d250dd8d25b 100644 --- a/app/assets/javascripts/notes/index.js +++ b/app/assets/javascripts/notes/index.js @@ -1,17 +1,25 @@ import Vue from 'vue'; -import issueNotesApp from './components/issue_notes_app.vue'; +import notesApp from './components/notes_app.vue'; document.addEventListener('DOMContentLoaded', () => new Vue({ el: '#js-vue-notes', components: { - issueNotesApp, + notesApp, }, data() { const notesDataset = document.getElementById('js-vue-notes').dataset; + const parsedUserData = JSON.parse(notesDataset.currentUserData); + const currentUserData = parsedUserData ? { + id: parsedUserData.id, + name: parsedUserData.name, + username: parsedUserData.username, + avatar_url: parsedUserData.avatar_path || parsedUserData.avatar_url, + path: parsedUserData.path, + } : {}; return { noteableData: JSON.parse(notesDataset.noteableData), - currentUserData: JSON.parse(notesDataset.currentUserData), + currentUserData, notesData: { lastFetchedAt: notesDataset.lastFetchedAt, discussionsPath: notesDataset.discussionsPath, @@ -24,7 +32,7 @@ document.addEventListener('DOMContentLoaded', () => new Vue({ }; }, render(createElement) { - return createElement('issue-notes-app', { + return createElement('notes-app', { props: { noteableData: this.noteableData, notesData: this.notesData, diff --git a/app/assets/javascripts/notifications_dropdown.js b/app/assets/javascripts/notifications_dropdown.js index f90ac2d9f71..9570d1c00aa 100644 --- a/app/assets/javascripts/notifications_dropdown.js +++ b/app/assets/javascripts/notifications_dropdown.js @@ -1,31 +1,25 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, one-var-declaration-per-line, no-unused-vars, consistent-return, prefer-arrow-callback, no-else-return, max-len */ import Flash from './flash'; -(function() { - this.NotificationsDropdown = (function() { - function NotificationsDropdown() { - $(document).off('click', '.update-notification').on('click', '.update-notification', function(e) { - var form, label, notificationLevel; - e.preventDefault(); - if ($(this).is('.is-active') && $(this).data('notification-level') === 'custom') { - return; - } - notificationLevel = $(this).data('notification-level'); - label = $(this).data('notification-title'); - form = $(this).parents('.notification-form:first'); - form.find('.js-notification-loading').toggleClass('fa-bell fa-spin fa-spinner'); - form.find('#notification_setting_level').val(notificationLevel); - return form.submit(); - }); - $(document).off('ajax:success', '.notification-form').on('ajax:success', '.notification-form', function(e, data) { - if (data.saved) { - return $(e.currentTarget).closest('.js-notification-dropdown').replaceWith(data.html); - } else { - return new Flash('Failed to save new settings', 'alert'); - } - }); +export default function notificationsDropdown() { + $(document).on('click', '.update-notification', function updateNotificationCallback(e) { + e.preventDefault(); + if ($(this).is('.is-active') && $(this).data('notification-level') === 'custom') { + return; } - return NotificationsDropdown; - })(); -}).call(window); + const notificationLevel = $(this).data('notification-level'); + const form = $(this).parents('.notification-form:first'); + + form.find('.js-notification-loading').toggleClass('fa-bell fa-spin fa-spinner'); + form.find('#notification_setting_level').val(notificationLevel); + form.submit(); + }); + + $(document).on('ajax:success', '.notification-form', (e, data) => { + if (data.saved) { + $(e.currentTarget).closest('.js-notification-dropdown').replaceWith(data.html); + } else { + Flash('Failed to save new settings', 'alert'); + } + }); +} diff --git a/app/assets/javascripts/notifications_form.js b/app/assets/javascripts/notifications_form.js index 2ab9c4fed2c..4534360d577 100644 --- a/app/assets/javascripts/notifications_form.js +++ b/app/assets/javascripts/notifications_form.js @@ -1,55 +1,50 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, one-var, one-var-declaration-per-line, newline-per-chained-call, comma-dangle, consistent-return, prefer-arrow-callback, max-len */ -(function() { - this.NotificationsForm = (function() { - function NotificationsForm() { - this.toggleCheckbox = this.toggleCheckbox.bind(this); - this.removeEventListeners(); - this.initEventListeners(); - } +export default class NotificationsForm { + constructor() { + this.toggleCheckbox = this.toggleCheckbox.bind(this); + this.initEventListeners(); + } - NotificationsForm.prototype.removeEventListeners = function() { - return $(document).off('change', '.js-custom-notification-event'); - }; + initEventListeners() { + $(document).on('change', '.js-custom-notification-event', this.toggleCheckbox); + } - NotificationsForm.prototype.initEventListeners = function() { - return $(document).on('change', '.js-custom-notification-event', this.toggleCheckbox); - }; + toggleCheckbox(e) { + const $checkbox = $(e.currentTarget); + const $parent = $checkbox.closest('.checkbox'); - NotificationsForm.prototype.toggleCheckbox = function(e) { - var $checkbox, $parent; - $checkbox = $(e.currentTarget); - $parent = $checkbox.closest('.checkbox'); - return this.saveEvent($checkbox, $parent); - }; + this.saveEvent($checkbox, $parent); + } - NotificationsForm.prototype.showCheckboxLoadingSpinner = function($parent) { - return $parent.addClass('is-loading').find('.custom-notification-event-loading').removeClass('fa-check').addClass('fa-spin fa-spinner').removeClass('is-done'); - }; + // eslint-disable-next-line class-methods-use-this + showCheckboxLoadingSpinner($parent) { + $parent.addClass('is-loading') + .find('.custom-notification-event-loading') + .removeClass('fa-check') + .addClass('fa-spin fa-spinner') + .removeClass('is-done'); + } - NotificationsForm.prototype.saveEvent = function($checkbox, $parent) { - var form; - form = $parent.parents('form:first'); - return $.ajax({ - url: form.attr('action'), - method: form.attr('method'), - dataType: 'json', - data: form.serialize(), - beforeSend: (function(_this) { - return function() { - return _this.showCheckboxLoadingSpinner($parent); - }; - })(this) - }).done(function(data) { - $checkbox.enable(); - if (data.saved) { - $parent.find('.custom-notification-event-loading').toggleClass('fa-spin fa-spinner fa-check is-done'); - return setTimeout(function() { - return $parent.removeClass('is-loading').find('.custom-notification-event-loading').toggleClass('fa-spin fa-spinner fa-check is-done'); - }, 2000); - } - }); - }; + saveEvent($checkbox, $parent) { + const form = $parent.parents('form:first'); - return NotificationsForm; - })(); -}).call(window); + return $.ajax({ + url: form.attr('action'), + method: form.attr('method'), + dataType: 'json', + data: form.serialize(), + beforeSend: () => { + this.showCheckboxLoadingSpinner($parent); + }, + }).done((data) => { + $checkbox.enable(); + if (data.saved) { + $parent.find('.custom-notification-event-loading').toggleClass('fa-spin fa-spinner fa-check is-done'); + setTimeout(() => { + $parent.removeClass('is-loading') + .find('.custom-notification-event-loading') + .toggleClass('fa-spin fa-spinner fa-check is-done'); + }, 2000); + } + }); + } +} diff --git a/app/assets/javascripts/pager.js b/app/assets/javascripts/pager.js index e3fc1e2fc2f..6552a88b606 100644 --- a/app/assets/javascripts/pager.js +++ b/app/assets/javascripts/pager.js @@ -1,78 +1,74 @@ import { getParameterByName } from '~/lib/utils/common_utils'; -import '~/lib/utils/url_utility'; +import { removeParams } from './lib/utils/url_utility'; -(() => { - const ENDLESS_SCROLL_BOTTOM_PX = 400; - const ENDLESS_SCROLL_FIRE_DELAY_MS = 1000; +const ENDLESS_SCROLL_BOTTOM_PX = 400; +const ENDLESS_SCROLL_FIRE_DELAY_MS = 1000; - const Pager = { - init(limit = 0, preload = false, disable = false, prepareData = $.noop, callback = $.noop) { - this.url = $('.content_list').data('href') || gl.utils.removeParams(['limit', 'offset']); - this.limit = limit; - this.offset = parseInt(getParameterByName('offset'), 10) || this.limit; - this.disable = disable; - this.prepareData = prepareData; - this.callback = callback; - this.loading = $('.loading').first(); - if (preload) { - this.offset = 0; - this.getOld(); - } - this.initLoadMore(); - }, +export default { + init(limit = 0, preload = false, disable = false, prepareData = $.noop, callback = $.noop) { + this.url = $('.content_list').data('href') || removeParams(['limit', 'offset']); + this.limit = limit; + this.offset = parseInt(getParameterByName('offset'), 10) || this.limit; + this.disable = disable; + this.prepareData = prepareData; + this.callback = callback; + this.loading = $('.loading').first(); + if (preload) { + this.offset = 0; + this.getOld(); + } + this.initLoadMore(); + }, - getOld() { - this.loading.show(); - $.ajax({ - type: 'GET', - url: this.url, - data: `limit=${this.limit}&offset=${this.offset}`, - dataType: 'json', - error: () => this.loading.hide(), - success: (data) => { - this.append(data.count, this.prepareData(data.html)); - this.callback(); + getOld() { + this.loading.show(); + $.ajax({ + type: 'GET', + url: this.url, + data: `limit=${this.limit}&offset=${this.offset}`, + dataType: 'json', + error: () => this.loading.hide(), + success: (data) => { + this.append(data.count, this.prepareData(data.html)); + this.callback(); - // keep loading until we've filled the viewport height - if (!this.disable && !this.isScrollable()) { - this.getOld(); - } else { - this.loading.hide(); - } - }, - }); - }, + // keep loading until we've filled the viewport height + if (!this.disable && !this.isScrollable()) { + this.getOld(); + } else { + this.loading.hide(); + } + }, + }); + }, - append(count, html) { - $('.content_list').append(html); - if (count > 0) { - this.offset += count; - } else { - this.disable = true; - } - }, + append(count, html) { + $('.content_list').append(html); + if (count > 0) { + this.offset += count; + } else { + this.disable = true; + } + }, - isScrollable() { - const $w = $(window); - return $(document).height() > $w.height() + $w.scrollTop() + ENDLESS_SCROLL_BOTTOM_PX; - }, + isScrollable() { + const $w = $(window); + return $(document).height() > $w.height() + $w.scrollTop() + ENDLESS_SCROLL_BOTTOM_PX; + }, - initLoadMore() { - $(document).unbind('scroll'); - $(document).endlessScroll({ - bottomPixels: ENDLESS_SCROLL_BOTTOM_PX, - fireDelay: ENDLESS_SCROLL_FIRE_DELAY_MS, - fireOnce: true, - ceaseFire: () => this.disable === true, - callback: () => { - if (!this.loading.is(':visible')) { - this.loading.show(); - this.getOld(); - } - }, - }); - }, - }; - - window.Pager = Pager; -})(); + initLoadMore() { + $(document).unbind('scroll'); + $(document).endlessScroll({ + bottomPixels: ENDLESS_SCROLL_BOTTOM_PX, + fireDelay: ENDLESS_SCROLL_FIRE_DELAY_MS, + fireOnce: true, + ceaseFire: () => this.disable === true, + callback: () => { + if (!this.loading.is(':visible')) { + this.loading.show(); + this.getOld(); + } + }, + }); + }, +}; diff --git a/app/assets/javascripts/performance_bar.js b/app/assets/javascripts/performance_bar.js index 9bbdf7f513c..0562a681c4b 100644 --- a/app/assets/javascripts/performance_bar.js +++ b/app/assets/javascripts/performance_bar.js @@ -1,5 +1,6 @@ import 'vendor/peek'; import 'vendor/peek.performance_bar'; +import { getParameterValues } from './lib/utils/url_utility'; export default class PerformanceBar { constructor(opts) { @@ -39,7 +40,7 @@ export default class PerformanceBar { } handleLineProfileLink(e) { - const lineProfilerParameter = gl.utils.getParameterValues('lineprofiler'); + const lineProfilerParameter = getParameterValues('lineprofiler'); const lineProfilerParameterRegex = new RegExp(`lineprofiler=${lineProfilerParameter[0]}`); const shouldToggleModal = lineProfilerParameter.length > 0 && lineProfilerParameterRegex.test(e.currentTarget.href); diff --git a/app/assets/javascripts/pipelines/components/empty_state.vue b/app/assets/javascripts/pipelines/components/empty_state.vue index 0eaac8dd64f..78322f30685 100644 --- a/app/assets/javascripts/pipelines/components/empty_state.vue +++ b/app/assets/javascripts/pipelines/components/empty_state.vue @@ -1,36 +1,41 @@ <script> -export default { - props: { - helpPagePath: { - type: String, - required: true, + export default { + props: { + helpPagePath: { + type: String, + required: true, + }, + emptyStateSvgPath: { + type: String, + required: true, + }, }, - emptyStateSvgPath: { - type: String, - required: true, - }, - }, -}; + }; </script> - <template> <div class="row empty-state js-empty-state"> <div class="col-xs-12"> - <div class="svg-content"> - <img :src="emptyStateSvgPath"/> + <div class="svg-content svg-250"> + <img :src="emptyStateSvgPath" /> </div> </div> - <div class="col-xs-12 text-center"> + <div class="col-xs-12"> <div class="text-content"> - <h4>Build with confidence</h4> + <h4 class="text-center"> + {{ s__("Pipelines|Build with confidence") }} + </h4> <p> - Continous Integration can help catch bugs by running your tests automatically, - while Continuous Deployment can help you deliver code to your product environment. + {{ s__("Pipelines|Continous Integration can help catch bugs by running your tests automatically, while Continuous Deployment can help you deliver code to your product environment.") }} </p> - <a :href="helpPagePath" class="btn btn-info"> - Get started with Pipelines - </a> + <div class="text-center"> + <a + :href="helpPagePath" + class="btn btn-info" + > + {{ s__("Pipelines|Get started with Pipelines") }} + </a> + </div> </div> </div> </div> diff --git a/app/assets/javascripts/pipelines/components/graph/job_component.vue b/app/assets/javascripts/pipelines/components/graph/job_component.vue index 08199b4234a..b01c799643c 100644 --- a/app/assets/javascripts/pipelines/components/graph/job_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/job_component.vue @@ -59,8 +59,26 @@ }, computed: { + status() { + return this.job && this.job.status ? this.job.status : {}; + }, + tooltipText() { - return `${this.job.name} - ${this.job.status.label}`; + const textBuilder = []; + + if (this.job.name) { + textBuilder.push(this.job.name); + } + + if (this.job.name && this.status.label) { + textBuilder.push('-'); + } + + if (this.status.label) { + textBuilder.push(`${this.job.status.label}`); + } + + return textBuilder.join(' '); }, /** @@ -78,8 +96,8 @@ <div class="ci-job-component"> <a v-tooltip - v-if="job.status.has_details" - :href="job.status.details_path" + v-if="status.has_details" + :href="status.details_path" :title="tooltipText" :class="cssClassJobName" data-container="body" @@ -95,6 +113,7 @@ <div v-else v-tooltip + class="js-job-component-tooltip" :title="tooltipText" :class="cssClassJobName" data-container="body" @@ -108,18 +127,18 @@ <action-component v-if="hasAction && !isDropdown" - :tooltip-text="job.status.action.title" - :link="job.status.action.path" - :action-icon="job.status.action.icon" - :action-method="job.status.action.method" + :tooltip-text="status.action.title" + :link="status.action.path" + :action-icon="status.action.icon" + :action-method="status.action.method" /> <dropdown-action-component v-if="hasAction && isDropdown" - :tooltip-text="job.status.action.title" - :link="job.status.action.path" - :action-icon="job.status.action.icon" - :action-method="job.status.action.method" + :tooltip-text="status.action.title" + :link="status.action.path" + :action-icon="status.action.icon" + :action-method="status.action.method" /> </div> </template> diff --git a/app/assets/javascripts/pipelines/pipelines_bundle.js b/app/assets/javascripts/pipelines/pipelines_bundle.js index 923d9bfb248..3e4b6eeb5bf 100644 --- a/app/assets/javascripts/pipelines/pipelines_bundle.js +++ b/app/assets/javascripts/pipelines/pipelines_bundle.js @@ -1,6 +1,9 @@ import Vue from 'vue'; import PipelinesStore from './stores/pipelines_store'; import pipelinesComponent from './components/pipelines.vue'; +import Translate from '../vue_shared/translate'; + +Vue.use(Translate); document.addEventListener('DOMContentLoaded', () => new Vue({ el: '#pipelines-list-vue', diff --git a/app/assets/javascripts/profile/account/components/delete_account_modal.vue b/app/assets/javascripts/profile/account/components/delete_account_modal.vue index 6348a2e331d..78be6b6e884 100644 --- a/app/assets/javascripts/profile/account/components/delete_account_modal.vue +++ b/app/assets/javascripts/profile/account/components/delete_account_modal.vue @@ -1,5 +1,5 @@ <script> - import popupDialog from '../../../vue_shared/components/popup_dialog.vue'; + import modal from '../../../vue_shared/components/modal.vue'; import { __, s__, sprintf } from '../../../locale'; import csrf from '../../../lib/utils/csrf'; @@ -26,7 +26,7 @@ }; }, components: { - popupDialog, + modal, }, computed: { csrfToken() { @@ -89,7 +89,7 @@ Once you confirm %{deleteAccount}, it cannot be undone or recovered.`), <template> <div> - <popup-dialog + <modal v-if="isOpen" :title="s__('Profiles|Delete your account?')" :text="text" @@ -134,7 +134,7 @@ Once you confirm %{deleteAccount}, it cannot be undone or recovered.`), </form> </template> - </popup-dialog> + </modal> <button type="button" diff --git a/app/assets/javascripts/project.js b/app/assets/javascripts/project.js index 3131e71d9d6..d4f26b81f30 100644 --- a/app/assets/javascripts/project.js +++ b/app/assets/javascripts/project.js @@ -1,6 +1,7 @@ /* eslint-disable func-names, space-before-function-paren, no-var, consistent-return, no-new, prefer-arrow-callback, no-return-assign, one-var, one-var-declaration-per-line, object-shorthand, no-else-return, newline-per-chained-call, no-shadow, vars-on-top, prefer-template, max-len */ import Cookies from 'js-cookie'; +import { visitUrl } from './lib/utils/url_utility'; import projectSelect from './project_select'; export default class Project { @@ -122,7 +123,7 @@ export default class Project { var action = $form.attr('action'); var divider = action.indexOf('?') === -1 ? '?' : '&'; if (shouldVisit) { - gl.utils.visitUrl(`${action}${divider}${$form.serialize()}`); + visitUrl(`${action}${divider}${$form.serialize()}`); } } }, diff --git a/app/assets/javascripts/project_find_file.js b/app/assets/javascripts/project_find_file.js index 19682b20a4a..0da32b4a3cc 100644 --- a/app/assets/javascripts/project_find_file.js +++ b/app/assets/javascripts/project_find_file.js @@ -2,169 +2,163 @@ import fuzzaldrinPlus from 'fuzzaldrin-plus'; -(function() { - this.ProjectFindFile = (function() { - var highlighter; - - function ProjectFindFile(element1, options) { - this.element = element1; - this.options = options; - this.goToBlob = this.goToBlob.bind(this); - this.goToTree = this.goToTree.bind(this); - this.selectRowDown = this.selectRowDown.bind(this); - this.selectRowUp = this.selectRowUp.bind(this); - this.filePaths = {}; - this.inputElement = this.element.find(".file-finder-input"); - // init event - this.initEvent(); - // focus text input box - this.inputElement.focus(); - // load file list - this.load(this.options.url); +// highlight text(awefwbwgtc -> <b>a</b>wefw<b>b</b>wgt<b>c</b> ) +const highlighter = function(element, text, matches) { + var highlightText, j, lastIndex, len, matchIndex, matchedChars, unmatched; + lastIndex = 0; + highlightText = ""; + matchedChars = []; + for (j = 0, len = matches.length; j < len; j += 1) { + matchIndex = matches[j]; + unmatched = text.substring(lastIndex, matchIndex); + if (unmatched) { + if (matchedChars.length) { + element.append(matchedChars.join("").bold()); + } + matchedChars = []; + element.append(document.createTextNode(unmatched)); } - - ProjectFindFile.prototype.initEvent = function() { - this.inputElement.off("keyup"); - this.inputElement.on("keyup", (function(_this) { - return function(event) { - var oldValue, ref, target, value; - target = $(event.target); - value = target.val(); - oldValue = (ref = target.data("oldValue")) != null ? ref : ""; - if (value !== oldValue) { - target.data("oldValue", value); - _this.findFile(); - return _this.element.find("tr.tree-item").eq(0).addClass("selected").focus(); - } - }; - })(this)); - }; - - ProjectFindFile.prototype.findFile = function() { - var result, searchText; - searchText = this.inputElement.val(); - result = searchText.length > 0 ? fuzzaldrinPlus.filter(this.filePaths, searchText) : this.filePaths; - return this.renderList(result, searchText); - // find file - }; + matchedChars.push(text[matchIndex]); + lastIndex = matchIndex + 1; + } + if (matchedChars.length) { + element.append(matchedChars.join("").bold()); + } + return element.append(document.createTextNode(text.substring(lastIndex))); +}; + +export default class ProjectFindFile { + constructor(element1, options) { + this.element = element1; + this.options = options; + this.goToBlob = this.goToBlob.bind(this); + this.goToTree = this.goToTree.bind(this); + this.selectRowDown = this.selectRowDown.bind(this); + this.selectRowUp = this.selectRowUp.bind(this); + this.filePaths = {}; + this.inputElement = this.element.find(".file-finder-input"); + // init event + this.initEvent(); + // focus text input box + this.inputElement.focus(); + // load file list + this.load(this.options.url); + } + + initEvent() { + this.inputElement.off("keyup"); + this.inputElement.on("keyup", (function(_this) { + return function(event) { + var oldValue, ref, target, value; + target = $(event.target); + value = target.val(); + oldValue = (ref = target.data("oldValue")) != null ? ref : ""; + if (value !== oldValue) { + target.data("oldValue", value); + _this.findFile(); + return _this.element.find("tr.tree-item").eq(0).addClass("selected").focus(); + } + }; + })(this)); + } + + findFile() { + var result, searchText; + searchText = this.inputElement.val(); + result = searchText.length > 0 ? fuzzaldrinPlus.filter(this.filePaths, searchText) : this.filePaths; + return this.renderList(result, searchText); + // find file + } // files pathes load - ProjectFindFile.prototype.load = function(url) { - return $.ajax({ - url: url, - method: "get", - dataType: "json", - success: (function(_this) { - return function(data) { - _this.element.find(".loading").hide(); - _this.filePaths = data; - _this.findFile(); - return _this.element.find(".files-slider tr.tree-item").eq(0).addClass("selected").focus(); - }; - })(this) - }); - }; + load(url) { + return $.ajax({ + url: url, + method: "get", + dataType: "json", + success: (function(_this) { + return function(data) { + _this.element.find(".loading").hide(); + _this.filePaths = data; + _this.findFile(); + return _this.element.find(".files-slider tr.tree-item").eq(0).addClass("selected").focus(); + }; + })(this) + }); + } // render result - ProjectFindFile.prototype.renderList = function(filePaths, searchText) { - var blobItemUrl, filePath, html, i, j, len, matches, results; - this.element.find(".tree-table > tbody").empty(); - results = []; - for (i = j = 0, len = filePaths.length; j < len; i = (j += 1)) { - filePath = filePaths[i]; - if (i === 20) { - break; - } - if (searchText) { - matches = fuzzaldrinPlus.match(filePath, searchText); - } - blobItemUrl = this.options.blobUrlTemplate + "/" + filePath; - html = this.makeHtml(filePath, matches, blobItemUrl); - results.push(this.element.find(".tree-table > tbody").append(html)); - } - return results; - }; - - // highlight text(awefwbwgtc -> <b>a</b>wefw<b>b</b>wgt<b>c</b> ) - highlighter = function(element, text, matches) { - var highlightText, j, lastIndex, len, matchIndex, matchedChars, unmatched; - lastIndex = 0; - highlightText = ""; - matchedChars = []; - for (j = 0, len = matches.length; j < len; j += 1) { - matchIndex = matches[j]; - unmatched = text.substring(lastIndex, matchIndex); - if (unmatched) { - if (matchedChars.length) { - element.append(matchedChars.join("").bold()); - } - matchedChars = []; - element.append(document.createTextNode(unmatched)); - } - matchedChars.push(text[matchIndex]); - lastIndex = matchIndex + 1; - } - if (matchedChars.length) { - element.append(matchedChars.join("").bold()); + renderList(filePaths, searchText) { + var blobItemUrl, filePath, html, i, j, len, matches, results; + this.element.find(".tree-table > tbody").empty(); + results = []; + for (i = j = 0, len = filePaths.length; j < len; i = (j += 1)) { + filePath = filePaths[i]; + if (i === 20) { + break; } - return element.append(document.createTextNode(text.substring(lastIndex))); - }; - - // make tbody row html - ProjectFindFile.prototype.makeHtml = function(filePath, matches, blobItemUrl) { - var $tr; - $tr = $("<tr class='tree-item'><td class='tree-item-file-name link-container'><a><i class='fa fa-file-text-o fa-fw'></i><span class='str-truncated'></span></a></td></tr>"); - if (matches) { - $tr.find("a").replaceWith(highlighter($tr.find("a"), filePath, matches).attr("href", blobItemUrl)); - } else { - $tr.find("a").attr("href", blobItemUrl); - $tr.find(".str-truncated").text(filePath); + if (searchText) { + matches = fuzzaldrinPlus.match(filePath, searchText); } - return $tr; - }; - - ProjectFindFile.prototype.selectRow = function(type) { - var next, rows, selectedRow; - rows = this.element.find(".files-slider tr.tree-item"); - selectedRow = this.element.find(".files-slider tr.tree-item.selected"); - if (rows && rows.length > 0) { - if (selectedRow && selectedRow.length > 0) { - if (type === "UP") { - next = selectedRow.prev(); - } else if (type === "DOWN") { - next = selectedRow.next(); - } - if (next.length > 0) { - selectedRow.removeClass("selected"); - selectedRow = next; - } - } else { - selectedRow = rows.eq(0); + blobItemUrl = this.options.blobUrlTemplate + "/" + filePath; + html = ProjectFindFile.makeHtml(filePath, matches, blobItemUrl); + results.push(this.element.find(".tree-table > tbody").append(html)); + } + return results; + } + + // make tbody row html + static makeHtml(filePath, matches, blobItemUrl) { + var $tr; + $tr = $("<tr class='tree-item'><td class='tree-item-file-name link-container'><a><i class='fa fa-file-text-o fa-fw'></i><span class='str-truncated'></span></a></td></tr>"); + if (matches) { + $tr.find("a").replaceWith(highlighter($tr.find("a"), filePath, matches).attr("href", blobItemUrl)); + } else { + $tr.find("a").attr("href", blobItemUrl); + $tr.find(".str-truncated").text(filePath); + } + return $tr; + } + + selectRow(type) { + var next, rows, selectedRow; + rows = this.element.find(".files-slider tr.tree-item"); + selectedRow = this.element.find(".files-slider tr.tree-item.selected"); + if (rows && rows.length > 0) { + if (selectedRow && selectedRow.length > 0) { + if (type === "UP") { + next = selectedRow.prev(); + } else if (type === "DOWN") { + next = selectedRow.next(); + } + if (next.length > 0) { + selectedRow.removeClass("selected"); + selectedRow = next; } - return selectedRow.addClass("selected").focus(); + } else { + selectedRow = rows.eq(0); } - }; - - ProjectFindFile.prototype.selectRowUp = function() { - return this.selectRow("UP"); - }; + return selectedRow.addClass("selected").focus(); + } + } - ProjectFindFile.prototype.selectRowDown = function() { - return this.selectRow("DOWN"); - }; + selectRowUp() { + return this.selectRow("UP"); + } - ProjectFindFile.prototype.goToTree = function() { - return location.href = this.options.treeUrl; - }; + selectRowDown() { + return this.selectRow("DOWN"); + } - ProjectFindFile.prototype.goToBlob = function() { - var $link = this.element.find(".tree-item.selected .tree-item-file-name a"); + goToTree() { + return location.href = this.options.treeUrl; + } - if ($link.length) { - $link.get(0).click(); - } - }; + goToBlob() { + var $link = this.element.find(".tree-item.selected .tree-item-file-name a"); - return ProjectFindFile; - })(); -}).call(window); + if ($link.length) { + $link.get(0).click(); + } + } +} diff --git a/app/assets/javascripts/project_variables.js b/app/assets/javascripts/project_variables.js deleted file mode 100644 index 567c311f119..00000000000 --- a/app/assets/javascripts/project_variables.js +++ /dev/null @@ -1,39 +0,0 @@ - -const HIDDEN_VALUE_TEXT = '******'; - -export default class ProjectVariables { - constructor() { - this.$revealBtn = $('.js-btn-toggle-reveal-values'); - this.$revealBtn.on('click', this.toggleRevealState.bind(this)); - } - - toggleRevealState(e) { - e.preventDefault(); - - const oldStatus = this.$revealBtn.attr('data-status'); - let newStatus = 'hidden'; - let newAction = 'Reveal Values'; - - if (oldStatus === 'hidden') { - newStatus = 'revealed'; - newAction = 'Hide Values'; - } - - this.$revealBtn.attr('data-status', newStatus); - - const $variables = $('.variable-value'); - - $variables.each((_, variable) => { - const $variable = $(variable); - let newText = HIDDEN_VALUE_TEXT; - - if (newStatus === 'revealed') { - newText = $variable.attr('data-value'); - } - - $variable.text(newText); - }); - - this.$revealBtn.text(newAction); - } -} diff --git a/app/assets/javascripts/projects/project_import_gitlab_project.js b/app/assets/javascripts/projects/project_import_gitlab_project.js index c34927499fc..cec6f0dd5a3 100644 --- a/app/assets/javascripts/projects/project_import_gitlab_project.js +++ b/app/assets/javascripts/projects/project_import_gitlab_project.js @@ -1,7 +1,7 @@ -import '../lib/utils/url_utility'; +import { getParameterValues } from '../lib/utils/url_utility'; const bindEvents = () => { - const path = gl.utils.getParameterValues('path')[0]; + const path = getParameterValues('path')[0]; // get the path url and append it in the inputS $('.js-path-name').val(path); diff --git a/app/assets/javascripts/render_gfm.js b/app/assets/javascripts/render_gfm.js index c91a0d9ba41..5482c55f8bb 100644 --- a/app/assets/javascripts/render_gfm.js +++ b/app/assets/javascripts/render_gfm.js @@ -1,12 +1,12 @@ import renderMath from './render_math'; import renderMermaid from './render_mermaid'; - +import syntaxHighlight from './syntax_highlight'; // Render Gitlab flavoured Markdown // // Delegates to syntax highlight and render math & mermaid diagrams. // $.fn.renderGFM = function renderGFM() { - this.find('.js-syntax-highlight').syntaxHighlight(); + syntaxHighlight(this.find('.js-syntax-highlight')); renderMath(this.find('.js-render-math')); renderMermaid(this.find('.js-render-mermaid')); return this; diff --git a/app/assets/javascripts/repo/components/new_dropdown/modal.vue b/app/assets/javascripts/repo/components/new_dropdown/modal.vue index ac1f613bb71..c191af7dec3 100644 --- a/app/assets/javascripts/repo/components/new_dropdown/modal.vue +++ b/app/assets/javascripts/repo/components/new_dropdown/modal.vue @@ -1,7 +1,7 @@ <script> import { mapActions } from 'vuex'; import { __ } from '../../../locale'; - import popupDialog from '../../../vue_shared/components/popup_dialog.vue'; + import modal from '../../../vue_shared/components/modal.vue'; export default { props: { @@ -20,7 +20,7 @@ }; }, components: { - popupDialog, + modal, }, methods: { ...mapActions([ @@ -68,7 +68,7 @@ </script> <template> - <popup-dialog + <modal :title="modalTitle" :primary-button-label="buttonLabel" kind="success" @@ -94,5 +94,5 @@ </div> </fieldset> </form> - </popup-dialog> + </modal> </template> diff --git a/app/assets/javascripts/repo/components/repo_commit_section.vue b/app/assets/javascripts/repo/components/repo_commit_section.vue index d3344d0c8dc..4e0178072cb 100644 --- a/app/assets/javascripts/repo/components/repo_commit_section.vue +++ b/app/assets/javascripts/repo/components/repo_commit_section.vue @@ -2,12 +2,12 @@ import { mapGetters, mapState, mapActions } from 'vuex'; import tooltip from '../../vue_shared/directives/tooltip'; import icon from '../../vue_shared/components/icon.vue'; -import PopupDialog from '../../vue_shared/components/popup_dialog.vue'; +import modal from '../../vue_shared/components/modal.vue'; import commitFilesList from './commit_sidebar/list.vue'; export default { components: { - PopupDialog, + modal, icon, commitFilesList, }, @@ -16,7 +16,7 @@ export default { }, data() { return { - showNewBranchDialog: false, + showNewBranchModal: false, submitCommitsLoading: false, startNewMR: false, commitMessage: '', @@ -58,7 +58,7 @@ export default { start_branch: createNewBranch ? this.currentBranch : undefined, }; - this.showNewBranchDialog = false; + this.showNewBranchModal = false; this.submitCommitsLoading = true; this.commitChanges({ payload, newMr: this.startNewMR }) @@ -76,7 +76,7 @@ export default { this.checkCommitStatus() .then((branchChanged) => { if (branchChanged) { - this.showNewBranchDialog = true; + this.showNewBranchModal = true; } else { this.makeCommit(); } @@ -99,13 +99,13 @@ export default { 'is-collapsed': collapsed, }" > - <popup-dialog - v-if="showNewBranchDialog" + <modal + v-if="showNewBranchModal" :primary-button-label="__('Create new branch')" kind="primary" :title="__('Branch has changed')" :text="__('This branch has changed since you started editing. Would you like to create a new branch?')" - @toggle="showNewBranchDialog = false" + @toggle="showNewBranchModal = false" @submit="makeCommit(true)" /> <button diff --git a/app/assets/javascripts/repo/components/repo_edit_button.vue b/app/assets/javascripts/repo/components/repo_edit_button.vue index 6c1bb4b8566..37bd9003e96 100644 --- a/app/assets/javascripts/repo/components/repo_edit_button.vue +++ b/app/assets/javascripts/repo/components/repo_edit_button.vue @@ -1,10 +1,10 @@ <script> import { mapGetters, mapActions, mapState } from 'vuex'; -import popupDialog from '../../vue_shared/components/popup_dialog.vue'; +import modal from '../../vue_shared/components/modal.vue'; export default { components: { - popupDialog, + modal, }, computed: { ...mapState([ @@ -43,7 +43,7 @@ export default { {{buttonLabel}} </span> </button> - <popup-dialog + <modal v-if="discardPopupOpen" class="text-left" :primary-button-label="__('Discard changes')" diff --git a/app/assets/javascripts/repo/components/repo_preview.vue b/app/assets/javascripts/repo/components/repo_preview.vue index 6ce9267f598..3d1e0297bd5 100644 --- a/app/assets/javascripts/repo/components/repo_preview.vue +++ b/app/assets/javascripts/repo/components/repo_preview.vue @@ -1,6 +1,7 @@ <script> -/* global LineHighlighter */ import { mapGetters } from 'vuex'; +import LineHighlighter from '../../line_highlighter'; +import syntaxHighlight from '../../syntax_highlight'; export default { computed: { @@ -13,7 +14,7 @@ export default { }, methods: { highlightFile() { - $(this.$el).find('.file-content').syntaxHighlight(); + syntaxHighlight($(this.$el).find('.file-content')); }, }, mounted() { diff --git a/app/assets/javascripts/repo/stores/actions.js b/app/assets/javascripts/repo/stores/actions.js index 120ce96f44d..af5dcf054ef 100644 --- a/app/assets/javascripts/repo/stores/actions.js +++ b/app/assets/javascripts/repo/stores/actions.js @@ -1,9 +1,10 @@ import Vue from 'vue'; +import { visitUrl } from '../../lib/utils/url_utility'; import flash from '../../flash'; import service from '../services'; import * as types from './mutation_types'; -export const redirectToUrl = (_, url) => gl.utils.visitUrl(url); +export const redirectToUrl = (_, url) => visitUrl(url); export const setInitialData = ({ commit }, data) => commit(types.SET_INITIAL_DATA, data); diff --git a/app/assets/javascripts/repo/stores/actions/tree.js b/app/assets/javascripts/repo/stores/actions/tree.js index aa830e946a2..7c251e26bed 100644 --- a/app/assets/javascripts/repo/stores/actions/tree.js +++ b/app/assets/javascripts/repo/stores/actions/tree.js @@ -1,3 +1,4 @@ +import { visitUrl } from '../../../lib/utils/url_utility'; import { normalizeHeaders } from '../../../lib/utils/common_utils'; import flash from '../../../flash'; import service from '../../services'; @@ -73,7 +74,7 @@ export const clickedTreeRow = ({ commit, dispatch }, row) => { } else if (row.type === 'submodule') { commit(types.TOGGLE_LOADING, row); - gl.utils.visitUrl(row.url); + visitUrl(row.url); } else if (row.type === 'blob' && row.opened) { dispatch('setFileActive', row); } else { diff --git a/app/assets/javascripts/right_sidebar.js b/app/assets/javascripts/right_sidebar.js index fa7f6825d7e..b830fcf7e80 100644 --- a/app/assets/javascripts/right_sidebar.js +++ b/app/assets/javascripts/right_sidebar.js @@ -3,226 +3,228 @@ import _ from 'underscore'; import Cookies from 'js-cookie'; -(function() { - this.Sidebar = (function() { - function Sidebar(currentUser) { - this.toggleTodo = this.toggleTodo.bind(this); - this.sidebar = $('aside'); - - this.removeListeners(); - this.addEventListeners(); +function Sidebar(currentUser) { + this.toggleTodo = this.toggleTodo.bind(this); + this.sidebar = $('aside'); + + this.removeListeners(); + this.addEventListeners(); +} + +Sidebar.initialize = function(currentUser) { + if (!this.instance) { + this.instance = new Sidebar(currentUser); + } +}; + +Sidebar.prototype.removeListeners = function () { + this.sidebar.off('click', '.sidebar-collapsed-icon'); + this.sidebar.off('hidden.gl.dropdown'); + $('.dropdown').off('loading.gl.dropdown'); + $('.dropdown').off('loaded.gl.dropdown'); + $(document).off('click', '.js-sidebar-toggle'); +}; + +Sidebar.prototype.addEventListeners = function() { + const $document = $(document); + + this.sidebar.on('click', '.sidebar-collapsed-icon', this, this.sidebarCollapseClicked); + this.sidebar.on('hidden.gl.dropdown', this, this.onSidebarDropdownHidden); + $('.dropdown').on('loading.gl.dropdown', this.sidebarDropdownLoading); + $('.dropdown').on('loaded.gl.dropdown', this.sidebarDropdownLoaded); + + $document.on('click', '.js-sidebar-toggle', this.sidebarToggleClicked); + return $(document).off('click', '.js-issuable-todo').on('click', '.js-issuable-todo', this.toggleTodo); +}; + +Sidebar.prototype.sidebarToggleClicked = function (e, triggered) { + var $allGutterToggleIcons, $this, $thisIcon; + e.preventDefault(); + $this = $(this); + $thisIcon = $this.find('i'); + $allGutterToggleIcons = $('.js-sidebar-toggle i'); + if ($thisIcon.hasClass('fa-angle-double-right')) { + $allGutterToggleIcons.removeClass('fa-angle-double-right').addClass('fa-angle-double-left'); + $('aside.right-sidebar').removeClass('right-sidebar-expanded').addClass('right-sidebar-collapsed'); + $('.layout-page').removeClass('right-sidebar-expanded').addClass('right-sidebar-collapsed'); + } else { + $allGutterToggleIcons.removeClass('fa-angle-double-left').addClass('fa-angle-double-right'); + $('aside.right-sidebar').removeClass('right-sidebar-collapsed').addClass('right-sidebar-expanded'); + $('.layout-page').removeClass('right-sidebar-collapsed').addClass('right-sidebar-expanded'); + + if (gl.lazyLoader) gl.lazyLoader.loadCheck(); + } + if (!triggered) { + Cookies.set("collapsed_gutter", $('.right-sidebar').hasClass('right-sidebar-collapsed')); + } +}; + +Sidebar.prototype.toggleTodo = function(e) { + var $btnText, $this, $todoLoading, ajaxType, url; + $this = $(e.currentTarget); + ajaxType = $this.attr('data-delete-path') ? 'DELETE' : 'POST'; + if ($this.attr('data-delete-path')) { + url = "" + ($this.attr('data-delete-path')); + } else { + url = "" + ($this.data('url')); + } + + $this.tooltip('hide'); + + return $.ajax({ + url: url, + type: ajaxType, + dataType: 'json', + data: { + issuable_id: $this.data('issuable-id'), + issuable_type: $this.data('issuable-type') + }, + beforeSend: (function(_this) { + return function() { + $('.js-issuable-todo').disable() + .addClass('is-loading'); + }; + })(this) + }).done((function(_this) { + return function(data) { + return _this.todoUpdateDone(data); + }; + })(this)); +}; + +Sidebar.prototype.todoUpdateDone = function(data) { + const deletePath = data.delete_path ? data.delete_path : null; + const attrPrefix = deletePath ? 'mark' : 'todo'; + const $todoBtns = $('.js-issuable-todo'); + + $(document).trigger('todo:toggle', data.count); + + $todoBtns.each((i, el) => { + const $el = $(el); + const $elText = $el.find('.js-issuable-todo-inner'); + + $el.removeClass('is-loading') + .enable() + .attr('aria-label', $el.data(`${attrPrefix}-text`)) + .attr('data-delete-path', deletePath) + .attr('title', $el.data(`${attrPrefix}-text`)); + + if ($el.hasClass('has-tooltip')) { + $el.tooltip('fixTitle'); } - Sidebar.prototype.removeListeners = function () { - this.sidebar.off('click', '.sidebar-collapsed-icon'); - this.sidebar.off('hidden.gl.dropdown'); - $('.dropdown').off('loading.gl.dropdown'); - $('.dropdown').off('loaded.gl.dropdown'); - $(document).off('click', '.js-sidebar-toggle'); - }; - - Sidebar.prototype.addEventListeners = function() { - const $document = $(document); - - this.sidebar.on('click', '.sidebar-collapsed-icon', this, this.sidebarCollapseClicked); - this.sidebar.on('hidden.gl.dropdown', this, this.onSidebarDropdownHidden); - $('.dropdown').on('loading.gl.dropdown', this.sidebarDropdownLoading); - $('.dropdown').on('loaded.gl.dropdown', this.sidebarDropdownLoaded); - - $document.on('click', '.js-sidebar-toggle', this.sidebarToggleClicked); - return $(document).off('click', '.js-issuable-todo').on('click', '.js-issuable-todo', this.toggleTodo); - }; - - Sidebar.prototype.sidebarToggleClicked = function (e, triggered) { - var $allGutterToggleIcons, $this, $thisIcon; - e.preventDefault(); - $this = $(this); - $thisIcon = $this.find('i'); - $allGutterToggleIcons = $('.js-sidebar-toggle i'); - if ($thisIcon.hasClass('fa-angle-double-right')) { - $allGutterToggleIcons.removeClass('fa-angle-double-right').addClass('fa-angle-double-left'); - $('aside.right-sidebar').removeClass('right-sidebar-expanded').addClass('right-sidebar-collapsed'); - $('.page-with-sidebar').removeClass('right-sidebar-expanded').addClass('right-sidebar-collapsed'); - } else { - $allGutterToggleIcons.removeClass('fa-angle-double-left').addClass('fa-angle-double-right'); - $('aside.right-sidebar').removeClass('right-sidebar-collapsed').addClass('right-sidebar-expanded'); - $('.page-with-sidebar').removeClass('right-sidebar-collapsed').addClass('right-sidebar-expanded'); - - if (gl.lazyLoader) gl.lazyLoader.loadCheck(); - } - if (!triggered) { - Cookies.set("collapsed_gutter", $('.right-sidebar').hasClass('right-sidebar-collapsed')); - } - }; - - Sidebar.prototype.toggleTodo = function(e) { - var $btnText, $this, $todoLoading, ajaxType, url; - $this = $(e.currentTarget); - ajaxType = $this.attr('data-delete-path') ? 'DELETE' : 'POST'; - if ($this.attr('data-delete-path')) { - url = "" + ($this.attr('data-delete-path')); - } else { - url = "" + ($this.data('url')); - } - - $this.tooltip('hide'); - - return $.ajax({ - url: url, - type: ajaxType, - dataType: 'json', - data: { - issuable_id: $this.data('issuable-id'), - issuable_type: $this.data('issuable-type') - }, - beforeSend: (function(_this) { - return function() { - $('.js-issuable-todo').disable() - .addClass('is-loading'); - }; - })(this) - }).done((function(_this) { - return function(data) { - return _this.todoUpdateDone(data); - }; - })(this)); - }; - - Sidebar.prototype.todoUpdateDone = function(data) { - const deletePath = data.delete_path ? data.delete_path : null; - const attrPrefix = deletePath ? 'mark' : 'todo'; - const $todoBtns = $('.js-issuable-todo'); - - $(document).trigger('todo:toggle', data.count); - - $todoBtns.each((i, el) => { - const $el = $(el); - const $elText = $el.find('.js-issuable-todo-inner'); - - $el.removeClass('is-loading') - .enable() - .attr('aria-label', $el.data(`${attrPrefix}-text`)) - .attr('data-delete-path', deletePath) - .attr('title', $el.data(`${attrPrefix}-text`)); - - if ($el.hasClass('has-tooltip')) { - $el.tooltip('fixTitle'); - } - - if ($el.data(`${attrPrefix}-icon`)) { - $elText.html($el.data(`${attrPrefix}-icon`)); - } else { - $elText.text($el.data(`${attrPrefix}-text`)); - } - }); - }; - - Sidebar.prototype.sidebarDropdownLoading = function(e) { - var $loading, $sidebarCollapsedIcon, i, img; - $sidebarCollapsedIcon = $(this).closest('.block').find('.sidebar-collapsed-icon'); - img = $sidebarCollapsedIcon.find('img'); - i = $sidebarCollapsedIcon.find('i'); - $loading = $('<i class="fa fa-spinner fa-spin"></i>'); - if (img.length) { - img.before($loading); - return img.hide(); - } else if (i.length) { - i.before($loading); - return i.hide(); - } - }; - - Sidebar.prototype.sidebarDropdownLoaded = function(e) { - var $sidebarCollapsedIcon, i, img; - $sidebarCollapsedIcon = $(this).closest('.block').find('.sidebar-collapsed-icon'); - img = $sidebarCollapsedIcon.find('img'); - $sidebarCollapsedIcon.find('i.fa-spin').remove(); - i = $sidebarCollapsedIcon.find('i'); - if (img.length) { - return img.show(); - } else { - return i.show(); - } - }; - - Sidebar.prototype.sidebarCollapseClicked = function(e) { - var $block, sidebar; - if ($(e.currentTarget).hasClass('dont-change-state')) { - return; - } - sidebar = e.data; - e.preventDefault(); - $block = $(this).closest('.block'); - return sidebar.openDropdown($block); - }; - - Sidebar.prototype.openDropdown = function(blockOrName) { - var $block; - $block = _.isString(blockOrName) ? this.getBlock(blockOrName) : blockOrName; - if (!this.isOpen()) { - this.setCollapseAfterUpdate($block); - this.toggleSidebar('open'); - } - - // Wait for the sidebar to trigger('click') open - // so it doesn't cause our dropdown to close preemptively - setTimeout(() => { - $block.find('.js-sidebar-dropdown-toggle').trigger('click'); - }); - }; - - Sidebar.prototype.setCollapseAfterUpdate = function($block) { - $block.addClass('collapse-after-update'); - return $('.page-with-sidebar').addClass('with-overlay'); - }; - - Sidebar.prototype.onSidebarDropdownHidden = function(e) { - var $block, sidebar; - sidebar = e.data; - e.preventDefault(); - $block = $(e.target).closest('.block'); - return sidebar.sidebarDropdownHidden($block); - }; - - Sidebar.prototype.sidebarDropdownHidden = function($block) { - if ($block.hasClass('collapse-after-update')) { - $block.removeClass('collapse-after-update'); - $('.page-with-sidebar').removeClass('with-overlay'); - return this.toggleSidebar('hide'); - } - }; - - Sidebar.prototype.triggerOpenSidebar = function() { - return this.sidebar.find('.js-sidebar-toggle').trigger('click'); - }; - - Sidebar.prototype.toggleSidebar = function(action) { - if (action == null) { - action = 'toggle'; - } - if (action === 'toggle') { - this.triggerOpenSidebar(); - } - if (action === 'open') { - if (!this.isOpen()) { - this.triggerOpenSidebar(); - } - } - if (action === 'hide') { - if (this.isOpen()) { - return this.triggerOpenSidebar(); - } - } - }; + if ($el.data(`${attrPrefix}-icon`)) { + $elText.html($el.data(`${attrPrefix}-icon`)); + } else { + $elText.text($el.data(`${attrPrefix}-text`)); + } + }); +}; + +Sidebar.prototype.sidebarDropdownLoading = function(e) { + var $loading, $sidebarCollapsedIcon, i, img; + $sidebarCollapsedIcon = $(this).closest('.block').find('.sidebar-collapsed-icon'); + img = $sidebarCollapsedIcon.find('img'); + i = $sidebarCollapsedIcon.find('i'); + $loading = $('<i class="fa fa-spinner fa-spin"></i>'); + if (img.length) { + img.before($loading); + return img.hide(); + } else if (i.length) { + i.before($loading); + return i.hide(); + } +}; + +Sidebar.prototype.sidebarDropdownLoaded = function(e) { + var $sidebarCollapsedIcon, i, img; + $sidebarCollapsedIcon = $(this).closest('.block').find('.sidebar-collapsed-icon'); + img = $sidebarCollapsedIcon.find('img'); + $sidebarCollapsedIcon.find('i.fa-spin').remove(); + i = $sidebarCollapsedIcon.find('i'); + if (img.length) { + return img.show(); + } else { + return i.show(); + } +}; + +Sidebar.prototype.sidebarCollapseClicked = function(e) { + var $block, sidebar; + if ($(e.currentTarget).hasClass('dont-change-state')) { + return; + } + sidebar = e.data; + e.preventDefault(); + $block = $(this).closest('.block'); + return sidebar.openDropdown($block); +}; + +Sidebar.prototype.openDropdown = function(blockOrName) { + var $block; + $block = _.isString(blockOrName) ? this.getBlock(blockOrName) : blockOrName; + if (!this.isOpen()) { + this.setCollapseAfterUpdate($block); + this.toggleSidebar('open'); + } + + // Wait for the sidebar to trigger('click') open + // so it doesn't cause our dropdown to close preemptively + setTimeout(() => { + $block.find('.js-sidebar-dropdown-toggle').trigger('click'); + }); +}; + +Sidebar.prototype.setCollapseAfterUpdate = function($block) { + $block.addClass('collapse-after-update'); + return $('.layout-page').addClass('with-overlay'); +}; + +Sidebar.prototype.onSidebarDropdownHidden = function(e) { + var $block, sidebar; + sidebar = e.data; + e.preventDefault(); + $block = $(e.target).closest('.block'); + return sidebar.sidebarDropdownHidden($block); +}; + +Sidebar.prototype.sidebarDropdownHidden = function($block) { + if ($block.hasClass('collapse-after-update')) { + $block.removeClass('collapse-after-update'); + $('.layout-page').removeClass('with-overlay'); + return this.toggleSidebar('hide'); + } +}; + +Sidebar.prototype.triggerOpenSidebar = function() { + return this.sidebar.find('.js-sidebar-toggle').trigger('click'); +}; + +Sidebar.prototype.toggleSidebar = function(action) { + if (action == null) { + action = 'toggle'; + } + if (action === 'toggle') { + this.triggerOpenSidebar(); + } + if (action === 'open') { + if (!this.isOpen()) { + this.triggerOpenSidebar(); + } + } + if (action === 'hide') { + if (this.isOpen()) { + return this.triggerOpenSidebar(); + } + } +}; - Sidebar.prototype.isOpen = function() { - return this.sidebar.is('.right-sidebar-expanded'); - }; +Sidebar.prototype.isOpen = function() { + return this.sidebar.is('.right-sidebar-expanded'); +}; - Sidebar.prototype.getBlock = function(name) { - return this.sidebar.find(".block." + name); - }; +Sidebar.prototype.getBlock = function(name) { + return this.sidebar.find(".block." + name); +}; - return Sidebar; - })(); -}).call(window); +export default Sidebar; diff --git a/app/assets/javascripts/search.js b/app/assets/javascripts/search.js index 07fee53d814..363322af47a 100644 --- a/app/assets/javascripts/search.js +++ b/app/assets/javascripts/search.js @@ -1,118 +1,113 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, one-var-declaration-per-line, object-shorthand, prefer-arrow-callback, comma-dangle, prefer-template, quotes, no-else-return, max-len */ import Flash from './flash'; import Api from './api'; -(function() { - this.Search = (function() { - function Search() { - var $groupDropdown, $projectDropdown; - $groupDropdown = $('.js-search-group-dropdown'); - $projectDropdown = $('.js-search-project-dropdown'); - this.groupId = $groupDropdown.data('group-id'); - this.eventListeners(); - $groupDropdown.glDropdown({ - selectable: true, - filterable: true, - fieldName: 'group_id', - search: { - fields: ['full_name'] - }, - data: function(term, callback) { - return Api.groups(term, {}, function(data) { +export default class Search { + constructor() { + const $groupDropdown = $('.js-search-group-dropdown'); + const $projectDropdown = $('.js-search-project-dropdown'); + + this.searchInput = '.js-search-input'; + this.searchClear = '.js-search-clear'; + + this.groupId = $groupDropdown.data('group-id'); + this.eventListeners(); + + $groupDropdown.glDropdown({ + selectable: true, + filterable: true, + fieldName: 'group_id', + search: { + fields: ['full_name'], + }, + data(term, callback) { + return Api.groups(term, {}, (data) => { + data.unshift({ + full_name: 'Any', + }); + data.splice(1, 0, 'divider'); + return callback(data); + }); + }, + id(obj) { + return obj.id; + }, + text(obj) { + return obj.full_name; + }, + toggleLabel(obj) { + return `${($groupDropdown.data('default-label'))} ${obj.full_name}`; + }, + clicked: () => Search.submitSearch(), + }); + + $projectDropdown.glDropdown({ + selectable: true, + filterable: true, + fieldName: 'project_id', + search: { + fields: ['name'], + }, + data: (term, callback) => { + this.getProjectsData(term) + .then((data) => { data.unshift({ - full_name: 'Any' + name_with_namespace: 'Any', }); data.splice(1, 0, 'divider'); - return callback(data); - }); - }, - id: function(obj) { - return obj.id; - }, - text: function(obj) { - return obj.full_name; - }, - toggleLabel: function(obj) { - return ($groupDropdown.data('default-label')) + " " + obj.full_name; - }, - clicked: (function(_this) { - return function() { - return _this.submitSearch(); - }; - })(this) - }); - $projectDropdown.glDropdown({ - selectable: true, - filterable: true, - fieldName: 'project_id', - search: { - fields: ['name'] - }, - data: (term, callback) => { - this.getProjectsData(term) - .then((data) => { - data.unshift({ - name_with_namespace: 'Any' - }); - data.splice(1, 0, 'divider'); - return data; - }) - .then(data => callback(data)) - .catch(() => new Flash('Error fetching projects')); - }, - id: function(obj) { - return obj.id; - }, - text: function(obj) { - return obj.name_with_namespace; - }, - toggleLabel: function(obj) { - return ($projectDropdown.data('default-label')) + " " + obj.name_with_namespace; - }, - clicked: (function(_this) { - return function() { - return _this.submitSearch(); - }; - })(this) - }); - } + return data; + }) + .then(data => callback(data)) + .catch(() => new Flash('Error fetching projects')); + }, + id(obj) { + return obj.id; + }, + text(obj) { + return obj.name_with_namespace; + }, + toggleLabel(obj) { + return `${($projectDropdown.data('default-label'))} ${obj.name_with_namespace}`; + }, + clicked: () => Search.submitSearch(), + }); + } - Search.prototype.eventListeners = function() { - $(document).off('keyup', '.js-search-input').on('keyup', '.js-search-input', this.searchKeyUp); - return $(document).off('click', '.js-search-clear').on('click', '.js-search-clear', this.clearSearchField); - }; + eventListeners() { + $(document) + .off('keyup', this.searchInput) + .on('keyup', this.searchInput, this.searchKeyUp); + $(document) + .off('click', this.searchClear) + .on('click', this.searchClear, this.clearSearchField.bind(this)); + } - Search.prototype.submitSearch = function() { - return $('.js-search-form').submit(); - }; + static submitSearch() { + return $('.js-search-form').submit(); + } - Search.prototype.searchKeyUp = function() { - var $input; - $input = $(this); - if ($input.val() === '') { - return $('.js-search-clear').addClass('hidden'); - } else { - return $('.js-search-clear').removeClass('hidden'); - } - }; - - Search.prototype.clearSearchField = function() { - return $('.js-search-input').val('').trigger('keyup').focus(); - }; + searchKeyUp() { + const $input = $(this); + if ($input.val() === '') { + $('.js-search-clear').addClass('hidden'); + } else { + $('.js-search-clear').removeClass('hidden'); + } + } - Search.prototype.getProjectsData = function(term) { - return new Promise((resolve) => { - if (this.groupId) { - Api.groupProjects(this.groupId, term, resolve); - } else { - Api.projects(term, { - order_by: 'id', - }, resolve); - } - }); - }; + clearSearchField() { + return $(this.searchInput).val('').trigger('keyup').focus(); + } - return Search; - })(); -}).call(window); + getProjectsData(term) { + return new Promise((resolve) => { + if (this.groupId) { + Api.groupProjects(this.groupId, term, resolve); + } else { + Api.projects(term, { + order_by: 'id', + }, resolve); + } + }); + } +} diff --git a/app/assets/javascripts/search_autocomplete.js b/app/assets/javascripts/search_autocomplete.js index e40a3596200..98b524f7e3f 100644 --- a/app/assets/javascripts/search_autocomplete.js +++ b/app/assets/javascripts/search_autocomplete.js @@ -8,448 +8,445 @@ import { isInGroupsPage, isInProjectPage, getGroupSlug, getProjectSlug } from '. * When the user clicks `x` button it cleans the input and closes the dropdown. */ -((global) => { - const KEYCODE = { - ESCAPE: 27, - BACKSPACE: 8, - ENTER: 13, - UP: 38, - DOWN: 40, - }; - - class SearchAutocomplete { - constructor({ wrap, optsEl, autocompletePath, projectId, projectRef } = {}) { - this.bindEventContext(); - this.wrap = wrap || $('.search'); - this.optsEl = optsEl || this.wrap.find('.search-autocomplete-opts'); - this.autocompletePath = autocompletePath || this.optsEl.data('autocomplete-path'); - this.projectId = projectId || (this.optsEl.data('autocomplete-project-id') || ''); - this.projectRef = projectRef || (this.optsEl.data('autocomplete-project-ref') || ''); - this.dropdown = this.wrap.find('.dropdown'); - this.dropdownToggle = this.wrap.find('.js-dropdown-search-toggle'); - this.dropdownContent = this.dropdown.find('.dropdown-content'); - this.locationBadgeEl = this.getElement('.location-badge'); - this.scopeInputEl = this.getElement('#scope'); - this.searchInput = this.getElement('.search-input'); - this.projectInputEl = this.getElement('#search_project_id'); - this.groupInputEl = this.getElement('#group_id'); - this.searchCodeInputEl = this.getElement('#search_code'); - this.repositoryInputEl = this.getElement('#repository_ref'); - this.clearInput = this.getElement('.js-clear-input'); - this.saveOriginalState(); - - // Only when user is logged in - if (gon.current_user_id) { - this.createAutocomplete(); - } +const KEYCODE = { + ESCAPE: 27, + BACKSPACE: 8, + ENTER: 13, + UP: 38, + DOWN: 40, +}; + +function setSearchOptions() { + var $projectOptionsDataEl = $('.js-search-project-options'); + var $groupOptionsDataEl = $('.js-search-group-options'); + var $dashboardOptionsDataEl = $('.js-search-dashboard-options'); + + if ($projectOptionsDataEl.length) { + gl.projectOptions = gl.projectOptions || {}; + + var projectPath = $projectOptionsDataEl.data('project-path'); + + gl.projectOptions[projectPath] = { + name: $projectOptionsDataEl.data('name'), + issuesPath: $projectOptionsDataEl.data('issues-path'), + issuesDisabled: $projectOptionsDataEl.data('issues-disabled'), + mrPath: $projectOptionsDataEl.data('mr-path'), + }; + } - this.searchInput.addClass('disabled'); - this.saveTextLength(); - this.bindEvents(); - this.dropdownToggle.dropdown(); - } + if ($groupOptionsDataEl.length) { + gl.groupOptions = gl.groupOptions || {}; - // Finds an element inside wrapper element - bindEventContext() { - this.onSearchInputBlur = this.onSearchInputBlur.bind(this); - this.onClearInputClick = this.onClearInputClick.bind(this); - this.onSearchInputFocus = this.onSearchInputFocus.bind(this); - this.onSearchInputKeyUp = this.onSearchInputKeyUp.bind(this); - this.onSearchInputKeyDown = this.onSearchInputKeyDown.bind(this); - } - getElement(selector) { - return this.wrap.find(selector); - } + var groupPath = $groupOptionsDataEl.data('group-path'); - saveOriginalState() { - return this.originalState = this.serializeState(); - } + gl.groupOptions[groupPath] = { + name: $groupOptionsDataEl.data('name'), + issuesPath: $groupOptionsDataEl.data('issues-path'), + mrPath: $groupOptionsDataEl.data('mr-path'), + }; + } - saveTextLength() { - return this.lastTextLength = this.searchInput.val().length; + if ($dashboardOptionsDataEl.length) { + gl.dashboardOptions = { + issuesPath: $dashboardOptionsDataEl.data('issues-path'), + mrPath: $dashboardOptionsDataEl.data('mr-path'), + }; + } +} + +export default class SearchAutocomplete { + constructor({ wrap, optsEl, autocompletePath, projectId, projectRef } = {}) { + setSearchOptions(); + this.bindEventContext(); + this.wrap = wrap || $('.search'); + this.optsEl = optsEl || this.wrap.find('.search-autocomplete-opts'); + this.autocompletePath = autocompletePath || this.optsEl.data('autocomplete-path'); + this.projectId = projectId || (this.optsEl.data('autocomplete-project-id') || ''); + this.projectRef = projectRef || (this.optsEl.data('autocomplete-project-ref') || ''); + this.dropdown = this.wrap.find('.dropdown'); + this.dropdownToggle = this.wrap.find('.js-dropdown-search-toggle'); + this.dropdownContent = this.dropdown.find('.dropdown-content'); + this.locationBadgeEl = this.getElement('.location-badge'); + this.scopeInputEl = this.getElement('#scope'); + this.searchInput = this.getElement('.search-input'); + this.projectInputEl = this.getElement('#search_project_id'); + this.groupInputEl = this.getElement('#group_id'); + this.searchCodeInputEl = this.getElement('#search_code'); + this.repositoryInputEl = this.getElement('#repository_ref'); + this.clearInput = this.getElement('.js-clear-input'); + this.saveOriginalState(); + + // Only when user is logged in + if (gon.current_user_id) { + this.createAutocomplete(); } - createAutocomplete() { - return this.searchInput.glDropdown({ - filterInputBlur: false, - filterable: true, - filterRemote: true, - highlight: true, - enterCallback: false, - filterInput: 'input#search', - search: { - fields: ['text'], - }, - id: this.getSearchText, - data: this.getData.bind(this), - selectable: true, - clicked: this.onClick.bind(this), - }); + this.searchInput.addClass('disabled'); + this.saveTextLength(); + this.bindEvents(); + this.dropdownToggle.dropdown(); + } + + // Finds an element inside wrapper element + bindEventContext() { + this.onSearchInputBlur = this.onSearchInputBlur.bind(this); + this.onClearInputClick = this.onClearInputClick.bind(this); + this.onSearchInputFocus = this.onSearchInputFocus.bind(this); + this.onSearchInputKeyUp = this.onSearchInputKeyUp.bind(this); + this.onSearchInputKeyDown = this.onSearchInputKeyDown.bind(this); + } + getElement(selector) { + return this.wrap.find(selector); + } + + saveOriginalState() { + return this.originalState = this.serializeState(); + } + + saveTextLength() { + return this.lastTextLength = this.searchInput.val().length; + } + + createAutocomplete() { + return this.searchInput.glDropdown({ + filterInputBlur: false, + filterable: true, + filterRemote: true, + highlight: true, + enterCallback: false, + filterInput: 'input#search', + search: { + fields: ['text'], + }, + id: this.getSearchText, + data: this.getData.bind(this), + selectable: true, + clicked: this.onClick.bind(this), + }); + } + + getSearchText(selectedObject, el) { + return selectedObject.id ? selectedObject.text : ''; + } + + getData(term, callback) { + if (!term) { + const contents = this.getCategoryContents(); + if (contents) { + this.searchInput.data('glDropdown').filter.options.callback(contents); + this.enableAutocomplete(); + } + return; } - getSearchText(selectedObject, el) { - return selectedObject.id ? selectedObject.text : ''; + // Prevent multiple ajax calls + if (this.loadingSuggestions) { + return; } - getData(term, callback) { - if (!term) { - const contents = this.getCategoryContents(); - if (contents) { - this.searchInput.data('glDropdown').filter.options.callback(contents); - this.enableAutocomplete(); - } - return; - } + this.loadingSuggestions = true; - // Prevent multiple ajax calls - if (this.loadingSuggestions) { + return $.get(this.autocompletePath, { + project_id: this.projectId, + project_ref: this.projectRef, + term: term, + }, (response) => { + var firstCategory, i, lastCategory, len, suggestion; + // Hide dropdown menu if no suggestions returns + if (!response.length) { + this.disableAutocomplete(); return; } - this.loadingSuggestions = true; - - return $.get(this.autocompletePath, { - project_id: this.projectId, - project_ref: this.projectRef, - term: term, - }, (response) => { - var firstCategory, i, lastCategory, len, suggestion; - // Hide dropdown menu if no suggestions returns - if (!response.length) { - this.disableAutocomplete(); - return; - } - - const data = []; - // List results - firstCategory = true; - for (i = 0, len = response.length; i < len; i += 1) { - suggestion = response[i]; - // Add group header before list each group - if (lastCategory !== suggestion.category) { - if (!firstCategory) { - data.push('separator'); - } - if (firstCategory) { - firstCategory = false; - } - data.push({ - header: suggestion.category, - }); - lastCategory = suggestion.category; + const data = []; + // List results + firstCategory = true; + for (i = 0, len = response.length; i < len; i += 1) { + suggestion = response[i]; + // Add group header before list each group + if (lastCategory !== suggestion.category) { + if (!firstCategory) { + data.push('separator'); + } + if (firstCategory) { + firstCategory = false; } data.push({ - id: (suggestion.category.toLowerCase()) + "-" + suggestion.id, - category: suggestion.category, - text: suggestion.label, - url: suggestion.url, - }); - } - // Add option to proceed with the search - if (data.length) { - data.push('separator'); - data.push({ - text: "Result name contains \"" + term + "\"", - url: "/search?search=" + term + "&project_id=" + (this.projectInputEl.val()) + "&group_id=" + (this.groupInputEl.val()), + header: suggestion.category, }); + lastCategory = suggestion.category; } - return callback(data); - }) - .always(() => { this.loadingSuggestions = false; }); - } - - getCategoryContents() { - const userId = gon.current_user_id; - const userName = gon.current_username; - const { projectOptions, groupOptions, dashboardOptions } = gl; - - // Get options - let options; - if (isInGroupsPage() && groupOptions) { - options = groupOptions[getGroupSlug()]; - } else if (isInProjectPage() && projectOptions) { - options = projectOptions[getProjectSlug()]; - } else if (dashboardOptions) { - options = dashboardOptions; + data.push({ + id: (suggestion.category.toLowerCase()) + "-" + suggestion.id, + category: suggestion.category, + text: suggestion.label, + url: suggestion.url, + }); } - - const { issuesPath, mrPath, name, issuesDisabled } = options; - const baseItems = []; - - if (name) { - baseItems.push({ - header: `${name}`, + // Add option to proceed with the search + if (data.length) { + data.push('separator'); + data.push({ + text: "Result name contains \"" + term + "\"", + url: "/search?search=" + term + "&project_id=" + (this.projectInputEl.val()) + "&group_id=" + (this.groupInputEl.val()), }); } + return callback(data); + }) + .always(() => { this.loadingSuggestions = false; }); + } - const issueItems = [ - { - text: 'Issues assigned to me', - url: `${issuesPath}/?assignee_username=${userName}`, - }, - { - text: "Issues I've created", - url: `${issuesPath}/?author_username=${userName}`, - }, - ]; - const mergeRequestItems = [ - { - text: 'Merge requests assigned to me', - url: `${mrPath}/?assignee_username=${userName}`, - }, - { - text: "Merge requests I've created", - url: `${mrPath}/?author_username=${userName}`, - }, - ]; - - let items; - if (issuesDisabled) { - items = baseItems.concat(mergeRequestItems); - } else { - items = baseItems.concat(...issueItems, 'separator', ...mergeRequestItems); - } - return items; + getCategoryContents() { + const userId = gon.current_user_id; + const userName = gon.current_username; + const { projectOptions, groupOptions, dashboardOptions } = gl; + + // Get options + let options; + if (isInGroupsPage() && groupOptions) { + options = groupOptions[getGroupSlug()]; + } else if (isInProjectPage() && projectOptions) { + options = projectOptions[getProjectSlug()]; + } else if (dashboardOptions) { + options = dashboardOptions; } - serializeState() { - return { - // Search Criteria - search_project_id: this.projectInputEl.val(), - group_id: this.groupInputEl.val(), - search_code: this.searchCodeInputEl.val(), - repository_ref: this.repositoryInputEl.val(), - scope: this.scopeInputEl.val(), - // Location badge - _location: this.locationBadgeEl.text(), - }; + const { issuesPath, mrPath, name, issuesDisabled } = options; + const baseItems = []; + + if (name) { + baseItems.push({ + header: `${name}`, + }); } - bindEvents() { - this.searchInput.on('keydown', this.onSearchInputKeyDown); - this.searchInput.on('keyup', this.onSearchInputKeyUp); - this.searchInput.on('focus', this.onSearchInputFocus); - this.searchInput.on('blur', this.onSearchInputBlur); - this.clearInput.on('click', this.onClearInputClick); - this.locationBadgeEl.on('click', () => this.searchInput.focus()); + const issueItems = [ + { + text: 'Issues assigned to me', + url: `${issuesPath}/?assignee_username=${userName}`, + }, + { + text: "Issues I've created", + url: `${issuesPath}/?author_username=${userName}`, + }, + ]; + const mergeRequestItems = [ + { + text: 'Merge requests assigned to me', + url: `${mrPath}/?assignee_username=${userName}`, + }, + { + text: "Merge requests I've created", + url: `${mrPath}/?author_username=${userName}`, + }, + ]; + + let items; + if (issuesDisabled) { + items = baseItems.concat(mergeRequestItems); + } else { + items = baseItems.concat(...issueItems, 'separator', ...mergeRequestItems); } + return items; + } - enableAutocomplete() { - // No need to enable anything if user is not logged in - if (!gon.current_user_id) { - return; - } + serializeState() { + return { + // Search Criteria + search_project_id: this.projectInputEl.val(), + group_id: this.groupInputEl.val(), + search_code: this.searchCodeInputEl.val(), + repository_ref: this.repositoryInputEl.val(), + scope: this.scopeInputEl.val(), + // Location badge + _location: this.locationBadgeEl.text(), + }; + } - // If the dropdown is closed, we'll open it - if (!this.dropdown.hasClass('open')) { - this.loadingSuggestions = false; - this.dropdownToggle.dropdown('toggle'); - return this.searchInput.removeClass('disabled'); - } + bindEvents() { + this.searchInput.on('keydown', this.onSearchInputKeyDown); + this.searchInput.on('keyup', this.onSearchInputKeyUp); + this.searchInput.on('focus', this.onSearchInputFocus); + this.searchInput.on('blur', this.onSearchInputBlur); + this.clearInput.on('click', this.onClearInputClick); + this.locationBadgeEl.on('click', () => this.searchInput.focus()); + } + + enableAutocomplete() { + // No need to enable anything if user is not logged in + if (!gon.current_user_id) { + return; } - // Saves last length of the entered text - onSearchInputKeyDown() { - return this.saveTextLength(); + // If the dropdown is closed, we'll open it + if (!this.dropdown.hasClass('open')) { + this.loadingSuggestions = false; + this.dropdownToggle.dropdown('toggle'); + return this.searchInput.removeClass('disabled'); } + } - onSearchInputKeyUp(e) { - switch (e.keyCode) { - case KEYCODE.BACKSPACE: - // when trying to remove the location badge - if (this.lastTextLength === 0 && this.badgePresent()) { - this.removeLocationBadge(); - } - // When removing the last character and no badge is present - if (this.lastTextLength === 1) { - this.disableAutocomplete(); - } - // When removing any character from existin value - if (this.lastTextLength > 1) { - this.enableAutocomplete(); - } - break; - case KEYCODE.ESCAPE: - this.restoreOriginalState(); - break; - case KEYCODE.ENTER: + // Saves last length of the entered text + onSearchInputKeyDown() { + return this.saveTextLength(); + } + + onSearchInputKeyUp(e) { + switch (e.keyCode) { + case KEYCODE.BACKSPACE: + // when trying to remove the location badge + if (this.lastTextLength === 0 && this.badgePresent()) { + this.removeLocationBadge(); + } + // When removing the last character and no badge is present + if (this.lastTextLength === 1) { + this.disableAutocomplete(); + } + // When removing any character from existin value + if (this.lastTextLength > 1) { + this.enableAutocomplete(); + } + break; + case KEYCODE.ESCAPE: + this.restoreOriginalState(); + break; + case KEYCODE.ENTER: + this.disableAutocomplete(); + break; + case KEYCODE.UP: + case KEYCODE.DOWN: + return; + default: + // Handle the case when deleting the input value other than backspace + // e.g. Pressing ctrl + backspace or ctrl + x + if (this.searchInput.val() === '') { this.disableAutocomplete(); - break; - case KEYCODE.UP: - case KEYCODE.DOWN: - return; - default: - // Handle the case when deleting the input value other than backspace - // e.g. Pressing ctrl + backspace or ctrl + x - if (this.searchInput.val() === '') { - this.disableAutocomplete(); - } else { - // We should display the menu only when input is not empty - if (e.keyCode !== KEYCODE.ENTER) { - this.enableAutocomplete(); - } + } else { + // We should display the menu only when input is not empty + if (e.keyCode !== KEYCODE.ENTER) { + this.enableAutocomplete(); } - } - this.wrap.toggleClass('has-value', !!e.target.value); + } } + this.wrap.toggleClass('has-value', !!e.target.value); + } - onSearchInputFocus() { - this.isFocused = true; - this.wrap.addClass('search-active'); - if (this.getValue() === '') { - return this.getData(); - } + onSearchInputFocus() { + this.isFocused = true; + this.wrap.addClass('search-active'); + if (this.getValue() === '') { + return this.getData(); } + } - getValue() { - return this.searchInput.val(); - } + getValue() { + return this.searchInput.val(); + } - onClearInputClick(e) { - e.preventDefault(); - this.wrap.toggleClass('has-value', !!e.target.value); - return this.searchInput.val('').focus(); - } + onClearInputClick(e) { + e.preventDefault(); + this.wrap.toggleClass('has-value', !!e.target.value); + return this.searchInput.val('').focus(); + } - onSearchInputBlur(e) { - this.isFocused = false; - this.wrap.removeClass('search-active'); - // If input is blank then restore state - if (this.searchInput.val() === '') { - return this.restoreOriginalState(); - } + onSearchInputBlur(e) { + this.isFocused = false; + this.wrap.removeClass('search-active'); + // If input is blank then restore state + if (this.searchInput.val() === '') { + return this.restoreOriginalState(); } + } - addLocationBadge(item) { - var badgeText, category, value; - category = item.category != null ? item.category + ": " : ''; - value = item.value != null ? item.value : ''; - badgeText = "" + category + value; - this.locationBadgeEl.text(badgeText).show(); - return this.wrap.addClass('has-location-badge'); - } + addLocationBadge(item) { + var badgeText, category, value; + category = item.category != null ? item.category + ": " : ''; + value = item.value != null ? item.value : ''; + badgeText = "" + category + value; + this.locationBadgeEl.text(badgeText).show(); + return this.wrap.addClass('has-location-badge'); + } - hasLocationBadge() { - return this.wrap.is('.has-location-badge'); - } + hasLocationBadge() { + return this.wrap.is('.has-location-badge'); + } - restoreOriginalState() { - var i, input, inputs, len; - inputs = Object.keys(this.originalState); - for (i = 0, len = inputs.length; i < len; i += 1) { - input = inputs[i]; - this.getElement("#" + input).val(this.originalState[input]); - } - if (this.originalState._location === '') { - return this.locationBadgeEl.hide(); - } else { - return this.addLocationBadge({ - value: this.originalState._location, - }); - } + restoreOriginalState() { + var i, input, inputs, len; + inputs = Object.keys(this.originalState); + for (i = 0, len = inputs.length; i < len; i += 1) { + input = inputs[i]; + this.getElement("#" + input).val(this.originalState[input]); } - - badgePresent() { - return this.locationBadgeEl.length; + if (this.originalState._location === '') { + return this.locationBadgeEl.hide(); + } else { + return this.addLocationBadge({ + value: this.originalState._location, + }); } + } - resetSearchState() { - var i, input, inputs, len, results; - inputs = Object.keys(this.originalState); - results = []; - for (i = 0, len = inputs.length; i < len; i += 1) { - input = inputs[i]; - // _location isnt a input - if (input === '_location') { - break; - } - results.push(this.getElement("#" + input).val('')); + badgePresent() { + return this.locationBadgeEl.length; + } + + resetSearchState() { + var i, input, inputs, len, results; + inputs = Object.keys(this.originalState); + results = []; + for (i = 0, len = inputs.length; i < len; i += 1) { + input = inputs[i]; + // _location isnt a input + if (input === '_location') { + break; } - return results; + results.push(this.getElement("#" + input).val('')); } + return results; + } - removeLocationBadge() { - this.locationBadgeEl.hide(); - this.resetSearchState(); - this.wrap.removeClass('has-location-badge'); - return this.disableAutocomplete(); - } + removeLocationBadge() { + this.locationBadgeEl.hide(); + this.resetSearchState(); + this.wrap.removeClass('has-location-badge'); + return this.disableAutocomplete(); + } - disableAutocomplete() { - if (!this.searchInput.hasClass('disabled') && this.dropdown.hasClass('open')) { - this.searchInput.addClass('disabled'); - this.dropdown.removeClass('open').trigger('hidden.bs.dropdown'); - this.restoreMenu(); - } + disableAutocomplete() { + if (!this.searchInput.hasClass('disabled') && this.dropdown.hasClass('open')) { + this.searchInput.addClass('disabled'); + this.dropdown.removeClass('open').trigger('hidden.bs.dropdown'); + this.restoreMenu(); } + } - restoreMenu() { - var html; - html = '<ul><li class="dropdown-menu-empty-item"><a>Loading...</a></li></ul>'; - return this.dropdownContent.html(html); - } + restoreMenu() { + var html; + html = '<ul><li class="dropdown-menu-empty-item"><a>Loading...</a></li></ul>'; + return this.dropdownContent.html(html); + } - onClick(item, $el, e) { - if (location.pathname.indexOf(item.url) !== -1) { - if (!e.metaKey) e.preventDefault(); - if (!this.badgePresent) { - if (item.category === 'Projects') { - this.projectInputEl.val(item.id); - this.addLocationBadge({ - value: 'This project', - }); - } - if (item.category === 'Groups') { - this.groupInputEl.val(item.id); - this.addLocationBadge({ - value: 'This group', - }); - } + onClick(item, $el, e) { + if (location.pathname.indexOf(item.url) !== -1) { + if (!e.metaKey) e.preventDefault(); + if (!this.badgePresent) { + if (item.category === 'Projects') { + this.projectInputEl.val(item.id); + this.addLocationBadge({ + value: 'This project', + }); + } + if (item.category === 'Groups') { + this.groupInputEl.val(item.id); + this.addLocationBadge({ + value: 'This group', + }); } - $el.removeClass('is-active'); - this.disableAutocomplete(); - return this.searchInput.val('').focus(); } + $el.removeClass('is-active'); + this.disableAutocomplete(); + return this.searchInput.val('').focus(); } } - - global.SearchAutocomplete = SearchAutocomplete; - - $(function() { - var $projectOptionsDataEl = $('.js-search-project-options'); - var $groupOptionsDataEl = $('.js-search-group-options'); - var $dashboardOptionsDataEl = $('.js-search-dashboard-options'); - - if ($projectOptionsDataEl.length) { - gl.projectOptions = gl.projectOptions || {}; - - var projectPath = $projectOptionsDataEl.data('project-path'); - - gl.projectOptions[projectPath] = { - name: $projectOptionsDataEl.data('name'), - issuesPath: $projectOptionsDataEl.data('issues-path'), - issuesDisabled: $projectOptionsDataEl.data('issues-disabled'), - mrPath: $projectOptionsDataEl.data('mr-path'), - }; - } - - if ($groupOptionsDataEl.length) { - gl.groupOptions = gl.groupOptions || {}; - - var groupPath = $groupOptionsDataEl.data('group-path'); - - gl.groupOptions[groupPath] = { - name: $groupOptionsDataEl.data('name'), - issuesPath: $groupOptionsDataEl.data('issues-path'), - mrPath: $groupOptionsDataEl.data('mr-path'), - }; - } - - if ($dashboardOptionsDataEl.length) { - gl.dashboardOptions = { - issuesPath: $dashboardOptionsDataEl.data('issues-path'), - mrPath: $dashboardOptionsDataEl.data('mr-path'), - }; - } - }); -})(window.gl || (window.gl = {})); +} diff --git a/app/assets/javascripts/shortcuts.js b/app/assets/javascripts/shortcuts.js index ebe7a99ffae..130730b1700 100644 --- a/app/assets/javascripts/shortcuts.js +++ b/app/assets/javascripts/shortcuts.js @@ -1,5 +1,6 @@ import Cookies from 'js-cookie'; import Mousetrap from 'mousetrap'; +import { refreshCurrentPage, visitUrl } from './lib/utils/url_utility'; import findAndFollowLink from './shortcuts_dashboard_navigation'; const defaultStopCallback = Mousetrap.stopCallback; @@ -38,7 +39,7 @@ export default class Shortcuts { if (typeof findFileURL !== 'undefined' && findFileURL !== null) { Mousetrap.bind('t', () => { - gl.utils.visitUrl(findFileURL); + visitUrl(findFileURL); }); } @@ -62,7 +63,7 @@ export default class Shortcuts { } else { Cookies.set(performanceBarCookieName, 'true', { path: '/' }); } - gl.utils.refreshCurrentPage(); + refreshCurrentPage(); } static toggleMarkdownPreview(e) { diff --git a/app/assets/javascripts/shortcuts_blob.js b/app/assets/javascripts/shortcuts_blob.js index fbc57bb4304..cf309be4f6f 100644 --- a/app/assets/javascripts/shortcuts_blob.js +++ b/app/assets/javascripts/shortcuts_blob.js @@ -1,5 +1,5 @@ /* global Mousetrap */ - +import { getLocationHash, visitUrl } from './lib/utils/url_utility'; import Shortcuts from './shortcuts'; const defaults = { @@ -18,9 +18,9 @@ export default class ShortcutsBlob extends Shortcuts { moveToFilePermalink() { if (this.options.fileBlobPermalinkUrl) { - const hash = gl.utils.getLocationHash(); + const hash = getLocationHash(); const hashUrlString = hash ? `#${hash}` : ''; - gl.utils.visitUrl(`${this.options.fileBlobPermalinkUrl}${hashUrlString}`); + visitUrl(`${this.options.fileBlobPermalinkUrl}${hashUrlString}`); } } } diff --git a/app/assets/javascripts/shortcuts_issuable.js b/app/assets/javascripts/shortcuts_issuable.js index 4f4f606d293..292e3d6a657 100644 --- a/app/assets/javascripts/shortcuts_issuable.js +++ b/app/assets/javascripts/shortcuts_issuable.js @@ -1,8 +1,8 @@ /* global Mousetrap */ -/* global sidebar */ import _ from 'underscore'; import 'mousetrap'; +import Sidebar from './right_sidebar'; import ShortcutsNavigation from './shortcuts_navigation'; import { CopyAsGFM } from './behaviors/copy_as_gfm'; @@ -11,7 +11,7 @@ export default class ShortcutsIssuable extends ShortcutsNavigation { super(); this.$replyField = isMergeRequest ? $('.js-main-target-form #note_note') : $('.js-main-target-form .js-vue-comment-form'); - this.editBtn = document.querySelector('.issuable-edit'); + this.editBtn = document.querySelector('.js-issuable-edit'); Mousetrap.bind('a', () => ShortcutsIssuable.openSidebarDropdown('assignee')); Mousetrap.bind('m', () => ShortcutsIssuable.openSidebarDropdown('milestone')); @@ -69,7 +69,7 @@ export default class ShortcutsIssuable extends ShortcutsNavigation { } static openSidebarDropdown(name) { - sidebar.openDropdown(name); + Sidebar.instance.openDropdown(name); return false; } } diff --git a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.js b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.js index 74c17bc14a2..9e47039d920 100644 --- a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.js +++ b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.js @@ -1,22 +1,32 @@ import Flash from '../../../flash'; import AssigneeTitle from './assignee_title'; import Assignees from './assignees'; - import Store from '../../stores/sidebar_store'; -import Mediator from '../../sidebar_mediator'; - import eventHub from '../../event_hub'; export default { name: 'SidebarAssignees', data() { return { - mediator: new Mediator(), store: new Store(), loading: false, - field: '', }; }, + props: { + mediator: { + type: Object, + required: true, + }, + field: { + type: String, + required: true, + }, + signedIn: { + type: Boolean, + required: false, + default: false, + }, + }, components: { 'assignee-title': AssigneeTitle, assignees: Assignees, @@ -61,10 +71,6 @@ export default { eventHub.$off('sidebar.removeAllAssignees', this.removeAllAssignees); eventHub.$off('sidebar.saveAssignees', this.saveAssignees); }, - beforeMount() { - this.field = this.$el.dataset.field; - this.signedIn = typeof this.$el.dataset.signedIn !== 'undefined'; - }, template: ` <div> <assignee-title diff --git a/app/assets/javascripts/sidebar/components/participants/sidebar_participants.vue b/app/assets/javascripts/sidebar/components/participants/sidebar_participants.vue index c1296b28db7..6fcd2f95309 100644 --- a/app/assets/javascripts/sidebar/components/participants/sidebar_participants.vue +++ b/app/assets/javascripts/sidebar/components/participants/sidebar_participants.vue @@ -1,15 +1,19 @@ <script> import Store from '../../stores/sidebar_store'; -import Mediator from '../../sidebar_mediator'; import participants from './participants.vue'; export default { data() { return { - mediator: new Mediator(), store: new Store(), }; }, + props: { + mediator: { + type: Object, + required: true, + }, + }, components: { participants, }, @@ -21,6 +25,7 @@ export default { <participants :loading="store.isFetching.participants" :participants="store.participants" - :number-of-less-participants="7" /> + :number-of-less-participants="7" + /> </div> </template> diff --git a/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue b/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue index 25acc099699..f4bae1d3dd5 100644 --- a/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue +++ b/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions.vue @@ -1,6 +1,5 @@ <script> import Store from '../../stores/sidebar_store'; -import Mediator from '../../sidebar_mediator'; import eventHub from '../../event_hub'; import Flash from '../../../flash'; import { __ } from '../../../locale'; @@ -9,11 +8,15 @@ import subscriptions from './subscriptions.vue'; export default { data() { return { - mediator: new Mediator(), store: new Store(), }; }, - + props: { + mediator: { + type: Object, + required: true, + }, + }, components: { subscriptions, }, diff --git a/app/assets/javascripts/sidebar/mount_sidebar.js b/app/assets/javascripts/sidebar/mount_sidebar.js index 4032f156b15..56cc78ca0ca 100644 --- a/app/assets/javascripts/sidebar/mount_sidebar.js +++ b/app/assets/javascripts/sidebar/mount_sidebar.js @@ -10,6 +10,27 @@ import Translate from '../vue_shared/translate'; Vue.use(Translate); +function mountAssigneesComponent(mediator) { + const el = document.getElementById('js-vue-sidebar-assignees'); + + if (!el) return; + + // eslint-disable-next-line no-new + new Vue({ + el, + components: { + SidebarAssignees, + }, + render: createElement => createElement('sidebar-assignees', { + props: { + mediator, + field: el.dataset.field, + signedIn: el.hasAttribute('data-signed-in'), + }, + }), + }); +} + function mountConfidentialComponent(mediator) { const el = document.getElementById('js-confidential-entry-point'); @@ -49,9 +70,10 @@ function mountLockComponent(mediator) { }).$mount(el); } -function mountParticipantsComponent() { +function mountParticipantsComponent(mediator) { const el = document.querySelector('.js-sidebar-participants-entry-point'); + // eslint-disable-next-line no-new if (!el) return; // eslint-disable-next-line no-new @@ -60,11 +82,15 @@ function mountParticipantsComponent() { components: { sidebarParticipants, }, - render: createElement => createElement('sidebar-participants', {}), + render: createElement => createElement('sidebar-participants', { + props: { + mediator, + }, + }), }); } -function mountSubscriptionsComponent() { +function mountSubscriptionsComponent(mediator) { const el = document.querySelector('.js-sidebar-subscriptions-entry-point'); if (!el) return; @@ -75,22 +101,35 @@ function mountSubscriptionsComponent() { components: { sidebarSubscriptions, }, - render: createElement => createElement('sidebar-subscriptions', {}), + render: createElement => createElement('sidebar-subscriptions', { + props: { + mediator, + }, + }), }); } -function mount(mediator) { - const sidebarAssigneesEl = document.getElementById('js-vue-sidebar-assignees'); - // Only create the sidebarAssignees vue app if it is found in the DOM - // We currently do not use sidebarAssignees for the MR page - if (sidebarAssigneesEl) { - new Vue(SidebarAssignees).$mount(sidebarAssigneesEl); - } +function mountTimeTrackingComponent() { + const el = document.getElementById('issuable-time-tracker'); + if (!el) return; + + // eslint-disable-next-line no-new + new Vue({ + el, + components: { + SidebarTimeTracking, + }, + render: createElement => createElement('sidebar-time-tracking', {}), + }); +} + +export function mountSidebar(mediator) { + mountAssigneesComponent(mediator); mountConfidentialComponent(mediator); mountLockComponent(mediator); - mountParticipantsComponent(); - mountSubscriptionsComponent(); + mountParticipantsComponent(mediator); + mountSubscriptionsComponent(mediator); new SidebarMoveIssue( mediator, @@ -98,7 +137,9 @@ function mount(mediator) { $('.js-move-issue-confirmation-button'), ).init(); - new Vue(SidebarTimeTracking).$mount('#issuable-time-tracker'); + mountTimeTrackingComponent(); } -export default mount; +export function getSidebarOptions() { + return JSON.parse(document.querySelector('.js-sidebar-options').innerHTML); +} diff --git a/app/assets/javascripts/sidebar/sidebar_bundle.js b/app/assets/javascripts/sidebar/sidebar_bundle.js index f78287e504b..04c39d7b6b5 100644 --- a/app/assets/javascripts/sidebar/sidebar_bundle.js +++ b/app/assets/javascripts/sidebar/sidebar_bundle.js @@ -1,9 +1,8 @@ import Mediator from './sidebar_mediator'; -import mountSidebar from './mount_sidebar'; +import { mountSidebar, getSidebarOptions } from './mount_sidebar'; function domContentLoaded() { - const sidebarOptions = JSON.parse(document.querySelector('.js-sidebar-options').innerHTML); - const mediator = new Mediator(sidebarOptions); + const mediator = new Mediator(getSidebarOptions()); mediator.fetch(); mountSidebar(mediator); diff --git a/app/assets/javascripts/sidebar/sidebar_mediator.js b/app/assets/javascripts/sidebar/sidebar_mediator.js index d4c07a188b3..d86557e870a 100644 --- a/app/assets/javascripts/sidebar/sidebar_mediator.js +++ b/app/assets/javascripts/sidebar/sidebar_mediator.js @@ -1,3 +1,4 @@ +import { visitUrl } from '../lib/utils/url_utility'; import Flash from '../flash'; import Service from './services/sidebar_service'; import Store from './stores/sidebar_store'; @@ -7,7 +8,6 @@ export default class SidebarMediator { if (!SidebarMediator.singleton) { this.initSingleton(options); } - return SidebarMediator.singleton; } @@ -81,7 +81,7 @@ export default class SidebarMediator { .then(response => response.json()) .then((data) => { if (location.pathname !== data.web_url) { - gl.utils.visitUrl(data.web_url); + visitUrl(data.web_url); } }); } diff --git a/app/assets/javascripts/sidebar/stores/sidebar_store.js b/app/assets/javascripts/sidebar/stores/sidebar_store.js index 73eb25e2333..f20cc6d8cca 100644 --- a/app/assets/javascripts/sidebar/stores/sidebar_store.js +++ b/app/assets/javascripts/sidebar/stores/sidebar_store.js @@ -1,33 +1,37 @@ export default class SidebarStore { - constructor(store) { + constructor(options) { if (!SidebarStore.singleton) { - const { currentUser, rootPath, editable } = store; - this.currentUser = currentUser; - this.rootPath = rootPath; - this.editable = editable; - this.timeEstimate = 0; - this.totalTimeSpent = 0; - this.humanTimeEstimate = ''; - this.humanTimeSpent = ''; - this.assignees = []; - this.isFetching = { - assignees: true, - participants: true, - subscriptions: true, - }; - this.isLoading = {}; - this.autocompleteProjects = []; - this.moveToProjectId = 0; - this.isLockDialogOpen = false; - this.participants = []; - this.subscribed = null; - - SidebarStore.singleton = this; + this.initSingleton(options); } return SidebarStore.singleton; } + initSingleton(options) { + const { currentUser, rootPath, editable } = options; + this.currentUser = currentUser; + this.rootPath = rootPath; + this.editable = editable; + this.timeEstimate = 0; + this.totalTimeSpent = 0; + this.humanTimeEstimate = ''; + this.humanTimeSpent = ''; + this.assignees = []; + this.isFetching = { + assignees: true, + participants: true, + subscriptions: true, + }; + this.isLoading = {}; + this.autocompleteProjects = []; + this.moveToProjectId = 0; + this.isLockDialogOpen = false; + this.participants = []; + this.subscribed = null; + + SidebarStore.singleton = this; + } + setAssigneeData(data) { this.isFetching.assignees = false; if (data.assignees) { diff --git a/app/assets/javascripts/single_file_diff.js b/app/assets/javascripts/single_file_diff.js index 3f811c59cb9..95e51bc4e7a 100644 --- a/app/assets/javascripts/single_file_diff.js +++ b/app/assets/javascripts/single_file_diff.js @@ -2,6 +2,7 @@ import FilesCommentButton from './files_comment_button'; import imageDiffHelper from './image_diff/helpers/index'; +import syntaxHighlight from './syntax_highlight'; const WRAPPER = '<div class="diff-content"></div>'; const LOADING_HTML = '<i class="fa fa-spinner fa-spin"></i>'; @@ -64,7 +65,7 @@ export default class SingleFileDiff { _this.loadingContent.hide(); if (data.html) { _this.content = $(data.html); - _this.content.syntaxHighlight(); + syntaxHighlight(_this.content); } else { _this.hasError = true; _this.content = $(ERROR_HTML); diff --git a/app/assets/javascripts/syntax_highlight.js b/app/assets/javascripts/syntax_highlight.js index 662d6b36c16..62bdef76c55 100644 --- a/app/assets/javascripts/syntax_highlight.js +++ b/app/assets/javascripts/syntax_highlight.js @@ -10,17 +10,15 @@ // <div class="js-syntax-highlight"></div> // -$.fn.syntaxHighlight = function() { - var $children; - - if ($(this).hasClass('js-syntax-highlight')) { +export default function syntaxHighlight(el) { + if ($(el).hasClass('js-syntax-highlight')) { // Given the element itself, apply highlighting - return $(this).addClass(gon.user_color_scheme); + return $(el).addClass(gon.user_color_scheme); } else { // Given a parent element, recurse to any of its applicable children - $children = $(this).find('.js-syntax-highlight'); + const $children = $(el).find('.js-syntax-highlight'); if ($children.length) { - return $children.syntaxHighlight(); + return syntaxHighlight($children); } } -}; +} diff --git a/app/assets/javascripts/todos.js b/app/assets/javascripts/todos.js index 2fffe09c74e..748caecf153 100644 --- a/app/assets/javascripts/todos.js +++ b/app/assets/javascripts/todos.js @@ -1,5 +1,5 @@ /* eslint-disable class-methods-use-this, no-unneeded-ternary, quote-props */ - +import { visitUrl } from './lib/utils/url_utility'; import UsersSelect from './users_select'; import { isMetaClick } from './lib/utils/common_utils'; @@ -150,7 +150,7 @@ export default class Todos { window.open(todoLink, windowTarget); } else { - gl.utils.visitUrl(todoLink); + visitUrl(todoLink); } } } diff --git a/app/assets/javascripts/tree.js b/app/assets/javascripts/tree.js index 7777ed1c3dc..1a0b2c0415b 100644 --- a/app/assets/javascripts/tree.js +++ b/app/assets/javascripts/tree.js @@ -1,4 +1,5 @@ /* eslint-disable func-names, space-before-function-paren, wrap-iife, max-len, quotes, consistent-return, no-var, one-var, one-var-declaration-per-line, no-else-return, prefer-arrow-callback, class-methods-use-this */ +import { visitUrl } from './lib/utils/url_utility'; export default class TreeView { constructor() { @@ -14,7 +15,7 @@ export default class TreeView { e.preventDefault(); return window.open(path, '_blank'); } else { - return gl.utils.visitUrl(path); + return visitUrl(path); } } }); @@ -56,7 +57,7 @@ export default class TreeView { } else if (e.which === 13) { path = $('.tree-item.selected .tree-item-file-name a').attr('href'); if (path) { - return gl.utils.visitUrl(path); + return visitUrl(path); } } }); diff --git a/app/assets/javascripts/users/activity_calendar.js b/app/assets/javascripts/users/activity_calendar.js index 5e947769f8a..4fa8c680580 100644 --- a/app/assets/javascripts/users/activity_calendar.js +++ b/app/assets/javascripts/users/activity_calendar.js @@ -1,5 +1,6 @@ import _ from 'underscore'; import d3 from 'd3'; +import { getDayName, getDayDifference } from '../lib/utils/datetime_utility'; const LOADING_HTML = ` <div class="text-center"> @@ -17,7 +18,7 @@ function getSystemDate(systemUtcOffsetSeconds) { function formatTooltipText({ date, count }) { const dateObject = new Date(date); - const dateDayName = gl.utils.getDayName(dateObject); + const dateDayName = getDayName(dateObject); const dateText = dateObject.format('mmm d, yyyy'); let contribText = 'No contributions'; @@ -51,7 +52,7 @@ export default class ActivityCalendar { const oneYearAgo = new Date(today); oneYearAgo.setFullYear(today.getFullYear() - 1); - const days = gl.utils.getDayDifference(oneYearAgo, today); + const days = getDayDifference(oneYearAgo, today); for (let i = 0; i <= days; i += 1) { const date = new Date(oneYearAgo); diff --git a/app/assets/javascripts/users/user_tabs.js b/app/assets/javascripts/users/user_tabs.js index 1215b265e28..992baa9a1ef 100644 --- a/app/assets/javascripts/users/user_tabs.js +++ b/app/assets/javascripts/users/user_tabs.js @@ -1,4 +1,6 @@ +import Activities from '../activities'; import ActivityCalendar from './activity_calendar'; +import { localTimeAgo } from '../lib/utils/datetime_utility'; /** * UserTabs @@ -138,7 +140,7 @@ export default class UserTabs { const tabSelector = `div#${action}`; this.$parentEl.find(tabSelector).html(data.html); this.loaded[action] = true; - gl.utils.localTimeAgo($('.js-timeago', tabSelector)); + localTimeAgo($('.js-timeago', tabSelector)); }, }); } @@ -169,7 +171,7 @@ export default class UserTabs { }); // eslint-disable-next-line no-new - new gl.Activities(); + new Activities(); this.loaded.activity = true; } diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_deployment.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_deployment.js index e86a0f7e749..ee1a45cc754 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_deployment.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_deployment.js @@ -1,4 +1,5 @@ -import '~/lib/utils/datetime_utility'; +import { getTimeago } from '~/lib/utils/datetime_utility'; +import { visitUrl } from '../../lib/utils/url_utility'; import Flash from '../../flash'; import MemoryUsage from './mr_widget_memory_usage'; import StatusIcon from './mr_widget_status_icon'; @@ -16,7 +17,7 @@ export default { }, methods: { formatDate(date) { - return gl.utils.getTimeago().format(date); + return getTimeago().format(date); }, hasExternalUrls(deployment = {}) { return deployment.external_url && deployment.external_url_formatted; @@ -36,7 +37,7 @@ export default { .then(res => res.json()) .then((res) => { if (res.redirect_url) { - gl.utils.visitUrl(res.redirect_url); + visitUrl(res.redirect_url); } }) .catch(() => { diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.js index 05c4a28be88..43b2d238f65 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.js @@ -65,10 +65,12 @@ export default { <div class="mr-widget-body media"> <status-icon status="success" /> <div class="media-body"> - <h4> - Set by - <mr-widget-author :author="mr.setToMWPSBy" /> - to be merged automatically when the pipeline succeeds + <h4 class="flex-container-block"> + <span class="append-right-10"> + Set by + <mr-widget-author :author="mr.setToMWPSBy" /> + to be merged automatically when the pipeline succeeds + </span> <a v-if="mr.canCancelAutomaticMerge" @click.prevent="cancelAutomaticMerge" @@ -94,8 +96,13 @@ export default { <p v-if="mr.shouldRemoveSourceBranch"> The source branch will be removed </p> - <p v-else> - The source branch will not be removed + <p + v-else + class="flex-container-block" + > + <span class="append-right-10"> + The source branch will not be removed + </span> <a v-if="canRemoveSourceBranch" :disabled="isRemovingSourceBranch" diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js index c1f7e64f580..707766e08e4 100644 --- a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js +++ b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js @@ -1,5 +1,6 @@ import Timeago from 'timeago.js'; import { getStateKey } from '../dependencies'; +import { formatDate } from '../../lib/utils/datetime_utility'; export default class MergeRequestStore { @@ -122,7 +123,7 @@ export default class MergeRequestStore { static getEventObject(event) { return { author: MergeRequestStore.getAuthorObject(event), - updatedAt: gl.utils.formatDate(MergeRequestStore.getEventUpdatedAtDate(event)), + updatedAt: formatDate(MergeRequestStore.getEventUpdatedAtDate(event)), formattedUpdatedAt: MergeRequestStore.getEventDate(event), }; } diff --git a/app/assets/javascripts/vue_shared/components/icon.vue b/app/assets/javascripts/vue_shared/components/icon.vue index 4216660da8c..365229ea274 100644 --- a/app/assets/javascripts/vue_shared/components/icon.vue +++ b/app/assets/javascripts/vue_shared/components/icon.vue @@ -36,6 +36,30 @@ required: false, default: '', }, + + width: { + type: Number, + required: false, + default: null, + }, + + height: { + type: Number, + required: false, + default: null, + }, + + y: { + type: Number, + required: false, + default: null, + }, + + x: { + type: Number, + required: false, + default: null, + }, }, computed: { @@ -51,7 +75,11 @@ <template> <svg - :class="[iconSizeClass, cssClasses]"> + :class="[iconSizeClass, cssClasses]" + :width="width" + :height="height" + :x="x" + :y="y"> <use v-bind="{'xlink:href':spriteHref}"/> </svg> diff --git a/app/assets/javascripts/vue_shared/components/memory_graph.js b/app/assets/javascripts/vue_shared/components/memory_graph.js index 643b77e04c7..f37ef1a5ca3 100644 --- a/app/assets/javascripts/vue_shared/components/memory_graph.js +++ b/app/assets/javascripts/vue_shared/components/memory_graph.js @@ -1,3 +1,5 @@ +import { getTimeago } from '../../lib/utils/datetime_utility'; + export default { name: 'MemoryGraph', props: { @@ -16,7 +18,7 @@ export default { }, computed: { getFormattedMedian() { - const deployedSince = gl.utils.getTimeago().format(this.deploymentTime * 1000); + const deployedSince = getTimeago().format(this.deploymentTime * 1000); return `Deployed ${deployedSince}`; }, }, diff --git a/app/assets/javascripts/vue_shared/components/popup_dialog.vue b/app/assets/javascripts/vue_shared/components/modal.vue index 47efee64c6e..55f466b7b41 100644 --- a/app/assets/javascripts/vue_shared/components/popup_dialog.vue +++ b/app/assets/javascripts/vue_shared/components/modal.vue @@ -1,6 +1,6 @@ <script> export default { - name: 'popup-dialog', + name: 'modal', props: { title: { @@ -38,7 +38,8 @@ export default { }, primaryButtonLabel: { type: String, - required: true, + required: false, + default: '', }, submitDisabled: { type: Boolean, @@ -74,7 +75,7 @@ export default { <template> <div class="modal-open"> <div - class="modal popup-dialog" + class="modal show" role="dialog" tabindex="-1" > @@ -113,8 +114,9 @@ export default { {{ closeButtonLabel }} </button> <button + v-if="primaryButtonLabel" type="button" - class="btn pull-right" + class="btn pull-right js-primary-button" :disabled="submitDisabled" :class="btnKindClass" @click="emitSubmit(true)"> diff --git a/app/assets/javascripts/vue_shared/components/recaptcha_modal.vue b/app/assets/javascripts/vue_shared/components/recaptcha_modal.vue new file mode 100644 index 00000000000..8053c65d498 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/recaptcha_modal.vue @@ -0,0 +1,85 @@ +<script> +import modal from './modal.vue'; + +export default { + name: 'recaptcha-modal', + + props: { + html: { + type: String, + required: false, + default: '', + }, + }, + + data() { + return { + script: {}, + scriptSrc: 'https://www.google.com/recaptcha/api.js', + }; + }, + + components: { + modal, + }, + + methods: { + appendRecaptchaScript() { + this.removeRecaptchaScript(); + + const script = document.createElement('script'); + script.src = this.scriptSrc; + script.classList.add('js-recaptcha-script'); + script.async = true; + script.defer = true; + + this.script = script; + + document.body.appendChild(script); + }, + + removeRecaptchaScript() { + if (this.script instanceof Element) this.script.remove(); + }, + + close() { + this.removeRecaptchaScript(); + this.$emit('close'); + }, + + submit() { + this.$el.querySelector('form').submit(); + }, + }, + + watch: { + html() { + this.appendRecaptchaScript(); + }, + }, + + mounted() { + window.recaptchaDialogCallback = this.submit.bind(this); + }, +}; +</script> + +<template> +<modal + kind="warning" + class="recaptcha-modal js-recaptcha-modal" + :hide-footer="true" + :title="__('Please solve the reCAPTCHA')" + @toggle="close" +> + <div slot="body"> + <p> + {{__('We want to be sure it is you, please confirm you are not a robot.')}} + </p> + <div + ref="recaptcha" + v-html="html" + ></div> + </div> +</modal> +</template> diff --git a/app/assets/javascripts/vue_shared/components/toggle_button.vue b/app/assets/javascripts/vue_shared/components/toggle_button.vue index ddc9ddbc3a3..4277d9281a0 100644 --- a/app/assets/javascripts/vue_shared/components/toggle_button.vue +++ b/app/assets/javascripts/vue_shared/components/toggle_button.vue @@ -1,6 +1,13 @@ <script> + import { s__ } from '../../locale'; + import icon from './icon.vue'; import loadingIcon from './loading_icon.vue'; + const ICON_ON = 'status_success_borderless'; + const ICON_OFF = 'status_failed_borderless'; + const LABEL_ON = s__('ToggleButton|Toggle Status: ON'); + const LABEL_OFF = s__('ToggleButton|Toggle Status: OFF'); + export default { props: { name: { @@ -22,19 +29,10 @@ required: false, default: false, }, - enabledText: { - type: String, - required: false, - default: 'Enabled', - }, - disabledText: { - type: String, - required: false, - default: 'Disabled', - }, }, components: { + icon, loadingIcon, }, @@ -43,6 +41,15 @@ event: 'change', }, + computed: { + toggleIcon() { + return this.value ? ICON_ON : ICON_OFF; + }, + ariaLabel() { + return this.value ? LABEL_ON : LABEL_OFF; + }, + }, + methods: { toggleFeature() { if (!this.disabledInput) this.$emit('change', !this.value); @@ -60,10 +67,8 @@ /> <button type="button" - aria-label="Toggle" class="project-feature-toggle" - :data-enabled-text="enabledText" - :data-disabled-text="disabledText" + :aria-label="ariaLabel" :class="{ 'is-checked': value, 'is-disabled': disabledInput, @@ -72,6 +77,11 @@ @click="toggleFeature" > <loadingIcon class="loading-icon" /> + <span class="toggle-icon"> + <icon + css-classes="toggle-icon-svg" + :name="toggleIcon"/> + </span> </button> </label> </template> diff --git a/app/assets/javascripts/vue_shared/mixins/recaptcha_modal_implementor.js b/app/assets/javascripts/vue_shared/mixins/recaptcha_modal_implementor.js new file mode 100644 index 00000000000..ff1f565e79a --- /dev/null +++ b/app/assets/javascripts/vue_shared/mixins/recaptcha_modal_implementor.js @@ -0,0 +1,36 @@ +import recaptchaModal from '../components/recaptcha_modal.vue'; + +export default { + data() { + return { + showRecaptcha: false, + recaptchaHTML: '', + }; + }, + + components: { + recaptchaModal, + }, + + methods: { + openRecaptcha() { + this.showRecaptcha = true; + }, + + closeRecaptcha() { + this.showRecaptcha = false; + }, + + checkForSpam(data) { + if (!data.recaptcha_html) return data; + + this.recaptchaHTML = data.recaptcha_html; + + const spamError = new Error(data.error_message); + spamError.name = 'SpamError'; + spamError.message = 'SpamError'; + + throw spamError; + }, + }, +}; diff --git a/app/assets/javascripts/vue_shared/mixins/timeago.js b/app/assets/javascripts/vue_shared/mixins/timeago.js index 20f63ab663c..4e3b9d7b767 100644 --- a/app/assets/javascripts/vue_shared/mixins/timeago.js +++ b/app/assets/javascripts/vue_shared/mixins/timeago.js @@ -1,4 +1,4 @@ -import '../../lib/utils/datetime_utility'; +import { formatDate, getTimeago } from '../../lib/utils/datetime_utility'; /** * Mixin with time ago methods used in some vue components @@ -6,13 +6,13 @@ import '../../lib/utils/datetime_utility'; export default { methods: { timeFormated(time) { - const timeago = gl.utils.getTimeago(); + const timeago = getTimeago(); return timeago.format(time); }, tooltipTitle(time) { - return gl.utils.formatDate(time); + return formatDate(time); }, }, }; diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss index 3f630f82e29..fcc420923f9 100644 --- a/app/assets/stylesheets/framework/buttons.scss +++ b/app/assets/stylesheets/framework/buttons.scss @@ -4,8 +4,8 @@ padding: 1px 5px; font-size: 12px; color: $blue-500; - width: 23px; - height: 23px; + width: 24px; + height: 24px; border: 1px solid $blue-500; &:hover, diff --git a/app/assets/stylesheets/framework/contextual-sidebar.scss b/app/assets/stylesheets/framework/contextual-sidebar.scss index b73932eb7e1..8baf7ca23a4 100644 --- a/app/assets/stylesheets/framework/contextual-sidebar.scss +++ b/app/assets/stylesheets/framework/contextual-sidebar.scss @@ -1,4 +1,6 @@ .page-with-contextual-sidebar { + transition: padding-left $sidebar-transition-duration; + @media (min-width: $screen-md-min) { padding-left: $contextual-sidebar-collapsed-width; } @@ -27,8 +29,10 @@ .context-header { position: relative; margin-right: 2px; + width: $contextual-sidebar-width; a { + transition: padding $sidebar-transition-duration; font-weight: $gl-font-weight-bold; display: flex; align-items: center; @@ -63,10 +67,10 @@ } .nav-sidebar { + transition: width $sidebar-transition-duration, left $sidebar-transition-duration; position: fixed; z-index: 400; width: $contextual-sidebar-width; - transition: left $sidebar-transition-duration; top: $header-height; bottom: 0; left: 0; @@ -74,16 +78,15 @@ box-shadow: inset -2px 0 0 $border-color; transform: translate3d(0, 0, 0); - &:not(.sidebar-icons-only) { + &:not(.sidebar-collapsed-desktop) { @media (min-width: $screen-sm-min) and (max-width: $screen-md-max) { box-shadow: inset -2px 0 0 $border-color, 2px 1px 3px $dropdown-shadow-color; } } - &.sidebar-icons-only { - width: auto; - min-width: $contextual-sidebar-collapsed-width; + &.sidebar-collapsed-desktop { + width: $contextual-sidebar-collapsed-width; .nav-sidebar-inner-scroll { overflow-x: hidden; @@ -108,12 +111,11 @@ } } - &.nav-sidebar-expanded { + &.sidebar-expanded-mobile { left: 0; } a { - transition: none; text-decoration: none; } @@ -126,9 +128,10 @@ white-space: nowrap; a { + transition: padding $sidebar-transition-duration; display: flex; align-items: center; - padding: 12px 16px; + padding: 12px 15px; color: $gl-text-color-secondary; } @@ -288,7 +291,8 @@ > a { margin-left: 4px; - padding-left: 12px; + // Subtract width of left border on active element + padding-left: 11px; } .badge { @@ -313,15 +317,17 @@ .toggle-sidebar-button, .close-nav-button { width: $contextual-sidebar-width - 2px; + transition: width $sidebar-transition-duration; position: fixed; bottom: 0; - padding: 16px; + padding: $gl-padding; background-color: $gray-light; border: 0; border-top: 2px solid $border-color; color: $gl-text-color-secondary; display: flex; align-items: center; + line-height: 1; svg { margin-right: 8px; @@ -343,20 +349,21 @@ } } +.collapse-text { + white-space: nowrap; + overflow: hidden; +} -.sidebar-icons-only { +.sidebar-collapsed-desktop { .context-header { - height: 61px; + height: 60px; + width: $contextual-sidebar-collapsed-width; a { padding: 10px 4px; } } - li a { - padding: 12px 15px; - } - .sidebar-top-level-items > li { &.active a { padding-left: 12px; @@ -374,8 +381,8 @@ } .toggle-sidebar-button { - width: $contextual-sidebar-collapsed-width - 2px; padding: 16px; + width: $contextual-sidebar-collapsed-width - 2px; .collapse-text, .icon-angle-double-left { diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index 8d83554d813..478269f3fcf 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -143,20 +143,48 @@ } } +@mixin dropdown-item-hover { + background-color: $dropdown-item-hover-bg; + color: $gl-text-color; + outline: 0; + + // make sure the text color is not overriden + &.text-danger { + color: $brand-danger; + } + + .avatar { + border-color: $white-light; + } +} + @mixin dropdown-link { + background: transparent; + border: 0; + border-radius: 0; + box-shadow: none; display: block; + font-weight: $gl-font-weight-normal; position: relative; - padding: 5px 8px; + padding: 8px 16px; color: $gl-text-color; - line-height: initial; - border-radius: 2px; - white-space: nowrap; + line-height: normal; + white-space: normal; overflow: hidden; + text-align: left; + width: 100%; + + // make sure the text color is not overriden + &.text-danger { + color: $brand-danger; + } &:hover, + &:active, &:focus, &.is-focused { - background-color: $dropdown-link-hover-bg; + @include dropdown-item-hover; + text-decoration: none; .badge { @@ -166,6 +194,13 @@ &.dropdown-menu-user-link { line-height: 16px; + padding-top: 10px; + padding-bottom: 7px; + white-space: nowrap; + + .dropdown-menu-user-username { + display: block; + } } .icon-play { @@ -187,8 +222,8 @@ z-index: 300; min-width: 240px; max-width: 500px; - margin-top: 2px; - margin-bottom: 2px; + margin-top: $dropdown-vertical-offset; + margin-bottom: 24px; font-size: 14px; font-weight: $gl-font-weight-normal; padding: 8px 0; @@ -197,6 +232,10 @@ border-radius: $border-radius-base; box-shadow: 0 2px 4px $dropdown-shadow-color; + &.dropdown-open-top { + margin-bottom: $dropdown-vertical-offset; + } + &.dropdown-open-left { right: 0; left: auto; @@ -227,16 +266,27 @@ } li { + display: block; text-align: left; list-style: none; - padding: 0 10px; + padding: 0 1px; + + a, + button, + .menu-item { + @include dropdown-link; + } } .divider { height: 1px; - margin: 6px 10px; + margin: 6px 0; padding: 0; background-color: $dropdown-divider-color; + + &:hover { + background-color: $dropdown-divider-color; + } } .separator { @@ -247,10 +297,6 @@ background-color: $dropdown-divider-color; } - a { - @include dropdown-link; - } - .dropdown-menu-empty-item a { &:hover, &:focus { @@ -262,7 +308,7 @@ color: $gl-text-color-secondary; font-size: 13px; line-height: 22px; - padding: 0 16px; + padding: 8px 16px; } &.capitalize-header .dropdown-header { @@ -277,7 +323,7 @@ .separator + .dropdown-header, .separator + .dropdown-bold-header { - padding-top: 2px; + padding-top: 10px; } .unclickable { @@ -298,48 +344,28 @@ } .dropdown-menu li { - padding: $gl-btn-padding; cursor: pointer; + &.droplab-item-active button { + @include dropdown-item-hover; + } + > a, > button { display: flex; margin: 0; - padding: 0; - border-radius: 0; text-overflow: inherit; - background-color: inherit; - color: inherit; - border: inherit; text-align: left; - &:hover, - &:focus { - background-color: inherit; - color: inherit; - } - &.btn .fa:not(:last-child) { margin-left: 5px; } } - &:hover, - &:focus { - background-color: $dropdown-hover-color; - color: $white-light; - } - &.droplab-item-selected i { visibility: visible; } - &.divider { - margin: 0 8px; - padding: 0; - border-top: $gray-darkest; - } - .icon { visibility: hidden; } @@ -431,11 +457,6 @@ } } -.dropdown-menu-user-link { - padding-top: 10px; - padding-bottom: 7px; -} - .dropdown-menu-user-full-name { display: block; font-weight: $gl-font-weight-normal; @@ -464,41 +485,44 @@ .dropdown-menu-align-right { left: auto; right: 0; - margin-top: -5px; } .dropdown-menu-selectable { - a { - padding-left: 26px; - position: relative; + li { + a { + padding: 8px 40px; + position: relative; + + &.is-indeterminate, + &.is-active { + color: $gl-text-color; + + &::before { + position: absolute; + left: 16px; + top: 16px; + transform: translateY(-50%); + font: normal normal normal 14px/1 FontAwesome; + font-size: inherit; + text-rendering: auto; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + } - &.is-indeterminate, - &.is-active { - font-weight: $gl-font-weight-bold; - color: $gl-text-color; - - &::before { - position: absolute; - left: 6px; - top: 50%; - transform: translateY(-50%); - font: normal normal normal 14px/1 FontAwesome; - font-size: inherit; - text-rendering: auto; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; + &.dropdown-menu-user-link { + &::before { + top: 50%; + } + } } - } - &.is-indeterminate::before { - content: "\f068"; - } + &.is-indeterminate::before { + content: "\f068"; + } - &.is-active::before { - content: "\f00c"; - position: absolute; - top: 50%; - transform: translateY(-50%); + &.is-active::before { + content: "\f00c"; + } } } } @@ -735,136 +759,6 @@ } } -@mixin dropdown-item-hover { - background-color: $dropdown-item-hover-bg; - color: $gl-text-color; -} - -// TODO: change global style and remove mixin -@mixin new-style-dropdown($selector: '') { - #{$selector}.dropdown-menu, - #{$selector}.dropdown-menu-nav { - margin-bottom: 24px; - - &.dropdown-open-top { - margin-bottom: $dropdown-vertical-offset; - } - - li { - display: block; - padding: 0 1px; - - &:hover { - background-color: transparent; - } - - &.divider { - margin: 6px 0; - - &:hover { - background-color: $dropdown-divider-color; - } - } - - &.dropdown-header { - padding: 8px 16px; - } - - &.droplab-item-active button { - @include dropdown-item-hover; - } - - a, - button, - .menu-item { - margin-bottom: 0; - border-radius: 0; - box-shadow: none; - padding: 8px 16px; - text-align: left; - white-space: normal; - width: 100%; - font-weight: $gl-font-weight-normal; - line-height: normal; - - &.dropdown-menu-user-link { - white-space: nowrap; - - .dropdown-menu-user-username { - display: block; - } - } - - // make sure the text color is not overriden - &.text-danger { - color: $brand-danger; - } - - &.is-focused, - &:hover, - &:active, - &:focus { - @include dropdown-item-hover; - - background-color: $dropdown-item-hover-bg; - color: $gl-text-color; - - // make sure the text color is not overriden - &.text-danger { - color: $brand-danger; - } - } - - &.is-active { - font-weight: inherit; - - &::before { - top: 16px; - } - - &.dropdown-menu-user-link::before { - top: 50%; - transform: translateY(-50%); - } - } - } - - &.dropdown-menu-empty-item a { - &:hover, - &:focus { - background-color: transparent; - } - } - } - - &.dropdown-menu-selectable { - li { - a { - padding: 8px 40px; - - &.is-indeterminate::before, - &.is-active::before { - left: 16px; - } - } - } - } - } - - #{$selector}.dropdown-menu-align-right { - margin-top: 2px; - } - - .open { - #{$selector}.dropdown-menu, - #{$selector}.dropdown-menu-nav { - @media (max-width: $screen-xs-max) { - max-width: 100%; - } - } - } -} - @media (max-width: $screen-xs-max) { .navbar-gitlab { li.header-projects, @@ -891,9 +785,6 @@ } } -@include new-style-dropdown('.breadcrumbs-list .dropdown '); -@include new-style-dropdown('.js-namespace-select + '); - header.header-content .dropdown-menu.projects-dropdown-menu { padding: 0; } diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss index 609f33582e1..1588036aeae 100644 --- a/app/assets/stylesheets/framework/files.scss +++ b/app/assets/stylesheets/framework/files.scss @@ -396,3 +396,8 @@ span.idiff { .file-fork-suggestion-note { margin-right: 1.5em; } + +.label-lfs { + color: $common-gray-light; + border: 1px solid $common-gray-light; +} diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss index cec38eea464..2d7465401f1 100644 --- a/app/assets/stylesheets/framework/filters.scss +++ b/app/assets/stylesheets/framework/filters.scss @@ -50,8 +50,6 @@ } .filtered-search-wrapper { - @include new-style-dropdown; - display: -webkit-flex; display: flex; @@ -165,16 +163,6 @@ } } -.droplab-dropdown li.filtered-search-token { - padding: 0; - - &:hover, - &:focus { - background-color: inherit; - color: inherit; - } -} - .filtered-search-term { .name { background-color: inherit; @@ -336,21 +324,12 @@ .filtered-search-history-dropdown-content { max-height: none; -} - -.filtered-search-history-dropdown-item, -.filtered-search-history-clear-button { - @include dropdown-link; - - overflow: hidden; - width: 100%; - margin: 0.5em 0; - background-color: transparent; - border: 0; - text-align: left; - white-space: nowrap; - text-overflow: ellipsis; + .filtered-search-history-dropdown-item, + .filtered-search-history-clear-button { + white-space: nowrap; + text-overflow: ellipsis; + } } .filtered-search-history-dropdown-token { @@ -402,24 +381,9 @@ } } -%filter-dropdown-item-btn-hover { - text-decoration: none; - outline: 0; - - .avatar { - border-color: $white-light; - } -} - .droplab-dropdown .dropdown-menu .filter-dropdown-item { .btn { - border: 0; - width: 100%; - text-align: left; - padding: 8px 16px; text-overflow: ellipsis; - overflow: hidden; - border-radius: 0; .fa { width: 15px; @@ -434,11 +398,6 @@ height: 17px; top: 0; } - - &:hover, - &:focus { - @extend %filter-dropdown-item-btn-hover; - } } .dropdown-light-content { @@ -459,17 +418,9 @@ word-break: break-all; } } - - &.droplab-item-active .btn { - @extend %filter-dropdown-item-btn-hover; - } } .filter-dropdown-loading { padding: 8px 16px; text-align: center; } - -.issues-details-filters { - @include new-style-dropdown; -} diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index f985a3aea5c..29714e348a0 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -1,10 +1,4 @@ -.content-wrapper.page-with-new-nav { - margin-top: $header-height; -} - .navbar-gitlab { - @include new-style-dropdown; - &.navbar-gitlab { padding: 0 16px; z-index: 1000; diff --git a/app/assets/stylesheets/framework/images.scss b/app/assets/stylesheets/framework/images.scss index 78a8e57ddbb..aa2d30a3cef 100644 --- a/app/assets/stylesheets/framework/images.scss +++ b/app/assets/stylesheets/framework/images.scss @@ -19,6 +19,13 @@ max-width: 425px; width: 100%; } + + &.svg-250 { + img, + svg { + width: 250px; + } + } } @mixin svg-size($size) { diff --git a/app/assets/stylesheets/framework/issue_box.scss b/app/assets/stylesheets/framework/issue_box.scss index 1537b0744cc..1d8bd26cf1a 100644 --- a/app/assets/stylesheets/framework/issue_box.scss +++ b/app/assets/stylesheets/framework/issue_box.scss @@ -24,10 +24,14 @@ font-size: $gl-font-size; line-height: 25px; - &.status-box-closed { + &.status-box-mr-closed { background-color: $gl-danger; } + &.status-box-issue-closed { + background-color: $gl-primary; + } + &.status-box-merged { background-color: $gl-primary; } diff --git a/app/assets/stylesheets/framework/layout.scss b/app/assets/stylesheets/framework/layout.scss index cb324ccc440..3f0268541a4 100644 --- a/app/assets/stylesheets/framework/layout.scss +++ b/app/assets/stylesheets/framework/layout.scss @@ -24,6 +24,7 @@ body { } .content-wrapper { + margin-top: $header-height; padding-bottom: 100px; } @@ -105,11 +106,11 @@ body { } } -.page-with-sidebar > .content-wrapper { +.layout-page > .content-wrapper { min-height: calc(100vh - #{$header-height}); } -.with-performance-bar .page-with-sidebar { +.with-performance-bar .layout-page { margin-top: $header-height + $performance-bar-height; } diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss index e6e6c4c3963..f79a71221c4 100644 --- a/app/assets/stylesheets/framework/lists.scss +++ b/app/assets/stylesheets/framework/lists.scss @@ -132,8 +132,6 @@ ul.content-list { } .controls { - @include new-style-dropdown; - float: right; > .control-text { diff --git a/app/assets/stylesheets/framework/mobile.scss b/app/assets/stylesheets/framework/mobile.scss index 600a1f53b58..a12f28efce6 100644 --- a/app/assets/stylesheets/framework/mobile.scss +++ b/app/assets/stylesheets/framework/mobile.scss @@ -111,21 +111,4 @@ aside:not(.right-sidebar) { display: none; } - - .show-aside { - display: block !important; - } -} - -.show-aside { - display: none; - position: fixed; - right: 0; - top: 30%; - padding: 5px 15px; - background: $show-aside-bg; - font-size: 20px; - color: $show-aside-color; - z-index: 100; - box-shadow: 0 1px 2px $show-aside-shadow; } diff --git a/app/assets/stylesheets/framework/modal.scss b/app/assets/stylesheets/framework/modal.scss index 5c9838c1029..1be66d0ab21 100644 --- a/app/assets/stylesheets/framework/modal.scss +++ b/app/assets/stylesheets/framework/modal.scss @@ -44,7 +44,21 @@ body.modal-open { } } -.modal.popup-dialog { - display: block; +.modal { + background-color: $black-transparent; + z-index: 2100; + + @media (min-width: $screen-md-min) { + .modal-dialog { + margin: 30px auto; + } + } } +.recaptcha-modal .recaptcha-form { + display: inline-block; + + .recaptcha { + margin: 0; + } +} diff --git a/app/assets/stylesheets/framework/secondary-navigation-elements.scss b/app/assets/stylesheets/framework/secondary-navigation-elements.scss index 8498b37abe4..5f67126bafa 100644 --- a/app/assets/stylesheets/framework/secondary-navigation-elements.scss +++ b/app/assets/stylesheets/framework/secondary-navigation-elements.scss @@ -86,8 +86,6 @@ } .nav-controls { - @include new-style-dropdown; - display: inline-block; float: right; text-align: right; diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss index 792981fdc48..0742c0a2a09 100644 --- a/app/assets/stylesheets/framework/sidebar.scss +++ b/app/assets/stylesheets/framework/sidebar.scss @@ -50,6 +50,11 @@ &:not(.disabled) { cursor: pointer; } + + svg { + width: $gl-padding; + height: $gl-padding; + } } } @@ -139,10 +144,6 @@ } } -.issuable-sidebar { - @include new-style-dropdown; -} - .pikaday-container { .pika-single { margin-top: 2px; diff --git a/app/assets/stylesheets/framework/toggle.scss b/app/assets/stylesheets/framework/toggle.scss index 71765da3908..0cd83df218f 100644 --- a/app/assets/stylesheets/framework/toggle.scss +++ b/app/assets/stylesheets/framework/toggle.scss @@ -27,7 +27,7 @@ border: 0; outline: 0; display: block; - width: 100px; + width: 50px; height: 24px; cursor: pointer; user-select: none; @@ -42,31 +42,31 @@ background: none; } - &::before { - color: $feature-toggle-text-color; - font-size: 12px; - line-height: 24px; - position: absolute; - top: 0; - left: 25px; - right: 5px; - text-align: center; - overflow: hidden; - text-overflow: ellipsis; - animation: animate-disabled .2s ease-in; - content: attr(data-disabled-text); - } - - &::after { + .toggle-icon { position: relative; display: block; - content: ""; - width: 22px; - height: 18px; left: 0; border-radius: 9px; background: $feature-toggle-color; transition: all .2s ease; + + &, + .toggle-icon-svg { + width: 18px; + height: 18px; + } + + .toggle-icon-svg { + fill: $feature-toggle-color-disabled; + } + + .toggle-status-checked { + display: none; + } + + .toggle-status-unchecked { + display: inline; + } } .loading-icon { @@ -77,11 +77,10 @@ top: 50%; left: 50%; transform: translate(-50%, -50%); - } &.is-loading { - &::before { + .toggle-icon { display: none; } @@ -100,15 +99,20 @@ &.is-checked { background: $feature-toggle-color-enabled; - &::before { - left: 5px; - right: 25px; - animation: animate-enabled .2s ease-in; - content: attr(data-enabled-text); - } + .toggle-icon { + left: calc(100% - 18px); - &::after { - left: calc(100% - 22px); + .toggle-icon-svg { + fill: $feature-toggle-color-enabled; + } + + .toggle-status-checked { + display: inline; + } + + .toggle-status-unchecked { + display: none; + } } } diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss index 0817cce114c..11c1aeea871 100644 --- a/app/assets/stylesheets/framework/typography.scss +++ b/app/assets/stylesheets/framework/typography.scss @@ -343,8 +343,6 @@ a > code { @extend .ref-name; } -@include new-style-dropdown('.git-revision-dropdown'); - /** * Apply Markdown typography * diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 4f99c27eff1..b84d6c140be 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -5,10 +5,9 @@ $grid-size: 8px; $gutter_collapsed_width: 62px; $gutter_width: 290px; $gutter_inner_width: 250px; -$sidebar-transition-duration: .15s; +$sidebar-transition-duration: .3s; $sidebar-breakpoint: 1024px; $default-transition-duration: .15s; -$right-sidebar-transition-duration: .3s; $contextual-sidebar-width: 220px; $contextual-sidebar-collapsed-width: 50px; @@ -246,9 +245,6 @@ $btn-sm-side-margin: 7px; $btn-xs-side-margin: 5px; $issue-status-expired: $orange-500; $issuable-sidebar-color: $gl-text-color-secondary; -$show-aside-bg: #eee; -$show-aside-color: #777; -$show-aside-shadow: #ddd; $group-path-color: #999; $namespace-kind-color: #aaa; $panel-heading-link-color: #777; @@ -722,7 +718,7 @@ $issuable-warning-icon-margin: 4px; Image Commenting cursor */ $image-comment-cursor-left-offset: 12; -$image-comment-cursor-top-offset: 30; +$image-comment-cursor-top-offset: 12; /* Popup diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss index 3683afa07de..2803144ef1d 100644 --- a/app/assets/stylesheets/pages/boards.scss +++ b/app/assets/stylesheets/pages/boards.scss @@ -57,7 +57,7 @@ position: relative; @media (min-width: $screen-sm-min) { - transition: width $right-sidebar-transition-duration; + transition: width $sidebar-transition-duration; width: 100%; &.is-compact { @@ -415,7 +415,7 @@ margin: 5px; } -.page-with-contextual-sidebar.page-with-sidebar .issue-boards-sidebar { +.page-with-contextual-sidebar.layout-page .issue-boards-sidebar { .issuable-sidebar-header { position: relative; } @@ -453,8 +453,8 @@ .right-sidebar.right-sidebar-expanded { &.boards-sidebar-slide-enter-active, &.boards-sidebar-slide-leave-active { - transition: width $right-sidebar-transition-duration, - padding $right-sidebar-transition-duration; + transition: width $sidebar-transition-duration, + padding $sidebar-transition-duration; } &.boards-sidebar-slide-enter, diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss index f139f4ab650..98d460339cd 100644 --- a/app/assets/stylesheets/pages/builds.scss +++ b/app/assets/stylesheets/pages/builds.scss @@ -323,8 +323,6 @@ } .build-dropdown { - @include new-style-dropdown; - margin: $gl-padding 0; padding: 0; diff --git a/app/assets/stylesheets/pages/clusters.scss b/app/assets/stylesheets/pages/clusters.scss index c303f016ff9..88d44131d5b 100644 --- a/app/assets/stylesheets/pages/clusters.scss +++ b/app/assets/stylesheets/pages/clusters.scss @@ -13,8 +13,6 @@ max-width: 100%; } -@include new-style-dropdown('.clusters-dropdown '); - .clusters-container { .nav-bar-right { padding: $gl-padding-top $gl-padding; diff --git a/app/assets/stylesheets/pages/cycle_analytics.scss b/app/assets/stylesheets/pages/cycle_analytics.scss index 292e0ad394b..3b35beb7695 100644 --- a/app/assets/stylesheets/pages/cycle_analytics.scss +++ b/app/assets/stylesheets/pages/cycle_analytics.scss @@ -1,6 +1,4 @@ #cycle-analytics { - @include new-style-dropdown; - max-width: 1000px; margin: 24px auto 0; position: relative; diff --git a/app/assets/stylesheets/pages/detail_page.scss b/app/assets/stylesheets/pages/detail_page.scss index 52e4d904b9b..2f2c04206e2 100644 --- a/app/assets/stylesheets/pages/detail_page.scss +++ b/app/assets/stylesheets/pages/detail_page.scss @@ -32,8 +32,6 @@ } .detail-page-header-actions { - @include new-style-dropdown; - align-self: center; flex-shrink: 0; flex: 0 0 auto; diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index 848d7f144dc..60b07537799 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -581,8 +581,6 @@ } .commit-stat-summary { - @include new-style-dropdown; - @media (min-width: $screen-sm-min) { margin-left: -$gl-padding; padding-left: $gl-padding; @@ -732,18 +730,18 @@ .frame.click-to-comment { position: relative; - cursor: image-url('icon_image_comment.svg') + cursor: image-url('illustrations/image_comment_light_cursor.svg') $image-comment-cursor-left-offset $image-comment-cursor-top-offset, auto; // Retina cursor - cursor: -webkit-image-set(image-url('icon_image_comment.svg') 1x, image-url('icon_image_comment@2x.svg') 2x) + cursor: -webkit-image-set(image-url('illustrations/image_comment_light_cursor.svg') 1x, image-url('illustrations/image_comment_light_cursor@2x.svg') 2x) $image-comment-cursor-left-offset $image-comment-cursor-top-offset, auto; .comment-indicator { position: absolute; padding: 0; width: (2px * $image-comment-cursor-left-offset); - height: (1px * $image-comment-cursor-top-offset); + height: (2px * $image-comment-cursor-top-offset); // center the indicator to match the top left click region margin-top: (-1px * $image-comment-cursor-top-offset) + 2; margin-left: (-1px * $image-comment-cursor-left-offset) + 1; @@ -778,15 +776,20 @@ .frame .badge, .frame .image-comment-badge { // Center align badges on the frame - transform: translate3d(-50%, -50%, 0); + transform: translate(-50%, -50%); } .image-comment-badge { - @include btn-comment-icon; position: absolute; + width: 24px; + height: 24px; + padding: 0; + background: none; + border: 0; - &.inverted { - border-color: $white-light; + > svg { + width: 100%; + height: 100%; } } diff --git a/app/assets/stylesheets/pages/editor.scss b/app/assets/stylesheets/pages/editor.scss index c586dab4cf2..8ecda50602d 100644 --- a/app/assets/stylesheets/pages/editor.scss +++ b/app/assets/stylesheets/pages/editor.scss @@ -204,8 +204,6 @@ .gitlab-ci-yml-selector, .dockerfile-selector, .template-type-selector { - @include new-style-dropdown; - display: inline-block; vertical-align: top; font-family: $regular_font; diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss index b0795353ec1..f4882305c57 100644 --- a/app/assets/stylesheets/pages/environments.scss +++ b/app/assets/stylesheets/pages/environments.scss @@ -12,8 +12,6 @@ .environments-container { .ci-table { - @include new-style-dropdown; - .deployment-column { > span { word-break: break-all; @@ -201,8 +199,9 @@ stroke-width: 1; } -.deploy-info-text { - dominant-baseline: text-before-edge; +.divider-line { + stroke-width: 1; + stroke: $gray-darkest; } .prometheus-state { @@ -312,6 +311,20 @@ stroke: $gray-darker; } + .deploy-info-text { + dominant-baseline: text-before-edge; + font-size: 12px; + } + + .deploy-info-text-link { + font-family: $monospace_font; + fill: $gl-link-color; + + &:hover { + fill: $gl-link-hover-color; + } + } + @media (max-width: $screen-sm-max) { .label-axis-text, .text-metric-usage, diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 11ee1232bfe..e19196e0c41 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -126,7 +126,7 @@ top: $header-height; bottom: 0; right: 0; - transition: width $right-sidebar-transition-duration; + transition: width $sidebar-transition-duration; background: $gray-light; z-index: 200; overflow: hidden; @@ -470,7 +470,8 @@ } } - .milestone-title span { + .milestone-title span, + .collapse-truncated-title { @include str-truncated(100%); display: block; margin: 0 4px; @@ -487,12 +488,6 @@ } } - .dropdown-content { - a:hover { - color: inherit; - } - } - .dropdown-menu-toggle { width: 100%; padding-top: 6px; @@ -511,10 +506,6 @@ } } -.sidebar-move-issue-dropdown { - @include new-style-dropdown; -} - .sidebar-move-issue-confirmation-button { width: 100%; @@ -619,11 +610,19 @@ } .issuable-status-box { - float: none; - display: inline-block; + align-self: stretch; + display: flex; + justify-content: center; + align-items: center; margin-top: 0; - height: auto; - align-self: center; + padding-left: 9px; + padding-right: 9px; + + @media (min-width: $screen-sm-min) { + display: inline-block; + height: auto; + align-self: center; + } } .issuable-gutter-toggle { diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss index af1df8b8802..c48e58af691 100644 --- a/app/assets/stylesheets/pages/issues.scss +++ b/app/assets/stylesheets/pages/issues.scss @@ -142,8 +142,6 @@ ul.related-merge-requests > li { } .issue-form { - @include new-style-dropdown; - .select2-container { width: 250px !important; } diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss index 92abe82df4c..e8cd8a4905c 100644 --- a/app/assets/stylesheets/pages/labels.scss +++ b/app/assets/stylesheets/pages/labels.scss @@ -116,8 +116,6 @@ } .manage-labels-list { - @include new-style-dropdown; - > li:not(.empty-message):not(.is-not-draggable) { background-color: $white-light; cursor: move; diff --git a/app/assets/stylesheets/pages/members.scss b/app/assets/stylesheets/pages/members.scss index 18c48405ecd..3422829de58 100644 --- a/app/assets/stylesheets/pages/members.scss +++ b/app/assets/stylesheets/pages/members.scss @@ -58,8 +58,6 @@ } .member-form-control { - @include new-style-dropdown; - @media (max-width: $screen-xs-max) { padding-bottom: 5px; margin-left: 0; @@ -73,8 +71,6 @@ } .member-search-form { - @include new-style-dropdown; - position: relative; @media (min-width: $screen-sm-min) { diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index 2afb17334e3..e75a35d78ad 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -485,8 +485,6 @@ } .mr-source-target { - @include new-style-dropdown; - display: flex; flex-wrap: wrap; justify-content: space-between; @@ -608,8 +606,6 @@ } .mr-version-controls { - @include new-style-dropdown; - position: relative; background: $gray-light; color: $gl-text-color; @@ -727,7 +723,3 @@ font-size: 16px; } } - -.merge-request-form { - @include new-style-dropdown; -} diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss index ebb5d121433..6d4ccd53e12 100644 --- a/app/assets/stylesheets/pages/note_form.scss +++ b/app/assets/stylesheets/pages/note_form.scss @@ -23,8 +23,6 @@ .new-note, .note-edit-form { .note-form-actions { - @include new-style-dropdown; - position: relative; margin: $gl-padding 0 0; } diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index 4d5613c292b..26e6e8688b6 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -490,8 +490,6 @@ ul.notes { } .note-actions { - @include new-style-dropdown; - align-self: flex-start; flex-shrink: 0; display: inline-flex; diff --git a/app/assets/stylesheets/pages/notifications.scss b/app/assets/stylesheets/pages/notifications.scss index c28b1e68008..bdf07a99daf 100644 --- a/app/assets/stylesheets/pages/notifications.scss +++ b/app/assets/stylesheets/pages/notifications.scss @@ -14,7 +14,3 @@ font-size: 18px; } } - -.notification-form { - @include new-style-dropdown; -} diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index cb24274c612..9805fc4f882 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -286,8 +286,6 @@ // Pipeline visualization .pipeline-actions { - @include new-style-dropdown; - border-bottom: 0; } @@ -703,9 +701,6 @@ button.mini-pipeline-graph-dropdown-toggle { } } -@include new-style-dropdown('.big-pipeline-graph-dropdown-menu'); -@include new-style-dropdown('.mini-pipeline-graph-dropdown-menu'); - // dropdown content for big and mini pipeline .big-pipeline-graph-dropdown-menu, .mini-pipeline-graph-dropdown-menu { @@ -804,7 +799,6 @@ button.mini-pipeline-graph-dropdown-toggle { font-weight: normal; line-height: $line-height-base; white-space: nowrap; - border-radius: 3px; .ci-job-name-component { align-items: center; diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 674588752d2..6f4c678c4b8 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -323,8 +323,6 @@ } .project-repo-buttons { - @include new-style-dropdown; - .project-action-button .dropdown-menu { max-height: 250px; overflow-y: auto; @@ -898,8 +896,6 @@ pre.light-well { .new-protected-branch, .new-protected-tag { - @include new-style-dropdown; - label { margin-top: 6px; font-weight: $gl-font-weight-normal; @@ -919,8 +915,6 @@ pre.light-well { .protected-branches-list, .protected-tags-list { - @include new-style-dropdown; - margin-bottom: 30px; .settings-message { diff --git a/app/assets/stylesheets/pages/repo.scss b/app/assets/stylesheets/pages/repo.scss index 402412eae71..6eb92c7baee 100644 --- a/app/assets/stylesheets/pages/repo.scss +++ b/app/assets/stylesheets/pages/repo.scss @@ -1,16 +1,3 @@ -.modal.popup-dialog { - display: block; - background-color: $black-transparent; - z-index: 2100; - - @media (min-width: $screen-md-min) { - .modal-dialog { - width: 600px; - margin: 30px auto; - } - } -} - .project-refs-form, .project-refs-target-form { display: inline-block; diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss index fe455a04960..49c8e546bf2 100644 --- a/app/assets/stylesheets/pages/search.scss +++ b/app/assets/stylesheets/pages/search.scss @@ -116,11 +116,6 @@ input[type="checkbox"]:hover { opacity: 0; display: block; left: -5px; - padding: 0; - - ul { - padding: 10px 0; - } } .dropdown-content { @@ -185,8 +180,6 @@ input[type="checkbox"]:hover { } .search-holder { - @include new-style-dropdown; - @media (min-width: $screen-sm-min) { display: -webkit-flex; display: flex; diff --git a/app/assets/stylesheets/pages/todos.scss b/app/assets/stylesheets/pages/todos.scss index 2139a029fc7..a79772ea37b 100644 --- a/app/assets/stylesheets/pages/todos.scss +++ b/app/assets/stylesheets/pages/todos.scss @@ -265,7 +265,3 @@ font-weight: $gl-font-weight-bold; } } - -.todos-filters { - @include new-style-dropdown; -} diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss index 5d14323e4bc..e0ee7e9aa3d 100644 --- a/app/assets/stylesheets/pages/tree.scss +++ b/app/assets/stylesheets/pages/tree.scss @@ -1,6 +1,4 @@ .tree-holder { - @include new-style-dropdown; - .nav-block { margin: 10px 0; diff --git a/app/controllers/admin/groups_controller.rb b/app/controllers/admin/groups_controller.rb index 2ce26de1768..a94726887d9 100644 --- a/app/controllers/admin/groups_controller.rb +++ b/app/controllers/admin/groups_controller.rb @@ -1,4 +1,6 @@ class Admin::GroupsController < Admin::ApplicationController + include MembersPresentation + before_action :group, only: [:edit, :update, :destroy, :project_update, :members_update] def index @@ -10,8 +12,10 @@ class Admin::GroupsController < Admin::ApplicationController def show @group = Group.with_statistics.joins(:route).group('routes.path').find_by_full_path(params[:id]) - @members = @group.members.order("access_level DESC").page(params[:members_page]) - @requesters = AccessRequestsFinder.new(@group).execute(current_user) + @members = present_members( + @group.members.order("access_level DESC").page(params[:members_page])) + @requesters = present_members( + AccessRequestsFinder.new(@group).execute(current_user)) @projects = @group.projects.with_statistics.page(params[:projects_page]) end diff --git a/app/controllers/admin/health_check_controller.rb b/app/controllers/admin/health_check_controller.rb index 65a17828feb..61247b280b3 100644 --- a/app/controllers/admin/health_check_controller.rb +++ b/app/controllers/admin/health_check_controller.rb @@ -5,7 +5,7 @@ class Admin::HealthCheckController < Admin::ApplicationController end def reset_storage_health - Gitlab::Git::Storage::CircuitBreaker.reset_all! + Gitlab::Git::Storage::FailureInfo.reset_all! redirect_to admin_health_check_path, notice: _('Git storage health information has been reset') end diff --git a/app/controllers/admin/projects_controller.rb b/app/controllers/admin/projects_controller.rb index 50cf2643390..3afe66c3566 100644 --- a/app/controllers/admin/projects_controller.rb +++ b/app/controllers/admin/projects_controller.rb @@ -1,4 +1,6 @@ class Admin::ProjectsController < Admin::ApplicationController + include MembersPresentation + before_action :project, only: [:show, :transfer, :repository_check] before_action :group, only: [:show, :transfer] @@ -19,11 +21,14 @@ class Admin::ProjectsController < Admin::ApplicationController def show if @group - @group_members = @group.members.order("access_level DESC").page(params[:group_members_page]) + @group_members = present_members( + @group.members.order("access_level DESC").page(params[:group_members_page])) end - @project_members = @project.members.page(params[:project_members_page]) - @requesters = AccessRequestsFinder.new(@project).execute(current_user) + @project_members = present_members( + @project.members.page(params[:project_members_page])) + @requesters = present_members( + AccessRequestsFinder.new(@project).execute(current_user)) end def transfer diff --git a/app/controllers/concerns/boards_responses.rb b/app/controllers/concerns/boards_responses.rb index 2c9c095a5d7..a145049dc7d 100644 --- a/app/controllers/concerns/boards_responses.rb +++ b/app/controllers/concerns/boards_responses.rb @@ -24,11 +24,11 @@ module BoardsResponses end def respond_with_boards - respond_with(@boards) + respond_with(@boards) # rubocop:disable Gitlab/ModuleWithInstanceVariables end def respond_with_board - respond_with(@board) + respond_with(@board) # rubocop:disable Gitlab/ModuleWithInstanceVariables end def respond_with(resource) diff --git a/app/controllers/concerns/creates_commit.rb b/app/controllers/concerns/creates_commit.rb index 782f0be9c4a..6f4fdcdaa4f 100644 --- a/app/controllers/concerns/creates_commit.rb +++ b/app/controllers/concerns/creates_commit.rb @@ -1,6 +1,8 @@ module CreatesCommit extend ActiveSupport::Concern + include Gitlab::Utils::StrongMemoize + # rubocop:disable Gitlab/ModuleWithInstanceVariables def create_commit(service, success_path:, failure_path:, failure_view: nil, success_notice: nil) if can?(current_user, :push_code, @project) @project_to_commit_into = @project @@ -45,6 +47,7 @@ module CreatesCommit end end end + # rubocop:enable Gitlab/ModuleWithInstanceVariables def authorize_edit_tree! return if can_collaborate_with_project? @@ -77,6 +80,7 @@ module CreatesCommit end end + # rubocop:disable Gitlab/ModuleWithInstanceVariables def new_merge_request_path project_new_merge_request_path( @project_to_commit_into, @@ -88,20 +92,28 @@ module CreatesCommit } ) end + # rubocop:enable Gitlab/ModuleWithInstanceVariables def existing_merge_request_path - project_merge_request_path(@project, @merge_request) + project_merge_request_path(@project, @merge_request) # rubocop:disable Gitlab/ModuleWithInstanceVariables end + # rubocop:disable Gitlab/ModuleWithInstanceVariables def merge_request_exists? - return @merge_request if defined?(@merge_request) - - @merge_request = MergeRequestsFinder.new(current_user, project_id: @project.id).execute.opened - .find_by(source_project_id: @project_to_commit_into, source_branch: @branch_name, target_branch: @start_branch) + strong_memoize(:merge_request) do + MergeRequestsFinder.new(current_user, project_id: @project.id) + .execute + .opened + .find_by( + source_project_id: @project_to_commit_into, + source_branch: @branch_name, + target_branch: @start_branch) + end end + # rubocop:enable Gitlab/ModuleWithInstanceVariables def different_project? - @project_to_commit_into != @project + @project_to_commit_into != @project # rubocop:disable Gitlab/ModuleWithInstanceVariables end def create_merge_request? @@ -109,6 +121,6 @@ module CreatesCommit # as the target branch in the same project, # we don't want to create a merge request. params[:create_merge_request].present? && - (different_project? || @start_branch != @branch_name) + (different_project? || @start_branch != @branch_name) # rubocop:disable Gitlab/ModuleWithInstanceVariables end end diff --git a/app/controllers/concerns/group_tree.rb b/app/controllers/concerns/group_tree.rb index 9d4f97aa443..b10147835f3 100644 --- a/app/controllers/concerns/group_tree.rb +++ b/app/controllers/concerns/group_tree.rb @@ -1,4 +1,5 @@ module GroupTree + # rubocop:disable Gitlab/ModuleWithInstanceVariables def render_group_tree(groups) @groups = if params[:filter].present? Gitlab::GroupHierarchy.new(groups.search(params[:filter])) @@ -20,5 +21,6 @@ module GroupTree render json: serializer.represent(@groups) end end + # rubocop:enable Gitlab/ModuleWithInstanceVariables end end diff --git a/app/controllers/concerns/issuable_actions.rb b/app/controllers/concerns/issuable_actions.rb index 744e448e8df..c3013884369 100644 --- a/app/controllers/concerns/issuable_actions.rb +++ b/app/controllers/concerns/issuable_actions.rb @@ -17,15 +17,15 @@ module IssuableActions end def update - @issuable = update_service.execute(issuable) + @issuable = update_service.execute(issuable) # rubocop:disable Gitlab/ModuleWithInstanceVariables respond_to do |format| format.html do - recaptcha_check_with_fallback { render :edit } + recaptcha_check_if_spammable { render :edit } end format.json do - render_entity_json + recaptcha_check_if_spammable(false) { render_entity_json } end end @@ -80,10 +80,16 @@ module IssuableActions private + def recaptcha_check_if_spammable(should_redirect = true, &block) + return yield unless issuable.is_a? Spammable + + recaptcha_check_with_fallback(should_redirect, &block) + end + def render_conflict_response respond_to do |format| format.html do - @conflict = true + @conflict = true # rubocop:disable Gitlab/ModuleWithInstanceVariables render :edit end @@ -98,7 +104,7 @@ module IssuableActions end def labels - @labels ||= LabelsFinder.new(current_user, project_id: @project.id).execute + @labels ||= LabelsFinder.new(current_user, project_id: @project.id).execute # rubocop:disable Gitlab/ModuleWithInstanceVariables end def authorize_destroy_issuable! @@ -108,7 +114,7 @@ module IssuableActions end def authorize_admin_issuable! - unless can?(current_user, :"admin_#{resource_name}", @project) + unless can?(current_user, :"admin_#{resource_name}", @project) # rubocop:disable Gitlab/ModuleWithInstanceVariables return access_denied! end end @@ -142,6 +148,7 @@ module IssuableActions @resource_name ||= controller_name.singularize end + # rubocop:disable Gitlab/ModuleWithInstanceVariables def render_entity_json if @issuable.valid? render json: serializer.represent(@issuable) @@ -149,6 +156,7 @@ module IssuableActions render json: { errors: @issuable.errors.full_messages }, status: :unprocessable_entity end end + # rubocop:enable Gitlab/ModuleWithInstanceVariables def serializer raise NotImplementedError @@ -159,6 +167,6 @@ module IssuableActions end def parent - @project || @group + @project || @group # rubocop:disable Gitlab/ModuleWithInstanceVariables end end diff --git a/app/controllers/concerns/issuable_collections.rb b/app/controllers/concerns/issuable_collections.rb index f3c9251225f..b25e753a5ad 100644 --- a/app/controllers/concerns/issuable_collections.rb +++ b/app/controllers/concerns/issuable_collections.rb @@ -2,6 +2,7 @@ module IssuableCollections extend ActiveSupport::Concern include SortingHelper include Gitlab::IssuableMetadata + include Gitlab::Utils::StrongMemoize included do helper_method :finder @@ -9,6 +10,7 @@ module IssuableCollections private + # rubocop:disable Gitlab/ModuleWithInstanceVariables def set_issuables_index @issuables = issuables_collection @issuables = @issuables.page(params[:page]) @@ -33,6 +35,7 @@ module IssuableCollections @users.push(author) if author end end + # rubocop:enable Gitlab/ModuleWithInstanceVariables def issuables_collection finder.execute.preload(preload_for_collection) @@ -41,7 +44,7 @@ module IssuableCollections def redirect_out_of_range(total_pages) return false if total_pages.zero? - out_of_range = @issuables.current_page > total_pages + out_of_range = @issuables.current_page > total_pages # rubocop:disable Gitlab/ModuleWithInstanceVariables if out_of_range redirect_to(url_for(params.merge(page: total_pages, only_path: true))) @@ -51,7 +54,7 @@ module IssuableCollections end def issuable_page_count - page_count_for_relation(@issuables, finder.row_count) + page_count_for_relation(@issuables, finder.row_count) # rubocop:disable Gitlab/ModuleWithInstanceVariables end def page_count_for_relation(relation, row_count) @@ -66,6 +69,7 @@ module IssuableCollections finder_class.new(current_user, filter_params) end + # rubocop:disable Gitlab/ModuleWithInstanceVariables def filter_params set_sort_order_from_cookie set_default_state @@ -90,6 +94,7 @@ module IssuableCollections @filter_params.permit(IssuableFinder::VALID_PARAMS) end + # rubocop:enable Gitlab/ModuleWithInstanceVariables def set_default_state params[:state] = 'opened' if params[:state].blank? @@ -129,9 +134,9 @@ module IssuableCollections end def finder - return @finder if defined?(@finder) - - @finder = issuable_finder_for(@finder_type) + strong_memoize(:finder) do + issuable_finder_for(@finder_type) # rubocop:disable Gitlab/ModuleWithInstanceVariables + end end def collection_type diff --git a/app/controllers/concerns/issues_action.rb b/app/controllers/concerns/issues_action.rb index ad594903331..d4cccbe6442 100644 --- a/app/controllers/concerns/issues_action.rb +++ b/app/controllers/concerns/issues_action.rb @@ -2,6 +2,7 @@ module IssuesAction extend ActiveSupport::Concern include IssuableCollections + # rubocop:disable Gitlab/ModuleWithInstanceVariables def issues @finder_type = IssuesFinder @label = finder.labels.first @@ -17,4 +18,5 @@ module IssuesAction format.atom { render layout: 'xml.atom' } end end + # rubocop:enable Gitlab/ModuleWithInstanceVariables end diff --git a/app/controllers/concerns/members_presentation.rb b/app/controllers/concerns/members_presentation.rb new file mode 100644 index 00000000000..c0622516fd3 --- /dev/null +++ b/app/controllers/concerns/members_presentation.rb @@ -0,0 +1,11 @@ +module MembersPresentation + extend ActiveSupport::Concern + + def present_members(members) + Gitlab::View::Presenter::Factory.new( + members, + current_user: current_user, + presenter_class: MembersPresenter + ).fabricate! + end +end diff --git a/app/controllers/concerns/merge_requests_action.rb b/app/controllers/concerns/merge_requests_action.rb index 8b569a01afd..4d44df3bba9 100644 --- a/app/controllers/concerns/merge_requests_action.rb +++ b/app/controllers/concerns/merge_requests_action.rb @@ -2,6 +2,7 @@ module MergeRequestsAction extend ActiveSupport::Concern include IssuableCollections + # rubocop:disable Gitlab/ModuleWithInstanceVariables def merge_requests @finder_type = MergeRequestsFinder @label = finder.labels.first @@ -10,6 +11,7 @@ module MergeRequestsAction @issuable_meta_data = issuable_meta_data(@merge_requests, collection_type) end + # rubocop:enable Gitlab/ModuleWithInstanceVariables private diff --git a/app/controllers/concerns/milestone_actions.rb b/app/controllers/concerns/milestone_actions.rb index 081f3336780..d92cf8b4894 100644 --- a/app/controllers/concerns/milestone_actions.rb +++ b/app/controllers/concerns/milestone_actions.rb @@ -6,7 +6,7 @@ module MilestoneActions format.html { redirect_to milestone_redirect_path } format.json do render json: tabs_json("shared/milestones/_merge_requests_tab", { - merge_requests: @milestone.sorted_merge_requests, + merge_requests: @milestone.sorted_merge_requests, # rubocop:disable Gitlab/ModuleWithInstanceVariables show_project_name: true }) end @@ -18,7 +18,7 @@ module MilestoneActions format.html { redirect_to milestone_redirect_path } format.json do render json: tabs_json("shared/milestones/_participants_tab", { - users: @milestone.participants + users: @milestone.participants # rubocop:disable Gitlab/ModuleWithInstanceVariables }) end end @@ -29,7 +29,7 @@ module MilestoneActions format.html { redirect_to milestone_redirect_path } format.json do render json: tabs_json("shared/milestones/_labels_tab", { - labels: @milestone.labels + labels: @milestone.labels # rubocop:disable Gitlab/ModuleWithInstanceVariables }) end end @@ -43,6 +43,7 @@ module MilestoneActions } end + # rubocop:disable Gitlab/ModuleWithInstanceVariables def milestone_redirect_path if @project project_milestone_path(@project, @milestone) @@ -52,4 +53,5 @@ module MilestoneActions dashboard_milestone_path(@milestone.safe_title, title: @milestone.title) end end + # rubocop:enable Gitlab/ModuleWithInstanceVariables end diff --git a/app/controllers/concerns/notes_actions.rb b/app/controllers/concerns/notes_actions.rb index be2e1b47feb..e82a5650935 100644 --- a/app/controllers/concerns/notes_actions.rb +++ b/app/controllers/concerns/notes_actions.rb @@ -1,5 +1,6 @@ module NotesActions include RendersNotes + include Gitlab::Utils::StrongMemoize extend ActiveSupport::Concern included do @@ -30,6 +31,7 @@ module NotesActions render json: notes_json end + # rubocop:disable Gitlab/ModuleWithInstanceVariables def create create_params = note_params.merge( merge_request_diff_head_sha: params[:merge_request_diff_head_sha], @@ -47,7 +49,9 @@ module NotesActions format.html { redirect_back_or_default } end end + # rubocop:enable Gitlab/ModuleWithInstanceVariables + # rubocop:disable Gitlab/ModuleWithInstanceVariables def update @note = Notes::UpdateService.new(project, current_user, note_params).execute(note) @@ -60,6 +64,7 @@ module NotesActions format.html { redirect_back_or_default } end end + # rubocop:enable Gitlab/ModuleWithInstanceVariables def destroy if note.editable? @@ -138,7 +143,7 @@ module NotesActions end else template = "discussions/_diff_discussion" - @fresh_discussion = true + @fresh_discussion = true # rubocop:disable Gitlab/ModuleWithInstanceVariables locals = { discussions: [discussion], on_image: on_image } end @@ -191,7 +196,7 @@ module NotesActions end def noteable - @noteable ||= notes_finder.target || @note&.noteable + @noteable ||= notes_finder.target || @note&.noteable # rubocop:disable Gitlab/ModuleWithInstanceVariables end def require_noteable! @@ -211,20 +216,21 @@ module NotesActions end def note_project - return @note_project if defined?(@note_project) - return nil unless project + strong_memoize(:note_project) do + return nil unless project - note_project_id = params[:note_project_id] + note_project_id = params[:note_project_id] - @note_project = - if note_project_id.present? - Project.find(note_project_id) - else - project - end + the_project = + if note_project_id.present? + Project.find(note_project_id) + else + project + end - return access_denied! unless can?(current_user, :create_note, @note_project) + return access_denied! unless can?(current_user, :create_note, the_project) - @note_project + the_project + end end end diff --git a/app/controllers/concerns/oauth_applications.rb b/app/controllers/concerns/oauth_applications.rb index 9849aa93fa6..f0a68f23566 100644 --- a/app/controllers/concerns/oauth_applications.rb +++ b/app/controllers/concerns/oauth_applications.rb @@ -14,6 +14,6 @@ module OauthApplications end def load_scopes - @scopes = Doorkeeper.configuration.scopes + @scopes ||= Doorkeeper.configuration.scopes end end diff --git a/app/controllers/concerns/preview_markdown.rb b/app/controllers/concerns/preview_markdown.rb index e9b9e9b38bc..90bb7a87b45 100644 --- a/app/controllers/concerns/preview_markdown.rb +++ b/app/controllers/concerns/preview_markdown.rb @@ -1,6 +1,7 @@ module PreviewMarkdown extend ActiveSupport::Concern + # rubocop:disable Gitlab/ModuleWithInstanceVariables def preview_markdown result = PreviewMarkdownService.new(@project, current_user, params).execute @@ -20,4 +21,5 @@ module PreviewMarkdown } } end + # rubocop:enable Gitlab/ModuleWithInstanceVariables end diff --git a/app/controllers/concerns/renders_commits.rb b/app/controllers/concerns/renders_commits.rb index bb2c1dfa00a..fb41dc1e8a8 100644 --- a/app/controllers/concerns/renders_commits.rb +++ b/app/controllers/concerns/renders_commits.rb @@ -1,6 +1,6 @@ module RendersCommits def prepare_commits_for_rendering(commits) - Banzai::CommitRenderer.render(commits, @project, current_user) + Banzai::CommitRenderer.render(commits, @project, current_user) # rubocop:disable Gitlab/ModuleWithInstanceVariables commits end diff --git a/app/controllers/concerns/renders_notes.rb b/app/controllers/concerns/renders_notes.rb index 824ad06465c..e7ef297879f 100644 --- a/app/controllers/concerns/renders_notes.rb +++ b/app/controllers/concerns/renders_notes.rb @@ -1,4 +1,5 @@ module RendersNotes + # rubocop:disable Gitlab/ModuleWithInstanceVariables def prepare_notes_for_rendering(notes, noteable = nil) preload_noteable_for_regular_notes(notes) preload_max_access_for_authors(notes, @project) @@ -7,6 +8,7 @@ module RendersNotes notes end + # rubocop:enable Gitlab/ModuleWithInstanceVariables private diff --git a/app/controllers/concerns/service_params.rb b/app/controllers/concerns/service_params.rb index be2e6c7f193..3d61458c064 100644 --- a/app/controllers/concerns/service_params.rb +++ b/app/controllers/concerns/service_params.rb @@ -66,7 +66,7 @@ module ServiceParams FILTER_BLANK_PARAMS = [:password].freeze def service_params - dynamic_params = @service.event_channel_names + @service.event_names + dynamic_params = @service.event_channel_names + @service.event_names # rubocop:disable Gitlab/ModuleWithInstanceVariables service_params = params.permit(:id, service: ALLOWED_PARAMS_CE + dynamic_params) if service_params[:service].is_a?(Hash) diff --git a/app/controllers/concerns/snippets_actions.rb b/app/controllers/concerns/snippets_actions.rb index ffea712a833..9095cc7f783 100644 --- a/app/controllers/concerns/snippets_actions.rb +++ b/app/controllers/concerns/snippets_actions.rb @@ -4,6 +4,7 @@ module SnippetsActions def edit end + # rubocop:disable Gitlab/ModuleWithInstanceVariables def raw disposition = params[:inline] == 'false' ? 'attachment' : 'inline' @@ -14,6 +15,7 @@ module SnippetsActions filename: @snippet.sanitized_file_name ) end + # rubocop:enable Gitlab/ModuleWithInstanceVariables private diff --git a/app/controllers/concerns/spammable_actions.rb b/app/controllers/concerns/spammable_actions.rb index ada0dde87fb..922aa58a00f 100644 --- a/app/controllers/concerns/spammable_actions.rb +++ b/app/controllers/concerns/spammable_actions.rb @@ -2,6 +2,7 @@ module SpammableActions extend ActiveSupport::Concern include Recaptcha::Verify + include Gitlab::Utils::StrongMemoize included do before_action :authorize_submit_spammable!, only: :mark_as_spam @@ -18,13 +19,13 @@ module SpammableActions private def ensure_spam_config_loaded! - return @spam_config_loaded if defined?(@spam_config_loaded) - - @spam_config_loaded = Gitlab::Recaptcha.load_configurations! + strong_memoize(:spam_config_loaded) do + Gitlab::Recaptcha.load_configurations! + end end - def recaptcha_check_with_fallback(&fallback) - if spammable.valid? + def recaptcha_check_with_fallback(should_redirect = true, &fallback) + if should_redirect && spammable.valid? redirect_to spammable_path elsif render_recaptcha? ensure_spam_config_loaded! @@ -33,7 +34,18 @@ module SpammableActions flash[:alert] = 'There was an error with the reCAPTCHA. Please solve the reCAPTCHA again.' end - render :verify + respond_to do |format| + format.html do + render :verify + end + + format.json do + locals = { spammable: spammable, script: false, has_submit: false } + recaptcha_html = render_to_string(partial: 'shared/recaptcha_form', formats: :html, locals: locals) + + render json: { recaptcha_html: recaptcha_html } + end + end else yield end diff --git a/app/controllers/concerns/toggle_subscription_action.rb b/app/controllers/concerns/toggle_subscription_action.rb index 92cb534343e..776583579e8 100644 --- a/app/controllers/concerns/toggle_subscription_action.rb +++ b/app/controllers/concerns/toggle_subscription_action.rb @@ -12,7 +12,7 @@ module ToggleSubscriptionAction private def subscribable_project - @project || raise(NotImplementedError) + @project ||= raise(NotImplementedError) end def subscribable_resource diff --git a/app/controllers/concerns/with_performance_bar.rb b/app/controllers/concerns/with_performance_bar.rb index ed253042701..230bbe4b1aa 100644 --- a/app/controllers/concerns/with_performance_bar.rb +++ b/app/controllers/concerns/with_performance_bar.rb @@ -6,6 +6,7 @@ module WithPerformanceBar end def peek_enabled? + return true if Rails.env.development? return false unless Gitlab::PerformanceBar.enabled?(current_user) if RequestStore.active? diff --git a/app/controllers/groups/group_members_controller.rb b/app/controllers/groups/group_members_controller.rb index 8fc234a62b1..21e77431176 100644 --- a/app/controllers/groups/group_members_controller.rb +++ b/app/controllers/groups/group_members_controller.rb @@ -1,5 +1,6 @@ class Groups::GroupMembersController < Groups::ApplicationController include MembershipActions + include MembersPresentation include SortingHelper # Authorize @@ -14,15 +15,17 @@ class Groups::GroupMembersController < Groups::ApplicationController @members = @members.search(params[:search]) if params[:search].present? @members = @members.sort(@sort) @members = @members.page(params[:page]).per(50) - @members.includes(:user) + @members = present_members(@members.includes(:user)) - @requesters = AccessRequestsFinder.new(@group).execute(current_user) + @requesters = present_members( + AccessRequestsFinder.new(@group).execute(current_user)) @group_member = @group.group_members.new end def update - @group_member = @group.group_members.find(params[:id]) + @group_member = @group.members_and_requesters.find(params[:id]) + .present(current_user: current_user) return render_403 unless can?(current_user, :update_group_member, @group_member) diff --git a/app/controllers/health_controller.rb b/app/controllers/health_controller.rb index 98c2aaa3526..a931b456a93 100644 --- a/app/controllers/health_controller.rb +++ b/app/controllers/health_controller.rb @@ -1,5 +1,5 @@ class HealthController < ActionController::Base - protect_from_forgery with: :exception + protect_from_forgery with: :exception, except: :storage_check include RequiresWhitelistedMonitoringClient CHECKS = [ @@ -23,6 +23,15 @@ class HealthController < ActionController::Base render_check_results(results) end + def storage_check + results = Gitlab::Git::Storage::Checker.check_all + + render json: { + check_interval: Gitlab::CurrentSettings.current_application_settings.circuitbreaker_check_interval, + results: results + } + end + private def render_check_results(results) diff --git a/app/controllers/profiles/personal_access_tokens_controller.rb b/app/controllers/profiles/personal_access_tokens_controller.rb index 6d9873e38df..346eab4ba19 100644 --- a/app/controllers/profiles/personal_access_tokens_controller.rb +++ b/app/controllers/profiles/personal_access_tokens_controller.rb @@ -8,7 +8,7 @@ class Profiles::PersonalAccessTokensController < Profiles::ApplicationController @personal_access_token = finder.build(personal_access_token_params) if @personal_access_token.save - flash[:personal_access_token] = @personal_access_token.token + PersonalAccessToken.redis_store!(current_user.id, @personal_access_token.token) redirect_to profile_personal_access_tokens_path, notice: "Your new personal access token has been created." else set_index_vars @@ -43,5 +43,7 @@ class Profiles::PersonalAccessTokensController < Profiles::ApplicationController @inactive_personal_access_tokens = finder(state: 'inactive').execute @active_personal_access_tokens = finder(state: 'active').execute.order(:expires_at) + + @new_personal_access_token = PersonalAccessToken.redis_getdel(current_user.id) end end diff --git a/app/controllers/projects/clusters_controller.rb b/app/controllers/projects/clusters_controller.rb index 0907daacbc3..4a7879db313 100644 --- a/app/controllers/projects/clusters_controller.rb +++ b/app/controllers/projects/clusters_controller.rb @@ -8,11 +8,8 @@ class Projects::ClustersController < Projects::ApplicationController STATUS_POLLING_INTERVAL = 10_000 def index - @scope = params[:scope] || 'all' - @clusters = ClustersFinder.new(project, current_user, @scope).execute.page(params[:page]) - @active_count = ClustersFinder.new(project, current_user, :active).execute.count - @inactive_count = ClustersFinder.new(project, current_user, :inactive).execute.count - @all_count = @active_count + @inactive_count + clusters = ClustersFinder.new(project, current_user, :all).execute + @clusters = clusters.page(params[:page]).per(20) end def new diff --git a/app/controllers/projects/jobs_controller.rb b/app/controllers/projects/jobs_controller.rb index 1c4c09c772f..4865ec3dfe5 100644 --- a/app/controllers/projects/jobs_controller.rb +++ b/app/controllers/projects/jobs_controller.rb @@ -110,7 +110,7 @@ class Projects::JobsController < Projects::ApplicationController def erase if @build.erase(erased_by: current_user) redirect_to project_job_path(project, @build), - notice: "Build has been successfully erased!" + notice: "Job has been successfully erased!" else respond_422 end diff --git a/app/controllers/projects/merge_requests/creations_controller.rb b/app/controllers/projects/merge_requests/creations_controller.rb index 1511fc08c89..dc524b790a0 100644 --- a/app/controllers/projects/merge_requests/creations_controller.rb +++ b/app/controllers/projects/merge_requests/creations_controller.rb @@ -9,7 +9,10 @@ class Projects::MergeRequests::CreationsController < Projects::MergeRequests::Ap before_action :build_merge_request, except: [:create] def new - define_new_vars + # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/40934 + Gitlab::GitalyClient.allow_n_plus_1_calls do + define_new_vars + end end def create diff --git a/app/controllers/projects/notes_controller.rb b/app/controllers/projects/notes_controller.rb index 627cb2bd93c..5940fae8dd0 100644 --- a/app/controllers/projects/notes_controller.rb +++ b/app/controllers/projects/notes_controller.rb @@ -11,7 +11,7 @@ class Projects::NotesController < Projects::ApplicationController # 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) + # but fails for tests because there is a :create method on FactoryBot (one of the ancestors) # # see https://github.com/rails/rails/blob/v4.2.7/actionpack/lib/abstract_controller/base.rb#L78 # diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb index d925dcd21ff..d7372beb9d3 100644 --- a/app/controllers/projects/project_members_controller.rb +++ b/app/controllers/projects/project_members_controller.rb @@ -1,5 +1,6 @@ class Projects::ProjectMembersController < Projects::ApplicationController include MembershipActions + include MembersPresentation include SortingHelper # Authorize @@ -20,13 +21,14 @@ class Projects::ProjectMembersController < Projects::ApplicationController @group_links = @group_links.where(group_id: @project.invited_groups.search(params[:search]).select(:id)) end - @project_members = @project_members.sort(@sort).page(params[:page]) - @requesters = AccessRequestsFinder.new(@project).execute(current_user) + @project_members = present_members(@project_members.sort(@sort).page(params[:page])) + @requesters = present_members(AccessRequestsFinder.new(@project).execute(current_user)) @project_member = @project.project_members.new end def update - @project_member = @project.project_members.find(params[:id]) + @project_member = @project.members_and_requesters.find(params[:id]) + .present(current_user: current_user) return render_403 unless can?(current_user, :update_project_member, @project_member) diff --git a/app/controllers/projects/tree_controller.rb b/app/controllers/projects/tree_controller.rb index f3719059f88..f752a46f828 100644 --- a/app/controllers/projects/tree_controller.rb +++ b/app/controllers/projects/tree_controller.rb @@ -26,6 +26,7 @@ class Projects::TreeController < Projects::ApplicationController respond_to do |format| format.html do + lfs_blob_ids @last_commit = @repository.last_commit_for_path(@commit.id, @tree.path) || @commit end diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 3882fa4791d..6f609348402 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -9,6 +9,7 @@ class ProjectsController < Projects::ApplicationController before_action :repository, except: [:index, :new, :create] before_action :assign_ref_vars, only: [:show], if: :repo_exists? before_action :tree, only: [:show], if: [:repo_exists?, :project_view_files?] + before_action :lfs_blob_ids, only: [:show], if: [:repo_exists?, :project_view_files?] before_action :project_export_enabled, only: [:export, :download_export, :remove_export, :generate_new_export] # Authorize @@ -272,7 +273,7 @@ class ProjectsController < Projects::ApplicationController render 'projects/empty' if @project.empty_repo? else - if @project.wiki_enabled? + if can?(current_user, :read_wiki, @project) @project_wiki = @project.wiki @wiki_home = @project_wiki.find_page('home', params[:version_id]) elsif @project.feature_available?(:issues, current_user) diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb index dccde46fa33..b12ea760668 100644 --- a/app/helpers/application_settings_helper.rb +++ b/app/helpers/application_settings_helper.rb @@ -124,17 +124,6 @@ module ApplicationSettingsHelper _('The number of attempts GitLab will make to access a storage.') end - def circuitbreaker_backoff_threshold_help_text - _("The number of failures after which GitLab will start temporarily "\ - "disabling access to a storage shard on a host") - end - - def circuitbreaker_failure_wait_time_help_text - _("When access to a storage fails. GitLab will prevent access to the "\ - "storage for the time specified here. This allows the filesystem to "\ - "recover. Repositories on failing shards are temporarly unavailable") - end - def circuitbreaker_failure_reset_time_help_text _("The time in seconds GitLab will keep failure information. When no "\ "failures occur during this time, information about the mount is reset.") @@ -145,6 +134,11 @@ module ApplicationSettingsHelper "timeout error will be raised.") end + def circuitbreaker_check_interval_help_text + _("The time in seconds between storage checks. When a previous check did "\ + "complete yet, GitLab will skip a check.") + end + def visible_attributes [ :admin_notification_email, @@ -154,10 +148,9 @@ module ApplicationSettingsHelper :akismet_enabled, :auto_devops_enabled, :circuitbreaker_access_retries, - :circuitbreaker_backoff_threshold, + :circuitbreaker_check_interval, :circuitbreaker_failure_count_threshold, :circuitbreaker_failure_reset_time, - :circuitbreaker_failure_wait_time, :circuitbreaker_storage_timeout, :clientside_sentry_dsn, :clientside_sentry_enabled, diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb index 18075ee8be7..556ed233ccf 100644 --- a/app/helpers/blob_helper.rb +++ b/app/helpers/blob_helper.rb @@ -118,20 +118,24 @@ module BlobHelper icon("#{file_type_icon_class('file', mode, name)} fw") end - def blob_raw_path + def blob_raw_url(only_path: false) if @build && @entry - raw_project_job_artifacts_path(@project, @build, path: @entry.path) + raw_project_job_artifacts_url(@project, @build, path: @entry.path, only_path: only_path) elsif @snippet if @snippet.project_id - raw_project_snippet_path(@project, @snippet) + raw_project_snippet_url(@project, @snippet, only_path: only_path) else - raw_snippet_path(@snippet) + raw_snippet_url(@snippet, only_path: only_path) end elsif @blob - project_raw_path(@project, @id) + project_raw_url(@project, @id, only_path: only_path) end end + def blob_raw_path + blob_raw_url(only_path: true) + end + # SVGs can contain malicious JavaScript; only include whitelisted # elements and attributes. Note that this whitelist is by no means complete # and may omit some elements. diff --git a/app/helpers/builds_helper.rb b/app/helpers/builds_helper.rb index aa3a9a055a0..4ec63fdaffc 100644 --- a/app/helpers/builds_helper.rb +++ b/app/helpers/builds_helper.rb @@ -20,8 +20,7 @@ module BuildsHelper def javascript_build_options { - page_url: project_job_url(@project, @build), - build_url: project_job_url(@project, @build, :json), + page_path: project_job_path(@project, @build), build_status: @build.status, build_stage: @build.stage, log_state: '' diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb index e82136f0177..1ce487e6592 100644 --- a/app/helpers/diff_helper.rb +++ b/app/helpers/diff_helper.rb @@ -104,15 +104,23 @@ module DiffHelper ].join(' ').html_safe end - def diff_file_blob_raw_path(diff_file) - project_raw_path(@project, tree_join(diff_file.content_sha, diff_file.file_path)) + def diff_file_blob_raw_url(diff_file, only_path: false) + project_raw_url(@project, tree_join(diff_file.content_sha, diff_file.file_path), only_path: only_path) end - def diff_file_old_blob_raw_path(diff_file) + def diff_file_old_blob_raw_url(diff_file, only_path: false) sha = diff_file.old_content_sha return unless sha - project_raw_path(@project, tree_join(diff_file.old_content_sha, diff_file.old_path)) + project_raw_url(@project, tree_join(diff_file.old_content_sha, diff_file.old_path), only_path: only_path) + end + + def diff_file_blob_raw_path(diff_file) + diff_file_blob_raw_url(diff_file, only_path: true) + end + + def diff_file_old_blob_raw_path(diff_file) + diff_file_old_blob_raw_url(diff_file, only_path: true) end def diff_file_html_data(project, diff_file_path, diff_commit_id) diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb index 212cdbb8157..0f110bd25c5 100644 --- a/app/helpers/issues_helper.rb +++ b/app/helpers/issues_helper.rb @@ -74,7 +74,7 @@ module IssuesHelper elsif item.try(:merged?) 'status-box-merged' elsif item.closed? - 'status-box-closed' + 'status-box-mr-closed' elsif item.try(:upcoming?) 'status-box-upcoming' else diff --git a/app/helpers/labels_helper.rb b/app/helpers/labels_helper.rb index e1ba7898ee6..c1c19062c91 100644 --- a/app/helpers/labels_helper.rb +++ b/app/helpers/labels_helper.rb @@ -1,6 +1,13 @@ module LabelsHelper include ActionView::Helpers::TagHelper + def show_label_issuables_link?(label, issuables_type, current_user: nil, project: nil) + return true if label.is_a?(GroupLabel) + return true unless project + + project.feature_available?(issuables_type, current_user) + end + # Link to a Label # # label - Label object to link to diff --git a/app/helpers/members_helper.rb b/app/helpers/members_helper.rb index 41d471cc92f..a3129cac2b1 100644 --- a/app/helpers/members_helper.rb +++ b/app/helpers/members_helper.rb @@ -1,11 +1,4 @@ module MembersHelper - # Returns a `<action>_<source>_member` association, e.g.: - # - admin_project_member, update_project_member, destroy_project_member - # - admin_group_member, update_group_member, destroy_group_member - def action_member_permission(action, member) - "#{action}_#{member.type.underscore}".to_sym - end - def remove_member_message(member, user: nil) user = current_user if defined?(current_user) diff --git a/app/helpers/preferences_helper.rb b/app/helpers/preferences_helper.rb index 8e822ed0ea2..aaee6eaeedd 100644 --- a/app/helpers/preferences_helper.rb +++ b/app/helpers/preferences_helper.rb @@ -58,7 +58,7 @@ module PreferencesHelper user_view elsif user_view == "activity" "activity" - elsif @project.wiki_enabled? + elsif can?(current_user, :read_wiki, @project) "wiki" elsif @project.feature_available?(:issues, current_user) "projects/issues/issues" diff --git a/app/helpers/storage_health_helper.rb b/app/helpers/storage_health_helper.rb index 4d2180f7eee..b76c1228220 100644 --- a/app/helpers/storage_health_helper.rb +++ b/app/helpers/storage_health_helper.rb @@ -18,16 +18,12 @@ module StorageHealthHelper current_failures = circuit_breaker.failure_count translation_params = { number_of_failures: current_failures, - maximum_failures: maximum_failures, - number_of_seconds: circuit_breaker.failure_wait_time } + maximum_failures: maximum_failures } if circuit_breaker.circuit_broken? s_("%{number_of_failures} of %{maximum_failures} failures. GitLab will not "\ "retry automatically. Reset storage information when the problem is "\ "resolved.") % translation_params - elsif circuit_breaker.backing_off? - _("%{number_of_failures} of %{maximum_failures} failures. GitLab will "\ - "block access for %{number_of_seconds} seconds.") % translation_params else _("%{number_of_failures} of %{maximum_failures} failures. GitLab will "\ "allow access on the next attempt.") % translation_params diff --git a/app/mailers/emails/notes.rb b/app/mailers/emails/notes.rb index 77a82b895ce..50e17fe7717 100644 --- a/app/mailers/emails/notes.rb +++ b/app/mailers/emails/notes.rb @@ -5,7 +5,7 @@ module Emails @commit = @note.noteable @target_url = project_commit_url(*note_target_url_options) - mail_answer_thread(@commit, note_thread_options(recipient_id)) + mail_answer_note_thread(@commit, @note, note_thread_options(recipient_id)) end def note_issue_email(recipient_id, note_id) @@ -13,7 +13,7 @@ module Emails @issue = @note.noteable @target_url = project_issue_url(*note_target_url_options) - mail_answer_thread(@issue, note_thread_options(recipient_id)) + mail_answer_note_thread(@issue, @note, note_thread_options(recipient_id)) end def note_merge_request_email(recipient_id, note_id) @@ -21,7 +21,7 @@ module Emails @merge_request = @note.noteable @target_url = project_merge_request_url(*note_target_url_options) - mail_answer_thread(@merge_request, note_thread_options(recipient_id)) + mail_answer_note_thread(@merge_request, @note, note_thread_options(recipient_id)) end def note_snippet_email(recipient_id, note_id) @@ -29,7 +29,7 @@ module Emails @snippet = @note.noteable @target_url = project_snippet_url(*note_target_url_options) - mail_answer_thread(@snippet, note_thread_options(recipient_id)) + mail_answer_note_thread(@snippet, @note, note_thread_options(recipient_id)) end def note_personal_snippet_email(recipient_id, note_id) @@ -37,7 +37,7 @@ module Emails @snippet = @note.noteable @target_url = snippet_url(@note.noteable) - mail_answer_thread(@snippet, note_thread_options(recipient_id)) + mail_answer_note_thread(@snippet, @note, note_thread_options(recipient_id)) end private diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb index 9efabe3f44e..ec886e993c3 100644 --- a/app/mailers/notify.rb +++ b/app/mailers/notify.rb @@ -119,8 +119,8 @@ class Notify < BaseMailer headers['Reply-To'] = address fallback_reply_message_id = "<reply-#{reply_key}@#{Gitlab.config.gitlab.host}>".freeze - headers['References'] ||= '' - headers['References'] << ' ' << fallback_reply_message_id + headers['References'] ||= [] + headers['References'] << fallback_reply_message_id @reply_by_email = true end @@ -156,6 +156,18 @@ class Notify < BaseMailer mail_thread(model, headers) end + def mail_answer_note_thread(model, note, headers = {}) + headers['Message-ID'] = message_id(note) + headers['In-Reply-To'] = message_id(note.references.last) + headers['References'] = note.references.map { |ref| message_id(ref) } + + headers['X-GitLab-Discussion-ID'] = note.discussion.id if note.part_of_discussion? + + headers[:subject]&.prepend('Re: ') + + mail_thread(model, headers) + end + def reply_key @reply_key ||= SentNotification.reply_key end diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 3117c98c846..253e213af81 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -153,11 +153,10 @@ class ApplicationSetting < ActiveRecord::Base presence: true, numericality: { greater_than_or_equal_to: 0 } - validates :circuitbreaker_backoff_threshold, - :circuitbreaker_failure_count_threshold, - :circuitbreaker_failure_wait_time, + validates :circuitbreaker_failure_count_threshold, :circuitbreaker_failure_reset_time, :circuitbreaker_storage_timeout, + :circuitbreaker_check_interval, presence: true, numericality: { only_integer: true, greater_than_or_equal_to: 0 } @@ -165,13 +164,6 @@ class ApplicationSetting < ActiveRecord::Base presence: true, numericality: { only_integer: true, greater_than_or_equal_to: 1 } - validates_each :circuitbreaker_backoff_threshold do |record, attr, value| - if value.to_i >= record.circuitbreaker_failure_count_threshold - record.errors.add(attr, _("The circuitbreaker backoff threshold should be "\ - "lower than the failure count threshold")) - end - end - validates :gitaly_timeout_default, presence: true, numericality: { only_integer: true, greater_than_or_equal_to: 0 } diff --git a/app/models/blob.rb b/app/models/blob.rb index 29e762724e3..19ad110db58 100644 --- a/app/models/blob.rb +++ b/app/models/blob.rb @@ -77,9 +77,15 @@ class Blob < SimpleDelegator end def self.lazy(project, commit_id, path) - BatchLoader.for(commit_id: commit_id, path: path).batch do |items, loader| - project.repository.blobs_at(items.map(&:values)).each do |blob| - loader.call({ commit_id: blob.commit_id, path: blob.path }, blob) if blob + BatchLoader.for({ project: project, commit_id: commit_id, path: path }).batch do |items, loader| + items_by_project = items.group_by { |i| i[:project] } + + items_by_project.each do |project, items| + items = items.map { |i| i.values_at(:commit_id, :path) } + + project.repository.blobs_at(items).each do |blob| + loader.call({ project: blob.project, commit_id: blob.commit_id, path: blob.path }, blob) if blob + end end end end diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 85960f1b6bb..83fe23606d1 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -491,7 +491,6 @@ module Ci end def valid_dependency? - return false unless complete? return false if artifacts_expired? return false if erased? diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index eebbf7c4218..28f154581a9 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -228,6 +228,10 @@ module Ci statuses.select(:stage).distinct.count end + def total_size + statuses.count(:id) + end + def stages_names statuses.order(:stage_idx).distinct .pluck(:stage, :stage_idx).map(&:first) diff --git a/app/models/commit.rb b/app/models/commit.rb index 307e4fcedfe..13c31111134 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -52,6 +52,20 @@ class Commit diffs.reduce(0) { |sum, d| sum + Gitlab::Git::Util.count_lines(d.diff) } end + def order_by(collection:, order_by:, sort:) + return collection unless %w[email name commits].include?(order_by) + return collection unless %w[asc desc].include?(sort) + + collection.sort do |a, b| + operands = [a, b].tap { |o| o.reverse! if sort == 'desc' } + + attr1, attr2 = operands.first.public_send(order_by), operands.second.public_send(order_by) # rubocop:disable PublicSend + + # use case insensitive comparison for string values + order_by.in?(%w[email name]) ? attr1.casecmp(attr2) : attr1 <=> attr2 + end + end + # Truncate sha to 8 characters def truncate_sha(sha) sha[0..MIN_SHA_LENGTH] diff --git a/app/models/concerns/cache_markdown_field.rb b/app/models/concerns/cache_markdown_field.rb index 98776eab424..90ad644ce34 100644 --- a/app/models/concerns/cache_markdown_field.rb +++ b/app/models/concerns/cache_markdown_field.rb @@ -85,8 +85,7 @@ 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).present? && __send__(markdown_field).present? # rubocop:disable GitlabSecurity/PublicSend - return false unless cached + return false if cached_html_for(markdown_field).nil? && !__send__(markdown_field).nil? # rubocop:disable GitlabSecurity/PublicSend markdown_changed = attribute_changed?(markdown_field) || false html_changed = attribute_changed?(html_field) || false diff --git a/app/models/concerns/mentionable.rb b/app/models/concerns/mentionable.rb index b43eaeaeea0..c013e5a708f 100644 --- a/app/models/concerns/mentionable.rb +++ b/app/models/concerns/mentionable.rb @@ -44,13 +44,11 @@ module Mentionable end def all_references(current_user = nil, extractor: nil) - @extractors ||= {} - # Use custom extractor if it's passed in the function parameters. if extractor - @extractors[current_user] = extractor + extractors[current_user] = extractor else - extractor = @extractors[current_user] ||= Gitlab::ReferenceExtractor.new(project, current_user) + extractor = extractors[current_user] ||= Gitlab::ReferenceExtractor.new(project, current_user) extractor.reset_memoized_values end @@ -69,6 +67,10 @@ module Mentionable extractor end + def extractors + @extractors ||= {} + end + def mentioned_users(current_user = nil) all_references(current_user).users end diff --git a/app/models/concerns/milestoneish.rb b/app/models/concerns/milestoneish.rb index 7026f565706..fd6703831e4 100644 --- a/app/models/concerns/milestoneish.rb +++ b/app/models/concerns/milestoneish.rb @@ -103,9 +103,11 @@ module Milestoneish end def memoize_per_user(user, method_name) - @memoized ||= {} - @memoized[method_name] ||= {} - @memoized[method_name][user&.id] ||= yield + memoized_users[method_name][user&.id] ||= yield + end + + def memoized_users + @memoized_users ||= Hash.new { |h, k| h[k] = {} } end # override in a class that includes this module to get a faster query diff --git a/app/models/concerns/noteable.rb b/app/models/concerns/noteable.rb index 5d75b2aa6a3..86f28f30032 100644 --- a/app/models/concerns/noteable.rb +++ b/app/models/concerns/noteable.rb @@ -46,6 +46,7 @@ module Noteable notes.inc_relations_for_view.grouped_diff_discussions(*args) end + # rubocop:disable Gitlab/ModuleWithInstanceVariables def resolvable_discussions @resolvable_discussions ||= if defined?(@discussions) @@ -54,6 +55,7 @@ module Noteable discussion_notes.resolvable.discussions(self) end end + # rubocop:enable Gitlab/ModuleWithInstanceVariables def discussions_resolvable? resolvable_discussions.any?(&:resolvable?) diff --git a/app/models/concerns/participable.rb b/app/models/concerns/participable.rb index ce69fd34ac5..e48bc0be410 100644 --- a/app/models/concerns/participable.rb +++ b/app/models/concerns/participable.rb @@ -56,15 +56,17 @@ module Participable # # Returns an Array of User instances. def participants(current_user = nil) - @participants ||= Hash.new do |hash, user| - hash[user] = raw_participants(user) - end - - @participants[current_user] + all_participants[current_user] end private + def all_participants + @all_participants ||= Hash.new do |hash, user| + hash[user] = raw_participants(user) + end + end + def raw_participants(current_user = nil) current_user ||= author ext = Gitlab::ReferenceExtractor.new(project, current_user) diff --git a/app/models/concerns/relative_positioning.rb b/app/models/concerns/relative_positioning.rb index e961c97e337..835f26aa57b 100644 --- a/app/models/concerns/relative_positioning.rb +++ b/app/models/concerns/relative_positioning.rb @@ -52,7 +52,7 @@ module RelativePositioning # to its predecessor. This process will recursively move all the predecessors until we have a place if (after.relative_position - before.relative_position) < 2 before.move_before - @positionable_neighbours = [before] + @positionable_neighbours = [before] # rubocop:disable Gitlab/ModuleWithInstanceVariables end self.relative_position = position_between(before.relative_position, after.relative_position) @@ -65,7 +65,7 @@ module RelativePositioning if before.shift_after? issue_to_move = self.class.in_projects(project_ids).find_by!(relative_position: pos_after) issue_to_move.move_after - @positionable_neighbours = [issue_to_move] + @positionable_neighbours = [issue_to_move] # rubocop:disable Gitlab/ModuleWithInstanceVariables pos_after = issue_to_move.relative_position end @@ -80,7 +80,7 @@ module RelativePositioning if after.shift_before? issue_to_move = self.class.in_projects(project_ids).find_by!(relative_position: pos_before) issue_to_move.move_before - @positionable_neighbours = [issue_to_move] + @positionable_neighbours = [issue_to_move] # rubocop:disable Gitlab/ModuleWithInstanceVariables pos_before = issue_to_move.relative_position end @@ -132,6 +132,7 @@ module RelativePositioning end end + # rubocop:disable Gitlab/ModuleWithInstanceVariables def save_positionable_neighbours return unless @positionable_neighbours @@ -140,4 +141,5 @@ module RelativePositioning status end + # rubocop:enable Gitlab/ModuleWithInstanceVariables end diff --git a/app/models/concerns/resolvable_discussion.rb b/app/models/concerns/resolvable_discussion.rb index f006a271327..b6c7b6735b9 100644 --- a/app/models/concerns/resolvable_discussion.rb +++ b/app/models/concerns/resolvable_discussion.rb @@ -31,15 +31,11 @@ module ResolvableDiscussion end def resolvable? - return @resolvable if @resolvable.present? - - @resolvable = potentially_resolvable? && notes.any?(&:resolvable?) + @resolvable ||= potentially_resolvable? && notes.any?(&:resolvable?) end def resolved? - return @resolved if @resolved.present? - - @resolved = resolvable? && notes.none?(&:to_be_resolved?) + @resolved ||= resolvable? && notes.none?(&:to_be_resolved?) end def first_note @@ -49,13 +45,13 @@ module ResolvableDiscussion def first_note_to_resolve return unless resolvable? - @first_note_to_resolve ||= notes.find(&:to_be_resolved?) + @first_note_to_resolve ||= notes.find(&:to_be_resolved?) # rubocop:disable Gitlab/ModuleWithInstanceVariables end def last_resolved_note return unless resolved? - @last_resolved_note ||= resolved_notes.sort_by(&:resolved_at).last + @last_resolved_note ||= resolved_notes.sort_by(&:resolved_at).last # rubocop:disable Gitlab/ModuleWithInstanceVariables end def resolved_notes @@ -95,7 +91,7 @@ module ResolvableDiscussion yield(notes_relation) # Set the notes array to the updated notes - @notes = notes_relation.fresh.to_a + @notes = notes_relation.fresh.to_a # rubocop:disable Gitlab/ModuleWithInstanceVariables self.class.memoized_values.each do |var| instance_variable_set(:"@#{var}", nil) diff --git a/app/models/concerns/routable.rb b/app/models/concerns/routable.rb index 22fde2eb134..5c1cce98ad4 100644 --- a/app/models/concerns/routable.rb +++ b/app/models/concerns/routable.rb @@ -88,7 +88,7 @@ module Routable def full_name if route && route.name.present? - @full_name ||= route.name + @full_name ||= route.name # rubocop:disable Gitlab/ModuleWithInstanceVariables else update_route if persisted? @@ -112,7 +112,7 @@ module Routable def expires_full_path_cache RequestStore.delete(full_path_key) if RequestStore.active? - @full_path = nil + @full_path = nil # rubocop:disable Gitlab/ModuleWithInstanceVariables end def build_full_path @@ -127,7 +127,7 @@ module Routable def uncached_full_path if route && route.path.present? - @full_path ||= route.path + @full_path ||= route.path # rubocop:disable Gitlab/ModuleWithInstanceVariables else update_route if persisted? @@ -166,7 +166,7 @@ module Routable route || build_route(source: self) route.path = build_full_path route.name = build_full_name - @full_path = nil - @full_name = nil + @full_path = nil # rubocop:disable Gitlab/ModuleWithInstanceVariables + @full_name = nil # rubocop:disable Gitlab/ModuleWithInstanceVariables end end diff --git a/app/models/concerns/spammable.rb b/app/models/concerns/spammable.rb index 731d9b9a745..5e4274619c4 100644 --- a/app/models/concerns/spammable.rb +++ b/app/models/concerns/spammable.rb @@ -12,6 +12,7 @@ module Spammable attr_accessor :spam attr_accessor :spam_log + alias_method :spam?, :spam after_validation :check_for_spam, on: [:create, :update] @@ -34,10 +35,6 @@ module Spammable end end - def spam? - @spam - end - def check_for_spam error_msg = if Gitlab::Recaptcha.enabled? "Your #{spammable_entity_type} has been recognized as spam. "\ diff --git a/app/models/concerns/taskable.rb b/app/models/concerns/taskable.rb index 25e2d8ea24e..d07041c2fdf 100644 --- a/app/models/concerns/taskable.rb +++ b/app/models/concerns/taskable.rb @@ -39,7 +39,7 @@ module Taskable def task_list_items return [] if description.blank? - @task_list_items ||= Taskable.get_tasks(description) + @task_list_items ||= Taskable.get_tasks(description) # rubocop:disable Gitlab/ModuleWithInstanceVariables end def tasks diff --git a/app/models/concerns/time_trackable.rb b/app/models/concerns/time_trackable.rb index 9f403d96ed5..89fe6527647 100644 --- a/app/models/concerns/time_trackable.rb +++ b/app/models/concerns/time_trackable.rb @@ -21,6 +21,7 @@ module TimeTrackable has_many :timelogs, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent end + # rubocop:disable Gitlab/ModuleWithInstanceVariables def spend_time(options) @time_spent = options[:duration] @time_spent_user = options[:user] @@ -36,6 +37,7 @@ module TimeTrackable end end alias_method :spend_time=, :spend_time + # rubocop:enable Gitlab/ModuleWithInstanceVariables def total_time_spent timelogs.sum(:time_spent) @@ -52,9 +54,10 @@ module TimeTrackable private def reset_spent_time - timelogs.new(time_spent: total_time_spent * -1, user: @time_spent_user) + timelogs.new(time_spent: total_time_spent * -1, user: @time_spent_user) # rubocop:disable Gitlab/ModuleWithInstanceVariables end + # rubocop:disable Gitlab/ModuleWithInstanceVariables def add_or_subtract_spent_time timelogs.new( time_spent: time_spent, @@ -62,16 +65,19 @@ module TimeTrackable spent_at: @spent_at ) end + # rubocop:enable Gitlab/ModuleWithInstanceVariables def check_negative_time_spent return if time_spent.nil? || time_spent == :reset - # we need to cache the total time spent so multiple calls to #valid? - # doesn't give a false error - @original_total_time_spent ||= total_time_spent - - if time_spent < 0 && (time_spent.abs > @original_total_time_spent) + if time_spent < 0 && (time_spent.abs > original_total_time_spent) errors.add(:time_spent, 'Time to subtract exceeds the total time spent') end end + + # we need to cache the total time spent so multiple calls to #valid? + # doesn't give a false error + def original_total_time_spent + @original_total_time_spent ||= total_time_spent + end end diff --git a/app/models/event.rb b/app/models/event.rb index 0997b056c6a..6053594fab5 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -72,7 +72,7 @@ class Event < ActiveRecord::Base # We're using preload for "push_event_payload" as otherwise the association # is not always available (depending on the query being built). includes(:author, :project, project: :namespace) - .preload(:target, :push_event_payload) + .preload(:push_event_payload, target: :author) end scope :for_milestone_id, ->(milestone_id) { where(target_type: "Milestone", target_id: milestone_id) } diff --git a/app/models/identity.rb b/app/models/identity.rb index ff811e19f8a..99d99bc6deb 100644 --- a/app/models/identity.rb +++ b/app/models/identity.rb @@ -14,11 +14,11 @@ class Identity < ActiveRecord::Base end def ldap? - provider.starts_with?('ldap') + Gitlab::OAuth::Provider.ldap_provider?(provider) end def self.normalize_uid(provider, uid) - if provider.to_s.starts_with?('ldap') + if Gitlab::OAuth::Provider.ldap_provider?(provider) Gitlab::LDAP::Person.normalize_dn(uid) else uid.to_s diff --git a/app/models/issue.rb b/app/models/issue.rb index 33db197e612..dc64888b6fc 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -10,6 +10,9 @@ class Issue < ActiveRecord::Base include RelativePositioning include TimeTrackable include ThrottledTouch + include IgnorableColumn + + ignore_column :assignee_id, :branch_name DueDateStruct = Struct.new(:title, :name).freeze NoDueDate = DueDateStruct.new('No Due Date', '0').freeze diff --git a/app/models/member.rb b/app/models/member.rb index 2fe5fda985f..c47145667b5 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -4,6 +4,7 @@ class Member < ActiveRecord::Base include Importable include Expirable include Gitlab::Access + include Presentable attr_accessor :raw_invite_token diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 422f138c4ea..c39789b047d 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -8,6 +8,7 @@ class MergeRequest < ActiveRecord::Base include ManualInverseAssociation include EachBatch include ThrottledTouch + include Gitlab::Utils::StrongMemoize ignore_column :locked_at, :ref_fetched @@ -52,6 +53,7 @@ class MergeRequest < ActiveRecord::Base serialize :merge_params, Hash # rubocop:disable Cop/ActiveRecordSerialize after_create :ensure_merge_request_diff, unless: :importing? + after_update :clear_memoized_shas after_update :reload_diff_if_branch_changed # When this attribute is true some MR validation is ignored @@ -83,6 +85,14 @@ class MergeRequest < ActiveRecord::Base transition locked: :opened end + before_transition any => :opened do |merge_request| + merge_request.merge_jid = nil + + merge_request.run_after_commit do + UpdateHeadPipelineForMergeRequestWorker.perform_async(merge_request.id) + end + end + state :opened state :closed state :merged @@ -387,13 +397,17 @@ class MergeRequest < ActiveRecord::Base end def source_branch_head - return unless source_project - - source_project.repository.commit(source_branch_ref) if source_branch_ref + strong_memoize(:source_branch_head) do + if source_project && source_branch_ref + source_project.repository.commit(source_branch_ref) + end + end end def target_branch_head - target_project.repository.commit(target_branch_ref) + strong_memoize(:target_branch_head) do + target_project.repository.commit(target_branch_ref) + end end def branch_merge_base_commit @@ -525,6 +539,13 @@ class MergeRequest < ActiveRecord::Base end end + def clear_memoized_shas + @target_branch_sha = @source_branch_sha = nil + + clear_memoization(:source_branch_head) + clear_memoization(:target_branch_head) + end + def reload_diff_if_branch_changed if (source_branch_changed? || target_branch_changed?) && (source_branch_head && target_branch_head) @@ -866,11 +887,11 @@ class MergeRequest < ActiveRecord::Base def state_icon_name if merged? - "check" + "git-merge" elsif closed? - "times" + "close" else - "circle-o" + "issue-open-m" end end diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb index c37aa0a594b..e35de9b97ee 100644 --- a/app/models/merge_request_diff.rb +++ b/app/models/merge_request_diff.rb @@ -104,19 +104,19 @@ class MergeRequestDiff < ActiveRecord::Base def base_commit return unless base_commit_sha - project.commit(base_commit_sha) + project.commit_by(oid: base_commit_sha) end def start_commit return unless start_commit_sha - project.commit(start_commit_sha) + project.commit_by(oid: start_commit_sha) end def head_commit return unless head_commit_sha - project.commit(head_commit_sha) + project.commit_by(oid: head_commit_sha) end def commit_shas diff --git a/app/models/namespace.rb b/app/models/namespace.rb index 901dbf2ba69..0ff169d4531 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -40,6 +40,7 @@ class Namespace < ActiveRecord::Base namespace_path: true validate :nesting_level_allowed + validate :allowed_path_by_redirects delegate :name, to: :owner, allow_nil: true, prefix: true @@ -257,4 +258,14 @@ class Namespace < ActiveRecord::Base Namespace.where(id: descendants.select(:id)) .update_all(share_with_group_lock: true) end + + def allowed_path_by_redirects + return if path.nil? + + errors.add(:path, "#{path} has been taken before. Please use another one") if namespace_previously_created_with_same_path? + end + + def namespace_previously_created_with_same_path? + RedirectRoute.permanent.exists?(path: path) + end end diff --git a/app/models/note.rb b/app/models/note.rb index c4c2ab8e67d..184fbd5f5ae 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -360,6 +360,16 @@ class Note < ActiveRecord::Base end end + def references + refs = [noteable] + + if part_of_discussion? + refs += discussion.notes.take_while { |n| n.id < id } + end + + refs + end + def expire_etag_cache return unless noteable&.discussions_rendered_on_frontend? @@ -401,6 +411,9 @@ class Note < ActiveRecord::Base end noteable_object&.touch + + # We return the noteable object so we can re-use it in EE for ElasticSearch. + noteable_object end def banzai_render_context(field) diff --git a/app/models/personal_access_token.rb b/app/models/personal_access_token.rb index cfcb03138b7..063dc521324 100644 --- a/app/models/personal_access_token.rb +++ b/app/models/personal_access_token.rb @@ -3,6 +3,8 @@ class PersonalAccessToken < ActiveRecord::Base include TokenAuthenticatable add_authentication_token_field :token + REDIS_EXPIRY_TIME = 3.minutes + serialize :scopes, Array # rubocop:disable Cop/ActiveRecordSerialize belongs_to :user @@ -27,6 +29,21 @@ class PersonalAccessToken < ActiveRecord::Base !revoked? && !expired? end + def self.redis_getdel(user_id) + Gitlab::Redis::SharedState.with do |redis| + token = redis.get(redis_shared_state_key(user_id)) + redis.del(redis_shared_state_key(user_id)) + token + end + end + + def self.redis_store!(user_id, token) + Gitlab::Redis::SharedState.with do |redis| + redis.set(redis_shared_state_key(user_id), token, ex: REDIS_EXPIRY_TIME) + token + end + end + protected def validate_scopes @@ -38,4 +55,8 @@ class PersonalAccessToken < ActiveRecord::Base def set_default_scopes self.scopes = Gitlab::Auth::DEFAULT_SCOPES if self.scopes.empty? end + + def self.redis_shared_state_key(user_id) + "gitlab:personal_access_token:#{user_id}" + end end diff --git a/app/models/project.rb b/app/models/project.rb index 6ae15a0a50f..5183a216c53 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -659,7 +659,8 @@ class Project < ActiveRecord::Base end def import_started? - import? && import_status == 'started' + # import? does SQL work so only run it if it looks like there's an import running + import_status == 'started' && import? end def import_scheduled? @@ -1147,7 +1148,7 @@ class Project < ActiveRecord::Base def change_head(branch) if repository.branch_exists?(branch) repository.before_change_head - repository.write_ref('HEAD', "refs/heads/#{branch}") + repository.write_ref('HEAD', "refs/heads/#{branch}", force: true) repository.copy_gitattributes(branch) repository.after_change_head reload_default_branch diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb index 1c065e1ddbd..2be35b6ea9d 100644 --- a/app/models/project_services/jira_service.rb +++ b/app/models/project_services/jira_service.rb @@ -46,6 +46,8 @@ class JiraService < IssueTrackerService context_path: url.path, auth_type: :basic, read_timeout: 120, + use_cookies: true, + additional_cookies: ['OBBasicAuth=fromDialog'], use_ssl: url.scheme == 'https' } end diff --git a/app/models/redirect_route.rb b/app/models/redirect_route.rb index 31de204d824..20532527346 100644 --- a/app/models/redirect_route.rb +++ b/app/models/redirect_route.rb @@ -17,4 +17,32 @@ class RedirectRoute < ActiveRecord::Base where(wheres, path, "#{sanitize_sql_like(path)}/%") end + + scope :permanent, -> do + if column_permanent_exists? + where(permanent: true) + else + none + end + end + + scope :temporary, -> do + if column_permanent_exists? + where(permanent: [false, nil]) + else + all + end + end + + default_value_for :permanent, false + + def permanent=(value) + if self.class.column_permanent_exists? + super + end + end + + def self.column_permanent_exists? + ActiveRecord::Base.connection.column_exists?(:redirect_routes, :permanent) + end end diff --git a/app/models/repository.rb b/app/models/repository.rb index 751306188a0..552a354d1ce 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -19,6 +19,7 @@ class Repository attr_accessor :full_path, :disk_path, :project, :is_wiki delegate :ref_name_for_sha, to: :raw_repository + delegate :write_ref, to: :raw_repository CreateTreeError = Class.new(StandardError) @@ -237,11 +238,10 @@ class Repository # This will still fail if the file is corrupted (e.g. 0 bytes) begin - write_ref(keep_around_ref_name(sha), sha) - rescue Rugged::ReferenceError => ex - Rails.logger.error "Unable to create #{REF_KEEP_AROUND} reference for repository #{path}: #{ex}" - rescue Rugged::OSError => ex - raise unless ex.message =~ /Failed to create locked file/ && ex.message =~ /File exists/ + write_ref(keep_around_ref_name(sha), sha, force: true) + rescue Gitlab::Git::Repository::GitError => ex + # Necessary because https://gitlab.com/gitlab-org/gitlab-ce/issues/20156 + return true if ex.message =~ /Failed to create locked file/ && ex.message =~ /File exists/ Rails.logger.error "Unable to create #{REF_KEEP_AROUND} reference for repository #{path}: #{ex}" end @@ -251,10 +251,6 @@ class Repository ref_exists?(keep_around_ref_name(sha)) end - def write_ref(ref_path, sha) - rugged.references.create(ref_path, sha, force: true) - end - def diverging_commit_counts(branch) root_ref_hash = raw_repository.commit(root_ref).id cache.fetch(:"diverging_commit_counts_#{branch.name}") do @@ -690,7 +686,9 @@ class Repository def tags_sorted_by(value) case value - when 'name' + when 'name_asc' + VersionSorter.sort(tags) { |tag| tag.name } + when 'name_desc' VersionSorter.rsort(tags) { |tag| tag.name } when 'updated_desc' tags_sorted_by_committed_date.reverse @@ -701,10 +699,14 @@ class Repository end end - def contributors + # Params: + # + # order_by: name|email|commits + # sort: asc|desc default: 'asc' + def contributors(order_by: nil, sort: 'asc') commits = self.commits(nil, limit: 2000, offset: 0, skip_merges: true) - commits.group_by(&:author_email).map do |email, commits| + commits = commits.group_by(&:author_email).map do |email, commits| contributor = Gitlab::Contributor.new contributor.email = email @@ -718,6 +720,7 @@ class Repository contributor end + Commit.order_by(collection: commits, order_by: order_by, sort: sort) end def refs_contains_sha(ref_type, sha) @@ -931,7 +934,7 @@ class Repository def merge_base(first_commit_id, second_commit_id) first_commit_id = commit(first_commit_id).try(:id) || first_commit_id second_commit_id = commit(second_commit_id).try(:id) || second_commit_id - rugged.merge_base(first_commit_id, second_commit_id) + raw_repository.merge_base(first_commit_id, second_commit_id) rescue Rugged::ReferenceError nil end @@ -971,8 +974,7 @@ class Repository tmp_remote_name = true end - add_remote(remote_name, url) - set_remote_as_mirror(remote_name, refmap: refmap) + add_remote(remote_name, url, mirror_refmap: refmap) fetch_remote(remote_name, forced: forced) ensure remove_remote(remote_name) if tmp_remote_name @@ -995,7 +997,7 @@ class Repository end def create_ref(ref, ref_path) - raw_repository.write_ref(ref_path, ref) + write_ref(ref_path, ref) end def ls_files(ref) diff --git a/app/models/route.rb b/app/models/route.rb index 97e8a6ad9e9..7ba3ec06041 100644 --- a/app/models/route.rb +++ b/app/models/route.rb @@ -8,6 +8,8 @@ class Route < ActiveRecord::Base presence: true, uniqueness: { case_sensitive: false } + validate :ensure_permanent_paths + after_create :delete_conflicting_redirects after_update :delete_conflicting_redirects, if: :path_changed? after_update :create_redirect_for_old_path @@ -40,7 +42,7 @@ class Route < ActiveRecord::Base # We are not calling route.delete_conflicting_redirects here, in hopes # of avoiding deadlocks. The parent (self, in this method) already # called it, which deletes conflicts for all descendants. - route.create_redirect(old_path) if attributes[:path] + route.create_redirect(old_path, permanent: permanent_redirect?) if attributes[:path] end end end @@ -50,16 +52,30 @@ class Route < ActiveRecord::Base end def conflicting_redirects - RedirectRoute.matching_path_and_descendants(path) + RedirectRoute.temporary.matching_path_and_descendants(path) end - def create_redirect(path) - RedirectRoute.create(source: source, path: path) + def create_redirect(path, permanent: false) + RedirectRoute.create(source: source, path: path, permanent: permanent) end private def create_redirect_for_old_path - create_redirect(path_was) if path_changed? + create_redirect(path_was, permanent: permanent_redirect?) if path_changed? + end + + def permanent_redirect? + source_type != "Project" + end + + def ensure_permanent_paths + return if path.nil? + + errors.add(:path, "#{path} has been taken before. Please use another one") if conflicting_redirect_exists? + end + + def conflicting_redirect_exists? + RedirectRoute.permanent.matching_path_and_descendants(path).exists? end end diff --git a/app/models/user.rb b/app/models/user.rb index af1c36d9c93..51941f43919 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -315,6 +315,8 @@ class User < ActiveRecord::Base # # Returns an ActiveRecord::Relation. def search(query) + query = query.downcase + order = <<~SQL CASE WHEN users.name = %{query} THEN 0 @@ -324,8 +326,11 @@ class User < ActiveRecord::Base END SQL - fuzzy_search(query, [:name, :email, :username]) - .reorder(order % { query: ActiveRecord::Base.connection.quote(query) }, :name) + where( + fuzzy_arel_match(:name, query) + .or(fuzzy_arel_match(:username, query)) + .or(arel_table[:email].eq(query)) + ).reorder(order % { query: ActiveRecord::Base.connection.quote(query) }, :name) end # searches user by given pattern @@ -333,15 +338,17 @@ class User < ActiveRecord::Base # This method uses ILIKE on PostgreSQL and LIKE on MySQL. def search_with_secondary_emails(query) + query = query.downcase + email_table = Email.arel_table matched_by_emails_user_ids = email_table .project(email_table[:user_id]) - .where(Email.fuzzy_arel_match(:email, query)) + .where(email_table[:email].eq(query)) where( fuzzy_arel_match(:name, query) - .or(fuzzy_arel_match(:email, query)) .or(fuzzy_arel_match(:username, query)) + .or(arel_table[:email].eq(query)) .or(arel_table[:id].in(matched_by_emails_user_ids)) ) end @@ -731,7 +738,7 @@ class User < ActiveRecord::Base def ldap_user? if identities.loaded? - identities.find { |identity| identity.provider.start_with?('ldap') && !identity.extern_uid.nil? } + identities.find { |identity| Gitlab::OAuth::Provider.ldap_provider?(identity.provider) && !identity.extern_uid.nil? } else identities.exists?(["provider LIKE ? AND extern_uid IS NOT NULL", "ldap%"]) end @@ -1054,13 +1061,13 @@ class User < ActiveRecord::Base end def todos_done_count(force: false) - Rails.cache.fetch(['users', id, 'todos_done_count'], force: force) do + Rails.cache.fetch(['users', id, 'todos_done_count'], force: force, expires_in: 20.minutes) do TodosFinder.new(self, state: :done).execute.count end end def todos_pending_count(force: false) - Rails.cache.fetch(['users', id, 'todos_pending_count'], force: force) do + Rails.cache.fetch(['users', id, 'todos_pending_count'], force: force, expires_in: 20.minutes) do TodosFinder.new(self, state: :pending).execute.count end end diff --git a/app/models/user_synced_attributes_metadata.rb b/app/models/user_synced_attributes_metadata.rb index 9f374304164..548b99b69d9 100644 --- a/app/models/user_synced_attributes_metadata.rb +++ b/app/models/user_synced_attributes_metadata.rb @@ -6,11 +6,11 @@ class UserSyncedAttributesMetadata < ActiveRecord::Base SYNCABLE_ATTRIBUTES = %i[name email location].freeze def read_only?(attribute) - Gitlab.config.omniauth.sync_profile_from_provider && synced?(attribute) + sync_profile_from_provider? && synced?(attribute) end def read_only_attributes - return [] unless Gitlab.config.omniauth.sync_profile_from_provider + return [] unless sync_profile_from_provider? SYNCABLE_ATTRIBUTES.select { |key| synced?(key) } end @@ -22,4 +22,10 @@ class UserSyncedAttributesMetadata < ActiveRecord::Base def set_attribute_synced(attribute, value) write_attribute("#{attribute}_synced", value) end + + private + + def sync_profile_from_provider? + Gitlab::OAuth::Provider.sync_profile_from_provider?(provider) + end end diff --git a/app/presenters/group_member_presenter.rb b/app/presenters/group_member_presenter.rb new file mode 100644 index 00000000000..8f53dfa105e --- /dev/null +++ b/app/presenters/group_member_presenter.rb @@ -0,0 +1,15 @@ +class GroupMemberPresenter < MemberPresenter + private + + def admin_member_permission + :admin_group_member + end + + def update_member_permission + :update_group_member + end + + def destroy_member_permission + :destroy_group_member + end +end diff --git a/app/presenters/member_presenter.rb b/app/presenters/member_presenter.rb new file mode 100644 index 00000000000..7d2f9303b8f --- /dev/null +++ b/app/presenters/member_presenter.rb @@ -0,0 +1,38 @@ +class MemberPresenter < Gitlab::View::Presenter::Delegated + presents :member + + def access_level_roles + member.class.access_level_roles + end + + def can_resend_invite? + invite? && + can?(current_user, admin_member_permission, source) + end + + def can_update? + can?(current_user, update_member_permission, member) + end + + def can_remove? + can?(current_user, destroy_member_permission, member) + end + + def can_approve? + request? && can_update? + end + + private + + def admin_member_permission + raise NotImplementedError + end + + def update_member_permission + raise NotImplementedError + end + + def destroy_member_permission + raise NotImplementedError + end +end diff --git a/app/presenters/members_presenter.rb b/app/presenters/members_presenter.rb new file mode 100644 index 00000000000..e4aba37b69e --- /dev/null +++ b/app/presenters/members_presenter.rb @@ -0,0 +1,15 @@ +class MembersPresenter < Gitlab::View::Presenter::Delegated + include Enumerable + + presents :members + + def to_ary + to_a + end + + def each + members.each do |member| + yield member.present(current_user: current_user) + end + end +end diff --git a/app/presenters/project_member_presenter.rb b/app/presenters/project_member_presenter.rb new file mode 100644 index 00000000000..7f42d2b70df --- /dev/null +++ b/app/presenters/project_member_presenter.rb @@ -0,0 +1,15 @@ +class ProjectMemberPresenter < MemberPresenter + private + + def admin_member_permission + :admin_project_member + end + + def update_member_permission + :update_project_member + end + + def destroy_member_permission + :destroy_project_member + end +end diff --git a/app/serializers/concerns/with_pagination.rb b/app/serializers/concerns/with_pagination.rb index d29e22d6740..89631b73fcf 100644 --- a/app/serializers/concerns/with_pagination.rb +++ b/app/serializers/concerns/with_pagination.rb @@ -14,7 +14,7 @@ module WithPagination # we shouldn't try to paginate single resources def represent(resource, opts = {}) if paginated? && resource.respond_to?(:page) - super(@paginator.paginate(resource), opts) + super(paginator.paginate(resource), opts) else super(resource, opts) end diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb index 1e5f2ed4dd2..c8b112132b3 100644 --- a/app/services/ci/create_pipeline_service.rb +++ b/app/services/ci/create_pipeline_service.rb @@ -12,18 +12,19 @@ module Ci def execute(source, ignore_skip_ci: false, save_on_errors: true, trigger_request: nil, schedule: nil, &block) @pipeline = Ci::Pipeline.new - command = OpenStruct.new(source: source, - origin_ref: params[:ref], - checkout_sha: params[:checkout_sha], - after_sha: params[:after], - before_sha: params[:before], - trigger_request: trigger_request, - schedule: schedule, - ignore_skip_ci: ignore_skip_ci, - save_incompleted: save_on_errors, - seeds_block: block, - project: project, - current_user: current_user) + command = Gitlab::Ci::Pipeline::Chain::Command.new( + source: source, + origin_ref: params[:ref], + checkout_sha: params[:checkout_sha], + after_sha: params[:after], + before_sha: params[:before], + trigger_request: trigger_request, + schedule: schedule, + ignore_skip_ci: ignore_skip_ci, + save_incompleted: save_on_errors, + seeds_block: block, + project: project, + current_user: current_user) sequence = Gitlab::Ci::Pipeline::Chain::Sequence .new(pipeline, command, SEQUENCE) @@ -80,7 +81,7 @@ module Ci end def related_merge_requests - MergeRequest.where(source_project: pipeline.project, source_branch: pipeline.ref) + MergeRequest.opened.where(source_project: pipeline.project, source_branch: pipeline.ref) end end end diff --git a/app/services/ci/register_job_service.rb b/app/services/ci/register_job_service.rb index c8b6450c9b5..f832b79ef21 100644 --- a/app/services/ci/register_job_service.rb +++ b/app/services/ci/register_job_service.rb @@ -38,11 +38,15 @@ module Ci begin # In case when 2 runners try to assign the same build, second runner will be declined # with StateMachines::InvalidTransition or StaleObjectError when doing run! or save method. - build.runner_id = runner.id - build.run! - register_success(build) - - return Result.new(build, true) + begin + build.runner_id = runner.id + build.run! + register_success(build) + + return Result.new(build, true) + rescue Ci::Build::MissingDependenciesError + build.drop!(:missing_dependency_failure) + end rescue StateMachines::InvalidTransition, ActiveRecord::StaleObjectError # We are looping to find another build that is not conflicting # It also indicates that this build can be picked and passed to runner. @@ -54,9 +58,6 @@ module Ci # we still have to return 409 in the end, # to make sure that this is properly handled by runner. valid = false - rescue Ci::Build::MissingDependenciesError - build.drop!(:missing_dependency_failure) - valid = false end end diff --git a/app/services/concerns/issues/resolve_discussions.rb b/app/services/concerns/issues/resolve_discussions.rb index 7d45b4aa26a..26eb274f4d5 100644 --- a/app/services/concerns/issues/resolve_discussions.rb +++ b/app/services/concerns/issues/resolve_discussions.rb @@ -1,24 +1,28 @@ module Issues module ResolveDiscussions + include Gitlab::Utils::StrongMemoize + attr_reader :merge_request_to_resolve_discussions_of_iid, :discussion_to_resolve_id + # rubocop:disable Gitlab/ModuleWithInstanceVariables def filter_resolve_discussion_params @merge_request_to_resolve_discussions_of_iid ||= params.delete(:merge_request_to_resolve_discussions_of) @discussion_to_resolve_id ||= params.delete(:discussion_to_resolve) end + # rubocop:enable Gitlab/ModuleWithInstanceVariables def merge_request_to_resolve_discussions_of - return @merge_request_to_resolve_discussions_of if defined?(@merge_request_to_resolve_discussions_of) - - @merge_request_to_resolve_discussions_of = MergeRequestsFinder.new(current_user, project_id: project.id) - .execute - .find_by(iid: merge_request_to_resolve_discussions_of_iid) + strong_memoize(:merge_request_to_resolve_discussions_of) do + MergeRequestsFinder.new(current_user, project_id: project.id) + .execute + .find_by(iid: merge_request_to_resolve_discussions_of_iid) + end end def discussions_to_resolve return [] unless merge_request_to_resolve_discussions_of - @discussions_to_resolve ||= + @discussions_to_resolve ||= # rubocop:disable Gitlab/ModuleWithInstanceVariables if discussion_to_resolve_id discussion_or_nil = merge_request_to_resolve_discussions_of .find_discussion(discussion_to_resolve_id) diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb index 2c51ac13815..e7463e6e25c 100644 --- a/app/services/issuable_base_service.rb +++ b/app/services/issuable_base_service.rb @@ -106,12 +106,14 @@ class IssuableBaseService < BaseService end def merge_quick_actions_into_params!(issuable) + original_description = params.fetch(:description, issuable.description) + description, command_params = QuickActions::InterpretService.new(project, current_user) - .execute(params[:description], issuable) + .execute(original_description, issuable) # Avoid a description already set on an issuable to be overwritten by a nil - params[:description] = description if params.key?(:description) + params[:description] = description if description params.merge!(command_params) end diff --git a/app/services/members/approve_access_request_service.rb b/app/services/members/approve_access_request_service.rb index c13f289f61e..2a2bb0cae5b 100644 --- a/app/services/members/approve_access_request_service.rb +++ b/app/services/members/approve_access_request_service.rb @@ -35,8 +35,17 @@ module Members def can_update_access_requester?(access_requester, opts = {}) access_requester && ( opts[:force] || - can?(current_user, action_member_permission(:update, access_requester), access_requester) + can?(current_user, update_member_permission(access_requester), access_requester) ) end + + def update_member_permission(member) + case member + when GroupMember + :update_group_member + when ProjectMember + :update_project_member + end + end end end diff --git a/app/services/members/destroy_service.rb b/app/services/members/destroy_service.rb index 46c505baf8b..05b93ac8fdb 100644 --- a/app/services/members/destroy_service.rb +++ b/app/services/members/destroy_service.rb @@ -36,7 +36,16 @@ module Members end def can_destroy_member?(member) - member && can?(current_user, action_member_permission(:destroy, member), member) + member && can?(current_user, destroy_member_permission(member), member) + end + + def destroy_member_permission(member) + case member + when GroupMember + :destroy_group_member + when ProjectMember + :destroy_project_member + end end end end diff --git a/app/services/spam_check_service.rb b/app/services/spam_check_service.rb index 11030bee8f1..d4ade869777 100644 --- a/app/services/spam_check_service.rb +++ b/app/services/spam_check_service.rb @@ -7,16 +7,19 @@ # - params with :request # module SpamCheckService + # rubocop:disable Gitlab/ModuleWithInstanceVariables def filter_spam_check_params @request = params.delete(:request) @api = params.delete(:api) @recaptcha_verified = params.delete(:recaptcha_verified) @spam_log_id = params.delete(:spam_log_id) end + # rubocop:enable Gitlab/ModuleWithInstanceVariables # In order to be proceed to the spam check process, @spammable has to be # a dirty instance, which means it should be already assigned with the new # attribute values. + # rubocop:disable Gitlab/ModuleWithInstanceVariables def spam_check(spammable, user) spam_service = SpamService.new(spammable, @request) @@ -24,4 +27,5 @@ module SpamCheckService user.spam_logs.find_by(id: @spam_log_id)&.update!(recaptcha_verified: true) end end + # rubocop:enable Gitlab/ModuleWithInstanceVariables end diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index a9d0503bc73..3e2dbb07a6c 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -546,6 +546,12 @@ %fieldset %legend Git Storage Circuitbreaker settings .form-group + = f.label :circuitbreaker_check_interval, _('Check interval'), class: 'control-label col-sm-2' + .col-sm-10 + = f.number_field :circuitbreaker_check_interval, class: 'form-control' + .help-block + = circuitbreaker_check_interval_help_text + .form-group = f.label :circuitbreaker_access_retries, _('Number of access attempts'), class: 'control-label col-sm-2' .col-sm-10 = f.number_field :circuitbreaker_access_retries, class: 'form-control' @@ -558,18 +564,6 @@ .help-block = circuitbreaker_storage_timeout_help_text .form-group - = f.label :circuitbreaker_backoff_threshold, _('Number of failures before backing off'), class: 'control-label col-sm-2' - .col-sm-10 - = f.number_field :circuitbreaker_backoff_threshold, class: 'form-control' - .help-block - = circuitbreaker_backoff_threshold_help_text - .form-group - = f.label :circuitbreaker_failure_wait_time, _('Seconds to wait after a storage failure'), class: 'control-label col-sm-2' - .col-sm-10 - = f.number_field :circuitbreaker_failure_wait_time, class: 'form-control' - .help-block - = circuitbreaker_failure_wait_time_help_text - .form-group = f.label :circuitbreaker_failure_count_threshold, _('Maximum git storage failures'), class: 'control-label col-sm-2' .col-sm-10 = f.number_field :circuitbreaker_failure_count_threshold, class: 'form-control' diff --git a/app/views/admin/system_info/show.html.haml b/app/views/admin/system_info/show.html.haml index 6bf979a937e..23f9927cfee 100644 --- a/app/views/admin/system_info/show.html.haml +++ b/app/views/admin/system_info/show.html.haml @@ -15,7 +15,7 @@ Unable to collect CPU info .col-sm-4 .light-well - %h4 Memory + %h4 Memory Usage .data - if @memory %h1 #{number_to_human_size(@memory.active_bytes)} / #{number_to_human_size(@memory.total_bytes)} @@ -24,7 +24,7 @@ Unable to collect memory info .col-sm-4 .light-well - %h4 Disks + %h4 Disk Usage .data - @disks.each do |disk| %h1 #{number_to_human_size(disk[:bytes_used])} / #{number_to_human_size(disk[:bytes_total])} @@ -34,4 +34,4 @@ .light-well %h4 Uptime .data - %h1= time_ago_with_tooltip(Rails.application.config.booted_at) + %h1= distance_of_time_in_words_to_now(Rails.application.config.booted_at) diff --git a/app/views/admin/users/show.html.haml b/app/views/admin/users/show.html.haml index 98ff592eb64..63c5a15de1c 100644 --- a/app/views/admin/users/show.html.haml +++ b/app/views/admin/users/show.html.haml @@ -157,7 +157,6 @@ %ul %li User will not be able to login %li User will not be able to access git repositories - %li User will be removed from joined projects and groups %li Personal projects will be left %li Owned groups will be left %br diff --git a/app/views/ci/variables/_index.html.haml b/app/views/ci/variables/_index.html.haml index 2bac69bc536..6e399fc7392 100644 --- a/app/views/ci/variables/_index.html.haml +++ b/app/views/ci/variables/_index.html.haml @@ -10,5 +10,7 @@ %p.settings-message.text-center.append-bottom-0 No variables found, add one with the form above. - else - = render "ci/variables/table" - %button.btn.btn-info.js-btn-toggle-reveal-values{ "data-status" => 'hidden' } Reveal Values + .js-secret-variable-table + = render "ci/variables/table" + %button.btn.btn-info.js-secret-value-reveal-button{ data: { secret_reveal_status: 'false' } } + = n_('Reveal value', 'Reveal values', @variables.size) diff --git a/app/views/ci/variables/_table.html.haml b/app/views/ci/variables/_table.html.haml index 71a0b56c4f4..2298930d0c7 100644 --- a/app/views/ci/variables/_table.html.haml +++ b/app/views/ci/variables/_table.html.haml @@ -15,7 +15,11 @@ - if variable.id? %tr %td.variable-key= variable.key - %td.variable-value{ "data-value" => variable.value }****** + %td.variable-value + %span.js-secret-value-placeholder + = '*' * 6 + %span.hide.js-secret-value + = variable.value %td.variable-protected= Gitlab::Utils.boolean_to_yes_no(variable.protected) %td.variable-menu = link_to variable.edit_path, class: "btn btn-transparent btn-variable-edit" do diff --git a/app/views/dashboard/todos/index.html.haml b/app/views/dashboard/todos/index.html.haml index a5686002328..20ca6ec969a 100644 --- a/app/views/dashboard/todos/index.html.haml +++ b/app/views/dashboard/todos/index.html.haml @@ -83,12 +83,12 @@ You're all done! - elsif current_user.todos.any? .todos-all-done - .svg-content + .svg-content.svg-250 = image_tag 'illustrations/todos_all_done.svg' - if todos_filter_empty? %h4.text-center = Gitlab.config.gitlab.no_todos_messages.sample - %p.text-center + %p Are you looking for things to do? Take a look at = succeed "," do = link_to "the opened issues", issues_dashboard_path @@ -104,7 +104,7 @@ = image_tag 'illustrations/todos_empty.svg' .todos-empty-content %h4 - Todos let you see what you should do next. + Todos let you see what you should do next %p When an issue or merge request is assigned to you, or when you %strong diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml index 25ed610466a..eba9cd253bb 100644 --- a/app/views/layouts/_page.html.haml +++ b/app/views/layouts/_page.html.haml @@ -1,7 +1,7 @@ -.page-with-sidebar{ class: page_with_sidebar_class } +.layout-page{ class: page_with_sidebar_class } - if defined?(nav) && nav = render "layouts/nav/sidebar/#{nav}" - .content-wrapper.page-with-new-nav + .content-wrapper = render 'shared/outdated_browser' .mobile-overlay .alert-wrapper diff --git a/app/views/layouts/_recaptcha_verification.html.haml b/app/views/layouts/_recaptcha_verification.html.haml index 77c77dc6754..e6f87ddd383 100644 --- a/app/views/layouts/_recaptcha_verification.html.haml +++ b/app/views/layouts/_recaptcha_verification.html.haml @@ -1,5 +1,4 @@ - humanized_resource_name = spammable.class.model_name.human.downcase -- resource_name = spammable.class.model_name.singular %h3.page-title Anti-spam verification @@ -8,16 +7,4 @@ %p #{"We detected potential spam in the #{humanized_resource_name}. Please solve the reCAPTCHA to proceed."} -= form_for form do |f| - .recaptcha - - params[resource_name].each do |field, value| - = hidden_field(resource_name, field, value: value) - = hidden_field_tag(:spam_log_id, spammable.spam_log.id) - = hidden_field_tag(:recaptcha_verification, true) - = recaptcha_tags - - -# Yields a block with given extra params. - = yield - - .row-content-block.footer-block - = f.submit "Submit #{humanized_resource_name}", class: 'btn btn-create' += render 'shared/recaptcha_form', spammable: spammable diff --git a/app/views/layouts/_search.html.haml b/app/views/layouts/_search.html.haml index 30ae385f62f..52587760ba4 100644 --- a/app/views/layouts/_search.html.haml +++ b/app/views/layouts/_search.html.haml @@ -13,7 +13,14 @@ .location-badge= label .search-input-wrap .dropdown{ data: { url: search_autocomplete_path } } - = search_field_tag 'search', nil, placeholder: 'Search', class: 'search-input dropdown-menu-toggle no-outline js-search-dashboard-options', spellcheck: false, tabindex: '1', autocomplete: 'off', data: { issues_path: issues_dashboard_url, mr_path: merge_requests_dashboard_url }, aria: { label: 'Search' } + = search_field_tag 'search', nil, placeholder: 'Search', + class: 'search-input dropdown-menu-toggle no-outline js-search-dashboard-options', + spellcheck: false, + tabindex: '1', + autocomplete: 'off', + data: { issues_path: issues_dashboard_path, + mr_path: merge_requests_dashboard_path }, + aria: { label: 'Search' } %button.hidden.js-dropdown-search-toggle{ type: 'button', data: { toggle: 'dropdown' } } .dropdown-menu.dropdown-select = dropdown_content do diff --git a/app/views/layouts/nav/projects_dropdown/_show.html.haml b/app/views/layouts/nav/projects_dropdown/_show.html.haml index a7370180bf6..32a24c101fc 100644 --- a/app/views/layouts/nav/projects_dropdown/_show.html.haml +++ b/app/views/layouts/nav/projects_dropdown/_show.html.haml @@ -1,4 +1,4 @@ -- project_meta = { id: @project.id, name: @project.name, namespace: @project.name_with_namespace, web_url: @project.web_url, avatar_url: @project.avatar_url } if @project&.persisted? +- project_meta = { id: @project.id, name: @project.name, namespace: @project.name_with_namespace, web_url: project_path(@project), avatar_url: @project.avatar_url } if @project&.persisted? .projects-dropdown-container .project-dropdown-sidebar %ul diff --git a/app/views/layouts/nav/sidebar/_admin.html.haml b/app/views/layouts/nav/sidebar/_admin.html.haml index 0ec07605631..cb8db306b56 100644 --- a/app/views/layouts/nav/sidebar/_admin.html.haml +++ b/app/views/layouts/nav/sidebar/_admin.html.haml @@ -1,4 +1,4 @@ -.nav-sidebar{ class: ("sidebar-icons-only" if collapsed_sidebar?) } +.nav-sidebar{ class: ("sidebar-collapsed-desktop" if collapsed_sidebar?) } .nav-sidebar-inner-scroll .context-header = link_to admin_root_path, title: 'Admin Overview' do diff --git a/app/views/layouts/nav/sidebar/_group.html.haml b/app/views/layouts/nav/sidebar/_group.html.haml index 0bf318b0b66..0c27b09f7b1 100644 --- a/app/views/layouts/nav/sidebar/_group.html.haml +++ b/app/views/layouts/nav/sidebar/_group.html.haml @@ -1,7 +1,7 @@ - issues = IssuesFinder.new(current_user, group_id: @group.id, state: 'opened').execute - merge_requests = MergeRequestsFinder.new(current_user, group_id: @group.id, state: 'opened', non_archived: true).execute -.nav-sidebar{ class: ("sidebar-icons-only" if collapsed_sidebar?) } +.nav-sidebar{ class: ("sidebar-collapsed-desktop" if collapsed_sidebar?) } .nav-sidebar-inner-scroll .context-header = link_to group_path(@group), title: @group.name do diff --git a/app/views/layouts/nav/sidebar/_profile.html.haml b/app/views/layouts/nav/sidebar/_profile.html.haml index 7e23f9c1f05..a5a62a0695f 100644 --- a/app/views/layouts/nav/sidebar/_profile.html.haml +++ b/app/views/layouts/nav/sidebar/_profile.html.haml @@ -1,4 +1,4 @@ -.nav-sidebar{ class: ("sidebar-icons-only" if collapsed_sidebar?) } +.nav-sidebar{ class: ("sidebar-collapsed-desktop" if collapsed_sidebar?) } .nav-sidebar-inner-scroll .context-header = link_to profile_path, title: 'Profile Settings' do diff --git a/app/views/layouts/nav/sidebar/_project.html.haml b/app/views/layouts/nav/sidebar/_project.html.haml index 53a9162b703..be39f577ba7 100644 --- a/app/views/layouts/nav/sidebar/_project.html.haml +++ b/app/views/layouts/nav/sidebar/_project.html.haml @@ -1,4 +1,4 @@ -.nav-sidebar{ class: ("sidebar-icons-only" if collapsed_sidebar?) } +.nav-sidebar{ class: ("sidebar-collapsed-desktop" if collapsed_sidebar?) } .nav-sidebar-inner-scroll - can_edit = can?(current_user, :admin_project, @project) .context-header diff --git a/app/views/notify/pipeline_success_email.html.haml b/app/views/notify/pipeline_success_email.html.haml index 574a8f2fa50..bae37292d62 100644 --- a/app/views/notify/pipeline_success_email.html.haml +++ b/app/views/notify/pipeline_success_email.html.haml @@ -109,7 +109,7 @@ API %tr %td{ colspan: 2, style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#333333;font-size:15px;font-weight:300;line-height:1.4;padding:15px 5px;text-align:center;" } - - job_count = @pipeline.statuses.latest.size + - job_count = @pipeline.total_size - stage_count = @pipeline.stages_count successfully completed #{job_count} #{'job'.pluralize(job_count)} diff --git a/app/views/notify/pipeline_success_email.text.erb b/app/views/notify/pipeline_success_email.text.erb index ddced2279e1..39622cf7f02 100644 --- a/app/views/notify/pipeline_success_email.text.erb +++ b/app/views/notify/pipeline_success_email.text.erb @@ -22,11 +22,11 @@ Committed by: <%= commit.committer_name %> <% end -%> <% end -%> -<% build_count = @pipeline.statuses.latest.size -%> +<% job_count = @pipeline.total_size -%> <% stage_count = @pipeline.stages_count -%> <% if @pipeline.user -%> Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) triggered by <%= @pipeline.user.name %> ( <%= user_url(@pipeline.user) %> ) <% else -%> Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) triggered by API <% end -%> -successfully completed <%= build_count %> <%= 'build'.pluralize(build_count) %> in <%= stage_count %> <%= 'stage'.pluralize(stage_count) %>. +successfully completed <%= job_count %> <%= 'job'.pluralize(job_count) %> in <%= stage_count %> <%= 'stage'.pluralize(stage_count) %>. diff --git a/app/views/profiles/personal_access_tokens/index.html.haml b/app/views/profiles/personal_access_tokens/index.html.haml index 26c2e4c5936..f445e5a2417 100644 --- a/app/views/profiles/personal_access_tokens/index.html.haml +++ b/app/views/profiles/personal_access_tokens/index.html.haml @@ -15,14 +15,13 @@ They are the only accepted password when you have Two-Factor Authentication (2FA) enabled. .col-lg-8 - - - if flash[:personal_access_token] + - if @new_personal_access_token .created-personal-access-token-container %h5.prepend-top-0 Your New Personal Access Token .form-group - = text_field_tag 'created-personal-access-token', flash[:personal_access_token], readonly: true, class: "form-control js-select-on-focus", 'aria-describedby' => "created-personal-access-token-help-block" - = clipboard_button(text: flash[:personal_access_token], title: "Copy personal access token to clipboard", placement: "left") + = text_field_tag 'created-personal-access-token', @new_personal_access_token, readonly: true, class: "form-control js-select-on-focus", 'aria-describedby' => "created-personal-access-token-help-block" + = clipboard_button(text: @new_personal_access_token, title: "Copy personal access token to clipboard", placement: "left") %span#created-personal-access-token-help-block.help-block.text-danger Make sure you save it - you won't be able to access it again. %hr diff --git a/app/views/projects/_merge_request_merge_settings.html.haml b/app/views/projects/_merge_request_merge_settings.html.haml index 1dd8778f800..f6e5712ce81 100644 --- a/app/views/projects/_merge_request_merge_settings.html.haml +++ b/app/views/projects/_merge_request_merge_settings.html.haml @@ -8,7 +8,7 @@ %br %span.descr Pipelines need to be configured to enable this feature. - = link_to icon('question-circle'), help_page_path('user/project/merge_requests/merge_when_pipeline_succeeds', anchor: 'only-allow-merge-requests-to-be-merged-if-the-pipeline-succeeds') + = link_to icon('question-circle'), help_page_path('user/project/merge_requests/merge_when_pipeline_succeeds', anchor: 'only-allow-merge-requests-to-be-merged-if-the-pipeline-succeeds'), target: '_blank' .checkbox = form.label :only_allow_merge_if_all_discussions_are_resolved do = form.check_box :only_allow_merge_if_all_discussions_are_resolved diff --git a/app/views/projects/blob/_header_content.html.haml b/app/views/projects/blob/_header_content.html.haml index 98bedae650a..5d457a50c49 100644 --- a/app/views/projects/blob/_header_content.html.haml +++ b/app/views/projects/blob/_header_content.html.haml @@ -8,3 +8,6 @@ %small = number_to_human_size(blob.raw_size) + + - if blob.stored_externally? && blob.external_storage == :lfs + %span.label.label-lfs.append-right-5 LFS diff --git a/app/views/projects/blob/_template_selectors.html.haml b/app/views/projects/blob/_template_selectors.html.haml index 2a178325041..5b092427496 100644 --- a/app/views/projects/blob/_template_selectors.html.haml +++ b/app/views/projects/blob/_template_selectors.html.haml @@ -3,15 +3,15 @@ Template .template-selector-dropdowns-wrap .template-type-selector.js-template-type-selector-wrap.hidden - = dropdown_tag("Choose type", options: { toggle_class: 'btn js-template-type-selector', title: "Choose a template type" } ) + = dropdown_tag("Choose type", options: { toggle_class: 'js-template-type-selector', title: "Choose a template type" } ) .license-selector.js-license-selector-wrap.js-template-selector-wrap.hidden - = dropdown_tag("Apply a license template", options: { toggle_class: 'btn js-license-selector', title: "Apply a license", filter: true, placeholder: "Filter", data: { data: licenses_for_select, project: @project.name, fullname: @project.namespace.human_name } } ) + = dropdown_tag("Apply a license template", options: { toggle_class: 'js-license-selector', title: "Apply a license", filter: true, placeholder: "Filter", data: { data: licenses_for_select, project: @project.name, fullname: @project.namespace.human_name } } ) .gitignore-selector.js-gitignore-selector-wrap.js-template-selector-wrap.hidden - = dropdown_tag("Apply a .gitignore template", options: { toggle_class: 'btn js-gitignore-selector', title: "Apply a template", filter: true, placeholder: "Filter", data: { data: gitignore_names } } ) + = dropdown_tag("Apply a .gitignore template", options: { toggle_class: 'js-gitignore-selector', title: "Apply a template", filter: true, placeholder: "Filter", data: { data: gitignore_names } } ) .gitlab-ci-yml-selector.js-gitlab-ci-yml-selector-wrap.js-template-selector-wrap.hidden - = dropdown_tag("Apply a GitLab CI Yaml template", options: { toggle_class: 'btn js-gitlab-ci-yml-selector', title: "Apply a template", filter: true, placeholder: "Filter", data: { data: gitlab_ci_ymls } } ) + = dropdown_tag("Apply a GitLab CI Yaml template", options: { toggle_class: 'js-gitlab-ci-yml-selector', title: "Apply a template", filter: true, placeholder: "Filter", data: { data: gitlab_ci_ymls } } ) .dockerfile-selector.js-dockerfile-selector-wrap.js-template-selector-wrap.hidden - = dropdown_tag("Apply a Dockerfile template", options: { toggle_class: 'btn js-dockerfile-selector', title: "Apply a template", filter: true, placeholder: "Filter", data: { data: dockerfile_names } } ) + = dropdown_tag("Apply a Dockerfile template", options: { toggle_class: 'js-dockerfile-selector', title: "Apply a template", filter: true, placeholder: "Filter", data: { data: dockerfile_names } } ) .template-selectors-undo-menu.hidden %span.text-info Template applied %button.btn.btn-sm.btn-info Undo diff --git a/app/views/projects/blob/viewers/_image.html.haml b/app/views/projects/blob/viewers/_image.html.haml index 26ea028c5d7..2a8cefac005 100644 --- a/app/views/projects/blob/viewers/_image.html.haml +++ b/app/views/projects/blob/viewers/_image.html.haml @@ -1,2 +1,3 @@ .file-content.image_file - = image_tag(blob_raw_path, alt: viewer.blob.name) + -# Uses the full URL rather than the path, to prevent it from getting prefixed with the asset host. + = image_tag(blob_raw_url, alt: viewer.blob.name) diff --git a/app/views/projects/branches/new.html.haml b/app/views/projects/branches/new.html.haml index 2baaaf6ac5b..e9d8fc75142 100644 --- a/app/views/projects/branches/new.html.haml +++ b/app/views/projects/branches/new.html.haml @@ -20,7 +20,7 @@ .col-sm-10.create-from .dropdown = hidden_field_tag :ref, default_ref - = button_tag type: 'button', title: default_ref, class: 'dropdown-menu-toggle wide form-control js-branch-select git-revision-dropdown-toggle', required: true, data: { toggle: 'dropdown', selected: default_ref, field_name: 'ref' } do + = button_tag type: 'button', title: default_ref, class: 'dropdown-menu-toggle wide js-branch-select git-revision-dropdown-toggle', required: true, data: { toggle: 'dropdown', selected: default_ref, field_name: 'ref' } do .text-left.dropdown-toggle-text= default_ref = icon('chevron-down') = render 'shared/ref_dropdown', dropdown_class: 'wide' diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml index c1842527480..86510b8ab93 100644 --- a/app/views/projects/ci/builds/_build.html.haml +++ b/app/views/projects/ci/builds/_build.html.haml @@ -14,7 +14,7 @@ %td.branch-commit - if can?(current_user, :read_build, job) - = link_to project_job_url(job.project, job) do + = link_to project_job_path(job.project, job) do %span.build-link ##{job.id} - else %span.build-link ##{job.id} diff --git a/app/views/projects/clusters/_cluster.html.haml b/app/views/projects/clusters/_cluster.html.haml index 18ca01d2d49..ad696daa259 100644 --- a/app/views/projects/clusters/_cluster.html.haml +++ b/app/views/projects/clusters/_cluster.html.haml @@ -16,7 +16,8 @@ class: "js-toggle-cluster-list project-feature-toggle #{'is-checked' if cluster.enabled?} #{'is-disabled' if !cluster.can_toggle_cluster?}", "aria-label": s_("ClusterIntegration|Toggle Cluster"), disabled: !cluster.can_toggle_cluster?, - data: { "enabled-text": s_("ClusterIntegration|Active"), - "disabled-text": s_("ClusterIntegration|Inactive"), - endpoint: namespace_project_cluster_path(@project.namespace, @project, cluster, format: :json) } } + data: { endpoint: namespace_project_cluster_path(@project.namespace, @project, cluster, format: :json) } } = icon("spinner spin", class: "loading-icon") + %span.toggle-icon + = sprite_icon('status_success_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-checked') + = sprite_icon('status_failed_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-unchecked') diff --git a/app/views/projects/clusters/_empty_state.html.haml b/app/views/projects/clusters/_empty_state.html.haml index e629cc58b06..b525f4efc83 100644 --- a/app/views/projects/clusters/_empty_state.html.haml +++ b/app/views/projects/clusters/_empty_state.html.haml @@ -1,12 +1,12 @@ .row.empty-state .col-xs-12 .svg-content= image_tag 'illustrations/clusters_empty.svg' - .col-xs-12.text-center + .col-xs-12 .text-content - %h4= s_('ClusterIntegration|Integrate cluster automation') + %h4.text-center= s_('ClusterIntegration|Integrate cluster automation') - link_to_help_page = link_to(s_('ClusterIntegration|Learn more about Clusters'), help_page_path('user/project/clusters/index'), target: '_blank', rel: 'noopener noreferrer') %p= s_('ClusterIntegration|Clusters allow you to use review apps, deploy your applications, run your pipelines, and much more in an easy way. %{link_to_help_page}').html_safe % { link_to_help_page: link_to_help_page} - %p + .text-center = link_to s_('ClusterIntegration|Add cluster'), new_project_cluster_path(@project), class: 'btn btn-success' diff --git a/app/views/projects/clusters/_enabled.html.haml b/app/views/projects/clusters/_enabled.html.haml index 70c677f7856..547b3c8446f 100644 --- a/app/views/projects/clusters/_enabled.html.haml +++ b/app/views/projects/clusters/_enabled.html.haml @@ -7,8 +7,10 @@ %button{ type: 'button', class: "js-toggle-cluster project-feature-toggle #{'is-checked' unless !@cluster.enabled?} #{'is-disabled' unless can?(current_user, :update_cluster, @cluster)}", "aria-label": s_("ClusterIntegration|Toggle Cluster"), - disabled: !can?(current_user, :update_cluster, @cluster), - data: { "enabled-text": s_("ClusterIntegration|Active"), "disabled-text": s_("ClusterIntegration|Inactive"), } } + disabled: !can?(current_user, :update_cluster, @cluster) } + %span.toggle-icon + = sprite_icon('status_success_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-checked') + = sprite_icon('status_failed_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-unchecked') - if can?(current_user, :update_cluster, @cluster) .form-group diff --git a/app/views/projects/clusters/_tabs.html.haml b/app/views/projects/clusters/_tabs.html.haml deleted file mode 100644 index c8120e806fa..00000000000 --- a/app/views/projects/clusters/_tabs.html.haml +++ /dev/null @@ -1,16 +0,0 @@ -.top-area.scrolling-tabs-container.inner-page-scroll-tabs - .fade-left= icon("angle-left") - .fade-right= icon("angle-right") - %ul.nav-links.scrolling-tabs - %li{ class: ('active' if @scope == 'active') }> - = link_to project_clusters_path(@project, scope: :active), class: "js-active-tab" do - = s_("ClusterIntegration|Active") - %span.badge= @active_count - %li{ class: ('active' if @scope == 'inactive') }> - = link_to project_clusters_path(@project, scope: :inactive), class: "js-inactive-tab" do - = s_("ClusterIntegration|Inactive") - %span.badge= @inactive_count - %li{ class: ('active' if @scope.nil? || @scope == 'all') }> - = link_to project_clusters_path(@project), class: "js-all-tab" do - = s_("ClusterIntegration|All") - %span.badge= @all_count diff --git a/app/views/projects/clusters/index.html.haml b/app/views/projects/clusters/index.html.haml index 104e39b0e06..bec512be91c 100644 --- a/app/views/projects/clusters/index.html.haml +++ b/app/views/projects/clusters/index.html.haml @@ -2,8 +2,12 @@ - page_title "Clusters" .clusters-container - - if !@clusters.empty? - = render "tabs" + - if @clusters.empty? + = render "empty_state" + - else + .top-area.adjust + .nav-text + = s_("ClusterIntegration|Clusters can be used to deploy applications and to provide Review Apps for this project") .ci-table.js-clusters-list .gl-responsive-table-row.table-row-header{ role: "row" } .table-section.section-30{ role: "rowheader" } @@ -16,9 +20,3 @@ - @clusters.each do |cluster| = render "cluster", cluster: cluster.present(current_user: current_user) = paginate @clusters, theme: "gitlab" - - elsif @scope == 'all' - = render "empty_state" - - else - = render "tabs" - .prepend-top-20.text-center - = s_("ClusterIntegration|There are no clusters to show") diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml index 618a6355d23..d66066a6d0b 100644 --- a/app/views/projects/commits/_commit.html.haml +++ b/app/views/projects/commits/_commit.html.haml @@ -38,8 +38,8 @@ .commiter - commit_author_link = commit_author_link(commit, avatar: false, size: 24) - - commit_timeago = time_ago_with_tooltip(commit.committed_date, placement: 'bottom') - - commit_text = _('%{commit_author_link} committed %{commit_timeago}') % { commit_author_link: commit_author_link, commit_timeago: commit_timeago } + - commit_timeago = time_ago_with_tooltip(commit.authored_date, placement: 'bottom') + - commit_text = _('%{commit_author_link} authored %{commit_timeago}') % { commit_author_link: commit_author_link, commit_timeago: commit_timeago } #{ commit_text.html_safe } .commit-actions.flex-row.hidden-xs diff --git a/app/views/projects/commits/show.html.haml b/app/views/projects/commits/show.html.haml index ef305120525..ab371521840 100644 --- a/app/views/projects/commits/show.html.haml +++ b/app/views/projects/commits/show.html.haml @@ -3,7 +3,7 @@ - page_title _("Commits"), @ref = content_for :meta_tags do - = auto_discovery_link_tag(:atom, project_commits_url(@project, @ref, rss_url_options), title: "#{@project.name}:#{@ref} commits") + = auto_discovery_link_tag(:atom, project_commits_path(@project, @ref, rss_url_options), title: "#{@project.name}:#{@ref} commits") .js-project-commits-show{ 'data-commits-limit' => @limit } %div{ class: container_class } diff --git a/app/views/projects/diffs/_replaced_image_diff.html.haml b/app/views/projects/diffs/_replaced_image_diff.html.haml index 8fc232b464e..6dffc7c4390 100644 --- a/app/views/projects/diffs/_replaced_image_diff.html.haml +++ b/app/views/projects/diffs/_replaced_image_diff.html.haml @@ -1,7 +1,7 @@ - blob = diff_file.blob - old_blob = diff_file.old_blob -- blob_raw_path = diff_file_blob_raw_path(diff_file) -- old_blob_raw_path = diff_file_old_blob_raw_path(diff_file) +- blob_raw_url = diff_file_blob_raw_url(diff_file) +- old_blob_raw_url = diff_file_old_blob_raw_url(diff_file) - click_to_comment = local_assigns.fetch(:click_to_comment, true) - diff_view_data = local_assigns.fetch(:diff_view_data, '') - class_name = '' @@ -13,7 +13,7 @@ .two-up.view .wrap .frame.deleted - = image_tag(old_blob_raw_path, alt: diff_file.old_path, lazy: false) + = image_tag(old_blob_raw_url, alt: diff_file.old_path, lazy: false) %p.image-info.hide %span.meta-filesize= number_to_human_size(old_blob.size) | @@ -23,7 +23,7 @@ %strong H: %span.meta-height .wrap - = render partial: "projects/diffs/image_diff_frame", locals: { class_name: "added js-image-frame #{class_name}", position: position, note_type: DiffNote.name, image_path: blob_raw_path, alt: diff_file.new_path } + = render partial: "projects/diffs/image_diff_frame", locals: { class_name: "added js-image-frame #{class_name}", position: position, note_type: DiffNote.name, image_path: blob_raw_url, alt: diff_file.new_path } %p.image-info.hide %span.meta-filesize= number_to_human_size(blob.size) | @@ -36,9 +36,9 @@ .swipe.view.hide .swipe-frame .frame.deleted - = image_tag(old_blob_raw_path, alt: diff_file.old_path, lazy: false) + = image_tag(old_blob_raw_url, alt: diff_file.old_path, lazy: false) .swipe-wrap - = render partial: "projects/diffs/image_diff_frame", locals: { class_name: "added js-image-frame #{class_name}", position: position, note_type: DiffNote.name, image_path: blob_raw_path, alt: diff_file.new_path } + = render partial: "projects/diffs/image_diff_frame", locals: { class_name: "added js-image-frame #{class_name}", position: position, note_type: DiffNote.name, image_path: blob_raw_url, alt: diff_file.new_path } %span.swipe-bar %span.top-handle %span.bottom-handle @@ -46,8 +46,8 @@ .onion-skin.view.hide .onion-skin-frame .frame.deleted - = image_tag(old_blob_raw_path, alt: diff_file.old_path, lazy: false) - = render partial: "projects/diffs/image_diff_frame", locals: { class_name: "added js-image-frame #{class_name}", position: position, note_type: DiffNote.name, image_path: blob_raw_path, alt: diff_file.new_path } + = image_tag(old_blob_raw_url, alt: diff_file.old_path, lazy: false) + = render partial: "projects/diffs/image_diff_frame", locals: { class_name: "added js-image-frame #{class_name}", position: position, note_type: DiffNote.name, image_path: blob_raw_url, alt: diff_file.new_path } .controls .transparent .drag-track diff --git a/app/views/projects/diffs/_single_image_diff.html.haml b/app/views/projects/diffs/_single_image_diff.html.haml index 6b0c6bbe48f..12be8beab39 100644 --- a/app/views/projects/diffs/_single_image_diff.html.haml +++ b/app/views/projects/diffs/_single_image_diff.html.haml @@ -1,7 +1,7 @@ - blob = diff_file.blob - old_blob = diff_file.old_blob -- blob_raw_path = diff_file_blob_raw_path(diff_file) -- old_blob_raw_path = diff_file_old_blob_raw_path(diff_file) +- blob_raw_url = diff_file_blob_raw_url(diff_file) +- old_blob_raw_url = diff_file_old_blob_raw_url(diff_file) - click_to_comment = local_assigns.fetch(:click_to_comment, true) - diff_view_data = local_assigns.fetch(:diff_view_data, '') - class_name = '' @@ -12,5 +12,5 @@ .image.js-single-image{ data: diff_view_data } .wrap - single_class_name = diff_file.deleted_file? ? 'deleted' : 'added' - = render partial: "projects/diffs/image_diff_frame", locals: { class_name: "#{single_class_name} #{class_name} js-image-frame", position: position, note_type: DiffNote.name, image_path: blob_raw_path, alt: diff_file.file_path } + = render partial: "projects/diffs/image_diff_frame", locals: { class_name: "#{single_class_name} #{class_name} js-image-frame", position: position, note_type: DiffNote.name, image_path: blob_raw_url, alt: diff_file.file_path } %p.image-info= number_to_human_size(blob.size) diff --git a/app/views/projects/environments/metrics.html.haml b/app/views/projects/environments/metrics.html.haml index e0aedcac5e1..ad94113fffd 100644 --- a/app/views/projects/environments/metrics.html.haml +++ b/app/views/projects/environments/metrics.html.haml @@ -15,8 +15,10 @@ #prometheus-graphs{ data: { "settings-path": edit_project_service_path(@project, 'prometheus'), "documentation-path": help_page_path('administration/monitoring/prometheus/index.md'), - "empty-getting-started-svg-path": image_path('illustrations/monitoring/getting_started'), - "empty-loading-svg-path": image_path('illustrations/monitoring/loading'), - "empty-unable-to-connect-svg-path": image_path('illustrations/monitoring/unable_to_connect'), + "empty-getting-started-svg-path": image_path('illustrations/monitoring/getting_started.svg'), + "empty-loading-svg-path": image_path('illustrations/monitoring/loading.svg'), + "empty-unable-to-connect-svg-path": image_path('illustrations/monitoring/unable_to_connect.svg'), "additional-metrics": additional_metrics_project_environment_path(@project, @environment, format: :json), + "project-path": project_path(@project), + "tags-path": project_tags_path(@project), "has-metrics": "#{@environment.has_metrics?}", deployment_endpoint: project_environment_deployments_path(@project, @environment, format: :json) } } diff --git a/app/views/projects/issues/_discussion.html.haml b/app/views/projects/issues/_discussion.html.haml index 1eccc0509bd..9779c1985d5 100644 --- a/app/views/projects/issues/_discussion.html.haml +++ b/app/views/projects/issues/_discussion.html.haml @@ -14,4 +14,4 @@ notes_path: notes_url, last_fetched_at: Time.now.to_i, noteable_data: serialize_issuable(@issue), - current_user_data: UserSerializer.new.represent(current_user).to_json } } + current_user_data: UserSerializer.new.represent(current_user, only_path: true).to_json } } diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml index 2f7aece7440..eab7879c7bf 100644 --- a/app/views/projects/issues/show.html.haml +++ b/app/views/projects/issues/show.html.haml @@ -14,12 +14,12 @@ .detail-page-header .detail-page-header-body - .issuable-status-box.status-box.status-box-closed{ class: issue_button_visibility(@issue, false) } - = icon('check', class: "hidden-sm hidden-md hidden-lg") + .issuable-status-box.status-box.status-box-issue-closed{ class: issue_button_visibility(@issue, false) } + = sprite_icon('mobile-issue-close', size: 16, css_class: 'hidden-sm hidden-md hidden-lg') %span.hidden-xs Closed .issuable-status-box.status-box.status-box-open{ class: issue_button_visibility(@issue, true) } - = icon('circle-o', class: "hidden-sm hidden-md hidden-lg") + = sprite_icon('issue-open-m', size: 16, css_class: 'hidden-sm hidden-md hidden-lg') %span.hidden-xs Open .issuable-meta @@ -40,7 +40,7 @@ .dropdown-menu.dropdown-menu-align-right.hidden-lg %ul - if can_update_issue - %li= link_to 'Edit', edit_project_issue_path(@project, @issue), class: 'issuable-edit' + %li= link_to 'Edit', edit_project_issue_path(@project, @issue), class: 'js-issuable-edit' - unless current_user == @issue.author %li= link_to 'Report abuse', new_abuse_report_path(user_id: @issue.author.id, ref_url: issue_url(@issue)) - if can_update_issue @@ -53,7 +53,7 @@ %li= link_to 'New issue', new_project_issue_path(@project), title: 'New issue', id: 'new_issue_link' - if can_update_issue - = link_to 'Edit', edit_project_issue_path(@project, @issue), class: 'hidden-xs hidden-sm btn btn-grouped issuable-edit' + = link_to 'Edit', edit_project_issue_path(@project, @issue), class: 'hidden-xs hidden-sm btn btn-grouped js-issuable-edit' = render 'shared/issuable/close_reopen_button', issuable: @issue, can_update: can_update_issue diff --git a/app/views/projects/merge_requests/_mr_title.html.haml b/app/views/projects/merge_requests/_mr_title.html.haml index 135f9ab0aff..22c8b6b513d 100644 --- a/app/views/projects/merge_requests/_mr_title.html.haml +++ b/app/views/projects/merge_requests/_mr_title.html.haml @@ -7,7 +7,7 @@ .detail-page-header .detail-page-header-body .issuable-status-box.status-box{ class: status_box_class(@merge_request) } - = icon(@merge_request.state_icon_name, class: "hidden-sm hidden-md hidden-lg") + = sprite_icon(@merge_request.state_icon_name, size: 16, css_class: 'hidden-sm hidden-md hidden-lg') %span.hidden-xs = @merge_request.state_human_name @@ -27,7 +27,7 @@ .dropdown-menu.dropdown-menu-align-right.hidden-lg %ul - if can_update_merge_request - %li= link_to 'Edit', edit_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: 'issuable-edit' + %li= link_to 'Edit', edit_namespace_project_merge_request_path(@project.namespace, @project, @merge_request) - unless current_user == @merge_request.author %li= link_to 'Report abuse', new_abuse_report_path(user_id: @merge_request.author.id, ref_url: merge_request_url(@merge_request)) - if can_update_merge_request @@ -37,6 +37,6 @@ = link_to 'Reopen', merge_request_path(@merge_request, merge_request: { state_event: :reopen }), method: :put, class: 'reopen-mr-link', title: 'Reopen merge request' - if can_update_merge_request - = link_to 'Edit', edit_project_merge_request_path(@project, @merge_request), class: "hidden-xs hidden-sm btn btn-grouped issuable-edit" + = link_to 'Edit', edit_project_merge_request_path(@project, @merge_request), class: "hidden-xs hidden-sm btn btn-grouped js-issuable-edit" = render 'shared/issuable/close_reopen_button', issuable: @merge_request, can_update: can_update_merge_request diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml index 4bb97ecdd16..2f56630c22e 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -86,7 +86,7 @@ = icon('bug', text: 'Fogbugz') %div - if gitea_import_enabled? - = link_to new_import_gitea_url, class: 'btn import_gitea' do + = link_to new_import_gitea_path, class: 'btn import_gitea' do = custom_icon('go_logo') Gitea %div diff --git a/app/views/projects/pipelines/_info.html.haml b/app/views/projects/pipelines/_info.html.haml index f5149306734..01ea9356af5 100644 --- a/app/views/projects/pipelines/_info.html.haml +++ b/app/views/projects/pipelines/_info.html.haml @@ -13,7 +13,7 @@ .well-segment.pipeline-info .icon-container = icon('clock-o') - = pluralize @pipeline.statuses.count(:id), "job" + = pluralize @pipeline.total_size, "job" - if @pipeline.ref from = link_to @pipeline.ref, project_ref_path(@project, @pipeline.ref), class: "ref-name" diff --git a/app/views/projects/pipelines/_with_tabs.html.haml b/app/views/projects/pipelines/_with_tabs.html.haml index ad61f033a1c..398a1c46746 100644 --- a/app/views/projects/pipelines/_with_tabs.html.haml +++ b/app/views/projects/pipelines/_with_tabs.html.haml @@ -8,7 +8,7 @@ %li.js-builds-tab-link = link_to builds_project_pipeline_path(@project, @pipeline), data: {target: 'div#js-tab-builds', action: 'builds', toggle: 'tab' }, class: 'builds-tab' do Jobs - %span.badge.js-builds-counter= pipeline.statuses.count + %span.badge.js-builds-counter= pipeline.total_size - if failed_builds.present? %li.js-failures-tab-link = link_to failures_project_pipeline_path(@project, @pipeline), data: {target: 'div#js-tab-failures', action: 'failures', toggle: 'tab' }, class: 'failures-tab' do diff --git a/app/views/projects/pipelines_settings/_show.html.haml b/app/views/projects/pipelines_settings/_show.html.haml index c63e716180c..c5f9f5aa15b 100644 --- a/app/views/projects/pipelines_settings/_show.html.haml +++ b/app/views/projects/pipelines_settings/_show.html.haml @@ -40,10 +40,14 @@ = form.text_field :domain, class: 'form-control', placeholder: 'domain.com' %hr - .form-group.append-bottom-default + .form-group.append-bottom-default.js-secret-runner-token = f.label :runners_token, "Runner token", class: 'label-light' - = f.text_field :runners_token, class: "form-control", placeholder: 'xEeFCaDAB89' + .form-control.js-secret-value-placeholder + = '*' * 20 + = f.text_field :runners_token, class: "form-control hide js-secret-value", placeholder: 'xEeFCaDAB89' %p.help-block The secure token used by the Runner to checkout the project + %button.btn.btn-info.prepend-top-10.js-secret-value-reveal-button{ type: 'button', data: { secret_reveal_status: 'false' } } + = _('Reveal value') %hr .form-group diff --git a/app/views/projects/project_members/_team.html.haml b/app/views/projects/project_members/_team.html.haml index e71d58ec26d..16bcf671c25 100644 --- a/app/views/projects/project_members/_team.html.haml +++ b/app/views/projects/project_members/_team.html.haml @@ -1,11 +1,13 @@ +- project = local_assigns.fetch(:project) +- members = local_assigns.fetch(:members) + .panel.panel-default .panel-heading.flex-project-members-panel %span.flex-project-title Members of - %strong - #{@project.name} - %span.badge= @project_members.total_count - = form_tag project_project_members_path(@project), method: :get, class: 'form-inline member-search-form flex-project-members-form' do + %strong= project.name + %span.badge= members.total_count + = form_tag project_project_members_path(project), method: :get, class: 'form-inline member-search-form flex-project-members-form' do .form-group = search_field_tag :search, params[:search], { placeholder: 'Find existing members by name', class: 'form-control', spellcheck: false } %button.member-search-btn{ type: "submit", "aria-label" => "Submit search" } diff --git a/app/views/projects/project_members/index.html.haml b/app/views/projects/project_members/index.html.haml index fd5d3ec56da..d81103c3a92 100644 --- a/app/views/projects/project_members/index.html.haml +++ b/app/views/projects/project_members/index.html.haml @@ -37,5 +37,5 @@ - if @group_links.any? = render 'projects/project_members/groups', group_links: @group_links - = render 'projects/project_members/team', members: @project_members + = render 'projects/project_members/team', project: @project, members: @project_members = paginate @project_members, theme: "gitlab" diff --git a/app/views/projects/tags/new.html.haml b/app/views/projects/tags/new.html.haml index 031efa903c5..6e105a5521a 100644 --- a/app/views/projects/tags/new.html.haml +++ b/app/views/projects/tags/new.html.haml @@ -14,13 +14,13 @@ .form-group = label_tag :tag_name, nil, class: 'control-label' .col-sm-10 - = text_field_tag :tag_name, params[:tag_name], required: true, tabindex: 1, autofocus: true, class: 'form-control' + = text_field_tag :tag_name, params[:tag_name], required: true, autofocus: true, class: 'form-control' .form-group = label_tag :ref, 'Create from', class: 'control-label' .col-sm-10.create-from .dropdown = hidden_field_tag :ref, default_ref - = button_tag type: 'button', title: default_ref, class: 'dropdown-menu-toggle wide form-control js-branch-select', required: true, data: { toggle: 'dropdown', selected: default_ref, field_name: 'ref' } do + = button_tag type: 'button', title: default_ref, class: 'dropdown-menu-toggle wide js-branch-select', required: true, data: { toggle: 'dropdown', selected: default_ref, field_name: 'ref' } do .text-left.dropdown-toggle-text= default_ref = render 'shared/ref_dropdown', dropdown_class: 'wide' .help-block @@ -28,7 +28,7 @@ .form-group = label_tag :message, nil, class: 'control-label' .col-sm-10 - = text_area_tag :message, @message, required: false, tabindex: 3, class: 'form-control', rows: 5 + = text_area_tag :message, @message, required: false, class: 'form-control', rows: 5 .help-block = s_('TagsPage|Optionally, add a message to the tag.') %hr @@ -41,6 +41,6 @@ .help-block = s_('TagsPage|Optionally, add release notes to the tag. They will be stored in the GitLab database and displayed on the tags page.') .form-actions - = button_tag s_('TagsPage|Create tag'), class: 'btn btn-create', tabindex: 3 + = button_tag s_('TagsPage|Create tag'), class: 'btn btn-create' = link_to s_('TagsPage|Cancel'), project_tags_path(@project), class: 'btn btn-cancel' %script#availableRefs{ type: "application/json" }= @project.repository.ref_names.to_json.html_safe diff --git a/app/views/projects/tree/_blob_item.html.haml b/app/views/projects/tree/_blob_item.html.haml index c51af901699..8c1c532cb3e 100644 --- a/app/views/projects/tree/_blob_item.html.haml +++ b/app/views/projects/tree/_blob_item.html.haml @@ -1,9 +1,12 @@ +- is_lfs_blob = @lfs_blob_ids.include?(blob_item.id) %tr{ class: "tree-item #{tree_hex_class(blob_item)}" } %td.tree-item-file-name = tree_icon(type, blob_item.mode, blob_item.name) - file_name = blob_item.name = link_to project_blob_path(@project, tree_join(@id || @commit.id, blob_item.name)), class: 'str-truncated', title: file_name do %span= file_name + - if is_lfs_blob + %span.label.label-lfs.prepend-left-5 LFS %td.hidden-xs.tree-commit %td.tree-time-ago.cgray.text-right = render 'projects/tree/spinner' diff --git a/app/views/shared/_label.html.haml b/app/views/shared/_label.html.haml index 3fcc33044e9..81d07074325 100644 --- a/app/views/shared/_label.html.haml +++ b/app/views/shared/_label.html.haml @@ -2,6 +2,8 @@ - status = label_subscription_status(label, @project).inquiry if current_user - subject = local_assigns[:subject] - toggle_subscription_path = toggle_subscription_label_path(label, @project) if current_user +- show_label_merge_requests_link = show_label_issuables_link?(label, :merge_requests, project: @project) +- show_label_issues_link = show_label_issuables_link?(label, :issues, project: @project) %li{ id: label_css_id, data: { id: label.id } } = render "shared/label_row", label: label @@ -12,12 +14,14 @@ = icon('caret-down') .dropdown-menu.dropdown-menu-align-right %ul - %li - = link_to_label(label, subject: subject, type: :merge_request) do - View merge requests - %li - = link_to_label(label, subject: subject) do - View open issues + - if show_label_merge_requests_link + %li + = link_to_label(label, subject: subject, type: :merge_request) do + View merge requests + - if show_label_issues_link + %li + = link_to_label(label, subject: subject) do + View open issues - if current_user %li.label-subscription - if can_subscribe_to_label_in_different_levels?(label) @@ -35,13 +39,20 @@ %li = link_to 'Edit', edit_label_path(label) %li - = link_to 'Delete', destroy_label_path(label), title: 'Delete', method: :delete, data: {confirm: 'Remove this label? Are you sure?'} + = link_to 'Delete', + destroy_label_path(label), + title: 'Delete', + method: :delete, + data: {confirm: 'Remove this label? Are you sure?'}, + class: 'text-danger' .pull-right.hidden-xs.hidden-sm.hidden-md - = link_to_label(label, subject: subject, type: :merge_request, css_class: 'btn btn-transparent btn-action btn-link') do - view merge requests - = link_to_label(label, subject: subject, css_class: 'btn btn-transparent btn-action btn-link') do - view open issues + - if show_label_merge_requests_link + = link_to_label(label, subject: subject, type: :merge_request, css_class: 'btn btn-transparent btn-action btn-link') do + view merge requests + - if show_label_issues_link + = link_to_label(label, subject: subject, css_class: 'btn btn-transparent btn-action btn-link') do + view open issues - if current_user .label-subscription.inline diff --git a/app/views/shared/_outdated_browser.html.haml b/app/views/shared/_outdated_browser.html.haml index a638b0a805e..8ddb1b2bc99 100644 --- a/app/views/shared/_outdated_browser.html.haml +++ b/app/views/shared/_outdated_browser.html.haml @@ -4,5 +4,5 @@ GitLab may not work properly because you are using an outdated web browser. %br Please install a - = link_to 'supported web browser', help_page_url('install/requirements', anchor: 'supported-web-browsers') + = link_to 'supported web browser', help_page_path('install/requirements', anchor: 'supported-web-browsers') for a better experience. diff --git a/app/views/shared/_recaptcha_form.html.haml b/app/views/shared/_recaptcha_form.html.haml new file mode 100644 index 00000000000..0e816870f15 --- /dev/null +++ b/app/views/shared/_recaptcha_form.html.haml @@ -0,0 +1,19 @@ +- resource_name = spammable.class.model_name.singular +- humanized_resource_name = spammable.class.model_name.human.downcase +- script = local_assigns.fetch(:script, true) +- has_submit = local_assigns.fetch(:has_submit, true) + += form_for resource_name, method: :post, html: { class: 'recaptcha-form js-recaptcha-form' } do |f| + .recaptcha + - params[resource_name].each do |field, value| + = hidden_field(resource_name, field, value: value) + = hidden_field_tag(:spam_log_id, spammable.spam_log.id) + = hidden_field_tag(:recaptcha_verification, true) + = recaptcha_tags script: script, callback: 'recaptchaDialogCallback' + + -# Yields a block with given extra params. + = yield + + - if has_submit + .row-content-block.footer-block + = f.submit "Submit #{humanized_resource_name}", class: 'btn btn-create' diff --git a/app/views/shared/_show_aside.html.haml b/app/views/shared/_show_aside.html.haml deleted file mode 100644 index 3ac9b11b4fa..00000000000 --- a/app/views/shared/_show_aside.html.haml +++ /dev/null @@ -1,2 +0,0 @@ -= link_to '#aside', class: 'show-aside' do - %i.fa.fa-angle-left diff --git a/app/views/shared/empty_states/_issues.html.haml b/app/views/shared/empty_states/_issues.html.haml index de26fa8bbf3..e039a73cd3b 100644 --- a/app/views/shared/empty_states/_issues.html.haml +++ b/app/views/shared/empty_states/_issues.html.haml @@ -6,18 +6,21 @@ .col-xs-12 .svg-content = image_tag 'illustrations/issues.svg' - .col-xs-12.text-center + .col-xs-12 .text-content - if has_button && current_user %h4 - The Issue Tracker is the place to add things that need to be improved or solved in a project + = _("The Issue Tracker is the place to add things that need to be improved or solved in a project") %p - Issues can be bugs, tasks or ideas to be discussed. - Also, issues are searchable and filterable. - - if project_select_button - = render 'shared/new_project_item_select', path: 'issues/new', label: 'New issue', type: :issues - - else - = link_to 'New issue', button_path, class: 'btn btn-new', title: 'New issue', id: 'new_issue_link' + = _("Issues can be bugs, tasks or ideas to be discussed. Also, issues are searchable and filterable.") + .text-center + - if project_select_button + = render 'shared/new_project_item_select', path: 'issues/new', label: 'New issue', type: :issues + - else + = link_to 'New issue', button_path, class: 'btn btn-success', title: 'New issue', id: 'new_issue_link' - else + %h4.text-center= _("There are no issues to show") + %p + = _("The Issue Tracker is the place to add things that need to be improved or solved in a project. You can register or sign in to create issues for this project.") .text-center - %h4 There are no issues to show. + = link_to _('Register / Sign In'), new_user_session_path, class: 'btn btn-success' diff --git a/app/views/shared/empty_states/_labels.html.haml b/app/views/shared/empty_states/_labels.html.haml index a65634dce53..04db9de3606 100644 --- a/app/views/shared/empty_states/_labels.html.haml +++ b/app/views/shared/empty_states/_labels.html.haml @@ -2,10 +2,10 @@ .col-xs-12 .svg-content = image_tag 'illustrations/labels.svg' - .col-xs-12.text-center + .col-xs-12 .text-content - %h4 Labels can be applied to issues and merge requests to categorize them. - %p You can also star a label to make it a priority label. + %h4= _("Labels can be applied to issues and merge requests to categorize them.") + %p= _("You can also star a label to make it a priority label.") - if can?(current_user, :admin_label, @project) - = link_to 'New label', new_project_label_path(@project), class: 'btn btn-new', title: 'New label', id: 'new_label_link' - = link_to 'Generate a default set of labels', generate_project_labels_path(@project), method: :post, class: 'btn btn-success btn-inverted', title: 'Generate a default set of labels', id: 'generate_labels_link' + = link_to _('New label'), new_project_label_path(@project), class: 'btn btn-new', title: _('New label'), id: 'new_label_link' + = link_to _('Generate a default set of labels'), generate_project_labels_path(@project), method: :post, class: 'btn btn-success btn-inverted', title: _('Generate a default set of labels'), id: 'generate_labels_link' diff --git a/app/views/shared/empty_states/_merge_requests.html.haml b/app/views/shared/empty_states/_merge_requests.html.haml index 67f906903e9..2edf3557df4 100644 --- a/app/views/shared/empty_states/_merge_requests.html.haml +++ b/app/views/shared/empty_states/_merge_requests.html.haml @@ -6,17 +6,18 @@ .col-xs-12 .svg-content = image_tag 'illustrations/merge_requests.svg' - .col-xs-12.text-center + .col-xs-12 .text-content - if has_button %h4 - Merge requests are a place to propose changes you've made to a project and discuss those changes with others. + = _("Merge requests are a place to propose changes you've made to a project and discuss those changes with others") %p - Interested parties can even contribute by pushing commits if they want to. - - if project_select_button - = render 'shared/new_project_item_select', path: 'merge_requests/new', label: 'New merge request', type: :merge_requests - - else - = link_to 'New merge request', button_path, class: 'btn btn-new', title: 'New merge request', id: 'new_merge_request_link' + = _("Interested parties can even contribute by pushing commits if they want to.") + .text-center + - if project_select_button + = render 'shared/new_project_item_select', path: 'merge_requests/new', label: _('New merge request'), type: :merge_requests + - else + = link_to _('New merge request'), button_path, class: 'btn btn-new', title: _('New merge request'), id: 'new_merge_request_link' - else %h4.text-center - There are no merge requests to show. + = _("There are no merge requests to show") diff --git a/app/views/shared/members/_member.html.haml b/app/views/shared/members/_member.html.haml index 2c27dd638a7..71878e93255 100644 --- a/app/views/shared/members/_member.html.haml +++ b/app/views/shared/members/_member.html.haml @@ -1,9 +1,9 @@ - show_roles = local_assigns.fetch(:show_roles, true) - show_controls = local_assigns.fetch(:show_controls, true) - force_mobile_view = local_assigns.fetch(:force_mobile_view, false) +- member = local_assigns.fetch(:member) - user = local_assigns.fetch(:user, member.user) - source = member.source -- can_admin_member = can?(current_user, action_member_permission(:update, member), member) %li.member{ class: dom_class(member), id: dom_id(member) } %span.list-item-name @@ -50,18 +50,17 @@ .controls.member-controls - if show_controls && member.source == current_resource - - if member.invite? && can?(current_user, action_member_permission(:admin, member), member.source) + - if member.can_resend_invite? = link_to icon('paper-plane'), polymorphic_path([:resend_invite, member]), method: :post, class: 'btn btn-default prepend-left-10 hidden-xs', title: 'Resend invite' - - if user != current_user && can_admin_member + - if user != current_user && member.can_update? = form_for member, remote: true, html: { class: 'form-horizontal js-edit-member-form' } do |f| = f.hidden_field :access_level .member-form-control.dropdown.append-right-5 %button.dropdown-menu-toggle.js-member-permissions-dropdown{ type: "button", - disabled: !can_admin_member, data: { toggle: "dropdown", field_name: "#{f.object_name}[access_level]" } } %span.dropdown-toggle-text = member.human_access @@ -70,23 +69,22 @@ = dropdown_title("Change permissions") .dropdown-content %ul - - member.class.access_level_roles.each do |role, role_id| + - member.access_level_roles.each do |role, role_id| %li = link_to role, "javascript:void(0)", class: ("is-active" if member.access_level == role_id), data: { id: role_id, el_id: dom_id(member) } .prepend-left-5.clearable-input.member-form-control - = f.text_field :expires_at, class: 'form-control js-access-expiration-date js-member-update-control', placeholder: 'Expiration date', id: "member_expires_at_#{member.id}", disabled: !can_admin_member, data: { el_id: dom_id(member) } + = f.text_field :expires_at, + class: 'form-control js-access-expiration-date js-member-update-control', + placeholder: 'Expiration date', + id: "member_expires_at_#{member.id}", + data: { el_id: dom_id(member) } %i.clear-icon.js-clear-input - else %span.member-access-text= member.human_access - - if member.invite? && can?(current_user, action_member_permission(:admin, member), member.source) - = link_to 'Resend invite', polymorphic_path([:resend_invite, member]), - method: :post, - class: 'btn btn-default prepend-left-10 visible-xs-block' - - - elsif member.request? && can_admin_member + - if member.can_approve? = link_to polymorphic_path([:approve_access_request, member]), method: :post, class: 'btn btn-success prepend-left-10', @@ -96,7 +94,7 @@ - unless force_mobile_view = icon('check inverse', class: 'hidden-xs') - - if can?(current_user, action_member_permission(:destroy, member), member) + - if member.can_remove? - if current_user == user = link_to icon('sign-out', text: 'Leave'), polymorphic_path([:leave, member.source, :members]), method: :delete, diff --git a/app/views/shared/members/_requests.html.haml b/app/views/shared/members/_requests.html.haml index 09b9944082f..1fbd6bcc4cb 100644 --- a/app/views/shared/members/_requests.html.haml +++ b/app/views/shared/members/_requests.html.haml @@ -1,10 +1,13 @@ +- membership_source = local_assigns.fetch(:membership_source) +- requesters = local_assigns.fetch(:requesters) - force_mobile_view = local_assigns.fetch(:force_mobile_view, false) -- if requesters.any? - .panel.panel-default.prepend-top-default{ class: ('panel-mobile' if force_mobile_view ) } - .panel-heading - Users requesting access to - %strong= membership_source.name - %span.badge= requesters.size - %ul.content-list.members-list - = render partial: 'shared/members/member', collection: requesters, as: :member, locals: { force_mobile_view: force_mobile_view } +- return if requesters.empty? + +.panel.panel-default.prepend-top-default{ class: ('panel-mobile' if force_mobile_view ) } + .panel-heading + Users requesting access to + %strong= membership_source.name + %span.badge= requesters.size + %ul.content-list.members-list + = render partial: 'shared/members/member', collection: requesters, as: :member, locals: { force_mobile_view: force_mobile_view } diff --git a/app/views/shared/notes/_note.html.haml b/app/views/shared/notes/_note.html.haml index c978d9e4821..98e0161f7d1 100644 --- a/app/views/shared/notes/_note.html.haml +++ b/app/views/shared/notes/_note.html.haml @@ -20,8 +20,8 @@ - if note.is_a?(DiffNote) && note.on_image? - if show_image_comment_badge && note_counter == 0 -# Only show this for the first comment in the discussion - %span.image-comment-badge.inverted - = icon('comment-o') + %span.image-comment-badge + = sprite_icon('image-comment-dark') - elsif note_counter == 0 - counter = badge_counter if local_assigns[:badge_counter] - badge_class = "hidden" if @fresh_discussion || counter.nil? diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml new file mode 100644 index 00000000000..ba31a5aa9c2 --- /dev/null +++ b/app/workers/all_queues.yml @@ -0,0 +1,98 @@ +--- +- cronjob:admin_email +- cronjob:expire_build_artifacts +- cronjob:gitlab_usage_ping +- cronjob:import_export_project_cleanup +- cronjob:pipeline_schedule +- cronjob:prune_old_events +- cronjob:remove_expired_group_links +- cronjob:remove_expired_members +- cronjob:remove_old_web_hook_logs +- cronjob:remove_unreferenced_lfs_objects +- cronjob:repository_archive_cache +- cronjob:repository_check_batch +- cronjob:requests_profiles +- cronjob:schedule_update_user_activity +- cronjob:stuck_ci_jobs +- cronjob:stuck_import_jobs +- cronjob:stuck_merge_jobs +- cronjob:trending_projects + +- gcp_cluster:cluster_install_app +- gcp_cluster:cluster_provision +- gcp_cluster:cluster_wait_for_app_installation +- gcp_cluster:wait_for_cluster_creation + +- github_import_advance_stage +- github_importer:github_import_import_diff_note +- github_importer:github_import_import_issue +- github_importer:github_import_import_note +- github_importer:github_import_import_pull_request +- github_importer:github_import_refresh_import_jid +- github_importer:github_import_stage_finish_import +- github_importer:github_import_stage_import_base_data +- github_importer:github_import_stage_import_issues_and_diff_notes +- github_importer:github_import_stage_import_notes +- github_importer:github_import_stage_import_pull_requests +- github_importer:github_import_stage_import_repository + +- pipeline_cache:expire_job_cache +- pipeline_cache:expire_pipeline_cache +- pipeline_creation:create_pipeline +- pipeline_default:build_coverage +- pipeline_default:build_trace_sections +- pipeline_default:pipeline_metrics +- pipeline_default:pipeline_notification +- pipeline_default:update_head_pipeline_for_merge_request +- pipeline_hooks:build_hooks +- pipeline_hooks:pipeline_hooks +- pipeline_processing:build_finished +- pipeline_processing:build_queue +- pipeline_processing:build_success +- pipeline_processing:pipeline_process +- pipeline_processing:pipeline_success +- pipeline_processing:pipeline_update +- pipeline_processing:stage_update + +- repository_check:repository_check_clear +- repository_check:repository_check_single_repository + +- default +- mailers # ActionMailer::DeliveryJob.queue_name + +- authorized_projects +- background_migration +- create_gpg_signature +- delete_merged_branches +- delete_user +- email_receiver +- emails_on_push +- expire_build_instance_artifacts +- git_garbage_collect +- gitlab_shell +- group_destroy +- invalid_gpg_signature_update +- irker +- merge +- namespaceless_project_destroy +- new_issue +- new_merge_request +- new_note +- pages +- post_receive +- process_commit +- project_cache +- project_destroy +- project_export +- project_migrate_hashed_storage +- project_service +- propagate_service_template +- reactive_caching +- repository_fork +- repository_import +- storage_migrator +- system_hook_push +- update_merge_requests +- update_user_activity +- upload_checksum +- web_hook diff --git a/app/workers/build_finished_worker.rb b/app/workers/build_finished_worker.rb index 5efa9180f5e..97d80305bec 100644 --- a/app/workers/build_finished_worker.rb +++ b/app/workers/build_finished_worker.rb @@ -2,7 +2,7 @@ class BuildFinishedWorker include ApplicationWorker include PipelineQueue - enqueue_in group: :processing + queue_namespace :pipeline_processing def perform(build_id) Ci::Build.find_by(id: build_id).try do |build| diff --git a/app/workers/build_hooks_worker.rb b/app/workers/build_hooks_worker.rb index 6705a1c2709..cbfca8c342c 100644 --- a/app/workers/build_hooks_worker.rb +++ b/app/workers/build_hooks_worker.rb @@ -2,7 +2,7 @@ class BuildHooksWorker include ApplicationWorker include PipelineQueue - enqueue_in group: :hooks + queue_namespace :pipeline_hooks def perform(build_id) Ci::Build.find_by(id: build_id) diff --git a/app/workers/build_queue_worker.rb b/app/workers/build_queue_worker.rb index fc775a84dc0..e4f4e6c1d9e 100644 --- a/app/workers/build_queue_worker.rb +++ b/app/workers/build_queue_worker.rb @@ -2,7 +2,7 @@ class BuildQueueWorker include ApplicationWorker include PipelineQueue - enqueue_in group: :processing + queue_namespace :pipeline_processing def perform(build_id) Ci::Build.find_by(id: build_id).try do |build| diff --git a/app/workers/build_success_worker.rb b/app/workers/build_success_worker.rb index ec049821ad7..4b9097bc5e4 100644 --- a/app/workers/build_success_worker.rb +++ b/app/workers/build_success_worker.rb @@ -2,7 +2,7 @@ class BuildSuccessWorker include ApplicationWorker include PipelineQueue - enqueue_in group: :processing + queue_namespace :pipeline_processing def perform(build_id) Ci::Build.find_by(id: build_id).try do |build| diff --git a/app/workers/concerns/application_worker.rb b/app/workers/concerns/application_worker.rb index 9c3bdabc49e..37586e161c9 100644 --- a/app/workers/concerns/application_worker.rb +++ b/app/workers/concerns/application_worker.rb @@ -3,13 +3,23 @@ Sidekiq::Worker.extend ActiveSupport::Concern module ApplicationWorker extend ActiveSupport::Concern - include Sidekiq::Worker + include Sidekiq::Worker # rubocop:disable Cop/IncludeSidekiqWorker included do - sidekiq_options queue: base_queue_name + set_queue end module ClassMethods + def inherited(subclass) + subclass.set_queue + end + + def set_queue + queue_name = [queue_namespace, base_queue_name].compact.join(':') + + sidekiq_options queue: queue_name # rubocop:disable Cop/SidekiqOptionsQueue + end + def base_queue_name name .sub(/\AGitlab::/, '') @@ -18,6 +28,16 @@ module ApplicationWorker .tr('/', '_') end + def queue_namespace(new_namespace = nil) + if new_namespace + sidekiq_options queue_namespace: new_namespace + + set_queue + else + get_sidekiq_options['queue_namespace']&.to_s + end + end + def queue get_sidekiq_options['queue'].to_s end diff --git a/app/workers/concerns/cluster_queue.rb b/app/workers/concerns/cluster_queue.rb index a5074d13220..24b9f145220 100644 --- a/app/workers/concerns/cluster_queue.rb +++ b/app/workers/concerns/cluster_queue.rb @@ -5,6 +5,6 @@ module ClusterQueue extend ActiveSupport::Concern included do - sidekiq_options queue: :gcp_cluster + queue_namespace :gcp_cluster end end diff --git a/app/workers/concerns/cronjob_queue.rb b/app/workers/concerns/cronjob_queue.rb index e918bb011e0..b6581779f6a 100644 --- a/app/workers/concerns/cronjob_queue.rb +++ b/app/workers/concerns/cronjob_queue.rb @@ -4,6 +4,7 @@ module CronjobQueue extend ActiveSupport::Concern included do - sidekiq_options queue: :cronjob, retry: false + queue_namespace :cronjob + sidekiq_options retry: false end end diff --git a/app/workers/concerns/gitlab/github_import/queue.rb b/app/workers/concerns/gitlab/github_import/queue.rb index a2bee361b86..22c2ce458e8 100644 --- a/app/workers/concerns/gitlab/github_import/queue.rb +++ b/app/workers/concerns/gitlab/github_import/queue.rb @@ -4,12 +4,14 @@ module Gitlab extend ActiveSupport::Concern included do + queue_namespace :github_importer + # If a job produces an error it may block a stage from advancing # forever. To prevent this from happening we prevent jobs from going to # the dead queue. This does mean some resources may not be imported, but # this is better than a project being stuck in the "import" state # forever. - sidekiq_options queue: 'github_importer', dead: false, retry: 5 + sidekiq_options dead: false, retry: 5 end end end diff --git a/app/workers/concerns/new_issuable.rb b/app/workers/concerns/new_issuable.rb index eb0d6c9c36c..526ed0bad07 100644 --- a/app/workers/concerns/new_issuable.rb +++ b/app/workers/concerns/new_issuable.rb @@ -9,15 +9,15 @@ module NewIssuable end def set_user(user_id) - @user = User.find_by(id: user_id) + @user = User.find_by(id: user_id) # rubocop:disable Gitlab/ModuleWithInstanceVariables - log_error(User, user_id) unless @user + log_error(User, user_id) unless @user # rubocop:disable Gitlab/ModuleWithInstanceVariables end def set_issuable(issuable_id) - @issuable = issuable_class.find_by(id: issuable_id) + @issuable = issuable_class.find_by(id: issuable_id) # rubocop:disable Gitlab/ModuleWithInstanceVariables - log_error(issuable_class, issuable_id) unless @issuable + log_error(issuable_class, issuable_id) unless @issuable # rubocop:disable Gitlab/ModuleWithInstanceVariables end def log_error(record_class, record_id) diff --git a/app/workers/concerns/pipeline_queue.rb b/app/workers/concerns/pipeline_queue.rb index ddf45b91345..e77093a6902 100644 --- a/app/workers/concerns/pipeline_queue.rb +++ b/app/workers/concerns/pipeline_queue.rb @@ -5,14 +5,6 @@ module PipelineQueue extend ActiveSupport::Concern included do - sidekiq_options queue: 'pipeline_default' - end - - class_methods do - def enqueue_in(group:) - raise ArgumentError, 'Unspecified queue group!' if group.empty? - - sidekiq_options queue: "pipeline_#{group}" - end + queue_namespace :pipeline_default end end diff --git a/app/workers/concerns/repository_check_queue.rb b/app/workers/concerns/repository_check_queue.rb index a597321ccf4..43fb66c31b0 100644 --- a/app/workers/concerns/repository_check_queue.rb +++ b/app/workers/concerns/repository_check_queue.rb @@ -3,6 +3,8 @@ module RepositoryCheckQueue extend ActiveSupport::Concern included do - sidekiq_options queue: :repository_check, retry: false + queue_namespace :repository_check + + sidekiq_options retry: false end end diff --git a/app/workers/create_pipeline_worker.rb b/app/workers/create_pipeline_worker.rb index 00cd7b85b9f..c3ac35e54f5 100644 --- a/app/workers/create_pipeline_worker.rb +++ b/app/workers/create_pipeline_worker.rb @@ -2,7 +2,7 @@ class CreatePipelineWorker include ApplicationWorker include PipelineQueue - enqueue_in group: :creation + queue_namespace :pipeline_creation def perform(project_id, user_id, ref, source, params = {}) project = Project.find(project_id) diff --git a/app/workers/expire_job_cache_worker.rb b/app/workers/expire_job_cache_worker.rb index a591e2da519..7217364a9f2 100644 --- a/app/workers/expire_job_cache_worker.rb +++ b/app/workers/expire_job_cache_worker.rb @@ -2,7 +2,7 @@ class ExpireJobCacheWorker include ApplicationWorker include PipelineQueue - enqueue_in group: :cache + queue_namespace :pipeline_cache def perform(job_id) job = CommitStatus.joins(:pipeline, :project).find_by(id: job_id) diff --git a/app/workers/expire_pipeline_cache_worker.rb b/app/workers/expire_pipeline_cache_worker.rb index a3ac32b437d..3e34de22c19 100644 --- a/app/workers/expire_pipeline_cache_worker.rb +++ b/app/workers/expire_pipeline_cache_worker.rb @@ -2,7 +2,7 @@ class ExpirePipelineCacheWorker include ApplicationWorker include PipelineQueue - enqueue_in group: :cache + queue_namespace :pipeline_cache def perform(pipeline_id) pipeline = Ci::Pipeline.find_by(id: pipeline_id) diff --git a/app/workers/gitlab/github_import/advance_stage_worker.rb b/app/workers/gitlab/github_import/advance_stage_worker.rb index 400396d5755..f7f498af840 100644 --- a/app/workers/gitlab/github_import/advance_stage_worker.rb +++ b/app/workers/gitlab/github_import/advance_stage_worker.rb @@ -9,7 +9,7 @@ module Gitlab class AdvanceStageWorker include ApplicationWorker - sidekiq_options queue: 'github_importer_advance_stage', dead: false + sidekiq_options dead: false INTERVAL = 30.seconds.to_i diff --git a/app/workers/pages_worker.rb b/app/workers/pages_worker.rb index 62f733c02fc..3ec81d040b4 100644 --- a/app/workers/pages_worker.rb +++ b/app/workers/pages_worker.rb @@ -1,7 +1,7 @@ class PagesWorker include ApplicationWorker - sidekiq_options queue: :pages, retry: false + sidekiq_options retry: false def perform(action, *arg) send(action, *arg) # rubocop:disable GitlabSecurity/PublicSend diff --git a/app/workers/pipeline_hooks_worker.rb b/app/workers/pipeline_hooks_worker.rb index 661c29efe88..c94918ff4ee 100644 --- a/app/workers/pipeline_hooks_worker.rb +++ b/app/workers/pipeline_hooks_worker.rb @@ -2,7 +2,7 @@ class PipelineHooksWorker include ApplicationWorker include PipelineQueue - enqueue_in group: :hooks + queue_namespace :pipeline_hooks def perform(pipeline_id) Ci::Pipeline.find_by(id: pipeline_id) diff --git a/app/workers/pipeline_process_worker.rb b/app/workers/pipeline_process_worker.rb index 07dbf6a971e..24424b3f472 100644 --- a/app/workers/pipeline_process_worker.rb +++ b/app/workers/pipeline_process_worker.rb @@ -2,7 +2,7 @@ class PipelineProcessWorker include ApplicationWorker include PipelineQueue - enqueue_in group: :processing + queue_namespace :pipeline_processing def perform(pipeline_id) Ci::Pipeline.find_by(id: pipeline_id) diff --git a/app/workers/pipeline_success_worker.rb b/app/workers/pipeline_success_worker.rb index 68c40a259e1..2ab0739a17f 100644 --- a/app/workers/pipeline_success_worker.rb +++ b/app/workers/pipeline_success_worker.rb @@ -2,7 +2,7 @@ class PipelineSuccessWorker include ApplicationWorker include PipelineQueue - enqueue_in group: :processing + queue_namespace :pipeline_processing def perform(pipeline_id) Ci::Pipeline.find_by(id: pipeline_id).try do |pipeline| diff --git a/app/workers/pipeline_update_worker.rb b/app/workers/pipeline_update_worker.rb index 24a8a9fbed5..fc9da2d45b1 100644 --- a/app/workers/pipeline_update_worker.rb +++ b/app/workers/pipeline_update_worker.rb @@ -2,7 +2,7 @@ class PipelineUpdateWorker include ApplicationWorker include PipelineQueue - enqueue_in group: :processing + queue_namespace :pipeline_processing def perform(pipeline_id) Ci::Pipeline.find_by(id: pipeline_id) diff --git a/app/workers/stage_update_worker.rb b/app/workers/stage_update_worker.rb index 69f2318d83b..e4b683fca33 100644 --- a/app/workers/stage_update_worker.rb +++ b/app/workers/stage_update_worker.rb @@ -2,7 +2,7 @@ class StageUpdateWorker include ApplicationWorker include PipelineQueue - enqueue_in group: :processing + queue_namespace :pipeline_processing def perform(stage_id) Ci::Stage.find_by(id: stage_id).try do |stage| diff --git a/app/workers/stuck_merge_jobs_worker.rb b/app/workers/stuck_merge_jobs_worker.rb index 36d2a2e6466..16394293c79 100644 --- a/app/workers/stuck_merge_jobs_worker.rb +++ b/app/workers/stuck_merge_jobs_worker.rb @@ -23,7 +23,12 @@ class StuckMergeJobsWorker merge_requests = MergeRequest.where(id: completed_ids) merge_requests.where.not(merge_commit_sha: nil).update_all(state: :merged) - merge_requests.where(merge_commit_sha: nil).update_all(state: :opened, merge_jid: nil) + + merge_requests_to_reopen = merge_requests.where(merge_commit_sha: nil) + + # Do not reopen merge requests using direct queries. + # We rely on state machine callbacks to update head_pipeline_id + merge_requests_to_reopen.each(&:unlock_mr) Rails.logger.info("Updated state of locked merge jobs. JIDs: #{completed_jids.join(', ')}") end diff --git a/app/workers/update_head_pipeline_for_merge_request_worker.rb b/app/workers/update_head_pipeline_for_merge_request_worker.rb index 0a2e9b63578..f09d89aa170 100644 --- a/app/workers/update_head_pipeline_for_merge_request_worker.rb +++ b/app/workers/update_head_pipeline_for_merge_request_worker.rb @@ -1,15 +1,25 @@ class UpdateHeadPipelineForMergeRequestWorker include ApplicationWorker - - sidekiq_options queue: 'pipeline_default' + include PipelineQueue def perform(merge_request_id) merge_request = MergeRequest.find(merge_request_id) pipeline = Ci::Pipeline.where(project: merge_request.source_project, ref: merge_request.source_branch).last return unless pipeline && pipeline.latest? - raise ArgumentError, 'merge request sha does not equal pipeline sha' if merge_request.diff_head_sha != pipeline.sha + + if merge_request.diff_head_sha != pipeline.sha + log_error_message_for(merge_request) + + return + end merge_request.update_attribute(:head_pipeline_id, pipeline.id) end + + def log_error_message_for(merge_request) + Rails.logger.error( + "Outdated head pipeline for active merge request: id=#{merge_request.id}, source_branch=#{merge_request.source_branch}, diff_head_sha=#{merge_request.diff_head_sha}" + ) + end end diff --git a/bin/storage_check b/bin/storage_check new file mode 100755 index 00000000000..5a818732bd1 --- /dev/null +++ b/bin/storage_check @@ -0,0 +1,11 @@ +#!/usr/bin/env ruby + +require 'optparse' +require 'net/http' +require 'json' +require 'socket' +require 'logger' + +require_relative '../lib/gitlab/storage_check' + +Gitlab::StorageCheck::CLI.start!(ARGV) diff --git a/changelogs/unreleased/13695-order-contributors-in-api.yml b/changelogs/unreleased/13695-order-contributors-in-api.yml new file mode 100644 index 00000000000..26bf8650a4a --- /dev/null +++ b/changelogs/unreleased/13695-order-contributors-in-api.yml @@ -0,0 +1,5 @@ +--- +title: Adds ordering to projects contributors in API +merge_request: 15469 +author: Jacopo Beschi @jacopo-beschi +type: added diff --git a/changelogs/unreleased/15832-fix-access-level-update-for-requesters.yml b/changelogs/unreleased/15832-fix-access-level-update-for-requesters.yml new file mode 100644 index 00000000000..9d6c958cb3e --- /dev/null +++ b/changelogs/unreleased/15832-fix-access-level-update-for-requesters.yml @@ -0,0 +1,5 @@ +--- +title: Fix error that was preventing users to change the access level of access requests for Groups or Projects +merge_request: 15832 +author: +type: fixed diff --git a/changelogs/unreleased/25317-prioritize-author-date-over-commit.yml b/changelogs/unreleased/25317-prioritize-author-date-over-commit.yml new file mode 100644 index 00000000000..a5f6d316a7d --- /dev/null +++ b/changelogs/unreleased/25317-prioritize-author-date-over-commit.yml @@ -0,0 +1,5 @@ +--- +title: Show authored date rather than committed date on the commit list +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/28004-consider-refactoring-member-view-by-using-presenter.yml b/changelogs/unreleased/28004-consider-refactoring-member-view-by-using-presenter.yml new file mode 100644 index 00000000000..0e91d4ae403 --- /dev/null +++ b/changelogs/unreleased/28004-consider-refactoring-member-view-by-using-presenter.yml @@ -0,0 +1,4 @@ +--- +title: Refactor member view using a Presenter +merge_request: 9645 +author: TM Lee diff --git a/changelogs/unreleased/29483-no-feedback-when-checking-on-checklist-if-potential-spam-was-detected.yml b/changelogs/unreleased/29483-no-feedback-when-checking-on-checklist-if-potential-spam-was-detected.yml new file mode 100644 index 00000000000..6bfcc5e70de --- /dev/null +++ b/changelogs/unreleased/29483-no-feedback-when-checking-on-checklist-if-potential-spam-was-detected.yml @@ -0,0 +1,5 @@ +--- +title: Add recaptcha modal to issue updates detected as spam +merge_request: 15408 +author: +type: fixed diff --git a/changelogs/unreleased/33926-update-issuable-icons.yml b/changelogs/unreleased/33926-update-issuable-icons.yml new file mode 100644 index 00000000000..87076dde545 --- /dev/null +++ b/changelogs/unreleased/33926-update-issuable-icons.yml @@ -0,0 +1,5 @@ +--- +title: Update issuable status icons +merge_request: 15898 +author: +type: changed diff --git a/changelogs/unreleased/35385-allow-git-pull-push-on-project-redirects.yml b/changelogs/unreleased/35385-allow-git-pull-push-on-project-redirects.yml new file mode 100644 index 00000000000..31450287caf --- /dev/null +++ b/changelogs/unreleased/35385-allow-git-pull-push-on-project-redirects.yml @@ -0,0 +1,5 @@ +--- +title: Allow git pull/push on group/user/project redirects +merge_request: 15670 +author: +type: added diff --git a/changelogs/unreleased/35724-animate-sidebar.yml b/changelogs/unreleased/35724-animate-sidebar.yml new file mode 100644 index 00000000000..5d0b46a23c8 --- /dev/null +++ b/changelogs/unreleased/35724-animate-sidebar.yml @@ -0,0 +1,5 @@ +--- +title: Animate contextual sidebar on collapse/expand +merge_request: +author: +type: changed diff --git a/changelogs/unreleased/38019-hide-runner-token.yml b/changelogs/unreleased/38019-hide-runner-token.yml new file mode 100644 index 00000000000..11ae0a685ef --- /dev/null +++ b/changelogs/unreleased/38019-hide-runner-token.yml @@ -0,0 +1,5 @@ +--- +title: Hide runner token in CI/CD settings page +merge_request: +author: +type: added diff --git a/changelogs/unreleased/38032-deploy-markers-should-be-more-verbose.yml b/changelogs/unreleased/38032-deploy-markers-should-be-more-verbose.yml new file mode 100644 index 00000000000..a1f28b3ba0f --- /dev/null +++ b/changelogs/unreleased/38032-deploy-markers-should-be-more-verbose.yml @@ -0,0 +1,5 @@ +--- +title: Changed the deploy markers on the prometheus dashboard to be more verbose +merge_request: 38032 +author: +type: changed diff --git a/changelogs/unreleased/38145_ux_issues_in_system_info_page.yml b/changelogs/unreleased/38145_ux_issues_in_system_info_page.yml new file mode 100644 index 00000000000..d2358750518 --- /dev/null +++ b/changelogs/unreleased/38145_ux_issues_in_system_info_page.yml @@ -0,0 +1,5 @@ +--- +title: Fixes the wording of headers in system info page +merge_request: 15802 +author: Gilbert Roulot +type: fixed diff --git a/changelogs/unreleased/38239-update-toggle-design.yml b/changelogs/unreleased/38239-update-toggle-design.yml new file mode 100644 index 00000000000..4d9034e8515 --- /dev/null +++ b/changelogs/unreleased/38239-update-toggle-design.yml @@ -0,0 +1,5 @@ +--- +title: Update feature toggle design to use icons and make it i18n friendly +merge_request: 15904 +author: +type: changed diff --git a/changelogs/unreleased/38541-cancel-alignment.yml b/changelogs/unreleased/38541-cancel-alignment.yml new file mode 100644 index 00000000000..c6d5136dd57 --- /dev/null +++ b/changelogs/unreleased/38541-cancel-alignment.yml @@ -0,0 +1,5 @@ +--- +title: fix button alignment on MWPS component +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/39608-comment-on-image-discussions-tab-alignment.yml b/changelogs/unreleased/39608-comment-on-image-discussions-tab-alignment.yml new file mode 100644 index 00000000000..5021fe88caf --- /dev/null +++ b/changelogs/unreleased/39608-comment-on-image-discussions-tab-alignment.yml @@ -0,0 +1,5 @@ +--- +title: Update comment on image cursor and icons +merge_request: 15760 +author: +type: fixed diff --git a/changelogs/unreleased/40031-include-assset_sync-gem.yml b/changelogs/unreleased/40031-include-assset_sync-gem.yml new file mode 100644 index 00000000000..93ce565b32c --- /dev/null +++ b/changelogs/unreleased/40031-include-assset_sync-gem.yml @@ -0,0 +1,5 @@ +--- +title: Add assets_sync gem to Gemfile +merge_request: 15734 +author: +type: added diff --git a/changelogs/unreleased/40509_sorting_tags_api.yml b/changelogs/unreleased/40509_sorting_tags_api.yml new file mode 100644 index 00000000000..38b198d0fe3 --- /dev/null +++ b/changelogs/unreleased/40509_sorting_tags_api.yml @@ -0,0 +1,5 @@ +--- +title: add support for sorting in tags api +merge_request: 15772 +author: haseebeqx +type: added diff --git a/changelogs/unreleased/40555-replace-absolute-urls-with-related-branches-to-avoid-hostname.yml b/changelogs/unreleased/40555-replace-absolute-urls-with-related-branches-to-avoid-hostname.yml deleted file mode 100644 index 4f0eaf8472f..00000000000 --- a/changelogs/unreleased/40555-replace-absolute-urls-with-related-branches-to-avoid-hostname.yml +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: Fix related branches/Merge requests failing to load when the hostname setting - is changed -merge_request: -author: -type: fixed diff --git a/changelogs/unreleased/40711-fix-forking-hashed-projects.yml b/changelogs/unreleased/40711-fix-forking-hashed-projects.yml deleted file mode 100644 index 116d7d4e9cf..00000000000 --- a/changelogs/unreleased/40711-fix-forking-hashed-projects.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix the fork project functionality for projects with hashed storage -merge_request: 15671 -author: -type: fixed diff --git a/changelogs/unreleased/40715-updateendpoint-undefined-on-issue-page.yml b/changelogs/unreleased/40715-updateendpoint-undefined-on-issue-page.yml deleted file mode 100644 index 0328a693354..00000000000 --- a/changelogs/unreleased/40715-updateendpoint-undefined-on-issue-page.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix updateEndpoint undefined error for issue_show app root -merge_request: 15698 -author: -type: fixed diff --git a/changelogs/unreleased/40895-fix-frequent-projects-stale-path.yml b/changelogs/unreleased/40895-fix-frequent-projects-stale-path.yml new file mode 100644 index 00000000000..485133b46a7 --- /dev/null +++ b/changelogs/unreleased/40895-fix-frequent-projects-stale-path.yml @@ -0,0 +1,5 @@ +--- +title: Use relative URL for projects to avoid storing domains +merge_request: 15876 +author: +type: fixed diff --git a/changelogs/unreleased/41016-import-gitlab-shell-projects.yml b/changelogs/unreleased/41016-import-gitlab-shell-projects.yml new file mode 100644 index 00000000000..47a9e9c3eec --- /dev/null +++ b/changelogs/unreleased/41016-import-gitlab-shell-projects.yml @@ -0,0 +1,6 @@ +--- +title: Import some code and functionality from gitlab-shell to improve subprocess + handling +merge_request: +author: +type: other diff --git a/changelogs/unreleased/add-tcp-check-rake-task.yml b/changelogs/unreleased/add-tcp-check-rake-task.yml new file mode 100644 index 00000000000..a7c04bd0d55 --- /dev/null +++ b/changelogs/unreleased/add-tcp-check-rake-task.yml @@ -0,0 +1,5 @@ +--- +title: Add a gitlab:tcp_check rake task +merge_request: 15759 +author: +type: added diff --git a/changelogs/unreleased/anchor-issue-references.yml b/changelogs/unreleased/anchor-issue-references.yml new file mode 100644 index 00000000000..78896427417 --- /dev/null +++ b/changelogs/unreleased/anchor-issue-references.yml @@ -0,0 +1,6 @@ +--- +title: Fix false positive issue references in merge requests caused by header anchor + links. +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/bvl-circuitbreaker-keys-set.yml b/changelogs/unreleased/bvl-circuitbreaker-keys-set.yml deleted file mode 100644 index a56456240df..00000000000 --- a/changelogs/unreleased/bvl-circuitbreaker-keys-set.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Keep track of all circuitbreaker keys in a set -merge_request: 15613 -author: -type: performance diff --git a/changelogs/unreleased/bvl-circuitbreaker-process.yml b/changelogs/unreleased/bvl-circuitbreaker-process.yml new file mode 100644 index 00000000000..595dd13f724 --- /dev/null +++ b/changelogs/unreleased/bvl-circuitbreaker-process.yml @@ -0,0 +1,5 @@ +--- +title: Monitor NFS shards for circuitbreaker in a separate process +merge_request: 15426 +author: +type: changed diff --git a/changelogs/unreleased/bvl-double-fork.yml b/changelogs/unreleased/bvl-double-fork.yml deleted file mode 100644 index 50bc1adde26..00000000000 --- a/changelogs/unreleased/bvl-double-fork.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Correctly link to a forked project from the new fork page. -merge_request: 15653 -author: -type: fixed diff --git a/changelogs/unreleased/bvl-fork-networks-for-deleted-projects.yml b/changelogs/unreleased/bvl-fork-networks-for-deleted-projects.yml deleted file mode 100644 index 2acb98db785..00000000000 --- a/changelogs/unreleased/bvl-fork-networks-for-deleted-projects.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Create a fork network for forks with a deleted source -merge_request: 15595 -author: -type: fixed diff --git a/changelogs/unreleased/dm-image-blob-diff-full-url.yml b/changelogs/unreleased/dm-image-blob-diff-full-url.yml new file mode 100644 index 00000000000..db44a5a16b5 --- /dev/null +++ b/changelogs/unreleased/dm-image-blob-diff-full-url.yml @@ -0,0 +1,5 @@ +--- +title: Use app host instead of asset host when rendering image blob or diff +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/dm-ldap-email-readonly.yml b/changelogs/unreleased/dm-ldap-email-readonly.yml new file mode 100644 index 00000000000..744b21f901e --- /dev/null +++ b/changelogs/unreleased/dm-ldap-email-readonly.yml @@ -0,0 +1,5 @@ +--- +title: Make sure user email is read only when synced with LDAP +merge_request: 15915 +author: +type: fixed diff --git a/changelogs/unreleased/docs-add-why-do-i-get-signed-out-authentication-section.yml b/changelogs/unreleased/docs-add-why-do-i-get-signed-out-authentication-section.yml new file mode 100644 index 00000000000..bc245880ed0 --- /dev/null +++ b/changelogs/unreleased/docs-add-why-do-i-get-signed-out-authentication-section.yml @@ -0,0 +1,5 @@ +--- +title: Add docs for why you might be signed out when using the Remember me token +merge_request: 15756 +author: +type: other diff --git a/changelogs/unreleased/feature-40842-provide-oracles-webgate-cookies-to-jira-requests.yml b/changelogs/unreleased/feature-40842-provide-oracles-webgate-cookies-to-jira-requests.yml new file mode 100644 index 00000000000..d5ff5bc4627 --- /dev/null +++ b/changelogs/unreleased/feature-40842-provide-oracles-webgate-cookies-to-jira-requests.yml @@ -0,0 +1,6 @@ +--- +title: Provide additional cookies to JIRA service requests to allow Oracle WebGates + Basic Auth +merge_request: +author: Stanislaw Wozniak +type: changed diff --git a/changelogs/unreleased/fix-create-mr-from-issue-with-template.yml b/changelogs/unreleased/fix-create-mr-from-issue-with-template.yml new file mode 100644 index 00000000000..8668aa18669 --- /dev/null +++ b/changelogs/unreleased/fix-create-mr-from-issue-with-template.yml @@ -0,0 +1,5 @@ +--- +title: Execute quick actions (if present) when creating MR from issue +merge_request: 15810 +author: +type: fixed diff --git a/changelogs/unreleased/fix-event-target-author-preloading.yml b/changelogs/unreleased/fix-event-target-author-preloading.yml new file mode 100644 index 00000000000..c6154cc0835 --- /dev/null +++ b/changelogs/unreleased/fix-event-target-author-preloading.yml @@ -0,0 +1,5 @@ +--- +title: Fix N+1 query when displaying events +merge_request: +author: +type: performance diff --git a/changelogs/unreleased/fix_build_count_in_pipeline_success_maild.yml b/changelogs/unreleased/fix_build_count_in_pipeline_success_maild.yml new file mode 100644 index 00000000000..c39bba62271 --- /dev/null +++ b/changelogs/unreleased/fix_build_count_in_pipeline_success_maild.yml @@ -0,0 +1,5 @@ +--- +title: fix build count in pipeline success mail +merge_request: 15827 +author: Christiaan Van den Poel +type: fixed diff --git a/changelogs/unreleased/issue-description-field-typo.yml b/changelogs/unreleased/issue-description-field-typo.yml new file mode 100644 index 00000000000..9c4c179876d --- /dev/null +++ b/changelogs/unreleased/issue-description-field-typo.yml @@ -0,0 +1,5 @@ +--- +title: Fixed typo for issue description field declaration +merge_request: +author: Marcus Amargi +type: fixed diff --git a/changelogs/unreleased/lfs-badge.yml b/changelogs/unreleased/lfs-badge.yml new file mode 100644 index 00000000000..e4ed4d6741f --- /dev/null +++ b/changelogs/unreleased/lfs-badge.yml @@ -0,0 +1,5 @@ +--- +title: Added badge to tree & blob views to indicate LFS tracked files +merge_request: +author: +type: added diff --git a/changelogs/unreleased/mk-fix-schema-dump-of-untracked-files-for-uploads.yml b/changelogs/unreleased/mk-fix-schema-dump-of-untracked-files-for-uploads.yml new file mode 100644 index 00000000000..2691e85320c --- /dev/null +++ b/changelogs/unreleased/mk-fix-schema-dump-of-untracked-files-for-uploads.yml @@ -0,0 +1,5 @@ +--- +title: Fix error during schema dump. +merge_request: 15866 +author: +type: fixed diff --git a/changelogs/unreleased/multiple-clusters-single-list.yml b/changelogs/unreleased/multiple-clusters-single-list.yml new file mode 100644 index 00000000000..55743f3c00e --- /dev/null +++ b/changelogs/unreleased/multiple-clusters-single-list.yml @@ -0,0 +1,5 @@ +--- +title: Present multiple clusters in a single list instead of a tabbed view +merge_request: 15669 +author: +type: changed diff --git a/changelogs/unreleased/optimize-issues-avoid-noop-empty-cache-updates2.yml b/changelogs/unreleased/optimize-issues-avoid-noop-empty-cache-updates2.yml new file mode 100644 index 00000000000..e0c3136be69 --- /dev/null +++ b/changelogs/unreleased/optimize-issues-avoid-noop-empty-cache-updates2.yml @@ -0,0 +1,6 @@ +--- +title: Treat empty markdown and html strings as valid cached text, not missing cache + that needs to be updated +merge_request: +author: +type: performance diff --git a/changelogs/unreleased/optimize-projects-for-imported-projects.yml b/changelogs/unreleased/optimize-projects-for-imported-projects.yml new file mode 100644 index 00000000000..13186fa36d5 --- /dev/null +++ b/changelogs/unreleased/optimize-projects-for-imported-projects.yml @@ -0,0 +1,6 @@ +--- +title: check the import_status field before doing SQL operations to check the import + url +merge_request: +author: +type: performance diff --git a/changelogs/unreleased/protected-branches-names.yml b/changelogs/unreleased/protected-branches-names.yml deleted file mode 100644 index 3c6767df571..00000000000 --- a/changelogs/unreleased/protected-branches-names.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Only load branch names for protected branch checks -merge_request: -author: -type: performance diff --git a/changelogs/unreleased/remove-incorrect-guidance.yml b/changelogs/unreleased/remove-incorrect-guidance.yml new file mode 100644 index 00000000000..eeb5745698f --- /dev/null +++ b/changelogs/unreleased/remove-incorrect-guidance.yml @@ -0,0 +1,6 @@ +--- +title: Removed incorrect guidance stating blocked users will be removed from groups + and project as members +merge_request: 15947 +author: CesarApodaca +type: fixed diff --git a/changelogs/unreleased/remove-tabindexes-from-tag-form.yml b/changelogs/unreleased/remove-tabindexes-from-tag-form.yml new file mode 100644 index 00000000000..a15bf2a7a4f --- /dev/null +++ b/changelogs/unreleased/remove-tabindexes-from-tag-form.yml @@ -0,0 +1,5 @@ +--- +title: removed tabindexes from tag form +merge_request: +author: Marcus Amargi +type: changed diff --git a/changelogs/unreleased/sh-fix-root-ref-repository.yml b/changelogs/unreleased/sh-fix-root-ref-repository.yml deleted file mode 100644 index 0670db84fa6..00000000000 --- a/changelogs/unreleased/sh-fix-root-ref-repository.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: "Gracefully handle case when repository's root ref does not exist" -merge_request: -author: -type: fixed diff --git a/changelogs/unreleased/sh-optimize-groups-api.yml b/changelogs/unreleased/sh-optimize-groups-api.yml deleted file mode 100644 index 37b74715a81..00000000000 --- a/changelogs/unreleased/sh-optimize-groups-api.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Optimize API /groups/:id/projects by preloading associations -merge_request: -author: -type: performance diff --git a/changelogs/unreleased/sh-remove-allocation-tracking-influxdb.yml b/changelogs/unreleased/sh-remove-allocation-tracking-influxdb.yml new file mode 100644 index 00000000000..b98573df303 --- /dev/null +++ b/changelogs/unreleased/sh-remove-allocation-tracking-influxdb.yml @@ -0,0 +1,5 @@ +--- +title: Remove allocation tracking code from InfluxDB sampler for performance +merge_request: +author: +type: performance diff --git a/changelogs/unreleased/sophie-h-gitlab-ce-patch-15.yml b/changelogs/unreleased/sophie-h-gitlab-ce-patch-15.yml new file mode 100644 index 00000000000..b5e3210c737 --- /dev/null +++ b/changelogs/unreleased/sophie-h-gitlab-ce-patch-15.yml @@ -0,0 +1,5 @@ +--- +title: Hide link to issues/MRs from labels list if issues/MRs are disabled. +merge_request: 15863 +author: Sophie Herold +type: fixed diff --git a/changelogs/unreleased/tc-correct-email-in-reply-to.yml b/changelogs/unreleased/tc-correct-email-in-reply-to.yml new file mode 100644 index 00000000000..1c8043f6a5c --- /dev/null +++ b/changelogs/unreleased/tc-correct-email-in-reply-to.yml @@ -0,0 +1,5 @@ +--- +title: Make mail notifications of discussion notes In-Reply-To of each other +merge_request: 14289 +author: +type: changed diff --git a/changelogs/unreleased/winh-translate-contributors-page-dates.yml b/changelogs/unreleased/winh-translate-contributors-page-dates.yml new file mode 100644 index 00000000000..74801bbd86e --- /dev/null +++ b/changelogs/unreleased/winh-translate-contributors-page-dates.yml @@ -0,0 +1,5 @@ +--- +title: Translate date ranges on contributors page +merge_request: 15846 +author: +type: changed diff --git a/changelogs/unreleased/zj-memoization-mr-commits.yml b/changelogs/unreleased/zj-memoization-mr-commits.yml new file mode 100644 index 00000000000..59dfc6d6049 --- /dev/null +++ b/changelogs/unreleased/zj-memoization-mr-commits.yml @@ -0,0 +1,5 @@ +--- +title: Cache commits for MergeRequest diffs +merge_request: +author: +type: performance diff --git a/config/application.rb b/config/application.rb index 6436f887d14..1110199b888 100644 --- a/config/application.rb +++ b/config/application.rb @@ -34,6 +34,10 @@ module Gitlab config.generators.templates.push("#{config.root}/generator_templates") + # Rake tasks ignore the eager loading settings, so we need to set the + # autoload paths explicitly + config.autoload_paths = config.eager_load_paths.dup + # Only load the plugins named here, in the order given (default is alphabetical). # :all can be used as a placeholder for all plugins not explicitly named. # config.plugins = [ :exception_notification, :ssl_requirement, :all ] @@ -159,7 +163,7 @@ module Gitlab config.middleware.insert_after ActionDispatch::Flash, 'Gitlab::Middleware::ReadOnly' config.generators do |g| - g.factory_girl false + g.factory_bot false end config.after_initialize do diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index c8b6018bc1b..f2f05b3eeb2 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -383,6 +383,7 @@ production: &base # Sync user's profile from the specified Omniauth providers every time the user logs in (default: empty). # Define the allowed providers using an array, e.g. ["cas3", "saml", "twitter"], # or as true/false to allow all providers or none. + # When authenticating using LDAP, the user's email is always synced. # sync_profile_from_provider: [] # Select which info to sync from the providers above. (default: email). diff --git a/config/initializers/active_record_schema_ignore_tables.rb b/config/initializers/active_record_schema_ignore_tables.rb new file mode 100644 index 00000000000..661135f8ade --- /dev/null +++ b/config/initializers/active_record_schema_ignore_tables.rb @@ -0,0 +1,2 @@ +# Ignore table used temporarily in background migration +ActiveRecord::SchemaDumper.ignore_tables = ["untracked_files_for_uploads"] diff --git a/config/initializers/asset_sync.rb b/config/initializers/asset_sync.rb new file mode 100644 index 00000000000..db8500f6231 --- /dev/null +++ b/config/initializers/asset_sync.rb @@ -0,0 +1,31 @@ +AssetSync.configure do |config| + # Disable the asset_sync gem by default. If it is enabled, but not configured, + # asset_sync will cause the build to fail. + config.enabled = if ENV.has_key?('ASSET_SYNC_ENABLED') + ENV['ASSET_SYNC_ENABLED'] == 'true' + else + false + end + + # Pulled from https://github.com/AssetSync/asset_sync/blob/v2.2.0/lib/asset_sync/engine.rb#L15-L40 + # This allows us to disable asset_sync by default and configure through environment variables + # Updates to asset_sync gem should be checked + config.fog_provider = ENV['FOG_PROVIDER'] if ENV.has_key?('FOG_PROVIDER') + config.fog_directory = ENV['FOG_DIRECTORY'] if ENV.has_key?('FOG_DIRECTORY') + config.fog_region = ENV['FOG_REGION'] if ENV.has_key?('FOG_REGION') + + config.aws_access_key_id = ENV['AWS_ACCESS_KEY_ID'] if ENV.has_key?('AWS_ACCESS_KEY_ID') + config.aws_secret_access_key = ENV['AWS_SECRET_ACCESS_KEY'] if ENV.has_key?('AWS_SECRET_ACCESS_KEY') + config.aws_reduced_redundancy = ENV['AWS_REDUCED_REDUNDANCY'] == true if ENV.has_key?('AWS_REDUCED_REDUNDANCY') + + config.rackspace_username = ENV['RACKSPACE_USERNAME'] if ENV.has_key?('RACKSPACE_USERNAME') + config.rackspace_api_key = ENV['RACKSPACE_API_KEY'] if ENV.has_key?('RACKSPACE_API_KEY') + + config.google_storage_access_key_id = ENV['GOOGLE_STORAGE_ACCESS_KEY_ID'] if ENV.has_key?('GOOGLE_STORAGE_ACCESS_KEY_ID') + config.google_storage_secret_access_key = ENV['GOOGLE_STORAGE_SECRET_ACCESS_KEY'] if ENV.has_key?('GOOGLE_STORAGE_SECRET_ACCESS_KEY') + + config.existing_remote_files = ENV['ASSET_SYNC_EXISTING_REMOTE_FILES'] || "keep" + + config.gzip_compression = (ENV['ASSET_SYNC_GZIP_COMPRESSION'] == 'true') if ENV.has_key?('ASSET_SYNC_GZIP_COMPRESSION') + config.manifest = (ENV['ASSET_SYNC_MANIFEST'] == 'true') if ENV.has_key?('ASSET_SYNC_MANIFEST') +end diff --git a/config/initializers/fix_local_cache_middleware.rb b/config/initializers/fix_local_cache_middleware.rb index cb37f9ed22c..2644ee6a7d3 100644 --- a/config/initializers/fix_local_cache_middleware.rb +++ b/config/initializers/fix_local_cache_middleware.rb @@ -6,7 +6,7 @@ module LocalCacheRegistryCleanupWithEnsure def call(env) LocalCacheRegistry.set_cache_for(local_cache_key, LocalStore.new) - response = @app.call(env) + response = @app.call(env) # rubocop:disable Gitlab/ModuleWithInstanceVariables response[2] = ::Rack::BodyProxy.new(response[2]) do LocalCacheRegistry.set_cache_for(local_cache_key, nil) end diff --git a/config/initializers/flipper.rb b/config/initializers/flipper.rb index bfab8c77a4b..cc9167d29b9 100644 --- a/config/initializers/flipper.rb +++ b/config/initializers/flipper.rb @@ -1,8 +1,22 @@ -require 'flipper/middleware/memoizer' +require 'flipper/adapters/active_record' +require 'flipper/adapters/active_support_cache_store' -unless Rails.env.test? - Rails.application.config.middleware.use Flipper::Middleware::Memoizer, - lambda { Feature.flipper } +Flipper.configure do |config| + config.default do + adapter = Flipper::Adapters::ActiveRecord.new( + feature_class: Feature::FlipperFeature, gate_class: Feature::FlipperGate) + cached_adapter = Flipper::Adapters::ActiveSupportCacheStore.new( + adapter, + Rails.cache, + expires_in: 10.seconds) + + Flipper.new(cached_adapter) + end +end - Feature.register_feature_groups +Feature.register_feature_groups + +unless Rails.env.test? + require 'flipper/middleware/memoizer' + Rails.application.config.middleware.use Flipper::Middleware::Memoizer end diff --git a/config/initializers/rspec_profiling.rb b/config/initializers/rspec_profiling.rb index 16b9d5b15e5..2de310753a9 100644 --- a/config/initializers/rspec_profiling.rb +++ b/config/initializers/rspec_profiling.rb @@ -19,10 +19,10 @@ module RspecProfilingExt def example_finished(*args) super rescue => err - return if @already_logged_example_finished_error + return if @already_logged_example_finished_error # rubocop:disable Gitlab/ModuleWithInstanceVariables $stderr.puts "rspec_profiling couldn't collect an example: #{err}. Further warnings suppressed." - @already_logged_example_finished_error = true + @already_logged_example_finished_error = true # rubocop:disable Gitlab/ModuleWithInstanceVariables end alias_method :example_passed, :example_finished diff --git a/config/initializers/rugged_use_gitlab_git_attributes.rb b/config/initializers/rugged_use_gitlab_git_attributes.rb index 7d652799786..1cfb3bcb4bd 100644 --- a/config/initializers/rugged_use_gitlab_git_attributes.rb +++ b/config/initializers/rugged_use_gitlab_git_attributes.rb @@ -15,8 +15,11 @@ module Rugged class Repository module UseGitlabGitAttributes def fetch_attributes(name, *) + attributes.attributes(name) + end + + def attributes @attributes ||= Gitlab::Git::Attributes.new(path) - @attributes.attributes(name) end end diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb index ba4481ae602..0f164e628f9 100644 --- a/config/initializers/sidekiq.rb +++ b/config/initializers/sidekiq.rb @@ -42,6 +42,8 @@ Sidekiq.configure_server do |config| Gitlab::SidekiqThrottler.execute! + Gitlab::SidekiqVersioning.install! + config = Gitlab::Database.config || Rails.application.config.database_configuration[Rails.env] config['pool'] = Sidekiq.options[:concurrency] @@ -60,19 +62,3 @@ Sidekiq.configure_client do |config| chain.add Gitlab::SidekiqStatus::ClientMiddleware end end - -# The Sidekiq client API always adds the queue to the Sidekiq queue -# list, but mail_room and gitlab-shell do not. This is only necessary -# for monitoring. -begin - queues = Gitlab::SidekiqConfig.worker_queues - - Sidekiq.redis do |conn| - conn.pipelined do - queues.each do |queue| - conn.sadd('queues', queue) - end - end - end -rescue Redis::BaseError, SocketError, Errno::ENOENT, Errno::EADDRNOTAVAIL, Errno::EAFNOSUPPORT, Errno::ECONNRESET, Errno::ECONNREFUSED -end diff --git a/config/no_todos_messages.yml b/config/no_todos_messages.yml index 264a975b614..da721a9b6e6 100644 --- a/config/no_todos_messages.yml +++ b/config/no_todos_messages.yml @@ -3,9 +3,9 @@ # # If you come up with a fun one, please feel free to contribute it to GitLab! # https://about.gitlab.com/contributing/ ---- -- Good job! Looks like you don't have any todos left. +--- +- Good job! Looks like you don't have any todos left - Isn't an empty todo list beautiful? - Give yourself a pat on the back! - Nothing left to do, high five! -- Henceforth you shall be known as "Todo Destroyer". +- Henceforth you shall be known as "Todo Destroyer" diff --git a/config/routes.rb b/config/routes.rb index 4f27fea0e92..016140e0ede 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -42,6 +42,7 @@ Rails.application.routes.draw do scope path: '-' do get 'liveness' => 'health#liveness' get 'readiness' => 'health#readiness' + post 'storage_check' => 'health#storage_check' resources :metrics, only: [:index] mount Peek::Railtie => '/peek' diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml index bc7c431731a..31a38f2b508 100644 --- a/config/sidekiq_queues.yml +++ b/config/sidekiq_queues.yml @@ -25,8 +25,6 @@ - [new_note, 2] - [new_issue, 2] - [new_merge_request, 2] - - [build, 2] - - [pipeline, 2] - [pipeline_processing, 5] - [pipeline_creation, 4] - [pipeline_default, 3] @@ -38,11 +36,12 @@ - [mailers, 2] - [invalid_gpg_signature_update, 2] - [create_gpg_signature, 2] + - [rebase, 2] - [upload_checksum, 1] - [repository_fork, 1] - [repository_import, 1] - [github_importer, 1] - - [github_importer_advance_stage, 1] + - [github_import_advance_stage, 1] - [project_service, 1] - [delete_user, 1] - [delete_merged_branches, 1] diff --git a/db/fixtures/development/17_cycle_analytics.rb b/db/fixtures/development/17_cycle_analytics.rb index 96c6d954ff7..d7be6f5950f 100644 --- a/db/fixtures/development/17_cycle_analytics.rb +++ b/db/fixtures/development/17_cycle_analytics.rb @@ -140,8 +140,8 @@ class Gitlab::Seeder::CycleAnalytics issue.update(milestone: @project.milestones.sample) else label_name = "#{FFaker::Product.brand}-#{FFaker::Product.brand}-#{rand(1000)}" - list_label = FactoryGirl.create(:label, title: label_name, project: issue.project) - FactoryGirl.create(:list, board: FactoryGirl.create(:board, project: issue.project), label: list_label) + list_label = FactoryBot.create(:label, title: label_name, project: issue.project) + FactoryBot.create(:list, board: FactoryBot.create(:board, project: issue.project), label: list_label) issue.update(labels: [list_label]) end diff --git a/db/migrate/20160610194713_remove_deprecated_issues_tracker_columns_from_projects.rb b/db/migrate/20160610194713_remove_deprecated_issues_tracker_columns_from_projects.rb index 477b2106dea..21b367711c3 100644 --- a/db/migrate/20160610194713_remove_deprecated_issues_tracker_columns_from_projects.rb +++ b/db/migrate/20160610194713_remove_deprecated_issues_tracker_columns_from_projects.rb @@ -1,3 +1,4 @@ +# rubocop:disable Migration/RemoveColumn class RemoveDeprecatedIssuesTrackerColumnsFromProjects < ActiveRecord::Migration def change remove_column :projects, :issues_tracker, :string, default: 'gitlab', null: false diff --git a/db/migrate/20160610301627_remove_notification_level_from_users.rb b/db/migrate/20160610301627_remove_notification_level_from_users.rb index 8afb14df2cf..356e53b4b23 100644 --- a/db/migrate/20160610301627_remove_notification_level_from_users.rb +++ b/db/migrate/20160610301627_remove_notification_level_from_users.rb @@ -1,3 +1,4 @@ +# rubocop:disable Migration/RemoveColumn class RemoveNotificationLevelFromUsers < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers diff --git a/db/migrate/20160705055809_remove_developers_can_push_from_protected_branches.rb b/db/migrate/20160705055809_remove_developers_can_push_from_protected_branches.rb index 52a9819c628..058bd539e65 100644 --- a/db/migrate/20160705055809_remove_developers_can_push_from_protected_branches.rb +++ b/db/migrate/20160705055809_remove_developers_can_push_from_protected_branches.rb @@ -1,3 +1,4 @@ +# rubocop:disable Migration/RemoveColumn # See http://doc.gitlab.com/ce/development/migration_style_guide.html # for more information on how to write migrations for GitLab. diff --git a/db/migrate/20160705055813_remove_developers_can_merge_from_protected_branches.rb b/db/migrate/20160705055813_remove_developers_can_merge_from_protected_branches.rb index 4a7bde7f9f3..d0e5da4d28b 100644 --- a/db/migrate/20160705055813_remove_developers_can_merge_from_protected_branches.rb +++ b/db/migrate/20160705055813_remove_developers_can_merge_from_protected_branches.rb @@ -1,3 +1,4 @@ +# rubocop:disable Migration/RemoveColumn # See http://doc.gitlab.com/ce/development/migration_style_guide.html # for more information on how to write migrations for GitLab. diff --git a/db/migrate/20160729173930_remove_project_id_from_spam_logs.rb b/db/migrate/20160729173930_remove_project_id_from_spam_logs.rb index e28ab31d629..baf254c3bcc 100644 --- a/db/migrate/20160729173930_remove_project_id_from_spam_logs.rb +++ b/db/migrate/20160729173930_remove_project_id_from_spam_logs.rb @@ -1,3 +1,4 @@ +# rubocop:disable Migration/RemoveColumn # See http://doc.gitlab.com/ce/development/migration_style_guide.html # for more information on how to write migrations for GitLab. diff --git a/db/migrate/20160831223750_remove_features_enabled_from_projects.rb b/db/migrate/20160831223750_remove_features_enabled_from_projects.rb index aec709aaf59..9eafd8b9477 100644 --- a/db/migrate/20160831223750_remove_features_enabled_from_projects.rb +++ b/db/migrate/20160831223750_remove_features_enabled_from_projects.rb @@ -1,3 +1,4 @@ +# rubocop:disable Migration/RemoveColumn # See http://doc.gitlab.com/ce/development/migration_style_guide.html # for more information on how to write migrations for GitLab. diff --git a/db/migrate/20160913162434_remove_projects_pushes_since_gc.rb b/db/migrate/20160913162434_remove_projects_pushes_since_gc.rb index df7d922b816..f32167037e0 100644 --- a/db/migrate/20160913162434_remove_projects_pushes_since_gc.rb +++ b/db/migrate/20160913162434_remove_projects_pushes_since_gc.rb @@ -1,3 +1,4 @@ +# rubocop:disable Migration/RemoveColumn # See http://doc.gitlab.com/ce/development/migration_style_guide.html # for more information on how to write migrations for GitLab. diff --git a/db/migrate/20161018024550_remove_priority_from_labels.rb b/db/migrate/20161018024550_remove_priority_from_labels.rb index b7416cca664..bc25a43526c 100644 --- a/db/migrate/20161018024550_remove_priority_from_labels.rb +++ b/db/migrate/20161018024550_remove_priority_from_labels.rb @@ -1,3 +1,4 @@ +# rubocop:disable Migration/RemoveColumn class RemovePriorityFromLabels < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers diff --git a/db/migrate/20161201160452_migrate_project_statistics.rb b/db/migrate/20161201160452_migrate_project_statistics.rb index 82fbdf02444..a547409aaa5 100644 --- a/db/migrate/20161201160452_migrate_project_statistics.rb +++ b/db/migrate/20161201160452_migrate_project_statistics.rb @@ -1,3 +1,4 @@ +# rubocop:disable Migration/RemoveColumn class MigrateProjectStatistics < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers diff --git a/db/migrate/20170222143500_remove_old_project_id_columns.rb b/db/migrate/20170222143500_remove_old_project_id_columns.rb index 268144a2552..9bed38a3444 100644 --- a/db/migrate/20170222143500_remove_old_project_id_columns.rb +++ b/db/migrate/20170222143500_remove_old_project_id_columns.rb @@ -1,3 +1,4 @@ +# rubocop:disable Migration/RemoveColumn # rubocop:disable RemoveIndex class RemoveOldProjectIdColumns < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers diff --git a/db/migrate/20170301205639_remove_unused_ci_tables_and_columns.rb b/db/migrate/20170301205639_remove_unused_ci_tables_and_columns.rb index 1a77d5934a3..0535c2ddaf2 100644 --- a/db/migrate/20170301205639_remove_unused_ci_tables_and_columns.rb +++ b/db/migrate/20170301205639_remove_unused_ci_tables_and_columns.rb @@ -1,3 +1,4 @@ +# rubocop:disable Migration/RemoveColumn # rubocop:disable Migration/Datetime class RemoveUnusedCiTablesAndColumns < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers diff --git a/db/migrate/20170315174634_revert_add_notified_of_own_activity_to_users.rb b/db/migrate/20170315174634_revert_add_notified_of_own_activity_to_users.rb index 807dfcb385d..9b9098d115d 100644 --- a/db/migrate/20170315174634_revert_add_notified_of_own_activity_to_users.rb +++ b/db/migrate/20170315174634_revert_add_notified_of_own_activity_to_users.rb @@ -1,3 +1,4 @@ +# rubocop:disable Migration/RemoveColumn # rubocop:disable Migration/UpdateLargeTable class RevertAddNotifiedOfOwnActivityToUsers < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers diff --git a/db/migrate/20171122131600_add_new_project_guidelines_to_appearances.rb b/db/migrate/20171122131600_add_new_project_guidelines_to_appearances.rb index f141c442d97..328cc65a549 100644 --- a/db/migrate/20171122131600_add_new_project_guidelines_to_appearances.rb +++ b/db/migrate/20171122131600_add_new_project_guidelines_to_appearances.rb @@ -4,6 +4,12 @@ class AddNewProjectGuidelinesToAppearances < ActiveRecord::Migration DOWNTIME = false def change + # Clears the current Appearance cache otherwise it breaks since + # new_project_guidelines_html would be missing. See + # https://gitlab.com/gitlab-org/gitlab-ce/issues/41041 + # We're not using Appearance#flush_redis_cache on purpose here. + Rails.cache.delete('current_appearance') + change_table :appearances do |t| t.text :new_project_guidelines t.text :new_project_guidelines_html diff --git a/db/migrate/20171123094802_add_circuitbreaker_check_interval_to_application_settings.rb b/db/migrate/20171123094802_add_circuitbreaker_check_interval_to_application_settings.rb new file mode 100644 index 00000000000..213d46018fc --- /dev/null +++ b/db/migrate/20171123094802_add_circuitbreaker_check_interval_to_application_settings.rb @@ -0,0 +1,20 @@ +class AddCircuitbreakerCheckIntervalToApplicationSettings < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + + disable_ddl_transaction! + + def up + add_column_with_default :application_settings, + :circuitbreaker_check_interval, + :integer, + default: 1 + end + + def down + remove_column :application_settings, + :circuitbreaker_check_interval + end +end diff --git a/db/migrate/20171204204233_add_permanent_to_redirect_route.rb b/db/migrate/20171204204233_add_permanent_to_redirect_route.rb new file mode 100644 index 00000000000..f3ae471201e --- /dev/null +++ b/db/migrate/20171204204233_add_permanent_to_redirect_route.rb @@ -0,0 +1,18 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddPermanentToRedirectRoute < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + disable_ddl_transaction! + + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + + def up + add_column(:redirect_routes, :permanent, :boolean) + end + + def down + remove_column(:redirect_routes, :permanent) + end +end diff --git a/db/migrate/20171206221519_add_permanent_index_to_redirect_route.rb b/db/migrate/20171206221519_add_permanent_index_to_redirect_route.rb new file mode 100644 index 00000000000..33ce7e1aa68 --- /dev/null +++ b/db/migrate/20171206221519_add_permanent_index_to_redirect_route.rb @@ -0,0 +1,19 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddPermanentIndexToRedirectRoute < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + + disable_ddl_transaction! + + def up + add_concurrent_index(:redirect_routes, :permanent) + end + + def down + remove_concurrent_index(:redirect_routes, :permanent) if index_exists?(:redirect_routes, :permanent) + end +end diff --git a/db/post_migrate/20170523073948_remove_assignee_id_from_issue.rb b/db/post_migrate/20170523073948_remove_assignee_id_from_issue.rb new file mode 100644 index 00000000000..006d17b4d62 --- /dev/null +++ b/db/post_migrate/20170523073948_remove_assignee_id_from_issue.rb @@ -0,0 +1,48 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class RemoveAssigneeIdFromIssue < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + + # When a migration requires downtime you **must** uncomment the following + # constant and define a short and easy to understand explanation as to why the + # migration requires downtime. + # DOWNTIME_REASON = '' + + # When using the methods "add_concurrent_index", "remove_concurrent_index" or + # "add_column_with_default" you must disable the use of transactions + # as these methods can not run in an existing transaction. + # When using "add_concurrent_index" or "remove_concurrent_index" methods make sure + # that either of them is the _only_ method called in the migration, + # any other changes should go in a separate migration. + # This ensures that upon failure _only_ the index creation or removing fails + # and can be retried or reverted easily. + # + # To disable transactions uncomment the following line and remove these + # comments: + disable_ddl_transaction! + + class Issue < ActiveRecord::Base + self.table_name = 'issues' + + include ::EachBatch + end + + def up + remove_column :issues, :assignee_id + end + + def down + add_column :issues, :assignee_id, :integer + add_concurrent_index :issues, :assignee_id + + update_value = Arel.sql('(SELECT user_id FROM issue_assignees WHERE issue_assignees.issue_id = issues.id LIMIT 1)') + + # This is only used in the down step, so we can ignore the RuboCop warning + # about large tables, as this is very unlikely to be run on GitLab.com + update_column_in_batches(:issues, :assignee_id, update_value) # rubocop:disable Migration/UpdateLargeTable + end +end diff --git a/db/migrate/20171106154015_remove_issues_branch_name.rb b/db/post_migrate/20171106154015_remove_issues_branch_name.rb index 3d08225c96d..162b6bafab4 100644 --- a/db/migrate/20171106154015_remove_issues_branch_name.rb +++ b/db/post_migrate/20171106154015_remove_issues_branch_name.rb @@ -1,3 +1,4 @@ +# rubocop:disable Migration/RemoveColumn # See http://doc.gitlab.com/ce/development/migration_style_guide.html # for more information on how to write migrations for GitLab. diff --git a/db/post_migrate/20171123101020_update_circuitbreaker_defaults.rb b/db/post_migrate/20171123101020_update_circuitbreaker_defaults.rb new file mode 100644 index 00000000000..8e1c9e6d6bb --- /dev/null +++ b/db/post_migrate/20171123101020_update_circuitbreaker_defaults.rb @@ -0,0 +1,34 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class UpdateCircuitbreakerDefaults < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + class ApplicationSetting < ActiveRecord::Base; end + + def up + change_column_default :application_settings, + :circuitbreaker_failure_count_threshold, + 3 + change_column_default :application_settings, + :circuitbreaker_storage_timeout, + 15 + + ApplicationSetting.update_all(circuitbreaker_failure_count_threshold: 3, + circuitbreaker_storage_timeout: 15) + end + + def down + change_column_default :application_settings, + :circuitbreaker_failure_count_threshold, + 160 + change_column_default :application_settings, + :circuitbreaker_storage_timeout, + 30 + + ApplicationSetting.update_all(circuitbreaker_failure_count_threshold: 160, + circuitbreaker_storage_timeout: 30) + end +end diff --git a/db/post_migrate/20171123101046_remove_old_circuitbreaker_config.rb b/db/post_migrate/20171123101046_remove_old_circuitbreaker_config.rb new file mode 100644 index 00000000000..e646d4d3224 --- /dev/null +++ b/db/post_migrate/20171123101046_remove_old_circuitbreaker_config.rb @@ -0,0 +1,26 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class RemoveOldCircuitbreakerConfig < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def up + remove_column :application_settings, + :circuitbreaker_backoff_threshold + remove_column :application_settings, + :circuitbreaker_failure_wait_time + end + + def down + add_column :application_settings, + :circuitbreaker_backoff_threshold, + :integer, + default: 80 + add_column :application_settings, + :circuitbreaker_failure_wait_time, + :integer, + default: 30 + end +end diff --git a/db/post_migrate/20171213160445_migrate_github_importer_advance_stage_sidekiq_queue.rb b/db/post_migrate/20171213160445_migrate_github_importer_advance_stage_sidekiq_queue.rb new file mode 100644 index 00000000000..149c28f1946 --- /dev/null +++ b/db/post_migrate/20171213160445_migrate_github_importer_advance_stage_sidekiq_queue.rb @@ -0,0 +1,16 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class MigrateGithubImporterAdvanceStageSidekiqQueue < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def up + sidekiq_queue_migrate 'github_importer_advance_stage', to: 'github_import_advance_stage' + end + + def down + sidekiq_queue_migrate 'github_import_advance_stage', to: 'github_importer_advance_stage' + end +end diff --git a/db/schema.rb b/db/schema.rb index 6ea3ab54742..2048c50f892 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20171205190711) do +ActiveRecord::Schema.define(version: 20171213160445) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -135,12 +135,10 @@ ActiveRecord::Schema.define(version: 20171205190711) do t.boolean "hashed_storage_enabled", default: false, null: false t.boolean "project_export_enabled", default: true, null: false t.boolean "auto_devops_enabled", default: false, null: false - t.integer "circuitbreaker_failure_count_threshold", default: 160 - t.integer "circuitbreaker_failure_wait_time", default: 30 + t.integer "circuitbreaker_failure_count_threshold", default: 3 t.integer "circuitbreaker_failure_reset_time", default: 1800 - t.integer "circuitbreaker_storage_timeout", default: 30 + t.integer "circuitbreaker_storage_timeout", default: 15 t.integer "circuitbreaker_access_retries", default: 3 - t.integer "circuitbreaker_backoff_threshold", default: 80 t.boolean "throttle_unauthenticated_enabled", default: false, null: false t.integer "throttle_unauthenticated_requests_per_period", default: 3600, null: false t.integer "throttle_unauthenticated_period_in_seconds", default: 3600, null: false @@ -150,6 +148,7 @@ ActiveRecord::Schema.define(version: 20171205190711) do t.boolean "throttle_authenticated_web_enabled", default: false, null: false t.integer "throttle_authenticated_web_requests_per_period", default: 7200, null: false t.integer "throttle_authenticated_web_period_in_seconds", default: 3600, null: false + t.integer "circuitbreaker_check_interval", default: 1, null: false t.boolean "password_authentication_enabled_for_web" t.boolean "password_authentication_enabled_for_git", default: true t.integer "gitaly_timeout_default", default: 55, null: false @@ -842,7 +841,6 @@ ActiveRecord::Schema.define(version: 20171205190711) do create_table "issues", force: :cascade do |t| t.string "title" - t.integer "assignee_id" t.integer "author_id" t.integer "project_id" t.datetime "created_at" @@ -868,7 +866,6 @@ ActiveRecord::Schema.define(version: 20171205190711) do t.datetime_with_timezone "closed_at" end - add_index "issues", ["assignee_id"], name: "index_issues_on_assignee_id", using: :btree add_index "issues", ["author_id"], name: "index_issues_on_author_id", using: :btree add_index "issues", ["confidential"], name: "index_issues_on_confidential", using: :btree add_index "issues", ["deleted_at"], name: "index_issues_on_deleted_at", using: :btree @@ -1527,10 +1524,12 @@ ActiveRecord::Schema.define(version: 20171205190711) do t.string "path", null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.boolean "permanent" end add_index "redirect_routes", ["path"], name: "index_redirect_routes_on_path", unique: true, using: :btree add_index "redirect_routes", ["path"], name: "index_redirect_routes_on_path_text_pattern_ops", using: :btree, opclasses: {"path"=>"varchar_pattern_ops"} + add_index "redirect_routes", ["permanent"], name: "index_redirect_routes_on_permanent", using: :btree add_index "redirect_routes", ["source_type", "source_id"], name: "index_redirect_routes_on_source_type_and_source_id", using: :btree create_table "releases", force: :cascade do |t| diff --git a/doc/administration/auth/README.md b/doc/administration/auth/README.md index ee9b9a9466a..373d4239f71 100644 --- a/doc/administration/auth/README.md +++ b/doc/administration/auth/README.md @@ -14,3 +14,4 @@ providers. - [CAS](../../integration/cas.md) Configure GitLab to sign in using CAS - [SAML](../../integration/saml.md) Configure GitLab as a SAML 2.0 Service Provider - [Okta](okta.md) Configure GitLab to sign in using Okta +- [Authentiq](authentiq.md): Enable the Authentiq OmniAuth provider for passwordless authentication diff --git a/doc/administration/high_availability/README.md b/doc/administration/high_availability/README.md index a88e67bfeb5..ea8077f0623 100644 --- a/doc/administration/high_availability/README.md +++ b/doc/administration/high_availability/README.md @@ -37,6 +37,7 @@ Follow the steps below to configure an active/active setup: 1. [Configure the database](database.md) 1. [Configure Redis](redis.md) + 1. [Configure Redis for GitLab source installations](redis_source.md) 1. [Configure NFS](nfs.md) 1. [Configure the GitLab application servers](gitlab.md) 1. [Configure the load balancers](load_balancer.md) diff --git a/doc/administration/index.md b/doc/administration/index.md index c8d28d8485a..0b199eecefd 100644 --- a/doc/administration/index.md +++ b/doc/administration/index.md @@ -16,26 +16,32 @@ Learn how to install, configure, update, and maintain your GitLab instance. - [Install](../install/README.md): Requirements, directory structures, and installation methods. - [High Availability](high_availability/README.md): Configure multiple servers for scaling or high availability. + - [High Availability on AWS](../university/high-availability/aws/README.md): Set up GitLab HA on Amazon AWS. ### Configuring GitLab - [Adjust your instance's timezone](../workflow/timezone.md): Customize the default time zone of GitLab. -- [Header logo](../customization/branded_page_and_email_header.md): Change the logo on all pages and email headers. -- [Welcome message](../customization/welcome_message.md): Add a custom welcome message to the sign-in page. - [System hooks](../system_hooks/system_hooks.md): Notifications when users, projects and keys are changed. - [Security](../security/README.md): Learn what you can do to further secure your GitLab instance. - [Usage statistics, version check, and usage ping](../user/admin_area/settings/usage_statistics.md): Enable or disable information about your instance to be sent to GitLab, Inc. - [Polling](polling.md): Configure how often the GitLab UI polls for updates. - [GitLab Pages configuration](pages/index.md): Enable and configure GitLab Pages. -- [GitLab Pages configuration for installations from the source](pages/source.md): Enable and configure GitLab Pages on +- [GitLab Pages configuration for GitLab source installations](pages/source.md): Enable and configure GitLab Pages on [source installations](../install/installation.md#installation-from-source). - [Environment variables](environment_variables.md): Supported environment variables that can be used to override their defaults values in order to configure GitLab. +#### Customizing GitLab's appearance + +- [Header logo](../customization/branded_page_and_email_header.md): Change the logo on all pages and email headers. +- [Branded login page](../customization/branded_login_page.md): Customize the login page with your own logo, title, and description. +- [Welcome message](../customization/welcome_message.md): Add a custom welcome message to the sign-in page. +- ["New Project" page](../customization/new_project_page.md): Customize the text to be displayed on the page that opens whenever your users create a new project. + ### Maintaining GitLab - [Raketasks](../raketasks/README.md): Perform various tasks for maintenance, backups, automatic webhooks setup, etc. - [Backup and restore](../raketasks/backup_restore.md): Backup and restore your GitLab instance. -- [Operations](operations.md): Keeping GitLab up and running (clean up Redis sessions, moving repositories, Sidekiq Job throttling, Sidekiq MemoryKiller, Unicorn). +- [Operations](operations/index.md): Keeping GitLab up and running (clean up Redis sessions, moving repositories, Sidekiq Job throttling, Sidekiq MemoryKiller, Unicorn). - [Restart GitLab](restart_gitlab.md): Learn how to restart GitLab and its components. #### Updating GitLab @@ -74,6 +80,7 @@ server with IMAP authentication on Ubuntu, to be used with Reply by email. - [Issue closing pattern](issue_closing_pattern.md): Customize how to close an issue from commit messages. - [Gitaly](gitaly/index.md): Configuring Gitaly, GitLab's Git repository storage service. - [Default labels](../user/admin_area/labels.html): Create labels that will be automatically added to every new project. +- [Restrict the use of public or internal projects](../public_access/public_access.md#restricting-the-use-of-public-or-internal-projects): Restrict the use of visibility levels for users when they create a project or a snippet. ### Repository settings @@ -99,20 +106,22 @@ server with IMAP authentication on Ubuntu, to be used with Reply by email. ## Monitoring GitLab -- [Monitoring uptime](../user/admin_area/monitoring/health_check.md): Check the server status using the health check endpoint. - - [IP whitelist](monitoring/ip_whitelist.md): Monitor endpoints that provide health check information when probed. -- [Monitoring GitHub imports](monitoring/github_imports.md): GitLab's GitHub Importer displays Prometheus metrics to monitor the health and progress of the importer. +- [Monitoring GitLab](monitoring/index.md): + - [Monitoring uptime](../user/admin_area/monitoring/health_check.md): Check the server status using the health check endpoint. + - [IP whitelist](monitoring/ip_whitelist.md): Monitor endpoints that provide health check information when probed. + - [Monitoring GitHub imports](monitoring/github_imports.md): GitLab's GitHub Importer displays Prometheus metrics to monitor the health and progress of the importer. - [Conversational Development (ConvDev) Index](../user/admin_area/monitoring/convdev.md): Provides an overview of your entire instance's feature usage. ### Performance Monitoring -- [GitLab Performance Monitoring](monitoring/performance/gitlab_configuration.md): Enable GitLab Performance Monitoring. -- [GitLab performance monitoring with InfluxDB](monitoring/performance/introduction.md): Configure GitLab and InfluxDB for measuring performance metrics. - - [InfluxDB Schema](monitoring/performance/influxdb_schema.md): Measurements stored in InfluxDB. -- [GitLab performance monitoring with Prometheus](monitoring/prometheus/index.md): Configure GitLab and Prometheus for measuring performance metrics. -- [GitLab performance monitoring with Grafana](monitoring/prometheus/index.md): Configure GitLab to visualize time series metrics through graphs and dashboards. -- [Request Profiling](monitoring/performance/request_profiling.md): Get a detailed profile on slow requests. -- [Performance Bar](monitoring/performance/performance_bar.md): Get performance information for the current page. +- [GitLab Performance Monitoring](monitoring/performance/index.md): + - [Enable Performance Monitoring](monitoring/performance/gitlab_configuration.md): Enable GitLab Performance Monitoring. + - [GitLab performance monitoring with InfluxDB](monitoring/performance/influxdb_configuration.md): Configure GitLab and InfluxDB for measuring performance metrics. + - [InfluxDB Schema](monitoring/performance/influxdb_schema.md): Measurements stored in InfluxDB. + - [GitLab performance monitoring with Prometheus](monitoring/prometheus/index.md): Configure GitLab and Prometheus for measuring performance metrics. + - [GitLab performance monitoring with Grafana](monitoring/performance/grafana_configuration.md): Configure GitLab to visualize time series metrics through graphs and dashboards. + - [Request Profiling](monitoring/performance/request_profiling.md): Get a detailed profile on slow requests. + - [Performance Bar](monitoring/performance/performance_bar.md): Get performance information for the current page. ## Troubleshooting diff --git a/doc/administration/monitoring/index.md b/doc/administration/monitoring/index.md new file mode 100644 index 00000000000..d6333ee62b4 --- /dev/null +++ b/doc/administration/monitoring/index.md @@ -0,0 +1,9 @@ +# Monitoring GitLab + +Explore our features to monitor your GitLab instance: + +- [Performance monitoring](performance/index.md): GitLab Performance Monitoring makes it possible to measure a wide variety of statistics of your instance. +- [Prometheus](prometheus/index.md): Prometheus is a powerful time-series monitoring service, providing a flexible platform for monitoring GitLab and other software products. +- [GitHub imports](github_imports.md): Monitor the health and progress of GitLab's GitHub importer with various Prometheus metrics. +- [Monitoring uptime](../user/admin_area/monitoring/health_check.md): Check the server status using the health check endpoint. + - [IP whitelists](ip_whitelist.md): Configure GitLab for monitoring endpoints that provide health check information when probed. diff --git a/doc/administration/monitoring/performance/index.md b/doc/administration/monitoring/performance/index.md new file mode 100644 index 00000000000..f5f0363ed38 --- /dev/null +++ b/doc/administration/monitoring/performance/index.md @@ -0,0 +1,72 @@ +# GitLab Performance Monitoring + +GitLab comes with its own application performance measuring system as of GitLab +8.4, simply called "GitLab Performance Monitoring". GitLab Performance Monitoring is available in both the +Community and Enterprise editions. + +Apart from this introduction, you are advised to read through the following +documents in order to understand and properly configure GitLab Performance Monitoring: + +- [GitLab Configuration](gitlab_configuration.md) +- [InfluxDB Install/Configuration](influxdb_configuration.md) +- [InfluxDB Schema](influxdb_schema.md) +- [Grafana Install/Configuration](grafana_configuration.md) +- [Performance bar](performance_bar.md) +- [Request profiling](request_profiling.md) + +>**Note:** +Omnibus GitLab 8.16 includes Prometheus as an additional tool to collect +metrics. It will eventually replace InfluxDB when their metrics collection is +on par. Read more in the [Prometheus documentation](../prometheus/index.md). + +## Introduction to GitLab Performance Monitoring + +GitLab Performance Monitoring makes it possible to measure a wide variety of statistics +including (but not limited to): + +- The time it took to complete a transaction (a web request or Sidekiq job). +- The time spent in running SQL queries and rendering HAML views. +- The time spent executing (instrumented) Ruby methods. +- Ruby object allocations, and retained objects in particular. +- System statistics such as the process' memory usage and open file descriptors. +- Ruby garbage collection statistics. + +Metrics data is written to [InfluxDB][influxdb] over [UDP][influxdb-udp]. Stored +data can be visualized using [Grafana][grafana] or any other application that +supports reading data from InfluxDB. Alternatively data can be queried using the +InfluxDB CLI. + +## Metric Types + +Two types of metrics are collected: + +1. Transaction specific metrics. +1. Sampled metrics, collected at a certain interval in a separate thread. + +### Transaction Metrics + +Transaction metrics are metrics that can be associated with a single +transaction. This includes statistics such as the transaction duration, timings +of any executed SQL queries, time spent rendering HAML views, etc. These metrics +are collected for every Rack request and Sidekiq job processed. + +### Sampled Metrics + +Sampled metrics are metrics that can't be associated with a single transaction. +Examples include garbage collection statistics and retained Ruby objects. These +metrics are collected at a regular interval. This interval is made up out of two +parts: + +1. A user defined interval. +1. A randomly generated offset added on top of the interval, the same offset + can't be used twice in a row. + +The actual interval can be anywhere between a half of the defined interval and a +half above the interval. For example, for a user defined interval of 15 seconds +the actual interval can be anywhere between 7.5 and 22.5. The interval is +re-generated for every sampling run instead of being generated once and re-used +for the duration of the process' lifetime. + +[influxdb]: https://influxdata.com/time-series-platform/influxdb/ +[influxdb-udp]: https://docs.influxdata.com/influxdb/v0.9/write_protocols/udp/ +[grafana]: http://grafana.org/ diff --git a/doc/administration/monitoring/performance/introduction.md b/doc/administration/monitoring/performance/introduction.md index 17c2b4b70d3..37a5388d2fc 100644 --- a/doc/administration/monitoring/performance/introduction.md +++ b/doc/administration/monitoring/performance/introduction.md @@ -1,70 +1 @@ -# GitLab Performance Monitoring - -GitLab comes with its own application performance measuring system as of GitLab -8.4, simply called "GitLab Performance Monitoring". GitLab Performance Monitoring is available in both the -Community and Enterprise editions. - -Apart from this introduction, you are advised to read through the following -documents in order to understand and properly configure GitLab Performance Monitoring: - -- [GitLab Configuration](gitlab_configuration.md) -- [InfluxDB Install/Configuration](influxdb_configuration.md) -- [InfluxDB Schema](influxdb_schema.md) -- [Grafana Install/Configuration](grafana_configuration.md) - ->**Note:** -Omnibus GitLab 8.16 includes Prometheus as an additional tool to collect -metrics. It will eventually replace InfluxDB when their metrics collection is -on par. Read more in the [Prometheus documentation](../prometheus/index.md). - -## Introduction to GitLab Performance Monitoring - -GitLab Performance Monitoring makes it possible to measure a wide variety of statistics -including (but not limited to): - -- The time it took to complete a transaction (a web request or Sidekiq job). -- The time spent in running SQL queries and rendering HAML views. -- The time spent executing (instrumented) Ruby methods. -- Ruby object allocations, and retained objects in particular. -- System statistics such as the process' memory usage and open file descriptors. -- Ruby garbage collection statistics. - -Metrics data is written to [InfluxDB][influxdb] over [UDP][influxdb-udp]. Stored -data can be visualized using [Grafana][grafana] or any other application that -supports reading data from InfluxDB. Alternatively data can be queried using the -InfluxDB CLI. - -## Metric Types - -Two types of metrics are collected: - -1. Transaction specific metrics. -1. Sampled metrics, collected at a certain interval in a separate thread. - -### Transaction Metrics - -Transaction metrics are metrics that can be associated with a single -transaction. This includes statistics such as the transaction duration, timings -of any executed SQL queries, time spent rendering HAML views, etc. These metrics -are collected for every Rack request and Sidekiq job processed. - -### Sampled Metrics - -Sampled metrics are metrics that can't be associated with a single transaction. -Examples include garbage collection statistics and retained Ruby objects. These -metrics are collected at a regular interval. This interval is made up out of two -parts: - -1. A user defined interval. -1. A randomly generated offset added on top of the interval, the same offset - can't be used twice in a row. - -The actual interval can be anywhere between a half of the defined interval and a -half above the interval. For example, for a user defined interval of 15 seconds -the actual interval can be anywhere between 7.5 and 22.5. The interval is -re-generated for every sampling run instead of being generated once and re-used -for the duration of the process' lifetime. - -[influxdb]: https://influxdata.com/time-series-platform/influxdb/ -[influxdb-udp]: https://docs.influxdata.com/influxdb/v0.9/write_protocols/udp/ -[grafana]: http://grafana.org/ +This document was moved to [another location](index.md). diff --git a/doc/administration/monitoring/prometheus/gitlab_metrics.md b/doc/administration/monitoring/prometheus/gitlab_metrics.md index 11d5e077a36..f495990d9a4 100644 --- a/doc/administration/monitoring/prometheus/gitlab_metrics.md +++ b/doc/administration/monitoring/prometheus/gitlab_metrics.md @@ -45,8 +45,9 @@ In this experimental phase, only a few metrics are available: | redis_ping_success | Gauge | 9.4 | Whether or not the last redis ping succeeded | | redis_ping_latency_seconds | Gauge | 9.4 | Round trip time of the redis ping | | user_session_logins_total | Counter | 9.4 | Counter of how many users have logged in | -| filesystem_circuitbreaker_latency_seconds | Histogram | 9.5 | Latency of the stat check the circuitbreaker uses to probe a shard | +| filesystem_circuitbreaker_latency_seconds | Gauge | 9.5 | Time spent validating if a storage is accessible | | filesystem_circuitbreaker | Gauge | 9.5 | Wether or not the circuit for a certain shard is broken or not | +| circuitbreaker_storage_check_duration_seconds | Histogram | 10.3 | Time a single storage probe took | ## Metrics shared directory diff --git a/doc/administration/operations.md b/doc/administration/operations.md index 0daceb98d99..4797d2a3206 100644 --- a/doc/administration/operations.md +++ b/doc/administration/operations.md @@ -1,7 +1 @@ -# GitLab operations - -- [Sidekiq MemoryKiller](operations/sidekiq_memory_killer.md) -- [Sidekiq Job throttling](operations/sidekiq_job_throttling.md) -- [Cleaning up Redis sessions](operations/cleaning_up_redis_sessions.md) -- [Understanding Unicorn and unicorn-worker-killer](operations/unicorn.md) -- [Moving repositories to a new location](operations/moving_repositories.md) +This document was moved to [another location](operations/index.md). diff --git a/doc/administration/operations/index.md b/doc/administration/operations/index.md new file mode 100644 index 00000000000..320d71a9527 --- /dev/null +++ b/doc/administration/operations/index.md @@ -0,0 +1,16 @@ +# Performing Operations in GitLab + +Keep your GitLab instance up and running smoothly. + +- [Clean up Redis sessions](cleaning_up_redis_sessions.md): Prior to GitLab 7.3, +user sessions did not automatically expire from Redis. If +you have been running a large GitLab server (thousands of users) since before +GitLab 7.3 we recommend cleaning up stale sessions to compact the Redis +database after you upgrade to GitLab 7.3. +- [Moving repositories](moving_repositories.md): Moving all repositories managed +by GitLab to another file system or another server. +- [Sidekiq job throttling](sidekiq_job_throttling.md): Throttle Sidekiq queues +that to prioritize important jobs. +- [Sidekiq MemoryKiller](sidekiq_memory_killer.md): Configure Sidekiq MemoryKiller +to restart Sidekiq. +- [Unicorn](unicorn.md): Understand Unicorn and unicorn-worker-killer.
\ No newline at end of file diff --git a/doc/administration/raketasks/maintenance.md b/doc/administration/raketasks/maintenance.md index 136192191f9..ecf92c379fd 100644 --- a/doc/administration/raketasks/maintenance.md +++ b/doc/administration/raketasks/maintenance.md @@ -221,3 +221,22 @@ sudo gitlab-rake gitlab:shell:create_hooks cd /home/git/gitlab sudo -u git -H bundle exec rake gitlab:shell:create_hooks RAILS_ENV=production ``` + +## Check TCP connectivity to a remote site + +Sometimes you need to know if your GitLab installation can connect to a TCP +service on another machine - perhaps a PostgreSQL or HTTPS server. A rake task +is included to help you with this: + +**Omnibus Installation** + +``` +sudo gitlab-rake gitlab:tcp_check[example.com,80] +``` + +**Source Installation** + +``` +cd /home/git/gitlab +sudo -u git -H bundle exec rake gitlab:tcp_check[example.com,80] RAILS_ENV=production +``` diff --git a/doc/administration/reply_by_email.md b/doc/administration/reply_by_email.md index 1304476e678..3a2cced37bf 100644 --- a/doc/administration/reply_by_email.md +++ b/doc/administration/reply_by_email.md @@ -89,9 +89,11 @@ email address in order to sign up. If you also host a public-facing GitLab instance at `hooli.com` and set your incoming email domain to `hooli.com`, an attacker could abuse the "Create new -issue by email" feature by using a project's unique address as the email when -signing up for Slack, which would send a confirmation email, which would create -a new issue on the project owned by the attacker, allowing them to click the +issue by email" or +"[Create new merge request by email](../user/project/merge_requests/index.md#create-new-merge-requests-by-email)" +features by using a project's unique address as the email when signing up for +Slack, which would send a confirmation email, which would create a new issue or +merge request on the project owned by the attacker, allowing them to click the confirmation link and validate their account on your company's private Slack instance. diff --git a/doc/api/groups.md b/doc/api/groups.md index c1b5737c247..de730cdd869 100644 --- a/doc/api/groups.md +++ b/doc/api/groups.md @@ -365,13 +365,15 @@ POST /groups Parameters: -- `name` (required) - The name of the group -- `path` (required) - The path of the group -- `description` (optional) - The group's description -- `visibility` (optional) - The group's visibility. Can be `private`, `internal`, or `public`. -- `lfs_enabled` (optional) - Enable/disable Large File Storage (LFS) for the projects in this group -- `request_access_enabled` (optional) - Allow users to request member access. -- `parent_id` (optional) - The parent group id for creating nested group. +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `name` | string | yes | The name of the group | +| `path` | string | yes | The path of the group | +| `description` | string | no | The group's description | +| `visibility` | string | no | The group's visibility. Can be `private`, `internal`, or `public`. | +| `lfs_enabled` | boolean | no | Enable/disable Large File Storage (LFS) for the projects in this group | +| `request_access_enabled` | boolean | no | Allow users to request member access. | +| `parent_id` | integer | no | The parent group id for creating nested group. | ## Transfer project to group @@ -383,8 +385,10 @@ POST /groups/:id/projects/:project_id Parameters: -- `id` (required) - The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user -- `project_id` (required) - The ID or path of a project +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user | +| `project_id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) | ## Update group diff --git a/doc/api/issues.md b/doc/api/issues.md index ec8ff3cd3f3..d2fefbe68aa 100644 --- a/doc/api/issues.md +++ b/doc/api/issues.md @@ -514,9 +514,9 @@ PUT /projects/:id/issues/:issue_iid | `title` | string | no | The title of an issue | | `description` | string | no | The description of an issue | | `confidential` | boolean | no | Updates an issue to be confidential | -| `assignee_ids` | Array[integer] | no | The ID of the users to assign the issue to | -| `milestone_id` | integer | no | The ID of a milestone to assign the issue to | -| `labels` | string | no | Comma-separated label names for an issue | +| `assignee_ids` | Array[integer] | no | The ID of the user(s) to assign the issue to. Set to `0` or provide an empty value to unassign all assignees. | +| `milestone_id` | integer | no | The ID of a milestone to assign the issue to. Set to `0` or provide an empty value to unassign a milestone.| +| `labels` | string | no | Comma-separated label names for an issue. Set to an empty string to unassign all labels. | | `state_event` | string | no | The state event of an issue. Set `close` to close the issue and `reopen` to reopen it | | `updated_at` | string | no | Date time string, ISO 8601 formatted, e.g. `2016-03-11T03:45:40Z` (requires admin or project owner rights) | | `due_date` | string | no | Date time string in the format YEAR-MONTH-DAY, e.g. `2016-03-11` | diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md index b2e4b6d0955..880b0ed2c65 100644 --- a/doc/api/merge_requests.md +++ b/doc/api/merge_requests.md @@ -524,15 +524,15 @@ PUT /projects/:id/merge_requests/:merge_request_iid | Attribute | Type | Required | Description | | --------- | ---- | -------- | ----------- | -| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | +| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | | `merge_request_iid` | integer | yes | The ID of a merge request | | `target_branch` | string | no | The target branch | | `title` | string | no | Title of MR | -| `assignee_id` | integer | no | Assignee user ID | +| `assignee_id` | integer | no | The ID of the user to assign the merge request to. Set to `0` or provide an empty value to unassign all assignees. | +| `milestone_id` | integer | no | The ID of a milestone to assign the merge request to. Set to `0` or provide an empty value to unassign a milestone.| +| `labels` | string | no | Comma-separated label names for an merge request. Set to an empty string to unassign all labels. | | `description` | string | no | Description of MR | | `state_event` | string | no | New state (close/reopen) | -| `labels` | string | no | Labels for MR as a comma-separated list | -| `milestone_id` | integer | no | The ID of a milestone | | `remove_source_branch` | boolean | no | Flag indicating if a merge request should remove the source branch when merging | | `discussion_locked` | boolean | no | Flag indicating if the merge request's discussion is locked. If the discussion is locked only project members can add, edit or resolve comments. | diff --git a/doc/api/repositories.md b/doc/api/repositories.md index 594babc74be..03b32577872 100644 --- a/doc/api/repositories.md +++ b/doc/api/repositories.md @@ -182,6 +182,8 @@ GET /projects/:id/repository/contributors Parameters: - `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user +- `order_by` (optional) - Return contributors ordered by `name`, `email`, or `commits` fields. If not given contributors are ordered by commit date. +- `sort` (optional) - Return contributors sorted in `asc` or `desc` order. Default is `asc` Response: diff --git a/doc/api/settings.md b/doc/api/settings.md index 22fb2baa8ec..0e4758cda2d 100644 --- a/doc/api/settings.md +++ b/doc/api/settings.md @@ -70,10 +70,9 @@ PUT /application/settings | `akismet_api_key` | string | no | API key for akismet spam protection | | `akismet_enabled` | boolean | no | Enable or disable akismet spam protection | | `circuitbreaker_access_retries | integer | no | The number of attempts GitLab will make to access a storage. | -| `circuitbreaker_backoff_threshold | integer | no | The number of failures after which GitLab will start temporarily disabling access to a storage shard on a host. | +| `circuitbreaker_check_interval` | integer | no | Number of seconds in between storage checks. | | `circuitbreaker_failure_count_threshold` | integer | no | The number of failures of after which GitLab will completely prevent access to the storage. | | `circuitbreaker_failure_reset_time` | integer | no | Time in seconds GitLab will keep storage failure information. When no failures occur during this time, the failure information is reset. | -| `circuitbreaker_failure_wait_time` | integer | no | Time in seconds GitLab will block access to a failing storage to allow it to recover. | | `circuitbreaker_storage_timeout` | integer | no | Seconds to wait for a storage access attempt | | `clientside_sentry_dsn` | string | no | Required if `clientside_sentry_dsn` is enabled | | `clientside_sentry_enabled` | boolean | no | Enable Sentry error reporting for the client side | diff --git a/doc/api/tags.md b/doc/api/tags.md index bebe6536b6e..fa25dc76452 100644 --- a/doc/api/tags.md +++ b/doc/api/tags.md @@ -12,7 +12,11 @@ GET /projects/:id/repository/tags Parameters: -- `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer/string| yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user| +| `order_by` | string | no | Return tags ordered by `name` or `updated` fields. Default is `updated` | +| `sort` | string | no | Return tags sorted in `asc` or `desc` order. Default is `desc` | ```json [ diff --git a/doc/articles/laravel_with_gitlab_and_envoy/index.md b/doc/articles/laravel_with_gitlab_and_envoy/index.md index e0d8fb8d081..b20bd8c247a 100644 --- a/doc/articles/laravel_with_gitlab_and_envoy/index.md +++ b/doc/articles/laravel_with_gitlab_and_envoy/index.md @@ -502,8 +502,8 @@ stages: unit_test: stage: test script: - - composer install - cp .env.example .env + - composer install - php artisan key:generate - php artisan migrate - vendor/bin/phpunit diff --git a/doc/ci/docker/using_docker_images.md b/doc/ci/docker/using_docker_images.md index ecb8f15c851..fb5bfe26bb0 100644 --- a/doc/ci/docker/using_docker_images.md +++ b/doc/ci/docker/using_docker_images.md @@ -319,45 +319,62 @@ As you can see, the syntax of `command` is similar to [Dockerfile's `CMD`][cmd]. > Introduced in GitLab and GitLab Runner 9.4. Read more about the [extended configuration options](#extended-docker-configuration-options). +Before showing the available entrypoint override methods, let's describe shortly +how the Runner starts and uses a Docker image for the containers used in the +CI jobs: + +1. The Runner starts a Docker container using the defined entrypoint (default + from `Dockerfile` that may be overridden in `.gitlab-ci.yml`) +1. The Runner attaches itself to a running container. +1. The Runner prepares a script (the combination of + [`before_script`](../yaml/README.md#before_script), + [`script`](../yaml/README.md#script), + and [`after_script`](../yaml/README.md#after_script)). +1. The Runner sends the script to the container's shell STDIN and receives the + output. + +To override the entrypoint of a Docker image, the recommended solution is to +define an empty `entrypoint` in `.gitlab-ci.yml`, so the Runner doesn't start +a useless shell layer. However, that will not work for all Docker versions, and +you should check which one your Runner is using. Specifically: + +- If Docker 17.06 or later is used, the `entrypoint` can be set to an empty value. +- If Docker 17.03 or previous versions are used, the `entrypoint` can be set to + `/bin/sh -c`, `/bin/bash -c` or an equivalent shell available in the image. + +The syntax of `image:entrypoint` is similar to [Dockerfile's `ENTRYPOINT`][entrypoint]. + +---- + Let's assume you have a `super/sql:experimental` image with some SQL database inside it and you would like to use it as a base image for your job because you want to execute some tests with this database binary. Let's also assume that this image is configured with `/usr/bin/super-sql run` as an entrypoint. That -means, that when starting the container without additional options, it will run +means that when starting the container without additional options, it will run the database's process, while Runner expects that the image will have no -entrypoint or at least will start with a shell as its entrypoint. - -Before the new extended Docker configuration options, you would need to create -your own image based on the `super/sql:experimental` image, set the entrypoint -to a shell and then use it in job's configuration, like: +entrypoint or that the entrypoint is prepared to start a shell command. -```Dockerfile -# my-super-sql:experimental image's Dockerfile +With the extended Docker configuration options, instead of creating your +own image based on `super/sql:experimental`, setting the `ENTRYPOINT` +to a shell, and then using the new image in your CI job, you can now simply +define an `entrypoint` in `.gitlab-ci.yml`. -FROM super/sql:experimental -ENTRYPOINT ["/bin/sh"] -``` +**For Docker 17.06+:** ```yaml -# .gitlab-ci.yml - -image: my-super-sql:experimental +image: + name: super/sql:experimental + entrypoint: [""] ``` -After the new extended Docker configuration options, you can now simply -set an `entrypoint` in `.gitlab-ci.yml`, like: +**For Docker =< 17.03:** ```yaml -# .gitlab-ci.yml - image: name: super/sql:experimental - entrypoint: ["/bin/sh"] + entrypoint: ["/bin/sh", "-c"] ``` -As you can see the syntax of `entrypoint` is similar to -[Dockerfile's `ENTRYPOINT`][entrypoint]. - ## Define image and services in `config.toml` Look for the `[runners.docker]` section: diff --git a/doc/ci/ssh_keys/README.md b/doc/ci/ssh_keys/README.md index e5a2bbd1773..df0e1521150 100644 --- a/doc/ci/ssh_keys/README.md +++ b/doc/ci/ssh_keys/README.md @@ -1,84 +1,106 @@ -# Using SSH keys +--- +last_updated: 2017-12-13 +--- + +# Using SSH keys with GitLab CI/CD GitLab currently doesn't have built-in support for managing SSH keys in a build -environment. +environment (where the GitLab Runner runs). The SSH keys can be useful when: 1. You want to checkout internal submodules -2. You want to download private packages using your package manager (eg. bundler) -3. You want to deploy your application to eg. Heroku or your own server -4. You want to execute SSH commands from the build server to the remote server -5. You want to rsync files from your build server to the remote server +1. You want to download private packages using your package manager (e.g., Bundler) +1. You want to deploy your application to your own server, or, for example, Heroku +1. You want to execute SSH commands from the build environment to a remote server +1. You want to rsync files from the build environment to a remote server If anything of the above rings a bell, then you most likely need an SSH key. -## Inject keys in your build server - The most widely supported method is to inject an SSH key into your build -environment by extending your `.gitlab-ci.yml`. - -This is the universal solution which works with any type of executor -(docker, shell, etc.). - -### How it works - -1. Create a new SSH key pair with [ssh-keygen][] -2. Add the private key as a **Secret Variable** to the project -3. Run the [ssh-agent][] during job to load the private key. +environment by extending your `.gitlab-ci.yml`, and it's a solution which works +with any type of [executor](https://docs.gitlab.com/runner/executors/) +(Docker, shell, etc.). + +## How it works + +1. Create a new SSH key pair locally with [ssh-keygen](http://linux.die.net/man/1/ssh-keygen) +1. Add the private key as a [secret variable](../variables/README.md) to + your project +1. Run the [ssh-agent](http://linux.die.net/man/1/ssh-agent) during job to load + the private key. +1. Copy the public key to the servers you want to have access to (usually in + `~/.ssh/authorized_keys`) or add it as a [deploy key](../../ssh/README.md#deploy-keys) + if you are accessing a private GitLab repository. + +NOTE: **Note:** +The private key will not be displayed in the job trace, unless you enable +[debug tracing](../variables/README.md#debug-tracing). You might also want to +check the [visibility of your pipelines](../../user/project/pipelines/settings.md#visibility-of-pipelines). ## SSH keys when using the Docker executor -You will first need to create an SSH key pair. For more information, follow the -instructions to [generate an SSH key](../../ssh/README.md). Do not add a -passphrase to the SSH key, or the `before_script` will prompt for it. - -Then, create a new **Secret Variable** in your project settings on GitLab -following **Settings > CI/CD** and look for the "Secret Variables" section. -As **Key** add the name `SSH_PRIVATE_KEY` and in the **Value** field paste the -content of your _private_ key that you created earlier. - -It is also good practice to check the server's own public key to make sure you -are not being targeted by a man-in-the-middle attack. To do this, add another -variable named `SSH_SERVER_HOSTKEYS`. To find out the hostkeys of your server, run -the `ssh-keyscan YOUR_SERVER` command from a trusted network (ideally, from the -server itself), and paste its output into the `SSH_SERVER_HOSTKEYS` variable. If -you need to connect to multiple servers, concatenate all the server public keys -that you collected into the **Value** of the variable. There must be one key per -line. - -Next you need to modify your `.gitlab-ci.yml` with a `before_script` action. -Add it to the top: - -``` -before_script: - # Install ssh-agent if not already installed, it is required by Docker. - # (change apt-get to yum if you use a CentOS-based image) - - 'which ssh-agent || ( apt-get update -y && apt-get install openssh-client -y )' - - # Run ssh-agent (inside the build environment) - - eval $(ssh-agent -s) - - # Add the SSH key stored in SSH_PRIVATE_KEY variable to the agent store - - ssh-add <(echo "$SSH_PRIVATE_KEY") - - # For Docker builds disable host key checking. Be aware that by adding that - # you are suspectible to man-in-the-middle attacks. - # WARNING: Use this only with the Docker executor, if you use it with shell - # you will overwrite your user's SSH config. - - mkdir -p ~/.ssh - - '[[ -f /.dockerenv ]] && echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config' - # In order to properly check the server's host key, assuming you created the - # SSH_SERVER_HOSTKEYS variable previously, uncomment the following two lines - # instead. - # - mkdir -p ~/.ssh - # - '[[ -f /.dockerenv ]] && echo "$SSH_SERVER_HOSTKEYS" > ~/.ssh/known_hosts' -``` - -As a final step, add the _public_ key from the one you created earlier to the -services that you want to have an access to from within the build environment. -If you are accessing a private GitLab repository you need to add it as a -[deploy key](../../ssh/README.md#deploy-keys). +When your CI/CD jobs run inside Docker containers (meaning the environment is +contained) and you want to deploy your code in a private server, you need a way +to access it. This is where an SSH key pair comes in handy. + +1. You will first need to create an SSH key pair. For more information, follow + the instructions to [generate an SSH key](../../ssh/README.md#generating-a-new-ssh-key-pair). + **Do not** add a passphrase to the SSH key, or the `before_script` will\ + prompt for it. + +1. Create a new [secret variable](../variables/README.md#secret-variables). + As **Key** enter the name `SSH_PRIVATE_KEY` and in the **Value** field paste + the content of your _private_ key that you created earlier. + +1. Modify your `.gitlab-ci.yml` with a `before_script` action. In the following + example, a Debian based image is assumed. Edit to your needs: + + ```yaml + before_script: + ## + ## Install ssh-agent if not already installed, it is required by Docker. + ## (change apt-get to yum if you use an RPM-based image) + ## + - 'which ssh-agent || ( apt-get update -y && apt-get install openssh-client -y )' + + ## + ## Run ssh-agent (inside the build environment) + ## + - eval $(ssh-agent -s) + + ## + ## Add the SSH key stored in SSH_PRIVATE_KEY variable to the agent store + ## We're using tr to fix line endings which makes ed25519 keys work + ## without extra base64 encoding. + ## https://gitlab.com/gitlab-examples/ssh-private-key/issues/1#note_48526556 + ## + - echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add - > /dev/null + + ## + ## Create the SSH directory and give it the right permissions + ## + - mkdir -p ~/.ssh + - chmod 700 ~/.ssh + + ## + ## Optionally, if you will be using any Git commands, set the user name and + ## and email. + ## + #- git config --global user.email "user@example.com" + #- git config --global user.name "User name" + ``` + + NOTE: **Note:** + The [`before_script`](../yaml/README.md#before-script) can be set globally + or per-job. + +1. Make sure the private server's [SSH host keys are verified](#verifying-the-ssh-host-keys). + +1. As a final step, add the _public_ key from the one you created in the first + step to the services that you want to have an access to from within the build + environment. If you are accessing a private GitLab repository you need to add + it as a [deploy key](../../ssh/README.md#deploy-keys). That's it! You can now have access to private servers or repositories in your build environment. @@ -91,24 +113,93 @@ SSH key. You can generate the SSH key from the machine that GitLab Runner is installed on, and use that key for all projects that are run on this machine. -First, you need to login to the server that runs your jobs. +1. First, you need to login to the server that runs your jobs. + +1. Then from the terminal login as the `gitlab-runner` user: -Then from the terminal login as the `gitlab-runner` user and generate the SSH -key pair as described in the [SSH keys documentation](../../ssh/README.md). + ``` + sudo su - gitlab-runner + ``` -As a final step, add the _public_ key from the one you created earlier to the -services that you want to have an access to from within the build environment. -If you are accessing a private GitLab repository you need to add it as a -[deploy key](../../ssh/README.md#deploy-keys). +1. Generate the SSH key pair as described in the instructions to + [generate an SSH key](../../ssh/README.md#generating-a-new-ssh-key-pair). + **Do not** add a passphrase to the SSH key, or the `before_script` will + prompt for it. + +1. As a final step, add the _public_ key from the one you created earlier to the + services that you want to have an access to from within the build environment. + If you are accessing a private GitLab repository you need to add it as a + [deploy key](../../ssh/README.md#deploy-keys). Once done, try to login to the remote server in order to accept the fingerprint: ```bash -ssh <address-of-my-server> +ssh example.com +``` + +For accessing repositories on GitLab.com, you would use `git@gitlab.com`. + +## Verifying the SSH host keys + +It is a good practice to check the private server's own public key to make sure +you are not being targeted by a man-in-the-middle attack. In case anything +suspicious happens, you will notice it since the job would fail (the SSH +connection would fail if the public keys would not match). + +To find out the host keys of your server, run the `ssh-keyscan` command from a +trusted network (ideally, from the private server itself): + +```sh +## Use the domain name +ssh-keyscan example.com + +## Or use an IP +ssh-keyscan 1.2.3.4 ``` -For accessing repositories on GitLab.com, the `<address-of-my-server>` would be -`git@gitlab.com`. +Create a new [secret variable](../variables/README.md#secret-variables) with +`SSH_KNOWN_HOSTS` as "Key", and as a "Value" add the output of `ssh-keyscan`. + +NOTE: **Note:** +If you need to connect to multiple servers, all the server host keys +need to be collected in the **Value** of the variable, one key per line. + +TIP: **Tip:** +By using a secret variable instead of `ssh-keyscan` directly inside +`.gitlab-ci.yml`, it has the benefit that you don't have to change `.gitlab-ci.yml` +if the host domain name changes for some reason. Also, the values are predefined +by you, meaning that if the host keys suddenly change, the CI/CD job will fail, +and you'll know there's something wrong with the server or the network. + +Now that the `SSH_KNOWN_HOSTS` variable is created, in addition to the +[content of `.gitlab-ci.yml`](#ssh-keys-when-using-the-docker-executor) +above, here's what more you need to add: + + ```yaml +before_script: + ## + ## Assuming you created the SSH_KNOWN_HOSTS variable, uncomment the + ## following two lines. + ## + - echo "$SSH_KNOWN_HOSTS" > ~/.ssh/known_hosts' + - chmod 644 ~/.ssh/known_hosts + + ## + ## Alternatively, use ssh-keyscan to scan the keys of your private server. + ## Replace example.com with your private server's domain name. Repeat that + ## command if you have more than one server to connect to. + ## + #- ssh-keyscan example.com >> ~/.ssh/known_hosts + #- chmod 644 ~/.ssh/known_hosts + + ## + ## You can optionally disable host key checking. Be aware that by adding that + ## you are suspectible to man-in-the-middle attacks. + ## WARNING: Use this only with the Docker executor, if you use it with shell + ## you will overwrite your user's SSH config. + ## + #- '[[ -f /.dockerenv ]] && echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config' +``` ## Example project @@ -119,6 +210,4 @@ that runs on [GitLab.com](https://gitlab.com) using our publicly available Want to hack on it? Simply fork it, commit and push your changes. Within a few moments the changes will be picked by a public runner and the job will begin. -[ssh-keygen]: http://linux.die.net/man/1/ssh-keygen -[ssh-agent]: http://linux.die.net/man/1/ssh-agent [ssh-example-repo]: https://gitlab.com/gitlab-examples/ssh-private-key/ diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md index a9e6bda9916..b9d4a2098ed 100644 --- a/doc/ci/variables/README.md +++ b/doc/ci/variables/README.md @@ -213,14 +213,15 @@ An example project service that defines deployment variables is ## Debug tracing > Introduced in GitLab Runner 1.7. -> -> **WARNING:** Enabling debug tracing can have severe security implications. The - output **will** contain the content of all your secret variables and any other - secrets! The output **will** be uploaded to the GitLab server and made visible - in job traces! + +CAUTION: **Warning:** +Enabling debug tracing can have severe security implications. The +output **will** contain the content of all your secret variables and any other +secrets! The output **will** be uploaded to the GitLab server and made visible +in job traces! By default, GitLab Runner hides most of the details of what it is doing when -processing a job. This behaviour keeps job traces short, and prevents secrets +processing a job. This behavior keeps job traces short, and prevents secrets from being leaked into the trace unless your script writes them to the screen. If a job isn't working as expected, this can make the problem difficult to diff --git a/doc/customization/issue_closing.md b/doc/customization/issue_closing.md index 31164ccd465..d14ba6ad522 100644 --- a/doc/customization/issue_closing.md +++ b/doc/customization/issue_closing.md @@ -1,3 +1,7 @@ +--- +comments: false +--- + This document was split into: - [administration/issue_closing_pattern.md](../administration/issue_closing_pattern.md). diff --git a/doc/customization/welcome_message.md b/doc/customization/welcome_message.md index a0cb234bea0..0aef0bf5abb 100644 --- a/doc/customization/welcome_message.md +++ b/doc/customization/welcome_message.md @@ -8,5 +8,5 @@ It is possible to add a markdown-formatted welcome message to your GitLab sign-in page. Users of GitLab Enterprise Edition should use the [branded login page feature](branded_login_page.md) instead. -The welcome message (extra_sign_in_text) can now be set/changed in the Admin UI. +The welcome message (extra_sign_in_text) can now be set/changed in the Admin UI. Admin area > Settings diff --git a/doc/development/README.md b/doc/development/README.md index 6892838be7f..b624aa37c70 100644 --- a/doc/development/README.md +++ b/doc/development/README.md @@ -16,7 +16,8 @@ comments: false - [GitLab core team & GitLab Inc. contribution process](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/PROCESS.md) - [Generate a changelog entry with `bin/changelog`](changelog.md) - [Code review guidelines](code_review.md) for reviewing code and having code reviewed. -- [Limit conflicts with EE when developing on CE](limit_ee_conflicts.md) +- [Automatic CE->EE merge](automatic_ce_ee_merge.md) +- [Guidelines for implementing Enterprise Edition features](ee_features.md) ## UX and frontend guides @@ -36,6 +37,7 @@ comments: false - [`Gemfile` guidelines](gemfile.md) - [Sidekiq debugging](sidekiq_debugging.md) - [Gotchas](gotchas.md) to avoid +- [Avoid modules with instance variables](module_with_instance_variables.md) if possible - [Issue and merge requests state models](object_state_models.md) - [How to dump production data to staging](db_dump.md) - [Working with the GitHub importer](github_importer.md) @@ -80,10 +82,9 @@ comments: false ## Documentation guides -- [Documentation styleguide](doc_styleguide.md): Use this styleguide if you are - contributing to the documentation. - [Writing documentation](writing_documentation.md) - - [Distinction between general documentation and technical articles](writing_documentation.md#distinction-between-general-documentation-and-technical-articles) +- [Documentation styleguide](doc_styleguide.md) +- [Markdown](../user/markdown.md) ## Internationalization (i18n) guides diff --git a/doc/development/automatic_ce_ee_merge.md b/doc/development/automatic_ce_ee_merge.md new file mode 100644 index 00000000000..4b9791c95bc --- /dev/null +++ b/doc/development/automatic_ce_ee_merge.md @@ -0,0 +1,93 @@ +# Automatic CE->EE merge + +GitLab Community Edition is merged automatically every 3 hours into the +Enterprise Edition (look for the [`CE Upstream` merge requests]). + +This merge is done automatically in a +[scheduled pipeline](https://gitlab.com/gitlab-org/release-tools/-/jobs/43201679). +If a merge is already in progress, the job [doesn't create a new one](https://gitlab.com/gitlab-org/release-tools/-/jobs/43157687). + +**If you are pinged in a `CE Upstream` merge request to resolve a conflict, +please resolve the conflict as soon as possible or ask someone else to do it!** + +>**Note:** +It's ok to resolve more conflicts than the one that you are asked to resolve. In +that case, it's a good habit to ask for a double-check on your resolution by +someone who is familiar with the code you touched. + +[`CE Upstream` merge requests]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests?label_name%5B%5D=CE+upstream + +## Always merge EE merge requests before their CE counterparts + +**In order to avoid conflicts in the CE->EE merge, you should always merge the +EE version of your CE merge request first, if present.** + +The rationale for this is that as CE->EE merges are done automatically every few +hours, it can happen that: + +1. A CE merge request that needs EE-specific changes is merged +1. The automatic CE->EE merge happens +1. Conflicts due to the CE merge request occur since its EE merge request isn't + merged yet +1. The automatic merge bot will ping someone to resolve the conflict **that are + already resolved in the EE merge request that isn't merged yet** + +That's a waste of time, and that's why you should merge EE merge request before +their CE counterpart. + +## Avoiding CE->EE merge conflicts beforehand + +To avoid the conflicts beforehand, check out the +[Guidelines for implementing Enterprise Edition features](ee_features.md). + +In any case, the CI `ee_compat_check` job will tell you if you need to open an +EE version of your CE merge request. + +### Conflicts detection in CE merge requests + +For each commit (except on `master`), the `ee_compat_check` CI job tries to +detect if the current branch's changes will conflict during the CE->EE merge. + +The job reports what files are conflicting and how to setup a merge request +against EE. + +#### How the job works + +1. Generates the diff between your branch and current CE `master` +1. Tries to apply it to current EE `master` +1. If it applies cleanly, the job succeeds, otherwise... +1. Detects a branch with the `ee-` prefix or `-ee` suffix in EE +1. If it exists, generate the diff between this branch and current EE `master` +1. Tries to apply it to current EE `master` +1. If it applies cleanly, the job succeeds + +In the case where the job fails, it means you should create a `ee-<ce_branch>` +or `<ce_branch>-ee` branch, push it to EE and open a merge request against EE +`master`. +At this point if you retry the failing job in your CE merge request, it should +now pass. + +Notes: + +- This task is not a silver-bullet, its current goal is to bring awareness to + developers that their work needs to be ported to EE. +- Community contributors shouldn't be required to submit merge requests against + EE, but reviewers should take actions by either creating such EE merge request + or asking a GitLab developer to do it **before the merge request is merged**. +- If you branch is too far behind `master`, the job will fail. In that case you + should rebase your branch upon latest `master`. +- Code reviews for merge requests often consist of multiple iterations of + feedback and fixes. There is no need to update your EE MR after each + iteration. Instead, create an EE MR as soon as you see the + `ee_compat_check` job failing. After you receive the final approval + from a Maintainer (but **before the CE MR is merged**) update the EE MR. + This helps to identify significant conflicts sooner, but also reduces the + number of times you have to resolve conflicts. +- Please remember to + [always have your EE merge request merged before the CE version](#always-merge-ee-merge-requests-before-their-ce-counterparts). +- You can use [`git rerere`](https://git-scm.com/blog/2010/03/08/rerere.html) + to avoid resolving the same conflicts multiple times. + +--- + +[Return to Development documentation](README.md) diff --git a/doc/development/ee_features.md b/doc/development/ee_features.md index 932a44f65e4..1af839a27e1 100644 --- a/doc/development/ee_features.md +++ b/doc/development/ee_features.md @@ -1,4 +1,4 @@ -# Guidelines for implementing Enterprise Edition feature +# Guidelines for implementing Enterprise Edition features - **Write the code and the tests.**: As with any code, EE features should have good test coverage to prevent regressions. @@ -380,3 +380,9 @@ to avoid conflicts during CE to EE merge. } } ``` + +## gitlab-svgs + +Conflicts in `app/assets/images/icons.json` or `app/assets/images/icons.svg` can +be resolved simply by regenerating those assets with +[`yarn run svg`](https://gitlab.com/gitlab-org/gitlab-svgs). diff --git a/doc/development/fe_guide/dropdowns.md b/doc/development/fe_guide/dropdowns.md index e1660ac5caa..6314f8f38d2 100644 --- a/doc/development/fe_guide/dropdowns.md +++ b/doc/development/fe_guide/dropdowns.md @@ -4,15 +4,15 @@ ## How to style a bootstrap dropdown 1. Use the HTML structure provided by the [docs][bootstrap-dropdowns] 1. Add a specific class to the top level `.dropdown` element - - + + ```Haml .dropdown.my-dropdown %button{ type: 'button', data: { toggle: 'dropdown' }, 'aria-haspopup': true, 'aria-expanded': false } %span.dropdown-toggle-text Toggle Dropdown = icon('chevron-down') - + %ul.dropdown-menu %li %a @@ -29,10 +29,4 @@ item! ``` -1. Include the mixin in CSS - - ```SCSS - @include new-style-dropdown('.my-dropdown '); - ``` - [bootstrap-dropdowns]: https://getbootstrap.com/docs/3.3/javascript/#dropdowns diff --git a/doc/development/fe_guide/style_guide_js.md b/doc/development/fe_guide/style_guide_js.md index 10f4c5a0902..1cd66f27492 100644 --- a/doc/development/fe_guide/style_guide_js.md +++ b/doc/development/fe_guide/style_guide_js.md @@ -86,34 +86,34 @@ followed by any global declarations, then a blank newline prior to any imports o #### Modules, Imports, and Exports 1. Use ES module syntax to import modules - ```javascript - // bad - const SomeClass = require('some_class'); + ```javascript + // bad + const SomeClass = require('some_class'); - // good - import SomeClass from 'some_class'; + // good + import SomeClass from 'some_class'; - // bad - module.exports = SomeClass; + // bad + module.exports = SomeClass; - // good - export default SomeClass; - ``` - - Import statements are following usual naming guidelines, for example object literals use camel case: - - ```javascript - // some_object file - export default { - key: 'value', - }; - - // bad - import ObjectLiteral from 'some_object'; + // good + export default SomeClass; + ``` + + Import statements are following usual naming guidelines, for example object literals use camel case: - // good - import objectLiteral from 'some_object'; - ``` + ```javascript + // some_object file + export default { + key: 'value', + }; + + // bad + import ObjectLiteral from 'some_object'; + + // good + import objectLiteral from 'some_object'; + ``` 1. Relative paths: when importing a module in the same directory, a child directory, or an immediate parent directory prefer relative paths. When @@ -334,33 +334,33 @@ A forEach will cause side effects, it will be mutating the array being iterated. #### Alignment 1. Follow these alignment styles for the template method: 1. With more than one attribute, all attributes should be on a new line: - ```javascript - // bad - <component v-if="bar" - param="baz" /> + ```javascript + // bad + <component v-if="bar" + param="baz" /> - <button class="btn">Click me</button> + <button class="btn">Click me</button> - // good - <component - v-if="bar" - param="baz" - /> + // good + <component + v-if="bar" + param="baz" + /> - <button class="btn"> - Click me - </button> - ``` + <button class="btn"> + Click me + </button> + ``` 1. The tag can be inline if there is only one attribute: - ```javascript - // good - <component bar="bar" /> + ```javascript + // good + <component bar="bar" /> - // good - <component - bar="bar" - /> - ``` + // good + <component + bar="bar" + /> + ``` #### Quotes 1. Always use double quotes `"` inside templates and single quotes `'` for all other JS. @@ -414,7 +414,6 @@ A forEach will cause side effects, it will be mutating the array being iterated. 1. Default key should be provided if the prop is not required. _Note:_ There are some scenarios where we need to check for the existence of the property. On those a default key should not be provided. - ```javascript // good props: { @@ -494,21 +493,20 @@ On those a default key should not be provided. #### Ordering 1. Tag order in `.vue` file - - ``` - <script> - // ... - </script> - - <template> - // ... - </template> - - // We don't use scoped styles but there are few instances of this - <style> - // ... - </style> - ``` + ``` + <script> + // ... + </script> + + <template> + // ... + </template> + + // We don't use scoped styles but there are few instances of this + <style> + // ... + </style> + ``` 1. Properties in a Vue Component: 1. `name` diff --git a/doc/development/gotchas.md b/doc/development/gotchas.md index c2ca8966a3f..5786287d00c 100644 --- a/doc/development/gotchas.md +++ b/doc/development/gotchas.md @@ -8,7 +8,7 @@ might encounter or should avoid during development of GitLab CE and EE. Consider the following factory: ```ruby -FactoryGirl.define do +FactoryBot.define do factory :label do sequence(:title) { |n| "label#{n}" } end @@ -53,7 +53,7 @@ When run, this spec doesn't do what we might expect: (compared using ==) ``` -That's because FactoryGirl sequences are not reseted for each example. +That's because FactoryBot sequences are not reseted for each example. Please remember that sequence-generated values exist only to avoid having to explicitly set attributes that have a uniqueness constraint when using a factory. diff --git a/doc/development/i18n/externalization.md b/doc/development/i18n/externalization.md index 4b65a0f4a35..f493ad4ae66 100644 --- a/doc/development/i18n/externalization.md +++ b/doc/development/i18n/externalization.md @@ -215,6 +215,9 @@ There is also and alternative method to [translate messages from validation erro sprintf(__('Hello %{username}'), { username: 'Joe' }) => 'Hello Joe' ``` +The placeholders should match the code style of the respective source file. +For example use `%{created_at}` in Ruby but `%{createdAt}` in JavaScript. + ### Plurals - In Ruby/HAML: @@ -259,6 +262,21 @@ Sometimes you need to add some context to the text that you want to translate s__('OpenedNDaysAgo|Opened') ``` +### Dates / times + +- In JavaScript: + +```js +import { createDateTimeFormat } from '.../locale'; + +const dateFormat = createDateTimeFormat({ year: 'numeric', month: 'long', day: 'numeric' }); +console.log(dateFormat.format(new Date('2063-04-05'))) // April 5, 2063 +``` + +This makes use of [`Intl.DateTimeFormat`]. + +[`Intl.DateTimeFormat`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DateTimeFormat + ## Adding a new language Let's suppose you want to add translations for a new language, let's say French. diff --git a/doc/development/i18n/index.md b/doc/development/i18n/index.md index 4cb2624c098..8aa0462d213 100644 --- a/doc/development/i18n/index.md +++ b/doc/development/i18n/index.md @@ -59,6 +59,7 @@ Requests to become a proof reader will be considered on the merits of previous t - French - German - Italian + - [Paolo Falomo](https://crowdin.com/profile/paolo.falomo) - Japanese - Korean - [Huang Tao](https://crowdin.com/profile/htve) diff --git a/doc/development/limit_ee_conflicts.md b/doc/development/limit_ee_conflicts.md deleted file mode 100644 index ba82babb38a..00000000000 --- a/doc/development/limit_ee_conflicts.md +++ /dev/null @@ -1,347 +0,0 @@ -# Limit conflicts with EE when developing on CE - -This guide contains best-practices for avoiding conflicts between CE and EE. - -## Daily CE Upstream merge - -GitLab Community Edition is merged daily into the Enterprise Edition (look for -the [`CE Upstream` merge requests]). The daily merge is currently done manually -by four individuals. - -**If a developer pings you in a `CE Upstream` merge request for help with -resolving conflicts, please help them because it means that you didn't do your -job to reduce the conflicts nor to ease their resolution in the first place!** - -To avoid the conflicts beforehand when working on CE, there are a few tools and -techniques that can help you: - -- know what are the usual types of conflicts and how to prevent them -- the CI `rake ee_compat_check` job tells you if you need to open an EE-version - of your CE merge request - -[`CE Upstream` merge requests]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests?label_name%5B%5D=CE+upstream - -## Check the status of the CI `rake ee_compat_check` job - -For each commit (except on `master`), the `rake ee_compat_check` CI job tries to -detect if the current branch's changes will conflict during the CE->EE merge. - -The job reports what files are conflicting and how to setup a merge request -against EE. Here is roughly how it works: - -1. Generates the diff between your branch and current CE `master` -1. Tries to apply it to current EE `master` -1. If it applies cleanly, the job succeeds, otherwise... -1. Detects a branch with the `-ee` suffix in EE -1. If it exists, generate the diff between this branch and current EE `master` -1. Tries to apply it to current EE `master` -1. If it applies cleanly, the job succeeds - -In the case where the job fails, it means you should create a `<ce_branch>-ee` -branch, push it to EE and open a merge request against EE `master`. At this -point if you retry the failing job in your CE merge request, it should now pass. - -Notes: - -- This task is not a silver-bullet, its current goal is to bring awareness to - developers that their work needs to be ported to EE. -- Community contributors shouldn't submit merge requests against EE, but - reviewers should take actions by either creating such EE merge request or - asking a GitLab developer to do it once the merge request is merged. -- If you branch is more than 500 commits behind `master`, the job will fail and - you should rebase your branch upon latest `master`. -- Code reviews for merge requests often consist of multiple iterations of - feedback and fixes. There is no need to update your EE MR after each - iteration. Instead, create an EE MR as soon as you see the - `rake ee_compat_check` job failing. After you receive the final acceptance - from a Maintainer (but before the CE MR is merged) update the EE MR. - This helps to identify significant conflicts sooner, but also reduces the - number of times you have to resolve conflicts. -- You can use [`git rerere`](https://git-scm.com/blog/2010/03/08/rerere.html) - to avoid resolving the same conflicts multiple times. - -## Possible type of conflicts - -### Controllers - -#### List or arrays are augmented in EE - -In controllers, the most common type of conflict is with `before_action` that -has a list of actions in CE but EE adds some actions to that list. - -The same problem often occurs for `params.require` / `params.permit` calls. - -##### Mitigations - -Separate CE and EE actions/keywords. For instance for `params.require` in -`ProjectsController`: - -```ruby -def project_params - params.require(:project).permit(project_params_ce) - # On EE, this is always: - # params.require(:project).permit(project_params_ce << project_params_ee) -end - -# Always returns an array of symbols, created however best fits the use case. -# It _should_ be sorted alphabetically. -def project_params_ce - %i[ - description - name - path - ] -end - -# (On EE) -def project_params_ee - %i[ - approvals_before_merge - approver_group_ids - approver_ids - ... - ] -end -``` - -#### Additional condition(s) in EE - -For instance for LDAP: - -```diff - def destroy - @key = current_user.keys.find(params[:id]) - - @key.destroy - + @key.destroy unless @key.is_a? LDAPKey - - respond_to do |format| -``` - -Or for Geo: - -```diff -def after_sign_out_path_for(resource) -- current_application_settings.after_sign_out_path.presence || new_user_session_path -+ if Gitlab::Geo.secondary? -+ Gitlab::Geo.primary_node.oauth_logout_url(@geo_logout_state) -+ else -+ current_application_settings.after_sign_out_path.presence || new_user_session_path -+ end -end -``` - -Or even for audit log: - -```diff -def approve_access_request -- Members::ApproveAccessRequestService.new(membershipable, current_user, params).execute -+ member = Members::ApproveAccessRequestService.new(membershipable, current_user, params).execute -+ -+ log_audit_event(member, action: :create) - - redirect_to polymorphic_url([membershipable, :members]) -end -``` - -### Views - -#### Additional view code in EE - -A block of code added in CE conflicts because there is already another block -at the same place in EE - -##### Mitigations - -Blocks of code that are EE-specific should be moved to partials as much as -possible to avoid conflicts with big chunks of HAML code that that are not fun -to resolve when you add the indentation to the equation. - -For instance this kind of thing: - -```haml -.form-group.detail-page-description - = form.label :description, 'Description', class: 'control-label' - .col-sm-10 - = render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do - = render 'projects/zen', f: form, attr: :description, - classes: 'note-textarea', - placeholder: "Write a comment or drag your files here...", - supports_quick_actions: !issuable.persisted? - = render 'projects/notes/hints', supports_quick_actions: !issuable.persisted? - .clearfix - .error-alert -- if issuable.is_a?(Issue) - .form-group - .col-sm-offset-2.col-sm-10 - .checkbox - = form.label :confidential do - = form.check_box :confidential - This issue is confidential and should only be visible to team members with at least Reporter access. -- if can?(current_user, :"admin_#{issuable.to_ability_name}", issuable.project) - - has_due_date = issuable.has_attribute?(:due_date) - %hr - .row - %div{ class: (has_due_date ? "col-lg-6" : "col-sm-12") } - .form-group.issue-assignee - = form.label :assignee_id, "Assignee", class: "control-label #{"col-lg-4" if has_due_date}" - .col-sm-10{ class: ("col-lg-8" if has_due_date) } - .issuable-form-select-holder - - if issuable.assignee_id - = form.hidden_field :assignee_id - = dropdown_tag(user_dropdown_label(issuable.assignee_id, "Assignee"), options: { toggle_class: "js-dropdown-keep-input js-user-search js-issuable-form-dropdown js-assignee-search", title: "Select assignee", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-assignee js-filter-submit", - placeholder: "Search assignee", data: { first_user: current_user.try(:username), null_user: true, current_user: true, project_id: project.try(:id), selected: issuable.assignee_id, field_name: "#{issuable.class.model_name.param_key}[assignee_id]", default_label: "Assignee"} }) - .form-group.issue-milestone - = form.label :milestone_id, "Milestone", class: "control-label #{"col-lg-4" if has_due_date}" - .col-sm-10{ class: ("col-lg-8" if has_due_date) } - .issuable-form-select-holder - = render "shared/issuable/milestone_dropdown", selected: issuable.milestone, name: "#{issuable.class.model_name.param_key}[milestone_id]", show_any: false, show_upcoming: false, extra_class: "js-issuable-form-dropdown js-dropdown-keep-input", dropdown_title: "Select milestone" - .form-group - - has_labels = @labels && @labels.any? - = form.label :label_ids, "Labels", class: "control-label #{"col-lg-4" if has_due_date}" - = form.hidden_field :label_ids, multiple: true, value: '' - .col-sm-10{ class: "#{"col-lg-8" if has_due_date} #{'issuable-form-padding-top' if !has_labels}" } - .issuable-form-select-holder - = render "shared/issuable/label_dropdown", classes: ["js-issuable-form-dropdown"], selected: issuable.labels, data_options: { field_name: "#{issuable.class.model_name.param_key}[label_ids][]", show_any: false }, dropdown_title: "Select label" - - if issuable.respond_to?(:weight) - - weight_options = Issue.weight_options - - weight_options.delete(Issue::WEIGHT_ALL) - - weight_options.delete(Issue::WEIGHT_ANY) - .form-group - = form.label :label_ids, class: "control-label #{"col-lg-4" if has_due_date}" do - Weight - .col-sm-10{ class: ("col-lg-8" if has_due_date) } - .issuable-form-select-holder - - if issuable.weight - = form.hidden_field :weight - = dropdown_tag(issuable.weight || "Weight", options: { title: "Select weight", toggle_class: 'js-weight-select js-issuable-form-weight', dropdown_class: "dropdown-menu-selectable dropdown-menu-weight", - placeholder: "Search weight", data: { field_name: "#{issuable.class.model_name.param_key}[weight]" , default_label: "Weight" } }) do - %ul - - weight_options.each do |weight| - %li - %a{href: "#", data: { id: weight, none: weight === Issue::WEIGHT_NONE }, class: ("is-active" if issuable.weight == weight)} - = weight - - if has_due_date - .col-lg-6 - .form-group - = form.label :due_date, "Due date", class: "control-label" - .col-sm-10 - .issuable-form-select-holder - = form.text_field :due_date, id: "issuable-due-date", class: "datepicker form-control", placeholder: "Select due date" -``` - -could be simplified by using partials: - -```haml -= render 'shared/issuable/form/description', issuable: issuable, form: form - -- if issuable.respond_to?(:confidential) - .form-group - .col-sm-offset-2.col-sm-10 - .checkbox - = form.label :confidential do - = form.check_box :confidential - This issue is confidential and should only be visible to team members with at least Reporter access. - -= render 'shared/issuable/form/metadata', issuable: issuable, form: form -``` - -and then the `app/views/shared/issuable/form/_metadata.html.haml` could be as follows: - -```haml -- issuable = local_assigns.fetch(:issuable) - -- return unless can?(current_user, :"admin_#{issuable.to_ability_name}", issuable.project) - -- has_due_date = issuable.has_attribute?(:due_date) -- has_labels = @labels && @labels.any? -- form = local_assigns.fetch(:form) - -%hr -.row - %div{ class: (has_due_date ? "col-lg-6" : "col-sm-12") } - .form-group.issue-assignee - = form.label :assignee_id, "Assignee", class: "control-label #{"col-lg-4" if has_due_date}" - .col-sm-10{ class: ("col-lg-8" if has_due_date) } - .issuable-form-select-holder - - if issuable.assignee_id - = form.hidden_field :assignee_id - = dropdown_tag(user_dropdown_label(issuable.assignee_id, "Assignee"), options: { toggle_class: "js-dropdown-keep-input js-user-search js-issuable-form-dropdown js-assignee-search", title: "Select assignee", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-assignee js-filter-submit", - placeholder: "Search assignee", data: { first_user: current_user.try(:username), null_user: true, current_user: true, project_id: issuable.project.try(:id), selected: issuable.assignee_id, field_name: "#{issuable.class.model_name.param_key}[assignee_id]", default_label: "Assignee"} }) - .form-group.issue-milestone - = form.label :milestone_id, "Milestone", class: "control-label #{"col-lg-4" if has_due_date}" - .col-sm-10{ class: ("col-lg-8" if has_due_date) } - .issuable-form-select-holder - = render "shared/issuable/milestone_dropdown", selected: issuable.milestone, name: "#{issuable.class.model_name.param_key}[milestone_id]", show_any: false, show_upcoming: false, extra_class: "js-issuable-form-dropdown js-dropdown-keep-input", dropdown_title: "Select milestone" - .form-group - - has_labels = @labels && @labels.any? - = form.label :label_ids, "Labels", class: "control-label #{"col-lg-4" if has_due_date}" - = form.hidden_field :label_ids, multiple: true, value: '' - .col-sm-10{ class: "#{"col-lg-8" if has_due_date} #{'issuable-form-padding-top' if !has_labels}" } - .issuable-form-select-holder - = render "shared/issuable/label_dropdown", classes: ["js-issuable-form-dropdown"], selected: issuable.labels, data_options: { field_name: "#{issuable.class.model_name.param_key}[label_ids][]", show_any: false }, dropdown_title: "Select label" - - = render "shared/issuable/form/weight", issuable: issuable, form: form - - - if has_due_date - .col-lg-6 - .form-group - = form.label :due_date, "Due date", class: "control-label" - .col-sm-10 - .issuable-form-select-holder - = form.text_field :due_date, id: "issuable-due-date", class: "datepicker form-control", placeholder: "Select due date" -``` - -and then the `app/views/shared/issuable/form/_weight.html.haml` could be as follows: - -```haml -- issuable = local_assigns.fetch(:issuable) - -- return unless issuable.respond_to?(:weight) - -- has_due_date = issuable.has_attribute?(:due_date) -- form = local_assigns.fetch(:form) - -.form-group - = form.label :label_ids, class: "control-label #{"col-lg-4" if has_due_date}" do - Weight - .col-sm-10{ class: ("col-lg-8" if has_due_date) } - .issuable-form-select-holder - - if issuable.weight - = form.hidden_field :weight - - = weight_dropdown_tag(issuable, toggle_class: 'js-issuable-form-weight') do - %ul - - Issue.weight_options.each do |weight| - %li - %a{ href: '#', data: { id: weight, none: weight === Issue::WEIGHT_NONE }, class: ("is-active" if issuable.weight == weight) } - = weight -``` - -Note: - -- The safeguards at the top allow to get rid of an unneccessary indentation level -- Here we only moved the 'Weight' code to a partial since this is the only - EE-specific code in that view, so it's the most likely to conflict, but you - are encouraged to use partials even for code that's in CE to logically split - big views into several smaller files. - -#### Indentation issue - -Sometimes a code block is indented more or less in EE because there's an -additional condition. - -##### Mitigations - -Blocks of code that are EE-specific should be moved to partials as much as -possible to avoid conflicts with big chunks of HAML code that that are not fun -to resolve when you add the indentation in the equation. - -### Assets - -#### gitlab-svgs - -Conflicts in `app/assets/images/icons.json` or `app/assets/images/icons.svg` can be resolved simply by regenerating those assets with [`yarn run svg`](https://gitlab.com/gitlab-org/gitlab-svgs). - ---- - -[Return to Development documentation](README.md) diff --git a/doc/development/module_with_instance_variables.md b/doc/development/module_with_instance_variables.md new file mode 100644 index 00000000000..48a1b7f847e --- /dev/null +++ b/doc/development/module_with_instance_variables.md @@ -0,0 +1,242 @@ +## Modules with instance variables could be considered harmful + +### Background + +Rails somehow encourages people using modules and instance variables +everywhere. For example, using instance variables in the controllers, +helpers, and views. They're also encouraging the use of +`ActiveSupport::Concern`, which further strengthens the idea of +saving everything in a giant, single object, and people could access +everything in that one giant object. + +### The problems + +Of course this is convenient to develop, because we just have everything +within reach. However this has a number of downsides when that chosen object +is growing, it would later become out of control for the same reason. + +There are just too many things in the same context, and we don't know if +those things are tightly coupled or not, depending on each others or not. +It's very hard to tell when the complexity grows to a point, and it makes +tracking the code also extremely hard. For example, a class could be using +3 different instance variables, and all of them could be initialized and +manipulated from 3 different modules. It's hard to track when those variables +start giving us troubles. We don't know which module would suddenly change +one of the variables. Everything could touch anything. + +### Similar concerns + +People are saying multiple inheritance is bad. Mixing multiple modules with +multiple instance variables scattering everywhere suffer from the same issue. +The same applies to `ActiveSupport::Concern`. See: +[Consider replacing concerns with dedicated classes & composition]( +https://gitlab.com/gitlab-org/gitlab-ce/issues/23786) + +There's also a similar idea: +[Use decorators and interface segregation to solve overgrowing models problem]( +https://gitlab.com/gitlab-org/gitlab-ce/issues/13484) + +Note that `included` doesn't solve the whole issue. They define the +dependencies, but they still allow each modules to talk implicitly via the +instance variables in the final giant object, and that's where the problem is. + +### Solutions + +We should split the giant object into multiple objects, and they communicate +with each other with the API, i.e. public methods. In short, composition over +inheritance. This way, each smaller objects would have their own respective +limited states, i.e. instance variables. If one instance variable goes wrong, +we would be very clear that it's from that single small object, because +no one else could be touching it. + +With clearly defined API, this would make things less coupled and much easier +to debug and track, and much more extensible for other objects to use, because +they communicate in a clear way, rather than implicit dependencies. + +### Acceptable use + +However, it's not always bad to use instance variables in a module, +as long as it's contained in the same module; that is, no other modules or +objects are touching them, then it would be an acceptable use. + +We especially allow the case where a single instance variable is used with +`||=` to setup the value. This would look like: + +``` ruby +module M + def f + @f ||= true + end +end +``` + +Unfortunately it's not easy to code more complex rules into the cop, so +we rely on people's best judgement. If we could find another good pattern +we could easily add to the cop, we should do it. + +### How to rewrite and avoid disabling this cop + +Even if we could just disable the cop, we should avoid doing so. Some code +could be easily rewritten in simple form. Consider this acceptable method: + +``` ruby +module Gitlab + module Emoji + def emoji_unicode_version(name) + @emoji_unicode_versions_by_name ||= + JSON.parse(File.read(Rails.root.join('fixtures', 'emojis', 'emoji-unicode-version-map.json'))) + @emoji_unicode_versions_by_name[name] + end + end +end +``` + +This method is totally fine because it's already self-contained. No other +methods should be using `@emoji_unicode_versions_by_name` and we're good. +However it's still offending the cop because it's not just `||=`, and the +cop is not smart enough to judge that this is fine. + +On the other hand, we could split this method into two: + +``` ruby +module Gitlab + module Emoji + def emoji_unicode_version(name) + emoji_unicode_versions_by_name[name] + end + + private + + def emoji_unicode_versions_by_name + @emoji_unicode_versions_by_name ||= + JSON.parse(File.read(Rails.root.join('fixtures', 'emojis', 'emoji-unicode-version-map.json'))) + end + end +end +``` + +Now the cop won't complain. Here's a bad example which we could rewrite: + +``` ruby +module SpamCheckService + def filter_spam_check_params + @request = params.delete(:request) + @api = params.delete(:api) + @recaptcha_verified = params.delete(:recaptcha_verified) + @spam_log_id = params.delete(:spam_log_id) + end + + def spam_check(spammable, user) + spam_service = SpamService.new(spammable, @request) + + spam_service.when_recaptcha_verified(@recaptcha_verified, @api) do + user.spam_logs.find_by(id: @spam_log_id)&.update!(recaptcha_verified: true) + end + end +end +``` + +There are several implicit dependencies here. First, `params` should be +defined before use. Second, `filter_spam_check_params` should be called +before `spam_check`. These are all implicit and the includer could be using +those instance variables without awareness. + +This should be rewritten like: + +``` ruby +class SpamCheckService + def initialize(request:, api:, recaptcha_verified:, spam_log_id:) + @request = request + @api = api + @recaptcha_verified = recaptcha_verified + @spam_log_id = spam_log_id + end + + def spam_check(spammable, user) + spam_service = SpamService.new(spammable, @request) + + spam_service.when_recaptcha_verified(@recaptcha_verified, @api) do + user.spam_logs.find_by(id: @spam_log_id)&.update!(recaptcha_verified: true) + end + end +end +``` + +And use it like: + +``` ruby +class UpdateSnippetService < BaseService + def execute + # ... + spam = SpamCheckService.new(params.slice!(:request, :api, :recaptcha_verified, :spam_log_id)) + + spam.check(snippet, current_user) + # ... + end +end +``` + +This way, all those instance variables are isolated in `SpamCheckService` +rather than whatever includes the module, and those modules which were also +included, making it much easier to track down any issues, +and reducing the chance of having name conflicts. + +### How to disable this cop + +Put the disabling comment right after your code in the same line: + +``` ruby +module M + def violating_method + @f + @g # rubocop:disable Gitlab/ModuleWithInstanceVariables + end +end +``` + +If there are multiple lines, you could also enable and disable for a section: + +``` ruby +module M + # rubocop:disable Gitlab/ModuleWithInstanceVariables + def violating_method + @f = 0 + @g = 1 + @h = 2 + end + # rubocop:enable Gitlab/ModuleWithInstanceVariables +end +``` + +Note that you need to enable it at some point, otherwise everything below +won't be checked. + +### Things we might need to ignore right now + +Because of the way Rails helpers and mailers work, we might not be able to +avoid the use of instance variables there. For those cases, we could ignore +them at the moment. At least we're not going to share those modules with +other random objects, so they're still somewhat isolated. + +### Instance variables in views + +They're bad because we can't easily tell who's using the instance variables +(from controller's point of view) and where we set them up (from partials' +point of view), making it extremely hard to track data dependency. + +We're trying to use something like this instead: + +``` haml += render 'projects/commits/commit', commit: commit, ref: ref, project: project +``` + +And in the partial: + +``` haml +- ref = local_assigns.fetch(:ref) +- commit = local_assigns.fetch(:commit) +- project = local_assigns.fetch(:project) +``` + +This way it's clearer where those values were coming from, and we gain the +benefit to have typo check over using instance variables. In the future, +we should also forbid the use of instance variables in partials. diff --git a/doc/development/performance.md b/doc/development/performance.md index 04419650b12..e7c5a6ca07a 100644 --- a/doc/development/performance.md +++ b/doc/development/performance.md @@ -37,7 +37,7 @@ graphs/dashboards. GitLab provides built-in tools to aid the process of improving performance: * [Sherlock](profiling.md#sherlock) -* [GitLab Performance Monitoring](../administration/monitoring/performance/introduction.md) +* [GitLab Performance Monitoring](../administration/monitoring/performance/index.md) * [Request Profiling](../administration/monitoring/performance/request_profiling.md) * [QueryRecoder](query_recorder.md) for preventing `N+1` regressions diff --git a/doc/development/sidekiq_style_guide.md b/doc/development/sidekiq_style_guide.md index 085fb8f902c..59ebf41e09f 100644 --- a/doc/development/sidekiq_style_guide.md +++ b/doc/development/sidekiq_style_guide.md @@ -9,25 +9,54 @@ All workers should include `ApplicationWorker` instead of `Sidekiq::Worker`, which adds some convenience methods and automatically sets the queue based on the worker's name. -## Default Queue +## Dedicated Queues -Use of the "default" queue is not allowed. Every worker should use a queue that -matches the worker's purpose the closest. For example, workers that are to be -executed periodically should use the "cronjob" queue. +All workers should use their own queue, which is automatically set based on the +worker class name. For a worker named `ProcessSomethingWorker`, the queue name +would be `process_something`. If you're not sure what queue a worker uses, +you can find it using `SomeWorker.queue`. There is almost never a reason to +manually override the queue name using `sidekiq_options queue: :some_queue`. -A list of all available queues can be found in `config/sidekiq_queues.yml`. +## Queue Namespaces -## Dedicated Queues +While different workers cannot share a queue, they can share a queue namespace. -Most workers should use their own queue, which is automatically set based on the -worker class name. For a worker named `ProcessSomethingWorker`, the queue name -would be `process_something`. If you're not sure what a worker's queue name is, -you can find it using `SomeWorker.queue`. +Defining a queue namespace for a worker makes it possible to start a Sidekiq +process that automatically handles jobs for all workers in that namespace, +without needing to explicitly list all their queue names. If, for example, all +workers that are managed by sidekiq-cron use the `cronjob` queue namespace, we +can spin up a Sidekiq process specifically for these kinds of scheduled jobs. +If a new worker using the `cronjob` namespace is added later on, the Sidekiq +process will automatically pick up jobs for that worker too (after having been +restarted), without the need to change any configuration. + +A queue namespace can be set using the `queue_namespace` DSL class method: + +```ruby +class SomeScheduledTaskWorker + include ApplicationWorker + + queue_namespace :cronjob + + # ... +end +``` + +Behind the scenes, this will set `SomeScheduledTaskWorker.queue` to +`cronjob:some_scheduled_task`. Commonly used namespaces will have their own +concern module that can easily be included into the worker class, and that may +set other Sidekiq options besides the queue namespace. `CronjobQueue`, for +example, sets the namespace, but also disables retries. + +`bundle exec sidekiq` is namespace-aware, and will automatically listen on all +queues in a namespace (technically: all queues prefixed with the namespace name) +when a namespace is provided instead of a simple queue name in the `--queue` +(`-q`) option, or in the `:queues:` section in `config/sidekiq_queues.yml`. -In some cases multiple workers do use the same queue. For example, the various -workers for updating CI pipelines all use the `pipeline` queue. Adding workers -to existing queues should be done with care, as adding more workers can lead to -slow jobs blocking work (even for different jobs) on the shared queue. +Note that adding a worker to an existing namespace should be done with care, as +the extra jobs will take resources away from jobs from workers that were already +there, if the resources available to the Sidekiq process handling the namespace +are not adjusted appropriately. ## Tests @@ -36,7 +65,7 @@ tests should be placed in `spec/workers`. ## Removing or renaming queues -Try to avoid renaming or removing queues in minor and patch releases. +Try to avoid renaming or removing workers and their queues in minor and patch releases. During online update instance can have pending jobs and removing the queue can lead to those jobs being stuck forever. If you can't write migration for those Sidekiq jobs, please consider doing rename or remove queue in major release only. diff --git a/doc/development/testing_guide/best_practices.md b/doc/development/testing_guide/best_practices.md index 8b7b015427f..edb8f372ea3 100644 --- a/doc/development/testing_guide/best_practices.md +++ b/doc/development/testing_guide/best_practices.md @@ -8,8 +8,8 @@ and effective _as well as_ fast. Here are some things to keep in mind regarding test performance: -- `double` and `spy` are faster than `FactoryGirl.build(...)` -- `FactoryGirl.build(...)` and `.build_stubbed` are faster than `.create`. +- `double` and `spy` are faster than `FactoryBot.build(...)` +- `FactoryBot.build(...)` and `.build_stubbed` are faster than `.create`. - Don't `create` an object when `build`, `build_stubbed`, `attributes_for`, `spy`, or `double` will do. Database persistence is slow! - Don't mark a feature as requiring JavaScript (through `@javascript` in @@ -254,13 +254,13 @@ end ### Factories -GitLab uses [factory_girl] as a test fixture replacement. +GitLab uses [factory_bot] as a test fixture replacement. - Factory definitions live in `spec/factories/`, named using the pluralization of their corresponding model (`User` factories are defined in `users.rb`). - There should be only one top-level factory definition per file. -- FactoryGirl methods are mixed in to all RSpec groups. This means you can (and - should) call `create(...)` instead of `FactoryGirl.create(...)`. +- FactoryBot methods are mixed in to all RSpec groups. This means you can (and + should) call `create(...)` instead of `FactoryBot.create(...)`. - Make use of [traits] to clean up definitions and usages. - When defining a factory, don't define attributes that are not required for the resulting record to pass validation. @@ -269,8 +269,8 @@ GitLab uses [factory_girl] as a test fixture replacement. - Factories don't have to be limited to `ActiveRecord` objects. [See example](https://gitlab.com/gitlab-org/gitlab-ce/commit/0b8cefd3b2385a21cfed779bd659978c0402766d). -[factory_girl]: https://github.com/thoughtbot/factory_girl -[traits]: http://www.rubydoc.info/gems/factory_girl/file/GETTING_STARTED.md#Traits +[factory_bot]: https://github.com/thoughtbot/factory_bot +[traits]: http://www.rubydoc.info/gems/factory_bot/file/GETTING_STARTED.md#Traits ### Fixtures diff --git a/doc/development/testing_guide/index.md b/doc/development/testing_guide/index.md index 8045bbad7ba..65386f231a0 100644 --- a/doc/development/testing_guide/index.md +++ b/doc/development/testing_guide/index.md @@ -33,7 +33,7 @@ changes should be tested. ## [Testing best practices](best_practices.md) -Everything you should know about how to write good tests: RSpec, FactoryGirl, +Everything you should know about how to write good tests: RSpec, FactoryBot, system tests, parameterized tests etc. --- diff --git a/doc/development/ux_guide/components.md b/doc/development/ux_guide/components.md index 16a811dbc74..d396964e7c1 100644 --- a/doc/development/ux_guide/components.md +++ b/doc/development/ux_guide/components.md @@ -10,7 +10,7 @@ * [Tables](#tables) * [Blocks](#blocks) * [Panels](#panels) -* [Dialog modals](#dialog-modals) +* [Modals](#modals) * [Alerts](#alerts) * [Forms](#forms) * [Search box](#search-box) @@ -255,18 +255,18 @@ Skeleton loading can replace any existing UI elements for the period in which th --- -## Dialog modals +## Modals -Dialog modals are only used for having a conversation and confirmation with the user. The user is not able to access the features on the main page until closing the modal. +Modals are only used for having a conversation and confirmation with the user. The user is not able to access the features on the main page until closing the modal. ### Usage -* When the action is irreversible, dialog modals provide the details and confirm with the user before they take an advanced action. -* When the action will affect privacy or authorization, dialog modals provide advanced information and confirm with the user. +* When the action is irreversible, modals provide the details and confirm with the user before they take an advanced action. +* When the action will affect privacy or authorization, modals provide advanced information and confirm with the user. ### Style -* Dialog modals contain the header, body, and actions. +* Modals contain the header, body, and actions. * **Header(1):** The header title is a question instead of a descriptive phrase. * **Body(2):** The content in body should never be ambiguous and unclear. It provides specific information. * **Actions(3):** Contains a affirmative action, a dismissive action, and an extra action. The order of actions from left to right: Dismissive action → Extra action → Affirmative action @@ -277,13 +277,13 @@ Dialog modals are only used for having a conversation and confirmation with the ### Placement -* Dialog modals should always be the center of the screen horizontally and be positioned **72px** from the top. +* Modals should always be the center of the screen horizontally and be positioned **72px** from the top. -| Dialog with 2 actions | Dialog with 3 actions | Special confirmation | +| Modal with 2 actions | Modal with 3 actions | Special confirmation | | --------------------- | --------------------- | -------------------- | |  |  |  | -> TODO: Special case for dialog modal. +> TODO: Special case for modal. --- diff --git a/doc/development/ux_guide/copy.md b/doc/development/ux_guide/copy.md index 12e8d0a31bb..af842da7f62 100644 --- a/doc/development/ux_guide/copy.md +++ b/doc/development/ux_guide/copy.md @@ -46,11 +46,11 @@ Avoid using periods in solitary sentences in these elements: * Labels
* Hover text
* Bulleted lists
-* Dialog body text
+* Modal body text
Periods should be used for:
-* Lists or dialogs with multiple sentences
+* Lists or modals with multiple sentences
* Any sentence followed by a link
| :white_check_mark: **Do** place periods after sentences followed by a link | :no_entry_sign: **Don’t** place periods after a link if it‘s not followed by a sentence |
@@ -80,7 +80,7 @@ Omit punctuation after phrases and labels to create a cleaner and more readable | Punctuation mark | Copy and paste | HTML entity | Unicode | Mac shortcut | Windows shortcut | Description |
|---|---|---|---|---|---|---|
-| Period | **.** | | | | | Omit for single sentences in affordances like labels, hover text, bulleted lists, and dialog body text.<br><br>Use in lists or dialogs with multiple sentences, and any sentence followed by a link or inline code.<br><br>Place inside quotation marks unless you’re telling the reader what to enter and it’s ambiguous whether to include the period. |
+| Period | **.** | | | | | Omit for single sentences in affordances like labels, hover text, bulleted lists, and modal body text.<br><br>Use in lists or modals with multiple sentences, and any sentence followed by a link or inline code.<br><br>Place inside quotation marks unless you’re telling the reader what to enter and it’s ambiguous whether to include the period. |
| Comma | **,** | | | | | Place inside quotation marks.<br><br>Use a [serial comma][serial comma] in lists of three or more terms. |
| Exclamation point | **!** | | | | | Avoid exclamation points as they tend to come across as shouting. Some exceptions include greetings or congratulatory messages. |
| Colon | **:** | `:` | `\u003A` | | | Omit from labels, for example, in the labels for fields in a form. |
@@ -88,7 +88,7 @@ Omit punctuation after phrases and labels to create a cleaner and more readable | Quotation marks | **“**<br><br>**”**<br><br>**‘**<br><br>**’** | `“`<br><br>`”`<br><br>`‘`<br><br>`’` | `\u201C`<br><br>`\u201D`<br><br>`\u2018`<br><br>`\u2019` | <kbd>⌥ Option</kbd>+<kbd>[</kbd><br><br><kbd>⌥ Option</kbd>+<kbd>⇧ Shift</kbd>+<kbd>[</kbd><br><br><kbd>⌥ Option</kbd>+<kbd>]</kbd><br><br><kbd>⌥ Option</kbd>+<kbd>⇧ Shift</kbd>+<kbd>]</kbd> | <kbd>Alt</kbd>+<kbd>0 1 4 7</kbd><br><br><kbd>Alt</kbd>+<kbd>0 1 4 8</kbd><br><br><kbd>Alt</kbd>+<kbd>0 1 4 5</kbd><br><br><kbd>Alt</kbd>+<kbd>0 1 4 6</kbd> | Use proper quotation marks (also known as smart quotes, curly quotes, or typographer’s quotes) for quotes. Single quotation marks are used for quotes inside of quotes.<br><br>The right single quotation mark symbol is also used for apostrophes.<br><br>Don’t use primes, straight quotes, or free-standing accents for quotation marks. |
| Primes | **′**<br><br>**″** | `′`<br><br>`″` | `\u2032`<br><br>`\u2033` | | <kbd>Alt</kbd>+<kbd>8 2 4 2</kbd><br><br><kbd>Alt</kbd>+<kbd>8 2 4 3</kbd> | Use prime (′) only in abbreviations for feet, arcminutes, and minutes: 3° 15′<br><br>Use double-prime (″) only in abbreviations for inches, arcseconds, and seconds: 3° 15′ 35″<br><br>Don’t use quotation marks, straight quotes, or free-standing accents for primes. |
| Straight quotes and accents | **"**<br><br>**'**<br><br>**`**<br><br>**´** | `"`<br><br>`'`<br><br>```<br><br>`´` | `\u0022`<br><br>`\u0027`<br><br>`\u0060`<br><br>`\u00B4` | | | Don’t use straight quotes or free-standing accents for primes or quotation marks.<br><br>Proper typography never uses straight quotes. They are left over from the age of typewriters and their only modern use is for code. |
-| Ellipsis | **…** | `…` | | <kbd>⌥ Option</kbd>+<kbd>;</kbd> | <kbd>Alt</kbd>+<kbd>0 1 3 3</kbd> | Use to indicate an action in progress (“Downloading…”) or incomplete or truncated text. No space before the ellipsis.<br><br>Omit from menu items or buttons that open a dialog or start some other process. |
+| Ellipsis | **…** | `…` | | <kbd>⌥ Option</kbd>+<kbd>;</kbd> | <kbd>Alt</kbd>+<kbd>0 1 3 3</kbd> | Use to indicate an action in progress (“Downloading…”) or incomplete or truncated text. No space before the ellipsis.<br><br>Omit from menu items or buttons that open a modal or start some other process. |
| Chevrons | **«**<br><br>**»**<br><br>**‹**<br><br>**›**<br><br>**<**<br><br>**>** | `«`<br><br>`»`<br><br>`‹`<br><br>`›`<br><br>`<`<br><br>`>` | `\u00AB`<br><br>`\u00BB`<br><br>`\u2039`<br><br>`\u203A`<br><br>`\u003C`<br><br>`\u003E`<br><br> | | | Omit from links or buttons that open another page or move to the next or previous step in a process. Also known as angle brackets, angular quote brackets, or guillemets. |
| Em dash | **—** | `—` | `\u2014` | <kbd>⌥ Option</kbd>+<kbd>⇧ Shift</kbd>+<kbd>-</kbd> | <kbd>Alt</kbd>+<kbd>0 1 5 1</kbd> | Avoid using dashes to separate text. If you must use dashes for this purpose — like this — use an em dash surrounded by spaces. |
| En dash | **–** | `–` | `\u2013` | <kbd>⌥ Option</kbd>+<kbd>-</kbd> | <kbd>Alt</kbd>+<kbd>0 1 5 0</kbd> | Use an en dash without spaces instead of a hyphen to indicate a range of values, such as numbers, times, and dates: “3–5 kg”, “8:00 AM–12:30 PM”, “10–17 Jan” |
@@ -175,7 +175,7 @@ A **comment** is a written piece of text that users of GitLab can create. Commen #### Discussion
A **discussion** is a group of 1 or more comments. A discussion can include subdiscussions. Some discussions have the special capability of being able to be **resolved**. Both the comments in the discussion and the discussion itself can be resolved.
-## Confirmation dialogs
+## Modals
- Destruction buttons should be clear and always say what they are destroying.
E.g., `Delete page` instead of just `Delete`.
@@ -184,6 +184,8 @@ A **discussion** is a group of 1 or more comments. A discussion can include subd - Avoid the word `cancel` or `canceled` in the descriptive copy. It can be
confusing when you then see the `Cancel` button.
+see also: guidelines for [modal components](components.md#modals)
+
---
Portions of this page are modifications based on work created and shared by the [Android Open Source Project][android project] and used according to terms described in the [Creative Commons 2.5 Attribution License][creative commons].
diff --git a/doc/development/writing_documentation.md b/doc/development/writing_documentation.md index b6def7ef541..43a79ffcaa5 100644 --- a/doc/development/writing_documentation.md +++ b/doc/development/writing_documentation.md @@ -1,75 +1,14 @@ # Writing documentation - **General Documentation**: written by the [developers responsible by creating features](#contributing-to-docs). Should be submitted in the same merge request containing code. Feature proposals (by GitLab contributors) should also be accompanied by its respective documentation. They can be later improved by PMs and Technical Writers. - - **Technical Articles**: written by any [GitLab Team](https://about.gitlab.com/team/) member, GitLab contributors, or [Community Writers](https://about.gitlab.com/handbook/product/technical-writing/community-writers/). + - **[Technical Articles](#technical-articles)**: written by any [GitLab Team](https://about.gitlab.com/team/) member, GitLab contributors, or [Community Writers](https://about.gitlab.com/handbook/product/technical-writing/community-writers/). - **Indexes per topic**: initially prepared by the Technical Writing Team, and kept up-to-date by developers and PMs in the same merge request containing code. They gather all resources for that topic in a single page (user and admin documentation, articles, and third-party docs). -## Distinction between General Documentation and Technical Articles - -### General documentation - -General documentation is categorized by _User_, _Admin_, and _Contributor_, and describe what that feature is, what it does, and its available settings. - -### Technical Articles - -Technical articles replace technical content that once lived in the [GitLab Blog](https://about.gitlab.com/blog/), where they got out-of-date and weren't easily found. - -They are topic-related documentation, written with an user-friendly approach and language, aiming to provide the community with guidance on specific processes to achieve certain objectives. - -A technical article guides users and/or admins to achieve certain objectives (within guides and tutorials), or provide an overview of that particular topic or feature (within technical overviews). It can also describe the use, implementation, or integration of third-party tools with GitLab. - -They live under `doc/articles/article-title/index.md`, and their images should be placed under `doc/articles/article-title/img/`. Find a list of existing [technical articles](../articles/index.md) here. - -#### Types of Technical Articles - -- **User guides**: technical content to guide regular users from point A to point B -- **Admin guides**: technical content to guide administrators of GitLab instances from point A to point B -- **Technical Overviews**: technical content describing features, solutions, and third-party integrations -- **Tutorials**: technical content provided step-by-step on how to do things, or how to reach very specific objectives - -#### Understanding guides, tutorials, and technical overviews - -Suppose there's a process to go from point A to point B in 5 steps: `(A) 1 > 2 > 3 > 4 > 5 (B)`. - -A **guide** can be understood as a description of certain processes to achieve a particular objective. A guide brings you from A to B describing the characteristics of that process, but not necessarily going over each step. It can mention, for example, steps 2 and 3, but does not necessarily explain how to accomplish them. - -- Live example: "GitLab Pages from A to Z - [Part 1](../user/project/pages/getting_started_part_one.md) to [Part 4](../user/project/pages/getting_started_part_four.md)" - -A **tutorial** requires a clear **step-by-step** guidance to achieve a singular objective. It brings you from A to B, describing precisely all the necessary steps involved in that process, showing each of the 5 steps to go from A to B. -It does not only describes steps 2 and 3, but also shows you how to accomplish them. - -- Live example (on the blog): [Hosting on GitLab.com with GitLab Pages](https://about.gitlab.com/2016/04/07/gitlab-pages-setup/) - -A **technical overview** is a description of what a certain feature is, and what it does, but does not walk -through the process of how to use it systematically. - -- Live example (on the blog): [GitLab Workflow, an overview](https://about.gitlab.com/2016/10/25/gitlab-workflow-an-overview/) - -#### Special format - -Every **Technical Article** contains, in the very beginning, a blockquote with the following information: - -- A reference to the **type of article** (user guide, admin guide, tech overview, tutorial) -- A reference to the **knowledge level** expected from the reader to be able to follow through (beginner, intermediate, advanced) -- A reference to the **author's name** and **GitLab.com handle** -- A reference of the **publication date** - -```md -> **Article [Type](../../development/writing_documentation.html#types-of-technical-articles):** tutorial || -> **Level:** intermediary || -> **Author:** [Name Surname](https://gitlab.com/username) || -> **Publication date:** AAAA/MM/DD -``` - -#### Technical Articles - Writing Method - -Use the [writing method](https://about.gitlab.com/handbook/product/technical-writing/#writing-method) defined by the Technical Writing team. - ## Documentation style guidelines All the docs follow the same [styleguide](doc_styleguide.md). -### Contributing to docs +## Contributing to docs Whenever a feature is changed, updated, introduced, or deprecated, the merge request introducing these changes must be accompanied by the documentation @@ -118,13 +57,31 @@ and for every **major** feature present in Community Edition. Currently GitLab docs use Redcarpet as [markdown](../user/markdown.md) engine, but there's an [open discussion](https://gitlab.com/gitlab-com/gitlab-docs/issues/50) for implementing Kramdown in the near future. -## Testing +### Previewing locally -We try to treat documentation as code, thus have implemented some testing. +To preview your changes to documentation locally, please follow +this [development guide](https://gitlab.com/gitlab-com/gitlab-docs/blob/master/README.md#development). + +### Testing + +We treat documentation as code, thus have implemented some testing. Currently, the following tests are in place: 1. `docs lint`: Check that all internal (relative) links work correctly and - that all cURL examples in API docs use the full switches. + that all cURL examples in API docs use the full switches. It's recommended + to [check locally](#previewing-locally) before pushing to GitLab by executing the command + `bundle exec nanoc check internal_links` on your local + [`gitlab-docs`](https://gitlab.com/gitlab-com/gitlab-docs) directory. +1. [`ee_compat_check`](https://docs.gitlab.com/ee/development/automatic_ce_ee_merge.html#avoiding-ce-gt-ee-merge-conflicts-beforehand) (runs on CE only): + When you submit a merge request to GitLab Community Edition (CE), + there is this additional job that runs against Enterprise Edition (EE) + and checks if your changes can apply cleanly to the EE codebase. + If that job fails, read the instructions in the job log for what to do next. + As CE is merged into EE once a day, it's important to avoid merge conflicts. + Submitting an EE-equivalent merge request cherry-picking all commits from CE to EE is + essential to avoid them. + +### Branch naming If your contribution contains **only** documentation changes, you can speed up the CI process by following some branch naming conventions. You have three @@ -139,17 +96,7 @@ choices: If your branch name matches any of the above, it will run only the docs tests. If it doesn't, the whole test suite will run (including docs). ---- - -When you submit a merge request to GitLab Community Edition (CE), there is an -additional job called `rake ee_compat_check` that runs against Enterprise -Edition (EE) and checks if your changes can apply cleanly to the EE codebase. -If that job fails, read the instructions in the job log for what to do next. -Contributors do not need to submit their changes to EE, GitLab Inc. employees -on the other hand need to make sure that their changes apply cleanly to both -CE and EE. - -## Previewing the changes live +### Previewing the changes live If you want to preview the doc changes of your merge request live, you can use the manual `review-docs-deploy` job in your merge request. You will need at @@ -164,7 +111,7 @@ You will need to push a branch to those repositories, it doesn't work for forks. TIP: **Tip:** If your branch contains only documentation changes, you can use -[special branch names](#testing) to avoid long running pipelines. +[special branch names](#branch-naming) to avoid long running pipelines. In the mini pipeline graph, you should see an `>>` icon. Clicking on it will reveal the `review-docs-deploy` job. Hit the play button for the job to start. @@ -209,12 +156,12 @@ working on. If you don't, the remote docs branch won't be removed either, and the server where the Review Apps are hosted will eventually be out of disk space. -### Behind the scenes +#### Technical aspects If you want to know the hot details, here's what's really happening: 1. You manually run the `review-docs-deploy` job in a CE/EE merge request. -1. The job runs the [`scirpts/trigger-build-docs`](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/scripts/trigger-build-docs) +1. The job runs the [`scripts/trigger-build-docs`](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/scripts/trigger-build-docs) script with the `deploy` flag, which in turn: 1. Takes your branch name and applies the following: - The slug of the branch name is used to avoid special characters since @@ -243,3 +190,65 @@ The following GitLab features are used among others: - [Review Apps](../ci/review_apps/index.md) - [Artifacts](../ci/yaml/README.md#artifacts) - [Specific Runner](../ci/runners/README.md#locking-a-specific-runner-from-being-enabled-for-other-projects) + +## General Documentation vs Technical Articles + +### General documentation + +General documentation is categorized by _User_, _Admin_, and _Contributor_, and describe what that feature is, what it does, and its available settings. + +### Technical Articles + +Technical articles replace technical content that once lived in the [GitLab Blog](https://about.gitlab.com/blog/), where they got out-of-date and weren't easily found. + +They are topic-related documentation, written with an user-friendly approach and language, aiming to provide the community with guidance on specific processes to achieve certain objectives. + +A technical article guides users and/or admins to achieve certain objectives (within guides and tutorials), or provide an overview of that particular topic or feature (within technical overviews). It can also describe the use, implementation, or integration of third-party tools with GitLab. + +They should be placed in a new directory named `/article-title/index.md` under a topic-related folder, and their images should be placed in `/article-title/img/`. For example, a new article on GitLab Pages should be placed in `doc/user/project/pages/article-title/` and a new article on GitLab CI/CD should be placed in `doc/ci/article-title/`. + +#### Types of Technical Articles + +- **User guides**: technical content to guide regular users from point A to point B +- **Admin guides**: technical content to guide administrators of GitLab instances from point A to point B +- **Technical Overviews**: technical content describing features, solutions, and third-party integrations +- **Tutorials**: technical content provided step-by-step on how to do things, or how to reach very specific objectives + +#### Understanding guides, tutorials, and technical overviews + +Suppose there's a process to go from point A to point B in 5 steps: `(A) 1 > 2 > 3 > 4 > 5 (B)`. + +A **guide** can be understood as a description of certain processes to achieve a particular objective. A guide brings you from A to B describing the characteristics of that process, but not necessarily going over each step. It can mention, for example, steps 2 and 3, but does not necessarily explain how to accomplish them. + +- Live example: "GitLab Pages from A to Z - [Part 1](../user/project/pages/getting_started_part_one.md) to [Part 4](../user/project/pages/getting_started_part_four.md)" + +A **tutorial** requires a clear **step-by-step** guidance to achieve a singular objective. It brings you from A to B, describing precisely all the necessary steps involved in that process, showing each of the 5 steps to go from A to B. +It does not only describes steps 2 and 3, but also shows you how to accomplish them. + +- Live example (on the blog): [Hosting on GitLab.com with GitLab Pages](https://about.gitlab.com/2016/04/07/gitlab-pages-setup/) + +A **technical overview** is a description of what a certain feature is, and what it does, but does not walk +through the process of how to use it systematically. + +- Live example (on the blog): [GitLab Workflow, an overview](https://about.gitlab.com/2016/10/25/gitlab-workflow-an-overview/) + +#### Special format + +Every **Technical Article** contains, in the very beginning, a blockquote with the following information: + +- A reference to the **type of article** (user guide, admin guide, tech overview, tutorial) +- A reference to the **knowledge level** expected from the reader to be able to follow through (beginner, intermediate, advanced) +- A reference to the **author's name** and **GitLab.com handle** +- A reference of the **publication date** + +```md +> **[Article Type](../../development/writing_documentation.html#types-of-technical-articles):** tutorial || +> **Level:** intermediary || +> **Author:** [Name Surname](https://gitlab.com/username) || +> **Publication date:** AAAA-MM-DD +``` + +#### Technical Articles - Writing Method + +Use the [writing method](https://about.gitlab.com/handbook/product/technical-writing/#writing-method) defined by the Technical Writing team. + diff --git a/doc/install/installation.md b/doc/install/installation.md index 570b0d5b22f..56888b05609 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -80,7 +80,7 @@ Make sure you have the right version of Git installed # Install Git sudo apt-get install -y git-core - # Make sure Git is version 2.13.6 or higher + # Make sure Git is version 2.14.3 or higher git --version Is the system packaged Git too old? Remove it and compile from source. @@ -299,9 +299,9 @@ sudo usermod -aG redis git ### Clone the Source # Clone GitLab repository - sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 10-2-stable gitlab + sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 10-3-stable gitlab -**Note:** You can change `10-2-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server! +**Note:** You can change `10-3-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server! ### Configure It diff --git a/doc/integration/omniauth.md b/doc/integration/omniauth.md index 0e20b8096e9..20087a981f9 100644 --- a/doc/integration/omniauth.md +++ b/doc/integration/omniauth.md @@ -229,16 +229,18 @@ In order to enable/disable an OmniAuth provider, go to Admin Area -> Settings -> ## Keep OmniAuth user profiles up to date You can enable profile syncing from selected OmniAuth providers and for all or for specific user information. - + +When authenticating using LDAP, the user's email is always synced. + ```ruby gitlab_rails['sync_profile_from_provider'] = ['twitter', 'google_oauth2'] gitlab_rails['sync_profile_attributes'] = ['name', 'email', 'location'] ``` - + **For installations from source** - + ```yaml omniauth: sync_profile_from_provider: ['twitter', 'google_oauth2'] - sync_profile_claims_from_provider: ['email', 'location'] - ```
\ No newline at end of file + sync_profile_attributes: ['email', 'location'] + ``` diff --git a/doc/monitoring/performance/introduction.md b/doc/monitoring/performance/introduction.md index ae88baa0c14..4d6f02b6547 100644 --- a/doc/monitoring/performance/introduction.md +++ b/doc/monitoring/performance/introduction.md @@ -1 +1 @@ -This document was moved to [administration/monitoring/performance/introduction](../../administration/monitoring/performance/introduction.md). +This document was moved to [administration/monitoring/performance/introduction](../../administration/monitoring/performance/index.md). diff --git a/doc/operations/README.md b/doc/operations/README.md index 58f16aff7bd..d7a83948b87 100644 --- a/doc/operations/README.md +++ b/doc/operations/README.md @@ -1 +1 @@ -This document was moved to [administration/operations](../administration/operations.md). +This document was moved to [another location](../administration/operations/index.md). diff --git a/doc/topics/authentication/index.md b/doc/topics/authentication/index.md index 597c98fbf6b..1f30909b0aa 100644 --- a/doc/topics/authentication/index.md +++ b/doc/topics/authentication/index.md @@ -6,6 +6,7 @@ This page gathers all the resources for the topic **Authentication** within GitL - [SSH](../../ssh/README.md) - [Two-Factor Authentication (2FA)](../../user/profile/account/two_factor_authentication.md#two-factor-authentication) +- [Why do I keep getting signed out?](../../user/profile/index.md#why-do-i-keep-getting-signed-out) - **Articles:** - [Support for Universal 2nd Factor Authentication - YubiKeys](https://about.gitlab.com/2016/06/22/gitlab-adds-support-for-u2f/) - [Security Webcast with Yubico](https://about.gitlab.com/2016/08/31/gitlab-and-yubico-security-webcast/) diff --git a/doc/topics/autodevops/index.md b/doc/topics/autodevops/index.md index d100b431721..0b48596006d 100644 --- a/doc/topics/autodevops/index.md +++ b/doc/topics/autodevops/index.md @@ -19,6 +19,7 @@ project in an easy and automatic way: 1. [Auto Build](#auto-build) 1. [Auto Test](#auto-test) 1. [Auto Code Quality](#auto-code-quality) +1. [Auto SAST (Static Application Security Testing)](#auto-sast) 1. [Auto Review Apps](#auto-review-apps) 1. [Auto Deploy](#auto-deploy) 1. [Auto Monitoring](#auto-monitoring) @@ -147,6 +148,10 @@ has a `.gitlab-ci.yml` or not: do that in a branch to test Auto DevOps before committing to `master`. NOTE: **Note:** +Starting with GitLab 10.3, when enabling Auto DevOps, a pipeline is +automatically run on the default branch. + +NOTE: **Note:** If you are a GitLab Administrator, you can enable Auto DevOps instance wide in **Admin Area > Settings > Continuous Integration and Deployment**. Doing that, all the projects that haven't explicitly set an option will have Auto DevOps @@ -198,6 +203,18 @@ out. In GitLab Enterprise Edition Starter, differences between the source and target branches are [shown in the merge request widget](https://docs.gitlab.com/ee/user/project/merge_requests/code_quality_diff.html). +### Auto SAST + +> Introduced in [GitLab Enterprise Edition Ultimate][ee] 10.3. + +Static Application Security Testing (SAST) uses the +[gl-sast Docker image](https://gitlab.com/gitlab-org/gl-sast) to run static +analysis on the current code and checks for potential security issues. Once the +report is created, it's uploaded as an artifact which you can later download and +check out. + +Any security warnings are also [shown in the merge request widget](https://docs.gitlab.com/ee/user/project/merge_requests/sast.html). + ### Auto Review Apps NOTE: **Note:** @@ -536,3 +553,4 @@ curl --data "value=true" --header "PRIVATE-TOKEN: personal_access_token" https:/ [postgresql]: https://www.postgresql.org/ [Auto DevOps template]: https://gitlab.com/gitlab-org/gitlab-ci-yml/blob/master/Auto-DevOps.gitlab-ci.yml [GitLab Omnibus Helm Chart]: ../../install/kubernetes/gitlab_omnibus.md +[ee]: https://about.gitlab.com/gitlab-ee/ diff --git a/doc/topics/git/index.md b/doc/topics/git/index.md index df56f031970..588f4fa369f 100644 --- a/doc/topics/git/index.md +++ b/doc/topics/git/index.md @@ -61,6 +61,10 @@ We've gathered some resources to help you to get the best from Git with GitLab. - [Getting Started with Git LFS](https://about.gitlab.com/2017/01/30/getting-started-with-git-lfs-tutorial/) - [Towards a production quality open source Git LFS server](https://about.gitlab.com/2015/08/13/towards-a-production-quality-open-source-git-lfs-server/) +## Troubleshooting + +- Learn a few [Git troubleshooting](troubleshooting_git.md) techniques to help you out. + ## General information - **Articles:** diff --git a/doc/topics/git/troubleshooting_git.md b/doc/topics/git/troubleshooting_git.md new file mode 100644 index 00000000000..8555c5e91ea --- /dev/null +++ b/doc/topics/git/troubleshooting_git.md @@ -0,0 +1,82 @@ +# Troubleshooting Git + +Sometimes things don't work the way they should or as you might expect when +you're using Git. Here are some tips on troubleshooting and resolving issues +with Git. + +## Broken pipe errors on git push + +'Broken pipe' errors can occur when attempting to push to a remote repository. +When pushing you will usually see: + +``` +Write failed: Broken pipe +fatal: The remote end hung up unexpectedly +``` + +To fix this issue, here are some possible solutions. + +### Increase the POST buffer size in Git + +**If pushing over HTTP**, you can try increasing the POST buffer size in Git's +configuration. Open a terminal and enter: + +```sh +git config http.postBuffer 52428800 +``` + +The value is specified in bytes, so in the above case the buffer size has been +set to 50MB. The default is 1MB. + +### Check your SSH configuration + +**If pushing over SSH**, first check your SSH configuration as 'Broken pipe' +errors can sometimes be caused by underlying issues with SSH (such as +authentication). Make sure that SSH is correctly configured by following the +instructions in the [SSH troubleshooting] docs. + +There's another option where you can prevent session timeouts by configuring +SSH 'keep alive' either on the client or on the server (if you are a GitLab +admin and have access to the server). + +NOTE: **Note:** configuring *both* the client and the server is unnecessary. + +**To configure SSH on the client side**: + +- On UNIX, edit `~/.ssh/config` (create the file if it doesn’t exist) and + add or edit: + + ``` + Host your-gitlab-instance-url.com + ServerAliveInterval 60 + ServerAliveCountMax 5 + ``` + +- On Windows, if you are using PuTTY, go to your session properties, then + navigate to "Connection" and under "Sending of null packets to keep + session active", set "Seconds between keepalives (0 to turn off)" to `60`. + +**To configure SSH on the server side**, edit `/etc/ssh/sshd_config` and add: + +``` +ClientAliveInterval 60 +ClientAliveCountMax 5 +``` + +### Running a git repack + +**If 'pack-objects' type errors are also being displayed**, you can try to +run a `git repack` before attempting to push to the remote repository again: + +```sh +git repack +git push +``` + +### Upgrade your Git client + +In case you're running an older version of Git (< 2.9), consider upgrading +to >= 2.9 (see [Broken pipe when pushing to Git repository][Broken-Pipe]). + +[SSH troubleshooting]: ../../ssh/README.md#troubleshooting "SSH Troubleshooting" +[Broken-Pipe]: https://stackoverflow.com/questions/19120120/broken-pipe-when-pushing-to-git-repository/36971469#36971469 "StackOverflow: 'Broken pipe when pushing to Git repository'" diff --git a/doc/update/10.2-to-10.3.md b/doc/update/10.2-to-10.3.md new file mode 100644 index 00000000000..07f9ee965f0 --- /dev/null +++ b/doc/update/10.2-to-10.3.md @@ -0,0 +1,360 @@ +--- +comments: false +--- + +# From 10.2 to 10.3 + +Make sure you view this update guide from the tag (version) of GitLab you would +like to install. In most cases this should be the highest numbered production +tag (without rc in it). You can select the tag in the version dropdown at the +top left corner of GitLab (below the menu bar). + +If the highest number stable branch is unclear please check the +[GitLab Blog](https://about.gitlab.com/blog/archives.html) for installation +guide links by version. + +### 1. Stop server + +```bash +sudo service gitlab stop +``` + +### 2. Backup + +```bash +cd /home/git/gitlab + +sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production +``` + +### 3. Update Ruby + +NOTE: GitLab 9.0 and higher only support Ruby 2.3.x and dropped support for Ruby 2.1.x. Be +sure to upgrade your interpreter if necessary. + +You can check which version you are running with `ruby -v`. + +Download and compile Ruby: + +```bash +mkdir /tmp/ruby && cd /tmp/ruby +curl --remote-name --progress https://cache.ruby-lang.org/pub/ruby/2.3/ruby-2.3.5.tar.gz +echo '3247e217d6745c27ef23bdc77b6abdb4b57a118f ruby-2.3.5.tar.gz' | shasum -c - && tar xzf ruby-2.3.5.tar.gz +cd ruby-2.3.5 +./configure --disable-install-rdoc +make +sudo make install +``` + +Install Bundler: + +```bash +sudo gem install bundler --no-ri --no-rdoc +``` + +### 4. Update Node + +GitLab now runs [webpack](http://webpack.js.org) to compile frontend assets and +it has a minimum requirement of node v4.3.0. + +You can check which version you are running with `node -v`. If you are running +a version older than `v4.3.0` you will need to update to a newer version. You +can find instructions to install from community maintained packages or compile +from source at the nodejs.org website. + +<https://nodejs.org/en/download/> + + +Since 8.17, GitLab requires the use of yarn `>= v0.17.0` to manage +JavaScript dependencies. + +```bash +curl --silent --show-error https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add - +echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list +sudo apt-get update +sudo apt-get install yarn +``` + +More information can be found on the [yarn website](https://yarnpkg.com/en/docs/install). + +### 5. Update Go + +NOTE: GitLab 9.2 and higher only supports Go 1.8.3 and dropped support for Go +1.5.x through 1.7.x. Be sure to upgrade your installation if necessary. + +You can check which version you are running with `go version`. + +Download and install Go: + +```bash +# Remove former Go installation folder +sudo rm -rf /usr/local/go + +curl --remote-name --progress https://storage.googleapis.com/golang/go1.8.3.linux-amd64.tar.gz +echo '1862f4c3d3907e59b04a757cfda0ea7aa9ef39274af99a784f5be843c80c6772 go1.8.3.linux-amd64.tar.gz' | shasum -a256 -c - && \ + sudo tar -C /usr/local -xzf go1.8.3.linux-amd64.tar.gz +sudo ln -sf /usr/local/go/bin/{go,godoc,gofmt} /usr/local/bin/ +rm go1.8.3.linux-amd64.tar.gz +``` + +### 6. 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 +sudo -u git -H git checkout -- locale +``` + +For GitLab Community Edition: + +```bash +cd /home/git/gitlab + +sudo -u git -H git checkout 10-3-stable +``` + +OR + +For GitLab Enterprise Edition: + +```bash +cd /home/git/gitlab + +sudo -u git -H git checkout 10-3-stable-ee +``` + +### 7. Update gitlab-shell + +```bash +cd /home/git/gitlab-shell + +sudo -u git -H git fetch --all --tags +sudo -u git -H git checkout v$(</home/git/gitlab/GITLAB_SHELL_VERSION) +sudo -u git -H bin/compile +``` + +### 8. Update gitlab-workhorse + +Install and compile gitlab-workhorse. GitLab-Workhorse uses +[GNU Make](https://www.gnu.org/software/make/). +If you are not using Linux you may have to run `gmake` instead of +`make` below. + +```bash +cd /home/git/gitlab-workhorse + +sudo -u git -H git fetch --all --tags +sudo -u git -H git checkout v$(</home/git/gitlab/GITLAB_WORKHORSE_VERSION) +sudo -u git -H make +``` + +### 9. Update Gitaly + +#### New Gitaly configuration options required + +In order to function Gitaly needs some additional configuration information. Below we assume you installed Gitaly in `/home/git/gitaly` and GitLab Shell in `/home/git/gitlab-shell`. + +```shell +echo ' +[gitaly-ruby] +dir = "/home/git/gitaly/ruby" + +[gitlab-shell] +dir = "/home/git/gitlab-shell" +' | sudo -u git tee -a /home/git/gitaly/config.toml +``` + +#### Check Gitaly configuration + +Due to a bug in the `rake gitlab:gitaly:install` script your Gitaly +configuration file may contain syntax errors. The block name +`[[storages]]`, which may occur more than once in your `config.toml` +file, should be `[[storage]]` instead. + +```shell +sudo -u git -H sed -i.pre-10.1 's/\[\[storages\]\]/[[storage]]/' /home/git/gitaly/config.toml +``` + +#### Compile Gitaly + +```shell +cd /home/git/gitaly +sudo -u git -H git fetch --all --tags +sudo -u git -H git checkout v$(</home/git/gitlab/GITALY_SERVER_VERSION) +sudo -u git -H make +``` + +### 10. Update MySQL permissions + +If you are using MySQL you need to grant the GitLab user the necessary +permissions on the database: + +```bash +mysql -u root -p -e "GRANT TRIGGER ON \`gitlabhq_production\`.* TO 'git'@'localhost';" +``` + +If you use MySQL with replication, or just have MySQL configured with binary logging, +you will need to also run the following on all of your MySQL servers: + +```bash +mysql -u root -p -e "SET GLOBAL log_bin_trust_function_creators = 1;" +``` + +You can make this setting permanent by adding it to your `my.cnf`: + +``` +log_bin_trust_function_creators=1 +``` + +### 11. Update configuration files + +#### New configuration options for `gitlab.yml` + +There might be configuration options available for [`gitlab.yml`][yaml]. View them with the command below and apply them manually to your current `gitlab.yml`: + +```sh +cd /home/git/gitlab + +git diff origin/10-2-stable:config/gitlab.yml.example origin/10-3-stable:config/gitlab.yml.example +``` + +#### Nginx configuration + +Ensure you're still up-to-date with the latest NGINX configuration changes: + +```sh +cd /home/git/gitlab + +# For HTTPS configurations +git diff origin/10-2-stable:lib/support/nginx/gitlab-ssl origin/10-3-stable:lib/support/nginx/gitlab-ssl + +# For HTTP configurations +git diff origin/10-2-stable:lib/support/nginx/gitlab origin/10-3-stable:lib/support/nginx/gitlab +``` + +If you are using Strict-Transport-Security in your installation to continue using it you must enable it in your Nginx +configuration as GitLab application no longer handles setting it. + +If you are using Apache instead of NGINX please see the updated [Apache templates]. +Also note that because Apache does not support upstreams behind Unix sockets you +will need to let gitlab-workhorse listen on a TCP port. You can do this +via [/etc/default/gitlab]. + +[Apache templates]: https://gitlab.com/gitlab-org/gitlab-recipes/tree/master/web-server/apache +[/etc/default/gitlab]: https://gitlab.com/gitlab-org/gitlab-ce/blob/10-3-stable/lib/support/init.d/gitlab.default.example#L38 + +#### SMTP configuration + +If you're installing from source and use SMTP to deliver mail, you will need to add the following line +to config/initializers/smtp_settings.rb: + +```ruby +ActionMailer::Base.delivery_method = :smtp +``` + +See [smtp_settings.rb.sample] as an example. + +[smtp_settings.rb.sample]: https://gitlab.com/gitlab-org/gitlab-ce/blob/10-3-stable/config/initializers/smtp_settings.rb.sample#L13 + +#### Init script + +There might be new configuration options available for [`gitlab.default.example`][gl-example]. View them with the command below and apply them manually to your current `/etc/default/gitlab`: + +```sh +cd /home/git/gitlab + +git diff origin/10-2-stable:lib/support/init.d/gitlab.default.example origin/10-3-stable:lib/support/init.d/gitlab.default.example +``` + +Ensure you're still up-to-date with the latest init script changes: + +```bash +cd /home/git/gitlab + +sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab +``` + +For Ubuntu 16.04.1 LTS: + +```bash +sudo systemctl daemon-reload +``` + +### 12. Install libs, migrations, etc. + +```bash +cd /home/git/gitlab + +# MySQL installations (note: the line below states '--without postgres') +sudo -u git -H bundle install --without postgres development test --deployment + +# PostgreSQL installations (note: the line below states '--without mysql') +sudo -u git -H bundle install --without mysql development test --deployment + +# Optional: clean up old gems +sudo -u git -H bundle clean + +# Run database migrations +sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production + +# Compile GetText PO files + +sudo -u git -H bundle exec rake gettext:compile RAILS_ENV=production + +# Update node dependencies and recompile assets +sudo -u git -H bundle exec rake yarn:install gitlab:assets:clean gitlab:assets:compile RAILS_ENV=production NODE_ENV=production + +# Clean up cache +sudo -u git -H bundle exec rake cache:clear RAILS_ENV=production +``` + +**MySQL installations**: Run through the `MySQL strings limits` and `Tables and data conversion to utf8mb4` [tasks](../install/database_mysql.md). + +### 13. Start application + +```bash +sudo service gitlab start +sudo service nginx restart +``` + +### 14. Check application status + +Check if GitLab and its environment are configured correctly: + +```bash +cd /home/git/gitlab + +sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production +``` + +To make sure you didn't miss anything run a more thorough check: + +```bash +cd /home/git/gitlab + +sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production +``` + +If all items are green, then congratulations, the upgrade is complete! + +## Things went south? Revert to previous version (10.0) + +### 1. Revert the code to the previous version + +Follow the [upgrade guide from 9.5 to 10.0](9.5-to-10.0.md), except for the +database migration (the backup is already migrated to the previous version). + +### 2. Restore from the backup + +```bash +cd /home/git/gitlab + +sudo -u git -H bundle exec rake gitlab:backup:restore RAILS_ENV=production +``` + +If you have more than one backup `*.tar` file(s) please add `BACKUP=timestamp_of_backup` to the command above. + +[yaml]: https://gitlab.com/gitlab-org/gitlab-ce/blob/10-3-stable/config/gitlab.yml.example +[gl-example]: https://gitlab.com/gitlab-org/gitlab-ce/blob/10-3-stable/lib/support/init.d/gitlab.default.example diff --git a/doc/user/discussions/img/commit_comment_mr_context.png b/doc/user/discussions/img/commit_comment_mr_context.png Binary files differnew file mode 100644 index 00000000000..b363e0035e8 --- /dev/null +++ b/doc/user/discussions/img/commit_comment_mr_context.png diff --git a/doc/user/discussions/img/commit_comment_mr_discussions_tab.png b/doc/user/discussions/img/commit_comment_mr_discussions_tab.png Binary files differnew file mode 100644 index 00000000000..2b06cdcc055 --- /dev/null +++ b/doc/user/discussions/img/commit_comment_mr_discussions_tab.png diff --git a/doc/user/discussions/img/merge_request_commits_tab.png b/doc/user/discussions/img/merge_request_commits_tab.png Binary files differnew file mode 100644 index 00000000000..41a3648f390 --- /dev/null +++ b/doc/user/discussions/img/merge_request_commits_tab.png diff --git a/doc/user/discussions/index.md b/doc/user/discussions/index.md index 2206b2860f4..eacfe2baa27 100644 --- a/doc/user/discussions/index.md +++ b/doc/user/discussions/index.md @@ -32,6 +32,43 @@ hide discussions that are no longer relevant. Comments and discussions can be resolved by anyone with at least Developer access to the project or the author of the merge request. +### Commit discussions in the context of a merge request + +> [Introduced][ce-31847] in GitLab 10.3. + +For reviewers with commit-based workflow, it may be useful to add discussions to +specific commit diffs in the context of a merge request. These discussions will +persist through a commit ID change when: + +- force-pushing after a rebase +- amending a commit + +To create a commit diff discussion: + +1. Navigate to the merge request **Commits** tab. A list of commits that + constitute the merge request will be shown. + +  + +1. Navigate to a specific commit, click on the **Changes** tab (where you + will only be presented diffs from the selected commit), and leave a comment. + +  + +1. Any discussions created this way will be shown in the merge request's + **Discussions** tab and are resolvable. + +  + +Discussions created this way will only appear in the original merge request +and not when navigating to that commit under your project's +**Repository > Commits** page. + +TIP: **Tip:** +When a link of a commit reference is found in a discussion inside a merge +request, it will be automatically converted to a link in the context of the +current merge request. + ### Jumping between unresolved discussions When a merge request has a large number of comments it can be difficult to track @@ -133,6 +170,15 @@ From now on, any discussions on a diff will be resolved by default if a push makes that diff section outdated. Discussions on lines that don't change and top-level resolvable discussions are not automatically resolved. +## Commit discussions + +You can add comments and discussion threads to a particular commit under your +project's **Repository > Commits**. + +CAUTION: **Attention:** +Discussions created this way will be lost if the commit ID changes after a +force push. + ## Threaded discussions > [Introduced][ce-7527] in GitLab 9.1. @@ -229,6 +275,7 @@ edit existing comments. Non-team members are restricted from adding or editing c [ce-14053]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/14053 [ce-14061]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/14061 [ce-14531]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/14531 +[ce-31847]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/31847 [resolve-discussion-button]: img/resolve_discussion_button.png [resolve-comment-button]: img/resolve_comment_button.png [discussion-view]: img/discussion_view.png diff --git a/doc/user/group/index.md b/doc/user/group/index.md index a1671f9dd91..1733017cbc0 100644 --- a/doc/user/group/index.md +++ b/doc/user/group/index.md @@ -197,11 +197,11 @@ username, you can create a new group and transfer projects to it. Changing a group's path can have unintended side effects. * Existing web URLs for the group and anything under it (i.e. projects) will -redirect to the new URLs -* Existing Git remote URLs for projects under the group will no longer work, but -Git responses will show an error with the new remote URL -* The original namespace can be claimed again by any group or user, which will -destroy web redirects and Git remote warnings +redirect to the new URLs. +* Existing Git remote URLs for projects under the group will redirect to the new remote URL, and they +will show a warning with the new remote URL. +* The redirect to the new URL is permanent, that implies the original namespace +can't be claimed again by any group or user. * If you are vacating the path so it can be claimed by another group or user, you may need to rename the group name as well since both names and paths must be unique diff --git a/doc/user/markdown.md b/doc/user/markdown.md index a671c92640a..552abac747b 100644 --- a/doc/user/markdown.md +++ b/doc/user/markdown.md @@ -195,12 +195,23 @@ With inline diffs tags you can display {+ additions +} or [- deletions -]. The wrapping tags can be either curly braces or square brackets [+ additions +] or {- deletions -}. +Examples: + +``` +- {+ additions +} +- [+ additions +] +- {- deletions -} +- [- deletions -] +``` + However the wrapping tags cannot be mixed as such: +``` - {+ additions +] - [+ additions +} - {- deletions -] - [- deletions -} +``` ### Emoji diff --git a/doc/user/profile/index.md b/doc/user/profile/index.md index 5fcc0501dc1..dae4cbe170b 100644 --- a/doc/user/profile/index.md +++ b/doc/user/profile/index.md @@ -1,8 +1,32 @@ # User account -When logged into their GitLab account, users can customize their +When signed into their GitLab account, users can customize their experience according to the best approach to their cases. +## Signing in + +There are several ways to sign into your GitLab account. +See the [authentication topic](../../topics/authentication/index.md) for more details. + +### Why do I keep getting signed out? + +When signing in to the main GitLab application, a `_gitlab_session` cookie is +set. `_gitlab_session` is cleared client-side when you close your browser +and expires after "Application settings -> Session duration (minutes)"/`session_expire_delay` +(defaults to `10080` minutes = 7 days). + +When signing in to the main GitLab application, you can also check the +"Remember me" option which sets the `remember_user_token` +cookie (via [`devise`](https://github.com/plataformatec/devise)). +`remember_user_token` expires after +`config/initializers/devise.rb` -> `config.remember_for` (defaults to 2 weeks). + +When the `_gitlab_session` expires or isn't available, GitLab uses the `remember_user_token` +to get you a new `_gitlab_session` and keep you signed in through browser restarts. + +After your `remember_user_token` expires and your `_gitlab_session` is cleared/expired, +you will be asked to sign in again to verify your identity (which is for security reasons). + ## Username Your `username` is a unique [`namespace`](../group/index.md#namespaces) @@ -21,11 +45,10 @@ Alternatively, you can follow [this detailed procedure from the GitLab Team Hand Changing your username can have unintended side effects. * Existing web URLs for the user and anything under it (i.e. projects) will -redirect to the new URLs -* Existing Git remote URLs for projects under the user will no longer work, but -Git responses will show an error with the new remote URL -* The original namespace can be claimed again by any group or user, which will -destroy any web redirects and Git remote warnings +redirect to the new URLs. +* Existing Git remote URLs for projects under the user will redirect to the new remote URL. Git responses +will show a warning with the new remote URL. +* The redirect to the new URL is permanent, that implies the original namespace can't be claimed again by any group or user. > It is currently not possible to rename a namespace if it contains a project with container registry tags, because the project cannot be moved. diff --git a/doc/user/project/integrations/jira.md b/doc/user/project/integrations/jira.md index 93aec56f8dc..7dc234a9759 100644 --- a/doc/user/project/integrations/jira.md +++ b/doc/user/project/integrations/jira.md @@ -98,6 +98,9 @@ password as they will be needed when configuring GitLab in the next section. - GitLab 8.14 introduced a new way to integrate with JIRA which greatly simplified the configuration options you have to enter. If you are using an older version, [follow this documentation][jira-repo-old-docs]. +- In order to support Oracle's Access Manager, GitLab will send additional cookies + to enable Basic Auth. The cookie being added to each request is `OBBasicAuth` with + a value of `fromDialog`. To enable JIRA integration in a project, navigate to the [Integrations page](project_services.md#accessing-the-project-services), click diff --git a/doc/user/project/merge_requests/img/create_from_email.png b/doc/user/project/merge_requests/img/create_from_email.png Binary files differnew file mode 100644 index 00000000000..71eb4bf267d --- /dev/null +++ b/doc/user/project/merge_requests/img/create_from_email.png diff --git a/doc/user/project/merge_requests/img/merge_request_diff_file_navigation.png b/doc/user/project/merge_requests/img/merge_request_diff_file_navigation.png Binary files differindex 9b8aee47411..4eee734ff8d 100644 --- a/doc/user/project/merge_requests/img/merge_request_diff_file_navigation.png +++ b/doc/user/project/merge_requests/img/merge_request_diff_file_navigation.png diff --git a/doc/user/project/merge_requests/index.md b/doc/user/project/merge_requests/index.md index d76ea259301..7037d7f5989 100644 --- a/doc/user/project/merge_requests/index.md +++ b/doc/user/project/merge_requests/index.md @@ -27,7 +27,7 @@ With GitLab merge requests, you can: - [Resolve merge conflicts from the UI](#resolve-conflicts) - Enable [fast-forward merge requests](#fast-forward-merge-requests) - Enable [semi-linear history merge requests](#semi-linear-history-merge-requests) as another security layer to guarantee the pipeline is passing in the target branch -- [Create new merge requests by email](#create_by_email) +- [Create new merge requests by email](#create-new-merge-requests-by-email) With **[GitLab Enterprise Edition][ee]**, you can also: @@ -138,7 +138,13 @@ You can create a new merge request by sending an email to a user-specific email address. The address can be obtained on the merge requests page by clicking on a **Email a new merge request to this project** button. The subject will be used as the source branch name for the new merge request and the target branch -will be the default branch for the project. +will be the default branch for the project. The message body (if not empty) +will be used as the merge request description. You need +["Reply by email"](../../../administration/reply_by_email.md) enabled to use +this feature. If it's not enabled to your instance, you may ask your GitLab +administrator to do so. + + ## Revert changes diff --git a/doc/user/project/milestones/img/progress.png b/doc/user/project/milestones/img/progress.png Binary files differdeleted file mode 100644 index c85aecca729..00000000000 --- a/doc/user/project/milestones/img/progress.png +++ /dev/null diff --git a/doc/user/project/milestones/img/sidebar.png b/doc/user/project/milestones/img/sidebar.png Binary files differnew file mode 100644 index 00000000000..274962a936c --- /dev/null +++ b/doc/user/project/milestones/img/sidebar.png diff --git a/doc/user/project/milestones/index.md b/doc/user/project/milestones/index.md index 83adbd8cce2..20249926910 100644 --- a/doc/user/project/milestones/index.md +++ b/doc/user/project/milestones/index.md @@ -47,13 +47,15 @@ special options available when filtering by milestone: date less than today. Note that this can return results from several milestones in the same project. -## Milestone progress statistics +## Milestone sidebar -Milestone statistics can be viewed in the milestone sidebar. The milestone percentage statistic -is calculated as; closed and merged merge requests plus all closed issues divided by +The milestone sidebar shows percentage complete, start date and due date, +issues, total issue weight, total issue time spent, and merge requests. + +The percentage complete is calcualted as: Closed and merged merge requests plus all closed issues divided by total merge requests and issues. - + ## Quick actions diff --git a/doc/user/project/pipelines/settings.md b/doc/user/project/pipelines/settings.md index daa5463d680..43451844f2d 100644 --- a/doc/user/project/pipelines/settings.md +++ b/doc/user/project/pipelines/settings.md @@ -68,7 +68,7 @@ in the pipelines settings page. Access to pipelines and job details (including output of logs and artifacts) is checked against your current user access level and the **Public pipelines** -project setting. +project setting under your project's **Settings > CI/CD > General pipelines settings**. If **Public pipelines** is enabled (default): diff --git a/doc/user/project/settings/index.md b/doc/user/project/settings/index.md index a234a647b77..2b6fde1e2a5 100644 --- a/doc/user/project/settings/index.md +++ b/doc/user/project/settings/index.md @@ -50,3 +50,9 @@ Here you can run housekeeping, archive, rename, transfer, or remove a project. It's possible to mark a project as archived via the Project Settings. An archived project will be hidden by default in the project listings. An archived project can be fully restored and will therefore retain it's repository and all associated resources whilst in an archived state. + +#### Renaming a project + +>**Note:** Only Project Owners and Admin users have the permission to rename a project + +It's possible to rename a project from "Rename repository" or "Transfer project" sections. When doing so, you will need to update your local repositories to point to the new location, otherwise Git operations will be rejected. diff --git a/features/steps/groups.rb b/features/steps/groups.rb index a2d9a0332e0..753694a5392 100644 --- a/features/steps/groups.rb +++ b/features/steps/groups.rb @@ -138,7 +138,7 @@ class Spinach::Features::Groups < Spinach::FeatureSteps private def assigned_to_me(key) - project.send(key).where(assignee_id: current_user.id) + project.send(key).assigned_to(current_user) end def project diff --git a/features/steps/shared/issuable.rb b/features/steps/shared/issuable.rb index 714985f2051..f90247c3fe8 100644 --- a/features/steps/shared/issuable.rb +++ b/features/steps/shared/issuable.rb @@ -2,7 +2,7 @@ module SharedIssuable include Spinach::DSL def edit_issuable - find('.issuable-edit', visible: true).click + find('.js-issuable-edit', visible: true).click end step 'project "Community" has "Community issue" open issue' do diff --git a/features/support/env.rb b/features/support/env.rb index 5962745d501..91a92314959 100644 --- a/features/support/env.rb +++ b/features/support/env.rb @@ -27,7 +27,7 @@ Spinach.hooks.before_run do # web editor and merge TestEnv.disable_pre_receive - include FactoryGirl::Syntax::Methods + include FactoryBot::Syntax::Methods include GitlabRoutingHelper end @@ -42,11 +42,11 @@ module StdoutReporterWithScenarioLocation # Override the standard reporter to show filename and line number next to each # scenario for easy, focused re-runs def before_scenario_run(scenario, step_definitions = nil) - @max_step_name_length = scenario.steps.map(&:name).map(&:length).max if scenario.steps.any? + @max_step_name_length = scenario.steps.map(&:name).map(&:length).max if scenario.steps.any? # rubocop:disable Gitlab/ModuleWithInstanceVariables name = scenario.name # This number has no significance, it's just to line things up - max_length = @max_step_name_length + 19 + max_length = @max_step_name_length + 19 # rubocop:disable Gitlab/ModuleWithInstanceVariables out.puts "\n #{'Scenario:'.green} #{name.light_green.ljust(max_length)}" \ " # #{scenario.feature.filename}:#{scenario.line}" end diff --git a/lib/api/circuit_breakers.rb b/lib/api/circuit_breakers.rb index 118883f5ea5..598c76f6168 100644 --- a/lib/api/circuit_breakers.rb +++ b/lib/api/circuit_breakers.rb @@ -41,7 +41,7 @@ module API detail 'This feature was introduced in GitLab 9.5' end delete do - Gitlab::Git::Storage::CircuitBreaker.reset_all! + Gitlab::Git::Storage::FailureInfo.reset_all! end end end diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 62ee20bf7de..928706dfda7 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -16,10 +16,13 @@ module API class UserBasic < UserSafe expose :state + expose :avatar_url do |user, options| user.avatar_url(only_path: false) end + expose :avatar_path, if: ->(user, options) { options.fetch(:only_path, false) && user.avatar_path } + expose :web_url do |user, options| Gitlab::Routing.url_helpers.user_url(user) end @@ -245,8 +248,21 @@ module API end class GroupDetail < Group - expose :projects, using: Entities::Project - expose :shared_projects, using: Entities::Project + expose :projects, using: Entities::Project do |group, options| + GroupProjectsFinder.new( + group: group, + current_user: options[:current_user], + options: { only_owned: true } + ).execute + end + + expose :shared_projects, using: Entities::Project do |group, options| + GroupProjectsFinder.new( + group: group, + current_user: options[:current_user], + options: { only_shared: true } + ).execute + end end class Commit < Grape::Entity diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index 686bf7a3c2b..9ba15893f55 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -32,6 +32,11 @@ module API end end + # rubocop:disable Gitlab/ModuleWithInstanceVariables + # We can't rewrite this with StrongMemoize because `sudo!` would + # actually write to `@current_user`, and `sudo?` would immediately + # call `current_user` again which reads from `@current_user`. + # We should rewrite this in a way that using StrongMemoize is possible def current_user return @current_user if defined?(@current_user) @@ -45,6 +50,7 @@ module API @current_user end + # rubocop:enable Gitlab/ModuleWithInstanceVariables def sudo? initial_current_user != current_user @@ -415,6 +421,7 @@ module API private + # rubocop:disable Gitlab/ModuleWithInstanceVariables def initial_current_user return @initial_current_user if defined?(@initial_current_user) @@ -424,6 +431,7 @@ module API unauthorized! end end + # rubocop:enable Gitlab/ModuleWithInstanceVariables def sudo! return unless sudo_identifier @@ -443,7 +451,7 @@ module API sudoed_user = find_user(sudo_identifier) not_found!("User with ID or username '#{sudo_identifier}'") unless sudoed_user - @current_user = sudoed_user + @current_user = sudoed_user # rubocop:disable Gitlab/ModuleWithInstanceVariables end def sudo_identifier diff --git a/lib/api/helpers/internal_helpers.rb b/lib/api/helpers/internal_helpers.rb index d6dea4c30e3..eff1c5b70ea 100644 --- a/lib/api/helpers/internal_helpers.rb +++ b/lib/api/helpers/internal_helpers.rb @@ -6,18 +6,16 @@ module API 'git-upload-pack' => [:ssh_upload_pack, Gitlab::GitalyClient::MigrationStatus::OPT_OUT] }.freeze + attr_reader :redirected_path + def wiki? - set_project unless defined?(@wiki) - @wiki + set_project unless defined?(@wiki) # rubocop:disable Gitlab/ModuleWithInstanceVariables + @wiki # rubocop:disable Gitlab/ModuleWithInstanceVariables end def project - set_project unless defined?(@project) - @project - end - - def redirected_path - @redirected_path + set_project unless defined?(@project) # rubocop:disable Gitlab/ModuleWithInstanceVariables + @project # rubocop:disable Gitlab/ModuleWithInstanceVariables end def ssh_authentication_abilities @@ -69,6 +67,7 @@ module API private + # rubocop:disable Gitlab/ModuleWithInstanceVariables def set_project if params[:gl_repository] @project, @wiki = Gitlab::GlRepository.parse(params[:gl_repository]) @@ -77,6 +76,7 @@ module API @project, @wiki, @redirected_path = Gitlab::RepoPath.parse(params[:project]) end end + # rubocop:enable Gitlab/ModuleWithInstanceVariables # Project id to pass between components that don't share/don't have # access to the same filesystem mounts diff --git a/lib/api/internal.rb b/lib/api/internal.rb index 451121a4cea..ccaaeca10d4 100644 --- a/lib/api/internal.rb +++ b/lib/api/internal.rb @@ -4,6 +4,7 @@ module API before { authenticate_by_gitlab_shell_token! } helpers ::API::Helpers::InternalHelpers + helpers ::Gitlab::Identifier namespace 'internal' do # Check if git command is allowed to project @@ -176,17 +177,25 @@ module API post '/post_receive' do status 200 - PostReceive.perform_async(params[:gl_repository], params[:identifier], params[:changes]) broadcast_message = BroadcastMessage.current&.last&.message reference_counter_decreased = Gitlab::ReferenceCounter.new(params[:gl_repository]).decrease - { + output = { merge_request_urls: merge_request_urls, broadcast_message: broadcast_message, reference_counter_decreased: reference_counter_decreased } + + project = Gitlab::GlRepository.parse(params[:gl_repository]).first + user = identify(params[:identifier]) + redirect_message = Gitlab::Checks::ProjectMoved.fetch_redirect_message(user.id, project.id) + if redirect_message + output[:redirected_message] = redirect_message + end + + output end end end diff --git a/lib/api/issues.rb b/lib/api/issues.rb index e60e00d7956..5f943ba27d1 100644 --- a/lib/api/issues.rb +++ b/lib/api/issues.rb @@ -161,6 +161,8 @@ module API use :issue_params end post ':id/issues' do + authorize! :create_issue, user_project + # Setting created_at time only allowed for admins and project owners unless current_user.admin? || user_project.owner == current_user params.delete(:created_at) diff --git a/lib/api/repositories.rb b/lib/api/repositories.rb index 7887b886c03..4f36bbd760f 100644 --- a/lib/api/repositories.rb +++ b/lib/api/repositories.rb @@ -110,10 +110,12 @@ module API end params do use :pagination + optional :order_by, type: String, values: %w[email name commits], default: nil, desc: 'Return contributors ordered by `name` or `email` or `commits`' + optional :sort, type: String, values: %w[asc desc], default: nil, desc: 'Sort by asc (ascending) or desc (descending)' end get ':id/repository/contributors' do begin - contributors = ::Kaminari.paginate_array(user_project.repository.contributors) + contributors = ::Kaminari.paginate_array(user_project.repository.contributors(order_by: params[:order_by], sort: params[:sort])) present paginate(contributors), with: Entities::Contributor rescue not_found! diff --git a/lib/api/tags.rb b/lib/api/tags.rb index 0d394a7b441..5e0afc6a7e4 100644 --- a/lib/api/tags.rb +++ b/lib/api/tags.rb @@ -14,10 +14,15 @@ module API success Entities::Tag end params do + optional :sort, type: String, values: %w[asc desc], default: 'desc', + desc: 'Return tags sorted in updated by `asc` or `desc` order.' + optional :order_by, type: String, values: %w[name updated], default: 'updated', + desc: 'Return tags ordered by `name` or `updated` fields.' use :pagination end get ':id/repository/tags' do - tags = ::Kaminari.paginate_array(user_project.repository.tags.sort_by(&:name).reverse) + tags = ::Kaminari.paginate_array(::TagsFinder.new(user_project.repository, sort: "#{params[:order_by]}_#{params[:sort]}").execute) + present paginate(tags), with: Entities::Tag, project: user_project end diff --git a/lib/banzai/filter/table_of_contents_filter.rb b/lib/banzai/filter/table_of_contents_filter.rb index 47151626208..97244159985 100644 --- a/lib/banzai/filter/table_of_contents_filter.rb +++ b/lib/banzai/filter/table_of_contents_filter.rb @@ -32,6 +32,7 @@ module Banzai .gsub(PUNCTUATION_REGEXP, '') # remove punctuation .tr(' ', '-') # replace spaces with dash .squeeze('-') # replace multiple dashes with one + .gsub(/\A(\d+)\z/, 'anchor-\1') # digits-only hrefs conflict with issue refs uniq = headers[id] > 0 ? "-#{headers[id]}" : '' headers[id] += 1 diff --git a/lib/extracts_path.rb b/lib/extracts_path.rb index 721ed97bb6b..d8aca3304c5 100644 --- a/lib/extracts_path.rb +++ b/lib/extracts_path.rb @@ -40,7 +40,7 @@ module ExtractsPath def extract_ref(id) pair = ['', ''] - return pair unless @project + return pair unless @project # rubocop:disable Gitlab/ModuleWithInstanceVariables if id =~ /^(\h{40})(.+)/ # If the ref appears to be a SHA, we're done, just split the string @@ -104,6 +104,7 @@ module ExtractsPath # # Automatically renders `not_found!` if a valid tree path could not be # resolved (e.g., when a user inserts an invalid path or ref). + # rubocop:disable Gitlab/ModuleWithInstanceVariables def assign_ref_vars # assign allowed options allowed_options = ["filter_ref"] @@ -127,13 +128,18 @@ module ExtractsPath @hex_path = Digest::SHA1.hexdigest(@path) @logs_path = logs_file_project_ref_path(@project, @ref, @path) - rescue RuntimeError, NoMethodError, InvalidPathError render_404 end + # rubocop:enable Gitlab/ModuleWithInstanceVariables def tree - @tree ||= @repo.tree(@commit.id, @path) + @tree ||= @repo.tree(@commit.id, @path) # rubocop:disable Gitlab/ModuleWithInstanceVariables + end + + def lfs_blob_ids + blob_ids = tree.blobs.map(&:id) + @lfs_blob_ids = Gitlab::Git::Blob.batch_lfs_pointers(@project.repository, blob_ids).map(&:id) # rubocop:disable Gitlab/ModuleWithInstanceVariables end private @@ -146,8 +152,8 @@ module ExtractsPath end def ref_names - return [] unless @project + return [] unless @project # rubocop:disable Gitlab/ModuleWithInstanceVariables - @ref_names ||= @project.repository.ref_names + @ref_names ||= @project.repository.ref_names # rubocop:disable Gitlab/ModuleWithInstanceVariables end end diff --git a/lib/feature.rb b/lib/feature.rb index ac3bc65c0d5..8e9ba5c530a 100644 --- a/lib/feature.rb +++ b/lib/feature.rb @@ -1,5 +1,3 @@ -require 'flipper/adapters/active_record' - class Feature # Classes to override flipper table names class FlipperFeature < Flipper::Adapters::ActiveRecord::Feature @@ -62,12 +60,7 @@ class Feature end def flipper - @flipper ||= begin - adapter = Flipper::Adapters::ActiveRecord.new( - feature_class: FlipperFeature, gate_class: FlipperGate) - - Flipper.new(adapter) - end + @flipper ||= Flipper.instance end # This method is called from config/initializers/flipper.rb and can be used diff --git a/lib/gitlab/bare_repository_import/importer.rb b/lib/gitlab/bare_repository_import/importer.rb index 196de667805..298409d8b5a 100644 --- a/lib/gitlab/bare_repository_import/importer.rb +++ b/lib/gitlab/bare_repository_import/importer.rb @@ -55,6 +55,7 @@ module Gitlab name: project_name, path: project_name, skip_disk_validation: true, + import_type: 'gitlab_project', namespace_id: group&.id).execute if project.persisted? && mv_repo(project) diff --git a/lib/gitlab/bare_repository_import/repository.rb b/lib/gitlab/bare_repository_import/repository.rb index 8574ac6eb30..fa7891c8dcc 100644 --- a/lib/gitlab/bare_repository_import/repository.rb +++ b/lib/gitlab/bare_repository_import/repository.rb @@ -7,6 +7,8 @@ module Gitlab @root_path = root_path @repo_path = repo_path + @root_path << '/' unless root_path.ends_with?('/') + # Split path into 'all/the/namespaces' and 'project_name' @group_path, _, @project_name = repo_relative_path.rpartition('/') end diff --git a/lib/gitlab/cache/request_cache.rb b/lib/gitlab/cache/request_cache.rb index 754a45c3257..ecc85f847d4 100644 --- a/lib/gitlab/cache/request_cache.rb +++ b/lib/gitlab/cache/request_cache.rb @@ -45,11 +45,13 @@ module Gitlab klass.prepend(extension) end + attr_accessor :request_cache_key_block + def request_cache_key(&block) if block_given? - @request_cache_key = block + self.request_cache_key_block = block else - @request_cache_key + request_cache_key_block end end diff --git a/lib/gitlab/checks/project_moved.rb b/lib/gitlab/checks/project_moved.rb new file mode 100644 index 00000000000..3a1c0a3455e --- /dev/null +++ b/lib/gitlab/checks/project_moved.rb @@ -0,0 +1,65 @@ +module Gitlab + module Checks + class ProjectMoved + REDIRECT_NAMESPACE = "redirect_namespace".freeze + + def initialize(project, user, redirected_path, protocol) + @project = project + @user = user + @redirected_path = redirected_path + @protocol = protocol + end + + def self.fetch_redirect_message(user_id, project_id) + redirect_key = redirect_message_key(user_id, project_id) + + Gitlab::Redis::SharedState.with do |redis| + message = redis.get(redirect_key) + redis.del(redirect_key) + message + end + end + + def add_redirect_message + Gitlab::Redis::SharedState.with do |redis| + key = self.class.redirect_message_key(user.id, project.id) + redis.setex(key, 5.minutes, redirect_message) + end + end + + def redirect_message(rejected: false) + <<~MESSAGE.strip_heredoc + Project '#{redirected_path}' was moved to '#{project.full_path}'. + + Please update your Git remote: + + #{remote_url_message(rejected)} + MESSAGE + end + + def permanent_redirect? + RedirectRoute.permanent.exists?(path: redirected_path) + end + + private + + attr_reader :project, :redirected_path, :protocol, :user + + def self.redirect_message_key(user_id, project_id) + "#{REDIRECT_NAMESPACE}:#{user_id}:#{project_id}" + end + + def remote_url_message(rejected) + if rejected + "git remote set-url origin #{url} and try again." + else + "git remote set-url origin #{url}" + end + end + + def url + protocol == 'ssh' ? project.ssh_url_to_repo : project.http_url_to_repo + end + end + end +end diff --git a/lib/gitlab/ci/charts.rb b/lib/gitlab/ci/charts.rb index 7df7b542d91..525563a97f5 100644 --- a/lib/gitlab/ci/charts.rb +++ b/lib/gitlab/ci/charts.rb @@ -6,7 +6,7 @@ module Gitlab query .group("DATE(#{::Ci::Pipeline.table_name}.created_at)") .count(:created_at) - .transform_keys { |date| date.strftime(@format) } + .transform_keys { |date| date.strftime(@format) } # rubocop:disable Gitlab/ModuleWithInstanceVariables end def interval_step diff --git a/lib/gitlab/ci/config/entry/configurable.rb b/lib/gitlab/ci/config/entry/configurable.rb index 68b6742385a..db47c2f6185 100644 --- a/lib/gitlab/ci/config/entry/configurable.rb +++ b/lib/gitlab/ci/config/entry/configurable.rb @@ -29,15 +29,15 @@ module Gitlab self.class.nodes.each do |key, factory| factory - .value(@config[key]) + .value(config[key]) .with(key: key, parent: self) - @entries[key] = factory.create! + entries[key] = factory.create! end yield if block_given? - @entries.each_value do |entry| + entries.each_value do |entry| entry.compose!(deps) end end @@ -59,13 +59,13 @@ module Gitlab def helpers(*nodes) nodes.each do |symbol| define_method("#{symbol}_defined?") do - @entries[symbol]&.specified? + entries[symbol]&.specified? end define_method("#{symbol}_value") do - return unless @entries[symbol] && @entries[symbol].valid? + return unless entries[symbol] && entries[symbol].valid? - @entries[symbol].value + entries[symbol].value end end end diff --git a/lib/gitlab/ci/config/entry/node.rb b/lib/gitlab/ci/config/entry/node.rb index c868943c42e..1fba0b2db0b 100644 --- a/lib/gitlab/ci/config/entry/node.rb +++ b/lib/gitlab/ci/config/entry/node.rb @@ -90,6 +90,12 @@ module Gitlab def self.aspects @aspects ||= [] end + + private + + def entries + @entries + end end end end diff --git a/lib/gitlab/ci/config/entry/validatable.rb b/lib/gitlab/ci/config/entry/validatable.rb index 5ced778d311..e45787773a8 100644 --- a/lib/gitlab/ci/config/entry/validatable.rb +++ b/lib/gitlab/ci/config/entry/validatable.rb @@ -13,7 +13,7 @@ module Gitlab end def errors - @validator.messages + descendants.flat_map(&:errors) + @validator.messages + descendants.flat_map(&:errors) # rubocop:disable Gitlab/ModuleWithInstanceVariables end class_methods do diff --git a/lib/gitlab/ci/pipeline/chain/base.rb b/lib/gitlab/ci/pipeline/chain/base.rb index 8d82e1b288d..efed19da21c 100644 --- a/lib/gitlab/ci/pipeline/chain/base.rb +++ b/lib/gitlab/ci/pipeline/chain/base.rb @@ -3,14 +3,13 @@ module Gitlab module Pipeline module Chain class Base - attr_reader :pipeline, :project, :current_user + attr_reader :pipeline, :command + + delegate :project, :current_user, to: :command def initialize(pipeline, command) @pipeline = pipeline @command = command - - @project = command.project - @current_user = command.current_user end def perform! diff --git a/lib/gitlab/ci/pipeline/chain/build.rb b/lib/gitlab/ci/pipeline/chain/build.rb index a126dded1ae..70732d26bbd 100644 --- a/lib/gitlab/ci/pipeline/chain/build.rb +++ b/lib/gitlab/ci/pipeline/chain/build.rb @@ -3,20 +3,18 @@ module Gitlab module Pipeline module Chain class Build < Chain::Base - include Chain::Helpers - def perform! @pipeline.assign_attributes( source: @command.source, - project: @project, - ref: ref, - sha: sha, - before_sha: before_sha, - tag: tag_exists?, + project: @command.project, + ref: @command.ref, + sha: @command.sha, + before_sha: @command.before_sha, + tag: @command.tag_exists?, trigger_requests: Array(@command.trigger_request), - user: @current_user, + user: @command.current_user, pipeline_schedule: @command.schedule, - protected: protected_ref? + protected: @command.protected_ref? ) @pipeline.set_config_source @@ -25,32 +23,6 @@ module Gitlab def break? false end - - private - - def ref - @ref ||= Gitlab::Git.ref_name(origin_ref) - end - - def sha - @project.commit(origin_sha || origin_ref).try(:id) - end - - def origin_ref - @command.origin_ref - end - - def origin_sha - @command.checkout_sha || @command.after_sha - end - - def before_sha - @command.checkout_sha || @command.before_sha || Gitlab::Git::BLANK_SHA - end - - def protected_ref? - @project.protected_for?(ref) - end end end end diff --git a/lib/gitlab/ci/pipeline/chain/command.rb b/lib/gitlab/ci/pipeline/chain/command.rb new file mode 100644 index 00000000000..7b19b10e05b --- /dev/null +++ b/lib/gitlab/ci/pipeline/chain/command.rb @@ -0,0 +1,61 @@ +module Gitlab + module Ci + module Pipeline + module Chain + Command = Struct.new( + :source, :project, :current_user, + :origin_ref, :checkout_sha, :after_sha, :before_sha, + :trigger_request, :schedule, + :ignore_skip_ci, :save_incompleted, + :seeds_block + ) do + include Gitlab::Utils::StrongMemoize + + def initialize(**params) + params.each do |key, value| + self[key] = value + end + end + + def branch_exists? + strong_memoize(:is_branch) do + project.repository.branch_exists?(ref) + end + end + + def tag_exists? + strong_memoize(:is_tag) do + project.repository.tag_exists?(ref) + end + end + + def ref + strong_memoize(:ref) do + Gitlab::Git.ref_name(origin_ref) + end + end + + def sha + strong_memoize(:sha) do + project.commit(origin_sha || origin_ref).try(:id) + end + end + + def origin_sha + checkout_sha || after_sha + end + + def before_sha + self[:before_sha] || checkout_sha || Gitlab::Git::BLANK_SHA + end + + def protected_ref? + strong_memoize(:protected_ref) do + project.protected_for?(ref) + end + end + end + end + end + end +end diff --git a/lib/gitlab/ci/pipeline/chain/helpers.rb b/lib/gitlab/ci/pipeline/chain/helpers.rb index 02d81286f21..bf1380a1da9 100644 --- a/lib/gitlab/ci/pipeline/chain/helpers.rb +++ b/lib/gitlab/ci/pipeline/chain/helpers.rb @@ -3,18 +3,6 @@ module Gitlab module Pipeline module Chain module Helpers - def branch_exists? - return @is_branch if defined?(@is_branch) - - @is_branch = project.repository.branch_exists?(pipeline.ref) - end - - def tag_exists? - return @is_tag if defined?(@is_tag) - - @is_tag = project.repository.tag_exists?(pipeline.ref) - end - def error(message) pipeline.errors.add(:base, message) end diff --git a/lib/gitlab/ci/pipeline/chain/validate/abilities.rb b/lib/gitlab/ci/pipeline/chain/validate/abilities.rb index 4913a604079..13c6fedd831 100644 --- a/lib/gitlab/ci/pipeline/chain/validate/abilities.rb +++ b/lib/gitlab/ci/pipeline/chain/validate/abilities.rb @@ -14,7 +14,7 @@ module Gitlab unless allowed_to_trigger_pipeline? if can?(current_user, :create_pipeline, project) - return error("Insufficient permissions for protected ref '#{pipeline.ref}'") + return error("Insufficient permissions for protected ref '#{command.ref}'") else return error('Insufficient permissions to create a new pipeline') end @@ -29,7 +29,7 @@ module Gitlab if current_user allowed_to_create? else # legacy triggers don't have a corresponding user - !project.protected_for?(@pipeline.ref) + !@command.protected_ref? end end @@ -38,10 +38,10 @@ module Gitlab access = Gitlab::UserAccess.new(current_user, project: project) - if branch_exists? - access.can_update_branch?(@pipeline.ref) - elsif tag_exists? - access.can_create_tag?(@pipeline.ref) + if @command.branch_exists? + access.can_update_branch?(@command.ref) + elsif @command.tag_exists? + access.can_create_tag?(@command.ref) else true # Allow it for now and we'll reject when we check ref existence end diff --git a/lib/gitlab/ci/pipeline/chain/validate/repository.rb b/lib/gitlab/ci/pipeline/chain/validate/repository.rb index 70a4cfdbdea..9699c24e5b6 100644 --- a/lib/gitlab/ci/pipeline/chain/validate/repository.rb +++ b/lib/gitlab/ci/pipeline/chain/validate/repository.rb @@ -7,14 +7,11 @@ module Gitlab include Chain::Helpers def perform! - unless branch_exists? || tag_exists? + unless @command.branch_exists? || @command.tag_exists? return error('Reference not found') end - ## TODO, we check commit in the service, that is why - # there is no repository access here. - # - unless pipeline.sha + unless @command.sha return error('Commit not found') end end diff --git a/lib/gitlab/conflict/file_collection.rb b/lib/gitlab/conflict/file_collection.rb index fb28e80ff73..76aee5a3deb 100644 --- a/lib/gitlab/conflict/file_collection.rb +++ b/lib/gitlab/conflict/file_collection.rb @@ -4,11 +4,11 @@ module Gitlab attr_reader :merge_request, :resolver def initialize(merge_request) - source_repo = merge_request.source_project.repository.raw our_commit = merge_request.source_branch_head.raw their_commit = merge_request.target_branch_head.raw target_repo = merge_request.target_project.repository.raw - @resolver = Gitlab::Git::Conflict::Resolver.new(source_repo, our_commit, target_repo, their_commit) + @source_repo = merge_request.source_project.repository.raw + @resolver = Gitlab::Git::Conflict::Resolver.new(target_repo, our_commit.id, their_commit.id) @merge_request = merge_request end @@ -18,7 +18,9 @@ module Gitlab target_branch: merge_request.target_branch, commit_message: commit_message || default_commit_message } - resolver.resolve_conflicts(user, files, args) + resolver.resolve_conflicts(@source_repo, user, files, args) + ensure + @merge_request.clear_memoized_shas end def files diff --git a/lib/gitlab/current_settings.rb b/lib/gitlab/current_settings.rb index 642f0944354..91fd9cc7631 100644 --- a/lib/gitlab/current_settings.rb +++ b/lib/gitlab/current_settings.rb @@ -53,7 +53,7 @@ module Gitlab end def in_memory_application_settings - @in_memory_application_settings ||= ::ApplicationSetting.new(::ApplicationSetting.defaults) + @in_memory_application_settings ||= ::ApplicationSetting.new(::ApplicationSetting.defaults) # rubocop:disable Gitlab/ModuleWithInstanceVariables rescue ActiveRecord::StatementInvalid, ActiveRecord::UnknownAttributeError # In case migrations the application_settings table is not created yet, # we fallback to a simple OpenStruct diff --git a/lib/gitlab/cycle_analytics/base_event_fetcher.rb b/lib/gitlab/cycle_analytics/base_event_fetcher.rb index ab115afcaa5..e3e3767cc75 100644 --- a/lib/gitlab/cycle_analytics/base_event_fetcher.rb +++ b/lib/gitlab/cycle_analytics/base_event_fetcher.rb @@ -56,7 +56,9 @@ module Gitlab end def allowed_ids - nil + @allowed_ids ||= allowed_ids_finder_class + .new(@options[:current_user], project_id: @project.id) + .execute.where(id: event_result_ids).pluck(:id) end def event_result_ids diff --git a/lib/gitlab/cycle_analytics/base_query.rb b/lib/gitlab/cycle_analytics/base_query.rb index 58729d3ced8..dcbdf9a64b0 100644 --- a/lib/gitlab/cycle_analytics/base_query.rb +++ b/lib/gitlab/cycle_analytics/base_query.rb @@ -14,9 +14,9 @@ module Gitlab def stage_query query = mr_closing_issues_table.join(issue_table).on(issue_table[:id].eq(mr_closing_issues_table[:issue_id])) .join(issue_metrics_table).on(issue_table[:id].eq(issue_metrics_table[:issue_id])) - .where(issue_table[:project_id].eq(@project.id)) + .where(issue_table[:project_id].eq(@project.id)) # rubocop:disable Gitlab/ModuleWithInstanceVariables .where(issue_table[:deleted_at].eq(nil)) - .where(issue_table[:created_at].gteq(@options[:from])) + .where(issue_table[:created_at].gteq(@options[:from])) # rubocop:disable Gitlab/ModuleWithInstanceVariables # Load merge_requests query = query.join(mr_table, Arel::Nodes::OuterJoin) diff --git a/lib/gitlab/cycle_analytics/code_event_fetcher.rb b/lib/gitlab/cycle_analytics/code_event_fetcher.rb index d5bf6149749..06357c9b377 100644 --- a/lib/gitlab/cycle_analytics/code_event_fetcher.rb +++ b/lib/gitlab/cycle_analytics/code_event_fetcher.rb @@ -1,8 +1,6 @@ module Gitlab module CycleAnalytics class CodeEventFetcher < BaseEventFetcher - include MergeRequestAllowed - def initialize(*args) @projections = [mr_table[:title], mr_table[:iid], @@ -20,6 +18,10 @@ module Gitlab def serialize(event) AnalyticsMergeRequestSerializer.new(project: @project).represent(event) end + + def allowed_ids_finder_class + MergeRequestsFinder + end end end end diff --git a/lib/gitlab/cycle_analytics/issue_allowed.rb b/lib/gitlab/cycle_analytics/issue_allowed.rb deleted file mode 100644 index a7652a70641..00000000000 --- a/lib/gitlab/cycle_analytics/issue_allowed.rb +++ /dev/null @@ -1,9 +0,0 @@ -module Gitlab - module CycleAnalytics - module IssueAllowed - def allowed_ids - @allowed_ids ||= IssuesFinder.new(@options[:current_user], project_id: @project.id).execute.where(id: event_result_ids).pluck(:id) - end - end - end -end diff --git a/lib/gitlab/cycle_analytics/issue_event_fetcher.rb b/lib/gitlab/cycle_analytics/issue_event_fetcher.rb index 3df9cbdcfce..1754f91dccb 100644 --- a/lib/gitlab/cycle_analytics/issue_event_fetcher.rb +++ b/lib/gitlab/cycle_analytics/issue_event_fetcher.rb @@ -1,8 +1,6 @@ module Gitlab module CycleAnalytics class IssueEventFetcher < BaseEventFetcher - include IssueAllowed - def initialize(*args) @projections = [issue_table[:title], issue_table[:iid], @@ -18,6 +16,10 @@ module Gitlab def serialize(event) AnalyticsIssueSerializer.new(project: @project).represent(event) end + + def allowed_ids_finder_class + IssuesFinder + end end end end diff --git a/lib/gitlab/cycle_analytics/merge_request_allowed.rb b/lib/gitlab/cycle_analytics/merge_request_allowed.rb deleted file mode 100644 index 28f6db44759..00000000000 --- a/lib/gitlab/cycle_analytics/merge_request_allowed.rb +++ /dev/null @@ -1,9 +0,0 @@ -module Gitlab - module CycleAnalytics - module MergeRequestAllowed - def allowed_ids - @allowed_ids ||= MergeRequestsFinder.new(@options[:current_user], project_id: @project.id).execute.where(id: event_result_ids).pluck(:id) - end - end - end -end diff --git a/lib/gitlab/cycle_analytics/plan_event_fetcher.rb b/lib/gitlab/cycle_analytics/plan_event_fetcher.rb index 9230894877f..086203b9ccc 100644 --- a/lib/gitlab/cycle_analytics/plan_event_fetcher.rb +++ b/lib/gitlab/cycle_analytics/plan_event_fetcher.rb @@ -18,6 +18,10 @@ module Gitlab private + def allowed_ids + nil + end + def merge_request_diff_commits @merge_request_diff_commits ||= MergeRequestDiffCommit diff --git a/lib/gitlab/cycle_analytics/production_helper.rb b/lib/gitlab/cycle_analytics/production_helper.rb index d693443bfa4..7a889b3877f 100644 --- a/lib/gitlab/cycle_analytics/production_helper.rb +++ b/lib/gitlab/cycle_analytics/production_helper.rb @@ -2,7 +2,9 @@ module Gitlab module CycleAnalytics module ProductionHelper def stage_query - super.where(mr_metrics_table[:first_deployed_to_production_at].gteq(@options[:from])) + super + .where(mr_metrics_table[:first_deployed_to_production_at] + .gteq(@options[:from])) # rubocop:disable Gitlab/ModuleWithInstanceVariables end end end diff --git a/lib/gitlab/cycle_analytics/review_event_fetcher.rb b/lib/gitlab/cycle_analytics/review_event_fetcher.rb index 4c7b3f4467f..dada819a2a8 100644 --- a/lib/gitlab/cycle_analytics/review_event_fetcher.rb +++ b/lib/gitlab/cycle_analytics/review_event_fetcher.rb @@ -1,8 +1,6 @@ module Gitlab module CycleAnalytics class ReviewEventFetcher < BaseEventFetcher - include MergeRequestAllowed - def initialize(*args) @projections = [mr_table[:title], mr_table[:iid], @@ -14,9 +12,15 @@ module Gitlab super(*args) end + private + def serialize(event) AnalyticsMergeRequestSerializer.new(project: @project).represent(event) end + + def allowed_ids_finder_class + MergeRequestsFinder + end end end end diff --git a/lib/gitlab/cycle_analytics/staging_event_fetcher.rb b/lib/gitlab/cycle_analytics/staging_event_fetcher.rb index 36c0260dbfe..2f014153ca5 100644 --- a/lib/gitlab/cycle_analytics/staging_event_fetcher.rb +++ b/lib/gitlab/cycle_analytics/staging_event_fetcher.rb @@ -22,6 +22,10 @@ module Gitlab private + def allowed_ids + nil + end + def serialize(event) AnalyticsBuildSerializer.new.represent(event['build']) 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 index 7e492938eac..fd4a8832ec2 100644 --- a/lib/gitlab/database/rename_reserved_paths_migration/v1/migration_classes.rb +++ b/lib/gitlab/database/rename_reserved_paths_migration/v1/migration_classes.rb @@ -6,7 +6,7 @@ module Gitlab module Routable def full_path if route && route.path.present? - @full_path ||= route.path + @full_path ||= route.path # rubocop:disable Gitlab/ModuleWithInstanceVariables else update_route if persisted? @@ -30,7 +30,7 @@ module Gitlab def prepare_route route || build_route(source: self) route.path = build_full_path - @full_path = nil + @full_path = nil # rubocop:disable Gitlab/ModuleWithInstanceVariables end end diff --git a/lib/gitlab/ee_compat_check.rb b/lib/gitlab/ee_compat_check.rb index 4a9d3e52fae..37face8e7d0 100644 --- a/lib/gitlab/ee_compat_check.rb +++ b/lib/gitlab/ee_compat_check.rb @@ -280,7 +280,7 @@ module Gitlab The `#{branch}` branch applies cleanly to EE/master! Much ❤️! For more information, see - https://docs.gitlab.com/ce/development/limit_ee_conflicts.html#check-the-rake-ee_compat_check-in-your-merge-requests + https://docs.gitlab.com/ce/development/automatic_ce_ee_merge.html #{THANKS_FOR_READING_BANNER} } end @@ -357,7 +357,7 @@ module Gitlab Once this is done, you can retry this failed build, and it should pass. Stay 💪 ! For more information, see - https://docs.gitlab.com/ce/development/limit_ee_conflicts.html#check-the-rake-ee_compat_check-in-your-merge-requests + https://docs.gitlab.com/ce/development/automatic_ce_ee_merge.html #{THANKS_FOR_READING_BANNER} } end @@ -378,7 +378,7 @@ module Gitlab retry this build. Stay 💪 ! For more information, see - https://docs.gitlab.com/ce/development/limit_ee_conflicts.html#check-the-rake-ee_compat_check-in-your-merge-requests + https://docs.gitlab.com/ce/development/automatic_ce_ee_merge.html #{THANKS_FOR_READING_BANNER} } end diff --git a/lib/gitlab/email/handler/create_merge_request_handler.rb b/lib/gitlab/email/handler/create_merge_request_handler.rb index c63666b98c1..e2f7c1d0257 100644 --- a/lib/gitlab/email/handler/create_merge_request_handler.rb +++ b/lib/gitlab/email/handler/create_merge_request_handler.rb @@ -55,11 +55,13 @@ module Gitlab end def merge_request_params - { + params = { source_project_id: project.id, source_branch: mail.subject, target_project_id: project.id } + params[:description] = message if message.present? + params end end end diff --git a/lib/gitlab/emoji.rb b/lib/gitlab/emoji.rb index e3e36b35ce9..89cf659bce4 100644 --- a/lib/gitlab/emoji.rb +++ b/lib/gitlab/emoji.rb @@ -31,8 +31,7 @@ module Gitlab end def emoji_unicode_version(name) - @emoji_unicode_versions_by_name ||= JSON.parse(File.read(Rails.root.join('fixtures', 'emojis', 'emoji-unicode-version-map.json'))) - @emoji_unicode_versions_by_name[name] + emoji_unicode_versions_by_name[name] end def normalize_emoji_name(name) @@ -56,5 +55,12 @@ module Gitlab ActionController::Base.helpers.content_tag('gl-emoji', emoji_info['moji'], title: emoji_info['description'], data: data) end + + private + + def emoji_unicode_versions_by_name + @emoji_unicode_versions_by_name ||= + JSON.parse(File.read(Rails.root.join('fixtures', 'emojis', 'emoji-unicode-version-map.json'))) + end end end diff --git a/lib/gitlab/git/conflict/resolver.rb b/lib/gitlab/git/conflict/resolver.rb index de8cce41b6d..03e5c0fcd6f 100644 --- a/lib/gitlab/git/conflict/resolver.rb +++ b/lib/gitlab/git/conflict/resolver.rb @@ -5,38 +5,31 @@ module Gitlab ConflictSideMissing = Class.new(StandardError) ResolutionError = Class.new(StandardError) - def initialize(repository, our_commit, target_repository, their_commit) - @repository = repository - @our_commit = our_commit.rugged_commit + def initialize(target_repository, our_commit_oid, their_commit_oid) @target_repository = target_repository - @their_commit = their_commit.rugged_commit + @our_commit_oid = our_commit_oid + @their_commit_oid = their_commit_oid end def conflicts @conflicts ||= begin - target_index = @target_repository.rugged.merge_commits(@our_commit, @their_commit) + target_index = @target_repository.rugged.merge_commits(@our_commit_oid, @their_commit_oid) # We don't need to do `with_repo_branch_commit` here, because the target # project always fetches source refs when creating merge request diffs. - target_index.conflicts.map do |conflict| - raise ConflictSideMissing unless conflict[:theirs] && conflict[:ours] - - Gitlab::Git::Conflict::File.new( - @target_repository, - @our_commit.oid, - conflict, - target_index.merge_file(conflict[:ours][:path])[:data] - ) - end + conflict_files(@target_repository, target_index) end end - def resolve_conflicts(user, files, source_branch:, target_branch:, commit_message:) - @repository.with_repo_branch_commit(@target_repository, target_branch) do + def resolve_conflicts(source_repository, user, files, source_branch:, target_branch:, commit_message:) + source_repository.with_repo_branch_commit(@target_repository, target_branch) do + index = source_repository.rugged.merge_commits(@our_commit_oid, @their_commit_oid) + conflicts = conflict_files(source_repository, index) + files.each do |file_params| - conflict_file = conflict_for_path(file_params[:old_path], file_params[:new_path]) + conflict_file = conflict_for_path(conflicts, file_params[:old_path], file_params[:new_path]) - write_resolved_file_to_index(conflict_file, file_params) + write_resolved_file_to_index(source_repository, index, conflict_file, file_params) end unless index.conflicts.empty? @@ -47,14 +40,14 @@ module Gitlab commit_params = { message: commit_message, - parents: [@our_commit, @their_commit].map(&:oid) + parents: [@our_commit_oid, @their_commit_oid] } - @repository.commit_index(user, source_branch, index, commit_params) + source_repository.commit_index(user, source_branch, index, commit_params) end end - def conflict_for_path(old_path, new_path) + def conflict_for_path(conflicts, old_path, new_path) conflicts.find do |conflict| conflict.their_path == old_path && conflict.our_path == new_path end @@ -62,15 +55,20 @@ module Gitlab private - # We can only write when getting the merge index from the source - # project, because we will write to that project. We don't use this all - # the time because this fetches a ref into the source project, which - # isn't needed for reading. - def index - @index ||= @repository.rugged.merge_commits(@our_commit, @their_commit) + def conflict_files(repository, index) + index.conflicts.map do |conflict| + raise ConflictSideMissing unless conflict[:theirs] && conflict[:ours] + + Gitlab::Git::Conflict::File.new( + repository, + @our_commit_oid, + conflict, + index.merge_file(conflict[:ours][:path])[:data] + ) + end end - def write_resolved_file_to_index(file, params) + def write_resolved_file_to_index(repository, index, file, params) if params[:sections] resolved_lines = file.resolve_lines(params[:sections]) new_file = resolved_lines.map { |line| line[:full_line] }.join("\n") @@ -82,7 +80,8 @@ module Gitlab our_path = file.our_path - index.add(path: our_path, oid: @repository.rugged.write(new_file, :blob), mode: file.our_mode) + oid = repository.rugged.write(new_file, :blob) + index.add(path: our_path, oid: oid, mode: file.our_mode) index.conflict_remove(our_path) end end diff --git a/lib/gitlab/git/gitlab_projects.rb b/lib/gitlab/git/gitlab_projects.rb new file mode 100644 index 00000000000..d948d7895ed --- /dev/null +++ b/lib/gitlab/git/gitlab_projects.rb @@ -0,0 +1,258 @@ +module Gitlab + module Git + class GitlabProjects + include Gitlab::Git::Popen + + # Absolute path to directory where repositories are stored. + # Example: /home/git/repositories + attr_reader :shard_path + + # Relative path is a directory name for repository with .git at the end. + # Example: gitlab-org/gitlab-test.git + attr_reader :repository_relative_path + + # Absolute path to the repository. + # Example: /home/git/repositorities/gitlab-org/gitlab-test.git + attr_reader :repository_absolute_path + + # This is the path at which the gitlab-shell hooks directory can be found. + # It's essential for integration between git and GitLab proper. All new + # repositories should have their hooks directory symlinked here. + attr_reader :global_hooks_path + + attr_reader :logger + + def initialize(shard_path, repository_relative_path, global_hooks_path:, logger:) + @shard_path = shard_path + @repository_relative_path = repository_relative_path + + @logger = logger + @global_hooks_path = global_hooks_path + @repository_absolute_path = File.join(shard_path, repository_relative_path) + @output = StringIO.new + end + + def output + io = @output.dup + io.rewind + io.read + end + + def rm_project + logger.info "Removing repository <#{repository_absolute_path}>." + FileUtils.rm_rf(repository_absolute_path) + end + + # Move repository from one directory to another + # + # Example: gitlab/gitlab-ci.git -> randx/six.git + # + # Won't work if target namespace directory does not exist + # + def mv_project(new_path) + new_absolute_path = File.join(shard_path, new_path) + + # verify that the source repo exists + unless File.exist?(repository_absolute_path) + logger.error "mv-project failed: source path <#{repository_absolute_path}> does not exist." + return false + end + + # ...and that the target repo does not exist + if File.exist?(new_absolute_path) + logger.error "mv-project failed: destination path <#{new_absolute_path}> already exists." + return false + end + + logger.info "Moving repository from <#{repository_absolute_path}> to <#{new_absolute_path}>." + FileUtils.mv(repository_absolute_path, new_absolute_path) + end + + # Import project via git clone --bare + # URL must be publicly cloneable + def import_project(source, timeout) + # Skip import if repo already exists + return false if File.exist?(repository_absolute_path) + + masked_source = mask_password_in_url(source) + + logger.info "Importing project from <#{masked_source}> to <#{repository_absolute_path}>." + cmd = %W(git clone --bare -- #{source} #{repository_absolute_path}) + + success = run_with_timeout(cmd, timeout, nil) + + unless success + logger.error("Importing project from <#{masked_source}> to <#{repository_absolute_path}> failed.") + FileUtils.rm_rf(repository_absolute_path) + return false + end + + Gitlab::Git::Repository.create_hooks(repository_absolute_path, global_hooks_path) + + # The project was imported successfully. + # Remove the origin URL since it may contain password. + remove_origin_in_repo + + true + end + + def fork_repository(new_shard_path, new_repository_relative_path) + from_path = repository_absolute_path + to_path = File.join(new_shard_path, new_repository_relative_path) + + # The repository cannot already exist + if File.exist?(to_path) + logger.error "fork-repository failed: destination repository <#{to_path}> already exists." + return false + end + + # Ensure the namepsace / hashed storage directory exists + FileUtils.mkdir_p(File.dirname(to_path), mode: 0770) + + logger.info "Forking repository from <#{from_path}> to <#{to_path}>." + cmd = %W(git clone --bare --no-local -- #{from_path} #{to_path}) + + run(cmd, nil) && Gitlab::Git::Repository.create_hooks(to_path, global_hooks_path) + end + + def fetch_remote(name, timeout, force:, tags:, ssh_key: nil, known_hosts: nil) + tags_option = tags ? '--tags' : '--no-tags' + + logger.info "Fetching remote #{name} for repository #{repository_absolute_path}." + cmd = %W(git fetch #{name} --prune --quiet) + cmd << '--force' if force + cmd << tags_option + + setup_ssh_auth(ssh_key, known_hosts) do |env| + success = run_with_timeout(cmd, timeout, repository_absolute_path, env) + + unless success + logger.error "Fetching remote #{name} for repository #{repository_absolute_path} failed." + end + + success + end + end + + def push_branches(remote_name, timeout, force, branch_names) + logger.info "Pushing branches from #{repository_absolute_path} to remote #{remote_name}: #{branch_names}" + cmd = %w(git push) + cmd << '--force' if force + cmd += %W(-- #{remote_name}).concat(branch_names) + + success = run_with_timeout(cmd, timeout, repository_absolute_path) + + unless success + logger.error("Pushing branches to remote #{remote_name} failed.") + end + + success + end + + def delete_remote_branches(remote_name, branch_names) + branches = branch_names.map { |branch_name| ":#{branch_name}" } + + logger.info "Pushing deleted branches from #{repository_absolute_path} to remote #{remote_name}: #{branch_names}" + cmd = %W(git push -- #{remote_name}).concat(branches) + + success = run(cmd, repository_absolute_path) + + unless success + logger.error("Pushing deleted branches to remote #{remote_name} failed.") + end + + success + end + + protected + + def run(*args) + output, exitstatus = popen(*args) + @output << output + + exitstatus&.zero? + end + + def run_with_timeout(*args) + output, exitstatus = popen_with_timeout(*args) + @output << output + + exitstatus&.zero? + rescue Timeout::Error + @output.puts('Timed out') + + false + end + + def mask_password_in_url(url) + result = URI(url) + result.password = "*****" unless result.password.nil? + result.user = "*****" unless result.user.nil? # it's needed for oauth access_token + result + rescue + url + end + + def remove_origin_in_repo + cmd = %w(git remote rm origin) + run(cmd, repository_absolute_path) + end + + # Builds a small shell script that can be used to execute SSH with a set of + # custom options. + # + # Options are expanded as `'-oKey="Value"'`, so SSH will correctly interpret + # paths with spaces in them. We trust the user not to embed single or double + # quotes in the key or value. + def custom_ssh_script(options = {}) + args = options.map { |k, v| %Q{'-o#{k}="#{v}"'} }.join(' ') + + [ + "#!/bin/sh", + "exec ssh #{args} \"$@\"" + ].join("\n") + end + + # Known hosts data and private keys can be passed to gitlab-shell in the + # environment. If present, this method puts them into temporary files, writes + # a script that can substitute as `ssh`, setting the options to respect those + # files, and yields: { "GIT_SSH" => "/tmp/myScript" } + def setup_ssh_auth(key, known_hosts) + options = {} + + if key + key_file = Tempfile.new('gitlab-shell-key-file') + key_file.chmod(0o400) + key_file.write(key) + key_file.close + + options['IdentityFile'] = key_file.path + options['IdentitiesOnly'] = 'yes' + end + + if known_hosts + known_hosts_file = Tempfile.new('gitlab-shell-known-hosts') + known_hosts_file.chmod(0o400) + known_hosts_file.write(known_hosts) + known_hosts_file.close + + options['StrictHostKeyChecking'] = 'yes' + options['UserKnownHostsFile'] = known_hosts_file.path + end + + return yield({}) if options.empty? + + script = Tempfile.new('gitlab-shell-ssh-wrapper') + script.chmod(0o755) + script.write(custom_ssh_script(options)) + script.close + + yield('GIT_SSH' => script.path) + ensure + key_file&.close! + known_hosts_file&.close! + script&.close! + end + end + end +end diff --git a/lib/gitlab/git/operation_service.rb b/lib/gitlab/git/operation_service.rb index 7e8fe173056..ef5bdbaf819 100644 --- a/lib/gitlab/git/operation_service.rb +++ b/lib/gitlab/git/operation_service.rb @@ -126,7 +126,7 @@ module Gitlab oldrev = branch.target - if oldrev == repository.rugged.merge_base(newrev, branch.target) + if oldrev == repository.merge_base(newrev, branch.target) oldrev else raise Gitlab::Git::CommitError.new('Branch diverged') diff --git a/lib/gitlab/git/popen.rb b/lib/gitlab/git/popen.rb index d41fe78daa1..1ccca13ce2f 100644 --- a/lib/gitlab/git/popen.rb +++ b/lib/gitlab/git/popen.rb @@ -16,8 +16,8 @@ module Gitlab vars['PWD'] = path options = { chdir: path } - @cmd_output = "" - @cmd_status = 0 + cmd_output = "" + cmd_status = 0 Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr| yield(stdin) if block_given? stdin.close @@ -25,14 +25,14 @@ module Gitlab if lazy_block return lazy_block.call(stdout.lazy) else - @cmd_output << stdout.read + cmd_output << stdout.read end - @cmd_output << stderr.read - @cmd_status = wait_thr.value.exitstatus + cmd_output << stderr.read + cmd_status = wait_thr.value.exitstatus end - [@cmd_output, @cmd_status] + [cmd_output, cmd_status] end def popen_with_timeout(cmd, timeout, path, vars = {}) diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb index 91dd2fbbdbc..848a782446a 100644 --- a/lib/gitlab/git/repository.rb +++ b/lib/gitlab/git/repository.rb @@ -20,6 +20,7 @@ module Gitlab SEARCH_CONTEXT_LINES = 3 REBASE_WORKTREE_PREFIX = 'rebase'.freeze SQUASH_WORKTREE_PREFIX = 'squash'.freeze + GITALY_INTERNAL_URL = 'ssh://gitaly/internal.git'.freeze NoRepository = Class.new(StandardError) InvalidBlobName = Class.new(StandardError) @@ -38,10 +39,31 @@ module Gitlab repo = Rugged::Repository.init_at(repo_path, bare) repo.close - if symlink_hooks_to.present? - hooks_path = File.join(repo_path, 'hooks') - FileUtils.rm_rf(hooks_path) - FileUtils.ln_s(symlink_hooks_to, hooks_path) + create_hooks(repo_path, symlink_hooks_to) if symlink_hooks_to.present? + + true + end + + def create_hooks(repo_path, global_hooks_path) + local_hooks_path = File.join(repo_path, 'hooks') + real_local_hooks_path = :not_found + + begin + real_local_hooks_path = File.realpath(local_hooks_path) + rescue Errno::ENOENT + # real_local_hooks_path == :not_found + end + + # Do nothing if hooks already exist + unless real_local_hooks_path == File.realpath(global_hooks_path) + # Move the existing hooks somewhere safe + FileUtils.mv( + local_hooks_path, + "#{local_hooks_path}.old.#{Time.now.to_i}" + ) if File.exist?(local_hooks_path) + + # Create the hooks symlink + FileUtils.ln_sf(global_hooks_path, local_hooks_path) end true @@ -516,8 +538,15 @@ module Gitlab # Returns the SHA of the most recent common ancestor of +from+ and +to+ def merge_base_commit(from, to) - rugged.merge_base(from, to) + gitaly_migrate(:merge_base) do |is_enabled| + if is_enabled + gitaly_repository_client.find_merge_base(from, to) + else + rugged.merge_base(from, to) + end + end end + alias_method :merge_base, :merge_base_commit # Gitaly note: JV: check gitlab-ee before removing this method. def rugged_is_ancestor?(ancestor_id, descendant_id) @@ -887,8 +916,11 @@ module Gitlab end end - def add_remote(remote_name, url) + # If `mirror_refmap` is present the remote is set as mirror with that mapping + def add_remote(remote_name, url, mirror_refmap: nil) rugged.remotes.create(remote_name, url) + + set_remote_as_mirror(remote_name, refmap: mirror_refmap) if mirror_refmap rescue Rugged::ConfigError remote_update(remote_name, url: url) end @@ -1069,12 +1101,17 @@ module Gitlab end end - def write_ref(ref_path, ref) + def write_ref(ref_path, ref, force: false) raise ArgumentError, "invalid ref_path #{ref_path.inspect}" if ref_path.include?(' ') raise ArgumentError, "invalid ref #{ref.inspect}" if ref.include?("\x00") - input = "update #{ref_path}\x00#{ref}\x00\x00" - run_git!(%w[update-ref --stdin -z]) { |stdin| stdin.write(input) } + ref = "refs/heads/#{ref}" unless ref.start_with?("refs") || ref =~ /\A[a-f0-9]+\z/i + + rugged.references.create(ref_path, ref, force: force) + rescue Rugged::ReferenceError => ex + raise GitError, "could not create ref #{ref_path}: #{ex}" + rescue Rugged::OSError => ex + raise GitError, "could not create ref #{ref_path}: #{ex}" end def fetch_ref(source_repository, source_ref:, target_ref:) @@ -1128,12 +1165,24 @@ module Gitlab !has_visible_content? end - # Like all public `Gitlab::Git::Repository` methods, this method is part - # of `Repository`'s interface through `method_missing`. - # `Repository` has its own `fetch_remote` which uses `gitlab-shell` and - # takes some extra attributes, so we qualify this method name to prevent confusion. - def fetch_remote_without_shell(remote = 'origin') - run_git(['fetch', remote]).last.zero? + def fetch_repository_as_mirror(repository) + remote_name = "tmp-#{SecureRandom.hex}" + + # Notice that this feature flag is not for `fetch_repository_as_mirror` + # as a whole but for the fetching mechanism (file path or gitaly-ssh). + url, env = gitaly_migrate(:fetch_internal) do |is_enabled| + if is_enabled + repository = RemoteRepository.new(repository) unless repository.is_a?(RemoteRepository) + [GITALY_INTERNAL_URL, repository.fetch_env] + else + [repository.path, nil] + end + end + + add_remote(remote_name, url, mirror_refmap: :all_refs) + fetch_remote(remote_name, env: env) + ensure + remove_remote(remote_name) end def blob_at(sha, path) @@ -1160,9 +1209,15 @@ module Gitlab end def fsck - output, status = run_git(%W[--git-dir=#{path} fsck], nice: true) + gitaly_migrate(:git_fsck) do |is_enabled| + msg, status = if is_enabled + gitaly_fsck + else + shell_fsck + end - raise GitError.new("Could not fsck repository:\n#{output}") unless status.zero? + raise GitError.new("Could not fsck repository: #{msg}") unless status.zero? + end end def rebase(user, rebase_id, branch:, branch_sha:, remote_repository:, remote_branch:) @@ -1310,6 +1365,14 @@ module Gitlab File.write(File.join(worktree_info_path, 'sparse-checkout'), files) end + def gitaly_fsck + gitaly_repository_client.fsck + end + + def shell_fsck + run_git(%W[--git-dir=#{path} fsck], nice: true) + end + def rugged_fetch_source_branch(source_repository, source_branch, local_ref) with_repo_branch_commit(source_repository, source_branch) do |commit| if commit @@ -1837,7 +1900,7 @@ module Gitlab end def gitaly_fetch_ref(source_repository, source_ref:, target_ref:) - args = %W(fetch --no-tags -f ssh://gitaly/internal.git #{source_ref}:#{target_ref}) + args = %W(fetch --no-tags -f #{GITALY_INTERNAL_URL} #{source_ref}:#{target_ref}) run_git(args, env: source_repository.fetch_env) end @@ -1857,6 +1920,10 @@ module Gitlab rescue Rugged::ReferenceError raise ArgumentError, 'Invalid merge source' end + + def fetch_remote(remote_name = 'origin', env: nil) + run_git(['fetch', remote_name], env: env).last.zero? + end end end end diff --git a/lib/gitlab/git/repository_mirroring.rb b/lib/gitlab/git/repository_mirroring.rb index 392bef69e80..effb1f0ca19 100644 --- a/lib/gitlab/git/repository_mirroring.rb +++ b/lib/gitlab/git/repository_mirroring.rb @@ -17,33 +17,6 @@ module Gitlab rugged.config["remote.#{remote_name}.prune"] = true end - def set_remote_refmap(remote_name, refmap) - Array(refmap).each_with_index do |refspec, i| - refspec = REFMAPS[refspec] || refspec - - # We need multiple `fetch` entries, but Rugged only allows replacing a config, not adding to it. - # To make sure we start from scratch, we set the first using rugged, and use `git` for any others - if i == 0 - rugged.config["remote.#{remote_name}.fetch"] = refspec - else - run_git(%W[config --add remote.#{remote_name}.fetch #{refspec}]) - end - end - end - - # Like all_refs public `Gitlab::Git::Repository` methods, this method is part - # of `Repository`'s interface through `method_missing`. - # `Repository` has its own `fetch_as_mirror` which uses `gitlab-shell` and - # takes some extra attributes, so we qualify this method name to prevent confusion. - def fetch_as_mirror_without_shell(url) - remote_name = "tmp-#{SecureRandom.hex}" - add_remote(remote_name, url) - set_remote_as_mirror(remote_name) - fetch_remote_without_shell(remote_name) - ensure - remove_remote(remote_name) if remote_name - end - def remote_tags(remote) # Each line has this format: "dc872e9fa6963f8f03da6c8f6f264d0845d6b092\trefs/tags/v1.10.0\n" # We want to convert it to: [{ 'v1.10.0' => 'dc872e9fa6963f8f03da6c8f6f264d0845d6b092' }, ...] @@ -85,6 +58,20 @@ module Gitlab private + def set_remote_refmap(remote_name, refmap) + Array(refmap).each_with_index do |refspec, i| + refspec = REFMAPS[refspec] || refspec + + # We need multiple `fetch` entries, but Rugged only allows replacing a config, not adding to it. + # To make sure we start from scratch, we set the first using rugged, and use `git` for any others + if i == 0 + rugged.config["remote.#{remote_name}.fetch"] = refspec + else + run_git(%W[config --add remote.#{remote_name}.fetch #{refspec}]) + end + end + end + def list_remote_tags(remote) tag_list, exit_code, error = nil cmd = %W(#{Gitlab.config.git.bin_path} --git-dir=#{path} ls-remote --tags #{remote}) diff --git a/lib/gitlab/git/storage/checker.rb b/lib/gitlab/git/storage/checker.rb new file mode 100644 index 00000000000..d3c37f82101 --- /dev/null +++ b/lib/gitlab/git/storage/checker.rb @@ -0,0 +1,120 @@ +module Gitlab + module Git + module Storage + class Checker + include CircuitBreakerSettings + + attr_reader :storage_path, :storage, :hostname, :logger + METRICS_MUTEX = Mutex.new + STORAGE_TIMING_BUCKETS = [0.1, 0.15, 0.25, 0.33, 0.5, 1, 1.5, 2.5, 5, 10, 15].freeze + + def self.check_all(logger = Rails.logger) + threads = Gitlab.config.repositories.storages.keys.map do |storage_name| + Thread.new do + Thread.current[:result] = new(storage_name, logger).check_with_lease + end + end + + threads.map do |thread| + thread.join + thread[:result] + end + end + + def self.check_histogram + @check_histogram ||= + METRICS_MUTEX.synchronize do + @check_histogram || Gitlab::Metrics.histogram(:circuitbreaker_storage_check_duration_seconds, + 'Storage check time in seconds', + {}, + STORAGE_TIMING_BUCKETS + ) + end + end + + def initialize(storage, logger = Rails.logger) + @storage = storage + config = Gitlab.config.repositories.storages[@storage] + @storage_path = config['path'] + @logger = logger + + @hostname = Gitlab::Environment.hostname + end + + def check_with_lease + lease_key = "storage_check:#{cache_key}" + lease = Gitlab::ExclusiveLease.new(lease_key, timeout: storage_timeout) + result = { storage: storage, success: nil } + + if uuid = lease.try_obtain + result[:success] = check + + Gitlab::ExclusiveLease.cancel(lease_key, uuid) + else + logger.warn("#{hostname}: #{storage}: Skipping check, previous check still running") + end + + result + end + + def check + if perform_access_check + track_storage_accessible + true + else + track_storage_inaccessible + logger.error("#{hostname}: #{storage}: Not accessible.") + false + end + end + + private + + def perform_access_check + start_time = Gitlab::Metrics::System.monotonic_time + + Gitlab::Git::Storage::ForkedStorageCheck.storage_available?(storage_path, storage_timeout, access_retries) + ensure + execution_time = Gitlab::Metrics::System.monotonic_time - start_time + self.class.check_histogram.observe({ storage: storage }, execution_time) + end + + def track_storage_inaccessible + first_failure = current_failure_info.first_failure || Time.now + last_failure = Time.now + + Gitlab::Git::Storage.redis.with do |redis| + redis.pipelined do + redis.hset(cache_key, :first_failure, first_failure.to_i) + redis.hset(cache_key, :last_failure, last_failure.to_i) + redis.hincrby(cache_key, :failure_count, 1) + redis.expire(cache_key, failure_reset_time) + maintain_known_keys(redis) + end + end + end + + def track_storage_accessible + Gitlab::Git::Storage.redis.with do |redis| + redis.pipelined do + redis.hset(cache_key, :first_failure, nil) + redis.hset(cache_key, :last_failure, nil) + redis.hset(cache_key, :failure_count, 0) + maintain_known_keys(redis) + end + end + end + + def maintain_known_keys(redis) + expire_time = Time.now.to_i + failure_reset_time + redis.zadd(Gitlab::Git::Storage::REDIS_KNOWN_KEYS, expire_time, cache_key) + redis.zremrangebyscore(Gitlab::Git::Storage::REDIS_KNOWN_KEYS, '-inf', Time.now.to_i) + end + + def current_failure_info + FailureInfo.load(cache_key) + end + end + end + end +end diff --git a/lib/gitlab/git/storage/circuit_breaker.rb b/lib/gitlab/git/storage/circuit_breaker.rb index 4328c0ea29b..898bb1b65be 100644 --- a/lib/gitlab/git/storage/circuit_breaker.rb +++ b/lib/gitlab/git/storage/circuit_breaker.rb @@ -4,22 +4,11 @@ module Gitlab class CircuitBreaker include CircuitBreakerSettings - FailureInfo = Struct.new(:last_failure, :failure_count) - attr_reader :storage, - :hostname, - :storage_path - - delegate :last_failure, :failure_count, to: :failure_info - - def self.reset_all! - Gitlab::Git::Storage.redis.with do |redis| - all_storage_keys = redis.zrange(Gitlab::Git::Storage::REDIS_KNOWN_KEYS, 0, -1) - redis.del(*all_storage_keys) unless all_storage_keys.empty? - end + :hostname - RequestStore.delete(:circuitbreaker_cache) - end + delegate :last_failure, :failure_count, :no_failures?, + to: :failure_info def self.for_storage(storage) cached_circuitbreakers = RequestStore.fetch(:circuitbreaker_cache) do @@ -46,9 +35,6 @@ module Gitlab def initialize(storage, hostname) @storage = storage @hostname = hostname - - config = Gitlab.config.repositories.storages[@storage] - @storage_path = config['path'] end def perform @@ -65,15 +51,6 @@ module Gitlab failure_count > failure_count_threshold end - def backing_off? - return false if no_failures? - - recent_failure = last_failure > failure_wait_time.seconds.ago - too_many_failures = failure_count > backoff_threshold - - recent_failure && too_many_failures - end - private # The circuitbreaker can be enabled for the entire fleet using a Feature @@ -86,88 +63,13 @@ module Gitlab end def failure_info - @failure_info ||= get_failure_info - end - - # Memoizing the `storage_available` call means we only do it once per - # request when the storage is available. - # - # When the storage appears not available, and the memoized value is `false` - # we might want to try again. - def storage_available? - return @storage_available if @storage_available - - if @storage_available = Gitlab::Git::Storage::ForkedStorageCheck - .storage_available?(storage_path, storage_timeout, access_retries) - track_storage_accessible - else - track_storage_inaccessible - end - - @storage_available + @failure_info ||= FailureInfo.load(cache_key) end def check_storage_accessible! if circuit_broken? raise Gitlab::Git::Storage::CircuitOpen.new("Circuit for #{storage} is broken", failure_reset_time) end - - if backing_off? - raise Gitlab::Git::Storage::Failing.new("Backing off access to #{storage}", failure_wait_time) - end - - unless storage_available? - raise Gitlab::Git::Storage::Inaccessible.new("#{storage} not accessible", failure_wait_time) - end - end - - def no_failures? - last_failure.blank? && failure_count == 0 - end - - def track_storage_inaccessible - @failure_info = FailureInfo.new(Time.now, failure_count + 1) - - Gitlab::Git::Storage.redis.with do |redis| - redis.pipelined do - redis.hset(cache_key, :last_failure, last_failure.to_i) - redis.hincrby(cache_key, :failure_count, 1) - redis.expire(cache_key, failure_reset_time) - maintain_known_keys(redis) - end - end - end - - def track_storage_accessible - @failure_info = FailureInfo.new(nil, 0) - - Gitlab::Git::Storage.redis.with do |redis| - redis.pipelined do - redis.hset(cache_key, :last_failure, nil) - redis.hset(cache_key, :failure_count, 0) - maintain_known_keys(redis) - end - end - end - - def maintain_known_keys(redis) - expire_time = Time.now.to_i + failure_reset_time - redis.zadd(Gitlab::Git::Storage::REDIS_KNOWN_KEYS, expire_time, cache_key) - redis.zremrangebyscore(Gitlab::Git::Storage::REDIS_KNOWN_KEYS, '-inf', Time.now.to_i) - end - - def get_failure_info - last_failure, failure_count = Gitlab::Git::Storage.redis.with do |redis| - redis.hmget(cache_key, :last_failure, :failure_count) - end - - last_failure = Time.at(last_failure.to_i) if last_failure.present? - - FailureInfo.new(last_failure, failure_count.to_i) - end - - def cache_key - @cache_key ||= "#{Gitlab::Git::Storage::REDIS_KEY_PREFIX}#{storage}:#{hostname}" end end end diff --git a/lib/gitlab/git/storage/circuit_breaker_settings.rb b/lib/gitlab/git/storage/circuit_breaker_settings.rb index 257fe8cd8f0..c9e225f187d 100644 --- a/lib/gitlab/git/storage/circuit_breaker_settings.rb +++ b/lib/gitlab/git/storage/circuit_breaker_settings.rb @@ -6,10 +6,6 @@ module Gitlab application_settings.circuitbreaker_failure_count_threshold end - def failure_wait_time - application_settings.circuitbreaker_failure_wait_time - end - def failure_reset_time application_settings.circuitbreaker_failure_reset_time end @@ -22,8 +18,12 @@ module Gitlab application_settings.circuitbreaker_access_retries end - def backoff_threshold - application_settings.circuitbreaker_backoff_threshold + def check_interval + application_settings.circuitbreaker_check_interval + end + + def cache_key + @cache_key ||= "#{Gitlab::Git::Storage::REDIS_KEY_PREFIX}#{storage}:#{hostname}" end private diff --git a/lib/gitlab/git/storage/failure_info.rb b/lib/gitlab/git/storage/failure_info.rb new file mode 100644 index 00000000000..387279c110d --- /dev/null +++ b/lib/gitlab/git/storage/failure_info.rb @@ -0,0 +1,39 @@ +module Gitlab + module Git + module Storage + class FailureInfo + attr_accessor :first_failure, :last_failure, :failure_count + + def self.reset_all! + Gitlab::Git::Storage.redis.with do |redis| + all_storage_keys = redis.zrange(Gitlab::Git::Storage::REDIS_KNOWN_KEYS, 0, -1) + redis.del(*all_storage_keys) unless all_storage_keys.empty? + end + + RequestStore.delete(:circuitbreaker_cache) + end + + def self.load(cache_key) + first_failure, last_failure, failure_count = Gitlab::Git::Storage.redis.with do |redis| + redis.hmget(cache_key, :first_failure, :last_failure, :failure_count) + end + + last_failure = Time.at(last_failure.to_i) if last_failure.present? + first_failure = Time.at(first_failure.to_i) if first_failure.present? + + new(first_failure, last_failure, failure_count.to_i) + end + + def initialize(first_failure, last_failure, failure_count) + @first_failure = first_failure + @last_failure = last_failure + @failure_count = failure_count + end + + def no_failures? + first_failure.blank? && last_failure.blank? && failure_count == 0 + end + end + end + end +end diff --git a/lib/gitlab/git/storage/null_circuit_breaker.rb b/lib/gitlab/git/storage/null_circuit_breaker.rb index a12d52d295f..261c936c689 100644 --- a/lib/gitlab/git/storage/null_circuit_breaker.rb +++ b/lib/gitlab/git/storage/null_circuit_breaker.rb @@ -11,6 +11,9 @@ module Gitlab # These will always have nil values attr_reader :storage_path + delegate :last_failure, :failure_count, :no_failures?, + to: :failure_info + def initialize(storage, hostname, error: nil) @storage = storage @hostname = hostname @@ -29,16 +32,17 @@ module Gitlab false end - def last_failure - circuit_broken? ? Time.now : nil - end - - def failure_count - circuit_broken? ? failure_count_threshold : 0 - end - def failure_info - Gitlab::Git::Storage::CircuitBreaker::FailureInfo.new(last_failure, failure_count) + @failure_info ||= + if circuit_broken? + Gitlab::Git::Storage::FailureInfo.new(Time.now, + Time.now, + failure_count_threshold) + else + Gitlab::Git::Storage::FailureInfo.new(nil, + nil, + 0) + end end end end diff --git a/lib/gitlab/git_access.rb b/lib/gitlab/git_access.rb index 9d7d921bb9c..56f6febe86d 100644 --- a/lib/gitlab/git_access.rb +++ b/lib/gitlab/git_access.rb @@ -102,18 +102,15 @@ module Gitlab end def check_project_moved! - return unless redirected_path + return if redirected_path.nil? - url = protocol == 'ssh' ? project.ssh_url_to_repo : project.http_url_to_repo - message = <<-MESSAGE.strip_heredoc - Project '#{redirected_path}' was moved to '#{project.full_path}'. + project_moved = Checks::ProjectMoved.new(project, user, redirected_path, protocol) - Please update your Git remote and try again: - - git remote set-url origin #{url} - MESSAGE - - raise ProjectMovedError, message + if project_moved.permanent_redirect? + project_moved.add_redirect_message + else + raise ProjectMovedError, project_moved.redirect_message(rejected: true) + end end def check_command_disabled!(cmd) diff --git a/lib/gitlab/gitaly_client/repository_service.rb b/lib/gitlab/gitaly_client/repository_service.rb index b9e606592d7..c1f95396878 100644 --- a/lib/gitlab/gitaly_client/repository_service.rb +++ b/lib/gitlab/gitaly_client/repository_service.rb @@ -69,6 +69,16 @@ module Gitlab response.value end + def find_merge_base(*revisions) + request = Gitaly::FindMergeBaseRequest.new( + repository: @gitaly_repo, + revisions: revisions.map { |r| GitalyClient.encode(r) } + ) + + response = GitalyClient.call(@storage, :repository_service, :find_merge_base, request) + response.base.presence + end + def fetch_source_branch(source_repository, source_branch, local_ref) request = Gitaly::FetchSourceBranchRequest.new( repository: @gitaly_repo, @@ -87,6 +97,17 @@ module Gitlab response.result end + + def fsck + request = Gitaly::FsckRequest.new(repository: @gitaly_repo) + response = GitalyClient.call(@storage, :repository_service, :fsck, request) + + if response.error.empty? + return "", 0 + else + return response.error.b, 1 + end + end end end end diff --git a/lib/gitlab/identifier.rb b/lib/gitlab/identifier.rb index 94678b6ec40..3f3f10596c5 100644 --- a/lib/gitlab/identifier.rb +++ b/lib/gitlab/identifier.rb @@ -2,9 +2,8 @@ # key-13 or user-36 or last commit module Gitlab module Identifier - def identify(identifier, project, newrev) + def identify(identifier, project = nil, newrev = nil) if identifier.blank? - # Local push from gitlab identify_using_commit(project, newrev) elsif identifier =~ /\Auser-\d+\Z/ # git push over http @@ -17,6 +16,8 @@ module Gitlab # Tries to identify a user based on a commit SHA. def identify_using_commit(project, ref) + return if project.nil? && ref.nil? + commit = project.commit(ref) return if !commit || !commit.author_email diff --git a/lib/gitlab/import_export/command_line_util.rb b/lib/gitlab/import_export/command_line_util.rb index 90942774a2e..0135b3c6f22 100644 --- a/lib/gitlab/import_export/command_line_util.rb +++ b/lib/gitlab/import_export/command_line_util.rb @@ -32,7 +32,7 @@ module Gitlab def execute(cmd) output, status = Gitlab::Popen.popen(cmd) - @shared.error(Gitlab::ImportExport::Error.new(output.to_s)) unless status.zero? + @shared.error(Gitlab::ImportExport::Error.new(output.to_s)) unless status.zero? # rubocop:disable Gitlab/ModuleWithInstanceVariables status.zero? end diff --git a/lib/gitlab/ldap/user.rb b/lib/gitlab/ldap/user.rb index 3945df27eed..84ee94e38e4 100644 --- a/lib/gitlab/ldap/user.rb +++ b/lib/gitlab/ldap/user.rb @@ -36,10 +36,6 @@ module Gitlab ldap_config.block_auto_created_users end - def sync_profile_from_provider? - true - end - def allowed? Gitlab::LDAP::Access.allowed?(gl_user) end diff --git a/lib/gitlab/metrics/influx_db.rb b/lib/gitlab/metrics/influx_db.rb index bdf7910b7c7..6ea132fc5bf 100644 --- a/lib/gitlab/metrics/influx_db.rb +++ b/lib/gitlab/metrics/influx_db.rb @@ -154,6 +154,7 @@ module Gitlab # When enabled this should be set before being used as the usual pattern # "@foo ||= bar" is _not_ thread-safe. + # rubocop:disable Gitlab/ModuleWithInstanceVariables def pool if influx_metrics_enabled? if @pool.nil? @@ -170,6 +171,7 @@ module Gitlab @pool end end + # rubocop:enable Gitlab/ModuleWithInstanceVariables end end end diff --git a/lib/gitlab/metrics/method_call.rb b/lib/gitlab/metrics/method_call.rb index 65d55576ac2..9112164f22e 100644 --- a/lib/gitlab/metrics/method_call.rb +++ b/lib/gitlab/metrics/method_call.rb @@ -1,7 +1,11 @@ +# rubocop:disable Style/ClassVars + module Gitlab module Metrics # Class for tracking timing information about method calls class MethodCall + @@measurement_enabled_cache = Concurrent::AtomicBoolean.new(false) + @@measurement_enabled_cache_expires_at = Concurrent::AtomicFixnum.new(Time.now.to_i) MUTEX = Mutex.new BASE_LABELS = { module: nil, method: nil }.freeze attr_reader :real_time, :cpu_time, :call_count, :labels @@ -18,6 +22,10 @@ module Gitlab end end + def self.measurement_enabled_cache_expires_at + @@measurement_enabled_cache_expires_at + end + # name - The full name of the method (including namespace) such as # `User#sign_in`. # @@ -72,7 +80,14 @@ module Gitlab end def call_measurement_enabled? - Feature.get(:prometheus_metrics_method_instrumentation).enabled? + expires_at = @@measurement_enabled_cache_expires_at.value + if expires_at < Time.now.to_i + if @@measurement_enabled_cache_expires_at.compare_and_set(expires_at, 1.minute.from_now.to_i) + @@measurement_enabled_cache.value = Feature.get(:prometheus_metrics_method_instrumentation).enabled? + end + end + + @@measurement_enabled_cache.value end end end diff --git a/lib/gitlab/metrics/prometheus.rb b/lib/gitlab/metrics/prometheus.rb index 09103b4ca2d..b0b8e8436db 100644 --- a/lib/gitlab/metrics/prometheus.rb +++ b/lib/gitlab/metrics/prometheus.rb @@ -4,6 +4,7 @@ module Gitlab module Metrics module Prometheus include Gitlab::CurrentSettings + include Gitlab::Utils::StrongMemoize REGISTRY_MUTEX = Mutex.new PROVIDER_MUTEX = Mutex.new @@ -17,16 +18,18 @@ module Gitlab end def prometheus_metrics_enabled? - return @prometheus_metrics_enabled if defined?(@prometheus_metrics_enabled) - - @prometheus_metrics_enabled = prometheus_metrics_enabled_unmemoized + strong_memoize(:prometheus_metrics_enabled) do + prometheus_metrics_enabled_unmemoized + end end def registry - return @registry if @registry - - REGISTRY_MUTEX.synchronize do - @registry ||= ::Prometheus::Client.registry + strong_memoize(:registry) do + REGISTRY_MUTEX.synchronize do + strong_memoize(:registry) do + ::Prometheus::Client.registry + end + end end end diff --git a/lib/gitlab/metrics/samplers/influx_sampler.rb b/lib/gitlab/metrics/samplers/influx_sampler.rb index f4f9b5ca792..5a0f7f28fc8 100644 --- a/lib/gitlab/metrics/samplers/influx_sampler.rb +++ b/lib/gitlab/metrics/samplers/influx_sampler.rb @@ -27,7 +27,6 @@ module Gitlab def sample sample_memory_usage sample_file_descriptors - sample_objects sample_gc flush @@ -48,29 +47,6 @@ module Gitlab add_metric('file_descriptors', value: System.file_descriptor_count) end - if Metrics.mri? - def sample_objects - sample = Allocations.to_hash - counts = sample.each_with_object({}) do |(klass, count), hash| - name = klass.name - - next unless name - - hash[name] = count - end - - # Symbols aren't allocated so we'll need to add those manually. - counts['Symbol'] = Symbol.all_symbols.length - - counts.each do |name, count| - add_metric('object_counts', { count: count }, type: name) - end - end - else - def sample_objects - end - end - def sample_gc time = GC::Profiler.total_time * 1000.0 stats = GC.stat.merge(total_time: time) diff --git a/lib/gitlab/metrics/samplers/ruby_sampler.rb b/lib/gitlab/metrics/samplers/ruby_sampler.rb index f4901be9581..b68800417a2 100644 --- a/lib/gitlab/metrics/samplers/ruby_sampler.rb +++ b/lib/gitlab/metrics/samplers/ruby_sampler.rb @@ -48,7 +48,6 @@ module Gitlab def sample start_time = System.monotonic_time sample_gc - sample_objects metrics[:memory_usage].set(labels, System.memory_usage) metrics[:file_descriptors].set(labels, System.file_descriptor_count) @@ -68,32 +67,6 @@ module Gitlab end end - def sample_objects - list_objects.each do |name, count| - metrics[:objects_total].set(labels.merge(class: name), count) - end - end - - if Metrics.mri? - def list_objects - sample = Allocations.to_hash - counts = sample.each_with_object({}) do |(klass, count), hash| - name = klass.name - - next unless name - - hash[name] = count - end - - # Symbols aren't allocated so we'll need to add those manually. - counts['Symbol'] = Symbol.all_symbols.length - counts - end - else - def list_objects - end - end - def worker_label return {} unless defined?(Unicorn::Worker) diff --git a/lib/gitlab/o_auth/provider.rb b/lib/gitlab/o_auth/provider.rb index ac9d66c836d..657db29c85a 100644 --- a/lib/gitlab/o_auth/provider.rb +++ b/lib/gitlab/o_auth/provider.rb @@ -19,6 +19,18 @@ module Gitlab name.to_s.start_with?('ldap') end + def self.sync_profile_from_provider?(provider) + return true if ldap_provider?(provider) + + providers = Gitlab.config.omniauth.sync_profile_from_provider + + if providers.is_a?(Array) + providers.include?(provider) + else + providers + end + end + def self.config_for(name) name = name.to_s if ldap_provider?(name) diff --git a/lib/gitlab/o_auth/user.rb b/lib/gitlab/o_auth/user.rb index 552133234a3..d33f33d192f 100644 --- a/lib/gitlab/o_auth/user.rb +++ b/lib/gitlab/o_auth/user.rb @@ -12,7 +12,7 @@ module Gitlab def initialize(auth_hash) self.auth_hash = auth_hash - update_profile if sync_profile_from_provider? + update_profile add_or_update_user_identities end @@ -195,29 +195,31 @@ module Gitlab end def sync_profile_from_provider? - providers = Gitlab.config.omniauth.sync_profile_from_provider - - if providers.is_a?(Array) - providers.include?(auth_hash.provider) - else - providers - end + Gitlab::OAuth::Provider.sync_profile_from_provider?(auth_hash.provider) end def update_profile - user_synced_attributes_metadata = gl_user.user_synced_attributes_metadata || gl_user.build_user_synced_attributes_metadata - - UserSyncedAttributesMetadata::SYNCABLE_ATTRIBUTES.each do |key| - if auth_hash.has_attribute?(key) && gl_user.sync_attribute?(key) - gl_user[key] = auth_hash.public_send(key) # rubocop:disable GitlabSecurity/PublicSend - user_synced_attributes_metadata.set_attribute_synced(key, true) - else - user_synced_attributes_metadata.set_attribute_synced(key, false) + return unless sync_profile_from_provider? || creating_linked_ldap_user? + + metadata = gl_user.user_synced_attributes_metadata || gl_user.build_user_synced_attributes_metadata + + if sync_profile_from_provider? + UserSyncedAttributesMetadata::SYNCABLE_ATTRIBUTES.each do |key| + if auth_hash.has_attribute?(key) && gl_user.sync_attribute?(key) + gl_user[key] = auth_hash.public_send(key) # rubocop:disable GitlabSecurity/PublicSend + metadata.set_attribute_synced(key, true) + else + metadata.set_attribute_synced(key, false) + end end + + metadata.provider = auth_hash.provider end - user_synced_attributes_metadata.provider = auth_hash.provider - gl_user.user_synced_attributes_metadata = user_synced_attributes_metadata + if creating_linked_ldap_user? && gl_user.email == ldap_person.email.first + metadata.set_attribute_synced(:email, true) + metadata.provider = ldap_person.provider + end end def log diff --git a/lib/gitlab/shell.rb b/lib/gitlab/shell.rb index a22a63665be..9cdd3d22f18 100644 --- a/lib/gitlab/shell.rb +++ b/lib/gitlab/shell.rb @@ -66,7 +66,7 @@ module Gitlab # Init new repository # # storage - project's storage name - # name - project path with namespace + # name - project disk path # # Ex. # add_repository("/path/to/storage", "gitlab/gitlab-ci") @@ -94,23 +94,28 @@ module Gitlab # Import repository # # storage - project's storage path - # name - project path with namespace + # name - project disk path + # url - URL to import from # # Ex. - # import_repository("/path/to/storage", "gitlab/gitlab-ci", "https://github.com/randx/six.git") + # import_repository("/path/to/storage", "gitlab/gitlab-ci", "https://gitlab.com/gitlab-org/gitlab-test.git") # # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/387 def import_repository(storage, name, url) # The timeout ensures the subprocess won't hang forever - cmd = [gitlab_shell_projects_path, 'import-project', - storage, "#{name}.git", url, "#{Gitlab.config.gitlab_shell.git_timeout}"] - gitlab_shell_fast_execute_raise_error(cmd) + cmd = gitlab_projects(storage, "#{name}.git") + success = cmd.import_project(url, git_timeout) + + raise Error, cmd.output unless success + + success end # Fetch remote for repository # # repository - an instance of Git::Repository # remote - remote name + # ssh_auth - SSH known_hosts data and a private key to use for public-key authentication # forced - should we use --force flag? # no_tags - should we use --no-tags flag? # @@ -131,16 +136,15 @@ module Gitlab # Move repository # storage - project's storage path - # path - project path with namespace - # new_path - new project path with namespace + # path - project disk path + # new_path - new project disk path # # Ex. # mv_repository("/path/to/storage", "gitlab/gitlab-ci", "randx/gitlab-ci-new") # # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/387 def mv_repository(storage, path, new_path) - gitlab_shell_fast_execute([gitlab_shell_projects_path, 'mv-project', - storage, "#{path}.git", "#{new_path}.git"]) + gitlab_projects(storage, "#{path}.git").mv_project("#{new_path}.git") end # Fork repository to new path @@ -154,30 +158,21 @@ module Gitlab # # Gitaly note: JV: not easy to migrate because this involves two Gitaly servers, not one. def fork_repository(forked_from_storage, forked_from_disk_path, forked_to_storage, forked_to_disk_path) - gitlab_shell_fast_execute( - [ - gitlab_shell_projects_path, - 'fork-repository', - forked_from_storage, - "#{forked_from_disk_path}.git", - forked_to_storage, - "#{forked_to_disk_path}.git" - ] - ) + gitlab_projects(forked_from_storage, "#{forked_from_disk_path}.git") + .fork_repository(forked_to_storage, "#{forked_to_disk_path}.git") end # Remove repository from file system # # storage - project's storage path - # name - project path with namespace + # name - project disk path # # Ex. # remove_repository("/path/to/storage", "gitlab/gitlab-ci") # # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/387 def remove_repository(storage, name) - gitlab_shell_fast_execute([gitlab_shell_projects_path, - 'rm-project', storage, "#{name}.git"]) + gitlab_projects(storage, "#{name}.git").rm_project end # Add new key to gitlab-shell @@ -311,6 +306,47 @@ module Gitlab end end + # Push branch to remote repository + # + # storage - project's storage path + # project_name - project's disk path + # remote_name - remote name + # branch_names - remote branch names to push + # forced - should we use --force flag + # + # Ex. + # push_remote_branches('/path/to/storage', 'gitlab-org/gitlab-test' 'upstream', ['feature']) + # + def push_remote_branches(storage, project_name, remote_name, branch_names, forced: true) + cmd = gitlab_projects(storage, "#{project_name}.git") + + success = cmd.push_branches(remote_name, git_timeout, forced, branch_names) + + raise Error, cmd.output unless success + + success + end + + # Delete branch from remote repository + # + # storage - project's storage path + # project_name - project's disk path + # remote_name - remote name + # branch_names - remote branch names + # + # Ex. + # delete_remote_branches('/path/to/storage', 'gitlab-org/gitlab-test', 'upstream', ['feature']) + # + def delete_remote_branches(storage, project_name, remote_name, branch_names) + cmd = gitlab_projects(storage, "#{project_name}.git") + + success = cmd.delete_remote_branches(remote_name, branch_names) + + raise Error, cmd.output unless success + + success + end + protected def gitlab_shell_path @@ -341,24 +377,35 @@ module Gitlab private - def local_fetch_remote(storage, name, remote, ssh_auth: nil, forced: false, no_tags: false) - args = [gitlab_shell_projects_path, 'fetch-remote', storage, name, remote, "#{Gitlab.config.gitlab_shell.git_timeout}"] - args << '--force' if forced - args << '--no-tags' if no_tags + def gitlab_projects(shard_path, disk_path) + Gitlab::Git::GitlabProjects.new( + shard_path, + disk_path, + global_hooks_path: Gitlab.config.gitlab_shell.hooks_path, + logger: Rails.logger + ) + end - vars = {} + def local_fetch_remote(storage_path, repository_relative_path, remote, ssh_auth: nil, forced: false, no_tags: false) + vars = { force: forced, tags: !no_tags } if ssh_auth&.ssh_import? if ssh_auth.ssh_key_auth? && ssh_auth.ssh_private_key.present? - vars['GITLAB_SHELL_SSH_KEY'] = ssh_auth.ssh_private_key + vars[:ssh_key] = ssh_auth.ssh_private_key end if ssh_auth.ssh_known_hosts.present? - vars['GITLAB_SHELL_KNOWN_HOSTS'] = ssh_auth.ssh_known_hosts + vars[:known_hosts] = ssh_auth.ssh_known_hosts end end - gitlab_shell_fast_execute_raise_error(args, vars) + cmd = gitlab_projects(storage_path, repository_relative_path) + + success = cmd.fetch_remote(remote, git_timeout, vars) + + raise Error, cmd.output unless success + + success end def gitlab_shell_fast_execute(cmd) @@ -394,6 +441,10 @@ module Gitlab Gitlab::GitalyClient::NamespaceService.new(storage) end + def git_timeout + Gitlab.config.gitlab_shell.git_timeout + end + def gitaly_migrate(method, &block) Gitlab::GitalyClient.migrate(method, &block) rescue GRPC::NotFound, GRPC::BadStatus => e diff --git a/lib/gitlab/sidekiq_config.rb b/lib/gitlab/sidekiq_config.rb index dc9886732b5..c3d7814551c 100644 --- a/lib/gitlab/sidekiq_config.rb +++ b/lib/gitlab/sidekiq_config.rb @@ -1,16 +1,35 @@ require 'yaml' +require 'set' module Gitlab module SidekiqConfig - def self.redis_queues - @redis_queues ||= Sidekiq::Queue.all.map(&:name) + # This method is called by `bin/sidekiq-cluster` in EE, which runs outside + # of bundler/Rails context, so we cannot use any gem or Rails methods. + def self.worker_queues(rails_path = Rails.root.to_s) + @worker_queues ||= {} + @worker_queues[rails_path] ||= YAML.load_file(File.join(rails_path, 'app/workers/all_queues.yml')) end # This method is called by `bin/sidekiq-cluster` in EE, which runs outside # of bundler/Rails context, so we cannot use any gem or Rails methods. - def self.config_queues(rails_path = Rails.root.to_s) + def self.expand_queues(queues, all_queues = self.worker_queues) + return [] if queues.empty? + + queues_set = all_queues.to_set + + queues.flat_map do |queue| + [queue, *queues_set.grep(/\A#{queue}:/)] + end + end + + def self.redis_queues + # Not memoized, because this can change during the life of the application + Sidekiq::Queue.all.map(&:name) + end + + def self.config_queues @config_queues ||= begin - config = YAML.load_file(File.join(rails_path, 'config', 'sidekiq_queues.yml')) + config = YAML.load_file(Rails.root.join('config/sidekiq_queues.yml')) config[:queues].map(&:first) end end @@ -23,14 +42,6 @@ module Gitlab @workers ||= find_workers(Rails.root.join('app', 'workers')) end - def self.default_queues - [ActionMailer::DeliveryJob.queue_name, 'default'] - end - - def self.worker_queues - @worker_queues ||= (workers.map(&:queue) + default_queues).uniq - end - def self.find_workers(root) concerns = root.join('concerns').to_s @@ -43,7 +54,7 @@ module Gitlab ns.camelize.constantize end - # Skip concerns + # Skip things that aren't workers workers.select { |w| w < Sidekiq::Worker } end end diff --git a/lib/gitlab/sidekiq_versioning.rb b/lib/gitlab/sidekiq_versioning.rb new file mode 100644 index 00000000000..9683214ec18 --- /dev/null +++ b/lib/gitlab/sidekiq_versioning.rb @@ -0,0 +1,25 @@ +module Gitlab + module SidekiqVersioning + def self.install! + Sidekiq::Manager.prepend SidekiqVersioning::Manager + + # The Sidekiq client API always adds the queue to the Sidekiq queue + # list, but mail_room and gitlab-shell do not. This is only necessary + # for monitoring. + begin + queues = SidekiqConfig.worker_queues + + if queues.any? + Sidekiq.redis do |conn| + conn.pipelined do + queues.each do |queue| + conn.sadd('queues', queue) + end + end + end + end + rescue ::Redis::BaseError, SocketError, Errno::ENOENT, Errno::EADDRNOTAVAIL, Errno::EAFNOSUPPORT, Errno::ECONNRESET, Errno::ECONNREFUSED + end + end + end +end diff --git a/lib/gitlab/sidekiq_versioning/manager.rb b/lib/gitlab/sidekiq_versioning/manager.rb new file mode 100644 index 00000000000..308be0fdf76 --- /dev/null +++ b/lib/gitlab/sidekiq_versioning/manager.rb @@ -0,0 +1,12 @@ +module Gitlab + module SidekiqVersioning + module Manager + def initialize(options = {}) + options[:strict] = false + options[:queues] = SidekiqConfig.expand_queues(options[:queues]) + Sidekiq.logger.info "Listening on queues #{options[:queues].uniq.sort}" + super + end + end + end +end diff --git a/lib/gitlab/slash_commands/presenters/issue_base.rb b/lib/gitlab/slash_commands/presenters/issue_base.rb index 341f2aabdd0..31c1e97efba 100644 --- a/lib/gitlab/slash_commands/presenters/issue_base.rb +++ b/lib/gitlab/slash_commands/presenters/issue_base.rb @@ -11,32 +11,36 @@ module Gitlab end def project - @resource.project + resource.project end def author - @resource.author + resource.author end def fields [ { title: "Assignee", - value: @resource.assignees.any? ? @resource.assignees.first.name : "_None_", + value: resource.assignees.any? ? resource.assignees.first.name : "_None_", short: true }, { title: "Milestone", - value: @resource.milestone ? @resource.milestone.title : "_None_", + value: resource.milestone ? resource.milestone.title : "_None_", short: true }, { title: "Labels", - value: @resource.labels.any? ? @resource.label_names.join(', ') : "_None_", + value: resource.labels.any? ? resource.label_names.join(', ') : "_None_", short: true } ] end + + private + + attr_reader :resource end end end diff --git a/lib/gitlab/storage_check.rb b/lib/gitlab/storage_check.rb new file mode 100644 index 00000000000..fe81513c9ec --- /dev/null +++ b/lib/gitlab/storage_check.rb @@ -0,0 +1,11 @@ +require_relative 'storage_check/cli' +require_relative 'storage_check/gitlab_caller' +require_relative 'storage_check/option_parser' +require_relative 'storage_check/response' + +module Gitlab + module StorageCheck + ENDPOINT = '/-/storage_check'.freeze + Options = Struct.new(:target, :token, :interval, :dryrun) + end +end diff --git a/lib/gitlab/storage_check/cli.rb b/lib/gitlab/storage_check/cli.rb new file mode 100644 index 00000000000..04bf1bf1d26 --- /dev/null +++ b/lib/gitlab/storage_check/cli.rb @@ -0,0 +1,69 @@ +module Gitlab + module StorageCheck + class CLI + def self.start!(args) + runner = new(Gitlab::StorageCheck::OptionParser.parse!(args)) + runner.start_loop + end + + attr_reader :logger, :options + + def initialize(options) + @options = options + @logger = Logger.new(STDOUT) + end + + def start_loop + logger.info "Checking #{options.target} every #{options.interval} seconds" + + if options.dryrun + logger.info "Dryrun, exiting..." + return + end + + begin + loop do + response = GitlabCaller.new(options).call! + log_response(response) + update_settings(response) + + sleep options.interval + end + rescue Interrupt + logger.info "Ending storage-check" + end + end + + def update_settings(response) + previous_interval = options.interval + + if response.valid? + options.interval = response.check_interval || previous_interval + end + + if previous_interval != options.interval + logger.info "Interval changed: #{options.interval} seconds" + end + end + + def log_response(response) + unless response.valid? + return logger.error("Invalid response checking nfs storage: #{response.http_response.inspect}") + end + + if response.responsive_shards.any? + logger.debug("Responsive shards: #{response.responsive_shards.join(', ')}") + end + + warnings = [] + if response.skipped_shards.any? + warnings << "Skipped shards: #{response.skipped_shards.join(', ')}" + end + if response.failing_shards.any? + warnings << "Failing shards: #{response.failing_shards.join(', ')}" + end + logger.warn(warnings.join(' - ')) if warnings.any? + end + end + end +end diff --git a/lib/gitlab/storage_check/gitlab_caller.rb b/lib/gitlab/storage_check/gitlab_caller.rb new file mode 100644 index 00000000000..44952b68844 --- /dev/null +++ b/lib/gitlab/storage_check/gitlab_caller.rb @@ -0,0 +1,39 @@ +require 'excon' + +module Gitlab + module StorageCheck + class GitlabCaller + def initialize(options) + @options = options + end + + def call! + Gitlab::StorageCheck::Response.new(get_response) + rescue Errno::ECONNREFUSED, Excon::Error + # Server not ready, treated as invalid response. + Gitlab::StorageCheck::Response.new(nil) + end + + def get_response + scheme, *other_parts = URI.split(@options.target) + socket_path = if scheme == 'unix' + other_parts.compact.join + end + + connection = Excon.new(@options.target, socket: socket_path) + connection.post(path: Gitlab::StorageCheck::ENDPOINT, + headers: headers) + end + + def headers + @headers ||= begin + headers = {} + headers['Content-Type'] = headers['Accept'] = 'application/json' + headers['TOKEN'] = @options.token if @options.token + + headers + end + end + end + end +end diff --git a/lib/gitlab/storage_check/option_parser.rb b/lib/gitlab/storage_check/option_parser.rb new file mode 100644 index 00000000000..66ed7906f97 --- /dev/null +++ b/lib/gitlab/storage_check/option_parser.rb @@ -0,0 +1,39 @@ +module Gitlab + module StorageCheck + class OptionParser + def self.parse!(args) + # Start out with some defaults + options = Gitlab::StorageCheck::Options.new(nil, nil, 1, false) + + parser = ::OptionParser.new do |opts| + opts.banner = "Usage: bin/storage_check [options]" + + opts.on('-t=string', '--target string', 'URL or socket to trigger storage check') do |value| + options.target = value + end + + opts.on('-T=string', '--token string', 'Health token to use') { |value| options.token = value } + + opts.on('-i=n', '--interval n', ::OptionParser::DecimalInteger, 'Seconds between checks') do |value| + options.interval = value + end + + opts.on('-d', '--dryrun', "Output what will be performed, but don't start the process") do |value| + options.dryrun = value + end + end + parser.parse!(args) + + unless options.target + raise ::OptionParser::InvalidArgument.new('Provide a URI to provide checks') + end + + if URI.parse(options.target).scheme.nil? + raise ::OptionParser::InvalidArgument.new('Add the scheme to the target, `unix://`, `https://` or `http://` are supported') + end + + options + end + end + end +end diff --git a/lib/gitlab/storage_check/response.rb b/lib/gitlab/storage_check/response.rb new file mode 100644 index 00000000000..326ab236e3e --- /dev/null +++ b/lib/gitlab/storage_check/response.rb @@ -0,0 +1,77 @@ +require 'json' + +module Gitlab + module StorageCheck + class Response + attr_reader :http_response + + def initialize(http_response) + @http_response = http_response + end + + def valid? + @http_response && (200...299).cover?(@http_response.status) && + @http_response.headers['Content-Type'].include?('application/json') && + parsed_response + end + + def check_interval + return nil unless parsed_response + + parsed_response['check_interval'] + end + + def responsive_shards + divided_results[:responsive_shards] + end + + def skipped_shards + divided_results[:skipped_shards] + end + + def failing_shards + divided_results[:failing_shards] + end + + private + + def results + return [] unless parsed_response + + parsed_response['results'] + end + + def divided_results + return @divided_results if @divided_results + + @divided_results = {} + @divided_results[:responsive_shards] = [] + @divided_results[:skipped_shards] = [] + @divided_results[:failing_shards] = [] + + results.each do |info| + name = info['storage'] + + case info['success'] + when true + @divided_results[:responsive_shards] << name + when false + @divided_results[:failing_shards] << name + else + @divided_results[:skipped_shards] << name + end + end + + @divided_results + end + + def parsed_response + return @parsed_response if defined?(@parsed_response) + + @parsed_response = JSON.parse(@http_response.body) + rescue JSON::JSONError + @parsed_response = nil + end + end + end +end diff --git a/lib/gitlab/tcp_checker.rb b/lib/gitlab/tcp_checker.rb new file mode 100644 index 00000000000..6e24e46d0ea --- /dev/null +++ b/lib/gitlab/tcp_checker.rb @@ -0,0 +1,45 @@ +module Gitlab + class TcpChecker + attr_reader :remote_host, :remote_port, :local_host, :local_port, :error + + def initialize(remote_host, remote_port, local_host = nil, local_port = nil) + @remote_host = remote_host + @remote_port = remote_port + @local_host = local_host + @local_port = local_port + end + + def local + join_host_port(local_host, local_port) + end + + def remote + join_host_port(remote_host, remote_port) + end + + def check(timeout: 10) + Socket.tcp( + remote_host, remote_port, + local_host, local_port, + connect_timeout: timeout + ) do |sock| + @local_port, @local_host = Socket.unpack_sockaddr_in(sock.local_address) + @remote_port, @remote_host = Socket.unpack_sockaddr_in(sock.remote_address) + end + + true + rescue => err + @error = err + + false + end + + private + + def join_host_port(host, port) + host = "[#{host}]" if host.include?(':') + + "#{host}:#{port}" + end + end +end diff --git a/lib/gitlab/utils/strong_memoize.rb b/lib/gitlab/utils/strong_memoize.rb index a2ac9285b56..fe091f4611b 100644 --- a/lib/gitlab/utils/strong_memoize.rb +++ b/lib/gitlab/utils/strong_memoize.rb @@ -11,6 +11,8 @@ module Gitlab # # We could write it like: # + # include Gitlab::Utils::StrongMemoize + # # def trigger_from_token # strong_memoize(:trigger) do # Ci::Trigger.find_by_token(params[:token].to_s) @@ -18,14 +20,22 @@ module Gitlab # end # def strong_memoize(name) - ivar_name = "@#{name}" - - if instance_variable_defined?(ivar_name) - instance_variable_get(ivar_name) + if instance_variable_defined?(ivar(name)) + instance_variable_get(ivar(name)) else - instance_variable_set(ivar_name, yield) + instance_variable_set(ivar(name), yield) end end + + def clear_memoization(name) + remove_instance_variable(ivar(name)) if instance_variable_defined?(ivar(name)) + end + + private + + def ivar(name) + "@#{name}" + end end end end diff --git a/lib/gitlab/view/presenter/factory.rb b/lib/gitlab/view/presenter/factory.rb index d172d61e2c9..570f0723e39 100644 --- a/lib/gitlab/view/presenter/factory.rb +++ b/lib/gitlab/view/presenter/factory.rb @@ -16,7 +16,7 @@ module Gitlab attr_reader :subject, :attributes def presenter_class - "#{subject.class.name}Presenter".constantize + attributes.delete(:presenter_class) { "#{subject.class.name}Presenter".constantize } end end end diff --git a/lib/tasks/gettext.rake b/lib/tasks/gettext.rake index 35ba729c156..247d7be7d78 100644 --- a/lib/tasks/gettext.rake +++ b/lib/tasks/gettext.rake @@ -23,6 +23,7 @@ namespace :gettext do desc 'Lint all po files in `locale/' task lint: :environment do require 'simple_po_parser' + require 'gitlab/utils' FastGettext.silence_errors files = Dir.glob(Rails.root.join('locale/*/gitlab.po')) diff --git a/lib/tasks/gitlab/task_helpers.rb b/lib/tasks/gitlab/task_helpers.rb index 8a63f486fa3..6723662703c 100644 --- a/lib/tasks/gitlab/task_helpers.rb +++ b/lib/tasks/gitlab/task_helpers.rb @@ -1,10 +1,13 @@ require 'rainbow/ext/string' +require 'gitlab/utils/strong_memoize' module Gitlab TaskFailedError = Class.new(StandardError) TaskAbortedByUserError = Class.new(StandardError) module TaskHelpers + include Gitlab::Utils::StrongMemoize + extend self # Ask if the user wants to continue @@ -105,16 +108,16 @@ module Gitlab end def gitlab_user? - return @is_gitlab_user unless @is_gitlab_user.nil? - - current_user = run_command(%w(whoami)).chomp - @is_gitlab_user = current_user == gitlab_user + strong_memoize(:is_gitlab_user) do + current_user = run_command(%w(whoami)).chomp + current_user == gitlab_user + end end def warn_user_is_not_gitlab - return if @warned_user_not_gitlab + return if gitlab_user? - unless gitlab_user? + strong_memoize(:warned_user_not_gitlab) do current_user = run_command(%w(whoami)).chomp puts " Warning ".color(:black).background(:yellow) @@ -122,8 +125,6 @@ module Gitlab puts " Things may work\/fail for the wrong reasons." puts " For correct results you should run this as user #{gitlab_user.color(:magenta)}." puts "" - - @warned_user_not_gitlab = true end end diff --git a/lib/tasks/gitlab/tcp_check.rake b/lib/tasks/gitlab/tcp_check.rake new file mode 100644 index 00000000000..1400f57d6b9 --- /dev/null +++ b/lib/tasks/gitlab/tcp_check.rake @@ -0,0 +1,20 @@ +namespace :gitlab do + desc "GitLab | Check TCP connectivity to a specific host and port" + task :tcp_check, [:host, :port] => :environment do |_t, args| + unless args.host && args.port + puts "Please specify a host and port: `rake gitlab:tcp_check[example.com,80]`".color(:red) + exit 1 + end + + checker = Gitlab::TcpChecker.new(args.host, args.port) + + if checker.check + puts "TCP connection from #{checker.local} to #{checker.remote} succeeded".color(:green) + else + puts "TCP connection to #{checker.remote} failed: #{checker.error}".color(:red) + puts + puts 'Check that host and port are correct, and that the traffic is permitted through any firewalls.' + exit 1 + end + end +end diff --git a/package.json b/package.json index c0a4db122bd..9e816e007ee 100644 --- a/package.json +++ b/package.json @@ -80,7 +80,7 @@ "worker-loader": "^1.1.0" }, "devDependencies": { - "@gitlab-org/gitlab-svgs": "^1.1.1", + "@gitlab-org/gitlab-svgs": "^1.3.0", "babel-plugin-istanbul": "^4.1.5", "eslint": "^3.10.1", "eslint-config-airbnb-base": "^10.0.1", diff --git a/qa/Dockerfile b/qa/Dockerfile index 9b6ffff7c4d..ed2ee73bea0 100644 --- a/qa/Dockerfile +++ b/qa/Dockerfile @@ -1,4 +1,4 @@ -FROM ruby:2.3 +FROM ruby:2.4 LABEL maintainer "Grzegorz Bizon <grzegorz@gitlab.com>" ENV DEBIAN_FRONTEND noninteractive diff --git a/qa/Gemfile b/qa/Gemfile index ff29824529f..4c866a3f893 100644 --- a/qa/Gemfile +++ b/qa/Gemfile @@ -1,8 +1,8 @@ source 'https://rubygems.org' -gem 'pry-byebug', '~> 3.4.1', platform: :mri -gem 'capybara', '~> 2.12.1' -gem 'capybara-screenshot', '~> 1.0.14' -gem 'rake', '~> 12.0.0' -gem 'rspec', '~> 3.5' -gem 'selenium-webdriver', '~> 2.53' +gem 'pry-byebug', '~> 3.5.1', platform: :mri +gem 'capybara', '~> 2.16.1' +gem 'capybara-screenshot', '~> 1.0.18' +gem 'rake', '~> 12.3.0' +gem 'rspec', '~> 3.7' +gem 'selenium-webdriver', '~> 3.8.0' diff --git a/qa/Gemfile.lock b/qa/Gemfile.lock index 22d12b479cb..88d5fe834a0 100644 --- a/qa/Gemfile.lock +++ b/qa/Gemfile.lock @@ -1,78 +1,72 @@ GEM remote: https://rubygems.org/ specs: - addressable (2.5.0) - public_suffix (~> 2.0, >= 2.0.2) - byebug (9.0.6) - capybara (2.12.1) + addressable (2.5.2) + public_suffix (>= 2.0.2, < 4.0) + byebug (9.1.0) + capybara (2.16.1) addressable - mime-types (>= 1.16) + mini_mime (>= 0.1.3) nokogiri (>= 1.3.3) rack (>= 1.0.0) rack-test (>= 0.5.4) xpath (~> 2.0) - capybara-screenshot (1.0.14) + capybara-screenshot (1.0.18) capybara (>= 1.0, < 3) launchy - childprocess (0.7.0) + childprocess (0.8.0) ffi (~> 1.0, >= 1.0.11) - coderay (1.1.1) + coderay (1.1.2) diff-lcs (1.3) ffi (1.9.18) launchy (2.4.3) addressable (~> 2.3) - method_source (0.8.2) - mime-types (3.1) - mime-types-data (~> 3.2015) - mime-types-data (3.2016.0521) + method_source (0.9.0) + mini_mime (1.0.0) mini_portile2 (2.3.0) nokogiri (1.8.1) mini_portile2 (~> 2.3.0) - pry (0.10.4) + pry (0.11.3) coderay (~> 1.1.0) - method_source (~> 0.8.1) - slop (~> 3.4) - pry-byebug (3.4.2) - byebug (~> 9.0) + method_source (~> 0.9.0) + pry-byebug (3.5.1) + byebug (~> 9.1) pry (~> 0.10) - public_suffix (2.0.5) - rack (2.0.1) - rack-test (0.6.3) - rack (>= 1.0) - rake (12.0.0) - rspec (3.5.0) - rspec-core (~> 3.5.0) - rspec-expectations (~> 3.5.0) - rspec-mocks (~> 3.5.0) - rspec-core (3.5.4) - rspec-support (~> 3.5.0) - rspec-expectations (3.5.0) + public_suffix (3.0.1) + rack (2.0.3) + rack-test (0.8.2) + rack (>= 1.0, < 3) + rake (12.3.0) + rspec (3.7.0) + rspec-core (~> 3.7.0) + rspec-expectations (~> 3.7.0) + rspec-mocks (~> 3.7.0) + rspec-core (3.7.0) + rspec-support (~> 3.7.0) + rspec-expectations (3.7.0) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.5.0) - rspec-mocks (3.5.0) + rspec-support (~> 3.7.0) + rspec-mocks (3.7.0) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.5.0) - rspec-support (3.5.0) + rspec-support (~> 3.7.0) + rspec-support (3.7.0) rubyzip (1.2.1) - selenium-webdriver (2.53.4) + selenium-webdriver (3.8.0) childprocess (~> 0.5) rubyzip (~> 1.0) - websocket (~> 1.0) - slop (3.6.0) - websocket (1.2.4) - xpath (2.0.0) + xpath (2.1.0) nokogiri (~> 1.3) PLATFORMS ruby DEPENDENCIES - capybara (~> 2.12.1) - capybara-screenshot (~> 1.0.14) - pry-byebug (~> 3.4.1) - rake (~> 12.0.0) - rspec (~> 3.5) - selenium-webdriver (~> 2.53) + capybara (~> 2.16.1) + capybara-screenshot (~> 1.0.18) + pry-byebug (~> 3.5.1) + rake (~> 12.3.0) + rspec (~> 3.7) + selenium-webdriver (~> 3.8.0) BUNDLED WITH - 1.15.4 + 1.16.0 @@ -9,6 +9,28 @@ module QA autoload :User, 'qa/runtime/user' autoload :Namespace, 'qa/runtime/namespace' autoload :Scenario, 'qa/runtime/scenario' + autoload :Browser, 'qa/runtime/browser' + end + + ## + # GitLab QA fabrication mechanisms + # + module Factory + autoload :Base, 'qa/factory/base' + + module Resource + autoload :Sandbox, 'qa/factory/resource/sandbox' + autoload :Group, 'qa/factory/resource/group' + autoload :Project, 'qa/factory/resource/project' + end + + module Repository + autoload :Push, 'qa/factory/repository/push' + end + + module Settings + autoload :HashedStorage, 'qa/factory/settings/hashed_storage' + end end ## @@ -33,27 +55,6 @@ module QA autoload :Mattermost, 'qa/scenario/test/integration/mattermost' end end - - ## - # GitLab instance scenarios. - # - module Gitlab - module Group - autoload :Create, 'qa/scenario/gitlab/group/create' - end - - module Project - autoload :Create, 'qa/scenario/gitlab/project/create' - end - - module Sandbox - autoload :Prepare, 'qa/scenario/gitlab/sandbox/prepare' - end - - module Admin - autoload :HashedStorage, 'qa/scenario/gitlab/admin/hashed_storage' - end - end end ## @@ -65,7 +66,6 @@ module QA autoload :Base, 'qa/page/base' module Main - autoload :Entry, 'qa/page/main/entry' autoload :Login, 'qa/page/main/login' autoload :Menu, 'qa/page/main/menu' autoload :OAuth, 'qa/page/main/oauth' diff --git a/qa/qa/factory/base.rb b/qa/qa/factory/base.rb new file mode 100644 index 00000000000..7b951a99b69 --- /dev/null +++ b/qa/qa/factory/base.rb @@ -0,0 +1,16 @@ +module QA + module Factory + class Base + def self.fabricate!(*args) + new.tap do |factory| + yield factory if block_given? + return factory.fabricate!(*args) + end + end + + def fabricate!(*_args) + raise NotImplementedError + end + end + end +end diff --git a/qa/qa/factory/repository/push.rb b/qa/qa/factory/repository/push.rb new file mode 100644 index 00000000000..1d5375d8c76 --- /dev/null +++ b/qa/qa/factory/repository/push.rb @@ -0,0 +1,45 @@ +require "pry-byebug" + +module QA + module Factory + module Repository + class Push < Factory::Base + PAGE_REGEX_CHECK = + %r{\/#{Runtime::Namespace.sandbox_name}\/qa-test[^\/]+\/{1}[^\/]+\z}.freeze + + attr_writer :file_name, + :file_content, + :commit_message, + :branch_name + + def initialize + @file_name = 'file.txt' + @file_content = '# This is test project' + @commit_message = "Add #{@file_name}" + @branch_name = 'master' + end + + def fabricate! + Git::Repository.perform do |repository| + repository.location = Page::Project::Show.act do + unless PAGE_REGEX_CHECK.match(current_path) + raise "To perform this scenario the current page should be project show." + end + + choose_repository_clone_http + repository_location + end + + repository.use_default_credentials + repository.clone + repository.configure_identity('GitLab QA', 'root@gitlab.com') + + repository.add_file(@file_name, @file_content) + repository.commit(@commit_message) + repository.push_changes(@branch_name) + end + end + end + end + end +end diff --git a/qa/qa/factory/resource/group.rb b/qa/qa/factory/resource/group.rb new file mode 100644 index 00000000000..a081cd94d39 --- /dev/null +++ b/qa/qa/factory/resource/group.rb @@ -0,0 +1,23 @@ +module QA + module Factory + module Resource + class Group < Factory::Base + attr_writer :path, :description + + def initialize + @path = Runtime::Namespace.name + @description = "QA test run at #{Runtime::Namespace.time}" + end + + def fabricate! + Page::Group::New.perform do |group| + group.set_path(@path) + group.set_description(@description) + group.set_visibility('Private') + group.create + end + end + end + end + end +end diff --git a/qa/qa/factory/resource/project.rb b/qa/qa/factory/resource/project.rb new file mode 100644 index 00000000000..64fcfb084bb --- /dev/null +++ b/qa/qa/factory/resource/project.rb @@ -0,0 +1,40 @@ +require 'securerandom' + +module QA + module Factory + module Resource + class Project < Factory::Base + attr_writer :description + + def name=(name) + @name = "#{name}-#{SecureRandom.hex(8)}" + end + + def fabricate! + Factory::Resource::Sandbox.fabricate! + + Page::Group::Show.perform do |page| + if page.has_subgroup?(Runtime::Namespace.name) + page.go_to_subgroup(Runtime::Namespace.name) + else + page.go_to_new_subgroup + + Factory::Resource::Group.fabricate! do |group| + group.path = Runtime::Namespace.name + end + end + + page.go_to_new_project + end + + Page::Project::New.perform do |page| + page.choose_test_namespace + page.choose_name(@name) + page.add_description(@description) + page.create_new_project + end + end + end + end + end +end diff --git a/qa/qa/factory/resource/sandbox.rb b/qa/qa/factory/resource/sandbox.rb new file mode 100644 index 00000000000..fd2177915c5 --- /dev/null +++ b/qa/qa/factory/resource/sandbox.rb @@ -0,0 +1,28 @@ +module QA + module Factory + module Resource + ## + # Ensure we're in our sandbox namespace, either by navigating to it or by + # creating it if it doesn't yet exist. + # + class Sandbox < Factory::Base + def fabricate! + Page::Main::Menu.act { go_to_groups } + + Page::Dashboard::Groups.perform do |page| + if page.has_group?(Runtime::Namespace.sandbox_name) + page.go_to_group(Runtime::Namespace.sandbox_name) + else + page.go_to_new_group + + Resource::Group.fabricate! do |group| + group.path = Runtime::Namespace.sandbox_name + group.description = 'GitLab QA Sandbox' + end + end + end + end + end + end + end +end diff --git a/qa/qa/factory/settings/hashed_storage.rb b/qa/qa/factory/settings/hashed_storage.rb new file mode 100644 index 00000000000..eb3b28f2613 --- /dev/null +++ b/qa/qa/factory/settings/hashed_storage.rb @@ -0,0 +1,22 @@ +module QA + module Factory + module Settings + class HashedStorage < Factory::Base + def fabricate!(*traits) + raise ArgumentError unless traits.include?(:enabled) + + Page::Main::Login.act { sign_in_using_credentials } + Page::Main::Menu.act { go_to_admin_area } + Page::Admin::Menu.act { go_to_settings } + + Page::Admin::Settings.act do + enable_hashed_storage + save_settings + end + + QA::Page::Main::Menu.act { sign_out } + end + end + end + end +end diff --git a/qa/qa/page/base.rb b/qa/qa/page/base.rb index f9a93ef051e..99eba02b6e3 100644 --- a/qa/qa/page/base.rb +++ b/qa/qa/page/base.rb @@ -10,6 +10,18 @@ module QA visit current_url end + def wait(css = '.application', time: 60) + Time.now.tap do |start| + while Time.now - start < time + break if page.has_css?(css, wait: 5) + + refresh + end + end + + yield if block_given? + end + def scroll_to(selector, text: nil) page.execute_script <<~JS var elements = Array.from(document.querySelectorAll('#{selector}')); @@ -24,6 +36,10 @@ module QA page.within(selector) { yield } if block_given? end + + def self.path + raise NotImplementedError + end end end end diff --git a/qa/qa/page/group/show.rb b/qa/qa/page/group/show.rb index 8080deda675..100e71ae157 100644 --- a/qa/qa/page/group/show.rb +++ b/qa/qa/page/group/show.rb @@ -6,7 +6,13 @@ module QA click_link name end + def filter_by_name(name) + fill_in 'Filter by name...', with: name + end + def has_subgroup?(name) + filter_by_name(name) + page.has_link?(name) end diff --git a/qa/qa/page/main/entry.rb b/qa/qa/page/main/entry.rb deleted file mode 100644 index ae6484b4bfe..00000000000 --- a/qa/qa/page/main/entry.rb +++ /dev/null @@ -1,26 +0,0 @@ -module QA - module Page - module Main - class Entry < Page::Base - def visit_login_page - visit("#{Runtime::Scenario.gitlab_address}/users/sign_in") - wait_for_instance_to_be_ready - end - - private - - def wait_for_instance_to_be_ready - # This resolves cold boot / background tasks problems - # - start = Time.now - - while Time.now - start < 240 - break if page.has_css?('.application', wait: 10) - - refresh - end - end - end - end - end -end diff --git a/qa/qa/page/main/login.rb b/qa/qa/page/main/login.rb index 8b0111a78a2..f88325f408b 100644 --- a/qa/qa/page/main/login.rb +++ b/qa/qa/page/main/login.rb @@ -2,6 +2,10 @@ module QA module Page module Main class Login < Page::Base + def initialize + wait('.application', time: 500) + end + def sign_in_using_credentials if page.has_content?('Change your password') fill_in :user_password, with: Runtime::User.password @@ -13,6 +17,10 @@ module QA fill_in :user_password, with: Runtime::User.password click_button 'Sign in' end + + def self.path + '/users/sign_in' + end end end end diff --git a/qa/qa/page/mattermost/login.rb b/qa/qa/page/mattermost/login.rb index 42ab9c6f675..8ffd4fdad13 100644 --- a/qa/qa/page/mattermost/login.rb +++ b/qa/qa/page/mattermost/login.rb @@ -2,10 +2,6 @@ module QA module Page module Mattermost class Login < Page::Base - def initialize - visit(Runtime::Scenario.mattermost_address + '/login') - end - def sign_in_using_oauth click_link class: 'btn btn-custom-login gitlab' @@ -13,6 +9,10 @@ module QA click_button 'Authorize' end end + + def self.path + '/login' + end end end end diff --git a/qa/qa/runtime/browser.rb b/qa/qa/runtime/browser.rb new file mode 100644 index 00000000000..220bb45741b --- /dev/null +++ b/qa/qa/runtime/browser.rb @@ -0,0 +1,112 @@ +require 'rspec/core' +require 'capybara/rspec' +require 'capybara-screenshot/rspec' +require 'selenium-webdriver' + +module QA + module Runtime + class Browser + include QA::Scenario::Actable + + def initialize + self.class.configure! + end + + ## + # Visit a page that belongs to a GitLab instance under given address. + # + # Example: + # + # visit(:gitlab, Page::Main::Login) + # visit('http://gitlab.example/users/sign_in') + # + # In case of an address that is a symbol we will try to guess address + # based on `Runtime::Scenario#something_address`. + # + def visit(address, page, &block) + Browser::Session.new(address, page).tap do |session| + session.perform(&block) + end + end + + def self.visit(address, page, &block) + new.visit(address, page, &block) + end + + def self.configure! + return if Capybara.drivers.include?(:chrome) + + Capybara.register_driver :chrome do |app| + capabilities = Selenium::WebDriver::Remote::Capabilities.chrome( + 'chromeOptions' => { + 'args' => %w[headless no-sandbox disable-gpu window-size=1280,1680] + } + ) + + Capybara::Selenium::Driver + .new(app, browser: :chrome, desired_capabilities: capabilities) + end + + Capybara::Screenshot.register_driver(:chrome) do |driver, path| + driver.browser.save_screenshot(path) + end + + # Keep only the screenshots generated from the last failing test suite + Capybara::Screenshot.prune_strategy = :keep_last_run + + Capybara.configure do |config| + config.default_driver = :chrome + config.javascript_driver = :chrome + config.default_max_wait_time = 10 + # https://github.com/mattheworiordan/capybara-screenshot/issues/164 + config.save_path = 'tmp' + end + end + + class Session + include Capybara::DSL + + def initialize(instance, page = nil) + @instance = instance + @address = host + page&.path + end + + def host + if @instance.is_a?(Symbol) + Runtime::Scenario.send("#{@instance}_address") + else + @instance.to_s + end + end + + def perform(&block) + visit(@address) + + yield if block_given? + rescue + raise if block.nil? + + # RSpec examples will take care of screenshots on their own + # + unless block.binding.receiver.is_a?(RSpec::Core::ExampleGroup) + screenshot_and_save_page + end + + raise + ensure + clear! if block_given? + end + + ## + # Selenium allows to reset session cookies for current domain only. + # + # See gitlab-org/gitlab-qa#102 + # + def clear! + visit(@address) + reset_session! + end + end + end + end +end diff --git a/qa/qa/runtime/scenario.rb b/qa/qa/runtime/scenario.rb index 7ef59046640..15d4112d347 100644 --- a/qa/qa/runtime/scenario.rb +++ b/qa/qa/runtime/scenario.rb @@ -6,13 +6,15 @@ module QA module Scenario extend self - attr_reader :attributes + def attributes + @attributes ||= {} + end def define(attribute, value) - (@attributes ||= {}).store(attribute.to_sym, value) + attributes.store(attribute.to_sym, value) define_singleton_method(attribute) do - @attributes[attribute.to_sym].tap do |value| + attributes[attribute.to_sym].tap do |value| if value.to_s.empty? raise ArgumentError, "Empty `#{attribute}` attribute!" end diff --git a/qa/qa/scenario/entrypoint.rb b/qa/qa/scenario/entrypoint.rb index b9d924651a0..ae099fd911e 100644 --- a/qa/qa/scenario/entrypoint.rb +++ b/qa/qa/scenario/entrypoint.rb @@ -8,7 +8,6 @@ module QA include Bootable def perform(address, *files) - Specs::Config.act { configure_capybara! } Runtime::Scenario.define(:gitlab_address, address) ## diff --git a/qa/qa/scenario/gitlab/admin/hashed_storage.rb b/qa/qa/scenario/gitlab/admin/hashed_storage.rb deleted file mode 100644 index ac2cd549829..00000000000 --- a/qa/qa/scenario/gitlab/admin/hashed_storage.rb +++ /dev/null @@ -1,25 +0,0 @@ -module QA - module Scenario - module Gitlab - module Admin - class HashedStorage < Scenario::Template - def perform(*traits) - raise ArgumentError unless traits.include?(:enabled) - - Page::Main::Entry.act { visit_login_page } - Page::Main::Login.act { sign_in_using_credentials } - Page::Main::Menu.act { go_to_admin_area } - Page::Admin::Menu.act { go_to_settings } - - Page::Admin::Settings.act do - enable_hashed_storage - save_settings - end - - QA::Page::Main::Menu.act { sign_out } - end - end - end - end - end -end diff --git a/qa/qa/scenario/gitlab/group/create.rb b/qa/qa/scenario/gitlab/group/create.rb deleted file mode 100644 index 8e6c7c7ad80..00000000000 --- a/qa/qa/scenario/gitlab/group/create.rb +++ /dev/null @@ -1,27 +0,0 @@ -require 'securerandom' - -module QA - module Scenario - module Gitlab - module Group - class Create < Scenario::Template - attr_writer :path, :description - - def initialize - @path = Runtime::Namespace.name - @description = "QA test run at #{Runtime::Namespace.time}" - end - - def perform - Page::Group::New.perform do |group| - group.set_path(@path) - group.set_description(@description) - group.set_visibility('Private') - group.create - end - end - end - end - end - end -end diff --git a/qa/qa/scenario/gitlab/project/create.rb b/qa/qa/scenario/gitlab/project/create.rb deleted file mode 100644 index bb3b9e19c0f..00000000000 --- a/qa/qa/scenario/gitlab/project/create.rb +++ /dev/null @@ -1,42 +0,0 @@ -require 'securerandom' - -module QA - module Scenario - module Gitlab - module Project - class Create < Scenario::Template - attr_writer :description - - def name=(name) - @name = "#{name}-#{SecureRandom.hex(8)}" - end - - def perform - Scenario::Gitlab::Sandbox::Prepare.perform - - Page::Group::Show.perform do |page| - if page.has_subgroup?(Runtime::Namespace.name) - page.go_to_subgroup(Runtime::Namespace.name) - else - page.go_to_new_subgroup - - Scenario::Gitlab::Group::Create.perform do |group| - group.path = Runtime::Namespace.name - end - end - - page.go_to_new_project - end - - Page::Project::New.perform do |page| - page.choose_test_namespace - page.choose_name(@name) - page.add_description(@description) - page.create_new_project - end - end - end - end - end - end -end diff --git a/qa/qa/scenario/gitlab/sandbox/prepare.rb b/qa/qa/scenario/gitlab/sandbox/prepare.rb deleted file mode 100644 index 990de456e20..00000000000 --- a/qa/qa/scenario/gitlab/sandbox/prepare.rb +++ /dev/null @@ -1,28 +0,0 @@ -module QA - module Scenario - module Gitlab - module Sandbox - # Ensure we're in our sandbox namespace, either by navigating to it or - # by creating it if it doesn't yet exist - class Prepare < Scenario::Template - def perform - Page::Main::Menu.act { go_to_groups } - - Page::Dashboard::Groups.perform do |page| - if page.has_group?(Runtime::Namespace.sandbox_name) - page.go_to_group(Runtime::Namespace.sandbox_name) - else - page.go_to_new_group - - Scenario::Gitlab::Group::Create.perform do |group| - group.path = Runtime::Namespace.sandbox_name - group.description = 'QA sandbox' - end - end - end - end - end - end - end - end -end diff --git a/qa/qa/specs/config.rb b/qa/qa/specs/config.rb deleted file mode 100644 index bce7923e52d..00000000000 --- a/qa/qa/specs/config.rb +++ /dev/null @@ -1,63 +0,0 @@ -require 'rspec/core' -require 'capybara/rspec' -require 'capybara-screenshot/rspec' -require 'selenium-webdriver' - -# rubocop:disable Metrics/MethodLength -# rubocop:disable Metrics/LineLength - -module QA - module Specs - class Config < Scenario::Template - include Scenario::Actable - - def perform - configure_rspec! - configure_capybara! - end - - def configure_rspec! - RSpec.configure do |config| - config.expect_with :rspec do |expectations| - expectations.include_chain_clauses_in_custom_matcher_descriptions = true - end - - config.mock_with :rspec do |mocks| - mocks.verify_partial_doubles = true - end - - config.order = :random - Kernel.srand config.seed - config.formatter = :documentation - config.color = true - end - end - - def configure_capybara! - Capybara.register_driver :chrome do |app| - capabilities = Selenium::WebDriver::Remote::Capabilities.chrome( - 'chromeOptions' => { - 'args' => %w[headless no-sandbox disable-gpu window-size=1280,1680] - } - ) - - Capybara::Selenium::Driver - .new(app, browser: :chrome, desired_capabilities: capabilities) - end - - Capybara::Screenshot.register_driver(:chrome) do |driver, path| - driver.browser.save_screenshot(path) - end - - Capybara.configure do |config| - config.default_driver = :chrome - config.javascript_driver = :chrome - config.default_max_wait_time = 10 - - # https://github.com/mattheworiordan/capybara-screenshot/issues/164 - config.save_path = 'tmp' - end - end - end - end -end diff --git a/qa/qa/specs/features/login/standard_spec.rb b/qa/qa/specs/features/login/standard_spec.rb index b155708c387..9eaa2b772e6 100644 --- a/qa/qa/specs/features/login/standard_spec.rb +++ b/qa/qa/specs/features/login/standard_spec.rb @@ -1,7 +1,7 @@ module QA - feature 'standard root login', :core do + feature 'standard user login', :core do scenario 'user logs in using credentials' do - Page::Main::Entry.act { visit_login_page } + Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.act { sign_in_using_credentials } # TODO, since `Signed in successfully` message was removed diff --git a/qa/qa/specs/features/mattermost/group_create_spec.rb b/qa/qa/specs/features/mattermost/group_create_spec.rb index 853a9a6a4f4..b3dbe44bf6e 100644 --- a/qa/qa/specs/features/mattermost/group_create_spec.rb +++ b/qa/qa/specs/features/mattermost/group_create_spec.rb @@ -1,7 +1,7 @@ module QA feature 'create a new group', :mattermost do scenario 'creating a group with a mattermost team' do - Page::Main::Entry.act { visit_login_page } + Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.act { sign_in_using_credentials } Page::Main::Menu.act { go_to_groups } diff --git a/qa/qa/specs/features/mattermost/login_spec.rb b/qa/qa/specs/features/mattermost/login_spec.rb index 1fde3f89a99..637bbdd643a 100644 --- a/qa/qa/specs/features/mattermost/login_spec.rb +++ b/qa/qa/specs/features/mattermost/login_spec.rb @@ -1,24 +1,17 @@ module QA feature 'logging in to Mattermost', :mattermost do scenario 'can use gitlab oauth' do - Page::Main::Entry.act { visit_login_page } - Page::Main::Login.act { sign_in_using_credentials } - Page::Mattermost::Login.act { sign_in_using_oauth } + Runtime::Browser.visit(:gitlab, Page::Main::Login) do + Page::Main::Login.act { sign_in_using_credentials } - Page::Mattermost::Main.perform do |page| - expect(page).to have_content(/(Welcome to: Mattermost|Logout GitLab Mattermost)/) - end - end + Runtime::Browser.visit(:mattermost, Page::Mattermost::Login) do + Page::Mattermost::Login.act { sign_in_using_oauth } - ## - # TODO, temporary workaround for gitlab-org/gitlab-qa#102. - # - after do - visit Runtime::Scenario.mattermost_address - reset_session! - - visit Runtime::Scenario.gitlab_address - reset_session! + Page::Mattermost::Main.perform do |page| + expect(page).to have_content(/(Welcome to: Mattermost|Logout GitLab Mattermost)/) + end + end + end end end end diff --git a/qa/qa/specs/features/project/create_spec.rb b/qa/qa/specs/features/project/create_spec.rb index aba0c2b4c14..61c19378ae0 100644 --- a/qa/qa/specs/features/project/create_spec.rb +++ b/qa/qa/specs/features/project/create_spec.rb @@ -1,10 +1,10 @@ module QA feature 'create a new project', :core do scenario 'user creates a new project' do - Page::Main::Entry.act { visit_login_page } + Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.act { sign_in_using_credentials } - Scenario::Gitlab::Project::Create.perform do |project| + Factory::Resource::Project.fabricate! do |project| project.name = 'awesome-project' project.description = 'create awesome project test' end diff --git a/qa/qa/specs/features/repository/clone_spec.rb b/qa/qa/specs/features/repository/clone_spec.rb index 5cc3b3b9c1b..2adb7524a46 100644 --- a/qa/qa/specs/features/repository/clone_spec.rb +++ b/qa/qa/specs/features/repository/clone_spec.rb @@ -9,10 +9,10 @@ module QA end before do - Page::Main::Entry.act { visit_login_page } + Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.act { sign_in_using_credentials } - Scenario::Gitlab::Project::Create.perform do |scenario| + Factory::Resource::Project.fabricate! do |scenario| scenario.name = 'project-with-code' scenario.description = 'project for git clone tests' end diff --git a/qa/qa/specs/features/repository/push_spec.rb b/qa/qa/specs/features/repository/push_spec.rb index 30935dc1e13..e47c769b015 100644 --- a/qa/qa/specs/features/repository/push_spec.rb +++ b/qa/qa/specs/features/repository/push_spec.rb @@ -2,29 +2,18 @@ module QA feature 'push code to repository', :core do context 'with regular account over http' do scenario 'user pushes code to the repository' do - Page::Main::Entry.act { visit_login_page } + Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.act { sign_in_using_credentials } - Scenario::Gitlab::Project::Create.perform do |scenario| + Factory::Resource::Project.fabricate! do |scenario| scenario.name = 'project_with_code' scenario.description = 'project with repository' end - Git::Repository.perform do |repository| - repository.location = Page::Project::Show.act do - choose_repository_clone_http - repository_location - end - - repository.use_default_credentials - - repository.act do - clone - configure_identity('GitLab QA', 'root@gitlab.com') - add_file('README.md', '# This is test project') - commit('Add README.md') - push_changes - end + Factory::Repository::Push.fabricate! do |scenario| + scenario.file_name = 'README.md' + scenario.file_content = '# This is test project' + scenario.commit_message = 'Add README.md' end Page::Project::Show.act do diff --git a/qa/qa/specs/runner.rb b/qa/qa/specs/runner.rb index f98b8f88e9a..3f7b75df986 100644 --- a/qa/qa/specs/runner.rb +++ b/qa/qa/specs/runner.rb @@ -17,7 +17,7 @@ module QA tags.to_a.each { |tag| args.push(['-t', tag.to_s]) } args.push(files) - Specs::Config.perform + Runtime::Browser.configure! RSpec::Core::Runner.run(args.flatten, $stderr, $stdout).tap do |status| abort if status.nonzero? diff --git a/rubocop/cop/gitlab/module_with_instance_variables.rb b/rubocop/cop/gitlab/module_with_instance_variables.rb new file mode 100644 index 00000000000..5c9cde98512 --- /dev/null +++ b/rubocop/cop/gitlab/module_with_instance_variables.rb @@ -0,0 +1,63 @@ +module RuboCop + module Cop + module Gitlab + class ModuleWithInstanceVariables < RuboCop::Cop::Cop + MSG = <<~EOL.freeze + Do not use instance variables in a module. Please read this + for the rationale behind it: + + https://docs.gitlab.com/ee/development/module_with_instance_variables.html + EOL + + def on_module(node) + check_method_definition(node) + + # Not sure why some module would have an extra begin wrapping around + node.each_child_node(:begin) do |begin_node| + check_method_definition(begin_node) + end + end + + private + + def check_method_definition(node) + node.each_child_node(:def) do |definition| + # We allow this pattern: + # + # def f + # @f ||= true + # end + if only_ivar_or_assignment?(definition) + # We don't allow if any other ivar is used + definition.each_descendant(:ivar) do |offense| + add_offense(offense, :expression) + end + # We allow initialize method and single ivar + elsif !initialize_method?(definition) && !single_ivar?(definition) + definition.each_descendant(:ivar, :ivasgn) do |offense| + add_offense(offense, :expression) + end + end + end + end + + def only_ivar_or_assignment?(definition) + node = definition.child_nodes.last + + definition.child_nodes.size == 2 && + node.or_asgn_type? && node.child_nodes.first.ivasgn_type? + end + + def single_ivar?(definition) + node = definition.child_nodes.last + + definition.child_nodes.size == 2 && node.ivar_type? + end + + def initialize_method?(definition) + definition.children.first == :initialize + end + end + end + end +end diff --git a/rubocop/cop/include_sidekiq_worker.rb b/rubocop/cop/include_sidekiq_worker.rb new file mode 100644 index 00000000000..4a6332286a2 --- /dev/null +++ b/rubocop/cop/include_sidekiq_worker.rb @@ -0,0 +1,29 @@ +require_relative '../spec_helpers' + +module RuboCop + module Cop + # Cop that makes sure workers include `ApplicationWorker`, not `Sidekiq::Worker`. + class IncludeSidekiqWorker < RuboCop::Cop::Cop + include SpecHelpers + + MSG = 'Include `ApplicationWorker`, not `Sidekiq::Worker`.'.freeze + + def_node_matcher :includes_sidekiq_worker?, <<~PATTERN + (send nil :include (const (const nil :Sidekiq) :Worker)) + PATTERN + + def on_send(node) + return if in_spec?(node) + return unless includes_sidekiq_worker?(node) + + add_offense(node.arguments.first, :expression) + end + + def autocorrect(node) + lambda do |corrector| + corrector.replace(node.source_range, 'ApplicationWorker') + end + end + end + end +end diff --git a/rubocop/cop/migration/remove_column.rb b/rubocop/cop/migration/remove_column.rb new file mode 100644 index 00000000000..e53eb2e07b2 --- /dev/null +++ b/rubocop/cop/migration/remove_column.rb @@ -0,0 +1,30 @@ +require_relative '../../migration_helpers' + +module RuboCop + module Cop + module Migration + # Cop that checks if remove_column is used in a regular (not + # post-deployment) migration. + class RemoveColumn < RuboCop::Cop::Cop + include MigrationHelpers + + MSG = '`remove_column` must only be used in post-deployment migrations'.freeze + + def on_def(node) + def_method = node.children[0] + + return unless in_migration?(node) && !in_post_deployment_migration?(node) + return unless def_method == :change || def_method == :up + + node.each_descendant(:send) do |send_node| + send_method = send_node.children[1] + + if send_method == :remove_column + add_offense(send_node, :selector) + end + end + end + end + end + end +end diff --git a/rubocop/cop/sidekiq_options_queue.rb b/rubocop/cop/sidekiq_options_queue.rb new file mode 100644 index 00000000000..43b35ba0214 --- /dev/null +++ b/rubocop/cop/sidekiq_options_queue.rb @@ -0,0 +1,27 @@ +require_relative '../spec_helpers' + +module RuboCop + module Cop + # Cop that prevents manually setting a queue in Sidekiq workers. + class SidekiqOptionsQueue < RuboCop::Cop::Cop + include SpecHelpers + + MSG = 'Do not manually set a queue; `ApplicationWorker` sets one automatically.'.freeze + + def_node_matcher :sidekiq_options?, <<~PATTERN + (send nil :sidekiq_options $...) + PATTERN + + def on_send(node) + return if in_spec?(node) + return unless sidekiq_options?(node) + + node.arguments.first.each_node(:pair) do |pair| + key_name = pair.key.children[0] + + add_offense(pair, :expression) if key_name == :queue + end + end + end + end +end diff --git a/rubocop/migration_helpers.rb b/rubocop/migration_helpers.rb index c3473771178..c066d424437 100644 --- a/rubocop/migration_helpers.rb +++ b/rubocop/migration_helpers.rb @@ -7,5 +7,11 @@ module RuboCop dirname.end_with?('db/migrate', 'db/post_migrate') end + + def in_post_deployment_migration?(node) + dirname = File.dirname(node.location.expression.source_buffer.name) + + dirname.end_with?('db/post_migrate') + end end end diff --git a/rubocop/rubocop.rb b/rubocop/rubocop.rb index 7621ea50da9..8aa82e9413d 100644 --- a/rubocop/rubocop.rb +++ b/rubocop/rubocop.rb @@ -3,10 +3,13 @@ require_relative 'cop/active_record_serialize' require_relative 'cop/custom_error_class' require_relative 'cop/gem_fetcher' require_relative 'cop/in_batches' +require_relative 'cop/include_sidekiq_worker' require_relative 'cop/line_break_after_guard_clauses' require_relative 'cop/polymorphic_associations' require_relative 'cop/project_path_helper' require_relative 'cop/redirect_with_status' +require_relative 'cop/gitlab/module_with_instance_variables' +require_relative 'cop/sidekiq_options_queue' require_relative 'cop/migration/add_column' require_relative 'cop/migration/add_concurrent_foreign_key' require_relative 'cop/migration/add_concurrent_index' @@ -14,6 +17,7 @@ require_relative 'cop/migration/add_index' require_relative 'cop/migration/add_timestamps' require_relative 'cop/migration/datetime' require_relative 'cop/migration/hash_index' +require_relative 'cop/migration/remove_column' require_relative 'cop/migration/remove_concurrent_index' require_relative 'cop/migration/remove_index' require_relative 'cop/migration/reversible_add_column_with_default' diff --git a/scripts/trigger-build-omnibus b/scripts/trigger-build-omnibus index 3c5c22c9372..85ea4aa74ac 100755 --- a/scripts/trigger-build-omnibus +++ b/scripts/trigger-build-omnibus @@ -21,6 +21,7 @@ module Omnibus if id puts "Triggered https://gitlab.com/#{Omnibus::PROJECT_PATH}/pipelines/#{id}" + puts "Waiting for downstream pipeline status" else raise "Trigger failed! The response from the trigger is: #{res.body}" end @@ -39,7 +40,9 @@ module Omnibus "ref" => ENV["OMNIBUS_BRANCH"] || "master", "variables[GITLAB_VERSION]" => ENV["CI_COMMIT_SHA"], "variables[ALTERNATIVE_SOURCES]" => true, - "variables[ee]" => ee? ? 'true' : 'false' + "variables[ee]" => ee? ? 'true' : 'false', + "variables[TRIGGERED_USER]" => ENV["GITLAB_USER_NAME"], + "variables[TRIGGER_SOURCE]" => "https://gitlab.com/gitlab-org/#{ENV['CI_PROJECT_NAME']}/-/jobs/#{ENV['CI_JOB_ID']}" } end @@ -63,14 +66,14 @@ module Omnibus def wait! loop do - raise 'Pipeline timeout!' if timeout? + raise "Pipeline timed out after waiting for #{duration} minutes!" if timeout? case status - when :pending, :running - puts "Waiting another #{INTERVAL} seconds ..." + when :created, :pending, :running + print "." sleep INTERVAL when :success - puts "Omnibus pipeline succeeded!" + puts "Omnibus pipeline succeeded in #{duration} minutes!" break else raise "Omnibus pipeline did not succeed!" @@ -84,6 +87,10 @@ module Omnibus Time.now.to_i > (@start + MAX_DURATION) end + def duration + (Time.now.to_i - @start) / 60 + end + def status req = Net::HTTP::Get.new(@uri) req['PRIVATE-TOKEN'] = ENV['GITLAB_QA_ACCESS_TOKEN'] diff --git a/spec/bin/storage_check_spec.rb b/spec/bin/storage_check_spec.rb new file mode 100644 index 00000000000..02f6fcb6e3a --- /dev/null +++ b/spec/bin/storage_check_spec.rb @@ -0,0 +1,13 @@ +require 'spec_helper' + +describe 'bin/storage_check' do + it 'is executable' do + command = %w[bin/storage_check -t unix://the/path/to/a/unix-socket.sock -i 10 -d] + expected_output = 'Checking unix://the/path/to/a/unix-socket.sock every 10 seconds' + + output, status = Gitlab::Popen.popen(command, Rails.root.to_s) + + expect(status).to eq(0) + expect(output).to include(expected_output) + end +end diff --git a/spec/controllers/admin/health_check_controller_spec.rb b/spec/controllers/admin/health_check_controller_spec.rb index 0b8e0c8a065..d15ee0021d9 100644 --- a/spec/controllers/admin/health_check_controller_spec.rb +++ b/spec/controllers/admin/health_check_controller_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Admin::HealthCheckController, broken_storage: true do +describe Admin::HealthCheckController do let(:admin) { create(:admin) } before do @@ -17,7 +17,7 @@ describe Admin::HealthCheckController, broken_storage: true do describe 'POST reset_storage_health' do it 'resets all storage health information' do - expect(Gitlab::Git::Storage::CircuitBreaker).to receive(:reset_all!) + expect(Gitlab::Git::Storage::FailureInfo).to receive(:reset_all!) post :reset_storage_health end diff --git a/spec/controllers/groups/group_members_controller_spec.rb b/spec/controllers/groups/group_members_controller_spec.rb index 9c6d584f59b..362d5cc4514 100644 --- a/spec/controllers/groups/group_members_controller_spec.rb +++ b/spec/controllers/groups/group_members_controller_spec.rb @@ -62,6 +62,25 @@ describe Groups::GroupMembersController do end end + describe 'PUT update' do + let(:requester) { create(:group_member, :access_request, group: group) } + + before do + group.add_owner(user) + sign_in(user) + end + + Gitlab::Access.options.each do |label, value| + it "can change the access level to #{label}" do + xhr :put, :update, group_member: { access_level: value }, + group_id: group, + id: requester + + expect(requester.reload.human_access).to eq(label) + end + end + end + describe 'DELETE destroy' do let(:member) { create(:group_member, :developer, group: group) } diff --git a/spec/controllers/health_controller_spec.rb b/spec/controllers/health_controller_spec.rb index 9e9cf4f2c1f..95946def5f9 100644 --- a/spec/controllers/health_controller_spec.rb +++ b/spec/controllers/health_controller_spec.rb @@ -14,6 +14,48 @@ describe HealthController do stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false') end + describe '#storage_check' do + before do + allow(Gitlab::RequestContext).to receive(:client_ip).and_return(whitelisted_ip) + end + + subject { post :storage_check } + + it 'checks all the configured storages' do + expect(Gitlab::Git::Storage::Checker).to receive(:check_all).and_call_original + + subject + end + + it 'returns the check interval' do + stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'true') + stub_application_setting(circuitbreaker_check_interval: 10) + + subject + + expect(json_response['check_interval']).to eq(10) + end + + context 'with failing storages', :broken_storage do + before do + stub_storage_settings( + broken: { path: 'tmp/tests/non-existent-repositories' } + ) + end + + it 'includes the failure information' do + subject + + expected_results = [ + { 'storage' => 'broken', 'success' => false }, + { 'storage' => 'default', 'success' => true } + ] + + expect(json_response['results']).to eq(expected_results) + end + end + end + describe '#readiness' do shared_context 'endpoint responding with readiness data' do let(:request_params) { {} } diff --git a/spec/controllers/projects/clusters_controller_spec.rb b/spec/controllers/projects/clusters_controller_spec.rb index 280b7e4d8b9..a3b13647c92 100644 --- a/spec/controllers/projects/clusters_controller_spec.rb +++ b/spec/controllers/projects/clusters_controller_spec.rb @@ -27,13 +27,6 @@ describe Projects::ClustersController do expect(assigns(:clusters)).to match_array([enabled_cluster, disabled_cluster]) end - it 'assigns counters to correct values' do - go - - expect(assigns(:active_count)).to eq(1) - expect(assigns(:inactive_count)).to eq(1) - end - context 'when page is specified' do let(:last_page) { project.clusters.page.total_pages } @@ -48,20 +41,6 @@ describe Projects::ClustersController do expect(assigns(:clusters).current_page).to eq(last_page) end end - - context 'when only enabled clusters are requested' do - it 'returns only enabled clusters' do - get :index, namespace_id: project.namespace, project_id: project, scope: 'active' - expect(assigns(:clusters)).to all(have_attributes(enabled: true)) - end - end - - context 'when only disabled clusters are requested' do - it 'returns only disabled clusters' do - get :index, namespace_id: project.namespace, project_id: project, scope: 'inactive' - expect(assigns(:clusters)).to all(have_attributes(enabled: false)) - end - end end context 'when project does not have a cluster' do @@ -74,13 +53,6 @@ describe Projects::ClustersController do expect(response).to render_template(:index, partial: :empty_state) expect(assigns(:clusters)).to eq([]) end - - it 'assigns counters to zero' do - go - - expect(assigns(:active_count)).to eq(0) - expect(assigns(:inactive_count)).to eq(0) - end end end diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb index 4dbbaecdd6d..c5d08cb0b9d 100644 --- a/spec/controllers/projects/issues_controller_spec.rb +++ b/spec/controllers/projects/issues_controller_spec.rb @@ -272,6 +272,20 @@ describe Projects::IssuesController do expect(response).to have_http_status(:ok) expect(issue.reload.title).to eq('New title') end + + context 'when Akismet is enabled and the issue is identified as spam' do + before do + stub_application_setting(recaptcha_enabled: true) + allow_any_instance_of(SpamService).to receive(:check_for_spam?).and_return(true) + allow_any_instance_of(AkismetService).to receive(:spam?).and_return(true) + end + + it 'renders json with recaptcha_html' do + subject + + expect(JSON.parse(response.body)).to have_key('recaptcha_html') + end + end end context 'when user does not have access to update issue' do @@ -504,17 +518,16 @@ describe Projects::IssuesController do expect(spam_logs.first.recaptcha_verified).to be_falsey end - it 'renders json errors' do + it 'renders recaptcha_html json response' do update_issue - expect(json_response) - .to eql("errors" => ["Your issue has been recognized as spam. Please, change the content or solve the reCAPTCHA to proceed."]) + expect(json_response).to have_key('recaptcha_html') end - it 'returns 422 status' do + it 'returns 200 status' do update_issue - expect(response).to have_gitlab_http_status(422) + expect(response).to have_gitlab_http_status(200) end end diff --git a/spec/controllers/projects/project_members_controller_spec.rb b/spec/controllers/projects/project_members_controller_spec.rb index a34dc27a5ed..290dba0610a 100644 --- a/spec/controllers/projects/project_members_controller_spec.rb +++ b/spec/controllers/projects/project_members_controller_spec.rb @@ -66,6 +66,26 @@ describe Projects::ProjectMembersController do end end + describe 'PUT update' do + let(:requester) { create(:project_member, :access_request, project: project) } + + before do + project.add_master(user) + sign_in(user) + end + + Gitlab::Access.options.each do |label, value| + it "can change the access level to #{label}" do + xhr :put, :update, project_member: { access_level: value }, + namespace_id: project.namespace, + project_id: project, + id: requester + + expect(requester.reload.human_access).to eq(label) + end + end + end + describe 'DELETE destroy' do let(:member) { create(:project_member, :developer, project: project) } diff --git a/spec/factories/abuse_reports.rb b/spec/factories/abuse_reports.rb index 8f6422a7825..021971ac421 100644 --- a/spec/factories/abuse_reports.rb +++ b/spec/factories/abuse_reports.rb @@ -1,4 +1,4 @@ -FactoryGirl.define do +FactoryBot.define do factory :abuse_report do reporter factory: :user user diff --git a/spec/factories/appearances.rb b/spec/factories/appearances.rb index 860973024c9..5f9c57c0c8d 100644 --- a/spec/factories/appearances.rb +++ b/spec/factories/appearances.rb @@ -1,6 +1,6 @@ -# Read about factories at https://github.com/thoughtbot/factory_girl +# Read about factories at https://github.com/thoughtbot/factory_bot -FactoryGirl.define do +FactoryBot.define do factory :appearance do title "MepMep" description "This is my Community Edition instance" diff --git a/spec/factories/application_settings.rb b/spec/factories/application_settings.rb index aef65e724c2..3ecc90b6573 100644 --- a/spec/factories/application_settings.rb +++ b/spec/factories/application_settings.rb @@ -1,4 +1,4 @@ -FactoryGirl.define do +FactoryBot.define do factory :application_setting do end end diff --git a/spec/factories/award_emoji.rb b/spec/factories/award_emoji.rb index 4b858df52c9..a0abbbce686 100644 --- a/spec/factories/award_emoji.rb +++ b/spec/factories/award_emoji.rb @@ -1,4 +1,4 @@ -FactoryGirl.define do +FactoryBot.define do factory :award_emoji do name "thumbsup" user diff --git a/spec/factories/boards.rb b/spec/factories/boards.rb index 1ec042a6ab4..1e125237ae8 100644 --- a/spec/factories/boards.rb +++ b/spec/factories/boards.rb @@ -1,4 +1,4 @@ -FactoryGirl.define do +FactoryBot.define do factory :board do project diff --git a/spec/factories/broadcast_messages.rb b/spec/factories/broadcast_messages.rb index c2fdf89213a..9a65e7f8a3f 100644 --- a/spec/factories/broadcast_messages.rb +++ b/spec/factories/broadcast_messages.rb @@ -1,4 +1,4 @@ -FactoryGirl.define do +FactoryBot.define do factory :broadcast_message do message "MyText" starts_at 1.day.ago diff --git a/spec/factories/chat_names.rb b/spec/factories/chat_names.rb index 9a0be1a4598..56993e5da18 100644 --- a/spec/factories/chat_names.rb +++ b/spec/factories/chat_names.rb @@ -1,4 +1,4 @@ -FactoryGirl.define do +FactoryBot.define do factory :chat_name, class: ChatName do user factory: :user service factory: :service diff --git a/spec/factories/chat_teams.rb b/spec/factories/chat_teams.rb index ffedf69a69b..d048c46d6b6 100644 --- a/spec/factories/chat_teams.rb +++ b/spec/factories/chat_teams.rb @@ -1,4 +1,4 @@ -FactoryGirl.define do +FactoryBot.define do factory :chat_team, class: ChatTeam do sequence(:team_id) { |n| "abcdefghijklm#{n}" } namespace factory: :group diff --git a/spec/factories/ci/build_trace_section_names.rb b/spec/factories/ci/build_trace_section_names.rb index 1c16225f0e5..ce07e716dde 100644 --- a/spec/factories/ci/build_trace_section_names.rb +++ b/spec/factories/ci/build_trace_section_names.rb @@ -1,4 +1,4 @@ -FactoryGirl.define do +FactoryBot.define do factory :ci_build_trace_section_name, class: Ci::BuildTraceSectionName do sequence(:name) { |n| "section_#{n}" } project factory: :project diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb index c868525cbc0..dc1d88c92dc 100644 --- a/spec/factories/ci/builds.rb +++ b/spec/factories/ci/builds.rb @@ -1,6 +1,6 @@ include ActionDispatch::TestProcess -FactoryGirl.define do +FactoryBot.define do factory :ci_build, class: Ci::Build do name 'test' stage 'test' diff --git a/spec/factories/ci/group_variables.rb b/spec/factories/ci/group_variables.rb index 565ced9eb1a..64716842b12 100644 --- a/spec/factories/ci/group_variables.rb +++ b/spec/factories/ci/group_variables.rb @@ -1,4 +1,4 @@ -FactoryGirl.define do +FactoryBot.define do factory :ci_group_variable, class: Ci::GroupVariable do sequence(:key) { |n| "VARIABLE_#{n}" } value 'VARIABLE_VALUE' diff --git a/spec/factories/ci/job_artifacts.rb b/spec/factories/ci/job_artifacts.rb index 538dc422832..46afba2953c 100644 --- a/spec/factories/ci/job_artifacts.rb +++ b/spec/factories/ci/job_artifacts.rb @@ -1,6 +1,6 @@ include ActionDispatch::TestProcess -FactoryGirl.define do +FactoryBot.define do factory :ci_job_artifact, class: Ci::JobArtifact do job factory: :ci_build file_type :archive diff --git a/spec/factories/ci/pipeline_schedule.rb b/spec/factories/ci/pipeline_schedule.rb index 564fef6833b..b2b79807429 100644 --- a/spec/factories/ci/pipeline_schedule.rb +++ b/spec/factories/ci/pipeline_schedule.rb @@ -1,4 +1,4 @@ -FactoryGirl.define do +FactoryBot.define do factory :ci_pipeline_schedule, class: Ci::PipelineSchedule do cron '0 1 * * *' cron_timezone Gitlab::Ci::CronParser::VALID_SYNTAX_SAMPLE_TIME_ZONE diff --git a/spec/factories/ci/pipeline_schedule_variables.rb b/spec/factories/ci/pipeline_schedule_variables.rb index ca64d1aada0..8d29118e310 100644 --- a/spec/factories/ci/pipeline_schedule_variables.rb +++ b/spec/factories/ci/pipeline_schedule_variables.rb @@ -1,4 +1,4 @@ -FactoryGirl.define do +FactoryBot.define do factory :ci_pipeline_schedule_variable, class: Ci::PipelineScheduleVariable do sequence(:key) { |n| "VARIABLE_#{n}" } value 'VARIABLE_VALUE' diff --git a/spec/factories/ci/pipeline_variables.rb b/spec/factories/ci/pipeline_variables.rb index 7c1a7faec08..b18055d7b3a 100644 --- a/spec/factories/ci/pipeline_variables.rb +++ b/spec/factories/ci/pipeline_variables.rb @@ -1,4 +1,4 @@ -FactoryGirl.define do +FactoryBot.define do factory :ci_pipeline_variable, class: Ci::PipelineVariable do sequence(:key) { |n| "VARIABLE_#{n}" } value 'VARIABLE_VALUE' diff --git a/spec/factories/ci/pipelines.rb b/spec/factories/ci/pipelines.rb index f994c2df821..51a767e5b93 100644 --- a/spec/factories/ci/pipelines.rb +++ b/spec/factories/ci/pipelines.rb @@ -1,4 +1,4 @@ -FactoryGirl.define do +FactoryBot.define do factory :ci_empty_pipeline, class: Ci::Pipeline do source :push ref 'master' diff --git a/spec/factories/ci/runner_projects.rb b/spec/factories/ci/runner_projects.rb index fa76d0ecd8c..f605e90ceed 100644 --- a/spec/factories/ci/runner_projects.rb +++ b/spec/factories/ci/runner_projects.rb @@ -1,4 +1,4 @@ -FactoryGirl.define do +FactoryBot.define do factory :ci_runner_project, class: Ci::RunnerProject do runner factory: :ci_runner project diff --git a/spec/factories/ci/runners.rb b/spec/factories/ci/runners.rb index 88bb755d068..34b8b246d0f 100644 --- a/spec/factories/ci/runners.rb +++ b/spec/factories/ci/runners.rb @@ -1,4 +1,4 @@ -FactoryGirl.define do +FactoryBot.define do factory :ci_runner, class: Ci::Runner do sequence(:description) { |n| "My runner#{n}" } diff --git a/spec/factories/ci/stages.rb b/spec/factories/ci/stages.rb index b2ded945738..25309033571 100644 --- a/spec/factories/ci/stages.rb +++ b/spec/factories/ci/stages.rb @@ -1,4 +1,4 @@ -FactoryGirl.define do +FactoryBot.define do factory :ci_stage, class: Ci::LegacyStage do skip_create diff --git a/spec/factories/ci/trigger_requests.rb b/spec/factories/ci/trigger_requests.rb index 40b8848920e..0e9fc3d0014 100644 --- a/spec/factories/ci/trigger_requests.rb +++ b/spec/factories/ci/trigger_requests.rb @@ -1,4 +1,4 @@ -FactoryGirl.define do +FactoryBot.define do factory :ci_trigger_request, class: Ci::TriggerRequest do trigger factory: :ci_trigger end diff --git a/spec/factories/ci/triggers.rb b/spec/factories/ci/triggers.rb index 3734c7040c0..742d9efba2d 100644 --- a/spec/factories/ci/triggers.rb +++ b/spec/factories/ci/triggers.rb @@ -1,4 +1,4 @@ -FactoryGirl.define do +FactoryBot.define do factory :ci_trigger_without_token, class: Ci::Trigger do owner diff --git a/spec/factories/ci/variables.rb b/spec/factories/ci/variables.rb index d8fd513ffcf..3d014b9b54f 100644 --- a/spec/factories/ci/variables.rb +++ b/spec/factories/ci/variables.rb @@ -1,4 +1,4 @@ -FactoryGirl.define do +FactoryBot.define do factory :ci_variable, class: Ci::Variable do sequence(:key) { |n| "VARIABLE_#{n}" } value 'VARIABLE_VALUE' diff --git a/spec/factories/clusters/applications/helm.rb b/spec/factories/clusters/applications/helm.rb index fab37195113..d82fa8e34aa 100644 --- a/spec/factories/clusters/applications/helm.rb +++ b/spec/factories/clusters/applications/helm.rb @@ -1,4 +1,4 @@ -FactoryGirl.define do +FactoryBot.define do factory :cluster_applications_helm, class: Clusters::Applications::Helm do cluster factory: %i(cluster provided_by_gcp) diff --git a/spec/factories/clusters/applications/ingress.rb b/spec/factories/clusters/applications/ingress.rb index b103a980655..85f54a9d744 100644 --- a/spec/factories/clusters/applications/ingress.rb +++ b/spec/factories/clusters/applications/ingress.rb @@ -1,4 +1,4 @@ -FactoryGirl.define do +FactoryBot.define do factory :cluster_applications_ingress, class: Clusters::Applications::Ingress do cluster factory: %i(cluster provided_by_gcp) diff --git a/spec/factories/clusters/clusters.rb b/spec/factories/clusters/clusters.rb index 9e73a19e856..20d5580f0c2 100644 --- a/spec/factories/clusters/clusters.rb +++ b/spec/factories/clusters/clusters.rb @@ -1,4 +1,4 @@ -FactoryGirl.define do +FactoryBot.define do factory :cluster, class: Clusters::Cluster do user name 'test-cluster' diff --git a/spec/factories/clusters/platforms/kubernetes.rb b/spec/factories/clusters/platforms/kubernetes.rb index 8b3e6ff35fa..89f6ddebf6a 100644 --- a/spec/factories/clusters/platforms/kubernetes.rb +++ b/spec/factories/clusters/platforms/kubernetes.rb @@ -1,4 +1,4 @@ -FactoryGirl.define do +FactoryBot.define do factory :cluster_platform_kubernetes, class: Clusters::Platforms::Kubernetes do cluster namespace nil diff --git a/spec/factories/clusters/providers/gcp.rb b/spec/factories/clusters/providers/gcp.rb index a815410512a..a002ab28519 100644 --- a/spec/factories/clusters/providers/gcp.rb +++ b/spec/factories/clusters/providers/gcp.rb @@ -1,4 +1,4 @@ -FactoryGirl.define do +FactoryBot.define do factory :cluster_provider_gcp, class: Clusters::Providers::Gcp do cluster gcp_project_id 'test-gcp-project' diff --git a/spec/factories/commit_statuses.rb b/spec/factories/commit_statuses.rb index abbe37df90e..ce5fbc343ee 100644 --- a/spec/factories/commit_statuses.rb +++ b/spec/factories/commit_statuses.rb @@ -1,4 +1,4 @@ -FactoryGirl.define do +FactoryBot.define do factory :commit_status, class: CommitStatus do name 'default' stage 'test' diff --git a/spec/factories/commits.rb b/spec/factories/commits.rb index f4f12a095fc..84a8bc56640 100644 --- a/spec/factories/commits.rb +++ b/spec/factories/commits.rb @@ -1,16 +1,29 @@ require_relative '../support/repo_helpers' -FactoryGirl.define do +FactoryBot.define do factory :commit do - git_commit RepoHelpers.sample_commit + transient do + author nil + end + + git_commit do + commit = RepoHelpers.sample_commit + + if author + commit.author_email = author.email + commit.author_name = author.name + end + + commit + end project initialize_with do new(git_commit, project) end - after(:build) do |commit| - allow(commit).to receive(:author).and_return build(:author) + after(:build) do |commit, evaluator| + allow(commit).to receive(:author).and_return(evaluator.author || build(:author)) end trait :without_author do diff --git a/spec/factories/container_repositories.rb b/spec/factories/container_repositories.rb index 3fcad9fd4b3..62a89a12ef5 100644 --- a/spec/factories/container_repositories.rb +++ b/spec/factories/container_repositories.rb @@ -1,4 +1,4 @@ -FactoryGirl.define do +FactoryBot.define do factory :container_repository do name 'test_container_image' project diff --git a/spec/factories/conversational_development_index_metrics.rb b/spec/factories/conversational_development_index_metrics.rb index 3806c43ba15..abf37fb861e 100644 --- a/spec/factories/conversational_development_index_metrics.rb +++ b/spec/factories/conversational_development_index_metrics.rb @@ -1,4 +1,4 @@ -FactoryGirl.define do +FactoryBot.define do factory :conversational_development_index_metric, class: ConversationalDevelopmentIndex::Metric do leader_issues 9.256 instance_issues 1.234 diff --git a/spec/factories/deploy_keys_projects.rb b/spec/factories/deploy_keys_projects.rb index 27cece487bd..30a6d468ed3 100644 --- a/spec/factories/deploy_keys_projects.rb +++ b/spec/factories/deploy_keys_projects.rb @@ -1,4 +1,4 @@ -FactoryGirl.define do +FactoryBot.define do factory :deploy_keys_project do deploy_key project diff --git a/spec/factories/deployments.rb b/spec/factories/deployments.rb index 0dd1238d6e2..9d7d5e56611 100644 --- a/spec/factories/deployments.rb +++ b/spec/factories/deployments.rb @@ -1,4 +1,4 @@ -FactoryGirl.define do +FactoryBot.define do factory :deployment, class: Deployment do sha '97de212e80737a608d939f648d959671fb0a0142' ref 'master' diff --git a/spec/factories/emails.rb b/spec/factories/emails.rb index c9ab87a15aa..4dc7961060a 100644 --- a/spec/factories/emails.rb +++ b/spec/factories/emails.rb @@ -1,4 +1,4 @@ -FactoryGirl.define do +FactoryBot.define do factory :email do user email { generate(:email_alias) } diff --git a/spec/factories/environments.rb b/spec/factories/environments.rb index 9034476d094..b5db57d5148 100644 --- a/spec/factories/environments.rb +++ b/spec/factories/environments.rb @@ -1,4 +1,4 @@ -FactoryGirl.define do +FactoryBot.define do factory :environment, class: Environment do sequence(:name) { |n| "environment#{n}" } diff --git a/spec/factories/events.rb b/spec/factories/events.rb index ad9f7e2caef..ed275243ac9 100644 --- a/spec/factories/events.rb +++ b/spec/factories/events.rb @@ -1,4 +1,4 @@ -FactoryGirl.define do +FactoryBot.define do factory :event do project author factory: :user diff --git a/spec/factories/file_uploaders.rb b/spec/factories/file_uploaders.rb index 622571390d2..8404985bfea 100644 --- a/spec/factories/file_uploaders.rb +++ b/spec/factories/file_uploaders.rb @@ -1,4 +1,4 @@ -FactoryGirl.define do +FactoryBot.define do factory :file_uploader do skip_create diff --git a/spec/factories/fork_network_members.rb b/spec/factories/fork_network_members.rb index 509c4e1fa1c..850e3f158f1 100644 --- a/spec/factories/fork_network_members.rb +++ b/spec/factories/fork_network_members.rb @@ -1,4 +1,4 @@ -FactoryGirl.define do +FactoryBot.define do factory :fork_network_member do association :project association :fork_network diff --git a/spec/factories/fork_networks.rb b/spec/factories/fork_networks.rb index f42d36f3d19..813b1943eb2 100644 --- a/spec/factories/fork_networks.rb +++ b/spec/factories/fork_networks.rb @@ -1,4 +1,4 @@ -FactoryGirl.define do +FactoryBot.define do factory :fork_network do association :root_project, factory: :project end diff --git a/spec/factories/forked_project_links.rb b/spec/factories/forked_project_links.rb index 9b34651a4ae..bc59fea81ec 100644 --- a/spec/factories/forked_project_links.rb +++ b/spec/factories/forked_project_links.rb @@ -1,4 +1,4 @@ -FactoryGirl.define do +FactoryBot.define do factory :forked_project_link do association :forked_to_project, factory: [:project, :repository] association :forked_from_project, factory: [:project, :repository] diff --git a/spec/factories/gitaly/commit.rb b/spec/factories/gitaly/commit.rb index e7966cee77b..5034c3d0e48 100644 --- a/spec/factories/gitaly/commit.rb +++ b/spec/factories/gitaly/commit.rb @@ -1,4 +1,4 @@ -FactoryGirl.define do +FactoryBot.define do sequence(:gitaly_commit_id) { Digest::SHA1.hexdigest(Time.now.to_f.to_s) } factory :gitaly_commit, class: Gitaly::GitCommit do diff --git a/spec/factories/gitaly/commit_author.rb b/spec/factories/gitaly/commit_author.rb index 341873a2002..aaf634ce08b 100644 --- a/spec/factories/gitaly/commit_author.rb +++ b/spec/factories/gitaly/commit_author.rb @@ -1,4 +1,4 @@ -FactoryGirl.define do +FactoryBot.define do factory :gitaly_commit_author, class: Gitaly::CommitAuthor do skip_create diff --git a/spec/factories/gpg_key_subkeys.rb b/spec/factories/gpg_key_subkeys.rb index 66ecb44d84b..57eaaee345f 100644 --- a/spec/factories/gpg_key_subkeys.rb +++ b/spec/factories/gpg_key_subkeys.rb @@ -1,6 +1,6 @@ require_relative '../support/gpg_helpers' -FactoryGirl.define do +FactoryBot.define do factory :gpg_key_subkey do gpg_key diff --git a/spec/factories/gpg_keys.rb b/spec/factories/gpg_keys.rb index 93218e5763e..b8aabf74221 100644 --- a/spec/factories/gpg_keys.rb +++ b/spec/factories/gpg_keys.rb @@ -1,6 +1,6 @@ require_relative '../support/gpg_helpers' -FactoryGirl.define do +FactoryBot.define do factory :gpg_key do key GpgHelpers::User1.public_key user diff --git a/spec/factories/gpg_signature.rb b/spec/factories/gpg_signature.rb index e9798ff6a41..4620caff823 100644 --- a/spec/factories/gpg_signature.rb +++ b/spec/factories/gpg_signature.rb @@ -1,6 +1,6 @@ require_relative '../support/gpg_helpers' -FactoryGirl.define do +FactoryBot.define do factory :gpg_signature do commit_sha { Digest::SHA1.hexdigest(SecureRandom.hex) } project diff --git a/spec/factories/group_custom_attributes.rb b/spec/factories/group_custom_attributes.rb index 7ff5f376e8b..d2f45d5d3ce 100644 --- a/spec/factories/group_custom_attributes.rb +++ b/spec/factories/group_custom_attributes.rb @@ -1,4 +1,4 @@ -FactoryGirl.define do +FactoryBot.define do factory :group_custom_attribute do group sequence(:key) { |n| "key#{n}" } diff --git a/spec/factories/group_members.rb b/spec/factories/group_members.rb index 32cbfe28a60..1c2214e9481 100644 --- a/spec/factories/group_members.rb +++ b/spec/factories/group_members.rb @@ -1,4 +1,4 @@ -FactoryGirl.define do +FactoryBot.define do factory :group_member do access_level { GroupMember::OWNER } group diff --git a/spec/factories/groups.rb b/spec/factories/groups.rb index 52f76b094a3..1512f5a0e58 100644 --- a/spec/factories/groups.rb +++ b/spec/factories/groups.rb @@ -1,4 +1,4 @@ -FactoryGirl.define do +FactoryBot.define do factory :group, class: Group, parent: :namespace do sequence(:name) { |n| "group#{n}" } path { name.downcase.gsub(/\s/, '_') } diff --git a/spec/factories/identities.rb b/spec/factories/identities.rb index 26ef6f18698..122d0d42938 100644 --- a/spec/factories/identities.rb +++ b/spec/factories/identities.rb @@ -1,4 +1,4 @@ -FactoryGirl.define do +FactoryBot.define do factory :identity do provider 'ldapmain' extern_uid 'my-ldap-id' diff --git a/spec/factories/instance_configuration.rb b/spec/factories/instance_configuration.rb index 406c7c3caf1..31866a9c221 100644 --- a/spec/factories/instance_configuration.rb +++ b/spec/factories/instance_configuration.rb @@ -1,4 +1,4 @@ -FactoryGirl.define do +FactoryBot.define do factory :instance_configuration do skip_create end diff --git a/spec/factories/issues.rb b/spec/factories/issues.rb index 7c3b80198f9..5ed6b017dee 100644 --- a/spec/factories/issues.rb +++ b/spec/factories/issues.rb @@ -1,4 +1,4 @@ -FactoryGirl.define do +FactoryBot.define do factory :issue do title { generate(:title) } author diff --git a/spec/factories/keys.rb b/spec/factories/keys.rb index 3f7c794b14a..e6eb76f71d3 100644 --- a/spec/factories/keys.rb +++ b/spec/factories/keys.rb @@ -1,6 +1,6 @@ require_relative '../support/helpers/key_generator_helper' -FactoryGirl.define do +FactoryBot.define do factory :key do title key { Spec::Support::Helpers::KeyGeneratorHelper.new(1024).generate + ' dummy@gitlab.com' } diff --git a/spec/factories/label_links.rb b/spec/factories/label_links.rb index 3580174e873..007847d9cf4 100644 --- a/spec/factories/label_links.rb +++ b/spec/factories/label_links.rb @@ -1,4 +1,4 @@ -FactoryGirl.define do +FactoryBot.define do factory :label_link do label target factory: :issue diff --git a/spec/factories/label_priorities.rb b/spec/factories/label_priorities.rb index 7430466fc57..c4824faad53 100644 --- a/spec/factories/label_priorities.rb +++ b/spec/factories/label_priorities.rb @@ -1,4 +1,4 @@ -FactoryGirl.define do +FactoryBot.define do factory :label_priority do project label diff --git a/spec/factories/labels.rb b/spec/factories/labels.rb index 416317d677b..f759b6d499d 100644 --- a/spec/factories/labels.rb +++ b/spec/factories/labels.rb @@ -1,4 +1,4 @@ -FactoryGirl.define do +FactoryBot.define do trait :base_label do title { generate(:label_title) } color "#990000" diff --git a/spec/factories/lfs_objects.rb b/spec/factories/lfs_objects.rb index 477fab9e964..8eb709022ce 100644 --- a/spec/factories/lfs_objects.rb +++ b/spec/factories/lfs_objects.rb @@ -1,6 +1,6 @@ include ActionDispatch::TestProcess -FactoryGirl.define do +FactoryBot.define do factory :lfs_object do sequence(:oid) { |n| "b68143e6463773b1b6c6fd009a76c32aeec041faff32ba2ed42fd7f708a%05x" % n } size 499013 diff --git a/spec/factories/lfs_objects_projects.rb b/spec/factories/lfs_objects_projects.rb index 1ed0355c8e4..c225387a5de 100644 --- a/spec/factories/lfs_objects_projects.rb +++ b/spec/factories/lfs_objects_projects.rb @@ -1,4 +1,4 @@ -FactoryGirl.define do +FactoryBot.define do factory :lfs_objects_project do lfs_object project diff --git a/spec/factories/lists.rb b/spec/factories/lists.rb index 48142d3c49b..210c58b21e9 100644 --- a/spec/factories/lists.rb +++ b/spec/factories/lists.rb @@ -1,4 +1,4 @@ -FactoryGirl.define do +FactoryBot.define do factory :list do board label diff --git a/spec/factories/merge_requests.rb b/spec/factories/merge_requests.rb index cc6cef63b47..40558c88d15 100644 --- a/spec/factories/merge_requests.rb +++ b/spec/factories/merge_requests.rb @@ -1,4 +1,4 @@ -FactoryGirl.define do +FactoryBot.define do factory :merge_request do title { generate(:title) } author diff --git a/spec/factories/merge_requests_closing_issues.rb b/spec/factories/merge_requests_closing_issues.rb index fdbdc00cad7..ee0606a72b6 100644 --- a/spec/factories/merge_requests_closing_issues.rb +++ b/spec/factories/merge_requests_closing_issues.rb @@ -1,4 +1,4 @@ -FactoryGirl.define do +FactoryBot.define do factory :merge_requests_closing_issues do issue merge_request diff --git a/spec/factories/milestones.rb b/spec/factories/milestones.rb index b5298b2f969..f95632e7187 100644 --- a/spec/factories/milestones.rb +++ b/spec/factories/milestones.rb @@ -1,4 +1,4 @@ -FactoryGirl.define do +FactoryBot.define do factory :milestone do title diff --git a/spec/factories/namespaces.rb b/spec/factories/namespaces.rb index 1b1fc4ce80d..f94b09cff15 100644 --- a/spec/factories/namespaces.rb +++ b/spec/factories/namespaces.rb @@ -1,4 +1,4 @@ -FactoryGirl.define do +FactoryBot.define do factory :namespace do sequence(:name) { |n| "namespace#{n}" } path { name.downcase.gsub(/\s/, '_') } diff --git a/spec/factories/notes.rb b/spec/factories/notes.rb index 471bfb3213a..707ecbd6be5 100644 --- a/spec/factories/notes.rb +++ b/spec/factories/notes.rb @@ -2,7 +2,7 @@ require_relative '../support/repo_helpers' include ActionDispatch::TestProcess -FactoryGirl.define do +FactoryBot.define do factory :note do project note { generate(:title) } diff --git a/spec/factories/notification_settings.rb b/spec/factories/notification_settings.rb index e9171528d86..5116ef33f5d 100644 --- a/spec/factories/notification_settings.rb +++ b/spec/factories/notification_settings.rb @@ -1,4 +1,4 @@ -FactoryGirl.define do +FactoryBot.define do factory :notification_setting do source factory: :project user diff --git a/spec/factories/oauth_access_grants.rb b/spec/factories/oauth_access_grants.rb index 543b3e99274..9e6af24c4eb 100644 --- a/spec/factories/oauth_access_grants.rb +++ b/spec/factories/oauth_access_grants.rb @@ -1,4 +1,4 @@ -FactoryGirl.define do +FactoryBot.define do factory :oauth_access_grant do resource_owner_id { create(:user).id } application diff --git a/spec/factories/oauth_access_tokens.rb b/spec/factories/oauth_access_tokens.rb index a46bc1d8ce8..eabfd6cd830 100644 --- a/spec/factories/oauth_access_tokens.rb +++ b/spec/factories/oauth_access_tokens.rb @@ -1,4 +1,4 @@ -FactoryGirl.define do +FactoryBot.define do factory :oauth_access_token do resource_owner application diff --git a/spec/factories/oauth_applications.rb b/spec/factories/oauth_applications.rb index c7ede40f240..4427da1d6c7 100644 --- a/spec/factories/oauth_applications.rb +++ b/spec/factories/oauth_applications.rb @@ -1,4 +1,4 @@ -FactoryGirl.define do +FactoryBot.define do factory :oauth_application, class: 'Doorkeeper::Application', aliases: [:application] do sequence(:name) { |n| "OAuth App #{n}" } uid { Doorkeeper::OAuth::Helpers::UniqueToken.generate } diff --git a/spec/factories/pages_domains.rb b/spec/factories/pages_domains.rb index 6d2e45f41ba..61b04708da2 100644 --- a/spec/factories/pages_domains.rb +++ b/spec/factories/pages_domains.rb @@ -1,4 +1,4 @@ -FactoryGirl.define do +FactoryBot.define do factory :pages_domain, class: 'PagesDomain' do domain 'my.domain.com' diff --git a/spec/factories/personal_access_tokens.rb b/spec/factories/personal_access_tokens.rb index 06acaff6cd0..1b12f84d7b8 100644 --- a/spec/factories/personal_access_tokens.rb +++ b/spec/factories/personal_access_tokens.rb @@ -1,4 +1,4 @@ -FactoryGirl.define do +FactoryBot.define do factory :personal_access_token do user token { SecureRandom.hex(50) } diff --git a/spec/factories/project_auto_devops.rb b/spec/factories/project_auto_devops.rb index 8d124dc2381..5ce1988c76f 100644 --- a/spec/factories/project_auto_devops.rb +++ b/spec/factories/project_auto_devops.rb @@ -1,4 +1,4 @@ -FactoryGirl.define do +FactoryBot.define do factory :project_auto_devops do project enabled true diff --git a/spec/factories/project_custom_attributes.rb b/spec/factories/project_custom_attributes.rb index 5eedeb86304..099d2d7ff19 100644 --- a/spec/factories/project_custom_attributes.rb +++ b/spec/factories/project_custom_attributes.rb @@ -1,4 +1,4 @@ -FactoryGirl.define do +FactoryBot.define do factory :project_custom_attribute do project sequence(:key) { |n| "key#{n}" } diff --git a/spec/factories/project_group_links.rb b/spec/factories/project_group_links.rb index e73cc05f9d7..d5ace9425a0 100644 --- a/spec/factories/project_group_links.rb +++ b/spec/factories/project_group_links.rb @@ -1,4 +1,4 @@ -FactoryGirl.define do +FactoryBot.define do factory :project_group_link do project group diff --git a/spec/factories/project_hooks.rb b/spec/factories/project_hooks.rb index accae636a3a..493b7bc021c 100644 --- a/spec/factories/project_hooks.rb +++ b/spec/factories/project_hooks.rb @@ -1,4 +1,4 @@ -FactoryGirl.define do +FactoryBot.define do factory :project_hook do url { generate(:url) } enable_ssl_verification false diff --git a/spec/factories/project_members.rb b/spec/factories/project_members.rb index 9cf3a1e8e8a..4260f52498d 100644 --- a/spec/factories/project_members.rb +++ b/spec/factories/project_members.rb @@ -1,4 +1,4 @@ -FactoryGirl.define do +FactoryBot.define do factory :project_member do user project diff --git a/spec/factories/project_statistics.rb b/spec/factories/project_statistics.rb index 6c2ed7c6581..2d0f698475d 100644 --- a/spec/factories/project_statistics.rb +++ b/spec/factories/project_statistics.rb @@ -1,4 +1,4 @@ -FactoryGirl.define do +FactoryBot.define do factory :project_statistics do project diff --git a/spec/factories/project_wikis.rb b/spec/factories/project_wikis.rb index 38fcab7466d..89d8248f9f4 100644 --- a/spec/factories/project_wikis.rb +++ b/spec/factories/project_wikis.rb @@ -1,4 +1,4 @@ -FactoryGirl.define do +FactoryBot.define do factory :project_wiki do skip_create diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb index 4034e7905ad..d0f3911f730 100644 --- a/spec/factories/projects.rb +++ b/spec/factories/projects.rb @@ -1,6 +1,6 @@ require_relative '../support/test_env' -FactoryGirl.define do +FactoryBot.define do # Project without repository # # Project does not have bare repository. diff --git a/spec/factories/protected_branches.rb b/spec/factories/protected_branches.rb index fe0cbfc4444..39460834d06 100644 --- a/spec/factories/protected_branches.rb +++ b/spec/factories/protected_branches.rb @@ -1,4 +1,4 @@ -FactoryGirl.define do +FactoryBot.define do factory :protected_branch do name project diff --git a/spec/factories/protected_tags.rb b/spec/factories/protected_tags.rb index 225588b23cc..df9c8b3cb63 100644 --- a/spec/factories/protected_tags.rb +++ b/spec/factories/protected_tags.rb @@ -1,4 +1,4 @@ -FactoryGirl.define do +FactoryBot.define do factory :protected_tag do name project diff --git a/spec/factories/releases.rb b/spec/factories/releases.rb index 74497dc82c0..d80c65cf8bb 100644 --- a/spec/factories/releases.rb +++ b/spec/factories/releases.rb @@ -1,4 +1,4 @@ -FactoryGirl.define do +FactoryBot.define do factory :release do tag "v1.1.0" description "Awesome release" diff --git a/spec/factories/sent_notifications.rb b/spec/factories/sent_notifications.rb index c2febf5b438..80872067233 100644 --- a/spec/factories/sent_notifications.rb +++ b/spec/factories/sent_notifications.rb @@ -1,4 +1,4 @@ -FactoryGirl.define do +FactoryBot.define do factory :sent_notification do project recipient factory: :user diff --git a/spec/factories/sequences.rb b/spec/factories/sequences.rb index c0232ba5bf6..f2b6e7a11f9 100644 --- a/spec/factories/sequences.rb +++ b/spec/factories/sequences.rb @@ -1,4 +1,4 @@ -FactoryGirl.define do +FactoryBot.define do sequence(:username) { |n| "user#{n}" } sequence(:name) { |n| "John Doe#{n}" } sequence(:email) { |n| "user#{n}@example.org" } diff --git a/spec/factories/service_hooks.rb b/spec/factories/service_hooks.rb index e3f88ab8fcc..c907862b4f6 100644 --- a/spec/factories/service_hooks.rb +++ b/spec/factories/service_hooks.rb @@ -1,4 +1,4 @@ -FactoryGirl.define do +FactoryBot.define do factory :service_hook do url { generate(:url) } service diff --git a/spec/factories/services.rb b/spec/factories/services.rb index ccf63f3ffa4..4b0377967c7 100644 --- a/spec/factories/services.rb +++ b/spec/factories/services.rb @@ -1,4 +1,4 @@ -FactoryGirl.define do +FactoryBot.define do factory :service do project type 'Service' diff --git a/spec/factories/snippets.rb b/spec/factories/snippets.rb index 075bccd7f94..2ab9a56d255 100644 --- a/spec/factories/snippets.rb +++ b/spec/factories/snippets.rb @@ -1,4 +1,4 @@ -FactoryGirl.define do +FactoryBot.define do factory :snippet do author title { generate(:title) } diff --git a/spec/factories/spam_logs.rb b/spec/factories/spam_logs.rb index e369f9f13e9..a467f850a80 100644 --- a/spec/factories/spam_logs.rb +++ b/spec/factories/spam_logs.rb @@ -1,4 +1,4 @@ -FactoryGirl.define do +FactoryBot.define do factory :spam_log do user sequence(:source_ip) { |n| "42.42.42.#{n % 255}" } diff --git a/spec/factories/subscriptions.rb b/spec/factories/subscriptions.rb index 1ae7fc9f384..a4bc4e87b0a 100644 --- a/spec/factories/subscriptions.rb +++ b/spec/factories/subscriptions.rb @@ -1,4 +1,4 @@ -FactoryGirl.define do +FactoryBot.define do factory :subscription do user project diff --git a/spec/factories/system_hooks.rb b/spec/factories/system_hooks.rb index 841e1e293e8..9e00eeb6ef1 100644 --- a/spec/factories/system_hooks.rb +++ b/spec/factories/system_hooks.rb @@ -1,4 +1,4 @@ -FactoryGirl.define do +FactoryBot.define do factory :system_hook do url { generate(:url) } end diff --git a/spec/factories/system_note_metadata.rb b/spec/factories/system_note_metadata.rb index f487a2d7e4a..e913068da40 100644 --- a/spec/factories/system_note_metadata.rb +++ b/spec/factories/system_note_metadata.rb @@ -1,4 +1,4 @@ -FactoryGirl.define do +FactoryBot.define do factory :system_note_metadata do note action 'merge' diff --git a/spec/factories/timelogs.rb b/spec/factories/timelogs.rb index 6f1545418eb..af34b0681e2 100644 --- a/spec/factories/timelogs.rb +++ b/spec/factories/timelogs.rb @@ -1,6 +1,6 @@ -# Read about factories at https://github.com/thoughtbot/factory_girl +# Read about factories at https://github.com/thoughtbot/factory_bot -FactoryGirl.define do +FactoryBot.define do factory :timelog do time_spent 3600 user diff --git a/spec/factories/todos.rb b/spec/factories/todos.rb index 4975befbfe3..6a6de665dd1 100644 --- a/spec/factories/todos.rb +++ b/spec/factories/todos.rb @@ -1,4 +1,4 @@ -FactoryGirl.define do +FactoryBot.define do factory :todo do project author diff --git a/spec/factories/trending_project.rb b/spec/factories/trending_project.rb index 246176611dc..f7c634fd21f 100644 --- a/spec/factories/trending_project.rb +++ b/spec/factories/trending_project.rb @@ -1,4 +1,4 @@ -FactoryGirl.define do +FactoryBot.define do # TrendingProject factory :trending_project, class: 'TrendingProject' do project diff --git a/spec/factories/u2f_registrations.rb b/spec/factories/u2f_registrations.rb index df92b079581..26090b08966 100644 --- a/spec/factories/u2f_registrations.rb +++ b/spec/factories/u2f_registrations.rb @@ -1,4 +1,4 @@ -FactoryGirl.define do +FactoryBot.define do factory :u2f_registration do certificate { FFaker::BaconIpsum.characters(728) } key_handle { FFaker::BaconIpsum.characters(86) } diff --git a/spec/factories/uploads.rb b/spec/factories/uploads.rb index e18f1a6bd4a..c39500faea1 100644 --- a/spec/factories/uploads.rb +++ b/spec/factories/uploads.rb @@ -1,4 +1,4 @@ -FactoryGirl.define do +FactoryBot.define do factory :upload do model { build(:project) } path { "uploads/-/system/project/avatar/avatar.jpg" } diff --git a/spec/factories/user_agent_details.rb b/spec/factories/user_agent_details.rb index 9763cc0cf15..7183a8e1140 100644 --- a/spec/factories/user_agent_details.rb +++ b/spec/factories/user_agent_details.rb @@ -1,4 +1,4 @@ -FactoryGirl.define do +FactoryBot.define do factory :user_agent_detail do ip_address '127.0.0.1' user_agent 'AppleWebKit/537.36' diff --git a/spec/factories/user_custom_attributes.rb b/spec/factories/user_custom_attributes.rb index 278cf290d4f..a184a2e0f17 100644 --- a/spec/factories/user_custom_attributes.rb +++ b/spec/factories/user_custom_attributes.rb @@ -1,4 +1,4 @@ -FactoryGirl.define do +FactoryBot.define do factory :user_custom_attribute do user sequence(:key) { |n| "key#{n}" } diff --git a/spec/factories/users.rb b/spec/factories/users.rb index 4000cd085b7..e62e0b263ca 100644 --- a/spec/factories/users.rb +++ b/spec/factories/users.rb @@ -1,4 +1,4 @@ -FactoryGirl.define do +FactoryBot.define do factory :user, aliases: [:author, :assignee, :recipient, :owner, :resource_owner] do email { generate(:email) } name { generate(:name) } @@ -58,6 +58,10 @@ FactoryGirl.define do end end + trait :readme do + project_view :readme + end + factory :omniauth_user do transient do extern_uid '123456' diff --git a/spec/factories/web_hook_log.rb b/spec/factories/web_hook_log.rb index 230b3f6b26e..17837260a4b 100644 --- a/spec/factories/web_hook_log.rb +++ b/spec/factories/web_hook_log.rb @@ -1,4 +1,4 @@ -FactoryGirl.define do +FactoryBot.define do factory :web_hook_log do web_hook factory: :project_hook trigger 'push_hooks' diff --git a/spec/factories/wiki_directories.rb b/spec/factories/wiki_directories.rb index 3b4cfc380b8..b105c82b19d 100644 --- a/spec/factories/wiki_directories.rb +++ b/spec/factories/wiki_directories.rb @@ -1,4 +1,4 @@ -FactoryGirl.define do +FactoryBot.define do factory :wiki_directory do skip_create diff --git a/spec/factories/wiki_pages.rb b/spec/factories/wiki_pages.rb index 4105f59e289..2335b5118dd 100644 --- a/spec/factories/wiki_pages.rb +++ b/spec/factories/wiki_pages.rb @@ -1,6 +1,6 @@ require 'ostruct' -FactoryGirl.define do +FactoryBot.define do factory :wiki_page do transient do attrs do diff --git a/spec/factories_spec.rb b/spec/factories_spec.rb index 09b3c0b0994..66b71d0f556 100644 --- a/spec/factories_spec.rb +++ b/spec/factories_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe 'factories' do - FactoryGirl.factories.each do |factory| + FactoryBot.factories.each do |factory| describe "#{factory.name} factory" do it 'does not raise error when built' do expect { build(factory.name) }.not_to raise_error diff --git a/spec/features/admin/admin_health_check_spec.rb b/spec/features/admin/admin_health_check_spec.rb index 4430fc15501..ac3392b49f9 100644 --- a/spec/features/admin/admin_health_check_spec.rb +++ b/spec/features/admin/admin_health_check_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -feature "Admin Health Check", :feature, :broken_storage do +feature "Admin Health Check", :feature do include StubENV before do @@ -36,6 +36,7 @@ feature "Admin Health Check", :feature, :broken_storage do context 'when services are up' do before do + stub_storage_settings({}) # Hide the broken storage visit admin_health_check_path end @@ -56,10 +57,8 @@ feature "Admin Health Check", :feature, :broken_storage do end end - context 'with repository storage failures' do + context 'with repository storage failures', :broken_storage do before do - # Track a failure - Gitlab::Git::Storage::CircuitBreaker.for_storage('broken').perform { nil } rescue nil visit admin_health_check_path end @@ -67,9 +66,10 @@ feature "Admin Health Check", :feature, :broken_storage do hostname = Gitlab::Environment.hostname maximum_failures = Gitlab::CurrentSettings.current_application_settings .circuitbreaker_failure_count_threshold + number_of_failures = maximum_failures + 1 - expect(page).to have_content('broken: failed storage access attempt on host:') - expect(page).to have_content("#{hostname}: 1 of #{maximum_failures} failures.") + expect(page).to have_content("broken: #{number_of_failures} failed storage access attempts:") + expect(page).to have_content("#{hostname}: #{number_of_failures} of #{maximum_failures} failures.") end it 'allows resetting storage failures' do diff --git a/spec/features/admin/admin_runners_spec.rb b/spec/features/admin/admin_runners_spec.rb index e3bb16af38a..c1c54177167 100644 --- a/spec/features/admin/admin_runners_spec.rb +++ b/spec/features/admin/admin_runners_spec.rb @@ -13,8 +13,8 @@ describe "Admin Runners" do context "when there are runners" do before do - runner = FactoryGirl.create(:ci_runner, contacted_at: Time.now) - FactoryGirl.create(:ci_build, pipeline: pipeline, runner_id: runner.id) + runner = FactoryBot.create(:ci_runner, contacted_at: Time.now) + FactoryBot.create(:ci_build, pipeline: pipeline, runner_id: runner.id) visit admin_runners_path end @@ -25,8 +25,8 @@ describe "Admin Runners" do describe 'search' do before do - FactoryGirl.create :ci_runner, description: 'runner-foo' - FactoryGirl.create :ci_runner, description: 'runner-bar' + FactoryBot.create :ci_runner, description: 'runner-foo' + FactoryBot.create :ci_runner, description: 'runner-bar' end it 'shows correct runner when description matches' do @@ -62,11 +62,11 @@ describe "Admin Runners" do end describe "Runner show page" do - let(:runner) { FactoryGirl.create :ci_runner } + let(:runner) { FactoryBot.create :ci_runner } before do - @project1 = FactoryGirl.create(:project) - @project2 = FactoryGirl.create(:project) + @project1 = FactoryBot.create(:project) + @project2 = FactoryBot.create(:project) visit admin_runner_path(runner) end diff --git a/spec/features/admin/admin_system_info_spec.rb b/spec/features/admin/admin_system_info_spec.rb index 1fd1cda694a..5a989319d5b 100644 --- a/spec/features/admin/admin_system_info_spec.rb +++ b/spec/features/admin/admin_system_info_spec.rb @@ -18,8 +18,8 @@ describe 'Admin System Info' do it 'shows system info page' do expect(page).to have_content 'CPU 2 cores' - expect(page).to have_content 'Memory 4 GB / 16 GB' - expect(page).to have_content 'Disks' + expect(page).to have_content 'Memory Usage 4 GB / 16 GB' + expect(page).to have_content 'Disk Usage' expect(page).to have_content 'Uptime' end end @@ -33,8 +33,8 @@ describe 'Admin System Info' do it 'shows system info page with no CPU info' do expect(page).to have_content 'CPU Unable to collect CPU info' - expect(page).to have_content 'Memory 4 GB / 16 GB' - expect(page).to have_content 'Disks' + expect(page).to have_content 'Memory Usage 4 GB / 16 GB' + expect(page).to have_content 'Disk Usage' expect(page).to have_content 'Uptime' end end @@ -48,8 +48,8 @@ describe 'Admin System Info' do it 'shows system info page with no CPU info' do expect(page).to have_content 'CPU 2 cores' - expect(page).to have_content 'Memory Unable to collect memory info' - expect(page).to have_content 'Disks' + expect(page).to have_content 'Memory Usage Unable to collect memory info' + expect(page).to have_content 'Disk Usage' expect(page).to have_content 'Uptime' end end diff --git a/spec/features/commits_spec.rb b/spec/features/commits_spec.rb index c870910c8ea..77dcdf89f37 100644 --- a/spec/features/commits_spec.rb +++ b/spec/features/commits_spec.rb @@ -197,7 +197,7 @@ describe 'Commits' do commits = project.repository.commits(branch_name) commits.each do |commit| - expect(page).to have_content("committed #{commit.committed_date.strftime("%b %d, %Y")}") + expect(page).to have_content("authored #{commit.authored_date.strftime("%b %d, %Y")}") end end diff --git a/spec/features/dashboard/todos/todos_spec.rb b/spec/features/dashboard/todos/todos_spec.rb index 6f916078b1a..94133c62b5c 100644 --- a/spec/features/dashboard/todos/todos_spec.rb +++ b/spec/features/dashboard/todos/todos_spec.rb @@ -13,7 +13,7 @@ feature 'Dashboard Todos' do end it 'shows "All done" message' do - expect(page).to have_content 'Todos let you see what you should do next.' + expect(page).to have_content 'Todos let you see what you should do next' end end diff --git a/spec/features/group_variables_spec.rb b/spec/features/group_variables_spec.rb index d2d0be35f1c..e9b375f4c94 100644 --- a/spec/features/group_variables_spec.rb +++ b/spec/features/group_variables_spec.rb @@ -24,7 +24,7 @@ feature 'Group variables', :js do expect(find(".variable-value")).to have_content('******') expect(find(".variable-protected")).to have_content('Yes') end - click_on 'Reveal Values' + click_on 'Reveal value' page.within('.variables-table') do expect(find(".variable-value")).to have_content('AAA123') end diff --git a/spec/features/groups/labels/user_sees_links_to_issuables.rb b/spec/features/groups/labels/user_sees_links_to_issuables.rb new file mode 100644 index 00000000000..5d6290d2109 --- /dev/null +++ b/spec/features/groups/labels/user_sees_links_to_issuables.rb @@ -0,0 +1,15 @@ +require 'spec_helper' + +feature 'Groups > Labels > User sees links to issuables' do + set(:group) { create(:group, :public) } + + before do + create(:group_label, group: group, title: 'bug') + visit group_labels_path(group) + end + + scenario 'shows links to MRs and issues' do + expect(page).to have_link('view merge requests') + expect(page).to have_link('view open issues') + end +end diff --git a/spec/features/groups/members/manage_members.rb b/spec/features/groups/members/manage_members.rb index da1e17225db..21f7b4999ad 100644 --- a/spec/features/groups/members/manage_members.rb +++ b/spec/features/groups/members/manage_members.rb @@ -38,6 +38,27 @@ feature 'Groups > Members > Manage members' do end end + scenario 'do not disclose email addresses', :js do + group.add_owner(user1) + create(:user, email: 'undisclosed_email@gitlab.com', name: "Jane 'invisible' Doe") + + visit group_group_members_path(group) + + find('.select2-container').click + select_input = find('.select2-input') + + select_input.send_keys('@gitlab.com') + wait_for_requests + + expect(page).to have_content('No matches found') + + select_input.native.clear + select_input.send_keys('undisclosed_email@gitlab.com') + wait_for_requests + + expect(page).to have_content("Jane 'invisible' Doe") + end + scenario 'remove user from group', :js do group.add_owner(user1) group.add_developer(user2) diff --git a/spec/features/help_pages_spec.rb b/spec/features/help_pages_spec.rb index 93be3b066ee..ab896a310be 100644 --- a/spec/features/help_pages_spec.rb +++ b/spec/features/help_pages_spec.rb @@ -37,7 +37,7 @@ describe 'Help Pages' do context 'in a production environment with version check enabled', :js do before do allow(Rails.env).to receive(:production?) { true } - allow_any_instance_of(ApplicationSetting).to receive(:version_check_enabled) { true } + stub_application_setting(version_check_enabled: true) allow_any_instance_of(VersionCheck).to receive(:url) { '/version-check-url' } sign_in(create(:user)) @@ -56,9 +56,9 @@ describe 'Help Pages' do describe 'when help page is customized' do before do - allow_any_instance_of(ApplicationSetting).to receive(:help_page_hide_commercial_content?) { true } - allow_any_instance_of(ApplicationSetting).to receive(:help_page_text) { "My Custom Text" } - allow_any_instance_of(ApplicationSetting).to receive(:help_page_support_url) { "http://example.com/help" } + stub_application_setting(help_page_hide_commercial_content: true, + help_page_text: 'My Custom Text', + help_page_support_url: 'http://example.com/help') sign_in(create(:user)) visit help_path diff --git a/spec/features/issues/gfm_autocomplete_spec.rb b/spec/features/issues/gfm_autocomplete_spec.rb index c31b636d67f..6a9a80235c1 100644 --- a/spec/features/issues/gfm_autocomplete_spec.rb +++ b/spec/features/issues/gfm_autocomplete_spec.rb @@ -15,7 +15,7 @@ feature 'GFM autocomplete', :js do end it 'updates issue descripton with GFM reference' do - find('.issuable-edit').click + find('.js-issuable-edit').click simulate_input('#issue-description', "@#{user.name[0...3]}") diff --git a/spec/features/merge_requests/image_diff_notes.rb b/spec/features/merge_requests/image_diff_notes.rb index 3c53b51e330..021c4e03428 100644 --- a/spec/features/merge_requests/image_diff_notes.rb +++ b/spec/features/merge_requests/image_diff_notes.rb @@ -185,6 +185,18 @@ feature 'image diff notes', :js do expect(page).to have_content(diff_note.note) end end + + describe 'image view modes' do + before do + visit project_commit_path(project, '2f63565e7aac07bcdadb654e253078b727143ec4') + end + + it 'resizes image in onion skin view mode' do + find('.view-modes-menu .onion-skin').click + + expect(find('.onion-skin-frame')['style']).to match('width: 228px; height: 240px;') + end + end end def create_image_diff_note diff --git a/spec/features/profile_spec.rb b/spec/features/profile_spec.rb index c60883911f7..0848857ed1e 100644 --- a/spec/features/profile_spec.rb +++ b/spec/features/profile_spec.rb @@ -25,7 +25,7 @@ describe 'Profile account page', :js do fill_in 'password', with: '12345678' - page.within '.popup-dialog' do + page.within '.modal' do click_button 'Delete account' end @@ -38,7 +38,7 @@ describe 'Profile account page', :js do fill_in 'password', with: 'testing123' - page.within '.popup-dialog' do + page.within '.modal' do click_button 'Delete account' end diff --git a/spec/features/projects/clusters_spec.rb b/spec/features/projects/clusters_spec.rb index 008bdf2044b..93929bf6814 100644 --- a/spec/features/projects/clusters_spec.rb +++ b/spec/features/projects/clusters_spec.rb @@ -35,17 +35,6 @@ feature 'Clusters', :js do expect(page).to have_selector('.gl-responsive-table-row', count: 2) end - it 'user sees navigation tabs' do - expect(page.find('.js-active-tab').text).to include('Active') - expect(page.find('.js-active-tab .badge').text).to include('1') - - expect(page.find('.js-inactive-tab').text).to include('Inactive') - expect(page.find('.js-inactive-tab .badge').text).to include('0') - - expect(page.find('.js-all-tab').text).to include('All') - expect(page.find('.js-all-tab .badge').text).to include('1') - end - context 'inline update of cluster' do it 'user can update cluster' do expect(page).to have_selector('.js-toggle-cluster-list') diff --git a/spec/features/projects/labels/user_sees_links_to_issuables.rb b/spec/features/projects/labels/user_sees_links_to_issuables.rb new file mode 100644 index 00000000000..aa56fd7f74e --- /dev/null +++ b/spec/features/projects/labels/user_sees_links_to_issuables.rb @@ -0,0 +1,75 @@ +require 'spec_helper' + +feature 'Projects > Labels > User sees links to issuables' do + set(:user) { create(:user) } + + before do + label # creates the label + project.add_developer(user) + sign_in user + visit project_labels_path(project) + end + + context 'with a project label' do + let(:label) { create(:label, project: project, title: 'bug') } + + context 'when merge requests and issues are enabled for the project' do + let(:project) { create(:project, :public) } + + scenario 'shows links to MRs and issues' do + expect(page).to have_link('view merge requests') + expect(page).to have_link('view open issues') + end + end + + context 'when issues are disabled for the project' do + let(:project) { create(:project, :public, issues_access_level: ProjectFeature::DISABLED) } + + scenario 'shows links to MRs but not to issues' do + expect(page).to have_link('view merge requests') + expect(page).not_to have_link('view open issues') + end + end + + context 'when merge requests are disabled for the project' do + let(:project) { create(:project, :public, merge_requests_access_level: ProjectFeature::DISABLED) } + + scenario 'shows links to issues but not to MRs' do + expect(page).not_to have_link('view merge requests') + expect(page).to have_link('view open issues') + end + end + end + + context 'with a group label' do + set(:group) { create(:group) } + let(:label) { create(:group_label, group: group, title: 'bug') } + + context 'when merge requests and issues are enabled for the project' do + let(:project) { create(:project, :public, namespace: group) } + + scenario 'shows links to MRs and issues' do + expect(page).to have_link('view merge requests') + expect(page).to have_link('view open issues') + end + end + + context 'when issues are disabled for the project' do + let(:project) { create(:project, :public, namespace: group, issues_access_level: ProjectFeature::DISABLED) } + + scenario 'shows links to MRs and issues' do + expect(page).to have_link('view merge requests') + expect(page).to have_link('view open issues') + end + end + + context 'when merge requests are disabled for the project' do + let(:project) { create(:project, :public, namespace: group, merge_requests_access_level: ProjectFeature::DISABLED) } + + scenario 'shows links to MRs and issues' do + expect(page).to have_link('view merge requests') + expect(page).to have_link('view open issues') + end + end + end +end diff --git a/spec/features/projects/pipelines/pipeline_spec.rb b/spec/features/projects/pipelines/pipeline_spec.rb index 888e290292b..3987cea0b4f 100644 --- a/spec/features/projects/pipelines/pipeline_spec.rb +++ b/spec/features/projects/pipelines/pipeline_spec.rb @@ -152,7 +152,7 @@ describe 'Pipeline', :js do end it 'shows counter in Jobs tab' do - expect(page.find('.js-builds-counter').text).to eq(pipeline.statuses.count.to_s) + expect(page.find('.js-builds-counter').text).to eq(pipeline.total_size.to_s) end it 'shows Pipeline tab as active' do @@ -248,7 +248,7 @@ describe 'Pipeline', :js do end it 'shows counter in Jobs tab' do - expect(page.find('.js-builds-counter').text).to eq(pipeline.statuses.count.to_s) + expect(page.find('.js-builds-counter').text).to eq(pipeline.total_size.to_s) end it 'shows Jobs tab as active' do diff --git a/spec/features/projects/tree/create_directory_spec.rb b/spec/features/projects/tree/create_directory_spec.rb index 156293289dd..8f06328962e 100644 --- a/spec/features/projects/tree/create_directory_spec.rb +++ b/spec/features/projects/tree/create_directory_spec.rb @@ -20,7 +20,7 @@ feature 'Multi-file editor new directory', :js do click_link('New directory') - page.within('.popup-dialog') do + page.within('.modal') do find('.form-control').set('foldername') click_button('Create directory') diff --git a/spec/features/projects/tree/create_file_spec.rb b/spec/features/projects/tree/create_file_spec.rb index 8fb8476e631..bdebc12ef47 100644 --- a/spec/features/projects/tree/create_file_spec.rb +++ b/spec/features/projects/tree/create_file_spec.rb @@ -20,7 +20,7 @@ feature 'Multi-file editor new file', :js do click_link('New file') - page.within('.popup-dialog') do + page.within('.modal') do find('.form-control').set('filename') click_button('Create file') diff --git a/spec/features/projects/tree/tree_show_spec.rb b/spec/features/projects/tree/tree_show_spec.rb new file mode 100644 index 00000000000..c8a17871508 --- /dev/null +++ b/spec/features/projects/tree/tree_show_spec.rb @@ -0,0 +1,28 @@ +require 'spec_helper' + +feature 'Projects tree' do + let(:user) { create(:user) } + let(:project) { create(:project, :repository) } + + before do + project.add_master(user) + sign_in(user) + + visit project_tree_path(project, 'master') + end + + it 'renders tree table' do + expect(page).to have_selector('.tree-item') + expect(page).not_to have_selector('.label-lfs', text: 'LFS') + end + + context 'LFS' do + before do + visit project_tree_path(project, File.join('master', 'files/lfs')) + end + + it 'renders LFS badge on blob item' do + expect(page).to have_selector('.label-lfs', text: 'LFS') + end + end +end diff --git a/spec/features/variables_spec.rb b/spec/features/variables_spec.rb index c78f7d0d9be..dde60c83536 100644 --- a/spec/features/variables_spec.rb +++ b/spec/features/variables_spec.rb @@ -65,14 +65,14 @@ describe 'Project variables', :js do expect(page).to have_content('******') end - click_button('Reveal Values') + click_button('Reveal values') page.within('.variables-table') do expect(page).to have_content('key') expect(page).to have_content('key value') end - click_button('Hide Values') + click_button('Hide values') page.within('.variables-table') do expect(page).to have_content('key') diff --git a/spec/fixtures/api/schemas/contributor.json b/spec/fixtures/api/schemas/contributor.json new file mode 100644 index 00000000000..e88470a2363 --- /dev/null +++ b/spec/fixtures/api/schemas/contributor.json @@ -0,0 +1,18 @@ +{ + "type": "object", + "required" : [ + "name", + "email", + "commits", + "additions", + "deletions" + ], + "properties" : { + "name": { "type": "string" }, + "email": { "type": "string" }, + "commits": { "type": "integer" }, + "additions": { "type": "integer" }, + "deletions": { "type": "integer" } + }, + "additionalProperties": false +} diff --git a/spec/fixtures/api/schemas/contributors.json b/spec/fixtures/api/schemas/contributors.json new file mode 100644 index 00000000000..a9f1d1ea64f --- /dev/null +++ b/spec/fixtures/api/schemas/contributors.json @@ -0,0 +1,4 @@ +{ + "type": "array", + "items": { "$ref": "contributor.json" } +} diff --git a/spec/fixtures/emails/valid_new_merge_request.eml b/spec/fixtures/emails/valid_new_merge_request.eml index 480675a6d7e..729df674604 100644 --- a/spec/fixtures/emails/valid_new_merge_request.eml +++ b/spec/fixtures/emails/valid_new_merge_request.eml @@ -16,3 +16,5 @@ X-Sieve: CMU Sieve 2.2 X-Received: by 10.0.0.1 with SMTP id n7mr11234144ipb.85.1371157428600; Thu, 13 Jun 2013 14:03:48 -0700 (PDT) X-Scanned-By: MIMEDefang 2.69 on IPv6:2001:470:1d:165::1 + +Merge request description diff --git a/spec/fixtures/emails/valid_new_merge_request_no_description.eml b/spec/fixtures/emails/valid_new_merge_request_no_description.eml new file mode 100644 index 00000000000..480675a6d7e --- /dev/null +++ b/spec/fixtures/emails/valid_new_merge_request_no_description.eml @@ -0,0 +1,18 @@ +Return-Path: <jake@adventuretime.ooo> +Received: from iceking.adventuretime.ooo ([unix socket]) by iceking (Cyrus v2.2.13-Debian-2.2.13-19+squeeze3) with LMTPA; Thu, 13 Jun 2013 17:03:50 -0400 +Received: from mail-ie0-x234.google.com (mail-ie0-x234.google.com [IPv6:2607:f8b0:4001:c03::234]) by iceking.adventuretime.ooo (8.14.3/8.14.3/Debian-9.4) with ESMTP id r5DL3nFJ016967 (version=TLSv1/SSLv3 cipher=RC4-SHA bits=128 verify=NOT) for <incoming+gitlabhq/gitlabhq@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 17:03:50 -0400 +Received: by mail-ie0-f180.google.com with SMTP id f4so21977375iea.25 for <incoming+gitlabhq/gitlabhq@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 14:03:48 -0700 +Received: by 10.0.0.1 with HTTP; Thu, 13 Jun 2013 14:03:48 -0700 +Date: Thu, 13 Jun 2013 17:03:48 -0400 +From: Jake the Dog <jake@adventuretime.ooo> +To: incoming+gitlabhq/gitlabhq+merge-request+auth_token@appmail.adventuretime.ooo +Message-ID: <CADkmRc+rNGAGGbV2iE5p918UVy4UyJqVcXRO2=otppgzduJSg@mail.gmail.com> +Subject: feature +Mime-Version: 1.0 +Content-Type: text/plain; + charset=ISO-8859-1 +Content-Transfer-Encoding: 7bit +X-Sieve: CMU Sieve 2.2 +X-Received: by 10.0.0.1 with SMTP id n7mr11234144ipb.85.1371157428600; Thu, + 13 Jun 2013 14:03:48 -0700 (PDT) +X-Scanned-By: MIMEDefang 2.69 on IPv6:2001:470:1d:165::1 diff --git a/spec/fixtures/markdown.md.erb b/spec/fixtures/markdown.md.erb index 638cd8b07c8..71abb6da607 100644 --- a/spec/fixtures/markdown.md.erb +++ b/spec/fixtures/markdown.md.erb @@ -258,12 +258,23 @@ With inline diffs tags you can display {+ additions +} or [- deletions -]. The wrapping tags can be either curly braces or square brackets [+ additions +] or {- deletions -}. -However the wrapping tags can not be mixed as such - +Examples: +``` +- {+ additions +} +- [+ additions +] +- {- deletions -} +- [- deletions -] +``` + +However the wrapping tags cannot be mixed as such: + +``` - {+ additions +] - [+ additions +} -- {- delletions -] -- [- delletions -} +- {- deletions -] +- [- deletions -} +``` ### Videos diff --git a/spec/helpers/labels_helper_spec.rb b/spec/helpers/labels_helper_spec.rb index 4ac4302adfd..0286d36952c 100644 --- a/spec/helpers/labels_helper_spec.rb +++ b/spec/helpers/labels_helper_spec.rb @@ -1,6 +1,69 @@ require 'spec_helper' describe LabelsHelper do + describe '#show_label_issuables_link?' do + shared_examples 'a valid response to show_label_issuables_link?' do |issuables_type, when_enabled = true, when_disabled = false| + let(:context_project) { project } + + context "when asking for a #{issuables_type} link" do + subject { show_label_issuables_link?(label, issuables_type, project: context_project) } + + context "when #{issuables_type} are enabled for the project" do + let(:project) { create(:project, "#{issuables_type}_access_level": ProjectFeature::ENABLED) } + + it { is_expected.to be(when_enabled) } + end + + context "when #{issuables_type} are disabled for the project" do + let(:project) { create(:project, :public, "#{issuables_type}_access_level": ProjectFeature::DISABLED) } + + it { is_expected.to be(when_disabled) } + end + end + end + + context 'with a project label' do + let(:label) { create(:label, project: project, title: 'bug') } + + context 'when asking for an issue link' do + it_behaves_like 'a valid response to show_label_issuables_link?', :issues + end + + context 'when asking for a merge requests link' do + it_behaves_like 'a valid response to show_label_issuables_link?', :merge_requests + end + end + + context 'with a group label' do + set(:group) { create(:group) } + let(:label) { create(:group_label, group: group, title: 'bug') } + + context 'when asking for an issue link' do + context 'in the context of a project' do + it_behaves_like 'a valid response to show_label_issuables_link?', :issues, true, true + end + + context 'in the context of a group' do + let(:context_project) { nil } + + it_behaves_like 'a valid response to show_label_issuables_link?', :issues, true, true + end + end + + context 'when asking for a merge requests link' do + context 'in the context of a project' do + it_behaves_like 'a valid response to show_label_issuables_link?', :merge_requests, true, true + end + + context 'in the context of a group' do + let(:context_project) { nil } + + it_behaves_like 'a valid response to show_label_issuables_link?', :merge_requests, true, true + end + end + end + end + describe 'link_to_label' do let(:project) { create(:project) } let(:label) { create(:label, project: project) } diff --git a/spec/helpers/members_helper_spec.rb b/spec/helpers/members_helper_spec.rb index 33186cf50d5..45ffbeb27a4 100644 --- a/spec/helpers/members_helper_spec.rb +++ b/spec/helpers/members_helper_spec.rb @@ -1,14 +1,6 @@ require 'spec_helper' describe MembersHelper do - describe '#action_member_permission' do - let(:project_member) { build(:project_member) } - let(:group_member) { build(:group_member) } - - it { expect(action_member_permission(:admin, project_member)).to eq :admin_project_member } - it { expect(action_member_permission(:admin, group_member)).to eq :admin_group_member } - end - describe '#remove_member_message' do let(:requester) { create(:user) } let(:project) { create(:project, :public, :access_requestable) } diff --git a/spec/helpers/preferences_helper_spec.rb b/spec/helpers/preferences_helper_spec.rb index 8b8080563d3..749aa25e632 100644 --- a/spec/helpers/preferences_helper_spec.rb +++ b/spec/helpers/preferences_helper_spec.rb @@ -77,15 +77,6 @@ describe PreferencesHelper do end end - def stub_user(messages = {}) - if messages.empty? - allow(helper).to receive(:current_user).and_return(nil) - else - allow(helper).to receive(:current_user) - .and_return(double('user', messages)) - end - end - describe '#default_project_view' do context 'user not signed in' do before do @@ -125,5 +116,70 @@ describe PreferencesHelper do end end end + + context 'user signed in' do + let(:user) { create(:user, :readme) } + let(:project) { create(:project, :public, :repository) } + + before do + helper.instance_variable_set(:@project, project) + allow(helper).to receive(:current_user).and_return(user) + end + + context 'when the user is allowed to see the code' do + it 'returns the project view' do + allow(helper).to receive(:can?).with(user, :download_code, project).and_return(true) + + expect(helper.default_project_view).to eq('readme') + end + end + + context 'with wikis enabled and the right policy for the user' do + before do + project.project_feature.update_attribute(:issues_access_level, 0) + allow(helper).to receive(:can?).with(user, :download_code, project).and_return(false) + end + + it 'returns wiki if the user has the right policy' do + allow(helper).to receive(:can?).with(user, :read_wiki, project).and_return(true) + + expect(helper.default_project_view).to eq('wiki') + end + + it 'returns customize_workflow if the user does not have the right policy' do + allow(helper).to receive(:can?).with(user, :read_wiki, project).and_return(false) + + expect(helper.default_project_view).to eq('customize_workflow') + end + end + + context 'with issues as a feature available' do + it 'return issues' do + allow(helper).to receive(:can?).with(user, :download_code, project).and_return(false) + allow(helper).to receive(:can?).with(user, :read_wiki, project).and_return(false) + + expect(helper.default_project_view).to eq('projects/issues/issues') + end + end + + context 'with no activity, no wikies and no issues' do + it 'returns customize_workflow as default' do + project.project_feature.update_attribute(:issues_access_level, 0) + allow(helper).to receive(:can?).with(user, :download_code, project).and_return(false) + allow(helper).to receive(:can?).with(user, :read_wiki, project).and_return(false) + + expect(helper.default_project_view).to eq('customize_workflow') + end + end + end + end + + def stub_user(messages = {}) + if messages.empty? + allow(helper).to receive(:current_user).and_return(nil) + else + allow(helper).to receive(:current_user) + .and_return(double('user', messages)) + end end end diff --git a/spec/helpers/runners_helper_spec.rb b/spec/helpers/runners_helper_spec.rb index 35f91b7decf..a4a483e68a8 100644 --- a/spec/helpers/runners_helper_spec.rb +++ b/spec/helpers/runners_helper_spec.rb @@ -2,17 +2,17 @@ require 'spec_helper' describe RunnersHelper do it "returns - not contacted yet" do - runner = FactoryGirl.build :ci_runner + runner = FactoryBot.build :ci_runner expect(runner_status_icon(runner)).to include("not connected yet") end it "returns offline text" do - runner = FactoryGirl.build(:ci_runner, contacted_at: 1.day.ago, active: true) + runner = FactoryBot.build(:ci_runner, contacted_at: 1.day.ago, active: true) expect(runner_status_icon(runner)).to include("Runner is offline") end it "returns online text" do - runner = FactoryGirl.build(:ci_runner, contacted_at: 1.second.ago, active: true) + runner = FactoryBot.build(:ci_runner, contacted_at: 1.second.ago, active: true) expect(runner_status_icon(runner)).to include("Runner is online") end end diff --git a/spec/helpers/tree_helper_spec.rb b/spec/helpers/tree_helper_spec.rb index c358ccae9c3..d3b1be599dd 100644 --- a/spec/helpers/tree_helper_spec.rb +++ b/spec/helpers/tree_helper_spec.rb @@ -9,6 +9,7 @@ describe TreeHelper do before do @id = sha @project = project + @lfs_blob_ids = [] end it 'displays all entries without a warning' do diff --git a/spec/javascripts/activities_spec.js b/spec/javascripts/activities_spec.js index e8c5f721423..7a9c539e9d0 100644 --- a/spec/javascripts/activities_spec.js +++ b/spec/javascripts/activities_spec.js @@ -1,8 +1,7 @@ /* eslint-disable no-unused-expressions, no-prototype-builtins, no-new, no-shadow, max-len */ import 'vendor/jquery.endless-scroll'; -import '~/pager'; -import '~/activities'; +import Activities from '~/activities'; (() => { window.gon || (window.gon = {}); @@ -35,7 +34,7 @@ import '~/activities'; describe('Activities', () => { beforeEach(() => { loadFixtures(fixtureTemplate); - new gl.Activities(); + new Activities(); }); for (let i = 0; i < filters.length; i += 1) { diff --git a/spec/javascripts/behaviors/secret_values_spec.js b/spec/javascripts/behaviors/secret_values_spec.js new file mode 100644 index 00000000000..9eeae474e7d --- /dev/null +++ b/spec/javascripts/behaviors/secret_values_spec.js @@ -0,0 +1,146 @@ +import SecretValues from '~/behaviors/secret_values'; + +function generateFixtureMarkup(secrets, isRevealed) { + return ` + <div class="js-secret-container"> + ${secrets.map(secret => ` + <div class="js-secret-value-placeholder"> + *** + </div> + <div class="hide js-secret-value"> + ${secret} + </div> + `).join('')} + <button + class="js-secret-value-reveal-button" + data-secret-reveal-status="${isRevealed}" + > + ... + </button> + </div> + `; +} + +function setupSecretFixture(secrets, isRevealed) { + const wrapper = document.createElement('div'); + wrapper.innerHTML = generateFixtureMarkup(secrets, isRevealed); + + const secretValues = new SecretValues(wrapper.querySelector('.js-secret-container')); + secretValues.init(); + + return wrapper; +} + +describe('setupSecretValues', () => { + describe('with a single secret', () => { + const secrets = ['mysecret123']; + + it('should have correct "Reveal" label when values are hidden', () => { + const wrapper = setupSecretFixture(secrets, false); + const revealButton = wrapper.querySelector('.js-secret-value-reveal-button'); + + expect(revealButton.textContent).toEqual('Reveal value'); + }); + + it('should have correct "Hide" label when values are shown', () => { + const wrapper = setupSecretFixture(secrets, true); + const revealButton = wrapper.querySelector('.js-secret-value-reveal-button'); + + expect(revealButton.textContent).toEqual('Hide value'); + }); + + it('should value hidden initially', () => { + const wrapper = setupSecretFixture(secrets, false); + const values = wrapper.querySelectorAll('.js-secret-value'); + const placeholders = wrapper.querySelectorAll('.js-secret-value-placeholder'); + + expect(values.length).toEqual(1); + expect(values[0].classList.contains('hide')).toEqual(true); + expect(placeholders.length).toEqual(1); + expect(placeholders[0].classList.contains('hide')).toEqual(false); + }); + + it('should toggle value and placeholder', () => { + const wrapper = setupSecretFixture(secrets, false); + const revealButton = wrapper.querySelector('.js-secret-value-reveal-button'); + const values = wrapper.querySelectorAll('.js-secret-value'); + const placeholders = wrapper.querySelectorAll('.js-secret-value-placeholder'); + + revealButton.click(); + + expect(values.length).toEqual(1); + expect(values[0].classList.contains('hide')).toEqual(false); + expect(placeholders.length).toEqual(1); + expect(placeholders[0].classList.contains('hide')).toEqual(true); + + revealButton.click(); + + expect(values.length).toEqual(1); + expect(values[0].classList.contains('hide')).toEqual(true); + expect(placeholders.length).toEqual(1); + expect(placeholders[0].classList.contains('hide')).toEqual(false); + }); + }); + + describe('with a multiple secrets', () => { + const secrets = ['mysecret123', 'happygoat456', 'tanuki789']; + + it('should have correct "Reveal" label when values are hidden', () => { + const wrapper = setupSecretFixture(secrets, false); + const revealButton = wrapper.querySelector('.js-secret-value-reveal-button'); + + expect(revealButton.textContent).toEqual('Reveal values'); + }); + + it('should have correct "Hide" label when values are shown', () => { + const wrapper = setupSecretFixture(secrets, true); + const revealButton = wrapper.querySelector('.js-secret-value-reveal-button'); + + expect(revealButton.textContent).toEqual('Hide values'); + }); + + it('should have all values hidden initially', () => { + const wrapper = setupSecretFixture(secrets, false); + const values = wrapper.querySelectorAll('.js-secret-value'); + const placeholders = wrapper.querySelectorAll('.js-secret-value-placeholder'); + + expect(values.length).toEqual(3); + values.forEach((value) => { + expect(value.classList.contains('hide')).toEqual(true); + }); + expect(placeholders.length).toEqual(3); + placeholders.forEach((placeholder) => { + expect(placeholder.classList.contains('hide')).toEqual(false); + }); + }); + + it('should toggle values and placeholders', () => { + const wrapper = setupSecretFixture(secrets, false); + const revealButton = wrapper.querySelector('.js-secret-value-reveal-button'); + const values = wrapper.querySelectorAll('.js-secret-value'); + const placeholders = wrapper.querySelectorAll('.js-secret-value-placeholder'); + + revealButton.click(); + + expect(values.length).toEqual(3); + values.forEach((value) => { + expect(value.classList.contains('hide')).toEqual(false); + }); + expect(placeholders.length).toEqual(3); + placeholders.forEach((placeholder) => { + expect(placeholder.classList.contains('hide')).toEqual(true); + }); + + revealButton.click(); + + expect(values.length).toEqual(3); + values.forEach((value) => { + expect(value.classList.contains('hide')).toEqual(true); + }); + expect(placeholders.length).toEqual(3); + placeholders.forEach((placeholder) => { + expect(placeholder.classList.contains('hide')).toEqual(false); + }); + }); + }); +}); diff --git a/spec/javascripts/boards/boards_store_spec.js b/spec/javascripts/boards/boards_store_spec.js index 9e5b0bd3efe..0e656858182 100644 --- a/spec/javascripts/boards/boards_store_spec.js +++ b/spec/javascripts/boards/boards_store_spec.js @@ -9,7 +9,6 @@ import Vue from 'vue'; import Cookies from 'js-cookie'; -import '~/lib/utils/url_utility'; import '~/boards/models/issue'; import '~/boards/models/label'; import '~/boards/models/list'; diff --git a/spec/javascripts/boards/issue_spec.js b/spec/javascripts/boards/issue_spec.js index 10b88878c2a..41dcb19df3c 100644 --- a/spec/javascripts/boards/issue_spec.js +++ b/spec/javascripts/boards/issue_spec.js @@ -4,7 +4,6 @@ /* global mockBoardService */ import Vue from 'vue'; -import '~/lib/utils/url_utility'; import '~/boards/models/issue'; import '~/boards/models/label'; import '~/boards/models/list'; diff --git a/spec/javascripts/boards/list_spec.js b/spec/javascripts/boards/list_spec.js index d4627223a12..eead396ca7e 100644 --- a/spec/javascripts/boards/list_spec.js +++ b/spec/javascripts/boards/list_spec.js @@ -9,7 +9,6 @@ import Vue from 'vue'; -import '~/lib/utils/url_utility'; import '~/boards/models/issue'; import '~/boards/models/label'; import '~/boards/models/list'; diff --git a/spec/javascripts/collapsed_sidebar_todo_spec.js b/spec/javascripts/collapsed_sidebar_todo_spec.js index 974815fe939..5026eaafaca 100644 --- a/spec/javascripts/collapsed_sidebar_todo_spec.js +++ b/spec/javascripts/collapsed_sidebar_todo_spec.js @@ -1,7 +1,6 @@ -/* global Sidebar */ /* eslint-disable no-new */ import _ from 'underscore'; -import '~/right_sidebar'; +import Sidebar from '~/right_sidebar'; describe('Issuable right sidebar collapsed todo toggle', () => { const fixtureName = 'issues/open-issue.html.raw'; diff --git a/spec/javascripts/commits_spec.js b/spec/javascripts/commits_spec.js index e5a5e3293b9..d0176520440 100644 --- a/spec/javascripts/commits_spec.js +++ b/spec/javascripts/commits_spec.js @@ -1,5 +1,4 @@ import 'vendor/jquery.endless-scroll'; -import '~/pager'; import CommitsList from '~/commits'; describe('Commits List', () => { diff --git a/spec/javascripts/datetime_utility_spec.js b/spec/javascripts/datetime_utility_spec.js index 0f7bf9ec712..2e5b65f5610 100644 --- a/spec/javascripts/datetime_utility_spec.js +++ b/spec/javascripts/datetime_utility_spec.js @@ -1,110 +1,108 @@ import * as datetimeUtility from '~/lib/utils/datetime_utility'; -(() => { - describe('Date time utils', () => { - describe('timeFor', () => { - it('returns `past due` when in past', () => { - const date = new Date(); - date.setFullYear(date.getFullYear() - 1); - - expect( - gl.utils.timeFor(date), - ).toBe('Past due'); - }); - - it('returns remaining time when in the future', () => { - const date = new Date(); - date.setFullYear(date.getFullYear() + 1); - - // Add a day to prevent a transient error. If date is even 1 second - // short of a full year, timeFor will return '11 months remaining' - date.setDate(date.getDate() + 1); - - expect( - gl.utils.timeFor(date), - ).toBe('1 year remaining'); - }); +describe('Date time utils', () => { + describe('timeFor', () => { + it('returns `past due` when in past', () => { + const date = new Date(); + date.setFullYear(date.getFullYear() - 1); + + expect( + datetimeUtility.timeFor(date), + ).toBe('Past due'); }); - describe('get day name', () => { - it('should return Sunday', () => { - const day = gl.utils.getDayName(new Date('07/17/2016')); - expect(day).toBe('Sunday'); - }); - - it('should return Monday', () => { - const day = gl.utils.getDayName(new Date('07/18/2016')); - expect(day).toBe('Monday'); - }); - - it('should return Tuesday', () => { - const day = gl.utils.getDayName(new Date('07/19/2016')); - expect(day).toBe('Tuesday'); - }); - - it('should return Wednesday', () => { - const day = gl.utils.getDayName(new Date('07/20/2016')); - expect(day).toBe('Wednesday'); - }); - - it('should return Thursday', () => { - const day = gl.utils.getDayName(new Date('07/21/2016')); - expect(day).toBe('Thursday'); - }); - - it('should return Friday', () => { - const day = gl.utils.getDayName(new Date('07/22/2016')); - expect(day).toBe('Friday'); - }); - - it('should return Saturday', () => { - const day = gl.utils.getDayName(new Date('07/23/2016')); - expect(day).toBe('Saturday'); - }); - }); + it('returns remaining time when in the future', () => { + const date = new Date(); + date.setFullYear(date.getFullYear() + 1); + + // Add a day to prevent a transient error. If date is even 1 second + // short of a full year, timeFor will return '11 months remaining' + date.setDate(date.getDate() + 1); - describe('get day difference', () => { - it('should return 7', () => { - const firstDay = new Date('07/01/2016'); - const secondDay = new Date('07/08/2016'); - const difference = gl.utils.getDayDifference(firstDay, secondDay); - expect(difference).toBe(7); - }); - - it('should return 31', () => { - const firstDay = new Date('07/01/2016'); - const secondDay = new Date('08/01/2016'); - const difference = gl.utils.getDayDifference(firstDay, secondDay); - expect(difference).toBe(31); - }); - - it('should return 365', () => { - const firstDay = new Date('07/02/2015'); - const secondDay = new Date('07/01/2016'); - const difference = gl.utils.getDayDifference(firstDay, secondDay); - expect(difference).toBe(365); - }); + expect( + datetimeUtility.timeFor(date), + ).toBe('1 year remaining'); }); }); - describe('timeIntervalInWords', () => { - it('should return string with number of minutes and seconds', () => { - expect(datetimeUtility.timeIntervalInWords(9.54)).toEqual('9 seconds'); - expect(datetimeUtility.timeIntervalInWords(1)).toEqual('1 second'); - expect(datetimeUtility.timeIntervalInWords(200)).toEqual('3 minutes 20 seconds'); - expect(datetimeUtility.timeIntervalInWords(6008)).toEqual('100 minutes 8 seconds'); + describe('get day name', () => { + it('should return Sunday', () => { + const day = datetimeUtility.getDayName(new Date('07/17/2016')); + expect(day).toBe('Sunday'); + }); + + it('should return Monday', () => { + const day = datetimeUtility.getDayName(new Date('07/18/2016')); + expect(day).toBe('Monday'); + }); + + it('should return Tuesday', () => { + const day = datetimeUtility.getDayName(new Date('07/19/2016')); + expect(day).toBe('Tuesday'); + }); + + it('should return Wednesday', () => { + const day = datetimeUtility.getDayName(new Date('07/20/2016')); + expect(day).toBe('Wednesday'); + }); + + it('should return Thursday', () => { + const day = datetimeUtility.getDayName(new Date('07/21/2016')); + expect(day).toBe('Thursday'); + }); + + it('should return Friday', () => { + const day = datetimeUtility.getDayName(new Date('07/22/2016')); + expect(day).toBe('Friday'); + }); + + it('should return Saturday', () => { + const day = datetimeUtility.getDayName(new Date('07/23/2016')); + expect(day).toBe('Saturday'); }); }); - describe('dateInWords', () => { - const date = new Date('07/01/2016'); + describe('get day difference', () => { + it('should return 7', () => { + const firstDay = new Date('07/01/2016'); + const secondDay = new Date('07/08/2016'); + const difference = datetimeUtility.getDayDifference(firstDay, secondDay); + expect(difference).toBe(7); + }); - it('should return date in words', () => { - expect(datetimeUtility.dateInWords(date)).toEqual('July 1, 2016'); + it('should return 31', () => { + const firstDay = new Date('07/01/2016'); + const secondDay = new Date('08/01/2016'); + const difference = datetimeUtility.getDayDifference(firstDay, secondDay); + expect(difference).toBe(31); }); - it('should return abbreviated month name', () => { - expect(datetimeUtility.dateInWords(date, true)).toEqual('Jul 1, 2016'); + it('should return 365', () => { + const firstDay = new Date('07/02/2015'); + const secondDay = new Date('07/01/2016'); + const difference = datetimeUtility.getDayDifference(firstDay, secondDay); + expect(difference).toBe(365); }); }); -})(); +}); + +describe('timeIntervalInWords', () => { + it('should return string with number of minutes and seconds', () => { + expect(datetimeUtility.timeIntervalInWords(9.54)).toEqual('9 seconds'); + expect(datetimeUtility.timeIntervalInWords(1)).toEqual('1 second'); + expect(datetimeUtility.timeIntervalInWords(200)).toEqual('3 minutes 20 seconds'); + expect(datetimeUtility.timeIntervalInWords(6008)).toEqual('100 minutes 8 seconds'); + }); +}); + +describe('dateInWords', () => { + const date = new Date('07/01/2016'); + + it('should return date in words', () => { + expect(datetimeUtility.dateInWords(date)).toEqual('July 1, 2016'); + }); + + it('should return abbreviated month name', () => { + expect(datetimeUtility.dateInWords(date, true)).toEqual('Jul 1, 2016'); + }); +}); diff --git a/spec/javascripts/deploy_keys/components/key_spec.js b/spec/javascripts/deploy_keys/components/key_spec.js index 5b64cbb2dfc..2f28c5bbf01 100644 --- a/spec/javascripts/deploy_keys/components/key_spec.js +++ b/spec/javascripts/deploy_keys/components/key_spec.js @@ -1,6 +1,7 @@ import Vue from 'vue'; import DeployKeysStore from '~/deploy_keys/store'; import key from '~/deploy_keys/components/key.vue'; +import { getTimeago } from '~/lib/utils/datetime_utility'; describe('Deploy keys key', () => { let vm; @@ -37,7 +38,7 @@ describe('Deploy keys key', () => { it('renders human friendly formatted created date', () => { expect( vm.$el.querySelector('.key-created-at').textContent.trim(), - ).toBe(`created ${gl.utils.getTimeago().format(deployKey.created_at)}`); + ).toBe(`created ${getTimeago().format(deployKey.created_at)}`); }); it('shows edit button', () => { diff --git a/spec/javascripts/filtered_search/filtered_search_manager_spec.js b/spec/javascripts/filtered_search/filtered_search_manager_spec.js index 230c15e5de6..5111632d681 100644 --- a/spec/javascripts/filtered_search/filtered_search_manager_spec.js +++ b/spec/javascripts/filtered_search/filtered_search_manager_spec.js @@ -1,8 +1,8 @@ +import * as urlUtils from '~/lib/utils/url_utility'; import * as recentSearchesStoreSrc from '~/filtered_search/stores/recent_searches_store'; import RecentSearchesService from '~/filtered_search/services/recent_searches_service'; import RecentSearchesServiceError from '~/filtered_search/services/recent_searches_service_error'; import RecentSearchesRoot from '~/filtered_search/recent_searches_root'; -import '~/lib/utils/url_utility'; import '~/lib/utils/common_utils'; import '~/filtered_search/filtered_search_token_keys'; import '~/filtered_search/filtered_search_tokenizer'; @@ -162,7 +162,7 @@ describe('Filtered Search Manager', () => { it('should search with a single word', (done) => { input.value = 'searchTerm'; - spyOn(gl.utils, 'visitUrl').and.callFake((url) => { + spyOn(urlUtils, 'visitUrl').and.callFake((url) => { expect(url).toEqual(`${defaultParams}&search=searchTerm`); done(); }); @@ -173,7 +173,7 @@ describe('Filtered Search Manager', () => { it('should search with multiple words', (done) => { input.value = 'awesome search terms'; - spyOn(gl.utils, 'visitUrl').and.callFake((url) => { + spyOn(urlUtils, 'visitUrl').and.callFake((url) => { expect(url).toEqual(`${defaultParams}&search=awesome+search+terms`); done(); }); @@ -184,7 +184,7 @@ describe('Filtered Search Manager', () => { it('should search with special characters', (done) => { input.value = '~!@#$%^&*()_+{}:<>,.?/'; - spyOn(gl.utils, 'visitUrl').and.callFake((url) => { + spyOn(urlUtils, 'visitUrl').and.callFake((url) => { expect(url).toEqual(`${defaultParams}&search=~!%40%23%24%25%5E%26*()_%2B%7B%7D%3A%3C%3E%2C.%3F%2F`); done(); }); @@ -198,7 +198,7 @@ describe('Filtered Search Manager', () => { ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug')} `); - spyOn(gl.utils, 'visitUrl').and.callFake((url) => { + spyOn(urlUtils, 'visitUrl').and.callFake((url) => { expect(url).toEqual(`${defaultParams}&label_name[]=bug`); done(); }); diff --git a/spec/javascripts/fly_out_nav_spec.js b/spec/javascripts/fly_out_nav_spec.js index 4f20e31f511..a3fa07d5bc2 100644 --- a/spec/javascripts/fly_out_nav_spec.js +++ b/spec/javascripts/fly_out_nav_spec.js @@ -253,7 +253,7 @@ describe('Fly out sidebar navigation', () => { it('shows collapsed only sub-items if icon only sidebar', () => { const subItems = el.querySelector('.sidebar-sub-level-items'); const sidebar = document.createElement('div'); - sidebar.classList.add('sidebar-icons-only'); + sidebar.classList.add('sidebar-collapsed-desktop'); subItems.classList.add('is-fly-out-only'); setSidebar(sidebar); @@ -343,7 +343,7 @@ describe('Fly out sidebar navigation', () => { it('returns true when active & collapsed sidebar', () => { const sidebar = document.createElement('div'); - sidebar.classList.add('sidebar-icons-only'); + sidebar.classList.add('sidebar-collapsed-desktop'); el.classList.add('active'); setSidebar(sidebar); diff --git a/spec/javascripts/gl_dropdown_spec.js b/spec/javascripts/gl_dropdown_spec.js index ca048123bf7..b13d1bf8dff 100644 --- a/spec/javascripts/gl_dropdown_spec.js +++ b/spec/javascripts/gl_dropdown_spec.js @@ -2,7 +2,7 @@ import '~/gl_dropdown'; import '~/lib/utils/common_utils'; -import '~/lib/utils/url_utility'; +import * as urlUtils from '~/lib/utils/url_utility'; describe('glDropdown', function describeDropdown() { preloadFixtures('static/gl_dropdown.html.raw'); @@ -137,13 +137,13 @@ describe('glDropdown', function describeDropdown() { expect(this.dropdownContainerElement).toHaveClass('open'); const randomIndex = Math.floor(Math.random() * (this.projectsData.length - 1)) + 0; navigateWithKeys('down', randomIndex, () => { - spyOn(gl.utils, 'visitUrl').and.stub(); + spyOn(urlUtils, 'visitUrl').and.stub(); navigateWithKeys('enter', null, () => { expect(this.dropdownContainerElement).not.toHaveClass('open'); const link = $(`${ITEM_SELECTOR}:eq(${randomIndex}) a`, this.$dropdownMenuElement); expect(link).toHaveClass('is-active'); const linkedLocation = link.attr('href'); - if (linkedLocation && linkedLocation !== '#') expect(gl.utils.visitUrl).toHaveBeenCalledWith(linkedLocation); + if (linkedLocation && linkedLocation !== '#') expect(urlUtils.visitUrl).toHaveBeenCalledWith(linkedLocation); }); }); }); diff --git a/spec/javascripts/graphs/stat_graph_contributors_spec.js b/spec/javascripts/graphs/stat_graph_contributors_spec.js new file mode 100644 index 00000000000..962423462e7 --- /dev/null +++ b/spec/javascripts/graphs/stat_graph_contributors_spec.js @@ -0,0 +1,26 @@ +import ContributorsStatGraph from '~/graphs/stat_graph_contributors'; +import { ContributorsGraph } from '~/graphs/stat_graph_contributors_graph'; + +import { setLanguage } from '../helpers/locale_helper'; + +describe('ContributorsStatGraph', () => { + describe('change_date_header', () => { + beforeAll(() => { + setLanguage('de'); + }); + + afterAll(() => { + setLanguage(null); + }); + + it('uses the locale to display date ranges', () => { + ContributorsGraph.init_x_domain([{ date: '2013-01-31' }, { date: '2012-01-31' }]); + setFixtures('<div id="date_header"></div>'); + const graph = new ContributorsStatGraph(); + + graph.change_date_header(); + + expect(document.getElementById('date_header').innerText).toBe('31. Januar 2012 – 31. Januar 2013'); + }); + }); +}); diff --git a/spec/javascripts/groups/components/app_spec.js b/spec/javascripts/groups/components/app_spec.js index 59d4f7c45c6..97e39f6411b 100644 --- a/spec/javascripts/groups/components/app_spec.js +++ b/spec/javascripts/groups/components/app_spec.js @@ -1,9 +1,9 @@ import Vue from 'vue'; +import * as utils from '~/lib/utils/url_utility'; import appComponent from '~/groups/components/app.vue'; import groupFolderComponent from '~/groups/components/group_folder.vue'; import groupItemComponent from '~/groups/components/group_item.vue'; - import eventHub from '~/groups/event_hub'; import GroupsStore from '~/groups/store/groups_store'; import GroupsService from '~/groups/service/groups_service'; @@ -176,7 +176,7 @@ describe('AppComponent', () => { it('should fetch groups for provided page details and update window state', (done) => { spyOn(vm, 'fetchGroups').and.returnValue(returnServicePromise(mockGroups)); spyOn(vm, 'updateGroups').and.callThrough(); - spyOn(gl.utils, 'mergeUrlParams').and.callThrough(); + spyOn(utils, 'mergeUrlParams').and.callThrough(); spyOn(window.history, 'replaceState'); spyOn($, 'scrollTo'); @@ -192,7 +192,7 @@ describe('AppComponent', () => { setTimeout(() => { expect(vm.isLoading).toBeFalsy(); expect($.scrollTo).toHaveBeenCalledWith(0); - expect(gl.utils.mergeUrlParams).toHaveBeenCalledWith({ page: 2 }, jasmine.any(String)); + expect(utils.mergeUrlParams).toHaveBeenCalledWith({ page: 2 }, jasmine.any(String)); expect(window.history.replaceState).toHaveBeenCalledWith({ page: jasmine.any(String), }, jasmine.any(String), jasmine.any(String)); diff --git a/spec/javascripts/groups/components/group_item_spec.js b/spec/javascripts/groups/components/group_item_spec.js index 0f4fbdae445..618d0022e4f 100644 --- a/spec/javascripts/groups/components/group_item_spec.js +++ b/spec/javascripts/groups/components/group_item_spec.js @@ -1,5 +1,5 @@ import Vue from 'vue'; - +import * as urlUtils from '~/lib/utils/url_utility'; import groupItemComponent from '~/groups/components/group_item.vue'; import groupFolderComponent from '~/groups/components/group_folder.vue'; import eventHub from '~/groups/event_hub'; @@ -136,13 +136,13 @@ describe('GroupItemComponent', () => { const group = Object.assign({}, mockParentGroupItem); group.childrenCount = 0; const newVm = createComponent(group); - spyOn(gl.utils, 'visitUrl').and.stub(); + spyOn(urlUtils, 'visitUrl').and.stub(); spyOn(eventHub, '$emit'); newVm.onClickRowGroup(event); setTimeout(() => { expect(eventHub.$emit).not.toHaveBeenCalled(); - expect(gl.utils.visitUrl).toHaveBeenCalledWith(newVm.group.relativePath); + expect(urlUtils.visitUrl).toHaveBeenCalledWith(newVm.group.relativePath); done(); }, 0); }); diff --git a/spec/javascripts/groups/components/item_actions_spec.js b/spec/javascripts/groups/components/item_actions_spec.js index 2ce1a749a96..7a5c1da4d1d 100644 --- a/spec/javascripts/groups/components/item_actions_spec.js +++ b/spec/javascripts/groups/components/item_actions_spec.js @@ -36,27 +36,27 @@ describe('ItemActionsComponent', () => { describe('methods', () => { describe('onLeaveGroup', () => { - it('should change `dialogStatus` prop to `true` which shows confirmation dialog', () => { - expect(vm.dialogStatus).toBeFalsy(); + it('should change `modalStatus` prop to `true` which shows confirmation dialog', () => { + expect(vm.modalStatus).toBeFalsy(); vm.onLeaveGroup(); - expect(vm.dialogStatus).toBeTruthy(); + expect(vm.modalStatus).toBeTruthy(); }); }); describe('leaveGroup', () => { - it('should change `dialogStatus` prop to `false` and emit `leaveGroup` event with required params when called with `leaveConfirmed` as `true`', () => { + it('should change `modalStatus` prop to `false` and emit `leaveGroup` event with required params when called with `leaveConfirmed` as `true`', () => { spyOn(eventHub, '$emit'); - vm.dialogStatus = true; + vm.modalStatus = true; vm.leaveGroup(true); - expect(vm.dialogStatus).toBeFalsy(); + expect(vm.modalStatus).toBeFalsy(); expect(eventHub.$emit).toHaveBeenCalledWith('leaveGroup', vm.group, vm.parentGroup); }); - it('should change `dialogStatus` prop to `false` and should NOT emit `leaveGroup` event when called with `leaveConfirmed` as `false`', () => { + it('should change `modalStatus` prop to `false` and should NOT emit `leaveGroup` event when called with `leaveConfirmed` as `false`', () => { spyOn(eventHub, '$emit'); - vm.dialogStatus = true; + vm.modalStatus = true; vm.leaveGroup(false); - expect(vm.dialogStatus).toBeFalsy(); + expect(vm.modalStatus).toBeFalsy(); expect(eventHub.$emit).not.toHaveBeenCalled(); }); }); @@ -99,9 +99,9 @@ describe('ItemActionsComponent', () => { newVm.$destroy(); }); - it('should show modal dialog when `dialogStatus` is set to `true`', () => { - vm.dialogStatus = true; - const modalDialogEl = vm.$el.querySelector('.modal.popup-dialog'); + it('should show modal dialog when `modalStatus` is set to `true`', () => { + vm.modalStatus = true; + const modalDialogEl = vm.$el.querySelector('.modal'); expect(modalDialogEl).toBeDefined(); expect(modalDialogEl.querySelector('.modal-title').innerText.trim()).toBe('Are you sure?'); expect(modalDialogEl.querySelector('.btn.btn-warning').innerText.trim()).toBe('Leave'); diff --git a/spec/javascripts/helpers/locale_helper.js b/spec/javascripts/helpers/locale_helper.js new file mode 100644 index 00000000000..99e6ce61234 --- /dev/null +++ b/spec/javascripts/helpers/locale_helper.js @@ -0,0 +1,11 @@ +/* eslint-disable import/prefer-default-export */ + +export const setLanguage = (languageCode) => { + const htmlElement = document.querySelector('html'); + + if (languageCode) { + htmlElement.setAttribute('lang', languageCode); + } else { + htmlElement.removeAttribute('lang'); + } +}; diff --git a/spec/javascripts/image_diff/helpers/badge_helper_spec.js b/spec/javascripts/image_diff/helpers/badge_helper_spec.js index fb9c7e59031..ce3add1fd90 100644 --- a/spec/javascripts/image_diff/helpers/badge_helper_spec.js +++ b/spec/javascripts/image_diff/helpers/badge_helper_spec.js @@ -89,15 +89,8 @@ describe('badge helper', () => { }); it('should create icon comment button', () => { - const iconEl = buttonEl.querySelector('i'); + const iconEl = buttonEl.querySelector('svg'); expect(iconEl).toBeDefined(); - expect(iconEl.classList.contains('fa')).toEqual(true); - expect(iconEl.classList.contains('fa-comment-o')).toEqual(true); - }); - - it('should have .image-comment-badge.inverted in button class', () => { - expect(buttonEl.classList.contains('image-comment-badge')).toEqual(true); - expect(buttonEl.classList.contains('inverted')).toEqual(true); }); }); diff --git a/spec/javascripts/issue_show/components/app_spec.js b/spec/javascripts/issue_show/components/app_spec.js index b47a8bf705f..7159148f8fa 100644 --- a/spec/javascripts/issue_show/components/app_spec.js +++ b/spec/javascripts/issue_show/components/app_spec.js @@ -1,9 +1,11 @@ import Vue from 'vue'; import '~/render_math'; import '~/render_gfm'; +import * as urlUtils from '~/lib/utils/url_utility'; import issuableApp from '~/issue_show/components/app.vue'; import eventHub from '~/issue_show/event_hub'; import issueShowData from '../mock_data'; +import setTimeoutPromise from '../../helpers/set_timeout_promise_helper'; function formatText(text) { return text.trim().replace(/\s\s+/g, ' '); @@ -55,6 +57,8 @@ describe('Issuable output', () => { Vue.http.interceptors = _.without(Vue.http.interceptors, interceptor); vm.poll.stop(); + + vm.$destroy(); }); it('should render a title/description/edited and update title/description/edited on update', (done) => { @@ -177,7 +181,7 @@ describe('Issuable output', () => { }); it('does not redirect if issue has not moved', (done) => { - spyOn(gl.utils, 'visitUrl'); + spyOn(urlUtils, 'visitUrl'); spyOn(vm.service, 'updateIssuable').and.callFake(() => new Promise((resolve) => { resolve({ json() { @@ -193,7 +197,7 @@ describe('Issuable output', () => { setTimeout(() => { expect( - gl.utils.visitUrl, + urlUtils.visitUrl, ).not.toHaveBeenCalled(); done(); @@ -201,7 +205,7 @@ describe('Issuable output', () => { }); it('redirects if returned web_url has changed', (done) => { - spyOn(gl.utils, 'visitUrl'); + spyOn(urlUtils, 'visitUrl'); spyOn(vm.service, 'updateIssuable').and.callFake(() => new Promise((resolve) => { resolve({ json() { @@ -217,7 +221,7 @@ describe('Issuable output', () => { setTimeout(() => { expect( - gl.utils.visitUrl, + urlUtils.visitUrl, ).toHaveBeenCalledWith('/testing-issue-move'); done(); @@ -268,9 +272,55 @@ describe('Issuable output', () => { }); }); + it('opens recaptcha modal if update rejected as spam', (done) => { + function mockScriptSrc() { + const recaptchaChild = vm.$children + .find(child => child.$options._componentTag === 'recaptcha-modal'); // eslint-disable-line no-underscore-dangle + + recaptchaChild.scriptSrc = '//scriptsrc'; + } + + let modal; + const promise = new Promise((resolve) => { + resolve({ + json() { + return { + recaptcha_html: '<div class="g-recaptcha">recaptcha_html</div>', + }; + }, + }); + }); + + spyOn(vm.service, 'updateIssuable').and.returnValue(promise); + + vm.canUpdate = true; + vm.showForm = true; + + vm.$nextTick() + .then(() => mockScriptSrc()) + .then(() => vm.updateIssuable()) + .then(promise) + .then(() => setTimeoutPromise()) + .then(() => { + modal = vm.$el.querySelector('.js-recaptcha-modal'); + + expect(modal.style.display).not.toEqual('none'); + expect(modal.querySelector('.g-recaptcha').textContent).toEqual('recaptcha_html'); + expect(document.body.querySelector('.js-recaptcha-script').src).toMatch('//scriptsrc'); + }) + .then(() => modal.querySelector('.close').click()) + .then(() => vm.$nextTick()) + .then(() => { + expect(modal.style.display).toEqual('none'); + expect(document.body.querySelector('.js-recaptcha-script')).toBeNull(); + }) + .then(done) + .catch(done.fail); + }); + describe('deleteIssuable', () => { it('changes URL when deleted', (done) => { - spyOn(gl.utils, 'visitUrl'); + spyOn(urlUtils, 'visitUrl'); spyOn(vm.service, 'deleteIssuable').and.callFake(() => new Promise((resolve) => { resolve({ json() { @@ -283,7 +333,7 @@ describe('Issuable output', () => { setTimeout(() => { expect( - gl.utils.visitUrl, + urlUtils.visitUrl, ).toHaveBeenCalledWith('/test'); done(); @@ -291,7 +341,7 @@ describe('Issuable output', () => { }); it('stops polling when deleting', (done) => { - spyOn(gl.utils, 'visitUrl'); + spyOn(urlUtils, 'visitUrl'); spyOn(vm.poll, 'stop').and.callThrough(); spyOn(vm.service, 'deleteIssuable').and.callFake(() => new Promise((resolve) => { resolve({ diff --git a/spec/javascripts/issue_show/components/description_spec.js b/spec/javascripts/issue_show/components/description_spec.js index 163e5cdd062..0da25bdca9c 100644 --- a/spec/javascripts/issue_show/components/description_spec.js +++ b/spec/javascripts/issue_show/components/description_spec.js @@ -51,6 +51,35 @@ describe('Description component', () => { }); }); + it('opens recaptcha dialog if update rejected as spam', (done) => { + let modal; + const recaptchaChild = vm.$children + .find(child => child.$options._componentTag === 'recaptcha-modal'); // eslint-disable-line no-underscore-dangle + + recaptchaChild.scriptSrc = '//scriptsrc'; + + vm.taskListUpdateSuccess({ + recaptcha_html: '<div class="g-recaptcha">recaptcha_html</div>', + }); + + vm.$nextTick() + .then(() => { + modal = vm.$el.querySelector('.js-recaptcha-modal'); + + expect(modal.style.display).not.toEqual('none'); + expect(modal.querySelector('.g-recaptcha').textContent).toEqual('recaptcha_html'); + expect(document.body.querySelector('.js-recaptcha-script').src).toMatch('//scriptsrc'); + }) + .then(() => modal.querySelector('.close').click()) + .then(() => vm.$nextTick()) + .then(() => { + expect(modal.style.display).toEqual('none'); + expect(document.body.querySelector('.js-recaptcha-script')).toBeNull(); + }) + .then(done) + .catch(done.fail); + }); + describe('TaskList', () => { beforeEach(() => { vm = mountComponent(DescriptionComponent, Object.assign({}, props, { @@ -86,6 +115,7 @@ describe('Description component', () => { dataType: 'issuableType', fieldName: 'description', selector: '.detail-page-description', + onSuccess: jasmine.any(Function), }); done(); }); diff --git a/spec/javascripts/issue_spec.js b/spec/javascripts/issue_spec.js index 3636aac79a0..2cd2e63b15d 100644 --- a/spec/javascripts/issue_spec.js +++ b/spec/javascripts/issue_spec.js @@ -55,7 +55,7 @@ describe('Issue', function() { } function findElements(isIssueInitiallyOpen) { - $boxClosed = $('div.status-box-closed'); + $boxClosed = $('div.status-box-issue-closed'); expect($boxClosed).toExist(); expect($boxClosed).toHaveText('Closed'); diff --git a/spec/javascripts/job_spec.js b/spec/javascripts/job_spec.js index 5e67911d338..4f06237deb5 100644 --- a/spec/javascripts/job_spec.js +++ b/spec/javascripts/job_spec.js @@ -1,6 +1,6 @@ import { bytesToKiB } from '~/lib/utils/number_utils'; +import * as urlUtils from '~/lib/utils/url_utility'; import '~/lib/utils/datetime_utility'; -import '~/lib/utils/url_utility'; import Job from '~/job'; import '~/breakpoints'; @@ -28,7 +28,7 @@ describe('Job', () => { }); it('copies build options', function () { - expect(this.job.pageUrl).toBe(JOB_URL); + expect(this.job.pagePath).toBe(JOB_URL); expect(this.job.buildStatus).toBe('success'); expect(this.job.buildStage).toBe('test'); expect(this.job.state).toBe(''); @@ -65,7 +65,7 @@ describe('Job', () => { const deferred2 = $.Deferred(); const deferred3 = $.Deferred(); spyOn($, 'ajax').and.returnValues(deferred1.promise(), deferred2.promise(), deferred3.promise()); - spyOn(gl.utils, 'visitUrl'); + spyOn(urlUtils, 'visitUrl'); deferred1.resolve({ html: '<span>Update<span>', @@ -103,7 +103,7 @@ describe('Job', () => { spyOn($, 'ajax').and.returnValues(deferred1.promise(), deferred2.promise(), deferred3.promise()); - spyOn(gl.utils, 'visitUrl'); + spyOn(urlUtils, 'visitUrl'); deferred1.resolve({ html: '<span>Update<span>', @@ -134,7 +134,7 @@ describe('Job', () => { describe('truncated information', () => { describe('when size is less than total', () => { it('shows information about truncated log', () => { - spyOn(gl.utils, 'visitUrl'); + spyOn(urlUtils, 'visitUrl'); const deferred = $.Deferred(); spyOn($, 'ajax').and.returnValue(deferred.promise()); @@ -153,7 +153,7 @@ describe('Job', () => { it('shows the size in KiB', () => { const size = 50; - spyOn(gl.utils, 'visitUrl'); + spyOn(urlUtils, 'visitUrl'); const deferred = $.Deferred(); spyOn($, 'ajax').and.returnValue(deferred.promise()); @@ -179,7 +179,7 @@ describe('Job', () => { spyOn($, 'ajax').and.returnValues(deferred1.promise(), deferred2.promise(), deferred3.promise()); - spyOn(gl.utils, 'visitUrl'); + spyOn(urlUtils, 'visitUrl'); deferred1.resolve({ html: '<span>Update</span>', @@ -214,7 +214,7 @@ describe('Job', () => { it('renders the raw link', () => { const deferred = $.Deferred(); - spyOn(gl.utils, 'visitUrl'); + spyOn(urlUtils, 'visitUrl'); spyOn($, 'ajax').and.returnValue(deferred.promise()); deferred.resolve({ @@ -236,7 +236,7 @@ describe('Job', () => { describe('when size is equal than total', () => { it('does not show the trunctated information', () => { const deferred = $.Deferred(); - spyOn(gl.utils, 'visitUrl'); + spyOn(urlUtils, 'visitUrl'); spyOn($, 'ajax').and.returnValue(deferred.promise()); deferred.resolve({ @@ -257,7 +257,7 @@ describe('Job', () => { describe('output trace', () => { beforeEach(() => { const deferred = $.Deferred(); - spyOn(gl.utils, 'visitUrl'); + spyOn(urlUtils, 'visitUrl'); spyOn($, 'ajax').and.returnValue(deferred.promise()); deferred.resolve({ diff --git a/spec/javascripts/lib/utils/tick_formats_spec.js b/spec/javascripts/lib/utils/tick_formats_spec.js new file mode 100644 index 00000000000..283989b4fc8 --- /dev/null +++ b/spec/javascripts/lib/utils/tick_formats_spec.js @@ -0,0 +1,40 @@ +import { dateTickFormat, initDateFormats } from '~/lib/utils/tick_formats'; + +import { setLanguage } from '../../helpers/locale_helper'; + +describe('tick formats', () => { + describe('dateTickFormat', () => { + beforeAll(() => { + setLanguage('de'); + initDateFormats(); + }); + + afterAll(() => { + setLanguage(null); + }); + + it('returns year for first of January', () => { + const tick = dateTickFormat(new Date('2001-01-01')); + + expect(tick).toBe('2001'); + }); + + it('returns month for first of February', () => { + const tick = dateTickFormat(new Date('2001-02-01')); + + expect(tick).toBe('Februar'); + }); + + it('returns day and month for second of February', () => { + const tick = dateTickFormat(new Date('2001-02-02')); + + expect(tick).toBe('2. Feb.'); + }); + + it('ignores time', () => { + const tick = dateTickFormat(new Date('2001-02-02 12:34:56')); + + expect(tick).toBe('2. Feb.'); + }); + }); +}); diff --git a/spec/javascripts/line_highlighter_spec.js b/spec/javascripts/line_highlighter_spec.js index 645664a5219..89f4b85541d 100644 --- a/spec/javascripts/line_highlighter_spec.js +++ b/spec/javascripts/line_highlighter_spec.js @@ -1,7 +1,6 @@ /* eslint-disable space-before-function-paren, no-var, no-param-reassign, quotes, prefer-template, no-else-return, new-cap, dot-notation, no-return-assign, comma-dangle, no-new, one-var, one-var-declaration-per-line, jasmine/no-spec-dupes, no-underscore-dangle, max-len */ -/* global LineHighlighter */ -import '~/line_highlighter'; +import LineHighlighter from '~/line_highlighter'; (function() { describe('LineHighlighter', function() { diff --git a/spec/javascripts/locale/index_spec.js b/spec/javascripts/locale/index_spec.js new file mode 100644 index 00000000000..29b0b21eed7 --- /dev/null +++ b/spec/javascripts/locale/index_spec.js @@ -0,0 +1,35 @@ +import { createDateTimeFormat, languageCode } from '~/locale'; + +import { setLanguage } from '../helpers/locale_helper'; + +describe('locale', () => { + afterEach(() => { + setLanguage(null); + }); + + describe('languageCode', () => { + it('parses the lang attribute', () => { + setLanguage('ja'); + + expect(languageCode()).toBe('ja'); + }); + + it('falls back to English', () => { + setLanguage(null); + + expect(languageCode()).toBe('en'); + }); + }); + + describe('createDateTimeFormat', () => { + beforeEach(() => { + setLanguage('de'); + }); + + it('creates an instance of Intl.DateTimeFormat', () => { + const dateFormat = createDateTimeFormat({ year: 'numeric', month: 'long', day: 'numeric' }); + + expect(dateFormat.format(new Date(2015, 6, 3))).toBe('3. Juli 2015'); + }); + }); +}); diff --git a/spec/javascripts/merge_request_spec.js b/spec/javascripts/merge_request_spec.js index 70ae63ba036..2f02c11482f 100644 --- a/spec/javascripts/merge_request_spec.js +++ b/spec/javascripts/merge_request_spec.js @@ -1,7 +1,6 @@ /* eslint-disable space-before-function-paren, no-return-assign */ -/* global MergeRequest */ -import '~/merge_request'; +import MergeRequest from '~/merge_request'; import CloseReopenReportToggle from '~/close_reopen_report_toggle'; import IssuablesHelper from '~/helpers/issuables_helper'; diff --git a/spec/javascripts/merge_request_tabs_spec.js b/spec/javascripts/merge_request_tabs_spec.js index e441d1153ed..31426ceb110 100644 --- a/spec/javascripts/merge_request_tabs_spec.js +++ b/spec/javascripts/merge_request_tabs_spec.js @@ -1,7 +1,8 @@ /* eslint-disable no-var, comma-dangle, object-shorthand */ /* global Notes */ -import '~/merge_request_tabs'; +import * as urlUtils from '~/lib/utils/url_utility'; +import MergeRequestTabs from '~/merge_request_tabs'; import '~/commit/pipelines/pipelines_bundle'; import '~/breakpoints'; import '~/lib/utils/common_utils'; @@ -31,7 +32,7 @@ import 'vendor/jquery.scrollTo'; ); beforeEach(function () { - this.class = new gl.MergeRequestTabs({ stubLocation: stubLocation }); + this.class = new MergeRequestTabs({ stubLocation: stubLocation }); setLocation(); this.spies = { @@ -333,7 +334,7 @@ import 'vendor/jquery.scrollTo'; describe('with note fragment hash', () => { it('should expand and scroll to linked fragment hash #note_xxx', function () { - spyOn(window.gl.utils, 'getLocationHash').and.returnValue(noteId); + spyOn(urlUtils, 'getLocationHash').and.returnValue(noteId); this.class.loadDiff('/foo/bar/merge_requests/1/diffs'); expect(noteId.length).toBeGreaterThan(0); @@ -345,7 +346,7 @@ import 'vendor/jquery.scrollTo'; }); it('should gracefully ignore non-existant fragment hash', function () { - spyOn(window.gl.utils, 'getLocationHash').and.returnValue('note_something-that-does-not-exist'); + spyOn(urlUtils, 'getLocationHash').and.returnValue('note_something-that-does-not-exist'); this.class.loadDiff('/foo/bar/merge_requests/1/diffs'); expect(window.notes.toggleDiffNote).not.toHaveBeenCalled(); @@ -354,7 +355,7 @@ import 'vendor/jquery.scrollTo'; describe('with line number fragment hash', () => { it('should gracefully ignore line number fragment hash', function () { - spyOn(window.gl.utils, 'getLocationHash').and.returnValue(noteLineNumId); + spyOn(urlUtils, 'getLocationHash').and.returnValue(noteLineNumId); this.class.loadDiff('/foo/bar/merge_requests/1/diffs'); expect(noteLineNumId.length).toBeGreaterThan(0); @@ -387,7 +388,7 @@ import 'vendor/jquery.scrollTo'; describe('with note fragment hash', () => { it('should expand and scroll to linked fragment hash #note_xxx', function () { - spyOn(window.gl.utils, 'getLocationHash').and.returnValue(noteId); + spyOn(urlUtils, 'getLocationHash').and.returnValue(noteId); this.class.loadDiff('/foo/bar/merge_requests/1/diffs'); @@ -400,7 +401,7 @@ import 'vendor/jquery.scrollTo'; }); it('should gracefully ignore non-existant fragment hash', function () { - spyOn(window.gl.utils, 'getLocationHash').and.returnValue('note_something-that-does-not-exist'); + spyOn(urlUtils, 'getLocationHash').and.returnValue('note_something-that-does-not-exist'); this.class.loadDiff('/foo/bar/merge_requests/1/diffs'); expect(window.notes.toggleDiffNote).not.toHaveBeenCalled(); @@ -409,7 +410,7 @@ import 'vendor/jquery.scrollTo'; describe('with line number fragment hash', () => { it('should gracefully ignore line number fragment hash', function () { - spyOn(window.gl.utils, 'getLocationHash').and.returnValue(noteLineNumId); + spyOn(urlUtils, 'getLocationHash').and.returnValue(noteLineNumId); this.class.loadDiff('/foo/bar/merge_requests/1/diffs'); expect(noteLineNumId.length).toBeGreaterThan(0); diff --git a/spec/javascripts/monitoring/graph/deployment_spec.js b/spec/javascripts/monitoring/graph/deployment_spec.js index dea42d755d4..bf6ada8185e 100644 --- a/spec/javascripts/monitoring/graph/deployment_spec.js +++ b/spec/javascripts/monitoring/graph/deployment_spec.js @@ -118,7 +118,7 @@ describe('MonitoringDeployment', () => { ).not.toEqual('display: none;'); }); - it('shows the refText inside a text element with the deploy-info-text class', () => { + it('contains date, refs and the "deployed" text', () => { reducedDeploymentData[0].showDeploymentFlag = true; const component = createComponent({ showDeployInfo: true, @@ -129,8 +129,31 @@ describe('MonitoringDeployment', () => { }); expect( - component.$el.querySelector('.deploy-info-text').firstChild.nodeValue.trim(), - ).toEqual(component.refText(reducedDeploymentData[0])); + component.$el.querySelectorAll('.deploy-info-text'), + ).toContainText('Deployed'); + + expect( + component.$el.querySelectorAll('.deploy-info-text'), + ).toContainText('Wed, May 31'); + + expect( + component.$el.querySelectorAll('.deploy-info-text'), + ).toContainText(component.refText(reducedDeploymentData[0])); + }); + + it('contains a link to the commit contents', () => { + reducedDeploymentData[0].showDeploymentFlag = true; + const component = createComponent({ + showDeployInfo: true, + deploymentData: reducedDeploymentData, + graphHeight: 300, + graphWidth: 440, + graphHeightOffset: 120, + }); + + expect( + component.$el.querySelectorAll('.deploy-info-text-link')[0].parentElement.getAttribute('xlink:href'), + ).not.toEqual(''); }); it('should contain a hidden gradient', () => { diff --git a/spec/javascripts/monitoring/graph_spec.js b/spec/javascripts/monitoring/graph_spec.js index fd79abe241a..b1d69752bad 100644 --- a/spec/javascripts/monitoring/graph_spec.js +++ b/spec/javascripts/monitoring/graph_spec.js @@ -4,6 +4,8 @@ import MonitoringMixins from '~/monitoring/mixins/monitoring_mixins'; import eventHub from '~/monitoring/event_hub'; import { deploymentData, convertDatesMultipleSeries, singleRowMetricsMultipleSeries } from './mock_data'; +const tagsPath = 'http://test.host/frontend-fixtures/environments-project/tags'; +const projectPath = 'http://test.host/frontend-fixtures/environments-project'; const createComponent = (propsData) => { const Component = Vue.extend(Graph); @@ -25,6 +27,8 @@ describe('Graph', () => { classType: 'col-md-6', updateAspectRatio: false, deploymentData, + tagsPath, + projectPath, }); expect(component.$el.querySelector('.text-center').innerText.trim()).toBe(component.graphData.title); @@ -37,6 +41,8 @@ describe('Graph', () => { classType: 'col-md-6', updateAspectRatio: false, deploymentData, + tagsPath, + projectPath, }); const transformedHeight = `${component.graphHeight - 100}`; @@ -50,6 +56,8 @@ describe('Graph', () => { classType: 'col-md-6', updateAspectRatio: false, deploymentData, + tagsPath, + projectPath, }); const viewBoxArray = component.outerViewBox.split(' '); @@ -65,6 +73,8 @@ describe('Graph', () => { classType: 'col-md-6', updateAspectRatio: false, deploymentData, + tagsPath, + projectPath, }); spyOn(eventHub, '$emit'); @@ -81,6 +91,8 @@ describe('Graph', () => { classType: 'col-md-6', updateAspectRatio: false, deploymentData, + tagsPath, + projectPath, }); expect(component.yAxisLabel).toEqual(component.graphData.y_label); @@ -98,6 +110,8 @@ describe('Graph', () => { hoveredDate: new Date('Sun Aug 27 2017 06:11:51 GMT-0500 (CDT)'), currentDeployXPos: null, }, + tagsPath, + projectPath, }); component.positionFlag(); diff --git a/spec/javascripts/monitoring/mock_data.js b/spec/javascripts/monitoring/mock_data.js index 6b34855b8b2..1f4e858e731 100644 --- a/spec/javascripts/monitoring/mock_data.js +++ b/spec/javascripts/monitoring/mock_data.js @@ -2430,33 +2430,39 @@ export const deploymentData = [ id: 111, iid: 3, sha: 'f5bcd1d9dac6fa4137e2510b9ccd134ef2e84187', + commitUrl: 'http://test.host/frontend-fixtures/environments-project/commit/f5bcd1d9dac6fa4137e2510b9ccd134ef2e84187', ref: { name: 'master' }, created_at: '2017-05-31T21:23:37.881Z', tag: false, + tagUrl: 'http://test.host/frontend-fixtures/environments-project/tags/false', 'last?': true }, { id: 110, iid: 2, sha: 'f5bcd1d9dac6fa4137e2510b9ccd134ef2e84187', + commitUrl: 'http://test.host/frontend-fixtures/environments-project/commit/f5bcd1d9dac6fa4137e2510b9ccd134ef2e84187', ref: { name: 'master' }, created_at: '2017-05-30T20:08:04.629Z', tag: false, + tagUrl: 'http://test.host/frontend-fixtures/environments-project/tags/false', 'last?': false }, { id: 109, iid: 1, sha: '6511e58faafaa7ad2228990ec57f19d66f7db7c2', + commitUrl: 'http://test.host/frontend-fixtures/environments-project/commit/6511e58faafaa7ad2228990ec57f19d66f7db7c2', ref: { name: 'update2-readme' }, created_at: '2017-05-30T17:42:38.409Z', tag: false, + tagUrl: 'http://test.host/frontend-fixtures/environments-project/tags/false', 'last?': false } ]; diff --git a/spec/javascripts/notes/components/issue_comment_form_spec.js b/spec/javascripts/notes/components/comment_form_spec.js index 04a7f8e32f1..20e352dd8bd 100644 --- a/spec/javascripts/notes/components/issue_comment_form_spec.js +++ b/spec/javascripts/notes/components/comment_form_spec.js @@ -1,7 +1,7 @@ import Vue from 'vue'; import Autosize from 'autosize'; import store from '~/notes/stores'; -import issueCommentForm from '~/notes/components/issue_comment_form.vue'; +import issueCommentForm from '~/notes/components/comment_form.vue'; import { loggedOutnoteableData, notesDataMock, userDataMock, noteableDataMock } from '../mock_data'; import { keyboardDownEvent } from '../../issue_show/helpers'; diff --git a/spec/javascripts/notes/components/issue_note_app_spec.js b/spec/javascripts/notes/components/note_app_spec.js index 8e43037f356..7c8d6685ee1 100644 --- a/spec/javascripts/notes/components/issue_note_app_spec.js +++ b/spec/javascripts/notes/components/note_app_spec.js @@ -1,26 +1,15 @@ import Vue from 'vue'; -import issueNotesApp from '~/notes/components/issue_notes_app.vue'; +import notesApp from '~/notes/components/notes_app.vue'; import service from '~/notes/services/notes_service'; import * as mockData from '../mock_data'; +import getSetTimeoutPromise from '../../helpers/set_timeout_promise_helper'; -describe('issue_note_app', () => { +describe('note_app', () => { let mountComponent; let vm; - const individualNoteInterceptor = (request, next) => { - next(request.respondWith(JSON.stringify(mockData.individualNoteServerResponse), { - status: 200, - })); - }; - - const discussionNoteInterceptor = (request, next) => { - next(request.respondWith(JSON.stringify(mockData.discussionNoteServerResponse), { - status: 200, - })); - }; - beforeEach(() => { - const IssueNotesApp = Vue.extend(issueNotesApp); + const IssueNotesApp = Vue.extend(notesApp); mountComponent = (data) => { const props = data || { @@ -74,16 +63,16 @@ describe('issue_note_app', () => { describe('render', () => { beforeEach(() => { - Vue.http.interceptors.push(individualNoteInterceptor); + Vue.http.interceptors.push(mockData.individualNoteInterceptor); vm = mountComponent(); }); afterEach(() => { - Vue.http.interceptors = _.without(Vue.http.interceptors, individualNoteInterceptor); + Vue.http.interceptors = _.without(Vue.http.interceptors, mockData.individualNoteInterceptor); }); it('should render list of notes', (done) => { - const note = mockData.individualNoteServerResponse[0].notes[0]; + const note = mockData.INDIVIDUAL_NOTE_RESPONSE_MAP.GET['/gitlab-org/gitlab-ce/issues/26/discussions.json'][0].notes[0]; setTimeout(() => { expect( @@ -129,13 +118,16 @@ describe('issue_note_app', () => { describe('update note', () => { describe('individual note', () => { beforeEach(() => { - Vue.http.interceptors.push(individualNoteInterceptor); - spyOn(service, 'updateNote').and.callFake(() => Promise.resolve()); + Vue.http.interceptors.push(mockData.individualNoteInterceptor); + spyOn(service, 'updateNote').and.callThrough(); vm = mountComponent(); }); afterEach(() => { - Vue.http.interceptors = _.without(Vue.http.interceptors, individualNoteInterceptor); + Vue.http.interceptors = _.without( + Vue.http.interceptors, + mockData.individualNoteInterceptor, + ); }); it('renders edit form', (done) => { @@ -149,28 +141,36 @@ describe('issue_note_app', () => { }); it('calls the service to update the note', (done) => { - setTimeout(() => { - vm.$el.querySelector('.js-note-edit').click(); - Vue.nextTick(() => { + getSetTimeoutPromise() + .then(() => { + vm.$el.querySelector('.js-note-edit').click(); + }) + .then(Vue.nextTick) + .then(() => { vm.$el.querySelector('.js-vue-issue-note-form').value = 'this is a note'; vm.$el.querySelector('.js-vue-issue-save').click(); expect(service.updateNote).toHaveBeenCalled(); - done(); - }); - }, 0); + }) + // Wait for the requests to finish before destroying + .then(Vue.nextTick) + .then(done) + .catch(done.fail); }); }); describe('dicussion note', () => { beforeEach(() => { - Vue.http.interceptors.push(discussionNoteInterceptor); - spyOn(service, 'updateNote').and.callFake(() => Promise.resolve()); + Vue.http.interceptors.push(mockData.discussionNoteInterceptor); + spyOn(service, 'updateNote').and.callThrough(); vm = mountComponent(); }); afterEach(() => { - Vue.http.interceptors = _.without(Vue.http.interceptors, discussionNoteInterceptor); + Vue.http.interceptors = _.without( + Vue.http.interceptors, + mockData.discussionNoteInterceptor, + ); }); it('renders edit form', (done) => { @@ -184,16 +184,21 @@ describe('issue_note_app', () => { }); it('updates the note and resets the edit form', (done) => { - setTimeout(() => { - vm.$el.querySelector('.js-note-edit').click(); - Vue.nextTick(() => { + getSetTimeoutPromise() + .then(() => { + vm.$el.querySelector('.js-note-edit').click(); + }) + .then(Vue.nextTick) + .then(() => { vm.$el.querySelector('.js-vue-issue-note-form').value = 'this is a note'; vm.$el.querySelector('.js-vue-issue-save').click(); expect(service.updateNote).toHaveBeenCalled(); - done(); - }); - }, 0); + }) + // Wait for the requests to finish before destroying + .then(Vue.nextTick) + .then(done) + .catch(done.fail); }); }); }); @@ -216,12 +221,12 @@ describe('issue_note_app', () => { describe('edit form', () => { beforeEach(() => { - Vue.http.interceptors.push(individualNoteInterceptor); + Vue.http.interceptors.push(mockData.individualNoteInterceptor); vm = mountComponent(); }); afterEach(() => { - Vue.http.interceptors = _.without(Vue.http.interceptors, individualNoteInterceptor); + Vue.http.interceptors = _.without(Vue.http.interceptors, mockData.individualNoteInterceptor); }); it('should render markdown docs url', (done) => { diff --git a/spec/javascripts/notes/components/issue_note_body_spec.js b/spec/javascripts/notes/components/note_body_spec.js index 37aad50737b..b42e7943b98 100644 --- a/spec/javascripts/notes/components/issue_note_body_spec.js +++ b/spec/javascripts/notes/components/note_body_spec.js @@ -1,7 +1,7 @@ import Vue from 'vue'; import store from '~/notes/stores'; -import noteBody from '~/notes/components/issue_note_body.vue'; +import noteBody from '~/notes/components/note_body.vue'; import { noteableDataMock, notesDataMock, note } from '../mock_data'; describe('issue_note_body component', () => { diff --git a/spec/javascripts/notes/components/issue_note_form_spec.js b/spec/javascripts/notes/components/note_form_spec.js index d42ef239711..86e9e2a32a9 100644 --- a/spec/javascripts/notes/components/issue_note_form_spec.js +++ b/spec/javascripts/notes/components/note_form_spec.js @@ -1,6 +1,6 @@ import Vue from 'vue'; import store from '~/notes/stores'; -import issueNoteForm from '~/notes/components/issue_note_form.vue'; +import issueNoteForm from '~/notes/components/note_form.vue'; import { noteableDataMock, notesDataMock } from '../mock_data'; import { keyboardDownEvent } from '../../issue_show/helpers'; diff --git a/spec/javascripts/notes/components/issue_discussion_spec.js b/spec/javascripts/notes/components/noteable_discussion_spec.js index b6ae55d44f5..19504e4f7c8 100644 --- a/spec/javascripts/notes/components/issue_discussion_spec.js +++ b/spec/javascripts/notes/components/noteable_discussion_spec.js @@ -1,6 +1,6 @@ import Vue from 'vue'; import store from '~/notes/stores'; -import issueDiscussion from '~/notes/components/issue_discussion.vue'; +import issueDiscussion from '~/notes/components/noteable_discussion.vue'; import { noteableDataMock, discussionMock, notesDataMock } from '../mock_data'; describe('issue_discussion component', () => { @@ -30,7 +30,7 @@ describe('issue_discussion component', () => { it('should render discussion header', () => { expect(vm.$el.querySelector('.discussion-header')).toBeDefined(); - expect(vm.$el.querySelectorAll('.notes li').length).toEqual(discussionMock.notes.length); + expect(vm.$el.querySelector('.notes').children.length).toEqual(discussionMock.notes.length); }); describe('actions', () => { diff --git a/spec/javascripts/notes/components/issue_note_spec.js b/spec/javascripts/notes/components/noteable_note_spec.js index 73fd188dbe5..c8a6cb7e612 100644 --- a/spec/javascripts/notes/components/issue_note_spec.js +++ b/spec/javascripts/notes/components/noteable_note_spec.js @@ -1,7 +1,7 @@ import Vue from 'vue'; import store from '~/notes/stores'; -import issueNote from '~/notes/components/issue_note.vue'; +import issueNote from '~/notes/components/noteable_note.vue'; import { noteableDataMock, notesDataMock, note } from '../mock_data'; describe('issue_note', () => { @@ -41,4 +41,19 @@ describe('issue_note', () => { it('should render issue body', () => { expect(vm.$el.querySelector('.note-text').innerHTML).toEqual(note.note_html); }); + + it('prevents note preview xss', (done) => { + const imgSrc = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'; + const noteBody = `<img src="${imgSrc}" onload="alert(1)" />`; + const alertSpy = spyOn(window, 'alert'); + vm.updateNote = () => new Promise($.noop); + + vm.formUpdateHandler(noteBody, null, $.noop); + + setTimeout(() => { + expect(alertSpy).not.toHaveBeenCalled(); + expect(vm.note.note_html).toEqual(_.escape(noteBody)); + done(); + }, 0); + }); }); diff --git a/spec/javascripts/notes/mock_data.js b/spec/javascripts/notes/mock_data.js index 42497de3c55..6b608adff15 100644 --- a/spec/javascripts/notes/mock_data.js +++ b/spec/javascripts/notes/mock_data.js @@ -312,138 +312,212 @@ export const loggedOutnoteableData = { "preview_note_path": "/gitlab-org/gitlab-ce/preview_markdown?quick_actions_target_id=98&quick_actions_target_type=Issue" } -export const individualNoteServerResponse = [{ - "id": "0fb4e0e3f9276e55ff32eb4195add694aece4edd", - "reply_id": "0fb4e0e3f9276e55ff32eb4195add694aece4edd", - "expanded": true, - "notes": [{ - "id": 1390, - "attachment": { - "url": null, - "filename": null, - "image": false - }, - "author": { - "id": 1, - "name": "Root", - "username": "root", - "state": "active", - "avatar_url": null, - "path": "/root" - }, - "created_at": "2017-08-01T17:09:33.762Z", - "updated_at": "2017-08-01T17:09:33.762Z", - "system": false, - "noteable_id": 98, - "noteable_type": "Issue", - "type": null, - "human_access": "Owner", - "note": "sdfdsaf", - "note_html": "\u003cp dir=\"auto\"\u003esdfdsaf\u003c/p\u003e", - "current_user": { - "can_edit": true +export const INDIVIDUAL_NOTE_RESPONSE_MAP = { + 'GET': { + '/gitlab-org/gitlab-ce/issues/26/discussions.json': [{ + "id": "0fb4e0e3f9276e55ff32eb4195add694aece4edd", + "reply_id": "0fb4e0e3f9276e55ff32eb4195add694aece4edd", + "expanded": true, + "notes": [{ + "id": 1390, + "attachment": { + "url": null, + "filename": null, + "image": false + }, + "author": { + "id": 1, + "name": "Root", + "username": "root", + "state": "active", + "avatar_url": null, + "path": "/root" + }, + "created_at": "2017-08-01T17:09:33.762Z", + "updated_at": "2017-08-01T17:09:33.762Z", + "system": false, + "noteable_id": 98, + "noteable_type": "Issue", + "type": null, + "human_access": "Owner", + "note": "sdfdsaf", + "note_html": "\u003cp dir=\"auto\"\u003esdfdsaf\u003c/p\u003e", + "current_user": { + "can_edit": true + }, + "discussion_id": "0fb4e0e3f9276e55ff32eb4195add694aece4edd", + "emoji_awardable": true, + "award_emoji": [{ + "name": "baseball", + "user": { + "id": 1, + "name": "Root", + "username": "root" + } + }, { + "name": "art", + "user": { + "id": 1, + "name": "Root", + "username": "root" + } + }], + "toggle_award_path": "/gitlab-org/gitlab-ce/notes/1390/toggle_award_emoji", + "report_abuse_path": "/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F26%23note_1390\u0026user_id=1", + "path": "/gitlab-org/gitlab-ce/notes/1390" + }], + "individual_note": true + }, { + "id": "70d5c92a4039a36c70100c6691c18c27e4b0a790", + "reply_id": "70d5c92a4039a36c70100c6691c18c27e4b0a790", + "expanded": true, + "notes": [{ + "id": 1391, + "attachment": { + "url": null, + "filename": null, + "image": false + }, + "author": { + "id": 1, + "name": "Root", + "username": "root", + "state": "active", + "avatar_url": null, + "path": "/root" + }, + "created_at": "2017-08-02T10:51:38.685Z", + "updated_at": "2017-08-02T10:51:38.685Z", + "system": false, + "noteable_id": 98, + "noteable_type": "Issue", + "type": null, + "human_access": "Owner", + "note": "New note!", + "note_html": "\u003cp dir=\"auto\"\u003eNew note!\u003c/p\u003e", + "current_user": { + "can_edit": true + }, + "discussion_id": "70d5c92a4039a36c70100c6691c18c27e4b0a790", + "emoji_awardable": true, + "award_emoji": [], + "toggle_award_path": "/gitlab-org/gitlab-ce/notes/1391/toggle_award_emoji", + "report_abuse_path": "/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F26%23note_1391\u0026user_id=1", + "path": "/gitlab-org/gitlab-ce/notes/1391" + }], + "individual_note": true + }], + '/gitlab-org/gitlab-ce/noteable/issue/98/notes': { + last_fetched_at: 1512900838, + notes: [], }, - "discussion_id": "0fb4e0e3f9276e55ff32eb4195add694aece4edd", - "emoji_awardable": true, - "award_emoji": [{ - "name": "baseball", - "user": { + }, + 'PUT': { + '/gitlab-org/gitlab-ce/notes/1471': { + "commands_changes": null, + "valid": true, + "id": 1471, + "attachment": null, + "author": { "id": 1, "name": "Root", - "username": "root" - } - }, { - "name": "art", - "user": { + "username": "root", + "state": "active", + "avatar_url": null, + "path": "/root" + }, + "created_at": "2017-08-08T16:53:00.666Z", + "updated_at": "2017-12-10T11:03:21.876Z", + "system": false, + "noteable_id": 124, + "noteable_type": "Issue", + "noteable_iid": 29, + "type": "DiscussionNote", + "human_access": "Owner", + "note": "Adding a comment", + "note_html": "\u003cp dir=\"auto\"\u003eAdding a comment\u003c/p\u003e", + "last_edited_at": "2017-12-10T11:03:21.876Z", + "last_edited_by": { "id": 1, - "name": "Root", - "username": "root" - } - }], - "toggle_award_path": "/gitlab-org/gitlab-ce/notes/1390/toggle_award_emoji", - "report_abuse_path": "/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F26%23note_1390\u0026user_id=1", - "path": "/gitlab-org/gitlab-ce/notes/1390" - }], - "individual_note": true - }, { - "id": "70d5c92a4039a36c70100c6691c18c27e4b0a790", - "reply_id": "70d5c92a4039a36c70100c6691c18c27e4b0a790", - "expanded": true, - "notes": [{ - "id": 1391, - "attachment": { - "url": null, - "filename": null, - "image": false - }, - "author": { - "id": 1, - "name": "Root", - "username": "root", - "state": "active", - "avatar_url": null, - "path": "/root" - }, - "created_at": "2017-08-02T10:51:38.685Z", - "updated_at": "2017-08-02T10:51:38.685Z", - "system": false, - "noteable_id": 98, - "noteable_type": "Issue", - "type": null, - "human_access": "Owner", - "note": "New note!", - "note_html": "\u003cp dir=\"auto\"\u003eNew note!\u003c/p\u003e", - "current_user": { - "can_edit": true + "name": 'Root', + "username": 'root', + "state": 'active', + "avatar_url": null, + "path": '/root', + }, + "current_user": { + "can_edit": true + }, + "discussion_id": "a3ed36e29b1957efb3b68c53e2d7a2b24b1df052", + "emoji_awardable": true, + "award_emoji": [], + "toggle_award_path": "/gitlab-org/gitlab-ce/notes/1471/toggle_award_emoji", + "report_abuse_path": "/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F29%23note_1471\u0026user_id=1", + "path": "/gitlab-org/gitlab-ce/notes/1471" }, - "discussion_id": "70d5c92a4039a36c70100c6691c18c27e4b0a790", - "emoji_awardable": true, - "award_emoji": [], - "toggle_award_path": "/gitlab-org/gitlab-ce/notes/1391/toggle_award_emoji", - "report_abuse_path": "/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F26%23note_1391\u0026user_id=1", - "path": "/gitlab-org/gitlab-ce/notes/1391" - }], - "individual_note": true -}]; + } +}; -export const discussionNoteServerResponse = [{ - "id": "a3ed36e29b1957efb3b68c53e2d7a2b24b1df052", - "reply_id": "a3ed36e29b1957efb3b68c53e2d7a2b24b1df052", - "expanded": true, - "notes": [{ - "id": 1471, - "attachment": { - "url": null, - "filename": null, - "image": false - }, - "author": { - "id": 1, - "name": "Root", - "username": "root", - "state": "active", - "avatar_url": null, - "path": "/root" - }, - "created_at": "2017-08-08T16:53:00.666Z", - "updated_at": "2017-08-08T16:53:00.666Z", - "system": false, - "noteable_id": 124, - "noteable_type": "Issue", - "noteable_iid": 29, - "type": "DiscussionNote", - "human_access": "Owner", - "note": "Adding a comment", - "note_html": "\u003cp dir=\"auto\"\u003eAdding a comment\u003c/p\u003e", - "current_user": { - "can_edit": true - }, - "discussion_id": "a3ed36e29b1957efb3b68c53e2d7a2b24b1df052", - "emoji_awardable": true, - "award_emoji": [], - "toggle_award_path": "/gitlab-org/gitlab-ce/notes/1471/toggle_award_emoji", - "report_abuse_path": "/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F29%23note_1471\u0026user_id=1", - "path": "/gitlab-org/gitlab-ce/notes/1471" - }], - "individual_note": false -}]; +export const DISCUSSION_NOTE_RESPONSE_MAP = { + ...INDIVIDUAL_NOTE_RESPONSE_MAP, + 'GET': { + ...INDIVIDUAL_NOTE_RESPONSE_MAP.GET, + '/gitlab-org/gitlab-ce/issues/26/discussions.json': [{ + "id": "a3ed36e29b1957efb3b68c53e2d7a2b24b1df052", + "reply_id": "a3ed36e29b1957efb3b68c53e2d7a2b24b1df052", + "expanded": true, + "notes": [{ + "id": 1471, + "attachment": { + "url": null, + "filename": null, + "image": false + }, + "author": { + "id": 1, + "name": "Root", + "username": "root", + "state": "active", + "avatar_url": null, + "path": "/root" + }, + "created_at": "2017-08-08T16:53:00.666Z", + "updated_at": "2017-08-08T16:53:00.666Z", + "system": false, + "noteable_id": 124, + "noteable_type": "Issue", + "noteable_iid": 29, + "type": "DiscussionNote", + "human_access": "Owner", + "note": "Adding a comment", + "note_html": "\u003cp dir=\"auto\"\u003eAdding a comment\u003c/p\u003e", + "current_user": { + "can_edit": true + }, + "discussion_id": "a3ed36e29b1957efb3b68c53e2d7a2b24b1df052", + "emoji_awardable": true, + "award_emoji": [], + "toggle_award_path": "/gitlab-org/gitlab-ce/notes/1471/toggle_award_emoji", + "report_abuse_path": "/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F29%23note_1471\u0026user_id=1", + "path": "/gitlab-org/gitlab-ce/notes/1471" + }], + "individual_note": false + }], + }, +}; + +export function individualNoteInterceptor(request, next) { + const body = INDIVIDUAL_NOTE_RESPONSE_MAP[request.method.toUpperCase()][request.url]; + + next(request.respondWith(JSON.stringify(body), { + status: 200, + })); +} + +export function discussionNoteInterceptor(request, next) { + const body = DISCUSSION_NOTE_RESPONSE_MAP[request.method.toUpperCase()][request.url]; + + next(request.respondWith(JSON.stringify(body), { + status: 200, + })); +} diff --git a/spec/javascripts/notes_spec.js b/spec/javascripts/notes_spec.js index 677a389b88f..e09b8dc7fc5 100644 --- a/spec/javascripts/notes_spec.js +++ b/spec/javascripts/notes_spec.js @@ -1,6 +1,7 @@ /* eslint-disable space-before-function-paren, no-unused-expressions, no-var, object-shorthand, comma-dangle, max-len */ /* global Notes */ +import * as urlUtils from '~/lib/utils/url_utility'; import 'autosize'; import '~/gl_form'; import '~/lib/utils/text_utility'; @@ -168,8 +169,7 @@ import '~/notes'; }); it('sets target when hash matches', () => { - spyOn(gl.utils, 'getLocationHash'); - gl.utils.getLocationHash.and.returnValue(hash); + spyOn(urlUtils, 'getLocationHash').and.returnValue(hash); Notes.updateNoteTargetSelector($note); @@ -178,8 +178,7 @@ import '~/notes'; }); it('unsets target when hash does not match', () => { - spyOn(gl.utils, 'getLocationHash'); - gl.utils.getLocationHash.and.returnValue('note_doesnotexist'); + spyOn(urlUtils, 'getLocationHash').and.returnValue('note_doesnotexist'); Notes.updateNoteTargetSelector($note); @@ -187,8 +186,7 @@ import '~/notes'; }); it('unsets target when there is not a hash fragment anymore', () => { - spyOn(gl.utils, 'getLocationHash'); - gl.utils.getLocationHash.and.returnValue(null); + spyOn(urlUtils, 'getLocationHash').and.returnValue(null); Notes.updateNoteTargetSelector($note); @@ -224,7 +222,6 @@ import '~/notes'; notes.note_ids = []; notes.updatedNotesTrackingMap = {}; - spyOn(gl.utils, 'localTimeAgo'); spyOn(Notes, 'isNewNote').and.callThrough(); spyOn(Notes, 'isUpdatedNote').and.callThrough(); spyOn(Notes, 'animateAppendNote').and.callThrough(); @@ -351,7 +348,6 @@ import '~/notes'; ]); notes.note_ids = []; - spyOn(gl.utils, 'localTimeAgo'); spyOn(Notes, 'isNewNote'); spyOn(Notes, 'animateAppendNote'); Notes.isNewNote.and.returnValue(true); diff --git a/spec/javascripts/pager_spec.js b/spec/javascripts/pager_spec.js index 1d3e1263371..2fd87754238 100644 --- a/spec/javascripts/pager_spec.js +++ b/spec/javascripts/pager_spec.js @@ -1,14 +1,9 @@ /* global fixture */ -import '~/pager'; +import * as utils from '~/lib/utils/url_utility'; +import Pager from '~/pager'; describe('pager', () => { - const Pager = window.Pager; - - it('is defined on window', () => { - expect(window.Pager).toBeDefined(); - }); - describe('init', () => { const originalHref = window.location.href; @@ -30,7 +25,7 @@ describe('pager', () => { it('should use current url if data-href attribute not provided', () => { const href = `${gl.TEST_HOST}/some_list`; - spyOn(gl.utils, 'removeParams').and.returnValue(href); + spyOn(utils, 'removeParams').and.returnValue(href); Pager.init(); expect(Pager.url).toBe(href); }); @@ -44,9 +39,9 @@ describe('pager', () => { it('keeps extra query parameters from url', () => { window.history.replaceState({}, null, '?filter=test&offset=100'); const href = `${gl.TEST_HOST}/some_list?filter=test`; - spyOn(gl.utils, 'removeParams').and.returnValue(href); + spyOn(utils, 'removeParams').and.returnValue(href); Pager.init(); - expect(gl.utils.removeParams).toHaveBeenCalledWith(['limit', 'offset']); + expect(utils.removeParams).toHaveBeenCalledWith(['limit', 'offset']); expect(Pager.url).toEqual(href); }); }); diff --git a/spec/javascripts/pipelines/graph/job_component_spec.js b/spec/javascripts/pipelines/graph/job_component_spec.js index 23c87610d83..35e36e9c353 100644 --- a/spec/javascripts/pipelines/graph/job_component_spec.js +++ b/spec/javascripts/pipelines/graph/job_component_spec.js @@ -113,4 +113,35 @@ describe('pipeline graph job component', () => { component.$el.querySelector('a').classList.contains('css-class-job-name'), ).toBe(true); }); + + describe('status label', () => { + it('should not render status label when it is not provided', () => { + component = mountComponent(JobComponent, { + job: { + id: 4256, + name: 'test', + status: { + icon: 'icon_status_success', + }, + }, + }); + + expect(component.$el.querySelector('.js-job-component-tooltip').getAttribute('data-original-title')).toEqual('test'); + }); + + it('should not render status label when it is provided', () => { + component = mountComponent(JobComponent, { + job: { + id: 4256, + name: 'test', + status: { + icon: 'icon_status_success', + label: 'success', + }, + }, + }); + + expect(component.$el.querySelector('.js-job-component-tooltip').getAttribute('data-original-title')).toEqual('test - success'); + }); + }); }); diff --git a/spec/javascripts/repo/components/repo_commit_section_spec.js b/spec/javascripts/repo/components/repo_commit_section_spec.js index 1c794123095..72712e058e5 100644 --- a/spec/javascripts/repo/components/repo_commit_section_spec.js +++ b/spec/javascripts/repo/components/repo_commit_section_spec.js @@ -1,4 +1,5 @@ import Vue from 'vue'; +import * as urlUtils from '~/lib/utils/url_utility'; import store from '~/repo/stores'; import service from '~/repo/services'; import repoCommitSection from '~/repo/components/repo_commit_section.vue'; @@ -97,7 +98,7 @@ describe('RepoCommitSection', () => { }); it('redirects to MR creation page if start new MR checkbox checked', (done) => { - spyOn(gl.utils, 'visitUrl'); + spyOn(urlUtils, 'visitUrl'); vm.startNewMR = true; vm.makeCommit(); @@ -105,7 +106,7 @@ describe('RepoCommitSection', () => { getSetTimeoutPromise() .then(() => Vue.nextTick()) .then(() => { - expect(gl.utils.visitUrl).toHaveBeenCalled(); + expect(urlUtils.visitUrl).toHaveBeenCalled(); }) .then(done) .catch(done.fail); diff --git a/spec/javascripts/repo/stores/actions/tree_spec.js b/spec/javascripts/repo/stores/actions/tree_spec.js index 393a797c6a3..2bbc49d5a9f 100644 --- a/spec/javascripts/repo/stores/actions/tree_spec.js +++ b/spec/javascripts/repo/stores/actions/tree_spec.js @@ -1,4 +1,5 @@ import Vue from 'vue'; +import * as urlUtils from '~/lib/utils/url_utility'; import store from '~/repo/stores'; import service from '~/repo/services'; import { file, resetStore } from '../../helpers'; @@ -255,7 +256,7 @@ describe('Multi-file store tree actions', () => { let row; beforeEach(() => { - spyOn(gl.utils, 'visitUrl'); + spyOn(urlUtils, 'visitUrl'); row = { url: 'submoduleurl', @@ -276,7 +277,7 @@ describe('Multi-file store tree actions', () => { it('opens submodule URL', (done) => { store.dispatch('clickedTreeRow', row) .then(() => { - expect(gl.utils.visitUrl).toHaveBeenCalledWith('submoduleurl'); + expect(urlUtils.visitUrl).toHaveBeenCalledWith('submoduleurl'); done(); }).catch(done.fail); diff --git a/spec/javascripts/repo/stores/actions_spec.js b/spec/javascripts/repo/stores/actions_spec.js index f2a7a698912..21d87e46216 100644 --- a/spec/javascripts/repo/stores/actions_spec.js +++ b/spec/javascripts/repo/stores/actions_spec.js @@ -1,4 +1,5 @@ import Vue from 'vue'; +import * as urlUtils from '~/lib/utils/url_utility'; import store from '~/repo/stores'; import service from '~/repo/services'; import { resetStore, file } from '../helpers'; @@ -10,11 +11,11 @@ describe('Multi-file store actions', () => { describe('redirectToUrl', () => { it('calls visitUrl', (done) => { - spyOn(gl.utils, 'visitUrl'); + spyOn(urlUtils, 'visitUrl'); store.dispatch('redirectToUrl', 'test') .then(() => { - expect(gl.utils.visitUrl).toHaveBeenCalledWith('test'); + expect(urlUtils.visitUrl).toHaveBeenCalledWith('test'); done(); }) @@ -326,13 +327,13 @@ describe('Multi-file store actions', () => { }); it('redirects to new merge request page', (done) => { - spyOn(gl.utils, 'visitUrl'); + spyOn(urlUtils, 'visitUrl'); store.state.endpoints.newMergeRequestUrl = 'newMergeRequestUrl?branch='; store.dispatch('commitChanges', { payload, newMr: true }) .then(() => { - expect(gl.utils.visitUrl).toHaveBeenCalledWith('newMergeRequestUrl?branch=master'); + expect(urlUtils.visitUrl).toHaveBeenCalledWith('newMergeRequestUrl?branch=master'); done(); }).catch(done.fail); diff --git a/spec/javascripts/right_sidebar_spec.js b/spec/javascripts/right_sidebar_spec.js index 5505f983d71..3267e29585b 100644 --- a/spec/javascripts/right_sidebar_spec.js +++ b/spec/javascripts/right_sidebar_spec.js @@ -1,8 +1,7 @@ /* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, new-parens, no-return-assign, new-cap, vars-on-top, max-len */ -/* global Sidebar */ import '~/commons/bootstrap'; -import '~/right_sidebar'; +import Sidebar from '~/right_sidebar'; (function() { var $aside, $icon, $labelsIcon, $page, $toggle, assertSidebarState; @@ -41,7 +40,7 @@ import '~/right_sidebar'; loadFixtures(fixtureName); this.sidebar = new Sidebar; $aside = $('.right-sidebar'); - $page = $('.page-with-sidebar'); + $page = $('.layout-page'); $icon = $aside.find('i'); $toggle = $aside.find('.js-sidebar-toggle'); return $labelsIcon = $aside.find('.sidebar-collapsed-icon'); diff --git a/spec/javascripts/search_autocomplete_spec.js b/spec/javascripts/search_autocomplete_spec.js index fdfc59a6f12..206f95abc1a 100644 --- a/spec/javascripts/search_autocomplete_spec.js +++ b/spec/javascripts/search_autocomplete_spec.js @@ -1,8 +1,9 @@ /* eslint-disable space-before-function-paren, max-len, no-var, one-var, one-var-declaration-per-line, no-unused-expressions, consistent-return, no-param-reassign, default-case, no-return-assign, comma-dangle, object-shorthand, prefer-template, quotes, new-parens, vars-on-top, new-cap, max-len */ import '~/gl_dropdown'; -import '~/search_autocomplete'; +import SearchAutocomplete from '~/search_autocomplete'; import '~/lib/utils/common_utils'; +import * as urlUtils from '~/lib/utils/url_utility'; (function() { var assertLinks, dashboardIssuesPath, dashboardMRsPath, groupIssuesPath, groupMRsPath, groupName, mockDashboardOptions, mockGroupOptions, mockProjectOptions, projectIssuesPath, projectMRsPath, projectName, userId, widget; @@ -121,13 +122,13 @@ import '~/lib/utils/common_utils'; loadFixtures('static/search_autocomplete.html.raw'); // Prevent turbolinks from triggering within gl_dropdown - spyOn(window.gl.utils, 'visitUrl').and.returnValue(true); + spyOn(urlUtils, 'visitUrl').and.returnValue(true); window.gon = {}; window.gon.current_user_id = userId; window.gon.current_username = userName; - return widget = new gl.SearchAutocomplete; + return widget = new SearchAutocomplete(); }); afterEach(function() { diff --git a/spec/javascripts/sidebar/sidebar_assignees_spec.js b/spec/javascripts/sidebar/sidebar_assignees_spec.js index 929ba75e67d..b97e24d9dcf 100644 --- a/spec/javascripts/sidebar/sidebar_assignees_spec.js +++ b/spec/javascripts/sidebar/sidebar_assignees_spec.js @@ -4,20 +4,29 @@ import SidebarMediator from '~/sidebar/sidebar_mediator'; import SidebarService from '~/sidebar/services/sidebar_service'; import SidebarStore from '~/sidebar/stores/sidebar_store'; import Mock from './mock_data'; +import mountComponent from '../helpers/vue_mount_component_helper'; describe('sidebar assignees', () => { - let component; - let SidebarAssigneeComponent; + let vm; + let mediator; + let sidebarAssigneesEl; preloadFixtures('issues/open-issue.html.raw'); beforeEach(() => { Vue.http.interceptors.push(Mock.sidebarMockInterceptor); - SidebarAssigneeComponent = Vue.extend(SidebarAssignees); - spyOn(SidebarMediator.prototype, 'saveAssignees').and.callThrough(); - spyOn(SidebarMediator.prototype, 'assignYourself').and.callThrough(); - this.mediator = new SidebarMediator(Mock.mediator); + loadFixtures('issues/open-issue.html.raw'); - this.sidebarAssigneesEl = document.querySelector('#js-vue-sidebar-assignees'); + + mediator = new SidebarMediator(Mock.mediator); + spyOn(mediator, 'saveAssignees').and.callThrough(); + spyOn(mediator, 'assignYourself').and.callThrough(); + + const SidebarAssigneeComponent = Vue.extend(SidebarAssignees); + sidebarAssigneesEl = document.querySelector('#js-vue-sidebar-assignees'); + vm = mountComponent(SidebarAssigneeComponent, { + mediator, + field: sidebarAssigneesEl.dataset.field, + }, sidebarAssigneesEl); }); afterEach(() => { @@ -28,30 +37,24 @@ describe('sidebar assignees', () => { }); it('calls the mediator when saves the assignees', () => { - component = new SidebarAssigneeComponent() - .$mount(this.sidebarAssigneesEl); - component.saveAssignees(); - - expect(SidebarMediator.prototype.saveAssignees).toHaveBeenCalled(); + vm.saveAssignees(); + expect(mediator.saveAssignees).toHaveBeenCalled(); }); it('calls the mediator when "assignSelf" method is called', () => { - component = new SidebarAssigneeComponent() - .$mount(this.sidebarAssigneesEl); - component.assignSelf(); + vm.assignSelf(); - expect(SidebarMediator.prototype.assignYourself).toHaveBeenCalled(); - expect(this.mediator.store.assignees.length).toEqual(1); + expect(mediator.assignYourself).toHaveBeenCalled(); + expect(mediator.store.assignees.length).toEqual(1); }); it('hides assignees until fetched', (done) => { - component = new SidebarAssigneeComponent().$mount(this.sidebarAssigneesEl); - const currentAssignee = this.sidebarAssigneesEl.querySelector('.value'); + const currentAssignee = sidebarAssigneesEl.querySelector('.value'); expect(currentAssignee).toBe(null); - component.store.isFetching.assignees = false; + vm.store.isFetching.assignees = false; Vue.nextTick(() => { - expect(component.$el.querySelector('.value')).toBeVisible(); + expect(vm.$el.querySelector('.value')).toBeVisible(); done(); }); }); diff --git a/spec/javascripts/sidebar/sidebar_mediator_spec.js b/spec/javascripts/sidebar/sidebar_mediator_spec.js index 14c34d5a78c..9efd109b996 100644 --- a/spec/javascripts/sidebar/sidebar_mediator_spec.js +++ b/spec/javascripts/sidebar/sidebar_mediator_spec.js @@ -1,4 +1,5 @@ import Vue from 'vue'; +import * as urlUtils from '~/lib/utils/url_utility'; import SidebarMediator from '~/sidebar/sidebar_mediator'; import SidebarStore from '~/sidebar/stores/sidebar_store'; import SidebarService from '~/sidebar/services/sidebar_service'; @@ -85,12 +86,12 @@ describe('Sidebar mediator', () => { const moveToProjectId = 7; this.mediator.store.setMoveToProjectId(moveToProjectId); spyOn(this.mediator.service, 'moveIssue').and.callThrough(); - spyOn(gl.utils, 'visitUrl'); + spyOn(urlUtils, 'visitUrl'); this.mediator.moveIssue() .then(() => { expect(this.mediator.service.moveIssue).toHaveBeenCalledWith(moveToProjectId); - expect(gl.utils.visitUrl).toHaveBeenCalledWith('/root/some-project/issues/5'); + expect(urlUtils.visitUrl).toHaveBeenCalledWith('/root/some-project/issues/5'); }) .then(done) .catch(done.fail); diff --git a/spec/javascripts/sidebar/sidebar_subscriptions_spec.js b/spec/javascripts/sidebar/sidebar_subscriptions_spec.js index 7adf22b0f1f..a6113cb0bae 100644 --- a/spec/javascripts/sidebar/sidebar_subscriptions_spec.js +++ b/spec/javascripts/sidebar/sidebar_subscriptions_spec.js @@ -26,11 +26,14 @@ describe('Sidebar Subscriptions', function () { }); it('calls the mediator toggleSubscription on event', () => { - spyOn(SidebarMediator.prototype, 'toggleSubscription').and.returnValue(Promise.resolve()); - vm = mountComponent(SidebarSubscriptions, {}); + const mediator = new SidebarMediator(); + spyOn(mediator, 'toggleSubscription').and.returnValue(Promise.resolve()); + vm = mountComponent(SidebarSubscriptions, { + mediator, + }); eventHub.$emit('toggleSubscription'); - expect(SidebarMediator.prototype.toggleSubscription).toHaveBeenCalled(); + expect(mediator.toggleSubscription).toHaveBeenCalled(); }); }); diff --git a/spec/javascripts/syntax_highlight_spec.js b/spec/javascripts/syntax_highlight_spec.js index 946f98379ce..763a15e710b 100644 --- a/spec/javascripts/syntax_highlight_spec.js +++ b/spec/javascripts/syntax_highlight_spec.js @@ -1,44 +1,42 @@ /* eslint-disable space-before-function-paren, no-var, no-return-assign, quotes */ -import '~/syntax_highlight'; +import syntaxHighlight from '~/syntax_highlight'; -(function() { - describe('Syntax Highlighter', function() { - var stubUserColorScheme; - stubUserColorScheme = function(value) { - if (window.gon == null) { - window.gon = {}; - } - return window.gon.user_color_scheme = value; - }; - describe('on a js-syntax-highlight element', function() { - beforeEach(function() { - return setFixtures('<div class="js-syntax-highlight"></div>'); - }); - return it('applies syntax highlighting', function() { - stubUserColorScheme('monokai'); - $('.js-syntax-highlight').syntaxHighlight(); - return expect($('.js-syntax-highlight')).toHaveClass('monokai'); - }); +describe('Syntax Highlighter', function() { + var stubUserColorScheme; + stubUserColorScheme = function(value) { + if (window.gon == null) { + window.gon = {}; + } + return window.gon.user_color_scheme = value; + }; + describe('on a js-syntax-highlight element', function() { + beforeEach(function() { + return setFixtures('<div class="js-syntax-highlight"></div>'); }); - return describe('on a parent element', function() { - beforeEach(function() { - return setFixtures("<div class=\"parent\">\n <div class=\"js-syntax-highlight\"></div>\n <div class=\"foo\"></div>\n <div class=\"js-syntax-highlight\"></div>\n</div>"); - }); - it('applies highlighting to all applicable children', function() { - stubUserColorScheme('monokai'); - $('.parent').syntaxHighlight(); - expect($('.parent, .foo')).not.toHaveClass('monokai'); - return expect($('.monokai').length).toBe(2); - }); - return it('prevents an infinite loop when no matches exist', function() { - var highlight; - setFixtures('<div></div>'); - highlight = function() { - return $('div').syntaxHighlight(); - }; - return expect(highlight).not.toThrow(); - }); + return it('applies syntax highlighting', function() { + stubUserColorScheme('monokai'); + syntaxHighlight($('.js-syntax-highlight')); + return expect($('.js-syntax-highlight')).toHaveClass('monokai'); }); }); -}).call(window); + return describe('on a parent element', function() { + beforeEach(function() { + return setFixtures("<div class=\"parent\">\n <div class=\"js-syntax-highlight\"></div>\n <div class=\"foo\"></div>\n <div class=\"js-syntax-highlight\"></div>\n</div>"); + }); + it('applies highlighting to all applicable children', function() { + stubUserColorScheme('monokai'); + syntaxHighlight($('.parent')); + expect($('.parent, .foo')).not.toHaveClass('monokai'); + return expect($('.monokai').length).toBe(2); + }); + return it('prevents an infinite loop when no matches exist', function() { + var highlight; + setFixtures('<div></div>'); + highlight = function() { + return syntaxHighlight($('div')); + }; + return expect(highlight).not.toThrow(); + }); + }); +}); diff --git a/spec/javascripts/test_bundle.js b/spec/javascripts/test_bundle.js index fd7aa332d17..6897c991066 100644 --- a/spec/javascripts/test_bundle.js +++ b/spec/javascripts/test_bundle.js @@ -17,6 +17,12 @@ Vue.config.warnHandler = (msg, vm, trace) => { fail(`${msg}${trace}`); }; +let hasVueErrors = false; +Vue.config.errorHandler = function (err) { + hasVueErrors = true; + fail(err); +}; + Vue.use(VueResource); // enable test fixtures @@ -72,7 +78,7 @@ testsContext.keys().forEach(function (path) { describe('test errors', () => { beforeAll((done) => { - if (hasUnhandledPromiseRejections || hasVueWarnings) { + if (hasUnhandledPromiseRejections || hasVueWarnings || hasVueErrors) { setTimeout(done, 1000); } else { done(); @@ -86,6 +92,10 @@ describe('test errors', () => { it('has no Vue warnings', () => { expect(hasVueWarnings).toBe(false); }); + + it('has no Vue error', () => { + expect(hasVueErrors).toBe(false); + }); }); // if we're generating coverage reports, make sure to include all files so diff --git a/spec/javascripts/todos_spec.js b/spec/javascripts/todos_spec.js index 7d3c9319a11..59e16f0786e 100644 --- a/spec/javascripts/todos_spec.js +++ b/spec/javascripts/todos_spec.js @@ -1,3 +1,4 @@ +import * as urlUtils from '~/lib/utils/url_utility'; import Todos from '~/todos'; import '~/lib/utils/common_utils'; @@ -16,7 +17,7 @@ describe('Todos', () => { it('opens the todo url', (done) => { const todoLink = todoItem.dataset.url; - spyOn(gl.utils, 'visitUrl').and.callFake((url) => { + spyOn(urlUtils, 'visitUrl').and.callFake((url) => { expect(url).toEqual(todoLink); done(); }); @@ -31,7 +32,7 @@ describe('Todos', () => { beforeEach(() => { metakeyEvent = $.Event('click', { keyCode: 91, ctrlKey: true }); - visitUrlSpy = spyOn(gl.utils, 'visitUrl').and.callFake(() => {}); + visitUrlSpy = spyOn(urlUtils, 'visitUrl').and.callFake(() => {}); windowOpenSpy = spyOn(window, 'open').and.callFake(() => {}); }); diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_deployment_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_deployment_spec.js index 7ee998c8fce..db7d083065b 100644 --- a/spec/javascripts/vue_mr_widget/components/mr_widget_deployment_spec.js +++ b/spec/javascripts/vue_mr_widget/components/mr_widget_deployment_spec.js @@ -1,6 +1,8 @@ import Vue from 'vue'; +import * as urlUtils from '~/lib/utils/url_utility'; import deploymentComponent from '~/vue_merge_request_widget/components/mr_widget_deployment'; import MRWidgetService from '~/vue_merge_request_widget/services/mr_widget_service'; +import { getTimeago } from '~/lib/utils/datetime_utility'; const deploymentMockData = [ { @@ -48,7 +50,7 @@ describe('MRWidgetDeployment', () => { describe('formatDate', () => { it('should work', () => { - const readable = gl.utils.getTimeago().format(deployment.deployed_at); + const readable = getTimeago().format(deployment.deployed_at); expect(vm.formatDate(deployment.deployed_at)).toEqual(readable); }); }); @@ -108,13 +110,13 @@ describe('MRWidgetDeployment', () => { it('should show a confirm dialog and call service.stopEnvironment when confirmed', (done) => { spyOn(window, 'confirm').and.returnValue(true); spyOn(MRWidgetService, 'stopEnvironment').and.returnValue(returnPromise(true)); - spyOn(gl.utils, 'visitUrl').and.returnValue(true); + spyOn(urlUtils, 'visitUrl').and.returnValue(true); vm = mockStopEnvironment(); expect(window.confirm).toHaveBeenCalled(); expect(MRWidgetService.stopEnvironment).toHaveBeenCalledWith(deploymentMockData.stop_url); setTimeout(() => { - expect(gl.utils.visitUrl).toHaveBeenCalledWith(url); + expect(urlUtils.visitUrl).toHaveBeenCalledWith(url); done(); }, 333); }); diff --git a/spec/javascripts/vue_shared/components/modal_spec.js b/spec/javascripts/vue_shared/components/modal_spec.js new file mode 100644 index 00000000000..721f4044659 --- /dev/null +++ b/spec/javascripts/vue_shared/components/modal_spec.js @@ -0,0 +1,12 @@ +import Vue from 'vue'; +import modal from '~/vue_shared/components/modal.vue'; +import mountComponent from '../../helpers/vue_mount_component_helper'; + +describe('Modal', () => { + it('does not render a primary button if no primaryButtonLabel', () => { + const modalComponent = Vue.extend(modal); + const vm = mountComponent(modalComponent); + + expect(vm.$el.querySelector('.js-primary-button')).toBeNull(); + }); +}); diff --git a/spec/javascripts/vue_shared/components/time_ago_tooltip_spec.js b/spec/javascripts/vue_shared/components/time_ago_tooltip_spec.js index b4c1f70ed1e..b4fb568f1d4 100644 --- a/spec/javascripts/vue_shared/components/time_ago_tooltip_spec.js +++ b/spec/javascripts/vue_shared/components/time_ago_tooltip_spec.js @@ -1,6 +1,6 @@ import Vue from 'vue'; import timeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; -import '~/lib/utils/datetime_utility'; +import { formatDate, getTimeago } from '~/lib/utils/datetime_utility'; describe('Time ago with tooltip component', () => { let TimeagoTooltip; @@ -24,10 +24,10 @@ describe('Time ago with tooltip component', () => { expect(vm.$el.tagName).toEqual('TIME'); expect( vm.$el.getAttribute('data-original-title'), - ).toEqual(gl.utils.formatDate('2017-05-08T14:57:39.781Z')); + ).toEqual(formatDate('2017-05-08T14:57:39.781Z')); expect(vm.$el.getAttribute('data-placement')).toEqual('top'); - const timeago = gl.utils.getTimeago(); + const timeago = getTimeago(); expect(vm.$el.textContent.trim()).toEqual(timeago.format('2017-05-08T14:57:39.781Z')); }); diff --git a/spec/javascripts/vue_shared/components/toggle_button_spec.js b/spec/javascripts/vue_shared/components/toggle_button_spec.js index 447d74d4e08..859995d33fa 100644 --- a/spec/javascripts/vue_shared/components/toggle_button_spec.js +++ b/spec/javascripts/vue_shared/components/toggle_button_spec.js @@ -30,9 +30,9 @@ describe('Toggle Button', () => { expect(vm.$el.querySelector('input').getAttribute('value')).toEqual('true'); }); - it('renders Enabled and Disabled text data attributes', () => { - expect(vm.$el.querySelector('button').getAttribute('data-enabled-text')).toEqual('Enabled'); - expect(vm.$el.querySelector('button').getAttribute('data-disabled-text')).toEqual('Disabled'); + it('renders input status icon', () => { + expect(vm.$el.querySelectorAll('span.toggle-icon').length).toEqual(1); + expect(vm.$el.querySelectorAll('svg.s16.toggle-icon-svg').length).toEqual(1); }); }); @@ -49,6 +49,14 @@ describe('Toggle Button', () => { expect(vm.$el.querySelector('button').classList.contains('is-checked')).toEqual(true); }); + it('sets aria-label representing toggle state', () => { + vm.value = true; + expect(vm.ariaLabel).toEqual('Toggle Status: ON'); + + vm.value = false; + expect(vm.ariaLabel).toEqual('Toggle Status: OFF'); + }); + it('emits change event when clicked', () => { vm.$el.querySelector('button').click(); diff --git a/spec/lib/banzai/filter/table_of_contents_filter_spec.rb b/spec/lib/banzai/filter/table_of_contents_filter_spec.rb index 85eddde732e..0cfef4ff5bf 100644 --- a/spec/lib/banzai/filter/table_of_contents_filter_spec.rb +++ b/spec/lib/banzai/filter/table_of_contents_filter_spec.rb @@ -65,6 +65,13 @@ describe Banzai::Filter::TableOfContentsFilter do expect(doc.css('h2 a').first.attr('href')).to eq '#one-1' end + it 'prepends a prefix to digits-only ids' do + doc = filter(header(1, "123") + header(2, "1.0")) + + expect(doc.css('h1 a').first.attr('href')).to eq '#anchor-123' + expect(doc.css('h2 a').first.attr('href')).to eq '#anchor-10' + end + it 'supports Unicode' do doc = filter(header(1, '한글')) expect(doc.css('h1 a').first.attr('id')).to eq 'user-content-한글' diff --git a/spec/lib/gitlab/background_migration/create_fork_network_memberships_range_spec.rb b/spec/lib/gitlab/background_migration/create_fork_network_memberships_range_spec.rb index 79d2c071446..e1c4f9cfea7 100644 --- a/spec/lib/gitlab/background_migration/create_fork_network_memberships_range_spec.rb +++ b/spec/lib/gitlab/background_migration/create_fork_network_memberships_range_spec.rb @@ -2,19 +2,20 @@ require 'spec_helper' describe Gitlab::BackgroundMigration::CreateForkNetworkMembershipsRange, :migration, schema: 20170929131201 do let(:migration) { described_class.new } + let(:projects) { table(:projects) } - let(:base1) { create(:project) } - let(:base1_fork1) { create(:project) } - let(:base1_fork2) { create(:project) } + let(:base1) { projects.create } + let(:base1_fork1) { projects.create } + let(:base1_fork2) { projects.create } - let(:base2) { create(:project) } - let(:base2_fork1) { create(:project) } - let(:base2_fork2) { create(:project) } + let(:base2) { projects.create } + let(:base2_fork1) { projects.create } + let(:base2_fork2) { projects.create } - let(:fork_of_fork) { create(:project) } - let(:fork_of_fork2) { create(:project) } - let(:second_level_fork) { create(:project) } - let(:third_level_fork) { create(:project) } + let(:fork_of_fork) { projects.create } + let(:fork_of_fork2) { projects.create } + let(:second_level_fork) { projects.create } + let(:third_level_fork) { projects.create } let(:fork_network1) { fork_networks.find_by(root_project_id: base1.id) } let(:fork_network2) { fork_networks.find_by(root_project_id: base2.id) } @@ -97,7 +98,7 @@ describe Gitlab::BackgroundMigration::CreateForkNetworkMembershipsRange, :migrat end it 'does not miss members for forks of forks for which the root was deleted' do - forked_project_links.create(id: 9, forked_from_project_id: base1_fork1.id, forked_to_project_id: create(:project).id) + forked_project_links.create(id: 9, forked_from_project_id: base1_fork1.id, forked_to_project_id: projects.create.id) base1.destroy expect(migration.missing_members?(7, 10)).to be_falsy @@ -105,8 +106,8 @@ describe Gitlab::BackgroundMigration::CreateForkNetworkMembershipsRange, :migrat context 'with more forks' do before do - forked_project_links.create(id: 9, forked_from_project_id: fork_of_fork.id, forked_to_project_id: create(:project).id) - forked_project_links.create(id: 10, forked_from_project_id: fork_of_fork.id, forked_to_project_id: create(:project).id) + forked_project_links.create(id: 9, forked_from_project_id: fork_of_fork.id, forked_to_project_id: projects.create.id) + forked_project_links.create(id: 10, forked_from_project_id: fork_of_fork.id, forked_to_project_id: projects.create.id) end it 'only processes a single batch of links at a time' do diff --git a/spec/lib/gitlab/background_migration/migrate_events_to_push_event_payloads_spec.rb b/spec/lib/gitlab/background_migration/migrate_events_to_push_event_payloads_spec.rb index cb52d971047..7351d45336a 100644 --- a/spec/lib/gitlab/background_migration/migrate_events_to_push_event_payloads_spec.rb +++ b/spec/lib/gitlab/background_migration/migrate_events_to_push_event_payloads_spec.rb @@ -225,9 +225,10 @@ describe Gitlab::BackgroundMigration::MigrateEventsToPushEventPayloads, :migrati let(:user_class) { table(:users) } let(:author) { build(:user).becomes(user_class).tap(&:save!).becomes(User) } let(:namespace) { create(:namespace, owner: author) } - let(:project) { create(:project_empty_repo, namespace: namespace, creator: author) } + let(:projects) { table(:projects) } + let(:project) { projects.create(namespace_id: namespace.id, creator_id: author.id) } - # We can not rely on FactoryGirl as the state of Event may change in ways that + # We can not rely on FactoryBot as the state of Event may change in ways that # the background migration does not expect, hence we use the Event class of # the migration itself. def create_push_event(project, author, data = nil) diff --git a/spec/lib/gitlab/background_migration/populate_fork_networks_range_spec.rb b/spec/lib/gitlab/background_migration/populate_fork_networks_range_spec.rb index e52baf8dde7..8582af96199 100644 --- a/spec/lib/gitlab/background_migration/populate_fork_networks_range_spec.rb +++ b/spec/lib/gitlab/background_migration/populate_fork_networks_range_spec.rb @@ -2,10 +2,11 @@ require 'spec_helper' describe Gitlab::BackgroundMigration::PopulateForkNetworksRange, :migration, schema: 20170929131201 do let(:migration) { described_class.new } - let(:base1) { create(:project) } + let(:projects) { table(:projects) } + let(:base1) { projects.create } - let(:base2) { create(:project) } - let(:base2_fork1) { create(:project) } + let(:base2) { projects.create } + let(:base2_fork1) { projects.create } let!(:forked_project_links) { table(:forked_project_links) } let!(:fork_networks) { table(:fork_networks) } @@ -18,10 +19,10 @@ describe Gitlab::BackgroundMigration::PopulateForkNetworksRange, :migration, sch # A normal fork link forked_project_links.create(id: 1, forked_from_project_id: base1.id, - forked_to_project_id: create(:project).id) + forked_to_project_id: projects.create.id) forked_project_links.create(id: 2, forked_from_project_id: base1.id, - forked_to_project_id: create(:project).id) + forked_to_project_id: projects.create.id) forked_project_links.create(id: 3, forked_from_project_id: base2.id, forked_to_project_id: base2_fork1.id) @@ -29,10 +30,10 @@ describe Gitlab::BackgroundMigration::PopulateForkNetworksRange, :migration, sch # create a fork of a fork forked_project_links.create(id: 4, forked_from_project_id: base2_fork1.id, - forked_to_project_id: create(:project).id) + forked_to_project_id: projects.create.id) forked_project_links.create(id: 5, - forked_from_project_id: create(:project).id, - forked_to_project_id: create(:project).id) + forked_from_project_id: projects.create.id, + forked_to_project_id: projects.create.id) # Stub out the calls to the other migrations allow(BackgroundMigrationWorker).to receive(:perform_in) @@ -63,7 +64,7 @@ describe Gitlab::BackgroundMigration::PopulateForkNetworksRange, :migration, sch end it 'creates a fork network for the fork of which the source was deleted' do - fork = create(:project) + fork = projects.create forked_project_links.create(id: 6, forked_from_project_id: 99999, forked_to_project_id: fork.id) migration.perform(5, 8) diff --git a/spec/lib/gitlab/background_migration/populate_untracked_uploads_spec.rb b/spec/lib/gitlab/background_migration/populate_untracked_uploads_spec.rb index b80df6956b0..be45c00dfe6 100644 --- a/spec/lib/gitlab/background_migration/populate_untracked_uploads_spec.rb +++ b/spec/lib/gitlab/background_migration/populate_untracked_uploads_spec.rb @@ -182,13 +182,22 @@ describe Gitlab::BackgroundMigration::PopulateUntrackedUploads, :sidekiq do end context 'for a pre-Markdown Note attachment file path' do - class Note < ActiveRecord::Base - has_many :uploads, as: :model, dependent: :destroy + let(:model) { create(:note, :with_attachment) } + let!(:expected_upload_attrs) { Upload.where(model_type: 'Note', model_id: model.id).first.attributes.slice('path', 'uploader', 'size', 'checksum') } + let!(:untracked_file) { untracked_files_for_uploads.create!(path: expected_upload_attrs['path']) } + + before do + Upload.where(model_type: 'Note', model_id: model.id).delete_all end - let(:model) { create(:note, :with_attachment) } + # Can't use the shared example because Note doesn't have an `uploads` association + it 'creates an Upload record' do + expect do + subject.perform(1, untracked_files_for_uploads.last.id) + end.to change { Upload.where(model_type: 'Note', model_id: model.id).count }.from(0).to(1) - it_behaves_like 'non_markdown_file' + expect(Upload.where(model_type: 'Note', model_id: model.id).first.attributes).to include(expected_upload_attrs) + end end context 'for a user avatar file path' do diff --git a/spec/lib/gitlab/bare_repository_import/importer_spec.rb b/spec/lib/gitlab/bare_repository_import/importer_spec.rb index 7f3bf5fc41c..8a83e446935 100644 --- a/spec/lib/gitlab/bare_repository_import/importer_spec.rb +++ b/spec/lib/gitlab/bare_repository_import/importer_spec.rb @@ -132,6 +132,23 @@ describe Gitlab::BareRepositoryImport::Importer, repository: true do expect(File).to exist(File.join(project.repository_storage_path, project.disk_path + '.git')) end + + it 'moves an existing project to the correct path' do + # This is a quick way to get a valid repository instead of copying an + # existing one. Since it's not persisted, the importer will try to + # create the project. + project = build(:project, :repository) + original_commit_count = project.repository.commit_count + + bare_repo = Gitlab::BareRepositoryImport::Repository.new(project.repository_storage_path, project.repository.path) + gitlab_importer = described_class.new(admin, bare_repo) + + expect(gitlab_importer).to receive(:create_project).and_call_original + + new_project = gitlab_importer.create_project_if_needed + + expect(new_project.repository.commit_count).to eq(original_commit_count) + end end context 'with Wiki' do diff --git a/spec/lib/gitlab/bare_repository_import/repository_spec.rb b/spec/lib/gitlab/bare_repository_import/repository_spec.rb index 2db737f5fb6..61b73abcba4 100644 --- a/spec/lib/gitlab/bare_repository_import/repository_spec.rb +++ b/spec/lib/gitlab/bare_repository_import/repository_spec.rb @@ -46,6 +46,13 @@ describe ::Gitlab::BareRepositoryImport::Repository do describe '#project_full_path' do it 'returns the project full path' do expect(project_repo_path.repo_path).to eq('/full/path/to/repo.git') + expect(project_repo_path.project_full_path).to eq('to/repo') + end + + it 'with no trailing slash in the root path' do + repo_path = described_class.new('/full/path', '/full/path/to/repo.git') + + expect(repo_path.project_full_path).to eq('to/repo') end end end diff --git a/spec/lib/gitlab/checks/project_moved_spec.rb b/spec/lib/gitlab/checks/project_moved_spec.rb new file mode 100644 index 00000000000..fa1575e2177 --- /dev/null +++ b/spec/lib/gitlab/checks/project_moved_spec.rb @@ -0,0 +1,81 @@ +require 'rails_helper' + +describe Gitlab::Checks::ProjectMoved, :clean_gitlab_redis_shared_state do + let(:user) { create(:user) } + let(:project) { create(:project) } + + describe '.fetch_redirct_message' do + context 'with a redirect message queue' do + it 'should return the redirect message' do + project_moved = described_class.new(project, user, 'foo/bar', 'http') + project_moved.add_redirect_message + + expect(described_class.fetch_redirect_message(user.id, project.id)).to eq(project_moved.redirect_message) + end + + it 'should delete the redirect message from redis' do + project_moved = described_class.new(project, user, 'foo/bar', 'http') + project_moved.add_redirect_message + + expect(Gitlab::Redis::SharedState.with { |redis| redis.get("redirect_namespace:#{user.id}:#{project.id}") }).not_to be_nil + described_class.fetch_redirect_message(user.id, project.id) + expect(Gitlab::Redis::SharedState.with { |redis| redis.get("redirect_namespace:#{user.id}:#{project.id}") }).to be_nil + end + end + + context 'with no redirect message queue' do + it 'should return nil' do + expect(described_class.fetch_redirect_message(1, 2)).to be_nil + end + end + end + + describe '#add_redirect_message' do + it 'should queue a redirect message' do + project_moved = described_class.new(project, user, 'foo/bar', 'http') + expect(project_moved.add_redirect_message).to eq("OK") + end + end + + describe '#redirect_message' do + context 'when the push is rejected' do + it 'should return a redirect message telling the user to try again' do + project_moved = described_class.new(project, user, 'foo/bar', 'http') + message = "Project 'foo/bar' was moved to '#{project.full_path}'." + + "\n\nPlease update your Git remote:" + + "\n\n git remote set-url origin #{project.http_url_to_repo} and try again.\n" + + expect(project_moved.redirect_message(rejected: true)).to eq(message) + end + end + + context 'when the push is not rejected' do + it 'should return a redirect message' do + project_moved = described_class.new(project, user, 'foo/bar', 'http') + message = "Project 'foo/bar' was moved to '#{project.full_path}'." + + "\n\nPlease update your Git remote:" + + "\n\n git remote set-url origin #{project.http_url_to_repo}\n" + + expect(project_moved.redirect_message).to eq(message) + end + end + end + + describe '#permanent_redirect?' do + context 'with a permanent RedirectRoute' do + it 'should return true' do + project.route.create_redirect('foo/bar', permanent: true) + project_moved = described_class.new(project, user, 'foo/bar', 'http') + expect(project_moved.permanent_redirect?).to be_truthy + end + end + + context 'without a permanent RedirectRoute' do + it 'should return false' do + project.route.create_redirect('foo/bar') + project_moved = described_class.new(project, user, 'foo/bar', 'http') + expect(project_moved.permanent_redirect?).to be_falsy + end + end + end +end diff --git a/spec/lib/gitlab/ci/pipeline/chain/build_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/build_spec.rb index 0f1d72080c5..3ae7053a995 100644 --- a/spec/lib/gitlab/ci/pipeline/chain/build_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/chain/build_spec.rb @@ -6,46 +6,81 @@ describe Gitlab::Ci::Pipeline::Chain::Build do let(:pipeline) { Ci::Pipeline.new } let(:command) do - double('command', source: :push, - origin_ref: 'master', - checkout_sha: project.commit.id, - after_sha: nil, - before_sha: nil, - trigger_request: nil, - schedule: nil, - project: project, - current_user: user) + Gitlab::Ci::Pipeline::Chain::Command.new( + source: :push, + origin_ref: 'master', + checkout_sha: project.commit.id, + after_sha: nil, + before_sha: nil, + trigger_request: nil, + schedule: nil, + project: project, + current_user: user) end let(:step) { described_class.new(pipeline, command) } before do stub_repository_ci_yaml_file(sha: anything) - - step.perform! end it 'never breaks the chain' do + step.perform! + expect(step.break?).to be false end it 'fills pipeline object with data' do + step.perform! + expect(pipeline.sha).not_to be_empty expect(pipeline.sha).to eq project.commit.id expect(pipeline.ref).to eq 'master' + expect(pipeline.tag).to be false expect(pipeline.user).to eq user expect(pipeline.project).to eq project end it 'sets a valid config source' do + step.perform! + expect(pipeline.repository_source?).to be true end it 'returns a valid pipeline' do + step.perform! + expect(pipeline).to be_valid end it 'does not persist a pipeline' do + step.perform! + expect(pipeline).not_to be_persisted end + + context 'when pipeline is running for a tag' do + let(:command) do + Gitlab::Ci::Pipeline::Chain::Command.new( + source: :push, + origin_ref: 'mytag', + checkout_sha: project.commit.id, + after_sha: nil, + before_sha: nil, + trigger_request: nil, + schedule: nil, + project: project, + current_user: user) + end + + before do + allow_any_instance_of(Repository).to receive(:tag_exists?).with('mytag').and_return(true) + + step.perform! + end + + it 'correctly indicated that this is a tagged pipeline' do + expect(pipeline).to be_tag + end + end end diff --git a/spec/lib/gitlab/ci/pipeline/chain/command_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/command_spec.rb new file mode 100644 index 00000000000..75a177d2d1f --- /dev/null +++ b/spec/lib/gitlab/ci/pipeline/chain/command_spec.rb @@ -0,0 +1,185 @@ +require 'spec_helper' + +describe Gitlab::Ci::Pipeline::Chain::Command do + set(:project) { create(:project, :repository) } + + describe '#initialize' do + subject do + described_class.new(origin_ref: 'master') + end + + it 'properly initialises object from hash' do + expect(subject.origin_ref).to eq('master') + end + end + + context 'handling of origin_ref' do + let(:command) { described_class.new(project: project, origin_ref: origin_ref) } + + describe '#branch_exists?' do + subject { command.branch_exists? } + + context 'for existing branch' do + let(:origin_ref) { 'master' } + + it { is_expected.to eq(true) } + end + + context 'for invalid branch' do + let(:origin_ref) { 'something' } + + it { is_expected.to eq(false) } + end + end + + describe '#tag_exists?' do + subject { command.tag_exists? } + + context 'for existing ref' do + let(:origin_ref) { 'v1.0.0' } + + it { is_expected.to eq(true) } + end + + context 'for invalid ref' do + let(:origin_ref) { 'something' } + + it { is_expected.to eq(false) } + end + end + + describe '#ref' do + subject { command.ref } + + context 'for regular ref' do + let(:origin_ref) { 'master' } + + it { is_expected.to eq('master') } + end + + context 'for branch ref' do + let(:origin_ref) { 'refs/heads/master' } + + it { is_expected.to eq('master') } + end + + context 'for tag ref' do + let(:origin_ref) { 'refs/tags/1.0.0' } + + it { is_expected.to eq('1.0.0') } + end + + context 'for other refs' do + let(:origin_ref) { 'refs/merge-requests/11/head' } + + it { is_expected.to eq('refs/merge-requests/11/head') } + end + end + end + + describe '#sha' do + subject { command.sha } + + context 'when invalid checkout_sha is specified' do + let(:command) { described_class.new(project: project, checkout_sha: 'aaa') } + + it 'returns empty value' do + is_expected.to be_nil + end + end + + context 'when a valid checkout_sha is specified' do + let(:command) { described_class.new(project: project, checkout_sha: project.commit.id) } + + it 'returns checkout_sha' do + is_expected.to eq(project.commit.id) + end + end + + context 'when a valid after_sha is specified' do + let(:command) { described_class.new(project: project, after_sha: project.commit.id) } + + it 'returns after_sha' do + is_expected.to eq(project.commit.id) + end + end + + context 'when a valid origin_ref is specified' do + let(:command) { described_class.new(project: project, origin_ref: 'HEAD') } + + it 'returns SHA for given ref' do + is_expected.to eq(project.commit.id) + end + end + end + + describe '#origin_sha' do + subject { command.origin_sha } + + context 'when using checkout_sha and after_sha' do + let(:command) { described_class.new(project: project, checkout_sha: 'aaa', after_sha: 'bbb') } + + it 'uses checkout_sha' do + is_expected.to eq('aaa') + end + end + + context 'when using after_sha only' do + let(:command) { described_class.new(project: project, after_sha: 'bbb') } + + it 'uses after_sha' do + is_expected.to eq('bbb') + end + end + end + + describe '#before_sha' do + subject { command.before_sha } + + context 'when using checkout_sha and before_sha' do + let(:command) { described_class.new(project: project, checkout_sha: 'aaa', before_sha: 'bbb') } + + it 'uses before_sha' do + is_expected.to eq('bbb') + end + end + + context 'when using checkout_sha only' do + let(:command) { described_class.new(project: project, checkout_sha: 'aaa') } + + it 'uses checkout_sha' do + is_expected.to eq('aaa') + end + end + + context 'when checkout_sha and before_sha are empty' do + let(:command) { described_class.new(project: project) } + + it 'uses BLANK_SHA' do + is_expected.to eq(Gitlab::Git::BLANK_SHA) + end + end + end + + describe '#protected_ref?' do + let(:command) { described_class.new(project: project, origin_ref: 'my-branch') } + + subject { command.protected_ref? } + + context 'when a ref is protected' do + before do + expect_any_instance_of(Project).to receive(:protected_for?).with('my-branch').and_return(true) + end + + it { is_expected.to eq(true) } + end + + context 'when a ref is unprotected' do + before do + expect_any_instance_of(Project).to receive(:protected_for?).with('my-branch').and_return(false) + end + + it { is_expected.to eq(false) } + end + end +end diff --git a/spec/lib/gitlab/ci/pipeline/chain/create_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/create_spec.rb index f54e2326b06..1b03227d67b 100644 --- a/spec/lib/gitlab/ci/pipeline/chain/create_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/chain/create_spec.rb @@ -10,9 +10,9 @@ describe Gitlab::Ci::Pipeline::Chain::Create do end let(:command) do - double('command', project: project, - current_user: user, - seeds_block: nil) + Gitlab::Ci::Pipeline::Chain::Command.new( + project: project, + current_user: user, seeds_block: nil) end let(:step) { described_class.new(pipeline, command) } diff --git a/spec/lib/gitlab/ci/pipeline/chain/sequence_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/sequence_spec.rb index e165e0fac2a..eca23694a2b 100644 --- a/spec/lib/gitlab/ci/pipeline/chain/sequence_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/chain/sequence_spec.rb @@ -5,7 +5,7 @@ describe Gitlab::Ci::Pipeline::Chain::Sequence do set(:user) { create(:user) } let(:pipeline) { build_stubbed(:ci_pipeline) } - let(:command) { double('command' ) } + let(:command) { Gitlab::Ci::Pipeline::Chain::Command.new } let(:first_step) { spy('first step') } let(:second_step) { spy('second step') } let(:sequence) { [first_step, second_step] } diff --git a/spec/lib/gitlab/ci/pipeline/chain/skip_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/skip_spec.rb index 32bd5de829b..dc13cae961c 100644 --- a/spec/lib/gitlab/ci/pipeline/chain/skip_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/chain/skip_spec.rb @@ -6,10 +6,11 @@ describe Gitlab::Ci::Pipeline::Chain::Skip do set(:pipeline) { create(:ci_pipeline, project: project) } let(:command) do - double('command', project: project, - current_user: user, - ignore_skip_ci: false, - save_incompleted: true) + Gitlab::Ci::Pipeline::Chain::Command.new( + project: project, + current_user: user, + ignore_skip_ci: false, + save_incompleted: true) end let(:step) { described_class.new(pipeline, command) } diff --git a/spec/lib/gitlab/ci/pipeline/chain/validate/abilities_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/validate/abilities_spec.rb index 0bbdd23f4d6..a973ccda8de 100644 --- a/spec/lib/gitlab/ci/pipeline/chain/validate/abilities_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/chain/validate/abilities_spec.rb @@ -5,11 +5,12 @@ describe Gitlab::Ci::Pipeline::Chain::Validate::Abilities do set(:user) { create(:user) } let(:pipeline) do - build_stubbed(:ci_pipeline, ref: ref, project: project) + build_stubbed(:ci_pipeline, project: project) end let(:command) do - double('command', project: project, current_user: user) + Gitlab::Ci::Pipeline::Chain::Command.new( + project: project, current_user: user, origin_ref: ref) end let(:step) { described_class.new(pipeline, command) } diff --git a/spec/lib/gitlab/ci/pipeline/chain/validate/config_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/validate/config_spec.rb index 8357af38f92..5c12c6e6392 100644 --- a/spec/lib/gitlab/ci/pipeline/chain/validate/config_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/chain/validate/config_spec.rb @@ -5,9 +5,10 @@ describe Gitlab::Ci::Pipeline::Chain::Validate::Config do set(:user) { create(:user) } let(:command) do - double('command', project: project, - current_user: user, - save_incompleted: true) + Gitlab::Ci::Pipeline::Chain::Command.new( + project: project, + current_user: user, + save_incompleted: true) end let!(:step) { described_class.new(pipeline, command) } diff --git a/spec/lib/gitlab/ci/pipeline/chain/validate/repository_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/validate/repository_spec.rb index bb356efe9ad..fb1b53fc55c 100644 --- a/spec/lib/gitlab/ci/pipeline/chain/validate/repository_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/chain/validate/repository_spec.rb @@ -3,10 +3,7 @@ require 'spec_helper' describe Gitlab::Ci::Pipeline::Chain::Validate::Repository do set(:project) { create(:project, :repository) } set(:user) { create(:user) } - - let(:command) do - double('command', project: project, current_user: user) - end + let(:pipeline) { build_stubbed(:ci_pipeline) } let!(:step) { described_class.new(pipeline, command) } @@ -14,9 +11,10 @@ describe Gitlab::Ci::Pipeline::Chain::Validate::Repository do step.perform! end - context 'when pipeline ref and sha exists' do - let(:pipeline) do - build_stubbed(:ci_pipeline, ref: 'master', sha: '123', project: project) + context 'when ref and sha exists' do + let(:command) do + Gitlab::Ci::Pipeline::Chain::Command.new( + project: project, current_user: user, origin_ref: 'master', checkout_sha: project.commit.id) end it 'does not break the chain' do @@ -28,9 +26,10 @@ describe Gitlab::Ci::Pipeline::Chain::Validate::Repository do end end - context 'when pipeline ref does not exist' do - let(:pipeline) do - build_stubbed(:ci_pipeline, ref: 'something', project: project) + context 'when ref does not exist' do + let(:command) do + Gitlab::Ci::Pipeline::Chain::Command.new( + project: project, current_user: user, origin_ref: 'something') end it 'breaks the chain' do @@ -43,9 +42,10 @@ describe Gitlab::Ci::Pipeline::Chain::Validate::Repository do end end - context 'when pipeline does not have SHA set' do - let(:pipeline) do - build_stubbed(:ci_pipeline, ref: 'master', sha: nil, project: project) + context 'when does not have existing SHA set' do + let(:command) do + Gitlab::Ci::Pipeline::Chain::Command.new( + project: project, current_user: user, origin_ref: 'master', checkout_sha: 'something') end it 'breaks the chain' do diff --git a/spec/lib/gitlab/cycle_analytics/base_event_fetcher_spec.rb b/spec/lib/gitlab/cycle_analytics/base_event_fetcher_spec.rb index 0560c47f03f..3fe0493ed9b 100644 --- a/spec/lib/gitlab/cycle_analytics/base_event_fetcher_spec.rb +++ b/spec/lib/gitlab/cycle_analytics/base_event_fetcher_spec.rb @@ -23,6 +23,8 @@ describe Gitlab::CycleAnalytics::BaseEventFetcher do allow_any_instance_of(described_class).to receive(:serialize) do |event| event end + allow_any_instance_of(described_class) + .to receive(:allowed_ids).and_return(nil) stub_const('Gitlab::CycleAnalytics::BaseEventFetcher::MAX_EVENTS', max_events) diff --git a/spec/lib/gitlab/email/handler/create_merge_request_handler_spec.rb b/spec/lib/gitlab/email/handler/create_merge_request_handler_spec.rb index e361d1a7393..dc1a93367a4 100644 --- a/spec/lib/gitlab/email/handler/create_merge_request_handler_spec.rb +++ b/spec/lib/gitlab/email/handler/create_merge_request_handler_spec.rb @@ -10,14 +10,14 @@ describe Gitlab::Email::Handler::CreateMergeRequestHandler do stub_config_setting(host: 'localhost') end + after do + TestEnv.clean_test_path + end + let(:email_raw) { fixture_file('emails/valid_new_merge_request.eml') } let(:namespace) { create(:namespace, path: 'gitlabhq') } - # project's git repository is not deleted when project is deleted - # between tests. Then tests fail because re-creation of the project with - # the same name fails on existing git repository -> skip_disk_validation - # ignores repository existence on disk - let!(:project) { create(:project, :public, :repository, skip_disk_validation: true, namespace: namespace, path: 'gitlabhq') } + let!(:project) { create(:project, :public, :repository, namespace: namespace, path: 'gitlabhq') } let!(:user) do create( :user, @@ -49,6 +49,7 @@ describe Gitlab::Email::Handler::CreateMergeRequestHandler do expect(merge_request.author).to eq(user) expect(merge_request.source_branch).to eq('feature') expect(merge_request.title).to eq('Feature added') + expect(merge_request.description).to eq('Merge request description') expect(merge_request.target_branch).to eq(project.default_branch) end end @@ -79,6 +80,17 @@ describe Gitlab::Email::Handler::CreateMergeRequestHandler do expect { receiver.execute }.to raise_error(Gitlab::Email::InvalidMergeRequestError) end end + + context "when the message body is blank" do + let(:email_raw) { fixture_file("emails/valid_new_merge_request_no_description.eml") } + + it "creates a new merge request with description set from the last commit" do + expect { receiver.execute }.to change { project.merge_requests.count }.by(1) + merge_request = project.merge_requests.last + + expect(merge_request.description).to eq('Signed-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>') + end + end end end end diff --git a/spec/lib/gitlab/git/gitlab_projects_spec.rb b/spec/lib/gitlab/git/gitlab_projects_spec.rb new file mode 100644 index 00000000000..18906955df6 --- /dev/null +++ b/spec/lib/gitlab/git/gitlab_projects_spec.rb @@ -0,0 +1,309 @@ +require 'spec_helper' + +describe Gitlab::Git::GitlabProjects do + after do + TestEnv.clean_test_path + end + + let(:project) { create(:project, :repository) } + + if $VERBOSE + let(:logger) { Logger.new(STDOUT) } + else + let(:logger) { double('logger').as_null_object } + end + + let(:tmp_repos_path) { TestEnv.repos_path } + let(:repo_name) { project.disk_path + '.git' } + let(:tmp_repo_path) { File.join(tmp_repos_path, repo_name) } + let(:gl_projects) { build_gitlab_projects(tmp_repos_path, repo_name) } + + describe '#initialize' do + it { expect(gl_projects.shard_path).to eq(tmp_repos_path) } + it { expect(gl_projects.repository_relative_path).to eq(repo_name) } + it { expect(gl_projects.repository_absolute_path).to eq(File.join(tmp_repos_path, repo_name)) } + it { expect(gl_projects.logger).to eq(logger) } + end + + describe '#mv_project' do + let(:new_repo_path) { File.join(tmp_repos_path, 'repo.git') } + + it 'moves a repo directory' do + expect(File.exist?(tmp_repo_path)).to be_truthy + + message = "Moving repository from <#{tmp_repo_path}> to <#{new_repo_path}>." + expect(logger).to receive(:info).with(message) + + expect(gl_projects.mv_project('repo.git')).to be_truthy + + expect(File.exist?(tmp_repo_path)).to be_falsy + expect(File.exist?(new_repo_path)).to be_truthy + end + + it "fails if the source path doesn't exist" do + expect(logger).to receive(:error).with("mv-project failed: source path <#{tmp_repos_path}/bad-src.git> does not exist.") + + result = build_gitlab_projects(tmp_repos_path, 'bad-src.git').mv_project('repo.git') + expect(result).to be_falsy + end + + it 'fails if the destination path already exists' do + FileUtils.mkdir_p(File.join(tmp_repos_path, 'already-exists.git')) + + message = "mv-project failed: destination path <#{tmp_repos_path}/already-exists.git> already exists." + expect(logger).to receive(:error).with(message) + + expect(gl_projects.mv_project('already-exists.git')).to be_falsy + end + end + + describe '#rm_project' do + it 'removes a repo directory' do + expect(File.exist?(tmp_repo_path)).to be_truthy + expect(logger).to receive(:info).with("Removing repository <#{tmp_repo_path}>.") + + expect(gl_projects.rm_project).to be_truthy + + expect(File.exist?(tmp_repo_path)).to be_falsy + end + end + + describe '#push_branches' do + let(:remote_name) { 'remote-name' } + let(:branch_name) { 'master' } + let(:cmd) { %W(git push -- #{remote_name} #{branch_name}) } + let(:force) { false } + + subject { gl_projects.push_branches(remote_name, 600, force, [branch_name]) } + + it 'executes the command' do + stub_spawn(cmd, 600, tmp_repo_path, success: true) + + is_expected.to be_truthy + end + + it 'fails' do + stub_spawn(cmd, 600, tmp_repo_path, success: false) + + is_expected.to be_falsy + end + + context 'with --force' do + let(:cmd) { %W(git push --force -- #{remote_name} #{branch_name}) } + let(:force) { true } + + it 'executes the command' do + stub_spawn(cmd, 600, tmp_repo_path, success: true) + + is_expected.to be_truthy + end + end + end + + describe '#fetch_remote' do + let(:remote_name) { 'remote-name' } + let(:branch_name) { 'master' } + let(:force) { false } + let(:tags) { true } + let(:args) { { force: force, tags: tags }.merge(extra_args) } + let(:extra_args) { {} } + let(:cmd) { %W(git fetch #{remote_name} --prune --quiet --tags) } + + subject { gl_projects.fetch_remote(remote_name, 600, args) } + + def stub_tempfile(name, filename, opts = {}) + chmod = opts.delete(:chmod) + file = StringIO.new + + allow(file).to receive(:close!) + allow(file).to receive(:path).and_return(name) + + expect(Tempfile).to receive(:new).with(filename).and_return(file) + expect(file).to receive(:chmod).with(chmod) if chmod + + file + end + + context 'with default args' do + it 'executes the command' do + stub_spawn(cmd, 600, tmp_repo_path, {}, success: true) + + is_expected.to be_truthy + end + + it 'fails' do + stub_spawn(cmd, 600, tmp_repo_path, {}, success: false) + + is_expected.to be_falsy + end + end + + context 'with --force' do + let(:force) { true } + let(:cmd) { %W(git fetch #{remote_name} --prune --quiet --force --tags) } + + it 'executes the command with forced option' do + stub_spawn(cmd, 600, tmp_repo_path, {}, success: true) + + is_expected.to be_truthy + end + end + + context 'with --no-tags' do + let(:tags) { false } + let(:cmd) { %W(git fetch #{remote_name} --prune --quiet --no-tags) } + + it 'executes the command' do + stub_spawn(cmd, 600, tmp_repo_path, {}, success: true) + + is_expected.to be_truthy + end + end + + describe 'with an SSH key' do + let(:extra_args) { { ssh_key: 'SSH KEY' } } + + it 'sets GIT_SSH to a custom script' do + script = stub_tempfile('scriptFile', 'gitlab-shell-ssh-wrapper', chmod: 0o755) + key = stub_tempfile('/tmp files/keyFile', 'gitlab-shell-key-file', chmod: 0o400) + + stub_spawn(cmd, 600, tmp_repo_path, { 'GIT_SSH' => 'scriptFile' }, success: true) + + is_expected.to be_truthy + + expect(script.string).to eq("#!/bin/sh\nexec ssh '-oIdentityFile=\"/tmp files/keyFile\"' '-oIdentitiesOnly=\"yes\"' \"$@\"") + expect(key.string).to eq('SSH KEY') + end + end + + describe 'with known_hosts data' do + let(:extra_args) { { known_hosts: 'KNOWN HOSTS' } } + + it 'sets GIT_SSH to a custom script' do + script = stub_tempfile('scriptFile', 'gitlab-shell-ssh-wrapper', chmod: 0o755) + key = stub_tempfile('/tmp files/knownHosts', 'gitlab-shell-known-hosts', chmod: 0o400) + + stub_spawn(cmd, 600, tmp_repo_path, { 'GIT_SSH' => 'scriptFile' }, success: true) + + is_expected.to be_truthy + + expect(script.string).to eq("#!/bin/sh\nexec ssh '-oStrictHostKeyChecking=\"yes\"' '-oUserKnownHostsFile=\"/tmp files/knownHosts\"' \"$@\"") + expect(key.string).to eq('KNOWN HOSTS') + end + end + end + + describe '#import_project' do + let(:project) { create(:project) } + let(:import_url) { TestEnv.factory_repo_path_bare } + let(:cmd) { %W(git clone --bare -- #{import_url} #{tmp_repo_path}) } + let(:timeout) { 600 } + + subject { gl_projects.import_project(import_url, timeout) } + + context 'success import' do + it 'imports a repo' do + expect(File.exist?(File.join(tmp_repo_path, 'HEAD'))).to be_falsy + + message = "Importing project from <#{import_url}> to <#{tmp_repo_path}>." + expect(logger).to receive(:info).with(message) + + is_expected.to be_truthy + + expect(File.exist?(File.join(tmp_repo_path, 'HEAD'))).to be_truthy + end + end + + context 'already exists' do + it "doesn't import" do + FileUtils.mkdir_p(tmp_repo_path) + + is_expected.to be_falsy + end + end + + context 'timeout' do + it 'does not import a repo' do + stub_spawn_timeout(cmd, timeout, nil) + + message = "Importing project from <#{import_url}> to <#{tmp_repo_path}> failed." + expect(logger).to receive(:error).with(message) + + is_expected.to be_falsy + + expect(gl_projects.output).to eq("Timed out\n") + expect(File.exist?(File.join(tmp_repo_path, 'HEAD'))).to be_falsy + end + end + end + + describe '#fork_repository' do + let(:dest_repos_path) { tmp_repos_path } + let(:dest_repo_name) { File.join('@hashed', 'aa', 'bb', 'xyz.git') } + let(:dest_repo) { File.join(dest_repos_path, dest_repo_name) } + let(:dest_namespace) { File.dirname(dest_repo) } + + subject { gl_projects.fork_repository(dest_repos_path, dest_repo_name) } + + before do + FileUtils.mkdir_p(dest_repos_path) + end + + after do + FileUtils.rm_rf(dest_repos_path) + end + + it 'forks the repository' do + message = "Forking repository from <#{tmp_repo_path}> to <#{dest_repo}>." + expect(logger).to receive(:info).with(message) + + is_expected.to be_truthy + + expect(File.exist?(dest_repo)).to be_truthy + expect(File.exist?(File.join(dest_repo, 'hooks', 'pre-receive'))).to be_truthy + expect(File.exist?(File.join(dest_repo, 'hooks', 'post-receive'))).to be_truthy + end + + it 'does not fork if a project of the same name already exists' do + # create a fake project at the intended destination + FileUtils.mkdir_p(dest_repo) + + # trying to fork again should fail as the repo already exists + message = "fork-repository failed: destination repository <#{dest_repo}> already exists." + expect(logger).to receive(:error).with(message) + + is_expected.to be_falsy + end + + context 'different storages' do + let(:dest_repos_path) { File.join(File.dirname(tmp_repos_path), 'alternative') } + + it 'forks the repo' do + is_expected.to be_truthy + + expect(File.exist?(dest_repo)).to be_truthy + expect(File.exist?(File.join(dest_repo, 'hooks', 'pre-receive'))).to be_truthy + expect(File.exist?(File.join(dest_repo, 'hooks', 'post-receive'))).to be_truthy + end + end + end + + def build_gitlab_projects(*args) + described_class.new( + *args, + global_hooks_path: Gitlab.config.gitlab_shell.hooks_path, + logger: logger + ) + end + + def stub_spawn(*args, success: true) + exitstatus = success ? 0 : nil + expect(gl_projects).to receive(:popen_with_timeout).with(*args) + .and_return(["output", exitstatus]) + end + + def stub_spawn_timeout(*args) + expect(gl_projects).to receive(:popen_with_timeout).with(*args) + .and_raise(Timeout::Error) + end +end diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index f19b65a5f71..03a9cc488ca 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -19,6 +19,51 @@ describe Gitlab::Git::Repository, seed_helper: true do let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '') } + describe '.create_hooks' do + let(:repo_path) { File.join(TestEnv.repos_path, 'hook-test.git') } + let(:hooks_dir) { File.join(repo_path, 'hooks') } + let(:target_hooks_dir) { Gitlab.config.gitlab_shell.hooks_path } + let(:existing_target) { File.join(repo_path, 'foobar') } + + before do + FileUtils.rm_rf(repo_path) + FileUtils.mkdir_p(repo_path) + end + + context 'hooks is a directory' do + let(:existing_file) { File.join(hooks_dir, 'my-file') } + + before do + FileUtils.mkdir_p(hooks_dir) + FileUtils.touch(existing_file) + described_class.create_hooks(repo_path, target_hooks_dir) + end + + it { expect(File.readlink(hooks_dir)).to eq(target_hooks_dir) } + it { expect(Dir[File.join(repo_path, "hooks.old.*/my-file")].count).to eq(1) } + end + + context 'hooks is a valid symlink' do + before do + FileUtils.mkdir_p existing_target + File.symlink(existing_target, hooks_dir) + described_class.create_hooks(repo_path, target_hooks_dir) + end + + it { expect(File.readlink(hooks_dir)).to eq(target_hooks_dir) } + end + + context 'hooks is a broken symlink' do + before do + FileUtils.rm_f(existing_target) + File.symlink(existing_target, hooks_dir) + described_class.create_hooks(repo_path, target_hooks_dir) + end + + it { expect(File.readlink(hooks_dir)).to eq(target_hooks_dir) } + end + end + describe "Respond to" do subject { repository } @@ -588,12 +633,12 @@ describe Gitlab::Git::Repository, seed_helper: true do end end - describe '#fetch_as_mirror_without_shell' do + describe '#fetch_repository_as_mirror' do let(:new_repository) do Gitlab::Git::Repository.new('default', 'my_project.git', '') end - subject { new_repository.fetch_as_mirror_without_shell(repository.path) } + subject { new_repository.fetch_repository_as_mirror(repository) } before do Gitlab::Shell.new.add_repository('default', 'my_project') @@ -603,7 +648,7 @@ describe Gitlab::Git::Repository, seed_helper: true do Gitlab::Shell.new.remove_repository(TestEnv.repos_path, 'my_project') end - it 'fetches a url as a mirror remote' do + it 'fetches a repository as a mirror remote' do subject expect(refs(new_repository.path)).to eq(refs(repository.path)) @@ -1662,21 +1707,6 @@ describe Gitlab::Git::Repository, seed_helper: true do end end - describe '#fetch_remote_without_shell' do - let(:git_path) { Gitlab.config.git.bin_path } - let(:remote_name) { 'my_remote' } - - subject { repository.fetch_remote_without_shell(remote_name) } - - it 'fetches the remote and returns true if the command was successful' do - expect(repository).to receive(:popen) - .with(%W(#{git_path} fetch #{remote_name}), repository.path, {}) - .and_return(['', 0]) - - expect(subject).to be(true) - end - end - describe '#merge' do let(:repository) do Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '') diff --git a/spec/lib/gitlab/git/storage/checker_spec.rb b/spec/lib/gitlab/git/storage/checker_spec.rb new file mode 100644 index 00000000000..d74c3bcb04c --- /dev/null +++ b/spec/lib/gitlab/git/storage/checker_spec.rb @@ -0,0 +1,132 @@ +require 'spec_helper' + +describe Gitlab::Git::Storage::Checker, :clean_gitlab_redis_shared_state do + let(:storage_name) { 'default' } + let(:hostname) { Gitlab::Environment.hostname } + let(:cache_key) { "storage_accessible:#{storage_name}:#{hostname}" } + + subject(:checker) { described_class.new(storage_name) } + + def value_from_redis(name) + Gitlab::Git::Storage.redis.with do |redis| + redis.hmget(cache_key, name) + end.first + end + + def set_in_redis(name, value) + Gitlab::Git::Storage.redis.with do |redis| + redis.hmset(cache_key, name, value) + end.first + end + + describe '.check_all' do + it 'calls a check for each storage' do + fake_checker_default = double + fake_checker_broken = double + fake_logger = fake_logger + + expect(described_class).to receive(:new).with('default', fake_logger) { fake_checker_default } + expect(described_class).to receive(:new).with('broken', fake_logger) { fake_checker_broken } + expect(fake_checker_default).to receive(:check_with_lease) + expect(fake_checker_broken).to receive(:check_with_lease) + + described_class.check_all(fake_logger) + end + + context 'with broken storage', :broken_storage do + it 'returns the results' do + expected_result = [ + { storage: 'default', success: true }, + { storage: 'broken', success: false } + ] + + expect(described_class.check_all).to eq(expected_result) + end + end + end + + describe '#initialize' do + it 'assigns the settings' do + expect(checker.hostname).to eq(hostname) + expect(checker.storage).to eq('default') + expect(checker.storage_path).to eq(TestEnv.repos_path) + end + end + + describe '#check_with_lease' do + it 'only allows one check at a time' do + expect(checker).to receive(:check).once { sleep 1 } + + thread = Thread.new { checker.check_with_lease } + checker.check_with_lease + thread.join + end + + it 'returns a result hash' do + expect(checker.check_with_lease).to eq(storage: 'default', success: true) + end + end + + describe '#check' do + it 'tracks that the storage was accessible' do + set_in_redis(:failure_count, 10) + set_in_redis(:last_failure, Time.now.to_f) + + checker.check + + expect(value_from_redis(:failure_count).to_i).to eq(0) + expect(value_from_redis(:last_failure)).to be_empty + expect(value_from_redis(:first_failure)).to be_empty + end + + it 'calls the check with the correct arguments' do + stub_application_setting(circuitbreaker_storage_timeout: 30, + circuitbreaker_access_retries: 3) + + expect(Gitlab::Git::Storage::ForkedStorageCheck) + .to receive(:storage_available?).with(TestEnv.repos_path, 30, 3) + .and_call_original + + checker.check + end + + it 'returns `true`' do + expect(checker.check).to eq(true) + end + + it 'maintains known storage keys' do + Timecop.freeze do + # Insert an old key to expire + old_entry = Time.now.to_i - 3.days.to_i + Gitlab::Git::Storage.redis.with do |redis| + redis.zadd(Gitlab::Git::Storage::REDIS_KNOWN_KEYS, old_entry, 'to_be_removed') + end + + checker.check + + known_keys = Gitlab::Git::Storage.redis.with do |redis| + redis.zrange(Gitlab::Git::Storage::REDIS_KNOWN_KEYS, 0, -1) + end + + expect(known_keys).to contain_exactly(cache_key) + end + end + + context 'the storage is not available', :broken_storage do + let(:storage_name) { 'broken' } + + it 'tracks that the storage was inaccessible' do + Timecop.freeze do + expect { checker.check }.to change { value_from_redis(:failure_count).to_i }.by(1) + + expect(value_from_redis(:last_failure)).not_to be_empty + expect(value_from_redis(:first_failure)).not_to be_empty + end + end + + it 'returns `false`' do + expect(checker.check).to eq(false) + end + end + end +end diff --git a/spec/lib/gitlab/git/storage/circuit_breaker_spec.rb b/spec/lib/gitlab/git/storage/circuit_breaker_spec.rb index f34c9f09057..210b90bfba9 100644 --- a/spec/lib/gitlab/git/storage/circuit_breaker_spec.rb +++ b/spec/lib/gitlab/git/storage/circuit_breaker_spec.rb @@ -1,11 +1,18 @@ require 'spec_helper' -describe Gitlab::Git::Storage::CircuitBreaker, clean_gitlab_redis_shared_state: true, broken_storage: true do +describe Gitlab::Git::Storage::CircuitBreaker, :broken_storage do let(:storage_name) { 'default' } let(:circuit_breaker) { described_class.new(storage_name, hostname) } let(:hostname) { Gitlab::Environment.hostname } let(:cache_key) { "storage_accessible:#{storage_name}:#{hostname}" } + def set_in_redis(name, value) + Gitlab::Git::Storage.redis.with do |redis| + redis.zadd(Gitlab::Git::Storage::REDIS_KNOWN_KEYS, 0, cache_key) + redis.hmset(cache_key, name, value) + end.first + end + before do # Override test-settings for the circuitbreaker with something more realistic # for these specs. @@ -19,36 +26,7 @@ describe Gitlab::Git::Storage::CircuitBreaker, clean_gitlab_redis_shared_state: ) end - def value_from_redis(name) - Gitlab::Git::Storage.redis.with do |redis| - redis.hmget(cache_key, name) - end.first - end - - def set_in_redis(name, value) - Gitlab::Git::Storage.redis.with do |redis| - redis.zadd(Gitlab::Git::Storage::REDIS_KNOWN_KEYS, 0, cache_key) - redis.hmset(cache_key, name, value) - end.first - end - - describe '.reset_all!' do - it 'clears all entries form redis' do - set_in_redis(:failure_count, 10) - - described_class.reset_all! - - key_exists = Gitlab::Git::Storage.redis.with { |redis| redis.exists(cache_key) } - - expect(key_exists).to be_falsey - end - - it 'does not break when there are no keys in redis' do - expect { described_class.reset_all! }.not_to raise_error - end - end - - describe '.for_storage' do + describe '.for_storage', :request_store do it 'only builds a single circuitbreaker per storage' do expect(described_class).to receive(:new).once.and_call_original @@ -71,7 +49,6 @@ describe Gitlab::Git::Storage::CircuitBreaker, clean_gitlab_redis_shared_state: it 'assigns the settings' do expect(circuit_breaker.hostname).to eq(hostname) expect(circuit_breaker.storage).to eq('default') - expect(circuit_breaker.storage_path).to eq(TestEnv.repos_path) end end @@ -91,9 +68,9 @@ describe Gitlab::Git::Storage::CircuitBreaker, clean_gitlab_redis_shared_state: end end - describe '#failure_wait_time' do + describe '#check_interval' do it 'reads the value from settings' do - expect(circuit_breaker.failure_wait_time).to eq(1) + expect(circuit_breaker.check_interval).to eq(1) end end @@ -114,12 +91,6 @@ describe Gitlab::Git::Storage::CircuitBreaker, clean_gitlab_redis_shared_state: expect(circuit_breaker.access_retries).to eq(4) end end - - describe '#backoff_threshold' do - it 'reads the value from settings' do - expect(circuit_breaker.backoff_threshold).to eq(5) - end - end end describe '#perform' do @@ -134,19 +105,6 @@ describe Gitlab::Git::Storage::CircuitBreaker, clean_gitlab_redis_shared_state: end end - it 'raises the correct exception when backing off' do - Timecop.freeze do - set_in_redis(:last_failure, 1.second.ago.to_f) - set_in_redis(:failure_count, 90) - - expect { |b| circuit_breaker.perform(&b) } - .to raise_error do |exception| - expect(exception).to be_kind_of(Gitlab::Git::Storage::Failing) - expect(exception.retry_after).to eq(30) - end - end - end - it 'yields the block' do expect { |b| circuit_breaker.perform(&b) } .to yield_control @@ -170,54 +128,6 @@ describe Gitlab::Git::Storage::CircuitBreaker, clean_gitlab_redis_shared_state: .to raise_error(Rugged::OSError) end - it 'tracks that the storage was accessible' do - set_in_redis(:failure_count, 10) - set_in_redis(:last_failure, Time.now.to_f) - - circuit_breaker.perform { '' } - - expect(value_from_redis(:failure_count).to_i).to eq(0) - expect(value_from_redis(:last_failure)).to be_empty - expect(circuit_breaker.failure_count).to eq(0) - expect(circuit_breaker.last_failure).to be_nil - end - - it 'maintains known storage keys' do - Timecop.freeze do - # Insert an old key to expire - old_entry = Time.now.to_i - 3.days.to_i - Gitlab::Git::Storage.redis.with do |redis| - redis.zadd(Gitlab::Git::Storage::REDIS_KNOWN_KEYS, old_entry, 'to_be_removed') - end - - circuit_breaker.perform { '' } - - known_keys = Gitlab::Git::Storage.redis.with do |redis| - redis.zrange(Gitlab::Git::Storage::REDIS_KNOWN_KEYS, 0, -1) - end - - expect(known_keys).to contain_exactly(cache_key) - end - end - - it 'only performs the accessibility check once' do - expect(Gitlab::Git::Storage::ForkedStorageCheck) - .to receive(:storage_available?).once.and_call_original - - 2.times { circuit_breaker.perform { '' } } - end - - it 'calls the check with the correct arguments' do - stub_application_setting(circuitbreaker_storage_timeout: 30, - circuitbreaker_access_retries: 3) - - expect(Gitlab::Git::Storage::ForkedStorageCheck) - .to receive(:storage_available?).with(TestEnv.repos_path, 30, 3) - .and_call_original - - circuit_breaker.perform { '' } - end - context 'with the feature disabled' do before do stub_feature_flags(git_storage_circuit_breaker: false) @@ -240,31 +150,6 @@ describe Gitlab::Git::Storage::CircuitBreaker, clean_gitlab_redis_shared_state: expect(result).to eq('hello') end end - - context 'the storage is not available' do - let(:storage_name) { 'broken' } - - it 'raises the correct exception' do - expect(circuit_breaker).to receive(:track_storage_inaccessible) - - expect { circuit_breaker.perform { '' } } - .to raise_error do |exception| - expect(exception).to be_kind_of(Gitlab::Git::Storage::Inaccessible) - expect(exception.retry_after).to eq(30) - end - end - - it 'tracks that the storage was inaccessible' do - Timecop.freeze do - expect { circuit_breaker.perform { '' } }.to raise_error(Gitlab::Git::Storage::Inaccessible) - - expect(value_from_redis(:failure_count).to_i).to eq(1) - expect(value_from_redis(:last_failure)).not_to be_empty - expect(circuit_breaker.failure_count).to eq(1) - expect(circuit_breaker.last_failure).to be_within(1.second).of(Time.now) - end - end - end end describe '#circuit_broken?' do @@ -283,32 +168,6 @@ describe Gitlab::Git::Storage::CircuitBreaker, clean_gitlab_redis_shared_state: end end - describe '#backing_off?' do - it 'is true when there was a recent failure' do - Timecop.freeze do - set_in_redis(:last_failure, 1.second.ago.to_f) - set_in_redis(:failure_count, 90) - - expect(circuit_breaker.backing_off?).to be_truthy - end - end - - context 'the `failure_wait_time` is set to 0' do - before do - stub_application_setting(circuitbreaker_failure_wait_time: 0) - end - - it 'is working even when there are failures' do - Timecop.freeze do - set_in_redis(:last_failure, 0.seconds.ago.to_f) - set_in_redis(:failure_count, 90) - - expect(circuit_breaker.backing_off?).to be_falsey - end - end - end - end - describe '#last_failure' do it 'returns the last failure time' do time = Time.parse("2017-05-26 17:52:30") diff --git a/spec/lib/gitlab/git/storage/failure_info_spec.rb b/spec/lib/gitlab/git/storage/failure_info_spec.rb new file mode 100644 index 00000000000..bae88fdda86 --- /dev/null +++ b/spec/lib/gitlab/git/storage/failure_info_spec.rb @@ -0,0 +1,70 @@ +require 'spec_helper' + +describe Gitlab::Git::Storage::FailureInfo, :broken_storage do + let(:storage_name) { 'default' } + let(:hostname) { Gitlab::Environment.hostname } + let(:cache_key) { "storage_accessible:#{storage_name}:#{hostname}" } + + def value_from_redis(name) + Gitlab::Git::Storage.redis.with do |redis| + redis.hmget(cache_key, name) + end.first + end + + def set_in_redis(name, value) + Gitlab::Git::Storage.redis.with do |redis| + redis.zadd(Gitlab::Git::Storage::REDIS_KNOWN_KEYS, 0, cache_key) + redis.hmset(cache_key, name, value) + end.first + end + + describe '.reset_all!' do + it 'clears all entries form redis' do + set_in_redis(:failure_count, 10) + + described_class.reset_all! + + key_exists = Gitlab::Git::Storage.redis.with { |redis| redis.exists(cache_key) } + + expect(key_exists).to be_falsey + end + + it 'does not break when there are no keys in redis' do + expect { described_class.reset_all! }.not_to raise_error + end + end + + describe '.load' do + it 'loads failure information for a storage on a host' do + first_failure = Time.parse("2017-11-14 17:52:30") + last_failure = Time.parse("2017-11-14 18:54:37") + failure_count = 11 + + set_in_redis(:first_failure, first_failure.to_i) + set_in_redis(:last_failure, last_failure.to_i) + set_in_redis(:failure_count, failure_count.to_i) + + info = described_class.load(cache_key) + + expect(info.first_failure).to eq(first_failure) + expect(info.last_failure).to eq(last_failure) + expect(info.failure_count).to eq(failure_count) + end + end + + describe '#no_failures?' do + it 'is true when there are no failures' do + info = described_class.new(nil, nil, 0) + + expect(info.no_failures?).to be_truthy + end + + it 'is false when there are failures' do + info = described_class.new(Time.parse("2017-11-14 17:52:30"), + Time.parse("2017-11-14 18:54:37"), + 20) + + expect(info.no_failures?).to be_falsy + end + end +end diff --git a/spec/lib/gitlab/git/storage/health_spec.rb b/spec/lib/gitlab/git/storage/health_spec.rb index d7a52a04fbb..bb670fc5d94 100644 --- a/spec/lib/gitlab/git/storage/health_spec.rb +++ b/spec/lib/gitlab/git/storage/health_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Gitlab::Git::Storage::Health, clean_gitlab_redis_shared_state: true, broken_storage: true do +describe Gitlab::Git::Storage::Health, broken_storage: true do let(:host1_key) { 'storage_accessible:broken:web01' } let(:host2_key) { 'storage_accessible:default:kiq01' } diff --git a/spec/lib/gitlab/git/storage/null_circuit_breaker_spec.rb b/spec/lib/gitlab/git/storage/null_circuit_breaker_spec.rb index 5db37f55e03..93ad20011de 100644 --- a/spec/lib/gitlab/git/storage/null_circuit_breaker_spec.rb +++ b/spec/lib/gitlab/git/storage/null_circuit_breaker_spec.rb @@ -27,7 +27,7 @@ describe Gitlab::Git::Storage::NullCircuitBreaker do end describe '#failure_info' do - it { Timecop.freeze { expect(breaker.failure_info).to eq(Gitlab::Git::Storage::CircuitBreaker::FailureInfo.new(Time.now, breaker.failure_count_threshold)) } } + it { expect(breaker.failure_info.no_failures?).to be_falsy } end end @@ -49,7 +49,7 @@ describe Gitlab::Git::Storage::NullCircuitBreaker do end describe '#failure_info' do - it { expect(breaker.failure_info).to eq(Gitlab::Git::Storage::CircuitBreaker::FailureInfo.new(nil, 0)) } + it { expect(breaker.failure_info.no_failures?).to be_truthy } end end diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb index c9643c5da47..2db560c2cec 100644 --- a/spec/lib/gitlab/git_access_spec.rb +++ b/spec/lib/gitlab/git_access_spec.rb @@ -193,7 +193,15 @@ describe Gitlab::GitAccess do let(:actor) { build(:rsa_deploy_key_2048, user: user) } end - describe '#check_project_moved!' do + shared_examples 'check_project_moved' do + it 'enqueues a redirected message' do + push_access_check + + expect(Gitlab::Checks::ProjectMoved.fetch_redirect_message(user.id, project.id)).not_to be_nil + end + end + + describe '#check_project_moved!', :clean_gitlab_redis_shared_state do before do project.add_master(user) end @@ -207,7 +215,40 @@ describe Gitlab::GitAccess do end end - context 'when a redirect was followed to find the project' do + context 'when a permanent redirect and ssh protocol' do + let(:redirected_path) { 'some/other-path' } + + before do + allow_any_instance_of(Gitlab::Checks::ProjectMoved).to receive(:permanent_redirect?).and_return(true) + end + + it 'allows push and pull access' do + aggregate_failures do + expect { push_access_check }.not_to raise_error + end + end + + it_behaves_like 'check_project_moved' + end + + context 'with a permanent redirect and http protocol' do + let(:redirected_path) { 'some/other-path' } + let(:protocol) { 'http' } + + before do + allow_any_instance_of(Gitlab::Checks::ProjectMoved).to receive(:permanent_redirect?).and_return(true) + end + + it 'allows_push and pull access' do + aggregate_failures do + expect { push_access_check }.not_to raise_error + end + end + + it_behaves_like 'check_project_moved' + end + + context 'with a temporal redirect and ssh protocol' do let(:redirected_path) { 'some/other-path' } it 'blocks push and pull access' do @@ -219,16 +260,15 @@ describe Gitlab::GitAccess do expect { pull_access_check }.to raise_error(described_class::ProjectMovedError, /git remote set-url origin #{project.ssh_url_to_repo}/) end end + end - context 'http protocol' do - let(:protocol) { 'http' } + context 'with a temporal redirect and http protocol' do + let(:redirected_path) { 'some/other-path' } + let(:protocol) { 'http' } - it 'includes the path to the project using HTTP' do - aggregate_failures do - expect { push_access_check }.to raise_error(described_class::ProjectMovedError, /git remote set-url origin #{project.http_url_to_repo}/) - expect { pull_access_check }.to raise_error(described_class::ProjectMovedError, /git remote set-url origin #{project.http_url_to_repo}/) - end - end + it 'does not allow to push and pull access' do + expect { push_access_check }.to raise_error(described_class::ProjectMovedError, /git remote set-url origin #{project.http_url_to_repo}/) + expect { pull_access_check }.to raise_error(described_class::ProjectMovedError, /git remote set-url origin #{project.http_url_to_repo}/) end end end diff --git a/spec/lib/gitlab/identifier_spec.rb b/spec/lib/gitlab/identifier_spec.rb index cfaeb1f0d4f..0385dd762c2 100644 --- a/spec/lib/gitlab/identifier_spec.rb +++ b/spec/lib/gitlab/identifier_spec.rb @@ -70,6 +70,10 @@ describe Gitlab::Identifier do expect(identifier.identify_using_commit(project, '123')).to eq(user) end end + + it 'returns nil if the project & ref are not present' do + expect(identifier.identify_using_commit(nil, nil)).to be_nil + end end describe '#identify_using_user' do diff --git a/spec/lib/gitlab/ldap/user_spec.rb b/spec/lib/gitlab/ldap/user_spec.rb index 260df6e4dae..048caa38fcf 100644 --- a/spec/lib/gitlab/ldap/user_spec.rb +++ b/spec/lib/gitlab/ldap/user_spec.rb @@ -38,7 +38,6 @@ describe Gitlab::LDAP::User do it "does not mark existing ldap user as changed" do create(:omniauth_user, email: 'john@example.com', extern_uid: 'uid=john smith,ou=people,dc=example,dc=com', provider: 'ldapmain') - ldap_user.gl_user.user_synced_attributes_metadata(provider: 'ldapmain', email: true) expect(ldap_user.changed?).to be_falsey end end @@ -144,11 +143,15 @@ describe Gitlab::LDAP::User do expect(ldap_user.gl_user.email).to eq(info[:email]) end - it "has user_synced_attributes_metadata email set to true" do + it "has email set as synced" do expect(ldap_user.gl_user.user_synced_attributes_metadata.email_synced).to be_truthy end - it "has synced_attribute_provider set to ldapmain" do + it "has email set as read-only" do + expect(ldap_user.gl_user.read_only_attribute?(:email)).to be_truthy + end + + it "has synced attributes provider set to ldapmain" do expect(ldap_user.gl_user.user_synced_attributes_metadata.provider).to eql 'ldapmain' end end @@ -162,9 +165,13 @@ describe Gitlab::LDAP::User do expect(ldap_user.gl_user.temp_oauth_email?).to be_truthy end - it "has synced attribute email set to false" do + it "has email set as not synced" do expect(ldap_user.gl_user.user_synced_attributes_metadata.email_synced).to be_falsey end + + it "does not have email set as read-only" do + expect(ldap_user.gl_user.read_only_attribute?(:email)).to be_falsey + end end end diff --git a/spec/lib/gitlab/metrics/method_call_spec.rb b/spec/lib/gitlab/metrics/method_call_spec.rb index 5341addf911..78767d06462 100644 --- a/spec/lib/gitlab/metrics/method_call_spec.rb +++ b/spec/lib/gitlab/metrics/method_call_spec.rb @@ -20,9 +20,39 @@ describe Gitlab::Metrics::MethodCall do context 'prometheus instrumentation is enabled' do before do + allow(Feature.get(:prometheus_metrics_method_instrumentation)).to receive(:enabled?).and_call_original + described_class.measurement_enabled_cache_expires_at.value = Time.now.to_i - 1 Feature.get(:prometheus_metrics_method_instrumentation).enable end + around do |example| + Timecop.freeze do + example.run + end + end + + it 'caches subsequent invocations of feature check' do + 10.times do + method_call.measure { 'foo' } + end + + expect(Feature.get(:prometheus_metrics_method_instrumentation)).to have_received(:enabled?).once + end + + it 'expires feature check cache after 1 minute' do + method_call.measure { 'foo' } + + Timecop.travel(1.minute.from_now) do + method_call.measure { 'foo' } + end + + Timecop.travel(1.minute.from_now + 1.second) do + method_call.measure { 'foo' } + end + + expect(Feature.get(:prometheus_metrics_method_instrumentation)).to have_received(:enabled?).twice + end + it 'observes the performance of the supplied block' do expect(described_class.call_duration_histogram) .to receive(:observe) @@ -34,6 +64,8 @@ describe Gitlab::Metrics::MethodCall do context 'prometheus instrumentation is disabled' do before do + described_class.measurement_enabled_cache_expires_at.value = Time.now.to_i - 1 + Feature.get(:prometheus_metrics_method_instrumentation).disable end diff --git a/spec/lib/gitlab/metrics/samplers/influx_sampler_spec.rb b/spec/lib/gitlab/metrics/samplers/influx_sampler_spec.rb index 667e4747897..f66451c5188 100644 --- a/spec/lib/gitlab/metrics/samplers/influx_sampler_spec.rb +++ b/spec/lib/gitlab/metrics/samplers/influx_sampler_spec.rb @@ -21,7 +21,6 @@ describe Gitlab::Metrics::Samplers::InfluxSampler do it 'samples various statistics' do expect(sampler).to receive(:sample_memory_usage) expect(sampler).to receive(:sample_file_descriptors) - expect(sampler).to receive(:sample_objects) expect(sampler).to receive(:sample_gc) expect(sampler).to receive(:flush) @@ -72,28 +71,6 @@ describe Gitlab::Metrics::Samplers::InfluxSampler do end end - if Gitlab::Metrics.mri? - describe '#sample_objects' do - it 'adds a metric containing the amount of allocated objects' do - expect(sampler).to receive(:add_metric) - .with(/object_counts/, an_instance_of(Hash), an_instance_of(Hash)) - .at_least(:once) - .and_call_original - - sampler.sample_objects - end - - it 'ignores classes without a name' do - expect(Allocations).to receive(:to_hash).and_return({ Class.new => 4 }) - - expect(sampler).not_to receive(:add_metric) - .with('object_counts', an_instance_of(Hash), type: nil) - - sampler.sample_objects - end - end - end - describe '#sample_gc' do it 'adds a metric containing garbage collection statistics' do expect(GC::Profiler).to receive(:total_time).and_return(0.24) diff --git a/spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb b/spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb index 53699327da1..375cbf8a9ca 100644 --- a/spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb +++ b/spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb @@ -11,7 +11,6 @@ describe Gitlab::Metrics::Samplers::RubySampler do it 'samples various statistics' do expect(Gitlab::Metrics::System).to receive(:memory_usage) expect(Gitlab::Metrics::System).to receive(:file_descriptor_count) - expect(sampler).to receive(:sample_objects) expect(sampler).to receive(:sample_gc) sampler.sample @@ -65,26 +64,4 @@ describe Gitlab::Metrics::Samplers::RubySampler do sampler.sample end end - - if Gitlab::Metrics.mri? - describe '#sample_objects' do - it 'adds a metric containing the amount of allocated objects' do - expect(sampler.metrics[:objects_total]).to receive(:set) - .with(include(class: anything), be > 0) - .at_least(:once) - .and_call_original - - sampler.sample - end - - it 'ignores classes without a name' do - expect(Allocations).to receive(:to_hash).and_return({ Class.new => 4 }) - - expect(sampler.metrics[:objects_total]).not_to receive(:set) - .with(include(class: 'object_counts'), anything) - - sampler.sample - end - end - end end diff --git a/spec/lib/gitlab/o_auth/user_spec.rb b/spec/lib/gitlab/o_auth/user_spec.rb index 2f19fb7312d..6334bcd0156 100644 --- a/spec/lib/gitlab/o_auth/user_spec.rb +++ b/spec/lib/gitlab/o_auth/user_spec.rb @@ -202,11 +202,13 @@ describe Gitlab::OAuth::User do end context "and no account for the LDAP user" do - it "creates a user with dual LDAP and omniauth identities" do + before do allow(Gitlab::LDAP::Person).to receive(:find_by_uid).and_return(ldap_user) oauth_user.save + end + it "creates a user with dual LDAP and omniauth identities" do expect(gl_user).to be_valid expect(gl_user.username).to eql uid expect(gl_user.email).to eql 'johndoe@example.com' @@ -219,6 +221,18 @@ describe Gitlab::OAuth::User do ] ) end + + it "has email set as synced" do + expect(gl_user.user_synced_attributes_metadata.email_synced).to be_truthy + end + + it "has email set as read-only" do + expect(gl_user.read_only_attribute?(:email)).to be_truthy + end + + it "has synced attributes provider set to ldapmain" do + expect(gl_user.user_synced_attributes_metadata.provider).to eql 'ldapmain' + end end context "and LDAP user has an account already" do @@ -440,11 +454,15 @@ describe Gitlab::OAuth::User do expect(gl_user.email).to eq(info_hash[:email]) end - it "has external_attributes set to true" do - expect(gl_user.user_synced_attributes_metadata).not_to be_nil + it "has email set as synced" do + expect(gl_user.user_synced_attributes_metadata.email_synced).to be_truthy + end + + it "has email set as read-only" do + expect(gl_user.read_only_attribute?(:email)).to be_truthy end - it "has attributes_provider set to my-provider" do + it "has synced attributes provider set to my-provider" do expect(gl_user.user_synced_attributes_metadata.provider).to eql 'my-provider' end end @@ -458,10 +476,13 @@ describe Gitlab::OAuth::User do expect(gl_user.email).not_to eq(info_hash[:email]) end - it "has user_synced_attributes_metadata set to nil" do - expect(gl_user.user_synced_attributes_metadata.provider).to eql 'my-provider' + it "has email set as not synced" do expect(gl_user.user_synced_attributes_metadata.email_synced).to be_falsey end + + it "does not have email set as read-only" do + expect(gl_user.read_only_attribute?(:email)).to be_falsey + end end end @@ -508,11 +529,15 @@ describe Gitlab::OAuth::User do expect(gl_user.email).to eq(info_hash[:email]) end - it "has email_synced_attribute set to true" do + it "has email set as synced" do expect(gl_user.user_synced_attributes_metadata.email_synced).to be(true) end - it "has my-provider as attributes_provider" do + it "has email set as read-only" do + expect(gl_user.read_only_attribute?(:email)).to be_truthy + end + + it "has synced attributes provider set to my-provider" do expect(gl_user.user_synced_attributes_metadata.provider).to eql 'my-provider' end end @@ -524,7 +549,14 @@ describe Gitlab::OAuth::User do it "does not update the user email" do expect(gl_user.email).not_to eq(info_hash[:email]) - expect(gl_user.user_synced_attributes_metadata.email_synced).to be(false) + end + + it "has email set as not synced" do + expect(gl_user.user_synced_attributes_metadata.email_synced).to be_falsey + end + + it "does not have email set as read-only" do + expect(gl_user.read_only_attribute?(:email)).to be_falsey end end end diff --git a/spec/lib/gitlab/reference_extractor_spec.rb b/spec/lib/gitlab/reference_extractor_spec.rb index ef874368077..8ec3f55e6de 100644 --- a/spec/lib/gitlab/reference_extractor_spec.rb +++ b/spec/lib/gitlab/reference_extractor_spec.rb @@ -115,6 +115,15 @@ describe Gitlab::ReferenceExtractor do end end + it 'does not include anchors from table of contents in issue references' do + issue1 = create(:issue, project: project) + issue2 = create(:issue, project: project) + + subject.analyze("not real issue <h4>#{issue1.iid}</h4>, real issue #{issue2.to_reference}") + + expect(subject.issues).to match_array([issue2]) + end + it 'accesses valid issue objects' do @i0 = create(:issue, project: project) @i1 = create(:issue, project: project) diff --git a/spec/lib/gitlab/shell_spec.rb b/spec/lib/gitlab/shell_spec.rb index eec6858a5de..dd779b04741 100644 --- a/spec/lib/gitlab/shell_spec.rb +++ b/spec/lib/gitlab/shell_spec.rb @@ -2,12 +2,19 @@ require 'spec_helper' require 'stringio' describe Gitlab::Shell do - let(:project) { double('Project', id: 7, path: 'diaspora') } + set(:project) { create(:project, :repository) } + let(:gitlab_shell) { described_class.new } let(:popen_vars) { { 'GIT_TERMINAL_PROMPT' => ENV['GIT_TERMINAL_PROMPT'] } } + let(:gitlab_projects) { double('gitlab_projects') } + let(:timeout) { Gitlab.config.gitlab_shell.git_timeout } before do allow(Project).to receive(:find).and_return(project) + + allow(gitlab_shell).to receive(:gitlab_projects) + .with(project.repository_storage_path, project.disk_path + '.git') + .and_return(gitlab_projects) end it { is_expected.to respond_to :add_key } @@ -44,38 +51,6 @@ describe Gitlab::Shell do end end - describe 'projects commands' do - let(:gitlab_shell_path) { File.expand_path('tmp/tests/gitlab-shell') } - let(:projects_path) { File.join(gitlab_shell_path, 'bin/gitlab-projects') } - let(:gitlab_shell_hooks_path) { File.join(gitlab_shell_path, 'hooks') } - - before do - allow(Gitlab.config.gitlab_shell).to receive(:path).and_return(gitlab_shell_path) - allow(Gitlab.config.gitlab_shell).to receive(:hooks_path).and_return(gitlab_shell_hooks_path) - allow(Gitlab.config.gitlab_shell).to receive(:git_timeout).and_return(800) - end - - describe '#mv_repository' do - it 'executes the command' do - expect(gitlab_shell).to receive(:gitlab_shell_fast_execute).with( - [projects_path, 'mv-project', 'storage/path', 'project/path.git', 'new/path.git'] - ) - gitlab_shell.mv_repository('storage/path', 'project/path', 'new/path') - end - end - - describe '#add_key' do - it 'removes trailing garbage' do - allow(gitlab_shell).to receive(:gitlab_shell_keys_path).and_return(:gitlab_shell_keys_path) - expect(gitlab_shell).to receive(:gitlab_shell_fast_execute).with( - [:gitlab_shell_keys_path, 'add-key', 'key-123', 'ssh-rsa foobar'] - ) - - gitlab_shell.add_key('key-123', 'ssh-rsa foobar trailing garbage') - end - end - end - describe Gitlab::Shell::KeyAdder do describe '#add_key' do it 'removes trailing garbage' do @@ -121,6 +96,17 @@ describe Gitlab::Shell do allow(Gitlab.config.gitlab_shell).to receive(:git_timeout).and_return(800) end + describe '#add_key' do + it 'removes trailing garbage' do + allow(gitlab_shell).to receive(:gitlab_shell_keys_path).and_return(:gitlab_shell_keys_path) + expect(gitlab_shell).to receive(:gitlab_shell_fast_execute).with( + [:gitlab_shell_keys_path, 'add-key', 'key-123', 'ssh-rsa foobar'] + ) + + gitlab_shell.add_key('key-123', 'ssh-rsa foobar trailing garbage') + end + end + describe '#add_repository' do shared_examples '#add_repository' do let(:repository_storage) { 'default' } @@ -162,83 +148,76 @@ describe Gitlab::Shell do end describe '#remove_repository' do + subject { gitlab_shell.remove_repository(project.repository_storage_path, project.disk_path) } + it 'returns true when the command succeeds' do - expect(Gitlab::Popen).to receive(:popen) - .with([projects_path, 'rm-project', 'current/storage', 'project/path.git'], - nil, popen_vars).and_return([nil, 0]) + expect(gitlab_projects).to receive(:rm_project) { true } - expect(gitlab_shell.remove_repository('current/storage', 'project/path')).to be true + is_expected.to be_truthy end it 'returns false when the command fails' do - expect(Gitlab::Popen).to receive(:popen) - .with([projects_path, 'rm-project', 'current/storage', 'project/path.git'], - nil, popen_vars).and_return(["error", 1]) + expect(gitlab_projects).to receive(:rm_project) { false } - expect(gitlab_shell.remove_repository('current/storage', 'project/path')).to be false + is_expected.to be_falsy end end describe '#mv_repository' do it 'returns true when the command succeeds' do - expect(Gitlab::Popen).to receive(:popen) - .with([projects_path, 'mv-project', 'current/storage', 'project/path.git', 'project/newpath.git'], - nil, popen_vars).and_return([nil, 0]) + expect(gitlab_projects).to receive(:mv_project).with('project/newpath.git') { true } - expect(gitlab_shell.mv_repository('current/storage', 'project/path', 'project/newpath')).to be true + expect(gitlab_shell.mv_repository(project.repository_storage_path, project.disk_path, 'project/newpath')).to be_truthy end it 'returns false when the command fails' do - expect(Gitlab::Popen).to receive(:popen) - .with([projects_path, 'mv-project', 'current/storage', 'project/path.git', 'project/newpath.git'], - nil, popen_vars).and_return(["error", 1]) + expect(gitlab_projects).to receive(:mv_project).with('project/newpath.git') { false } - expect(gitlab_shell.mv_repository('current/storage', 'project/path', 'project/newpath')).to be false + expect(gitlab_shell.mv_repository(project.repository_storage_path, project.disk_path, 'project/newpath')).to be_falsy end end describe '#fork_repository' do + subject do + gitlab_shell.fork_repository( + project.repository_storage_path, + project.disk_path, + 'new/storage', + 'fork/path' + ) + end + it 'returns true when the command succeeds' do - expect(Gitlab::Popen).to receive(:popen) - .with([projects_path, 'fork-repository', 'current/storage', 'project/path.git', 'new/storage', 'fork/path.git'], - nil, popen_vars).and_return([nil, 0]) + expect(gitlab_projects).to receive(:fork_repository).with('new/storage', 'fork/path.git') { true } - expect(gitlab_shell.fork_repository('current/storage', 'project/path', 'new/storage', 'fork/path')).to be true + is_expected.to be_truthy end it 'return false when the command fails' do - expect(Gitlab::Popen).to receive(:popen) - .with([projects_path, 'fork-repository', 'current/storage', 'project/path.git', 'new/storage', 'fork/path.git'], - nil, popen_vars).and_return(["error", 1]) + expect(gitlab_projects).to receive(:fork_repository).with('new/storage', 'fork/path.git') { false } - expect(gitlab_shell.fork_repository('current/storage', 'project/path', 'new/storage', 'fork/path')).to be false + is_expected.to be_falsy end end shared_examples 'fetch_remote' do |gitaly_on| - let(:project2) { create(:project, :repository) } - let(:repository) { project2.repository } + let(:repository) { project.repository } def fetch_remote(ssh_auth = nil) - gitlab_shell.fetch_remote(repository.raw_repository, 'new/storage', ssh_auth: ssh_auth) + gitlab_shell.fetch_remote(repository.raw_repository, 'remote-name', ssh_auth: ssh_auth) end - def expect_popen(fail = false, vars = {}) - popen_args = [ - projects_path, - 'fetch-remote', - TestEnv.repos_path, - repository.relative_path, - 'new/storage', - Gitlab.config.gitlab_shell.git_timeout.to_s - ] - - return_value = fail ? ["error", 1] : [nil, 0] + def expect_gitlab_projects(fail = false, options = {}) + expect(gitlab_projects).to receive(:fetch_remote).with( + 'remote-name', + timeout, + options + ).and_return(!fail) - expect(Gitlab::Popen).to receive(:popen).with(popen_args, nil, popen_vars.merge(vars)).and_return(return_value) + allow(gitlab_projects).to receive(:output).and_return('error') if fail end - def expect_gitaly_call(fail, vars = {}) + def expect_gitaly_call(fail, options = {}) receive_fetch_remote = if fail receive(:fetch_remote).and_raise(GRPC::NotFound) @@ -250,12 +229,12 @@ describe Gitlab::Shell do end if gitaly_on - def expect_call(fail, vars = {}) - expect_gitaly_call(fail, vars) + def expect_call(fail, options = {}) + expect_gitaly_call(fail, options) end else - def expect_call(fail, vars = {}) - expect_popen(fail, vars) + def expect_call(fail, options = {}) + expect_gitlab_projects(fail, options) end end @@ -271,20 +250,27 @@ describe Gitlab::Shell do end it 'returns true when the command succeeds' do - expect_call(false) + expect_call(false, force: false, tags: true) expect(fetch_remote).to be_truthy end it 'raises an exception when the command fails' do - expect_call(true) + expect_call(true, force: false, tags: true) expect { fetch_remote }.to raise_error(Gitlab::Shell::Error) end + it 'allows forced and no_tags to be changed' do + expect_call(false, force: true, tags: false) + + result = gitlab_shell.fetch_remote(repository.raw_repository, 'remote-name', forced: true, no_tags: true) + expect(result).to be_truthy + end + context 'SSH auth' do it 'passes the SSH key if specified' do - expect_call(false, 'GITLAB_SHELL_SSH_KEY' => 'foo') + expect_call(false, force: false, tags: true, ssh_key: 'foo') ssh_auth = build_ssh_auth(ssh_key_auth?: true, ssh_private_key: 'foo') @@ -292,7 +278,7 @@ describe Gitlab::Shell do end it 'does not pass an empty SSH key' do - expect_call(false) + expect_call(false, force: false, tags: true) ssh_auth = build_ssh_auth(ssh_key_auth: true, ssh_private_key: '') @@ -300,7 +286,7 @@ describe Gitlab::Shell do end it 'does not pass the key unless SSH key auth is to be used' do - expect_call(false) + expect_call(false, force: false, tags: true) ssh_auth = build_ssh_auth(ssh_key_auth: false, ssh_private_key: 'foo') @@ -308,7 +294,7 @@ describe Gitlab::Shell do end it 'passes the known_hosts data if specified' do - expect_call(false, 'GITLAB_SHELL_KNOWN_HOSTS' => 'foo') + expect_call(false, force: false, tags: true, known_hosts: 'foo') ssh_auth = build_ssh_auth(ssh_known_hosts: 'foo') @@ -316,7 +302,7 @@ describe Gitlab::Shell do end it 'does not pass empty known_hosts data' do - expect_call(false) + expect_call(false, force: false, tags: true) ssh_auth = build_ssh_auth(ssh_known_hosts: '') @@ -324,7 +310,7 @@ describe Gitlab::Shell do end it 'does not pass known_hosts data unless SSH is to be used' do - expect_call(false, popen_vars) + expect_call(false, force: false, tags: true) ssh_auth = build_ssh_auth(ssh_import?: false, ssh_known_hosts: 'foo') @@ -342,20 +328,79 @@ describe Gitlab::Shell do end describe '#import_repository' do + let(:import_url) { 'https://gitlab.com/gitlab-org/gitlab-ce.git' } + it 'returns true when the command succeeds' do - expect(Gitlab::Popen).to receive(:popen) - .with([projects_path, 'import-project', 'current/storage', 'project/path.git', 'https://gitlab.com/gitlab-org/gitlab-ce.git', "800"], - nil, popen_vars).and_return([nil, 0]) + expect(gitlab_projects).to receive(:import_project).with(import_url, timeout) { true } - expect(gitlab_shell.import_repository('current/storage', 'project/path', 'https://gitlab.com/gitlab-org/gitlab-ce.git')).to be true + result = gitlab_shell.import_repository(project.repository_storage_path, project.disk_path, import_url) + + expect(result).to be_truthy end it 'raises an exception when the command fails' do - expect(Gitlab::Popen).to receive(:popen) - .with([projects_path, 'import-project', 'current/storage', 'project/path.git', 'https://gitlab.com/gitlab-org/gitlab-ce.git', "800"], - nil, popen_vars).and_return(["error", 1]) + allow(gitlab_projects).to receive(:output) { 'error' } + expect(gitlab_projects).to receive(:import_project) { false } + + expect do + gitlab_shell.import_repository(project.repository_storage_path, project.disk_path, import_url) + end.to raise_error(Gitlab::Shell::Error, "error") + end + end + + describe '#push_remote_branches' do + subject(:result) do + gitlab_shell.push_remote_branches( + project.repository_storage_path, + project.disk_path, + 'downstream-remote', + ['master'] + ) + end + + it 'executes the command' do + expect(gitlab_projects).to receive(:push_branches) + .with('downstream-remote', timeout, true, ['master']) + .and_return(true) + + is_expected.to be_truthy + end + + it 'fails to execute the command' do + allow(gitlab_projects).to receive(:output) { 'error' } + expect(gitlab_projects).to receive(:push_branches) + .with('downstream-remote', timeout, true, ['master']) + .and_return(false) + + expect { result }.to raise_error(Gitlab::Shell::Error, 'error') + end + end + + describe '#delete_remote_branches' do + subject(:result) do + gitlab_shell.delete_remote_branches( + project.repository_storage_path, + project.disk_path, + 'downstream-remote', + ['master'] + ) + end + + it 'executes the command' do + expect(gitlab_projects).to receive(:delete_remote_branches) + .with('downstream-remote', ['master']) + .and_return(true) + + is_expected.to be_truthy + end + + it 'fails to execute the command' do + allow(gitlab_projects).to receive(:output) { 'error' } + expect(gitlab_projects).to receive(:delete_remote_branches) + .with('downstream-remote', ['master']) + .and_return(false) - expect { gitlab_shell.import_repository('current/storage', 'project/path', 'https://gitlab.com/gitlab-org/gitlab-ce.git') }.to raise_error(Gitlab::Shell::Error, "error") + expect { result }.to raise_error(Gitlab::Shell::Error, 'error') end end end diff --git a/spec/lib/gitlab/sidekiq_config_spec.rb b/spec/lib/gitlab/sidekiq_config_spec.rb index 09f95be2213..0c66d764851 100644 --- a/spec/lib/gitlab/sidekiq_config_spec.rb +++ b/spec/lib/gitlab/sidekiq_config_spec.rb @@ -16,9 +16,30 @@ describe Gitlab::SidekiqConfig do expect(queues).to include('post_receive') expect(queues).to include('merge') - expect(queues).to include('cronjob') + expect(queues).to include('cronjob:stuck_import_jobs') expect(queues).to include('mailers') expect(queues).to include('default') end end + + describe '.expand_queues' do + it 'expands queue namespaces to concrete queue names' do + queues = described_class.expand_queues(%w[cronjob]) + + expect(queues).to include('cronjob:stuck_import_jobs') + expect(queues).to include('cronjob:stuck_merge_jobs') + end + + it 'lets concrete queue names pass through' do + queues = described_class.expand_queues(%w[post_receive]) + + expect(queues).to include('post_receive') + end + + it 'lets unknown queues pass through' do + queues = described_class.expand_queues(%w[unknown]) + + expect(queues).to include('unknown') + end + end end diff --git a/spec/lib/gitlab/sidekiq_versioning/manager_spec.rb b/spec/lib/gitlab/sidekiq_versioning/manager_spec.rb new file mode 100644 index 00000000000..7debf70a16f --- /dev/null +++ b/spec/lib/gitlab/sidekiq_versioning/manager_spec.rb @@ -0,0 +1,22 @@ +require 'spec_helper' + +describe Gitlab::SidekiqVersioning::Manager do + before do + Sidekiq::Manager.prepend described_class + end + + describe '#initialize' do + it 'listens on all expanded queues' do + manager = Sidekiq::Manager.new(queues: %w[post_receive repository_fork cronjob unknown]) + + queues = manager.options[:queues] + + expect(queues).to include('post_receive') + expect(queues).to include('repository_fork') + expect(queues).to include('cronjob') + expect(queues).to include('cronjob:stuck_import_jobs') + expect(queues).to include('cronjob:stuck_merge_jobs') + expect(queues).to include('unknown') + end + end +end diff --git a/spec/lib/gitlab/sidekiq_versioning_spec.rb b/spec/lib/gitlab/sidekiq_versioning_spec.rb new file mode 100644 index 00000000000..fa6d42e730d --- /dev/null +++ b/spec/lib/gitlab/sidekiq_versioning_spec.rb @@ -0,0 +1,44 @@ +require 'spec_helper' + +describe Gitlab::SidekiqVersioning, :sidekiq, :redis do + let(:foo_worker) do + Class.new do + def self.name + 'FooWorker' + end + + include ApplicationWorker + end + end + + let(:bar_worker) do + Class.new do + def self.name + 'BarWorker' + end + + include ApplicationWorker + end + end + + before do + allow(Gitlab::SidekiqConfig).to receive(:workers).and_return([foo_worker, bar_worker]) + allow(Gitlab::SidekiqConfig).to receive(:worker_queues).and_return([foo_worker.queue, bar_worker.queue]) + end + + describe '.install!' do + it 'prepends SidekiqVersioning::Manager into Sidekiq::Manager' do + described_class.install! + + expect(Sidekiq::Manager).to include(Gitlab::SidekiqVersioning::Manager) + end + + it 'registers all versionless and versioned queues with Redis' do + described_class.install! + + queues = Sidekiq::Queue.all.map(&:name) + expect(queues).to include('foo') + expect(queues).to include('bar') + end + end +end diff --git a/spec/lib/gitlab/storage_check/cli_spec.rb b/spec/lib/gitlab/storage_check/cli_spec.rb new file mode 100644 index 00000000000..6db0925899c --- /dev/null +++ b/spec/lib/gitlab/storage_check/cli_spec.rb @@ -0,0 +1,19 @@ +require 'spec_helper' + +describe Gitlab::StorageCheck::CLI do + let(:options) { Gitlab::StorageCheck::Options.new('unix://tmp/socket.sock', nil, 1, false) } + subject(:runner) { described_class.new(options) } + + describe '#update_settings' do + it 'updates the interval when changed in a valid response and logs the change' do + fake_response = double + expect(fake_response).to receive(:valid?).and_return(true) + expect(fake_response).to receive(:check_interval).and_return(42) + expect(runner.logger).to receive(:info) + + runner.update_settings(fake_response) + + expect(options.interval).to eq(42) + end + end +end diff --git a/spec/lib/gitlab/storage_check/gitlab_caller_spec.rb b/spec/lib/gitlab/storage_check/gitlab_caller_spec.rb new file mode 100644 index 00000000000..d869022fd31 --- /dev/null +++ b/spec/lib/gitlab/storage_check/gitlab_caller_spec.rb @@ -0,0 +1,46 @@ +require 'spec_helper' + +describe Gitlab::StorageCheck::GitlabCaller do + let(:options) { Gitlab::StorageCheck::Options.new('unix://tmp/socket.sock', nil, nil, false) } + subject(:gitlab_caller) { described_class.new(options) } + + describe '#call!' do + context 'when a socket is given' do + it 'calls a socket' do + fake_connection = double + expect(fake_connection).to receive(:post) + expect(Excon).to receive(:new).with('unix://tmp/socket.sock', socket: "tmp/socket.sock") { fake_connection } + + gitlab_caller.call! + end + end + + context 'when a host is given' do + let(:options) { Gitlab::StorageCheck::Options.new('http://localhost:8080', nil, nil, false) } + + it 'it calls a http response' do + fake_connection = double + expect(Excon).to receive(:new).with('http://localhost:8080', socket: nil) { fake_connection } + expect(fake_connection).to receive(:post) + + gitlab_caller.call! + end + end + end + + describe '#headers' do + it 'Adds the JSON header' do + headers = gitlab_caller.headers + + expect(headers['Content-Type']).to eq('application/json') + end + + context 'when a token was provided' do + let(:options) { Gitlab::StorageCheck::Options.new('unix://tmp/socket.sock', 'atoken', nil, false) } + + it 'adds it to the headers' do + expect(gitlab_caller.headers['TOKEN']).to eq('atoken') + end + end + end +end diff --git a/spec/lib/gitlab/storage_check/option_parser_spec.rb b/spec/lib/gitlab/storage_check/option_parser_spec.rb new file mode 100644 index 00000000000..cad4dfbefcf --- /dev/null +++ b/spec/lib/gitlab/storage_check/option_parser_spec.rb @@ -0,0 +1,31 @@ +require 'spec_helper' + +describe Gitlab::StorageCheck::OptionParser do + describe '.parse!' do + it 'assigns all options' do + args = %w(--target unix://tmp/hello/world.sock --token thetoken --interval 42) + + options = described_class.parse!(args) + + expect(options.token).to eq('thetoken') + expect(options.interval).to eq(42) + expect(options.target).to eq('unix://tmp/hello/world.sock') + end + + it 'requires the interval to be a number' do + args = %w(--target unix://tmp/hello/world.sock --interval fortytwo) + + expect { described_class.parse!(args) }.to raise_error(OptionParser::InvalidArgument) + end + + it 'raises an error if the scheme is not included' do + args = %w(--target tmp/hello/world.sock) + + expect { described_class.parse!(args) }.to raise_error(OptionParser::InvalidArgument) + end + + it 'raises an error if both socket and host are missing' do + expect { described_class.parse!([]) }.to raise_error(OptionParser::InvalidArgument) + end + end +end diff --git a/spec/lib/gitlab/storage_check/response_spec.rb b/spec/lib/gitlab/storage_check/response_spec.rb new file mode 100644 index 00000000000..0ff2963e443 --- /dev/null +++ b/spec/lib/gitlab/storage_check/response_spec.rb @@ -0,0 +1,54 @@ +require 'spec_helper' + +describe Gitlab::StorageCheck::Response do + let(:fake_json) do + { + check_interval: 42, + results: [ + { storage: 'working', success: true }, + { storage: 'skipped', success: nil }, + { storage: 'failing', success: false } + ] + }.to_json + end + + let(:fake_http_response) do + fake_response = instance_double("Excon::Response - Status check") + allow(fake_response).to receive(:status).and_return(200) + allow(fake_response).to receive(:body).and_return(fake_json) + allow(fake_response).to receive(:headers).and_return('Content-Type' => 'application/json') + + fake_response + end + let(:response) { described_class.new(fake_http_response) } + + describe '#valid?' do + it 'is valid for a success response with parseable JSON' do + expect(response).to be_valid + end + end + + describe '#check_interval' do + it 'returns the result from the JSON' do + expect(response.check_interval).to eq(42) + end + end + + describe '#responsive_shards' do + it 'contains the names of working shards' do + expect(response.responsive_shards).to contain_exactly('working') + end + end + + describe '#skipped_shards' do + it 'contains the names of skipped shards' do + expect(response.skipped_shards).to contain_exactly('skipped') + end + end + + describe '#failing_shards' do + it 'contains the name of failing shards' do + expect(response.failing_shards).to contain_exactly('failing') + end + end +end diff --git a/spec/lib/gitlab/tcp_checker_spec.rb b/spec/lib/gitlab/tcp_checker_spec.rb new file mode 100644 index 00000000000..4acf0334496 --- /dev/null +++ b/spec/lib/gitlab/tcp_checker_spec.rb @@ -0,0 +1,32 @@ +require 'spec_helper' + +describe Gitlab::TcpChecker do + before do + @server = TCPServer.new('localhost', 0) + _, @port, _, @ip = @server.addr + end + + after do + @server.close + end + + subject(:checker) { described_class.new(@ip, @port) } + + describe '#check' do + subject { checker.check } + + it 'can connect to an open port' do + is_expected.to be_truthy + + expect(checker.error).to be_nil + end + + it 'fails to connect to a closed port' do + @server.close + + is_expected.to be_falsy + + expect(checker.error).to be_a(Errno::ECONNREFUSED) + end + end +end diff --git a/spec/lib/gitlab/utils/strong_memoize_spec.rb b/spec/lib/gitlab/utils/strong_memoize_spec.rb index 4a104ab6d97..473f8100771 100644 --- a/spec/lib/gitlab/utils/strong_memoize_spec.rb +++ b/spec/lib/gitlab/utils/strong_memoize_spec.rb @@ -49,4 +49,16 @@ describe Gitlab::Utils::StrongMemoize do end end end + + describe '#clear_memoization' do + let(:value) { 'mepmep' } + + it 'removes the instance variable' do + object.method_name + + object.clear_memoization(:method_name) + + expect(object.instance_variable_defined?(:@method_name)).to be(false) + end + end end diff --git a/spec/lib/gitlab/view/presenter/factory_spec.rb b/spec/lib/gitlab/view/presenter/factory_spec.rb index 70d2e22b48f..6120bafb2e3 100644 --- a/spec/lib/gitlab/view/presenter/factory_spec.rb +++ b/spec/lib/gitlab/view/presenter/factory_spec.rb @@ -27,5 +27,13 @@ describe Gitlab::View::Presenter::Factory do expect(presenter).to be_a(Ci::BuildPresenter) end + + it 'uses the presenter_class if given on #initialize' do + MyCustomPresenter = Class.new(described_class) + + presenter = described_class.new(build, presenter_class: MyCustomPresenter).fabricate! + + expect(presenter).to be_a(MyCustomPresenter) + end end end diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb index e1d71a9573b..4d0a3942996 100644 --- a/spec/mailers/notify_spec.rb +++ b/spec/mailers/notify_spec.rb @@ -342,6 +342,46 @@ describe Notify do end end + context 'for issue notes' do + let(:host) { Gitlab.config.gitlab.host } + + context 'in discussion' do + set(:first_note) { create(:discussion_note_on_issue) } + set(:second_note) { create(:discussion_note_on_issue, in_reply_to: first_note) } + set(:third_note) { create(:discussion_note_on_issue, in_reply_to: second_note) } + + subject { described_class.note_issue_email(recipient.id, third_note.id) } + + it 'has In-Reply-To header pointing to previous note in discussion' do + expect(subject.header['In-Reply-To'].message_ids).to eq(["note_#{second_note.id}@#{host}"]) + end + + it 'has References header including the notes and issue of the discussion' do + expect(subject.header['References'].message_ids).to include("issue_#{first_note.noteable.id}@#{host}", + "note_#{first_note.id}@#{host}", + "note_#{second_note.id}@#{host}") + end + + it 'has X-GitLab-Discussion-ID header' do + expect(subject.header['X-GitLab-Discussion-ID'].value).to eq(third_note.discussion.id) + end + end + + context 'individual issue comments' do + set(:note) { create(:note_on_issue) } + + subject { described_class.note_issue_email(recipient.id, note.id) } + + it 'has In-Reply-To header pointing to the issue' do + expect(subject.header['In-Reply-To'].message_ids).to eq(["issue_#{note.noteable.id}@#{host}"]) + end + + it 'has References header including the notes and issue of the discussion' do + expect(subject.header['References'].message_ids).to include("issue_#{note.noteable.id}@#{host}") + end + end + end + context 'for snippet notes' do let(:project_snippet) { create(:project_snippet, project: project) } let(:project_snippet_note) { create(:note_on_project_snippet, project: project, noteable: project_snippet) } diff --git a/spec/migrations/migrate_gcp_clusters_to_new_clusters_architectures_spec.rb b/spec/migrations/migrate_gcp_clusters_to_new_clusters_architectures_spec.rb index 05f281fffff..57ee2adaaff 100644 --- a/spec/migrations/migrate_gcp_clusters_to_new_clusters_architectures_spec.rb +++ b/spec/migrations/migrate_gcp_clusters_to_new_clusters_architectures_spec.rb @@ -2,9 +2,10 @@ require 'spec_helper' require Rails.root.join('db', 'post_migrate', '20171013104327_migrate_gcp_clusters_to_new_clusters_architectures.rb') describe MigrateGcpClustersToNewClustersArchitectures, :migration do - let(:project) { create(:project) } + let(:projects) { table(:projects) } + let(:project) { projects.create } let(:user) { create(:user) } - let(:service) { create(:kubernetes_service, project: project) } + let(:service) { create(:kubernetes_service, project_id: project.id) } context 'when cluster is being created' do let(:project_id) { project.id } @@ -56,8 +57,7 @@ describe MigrateGcpClustersToNewClustersArchitectures, :migration do expect(cluster.provider_type).to eq('gcp') expect(cluster.platform_type).to eq('kubernetes') - expect(cluster.project).to eq(project) - expect(project.clusters).to include(cluster) + expect(cluster.project_ids).to include(project.id) expect(cluster.provider_gcp.cluster).to eq(cluster) expect(cluster.provider_gcp.status).to eq(status) @@ -133,8 +133,7 @@ describe MigrateGcpClustersToNewClustersArchitectures, :migration do expect(cluster.provider_type).to eq('gcp') expect(cluster.platform_type).to eq('kubernetes') - expect(cluster.project).to eq(project) - expect(project.clusters).to include(cluster) + expect(cluster.project_ids).to include(project.id) expect(cluster.provider_gcp.cluster).to eq(cluster) expect(cluster.provider_gcp.status).to eq(status) diff --git a/spec/migrations/remove_assignee_id_from_issue_spec.rb b/spec/migrations/remove_assignee_id_from_issue_spec.rb new file mode 100644 index 00000000000..2c6f992d3ae --- /dev/null +++ b/spec/migrations/remove_assignee_id_from_issue_spec.rb @@ -0,0 +1,37 @@ +require 'spec_helper' +require Rails.root.join('db', 'post_migrate', '20170523073948_remove_assignee_id_from_issue.rb') + +describe RemoveAssigneeIdFromIssue, :migration do + let(:issues) { table(:issues) } + let(:issue_assignees) { table(:issue_assignees) } + let(:users) { table(:users) } + + let!(:user_1) { users.create(email: 'email1@example.com') } + let!(:user_2) { users.create(email: 'email2@example.com') } + let!(:user_3) { users.create(email: 'email3@example.com') } + + def create_issue(assignees:) + issues.create.tap do |issue| + assignees.each do |assignee| + issue_assignees.create(issue_id: issue.id, user_id: assignee.id) + end + end + end + + let!(:issue_single_assignee) { create_issue(assignees: [user_1]) } + let!(:issue_no_assignee) { create_issue(assignees: []) } + let!(:issue_multiple_assignees) { create_issue(assignees: [user_2, user_3]) } + + describe '#down' do + it 'sets the assignee_id to a random matching assignee from the assignees table' do + migrate! + disable_migrations_output { described_class.new.down } + + expect(issue_single_assignee.reload.assignee_id).to eq(user_1.id) + expect(issue_no_assignee.reload.assignee_id).to be_nil + expect(issue_multiple_assignees.reload.assignee_id).to eq(user_2.id).or(user_3.id) + + disable_migrations_output { described_class.new.up } + end + end +end diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb index 0b7e16cc33c..ef480e7a80a 100644 --- a/spec/models/application_setting_spec.rb +++ b/spec/models/application_setting_spec.rb @@ -115,9 +115,8 @@ describe ApplicationSetting do end context 'circuitbreaker settings' do - [:circuitbreaker_backoff_threshold, - :circuitbreaker_failure_count_threshold, - :circuitbreaker_failure_wait_time, + [:circuitbreaker_failure_count_threshold, + :circuitbreaker_check_interval, :circuitbreaker_failure_reset_time, :circuitbreaker_storage_timeout].each do |field| it "Validates #{field} as number" do @@ -126,16 +125,6 @@ describe ApplicationSetting do .is_greater_than_or_equal_to(0) end end - - it 'requires the `backoff_threshold` to be lower than the `failure_count_threshold`' do - setting.circuitbreaker_failure_count_threshold = 10 - setting.circuitbreaker_backoff_threshold = 15 - failure_message = "The circuitbreaker backoff threshold should be lower "\ - "than the failure count threshold" - - expect(setting).not_to be_valid - expect(setting.errors[:circuitbreaker_backoff_threshold]).to include(failure_message) - end end context 'repository storages' do diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index a6258676767..871e8b47650 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -132,11 +132,9 @@ describe Ci::Build do end describe '#artifacts?' do - context 'when new artifacts are used' do - let(:build) { create(:ci_build, :artifacts) } - - subject { build.artifacts? } + subject { build.artifacts? } + context 'when new artifacts are used' do context 'artifacts archive does not exist' do let(:build) { create(:ci_build) } @@ -144,25 +142,19 @@ describe Ci::Build do end context 'artifacts archive exists' do + let(:build) { create(:ci_build, :artifacts) } + it { is_expected.to be_truthy } context 'is expired' do - let!(:build) { create(:ci_build, :artifacts, :expired) } + let(:build) { create(:ci_build, :artifacts, :expired) } it { is_expected.to be_falsy } end - - context 'is not expired' do - it { is_expected.to be_truthy } - end end end context 'when legacy artifacts are used' do - let(:build) { create(:ci_build, :legacy_artifacts) } - - subject { build.artifacts? } - context 'artifacts archive does not exist' do let(:build) { create(:ci_build) } @@ -170,17 +162,15 @@ describe Ci::Build do end context 'artifacts archive exists' do + let(:build) { create(:ci_build, :legacy_artifacts) } + it { is_expected.to be_truthy } context 'is expired' do - let!(:build) { create(:ci_build, :legacy_artifacts, :expired) } + let(:build) { create(:ci_build, :legacy_artifacts, :expired) } it { is_expected.to be_falsy } end - - context 'is not expired' do - it { is_expected.to be_truthy } - end end end end @@ -1871,9 +1861,9 @@ describe Ci::Build do describe 'state transition: any => [:running]' do shared_examples 'validation is active' do context 'when depended job has not been completed yet' do - let!(:pre_stage_job) { create(:ci_build, :running, pipeline: pipeline, name: 'test', stage_idx: 0) } + let!(:pre_stage_job) { create(:ci_build, :manual, pipeline: pipeline, name: 'test', stage_idx: 0) } - it { expect { job.run! }.to raise_error(Ci::Build::MissingDependenciesError) } + it { expect { job.run! }.not_to raise_error(Ci::Build::MissingDependenciesError) } end context 'when artifacts of depended job has been expired' do @@ -1895,11 +1885,10 @@ describe Ci::Build do shared_examples 'validation is not active' do context 'when depended job has not been completed yet' do - let!(:pre_stage_job) { create(:ci_build, :running, pipeline: pipeline, name: 'test', stage_idx: 0) } + let!(:pre_stage_job) { create(:ci_build, :manual, pipeline: pipeline, name: 'test', stage_idx: 0) } it { expect { job.run! }.not_to raise_error } end - context 'when artifacts of depended job has been expired' do let!(:pre_stage_job) { create(:ci_build, :success, :expired, pipeline: pipeline, name: 'test', stage_idx: 0) } diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index bb89e093890..a1f63a2534b 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -116,7 +116,7 @@ describe Ci::Pipeline, :mailer do end it "calculates average when there is one build without coverage" do - FactoryGirl.create(:ci_build, pipeline: pipeline) + FactoryBot.create(:ci_build, pipeline: pipeline) expect(pipeline.coverage).to be_nil end end @@ -435,7 +435,7 @@ describe Ci::Pipeline, :mailer do describe 'merge request metrics' do let(:project) { create(:project, :repository) } - let(:pipeline) { FactoryGirl.create(:ci_empty_pipeline, status: 'created', project: project, ref: 'master', sha: project.repository.commit('master').id) } + let(:pipeline) { FactoryBot.create(:ci_empty_pipeline, status: 'created', project: project, ref: 'master', sha: project.repository.commit('master').id) } let!(:merge_request) { create(:merge_request, source_project: project, source_branch: pipeline.ref) } before do @@ -1530,4 +1530,16 @@ describe Ci::Pipeline, :mailer do expect(query_count).to eq(1) end end + + describe '#total_size' do + let!(:build_job1) { create(:ci_build, pipeline: pipeline, stage_idx: 0) } + let!(:build_job2) { create(:ci_build, pipeline: pipeline, stage_idx: 0) } + let!(:test_job_failed_and_retried) { create(:ci_build, :failed, :retried, pipeline: pipeline, stage_idx: 1) } + let!(:second_test_job) { create(:ci_build, pipeline: pipeline, stage_idx: 1) } + let!(:deploy_job) { create(:ci_build, pipeline: pipeline, stage_idx: 2) } + + it 'returns all jobs (including failed and retried)' do + expect(pipeline.total_size).to eq(5) + end + end end diff --git a/spec/models/ci/runner_spec.rb b/spec/models/ci/runner_spec.rb index a93e7e233a8..b2b64e6ff48 100644 --- a/spec/models/ci/runner_spec.rb +++ b/spec/models/ci/runner_spec.rb @@ -51,24 +51,24 @@ describe Ci::Runner do describe '#display_name' do it 'returns the description if it has a value' do - runner = FactoryGirl.build(:ci_runner, description: 'Linux/Ruby-1.9.3-p448') + runner = FactoryBot.build(:ci_runner, description: 'Linux/Ruby-1.9.3-p448') expect(runner.display_name).to eq 'Linux/Ruby-1.9.3-p448' end it 'returns the token if it does not have a description' do - runner = FactoryGirl.create(:ci_runner) + runner = FactoryBot.create(:ci_runner) expect(runner.display_name).to eq runner.description end it 'returns the token if the description is an empty string' do - runner = FactoryGirl.build(:ci_runner, description: '', token: 'token') + runner = FactoryBot.build(:ci_runner, description: '', token: 'token') expect(runner.display_name).to eq runner.token end end describe '#assign_to' do - let!(:project) { FactoryGirl.create :project } - let!(:shared_runner) { FactoryGirl.create(:ci_runner, :shared) } + let!(:project) { FactoryBot.create :project } + let!(:shared_runner) { FactoryBot.create(:ci_runner, :shared) } before do shared_runner.assign_to(project) @@ -83,15 +83,15 @@ describe Ci::Runner do subject { described_class.online } before do - @runner1 = FactoryGirl.create(:ci_runner, :shared, contacted_at: 1.year.ago) - @runner2 = FactoryGirl.create(:ci_runner, :shared, contacted_at: 1.second.ago) + @runner1 = FactoryBot.create(:ci_runner, :shared, contacted_at: 1.year.ago) + @runner2 = FactoryBot.create(:ci_runner, :shared, contacted_at: 1.second.ago) end it { is_expected.to eq([@runner2])} end describe '#online?' do - let(:runner) { FactoryGirl.create(:ci_runner, :shared) } + let(:runner) { FactoryBot.create(:ci_runner, :shared) } subject { runner.online? } @@ -268,7 +268,7 @@ describe Ci::Runner do end describe '#status' do - let(:runner) { FactoryGirl.create(:ci_runner, :shared, contacted_at: 1.second.ago) } + let(:runner) { FactoryBot.create(:ci_runner, :shared, contacted_at: 1.second.ago) } subject { runner.status } @@ -442,9 +442,9 @@ describe Ci::Runner do describe "belongs_to_one_project?" do it "returns false if there are two projects runner assigned to" do - runner = FactoryGirl.create(:ci_runner) - project = FactoryGirl.create(:project) - project1 = FactoryGirl.create(:project) + runner = FactoryBot.create(:ci_runner) + project = FactoryBot.create(:project) + project1 = FactoryBot.create(:project) project.runners << runner project1.runners << runner @@ -452,8 +452,8 @@ describe Ci::Runner do end it "returns true" do - runner = FactoryGirl.create(:ci_runner) - project = FactoryGirl.create(:project) + runner = FactoryBot.create(:ci_runner) + project = FactoryBot.create(:project) project.runners << runner expect(runner.belongs_to_one_project?).to be_truthy diff --git a/spec/models/concerns/cache_markdown_field_spec.rb b/spec/models/concerns/cache_markdown_field_spec.rb index 129dfa07f15..3c7f578975b 100644 --- a/spec/models/concerns/cache_markdown_field_spec.rb +++ b/spec/models/concerns/cache_markdown_field_spec.rb @@ -102,6 +102,26 @@ describe CacheMarkdownField do it { expect(thing.cached_markdown_version).to eq(CacheMarkdownField::CACHE_VERSION) } end + context 'when a markdown field is set repeatedly to an empty string' do + it do + expect(thing).to receive(:refresh_markdown_cache).once + thing.foo = '' + thing.save + thing.foo = '' + thing.save + end + end + + context 'when a markdown field is set repeatedly to a string which renders as empty html' do + it do + expect(thing).to receive(:refresh_markdown_cache).once + thing.foo = '[//]: # (This is also a comment.)' + thing.save + thing.foo = '[//]: # (This is also a comment.)' + thing.save + end + end + context 'a non-markdown field changed' do before do thing.bar = 'OK' diff --git a/spec/models/deployment_spec.rb b/spec/models/deployment_spec.rb index c5708e70ef9..ba8aa13d5ad 100644 --- a/spec/models/deployment_spec.rb +++ b/spec/models/deployment_spec.rb @@ -126,7 +126,7 @@ describe Deployment do subject { deployment.stop_action } context 'when no other actions' do - let(:deployment) { FactoryGirl.build(:deployment, deployable: build) } + let(:deployment) { FactoryBot.build(:deployment, deployable: build) } it { is_expected.to be_nil } end @@ -135,13 +135,13 @@ describe Deployment do let!(:close_action) { create(:ci_build, :manual, pipeline: build.pipeline, name: 'close_app') } context 'when matching action is defined' do - let(:deployment) { FactoryGirl.build(:deployment, deployable: build, on_stop: 'close_other_app') } + let(:deployment) { FactoryBot.build(:deployment, deployable: build, on_stop: 'close_other_app') } it { is_expected.to be_nil } end context 'when no matching action is defined' do - let(:deployment) { FactoryGirl.build(:deployment, deployable: build, on_stop: 'close_app') } + let(:deployment) { FactoryBot.build(:deployment, deployable: build, on_stop: 'close_app') } it { is_expected.to eq(close_action) } end @@ -159,7 +159,7 @@ describe Deployment do context 'when matching action is defined' do let(:build) { create(:ci_build) } - let(:deployment) { FactoryGirl.build(:deployment, deployable: build, on_stop: 'close_app') } + let(:deployment) { FactoryBot.build(:deployment, deployable: build, on_stop: 'close_app') } let!(:close_action) { create(:ci_build, :manual, pipeline: build.pipeline, name: 'close_app') } it { is_expected.to be_truthy } diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 30a5a3bbff7..bb63abd167b 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -124,6 +124,7 @@ describe MergeRequest do context 'when the target branch does not exist' do before do project.repository.rm_branch(subject.author, subject.target_branch) + subject.clear_memoized_shas end it 'returns nil' do @@ -600,30 +601,30 @@ describe MergeRequest do end describe '#can_remove_source_branch?' do - let(:user) { create(:user) } - let(:user2) { create(:user) } + set(:user) { create(:user) } + set(:merge_request) { create(:merge_request, :simple) } - before do - subject.source_project.team << [user, :master] + subject { merge_request } - subject.source_branch = "feature" - subject.target_branch = "master" - subject.save! + before do + subject.source_project.add_master(user) end it "can't be removed when its a protected branch" do allow(ProtectedBranch).to receive(:protected?).and_return(true) + expect(subject.can_remove_source_branch?(user)).to be_falsey end it "can't remove a root ref" do - subject.source_branch = "master" - subject.target_branch = "feature" + subject.update(source_branch: 'master', target_branch: 'feature') expect(subject.can_remove_source_branch?(user)).to be_falsey end it "is unable to remove the source branch for a project the user cannot push to" do + user2 = create(:user) + expect(subject.can_remove_source_branch?(user2)).to be_falsey end @@ -634,6 +635,7 @@ describe MergeRequest do end it "cannot be removed if the last commit is not also the head of the source branch" do + subject.clear_memoized_shas subject.source_branch = "lfs" expect(subject.can_remove_source_branch?(user)).to be_falsey @@ -733,7 +735,7 @@ describe MergeRequest do before do project.repository.raw_repository.delete_branch(subject.target_branch) - subject.reload + subject.clear_memoized_shas end it 'does not crash' do @@ -1404,6 +1406,16 @@ describe MergeRequest do subject.reload_diff end + + context 'when using the after_update hook to update' do + context 'when the branches are updated' do + it 'uses the new heads to generate the diff' do + expect { subject.update!(source_branch: subject.target_branch, target_branch: subject.source_branch) } + .to change { subject.merge_request_diff.start_commit_sha } + .and change { subject.merge_request_diff.head_commit_sha } + end + end + end end describe '#update_diff_discussion_positions' do @@ -1468,6 +1480,7 @@ describe MergeRequest do context 'when the target branch does not exist' do before do subject.project.repository.rm_branch(subject.author, subject.target_branch) + subject.clear_memoized_shas end it 'returns nil' do @@ -1855,4 +1868,20 @@ describe MergeRequest do it_behaves_like 'throttled touch' do subject { create(:merge_request, updated_at: 1.hour.ago) } end + + context 'state machine transitions' do + describe '#unlock_mr' do + subject { create(:merge_request, state: 'locked', merge_jid: 123) } + + it 'updates merge request head pipeline and sets merge_jid to nil' do + pipeline = create(:ci_empty_pipeline, project: subject.project, ref: subject.source_branch, sha: subject.source_branch_sha) + + subject.unlock_mr + + subject.reload + expect(subject.head_pipeline).to eq(pipeline) + expect(subject.merge_jid).to be_nil + end + end + end end diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb index 3817f20bfe7..b7c6286fd83 100644 --- a/spec/models/namespace_spec.rb +++ b/spec/models/namespace_spec.rb @@ -559,4 +559,34 @@ describe Namespace do end end end + + describe "#allowed_path_by_redirects" do + let(:namespace1) { create(:namespace, path: 'foo') } + + context "when the path has been taken before" do + before do + namespace1.path = 'bar' + namespace1.save! + end + + it 'should be invalid' do + namespace2 = build(:group, path: 'foo') + expect(namespace2).to be_invalid + end + + it 'should return an error on path' do + namespace2 = build(:group, path: 'foo') + namespace2.valid? + expect(namespace2.errors.messages[:path].first).to eq('foo has been taken before. Please use another one') + end + end + + context "when the path has not been taken before" do + it 'should be valid' do + expect(RedirectRoute.count).to eq(0) + namespace = build(:namespace) + expect(namespace).to be_valid + end + end + end end diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb index e1a0c55b6a6..cefbf60b28c 100644 --- a/spec/models/note_spec.rb +++ b/spec/models/note_spec.rb @@ -756,6 +756,28 @@ describe Note do end end + describe '#references' do + context 'when part of a discussion' do + it 'references all earlier notes in the discussion' do + first_note = create(:discussion_note_on_issue) + second_note = create(:discussion_note_on_issue, in_reply_to: first_note) + third_note = create(:discussion_note_on_issue, in_reply_to: second_note) + create(:discussion_note_on_issue, in_reply_to: third_note) + + expect(third_note.references).to eq([first_note.noteable, first_note, second_note]) + end + end + + context 'when not part of a discussion' do + subject { create(:note) } + let(:note) { create(:note, in_reply_to: subject) } + + it 'returns the noteable' do + expect(note.references).to eq([note.noteable]) + end + end + end + describe 'expiring ETag cache' do let(:note) { build(:note_on_issue) } diff --git a/spec/models/personal_access_token_spec.rb b/spec/models/personal_access_token_spec.rb index 01440b15674..2bb1c49b740 100644 --- a/spec/models/personal_access_token_spec.rb +++ b/spec/models/personal_access_token_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe PersonalAccessToken do + subject { described_class } + describe '.build' do let(:personal_access_token) { build(:personal_access_token) } let(:invalid_personal_access_token) { build(:personal_access_token, :invalid) } @@ -45,6 +47,29 @@ describe PersonalAccessToken do end end + describe 'Redis storage' do + let(:user_id) { 123 } + let(:token) { 'abc000foo' } + + before do + subject.redis_store!(user_id, token) + end + + it 'returns stored data' do + expect(subject.redis_getdel(user_id)).to eq(token) + end + + context 'after deletion' do + before do + expect(subject.redis_getdel(user_id)).to eq(token) + end + + it 'token is removed' do + expect(subject.redis_getdel(user_id)).to be_nil + end + end + end + context "validations" do let(:personal_access_token) { build(:personal_access_token) } diff --git a/spec/models/project_services/jira_service_spec.rb b/spec/models/project_services/jira_service_spec.rb index ad22fb2a386..c9b3c6cf602 100644 --- a/spec/models/project_services/jira_service_spec.rb +++ b/spec/models/project_services/jira_service_spec.rb @@ -395,6 +395,26 @@ describe JiraService do end end + describe 'additional cookies' do + let(:project) { create(:project) } + + context 'provides additional cookies to allow basic auth with oracle webgate' do + before do + @service = project.create_jira_service( + active: true, properties: { url: 'http://jira.com' }) + end + + after do + @service.destroy! + end + + it 'is initialized' do + expect(@service.options[:use_cookies]).to eq(true) + expect(@service.options[:additional_cookies]).to eq(["OBBasicAuth=fromDialog"]) + end + end + end + describe 'project and issue urls' do let(:project) { create(:project) } diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index f4699fd243d..f805f2dcddb 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -289,12 +289,12 @@ describe Project do describe 'project token' do it 'sets an random token if none provided' do - project = FactoryGirl.create :project, runners_token: '' + project = FactoryBot.create :project, runners_token: '' expect(project.runners_token).not_to eq('') end it 'does not set an random token if one provided' do - project = FactoryGirl.create :project, runners_token: 'my-token' + project = FactoryBot.create :project, runners_token: 'my-token' expect(project.runners_token).to eq('my-token') end end @@ -1863,10 +1863,11 @@ describe Project do project.change_head(project.default_branch) end - it 'creates the new reference with rugged' do - expect(project.repository.rugged.references).to receive(:create).with('HEAD', + it 'creates the new reference' do + expect(project.repository.raw_repository).to receive(:write_ref).with('HEAD', "refs/heads/#{project.default_branch}", force: true) + project.change_head(project.default_branch) end diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 82ed1ecee33..799d99c0369 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -29,7 +29,9 @@ describe Repository do def expect_to_raise_storage_error expect { yield }.to raise_error do |exception| storage_exceptions = [Gitlab::Git::Storage::Inaccessible, Gitlab::Git::CommandError, GRPC::Unavailable] - expect(exception.class).to be_in(storage_exceptions) + known_exception = storage_exceptions.select { |e| exception.is_a?(e) } + + expect(known_exception).not_to be_nil end end @@ -57,12 +59,18 @@ describe Repository do end describe 'tags_sorted_by' do - context 'name' do - subject { repository.tags_sorted_by('name').map(&:name) } + context 'name_desc' do + subject { repository.tags_sorted_by('name_desc').map(&:name) } it { is_expected.to eq(['v1.1.0', 'v1.0.0']) } end + context 'name_asc' do + subject { repository.tags_sorted_by('name_asc').map(&:name) } + + it { is_expected.to eq(['v1.0.0', 'v1.1.0']) } + end + context 'updated' do let(:tag_a) { repository.find_tag('v1.0.0') } let(:tag_b) { repository.find_tag('v1.1.0') } @@ -634,9 +642,7 @@ describe Repository do end describe '#fetch_ref' do - # Setting the var here, sidesteps the stub that makes gitaly raise an error - # before the actual test call - set(:broken_repository) { create(:project, :broken_storage).repository } + let(:broken_repository) { create(:project, :broken_storage).repository } describe 'when storage is broken', :broken_storage do it 'should raise a storage error' do @@ -1007,7 +1013,7 @@ describe Repository do it 'runs without errors' do # old_rev is an ancestor of new_rev - expect(repository.rugged.merge_base(old_rev, new_rev)).to eq(old_rev) + expect(repository.merge_base(old_rev, new_rev)).to eq(old_rev) # old_rev is not a direct ancestor (parent) of new_rev expect(repository.rugged.lookup(new_rev).parent_ids).not_to include(old_rev) @@ -1029,7 +1035,7 @@ describe Repository do it 'raises an exception' do # The 'master' branch is NOT an ancestor of new_rev. - expect(repository.rugged.merge_base(old_rev, new_rev)).not_to eq(old_rev) + expect(repository.merge_base(old_rev, new_rev)).not_to eq(old_rev) # Updating 'master' to new_rev would lose the commits on 'master' that # are not contained in new_rev. This should not be allowed. @@ -1916,6 +1922,23 @@ describe Repository do File.delete(path) end + + it "attempting to call keep_around when exists a lock does not fail" do + ref = repository.send(:keep_around_ref_name, sample_commit.id) + path = File.join(repository.path, ref) + lock_path = "#{path}.lock" + + FileUtils.mkdir_p(File.dirname(path)) + File.open(lock_path, 'w') { |f| f.write('') } + + begin + expect { repository.keep_around(sample_commit.id) }.not_to raise_error(Gitlab::Git::Repository::GitError) + + expect(File.exist?(lock_path)).to be_falsey + ensure + File.delete(path) + end + end end describe '#update_ref' do @@ -2343,4 +2366,111 @@ describe Repository do end end end + + describe '#contributors' do + let(:author_a) { build(:author, email: 'tiagonbotelho@hotmail.com', name: 'tiagonbotelho') } + let(:author_b) { build(:author, email: 'gitlab@winniehell.de', name: 'Winnie') } + let(:author_c) { build(:author, email: 'douwe@gitlab.com', name: 'Douwe Maan') } + let(:stubbed_commits) do + [build(:commit, author: author_a), + build(:commit, author: author_a), + build(:commit, author: author_b), + build(:commit, author: author_c), + build(:commit, author: author_c), + build(:commit, author: author_c)] + end + let(:order_by) { nil } + let(:sort) { nil } + + before do + allow(repository).to receive(:commits).with(nil, limit: 2000, offset: 0, skip_merges: true).and_return(stubbed_commits) + end + + subject { repository.contributors(order_by: order_by, sort: sort) } + + def expect_contributors(*contributors) + expect(subject.map(&:email)).to eq(contributors.map(&:email)) + end + + it 'returns the array of Gitlab::Contributor for the repository' do + expect_contributors(author_a, author_b, author_c) + end + + context 'order_by email' do + let(:order_by) { 'email' } + + context 'asc' do + let(:sort) { 'asc' } + + it 'returns all the contributors ordered by email asc case insensitive' do + expect_contributors(author_c, author_b, author_a) + end + end + + context 'desc' do + let(:sort) { 'desc' } + + it 'returns all the contributors ordered by email desc case insensitive' do + expect_contributors(author_a, author_b, author_c) + end + end + end + + context 'order_by name' do + let(:order_by) { 'name' } + + context 'asc' do + let(:sort) { 'asc' } + + it 'returns all the contributors ordered by name asc case insensitive' do + expect_contributors(author_c, author_a, author_b) + end + end + + context 'desc' do + let(:sort) { 'desc' } + + it 'returns all the contributors ordered by name desc case insensitive' do + expect_contributors(author_b, author_a, author_c) + end + end + end + + context 'order_by commits' do + let(:order_by) { 'commits' } + + context 'asc' do + let(:sort) { 'asc' } + + it 'returns all the contributors ordered by commits asc' do + expect_contributors(author_b, author_a, author_c) + end + end + + context 'desc' do + let(:sort) { 'desc' } + + it 'returns all the contributors ordered by commits desc' do + expect_contributors(author_c, author_a, author_b) + end + end + end + + context 'invalid ordering' do + let(:order_by) { 'unknown' } + + it 'returns the contributors unsorted' do + expect_contributors(author_a, author_b, author_c) + end + end + + context 'invalid sorting' do + let(:order_by) { 'name' } + let(:sort) { 'unknown' } + + it 'returns the contributors unsorted' do + expect_contributors(author_a, author_b, author_c) + end + end + end end diff --git a/spec/models/route_spec.rb b/spec/models/route_spec.rb index fece370c03f..ddad6862a63 100644 --- a/spec/models/route_spec.rb +++ b/spec/models/route_spec.rb @@ -87,6 +87,7 @@ describe Route do end context 'when conflicting redirects exist' do + let(:route) { create(:project).route } let!(:conflicting_redirect1) { route.create_redirect('bar/test') } let!(:conflicting_redirect2) { route.create_redirect('bar/test/foo') } let!(:conflicting_redirect3) { route.create_redirect('gitlab-org') } @@ -141,11 +142,50 @@ describe Route do expect(redirect_route.source).to eq(route.source) expect(redirect_route.path).to eq('foo') end + + context 'when the source is a Project' do + it 'creates a temporal RedirectRoute' do + project = create(:project) + route = project.route + redirect_route = route.create_redirect('foo') + expect(redirect_route.permanent?).to be_falsy + end + end + + context 'when the source is not a project' do + it 'creates a permanent RedirectRoute' do + redirect_route = route.create_redirect('foo', permanent: true) + expect(redirect_route.permanent?).to be_truthy + end + end end describe '#delete_conflicting_redirects' do + context 'with permanent redirect' do + it 'does not delete the redirect' do + route.create_redirect("#{route.path}/foo", permanent: true) + + expect do + route.delete_conflicting_redirects + end.not_to change { RedirectRoute.count } + end + end + + context 'with temporal redirect' do + let(:route) { create(:project).route } + + it 'deletes the redirect' do + route.create_redirect("#{route.path}/foo") + + expect do + route.delete_conflicting_redirects + end.to change { RedirectRoute.count }.by(-1) + end + end + context 'when a redirect route with the same path exists' do context 'when the redirect route has matching case' do + let(:route) { create(:project).route } let!(:redirect1) { route.create_redirect(route.path) } it 'deletes the redirect' do @@ -169,6 +209,7 @@ describe Route do end context 'when the redirect route is differently cased' do + let(:route) { create(:project).route } let!(:redirect1) { route.create_redirect(route.path.upcase) } it 'deletes the redirect' do @@ -185,7 +226,32 @@ describe Route do expect(route.conflicting_redirects).to be_an(ActiveRecord::Relation) end + context 'with permanent redirects' do + it 'does not return anything' do + route.create_redirect("#{route.path}/foo", permanent: true) + route.create_redirect("#{route.path}/foo/bar", permanent: true) + route.create_redirect("#{route.path}/baz/quz", permanent: true) + + expect(route.conflicting_redirects).to be_empty + end + end + + context 'with temporal redirects' do + let(:route) { create(:project).route } + + it 'returns the redirect routes' do + route = create(:project).route + redirect1 = route.create_redirect("#{route.path}/foo") + redirect2 = route.create_redirect("#{route.path}/foo/bar") + redirect3 = route.create_redirect("#{route.path}/baz/quz") + + expect(route.conflicting_redirects).to match_array([redirect1, redirect2, redirect3]) + end + end + context 'when a redirect route with the same path exists' do + let(:route) { create(:project).route } + context 'when the redirect route has matching case' do let!(:redirect1) { route.create_redirect(route.path) } @@ -214,4 +280,42 @@ describe Route do end end end + + describe "#conflicting_redirect_exists?" do + context 'when a conflicting redirect exists' do + let(:group1) { create(:group, path: 'foo') } + let(:group2) { create(:group, path: 'baz') } + + it 'should not be saved' do + group1.path = 'bar' + group1.save + + group2.path = 'foo' + + expect(group2.save).to be_falsy + end + + it 'should return an error on path' do + group1.path = 'bar' + group1.save + + group2.path = 'foo' + group2.valid? + expect(group2.errors["route.path"].first).to eq('foo has been taken before. Please use another one') + end + end + + context 'when a conflicting redirect does not exist' do + let(:project1) { create(:project, path: 'foo') } + let(:project2) { create(:project, path: 'baz') } + + it 'should be saved' do + project1.path = 'bar' + project1.save + + project2.path = 'foo' + expect(project2.save).to be_truthy + end + end + end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 03c96a8f5aa..4687d9dfa00 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -913,11 +913,11 @@ describe User do describe 'email matching' do it 'returns users with a matching Email' do - expect(described_class.search(user.email)).to eq([user, user2]) + expect(described_class.search(user.email)).to eq([user]) end - it 'returns users with a partially matching Email' do - expect(described_class.search(user.email[0..2])).to eq([user, user2]) + it 'does not return users with a partially matching Email' do + expect(described_class.search(user.email[0..2])).not_to include(user, user2) end it 'returns users with a matching Email regardless of the casing' do @@ -973,8 +973,8 @@ describe User do expect(search_with_secondary_emails(user.email)).to eq([user]) end - it 'returns users with a partially matching email' do - expect(search_with_secondary_emails(user.email[0..2])).to eq([user]) + it 'does not return users with a partially matching email' do + expect(search_with_secondary_emails(user.email[0..2])).not_to include([user]) end it 'returns users with a matching email regardless of the casing' do @@ -997,29 +997,8 @@ describe User do expect(search_with_secondary_emails(email.email)).to eq([email.user]) end - it 'returns users with a matching part of secondary email' do - expect(search_with_secondary_emails(email.email[1..4])).to eq([email.user]) - end - - it 'return users with a matching part of secondary email regardless of case' do - expect(search_with_secondary_emails(email.email[1..4].upcase)).to eq([email.user]) - expect(search_with_secondary_emails(email.email[1..4].downcase)).to eq([email.user]) - expect(search_with_secondary_emails(email.email[1..4].capitalize)).to eq([email.user]) - end - - it 'returns multiple users with matching secondary emails' do - email1 = create(:email, email: '1_testemail@example.com') - email2 = create(:email, email: '2_testemail@example.com') - email3 = create(:email, email: 'other@email.com') - email3.user.update_attributes!(email: 'another@mail.com') - - expect( - search_with_secondary_emails('testemail@example.com').map(&:id) - ).to include(email1.user.id, email2.user.id) - - expect( - search_with_secondary_emails('testemail@example.com').map(&:id) - ).not_to include(email3.user.id) + it 'does not return users with a matching part of secondary email' do + expect(search_with_secondary_emails(email.email[1..4])).not_to include([email.user]) end end @@ -2592,4 +2571,28 @@ describe User do include_examples 'max member access for groups' end end + + describe "#username_previously_taken?" do + let(:user1) { create(:user, username: 'foo') } + + context 'when the username has been taken before' do + before do + user1.username = 'bar' + user1.save! + end + + it 'should raise an ActiveRecord::RecordInvalid exception' do + user2 = build(:user, username: 'foo') + expect { user2.save! }.to raise_error(ActiveRecord::RecordInvalid, /Path foo has been taken before/) + end + end + + context 'when the username has not been taken before' do + it 'should be valid' do + expect(RedirectRoute.count).to eq(0) + user2 = build(:user, username: 'baz') + expect(user2).to be_valid + end + end + end end diff --git a/spec/presenters/group_member_presenter_spec.rb b/spec/presenters/group_member_presenter_spec.rb new file mode 100644 index 00000000000..c00e41725d9 --- /dev/null +++ b/spec/presenters/group_member_presenter_spec.rb @@ -0,0 +1,138 @@ +require 'spec_helper' + +describe GroupMemberPresenter do + let(:user) { double(:user) } + let(:group) { double(:group) } + let(:group_member) { double(:group_member, source: group) } + let(:presenter) { described_class.new(group_member, current_user: user) } + + describe '#can_resend_invite?' do + context 'when group_member is invited' do + before do + expect(group_member).to receive(:invite?).and_return(true) + end + + context 'and user can admin_group_member' do + before do + allow(presenter).to receive(:can?).with(user, :admin_group_member, group).and_return(true) + end + + it { expect(presenter.can_resend_invite?).to eq(true) } + end + + context 'and user cannot admin_group_member' do + before do + allow(presenter).to receive(:can?).with(user, :admin_group_member, group).and_return(false) + end + + it { expect(presenter.can_resend_invite?).to eq(false) } + end + end + + context 'when group_member is not invited' do + before do + expect(group_member).to receive(:invite?).and_return(false) + end + + context 'and user can admin_group_member' do + before do + allow(presenter).to receive(:can?).with(user, :admin_group_member, group).and_return(true) + end + + it { expect(presenter.can_resend_invite?).to eq(false) } + end + + context 'and user cannot admin_group_member' do + before do + allow(presenter).to receive(:can?).with(user, :admin_group_member, group).and_return(false) + end + + it { expect(presenter.can_resend_invite?).to eq(false) } + end + end + end + + describe '#can_update?' do + context 'when user can update_group_member' do + before do + allow(presenter).to receive(:can?).with(user, :update_group_member, presenter).and_return(true) + end + + it { expect(presenter.can_update?).to eq(true) } + end + + context 'when user cannot update_group_member' do + before do + allow(presenter).to receive(:can?).with(user, :update_group_member, presenter).and_return(false) + allow(presenter).to receive(:can?).with(user, :override_group_member, presenter).and_return(false) + end + + it { expect(presenter.can_update?).to eq(false) } + end + end + + describe '#can_remove?' do + context 'when user can destroy_group_member' do + before do + allow(presenter).to receive(:can?).with(user, :destroy_group_member, presenter).and_return(true) + end + + it { expect(presenter.can_remove?).to eq(true) } + end + + context 'when user cannot destroy_group_member' do + before do + allow(presenter).to receive(:can?).with(user, :destroy_group_member, presenter).and_return(false) + end + + it { expect(presenter.can_remove?).to eq(false) } + end + end + + describe '#can_approve?' do + context 'when group_member has request an invite' do + before do + expect(group_member).to receive(:request?).and_return(true) + end + + context 'when user can update_group_member' do + before do + allow(presenter).to receive(:can?).with(user, :update_group_member, presenter).and_return(true) + end + + it { expect(presenter.can_approve?).to eq(true) } + end + + context 'when user cannot update_group_member' do + before do + allow(presenter).to receive(:can?).with(user, :update_group_member, presenter).and_return(false) + allow(presenter).to receive(:can?).with(user, :override_group_member, presenter).and_return(false) + end + + it { expect(presenter.can_approve?).to eq(false) } + end + end + + context 'when group_member did not request an invite' do + before do + expect(group_member).to receive(:request?).and_return(false) + end + + context 'when user can update_group_member' do + before do + allow(presenter).to receive(:can?).with(user, :update_group_member, presenter).and_return(true) + end + + it { expect(presenter.can_approve?).to eq(false) } + end + + context 'when user cannot update_group_member' do + before do + allow(presenter).to receive(:can?).with(user, :update_group_member, presenter).and_return(false) + end + + it { expect(presenter.can_approve?).to eq(false) } + end + end + end +end diff --git a/spec/presenters/project_member_presenter_spec.rb b/spec/presenters/project_member_presenter_spec.rb new file mode 100644 index 00000000000..83db5c56cdf --- /dev/null +++ b/spec/presenters/project_member_presenter_spec.rb @@ -0,0 +1,138 @@ +require 'spec_helper' + +describe ProjectMemberPresenter do + let(:user) { double(:user) } + let(:project) { double(:project) } + let(:project_member) { double(:project_member, source: project) } + let(:presenter) { described_class.new(project_member, current_user: user) } + + describe '#can_resend_invite?' do + context 'when project_member is invited' do + before do + expect(project_member).to receive(:invite?).and_return(true) + end + + context 'and user can admin_project_member' do + before do + allow(presenter).to receive(:can?).with(user, :admin_project_member, project).and_return(true) + end + + it { expect(presenter.can_resend_invite?).to eq(true) } + end + + context 'and user cannot admin_project_member' do + before do + allow(presenter).to receive(:can?).with(user, :admin_project_member, project).and_return(false) + end + + it { expect(presenter.can_resend_invite?).to eq(false) } + end + end + + context 'when project_member is not invited' do + before do + expect(project_member).to receive(:invite?).and_return(false) + end + + context 'and user can admin_project_member' do + before do + allow(presenter).to receive(:can?).with(user, :admin_project_member, project).and_return(true) + end + + it { expect(presenter.can_resend_invite?).to eq(false) } + end + + context 'and user cannot admin_project_member' do + before do + allow(presenter).to receive(:can?).with(user, :admin_project_member, project).and_return(false) + end + + it { expect(presenter.can_resend_invite?).to eq(false) } + end + end + end + + describe '#can_update?' do + context 'when user can update_project_member' do + before do + allow(presenter).to receive(:can?).with(user, :update_project_member, presenter).and_return(true) + end + + it { expect(presenter.can_update?).to eq(true) } + end + + context 'when user cannot update_project_member' do + before do + allow(presenter).to receive(:can?).with(user, :update_project_member, presenter).and_return(false) + allow(presenter).to receive(:can?).with(user, :override_project_member, presenter).and_return(false) + end + + it { expect(presenter.can_update?).to eq(false) } + end + end + + describe '#can_remove?' do + context 'when user can destroy_project_member' do + before do + allow(presenter).to receive(:can?).with(user, :destroy_project_member, presenter).and_return(true) + end + + it { expect(presenter.can_remove?).to eq(true) } + end + + context 'when user cannot destroy_project_member' do + before do + allow(presenter).to receive(:can?).with(user, :destroy_project_member, presenter).and_return(false) + end + + it { expect(presenter.can_remove?).to eq(false) } + end + end + + describe '#can_approve?' do + context 'when project_member has request an invite' do + before do + expect(project_member).to receive(:request?).and_return(true) + end + + context 'and user can update_project_member' do + before do + allow(presenter).to receive(:can?).with(user, :update_project_member, presenter).and_return(true) + end + + it { expect(presenter.can_approve?).to eq(true) } + end + + context 'and user cannot update_project_member' do + before do + allow(presenter).to receive(:can?).with(user, :update_project_member, presenter).and_return(false) + allow(presenter).to receive(:can?).with(user, :override_project_member, presenter).and_return(false) + end + + it { expect(presenter.can_approve?).to eq(false) } + end + end + + context 'when project_member did not request an invite' do + before do + expect(project_member).to receive(:request?).and_return(false) + end + + context 'and user can update_project_member' do + before do + allow(presenter).to receive(:can?).with(user, :update_project_member, presenter).and_return(true) + end + + it { expect(presenter.can_approve?).to eq(false) } + end + + context 'and user cannot update_project_member' do + before do + allow(presenter).to receive(:can?).with(user, :update_project_member, presenter).and_return(false) + end + + it { expect(presenter.can_approve?).to eq(false) } + end + end + end +end diff --git a/spec/requests/api/circuit_breakers_spec.rb b/spec/requests/api/circuit_breakers_spec.rb index 3b858c40fd6..fe76f057115 100644 --- a/spec/requests/api/circuit_breakers_spec.rb +++ b/spec/requests/api/circuit_breakers_spec.rb @@ -47,7 +47,7 @@ describe API::CircuitBreakers do describe 'DELETE circuit_breakers/repository_storage' do it 'clears all circuit_breakers' do - expect(Gitlab::Git::Storage::CircuitBreaker).to receive(:reset_all!) + expect(Gitlab::Git::Storage::FailureInfo).to receive(:reset_all!) delete api('/circuit_breakers/repository_storage', admin) diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb index 554723d6b1e..6330c140246 100644 --- a/spec/requests/api/groups_spec.rb +++ b/spec/requests/api/groups_spec.rb @@ -173,6 +173,28 @@ describe API::Groups do end describe "GET /groups/:id" do + # Given a group, create one project for each visibility level + # + # group - Group to add projects to + # share_with - If provided, each project will be shared with this Group + # + # Returns a Hash of visibility_level => Project pairs + def add_projects_to_group(group, share_with: nil) + projects = { + public: create(:project, :public, namespace: group), + internal: create(:project, :internal, namespace: group), + private: create(:project, :private, namespace: group) + } + + if share_with + create(:project_group_link, project: projects[:public], group: share_with) + create(:project_group_link, project: projects[:internal], group: share_with) + create(:project_group_link, project: projects[:private], group: share_with) + end + + projects + end + context 'when unauthenticated' do it 'returns 404 for a private group' do get api("/groups/#{group2.id}") @@ -183,6 +205,26 @@ describe API::Groups do get api("/groups/#{group1.id}") expect(response).to have_gitlab_http_status(200) end + + it 'returns only public projects in the group' do + public_group = create(:group, :public) + projects = add_projects_to_group(public_group) + + get api("/groups/#{public_group.id}") + + expect(json_response['projects'].map { |p| p['id'].to_i }) + .to contain_exactly(projects[:public].id) + end + + it 'returns only public projects shared with the group' do + public_group = create(:group, :public) + projects = add_projects_to_group(public_group, share_with: group1) + + get api("/groups/#{group1.id}") + + expect(json_response['shared_projects'].map { |p| p['id'].to_i }) + .to contain_exactly(projects[:public].id) + end end context "when authenticated as user" do @@ -222,6 +264,26 @@ describe API::Groups do expect(response).to have_gitlab_http_status(404) end + + it 'returns only public and internal projects in the group' do + public_group = create(:group, :public) + projects = add_projects_to_group(public_group) + + get api("/groups/#{public_group.id}", user2) + + expect(json_response['projects'].map { |p| p['id'].to_i }) + .to contain_exactly(projects[:public].id, projects[:internal].id) + end + + it 'returns only public and internal projects shared with the group' do + public_group = create(:group, :public) + projects = add_projects_to_group(public_group, share_with: group1) + + get api("/groups/#{group1.id}", user2) + + expect(json_response['shared_projects'].map { |p| p['id'].to_i }) + .to contain_exactly(projects[:public].id, projects[:internal].id) + end end context "when authenticated as admin" do diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb index 67e1539cbc3..3c31980b273 100644 --- a/spec/requests/api/internal_spec.rb +++ b/spec/requests/api/internal_spec.rb @@ -537,16 +537,7 @@ describe API::Internal do context 'the project path was changed' do let!(:old_path_to_repo) { project.repository.path_to_repo } - let!(:old_full_path) { project.full_path } - let(:project_moved_message) do - <<-MSG.strip_heredoc - Project '#{old_full_path}' was moved to '#{project.full_path}'. - - Please update your Git remote and try again: - - git remote set-url origin #{project.ssh_url_to_repo} - MSG - end + let!(:repository) { project.repository } before do project.team << [user, :developer] @@ -555,19 +546,17 @@ describe API::Internal do end it 'rejects the push' do - push_with_path(key, old_path_to_repo) + push(key, project) expect(response).to have_gitlab_http_status(200) - expect(json_response['status']).to be_falsey - expect(json_response['message']).to eq(project_moved_message) + expect(json_response['status']).to be_falsy end it 'rejects the SSH pull' do - pull_with_path(key, old_path_to_repo) + pull(key, project) expect(response).to have_gitlab_http_status(200) - expect(json_response['status']).to be_falsey - expect(json_response['message']).to eq(project_moved_message) + expect(json_response['status']).to be_falsy end end end @@ -695,7 +684,7 @@ describe API::Internal do # end # end - describe 'POST /internal/post_receive' do + describe 'POST /internal/post_receive', :clean_gitlab_redis_shared_state do let(:identifier) { 'key-123' } let(:valid_params) do @@ -713,6 +702,8 @@ describe API::Internal do before do project.team << [user, :developer] + allow(described_class).to receive(:identify).and_return(user) + allow_any_instance_of(Gitlab::Identifier).to receive(:identify).and_return(user) end it 'enqueues a PostReceive worker job' do @@ -780,6 +771,19 @@ describe API::Internal do expect(json_response['broadcast_message']).to eq(nil) end end + + context 'with a redirected data' do + it 'returns redirected message on the response' do + project_moved = Gitlab::Checks::ProjectMoved.new(project, user, 'foo/baz', 'http') + project_moved.add_redirect_message + + post api("/internal/post_receive"), valid_params + + expect(response).to have_gitlab_http_status(200) + expect(json_response["redirected_message"]).to be_present + expect(json_response["redirected_message"]).to eq(project_moved.redirect_message) + end + end end describe 'POST /internal/pre_receive' do diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb index 99525cd0a6a..3f5070a1fd2 100644 --- a/spec/requests/api/issues_spec.rb +++ b/spec/requests/api/issues_spec.rb @@ -860,6 +860,20 @@ describe API::Issues, :mailer do end end + context 'user does not have permissions to create issue' do + let(:not_member) { create(:user) } + + before do + project.project_feature.update(issues_access_level: ProjectFeature::PRIVATE) + end + + it 'renders 403' do + post api("/projects/#{project.id}/issues", not_member), title: 'new issue' + + expect(response).to have_gitlab_http_status(403) + end + end + it 'creates a new project issue' do post api("/projects/#{project.id}/issues", user), title: 'new issue', labels: 'label, label2', weight: 3, diff --git a/spec/requests/api/repositories_spec.rb b/spec/requests/api/repositories_spec.rb index 9f2ff3b5af6..741800ff61d 100644 --- a/spec/requests/api/repositories_spec.rb +++ b/spec/requests/api/repositories_spec.rb @@ -378,6 +378,28 @@ describe API::Repositories do expect(first_contributor['additions']).to eq(0) expect(first_contributor['deletions']).to eq(0) end + + context 'using sorting' do + context 'by commits desc' do + it 'returns the repository contribuors sorted by commits desc' do + get api(route, current_user), { order_by: 'commits', sort: 'desc' } + + expect(response).to have_gitlab_http_status(200) + expect(response).to match_response_schema('contributors') + expect(json_response.first['commits']).to be > json_response.last['commits'] + end + end + + context 'by name desc' do + it 'returns the repository contribuors sorted by name asc case insensitive' do + get api(route, current_user), { order_by: 'name', sort: 'asc' } + + expect(response).to have_gitlab_http_status(200) + expect(response).to match_response_schema('contributors') + expect(json_response.first['name'].downcase).to be < json_response.last['name'].downcase + end + end + end end context 'when unauthenticated', 'and project is public' do diff --git a/spec/requests/api/settings_spec.rb b/spec/requests/api/settings_spec.rb index 63175c40a18..015d4b9a491 100644 --- a/spec/requests/api/settings_spec.rb +++ b/spec/requests/api/settings_spec.rb @@ -54,7 +54,7 @@ describe API::Settings, 'Settings' do dsa_key_restriction: 2048, ecdsa_key_restriction: 384, ed25519_key_restriction: 256, - circuitbreaker_failure_wait_time: 2 + circuitbreaker_check_interval: 2 expect(response).to have_gitlab_http_status(200) expect(json_response['default_projects_limit']).to eq(3) @@ -75,7 +75,7 @@ describe API::Settings, 'Settings' do expect(json_response['dsa_key_restriction']).to eq(2048) expect(json_response['ecdsa_key_restriction']).to eq(384) expect(json_response['ed25519_key_restriction']).to eq(256) - expect(json_response['circuitbreaker_failure_wait_time']).to eq(2) + expect(json_response['circuitbreaker_check_interval']).to eq(2) end end diff --git a/spec/requests/api/tags_spec.rb b/spec/requests/api/tags_spec.rb index 0bf7863bdc8..e2b19ad59f9 100644 --- a/spec/requests/api/tags_spec.rb +++ b/spec/requests/api/tags_spec.rb @@ -16,6 +16,44 @@ describe API::Tags do describe 'GET /projects/:id/repository/tags' do let(:route) { "/projects/#{project_id}/repository/tags" } + context 'sorting' do + let(:current_user) { user } + + it 'sorts by descending order by default' do + get api(route, current_user) + + desc_order_tags = project.repository.tags.sort_by { |tag| tag.dereferenced_target.committed_date } + desc_order_tags.reverse!.map! { |tag| tag.dereferenced_target.id } + + expect(json_response.map { |tag| tag['commit']['id'] }).to eq(desc_order_tags) + end + + it 'sorts by ascending order if specified' do + get api("#{route}?sort=asc", current_user) + + asc_order_tags = project.repository.tags.sort_by { |tag| tag.dereferenced_target.committed_date } + asc_order_tags.map! { |tag| tag.dereferenced_target.id } + + expect(json_response.map { |tag| tag['commit']['id'] }).to eq(asc_order_tags) + end + + it 'sorts by name in descending order when requested' do + get api("#{route}?order_by=name", current_user) + + ordered_by_name = project.repository.tags.map { |tag| tag.name }.sort.reverse + + expect(json_response.map { |tag| tag['name'] }).to eq(ordered_by_name) + end + + it 'sorts by name in ascending order when requested' do + get api("#{route}?order_by=name&sort=asc", current_user) + + ordered_by_name = project.repository.tags.map { |tag| tag.name }.sort + + expect(json_response.map { |tag| tag['name'] }).to eq(ordered_by_name) + end + end + shared_examples_for 'repository tags' do it 'returns the repository tags' do get api(route, current_user) diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb index a16f98bec36..fa02fffc82a 100644 --- a/spec/requests/git_http_spec.rb +++ b/spec/requests/git_http_spec.rb @@ -324,9 +324,9 @@ describe 'Git HTTP requests' do <<-MSG.strip_heredoc Project '#{redirect.path}' was moved to '#{project.full_path}'. - Please update your Git remote and try again: + Please update your Git remote: - git remote set-url origin #{project.http_url_to_repo} + git remote set-url origin #{project.http_url_to_repo} and try again. MSG end @@ -533,9 +533,9 @@ describe 'Git HTTP requests' do <<-MSG.strip_heredoc Project '#{redirect.path}' was moved to '#{project.full_path}'. - Please update your Git remote and try again: + Please update your Git remote: - git remote set-url origin #{project.http_url_to_repo} + git remote set-url origin #{project.http_url_to_repo} and try again. MSG end diff --git a/spec/rubocop/cop/gitlab/module_with_instance_variables_spec.rb b/spec/rubocop/cop/gitlab/module_with_instance_variables_spec.rb new file mode 100644 index 00000000000..1fd40653f79 --- /dev/null +++ b/spec/rubocop/cop/gitlab/module_with_instance_variables_spec.rb @@ -0,0 +1,157 @@ +require 'spec_helper' +require 'rubocop' +require 'rubocop/rspec/support' +require_relative '../../../../rubocop/cop/gitlab/module_with_instance_variables' + +describe RuboCop::Cop::Gitlab::ModuleWithInstanceVariables do + include CopHelper + + subject(:cop) { described_class.new } + + shared_examples('registering offense') do |options| + let(:offending_lines) { options[:offending_lines] } + + it 'registers an offense when instance variable is used in a module' do + inspect_source(cop, source) + + aggregate_failures do + expect(cop.offenses.size).to eq(offending_lines.size) + expect(cop.offenses.map(&:line)).to eq(offending_lines) + end + end + end + + shared_examples('not registering offense') do + it 'does not register offenses' do + inspect_source(cop, source) + + expect(cop.offenses).to be_empty + end + end + + context 'when source is a regular module' do + it_behaves_like 'registering offense', offending_lines: [3] do + let(:source) do + <<~RUBY + module M + def f + @f = true + end + end + RUBY + end + end + end + + context 'when source is a nested module' do + it_behaves_like 'registering offense', offending_lines: [4] do + let(:source) do + <<~RUBY + module N + module M + def f + @f = true + end + end + end + RUBY + end + end + end + + context 'when source is a nested module with multiple offenses' do + it_behaves_like 'registering offense', offending_lines: [4, 12] do + let(:source) do + <<~RUBY + module N + module M + def f + @f = true + end + + def g + true + end + + def h + @h = true + end + end + end + RUBY + end + end + end + + context 'when source is using simple or ivar assignment' do + it_behaves_like 'not registering offense' do + let(:source) do + <<~RUBY + module M + def f + @f ||= true + end + end + RUBY + end + end + end + + context 'when source is using simple ivar' do + it_behaves_like 'not registering offense' do + let(:source) do + <<~RUBY + module M + def f? + @f + end + end + RUBY + end + end + end + + context 'when source is defining initialize' do + it_behaves_like 'not registering offense' do + let(:source) do + <<~RUBY + module M + def initialize + @a = 1 + @b = 2 + end + end + RUBY + end + end + end + + context 'when source is using simple or ivar assignment with other ivar' do + it_behaves_like 'registering offense', offending_lines: [3] do + let(:source) do + <<~RUBY + module M + def f + @f ||= g(@g) + end + end + RUBY + end + end + end + + context 'when source is using or ivar assignment with something else' do + it_behaves_like 'registering offense', offending_lines: [3, 4] do + let(:source) do + <<~RUBY + module M + def f + @f ||= true + @f.to_s + end + end + RUBY + end + end + end +end diff --git a/spec/rubocop/cop/include_sidekiq_worker_spec.rb b/spec/rubocop/cop/include_sidekiq_worker_spec.rb new file mode 100644 index 00000000000..7f406535dda --- /dev/null +++ b/spec/rubocop/cop/include_sidekiq_worker_spec.rb @@ -0,0 +1,31 @@ +require 'spec_helper' +require 'rubocop' +require 'rubocop/rspec/support' +require_relative '../../../rubocop/cop/include_sidekiq_worker' + +describe RuboCop::Cop::IncludeSidekiqWorker do + include CopHelper + + subject(:cop) { described_class.new } + + context 'when `Sidekiq::Worker` is included' do + let(:source) { 'include Sidekiq::Worker' } + let(:correct_source) { 'include ApplicationWorker' } + + it 'registers an offense ' do + inspect_source(cop, source) + + aggregate_failures do + expect(cop.offenses.size).to eq(1) + expect(cop.offenses.map(&:line)).to eq([1]) + expect(cop.highlights).to eq(['Sidekiq::Worker']) + end + end + + it 'autocorrects to the right version' do + autocorrected = autocorrect_source(cop, source) + + expect(autocorrected).to eq(correct_source) + end + end +end diff --git a/spec/rubocop/cop/migration/remove_column_spec.rb b/spec/rubocop/cop/migration/remove_column_spec.rb new file mode 100644 index 00000000000..89112f01723 --- /dev/null +++ b/spec/rubocop/cop/migration/remove_column_spec.rb @@ -0,0 +1,68 @@ +require 'spec_helper' + +require 'rubocop' +require 'rubocop/rspec/support' + +require_relative '../../../../rubocop/cop/migration/remove_column' + +describe RuboCop::Cop::Migration::RemoveColumn do + include CopHelper + + subject(:cop) { described_class.new } + + def source(meth = 'change') + "def #{meth}; remove_column :table, :column; end" + end + + context 'in a regular migration' do + before do + allow(cop).to receive(:in_migration?).and_return(true) + allow(cop).to receive(:in_post_deployment_migration?).and_return(false) + end + + it 'registers an offense when remove_column is used in the change method' do + inspect_source(cop, source('change')) + + aggregate_failures do + expect(cop.offenses.size).to eq(1) + expect(cop.offenses.map(&:line)).to eq([1]) + end + end + + it 'registers an offense when remove_column is used in the up method' do + inspect_source(cop, source('up')) + + aggregate_failures do + expect(cop.offenses.size).to eq(1) + expect(cop.offenses.map(&:line)).to eq([1]) + end + end + + it 'registers no offense when remove_column is used in the down method' do + inspect_source(cop, source('down')) + + expect(cop.offenses.size).to eq(0) + end + end + + context 'in a post-deployment migration' do + before do + allow(cop).to receive(:in_migration?).and_return(true) + allow(cop).to receive(:in_post_deployment_migration?).and_return(true) + end + + it 'registers no offense' do + inspect_source(cop, source) + + expect(cop.offenses.size).to eq(0) + end + end + + context 'outside of a migration' do + it 'registers no offense' do + inspect_source(cop, source) + + expect(cop.offenses.size).to eq(0) + end + end +end diff --git a/spec/rubocop/cop/sidekiq_options_queue_spec.rb b/spec/rubocop/cop/sidekiq_options_queue_spec.rb new file mode 100644 index 00000000000..a31de381631 --- /dev/null +++ b/spec/rubocop/cop/sidekiq_options_queue_spec.rb @@ -0,0 +1,26 @@ +require 'spec_helper' +require 'rubocop' +require 'rubocop/rspec/support' +require_relative '../../../rubocop/cop/sidekiq_options_queue' + +describe RuboCop::Cop::SidekiqOptionsQueue do + include CopHelper + + subject(:cop) { described_class.new } + + it 'registers an offense when `sidekiq_options` is used with the `queue` option' do + inspect_source(cop, 'sidekiq_options queue: "some_queue"') + + aggregate_failures do + expect(cop.offenses.size).to eq(1) + expect(cop.offenses.map(&:line)).to eq([1]) + expect(cop.highlights).to eq(['queue: "some_queue"']) + end + end + + it 'does not register an offense when `sidekiq_options` is used with another option' do + inspect_source(cop, 'sidekiq_options retry: false') + + expect(cop.offenses).to be_empty + end +end diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb index b0de8d447a2..267258b33a8 100644 --- a/spec/services/ci/create_pipeline_service_spec.rb +++ b/spec/services/ci/create_pipeline_service_spec.rb @@ -64,6 +64,18 @@ describe Ci::CreatePipelineService do create(:merge_request, source_branch: 'master', target_branch: "branch_2", source_project: project) end + context 'when related merge request is already merged' do + let!(:merged_merge_request) do + create(:merge_request, source_branch: 'master', target_branch: "branch_2", source_project: project, state: 'merged') + end + + it 'does not schedule update head pipeline job' do + expect(UpdateHeadPipelineForMergeRequestWorker).not_to receive(:perform_async).with(merged_merge_request.id) + + execute_service + end + end + context 'when the head pipeline sha equals merge request sha' do it 'updates head pipeline of each merge request' do merge_request_1 @@ -77,13 +89,13 @@ describe Ci::CreatePipelineService do end context 'when the head pipeline sha does not equal merge request sha' do - it 'raises the ArgumentError error from worker and does not update the head piepeline of MRs' do + it 'does not update the head piepeline of MRs' do merge_request_1 merge_request_2 allow_any_instance_of(Ci::Pipeline).to receive(:latest?).and_return(true) - expect { execute_service(after: 'ae73cb07c9eeaf35924a10f713b364d32b2dd34f') }.to raise_error(ArgumentError) + expect { execute_service(after: 'ae73cb07c9eeaf35924a10f713b364d32b2dd34f') }.not_to raise_error last_pipeline = Ci::Pipeline.last @@ -518,5 +530,20 @@ describe Ci::CreatePipelineService do end end end + + context 'when pipeline is running for a tag' do + before do + config = YAML.dump(test: { script: 'test', only: ['branches'] }, + deploy: { script: 'deploy', only: ['tags'] }) + + stub_ci_pipeline_yaml_file(config) + end + + it 'creates a tagged pipeline' do + pipeline = execute_service(ref: 'v1.0.0') + + expect(pipeline.tag?).to be true + end + end end end diff --git a/spec/services/ci/register_job_service_spec.rb b/spec/services/ci/register_job_service_spec.rb index 3ee59014b5b..97a563c1ce1 100644 --- a/spec/services/ci/register_job_service_spec.rb +++ b/spec/services/ci/register_job_service_spec.rb @@ -2,11 +2,11 @@ require 'spec_helper' module Ci describe RegisterJobService do - let!(:project) { FactoryGirl.create :project, shared_runners_enabled: false } - let!(:pipeline) { FactoryGirl.create :ci_pipeline, project: project } - let!(:pending_job) { FactoryGirl.create :ci_build, pipeline: pipeline } - let!(:shared_runner) { FactoryGirl.create(:ci_runner, is_shared: true) } - let!(:specific_runner) { FactoryGirl.create(:ci_runner, is_shared: false) } + let!(:project) { FactoryBot.create :project, shared_runners_enabled: false } + let!(:pipeline) { FactoryBot.create :ci_pipeline, project: project } + let!(:pending_job) { FactoryBot.create :ci_build, pipeline: pipeline } + let!(:shared_runner) { FactoryBot.create(:ci_runner, is_shared: true) } + let!(:specific_runner) { FactoryBot.create(:ci_runner, is_shared: false) } before do specific_runner.assign_to(project) @@ -74,11 +74,11 @@ module Ci let!(:project3) { create :project, shared_runners_enabled: true } let!(:pipeline3) { create :ci_pipeline, project: project3 } let!(:build1_project1) { pending_job } - let!(:build2_project1) { FactoryGirl.create :ci_build, pipeline: pipeline } - let!(:build3_project1) { FactoryGirl.create :ci_build, pipeline: pipeline } - let!(:build1_project2) { FactoryGirl.create :ci_build, pipeline: pipeline2 } - let!(:build2_project2) { FactoryGirl.create :ci_build, pipeline: pipeline2 } - let!(:build1_project3) { FactoryGirl.create :ci_build, pipeline: pipeline3 } + let!(:build2_project1) { FactoryBot.create :ci_build, pipeline: pipeline } + let!(:build3_project1) { FactoryBot.create :ci_build, pipeline: pipeline } + let!(:build1_project2) { FactoryBot.create :ci_build, pipeline: pipeline2 } + let!(:build2_project2) { FactoryBot.create :ci_build, pipeline: pipeline2 } + let!(:build1_project3) { FactoryBot.create :ci_build, pipeline: pipeline3 } it 'prefers projects without builds first' do # it gets for one build from each of the projects @@ -287,9 +287,9 @@ module Ci shared_examples 'validation is active' do context 'when depended job has not been completed yet' do - let!(:pre_stage_job) { create(:ci_build, :running, pipeline: pipeline, name: 'test', stage_idx: 0) } + let!(:pre_stage_job) { create(:ci_build, :manual, pipeline: pipeline, name: 'test', stage_idx: 0) } - it_behaves_like 'not pick' + it { expect(subject).to eq(pending_job) } end context 'when artifacts of depended job has been expired' do @@ -307,15 +307,27 @@ module Ci it_behaves_like 'not pick' end + + context 'when job object is staled' do + let!(:pre_stage_job) { create(:ci_build, :success, :expired, pipeline: pipeline, name: 'test', stage_idx: 0) } + + before do + allow_any_instance_of(Ci::Build).to receive(:drop!) + .and_raise(ActiveRecord::StaleObjectError.new(pending_job, :drop!)) + end + + it 'does not drop nor pick' do + expect(subject).to be_nil + end + end end shared_examples 'validation is not active' do context 'when depended job has not been completed yet' do - let!(:pre_stage_job) { create(:ci_build, :running, pipeline: pipeline, name: 'test', stage_idx: 0) } + let!(:pre_stage_job) { create(:ci_build, :manual, pipeline: pipeline, name: 'test', stage_idx: 0) } it { expect(subject).to eq(pending_job) } end - context 'when artifacts of depended job has been expired' do let!(:pre_stage_job) { create(:ci_build, :success, :expired, pipeline: pipeline, name: 'test', stage_idx: 0) } diff --git a/spec/services/ci/retry_build_service_spec.rb b/spec/services/ci/retry_build_service_spec.rb index d48a44fa57f..a06397a0782 100644 --- a/spec/services/ci/retry_build_service_spec.rb +++ b/spec/services/ci/retry_build_service_spec.rb @@ -40,7 +40,7 @@ describe Ci::RetryBuildService do description: 'my-job', stage: 'test', pipeline: pipeline, auto_canceled_by: create(:ci_empty_pipeline, project: project)) do |build| ## - # TODO, workaround for FactoryGirl limitation when having both + # TODO, workaround for FactoryBot limitation when having both # stage (text) and stage_id (integer) columns in the table. build.stage_id = stage.id end diff --git a/spec/services/members/authorized_destroy_service_spec.rb b/spec/services/members/authorized_destroy_service_spec.rb index 2d04d824180..d4ef31c0c74 100644 --- a/spec/services/members/authorized_destroy_service_spec.rb +++ b/spec/services/members/authorized_destroy_service_spec.rb @@ -45,7 +45,7 @@ describe Members::AuthorizedDestroyService do expect { described_class.new(member, member_user).execute } .to change { number_of_assigned_issuables(member_user) }.from(4).to(2) - expect(issue.reload.assignee_id).to be_nil + expect(issue.reload.assignee_ids).to be_empty expect(merge_request.reload.assignee_id).to be_nil end end diff --git a/spec/services/merge_requests/conflicts/resolve_service_spec.rb b/spec/services/merge_requests/conflicts/resolve_service_spec.rb index 5376083e7f5..e28d8d7ae5c 100644 --- a/spec/services/merge_requests/conflicts/resolve_service_spec.rb +++ b/spec/services/merge_requests/conflicts/resolve_service_spec.rb @@ -213,7 +213,7 @@ describe MergeRequests::Conflicts::ResolveService do MergeRequests::Conflicts::ListService.new(merge_request).conflicts.resolver end let(:regex_conflict) do - resolver.conflict_for_path('files/ruby/regex.rb', 'files/ruby/regex.rb') + resolver.conflict_for_path(resolver.conflicts, 'files/ruby/regex.rb', 'files/ruby/regex.rb') end let(:invalid_params) do diff --git a/spec/services/merge_requests/create_from_issue_service_spec.rb b/spec/services/merge_requests/create_from_issue_service_spec.rb index a7ab389b357..623b182b205 100644 --- a/spec/services/merge_requests/create_from_issue_service_spec.rb +++ b/spec/services/merge_requests/create_from_issue_service_spec.rb @@ -100,5 +100,17 @@ describe MergeRequests::CreateFromIssueService do expect(result[:merge_request].target_branch).to eq(project.default_branch) end + + it 'executes quick actions if the build service sets them in the description' do + allow(service).to receive(:merge_request).and_wrap_original do |m, *args| + m.call(*args).tap do |merge_request| + merge_request.description = "/assign #{user.to_reference}" + end + end + + result = service.execute + + expect(result[:merge_request].assignee).to eq(user) + end end end diff --git a/spec/services/merge_requests/merge_service_spec.rb b/spec/services/merge_requests/merge_service_spec.rb index f86f1ac2443..c38ddf4612b 100644 --- a/spec/services/merge_requests/merge_service_spec.rb +++ b/spec/services/merge_requests/merge_service_spec.rb @@ -1,14 +1,14 @@ require 'spec_helper' describe MergeRequests::MergeService do - let(:user) { create(:user) } - let(:user2) { create(:user) } + set(:user) { create(:user) } + set(:user2) { create(:user) } let(:merge_request) { create(:merge_request, :simple, author: user2, assignee: user2) } let(:project) { merge_request.project } before do - project.team << [user, :master] - project.team << [user2, :developer] + project.add_master(user) + project.add_developer(user2) end describe '#execute' do diff --git a/spec/services/users/keys_count_service_spec.rb b/spec/services/users/keys_count_service_spec.rb index a188cf86772..bee8380e8b7 100644 --- a/spec/services/users/keys_count_service_spec.rb +++ b/spec/services/users/keys_count_service_spec.rb @@ -15,14 +15,12 @@ describe Users::KeysCountService, :use_clean_rails_memory_store_caching do expect(service.count).to eq(1) end - it 'caches the number of keys in Redis' do + it 'caches the number of keys in Redis', :request_store do + service.delete_cache + control_count = ActiveRecord::QueryRecorder.new { service.count }.count service.delete_cache - recorder = ActiveRecord::QueryRecorder.new do - 2.times { service.count } - end - - expect(recorder.count).to eq(1) + expect { 2.times { service.count } }.not_to exceed_query_limit(control_count) end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 242a2230b67..f51bb44086b 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -121,18 +121,6 @@ RSpec.configure do |config| reset_delivered_emails! end - # Stub the `ForkedStorageCheck.storage_available?` method unless - # `:broken_storage` metadata is defined - # - # This check can be slow and is unnecessary in a test environment where we - # know the storage is available, because we create it at runtime - config.before(:example) do |example| - unless example.metadata[:broken_storage] - allow(Gitlab::Git::Storage::ForkedStorageCheck) - .to receive(:storage_available?).and_return(true) - end - end - config.around(:each, :use_clean_rails_memory_store_caching) do |example| caching_store = Rails.cache Rails.cache = ActiveSupport::Cache::MemoryStore.new @@ -195,7 +183,7 @@ RSpec::Matchers.define :match_asset_path do |expected| end end -FactoryGirl::SyntaxRunner.class_eval do +FactoryBot::SyntaxRunner.class_eval do include RSpec::Mocks::ExampleMethods end diff --git a/spec/support/batch_loader.rb b/spec/support/batch_loader.rb new file mode 100644 index 00000000000..bb790e660a6 --- /dev/null +++ b/spec/support/batch_loader.rb @@ -0,0 +1,5 @@ +RSpec.configure do |config| + config.after do + BatchLoader::Executor.clear_current + end +end diff --git a/spec/support/factory_girl.rb b/spec/support/factory_girl.rb index eec437fb3aa..c7890e49c66 100644 --- a/spec/support/factory_girl.rb +++ b/spec/support/factory_girl.rb @@ -1,3 +1,3 @@ RSpec.configure do |config| - config.include FactoryGirl::Syntax::Methods + config.include FactoryBot::Syntax::Methods end diff --git a/spec/support/markdown_feature.rb b/spec/support/markdown_feature.rb index c90359d7cfa..a0d854d3641 100644 --- a/spec/support/markdown_feature.rb +++ b/spec/support/markdown_feature.rb @@ -8,7 +8,7 @@ # The class renders `spec/fixtures/markdown.md.erb` using ERB, allowing for # reference to the factory-created objects. class MarkdownFeature - include FactoryGirl::Syntax::Methods + include FactoryBot::Syntax::Methods def user @user ||= create(:user) diff --git a/spec/support/stored_repositories.rb b/spec/support/stored_repositories.rb index f3deae0f455..f9121cce985 100644 --- a/spec/support/stored_repositories.rb +++ b/spec/support/stored_repositories.rb @@ -12,6 +12,25 @@ RSpec.configure do |config| raise GRPC::Unavailable.new('Gitaly broken in this spec') end - Gitlab::Git::Storage::CircuitBreaker.reset_all! + # Track the maximum number of failures + first_failure = Time.parse("2017-11-14 17:52:30") + last_failure = Time.parse("2017-11-14 18:54:37") + failure_count = Gitlab::CurrentSettings + .current_application_settings + .circuitbreaker_failure_count_threshold + 1 + cache_key = "#{Gitlab::Git::Storage::REDIS_KEY_PREFIX}broken:#{Gitlab::Environment.hostname}" + + Gitlab::Git::Storage.redis.with do |redis| + redis.pipelined do + redis.zadd(Gitlab::Git::Storage::REDIS_KNOWN_KEYS, 0, cache_key) + redis.hset(cache_key, :first_failure, first_failure.to_i) + redis.hset(cache_key, :last_failure, last_failure.to_i) + redis.hset(cache_key, :failure_count, failure_count.to_i) + end + end + end + + config.after(:each, :broken_storage) do + Gitlab::Git::Storage.redis.with(&:flushall) end end diff --git a/spec/support/stub_configuration.rb b/spec/support/stub_configuration.rb index 4ead78529c3..9f08c139322 100644 --- a/spec/support/stub_configuration.rb +++ b/spec/support/stub_configuration.rb @@ -7,6 +7,9 @@ module StubConfiguration allow_any_instance_of(ApplicationSetting).to receive_messages(to_settings(messages)) allow(Gitlab::CurrentSettings.current_application_settings) .to receive_messages(to_settings(messages)) + + # Ensure that we don't use the Markdown cache when stubbing these values + allow_any_instance_of(ApplicationSetting).to receive(:cached_html_up_to_date?).and_return(false) end def stub_not_protect_default_branch @@ -43,6 +46,8 @@ module StubConfiguration end def stub_storage_settings(messages) + messages.deep_stringify_keys! + # Default storage is always required messages['default'] ||= Gitlab.config.repositories.storages.default messages.each do |storage_name, storage_settings| diff --git a/spec/support/stub_env.rb b/spec/support/stub_env.rb index 19fbe572930..f621463e621 100644 --- a/spec/support/stub_env.rb +++ b/spec/support/stub_env.rb @@ -17,6 +17,7 @@ module StubENV def add_stubbed_value(key, value) allow(ENV).to receive(:[]).with(key).and_return(value) + allow(ENV).to receive(:key?).with(key).and_return(true) allow(ENV).to receive(:fetch).with(key).and_return(value) allow(ENV).to receive(:fetch).with(key, anything()) do |_, default_val| value || default_val @@ -29,6 +30,7 @@ module StubENV def init_stub allow(ENV).to receive(:[]).and_call_original + allow(ENV).to receive(:key?).and_call_original allow(ENV).to receive(:fetch).and_call_original add_stubbed_value(STUBBED_KEY, true) end diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb index b300b493f86..ffc051a3fff 100644 --- a/spec/support/test_env.rb +++ b/spec/support/test_env.rb @@ -82,10 +82,10 @@ module TestEnv setup_gitaly - # Create repository for FactoryGirl.create(:project) + # Create repository for FactoryBot.create(:project) setup_factory_repo - # Create repository for FactoryGirl.create(:forked_project_with_submodules) + # Create repository for FactoryBot.create(:forked_project_with_submodules) setup_forked_repo end diff --git a/spec/uploaders/records_uploads_spec.rb b/spec/uploaders/records_uploads_spec.rb index bb32ee62ccb..7ef7fb7d758 100644 --- a/spec/uploaders/records_uploads_spec.rb +++ b/spec/uploaders/records_uploads_spec.rb @@ -8,7 +8,7 @@ describe RecordsUploads do storage :file def model - FactoryGirl.build_stubbed(:user) + FactoryBot.build_stubbed(:user) end end diff --git a/spec/views/projects/jobs/show.html.haml_spec.rb b/spec/views/projects/jobs/show.html.haml_spec.rb index 6139529013f..6a67da79ec5 100644 --- a/spec/views/projects/jobs/show.html.haml_spec.rb +++ b/spec/views/projects/jobs/show.html.haml_spec.rb @@ -187,7 +187,7 @@ describe 'projects/jobs/show' do context 'when incomplete trigger_request is used' do before do - build.trigger_request = FactoryGirl.build(:ci_trigger_request, trigger: nil) + build.trigger_request = FactoryBot.build(:ci_trigger_request, trigger: nil) end it 'test should not render token block' do @@ -199,7 +199,7 @@ describe 'projects/jobs/show' do context 'when complete trigger_request is used' do before do - build.trigger_request = FactoryGirl.build(:ci_trigger_request) + build.trigger_request = FactoryBot.build(:ci_trigger_request) end it 'should render token' do diff --git a/spec/views/projects/tree/_blob_item.html.haml_spec.rb b/spec/views/projects/tree/_blob_item.html.haml_spec.rb new file mode 100644 index 00000000000..6a477c712ff --- /dev/null +++ b/spec/views/projects/tree/_blob_item.html.haml_spec.rb @@ -0,0 +1,40 @@ +require 'spec_helper' + +describe 'projects/tree/_blob_item' do + let(:project) { create(:project, :repository) } + let(:repository) { project.repository } + let(:blob_item) { Gitlab::Git::Tree.where(repository, SeedRepo::Commit::ID, 'files/ruby').first } + + before do + assign(:project, project) + assign(:repository, repository) + assign(:id, File.join('master', '')) + assign(:lfs_blob_ids, []) + end + + it 'renders blob item' do + render_partial(blob_item) + + expect(rendered).to have_content(blob_item.name) + expect(rendered).not_to have_selector('.label-lfs', text: 'LFS') + end + + describe 'LFS blob' do + before do + assign(:lfs_blob_ids, [blob_item].map(&:id)) + + render_partial(blob_item) + end + + it 'renders LFS badge' do + expect(rendered).to have_selector('.label-lfs', text: 'LFS') + end + end + + def render_partial(blob_item) + render partial: 'projects/tree/blob_item', locals: { + blob_item: blob_item, + type: 'blob' + } + end +end diff --git a/spec/views/projects/tree/show.html.haml_spec.rb b/spec/views/projects/tree/show.html.haml_spec.rb index 3c25e341b39..44b32df0395 100644 --- a/spec/views/projects/tree/show.html.haml_spec.rb +++ b/spec/views/projects/tree/show.html.haml_spec.rb @@ -9,6 +9,7 @@ describe 'projects/tree/show' do before do assign(:project, project) assign(:repository, repository) + assign(:lfs_blob_ids, []) allow(view).to receive(:can?).and_return(true) allow(view).to receive(:can_collaborate_with_project?).and_return(true) diff --git a/spec/workers/concerns/application_worker_spec.rb b/spec/workers/concerns/application_worker_spec.rb index 0145563e0ed..901d77178bc 100644 --- a/spec/workers/concerns/application_worker_spec.rb +++ b/spec/workers/concerns/application_worker_spec.rb @@ -17,6 +17,14 @@ describe ApplicationWorker do end end + describe '.queue_namespace' do + it 'sets the queue name based on the class name' do + worker.queue_namespace :some_namespace + + expect(worker.queue).to eq('some_namespace:foo_bar_dummy') + end + end + describe '.queue' do it 'returns the queue name' do worker.sidekiq_options queue: :some_queue diff --git a/spec/workers/concerns/cluster_queue_spec.rb b/spec/workers/concerns/cluster_queue_spec.rb index 5049886b55c..4118b9aa194 100644 --- a/spec/workers/concerns/cluster_queue_spec.rb +++ b/spec/workers/concerns/cluster_queue_spec.rb @@ -14,6 +14,6 @@ describe ClusterQueue do it 'sets a default pipelines queue automatically' do expect(worker.sidekiq_options['queue']) - .to eq :gcp_cluster + .to eq 'gcp_cluster:dummy' end end diff --git a/spec/workers/concerns/cronjob_queue_spec.rb b/spec/workers/concerns/cronjob_queue_spec.rb index 3ae1c5f54d8..c042a52f41f 100644 --- a/spec/workers/concerns/cronjob_queue_spec.rb +++ b/spec/workers/concerns/cronjob_queue_spec.rb @@ -13,7 +13,7 @@ describe CronjobQueue do end it 'sets the queue name of a worker' do - expect(worker.sidekiq_options['queue'].to_s).to eq('cronjob') + expect(worker.sidekiq_options['queue'].to_s).to eq('cronjob:dummy') end it 'disables retrying of failed jobs' do diff --git a/spec/workers/concerns/gitlab/github_import/queue_spec.rb b/spec/workers/concerns/gitlab/github_import/queue_spec.rb index 9c69ee32da1..a96f583aff7 100644 --- a/spec/workers/concerns/gitlab/github_import/queue_spec.rb +++ b/spec/workers/concerns/gitlab/github_import/queue_spec.rb @@ -11,6 +11,6 @@ describe Gitlab::GithubImport::Queue do include Gitlab::GithubImport::Queue end - expect(worker.sidekiq_options['queue']).to eq('github_importer') + expect(worker.sidekiq_options['queue']).to eq('github_importer:dummy') end end diff --git a/spec/workers/concerns/pipeline_queue_spec.rb b/spec/workers/concerns/pipeline_queue_spec.rb index dd911760948..a312b307fce 100644 --- a/spec/workers/concerns/pipeline_queue_spec.rb +++ b/spec/workers/concerns/pipeline_queue_spec.rb @@ -14,15 +14,6 @@ describe PipelineQueue do it 'sets a default pipelines queue automatically' do expect(worker.sidekiq_options['queue']) - .to eq 'pipeline_default' - end - - describe '.enqueue_in' do - it 'sets a custom sidekiq queue with prefix and group' do - worker.enqueue_in(group: :processing) - - expect(worker.sidekiq_options['queue']) - .to eq 'pipeline_processing' - end + .to eq 'pipeline_default:dummy' end end diff --git a/spec/workers/concerns/repository_check_queue_spec.rb b/spec/workers/concerns/repository_check_queue_spec.rb index fdbbfcc90a5..d2eeecfc9a8 100644 --- a/spec/workers/concerns/repository_check_queue_spec.rb +++ b/spec/workers/concerns/repository_check_queue_spec.rb @@ -13,7 +13,7 @@ describe RepositoryCheckQueue do end it 'sets the queue name of a worker' do - expect(worker.sidekiq_options['queue'].to_s).to eq('repository_check') + expect(worker.sidekiq_options['queue'].to_s).to eq('repository_check:dummy') end it 'disables retrying of failed jobs' do diff --git a/spec/workers/every_sidekiq_worker_spec.rb b/spec/workers/every_sidekiq_worker_spec.rb index 7ee0a51a263..9e3b99b3502 100644 --- a/spec/workers/every_sidekiq_worker_spec.rb +++ b/spec/workers/every_sidekiq_worker_spec.rb @@ -1,21 +1,36 @@ require 'spec_helper' describe 'Every Sidekiq worker' do - it 'includes ApplicationWorker' do - expect(Gitlab::SidekiqConfig.workers).to all(include(ApplicationWorker)) - end - it 'does not use the default queue' do expect(Gitlab::SidekiqConfig.workers.map(&:queue)).not_to include('default') end it 'uses the cronjob queue when the worker runs as a cronjob' do - expect(Gitlab::SidekiqConfig.cron_workers.map(&:queue)).to all(eq('cronjob')) + expect(Gitlab::SidekiqConfig.cron_workers.map(&:queue)).to all(start_with('cronjob:')) + end + + it 'has its queue in app/workers/all_queues.yml', :aggregate_failures do + file_worker_queues = Gitlab::SidekiqConfig.worker_queues.to_set + + worker_queues = Gitlab::SidekiqConfig.workers.map(&:queue).to_set + worker_queues << ActionMailer::DeliveryJob.queue_name + worker_queues << 'default' + + missing_from_file = worker_queues - file_worker_queues + expect(missing_from_file).to be_empty, "expected #{missing_from_file.to_a.inspect} to be in app/workers/all_queues.yml" + + unncessarily_in_file = file_worker_queues - worker_queues + expect(unncessarily_in_file).to be_empty, "expected #{unncessarily_in_file.to_a.inspect} not to be in app/workers/all_queues.yml" end - it 'defines the queue in the Sidekiq configuration file' do - config_queue_names = Gitlab::SidekiqConfig.config_queues.to_set + it 'has its queue or namespace in config/sidekiq_queues.yml', :aggregate_failures do + config_queues = Gitlab::SidekiqConfig.config_queues.to_set + + Gitlab::SidekiqConfig.workers.each do |worker| + queue = worker.queue + queue_namespace = queue.split(':').first - expect(Gitlab::SidekiqConfig.worker_queues).to all(be_in(config_queue_names)) + expect(config_queues).to include(queue).or(include(queue_namespace)) + end end end diff --git a/spec/workers/stuck_merge_jobs_worker_spec.rb b/spec/workers/stuck_merge_jobs_worker_spec.rb index f8b55e873df..c2c2a5f9121 100644 --- a/spec/workers/stuck_merge_jobs_worker_spec.rb +++ b/spec/workers/stuck_merge_jobs_worker_spec.rb @@ -14,7 +14,6 @@ describe StuckMergeJobsWorker do mr_with_sha.reload mr_without_sha.reload - expect(mr_with_sha).to be_merged expect(mr_without_sha).to be_opened expect(mr_with_sha.merge_jid).to be_present @@ -24,10 +23,13 @@ describe StuckMergeJobsWorker do it 'updates merge request to opened when locked but has not been merged' do allow(Gitlab::SidekiqStatus).to receive(:completed_jids).and_return(%w(123)) merge_request = create(:merge_request, :locked, merge_jid: '123', state: :locked) + pipeline = create(:ci_empty_pipeline, project: merge_request.project, ref: merge_request.source_branch, sha: merge_request.source_branch_sha) worker.perform - expect(merge_request.reload).to be_opened + merge_request.reload + expect(merge_request).to be_opened + expect(merge_request.head_pipeline).to eq(pipeline) end it 'logs updated stuck merge job ids' do diff --git a/spec/workers/update_head_pipeline_for_merge_request_worker_spec.rb b/spec/workers/update_head_pipeline_for_merge_request_worker_spec.rb index 522e1566271..9adde5fc21a 100644 --- a/spec/workers/update_head_pipeline_for_merge_request_worker_spec.rb +++ b/spec/workers/update_head_pipeline_for_merge_request_worker_spec.rb @@ -22,7 +22,7 @@ describe UpdateHeadPipelineForMergeRequestWorker do end it 'does not update head_pipeline_id' do - expect { subject.perform(merge_request.id) }.to raise_error(ArgumentError) + expect { subject.perform(merge_request.id) }.not_to raise_error expect(merge_request.reload.head_pipeline_id).to eq(nil) end diff --git a/vendor/Dockerfile/CONTRIBUTING.md b/vendor/Dockerfile/CONTRIBUTING.md index 0878db6dd9e..3e98f2e7b5b 100644 --- a/vendor/Dockerfile/CONTRIBUTING.md +++ b/vendor/Dockerfile/CONTRIBUTING.md @@ -1,22 +1,15 @@ -The canonical repository for `Dockerfile` templates is -https://gitlab.com/gitlab-org/Dockerfile. +## Developer Certificate of Origin + License -GitLab only mirrors the templates. Please submit your merge requests to -https://gitlab.com/gitlab-org/Dockerfile. +By contributing to GitLab B.V., You accept and agree to the following terms and +conditions for Your present and future Contributions submitted to GitLab B.V. +Except for the license granted herein to GitLab B.V. and recipients of software +distributed by GitLab B.V., You reserve all right, title, and interest in and to +Your Contributions. All Contributions are subject to the following DCO + License +terms. -## Contributing +[DCO + License](https://gitlab.com/gitlab-org/dco/blob/master/README.md) -Thank you for your interest in contributing to this GitLab project! We welcome -all contributions. By participating in this project, you agree to abide by the -[code of conduct](#code-of-conduct). - -## Contributor license agreement - -By submitting code as an individual you agree to the [individual contributor -license agreement][individual-agreement]. - -By submitting code as an entity you agree to the [corporate contributor license -agreement][corporate-agreement]. +_This notice should stay as the first item in the CONTRIBUTING.md file._ ## Code of conduct diff --git a/vendor/Dockerfile/LICENSE b/vendor/Dockerfile/LICENSE index d6c93c6fcf7..27a215686e7 100644 --- a/vendor/Dockerfile/LICENSE +++ b/vendor/Dockerfile/LICENSE @@ -1,6 +1,6 @@ -The MIT License (MIT) +Copyright (c) 2011-2017 GitLab B.V. -Copyright (c) 2016-2017 GitLab.org +With regard to the GitLab Software: Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -9,13 +9,17 @@ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +For all third party components incorporated into the GitLab Software, those +components are licensed under the original license provided by the owner of the +applicable component. diff --git a/vendor/gitignore/Global/Matlab.gitignore b/vendor/gitignore/Global/Matlab.gitignore index cca150a88dd..7996ad5058e 100644 --- a/vendor/gitignore/Global/Matlab.gitignore +++ b/vendor/gitignore/Global/Matlab.gitignore @@ -1,5 +1,5 @@ ##--------------------------------------------------- -## Remove autosaves generated by the Matlab editor +## Remove autosaves generated by the MATLAB editor ## We have git for backups! ##--------------------------------------------------- @@ -14,6 +14,7 @@ # Simulink Code Generation slprj/ +sccprj/ # Session info octave-workspace diff --git a/vendor/gitignore/Go.gitignore b/vendor/gitignore/Go.gitignore index a1338d68517..ea58090bd21 100644 --- a/vendor/gitignore/Go.gitignore +++ b/vendor/gitignore/Go.gitignore @@ -9,6 +9,3 @@ # Output of the go coverage tool, specifically when used with LiteIDE *.out - -# Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 -.glide/ diff --git a/vendor/gitignore/Haskell.gitignore b/vendor/gitignore/Haskell.gitignore index eee88b2f0f7..82f3a88e17b 100644 --- a/vendor/gitignore/Haskell.gitignore +++ b/vendor/gitignore/Haskell.gitignore @@ -17,5 +17,6 @@ cabal.sandbox.config *.eventlog .stack-work/ cabal.project.local +cabal.project.local~ .HTF/ .ghc.environment.* diff --git a/vendor/gitignore/Jekyll.gitignore b/vendor/gitignore/Jekyll.gitignore index 5c91b60c063..2ca868298ce 100644 --- a/vendor/gitignore/Jekyll.gitignore +++ b/vendor/gitignore/Jekyll.gitignore @@ -1,3 +1,4 @@ _site/ .sass-cache/ +.jekyll-cache/ .jekyll-metadata diff --git a/vendor/gitignore/ROS.gitignore b/vendor/gitignore/ROS.gitignore index f8bcd117371..425641f2c3a 100644 --- a/vendor/gitignore/ROS.gitignore +++ b/vendor/gitignore/ROS.gitignore @@ -1,3 +1,5 @@ +devel/ +logs/ build/ bin/ lib/ diff --git a/vendor/gitignore/Symfony.gitignore b/vendor/gitignore/Symfony.gitignore index 85fd714a965..d098259ffb0 100644 --- a/vendor/gitignore/Symfony.gitignore +++ b/vendor/gitignore/Symfony.gitignore @@ -25,6 +25,7 @@ /bin/* !bin/console !bin/symfony_requirements +/vendor/ # Assets and user uploads /web/bundles/ @@ -37,6 +38,9 @@ # Build data /build/ +# Composer PHAR +/composer.phar + # Backup entities generated with doctrine:generate:entities command **/Entity/*~ diff --git a/vendor/gitignore/TeX.gitignore b/vendor/gitignore/TeX.gitignore index b6418e51766..9bb63365618 100644 --- a/vendor/gitignore/TeX.gitignore +++ b/vendor/gitignore/TeX.gitignore @@ -215,7 +215,11 @@ TSWLatexianTemp* *~[0-9]* # auto folder when using emacs and auctex -/auto/* +./auto/* +*.el # expex forward references with \gathertags *-tags.tex + +# standalone packages +*.sta diff --git a/vendor/gitignore/Unity.gitignore b/vendor/gitignore/Unity.gitignore index eb83a8f122d..75e5b1405da 100644 --- a/vendor/gitignore/Unity.gitignore +++ b/vendor/gitignore/Unity.gitignore @@ -1,9 +1,9 @@ -/[Ll]ibrary/ -/[Tt]emp/ -/[Oo]bj/ -/[Bb]uild/ -/[Bb]uilds/ -/Assets/AssetStoreTools* +[Ll]ibrary/ +[Tt]emp/ +[Oo]bj/ +[Bb]uild/ +[Bb]uilds/ +Assets/AssetStoreTools* # Visual Studio 2015 cache directory /.vs/ @@ -25,6 +25,7 @@ ExportedObj/ # Unity3D generated meta files *.pidb.meta +*.pdb.meta # Unity3D Generated File On Crash Reports sysinfo.txt diff --git a/vendor/gitignore/UnrealEngine.gitignore b/vendor/gitignore/UnrealEngine.gitignore index 6c6e1c327fd..1daca8b50d9 100644 --- a/vendor/gitignore/UnrealEngine.gitignore +++ b/vendor/gitignore/UnrealEngine.gitignore @@ -50,6 +50,7 @@ SourceArt/**/*.tga # Binary Files Binaries/* +Plugins/*/Binaries/* # Builds Build/* @@ -70,6 +71,7 @@ Saved/* # Compiled source files for the engine to use Intermediate/* +Plugins/*/Intermediate/* # Cache files for the editor to use DerivedDataCache/* diff --git a/vendor/gitignore/VisualStudio.gitignore b/vendor/gitignore/VisualStudio.gitignore index 509668db67a..6217e6c48e9 100644 --- a/vendor/gitignore/VisualStudio.gitignore +++ b/vendor/gitignore/VisualStudio.gitignore @@ -24,11 +24,14 @@ bld/ [Oo]bj/ [Ll]og/ -# Visual Studio 2015 cache/options directory +# Visual Studio 2015/2017 cache/options directory .vs/ # Uncomment if you have tasks that create the project's static files in wwwroot #wwwroot/ +# Visual Studio 2017 auto generated files +Generated\ Files/ + # MSTest test Results [Tt]est[Rr]esult*/ [Bb]uild[Ll]og.* @@ -51,6 +54,10 @@ project.fragment.lock.json artifacts/ **/Properties/launchSettings.json +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio *_i.c *_p.c *_i.h @@ -247,7 +254,7 @@ FakesAssemblies/ .ntvs_analysis.dat node_modules/ -# Typescript v1 declaration files +# TypeScript v1 declaration files typings/ # Visual Studio 6 build log @@ -303,3 +310,6 @@ __pycache__/ # OpenCover UI analysis results OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ diff --git a/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml b/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml index 88261502d7f..275487071f3 100644 --- a/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml +++ b/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml @@ -83,6 +83,16 @@ codequality: artifacts: paths: [codeclimate.json] +sast: + image: registry.gitlab.com/gitlab-org/gl-sast:latest + variables: + POSTGRES_DB: "false" + allow_failure: true + script: + - sast . + artifacts: + paths: [gl-sast-report.json] + review: stage: review script: @@ -218,8 +228,19 @@ production: --volume /var/run/docker.sock:/var/run/docker.sock \ --volume /tmp/cc:/tmp/cc" - docker run ${cc_opts} codeclimate/codeclimate init - docker run ${cc_opts} codeclimate/codeclimate analyze -f json > codeclimate.json + docker run ${cc_opts} codeclimate/codeclimate:0.69.0 init + docker run ${cc_opts} codeclimate/codeclimate:0.69.0 analyze -f json > codeclimate.json + } + + function sast() { + case "$CI_SERVER_VERSION" in + *-ee) + /app/bin/run "$@" + ;; + *) + echo "GitLab EE is required" + ;; + esac } function deploy() { @@ -345,6 +366,13 @@ production: } function build() { + + if [[ -n "$CI_REGISTRY_USER" ]]; then + echo "Logging to GitLab Container Registry with CI credentials..." + docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" "$CI_REGISTRY" + echo "" + fi + if [[ -f Dockerfile ]]; then echo "Building Dockerfile-based application..." docker build -t "$CI_APPLICATION_REPOSITORY:$CI_APPLICATION_TAG" . @@ -362,12 +390,6 @@ production: echo "" fi - if [[ -n "$CI_REGISTRY_USER" ]]; then - echo "Logging to GitLab Container Registry with CI credentials..." - docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" "$CI_REGISTRY" - echo "" - fi - echo "Pushing to GitLab Container Registry..." docker push "$CI_APPLICATION_REPOSITORY:$CI_APPLICATION_TAG" echo "" @@ -402,7 +424,9 @@ production: name="$name-$track" fi - helm delete "$name" || true + if [[ -n "$(helm ls -q "^$name$")" ]]; then + helm delete "$name" + fi } before_script: diff --git a/vendor/gitlab-ci-yml/CONTRIBUTING.md b/vendor/gitlab-ci-yml/CONTRIBUTING.md index d4c057bf9dc..d33a1f06f26 100644 --- a/vendor/gitlab-ci-yml/CONTRIBUTING.md +++ b/vendor/gitlab-ci-yml/CONTRIBUTING.md @@ -1,16 +1,15 @@ -## Contributing +## Developer Certificate of Origin + License -Thank you for your interest in contributing to this GitLab project! We welcome -all contributions. By participating in this project, you agree to abide by the -[code of conduct](#code-of-conduct). +By contributing to GitLab B.V., You accept and agree to the following terms and +conditions for Your present and future Contributions submitted to GitLab B.V. +Except for the license granted herein to GitLab B.V. and recipients of software +distributed by GitLab B.V., You reserve all right, title, and interest in and to +Your Contributions. All Contributions are subject to the following DCO + License +terms. -## Contributor license agreement +[DCO + License](https://gitlab.com/gitlab-org/dco/blob/master/README.md) -By submitting code as an individual you agree to the [individual contributor -license agreement][individual-agreement]. - -By submitting code as an entity you agree to the [corporate contributor license -agreement][corporate-agreement]. +_This notice should stay as the first item in the CONTRIBUTING.md file._ ## Code of conduct diff --git a/vendor/gitlab-ci-yml/Chef.gitlab-ci.yml b/vendor/gitlab-ci-yml/Chef.gitlab-ci.yml new file mode 100644 index 00000000000..4d5b6484d6e --- /dev/null +++ b/vendor/gitlab-ci-yml/Chef.gitlab-ci.yml @@ -0,0 +1,51 @@ +# This file uses Test Kitchen with the kitchen-dokken driver to +# perform functional testing. Doing so requires that your runner be a +# Docker runner configured for privileged mode. Please see +# https://docs.gitlab.com/runner/executors/docker.html#use-docker-in-docker-with-privileged-mode +# for help configuring your runner properly, or, if you want to switch +# to a different driver, see http://kitchen.ci/docs/drivers + +image: "chef/chefdk" +services: + - docker:dind + +variables: + DOCKER_HOST: "tcp://docker:2375" + KITCHEN_LOCAL_YAML: ".kitchen.dokken.yml" + +stages: + - lint + - unit + - functional + +foodcritic: + stage: lint + script: + - chef exec foodcritic . + +cookstyle: + stage: lint + script: + - chef exec cookstyle . + +chefspec: + stage: unit + script: + - chef exec rspec spec + +# Set up your test matrix here. Example: +#verify-centos-6: +# stage: functional +# before_script: +# - apt-get update +# - apt-get -y install rsync +# script: +# - kitchen verify default-centos-6 --destroy=always +# +#verify-centos-7: +# stage: functional +# before_script: +# - apt-get update +# - apt-get -y install rsync +# script: +# - kitchen verify default-centos-7 --destroy=always diff --git a/vendor/gitlab-ci-yml/Go.gitlab-ci.yml b/vendor/gitlab-ci-yml/Go.gitlab-ci.yml index 86e4985d8d2..d572d7a1edc 100644 --- a/vendor/gitlab-ci-yml/Go.gitlab-ci.yml +++ b/vendor/gitlab-ci-yml/Go.gitlab-ci.yml @@ -11,8 +11,8 @@ variables: # repository in /go/src/gitlab.com/namespace/project # Thus, making a symbolic link corrects this. before_script: - - mkdir -p $GOPATH/src/$REPO_NAME - - ln -svf $CI_PROJECT_DIR/* $GOPATH/src/$REPO_NAME + - mkdir -p $GOPATH/src/$(dirname $REPO_NAME) + - ln -svf $CI_PROJECT_DIR $GOPATH/src/$REPO_NAME - cd $GOPATH/src/$REPO_NAME stages: diff --git a/vendor/gitlab-ci-yml/LICENSE b/vendor/gitlab-ci-yml/LICENSE index d6c93c6fcf7..27a215686e7 100644 --- a/vendor/gitlab-ci-yml/LICENSE +++ b/vendor/gitlab-ci-yml/LICENSE @@ -1,6 +1,6 @@ -The MIT License (MIT) +Copyright (c) 2011-2017 GitLab B.V. -Copyright (c) 2016-2017 GitLab.org +With regard to the GitLab Software: Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -9,13 +9,17 @@ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +For all third party components incorporated into the GitLab Software, those +components are licensed under the original license provided by the owner of the +applicable component. diff --git a/vendor/gitlab-ci-yml/Rust.gitlab-ci.yml b/vendor/gitlab-ci-yml/Rust.gitlab-ci.yml index 6573eceaa59..1463161a04b 100644 --- a/vendor/gitlab-ci-yml/Rust.gitlab-ci.yml +++ b/vendor/gitlab-ci-yml/Rust.gitlab-ci.yml @@ -20,4 +20,4 @@ image: "rust:latest" test:cargo: script: - rustc --version && cargo --version # Print version info for debugging - - cargo test --verbose --jobs 1 --release # Don't paralize to make errors more readable + - cargo test --verbose --jobs 1 --release # Don't parallelise to make errors more readable diff --git a/vendor/licenses.csv b/vendor/licenses.csv index 6f6ca5f8b32..b6a5c2f81a0 100644 --- a/vendor/licenses.csv +++ b/vendor/licenses.csv @@ -1,460 +1,78 @@ -"","","MIT,ISC,Apache 2.0,New BSD,Simplified BSD" RedCloth,4.3.2,MIT -abbrev,1.0.9,ISC -abbrev,1.1.0,ISC -accepts,1.3.3,MIT ace-rails-ap,4.1.2,MIT -acorn,3.3.0,MIT -acorn,4.0.13,MIT -acorn,5.1.1,MIT -acorn-dynamic-import,2.0.2,MIT -acorn-jsx,3.0.1,MIT -actionmailer,4.2.8,MIT -actionpack,4.2.8,MIT -actionview,4.2.8,MIT -activejob,4.2.8,MIT -activemodel,4.2.8,MIT -activerecord,4.2.8,MIT -activesupport,4.2.8,MIT +actionmailer,4.2.10,MIT +actionpack,4.2.10,MIT +actionview,4.2.10,MIT +activejob,4.2.10,MIT +activemodel,4.2.10,MIT +activerecord,4.2.10,MIT +activesupport,4.2.10,MIT acts-as-taggable-on,4.0.0,MIT addressable,2.5.2,Apache 2.0 -after,0.8.2,MIT -ajv,4.11.8,MIT -ajv,5.2.2,MIT -ajv-keywords,1.5.1,MIT -ajv-keywords,2.1.0,MIT akismet,2.0.0,MIT -align-text,0.1.4,MIT allocations,1.0.5,MIT -alphanum-sort,1.0.2,MIT -amdefine,1.0.1,BSD-3-Clause OR MIT -ansi-escapes,1.4.0,MIT -ansi-html,0.0.5,"Apache, Version 2.0" -ansi-html,0.0.7,Apache 2.0 -ansi-regex,2.1.1,MIT -ansi-styles,2.2.1,MIT -ansi-styles,3.2.0,MIT -anymatch,1.3.2,ISC -append-transform,0.4.0,MIT -aproba,1.1.1,ISC -are-we-there-yet,1.1.4,ISC arel,6.0.4,MIT -argparse,1.0.9,MIT -arr-diff,2.0.0,MIT -arr-flatten,1.0.1,MIT -array-find,1.0.0,MIT -array-find-index,1.0.2,MIT -array-flatten,1.1.1,MIT -array-flatten,2.1.1,MIT -array-slice,0.2.3,MIT -array-union,1.0.2,MIT -array-uniq,1.0.3,MIT -array-unique,0.2.1,MIT -arraybuffer.slice,0.0.6,MIT -arrify,1.0.1,MIT asana,0.6.0,MIT asciidoctor,1.5.3,MIT asciidoctor-plantuml,0.0.7,MIT -asn1,0.2.3,MIT -asn1.js,4.9.1,MIT -assert,1.4.1,MIT -assert-plus,0.2.0,MIT -assert-plus,1.0.0,MIT -async,0.9.2,MIT -async,1.5.2,MIT -async,2.4.1,MIT -async-each,1.0.1,MIT -asynckit,0.4.0,MIT +asset_sync,2.2.0,MIT atomic,1.1.99,Apache 2.0 attr_encrypted,3.0.3,MIT attr_required,1.0.0,MIT -autoprefixer,6.7.7,MIT autoprefixer-rails,6.2.3,MIT -autosize,4.0.0,MIT -aws-sign2,0.6.0,Apache 2.0 -aws4,1.6.0,MIT axiom-types,0.1.1,MIT -axios,0.16.2,MIT -babel-code-frame,6.22.0,MIT -babel-core,6.23.1,MIT -babel-eslint,7.2.1,MIT -babel-generator,6.23.0,MIT -babel-helper-bindify-decorators,6.22.0,MIT -babel-helper-builder-binary-assignment-operator-visitor,6.22.0,MIT -babel-helper-call-delegate,6.22.0,MIT -babel-helper-define-map,6.23.0,MIT -babel-helper-explode-assignable-expression,6.22.0,MIT -babel-helper-explode-class,6.22.0,MIT -babel-helper-function-name,6.23.0,MIT -babel-helper-get-function-arity,6.22.0,MIT -babel-helper-hoist-variables,6.22.0,MIT -babel-helper-optimise-call-expression,6.23.0,MIT -babel-helper-regex,6.22.0,MIT -babel-helper-remap-async-to-generator,6.22.0,MIT -babel-helper-replace-supers,6.23.0,MIT -babel-helpers,6.23.0,MIT -babel-loader,7.1.1,MIT -babel-messages,6.23.0,MIT -babel-plugin-check-es2015-constants,6.22.0,MIT -babel-plugin-istanbul,4.0.0,New BSD -babel-plugin-syntax-async-functions,6.13.0,MIT -babel-plugin-syntax-async-generators,6.13.0,MIT -babel-plugin-syntax-class-properties,6.13.0,MIT -babel-plugin-syntax-decorators,6.13.0,MIT -babel-plugin-syntax-dynamic-import,6.18.0,MIT -babel-plugin-syntax-exponentiation-operator,6.13.0,MIT -babel-plugin-syntax-object-rest-spread,6.13.0,MIT -babel-plugin-syntax-trailing-function-commas,6.22.0,MIT -babel-plugin-transform-async-generator-functions,6.22.0,MIT -babel-plugin-transform-async-to-generator,6.22.0,MIT -babel-plugin-transform-class-properties,6.23.0,MIT -babel-plugin-transform-decorators,6.22.0,MIT -babel-plugin-transform-define,1.2.0,MIT -babel-plugin-transform-es2015-arrow-functions,6.22.0,MIT -babel-plugin-transform-es2015-block-scoped-functions,6.22.0,MIT -babel-plugin-transform-es2015-block-scoping,6.23.0,MIT -babel-plugin-transform-es2015-classes,6.23.0,MIT -babel-plugin-transform-es2015-computed-properties,6.22.0,MIT -babel-plugin-transform-es2015-destructuring,6.23.0,MIT -babel-plugin-transform-es2015-duplicate-keys,6.22.0,MIT -babel-plugin-transform-es2015-for-of,6.23.0,MIT -babel-plugin-transform-es2015-function-name,6.22.0,MIT -babel-plugin-transform-es2015-literals,6.22.0,MIT -babel-plugin-transform-es2015-modules-amd,6.24.0,MIT -babel-plugin-transform-es2015-modules-commonjs,6.24.0,MIT -babel-plugin-transform-es2015-modules-systemjs,6.23.0,MIT -babel-plugin-transform-es2015-modules-umd,6.24.0,MIT -babel-plugin-transform-es2015-object-super,6.22.0,MIT -babel-plugin-transform-es2015-parameters,6.23.0,MIT -babel-plugin-transform-es2015-shorthand-properties,6.22.0,MIT -babel-plugin-transform-es2015-spread,6.22.0,MIT -babel-plugin-transform-es2015-sticky-regex,6.22.0,MIT -babel-plugin-transform-es2015-template-literals,6.22.0,MIT -babel-plugin-transform-es2015-typeof-symbol,6.23.0,MIT -babel-plugin-transform-es2015-unicode-regex,6.22.0,MIT -babel-plugin-transform-exponentiation-operator,6.22.0,MIT -babel-plugin-transform-object-rest-spread,6.23.0,MIT -babel-plugin-transform-regenerator,6.22.0,MIT -babel-plugin-transform-strict-mode,6.22.0,MIT -babel-preset-es2015,6.24.0,MIT -babel-preset-es2016,6.22.0,MIT -babel-preset-es2017,6.22.0,MIT -babel-preset-latest,6.24.0,MIT -babel-preset-stage-2,6.22.0,MIT -babel-preset-stage-3,6.22.0,MIT -babel-register,6.23.0,MIT -babel-runtime,6.22.0,MIT -babel-template,6.23.0,MIT -babel-traverse,6.23.1,MIT -babel-types,6.23.0,MIT babosa,1.0.2,MIT -babylon,6.16.1,MIT -backo2,1.0.2,MIT -balanced-match,0.4.2,MIT -balanced-match,1.0.0,MIT base32,0.3.2,MIT -base64-arraybuffer,0.1.5,MIT -base64-js,1.2.0,MIT -base64id,1.0.0,MIT -batch,0.6.1,MIT +batch-loader,1.1.1,MIT bcrypt,3.1.11,MIT -bcrypt-pbkdf,1.0.1,New BSD bcrypt_pbkdf,1.0.0,MIT -better-assert,1.0.2,MIT -big.js,3.1.3,MIT -binary-extensions,1.10.0,MIT bindata,2.4.1,ruby -blob,0.0.4,unknown -block-stream,0.0.9,ISC -bluebird,2.11.0,MIT -bluebird,3.5.0,MIT -bn.js,4.11.6,MIT -body-parser,1.17.2,MIT -bonjour,3.5.0,MIT -boom,2.10.1,New BSD bootstrap-sass,3.3.6,MIT bootstrap_form,2.7.0,MIT -brace-expansion,1.1.7,MIT -brace-expansion,1.1.8,MIT -braces,0.1.5,MIT -braces,1.8.5,MIT -brorand,1.0.7,MIT browser,2.2.0,MIT -browserify-aes,1.0.6,MIT -browserify-cipher,1.0.0,MIT -browserify-des,1.0.0,MIT -browserify-rsa,4.0.1,MIT -browserify-sign,4.0.0,ISC -browserify-zlib,0.1.4,MIT -browserslist,1.7.7,MIT -buffer,4.9.1,MIT -buffer-indexof,1.1.0,MIT -buffer-shims,1.0.0,MIT -buffer-xor,1.0.3,MIT builder,3.2.3,MIT -builtin-modules,1.1.1,MIT -builtin-status-codes,3.0.0,MIT -bytes,2.4.0,MIT -bytes,2.5.0,MIT -caller-path,0.1.0,MIT -callsite,1.0.0,unknown -callsites,0.2.0,MIT -camelcase,1.2.1,MIT -camelcase,2.1.1,MIT -camelcase,3.0.0,MIT -camelcase,4.1.0,MIT -camelcase-keys,2.1.0,MIT -caniuse-api,1.6.1,MIT -caniuse-db,1.0.30000649,CC-BY-4.0 carrierwave,1.2.1,MIT -caseless,0.12.0,Apache 2.0 cause,0.1,MIT -center-align,0.1.3,MIT -chalk,1.1.3,MIT -chalk,2.3.0,MIT charlock_holmes,0.7.5,MIT -chokidar,1.7.0,MIT chronic,0.10.2,MIT chronic_duration,0.10.6,MIT chunky_png,1.3.5,MIT -cipher-base,1.0.3,MIT -circular-json,0.3.3,MIT citrus,3.0.2,MIT -clap,1.1.3,MIT -cli-cursor,1.0.2,MIT -cli-width,2.1.0,ISC -clipboard,1.6.1,MIT -cliui,2.1.0,ISC -cliui,3.2.0,ISC -clone,1.0.2,MIT -co,4.6.0,MIT -coa,1.0.1,MIT -code-point-at,1.1.0,MIT coercible,1.0.0,MIT -color,0.11.4,MIT -color-convert,1.9.0,MIT -color-name,1.1.2,MIT -color-string,0.3.0,MIT -colormin,1.1.2,MIT -colors,1.1.2,MIT -combine-lists,1.0.1,MIT -combined-stream,1.0.5,MIT -commander,2.9.0,MIT -commondir,1.0.1,MIT -component-bind,1.0.0,unknown -component-emitter,1.1.2,unknown -component-emitter,1.2.1,MIT -component-inherit,0.0.3,unknown -compressible,2.0.11,MIT -compression,1.7.0,MIT -compression-webpack-plugin,1.0.0,MIT -concat-map,0.0.1,MIT -concat-stream,1.6.0,MIT concurrent-ruby-ext,1.0.5,MIT -config-chain,1.1.11,MIT -configstore,1.4.0,Simplified BSD -connect,3.6.3,MIT -connect-history-api-fallback,1.3.0,MIT connection_pool,2.2.1,MIT -console-browserify,1.1.0,MIT -console-control-strings,1.1.0,ISC -consolidate,0.14.5,MIT -constants-browserify,1.0.0,MIT -contains-path,0.1.0,MIT -content-disposition,0.5.2,MIT -content-type,1.0.2,MIT -convert-source-map,1.3.0,MIT -cookie,0.3.1,MIT -cookie-signature,1.0.6,MIT -copy-webpack-plugin,4.0.1,MIT -core-js,2.3.0,MIT -core-js,2.4.1,MIT -core-util-is,1.0.2,MIT -cosmiconfig,2.1.1,MIT crack,0.4.3,MIT -create-ecdh,4.0.0,MIT -create-hash,1.1.2,MIT -create-hmac,1.1.4,MIT creole,0.5.0,ruby -cropper,2.3.0,MIT -cross-spawn,5.1.0,MIT -cryptiles,2.0.5,New BSD -crypto-browserify,3.11.0,MIT -css-color-names,0.0.4,MIT -css-loader,0.28.0,MIT -css-selector-tokenizer,0.6.0,MIT -css-selector-tokenizer,0.7.0,MIT css_parser,1.5.0,MIT -cssesc,0.1.0,MIT -cssnano,3.10.0,MIT -csso,2.3.2,MIT -currently-unhandled,0.4.1,MIT -custom-event,1.0.1,MIT -d,0.1.1,MIT -d,1.0.0,MIT -d3,3.5.11,New BSD d3_rails,3.5.11,MIT -dashdash,1.14.1,MIT -date-now,0.1.4,MIT -de-indent,1.0.2,MIT -debug,2.2.0,MIT -debug,2.3.3,MIT -debug,2.6.7,MIT -debug,2.6.8,MIT debugger-ruby_core_source,1.3.8,MIT -decamelize,1.2.0,MIT deckar01-task_list,2.0.0,MIT declarative,0.0.10,MIT declarative-option,0.1.0,MIT -decompress-response,3.3.0,MIT -deep-equal,1.0.1,MIT -deep-extend,0.4.2,MIT -deep-is,0.1.3,MIT -default-require-extensions,1.0.0,MIT default_value_for,3.0.2,MIT -defined,1.0.0,MIT -del,2.2.2,MIT -del,3.0.0,MIT -delayed-stream,1.0.0,MIT -delegate,3.1.2,MIT -delegates,1.0.0,MIT -depd,1.1.0,MIT -depd,1.1.1,MIT -des.js,1.0.0,MIT descendants_tracker,0.0.4,MIT -destroy,1.0.4,MIT -detect-indent,4.0.0,MIT -detect-node,2.0.3,ISC devise,4.2.0,MIT devise-two-factor,3.0.0,MIT -di,0.0.1,MIT diff-lcs,1.3,"MIT,Artistic-2.0,GPL-2.0+" -diffie-hellman,5.0.2,MIT diffy,3.1.0,MIT -dns-equal,1.0.0,MIT -dns-packet,1.2.2,MIT -dns-txt,2.0.2,MIT -doctrine,1.5.0,BSD -doctrine,2.0.0,Apache 2.0 -document-register-element,1.3.0,MIT -dom-serialize,2.2.1,MIT -dom-serializer,0.1.0,MIT -domain-browser,1.1.7,MIT domain_name,0.5.20161021,"Simplified BSD,New BSD,Mozilla Public License 2.0" -domelementtype,1.1.3,unknown -domelementtype,1.3.0,unknown -domhandler,2.3.0,unknown -domutils,1.5.1,unknown doorkeeper,4.2.6,MIT doorkeeper-openid_connect,1.2.0,MIT -dropzone,4.2.0,MIT dropzonejs-rails,0.7.2,MIT -duplexer,0.1.1,MIT -duplexer3,0.1.4,New BSD -duplexify,3.5.1,MIT -ecc-jsbn,0.1.1,MIT -editorconfig,0.13.2,MIT -ee-first,1.1.1,MIT -ejs,2.5.6,Apache 2.0 -electron-to-chromium,1.3.3,ISC -elliptic,6.3.3,MIT email_reply_trimmer,0.1.6,MIT -emoji-unicode-version,0.2.1,MIT -emojis-list,2.1.0,MIT -encodeurl,1.0.1,MIT encryptor,3.0.0,MIT -end-of-stream,1.4.0,MIT -engine.io,1.8.3,MIT -engine.io-client,1.8.3,MIT -engine.io-parser,1.3.2,MIT -enhanced-resolve,0.9.1,MIT -enhanced-resolve,3.4.1,MIT -ent,2.2.0,MIT -entities,1.1.1,BSD-like equalizer,0.0.11,MIT -errno,0.1.4,MIT -error-ex,1.3.0,MIT erubis,2.7.0,MIT -es5-ext,0.10.24,MIT -es6-iterator,2.0.1,MIT -es6-map,0.1.5,MIT -es6-promise,3.0.2,MIT -es6-set,0.1.5,MIT -es6-symbol,3.1.1,MIT -es6-weak-map,2.0.1,MIT -escape-html,1.0.3,MIT -escape-string-regexp,1.0.5,MIT escape_utils,1.1.1,MIT -escodegen,1.8.1,Simplified BSD -escope,3.6.0,Simplified BSD -eslint,3.19.0,MIT -eslint-config-airbnb-base,10.0.1,MIT -eslint-import-resolver-node,0.2.3,MIT -eslint-import-resolver-webpack,0.8.3,MIT -eslint-module-utils,2.0.0,MIT -eslint-plugin-filenames,1.1.0,MIT -eslint-plugin-html,2.0.1,ISC -eslint-plugin-import,2.2.0,MIT -eslint-plugin-jasmine,2.2.0,MIT -eslint-plugin-promise,3.5.0,ISC -espree,3.5.0,Simplified BSD -esprima,2.7.3,Simplified BSD -esprima,4.0.0,Simplified BSD -esquery,1.0.0,BSD -esrecurse,4.1.0,Simplified BSD -estraverse,1.9.3,BSD -estraverse,4.1.1,Simplified BSD -estraverse,4.2.0,Simplified BSD -esutils,2.0.2,BSD et-orbi,1.0.3,MIT -etag,1.8.0,MIT -eve-raphael,0.5.0,Apache 2.0 -event-emitter,0.3.5,MIT -event-stream,3.3.4,MIT -eventemitter3,1.2.0,MIT -events,1.1.1,MIT -eventsource,0.1.6,MIT -evp_bytestokey,1.0.0,MIT excon,0.57.1,MIT -execa,0.7.0,MIT execjs,2.6.0,MIT -exit-hook,1.1.1,MIT -expand-braces,0.1.2,MIT -expand-brackets,0.1.5,MIT -expand-range,0.1.1,MIT -expand-range,1.8.2,MIT -exports-loader,0.6.4,MIT -express,4.15.4,MIT expression_parser,0.9.0,MIT -extend,3.0.1,MIT -extglob,0.3.2,MIT -extsprintf,1.0.2,MIT faraday,0.12.2,MIT faraday_middleware,0.11.0.1,MIT faraday_middleware-multi_json,0.0.6,MIT -fast-deep-equal,1.0.0,MIT -fast-levenshtein,2.0.6,MIT fast_gettext,1.4.0,"MIT,ruby" -fastparse,1.1.1,MIT -faye-websocket,0.10.0,MIT -faye-websocket,0.11.1,MIT -faye-websocket,0.7.3,MIT ffi,1.9.18,New BSD -figures,1.7.0,MIT -file-entry-cache,2.0.0,MIT -file-loader,0.11.1,MIT -filename-regex,2.0.0,MIT -fileset,2.0.3,MIT -filesize,3.3.0,New BSD -filesize,3.5.10,New BSD -fill-range,2.2.3,MIT -finalhandler,1.0.4,MIT -find-cache-dir,1.0.0,MIT -find-root,0.1.2,MIT -find-up,1.1.2,MIT -find-up,2.1.0,MIT -flat-cache,1.2.2,MIT -flatten,1.0.2,MIT flipper,0.10.2,MIT flipper-active_record,0.10.2,MIT flowdock,0.7.1,MIT @@ -467,392 +85,93 @@ fog-local,0.3.1,MIT fog-openstack,0.1.21,MIT fog-rackspace,0.1.1,MIT fog-xml,0.1.3,MIT -follow-redirects,1.2.3,MIT font-awesome-rails,4.7.0.1,"MIT,SIL Open Font License" -for-in,0.1.6,MIT -for-own,0.1.4,MIT -forever-agent,0.6.1,Apache 2.0 -form-data,2.1.4,MIT formatador,0.2.5,MIT -forwarded,0.1.0,MIT -fresh,0.5.0,MIT -from,0.1.7,MIT -fs-access,1.0.1,MIT -fs-extra,0.26.7,MIT -fs.realpath,1.0.0,ISC -fsevents,1.1.2,MIT -fstream,1.0.11,ISC -fstream-ignore,1.0.5,ISC -function-bind,1.1.0,MIT -fuzzaldrin-plus,0.5.0,MIT -gauge,2.7.4,ISC gemnasium-gitlab-service,0.2.6,MIT gemojione,3.3.0,MIT -generate-function,2.0.0,MIT -generate-object-property,1.2.0,MIT -get-caller-file,1.0.2,ISC -get-stdin,4.0.1,MIT -get-stream,3.0.0,MIT get_process_mem,0.2.0,MIT -getpass,0.1.7,MIT gettext_i18n_rails,1.8.0,MIT gettext_i18n_rails_js,1.2.0,MIT -gitaly-proto,0.51.0,MIT +gitaly-proto,0.59.0,MIT github-linguist,4.7.6,MIT github-markup,1.6.1,MIT gitlab-flowdock-git-hook,1.0.1,MIT gitlab-grit,2.8.2,MIT gitlab-markup,1.6.3,MIT -gitlab-svgs,1.0.4,unknown gitlab_omniauth-ldap,2.0.4,MIT -glob,5.0.15,ISC -glob,6.0.4,ISC -glob,7.1.1,ISC -glob,7.1.2,ISC -glob-base,0.3.0,MIT -glob-parent,2.0.0,ISC -globalid,0.3.7,MIT -globals,9.18.0,MIT -globby,5.0.0,MIT -globby,6.1.0,MIT +globalid,0.4.1,MIT gollum-grit_adapter,1.0.1,MIT gollum-lib,4.2.7,MIT gollum-rugged_adapter,0.4.4,MIT gon,6.1.0,MIT -good-listener,1.2.2,MIT google-api-client,0.13.6,Apache 2.0 google-protobuf,3.4.1.1,New BSD -googleapis-common-protos-types,1.0.0,Apache 2.0 googleauth,0.5.3,Apache 2.0 -got,3.3.1,MIT -got,7.1.0,MIT gpgme,2.0.13,LGPL-2.1+ -graceful-fs,4.1.11,ISC -graceful-readlink,1.0.1,MIT grape,1.0.0,MIT grape-entity,0.6.0,MIT grape-route-helpers,2.1.0,MIT grape_logging,1.7.0,MIT -grpc,1.6.6,Apache 2.0 -gzip-size,3.0.0,MIT +grpc,1.4.5,New BSD hamlit,2.6.1,MIT -handle-thing,1.2.5,MIT -handlebars,4.0.6,MIT -har-schema,1.0.5,ISC -har-validator,4.2.1,ISC -has,1.0.1,MIT -has-ansi,2.0.0,MIT -has-binary,0.1.7,MIT -has-cors,1.1.0,MIT -has-flag,1.0.0,MIT -has-flag,2.0.0,MIT -has-symbol-support-x,1.3.0,MIT -has-to-string-tag-x,1.3.0,MIT -has-unicode,2.0.1,ISC -hash-sum,1.0.2,MIT -hash.js,1.0.3,MIT hashie,3.5.6,MIT hashie-forbidden_attributes,0.1.1,MIT -hawk,3.1.3,New BSD -he,1.1.1,MIT health_check,2.6.0,MIT hipchat,1.5.2,MIT -hoek,2.16.3,New BSD -home-or-tmp,2.0.0,MIT -hosted-git-info,2.2.0,ISC -hpack.js,2.1.6,MIT -html-comment-regex,1.1.1,MIT -html-entities,1.2.0,MIT html-pipeline,1.11.0,MIT html2text,0.2.0,MIT htmlentities,4.3.4,MIT -htmlparser2,3.9.2,MIT http,0.9.8,MIT http-cookie,1.0.3,MIT -http-deceiver,1.2.7,MIT -http-errors,1.6.1,MIT -http-errors,1.6.2,MIT http-form_data,1.0.1,MIT -http-proxy,1.16.2,MIT -http-proxy-middleware,0.17.4,MIT -http-signature,1.1.1,MIT http_parser.rb,0.6.0,MIT httparty,0.13.7,MIT httpclient,2.8.2,ruby -https-browserify,0.0.1,MIT -i18n,0.8.6,MIT +i18n,0.9.1,MIT ice_nine,0.11.2,MIT -iconv-lite,0.4.15,MIT -icss-replace-symbols,1.0.2,ISC -ieee754,1.1.8,New BSD -ignore,3.3.3,MIT -ignore-by-default,1.0.1,ISC -immediate,3.0.6,MIT -imports-loader,0.7.1,MIT -imurmurhash,0.1.4,MIT -indent-string,2.1.0,MIT -indexes-of,1.0.1,MIT -indexof,0.0.1,unknown -infinity-agent,2.0.3,MIT -inflight,1.0.6,ISC influxdb,0.2.3,MIT -inherits,2.0.1,ISC -inherits,2.0.3,ISC -ini,1.3.4,ISC -inquirer,0.12.0,MIT -internal-ip,1.2.0,MIT -interpret,1.0.1,MIT -invariant,2.2.2,New BSD -invert-kv,1.0.0,MIT -ip,1.1.5,MIT -ipaddr.js,1.4.0,MIT ipaddress,0.8.3,MIT -is-absolute,0.2.6,MIT -is-absolute-url,2.1.0,MIT -is-arrayish,0.2.1,MIT -is-binary-path,1.0.1,MIT -is-buffer,1.1.5,MIT -is-builtin-module,1.0.0,MIT -is-dotfile,1.0.2,MIT -is-equal-shallow,0.1.3,MIT -is-extendable,0.1.1,MIT -is-extglob,1.0.0,MIT -is-extglob,2.1.1,MIT -is-finite,1.0.2,MIT -is-fullwidth-code-point,1.0.0,MIT -is-fullwidth-code-point,2.0.0,MIT -is-glob,2.0.1,MIT -is-glob,3.1.0,MIT -is-my-json-valid,2.16.0,MIT -is-npm,1.0.0,MIT -is-number,0.1.1,MIT -is-number,2.1.0,MIT -is-object,1.0.1,MIT -is-path-cwd,1.0.0,MIT -is-path-in-cwd,1.0.0,MIT -is-path-inside,1.0.0,MIT -is-plain-obj,1.1.0,MIT -is-posix-bracket,0.1.1,MIT -is-primitive,2.0.0,MIT -is-property,1.0.2,MIT -is-redirect,1.0.0,MIT -is-relative,0.2.1,MIT -is-resolvable,1.0.0,MIT -is-retry-allowed,1.1.0,MIT -is-stream,1.1.0,MIT -is-svg,2.1.0,MIT -is-typedarray,1.0.0,MIT -is-unc-path,0.1.2,MIT -is-utf8,0.2.1,MIT -is-windows,0.2.0,MIT -isarray,0.0.1,MIT -isarray,1.0.0,MIT -isbinaryfile,3.0.2,MIT -isexe,1.1.2,ISC -isobject,2.1.0,MIT -isstream,0.1.2,MIT -istanbul,0.4.5,New BSD -istanbul-api,1.1.1,New BSD -istanbul-lib-coverage,1.0.1,New BSD -istanbul-lib-hook,1.0.0,New BSD -istanbul-lib-instrument,1.4.2,New BSD -istanbul-lib-report,1.0.0-alpha.3,New BSD -istanbul-lib-source-maps,1.1.0,New BSD -istanbul-reports,1.0.1,New BSD -isurl,1.0.0,MIT -jasmine-core,2.6.3,MIT -jasmine-jquery,2.1.1,MIT -jed,1.1.1,MIT jira-ruby,1.4.1,MIT -jodid25519,1.0.2,MIT -jquery,2.2.1,MIT jquery-atwho-rails,1.3.2,MIT -jquery-rails,4.1.1,MIT -jquery-ujs,1.2.1,MIT -js-base64,2.1.9,BSD -js-beautify,1.6.12,MIT -js-cookie,2.1.3,MIT -js-tokens,3.0.1,MIT -js-yaml,3.7.0,MIT -js-yaml,3.9.1,MIT -jsbn,0.1.1,MIT -jsesc,0.5.0,MIT -jsesc,1.3.0,MIT +jquery-rails,4.3.1,MIT json,1.8.6,ruby json-jwt,1.7.2,MIT -json-loader,0.5.7,MIT -json-schema,0.2.3,"AFLv2.1,BSD" -json-schema-traverse,0.3.1,MIT -json-stable-stringify,1.0.1,MIT -json-stringify-safe,5.0.1,ISC -json3,3.3.2,MIT -json5,0.5.1,MIT -jsonfile,2.4.0,MIT -jsonify,0.0.0,Public Domain -jsonpointer,4.0.1,MIT -jsprim,1.4.0,MIT -jszip,3.1.3,(MIT OR GPL-3.0) -jszip-utils,0.0.2,MIT or GPLv3 jwt,1.5.6,MIT kaminari,1.0.1,MIT kaminari-actionview,1.0.1,MIT kaminari-activerecord,1.0.1,MIT kaminari-core,1.0.1,MIT -karma,1.7.0,MIT -karma-chrome-launcher,2.1.1,MIT -karma-coverage-istanbul-reporter,0.2.0,MIT -karma-jasmine,1.1.0,MIT -karma-mocha-reporter,2.2.2,MIT -karma-sourcemap-loader,0.3.7,MIT -karma-webpack,2.0.4,MIT kgio,2.10.0,LGPL-2.1+ -kind-of,3.1.0,MIT -klaw,1.3.1,MIT kubeclient,2.2.0,MIT -latest-version,1.0.1,MIT -lazy-cache,1.0.4,MIT -lcid,1.0.0,MIT -levn,0.3.0,MIT licensee,8.7.0,MIT -lie,3.1.1,MIT little-plugger,1.1.4,MIT -load-json-file,1.1.0,MIT -load-json-file,2.0.0,MIT -loader-runner,2.3.0,MIT -loader-utils,0.2.16,MIT -loader-utils,1.1.0,MIT locale,2.1.2,"ruby,LGPLv3+" -locate-path,2.0.0,MIT -lodash,3.10.1,MIT -lodash,4.17.4,MIT -lodash._baseassign,3.2.0,MIT -lodash._basecopy,3.0.1,MIT -lodash._baseget,3.7.2,MIT -lodash._bindcallback,3.0.1,MIT -lodash._createassigner,3.1.1,MIT -lodash._getnative,3.9.1,MIT -lodash._isiterateecall,3.0.9,MIT -lodash._topath,3.8.1,MIT -lodash.assign,3.2.0,MIT -lodash.camelcase,4.1.1,MIT -lodash.camelcase,4.3.0,MIT -lodash.capitalize,4.2.1,MIT -lodash.cond,4.5.2,MIT -lodash.deburr,4.1.0,MIT -lodash.defaults,3.1.2,MIT -lodash.get,3.7.0,MIT -lodash.get,4.4.2,MIT -lodash.isarguments,3.1.0,MIT -lodash.isarray,3.0.4,MIT -lodash.kebabcase,4.0.1,MIT -lodash.keys,3.1.2,MIT -lodash.memoize,4.1.2,MIT -lodash.restparam,3.6.1,MIT -lodash.snakecase,4.0.1,MIT -lodash.uniq,4.5.0,MIT -lodash.words,4.2.0,MIT -log4js,0.6.38,Apache 2.0 logging,2.2.2,MIT -loglevel,1.4.1,MIT lograge,0.5.1,MIT -longest,1.0.1,MIT loofah,2.0.3,MIT -loose-envify,1.3.1,MIT -loud-rejection,1.6.0,MIT -lowercase-keys,1.0.0,MIT -lru-cache,2.2.4,MIT -lru-cache,3.2.0,ISC -lru-cache,4.0.2,ISC -macaddress,0.2.8,MIT -mail,2.6.6,MIT +mail,2.7.0,MIT mail_room,0.9.1,MIT -make-dir,1.0.0,MIT -map-obj,1.0.1,MIT -map-stream,0.1.0,unknown -marked,0.3.6,MIT -math-expression-evaluator,1.2.16,MIT -media-typer,0.3.0,MIT -mem,1.1.0,MIT memoist,0.16.0,MIT -memory-fs,0.2.0,MIT -memory-fs,0.4.1,MIT -meow,3.7.0,MIT -merge-descriptors,1.0.1,MIT method_source,0.8.2,MIT -methods,1.1.2,MIT -micromatch,2.3.11,MIT -miller-rabin,4.0.0,MIT -mime,1.3.4,MIT -mime-db,1.27.0,MIT -mime-db,1.29.0,MIT -mime-types,2.1.15,MIT mime-types,3.1,MIT mime-types-data,3.2016.0521,MIT mimemagic,0.3.0,MIT -mimic-fn,1.1.0,MIT -mimic-response,1.0.0,MIT +mini_mime,0.1.4,MIT mini_portile2,2.3.0,MIT -minimalistic-assert,1.0.0,ISC -minimatch,3.0.3,ISC -minimatch,3.0.4,ISC -minimist,0.0.8,MIT -minimist,1.2.0,MIT -mkdirp,0.5.1,MIT -mmap2,2.2.7,ruby -moment,2.17.1,MIT -monaco-editor,0.10.0,MIT -mousetrap,1.4.6,Apache 2.0 mousetrap-rails,1.4.6,"MIT,Apache" -ms,0.7.1,MIT -ms,0.7.2,MIT -ms,2.0.0,MIT multi_json,1.12.2,MIT multi_xml,0.6.0,MIT -multicast-dns,6.1.1,MIT -multicast-dns-service-types,1.1.0,MIT multipart-post,2.0.0,MIT mustermann,1.0.0,MIT mustermann-grape,1.0.0,MIT -mute-stream,0.0.5,ISC mysql2,0.4.5,MIT -name-all-modules-plugin,1.0.1,MIT -nan,2.6.2,MIT -natural-compare,1.4.0,MIT -negotiator,0.6.1,MIT -nested-error-stacks,1.0.2,MIT net-ldap,0.16.0,MIT net-ssh,4.1.0,MIT netrc,0.11.0,MIT -node-dir,0.1.17,MIT -node-forge,0.6.33,BSD -node-libs-browser,1.1.1,MIT -node-libs-browser,2.0.0,MIT -node-pre-gyp,0.6.36,New BSD -node-pre-gyp,0.6.37,New BSD -nodemon,1.11.0,MIT nokogiri,1.8.1,MIT -nopt,1.0.10,MIT -nopt,3.0.6,ISC -nopt,4.0.1,ISC -normalize-package-data,2.4.0,Simplified BSD -normalize-path,2.1.1,MIT -normalize-range,0.1.2,MIT -normalize-url,1.9.1,MIT -npm-run-path,2.0.2,MIT -npmlog,4.1.0,ISC -null-check,1.0.0,MIT -num2fraction,1.2.2,MIT -number-is-nan,1.0.1,MIT numerizer,0.1.1,MIT oauth,0.5.1,MIT -oauth-sign,0.8.2,Apache 2.0 oauth2,1.4.0,MIT -object-assign,3.0.0,MIT -object-assign,4.1.0,MIT -object-assign,4.1.1,MIT -object-component,0.0.3,unknown -object.omit,2.0.1,MIT -obuf,1.1.1,MIT octokit,4.6.2,MIT oj,2.17.5,MIT omniauth,1.4.2,MIT @@ -873,54 +192,10 @@ omniauth-saml,1.7.0,MIT omniauth-shibboleth,1.2.1,MIT omniauth-twitter,1.2.1,MIT omniauth_crowd,2.2.3,MIT -on-finished,2.3.0,MIT -on-headers,1.0.1,MIT -once,1.4.0,ISC -onetime,1.1.0,MIT -opener,1.4.3,(WTFPL OR MIT) -opn,4.0.2,MIT -optimist,0.6.1,MIT/X11 -optionator,0.8.2,MIT -options,0.0.6,MIT org-ruby,0.9.12,MIT -original,1.0.0,MIT orm_adapter,0.5.0,MIT os,0.9.6,MIT -os-browserify,0.2.1,MIT -os-homedir,1.0.2,MIT -os-locale,1.4.0,MIT -os-locale,2.1.0,MIT -os-tmpdir,1.0.2,MIT -osenv,0.1.4,ISC -p-cancelable,0.3.0,MIT -p-finally,1.0.0,MIT -p-limit,1.1.0,MIT -p-locate,2.0.0,MIT -p-map,1.1.1,MIT -p-timeout,1.2.0,MIT -package-json,1.2.0,MIT -pako,0.2.9,MIT -pako,1.0.5,(MIT AND Zlib) paranoia,2.3.1,MIT -parse-asn1,5.0.0,ISC -parse-glob,3.0.4,MIT -parse-json,2.2.0,MIT -parsejson,0.0.3,MIT -parseqs,0.0.5,MIT -parseuri,0.0.5,MIT -parseurl,1.3.1,MIT -path-browserify,0.0.0,MIT -path-exists,2.1.0,MIT -path-exists,3.0.0,MIT -path-is-absolute,1.0.1,MIT -path-is-inside,1.0.2,(WTFPL OR MIT) -path-key,2.0.1,MIT -path-parse,1.0.5,MIT -path-to-regexp,0.1.7,MIT -path-type,1.1.0,MIT -path-type,2.0.0,MIT -pause-stream,0.0.11,"MIT,Apache2" -pbkdf2,3.0.9,MIT peek,1.0.1,MIT peek-gc,0.0.2,MIT peek-host,1.0.0,MIT @@ -930,86 +205,14 @@ peek-pg,1.3.0,MIT peek-rblineprof,0.2.0,MIT peek-redis,1.2.0,MIT peek-sidekiq,1.0.3,MIT -performance-now,0.2.0,MIT pg,0.18.4,"BSD,ruby,GPL" -pify,2.3.0,MIT -pify,3.0.0,MIT -pikaday,1.6.1,MIT -pinkie,2.0.4,MIT -pinkie-promise,2.0.1,MIT -pkg-dir,1.0.0,MIT -pkg-dir,2.0.0,MIT -pkg-up,1.0.0,MIT -pluralize,1.2.1,MIT po_to_json,1.0.1,MIT -portfinder,1.0.13,MIT posix-spawn,0.3.13,MIT -postcss,5.2.16,MIT -postcss-calc,5.3.1,MIT -postcss-colormin,2.2.2,MIT -postcss-convert-values,2.6.1,MIT -postcss-discard-comments,2.0.4,MIT -postcss-discard-duplicates,2.1.0,MIT -postcss-discard-empty,2.1.0,MIT -postcss-discard-overridden,0.1.1,MIT -postcss-discard-unused,2.2.3,MIT -postcss-filter-plugins,2.0.2,MIT -postcss-load-config,1.2.0,MIT -postcss-load-options,1.2.0,MIT -postcss-load-plugins,2.3.0,MIT -postcss-merge-idents,2.1.7,MIT -postcss-merge-longhand,2.0.2,MIT -postcss-merge-rules,2.1.2,MIT -postcss-message-helpers,2.0.0,MIT -postcss-minify-font-values,1.0.5,MIT -postcss-minify-gradients,1.0.5,MIT -postcss-minify-params,1.2.2,MIT -postcss-minify-selectors,2.1.1,MIT -postcss-modules-extract-imports,1.0.1,ISC -postcss-modules-local-by-default,1.1.1,MIT -postcss-modules-scope,1.0.2,ISC -postcss-modules-values,1.2.2,ISC -postcss-normalize-charset,1.1.1,MIT -postcss-normalize-url,3.0.8,MIT -postcss-ordered-values,2.2.3,MIT -postcss-reduce-idents,2.4.0,MIT -postcss-reduce-initial,1.0.1,MIT -postcss-reduce-transforms,1.0.4,MIT -postcss-selector-parser,2.2.3,MIT -postcss-svgo,2.1.6,MIT -postcss-unique-selectors,2.0.2,MIT -postcss-value-parser,3.3.0,MIT -postcss-zindex,2.2.0,MIT -prelude-ls,1.1.2,MIT premailer,1.10.4,New BSD premailer-rails,1.9.7,MIT -prepend-http,1.0.4,MIT -preserve,0.2.0,MIT -prismjs,1.6.0,MIT -private,0.1.7,MIT -process,0.11.9,MIT -process-nextick-args,1.0.7,MIT -progress,1.1.8,MIT -prometheus-client-mmap,0.7.0.beta18,Apache 2.0 -proto-list,1.2.4,ISC -proxy-addr,1.1.5,MIT -prr,0.0.0,MIT -ps-tree,1.1.0,MIT -pseudomap,1.0.2,ISC -public-encrypt,4.0.0,MIT +prometheus-client-mmap,0.7.0.beta43,Apache 2.0 public_suffix,3.0.0,MIT -punycode,1.3.2,MIT -punycode,1.4.1,MIT pyu-ruby-sasl,0.0.3.3,MIT -q,1.5.0,MIT -qjobs,1.1.5,MIT -qs,6.4.0,New BSD -qs,6.5.0,New BSD -query-string,4.3.2,MIT -querystring,0.2.0,MIT -querystring-es3,0.2.1,MIT -querystringify,0.0.4,MIT -querystringify,1.0.0,MIT rack,1.6.8,MIT rack-accept,0.4.5,MIT rack-attack,4.4.1,MIT @@ -1018,88 +221,35 @@ rack-oauth2,1.2.3,MIT rack-protection,1.5.3,MIT rack-proxy,0.6.0,MIT rack-test,0.6.3,MIT -rails,4.2.8,MIT +rails,4.2.10,MIT rails-deprecated_sanitizer,1.0.3,MIT rails-dom-testing,1.0.8,MIT rails-html-sanitizer,1.0.3,MIT rails-i18n,4.0.9,MIT -railties,4.2.8,MIT +railties,4.2.10,MIT rainbow,2.2.2,MIT raindrops,0.18.0,LGPL-2.1+ -rake,12.1.0,MIT -randomatic,1.1.6,MIT -randombytes,2.0.3,MIT -range-parser,1.2.0,MIT -raphael,2.2.7,MIT -raven-js,3.14.0,Simplified BSD -raw-body,2.2.0,MIT -raw-loader,0.5.1,MIT +rake,12.3.0,MIT rbnacl,4.0.2,MIT rbnacl-libsodium,1.0.11,MIT -rc,1.2.1,(BSD-2-Clause OR MIT OR Apache-2.0) rdoc,4.2.2,ruby re2,1.1.1,New BSD -react-dev-utils,0.5.2,New BSD -read-all-stream,3.1.0,MIT -read-pkg,1.1.0,MIT -read-pkg,2.0.0,MIT -read-pkg-up,1.0.1,MIT -read-pkg-up,2.0.0,MIT -readable-stream,1.0.34,MIT -readable-stream,2.0.6,MIT -readable-stream,2.2.9,MIT -readable-stream,2.3.3,MIT -readdirp,2.1.0,MIT -readline2,1.0.1,MIT recaptcha,3.0.0,MIT -rechoir,0.6.2,MIT recursive-open-struct,1.0.0,MIT -recursive-readdir,2.1.1,MIT redcarpet,3.4.0,MIT -redent,1.0.0,MIT redis,3.3.3,MIT -redis-actionpack,5.0.1,MIT -redis-activesupport,5.0.1,MIT +redis-actionpack,5.0.2,MIT +redis-activesupport,5.0.4,MIT redis-namespace,1.5.2,MIT -redis-rack,1.6.0,MIT -redis-rails,5.0.1,MIT -redis-store,1.2.0,MIT -reduce-css-calc,1.3.0,MIT -reduce-function-call,1.0.2,MIT -regenerate,1.3.2,MIT -regenerator-runtime,0.10.1,MIT -regenerator-transform,0.9.8,BSD -regex-cache,0.4.3,MIT -regexpu-core,1.0.0,MIT -regexpu-core,2.0.0,MIT -registry-url,3.1.0,MIT -regjsgen,0.2.0,MIT -regjsparser,0.1.5,BSD -remove-trailing-separator,1.1.0,ISC -repeat-element,1.1.2,MIT -repeat-string,0.2.2,MIT -repeat-string,1.6.1,MIT -repeating,1.1.3,MIT -repeating,2.0.1,MIT +redis-rack,2.0.3,MIT +redis-rails,5.0.2,MIT +redis-store,1.4.1,MIT representable,3.0.4,MIT -request,2.81.0,Apache 2.0 request_store,1.3.1,MIT -require-directory,2.1.1,MIT -require-from-string,1.2.1,MIT -require-main-filename,1.0.1,ISC -require-uncached,1.0.3,MIT -requires-port,1.0.0,MIT -resolve,1.1.7,MIT -resolve,1.2.0,MIT -resolve-from,1.0.1,MIT responders,2.3.0,MIT rest-client,2.0.0,MIT -restore-cursor,1.0.1,MIT retriable,3.1.1,MIT -right-align,0.1.3,MIT -rimraf,2.6.1,ISC rinku,2.0.0,ISC -ripemd160,1.0.1,New BSD rotp,2.1.2,MIT rouge,2.2.1,MIT rqrcode,0.7.0,MIT @@ -1112,244 +262,51 @@ rubyntlm,0.6.2,MIT rubypants,0.2.0,BSD rufus-scheduler,3.4.0,MIT rugged,0.26.0,MIT -run-async,0.1.0,MIT -rx-lite,3.1.2,Apache 2.0 -safe-buffer,5.0.1,MIT -safe-buffer,5.1.1,MIT safe_yaml,1.0.4,MIT sanitize,2.1.0,MIT sass,3.4.22,MIT sass-rails,5.0.6,MIT sawyer,0.8.1,MIT -sax,1.2.2,ISC securecompare,1.0.0,MIT seed-fu,2.3.6,MIT -select,1.1.2,MIT -select-hose,2.0.0,MIT -select2,3.5.2-browserify,unknown select2-rails,3.5.9.3,MIT -selfsigned,1.10.1,MIT -semver,4.3.6,ISC -semver,5.3.0,ISC -semver-diff,2.1.0,MIT -send,0.15.4,MIT sentry-raven,2.5.3,Apache 2.0 -serve-index,1.9.0,MIT -serve-static,1.12.4,MIT -set-blocking,2.0.0,ISC -set-immediate-shim,1.0.1,MIT -setimmediate,1.0.5,MIT -setprototypeof,1.0.3,ISC settingslogic,2.0.9,MIT sexp_processor,4.9.0,MIT -sha.js,2.4.8,MIT -shebang-command,1.2.0,MIT -shebang-regex,1.0.0,MIT -shelljs,0.7.8,New BSD sidekiq,5.0.4,LGPL sidekiq-cron,0.6.0,MIT sidekiq-limit_fetch,3.4.0,MIT -sigmund,1.0.1,ISC -signal-exit,3.0.2,ISC signet,0.7.3,Apache 2.0 slack-notifier,1.5.1,MIT -slash,1.0.0,MIT -slice-ansi,0.0.4,MIT -slide,1.1.6,ISC -sntp,1.0.9,BSD -socket.io,1.7.3,MIT -socket.io-adapter,0.5.0,MIT -socket.io-client,1.7.3,MIT -socket.io-parser,2.3.1,MIT -sockjs,0.3.18,MIT -sockjs-client,1.0.1,MIT -sockjs-client,1.1.4,MIT -sort-keys,1.1.2,MIT -source-list-map,0.1.8,MIT -source-list-map,2.0.0,MIT -source-map,0.1.43,BSD -source-map,0.2.0,BSD -source-map,0.4.4,New BSD -source-map,0.5.6,New BSD -source-map-support,0.4.11,MIT -spdx-correct,1.0.2,Apache 2.0 -spdx-expression-parse,1.0.4,(MIT AND CC-BY-3.0) -spdx-license-ids,1.2.2,Unlicense -spdy,3.4.7,MIT -spdy-transport,2.0.20,MIT -split,0.3.3,MIT -sprintf-js,1.0.3,New BSD sprockets,3.7.1,MIT -sprockets-rails,3.2.0,MIT -sql.js,0.4.0,MIT -sshpk,1.13.0,MIT +sprockets-rails,3.2.1,MIT state_machines,0.4.0,MIT state_machines-activemodel,0.4.0,MIT state_machines-activerecord,0.4.0,MIT -statuses,1.3.1,MIT -stream-browserify,2.0.1,MIT -stream-combiner,0.0.4,MIT -stream-http,2.6.3,MIT -stream-shift,1.0.0,MIT -strict-uri-encode,1.1.0,MIT -string-length,1.0.1,MIT -string-width,1.0.2,MIT -string-width,2.0.0,MIT -string_decoder,0.10.31,MIT -string_decoder,1.0.1,MIT -string_decoder,1.0.3,MIT stringex,2.7.1,MIT -stringstream,0.0.5,MIT -strip-ansi,3.0.1,MIT -strip-bom,2.0.0,MIT -strip-bom,3.0.0,MIT -strip-eof,1.0.0,MIT -strip-indent,1.0.1,MIT -strip-json-comments,2.0.1,MIT -supports-color,2.0.0,MIT -supports-color,3.2.3,MIT -supports-color,4.2.1,MIT -svg4everybody,2.1.9,CC0-1.0 -svgo,0.7.2,MIT sys-filesystem,1.1.6,Artistic 2.0 -table,3.8.3,New BSD -tapable,0.1.10,MIT -tapable,0.2.8,MIT -tar,2.2.1,ISC -tar-pack,3.4.0,Simplified BSD temple,0.7.7,MIT -test-exclude,4.0.0,ISC text,1.3.1,MIT -text-table,0.2.0,MIT thor,0.19.4,MIT thread_safe,0.3.6,Apache 2.0 -three,0.84.0,MIT -three-orbit-controls,82.1.0,MIT -three-stl-loader,1.0.4,MIT -through,2.3.8,MIT -thunky,0.1.0,unknown tilt,2.0.6,MIT -timeago.js,2.0.5,MIT -timed-out,2.0.0,MIT -timed-out,4.0.1,MIT -timers-browserify,1.4.2,MIT -timers-browserify,2.0.4,MIT timfel-krb5-auth,0.8.3,LGPL -tiny-emitter,1.1.0,MIT -tmp,0.0.31,MIT -to-array,0.1.4,MIT -to-arraybuffer,1.0.1,MIT -to-fast-properties,1.0.2,MIT toml-rb,0.3.15,MIT -touch,1.0.0,ISC -tough-cookie,2.3.2,New BSD -traverse,0.6.6,MIT -trim-newlines,1.0.0,MIT -trim-right,1.0.1,MIT truncato,0.7.10,MIT -tryit,1.0.3,MIT -ts-loader,3.1.1,MIT -tty-browserify,0.0.0,MIT -tunnel-agent,0.6.0,Apache 2.0 -tweetnacl,0.14.5,Unlicense -type-check,0.3.2,MIT -type-is,1.6.15,MIT -typedarray,0.0.6,MIT -typescript,2.6.1,Apache 2.0 -tzinfo,1.2.3,MIT +tzinfo,1.2.4,MIT u2f,0.2.1,MIT uber,0.1.0,MIT uglifier,2.7.2,MIT -uglify-js,2.8.29,Simplified BSD -uglify-to-browserify,1.0.2,MIT -uglifyjs-webpack-plugin,0.4.6,MIT -uid-number,0.0.6,ISC -ultron,1.0.2,MIT -ultron,1.1.0,MIT -unc-path-regex,0.1.2,MIT -undefsafe,0.0.3,MIT / http://rem.mit-license.org -underscore,1.8.3,MIT unf,0.1.4,BSD unf_ext,0.0.7.4,MIT unicorn,5.1.0,ruby unicorn-worker-killer,0.4.4,ruby -uniq,1.0.1,MIT -uniqid,4.1.1,MIT -uniqs,2.0.0,MIT -unpipe,1.0.0,MIT -update-notifier,0.5.0,Simplified BSD -url,0.11.0,MIT -url-loader,0.5.8,MIT -url-parse,1.0.5,MIT -url-parse,1.1.7,MIT -url-parse,1.1.9,MIT -url-parse-lax,1.0.0,MIT -url-to-options,1.0.1,MIT url_safe_base64,0.2.2,MIT -user-home,2.0.0,MIT -useragent,2.2.1,MIT -util,0.10.3,MIT -util-deprecate,1.0.2,MIT -utils-merge,1.0.0,MIT -uuid,2.0.3,MIT -uuid,3.0.1,MIT -validate-npm-package-license,3.0.1,Apache 2.0 validates_hostname,1.0.6,MIT -vary,1.1.1,MIT -vendors,1.0.1,MIT -verror,1.3.6,MIT version_sorter,2.1.0,MIT virtus,1.0.5,MIT -visibilityjs,1.2.4,MIT -vm-browserify,0.0.4,MIT vmstat,2.3.0,MIT -void-elements,2.0.1,MIT -vue,2.5.2,MIT -vue-hot-reload-api,2.0.11,MIT -vue-loader,11.3.4,MIT -vue-resource,1.3.4,MIT -vue-style-loader,2.0.5,MIT -vue-template-compiler,2.5.2,MIT -vue-template-es2015-compiler,1.5.1,MIT -vuex,3.0.0,MIT warden,1.2.6,MIT -watchpack,1.4.0,MIT -wbuf,1.7.2,MIT -webpack,3.5.5,MIT -webpack-bundle-analyzer,2.8.2,MIT -webpack-dev-middleware,1.11.0,MIT -webpack-dev-server,2.7.1,MIT webpack-rails,0.9.10,MIT -webpack-sources,1.0.1,MIT -webpack-stats-plugin,0.1.5,MIT -websocket-driver,0.6.5,MIT -websocket-extensions,0.1.1,MIT -whet.extend,0.9.9,MIT -which,1.2.12,ISC -which-module,1.0.0,ISC -which-module,2.0.0,ISC -wide-align,1.1.2,ISC wikicloth,0.8.1,MIT -window-size,0.1.0,MIT -wordwrap,0.0.2,MIT/X11 -wordwrap,0.0.3,MIT -wordwrap,1.0.0,MIT -wrap-ansi,2.1.0,MIT -wrappy,1.0.2,ISC -write,0.2.1,MIT -write-file-atomic,1.3.4,ISC -ws,1.1.2,MIT -ws,2.3.1,MIT -wtf-8,1.0.0,MIT -xdg-basedir,2.0.0,MIT xml-simple,1.1.5,ruby -xmlhttprequest-ssl,1.5.3,MIT -xtend,4.0.1,MIT -y18n,3.2.1,ISC -yallist,2.1.2,ISC -yargs,3.10.0,MIT -yargs,6.6.0,MIT -yargs,8.0.2,MIT -yargs-parser,4.2.1,ISC -yargs-parser,7.0.0,ISC -yeast,0.1.2,MIT diff --git a/yarn.lock b/yarn.lock index 69e8f8a0647..c4d1bd3c682 100644 --- a/yarn.lock +++ b/yarn.lock @@ -54,9 +54,9 @@ lodash "^4.2.0" to-fast-properties "^2.0.0" -"@gitlab-org/gitlab-svgs@^1.1.1": - version "1.1.3" - resolved "https://registry.yarnpkg.com/@gitlab-org/gitlab-svgs/-/gitlab-svgs-1.1.3.tgz#2beead1bcdd83e7400de29b01014bf17bf76318e" +"@gitlab-org/gitlab-svgs@^1.3.0": + version "1.3.0" + resolved "https://registry.yarnpkg.com/@gitlab-org/gitlab-svgs/-/gitlab-svgs-1.3.0.tgz#07f2aa75d6e0e857eaa20c38a3bad7e6c22c420c" "@types/jquery@^2.0.40": version "2.0.48" @@ -116,16 +116,7 @@ ajv@^4.7.0, ajv@^4.9.1: co "^4.6.0" json-stable-stringify "^1.0.1" -ajv@^5.0.0: - version "5.4.0" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.4.0.tgz#32d1cf08dbc80c432f426f12e10b2511f6b46474" - dependencies: - co "^4.6.0" - fast-deep-equal "^1.0.0" - fast-json-stable-stringify "^2.0.0" - json-schema-traverse "^0.3.0" - -ajv@^5.1.5: +ajv@^5.0.0, ajv@^5.1.5: version "5.2.2" resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.2.2.tgz#47c68d69e86f5d953103b0074a9430dc63da5e39" dependencies: @@ -2540,10 +2531,6 @@ fast-deep-equal@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz#96256a3bc975595eb36d82e9929d060d893439ff" -fast-json-stable-stringify@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2" - fast-levenshtein@~2.0.4: version "2.0.6" resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" |