diff options
537 files changed, 8690 insertions, 3603 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 6088a1b3515..adf097b52f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,17 @@ documentation](doc/development/changelog.md) for instructions on adding your own entry. +## 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 +248,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 +507,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/CONTRIBUTING.md b/CONTRIBUTING.md index 4930b541ba2..01d4a546b97 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -598,6 +598,7 @@ merge request: present time and never use past tense (has been/was). For example instead of _prohibited this user from being saved due to the following errors:_ the text should be _sorry, we could not create your account because:_ +1. Code should be written in [US English][us-english] This is also the style used by linting tools such as [RuboCop](https://github.com/bbatsov/rubocop), @@ -663,6 +664,7 @@ available at [http://contributor-covenant.org/version/1/1/0/](http://contributor [GitLab Inc engineering workflow]: https://about.gitlab.com/handbook/engineering/workflow/#labelling-issues [polling-etag]: https://docs.gitlab.com/ce/development/polling.html [testing]: doc/development/testing_guide/index.md +[us-english]: https://en.wikipedia.org/wiki/American_English [^1]: Please note that specs other than JavaScript specs are considered backend code. diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index 46448c71b9d..cb6b534abe1 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -0.57.0 +0.59.0 diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION index 509b0b618ad..269fb5dfe2c 100644 --- a/GITLAB_SHELL_VERSION +++ b/GITLAB_SHELL_VERSION @@ -1 +1 @@ -5.10.0 +5.10.2 @@ -171,7 +171,7 @@ gem 're2', '~> 1.1.1' gem 'version_sorter', '~> 2.1.0' # Cache -gem 'redis-rails', '~> 5.0.1' +gem 'redis-rails', '~> 5.0.2' # Redis gem 'redis', '~> 3.2' @@ -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 @@ -411,3 +411,6 @@ gem 'flipper-active_record', '~> 0.10.2' # 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 16b7c0da8e6..efae71efdb7 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) @@ -486,7 +491,6 @@ GEM mini_mime (0.1.4) mini_portile2 (2.3.0) minitest (5.7.0) - mmap2 (2.2.9) mousetrap-rails (1.4.6) multi_json (1.12.2) multi_xml (0.6.0) @@ -623,8 +627,7 @@ GEM parser unparser procto (0.0.3) - prometheus-client-mmap (0.7.0.beta39) - mmap2 (~> 2.2, >= 2.2.9) + prometheus-client-mmap (0.7.0.beta43) pry (0.10.4) coderay (~> 1.1.0) method_source (~> 0.8.1) @@ -699,24 +702,24 @@ GEM recursive-open-struct (1.0.0) redcarpet (3.4.0) redis (3.3.3) - redis-actionpack (5.0.1) + redis-actionpack (5.0.2) actionpack (>= 4.0, < 6) redis-rack (>= 1, < 3) - redis-store (>= 1.1.0, < 1.4.0) - redis-activesupport (5.0.1) + redis-store (>= 1.1.0, < 2) + redis-activesupport (5.0.4) activesupport (>= 3, < 6) - redis-store (~> 1.2.0) + redis-store (>= 1.3, < 2) redis-namespace (1.5.2) redis (~> 3.0, >= 3.0.4) - redis-rack (1.6.0) - rack (~> 1.5) - redis-store (~> 1.2.0) - redis-rails (5.0.1) - redis-actionpack (~> 5.0.0) - redis-activesupport (~> 5.0.0) - redis-store (~> 1.2.0) - redis-store (1.2.0) - redis (>= 2.2) + redis-rack (2.0.3) + rack (>= 1.5, < 3) + redis-store (>= 1.2, < 2) + redis-rails (5.0.2) + redis-actionpack (>= 5.0, < 6) + redis-activesupport (>= 5.0, < 6) + redis-store (>= 1.2, < 2) + redis-store (1.4.1) + redis (>= 2.2, < 5) representable (3.0.4) declarative (< 0.1.0) declarative-option (< 0.2.0) @@ -977,6 +980,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) @@ -1109,7 +1113,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) @@ -1130,7 +1134,7 @@ DEPENDENCIES redcarpet (~> 3.4) redis (~> 3.2) redis-namespace (~> 1.5.2) - redis-rails (~> 5.0.1) + redis-rails (~> 5.0.2) request_store (~> 1.3) responders (~> 2.0) rouge (~> 2.0) 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/icons.json b/app/assets/images/icons.json index 6befc551263..4e967936ce0 100644 --- a/app/assets/images/icons.json +++ b/app/assets/images/icons.json @@ -1 +1 @@ -{"iconCount":179,"spriteSize":81882,"icons":["abuse","account","admin","angle-double-left","angle-double-right","angle-down","angle-left","angle-right","angle-up","appearance","applications","approval","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":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 diff --git a/app/assets/images/icons.svg b/app/assets/images/icons.svg index 74e1c8c22f6..77ce6b2d89f 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-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="eufirst-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="eusecond-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="euthird-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 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 diff --git a/app/assets/images/illustrations/clusters_empty.svg b/app/assets/images/illustrations/clusters_empty.svg index c13228638be..39627a1c314 100644 --- a/app/assets/images/illustrations/clusters_empty.svg +++ b/app/assets/images/illustrations/clusters_empty.svg @@ -1 +1 @@ -<svg height="128" viewBox="0 0 142 128" width="142" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><path d="M94 62h20v4H94z" fill="#f0edf8"/><path d="M84.828 84l17.678 17.678-2.828 2.828L82 86.828z" fill="#fee1d3"/><path d="M42.828 24l17.678 17.678-2.828 2.828L40 26.828zM40 101.678L57.678 84l2.828 2.828-17.678 17.678z" fill="#f0edf8"/><g fill="#fee1d3"><path d="M82 41.678L99.678 24l2.828 2.828-17.678 17.678zM28 62h20v4H28z"/><rect height="30" rx="5" width="30" y="49"/></g><rect height="26" rx="5" stroke="#fdc4a8" stroke-width="4" width="26" x="2" y="51"/><rect fill="#c3b8e3" height="50" rx="10" width="50" x="46" y="39"/><rect height="46" rx="10" stroke="#6b4fbb" stroke-width="4" width="46" x="48" y="41"/><rect fill="#fef0e8" height="30" rx="5" width="30" x="84"/><rect height="26" rx="5" stroke="#fee1d3" stroke-width="4" width="26" x="86" y="2"/><rect fill="#fee1d3" height="30" rx="5" width="30" x="84" y="98"/><rect height="26" rx="5" stroke="#fdc4a8" stroke-width="4" width="26" x="86" y="100"/><rect fill="#f0edf8" height="30" rx="5" width="30" x="112" y="49"/><rect height="26" rx="5" stroke="#e1dbf1" stroke-width="4" width="26" x="114" y="51"/><rect fill="#f0edf8" height="30" rx="5" width="30" x="28" y="98"/><rect height="26" rx="5" stroke="#e1dbf1" stroke-width="4" width="26" x="30" y="100"/><rect fill="#f0edf8" height="30" rx="5" width="30" x="28"/><rect height="26" rx="5" stroke="#e1dbf1" stroke-width="4" width="26" x="30" y="2"/></g></svg>
\ No newline at end of file +<svg height="128" viewBox="0 0 142 128" width="142" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><path d="M94 62h20v4H94z" fill="#f0edf8"/><path d="M84.828 84l17.678 17.678-2.828 2.828L82 86.828z" fill="#fee1d3"/><path d="M42.828 24l17.678 17.678-2.828 2.828L40 26.828zM40 101.678L57.678 84l2.828 2.828-17.678 17.678z" fill="#f0edf8"/><path d="M82 41.678L99.678 24l2.828 2.828-17.678 17.678zM28 62h20v4H28zM3 52h24v24H3z" fill="#fee1d3"/><path d="M31 3h24v24H31z" fill="#f0edf8"/><path d="M87 3h24v24H87z" fill="#fef0e8"/><path d="M115 52h24v24h-24z" fill="#f0edf8"/><path d="M87 101h24v24H87z" fill="#fee1d3"/><path d="M31 101h24v24H31z" fill="#f0edf8"/><path d="M49 42h44v44H49z" fill="#c3b8e3"/><g fill-rule="nonzero"><path d="M5 53a1 1 0 0 0-1 1v20a1 1 0 0 0 1 1h20a1 1 0 0 0 1-1V54a1 1 0 0 0-1-1zm0-4h20a5 5 0 0 1 5 5v20a5 5 0 0 1-5 5H5a5 5 0 0 1-5-5V54a5 5 0 0 1 5-5z" fill="#fdc4a8"/><path d="M56 43a6 6 0 0 0-6 6v30a6 6 0 0 0 6 6h30a6 6 0 0 0 6-6V49a6 6 0 0 0-6-6zm0-4h30c5.523 0 10 4.477 10 10v30c0 5.523-4.477 10-10 10H56c-5.523 0-10-4.477-10-10V49c0-5.523 4.477-10 10-10z" fill="#6b4fbb"/><path d="M89 4a1 1 0 0 0-1 1v20a1 1 0 0 0 1 1h20a1 1 0 0 0 1-1V5a1 1 0 0 0-1-1zm0-4h20a5 5 0 0 1 5 5v20a5 5 0 0 1-5 5H89a5 5 0 0 1-5-5V5a5 5 0 0 1 5-5z" fill="#fee1d3"/><path d="M89 102a1 1 0 0 0-1 1v20a1 1 0 0 0 1 1h20a1 1 0 0 0 1-1v-20a1 1 0 0 0-1-1zm0-4h20a5 5 0 0 1 5 5v20a5 5 0 0 1-5 5H89a5 5 0 0 1-5-5v-20a5 5 0 0 1 5-5z" fill="#fdc4a8"/><path d="M117 53a1 1 0 0 0-1 1v20a1 1 0 0 0 1 1h20a1 1 0 0 0 1-1V54a1 1 0 0 0-1-1zm0-4h20a5 5 0 0 1 5 5v20a5 5 0 0 1-5 5h-20a5 5 0 0 1-5-5V54a5 5 0 0 1 5-5zM33 102a1 1 0 0 0-1 1v20a1 1 0 0 0 1 1h20a1 1 0 0 0 1-1v-20a1 1 0 0 0-1-1zm0-4h20a5 5 0 0 1 5 5v20a5 5 0 0 1-5 5H33a5 5 0 0 1-5-5v-20a5 5 0 0 1 5-5zM33 4a1 1 0 0 0-1 1v20a1 1 0 0 0 1 1h20a1 1 0 0 0 1-1V5a1 1 0 0 0-1-1zm0-4h20a5 5 0 0 1 5 5v20a5 5 0 0 1-5 5H33a5 5 0 0 1-5-5V5a5 5 0 0 1 5-5z" fill="#e1dbf1"/></g></g></svg>
\ No newline at end of file diff --git a/app/assets/images/illustrations/merge_request_changes_empty.svg b/app/assets/images/illustrations/merge_request_changes_empty.svg new file mode 100644 index 00000000000..707efa736e4 --- /dev/null +++ b/app/assets/images/illustrations/merge_request_changes_empty.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 374 268" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><rect id="d" width="230" height="176" rx="10" fill="#fff"/><rect id="e" width="14" rx="2" height="4"/><rect id="f" width="14" x="40" rx="2" height="4"/><rect id="g" width="14" x="40" y="24" rx="2" height="4"/><rect id="h" width="7" x="20" y="12" rx="2" height="4"/><rect id="i" width="7" y="24" rx="2" height="4"/><rect id="j" width="7" x="33" y="12" rx="2" height="4"/><circle id="l" cx="31" cy="31" r="31"/><circle id="c" cx="35" cy="35" r="35"/><circle id="a" cx="44" cy="44" r="44"/><circle id="b" cx="31" cy="31" r="31"/></defs><g fill="none" fill-rule="evenodd"><g transform="translate(0 94)"><circle cx="57" cy="57" r="44" fill="#f9f9f9"/><g transform="rotate(-7.999 120.507 -22.508)"><use fill="#fff" xlink:href="#a"/><circle cx="44" cy="44" r="42" stroke="#eee" stroke-width="4"/><path fill="#fee1d3" fill-rule="nonzero" d="M34.394 55.736A4 4 0 0 1 36.706 55H56a6 6 0 0 0 6-6V35a6 6 0 0 0-6-6H34a6 6 0 0 0-6 6v25.26l6.394-4.529m2.312 3.264l-7.972 5.647A3.001 3.001 0 0 1 24 62.194v-27.2c0-5.523 4.477-10 10-10h22c5.523 0 10 4.477 10 10v14c0 5.523-4.477 10-10 10H36.706"/><path fill="#fc6d26" d="M38 40a2 2 0 1 1 .001 3.999A2 2 0 0 1 38 40m7 0a2 2 0 1 1 .001 3.999A2 2 0 0 1 45 40m7 0a2 2 0 1 1 .001 3.999A2 2 0 0 1 52 40"/></g></g><g transform="translate(48)"><circle cx="41" cy="41" r="31" fill="#f9f9f9"/><g transform="rotate(-7.999 84.554 -15.551)"><use fill="#fff" xlink:href="#b"/><circle cx="31" cy="31" r="29" stroke="#eee" stroke-width="4"/><rect width="20" height="4" x="21" y="29" fill="#6b4fbb" rx="2"/></g></g><path fill="#f9f9f9" d="M235.58 229H102c-6.627 0-12-5.373-12-12V65c0-6.627 5.373-12 12-12h206c6.627 0 12 5.373 12 12v18.399A34.834 34.834 0 0 1 337 79c19.33 0 35 15.67 35 35s-15.67 35-35 35a34.831 34.831 0 0 1-17-4.399v72.4c0 6.627-5.373 12-12 12h-11.58c.381 1.941.58 3.947.58 6 0 17.12-13.879 31-31 31-17.12 0-31-13.879-31-31 0-2.053.2-4.059.58-6"/><g transform="translate(87 50)"><g transform="rotate(7.999 -44.933 1563.894)"><use fill="#fff" xlink:href="#c"/><circle cx="35" cy="35" r="33" stroke="#eee" stroke-width="4"/><g transform="translate(20 19)"><circle cx="15" cy="16" r="15" fill="#f4f1fa" stroke="#6b4fbb" stroke-width="3"/><g fill="#6b4fbb"><path d="M19.419 6.996h-.007L16.959 4l-2.454 2.997H14.5L12.046 4 9.591 6.998h-.003L7.133 4 4.677 6.999H2.001c2.605-4.204 7.231-7 12.502-7 5.269 0 9.892 2.793 12.498 6.994h-2.676l-2.452-2.994-2.453 2.996"/><circle cx="9.5" cy="17.5" r="1.5"/><circle cx="20.5" cy="17.5" r="1.5"/></g></g></g><use xlink:href="#d"/><rect width="226" height="172" x="2" y="2" stroke="#eee" stroke-width="4" rx="10"/><rect width="4" height="122" x="33" y="42" fill="#eee" rx="2"/><g transform="translate(13 59)"><rect width="10" height="4" fill="#fee1d3" rx="2"/><rect width="10" height="4" y="12" fill="#f0edf8" rx="2"/><rect width="10" height="4" y="24" fill="#fef0e9" rx="2"/><rect width="10" height="4" y="36" fill="#fee1d3" rx="2"/><rect width="10" height="4" y="48" fill="#e1dbf1" rx="2"/><rect width="10" height="4" y="60" fill="#f0edf8" rx="2"/><rect width="10" height="4" y="72" fill="#fef0e9" rx="2"/><rect width="10" height="4" y="84" fill="#fee1d3" rx="2"/></g><g transform="translate(55 59)"><use fill="#6b4fbb" xlink:href="#e"/><rect width="14" height="4" x="20" fill="#f0edf8" rx="2"/><use fill="#fef0e9" xlink:href="#f"/><rect width="14" height="4" y="12" fill="#f0edf8" rx="2"/><use fill="#fef0e9" xlink:href="#g"/><rect width="14" height="4" y="48" fill="#e1dbf1" rx="2"/><rect width="14" height="4" x="40" y="36" fill="#fef0e9" rx="2"/><use fill="#fee1d3" xlink:href="#h"/><rect width="7" height="4" x="27" y="36" fill="#6b4fbb" rx="2"/><rect width="7" height="4" x="20" y="48" fill="#fee1d3" rx="2"/><use fill="#fc6d26" xlink:href="#i"/><rect width="21" height="4" x="13" y="24" fill="#e1dbf1" rx="2"/><rect width="21" height="4" y="36" fill="#eee" rx="2"/><use fill="#6b4fbb" xlink:href="#j"/><g transform="translate(98)"><use fill="#fee1d3" xlink:href="#e"/><rect width="14" height="4" x="20" fill="#f0edf8" rx="2"/><use fill="#fc6d26" xlink:href="#f"/><rect width="14" height="4" y="12" fill="#fef0e9" rx="2" id="k"/><use fill="#e1dbf1" xlink:href="#g"/><rect width="14" height="4" y="48" fill="#f0edf8" rx="2"/><rect width="14" height="4" x="40" y="36" fill="#fee1d3" rx="2"/><use fill="#fc6d26" xlink:href="#h"/><rect width="7" height="4" x="27" y="36" fill="#6b4fbb" rx="2"/><rect width="7" height="4" x="20" y="48" fill="#fc6d26" rx="2"/><use fill="#6b4fbb" xlink:href="#i"/><rect width="21" height="4" x="13" y="24" fill="#fee1d3" rx="2"/><rect width="21" height="4" y="36" fill="#fef0e9" rx="2"/><use fill="#6b4fbb" xlink:href="#j"/></g><g transform="translate(0 60)"><use fill="#f0edf8" xlink:href="#e"/><rect width="14" height="4" x="20" fill="#6b4fbb" rx="2"/><use fill="#e1dbf1" xlink:href="#f"/><use xlink:href="#k"/><use fill="#fee1d3" xlink:href="#g"/><use fill="#eee" xlink:href="#h"/><use fill="#6b4fbb" xlink:href="#i"/><rect width="21" height="4" x="13" y="24" fill="#fef0e9" rx="2"/><use fill="#fc6d26" xlink:href="#j"/></g><rect width="4" height="63" x="74" y="13" fill="#eee" rx="2"/></g><rect width="230" height="4" y="27" fill="#eee" rx="2"/></g><g transform="rotate(7.999 -1289.786 1797.583)"><use fill="#fff" xlink:href="#l"/><circle cx="31" cy="31" r="29" stroke="#eee" stroke-width="4"/><path fill="#fc6d26" d="M29 29h-6a2 2 0 1 0 0 4h6v6a2 2 0 1 0 4 0v-6h6a2 2 0 1 0 0-4h-6v-6a2 2 0 1 0-4 0v6"/></g></g></svg>
\ No newline at end of file diff --git a/app/assets/javascripts/admin.js b/app/assets/javascripts/admin.js index 34669dd13d6..b0b72c40f25 100644 --- a/app/assets/javascripts/admin.js +++ b/app/assets/javascripts/admin.js @@ -1,4 +1,5 @@ /* 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 */ +import { refreshCurrentPage } from './lib/utils/url_utility'; window.Admin = (function() { function Admin() { @@ -40,10 +41,10 @@ window.Admin = (function() { return $('.change-owner-link').show(); }); $('li.project_member').bind('ajax:success', function() { - return gl.utils.refreshCurrentPage(); + return refreshCurrentPage(); }); $('li.group_member').bind('ajax:success', function() { - return gl.utils.refreshCurrentPage(); + return refreshCurrentPage(); }); showBlacklistType = function() { if ($("input[name='blacklist_type']:checked").val() === 'file') { 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/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/compare.js b/app/assets/javascripts/compare.js index 9e5dbd64a7e..0ce467a3bd4 100644 --- a/app/assets/javascripts/compare.js +++ b/app/assets/javascripts/compare.js @@ -1,7 +1,7 @@ /* 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 */ -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 +34,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 +52,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({ @@ -84,7 +84,5 @@ window.Compare = (function() { gl.utils.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..cd20dde2951 100644 --- a/app/assets/javascripts/contextual_sidebar.js +++ b/app/assets/javascripts/contextual_sidebar.js @@ -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/action_btn.vue b/app/assets/javascripts/deploy_keys/components/action_btn.vue index 3f993213dd0..f9f2f9bf693 100644 --- a/app/assets/javascripts/deploy_keys/components/action_btn.vue +++ b/app/assets/javascripts/deploy_keys/components/action_btn.vue @@ -32,7 +32,9 @@ doAction() { this.isLoading = true; - eventHub.$emit(`${this.type}.key`, this.deployKey); + eventHub.$emit(`${this.type}.key`, this.deployKey, () => { + this.isLoading = false; + }); }, }, computed: { @@ -50,6 +52,9 @@ :disabled="isLoading" @click="doAction"> {{ text }} - <loading-icon v-if="isLoading" /> + <loading-icon + v-if="isLoading" + :inline="true" + /> </button> </template> diff --git a/app/assets/javascripts/deploy_keys/components/app.vue b/app/assets/javascripts/deploy_keys/components/app.vue index 54e13b79a4f..fe046449054 100644 --- a/app/assets/javascripts/deploy_keys/components/app.vue +++ b/app/assets/javascripts/deploy_keys/components/app.vue @@ -47,12 +47,15 @@ .then(() => this.fetchKeys()) .catch(() => new Flash('Error enabling deploy key')); }, - disableKey(deployKey) { + disableKey(deployKey, callback) { // eslint-disable-next-line no-alert if (confirm('You are going to remove this deploy key. Are you sure?')) { this.service.disableKey(deployKey.id) .then(() => this.fetchKeys()) + .then(callback) .catch(() => new Flash('Error removing deploy key')); + } else { + callback(); } }, }, 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/diff_notes_bundle.js b/app/assets/javascripts/diff_notes/diff_notes_bundle.js index 0863c3406bd..e0422057090 100644 --- a/app/assets/javascripts/diff_notes/diff_notes_bundle.js +++ b/app/assets/javascripts/diff_notes/diff_notes_bundle.js @@ -16,7 +16,8 @@ import './components/diff_note_avatars'; import './components/new_issue_for_discussion'; $(() => { - const projectPath = document.querySelector('.merge-request').dataset.projectPath; + const projectPathHolder = document.querySelector('.merge-request') || document.querySelector('.commit-box'); + const projectPath = projectPathHolder.dataset.projectPath; const COMPONENT_SELECTOR = 'resolve-btn, resolve-discussion-btn, jump-to-discussion, comment-and-resolve-btn, new-issue-for-discussion-btn'; window.gl = window.gl || {}; diff --git a/app/assets/javascripts/diff_notes/services/resolve.js b/app/assets/javascripts/diff_notes/services/resolve.js index 6eae54f830b..96fe23640af 100644 --- a/app/assets/javascripts/diff_notes/services/resolve.js +++ b/app/assets/javascripts/diff_notes/services/resolve.js @@ -43,7 +43,7 @@ class ResolveServiceClass { discussion.resolveAllNotes(resolvedBy); } - gl.mrWidget.checkStatus(); + if (gl.mrWidget) gl.mrWidget.checkStatus(); discussion.updateHeadline(data); }) .catch(() => new Flash('An error occurred when trying to resolve a discussion. Please try again.')); diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index 24476d3d757..299e43a4e90 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -22,8 +22,8 @@ import NewCommitForm from './new_commit_form'; import Project from './project'; import projectAvatar from './project_avatar'; /* global MergeRequest */ -/* global Compare */ -/* global CompareAutocomplete */ +import Compare from './compare'; +import initCompareAutocomplete from './compare_autocomplete'; /* global ProjectFindFile */ import ProjectNew from './project_new'; import projectImport from './project_import'; @@ -525,13 +525,6 @@ import ProjectVariables from './project_variables'; case 'projects:settings:ci_cd:show': // Initialize expandable settings panels initSettingsPanels(); - - import(/* webpackChunkName: "ci-cd-settings" */ './projects/ci_cd_settings_bundle') - .then(ciCdSettings => ciCdSettings.default()) - .catch((err) => { - Flash(s__('ProjectSettings|Problem setting up the CI/CD settings JavaScript')); - throw err; - }); case 'groups:settings:ci_cd:show': new ProjectVariables(); break; @@ -629,7 +622,7 @@ import ProjectVariables from './project_variables'; projectAvatar(); switch (path[1]) { case 'compare': - new CompareAutocomplete(); + initCompareAutocomplete(); break; case 'edit': shortcut_handler = new ShortcutsNavigation(); 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/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js index a642464c920..d918d80df8d 100644 --- a/app/assets/javascripts/gfm_auto_complete.js +++ b/app/assets/javascripts/gfm_auto_complete.js @@ -287,6 +287,10 @@ class GfmAutoComplete { } setupLabels($input) { + const fetchData = this.fetchData.bind(this); + const LABEL_COMMAND = { LABEL: '/label', UNLABEL: '/unlabel', RELABEL: '/relabel' }; + let command = ''; + $input.atwho({ at: '~', alias: 'labels', @@ -309,8 +313,45 @@ class GfmAutoComplete { title: sanitize(m.title), color: m.color, search: m.title, + set: m.set, })); }, + matcher(flag, subtext) { + const match = GfmAutoComplete.defaultMatcher(flag, subtext, this.app.controllers); + const subtextNodes = subtext.split(/\n+/g).pop().split(GfmAutoComplete.regexSubtext); + + // Check if ~ is followed by '/label', '/relabel' or '/unlabel' commands. + command = subtextNodes.find((node) => { + if (node === LABEL_COMMAND.LABEL || + node === LABEL_COMMAND.RELABEL || + node === LABEL_COMMAND.UNLABEL) { return node; } + return null; + }); + + return match && match.length ? match[1] : null; + }, + filter(query, data, searchKey) { + if (GfmAutoComplete.isLoading(data)) { + fetchData(this.$inputor, this.at); + return data; + } + + if (data === GfmAutoComplete.defaultLoadingData) { + return $.fn.atwho.default.callbacks.filter(query, data, searchKey); + } + + // The `LABEL_COMMAND.RELABEL` is intentionally skipped + // because we want to return all the labels (unfiltered) for that command. + if (command === LABEL_COMMAND.LABEL) { + // Return labels with set: undefined. + return data.filter(label => !label.set); + } else if (command === LABEL_COMMAND.UNLABEL) { + // Return labels with set: true. + return data.filter(label => label.set); + } + + return data; + }, }, }); } @@ -346,20 +387,7 @@ class GfmAutoComplete { return resultantValue; }, matcher(flag, subtext) { - // The below is taken from At.js source - // Tweaked to commands to start without a space only if char before is a non-word character - // https://github.com/ichord/At.js - const atSymbolsWithBar = Object.keys(this.app.controllers).join('|'); - const atSymbolsWithoutBar = Object.keys(this.app.controllers).join(''); - const targetSubtext = subtext.split(/\s+/g).pop(); - const resultantFlag = flag.replace(/[-[\]/{}()*+?.\\^$|]/g, '\\$&'); - - const accentAChar = decodeURI('%C3%80'); - const accentYChar = decodeURI('%C3%BF'); - - const regexp = new RegExp(`^(?:\\B|[^a-zA-Z0-9_${atSymbolsWithoutBar}]|\\s)${resultantFlag}(?!${atSymbolsWithBar})((?:[A-Za-z${accentAChar}-${accentYChar}0-9_'.+-]|[^\\x00-\\x7a])*)$`, 'gi'); - - const match = regexp.exec(targetSubtext); + const match = GfmAutoComplete.defaultMatcher(flag, subtext, this.app.controllers); if (match) { return match[1]; @@ -420,8 +448,27 @@ class GfmAutoComplete { return dataToInspect && (dataToInspect === loadingState || dataToInspect.name === loadingState); } + + static defaultMatcher(flag, subtext, controllers) { + // The below is taken from At.js source + // Tweaked to commands to start without a space only if char before is a non-word character + // https://github.com/ichord/At.js + const atSymbolsWithBar = Object.keys(controllers).join('|'); + const atSymbolsWithoutBar = Object.keys(controllers).join(''); + const targetSubtext = subtext.split(GfmAutoComplete.regexSubtext).pop(); + const resultantFlag = flag.replace(/[-[\]/{}()*+?.\\^$|]/g, '\\$&'); + + const accentAChar = decodeURI('%C3%80'); + const accentYChar = decodeURI('%C3%BF'); + + const regexp = new RegExp(`^(?:\\B|[^a-zA-Z0-9_${atSymbolsWithoutBar}]|\\s)${resultantFlag}(?!${atSymbolsWithBar})((?:[A-Za-z${accentAChar}-${accentYChar}0-9_'.+-]|[^\\x00-\\x7a])*)$`, 'gi'); + + return regexp.exec(targetSubtext); + } } +GfmAutoComplete.regexSubtext = new RegExp(/\s+/g); + GfmAutoComplete.defaultLoadingData = ['loading']; GfmAutoComplete.atTypeMap = { 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/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 0cd0c59a275..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); } } }, @@ -123,18 +124,23 @@ export default { :title="group.fullName" class="no-expand" data-placement="top" - > - {{group.name}} - </a> + >{{ + // ending bracket must be by closing tag to prevent + // link hover text-decoration from over-extending + group.name + }}</a> <span v-if="group.permission" - class="access-type" + class="user-access-role" > - {{s__('GroupsTreeRole|as')}} {{group.permission}} + {{group.permission}} </span> </div> <div - class="description">{{group.description}}</div> + v-if="group.description" + class="description"> + {{group.description}} + </div> </div> <group-folder v-if="group.isOpen && hasChildren" 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/issue.js b/app/assets/javascripts/issue.js index 7de07e9403d..91b5ef1c10a 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'); diff --git a/app/assets/javascripts/issue_show/components/app.vue b/app/assets/javascripts/issue_show/components/app.vue index 5bdc7c99503..fd1a50dd533 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 RecaptchaDialogImplementor from '../../vue_shared/mixins/recaptcha_dialog_implementor'; export default { props: { @@ -149,6 +150,11 @@ export default { editedComponent, formComponent, }, + + mixins: [ + RecaptchaDialogImplementor, + ], + 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}`); + } }); }, + + closeRecaptchaDialog() { + 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-dialog + v-show="showRecaptcha" + :html="recaptchaHTML" + @close="closeRecaptchaDialog" + /> + </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..feb73481422 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 RecaptchaDialogImplementor from '../../vue_shared/mixins/recaptcha_dialog_implementor'; export default { - mixins: [animateMixin], + mixins: [ + animateMixin, + RecaptchaDialogImplementor, + ], + 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-dialog + v-show="showRecaptcha" + :html="recaptchaHTML" + @close="closeRecaptcha" + /> </div> </template> diff --git a/app/assets/javascripts/issue_show/index.js b/app/assets/javascripts/issue_show/index.js index aca9dec2a96..a21ce41e65e 100644 --- a/app/assets/javascripts/issue_show/index.js +++ b/app/assets/javascripts/issue_show/index.js @@ -5,7 +5,7 @@ import '../vue_shared/vue_resource_interceptor'; document.addEventListener('DOMContentLoaded', () => { const initialDataEl = document.getElementById('js-issuable-app-initial-data'); - const initialData = JSON.parse(initialDataEl.innerHTML.replace(/"/g, '"')); + const props = JSON.parse(initialDataEl.innerHTML.replace(/"/g, '"')); $('.issuable-edit').on('click', (e) => { e.preventDefault(); @@ -18,32 +18,9 @@ document.addEventListener('DOMContentLoaded', () => { components: { issuableApp, }, - data() { - return { - ...initialData, - }; - }, render(createElement) { return createElement('issuable-app', { - props: { - canUpdate: this.canUpdate, - canDestroy: this.canDestroy, - endpoint: this.endpoint, - issuableRef: this.issuableRef, - initialTitleHtml: this.initialTitleHtml, - initialTitleText: this.initialTitleText, - initialDescriptionHtml: this.initialDescriptionHtml, - initialDescriptionText: this.initialDescriptionText, - issuableTemplates: this.issuableTemplates, - markdownPreviewPath: this.markdownPreviewPath, - markdownDocsPath: this.markdownDocsPath, - projectPath: this.projectPath, - projectNamespace: this.projectNamespace, - updatedAt: this.updatedAt, - updatedByName: this.updatedByName, - updatedByPath: this.updatedByPath, - initialTaskStatus: this.initialTaskStatus, - }, + props, }); }, }); diff --git a/app/assets/javascripts/job.js b/app/assets/javascripts/job.js index cf8fda9a4fa..06b0e02a870 100644 --- a/app/assets/javascripts/job.js +++ b/app/assets/javascripts/job.js @@ -1,4 +1,5 @@ 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'; @@ -9,7 +10,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 +168,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 +210,7 @@ export default class Job { } if (log.status !== this.buildStatus) { - gl.utils.visitUrl(this.pageUrl); + visitUrl(this.pagePath); } }) .fail(() => { 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/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js index 426a81a976d..d0578b230b1 100644 --- a/app/assets/javascripts/lib/utils/datetime_utility.js +++ b/app/assets/javascripts/lib/utils/datetime_utility.js @@ -35,8 +35,6 @@ window.dateFormat = dateFormat; w.gl.utils.localTimeAgo = function($timeagoEls, setTimeago = true) { $timeagoEls.each((i, el) => { - el.setAttribute('title', el.getAttribute('title')); - if (setTimeago) { // Recreate with custom template $(el).tooltip({ 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/main.js b/app/assets/javascripts/main.js index dcc0fa63b63..9e4047b6840 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -29,7 +29,7 @@ import './commit/image_file'; // lib/utils import { handleLocationHash } from './lib/utils/common_utils'; import './lib/utils/datetime_utility'; -import './lib/utils/url_utility'; +import { getLocationHash, visitUrl } from './lib/utils/url_utility'; // behaviors import './behaviors/'; @@ -40,9 +40,6 @@ 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'; @@ -122,7 +119,7 @@ $(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); } }); @@ -294,7 +291,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_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js index 54c1b7a268e..589e65647ac 100644 --- a/app/assets/javascripts/merge_request_tabs.js +++ b/app/assets/javascripts/merge_request_tabs.js @@ -11,6 +11,7 @@ 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'; @@ -317,7 +318,7 @@ import Diff from './diff'; // Scroll any linked note into view // Similar to `toggler_behavior` in the discussion tab - const hash = window.gl.utils.getLocationHash(); + const hash = getLocationHash(); const anchor = hash && $container.find(`.note[id="${hash}"]`); if (anchor && anchor.length > 0) { const notesContent = anchor.closest('.notes_content'); 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..2a570ac705e 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'; @@ -330,7 +331,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); diff --git a/app/assets/javascripts/notes/components/issue_note.vue b/app/assets/javascripts/notes/components/issue_note.vue index 8c81c5d6df3..3ceb961f58e 100644 --- a/app/assets/javascripts/notes/components/issue_note.vue +++ b/app/assets/javascripts/notes/components/issue_note.vue @@ -1,5 +1,6 @@ <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'; @@ -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(() => { diff --git a/app/assets/javascripts/notes/components/issue_notes_app.vue b/app/assets/javascripts/notes/components/issue_notes_app.vue index 4cfcffa2391..e4d01285d39 100644 --- a/app/assets/javascripts/notes/components/issue_notes_app.vue +++ b/app/assets/javascripts/notes/components/issue_notes_app.vue @@ -1,5 +1,6 @@ <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'; @@ -95,7 +96,7 @@ this.poll(); }, checkLocationHash() { - const hash = gl.utils.getLocationHash(); + const hash = getLocationHash(); const element = document.getElementById(hash); if (hash && element) { diff --git a/app/assets/javascripts/notes/components/note_actions.vue b/app/assets/javascripts/notes/components/note_actions.vue index 45fc6196be4..7fb45ed4d4b 100644 --- a/app/assets/javascripts/notes/components/note_actions.vue +++ b/app/assets/javascripts/notes/components/note_actions.vue @@ -86,7 +86,7 @@ <div class="note-actions"> <span v-if="accessLevel" - class="note-role note-role-access">{{accessLevel}}</span> + class="note-role user-access-role">{{accessLevel}}</span> <div v-if="canAddAwardEmoji" class="note-actions-item"> diff --git a/app/assets/javascripts/notes/index.js b/app/assets/javascripts/notes/index.js index 8d74c5de5cf..a94163a5f87 100644 --- a/app/assets/javascripts/notes/index.js +++ b/app/assets/javascripts/notes/index.js @@ -8,10 +8,18 @@ document.addEventListener('DOMContentLoaded', () => new Vue({ }, 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, diff --git a/app/assets/javascripts/pager.js b/app/assets/javascripts/pager.js index e3fc1e2fc2f..6792b984cc5 100644 --- a/app/assets/javascripts/pager.js +++ b/app/assets/javascripts/pager.js @@ -1,5 +1,5 @@ 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; @@ -7,7 +7,7 @@ import '~/lib/utils/url_utility'; 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.url = $('.content_list').data('href') || removeParams(['limit', 'offset']); this.limit = limit; this.offset = parseInt(getParameterByName('offset'), 10) || this.limit; this.disable = disable; 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/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/projects/ci_cd_settings_bundle.js b/app/assets/javascripts/projects/ci_cd_settings_bundle.js deleted file mode 100644 index 90e418f6771..00000000000 --- a/app/assets/javascripts/projects/ci_cd_settings_bundle.js +++ /dev/null @@ -1,19 +0,0 @@ -function updateAutoDevopsRadios(radioWrappers) { - radioWrappers.forEach((radioWrapper) => { - const radio = radioWrapper.querySelector('.js-auto-devops-enable-radio'); - const runPipelineCheckboxWrapper = radioWrapper.querySelector('.js-run-auto-devops-pipeline-checkbox-wrapper'); - const runPipelineCheckbox = radioWrapper.querySelector('.js-run-auto-devops-pipeline-checkbox'); - - if (runPipelineCheckbox) { - runPipelineCheckbox.checked = radio.checked; - runPipelineCheckboxWrapper.classList.toggle('hide', !radio.checked); - } - }); -} - -export default function initCiCdSettings() { - const radioWrappers = document.querySelectorAll('.js-auto-devops-enable-radio-wrapper'); - radioWrappers.forEach(radioWrapper => - radioWrapper.addEventListener('change', () => updateAutoDevopsRadios(radioWrappers)), - ); -} 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/repo/components/new_dropdown/index.vue b/app/assets/javascripts/repo/components/new_dropdown/index.vue index a5ee4f71281..781404cf8ca 100644 --- a/app/assets/javascripts/repo/components/new_dropdown/index.vue +++ b/app/assets/javascripts/repo/components/new_dropdown/index.vue @@ -2,9 +2,11 @@ import { mapState } from 'vuex'; import newModal from './modal.vue'; import upload from './upload.vue'; + import icon from '../../../vue_shared/components/icon.vue'; export default { components: { + icon, newModal, upload, }, @@ -41,11 +43,14 @@ data-toggle="dropdown" aria-label="Create new file or directory" > - <i - class="fa fa-plus" - aria-hidden="true" - > - </i> + <icon + name="plus" + css-classes="pull-left" + /> + <icon + name="arrow-down" + css-classes="pull-left" + /> </button> <ul class="dropdown-menu"> <li> 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/search_autocomplete.js b/app/assets/javascripts/search_autocomplete.js index 9dec5d7645a..e40a3596200 100644 --- a/app/assets/javascripts/search_autocomplete.js +++ b/app/assets/javascripts/search_autocomplete.js @@ -1,13 +1,20 @@ -/* eslint-disable comma-dangle, no-return-assign, one-var, no-var, no-underscore-dangle, one-var-declaration-per-line, no-unused-vars, no-cond-assign, consistent-return, object-shorthand, prefer-arrow-callback, func-names, space-before-function-paren, prefer-template, quotes, class-methods-use-this, no-unused-expressions, no-sequences, wrap-iife, no-lonely-if, no-else-return, no-param-reassign, vars-on-top, max-len */ +/* eslint-disable no-return-assign, one-var, no-var, no-underscore-dangle, one-var-declaration-per-line, no-unused-vars, no-cond-assign, consistent-return, object-shorthand, prefer-arrow-callback, func-names, space-before-function-paren, prefer-template, quotes, class-methods-use-this, no-sequences, wrap-iife, no-lonely-if, no-else-return, no-param-reassign, vars-on-top, max-len */ import { isInGroupsPage, isInProjectPage, getGroupSlug, getProjectSlug } from './lib/utils/common_utils'; +/** + * Search input in top navigation bar. + * On click, opens a dropdown + * As the user types it filters the results + * 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 + DOWN: 40, }; class SearchAutocomplete { @@ -19,6 +26,7 @@ import { isInGroupsPage, isInProjectPage, getGroupSlug, getProjectSlug } from '. 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'); @@ -29,13 +37,16 @@ import { isInGroupsPage, isInProjectPage, getGroupSlug, getProjectSlug } from '. 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(); } + this.searchInput.addClass('disabled'); this.saveTextLength(); this.bindEvents(); + this.dropdownToggle.dropdown(); } // Finds an element inside wrapper element @@ -43,7 +54,6 @@ import { isInGroupsPage, isInProjectPage, getGroupSlug, getProjectSlug } from '. this.onSearchInputBlur = this.onSearchInputBlur.bind(this); this.onClearInputClick = this.onClearInputClick.bind(this); this.onSearchInputFocus = this.onSearchInputFocus.bind(this); - this.onSearchInputClick = this.onSearchInputClick.bind(this); this.onSearchInputKeyUp = this.onSearchInputKeyUp.bind(this); this.onSearchInputKeyDown = this.onSearchInputKeyDown.bind(this); } @@ -68,12 +78,12 @@ import { isInGroupsPage, isInProjectPage, getGroupSlug, getProjectSlug } from '. enterCallback: false, filterInput: 'input#search', search: { - fields: ['text'] + fields: ['text'], }, id: this.getSearchText, data: this.getData.bind(this), selectable: true, - clicked: this.onClick.bind(this) + clicked: this.onClick.bind(this), }); } @@ -82,32 +92,35 @@ import { isInGroupsPage, isInProjectPage, getGroupSlug, getProjectSlug } from '. } getData(term, callback) { - var _this, contents, jqXHR; - _this = this; if (!term) { - if (contents = this.getCategoryContents()) { + const contents = this.getCategoryContents(); + if (contents) { this.searchInput.data('glDropdown').filter.options.callback(contents); this.enableAutocomplete(); } return; } + // Prevent multiple ajax calls if (this.loadingSuggestions) { return; } + this.loadingSuggestions = true; - return jqXHR = $.get(this.autocompletePath, { + + return $.get(this.autocompletePath, { project_id: this.projectId, project_ref: this.projectRef, - term: term - }, function(response) { - var data, firstCategory, i, lastCategory, len, suggestion; + term: term, + }, (response) => { + var firstCategory, i, lastCategory, len, suggestion; // Hide dropdown menu if no suggestions returns if (!response.length) { - _this.disableAutocomplete(); + this.disableAutocomplete(); return; } - data = []; + + const data = []; // List results firstCategory = true; for (i = 0, len = response.length; i < len; i += 1) { @@ -121,7 +134,7 @@ import { isInGroupsPage, isInProjectPage, getGroupSlug, getProjectSlug } from '. firstCategory = false; } data.push({ - header: suggestion.category + header: suggestion.category, }); lastCategory = suggestion.category; } @@ -129,7 +142,7 @@ import { isInGroupsPage, isInProjectPage, getGroupSlug, getProjectSlug } from '. id: (suggestion.category.toLowerCase()) + "-" + suggestion.id, category: suggestion.category, text: suggestion.label, - url: suggestion.url + url: suggestion.url, }); } // Add option to proceed with the search @@ -137,20 +150,21 @@ import { isInGroupsPage, isInProjectPage, getGroupSlug, getProjectSlug } from '. data.push('separator'); data.push({ text: "Result name contains \"" + term + "\"", - url: "/search?search=" + term + "&project_id=" + (_this.projectInputEl.val()) + "&group_id=" + (_this.groupInputEl.val()) + url: "/search?search=" + term + "&project_id=" + (this.projectInputEl.val()) + "&group_id=" + (this.groupInputEl.val()), }); } return callback(data); - }).always(function() { - return _this.loadingSuggestions = false; - }); + }) + .always(() => { this.loadingSuggestions = false; }); } getCategoryContents() { - var dashboardOptions, groupOptions, issuesPath, items, mrPath, name, options, projectOptions, userId, userName; - userId = gon.current_user_id; - userName = gon.current_username; - projectOptions = gl.projectOptions, groupOptions = gl.groupOptions, dashboardOptions = gl.dashboardOptions; + 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) { @@ -158,37 +172,42 @@ import { isInGroupsPage, isInProjectPage, getGroupSlug, getProjectSlug } from '. } else if (dashboardOptions) { options = dashboardOptions; } - issuesPath = options.issuesPath, mrPath = options.mrPath, name = options.name; - items = [ - { - header: "" + name - } - ]; + + const { issuesPath, mrPath, name, issuesDisabled } = options; + const baseItems = []; + + if (name) { + baseItems.push({ + header: `${name}`, + }); + } + const issueItems = [ { text: 'Issues assigned to me', - url: issuesPath + "/?assignee_username=" + userName - }, { + url: `${issuesPath}/?assignee_username=${userName}`, + }, + { text: "Issues I've created", - url: issuesPath + "/?author_username=" + userName - } + url: `${issuesPath}/?author_username=${userName}`, + }, ]; const mergeRequestItems = [ { text: 'Merge requests assigned to me', - url: mrPath + "/?assignee_username=" + userName - }, { + url: `${mrPath}/?assignee_username=${userName}`, + }, + { text: "Merge requests I've created", - url: mrPath + "/?author_username=" + userName - } + url: `${mrPath}/?author_username=${userName}`, + }, ]; - if (options.issuesDisabled) { - items = items.concat(mergeRequestItems); + + let items; + if (issuesDisabled) { + items = baseItems.concat(mergeRequestItems); } else { - items = items.concat(...issueItems, 'separator', ...mergeRequestItems); - } - if (!name) { - items.splice(0, 1); + items = baseItems.concat(...issueItems, 'separator', ...mergeRequestItems); } return items; } @@ -202,39 +221,34 @@ import { isInGroupsPage, isInProjectPage, getGroupSlug, getProjectSlug } from '. repository_ref: this.repositoryInputEl.val(), scope: this.scopeInputEl.val(), // Location badge - _location: this.locationBadgeEl.text() + _location: this.locationBadgeEl.text(), }; } bindEvents() { this.searchInput.on('keydown', this.onSearchInputKeyDown); this.searchInput.on('keyup', this.onSearchInputKeyUp); - this.searchInput.on('click', this.onSearchInputClick); this.searchInput.on('focus', this.onSearchInputFocus); this.searchInput.on('blur', this.onSearchInputBlur); this.clearInput.on('click', this.onClearInputClick); - return this.locationBadgeEl.on('click', (function(_this) { - return function() { - return _this.searchInput.focus(); - }; - })(this)); + this.locationBadgeEl.on('click', () => this.searchInput.focus()); } enableAutocomplete() { - var _this; // No need to enable anything if user is not logged in if (!gon.current_user_id) { return; } + + // If the dropdown is closed, we'll open it if (!this.dropdown.hasClass('open')) { - _this = this; this.loadingSuggestions = false; - this.dropdown.addClass('open').trigger('shown.bs.dropdown'); + this.dropdownToggle.dropdown('toggle'); return this.searchInput.removeClass('disabled'); } } - // Saves last length of the entered text + // Saves last length of the entered text onSearchInputKeyDown() { return this.saveTextLength(); } @@ -279,11 +293,6 @@ import { isInGroupsPage, isInProjectPage, getGroupSlug, getProjectSlug } from '. this.wrap.toggleClass('has-value', !!e.target.value); } - // Avoid falsy value to be returned - onSearchInputClick(e) { - return e.stopImmediatePropagation(); - } - onSearchInputFocus() { this.isFocused = true; this.wrap.addClass('search-active'); @@ -335,7 +344,7 @@ import { isInGroupsPage, isInProjectPage, getGroupSlug, getProjectSlug } from '. return this.locationBadgeEl.hide(); } else { return this.addLocationBadge({ - value: this.originalState._location + value: this.originalState._location, }); } } @@ -387,13 +396,13 @@ import { isInGroupsPage, isInProjectPage, getGroupSlug, getProjectSlug } from '. if (item.category === 'Projects') { this.projectInputEl.val(item.id); this.addLocationBadge({ - value: 'This project' + value: 'This project', }); } if (item.category === 'Groups') { this.groupInputEl.val(item.id); this.addLocationBadge({ - value: 'This group' + value: 'This group', }); } } @@ -420,7 +429,7 @@ import { isInGroupsPage, isInProjectPage, getGroupSlug, getProjectSlug } from '. name: $projectOptionsDataEl.data('name'), issuesPath: $projectOptionsDataEl.data('issues-path'), issuesDisabled: $projectOptionsDataEl.data('issues-disabled'), - mrPath: $projectOptionsDataEl.data('mr-path') + mrPath: $projectOptionsDataEl.data('mr-path'), }; } @@ -432,14 +441,14 @@ import { isInGroupsPage, isInProjectPage, getGroupSlug, getProjectSlug } from '. gl.groupOptions[groupPath] = { name: $groupOptionsDataEl.data('name'), issuesPath: $groupOptionsDataEl.data('issues-path'), - mrPath: $groupOptionsDataEl.data('mr-path') + mrPath: $groupOptionsDataEl.data('mr-path'), }; } if ($dashboardOptionsDataEl.length) { gl.dashboardOptions = { issuesPath: $dashboardOptionsDataEl.data('issues-path'), - mrPath: $dashboardOptionsDataEl.data('mr-path') + mrPath: $dashboardOptionsDataEl.data('mr-path'), }; } }); 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/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/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/vue_merge_request_widget/components/mr_widget_deployment.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_deployment.js index e86a0f7e749..32028a4a609 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 { visitUrl } from '../../lib/utils/url_utility'; import Flash from '../../flash'; import MemoryUsage from './mr_widget_memory_usage'; import StatusIcon from './mr_widget_status_icon'; @@ -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/mr_widget_options.js b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js index 1274db2c4c8..9cb3edead86 100644 --- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js +++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js @@ -1,3 +1,4 @@ +import Project from '~/project'; import SmartInterval from '~/smart_interval'; import Flash from '../flash'; import { @@ -140,6 +141,7 @@ export default { const el = document.createElement('div'); el.innerHTML = res.body; document.body.appendChild(el); + Project.initRefSwitcher(); } }) .catch(() => { 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/popup_dialog.vue b/app/assets/javascripts/vue_shared/components/popup_dialog.vue index 47efee64c6e..6d15bbd84ba 100644 --- a/app/assets/javascripts/vue_shared/components/popup_dialog.vue +++ b/app/assets/javascripts/vue_shared/components/popup_dialog.vue @@ -38,7 +38,8 @@ export default { }, primaryButtonLabel: { type: String, - required: true, + required: false, + default: '', }, submitDisabled: { type: Boolean, @@ -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_dialog.vue b/app/assets/javascripts/vue_shared/components/recaptcha_dialog.vue new file mode 100644 index 00000000000..3ec50f14eb4 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/recaptcha_dialog.vue @@ -0,0 +1,85 @@ +<script> +import PopupDialog from './popup_dialog.vue'; + +export default { + name: 'recaptcha-dialog', + + props: { + html: { + type: String, + required: false, + default: '', + }, + }, + + data() { + return { + script: {}, + scriptSrc: 'https://www.google.com/recaptcha/api.js', + }; + }, + + components: { + PopupDialog, + }, + + 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> +<popup-dialog + kind="warning" + class="recaptcha-dialog js-recaptcha-dialog" + :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> +</popup-dialog> +</template> diff --git a/app/assets/javascripts/vue_shared/mixins/recaptcha_dialog_implementor.js b/app/assets/javascripts/vue_shared/mixins/recaptcha_dialog_implementor.js new file mode 100644 index 00000000000..ef70f9432e3 --- /dev/null +++ b/app/assets/javascripts/vue_shared/mixins/recaptcha_dialog_implementor.js @@ -0,0 +1,36 @@ +import RecaptchaDialog from '../components/recaptcha_dialog.vue'; + +export default { + data() { + return { + showRecaptcha: false, + recaptchaHTML: '', + }; + }, + + components: { + RecaptchaDialog, + }, + + 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/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index a42fab50db5..73524d5cf60 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -237,19 +237,6 @@ li.note { } } -.browser-alert { - padding: 10px; - text-align: center; - background: $error-bg; - color: $white-light; - font-weight: $gl-font-weight-bold; - - a { - color: $white-light; - text-decoration: underline; - } -} - .warning_message { border-left: 4px solid $warning-message-border; color: $warning-message-color; diff --git a/app/assets/stylesheets/framework/contextual-sidebar.scss b/app/assets/stylesheets/framework/contextual-sidebar.scss index b73932eb7e1..26a2db99e0a 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,6 +317,7 @@ .toggle-sidebar-button, .close-nav-button { width: $contextual-sidebar-width - 2px; + transition: width $sidebar-transition-duration; position: fixed; bottom: 0; padding: 16px; @@ -343,20 +348,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 +380,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/filters.scss b/app/assets/stylesheets/framework/filters.scss index cf8165eab5b..cec38eea464 100644 --- a/app/assets/stylesheets/framework/filters.scss +++ b/app/assets/stylesheets/framework/filters.scss @@ -432,6 +432,7 @@ border-width: 1px; width: 17px; height: 17px; + top: 0; } &:hover, diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss index cd505bcaf6d..e6e6c4c3963 100644 --- a/app/assets/stylesheets/framework/lists.scss +++ b/app/assets/stylesheets/framework/lists.scss @@ -457,13 +457,11 @@ ul.indent-list { ul.group-list-tree { li.group-row { - &.has-description { - .title { - line-height: inherit; - } + &.has-description .title { + line-height: inherit; } - .title { + &:not(.has-description) .title { line-height: $list-text-height; } } diff --git a/app/assets/stylesheets/framework/modal.scss b/app/assets/stylesheets/framework/modal.scss index 5c9838c1029..ce551e6b7ce 100644 --- a/app/assets/stylesheets/framework/modal.scss +++ b/app/assets/stylesheets/framework/modal.scss @@ -48,3 +48,10 @@ body.modal-open { display: block; } +.recaptcha-dialog .recaptcha-form { + display: inline-block; + + .recaptcha { + margin: 0; + } +} diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss index 792981fdc48..3ebba4f9efb 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; + } } } diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 6525b39d55c..a6fa96d834c 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; @@ -408,7 +407,6 @@ $location-icon-color: #e7e9ed; * Notes */ $notes-light-color: $gl-text-color-secondary; -$notes-role-color: $gl-text-color-secondary; $note-disabled-comment-color: #b2b2b2; $note-targe3-outside: #fffff0; $note-targe3-inside: #ffffd3; @@ -557,6 +555,7 @@ $jq-ui-default-color: #777; /* * Label */ +$label-padding: 7px; $label-gray-bg: #f8fafc; $label-inverse-bg: #333; $label-remove-border: rgba(0, 0, 0, .1); diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss index 3683afa07de..862ea379cbc 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 { @@ -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/commits.scss b/app/assets/stylesheets/pages/commits.scss index b1850be8a5f..0cba223c6a6 100644 --- a/app/assets/stylesheets/pages/commits.scss +++ b/app/assets/stylesheets/pages/commits.scss @@ -189,6 +189,7 @@ .commit-content { padding-right: 10px; + white-space: normal; } .commit-actions { diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss index b0795353ec1..a5a6b7461a3 100644 --- a/app/assets/stylesheets/pages/environments.scss +++ b/app/assets/stylesheets/pages/environments.scss @@ -201,8 +201,9 @@ stroke-width: 1; } -.deploy-info-text { - dominant-baseline: text-before-edge; +.divider-line { + stroke-width: 1; + stroke: $gray-darkest; } .prometheus-state { @@ -312,6 +313,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/groups.scss b/app/assets/stylesheets/pages/groups.scss index 9b7dda9b648..f9a761e85fe 100644 --- a/app/assets/stylesheets/pages/groups.scss +++ b/app/assets/stylesheets/pages/groups.scss @@ -212,3 +212,15 @@ height: 50px; } } + +.user-access-role { + display: inline-block; + color: $gl-text-color-secondary; + font-size: 12px; + line-height: 20px; + margin: -5px 3px; + padding: 0 $label-padding; + border: 1px solid $border-color; + border-radius: $label-border-radius; + font-weight: $gl-font-weight-normal; +} diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 11ee1232bfe..2e26f2b5bbd 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; diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss index 443f5500684..92abe82df4c 100644 --- a/app/assets/stylesheets/pages/labels.scss +++ b/app/assets/stylesheets/pages/labels.scss @@ -104,7 +104,7 @@ } .color-label { - padding: 3px 7px; + padding: 3px $label-padding; border-radius: $label-border-radius; } diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index 5832cf4637f..2afb17334e3 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -384,6 +384,12 @@ } } +.nothing-here-block { + img { + width: 230px; + } +} + .mr-list { .merge-request { padding: 10px 0 10px 15px; diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss index 1e6992cb65e..ebb5d121433 100644 --- a/app/assets/stylesheets/pages/note_form.scss +++ b/app/assets/stylesheets/pages/note_form.scss @@ -141,20 +141,20 @@ .sidebar-item-icon { border-radius: $border-radius-default; - margin: 0 3px 0 -4px; - vertical-align: middle; + margin: 0 5px 0 0; + vertical-align: text-bottom; &.is-active { fill: $orange-600; } -} -.sidebar-collapsed-icon .sidebar-item-icon { - margin: 0; -} + .sidebar-collapsed-icon & { + margin: 0; + } -.sidebar-item-value .sidebar-item-icon { - fill: $theme-gray-700; + .sidebar-item-value & { + fill: $theme-gray-700; + } } .sidebar-item-warning-message { diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index a6009ab328e..4d5613c292b 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -619,26 +619,17 @@ ul.notes { } .note-role { + margin: 0 3px; +} + +.note-role-special { position: relative; display: inline-block; - color: $notes-role-color; + color: $gl-text-color-secondary; font-size: 12px; - line-height: 20px; - margin: 0 3px; - - &.note-role-access { - padding: 0 7px; - border: 1px solid $border-color; - border-radius: $label-border-radius; - } - - &.note-role-special { - text-shadow: 0 0 15px $gl-text-color-inverted; - } + text-shadow: 0 0 15px $gl-text-color-inverted; } - - /** * Line note button on the side of diffs */ diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss index 65b334662c2..5d14323e4bc 100644 --- a/app/assets/stylesheets/pages/tree.scss +++ b/app/assets/stylesheets/pages/tree.scss @@ -4,6 +4,11 @@ .nav-block { margin: 10px 0; + .btn .fa, + .btn svg { + color: $gl-text-color-secondary; + } + @media (min-width: $screen-sm-min) { display: flex; @@ -91,8 +96,12 @@ } .add-to-tree { - vertical-align: middle; - padding: 6px 10px; + vertical-align: top; + padding: 8px; + + svg { + top: 0; + } } .tree-table { 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/concerns/issuable_actions.rb b/app/controllers/concerns/issuable_actions.rb index 744e448e8df..281756af57a 100644 --- a/app/controllers/concerns/issuable_actions.rb +++ b/app/controllers/concerns/issuable_actions.rb @@ -21,11 +21,11 @@ module IssuableActions 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,6 +80,12 @@ 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 diff --git a/app/controllers/concerns/renders_member_access.rb b/app/controllers/concerns/renders_member_access.rb new file mode 100644 index 00000000000..d640378c24d --- /dev/null +++ b/app/controllers/concerns/renders_member_access.rb @@ -0,0 +1,23 @@ +module RendersMemberAccess + def prepare_groups_for_rendering(groups) + preload_max_member_access_for_collection(Group, groups) + + groups + end + + def prepare_projects_for_rendering(projects) + preload_max_member_access_for_collection(Project, projects) + + projects + end + + private + + def preload_max_member_access_for_collection(klass, collection) + return if !current_user || collection.blank? + + method_name = "max_member_access_for_#{klass.name.underscore}_ids" + + current_user.public_send(method_name, collection.ids) # rubocop:disable GitlabSecurity/PublicSend + end +end diff --git a/app/controllers/concerns/spammable_actions.rb b/app/controllers/concerns/spammable_actions.rb index ada0dde87fb..03d8e188093 100644 --- a/app/controllers/concerns/spammable_actions.rb +++ b/app/controllers/concerns/spammable_actions.rb @@ -23,8 +23,8 @@ module SpammableActions @spam_config_loaded = Gitlab::Recaptcha.load_configurations! 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 +33,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/uploads_actions.rb b/app/controllers/concerns/uploads_actions.rb index dec2e27335a..a6fb1f40001 100644 --- a/app/controllers/concerns/uploads_actions.rb +++ b/app/controllers/concerns/uploads_actions.rb @@ -1,4 +1,6 @@ module UploadsActions + include Gitlab::Utils::StrongMemoize + def create link_to_file = UploadService.new(model, params[:file], uploader_class).execute @@ -24,4 +26,25 @@ module UploadsActions send_file uploader.file.path, disposition: disposition end + + private + + def uploader + strong_memoize(:uploader) do + return if show_model.nil? + + file_uploader = FileUploader.new(show_model, params[:secret]) + file_uploader.retrieve_from_store!(params[:filename]) + + file_uploader + end + end + + def image_or_video? + uploader && uploader.exists? && uploader.image_or_video? + end + + def uploader_class + FileUploader + end end diff --git a/app/controllers/dashboard/projects_controller.rb b/app/controllers/dashboard/projects_controller.rb index d9884a47ec4..de9f8f9224a 100644 --- a/app/controllers/dashboard/projects_controller.rb +++ b/app/controllers/dashboard/projects_controller.rb @@ -1,5 +1,6 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController include ParamsBackwardCompatibility + include RendersMemberAccess before_action :set_non_archived_param before_action :default_sorting @@ -45,10 +46,12 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController end def load_projects(finder_params) - ProjectsFinder - .new(params: finder_params, current_user: current_user) - .execute - .includes(:route, :creator, namespace: [:route, :owner]) + projects = ProjectsFinder + .new(params: finder_params, current_user: current_user) + .execute + .includes(:route, :creator, namespace: [:route, :owner]) + + prepare_projects_for_rendering(projects) end def load_events diff --git a/app/controllers/explore/projects_controller.rb b/app/controllers/explore/projects_controller.rb index 762c6ebf3a3..c7273606a85 100644 --- a/app/controllers/explore/projects_controller.rb +++ b/app/controllers/explore/projects_controller.rb @@ -1,5 +1,6 @@ class Explore::ProjectsController < Explore::ApplicationController include ParamsBackwardCompatibility + include RendersMemberAccess before_action :set_non_archived_param @@ -49,10 +50,12 @@ class Explore::ProjectsController < Explore::ApplicationController private def load_projects - ProjectsFinder.new(current_user: current_user, params: params) - .execute - .includes(:route, namespace: :route) - .page(params[:page]) - .without_count + projects = ProjectsFinder.new(current_user: current_user, params: params) + .execute + .includes(:route, namespace: :route) + .page(params[:page]) + .without_count + + prepare_projects_for_rendering(projects) end end diff --git a/app/controllers/groups/group_members_controller.rb b/app/controllers/groups/group_members_controller.rb index 8fc234a62b1..5919bf54468 100644 --- a/app/controllers/groups/group_members_controller.rb +++ b/app/controllers/groups/group_members_controller.rb @@ -22,7 +22,7 @@ class Groups::GroupMembersController < Groups::ApplicationController end def update - @group_member = @group.group_members.find(params[:id]) + @group_member = @group.members_and_requesters.find(params[:id]) return render_403 unless can?(current_user, :update_group_member, @group_member) diff --git a/app/controllers/groups/uploads_controller.rb b/app/controllers/groups/uploads_controller.rb new file mode 100644 index 00000000000..e6bd9806401 --- /dev/null +++ b/app/controllers/groups/uploads_controller.rb @@ -0,0 +1,35 @@ +class Groups::UploadsController < Groups::ApplicationController + include UploadsActions + + skip_before_action :group, if: -> { action_name == 'show' && image_or_video? } + + before_action :authorize_upload_file!, only: [:create] + + private + + def show_model + strong_memoize(:show_model) do + group_id = params[:group_id] + + Group.find_by_full_path(group_id) + end + end + + def authorize_upload_file! + render_404 unless can?(current_user, :upload_file, group) + end + + def uploader + strong_memoize(:uploader) do + file_uploader = uploader_class.new(show_model, params[:secret]) + file_uploader.retrieve_from_store!(params[:filename]) + file_uploader + end + end + + def uploader_class + NamespaceFileUploader + end + + alias_method :model, :group +end 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/autocomplete_sources_controller.rb b/app/controllers/projects/autocomplete_sources_controller.rb index ffb54390965..45c66b63ea5 100644 --- a/app/controllers/projects/autocomplete_sources_controller.rb +++ b/app/controllers/projects/autocomplete_sources_controller.rb @@ -2,7 +2,7 @@ class Projects::AutocompleteSourcesController < Projects::ApplicationController before_action :load_autocomplete_service, except: [:members] def members - render json: ::Projects::ParticipantsService.new(@project, current_user).execute(noteable) + render json: ::Projects::ParticipantsService.new(@project, current_user).execute(target) end def issues @@ -14,7 +14,7 @@ class Projects::AutocompleteSourcesController < Projects::ApplicationController end def labels - render json: @autocomplete_service.labels + render json: @autocomplete_service.labels(target) end def milestones @@ -22,7 +22,7 @@ class Projects::AutocompleteSourcesController < Projects::ApplicationController end def commands - render json: @autocomplete_service.commands(noteable, params[:type]) + render json: @autocomplete_service.commands(target, params[:type]) end private @@ -31,13 +31,13 @@ class Projects::AutocompleteSourcesController < Projects::ApplicationController @autocomplete_service = ::Projects::AutocompleteService.new(@project, current_user) end - def noteable - case params[:type] - when 'Issue' + def target + case params[:type]&.downcase + when 'issue' IssuesFinder.new(current_user, project_id: @project.id).execute.find_by(iid: params[:type_id]) - when 'MergeRequest' + when 'mergerequest' MergeRequestsFinder.new(current_user, project_id: @project.id).execute.find_by(iid: params[:type_id]) - when 'Commit' + when 'commit' @project.commit(params[:type_id]) end end diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb index 6ff96a3f295..2e7344b1cad 100644 --- a/app/controllers/projects/commit_controller.rb +++ b/app/controllers/projects/commit_controller.rb @@ -134,6 +134,23 @@ class Projects::CommitController < Projects::ApplicationController @grouped_diff_discussions = commit.grouped_diff_discussions @discussions = commit.discussions + if merge_request_iid = params[:merge_request_iid] + @merge_request = MergeRequestsFinder.new(current_user, project_id: @project.id).find_by(iid: merge_request_iid) + + if @merge_request + @new_diff_note_attrs.merge!( + noteable_type: 'MergeRequest', + noteable_id: @merge_request.id + ) + + merge_request_commit_notes = @merge_request.notes.where(commit_id: @commit.id).inc_relations_for_view + merge_request_commit_diff_discussions = merge_request_commit_notes.grouped_diff_discussions(@commit.diff_refs) + @grouped_diff_discussions.merge!(merge_request_commit_diff_discussions) do |line_code, left, right| + left + right + end + end + end + @notes = (@grouped_diff_discussions.values.flatten + @discussions).flat_map(&:notes) @notes = prepare_notes_for_rendering(@notes, @commit) end diff --git a/app/controllers/projects/merge_requests/application_controller.rb b/app/controllers/projects/merge_requests/application_controller.rb index 1269759fc2b..793ae03fb88 100644 --- a/app/controllers/projects/merge_requests/application_controller.rb +++ b/app/controllers/projects/merge_requests/application_controller.rb @@ -28,7 +28,6 @@ class Projects::MergeRequests::ApplicationController < Projects::ApplicationCont :task_num, :title, :discussion_locked, - label_ids: [] ] end diff --git a/app/controllers/projects/merge_requests/diffs_controller.rb b/app/controllers/projects/merge_requests/diffs_controller.rb index 9f966889995..fe8525a488c 100644 --- a/app/controllers/projects/merge_requests/diffs_controller.rb +++ b/app/controllers/projects/merge_requests/diffs_controller.rb @@ -4,6 +4,7 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic include RendersNotes before_action :apply_diff_view_cookie! + before_action :commit before_action :define_diff_vars before_action :define_diff_comment_vars @@ -20,18 +21,33 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic private def define_diff_vars + @merge_request_diffs = @merge_request.merge_request_diffs.viewable.order_id_desc + @compare = commit || find_merge_request_diff_compare + return render_404 unless @compare + + @diffs = @compare.diffs(diff_options) + end + + def commit + return nil unless commit_id = params[:commit_id].presence + return nil unless @merge_request.all_commits.exists?(sha: commit_id) + + @commit ||= @project.commit(commit_id) + end + + def find_merge_request_diff_compare @merge_request_diff = - if params[:diff_id] - @merge_request.merge_request_diffs.viewable.find(params[:diff_id]) + if diff_id = params[:diff_id].presence + @merge_request.merge_request_diffs.viewable.find_by(id: diff_id) else @merge_request.merge_request_diff end - @merge_request_diffs = @merge_request.merge_request_diffs.viewable.order_id_desc + return unless @merge_request_diff + @comparable_diffs = @merge_request_diffs.select { |diff| diff.id < @merge_request_diff.id } - if params[:start_sha].present? - @start_sha = params[:start_sha] + if @start_sha = params[:start_sha].presence @start_version = @comparable_diffs.find { |diff| diff.head_commit_sha == @start_sha } unless @start_version @@ -40,20 +56,18 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic end end - @compare = - if @start_sha - @merge_request_diff.compare_with(@start_sha) - else - @merge_request_diff - end - - @diffs = @compare.diffs(diff_options) + if @start_sha + @merge_request_diff.compare_with(@start_sha) + else + @merge_request_diff + end end def define_diff_comment_vars @new_diff_note_attrs = { noteable_type: 'MergeRequest', - noteable_id: @merge_request.id + noteable_id: @merge_request.id, + commit_id: @commit&.id } @diff_notes_disabled = false diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 37acd1c9787..e7b3b73024b 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -7,11 +7,8 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo include IssuableCollections skip_before_action :merge_request, only: [:index, :bulk_update] - before_action :authorize_update_issuable!, only: [:close, :edit, :update, :remove_wip, :sort] - before_action :set_issuables_index, only: [:index] - before_action :authenticate_user!, only: [:assign_related_issues] def index diff --git a/app/controllers/projects/pipelines_settings_controller.rb b/app/controllers/projects/pipelines_settings_controller.rb index b890818c475..06ce7328fb5 100644 --- a/app/controllers/projects/pipelines_settings_controller.rb +++ b/app/controllers/projects/pipelines_settings_controller.rb @@ -29,7 +29,6 @@ class Projects::PipelinesSettingsController < Projects::ApplicationController :runners_token, :builds_enabled, :build_allow_git_fetch, :build_timeout_in_minutes, :build_coverage_regex, :public_builds, :auto_cancel_pending_pipelines, :ci_config_path, - :run_auto_devops_pipeline_implicit, :run_auto_devops_pipeline_explicit, auto_devops_attributes: [:id, :domain, :enabled] ) end diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb index d925dcd21ff..5a01a59481b 100644 --- a/app/controllers/projects/project_members_controller.rb +++ b/app/controllers/projects/project_members_controller.rb @@ -26,7 +26,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController end def update - @project_member = @project.project_members.find(params[:id]) + @project_member = @project.members_and_requesters.find(params[:id]) return render_403 unless can?(current_user, :update_project_member, @project_member) diff --git a/app/controllers/projects/uploads_controller.rb b/app/controllers/projects/uploads_controller.rb index 4d2fb17a19b..4685bbe80b4 100644 --- a/app/controllers/projects/uploads_controller.rb +++ b/app/controllers/projects/uploads_controller.rb @@ -8,31 +8,13 @@ class Projects::UploadsController < Projects::ApplicationController private - def uploader - return @uploader if defined?(@uploader) + def show_model + strong_memoize(:show_model) do + namespace = params[:namespace_id] + id = params[:project_id] - namespace = params[:namespace_id] - id = params[:project_id] - - file_project = Project.find_by_full_path("#{namespace}/#{id}") - - if file_project.nil? - @uploader = nil - return + Project.find_by_full_path("#{namespace}/#{id}") end - - @uploader = FileUploader.new(file_project, params[:secret]) - @uploader.retrieve_from_store!(params[:filename]) - - @uploader - end - - def image_or_video? - uploader && uploader.exists? && uploader.image_or_video? - end - - def uploader_class - FileUploader end alias_method :model, :project diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 3882fa4791d..8e9d6766d80 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -272,7 +272,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/controllers/users_controller.rb b/app/controllers/users_controller.rb index 5fca31b4956..575ec5c20f0 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -1,5 +1,6 @@ class UsersController < ApplicationController include RoutableActions + include RendersMemberAccess skip_before_action :authenticate_user! before_action :user, except: [:exists] @@ -116,14 +117,20 @@ class UsersController < ApplicationController @projects = PersonalProjectsFinder.new(user).execute(current_user) .page(params[:page]) + + prepare_projects_for_rendering(@projects) end def load_contributed_projects @contributed_projects = contributed_projects.joined(user) + + prepare_projects_for_rendering(@contributed_projects) end def load_groups @groups = JoinedGroupsFinder.new(user).execute(current_user) + + prepare_groups_for_rendering(@groups) end def load_snippets 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/auto_devops_helper.rb b/app/helpers/auto_devops_helper.rb index ec6194d204f..f4310ca2f06 100644 --- a/app/helpers/auto_devops_helper.rb +++ b/app/helpers/auto_devops_helper.rb @@ -8,22 +8,6 @@ module AutoDevopsHelper !project.ci_service end - def show_run_auto_devops_pipeline_checkbox_for_instance_setting?(project) - return false if project.repository.gitlab_ci_yml - - if project&.auto_devops&.enabled.present? - !project.auto_devops.enabled && current_application_settings.auto_devops_enabled? - else - current_application_settings.auto_devops_enabled? - end - end - - def show_run_auto_devops_pipeline_checkbox_for_explicit_setting?(project) - return false if project.repository.gitlab_ci_yml - - !project.auto_devops_enabled? - end - def auto_devops_warning_message(project) missing_domain = !project.auto_devops&.has_domain? missing_service = !project.deployment_platform&.active? 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/commits_helper.rb b/app/helpers/commits_helper.rb index f68e2cd3afa..2d304f7eb91 100644 --- a/app/helpers/commits_helper.rb +++ b/app/helpers/commits_helper.rb @@ -228,4 +228,12 @@ module CommitsHelper [commits, 0] end end + + def commit_path(project, commit, merge_request: nil) + if merge_request&.persisted? + diffs_project_merge_request_path(project, merge_request, commit_id: commit.id) + else + project_commit_path(project, commit) + end + end end diff --git a/app/helpers/markup_helper.rb b/app/helpers/markup_helper.rb index 1e4be2d4bcf..f78d41a0448 100644 --- a/app/helpers/markup_helper.rb +++ b/app/helpers/markup_helper.rb @@ -86,6 +86,8 @@ module MarkupHelper return '' unless text.present? context[:project] ||= @project + context[:group] ||= @group + html = markdown_unsafe(text, context) prepare_for_rendering(html, context) end diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb index 5b2c58d193d..ce57422f45d 100644 --- a/app/helpers/merge_requests_helper.rb +++ b/app/helpers/merge_requests_helper.rb @@ -101,6 +101,30 @@ module MergeRequestsHelper }.merge(merge_params_ee(merge_request)) end + def tab_link_for(merge_request, tab, options = {}, &block) + data_attrs = { + action: tab.to_s, + target: "##{tab}", + toggle: options.fetch(:force_link, false) ? '' : 'tab' + } + + url = case tab + when :show + data_attrs[:target] = '#notes' + method(:project_merge_request_path) + when :commits + method(:commits_project_merge_request_path) + when :pipelines + method(:pipelines_project_merge_request_path) + when :diffs + method(:diffs_project_merge_request_path) + else + raise "Cannot create tab #{tab}." + end + + link_to(url[merge_request.project, merge_request], data: data_attrs, &block) + end + def merge_params_ee(merge_request) {} end 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/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/ci/build.rb b/app/models/ci/build.rb index d2402b55184..85960f1b6bb 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -6,6 +6,8 @@ module Ci include Presentable include Importable + MissingDependenciesError = Class.new(StandardError) + belongs_to :runner belongs_to :trigger_request belongs_to :erased_by, class_name: 'User' @@ -139,6 +141,10 @@ module Ci Ci::Build.retry(build, build.user) end end + + before_transition any => [:running] do |build| + build.validates_dependencies! unless Feature.enabled?('ci_disable_validates_dependencies') + end end def detailed_status(current_user) @@ -478,6 +484,20 @@ module Ci options[:dependencies]&.empty? end + def validates_dependencies! + dependencies.each do |dependency| + raise MissingDependenciesError unless dependency.valid_dependency? + end + end + + def valid_dependency? + return false unless complete? + return false if artifacts_expired? + return false if erased? + + true + end + def hide_secrets(trace) return unless trace diff --git a/app/models/commit.rb b/app/models/commit.rb index 6b28d290f99..307e4fcedfe 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -1,3 +1,4 @@ +# coding: utf-8 class Commit extend ActiveModel::Naming extend Gitlab::Cache::RequestCache @@ -25,7 +26,7 @@ class Commit DIFF_HARD_LIMIT_FILES = 1000 DIFF_HARD_LIMIT_LINES = 50000 - MIN_SHA_LENGTH = 7 + MIN_SHA_LENGTH = Gitlab::Git::Commit::MIN_SHA_LENGTH COMMIT_SHA_PATTERN = /\h{#{MIN_SHA_LENGTH},40}/.freeze def banzai_render_context(field) diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index ee21ed8e420..c0263c0b4e2 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -43,7 +43,8 @@ class CommitStatus < ActiveRecord::Base script_failure: 1, api_failure: 2, stuck_or_timeout_failure: 3, - runner_system_failure: 4 + runner_system_failure: 4, + missing_dependency_failure: 5 } ## diff --git a/app/models/concerns/bulk_member_access_load.rb b/app/models/concerns/bulk_member_access_load.rb new file mode 100644 index 00000000000..984c4f53bf7 --- /dev/null +++ b/app/models/concerns/bulk_member_access_load.rb @@ -0,0 +1,46 @@ +# Returns and caches in thread max member access for a resource +# +module BulkMemberAccessLoad + extend ActiveSupport::Concern + + included do + # Determine the maximum access level for a group of resources in bulk. + # + # Returns a Hash mapping resource ID -> maximum access level. + def max_member_access_for_resource_ids(resource_klass, resource_ids, memoization_index = self.id, &block) + raise 'Block is mandatory' unless block_given? + + resource_ids = resource_ids.uniq + key = max_member_access_for_resource_key(resource_klass, memoization_index) + access = {} + + if RequestStore.active? + RequestStore.store[key] ||= {} + access = RequestStore.store[key] + end + + # Look up only the IDs we need + resource_ids = resource_ids - access.keys + + return access if resource_ids.empty? + + resource_access = yield(resource_ids) + + access.merge!(resource_access) + + missing_resource_ids = resource_ids - resource_access.keys + + missing_resource_ids.each do |resource_id| + access[resource_id] = Gitlab::Access::NO_ACCESS + end + + access + end + + private + + def max_member_access_for_resource_key(klass, memoization_index) + "max_member_access_for_#{klass.name.underscore.pluralize}:#{memoization_index}" + end + end +end diff --git a/app/models/concerns/discussion_on_diff.rb b/app/models/concerns/discussion_on_diff.rb index f5cbb3becad..4b4d519f3df 100644 --- a/app/models/concerns/discussion_on_diff.rb +++ b/app/models/concerns/discussion_on_diff.rb @@ -32,6 +32,10 @@ module DiscussionOnDiff first_note.position.new_path end + def on_merge_request_commit? + for_merge_request? && commit_id.present? + end + # Returns an array of at most 16 highlighted lines above a diff note def truncated_diff_lines(highlight: true) lines = highlight ? highlighted_diff_lines : diff_lines diff --git a/app/models/concerns/throttled_touch.rb b/app/models/concerns/throttled_touch.rb new file mode 100644 index 00000000000..ad0ff0f20d4 --- /dev/null +++ b/app/models/concerns/throttled_touch.rb @@ -0,0 +1,10 @@ +# ThrottledTouch can be used to throttle the number of updates triggered by +# calling "touch" on an ActiveRecord model. +module ThrottledTouch + # The amount of time to wait before "touch" can update a record again. + TOUCH_INTERVAL = 1.minute + + def touch(*args) + super if (Time.zone.now - updated_at) > TOUCH_INTERVAL + end +end diff --git a/app/models/diff_discussion.rb b/app/models/diff_discussion.rb index 6eba87da1a1..4a65738214b 100644 --- a/app/models/diff_discussion.rb +++ b/app/models/diff_discussion.rb @@ -24,7 +24,11 @@ class DiffDiscussion < Discussion return unless for_merge_request? return {} if active? - noteable.version_params_for(position.diff_refs) + if on_merge_request_commit? + { commit_id: commit_id } + else + noteable.version_params_for(position.diff_refs) + end end def reply_attributes diff --git a/app/models/diff_note.rb b/app/models/diff_note.rb index ae5f138a920..b53d44cda95 100644 --- a/app/models/diff_note.rb +++ b/app/models/diff_note.rb @@ -17,6 +17,7 @@ class DiffNote < Note validates :noteable_type, inclusion: { in: NOTEABLE_TYPES } validate :positions_complete validate :verify_supported + validate :diff_refs_match_commit, if: :for_commit? before_validation :set_original_position, on: :create before_validation :update_position, on: :create, if: :on_text? @@ -135,6 +136,12 @@ class DiffNote < Note errors.add(:position, "is invalid") end + def diff_refs_match_commit + return if self.original_position.diff_refs == self.commit.diff_refs + + errors.add(:commit_id, 'does not match the diff refs') + end + def keep_around_commits project.repository.keep_around(self.original_position.base_sha) project.repository.keep_around(self.original_position.start_sha) diff --git a/app/models/discussion.rb b/app/models/discussion.rb index 437df923d2d..92482a1a875 100644 --- a/app/models/discussion.rb +++ b/app/models/discussion.rb @@ -11,6 +11,7 @@ class Discussion :author, :noteable, + :commit_id, :for_commit?, :for_merge_request?, diff --git a/app/models/epic.rb b/app/models/epic.rb index 62898a02e2d..286b855de3f 100644 --- a/app/models/epic.rb +++ b/app/models/epic.rb @@ -1,7 +1,11 @@ # Placeholder class for model that is implemented in EE -# It will reserve (ee#3853) '&' as a reference prefix, but the table does not exists in CE +# It reserves '&' as a reference prefix, but the table does not exists in CE class Epic < ActiveRecord::Base - # TODO: this will be implemented as part of #3853 - def to_reference + def self.reference_prefix + '&' + end + + def self.reference_prefix_escaped + '&' 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/group.rb b/app/models/group.rb index 505e943e464..fddace03387 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -298,6 +298,10 @@ class Group < Namespace end end + def hashed_storage?(_feature) + false + end + private def update_two_factor_requirement diff --git a/app/models/issue.rb b/app/models/issue.rb index d6ef58d150b..bbda848c39d 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -9,6 +9,10 @@ class Issue < ActiveRecord::Base include FasterCacheKeys include RelativePositioning include TimeTrackable + include ThrottledTouch + include IgnorableColumn + + ignore_column :assignee_id DueDateStruct = Struct.new(:title, :name).freeze NoDueDate = DueDateStruct.new('No Due Date', '0').freeze diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index f2d639a3382..422f138c4ea 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -7,6 +7,7 @@ class MergeRequest < ActiveRecord::Base include TimeTrackable include ManualInverseAssociation include EachBatch + include ThrottledTouch ignore_column :locked_at, :ref_fetched @@ -648,6 +649,7 @@ class MergeRequest < ActiveRecord::Base .to_sql Note.from("(#{union}) #{Note.table_name}") + .includes(:noteable) end alias_method :discussion_notes, :related_notes @@ -924,21 +926,27 @@ class MergeRequest < ActiveRecord::Base .order(id: :desc) end - # Note that this could also return SHA from now dangling commits - # - def all_commit_shas - return commit_shas unless persisted? - - diffs_relation = merge_request_diffs - + def all_commits # MySQL doesn't support LIMIT in a subquery. - diffs_relation = diffs_relation.recent if Gitlab::Database.postgresql? + diffs_relation = if Gitlab::Database.postgresql? + merge_request_diffs.recent + else + merge_request_diffs + end MergeRequestDiffCommit .where(merge_request_diff: diffs_relation) .limit(10_000) - .pluck('sha') - .uniq + end + + # Note that this could also return SHA from now dangling commits + # + def all_commit_shas + @all_commit_shas ||= begin + return commit_shas unless persisted? + + all_commits.pluck(:sha).uniq + end end def merge_commit 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 340fe087f82..c4c2ab8e67d 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -15,6 +15,7 @@ class Note < ActiveRecord::Base include IgnorableColumn include Editable include Gitlab::SQL::Pattern + include ThrottledTouch module SpecialRole FIRST_TIME_CONTRIBUTOR = :first_time_contributor @@ -55,7 +56,7 @@ class Note < ActiveRecord::Base participant :author belongs_to :project - belongs_to :noteable, polymorphic: true, touch: true # rubocop:disable Cop/PolymorphicAssociations + belongs_to :noteable, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations belongs_to :author, class_name: "User" belongs_to :updated_by, class_name: "User" belongs_to :last_edited_by, class_name: 'User' @@ -118,6 +119,7 @@ class Note < ActiveRecord::Base before_validation :set_discussion_id, on: :create after_save :keep_around_commit, if: :for_project_noteable? after_save :expire_etag_cache + after_save :touch_noteable after_destroy :expire_etag_cache class << self @@ -228,16 +230,18 @@ class Note < ActiveRecord::Base for_personal_snippet? end + def commit + @commit ||= project.commit(commit_id) if commit_id.present? + end + # override to return commits, which are not active record def noteable - if for_commit? - @commit ||= project.commit(commit_id) - else - super - end - # Temp fix to prevent app crash - # if note commit id doesn't exist + return commit if for_commit? + + super rescue + # Temp fix to prevent app crash + # if note commit id doesn't exist nil end @@ -367,6 +371,42 @@ class Note < ActiveRecord::Base Gitlab::EtagCaching::Store.new.touch(key) end + def touch(*args) + # We're not using an explicit transaction here because this would in all + # cases result in all future queries going to the primary, even if no writes + # are performed. + # + # We touch the noteable first so its SELECT query can run before our writes, + # ensuring it runs on a secondary (if no prior write took place). + touch_noteable + super + end + + # By default Rails will issue an "SELECT *" for the relation, which is + # overkill for just updating the timestamps. To work around this we manually + # touch the data so we can SELECT only the columns we need. + def touch_noteable + # Commits are not stored in the DB so we can't touch them. + return if for_commit? + + assoc = association(:noteable) + + noteable_object = + if assoc.loaded? + noteable + else + # If the object is not loaded (e.g. when notes are loaded async) we + # _only_ want the data we actually need. + assoc.scope.select(:id, :updated_at).take + end + + noteable_object&.touch + end + + def banzai_render_context(field) + super.merge(noteable: noteable) + end + private def keep_around_commit 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 41657c171e2..6ae15a0a50f 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -227,7 +227,6 @@ class Project < ActiveRecord::Base delegate :members, to: :team, prefix: true delegate :add_user, :add_users, to: :team delegate :add_guest, :add_reporter, :add_developer, :add_master, to: :team - delegate :empty_repo?, to: :repository # Validations validates :creator, presence: true, on: :create @@ -499,6 +498,10 @@ class Project < ActiveRecord::Base auto_devops&.enabled.nil? && !current_application_settings.auto_devops_enabled? end + def empty_repo? + repository.empty? + end + def repository_storage_path Gitlab.config.repositories.storages[repository_storage].try(:[], 'path') end diff --git a/app/models/project_team.rb b/app/models/project_team.rb index 1d35426050e..c679758973a 100644 --- a/app/models/project_team.rb +++ b/app/models/project_team.rb @@ -1,4 +1,6 @@ class ProjectTeam + include BulkMemberAccessLoad + attr_accessor :project def initialize(project) @@ -157,39 +159,16 @@ class ProjectTeam # # Returns a Hash mapping user ID -> maximum access level. def max_member_access_for_user_ids(user_ids) - user_ids = user_ids.uniq - key = "max_member_access:#{project.id}" - - access = {} - - if RequestStore.active? - RequestStore.store[key] ||= {} - access = RequestStore.store[key] + max_member_access_for_resource_ids(User, user_ids, project.id) do |user_ids| + project.project_authorizations + .where(user: user_ids) + .group(:user_id) + .maximum(:access_level) end - - # Look up only the IDs we need - user_ids = user_ids - access.keys - - return access if user_ids.empty? - - users_access = project.project_authorizations - .where(user: user_ids) - .group(:user_id) - .maximum(:access_level) - - access.merge!(users_access) - - missing_user_ids = user_ids - users_access.keys - - missing_user_ids.each do |user_id| - access[user_id] = Gitlab::Access::NO_ACCESS - end - - access end def max_member_access(user_id) - max_member_access_for_user_ids([user_id])[user_id] || Gitlab::Access::NO_ACCESS + max_member_access_for_user_ids([user_id])[user_id] end private 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 1429774f0a9..d1ae3304e4a 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -37,7 +37,7 @@ class Repository issue_template_names merge_request_template_names).freeze # Methods that use cache_method but only memoize the value - MEMOIZED_CACHED_METHODS = %i(license empty_repo?).freeze + MEMOIZED_CACHED_METHODS = %i(license).freeze # Certain method caches should be refreshed when certain types of files are # changed. This Hash maps file types (as returned by Gitlab::FileDetector) to @@ -497,7 +497,11 @@ class Repository end cache_method :exists? - delegate :empty?, to: :raw_repository + def empty? + return true unless exists? + + !has_visible_content? + end cache_method :empty? # The size of this repository in megabytes. @@ -944,13 +948,8 @@ class Repository end end - def empty_repo? - !exists? || !has_visible_content? - end - cache_method :empty_repo?, memoize_only: true - def search_files_by_content(query, ref) - return [] if empty_repo? || query.blank? + return [] if empty? || query.blank? offset = 2 args = %W(grep -i -I -n --before-context #{offset} --after-context #{offset} -E -e #{Regexp.escape(query)} #{ref || root_ref}) @@ -959,7 +958,7 @@ class Repository end def search_files_by_name(query, ref) - return [] if empty_repo? || query.blank? + return [] if empty? || query.blank? args = %W(ls-tree --full-tree -r #{ref || root_ref} --name-status | #{Regexp.escape(query)}) 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 38ee4ed50c1..92b461ce3ed 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -17,6 +17,7 @@ class User < ActiveRecord::Base include FeatureGate include CreatedAtFilterable include IgnorableColumn + include BulkMemberAccessLoad DEFAULT_NOTIFICATION_LEVEL = :participating @@ -314,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 @@ -323,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 @@ -332,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 @@ -1053,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 @@ -1144,6 +1152,34 @@ class User < ActiveRecord::Base super end + # Determine the maximum access level for a group of projects in bulk. + # + # Returns a Hash mapping project ID -> maximum access level. + def max_member_access_for_project_ids(project_ids) + max_member_access_for_resource_ids(Project, project_ids) do |project_ids| + project_authorizations.where(project: project_ids) + .group(:project_id) + .maximum(:access_level) + end + end + + def max_member_access_for_project(project_id) + max_member_access_for_project_ids([project_id])[project_id] + end + + # Determine the maximum access level for a group of groups in bulk. + # + # Returns a Hash mapping project ID -> maximum access level. + def max_member_access_for_group_ids(group_ids) + max_member_access_for_resource_ids(Group, group_ids) do |group_ids| + group_members.where(source: group_ids).group(:source_id).maximum(:access_level) + end + end + + def max_member_access_for_group(group_id) + max_member_access_for_group_ids([group_id])[group_id] + end + protected # override, from Devise::Validatable diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb index a2518bc1080..d2d45e402b0 100644 --- a/app/policies/group_policy.rb +++ b/app/policies/group_policy.rb @@ -30,7 +30,12 @@ class GroupPolicy < BasePolicy rule { public_group } .enable :read_group rule { logged_in_viewable }.enable :read_group - rule { guest } .enable :read_group + + rule { guest }.policy do + enable :read_group + enable :upload_file + end + rule { admin } .enable :read_group rule { has_projects } .enable :read_group diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb index 1e5f2ed4dd2..85db2760e23 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) diff --git a/app/services/ci/register_job_service.rb b/app/services/ci/register_job_service.rb index 2ef76e03031..c8b6450c9b5 100644 --- a/app/services/ci/register_job_service.rb +++ b/app/services/ci/register_job_service.rb @@ -54,6 +54,9 @@ 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/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb index 434dda89db0..9f05535d4d4 100644 --- a/app/services/merge_requests/refresh_service.rb +++ b/app/services/merge_requests/refresh_service.rb @@ -6,7 +6,7 @@ module MergeRequests @oldrev, @newrev = oldrev, newrev @branch_name = Gitlab::Git.ref_name(ref) - find_new_commits + Gitlab::GitalyClient.allow_n_plus_1_calls(&method(:find_new_commits)) # Be sure to close outstanding MRs before reloading them to avoid generating an # empty diff during a manual merge close_merge_requests diff --git a/app/services/metrics_service.rb b/app/services/metrics_service.rb index 6b3939aeba5..236e9fe8c44 100644 --- a/app/services/metrics_service.rb +++ b/app/services/metrics_service.rb @@ -20,7 +20,7 @@ class MetricsService end def metrics_text - "#{health_metrics_text}#{prometheus_metrics_text}" + prometheus_metrics_text.concat(health_metrics_text) end private diff --git a/app/services/projects/autocomplete_service.rb b/app/services/projects/autocomplete_service.rb index 724a77c873a..1ae2c40872a 100644 --- a/app/services/projects/autocomplete_service.rb +++ b/app/services/projects/autocomplete_service.rb @@ -20,8 +20,23 @@ module Projects MergeRequestsFinder.new(current_user, project_id: project.id, state: 'opened').execute.select([:iid, :title]) end - def labels - LabelsFinder.new(current_user, project_id: project.id).execute.select([:title, :color]) + def labels(target = nil) + labels = LabelsFinder.new(current_user, project_id: project.id).execute.select([:color, :title]) + + return labels unless target&.respond_to?(:labels) + + issuable_label_titles = target.labels.pluck(:title) + + if issuable_label_titles + labels = labels.as_json(only: [:title, :color]) + + issuable_label_titles.each do |issuable_label_title| + found_label = labels.find { |label| label['title'] == issuable_label_title } + found_label[:set] = true if found_label + end + end + + labels end def commands(noteable, type) @@ -33,7 +48,7 @@ module Projects @project.merge_requests.build end - return [] unless noteable && noteable.is_a?(Issuable) + return [] unless noteable&.is_a?(Issuable) opts = { project: project, diff --git a/app/services/projects/fork_service.rb b/app/services/projects/fork_service.rb index eb5cce5ab98..03be7039b2a 100644 --- a/app/services/projects/fork_service.rb +++ b/app/services/projects/fork_service.rb @@ -1,6 +1,24 @@ module Projects class ForkService < BaseService - def execute + def execute(fork_to_project = nil) + if fork_to_project + link_existing_project(fork_to_project) + else + fork_new_project + end + end + + private + + def link_existing_project(fork_to_project) + return if fork_to_project.forked? + + link_fork_network(fork_to_project) + + fork_to_project + end + + def fork_new_project new_params = { forked_from_project_id: @project.id, visibility_level: allowed_visibility_level, @@ -21,15 +39,11 @@ module Projects builds_access_level = @project.project_feature.builds_access_level new_project.project_feature.update_attributes(builds_access_level: builds_access_level) - refresh_forks_count - link_fork_network(new_project) new_project end - private - def fork_network if @project.fork_network @project.fork_network @@ -43,9 +57,17 @@ module Projects end end - def link_fork_network(new_project) - fork_network.fork_network_members.create(project: new_project, + def link_fork_network(fork_to_project) + fork_network.fork_network_members.create(project: fork_to_project, forked_from_project: @project) + + # TODO: remove this when ForkedProjectLink model is removed + unless fork_to_project.forked_project_link + fork_to_project.create_forked_project_link(forked_to_project: fork_to_project, + forked_from_project: @project) + end + + refresh_forks_count end def refresh_forks_count diff --git a/app/services/projects/update_service.rb b/app/services/projects/update_service.rb index 72eecc61c96..ff4c73c886e 100644 --- a/app/services/projects/update_service.rb +++ b/app/services/projects/update_service.rb @@ -15,7 +15,7 @@ module Projects return error("Could not set the default branch") unless project.change_head(params[:default_branch]) end - if project.update_attributes(update_params) + if project.update_attributes(params.except(:default_branch)) if project.previous_changes.include?('path') project.rename_repo else @@ -32,15 +32,13 @@ module Projects end def run_auto_devops_pipeline? - params.dig(:run_auto_devops_pipeline_explicit) == 'true' || params.dig(:run_auto_devops_pipeline_implicit) == 'true' + return false if project.repository.gitlab_ci_yml || !project.auto_devops.previous_changes.include?('enabled') + + project.auto_devops.enabled? || (project.auto_devops.enabled.nil? && current_application_settings.auto_devops_enabled?) end private - def update_params - params.except(:default_branch, :run_auto_devops_pipeline_explicit, :run_auto_devops_pipeline_implicit) - end - def renaming_project_with_container_registry_tags? new_path = params[:path] diff --git a/app/services/protected_branches/access_level_params.rb b/app/services/protected_branches/access_level_params.rb new file mode 100644 index 00000000000..253ae8b0124 --- /dev/null +++ b/app/services/protected_branches/access_level_params.rb @@ -0,0 +1,33 @@ +module ProtectedBranches + class AccessLevelParams + attr_reader :type, :params + + def initialize(type, params) + @type = type + @params = params_with_default(params) + end + + def access_levels + ce_style_access_level + end + + private + + def params_with_default(params) + params[:"#{type}_access_level"] ||= Gitlab::Access::MASTER if use_default_access_level?(params) + params + end + + def use_default_access_level?(params) + true + end + + def ce_style_access_level + access_level = params[:"#{type}_access_level"] + + return [] unless access_level + + [{ access_level: access_level }] + end + end +end diff --git a/app/services/protected_branches/api_service.rb b/app/services/protected_branches/api_service.rb new file mode 100644 index 00000000000..4b40200644b --- /dev/null +++ b/app/services/protected_branches/api_service.rb @@ -0,0 +1,24 @@ +module ProtectedBranches + class ApiService < BaseService + def create + @push_params = AccessLevelParams.new(:push, params) + @merge_params = AccessLevelParams.new(:merge, params) + + verify_params! + + protected_branch_params = { + name: params[:name], + push_access_levels_attributes: @push_params.access_levels, + merge_access_levels_attributes: @merge_params.access_levels + } + + ::ProtectedBranches::CreateService.new(@project, @current_user, protected_branch_params).execute + end + + private + + def verify_params! + # EE-only + end + end +end diff --git a/app/uploaders/file_uploader.rb b/app/uploaders/file_uploader.rb index 71658df5b41..0b591e3bbbb 100644 --- a/app/uploaders/file_uploader.rb +++ b/app/uploaders/file_uploader.rb @@ -29,11 +29,11 @@ class FileUploader < GitlabUploader # model - Object that responds to `full_path` and `disk_path` # # Returns a String without a trailing slash - def self.dynamic_path_segment(project) - if project.hashed_storage?(:attachments) - dynamic_path_builder(project.disk_path) + def self.dynamic_path_segment(model) + if model.hashed_storage?(:attachments) + dynamic_path_builder(model.disk_path) else - dynamic_path_builder(project.full_path) + dynamic_path_builder(model.full_path) end end diff --git a/app/uploaders/namespace_file_uploader.rb b/app/uploaders/namespace_file_uploader.rb new file mode 100644 index 00000000000..672126e9ec2 --- /dev/null +++ b/app/uploaders/namespace_file_uploader.rb @@ -0,0 +1,15 @@ +class NamespaceFileUploader < FileUploader + def self.base_dir + File.join(root_dir, '-', 'system', 'namespace') + end + + def self.dynamic_path_segment(model) + dynamic_path_builder(model.id.to_s) + end + + private + + def secure_url + File.join('/uploads', @secret, file.filename) + end +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/dashboard/projects/_blank_state_admin_welcome.html.haml b/app/views/dashboard/projects/_blank_state_admin_welcome.html.haml index 573a4b93d67..c50b20a83dc 100644 --- a/app/views/dashboard/projects/_blank_state_admin_welcome.html.haml +++ b/app/views/dashboard/projects/_blank_state_admin_welcome.html.haml @@ -10,7 +10,7 @@ Projects are where you store your code, access issues, wiki and other features of GitLab. - if current_user.can_create_group? - = link_to admin_root_path, class: "blank-state-link" do + = link_to new_group_path, class: "blank-state-link" do .blank-state .blank-state-icon = custom_icon("add_new_group", size: 50) diff --git a/app/views/dashboard/projects/_projects.html.haml b/app/views/dashboard/projects/_projects.html.haml index 0ebd7c01bab..9e0e908e656 100644 --- a/app/views/dashboard/projects/_projects.html.haml +++ b/app/views/dashboard/projects/_projects.html.haml @@ -1 +1 @@ -= render 'shared/projects/list', projects: @projects, ci: true += render 'shared/projects/list', projects: @projects, ci: true, user: current_user diff --git a/app/views/discussions/_discussion.html.haml b/app/views/discussions/_discussion.html.haml index 0f03163a2e8..205320ed87c 100644 --- a/app/views/discussions/_discussion.html.haml +++ b/app/views/discussions/_discussion.html.haml @@ -32,9 +32,17 @@ - elsif discussion.diff_discussion? on = conditional_link_to url.present?, url do - - unless discussion.active? - an old version of - the diff + - if discussion.on_merge_request_commit? + - unless discussion.active? + an outdated change in + commit + + %span.commit-sha= Commit.truncate_sha(discussion.commit_id) + - else + - unless discussion.active? + an old version of + the diff + = time_ago_with_tooltip(discussion.created_at, placement: "bottom", html_class: "note-created-ago") = render "discussions/headline", discussion: discussion diff --git a/app/views/explore/projects/_projects.html.haml b/app/views/explore/projects/_projects.html.haml index 708fbc27f55..67f2f897137 100644 --- a/app/views/explore/projects/_projects.html.haml +++ b/app/views/explore/projects/_projects.html.haml @@ -1 +1 @@ -= render 'shared/projects/list', projects: projects += render 'shared/projects/list', projects: projects, user: current_user diff --git a/app/views/layouts/_init_auto_complete.html.haml b/app/views/layouts/_init_auto_complete.html.haml index fe0ec35d003..4276e6ee4bb 100644 --- a/app/views/layouts/_init_auto_complete.html.haml +++ b/app/views/layouts/_init_auto_complete.html.haml @@ -10,7 +10,7 @@ members: "#{members_project_autocomplete_sources_path(project, type: noteable_type, type_id: params[:id])}", issues: "#{issues_project_autocomplete_sources_path(project)}", mergeRequests: "#{merge_requests_project_autocomplete_sources_path(project)}", - labels: "#{labels_project_autocomplete_sources_path(project)}", + labels: "#{labels_project_autocomplete_sources_path(project, type: noteable_type, type_id: params[:id])}", milestones: "#{milestones_project_autocomplete_sources_path(project)}", commands: "#{commands_project_autocomplete_sources_path(project, type: noteable_type, type_id: params[:id])}" }; diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml index 1fd301d6850..25ed610466a 100644 --- a/app/views/layouts/_page.html.haml +++ b/app/views/layouts/_page.html.haml @@ -2,6 +2,7 @@ - if defined?(nav) && nav = render "layouts/nav/sidebar/#{nav}" .content-wrapper.page-with-new-nav + = render 'shared/outdated_browser' .mobile-overlay .alert-wrapper = render "layouts/broadcast" 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 4c5cc249159..52587760ba4 100644 --- a/app/views/layouts/_search.html.haml +++ b/app/views/layouts/_search.html.haml @@ -13,7 +13,15 @@ .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: { toggle: 'dropdown', 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 %ul diff --git a/app/views/layouts/group.html.haml b/app/views/layouts/group.html.haml index 08bd6fc311e..bfbfeee7c4b 100644 --- a/app/views/layouts/group.html.haml +++ b/app/views/layouts/group.html.haml @@ -4,4 +4,10 @@ - nav "group" - @left_sidebar = true +- content_for :page_specific_javascripts do + - if current_user + -# haml-lint:disable InlineJavaScript + :javascript + window.uploads_path = "#{group_uploads_path(@group)}"; + = render template: "layouts/application" diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml index e2407f6a428..99e7f3b568d 100644 --- a/app/views/layouts/header/_default.html.haml +++ b/app/views/layouts/header/_default.html.haml @@ -75,5 +75,3 @@ %span.sr-only Toggle navigation = sprite_icon('more', size: 12, css_class: 'more-icon js-navbar-toggle-right') = sprite_icon('close', size: 12, css_class: 'close-icon js-navbar-toggle-left') - -= render 'shared/outdated_browser' 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/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/_md_preview.html.haml b/app/views/projects/_md_preview.html.haml index 2cd5d0c60ea..c5e3a7945bd 100644 --- a/app/views/projects/_md_preview.html.haml +++ b/app/views/projects/_md_preview.html.haml @@ -2,7 +2,7 @@ - if defined?(@merge_request) && @merge_request.discussion_locked? .issuable-note-warning - = icon('lock', class: 'icon') + = sprite_icon('lock', size: 16, css_class: 'icon') %span = _('This merge request is locked.') = _('Only project members can comment.') 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/_advanced_settings.html.haml b/app/views/projects/clusters/_advanced_settings.html.haml index 2b3095eb94b..7032b892029 100644 --- a/app/views/projects/clusters/_advanced_settings.html.haml +++ b/app/views/projects/clusters/_advanced_settings.html.haml @@ -2,14 +2,14 @@ - if @cluster.managed? .append-bottom-20 %label.append-bottom-10 - = s_('ClusterIntegration|Google Container Engine') + = s_('ClusterIntegration|Google Kubernetes Engine') %p - - link_gke = link_to(s_('ClusterIntegration|Google Container Engine'), @cluster.gke_cluster_url, target: '_blank', rel: 'noopener noreferrer') + - link_gke = link_to(s_('ClusterIntegration|Google Kubernetes Engine'), @cluster.gke_cluster_url, target: '_blank', rel: 'noopener noreferrer') = s_('ClusterIntegration|Manage your cluster by visiting %{link_gke}').html_safe % { link_gke: link_gke } .well.form-group %label.text-danger = s_('ClusterIntegration|Remove cluster integration') %p - = s_('ClusterIntegration|Removing cluster integration will remove the cluster configuration you have added to this project. It will not delete your cluster on Google Container Engine.') - = link_to(s_('ClusterIntegration|Remove integration'), namespace_project_cluster_path(@project.namespace, @project, @cluster.id), method: :delete, class: 'btn btn-danger', data: { confirm: "Are you sure you want to remove cluster integration from this project? This will not delete your cluster on Google Container Engine"}) + = s_('ClusterIntegration|Removing cluster integration will remove the cluster configuration you have added to this project. It will not delete your cluster on Google Kubernetes Engine.') + = link_to(s_('ClusterIntegration|Remove integration'), namespace_project_cluster_path(@project.namespace, @project, @cluster.id), method: :delete, class: 'btn btn-danger', data: { confirm: "Are you sure you want to remove cluster integration from this project? This will not delete your cluster on Google Kubernetes Engine"}) diff --git a/app/views/projects/clusters/_banner.html.haml b/app/views/projects/clusters/_banner.html.haml index a1cc66eac92..76a66fb92a2 100644 --- a/app/views/projects/clusters/_banner.html.haml +++ b/app/views/projects/clusters/_banner.html.haml @@ -2,14 +2,14 @@ .settings-content .hidden.js-cluster-error.alert.alert-danger.alert-block.append-bottom-10{ role: 'alert' } - = s_('ClusterIntegration|Something went wrong while creating your cluster on Google Container Engine') + = s_('ClusterIntegration|Something went wrong while creating your cluster on Google Kubernetes Engine') %p.js-error-reason .hidden.js-cluster-creating.alert.alert-info.alert-block.append-bottom-10{ role: 'alert' } - = s_('ClusterIntegration|Cluster is being created on Google Container Engine...') + = s_('ClusterIntegration|Cluster is being created on Google Kubernetes Engine...') .hidden.js-cluster-success.alert.alert-success.alert-block.append-bottom-10{ role: 'alert' } - = s_('ClusterIntegration|Cluster was successfully created on Google Container Engine. Refresh the page to see cluster\'s details') + = s_('ClusterIntegration|Cluster was successfully created on Google Kubernetes Engine. Refresh the page to see cluster\'s details') %p - if @cluster.enabled? diff --git a/app/views/projects/clusters/_dropdown.html.haml b/app/views/projects/clusters/_dropdown.html.haml index 39188c7ca27..e36dd900f8d 100644 --- a/app/views/projects/clusters/_dropdown.html.haml +++ b/app/views/projects/clusters/_dropdown.html.haml @@ -7,6 +7,6 @@ = icon('chevron-down') %ul.dropdown-menu.clusters-dropdown-menu.dropdown-menu-full-width %li - = link_to(s_('ClusterIntegration|Create cluster on Google Container Engine'), gcp_new_namespace_project_clusters_path(@project.namespace, @project)) + = link_to(s_('ClusterIntegration|Create cluster on Google Kubernetes Engine'), gcp_new_namespace_project_clusters_path(@project.namespace, @project)) %li = link_to(s_('ClusterIntegration|Add an existing cluster'), user_new_namespace_project_clusters_path(@project.namespace, @project)) diff --git a/app/views/projects/clusters/gcp/_header.html.haml b/app/views/projects/clusters/gcp/_header.html.haml index cddb53c2688..f23d5b80e4f 100644 --- a/app/views/projects/clusters/gcp/_header.html.haml +++ b/app/views/projects/clusters/gcp/_header.html.haml @@ -4,11 +4,11 @@ = s_('ClusterIntegration|Please make sure that your Google account meets the following requirements:') %ul %li - - link_to_container_engine = link_to(s_('ClusterIntegration|access to Google Container Engine'), 'https://console.cloud.google.com', target: '_blank', rel: 'noopener noreferrer') - = s_('ClusterIntegration|Your account must have %{link_to_container_engine}').html_safe % { link_to_container_engine: link_to_container_engine } + - link_to_kubernetes_engine = link_to(s_('ClusterIntegration|access to Google Kubernetes Engine'), 'https://console.cloud.google.com', target: '_blank', rel: 'noopener noreferrer') + = s_('ClusterIntegration|Your account must have %{link_to_kubernetes_engine}').html_safe % { link_to_kubernetes_engine: link_to_kubernetes_engine } %li - - link_to_requirements = link_to(s_('ClusterIntegration|meets the requirements'), 'https://cloud.google.com/container-engine/docs/quickstart', target: '_blank', rel: 'noopener noreferrer') + - link_to_requirements = link_to(s_('ClusterIntegration|meets the requirements'), 'https://cloud.google.com/kubernetes-engine/docs/quickstart', target: '_blank', rel: 'noopener noreferrer') = s_('ClusterIntegration|Make sure your account %{link_to_requirements} to create clusters').html_safe % { link_to_requirements: link_to_requirements } %li - - link_to_container_project = link_to(s_('ClusterIntegration|Google Container Engine project'), target: '_blank', rel: 'noopener noreferrer') + - link_to_container_project = link_to(s_('ClusterIntegration|Google Kubernetes Engine project'), target: '_blank', rel: 'noopener noreferrer') = s_('ClusterIntegration|This account must have permissions to create a cluster in the %{link_to_container_project} specified below').html_safe % { link_to_container_project: link_to_container_project } diff --git a/app/views/projects/clusters/gcp/login.html.haml b/app/views/projects/clusters/gcp/login.html.haml index 790ba61fd86..e97ce01893a 100644 --- a/app/views/projects/clusters/gcp/login.html.haml +++ b/app/views/projects/clusters/gcp/login.html.haml @@ -5,7 +5,7 @@ .col-sm-4 = render 'projects/clusters/sidebar' .col-sm-8 - = render 'projects/clusters/dropdown', dropdown_text: s_('ClusterIntegration|Create cluster on Google Container Engine') + = render 'projects/clusters/dropdown', dropdown_text: s_('ClusterIntegration|Create cluster on Google Kubernetes Engine') = render 'header' .row .col-sm-8.col-sm-offset-4.signin-with-google diff --git a/app/views/projects/clusters/gcp/new.html.haml b/app/views/projects/clusters/gcp/new.html.haml index 9a79480c82f..8d92fb1e320 100644 --- a/app/views/projects/clusters/gcp/new.html.haml +++ b/app/views/projects/clusters/gcp/new.html.haml @@ -5,6 +5,6 @@ .col-sm-4 = render 'projects/clusters/sidebar' .col-sm-8 - = render 'projects/clusters/dropdown', dropdown_text: s_('ClusterIntegration|Create cluster on Google Container Engine') + = render 'projects/clusters/dropdown', dropdown_text: s_('ClusterIntegration|Create cluster on Google Kubernetes Engine') = render 'header' = render 'form' diff --git a/app/views/projects/clusters/new.html.haml b/app/views/projects/clusters/new.html.haml index 2e5bc34f64a..ddd13f8ea96 100644 --- a/app/views/projects/clusters/new.html.haml +++ b/app/views/projects/clusters/new.html.haml @@ -7,7 +7,7 @@ .col-sm-8 %h4.prepend-top-0= s_('ClusterIntegration|Choose how to set up cluster integration') - %p= s_('ClusterIntegration|Create a new cluster on Google Engine right from GitLab') + %p= s_('ClusterIntegration|Create a new cluster on Google Kubernetes Engine right from GitLab') = link_to s_('ClusterIntegration|Create on GKE'), gcp_new_namespace_project_clusters_path(@project.namespace, @project), class: 'btn append-bottom-20' %p= s_('ClusterIntegration|Enter the details for an existing Kubernetes cluster') = link_to s_('ClusterIntegration|Add an existing cluster'), user_new_namespace_project_clusters_path(@project.namespace, @project), class: 'btn append-bottom-20' diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml index 5f607c2ab25..09934c09865 100644 --- a/app/views/projects/commit/_commit_box.html.haml +++ b/app/views/projects/commit/_commit_box.html.haml @@ -47,7 +47,7 @@ %li= link_to s_("DownloadCommit|Email Patches"), project_commit_path(@project, @commit, format: :patch) %li= link_to s_("DownloadCommit|Plain Diff"), project_commit_path(@project, @commit, format: :diff) -.commit-box +.commit-box{ data: { project_path: project_path(@project) } } %h3.commit-title = markdown(@commit.title, pipeline: :single_line, author: @commit.author) - if @commit.description.present? @@ -80,3 +80,13 @@ - if last_pipeline.duration in = time_interval_in_words last_pipeline.duration + + - if @merge_request + .well-segment + = icon('info-circle fw') + + This commit is part of merge request + = succeed '.' do + = link_to @merge_request.to_reference, diffs_project_merge_request_path(@project, @merge_request, commit_id: @commit.id) + + Comments created here will be created in the context of that merge request. diff --git a/app/views/projects/commit/show.html.haml b/app/views/projects/commit/show.html.haml index abb292f8f27..2890e9d2b65 100644 --- a/app/views/projects/commit/show.html.haml +++ b/app/views/projects/commit/show.html.haml @@ -6,6 +6,9 @@ - @content_class = limited_container_width - page_title "#{@commit.title} (#{@commit.short_id})", "Commits" - page_description @commit.description +- content_for :page_specific_javascripts do + = page_specific_javascript_bundle_tag('common_vue') + = page_specific_javascript_bundle_tag('diff_notes') .container-fluid{ class: [limited_container_width, container_class] } = render "commit_box" diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml index 1b91a94a9f8..618a6355d23 100644 --- a/app/views/projects/commits/_commit.html.haml +++ b/app/views/projects/commits/_commit.html.haml @@ -1,7 +1,18 @@ -- ref = local_assigns.fetch(:ref) - -- cache_key = [project.full_path, commit.id, current_application_settings, @path.presence, current_controller?(:commits), I18n.locale] -- cache_key.push(commit.status(ref)) if commit.status(ref) +- view_details = local_assigns.fetch(:view_details, false) +- merge_request = local_assigns.fetch(:merge_request, nil) +- project = local_assigns.fetch(:project) { merge_request&.project } +- ref = local_assigns.fetch(:ref) { merge_request&.source_branch } + +- link = commit_path(project, commit, merge_request: merge_request) +- cache_key = [project.full_path, + commit.id, + current_application_settings, + @path.presence, + current_controller?(:commits), + merge_request&.iid, + view_details, + commit.status(ref), + I18n.locale].compact = cache(cache_key, expires_in: 1.day) do %li.commit.flex-row.js-toggle-container{ id: "commit-#{commit.short_id}" } @@ -11,7 +22,7 @@ .commit-detail .commit-content - = link_to_markdown_field(commit, :title, project_commit_path(project, commit.id), class: "commit-row-message item-title") + = link_to_markdown_field(commit, :title, link, class: "commit-row-message item-title") %span.commit-row-message.visible-xs-inline · = commit.short_id @@ -31,8 +42,7 @@ - commit_text = _('%{commit_author_link} committed %{commit_timeago}') % { commit_author_link: commit_author_link, commit_timeago: commit_timeago } #{ commit_text.html_safe } - - .commit-actions.hidden-xs + .commit-actions.flex-row.hidden-xs - if request.xhr? = render partial: 'projects/commit/signature', object: commit.signature - else @@ -41,6 +51,9 @@ - if commit.status(ref) = render_commit_status(commit, ref: ref) - = link_to commit.short_id, project_commit_path(project, commit), class: "commit-sha btn btn-transparent btn-link" + = link_to commit.short_id, link, class: "commit-sha btn btn-transparent btn-link" = clipboard_button(text: commit.id, title: _("Copy commit SHA to clipboard")) = link_to_browse_code(project, commit) + + - if view_details && merge_request + = link_to "View details", project_commit_path(project, commit.id, merge_request_iid: merge_request.iid), class: "btn btn-default" diff --git a/app/views/projects/commits/_commits.html.haml b/app/views/projects/commits/_commits.html.haml index d14897428d0..ac6852751be 100644 --- a/app/views/projects/commits/_commits.html.haml +++ b/app/views/projects/commits/_commits.html.haml @@ -1,4 +1,7 @@ -- ref = local_assigns.fetch(:ref) +- merge_request = local_assigns.fetch(:merge_request, nil) +- project = local_assigns.fetch(:project) { merge_request&.project } +- ref = local_assigns.fetch(:ref) { merge_request&.source_branch } + - commits, hidden = limited_commits(@commits) - commits.chunk { |c| c.committed_date.in_time_zone.to_date }.each do |day, commits| @@ -8,7 +11,7 @@ %li.commits-row{ data: { day: day } } %ul.content-list.commit-list.flex-list - = render partial: 'projects/commits/commit', collection: commits, locals: { project: project, ref: ref } + = render partial: 'projects/commits/commit', collection: commits, locals: { project: project, ref: ref, merge_request: merge_request } - if hidden > 0 %li.alert.alert-warning 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/environments/metrics.html.haml b/app/views/projects/environments/metrics.html.haml index e0aedcac5e1..56cf16c3778 100644 --- a/app/views/projects/environments/metrics.html.haml +++ b/app/views/projects/environments/metrics.html.haml @@ -19,4 +19,6 @@ "empty-loading-svg-path": image_path('illustrations/monitoring/loading'), "empty-unable-to-connect-svg-path": image_path('illustrations/monitoring/unable_to_connect'), "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 431d5a58daf..2f7aece7440 100644 --- a/app/views/projects/issues/show.html.haml +++ b/app/views/projects/issues/show.html.haml @@ -74,10 +74,10 @@ = edited_time_ago_with_tooltip(@issue, placement: 'bottom', html_class: 'issue-edited-ago js-issue-edited-ago') - #merge-requests{ data: { url: referenced_merge_requests_project_issue_url(@project, @issue) } } + #merge-requests{ data: { url: referenced_merge_requests_project_issue_path(@project, @issue) } } // This element is filled in using JavaScript. - #related-branches{ data: { url: related_branches_project_issue_url(@project, @issue) } } + #related-branches{ data: { url: related_branches_project_issue_path(@project, @issue) } } // This element is filled in using JavaScript. .content-block.emoji-block diff --git a/app/views/projects/merge_requests/_commits.html.haml b/app/views/projects/merge_requests/_commits.html.haml index 11793919ff7..b414518b597 100644 --- a/app/views/projects/merge_requests/_commits.html.haml +++ b/app/views/projects/merge_requests/_commits.html.haml @@ -5,4 +5,4 @@ = custom_icon ('illustration_no_commits') - else %ol#commits-list.list-unstyled - = render "projects/commits/commits", project: @merge_request.source_project, ref: @merge_request.source_branch + = render "projects/commits/commits", merge_request: @merge_request diff --git a/app/views/projects/merge_requests/diffs/_commit_widget.html.haml b/app/views/projects/merge_requests/diffs/_commit_widget.html.haml new file mode 100644 index 00000000000..2e5594f8cbe --- /dev/null +++ b/app/views/projects/merge_requests/diffs/_commit_widget.html.haml @@ -0,0 +1,5 @@ +- if @commit + .info-well.hidden-xs.prepend-top-default + .well-segment + %ul.blob-commit-info + = render 'projects/commits/commit', commit: @commit, merge_request: @merge_request, view_details: true diff --git a/app/views/projects/merge_requests/diffs/_different_base.html.haml b/app/views/projects/merge_requests/diffs/_different_base.html.haml new file mode 100644 index 00000000000..0e57066f9c9 --- /dev/null +++ b/app/views/projects/merge_requests/diffs/_different_base.html.haml @@ -0,0 +1,11 @@ +- if @merge_request_diff && different_base?(@start_version, @merge_request_diff) + .mr-version-controls + .content-block + = icon('info-circle') + Selected versions have different base commits. + Changes will include + = link_to project_compare_path(@project, from: @start_version.base_commit_sha, to: @merge_request_diff.base_commit_sha) do + new commits + from + = succeed '.' do + %code.ref-name= @merge_request.target_branch diff --git a/app/views/projects/merge_requests/diffs/_diffs.html.haml b/app/views/projects/merge_requests/diffs/_diffs.html.haml index 0d30d6da68f..60c91024b23 100644 --- a/app/views/projects/merge_requests/diffs/_diffs.html.haml +++ b/app/views/projects/merge_requests/diffs/_diffs.html.haml @@ -1,5 +1,18 @@ -- if @merge_request_diff.collected? || @merge_request_diff.overflow? - = render 'projects/merge_requests/diffs/versions' - = render "projects/diffs/diffs", diffs: @diffs, environment: @environment, merge_request: true -- elsif @merge_request_diff.empty? - .nothing-here-block Nothing to merge from #{@merge_request.source_branch} into #{@merge_request.target_branch} += render 'projects/merge_requests/diffs/version_controls' += render 'projects/merge_requests/diffs/different_base' += render 'projects/merge_requests/diffs/not_all_comments_displayed' += render 'projects/merge_requests/diffs/commit_widget' + +- if @merge_request_diff&.empty? + .nothing-here-block + = image_tag 'illustrations/merge_request_changes_empty.svg' + = succeed '.' do + No changes between + %span.ref-name= @merge_request.source_branch + and + %span.ref-name= @merge_request.target_branch + %p= link_to 'Create commit', project_new_blob_path(@project, @merge_request.source_branch), class: 'btn btn-save' +- else + - diff_viewable = @merge_request_diff ? @merge_request_diff.collected? || @merge_request_diff.overflow? : true + - if diff_viewable + = render "projects/diffs/diffs", diffs: @diffs, environment: @environment, merge_request: true diff --git a/app/views/projects/merge_requests/diffs/_not_all_comments_displayed.html.haml b/app/views/projects/merge_requests/diffs/_not_all_comments_displayed.html.haml new file mode 100644 index 00000000000..529fbb8547a --- /dev/null +++ b/app/views/projects/merge_requests/diffs/_not_all_comments_displayed.html.haml @@ -0,0 +1,17 @@ +- if @commit || @start_version || (@merge_request_diff && !@merge_request_diff.latest?) + .mr-version-controls + .content-block.comments-disabled-notif.clearfix + = icon('info-circle') + = succeed '.' do + - if @commit + Only comments from the following commit are shown below + - else + Not all comments are displayed because you're + - if @start_version + comparing two versions of the diff + - else + viewing an old version of the diff + .pull-right + = link_to diffs_project_merge_request_path(@merge_request.project, @merge_request), class: 'btn btn-sm' do + Show latest version + = "of the diff" if @commit diff --git a/app/views/projects/merge_requests/diffs/_versions.html.haml b/app/views/projects/merge_requests/diffs/_version_controls.html.haml index 9f7152b9824..1c26f0405d2 100644 --- a/app/views/projects/merge_requests/diffs/_versions.html.haml +++ b/app/views/projects/merge_requests/diffs/_version_controls.html.haml @@ -1,4 +1,4 @@ -- if @merge_request_diffs.size > 1 +- if @merge_request_diff && @merge_request_diffs.size > 1 .mr-version-controls .mr-version-menus-container.content-block Changes between @@ -71,27 +71,3 @@ (base) %div %strong.commit-sha= short_sha(@merge_request_diff.base_commit_sha) - - - if different_base?(@start_version, @merge_request_diff) - .content-block - = icon('info-circle') - Selected versions have different base commits. - Changes will include - = link_to project_compare_path(@project, from: @start_version.base_commit_sha, to: @merge_request_diff.base_commit_sha) do - new commits - from - = succeed '.' do - %code= @merge_request.target_branch - - - if @start_version || !@merge_request_diff.latest? - .comments-disabled-notif.content-block - = icon('info-circle') - Not all comments are displayed because you're - - if @start_version - comparing two versions - - else - viewing an old version - of the diff. - - .pull-right - = link_to 'Show latest version', diffs_project_merge_request_path(@project, @merge_request), class: 'btn btn-sm' diff --git a/app/views/projects/merge_requests/show.html.haml b/app/views/projects/merge_requests/show.html.haml index d88e3d794d3..abff702fd9d 100644 --- a/app/views/projects/merge_requests/show.html.haml +++ b/app/views/projects/merge_requests/show.html.haml @@ -8,7 +8,7 @@ = webpack_bundle_tag('common_vue') = webpack_bundle_tag('diff_notes') -.merge-request{ 'data-mr-action': "#{j params[:tab].presence || 'show'}", 'data-url' => merge_request_path(@merge_request, format: :json), 'data-project-path' => project_path(@merge_request.project) } +.merge-request{ data: { mr_action: j(params[:tab].presence || 'show'), url: merge_request_path(@merge_request, format: :json), project_path: project_path(@merge_request.project) } } = render "projects/merge_requests/mr_title" .merge-request-details.issuable-details{ data: { id: @merge_request.project.id } } @@ -38,21 +38,21 @@ .nav-links.scrolling-tabs %ul.merge-request-tabs %li.notes-tab - = link_to project_merge_request_path(@project, @merge_request), data: { target: 'div#notes', action: 'show', toggle: 'tab' } do + = tab_link_for @merge_request, :show, force_link: @commit.present? do Discussion %span.badge= @merge_request.related_notes.user.count - if @merge_request.source_project %li.commits-tab - = link_to commits_project_merge_request_path(@project, @merge_request), data: { target: 'div#commits', action: 'commits', toggle: 'tab' } do + = tab_link_for @merge_request, :commits do Commits %span.badge= @commits_count - if @pipelines.any? %li.pipelines-tab - = link_to pipelines_project_merge_request_path(@project, @merge_request), data: { target: '#pipelines', action: 'pipelines', toggle: 'tab' } do + = tab_link_for @merge_request, :pipelines do Pipelines %span.badge.js-pipelines-mr-count= @pipelines.size %li.diffs-tab - = link_to diffs_project_merge_request_path(@project, @merge_request), data: { target: 'div#diffs', action: 'diffs', toggle: 'tab' } do + = tab_link_for @merge_request, :diffs do Changes %span.badge= @merge_request.diff_size #resolve-count-app.line-resolve-all-container.prepend-top-10{ "v-cloak" => true } diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml index cad7c2e83db..2f56630c22e 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -18,7 +18,8 @@ A project is where you house your files (repository), plan your work (issues), and publish your documentation (wiki), #{link_to 'among other things', help_page_path("user/project/index.md", anchor: "projects-features"), target: '_blank'}. %p All features are enabled when you create a project, but you can disable the ones you don’t need in the project settings. - = brand_new_project_guidelines + .md + = brand_new_project_guidelines .col-lg-9.js-toggle-container %ul.nav-links.gitlab-tabs{ role: 'tablist' } %li.active{ role: 'presentation' } @@ -85,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/notes/_actions.html.haml b/app/views/projects/notes/_actions.html.haml index 4961835f12a..5ea653ccad5 100644 --- a/app/views/projects/notes/_actions.html.haml +++ b/app/views/projects/notes/_actions.html.haml @@ -3,7 +3,7 @@ %span.note-role.note-role-special.has-tooltip{ title: _("This is the author's first Merge Request to this project.") } = issuable_first_contribution_icon - if access.nonzero? - %span.note-role.note-role-access= Gitlab::Access.human_access(access) + %span.note-role.user-access-role= Gitlab::Access.human_access(access) - if note.resolvable? - can_resolve = can?(current_user, :resolve_note, note) diff --git a/app/views/projects/pipelines_settings/_show.html.haml b/app/views/projects/pipelines_settings/_show.html.haml index ee4fa663b9f..c63e716180c 100644 --- a/app/views/projects/pipelines_settings/_show.html.haml +++ b/app/views/projects/pipelines_settings/_show.html.haml @@ -6,46 +6,35 @@ %h5 Auto DevOps (Beta) %p Auto DevOps will automatically build, test, and deploy your application based on a predefined Continuous Integration and Delivery configuration. - This will happen starting with the next event (e.g.: push) that occurs to the project. = link_to 'Learn more about Auto DevOps', help_page_path('topics/autodevops/index.md') - message = auto_devops_warning_message(@project) - if message %p.settings-message.text-center = message.html_safe = f.fields_for :auto_devops_attributes, @auto_devops do |form| - .radio.js-auto-devops-enable-radio-wrapper + .radio = form.label :enabled_true do - = form.radio_button :enabled, 'true', class: 'js-auto-devops-enable-radio' + = form.radio_button :enabled, 'true' %strong Enable Auto DevOps %br %span.descr The Auto DevOps pipeline configuration will be used when there is no <code>.gitlab-ci.yml</code> in the project. - - if show_run_auto_devops_pipeline_checkbox_for_explicit_setting?(@project) - .checkbox.hide.js-run-auto-devops-pipeline-checkbox-wrapper - = label_tag 'project[run_auto_devops_pipeline_explicit]' do - = check_box_tag 'project[run_auto_devops_pipeline_explicit]', true, false, class: 'js-run-auto-devops-pipeline-checkbox' - = s_('ProjectSettings|Immediately run a pipeline on the default branch') - .radio.js-auto-devops-enable-radio-wrapper + .radio = form.label :enabled_false do - = form.radio_button :enabled, 'false', class: 'js-auto-devops-enable-radio' + = form.radio_button :enabled, 'false' %strong Disable Auto DevOps %br %span.descr An explicit <code>.gitlab-ci.yml</code> needs to be specified before you can begin using Continuous Integration and Delivery. - .radio.js-auto-devops-enable-radio-wrapper + .radio = form.label :enabled_ do - = form.radio_button :enabled, '', class: 'js-auto-devops-enable-radio' + = form.radio_button :enabled, '' %strong Instance default (#{current_application_settings.auto_devops_enabled? ? 'enabled' : 'disabled'}) %br %span.descr Follow the instance default to either have Auto DevOps enabled or disabled when there is no project specific <code>.gitlab-ci.yml</code>. - - if show_run_auto_devops_pipeline_checkbox_for_instance_setting?(@project) - .checkbox.hide.js-run-auto-devops-pipeline-checkbox-wrapper - = label_tag 'project[run_auto_devops_pipeline_implicit]' do - = check_box_tag 'project[run_auto_devops_pipeline_implicit]', true, false, class: 'js-run-auto-devops-pipeline-checkbox' - = s_('ProjectSettings|Immediately run a pipeline on the default branch') %p You need to specify a domain if you want to use Auto Review Apps and Auto Deploy stages. = form.text_field :domain, class: 'form-control', placeholder: 'domain.com' diff --git a/app/views/projects/tree/_old_tree_header.html.haml b/app/views/projects/tree/_old_tree_header.html.haml index 3a43dde8052..7f636b7e0e8 100644 --- a/app/views/projects/tree/_old_tree_header.html.haml +++ b/app/views/projects/tree/_old_tree_header.html.haml @@ -1,3 +1,8 @@ +- if on_top_of_branch? + - addtotree_toggle_attributes = { href: '#', 'data-toggle': 'dropdown', 'data-target': '.add-to-tree-dropdown' } +- else + - addtotree_toggle_attributes = { title: _("You can only add files when you are on a branch"), data: { container: 'body' }, class: 'disabled has-tooltip' } + %ul.breadcrumb.repo-breadcrumb %li = link_to project_tree_path(@project, @ref) do @@ -8,13 +13,10 @@ - if current_user %li - - if !on_top_of_branch? - %span.btn.add-to-tree.disabled.has-tooltip{ title: _("You can only add files when you are on a branch"), data: { container: 'body' } } - = icon('plus') - - else - %span.dropdown - %a.dropdown-toggle.btn.add-to-tree{ href: '#', "data-toggle" => "dropdown", "data-target" => ".add-to-tree-dropdown" } - = icon('plus') + %a.btn.add-to-tree{ addtotree_toggle_attributes } + = sprite_icon('plus', size: 16, css_class: 'pull-left') + = sprite_icon('arrow-down', size: 16, css_class: 'pull-left') + - if on_top_of_branch? .add-to-tree-dropdown %ul.dropdown-menu - if can_edit_tree? diff --git a/app/views/shared/_outdated_browser.html.haml b/app/views/shared/_outdated_browser.html.haml index c06d1ffa59b..8ddb1b2bc99 100644 --- a/app/views/shared/_outdated_browser.html.haml +++ b/app/views/shared/_outdated_browser.html.haml @@ -1,7 +1,8 @@ - if outdated_browser? - .browser-alert - 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') - for a better experience. + .flash-container + .flash-alert.text-center + GitLab may not work properly because you are using an outdated web browser. + %br + Please install a + = 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/groups/_group.html.haml b/app/views/shared/groups/_group.html.haml index 321d8767d08..90395600d4e 100644 --- a/app/views/shared/groups/_group.html.haml +++ b/app/views/shared/groups/_group.html.haml @@ -1,19 +1,7 @@ -- group_member = local_assigns[:group_member] -- full_name = true unless local_assigns[:full_name] == false -- group_name = full_name ? group.full_name : group.name -- css_class = '' unless local_assigns[:css_class] -- css_class += " no-description" if group.description.blank? - -%li.group-row{ class: css_class } - - if group_member - .controls.hidden-xs - - if can?(current_user, :admin_group, group) - = link_to edit_group_path(group), class: "btn" do - = sprite_icon('settings') - - = link_to leave_group_group_members_path(group), data: { confirm: leave_confirmation_message(group) }, method: :delete, class: "btn", title: s_("GroupsTree|Leave this group") do - = icon('sign-out') +- user = local_assigns.fetch(:user, current_user) +- access = user&.max_member_access_for_group(group.id) +%li.group-row{ class: ('no-description' if group.description.blank?) } .stats %span = icon('bookmark') @@ -30,11 +18,10 @@ = link_to group do = group_icon(group, class: "avatar s40 hidden-xs") .title - = link_to group_name, group, class: 'group-name' + = link_to group.full_name, group, class: 'group-name' - - if group_member - as - %span= group_member.human_access + - if access&.nonzero? + %span.user-access-role= Gitlab::Access.human_access(access) - if group.description.present? .description diff --git a/app/views/shared/groups/_list.html.haml b/app/views/shared/groups/_list.html.haml index aec8ecd1714..f50a6bd4d6a 100644 --- a/app/views/shared/groups/_list.html.haml +++ b/app/views/shared/groups/_list.html.haml @@ -1,6 +1,8 @@ - if groups.any? + - user = local_assigns[:user] + %ul.content-list - groups.each_with_index do |group, i| - = render "shared/groups/group", group: group + = render "shared/groups/group", group: group, user: user - else .nothing-here-block= s_("GroupsEmptyState|No groups found") diff --git a/app/views/shared/notes/_notes_with_form.html.haml b/app/views/shared/notes/_notes_with_form.html.haml index c6e18108c7a..e11f778adf5 100644 --- a/app/views/shared/notes/_notes_with_form.html.haml +++ b/app/views/shared/notes/_notes_with_form.html.haml @@ -27,7 +27,7 @@ - elsif discussion_locked .disabled-comment.text-center.prepend-top-default %span.issuable-note-warning - %span.icon= sprite_icon('lock', size: 14) + = sprite_icon('lock', size: 16, css_class: 'icon') %span This = issuable.class.to_s.titleize.downcase diff --git a/app/views/shared/projects/_list.html.haml b/app/views/shared/projects/_list.html.haml index 0bedfea3502..e1da05d8f08 100644 --- a/app/views/shared/projects/_list.html.haml +++ b/app/views/shared/projects/_list.html.haml @@ -5,18 +5,20 @@ - forks = false unless local_assigns[:forks] == true - ci = false unless local_assigns[:ci] == true - skip_namespace = false unless local_assigns[:skip_namespace] == true +- user = local_assigns[:user] - show_last_commit_as_description = false unless local_assigns[:show_last_commit_as_description] == true - remote = false unless local_assigns[:remote] == true -- load_pipeline_status(projects) .js-projects-list-holder - if any_projects?(projects) + - load_pipeline_status(projects) + %ul.projects-list - projects.each_with_index do |project, i| - css_class = (i >= projects_limit) || project.pending_delete? ? 'hide' : nil = render "shared/projects/project", project: project, skip_namespace: skip_namespace, avatar: avatar, stars: stars, css_class: css_class, ci: ci, use_creator_avatar: use_creator_avatar, - forks: forks, show_last_commit_as_description: show_last_commit_as_description + forks: forks, show_last_commit_as_description: show_last_commit_as_description, user: user - if @private_forks_count && @private_forks_count > 0 %li.project-row.private-forks-notice diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml index 98bfc7c4d36..003f5fa52eb 100644 --- a/app/views/shared/projects/_project.html.haml +++ b/app/views/shared/projects/_project.html.haml @@ -3,6 +3,8 @@ - forks = false unless local_assigns[:forks] == true - ci = false unless local_assigns[:ci] == true - skip_namespace = false unless local_assigns[:skip_namespace] == true +- user = local_assigns[:user] +- access = user&.max_member_access_for_project(project.id) unless user.nil? - css_class = '' unless local_assigns[:css_class] - show_last_commit_as_description = false unless local_assigns[:show_last_commit_as_description] == true && project.commit - css_class += " no-description" if project.description.blank? && !show_last_commit_as_description @@ -21,14 +23,19 @@ .project-details %h3.prepend-top-0.append-bottom-0 = link_to project_path(project), class: 'text-plain' do - %span.project-full-name + %span.project-full-name>< %span.namespace-name - if project.namespace && !skip_namespace = project.namespace.human_name \/ - %span.project-name + %span.project-name< = project.name + - if access&.nonzero? + -# haml-lint:disable UnnecessaryStringOutput + = ' ' # prevent haml from eating the space between elements + %span.user-access-role= Gitlab::Access.human_access(access) + - if show_last_commit_as_description .description.prepend-top-5 = link_to_markdown(project.commit.title, project_commit_path(project, project.commit), class: "commit-row-message") 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/15774-fix-39233-500-in-merge-request.yml b/changelogs/unreleased/15774-fix-39233-500-in-merge-request.yml new file mode 100644 index 00000000000..1179b3f20e6 --- /dev/null +++ b/changelogs/unreleased/15774-fix-39233-500-in-merge-request.yml @@ -0,0 +1,5 @@ +--- +title: 'fix #39233 - 500 in merge request' +merge_request: 15774 +author: Martin Nowak +type: fixed 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/22680-unlabel-slash-command-limit-autocomplete-to-applied-labels.yml b/changelogs/unreleased/22680-unlabel-slash-command-limit-autocomplete-to-applied-labels.yml new file mode 100644 index 00000000000..6d7f8655282 --- /dev/null +++ b/changelogs/unreleased/22680-unlabel-slash-command-limit-autocomplete-to-applied-labels.yml @@ -0,0 +1,5 @@ +--- +title: Limit autocomplete menu to applied labels +merge_request: 11110 +author: Vitaliy @blackst0ne Klachkov +type: added 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/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/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/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/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 new file mode 100644 index 00000000000..4f0eaf8472f --- /dev/null +++ b/changelogs/unreleased/40555-replace-absolute-urls-with-related-branches-to-avoid-hostname.yml @@ -0,0 +1,6 @@ +--- +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/40573-rename-gke-as-kubernetes-engine.yml b/changelogs/unreleased/40573-rename-gke-as-kubernetes-engine.yml new file mode 100644 index 00000000000..afbb869bdbb --- /dev/null +++ b/changelogs/unreleased/40573-rename-gke-as-kubernetes-engine.yml @@ -0,0 +1,5 @@ +--- +title: Rename GKE as Kubernetes Engine +merge_request: 15608 +author: Takuya Noguchi +type: other diff --git a/changelogs/unreleased/40715-updateendpoint-undefined-on-issue-page.yml b/changelogs/unreleased/40715-updateendpoint-undefined-on-issue-page.yml new file mode 100644 index 00000000000..0328a693354 --- /dev/null +++ b/changelogs/unreleased/40715-updateendpoint-undefined-on-issue-page.yml @@ -0,0 +1,5 @@ +--- +title: Fix updateEndpoint undefined error for issue_show app root +merge_request: 15698 +author: +type: fixed diff --git a/changelogs/unreleased/admin-welcome-new-group-link.yml b/changelogs/unreleased/admin-welcome-new-group-link.yml new file mode 100644 index 00000000000..9c939a0a3ad --- /dev/null +++ b/changelogs/unreleased/admin-welcome-new-group-link.yml @@ -0,0 +1,5 @@ +--- +title: Fixed admin welcome screen new group path +merge_request: +author: +type: fixed 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-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/commit-title-wrapping.yml b/changelogs/unreleased/commit-title-wrapping.yml new file mode 100644 index 00000000000..65c28b82b8b --- /dev/null +++ b/changelogs/unreleased/commit-title-wrapping.yml @@ -0,0 +1,5 @@ +--- +title: Fixed long commit links not wrapping correctly +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/deploy-keys-loading-icon.yml b/changelogs/unreleased/deploy-keys-loading-icon.yml new file mode 100644 index 00000000000..e3cb5bc6924 --- /dev/null +++ b/changelogs/unreleased/deploy-keys-loading-icon.yml @@ -0,0 +1,5 @@ +--- +title: Fixed deploy keys remove button loading state not resetting +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/dm-commit-diff-discussions-in-mr-context.yml b/changelogs/unreleased/dm-commit-diff-discussions-in-mr-context.yml new file mode 100644 index 00000000000..1f8b42ea21f --- /dev/null +++ b/changelogs/unreleased/dm-commit-diff-discussions-in-mr-context.yml @@ -0,0 +1,5 @@ +--- +title: Make diff notes created on a commit in a merge request to persist a rebase. +merge_request: 12148 +author: +type: added 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-sm-34834-missing-dependency-should-fail-job-2.yml b/changelogs/unreleased/feature-sm-34834-missing-dependency-should-fail-job-2.yml new file mode 100644 index 00000000000..ab85b8ee515 --- /dev/null +++ b/changelogs/unreleased/feature-sm-34834-missing-dependency-should-fail-job-2.yml @@ -0,0 +1,5 @@ +--- +title: Fail jobs if its dependency is missing +merge_request: 14009 +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-new-project-guidelines-styling.yml b/changelogs/unreleased/fix-new-project-guidelines-styling.yml new file mode 100644 index 00000000000..a97f5c485d4 --- /dev/null +++ b/changelogs/unreleased/fix-new-project-guidelines-styling.yml @@ -0,0 +1,5 @@ +--- +title: Use Markdown styling for new project guidelines +merge_request: 15785 +author: Markus Koller +type: fixed diff --git a/changelogs/unreleased/fj-40752-forks-api-not-using-services.yml b/changelogs/unreleased/fj-40752-forks-api-not-using-services.yml new file mode 100644 index 00000000000..cd7b87596e6 --- /dev/null +++ b/changelogs/unreleased/fj-40752-forks-api-not-using-services.yml @@ -0,0 +1,5 @@ +--- +title: Using appropiate services in the API for managing forks +merge_request: 15709 +author: +type: fixed diff --git a/changelogs/unreleased/merge-request-lock-icon-size-fix.yml b/changelogs/unreleased/merge-request-lock-icon-size-fix.yml new file mode 100644 index 00000000000..09c059a3011 --- /dev/null +++ b/changelogs/unreleased/merge-request-lock-icon-size-fix.yml @@ -0,0 +1,5 @@ +--- +title: Fixed merge request lock icon size +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/mk-add-old-attachments-to-uploads-table.yml b/changelogs/unreleased/mk-add-old-attachments-to-uploads-table.yml new file mode 100644 index 00000000000..499543ef883 --- /dev/null +++ b/changelogs/unreleased/mk-add-old-attachments-to-uploads-table.yml @@ -0,0 +1,5 @@ +--- +title: Add untracked files to uploads table +merge_request: 15270 +author: +type: other diff --git a/changelogs/unreleased/outdated-browser-position-fix.yml b/changelogs/unreleased/outdated-browser-position-fix.yml new file mode 100644 index 00000000000..801e45a28b3 --- /dev/null +++ b/changelogs/unreleased/outdated-browser-position-fix.yml @@ -0,0 +1,5 @@ +--- +title: Fixed outdated browser flash positioning +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/sh-fix-import-rake-task.yml b/changelogs/unreleased/sh-fix-import-rake-task.yml new file mode 100644 index 00000000000..9cd6d7e4a72 --- /dev/null +++ b/changelogs/unreleased/sh-fix-import-rake-task.yml @@ -0,0 +1,5 @@ +--- +title: Fix gitlab:import:repos Rake task moving repositories into the wrong location +merge_request: +author: +type: fixed 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/throttle-touching-of-objects.yml b/changelogs/unreleased/throttle-touching-of-objects.yml new file mode 100644 index 00000000000..0a57bea7c83 --- /dev/null +++ b/changelogs/unreleased/throttle-touching-of-objects.yml @@ -0,0 +1,5 @@ +--- +title: Throttle the number of UPDATEs triggered by touch +merge_request: +author: +type: performance diff --git a/changelogs/unreleased/update_mr_changes_empty_page.yml b/changelogs/unreleased/update_mr_changes_empty_page.yml new file mode 100644 index 00000000000..bae73c21e8f --- /dev/null +++ b/changelogs/unreleased/update_mr_changes_empty_page.yml @@ -0,0 +1,5 @@ +--- +title: Update empty state page of merge request 'changes' tab +merge_request: 15611 +author: Vitaliy @blackst0ne Klachkov +type: added diff --git a/config/initializers/7_prometheus_metrics.rb b/config/initializers/7_prometheus_metrics.rb index 43b1e943897..eb7959e4da6 100644 --- a/config/initializers/7_prometheus_metrics.rb +++ b/config/initializers/7_prometheus_metrics.rb @@ -11,14 +11,7 @@ Prometheus::Client.configure do |config| config.multiprocess_files_dir ||= Rails.root.join('tmp/prometheus_multiproc_dir') end - config.pid_provider = -> do - worker_id = Prometheus::Client::Support::Unicorn.worker_id - if worker_id.nil? - "process_pid_#{Process.pid}" - else - "worker_id_#{worker_id}" - end - end + config.pid_provider = Prometheus::Client::Support::Unicorn.method(:worker_pid_provider) end Gitlab::Application.configure do |config| 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/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/routes/group.rb b/config/routes/group.rb index db99e10bb9a..976837a246d 100644 --- a/config/routes/group.rb +++ b/config/routes/group.rb @@ -49,6 +49,12 @@ constraints(GroupUrlConstrainer.new) do post :resend_invite, on: :member delete :leave, on: :collection end + + resources :uploads, only: [:create] do + collection do + get ":secret/:filename", action: :show, as: :show, constraints: { filename: /[^\/]+/ } + end + end end scope(path: '*id', 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/20171103000000_set_uploads_path_size_for_mysql.rb b/db/migrate/20171103000000_set_uploads_path_size_for_mysql.rb new file mode 100644 index 00000000000..1fbe505f804 --- /dev/null +++ b/db/migrate/20171103000000_set_uploads_path_size_for_mysql.rb @@ -0,0 +1,25 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class SetUploadsPathSizeForMysql < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + + def up + # We need at least 297 at the moment. For more detail on that number, see: + # https://gitlab.com/gitlab-org/gitlab-ce/issues/40168#what-is-the-expected-correct-behavior + # + # Rails + PostgreSQL `string` is equivalent to a `text` field, but + # Rails + MySQL `string` is `varchar(255)` by default. Also, note that we + # have an upper limit because with a unique index, MySQL has a max key + # length of 3072 bytes which seems to correspond to `varchar(1024)`. + change_column :uploads, :path, :string, limit: 511 + end + + def down + # It was unspecified, which is varchar(255) by default in Rails for MySQL. + change_column :uploads, :path, :string + end +end diff --git a/db/migrate/20171106154015_remove_issues_branch_name.rb b/db/migrate/20171106154015_remove_issues_branch_name.rb index 3d08225c96d..162b6bafab4 100644 --- a/db/migrate/20171106154015_remove_issues_branch_name.rb +++ b/db/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/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/post_migrate/20171103140253_track_untracked_uploads.rb b/db/post_migrate/20171103140253_track_untracked_uploads.rb new file mode 100644 index 00000000000..548a94d2d38 --- /dev/null +++ b/db/post_migrate/20171103140253_track_untracked_uploads.rb @@ -0,0 +1,21 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class TrackUntrackedUploads < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + disable_ddl_transaction! + + DOWNTIME = false + MIGRATION = 'PrepareUntrackedUploads' + + def up + BackgroundMigrationWorker.perform_async(MIGRATION) + end + + def down + if table_exists?(:untracked_files_for_uploads) + drop_table :untracked_files_for_uploads + end + end +end 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/20171205190711_reschedule_fork_network_creation_caller.rb b/db/post_migrate/20171205190711_reschedule_fork_network_creation_caller.rb new file mode 100644 index 00000000000..30ff5173192 --- /dev/null +++ b/db/post_migrate/20171205190711_reschedule_fork_network_creation_caller.rb @@ -0,0 +1,27 @@ +class RescheduleForkNetworkCreationCaller < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + MIGRATION = 'PopulateForkNetworksRange'.freeze + BATCH_SIZE = 100 + DELAY_INTERVAL = 15.seconds + + disable_ddl_transaction! + + class ForkedProjectLink < ActiveRecord::Base + include EachBatch + + self.table_name = 'forked_project_links' + end + + def up + say 'Populating the `fork_networks` based on existing `forked_project_links`' + + queue_background_migration_jobs_by_range_at_intervals(ForkedProjectLink, MIGRATION, DELAY_INTERVAL, batch_size: BATCH_SIZE) + end + + def down + # nothing + end +end diff --git a/db/schema.rb b/db/schema.rb index 0984ca6487f..f0b1da16d53 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: 20171124150326) do +ActiveRecord::Schema.define(version: 20171206221519) 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: 20171124150326) 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: 20171124150326) 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: 20171124150326) 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: 20171124150326) 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: 20171124150326) 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| @@ -1737,7 +1736,7 @@ ActiveRecord::Schema.define(version: 20171124150326) do create_table "uploads", force: :cascade do |t| t.integer "size", limit: 8, null: false - t.string "path", null: false + t.string "path", limit: 511, null: false t.string "checksum", limit: 64 t.integer "model_id" t.string "model_type" diff --git a/doc/README.md b/doc/README.md index 95cb9683a15..11d52001440 100644 --- a/doc/README.md +++ b/doc/README.md @@ -13,13 +13,14 @@ GitLab offers the most scalable Git-based fully integrated platform for software - **GitLab Community Edition (CE)** is an [opensource product](https://gitlab.com/gitlab-org/gitlab-ce/), self-hosted, free to use. Every feature available in GitLab CE is also available on GitLab Enterprise Edition (Starter and Premium) and GitLab.com. - **GitLab Enterprise Edition (EE)** is an [opencore product](https://gitlab.com/gitlab-org/gitlab-ee/), -self-hosted, fully featured solution of GitLab, available under distinct [subscriptions](https://about.gitlab.com/products/): **GitLab Enterprise Edition Starter (EES)** and **GitLab Enterprise Edition Premium (EEP)**. +self-hosted, fully featured solution of GitLab, available under distinct [subscriptions](https://about.gitlab.com/products/): **GitLab Enterprise Edition Starter (EES)**, **GitLab Enterprise Edition Premium (EEP)**, and **GitLab Enterprise Edition Ultimate (EEU)**. - **GitLab.com**: SaaS GitLab solution, with [free and paid subscriptions](https://about.gitlab.com/gitlab-com/). GitLab.com is hosted by GitLab, Inc., and administrated by GitLab (users don't have access to admin settings). > **GitLab EE** contains all features available in **GitLab CE**, plus premium features available in each version: **Enterprise Edition Starter** -(**EES**) and **Enterprise Edition Premium** (**EEP**). Everything available in -**EES** is also available in **EEP**. +(**EES**), **Enterprise Edition Premium** (**EEP**), and **Enterprise Edition Ultimate** +(**EEU**). Everything available in **EES** is also available in **EEP**. Every feature +available in **EEP** is also available in **EEU**. ---- @@ -32,8 +33,8 @@ Shortcuts to GitLab's most visited docs: | [Using Docker images](ci/docker/using_docker_images.md) | [GitLab Pages](user/project/pages/index.md) | - [User documentation](user/index.md) -- [Administrator documentation](#administrator-documentation) -- [Technical Articles](articles/index.md) +- [Administrator documentation](administration/index.md) +- [Contributor documentation](#contributor-documentation) ## Getting started with GitLab @@ -133,83 +134,24 @@ Manage your [repositories](user/project/repository/index.md) from the UI (user i ## Administrator documentation -Learn how to administer your GitLab instance. Regular users don't -have access to GitLab administration tools and settings. +[Administration documentation](administration/index.md) applies to admin users of GitLab +self-hosted instances: -### Install, update, upgrade, migrate +- GitLab Community Edition +- GitLab [Enterprise Editions](https://about.gitlab.com/gitlab-ee/) + - Enterprise Edition Starter (EES) + - Enterprise Edition Premium (EEP) + - Enterprise Edition Ultimate (EEU) -- [Install](install/README.md): Requirements, directory structures and installation from source. -- [Mattermost](https://docs.gitlab.com/omnibus/gitlab-mattermost/): Integrate [Mattermost](https://about.mattermost.com/) with your GitLab installation. -- [Migrate GitLab CI to CE/EE](migrate_ci_to_ce/README.md): If you have an old GitLab installation (older than 8.0), follow this guide to migrate your existing GitLab CI data to GitLab CE/EE. -- [Restart GitLab](administration/restart_gitlab.md): Learn how to restart GitLab and its components. -- [Update](update/README.md): Update guides to upgrade your installation. - -### User permissions - -- [Access restrictions](user/admin_area/settings/visibility_and_access_controls.md#enabled-git-access-protocols): Define which Git access protocols can be used to talk to GitLab -- [Authentication/Authorization](topics/authentication/index.md#gitlab-administrators): Enforce 2FA, configure external authentication with LDAP, SAML, CAS and additional Omniauth providers. - -### Features - -- [Container Registry](administration/container_registry.md): Configure Docker Registry with GitLab. -- [Custom Git hooks](administration/custom_hooks.md): Custom Git hooks (on the filesystem) for when webhooks aren't enough. -- [Git LFS configuration](workflow/lfs/lfs_administration.md): Learn how to use LFS under GitLab. -- [GitLab Pages configuration](administration/pages/index.md): Configure GitLab Pages. -- [High Availability](administration/high_availability/README.md): Configure multiple servers for scaling or high availability. -- [User cohorts](user/admin_area/user_cohorts.md): View user activity over time. -- [Web terminals](administration/integration/terminal.md): Provide terminal access to environments from within GitLab. -- GitLab CI - - [CI admin settings](user/admin_area/settings/continuous_integration.md): Define max artifacts size and expiration time. - -### Integrations - -- [Integrations](integration/README.md): How to integrate with systems such as JIRA, Redmine, Twitter. -- [Mattermost](user/project/integrations/mattermost.md): Set up GitLab with Mattermost. - -### Monitoring - -- [GitLab performance monitoring with InfluxDB](administration/monitoring/performance/introduction.md): Configure GitLab and InfluxDB for measuring performance metrics. -- [GitLab performance monitoring with Prometheus](administration/monitoring/prometheus/index.md): Configure GitLab and Prometheus for measuring performance metrics. -- [Monitoring uptime](user/admin_area/monitoring/health_check.md): Check the server status using the health check endpoint. -- [Monitoring GitHub imports](administration/monitoring/github_imports.md) - -### Performance - -- [Housekeeping](administration/housekeeping.md): Keep your Git repository tidy and fast. -- [Operations](administration/operations.md): Keeping GitLab up and running. -- [Polling](administration/polling.md): Configure how often the GitLab UI polls for updates. -- [Request Profiling](administration/monitoring/performance/request_profiling.md): Get a detailed profile on slow requests. -- [Performance Bar](administration/monitoring/performance/performance_bar.md): Get performance information for the current page. - -### Customization - -- [Adjust your instance's timezone](workflow/timezone.md): Customize the default time zone of GitLab. -- [Environment variables](administration/environment_variables.md): Supported environment variables that can be used to override their defaults values in order to configure GitLab. -- [Header logo](customization/branded_page_and_email_header.md): Change the logo on the overall page and email header. -- [Issue closing pattern](administration/issue_closing_pattern.md): Customize how to close an issue from commit messages. -- [Libravatar](customization/libravatar.md): Use Libravatar instead of Gravatar for user avatars. -- [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 new project page. - -### Admin tools - -- [Gitaly](administration/gitaly/index.md): Configuring Gitaly, GitLab's Git repository storage service -- [Raketasks](raketasks/README.md): Backups, maintenance, automatic webhook setup and the importing of projects. - - [Backup and restore](raketasks/backup_restore.md): Backup and restore your GitLab instance. -- [Reply by email](administration/reply_by_email.md): Allow users to comment on issues and merge requests by replying to notification emails. -- [Repository checks](administration/repository_checks.md): Periodic Git repository checks. -- [Repository storage paths](administration/repository_storage_paths.md): Manage the paths used to store repositories. -- [Security](security/README.md): Learn what you can do to further secure your GitLab instance. -- [System hooks](system_hooks/system_hooks.md): Notifications when users, projects and keys are changed. - -### Troubleshooting - -- [Debugging tips](administration/troubleshooting/debug.md): Tips to debug problems when things go wrong -- [Log system](administration/logs.md): Where to look for logs. -- [Sidekiq Troubleshooting](administration/troubleshooting/sidekiq.md): Debug when Sidekiq appears hung and is not processing jobs. +Learn how to install, configure, update, upgrade, integrate, and maintain your own instance. +Regular users don't have access to GitLab administration tools and settings. ## Contributor documentation +GitLab Community Edition is [opensource](https://gitlab.com/gitlab-org/gitlab-ce/) +and Enterprise Editions are [opencore](https://gitlab.com/gitlab-org/gitlab-ee/). +Learn how to contribute to GitLab: + - [Development](development/README.md): All styleguides and explanations how to contribute. - [Legal](legal/README.md): Contributor license agreements. - [Writing documentation](development/writing_documentation.md): Contributing to GitLab Docs. diff --git a/doc/administration/index.md b/doc/administration/index.md new file mode 100644 index 00000000000..e6986a2b07f --- /dev/null +++ b/doc/administration/index.md @@ -0,0 +1,121 @@ +# Administrator documentation + +Learn how to administer your GitLab instance (Community Edition and +[Enterprise Editions](https://about.gitlab.com/gitlab-ee/)). +Regular users don't have access to GitLab administration tools and settings. + +GitLab.com is administered by GitLab, Inc., therefore, only GitLab team members have +access to its admin configurations. If you're a GitLab.com user, please check the +[user documentation](../user/index.html). + +## Installing and maintaining GitLab + +Learn how to install, configure, update, and maintain your GitLab instance. + +### Installing GitLab + +- [Install](../install/README.md): Requirements, directory structures, and installation methods. +- [High Availability](high_availability/README.md): Configure multiple servers for scaling or high availability. + +### 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 +[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. + +### 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/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 + +- [GitLab versions and maintenance policy](../policy/maintenance.md): Understand GitLab versions and releases (Major, Minor, Patch, Security), as well as update recommendations. +- [Update GitLab](../update/README.md): Update guides to upgrade your installation to a new version. +- [Downtimeless updates](../update/README.md#upgrading-without-downtime): Upgrade to a newer major, minor, or patch version of GitLab without taking your GitLab instance offline. +- [Migrate your GitLab CI/CD data to another version of GitLab](../migrate_ci_to_ce/README.md): If you have an old GitLab installation (older than 8.0), follow this guide to migrate your existing GitLab CI/CD data to another version of GitLab. + +### Upgrading or downgrading GitLab + +- [Upgrade from GitLab CE to GitLab EE](../update/README.md#upgrading-between-editions): learn how to upgrade GitLab Community Edition to GitLab Enterprise Editions. +- [Downgrade from GitLab EE to GitLab CE](../downgrade_ee_to_ce/README.md): Learn how to downgrade GitLab Enterprise Editions to Community Edition. + +### GitLab platform integrations + +- [Mattermost](https://docs.gitlab.com/omnibus/gitlab-mattermost/): Integrate with [Mattermost](https://about.mattermost.com/), an open source, private cloud workplace for web messaging. +- [PlantUML](integration/plantuml.md): Create simple diagrams in AsciiDoc and Markdown documents +created in snippets, wikis, and repos. +- [Web terminals](integration/terminal.md): Provide terminal access to your applications deployed to Kubernetes from within GitLab's CI/CD [environments](../ci/environments.md#web-terminals). + +## User settings and permissions + +- [Libravatar](../customization/libravatar.md): Use Libravatar instead of Gravatar for user avatars. +- [Sign-up restrictions](../user/admin_area/settings/sign_up_restrictions.md): block email addresses of specific domains, or whitelist only specific domains. +- [Access restrictions](../user/admin_area/settings/visibility_and_access_controls.md#enabled-git-access-protocols): Define which Git access protocols can be used to talk to GitLab (SSH, HTTP, HTTPS). +- [Authentication/Authorization](../topics/authentication/index.md#gitlab-administrators): Enforce 2FA, configure external authentication with LDAP, SAML, CAS and additional Omniauth providers. +- [Reply by email](reply_by_email.md): Allow users to comment on issues and merge requests by replying to notification emails. + - [Postfix for Reply by email](reply_by_email_postfix_setup.md): Set up a basic Postfix mail +server with IMAP authentication on Ubuntu, to be used with Reply by email. +- [User Cohorts](../user/admin_area/user_cohorts.md): Display the monthly cohorts of new users and their activities over time. + +## Project settings + +- [Container Registry](container_registry.md): Configure Container Registry with GitLab. +- [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. + +### Repository settings + +- [Repository checks](repository_checks.md): Periodic Git repository checks. +- [Repository storage paths](repository_storage_paths.md): Manage the paths used to store repositories. +- [Repository storage rake tasks](raketasks/storage.md): A collection of rake tasks to list and migrate existing projects and attachments associated with it from Legacy storage to Hashed storage. + +## Continuous Integration settings + +- [Enable/disable GitLab CI/CD](../ci/enable_or_disable_ci.md#site-wide-admin-setting): Enable or disable GitLab CI/CD for your instance. +- [GitLab CI/CD admin settings](../user/admin_area/settings/continuous_integration.md): Define max artifacts size and expiration time. +- [Job artifacts](job_artifacts.md): Enable, disable, and configure job artifacts (a set of files and directories which are outputted by a job when it completes successfully). +- [Artifacts size and expiration](../user/admin_area/settings/continuous_integration.md#maximum-artifacts-size): Define maximum artifacts limits and expiration date. +- [Register Shared and specific Runners](../ci/runners/README.md#registering-a-shared-runner): Learn how to register and configure Shared and specific Runners to your own instance. +- [Shared Runners pipelines quota](../user/admin_area/settings/continuous_integration.md#shared-runners-pipeline-minutes-quota): Limit the usage of pipeline minutes for Shared Runners. +- [Enable/disable Auto DevOps](../topics/autodevops/index.md#enabling-auto-devops): Enable or disable Auto DevOps for your instance. + +## Git configuration options + +- [Custom Git hooks](custom_hooks.md): Custom Git hooks (on the filesystem) for when webhooks aren't enough. +- [Git LFS configuration](../workflow/lfs/lfs_administration.md): Learn how to configure LFS for GitLab. +- [Housekeeping](housekeeping.md): Keep your Git repositories tidy and fast. + +## 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. +- [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. + +## Troubleshooting + +- [Debugging tips](troubleshooting/debug.md): Tips to debug problems when things go wrong +- [Log system](logs.md): Where to look for logs. +- [Sidekiq Troubleshooting](troubleshooting/sidekiq.md): Debug when Sidekiq appears hung and is not processing jobs. diff --git a/doc/administration/job_artifacts.md b/doc/administration/job_artifacts.md index 86b436d89dd..33f8a69c249 100644 --- a/doc/administration/job_artifacts.md +++ b/doc/administration/job_artifacts.md @@ -128,6 +128,45 @@ steps below. 1. Save the file and [restart GitLab][] for the changes to take effect. +## Validation for dependencies + +> Introduced in GitLab 10.3. + +To disable [the dependencies validation](../ci/yaml/README.md#when-a-dependent-job-will-fail), +you can flip the feature flag from a Rails console. + +--- + +**In Omnibus installations:** + +1. Enter the Rails console: + + ```sh + sudo gitlab-rails console + ``` + +1. Flip the switch and disable it: + + ```ruby + Feature.enable('ci_disable_validates_dependencies') + ``` +--- + +**In installations from source:** + +1. Enter the Rails console: + + ```sh + cd /home/git/gitlab + RAILS_ENV=production sudo -u git -H bundle exec rails console + ``` + +1. Flip the switch and disable it: + + ```ruby + Feature.enable('ci_disable_validates_dependencies') + ``` + ## Set the maximum file size of the artifacts Provided the artifacts are enabled, you can change the maximum file size of the 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/pages/index.md b/doc/administration/pages/index.md index 0c63b0b59a7..7d47aaac299 100644 --- a/doc/administration/pages/index.md +++ b/doc/administration/pages/index.md @@ -58,6 +58,9 @@ Before proceeding with the Pages configuration, you will need to: so that your users don't have to bring their own. 1. (Only for custom domains) Have a **secondary IP**. +NOTE: **Note:** +If your GitLab instance and the Pages daemon are deployed in a private network or behind a firewall, your GitLab Pages websites will only be accessible to devices/users that have access to the private network. + ### DNS configuration GitLab Pages expect to run on their own virtual host. In your DNS server/provider diff --git a/doc/api/protected_branches.md b/doc/api/protected_branches.md index 81fe854060a..950ead52560 100644 --- a/doc/api/protected_branches.md +++ b/doc/api/protected_branches.md @@ -136,7 +136,7 @@ DELETE /projects/:id/protected_branches/:name ``` ```bash -curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" 'https://gitlab.example.com/api/v4/projects/5/protected_branches/*-stable' +curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" 'https://gitlab.example.com/api/v4/projects/5/protected_branches/*-stable' ``` | Attribute | Type | Required | Description | 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/ci/autodeploy/quick_start_guide.md b/doc/ci/autodeploy/quick_start_guide.md index f76c2a2cf31..cc6c9ec0e0a 100644 --- a/doc/ci/autodeploy/quick_start_guide.md +++ b/doc/ci/autodeploy/quick_start_guide.md @@ -11,11 +11,11 @@ We made a minimal [Ruby application](https://gitlab.com/gitlab-examples/minimal- Let’s start by forking our sample application. Go to [the project page](https://gitlab.com/gitlab-examples/minimal-ruby-app) and press the `Fork` button. Soon you should have a project under your namespace with the necessary files. -## Setup your own cluster on Google Container Engine +## Setup your own cluster on Google Kubernetes Engine If you do not already have a Google Cloud account, create one at https://console.cloud.google.com. -Visit the [`Container Engine`](https://console.cloud.google.com/kubernetes/list) tab and create a new cluster. You can change the name and leave the rest of the default settings. Once you have your cluster running, you need to connect to the cluster by following the Google interface. +Visit the [`Kubernetes Engine`](https://console.cloud.google.com/kubernetes/list) tab and create a new cluster. You can change the name and leave the rest of the default settings. Once you have your cluster running, you need to connect to the cluster by following the Google interface. ## Connect to Kubernetes cluster diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index f40d2c5e347..32464cbb259 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -1153,6 +1153,20 @@ deploy: script: make deploy ``` +#### When a dependent job will fail + +> Introduced in GitLab 10.3. + +If the artifacts of the job that is set as a dependency have been +[expired](#artifacts-expire_in) or +[erased](../../user/project/pipelines/job_artifacts.md#erasing-artifacts), then +the dependent job will fail. + +NOTE: **Note:** +You can ask your administrator to +[flip this switch](../../administration/job_artifacts.md#validation-for-dependencies) +and bring back the old behavior. + ### before_script and after_script It's possible to overwrite the globally defined `before_script` and `after_script`: diff --git a/doc/development/README.md b/doc/development/README.md index 6892838be7f..7e4c767692a 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 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/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/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/writing_documentation.md b/doc/development/writing_documentation.md index b6def7ef541..48e04a40050 100644 --- a/doc/development/writing_documentation.md +++ b/doc/development/writing_documentation.md @@ -142,7 +142,7 @@ 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 +additional job called `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 diff --git a/doc/install/kubernetes/gitlab_chart.md b/doc/install/kubernetes/gitlab_chart.md index fa564d83785..96968c1e3ab 100644 --- a/doc/install/kubernetes/gitlab_chart.md +++ b/doc/install/kubernetes/gitlab_chart.md @@ -1,7 +1,7 @@ # GitLab Helm Chart > **Note**: * This chart is deprecated, and is being replaced by the [cloud native GitLab chart](https://gitlab.com/charts/helm.gitlab.io/blob/master/README.md). For more information on available charts, please see our [overview](index.md#chart-overview). -* These charts have been tested on Google Container Engine and Azure Container Service. Other Kubernetes installations may work as well, if not please [open an issue](https://gitlab.com/charts/charts.gitlab.io/issues). +* These charts have been tested on Google Kubernetes Engine and Azure Container Service. Other Kubernetes installations may work as well, if not please [open an issue](https://gitlab.com/charts/charts.gitlab.io/issues). For more information on available GitLab Helm Charts, please see our [overview](index.md#chart-overview). @@ -243,7 +243,7 @@ controller. For `nginx-ingress` you can check the on how to add the annotation to the `controller.service.annotations` array. >**Note:** -When using the `nginx-ingress` controller on Google Container Engine (GKE), and using the `external-traffic` annotation, +When using the `nginx-ingress` controller on Google Kubernetes Engine (GKE), and using the `external-traffic` annotation, you will need to additionally set the `controller.kind` to be DaemonSet. Otherwise only pods running on the same node as the nginx controller will be able to reach GitLab. This may result in pods within your cluster not being able to reach GitLab. See the [Kubernetes documentation](https://kubernetes.io/docs/tutorials/services/source-ip/#source-ip-for-services-with-typeloadbalancer) and diff --git a/doc/install/kubernetes/gitlab_omnibus.md b/doc/install/kubernetes/gitlab_omnibus.md index 6659c3cf7b2..5a5f8d67ff5 100644 --- a/doc/install/kubernetes/gitlab_omnibus.md +++ b/doc/install/kubernetes/gitlab_omnibus.md @@ -1,7 +1,7 @@ # GitLab-Omnibus Helm Chart > **Note:** * This Helm chart is in beta, and will be deprecated by the [cloud native GitLab chart](https://gitlab.com/charts/helm.gitlab.io/blob/master/README.md). -* These charts have been tested on Google Container Engine and Azure Container Service. Other Kubernetes installations may work as well, if not please [open an issue](https://gitlab.com/charts/charts.gitlab.io/issues). +* These charts have been tested on Google Kubernetes Engine and Azure Container Service. Other Kubernetes installations may work as well, if not please [open an issue](https://gitlab.com/charts/charts.gitlab.io/issues). This work is based partially on: https://github.com/lwolf/kubernetes-gitlab/. GitLab would like to thank Sergey Nuzhdin for his work. @@ -72,7 +72,7 @@ Other common configuration options: - `baseIP`: the desired [external IP address](#external-ip-recommended) - `gitlab`: Choose the [desired edition](https://about.gitlab.com/products), either `ee` or `ce`. `ce` is the default. - `gitlabEELicense`: For Enterprise Edition, the [license](https://docs.gitlab.com/ee/user/admin_area/license.html) can be installed directly via the Chart -- `provider`: Optimizes the deployment for a cloud provider. The default is `gke` for [Google Container Engine](https://cloud.google.com/container-engine/), with `acs` also supported for the [Azure Container Service](https://azure.microsoft.com/en-us/services/container-service/). +- `provider`: Optimizes the deployment for a cloud provider. The default is `gke` for [Google Kubernetes Engine](https://cloud.google.com/kubernetes-engine/), with `acs` also supported for the [Azure Container Service](https://azure.microsoft.com/en-us/services/container-service/). For additional configuration options, consult the [values.yaml](https://gitlab.com/charts/charts.gitlab.io/blob/master/charts/gitlab-omnibus/values.yaml). diff --git a/doc/install/kubernetes/gitlab_runner_chart.md b/doc/install/kubernetes/gitlab_runner_chart.md index 5e0d7493b61..ca9c95aeced 100644 --- a/doc/install/kubernetes/gitlab_runner_chart.md +++ b/doc/install/kubernetes/gitlab_runner_chart.md @@ -1,6 +1,6 @@ # GitLab Runner Helm Chart > **Note:** -These charts have been tested on Google Container Engine and Azure Container Service. Other Kubernetes installations may work as well, if not please [open an issue](https://gitlab.com/charts/charts.gitlab.io/issues). +These charts have been tested on Google Kubernetes Engine and Azure Container Service. Other Kubernetes installations may work as well, if not please [open an issue](https://gitlab.com/charts/charts.gitlab.io/issues). The `gitlab-runner` Helm chart deploys a GitLab Runner instance into your Kubernetes cluster. diff --git a/doc/install/kubernetes/index.md b/doc/install/kubernetes/index.md index dd350820c18..0932e1eee3a 100644 --- a/doc/install/kubernetes/index.md +++ b/doc/install/kubernetes/index.md @@ -1,5 +1,5 @@ # Installing GitLab on Kubernetes -> **Note**: These charts have been tested on Google Container Engine and Azure Container Service. Other Kubernetes installations may work as well, if not please [open an issue](https://gitlab.com/charts/charts.gitlab.io/issues). +> **Note**: These charts have been tested on Google Kubernetes Engine and Azure Container Service. Other Kubernetes installations may work as well, if not please [open an issue](https://gitlab.com/charts/charts.gitlab.io/issues). The easiest method to deploy GitLab on [Kubernetes](https://kubernetes.io/) is to take advantage of GitLab's Helm charts. [Helm] is a package 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/img/auto_devops_settings.png b/doc/topics/autodevops/img/auto_devops_settings.png Binary files differindex b572cc5b855..067c9da3fdc 100644 --- a/doc/topics/autodevops/img/auto_devops_settings.png +++ b/doc/topics/autodevops/img/auto_devops_settings.png diff --git a/doc/topics/autodevops/index.md b/doc/topics/autodevops/index.md index 914217772b8..d100b431721 100644 --- a/doc/topics/autodevops/index.md +++ b/doc/topics/autodevops/index.md @@ -129,8 +129,6 @@ full use of Auto DevOps. If this is your fist time, we recommend you follow the 1. Go to your project's **Settings > CI/CD > General pipelines settings** and find the Auto DevOps section 1. Select "Enable Auto DevOps" -1. After selecting an option to enable Auto DevOps, a checkbox will appear below - so you can immediately run a pipeline on the default branch 1. Optionally, but recommended, add in the [base domain](#auto-devops-base-domain) that will be used by Kubernetes to deploy your application 1. Hit **Save changes** for the changes to take effect diff --git a/doc/topics/autodevops/quick_start_guide.md b/doc/topics/autodevops/quick_start_guide.md index ffe05519d7b..4858735ee86 100644 --- a/doc/topics/autodevops/quick_start_guide.md +++ b/doc/topics/autodevops/quick_start_guide.md @@ -23,12 +23,12 @@ page](https://gitlab.com/auto-devops-examples/minimal-ruby-app) and press the **Fork** button. Soon you should have a project under your namespace with the necessary files. -## Setup your own cluster on Google Container Engine +## Setup your own cluster on Google Kubernetes Engine If you do not already have a Google Cloud account, create one at https://console.cloud.google.com. -Visit the [**Container Engine**](https://console.cloud.google.com/kubernetes/list) +Visit the [**Kubernetes Engine**](https://console.cloud.google.com/kubernetes/list) tab and create a new cluster. You can change the name and leave the rest of the default settings. Once you have your cluster running, you need to connect to the cluster by following the Google interface. diff --git a/doc/user/profile/index.md b/doc/user/profile/index.md index 5fcc0501dc1..04e615330ce 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) diff --git a/doc/user/project/index.md b/doc/user/project/index.md index 97d0d529886..5d91aef5735 100644 --- a/doc/user/project/index.md +++ b/doc/user/project/index.md @@ -64,7 +64,7 @@ common actions on issues or merge requests - [Pipeline settings](pipelines/settings.md): Set up Git strategy (choose the default way your repository is fetched from GitLab in a job), timeout (defines the maximum amount of time in minutes that a job is able run), custom path for `.gitlab-ci.yml`, test coverage parsing, pipeline's visibility, and much more - [GKE cluster integration](clusters/index.md): Connecting your GitLab project - with Google Container Engine + with Google Kubernetes Engine - [GitLab Pages](pages/index.md): Build, test, and deploy your static website with GitLab Pages diff --git a/doc/user/project/pages/index.md b/doc/user/project/pages/index.md index abe6b4cbd8e..8404d789de6 100644 --- a/doc/user/project/pages/index.md +++ b/doc/user/project/pages/index.md @@ -1,49 +1,78 @@ -# GitLab Pages documentation - -With GitLab Pages you can create static websites for your GitLab projects, -groups, or user accounts. You can use any static website generator: Jekyll, -Middleman, Hexo, Hugo, Pelican, you name it! Connect as many customs domains -as you like and bring your own TLS certificate to secure them. - -Here's some info we've gathered to get you started. - -## General info - -- [Product webpage](https://pages.gitlab.io) -- ["We're bringing GitLab Pages to CE"](https://about.gitlab.com/2016/12/24/were-bringing-gitlab-pages-to-community-edition/) -- [Pages group - templates](https://gitlab.com/pages) -- [General user documentation](introduction.md) -- [Admin documentation - Set GitLab Pages on your own GitLab instance](../../../administration/pages/index.md) -- ["We are changing the IP of GitLab Pages on GitLab.com"](https://about.gitlab.com/2017/03/06/we-are-changing-the-ip-of-gitlab-pages-on-gitlab-com/) - -## Getting started - -- **GitLab Pages from A to Z** - - [Part 1: Static sites and GitLab Pages domains](getting_started_part_one.md) - - [Part 2: Quick start guide - Setting up GitLab Pages](getting_started_part_two.md) - - [Part 3: Setting Up Custom Domains - DNS Records and SSL/TLS Certificates](getting_started_part_three.md) - - [Part 4: Creating and tweaking `.gitlab-ci.yml` for GitLab Pages](getting_started_part_four.md) -- **Static Site Generators - Blog posts series** - - [SSGs part 1: Static vs dynamic websites](https://about.gitlab.com/2016/06/03/ssg-overview-gitlab-pages-part-1-dynamic-x-static/) - - [SSGs part 2: Modern static site generators](https://about.gitlab.com/2016/06/10/ssg-overview-gitlab-pages-part-2/) - - [SSGs part 3: Build any SSG site with GitLab Pages](https://about.gitlab.com/2016/06/17/ssg-overview-gitlab-pages-part-3-examples-ci/) -- **Secure GitLab Pages custom domain with SSL/TLS certificates** - - [Let's Encrypt](https://about.gitlab.com/2016/04/11/tutorial-securing-your-gitlab-pages-with-tls-and-letsencrypt/) - - [CloudFlare](https://about.gitlab.com/2017/02/07/setting-up-gitlab-pages-with-cloudflare-certificates/) - - [StartSSL](https://about.gitlab.com/2016/06/24/secure-gitlab-pages-with-startssl/) -- **General** - - [Hosting on GitLab.com with GitLab Pages](https://about.gitlab.com/2016/04/07/gitlab-pages-setup/) a comprehensive step-by-step guide - - [Posting to your GitLab Pages blog from iOS](https://about.gitlab.com/2016/08/19/posting-to-your-gitlab-pages-blog-from-ios/) - -## Video tutorials - -- [How to publish a website with GitLab Pages on GitLab.com: from a forked project](https://youtu.be/TWqh9MtT4Bg) -- [How to Enable GitLab Pages for GitLab CE and EE (for Admins only)](https://youtu.be/dD8c7WNcc6s) +# GitLab Pages + +With GitLab Pages you can host your website at no cost. + +Your files live in a GitLab project's [repository](../repository/index.md), +from which you can deploy [static websites](#explore-gitlab-pages). +GitLab Pages supports all static site generators (SSGs). + +## Getting Started + +Follow the steps below to get your website live. They shouldn't take more than +5 minutes to complete: + +- 1. [Fork](../../../gitlab-basics/fork-project.md#how-to-fork-a-project) an [example project](https://gitlab.com/pages) +- 2. Change a file to trigger a GitLab CI/CD pipeline +- 3. Visit your project's **Settings > Pages** to see your **website link**, and click on it. Bam! Your website is live. + +_Further steps (optional):_ + +- 4. Remove the [fork relationship](getting_started_part_two.md#fork-a-project-to-get-started-from) (_You don't need the relationship unless you intent to contribute back to the example project you forked from_). +- 5. Make it a [user/group website](getting_started_part_one.md#user-and-group-websites) + +**Watch a video with the steps above: https://www.youtube.com/watch?v=TWqh9MtT4Bg** + +_Advanced options:_ + +- [Use a custom domain](getting_started_part_three.md#adding-your-custom-domain-to-gitlab-pages) +- Apply [SSL/TLS certification](getting_started_part_three.md#ssl-tls-certificates) to your custom domain + +## Explore GitLab Pages + +With GitLab Pages you can create [static websites](getting_started_part_one.md#what-you-need-to-know-before-getting-started) +for your GitLab projects, groups, or user accounts. You can use any static +website generator: Jekyll, Middleman, Hexo, Hugo, Pelican, you name it! +Connect as many custom domains as you like and bring your own TLS certificate +to secure them. + +Read the following tutorials to know more about: + +- [Static websites and GitLab Pages domains](getting_started_part_one.md) +- [Forking projects and creating new ones from scratch, URLs and baseurls](getting_started_part_two.md) +- [Custom domains and subdomains, DNS records, SSL/TLS certificates](getting_started_part_three.md) +- [How to create your own `.gitlab-ci.yml` for your site](getting_started_part_four.md) +- [Technical aspects, custom 404 pages, limitations](introduction.md) +- [Hosting on GitLab.com with GitLab Pages](https://about.gitlab.com/2016/04/07/gitlab-pages-setup/) (outdated) + +_Blog posts series about Static Site Generators (SSGs):_ + +- [SSGs part 1: Static vs dynamic websites](https://about.gitlab.com/2016/06/03/ssg-overview-gitlab-pages-part-1-dynamic-x-static/) +- [SSGs part 2: Modern static site generators](https://about.gitlab.com/2016/06/10/ssg-overview-gitlab-pages-part-2/) +- [SSGs part 3: Build any SSG site with GitLab Pages](https://about.gitlab.com/2016/06/17/ssg-overview-gitlab-pages-part-3-examples-ci/) + +_Blog posts for securing GitLab Pages custom domains with SSL/TLS certificates:_ + +- [CloudFlare](https://about.gitlab.com/2017/02/07/setting-up-gitlab-pages-with-cloudflare-certificates/) +- [Let's Encrypt](https://about.gitlab.com/2016/04/11/tutorial-securing-your-gitlab-pages-with-tls-and-letsencrypt/) (outdated) +- [StartSSL](https://about.gitlab.com/2016/06/24/secure-gitlab-pages-with-startssl/) (deprecated) ## Advanced use -- **Blog Posts** - - [GitLab CI: Run jobs sequentially, in parallel, or build a custom pipeline](https://about.gitlab.com/2016/07/29/the-basics-of-gitlab-ci/) - - [GitLab CI: Deployment & environments](https://about.gitlab.com/2016/08/26/ci-deployment-and-environments/) - - [Building a new GitLab docs site with Nanoc, GitLab CI, and GitLab Pages](https://about.gitlab.com/2016/12/07/building-a-new-gitlab-docs-site-with-nanoc-gitlab-ci-and-gitlab-pages/) - - [Publish code coverage reports with GitLab Pages](https://about.gitlab.com/2016/11/03/publish-code-coverage-report-with-gitlab-pages/) +- [Posting to your GitLab Pages blog from iOS](https://about.gitlab.com/2016/08/19/posting-to-your-gitlab-pages-blog-from-ios/) +- [GitLab CI: Run jobs sequentially, in parallel, or build a custom pipeline](https://about.gitlab.com/2016/07/29/the-basics-of-gitlab-ci/) +- [GitLab CI: Deployment & environments](https://about.gitlab.com/2016/08/26/ci-deployment-and-environments/) +- [Building a new GitLab docs site with Nanoc, GitLab CI, and GitLab Pages](https://about.gitlab.com/2016/12/07/building-a-new-gitlab-docs-site-with-nanoc-gitlab-ci-and-gitlab-pages/) +- [Publish code coverage reports with GitLab Pages](https://about.gitlab.com/2016/11/03/publish-code-coverage-report-with-gitlab-pages/) + +## Admin GitLab Pages for CE and EE + +Enable and configure GitLab Pages on your own instance (GitLab Community Edition and Enterprise Editions) with +the [admin guide](../../../administration/pages/index.md). + +**Watch the video: https://www.youtube.com/watch?v=dD8c7WNcc6s** + +## More information about GitLab Pages + +- For an overview, visit the [feature webpage](https://about.gitlab.com/features/pages/) +- Announcement (2016-12-24): ["We're bringing GitLab Pages to CE"](https://about.gitlab.com/2016/12/24/were-bringing-gitlab-pages-to-community-edition/) +- Announcement (2017-03-06): ["We are changing the IP of GitLab Pages on GitLab.com"](https://about.gitlab.com/2017/03/06/we-are-changing-the-ip-of-gitlab-pages-on-gitlab-com/) diff --git a/doc/user/project/pipelines/job_artifacts.md b/doc/user/project/pipelines/job_artifacts.md index f9a268fb789..402989f4508 100644 --- a/doc/user/project/pipelines/job_artifacts.md +++ b/doc/user/project/pipelines/job_artifacts.md @@ -44,7 +44,7 @@ the artifacts will be kept forever. For more examples on artifacts, follow the [artifacts reference in `.gitlab-ci.yml`](../../../ci/yaml/README.md#artifacts). -## Browsing job artifacts +## Browsing artifacts >**Note:** With GitLab 9.2, PDFs, images, videos and other formats can be previewed @@ -77,7 +77,7 @@ one HTML file that you can view directly online when --- -## Downloading job artifacts +## Downloading artifacts If you need to download the whole archive, there are buttons in various places inside GitLab that make that possible. @@ -102,7 +102,7 @@ inside GitLab that make that possible.  -## Downloading the latest job artifacts +## Downloading the latest artifacts It is possible to download the latest artifacts of a job via a well known URL so you can use it for scripting purposes. @@ -163,6 +163,18 @@ information in the UI.  +## Erasing artifacts + +DANGER: **Warning:** +This is a destructive action that leads to data loss. Use with caution. + +If you have at least Developer [permissions](../../permissions.md#gitlab-ci-cd-permissions) +on the project, you can erase a single job via the UI which will also remove the +artifacts and the job's trace. + +1. Navigate to a job's page. +1. Click the trash icon at the top right of the job's trace. +1. Confirm the deletion. [expiry date]: ../../../ci/yaml/README.md#artifacts-expire_in [ce-14399]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/14399 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/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/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/projects.rb b/lib/api/projects.rb index 14a4fc6f025..fa222bf2b1c 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -367,15 +367,16 @@ module API post ":id/fork/:forked_from_id" do authenticated_as_admin! - forked_from_project = find_project!(params[:forked_from_id]) - not_found!("Source Project") unless forked_from_project + fork_from_project = find_project!(params[:forked_from_id]) - if user_project.forked_from_project.nil? - user_project.create_forked_project_link(forked_to_project_id: user_project.id, forked_from_project_id: forked_from_project.id) + not_found!("Source Project") unless fork_from_project - ::Projects::ForksCountService.new(forked_from_project).refresh_cache + result = ::Projects::ForkService.new(fork_from_project, current_user).execute(user_project) + + if result + present user_project.reload, with: Entities::Project else - render_api_error!("Project already forked", 409) + render_api_error!("Project already forked", 409) if user_project.forked? end end @@ -383,11 +384,11 @@ module API delete ":id/fork" do authorize! :remove_fork_project, user_project - if user_project.forked? - destroy_conditionally!(user_project.forked_project_link) - else - not_modified! + result = destroy_conditionally!(user_project) do + ::Projects::UnlinkForkService.new(user_project, current_user).execute end + + result ? status(204) : not_modified! end desc 'Share the project with a group' do diff --git a/lib/api/protected_branches.rb b/lib/api/protected_branches.rb index b5021e8a712..614822509f0 100644 --- a/lib/api/protected_branches.rb +++ b/lib/api/protected_branches.rb @@ -39,10 +39,10 @@ module API end params do requires :name, type: String, desc: 'The name of the protected branch' - optional :push_access_level, type: Integer, default: Gitlab::Access::MASTER, + optional :push_access_level, type: Integer, values: ProtectedRefAccess::ALLOWED_ACCESS_LEVELS, desc: 'Access levels allowed to push (defaults: `40`, master access level)' - optional :merge_access_level, type: Integer, default: Gitlab::Access::MASTER, + optional :merge_access_level, type: Integer, values: ProtectedRefAccess::ALLOWED_ACCESS_LEVELS, desc: 'Access levels allowed to merge (defaults: `40`, master access level)' end @@ -52,15 +52,13 @@ module API conflict!("Protected branch '#{params[:name]}' already exists") end - protected_branch_params = { - name: params[:name], - push_access_levels_attributes: [{ access_level: params[:push_access_level] }], - merge_access_levels_attributes: [{ access_level: params[:merge_access_level] }] - } + # Replace with `declared(params)` after updating to grape v1.0.2 + # See https://github.com/ruby-grape/grape/pull/1710 + # and https://gitlab.com/gitlab-org/gitlab-ce/issues/40843 + declared_params = params.slice("name", "push_access_level", "merge_access_level", "allowed_to_push", "allowed_to_merge") - service_args = [user_project, current_user, protected_branch_params] - - protected_branch = ::ProtectedBranches::CreateService.new(*service_args).execute + api_service = ::ProtectedBranches::ApiService.new(user_project, current_user, declared_params) + protected_branch = api_service.create if protected_branch.persisted? present protected_branch, with: Entities::ProtectedBranch, project: user_project diff --git a/lib/backup/repository.rb b/lib/backup/repository.rb index b6d273b98c2..2a04c03919d 100644 --- a/lib/backup/repository.rb +++ b/lib/backup/repository.rb @@ -193,12 +193,9 @@ module Backup end def empty_repo?(project_or_wiki) - project_or_wiki.repository.expire_exists_cache # protect backups from stale cache - project_or_wiki.repository.empty_repo? - rescue => e - progress.puts "Ignoring repository error and continuing backing up project: #{display_repo_path(project_or_wiki)} - #{e.message}".color(:orange) - - false + # Protect against stale caches + project_or_wiki.repository.expire_emptiness_caches + project_or_wiki.repository.empty? end def repository_storage_paths_args diff --git a/lib/banzai/cross_project_reference.rb b/lib/banzai/cross_project_reference.rb index e2b57adf611..d8fb7705b2a 100644 --- a/lib/banzai/cross_project_reference.rb +++ b/lib/banzai/cross_project_reference.rb @@ -11,7 +11,7 @@ module Banzai # ref - String reference. # # Returns a Project, or nil if the reference can't be found - def project_from_ref(ref) + def parent_from_ref(ref) return context[:project] unless ref Project.find_by_full_path(ref) diff --git a/lib/banzai/filter/abstract_reference_filter.rb b/lib/banzai/filter/abstract_reference_filter.rb index 8975395aff1..e7e6a90b5fd 100644 --- a/lib/banzai/filter/abstract_reference_filter.rb +++ b/lib/banzai/filter/abstract_reference_filter.rb @@ -82,9 +82,9 @@ module Banzai end end - def project_from_ref_cached(ref) - cached_call(:banzai_project_refs, ref) do - project_from_ref(ref) + def from_ref_cached(ref) + cached_call("banzai_#{parent_type}_refs".to_sym, ref) do + parent_from_ref(ref) end end @@ -153,15 +153,20 @@ module Banzai # have `gfm` and `gfm-OBJECT_NAME` class names attached for styling. def object_link_filter(text, pattern, link_content: nil, link_reference: false) references_in(text, pattern) do |match, id, project_ref, namespace_ref, matches| - project_path = full_project_path(namespace_ref, project_ref) - project = project_from_ref_cached(project_path) + parent_path = if parent_type == :group + full_group_path(namespace_ref) + else + full_project_path(namespace_ref, project_ref) + end - if project + parent = from_ref_cached(parent_path) + + if parent object = if link_reference - find_object_from_link_cached(project, id) + find_object_from_link_cached(parent, id) else - find_object_cached(project, id) + find_object_cached(parent, id) end end @@ -169,13 +174,13 @@ module Banzai title = object_link_title(object) klass = reference_class(object_sym) - data = data_attributes_for(link_content || match, project, object, link: !!link_content) + data = data_attributes_for(link_content || match, parent, object, link: !!link_content) url = if matches.names.include?("url") && matches[:url] matches[:url] else - url_for_object_cached(object, project) + url_for_object_cached(object, parent) end content = link_content || object_link_text(object, matches) @@ -224,17 +229,24 @@ module Banzai # Returns a Hash containing all object references (e.g. issue IDs) per the # project they belong to. - def references_per_project - @references_per_project ||= begin + def references_per_parent + @references_per ||= {} + + @references_per[parent_type] ||= begin refs = Hash.new { |hash, key| hash[key] = Set.new } regex = Regexp.union(object_class.reference_pattern, object_class.link_reference_pattern) nodes.each do |node| node.to_html.scan(regex) do - project_path = full_project_path($~[:namespace], $~[:project]) + path = if parent_type == :project + full_project_path($~[:namespace], $~[:project]) + else + full_group_path($~[:group]) + end + symbol = $~[object_sym] - refs[project_path] << symbol if object_class.reference_valid?(symbol) + refs[path] << symbol if object_class.reference_valid?(symbol) end end @@ -244,35 +256,41 @@ module Banzai # Returns a Hash containing referenced projects grouped per their full # path. - def projects_per_reference - @projects_per_reference ||= begin + def parent_per_reference + @per_reference ||= {} + + @per_reference[parent_type] ||= begin refs = Set.new - references_per_project.each do |project_ref, _| - refs << project_ref + references_per_parent.each do |ref, _| + refs << ref end - find_projects_for_paths(refs.to_a).index_by(&:full_path) + find_for_paths(refs.to_a).index_by(&:full_path) end end - def projects_relation_for_paths(paths) - Project.where_full_path_in(paths).includes(:namespace) + def relation_for_paths(paths) + klass = parent_type.to_s.camelize.constantize + result = klass.where_full_path_in(paths) + return result if parent_type == :group + + result.includes(:namespace) if parent_type == :project end # Returns projects for the given paths. - def find_projects_for_paths(paths) + def find_for_paths(paths) if RequestStore.active? - cache = project_refs_cache + cache = refs_cache to_query = paths - cache.keys unless to_query.empty? - projects = projects_relation_for_paths(to_query) + records = relation_for_paths(to_query) found = [] - projects.each do |project| - ref = project.full_path - get_or_set_cache(cache, ref) { project } + records.each do |record| + ref = record.full_path + get_or_set_cache(cache, ref) { record } found << ref end @@ -284,33 +302,37 @@ module Banzai cache.slice(*paths).values.compact else - projects_relation_for_paths(paths) + relation_for_paths(paths) end end - def current_project_path - return unless project - - @current_project_path ||= project.full_path + def current_parent_path + @current_parent_path ||= parent&.full_path end def current_project_namespace_path - return unless project - - @current_project_namespace_path ||= project.namespace.full_path + @current_project_namespace_path ||= project&.namespace&.full_path end private def full_project_path(namespace, project_ref) - return current_project_path unless project_ref + return current_parent_path unless project_ref namespace_ref = namespace || current_project_namespace_path "#{namespace_ref}/#{project_ref}" end - def project_refs_cache - RequestStore[:banzai_project_refs] ||= {} + def refs_cache + RequestStore["banzai_#{parent_type}_refs".to_sym] ||= {} + end + + def parent_type + :project + end + + def parent + parent_type == :project ? project : group end end end diff --git a/lib/banzai/filter/commit_reference_filter.rb b/lib/banzai/filter/commit_reference_filter.rb index 714e0319025..eedb95197aa 100644 --- a/lib/banzai/filter/commit_reference_filter.rb +++ b/lib/banzai/filter/commit_reference_filter.rb @@ -22,10 +22,30 @@ module Banzai end end + def referenced_merge_request_commit_shas + return [] unless noteable.is_a?(MergeRequest) + + @referenced_merge_request_commit_shas ||= begin + referenced_shas = references_per_parent.values.reduce(:|).to_a + noteable.all_commit_shas.select do |sha| + referenced_shas.any? { |ref| Gitlab::Git.shas_eql?(sha, ref) } + end + end + end + def url_for_object(commit, project) h = Gitlab::Routing.url_helpers - h.project_commit_url(project, commit, - only_path: context[:only_path]) + + if referenced_merge_request_commit_shas.include?(commit.id) + h.diffs_project_merge_request_url(project, + noteable, + commit_id: commit.id, + only_path: only_path?) + else + h.project_commit_url(project, + commit, + only_path: only_path?) + end end def object_link_text_extras(object, matches) @@ -38,6 +58,16 @@ module Banzai extras end + + private + + def noteable + context[:noteable] + end + + def only_path? + context[:only_path] + end end end end diff --git a/lib/banzai/filter/epic_reference_filter.rb b/lib/banzai/filter/epic_reference_filter.rb new file mode 100644 index 00000000000..265924abe24 --- /dev/null +++ b/lib/banzai/filter/epic_reference_filter.rb @@ -0,0 +1,12 @@ +module Banzai + module Filter + # The actual filter is implemented in the EE mixin + class EpicReferenceFilter < IssuableReferenceFilter + self.reference_type = :epic + + def self.object_class + Epic + end + end + end +end diff --git a/lib/banzai/filter/issuable_reference_filter.rb b/lib/banzai/filter/issuable_reference_filter.rb new file mode 100644 index 00000000000..7addf09be73 --- /dev/null +++ b/lib/banzai/filter/issuable_reference_filter.rb @@ -0,0 +1,31 @@ +module Banzai + module Filter + class IssuableReferenceFilter < AbstractReferenceFilter + def records_per_parent + @records_per_project ||= {} + + @records_per_project[object_class.to_s.underscore] ||= begin + hash = Hash.new { |h, k| h[k] = {} } + + parent_per_reference.each do |path, parent| + record_ids = references_per_parent[path] + + parent_records(parent, record_ids).each do |record| + hash[parent][record.iid.to_i] = record + end + end + + hash + end + end + + def find_object(parent, iid) + records_per_parent[parent][iid] + end + + def parent_from_ref(ref) + parent_per_reference[ref || current_parent_path] + end + end + end +end diff --git a/lib/banzai/filter/issue_reference_filter.rb b/lib/banzai/filter/issue_reference_filter.rb index ce1ab977d3b..6877cae8c55 100644 --- a/lib/banzai/filter/issue_reference_filter.rb +++ b/lib/banzai/filter/issue_reference_filter.rb @@ -8,46 +8,24 @@ module Banzai # When external issues tracker like Jira is activated we should not # use issue reference pattern, but we should still be able # to reference issues from other GitLab projects. - class IssueReferenceFilter < AbstractReferenceFilter + class IssueReferenceFilter < IssuableReferenceFilter self.reference_type = :issue def self.object_class Issue end - def find_object(project, iid) - issues_per_project[project][iid] - end - def url_for_object(issue, project) IssuesHelper.url_for_issue(issue.iid, project, only_path: context[:only_path], internal: true) end - def project_from_ref(ref) - projects_per_reference[ref || current_project_path] - end - - # Returns a Hash containing the issues per Project instance. - def issues_per_project - @issues_per_project ||= begin - hash = Hash.new { |h, k| h[k] = {} } - - projects_per_reference.each do |path, project| - issue_ids = references_per_project[path] - issues = project.issues.where(iid: issue_ids.to_a) - - issues.each do |issue| - hash[project][issue.iid.to_i] = issue - end - end - - hash - end - end - def projects_relation_for_paths(paths) super(paths).includes(:gitlab_issue_tracker_service) end + + def parent_records(parent, ids) + parent.issues.where(iid: ids.to_a) + end end end end diff --git a/lib/banzai/filter/label_reference_filter.rb b/lib/banzai/filter/label_reference_filter.rb index 5364984c9d3..d5360ad8f68 100644 --- a/lib/banzai/filter/label_reference_filter.rb +++ b/lib/banzai/filter/label_reference_filter.rb @@ -33,7 +33,7 @@ module Banzai end def find_label(project_ref, label_id, label_name) - project = project_from_ref(project_ref) + project = parent_from_ref(project_ref) return unless project label_params = label_params(label_id, label_name) @@ -66,7 +66,7 @@ module Banzai def object_link_text(object, matches) project_path = full_project_path(matches[:namespace], matches[:project]) - project_from_ref = project_from_ref_cached(project_path) + project_from_ref = from_ref_cached(project_path) reference = project_from_ref.to_human_reference(project) label_suffix = " <i>in #{reference}</i>" if reference.present? diff --git a/lib/banzai/filter/merge_request_reference_filter.rb b/lib/banzai/filter/merge_request_reference_filter.rb index 0eab865ac04..b3cfa97d0e0 100644 --- a/lib/banzai/filter/merge_request_reference_filter.rb +++ b/lib/banzai/filter/merge_request_reference_filter.rb @@ -4,48 +4,19 @@ module Banzai # to merge requests that do not exist are ignored. # # This filter supports cross-project references. - class MergeRequestReferenceFilter < AbstractReferenceFilter + class MergeRequestReferenceFilter < IssuableReferenceFilter self.reference_type = :merge_request def self.object_class MergeRequest end - def find_object(project, iid) - merge_requests_per_project[project][iid] - end - def url_for_object(mr, project) h = Gitlab::Routing.url_helpers h.project_merge_request_url(project, mr, only_path: context[:only_path]) end - def project_from_ref(ref) - projects_per_reference[ref || current_project_path] - end - - # Returns a Hash containing the merge_requests per Project instance. - def merge_requests_per_project - @merge_requests_per_project ||= begin - hash = Hash.new { |h, k| h[k] = {} } - - projects_per_reference.each do |path, project| - merge_request_ids = references_per_project[path] - - merge_requests = project.merge_requests - .where(iid: merge_request_ids.to_a) - .includes(target_project: :namespace) - - merge_requests.each do |merge_request| - hash[project][merge_request.iid.to_i] = merge_request - end - end - - hash - end - end - def object_link_text_extras(object, matches) extras = super @@ -61,6 +32,12 @@ module Banzai extras end + + def parent_records(parent, ids) + parent.merge_requests + .where(iid: ids.to_a) + .includes(target_project: :namespace) + end end end end diff --git a/lib/banzai/filter/milestone_reference_filter.rb b/lib/banzai/filter/milestone_reference_filter.rb index bb5da310e09..2a6b0964ac5 100644 --- a/lib/banzai/filter/milestone_reference_filter.rb +++ b/lib/banzai/filter/milestone_reference_filter.rb @@ -38,7 +38,7 @@ module Banzai def find_milestone(project_ref, namespace_ref, milestone_id, milestone_name) project_path = full_project_path(namespace_ref, project_ref) - project = project_from_ref(project_path) + project = parent_from_ref(project_path) return unless project 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/banzai/filter/upload_link_filter.rb b/lib/banzai/filter/upload_link_filter.rb index 09844931be5..d64f9ac4eb6 100644 --- a/lib/banzai/filter/upload_link_filter.rb +++ b/lib/banzai/filter/upload_link_filter.rb @@ -8,7 +8,7 @@ module Banzai # class UploadLinkFilter < HTML::Pipeline::Filter def call - return doc unless project + return doc unless project || group doc.xpath('descendant-or-self::a[starts-with(@href, "/uploads/")]').each do |el| process_link_attr el.attribute('href') @@ -28,13 +28,27 @@ module Banzai end def build_url(uri) - File.join(Gitlab.config.gitlab.url, project.full_path, uri) + base_path = Gitlab.config.gitlab.url + + if group + urls = Gitlab::Routing.url_helpers + # we need to get last 2 parts of the uri which are secret and filename + uri_parts = uri.split(File::SEPARATOR) + file_path = urls.show_group_uploads_path(group, uri_parts[-2], uri_parts[-1]) + File.join(base_path, file_path) + else + File.join(base_path, project.full_path, uri) + end end def project context[:project] end + def group + context[:group] + end + # Ensure that a :project key exists in context # # Note that while the key might exist, its value could be nil! diff --git a/lib/banzai/issuable_extractor.rb b/lib/banzai/issuable_extractor.rb index cbabf9156de..49603d0b363 100644 --- a/lib/banzai/issuable_extractor.rb +++ b/lib/banzai/issuable_extractor.rb @@ -28,8 +28,8 @@ module Banzai issue_parser = Banzai::ReferenceParser::IssueParser.new(project, user) merge_request_parser = Banzai::ReferenceParser::MergeRequestParser.new(project, user) - issuables_for_nodes = issue_parser.issues_for_nodes(nodes).merge( - merge_request_parser.merge_requests_for_nodes(nodes) + issuables_for_nodes = issue_parser.records_for_nodes(nodes).merge( + merge_request_parser.records_for_nodes(nodes) ) # The project for the issue/MR might be pending for deletion! diff --git a/lib/banzai/object_renderer.rb b/lib/banzai/object_renderer.rb index ecb3affbba5..2691be81623 100644 --- a/lib/banzai/object_renderer.rb +++ b/lib/banzai/object_renderer.rb @@ -17,11 +17,11 @@ module Banzai # project - A Project to use for redacting Markdown. # user - The user viewing the Markdown/HTML documents, if any. - # context - A Hash containing extra attributes to use during redaction + # redaction_context - A Hash containing extra attributes to use during redaction def initialize(project, user = nil, redaction_context = {}) @project = project @user = user - @redaction_context = redaction_context + @redaction_context = base_context.merge(redaction_context) end # Renders and redacts an Array of objects. @@ -73,19 +73,19 @@ module Banzai # Returns a Banzai context for the given object and attribute. def context_for(object, attribute) - base_context.merge(object.banzai_render_context(attribute)) + @redaction_context.merge(object.banzai_render_context(attribute)) end def base_context - @base_context ||= @redaction_context.merge( + { current_user: user, project: project, skip_redaction: true - ) + } end def save_options - return {} unless base_context[:xhtml] + return {} unless @redaction_context[:xhtml] { save_with: Nokogiri::XML::Node::SaveOptions::AS_XHTML } end diff --git a/lib/banzai/reference_parser/epic_parser.rb b/lib/banzai/reference_parser/epic_parser.rb new file mode 100644 index 00000000000..08b8a4c9a0f --- /dev/null +++ b/lib/banzai/reference_parser/epic_parser.rb @@ -0,0 +1,12 @@ +module Banzai + module ReferenceParser + # The actual parser is implemented in the EE mixin + class EpicParser < IssuableParser + self.reference_type = :epic + + def records_for_nodes(_nodes) + {} + end + end + end +end diff --git a/lib/banzai/reference_parser/issuable_parser.rb b/lib/banzai/reference_parser/issuable_parser.rb new file mode 100644 index 00000000000..3953867eb83 --- /dev/null +++ b/lib/banzai/reference_parser/issuable_parser.rb @@ -0,0 +1,25 @@ +module Banzai + module ReferenceParser + class IssuableParser < BaseParser + def nodes_visible_to_user(user, nodes) + records = records_for_nodes(nodes) + + nodes.select do |node| + issuable = records[node] + + issuable && can_read_reference?(user, issuable) + end + end + + def referenced_by(nodes) + records = records_for_nodes(nodes) + + nodes.map { |node| records[node] }.compact.uniq + end + + def can_read_reference?(user, issuable) + can?(user, "read_#{issuable.class.to_s.underscore}".to_sym, issuable) + end + end + end +end diff --git a/lib/banzai/reference_parser/issue_parser.rb b/lib/banzai/reference_parser/issue_parser.rb index e0a8ca653cb..38d4e3f3e44 100644 --- a/lib/banzai/reference_parser/issue_parser.rb +++ b/lib/banzai/reference_parser/issue_parser.rb @@ -1,10 +1,10 @@ module Banzai module ReferenceParser - class IssueParser < BaseParser + class IssueParser < IssuableParser self.reference_type = :issue def nodes_visible_to_user(user, nodes) - issues = issues_for_nodes(nodes) + issues = records_for_nodes(nodes) readable_issues = Ability .issues_readable_by_user(issues.values, user).to_set @@ -14,13 +14,7 @@ module Banzai end end - def referenced_by(nodes) - issues = issues_for_nodes(nodes) - - nodes.map { |node| issues[node] }.compact.uniq - end - - def issues_for_nodes(nodes) + def records_for_nodes(nodes) @issues_for_nodes ||= grouped_objects_for_nodes( nodes, Issue.all.includes( diff --git a/lib/banzai/reference_parser/merge_request_parser.rb b/lib/banzai/reference_parser/merge_request_parser.rb index 75cbc7fdac4..a370ff5b5b3 100644 --- a/lib/banzai/reference_parser/merge_request_parser.rb +++ b/lib/banzai/reference_parser/merge_request_parser.rb @@ -1,25 +1,9 @@ module Banzai module ReferenceParser - class MergeRequestParser < BaseParser + class MergeRequestParser < IssuableParser self.reference_type = :merge_request - def nodes_visible_to_user(user, nodes) - merge_requests = merge_requests_for_nodes(nodes) - - nodes.select do |node| - merge_request = merge_requests[node] - - merge_request && can?(user, :read_merge_request, merge_request.project) - end - end - - def referenced_by(nodes) - merge_requests = merge_requests_for_nodes(nodes) - - nodes.map { |node| merge_requests[node] }.compact.uniq - end - - def merge_requests_for_nodes(nodes) + def records_for_nodes(nodes) @merge_requests_for_nodes ||= grouped_objects_for_nodes( nodes, MergeRequest.includes( @@ -40,10 +24,6 @@ module Banzai self.class.data_attribute ) end - - def can_read_reference?(user, ref_project, node) - can?(user, :read_merge_request, ref_project) - end end end end diff --git a/lib/gitlab/background_migration/populate_untracked_uploads.rb b/lib/gitlab/background_migration/populate_untracked_uploads.rb new file mode 100644 index 00000000000..81e95e5832d --- /dev/null +++ b/lib/gitlab/background_migration/populate_untracked_uploads.rb @@ -0,0 +1,259 @@ +# frozen_string_literal: true + +module Gitlab + module BackgroundMigration + # This class processes a batch of rows in `untracked_files_for_uploads` by + # adding each file to the `uploads` table if it does not exist. + class PopulateUntrackedUploads # rubocop:disable Metrics/ClassLength + # This class is responsible for producing the attributes necessary to + # track an uploaded file in the `uploads` table. + class UntrackedFile < ActiveRecord::Base # rubocop:disable Metrics/ClassLength, Metrics/LineLength + self.table_name = 'untracked_files_for_uploads' + + # Ends with /:random_hex/:filename + FILE_UPLOADER_PATH = %r{/\h+/[^/]+\z} + FULL_PATH_CAPTURE = %r{\A(.+)#{FILE_UPLOADER_PATH}} + + # These regex patterns are tested against a relative path, relative to + # the upload directory. + # For convenience, if there exists a capture group in the pattern, then + # it indicates the model_id. + PATH_PATTERNS = [ + { + pattern: %r{\A-/system/appearance/logo/(\d+)/}, + uploader: 'AttachmentUploader', + model_type: 'Appearance' + }, + { + pattern: %r{\A-/system/appearance/header_logo/(\d+)/}, + uploader: 'AttachmentUploader', + model_type: 'Appearance' + }, + { + pattern: %r{\A-/system/note/attachment/(\d+)/}, + uploader: 'AttachmentUploader', + model_type: 'Note' + }, + { + pattern: %r{\A-/system/user/avatar/(\d+)/}, + uploader: 'AvatarUploader', + model_type: 'User' + }, + { + pattern: %r{\A-/system/group/avatar/(\d+)/}, + uploader: 'AvatarUploader', + model_type: 'Namespace' + }, + { + pattern: %r{\A-/system/project/avatar/(\d+)/}, + uploader: 'AvatarUploader', + model_type: 'Project' + }, + { + pattern: FILE_UPLOADER_PATH, + uploader: 'FileUploader', + model_type: 'Project' + } + ].freeze + + def to_h + @upload_hash ||= { + path: upload_path, + uploader: uploader, + model_type: model_type, + model_id: model_id, + size: file_size, + checksum: checksum + } + end + + def upload_path + # UntrackedFile#path is absolute, but Upload#path depends on uploader + @upload_path ||= + if uploader == 'FileUploader' + # Path relative to project directory in uploads + matchd = path_relative_to_upload_dir.match(FILE_UPLOADER_PATH) + matchd[0].sub(%r{\A/}, '') # remove leading slash + else + path + end + end + + def uploader + matching_pattern_map[:uploader] + end + + def model_type + matching_pattern_map[:model_type] + end + + def model_id + return @model_id if defined?(@model_id) + + pattern = matching_pattern_map[:pattern] + matchd = path_relative_to_upload_dir.match(pattern) + + # If something is captured (matchd[1] is not nil), it is a model_id + # Only the FileUploader pattern will not match an ID + @model_id = matchd[1] ? matchd[1].to_i : file_uploader_model_id + end + + def file_size + File.size(absolute_path) + end + + def checksum + Digest::SHA256.file(absolute_path).hexdigest + end + + private + + def matching_pattern_map + @matching_pattern_map ||= PATH_PATTERNS.find do |path_pattern_map| + path_relative_to_upload_dir.match(path_pattern_map[:pattern]) + end + + unless @matching_pattern_map + raise "Unknown upload path pattern \"#{path}\"" + end + + @matching_pattern_map + end + + def file_uploader_model_id + matchd = path_relative_to_upload_dir.match(FULL_PATH_CAPTURE) + not_found_msg = <<~MSG + Could not capture project full_path from a FileUploader path: + "#{path_relative_to_upload_dir}" + MSG + raise not_found_msg unless matchd + + full_path = matchd[1] + project = Project.find_by_full_path(full_path) + return nil unless project + + project.id + end + + # Not including a leading slash + def path_relative_to_upload_dir + upload_dir = Gitlab::BackgroundMigration::PrepareUntrackedUploads::RELATIVE_UPLOAD_DIR # rubocop:disable Metrics/LineLength + base = %r{\A#{Regexp.escape(upload_dir)}/} + @path_relative_to_upload_dir ||= path.sub(base, '') + end + + def absolute_path + File.join(CarrierWave.root, path) + end + end + + # This class is used to query the `uploads` table. + class Upload < ActiveRecord::Base + self.table_name = 'uploads' + end + + def perform(start_id, end_id) + return unless migrate? + + files = UntrackedFile.where(id: start_id..end_id) + processed_files = insert_uploads_if_needed(files) + processed_files.delete_all + + drop_temp_table_if_finished + end + + private + + def migrate? + UntrackedFile.table_exists? && Upload.table_exists? + end + + def insert_uploads_if_needed(files) + filtered_files, error_files = filter_error_files(files) + filtered_files = filter_existing_uploads(filtered_files) + filtered_files = filter_deleted_models(filtered_files) + insert(filtered_files) + + processed_files = files.where.not(id: error_files.map(&:id)) + processed_files + end + + def filter_error_files(files) + files.partition do |file| + begin + file.to_h + true + rescue => e + msg = <<~MSG + Error parsing path "#{file.path}": + #{e.message} + #{e.backtrace.join("\n ")} + MSG + Rails.logger.error(msg) + false + end + end + end + + def filter_existing_uploads(files) + paths = files.map(&:upload_path) + existing_paths = Upload.where(path: paths).pluck(:path).to_set + + files.reject do |file| + existing_paths.include?(file.upload_path) + end + end + + # There are files on disk that are not in the uploads table because their + # model was deleted, and we don't delete the files on disk. + def filter_deleted_models(files) + ids = deleted_model_ids(files) + + files.reject do |file| + ids[file.model_type].include?(file.model_id) + end + end + + def deleted_model_ids(files) + ids = { + 'Appearance' => [], + 'Namespace' => [], + 'Note' => [], + 'Project' => [], + 'User' => [] + } + + # group model IDs by model type + files.each do |file| + ids[file.model_type] << file.model_id + end + + ids.each do |model_type, model_ids| + model_class = Object.const_get(model_type) + found_ids = model_class.where(id: model_ids.uniq).pluck(:id) + deleted_ids = ids[model_type] - found_ids + ids[model_type] = deleted_ids + end + + ids + end + + def insert(files) + rows = files.map do |file| + file.to_h.merge(created_at: 'NOW()') + end + + Gitlab::Database.bulk_insert('uploads', + rows, + disable_quote: :created_at) + end + + def drop_temp_table_if_finished + if UntrackedFile.all.empty? + UntrackedFile.connection.drop_table(:untracked_files_for_uploads, + if_exists: true) + end + end + end + end +end diff --git a/lib/gitlab/background_migration/prepare_untracked_uploads.rb b/lib/gitlab/background_migration/prepare_untracked_uploads.rb new file mode 100644 index 00000000000..476c46341ae --- /dev/null +++ b/lib/gitlab/background_migration/prepare_untracked_uploads.rb @@ -0,0 +1,163 @@ +# frozen_string_literal: true + +module Gitlab + module BackgroundMigration + # This class finds all non-hashed uploaded file paths and saves them to a + # `untracked_files_for_uploads` table. + class PrepareUntrackedUploads # rubocop:disable Metrics/ClassLength + # For bulk_queue_background_migration_jobs_by_range + include Database::MigrationHelpers + + FIND_BATCH_SIZE = 500 + RELATIVE_UPLOAD_DIR = "uploads".freeze + ABSOLUTE_UPLOAD_DIR = "#{CarrierWave.root}/#{RELATIVE_UPLOAD_DIR}".freeze + FOLLOW_UP_MIGRATION = 'PopulateUntrackedUploads'.freeze + START_WITH_CARRIERWAVE_ROOT_REGEX = %r{\A#{CarrierWave.root}/} + EXCLUDED_HASHED_UPLOADS_PATH = "#{ABSOLUTE_UPLOAD_DIR}/@hashed/*".freeze + EXCLUDED_TMP_UPLOADS_PATH = "#{ABSOLUTE_UPLOAD_DIR}/tmp/*".freeze + + # This class is used to iterate over batches of + # `untracked_files_for_uploads` rows. + class UntrackedFile < ActiveRecord::Base + include EachBatch + + self.table_name = 'untracked_files_for_uploads' + end + + def perform + ensure_temporary_tracking_table_exists + + # Since Postgres < 9.5 does not have ON CONFLICT DO NOTHING, and since + # doing inserts-if-not-exists without ON CONFLICT DO NOTHING would be + # slow, start with an empty table for Postgres < 9.5. + # That way we can do bulk inserts at ~30x the speed of individual + # inserts (~20 minutes worth of inserts at GitLab.com scale instead of + # ~10 hours). + # In all other cases, installations will get both bulk inserts and the + # ability for these jobs to retry without having to clear and reinsert. + clear_untracked_file_paths unless can_bulk_insert_and_ignore_duplicates? + + store_untracked_file_paths + + schedule_populate_untracked_uploads_jobs + end + + private + + def ensure_temporary_tracking_table_exists + table_name = :untracked_files_for_uploads + unless UntrackedFile.connection.table_exists?(table_name) + UntrackedFile.connection.create_table table_name do |t| + t.string :path, limit: 600, null: false + t.index :path, unique: true + end + end + end + + def clear_untracked_file_paths + UntrackedFile.delete_all + end + + def store_untracked_file_paths + return unless Dir.exist?(ABSOLUTE_UPLOAD_DIR) + + each_file_batch(ABSOLUTE_UPLOAD_DIR, FIND_BATCH_SIZE) do |file_paths| + insert_file_paths(file_paths) + end + end + + def each_file_batch(search_dir, batch_size, &block) + cmd = build_find_command(search_dir) + + Open3.popen2(*cmd) do |stdin, stdout, status_thread| + yield_paths_in_batches(stdout, batch_size, &block) + + raise "Find command failed" unless status_thread.value.success? + end + end + + def yield_paths_in_batches(stdout, batch_size, &block) + paths = [] + + stdout.each_line("\0") do |line| + paths << line.chomp("\0").sub(START_WITH_CARRIERWAVE_ROOT_REGEX, '') + + if paths.size >= batch_size + yield(paths) + paths = [] + end + end + + yield(paths) + end + + def build_find_command(search_dir) + cmd = %W[find -L #{search_dir} + -type f + ! ( -path #{EXCLUDED_HASHED_UPLOADS_PATH} -prune ) + ! ( -path #{EXCLUDED_TMP_UPLOADS_PATH} -prune ) + -print0] + + ionice = which_ionice + cmd = %W[#{ionice} -c Idle] + cmd if ionice + + log_msg = "PrepareUntrackedUploads find command: \"#{cmd.join(' ')}\"" + Rails.logger.info log_msg + + cmd + end + + def which_ionice + Gitlab::Utils.which('ionice') + rescue StandardError + # In this case, returning false is relatively safe, + # even though it isn't very nice + false + end + + def insert_file_paths(file_paths) + sql = insert_sql(file_paths) + + ActiveRecord::Base.connection.execute(sql) + end + + def insert_sql(file_paths) + if postgresql_pre_9_5? + "INSERT INTO #{table_columns_and_values_for_insert(file_paths)};" + elsif postgresql? + "INSERT INTO #{table_columns_and_values_for_insert(file_paths)}"\ + " ON CONFLICT DO NOTHING;" + else # MySQL + "INSERT IGNORE INTO"\ + " #{table_columns_and_values_for_insert(file_paths)};" + end + end + + def table_columns_and_values_for_insert(file_paths) + values = file_paths.map do |file_path| + ActiveRecord::Base.send(:sanitize_sql_array, ['(?)', file_path]) # rubocop:disable GitlabSecurity/PublicSend, Metrics/LineLength + end.join(', ') + + "#{UntrackedFile.table_name} (path) VALUES #{values}" + end + + def postgresql? + @postgresql ||= Gitlab::Database.postgresql? + end + + def can_bulk_insert_and_ignore_duplicates? + !postgresql_pre_9_5? + end + + def postgresql_pre_9_5? + @postgresql_pre_9_5 ||= postgresql? && + Gitlab::Database.version.to_f < 9.5 + end + + def schedule_populate_untracked_uploads_jobs + bulk_queue_background_migration_jobs_by_range( + UntrackedFile, FOLLOW_UP_MIGRATION) + end + end + end +end 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/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/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/create.rb b/lib/gitlab/ci/pipeline/chain/create.rb index d5e17a123df..d19a2519803 100644 --- a/lib/gitlab/ci/pipeline/chain/create.rb +++ b/lib/gitlab/ci/pipeline/chain/create.rb @@ -17,11 +17,27 @@ module Gitlab end rescue ActiveRecord::RecordInvalid => e error("Failed to persist the pipeline: #{e}") + ensure + if pipeline.builds.where(stage_id: nil).any? + invalid_builds_counter.increment(node: hostname) + end end def break? !pipeline.persisted? end + + private + + def invalid_builds_counter + @counter ||= Gitlab::Metrics + .counter(:gitlab_ci_invalid_builds_total, + 'Invalid builds without stage assigned counter') + end + + def hostname + @hostname ||= Socket.gethostname + 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/database.rb b/lib/gitlab/database.rb index 96922e1a62f..e51794fef99 100644 --- a/lib/gitlab/database.rb +++ b/lib/gitlab/database.rb @@ -120,15 +120,21 @@ module Gitlab # values. # return_ids - When set to true the return value will be an Array of IDs of # the inserted rows, this only works on PostgreSQL. - def self.bulk_insert(table, rows, return_ids: false) + # disable_quote - A key or an Array of keys to exclude from quoting (You + # become responsible for protection from SQL injection for + # these keys!) + def self.bulk_insert(table, rows, return_ids: false, disable_quote: []) return if rows.empty? keys = rows.first.keys columns = keys.map { |key| connection.quote_column_name(key) } return_ids = false if mysql? + disable_quote = Array(disable_quote).to_set tuples = rows.map do |row| - row.values_at(*keys).map { |value| connection.quote(value) } + keys.map do |k| + disable_quote.include?(k) ? row[k] : connection.quote(row[k]) + end end sql = <<-EOF diff --git a/lib/gitlab/diff/diff_refs.rb b/lib/gitlab/diff/diff_refs.rb index c98eefbce25..88e0db830f6 100644 --- a/lib/gitlab/diff/diff_refs.rb +++ b/lib/gitlab/diff/diff_refs.rb @@ -13,9 +13,9 @@ module Gitlab def ==(other) other.is_a?(self.class) && - shas_equal?(base_sha, other.base_sha) && - shas_equal?(start_sha, other.start_sha) && - shas_equal?(head_sha, other.head_sha) + Git.shas_eql?(base_sha, other.base_sha) && + Git.shas_eql?(start_sha, other.start_sha) && + Git.shas_eql?(head_sha, other.head_sha) end alias_method :eql?, :== @@ -47,22 +47,6 @@ module Gitlab CompareService.new(project, head_sha).execute(project, start_sha, straight: straight) end end - - private - - def shas_equal?(sha1, sha2) - return true if sha1 == sha2 - return false if sha1.nil? || sha2.nil? - return false unless sha1.class == sha2.class - - length = [sha1.length, sha2.length].min - - # If either of the shas is below the minimum length, we cannot be sure - # that they actually refer to the same commit because of hash collision. - return false if length < Commit::MIN_SHA_LENGTH - - sha1[0, length] == sha2[0, length] - end end end end diff --git a/lib/gitlab/diff/inline_diff.rb b/lib/gitlab/diff/inline_diff.rb index 2d7b57120a6..54783a07919 100644 --- a/lib/gitlab/diff/inline_diff.rb +++ b/lib/gitlab/diff/inline_diff.rb @@ -70,7 +70,7 @@ module Gitlab def find_changed_line_pairs(lines) # Prefixes of all diff lines, indicating their types # For example: `" - + -+ ---+++ --+ -++"` - line_prefixes = lines.each_with_object("") { |line, s| s << line[0] }.gsub(/[^ +-]/, ' ') + line_prefixes = lines.each_with_object("") { |line, s| s << (line[0] || ' ') }.gsub(/[^ +-]/, ' ') changed_line_pairs = [] line_prefixes.scan(LINE_PAIRS_PATTERN) do 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/git.rb b/lib/gitlab/git.rb index 1f31cdbc96d..1f7c35cafaa 100644 --- a/lib/gitlab/git.rb +++ b/lib/gitlab/git.rb @@ -70,6 +70,18 @@ module Gitlab def diff_line_code(file_path, new_line_position, old_line_position) "#{Digest::SHA1.hexdigest(file_path)}_#{old_line_position}_#{new_line_position}" end + + def shas_eql?(sha1, sha2) + return false if sha1.nil? || sha2.nil? + return false unless sha1.class == sha2.class + + # If either of the shas is below the minimum length, we cannot be sure + # that they actually refer to the same commit because of hash collision. + length = [sha1.length, sha2.length].min + return false if length < Gitlab::Git::Commit::MIN_SHA_LENGTH + + sha1[0, length] == sha2[0, length] + end end end end diff --git a/lib/gitlab/git/commit.rb b/lib/gitlab/git/commit.rb index 8900e2d7afe..e90b158fb34 100644 --- a/lib/gitlab/git/commit.rb +++ b/lib/gitlab/git/commit.rb @@ -6,6 +6,7 @@ module Gitlab attr_accessor :raw_commit, :head + MIN_SHA_LENGTH = 7 SERIALIZE_KEYS = [ :id, :message, :parent_ids, :authored_date, :author_name, :author_email, diff --git a/lib/gitlab/git/operation_service.rb b/lib/gitlab/git/operation_service.rb index e36d5410431..7e8fe173056 100644 --- a/lib/gitlab/git/operation_service.rb +++ b/lib/gitlab/git/operation_service.rb @@ -83,7 +83,7 @@ module Gitlab Gitlab::Git.check_namespace!(start_repository) start_repository = RemoteRepository.new(start_repository) unless start_repository.is_a?(RemoteRepository) - start_branch_name = nil if start_repository.empty_repo? + start_branch_name = nil if start_repository.empty? if start_branch_name && !start_repository.branch_exists?(start_branch_name) raise ArgumentError, "Cannot find branch #{start_branch_name} in #{start_repository.relative_path}" diff --git a/lib/gitlab/git/remote_repository.rb b/lib/gitlab/git/remote_repository.rb index 3685aa20669..6bd6e58feeb 100644 --- a/lib/gitlab/git/remote_repository.rb +++ b/lib/gitlab/git/remote_repository.rb @@ -24,10 +24,12 @@ module Gitlab @path = repository.path end - def empty_repo? + def empty? # We will override this implementation in gitaly-ruby because we cannot # use '@repository' there. - @repository.empty_repo? + # + # Caches and memoization used on the Rails side + !@repository.exists? || @repository.empty? end def commit_id(revision) diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb index cb787cff7b0..fbdd646ea13 100644 --- a/lib/gitlab/git/repository.rb +++ b/lib/gitlab/git/repository.rb @@ -76,9 +76,6 @@ module Gitlab @attributes = Gitlab::Git::Attributes.new(path) end - delegate :empty?, - to: :rugged - def ==(other) path == other.path end @@ -207,6 +204,13 @@ module Gitlab end end + # Git repository can contains some hidden refs like: + # /refs/notes/* + # /refs/git-as-svn/* + # /refs/pulls/* + # This refs by default not visible in project page and not cloned to client side. + alias_method :has_visible_content?, :has_local_branches? + def has_local_branches_rugged? rugged.branches.each(:local).any? do |ref| begin @@ -1008,7 +1012,7 @@ module Gitlab Gitlab::Git.check_namespace!(start_repository) start_repository = RemoteRepository.new(start_repository) unless start_repository.is_a?(RemoteRepository) - return yield nil if start_repository.empty_repo? + return yield nil if start_repository.empty? if start_repository.same_repository?(self) yield commit(start_branch_name) @@ -1124,24 +1128,8 @@ module Gitlab Gitlab::Git::Commit.find(self, ref) end - # Refactoring aid; allows us to copy code from app/models/repository.rb - def empty_repo? - !exists? || !has_visible_content? - end - - # - # Git repository can contains some hidden refs like: - # /refs/notes/* - # /refs/git-as-svn/* - # /refs/pulls/* - # This refs by default not visible in project page and not cloned to client side. - # - # This method return true if repository contains some content visible in project page. - # - def has_visible_content? - return @has_visible_content if defined?(@has_visible_content) - - @has_visible_content = has_local_branches? + def empty? + !has_visible_content? end def fetch_repository_as_mirror(repository) @@ -1188,9 +1176,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:) @@ -1338,6 +1332,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 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 8998c4b1a83..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) @@ -166,7 +163,7 @@ module Gitlab end if Gitlab::Database.read_only? - raise UnauthorizedError, ERROR_MESSAGES[:cannot_push_to_read_only] + raise UnauthorizedError, push_to_read_only_message end if deploy_key @@ -280,5 +277,9 @@ module Gitlab UserAccess.new(user, project: project) end end + + def push_to_read_only_message + ERROR_MESSAGES[:cannot_push_to_read_only] + end end end diff --git a/lib/gitlab/git_access_wiki.rb b/lib/gitlab/git_access_wiki.rb index 98f1f45b338..1c9477e84b2 100644 --- a/lib/gitlab/git_access_wiki.rb +++ b/lib/gitlab/git_access_wiki.rb @@ -19,10 +19,14 @@ module Gitlab end if Gitlab::Database.read_only? - raise UnauthorizedError, ERROR_MESSAGES[:read_only] + raise UnauthorizedError, push_to_read_only_message end true end + + def push_to_read_only_message + ERROR_MESSAGES[:read_only] + end end end diff --git a/lib/gitlab/gitaly_client.rb b/lib/gitlab/gitaly_client.rb index f27cd800bdd..b753ac46291 100644 --- a/lib/gitlab/gitaly_client.rb +++ b/lib/gitlab/gitaly_client.rb @@ -27,7 +27,7 @@ module Gitlab end SERVER_VERSION_FILE = 'GITALY_SERVER_VERSION'.freeze - MAXIMUM_GITALY_CALLS = 30 + MAXIMUM_GITALY_CALLS = 35 CLIENT_NAME = (Sidekiq.server? ? 'gitlab-sidekiq' : 'gitlab-web').freeze MUTEX = Mutex.new @@ -336,6 +336,12 @@ module Gitlab s.dup.force_encoding(Encoding::ASCII_8BIT) end + def self.binary_stringio(s) + io = StringIO.new(s || '') + io.set_encoding(Encoding::ASCII_8BIT) + io + end + def self.encode_repeated(a) Google::Protobuf::RepeatedField.new(:bytes, a.map { |s| self.encode(s) } ) end diff --git a/lib/gitlab/gitaly_client/repository_service.rb b/lib/gitlab/gitaly_client/repository_service.rb index b9e606592d7..a477d618f63 100644 --- a/lib/gitlab/gitaly_client/repository_service.rb +++ b/lib/gitlab/gitaly_client/repository_service.rb @@ -87,6 +87,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/gitaly_client/wiki_service.rb b/lib/gitlab/gitaly_client/wiki_service.rb index c8f065f5881..337d225d081 100644 --- a/lib/gitlab/gitaly_client/wiki_service.rb +++ b/lib/gitlab/gitaly_client/wiki_service.rb @@ -18,12 +18,11 @@ module Gitlab commit_details: gitaly_commit_details(commit_details) ) - strio = StringIO.new(content) + strio = GitalyClient.binary_stringio(content) enum = Enumerator.new do |y| until strio.eof? - chunk = strio.read(MAX_MSG_SIZE) - request.content = GitalyClient.encode(chunk) + request.content = strio.read(MAX_MSG_SIZE) y.yield request @@ -46,12 +45,11 @@ module Gitlab commit_details: gitaly_commit_details(commit_details) ) - strio = StringIO.new(content) + strio = GitalyClient.binary_stringio(content) enum = Enumerator.new do |y| until strio.eof? - chunk = strio.read(MAX_MSG_SIZE) - request.content = GitalyClient.encode(chunk) + request.content = strio.read(MAX_MSG_SIZE) y.yield request 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/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 436a9e9550d..b68800417a2 100644 --- a/lib/gitlab/metrics/samplers/ruby_sampler.rb +++ b/lib/gitlab/metrics/samplers/ruby_sampler.rb @@ -32,7 +32,7 @@ module Gitlab def init_metrics metrics = {} - metrics[:sampler_duration] = Metrics.histogram(with_prefix(:sampler_duration, :seconds), 'Sampler time', {}) + metrics[:sampler_duration] = Metrics.histogram(with_prefix(:sampler_duration, :seconds), 'Sampler time', { worker: nil }) metrics[:total_time] = Metrics.gauge(with_prefix(:gc, :time_total), 'Total GC time', labels, :livesum) GC.stat.keys.each do |key| metrics[key] = Metrics.gauge(with_prefix(:gc, key), to_doc_string(key), labels, :livesum) @@ -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,41 +67,15 @@ 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) worker_no = ::Prometheus::Client::Support::Unicorn.worker_id if worker_no - { unicorn: worker_no } + { worker: worker_no } else - { unicorn: 'master' } + { worker: 'master' } end end end diff --git a/lib/gitlab/reference_extractor.rb b/lib/gitlab/reference_extractor.rb index bc836dcc08d..9ff82d628c0 100644 --- a/lib/gitlab/reference_extractor.rb +++ b/lib/gitlab/reference_extractor.rb @@ -1,7 +1,7 @@ module Gitlab # Extract possible GFM references from an arbitrary String for further processing. class ReferenceExtractor < Banzai::ReferenceExtractor - REFERABLES = %i(user issue label milestone merge_request snippet commit commit_range directly_addressed_user).freeze + REFERABLES = %i(user issue label milestone merge_request snippet commit commit_range directly_addressed_user epic).freeze attr_accessor :project, :current_user, :author def initialize(project, current_user = nil) 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/utils.rb b/lib/gitlab/utils.rb index abb3d3a02c3..b3baaf036d8 100644 --- a/lib/gitlab/utils.rb +++ b/lib/gitlab/utils.rb @@ -46,5 +46,22 @@ module Gitlab def random_string Random.rand(Float::MAX.to_i).to_s(36) end + + # See: http://stackoverflow.com/questions/2108727/which-in-ruby-checking-if-program-exists-in-path-from-ruby + # Cross-platform way of finding an executable in the $PATH. + # + # which('ruby') #=> /usr/bin/ruby + def which(cmd, env = ENV) + exts = env['PATHEXT'] ? env['PATHEXT'].split(';') : [''] + + env['PATH'].split(File::PATH_SEPARATOR).each do |path| + exts.each do |ext| + exe = File.join(path, "#{cmd}#{ext}") + return exe if File.executable?(exe) && !File.directory?(exe) + end + end + + nil + end end end diff --git a/locale/bg/gitlab.po b/locale/bg/gitlab.po index 8e688dede89..374164cbe65 100644 --- a/locale/bg/gitlab.po +++ b/locale/bg/gitlab.po @@ -504,13 +504,13 @@ msgstr "" msgid "ClusterIntegration|Cluster integration is enabled for this project. Disabling this integration will not affect your cluster, it will only temporarily turn off GitLab's connection to it." msgstr "" -msgid "ClusterIntegration|Cluster is being created on Google Container Engine..." +msgid "ClusterIntegration|Cluster is being created on Google Kubernetes Engine..." msgstr "" msgid "ClusterIntegration|Cluster name" msgstr "" -msgid "ClusterIntegration|Cluster was successfully created on Google Container Engine" +msgid "ClusterIntegration|Cluster was successfully created on Google Kubernetes Engine" msgstr "" msgid "ClusterIntegration|Copy cluster name" @@ -519,7 +519,7 @@ msgstr "" msgid "ClusterIntegration|Create cluster" msgstr "" -msgid "ClusterIntegration|Create new cluster on Google Container Engine" +msgid "ClusterIntegration|Create new cluster on Google Kubernetes Engine" msgstr "" msgid "ClusterIntegration|Enable cluster integration" @@ -528,10 +528,10 @@ msgstr "" msgid "ClusterIntegration|Google Cloud Platform project ID" msgstr "" -msgid "ClusterIntegration|Google Container Engine" +msgid "ClusterIntegration|Google Kubernetes Engine" msgstr "" -msgid "ClusterIntegration|Google Container Engine project" +msgid "ClusterIntegration|Google Kubernetes Engine project" msgstr "" msgid "ClusterIntegration|Learn more about %{link_to_documentation}" @@ -585,7 +585,7 @@ msgstr "" msgid "ClusterIntegration|Something went wrong on our end." msgstr "" -msgid "ClusterIntegration|Something went wrong while creating your cluster on Google Container Engine" +msgid "ClusterIntegration|Something went wrong while creating your cluster on Google Kubernetes Engine" msgstr "" msgid "ClusterIntegration|Toggle Cluster" @@ -594,13 +594,13 @@ msgstr "" msgid "ClusterIntegration|With a cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way." msgstr "" -msgid "ClusterIntegration|Your account must have %{link_to_container_engine}" +msgid "ClusterIntegration|Your account must have %{link_to_kubernetes_engine}" msgstr "" msgid "ClusterIntegration|Zone" msgstr "" -msgid "ClusterIntegration|access to Google Container Engine" +msgid "ClusterIntegration|access to Google Kubernetes Engine" msgstr "" msgid "ClusterIntegration|cluster" diff --git a/locale/de/gitlab.po b/locale/de/gitlab.po index ef730d91c75..a79a7d1a353 100644 --- a/locale/de/gitlab.po +++ b/locale/de/gitlab.po @@ -504,13 +504,13 @@ msgstr "" msgid "ClusterIntegration|Cluster integration is enabled for this project. Disabling this integration will not affect your cluster, it will only temporarily turn off GitLab's connection to it." msgstr "" -msgid "ClusterIntegration|Cluster is being created on Google Container Engine..." +msgid "ClusterIntegration|Cluster is being created on Google Kubernetes Engine..." msgstr "" msgid "ClusterIntegration|Cluster name" msgstr "" -msgid "ClusterIntegration|Cluster was successfully created on Google Container Engine" +msgid "ClusterIntegration|Cluster was successfully created on Google Kubernetes Engine" msgstr "" msgid "ClusterIntegration|Copy cluster name" @@ -519,7 +519,7 @@ msgstr "" msgid "ClusterIntegration|Create cluster" msgstr "" -msgid "ClusterIntegration|Create new cluster on Google Container Engine" +msgid "ClusterIntegration|Create new cluster on Google Kubernetes Engine" msgstr "" msgid "ClusterIntegration|Enable cluster integration" @@ -528,10 +528,10 @@ msgstr "" msgid "ClusterIntegration|Google Cloud Platform project ID" msgstr "" -msgid "ClusterIntegration|Google Container Engine" +msgid "ClusterIntegration|Google Kubernetes Engine" msgstr "" -msgid "ClusterIntegration|Google Container Engine project" +msgid "ClusterIntegration|Google Kubernetes Engine project" msgstr "" msgid "ClusterIntegration|Learn more about %{link_to_documentation}" @@ -585,7 +585,7 @@ msgstr "" msgid "ClusterIntegration|Something went wrong on our end." msgstr "" -msgid "ClusterIntegration|Something went wrong while creating your cluster on Google Container Engine" +msgid "ClusterIntegration|Something went wrong while creating your cluster on Google Kubernetes Engine" msgstr "" msgid "ClusterIntegration|Toggle Cluster" @@ -594,13 +594,13 @@ msgstr "" msgid "ClusterIntegration|With a cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way." msgstr "" -msgid "ClusterIntegration|Your account must have %{link_to_container_engine}" +msgid "ClusterIntegration|Your account must have %{link_to_kubernetes_engine}" msgstr "" msgid "ClusterIntegration|Zone" msgstr "" -msgid "ClusterIntegration|access to Google Container Engine" +msgid "ClusterIntegration|access to Google Kubernetes Engine" msgstr "" msgid "ClusterIntegration|cluster" diff --git a/locale/eo/gitlab.po b/locale/eo/gitlab.po index 0a1b379b3d3..f7be343c4e1 100644 --- a/locale/eo/gitlab.po +++ b/locale/eo/gitlab.po @@ -504,13 +504,13 @@ msgstr "" msgid "ClusterIntegration|Cluster integration is enabled for this project. Disabling this integration will not affect your cluster, it will only temporarily turn off GitLab's connection to it." msgstr "" -msgid "ClusterIntegration|Cluster is being created on Google Container Engine..." +msgid "ClusterIntegration|Cluster is being created on Google Kubernetes Engine..." msgstr "" msgid "ClusterIntegration|Cluster name" msgstr "" -msgid "ClusterIntegration|Cluster was successfully created on Google Container Engine" +msgid "ClusterIntegration|Cluster was successfully created on Google Kubernetes Engine" msgstr "" msgid "ClusterIntegration|Copy cluster name" @@ -519,7 +519,7 @@ msgstr "" msgid "ClusterIntegration|Create cluster" msgstr "" -msgid "ClusterIntegration|Create new cluster on Google Container Engine" +msgid "ClusterIntegration|Create new cluster on Google Kubernetes Engine" msgstr "" msgid "ClusterIntegration|Enable cluster integration" @@ -528,10 +528,10 @@ msgstr "" msgid "ClusterIntegration|Google Cloud Platform project ID" msgstr "" -msgid "ClusterIntegration|Google Container Engine" +msgid "ClusterIntegration|Google Kubernetes Engine" msgstr "" -msgid "ClusterIntegration|Google Container Engine project" +msgid "ClusterIntegration|Google Kubernetes Engine project" msgstr "" msgid "ClusterIntegration|Learn more about %{link_to_documentation}" @@ -585,7 +585,7 @@ msgstr "" msgid "ClusterIntegration|Something went wrong on our end." msgstr "" -msgid "ClusterIntegration|Something went wrong while creating your cluster on Google Container Engine" +msgid "ClusterIntegration|Something went wrong while creating your cluster on Google Kubernetes Engine" msgstr "" msgid "ClusterIntegration|Toggle Cluster" @@ -594,13 +594,13 @@ msgstr "" msgid "ClusterIntegration|With a cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way." msgstr "" -msgid "ClusterIntegration|Your account must have %{link_to_container_engine}" +msgid "ClusterIntegration|Your account must have %{link_to_kubernetes_engine}" msgstr "" msgid "ClusterIntegration|Zone" msgstr "" -msgid "ClusterIntegration|access to Google Container Engine" +msgid "ClusterIntegration|access to Google Kubernetes Engine" msgstr "" msgid "ClusterIntegration|cluster" diff --git a/locale/es/gitlab.po b/locale/es/gitlab.po index 31ff4e08592..c35a3503019 100644 --- a/locale/es/gitlab.po +++ b/locale/es/gitlab.po @@ -504,13 +504,13 @@ msgstr "" msgid "ClusterIntegration|Cluster integration is enabled for this project. Disabling this integration will not affect your cluster, it will only temporarily turn off GitLab's connection to it." msgstr "" -msgid "ClusterIntegration|Cluster is being created on Google Container Engine..." +msgid "ClusterIntegration|Cluster is being created on Google Kubernetes Engine..." msgstr "" msgid "ClusterIntegration|Cluster name" msgstr "" -msgid "ClusterIntegration|Cluster was successfully created on Google Container Engine" +msgid "ClusterIntegration|Cluster was successfully created on Google Kubernetes Engine" msgstr "" msgid "ClusterIntegration|Copy cluster name" @@ -519,7 +519,7 @@ msgstr "" msgid "ClusterIntegration|Create cluster" msgstr "" -msgid "ClusterIntegration|Create new cluster on Google Container Engine" +msgid "ClusterIntegration|Create new cluster on Google Kubernetes Engine" msgstr "" msgid "ClusterIntegration|Enable cluster integration" @@ -528,10 +528,10 @@ msgstr "" msgid "ClusterIntegration|Google Cloud Platform project ID" msgstr "" -msgid "ClusterIntegration|Google Container Engine" +msgid "ClusterIntegration|Google Kubernetes Engine" msgstr "" -msgid "ClusterIntegration|Google Container Engine project" +msgid "ClusterIntegration|Google Kubernetes Engine project" msgstr "" msgid "ClusterIntegration|Learn more about %{link_to_documentation}" @@ -585,7 +585,7 @@ msgstr "" msgid "ClusterIntegration|Something went wrong on our end." msgstr "" -msgid "ClusterIntegration|Something went wrong while creating your cluster on Google Container Engine" +msgid "ClusterIntegration|Something went wrong while creating your cluster on Google Kubernetes Engine" msgstr "" msgid "ClusterIntegration|Toggle Cluster" @@ -594,13 +594,13 @@ msgstr "" msgid "ClusterIntegration|With a cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way." msgstr "" -msgid "ClusterIntegration|Your account must have %{link_to_container_engine}" +msgid "ClusterIntegration|Your account must have %{link_to_kubernetes_engine}" msgstr "" msgid "ClusterIntegration|Zone" msgstr "" -msgid "ClusterIntegration|access to Google Container Engine" +msgid "ClusterIntegration|access to Google Kubernetes Engine" msgstr "" msgid "ClusterIntegration|cluster" diff --git a/locale/fr/gitlab.po b/locale/fr/gitlab.po index 41fa86451f5..a0e523339db 100644 --- a/locale/fr/gitlab.po +++ b/locale/fr/gitlab.po @@ -504,14 +504,14 @@ msgstr "L'intégration du cluster est activée pour ce projet." msgid "ClusterIntegration|Cluster integration is enabled for this project. Disabling this integration will not affect your cluster, it will only temporarily turn off GitLab's connection to it." msgstr "L'intégration de cluster est activée pour ce projet. La désactivation de cette intégration n’affectera pas votre cluster, il coupera temporairement la connexion de GitLab à celui-ci." -msgid "ClusterIntegration|Cluster is being created on Google Container Engine..." -msgstr "Le cluster est en cours de création sur Google Container Engine…" +msgid "ClusterIntegration|Cluster is being created on Google Kubernetes Engine..." +msgstr "Le cluster est en cours de création sur Google Kubernetes Engine…" msgid "ClusterIntegration|Cluster name" msgstr "Nom du cluster" -msgid "ClusterIntegration|Cluster was successfully created on Google Container Engine" -msgstr "Le cluster a été correctement créé sur Google Container Engine" +msgid "ClusterIntegration|Cluster was successfully created on Google Kubernetes Engine" +msgstr "Le cluster a été correctement créé sur Google Kubernetes Engine" msgid "ClusterIntegration|Copy cluster name" msgstr "Copier le nom du cluster" @@ -519,8 +519,8 @@ msgstr "Copier le nom du cluster" msgid "ClusterIntegration|Create cluster" msgstr "Créer le cluster" -msgid "ClusterIntegration|Create new cluster on Google Container Engine" -msgstr "Créer un nouveau cluster sur Google Container Engine" +msgid "ClusterIntegration|Create new cluster on Google Kubernetes Engine" +msgstr "Créer un nouveau cluster sur Google Kubernetes Engine" msgid "ClusterIntegration|Enable cluster integration" msgstr "Activer l’intégration du cluster" @@ -528,11 +528,11 @@ msgstr "Activer l’intégration du cluster" msgid "ClusterIntegration|Google Cloud Platform project ID" msgstr "ID de projet Google Cloud Platform" -msgid "ClusterIntegration|Google Container Engine" -msgstr "Google Container Engine" +msgid "ClusterIntegration|Google Kubernetes Engine" +msgstr "Google Kubernetes Engine" -msgid "ClusterIntegration|Google Container Engine project" -msgstr "Google Container Engine" +msgid "ClusterIntegration|Google Kubernetes Engine project" +msgstr "Google Kubernetes Engine" msgid "ClusterIntegration|Learn more about %{link_to_documentation}" msgstr "En savoir plus sur %{link_to_documentation}" @@ -585,8 +585,8 @@ msgstr "Voir les zones" msgid "ClusterIntegration|Something went wrong on our end." msgstr "Un problème est survenu de notre côté." -msgid "ClusterIntegration|Something went wrong while creating your cluster on Google Container Engine" -msgstr "Un problème est survenu lors de la création de votre cluster sur Google Container Engine." +msgid "ClusterIntegration|Something went wrong while creating your cluster on Google Kubernetes Engine" +msgstr "Un problème est survenu lors de la création de votre cluster sur Google Kubernetes Engine." msgid "ClusterIntegration|Toggle Cluster" msgstr "Activer/désactiver le cluster" @@ -594,14 +594,14 @@ msgstr "Activer/désactiver le cluster" msgid "ClusterIntegration|With a cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way." msgstr "Avec un cluster associé à ce projet, vous pouvez utiliser des applications de revue, déployer vos applications, exécuter vos pipelines et bien plus encore, de manière très simple." -msgid "ClusterIntegration|Your account must have %{link_to_container_engine}" -msgstr "Votre compte doit disposer de %{link_to_container_engine}" +msgid "ClusterIntegration|Your account must have %{link_to_kubernetes_engine}" +msgstr "Votre compte doit disposer de %{link_to_kubernetes_engine}" msgid "ClusterIntegration|Zone" msgstr "Zone" -msgid "ClusterIntegration|access to Google Container Engine" -msgstr "Accéder à Google Container Engine" +msgid "ClusterIntegration|access to Google Kubernetes Engine" +msgstr "Accéder à Google Kubernetes Engine" msgid "ClusterIntegration|cluster" msgstr "cluster" diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 2220cc72502..3ebc7859232 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -494,13 +494,13 @@ msgstr "" msgid "ClusterIntegration|Cluster integration is enabled for this project. Disabling this integration will not affect your cluster, it will only temporarily turn off GitLab's connection to it." msgstr "" -msgid "ClusterIntegration|Cluster is being created on Google Container Engine..." +msgid "ClusterIntegration|Cluster is being created on Google Kubernetes Engine..." msgstr "" msgid "ClusterIntegration|Cluster name" msgstr "" -msgid "ClusterIntegration|Cluster was successfully created on Google Container Engine. Refresh the page to see cluster's details" +msgid "ClusterIntegration|Cluster was successfully created on Google Kubernetes Engine. Refresh the page to see cluster's details" msgstr "" msgid "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}" @@ -524,7 +524,7 @@ msgstr "" msgid "ClusterIntegration|Create cluster" msgstr "" -msgid "ClusterIntegration|Create cluster on Google Container Engine" +msgid "ClusterIntegration|Create cluster on Google Kubernetes Engine" msgstr "" msgid "ClusterIntegration|Create on GKE" @@ -551,10 +551,10 @@ msgstr "" msgid "ClusterIntegration|Google Cloud Platform project ID" msgstr "" -msgid "ClusterIntegration|Google Container Engine" +msgid "ClusterIntegration|Google Kubernetes Engine" msgstr "" -msgid "ClusterIntegration|Google Container Engine project" +msgid "ClusterIntegration|Google Kubernetes Engine project" msgstr "" msgid "ClusterIntegration|Helm Tiller" @@ -638,7 +638,7 @@ msgstr "" msgid "ClusterIntegration|Remove integration" msgstr "" -msgid "ClusterIntegration|Removing cluster integration will remove the cluster configuration you have added to this project. It will not delete your cluster on Google Container Engine." +msgid "ClusterIntegration|Removing cluster integration will remove the cluster configuration you have added to this project. It will not delete your cluster on Google Kubernetes Engine." msgstr "" msgid "ClusterIntegration|Request to begin installing failed" @@ -668,7 +668,7 @@ msgstr "" msgid "ClusterIntegration|Something went wrong on our end." msgstr "" -msgid "ClusterIntegration|Something went wrong while creating your cluster on Google Container Engine" +msgid "ClusterIntegration|Something went wrong while creating your cluster on Google Kubernetes Engine" msgstr "" msgid "ClusterIntegration|Something went wrong while installing %{title}" @@ -689,13 +689,13 @@ msgstr "" msgid "ClusterIntegration|With a cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way." msgstr "" -msgid "ClusterIntegration|Your account must have %{link_to_container_engine}" +msgid "ClusterIntegration|Your account must have %{link_to_kubernetes_engine}" msgstr "" msgid "ClusterIntegration|Zone" msgstr "" -msgid "ClusterIntegration|access to Google Container Engine" +msgid "ClusterIntegration|access to Google Kubernetes Engine" msgstr "" msgid "ClusterIntegration|cluster" diff --git a/locale/it/gitlab.po b/locale/it/gitlab.po index 7a132aeb238..8a987129452 100644 --- a/locale/it/gitlab.po +++ b/locale/it/gitlab.po @@ -504,13 +504,13 @@ msgstr "" msgid "ClusterIntegration|Cluster integration is enabled for this project. Disabling this integration will not affect your cluster, it will only temporarily turn off GitLab's connection to it." msgstr "" -msgid "ClusterIntegration|Cluster is being created on Google Container Engine..." +msgid "ClusterIntegration|Cluster is being created on Google Kubernetes Engine..." msgstr "" msgid "ClusterIntegration|Cluster name" msgstr "" -msgid "ClusterIntegration|Cluster was successfully created on Google Container Engine" +msgid "ClusterIntegration|Cluster was successfully created on Google Kubernetes Engine" msgstr "" msgid "ClusterIntegration|Copy cluster name" @@ -519,7 +519,7 @@ msgstr "" msgid "ClusterIntegration|Create cluster" msgstr "" -msgid "ClusterIntegration|Create new cluster on Google Container Engine" +msgid "ClusterIntegration|Create new cluster on Google Kubernetes Engine" msgstr "" msgid "ClusterIntegration|Enable cluster integration" @@ -528,10 +528,10 @@ msgstr "" msgid "ClusterIntegration|Google Cloud Platform project ID" msgstr "" -msgid "ClusterIntegration|Google Container Engine" +msgid "ClusterIntegration|Google Kubernetes Engine" msgstr "" -msgid "ClusterIntegration|Google Container Engine project" +msgid "ClusterIntegration|Google Kubernetes Engine project" msgstr "" msgid "ClusterIntegration|Learn more about %{link_to_documentation}" @@ -585,7 +585,7 @@ msgstr "" msgid "ClusterIntegration|Something went wrong on our end." msgstr "" -msgid "ClusterIntegration|Something went wrong while creating your cluster on Google Container Engine" +msgid "ClusterIntegration|Something went wrong while creating your cluster on Google Kubernetes Engine" msgstr "" msgid "ClusterIntegration|Toggle Cluster" @@ -594,13 +594,13 @@ msgstr "" msgid "ClusterIntegration|With a cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way." msgstr "" -msgid "ClusterIntegration|Your account must have %{link_to_container_engine}" +msgid "ClusterIntegration|Your account must have %{link_to_kubernetes_engine}" msgstr "" msgid "ClusterIntegration|Zone" msgstr "" -msgid "ClusterIntegration|access to Google Container Engine" +msgid "ClusterIntegration|access to Google Kubernetes Engine" msgstr "" msgid "ClusterIntegration|cluster" diff --git a/locale/ja/gitlab.po b/locale/ja/gitlab.po index 9045ae26c4a..8d93a936be9 100644 --- a/locale/ja/gitlab.po +++ b/locale/ja/gitlab.po @@ -497,13 +497,13 @@ msgstr "" msgid "ClusterIntegration|Cluster integration is enabled for this project. Disabling this integration will not affect your cluster, it will only temporarily turn off GitLab's connection to it." msgstr "" -msgid "ClusterIntegration|Cluster is being created on Google Container Engine..." +msgid "ClusterIntegration|Cluster is being created on Google Kubernetes Engine..." msgstr "" msgid "ClusterIntegration|Cluster name" msgstr "" -msgid "ClusterIntegration|Cluster was successfully created on Google Container Engine" +msgid "ClusterIntegration|Cluster was successfully created on Google Kubernetes Engine" msgstr "" msgid "ClusterIntegration|Copy cluster name" @@ -512,7 +512,7 @@ msgstr "" msgid "ClusterIntegration|Create cluster" msgstr "" -msgid "ClusterIntegration|Create new cluster on Google Container Engine" +msgid "ClusterIntegration|Create new cluster on Google Kubernetes Engine" msgstr "" msgid "ClusterIntegration|Enable cluster integration" @@ -521,10 +521,10 @@ msgstr "" msgid "ClusterIntegration|Google Cloud Platform project ID" msgstr "" -msgid "ClusterIntegration|Google Container Engine" +msgid "ClusterIntegration|Google Kubernetes Engine" msgstr "" -msgid "ClusterIntegration|Google Container Engine project" +msgid "ClusterIntegration|Google Kubernetes Engine project" msgstr "" msgid "ClusterIntegration|Learn more about %{link_to_documentation}" @@ -578,7 +578,7 @@ msgstr "" msgid "ClusterIntegration|Something went wrong on our end." msgstr "" -msgid "ClusterIntegration|Something went wrong while creating your cluster on Google Container Engine" +msgid "ClusterIntegration|Something went wrong while creating your cluster on Google Kubernetes Engine" msgstr "" msgid "ClusterIntegration|Toggle Cluster" @@ -587,13 +587,13 @@ msgstr "" msgid "ClusterIntegration|With a cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way." msgstr "" -msgid "ClusterIntegration|Your account must have %{link_to_container_engine}" +msgid "ClusterIntegration|Your account must have %{link_to_kubernetes_engine}" msgstr "" msgid "ClusterIntegration|Zone" msgstr "" -msgid "ClusterIntegration|access to Google Container Engine" +msgid "ClusterIntegration|access to Google Kubernetes Engine" msgstr "" msgid "ClusterIntegration|cluster" diff --git a/locale/ko/gitlab.po b/locale/ko/gitlab.po index ab74d4cbeae..d6c1ff2deeb 100644 --- a/locale/ko/gitlab.po +++ b/locale/ko/gitlab.po @@ -497,13 +497,13 @@ msgstr "" msgid "ClusterIntegration|Cluster integration is enabled for this project. Disabling this integration will not affect your cluster, it will only temporarily turn off GitLab's connection to it." msgstr "" -msgid "ClusterIntegration|Cluster is being created on Google Container Engine..." +msgid "ClusterIntegration|Cluster is being created on Google Kubernetes Engine..." msgstr "" msgid "ClusterIntegration|Cluster name" msgstr "" -msgid "ClusterIntegration|Cluster was successfully created on Google Container Engine" +msgid "ClusterIntegration|Cluster was successfully created on Google Kubernetes Engine" msgstr "" msgid "ClusterIntegration|Copy cluster name" @@ -512,7 +512,7 @@ msgstr "" msgid "ClusterIntegration|Create cluster" msgstr "" -msgid "ClusterIntegration|Create new cluster on Google Container Engine" +msgid "ClusterIntegration|Create new cluster on Google Kubernetes Engine" msgstr "" msgid "ClusterIntegration|Enable cluster integration" @@ -521,10 +521,10 @@ msgstr "" msgid "ClusterIntegration|Google Cloud Platform project ID" msgstr "" -msgid "ClusterIntegration|Google Container Engine" +msgid "ClusterIntegration|Google Kubernetes Engine" msgstr "" -msgid "ClusterIntegration|Google Container Engine project" +msgid "ClusterIntegration|Google Kubernetes Engine project" msgstr "" msgid "ClusterIntegration|Learn more about %{link_to_documentation}" @@ -578,7 +578,7 @@ msgstr "" msgid "ClusterIntegration|Something went wrong on our end." msgstr "" -msgid "ClusterIntegration|Something went wrong while creating your cluster on Google Container Engine" +msgid "ClusterIntegration|Something went wrong while creating your cluster on Google Kubernetes Engine" msgstr "" msgid "ClusterIntegration|Toggle Cluster" @@ -587,13 +587,13 @@ msgstr "" msgid "ClusterIntegration|With a cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way." msgstr "" -msgid "ClusterIntegration|Your account must have %{link_to_container_engine}" +msgid "ClusterIntegration|Your account must have %{link_to_kubernetes_engine}" msgstr "" msgid "ClusterIntegration|Zone" msgstr "" -msgid "ClusterIntegration|access to Google Container Engine" +msgid "ClusterIntegration|access to Google Kubernetes Engine" msgstr "" msgid "ClusterIntegration|cluster" diff --git a/locale/nl_NL/gitlab.po b/locale/nl_NL/gitlab.po index 7e33af9f747..68d1f809bb4 100644 --- a/locale/nl_NL/gitlab.po +++ b/locale/nl_NL/gitlab.po @@ -504,13 +504,13 @@ msgstr "" msgid "ClusterIntegration|Cluster integration is enabled for this project. Disabling this integration will not affect your cluster, it will only temporarily turn off GitLab's connection to it." msgstr "" -msgid "ClusterIntegration|Cluster is being created on Google Container Engine..." +msgid "ClusterIntegration|Cluster is being created on Google Kubernetes Engine..." msgstr "" msgid "ClusterIntegration|Cluster name" msgstr "" -msgid "ClusterIntegration|Cluster was successfully created on Google Container Engine" +msgid "ClusterIntegration|Cluster was successfully created on Google Kubernetes Engine" msgstr "" msgid "ClusterIntegration|Copy cluster name" @@ -519,7 +519,7 @@ msgstr "" msgid "ClusterIntegration|Create cluster" msgstr "" -msgid "ClusterIntegration|Create new cluster on Google Container Engine" +msgid "ClusterIntegration|Create new cluster on Google Kubernetes Engine" msgstr "" msgid "ClusterIntegration|Enable cluster integration" @@ -528,10 +528,10 @@ msgstr "" msgid "ClusterIntegration|Google Cloud Platform project ID" msgstr "" -msgid "ClusterIntegration|Google Container Engine" +msgid "ClusterIntegration|Google Kubernetes Engine" msgstr "" -msgid "ClusterIntegration|Google Container Engine project" +msgid "ClusterIntegration|Google Kubernetes Engine project" msgstr "" msgid "ClusterIntegration|Learn more about %{link_to_documentation}" @@ -585,7 +585,7 @@ msgstr "" msgid "ClusterIntegration|Something went wrong on our end." msgstr "" -msgid "ClusterIntegration|Something went wrong while creating your cluster on Google Container Engine" +msgid "ClusterIntegration|Something went wrong while creating your cluster on Google Kubernetes Engine" msgstr "" msgid "ClusterIntegration|Toggle Cluster" @@ -594,13 +594,13 @@ msgstr "" msgid "ClusterIntegration|With a cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way." msgstr "" -msgid "ClusterIntegration|Your account must have %{link_to_container_engine}" +msgid "ClusterIntegration|Your account must have %{link_to_kubernetes_engine}" msgstr "" msgid "ClusterIntegration|Zone" msgstr "" -msgid "ClusterIntegration|access to Google Container Engine" +msgid "ClusterIntegration|access to Google Kubernetes Engine" msgstr "" msgid "ClusterIntegration|cluster" diff --git a/locale/pl_PL/gitlab.po b/locale/pl_PL/gitlab.po index fb4a8af7217..c48909540b1 100644 --- a/locale/pl_PL/gitlab.po +++ b/locale/pl_PL/gitlab.po @@ -511,13 +511,13 @@ msgstr "" msgid "ClusterIntegration|Cluster integration is enabled for this project. Disabling this integration will not affect your cluster, it will only temporarily turn off GitLab's connection to it." msgstr "" -msgid "ClusterIntegration|Cluster is being created on Google Container Engine..." +msgid "ClusterIntegration|Cluster is being created on Google Kubernetes Engine..." msgstr "" msgid "ClusterIntegration|Cluster name" msgstr "" -msgid "ClusterIntegration|Cluster was successfully created on Google Container Engine" +msgid "ClusterIntegration|Cluster was successfully created on Google Kubernetes Engine" msgstr "" msgid "ClusterIntegration|Copy cluster name" @@ -526,7 +526,7 @@ msgstr "" msgid "ClusterIntegration|Create cluster" msgstr "" -msgid "ClusterIntegration|Create new cluster on Google Container Engine" +msgid "ClusterIntegration|Create new cluster on Google Kubernetes Engine" msgstr "" msgid "ClusterIntegration|Enable cluster integration" @@ -535,10 +535,10 @@ msgstr "" msgid "ClusterIntegration|Google Cloud Platform project ID" msgstr "" -msgid "ClusterIntegration|Google Container Engine" +msgid "ClusterIntegration|Google Kubernetes Engine" msgstr "" -msgid "ClusterIntegration|Google Container Engine project" +msgid "ClusterIntegration|Google Kubernetes Engine project" msgstr "" msgid "ClusterIntegration|Learn more about %{link_to_documentation}" @@ -592,7 +592,7 @@ msgstr "" msgid "ClusterIntegration|Something went wrong on our end." msgstr "" -msgid "ClusterIntegration|Something went wrong while creating your cluster on Google Container Engine" +msgid "ClusterIntegration|Something went wrong while creating your cluster on Google Kubernetes Engine" msgstr "" msgid "ClusterIntegration|Toggle Cluster" @@ -601,13 +601,13 @@ msgstr "" msgid "ClusterIntegration|With a cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way." msgstr "" -msgid "ClusterIntegration|Your account must have %{link_to_container_engine}" +msgid "ClusterIntegration|Your account must have %{link_to_kubernetes_engine}" msgstr "" msgid "ClusterIntegration|Zone" msgstr "" -msgid "ClusterIntegration|access to Google Container Engine" +msgid "ClusterIntegration|access to Google Kubernetes Engine" msgstr "" msgid "ClusterIntegration|cluster" diff --git a/locale/pt_BR/gitlab.po b/locale/pt_BR/gitlab.po index fa335bc819d..78e0967c3bc 100644 --- a/locale/pt_BR/gitlab.po +++ b/locale/pt_BR/gitlab.po @@ -504,14 +504,14 @@ msgstr "Integração do cluster está ativada nesse projeto." msgid "ClusterIntegration|Cluster integration is enabled for this project. Disabling this integration will not affect your cluster, it will only temporarily turn off GitLab's connection to it." msgstr "Integração do cluster está ativada para esse projeto. Desabilitar a integração não afetará seu cluster, mas desligará temporariamente a conexão do Gitlab com ele." -msgid "ClusterIntegration|Cluster is being created on Google Container Engine..." -msgstr "O cluster está sendo criado no Google Container Engine..." +msgid "ClusterIntegration|Cluster is being created on Google Kubernetes Engine..." +msgstr "O cluster está sendo criado no Google Kubernetes Engine..." msgid "ClusterIntegration|Cluster name" msgstr "Nome do cluster" -msgid "ClusterIntegration|Cluster was successfully created on Google Container Engine" -msgstr "O cluster foi criado com sucesso no Google Container Engine" +msgid "ClusterIntegration|Cluster was successfully created on Google Kubernetes Engine" +msgstr "O cluster foi criado com sucesso no Google Kubernetes Engine" msgid "ClusterIntegration|Copy cluster name" msgstr "Copiar nome do cluster" @@ -519,8 +519,8 @@ msgstr "Copiar nome do cluster" msgid "ClusterIntegration|Create cluster" msgstr "Criar cluster" -msgid "ClusterIntegration|Create new cluster on Google Container Engine" -msgstr "Criar novo cluster no Google Container Engine" +msgid "ClusterIntegration|Create new cluster on Google Kubernetes Engine" +msgstr "Criar novo cluster no Google Kubernetes Engine" msgid "ClusterIntegration|Enable cluster integration" msgstr "Ativar integração com o cluster" @@ -528,11 +528,11 @@ msgstr "Ativar integração com o cluster" msgid "ClusterIntegration|Google Cloud Platform project ID" msgstr "ID do projeto no Google Cloud Platform" -msgid "ClusterIntegration|Google Container Engine" -msgstr "Google Container Engine" +msgid "ClusterIntegration|Google Kubernetes Engine" +msgstr "Google Kubernetes Engine" -msgid "ClusterIntegration|Google Container Engine project" -msgstr "Projeto no Google Container Engine" +msgid "ClusterIntegration|Google Kubernetes Engine project" +msgstr "Projeto no Google Kubernetes Engine" msgid "ClusterIntegration|Learn more about %{link_to_documentation}" msgstr "Leia mais sobre %{link_to_documentation}" @@ -585,8 +585,8 @@ msgstr "Ver zonas" msgid "ClusterIntegration|Something went wrong on our end." msgstr "Alguma coisa deu errado do nosso lado." -msgid "ClusterIntegration|Something went wrong while creating your cluster on Google Container Engine" -msgstr "Algo deu errado ao criar seu cluster no Google Container Engine" +msgid "ClusterIntegration|Something went wrong while creating your cluster on Google Kubernetes Engine" +msgstr "Algo deu errado ao criar seu cluster no Google Kubernetes Engine" msgid "ClusterIntegration|Toggle Cluster" msgstr "Alternar cluster" @@ -594,14 +594,14 @@ msgstr "Alternar cluster" msgid "ClusterIntegration|With a cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way." msgstr "Com um cluster associado à esse projeto, você pode usar revisão de apps, fazer deploy de suas aplicações, rodar suas pipelines e muito mais de um jeito simples." -msgid "ClusterIntegration|Your account must have %{link_to_container_engine}" -msgstr "Sua conta precisa ter %{link_to_container_engine}" +msgid "ClusterIntegration|Your account must have %{link_to_kubernetes_engine}" +msgstr "Sua conta precisa ter %{link_to_kubernetes_engine}" msgid "ClusterIntegration|Zone" msgstr "Zona" -msgid "ClusterIntegration|access to Google Container Engine" -msgstr "Acesso ao Google Container Engine" +msgid "ClusterIntegration|access to Google Kubernetes Engine" +msgstr "Acesso ao Google Kubernetes Engine" msgid "ClusterIntegration|cluster" msgstr "cluster" diff --git a/locale/ru/gitlab.po b/locale/ru/gitlab.po index 9111f1eb3d2..b25a5d1e75b 100644 --- a/locale/ru/gitlab.po +++ b/locale/ru/gitlab.po @@ -511,14 +511,14 @@ msgstr "Ð˜Ð½Ñ‚ÐµÐ³Ñ€Ð°Ñ†Ð¸Ñ ÐºÐ»Ð°Ñтеров включена Ð´Ð»Ñ Ñтог msgid "ClusterIntegration|Cluster integration is enabled for this project. Disabling this integration will not affect your cluster, it will only temporarily turn off GitLab's connection to it." msgstr "Ð”Ð»Ñ Ñтого проекта включена Ð¸Ð½Ñ‚ÐµÐ³Ñ€Ð°Ñ†Ð¸Ñ ÐºÐ»Ð°Ñтеров. Отключение интеграции не повлиÑет на клаÑтер, но Ñоединение Ñ GitLab будет временно отключено." -msgid "ClusterIntegration|Cluster is being created on Google Container Engine..." -msgstr "СоздаетÑÑ ÐºÐ»Ð°Ñтер в Google Container Engine..." +msgid "ClusterIntegration|Cluster is being created on Google Kubernetes Engine..." +msgstr "СоздаетÑÑ ÐºÐ»Ð°Ñтер в Google Kubernetes Engine..." msgid "ClusterIntegration|Cluster name" msgstr "Ðазвание клаÑтера" -msgid "ClusterIntegration|Cluster was successfully created on Google Container Engine" -msgstr "КлаÑтер был уÑпешно Ñоздан в Google Container Engine" +msgid "ClusterIntegration|Cluster was successfully created on Google Kubernetes Engine" +msgstr "КлаÑтер был уÑпешно Ñоздан в Google Kubernetes Engine" msgid "ClusterIntegration|Copy cluster name" msgstr "Копировать название клаÑтера" @@ -526,8 +526,8 @@ msgstr "Копировать название клаÑтера" msgid "ClusterIntegration|Create cluster" msgstr "Создать клаÑтер" -msgid "ClusterIntegration|Create new cluster on Google Container Engine" -msgstr "Создать новый клаÑтер в Google Container Engine" +msgid "ClusterIntegration|Create new cluster on Google Kubernetes Engine" +msgstr "Создать новый клаÑтер в Google Kubernetes Engine" msgid "ClusterIntegration|Enable cluster integration" msgstr "Включить интеграцию Ñ ÐºÐ»Ð°Ñтерами" @@ -535,11 +535,11 @@ msgstr "Включить интеграцию Ñ ÐºÐ»Ð°Ñтерами" msgid "ClusterIntegration|Google Cloud Platform project ID" msgstr "Идентификатор проекта в Google Cloud Platform" -msgid "ClusterIntegration|Google Container Engine" -msgstr "Google Container Engine" +msgid "ClusterIntegration|Google Kubernetes Engine" +msgstr "Google Kubernetes Engine" -msgid "ClusterIntegration|Google Container Engine project" -msgstr "Проект Google Container Engine" +msgid "ClusterIntegration|Google Kubernetes Engine project" +msgstr "Проект Google Kubernetes Engine" msgid "ClusterIntegration|Learn more about %{link_to_documentation}" msgstr "Узнайте больше на %{link_to_documentation}" @@ -592,8 +592,8 @@ msgstr "См. зоны" msgid "ClusterIntegration|Something went wrong on our end." msgstr " У Ð½Ð°Ñ Ñ‡Ñ‚Ð¾-то пошло не так." -msgid "ClusterIntegration|Something went wrong while creating your cluster on Google Container Engine" -msgstr "Что-то пошло не так во Ð²Ñ€ÐµÐ¼Ñ ÑÐ¾Ð·Ð´Ð°Ð½Ð¸Ñ ÐºÐ»Ð°Ñтера в Google Container Engine" +msgid "ClusterIntegration|Something went wrong while creating your cluster on Google Kubernetes Engine" +msgstr "Что-то пошло не так во Ð²Ñ€ÐµÐ¼Ñ ÑÐ¾Ð·Ð´Ð°Ð½Ð¸Ñ ÐºÐ»Ð°Ñтера в Google Kubernetes Engine" msgid "ClusterIntegration|Toggle Cluster" msgstr "Переключить КлаÑтер" @@ -601,14 +601,14 @@ msgstr "Переключить КлаÑтер" msgid "ClusterIntegration|With a cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way." msgstr "ЕÑли привÑзать клаÑтер к Ñтому проекту, вы Ñ Ð»Ñ‘Ð³ÐºÐ¾Ñтью Ñможете иÑпользовать Ð¿Ñ€Ð¸Ð»Ð¾Ð¶ÐµÐ½Ð¸Ñ Ð´Ð»Ñ Ñ€ÐµÐ²ÑŒÑŽ, развертывать ваши приложениÑ, запуÑкать Ñборочные линии и многое другое." -msgid "ClusterIntegration|Your account must have %{link_to_container_engine}" -msgstr "Ваша ÑƒÑ‡ÐµÑ‚Ð½Ð°Ñ Ð·Ð°Ð¿Ð¸ÑÑŒ должна иметь %{link_to_container_engine}" +msgid "ClusterIntegration|Your account must have %{link_to_kubernetes_engine}" +msgstr "Ваша ÑƒÑ‡ÐµÑ‚Ð½Ð°Ñ Ð·Ð°Ð¿Ð¸ÑÑŒ должна иметь %{link_to_kubernetes_engine}" msgid "ClusterIntegration|Zone" msgstr "Зона" -msgid "ClusterIntegration|access to Google Container Engine" -msgstr "доÑтуп к Google Container Engine" +msgid "ClusterIntegration|access to Google Kubernetes Engine" +msgstr "доÑтуп к Google Kubernetes Engine" msgid "ClusterIntegration|cluster" msgstr "клаÑтер" diff --git a/locale/uk/gitlab.po b/locale/uk/gitlab.po index 73dfe949ded..53054bdaa27 100644 --- a/locale/uk/gitlab.po +++ b/locale/uk/gitlab.po @@ -511,14 +511,14 @@ msgstr "Ð†Ð½Ñ‚ÐµÐ³Ñ€Ð°Ñ†Ñ–Ñ Ñ–Ð· клаÑтером увімкнена Ð´Ð»Ñ Ñ msgid "ClusterIntegration|Cluster integration is enabled for this project. Disabling this integration will not affect your cluster, it will only temporarily turn off GitLab's connection to it." msgstr "Ð”Ð»Ñ Ñ†ÑŒÐ¾Ð³Ð¾ проекту увімкнена Ñ–Ð½Ñ‚ÐµÐ³Ñ€Ð°Ñ†Ñ–Ñ Ñ–Ð· клаÑтером. Ð’Ð¸ÐºÐ½ÐµÐ½Ð½Ñ Ñ–Ð½Ñ‚ÐµÐ³Ñ€Ð°Ñ†Ñ–Ñ— не вплине на клаÑтер, але з'Ñ”Ð´Ð½Ð°Ð½Ð½Ñ GitLab з ним буде тимчаÑово розірване." -msgid "ClusterIntegration|Cluster is being created on Google Container Engine..." -msgstr "СтворюєтьÑÑ ÐºÐ»Ð°Ñтер в Google Container Engine..." +msgid "ClusterIntegration|Cluster is being created on Google Kubernetes Engine..." +msgstr "СтворюєтьÑÑ ÐºÐ»Ð°Ñтер в Google Kubernetes Engine..." msgid "ClusterIntegration|Cluster name" msgstr "Ім'Ñ ÐºÐ»Ð°Ñтера" -msgid "ClusterIntegration|Cluster was successfully created on Google Container Engine" -msgstr "КлаÑтер був уÑпішно Ñтворений в Google Container Engine" +msgid "ClusterIntegration|Cluster was successfully created on Google Kubernetes Engine" +msgstr "КлаÑтер був уÑпішно Ñтворений в Google Kubernetes Engine" msgid "ClusterIntegration|Copy cluster name" msgstr "Копіювати назву клаÑтера" @@ -526,8 +526,8 @@ msgstr "Копіювати назву клаÑтера" msgid "ClusterIntegration|Create cluster" msgstr "Створити клаÑтер" -msgid "ClusterIntegration|Create new cluster on Google Container Engine" -msgstr "Створити новий клаÑтер в Google Container Engine" +msgid "ClusterIntegration|Create new cluster on Google Kubernetes Engine" +msgstr "Створити новий клаÑтер в Google Kubernetes Engine" msgid "ClusterIntegration|Enable cluster integration" msgstr "Увімкнути інтеграцію із клаÑтерами" @@ -535,11 +535,11 @@ msgstr "Увімкнути інтеграцію із клаÑтерами" msgid "ClusterIntegration|Google Cloud Platform project ID" msgstr "Ідентифікатор проекту в Google Cloud Platform" -msgid "ClusterIntegration|Google Container Engine" -msgstr "Google Container Engine" +msgid "ClusterIntegration|Google Kubernetes Engine" +msgstr "Google Kubernetes Engine" -msgid "ClusterIntegration|Google Container Engine project" -msgstr "Проект Google Container Engine" +msgid "ClusterIntegration|Google Kubernetes Engine project" +msgstr "Проект Google Kubernetes Engine" msgid "ClusterIntegration|Learn more about %{link_to_documentation}" msgstr "ДізнайтеÑÑ Ð±Ñ–Ð»ÑŒÑˆÐµ про %{link_to_documentation}" @@ -592,8 +592,8 @@ msgstr "ПереглÑнути зони" msgid "ClusterIntegration|Something went wrong on our end." msgstr "ЩоÑÑŒ пішло не так з нашого боку." -msgid "ClusterIntegration|Something went wrong while creating your cluster on Google Container Engine" -msgstr "ЩоÑÑŒ пішло не так під Ñ‡Ð°Ñ ÑÑ‚Ð²Ð¾Ñ€ÐµÐ½Ð½Ñ ÐºÐ»Ð°Ñтера в Google Container Engine" +msgid "ClusterIntegration|Something went wrong while creating your cluster on Google Kubernetes Engine" +msgstr "ЩоÑÑŒ пішло не так під Ñ‡Ð°Ñ ÑÑ‚Ð²Ð¾Ñ€ÐµÐ½Ð½Ñ ÐºÐ»Ð°Ñтера в Google Kubernetes Engine" msgid "ClusterIntegration|Toggle Cluster" msgstr "Переключити КлаÑтер" @@ -601,14 +601,14 @@ msgstr "Переключити КлаÑтер" msgid "ClusterIntegration|With a cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way." msgstr "За допомогою підключеного до цього проекту клаÑтера, ви можете викориÑтовувати Review Apps, розгортати ваші проекти, запуÑкати конвеєри збірки та багато іншого." -msgid "ClusterIntegration|Your account must have %{link_to_container_engine}" -msgstr "Ваш обліковий Ð·Ð°Ð¿Ð¸Ñ Ð¿Ð¾Ð²Ð¸Ð½ÐµÐ½ мати %{link_to_container_engine}" +msgid "ClusterIntegration|Your account must have %{link_to_kubernetes_engine}" +msgstr "Ваш обліковий Ð·Ð°Ð¿Ð¸Ñ Ð¿Ð¾Ð²Ð¸Ð½ÐµÐ½ мати %{link_to_kubernetes_engine}" msgid "ClusterIntegration|Zone" msgstr "Зона" -msgid "ClusterIntegration|access to Google Container Engine" -msgstr "доÑтуп до Google Container Engine" +msgid "ClusterIntegration|access to Google Kubernetes Engine" +msgstr "доÑтуп до Google Kubernetes Engine" msgid "ClusterIntegration|cluster" msgstr "клаÑтер" diff --git a/locale/zh_CN/gitlab.po b/locale/zh_CN/gitlab.po index 3a08b6c20a2..e1bc9219908 100644 --- a/locale/zh_CN/gitlab.po +++ b/locale/zh_CN/gitlab.po @@ -497,14 +497,14 @@ msgstr "æ¤é¡¹ç›®å·²å¯ç”¨é›†ç¾¤é›†æˆã€‚" msgid "ClusterIntegration|Cluster integration is enabled for this project. Disabling this integration will not affect your cluster, it will only temporarily turn off GitLab's connection to it." msgstr "æ¤é¡¹ç›®å·²å¯ç”¨é›†ç¾¤é›†æˆã€‚ç¦ç”¨æ¤é›†æˆä¸ä¼šå½±å“您的集群,它åªä¼šæš‚æ—¶å…³é— GitLab 的连接。" -msgid "ClusterIntegration|Cluster is being created on Google Container Engine..." -msgstr "集群æ£åœ¨ Google Container Engine 上创建..." +msgid "ClusterIntegration|Cluster is being created on Google Kubernetes Engine..." +msgstr "集群æ£åœ¨ Google Kubernetes Engine 上创建..." msgid "ClusterIntegration|Cluster name" msgstr "集群åç§°" -msgid "ClusterIntegration|Cluster was successfully created on Google Container Engine" -msgstr "集群已在 Google Container Engine 上æˆåŠŸåˆ›å»º" +msgid "ClusterIntegration|Cluster was successfully created on Google Kubernetes Engine" +msgstr "集群已在 Google Kubernetes Engine 上æˆåŠŸåˆ›å»º" msgid "ClusterIntegration|Copy cluster name" msgstr "å¤åˆ¶é›†ç¾¤åç§°" @@ -512,8 +512,8 @@ msgstr "å¤åˆ¶é›†ç¾¤åç§°" msgid "ClusterIntegration|Create cluster" msgstr "创建集群" -msgid "ClusterIntegration|Create new cluster on Google Container Engine" -msgstr "在 Google Container Engine 上创建新集群" +msgid "ClusterIntegration|Create new cluster on Google Kubernetes Engine" +msgstr "在 Google Kubernetes Engine 上创建新集群" msgid "ClusterIntegration|Enable cluster integration" msgstr "å¯ç”¨é›†ç¾¤é›†æˆ" @@ -521,11 +521,11 @@ msgstr "å¯ç”¨é›†ç¾¤é›†æˆ" msgid "ClusterIntegration|Google Cloud Platform project ID" msgstr "Google 云平å°é¡¹ç›®ID" -msgid "ClusterIntegration|Google Container Engine" -msgstr "Google Container Engine" +msgid "ClusterIntegration|Google Kubernetes Engine" +msgstr "Google Kubernetes Engine" -msgid "ClusterIntegration|Google Container Engine project" -msgstr "Google Container Engine 项目" +msgid "ClusterIntegration|Google Kubernetes Engine project" +msgstr "Google Kubernetes Engine 项目" msgid "ClusterIntegration|Learn more about %{link_to_documentation}" msgstr "了解详细%{link_to_documentation}" @@ -578,8 +578,8 @@ msgstr "查看区域" msgid "ClusterIntegration|Something went wrong on our end." msgstr "å‘生了内部错误" -msgid "ClusterIntegration|Something went wrong while creating your cluster on Google Container Engine" -msgstr "在 Google Container Engine 上创建集群时å‘生错误" +msgid "ClusterIntegration|Something went wrong while creating your cluster on Google Kubernetes Engine" +msgstr "在 Google Kubernetes Engine 上创建集群时å‘生错误" msgid "ClusterIntegration|Toggle Cluster" msgstr "切æ¢é›†ç¾¤" @@ -587,14 +587,14 @@ msgstr "切æ¢é›†ç¾¤" msgid "ClusterIntegration|With a cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way." msgstr "使用与æ¤é¡¹ç›®å…³è”的集群,您å¯ä»¥ä½¿ç”¨å®¡é˜…应用程åºï¼Œéƒ¨ç½²åº”用程åºï¼Œè¿è¡Œæµæ°´çº¿ç‰ç‰ã€‚" -msgid "ClusterIntegration|Your account must have %{link_to_container_engine}" -msgstr "æ‚¨çš„å¸æˆ·å¿…须有%{link_to_container_engine}" +msgid "ClusterIntegration|Your account must have %{link_to_kubernetes_engine}" +msgstr "æ‚¨çš„å¸æˆ·å¿…须有%{link_to_kubernetes_engine}" msgid "ClusterIntegration|Zone" msgstr "区域" -msgid "ClusterIntegration|access to Google Container Engine" -msgstr "访问 Google Container Engine" +msgid "ClusterIntegration|access to Google Kubernetes Engine" +msgstr "访问 Google Kubernetes Engine" msgid "ClusterIntegration|cluster" msgstr "集群" diff --git a/locale/zh_HK/gitlab.po b/locale/zh_HK/gitlab.po index 30d5b0c1416..b851809fc7c 100644 --- a/locale/zh_HK/gitlab.po +++ b/locale/zh_HK/gitlab.po @@ -497,13 +497,13 @@ msgstr "" msgid "ClusterIntegration|Cluster integration is enabled for this project. Disabling this integration will not affect your cluster, it will only temporarily turn off GitLab's connection to it." msgstr "" -msgid "ClusterIntegration|Cluster is being created on Google Container Engine..." +msgid "ClusterIntegration|Cluster is being created on Google Kubernetes Engine..." msgstr "" msgid "ClusterIntegration|Cluster name" msgstr "" -msgid "ClusterIntegration|Cluster was successfully created on Google Container Engine" +msgid "ClusterIntegration|Cluster was successfully created on Google Kubernetes Engine" msgstr "" msgid "ClusterIntegration|Copy cluster name" @@ -512,7 +512,7 @@ msgstr "" msgid "ClusterIntegration|Create cluster" msgstr "" -msgid "ClusterIntegration|Create new cluster on Google Container Engine" +msgid "ClusterIntegration|Create new cluster on Google Kubernetes Engine" msgstr "" msgid "ClusterIntegration|Enable cluster integration" @@ -521,10 +521,10 @@ msgstr "" msgid "ClusterIntegration|Google Cloud Platform project ID" msgstr "" -msgid "ClusterIntegration|Google Container Engine" +msgid "ClusterIntegration|Google Kubernetes Engine" msgstr "" -msgid "ClusterIntegration|Google Container Engine project" +msgid "ClusterIntegration|Google Kubernetes Engine project" msgstr "" msgid "ClusterIntegration|Learn more about %{link_to_documentation}" @@ -578,7 +578,7 @@ msgstr "" msgid "ClusterIntegration|Something went wrong on our end." msgstr "" -msgid "ClusterIntegration|Something went wrong while creating your cluster on Google Container Engine" +msgid "ClusterIntegration|Something went wrong while creating your cluster on Google Kubernetes Engine" msgstr "" msgid "ClusterIntegration|Toggle Cluster" @@ -587,13 +587,13 @@ msgstr "" msgid "ClusterIntegration|With a cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way." msgstr "" -msgid "ClusterIntegration|Your account must have %{link_to_container_engine}" +msgid "ClusterIntegration|Your account must have %{link_to_kubernetes_engine}" msgstr "" msgid "ClusterIntegration|Zone" msgstr "" -msgid "ClusterIntegration|access to Google Container Engine" +msgid "ClusterIntegration|access to Google Kubernetes Engine" msgstr "" msgid "ClusterIntegration|cluster" diff --git a/locale/zh_TW/gitlab.po b/locale/zh_TW/gitlab.po index 6531330074a..b6d4ed27487 100644 --- a/locale/zh_TW/gitlab.po +++ b/locale/zh_TW/gitlab.po @@ -497,13 +497,13 @@ msgstr "æ¤å°ˆæ¡ˆå·²ç¶“啟用å¢é›†æ•´åˆ" msgid "ClusterIntegration|Cluster integration is enabled for this project. Disabling this integration will not affect your cluster, it will only temporarily turn off GitLab's connection to it." msgstr "æ¤å°ˆæ¡ˆå·²å•Ÿç”¨å¢é›†æ•´åˆã€‚ç¦æ¢å¢é›†æ•´åˆä¸æœƒå½±éŸ¿æ‚¨çš„å¢é›†ï¼Œå®ƒåªæ˜¯æš«æ™‚關閉 GitLab 的連接。" -msgid "ClusterIntegration|Cluster is being created on Google Container Engine..." +msgid "ClusterIntegration|Cluster is being created on Google Kubernetes Engine..." msgstr "在 Google 容器引擎ä¸å»ºç«‹æ–°çš„å¢é›†" msgid "ClusterIntegration|Cluster name" msgstr "å¢é›†å稱" -msgid "ClusterIntegration|Cluster was successfully created on Google Container Engine" +msgid "ClusterIntegration|Cluster was successfully created on Google Kubernetes Engine" msgstr "在 Google 容器引擎上æˆåŠŸå»ºç«‹å¢é›†" msgid "ClusterIntegration|Copy cluster name" @@ -512,7 +512,7 @@ msgstr "複製å¢é›†å稱" msgid "ClusterIntegration|Create cluster" msgstr "建立å¢é›†" -msgid "ClusterIntegration|Create new cluster on Google Container Engine" +msgid "ClusterIntegration|Create new cluster on Google Kubernetes Engine" msgstr "在 Google 容器引擎ä¸å»ºç«‹æ–°çš„å¢é›†" msgid "ClusterIntegration|Enable cluster integration" @@ -521,10 +521,10 @@ msgstr "啟動å¢é›†æ•´åˆ" msgid "ClusterIntegration|Google Cloud Platform project ID" msgstr "Google 雲端專案 ID" -msgid "ClusterIntegration|Google Container Engine" +msgid "ClusterIntegration|Google Kubernetes Engine" msgstr "Google 容器引擎" -msgid "ClusterIntegration|Google Container Engine project" +msgid "ClusterIntegration|Google Kubernetes Engine project" msgstr "Google 容器引擎專案" msgid "ClusterIntegration|Learn more about %{link_to_documentation}" @@ -578,8 +578,8 @@ msgstr "查看å€åŸŸ" msgid "ClusterIntegration|Something went wrong on our end." msgstr "內部發生了錯誤" -msgid "ClusterIntegration|Something went wrong while creating your cluster on Google Container Engine" -msgstr "在 Google Container Engine 上建立å¢é›†æ™‚發生了錯誤" +msgid "ClusterIntegration|Something went wrong while creating your cluster on Google Kubernetes Engine" +msgstr "在 Google Kubernetes Engine 上建立å¢é›†æ™‚發生了錯誤" msgid "ClusterIntegration|Toggle Cluster" msgstr "å¢é›†é–‹é—œ" @@ -587,14 +587,14 @@ msgstr "å¢é›†é–‹é—œ" msgid "ClusterIntegration|With a cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way." msgstr "ç•¶å¢é›†é€£çµåˆ°æ¤å°ˆæ¡ˆï¼Œä½ å¯ä»¥ä½¿ç”¨è¤‡é–±æ‡‰ç”¨ (review apps)ï¼Œéƒ¨ç½²ä½ çš„æ‡‰ç”¨ç¨‹å¼ï¼ŒåŸ·è¡Œä½ çš„æµæ°´ç·š (pipelines),還有更多容易上手的方å¼å¯ä»¥ä½¿ç”¨ã€‚" -msgid "ClusterIntegration|Your account must have %{link_to_container_engine}" -msgstr "æ‚¨çš„å¸³è™Ÿå¿…é ˆæœ‰ %{link_to_container_engine}" +msgid "ClusterIntegration|Your account must have %{link_to_kubernetes_engine}" +msgstr "æ‚¨çš„å¸³è™Ÿå¿…é ˆæœ‰ %{link_to_kubernetes_engine}" msgid "ClusterIntegration|Zone" msgstr "å€åŸŸ" -msgid "ClusterIntegration|access to Google Container Engine" -msgstr "å˜å– Google Container Engine" +msgid "ClusterIntegration|access to Google Kubernetes Engine" +msgstr "å˜å– Google Kubernetes Engine" msgid "ClusterIntegration|cluster" msgstr "å¢é›†" @@ -46,6 +46,10 @@ module QA autoload :Create, 'qa/scenario/gitlab/project/create' end + module Repository + autoload :Push, 'qa/scenario/gitlab/repository/push' + end + module Sandbox autoload :Prepare, 'qa/scenario/gitlab/sandbox/prepare' end diff --git a/qa/qa/page/group/new.rb b/qa/qa/page/group/new.rb index cb743a7bf11..53fdaaed078 100644 --- a/qa/qa/page/group/new.rb +++ b/qa/qa/page/group/new.rb @@ -4,6 +4,7 @@ module QA class New < Page::Base def set_path(path) fill_in 'group_path', with: path + fill_in 'group_name', with: path end def set_description(description) diff --git a/qa/qa/scenario/gitlab/repository/push.rb b/qa/qa/scenario/gitlab/repository/push.rb new file mode 100644 index 00000000000..b00ab0c313a --- /dev/null +++ b/qa/qa/scenario/gitlab/repository/push.rb @@ -0,0 +1,47 @@ +require "pry-byebug" + +module QA + module Scenario + module Gitlab + module Repository + class Push < Scenario::Template + 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 perform + 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 +end diff --git a/qa/qa/specs/features/repository/push_spec.rb b/qa/qa/specs/features/repository/push_spec.rb index 30935dc1e13..5b930b9818a 100644 --- a/qa/qa/specs/features/repository/push_spec.rb +++ b/qa/qa/specs/features/repository/push_spec.rb @@ -10,21 +10,10 @@ module QA 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 + Scenario::Gitlab::Repository::Push.perform 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/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/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..eb52be3d731 100644 --- a/rubocop/rubocop.rb +++ b/rubocop/rubocop.rb @@ -14,6 +14,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..4ff0e8e10b7 100755 --- a/scripts/trigger-build-omnibus +++ b/scripts/trigger-build-omnibus @@ -66,7 +66,7 @@ module Omnibus raise 'Pipeline timeout!' if timeout? case status - when :pending, :running + when :created, :pending, :running puts "Waiting another #{INTERVAL} seconds ..." sleep INTERVAL when :success 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/groups/uploads_controller_spec.rb b/spec/controllers/groups/uploads_controller_spec.rb new file mode 100644 index 00000000000..67a11e56e94 --- /dev/null +++ b/spec/controllers/groups/uploads_controller_spec.rb @@ -0,0 +1,10 @@ +require 'spec_helper' + +describe Groups::UploadsController do + let(:model) { create(:group, :public) } + let(:params) do + { group_id: model } + end + + it_behaves_like 'handle uploads' +end 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/commit_controller_spec.rb b/spec/controllers/projects/commit_controller_spec.rb index fd90c0d8bad..694c64ae1ad 100644 --- a/spec/controllers/projects/commit_controller_spec.rb +++ b/spec/controllers/projects/commit_controller_spec.rb @@ -132,6 +132,22 @@ describe Projects::CommitController do expect(response).to be_success end end + + context 'in the context of a merge_request' do + let(:merge_request) { create(:merge_request, source_project: project) } + let(:commit) { merge_request.commits.first } + + it 'prepare diff notes in the context of the merge request' do + go(id: commit.id, merge_request_iid: merge_request.iid) + + expect(assigns(:new_diff_note_attrs)).to eq({ + noteable_type: 'MergeRequest', + noteable_id: merge_request.id, + commit_id: commit.id + }) + expect(response).to be_ok + end + end end describe 'GET branches' do @@ -323,7 +339,7 @@ describe Projects::CommitController do context 'when the commit does not exist' do before do - diff_for_path(id: commit.id.succ, old_path: existing_path, new_path: existing_path) + diff_for_path(id: commit.id.reverse, old_path: existing_path, new_path: existing_path) end it 'returns a 404' do 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/merge_requests/diffs_controller_spec.rb b/spec/controllers/projects/merge_requests/diffs_controller_spec.rb index 18a70bec103..ba97ccfbbd4 100644 --- a/spec/controllers/projects/merge_requests/diffs_controller_spec.rb +++ b/spec/controllers/projects/merge_requests/diffs_controller_spec.rb @@ -100,7 +100,8 @@ describe Projects::MergeRequests::DiffsController do expect(assigns(:diff_notes_disabled)).to be_falsey expect(assigns(:new_diff_note_attrs)).to eq(noteable_type: 'MergeRequest', - noteable_id: merge_request.id) + noteable_id: merge_request.id, + commit_id: nil) end it 'only renders the diffs for the path given' do diff --git a/spec/controllers/projects/pipelines_settings_controller_spec.rb b/spec/controllers/projects/pipelines_settings_controller_spec.rb index b2d83a02290..1cc488bef32 100644 --- a/spec/controllers/projects/pipelines_settings_controller_spec.rb +++ b/spec/controllers/projects/pipelines_settings_controller_spec.rb @@ -16,14 +16,13 @@ describe Projects::PipelinesSettingsController do patch :update, namespace_id: project.namespace.to_param, project_id: project, - project: { auto_devops_attributes: params, - run_auto_devops_pipeline_implicit: 'false', - run_auto_devops_pipeline_explicit: auto_devops_pipeline } + project: { + auto_devops_attributes: params + } end context 'when updating the auto_devops settings' do let(:params) { { enabled: '', domain: 'mepmep.md' } } - let(:auto_devops_pipeline) { 'false' } it 'redirects to the settings page' do subject @@ -44,7 +43,9 @@ describe Projects::PipelinesSettingsController do end context 'when run_auto_devops_pipeline is true' do - let(:auto_devops_pipeline) { 'true' } + before do + expect_any_instance_of(Projects::UpdateService).to receive(:run_auto_devops_pipeline?).and_return(true) + end it 'queues a CreatePipelineWorker' do expect(CreatePipelineWorker).to receive(:perform_async).with(project.id, user.id, project.default_branch, :web, any_args) @@ -54,7 +55,9 @@ describe Projects::PipelinesSettingsController do end context 'when run_auto_devops_pipeline is not true' do - let(:auto_devops_pipeline) { 'false' } + before do + expect_any_instance_of(Projects::UpdateService).to receive(:run_auto_devops_pipeline?).and_return(false) + end it 'does not queue a CreatePipelineWorker' do expect(CreatePipelineWorker).not_to receive(:perform_async).with(project.id, user.id, :web, any_args) 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/controllers/projects/uploads_controller_spec.rb b/spec/controllers/projects/uploads_controller_spec.rb index c2550b1efa7..d572085661d 100644 --- a/spec/controllers/projects/uploads_controller_spec.rb +++ b/spec/controllers/projects/uploads_controller_spec.rb @@ -1,247 +1,10 @@ -require('spec_helper') +require 'spec_helper' describe Projects::UploadsController do - let(:project) { create(:project) } - let(:user) { create(:user) } - let(:jpg) { fixture_file_upload(Rails.root + 'spec/fixtures/rails_sample.jpg', 'image/jpg') } - let(:txt) { fixture_file_upload(Rails.root + 'spec/fixtures/doc_sample.txt', 'text/plain') } - - describe "POST #create" do - before do - sign_in(user) - project.team << [user, :developer] - end - - context "without params['file']" do - it "returns an error" do - post :create, - namespace_id: project.namespace.to_param, - project_id: project, - format: :json - expect(response).to have_gitlab_http_status(422) - end - end - - context 'with valid image' do - before do - post :create, - namespace_id: project.namespace.to_param, - project_id: project, - file: jpg, - format: :json - end - - it 'returns a content with original filename, new link, and correct type.' do - expect(response.body).to match '\"alt\":\"rails_sample\"' - expect(response.body).to match "\"url\":\"/uploads" - end - - # NOTE: This is as close as we're getting to an Integration test for this - # behavior. We're avoiding a proper Feature test because those should be - # testing things entirely user-facing, which the Upload model is very much - # not. - it 'creates a corresponding Upload record' do - upload = Upload.last - - aggregate_failures do - expect(upload).to exist - expect(upload.model).to eq project - end - end - end - - context 'with valid non-image file' do - before do - post :create, - namespace_id: project.namespace.to_param, - project_id: project, - file: txt, - format: :json - end - - it 'returns a content with original filename, new link, and correct type.' do - expect(response.body).to match '\"alt\":\"doc_sample.txt\"' - expect(response.body).to match "\"url\":\"/uploads" - end - end + let(:model) { create(:project, :public) } + let(:params) do + { namespace_id: model.namespace.to_param, project_id: model } end - describe "GET #show" do - let(:go) do - get :show, - namespace_id: project.namespace.to_param, - project_id: project, - secret: "123456", - filename: "image.jpg" - end - - context "when the project is public" do - before do - project.update_attribute(:visibility_level, Project::PUBLIC) - end - - context "when not signed in" do - context "when the file exists" do - before do - allow_any_instance_of(FileUploader).to receive(:file).and_return(jpg) - allow(jpg).to receive(:exists?).and_return(true) - end - - it "responds with status 200" do - go - - expect(response).to have_gitlab_http_status(200) - end - end - - context "when the file doesn't exist" do - it "responds with status 404" do - go - - expect(response).to have_gitlab_http_status(404) - end - end - end - - context "when signed in" do - before do - sign_in(user) - end - - context "when the file exists" do - before do - allow_any_instance_of(FileUploader).to receive(:file).and_return(jpg) - allow(jpg).to receive(:exists?).and_return(true) - end - - it "responds with status 200" do - go - - expect(response).to have_gitlab_http_status(200) - end - end - - context "when the file doesn't exist" do - it "responds with status 404" do - go - - expect(response).to have_gitlab_http_status(404) - end - end - end - end - - context "when the project is private" do - before do - project.update_attribute(:visibility_level, Project::PRIVATE) - end - - context "when not signed in" do - context "when the file exists" do - before do - allow_any_instance_of(FileUploader).to receive(:file).and_return(jpg) - allow(jpg).to receive(:exists?).and_return(true) - end - - context "when the file is an image" do - before do - allow_any_instance_of(FileUploader).to receive(:image?).and_return(true) - end - - it "responds with status 200" do - go - - expect(response).to have_gitlab_http_status(200) - end - end - - context "when the file is not an image" do - it "redirects to the sign in page" do - go - - expect(response).to redirect_to(new_user_session_path) - end - end - end - - context "when the file doesn't exist" do - it "redirects to the sign in page" do - go - - expect(response).to redirect_to(new_user_session_path) - end - end - end - - context "when signed in" do - before do - sign_in(user) - end - - context "when the user has access to the project" do - before do - project.team << [user, :master] - end - - context "when the file exists" do - before do - allow_any_instance_of(FileUploader).to receive(:file).and_return(jpg) - allow(jpg).to receive(:exists?).and_return(true) - end - - it "responds with status 200" do - go - - expect(response).to have_gitlab_http_status(200) - end - end - - context "when the file doesn't exist" do - it "responds with status 404" do - go - - expect(response).to have_gitlab_http_status(404) - end - end - end - - context "when the user doesn't have access to the project" do - context "when the file exists" do - before do - allow_any_instance_of(FileUploader).to receive(:file).and_return(jpg) - allow(jpg).to receive(:exists?).and_return(true) - end - - context "when the file is an image" do - before do - allow_any_instance_of(FileUploader).to receive(:image?).and_return(true) - end - - it "responds with status 200" do - go - - expect(response).to have_gitlab_http_status(200) - end - end - - context "when the file is not an image" do - it "responds with status 404" do - go - - expect(response).to have_gitlab_http_status(404) - end - end - end - - context "when the file doesn't exist" do - it "responds with status 404" do - go - - expect(response).to have_gitlab_http_status(404) - end - end - end - end - end - end + it_behaves_like 'handle uploads' end diff --git a/spec/factories/notes.rb b/spec/factories/notes.rb index ab4ae123429..471bfb3213a 100644 --- a/spec/factories/notes.rb +++ b/spec/factories/notes.rb @@ -63,13 +63,19 @@ FactoryGirl.define do factory :diff_note_on_commit, traits: [:on_commit], class: DiffNote do association :project, :repository + + transient do + line_number 14 + diff_refs { project.commit(commit_id).try(:diff_refs) } + end + position do Gitlab::Diff::Position.new( old_path: "files/ruby/popen.rb", new_path: "files/ruby/popen.rb", old_line: nil, - new_line: 14, - diff_refs: project.commit(commit_id).try(:diff_refs) + new_line: line_number, + diff_refs: diff_refs ) end end diff --git a/spec/factories/uploads.rb b/spec/factories/uploads.rb index 3222c41c3d8..e18f1a6bd4a 100644 --- a/spec/factories/uploads.rb +++ b/spec/factories/uploads.rb @@ -4,5 +4,21 @@ FactoryGirl.define do path { "uploads/-/system/project/avatar/avatar.jpg" } size 100.kilobytes uploader "AvatarUploader" + + trait :personal_snippet do + model { build(:personal_snippet) } + uploader "PersonalFileUploader" + end + + trait :issuable_upload do + path { "#{SecureRandom.hex}/myfile.jpg" } + uploader "FileUploader" + end + + trait :namespace_upload do + path { "#{SecureRandom.hex}/myfile.jpg" } + model { build(:group) } + uploader "NamespaceFileUploader" + end end end diff --git a/spec/factories/users.rb b/spec/factories/users.rb index 4000cd085b7..8ace424f8af 100644 --- a/spec/factories/users.rb +++ b/spec/factories/users.rb @@ -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/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/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/issues/gfm_autocomplete_spec.rb b/spec/features/issues/gfm_autocomplete_spec.rb index 95d637265e0..c31b636d67f 100644 --- a/spec/features/issues/gfm_autocomplete_spec.rb +++ b/spec/features/issues/gfm_autocomplete_spec.rb @@ -220,6 +220,89 @@ feature 'GFM autocomplete', :js do end end + # This context has jsut one example in each contexts in order to improve spec performance. + context 'labels' do + let!(:backend) { create(:label, project: project, title: 'backend') } + let!(:bug) { create(:label, project: project, title: 'bug') } + let!(:feature_proposal) { create(:label, project: project, title: 'feature proposal') } + + context 'when no labels are assigned' do + it 'shows labels' do + note = find('#note-body') + + # It should show all the labels on "~". + type(note, '~') + expect_labels(shown: [backend, bug, feature_proposal]) + + # It should show all the labels on "/label ~". + type(note, '/label ~') + expect_labels(shown: [backend, bug, feature_proposal]) + + # It should show all the labels on "/relabel ~". + type(note, '/relabel ~') + expect_labels(shown: [backend, bug, feature_proposal]) + + # It should show no labels on "/unlabel ~". + type(note, '/unlabel ~') + expect_labels(not_shown: [backend, bug, feature_proposal]) + end + end + + context 'when some labels are assigned' do + before do + issue.labels << [backend] + end + + it 'shows labels' do + note = find('#note-body') + + # It should show all the labels on "~". + type(note, '~') + expect_labels(shown: [backend, bug, feature_proposal]) + + # It should show only unset labels on "/label ~". + type(note, '/label ~') + expect_labels(shown: [bug, feature_proposal], not_shown: [backend]) + + # It should show all the labels on "/relabel ~". + type(note, '/relabel ~') + expect_labels(shown: [backend, bug, feature_proposal]) + + # It should show only set labels on "/unlabel ~". + type(note, '/unlabel ~') + expect_labels(shown: [backend], not_shown: [bug, feature_proposal]) + end + end + + context 'when all labels are assigned' do + before do + issue.labels << [backend, bug, feature_proposal] + end + + it 'shows labels' do + note = find('#note-body') + + # It should show all the labels on "~". + type(note, '~') + expect_labels(shown: [backend, bug, feature_proposal]) + + # It should show no labels on "/label ~". + type(note, '/label ~') + expect_labels(not_shown: [backend, bug, feature_proposal]) + + # It should show all the labels on "/relabel ~". + type(note, '/relabel ~') + expect_labels(shown: [backend, bug, feature_proposal]) + + # It should show all the labels on "/unlabel ~". + type(note, '/unlabel ~') + expect_labels(shown: [backend, bug, feature_proposal]) + end + end + end + + private + def expect_to_wrap(should_wrap, item, note, value) expect(item).to have_content(value) expect(item).not_to have_content("\"#{value}\"") @@ -232,4 +315,27 @@ feature 'GFM autocomplete', :js do expect(note.value).not_to include("\"#{value}\"") end end + + def expect_labels(shown: nil, not_shown: nil) + page.within('.atwho-container') do + if shown + expect(page).to have_selector('.atwho-view li', count: shown.size) + shown.each { |label| expect(page).to have_content(label.title) } + end + + if not_shown + expect(page).not_to have_selector('.atwho-view li') unless shown + not_shown.each { |label| expect(page).not_to have_content(label.title) } + end + end + end + + # `note` is a textarea where the given text should be typed. + # We don't want to find it each time this function gets called. + def type(note, text) + page.within('.timeline-content-form') do + note.set('') + note.native.send_keys(text) + end + end end diff --git a/spec/features/markdown_spec.rb b/spec/features/markdown_spec.rb index cc1b187ff54..e285befc66f 100644 --- a/spec/features/markdown_spec.rb +++ b/spec/features/markdown_spec.rb @@ -207,8 +207,9 @@ describe 'GitLab Markdown' do before do @feat = MarkdownFeature.new - # `markdown` helper expects a `@project` variable + # `markdown` helper expects a `@project` and `@group` variable @project = @feat.project + @group = @feat.group end context 'default pipeline' do 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/merge_requests/versions_spec.rb b/spec/features/merge_requests/versions_spec.rb index 29f95039af8..482f2e51c8b 100644 --- a/spec/features/merge_requests/versions_spec.rb +++ b/spec/features/merge_requests/versions_spec.rb @@ -6,18 +6,47 @@ feature 'Merge Request versions', :js do let!(:merge_request_diff1) { merge_request.merge_request_diffs.create(head_commit_sha: '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9') } let!(:merge_request_diff2) { merge_request.merge_request_diffs.create(head_commit_sha: nil) } let!(:merge_request_diff3) { merge_request.merge_request_diffs.create(head_commit_sha: '5937ac0a7beb003549fc5fd26fc247adbce4a52e') } + let!(:params) { Hash.new } before do sign_in(create(:admin)) - visit diffs_project_merge_request_path(project, merge_request) + visit diffs_project_merge_request_path(project, merge_request, params) end - it 'show the latest version of the diff' do - page.within '.mr-version-dropdown' do - expect(page).to have_content 'latest version' + shared_examples 'allows commenting' do |file_id:, line_code:, comment:| + it do + diff_file_selector = ".diff-file[id='#{file_id}']" + line_code = "#{file_id}_#{line_code}" + + page.within(diff_file_selector) do + find(".line_holder[id='#{line_code}'] td:nth-of-type(1)").hover + find(".line_holder[id='#{line_code}'] button").click + + page.within("form[data-line-code='#{line_code}']") do + fill_in "note[note]", with: comment + find(".js-comment-button").click + end + + wait_for_requests + + expect(page).to have_content(comment) + end end + end - expect(page).to have_content '8 changed files' + describe 'compare with the latest version' do + it 'show the latest version of the diff' do + page.within '.mr-version-dropdown' do + expect(page).to have_content 'latest version' + end + + expect(page).to have_content '8 changed files' + end + + it_behaves_like 'allows commenting', + file_id: '7445606fbf8f3683cd42bdc54b05d7a0bc2dfc44', + line_code: '1_1', + comment: 'Typo, please fix.' end describe 'switch between versions' do @@ -62,24 +91,10 @@ feature 'Merge Request versions', :js do expect(page).to have_css(".diffs .notes[data-discussion-id='#{outdated_diff_note.discussion_id}']") end - it 'allows commenting' do - diff_file_selector = ".diff-file[id='7445606fbf8f3683cd42bdc54b05d7a0bc2dfc44']" - line_code = '7445606fbf8f3683cd42bdc54b05d7a0bc2dfc44_2_2' - - page.within(diff_file_selector) do - find(".line_holder[id='#{line_code}'] td:nth-of-type(1)").hover - find(".line_holder[id='#{line_code}'] button").click - - page.within("form[data-line-code='#{line_code}']") do - fill_in "note[note]", with: "Typo, please fix" - find(".js-comment-button").click - end - - wait_for_requests - - expect(page).to have_content("Typo, please fix") - end - end + it_behaves_like 'allows commenting', + file_id: '7445606fbf8f3683cd42bdc54b05d7a0bc2dfc44', + line_code: '2_2', + comment: 'Typo, please fix.' end describe 'compare with older version' do @@ -132,25 +147,6 @@ feature 'Merge Request versions', :js do expect(page).to have_css(".diffs .notes[data-discussion-id='#{outdated_diff_note.discussion_id}']") end - it 'allows commenting' do - diff_file_selector = ".diff-file[id='7445606fbf8f3683cd42bdc54b05d7a0bc2dfc44']" - line_code = '7445606fbf8f3683cd42bdc54b05d7a0bc2dfc44_4_4' - - page.within(diff_file_selector) do - find(".line_holder[id='#{line_code}'] td:nth-of-type(1)").hover - find(".line_holder[id='#{line_code}'] button").click - - page.within("form[data-line-code='#{line_code}']") do - fill_in "note[note]", with: "Typo, please fix" - find(".js-comment-button").click - end - - wait_for_requests - - expect(page).to have_content("Typo, please fix") - end - end - it 'show diff between new and old version' do expect(page).to have_content '4 changed files with 15 additions and 6 deletions' end @@ -162,6 +158,11 @@ feature 'Merge Request versions', :js do end expect(page).to have_content '8 changed files' end + + it_behaves_like 'allows commenting', + file_id: '7445606fbf8f3683cd42bdc54b05d7a0bc2dfc44', + line_code: '4_4', + comment: 'Typo, please fix.' end describe 'compare with same version' do @@ -210,4 +211,24 @@ feature 'Merge Request versions', :js do expect(page).to have_content '0 changed files' end end + + describe 'scoped in a commit' do + let(:params) { { commit_id: '570e7b2abdd848b95f2f578043fc23bd6f6fd24d' } } + + before do + wait_for_requests + end + + it 'should only show diffs from the commit' do + diff_commit_ids = find_all('.diff-file [data-commit-id]').map {|diff| diff['data-commit-id']} + + expect(diff_commit_ids).not_to be_empty + expect(diff_commit_ids).to all(eq(params[:commit_id])) + end + + it_behaves_like 'allows commenting', + file_id: '2f6fcd96b88b36ce98c38da085c795a27d92a3dd', + line_code: '6_6', + comment: 'Typo, please fix.' + end end diff --git a/spec/features/merge_requests/widget_spec.rb b/spec/features/merge_requests/widget_spec.rb index 2bad3b02250..3ee094c216e 100644 --- a/spec/features/merge_requests/widget_spec.rb +++ b/spec/features/merge_requests/widget_spec.rb @@ -63,6 +63,18 @@ describe 'Merge request', :js do expect(page).to have_selector('.accept-merge-request') expect(find('.accept-merge-request')['disabled']).not_to be(true) end + + it 'allows me to merge, see cherry-pick modal and load branches list' do + wait_for_requests + click_button 'Merge' + + wait_for_requests + click_link 'Cherry-pick' + page.find('.js-project-refs-dropdown').click + wait_for_requests + + expect(page.all('.js-cherry-pick-form .dropdown-content li').size).to be > 1 + end end context 'view merge request with external CI service' do diff --git a/spec/features/projects/clusters/gcp_spec.rb b/spec/features/projects/clusters/gcp_spec.rb index 5a00b463960..67b8901f8fb 100644 --- a/spec/features/projects/clusters/gcp_spec.rb +++ b/spec/features/projects/clusters/gcp_spec.rb @@ -46,15 +46,15 @@ feature 'Gcp Cluster', :js do end it 'user sees a cluster details page and creation status' do - expect(page).to have_content('Cluster is being created on Google Container Engine...') + expect(page).to have_content('Cluster is being created on Google Kubernetes Engine...') Clusters::Cluster.last.provider.make_created! - expect(page).to have_content('Cluster was successfully created on Google Container Engine') + expect(page).to have_content('Cluster was successfully created on Google Kubernetes Engine') end it 'user sees a error if something worng during creation' do - expect(page).to have_content('Cluster is being created on Google Container Engine...') + expect(page).to have_content('Cluster is being created on Google Kubernetes Engine...') Clusters::Cluster.last.provider.make_errored!('Something wrong!') diff --git a/spec/features/projects/settings/pipelines_settings_spec.rb b/spec/features/projects/settings/pipelines_settings_spec.rb index eb8e7265dd3..561f08cba00 100644 --- a/spec/features/projects/settings/pipelines_settings_spec.rb +++ b/spec/features/projects/settings/pipelines_settings_spec.rb @@ -59,107 +59,6 @@ feature "Pipelines settings" do expect(project.auto_devops).to be_present expect(project.auto_devops).not_to be_enabled end - - describe 'Immediately run pipeline checkbox option', :js do - context 'when auto devops is set to instance default (enabled)' do - before do - stub_application_setting(auto_devops_enabled: true) - project.create_auto_devops!(enabled: nil) - visit project_settings_ci_cd_path(project) - end - - it 'does not show checkboxes on page-load' do - expect(page).to have_selector('.js-run-auto-devops-pipeline-checkbox-wrapper.hide', count: 1, visible: false) - end - - it 'selecting explicit disabled hides all checkboxes' do - page.choose('project_auto_devops_attributes_enabled_false') - - expect(page).to have_selector('.js-run-auto-devops-pipeline-checkbox-wrapper.hide', count: 1, visible: false) - end - - it 'selecting explicit enabled hides all checkboxes because we are already enabled' do - page.choose('project_auto_devops_attributes_enabled_true') - - expect(page).to have_selector('.js-run-auto-devops-pipeline-checkbox-wrapper.hide', count: 1, visible: false) - end - end - - context 'when auto devops is set to instance default (disabled)' do - before do - stub_application_setting(auto_devops_enabled: false) - project.create_auto_devops!(enabled: nil) - visit project_settings_ci_cd_path(project) - end - - it 'does not show checkboxes on page-load' do - expect(page).to have_selector('.js-run-auto-devops-pipeline-checkbox-wrapper.hide', count: 1, visible: false) - end - - it 'selecting explicit disabled hides all checkboxes' do - page.choose('project_auto_devops_attributes_enabled_false') - - expect(page).to have_selector('.js-run-auto-devops-pipeline-checkbox-wrapper.hide', count: 1, visible: false) - end - - it 'selecting explicit enabled shows a checkbox' do - page.choose('project_auto_devops_attributes_enabled_true') - - expect(page).to have_selector('.js-run-auto-devops-pipeline-checkbox-wrapper:not(.hide)', count: 1) - end - end - - context 'when auto devops is set to explicit disabled' do - before do - stub_application_setting(auto_devops_enabled: true) - project.create_auto_devops!(enabled: false) - visit project_settings_ci_cd_path(project) - end - - it 'does not show checkboxes on page-load' do - expect(page).to have_selector('.js-run-auto-devops-pipeline-checkbox-wrapper.hide', count: 2, visible: false) - end - - it 'selecting explicit enabled shows a checkbox' do - page.choose('project_auto_devops_attributes_enabled_true') - - expect(page).to have_selector('.js-run-auto-devops-pipeline-checkbox-wrapper:not(.hide)', count: 1) - end - - it 'selecting instance default (enabled) shows a checkbox' do - page.choose('project_auto_devops_attributes_enabled_') - - expect(page).to have_selector('.js-run-auto-devops-pipeline-checkbox-wrapper:not(.hide)', count: 1) - end - end - - context 'when auto devops is set to explicit enabled' do - before do - stub_application_setting(auto_devops_enabled: false) - project.create_auto_devops!(enabled: true) - visit project_settings_ci_cd_path(project) - end - - it 'does not have any checkboxes' do - expect(page).not_to have_selector('.js-run-auto-devops-pipeline-checkbox-wrapper', visible: false) - end - end - - context 'when master contains a .gitlab-ci.yml file' do - let(:project) { create(:project, :repository) } - - before do - project.repository.create_file(user, '.gitlab-ci.yml', "script: ['test']", message: 'test', branch_name: project.default_branch) - stub_application_setting(auto_devops_enabled: true) - project.create_auto_devops!(enabled: false) - visit project_settings_ci_cd_path(project) - end - - it 'does not have any checkboxes' do - expect(page).not_to have_selector('.js-run-auto-devops-pipeline-checkbox-wrapper', visible: false) - end - end - end end end end diff --git a/spec/helpers/auto_devops_helper_spec.rb b/spec/helpers/auto_devops_helper_spec.rb index 7266e1b84d1..5e272af6073 100644 --- a/spec/helpers/auto_devops_helper_spec.rb +++ b/spec/helpers/auto_devops_helper_spec.rb @@ -82,104 +82,4 @@ describe AutoDevopsHelper do it { is_expected.to eq(false) } end end - - describe '.show_run_auto_devops_pipeline_checkbox_for_instance_setting?' do - subject { helper.show_run_auto_devops_pipeline_checkbox_for_instance_setting?(project) } - - context 'when master contains a .gitlab-ci.yml file' do - before do - allow(project.repository).to receive(:gitlab_ci_yml).and_return("script: ['test']") - end - - it { is_expected.to eq(false) } - end - - context 'when auto devops is explicitly enabled' do - before do - project.create_auto_devops!(enabled: true) - end - - it { is_expected.to eq(false) } - end - - context 'when auto devops is explicitly disabled' do - before do - project.create_auto_devops!(enabled: false) - end - - context 'when auto devops is enabled system-wide' do - before do - stub_application_setting(auto_devops_enabled: true) - end - - it { is_expected.to eq(true) } - end - - context 'when auto devops is disabled system-wide' do - before do - stub_application_setting(auto_devops_enabled: false) - end - - it { is_expected.to eq(false) } - end - end - - context 'when auto devops is set to instance setting' do - before do - project.create_auto_devops!(enabled: nil) - end - - it { is_expected.to eq(false) } - end - end - - describe '.show_run_auto_devops_pipeline_checkbox_for_explicit_setting?' do - subject { helper.show_run_auto_devops_pipeline_checkbox_for_explicit_setting?(project) } - - context 'when master contains a .gitlab-ci.yml file' do - before do - allow(project.repository).to receive(:gitlab_ci_yml).and_return("script: ['test']") - end - - it { is_expected.to eq(false) } - end - - context 'when auto devops is explicitly enabled' do - before do - project.create_auto_devops!(enabled: true) - end - - it { is_expected.to eq(false) } - end - - context 'when auto devops is explicitly disabled' do - before do - project.create_auto_devops!(enabled: false) - end - - it { is_expected.to eq(true) } - end - - context 'when auto devops is set to instance setting' do - before do - project.create_auto_devops!(enabled: nil) - end - - context 'when auto devops is enabled system-wide' do - before do - stub_application_setting(auto_devops_enabled: true) - end - - it { is_expected.to eq(false) } - end - - context 'when auto devops is disabled system-wide' do - before do - stub_application_setting(auto_devops_enabled: false) - end - - it { is_expected.to eq(true) } - end - end - end end diff --git a/spec/helpers/merge_requests_helper_spec.rb b/spec/helpers/merge_requests_helper_spec.rb index fd7900c32f4..3008528e60c 100644 --- a/spec/helpers/merge_requests_helper_spec.rb +++ b/spec/helpers/merge_requests_helper_spec.rb @@ -1,7 +1,9 @@ require 'spec_helper' describe MergeRequestsHelper do + include ActionView::Helpers::UrlHelper include ProjectForksHelper + describe 'ci_build_details_path' do let(:project) { create(:project) } let(:merge_request) { MergeRequest.new } @@ -41,4 +43,19 @@ describe MergeRequestsHelper do it { is_expected.to eq([source_title, target_title]) } end end + + describe '#tab_link_for' do + let(:merge_request) { create(:merge_request, :simple) } + let(:options) { Hash.new } + + subject { tab_link_for(merge_request, :show, options) { 'Discussion' } } + + describe 'supports the :force_link option' do + let(:options) { { force_link: true } } + + it 'removes the data-toggle attributes' do + is_expected.not_to match(/data-toggle="tab"/) + end + end + end end 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/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/deploy_keys/components/action_btn_spec.js b/spec/javascripts/deploy_keys/components/action_btn_spec.js index 5b93fbc5575..7025c3d836c 100644 --- a/spec/javascripts/deploy_keys/components/action_btn_spec.js +++ b/spec/javascripts/deploy_keys/components/action_btn_spec.js @@ -34,7 +34,7 @@ describe('Deploy keys action btn', () => { setTimeout(() => { expect( eventHub.$emit, - ).toHaveBeenCalledWith('enable.key', deployKey); + ).toHaveBeenCalledWith('enable.key', deployKey, jasmine.anything()); done(); }); diff --git a/spec/javascripts/deploy_keys/components/app_spec.js b/spec/javascripts/deploy_keys/components/app_spec.js index 700897f50b0..0ca9290d3d2 100644 --- a/spec/javascripts/deploy_keys/components/app_spec.js +++ b/spec/javascripts/deploy_keys/components/app_spec.js @@ -139,4 +139,18 @@ describe('Deploy keys app component', () => { it('hasKeys returns true when there are keys', () => { expect(vm.hasKeys).toEqual(3); }); + + it('resets remove button loading state', (done) => { + spyOn(window, 'confirm').and.returnValue(false); + + const btn = vm.$el.querySelector('.btn-warning'); + + btn.click(); + + Vue.nextTick(() => { + expect(btn.querySelector('.fa')).toBeNull(); + + done(); + }); + }); }); 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/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/issue_show/components/app_spec.js b/spec/javascripts/issue_show/components/app_spec.js index b47a8bf705f..729c3c29f22 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 dialog if update rejected as spam', (done) => { + function mockScriptSrc() { + const recaptchaChild = vm.$children + .find(child => child.$options._componentTag === 'recaptcha-dialog'); // 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-dialog'); + + 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..2e000a1063f 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-dialog'); // 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-dialog'); + + 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/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/datefix_spec.js b/spec/javascripts/lib/utils/datefix_spec.js index e58ac4300ba..a9f3abcf2a4 100644 --- a/spec/javascripts/lib/utils/datefix_spec.js +++ b/spec/javascripts/lib/utils/datefix_spec.js @@ -21,7 +21,7 @@ describe('datefix', () => { describe('pikadayToString', () => { it('should format a UTC date into yyyy-mm-dd format', () => { - expect(pikadayToString(new Date('2020-01-29'))).toEqual('2020-01-29'); + expect(pikadayToString(new Date('2020-01-29:00:00'))).toEqual('2020-01-29'); }); }); }); diff --git a/spec/javascripts/merge_request_tabs_spec.js b/spec/javascripts/merge_request_tabs_spec.js index e441d1153ed..5076435e7a8 100644 --- a/spec/javascripts/merge_request_tabs_spec.js +++ b/spec/javascripts/merge_request_tabs_spec.js @@ -1,6 +1,7 @@ /* eslint-disable no-var, comma-dangle, object-shorthand */ /* global Notes */ +import * as urlUtils from '~/lib/utils/url_utility'; import '~/merge_request_tabs'; import '~/commit/pipelines/pipelines_bundle'; import '~/breakpoints'; @@ -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_note_spec.js b/spec/javascripts/notes/components/issue_note_spec.js index 73fd188dbe5..bd888b2cbae 100644 --- a/spec/javascripts/notes/components/issue_note_spec.js +++ b/spec/javascripts/notes/components/issue_note_spec.js @@ -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_spec.js b/spec/javascripts/notes_spec.js index 677a389b88f..2612c5fd7bc 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); diff --git a/spec/javascripts/pager_spec.js b/spec/javascripts/pager_spec.js index 1d3e1263371..fe3ea996eac 100644 --- a/spec/javascripts/pager_spec.js +++ b/spec/javascripts/pager_spec.js @@ -1,5 +1,6 @@ /* global fixture */ +import * as utils from '~/lib/utils/url_utility'; import '~/pager'; describe('pager', () => { @@ -30,7 +31,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 +45,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/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/search_autocomplete_spec.js b/spec/javascripts/search_autocomplete_spec.js index a2394857b82..1e4c2c9faad 100644 --- a/spec/javascripts/search_autocomplete_spec.js +++ b/spec/javascripts/search_autocomplete_spec.js @@ -3,6 +3,7 @@ import '~/gl_dropdown'; import '~/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,7 +122,7 @@ 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; @@ -191,8 +192,6 @@ import '~/lib/utils/common_utils'; // browsers will not trigger default behavior (form submit, in this // example) on JavaScript-created keypresses. expect(submitSpy).not.toHaveBeenTriggered(); - // Does a worse job at capturing the intent of the test, but works. - expect(enterKeyEvent.isDefaultPrevented()).toBe(true); }); }); }).call(window); 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/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..d5b8947c86f 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,4 +1,5 @@ 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'; @@ -108,13 +109,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/popup_dialog_spec.js b/spec/javascripts/vue_shared/components/popup_dialog_spec.js new file mode 100644 index 00000000000..5c1d2a196f4 --- /dev/null +++ b/spec/javascripts/vue_shared/components/popup_dialog_spec.js @@ -0,0 +1,12 @@ +import Vue from 'vue'; +import PopupDialog from '~/vue_shared/components/popup_dialog.vue'; +import mountComponent from '../../helpers/vue_mount_component_helper'; + +describe('PopupDialog', () => { + it('does not render a primary button if no primaryButtonLabel', () => { + const popupDialog = Vue.extend(PopupDialog); + const vm = mountComponent(popupDialog); + + expect(vm.$el.querySelector('.js-primary-button')).toBeNull(); + }); +}); diff --git a/spec/lib/gitlab/backup/manager_spec.rb b/spec/lib/backup/manager_spec.rb index b68301a066a..b68301a066a 100644 --- a/spec/lib/gitlab/backup/manager_spec.rb +++ b/spec/lib/backup/manager_spec.rb diff --git a/spec/lib/backup/repository_spec.rb b/spec/lib/backup/repository_spec.rb new file mode 100644 index 00000000000..6ee3d531d6e --- /dev/null +++ b/spec/lib/backup/repository_spec.rb @@ -0,0 +1,69 @@ +require 'spec_helper' + +describe Backup::Repository do + let(:progress) { StringIO.new } + let!(:project) { create(:project) } + + before do + allow(progress).to receive(:puts) + allow(progress).to receive(:print) + + allow_any_instance_of(String).to receive(:color) do |string, _color| + string + end + + allow_any_instance_of(described_class).to receive(:progress).and_return(progress) + end + + describe '#dump' do + describe 'repo failure' do + before do + allow(Gitlab::Popen).to receive(:popen).and_return(['normal output', 0]) + end + + it 'does not raise error' do + expect { described_class.new.dump }.not_to raise_error + end + end + end + + describe '#restore' do + describe 'command failure' do + before do + allow(Gitlab::Popen).to receive(:popen).and_return(['error', 1]) + end + + it 'shows the appropriate error' do + described_class.new.restore + + expect(progress).to have_received(:puts).with("Ignoring error on #{project.full_path} - error") + end + end + end + + describe '#empty_repo?' do + context 'for a wiki' do + let(:wiki) { create(:project_wiki) } + + it 'invalidates the emptiness cache' do + expect(wiki.repository).to receive(:expire_emptiness_caches).once + + wiki.empty? + end + + context 'wiki repo has content' do + let!(:wiki_page) { create(:wiki_page, wiki: wiki) } + + it 'returns true, regardless of bad cache value' do + expect(described_class.new.send(:empty_repo?, wiki)).to be(false) + end + end + + context 'wiki repo does not have content' do + it 'returns true, regardless of bad cache value' do + expect(described_class.new.send(:empty_repo?, wiki)).to be_truthy + end + end + end + end +end diff --git a/spec/lib/banzai/cross_project_reference_spec.rb b/spec/lib/banzai/cross_project_reference_spec.rb index d70749536b8..68ca960caab 100644 --- a/spec/lib/banzai/cross_project_reference_spec.rb +++ b/spec/lib/banzai/cross_project_reference_spec.rb @@ -3,20 +3,20 @@ require 'spec_helper' describe Banzai::CrossProjectReference do include described_class - describe '#project_from_ref' do + describe '#parent_from_ref' do context 'when no project was referenced' do it 'returns the project from context' do project = double allow(self).to receive(:context).and_return({ project: project }) - expect(project_from_ref(nil)).to eq project + expect(parent_from_ref(nil)).to eq project end end context 'when referenced project does not exist' do it 'returns nil' do - expect(project_from_ref('invalid/reference')).to be_nil + expect(parent_from_ref('invalid/reference')).to be_nil end end @@ -27,7 +27,7 @@ describe Banzai::CrossProjectReference do expect(Project).to receive(:find_by_full_path) .with('cross/reference').and_return(project2) - expect(project_from_ref('cross/reference')).to eq project2 + expect(parent_from_ref('cross/reference')).to eq project2 end end end diff --git a/spec/lib/banzai/filter/abstract_reference_filter_spec.rb b/spec/lib/banzai/filter/abstract_reference_filter_spec.rb index 7c0ba9ee67f..1e82d18d056 100644 --- a/spec/lib/banzai/filter/abstract_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/abstract_reference_filter_spec.rb @@ -3,67 +3,67 @@ require 'spec_helper' describe Banzai::Filter::AbstractReferenceFilter do let(:project) { create(:project) } - describe '#references_per_project' do - it 'returns a Hash containing references grouped per project paths' do + describe '#references_per_parent' do + it 'returns a Hash containing references grouped per parent paths' do doc = Nokogiri::HTML.fragment("#1 #{project.full_path}#2") filter = described_class.new(doc, project: project) expect(filter).to receive(:object_class).exactly(4).times.and_return(Issue) expect(filter).to receive(:object_sym).twice.and_return(:issue) - refs = filter.references_per_project + refs = filter.references_per_parent expect(refs).to be_an_instance_of(Hash) expect(refs[project.full_path]).to eq(Set.new(%w[1 2])) end end - describe '#projects_per_reference' do - it 'returns a Hash containing projects grouped per project paths' do + describe '#parent_per_reference' do + it 'returns a Hash containing projects grouped per parent paths' do doc = Nokogiri::HTML.fragment('') filter = described_class.new(doc, project: project) - expect(filter).to receive(:references_per_project) + expect(filter).to receive(:references_per_parent) .and_return({ project.full_path => Set.new(%w[1]) }) - expect(filter.projects_per_reference) + expect(filter.parent_per_reference) .to eq({ project.full_path => project }) end end - describe '#find_projects_for_paths' do + describe '#find_for_paths' do let(:doc) { Nokogiri::HTML.fragment('') } let(:filter) { described_class.new(doc, project: project) } context 'with RequestStore disabled' do it 'returns a list of Projects for a list of paths' do - expect(filter.find_projects_for_paths([project.full_path])) + expect(filter.find_for_paths([project.full_path])) .to eq([project]) end it "return an empty array for paths that don't exist" do - expect(filter.find_projects_for_paths(['nonexistent/project'])) + expect(filter.find_for_paths(['nonexistent/project'])) .to eq([]) end end context 'with RequestStore enabled', :request_store do it 'returns a list of Projects for a list of paths' do - expect(filter.find_projects_for_paths([project.full_path])) + expect(filter.find_for_paths([project.full_path])) .to eq([project]) end context "when no project with that path exists" do it "returns no value" do - expect(filter.find_projects_for_paths(['nonexistent/project'])) + expect(filter.find_for_paths(['nonexistent/project'])) .to eq([]) end it "adds the ref to the project refs cache" do project_refs_cache = {} - allow(filter).to receive(:project_refs_cache).and_return(project_refs_cache) + allow(filter).to receive(:refs_cache).and_return(project_refs_cache) - filter.find_projects_for_paths(['nonexistent/project']) + filter.find_for_paths(['nonexistent/project']) expect(project_refs_cache).to eq({ 'nonexistent/project' => nil }) end @@ -71,11 +71,11 @@ describe Banzai::Filter::AbstractReferenceFilter do context 'when the project refs cache includes nil values' do before do # adds { 'nonexistent/project' => nil } to cache - filter.project_from_ref_cached('nonexistent/project') + filter.from_ref_cached('nonexistent/project') end it "return an empty array for paths that don't exist" do - expect(filter.find_projects_for_paths(['nonexistent/project'])) + expect(filter.find_for_paths(['nonexistent/project'])) .to eq([]) end end @@ -83,12 +83,12 @@ describe Banzai::Filter::AbstractReferenceFilter do end end - describe '#current_project_path' do - it 'returns the path of the current project' do + describe '#current_parent_path' do + it 'returns the path of the current parent' do doc = Nokogiri::HTML.fragment('') filter = described_class.new(doc, project: project) - expect(filter.current_project_path).to eq(project.full_path) + expect(filter.current_parent_path).to eq(project.full_path) end end end diff --git a/spec/lib/banzai/filter/commit_reference_filter_spec.rb b/spec/lib/banzai/filter/commit_reference_filter_spec.rb index 702fcac0c6f..080a5f57da9 100644 --- a/spec/lib/banzai/filter/commit_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/commit_reference_filter_spec.rb @@ -92,6 +92,18 @@ describe Banzai::Filter::CommitReferenceFilter do expect(link).not_to match %r(https?://) expect(link).to eq urls.project_commit_url(project, reference, only_path: true) end + + context "in merge request context" do + let(:noteable) { create(:merge_request, target_project: project, source_project: project) } + let(:commit) { noteable.commits.first } + + it 'handles merge request contextual commit references' do + url = urls.diffs_project_merge_request_url(project, noteable, commit_id: commit.id) + doc = reference_filter("See #{reference}", noteable: noteable) + + expect(doc.css('a').first[:href]).to eq(url) + end + end end context 'cross-project / cross-namespace complete reference' do diff --git a/spec/lib/banzai/filter/issue_reference_filter_spec.rb b/spec/lib/banzai/filter/issue_reference_filter_spec.rb index f70c69ef588..3a5f52ea23f 100644 --- a/spec/lib/banzai/filter/issue_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/issue_reference_filter_spec.rb @@ -157,6 +157,12 @@ describe Banzai::Filter::IssueReferenceFilter do expect(doc.text).to eq("Fixed (#{project2.full_path}##{issue.iid}.)") end + it 'includes default classes' do + doc = reference_filter("Fixed (#{reference}.)") + + expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-issue has-tooltip' + end + it 'ignores invalid issue IDs on the referenced project' do exp = act = "Fixed #{invalidate_reference(reference)}" @@ -201,6 +207,12 @@ describe Banzai::Filter::IssueReferenceFilter do expect(doc.text).to eq("Fixed (#{project2.path}##{issue.iid}.)") end + it 'includes default classes' do + doc = reference_filter("Fixed (#{reference}.)") + + expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-issue has-tooltip' + end + it 'ignores invalid issue IDs on the referenced project' do exp = act = "Fixed #{invalidate_reference(reference)}" @@ -245,6 +257,12 @@ describe Banzai::Filter::IssueReferenceFilter do expect(doc.text).to eq("Fixed (#{project2.path}##{issue.iid}.)") end + it 'includes default classes' do + doc = reference_filter("Fixed (#{reference}.)") + + expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-issue has-tooltip' + end + it 'ignores invalid issue IDs on the referenced project' do exp = act = "Fixed #{invalidate_reference(reference)}" @@ -269,8 +287,15 @@ describe Banzai::Filter::IssueReferenceFilter do it 'links with adjacent text' do doc = reference_filter("Fixed (#{reference}.)") + expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(issue.to_reference(project))} \(comment 123\)<\/a>\.\)/) end + + it 'includes default classes' do + doc = reference_filter("Fixed (#{reference}.)") + + expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-issue has-tooltip' + end end context 'cross-project reference in link href' do @@ -291,8 +316,15 @@ describe Banzai::Filter::IssueReferenceFilter do it 'links with adjacent text' do doc = reference_filter("Fixed (#{reference_link}.)") + expect(doc.to_html).to match(/\(<a.+>Reference<\/a>\.\)/) end + + it 'includes default classes' do + doc = reference_filter("Fixed (#{reference_link}.)") + + expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-issue has-tooltip' + end end context 'cross-project URL in link href' do @@ -313,8 +345,15 @@ describe Banzai::Filter::IssueReferenceFilter do it 'links with adjacent text' do doc = reference_filter("Fixed (#{reference_link}.)") + expect(doc.to_html).to match(/\(<a.+>Reference<\/a>\.\)/) end + + it 'includes default classes' do + doc = reference_filter("Fixed (#{reference_link}.)") + + expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-issue has-tooltip' + end end context 'group context' do @@ -387,19 +426,19 @@ describe Banzai::Filter::IssueReferenceFilter do end end - describe '#issues_per_project' do + describe '#records_per_parent' do context 'using an internal issue tracker' do it 'returns a Hash containing the issues per project' do doc = Nokogiri::HTML.fragment('') filter = described_class.new(doc, project: project) - expect(filter).to receive(:projects_per_reference) + expect(filter).to receive(:parent_per_reference) .and_return({ project.full_path => project }) - expect(filter).to receive(:references_per_project) + expect(filter).to receive(:references_per_parent) .and_return({ project.full_path => Set.new([issue.iid]) }) - expect(filter.issues_per_project) + expect(filter.records_per_parent) .to eq({ project => { issue.iid => issue } }) end end 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/banzai/filter/upload_link_filter_spec.rb b/spec/lib/banzai/filter/upload_link_filter_spec.rb index 60a88e903ef..76bc0c36ab7 100644 --- a/spec/lib/banzai/filter/upload_link_filter_spec.rb +++ b/spec/lib/banzai/filter/upload_link_filter_spec.rb @@ -89,7 +89,35 @@ describe Banzai::Filter::UploadLinkFilter do end end - context 'when project context does not exist' do + context 'in group context' do + let(:upload_link) { link('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg') } + let(:group) { create(:group) } + let(:filter_context) { { project: nil, group: group } } + let(:relative_path) { "groups/#{group.full_path}/-/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg" } + + it 'rewrites the link correctly' do + doc = raw_filter(upload_link, filter_context) + + expect(doc.at_css('a')['href']).to eq("#{Gitlab.config.gitlab.url}/#{relative_path}") + end + + it 'rewrites the link correctly for subgroup' do + subgroup = create(:group, parent: group) + relative_path = "groups/#{subgroup.full_path}/-/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg" + + doc = raw_filter(upload_link, { project: nil, group: subgroup }) + + expect(doc.at_css('a')['href']).to eq("#{Gitlab.config.gitlab.url}/#{relative_path}") + end + + it 'does not modify absolute URL' do + doc = filter(link('http://example.com'), filter_context) + + expect(doc.at_css('a')['href']).to eq 'http://example.com' + end + end + + context 'when project or group context does not exist' do let(:upload_link) { link('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg') } it 'does not raise error' do diff --git a/spec/lib/banzai/reference_parser/issue_parser_spec.rb b/spec/lib/banzai/reference_parser/issue_parser_spec.rb index 23dbe2b6238..4cef3bdb24b 100644 --- a/spec/lib/banzai/reference_parser/issue_parser_spec.rb +++ b/spec/lib/banzai/reference_parser/issue_parser_spec.rb @@ -70,12 +70,12 @@ describe Banzai::ReferenceParser::IssueParser do end end - describe '#issues_for_nodes' do + describe '#records_for_nodes' do it 'returns a Hash containing the issues for a list of nodes' do link['data-issue'] = issue.id.to_s nodes = [link] - expect(subject.issues_for_nodes(nodes)).to eq({ link => issue }) + expect(subject.records_for_nodes(nodes)).to eq({ link => issue }) end end end diff --git a/spec/lib/gitlab/background_migration/populate_untracked_uploads_spec.rb b/spec/lib/gitlab/background_migration/populate_untracked_uploads_spec.rb new file mode 100644 index 00000000000..b80df6956b0 --- /dev/null +++ b/spec/lib/gitlab/background_migration/populate_untracked_uploads_spec.rb @@ -0,0 +1,510 @@ +require 'spec_helper' + +describe Gitlab::BackgroundMigration::PopulateUntrackedUploads, :sidekiq do + include TrackUntrackedUploadsHelpers + + subject { described_class.new } + + let!(:untracked_files_for_uploads) { described_class::UntrackedFile } + let!(:uploads) { described_class::Upload } + + before do + DatabaseCleaner.clean + drop_temp_table_if_exists + ensure_temporary_tracking_table_exists + uploads.delete_all + end + + after(:all) do + drop_temp_table_if_exists + end + + context 'with untracked files and tracked files in untracked_files_for_uploads' do + let!(:appearance) { create_or_update_appearance(logo: uploaded_file, header_logo: uploaded_file) } + let!(:user1) { create(:user, :with_avatar) } + let!(:user2) { create(:user, :with_avatar) } + let!(:project1) { create(:project, :with_avatar) } + let!(:project2) { create(:project, :with_avatar) } + + before do + UploadService.new(project1, uploaded_file, FileUploader).execute # Markdown upload + UploadService.new(project2, uploaded_file, FileUploader).execute # Markdown upload + + # File records created by PrepareUntrackedUploads + untracked_files_for_uploads.create!(path: appearance.uploads.first.path) + untracked_files_for_uploads.create!(path: appearance.uploads.last.path) + untracked_files_for_uploads.create!(path: user1.uploads.first.path) + untracked_files_for_uploads.create!(path: user2.uploads.first.path) + untracked_files_for_uploads.create!(path: project1.uploads.first.path) + untracked_files_for_uploads.create!(path: project2.uploads.first.path) + untracked_files_for_uploads.create!(path: "#{Gitlab::BackgroundMigration::PrepareUntrackedUploads::RELATIVE_UPLOAD_DIR}/#{project1.full_path}/#{project1.uploads.last.path}") + untracked_files_for_uploads.create!(path: "#{Gitlab::BackgroundMigration::PrepareUntrackedUploads::RELATIVE_UPLOAD_DIR}/#{project2.full_path}/#{project2.uploads.last.path}") + + # Untrack 4 files + user2.uploads.delete_all + project2.uploads.delete_all # 2 files: avatar and a Markdown upload + appearance.uploads.where("path like '%header_logo%'").delete_all + end + + it 'adds untracked files to the uploads table' do + expect do + subject.perform(1, untracked_files_for_uploads.last.id) + end.to change { uploads.count }.from(4).to(8) + + expect(user2.uploads.count).to eq(1) + expect(project2.uploads.count).to eq(2) + expect(appearance.uploads.count).to eq(2) + end + + it 'deletes rows after processing them' do + expect(subject).to receive(:drop_temp_table_if_finished) # Don't drop the table so we can look at it + + expect do + subject.perform(1, untracked_files_for_uploads.last.id) + end.to change { untracked_files_for_uploads.count }.from(8).to(0) + end + + it 'does not create duplicate uploads of already tracked files' do + subject.perform(1, untracked_files_for_uploads.last.id) + + expect(user1.uploads.count).to eq(1) + expect(project1.uploads.count).to eq(2) + expect(appearance.uploads.count).to eq(2) + end + + it 'uses the start and end batch ids [only 1st half]' do + ids = untracked_files_for_uploads.all.order(:id).pluck(:id) + start_id = ids[0] + end_id = ids[3] + + expect do + subject.perform(start_id, end_id) + end.to change { uploads.count }.from(4).to(6) + + expect(user1.uploads.count).to eq(1) + expect(user2.uploads.count).to eq(1) + expect(appearance.uploads.count).to eq(2) + expect(project1.uploads.count).to eq(2) + expect(project2.uploads.count).to eq(0) + + # Only 4 have been either confirmed or added to uploads + expect(untracked_files_for_uploads.count).to eq(4) + end + + it 'uses the start and end batch ids [only 2nd half]' do + ids = untracked_files_for_uploads.all.order(:id).pluck(:id) + start_id = ids[4] + end_id = ids[7] + + expect do + subject.perform(start_id, end_id) + end.to change { uploads.count }.from(4).to(6) + + expect(user1.uploads.count).to eq(1) + expect(user2.uploads.count).to eq(0) + expect(appearance.uploads.count).to eq(1) + expect(project1.uploads.count).to eq(2) + expect(project2.uploads.count).to eq(2) + + # Only 4 have been either confirmed or added to uploads + expect(untracked_files_for_uploads.count).to eq(4) + end + + it 'does not drop the temporary tracking table after processing the batch, if there are still untracked rows' do + subject.perform(1, untracked_files_for_uploads.last.id - 1) + + expect(ActiveRecord::Base.connection.table_exists?(:untracked_files_for_uploads)).to be_truthy + end + + it 'drops the temporary tracking table after processing the batch, if there are no untracked rows left' do + subject.perform(1, untracked_files_for_uploads.last.id) + + expect(ActiveRecord::Base.connection.table_exists?(:untracked_files_for_uploads)).to be_falsey + end + + it 'does not block a whole batch because of one bad path' do + untracked_files_for_uploads.create!(path: "#{Gitlab::BackgroundMigration::PrepareUntrackedUploads::RELATIVE_UPLOAD_DIR}/#{project2.full_path}/._7d37bf4c747916390e596744117d5d1a") + expect(untracked_files_for_uploads.count).to eq(9) + expect(uploads.count).to eq(4) + + subject.perform(1, untracked_files_for_uploads.last.id) + + expect(untracked_files_for_uploads.count).to eq(1) + expect(uploads.count).to eq(8) + end + + it 'an unparseable path is shown in error output' do + bad_path = "#{Gitlab::BackgroundMigration::PrepareUntrackedUploads::RELATIVE_UPLOAD_DIR}/#{project2.full_path}/._7d37bf4c747916390e596744117d5d1a" + untracked_files_for_uploads.create!(path: bad_path) + + expect(Rails.logger).to receive(:error).with(/Error parsing path "#{bad_path}":/) + + subject.perform(1, untracked_files_for_uploads.last.id) + end + end + + context 'with no untracked files' do + it 'does not add to the uploads table (and does not raise error)' do + expect do + subject.perform(1, 1000) + end.not_to change { uploads.count }.from(0) + end + end + + describe 'upload outcomes for each path pattern' do + shared_examples_for 'non_markdown_file' do + let!(:expected_upload_attrs) { model.uploads.first.attributes.slice('path', 'uploader', 'size', 'checksum') } + let!(:untracked_file) { untracked_files_for_uploads.create!(path: expected_upload_attrs['path']) } + + before do + model.uploads.delete_all + end + + it 'creates an Upload record' do + expect do + subject.perform(1, untracked_files_for_uploads.last.id) + end.to change { model.reload.uploads.count }.from(0).to(1) + + expect(model.uploads.first.attributes).to include(expected_upload_attrs) + end + end + + context 'for an appearance logo file path' do + let(:model) { create_or_update_appearance(logo: uploaded_file) } + + it_behaves_like 'non_markdown_file' + end + + context 'for an appearance header_logo file path' do + let(:model) { create_or_update_appearance(header_logo: uploaded_file) } + + it_behaves_like 'non_markdown_file' + end + + context 'for a pre-Markdown Note attachment file path' do + class Note < ActiveRecord::Base + has_many :uploads, as: :model, dependent: :destroy + end + + let(:model) { create(:note, :with_attachment) } + + it_behaves_like 'non_markdown_file' + end + + context 'for a user avatar file path' do + let(:model) { create(:user, :with_avatar) } + + it_behaves_like 'non_markdown_file' + end + + context 'for a group avatar file path' do + let(:model) { create(:group, :with_avatar) } + + it_behaves_like 'non_markdown_file' + end + + context 'for a project avatar file path' do + let(:model) { create(:project, :with_avatar) } + + it_behaves_like 'non_markdown_file' + end + + context 'for a project Markdown attachment (notes, issues, MR descriptions) file path' do + let(:model) { create(:project) } + + before do + # Upload the file + UploadService.new(model, uploaded_file, FileUploader).execute + + # Create the untracked_files_for_uploads record + untracked_files_for_uploads.create!(path: "#{Gitlab::BackgroundMigration::PrepareUntrackedUploads::RELATIVE_UPLOAD_DIR}/#{model.full_path}/#{model.uploads.first.path}") + + # Save the expected upload attributes + @expected_upload_attrs = model.reload.uploads.first.attributes.slice('path', 'uploader', 'size', 'checksum') + + # Untrack the file + model.reload.uploads.delete_all + end + + it 'creates an Upload record' do + expect do + subject.perform(1, untracked_files_for_uploads.last.id) + end.to change { model.reload.uploads.count }.from(0).to(1) + + expect(model.uploads.first.attributes).to include(@expected_upload_attrs) + end + end + end +end + +describe Gitlab::BackgroundMigration::PopulateUntrackedUploads::UntrackedFile do + include TrackUntrackedUploadsHelpers + + let(:upload_class) { Gitlab::BackgroundMigration::PopulateUntrackedUploads::Upload } + + before(:all) do + ensure_temporary_tracking_table_exists + end + + after(:all) do + drop_temp_table_if_exists + end + + describe '#upload_path' do + def assert_upload_path(file_path, expected_upload_path) + untracked_file = create_untracked_file(file_path) + + expect(untracked_file.upload_path).to eq(expected_upload_path) + end + + context 'for an appearance logo file path' do + it 'returns the file path relative to the CarrierWave root' do + assert_upload_path('/-/system/appearance/logo/1/some_logo.jpg', 'uploads/-/system/appearance/logo/1/some_logo.jpg') + end + end + + context 'for an appearance header_logo file path' do + it 'returns the file path relative to the CarrierWave root' do + assert_upload_path('/-/system/appearance/header_logo/1/some_logo.jpg', 'uploads/-/system/appearance/header_logo/1/some_logo.jpg') + end + end + + context 'for a pre-Markdown Note attachment file path' do + it 'returns the file path relative to the CarrierWave root' do + assert_upload_path('/-/system/note/attachment/1234/some_attachment.pdf', 'uploads/-/system/note/attachment/1234/some_attachment.pdf') + end + end + + context 'for a user avatar file path' do + it 'returns the file path relative to the CarrierWave root' do + assert_upload_path('/-/system/user/avatar/1234/avatar.jpg', 'uploads/-/system/user/avatar/1234/avatar.jpg') + end + end + + context 'for a group avatar file path' do + it 'returns the file path relative to the CarrierWave root' do + assert_upload_path('/-/system/group/avatar/1234/avatar.jpg', 'uploads/-/system/group/avatar/1234/avatar.jpg') + end + end + + context 'for a project avatar file path' do + it 'returns the file path relative to the CarrierWave root' do + assert_upload_path('/-/system/project/avatar/1234/avatar.jpg', 'uploads/-/system/project/avatar/1234/avatar.jpg') + end + end + + context 'for a project Markdown attachment (notes, issues, MR descriptions) file path' do + it 'returns the file path relative to the project directory in uploads' do + project = create(:project) + random_hex = SecureRandom.hex + + assert_upload_path("/#{project.full_path}/#{random_hex}/Some file.jpg", "#{random_hex}/Some file.jpg") + end + end + end + + describe '#uploader' do + def assert_uploader(file_path, expected_uploader) + untracked_file = create_untracked_file(file_path) + + expect(untracked_file.uploader).to eq(expected_uploader) + end + + context 'for an appearance logo file path' do + it 'returns AttachmentUploader as a string' do + assert_uploader('/-/system/appearance/logo/1/some_logo.jpg', 'AttachmentUploader') + end + end + + context 'for an appearance header_logo file path' do + it 'returns AttachmentUploader as a string' do + assert_uploader('/-/system/appearance/header_logo/1/some_logo.jpg', 'AttachmentUploader') + end + end + + context 'for a pre-Markdown Note attachment file path' do + it 'returns AttachmentUploader as a string' do + assert_uploader('/-/system/note/attachment/1234/some_attachment.pdf', 'AttachmentUploader') + end + end + + context 'for a user avatar file path' do + it 'returns AvatarUploader as a string' do + assert_uploader('/-/system/user/avatar/1234/avatar.jpg', 'AvatarUploader') + end + end + + context 'for a group avatar file path' do + it 'returns AvatarUploader as a string' do + assert_uploader('/-/system/group/avatar/1234/avatar.jpg', 'AvatarUploader') + end + end + + context 'for a project avatar file path' do + it 'returns AvatarUploader as a string' do + assert_uploader('/-/system/project/avatar/1234/avatar.jpg', 'AvatarUploader') + end + end + + context 'for a project Markdown attachment (notes, issues, MR descriptions) file path' do + it 'returns FileUploader as a string' do + project = create(:project) + + assert_uploader("/#{project.full_path}/#{SecureRandom.hex}/Some file.jpg", 'FileUploader') + end + end + end + + describe '#model_type' do + def assert_model_type(file_path, expected_model_type) + untracked_file = create_untracked_file(file_path) + + expect(untracked_file.model_type).to eq(expected_model_type) + end + + context 'for an appearance logo file path' do + it 'returns Appearance as a string' do + assert_model_type('/-/system/appearance/logo/1/some_logo.jpg', 'Appearance') + end + end + + context 'for an appearance header_logo file path' do + it 'returns Appearance as a string' do + assert_model_type('/-/system/appearance/header_logo/1/some_logo.jpg', 'Appearance') + end + end + + context 'for a pre-Markdown Note attachment file path' do + it 'returns Note as a string' do + assert_model_type('/-/system/note/attachment/1234/some_attachment.pdf', 'Note') + end + end + + context 'for a user avatar file path' do + it 'returns User as a string' do + assert_model_type('/-/system/user/avatar/1234/avatar.jpg', 'User') + end + end + + context 'for a group avatar file path' do + it 'returns Namespace as a string' do + assert_model_type('/-/system/group/avatar/1234/avatar.jpg', 'Namespace') + end + end + + context 'for a project avatar file path' do + it 'returns Project as a string' do + assert_model_type('/-/system/project/avatar/1234/avatar.jpg', 'Project') + end + end + + context 'for a project Markdown attachment (notes, issues, MR descriptions) file path' do + it 'returns Project as a string' do + project = create(:project) + + assert_model_type("/#{project.full_path}/#{SecureRandom.hex}/Some file.jpg", 'Project') + end + end + end + + describe '#model_id' do + def assert_model_id(file_path, expected_model_id) + untracked_file = create_untracked_file(file_path) + + expect(untracked_file.model_id).to eq(expected_model_id) + end + + context 'for an appearance logo file path' do + it 'returns the ID as a string' do + assert_model_id('/-/system/appearance/logo/1/some_logo.jpg', 1) + end + end + + context 'for an appearance header_logo file path' do + it 'returns the ID as a string' do + assert_model_id('/-/system/appearance/header_logo/1/some_logo.jpg', 1) + end + end + + context 'for a pre-Markdown Note attachment file path' do + it 'returns the ID as a string' do + assert_model_id('/-/system/note/attachment/1234/some_attachment.pdf', 1234) + end + end + + context 'for a user avatar file path' do + it 'returns the ID as a string' do + assert_model_id('/-/system/user/avatar/1234/avatar.jpg', 1234) + end + end + + context 'for a group avatar file path' do + it 'returns the ID as a string' do + assert_model_id('/-/system/group/avatar/1234/avatar.jpg', 1234) + end + end + + context 'for a project avatar file path' do + it 'returns the ID as a string' do + assert_model_id('/-/system/project/avatar/1234/avatar.jpg', 1234) + end + end + + context 'for a project Markdown attachment (notes, issues, MR descriptions) file path' do + it 'returns the ID as a string' do + project = create(:project) + + assert_model_id("/#{project.full_path}/#{SecureRandom.hex}/Some file.jpg", project.id) + end + end + end + + describe '#file_size' do + context 'for an appearance logo file path' do + let(:appearance) { create_or_update_appearance(logo: uploaded_file) } + let(:untracked_file) { described_class.create!(path: appearance.uploads.first.path) } + + it 'returns the file size' do + expect(untracked_file.file_size).to eq(35255) + end + + it 'returns the same thing that CarrierWave would return' do + expect(untracked_file.file_size).to eq(appearance.logo.size) + end + end + + context 'for a project avatar file path' do + let(:project) { create(:project, avatar: uploaded_file) } + let(:untracked_file) { described_class.create!(path: project.uploads.first.path) } + + it 'returns the file size' do + expect(untracked_file.file_size).to eq(35255) + end + + it 'returns the same thing that CarrierWave would return' do + expect(untracked_file.file_size).to eq(project.avatar.size) + end + end + + context 'for a project Markdown attachment (notes, issues, MR descriptions) file path' do + let(:project) { create(:project) } + let(:untracked_file) { create_untracked_file("/#{project.full_path}/#{project.uploads.first.path}") } + + before do + UploadService.new(project, uploaded_file, FileUploader).execute + end + + it 'returns the file size' do + expect(untracked_file.file_size).to eq(35255) + end + + it 'returns the same thing that CarrierWave would return' do + expect(untracked_file.file_size).to eq(project.uploads.first.size) + end + end + end + + def create_untracked_file(path_relative_to_upload_dir) + described_class.create!(path: "#{Gitlab::BackgroundMigration::PrepareUntrackedUploads::RELATIVE_UPLOAD_DIR}#{path_relative_to_upload_dir}") + end +end diff --git a/spec/lib/gitlab/background_migration/prepare_untracked_uploads_spec.rb b/spec/lib/gitlab/background_migration/prepare_untracked_uploads_spec.rb new file mode 100644 index 00000000000..cd3f1a45270 --- /dev/null +++ b/spec/lib/gitlab/background_migration/prepare_untracked_uploads_spec.rb @@ -0,0 +1,242 @@ +require 'spec_helper' + +describe Gitlab::BackgroundMigration::PrepareUntrackedUploads, :sidekiq do + include TrackUntrackedUploadsHelpers + + let!(:untracked_files_for_uploads) { described_class::UntrackedFile } + + matcher :be_scheduled_migration do |*expected| + match do |migration| + BackgroundMigrationWorker.jobs.any? do |job| + job['args'] == [migration, expected] + end + end + + failure_message do |migration| + "Migration `#{migration}` with args `#{expected.inspect}` not scheduled!" + end + end + + before do + DatabaseCleaner.clean + + drop_temp_table_if_exists + end + + after do + drop_temp_table_if_exists + end + + around do |example| + # Especially important so the follow-up migration does not get run + Sidekiq::Testing.fake! do + example.run + end + end + + it 'ensures the untracked_files_for_uploads table exists' do + expect do + described_class.new.perform + end.to change { ActiveRecord::Base.connection.table_exists?(:untracked_files_for_uploads) }.from(false).to(true) + end + + it 'has a path field long enough for really long paths' do + described_class.new.perform + + component = 'a' * 255 + + long_path = [ + 'uploads', + component, # project.full_path + component # filename + ].flatten.join('/') + + record = untracked_files_for_uploads.create!(path: long_path) + expect(record.reload.path.size).to eq(519) + end + + context "test bulk insert with ON CONFLICT DO NOTHING or IGNORE" do + around do |example| + # If this is CI, we use Postgres 9.2 so this whole context should be + # skipped since we're unable to use ON CONFLICT DO NOTHING or IGNORE. + if described_class.new.send(:can_bulk_insert_and_ignore_duplicates?) + example.run + end + end + + context 'when files were uploaded before and after hashed storage was enabled' do + let!(:appearance) { create_or_update_appearance(logo: uploaded_file, header_logo: uploaded_file) } + let!(:user) { create(:user, :with_avatar) } + let!(:project1) { create(:project, :with_avatar) } + let(:project2) { create(:project) } # instantiate after enabling hashed_storage + + before do + # Markdown upload before enabling hashed_storage + UploadService.new(project1, uploaded_file, FileUploader).execute + + stub_application_setting(hashed_storage_enabled: true) + + # Markdown upload after enabling hashed_storage + UploadService.new(project2, uploaded_file, FileUploader).execute + end + + it 'adds unhashed files to the untracked_files_for_uploads table' do + described_class.new.perform + + expect(untracked_files_for_uploads.count).to eq(5) + end + + it 'adds files with paths relative to CarrierWave.root' do + described_class.new.perform + untracked_files_for_uploads.all.each do |file| + expect(file.path.start_with?('uploads/')).to be_truthy + end + end + + it 'does not add hashed files to the untracked_files_for_uploads table' do + described_class.new.perform + + hashed_file_path = project2.uploads.where(uploader: 'FileUploader').first.path + expect(untracked_files_for_uploads.where("path like '%#{hashed_file_path}%'").exists?).to be_falsey + end + + it 'correctly schedules the follow-up background migration jobs' do + described_class.new.perform + + expect(described_class::FOLLOW_UP_MIGRATION).to be_scheduled_migration(1, 5) + expect(BackgroundMigrationWorker.jobs.size).to eq(1) + end + + # E.g. from a previous failed run of this background migration + context 'when there is existing data in untracked_files_for_uploads' do + before do + described_class.new.perform + end + + it 'does not error or produce duplicates of existing data' do + expect do + described_class.new.perform + end.not_to change { untracked_files_for_uploads.count }.from(5) + end + end + + # E.g. The installation is in use at the time of migration, and someone has + # just uploaded a file + context 'when there are files in /uploads/tmp' do + let(:tmp_file) { Rails.root.join(described_class::ABSOLUTE_UPLOAD_DIR, 'tmp', 'some_file.jpg') } + + before do + FileUtils.touch(tmp_file) + end + + after do + FileUtils.rm(tmp_file) + end + + it 'does not add files from /uploads/tmp' do + described_class.new.perform + + expect(untracked_files_for_uploads.count).to eq(5) + end + end + end + end + + context 'test bulk insert without ON CONFLICT DO NOTHING or IGNORE' do + before do + # If this is CI, we use Postgres 9.2 so this stub has no effect. + # + # If this is being run on Postgres 9.5+ or MySQL, then this stub allows us + # to test the bulk insert functionality without ON CONFLICT DO NOTHING or + # IGNORE. + allow_any_instance_of(described_class).to receive(:postgresql_pre_9_5?).and_return(true) + end + + context 'when files were uploaded before and after hashed storage was enabled' do + let!(:appearance) { create_or_update_appearance(logo: uploaded_file, header_logo: uploaded_file) } + let!(:user) { create(:user, :with_avatar) } + let!(:project1) { create(:project, :with_avatar) } + let(:project2) { create(:project) } # instantiate after enabling hashed_storage + + before do + # Markdown upload before enabling hashed_storage + UploadService.new(project1, uploaded_file, FileUploader).execute + + stub_application_setting(hashed_storage_enabled: true) + + # Markdown upload after enabling hashed_storage + UploadService.new(project2, uploaded_file, FileUploader).execute + end + + it 'adds unhashed files to the untracked_files_for_uploads table' do + described_class.new.perform + + expect(untracked_files_for_uploads.count).to eq(5) + end + + it 'adds files with paths relative to CarrierWave.root' do + described_class.new.perform + untracked_files_for_uploads.all.each do |file| + expect(file.path.start_with?('uploads/')).to be_truthy + end + end + + it 'does not add hashed files to the untracked_files_for_uploads table' do + described_class.new.perform + + hashed_file_path = project2.uploads.where(uploader: 'FileUploader').first.path + expect(untracked_files_for_uploads.where("path like '%#{hashed_file_path}%'").exists?).to be_falsey + end + + it 'correctly schedules the follow-up background migration jobs' do + described_class.new.perform + + expect(described_class::FOLLOW_UP_MIGRATION).to be_scheduled_migration(1, 5) + expect(BackgroundMigrationWorker.jobs.size).to eq(1) + end + + # E.g. from a previous failed run of this background migration + context 'when there is existing data in untracked_files_for_uploads' do + before do + described_class.new.perform + end + + it 'does not error or produce duplicates of existing data' do + expect do + described_class.new.perform + end.not_to change { untracked_files_for_uploads.count }.from(5) + end + end + + # E.g. The installation is in use at the time of migration, and someone has + # just uploaded a file + context 'when there are files in /uploads/tmp' do + let(:tmp_file) { Rails.root.join(described_class::ABSOLUTE_UPLOAD_DIR, 'tmp', 'some_file.jpg') } + + before do + FileUtils.touch(tmp_file) + end + + after do + FileUtils.rm(tmp_file) + end + + it 'does not add files from /uploads/tmp' do + described_class.new.perform + + expect(untracked_files_for_uploads.count).to eq(5) + end + end + end + end + + # Very new or lightly-used installations that are running this migration + # may not have an upload directory because they have no uploads. + context 'when no files were ever uploaded' do + it 'does not add to the untracked_files_for_uploads table (and does not raise error)' do + described_class.new.perform + + expect(untracked_files_for_uploads.count).to eq(0) + end + end +end diff --git a/spec/lib/gitlab/backup/repository_spec.rb b/spec/lib/gitlab/backup/repository_spec.rb deleted file mode 100644 index 535cce12780..00000000000 --- a/spec/lib/gitlab/backup/repository_spec.rb +++ /dev/null @@ -1,117 +0,0 @@ -require 'spec_helper' - -describe Backup::Repository do - let(:progress) { StringIO.new } - let!(:project) { create(:project) } - - before do - allow(progress).to receive(:puts) - allow(progress).to receive(:print) - - allow_any_instance_of(String).to receive(:color) do |string, _color| - string - end - - allow_any_instance_of(described_class).to receive(:progress).and_return(progress) - end - - describe '#dump' do - describe 'repo failure' do - before do - allow_any_instance_of(Repository).to receive(:empty_repo?).and_raise(Rugged::OdbError) - allow(Gitlab::Popen).to receive(:popen).and_return(['normal output', 0]) - end - - it 'does not raise error' do - expect { described_class.new.dump }.not_to raise_error - end - - it 'shows the appropriate error' do - described_class.new.dump - - expect(progress).to have_received(:puts).with("Ignoring repository error and continuing backing up project: #{project.full_path} - Rugged::OdbError") - end - end - - describe 'command failure' do - before do - allow_any_instance_of(Repository).to receive(:empty_repo?).and_return(false) - allow(Gitlab::Popen).to receive(:popen).and_return(['error', 1]) - end - - it 'shows the appropriate error' do - described_class.new.dump - - expect(progress).to have_received(:puts).with("Ignoring error on #{project.full_path} - error") - end - end - end - - describe '#restore' do - describe 'command failure' do - before do - allow(Gitlab::Popen).to receive(:popen).and_return(['error', 1]) - end - - it 'shows the appropriate error' do - described_class.new.restore - - expect(progress).to have_received(:puts).with("Ignoring error on #{project.full_path} - error") - end - end - end - - describe '#empty_repo?' do - context 'for a wiki' do - let(:wiki) { create(:project_wiki) } - - context 'wiki repo has content' do - let!(:wiki_page) { create(:wiki_page, wiki: wiki) } - - before do - wiki.repository.exists? # initial cache - end - - context '`repository.exists?` is incorrectly cached as false' do - before do - repo = wiki.repository - repo.send(:cache).expire(:exists?) - repo.send(:cache).fetch(:exists?) { false } - repo.send(:instance_variable_set, :@exists, false) - end - - it 'returns false, regardless of bad cache value' do - expect(described_class.new.send(:empty_repo?, wiki)).to be_falsey - end - end - - context '`repository.exists?` is correctly cached as true' do - it 'returns false' do - expect(described_class.new.send(:empty_repo?, wiki)).to be_falsey - end - end - end - - context 'wiki repo does not have content' do - context '`repository.exists?` is incorrectly cached as true' do - before do - repo = wiki.repository - repo.send(:cache).expire(:exists?) - repo.send(:cache).fetch(:exists?) { true } - repo.send(:instance_variable_set, :@exists, true) - end - - it 'returns true, regardless of bad cache value' do - expect(described_class.new.send(:empty_repo?, wiki)).to be_truthy - end - end - - context '`repository.exists?` is correctly cached as false' do - it 'returns true' do - expect(described_class.new.send(:empty_repo?, wiki)).to be_truthy - end - end - end - end - end -end 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/database_spec.rb b/spec/lib/gitlab/database_spec.rb index a5657b81952..b2f13fae73f 100644 --- a/spec/lib/gitlab/database_spec.rb +++ b/spec/lib/gitlab/database_spec.rb @@ -221,6 +221,22 @@ describe Gitlab::Database do described_class.bulk_insert('test', rows) end + it 'does not quote values of a column in the disable_quote option' do + [1, 2, 4, 5].each do |i| + expect(connection).to receive(:quote).with(i) + end + + described_class.bulk_insert('test', rows, disable_quote: :c) + end + + it 'does not quote values of columns in the disable_quote option' do + [2, 5].each do |i| + expect(connection).to receive(:quote).with(i) + end + + described_class.bulk_insert('test', rows, disable_quote: [:a, :c]) + end + it 'handles non-UTF-8 data' do expect { described_class.bulk_insert('test', [{ a: "\255" }]) }.not_to raise_error end diff --git a/spec/lib/gitlab/diff/inline_diff_spec.rb b/spec/lib/gitlab/diff/inline_diff_spec.rb index 15451c2cf99..0a41362f606 100644 --- a/spec/lib/gitlab/diff/inline_diff_spec.rb +++ b/spec/lib/gitlab/diff/inline_diff_spec.rb @@ -31,6 +31,10 @@ describe Gitlab::Diff::InlineDiff do expect(subject[7]).to eq([17..17]) expect(subject[8]).to be_nil end + + it 'can handle unchanged empty lines' do + expect { described_class.for_lines(['- bar', '+ baz', '']) }.not_to raise_error + end end describe "#inline_diffs" do 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..51ce3116880 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, diff --git a/spec/lib/gitlab/git/remote_repository_spec.rb b/spec/lib/gitlab/git/remote_repository_spec.rb index 0506210887c..eb148cc3804 100644 --- a/spec/lib/gitlab/git/remote_repository_spec.rb +++ b/spec/lib/gitlab/git/remote_repository_spec.rb @@ -4,7 +4,7 @@ describe Gitlab::Git::RemoteRepository, seed_helper: true do let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '') } subject { described_class.new(repository) } - describe '#empty_repo?' do + describe '#empty?' do using RSpec::Parameterized::TableSyntax where(:repository, :result) do @@ -13,7 +13,7 @@ describe Gitlab::Git::RemoteRepository, seed_helper: true do end with_them do - it { expect(subject.empty_repo?).to eq(result) } + it { expect(subject.empty?).to eq(result) } end end diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index 4f24ca3c25f..e6845420f7d 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -257,7 +257,7 @@ describe Gitlab::Git::Repository, seed_helper: true do end describe '#empty?' do - it { expect(repository.empty?).to be_falsey } + it { expect(repository).not_to be_empty } end describe '#ref_names' do 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/git_spec.rb b/spec/lib/gitlab/git_spec.rb index 494dfe0e595..ce15057dd7d 100644 --- a/spec/lib/gitlab/git_spec.rb +++ b/spec/lib/gitlab/git_spec.rb @@ -38,4 +38,29 @@ describe Gitlab::Git do expect(described_class.ref_name(utf8_invalid_ref)).to eq("an_invalid_ref_Ã¥") end end + + describe '.shas_eql?' do + using RSpec::Parameterized::TableSyntax + + where(:sha1, :sha2, :result) do + sha = RepoHelpers.sample_commit.id + short_sha = sha[0, Gitlab::Git::Commit::MIN_SHA_LENGTH] + too_short_sha = sha[0, Gitlab::Git::Commit::MIN_SHA_LENGTH - 1] + + [ + [sha, sha, true], + [sha, short_sha, true], + [sha, sha.reverse, false], + [sha, too_short_sha, false], + [sha, nil, false] + ] + end + + with_them do + it { expect(described_class.shas_eql?(sha1, sha2)).to eq(result) } + it 'is commutative' do + expect(described_class.shas_eql?(sha2, sha1)).to eq(result) + end + 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/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/reference_extractor_spec.rb b/spec/lib/gitlab/reference_extractor_spec.rb index 476a3f1998d..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) @@ -250,4 +259,34 @@ describe Gitlab::ReferenceExtractor do subject { described_class.references_pattern } it { is_expected.to be_kind_of Regexp } end + + describe 'referables prefixes' do + def prefixes + described_class::REFERABLES.each_with_object({}) do |referable, result| + klass = referable.to_s.camelize.constantize + + next unless klass.respond_to?(:reference_prefix) + + prefix = klass.reference_prefix + result[prefix] ||= [] + result[prefix] << referable + end + end + + it 'returns all supported prefixes' do + expect(prefixes.keys.uniq).to match_array(%w(@ # ~ % ! $ &)) + end + + it 'does not allow one prefix for multiple referables if not allowed specificly' do + # make sure you are not overriding existing prefix before changing this hash + multiple_allowed = { + '@' => 3 + } + + prefixes.each do |prefix, referables| + expected_count = multiple_allowed[prefix] || 1 + expect(referables.count).to eq(expected_count) + end + 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/utils_spec.rb b/spec/lib/gitlab/utils_spec.rb index 3137a72fdc4..e872a5290c5 100644 --- a/spec/lib/gitlab/utils_spec.rb +++ b/spec/lib/gitlab/utils_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe Gitlab::Utils do - delegate :to_boolean, :boolean_to_yes_no, :slugify, :random_string, to: :described_class + delegate :to_boolean, :boolean_to_yes_no, :slugify, :random_string, :which, to: :described_class describe '.slugify' do { @@ -59,4 +59,12 @@ describe Gitlab::Utils do expect(random_string).to be_kind_of(String) end end + + describe '.which' do + it 'finds the full path to an executable binary' do + expect(File).to receive(:executable?).with('/bin/sh').and_return(true) + + expect(which('sh', 'PATH' => '/bin')).to eq('/bin/sh') + end + end end 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/migrations/track_untracked_uploads_spec.rb b/spec/migrations/track_untracked_uploads_spec.rb new file mode 100644 index 00000000000..7fe7a140e2f --- /dev/null +++ b/spec/migrations/track_untracked_uploads_spec.rb @@ -0,0 +1,27 @@ +require 'spec_helper' +require Rails.root.join('db', 'post_migrate', '20171103140253_track_untracked_uploads') + +describe TrackUntrackedUploads, :migration, :sidekiq do + include TrackUntrackedUploadsHelpers + + matcher :be_scheduled_migration do + match do |migration| + BackgroundMigrationWorker.jobs.any? do |job| + job['args'] == [migration] + end + end + + failure_message do |migration| + "Migration `#{migration}` with args `#{expected.inspect}` not scheduled!" + end + end + + it 'correctly schedules the follow-up background migration' do + Sidekiq::Testing.fake! do + migrate! + + expect(described_class::MIGRATION).to be_scheduled_migration + expect(BackgroundMigrationWorker.jobs.size).to eq(1) + 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 26d33663dad..a6258676767 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -1868,6 +1868,94 @@ describe Ci::Build do end end + 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) } + + it { expect { job.run! }.to raise_error(Ci::Build::MissingDependenciesError) } + 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) } + + it { expect { job.run! }.to raise_error(Ci::Build::MissingDependenciesError) } + end + + context 'when artifacts of depended job has been erased' do + let!(:pre_stage_job) { create(:ci_build, :success, pipeline: pipeline, name: 'test', stage_idx: 0, erased_at: 1.minute.ago) } + + before do + pre_stage_job.erase + end + + it { expect { job.run! }.to raise_error(Ci::Build::MissingDependenciesError) } + 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) } + + 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) } + + it { expect { job.run! }.not_to raise_error } + end + + context 'when artifacts of depended job has been erased' do + let!(:pre_stage_job) { create(:ci_build, :success, pipeline: pipeline, name: 'test', stage_idx: 0, erased_at: 1.minute.ago) } + + before do + pre_stage_job.erase + end + + it { expect { job.run! }.not_to raise_error } + end + end + + let!(:job) { create(:ci_build, :pending, pipeline: pipeline, stage_idx: 1, options: options) } + + context 'when validates for dependencies is enabled' do + before do + stub_feature_flags(ci_disable_validates_dependencies: false) + end + + let!(:pre_stage_job) { create(:ci_build, :success, pipeline: pipeline, name: 'test', stage_idx: 0) } + + context 'when "dependencies" keyword is not defined' do + let(:options) { {} } + + it { expect { job.run! }.not_to raise_error } + end + + context 'when "dependencies" keyword is empty' do + let(:options) { { dependencies: [] } } + + it { expect { job.run! }.not_to raise_error } + end + + context 'when "dependencies" keyword is specified' do + let(:options) { { dependencies: ['test'] } } + + it_behaves_like 'validation is active' + end + end + + context 'when validates for dependencies is disabled' do + let(:options) { { dependencies: ['test'] } } + + before do + stub_feature_flags(ci_disable_validates_dependencies: true) + end + + it_behaves_like 'validation is not active' + end + end + describe 'state transition when build fails' do let(:service) { MergeRequests::AddTodoWhenBuildFailsService.new(project, user) } diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index d4b1e7c8dd4..bb89e093890 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -1244,7 +1244,7 @@ describe Ci::Pipeline, :mailer do describe '#execute_hooks' do let!(:build_a) { create_build('a', 0) } - let!(:build_b) { create_build('b', 1) } + let!(:build_b) { create_build('b', 0) } let!(:hook) do create(:project_hook, project: project, pipeline_events: enabled) @@ -1300,6 +1300,8 @@ describe Ci::Pipeline, :mailer do end context 'when stage one failed' do + let!(:build_b) { create_build('b', 1) } + before do build_a.drop end diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb index a53b59c4e08..9df26f06a11 100644 --- a/spec/models/concerns/issuable_spec.rb +++ b/spec/models/concerns/issuable_spec.rb @@ -171,7 +171,7 @@ describe Issuable do it "returns false when record has been updated" do allow(issue).to receive(:today?).and_return(true) - issue.touch + issue.update_attribute(:updated_at, 1.hour.ago) expect(issue.new?).to be_falsey end end diff --git a/spec/models/diff_note_spec.rb b/spec/models/diff_note_spec.rb index 8389d5c5430..4d0b3245a13 100644 --- a/spec/models/diff_note_spec.rb +++ b/spec/models/diff_note_spec.rb @@ -9,13 +9,14 @@ describe DiffNote do let(:path) { "files/ruby/popen.rb" } + let(:diff_refs) { merge_request.diff_refs } let!(:position) do Gitlab::Diff::Position.new( old_path: path, new_path: path, old_line: nil, new_line: 14, - diff_refs: merge_request.diff_refs + diff_refs: diff_refs ) end @@ -25,7 +26,7 @@ describe DiffNote do new_path: path, old_line: 16, new_line: 22, - diff_refs: merge_request.diff_refs + diff_refs: diff_refs ) end @@ -158,25 +159,21 @@ describe DiffNote do describe "creation" do describe "updating of position" do context "when noteable is a commit" do - let(:diff_note) { create(:diff_note_on_commit, project: project, position: position) } + let(:diff_refs) { commit.diff_refs } - it "doesn't update the position" do - diff_note + subject { create(:diff_note_on_commit, project: project, position: position, commit_id: commit.id) } - expect(diff_note.original_position).to eq(position) - expect(diff_note.position).to eq(position) + it "doesn't update the position" do + is_expected.to have_attributes(original_position: position, + position: position) end end context "when noteable is a merge request" do - let(:diff_note) { create(:diff_note_on_merge_request, project: project, position: position, noteable: merge_request) } - context "when the note is active" do it "doesn't update the position" do - diff_note - - expect(diff_note.original_position).to eq(position) - expect(diff_note.position).to eq(position) + expect(subject.original_position).to eq(position) + expect(subject.position).to eq(position) end end @@ -186,10 +183,8 @@ describe DiffNote do end it "updates the position" do - diff_note - - expect(diff_note.original_position).to eq(position) - expect(diff_note.position).not_to eq(position) + expect(subject.original_position).to eq(position) + expect(subject.position).not_to eq(position) end end end diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb index 5f901262598..0ea287d007a 100644 --- a/spec/models/issue_spec.rb +++ b/spec/models/issue_spec.rb @@ -765,4 +765,8 @@ describe Issue do expect(described_class.public_only).to eq([public_issue]) end end + + it_behaves_like 'throttled touch' do + subject { create(:issue, updated_at: 1.hour.ago) } + end end diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index e606fb027b5..30a5a3bbff7 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -967,7 +967,7 @@ describe MergeRequest do end shared_examples 'returning all SHA' do - it 'returns all SHA from all merge_request_diffs' do + it 'returns all SHAs from all merge_request_diffs' do expect(subject.merge_request_diffs.size).to eq(2) expect(subject.all_commit_shas).to match_array(all_commit_shas) end @@ -1851,4 +1851,8 @@ describe MergeRequest do .to change { project.open_merge_requests_count }.from(1).to(0) end end + + it_behaves_like 'throttled touch' do + subject { create(:merge_request, updated_at: 1.hour.ago) } + 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 6e7e8c4c570..e1a0c55b6a6 100644 --- a/spec/models/note_spec.rb +++ b/spec/models/note_spec.rb @@ -5,7 +5,7 @@ describe Note do describe 'associations' do it { is_expected.to belong_to(:project) } - it { is_expected.to belong_to(:noteable).touch(true) } + it { is_expected.to belong_to(:noteable).touch(false) } it { is_expected.to belong_to(:author).class_name('User') } it { is_expected.to have_many(:todos).dependent(:destroy) } 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_spec.rb b/spec/models/project_spec.rb index bda1d1cb612..f4699fd243d 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -313,7 +313,6 @@ describe Project do it { is_expected.to delegate_method(method).to(:team) } end - it { is_expected.to delegate_method(:empty_repo?).to(:repository) } it { is_expected.to delegate_method(:members).to(:team).with_prefix(true) } it { is_expected.to delegate_method(:name).to(:owner).with_prefix(true).with_arguments(allow_nil: true) } end @@ -656,6 +655,24 @@ describe Project do end end + describe '#empty_repo?' do + context 'when the repo does not exist' do + let(:project) { build_stubbed(:project) } + + it 'returns true' do + expect(project.empty_repo?).to be(true) + end + end + + context 'when the repo exists' do + let(:project) { create(:project, :repository) } + let(:empty_project) { create(:project, :empty_repo) } + + it { expect(empty_project.empty_repo?).to be(true) } + it { expect(project.empty_repo?).to be(false) } + end + end + describe '#external_issue_tracker' do let(:project) { create(:project) } let(:ext_project) { create(:redmine_project) } diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index d37e3d2c527..358bc3dfb94 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 @@ -583,7 +585,7 @@ describe Repository do end it 'properly handles query when repo is empty' do - repository = create(:project).repository + repository = create(:project, :empty_repo).repository results = repository.search_files_by_content('test', 'master') expect(results).to match_array([]) @@ -619,7 +621,7 @@ describe Repository do end it 'properly handles query when repo is empty' do - repository = create(:project).repository + repository = create(:project, :empty_repo).repository results = repository.search_files_by_name('test', 'master') @@ -634,9 +636,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 @@ -1204,17 +1204,15 @@ describe Repository do let(:empty_repository) { create(:project_empty_repo).repository } it 'returns true for an empty repository' do - expect(empty_repository.empty?).to eq(true) + expect(empty_repository).to be_empty end it 'returns false for a non-empty repository' do - expect(repository.empty?).to eq(false) + expect(repository).not_to be_empty end it 'caches the output' do - expect(repository.raw_repository).to receive(:empty?) - .once - .and_return(false) + expect(repository.raw_repository).to receive(:has_visible_content?).once repository.empty? repository.empty? 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 b27c1b2cd1a..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 @@ -2433,4 +2412,187 @@ describe User do expect(user).not_to be_blocked end end + + describe '#max_member_access_for_project_ids' do + shared_examples 'max member access for projects' do + let(:user) { create(:user) } + let(:group) { create(:group) } + let(:owner_project) { create(:project, group: group) } + let(:master_project) { create(:project) } + let(:reporter_project) { create(:project) } + let(:developer_project) { create(:project) } + let(:guest_project) { create(:project) } + let(:no_access_project) { create(:project) } + + let(:projects) do + [owner_project, master_project, reporter_project, developer_project, guest_project, no_access_project].map(&:id) + end + + let(:expected) do + { + owner_project.id => Gitlab::Access::OWNER, + master_project.id => Gitlab::Access::MASTER, + reporter_project.id => Gitlab::Access::REPORTER, + developer_project.id => Gitlab::Access::DEVELOPER, + guest_project.id => Gitlab::Access::GUEST, + no_access_project.id => Gitlab::Access::NO_ACCESS + } + end + + before do + create(:group_member, user: user, group: group) + master_project.add_master(user) + reporter_project.add_reporter(user) + developer_project.add_developer(user) + guest_project.add_guest(user) + end + + it 'returns correct roles for different projects' do + expect(user.max_member_access_for_project_ids(projects)).to eq(expected) + end + end + + context 'with RequestStore enabled', :request_store do + include_examples 'max member access for projects' + + def access_levels(projects) + user.max_member_access_for_project_ids(projects) + end + + it 'does not perform extra queries when asked for projects who have already been found' do + access_levels(projects) + + expect { access_levels(projects) }.not_to exceed_query_limit(0) + + expect(access_levels(projects)).to eq(expected) + end + + it 'only requests the extra projects when uncached projects are passed' do + second_master_project = create(:project) + second_developer_project = create(:project) + second_master_project.add_master(user) + second_developer_project.add_developer(user) + + all_projects = projects + [second_master_project.id, second_developer_project.id] + + expected_all = expected.merge(second_master_project.id => Gitlab::Access::MASTER, + second_developer_project.id => Gitlab::Access::DEVELOPER) + + access_levels(projects) + + queries = ActiveRecord::QueryRecorder.new { access_levels(all_projects) } + + expect(queries.count).to eq(1) + expect(queries.log_message).to match(/\W(#{second_master_project.id}, #{second_developer_project.id})\W/) + expect(access_levels(all_projects)).to eq(expected_all) + end + end + + context 'with RequestStore disabled' do + include_examples 'max member access for projects' + end + end + + describe '#max_member_access_for_group_ids' do + shared_examples 'max member access for groups' do + let(:user) { create(:user) } + let(:owner_group) { create(:group) } + let(:master_group) { create(:group) } + let(:reporter_group) { create(:group) } + let(:developer_group) { create(:group) } + let(:guest_group) { create(:group) } + let(:no_access_group) { create(:group) } + + let(:groups) do + [owner_group, master_group, reporter_group, developer_group, guest_group, no_access_group].map(&:id) + end + + let(:expected) do + { + owner_group.id => Gitlab::Access::OWNER, + master_group.id => Gitlab::Access::MASTER, + reporter_group.id => Gitlab::Access::REPORTER, + developer_group.id => Gitlab::Access::DEVELOPER, + guest_group.id => Gitlab::Access::GUEST, + no_access_group.id => Gitlab::Access::NO_ACCESS + } + end + + before do + owner_group.add_owner(user) + master_group.add_master(user) + reporter_group.add_reporter(user) + developer_group.add_developer(user) + guest_group.add_guest(user) + end + + it 'returns correct roles for different groups' do + expect(user.max_member_access_for_group_ids(groups)).to eq(expected) + end + end + + context 'with RequestStore enabled', :request_store do + include_examples 'max member access for groups' + + def access_levels(groups) + user.max_member_access_for_group_ids(groups) + end + + it 'does not perform extra queries when asked for groups who have already been found' do + access_levels(groups) + + expect { access_levels(groups) }.not_to exceed_query_limit(0) + + expect(access_levels(groups)).to eq(expected) + end + + it 'only requests the extra groups when uncached groups are passed' do + second_master_group = create(:group) + second_developer_group = create(:group) + second_master_group.add_master(user) + second_developer_group.add_developer(user) + + all_groups = groups + [second_master_group.id, second_developer_group.id] + + expected_all = expected.merge(second_master_group.id => Gitlab::Access::MASTER, + second_developer_group.id => Gitlab::Access::DEVELOPER) + + access_levels(groups) + + queries = ActiveRecord::QueryRecorder.new { access_levels(all_groups) } + + expect(queries.count).to eq(1) + expect(queries.log_message).to match(/\W(#{second_master_group.id}, #{second_developer_group.id})\W/) + expect(access_levels(all_groups)).to eq(expected_all) + end + end + + context 'with RequestStore disabled' 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/policies/group_policy_spec.rb b/spec/policies/group_policy_spec.rb index 4f4e634829d..b4d25e06d9a 100644 --- a/spec/policies/group_policy_spec.rb +++ b/spec/policies/group_policy_spec.rb @@ -9,6 +9,8 @@ describe GroupPolicy do let(:admin) { create(:admin) } let(:group) { create(:group) } + let(:guest_permissions) { [:read_group, :upload_file, :read_namespace] } + let(:reporter_permissions) { [:admin_label] } let(:developer_permissions) { [:admin_milestones] } @@ -52,6 +54,7 @@ describe GroupPolicy do it do expect_allowed(:read_group) + expect_disallowed(:upload_file) expect_disallowed(*reporter_permissions) expect_disallowed(*developer_permissions) expect_disallowed(*master_permissions) @@ -64,7 +67,7 @@ describe GroupPolicy do let(:current_user) { guest } it do - expect_allowed(:read_group, :read_namespace) + expect_allowed(*guest_permissions) expect_disallowed(*reporter_permissions) expect_disallowed(*developer_permissions) expect_disallowed(*master_permissions) @@ -76,7 +79,7 @@ describe GroupPolicy do let(:current_user) { reporter } it do - expect_allowed(:read_group, :read_namespace) + expect_allowed(*guest_permissions) expect_allowed(*reporter_permissions) expect_disallowed(*developer_permissions) expect_disallowed(*master_permissions) @@ -88,7 +91,7 @@ describe GroupPolicy do let(:current_user) { developer } it do - expect_allowed(:read_group, :read_namespace) + expect_allowed(*guest_permissions) expect_allowed(*reporter_permissions) expect_allowed(*developer_permissions) expect_disallowed(*master_permissions) @@ -100,7 +103,7 @@ describe GroupPolicy do let(:current_user) { master } it do - expect_allowed(:read_group, :read_namespace) + expect_allowed(*guest_permissions) expect_allowed(*reporter_permissions) expect_allowed(*developer_permissions) expect_allowed(*master_permissions) @@ -114,7 +117,7 @@ describe GroupPolicy do it do allow(Group).to receive(:supports_nested_groups?).and_return(true) - expect_allowed(:read_group, :read_namespace) + expect_allowed(*guest_permissions) expect_allowed(*reporter_permissions) expect_allowed(*developer_permissions) expect_allowed(*master_permissions) @@ -128,7 +131,7 @@ describe GroupPolicy do it do allow(Group).to receive(:supports_nested_groups?).and_return(true) - expect_allowed(:read_group, :read_namespace) + expect_allowed(*guest_permissions) expect_allowed(*reporter_permissions) expect_allowed(*developer_permissions) expect_allowed(*master_permissions) @@ -187,7 +190,7 @@ describe GroupPolicy do let(:current_user) { nil } it do - expect_disallowed(:read_group) + expect_disallowed(*guest_permissions) expect_disallowed(*reporter_permissions) expect_disallowed(*developer_permissions) expect_disallowed(*master_permissions) @@ -199,7 +202,7 @@ describe GroupPolicy do let(:current_user) { guest } it do - expect_allowed(:read_group) + expect_allowed(*guest_permissions) expect_disallowed(*reporter_permissions) expect_disallowed(*developer_permissions) expect_disallowed(*master_permissions) @@ -211,7 +214,7 @@ describe GroupPolicy do let(:current_user) { reporter } it do - expect_allowed(:read_group) + expect_allowed(*guest_permissions) expect_allowed(*reporter_permissions) expect_disallowed(*developer_permissions) expect_disallowed(*master_permissions) @@ -223,7 +226,7 @@ describe GroupPolicy do let(:current_user) { developer } it do - expect_allowed(:read_group) + expect_allowed(*guest_permissions) expect_allowed(*reporter_permissions) expect_allowed(*developer_permissions) expect_disallowed(*master_permissions) @@ -235,7 +238,7 @@ describe GroupPolicy do let(:current_user) { master } it do - expect_allowed(:read_group) + expect_allowed(*guest_permissions) expect_allowed(*reporter_permissions) expect_allowed(*developer_permissions) expect_allowed(*master_permissions) @@ -249,7 +252,7 @@ describe GroupPolicy do it do allow(Group).to receive(:supports_nested_groups?).and_return(true) - expect_allowed(:read_group) + expect_allowed(*guest_permissions) expect_allowed(*reporter_permissions) expect_allowed(*developer_permissions) expect_allowed(*master_permissions) 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/protected_branches_spec.rb b/spec/requests/api/protected_branches_spec.rb index 07d7f96bd70..10e6a3c07c8 100644 --- a/spec/requests/api/protected_branches_spec.rb +++ b/spec/requests/api/protected_branches_spec.rb @@ -95,6 +95,12 @@ describe API::ProtectedBranches do describe 'POST /projects/:id/protected_branches' do let(:branch_name) { 'new_branch' } + let(:post_endpoint) { api("/projects/#{project.id}/protected_branches", user) } + + def expect_protection_to_be_successful + expect(response).to have_gitlab_http_status(201) + expect(json_response['name']).to eq(branch_name) + end context 'when authenticated as a master' do before do @@ -102,7 +108,7 @@ describe API::ProtectedBranches do end it 'protects a single branch' do - post api("/projects/#{project.id}/protected_branches", user), name: branch_name + post post_endpoint, name: branch_name expect(response).to have_gitlab_http_status(201) expect(json_response['name']).to eq(branch_name) @@ -111,8 +117,7 @@ describe API::ProtectedBranches do end it 'protects a single branch and developers can push' do - post api("/projects/#{project.id}/protected_branches", user), - name: branch_name, push_access_level: 30 + post post_endpoint, name: branch_name, push_access_level: 30 expect(response).to have_gitlab_http_status(201) expect(json_response['name']).to eq(branch_name) @@ -121,8 +126,7 @@ describe API::ProtectedBranches do end it 'protects a single branch and developers can merge' do - post api("/projects/#{project.id}/protected_branches", user), - name: branch_name, merge_access_level: 30 + post post_endpoint, name: branch_name, merge_access_level: 30 expect(response).to have_gitlab_http_status(201) expect(json_response['name']).to eq(branch_name) @@ -131,8 +135,7 @@ describe API::ProtectedBranches do end it 'protects a single branch and developers can push and merge' do - post api("/projects/#{project.id}/protected_branches", user), - name: branch_name, push_access_level: 30, merge_access_level: 30 + post post_endpoint, name: branch_name, push_access_level: 30, merge_access_level: 30 expect(response).to have_gitlab_http_status(201) expect(json_response['name']).to eq(branch_name) @@ -141,8 +144,7 @@ describe API::ProtectedBranches do end it 'protects a single branch and no one can push' do - post api("/projects/#{project.id}/protected_branches", user), - name: branch_name, push_access_level: 0 + post post_endpoint, name: branch_name, push_access_level: 0 expect(response).to have_gitlab_http_status(201) expect(json_response['name']).to eq(branch_name) @@ -151,8 +153,7 @@ describe API::ProtectedBranches do end it 'protects a single branch and no one can merge' do - post api("/projects/#{project.id}/protected_branches", user), - name: branch_name, merge_access_level: 0 + post post_endpoint, name: branch_name, merge_access_level: 0 expect(response).to have_gitlab_http_status(201) expect(json_response['name']).to eq(branch_name) @@ -161,8 +162,7 @@ describe API::ProtectedBranches do end it 'protects a single branch and no one can push or merge' do - post api("/projects/#{project.id}/protected_branches", user), - name: branch_name, push_access_level: 0, merge_access_level: 0 + post post_endpoint, name: branch_name, push_access_level: 0, merge_access_level: 0 expect(response).to have_gitlab_http_status(201) expect(json_response['name']).to eq(branch_name) @@ -171,7 +171,8 @@ describe API::ProtectedBranches do end it 'returns a 409 error if the same branch is protected twice' do - post api("/projects/#{project.id}/protected_branches", user), name: protected_name + post post_endpoint, name: protected_name + expect(response).to have_gitlab_http_status(409) end @@ -179,10 +180,9 @@ describe API::ProtectedBranches do let(:branch_name) { 'feature/*' } it "protects multiple branches with a wildcard in the name" do - post api("/projects/#{project.id}/protected_branches", user), name: branch_name + post post_endpoint, name: branch_name - expect(response).to have_gitlab_http_status(201) - expect(json_response['name']).to eq(branch_name) + expect_protection_to_be_successful expect(json_response['push_access_levels'][0]['access_level']).to eq(Gitlab::Access::MASTER) expect(json_response['merge_access_levels'][0]['access_level']).to eq(Gitlab::Access::MASTER) end @@ -195,7 +195,7 @@ describe API::ProtectedBranches do end it "returns a 403 error if guest" do - post api("/projects/#{project.id}/protected_branches/", user), name: branch_name + post post_endpoint, name: branch_name expect(response).to have_gitlab_http_status(403) end 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/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/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/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb index b0de8d447a2..41ce81e0651 100644 --- a/spec/services/ci/create_pipeline_service_spec.rb +++ b/spec/services/ci/create_pipeline_service_spec.rb @@ -518,5 +518,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 decdd577226..3ee59014b5b 100644 --- a/spec/services/ci/register_job_service_spec.rb +++ b/spec/services/ci/register_job_service_spec.rb @@ -276,6 +276,89 @@ module Ci end end + context 'when "dependencies" keyword is specified' do + shared_examples 'not pick' do + it 'does not pick the build and drops the build' do + expect(subject).to be_nil + expect(pending_job.reload).to be_failed + expect(pending_job).to be_missing_dependency_failure + end + end + + 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) } + + it_behaves_like 'not pick' + 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) } + + it_behaves_like 'not pick' + end + + context 'when artifacts of depended job has been erased' do + let!(:pre_stage_job) { create(:ci_build, :success, pipeline: pipeline, name: 'test', stage_idx: 0, erased_at: 1.minute.ago) } + + before do + pre_stage_job.erase + end + + it_behaves_like 'not pick' + 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) } + + 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) } + + it { expect(subject).to eq(pending_job) } + end + + context 'when artifacts of depended job has been erased' do + let!(:pre_stage_job) { create(:ci_build, :success, pipeline: pipeline, name: 'test', stage_idx: 0, erased_at: 1.minute.ago) } + + before do + pre_stage_job.erase + end + + it { expect(subject).to eq(pending_job) } + end + end + + before do + stub_feature_flags(ci_disable_validates_dependencies: false) + end + + let!(:pre_stage_job) { create(:ci_build, :success, pipeline: pipeline, name: 'test', stage_idx: 0) } + let!(:pending_job) { create(:ci_build, :pending, pipeline: pipeline, stage_idx: 1, options: { dependencies: ['test'] } ) } + + subject { execute(specific_runner) } + + context 'when validates for dependencies is enabled' do + before do + stub_feature_flags(ci_disable_validates_dependencies: false) + end + + it_behaves_like 'validation is active' + end + + context 'when validates for dependencies is disabled' do + before do + stub_feature_flags(ci_disable_validates_dependencies: true) + end + + it_behaves_like 'validation is not active' + end + end + def execute(runner) described_class.new(runner).execute.build 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/projects/fork_service_spec.rb b/spec/services/projects/fork_service_spec.rb index 53862283a27..4057caca2ac 100644 --- a/spec/services/projects/fork_service_spec.rb +++ b/spec/services/projects/fork_service_spec.rb @@ -3,210 +3,253 @@ require 'spec_helper' describe Projects::ForkService do include ProjectForksHelper let(:gitlab_shell) { Gitlab::Shell.new } + context 'when forking a new project' do + describe 'fork by user' do + before do + @from_user = create(:user) + @from_namespace = @from_user.namespace + avatar = fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png") + @from_project = create(:project, + :repository, + creator_id: @from_user.id, + namespace: @from_namespace, + star_count: 107, + avatar: avatar, + description: 'wow such project') + @to_user = create(:user) + @to_namespace = @to_user.namespace + @from_project.add_user(@to_user, :developer) + end - describe 'fork by user' do - before do - @from_user = create(:user) - @from_namespace = @from_user.namespace - avatar = fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png") - @from_project = create(:project, - :repository, - creator_id: @from_user.id, - namespace: @from_namespace, - star_count: 107, - avatar: avatar, - description: 'wow such project') - @to_user = create(:user) - @to_namespace = @to_user.namespace - @from_project.add_user(@to_user, :developer) - end + context 'fork project' do + context 'when forker is a guest' do + before do + @guest = create(:user) + @from_project.add_user(@guest, :guest) + end + subject { fork_project(@from_project, @guest) } - context 'fork project' do - context 'when forker is a guest' do - before do - @guest = create(:user) - @from_project.add_user(@guest, :guest) + it { is_expected.not_to be_persisted } + it { expect(subject.errors[:forked_from_project_id]).to eq(['is forbidden']) } end - subject { fork_project(@from_project, @guest) } - it { is_expected.not_to be_persisted } - it { expect(subject.errors[:forked_from_project_id]).to eq(['is forbidden']) } - end + describe "successfully creates project in the user namespace" do + let(:to_project) { fork_project(@from_project, @to_user, namespace: @to_user.namespace) } - describe "successfully creates project in the user namespace" do - let(:to_project) { fork_project(@from_project, @to_user, namespace: @to_user.namespace) } - - it { expect(to_project).to be_persisted } - it { expect(to_project.errors).to be_empty } - it { expect(to_project.owner).to eq(@to_user) } - it { expect(to_project.namespace).to eq(@to_user.namespace) } - it { expect(to_project.star_count).to be_zero } - it { expect(to_project.description).to eq(@from_project.description) } - it { expect(to_project.avatar.file).to be_exists } - - # This test is here because we had a bug where the from-project lost its - # avatar after being forked. - # https://gitlab.com/gitlab-org/gitlab-ce/issues/26158 - it "after forking the from-project still has its avatar" do - # If we do not fork the project first we cannot detect the bug. - expect(to_project).to be_persisted - - expect(@from_project.avatar.file).to be_exists - end + it { expect(to_project).to be_persisted } + it { expect(to_project.errors).to be_empty } + it { expect(to_project.owner).to eq(@to_user) } + it { expect(to_project.namespace).to eq(@to_user.namespace) } + it { expect(to_project.star_count).to be_zero } + it { expect(to_project.description).to eq(@from_project.description) } + it { expect(to_project.avatar.file).to be_exists } - it 'flushes the forks count cache of the source project' do - expect(@from_project.forks_count).to be_zero + # This test is here because we had a bug where the from-project lost its + # avatar after being forked. + # https://gitlab.com/gitlab-org/gitlab-ce/issues/26158 + it "after forking the from-project still has its avatar" do + # If we do not fork the project first we cannot detect the bug. + expect(to_project).to be_persisted - fork_project(@from_project, @to_user) + expect(@from_project.avatar.file).to be_exists + end - expect(@from_project.forks_count).to eq(1) - end + it 'flushes the forks count cache of the source project' do + expect(@from_project.forks_count).to be_zero - it 'creates a fork network with the new project and the root project set' do - to_project - fork_network = @from_project.reload.fork_network + fork_project(@from_project, @to_user) - expect(fork_network).not_to be_nil - expect(fork_network.root_project).to eq(@from_project) - expect(fork_network.projects).to contain_exactly(@from_project, to_project) - end - end + expect(@from_project.forks_count).to eq(1) + end - context 'creating a fork of a fork' do - let(:from_forked_project) { fork_project(@from_project, @to_user) } - let(:other_namespace) do - group = create(:group) - group.add_owner(@to_user) - group - end - let(:to_project) { fork_project(from_forked_project, @to_user, namespace: other_namespace) } + it 'creates a fork network with the new project and the root project set' do + to_project + fork_network = @from_project.reload.fork_network - it 'sets the root of the network to the root project' do - expect(to_project.fork_network.root_project).to eq(@from_project) + expect(fork_network).not_to be_nil + expect(fork_network.root_project).to eq(@from_project) + expect(fork_network.projects).to contain_exactly(@from_project, to_project) + end end - it 'sets the forked_from_project on the membership' do - expect(to_project.fork_network_member.forked_from_project).to eq(from_forked_project) + context 'creating a fork of a fork' do + let(:from_forked_project) { fork_project(@from_project, @to_user) } + let(:other_namespace) do + group = create(:group) + group.add_owner(@to_user) + group + end + let(:to_project) { fork_project(from_forked_project, @to_user, namespace: other_namespace) } + + it 'sets the root of the network to the root project' do + expect(to_project.fork_network.root_project).to eq(@from_project) + end + + it 'sets the forked_from_project on the membership' do + expect(to_project.fork_network_member.forked_from_project).to eq(from_forked_project) + end end end - end - context 'project already exists' do - it "fails due to validation, not transaction failure" do - @existing_project = create(:project, :repository, creator_id: @to_user.id, name: @from_project.name, namespace: @to_namespace) - @to_project = fork_project(@from_project, @to_user, namespace: @to_namespace) - expect(@existing_project).to be_persisted + context 'project already exists' do + it "fails due to validation, not transaction failure" do + @existing_project = create(:project, :repository, creator_id: @to_user.id, name: @from_project.name, namespace: @to_namespace) + @to_project = fork_project(@from_project, @to_user, namespace: @to_namespace) + expect(@existing_project).to be_persisted - expect(@to_project).not_to be_persisted - expect(@to_project.errors[:name]).to eq(['has already been taken']) - expect(@to_project.errors[:path]).to eq(['has already been taken']) + expect(@to_project).not_to be_persisted + expect(@to_project.errors[:name]).to eq(['has already been taken']) + expect(@to_project.errors[:path]).to eq(['has already been taken']) + end end - end - context 'repository already exists' do - let(:repository_storage) { 'default' } - let(:repository_storage_path) { Gitlab.config.repositories.storages[repository_storage]['path'] } + context 'repository already exists' do + let(:repository_storage) { 'default' } + let(:repository_storage_path) { Gitlab.config.repositories.storages[repository_storage]['path'] } - before do - gitlab_shell.add_repository(repository_storage, "#{@to_user.namespace.full_path}/#{@from_project.path}") - end + before do + gitlab_shell.add_repository(repository_storage, "#{@to_user.namespace.full_path}/#{@from_project.path}") + end - after do - gitlab_shell.remove_repository(repository_storage_path, "#{@to_user.namespace.full_path}/#{@from_project.path}") - end + after do + gitlab_shell.remove_repository(repository_storage_path, "#{@to_user.namespace.full_path}/#{@from_project.path}") + end - it 'does not allow creation' do - to_project = fork_project(@from_project, @to_user, namespace: @to_user.namespace) + it 'does not allow creation' do + to_project = fork_project(@from_project, @to_user, namespace: @to_user.namespace) - expect(to_project).not_to be_persisted - expect(to_project.errors.messages).to have_key(:base) - expect(to_project.errors.messages[:base].first).to match('There is already a repository with that name on disk') + expect(to_project).not_to be_persisted + expect(to_project.errors.messages).to have_key(:base) + expect(to_project.errors.messages[:base].first).to match('There is already a repository with that name on disk') + end end - end - context 'GitLab CI is enabled' do - it "forks and enables CI for fork" do - @from_project.enable_ci - @to_project = fork_project(@from_project, @to_user) - expect(@to_project.builds_enabled?).to be_truthy + context 'GitLab CI is enabled' do + it "forks and enables CI for fork" do + @from_project.enable_ci + @to_project = fork_project(@from_project, @to_user) + expect(@to_project.builds_enabled?).to be_truthy + end end - end - context "when project has restricted visibility level" do - context "and only one visibility level is restricted" do - before do - @from_project.update_attributes(visibility_level: Gitlab::VisibilityLevel::INTERNAL) - stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::INTERNAL]) + context "when project has restricted visibility level" do + context "and only one visibility level is restricted" do + before do + @from_project.update_attributes(visibility_level: Gitlab::VisibilityLevel::INTERNAL) + stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::INTERNAL]) + end + + it "creates fork with highest allowed level" do + forked_project = fork_project(@from_project, @to_user) + + expect(forked_project.visibility_level).to eq(Gitlab::VisibilityLevel::PUBLIC) + end end - it "creates fork with highest allowed level" do - forked_project = fork_project(@from_project, @to_user) + context "and all visibility levels are restricted" do + before do + stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC, Gitlab::VisibilityLevel::INTERNAL, Gitlab::VisibilityLevel::PRIVATE]) + end + + it "creates fork with private visibility levels" do + forked_project = fork_project(@from_project, @to_user) - expect(forked_project.visibility_level).to eq(Gitlab::VisibilityLevel::PUBLIC) + expect(forked_project.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE) + end end end + end - context "and all visibility levels are restricted" do - before do - stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC, Gitlab::VisibilityLevel::INTERNAL, Gitlab::VisibilityLevel::PRIVATE]) + describe 'fork to namespace' do + before do + @group_owner = create(:user) + @developer = create(:user) + @project = create(:project, :repository, + creator_id: @group_owner.id, + star_count: 777, + description: 'Wow, such a cool project!') + @group = create(:group) + @group.add_user(@group_owner, GroupMember::OWNER) + @group.add_user(@developer, GroupMember::DEVELOPER) + @project.add_user(@developer, :developer) + @project.add_user(@group_owner, :developer) + @opts = { namespace: @group } + end + + context 'fork project for group' do + it 'group owner successfully forks project into the group' do + to_project = fork_project(@project, @group_owner, @opts) + + expect(to_project).to be_persisted + expect(to_project.errors).to be_empty + expect(to_project.owner).to eq(@group) + expect(to_project.namespace).to eq(@group) + expect(to_project.name).to eq(@project.name) + expect(to_project.path).to eq(@project.path) + expect(to_project.description).to eq(@project.description) + expect(to_project.star_count).to be_zero end + end - it "creates fork with private visibility levels" do - forked_project = fork_project(@from_project, @to_user) + context 'fork project for group when user not owner' do + it 'group developer fails to fork project into the group' do + to_project = fork_project(@project, @developer, @opts) + expect(to_project.errors[:namespace]).to eq(['is not valid']) + end + end - expect(forked_project.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE) + context 'project already exists in group' do + it 'fails due to validation, not transaction failure' do + existing_project = create(:project, :repository, + name: @project.name, + namespace: @group) + to_project = fork_project(@project, @group_owner, @opts) + expect(existing_project.persisted?).to be_truthy + expect(to_project.errors[:name]).to eq(['has already been taken']) + expect(to_project.errors[:path]).to eq(['has already been taken']) end end end end - describe 'fork to namespace' do - before do - @group_owner = create(:user) - @developer = create(:user) - @project = create(:project, :repository, - creator_id: @group_owner.id, - star_count: 777, - description: 'Wow, such a cool project!') - @group = create(:group) - @group.add_user(@group_owner, GroupMember::OWNER) - @group.add_user(@developer, GroupMember::DEVELOPER) - @project.add_user(@developer, :developer) - @project.add_user(@group_owner, :developer) - @opts = { namespace: @group } + context 'when linking fork to an existing project' do + let(:fork_from_project) { create(:project, :public) } + let(:fork_to_project) { create(:project, :public) } + let(:user) { create(:user) } + + subject { described_class.new(fork_from_project, user) } + + def forked_from_project(project) + project.fork_network_member&.forked_from_project end - context 'fork project for group' do - it 'group owner successfully forks project into the group' do - to_project = fork_project(@project, @group_owner, @opts) - - expect(to_project).to be_persisted - expect(to_project.errors).to be_empty - expect(to_project.owner).to eq(@group) - expect(to_project.namespace).to eq(@group) - expect(to_project.name).to eq(@project.name) - expect(to_project.path).to eq(@project.path) - expect(to_project.description).to eq(@project.description) - expect(to_project.star_count).to be_zero + context 'if project is already forked' do + it 'does not create fork relation' do + allow(fork_to_project).to receive(:forked?).and_return(true) + expect(forked_from_project(fork_to_project)).to be_nil + expect(subject.execute(fork_to_project)).to be_nil + expect(forked_from_project(fork_to_project)).to be_nil end end - context 'fork project for group when user not owner' do - it 'group developer fails to fork project into the group' do - to_project = fork_project(@project, @developer, @opts) - expect(to_project.errors[:namespace]).to eq(['is not valid']) + context 'if project is not forked' do + it 'creates fork relation' do + expect(fork_to_project.forked?).to be false + expect(forked_from_project(fork_to_project)).to be_nil + + subject.execute(fork_to_project) + + expect(fork_to_project.forked?).to be true + expect(forked_from_project(fork_to_project)).to eq fork_from_project + expect(fork_to_project.forked_from_project).to eq fork_from_project end - end - context 'project already exists in group' do - it 'fails due to validation, not transaction failure' do - existing_project = create(:project, :repository, - name: @project.name, - namespace: @group) - to_project = fork_project(@project, @group_owner, @opts) - expect(existing_project.persisted?).to be_truthy - expect(to_project.errors[:name]).to eq(['has already been taken']) - expect(to_project.errors[:path]).to eq(['has already been taken']) + it 'flushes the forks count cache of the source project' do + expect(fork_from_project.forks_count).to be_zero + + subject.execute(fork_to_project) + + expect(fork_from_project.forks_count).to eq(1) end end end diff --git a/spec/services/projects/update_service_spec.rb b/spec/services/projects/update_service_spec.rb index fcd71857af3..d887f70efae 100644 --- a/spec/services/projects/update_service_spec.rb +++ b/spec/services/projects/update_service_spec.rb @@ -199,24 +199,53 @@ describe Projects::UpdateService do end describe '#run_auto_devops_pipeline?' do - subject { described_class.new(project, user, params).run_auto_devops_pipeline? } + subject { described_class.new(project, user).run_auto_devops_pipeline? } - context 'when neither pipeline setting is true' do - let(:params) { {} } + context 'when master contains a .gitlab-ci.yml file' do + before do + allow(project.repository).to receive(:gitlab_ci_yml).and_return("script: ['test']") + end it { is_expected.to eq(false) } end - context 'when run_auto_devops_pipeline_explicit is true' do - let(:params) { { run_auto_devops_pipeline_explicit: 'true' } } + context 'when auto devops is explicitly enabled' do + before do + project.create_auto_devops!(enabled: true) + end it { is_expected.to eq(true) } end - context 'when run_auto_devops_pipeline_implicit is true' do - let(:params) { { run_auto_devops_pipeline_implicit: 'true' } } + context 'when auto devops is explicitly disabled' do + before do + project.create_auto_devops!(enabled: false) + end - it { is_expected.to eq(true) } + it { is_expected.to eq(false) } + end + + context 'when auto devops is set to instance setting' do + before do + project.create_auto_devops!(enabled: nil) + allow(project.auto_devops).to receive(:previous_changes).and_return('enabled' => true) + end + + context 'when auto devops is enabled system-wide' do + before do + stub_application_setting(auto_devops_enabled: true) + end + + it { is_expected.to eq(true) } + end + + context 'when auto devops is disabled system-wide' do + before do + stub_application_setting(auto_devops_enabled: false) + end + + it { is_expected.to eq(false) } + end end end diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb index a918383ecd2..47412110b4b 100644 --- a/spec/services/system_note_service_spec.rb +++ b/spec/services/system_note_service_spec.rb @@ -692,9 +692,9 @@ describe SystemNoteService do describe '.new_commit_summary' do it 'escapes HTML titles' do commit = double(title: '<pre>This is a test</pre>', short_id: '12345678') - escaped = '* 12345678 - <pre>This is a test</pre>' + escaped = '<pre>This is a test</pre>' - expect(described_class.new_commit_summary([commit])).to eq([escaped]) + expect(described_class.new_commit_summary([commit])).to all(match(%r[- #{escaped}])) end end 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..f94fb8733d5 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 diff --git a/spec/support/google_api/cloud_platform_helpers.rb b/spec/support/google_api/cloud_platform_helpers.rb index dabf0db7666..8a073e58db8 100644 --- a/spec/support/google_api/cloud_platform_helpers.rb +++ b/spec/support/google_api/cloud_platform_helpers.rb @@ -63,7 +63,7 @@ module GoogleApi ## # gcloud container clusters create - # https://cloud.google.com/container-engine/reference/rest/v1/projects.zones.clusters/create + # https://cloud.google.com/kubernetes-engine/docs/reference/rest/v1/projects.zones.clusters/create # rubocop:disable Metrics/CyclomaticComplexity # rubocop:disable Metrics/PerceivedComplexity def cloud_platform_cluster_body(**options) diff --git a/spec/support/shared_examples/controllers/uploads_actions_shared_examples.rb b/spec/support/shared_examples/controllers/uploads_actions_shared_examples.rb new file mode 100644 index 00000000000..935c08221e0 --- /dev/null +++ b/spec/support/shared_examples/controllers/uploads_actions_shared_examples.rb @@ -0,0 +1,240 @@ +shared_examples 'handle uploads' do + let(:user) { create(:user) } + let(:jpg) { fixture_file_upload(Rails.root + 'spec/fixtures/rails_sample.jpg', 'image/jpg') } + let(:txt) { fixture_file_upload(Rails.root + 'spec/fixtures/doc_sample.txt', 'text/plain') } + + describe "POST #create" do + context 'when a user is not authorized to upload a file' do + it 'returns 404 status' do + post :create, params.merge(file: jpg, format: :json) + + expect(response.status).to eq(404) + end + end + + context 'when a user can upload a file' do + before do + sign_in(user) + model.add_developer(user) + end + + context "without params['file']" do + it "returns an error" do + post :create, params.merge(format: :json) + + expect(response).to have_gitlab_http_status(422) + end + end + + context 'with valid image' do + before do + post :create, params.merge(file: jpg, format: :json) + end + + it 'returns a content with original filename, new link, and correct type.' do + expect(response.body).to match '\"alt\":\"rails_sample\"' + expect(response.body).to match "\"url\":\"/uploads" + end + + # NOTE: This is as close as we're getting to an Integration test for this + # behavior. We're avoiding a proper Feature test because those should be + # testing things entirely user-facing, which the Upload model is very much + # not. + it 'creates a corresponding Upload record' do + upload = Upload.last + + aggregate_failures do + expect(upload).to exist + expect(upload.model).to eq(model) + end + end + end + + context 'with valid non-image file' do + before do + post :create, params.merge(file: txt, format: :json) + end + + it 'returns a content with original filename, new link, and correct type.' do + expect(response.body).to match '\"alt\":\"doc_sample.txt\"' + expect(response.body).to match "\"url\":\"/uploads" + end + end + end + end + + describe "GET #show" do + let(:show_upload) do + get :show, params.merge(secret: "123456", filename: "image.jpg") + end + + context "when the model is public" do + before do + model.update_attribute(:visibility_level, Gitlab::VisibilityLevel::PUBLIC) + end + + context "when not signed in" do + context "when the file exists" do + before do + allow_any_instance_of(FileUploader).to receive(:file).and_return(jpg) + allow(jpg).to receive(:exists?).and_return(true) + end + + it "responds with status 200" do + show_upload + + expect(response).to have_gitlab_http_status(200) + end + end + + context "when the file doesn't exist" do + it "responds with status 404" do + show_upload + + expect(response).to have_gitlab_http_status(404) + end + end + end + + context "when signed in" do + before do + sign_in(user) + end + + context "when the file exists" do + before do + allow_any_instance_of(FileUploader).to receive(:file).and_return(jpg) + allow(jpg).to receive(:exists?).and_return(true) + end + + it "responds with status 200" do + show_upload + + expect(response).to have_gitlab_http_status(200) + end + end + + context "when the file doesn't exist" do + it "responds with status 404" do + show_upload + + expect(response).to have_gitlab_http_status(404) + end + end + end + end + + context "when the model is private" do + before do + model.update_attribute(:visibility_level, Gitlab::VisibilityLevel::PRIVATE) + end + + context "when not signed in" do + context "when the file exists" do + before do + allow_any_instance_of(FileUploader).to receive(:file).and_return(jpg) + allow(jpg).to receive(:exists?).and_return(true) + end + + context "when the file is an image" do + before do + allow_any_instance_of(FileUploader).to receive(:image?).and_return(true) + end + + it "responds with status 200" do + show_upload + + expect(response).to have_gitlab_http_status(200) + end + end + + context "when the file is not an image" do + it "redirects to the sign in page" do + show_upload + + expect(response).to redirect_to(new_user_session_path) + end + end + end + + context "when the file doesn't exist" do + it "redirects to the sign in page" do + show_upload + + expect(response).to redirect_to(new_user_session_path) + end + end + end + + context "when signed in" do + before do + sign_in(user) + end + + context "when the user has access to the project" do + before do + model.add_developer(user) + end + + context "when the file exists" do + before do + allow_any_instance_of(FileUploader).to receive(:file).and_return(jpg) + allow(jpg).to receive(:exists?).and_return(true) + end + + it "responds with status 200" do + show_upload + + expect(response).to have_gitlab_http_status(200) + end + end + + context "when the file doesn't exist" do + it "responds with status 404" do + show_upload + + expect(response).to have_gitlab_http_status(404) + end + end + end + + context "when the user doesn't have access to the model" do + context "when the file exists" do + before do + allow_any_instance_of(FileUploader).to receive(:file).and_return(jpg) + allow(jpg).to receive(:exists?).and_return(true) + end + + context "when the file is an image" do + before do + allow_any_instance_of(FileUploader).to receive(:image?).and_return(true) + end + + it "responds with status 200" do + show_upload + + expect(response).to have_gitlab_http_status(200) + end + end + + context "when the file is not an image" do + it "responds with status 404" do + show_upload + + expect(response).to have_gitlab_http_status(404) + end + end + end + + context "when the file doesn't exist" do + it "responds with status 404" do + show_upload + + expect(response).to have_gitlab_http_status(404) + end + end + end + end + end + end +end diff --git a/spec/support/shared_examples/throttled_touch.rb b/spec/support/shared_examples/throttled_touch.rb new file mode 100644 index 00000000000..4a25bb9b750 --- /dev/null +++ b/spec/support/shared_examples/throttled_touch.rb @@ -0,0 +1,20 @@ +shared_examples_for 'throttled touch' do + describe '#touch' do + it 'updates the updated_at timestamp' do + Timecop.freeze do + subject.touch + expect(subject.updated_at).to eq(Time.zone.now) + end + end + + it 'updates the object at most once per minute' do + first_updated_at = Time.zone.now - (ThrottledTouch::TOUCH_INTERVAL * 2) + second_updated_at = Time.zone.now - (ThrottledTouch::TOUCH_INTERVAL * 1.5) + + Timecop.freeze(first_updated_at) { subject.touch } + Timecop.freeze(second_updated_at) { subject.touch } + + expect(subject.updated_at).to eq(first_updated_at) + end + end +end 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..b36cf3c544c 100644 --- a/spec/support/stub_configuration.rb +++ b/spec/support/stub_configuration.rb @@ -43,6 +43,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/track_untracked_uploads_helpers.rb b/spec/support/track_untracked_uploads_helpers.rb new file mode 100644 index 00000000000..d05eda08201 --- /dev/null +++ b/spec/support/track_untracked_uploads_helpers.rb @@ -0,0 +1,20 @@ +module TrackUntrackedUploadsHelpers + def uploaded_file + fixture_path = Rails.root.join('spec', 'fixtures', 'rails_sample.jpg') + fixture_file_upload(fixture_path) + end + + def ensure_temporary_tracking_table_exists + Gitlab::BackgroundMigration::PrepareUntrackedUploads.new.send(:ensure_temporary_tracking_table_exists) + end + + def drop_temp_table_if_exists + ActiveRecord::Base.connection.drop_table(:untracked_files_for_uploads) if ActiveRecord::Base.connection.table_exists?(:untracked_files_for_uploads) + end + + def create_or_update_appearance(attrs) + a = Appearance.first_or_initialize(title: 'foo', description: 'bar') + a.update!(attrs) + a + end +end diff --git a/spec/tasks/gitlab/backup_rake_spec.rb b/spec/tasks/gitlab/backup_rake_spec.rb index bf2e11bc360..b41c3b3958a 100644 --- a/spec/tasks/gitlab/backup_rake_spec.rb +++ b/spec/tasks/gitlab/backup_rake_spec.rb @@ -212,7 +212,7 @@ describe 'gitlab:app namespace rake task' do # Avoid asking gitaly about the root ref (which will fail beacuse of the # mocked storages) - allow_any_instance_of(Repository).to receive(:empty_repo?).and_return(false) + allow_any_instance_of(Repository).to receive(:empty?).and_return(false) end after do diff --git a/spec/uploaders/namespace_file_uploader_spec.rb b/spec/uploaders/namespace_file_uploader_spec.rb new file mode 100644 index 00000000000..c6c4500c179 --- /dev/null +++ b/spec/uploaders/namespace_file_uploader_spec.rb @@ -0,0 +1,21 @@ +require 'spec_helper' + +describe NamespaceFileUploader do + let(:group) { build_stubbed(:group) } + let(:uploader) { described_class.new(group) } + + describe "#store_dir" do + it "stores in the namespace id directory" do + expect(uploader.store_dir).to include(group.id.to_s) + end + end + + describe ".absolute_path" do + it "stores in thecorrect directory" do + upload_record = create(:upload, :namespace_upload, model: group) + + expect(described_class.absolute_path(upload_record)) + .to include("-/system/namespace/#{group.id}") + end + end +end diff --git a/spec/views/dashboard/projects/_blank_state_admin_welcome.haml.rb b/spec/views/dashboard/projects/_blank_state_admin_welcome.haml.rb new file mode 100644 index 00000000000..2f58eec86dc --- /dev/null +++ b/spec/views/dashboard/projects/_blank_state_admin_welcome.haml.rb @@ -0,0 +1,15 @@ +require 'spec_helper' + +describe 'dashboard/projects/_blank_state_admin_welcome.html.haml' do + let(:user) { create(:admin) } + + before do + allow(view).to receive(:current_user).and_return(user) + end + + it 'links to new group path' do + render + + expect(rendered).to have_link('Create a group', href: new_group_path) + end +end diff --git a/spec/views/projects/commit/show.html.haml_spec.rb b/spec/views/projects/commit/show.html.haml_spec.rb index 32c95c6bb0d..a9c32122600 100644 --- a/spec/views/projects/commit/show.html.haml_spec.rb +++ b/spec/views/projects/commit/show.html.haml_spec.rb @@ -2,14 +2,15 @@ require 'spec_helper' describe 'projects/commit/show.html.haml' do let(:project) { create(:project, :repository) } + let(:commit) { project.commit } before do assign(:project, project) assign(:repository, project.repository) - assign(:commit, project.commit) - assign(:noteable, project.commit) + assign(:commit, commit) + assign(:noteable, commit) assign(:notes, []) - assign(:diffs, project.commit.diffs) + assign(:diffs, commit.diffs) allow(view).to receive(:current_user).and_return(nil) allow(view).to receive(:can?).and_return(false) @@ -43,4 +44,19 @@ describe 'projects/commit/show.html.haml' do expect(rendered).not_to have_selector('.limit-container-width') end end + + context 'in the context of a merge request' do + let(:merge_request) { create(:merge_request, source_project: project, target_project: project) } + + before do + assign(:merge_request, merge_request) + render + end + + it 'shows that it is in the context of a merge request' do + merge_request_url = diffs_project_merge_request_url(project, merge_request, commit_id: commit.id) + expect(rendered).to have_content("This commit is part of merge request") + expect(rendered).to have_link(merge_request.to_reference, merge_request_url) + end + end end diff --git a/spec/views/projects/merge_requests/_commits.html.haml_spec.rb b/spec/views/projects/merge_requests/_commits.html.haml_spec.rb index efed2e02a1b..3ca67114558 100644 --- a/spec/views/projects/merge_requests/_commits.html.haml_spec.rb +++ b/spec/views/projects/merge_requests/_commits.html.haml_spec.rb @@ -25,8 +25,8 @@ describe 'projects/merge_requests/_commits.html.haml' do it 'shows commits from source project' do render - commit = source_project.commit(merge_request.source_branch) - href = project_commit_path(source_project, commit) + commit = merge_request.commits.first # HEAD + href = diffs_project_merge_request_path(target_project, merge_request, commit_id: commit) expect(rendered).to have_link(Commit.truncate_sha(commit.sha), href: href) end diff --git a/spec/views/projects/merge_requests/diffs/_diffs.html.haml_spec.rb b/spec/views/projects/merge_requests/diffs/_diffs.html.haml_spec.rb new file mode 100644 index 00000000000..e7c40421f1f --- /dev/null +++ b/spec/views/projects/merge_requests/diffs/_diffs.html.haml_spec.rb @@ -0,0 +1,36 @@ +require 'spec_helper' + +describe 'projects/merge_requests/diffs/_diffs.html.haml' do + include Devise::Test::ControllerHelpers + + let(:user) { create(:user) } + let(:project) { create(:project, :public, :repository) } + let(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: project, author: user) } + + before do + allow(view).to receive(:url_for).and_return(controller.request.fullpath) + + assign(:merge_request, merge_request) + assign(:environment, merge_request.environments_for(user).last) + assign(:diffs, merge_request.diffs) + assign(:merge_request_diffs, merge_request.diffs) + assign(:diff_notes_disabled, true) # disable note creation + assign(:use_legacy_diff_notes, false) + assign(:grouped_diff_discussions, {}) + assign(:notes, []) + end + + context 'for a commit' do + let(:commit) { merge_request.commits.last } + + before do + assign(:commit, commit) + end + + it "shows the commit scope" do + render + + expect(rendered).to have_content "Only comments from the following commit are shown below" + end + end +end diff --git a/yarn.lock b/yarn.lock index 88897476a15..69e8f8a0647 100644 --- a/yarn.lock +++ b/yarn.lock @@ -55,8 +55,8 @@ to-fast-properties "^2.0.0" "@gitlab-org/gitlab-svgs@^1.1.1": - version "1.1.1" - resolved "https://registry.yarnpkg.com/@gitlab-org/gitlab-svgs/-/gitlab-svgs-1.1.1.tgz#6e07ea02c3b104fa8b5d860a5e2fa9dab4edab96" + version "1.1.3" + resolved "https://registry.yarnpkg.com/@gitlab-org/gitlab-svgs/-/gitlab-svgs-1.1.3.tgz#2beead1bcdd83e7400de29b01014bf17bf76318e" "@types/jquery@^2.0.40": version "2.0.48" |