diff options
1703 files changed, 24941 insertions, 11029 deletions
diff --git a/.eslintrc.yml b/.eslintrc.yml index f851e3b67e6..b9c5973d7ac 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -71,7 +71,3 @@ rules: body: 1 ## Destructuring: https://eslint.org/docs/rules/prefer-destructuring prefer-destructuring: off - ## no-restricted-globals: https://eslint.org/docs/rules/no-restricted-globals - no-restricted-globals: off - ## no-multi-assign: https://eslint.org/docs/rules/no-multi-assign - no-multi-assign: off diff --git a/.flayignore b/.flayignore index 7faa6c7bb90..3e5063674ff 100644 --- a/.flayignore +++ b/.flayignore @@ -14,3 +14,12 @@ lib/gitlab/gitaly_client/ref_service.rb lib/gitlab/gitaly_client/commit_service.rb lib/gitlab/git/commit.rb lib/gitlab/git/tag.rb + +ee/db/**/* +ee/app/serializers/ee/merge_request_widget_entity.rb +ee/lib/api/epics.rb +ee/lib/api/geo_nodes.rb +ee/lib/ee/gitlab/ldap/sync/admin_users.rb +ee/app/workers/geo/file_download_dispatch_worker/job_artifact_job_finder.rb +ee/app/workers/geo/file_download_dispatch_worker/lfs_object_job_finder.rb +ee/spec/**/* diff --git a/.gitignore b/.gitignore index 51b77d5ac9e..21dc67384aa 100644 --- a/.gitignore +++ b/.gitignore @@ -76,3 +76,4 @@ eslint-report.html /.rspec /plugins/* /.gitlab_pages_secret +package-lock.json diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 1679ae378c9..e7304b9c057 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,4 +1,4 @@ -image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.4.4-golang-1.9-git-2.17-chrome-65.0-node-8.x-yarn-1.2-postgresql-9.6" +image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.4.4-golang-1.9-git-2.17-chrome-67.0-node-8.x-yarn-1.2-postgresql-9.6-graphicsmagick-1.3.29" .dedicated-runner: &dedicated-runner retry: 1 @@ -264,10 +264,10 @@ package-and-qa: <<: *single-script-job variables: <<: *single-script-job-variables - SCRIPT_NAME: trigger-build-omnibus + SCRIPT_NAME: trigger-build retry: 0 script: - - ./$SCRIPT_NAME + - ./$SCRIPT_NAME omnibus when: manual only: - //@gitlab-org/gitlab-ce @@ -394,7 +394,11 @@ compile-assets: - date - yarn install --frozen-lockfile --cache-folder .yarn-cache - date + - free -m - bundle exec rake gitlab:assets:compile + variables: + # we override the max_old_space_size to prevent OOM errors + NODE_OPTIONS: --max_old_space_size=3584 artifacts: expire_in: 7d paths: @@ -411,6 +415,7 @@ setup-test-env: script: - bundle exec ruby -Ispec -e 'require "spec_helper" ; TestEnv.init' - scripts/gitaly-test-build # Do not use 'bundle exec' here + - BUNDLE_GEMFILE=Gemfile.rails5 bundle install $BUNDLE_INSTALL_FLAGS artifacts: expire_in: 7d paths: @@ -586,6 +591,12 @@ downtime_check: - /(^docs[\/-].*|.*-docs$)/ - /(^qa[\/-].*|.*-qa$)/ +rails5_gemfile_lock_check: + <<: *dedicated-no-docs-no-db-pull-cache-job + <<: *except-docs-and-qa + script: + - scripts/rails5-gemfile-lock-check + ee_compat_check: <<: *rake-exec except: @@ -658,10 +669,13 @@ gitlab:assets:compile: SKIP_STORAGE_VALIDATION: "true" WEBPACK_REPORT: "true" NO_COMPRESSION: "true" + # we override the max_old_space_size to prevent OOM errors + NODE_OPTIONS: --max_old_space_size=3584 script: - date - yarn install --frozen-lockfile --production --cache-folder .yarn-cache - date + - free -m - bundle exec rake gitlab:assets:compile artifacts: name: webpack-report diff --git a/.gitlab/issue_templates/Feature Proposal.md b/.gitlab/issue_templates/Feature Proposal.md index 5b55eb1374b..c4065d3c4ea 100644 --- a/.gitlab/issue_templates/Feature Proposal.md +++ b/.gitlab/issue_templates/Feature Proposal.md @@ -1,9 +1,15 @@ -### Description +### Problem to solve -(Include problem, use cases, benefits, and/or goals) +### Further details + +(Include use cases, benefits, and/or goals) ### Proposal +### What does success look like, and how can we measure that? + +(If no way to measure success, link to an issue that will implement a way to measure this) + ### Links / references /label ~"feature proposal" diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 1fb352306d7..ccf301e6c78 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -487,7 +487,7 @@ Style/EmptyLiteral: - 'lib/gitlab/fogbugz_import/importer.rb' - 'lib/gitlab/git/diff_collection.rb' - 'lib/gitlab/gitaly_client.rb' - - 'scripts/trigger-build-omnibus' + - 'scripts/trigger-build' - 'spec/features/merge_requests/versions_spec.rb' - 'spec/helpers/merge_requests_helper_spec.rb' - 'spec/lib/gitlab/request_context_spec.rb' diff --git a/CHANGELOG.md b/CHANGELOG.md index ec92829f7d1..0d843c3f318 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ documentation](doc/development/changelog.md) for instructions on adding your own entry. +## 10.8.4 (2018-06-06) + +- No changes. + ## 10.8.3 (2018-05-30) ### Fixed (4 changes) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 64470a1f087..dcdf520ee6b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -182,7 +182,7 @@ Assigning a team label makes sure issues get the attention of the appropriate people. The current team labels are ~Distribution, ~"CI/CD", ~Discussion, ~Documentation, ~Quality, -~Geo, ~Gitaly, ~Monitoring, ~Platform, ~Release, ~"Security Products" and ~"UX". +~Geo, ~Gitaly, ~Monitoring, ~Platform, ~Release, ~"Security Products", ~"Configuration", and ~"UX". The descriptions on the [labels page][labels-page] explain what falls under the responsibility of each team. @@ -301,19 +301,28 @@ For guidance on UX implementation at GitLab, please refer to our [Design System] The UX team uses labels to manage their workflow. The ~"UX" label on an issue is a signal to the UX team that it will need UX attention. -To better understand the priority by which UX tackles issues, see the [UX section](https://about.gitlab.com/handbook/ux/) of the handbook. +To better understand the priority by which UX tackles issues, see the [UX section](https://about.gitlab.com/handbook/engineering/ux) of the handbook. -Once an issue has been worked on and is ready for development, a UXer applies the ~"UX ready" label to that issue. +Once an issue has been worked on and is ready for development, a UXer removes the ~"UX" label and applies the ~"UX ready" label to that issue. The UX team has a special type label called ~"design artifact". This label indicates that the final output -for an issue is a UX solution/design. The solution will be developed by frontend and/or backend in a subsequent milestone. +for an issue is a UX solution/design. The solution will be developed by frontend and/or backend in a subsequent milestone. Any issue labeled ~"design artifact" should not also be labeled ~"frontend" or ~"backend" since no development is needed until the solution has been decided. ~"design artifact" issues are like any other issue and should contain a milestone label, ~"Deliverable" or ~"Stretch", when scheduled in the current milestone. -Once the ~"design artifact" issue has been completed, the UXer removes the ~"design artifact" label and applies the ~"UX ready" label. The Product Manager can use the -existing issue or decide to create a whole new issue for the purpose of development. +To prevent the misunderstanding that a feature will be be delivered in the +assigned milestone, when only UX design is planned for that milestone, the +Product Manager should create a separate issue for the ~"design artifact", +assign the ~UX, ~"design artifact" and ~"Deliverable" labels, add a milestone +and use a title that makes it clear that the scheduled issue is design only +(e.g. `Design exploration for XYZ`). + +When the ~"design artifact" issue has been completed, the UXer removes the ~UX +label, adds the ~"UX ready" label and closes the issue. This indicates the +design artifact is complete. The UXer will also copy the designs to related +issues for implementation in an upcoming milestone. ## Issue tracker @@ -349,7 +358,7 @@ on those issues. Please select someone with relevant experience from the [GitLab team][team]. If there is nobody mentioned with that expertise look in the commit history for the affected files to find someone. -[described in our handbook]: https://about.gitlab.com/handbook/engineering/issues/issue-triage-policies/ +[described in our handbook]: https://about.gitlab.com/handbook/engineering/issue-triage/ [issue bash events]: https://gitlab.com/gitlab-org/gitlab-ce/issues/17815 ### Feature proposals @@ -512,7 +521,7 @@ request is as follows: 1. Write [tests](https://docs.gitlab.com/ee/development/rake_tasks.html#run-tests) and code 1. [Generate a changelog entry with `bin/changelog`][changelog] 1. If you are writing documentation, make sure to follow the - [documentation styleguide][doc-styleguide] + [documentation guidelines][doc-guidelines] 1. If you have multiple commits please combine them into a few logically organized commits by [squashing them][git-squash] 1. Push the commit(s) to your fork @@ -727,7 +736,7 @@ available at [http://contributor-covenant.org/version/1/1/0/](http://contributor [rss-source]: https://github.com/bbatsov/ruby-style-guide/blob/master/README.md#source-code-layout [rss-naming]: https://github.com/bbatsov/ruby-style-guide/blob/master/README.md#naming [changelog]: doc/development/changelog.md "Generate a changelog entry" -[doc-styleguide]: doc/development/doc_styleguide.md "Documentation styleguide" +[doc-guidelines]: doc/development/documentation/index.md "Documentation guidelines" [js-styleguide]: doc/development/fe_guide/style_guide_js.md "JavaScript styleguide" [scss-styleguide]: doc/development/fe_guide/style_guide_scss.md "SCSS styleguide" [newlines-styleguide]: doc/development/newlines_styleguide.md "Newlines styleguide" diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index e49057b3302..c64d9d48a48 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -0.104.0 +0.105.1 diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION index a8a18875682..b7f8ee41e69 100644 --- a/GITLAB_SHELL_VERSION +++ b/GITLAB_SHELL_VERSION @@ -1 +1 @@ -7.1.2 +7.1.4 diff --git a/GITLAB_WORKHORSE_VERSION b/GITLAB_WORKHORSE_VERSION index fae6e3d04b2..f77856a6f1a 100644 --- a/GITLAB_WORKHORSE_VERSION +++ b/GITLAB_WORKHORSE_VERSION @@ -1 +1 @@ -4.2.1 +4.3.1 @@ -93,6 +93,10 @@ gem 'grape', '~> 1.0' gem 'grape-entity', '~> 0.7.1' gem 'rack-cors', '~> 1.0.0', require: 'rack/cors' +# GraphQL API +gem 'graphql', '~> 1.8.0' +gem 'graphiql-rails', '~> 1.4.10' + # Disable strong_params so that Mash does not respond to :permitted? gem 'hashie-forbidden_attributes' @@ -104,6 +108,7 @@ gem 'hamlit', '~> 2.6.1' # Files attachments gem 'carrierwave', '~> 1.2' +gem 'mini_magick' # Drag and Drop UI gem 'dropzonejs-rails', '~> 0.7.1' @@ -129,7 +134,7 @@ gem 'seed-fu', '~> 2.3.7' # Markdown and HTML processing gem 'html-pipeline', '~> 2.7.1' gem 'deckar01-task_list', '2.0.0' -gem 'gitlab-markup', '~> 1.6.2' +gem 'gitlab-markup', '~> 1.6.4' gem 'redcarpet', '~> 3.4' gem 'commonmarker', '~> 0.17' gem 'RedCloth', '~> 4.3.2' @@ -328,7 +333,7 @@ group :development, :test do gem 'database_cleaner', '~> 1.5.0' gem 'factory_bot_rails', '~> 4.8.2' - gem 'rspec-rails', '~> 3.6.0' + gem 'rspec-rails', '~> 3.7.0' gem 'rspec-retry', '~> 0.4.5' gem 'rspec_profiling', '~> 0.0.5' gem 'rspec-set', '~> 0.1.3' @@ -342,7 +347,7 @@ group :development, :test do gem 'capybara', '~> 2.15' gem 'capybara-screenshot', '~> 1.0.0' - gem 'selenium-webdriver', '~> 3.5' + gem 'selenium-webdriver', '~> 3.12' gem 'spring', '~> 2.0.0' gem 'spring-commands-rspec', '~> 1.0.4' @@ -374,7 +379,7 @@ end group :test do gem 'shoulda-matchers', '~> 3.1.2', require: false - gem 'email_spec', '~> 1.6.0' + gem 'email_spec', '~> 2.2.0' gem 'json-schema', '~> 2.8.0' gem 'webmock', '~> 2.3.2' gem 'rails-controller-testing' if rails5? # Rails5 only gem. @@ -384,7 +389,7 @@ group :test do gem 'test-prof', '~> 0.2.5' end -gem 'octokit', '~> 4.8' +gem 'octokit', '~> 4.9' gem 'mail_room', '~> 0.9.1' @@ -404,18 +409,17 @@ gem 'vmstat', '~> 2.3.0' gem 'sys-filesystem', '~> 1.1.6' # SSH host key support -gem 'net-ssh', '~> 4.2.0' +gem 'net-ssh', '~> 5.0' gem 'sshkey', '~> 1.9.0' # Required for ED25519 SSH host key support group :ed25519 do - gem 'rbnacl-libsodium' - gem 'rbnacl', '~> 4.0' + gem 'ed25519', '~> 1.2' gem 'bcrypt_pbkdf', '~> 1.0' end # Gitaly GRPC client -gem 'gitaly-proto', '~> 0.100.0', require: 'gitaly' +gem 'gitaly-proto', '~> 0.101.0', require: 'gitaly' gem 'grpc', '~> 1.11.0' # Locked until https://github.com/google/protobuf/issues/4210 is closed diff --git a/Gemfile.lock b/Gemfile.lock index 2efd89bf40d..883e580b86b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -115,7 +115,7 @@ GEM mime-types (>= 1.16) cause (0.1) charlock_holmes (0.7.6) - childprocess (0.7.0) + childprocess (0.9.0) ffi (~> 1.0, >= 1.0.11) chronic (0.10.2) chronic_duration (0.10.6) @@ -172,15 +172,17 @@ GEM unf (>= 0.0.5, < 1.0.0) doorkeeper (4.3.2) railties (>= 4.2) - doorkeeper-openid_connect (1.3.0) + doorkeeper-openid_connect (1.4.0) doorkeeper (~> 4.3) json-jwt (~> 1.6) dropzonejs-rails (0.7.2) rails (> 3.1) + ed25519 (1.2.4) email_reply_trimmer (0.1.6) - email_spec (1.6.0) + email_spec (2.2.0) + htmlentities (~> 4.3.3) launchy (~> 2.1) - mail (~> 2.2) + mail (~> 2.7) encryptor (3.0.0) equalizer (0.0.11) erubis (2.7.0) @@ -281,7 +283,7 @@ GEM gettext_i18n_rails (>= 0.7.1) po_to_json (>= 1.0.0) rails (>= 3.2.0) - gitaly-proto (0.100.0) + gitaly-proto (0.101.0) google-protobuf (~> 3.1) grpc (~> 1.10) github-linguist (5.3.3) @@ -294,7 +296,7 @@ GEM flowdock (~> 0.7) gitlab-grit (>= 2.4.1) multi_json - gitlab-gollum-lib (4.2.7.2) + gitlab-gollum-lib (4.2.7.4) gemojione (~> 3.2) github-markup (~> 1.6) gollum-grit_adapter (~> 1.0) @@ -302,7 +304,7 @@ GEM rouge (~> 3.1) sanitize (~> 2.1) stringex (~> 2.6) - gitlab-gollum-rugged_adapter (0.4.4) + gitlab-gollum-rugged_adapter (0.4.4.1) mime-types (>= 1.15) rugged (~> 0.25) gitlab-grit (2.8.2) @@ -310,7 +312,7 @@ GEM diff-lcs (~> 1.1) mime-types (>= 1.16) posix-spawn (~> 0.3) - gitlab-markup (1.6.3) + gitlab-markup (1.6.4) gitlab-styles (2.3.2) rubocop (~> 0.51) rubocop-gitlab-security (~> 0.1.0) @@ -358,12 +360,16 @@ GEM grape-entity (0.7.1) activesupport (>= 4.0) multi_json (>= 1.3.2) - grape-path-helpers (1.0.1) - activesupport (~> 4) + grape-path-helpers (1.0.5) + activesupport (>= 4, < 5.1) grape (~> 1.0) rake (~> 12) grape_logging (1.7.0) grape + graphiql-rails (1.4.10) + railties + sprockets-rails + graphql (1.8.1) grpc (1.11.0) google-protobuf (~> 3.1) googleapis-common-protos-types (~> 1.0.0) @@ -446,7 +452,6 @@ GEM kgio (2.10.0) knapsack (1.16.0) rake - timecop (>= 0.1.0) kubeclient (3.1.0) http (~> 2.2.2) recursive-open-struct (~> 1.0, >= 1.0.4) @@ -493,6 +498,7 @@ GEM mime-types-data (~> 3.2015) mime-types-data (3.2016.0521) mimemagic (0.3.0) + mini_magick (4.8.0) mini_mime (1.0.0) mini_portile2 (2.3.0) minitest (5.7.0) @@ -505,7 +511,7 @@ GEM mustermann (~> 1.0.0) mysql2 (0.4.10) net-ldap (0.16.0) - net-ssh (4.2.0) + net-ssh (5.0.1) netrc (0.11.0) nokogiri (1.8.2) mini_portile2 (~> 2.3.0) @@ -517,7 +523,7 @@ GEM multi_json (~> 1.3) multi_xml (~> 0.5) rack (>= 1.2, < 3) - octokit (4.8.0) + octokit (4.9.0) sawyer (~> 0.8.0, >= 0.5.3) omniauth (1.8.1) hashie (>= 3.4.6, < 3.6.0) @@ -691,10 +697,6 @@ GEM ffi (>= 0.5.0, < 2) rblineprof (0.3.6) debugger-ruby_core_source (~> 1.3) - rbnacl (4.0.2) - ffi - rbnacl-libsodium (1.0.11) - rbnacl (>= 3.0.1) rdoc (6.0.4) re2 (1.1.1) recaptcha (3.0.0) @@ -740,36 +742,36 @@ GEM chunky_png rqrcode-rails3 (0.1.7) rqrcode (>= 0.4.2) - rspec (3.6.0) - rspec-core (~> 3.6.0) - rspec-expectations (~> 3.6.0) - rspec-mocks (~> 3.6.0) - rspec-core (3.6.0) - rspec-support (~> 3.6.0) - rspec-expectations (3.6.0) + rspec (3.7.0) + rspec-core (~> 3.7.0) + rspec-expectations (~> 3.7.0) + rspec-mocks (~> 3.7.0) + rspec-core (3.7.1) + rspec-support (~> 3.7.0) + rspec-expectations (3.7.0) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.6.0) - rspec-mocks (3.6.0) + rspec-support (~> 3.7.0) + rspec-mocks (3.7.0) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.6.0) + rspec-support (~> 3.7.0) rspec-parameterized (0.4.0) binding_of_caller parser proc_to_ast rspec (>= 2.13, < 4) unparser - rspec-rails (3.6.0) + rspec-rails (3.7.2) actionpack (>= 3.0) activesupport (>= 3.0) railties (>= 3.0) - rspec-core (~> 3.6.0) - rspec-expectations (~> 3.6.0) - rspec-mocks (~> 3.6.0) - rspec-support (~> 3.6.0) + rspec-core (~> 3.7.0) + rspec-expectations (~> 3.7.0) + rspec-mocks (~> 3.7.0) + rspec-support (~> 3.7.0) rspec-retry (0.4.5) rspec-core rspec-set (0.1.3) - rspec-support (3.6.0) + rspec-support (3.7.1) rspec_profiling (0.0.5) activerecord pg @@ -828,9 +830,9 @@ GEM activesupport (>= 3.1) select2-rails (3.5.9.3) thor (~> 0.14) - selenium-webdriver (3.5.0) + selenium-webdriver (3.12.0) childprocess (~> 0.5) - rubyzip (~> 1.0) + rubyzip (~> 1.2) sentry-raven (2.7.2) faraday (>= 0.7.6, < 1.0) settingslogic (2.0.9) @@ -1011,8 +1013,9 @@ DEPENDENCIES doorkeeper (~> 4.3) doorkeeper-openid_connect (~> 1.3) dropzonejs-rails (~> 0.7.1) + ed25519 (~> 1.2) email_reply_trimmer (~> 0.1) - email_spec (~> 1.6.0) + email_spec (~> 2.2.0) factory_bot_rails (~> 4.8.2) faraday (~> 0.12) fast_blank @@ -1036,12 +1039,12 @@ DEPENDENCIES gettext (~> 3.2.2) gettext_i18n_rails (~> 1.8.0) gettext_i18n_rails_js (~> 1.3) - gitaly-proto (~> 0.100.0) + gitaly-proto (~> 0.101.0) github-linguist (~> 5.3.3) gitlab-flowdock-git-hook (~> 1.0.1) gitlab-gollum-lib (~> 4.2) gitlab-gollum-rugged_adapter (~> 0.4.4) - gitlab-markup (~> 1.6.2) + gitlab-markup (~> 1.6.4) gitlab-styles (~> 2.3) gitlab_omniauth-ldap (~> 2.0.4) gon (~> 6.2) @@ -1052,6 +1055,8 @@ DEPENDENCIES grape-entity (~> 0.7.1) grape-path-helpers (~> 1.0) grape_logging (~> 1.7) + graphiql-rails (~> 1.4.10) + graphql (~> 1.8.0) grpc (~> 1.11.0) haml_lint (~> 0.26.0) hamlit (~> 2.6.1) @@ -1077,14 +1082,15 @@ DEPENDENCIES loofah (~> 2.2) mail_room (~> 0.9.1) method_source (~> 0.8) + mini_magick minitest (~> 5.7.0) mousetrap-rails (~> 1.4.6) mysql2 (~> 0.4.10) net-ldap - net-ssh (~> 4.2.0) + net-ssh (~> 5.0) nokogiri (~> 1.8.2) oauth2 (~> 1.4) - octokit (~> 4.8) + octokit (~> 4.9) omniauth (~> 1.8) omniauth-auth0 (~> 2.0.0) omniauth-authentiq (~> 0.3.3) @@ -1123,8 +1129,6 @@ DEPENDENCIES rainbow (~> 2.2) raindrops (~> 0.18) rblineprof (~> 0.3.6) - rbnacl (~> 4.0) - rbnacl-libsodium rdoc (~> 6.0) re2 (~> 1.1.1) recaptcha (~> 3.0) @@ -1137,7 +1141,7 @@ DEPENDENCIES rouge (~> 3.1) rqrcode-rails3 (~> 0.1.7) rspec-parameterized - rspec-rails (~> 3.6.0) + rspec-rails (~> 3.7.0) rspec-retry (~> 0.4.5) rspec-set (~> 0.1.3) rspec_profiling (~> 0.0.5) @@ -1154,7 +1158,7 @@ DEPENDENCIES scss_lint (~> 0.56.0) seed-fu (~> 2.3.7) select2-rails (~> 3.5.9) - selenium-webdriver (~> 3.5) + selenium-webdriver (~> 3.12) sentry-raven (~> 2.7) settingslogic (~> 2.0.9) sham_rack (~> 1.3.6) diff --git a/Gemfile.rails5.lock b/Gemfile.rails5.lock index af7305619eb..223717f1818 100644 --- a/Gemfile.rails5.lock +++ b/Gemfile.rails5.lock @@ -3,7 +3,7 @@ GEM specs: RedCloth (4.3.2) abstract_type (0.0.7) - ace-rails-ap (4.1.4) + ace-rails-ap (4.1.2) actioncable (5.0.7) actionpack (= 5.0.7) nio4r (>= 1.2, < 3.0) @@ -54,12 +54,12 @@ GEM akismet (2.0.0) allocations (1.0.5) arel (7.1.4) - asana (0.6.3) + asana (0.6.0) faraday (~> 0.9) faraday_middleware (~> 0.9) faraday_middleware-multi_json (~> 0.0) oauth2 (~> 1.0) - asciidoctor (1.5.6.1) + asciidoctor (1.5.6.2) asciidoctor-plantuml (0.0.8) asciidoctor (~> 1.5) asset_sync (2.4.0) @@ -71,10 +71,8 @@ GEM atomic (1.1.99) attr_encrypted (3.1.0) encryptor (~> 3.0.0) - attr_required (1.0.1) - autoprefixer-rails (8.1.0.1) - execjs - awesome_print (1.2.0) + attr_required (1.0.0) + awesome_print (1.8.0) axiom-types (0.1.1) descendants_tracker (~> 0.0.4) ice_nine (~> 0.11.0) @@ -90,15 +88,12 @@ GEM erubis (>= 2.6.6) rack (>= 0.9.0) bindata (2.4.3) - binding_of_caller (0.7.3) + binding_of_caller (0.7.2) debug_inspector (>= 0.0.1) blankslate (2.1.2.4) - bootstrap-sass (3.3.7) - autoprefixer-rails (>= 5.2.1) - sass (>= 3.3.4) bootstrap_form (2.7.0) brakeman (4.2.1) - browser (2.5.3) + browser (2.2.0) builder (3.2.3) bullet (5.5.1) activesupport (>= 3.0.0) @@ -107,32 +102,33 @@ GEM bundler (~> 1.2) thor (~> 0.18) byebug (9.0.6) - capybara (2.18.0) + capybara (2.15.1) addressable mini_mime (>= 0.1.3) nokogiri (>= 1.3.3) rack (>= 1.0.0) rack-test (>= 0.5.4) - xpath (>= 2.0, < 4.0) - capybara-screenshot (1.0.18) + xpath (~> 2.0) + capybara-screenshot (1.0.14) capybara (>= 1.0, < 3) launchy - carrierwave (1.2.2) + carrierwave (1.2.1) activemodel (>= 4.0.0) activesupport (>= 4.0.0) mime-types (>= 1.16) - charlock_holmes (0.7.5) + cause (0.1) + charlock_holmes (0.7.6) childprocess (0.9.0) ffi (~> 1.0, >= 1.0.11) chronic (0.10.2) chronic_duration (0.10.6) numerizer (~> 0.1.1) - chunky_png (1.3.10) + chunky_png (1.3.5) citrus (3.0.2) - coderay (1.1.2) + coderay (1.1.1) coercible (1.0.0) descendants_tracker (~> 0.0.1) - commonmarker (0.17.9) + commonmarker (0.17.8) ruby-enum (~> 0.5) concord (0.1.5) adamantium (~> 0.2.0) @@ -145,11 +141,11 @@ GEM safe_yaml (~> 1.0.0) crass (1.0.4) creole (0.5.0) - css_parser (1.6.0) + css_parser (1.5.0) addressable - daemons (1.2.6) + daemons (1.2.3) database_cleaner (1.5.3) - debug_inspector (0.0.3) + debug_inspector (0.0.2) debugger-ruby_core_source (1.3.8) deckar01-task_list (2.0.0) html-pipeline @@ -159,44 +155,46 @@ GEM activerecord (>= 3.2.0, < 5.2) descendants_tracker (0.0.4) thread_safe (~> 0.3, >= 0.3.1) - device_detector (1.0.1) - devise (4.4.1) + device_detector (1.0.0) + devise (4.4.3) bcrypt (~> 3.0) orm_adapter (~> 0.1) - railties (>= 4.1.0, < 5.2) + railties (>= 4.1.0, < 6.0) responders warden (~> 1.2.3) - devise-two-factor (3.0.2) - activesupport (< 5.2) + devise-two-factor (3.0.0) + activesupport attr_encrypted (>= 1.3, < 4, != 2) devise (~> 4.0) - railties (< 5.2) + railties rotp (~> 2.0) diff-lcs (1.3) diffy (3.1.0) docile (1.1.5) - domain_name (0.5.20170404) + domain_name (0.5.20180417) unf (>= 0.0.5, < 1.0.0) - doorkeeper (4.3.1) + doorkeeper (4.3.2) railties (>= 4.2) - doorkeeper-openid_connect (1.3.0) + doorkeeper-openid_connect (1.4.0) doorkeeper (~> 4.3) json-jwt (~> 1.6) - dropzonejs-rails (0.7.4) + dropzonejs-rails (0.7.2) rails (> 3.1) - email_reply_trimmer (0.1.10) - email_spec (1.6.0) + ed25519 (1.2.4) + email_reply_trimmer (0.1.6) + email_spec (2.2.0) + htmlentities (~> 4.3.3) launchy (~> 2.1) - mail (~> 2.2) + mail (~> 2.7) encryptor (3.0.0) equalizer (0.0.11) erubis (2.7.0) escape_utils (1.1.1) - et-orbi (1.0.9) + et-orbi (1.0.3) tzinfo - eventmachine (1.2.5) - excon (0.60.0) - execjs (2.7.0) + eventmachine (1.0.8) + excon (0.62.0) + execjs (2.6.0) expression_parser (0.9.0) factory_bot (4.8.2) activesupport (>= 3.0.0) @@ -212,8 +210,8 @@ GEM multi_json fast_blank (1.0.0) fast_gettext (1.6.0) - ffaker (2.8.1) - ffi (1.9.23) + ffaker (2.4.0) + ffi (1.9.18) flay (2.10.0) erubis (~> 2.7.0) path_expander (~> 1.0) @@ -251,13 +249,13 @@ GEM fog-json (1.0.2) fog-core (~> 1.0) multi_json (~> 1.10) - fog-local (0.5.0) - fog-core (>= 1.27, < 3.0) - fog-openstack (0.1.24) - fog-core (~> 1.40) + fog-local (0.3.1) + fog-core (~> 1.27) + fog-openstack (0.1.21) + fog-core (>= 1.40) fog-json (>= 1.0) ipaddress (>= 0.8) - fog-rackspace (0.1.5) + fog-rackspace (0.1.1) fog-core (>= 1.35) fog-json (>= 1.0) fog-xml (>= 0.1) @@ -265,8 +263,8 @@ GEM fog-xml (0.1.3) fog-core nokogiri (>= 1.5.11, < 2.0.0) - font-awesome-rails (4.7.0.3) - railties (>= 3.2, < 5.2) + font-awesome-rails (4.7.0.1) + railties (>= 3.2, < 5.1) foreman (0.84.0) thor (~> 0.19.1) formatador (0.2.5) @@ -277,7 +275,7 @@ GEM rugged (~> 0.21) gemojione (3.3.0) json - get_process_mem (0.2.1) + get_process_mem (0.2.0) gettext (3.2.9) locale (>= 2.0.5) text (>= 1.3.0) @@ -288,7 +286,7 @@ GEM gettext_i18n_rails (>= 0.7.1) po_to_json (>= 1.0.0) rails (>= 3.2.0) - gitaly-proto (0.99.0) + gitaly-proto (0.101.0) google-protobuf (~> 3.1) grpc (~> 1.10) github-linguist (5.3.3) @@ -301,7 +299,7 @@ GEM flowdock (~> 0.7) gitlab-grit (>= 2.4.1) multi_json - gitlab-gollum-lib (4.2.7.2) + gitlab-gollum-lib (4.2.7.4) gemojione (~> 3.2) github-markup (~> 1.6) gollum-grit_adapter (~> 1.0) @@ -309,7 +307,7 @@ GEM rouge (~> 3.1) sanitize (~> 2.1) stringex (~> 2.6) - gitlab-gollum-rugged_adapter (0.4.4) + gitlab-gollum-rugged_adapter (0.4.4.1) mime-types (>= 1.15) rugged (~> 0.25) gitlab-grit (2.8.2) @@ -317,7 +315,7 @@ GEM diff-lcs (~> 1.1) mime-types (>= 1.16) posix-spawn (~> 0.3) - gitlab-markup (1.6.3) + gitlab-markup (1.6.4) gitlab-styles (2.3.2) rubocop (~> 0.51) rubocop-gitlab-security (~> 0.1.0) @@ -353,9 +351,9 @@ GEM multi_json (~> 1.11) os (~> 0.9) signet (~> 0.7) - gpgme (2.0.16) - mini_portile2 (~> 2.3) - grape (1.0.2) + gpgme (2.0.13) + mini_portile2 (~> 2.1) + grape (1.0.3) activesupport builder mustermann-grape (~> 1.0.0) @@ -365,12 +363,16 @@ GEM grape-entity (0.7.1) activesupport (>= 4.0) multi_json (>= 1.3.2) - grape-route-helpers (2.1.0) - activesupport - grape (>= 0.16.0) - rake + grape-path-helpers (1.0.5) + activesupport (>= 4, < 5.1) + grape (~> 1.0) + rake (~> 12) grape_logging (1.7.0) grape + graphiql-rails (1.4.10) + railties + sprockets-rails + graphql (1.8.1) grpc (1.11.0) google-protobuf (~> 3.1) googleapis-common-protos-types (~> 1.0.0) @@ -383,23 +385,23 @@ GEM rake (>= 10, < 13) rubocop (>= 0.49.0) sysexits (~> 1.1) - hamlit (2.6.2) + hamlit (2.6.1) temple (~> 0.7.6) thor tilt - hashdiff (0.3.7) + hashdiff (0.3.4) hashie (3.5.7) hashie-forbidden_attributes (0.1.1) hashie (>= 3.0) health_check (2.6.0) rails (>= 4.0) - hipchat (1.5.4) + hipchat (1.5.2) httparty mimemagic html-pipeline (2.7.1) activesupport (>= 2) nokogiri (>= 1.4) - html2text (0.2.1) + html2text (0.2.0) nokogiri (~> 1.6) htmlentities (4.3.4) http (2.2.2) @@ -417,10 +419,13 @@ GEM httpclient (2.8.3) i18n (1.0.1) concurrent-ruby (~> 1.0) + icalendar (2.4.1) ice_nine (0.11.2) - influxdb (0.5.3) + influxdb (0.2.3) + cause + json ipaddress (0.8.3) - jira-ruby (1.5.0) + jira-ruby (1.4.1) activesupport multipart-post oauth (~> 0.5, >= 0.5.0) @@ -435,30 +440,30 @@ GEM json-schema (2.8.0) addressable (>= 2.4) jwt (1.5.6) - kaminari (1.1.1) + kaminari (1.0.1) activesupport (>= 4.1.0) - kaminari-actionview (= 1.1.1) - kaminari-activerecord (= 1.1.1) - kaminari-core (= 1.1.1) - kaminari-actionview (1.1.1) + kaminari-actionview (= 1.0.1) + kaminari-activerecord (= 1.0.1) + kaminari-core (= 1.0.1) + kaminari-actionview (1.0.1) actionview - kaminari-core (= 1.1.1) - kaminari-activerecord (1.1.1) + kaminari-core (= 1.0.1) + kaminari-activerecord (1.0.1) activerecord - kaminari-core (= 1.1.1) - kaminari-core (1.1.1) - kgio (2.11.2) + kaminari-core (= 1.0.1) + kaminari-core (1.0.1) + kgio (2.10.0) knapsack (1.16.0) rake - kubeclient (3.0.0) + kubeclient (3.1.0) http (~> 2.2.2) - recursive-open-struct (~> 1.0.4) + recursive-open-struct (~> 1.0, >= 1.0.4) rest-client (~> 2.0) launchy (2.4.3) addressable (~> 2.3) - letter_opener (1.6.0) + letter_opener (1.4.1) launchy (~> 2.2) - letter_opener_web (1.3.3) + letter_opener_web (1.3.0) actionmailer (>= 3.2) letter_opener (~> 1.0) railties (>= 3.2) @@ -477,7 +482,7 @@ GEM logging (2.2.2) little-plugger (~> 1.1) multi_json (~> 1.10) - lograge (0.9.0) + lograge (0.10.0) actionpack (>= 4) activesupport (>= 4) railties (>= 4) @@ -491,11 +496,12 @@ GEM memoist (0.16.0) memoizable (0.4.2) thread_safe (~> 0.3, >= 0.3.1) - method_source (0.9.0) + method_source (0.8.2) mime-types (3.1) mime-types-data (~> 3.2015) mime-types-data (3.2016.0521) - mimemagic (0.3.2) + mimemagic (0.3.0) + mini_magick (4.8.0) mini_mime (1.0.0) mini_portile2 (2.3.0) minitest (5.7.0) @@ -507,8 +513,8 @@ GEM mustermann-grape (1.0.0) mustermann (~> 1.0.0) mysql2 (0.4.10) - net-ldap (0.16.1) - net-ssh (4.2.0) + net-ldap (0.16.0) + net-ssh (5.0.1) netrc (0.11.0) nio4r (2.3.1) nokogiri (1.8.2) @@ -521,15 +527,16 @@ GEM multi_json (~> 1.3) multi_xml (~> 0.5) rack (>= 1.2, < 3) - octokit (4.8.0) + octokit (4.9.0) sawyer (~> 0.8.0, >= 0.5.3) omniauth (1.8.1) hashie (>= 3.4.6, < 3.6.0) rack (>= 1.6.2, < 3) omniauth-auth0 (2.0.0) omniauth-oauth2 (~> 1.4) - omniauth-authentiq (0.3.1) - omniauth-oauth2 (~> 1.3, >= 1.3.1) + omniauth-authentiq (0.3.3) + jwt (>= 1.5) + omniauth-oauth2 (>= 1.5) omniauth-azure-oauth2 (0.0.9) jwt (~> 1.0) omniauth (~> 1.0) @@ -561,7 +568,7 @@ GEM omniauth-oauth2 (1.5.0) oauth2 (~> 1.1) omniauth (~> 1.2) - omniauth-oauth2-generic (0.2.4) + omniauth-oauth2-generic (0.2.2) omniauth-oauth2 (~> 1.0) omniauth-saml (1.10.0) omniauth (~> 1.3, >= 1.3.2) @@ -580,7 +587,7 @@ GEM orm_adapter (0.5.0) os (0.9.6) parallel (1.12.1) - parser (2.5.0.5) + parser (2.5.1.0) ast (~> 2.4.0) parslet (1.5.0) blankslate (~> 2.0) @@ -616,9 +623,9 @@ GEM json (>= 1.6.0) posix-spawn (0.3.13) powerpack (0.1.1) - premailer (1.11.1) + premailer (1.10.4) addressable - css_parser (>= 1.6.0) + css_parser (>= 1.4.10) htmlentities (>= 4.0.0) premailer-rails (1.9.7) actionmailer (>= 3, < 6) @@ -628,15 +635,16 @@ GEM parser unparser procto (0.0.3) - prometheus-client-mmap (0.9.2) - pry (0.11.3) + prometheus-client-mmap (0.9.3) + pry (0.10.4) coderay (~> 1.1.0) - method_source (~> 0.9.0) - pry-byebug (3.4.3) - byebug (>= 9.0, < 9.1) + method_source (~> 0.8.1) + slop (~> 3.4) + pry-byebug (3.4.2) + byebug (~> 9.0) pry (~> 0.10) - pry-rails (0.3.6) - pry (>= 0.10.4) + pry-rails (0.3.5) + pry (>= 0.9.10) public_suffix (3.0.2) pyu-ruby-sasl (0.0.3.3) rack (2.0.5) @@ -653,7 +661,7 @@ GEM rack (>= 1.1) rack-protection (2.0.1) rack - rack-proxy (0.6.4) + rack-proxy (0.6.0) rack rack-test (0.6.3) rack (>= 1.0) @@ -691,22 +699,18 @@ GEM thor (>= 0.18.1, < 2.0) rainbow (2.2.2) rake - raindrops (0.19.0) + raindrops (0.18.0) rake (12.3.1) - rb-fsevent (0.10.3) + rb-fsevent (0.10.2) rb-inotify (0.9.10) ffi (>= 0.5.0, < 2) - rblineprof (0.3.7) + rblineprof (0.3.6) debugger-ruby_core_source (~> 1.3) - rbnacl (4.0.2) - ffi - rbnacl-libsodium (1.0.16) - rbnacl (>= 3.0.1) - rdoc (4.3.0) + rdoc (6.0.4) re2 (1.1.1) - recaptcha (3.4.0) + recaptcha (3.0.0) json - recursive-open-struct (1.0.5) + recursive-open-struct (1.1.0) redcarpet (3.4.0) redis (3.3.5) redis-actionpack (5.0.2) @@ -716,8 +720,8 @@ GEM redis-activesupport (5.0.4) activesupport (>= 3, < 6) redis-store (>= 1.3, < 2) - redis-namespace (1.5.3) - redis (~> 3.0, >= 3.0.4) + redis-namespace (1.6.0) + redis (>= 3.0.4) redis-rack (2.0.4) rack (>= 1.5, < 3) redis-store (>= 1.2, < 2) @@ -731,8 +735,7 @@ GEM declarative (< 0.1.0) declarative-option (< 0.2.0) uber (< 0.2.0) - request_store (1.4.0) - rack (>= 1.4) + request_store (1.3.1) responders (2.4.0) actionpack (>= 4.2.0, < 5.3) railties (>= 4.2.0, < 5.3) @@ -741,43 +744,43 @@ GEM mime-types (>= 1.16, < 4.0) netrc (~> 0.8) retriable (3.1.1) - rinku (2.0.4) + rinku (2.0.0) rotp (2.1.2) rouge (3.1.1) - rqrcode (0.10.1) - chunky_png (~> 1.0) + rqrcode (0.7.0) + chunky_png rqrcode-rails3 (0.1.7) rqrcode (>= 0.4.2) - rspec (3.6.0) - rspec-core (~> 3.6.0) - rspec-expectations (~> 3.6.0) - rspec-mocks (~> 3.6.0) - rspec-core (3.6.0) - rspec-support (~> 3.6.0) - rspec-expectations (3.6.0) + rspec (3.7.0) + rspec-core (~> 3.7.0) + rspec-expectations (~> 3.7.0) + rspec-mocks (~> 3.7.0) + rspec-core (3.7.1) + rspec-support (~> 3.7.0) + rspec-expectations (3.7.0) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.6.0) - rspec-mocks (3.6.0) + rspec-support (~> 3.7.0) + rspec-mocks (3.7.0) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.6.0) + rspec-support (~> 3.7.0) rspec-parameterized (0.4.0) binding_of_caller parser proc_to_ast rspec (>= 2.13, < 4) unparser - rspec-rails (3.6.1) + rspec-rails (3.7.2) actionpack (>= 3.0) activesupport (>= 3.0) railties (>= 3.0) - rspec-core (~> 3.6.0) - rspec-expectations (~> 3.6.0) - rspec-mocks (~> 3.6.0) - rspec-support (~> 3.6.0) - rspec-retry (0.4.6) + rspec-core (~> 3.7.0) + rspec-expectations (~> 3.7.0) + rspec-mocks (~> 3.7.0) + rspec-support (~> 3.7.0) + rspec-retry (0.4.5) rspec-core rspec-set (0.1.3) - rspec-support (3.6.0) + rspec-support (3.7.1) rspec_profiling (0.0.5) activerecord pg @@ -792,7 +795,7 @@ GEM unicode-display_width (~> 1.0, >= 1.0.1) rubocop-gitlab-security (0.1.1) rubocop (>= 0.51) - rubocop-rspec (1.22.2) + rubocop-rspec (1.22.1) rubocop (>= 0.52.1) ruby-enum (0.7.2) i18n @@ -802,14 +805,14 @@ GEM ruby-progressbar (1.9.0) ruby-saml (1.7.2) nokogiri (>= 1.5.10) - ruby_parser (3.11.0) - sexp_processor (~> 4.9) + ruby_parser (3.9.0) + sexp_processor (~> 4.1) rubyntlm (0.6.2) - rubypants (0.7.0) + rubypants (0.2.0) rubyzip (1.2.1) - rufus-scheduler (3.4.2) + rufus-scheduler (3.4.0) et-orbi (~> 1.0) - rugged (0.27.0) + rugged (0.27.1) safe_yaml (1.0.4) sanitize (2.1.0) nokogiri (>= 1.4.4) @@ -818,7 +821,7 @@ GEM sass-listen (4.0.0) rb-fsevent (~> 0.9, >= 0.9.4) rb-inotify (~> 0.9, >= 0.9.7) - sass-rails (5.0.7) + sass-rails (5.0.6) railties (>= 4.0.0, < 6) sass (~> 3.1) sprockets (>= 2.8, < 4.0) @@ -834,25 +837,25 @@ GEM seed-fu (2.3.7) activerecord (>= 3.1) activesupport (>= 3.1) - select2-rails (3.5.10) + select2-rails (3.5.9.3) thor (~> 0.14) - selenium-webdriver (3.11.0) + selenium-webdriver (3.12.0) childprocess (~> 0.5) rubyzip (~> 1.2) sentry-raven (2.7.2) faraday (>= 0.7.6, < 1.0) settingslogic (2.0.9) - sexp_processor (4.10.1) + sexp_processor (4.9.0) sham_rack (1.3.6) rack shoulda-matchers (3.1.2) activesupport (>= 4.0.0) - sidekiq (5.1.1) + sidekiq (5.1.3) concurrent-ruby (~> 1.0) connection_pool (~> 2.2, >= 2.2.0) rack-protection (>= 1.5.0) redis (>= 3.3.5, < 5) - sidekiq-cron (0.6.3) + sidekiq-cron (0.6.0) rufus-scheduler (>= 3.3.0) sidekiq (>= 4.2.1) sidekiq-limit_fetch (3.4.0) @@ -862,14 +865,15 @@ GEM faraday (~> 0.9) jwt (>= 1.5, < 3.0) multi_json (~> 1.10) - simple_po_parser (1.1.3) + simple_po_parser (1.1.2) simplecov (0.14.1) docile (~> 1.1.0) json (>= 1.8, < 3) simplecov-html (~> 0.10.0) - simplecov-html (0.10.2) + simplecov-html (0.10.0) slack-notifier (1.5.1) - spring (2.0.2) + slop (3.6.0) + spring (2.0.1) activesupport (>= 4.2) spring-commands-rspec (1.0.4) spring (>= 0.9.1) @@ -882,7 +886,7 @@ GEM sprockets (>= 3.0.0) sqlite3 (1.3.13) sshkey (1.9.0) - stackprof (0.2.11) + stackprof (0.2.10) state_machines (0.5.0) state_machines-activemodel (0.5.1) activemodel (>= 4.1, < 6.0) @@ -891,19 +895,19 @@ GEM activerecord (>= 4.1, < 6.0) state_machines-activemodel (>= 0.5.0) stringex (2.8.4) - sys-filesystem (1.1.9) + sys-filesystem (1.1.6) ffi sysexits (1.2.0) temple (0.7.7) test-prof (0.2.5) text (1.3.1) - thin (1.7.2) + thin (1.7.0) daemons (~> 1.0, >= 1.0.9) eventmachine (~> 1.0, >= 1.0.4) rack (>= 1, < 3) thor (0.19.4) thread_safe (0.3.6) - tilt (2.0.8) + tilt (2.0.6) timecop (0.8.1) timfel-krb5-auth (0.8.3) toml (0.1.2) @@ -923,7 +927,7 @@ GEM unf (0.1.4) unf_ext unf_ext (0.0.7.5) - unicode-display_width (1.3.0) + unicode-display_width (1.3.2) unicorn (5.1.0) kgio (~> 2.6) raindrops (~> 0.7) @@ -940,7 +944,7 @@ GEM parser (>= 2.3.1.2, < 2.6) procto (~> 0.0.2) url_safe_base64 (0.2.2) - validates_hostname (1.0.8) + validates_hostname (1.0.6) activerecord (>= 3.0) activesupport (>= 3.0) version_sorter (2.1.0) @@ -956,7 +960,7 @@ GEM addressable (>= 2.3.6) crack (>= 0.3.2) hashdiff - webpack-rails (0.9.11) + webpack-rails (0.9.10) railties (>= 3.2.0) websocket-driver (0.6.5) websocket-extensions (>= 0.1.0) @@ -967,8 +971,8 @@ GEM rinku with_env (1.1.0) xml-simple (1.1.5) - xpath (3.0.0) - nokogiri (~> 1.8) + xpath (2.1.0) + nokogiri (~> 1.3) PLATFORMS ruby @@ -986,7 +990,7 @@ DEPENDENCIES asciidoctor-plantuml (= 0.0.8) asset_sync (~> 2.4) attr_encrypted (~> 3.1.0) - awesome_print (~> 1.2.0) + awesome_print babosa (~> 1.0.2) base32 (~> 0.3.0) batch-loader (~> 1.2.1) @@ -994,7 +998,6 @@ DEPENDENCIES benchmark-ips (~> 2.3.0) better_errors (~> 2.1.0) binding_of_caller (~> 0.7.2) - bootstrap-sass (~> 3.3.0) bootstrap_form (~> 2.7.0) brakeman (~> 4.2) browser (~> 2.2) @@ -1020,8 +1023,9 @@ DEPENDENCIES doorkeeper (~> 4.3) doorkeeper-openid_connect (~> 1.3) dropzonejs-rails (~> 0.7.1) + ed25519 (~> 1.2) email_reply_trimmer (~> 0.1) - email_spec (~> 1.6.0) + email_spec (~> 2.2.0) factory_bot_rails (~> 4.8.2) faraday (~> 0.12) fast_blank @@ -1045,12 +1049,12 @@ DEPENDENCIES gettext (~> 3.2.2) gettext_i18n_rails (~> 1.8.0) gettext_i18n_rails_js (~> 1.3) - gitaly-proto (~> 0.99.0) + gitaly-proto (~> 0.101.0) github-linguist (~> 5.3.3) gitlab-flowdock-git-hook (~> 1.0.1) gitlab-gollum-lib (~> 4.2) gitlab-gollum-rugged_adapter (~> 0.4.4) - gitlab-markup (~> 1.6.2) + gitlab-markup (~> 1.6.4) gitlab-styles (~> 2.3) gitlab_omniauth-ldap (~> 2.0.4) gon (~> 6.2) @@ -1059,8 +1063,10 @@ DEPENDENCIES gpgme grape (~> 1.0) grape-entity (~> 0.7.1) - grape-route-helpers (~> 2.1.0) + grape-path-helpers (~> 1.0) grape_logging (~> 1.7) + graphiql-rails (~> 1.4.10) + graphql (~> 1.8.0) grpc (~> 1.11.0) haml_lint (~> 0.26.0) hamlit (~> 2.6.1) @@ -1070,6 +1076,7 @@ DEPENDENCIES html-pipeline (~> 2.7.1) html2text httparty (~> 0.13.3) + icalendar influxdb (~> 0.2) jira-ruby (~> 1.4) jquery-atwho-rails (~> 1.3.2) @@ -1077,7 +1084,7 @@ DEPENDENCIES jwt (~> 1.5.6) kaminari (~> 1.0) knapsack (~> 1.16) - kubeclient (~> 3.0) + kubeclient (~> 3.1.0) letter_opener_web (~> 1.3.0) license_finder (~> 3.1) licensee (~> 8.9) @@ -1085,17 +1092,18 @@ DEPENDENCIES loofah (~> 2.2) mail_room (~> 0.9.1) method_source (~> 0.8) + mini_magick minitest (~> 5.7.0) mousetrap-rails (~> 1.4.6) mysql2 (~> 0.4.10) net-ldap - net-ssh (~> 4.2.0) + net-ssh (~> 5.0) nokogiri (~> 1.8.2) oauth2 (~> 1.4) - octokit (~> 4.8) + octokit (~> 4.9) omniauth (~> 1.8) omniauth-auth0 (~> 2.0.0) - omniauth-authentiq (~> 0.3.1) + omniauth-authentiq (~> 0.3.3) omniauth-azure-oauth2 (~> 0.0.9) omniauth-cas3 (~> 1.1.4) omniauth-facebook (~> 4.0.0) @@ -1118,7 +1126,7 @@ DEPENDENCIES peek-sidekiq (~> 1.0.3) pg (~> 0.18.2) premailer-rails (~> 1.9.7) - prometheus-client-mmap (~> 0.9.2) + prometheus-client-mmap (~> 0.9.3) pry-byebug (~> 3.4.1) pry-rails (~> 0.3.4) rack-attack (~> 4.4.1) @@ -1132,21 +1140,19 @@ DEPENDENCIES rainbow (~> 2.2) raindrops (~> 0.18) rblineprof (~> 0.3.6) - rbnacl (~> 4.0) - rbnacl-libsodium - rdoc (~> 4.2) + rdoc (~> 6.0) re2 (~> 1.1.1) recaptcha (~> 3.0) redcarpet (~> 3.4) redis (~> 3.2) - redis-namespace (~> 1.5.2) + redis-namespace (~> 1.6.0) redis-rails (~> 5.0.2) request_store (~> 1.3) responders (~> 2.0) rouge (~> 3.1) rqrcode-rails3 (~> 0.1.7) rspec-parameterized - rspec-rails (~> 3.6.0) + rspec-rails (~> 3.7.0) rspec-retry (~> 0.4.5) rspec-set (~> 0.1.3) rspec_profiling (~> 0.0.5) @@ -1154,6 +1160,7 @@ DEPENDENCIES rubocop-rspec (~> 1.22.1) ruby-fogbugz (~> 0.2.1) ruby-prof (~> 0.17.0) + ruby-progressbar ruby_parser (~> 3.8) rufus-scheduler (~> 3.4) rugged (~> 0.27) @@ -1162,12 +1169,12 @@ DEPENDENCIES scss_lint (~> 0.56.0) seed-fu (~> 2.3.7) select2-rails (~> 3.5.9) - selenium-webdriver (~> 3.5) + selenium-webdriver (~> 3.12) sentry-raven (~> 2.7) settingslogic (~> 2.0.9) sham_rack (~> 1.3.6) shoulda-matchers (~> 3.1.2) - sidekiq (~> 5.0) + sidekiq (~> 5.1) sidekiq-cron (~> 0.6.0) sidekiq-limit_fetch (~> 3.4) simple_po_parser (~> 1.1.2) @@ -1199,4 +1206,4 @@ DEPENDENCIES wikicloth (= 0.8.1) BUNDLED WITH - 1.16.1 + 1.16.2 diff --git a/INSTALLATION_TYPE b/INSTALLATION_TYPE new file mode 100644 index 00000000000..5a18cd2fbf6 --- /dev/null +++ b/INSTALLATION_TYPE @@ -0,0 +1 @@ +source diff --git a/PROCESS.md b/PROCESS.md index f206506f7c5..a46fd8c25b4 100644 --- a/PROCESS.md +++ b/PROCESS.md @@ -168,6 +168,8 @@ the stable branch are: * Fixes for [regressions](#regressions) * Fixes for security issues +* Fixes or improvements to automated QA scenarios +* Documentation updates for changes in the same release * New or updated translations (as long as they do not touch application code) During the feature freeze all merge requests that are meant to go into the @@ -184,11 +186,7 @@ next patch release. If a merge request is to be picked into more than one release it will need one `Pick into X.Y` label per release where the merge request should be back-ported -to. - -For example, if the current patch release is `10.1.1` and a regression fix needs -to be backported down to the `9.5` release, you will need to assign it the -`10.1` milestone and the following labels: +to. For example: - `Pick into 10.1` - `Pick into 10.0` diff --git a/README.md b/README.md index 0266fe82c82..8bd667b3dac 100644 --- a/README.md +++ b/README.md @@ -126,5 +126,5 @@ Please see [Getting help for GitLab](https://about.gitlab.com/getting-help/) on ## Is it awesome? -Thanks for [asking this question](https://twitter.com/supersloth/status/489462789384056832) Joshua. [These people](https://twitter.com/gitlab/likes) seem to like it. + diff --git a/app/assets/images/ci_favicons/dev/favicon_status_canceled.ico b/app/assets/images/ci_favicons/dev/favicon_status_canceled.ico Binary files differdeleted file mode 100644 index 4af3582b60d..00000000000 --- a/app/assets/images/ci_favicons/dev/favicon_status_canceled.ico +++ /dev/null diff --git a/app/assets/images/ci_favicons/dev/favicon_status_created.ico b/app/assets/images/ci_favicons/dev/favicon_status_created.ico Binary files differdeleted file mode 100644 index 13639da2e8a..00000000000 --- a/app/assets/images/ci_favicons/dev/favicon_status_created.ico +++ /dev/null diff --git a/app/assets/images/ci_favicons/dev/favicon_status_failed.ico b/app/assets/images/ci_favicons/dev/favicon_status_failed.ico Binary files differdeleted file mode 100644 index 5f0e711b104..00000000000 --- a/app/assets/images/ci_favicons/dev/favicon_status_failed.ico +++ /dev/null diff --git a/app/assets/images/ci_favicons/dev/favicon_status_manual.ico b/app/assets/images/ci_favicons/dev/favicon_status_manual.ico Binary files differdeleted file mode 100644 index 8b1168a1267..00000000000 --- a/app/assets/images/ci_favicons/dev/favicon_status_manual.ico +++ /dev/null diff --git a/app/assets/images/ci_favicons/dev/favicon_status_not_found.ico b/app/assets/images/ci_favicons/dev/favicon_status_not_found.ico Binary files differdeleted file mode 100644 index ed19b69e1c5..00000000000 --- a/app/assets/images/ci_favicons/dev/favicon_status_not_found.ico +++ /dev/null diff --git a/app/assets/images/ci_favicons/dev/favicon_status_pending.ico b/app/assets/images/ci_favicons/dev/favicon_status_pending.ico Binary files differdeleted file mode 100644 index 5dfefd4cc5a..00000000000 --- a/app/assets/images/ci_favicons/dev/favicon_status_pending.ico +++ /dev/null diff --git a/app/assets/images/ci_favicons/dev/favicon_status_running.ico b/app/assets/images/ci_favicons/dev/favicon_status_running.ico Binary files differdeleted file mode 100644 index a41539c0e3e..00000000000 --- a/app/assets/images/ci_favicons/dev/favicon_status_running.ico +++ /dev/null diff --git a/app/assets/images/ci_favicons/dev/favicon_status_skipped.ico b/app/assets/images/ci_favicons/dev/favicon_status_skipped.ico Binary files differdeleted file mode 100644 index 2c1ae552b93..00000000000 --- a/app/assets/images/ci_favicons/dev/favicon_status_skipped.ico +++ /dev/null diff --git a/app/assets/images/ci_favicons/dev/favicon_status_success.ico b/app/assets/images/ci_favicons/dev/favicon_status_success.ico Binary files differdeleted file mode 100644 index 70f0ca61eca..00000000000 --- a/app/assets/images/ci_favicons/dev/favicon_status_success.ico +++ /dev/null diff --git a/app/assets/images/ci_favicons/dev/favicon_status_warning.ico b/app/assets/images/ci_favicons/dev/favicon_status_warning.ico Binary files differdeleted file mode 100644 index db289e03eb1..00000000000 --- a/app/assets/images/ci_favicons/dev/favicon_status_warning.ico +++ /dev/null diff --git a/app/assets/images/ci_favicons/favicon_status_canceled.ico b/app/assets/images/ci_favicons/favicon_status_canceled.ico Binary files differdeleted file mode 100644 index 23adcffff50..00000000000 --- a/app/assets/images/ci_favicons/favicon_status_canceled.ico +++ /dev/null diff --git a/app/assets/images/ci_favicons/favicon_status_canceled.png b/app/assets/images/ci_favicons/favicon_status_canceled.png Binary files differnew file mode 100644 index 00000000000..8adaa9c600b --- /dev/null +++ b/app/assets/images/ci_favicons/favicon_status_canceled.png diff --git a/app/assets/images/ci_favicons/favicon_status_created.ico b/app/assets/images/ci_favicons/favicon_status_created.ico Binary files differdeleted file mode 100644 index f9d93b390d8..00000000000 --- a/app/assets/images/ci_favicons/favicon_status_created.ico +++ /dev/null diff --git a/app/assets/images/ci_favicons/favicon_status_created.png b/app/assets/images/ci_favicons/favicon_status_created.png Binary files differnew file mode 100644 index 00000000000..ca788dd0034 --- /dev/null +++ b/app/assets/images/ci_favicons/favicon_status_created.png diff --git a/app/assets/images/ci_favicons/favicon_status_failed.ico b/app/assets/images/ci_favicons/favicon_status_failed.ico Binary files differdeleted file mode 100644 index 28a22ebf724..00000000000 --- a/app/assets/images/ci_favicons/favicon_status_failed.ico +++ /dev/null diff --git a/app/assets/images/ci_favicons/favicon_status_failed.png b/app/assets/images/ci_favicons/favicon_status_failed.png Binary files differnew file mode 100644 index 00000000000..93f1e2772fd --- /dev/null +++ b/app/assets/images/ci_favicons/favicon_status_failed.png diff --git a/app/assets/images/ci_favicons/favicon_status_manual.ico b/app/assets/images/ci_favicons/favicon_status_manual.ico Binary files differdeleted file mode 100644 index dbbf1abf30c..00000000000 --- a/app/assets/images/ci_favicons/favicon_status_manual.ico +++ /dev/null diff --git a/app/assets/images/ci_favicons/favicon_status_manual.png b/app/assets/images/ci_favicons/favicon_status_manual.png Binary files differnew file mode 100644 index 00000000000..c926062c806 --- /dev/null +++ b/app/assets/images/ci_favicons/favicon_status_manual.png diff --git a/app/assets/images/ci_favicons/favicon_status_not_found.ico b/app/assets/images/ci_favicons/favicon_status_not_found.ico Binary files differdeleted file mode 100644 index 49b9b232dd1..00000000000 --- a/app/assets/images/ci_favicons/favicon_status_not_found.ico +++ /dev/null diff --git a/app/assets/images/ci_favicons/favicon_status_not_found.png b/app/assets/images/ci_favicons/favicon_status_not_found.png Binary files differnew file mode 100644 index 00000000000..df3049315a9 --- /dev/null +++ b/app/assets/images/ci_favicons/favicon_status_not_found.png diff --git a/app/assets/images/ci_favicons/favicon_status_pending.ico b/app/assets/images/ci_favicons/favicon_status_pending.ico Binary files differdeleted file mode 100644 index 05962f3f148..00000000000 --- a/app/assets/images/ci_favicons/favicon_status_pending.ico +++ /dev/null diff --git a/app/assets/images/ci_favicons/favicon_status_pending.png b/app/assets/images/ci_favicons/favicon_status_pending.png Binary files differnew file mode 100644 index 00000000000..f7d67d4a230 --- /dev/null +++ b/app/assets/images/ci_favicons/favicon_status_pending.png diff --git a/app/assets/images/ci_favicons/favicon_status_running.ico b/app/assets/images/ci_favicons/favicon_status_running.ico Binary files differdeleted file mode 100644 index 7fa3d4d48d4..00000000000 --- a/app/assets/images/ci_favicons/favicon_status_running.ico +++ /dev/null diff --git a/app/assets/images/ci_favicons/favicon_status_running.png b/app/assets/images/ci_favicons/favicon_status_running.png Binary files differnew file mode 100644 index 00000000000..ff4167c4b20 --- /dev/null +++ b/app/assets/images/ci_favicons/favicon_status_running.png diff --git a/app/assets/images/ci_favicons/favicon_status_skipped.ico b/app/assets/images/ci_favicons/favicon_status_skipped.ico Binary files differdeleted file mode 100644 index b0c26b62068..00000000000 --- a/app/assets/images/ci_favicons/favicon_status_skipped.ico +++ /dev/null diff --git a/app/assets/images/ci_favicons/favicon_status_skipped.png b/app/assets/images/ci_favicons/favicon_status_skipped.png Binary files differnew file mode 100644 index 00000000000..a9c36464b69 --- /dev/null +++ b/app/assets/images/ci_favicons/favicon_status_skipped.png diff --git a/app/assets/images/ci_favicons/favicon_status_success.ico b/app/assets/images/ci_favicons/favicon_status_success.ico Binary files differdeleted file mode 100644 index b150960b5be..00000000000 --- a/app/assets/images/ci_favicons/favicon_status_success.ico +++ /dev/null diff --git a/app/assets/images/ci_favicons/favicon_status_success.png b/app/assets/images/ci_favicons/favicon_status_success.png Binary files differnew file mode 100644 index 00000000000..bcc30c73f5f --- /dev/null +++ b/app/assets/images/ci_favicons/favicon_status_success.png diff --git a/app/assets/images/ci_favicons/favicon_status_warning.ico b/app/assets/images/ci_favicons/favicon_status_warning.ico Binary files differdeleted file mode 100644 index 7e71d71684d..00000000000 --- a/app/assets/images/ci_favicons/favicon_status_warning.ico +++ /dev/null diff --git a/app/assets/images/ci_favicons/favicon_status_warning.png b/app/assets/images/ci_favicons/favicon_status_warning.png Binary files differnew file mode 100644 index 00000000000..6db3b0280f5 --- /dev/null +++ b/app/assets/images/ci_favicons/favicon_status_warning.png diff --git a/app/assets/images/favicon-blue.ico b/app/assets/images/favicon-blue.ico Binary files differdeleted file mode 100644 index 156fcf07588..00000000000 --- a/app/assets/images/favicon-blue.ico +++ /dev/null diff --git a/app/assets/images/favicon-blue.png b/app/assets/images/favicon-blue.png Binary files differnew file mode 100644 index 00000000000..2229fe79462 --- /dev/null +++ b/app/assets/images/favicon-blue.png diff --git a/app/assets/images/favicon-yellow.ico b/app/assets/images/favicon-yellow.ico Binary files differdeleted file mode 100644 index b650f277fb6..00000000000 --- a/app/assets/images/favicon-yellow.ico +++ /dev/null diff --git a/app/assets/images/favicon-yellow.png b/app/assets/images/favicon-yellow.png Binary files differnew file mode 100644 index 00000000000..2d5289818b4 --- /dev/null +++ b/app/assets/images/favicon-yellow.png diff --git a/app/assets/images/favicon.ico b/app/assets/images/favicon.ico Binary files differdeleted file mode 100644 index 3479cbbb46f..00000000000 --- a/app/assets/images/favicon.ico +++ /dev/null diff --git a/app/assets/images/favicon.png b/app/assets/images/favicon.png Binary files differnew file mode 100644 index 00000000000..845e0ec34a5 --- /dev/null +++ b/app/assets/images/favicon.png diff --git a/app/assets/javascripts/badges/components/badge.vue b/app/assets/javascripts/badges/components/badge.vue index d0f60e1d4cb..b4bfaee1d85 100644 --- a/app/assets/javascripts/badges/components/badge.vue +++ b/app/assets/javascripts/badges/components/badge.vue @@ -72,11 +72,11 @@ export default { rel="noopener noreferrer" > <img - class="project-badge" :src="imageUrlWithRetries" + class="project-badge" + aria-hidden="true" @load="onLoad" @error="onError" - aria-hidden="true" /> </a> @@ -91,9 +91,9 @@ export default { > <div class="btn btn-default btn-sm disabled"> <icon + :size="16" class="prepend-left-8 append-right-8" name="doc_image" - :size="16" aria-hidden="true" /> </div> @@ -105,16 +105,16 @@ export default { </div> <button + v-tooltip v-show="hasError" + :title="s__('Badges|Reload badge image')" class="btn btn-transparent btn-sm text-primary" type="button" - v-tooltip - :title="s__('Badges|Reload badge image')" @click="reloadImage" > <icon - name="retry" :size="16" + name="retry" /> </button> </div> diff --git a/app/assets/javascripts/badges/components/badge_form.vue b/app/assets/javascripts/badges/components/badge_form.vue index 5975cb9669e..7a13f74c570 100644 --- a/app/assets/javascripts/badges/components/badge_form.vue +++ b/app/assets/javascripts/badges/components/badge_form.vue @@ -153,10 +153,10 @@ export default { <label for="badge-link-url">{{ s__('Badges|Link') }}</label> <input id="badge-link-url" - type="text" - class="form-control" v-model="linkUrl" :placeholder="$options.badgeLinkUrlPlaceholder" + type="text" + class="form-control" @input="debouncedPreview" /> <span @@ -169,10 +169,10 @@ export default { <label for="badge-image-url">{{ s__('Badges|Badge image URL') }}</label> <input id="badge-image-url" - type="text" - class="form-control" v-model="imageUrl" :placeholder="$options.badgeImageUrlPlaceholder" + type="text" + class="form-control" @input="debouncedPreview" /> <span @@ -184,8 +184,8 @@ export default { <div class="form-group"> <label for="badge-preview">{{ s__('Badges|Badge image preview') }}</label> <badge - id="badge-preview" v-show="renderedBadge && !isRendering" + id="badge-preview" :image-url="renderedImageUrl" :link-url="renderedLinkUrl" /> @@ -202,16 +202,16 @@ export default { <div class="row-content-block"> <loading-button - type="submit" - container-class="btn btn-success" :disabled="!canSubmit" :loading="isSaving" :label="submitButtonLabel" + type="submit" + container-class="btn btn-success" /> <button + v-if="isEditing" class="btn btn-cancel" type="button" - v-if="isEditing" @click="onCancel" >{{ __('Cancel') }}</button> </div> diff --git a/app/assets/javascripts/badges/components/badge_list_row.vue b/app/assets/javascripts/badges/components/badge_list_row.vue index af062bdf8c6..98aa00af0d7 100644 --- a/app/assets/javascripts/badges/components/badge_list_row.vue +++ b/app/assets/javascripts/badges/components/badge_list_row.vue @@ -41,9 +41,9 @@ export default { <template> <div class="gl-responsive-table-row-layout gl-responsive-table-row"> <badge - class="table-section section-30" :image-url="badge.renderedImageUrl" :link-url="badge.renderedLinkUrl" + class="table-section section-30" /> <span class="table-section section-50 str-truncated">{{ badge.linkUrl }}</span> <div class="table-section section-10"> @@ -54,29 +54,29 @@ export default { v-if="canEditBadge" class="table-action-buttons"> <button + :disabled="badge.isDeleting" class="btn btn-default append-right-8" type="button" - :disabled="badge.isDeleting" @click="editBadge(badge)" > <icon - name="pencil" :size="16" :aria-label="__('Edit')" + name="pencil" /> </button> <button + :disabled="badge.isDeleting" class="btn btn-danger" type="button" data-toggle="modal" data-target="#delete-badge-modal" - :disabled="badge.isDeleting" @click="updateBadgeInModal(badge)" > <icon - name="remove" :size="16" :aria-label="__('Delete')" + name="remove" /> </button> <loading-icon diff --git a/app/assets/javascripts/badges/components/badge_settings.vue b/app/assets/javascripts/badges/components/badge_settings.vue index 83f78394238..cc47e56dd1e 100644 --- a/app/assets/javascripts/badges/components/badge_settings.vue +++ b/app/assets/javascripts/badges/components/badge_settings.vue @@ -44,8 +44,8 @@ export default { <gl-modal id="delete-badge-modal" :header-title-text="s__('Badges|Delete badge?')" - footer-primary-button-variant="danger" :footer-primary-button-text="s__('Badges|Delete badge')" + footer-primary-button-variant="danger" @submit="onSubmitModal"> <div class="well"> <badge diff --git a/app/assets/javascripts/behaviors/markdown/copy_as_gfm.js b/app/assets/javascripts/behaviors/markdown/copy_as_gfm.js index 75cf90de0b5..1ea6dd909e9 100644 --- a/app/assets/javascripts/behaviors/markdown/copy_as_gfm.js +++ b/app/assets/javascripts/behaviors/markdown/copy_as_gfm.js @@ -119,7 +119,7 @@ const gfmRules = { return el.outerHTML; }, 'dl'(el, text) { - let lines = text.trim().split('\n'); + let lines = text.replace(/\n\n/g, '\n').trim().split('\n'); // Add two spaces to the front of subsequent list items lines, // or leave the line entirely blank. lines = lines.map((l) => { @@ -129,9 +129,13 @@ const gfmRules = { return ` ${line}`; }); - return `<dl>\n${lines.join('\n')}\n</dl>`; + return `<dl>\n${lines.join('\n')}\n</dl>\n`; }, - 'sub, dt, dd, kbd, q, samp, var, ruby, rt, rp, abbr, summary, details'(el, text) { + 'dt, dd, summary, details'(el, text) { + const tag = el.nodeName.toLowerCase(); + return `<${tag}>${text}</${tag}>\n`; + }, + 'sup, sub, kbd, q, samp, var, ruby, rt, rp, abbr'(el, text) { const tag = el.nodeName.toLowerCase(); return `<${tag}>${text}</${tag}>`; }, @@ -215,22 +219,22 @@ const gfmRules = { return text.replace(/^- /mg, '1. '); }, 'h1'(el, text) { - return `# ${text.trim()}`; + return `# ${text.trim()}\n`; }, 'h2'(el, text) { - return `## ${text.trim()}`; + return `## ${text.trim()}\n`; }, 'h3'(el, text) { - return `### ${text.trim()}`; + return `### ${text.trim()}\n`; }, 'h4'(el, text) { - return `#### ${text.trim()}`; + return `#### ${text.trim()}\n`; }, 'h5'(el, text) { - return `##### ${text.trim()}`; + return `##### ${text.trim()}\n`; }, 'h6'(el, text) { - return `###### ${text.trim()}`; + return `###### ${text.trim()}\n`; }, 'strong'(el, text) { return `**${text}**`; @@ -241,11 +245,13 @@ const gfmRules = { 'del'(el, text) { return `~~${text}~~`; }, - 'sup'(el, text) { - return `^${text}`; - }, 'hr'(el) { - return '-----'; + // extra leading \n is to ensure that there is a blank line between + // a list followed by an hr, otherwise this breaks old redcarpet rendering + return '\n-----\n'; + }, + 'p'(el, text) { + return `${text.trim()}\n`; }, 'table'(el) { const theadEl = el.querySelector('thead'); @@ -263,7 +269,9 @@ const gfmRules = { let before = ''; let after = ''; - switch (cell.style.textAlign) { + const alignment = cell.align || cell.style.textAlign; + + switch (alignment) { case 'center': before = ':'; after = ':'; diff --git a/app/assets/javascripts/blob/viewer/index.js b/app/assets/javascripts/blob/viewer/index.js index f61c0be9230..5485248cfaf 100644 --- a/app/assets/javascripts/blob/viewer/index.js +++ b/app/assets/javascripts/blob/viewer/index.js @@ -70,7 +70,7 @@ export default class BlobViewer { const initialViewer = this.$fileHolder[0].querySelector('.blob-viewer:not(.hidden)'); let initialViewerName = initialViewer.getAttribute('data-type'); - if (this.switcher && location.hash.indexOf('#L') === 0) { + if (this.switcher && window.location.hash.indexOf('#L') === 0) { initialViewerName = 'simple'; } diff --git a/app/assets/javascripts/boards/components/board.js b/app/assets/javascripts/boards/components/board.js index ac06d79fb60..7920e08e4d8 100644 --- a/app/assets/javascripts/boards/components/board.js +++ b/app/assets/javascripts/boards/components/board.js @@ -1,6 +1,5 @@ /* eslint-disable comma-dangle, space-before-function-paren, one-var */ -import $ from 'jquery'; import Sortable from 'sortablejs'; import Vue from 'vue'; import AccessorUtilities from '../../lib/utils/accessor'; @@ -14,17 +13,28 @@ window.gl = window.gl || {}; window.gl.issueBoards = window.gl.issueBoards || {}; gl.issueBoards.Board = Vue.extend({ - template: '#js-board-template', components: { boardList, 'board-delete': gl.issueBoards.BoardDelete, BoardBlankState, }, props: { - list: Object, - disabled: Boolean, - issueLinkBase: String, - rootPath: String, + list: { + type: Object, + default: () => ({}), + }, + disabled: { + type: Boolean, + required: true, + }, + issueLinkBase: { + type: String, + required: true, + }, + rootPath: { + type: String, + required: true, + }, boardId: { type: String, required: true, @@ -46,56 +56,8 @@ gl.issueBoards.Board = Vue.extend({ }); }, deep: true, - }, - detailIssue: { - handler () { - if (!Object.keys(this.detailIssue.issue).length) return; - - const issue = this.list.findIssue(this.detailIssue.issue.id); - - if (issue) { - const offsetLeft = this.$el.offsetLeft; - const boardsList = document.querySelectorAll('.boards-list')[0]; - const left = boardsList.scrollLeft - offsetLeft; - let right = (offsetLeft + this.$el.offsetWidth); - - if (window.innerWidth > 768 && boardsList.classList.contains('is-compact')) { - // -290 here because width of boardsList is animating so therefore - // getting the width here is incorrect - // 290 is the width of the sidebar - right -= (boardsList.offsetWidth - 290); - } else { - right -= boardsList.offsetWidth; - } - - if (right - boardsList.scrollLeft > 0) { - $(boardsList).animate({ - scrollLeft: right - }, this.sortableOptions.animation); - } else if (left > 0) { - $(boardsList).animate({ - scrollLeft: offsetLeft - }, this.sortableOptions.animation); - } - } - }, - deep: true } }, - methods: { - showNewIssueForm() { - this.$refs['board-list'].showIssueForm = !this.$refs['board-list'].showIssueForm; - }, - toggleExpanded(e) { - if (this.list.isExpandable && !e.target.classList.contains('js-no-trigger-collapse')) { - this.list.isExpanded = !this.list.isExpanded; - - if (AccessorUtilities.isLocalStorageAccessSafe()) { - localStorage.setItem(`boards.${this.boardId}.${this.list.type}.expanded`, this.list.isExpanded); - } - } - }, - }, mounted () { this.sortableOptions = gl.issueBoards.getBoardSortableDefaultOptions({ disabled: this.disabled, @@ -125,4 +87,19 @@ gl.issueBoards.Board = Vue.extend({ this.list.isExpanded = !isCollapsed; } }, + methods: { + showNewIssueForm() { + this.$refs['board-list'].showIssueForm = !this.$refs['board-list'].showIssueForm; + }, + toggleExpanded(e) { + if (this.list.isExpandable && !e.target.classList.contains('js-no-trigger-collapse')) { + this.list.isExpanded = !this.list.isExpanded; + + if (AccessorUtilities.isLocalStorageAccessSafe()) { + localStorage.setItem(`boards.${this.boardId}.${this.list.type}.expanded`, this.list.isExpanded); + } + } + }, + }, + template: '#js-board-template', }); diff --git a/app/assets/javascripts/boards/components/board_blank_state.vue b/app/assets/javascripts/boards/components/board_blank_state.vue index 2049eeb9c30..286529b4d13 100644 --- a/app/assets/javascripts/boards/components/board_blank_state.vue +++ b/app/assets/javascripts/boards/components/board_blank_state.vue @@ -72,8 +72,8 @@ export default { :key="index" > <span - class="label-color" - :style="{ backgroundColor: label.color }"> + :style="{ backgroundColor: label.color }" + class="label-color"> </span> {{ label.title }} </li> diff --git a/app/assets/javascripts/boards/components/board_card.vue b/app/assets/javascripts/boards/components/board_card.vue index 33e3369b971..b7d3574bc80 100644 --- a/app/assets/javascripts/boards/components/board_card.vue +++ b/app/assets/javascripts/boards/components/board_card.vue @@ -77,7 +77,6 @@ export default { <template> <li - class="board-card" :class="{ 'user-can-drag': !disabled && issue.id, 'is-disabled': disabled || !issue.id, @@ -85,6 +84,7 @@ export default { }" :index="index" :data-issue-id="issue.id" + class="board-card" @mousedown="mouseDown" @mousemove="mouseMove" @mouseup="showIssue($event)"> diff --git a/app/assets/javascripts/boards/components/board_delete.js b/app/assets/javascripts/boards/components/board_delete.js index 7be98825fda..4dd9aebeed9 100644 --- a/app/assets/javascripts/boards/components/board_delete.js +++ b/app/assets/javascripts/boards/components/board_delete.js @@ -8,13 +8,16 @@ window.gl.issueBoards = window.gl.issueBoards || {}; gl.issueBoards.BoardDelete = Vue.extend({ props: { - list: Object + list: { + type: Object, + default: () => ({}), + }, }, methods: { deleteBoard () { $(this.$el).tooltip('hide'); - if (confirm('Are you sure you want to delete this list?')) { + if (window.confirm('Are you sure you want to delete this list?')) { this.list.destroy(); } } diff --git a/app/assets/javascripts/boards/components/board_list.vue b/app/assets/javascripts/boards/components/board_list.vue index 84a7f277227..5c7565234d8 100644 --- a/app/assets/javascripts/boards/components/board_list.vue +++ b/app/assets/javascripts/boards/components/board_list.vue @@ -87,10 +87,46 @@ export default { mounted() { const options = gl.issueBoards.getBoardSortableDefaultOptions({ scroll: true, - group: 'issues', disabled: this.disabled, filter: '.board-list-count, .is-disabled', dataIdAttr: 'data-issue-id', + group: { + name: 'issues', + /** + * Dynamically determine between which containers + * items can be moved or copied as + * Assignee lists (EE feature) require this behavior + */ + pull: (to, from, dragEl, e) => { + // As per Sortable's docs, `to` should provide + // reference to exact sortable container on which + // we're trying to drag element, but either it is + // a library's bug or our markup structure is too complex + // that `to` never points to correct container + // See https://github.com/RubaXa/Sortable/issues/1037 + // + // So we use `e.target` which is always accurate about + // which element we're currently dragging our card upon + // So from there, we can get reference to actual container + // and thus the container type to enable Copy or Move + if (e.target) { + const containerEl = e.target.closest('.js-board-list') || e.target.querySelector('.js-board-list'); + const toBoardType = containerEl.dataset.boardType; + + if (toBoardType) { + const fromBoardType = this.list.type; + + if ((fromBoardType === 'assignee' && toBoardType === 'label') || + (fromBoardType === 'label' && toBoardType === 'assignee')) { + return 'clone'; + } + } + } + + return true; + }, + revertClone: true, + }, onStart: (e) => { const card = this.$refs.issue[e.oldIndex]; @@ -169,21 +205,22 @@ export default { <template> <div class="board-list-component"> <div + v-if="loading" class="board-list-loading text-center" - aria-label="Loading issues" - v-if="loading"> + aria-label="Loading issues"> <loading-icon /> </div> <board-new-issue + v-if="list.type !== 'closed' && showIssueForm" :group-id="groupId" - :list="list" - v-if="list.type !== 'closed' && showIssueForm"/> + :list="list"/> <ul - class="board-list" v-show="!loading" ref="list" :data-board="list.id" - :class="{ 'is-smaller': showIssueForm }"> + :data-board-type="list.type" + :class="{ 'is-smaller': showIssueForm }" + class="board-list js-board-list"> <board-card v-for="(issue, index) in issues" ref="issue" @@ -196,8 +233,8 @@ export default { :disabled="disabled" :key="issue.id" /> <li - class="board-list-count text-center" v-if="showCount" + class="board-list-count text-center" data-issue-id="-1"> <loading-icon v-show="list.loadingMore" diff --git a/app/assets/javascripts/boards/components/board_new_issue.vue b/app/assets/javascripts/boards/components/board_new_issue.vue index e8dfd95f7ae..ec23b1e7c11 100644 --- a/app/assets/javascripts/boards/components/board_new_issue.vue +++ b/app/assets/javascripts/boards/components/board_new_issue.vue @@ -49,11 +49,12 @@ export default { this.error = false; const labels = this.list.label ? [this.list.label] : []; + const assignees = this.list.assignee ? [this.list.assignee] : []; const issue = new ListIssue({ title: this.title, labels, subscribed: true, - assignees: [], + assignees, project_id: this.selectedProject.id, }); @@ -95,26 +96,26 @@ export default { <div class="board-card"> <form @submit="submit($event)"> <div - class="flash-container" v-if="error" + class="flash-container" > <div class="flash-alert"> An error occurred. Please try again. </div> </div> <label - class="label-light" :for="list.id + '-title'" + class="label-light" > Title </label> <input + ref="input" + v-model="title" + :id="list.id + '-title'" class="form-control" type="text" - v-model="title" - ref="input" autocomplete="off" - :id="list.id + '-title'" /> <project-select v-if="groupId" @@ -122,10 +123,10 @@ export default { /> <div class="clearfix prepend-top-10"> <button + ref="submit-button" + :disabled="disabled" class="btn btn-success float-left" type="submit" - :disabled="disabled" - ref="submit-button" > Submit issue </button> @@ -141,4 +142,3 @@ export default { </div> </div> </template> - diff --git a/app/assets/javascripts/boards/components/board_sidebar.js b/app/assets/javascripts/boards/components/board_sidebar.js index c4ee4f6c855..82fe6b0c5fb 100644 --- a/app/assets/javascripts/boards/components/board_sidebar.js +++ b/app/assets/javascripts/boards/components/board_sidebar.js @@ -21,8 +21,17 @@ window.gl = window.gl || {}; window.gl.issueBoards = window.gl.issueBoards || {}; gl.issueBoards.BoardSidebar = Vue.extend({ + components: { + assigneeTitle, + assignees, + removeBtn: gl.issueBoards.RemoveIssueBtn, + subscriptions, + }, props: { - currentUser: Object + currentUser: { + type: Object, + default: () => ({}), + }, }, data() { return { @@ -64,6 +73,26 @@ gl.issueBoards.BoardSidebar = Vue.extend({ deep: true }, }, + created () { + // Get events from glDropdown + eventHub.$on('sidebar.removeAssignee', this.removeAssignee); + eventHub.$on('sidebar.addAssignee', this.addAssignee); + eventHub.$on('sidebar.removeAllAssignees', this.removeAllAssignees); + eventHub.$on('sidebar.saveAssignees', this.saveAssignees); + }, + beforeDestroy() { + eventHub.$off('sidebar.removeAssignee', this.removeAssignee); + eventHub.$off('sidebar.addAssignee', this.addAssignee); + eventHub.$off('sidebar.removeAllAssignees', this.removeAllAssignees); + eventHub.$off('sidebar.saveAssignees', this.saveAssignees); + }, + mounted () { + new IssuableContext(this.currentUser); + new MilestoneSelect(); + new DueDateSelectors(); + new LabelsSelect(); + new Sidebar(); + }, methods: { closeSidebar () { this.detail.issue = {}; @@ -97,30 +126,4 @@ gl.issueBoards.BoardSidebar = Vue.extend({ }); }, }, - created () { - // Get events from glDropdown - eventHub.$on('sidebar.removeAssignee', this.removeAssignee); - eventHub.$on('sidebar.addAssignee', this.addAssignee); - eventHub.$on('sidebar.removeAllAssignees', this.removeAllAssignees); - eventHub.$on('sidebar.saveAssignees', this.saveAssignees); - }, - beforeDestroy() { - eventHub.$off('sidebar.removeAssignee', this.removeAssignee); - eventHub.$off('sidebar.addAssignee', this.addAssignee); - eventHub.$off('sidebar.removeAllAssignees', this.removeAllAssignees); - eventHub.$off('sidebar.saveAssignees', this.saveAssignees); - }, - mounted () { - new IssuableContext(this.currentUser); - new MilestoneSelect(); - new DueDateSelectors(); - new LabelsSelect(); - new Sidebar(); - }, - components: { - assigneeTitle, - assignees, - removeBtn: gl.issueBoards.RemoveIssueBtn, - subscriptions, - }, }); diff --git a/app/assets/javascripts/boards/components/issue_card_inner.js b/app/assets/javascripts/boards/components/issue_card_inner.js index dcc07810d01..f7d7b910e2f 100644 --- a/app/assets/javascripts/boards/components/issue_card_inner.js +++ b/app/assets/javascripts/boards/components/issue_card_inner.js @@ -9,6 +9,9 @@ window.gl = window.gl || {}; window.gl.issueBoards = window.gl.issueBoards || {}; gl.issueBoards.IssueCardInner = Vue.extend({ + components: { + UserAvatarLink, + }, props: { issue: { type: Object, @@ -35,6 +38,7 @@ gl.issueBoards.IssueCardInner = Vue.extend({ groupId: { type: Number, required: false, + default: null, }, }, data() { @@ -44,9 +48,6 @@ gl.issueBoards.IssueCardInner = Vue.extend({ maxCounter: 99, }; }, - components: { - UserAvatarLink, - }, computed: { numberOverLimit() { return this.issue.assignees.length - this.limitBeforeCounter; diff --git a/app/assets/javascripts/boards/components/modal/empty_state.js b/app/assets/javascripts/boards/components/modal/empty_state.js index 1e5f2383223..888bc9d7ef2 100644 --- a/app/assets/javascripts/boards/components/modal/empty_state.js +++ b/app/assets/javascripts/boards/components/modal/empty_state.js @@ -4,9 +4,6 @@ import modalMixin from '../../mixins/modal_mixins'; gl.issueBoards.ModalEmptyState = Vue.extend({ mixins: [modalMixin], - data() { - return ModalStore.store; - }, props: { newIssuePath: { type: String, @@ -17,6 +14,9 @@ gl.issueBoards.ModalEmptyState = Vue.extend({ required: true, }, }, + data() { + return ModalStore.store; + }, computed: { contents() { const obj = { diff --git a/app/assets/javascripts/boards/components/modal/footer.js b/app/assets/javascripts/boards/components/modal/footer.js index 11bb3e98334..2745ca219ad 100644 --- a/app/assets/javascripts/boards/components/modal/footer.js +++ b/app/assets/javascripts/boards/components/modal/footer.js @@ -7,6 +7,9 @@ import ModalStore from '../../stores/modal_store'; import modalMixin from '../../mixins/modal_mixins'; gl.issueBoards.ModalFooter = Vue.extend({ + components: { + 'lists-dropdown': gl.issueBoards.ModalFooterListsDropdown, + }, mixins: [modalMixin], data() { return { @@ -52,9 +55,6 @@ gl.issueBoards.ModalFooter = Vue.extend({ this.toggleModal(false); }, }, - components: { - 'lists-dropdown': gl.issueBoards.ModalFooterListsDropdown, - }, template: ` <footer class="form-actions add-issues-footer"> diff --git a/app/assets/javascripts/boards/components/modal/header.js b/app/assets/javascripts/boards/components/modal/header.js index 67c29ebca72..5e511bb8935 100644 --- a/app/assets/javascripts/boards/components/modal/header.js +++ b/app/assets/javascripts/boards/components/modal/header.js @@ -5,6 +5,10 @@ import ModalStore from '../../stores/modal_store'; import modalMixin from '../../mixins/modal_mixins'; gl.issueBoards.ModalHeader = Vue.extend({ + components: { + 'modal-tabs': gl.issueBoards.ModalTabs, + modalFilters, + }, mixins: [modalMixin], props: { projectId: { @@ -42,10 +46,6 @@ gl.issueBoards.ModalHeader = Vue.extend({ ModalStore.toggleAll(); }, }, - components: { - 'modal-tabs': gl.issueBoards.ModalTabs, - modalFilters, - }, template: ` <div> <header class="add-issues-header form-actions"> diff --git a/app/assets/javascripts/boards/components/modal/index.js b/app/assets/javascripts/boards/components/modal/index.js index 3083b3e4405..c8b2f45f177 100644 --- a/app/assets/javascripts/boards/components/modal/index.js +++ b/app/assets/javascripts/boards/components/modal/index.js @@ -10,6 +10,13 @@ import './empty_state'; import ModalStore from '../../stores/modal_store'; gl.issueBoards.IssuesModal = Vue.extend({ + components: { + 'modal-header': gl.issueBoards.ModalHeader, + 'modal-list': gl.issueBoards.ModalList, + 'modal-footer': gl.issueBoards.ModalFooter, + 'empty-state': gl.issueBoards.ModalEmptyState, + loadingIcon, + }, props: { newIssuePath: { type: String, @@ -43,6 +50,22 @@ gl.issueBoards.IssuesModal = Vue.extend({ data() { return ModalStore.store; }, + computed: { + showList() { + if (this.activeTab === 'selected') { + return this.selectedIssues.length > 0; + } + + return this.issuesCount > 0; + }, + showEmptyState() { + if (!this.loading && this.issuesCount === 0) { + return true; + } + + return this.activeTab === 'selected' && this.selectedIssues.length === 0; + }, + }, watch: { page() { this.loadIssues(); @@ -80,6 +103,9 @@ gl.issueBoards.IssuesModal = Vue.extend({ deep: true, }, }, + created() { + this.page = 1; + }, methods: { loadIssues(clearIssues = false) { if (!this.showAddIssuesModal) return false; @@ -112,32 +138,6 @@ gl.issueBoards.IssuesModal = Vue.extend({ }); }, }, - computed: { - showList() { - if (this.activeTab === 'selected') { - return this.selectedIssues.length > 0; - } - - return this.issuesCount > 0; - }, - showEmptyState() { - if (!this.loading && this.issuesCount === 0) { - return true; - } - - return this.activeTab === 'selected' && this.selectedIssues.length === 0; - }, - }, - created() { - this.page = 1; - }, - components: { - 'modal-header': gl.issueBoards.ModalHeader, - 'modal-list': gl.issueBoards.ModalList, - 'modal-footer': gl.issueBoards.ModalFooter, - 'empty-state': gl.issueBoards.ModalEmptyState, - loadingIcon, - }, template: ` <div class="add-issues-modal" diff --git a/app/assets/javascripts/boards/components/modal/list.js b/app/assets/javascripts/boards/components/modal/list.js index f86896d2178..11061c72a7b 100644 --- a/app/assets/javascripts/boards/components/modal/list.js +++ b/app/assets/javascripts/boards/components/modal/list.js @@ -3,6 +3,9 @@ import bp from '../../../breakpoints'; import ModalStore from '../../stores/modal_store'; gl.issueBoards.ModalList = Vue.extend({ + components: { + 'issue-card-inner': gl.issueBoards.IssueCardInner, + }, props: { issueLinkBase: { type: String, @@ -20,13 +23,6 @@ gl.issueBoards.ModalList = Vue.extend({ data() { return ModalStore.store; }, - watch: { - activeTab() { - if (this.activeTab === 'all') { - ModalStore.purgeUnselectedIssues(); - } - }, - }, computed: { loopIssues() { if (this.activeTab === 'all') { @@ -50,6 +46,25 @@ gl.issueBoards.ModalList = Vue.extend({ return groups; }, }, + watch: { + activeTab() { + if (this.activeTab === 'all') { + ModalStore.purgeUnselectedIssues(); + } + }, + }, + mounted() { + this.scrollHandlerWrapper = this.scrollHandler.bind(this); + this.setColumnCountWrapper = this.setColumnCount.bind(this); + this.setColumnCount(); + + this.$refs.list.addEventListener('scroll', this.scrollHandlerWrapper); + window.addEventListener('resize', this.setColumnCountWrapper); + }, + beforeDestroy() { + this.$refs.list.removeEventListener('scroll', this.scrollHandlerWrapper); + window.removeEventListener('resize', this.setColumnCountWrapper); + }, methods: { scrollHandler() { const currentPage = Math.floor(this.issues.length / this.perPage); @@ -96,21 +111,6 @@ gl.issueBoards.ModalList = Vue.extend({ } }, }, - mounted() { - this.scrollHandlerWrapper = this.scrollHandler.bind(this); - this.setColumnCountWrapper = this.setColumnCount.bind(this); - this.setColumnCount(); - - this.$refs.list.addEventListener('scroll', this.scrollHandlerWrapper); - window.addEventListener('resize', this.setColumnCountWrapper); - }, - beforeDestroy() { - this.$refs.list.removeEventListener('scroll', this.scrollHandlerWrapper); - window.removeEventListener('resize', this.setColumnCountWrapper); - }, - components: { - 'issue-card-inner': gl.issueBoards.IssueCardInner, - }, template: ` <section class="add-issues-list add-issues-list-columns" diff --git a/app/assets/javascripts/boards/components/new_list_dropdown.js b/app/assets/javascripts/boards/components/new_list_dropdown.js index 71f49319c36..6dcd4aaec43 100644 --- a/app/assets/javascripts/boards/components/new_list_dropdown.js +++ b/app/assets/javascripts/boards/components/new_list_dropdown.js @@ -56,6 +56,7 @@ gl.issueBoards.newListDropdownInit = () => { filterable: true, selectable: true, multiSelect: true, + containerSelector: '.js-tab-container-labels .dropdown-page-one .dropdown-content', clicked (options) { const { e } = options; const label = options.selectedObj; diff --git a/app/assets/javascripts/boards/index.js b/app/assets/javascripts/boards/index.js index 29ab13b8e0b..cdad8d238e3 100644 --- a/app/assets/javascripts/boards/index.js +++ b/app/assets/javascripts/boards/index.js @@ -7,6 +7,7 @@ import Vue from 'vue'; import Flash from '~/flash'; import { __ } from '~/locale'; import '~/vue_shared/models/label'; +import '~/vue_shared/models/assignee'; import FilteredSearchBoards from './filtered_search_boards'; import eventHub from './eventhub'; @@ -15,7 +16,6 @@ import './models/issue'; import './models/list'; import './models/milestone'; import './models/project'; -import './models/assignee'; import './stores/boards_store'; import ModalStore from './stores/modal_store'; import BoardService from './services/board_service'; diff --git a/app/assets/javascripts/boards/models/assignee.js b/app/assets/javascripts/boards/models/assignee.js deleted file mode 100644 index 05dd449e4fd..00000000000 --- a/app/assets/javascripts/boards/models/assignee.js +++ /dev/null @@ -1,12 +0,0 @@ -/* eslint-disable no-unused-vars */ - -class ListAssignee { - constructor(user, defaultAvatar) { - this.id = user.id; - this.name = user.name; - this.username = user.username; - this.avatar = user.avatar_url || defaultAvatar; - } -} - -window.ListAssignee = ListAssignee; diff --git a/app/assets/javascripts/boards/models/list.js b/app/assets/javascripts/boards/models/list.js index 7144f4190e7..1f0fe7f9e85 100644 --- a/app/assets/javascripts/boards/models/list.js +++ b/app/assets/javascripts/boards/models/list.js @@ -1,12 +1,14 @@ /* eslint-disable space-before-function-paren, no-underscore-dangle, class-methods-use-this, consistent-return, no-shadow, no-param-reassign, max-len, no-unused-vars */ /* global ListIssue */ -/* global ListLabel */ + +import ListLabel from '~/vue_shared/models/label'; +import ListAssignee from '~/vue_shared/models/assignee'; import queryData from '../utils/query_data'; const PER_PAGE = 20; class List { - constructor (obj, defaultAvatar) { + constructor(obj, defaultAvatar) { this.id = obj.id; this._uid = this.guid(); this.position = obj.position; @@ -24,6 +26,9 @@ class List { if (obj.label) { this.label = new ListLabel(obj.label); + } else if (obj.user) { + this.assignee = new ListAssignee(obj.user); + this.title = this.assignee.name; } if (this.type !== 'blank' && this.id) { @@ -34,14 +39,26 @@ class List { } guid() { - const s4 = () => Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1); + const s4 = () => + Math.floor((1 + Math.random()) * 0x10000) + .toString(16) + .substring(1); return `${s4()}${s4()}-${s4()}-${s4()}-${s4()}-${s4()}${s4()}${s4()}`; } - save () { - return gl.boardService.createList(this.label.id) + save() { + const entity = this.label || this.assignee; + let entityType = ''; + if (this.label) { + entityType = 'label_id'; + } else { + entityType = 'assignee_id'; + } + + return gl.boardService + .createList(entity.id, entityType) .then(res => res.data) - .then((data) => { + .then(data => { this.id = data.id; this.type = data.list_type; this.position = data.position; @@ -50,25 +67,23 @@ class List { }); } - destroy () { + destroy() { const index = gl.issueBoards.BoardsStore.state.lists.indexOf(this); gl.issueBoards.BoardsStore.state.lists.splice(index, 1); gl.issueBoards.BoardsStore.updateNewListDropdown(this.id); - gl.boardService.destroyList(this.id) - .catch(() => { - // TODO: handle request error - }); + gl.boardService.destroyList(this.id).catch(() => { + // TODO: handle request error + }); } - update () { - gl.boardService.updateList(this.id, this.position) - .catch(() => { - // TODO: handle request error - }); + update() { + gl.boardService.updateList(this.id, this.position).catch(() => { + // TODO: handle request error + }); } - nextPage () { + nextPage() { if (this.issuesSize > this.issues.length) { if (this.issues.length / PER_PAGE >= 1) { this.page += 1; @@ -78,7 +93,7 @@ class List { } } - getIssues (emptyIssues = true) { + getIssues(emptyIssues = true) { const data = queryData(gl.issueBoards.BoardsStore.filter.path, { page: this.page }); if (this.label && data.label_name) { @@ -89,7 +104,8 @@ class List { this.loading = true; } - return gl.boardService.getIssuesForList(this.id, data) + return gl.boardService + .getIssuesForList(this.id, data) .then(res => res.data) .then((data) => { this.loading = false; @@ -103,11 +119,12 @@ class List { }); } - newIssue (issue) { + newIssue(issue) { this.addIssue(issue, null, 0); this.issuesSize += 1; - return gl.boardService.newIssue(this.id, issue) + return gl.boardService + .newIssue(this.id, issue) .then(res => res.data) .then((data) => { issue.id = data.id; @@ -123,13 +140,13 @@ class List { }); } - createIssues (data) { - data.forEach((issueObj) => { + createIssues(data) { + data.forEach(issueObj => { this.addIssue(new ListIssue(issueObj, this.defaultAvatar)); }); } - addIssue (issue, listFrom, newIndex) { + addIssue(issue, listFrom, newIndex) { let moveBeforeId = null; let moveAfterId = null; @@ -152,6 +169,13 @@ class List { issue.addLabel(this.label); } + if (this.assignee) { + if (listFrom && listFrom.type === 'assignee') { + issue.removeAssignee(listFrom.assignee); + } + issue.addAssignee(this.assignee); + } + if (listFrom) { this.issuesSize += 1; @@ -160,29 +184,29 @@ class List { } } - moveIssue (issue, oldIndex, newIndex, moveBeforeId, moveAfterId) { + moveIssue(issue, oldIndex, newIndex, moveBeforeId, moveAfterId) { this.issues.splice(oldIndex, 1); this.issues.splice(newIndex, 0, issue); - gl.boardService.moveIssue(issue.id, null, null, moveBeforeId, moveAfterId) - .catch(() => { - // TODO: handle request error - }); + gl.boardService.moveIssue(issue.id, null, null, moveBeforeId, moveAfterId).catch(() => { + // TODO: handle request error + }); } updateIssueLabel(issue, listFrom, moveBeforeId, moveAfterId) { - gl.boardService.moveIssue(issue.id, listFrom.id, this.id, moveBeforeId, moveAfterId) + gl.boardService + .moveIssue(issue.id, listFrom.id, this.id, moveBeforeId, moveAfterId) .catch(() => { // TODO: handle request error }); } - findIssue (id) { + findIssue(id) { return this.issues.find(issue => issue.id === id); } - removeIssue (removeIssue) { - this.issues = this.issues.filter((issue) => { + removeIssue(removeIssue) { + this.issues = this.issues.filter(issue => { const matchesRemove = removeIssue.id === issue.id; if (matchesRemove) { diff --git a/app/assets/javascripts/boards/services/board_service.js b/app/assets/javascripts/boards/services/board_service.js index 7c90597f77c..029b0971f2c 100644 --- a/app/assets/javascripts/boards/services/board_service.js +++ b/app/assets/javascripts/boards/services/board_service.js @@ -30,11 +30,13 @@ export default class BoardService { return axios.post(this.listsEndpointGenerate, {}); } - createList(labelId) { + createList(entityId, entityType) { + const list = { + [entityType]: entityId, + }; + return axios.post(this.listsEndpoint, { - list: { - label_id: labelId, - }, + list, }); } diff --git a/app/assets/javascripts/boards/stores/boards_store.js b/app/assets/javascripts/boards/stores/boards_store.js index 20e78edf2a2..ffe86468b12 100644 --- a/app/assets/javascripts/boards/stores/boards_store.js +++ b/app/assets/javascripts/boards/stores/boards_store.js @@ -103,8 +103,15 @@ gl.issueBoards.BoardsStore = { const listLabels = issueLists.map(listIssue => listIssue.label); if (!issueTo) { - // Add to new lists issues if it doesn't already exist - listTo.addIssue(issue, listFrom, newIndex); + // Check if target list assignee is already present in this issue + if ((listTo.type === 'assignee' && listFrom.type === 'assignee') && + issue.findAssignee(listTo.assignee)) { + const targetIssue = listTo.findIssue(issue.id); + targetIssue.removeAssignee(listFrom.assignee); + } else { + // Add to new lists issues if it doesn't already exist + listTo.addIssue(issue, listFrom, newIndex); + } } else { listTo.updateIssueLabel(issue, listFrom); issueTo.removeLabel(listFrom.label); @@ -115,7 +122,11 @@ gl.issueBoards.BoardsStore = { list.removeIssue(issue); }); issue.removeLabels(listLabels); - } else { + } else if (listTo.type === 'backlog' && listFrom.type === 'assignee') { + issue.removeAssignee(listFrom.assignee); + listFrom.removeIssue(issue); + } else if ((listTo.type !== 'label' && listFrom.type === 'assignee') || + (listTo.type !== 'assignee' && listFrom.type === 'label')) { listFrom.removeIssue(issue); } }, @@ -126,13 +137,14 @@ gl.issueBoards.BoardsStore = { list.moveIssue(issue, oldIndex, newIndex, beforeId, afterId); }, findList (key, val, type = 'label') { - return this.state.lists.filter((list) => { - const byType = type ? list['type'] === type : true; + const filteredList = this.state.lists.filter((list) => { + const byType = type ? (list.type === type) || (list.type === 'assignee') : true; return list[key] === val && byType; - })[0]; + }); + return filteredList[0]; }, updateFiltersUrl () { - history.pushState(null, null, `?${this.filter.path}`); + window.history.pushState(null, null, `?${this.filter.path}`); } }; diff --git a/app/assets/javascripts/clusters/components/application_row.vue b/app/assets/javascripts/clusters/components/application_row.vue index 30567993322..ec52fdfdf32 100644 --- a/app/assets/javascripts/clusters/components/application_row.vue +++ b/app/assets/javascripts/clusters/components/application_row.vue @@ -125,8 +125,8 @@ <template> <div - class="gl-responsive-table-row gl-responsive-table-row-col-span" :class="rowJsClass" + class="gl-responsive-table-row gl-responsive-table-row-col-span" > <div class="gl-responsive-table-row-layout" @@ -155,8 +155,8 @@ <slot name="description"></slot> </div> <div - class="table-section table-button-footer section-align-top" :class="{ 'section-20': showManageButton, 'section-15': !showManageButton }" + class="table-section table-button-footer section-align-top" role="gridcell" > <div @@ -164,18 +164,18 @@ class="btn-group table-action-buttons" > <a - class="btn" :href="manageLink" + class="btn" > {{ manageButtonLabel }} </a> </div> <div class="btn-group table-action-buttons"> <loading-button - class="js-cluster-application-install-button" :loading="installButtonLoading" :disabled="installButtonDisabled" :label="installButtonLabel" + class="js-cluster-application-install-button" @click="installClicked" /> </div> @@ -187,7 +187,7 @@ role="row" > <div - class="alert alert-danger alert-block append-bottom-0" + class="alert alert-danger alert-block append-bottom-0 clusters-error-alert" role="gridcell" > <div> diff --git a/app/assets/javascripts/clusters/components/applications.vue b/app/assets/javascripts/clusters/components/applications.vue index 9d6be555a2c..8ee7279e544 100644 --- a/app/assets/javascripts/clusters/components/applications.vue +++ b/app/assets/javascripts/clusters/components/applications.vue @@ -152,11 +152,11 @@ export default { <application-row id="helm" :title="applications.helm.title" - title-link="https://docs.helm.sh/" :status="applications.helm.status" :status-reason="applications.helm.statusReason" :request-status="applications.helm.requestStatus" :request-reason="applications.helm.requestReason" + title-link="https://docs.helm.sh/" > <div slot="description"> {{ s__(`ClusterIntegration|Helm streamlines installing @@ -168,11 +168,11 @@ export default { <application-row :id="ingressId" :title="applications.ingress.title" - title-link="https://kubernetes.io/docs/concepts/services-networking/ingress/" :status="applications.ingress.status" :status-reason="applications.ingress.statusReason" :request-status="applications.ingress.requestStatus" :request-reason="applications.ingress.requestReason" + title-link="https://kubernetes.io/docs/concepts/services-networking/ingress/" > <div slot="description"> <p> @@ -191,10 +191,10 @@ export default { class="input-group" > <input - type="text" id="ingress-ip-address" - class="form-control js-ip-address" :value="ingressExternalIp" + type="text" + class="form-control js-ip-address" readonly /> <span class="input-group-append"> @@ -255,12 +255,12 @@ export default { <application-row id="prometheus" :title="applications.prometheus.title" - title-link="https://prometheus.io/docs/introduction/overview/" :manage-link="managePrometheusPath" :status="applications.prometheus.status" :status-reason="applications.prometheus.statusReason" :request-status="applications.prometheus.requestStatus" :request-reason="applications.prometheus.requestReason" + title-link="https://prometheus.io/docs/introduction/overview/" > <div slot="description" @@ -271,11 +271,11 @@ export default { <application-row id="runner" :title="applications.runner.title" - title-link="https://docs.gitlab.com/runner/" :status="applications.runner.status" :status-reason="applications.runner.statusReason" :request-status="applications.runner.requestStatus" :request-reason="applications.runner.requestReason" + title-link="https://docs.gitlab.com/runner/" > <div slot="description"> {{ s__(`ClusterIntegration|GitLab Runner connects to this @@ -287,12 +287,12 @@ export default { <application-row id="jupyter" :title="applications.jupyter.title" - title-link="https://jupyterhub.readthedocs.io/en/stable/" :status="applications.jupyter.status" :status-reason="applications.jupyter.statusReason" :request-status="applications.jupyter.requestStatus" :request-reason="applications.jupyter.requestReason" :install-application-request-params="{ hostname: applications.jupyter.hostname }" + title-link="https://jupyterhub.readthedocs.io/en/stable/" > <div slot="description"> <p> @@ -311,10 +311,10 @@ export default { <div class="input-group"> <input - type="text" - class="form-control js-hostname" v-model="applications.jupyter.hostname" :readonly="jupyterInstalled" + type="text" + class="form-control js-hostname" /> <span class="input-group-btn" diff --git a/app/assets/javascripts/clusters/stores/clusters_store.js b/app/assets/javascripts/clusters/stores/clusters_store.js index 3a4ac09f67c..d90db7b103c 100644 --- a/app/assets/javascripts/clusters/stores/clusters_store.js +++ b/app/assets/javascripts/clusters/stores/clusters_store.js @@ -95,7 +95,7 @@ export default class ClusterStore { this.state.applications.jupyter.hostname = serverAppEntry.hostname || (this.state.applications.ingress.externalIp - ? `jupyter.${this.state.applications.ingress.externalIp}.xip.io` + ? `jupyter.${this.state.applications.ingress.externalIp}.nip.io` : ''); } }); diff --git a/app/assets/javascripts/commit/pipelines/pipelines_table.vue b/app/assets/javascripts/commit/pipelines/pipelines_table.vue index 24d63b99a29..95c4be64d35 100644 --- a/app/assets/javascripts/commit/pipelines/pipelines_table.vue +++ b/app/assets/javascripts/commit/pipelines/pipelines_table.vue @@ -77,9 +77,9 @@ <div class="content-list pipelines"> <loading-icon + v-if="isLoading" :label="s__('Pipelines|Loading Pipelines')" size="3" - v-if="isLoading" class="prepend-top-20" /> @@ -91,8 +91,8 @@ /> <div - class="table-holder" v-else-if="shouldRenderTable" + class="table-holder" > <pipelines-table-component :pipelines="state.pipelines" diff --git a/app/assets/javascripts/commits.js b/app/assets/javascripts/commits.js index 7e2a3573f81..9a3ea7a55b6 100644 --- a/app/assets/javascripts/commits.js +++ b/app/assets/javascripts/commits.js @@ -45,7 +45,7 @@ export default class CommitsList { this.content.fadeTo('fast', 1.0); // Change url so if user reload a page - search results are saved - history.replaceState({ + window.history.replaceState({ page: commitsUrl, }, document.title, commitsUrl); }) diff --git a/app/assets/javascripts/create_merge_request_dropdown.js b/app/assets/javascripts/create_merge_request_dropdown.js index 09d490106df..108082799ef 100644 --- a/app/assets/javascripts/create_merge_request_dropdown.js +++ b/app/assets/javascripts/create_merge_request_dropdown.js @@ -66,8 +66,14 @@ export default class CreateMergeRequestDropdown { } bindEvents() { - this.createMergeRequestButton.addEventListener('click', this.onClickCreateMergeRequestButton.bind(this)); - this.createTargetButton.addEventListener('click', this.onClickCreateMergeRequestButton.bind(this)); + this.createMergeRequestButton.addEventListener( + 'click', + this.onClickCreateMergeRequestButton.bind(this), + ); + this.createTargetButton.addEventListener( + 'click', + this.onClickCreateMergeRequestButton.bind(this), + ); this.branchInput.addEventListener('keyup', this.onChangeInput.bind(this)); this.dropdownToggle.addEventListener('click', this.onClickSetFocusOnBranchNameInput.bind(this)); this.refInput.addEventListener('keyup', this.onChangeInput.bind(this)); @@ -77,7 +83,8 @@ export default class CreateMergeRequestDropdown { checkAbilityToCreateBranch() { this.setUnavailableButtonState(); - axios.get(this.canCreatePath) + axios + .get(this.canCreatePath) .then(({ data }) => { this.setUnavailableButtonState(false); @@ -105,7 +112,8 @@ export default class CreateMergeRequestDropdown { createBranch() { this.isCreatingBranch = true; - return axios.post(this.createBranchPath) + return axios + .post(this.createBranchPath) .then(({ data }) => { this.branchCreated = true; window.location.href = data.url; @@ -116,7 +124,8 @@ export default class CreateMergeRequestDropdown { createMergeRequest() { this.isCreatingMergeRequest = true; - return axios.post(this.createMrPath) + return axios + .post(this.createMrPath) .then(({ data }) => { this.mergeRequestCreated = true; window.location.href = data.url; @@ -195,7 +204,8 @@ export default class CreateMergeRequestDropdown { getRef(ref, target = 'all') { if (!ref) return false; - return axios.get(this.refsPath + ref) + return axios + .get(`${this.refsPath}${encodeURIComponent(ref)}`) .then(({ data }) => { const branches = data[Object.keys(data)[0]]; const tags = data[Object.keys(data)[1]]; @@ -204,7 +214,8 @@ export default class CreateMergeRequestDropdown { if (target === 'branch') { result = CreateMergeRequestDropdown.findByValue(branches, ref); } else { - result = CreateMergeRequestDropdown.findByValue(branches, ref, true) || + result = + CreateMergeRequestDropdown.findByValue(branches, ref, true) || CreateMergeRequestDropdown.findByValue(tags, ref, true); this.suggestedRef = result; } @@ -255,11 +266,13 @@ export default class CreateMergeRequestDropdown { } isBusy() { - return this.isCreatingMergeRequest || + return ( + this.isCreatingMergeRequest || this.mergeRequestCreated || this.isCreatingBranch || this.branchCreated || - this.isGettingRef; + this.isGettingRef + ); } onChangeInput(event) { @@ -271,7 +284,8 @@ export default class CreateMergeRequestDropdown { value = this.branchInput.value; } else if (event.target === this.refInput) { target = 'ref'; - value = event.target.value.slice(0, event.target.selectionStart) + + value = + event.target.value.slice(0, event.target.selectionStart) + event.target.value.slice(event.target.selectionEnd); } else { return false; @@ -396,7 +410,8 @@ export default class CreateMergeRequestDropdown { showNotAvailableMessage(target) { const { input, message } = this.getTargetData(target); - const text = target === 'branch' ? __('Branch is already taken') : __('Source is not available'); + const text = + target === 'branch' ? __('Branch is already taken') : __('Source is not available'); this.removeMessage(target); input.classList.add('gl-field-error-outline'); @@ -459,11 +474,15 @@ export default class CreateMergeRequestDropdown { // target - 'branch' or 'ref' // ref - string - the new value to use as branch or ref updateCreatePaths(target, ref) { - const pathReplacement = `$1${ref}`; + const pathReplacement = `$1${encodeURIComponent(ref)}`; - this.createBranchPath = this.createBranchPath.replace(this.regexps[target].createBranchPath, - pathReplacement); - this.createMrPath = this.createMrPath.replace(this.regexps[target].createMrPath, - pathReplacement); + this.createBranchPath = this.createBranchPath.replace( + this.regexps[target].createBranchPath, + pathReplacement, + ); + this.createMrPath = this.createMrPath.replace( + this.regexps[target].createMrPath, + pathReplacement, + ); } } diff --git a/app/assets/javascripts/cycle_analytics/components/banner.vue b/app/assets/javascripts/cycle_analytics/components/banner.vue index 3204b8dd8e7..410d4873e55 100644 --- a/app/assets/javascripts/cycle_analytics/components/banner.vue +++ b/app/assets/javascripts/cycle_analytics/components/banner.vue @@ -23,9 +23,9 @@ <template> <div class="landing content-block"> <button + :aria-label="__('Dismiss Cycle Analytics introduction box')" class="js-ca-dismiss-button dismiss-button" type="button" - :aria-label="__('Dismiss Cycle Analytics introduction box')" @click="dismissOverviewDialog" > <i diff --git a/app/assets/javascripts/cycle_analytics/components/limit_warning_component.vue b/app/assets/javascripts/cycle_analytics/components/limit_warning_component.vue index 5be17081b58..b626b187651 100644 --- a/app/assets/javascripts/cycle_analytics/components/limit_warning_component.vue +++ b/app/assets/javascripts/cycle_analytics/components/limit_warning_component.vue @@ -19,14 +19,14 @@ class="events-info float-right" > <i - class="fa fa-warning" v-tooltip - aria-hidden="true" :title="n__( 'Limited to showing %d event at most', 'Limited to showing %d events at most', 50 )" + class="fa fa-warning" + aria-hidden="true" data-placement="top" > </i> diff --git a/app/assets/javascripts/cycle_analytics/components/stage_component.vue b/app/assets/javascripts/cycle_analytics/components/stage_component.vue index 907638d798a..312fe75dde4 100644 --- a/app/assets/javascripts/cycle_analytics/components/stage_component.vue +++ b/app/assets/javascripts/cycle_analytics/components/stage_component.vue @@ -38,8 +38,8 @@ <user-avatar-image :img-src="issue.author.avatarUrl"/> <h5 class="item-title issue-title"> <a - class="issue-title" :href="issue.url" + class="issue-title" > {{ issue.title }} </a> diff --git a/app/assets/javascripts/cycle_analytics/components/stage_review_component.vue b/app/assets/javascripts/cycle_analytics/components/stage_review_component.vue index 34aa04083e6..d4735d030fc 100644 --- a/app/assets/javascripts/cycle_analytics/components/stage_review_component.vue +++ b/app/assets/javascripts/cycle_analytics/components/stage_review_component.vue @@ -74,12 +74,12 @@ </template> <template v-else> <span - class="merge-request-branch" v-if="mergeRequest.branch" + class="merge-request-branch" > <icon - name="fork" :size="16" + name="fork" /> <a :href="mergeRequest.branch.url"> {{ mergeRequest.branch.name }} diff --git a/app/assets/javascripts/cycle_analytics/components/stage_staging_component.vue b/app/assets/javascripts/cycle_analytics/components/stage_staging_component.vue index 92f2a95a66a..22637485c01 100644 --- a/app/assets/javascripts/cycle_analytics/components/stage_staging_component.vue +++ b/app/assets/javascripts/cycle_analytics/components/stage_staging_component.vue @@ -38,8 +38,8 @@ <ul class="stage-event-list"> <li v-for="(build, i) in items" - class="stage-event-item item-build-component" :key="i" + class="stage-event-item item-build-component" > <div class="item-details"> <!-- FIXME: Pass an alt attribute here for accessibility --> @@ -52,8 +52,8 @@ #{{ build.id }} </a> <icon - name="fork" :size="16" + name="fork" /> <a :href="build.branch.url" diff --git a/app/assets/javascripts/cycle_analytics/components/stage_test_component.vue b/app/assets/javascripts/cycle_analytics/components/stage_test_component.vue index b84bb6ed792..a0796f299e7 100644 --- a/app/assets/javascripts/cycle_analytics/components/stage_test_component.vue +++ b/app/assets/javascripts/cycle_analytics/components/stage_test_component.vue @@ -64,8 +64,8 @@ #{{ build.id }} </a> <icon - name="fork" :size="16" + name="fork" /> <a :href="build.branch.url" diff --git a/app/assets/javascripts/deploy_keys/components/action_btn.vue b/app/assets/javascripts/deploy_keys/components/action_btn.vue index 67dda0e29cb..7399fc97d45 100644 --- a/app/assets/javascripts/deploy_keys/components/action_btn.vue +++ b/app/assets/javascripts/deploy_keys/components/action_btn.vue @@ -40,9 +40,9 @@ export default { <template> <button - class="btn" :class="[{ disabled: isLoading }, btnCssClass]" :disabled="isLoading" + class="btn" @click="doAction"> <slot></slot> <loading-icon diff --git a/app/assets/javascripts/deploy_keys/components/app.vue b/app/assets/javascripts/deploy_keys/components/app.vue index c41fe55db63..d91e4809126 100644 --- a/app/assets/javascripts/deploy_keys/components/app.vue +++ b/app/assets/javascripts/deploy_keys/components/app.vue @@ -98,7 +98,7 @@ export default { }, disableKey(deployKey, callback) { // eslint-disable-next-line no-alert - if (confirm(s__('DeployKeys|You are going to remove this deploy key. Are you sure?'))) { + if (window.confirm(s__('DeployKeys|You are going to remove this deploy key. Are you sure?'))) { this.service .disableKey(deployKey.id) .then(this.fetchKeys) @@ -116,8 +116,8 @@ export default { <div class="append-bottom-default deploy-keys"> <loading-icon v-if="isLoading && !hasKeys" - size="2" :label="s__('DeployKeys|Loading deploy keys')" + size="2" /> <template v-else-if="hasKeys"> <div class="top-area scrolling-tabs-container inner-page-scroll-tabs"> @@ -138,16 +138,16 @@ export default { <navigation-tabs :tabs="tabs" - @onChangeTab="onChangeTab" scope="deployKeys" + @onChangeTab="onChangeTab" /> </div> <keys-panel - class="qa-project-deploy-keys" :project-id="projectId" :keys="keys[currentTab]" :store="store" :endpoint="endpoint" + class="qa-project-deploy-keys" /> </template> </div> diff --git a/app/assets/javascripts/deploy_keys/components/key.vue b/app/assets/javascripts/deploy_keys/components/key.vue index 6c2af7fa768..f66ca070445 100644 --- a/app/assets/javascripts/deploy_keys/components/key.vue +++ b/app/assets/javascripts/deploy_keys/components/key.vue @@ -135,9 +135,9 @@ export default { <div class="table-mobile-content deploy-project-list"> <template v-if="projects.length > 0"> <a - class="label deploy-project-label" - :title="projectTooltipTitle(firstProject)" v-tooltip + :title="projectTooltipTitle(firstProject)" + class="label deploy-project-label" > <span> {{ firstProject.project.full_name }} @@ -145,22 +145,22 @@ export default { <icon :name="firstProject.can_push ? 'lock-open' : 'lock'"/> </a> <a + v-tooltip v-if="isExpandable" + :title="restProjectsTooltip" class="label deploy-project-label" @click="toggleExpanded" - :title="restProjectsTooltip" - v-tooltip > <span>{{ restProjectsLabel }}</span> </a> <a - v-else-if="isExpanded" + v-tooltip v-for="deployKeysProject in restProjects" + v-else-if="isExpanded" :key="deployKeysProject.project.full_path" - class="label deploy-project-label" :href="deployKeysProject.project.full_path" :title="projectTooltipTitle(deployKeysProject)" - v-tooltip + class="label deploy-project-label" > <span> {{ deployKeysProject.project.full_name }} @@ -181,8 +181,8 @@ export default { </div> <div class="table-mobile-content text-secondary key-created-at"> <span - :title="tooltipTitle(deployKey.created_at)" - v-tooltip> + v-tooltip + :title="tooltipTitle(deployKey.created_at)"> <icon name="calendar"/> <span>{{ timeFormated(deployKey.created_at) }}</span> </span> @@ -198,34 +198,34 @@ export default { {{ __('Enable') }} </action-btn> <a + v-tooltip v-if="deployKey.can_edit" - class="btn btn-default text-secondary" :href="editDeployKeyPath" :title="__('Edit')" + class="btn btn-default text-secondary" data-container="body" - v-tooltip > <icon name="pencil"/> </a> <action-btn + v-tooltip v-if="isRemovable" :deploy-key="deployKey" + :title="__('Remove')" btn-css-class="btn-danger" type="remove" - :title="__('Remove')" data-container="body" - v-tooltip > <icon name="remove"/> </action-btn> <action-btn + v-tooltip v-else-if="isEnabled" :deploy-key="deployKey" + :title="__('Disable')" btn-css-class="btn-warning" type="disable" - :title="__('Disable')" data-container="body" - v-tooltip > <icon name="cancel"/> </action-btn> diff --git a/app/assets/javascripts/deploy_keys/components/keys_panel.vue b/app/assets/javascripts/deploy_keys/components/keys_panel.vue index 3b146c7389a..2f057ca29f6 100644 --- a/app/assets/javascripts/deploy_keys/components/keys_panel.vue +++ b/app/assets/javascripts/deploy_keys/components/keys_panel.vue @@ -59,8 +59,8 @@ export default { /> </template> <div - class="settings-message text-center" v-else + class="settings-message text-center" > {{ s__('DeployKeys|No deploy keys found. Create one with the form above.') }} </div> diff --git a/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js b/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js index d1260ff5373..ed24d1775f4 100644 --- a/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js +++ b/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js @@ -6,7 +6,10 @@ import Vue from 'vue'; const CommentAndResolveBtn = Vue.extend({ props: { - discussionId: String, + discussionId: { + type: String, + required: true, + }, }, data() { return { diff --git a/app/assets/javascripts/diff_notes/components/diff_note_avatars.js b/app/assets/javascripts/diff_notes/components/diff_note_avatars.js index fe9b0795609..40f7c2fe5f3 100644 --- a/app/assets/javascripts/diff_notes/components/diff_note_avatars.js +++ b/app/assets/javascripts/diff_notes/components/diff_note_avatars.js @@ -7,7 +7,15 @@ import Notes from '../../notes'; import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue'; const DiffNoteAvatars = Vue.extend({ - props: ['discussionId'], + components: { + userAvatarImage, + }, + props: { + discussionId: { + type: String, + required: true, + }, + }, data() { return { isVisible: false, @@ -17,77 +25,6 @@ const DiffNoteAvatars = Vue.extend({ collapseIcon, }; }, - components: { - userAvatarImage, - }, - template: ` - <div class="diff-comment-avatar-holders" - :class="discussionClassName" - v-show="notesCount !== 0"> - <div v-if="!isVisible"> - <!-- FIXME: Pass an alt attribute here for accessibility --> - <user-avatar-image - v-for="note in notesSubset" - :key="note.id" - class="diff-comment-avatar js-diff-comment-avatar" - @click.native="clickedAvatar($event)" - :img-src="note.authorAvatar" - :tooltip-text="getTooltipText(note)" - :data-line-type="lineType" - :size="19" - data-html="true" - /> - <span v-if="notesCount > shownAvatars" - class="diff-comments-more-count has-tooltip js-diff-comment-avatar" - data-container="body" - data-placement="top" - ref="extraComments" - role="button" - :data-line-type="lineType" - :title="extraNotesTitle" - @click="clickedAvatar($event)">{{ moreText }}</span> - </div> - <button class="diff-notes-collapse js-diff-comment-avatar" - type="button" - aria-label="Show comments" - :data-line-type="lineType" - @click="clickedAvatar($event)" - v-if="isVisible" - v-html="collapseIcon"> - </button> - </div> - `, - mounted() { - this.$nextTick(() => { - this.addNoCommentClass(); - this.setDiscussionVisible(); - - this.lineType = $(this.$el).closest('.diff-line-num').hasClass('old_line') ? 'old' : 'new'; - }); - - $(document).on('toggle.comments', () => { - this.$nextTick(() => { - this.setDiscussionVisible(); - }); - }); - }, - beforeDestroy() { - this.addNoCommentClass(); - $(document).off('toggle.comments'); - }, - watch: { - storeState: { - handler() { - this.$nextTick(() => { - $('.has-tooltip', this.$el).tooltip('_fixTitle'); - - // We need to add/remove a class to an element that is outside the Vue instance - this.addNoCommentClass(); - }); - }, - deep: true, - }, - }, computed: { discussionClassName() { return `js-diff-avatars-${this.discussionId}`; @@ -128,6 +65,37 @@ const DiffNoteAvatars = Vue.extend({ return `${plusSign}${this.notesCount - this.shownAvatars}`; }, }, + watch: { + storeState: { + handler() { + this.$nextTick(() => { + $('.has-tooltip', this.$el).tooltip('_fixTitle'); + + // We need to add/remove a class to an element that is outside the Vue instance + this.addNoCommentClass(); + }); + }, + deep: true, + }, + }, + mounted() { + this.$nextTick(() => { + this.addNoCommentClass(); + this.setDiscussionVisible(); + + this.lineType = $(this.$el).closest('.diff-line-num').hasClass('old_line') ? 'old' : 'new'; + }); + + $(document).on('toggle.comments', () => { + this.$nextTick(() => { + this.setDiscussionVisible(); + }); + }); + }, + beforeDestroy() { + this.addNoCommentClass(); + $(document).off('toggle.comments'); + }, methods: { clickedAvatar(e) { Notes.instance.onAddDiffNote(e); @@ -164,6 +132,43 @@ const DiffNoteAvatars = Vue.extend({ return `${note.authorName}: ${note.noteTruncated}`; }, }, + template: ` + <div class="diff-comment-avatar-holders" + :class="discussionClassName" + v-show="notesCount !== 0"> + <div v-if="!isVisible"> + <!-- FIXME: Pass an alt attribute here for accessibility --> + <user-avatar-image + v-for="note in notesSubset" + :key="note.id" + class="diff-comment-avatar js-diff-comment-avatar" + @click.native="clickedAvatar($event)" + :img-src="note.authorAvatar" + :tooltip-text="getTooltipText(note)" + :data-line-type="lineType" + :size="19" + data-html="true" + /> + <span v-if="notesCount > shownAvatars" + class="diff-comments-more-count has-tooltip js-diff-comment-avatar" + data-container="body" + data-placement="top" + ref="extraComments" + role="button" + :data-line-type="lineType" + :title="extraNotesTitle" + @click="clickedAvatar($event)">{{ moreText }}</span> + </div> + <button class="diff-notes-collapse js-diff-comment-avatar" + type="button" + aria-label="Show comments" + :data-line-type="lineType" + @click="clickedAvatar($event)" + v-if="isVisible" + v-html="collapseIcon"> + </button> + </div> + `, }); Vue.component('diff-note-avatars', DiffNoteAvatars); diff --git a/app/assets/javascripts/diff_notes/components/jump_to_discussion.js b/app/assets/javascripts/diff_notes/components/jump_to_discussion.js index 8f9186dfb9a..2ce4b050763 100644 --- a/app/assets/javascripts/diff_notes/components/jump_to_discussion.js +++ b/app/assets/javascripts/diff_notes/components/jump_to_discussion.js @@ -10,7 +10,10 @@ import '../mixins/discussion'; const JumpToDiscussion = Vue.extend({ mixins: [DiscussionMixins], props: { - discussionId: String + discussionId: { + type: String, + required: true, + }, }, data: function () { return { @@ -52,6 +55,9 @@ const JumpToDiscussion = Vue.extend({ return lastId; } }, + created() { + this.discussion = this.discussions[this.discussionId]; + }, methods: { jumpToNextUnresolvedDiscussion: function () { let discussionsSelector; @@ -202,9 +208,6 @@ const JumpToDiscussion = Vue.extend({ }); } }, - created() { - this.discussion = this.discussions[this.discussionId]; - }, }); Vue.component('jump-to-discussion', JumpToDiscussion); diff --git a/app/assets/javascripts/diff_notes/components/resolve_btn.js b/app/assets/javascripts/diff_notes/components/resolve_btn.js index 8d66417abac..07f3be29090 100644 --- a/app/assets/javascripts/diff_notes/components/resolve_btn.js +++ b/app/assets/javascripts/diff_notes/components/resolve_btn.js @@ -8,14 +8,38 @@ import Flash from '../../flash'; const ResolveBtn = Vue.extend({ props: { - noteId: Number, - discussionId: String, - resolved: Boolean, - canResolve: Boolean, - resolvedBy: String, - authorName: String, - authorAvatar: String, - noteTruncated: String, + noteId: { + type: Number, + required: true, + }, + discussionId: { + type: String, + required: true, + }, + resolved: { + type: Boolean, + required: true, + }, + canResolve: { + type: Boolean, + required: true, + }, + resolvedBy: { + type: String, + required: true, + }, + authorName: { + type: String, + required: true, + }, + authorAvatar: { + type: String, + required: true, + }, + noteTruncated: { + type: String, + required: true, + }, }, data: function () { return { @@ -23,12 +47,6 @@ const ResolveBtn = Vue.extend({ loading: false }; }, - watch: { - 'discussions': { - handler: 'updateTooltip', - deep: true - } - }, computed: { discussion: function () { return this.discussions[this.discussionId]; @@ -56,6 +74,32 @@ const ResolveBtn = Vue.extend({ return this.note.resolved_by; }, }, + watch: { + 'discussions': { + handler: 'updateTooltip', + deep: true + } + }, + mounted: function () { + $(this.$refs.button).tooltip({ + container: 'body' + }); + }, + beforeDestroy: function () { + CommentsStore.delete(this.discussionId, this.noteId); + }, + created: function () { + CommentsStore.create({ + discussionId: this.discussionId, + noteId: this.noteId, + canResolve: this.canResolve, + resolved: this.resolved, + resolvedBy: this.resolvedBy, + authorName: this.authorName, + authorAvatar: this.authorAvatar, + noteTruncated: this.noteTruncated, + }); + }, methods: { updateTooltip: function () { this.$nextTick(() => { @@ -95,26 +139,6 @@ const ResolveBtn = Vue.extend({ .catch(() => new Flash('An error occurred when trying to resolve a comment. Please try again.')); } }, - mounted: function () { - $(this.$refs.button).tooltip({ - container: 'body' - }); - }, - beforeDestroy: function () { - CommentsStore.delete(this.discussionId, this.noteId); - }, - created: function () { - CommentsStore.create({ - discussionId: this.discussionId, - noteId: this.noteId, - canResolve: this.canResolve, - resolved: this.resolved, - resolvedBy: this.resolvedBy, - authorName: this.authorName, - authorAvatar: this.authorAvatar, - noteTruncated: this.noteTruncated, - }); - } }); Vue.component('resolve-btn', ResolveBtn); diff --git a/app/assets/javascripts/diff_notes/components/resolve_count.js b/app/assets/javascripts/diff_notes/components/resolve_count.js index fe7cf8f5fc1..9f613410e81 100644 --- a/app/assets/javascripts/diff_notes/components/resolve_count.js +++ b/app/assets/javascripts/diff_notes/components/resolve_count.js @@ -9,7 +9,10 @@ import '../mixins/discussion'; window.ResolveCount = Vue.extend({ mixins: [DiscussionMixins], props: { - loggedOut: Boolean + loggedOut: { + type: Boolean, + required: true, + }, }, data: function () { return { diff --git a/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js b/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js index 6a036e96171..210a00c5fc2 100644 --- a/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js +++ b/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js @@ -6,9 +6,18 @@ import Vue from 'vue'; const ResolveDiscussionBtn = Vue.extend({ props: { - discussionId: String, - mergeRequestId: Number, - canResolve: Boolean, + discussionId: { + type: String, + required: true, + }, + mergeRequestId: { + type: Number, + required: true, + }, + canResolve: { + type: Boolean, + required: true, + }, }, data: function() { return { @@ -45,16 +54,16 @@ const ResolveDiscussionBtn = Vue.extend({ } } }, + created: function () { + CommentsStore.createDiscussion(this.discussionId, this.canResolve); + + this.discussion = CommentsStore.state[this.discussionId]; + }, methods: { resolve: function () { ResolveService.toggleResolveForDiscussion(this.mergeRequestId, this.discussionId); } }, - created: function () { - CommentsStore.createDiscussion(this.discussionId, this.canResolve); - - this.discussion = CommentsStore.state[this.discussionId]; - } }); Vue.component('resolve-discussion-btn', ResolveDiscussionBtn); diff --git a/app/assets/javascripts/diff_notes/diff_notes_bundle.js b/app/assets/javascripts/diff_notes/diff_notes_bundle.js index e17daec6a92..d5161ab7df9 100644 --- a/app/assets/javascripts/diff_notes/diff_notes_bundle.js +++ b/app/assets/javascripts/diff_notes/diff_notes_bundle.js @@ -69,9 +69,10 @@ export default () => { gl.diffNotesCompileComponents(); - if (!hasVueMRDiscussionsCookie()) { + const resolveCountAppEl = document.querySelector('#resolve-count-app'); + if (!hasVueMRDiscussionsCookie() && resolveCountAppEl) { new Vue({ - el: '#resolve-count-app', + el: resolveCountAppEl, components: { 'resolve-count': ResolveCount }, diff --git a/app/assets/javascripts/environments/components/container.vue b/app/assets/javascripts/environments/components/container.vue index 6bd7c6b49cb..9aa224fa407 100644 --- a/app/assets/javascripts/environments/components/container.vue +++ b/app/assets/javascripts/environments/components/container.vue @@ -43,17 +43,17 @@ <div class="environments-container"> <loading-icon + v-if="isLoading" class="prepend-top-default" label="Loading environments" - v-if="isLoading" size="3" /> <slot name="emptyState"></slot> <div - class="table-holder" - v-if="!isLoading && environments.length > 0"> + v-if="!isLoading && environments.length > 0" + class="table-holder"> <environment-table :environments="environments" diff --git a/app/assets/javascripts/environments/components/environment_actions.vue b/app/assets/javascripts/environments/components/environment_actions.vue index 0b3fef9fcca..e3652fe739e 100644 --- a/app/assets/javascripts/environments/components/environment_actions.vue +++ b/app/assets/javascripts/environments/components/environment_actions.vue @@ -52,18 +52,18 @@ role="group"> <button v-tooltip + :title="title" + :aria-label="title" + :disabled="isLoading" type="button" class="dropdown btn btn-default dropdown-new js-dropdown-play-icon-container" data-container="body" data-toggle="dropdown" - :title="title" - :aria-label="title" - :disabled="isLoading" > <span> <icon - name="play" :size="12" + name="play" /> <i class="fa fa-caret-down" @@ -79,15 +79,15 @@ v-for="(action, i) in actions" :key="i"> <button + :class="{ disabled: isActionDisabled(action) }" + :disabled="isActionDisabled(action)" type="button" class="js-manual-action-link no-btn btn" @click="onClickAction(action.play_path)" - :class="{ disabled: isActionDisabled(action) }" - :disabled="isActionDisabled(action)" > <icon - name="play" :size="12" + name="play" /> <span> {{ action.name }} diff --git a/app/assets/javascripts/environments/components/environment_external_url.vue b/app/assets/javascripts/environments/components/environment_external_url.vue index ea6f1168c68..68195225d50 100644 --- a/app/assets/javascripts/environments/components/environment_external_url.vue +++ b/app/assets/javascripts/environments/components/environment_external_url.vue @@ -29,17 +29,17 @@ <template> <a v-tooltip + :title="title" + :aria-label="title" + :href="externalUrl" class="btn external-url" data-container="body" target="_blank" rel="noopener noreferrer nofollow" - :title="title" - :aria-label="title" - :href="externalUrl" > <icon - name="external-link" :size="12" + name="external-link" /> </a> </template> diff --git a/app/assets/javascripts/environments/components/environment_item.vue b/app/assets/javascripts/environments/components/environment_item.vue index 23aaab2c441..866e91057ec 100644 --- a/app/assets/javascripts/environments/components/environment_item.vue +++ b/app/assets/javascripts/environments/components/environment_item.vue @@ -427,11 +427,11 @@ </script> <template> <div - class="gl-responsive-table-row" :class="{ 'js-child-row environment-child-row': model.isChildren, 'folder-row': model.isFolder, }" + class="gl-responsive-table-row" role="row"> <div class="table-section section-10" @@ -446,19 +446,19 @@ </div> <a v-if="!model.isFolder" - class="environment-name flex-truncate-parent table-mobile-content" - :href="environmentPath"> + :href="environmentPath" + class="environment-name flex-truncate-parent table-mobile-content"> <span - class="flex-truncate-child" v-tooltip :title="model.name" + class="flex-truncate-child" >{{ model.name }}</span> </a> <span v-else class="folder-name" - @click="onClickFolder" - role="button"> + role="button" + @click="onClickFolder"> <span class="folder-icon"> <i @@ -503,11 +503,11 @@ <span v-if="!model.isFolder && deploymentHasUser"> by <user-avatar-link - class="js-deploy-user-container" :link-href="deploymentUser.web_url" :img-src="deploymentUser.avatar_url" :img-alt="userImageAltDescription" :tooltip-text="deploymentUser.username" + class="js-deploy-user-container" /> </span> </div> @@ -518,8 +518,8 @@ > <a v-if="shouldRenderBuildName" - class="build-link flex-truncate-parent" :href="buildPath" + class="build-link flex-truncate-parent" > <span class="flex-truncate-child">{{ buildName }}</span> </a> diff --git a/app/assets/javascripts/environments/components/environment_monitoring.vue b/app/assets/javascripts/environments/components/environment_monitoring.vue index 8df1b6317e3..947e8c901e9 100644 --- a/app/assets/javascripts/environments/components/environment_monitoring.vue +++ b/app/assets/javascripts/environments/components/environment_monitoring.vue @@ -28,16 +28,16 @@ <template> <a v-tooltip - class="btn monitoring-url d-none d-sm-none d-md-block" - data-container="body" - rel="noopener noreferrer nofollow" :href="monitoringUrl" :title="title" :aria-label="title" + class="btn monitoring-url d-none d-sm-none d-md-block" + data-container="body" + rel="noopener noreferrer nofollow" > <icon - name="chart" :size="12" + name="chart" /> </a> </template> diff --git a/app/assets/javascripts/environments/components/environment_rollback.vue b/app/assets/javascripts/environments/components/environment_rollback.vue index 7515d711c50..310835c5ea9 100644 --- a/app/assets/javascripts/environments/components/environment_rollback.vue +++ b/app/assets/javascripts/environments/components/environment_rollback.vue @@ -39,10 +39,10 @@ </script> <template> <button + :disabled="isLoading" type="button" class="btn d-none d-sm-none d-md-block" @click="onClick" - :disabled="isLoading" > <span v-if="isLastDeployment"> diff --git a/app/assets/javascripts/environments/components/environment_stop.vue b/app/assets/javascripts/environments/components/environment_stop.vue index 7055f208451..eba58bedd6d 100644 --- a/app/assets/javascripts/environments/components/environment_stop.vue +++ b/app/assets/javascripts/environments/components/environment_stop.vue @@ -40,7 +40,7 @@ methods: { onClick() { // eslint-disable-next-line no-alert - if (confirm('Are you sure you want to stop this environment?')) { + if (window.confirm('Are you sure you want to stop this environment?')) { this.isLoading = true; $(this.$el).tooltip('dispose'); @@ -54,13 +54,13 @@ <template> <button v-tooltip + :disabled="isLoading" + :title="title" + :aria-label="title" type="button" class="btn stop-env-link d-none d-sm-none d-md-block" data-container="body" @click="onClick" - :disabled="isLoading" - :title="title" - :aria-label="title" > <i class="fa fa-stop stop-env-icon" diff --git a/app/assets/javascripts/environments/components/environment_terminal_button.vue b/app/assets/javascripts/environments/components/environment_terminal_button.vue index 0dbbbb75e07..f8e3165f8cd 100644 --- a/app/assets/javascripts/environments/components/environment_terminal_button.vue +++ b/app/assets/javascripts/environments/components/environment_terminal_button.vue @@ -30,15 +30,15 @@ <template> <a v-tooltip - class="btn terminal-button d-none d-sm-none d-md-block" - data-container="body" :title="title" :aria-label="title" :href="terminalPath" + class="btn terminal-button d-none d-sm-none d-md-block" + data-container="body" > <icon - name="terminal" :size="12" + name="terminal" /> </a> </template> diff --git a/app/assets/javascripts/environments/components/environments_app.vue b/app/assets/javascripts/environments/components/environments_app.vue index 3da762446c9..b18f02343d6 100644 --- a/app/assets/javascripts/environments/components/environments_app.vue +++ b/app/assets/javascripts/environments/components/environments_app.vue @@ -93,8 +93,8 @@ <div class="top-area"> <tabs :tabs="tabs" - @onChangeTab="onChangeTab" scope="environments" + @onChangeTab="onChangeTab" /> <div @@ -119,8 +119,8 @@ @onChangePage="onChangePage" > <empty-state - slot="emptyState" v-if="!isLoading && state.environments.length === 0" + slot="emptyState" :new-path="newEnvironmentPath" :help-path="helpPagePath" :can-create-environment="canCreateEnvironment" diff --git a/app/assets/javascripts/environments/folder/environments_folder_view.vue b/app/assets/javascripts/environments/folder/environments_folder_view.vue index 5ef5e347387..5f72a39c5cb 100644 --- a/app/assets/javascripts/environments/folder/environments_folder_view.vue +++ b/app/assets/javascripts/environments/folder/environments_folder_view.vue @@ -39,8 +39,8 @@ <template> <div :class="cssContainerClass"> <div - class="top-area" v-if="!isLoading" + class="top-area" > <h4 class="js-folder-name environments-folder-name"> @@ -49,8 +49,8 @@ <tabs :tabs="tabs" - @onChangeTab="onChangeTab" scope="environments" + @onChangeTab="onChangeTab" /> </div> diff --git a/app/assets/javascripts/filtered_search/components/recent_searches_dropdown_content.vue b/app/assets/javascripts/filtered_search/components/recent_searches_dropdown_content.vue index 26618af9515..a8eb8d94be3 100644 --- a/app/assets/javascripts/filtered_search/components/recent_searches_dropdown_content.vue +++ b/app/assets/javascripts/filtered_search/components/recent_searches_dropdown_content.vue @@ -72,9 +72,9 @@ export default { @click="onItemActivated(item.text)"> <span> <span - class="filtered-search-history-dropdown-token" v-for="(token, index) in item.tokens" :key="`dropdown-token-${index}`" + class="filtered-search-history-dropdown-token" > <span class="name">{{ token.prefix }}</span> <span class="value">{{ token.suffix }}</span> diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js index 746a06b7c4f..7fbba7e27cb 100644 --- a/app/assets/javascripts/gl_dropdown.js +++ b/app/assets/javascripts/gl_dropdown.js @@ -602,7 +602,11 @@ GitLabDropdown = (function() { var selector; selector = '.dropdown-content'; if (this.dropdown.find(".dropdown-toggle-page").length) { - selector = ".dropdown-page-one .dropdown-content"; + if (this.options.containerSelector) { + selector = this.options.containerSelector; + } else { + selector = '.dropdown-page-one .dropdown-content'; + } } return $(selector, this.dropdown).empty(); diff --git a/app/assets/javascripts/group_label_subscription.js b/app/assets/javascripts/group_label_subscription.js index 5648cb9a888..d33e3a37580 100644 --- a/app/assets/javascripts/group_label_subscription.js +++ b/app/assets/javascripts/group_label_subscription.js @@ -1,7 +1,12 @@ import $ from 'jquery'; +import { __ } from '~/locale'; import axios from './lib/utils/axios_utils'; import flash from './flash'; -import { __ } from './locale'; + +const tooltipTitles = { + group: __('Unsubscribe at group level'), + project: __('Unsubscribe at project level'), +}; export default class GroupLabelSubscription { constructor(container) { @@ -35,6 +40,7 @@ export default class GroupLabelSubscription { this.$unsubscribeButtons.attr('data-url', url); axios.post(url) + .then(() => GroupLabelSubscription.setNewTooltip($btn)) .then(() => this.toggleSubscriptionButtons()) .catch(() => flash(__('There was an error when subscribing to this label.'))); } @@ -44,4 +50,14 @@ export default class GroupLabelSubscription { this.$subscribeButtons.toggleClass('hidden'); this.$unsubscribeButtons.toggleClass('hidden'); } + + static setNewTooltip($button) { + if (!$button.hasClass('js-subscribe-button')) return; + + const type = $button.hasClass('js-group-level') ? 'group' : 'project'; + const newTitle = tooltipTitles[type]; + + $('.js-unsubscribe-button', $button.closest('.label-actions-list')) + .tooltip('hide').attr('title', newTitle).tooltip('_fixTitle'); + } } diff --git a/app/assets/javascripts/groups/components/app.vue b/app/assets/javascripts/groups/components/app.vue index 22eb7bd44c5..1def38bb336 100644 --- a/app/assets/javascripts/groups/components/app.vue +++ b/app/assets/javascripts/groups/components/app.vue @@ -216,10 +216,10 @@ export default { <template> <div> <loading-icon - class="loading-animation prepend-top-20" - size="2" v-if="isLoading" :label="s__('GroupsTree|Loading groups')" + class="loading-animation prepend-top-20" + size="2" /> <groups-component v-if="!isLoading" @@ -230,10 +230,10 @@ export default { /> <deprecated-modal v-show="showModal" - kind="warning" :primary-button-label="__('Leave')" :title="__('Are you sure?')" :text="groupLeaveConfirmationMessage" + kind="warning" @cancel="hideLeaveGroupModal" @submit="leaveGroup" /> diff --git a/app/assets/javascripts/groups/components/group_item.vue b/app/assets/javascripts/groups/components/group_item.vue index 7f64a9bd741..efbf2e3a295 100644 --- a/app/assets/javascripts/groups/components/group_item.vue +++ b/app/assets/javascripts/groups/components/group_item.vue @@ -71,14 +71,14 @@ export default { <template> <li - @click.stop="onClickRowGroup" :id="groupDomId" :class="rowClass" class="group-row" + @click.stop="onClickRowGroup" > <div - class="group-row-contents" - :class="{ 'project-row-contents': !isGroup }"> + :class="{ 'project-row-contents': !isGroup }" + class="group-row-contents"> <item-actions v-if="isGroup" :group="group" @@ -99,8 +99,8 @@ export default { /> </div> <div - class="avatar-container prepend-top-8 prepend-left-5 s24 d-none d-sm-block" :class="{ 'content-loading': group.isChildrenLoading }" + class="avatar-container prepend-top-8 prepend-left-5 s24 d-none d-sm-block" > <a :href="group.relativePath" @@ -108,14 +108,14 @@ export default { > <img v-if="hasAvatar" - class="avatar s24" :src="group.avatarUrl" + class="avatar s24" /> <identicon v-else - size-class="s24" :entity-id="group.id" :entity-name="group.name" + size-class="s24" /> </a> </div> diff --git a/app/assets/javascripts/groups/components/item_actions.vue b/app/assets/javascripts/groups/components/item_actions.vue index 87065b3d6e3..24eec4901ec 100644 --- a/app/assets/javascripts/groups/components/item_actions.vue +++ b/app/assets/javascripts/groups/components/item_actions.vue @@ -54,13 +54,13 @@ export default { <a v-tooltip v-if="group.canLeave" - @click.prevent="onLeaveGroup" :href="group.leavePath" :title="leaveBtnTitle" :aria-label="leaveBtnTitle" data-container="body" data-placement="bottom" - class="leave-group btn no-expand"> + class="leave-group btn no-expand" + @click.prevent="onLeaveGroup"> <icon name="leave"/> </a> </div> diff --git a/app/assets/javascripts/groups/components/item_stats.vue b/app/assets/javascripts/groups/components/item_stats.vue index 168b4e4af2c..87ab5480c15 100644 --- a/app/assets/javascripts/groups/components/item_stats.vue +++ b/app/assets/javascripts/groups/components/item_stats.vue @@ -45,44 +45,44 @@ <div class="stats"> <item-stats-value v-if="isGroup" - css-class="number-subgroups" - icon-name="folder" :title="__('Subgroups')" :value="item.subgroupCount" + css-class="number-subgroups" + icon-name="folder" /> <item-stats-value v-if="isGroup" - css-class="number-projects" - icon-name="bookmark" :title="__('Projects')" :value="item.projectCount" + css-class="number-projects" + icon-name="bookmark" /> <item-stats-value v-if="isGroup" - css-class="number-users" - icon-name="users" :title="__('Members')" :value="item.memberCount" + css-class="number-users" + icon-name="users" /> <item-stats-value v-if="isProject" + :value="item.starCount" css-class="project-stars" icon-name="star" - :value="item.starCount" /> <item-stats-value - css-class="item-visibility" - tooltip-placement="left" :icon-name="visibilityIcon" :title="visibilityTooltip" + css-class="item-visibility" + tooltip-placement="left" /> <div - class="last-updated" v-if="isProject" + class="last-updated" > <time-ago-tooltip - tooltip-placement="bottom" :time="item.updatedAt" + tooltip-placement="bottom" /> </div> </div> diff --git a/app/assets/javascripts/groups/components/item_stats_value.vue b/app/assets/javascripts/groups/components/item_stats_value.vue index 4d86ac8023c..ef9f2bca76c 100644 --- a/app/assets/javascripts/groups/components/item_stats_value.vue +++ b/app/assets/javascripts/groups/components/item_stats_value.vue @@ -52,10 +52,10 @@ <template> <span v-tooltip - data-container="body" :data-placement="tooltipPlacement" :class="cssClass" :title="title" + data-container="body" > <icon :name="iconName" /> <span diff --git a/app/assets/javascripts/ide/components/activity_bar.vue b/app/assets/javascripts/ide/components/activity_bar.vue index 05dbc1410de..62697e0ecc3 100644 --- a/app/assets/javascripts/ide/components/activity_bar.vue +++ b/app/assets/javascripts/ide/components/activity_bar.vue @@ -1,4 +1,5 @@ <script> +import $ from 'jquery'; import { mapActions, mapGetters, mapState } from 'vuex'; import Icon from '~/vue_shared/components/icon.vue'; import tooltip from '~/vue_shared/directives/tooltip'; @@ -20,6 +21,13 @@ export default { }, methods: { ...mapActions(['updateActivityBarView']), + changedActivityView(e, view) { + e.currentTarget.blur(); + + this.updateActivityBarView(view); + + $(e.currentTarget).tooltip('hide'); + }, }, activityBarViews, }; @@ -31,12 +39,12 @@ export default { <li v-once> <a v-tooltip - data-container="body" - data-placement="right" :href="goBackUrl" - class="ide-sidebar-link" :title="s__('IDE|Go back')" :aria-label="s__('IDE|Go back')" + data-container="body" + data-placement="right" + class="ide-sidebar-link" > <icon :size="16" @@ -47,16 +55,16 @@ export default { <li> <button v-tooltip - data-container="body" - data-placement="right" - type="button" - class="ide-sidebar-link js-ide-edit-mode" :class="{ active: currentActivityView === $options.activityBarViews.edit }" - @click.prevent="updateActivityBarView($options.activityBarViews.edit)" :title="s__('IDE|Edit')" :aria-label="s__('IDE|Edit')" + data-container="body" + data-placement="right" + type="button" + class="ide-sidebar-link js-ide-edit-mode" + @click.prevent="changedActivityView($event, $options.activityBarViews.edit)" > <icon name="code" @@ -66,16 +74,16 @@ export default { <li> <button v-tooltip - data-container="body" - data-placement="right" - type="button" - class="ide-sidebar-link js-ide-review-mode" :class="{ active: currentActivityView === $options.activityBarViews.review }" - @click.prevent="updateActivityBarView($options.activityBarViews.review)" :title="s__('IDE|Review')" :aria-label="s__('IDE|Review')" + data-container="body" + data-placement="right" + type="button" + class="ide-sidebar-link js-ide-review-mode" + @click.prevent="changedActivityView($event, $options.activityBarViews.review)" > <icon name="file-modified" @@ -85,16 +93,16 @@ export default { <li v-show="hasChanges"> <button v-tooltip - data-container="body" - data-placement="right" - type="button" - class="ide-sidebar-link js-ide-commit-mode" :class="{ active: currentActivityView === $options.activityBarViews.commit }" - @click.prevent="updateActivityBarView($options.activityBarViews.commit)" :title="s__('IDE|Commit')" :aria-label="s__('IDE|Commit')" + data-container="body" + data-placement="right" + type="button" + class="ide-sidebar-link js-ide-commit-mode" + @click.prevent="changedActivityView($event, $options.activityBarViews.commit)" > <icon name="commit" diff --git a/app/assets/javascripts/ide/components/commit_sidebar/form.vue b/app/assets/javascripts/ide/components/commit_sidebar/form.vue index e2b42ab2642..955d9280728 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/form.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/form.vue @@ -91,7 +91,6 @@ export default { <template> <div - class="multi-file-commit-form" :class="{ 'is-compact': isCompact, 'is-full': !isCompact @@ -99,6 +98,7 @@ export default { :style="{ height: componentHeight ? `${componentHeight}px` : null, }" + class="multi-file-commit-form" > <transition name="commit-form-slide-up" @@ -108,12 +108,12 @@ export default { > <div v-if="isCompact" - class="commit-form-compact" ref="compactEl" + class="commit-form-compact" > <button - type="button" :disabled="!hasChanges" + type="button" class="btn btn-primary btn-sm btn-block" @click="toggleIsSmall" > @@ -126,8 +126,8 @@ export default { </div> <form v-if="!isCompact" - @submit.prevent.stop="commitChanges" ref="formEl" + @submit.prevent.stop="commitChanges" > <transition name="fade"> <success-message @@ -143,8 +143,8 @@ export default { <loading-button :loading="submitCommitLoading" :disabled="commitButtonDisabled" - container-class="btn btn-success btn-sm float-left" :label="__('Commit')" + container-class="btn btn-success btn-sm float-left" @click="commitChanges" /> <button diff --git a/app/assets/javascripts/ide/components/commit_sidebar/list.vue b/app/assets/javascripts/ide/components/commit_sidebar/list.vue index 1325fc993b2..d0fb0e3d99e 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/list.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/list.vue @@ -34,6 +34,10 @@ export default { type: String, required: true, }, + actionBtnIcon: { + type: String, + required: true, + }, itemActionComponent: { type: String, required: true, @@ -43,11 +47,15 @@ export default { required: false, default: false, }, - }, - data() { - return { - showActionButton: false, - }; + activeFileKey: { + type: String, + required: false, + default: null, + }, + keyPrefix: { + type: String, + required: true, + }, }, computed: { titleText() { @@ -55,15 +63,15 @@ export default { title: this.title, }); }, + filesLength() { + return this.fileList.length; + }, }, methods: { ...mapActions(['stageAllChanges', 'unstageAllChanges']), actionBtnClicked() { this[this.action](); }, - setShowActionButton(show) { - this.showActionButton = show; - }, }, }; </script> @@ -74,8 +82,6 @@ export default { > <header class="multi-file-commit-panel-header" - @mouseenter="setShowActionButton(true)" - @mouseleave="setShowActionButton(false)" > <div class="multi-file-commit-panel-header-title" @@ -86,24 +92,40 @@ export default { :size="18" /> {{ titleText }} - <span - v-show="!showActionButton" - class="ide-commit-file-count" - > - {{ fileList.length }} - </span> - <button - v-show="showActionButton" - type="button" - class="btn btn-blank btn-link ide-staged-action-btn" - @click="actionBtnClicked" - > - {{ actionBtnText }} - </button> + <div class="d-flex ml-auto"> + <button + v-tooltip + v-show="filesLength" + :class="{ + 'd-flex': filesLength + }" + :title="actionBtnText" + type="button" + class="btn btn-default ide-staged-action-btn p-0 order-1 align-items-center" + data-placement="bottom" + data-container="body" + data-boundary="viewport" + @click="actionBtnClicked" + > + <icon + :name="actionBtnIcon" + :size="12" + class="ml-auto mr-auto" + /> + </button> + <span + :class="{ + 'rounded-right': !filesLength + }" + class="ide-commit-file-count order-0 rounded-left text-center" + > + {{ filesLength }} + </span> + </div> </div> </header> <ul - v-if="fileList.length" + v-if="filesLength" class="multi-file-commit-list list-unstyled append-bottom-0" > <li @@ -113,8 +135,9 @@ export default { <list-item :file="file" :action-component="itemActionComponent" - :key-prefix="title" + :key-prefix="keyPrefix" :staged-list="stagedList" + :active-file-key="activeFileKey" /> </li> </ul> diff --git a/app/assets/javascripts/ide/components/commit_sidebar/list_collapsed.vue b/app/assets/javascripts/ide/components/commit_sidebar/list_collapsed.vue index 2254271c679..d376a004e84 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/list_collapsed.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/list_collapsed.vue @@ -38,14 +38,17 @@ export default { return this.modifiedFilesLength ? 'multi-file-modified' : ''; }, additionsTooltip() { - return sprintf(n__('1 %{type} addition', '%d %{type} additions', this.addedFilesLength), { + return sprintf(n__('1 %{type} addition', '%{count} %{type} additions', this.addedFilesLength), { type: this.title.toLowerCase(), + count: this.addedFilesLength, }); }, modifiedTooltip() { return sprintf( - n__('1 %{type} modification', '%d %{type} modifications', this.modifiedFilesLength), - { type: this.title.toLowerCase() }, + n__('1 %{type} modification', '%{count} %{type} modifications', this.modifiedFilesLength), { + type: this.title.toLowerCase(), + count: this.modifiedFilesLength, + }, ); }, titleTooltip() { diff --git a/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue b/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue index 03f3e4de83c..5cda7967130 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue @@ -1,5 +1,6 @@ <script> import { mapActions } from 'vuex'; +import tooltip from '~/vue_shared/directives/tooltip'; import Icon from '~/vue_shared/components/icon.vue'; import StageButton from './stage_button.vue'; import UnstageButton from './unstage_button.vue'; @@ -11,6 +12,9 @@ export default { StageButton, UnstageButton, }, + directives: { + tooltip, + }, props: { file: { type: Object, @@ -30,6 +34,11 @@ export default { required: false, default: false, }, + activeFileKey: { + type: String, + required: false, + default: null, + }, }, computed: { iconName() { @@ -39,6 +48,15 @@ export default { iconClass() { return `multi-file-${this.file.tempFile ? 'addition' : 'modified'} append-right-8`; }, + fullKey() { + return `${this.keyPrefix}-${this.file.key}`; + }, + isActive() { + return this.activeFileKey === this.fullKey; + }, + tooltipTitle() { + return this.file.path === this.file.name ? '' : this.file.path; + }, }, methods: { ...mapActions([ @@ -51,7 +69,7 @@ export default { openFileInEditor() { return this.openPendingTab({ file: this.file, - keyPrefix: this.keyPrefix.toLowerCase(), + keyPrefix: this.keyPrefix, }).then(changeViewer => { if (changeViewer) { this.updateViewer(viewerTypes.diff); @@ -70,24 +88,30 @@ export default { </script> <template> - <div class="multi-file-commit-list-item"> + <div class="multi-file-commit-list-item position-relative"> <button + v-tooltip + :title="tooltipTitle" + :class="{ + 'is-active': isActive + }" type="button" - class="multi-file-commit-list-path" + class="multi-file-commit-list-path w-100 border-0 ml-0 mr-0" @dblclick="fileAction" @click="openFileInEditor" > - <span class="multi-file-commit-list-file-path"> + <span class="multi-file-commit-list-file-path d-flex align-items-center"> <icon :name="iconName" :size="16" :css-classes="iconClass" - />{{ file.path }} + />{{ file.name }} </span> </button> <component :is="actionComponent" :path="file.path" + class="d-flex position-absolute" /> </div> </template> diff --git a/app/assets/javascripts/ide/components/commit_sidebar/message_field.vue b/app/assets/javascripts/ide/components/commit_sidebar/message_field.vue index f14fcdc88ed..40496c80a46 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/message_field.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/message_field.vue @@ -54,7 +54,7 @@ export default { placement: 'top', content: sprintf( __(` - The character highligher helps you keep the subject line to %{titleLength} characters + The character highlighter helps you keep the subject line to %{titleLength} characters and wrap the body at %{bodyLength} so they are readable in git. `), { titleLength: MAX_TITLE_LENGTH, bodyLength: MAX_BODY_LENGTH }, @@ -66,10 +66,10 @@ export default { <template> <fieldset class="common-note-form ide-commit-message-field"> <div - class="md-area" :class="{ 'is-focused': isFocused }" + class="md-area" > <div v-once @@ -92,10 +92,10 @@ export default { <div class="ide-commit-message-textarea-container"> <div class="ide-commit-message-highlights-container"> <div - class="note-textarea highlights monospace" :style="{ transform: `translate3d(0, ${-scrollTop}px, 0)` }" + class="note-textarea highlights monospace" > <div v-for="(line, index) in allLines" @@ -113,15 +113,15 @@ export default { </div> </div> <textarea - class="note-textarea ide-commit-message-textarea" - name="commit-message" + ref="textarea" :placeholder="__('Write a commit message...')" :value="text" + class="note-textarea ide-commit-message-textarea" + name="commit-message" @scroll="handleScroll" @input="onInput" @focus="updateIsFocused(true)" @blur="updateIsFocused(false)" - ref="textarea" > </textarea> </div> diff --git a/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue b/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue index 00f2312ae51..35ab3fd11df 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue @@ -58,12 +58,12 @@ export default { }" > <input - type="radio" - name="commit-action" :value="value" - @change="updateCommitAction($event.target.value)" :checked="commitAction === value" :disabled="disabled" + type="radio" + name="commit-action" + @change="updateCommitAction($event.target.value)" /> <span class="prepend-left-10"> <span @@ -80,9 +80,9 @@ export default { class="ide-commit-new-branch" > <input + :placeholder="newBranchName" type="text" class="form-control monospace" - :placeholder="newBranchName" @input="updateBranchName($event.target.value)" /> </div> diff --git a/app/assets/javascripts/ide/components/commit_sidebar/stage_button.vue b/app/assets/javascripts/ide/components/commit_sidebar/stage_button.vue index 52dce8412ab..7014b9f605e 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/stage_button.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/stage_button.vue @@ -25,35 +25,51 @@ export default { <template> <div v-once - class="multi-file-discard-btn" + class="multi-file-discard-btn dropdown" > <button v-tooltip - type="button" - class="btn btn-blank append-right-5" :aria-label="__('Stage changes')" :title="__('Stage changes')" + type="button" + class="btn btn-blank append-right-5 d-flex align-items-center" data-container="body" + data-boundary="viewport" + data-placement="bottom" @click.stop="stageChange(path)" > <icon - name="mobile-issue-close" :size="12" + name="mobile-issue-close" /> </button> <button v-tooltip + :title="__('More actions')" type="button" - class="btn btn-blank" - :aria-label="__('Discard changes')" - :title="__('Discard changes')" + class="btn btn-blank d-flex align-items-center" data-container="body" - @click.stop="discardFileChanges(path)" + data-boundary="viewport" + data-placement="bottom" + data-toggle="dropdown" + data-display="static" > <icon - name="remove" :size="12" + name="more" /> </button> + <div class="dropdown-menu dropdown-menu-right"> + <ul> + <li> + <button + type="button" + @click.stop="discardFileChanges(path)" + > + {{ __('Discard changes') }} + </button> + </li> + </ul> + </div> </div> </template> diff --git a/app/assets/javascripts/ide/components/commit_sidebar/unstage_button.vue b/app/assets/javascripts/ide/components/commit_sidebar/unstage_button.vue index 123d60da47e..9cec73ec00e 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/unstage_button.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/unstage_button.vue @@ -29,16 +29,18 @@ export default { > <button v-tooltip - type="button" - class="btn btn-blank" :aria-label="__('Unstage changes')" :title="__('Unstage changes')" + type="button" + class="btn btn-blank d-flex align-items-center" data-container="body" + data-boundary="viewport" + data-placement="bottom" @click="unstageChange(path)" > <icon - name="history" :size="12" + name="history" /> </button> </div> diff --git a/app/assets/javascripts/ide/components/editor_mode_dropdown.vue b/app/assets/javascripts/ide/components/editor_mode_dropdown.vue index b9af4d27145..95598c9aca6 100644 --- a/app/assets/javascripts/ide/components/editor_mode_dropdown.vue +++ b/app/assets/javascripts/ide/components/editor_mode_dropdown.vue @@ -44,11 +44,11 @@ export default { <ul> <li> <a - href="#" - @click.prevent="changeMode($options.viewerTypes.mr)" :class="{ 'is-active': viewer === $options.viewerTypes.mr, }" + href="#" + @click.prevent="changeMode($options.viewerTypes.mr)" > <strong class="dropdown-menu-inner-title"> {{ mergeReviewLine }} @@ -60,11 +60,11 @@ export default { </li> <li> <a - href="#" - @click.prevent="changeMode($options.viewerTypes.diff)" :class="{ 'is-active': viewer === $options.viewerTypes.diff, }" + href="#" + @click.prevent="changeMode($options.viewerTypes.diff)" > <strong class="dropdown-menu-inner-title">{{ __('Reviewing') }}</strong> <span class="dropdown-menu-inner-content"> diff --git a/app/assets/javascripts/ide/components/external_link.vue b/app/assets/javascripts/ide/components/external_link.vue index cf3316a8179..e24fe5bbccb 100644 --- a/app/assets/javascripts/ide/components/external_link.vue +++ b/app/assets/javascripts/ide/components/external_link.vue @@ -26,15 +26,15 @@ export default { > <a :href="file.permalink" - target="_blank" :title="s__('IDE|Open in file view')" + target="_blank" rel="noopener noreferrer" > <span class="vertical-align-middle">Open in file view</span> <icon + :size="16" name="external-link" css-classes="vertical-align-middle space-right" - :size="16" /> </a> </div> diff --git a/app/assets/javascripts/ide/components/file_finder/index.vue b/app/assets/javascripts/ide/components/file_finder/index.vue index cabb3f59b17..0ba33053717 100644 --- a/app/assets/javascripts/ide/components/file_finder/index.vue +++ b/app/assets/javascripts/ide/components/file_finder/index.vue @@ -173,38 +173,38 @@ export default { > <div class="dropdown-input"> <input + ref="searchInput" + :placeholder="__('Search files')" + v-model="searchText" type="search" class="dropdown-input-field" - :placeholder="__('Search files')" autocomplete="off" - v-model="searchText" - ref="searchInput" @keydown="onKeydown($event)" @keyup="onKeyup($event)" /> <i - aria-hidden="true" - class="fa fa-search dropdown-input-search" :class="{ hidden: showClearInputButton }" + aria-hidden="true" + class="fa fa-search dropdown-input-search" ></i> <i - role="button" :aria-label="__('Clear search input')" - class="fa fa-times dropdown-input-clear" :class="{ show: showClearInputButton }" + role="button" + class="fa fa-times dropdown-input-clear" @click="clearSearchInput" ></i> </div> <div> <virtual-list + ref="virtualScrollList" :size="listHeight" :remain="listShowCount" wtag="ul" - ref="virtualScrollList" > <template v-if="filteredBlobsLength"> <li @@ -212,11 +212,11 @@ export default { :key="file.key" > <item - class="disable-hover" :file="file" :search-text="searchText" :focused="index === focusedIndex" :index="index" + class="disable-hover" @click="openFile" @mouseover="onMouseOver" @mousemove="onMouseMove" diff --git a/app/assets/javascripts/ide/components/file_finder/item.vue b/app/assets/javascripts/ide/components/file_finder/item.vue index d4427420207..a4cf3edb981 100644 --- a/app/assets/javascripts/ide/components/file_finder/item.vue +++ b/app/assets/javascripts/ide/components/file_finder/item.vue @@ -59,11 +59,11 @@ export default { <template> <button - type="button" - class="diff-changed-file" :class="{ 'is-focused': focused, }" + type="button" + class="diff-changed-file" @click.prevent="clickRow" @mouseover="mouseOverRow" @mousemove="mouseMove" diff --git a/app/assets/javascripts/ide/components/ide.vue b/app/assets/javascripts/ide/components/ide.vue index f5f832521c5..f5f7f967a92 100644 --- a/app/assets/javascripts/ide/components/ide.vue +++ b/app/assets/javascripts/ide/components/ide.vue @@ -93,8 +93,8 @@ export default { :merge-request-id="currentMergeRequestId" /> <repo-editor - class="multi-file-edit-pane-content" :file="activeFile" + class="multi-file-edit-pane-content" /> </template> <template diff --git a/app/assets/javascripts/ide/components/ide_review.vue b/app/assets/javascripts/ide/components/ide_review.vue index 0c9ec3b00f0..f9978762c45 100644 --- a/app/assets/javascripts/ide/components/ide_review.vue +++ b/app/assets/javascripts/ide/components/ide_review.vue @@ -11,17 +11,20 @@ export default { }, computed: { ...mapGetters(['currentMergeRequest']), - ...mapState(['viewer']), + ...mapState(['viewer', 'currentMergeRequestId']), showLatestChangesText() { - return !this.currentMergeRequest || this.viewer === viewerTypes.diff; + return !this.currentMergeRequestId || this.viewer === viewerTypes.diff; }, showMergeRequestText() { - return this.currentMergeRequest && this.viewer === viewerTypes.mr; + return this.currentMergeRequestId && this.viewer === viewerTypes.mr; + }, + mergeRequestId() { + return `!${this.currentMergeRequest.iid}`; }, }, mounted() { this.$nextTick(() => { - this.updateViewer(this.currentMergeRequest ? viewerTypes.mr : viewerTypes.diff); + this.updateViewer(this.currentMergeRequestId ? viewerTypes.mr : viewerTypes.diff); }); }, methods: { @@ -33,8 +36,8 @@ export default { <template> <ide-tree-list :viewer-type="viewer" - header-class="ide-review-header" :disable-action-dropdown="true" + header-class="ide-review-header" > <template slot="header" @@ -54,7 +57,11 @@ export default { </template> <template v-else-if="showMergeRequestText"> {{ __('Merge request') }} - (<a :href="currentMergeRequest.web_url">!{{ currentMergeRequest.iid }}</a>) + (<a + v-if="currentMergeRequest" + :href="currentMergeRequest.web_url" + v-text="mergeRequestId" + ></a>) </template> </div> </template> diff --git a/app/assets/javascripts/ide/components/ide_side_bar.vue b/app/assets/javascripts/ide/components/ide_side_bar.vue index 3f980203911..21906674c4b 100644 --- a/app/assets/javascripts/ide/components/ide_side_bar.vue +++ b/app/assets/javascripts/ide/components/ide_side_bar.vue @@ -1,4 +1,5 @@ <script> +import $ from 'jquery'; import { mapState, mapGetters } from 'vuex'; import ProjectAvatarImage from '~/vue_shared/components/project_avatar/image.vue'; import Icon from '~/vue_shared/components/icon.vue'; @@ -13,6 +14,7 @@ import CommitSection from './repo_commit_section.vue'; import CommitForm from './commit_sidebar/form.vue'; import IdeReview from './ide_review.vue'; import SuccessMessage from './commit_sidebar/success_message.vue'; +import MergeRequestDropdown from './merge_requests/dropdown.vue'; import { activityBarViews } from '../constants'; export default { @@ -32,10 +34,12 @@ export default { CommitForm, IdeReview, SuccessMessage, + MergeRequestDropdown, }, data() { return { showTooltip: false, + showMergeRequestsDropdown: false, }; }, computed: { @@ -46,6 +50,7 @@ export default { 'changedFiles', 'stagedFiles', 'lastCommitMsg', + 'currentMergeRequestId', ]), ...mapGetters(['currentProject', 'someUncommitedChanges']), showSuccessMessage() { @@ -61,9 +66,39 @@ export default { watch: { currentBranchId() { this.$nextTick(() => { + if (!this.$refs.branchId) return; + this.showTooltip = this.$refs.branchId.scrollWidth > this.$refs.branchId.offsetWidth; }); }, + loading() { + this.$nextTick(() => { + this.addDropdownListeners(); + }); + }, + }, + mounted() { + this.addDropdownListeners(); + }, + beforeDestroy() { + $(this.$refs.mergeRequestDropdown) + .off('show.bs.dropdown') + .off('hide.bs.dropdown'); + }, + methods: { + addDropdownListeners() { + if (!this.$refs.mergeRequestDropdown) return; + + $(this.$refs.mergeRequestDropdown) + .on('show.bs.dropdown', () => { + this.toggleMergeRequestDropdown(); + }).on('hide.bs.dropdown', () => { + this.toggleMergeRequestDropdown(); + }); + }, + toggleMergeRequestDropdown() { + this.showMergeRequestsDropdown = !this.showMergeRequestsDropdown; + }, }, }; </script> @@ -80,53 +115,79 @@ export default { <div class="multi-file-commit-panel-inner"> <template v-if="loading"> <div - class="multi-file-loading-container" v-for="n in 3" :key="n" + class="multi-file-loading-container" > <skeleton-loading-container /> </div> </template> <template v-else> - <div class="context-header ide-context-header"> - <a - :href="currentProject.web_url" + <div + ref="mergeRequestDropdown" + class="context-header ide-context-header dropdown" + > + <button + type="button" + data-toggle="dropdown" > <div v-if="currentProject.avatar_url" class="avatar-container s40 project-avatar" > <project-avatar-image - class="avatar-container project-avatar" :link-href="currentProject.path" :img-src="currentProject.avatar_url" :img-alt="currentProject.name" :img-size="40" + class="avatar-container project-avatar" /> </div> <identicon v-else - size-class="s40" :entity-id="currentProject.id" :entity-name="currentProject.name" + size-class="s40" /> <div class="ide-sidebar-project-title"> <div class="sidebar-context-title"> {{ currentProject.name }} </div> - <div - class="sidebar-context-title ide-sidebar-branch-title" - ref="branchId" - v-tooltip - :title="branchTooltipTitle" - > - <icon - name="branch" - css-classes="append-right-5" - />{{ currentBranchId }} + <div class="d-flex"> + <div + v-tooltip + v-if="currentBranchId" + ref="branchId" + :title="branchTooltipTitle" + class="sidebar-context-title ide-sidebar-branch-title" + > + <icon + name="branch" + css-classes="append-right-5" + />{{ currentBranchId }} + </div> + <div + v-if="currentMergeRequestId" + :class="{ + 'prepend-left-8': currentBranchId + }" + class="sidebar-context-title ide-sidebar-branch-title" + > + <icon + name="git-merge" + css-classes="append-right-5" + />!{{ currentMergeRequestId }} + </div> </div> </div> - </a> + <icon + class="ml-auto" + name="chevron-down" + /> + </button> + <merge-request-dropdown + :show="showMergeRequestsDropdown" + /> </div> <div class="multi-file-commit-panel-inner-scroll"> <component diff --git a/app/assets/javascripts/ide/components/ide_status_bar.vue b/app/assets/javascripts/ide/components/ide_status_bar.vue index 368a2995ed9..0582ad32e92 100644 --- a/app/assets/javascripts/ide/components/ide_status_bar.vue +++ b/app/assets/javascripts/ide/components/ide_status_bar.vue @@ -35,9 +35,7 @@ export default { }, watch: { lastCommit() { - if (!this.isPollingInitialized) { - this.initPipelinePolling(); - } + this.initPipelinePolling(); }, }, mounted() { @@ -47,9 +45,8 @@ export default { if (this.intervalId) { clearInterval(this.intervalId); } - if (this.isPollingInitialized) { - this.stopPipelinePolling(); - } + + this.stopPipelinePolling(); }, methods: { ...mapActions('pipelines', ['fetchLatestPipeline', 'stopPipelinePolling']), @@ -59,8 +56,9 @@ export default { }, 1000); }, initPipelinePolling() { - this.fetchLatestPipeline(); - this.isPollingInitialized = true; + if (this.lastCommit) { + this.fetchLatestPipeline(); + } }, commitAgeUpdate() { if (this.lastCommit) { @@ -77,22 +75,22 @@ export default { <template> <footer class="ide-status-bar"> <div - class="ide-status-branch" v-if="lastCommit && lastCommitFormatedAge" + class="ide-status-branch" > <span - class="ide-status-pipeline" v-if="latestPipeline && latestPipeline.details" + class="ide-status-pipeline" > <ci-icon - :status="latestPipeline.details.status" v-tooltip + :status="latestPipeline.details.status" :title="latestPipeline.details.status.text" /> Pipeline <a - class="monospace" - :href="latestPipeline.details.status.details_path">#{{ latestPipeline.id }}</a> + :href="latestPipeline.details.status.details_path" + class="monospace">#{{ latestPipeline.id }}</a> {{ latestPipeline.details.status.text }} for </span> @@ -102,18 +100,18 @@ export default { /> <a v-tooltip - class="commit-sha" :title="lastCommit.message" :href="getCommitPath(lastCommit.short_id)" + class="commit-sha" >{{ lastCommit.short_id }}</a> by {{ lastCommit.author_name }} <time v-tooltip - data-placement="top" - data-container="body" :datetime="lastCommit.committed_date" :title="tooltipTitle(lastCommit.committed_date)" + data-placement="top" + data-container="body" > {{ lastCommitFormatedAge }} </time> @@ -131,8 +129,8 @@ export default { {{ file.eol }} </div> <div - class="ide-status-file" - v-if="file && !file.binary"> + v-if="file && !file.binary" + class="ide-status-file"> {{ file.editorRow }}:{{ file.editorColumn }} </div> <div diff --git a/app/assets/javascripts/ide/components/ide_tree_list.vue b/app/assets/javascripts/ide/components/ide_tree_list.vue index e64a09fcc90..0df99798d21 100644 --- a/app/assets/javascripts/ide/components/ide_tree_list.vue +++ b/app/assets/javascripts/ide/components/ide_tree_list.vue @@ -50,17 +50,17 @@ export default { > <template v-if="showLoading"> <div - class="multi-file-loading-container" v-for="n in 3" :key="n" + class="multi-file-loading-container" > <skeleton-loading-container /> </div> </template> <template v-else> <header - class="ide-tree-header" :class="headerClass" + class="ide-tree-header" > <slot name="header"></slot> </header> diff --git a/app/assets/javascripts/ide/components/jobs/detail.vue b/app/assets/javascripts/ide/components/jobs/detail.vue new file mode 100644 index 00000000000..f39ce545656 --- /dev/null +++ b/app/assets/javascripts/ide/components/jobs/detail.vue @@ -0,0 +1,137 @@ +<script> +import { mapActions, mapState } from 'vuex'; +import _ from 'underscore'; +import { __ } from '../../../locale'; +import tooltip from '../../../vue_shared/directives/tooltip'; +import Icon from '../../../vue_shared/components/icon.vue'; +import ScrollButton from './detail/scroll_button.vue'; +import JobDescription from './detail/description.vue'; + +const scrollPositions = { + top: 0, + bottom: 1, +}; + +export default { + directives: { + tooltip, + }, + components: { + Icon, + ScrollButton, + JobDescription, + }, + data() { + return { + scrollPos: scrollPositions.top, + }; + }, + computed: { + ...mapState('pipelines', ['detailJob']), + isScrolledToBottom() { + return this.scrollPos === scrollPositions.bottom; + }, + isScrolledToTop() { + return this.scrollPos === scrollPositions.top; + }, + jobOutput() { + return this.detailJob.output || __('No messages were logged'); + }, + }, + mounted() { + this.getTrace(); + }, + methods: { + ...mapActions('pipelines', ['fetchJobTrace', 'setDetailJob']), + scrollDown() { + if (this.$refs.buildTrace) { + this.$refs.buildTrace.scrollTo(0, this.$refs.buildTrace.scrollHeight); + } + }, + scrollUp() { + if (this.$refs.buildTrace) { + this.$refs.buildTrace.scrollTo(0, 0); + } + }, + scrollBuildLog: _.throttle(function buildLogScrollDebounce() { + const { scrollTop } = this.$refs.buildTrace; + const { offsetHeight, scrollHeight } = this.$refs.buildTrace; + + if (scrollTop + offsetHeight === scrollHeight) { + this.scrollPos = scrollPositions.bottom; + } else if (scrollTop === 0) { + this.scrollPos = scrollPositions.top; + } else { + this.scrollPos = ''; + } + }), + getTrace() { + return this.fetchJobTrace().then(() => this.scrollDown()); + }, + }, +}; +</script> + +<template> + <div class="ide-pipeline build-page d-flex flex-column flex-fill"> + <header class="ide-job-header d-flex align-items-center"> + <button + class="btn btn-default btn-sm d-flex" + @click="setDetailJob(null)" + > + <icon + name="chevron-left" + /> + {{ __('View jobs') }} + </button> + </header> + <div class="top-bar d-flex border-left-0"> + <job-description + :job="detailJob" + /> + <div class="controllers ml-auto"> + <a + v-tooltip + :title="__('Show complete raw log')" + :href="detailJob.rawPath" + data-placement="top" + data-container="body" + class="controllers-buttons" + target="_blank" + > + <i + aria-hidden="true" + class="fa fa-file-text-o" + ></i> + </a> + <scroll-button + :disabled="isScrolledToTop" + direction="up" + @click="scrollUp" + /> + <scroll-button + :disabled="isScrolledToBottom" + direction="down" + @click="scrollDown" + /> + </div> + </div> + <pre + ref="buildTrace" + class="build-trace mb-0 h-100" + @scroll="scrollBuildLog" + > + <code + v-show="!detailJob.isLoading" + class="bash" + v-html="jobOutput" + > + </code> + <div + v-show="detailJob.isLoading" + class="build-loader-animation" + > + </div> + </pre> + </div> +</template> diff --git a/app/assets/javascripts/ide/components/jobs/detail/description.vue b/app/assets/javascripts/ide/components/jobs/detail/description.vue new file mode 100644 index 00000000000..7e24974f7e5 --- /dev/null +++ b/app/assets/javascripts/ide/components/jobs/detail/description.vue @@ -0,0 +1,47 @@ +<script> +import Icon from '../../../../vue_shared/components/icon.vue'; +import CiIcon from '../../../../vue_shared/components/ci_icon.vue'; + +export default { + components: { + Icon, + CiIcon, + }, + props: { + job: { + type: Object, + required: true, + }, + }, + computed: { + jobId() { + return `#${this.job.id}`; + }, + }, +}; +</script> + +<template> + <div class="d-flex align-items-center"> + <ci-icon + :status="job.status" + :borderless="true" + :size="24" + class="d-flex" + /> + <span class="prepend-left-8"> + {{ job.name }} + <a + :href="job.path" + target="_blank" + class="ide-external-link" + > + {{ jobId }} + <icon + :size="12" + name="external-link" + /> + </a> + </span> + </div> +</template> diff --git a/app/assets/javascripts/ide/components/jobs/detail/scroll_button.vue b/app/assets/javascripts/ide/components/jobs/detail/scroll_button.vue new file mode 100644 index 00000000000..103a407987f --- /dev/null +++ b/app/assets/javascripts/ide/components/jobs/detail/scroll_button.vue @@ -0,0 +1,66 @@ +<script> +import { __ } from '../../../../locale'; +import Icon from '../../../../vue_shared/components/icon.vue'; +import tooltip from '../../../../vue_shared/directives/tooltip'; + +const directions = { + up: 'up', + down: 'down', +}; + +export default { + directives: { + tooltip, + }, + components: { + Icon, + }, + props: { + direction: { + type: String, + required: true, + validator(value) { + return Object.keys(directions).includes(value); + }, + }, + disabled: { + type: Boolean, + required: true, + }, + }, + computed: { + tooltipTitle() { + return this.direction === directions.up ? __('Scroll to top') : __('Scroll to bottom'); + }, + iconName() { + return `scroll_${this.direction}`; + }, + }, + methods: { + clickedScroll() { + this.$emit('click'); + }, + }, +}; +</script> + +<template> + <div + v-tooltip + :title="tooltipTitle" + class="controllers-buttons" + data-container="body" + data-placement="top" + > + <button + :disabled="disabled" + class="btn-scroll btn-transparent btn-blank" + type="button" + @click="clickedScroll" + > + <icon + :name="iconName" + /> + </button> + </div> +</template> diff --git a/app/assets/javascripts/ide/components/jobs/item.vue b/app/assets/javascripts/ide/components/jobs/item.vue index c33936021d4..7f4695a0451 100644 --- a/app/assets/javascripts/ide/components/jobs/item.vue +++ b/app/assets/javascripts/ide/components/jobs/item.vue @@ -1,11 +1,9 @@ <script> -import Icon from '../../../vue_shared/components/icon.vue'; -import CiIcon from '../../../vue_shared/components/ci_icon.vue'; +import JobDescription from './detail/description.vue'; export default { components: { - Icon, - CiIcon, + JobDescription, }, props: { job: { @@ -18,29 +16,29 @@ export default { return `#${this.job.id}`; }, }, + methods: { + clickViewLog() { + this.$emit('clickViewLog', this.job); + }, + }, }; </script> <template> <div class="ide-job-item"> - <ci-icon - :status="job.status" - :borderless="true" - :size="24" + <job-description + :job="job" + class="append-right-default" /> - <span class="prepend-left-8"> - {{ job.name }} - <a - :href="job.path" - target="_blank" - class="ide-external-link" + <div class="ml-auto align-self-center"> + <button + v-if="job.started" + type="button" + class="btn btn-default btn-sm" + @click="clickViewLog" > - {{ jobId }} - <icon - name="external-link" - :size="12" - /> - </a> - </span> + {{ __('View log') }} + </button> + </div> </div> </template> diff --git a/app/assets/javascripts/ide/components/jobs/list.vue b/app/assets/javascripts/ide/components/jobs/list.vue index bdd0364c9b9..3b16b860ecd 100644 --- a/app/assets/javascripts/ide/components/jobs/list.vue +++ b/app/assets/javascripts/ide/components/jobs/list.vue @@ -19,7 +19,7 @@ export default { }, }, methods: { - ...mapActions('pipelines', ['fetchJobs', 'toggleStageCollapsed']), + ...mapActions('pipelines', ['fetchJobs', 'toggleStageCollapsed', 'setDetailJob']), }, }; </script> @@ -38,6 +38,7 @@ export default { :stage="stage" @fetch="fetchJobs" @toggleCollapsed="toggleStageCollapsed" + @clickViewLog="setDetailJob" /> </template> </div> diff --git a/app/assets/javascripts/ide/components/jobs/stage.vue b/app/assets/javascripts/ide/components/jobs/stage.vue index 5b24bb1f5a7..15e881b7bc8 100644 --- a/app/assets/javascripts/ide/components/jobs/stage.vue +++ b/app/assets/javascripts/ide/components/jobs/stage.vue @@ -48,6 +48,9 @@ export default { toggleCollapsed() { this.$emit('toggleCollapsed', this.stage.id); }, + clickViewLog(job) { + this.$emit('clickViewLog', job); + }, }, }; </script> @@ -57,10 +60,10 @@ export default { class="ide-stage card prepend-top-default" > <div - class="card-header" :class="{ 'border-bottom-0': stage.isCollapsed }" + class="card-header" @click="toggleCollapsed" > <ci-icon @@ -69,10 +72,10 @@ export default { /> <strong v-tooltip="showTooltip" + ref="stageTitle" :title="showTooltip ? stage.name : null" data-container="body" class="prepend-left-8 ide-stage-title" - ref="stageTitle" > {{ stage.name }} </strong> @@ -90,8 +93,8 @@ export default { /> </div> <div - class="card-body" v-show="!stage.isCollapsed" + class="card-body" > <loading-icon v-if="showLoadingIcon" @@ -101,6 +104,7 @@ export default { v-for="job in stage.jobs" :key="job.id" :job="job" + @clickViewLog="clickViewLog" /> </template> </div> diff --git a/app/assets/javascripts/ide/components/merge_requests/dropdown.vue b/app/assets/javascripts/ide/components/merge_requests/dropdown.vue new file mode 100644 index 00000000000..4b9824bf04b --- /dev/null +++ b/app/assets/javascripts/ide/components/merge_requests/dropdown.vue @@ -0,0 +1,63 @@ +<script> +import { mapGetters } from 'vuex'; +import Tabs from '../../../vue_shared/components/tabs/tabs'; +import Tab from '../../../vue_shared/components/tabs/tab.vue'; +import List from './list.vue'; + +export default { + components: { + Tabs, + Tab, + List, + }, + props: { + show: { + type: Boolean, + required: true, + }, + }, + computed: { + ...mapGetters('mergeRequests', ['assignedData', 'createdData']), + createdMergeRequestLength() { + return this.createdData.mergeRequests.length; + }, + assignedMergeRequestLength() { + return this.assignedData.mergeRequests.length; + }, + }, +}; +</script> + +<template> + <div class="dropdown-menu ide-merge-requests-dropdown p-0"> + <tabs + v-if="show" + stop-propagation + > + <tab active> + <template slot="title"> + {{ __('Created by me') }} + <span class="badge badge-pill"> + {{ createdMergeRequestLength }} + </span> + </template> + <list + :empty-text="__('You have not created any merge requests')" + type="created" + /> + </tab> + <tab> + <template slot="title"> + {{ __('Assigned to me') }} + <span class="badge badge-pill"> + {{ assignedMergeRequestLength }} + </span> + </template> + <list + :empty-text="__('You do not have any assigned merge requests')" + type="assigned" + /> + </tab> + </tabs> + </div> +</template> diff --git a/app/assets/javascripts/ide/components/merge_requests/item.vue b/app/assets/javascripts/ide/components/merge_requests/item.vue new file mode 100644 index 00000000000..4e18376bd48 --- /dev/null +++ b/app/assets/javascripts/ide/components/merge_requests/item.vue @@ -0,0 +1,63 @@ +<script> +import Icon from '../../../vue_shared/components/icon.vue'; + +export default { + components: { + Icon, + }, + props: { + item: { + type: Object, + required: true, + }, + currentId: { + type: String, + required: true, + }, + currentProjectId: { + type: String, + required: true, + }, + }, + computed: { + isActive() { + return ( + this.item.iid === parseInt(this.currentId, 10) && + this.currentProjectId === this.item.projectPathWithNamespace + ); + }, + pathWithID() { + return `${this.item.projectPathWithNamespace}!${this.item.iid}`; + }, + }, + methods: { + clickItem() { + this.$emit('click', this.item); + }, + }, +}; +</script> + +<template> + <button + type="button" + class="btn-link d-flex align-items-center" + @click="clickItem" + > + <span class="d-flex append-right-default ide-merge-request-current-icon"> + <icon + v-if="isActive" + :size="18" + name="mobile-issue-close" + /> + </span> + <span> + <strong> + {{ item.title }} + </strong> + <span class="ide-merge-request-project-path d-block mt-1"> + {{ pathWithID }} + </span> + </span> + </button> +</template> diff --git a/app/assets/javascripts/ide/components/merge_requests/list.vue b/app/assets/javascripts/ide/components/merge_requests/list.vue new file mode 100644 index 00000000000..19d3e48ee10 --- /dev/null +++ b/app/assets/javascripts/ide/components/merge_requests/list.vue @@ -0,0 +1,132 @@ +<script> +import { mapActions, mapGetters, mapState } from 'vuex'; +import _ from 'underscore'; +import LoadingIcon from '../../../vue_shared/components/loading_icon.vue'; +import Item from './item.vue'; + +export default { + components: { + LoadingIcon, + Item, + }, + props: { + type: { + type: String, + required: true, + }, + emptyText: { + type: String, + required: true, + }, + }, + data() { + return { + search: '', + }; + }, + computed: { + ...mapGetters('mergeRequests', ['getData']), + ...mapState(['currentMergeRequestId', 'currentProjectId']), + data() { + return this.getData(this.type); + }, + isLoading() { + return this.data.isLoading; + }, + mergeRequests() { + return this.data.mergeRequests; + }, + hasMergeRequests() { + return this.mergeRequests.length !== 0; + }, + hasNoSearchResults() { + return this.search !== '' && !this.hasMergeRequests; + }, + }, + watch: { + isLoading: { + handler: 'focusSearch', + }, + }, + mounted() { + this.loadMergeRequests(); + }, + methods: { + ...mapActions('mergeRequests', ['fetchMergeRequests', 'openMergeRequest']), + loadMergeRequests() { + this.fetchMergeRequests({ type: this.type, search: this.search }); + }, + viewMergeRequest(item) { + this.openMergeRequest({ + projectPath: item.projectPathWithNamespace, + id: item.iid, + }); + }, + searchMergeRequests: _.debounce(function debounceSearch() { + this.loadMergeRequests(); + }, 250), + focusSearch() { + if (!this.isLoading) { + this.$nextTick(() => { + this.$refs.searchInput.focus(); + }); + } + }, + }, +}; +</script> + +<template> + <div> + <div class="dropdown-input mt-3 pb-3 mb-0 border-bottom"> + <input + ref="searchInput" + :placeholder="__('Search merge requests')" + v-model="search" + type="search" + class="dropdown-input-field" + @input="searchMergeRequests" + /> + <i + aria-hidden="true" + class="fa fa-search dropdown-input-search" + ></i> + </div> + <div class="dropdown-content ide-merge-requests-dropdown-content d-flex"> + <loading-icon + v-if="isLoading" + class="mt-3 mb-3 align-self-center ml-auto mr-auto" + size="2" + /> + <ul + v-else + class="mb-3 w-100" + > + <template v-if="hasMergeRequests"> + <li + v-for="item in mergeRequests" + :key="item.id" + > + <item + :item="item" + :current-id="currentMergeRequestId" + :current-project-id="currentProjectId" + @click="viewMergeRequest" + /> + </li> + </template> + <li + v-else + class="ide-merge-requests-empty d-flex align-items-center justify-content-center" + > + <template v-if="hasNoSearchResults"> + {{ __('No merge requests found') }} + </template> + <template v-else> + {{ emptyText }} + </template> + </li> + </ul> + </div> + </div> +</template> diff --git a/app/assets/javascripts/ide/components/mr_file_icon.vue b/app/assets/javascripts/ide/components/mr_file_icon.vue index 179a589d1ac..821be319cce 100644 --- a/app/assets/javascripts/ide/components/mr_file_icon.vue +++ b/app/assets/javascripts/ide/components/mr_file_icon.vue @@ -14,10 +14,10 @@ export default { <template> <icon - name="git-merge" v-tooltip :title="__('Part of merge request changes')" - css-classes="append-right-8" :size="12" + name="git-merge" + css-classes="append-right-8" /> </template> diff --git a/app/assets/javascripts/ide/components/new_dropdown/index.vue b/app/assets/javascripts/ide/components/new_dropdown/index.vue index f0b29702497..1e398d7e1aa 100644 --- a/app/assets/javascripts/ide/components/new_dropdown/index.vue +++ b/app/assets/javascripts/ide/components/new_dropdown/index.vue @@ -55,10 +55,10 @@ export default { <template> <div class="ide-new-btn"> <div - class="dropdown" :class="{ show: dropdownOpen, }" + class="dropdown" > <button type="button" @@ -67,19 +67,19 @@ export default { @click.stop="openDropdown()" > <icon - name="plus" :size="12" + name="plus" css-classes="float-left" /> <icon - name="arrow-down" :size="12" + name="arrow-down" css-classes="float-left" /> </button> <ul - class="dropdown-menu dropdown-menu-right" ref="dropdownMenu" + class="dropdown-menu dropdown-menu-right" > <li> <a diff --git a/app/assets/javascripts/ide/components/new_dropdown/modal.vue b/app/assets/javascripts/ide/components/new_dropdown/modal.vue index dd2800179ff..1e9668d5154 100644 --- a/app/assets/javascripts/ide/components/new_dropdown/modal.vue +++ b/app/assets/javascripts/ide/components/new_dropdown/modal.vue @@ -71,18 +71,18 @@ export default { > <form slot="body" - @submit.prevent="createEntryInStore" class="form-group row" + @submit.prevent="createEntryInStore" > <label class="label-light col-form-label col-sm-3"> {{ __('Name') }} </label> <div class="col-sm-9"> <input + ref="fieldName" + v-model="entryName" type="text" class="form-control" - v-model="entryName" - ref="fieldName" /> </div> </form> diff --git a/app/assets/javascripts/ide/components/new_dropdown/upload.vue b/app/assets/javascripts/ide/components/new_dropdown/upload.vue index c165af5ce52..1814924be39 100644 --- a/app/assets/javascripts/ide/components/new_dropdown/upload.vue +++ b/app/assets/javascripts/ide/components/new_dropdown/upload.vue @@ -67,9 +67,9 @@ </a> <input id="file-upload" + ref="fileUpload" type="file" class="hidden" - ref="fileUpload" /> </div> </template> diff --git a/app/assets/javascripts/ide/components/panes/right.vue b/app/assets/javascripts/ide/components/panes/right.vue index 703c4a70cfa..dedc2988618 100644 --- a/app/assets/javascripts/ide/components/panes/right.vue +++ b/app/assets/javascripts/ide/components/panes/right.vue @@ -4,6 +4,8 @@ import tooltip from '../../../vue_shared/directives/tooltip'; import Icon from '../../../vue_shared/components/icon.vue'; import { rightSidebarViews } from '../../constants'; import PipelinesList from '../pipelines/list.vue'; +import JobsDetail from '../jobs/detail.vue'; +import ResizablePanel from '../resizable_panel.vue'; export default { directives: { @@ -12,9 +14,17 @@ export default { components: { Icon, PipelinesList, + JobsDetail, + ResizablePanel, }, computed: { ...mapState(['rightPane']), + pipelinesActive() { + return ( + this.rightPane === rightSidebarViews.pipelines || + this.rightPane === rightSidebarViews.jobsDetail + ); + }, }, methods: { ...mapActions(['setRightPane']), @@ -32,24 +42,28 @@ export default { <div class="multi-file-commit-panel ide-right-sidebar" > - <div - class="multi-file-commit-panel-inner" + <resizable-panel v-if="rightPane" + :collapsible="false" + :initial-width="350" + :min-size="350" + class="multi-file-commit-panel-inner" + side="right" > <component :is="rightPane" /> - </div> + </resizable-panel> <nav class="ide-activity-bar"> <ul class="list-unstyled"> <li> <button v-tooltip - data-container="body" - data-placement="left" :title="__('Pipelines')" - class="ide-sidebar-link is-right" :class="{ - active: rightPane === $options.rightSidebarViews.pipelines + active: pipelinesActive }" + data-container="body" + data-placement="left" + class="ide-sidebar-link is-right" type="button" @click="clickTab($event, $options.rightSidebarViews.pipelines)" > diff --git a/app/assets/javascripts/ide/components/pipelines/list.vue b/app/assets/javascripts/ide/components/pipelines/list.vue index 06455fac439..5757dfdc925 100644 --- a/app/assets/javascripts/ide/components/pipelines/list.vue +++ b/app/assets/javascripts/ide/components/pipelines/list.vue @@ -75,8 +75,8 @@ export default { > #{{ latestPipeline.id }} <icon - name="external-link" :size="12" + name="external-link" /> </a> </span> @@ -94,7 +94,7 @@ export default { <p class="append-bottom-0"> {{ __('Found errors in your .gitlab-ci.yml:') }} </p> - <p class="append-bottom-0"> + <p class="append-bottom-0 break-word"> {{ latestPipeline.yamlError }} </p> <p diff --git a/app/assets/javascripts/ide/components/repo_commit_section.vue b/app/assets/javascripts/ide/components/repo_commit_section.vue index c5092d8e04d..c2c678ff0be 100644 --- a/app/assets/javascripts/ide/components/repo_commit_section.vue +++ b/app/assets/javascripts/ide/components/repo_commit_section.vue @@ -6,7 +6,7 @@ import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue'; import CommitFilesList from './commit_sidebar/list.vue'; import EmptyState from './commit_sidebar/empty_state.vue'; import * as consts from '../stores/modules/commit/constants'; -import { activityBarViews } from '../constants'; +import { activityBarViews, stageKeys } from '../constants'; export default { components: { @@ -27,11 +27,14 @@ export default { 'unusedSeal', ]), ...mapState('commit', ['commitMessage', 'submitCommitLoading']), - ...mapGetters(['lastOpenedFile', 'hasChanges', 'someUncommitedChanges']), + ...mapGetters(['lastOpenedFile', 'hasChanges', 'someUncommitedChanges', 'activeFile']), ...mapGetters('commit', ['commitButtonDisabled', 'discardDraftButtonDisabled']), showStageUnstageArea() { return !!(this.someUncommitedChanges || this.lastCommitMsg || !this.unusedSeal); }, + activeFileKey() { + return this.activeFile ? this.activeFile.key : null; + }, }, watch: { hasChanges() { @@ -44,6 +47,7 @@ export default { if (this.lastOpenedFile) { this.openPendingTab({ file: this.lastOpenedFile, + keyPrefix: this.lastOpenedFile.changed ? stageKeys.unstaged : stageKeys.staged, }) .then(changeViewer => { if (changeViewer) { @@ -62,6 +66,7 @@ export default { return this.updateCommitAction(consts.COMMIT_TO_NEW_BRANCH).then(() => this.commitChanges()); }, }, + stageKeys, }; </script> @@ -72,8 +77,8 @@ export default { <deprecated-modal id="ide-create-branch-modal" :primary-button-label="__('Create new branch')" - kind="success" :title="__('Branch has changed')" + kind="success" @submit="forceCreateNewBranch" > <template slot="body"> @@ -85,22 +90,28 @@ export default { v-if="showStageUnstageArea" > <commit-files-list - class="is-first" - icon-name="unstaged" :title="__('Unstaged')" + :key-prefix="$options.stageKeys.unstaged" :file-list="changedFiles" + :action-btn-text="__('Stage all changes')" + :active-file-key="activeFileKey" action="stageAllChanges" - :action-btn-text="__('Stage all')" + action-btn-icon="mobile-issue-close" item-action-component="stage-button" + class="is-first" + icon-name="unstaged" /> <commit-files-list - icon-name="staged" :title="__('Staged')" + :key-prefix="$options.stageKeys.staged" :file-list="stagedFiles" + :action-btn-text="__('Unstage all changes')" + :staged-list="true" + :active-file-key="activeFileKey" action="unstageAllChanges" - :action-btn-text="__('Unstage all')" + action-btn-icon="history" item-action-component="unstage-button" - :staged-list="true" + icon-name="staged" /> </template> <empty-state diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue index 93453989c08..08ee12fd98f 100644 --- a/app/assets/javascripts/ide/components/repo_editor.vue +++ b/app/assets/javascripts/ide/components/repo_editor.vue @@ -1,16 +1,16 @@ <script> -/* global monaco */ import { mapState, mapGetters, mapActions } from 'vuex'; import flash from '~/flash'; import ContentViewer from '~/vue_shared/components/content_viewer/content_viewer.vue'; +import DiffViewer from '~/vue_shared/components/diff_viewer/diff_viewer.vue'; import { activityBarViews, viewerTypes } from '../constants'; -import monacoLoader from '../monaco_loader'; import Editor from '../lib/editor'; import ExternalLink from './external_link.vue'; export default { components: { ContentViewer, + DiffViewer, ExternalLink, }, props: { @@ -20,7 +20,13 @@ export default { }, }, computed: { - ...mapState(['rightPanelCollapsed', 'viewer', 'panelResizing', 'currentActivityView']), + ...mapState([ + 'rightPanelCollapsed', + 'viewer', + 'panelResizing', + 'currentActivityView', + 'rightPane', + ]), ...mapGetters([ 'currentMergeRequest', 'getStagedFile', @@ -31,9 +37,18 @@ export default { shouldHideEditor() { return this.file && this.file.binary && !this.file.content; }, + showContentViewer() { + return ( + (this.shouldHideEditor || this.file.viewMode === 'preview') && + (this.viewer !== viewerTypes.mr || !this.file.mrChange) + ); + }, + showDiffViewer() { + return this.shouldHideEditor && this.file.mrChange && this.viewer === viewerTypes.mr; + }, editTabCSS() { return { - active: this.file.viewMode === 'edit', + active: this.file.viewMode === 'editor', }; }, previewTabCSS() { @@ -50,12 +65,12 @@ export default { // Compare key to allow for files opened in review mode to be cached differently if (oldVal.key !== this.file.key) { - this.initMonaco(); + this.initEditor(); if (this.currentActivityView !== activityBarViews.edit) { this.setFileViewMode({ file: this.file, - viewMode: 'edit', + viewMode: 'editor', }); } } @@ -64,7 +79,7 @@ export default { if (this.currentActivityView !== activityBarViews.edit) { this.setFileViewMode({ file: this.file, - viewMode: 'edit', + viewMode: 'editor', }); } }, @@ -79,20 +94,18 @@ export default { this.editor.updateDimensions(); } }, + rightPane() { + this.editor.updateDimensions(); + }, }, beforeDestroy() { this.editor.dispose(); }, mounted() { - if (this.editor && monaco) { - this.initMonaco(); - } else { - monacoLoader(['vs/editor/editor.main'], () => { - this.editor = Editor.create(monaco); - - this.initMonaco(); - }); + if (!this.editor) { + this.editor = Editor.create(); } + this.initEditor(); }, methods: { ...mapActions([ @@ -105,7 +118,7 @@ export default { 'updateViewer', 'removePendingTab', ]), - initMonaco() { + initEditor() { if (this.shouldHideEditor) return; this.editor.clearEditor(); @@ -118,7 +131,7 @@ export default { this.createEditorInstance(); }) .catch(err => { - flash('Error setting up monaco. Please try again.', 'alert', document, null, false, true); + flash('Error setting up editor. Please try again.', 'alert', document, null, false, true); throw err; }); }, @@ -197,14 +210,14 @@ export default { > <div class="ide-mode-tabs clearfix" > <ul - class="nav-links float-left" v-if="!shouldHideEditor && isEditModeActive" + class="nav-links float-left" > <li :class="editTabCSS"> <a href="javascript:void(0);" role="button" - @click.prevent="setFileViewMode({ file, viewMode: 'edit' })"> + @click.prevent="setFileViewMode({ file, viewMode: 'editor' })"> <template v-if="viewer === $options.viewerTypes.edit"> {{ __('Edit') }} </template> @@ -229,19 +242,27 @@ export default { /> </div> <div - v-show="!shouldHideEditor && file.viewMode === 'edit'" + v-show="!shouldHideEditor && file.viewMode ==='editor'" ref="editor" - class="multi-file-editor-holder" :class="{ 'is-readonly': isCommitModeActive, }" + class="multi-file-editor-holder" > </div> <content-viewer - v-if="shouldHideEditor || file.viewMode === 'preview'" + v-if="showContentViewer" :content="file.content || file.raw" :path="file.rawPath || file.path" :file-size="file.size" :project-path="file.projectId"/> + <diff-viewer + v-if="showDiffViewer" + :diff-mode="file.mrChange.diffMode" + :new-path="file.mrChange.new_path" + :new-sha="currentMergeRequest.sha" + :old-path="file.mrChange.old_path" + :old-sha="currentMergeRequest.baseCommitSha" + :project-path="file.projectId"/> </div> </template> diff --git a/app/assets/javascripts/ide/components/repo_file.vue b/app/assets/javascripts/ide/components/repo_file.vue index f56aeced806..c34547fcc60 100644 --- a/app/assets/javascripts/ide/components/repo_file.vue +++ b/app/assets/javascripts/ide/components/repo_file.vue @@ -120,17 +120,17 @@ export default { <template> <div> <div - class="file" :class="fileClass" - @click="clickFile" + class="file" role="button" + @click="clickFile" > <div class="file-name" > <span - class="ide-file-name str-truncated" :style="levelIndentation" + class="ide-file-name str-truncated" > <file-icon :file-name="file.name" @@ -156,10 +156,10 @@ export default { <icon v-tooltip :title="folderChangesTooltip" + :size="12" data-container="body" data-placement="right" name="file-modified" - :size="12" css-classes="prepend-left-5 multi-file-modified" /> </span> diff --git a/app/assets/javascripts/ide/components/repo_file_status_icon.vue b/app/assets/javascripts/ide/components/repo_file_status_icon.vue index 97589e116c5..76a3333be50 100644 --- a/app/assets/javascripts/ide/components/repo_file_status_icon.vue +++ b/app/assets/javascripts/ide/components/repo_file_status_icon.vue @@ -26,8 +26,8 @@ export default { <template> <span - v-if="file.file_lock" v-tooltip + v-if="file.file_lock" :title="lockTooltip" data-container="body" > diff --git a/app/assets/javascripts/ide/components/repo_loading_file.vue b/app/assets/javascripts/ide/components/repo_loading_file.vue index 3e47da88050..7a5ede82253 100644 --- a/app/assets/javascripts/ide/components/repo_loading_file.vue +++ b/app/assets/javascripts/ide/components/repo_loading_file.vue @@ -33,8 +33,8 @@ <td class="d-none d-sm-block"> <skeleton-loading-container - class="animation-container-right" :small="true" + class="animation-container-right" /> </td> </template> diff --git a/app/assets/javascripts/ide/components/repo_tab.vue b/app/assets/javascripts/ide/components/repo_tab.vue index fb26b973236..1ad52c1bd83 100644 --- a/app/assets/javascripts/ide/components/repo_tab.vue +++ b/app/assets/javascripts/ide/components/repo_tab.vue @@ -76,8 +76,8 @@ export default { @mouseout="mouseOutTab" > <div - class="multi-file-tab" :title="tab.url" + class="multi-file-tab" > <file-icon :file-name="tab.name" @@ -89,16 +89,16 @@ export default { /> </div> <button + :aria-label="closeLabel" + :disabled="tab.pending" type="button" class="multi-file-tab-close" @click.stop.prevent="closeFile(tab)" - :aria-label="closeLabel" - :disabled="tab.pending" > <icon v-if="!showChangedIcon" - name="close" :size="12" + name="close" /> <changed-file-icon v-else diff --git a/app/assets/javascripts/ide/components/repo_tabs.vue b/app/assets/javascripts/ide/components/repo_tabs.vue index 99e51097e12..c12a63e26be 100644 --- a/app/assets/javascripts/ide/components/repo_tabs.vue +++ b/app/assets/javascripts/ide/components/repo_tabs.vue @@ -52,8 +52,8 @@ export default { <template> <div class="multi-file-tabs"> <ul - class="list-unstyled append-bottom-0" ref="tabsScroller" + class="list-unstyled append-bottom-0" > <repo-tab v-for="tab in files" diff --git a/app/assets/javascripts/ide/components/resizable_panel.vue b/app/assets/javascripts/ide/components/resizable_panel.vue index 5ea2a2f6825..7277fcb7617 100644 --- a/app/assets/javascripts/ide/components/resizable_panel.vue +++ b/app/assets/javascripts/ide/components/resizable_panel.vue @@ -63,11 +63,11 @@ export default { <template> <div - class="multi-file-commit-panel" :class="{ 'is-collapsed': collapsed && collapsible, }" :style="panelStyle" + class="multi-file-commit-panel" @click="toggleFullbarCollapsed" > <slot></slot> @@ -77,9 +77,9 @@ export default { :start-size="initialWidth" :min-size="minSize" :max-size="$options.maxSize" + :side="side === 'right' ? 'left' : 'right'" @resize-start="setResizingStatus(true)" @resize-end="setResizingStatus(false)" - :side="side === 'right' ? 'left' : 'right'" /> </div> </template> diff --git a/app/assets/javascripts/ide/constants.js b/app/assets/javascripts/ide/constants.js index 33cd20caf52..12e0c3aeef0 100644 --- a/app/assets/javascripts/ide/constants.js +++ b/app/assets/javascripts/ide/constants.js @@ -21,6 +21,19 @@ export const viewerTypes = { diff: 'diff', }; +export const diffModes = { + replaced: 'replaced', + new: 'new', + deleted: 'deleted', + renamed: 'renamed', +}; + export const rightSidebarViews = { pipelines: 'pipelines-list', + jobsDetail: 'jobs-detail', +}; + +export const stageKeys = { + unstaged: 'unstaged', + staged: 'staged', }; diff --git a/app/assets/javascripts/ide/lib/common/model.js b/app/assets/javascripts/ide/lib/common/model.js index e5149b1f3ad..78e6f632728 100644 --- a/app/assets/javascripts/ide/lib/common/model.js +++ b/app/assets/javascripts/ide/lib/common/model.js @@ -1,32 +1,32 @@ +import { editor as monacoEditor, Uri } from 'monaco-editor'; import Disposable from './disposable'; import eventHub from '../../eventhub'; export default class Model { - constructor(monaco, file, head = null) { - this.monaco = monaco; + constructor(file, head = null) { this.disposable = new Disposable(); this.file = file; this.head = head; this.content = file.content !== '' ? file.content : file.raw; this.disposable.add( - (this.originalModel = this.monaco.editor.createModel( + (this.originalModel = monacoEditor.createModel( head ? head.content : this.file.raw, undefined, - new this.monaco.Uri(null, null, `original/${this.path}`), + new Uri(false, false, `original/${this.path}`), )), - (this.model = this.monaco.editor.createModel( + (this.model = monacoEditor.createModel( this.content, undefined, - new this.monaco.Uri(null, null, this.path), + new Uri(false, false, this.path), )), ); if (this.file.mrChange) { this.disposable.add( - (this.baseModel = this.monaco.editor.createModel( + (this.baseModel = monacoEditor.createModel( this.file.baseRaw, undefined, - new this.monaco.Uri(null, null, `target/${this.path}`), + new Uri(false, false, `target/${this.path}`), )), ); } diff --git a/app/assets/javascripts/ide/lib/common/model_manager.js b/app/assets/javascripts/ide/lib/common/model_manager.js index 7f643969480..bd9b8fc3fcc 100644 --- a/app/assets/javascripts/ide/lib/common/model_manager.js +++ b/app/assets/javascripts/ide/lib/common/model_manager.js @@ -3,8 +3,7 @@ import Disposable from './disposable'; import Model from './model'; export default class ModelManager { - constructor(monaco) { - this.monaco = monaco; + constructor() { this.disposable = new Disposable(); this.models = new Map(); } @@ -22,7 +21,7 @@ export default class ModelManager { return this.getModel(file.key); } - const model = new Model(this.monaco, file, head); + const model = new Model(file, head); this.models.set(model.path, model); this.disposable.add(model); diff --git a/app/assets/javascripts/ide/lib/diff/controller.js b/app/assets/javascripts/ide/lib/diff/controller.js index f579424cf33..046e562ba2b 100644 --- a/app/assets/javascripts/ide/lib/diff/controller.js +++ b/app/assets/javascripts/ide/lib/diff/controller.js @@ -1,4 +1,4 @@ -/* global monaco */ +import { Range } from 'monaco-editor'; import { throttle } from 'underscore'; import DirtyDiffWorker from './diff_worker'; import Disposable from '../common/disposable'; @@ -16,7 +16,7 @@ export const getDiffChangeType = change => { }; export const getDecorator = change => ({ - range: new monaco.Range(change.lineNumber, 1, change.endLineNumber, 1), + range: new Range(change.lineNumber, 1, change.endLineNumber, 1), options: { isWholeLine: true, linesDecorationsClassName: `dirty-diff dirty-diff-${getDiffChangeType(change)}`, diff --git a/app/assets/javascripts/ide/lib/diff/diff_worker.js b/app/assets/javascripts/ide/lib/diff/diff_worker.js index e74c4046330..f09930e8158 100644 --- a/app/assets/javascripts/ide/lib/diff/diff_worker.js +++ b/app/assets/javascripts/ide/lib/diff/diff_worker.js @@ -1,8 +1,10 @@ import { computeDiff } from './diff'; +// eslint-disable-next-line no-restricted-globals self.addEventListener('message', (e) => { const data = e.data; + // eslint-disable-next-line no-restricted-globals self.postMessage({ path: data.path, changes: computeDiff(data.originalContent, data.newContent), diff --git a/app/assets/javascripts/ide/lib/editor.js b/app/assets/javascripts/ide/lib/editor.js index 9c3bb9cc17d..02038fcb534 100644 --- a/app/assets/javascripts/ide/lib/editor.js +++ b/app/assets/javascripts/ide/lib/editor.js @@ -1,4 +1,5 @@ import _ from 'underscore'; +import { editor as monacoEditor, KeyCode, KeyMod } from 'monaco-editor'; import store from '../stores'; import DecorationsController from './decorations/controller'; import DirtyDiffController from './diff/controller'; @@ -8,6 +9,11 @@ import editorOptions, { defaultEditorOptions } from './editor_options'; import gitlabTheme from './themes/gl_theme'; import keymap from './keymap.json'; +function setupMonacoTheme() { + monacoEditor.defineTheme(gitlabTheme.themeName, gitlabTheme.monacoTheme); + monacoEditor.setTheme('gitlab'); +} + export const clearDomElement = el => { if (!el || !el.firstChild) return; @@ -17,24 +23,22 @@ export const clearDomElement = el => { }; export default class Editor { - static create(monaco) { - if (this.editorInstance) return this.editorInstance; - - this.editorInstance = new Editor(monaco); - + static create() { + if (!this.editorInstance) { + this.editorInstance = new Editor(); + } return this.editorInstance; } - constructor(monaco) { - this.monaco = monaco; + constructor() { this.currentModel = null; this.instance = null; this.dirtyDiffController = null; this.disposable = new Disposable(); - this.modelManager = new ModelManager(this.monaco); + this.modelManager = new ModelManager(); this.decorationsController = new DecorationsController(this); - this.setupMonacoTheme(); + setupMonacoTheme(); this.debouncedUpdate = _.debounce(() => { this.updateDimensions(); @@ -46,7 +50,7 @@ export default class Editor { clearDomElement(domElement); this.disposable.add( - (this.instance = this.monaco.editor.create(domElement, { + (this.instance = monacoEditor.create(domElement, { ...defaultEditorOptions, })), (this.dirtyDiffController = new DirtyDiffController( @@ -66,7 +70,7 @@ export default class Editor { clearDomElement(domElement); this.disposable.add( - (this.instance = this.monaco.editor.createDiffEditor(domElement, { + (this.instance = monacoEditor.createDiffEditor(domElement, { ...defaultEditorOptions, quickSuggestions: false, occurrencesHighlight: false, @@ -122,17 +126,11 @@ export default class Editor { modified: model.getModel(), }); - this.monaco.editor.createDiffNavigator(this.instance, { + monacoEditor.createDiffNavigator(this.instance, { alwaysRevealFirst: true, }); } - setupMonacoTheme() { - this.monaco.editor.defineTheme(gitlabTheme.themeName, gitlabTheme.monacoTheme); - - this.monaco.editor.setTheme('gitlab'); - } - clearEditor() { if (this.instance) { this.instance.setModel(null); @@ -200,7 +198,7 @@ export default class Editor { const getKeyCode = key => { const monacoKeyMod = key.indexOf('KEY_') === 0; - return monacoKeyMod ? this.monaco.KeyCode[key] : this.monaco.KeyMod[key]; + return monacoKeyMod ? KeyCode[key] : KeyMod[key]; }; keymap.forEach(command => { diff --git a/app/assets/javascripts/ide/lib/editor_options.js b/app/assets/javascripts/ide/lib/editor_options.js index 9f895d49f2e..e35595ab1fd 100644 --- a/app/assets/javascripts/ide/lib/editor_options.js +++ b/app/assets/javascripts/ide/lib/editor_options.js @@ -12,5 +12,6 @@ export const defaultEditorOptions = { export default [ { readOnly: model => !!model.file.file_lock, + quickSuggestions: model => !(model.language === 'markdown'), }, ]; diff --git a/app/assets/javascripts/ide/monaco_loader.js b/app/assets/javascripts/ide/monaco_loader.js deleted file mode 100644 index 142a220097b..00000000000 --- a/app/assets/javascripts/ide/monaco_loader.js +++ /dev/null @@ -1,16 +0,0 @@ -import monacoContext from 'monaco-editor/dev/vs/loader'; - -monacoContext.require.config({ - paths: { - vs: `${__webpack_public_path__}monaco-editor/vs`, // eslint-disable-line camelcase - }, -}); - -// ignore CDN config and use local assets path for service worker which cannot be cross-domain -const relativeRootPath = (gon && gon.relative_url_root) || ''; -const monacoPath = `${relativeRootPath}/assets/webpack/monaco-editor/vs`; -window.MonacoEnvironment = { getWorkerUrl: () => `${monacoPath}/base/worker/workerMain.js` }; - -// eslint-disable-next-line no-underscore-dangle -window.__monaco_context__ = monacoContext; -export default monacoContext.require; diff --git a/app/assets/javascripts/ide/services/index.js b/app/assets/javascripts/ide/services/index.js index e8b51f2b516..da9de25302a 100644 --- a/app/assets/javascripts/ide/services/index.js +++ b/app/assets/javascripts/ide/services/index.js @@ -9,7 +9,7 @@ export default { return Vue.http.get(endpoint, { params: { format: 'json' } }); }, getFileData(endpoint) { - return Vue.http.get(endpoint, { params: { format: 'json' } }); + return Vue.http.get(endpoint, { params: { format: 'json', viewer: 'none' } }); }, getRawFileData(file) { if (file.tempFile) { diff --git a/app/assets/javascripts/ide/stores/actions/merge_request.js b/app/assets/javascripts/ide/stores/actions/merge_request.js index 5ec9bd661bb..edb20ff96fc 100644 --- a/app/assets/javascripts/ide/stores/actions/merge_request.js +++ b/app/assets/javascripts/ide/stores/actions/merge_request.js @@ -17,9 +17,7 @@ export const getMergeRequestData = ( mergeRequestId, mergeRequest: data, }); - if (!state.currentMergeRequestId) { - commit(types.SET_CURRENT_MERGE_REQUEST, mergeRequestId); - } + commit(types.SET_CURRENT_MERGE_REQUEST, mergeRequestId); resolve(data); }) .catch(() => { diff --git a/app/assets/javascripts/ide/stores/actions/project.js b/app/assets/javascripts/ide/stores/actions/project.js index 46af47d2f81..0b99bce4a8e 100644 --- a/app/assets/javascripts/ide/stores/actions/project.js +++ b/app/assets/javascripts/ide/stores/actions/project.js @@ -13,8 +13,7 @@ export const getProjectData = ({ commit, state }, { namespace, projectId, force .then(data => { commit(types.TOGGLE_LOADING, { entry: state }); commit(types.SET_PROJECT, { projectPath: `${namespace}/${projectId}`, project: data }); - if (!state.currentProjectId) - commit(types.SET_CURRENT_PROJECT, `${namespace}/${projectId}`); + commit(types.SET_CURRENT_PROJECT, `${namespace}/${projectId}`); resolve(data); }) .catch(() => { diff --git a/app/assets/javascripts/ide/stores/modules/commit/actions.js b/app/assets/javascripts/ide/stores/modules/commit/actions.js index 0a0db4033c8..7219abc4185 100644 --- a/app/assets/javascripts/ide/stores/modules/commit/actions.js +++ b/app/assets/javascripts/ide/stores/modules/commit/actions.js @@ -49,31 +49,6 @@ export const setLastCommitMessage = ({ rootState, commit }, data) => { commit(rootTypes.SET_LAST_COMMIT_MSG, commitMsg, { root: true }); }; -export const checkCommitStatus = ({ rootState }) => - service - .getBranchData(rootState.currentProjectId, rootState.currentBranchId) - .then(({ data }) => { - const { id } = data.commit; - const selectedBranch = - rootState.projects[rootState.currentProjectId].branches[rootState.currentBranchId]; - - if (selectedBranch.workingReference !== id) { - return true; - } - - return false; - }) - .catch(() => - flash( - __('Error checking branch data. Please try again.'), - 'alert', - document, - null, - false, - true, - ), - ); - export const updateFilesAfterCommit = ({ commit, dispatch, rootState }, { data }) => { const selectedProject = rootState.projects[rootState.currentProjectId]; const lastCommit = { @@ -128,24 +103,17 @@ export const updateFilesAfterCommit = ({ commit, dispatch, rootState }, { data } export const commitChanges = ({ commit, state, getters, dispatch, rootState, rootGetters }) => { const newBranch = state.commitAction !== consts.COMMIT_TO_CURRENT_BRANCH; - const payload = createCommitPayload(getters.branchName, newBranch, state, rootState); - const getCommitStatus = newBranch ? Promise.resolve(false) : dispatch('checkCommitStatus'); + const payload = createCommitPayload({ + branch: getters.branchName, + newBranch, + state, + rootState, + }); commit(types.UPDATE_LOADING, true); - return getCommitStatus - .then( - branchChanged => - new Promise(resolve => { - if (branchChanged) { - // show the modal with a Bootstrap call - $('#ide-create-branch-modal').modal('show'); - } else { - resolve(); - } - }), - ) - .then(() => service.commit(rootState.currentProjectId, payload)) + return service + .commit(rootState.currentProjectId, payload) .then(({ data }) => { commit(types.UPDATE_LOADING, false); @@ -220,12 +188,16 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState, roo ); }) .catch(err => { - let errMsg = __('Error committing changes. Please try again.'); - if (err.response.data && err.response.data.message) { - errMsg += ` (${stripHtml(err.response.data.message)})`; + if (err.response.status === 400) { + $('#ide-create-branch-modal').modal('show'); + } else { + let errMsg = __('Error committing changes. Please try again.'); + if (err.response.data && err.response.data.message) { + errMsg += ` (${stripHtml(err.response.data.message)})`; + } + flash(errMsg, 'alert', document, null, false, true); + window.dispatchEvent(new Event('resize')); } - flash(errMsg, 'alert', document, null, false, true); - window.dispatchEvent(new Event('resize')); commit(types.UPDATE_LOADING, false); }); diff --git a/app/assets/javascripts/ide/stores/modules/merge_requests/actions.js b/app/assets/javascripts/ide/stores/modules/merge_requests/actions.js index d3050183bd3..551dd322c9b 100644 --- a/app/assets/javascripts/ide/stores/modules/merge_requests/actions.js +++ b/app/assets/javascripts/ide/stores/modules/merge_requests/actions.js @@ -1,25 +1,48 @@ import { __ } from '../../../../locale'; import Api from '../../../../api'; import flash from '../../../../flash'; +import router from '../../../ide_router'; +import { scopes } from './constants'; import * as types from './mutation_types'; +import * as rootTypes from '../../mutation_types'; -export const requestMergeRequests = ({ commit }) => commit(types.REQUEST_MERGE_REQUESTS); -export const receiveMergeRequestsError = ({ commit }) => { +export const requestMergeRequests = ({ commit }, type) => + commit(types.REQUEST_MERGE_REQUESTS, type); +export const receiveMergeRequestsError = ({ commit }, type) => { flash(__('Error loading merge requests.')); - commit(types.RECEIVE_MERGE_REQUESTS_ERROR); + commit(types.RECEIVE_MERGE_REQUESTS_ERROR, type); }; -export const receiveMergeRequestsSuccess = ({ commit }, data) => - commit(types.RECEIVE_MERGE_REQUESTS_SUCCESS, data); +export const receiveMergeRequestsSuccess = ({ commit }, { type, data }) => + commit(types.RECEIVE_MERGE_REQUESTS_SUCCESS, { type, data }); -export const fetchMergeRequests = ({ dispatch, state: { scope, state } }, search = '') => { - dispatch('requestMergeRequests'); - dispatch('resetMergeRequests'); +export const fetchMergeRequests = ({ dispatch, state: { state } }, { type, search = '' }) => { + const scope = scopes[type]; + dispatch('requestMergeRequests', type); + dispatch('resetMergeRequests', type); Api.mergeRequests({ scope, state, search }) - .then(({ data }) => dispatch('receiveMergeRequestsSuccess', data)) - .catch(() => dispatch('receiveMergeRequestsError')); + .then(({ data }) => dispatch('receiveMergeRequestsSuccess', { type, data })) + .catch(() => dispatch('receiveMergeRequestsError', type)); }; -export const resetMergeRequests = ({ commit }) => commit(types.RESET_MERGE_REQUESTS); +export const resetMergeRequests = ({ commit }, type) => commit(types.RESET_MERGE_REQUESTS, type); + +export const openMergeRequest = ({ commit, dispatch }, { projectPath, id }) => { + commit(rootTypes.CLEAR_PROJECTS, null, { root: true }); + commit(rootTypes.SET_CURRENT_MERGE_REQUEST, `${id}`, { root: true }); + commit(rootTypes.RESET_OPEN_FILES, null, { root: true }); + dispatch('setCurrentBranchId', '', { root: true }); + dispatch('pipelines/stopPipelinePolling', null, { root: true }) + .then(() => { + dispatch('pipelines/resetLatestPipeline', null, { root: true }); + dispatch('pipelines/clearEtagPoll', null, { root: true }); + }) + .catch(e => { + throw e; + }); + dispatch('setRightPane', null, { root: true }); + + router.push(`/project/${projectPath}/merge_requests/${id}`); +}; export default () => {}; diff --git a/app/assets/javascripts/ide/stores/modules/merge_requests/constants.js b/app/assets/javascripts/ide/stores/modules/merge_requests/constants.js index 64b7763f257..a7085c7d04c 100644 --- a/app/assets/javascripts/ide/stores/modules/merge_requests/constants.js +++ b/app/assets/javascripts/ide/stores/modules/merge_requests/constants.js @@ -1,6 +1,6 @@ export const scopes = { - assignedToMe: 'assigned-to-me', - createdByMe: 'created-by-me', + assigned: 'assigned-to-me', + created: 'created-by-me', }; export const states = { diff --git a/app/assets/javascripts/ide/stores/modules/merge_requests/getters.js b/app/assets/javascripts/ide/stores/modules/merge_requests/getters.js new file mode 100644 index 00000000000..8e2b234be8d --- /dev/null +++ b/app/assets/javascripts/ide/stores/modules/merge_requests/getters.js @@ -0,0 +1,4 @@ +export const getData = state => type => state[type]; + +export const assignedData = state => state.assigned; +export const createdData = state => state.created; diff --git a/app/assets/javascripts/ide/stores/modules/merge_requests/index.js b/app/assets/javascripts/ide/stores/modules/merge_requests/index.js index 04e7e0f08f1..2e6dfb420f4 100644 --- a/app/assets/javascripts/ide/stores/modules/merge_requests/index.js +++ b/app/assets/javascripts/ide/stores/modules/merge_requests/index.js @@ -1,5 +1,6 @@ import state from './state'; import * as actions from './actions'; +import * as getters from './getters'; import mutations from './mutations'; export default { @@ -7,4 +8,5 @@ export default { state: state(), actions, mutations, + getters, }; diff --git a/app/assets/javascripts/ide/stores/modules/merge_requests/mutations.js b/app/assets/javascripts/ide/stores/modules/merge_requests/mutations.js index 98102a68e08..971da0806bd 100644 --- a/app/assets/javascripts/ide/stores/modules/merge_requests/mutations.js +++ b/app/assets/javascripts/ide/stores/modules/merge_requests/mutations.js @@ -2,15 +2,15 @@ import * as types from './mutation_types'; export default { - [types.REQUEST_MERGE_REQUESTS](state) { - state.isLoading = true; + [types.REQUEST_MERGE_REQUESTS](state, type) { + state[type].isLoading = true; }, - [types.RECEIVE_MERGE_REQUESTS_ERROR](state) { - state.isLoading = false; + [types.RECEIVE_MERGE_REQUESTS_ERROR](state, type) { + state[type].isLoading = false; }, - [types.RECEIVE_MERGE_REQUESTS_SUCCESS](state, data) { - state.isLoading = false; - state.mergeRequests = data.map(mergeRequest => ({ + [types.RECEIVE_MERGE_REQUESTS_SUCCESS](state, { type, data }) { + state[type].isLoading = false; + state[type].mergeRequests = data.map(mergeRequest => ({ id: mergeRequest.id, iid: mergeRequest.iid, title: mergeRequest.title, @@ -20,7 +20,7 @@ export default { .replace(`/merge_requests/${mergeRequest.iid}`, ''), })); }, - [types.RESET_MERGE_REQUESTS](state) { - state.mergeRequests = []; + [types.RESET_MERGE_REQUESTS](state, type) { + state[type].mergeRequests = []; }, }; diff --git a/app/assets/javascripts/ide/stores/modules/merge_requests/state.js b/app/assets/javascripts/ide/stores/modules/merge_requests/state.js index 2947b686c1c..57eb6b04283 100644 --- a/app/assets/javascripts/ide/stores/modules/merge_requests/state.js +++ b/app/assets/javascripts/ide/stores/modules/merge_requests/state.js @@ -1,8 +1,13 @@ -import { scopes, states } from './constants'; +import { states } from './constants'; export default () => ({ - isLoading: false, - mergeRequests: [], - scope: scopes.assignedToMe, + created: { + isLoading: false, + mergeRequests: [], + }, + assigned: { + isLoading: false, + mergeRequests: [], + }, state: states.opened, }); diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/actions.js b/app/assets/javascripts/ide/stores/modules/pipelines/actions.js index 1ebe487263b..fe1dc9ac8f8 100644 --- a/app/assets/javascripts/ide/stores/modules/pipelines/actions.js +++ b/app/assets/javascripts/ide/stores/modules/pipelines/actions.js @@ -4,6 +4,7 @@ import { __ } from '../../../../locale'; import flash from '../../../../flash'; import Poll from '../../../../lib/utils/poll'; import service from '../../../services'; +import { rightSidebarViews } from '../../../constants'; import * as types from './mutation_types'; let eTagPoll; @@ -11,8 +12,12 @@ let eTagPoll; export const clearEtagPoll = () => { eTagPoll = null; }; -export const stopPipelinePolling = () => eTagPoll && eTagPoll.stop(); -export const restartPipelinePolling = () => eTagPoll && eTagPoll.restart(); +export const stopPipelinePolling = () => { + if (eTagPoll) eTagPoll.stop(); +}; +export const restartPipelinePolling = () => { + if (eTagPoll) eTagPoll.restart(); +}; export const requestLatestPipeline = ({ commit }) => commit(types.REQUEST_LATEST_PIPELINE); export const receiveLatestPipelineError = ({ commit, dispatch }) => { @@ -50,9 +55,9 @@ export const fetchLatestPipeline = ({ dispatch, rootGetters }) => { Visibility.change(() => { if (!Visibility.hidden()) { - eTagPoll.restart(); + dispatch('restartPipelinePolling'); } else { - eTagPoll.stop(); + dispatch('stopPipelinePolling'); } }); }; @@ -77,4 +82,33 @@ export const fetchJobs = ({ dispatch }, stage) => { export const toggleStageCollapsed = ({ commit }, stageId) => commit(types.TOGGLE_STAGE_COLLAPSE, stageId); +export const setDetailJob = ({ commit, dispatch }, job) => { + commit(types.SET_DETAIL_JOB, job); + dispatch('setRightPane', job ? rightSidebarViews.jobsDetail : rightSidebarViews.pipelines, { + root: true, + }); +}; + +export const requestJobTrace = ({ commit }) => commit(types.REQUEST_JOB_TRACE); +export const receiveJobTraceError = ({ commit }) => { + flash(__('Error fetching job trace')); + commit(types.RECEIVE_JOB_TRACE_ERROR); +}; +export const receiveJobTraceSuccess = ({ commit }, data) => + commit(types.RECEIVE_JOB_TRACE_SUCCESS, data); + +export const fetchJobTrace = ({ dispatch, state }) => { + dispatch('requestJobTrace'); + + return axios + .get(`${state.detailJob.path}/trace`, { params: { format: 'json' } }) + .then(({ data }) => dispatch('receiveJobTraceSuccess', data)) + .catch(() => dispatch('receiveJobTraceError')); +}; + +export const resetLatestPipeline = ({ commit }) => { + commit(types.RECEIVE_LASTEST_PIPELINE_SUCCESS, null); + commit(types.SET_DETAIL_JOB, null); +}; + export default () => {}; diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/mutation_types.js b/app/assets/javascripts/ide/stores/modules/pipelines/mutation_types.js index 3ddc8409c5b..f4c36b9d96f 100644 --- a/app/assets/javascripts/ide/stores/modules/pipelines/mutation_types.js +++ b/app/assets/javascripts/ide/stores/modules/pipelines/mutation_types.js @@ -7,3 +7,9 @@ export const RECEIVE_JOBS_ERROR = 'RECEIVE_JOBS_ERROR'; export const RECEIVE_JOBS_SUCCESS = 'RECEIVE_JOBS_SUCCESS'; export const TOGGLE_STAGE_COLLAPSE = 'TOGGLE_STAGE_COLLAPSE'; + +export const SET_DETAIL_JOB = 'SET_DETAIL_JOB'; + +export const REQUEST_JOB_TRACE = 'REQUEST_JOB_TRACE'; +export const RECEIVE_JOB_TRACE_ERROR = 'RECEIVE_JOB_TRACE_ERROR'; +export const RECEIVE_JOB_TRACE_SUCCESS = 'RECEIVE_JOB_TRACE_SUCCESS'; diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/mutations.js b/app/assets/javascripts/ide/stores/modules/pipelines/mutations.js index 745797e1ee5..5a2213bbe89 100644 --- a/app/assets/javascripts/ide/stores/modules/pipelines/mutations.js +++ b/app/assets/javascripts/ide/stores/modules/pipelines/mutations.js @@ -63,4 +63,17 @@ export default { isCollapsed: stage.id === id ? !stage.isCollapsed : stage.isCollapsed, })); }, + [types.SET_DETAIL_JOB](state, job) { + state.detailJob = { ...job }; + }, + [types.REQUEST_JOB_TRACE](state) { + state.detailJob.isLoading = true; + }, + [types.RECEIVE_JOB_TRACE_ERROR](state) { + state.detailJob.isLoading = false; + }, + [types.RECEIVE_JOB_TRACE_SUCCESS](state, data) { + state.detailJob.isLoading = false; + state.detailJob.output = data.html; + }, }; diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/state.js b/app/assets/javascripts/ide/stores/modules/pipelines/state.js index 0f83b315fff..8651e267b53 100644 --- a/app/assets/javascripts/ide/stores/modules/pipelines/state.js +++ b/app/assets/javascripts/ide/stores/modules/pipelines/state.js @@ -3,4 +3,5 @@ export default () => ({ isLoadingJobs: false, latestPipeline: null, stages: [], + detailJob: null, }); diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/utils.js b/app/assets/javascripts/ide/stores/modules/pipelines/utils.js index 9f4b0d7d726..a6caca2d2dc 100644 --- a/app/assets/javascripts/ide/stores/modules/pipelines/utils.js +++ b/app/assets/javascripts/ide/stores/modules/pipelines/utils.js @@ -4,4 +4,8 @@ export const normalizeJob = job => ({ name: job.name, status: job.status, path: job.build_path, + rawPath: `${job.build_path}/raw`, + started: job.started, + output: '', + isLoading: false, }); diff --git a/app/assets/javascripts/ide/stores/mutation_types.js b/app/assets/javascripts/ide/stores/mutation_types.js index fbfb92105d6..99b315ac4db 100644 --- a/app/assets/javascripts/ide/stores/mutation_types.js +++ b/app/assets/javascripts/ide/stores/mutation_types.js @@ -68,3 +68,6 @@ export const TOGGLE_FILE_FINDER = 'TOGGLE_FILE_FINDER'; export const BURST_UNUSED_SEAL = 'BURST_UNUSED_SEAL'; export const SET_RIGHT_PANE = 'SET_RIGHT_PANE'; + +export const CLEAR_PROJECTS = 'CLEAR_PROJECTS'; +export const RESET_OPEN_FILES = 'RESET_OPEN_FILES'; diff --git a/app/assets/javascripts/ide/stores/mutations.js b/app/assets/javascripts/ide/stores/mutations.js index eeaa7cb0ec3..48f1da4eccf 100644 --- a/app/assets/javascripts/ide/stores/mutations.js +++ b/app/assets/javascripts/ide/stores/mutations.js @@ -157,6 +157,12 @@ export default { [types.SET_LINKS](state, links) { Object.assign(state, { links }); }, + [types.CLEAR_PROJECTS](state) { + Object.assign(state, { projects: {}, trees: {} }); + }, + [types.RESET_OPEN_FILES](state) { + Object.assign(state, { openFiles: [] }); + }, ...projectMutations, ...mergeRequestMutation, ...fileMutations, diff --git a/app/assets/javascripts/ide/stores/mutations/file.js b/app/assets/javascripts/ide/stores/mutations/file.js index 13f123b6630..46547820425 100644 --- a/app/assets/javascripts/ide/stores/mutations/file.js +++ b/app/assets/javascripts/ide/stores/mutations/file.js @@ -1,5 +1,6 @@ /* eslint-disable no-param-reassign */ import * as types from '../mutation_types'; +import { diffModes } from '../../constants'; export default { [types.SET_FILE_ACTIVE](state, { path, active }) { @@ -46,6 +47,7 @@ export default { baseRaw: null, html: data.html, size: data.size, + lastCommitSha: data.last_commit_sha, }); }, [types.SET_FILE_RAW_DATA](state, { file, raw }) { @@ -85,8 +87,19 @@ export default { }); }, [types.SET_FILE_MERGE_REQUEST_CHANGE](state, { file, mrChange }) { + let diffMode = diffModes.replaced; + if (mrChange.new_file) { + diffMode = diffModes.new; + } else if (mrChange.deleted_file) { + diffMode = diffModes.deleted; + } else if (mrChange.renamed_file) { + diffMode = diffModes.renamed; + } Object.assign(state.entries[file.path], { - mrChange, + mrChange: { + ...mrChange, + diffMode, + }, }); }, [types.SET_FILE_VIEWMODE](state, { file, viewMode }) { diff --git a/app/assets/javascripts/ide/stores/utils.js b/app/assets/javascripts/ide/stores/utils.js index e0b9766fbee..10368a4d97c 100644 --- a/app/assets/javascripts/ide/stores/utils.js +++ b/app/assets/javascripts/ide/stores/utils.js @@ -17,6 +17,7 @@ export const dataStructure = () => ({ changed: false, staged: false, lastCommitPath: '', + lastCommitSha: '', lastCommit: { id: '', url: '', @@ -39,7 +40,7 @@ export const dataStructure = () => ({ editorColumn: 1, fileLanguage: '', eol: '', - viewMode: 'edit', + viewMode: 'editor', previewMode: null, size: 0, parentPath: null, @@ -104,7 +105,7 @@ export const setPageTitle = title => { document.title = title; }; -export const createCommitPayload = (branch, newBranch, state, rootState) => ({ +export const createCommitPayload = ({ branch, newBranch, state, rootState }) => ({ branch, commit_message: state.commitMessage, actions: rootState.stagedFiles.map(f => ({ @@ -112,6 +113,7 @@ export const createCommitPayload = (branch, newBranch, state, rootState) => ({ file_path: f.path, content: f.content, encoding: f.base64 ? 'base64' : 'text', + last_commit_id: newBranch ? undefined : f.lastCommitSha, })), start_branch: newBranch ? rootState.currentBranchId : undefined, }); diff --git a/app/assets/javascripts/ide/stores/workers/files_decorator_worker.js b/app/assets/javascripts/ide/stores/workers/files_decorator_worker.js index 0a1c253c637..fa35c215880 100644 --- a/app/assets/javascripts/ide/stores/workers/files_decorator_worker.js +++ b/app/assets/javascripts/ide/stores/workers/files_decorator_worker.js @@ -1,6 +1,7 @@ import { viewerInformationForPath } from '~/vue_shared/components/content_viewer/lib/viewer_utils'; import { decorateData, sortTree } from '../utils'; +// eslint-disable-next-line no-restricted-globals self.addEventListener('message', e => { const { data, projectId, branchId, tempFile = false, content = '', base64 = false } = e.data; @@ -89,6 +90,7 @@ self.addEventListener('message', e => { return acc; }, {}); + // eslint-disable-next-line no-restricted-globals self.postMessage({ entries, treeList: sortTree(treeList), diff --git a/app/assets/javascripts/importer_status.js b/app/assets/javascripts/importer_status.js index b469e1e2adc..f9ff0722c01 100644 --- a/app/assets/javascripts/importer_status.js +++ b/app/assets/javascripts/importer_status.js @@ -58,7 +58,7 @@ class ImporterStatus { job.find('.import-target').html(`<a href="${data.full_path}">${data.full_path}</a>`); $('table.import-jobs tbody').prepend(job); - job.addClass('active'); + job.addClass('table-active'); const connectingVerb = this.ciCdOnly ? __('connecting') : __('importing'); job.find('.import-actions').html(sprintf( _.escape(__('%{loadingIcon} Started')), { @@ -67,7 +67,15 @@ class ImporterStatus { false, )); }) - .catch(() => flash(__('An error occurred while importing project'))); + .catch((error) => { + let details = error; + + if (error.response && error.response.data && error.response.data.errors) { + details = error.response.data.errors; + } + + flash(__(`An error occurred while importing project: ${details}`)); + }); } autoUpdate() { @@ -81,7 +89,7 @@ class ImporterStatus { switch (job.import_status) { case 'finished': - jobItem.removeClass('active').addClass('success'); + jobItem.removeClass('table-active').addClass('table-success'); statusField.html(`<span><i class="fa fa-check"></i> ${__('Done')}</span>`); break; case 'scheduled': diff --git a/app/assets/javascripts/init_changes_dropdown.js b/app/assets/javascripts/init_changes_dropdown.js index 09cca1dc7d9..5c5a6e01848 100644 --- a/app/assets/javascripts/init_changes_dropdown.js +++ b/app/assets/javascripts/init_changes_dropdown.js @@ -1,5 +1,5 @@ import $ from 'jquery'; -import stickyMonitor from './lib/utils/sticky'; +import { stickyMonitor } from './lib/utils/sticky'; export default (stickyTop) => { stickyMonitor(document.querySelector('.js-diff-files-changed'), stickyTop); diff --git a/app/assets/javascripts/issue_show/components/app.vue b/app/assets/javascripts/issue_show/components/app.vue index e87a8ed7fea..b6364318537 100644 --- a/app/assets/javascripts/issue_show/components/app.vue +++ b/app/assets/javascripts/issue_show/components/app.vue @@ -226,7 +226,7 @@ .then(res => res.data) .then(data => this.checkForSpam(data)) .then((data) => { - if (location.pathname !== data.web_url) { + if (window.location.pathname !== data.web_url) { visitUrl(data.web_url); } diff --git a/app/assets/javascripts/issue_show/components/description.vue b/app/assets/javascripts/issue_show/components/description.vue index ae577e04a56..1174177f561 100644 --- a/app/assets/javascripts/issue_show/components/description.vue +++ b/app/assets/javascripts/issue_show/components/description.vue @@ -110,25 +110,25 @@ <template> <div v-if="descriptionHtml" - class="description" :class="{ 'js-task-list-container': canUpdate }" + class="description" > <div - class="wiki" + ref="gfm-content" :class="{ 'issue-realtime-pre-pulse': preAnimation, 'issue-realtime-trigger-pulse': pulseAnimation }" - v-html="descriptionHtml" - ref="gfm-content"> + class="wiki" + v-html="descriptionHtml"> </div> <textarea - class="hidden js-task-list-field" v-if="descriptionText" v-model="descriptionText" :data-update-url="updateUrl" + class="hidden js-task-list-field" > </textarea> diff --git a/app/assets/javascripts/issue_show/components/edit_actions.vue b/app/assets/javascripts/issue_show/components/edit_actions.vue index 7ef5e679881..597c6d69a81 100644 --- a/app/assets/javascripts/issue_show/components/edit_actions.vue +++ b/app/assets/javascripts/issue_show/components/edit_actions.vue @@ -38,7 +38,7 @@ }, deleteIssuable() { // eslint-disable-next-line no-alert - if (confirm('Issue will be removed! Are you sure?')) { + if (window.confirm('Issue will be removed! Are you sure?')) { this.deleteLoading = true; eventHub.$emit('delete.issuable'); @@ -51,16 +51,16 @@ <template> <div class="prepend-top-default append-bottom-default clearfix"> <button - class="btn btn-save float-left" :class="{ disabled: formState.updateLoading || !isSubmitEnabled }" - type="submit" :disabled="formState.updateLoading || !isSubmitEnabled" + class="btn btn-save float-left" + type="submit" @click.prevent="updateIssuable"> Save changes <i + v-if="formState.updateLoading" class="fa fa-spinner fa-spin" - aria-hidden="true" - v-if="formState.updateLoading"> + aria-hidden="true"> </i> </button> <button @@ -71,16 +71,16 @@ </button> <button v-if="shouldShowDeleteButton" - class="btn btn-danger float-right append-right-default" :class="{ disabled: deleteLoading }" - type="button" :disabled="deleteLoading" + class="btn btn-danger float-right append-right-default" + type="button" @click="deleteIssuable"> Delete <i + v-if="deleteLoading" class="fa fa-spinner fa-spin" - aria-hidden="true" - v-if="deleteLoading"> + aria-hidden="true"> </i> </button> </div> diff --git a/app/assets/javascripts/issue_show/components/edited.vue b/app/assets/javascripts/issue_show/components/edited.vue index 01097b5b35e..5ff5b1630b1 100644 --- a/app/assets/javascripts/issue_show/components/edited.vue +++ b/app/assets/javascripts/issue_show/components/edited.vue @@ -37,16 +37,16 @@ Edited <time-ago-tooltip v-if="updatedAt" - tooltip-placement="bottom" :time="updatedAt" + tooltip-placement="bottom" /> <span v-if="hasUpdatedBy" > by <a - class="author_link" :href="updatedByPath" + class="author_link" > <span>{{ updatedByName }}</span> </a> diff --git a/app/assets/javascripts/issue_show/components/fields/description.vue b/app/assets/javascripts/issue_show/components/fields/description.vue index d9fa2764d65..5f58f671c73 100644 --- a/app/assets/javascripts/issue_show/components/fields/description.vue +++ b/app/assets/javascripts/issue_show/components/fields/description.vue @@ -52,13 +52,13 @@ > <textarea id="issue-description" + ref="textarea" + slot="textarea" + v-model="formState.description" class="note-textarea js-gfm-input js-autosize markdown-area" data-supports-quick-actions="false" aria-label="Description" - v-model="formState.description" - ref="textarea" - slot="textarea" - placeholder="Write a comment or drag your files here..." + placeholder="Write a comment or drag your files here…" @keydown.meta.enter="updateIssuable" @keydown.ctrl.enter="updateIssuable"> </textarea> diff --git a/app/assets/javascripts/issue_show/components/fields/description_template.vue b/app/assets/javascripts/issue_show/components/fields/description_template.vue index 7db0488e306..e90d9fad94e 100644 --- a/app/assets/javascripts/issue_show/components/fields/description_template.vue +++ b/app/assets/javascripts/issue_show/components/fields/description_template.vue @@ -48,15 +48,15 @@ class="dropdown js-issuable-selector-wrap" data-issuable-type="issue"> <button + ref="toggle" + :data-namespace-path="projectNamespace" + :data-project-path="projectPath" + :data-data="issuableTemplatesJson" class="dropdown-menu-toggle js-issuable-selector" type="button" - ref="toggle" data-field-name="issuable_template" data-selected="null" - data-toggle="dropdown" - :data-namespace-path="projectNamespace" - :data-project-path="projectPath" - :data-data="issuableTemplatesJson"> + data-toggle="dropdown"> <span class="dropdown-toggle-text"> Choose a template </span> diff --git a/app/assets/javascripts/issue_show/components/fields/title.vue b/app/assets/javascripts/issue_show/components/fields/title.vue index c3abb9fd9d5..7d1526a64b4 100644 --- a/app/assets/javascripts/issue_show/components/fields/title.vue +++ b/app/assets/javascripts/issue_show/components/fields/title.vue @@ -21,11 +21,11 @@ </label> <input id="issuable-title" + v-model="formState.title" class="form-control" type="text" placeholder="Title" aria-label="Title" - v-model="formState.title" @keydown.meta.enter="updateIssuable" @keydown.ctrl.enter="updateIssuable" /> </fieldset> diff --git a/app/assets/javascripts/issue_show/components/form.vue b/app/assets/javascripts/issue_show/components/form.vue index ab8bd34762f..5bfc072e3da 100644 --- a/app/assets/javascripts/issue_show/components/form.vue +++ b/app/assets/javascripts/issue_show/components/form.vue @@ -72,8 +72,8 @@ <locked-warning v-if="formState.lockedWarningVisible" /> <div class="row"> <div - class="col-sm-4 col-lg-3" - v-if="hasIssuableTemplates"> + v-if="hasIssuableTemplates" + class="col-sm-4 col-lg-3"> <description-template :form-state="formState" :issuable-templates="issuableTemplates" diff --git a/app/assets/javascripts/issue_show/components/locked_warning.vue b/app/assets/javascripts/issue_show/components/locked_warning.vue index 1c2789f154a..ad0d40faf32 100644 --- a/app/assets/javascripts/issue_show/components/locked_warning.vue +++ b/app/assets/javascripts/issue_show/components/locked_warning.vue @@ -2,7 +2,7 @@ export default { computed: { currentPath() { - return location.pathname; + return window.location.pathname; }, }, }; diff --git a/app/assets/javascripts/issue_show/components/title.vue b/app/assets/javascripts/issue_show/components/title.vue index aec890a2ff6..12101c0daa5 100644 --- a/app/assets/javascripts/issue_show/components/title.vue +++ b/app/assets/javascripts/issue_show/components/title.vue @@ -67,11 +67,11 @@ <template> <div class="title-container"> <h2 - class="title" :class="{ 'issue-realtime-pre-pulse': preAnimation, 'issue-realtime-trigger-pulse': pulseAnimation }" + class="title" v-html="titleHtml" > </h2> @@ -80,11 +80,11 @@ v-if="showInlineEditButton && canUpdate" type="button" class="btn btn-default btn-edit btn-svg js-issuable-edit" - v-html="pencilIcon" title="Edit title and description" data-placement="bottom" data-container="body" @click="edit" + v-html="pencilIcon" > </button> </div> diff --git a/app/assets/javascripts/job.js b/app/assets/javascripts/job.js index 611e8200b4d..fc13f467675 100644 --- a/app/assets/javascripts/job.js +++ b/app/assets/javascripts/job.js @@ -1,14 +1,17 @@ import $ from 'jquery'; import _ from 'underscore'; -import StickyFill from 'stickyfilljs'; +import { polyfillSticky } from './lib/utils/sticky'; import axios from './lib/utils/axios_utils'; import { visitUrl } from './lib/utils/url_utility'; import bp from './breakpoints'; import { numberToHumanSize } from './lib/utils/number_utils'; import { setCiStatusFavicon } from './lib/utils/common_utils'; +import { isScrolledToBottom, scrollDown } from './lib/utils/scroll_utils'; +import LogOutputBehaviours from './lib/utils/logoutput_behaviours'; -export default class Job { +export default class Job extends LogOutputBehaviours { constructor(options) { + super(); this.timeout = null; this.state = null; this.fetchingStatusFavicon = false; @@ -29,10 +32,6 @@ export default class Job { this.$buildTraceOutput = $('.js-build-output'); this.$topBar = $('.js-top-bar'); - // Scroll controllers - this.$scrollTopBtn = $('.js-scroll-up'); - this.$scrollBottomBtn = $('.js-scroll-down'); - clearTimeout(this.timeout); this.initSidebar(); @@ -48,23 +47,14 @@ export default class Job { .off('click', '.stage-item') .on('click', '.stage-item', this.updateDropdown); - // add event listeners to the scroll buttons - this.$scrollTopBtn - .off('click') - .on('click', this.scrollToTop.bind(this)); - - this.$scrollBottomBtn - .off('click') - .on('click', this.scrollToBottom.bind(this)); - this.scrollThrottled = _.throttle(this.toggleScroll.bind(this), 100); this.$window .off('scroll') .on('scroll', () => { - if (!this.isScrolledToBottom()) { + if (!isScrolledToBottom()) { this.toggleScrollAnimation(false); - } else if (this.isScrolledToBottom() && !this.isLogComplete) { + } else if (isScrolledToBottom() && !this.isLogComplete) { this.toggleScrollAnimation(true); } this.scrollThrottled(); @@ -80,70 +70,11 @@ export default class Job { } initAffixTopArea() { - /** - If the browser does not support position sticky, it returns the position as static. - If the browser does support sticky, then we allow the browser to handle it, if not - then we use a polyfill - */ - if (this.$topBar.css('position') !== 'static') return; - - StickyFill.add(this.$topBar); - } - - // eslint-disable-next-line class-methods-use-this - canScroll() { - return $(document).height() > $(window).height(); - } - - toggleScroll() { - const $document = $(document); - const currentPosition = $document.scrollTop(); - const scrollHeight = $document.height(); - - const windowHeight = $(window).height(); - if (this.canScroll()) { - if (currentPosition > 0 && - (scrollHeight - currentPosition !== windowHeight)) { - // User is in the middle of the log - - this.toggleDisableButton(this.$scrollTopBtn, false); - this.toggleDisableButton(this.$scrollBottomBtn, false); - } else if (currentPosition === 0) { - // User is at Top of Log - - this.toggleDisableButton(this.$scrollTopBtn, true); - this.toggleDisableButton(this.$scrollBottomBtn, false); - } else if (this.isScrolledToBottom()) { - // User is at the bottom of the build log. - - this.toggleDisableButton(this.$scrollTopBtn, false); - this.toggleDisableButton(this.$scrollBottomBtn, true); - } - } else { - this.toggleDisableButton(this.$scrollTopBtn, true); - this.toggleDisableButton(this.$scrollBottomBtn, true); - } - } - // eslint-disable-next-line class-methods-use-this - isScrolledToBottom() { - const $document = $(document); - - const currentPosition = $document.scrollTop(); - const scrollHeight = $document.height(); - - const windowHeight = $(window).height(); - - return scrollHeight - currentPosition === windowHeight; - } - - // eslint-disable-next-line class-methods-use-this - scrollDown() { - const $document = $(document); - $document.scrollTop($document.height()); + polyfillSticky(this.$topBar); } scrollToBottom() { - this.scrollDown(); + scrollDown(); this.hasBeenScrolled = true; this.toggleScroll(); } @@ -154,12 +85,6 @@ export default class Job { this.toggleScroll(); } - // eslint-disable-next-line class-methods-use-this - toggleDisableButton($button, disable) { - if (disable && $button.prop('disabled')) return; - $button.prop('disabled', disable); - } - toggleScrollAnimation(toggle) { this.$scrollBottomBtn.toggleClass('animate', toggle); } @@ -191,7 +116,7 @@ export default class Job { this.state = log.state; } - this.isScrollInBottom = this.isScrolledToBottom(); + this.isScrollInBottom = isScrolledToBottom(); if (log.append) { this.$buildTraceOutput.append(log.html); @@ -231,7 +156,7 @@ export default class Job { }) .then(() => { if (this.isScrollInBottom) { - this.scrollDown(); + scrollDown(); } }) .then(() => this.toggleScroll()); diff --git a/app/assets/javascripts/jobs/components/header.vue b/app/assets/javascripts/jobs/components/header.vue index c1044f4cd42..1e7f4b2c3f7 100644 --- a/app/assets/javascripts/jobs/components/header.vue +++ b/app/assets/javascripts/jobs/components/header.vue @@ -42,6 +42,9 @@ export default { jobStarted() { return !this.job.started === false; }, + headerTime() { + return this.jobStarted ? this.job.started : this.job.created_at; + }, }, watch: { job() { @@ -71,13 +74,13 @@ export default { <ci-header v-if="shouldRenderContent" :status="status" - item-name="Job" :item-id="job.id" - :time="job.created_at" + :time="headerTime" :user="job.user" :actions="actions" :has-sidebar-button="true" :should-render-triggered-label="jobStarted" + item-name="Job" /> <loading-icon v-if="isLoading" diff --git a/app/assets/javascripts/jobs/components/sidebar_details_block.vue b/app/assets/javascripts/jobs/components/sidebar_details_block.vue index 8f3c66b0cbe..d2adf628050 100644 --- a/app/assets/javascripts/jobs/components/sidebar_details_block.vue +++ b/app/assets/javascripts/jobs/components/sidebar_details_block.vue @@ -101,8 +101,8 @@ export default { {{ __('Retry') }} </a> <button - type="button" :aria-label="__('Toggle Sidebar')" + type="button" class="btn btn-blank gutter-toggle float-right d-block d-md-none js-sidebar-build-toggle" > <i @@ -114,20 +114,20 @@ export default { </div> <template v-if="shouldRenderContent"> <div - class="block retry-link" v-if="job.retry_path || job.new_issue_path" + class="block retry-link" > <a v-if="job.new_issue_path" - class="js-new-issue btn btn-new btn-inverted" :href="job.new_issue_path" + class="js-new-issue btn btn-new btn-inverted" > {{ __('New issue') }} </a> <a v-if="canUserRetry" - class="js-retry-job btn btn-inverted-secondary" :href="job.retry_path" + class="js-retry-job btn btn-inverted-secondary" data-method="post" rel="nofollow" > @@ -136,8 +136,8 @@ export default { </div> <div :class="{block : renderBlock }"> <p - class="build-detail-row js-job-mr" v-if="job.merge_request" + class="build-detail-row js-job-mr" > <span class="build-light-text"> {{ __('Merge Request:') }} @@ -148,51 +148,51 @@ export default { </p> <detail-row - class="js-job-duration" v-if="job.duration" - title="Duration" :value="duration" + class="js-job-duration" + title="Duration" /> <detail-row - class="js-job-finished" v-if="job.finished_at" - title="Finished" :value="timeFormated(job.finished_at)" + class="js-job-finished" + title="Finished" /> <detail-row - class="js-job-erased" v-if="job.erased_at" - title="Erased" :value="timeFormated(job.erased_at)" + class="js-job-erased" + title="Erased" /> <detail-row - class="js-job-queued" v-if="job.queued" - title="Queued" :value="queued" + class="js-job-queued" + title="Queued" /> <detail-row - class="js-job-timeout" v-if="hasTimeout" - title="Timeout" :help-url="runnerHelpUrl" :value="timeout" + class="js-job-timeout" + title="Timeout" /> <detail-row - class="js-job-runner" v-if="job.runner" - title="Runner" :value="runnerId" + class="js-job-runner" + title="Runner" /> <detail-row - class="js-job-coverage" v-if="job.coverage" - title="Coverage" :value="coverage" + class="js-job-coverage" + title="Coverage" /> <p - class="build-detail-row js-job-tags" v-if="job.tags.length" + class="build-detail-row js-job-tags" > <span class="build-light-text"> {{ __('Tags:') }} @@ -210,8 +210,8 @@ export default { class="btn-group prepend-top-5" role="group"> <a - class="js-cancel-job btn btn-sm btn-default" :href="job.cancel_path" + class="js-cancel-job btn btn-sm btn-default" data-method="post" rel="nofollow" > @@ -221,8 +221,8 @@ export default { </div> </template> <loading-icon - class="prepend-top-10" v-if="isLoading" + class="prepend-top-10" size="2" /> </div> diff --git a/app/assets/javascripts/label_manager.js b/app/assets/javascripts/label_manager.js index 8c3de6e4045..8b01024b7d4 100644 --- a/app/assets/javascripts/label_manager.js +++ b/app/assets/javascripts/label_manager.js @@ -13,6 +13,7 @@ export default class LabelManager { this.otherLabels = otherLabels || $('.js-other-labels'); this.errorMessage = 'Unable to update label prioritization at this time'; this.emptyState = document.querySelector('#js-priority-labels-empty-state'); + this.$badgeItemTemplate = $('#js-badge-item-template'); this.sortable = Sortable.create(this.prioritizedLabels.get(0), { filter: '.empty-message', forceFallback: true, @@ -63,7 +64,11 @@ export default class LabelManager { $target = this.otherLabels; $from = this.prioritizedLabels; } - $label.detach().appendTo($target); + + const $detachedLabel = $label.detach(); + this.toggleLabelPriorityBadge($detachedLabel, action); + $detachedLabel.appendTo($target); + if ($from.find('li').length) { $from.find('.empty-message').removeClass('hidden'); } @@ -88,6 +93,14 @@ export default class LabelManager { } } + toggleLabelPriorityBadge($label, action) { + if (action === 'remove') { + $('.js-priority-badge', $label).remove(); + } else { + $('.label-links', $label).append(this.$badgeItemTemplate.clone().html()); + } + } + onPrioritySortUpdate() { this.savePrioritySort() .catch(() => flash(this.errorMessage)); diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js index eafdaf4a672..7d0ff53f366 100644 --- a/app/assets/javascripts/labels_select.js +++ b/app/assets/javascripts/labels_select.js @@ -426,7 +426,7 @@ export default class LabelsSelect { const tpl = _.template([ '<% _.each(labels, function(label){ %>', '<a href="<%- issueUpdateURL.slice(0, issueUpdateURL.lastIndexOf("/")) %>?label_name[]=<%- encodeURIComponent(label.title) %>">', - '<span class="label has-tooltip color-label" title="<%- label.description %>" style="background-color: <%- label.color %>; color: <%- label.text_color %>;">', + '<span class="badge label has-tooltip color-label" title="<%- label.description %>" style="background-color: <%- label.color %>; color: <%- label.text_color %>;">', '<%- label.title %>', '</span>', '</a>', diff --git a/app/assets/javascripts/lazy_loader.js b/app/assets/javascripts/lazy_loader.js index dbbf1637a47..9482d131344 100644 --- a/app/assets/javascripts/lazy_loader.js +++ b/app/assets/javascripts/lazy_loader.js @@ -44,8 +44,8 @@ export default class LazyLoader { requestAnimationFrame(() => this.checkElementsInView()); } checkElementsInView() { - const scrollTop = pageYOffset; - const visHeight = scrollTop + innerHeight + SCROLL_THRESHOLD; + const scrollTop = window.pageYOffset; + const visHeight = scrollTop + window.innerHeight + SCROLL_THRESHOLD; // Loading Images which are in the current viewport or close to them this.lazyImages = this.lazyImages.filter((selectedImage) => { diff --git a/app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js b/app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js index 3873f4528ce..c28ed04f94f 100644 --- a/app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js +++ b/app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js @@ -93,7 +93,7 @@ export default class LinkedTabs { const newState = `${copySource}${this.currentLocation.search}${this.currentLocation.hash}`; - history.replaceState({ + window.history.replaceState({ url: newState, }, document.title, newState); return newState; diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js index 8b5445d012b..d0b0e5e1ba1 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js +++ b/app/assets/javascripts/lib/utils/common_utils.js @@ -197,7 +197,10 @@ export const insertText = (target, text) => { // eslint-disable-next-line no-param-reassign target.value = newText; // eslint-disable-next-line no-param-reassign - target.selectionStart = target.selectionEnd = selectionStart + insertedText.length; + target.selectionStart = selectionStart + insertedText.length; + + // eslint-disable-next-line no-param-reassign + target.selectionEnd = selectionStart + insertedText.length; // Trigger autosave target.dispatchEvent(new Event('input')); @@ -384,6 +387,49 @@ export const backOff = (fn, timeout = 60000) => { }); }; +export const createOverlayIcon = (iconPath, overlayPath) => { + const faviconImage = document.createElement('img'); + + return new Promise((resolve) => { + faviconImage.onload = () => { + const size = 32; + + const canvas = document.createElement('canvas'); + canvas.width = size; + canvas.height = size; + + const context = canvas.getContext('2d'); + context.clearRect(0, 0, size, size); + context.drawImage( + faviconImage, 0, 0, faviconImage.width, faviconImage.height, 0, 0, size, size, + ); + + const overlayImage = document.createElement('img'); + overlayImage.onload = () => { + context.drawImage( + overlayImage, 0, 0, overlayImage.width, overlayImage.height, 0, 0, size, size, + ); + + const faviconWithOverlayUrl = canvas.toDataURL(); + + resolve(faviconWithOverlayUrl); + }; + overlayImage.src = overlayPath; + }; + faviconImage.src = iconPath; + }); +}; + +export const setFaviconOverlay = (overlayPath) => { + const faviconEl = document.getElementById('favicon'); + + if (!faviconEl) { return null; } + + const iconPath = faviconEl.getAttribute('data-original-href'); + + return createOverlayIcon(iconPath, overlayPath).then(faviconWithOverlayUrl => faviconEl.setAttribute('href', faviconWithOverlayUrl)); +}; + export const setFavicon = (faviconPath) => { const faviconEl = document.getElementById('favicon'); if (faviconEl && faviconPath) { @@ -393,8 +439,9 @@ export const setFavicon = (faviconPath) => { export const resetFavicon = () => { const faviconEl = document.getElementById('favicon'); - const originalFavicon = faviconEl ? faviconEl.getAttribute('href') : null; + if (faviconEl) { + const originalFavicon = faviconEl.getAttribute('data-original-href'); faviconEl.setAttribute('href', originalFavicon); } }; @@ -403,10 +450,9 @@ export const setCiStatusFavicon = pageUrl => axios.get(pageUrl) .then(({ data }) => { if (data && data.favicon) { - setFavicon(data.favicon); - } else { - resetFavicon(); + return setFaviconOverlay(data.favicon); } + return resetFavicon(); }) .catch(resetFavicon); diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js index 0ff23bbb061..7cca32dc6fa 100644 --- a/app/assets/javascripts/lib/utils/datetime_utility.js +++ b/app/assets/javascripts/lib/utils/datetime_utility.js @@ -79,37 +79,37 @@ export function getTimeago() { if (!timeagoInstance) { const localeRemaining = function getLocaleRemaining(number, index) { return [ - [s__('Timeago|less than a minute ago'), s__('Timeago|right now')], - [s__('Timeago|less than a minute ago'), s__('Timeago|%s seconds remaining')], - [s__('Timeago|about a minute ago'), s__('Timeago|1 minute remaining')], + [s__('Timeago|just now'), s__('Timeago|right now')], + [s__('Timeago|%s seconds ago'), s__('Timeago|%s seconds remaining')], + [s__('Timeago|1 minute ago'), s__('Timeago|1 minute remaining')], [s__('Timeago|%s minutes ago'), s__('Timeago|%s minutes remaining')], - [s__('Timeago|about an hour ago'), s__('Timeago|1 hour remaining')], - [s__('Timeago|about %s hours ago'), s__('Timeago|%s hours remaining')], - [s__('Timeago|a day ago'), s__('Timeago|1 day remaining')], + [s__('Timeago|1 hour ago'), s__('Timeago|1 hour remaining')], + [s__('Timeago|%s hours ago'), s__('Timeago|%s hours remaining')], + [s__('Timeago|1 day ago'), s__('Timeago|1 day remaining')], [s__('Timeago|%s days ago'), s__('Timeago|%s days remaining')], - [s__('Timeago|a week ago'), s__('Timeago|1 week remaining')], + [s__('Timeago|1 week ago'), s__('Timeago|1 week remaining')], [s__('Timeago|%s weeks ago'), s__('Timeago|%s weeks remaining')], - [s__('Timeago|a month ago'), s__('Timeago|1 month remaining')], + [s__('Timeago|1 month ago'), s__('Timeago|1 month remaining')], [s__('Timeago|%s months ago'), s__('Timeago|%s months remaining')], - [s__('Timeago|a year ago'), s__('Timeago|1 year remaining')], + [s__('Timeago|1 year ago'), s__('Timeago|1 year remaining')], [s__('Timeago|%s years ago'), s__('Timeago|%s years remaining')], ][index]; }; const locale = function getLocale(number, index) { return [ - [s__('Timeago|less than a minute ago'), s__('Timeago|right now')], - [s__('Timeago|less than a minute ago'), s__('Timeago|in %s seconds')], - [s__('Timeago|about a minute ago'), s__('Timeago|in 1 minute')], + [s__('Timeago|just now'), s__('Timeago|right now')], + [s__('Timeago|%s seconds ago'), s__('Timeago|in %s seconds')], + [s__('Timeago|1 minute ago'), s__('Timeago|in 1 minute')], [s__('Timeago|%s minutes ago'), s__('Timeago|in %s minutes')], - [s__('Timeago|about an hour ago'), s__('Timeago|in 1 hour')], - [s__('Timeago|about %s hours ago'), s__('Timeago|in %s hours')], - [s__('Timeago|a day ago'), s__('Timeago|in 1 day')], + [s__('Timeago|1 hour ago'), s__('Timeago|in 1 hour')], + [s__('Timeago|%s hours ago'), s__('Timeago|in %s hours')], + [s__('Timeago|1 day ago'), s__('Timeago|in 1 day')], [s__('Timeago|%s days ago'), s__('Timeago|in %s days')], - [s__('Timeago|a week ago'), s__('Timeago|in 1 week')], + [s__('Timeago|1 week ago'), s__('Timeago|in 1 week')], [s__('Timeago|%s weeks ago'), s__('Timeago|in %s weeks')], - [s__('Timeago|a month ago'), s__('Timeago|in 1 month')], + [s__('Timeago|1 month ago'), s__('Timeago|in 1 month')], [s__('Timeago|%s months ago'), s__('Timeago|in %s months')], - [s__('Timeago|a year ago'), s__('Timeago|in 1 year')], + [s__('Timeago|1 year ago'), s__('Timeago|in 1 year')], [s__('Timeago|%s years ago'), s__('Timeago|in %s years')], ][index]; }; @@ -270,6 +270,17 @@ export const totalDaysInMonth = date => { }; /** + * Returns number of days in a quarter from provided + * months array. + * + * @param {Array} quarter + */ +export const totalDaysInQuarter = quarter => quarter.reduce( + (acc, month) => acc + totalDaysInMonth(month), + 0, +); + +/** * Returns list of Dates referring to Sundays of the month * based on provided date * @@ -309,42 +320,27 @@ export const getSundays = date => { }; /** - * Returns list of Dates representing a timeframe of Months from month of provided date (inclusive) - * up to provided length - * - * For eg; - * If current month is January 2018 and `length` provided is `6` - * Then this method will return list of Date objects as follows; - * - * [ October 2017, November 2017, December 2017, January 2018, February 2018, March 2018 ] - * - * If current month is March 2018 and `length` provided is `3` - * Then this method will return list of Date objects as follows; - * - * [ February 2018, March 2018, April 2018 ] + * Returns list of Dates representing a timeframe of months from startDate and length * + * @param {Date} startDate * @param {Number} length - * @param {Date} date */ -export const getTimeframeWindow = (length, date) => { - if (!length) { +export const getTimeframeWindowFrom = (startDate, length) => { + if (!(startDate instanceof Date) || !length) { return []; } - const currentDate = date instanceof Date ? date : new Date(); - const currentMonthIndex = Math.floor(length / 2); - const timeframe = []; - - // Move date object backward to the first month of timeframe - currentDate.setDate(1); - currentDate.setMonth(currentDate.getMonth() - currentMonthIndex); - - // Iterate and update date for the size of length + // Iterate and set date for the size of length // and push date reference to timeframe list - for (let i = 0; i < length; i += 1) { - timeframe.push(new Date(currentDate.getTime())); - currentDate.setMonth(currentDate.getMonth() + 1); - } + const timeframe = new Array(length) + .fill() + .map( + (val, i) => new Date( + startDate.getFullYear(), + startDate.getMonth() + i, + 1, + ), + ); // Change date of last timeframe item to last date of the month timeframe[length - 1].setDate(totalDaysInMonth(timeframe[length - 1])); @@ -352,6 +348,29 @@ export const getTimeframeWindow = (length, date) => { return timeframe; }; +/** + * Returns count of day within current quarter from provided date + * and array of months for the quarter + * + * Eg; + * If date is 15 Feb 2018 + * and quarter is [Jan, Feb, Mar] + * + * Then 15th Feb is 46th day of the quarter + * Where 31 (days in Jan) + 15 (date of Feb). + * + * @param {Date} date + * @param {Array} quarter + */ +export const dayInQuarter = (date, quarter) => quarter.reduce((acc, month) => { + if (date.getMonth() > month.getMonth()) { + return acc + totalDaysInMonth(month); + } else if (date.getMonth() === month.getMonth()) { + return acc + date.getDate(); + } + return acc + 0; +}, 0); + window.gl = window.gl || {}; window.gl.utils = { ...(window.gl.utils || {}), diff --git a/app/assets/javascripts/lib/utils/http_status.js b/app/assets/javascripts/lib/utils/http_status.js index bb151929431..229d53b18b0 100644 --- a/app/assets/javascripts/lib/utils/http_status.js +++ b/app/assets/javascripts/lib/utils/http_status.js @@ -8,4 +8,5 @@ export default { OK: 200, MULTIPLE_CHOICES: 300, BAD_REQUEST: 400, + NOT_FOUND: 404, }; diff --git a/app/assets/javascripts/lib/utils/logoutput_behaviours.js b/app/assets/javascripts/lib/utils/logoutput_behaviours.js new file mode 100644 index 00000000000..1bf99d935ef --- /dev/null +++ b/app/assets/javascripts/lib/utils/logoutput_behaviours.js @@ -0,0 +1,46 @@ +import $ from 'jquery'; +import { canScroll, isScrolledToBottom, toggleDisableButton } from './scroll_utils'; + +export default class LogOutputBehaviours { + constructor() { + // Scroll buttons + this.$scrollTopBtn = $('.js-scroll-up'); + this.$scrollBottomBtn = $('.js-scroll-down'); + + this.$scrollTopBtn.off('click').on('click', this.scrollToTop.bind(this)); + this.$scrollBottomBtn.off('click').on('click', this.scrollToBottom.bind(this)); + } + + toggleScroll() { + const $document = $(document); + const currentPosition = $document.scrollTop(); + const scrollHeight = $document.height(); + + const windowHeight = $(window).height(); + if (canScroll()) { + if (currentPosition > 0 && scrollHeight - currentPosition !== windowHeight) { + // User is in the middle of the log + + toggleDisableButton(this.$scrollTopBtn, false); + toggleDisableButton(this.$scrollBottomBtn, false); + } else if (currentPosition === 0) { + // User is at Top of Log + + toggleDisableButton(this.$scrollTopBtn, true); + toggleDisableButton(this.$scrollBottomBtn, false); + } else if (isScrolledToBottom()) { + // User is at the bottom of the build log. + + toggleDisableButton(this.$scrollTopBtn, false); + toggleDisableButton(this.$scrollBottomBtn, true); + } + } else { + toggleDisableButton(this.$scrollTopBtn, true); + toggleDisableButton(this.$scrollBottomBtn, true); + } + } + + toggleScrollAnimation(toggle) { + this.$scrollBottomBtn.toggleClass('animate', toggle); + } +} diff --git a/app/assets/javascripts/lib/utils/number_utils.js b/app/assets/javascripts/lib/utils/number_utils.js index a02c79b787e..f086d962221 100644 --- a/app/assets/javascripts/lib/utils/number_utils.js +++ b/app/assets/javascripts/lib/utils/number_utils.js @@ -12,7 +12,7 @@ export function formatRelevantDigits(number) { let digitsLeft = ''; let relevantDigits = 0; let formattedNumber = ''; - if (!isNaN(Number(number))) { + if (!Number.isNaN(Number(number))) { digitsLeft = number.toString().split('.')[0]; switch (digitsLeft.length) { case 1: diff --git a/app/assets/javascripts/lib/utils/scroll_utils.js b/app/assets/javascripts/lib/utils/scroll_utils.js new file mode 100644 index 00000000000..9313b570863 --- /dev/null +++ b/app/assets/javascripts/lib/utils/scroll_utils.js @@ -0,0 +1,29 @@ +import $ from 'jquery'; + +export const canScroll = () => $(document).height() > $(window).height(); + +/** + * Checks if the entire page is scrolled down all the way to the bottom + */ +export const isScrolledToBottom = () => { + const $document = $(document); + + const currentPosition = $document.scrollTop(); + const scrollHeight = $document.height(); + + const windowHeight = $(window).height(); + + return scrollHeight - currentPosition === windowHeight; +}; + +export const scrollDown = () => { + const $document = $(document); + $document.scrollTop($document.height()); +}; + +export const toggleDisableButton = ($button, disable) => { + if (disable && $button.prop('disabled')) return; + $button.prop('disabled', disable); +}; + +export default {}; diff --git a/app/assets/javascripts/lib/utils/sticky.js b/app/assets/javascripts/lib/utils/sticky.js index 098afcfa1b4..15a4dd62012 100644 --- a/app/assets/javascripts/lib/utils/sticky.js +++ b/app/assets/javascripts/lib/utils/sticky.js @@ -1,3 +1,5 @@ +import StickyFill from 'stickyfilljs'; + export const createPlaceholder = () => { const placeholder = document.createElement('div'); placeholder.classList.add('sticky-placeholder'); @@ -28,7 +30,16 @@ export const isSticky = (el, scrollY, stickyTop, insertPlaceholder) => { } }; -export default (el, stickyTop, insertPlaceholder = true) => { +/** + * Create a listener that will toggle a 'is-stuck' class, based on the current scroll position. + * + * - If the current environment does not support `position: sticky`, do nothing. + * + * @param {HTMLElement} el The `position: sticky` element. + * @param {Number} stickyTop Used to determine when an element is stuck. + * @param {Boolean} insertPlaceholder Should a placeholder element be created when element is stuck? + */ +export const stickyMonitor = (el, stickyTop, insertPlaceholder = true) => { if (!el) return; if (typeof CSS === 'undefined' || !(CSS.supports('(position: -webkit-sticky) or (position: sticky)'))) return; @@ -37,3 +48,13 @@ export default (el, stickyTop, insertPlaceholder = true) => { passive: true, }); }; + +/** + * Polyfill the `position: sticky` behavior. + * + * - If the current environment supports `position: sticky`, do nothing. + * - Can receive an iterable element list (NodeList, jQuery collection, etc.) or single HTMLElement. + */ +export const polyfillSticky = (el) => { + StickyFill.add(el); +}; diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js index dd17544b656..72b72f4247d 100644 --- a/app/assets/javascripts/lib/utils/url_utility.js +++ b/app/assets/javascripts/lib/utils/url_utility.js @@ -85,9 +85,9 @@ export function redirectTo(url) { } export function webIDEUrl(route = undefined) { - let returnUrl = `${gon.relative_url_root}/-/ide/`; + let returnUrl = `${gon.relative_url_root || ''}/-/ide/`; if (route) { - returnUrl += `project${route}`; + returnUrl += `project${route.replace(new RegExp(`^${gon.relative_url_root || ''}`), '')}`; } return returnUrl; } diff --git a/app/assets/javascripts/line_highlighter.js b/app/assets/javascripts/line_highlighter.js index f2323f57455..303c5d8a894 100644 --- a/app/assets/javascripts/line_highlighter.js +++ b/app/assets/javascripts/line_highlighter.js @@ -35,7 +35,7 @@ const LineHighlighter = function(options = {}) { options.highlightLineClass = options.highlightLineClass || 'hll'; options.fileHolderSelector = options.fileHolderSelector || '.file-holder'; options.scrollFileHolder = options.scrollFileHolder || false; - options.hash = options.hash || location.hash; + options.hash = options.hash || window.location.hash; this.options = options; this._hash = options.hash; @@ -145,6 +145,8 @@ LineHighlighter.prototype.highlightRange = function(range) { var i, lineNumber, ref, ref1, results; if (range[1]) { results = []; + + // eslint-disable-next-line no-multi-assign for (lineNumber = i = ref = range[0], ref1 = range[1]; ref <= ref1 ? i <= ref1 : i >= ref1; lineNumber = ref <= ref1 ? (i += 1) : (i -= 1)) { results.push(this.highlightLine(lineNumber)); } @@ -170,7 +172,7 @@ LineHighlighter.prototype.setHash = function(firstLineNumber, lastLineNumber) { // // This method is stubbed in tests. LineHighlighter.prototype.__setLocationHash__ = function(value) { - return history.pushState({ + return window.history.pushState({ url: value // We're using pushState instead of assigning location.hash directly to // prevent the page from scrolling on the hashchange event diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index 9803bebfd10..c9ce838cd48 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -144,6 +144,7 @@ document.addEventListener('DOMContentLoaded', () => { $body.tooltip({ selector: '.has-tooltip, [data-toggle="tooltip"]', trigger: 'hover', + boundary: 'viewport', placement(tip, el) { return $(el).data('placement') || 'bottom'; }, diff --git a/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js b/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js index 2cb238529aa..e4ed8111824 100644 --- a/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js +++ b/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js @@ -11,9 +11,18 @@ import { __ } from '~/locale'; global.mergeConflicts.diffFileEditor = Vue.extend({ props: { - file: Object, - onCancelDiscardConfirmation: Function, - onAcceptDiscardConfirmation: Function + file: { + type: Object, + required: true, + }, + onCancelDiscardConfirmation: { + type: Function, + required: true, + }, + onAcceptDiscardConfirmation: { + type: Function, + required: true, + }, }, data() { return { diff --git a/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js b/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js index 56d6678e1bd..827cf5f478d 100644 --- a/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js +++ b/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js @@ -1,14 +1,19 @@ -/* eslint-disable no-param-reassign, comma-dangle */ +/* eslint-disable no-param-reassign */ import Vue from 'vue'; +import actionsMixin from '../mixins/line_conflict_actions'; +import utilsMixin from '../mixins/line_conflict_utils'; -((global) => { +(global => { global.mergeConflicts = global.mergeConflicts || {}; global.mergeConflicts.inlineConflictLines = Vue.extend({ + mixins: [utilsMixin, actionsMixin], props: { - file: Object + file: { + type: Object, + required: true, + }, }, - mixins: [global.mergeConflicts.utils, global.mergeConflicts.actions], }); })(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js b/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js index 0fc4a13450a..57e73e38d88 100644 --- a/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js +++ b/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js @@ -1,15 +1,20 @@ /* eslint-disable no-param-reassign, comma-dangle */ import Vue from 'vue'; +import actionsMixin from '../mixins/line_conflict_actions'; +import utilsMixin from '../mixins/line_conflict_utils'; ((global) => { global.mergeConflicts = global.mergeConflicts || {}; global.mergeConflicts.parallelConflictLines = Vue.extend({ + mixins: [utilsMixin, actionsMixin], props: { - file: Object + file: { + type: Object, + required: true, + }, }, - mixins: [global.mergeConflicts.utils, global.mergeConflicts.actions], template: ` <table> <tr class="line_holder parallel" v-for="section in file.parallelLines"> diff --git a/app/assets/javascripts/merge_conflicts/merge_conflict_service.js b/app/assets/javascripts/merge_conflicts/merge_conflict_service.js index c68b47c9348..64d69159222 100644 --- a/app/assets/javascripts/merge_conflicts/merge_conflict_service.js +++ b/app/assets/javascripts/merge_conflicts/merge_conflict_service.js @@ -1,23 +1,16 @@ -/* eslint-disable no-param-reassign, comma-dangle */ import axios from '../lib/utils/axios_utils'; -((global) => { - global.mergeConflicts = global.mergeConflicts || {}; - - class mergeConflictsService { - constructor(options) { - this.conflictsPath = options.conflictsPath; - this.resolveConflictsPath = options.resolveConflictsPath; - } - - fetchConflictsData() { - return axios.get(this.conflictsPath); - } +export default class MergeConflictsService { + constructor(options) { + this.conflictsPath = options.conflictsPath; + this.resolveConflictsPath = options.resolveConflictsPath; + } - submitResolveConflicts(data) { - return axios.post(this.resolveConflictsPath, data); - } + fetchConflictsData() { + return axios.get(this.conflictsPath); } - global.mergeConflicts.mergeConflictsService = mergeConflictsService; -})(window.gl || (window.gl = {})); + submitResolveConflicts(data) { + return axios.post(this.resolveConflictsPath, data); + } +} diff --git a/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js index 4abd5433bb5..491858c3602 100644 --- a/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js +++ b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js @@ -1,13 +1,9 @@ -/* eslint-disable new-cap, comma-dangle, no-new */ - import $ from 'jquery'; import Vue from 'vue'; -import Flash from '../flash'; +import createFlash from '../flash'; import initIssuableSidebar from '../init_issuable_sidebar'; import './merge_conflict_store'; -import './merge_conflict_service'; -import './mixins/line_conflict_utils'; -import './mixins/line_conflict_actions'; +import MergeConflictsService from './merge_conflict_service'; import './components/diff_file_editor'; import './components/inline_conflict_lines'; import './components/parallel_conflict_lines'; @@ -17,9 +13,9 @@ export default function initMergeConflicts() { const INTERACTIVE_RESOLVE_MODE = 'interactive'; const conflictsEl = document.querySelector('#conflicts'); const mergeConflictsStore = gl.mergeConflicts.mergeConflictsStore; - const mergeConflictsService = new gl.mergeConflicts.mergeConflictsService({ + const mergeConflictsService = new MergeConflictsService({ conflictsPath: conflictsEl.dataset.conflictsPath, - resolveConflictsPath: conflictsEl.dataset.resolveConflictsPath + resolveConflictsPath: conflictsEl.dataset.resolveConflictsPath, }); initIssuableSidebar(); @@ -29,17 +25,26 @@ export default function initMergeConflicts() { components: { 'diff-file-editor': gl.mergeConflicts.diffFileEditor, 'inline-conflict-lines': gl.mergeConflicts.inlineConflictLines, - 'parallel-conflict-lines': gl.mergeConflicts.parallelConflictLines + 'parallel-conflict-lines': gl.mergeConflicts.parallelConflictLines, }, data: mergeConflictsStore.state, computed: { - conflictsCountText() { return mergeConflictsStore.getConflictsCountText(); }, - readyToCommit() { return mergeConflictsStore.isReadyToCommit(); }, - commitButtonText() { return mergeConflictsStore.getCommitButtonText(); }, - showDiffViewTypeSwitcher() { return mergeConflictsStore.fileTextTypePresent(); } + conflictsCountText() { + return mergeConflictsStore.getConflictsCountText(); + }, + readyToCommit() { + return mergeConflictsStore.isReadyToCommit(); + }, + commitButtonText() { + return mergeConflictsStore.getCommitButtonText(); + }, + showDiffViewTypeSwitcher() { + return mergeConflictsStore.fileTextTypePresent(); + }, }, created() { - mergeConflictsService.fetchConflictsData() + mergeConflictsService + .fetchConflictsData() .then(({ data }) => { if (data.type === 'error') { mergeConflictsStore.setFailedRequest(data.message); @@ -87,9 +92,9 @@ export default function initMergeConflicts() { }) .catch(() => { mergeConflictsStore.setSubmitState(false); - new Flash('Failed to save merge conflicts resolutions. Please try again!'); + createFlash('Failed to save merge conflicts resolutions. Please try again!'); }); - } - } + }, + }, }); } diff --git a/app/assets/javascripts/merge_conflicts/mixins/line_conflict_actions.js b/app/assets/javascripts/merge_conflicts/mixins/line_conflict_actions.js index 53e000d7e9e..364ae2b2688 100644 --- a/app/assets/javascripts/merge_conflicts/mixins/line_conflict_actions.js +++ b/app/assets/javascripts/merge_conflicts/mixins/line_conflict_actions.js @@ -1,13 +1,7 @@ -/* eslint-disable no-param-reassign, comma-dangle */ - -((global) => { - global.mergeConflicts = global.mergeConflicts || {}; - - global.mergeConflicts.actions = { - methods: { - handleSelected(file, sectionId, selection) { - gl.mergeConflicts.mergeConflictsStore.handleSelected(file, sectionId, selection); - } - } - }; -})(window.gl || (window.gl = {})); +export default { + methods: { + handleSelected(file, sectionId, selection) { + gl.mergeConflicts.mergeConflictsStore.handleSelected(file, sectionId, selection); + }, + }, +}; diff --git a/app/assets/javascripts/merge_conflicts/mixins/line_conflict_utils.js b/app/assets/javascripts/merge_conflicts/mixins/line_conflict_utils.js index 0f475f62ee6..d25032fb142 100644 --- a/app/assets/javascripts/merge_conflicts/mixins/line_conflict_utils.js +++ b/app/assets/javascripts/merge_conflicts/mixins/line_conflict_utils.js @@ -1,19 +1,13 @@ -/* eslint-disable no-param-reassign, quote-props, comma-dangle */ - -((global) => { - global.mergeConflicts = global.mergeConflicts || {}; - - global.mergeConflicts.utils = { - methods: { - lineCssClass(line) { - return { - 'head': line.isHead, - 'origin': line.isOrigin, - 'match': line.hasMatch, - 'selected': line.isSelected, - 'unselected': line.isUnselected - }; - } - } - }; -})(window.gl || (window.gl = {})); +export default { + methods: { + lineCssClass(line) { + return { + head: line.isHead, + origin: line.isOrigin, + match: line.hasMatch, + selected: line.isSelected, + unselected: line.isUnselected, + }; + }, + }, +}; diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js index 493c119dc6f..65ab41559be 100644 --- a/app/assets/javascripts/merge_request_tabs.js +++ b/app/assets/javascripts/merge_request_tabs.js @@ -362,7 +362,7 @@ export default class MergeRequestTabs { // // status - Boolean, true to show, false to hide toggleLoading(status) { - $('.mr-loading-status .loading').toggleClass('hidden', !status); + $('.mr-loading-status .loading').toggleClass('hide', !status); } diffViewType() { diff --git a/app/assets/javascripts/milestone.js b/app/assets/javascripts/milestone.js index 325fa570f37..6da04020881 100644 --- a/app/assets/javascripts/milestone.js +++ b/app/assets/javascripts/milestone.js @@ -18,13 +18,13 @@ export default class Milestone { return $('a[data-toggle="tab"]').on('show.bs.tab', (e) => { const $target = $(e.target); - location.hash = $target.attr('href'); + window.location.hash = $target.attr('href'); this.loadTab($target); }); } // eslint-disable-next-line class-methods-use-this loadInitialTab() { - const $target = $(`.js-milestone-tabs a[href="${location.hash}"]`); + const $target = $(`.js-milestone-tabs a[href="${window.location.hash}"]`); if ($target.length) { $target.tab('show'); diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js index f8b3d3061f0..334279137d8 100644 --- a/app/assets/javascripts/milestone_select.js +++ b/app/assets/javascripts/milestone_select.js @@ -16,10 +16,10 @@ export default class MilestoneSelect { typeof currentProject === 'string' ? JSON.parse(currentProject) : currentProject; } - this.init(els, options); + MilestoneSelect.init(els, options); } - init(els, options) { + static init(els, options) { let $els = $(els); if (!els) { @@ -56,7 +56,7 @@ export default class MilestoneSelect { if (issueUpdateURL) { milestoneLinkTemplate = _.template( - '<a href="/<%- full_path %>/milestones/<%- iid %>" class="bold has-tooltip" data-container="body" title="<%- remaining %>"><%- title %></a>', + '<a href="<%- web_url %>" class="bold has-tooltip" data-container="body" title="<%- remaining %>"><%- title %></a>', ); milestoneLinkNoneTemplate = '<span class="no-value">None</span>'; } @@ -224,7 +224,6 @@ export default class MilestoneSelect { $selectBox.hide(); $value.css('display', ''); if (data.milestone != null) { - data.milestone.full_path = this.currentProject.full_path; data.milestone.remaining = timeFor(data.milestone.due_date); data.milestone.name = data.milestone.title; $value.html(milestoneLinkTemplate(data.milestone)); diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue index f5572be5fbf..e1c8b6a6d4a 100644 --- a/app/assets/javascripts/monitoring/components/dashboard.vue +++ b/app/assets/javascripts/monitoring/components/dashboard.vue @@ -139,7 +139,7 @@ export default { this.updateAspectRatio = true; }, toggleAspectRatio() { - this.updatedAspectRatios = this.updatedAspectRatios += 1; + this.updatedAspectRatios += 1; if (this.store.getMetricsCount() === this.updatedAspectRatios) { this.updateAspectRatio = !this.updateAspectRatio; this.updatedAspectRatios = 0; @@ -174,7 +174,10 @@ export default { :tags-path="tagsPath" :show-legend="showLegend" :small-graph="forceSmallGraph" - /> + > + <!-- EE content --> + {{ null }} + </graph> </graph-group> </div> <empty-state diff --git a/app/assets/javascripts/monitoring/components/empty_state.vue b/app/assets/javascripts/monitoring/components/empty_state.vue index c77f451c2d3..82b9a4b1adb 100644 --- a/app/assets/javascripts/monitoring/components/empty_state.vue +++ b/app/assets/javascripts/monitoring/components/empty_state.vue @@ -107,8 +107,8 @@ export default { <div class="state-button"> <a v-if="currentState.buttonPath" - class="btn btn-success" :href="currentState.buttonPath" + class="btn btn-success" > {{ currentState.buttonText }} </a> @@ -116,8 +116,8 @@ export default { <div class="state-button"> <a v-if="currentState.secondaryButtonPath" - class="btn" :href="currentState.secondaryButtonPath" + class="btn" > {{ currentState.secondaryButtonText }} </a> diff --git a/app/assets/javascripts/monitoring/components/graph.vue b/app/assets/javascripts/monitoring/components/graph.vue index de6755e0414..e5680a0499f 100644 --- a/app/assets/javascripts/monitoring/components/graph.vue +++ b/app/assets/javascripts/monitoring/components/graph.vue @@ -154,7 +154,7 @@ export default { point.x = e.clientX; point.y = e.clientY; point = point.matrixTransform(this.$refs.graphData.getScreenCTM().inverse()); - point.x = point.x += 7; + point.x += 7; const firstTimeSeries = this.timeSeries[0]; const timeValueOverlay = firstTimeSeries.timeSeriesScaleX.invert(point.x); const overlayIndex = bisectDate(firstTimeSeries.values, timeValueOverlay, 1); @@ -232,20 +232,25 @@ export default { @mouseover="showFlagContent = true" @mouseleave="showFlagContent = false" > - <h5 class="text-center graph-title"> - {{ graphData.title }} - </h5> + <div class="prometheus-graph-header"> + <h5 class="prometheus-graph-title"> + {{ graphData.title }} + </h5> + <div class="prometheus-graph-widgets"> + <slot></slot> + </div> + </div> <div - class="prometheus-svg-container" :style="paddingBottomRootSvg" + class="prometheus-svg-container" > <svg - :viewBox="outerViewBox" ref="baseSvg" + :viewBox="outerViewBox" > <g - class="x-axis" :transform="axisTransform" + class="x-axis" /> <g class="y-axis" @@ -260,9 +265,9 @@ export default { :unit-of-display="unitOfDisplay" /> <svg - class="graph-data" - :viewBox="innerViewBox" ref="graphData" + :viewBox="innerViewBox" + class="graph-data" > <graph-path v-for="(path, index) in timeSeries" @@ -282,11 +287,11 @@ export default { :graph-height-offset="graphHeightOffset" /> <rect - class="prometheus-graph-overlay" + ref="graphOverlay" :width="(graphWidth - 70)" :height="(graphHeight - 100)" + class="prometheus-graph-overlay" transform="translate(-5, 20)" - ref="graphOverlay" @mousemove="handleMouseOverGraph($event)" /> </svg> diff --git a/app/assets/javascripts/monitoring/components/graph/axis.vue b/app/assets/javascripts/monitoring/components/graph/axis.vue index fc4b3689dfd..8a604a51eb2 100644 --- a/app/assets/javascripts/monitoring/components/graph/axis.vue +++ b/app/assets/javascripts/monitoring/components/graph/axis.vue @@ -92,48 +92,48 @@ export default { <template> <g class="axis-label-container"> <line + :y1="yPosition" + :x2="graphWidth + 20" + :y2="yPosition" class="label-x-axis-line" stroke="#000000" stroke-width="1" x1="10" - :y1="yPosition" - :x2="graphWidth + 20" - :y2="yPosition" /> <line + :x2="10" + :y2="yPosition" class="label-y-axis-line" stroke="#000000" stroke-width="1" x1="10" y1="0" - :x2="10" - :y2="yPosition" /> <rect - class="rect-axis-text" :transform="rectTransform" :width="yLabelWidth" :height="yLabelHeight" + class="rect-axis-text" /> <text + ref="ylabel" + :transform="textTransform" class="label-axis-text y-label-text" text-anchor="middle" - :transform="textTransform" - ref="ylabel" > {{ yAxisLabelSentenceCase }} </text> <rect - class="rect-axis-text" :x="xPosition + 60" :y="graphHeight - 80" + class="rect-axis-text" width="35" height="50" /> <text - class="label-axis-text x-label-text" :x="xPosition + 60" :y="yPosition" + class="label-axis-text x-label-text" dy=".35em" > {{ timeString }} diff --git a/app/assets/javascripts/monitoring/components/graph/deployment.vue b/app/assets/javascripts/monitoring/components/graph/deployment.vue index 4012191ceb9..a7289ed53e8 100644 --- a/app/assets/javascripts/monitoring/components/graph/deployment.vue +++ b/app/assets/javascripts/monitoring/components/graph/deployment.vue @@ -33,18 +33,18 @@ export default { :key="index" :transform="transformDeploymentGroup(deployment)"> <rect + :height="calculatedHeight" x="0" y="0" - :height="calculatedHeight" width="3" fill="url(#shadow-gradient)" /> <line + :y2="calculatedHeight" class="deployment-line" x1="0" y1="0" x2="0" - :y2="calculatedHeight" stroke="#000" /> </g> diff --git a/app/assets/javascripts/monitoring/components/graph/flag.vue b/app/assets/javascripts/monitoring/components/graph/flag.vue index 8a771107de8..92fe98508ad 100644 --- a/app/assets/javascripts/monitoring/components/graph/flag.vue +++ b/app/assets/javascripts/monitoring/components/graph/flag.vue @@ -97,7 +97,7 @@ export default { ? this.deploymentFlagData.seriesIndex : indexFromCoordinates; const value = series.values[index] && series.values[index].value; - if (isNaN(value)) { + if (Number.isNaN(value)) { return '-'; } return `${formatRelevantDigits(value)}${this.unitOfDisplay}`; @@ -117,13 +117,13 @@ export default { <template> <div - class="prometheus-graph-cursor" :style="cursorStyle" + class="prometheus-graph-cursor" > <div v-if="showFlagContent" - class="prometheus-graph-flag popover" :class="flagOrientation" + class="prometheus-graph-flag popover" > <div class="arrow"></div> <div class="popover-title"> @@ -139,8 +139,8 @@ export default { > <div> <icon - name="commit" :size="12" + name="commit" /> <a :href="deploymentFlagData.commitUrl"> {{ deploymentFlagData.sha.slice(0, 8) }} @@ -150,8 +150,8 @@ export default { v-if="deploymentFlagData.tag" > <icon - name="label" :size="12" + name="label" /> <a :href="deploymentFlagData.tagUrl"> {{ deploymentFlagData.ref }} diff --git a/app/assets/javascripts/monitoring/components/graph/legend.vue b/app/assets/javascripts/monitoring/components/graph/legend.vue index da9280cf1f1..3276f3a1ceb 100644 --- a/app/assets/javascripts/monitoring/components/graph/legend.vue +++ b/app/assets/javascripts/monitoring/components/graph/legend.vue @@ -31,8 +31,8 @@ export default { <table class="prometheus-table"> <tr v-for="(series, index) in timeSeries" - :key="index" v-if="series.shouldRenderLegend" + :key="index" :class="isStable(series)" > <td> @@ -40,11 +40,11 @@ export default { </td> <track-line :track="series" /> <td - class="legend-metric-title" - v-if="timeSeries.length > 1"> + v-if="timeSeries.length > 1" + class="legend-metric-title"> <track-info - :track="series" - v-if="series.metricTag" /> + v-if="series.metricTag" + :track="series" /> <track-info v-else :track="series"> @@ -62,8 +62,8 @@ export default { :key="`track-line-${trackIndex}`"/> <td :key="`track-info-${trackIndex}`"> <track-info - class="legend-metric-title" - :track="track" /> + :track="track" + class="legend-metric-title" /> </td> </template> </tr> diff --git a/app/assets/javascripts/monitoring/components/graph/path.vue b/app/assets/javascripts/monitoring/components/graph/path.vue index 52f8aa2ee3f..a9b7ce586ce 100644 --- a/app/assets/javascripts/monitoring/components/graph/path.vue +++ b/app/assets/javascripts/monitoring/components/graph/path.vue @@ -44,26 +44,26 @@ export default { <template> <g transform="translate(-5, 20)"> <circle - class="circle-path" + v-if="showDot" :cx="currentCoordinates.currentX" :cy="currentCoordinates.currentY" :fill="lineColor" :stroke="lineColor" + class="circle-path" r="3" - v-if="showDot" /> <path - class="metric-area" :d="generatedAreaPath" :fill="areaColor" + class="metric-area" /> <path - class="metric-line" :d="generatedLinePath" :stroke="lineColor" + :stroke-dasharray="strokeDashArray" + class="metric-line" fill="none" stroke-width="1" - :stroke-dasharray="strokeDashArray" /> </g> </template> diff --git a/app/assets/javascripts/monitoring/components/graph/track_line.vue b/app/assets/javascripts/monitoring/components/graph/track_line.vue index 18be65fd1ef..ba3f93b39ff 100644 --- a/app/assets/javascripts/monitoring/components/graph/track_line.vue +++ b/app/assets/javascripts/monitoring/components/graph/track_line.vue @@ -24,11 +24,11 @@ export default { <line :stroke-dasharray="stylizedLine" :stroke="track.lineColor" - stroke-width="4" :x1="0" :x2="16" :y1="4" :y2="4" + stroke-width="4" /> </svg> </td> diff --git a/app/assets/javascripts/monitoring/utils/multiple_time_series.js b/app/assets/javascripts/monitoring/utils/multiple_time_series.js index 4d3f1f1a7cc..ed3a27dd68b 100644 --- a/app/assets/javascripts/monitoring/utils/multiple_time_series.js +++ b/app/assets/javascripts/monitoring/utils/multiple_time_series.js @@ -73,7 +73,7 @@ function queryTimeSeries(query, graphWidth, graphHeight, graphHeightOffset, xDom timeSeriesScaleX.ticks(d3.timeMinute, 60); timeSeriesScaleY.domain(yDom); - const defined = d => !isNaN(d.value) && d.value != null; + const defined = d => !Number.isNaN(d.value) && d.value != null; const lineFunction = d3 .line() diff --git a/app/assets/javascripts/network/branch_graph.js b/app/assets/javascripts/network/branch_graph.js index bd007c707f2..e4096ddb00d 100644 --- a/app/assets/javascripts/network/branch_graph.js +++ b/app/assets/javascripts/network/branch_graph.js @@ -112,6 +112,8 @@ export default (function() { fill: "#444" }); ref = this.days; + + // eslint-disable-next-line no-multi-assign for (mm = j = 0, len = ref.length; j < len; mm = (j += 1)) { day = ref[mm]; if (cuday !== day[0] || cumonth !== day[1]) { @@ -285,6 +287,8 @@ export default (function() { r = this.r; ref = commit.parents; results = []; + + // eslint-disable-next-line no-multi-assign for (i = j = 0, len = ref.length; j < len; i = (j += 1)) { parent = ref[i]; parentCommit = this.preparedCommits[parent[0]]; diff --git a/app/assets/javascripts/notebook/cells/code.vue b/app/assets/javascripts/notebook/cells/code.vue index b4067d229aa..18cef82cec0 100644 --- a/app/assets/javascripts/notebook/cells/code.vue +++ b/app/assets/javascripts/notebook/cells/code.vue @@ -39,10 +39,10 @@ export default { <template> <div class="cell"> <code-cell - type="input" :raw-code="rawInputCode" :count="cell.execution_count" - :code-css-class="codeCssClass" /> + :code-css-class="codeCssClass" + type="input" /> <output-cell v-if="hasOutput" :count="cell.execution_count" diff --git a/app/assets/javascripts/notebook/cells/code/index.vue b/app/assets/javascripts/notebook/cells/code/index.vue index 0f3083f05b2..7d2a1a33b98 100644 --- a/app/assets/javascripts/notebook/cells/code/index.vue +++ b/app/assets/javascripts/notebook/cells/code/index.vue @@ -48,9 +48,9 @@ :type="promptType" :count="count" /> <pre - class="language-python" - :class="codeCssClass" ref="code" + :class="codeCssClass" + class="language-python" v-text="code"> </pre> </div> diff --git a/app/assets/javascripts/notebook/cells/output/index.vue b/app/assets/javascripts/notebook/cells/output/index.vue index 91b2269a83a..4183b976814 100644 --- a/app/assets/javascripts/notebook/cells/output/index.vue +++ b/app/assets/javascripts/notebook/cells/output/index.vue @@ -78,10 +78,10 @@ <template> <component :is="componentName" - type="output" :output-type="outputType" :count="count" :raw-code="rawCode" :code-css-class="codeCssClass" + type="output" /> </template> diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js index b2c1a26bbae..27c5dedcf0b 100644 --- a/app/assets/javascripts/notes.js +++ b/app/assets/javascripts/notes.js @@ -315,7 +315,7 @@ export default class Notes { if (discussionNoteForm.length) { if ($textarea.val() !== '') { if ( - !confirm('Are you sure you want to cancel creating this comment?') + !window.confirm('Are you sure you want to cancel creating this comment?') ) { return; } @@ -329,7 +329,7 @@ export default class Notes { newText = $textarea.val(); if (originalText !== newText) { if ( - !confirm('Are you sure you want to cancel editing this comment?') + !window.confirm('Are you sure you want to cancel editing this comment?') ) { return; } @@ -1675,7 +1675,7 @@ export default class Notes { <div class="note-header"> <div class="note-header-info"> <a href="/${_.escape(currentUsername)}"> - <span class="d-none d-sm-block">${_.escape( + <span class="d-none d-sm-inline-block">${_.escape( currentUsername, )}</span> <span class="note-headline-light">${_.escape( @@ -1694,7 +1694,7 @@ export default class Notes { </li>`, ); - $tempNote.find('.d-none.d-sm-block').text(_.escape(currentUserFullname)); + $tempNote.find('.d-none.d-sm-inline-block').text(_.escape(currentUserFullname)); $tempNote .find('.note-headline-light') .text(`@${_.escape(currentUsername)}`); diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue index 72d7e22fba0..ad6dd3d9a09 100644 --- a/app/assets/javascripts/notes/components/comment_form.vue +++ b/app/assets/javascripts/notes/components/comment_form.vue @@ -312,8 +312,8 @@ Please check your network connection and try again.`; <div> <note-signed-out-widget v-if="!isLoggedIn" /> <discussion-locked-widget - issuable-type="issue" v-else-if="isLocked(getNoteableData) && !canCreateNote" + issuable-type="issue" /> <ul v-else-if="canCreateNote" @@ -345,23 +345,23 @@ Please check your network connection and try again.`; /> <markdown-field + ref="markdownField" :markdown-preview-path="markdownPreviewPath" :markdown-docs-path="markdownDocsPath" :quick-actions-docs-path="quickActionsDocsPath" - :add-spacing-classes="false" - ref="markdownField"> + :add-spacing-classes="false"> <textarea id="note-body" + ref="textarea" + slot="textarea" + v-model="note" + :disabled="isSubmitting" name="note[note]" class="note-textarea js-vue-comment-form js-gfm-input js-autosize markdown-area js-vue-textarea" data-supports-quick-actions="true" aria-label="Description" - v-model="note" - ref="textarea" - slot="textarea" - :disabled="isSubmitting" - placeholder="Write a comment or drag your files here..." + placeholder="Write a comment or drag your files here…" @keydown.up="editCurrentUserLastNote()" @keydown.meta.enter="handleSave()" @keydown.ctrl.enter="handleSave()"> @@ -372,10 +372,10 @@ js-gfm-input js-autosize markdown-area js-vue-textarea" class="float-left btn-group append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown"> <button - @click.prevent="handleSave()" :disabled="isSubmitButtonDisabled" class="btn btn-create comment-btn js-comment-button js-comment-submit-button" - type="submit"> + type="submit" + @click.prevent="handleSave()"> {{ __(commentButtonTitle) }} </button> <button @@ -434,20 +434,20 @@ append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown" <loading-button v-if="canUpdateIssue" :loading="isToggleStateButtonLoading" - @click="handleSave(true)" :container-class="[ actionButtonClassNames, 'btn btn-comment btn-comment-and-close js-action-button' ]" :disabled="isToggleStateButtonLoading || isSubmitting" :label="issueActionButtonTitle" + @click="handleSave(true)" /> <button - type="button" v-if="note.length" - @click="discard" - class="btn btn-cancel js-note-discard"> + type="button" + class="btn btn-cancel js-note-discard" + @click="discard"> Discard draft </button> </div> diff --git a/app/assets/javascripts/notes/components/diff_file_header.vue b/app/assets/javascripts/notes/components/diff_file_header.vue index 94d9dc69964..fc7b52be241 100644 --- a/app/assets/javascripts/notes/components/diff_file_header.vue +++ b/app/assets/javascripts/notes/components/diff_file_header.vue @@ -29,12 +29,12 @@ export default { <span> <icon name="archive" /> <strong - v-html="diffFile.submoduleLink" class="file-title-name" + v-html="diffFile.submoduleLink" ></strong> <clipboard-button - title="Copy file path to clipboard" :text="diffFile.submoduleLink" + title="Copy file path to clipboard" css-class="btn-default btn-transparent btn-clipboard" /> </span> @@ -48,16 +48,16 @@ export default { <span v-html="diffFile.blobIcon"></span> <span v-if="diffFile.renamedFile"> <strong - class="file-title-name has-tooltip" :title="diffFile.oldPath" + class="file-title-name has-tooltip" data-container="body" > {{ diffFile.oldPath }} </strong> → <strong - class="file-title-name has-tooltip" :title="diffFile.newPath" + class="file-title-name has-tooltip" data-container="body" > {{ diffFile.newPath }} @@ -66,8 +66,8 @@ export default { <strong v-else - class="file-title-name has-tooltip" :title="diffFile.oldPath" + class="file-title-name has-tooltip" data-container="body" > {{ diffFile.filePath }} @@ -78,8 +78,8 @@ export default { </component> <clipboard-button - title="Copy file path to clipboard" :text="diffFile.filePath" + title="Copy file path to clipboard" css-class="btn-default btn-transparent btn-clipboard" /> diff --git a/app/assets/javascripts/notes/components/diff_with_note.vue b/app/assets/javascripts/notes/components/diff_with_note.vue index ee01ec85bbb..cafb28910eb 100644 --- a/app/assets/javascripts/notes/components/diff_with_note.vue +++ b/app/assets/javascripts/notes/components/diff_with_note.vue @@ -60,8 +60,8 @@ export default { <template> <div ref="fileHolder" - class="diff-file file-holder" :class="diffFileClass" + class="diff-file file-holder" > <div class="js-file-title file-title file-title-flex-parent"> <diff-file-header @@ -74,11 +74,11 @@ export default { > <table> <component + v-for="(html, index) in diffRows" :is="rowTag(html)" :class="html.className" - v-for="(html, index) in diffRows" - v-html="html.outerHTML" :key="index" + v-html="html.outerHTML" /> <tr class="notes_holder"> <td diff --git a/app/assets/javascripts/notes/components/discussion_counter.vue b/app/assets/javascripts/notes/components/discussion_counter.vue index cbe4774a360..68e17ac8055 100644 --- a/app/assets/javascripts/notes/components/discussion_counter.vue +++ b/app/assets/javascripts/notes/components/discussion_counter.vue @@ -95,8 +95,8 @@ export default { class="btn-group" role="group"> <a - :href="resolveAllDiscussionsIssuePath" v-tooltip + :href="resolveAllDiscussionsIssuePath" title="Resolve all discussions in new issue" data-container="body" class="new-issue-for-discussion btn btn-default discussion-create-issue-btn"> @@ -108,11 +108,11 @@ export default { class="btn-group" role="group"> <button - @click="jumpToFirstDiscussion" v-tooltip title="Jump to first unresolved discussion" data-container="body" - class="btn btn-default discussion-next-btn"> + class="btn btn-default discussion-next-btn" + @click="jumpToFirstDiscussion"> <span v-html="nextDiscussionSvg"></span> </button> </div> diff --git a/app/assets/javascripts/notes/components/discussion_locked_widget.vue b/app/assets/javascripts/notes/components/discussion_locked_widget.vue index 13283b187d1..de0a5f8489b 100644 --- a/app/assets/javascripts/notes/components/discussion_locked_widget.vue +++ b/app/assets/javascripts/notes/components/discussion_locked_widget.vue @@ -14,8 +14,8 @@ export default { <div class="disabled-comment text-center"> <span class="issuable-note-warning inline"> <icon - name="lock" :size="16" + name="lock" class="icon" /> <span> diff --git a/app/assets/javascripts/notes/components/note_actions.vue b/app/assets/javascripts/notes/components/note_actions.vue index 626b0799581..0bf4258a257 100644 --- a/app/assets/javascripts/notes/components/note_actions.vue +++ b/app/assets/javascripts/notes/components/note_actions.vue @@ -129,12 +129,12 @@ export default { class="note-actions-item"> <button v-tooltip - @click="onResolve" :class="{ 'is-disabled': !resolvable, 'is-active': isResolved }" :title="resolveButtonTitle" :aria-label="resolveButtonTitle" type="button" - class="line-resolve-btn note-action-button"> + class="line-resolve-btn note-action-button" + @click="onResolve"> <template v-if="!isResolving"> <div v-if="isResolved" @@ -164,16 +164,16 @@ export default { > <loading-icon :inline="true" /> <span - v-html="emojiSmiling" - class="link-highlight award-control-icon-neutral"> + class="link-highlight award-control-icon-neutral" + v-html="emojiSmiling"> </span> <span - v-html="emojiSmiley" - class="link-highlight award-control-icon-positive"> + class="link-highlight award-control-icon-positive" + v-html="emojiSmiley"> </span> <span - v-html="emojiSmile" - class="link-highlight award-control-icon-super-positive"> + class="link-highlight award-control-icon-super-positive" + v-html="emojiSmile"> </span> </a> </div> @@ -181,16 +181,16 @@ export default { v-if="canEdit" class="note-actions-item"> <button - @click="onEdit" v-tooltip type="button" title="Edit comment" class="note-action-button js-note-edit btn btn-transparent" data-container="body" - data-placement="bottom"> + data-placement="bottom" + @click="onEdit"> <span - v-html="editSvg" - class="link-highlight"> + class="link-highlight" + v-html="editSvg"> </span> </button> </div> @@ -218,9 +218,9 @@ export default { </li> <li v-if="canEdit"> <button - @click.prevent="onDelete" class="btn btn-transparent js-note-delete js-note-delete" - type="button"> + type="button" + @click.prevent="onDelete"> <span class="text-danger"> Delete comment </span> diff --git a/app/assets/javascripts/notes/components/note_awards_list.vue b/app/assets/javascripts/notes/components/note_awards_list.vue index e8fd155a1ee..521b4d16286 100644 --- a/app/assets/javascripts/notes/components/note_awards_list.vue +++ b/app/assets/javascripts/notes/components/note_awards_list.vue @@ -199,10 +199,10 @@ export default { :key="index" :class="getAwardClassBindings(awardList, awardName)" :title="awardTitle(awardList)" - @click="handleAward(awardName)" class="btn award-control" data-placement="bottom" - type="button"> + type="button" + @click="handleAward(awardName)"> <span v-html="getAwardHTML(awardName)"></span> <span class="award-control-text js-counter"> {{ awardList.length }} @@ -220,16 +220,16 @@ export default { data-placement="bottom" type="button"> <span - v-html="emojiSmiling" - class="award-control-icon award-control-icon-neutral"> + class="award-control-icon award-control-icon-neutral" + v-html="emojiSmiling"> </span> <span - v-html="emojiSmiley" - class="award-control-icon award-control-icon-positive"> + class="award-control-icon award-control-icon-positive" + v-html="emojiSmiley"> </span> <span - v-html="emojiSmile" - class="award-control-icon award-control-icon-super-positive"> + class="award-control-icon award-control-icon-super-positive" + v-html="emojiSmile"> </span> <i aria-hidden="true" diff --git a/app/assets/javascripts/notes/components/note_body.vue b/app/assets/javascripts/notes/components/note_body.vue index 0cb626c14f4..864edcd2ec6 100644 --- a/app/assets/javascripts/notes/components/note_body.vue +++ b/app/assets/javascripts/notes/components/note_body.vue @@ -80,20 +80,20 @@ export default { <template> <div - :class="{ 'js-task-list-container': canEdit }" ref="note-body" + :class="{ 'js-task-list-container': canEdit }" class="note-body"> <div - v-html="note.note_html" - class="note-text md"></div> + class="note-text md" + v-html="note.note_html"></div> <note-form v-if="isEditing" ref="noteForm" - @handleFormUpdate="handleFormUpdate" - @cancelFormEdition="formCancelHandler" :is-editing="isEditing" :note-body="noteBody" :note-id="note.id" + @handleFormUpdate="handleFormUpdate" + @cancelFormEdition="formCancelHandler" /> <textarea v-if="canEdit" diff --git a/app/assets/javascripts/notes/components/note_form.vue b/app/assets/javascripts/notes/components/note_form.vue index c59a2e7a406..7254ef3357d 100644 --- a/app/assets/javascripts/notes/components/note_form.vue +++ b/app/assets/javascripts/notes/components/note_form.vue @@ -165,15 +165,15 @@ export default { :add-spacing-classes="false"> <textarea id="note_note" + ref="textarea" + slot="textarea" + :data-supports-quick-actions="!isEditing" + v-model="updatedNoteBody" name="note[note]" class="note-textarea js-gfm-input js-autosize markdown-area js-vue-issue-note-form js-vue-textarea" - :data-supports-quick-actions="!isEditing" aria-label="Description" - v-model="updatedNoteBody" - ref="textarea" - slot="textarea" - placeholder="Write a comment or drag your files here..." + placeholder="Write a comment or drag your files here…" @keydown.meta.enter="handleUpdate()" @keydown.ctrl.enter="handleUpdate()" @keydown.up="editMyLastNote()" @@ -182,23 +182,23 @@ js-autosize markdown-area js-vue-issue-note-form js-vue-textarea" </markdown-field> <div class="note-form-actions clearfix"> <button - type="button" - @click="handleUpdate()" :disabled="isDisabled" - class="js-vue-issue-save btn btn-save"> + type="button" + class="js-vue-issue-save btn btn-save" + @click="handleUpdate()"> {{ saveButtonTitle }} </button> <button v-if="note.resolvable" - @click.prevent="handleUpdate(true)" class="btn btn-nr btn-default append-right-10 js-comment-resolve-button" + @click.prevent="handleUpdate(true)" > {{ resolveButtonTitle }} </button> <button - @click="cancelHandler()" class="btn btn-cancel note-edit-cancel" - type="button"> + type="button" + @click="cancelHandler()"> Cancel </button> </div> diff --git a/app/assets/javascripts/notes/components/note_header.vue b/app/assets/javascripts/notes/components/note_header.vue index a4081957207..ffe3ba9c805 100644 --- a/app/assets/javascripts/notes/components/note_header.vue +++ b/app/assets/javascripts/notes/components/note_header.vue @@ -66,9 +66,9 @@ export default { v-if="includeToggle" class="discussion-actions"> <button - @click="handleToggle" class="note-action-button discussion-toggle-button js-vue-toggle-button" - type="button"> + type="button" + @click="handleToggle"> <i :class="toggleChevronClass" class="fa" @@ -90,16 +90,16 @@ export default { </template> <span v-if="actionTextHtml" - v-html="actionTextHtml" - class="system-note-message"> + class="system-note-message" + v-html="actionTextHtml"> </span> <span class="system-note-separator"> · </span> <a :href="noteTimestampLink" - @click="updateTargetNoteHash" - class="note-timestamp system-note-separator"> + class="note-timestamp system-note-separator" + @click="updateTargetNoteHash"> <time-ago-tooltip :time="createdAt" tooltip-placement="bottom" diff --git a/app/assets/javascripts/notes/components/noteable_discussion.vue b/app/assets/javascripts/notes/components/noteable_discussion.vue index 7f5aacaa3a2..3f865431155 100644 --- a/app/assets/javascripts/notes/components/noteable_discussion.vue +++ b/app/assets/javascripts/notes/components/noteable_discussion.vue @@ -152,7 +152,7 @@ export default { const msg = 'Are you sure you want to cancel creating this comment?'; // eslint-disable-next-line no-alert - if (!confirm(msg)) { + if (!window.confirm(msg)) { return; } } @@ -229,9 +229,9 @@ Please check your network connection and try again.`; :note-id="discussion.id" :include-toggle="true" :expanded="note.expanded" - @toggleHandler="toggleDiscussionHandler" action-text="started a discussion" class="discussion" + @toggleHandler="toggleDiscussionHandler" /> <note-edited-text v-if="lastUpdatedAt" @@ -269,19 +269,19 @@ Please check your network connection and try again.`; class="btn-group" role="group"> <button - @click="showReplyForm" type="button" class="js-vue-discussion-reply btn btn-text-field" - title="Add a reply">Reply...</button> + title="Add a reply" + @click="showReplyForm">Reply...</button> </div> <div v-if="note.resolvable" class="btn-group" role="group"> <button - @click="resolveHandler()" type="button" class="btn btn-default" + @click="resolveHandler()" > <i v-if="isResolving" @@ -301,8 +301,8 @@ Please check your network connection and try again.`; class="btn-group" role="group"> <a - :href="note.resolve_with_issue_path" v-tooltip + :href="note.resolve_with_issue_path" class="new-issue-for-discussion btn btn-default discussion-create-issue-btn" title="Resolve this discussion in a new issue" @@ -316,11 +316,11 @@ Please check your network connection and try again.`; class="btn-group" role="group"> <button - @click="jumpToDiscussion" v-tooltip class="btn btn-default discussion-next-btn" title="Jump to next unresolved discussion" data-container="body" + @click="jumpToDiscussion" > <span v-html="nextDiscussionsSvg"></span> </button> @@ -330,12 +330,12 @@ Please check your network connection and try again.`; </template> <note-form v-if="isReplying" - save-button-title="Comment" + ref="noteForm" :note="note" :is-editing="false" + save-button-title="Comment" @handleFormUpdate="saveReply" - @cancelFormEdition="cancelReplyForm" - ref="noteForm" /> + @cancelFormEdition="cancelReplyForm" /> <note-signed-out-widget v-if="!canReply" /> </div> </div> diff --git a/app/assets/javascripts/notes/components/noteable_note.vue b/app/assets/javascripts/notes/components/noteable_note.vue index 566f5c68e66..713f93456b1 100644 --- a/app/assets/javascripts/notes/components/noteable_note.vue +++ b/app/assets/javascripts/notes/components/noteable_note.vue @@ -77,7 +77,7 @@ export default { }, deleteHandler() { // eslint-disable-next-line no-alert - if (confirm('Are you sure you want to delete this comment?')) { + if (window.confirm('Are you sure you want to delete this comment?')) { this.isDeleting = true; this.deleteNote(this.note) @@ -129,7 +129,7 @@ export default { formCancelHandler(shouldConfirm, isDirty) { if (shouldConfirm && isDirty) { // eslint-disable-next-line no-alert - if (!confirm('Are you sure you want to cancel editing this comment?')) + if (!window.confirm('Are you sure you want to cancel editing this comment?')) return; } this.$refs.noteBody.resetAutoSave(); @@ -151,10 +151,10 @@ export default { <template> <li - class="note timeline-entry" :id="noteAnchorId" :class="classNameBindings" - :data-award-url="note.toggle_award_path"> + :data-award-url="note.toggle_award_path" + class="note timeline-entry"> <div class="timeline-entry-inner"> <div class="timeline-icon"> <user-avatar-link @@ -191,12 +191,12 @@ export default { /> </div> <note-body + ref="noteBody" :note="note" :can-edit="note.current_user.can_edit" :is-editing="isEditing" @handleFormUpdate="formUpdateHandler" @cancelFormEdition="formCancelHandler" - ref="noteBody" /> </div> </div> diff --git a/app/assets/javascripts/notes/constants.js b/app/assets/javascripts/notes/constants.js index c4de4826eda..5b5b1e89058 100644 --- a/app/assets/javascripts/notes/constants.js +++ b/app/assets/javascripts/notes/constants.js @@ -14,6 +14,7 @@ export const EPIC_NOTEABLE_TYPE = 'epic'; export const MERGE_REQUEST_NOTEABLE_TYPE = 'merge_request'; export const UNRESOLVE_NOTE_METHOD_NAME = 'delete'; export const RESOLVE_NOTE_METHOD_NAME = 'post'; +export const DESCRIPTION_TYPE = 'changed the description'; export const NOTEABLE_TYPE_MAPPING = { Issue: ISSUE_NOTEABLE_TYPE, diff --git a/app/assets/javascripts/notes/stores/collapse_utils.js b/app/assets/javascripts/notes/stores/collapse_utils.js new file mode 100644 index 00000000000..fa4a1c56b20 --- /dev/null +++ b/app/assets/javascripts/notes/stores/collapse_utils.js @@ -0,0 +1,108 @@ +import { n__, s__, sprintf } from '~/locale'; +import { DESCRIPTION_TYPE } from '../constants'; + +/** + * Changes the description from a note, returns 'changed the description n number of times' + */ +export const changeDescriptionNote = (note, descriptionChangedTimes, timeDifferenceMinutes) => { + const descriptionNote = Object.assign({}, note); + + descriptionNote.note_html = sprintf( + s__(`MergeRequest| + %{paragraphStart}changed the description %{descriptionChangedTimes} times %{timeDifferenceMinutes}%{paragraphEnd}`), + { + paragraphStart: '<p dir="auto">', + paragraphEnd: '</p>', + descriptionChangedTimes, + timeDifferenceMinutes: n__('within %d minute ', 'within %d minutes ', timeDifferenceMinutes), + }, + false, + ); + + descriptionNote.times_updated = descriptionChangedTimes; + + return descriptionNote; +}; + +/** + * Checks the time difference between two notes from their 'created_at' dates + * returns an integer + */ + +export const getTimeDifferenceMinutes = (noteBeggining, noteEnd) => { + const descriptionNoteBegin = new Date(noteBeggining.created_at); + const descriptionNoteEnd = new Date(noteEnd.created_at); + const timeDifferenceMinutes = (descriptionNoteEnd - descriptionNoteBegin) / 1000 / 60; + + return Math.ceil(timeDifferenceMinutes); +}; + +/** + * Checks if a note is a system note and if the content is description + * + * @param {Object} note + * @returns {Boolean} + */ +export const isDescriptionSystemNote = note => note.system && note.note === DESCRIPTION_TYPE; + +/** + * Collapses the system notes of a description type, e.g. Changed the description, n minutes ago + * the notes will collapse as long as they happen no more than 10 minutes away from each away + * in between the notes can be anything, another type of system note + * (such as 'changed the weight') or a comment. + * + * @param {Array} notes + * @returns {Array} + */ +export const collapseSystemNotes = notes => { + let lastDescriptionSystemNote = null; + let lastDescriptionSystemNoteIndex = -1; + let descriptionChangedTimes = 1; + + return notes.slice(0).reduce((acc, currentNote) => { + const note = currentNote.notes[0]; + + if (isDescriptionSystemNote(note)) { + // is it the first one? + if (!lastDescriptionSystemNote) { + lastDescriptionSystemNote = note; + lastDescriptionSystemNoteIndex = acc.length; + } else if (lastDescriptionSystemNote) { + const timeDifferenceMinutes = getTimeDifferenceMinutes( + lastDescriptionSystemNote, + note, + ); + + // are they less than 10 minutes appart? + if (timeDifferenceMinutes > 10) { + // reset counter + descriptionChangedTimes = 1; + // update the previous system note + lastDescriptionSystemNote = note; + lastDescriptionSystemNoteIndex = acc.length; + } else { + // increase counter + descriptionChangedTimes += 1; + + // delete the previous one + acc.splice(lastDescriptionSystemNoteIndex, 1); + + // replace the text of the current system note with the collapsed note. + currentNote.notes.splice( + 0, + 1, + changeDescriptionNote(note, descriptionChangedTimes, timeDifferenceMinutes), + ); + + // update the previous system note index + lastDescriptionSystemNoteIndex = acc.length; + } + } + } + acc.push(currentNote); + return acc; + }, []); +}; + +// for babel-rewire +export default {}; diff --git a/app/assets/javascripts/notes/stores/getters.js b/app/assets/javascripts/notes/stores/getters.js index 787be6f4c99..bc373e0d0fc 100644 --- a/app/assets/javascripts/notes/stores/getters.js +++ b/app/assets/javascripts/notes/stores/getters.js @@ -1,6 +1,8 @@ import _ from 'underscore'; +import { collapseSystemNotes } from './collapse_utils'; + +export const notes = state => collapseSystemNotes(state.notes); -export const notes = state => state.notes; export const targetNoteHash = state => state.targetNoteHash; export const getNotesData = state => state.notesData; diff --git a/app/assets/javascripts/pages/admin/jobs/index/components/stop_jobs_modal.vue b/app/assets/javascripts/pages/admin/jobs/index/components/stop_jobs_modal.vue index ba1d8e4d8db..bc84666779e 100644 --- a/app/assets/javascripts/pages/admin/jobs/index/components/stop_jobs_modal.vue +++ b/app/assets/javascripts/pages/admin/jobs/index/components/stop_jobs_modal.vue @@ -40,8 +40,8 @@ <gl-modal id="stop-jobs-modal" :header-title-text="s__('AdminArea|Stop all jobs?')" - footer-primary-button-variant="danger" :footer-primary-button-text="s__('AdminArea|Stop jobs')" + footer-primary-button-variant="danger" @submit="onSubmit" > {{ text }} diff --git a/app/assets/javascripts/pages/admin/projects/index/components/delete_project_modal.vue b/app/assets/javascripts/pages/admin/projects/index/components/delete_project_modal.vue index 343c65edb37..ff66d3a8ac4 100644 --- a/app/assets/javascripts/pages/admin/projects/index/components/delete_project_modal.vue +++ b/app/assets/javascripts/pages/admin/projects/index/components/delete_project_modal.vue @@ -83,9 +83,9 @@ id="delete-project-modal" :title="title" :text="text" - kind="danger" :primary-button-label="primaryButtonLabel" :submit-disabled="!canSubmit" + kind="danger" @submit="onSubmit" @cancel="onCancel" > @@ -107,15 +107,15 @@ value="delete" /> <input + :value="csrfToken" type="hidden" name="authenticity_token" - :value="csrfToken" /> <input + v-model="enteredProjectName" name="projectName" class="form-control" type="text" - v-model="enteredProjectName" aria-labelledby="input-label" autocomplete="off" /> diff --git a/app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue b/app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue index 9ce176744ba..cc2805a1901 100644 --- a/app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue +++ b/app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue @@ -116,10 +116,10 @@ id="delete-user-modal" :title="title" :text="text" - kind="danger" :primary-button-label="primaryButtonLabel" :secondary-button-label="secondaryButtonLabel" :submit-disabled="!canSubmit" + kind="danger" @submit="onSubmit" @cancel="onCancel" > @@ -141,15 +141,15 @@ value="delete" /> <input + :value="csrfToken" type="hidden" name="authenticity_token" - :value="csrfToken" /> <input + v-model="enteredUsername" type="text" name="username" class="form-control" - v-model="enteredUsername" aria-labelledby="input-label" autocomplete="off" /> @@ -160,11 +160,11 @@ slot-scope="props" > <button + :disabled="!canSubmit" type="button" class="btn js-secondary-button btn-warning" - :disabled="!canSubmit" - @click="onSecondaryAction" data-dismiss="modal" + @click="onSecondaryAction" > {{ secondaryButtonLabel }} </button> diff --git a/app/assets/javascripts/pages/milestones/shared/components/delete_milestone_modal.vue b/app/assets/javascripts/pages/milestones/shared/components/delete_milestone_modal.vue index 16f792d635a..4061c11ba8f 100644 --- a/app/assets/javascripts/pages/milestones/shared/components/delete_milestone_modal.vue +++ b/app/assets/javascripts/pages/milestones/shared/components/delete_milestone_modal.vue @@ -96,8 +96,8 @@ Once deleted, it cannot be undone or recovered.`), id="delete-milestone-modal" :title="title" :text="text" - kind="danger" :primary-button-label="s__('Milestones|Delete milestone')" + kind="danger" @submit="onSubmit"> <template diff --git a/app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue b/app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue index 2bda2aeb3a1..2c683a39f42 100644 --- a/app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue +++ b/app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue @@ -53,8 +53,8 @@ <template> <gl-modal id="promote-milestone-modal" - footer-primary-button-variant="warning" :footer-primary-button-text="s__('Milestones|Promote Milestone')" + footer-primary-button-variant="warning" @submit="onSubmit" > <template diff --git a/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors.js b/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors.js index 653e2502d01..37336a8cb69 100644 --- a/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors.js +++ b/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors.js @@ -36,7 +36,9 @@ export default (function() { var author_graph, author_header; author_header = _this.create_author_header(d); $(".contributors-list").append(author_header); - _this.authors[d.author_name] = author_graph = new ContributorsAuthorGraph(d.dates); + + author_graph = new ContributorsAuthorGraph(d.dates); + _this.authors[d.author_name] = author_graph; return author_graph.draw(); }; })(this)); diff --git a/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_util.js b/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_util.js index 77135ad1f0e..165446a4db6 100644 --- a/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_util.js +++ b/app/assets/javascripts/pages/projects/graphs/show/stat_graph_contributors_util.js @@ -111,10 +111,15 @@ export default { parse_log_entry: function(log_entry, field, date_range) { var parsed_entry; parsed_entry = {}; + parsed_entry.author_name = log_entry.author_name; parsed_entry.author_email = log_entry.author_email; parsed_entry.dates = {}; - parsed_entry.commits = parsed_entry.additions = parsed_entry.deletions = 0; + + parsed_entry.commits = 0; + parsed_entry.additions = 0; + parsed_entry.deletions = 0; + _.each(_.omit(log_entry, 'author_name', 'author_email'), (function(_this) { return function(value, key) { if (_this.in_range(value.date, date_range)) { diff --git a/app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue b/app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue index ad6df51bb7a..5d2247f6c6d 100644 --- a/app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue +++ b/app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue @@ -71,8 +71,8 @@ <template> <gl-modal id="promote-label-modal" - footer-primary-button-variant="warning" :footer-primary-button-text="s__('Labels|Promote Label')" + footer-primary-button-variant="warning" @submit="onSubmit" > <div diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue index 2d18fa2044b..d0613804067 100644 --- a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue +++ b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue @@ -65,11 +65,11 @@ <div class="cron-preset-radio-input"> <input id="custom" - class="label-light" - type="radio" :name="inputNameAttribute" :value="cronInterval" :checked="isEditable" + class="label-light" + type="radio" @click="toggleCustomInput(true)" /> @@ -90,11 +90,11 @@ <div class="cron-preset-radio-input"> <input id="every-day" - class="label-light" - type="radio" v-model="cronInterval" :name="inputNameAttribute" :value="cronIntervalPresets.everyDay" + class="label-light" + type="radio" @click="toggleCustomInput(false)" /> @@ -109,11 +109,11 @@ <div class="cron-preset-radio-input"> <input id="every-week" - class="label-light" - type="radio" v-model="cronInterval" :name="inputNameAttribute" :value="cronIntervalPresets.everyWeek" + class="label-light" + type="radio" @click="toggleCustomInput(false)" /> @@ -128,11 +128,11 @@ <div class="cron-preset-radio-input"> <input id="every-month" - class="label-light" - type="radio" v-model="cronInterval" :name="inputNameAttribute" :value="cronIntervalPresets.everyMonth" + class="label-light" + type="radio" @click="toggleCustomInput(false)" /> @@ -147,13 +147,13 @@ <div class="cron-interval-input-wrapper"> <input id="schedule_cron" - class="form-control inline cron-interval-input" - type="text" :placeholder="__('Define a custom pattern with cron syntax')" - required="true" v-model="cronInterval" :name="inputNameAttribute" :disabled="!isEditable" + class="form-control inline cron-interval-input" + type="text" + required="true" /> </div> </div> diff --git a/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js b/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js index 6c2a785c0af..37ef77c8e43 100644 --- a/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js +++ b/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js @@ -22,4 +22,18 @@ document.addEventListener('DOMContentLoaded', () => { errorBox: variableListEl.querySelector('.js-ci-variable-error-box'), saveEndpoint: variableListEl.dataset.saveEndpoint, }); + + // hide extra auto devops settings based on data-attributes + const autoDevOpsSettings = document.querySelector('.js-auto-devops-settings'); + const autoDevOpsExtraSettings = document.querySelector('.js-extra-settings'); + + autoDevOpsSettings.addEventListener('click', event => { + const target = event.target; + if (target.classList.contains('js-toggle-extra-settings')) { + autoDevOpsExtraSettings.classList.toggle( + 'hidden', + !!(target.dataset && target.dataset.hideExtraSettings), + ); + } + }); }); diff --git a/app/assets/javascripts/pages/projects/shared/permissions/components/project_feature_setting.vue b/app/assets/javascripts/pages/projects/shared/permissions/components/project_feature_setting.vue index 9b13b2a524f..06101290f6c 100644 --- a/app/assets/javascripts/pages/projects/shared/permissions/components/project_feature_setting.vue +++ b/app/assets/javascripts/pages/projects/shared/permissions/components/project_feature_setting.vue @@ -72,25 +72,25 @@ <template> <div - class="project-feature-controls" :data-for="name" + class="project-feature-controls" > <input v-if="name" - type="hidden" :name="name" :value="value" + type="hidden" /> <project-feature-toggle :value="featureEnabled" - @change="toggleFeature" :disabled-input="disabledInput" + @change="toggleFeature" /> <div class="select-wrapper"> <select + :disabled="displaySelectInput" class="form-control project-repo-select select-control" @change="selectOption" - :disabled="displaySelectInput" > <option v-for="[optionValue, optionName] in displayOptions" diff --git a/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue b/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue index 06b0ab184ed..ae88b765abf 100644 --- a/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue +++ b/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue @@ -175,16 +175,16 @@ <div> <div class="project-visibility-setting"> <project-setting-row - label="Project visibility" :help-path="visibilityHelpPath" + label="Project visibility" > <div class="project-feature-controls"> <div class="select-wrapper"> <select - name="project[visibility_level]" v-model="visibilityLevel" - class="form-control select-control" :disabled="!canChangeVisibilityLevel" + name="project[visibility_level]" + class="form-control select-control" > <option :value="visibilityOptions.PRIVATE" @@ -219,30 +219,30 @@ class="request-access" > <input + :value="requestAccessEnabled" type="hidden" name="project[request_access_enabled]" - :value="requestAccessEnabled" /> <input - type="checkbox" v-model="requestAccessEnabled" + type="checkbox" /> Allow users to request access </label> </project-setting-row> </div> <div - class="project-feature-settings" :class="{ 'highlight-changes': highlightChangesClass }" + class="project-feature-settings" > <project-setting-row label="Issues" help-text="Lightweight issue tracking system for this project" > <project-feature-setting - name="project[project_feature_attributes][issues_access_level]" :options="featureAccessLevelOptions" v-model="issuesAccessLevel" + name="project[project_feature_attributes][issues_access_level]" /> </project-setting-row> <project-setting-row @@ -250,9 +250,9 @@ help-text="View and edit files in this project" > <project-feature-setting - name="project[project_feature_attributes][repository_access_level]" :options="featureAccessLevelOptions" v-model="repositoryAccessLevel" + name="project[project_feature_attributes][repository_access_level]" /> </project-setting-row> <div class="project-feature-setting-group"> @@ -261,10 +261,10 @@ help-text="Submit changes to be merged upstream" > <project-feature-setting - name="project[project_feature_attributes][merge_requests_access_level]" :options="repoFeatureAccessLevelOptions" v-model="mergeRequestsAccessLevel" :disabled-input="!repositoryEnabled" + name="project[project_feature_attributes][merge_requests_access_level]" /> </project-setting-row> <project-setting-row @@ -272,34 +272,34 @@ help-text="Build, test, and deploy your changes" > <project-feature-setting - name="project[project_feature_attributes][builds_access_level]" :options="repoFeatureAccessLevelOptions" v-model="buildsAccessLevel" :disabled-input="!repositoryEnabled" + name="project[project_feature_attributes][builds_access_level]" /> </project-setting-row> <project-setting-row v-if="registryAvailable" - label="Container registry" :help-path="registryHelpPath" + label="Container registry" help-text="Every project can have its own space to store its Docker images" > <project-feature-toggle - name="project[container_registry_enabled]" v-model="containerRegistryEnabled" :disabled-input="!repositoryEnabled" + name="project[container_registry_enabled]" /> </project-setting-row> <project-setting-row v-if="lfsAvailable" - label="Git Large File Storage" :help-path="lfsHelpPath" + label="Git Large File Storage" help-text="Manages large files such as audio, video, and graphics files" > <project-feature-toggle - name="project[lfs_enabled]" v-model="lfsEnabled" :disabled-input="!repositoryEnabled" + name="project[lfs_enabled]" /> </project-setting-row> </div> @@ -308,9 +308,9 @@ help-text="Pages for project documentation" > <project-feature-setting - name="project[project_feature_attributes][wiki_access_level]" :options="featureAccessLevelOptions" v-model="wikiAccessLevel" + name="project[project_feature_attributes][wiki_access_level]" /> </project-setting-row> <project-setting-row @@ -318,9 +318,9 @@ help-text="Share code pastes with others out of Git repository" > <project-feature-setting - name="project[project_feature_attributes][snippets_access_level]" :options="featureAccessLevelOptions" v-model="snippetsAccessLevel" + name="project[project_feature_attributes][snippets_access_level]" /> </project-setting-row> </div> diff --git a/app/assets/javascripts/pages/projects/wikis/components/delete_wiki_modal.vue b/app/assets/javascripts/pages/projects/wikis/components/delete_wiki_modal.vue index 5765eed4d45..0289209ff1e 100644 --- a/app/assets/javascripts/pages/projects/wikis/components/delete_wiki_modal.vue +++ b/app/assets/javascripts/pages/projects/wikis/components/delete_wiki_modal.vue @@ -50,8 +50,8 @@ export default { <gl-modal id="delete-wiki-modal" :header-title-text="title" - footer-primary-button-variant="danger" :footer-primary-button-text="s__('WikiPageConfirmDelete|Delete page')" + footer-primary-button-variant="danger" @submit="onSubmit" > {{ message }} @@ -68,9 +68,9 @@ export default { value="delete" /> <input + :value="csrfToken" type="hidden" name="authenticity_token" - :value="csrfToken" /> </form> </gl-modal> diff --git a/app/assets/javascripts/pages/sessions/new/username_validator.js b/app/assets/javascripts/pages/sessions/new/username_validator.js index 825de01b5a2..87213c94eda 100644 --- a/app/assets/javascripts/pages/sessions/new/username_validator.js +++ b/app/assets/javascripts/pages/sessions/new/username_validator.js @@ -62,13 +62,13 @@ export default class UsernameValidator { return this.setPendingState(); } - if (!this.state.available) { - return this.setUnavailableState(); - } - if (!this.state.valid) { return this.setInvalidState(); } + + if (!this.state.available) { + return this.setUnavailableState(); + } } interceptInvalid(event) { @@ -89,7 +89,6 @@ export default class UsernameValidator { setAvailabilityState(usernameTaken) { if (usernameTaken) { - this.state.valid = false; this.state.available = false; } else { this.state.available = true; diff --git a/app/assets/javascripts/pages/users/user_tabs.js b/app/assets/javascripts/pages/users/user_tabs.js index 9404b06615e..a2ca03536f2 100644 --- a/app/assets/javascripts/pages/users/user_tabs.js +++ b/app/assets/javascripts/pages/users/user_tabs.js @@ -180,14 +180,14 @@ export default class UserTabs { } toggleLoading(status) { - return this.$parentEl.find('.loading-status .loading').toggleClass('hidden', !status); + return this.$parentEl.find('.loading-status .loading').toggleClass('hide', !status); } setCurrentAction(source) { let newState = source; newState = newState.replace(/\/+$/, ''); newState += this.windowLocation.search + this.windowLocation.hash; - history.replaceState( + window.history.replaceState( { url: newState, }, diff --git a/app/assets/javascripts/pdf/index.vue b/app/assets/javascripts/pdf/index.vue index 00f32d9de78..2f480ecdc69 100644 --- a/app/assets/javascripts/pdf/index.vue +++ b/app/assets/javascripts/pdf/index.vue @@ -56,8 +56,8 @@ <template> <div - class="pdf-viewer" - v-if="hasPDF"> + v-if="hasPDF" + class="pdf-viewer"> <page v-for="(page, index) in pages" :key="index" diff --git a/app/assets/javascripts/pdf/page/index.vue b/app/assets/javascripts/pdf/page/index.vue index fcba819beba..9f06833d560 100644 --- a/app/assets/javascripts/pdf/page/index.vue +++ b/app/assets/javascripts/pdf/page/index.vue @@ -43,9 +43,9 @@ <template> <canvas - class="pdf-page" ref="canvas" :data-page="number" + class="pdf-page" > </canvas> </template> diff --git a/app/assets/javascripts/performance_bar/components/detailed_metric.vue b/app/assets/javascripts/performance_bar/components/detailed_metric.vue index db8a0055acd..dc7d6d29b8f 100644 --- a/app/assets/javascripts/performance_bar/components/detailed_metric.vue +++ b/app/assets/javascripts/performance_bar/components/detailed_metric.vue @@ -39,9 +39,9 @@ export default { </script> <template> <div + v-if="currentRequest.details" :id="`peek-view-${metric}`" class="view" - v-if="currentRequest.details" > <button :data-target="`#modal-peek-${metric}-details`" @@ -56,6 +56,7 @@ export default { <gl-modal :id="`modal-peek-${metric}-details`" :header-title-text="header" + modal-size="xl" class="performance-bar-modal" > <table diff --git a/app/assets/javascripts/pipelines/components/graph/action_component.vue b/app/assets/javascripts/pipelines/components/graph/action_component.vue index 82b4ce083fb..1f152ed438d 100644 --- a/app/assets/javascripts/pipelines/components/graph/action_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/action_component.vue @@ -83,15 +83,16 @@ export default { </script> <template> <button - type="button" - @click="onClickAction" v-tooltip :title="tooltipText" + :class="cssClass" + :disabled="isDisabled" + type="button" class="js-ci-action btn btn-blank btn-transparent ci-action-icon-container ci-action-icon-wrapper" - :class="cssClass" data-container="body" - :disabled="isDisabled" + data-boundary="viewport" + @click="onClickAction" > <icon :name="actionIcon"/> </button> diff --git a/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue b/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue index e64afc94ef9..e047d10ac93 100644 --- a/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/dropdown_job_component.vue @@ -82,12 +82,13 @@ export default { <div class="ci-job-dropdown-container dropdown dropright"> <button v-tooltip + :title="tooltipText" type="button" data-toggle="dropdown" data-container="body" data-boundary="viewport" + data-display="static" class="dropdown-menu-toggle build-content" - :title="tooltipText" > <job-name-component diff --git a/app/assets/javascripts/pipelines/components/graph/job_component.vue b/app/assets/javascripts/pipelines/components/graph/job_component.vue index dc16d395bcb..886e62ab1a7 100644 --- a/app/assets/javascripts/pipelines/components/graph/job_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/job_component.vue @@ -107,11 +107,11 @@ export default { </a> <div - v-else v-tooltip - class="js-job-component-tooltip non-details-job-component" + v-else :title="tooltipText" :class="cssClassJobName" + class="js-job-component-tooltip non-details-job-component" data-html="true" data-container="body" > diff --git a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue index f32368947e8..2c728582b7c 100644 --- a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue @@ -52,8 +52,8 @@ export default { </script> <template> <li - class="stage-column" - :class="stageConnectorClass"> + :class="stageConnectorClass" + class="stage-column"> <div class="stage-name"> {{ title }} </div> @@ -62,9 +62,9 @@ export default { <li v-for="(job, index) in jobs" :key="job.id" - class="build" :class="buildConnnectorClass(index)" :id="jobId(job)" + class="build" > <div class="curve"></div> diff --git a/app/assets/javascripts/pipelines/components/header_component.vue b/app/assets/javascripts/pipelines/components/header_component.vue index e08c2092680..5b212ee8931 100644 --- a/app/assets/javascripts/pipelines/components/header_component.vue +++ b/app/assets/javascripts/pipelines/components/header_component.vue @@ -82,11 +82,11 @@ <ci-header v-if="shouldRenderContent" :status="status" - item-name="Pipeline" :item-id="pipeline.id" :time="pipeline.created_at" :user="pipeline.user" :actions="actions" + item-name="Pipeline" @actionClicked="postAction" /> <loading-icon diff --git a/app/assets/javascripts/pipelines/components/nav_controls.vue b/app/assets/javascripts/pipelines/components/nav_controls.vue index eba5678e3e5..1fce9f16ee0 100644 --- a/app/assets/javascripts/pipelines/components/nav_controls.vue +++ b/app/assets/javascripts/pipelines/components/nav_controls.vue @@ -50,10 +50,10 @@ <loading-button v-if="resetCachePath" - @click="onClickResetCache" :loading="isResetCacheButtonLoading" - class="btn btn-default js-clear-cache" :label="s__('Pipelines|Clear Runner Caches')" + class="btn btn-default js-clear-cache" + @click="onClickResetCache" /> <a diff --git a/app/assets/javascripts/pipelines/components/pipeline_url.vue b/app/assets/javascripts/pipelines/components/pipeline_url.vue index 4d965733f95..a107e579457 100644 --- a/app/assets/javascripts/pipelines/components/pipeline_url.vue +++ b/app/assets/javascripts/pipelines/components/pipeline_url.vue @@ -55,10 +55,10 @@ <span>by</span> <user-avatar-link v-if="user" - class="js-pipeline-url-user" :link-href="pipeline.user.path" :img-src="pipeline.user.avatar_url" :tooltip-text="pipeline.user.name" + class="js-pipeline-url-user" /> <span v-if="!user" @@ -67,31 +67,31 @@ </span> <div class="label-container"> <span - v-if="pipeline.flags.latest" v-tooltip + v-if="pipeline.flags.latest" class="js-pipeline-url-latest badge badge-success" title="Latest pipeline for this branch"> latest </span> <span - v-if="pipeline.flags.yaml_errors" v-tooltip - class="js-pipeline-url-yaml badge badge-danger" - :title="pipeline.yaml_errors"> + v-if="pipeline.flags.yaml_errors" + :title="pipeline.yaml_errors" + class="js-pipeline-url-yaml badge badge-danger"> yaml invalid </span> <span - v-if="pipeline.flags.failure_reason" v-tooltip - class="js-pipeline-url-failure badge badge-danger" - :title="pipeline.failure_reason"> + v-if="pipeline.flags.failure_reason" + :title="pipeline.failure_reason" + class="js-pipeline-url-failure badge badge-danger"> error </span> <a + v-popover="popoverOptions" v-if="pipeline.flags.auto_devops" tabindex="0" class="js-pipeline-url-autodevops badge badge-info autodevops-badge" - v-popover="popoverOptions" role="button"> Auto DevOps </a> diff --git a/app/assets/javascripts/pipelines/components/pipelines.vue b/app/assets/javascripts/pipelines/components/pipelines.vue index 497a09cec65..b31b4bad7a0 100644 --- a/app/assets/javascripts/pipelines/components/pipelines.vue +++ b/app/assets/javascripts/pipelines/components/pipelines.vue @@ -282,8 +282,8 @@ <template> <div class="pipelines-container"> <div - class="top-area scrolling-tabs-container inner-page-scroll-tabs" v-if="shouldRenderTabs || shouldRenderButtons" + class="top-area scrolling-tabs-container inner-page-scroll-tabs" > <div class="fade-left"> <i @@ -303,8 +303,8 @@ <navigation-tabs v-if="shouldRenderTabs" :tabs="tabs" - @onChangeTab="onChangeTab" scope="pipelines" + @onChangeTab="onChangeTab" /> <navigation-controls @@ -312,8 +312,8 @@ :new-pipeline-path="newPipelinePath" :reset-cache-path="resetCachePath" :ci-lint-path="ciLintPath" - @resetRunnersCache="handleResetRunnersCache" :is-reset-cache-button-loading="isResetCacheButtonLoading" + @resetRunnersCache="handleResetRunnersCache" /> </div> @@ -347,8 +347,8 @@ /> <div - class="table-holder" v-else-if="stateToRender === $options.stateMap.tableList" + class="table-holder" > <pipelines-table-component diff --git a/app/assets/javascripts/pipelines/components/pipelines_actions.vue b/app/assets/javascripts/pipelines/components/pipelines_actions.vue index e9bc3cf14ca..5070c253f11 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_actions.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_actions.vue @@ -44,13 +44,13 @@ <div class="btn-group"> <button v-tooltip + :disabled="isLoading" type="button" class="dropdown-new btn btn-default js-pipeline-dropdown-manual-actions" title="Manual job" data-toggle="dropdown" data-placement="top" aria-label="Manual job" - :disabled="isLoading" > <icon name="play" @@ -69,11 +69,11 @@ :key="i" > <button + :class="{ disabled: isActionDisabled(action) }" + :disabled="isActionDisabled(action)" type="button" class="js-pipeline-action-link no-btn btn" @click="onClickAction(action.path)" - :class="{ disabled: isActionDisabled(action) }" - :disabled="isActionDisabled(action)" > {{ action.name }} </button> diff --git a/app/assets/javascripts/pipelines/components/pipelines_artifacts.vue b/app/assets/javascripts/pipelines/components/pipelines_artifacts.vue index 31fcc9dd412..490df47e154 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_artifacts.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_artifacts.vue @@ -42,9 +42,9 @@ v-for="(artifact, i) in artifacts" :key="i"> <a + :href="artifact.path" rel="nofollow" download - :href="artifact.path" > Download {{ artifact.name }} artifacts </a> diff --git a/app/assets/javascripts/pipelines/components/pipelines_table.vue b/app/assets/javascripts/pipelines/components/pipelines_table.vue index 4318abe97e0..2e777783636 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_table.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_table.vue @@ -114,8 +114,8 @@ <modal id="confirmation-modal" :header-title-text="modalTitle" - footer-primary-button-variant="danger" :footer-primary-button-text="s__('Pipeline|Stop pipeline')" + footer-primary-button-variant="danger" @submit="onSubmit" > <span v-html="modalText"></span> diff --git a/app/assets/javascripts/pipelines/components/pipelines_table_row.vue b/app/assets/javascripts/pipelines/components/pipelines_table_row.vue index a3c17479e6f..b2744a30c2a 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_table_row.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_table_row.vue @@ -301,9 +301,9 @@ <div class="table-mobile-content"> <template v-if="pipeline.details.stages.length > 0"> <div - class="stage-container dropdown js-mini-pipeline-graph" v-for="(stage, index) in pipeline.details.stages" - :key="index"> + :key="index" + class="stage-container dropdown js-mini-pipeline-graph"> <pipeline-stage :type="$options.pipelinesTable" :stage="stage" @@ -331,28 +331,28 @@ <pipelines-artifacts-component v-if="pipeline.details.artifacts.length" - class="d-none d-sm-none d-md-block" :artifacts="pipeline.details.artifacts" + class="d-none d-sm-none d-md-block" /> <loading-button v-if="pipeline.flags.retryable" - @click="handleRetryClick" - container-class="js-pipelines-retry-button btn btn-default btn-retry" :loading="isRetrying" :disabled="isRetrying" + container-class="js-pipelines-retry-button btn btn-default btn-retry" + @click="handleRetryClick" > <icon name="repeat" /> </loading-button> <loading-button v-if="pipeline.flags.cancelable" - @click="handleCancelClick" + :loading="isCancelling" + :disabled="isCancelling" data-toggle="modal" data-target="#confirmation-modal" container-class="js-pipelines-cancel-button btn btn-remove" - :loading="isCancelling" - :disabled="isCancelling" + @click="handleCancelClick" > <icon name="close" /> </loading-button> diff --git a/app/assets/javascripts/pipelines/components/stage.vue b/app/assets/javascripts/pipelines/components/stage.vue index f9769815796..b9231c002fd 100644 --- a/app/assets/javascripts/pipelines/components/stage.vue +++ b/app/assets/javascripts/pipelines/components/stage.vue @@ -158,22 +158,23 @@ export default { <div class="dropdown"> <button v-tooltip + id="stageDropdown" + ref="dropdown" :class="triggerButtonClass" - @click="onClickStage" - class="mini-pipeline-graph-dropdown-toggle js-builds-dropdown-button" :title="stage.title" + class="mini-pipeline-graph-dropdown-toggle js-builds-dropdown-button" data-placement="top" data-toggle="dropdown" + data-display="static" type="button" - id="stageDropdown" aria-haspopup="true" aria-expanded="false" - ref="dropdown" + @click="onClickStage" > <span - aria-hidden="true" :aria-label="stage.title" + aria-hidden="true" > <icon :name="borderlessIcon" /> </span> diff --git a/app/assets/javascripts/pipelines/components/time_ago.vue b/app/assets/javascripts/pipelines/components/time_ago.vue index 79dbdca4010..0a97df2dc18 100644 --- a/app/assets/javascripts/pipelines/components/time_ago.vue +++ b/app/assets/javascripts/pipelines/components/time_ago.vue @@ -66,8 +66,8 @@ </div> <div class="table-mobile-content"> <p - class="duration" v-if="hasDuration" + class="duration" > <span v-html="iconTimerSvg"> </span> @@ -75,8 +75,8 @@ </p> <p - class="finished-at d-none d-sm-none d-md-block" v-if="hasFinishedTime" + class="finished-at d-none d-sm-none d-md-block" > <i @@ -87,9 +87,9 @@ <time v-tooltip + :title="tooltipTitle(finishedTime)" data-placement="top" - data-container="body" - :title="tooltipTitle(finishedTime)"> + data-container="body"> {{ timeFormated(finishedTime) }} </time> </p> diff --git a/app/assets/javascripts/profile/account/components/delete_account_modal.vue b/app/assets/javascripts/profile/account/components/delete_account_modal.vue index f50002afbf2..974629fa2af 100644 --- a/app/assets/javascripts/profile/account/components/delete_account_modal.vue +++ b/app/assets/javascripts/profile/account/components/delete_account_modal.vue @@ -80,10 +80,10 @@ Once you confirm %{deleteAccount}, it cannot be undone or recovered.`), id="delete-account-modal" :title="s__('Profiles|Delete your account?')" :text="text" - kind="danger" :primary-button-label="s__('Profiles|Delete account')" - @submit="onSubmit" - :submit-disabled="!canSubmit()"> + :submit-disabled="!canSubmit()" + kind="danger" + @submit="onSubmit"> <template slot="body" @@ -101,9 +101,9 @@ Once you confirm %{deleteAccount}, it cannot be undone or recovered.`), value="delete" /> <input + :value="csrfToken" type="hidden" name="authenticity_token" - :value="csrfToken" /> <p @@ -114,18 +114,18 @@ Once you confirm %{deleteAccount}, it cannot be undone or recovered.`), <input v-if="confirmWithPassword" + v-model="enteredPassword" name="password" class="form-control" type="password" - v-model="enteredPassword" aria-labelledby="input-label" /> <input v-else + v-model="enteredUsername" name="username" class="form-control" type="text" - v-model="enteredUsername" aria-labelledby="input-label" /> </form> diff --git a/app/assets/javascripts/profile/account/components/update_username.vue b/app/assets/javascripts/profile/account/components/update_username.vue index b37febe523c..ef484ddfd61 100644 --- a/app/assets/javascripts/profile/account/components/update_username.vue +++ b/app/assets/javascripts/profile/account/components/update_username.vue @@ -93,10 +93,10 @@ Please update your Git repository remotes as soon as possible.`), </div> <input :id="$options.inputId" - class="form-control" - required="required" v-model="newUsername" :disabled="isRequestPending" + class="form-control" + required="required" /> </div> <p class="form-text text-muted"> @@ -105,18 +105,18 @@ Please update your Git repository remotes as soon as possible.`), </div> <button :data-target="`#${$options.modalId}`" + :disabled="isRequestPending || newUsername === username" class="btn btn-warning" type="button" data-toggle="modal" - :disabled="isRequestPending || newUsername === username" > {{ $options.buttonText }} </button> <gl-modal :id="$options.modalId" :header-title-text="s__('Profiles|Change username') + '?'" - footer-primary-button-variant="warning" :footer-primary-button-text="$options.buttonText" + footer-primary-button-variant="warning" @submit="onConfirm" > <span v-html="modalText"></span> diff --git a/app/assets/javascripts/profile/gl_crop.js b/app/assets/javascripts/profile/gl_crop.js index 8f93156cdd1..ba120c4bbdf 100644 --- a/app/assets/javascripts/profile/gl_crop.js +++ b/app/assets/javascripts/profile/gl_crop.js @@ -139,6 +139,8 @@ import _ from 'underscore'; var array, binary, i, k, len, v; binary = atob(dataURL.split(',')[1]); array = []; + + // eslint-disable-next-line no-multi-assign for (k = i = 0, len = binary.length; i < len; k = (i += 1)) { v = binary[k]; array.push(binary.charCodeAt(k)); diff --git a/app/assets/javascripts/project_find_file.js b/app/assets/javascripts/project_find_file.js index 4c4acd487f8..17497283695 100644 --- a/app/assets/javascripts/project_find_file.js +++ b/app/assets/javascripts/project_find_file.js @@ -91,6 +91,8 @@ export default class ProjectFindFile { var blobItemUrl, filePath, html, i, j, len, matches, results; this.element.find(".tree-table > tbody").empty(); results = []; + + // eslint-disable-next-line no-multi-assign for (i = j = 0, len = filePaths.length; j < len; i = (j += 1)) { filePath = filePaths[i]; if (i === 20) { @@ -150,7 +152,7 @@ export default class ProjectFindFile { } goToTree() { - return location.href = this.options.treeUrl; + return window.location.href = this.options.treeUrl; } goToBlob() { diff --git a/app/assets/javascripts/project_import.js b/app/assets/javascripts/project_import.js index d2d26d6f67e..5a0d2b642eb 100644 --- a/app/assets/javascripts/project_import.js +++ b/app/assets/javascripts/project_import.js @@ -2,7 +2,7 @@ import { visitUrl } from './lib/utils/url_utility'; export default function projectImport() { setTimeout(() => { - visitUrl(location.href); + visitUrl(window.location.href); }, 5000); } diff --git a/app/assets/javascripts/project_label_subscription.js b/app/assets/javascripts/project_label_subscription.js index 6f06944ebb6..9049f87e037 100644 --- a/app/assets/javascripts/project_label_subscription.js +++ b/app/assets/javascripts/project_label_subscription.js @@ -3,6 +3,17 @@ import { __ } from './locale'; import axios from './lib/utils/axios_utils'; import flash from './flash'; +const tooltipTitles = { + group: { + subscribed: __('Unsubscribe at group level'), + unsubscribed: __('Subscribe at group level'), + }, + project: { + subscribed: __('Unsubscribe at project level'), + unsubscribed: __('Subscribe at project level'), + }, +}; + export default class ProjectLabelSubscription { constructor(container) { this.$container = $(container); @@ -15,12 +26,10 @@ export default class ProjectLabelSubscription { event.preventDefault(); const $btn = $(event.currentTarget); - const $span = $btn.find('span'); const url = $btn.attr('data-url'); const oldStatus = $btn.attr('data-status'); $btn.addClass('disabled'); - $span.toggleClass('hidden'); axios.post(url).then(() => { let newStatus; @@ -32,21 +41,28 @@ export default class ProjectLabelSubscription { [newStatus, newAction] = ['unsubscribed', 'Subscribe']; } - $span.toggleClass('hidden'); $btn.removeClass('disabled'); this.$buttons.attr('data-status', newStatus); this.$buttons.find('> span').text(newAction); - this.$buttons.map((button) => { + this.$buttons.map((i, button) => { const $button = $(button); + const originalTitle = $button.attr('data-original-title'); - if ($button.attr('data-original-title')) { - $button.tooltip('hide').attr('data-original-title', newAction).tooltip('_fixTitle'); + if (originalTitle) { + ProjectLabelSubscription.setNewTitle($button, originalTitle, newStatus, newAction); } return button; }); }).catch(() => flash(__('There was an error subscribing to this label.'))); } + + static setNewTitle($button, originalTitle, newStatus) { + const type = /group/.test(originalTitle) ? 'group' : 'project'; + const newTitle = tooltipTitles[type][newStatus]; + + $button.attr('title', newTitle).tooltip('_fixTitle'); + } } diff --git a/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_machine_type_dropdown.vue b/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_machine_type_dropdown.vue index ab7d2d41ece..d4497924ad8 100644 --- a/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_machine_type_dropdown.vue +++ b/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_machine_type_dropdown.vue @@ -89,14 +89,13 @@ export default { <div> <div class="js-gcp-machine-type-dropdown dropdown" - :class="{ 'gl-show-field-errors': hasErrors }" > <dropdown-hidden-input :name="fieldName" :value="selectedMachineType" /> <dropdown-button - :class="{ 'gl-field-error-outline': hasErrors }" + :class="{ 'border-danger': hasErrors }" :is-disabled="isDisabled" :is-loading="isLoading" :toggle-text="toggleText" @@ -132,9 +131,12 @@ export default { </div> </div> <span - class="form-text text-muted" - :class="{ 'gl-field-error': hasErrors }" v-if="hasErrors" + :class="{ + 'text-danger': hasErrors, + 'text-muted': !hasErrors + }" + class="form-text" > {{ errorMessage }} </span> diff --git a/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_project_id_dropdown.vue b/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_project_id_dropdown.vue index 25350ef0fa9..08d0a122579 100644 --- a/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_project_id_dropdown.vue +++ b/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_project_id_dropdown.vue @@ -147,7 +147,6 @@ export default { <div> <div class="js-gcp-project-id-dropdown dropdown" - :class="{ 'gl-show-field-errors': hasErrors }" > <dropdown-hidden-input :name="fieldName" @@ -155,7 +154,7 @@ export default { /> <dropdown-button :class="{ - 'gl-field-error-outline': hasErrors, + 'border-danger': hasErrors, 'read-only': hasOneProject }" :is-disabled="isDisabled" @@ -193,8 +192,11 @@ export default { </div> </div> <span - class="form-text text-muted" - :class="{ 'gl-field-error': hasErrors }" + :class="{ + 'text-danger': hasErrors, + 'text-muted': !hasErrors + }" + class="form-text" v-html="helpText" ></span> </div> diff --git a/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_zone_dropdown.vue b/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_zone_dropdown.vue index 8ee4eefcd91..b5476684c6a 100644 --- a/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_zone_dropdown.vue +++ b/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_zone_dropdown.vue @@ -63,14 +63,13 @@ export default { <div> <div class="js-gcp-zone-dropdown dropdown" - :class="{ 'gl-show-field-errors': hasErrors }" > <dropdown-hidden-input :name="fieldName" :value="selectedZone" /> <dropdown-button - :class="{ 'gl-field-error-outline': hasErrors }" + :class="{ 'border-danger': hasErrors }" :is-disabled="isDisabled" :is-loading="isLoading" :toggle-text="toggleText" @@ -106,9 +105,12 @@ export default { </div> </div> <span - class="form-text text-muted" - :class="{ 'gl-field-error': hasErrors }" v-if="hasErrors" + :class="{ + 'text-danger': hasErrors, + 'text-muted': !hasErrors + }" + class="form-text" > {{ errorMessage }} </span> diff --git a/app/assets/javascripts/projects/project_new.js b/app/assets/javascripts/projects/project_new.js index 888b1d6ce33..002edb4663c 100644 --- a/app/assets/javascripts/projects/project_new.js +++ b/app/assets/javascripts/projects/project_new.js @@ -90,7 +90,7 @@ const bindEvents = () => { function chooseTemplate() { $('.template-option').hide(); $projectFieldsForm.addClass('selected'); - $selectedIcon.removeClass('active'); + $selectedIcon.removeClass('d-block'); const value = $(this).val(); const templates = { rails: { @@ -109,7 +109,7 @@ const bindEvents = () => { const selectedTemplate = templates[value]; $selectedTemplateText.text(selectedTemplate.text); - $(selectedTemplate.icon).addClass('active'); + $(selectedTemplate.icon).addClass('d-block'); $templateProjectNameInput.focus(); } diff --git a/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue b/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue index 63f20a0041d..c772fca14bb 100644 --- a/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue +++ b/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue @@ -100,9 +100,9 @@ <template> <div> <loading-icon + v-if="isLoading" label="Loading pipeline status" size="3" - v-if="isLoading" /> <a v-else @@ -112,8 +112,8 @@ v-tooltip :title="statusTitle" :aria-label="statusTitle" - data-container="body" :status="ciStatus" + data-container="body" /> </a> </div> diff --git a/app/assets/javascripts/projects_dropdown/components/app.vue b/app/assets/javascripts/projects_dropdown/components/app.vue index 0bbd8a41753..73d49488299 100644 --- a/app/assets/javascripts/projects_dropdown/components/app.vue +++ b/app/assets/javascripts/projects_dropdown/components/app.vue @@ -132,14 +132,14 @@ export default { <div> <search/> <loading-icon - class="loading-animation prepend-top-20" - size="2" v-if="isLoadingProjects" :label="s__('ProjectsDropdown|Loading projects')" + class="loading-animation prepend-top-20" + size="2" /> <div - class="section-header" v-if="isFrequentsListVisible" + class="section-header" > {{ s__('ProjectsDropdown|Frequently visited') }} </div> diff --git a/app/assets/javascripts/projects_dropdown/components/projects_list_frequent.vue b/app/assets/javascripts/projects_dropdown/components/projects_list_frequent.vue index 246dbeaaded..625e0aa548c 100644 --- a/app/assets/javascripts/projects_dropdown/components/projects_list_frequent.vue +++ b/app/assets/javascripts/projects_dropdown/components/projects_list_frequent.vue @@ -37,14 +37,14 @@ class="list-unstyled" > <li - class="section-empty" v-if="isListEmpty" + class="section-empty" > {{ listEmptyMessage }} </li> <projects-list-item - v-else v-for="(project, index) in projects" + v-else :key="index" :project-id="project.id" :project-name="project.name" diff --git a/app/assets/javascripts/projects_dropdown/components/projects_list_item.vue b/app/assets/javascripts/projects_dropdown/components/projects_list_item.vue index 759cdd1ded9..eafbf6c99e2 100644 --- a/app/assets/javascripts/projects_dropdown/components/projects_list_item.vue +++ b/app/assets/javascripts/projects_dropdown/components/projects_list_item.vue @@ -79,36 +79,36 @@ class="projects-list-item-container" > <a - class="clearfix" :href="webUrl" + class="clearfix" > <div class="project-item-avatar-container" > <img v-if="hasAvatar" - class="avatar s32" :src="avatarUrl" + class="avatar s32" /> <identicon v-else - size-class="s32" :entity-id="projectId" :entity-name="projectName" + size-class="s32" /> </div> <div class="project-item-metadata-container" > <div - class="project-title" :title="projectName" + class="project-title" v-html="highlightedProjectName" > </div> <div - class="project-namespace" :title="namespace" + class="project-namespace" >{{ truncatedNamespace }}</div> </div> </a> diff --git a/app/assets/javascripts/projects_dropdown/components/projects_list_search.vue b/app/assets/javascripts/projects_dropdown/components/projects_list_search.vue index 8d0c29177e6..76e9cb9e53f 100644 --- a/app/assets/javascripts/projects_dropdown/components/projects_list_search.vue +++ b/app/assets/javascripts/projects_dropdown/components/projects_list_search.vue @@ -48,8 +48,8 @@ export default { {{ listEmptyMessage }} </li> <projects-list-item - v-else v-for="(project, index) in projects" + v-else :key="index" :project-id="project.id" :project-name="project.name" diff --git a/app/assets/javascripts/projects_dropdown/components/search.vue b/app/assets/javascripts/projects_dropdown/components/search.vue index 7fcd62dcdb8..28f2a18f2a6 100644 --- a/app/assets/javascripts/projects_dropdown/components/search.vue +++ b/app/assets/javascripts/projects_dropdown/components/search.vue @@ -49,11 +49,11 @@ class="search-input-container d-none d-sm-block" > <input - type="search" - class="form-control" ref="search" v-model="searchQuery" :placeholder="s__('ProjectsDropdown|Search your projects')" + type="search" + class="form-control" /> <i v-if="!searchQuery" diff --git a/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js b/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js index 1a75fdd75db..078ccbbbac2 100644 --- a/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js +++ b/app/assets/javascripts/prometheus_metrics/prometheus_metrics.js @@ -107,7 +107,7 @@ export default class PrometheusMetrics { if (data && data.success) { stop(data); } else { - this.backOffRequestCounter = this.backOffRequestCounter += 1; + this.backOffRequestCounter += 1; if (this.backOffRequestCounter < 3) { next(); } else { diff --git a/app/assets/javascripts/registry/components/app.vue b/app/assets/javascripts/registry/components/app.vue index ea0f7199a70..31f88675912 100644 --- a/app/assets/javascripts/registry/components/app.vue +++ b/app/assets/javascripts/registry/components/app.vue @@ -48,8 +48,8 @@ /> <collapsible-container - v-else-if="!isLoading && repos.length" v-for="(item, index) in repos" + v-else-if="!isLoading && repos.length" :key="index" :repo="item" /> diff --git a/app/assets/javascripts/registry/components/collapsible_container.vue b/app/assets/javascripts/registry/components/collapsible_container.vue index 2fc3778820b..4116c4a0489 100644 --- a/app/assets/javascripts/registry/components/collapsible_container.vue +++ b/app/assets/javascripts/registry/components/collapsible_container.vue @@ -62,15 +62,15 @@ <div class="container-image-head"> <button type="button" - @click="toggleRepo" class="js-toggle-repo btn-link" + @click="toggleRepo" > <i - class="fa" :class="{ 'fa-chevron-right': !isOpen, 'fa-chevron-up': isOpen, }" + class="fa" aria-hidden="true" > </i> @@ -86,12 +86,12 @@ <div class="controls d-none d-sm-block float-right"> <button + v-tooltip v-if="repo.canDelete" - type="button" - class="js-remove-repo btn btn-danger" :title="s__('ContainerRegistry|Remove repository')" :aria-label="s__('ContainerRegistry|Remove repository')" - v-tooltip + type="button" + class="js-remove-repo btn btn-danger" @click="handleDeleteRepository" > <i diff --git a/app/assets/javascripts/registry/components/table_registry.vue b/app/assets/javascripts/registry/components/table_registry.vue index e4a4b3bb129..9f4973c3490 100644 --- a/app/assets/javascripts/registry/components/table_registry.vue +++ b/app/assets/javascripts/registry/components/table_registry.vue @@ -118,13 +118,13 @@ <td class="content"> <button + v-tooltip v-if="item.canDelete" - type="button" - class="js-delete-registry btn btn-danger d-none d-sm-block float-right" :title="s__('ContainerRegistry|Remove tag')" :aria-label="s__('ContainerRegistry|Remove tag')" + type="button" + class="js-delete-registry btn btn-danger d-none d-sm-block float-right" data-container="body" - v-tooltip @click="handleDeleteRegistry(item)" > <i diff --git a/app/assets/javascripts/search_autocomplete.js b/app/assets/javascripts/search_autocomplete.js index 2da022fde63..ef3c71eeafe 100644 --- a/app/assets/javascripts/search_autocomplete.js +++ b/app/assets/javascripts/search_autocomplete.js @@ -438,7 +438,7 @@ export default class SearchAutocomplete { } onClick(item, $el, e) { - if (location.pathname.indexOf(item.url) !== -1) { + if (window.location.pathname.indexOf(item.url) !== -1) { if (!e.metaKey) e.preventDefault(); if (!this.badgePresent) { if (item.category === 'Projects') { diff --git a/app/assets/javascripts/settings_panels.js b/app/assets/javascripts/settings_panels.js index eecde4550f9..37b4a2a4c63 100644 --- a/app/assets/javascripts/settings_panels.js +++ b/app/assets/javascripts/settings_panels.js @@ -42,8 +42,8 @@ export default function initSettingsPanels() { } }); - if (location.hash) { - const $target = $(location.hash); + if (window.location.hash) { + const $target = $(window.location.hash); if ($target.length && $target.hasClass('settings')) { expandSection($target); } diff --git a/app/assets/javascripts/shortcuts_find_file.js b/app/assets/javascripts/shortcuts_find_file.js index 1e246a56b85..8658081c6c2 100644 --- a/app/assets/javascripts/shortcuts_find_file.js +++ b/app/assets/javascripts/shortcuts_find_file.js @@ -13,8 +13,8 @@ export default class ShortcutsFindFile extends ShortcutsNavigation { element === this.projectFindFile.inputElement[0] && (combo === 'up' || combo === 'down' || combo === 'esc' || combo === 'enter') ) { - // when press up/down key in textbox, cusor prevent to move to home/end - event.preventDefault(); + // when press up/down key in textbox, cursor prevent to move to home/end + e.preventDefault(); return false; } diff --git a/app/assets/javascripts/sidebar/components/assignees/assignees.vue b/app/assets/javascripts/sidebar/components/assignees/assignees.vue index 5a374d84796..d22a1e1ac66 100644 --- a/app/assets/javascripts/sidebar/components/assignees/assignees.vue +++ b/app/assets/javascripts/sidebar/components/assignees/assignees.vue @@ -125,13 +125,13 @@ export default { <template> <div> <div - class="sidebar-collapsed-icon sidebar-collapsed-user" - :class="{ 'multiple-users': hasMoreThanOneAssignee }" v-tooltip + :class="{ 'multiple-users': hasMoreThanOneAssignee }" + :title="collapsedTooltipTitle" + class="sidebar-collapsed-icon sidebar-collapsed-user" data-container="body" data-placement="left" data-boundary="viewport" - :title="collapsedTooltipTitle" > <i v-if="hasNoUsers" @@ -140,17 +140,17 @@ export default { > </i> <button - type="button" - class="btn-link" v-for="(user, index) in users" v-if="shouldRenderCollapsedAssignee(index)" :key="user.id" + type="button" + class="btn-link" > <img - width="24" - class="avatar avatar-inline s24" :alt="assigneeAlt(user)" :src="avatarUrl(user)" + width="24" + class="avatar avatar-inline s24" /> <span class="author"> {{ user.name }} @@ -186,14 +186,14 @@ export default { </template> <template v-else-if="hasOneUser"> <a - class="author_link bold" :href="assigneeUrl(firstUser)" + class="author_link bold" > <img - width="32" - class="avatar avatar-inline s32" :alt="assigneeAlt(firstUser)" :src="avatarUrl(firstUser)" + width="32" + class="avatar avatar-inline s32" /> <span class="author"> {{ firstUser.name }} @@ -206,23 +206,23 @@ export default { <template v-else> <div class="user-list"> <div - class="user-item" v-for="(user, index) in users" v-if="renderAssignee(index)" :key="user.id" + class="user-item" > <a + :href="assigneeUrl(user)" + :data-title="user.name" class="user-link has-tooltip" data-container="body" data-placement="bottom" - :href="assigneeUrl(user)" - :data-title="user.name" > <img - width="32" - class="avatar avatar-inline s32" :alt="assigneeAlt(user)" :src="avatarUrl(user)" + width="32" + class="avatar avatar-inline s32" /> </a> </div> diff --git a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue index b04a2eff798..123c92aff64 100644 --- a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue +++ b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue @@ -90,12 +90,12 @@ export default { /> <assignees v-if="!store.isFetching.assignees" - class="value" :root-path="store.rootPath" :users="store.assignees" :editable="store.editable" - @assign-self="assignSelf" :issuable-type="issuableType" + class="value" + @assign-self="assignSelf" /> </div> </template> diff --git a/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue b/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue index 3f6e2f05396..2b8d6207dea 100644 --- a/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue +++ b/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue @@ -54,7 +54,7 @@ export default { updateConfidentialAttribute(confidential) { this.service .update('issue', { confidential }) - .then(() => location.reload()) + .then(() => window.location.reload()) .catch(() => { Flash( __( @@ -70,13 +70,13 @@ export default { <template> <div class="block issuable-sidebar-item confidentiality"> <div - class="sidebar-collapsed-icon" - @click="toggleForm" v-tooltip + :title="tooltipLabel" + class="sidebar-collapsed-icon" data-container="body" data-placement="left" data-boundary="viewport" - :title="tooltipLabel" + @click="toggleForm" > <icon :name="confidentialityIcon" @@ -104,8 +104,8 @@ export default { v-if="!isConfidential" class="no-value sidebar-item-value"> <icon - name="eye" :size="16" + name="eye" aria-hidden="true" class="sidebar-item-icon inline" /> @@ -115,8 +115,8 @@ export default { v-else class="value sidebar-item-value hide-collapsed"> <icon - name="eye-slash" :size="16" + name="eye-slash" aria-hidden="true" class="sidebar-item-icon inline is-active" /> diff --git a/app/assets/javascripts/sidebar/components/lock/edit_form.vue b/app/assets/javascripts/sidebar/components/lock/edit_form.vue index d392977e5e2..4906dad22e1 100644 --- a/app/assets/javascripts/sidebar/components/lock/edit_form.vue +++ b/app/assets/javascripts/sidebar/components/lock/edit_form.vue @@ -44,14 +44,14 @@ export default { <div class="dropdown show"> <div class="dropdown-menu sidebar-item-warning-message"> <p - class="text" v-if="isLocked" + class="text" v-html="unlockWarning"> </p> <p - class="text" v-else + class="text" v-html="lockWarning"> </p> diff --git a/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue b/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue index fb69c741dcd..8bbc59f623a 100644 --- a/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue +++ b/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue @@ -76,7 +76,7 @@ export default { .update(this.issuableType, { discussion_locked: locked, }) - .then(() => location.reload()) + .then(() => window.location.reload()) .catch(() => Flash( this.__( @@ -94,13 +94,13 @@ export default { <template> <div class="block issuable-sidebar-item lock"> <div - class="sidebar-collapsed-icon" - @click="toggleForm" v-tooltip + :title="tooltipLabel" + class="sidebar-collapsed-icon" data-container="body" data-placement="left" data-boundary="viewport" - :title="tooltipLabel" + @click="toggleForm" > <icon :name="lockIcon" @@ -134,8 +134,8 @@ export default { class="value sidebar-item-value" > <icon - name="lock" :size="16" + name="lock" aria-hidden="true" class="sidebar-item-icon inline is-active" /> @@ -147,8 +147,8 @@ export default { class="no-value sidebar-item-value hide-collapsed" > <icon - name="lock-open" :size="16" + name="lock-open" aria-hidden="true" class="sidebar-item-icon inline" /> diff --git a/app/assets/javascripts/sidebar/components/participants/participants.vue b/app/assets/javascripts/sidebar/components/participants/participants.vue index 0a945fc7fd5..33dd6c981b6 100644 --- a/app/assets/javascripts/sidebar/components/participants/participants.vue +++ b/app/assets/javascripts/sidebar/components/participants/participants.vue @@ -80,12 +80,12 @@ <template> <div> <div - class="sidebar-collapsed-icon" v-tooltip + :title="participantLabel" + class="sidebar-collapsed-icon" data-container="body" data-placement="left" data-boundary="viewport" - :title="participantLabel" @click="onClickCollapsedIcon" > <i @@ -119,15 +119,15 @@ class="participants-author js-participants-author" > <a - class="author_link" :href="participant.web_url" + class="author_link" > <user-avatar-image :lazy="true" :img-src="participant.avatar_url" - css-classes="avatar-inline" :size="24" :tooltip-text="participant.name" + css-classes="avatar-inline" tooltip-placement="bottom" /> </a> diff --git a/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue b/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue index 6745c1aafff..448c8fc3602 100644 --- a/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue +++ b/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue @@ -97,9 +97,9 @@ </span> <toggle-button ref="toggleButton" - class="float-right hide-collapsed js-issuable-subscribe-button" :is-loading="showLoadingState" :value="subscribed" + class="float-right hide-collapsed js-issuable-subscribe-button" @change="toggleSubscription" /> </div> diff --git a/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue b/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue index 209af1ce152..1d030c4f67f 100644 --- a/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue +++ b/app/assets/javascripts/sidebar/components/time_tracking/collapsed_state.vue @@ -110,12 +110,12 @@ <template> <div - class="sidebar-collapsed-icon" v-tooltip + :title="tooltipText" + class="sidebar-collapsed-icon" data-container="body" data-placement="left" data-boundary="viewport" - :title="tooltipText" > <icon name="timer" /> <div class="time-tracking-collapsed-summary"> diff --git a/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue b/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue index 6f79310b1cc..d335c3f55af 100644 --- a/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue +++ b/app/assets/javascripts/sidebar/components/time_tracking/comparison_pane.vue @@ -1,8 +1,12 @@ <script> import { parseSeconds, stringifyTime } from '../../../lib/utils/pretty_time'; +import tooltip from '../../../vue_shared/directives/tooltip'; export default { name: 'TimeTrackingComparisonPane', + directives: { + tooltip, + }, props: { timeSpent: { type: Number, @@ -50,19 +54,17 @@ export default { <template> <div class="time-tracking-comparison-pane"> <div + v-tooltip + :title="timeRemainingTooltip" + :class="timeRemainingStatusClass" class="compare-meter" data-toggle="tooltip" data-placement="top" role="timeRemainingDisplay" - :aria-valuenow="timeRemainingTooltip" - :title="timeRemainingTooltip" - :data-original-title="timeRemainingTooltip" - :class="timeRemainingStatusClass" > <div - class="meter-container" - role="timeSpentPercent" :aria-valuenow="timeRemainingPercent" + class="meter-container" > <div :style="{ width: timeRemainingPercent }" diff --git a/app/assets/javascripts/sidebar/components/time_tracking/help_state.vue b/app/assets/javascripts/sidebar/components/time_tracking/help_state.vue index 825063d9ba6..19ec0f05a26 100644 --- a/app/assets/javascripts/sidebar/components/time_tracking/help_state.vue +++ b/app/assets/javascripts/sidebar/components/time_tracking/help_state.vue @@ -45,8 +45,8 @@ export default { <p v-html="spendText"> </p> <a - class="btn btn-default learn-more-button" :href="href" + class="btn btn-default learn-more-button" > {{ __('Learn more') }} </a> diff --git a/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue b/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue index 7d56d2fa5ee..ca3b9338c29 100644 --- a/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue +++ b/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue @@ -101,8 +101,8 @@ export default { <template> <div - class="time_tracker time-tracking-component-wrap" v-cloak + class="time_tracker time-tracking-component-wrap" > <time-tracking-collapsed-state :show-comparison-state="showComparisonState" @@ -116,8 +116,8 @@ export default { <div class="title hide-collapsed"> {{ __('Time tracking') }} <div - class="help-button float-right" v-if="!showHelpState" + class="help-button float-right" @click="toggleHelpState(true)" > <i @@ -127,8 +127,8 @@ export default { </i> </div> <div - class="close-help-button float-right" v-if="showHelpState" + class="close-help-button float-right" @click="toggleHelpState(false)" > <i diff --git a/app/assets/javascripts/sidebar/sidebar_mediator.js b/app/assets/javascripts/sidebar/sidebar_mediator.js index d86557e870a..d9ca5e46770 100644 --- a/app/assets/javascripts/sidebar/sidebar_mediator.js +++ b/app/assets/javascripts/sidebar/sidebar_mediator.js @@ -80,7 +80,7 @@ export default class SidebarMediator { return this.service.moveIssue(this.store.moveToProjectId) .then(response => response.json()) .then((data) => { - if (location.pathname !== data.web_url) { + if (window.location.pathname !== data.web_url) { visitUrl(data.web_url); } }); diff --git a/app/assets/javascripts/single_file_diff.js b/app/assets/javascripts/single_file_diff.js index 1afff0dba38..ae27c676fa0 100644 --- a/app/assets/javascripts/single_file_diff.js +++ b/app/assets/javascripts/single_file_diff.js @@ -11,7 +11,7 @@ import syntaxHighlight from './syntax_highlight'; const WRAPPER = '<div class="diff-content"></div>'; const LOADING_HTML = '<i class="fa fa-spinner fa-spin"></i>'; const ERROR_HTML = '<div class="nothing-here-block"><i class="fa fa-warning"></i> Could not load diff</div>'; -const COLLAPSED_HTML = '<div class="nothing-here-block diff-collapsed">This diff is collapsed. <a class="click-to-expand">Click to expand it.</a></div>'; +const COLLAPSED_HTML = '<div class="nothing-here-block diff-collapsed">This diff is collapsed. <button class="click-to-expand btn btn-link">Click to expand it.</button></div>'; export default class SingleFileDiff { constructor(file) { diff --git a/app/assets/javascripts/snippet/snippet_embed.js b/app/assets/javascripts/snippet/snippet_embed.js index 81ec483f2d9..873a506a92f 100644 --- a/app/assets/javascripts/snippet/snippet_embed.js +++ b/app/assets/javascripts/snippet/snippet_embed.js @@ -1,5 +1,5 @@ export default () => { - const { protocol, host, pathname } = location; + const { protocol, host, pathname } = window.location; const shareBtn = document.querySelector('.js-share-btn'); const embedBtn = document.querySelector('.js-embed-btn'); const snippetUrlArea = document.querySelector('.js-snippet-url-area'); diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js index cd954f75613..349614460e1 100644 --- a/app/assets/javascripts/users_select.js +++ b/app/assets/javascripts/users_select.js @@ -259,6 +259,7 @@ function UsersSelect(currentUser, els, options = {}) { showDivider = 0; if (firstUser) { // Move current user to the front of the list + // eslint-disable-next-line no-multi-assign for (index = j = 0, len = users.length; j < len; index = (j += 1)) { obj = users[index]; if (obj.username === firstUser) { @@ -561,6 +562,8 @@ function UsersSelect(currentUser, els, options = {}) { if (firstUser) { // Move current user to the front of the list ref = data.results; + + // eslint-disable-next-line no-multi-assign for (index = j = 0, len = ref.length; j < len; index = (j += 1)) { obj = ref[index]; if (obj.username === firstUser) { diff --git a/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue b/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue index 1608858da22..c44419d24e6 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue @@ -118,8 +118,8 @@ export default { </a> </template> <span - v-if="hasDeploymentTime" v-tooltip + v-if="hasDeploymentTime" :title="deployment.deployed_at_formatted" class="js-deploy-time" > @@ -127,9 +127,9 @@ export default { </span> <loading-button v-if="deployment.stop_url" + :loading="isStopping" container-class="btn btn-default btn-sm prepend-left-default" label="Stop environment" - :loading="isStopping" @click="stopEnvironment" /> </div> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/memory_usage.vue b/app/assets/javascripts/vue_merge_request_widget/components/memory_usage.vue index f012f9c6772..5e76f6b1cac 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/memory_usage.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/memory_usage.vue @@ -105,7 +105,7 @@ export default { MRWidgetService.fetchMetrics(this.metricsUrl) .then((res) => { if (res.status === statusCodes.NO_CONTENT) { - this.backOffRequestCounter = this.backOffRequestCounter += 1; + this.backOffRequestCounter += 1; /* eslint-disable no-unused-expressions */ this.backOffRequestCounter < 3 ? next() : stop(res); } else { diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author.vue index 8338fde61c7..22c2f74f900 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author.vue @@ -35,17 +35,17 @@ <template> <a :href="authorUrl" - class="author-link inline" :v-tooltip="showAuthorTooltip" :title="author.name" + class="author-link inline" > <img :src="avatarUrl" class="avatar avatar-inline s16" /> <span - class="author" v-if="showAuthorName" + class="author" > {{ author.name }} </span> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author_time.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author_time.vue index 644e4b7d81a..ba16cb9e2c8 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author_time.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author_time.vue @@ -1,11 +1,15 @@ <script> + import tooltip from '~/vue_shared/directives/tooltip'; import MrWidgetAuthor from './mr_widget_author.vue'; export default { - name: 'MRWidgetAuthorTime', + name: 'MrWidgetAuthorTime', components: { MrWidgetAuthor, }, + directives: { + tooltip, + }, props: { actionText: { type: String, @@ -31,9 +35,8 @@ {{ actionText }} <mr-widget-author :author="author" /> <time + v-tooltip :title="dateTitle" - data-toggle="tooltip" - data-placement="top" data-container="body" > {{ dateReadable }} diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue index 51a0fda6555..3ce9d8dc26a 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue @@ -59,11 +59,11 @@ export default { <strong> {{ s__("mrWidget|Request to merge") }} <span - class="label-branch js-source-branch" :class="{ 'label-truncated': isSourceBranchLong }" :title="isSourceBranchLong ? mr.sourceBranch : ''" - data-placement="bottom" :v-tooltip="isSourceBranchLong" + class="label-branch js-source-branch" + data-placement="bottom" v-html="mr.sourceBranchLink" > </span> @@ -77,10 +77,10 @@ export default { {{ s__("mrWidget|into") }} <span - class="label-branch" :v-tooltip="isTargetBranchLong" :class="{ 'label-truncatedtooltip': isTargetBranchLong }" :title="isTargetBranchLong ? mr.targetBranch : ''" + class="label-branch" data-placement="bottom" > <a @@ -108,9 +108,9 @@ export default { {{ s__("mrWidget|Web IDE") }} </a> <button + :disabled="mr.sourceBranchRemoved" data-target="#modal_merge_info" data-toggle="modal" - :disabled="mr.sourceBranchRemoved" class="btn btn-sm btn-default inline js-check-out-branch" type="button" > @@ -134,8 +134,8 @@ export default { <ul class="dropdown-menu dropdown-menu-right"> <li> <a - class="js-download-email-patches" :href="mr.emailPatchesPath" + class="js-download-email-patches" download > {{ s__("mrWidget|Email patches") }} @@ -143,8 +143,8 @@ export default { </li> <li> <a - class="js-download-plain-diff" :href="mr.plainDiffPath" + class="js-download-plain-diff" download > {{ s__("mrWidget|Plain diff") }} diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue index 48dff8c4916..2f0b5e12c12 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue @@ -67,8 +67,8 @@ export default { </template> <template v-else-if="hasPipeline"> <a - class="append-right-10" :href="status.details_path" + class="append-right-10" > <ci-icon :status="status" /> </a> @@ -96,8 +96,8 @@ export default { <span class="mr-widget-pipeline-graph"> <span - class="stage-cell" v-if="hasStages" + class="stage-cell" > <div v-for="(stage, i) in pipeline.details.stages" diff --git a/app/assets/javascripts/vue_merge_request_widget/components/source_branch_removal_status.vue b/app/assets/javascripts/vue_merge_request_widget/components/source_branch_removal_status.vue index 460437ceeff..56879c04d16 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/source_branch_removal_status.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/source_branch_removal_status.vue @@ -25,9 +25,9 @@ </span> <i v-tooltip - class="fa fa-question-circle" :title="tooltipTitle" :aria-label="tooltipTitle" + class="fa fa-question-circle" > </i> </p> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue index b404a592234..2133124347c 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue @@ -39,10 +39,10 @@ {{ s__("mrWidget|This merge request failed to be merged automatically") }} </span> <button - @click="refreshWidget" :disabled="isRefreshing" type="button" class="btn btn-sm btn-default" + @click="refreshWidget" > <loading-icon v-if="isRefreshing" diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_checking.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_checking.vue index caeaac75b45..ae6630dcd6f 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_checking.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_checking.vue @@ -11,8 +11,8 @@ <template> <div class="mr-widget-body media"> <status-icon - status="loading" :show-disabled-button="true" + status="loading" /> <div class="media-body space-children"> <span class="bold"> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.vue index 68b691fc914..25a57e520ee 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.vue @@ -1,11 +1,11 @@ <script> - import mrWidgetAuthorTime from '../../components/mr_widget_author_time.vue'; + import MrWidgetAuthorTime from '../../components/mr_widget_author_time.vue'; import statusIcon from '../mr_widget_status_icon.vue'; export default { name: 'MRWidgetClosed', components: { - mrWidgetAuthorTime, + MrWidgetAuthorTime, statusIcon, }, props: { diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue index 5c1500ab801..dff9ec657b9 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue @@ -20,8 +20,8 @@ <template> <div class="mr-widget-body media"> <status-icon - status="warning" :show-disabled-button="true" + status="warning" /> <div class="media-body space-children"> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue index 05fecd4de35..c302960f16e 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue @@ -1,5 +1,6 @@ <script> import { n__ } from '~/locale'; +import { stripHtml } from '~/lib/utils/text_utility'; import statusIcon from '../mr_widget_status_icon.vue'; import eventHub from '../../event_hub'; @@ -27,6 +28,9 @@ export default { }, computed: { + mergeError() { + return this.mr.mergeError ? stripHtml(this.mr.mergeError, ' ').trim() : ''; + }, timerText() { return n__( 'Refreshing in a second to show the updated status...', @@ -76,16 +80,16 @@ export default { </template> <template v-else> <status-icon - status="warning" :show-disabled-button="true" + status="warning" /> <div class="media-body space-children"> <span class="bold"> <span - class="has-error-message" v-if="mr.mergeError" + class="has-error-message" > - {{ mr.mergeError }}. + {{ mergeError }}. </span> <span v-else> {{ s__("mrWidget|Merge failed.") }} @@ -97,9 +101,9 @@ export default { </span> </span> <button - @click="refresh" class="btn btn-default btn-sm js-refresh-button" type="button" + @click="refresh" > {{ s__("mrWidget|Refresh now") }} </button> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.vue index e8352c362d6..0d9a560c88e 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.vue @@ -90,11 +90,11 @@ </span> <a v-if="mr.canCancelAutomaticMerge" - @click.prevent="cancelAutomaticMerge" :disabled="isCancellingAutoMerge" role="button" href="#" - class="btn btn-sm btn-default js-cancel-auto-merge"> + class="btn btn-sm btn-default js-cancel-auto-merge" + @click.prevent="cancelAutomaticMerge"> <i v-if="isCancellingAutoMerge" class="fa fa-spinner fa-spin" @@ -127,10 +127,10 @@ <a v-if="canRemoveSourceBranch" :disabled="isRemovingSourceBranch" - @click.prevent="removeSourceBranch" role="button" class="btn btn-sm btn-default js-remove-source-branch" href="#" + @click.prevent="removeSourceBranch" > <i v-if="isRemovingSourceBranch" diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue index eb581b807a2..985f44dee97 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue @@ -4,7 +4,7 @@ import loadingIcon from '~/vue_shared/components/loading_icon.vue'; import { s__, __ } from '~/locale'; import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; - import mrWidgetAuthorTime from '../../components/mr_widget_author_time.vue'; + import MrWidgetAuthorTime from '../../components/mr_widget_author_time.vue'; import statusIcon from '../mr_widget_status_icon.vue'; import eventHub from '../../event_hub'; @@ -14,7 +14,7 @@ tooltip, }, components: { - mrWidgetAuthorTime, + MrWidgetAuthorTime, loadingIcon, statusIcon, ClipboardButton, @@ -116,44 +116,44 @@ :date-readable="mr.metrics.readableMergedAt" /> <a - v-if="mr.canRevertInCurrentMR" v-tooltip + v-if="mr.canRevertInCurrentMR" + :title="revertTitle" class="btn btn-close btn-sm" href="#modal-revert-commit" data-toggle="modal" data-container="body" - :title="revertTitle" > {{ revertLabel }} </a> <a - v-else-if="mr.revertInForkPath" v-tooltip - class="btn btn-close btn-sm" - data-method="post" + v-else-if="mr.revertInForkPath" :href="mr.revertInForkPath" :title="revertTitle" + class="btn btn-close btn-sm" + data-method="post" > {{ revertLabel }} </a> <a - v-if="mr.canCherryPickInCurrentMR" v-tooltip + v-if="mr.canCherryPickInCurrentMR" + :title="cherryPickTitle" class="btn btn-default btn-sm" href="#modal-cherry-pick-commit" data-toggle="modal" data-container="body" - :title="cherryPickTitle" > {{ cherryPickLabel }} </a> <a - v-else-if="mr.cherryPickInForkPath" v-tooltip - class="btn btn-default btn-sm" - data-method="post" + v-else-if="mr.cherryPickInForkPath" :href="mr.cherryPickInForkPath" :title="cherryPickTitle" + class="btn btn-default btn-sm" + data-method="post" > {{ cherryPickLabel }} </a> @@ -186,10 +186,10 @@ > <span>{{ s__("mrWidget|You can remove source branch now") }}</span> <button - @click="removeSourceBranch" :disabled="isMakingRequest" type="button" class="btn btn-sm btn-default js-remove-branch-button" + @click="removeSourceBranch" > {{ s__("mrWidget|Remove Source Branch") }} </button> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.vue index 718c0e4b3c6..b0e96f74626 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.vue @@ -39,8 +39,8 @@ <template> <div class="mr-widget-body media"> <status-icon - status="warning" :show-disabled-button="true" + status="warning" /> <div class="media-body space-children"> @@ -51,9 +51,9 @@ {{ missingBranchNameMessage }} <i v-tooltip - class="fa fa-question-circle" :title="message" :aria-label="message" + class="fa fa-question-circle" > </i> </span> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_not_allowed.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_not_allowed.vue index e4af50b09f8..92eee2cf5dd 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_not_allowed.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_not_allowed.vue @@ -12,8 +12,8 @@ <template> <div class="mr-widget-body media"> <status-icon - status="success" :show-disabled-button="true" + status="success" /> <div class="media-body space-children"> <span class="bold"> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.vue index 6d7cc03f7ad..37ee5215cea 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.vue @@ -11,8 +11,8 @@ <template> <div class="mr-widget-body media"> <status-icon - status="warning" :show-disabled-button="true" + status="warning" /> <div class="media-body space-children"> <span class="bold"> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue index 143fd328d88..2d8c3d6be87 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue @@ -110,9 +110,9 @@ js-toggle-container accept-action media space-children" > <button + :disabled="isMakingRequest" type="button" class="btn btn-sm btn-reopen btn-success qa-mr-rebase-button" - :disabled="isMakingRequest" @click="rebase" > <loading-icon v-if="isMakingRequest" /> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_squash_before_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_squash_before_merge.vue index 875c3323dbb..25c1044fe2b 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_squash_before_merge.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_squash_before_merge.vue @@ -37,23 +37,23 @@ export default { <div class="accept-control inline"> <label class="merge-param-checkbox"> <input + :disabled="isMergeButtonDisabled" + v-model="squashBeforeMerge" type="checkbox" name="squash" class="qa-squash-checkbox" - :disabled="isMergeButtonDisabled" - v-model="squashBeforeMerge" @change="updateSquashModel" /> {{ __('Squash commits') }} </label> <a + v-tooltip :href="mr.squashBeforeMergeHelpPath" data-title="About this feature" data-placement="bottom" target="_blank" rel="noopener noreferrer nofollow" data-container="body" - v-tooltip > <icon name="question-o" diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/pipeline_failed.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/pipeline_failed.vue index 8d55477929f..2bb1a34412e 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/pipeline_failed.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/pipeline_failed.vue @@ -12,8 +12,8 @@ export default { <template> <div class="mr-widget-body media"> <status-icon - status="warning" :show-disabled-button="true" + status="warning" /> <div class="media-body space-children"> <span class="bold"> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue index 3a194320bd8..fe777a07189 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue @@ -235,11 +235,11 @@ export default { <div class="mr-widget-body-controls media space-children"> <span class="btn-group append-bottom-5"> <button - @click="handleMergeButtonClick()" :disabled="isMergeButtonDisabled" :class="mergeButtonClass" type="button" - class="qa-merge-button"> + class="qa-merge-button" + @click="handleMergeButtonClick()"> <i v-if="isMakingRequest" class="fa fa-spinner fa-spin" @@ -265,28 +265,28 @@ export default { role="menu"> <li> <a - @click.prevent="handleMergeButtonClick(true)" class="merge_when_pipeline_succeeds" - href="#"> + href="#" + @click.prevent="handleMergeButtonClick(true)"> <span class="media"> <span - v-html="successSvg" class="merge-opt-icon" - aria-hidden="true"></span> + aria-hidden="true" + v-html="successSvg"></span> <span class="media-body merge-opt-title">Merge when pipeline succeeds</span> </span> </a> </li> <li> <a - @click.prevent="handleMergeButtonClick(false, true)" class="accept-merge-request" - href="#"> + href="#" + @click.prevent="handleMergeButtonClick(false, true)"> <span class="media"> <span - v-html="warningSvg" class="merge-opt-icon" - aria-hidden="true"></span> + aria-hidden="true" + v-html="warningSvg"></span> <span class="media-body merge-opt-title">Merge immediately</span> </span> </a> @@ -299,8 +299,8 @@ export default { <input id="remove-source-branch-input" v-model="removeSourceBranch" - class="js-remove-source-branch-checkbox" :disabled="isRemoveSourceBranchButtonDisabled" + class="js-remove-source-branch-checkbox" type="checkbox"/> Remove source branch </label> @@ -317,10 +317,10 @@ export default { </span> <button v-else - @click="toggleCommitMessageEditor" :disabled="isMergeButtonDisabled" class="js-modify-commit-message-button btn btn-default btn-sm" - type="button"> + type="button" + @click="toggleCommitMessageEditor"> Modify commit message </button> </template> @@ -356,8 +356,8 @@ export default { </p> <div class="hint"> <a - @click.prevent="updateCommitMessage" href="#" + @click.prevent="updateCommitMessage" > {{ commitMessageLinkTitle }} </a> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/sha_mismatch.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/sha_mismatch.vue index 7cc07401911..16c903c923f 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/sha_mismatch.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/sha_mismatch.vue @@ -12,8 +12,8 @@ export default { <template> <div class="mr-widget-body media"> <status-icon - status="warning" :show-disabled-button="true" + status="warning" /> <div class="media-body space-children"> <span class="bold"> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/unresolved_discussions.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/unresolved_discussions.vue index 8ea3f22ecc2..5eb2058a03b 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/unresolved_discussions.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/unresolved_discussions.vue @@ -18,8 +18,8 @@ export default { <template> <div class="mr-widget-body media"> <status-icon - status="warning" :show-disabled-button="true" + status="warning" /> <div class="media-body space-children"> <span class="bold"> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue index fe2608e8212..89c9a41f316 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue @@ -43,8 +43,8 @@ export default { <template> <div class="mr-widget-body media"> <status-icon - status="warning" :show-disabled-button="Boolean(mr.removeWIPPath)" + status="warning" /> <div class="media-body space-children"> <span class="bold"> @@ -60,10 +60,10 @@ export default { </span> <button v-if="mr.removeWIPPath" - @click="removeWIP" :disabled="isMakingRequest" type="button" - class="btn btn-default btn-xs js-remove-wip"> + class="btn btn-default btn-sm js-remove-wip" + @click="removeWIP"> <i v-if="isMakingRequest" class="fa fa-spinner fa-spin" diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue index f69fe03fcb3..bc4ba3d050b 100644 --- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue +++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue @@ -36,7 +36,7 @@ import { notify, SourceBranchRemovalStatus, } from './dependencies'; -import { setFavicon } from '../lib/utils/common_utils'; +import { setFaviconOverlay } from '../lib/utils/common_utils'; export default { el: '#js-vue-mr-widget', @@ -159,8 +159,9 @@ export default { }, setFaviconHelper() { if (this.mr.ciStatusFaviconPath) { - setFavicon(this.mr.ciStatusFaviconPath); + return setFaviconOverlay(this.mr.ciStatusFaviconPath); } + return Promise.resolve(); }, fetchDeployments() { return this.service.fetchDeployments() @@ -265,10 +266,10 @@ export default { /> <section - v-if="mr.maintainerEditAllowed" + v-if="mr.allowCollaboration" class="mr-info-list mr-links" > - {{ s__("mrWidget|Allows edits from maintainers") }} + {{ s__("mrWidget|Allows commits from members who can merge to the target branch") }} </section> <mr-widget-related-links @@ -282,8 +283,8 @@ export default { /> </div> <div - class="mr-widget-footer" v-if="shouldRenderMergeHelp" + class="mr-widget-footer" > <mr-widget-merge-help /> </div> diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js index e5b7e1f1c68..134aaacf9d2 100644 --- a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js +++ b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js @@ -83,7 +83,7 @@ export default class MergeRequestStore { this.canBeMerged = data.can_be_merged || false; this.isMergeAllowed = data.mergeable || false; this.mergeOngoing = data.merge_ongoing; - this.maintainerEditAllowed = data.allow_maintainer_to_push; + this.allowCollaboration = data.allow_collaboration; // Cherry-pick and Revert actions related this.canCherryPickInCurrentMR = currentUser.can_cherry_pick_on_current_merge_request || false; diff --git a/app/assets/javascripts/vue_shared/components/ci_badge_link.vue b/app/assets/javascripts/vue_shared/components/ci_badge_link.vue index 0d64efcbf68..a2518e2a611 100644 --- a/app/assets/javascripts/vue_shared/components/ci_badge_link.vue +++ b/app/assets/javascripts/vue_shared/components/ci_badge_link.vue @@ -50,9 +50,9 @@ export default { </script> <template> <a + v-tooltip :href="status.details_path" :class="cssClass" - v-tooltip :title="!showText ? status.text : ''" > <ci-icon :status="status" /> diff --git a/app/assets/javascripts/vue_shared/components/clipboard_button.vue b/app/assets/javascripts/vue_shared/components/clipboard_button.vue index cb2cc3901ad..dc5760bce28 100644 --- a/app/assets/javascripts/vue_shared/components/clipboard_button.vue +++ b/app/assets/javascripts/vue_shared/components/clipboard_button.vue @@ -49,14 +49,14 @@ export default { <template> <button - type="button" - class="btn" + v-tooltip :class="cssClass" :title="title" :data-clipboard-text="text" - v-tooltip :data-container="tooltipContainer" :data-placement="tooltipPlacement" + type="button" + class="btn" > <i aria-hidden="true" diff --git a/app/assets/javascripts/vue_shared/components/commit.vue b/app/assets/javascripts/vue_shared/components/commit.vue index 8f250a6c989..13bca99dcb3 100644 --- a/app/assets/javascripts/vue_shared/components/commit.vue +++ b/app/assets/javascripts/vue_shared/components/commit.vue @@ -124,11 +124,11 @@ export default { </div> <a - class="ref-name" - :href="commitRef.ref_url" v-tooltip - data-container="body" + :href="commitRef.ref_url" :title="commitRef.name" + class="ref-name" + data-container="body" > {{ commitRef.name }} </a> @@ -139,8 +139,8 @@ export default { /> <a - class="commit-sha" :href="commitUrl" + class="commit-sha" > {{ shortSha }} </a> @@ -152,15 +152,15 @@ export default { > <user-avatar-link v-if="hasAuthor" - class="avatar-image-container" :link-href="author.path" :img-src="author.avatar_url" :img-alt="userImageAltDescription" :tooltip-text="author.username" + class="avatar-image-container" /> <a - class="commit-row-message" :href="commitUrl" + class="commit-row-message" > {{ title }} </a> diff --git a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/download_viewer.vue b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/download_viewer.vue index 7b5367ac19b..f1ef50d0e3d 100644 --- a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/download_viewer.vue +++ b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/download_viewer.vue @@ -32,7 +32,10 @@ export default { <div class="file-container"> <div class="file-content"> <p class="prepend-top-10 file-info"> - {{ fileName }} ({{ fileSizeReadable }}) + {{ fileName }} + <template v-if="fileSize > 0"> + ({{ fileSizeReadable }}) + </template> </p> <a :href="path" @@ -41,9 +44,9 @@ export default { download target="_blank"> <icon + :size="16" name="download" css-classes="float-left append-right-8" - :size="16" /> {{ __('Download') }} </a> diff --git a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/image_viewer.vue b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/image_viewer.vue index a5999f909ca..6851029018a 100644 --- a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/image_viewer.vue +++ b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/image_viewer.vue @@ -1,4 +1,5 @@ <script> +import _ from 'underscore'; import { numberToHumanSize } from '../../../../lib/utils/number_utils'; export default { @@ -12,6 +13,10 @@ export default { required: false, default: 0, }, + renderInfo: { + type: Boolean, + default: true, + }, }, data() { return { @@ -26,14 +31,34 @@ export default { return numberToHumanSize(this.fileSize); }, }, + beforeDestroy() { + window.removeEventListener('resize', this.resizeThrottled, false); + }, + mounted() { + // The onImgLoad may have happened before the control was actually mounted + this.onImgLoad(); + this.resizeThrottled = _.throttle(this.onImgLoad, 400); + window.addEventListener('resize', this.resizeThrottled, false); + }, methods: { onImgLoad() { const contentImg = this.$refs.contentImg; - this.isZoomable = - contentImg.naturalWidth > contentImg.width || contentImg.naturalHeight > contentImg.height; - this.width = contentImg.naturalWidth; - this.height = contentImg.naturalHeight; + if (contentImg) { + this.isZoomable = + contentImg.naturalWidth > contentImg.width || + contentImg.naturalHeight > contentImg.height; + + this.width = contentImg.naturalWidth; + this.height = contentImg.naturalHeight; + + this.$emit('imgLoaded', { + width: this.width, + height: this.height, + renderedWidth: contentImg.clientWidth, + renderedHeight: contentImg.clientHeight, + }); + } }, onImgClick() { if (this.isZoomable) this.isZoomed = !this.isZoomed; @@ -47,20 +72,22 @@ export default { <div class="file-content image_file"> <img ref="contentImg" - :class="{ 'isZoomable': isZoomable, 'isZoomed': isZoomed }" + :class="{ 'is-zoomable': isZoomable, 'is-zoomed': isZoomed }" :src="path" :alt="path" @load="onImgLoad" @click="onImgClick"/> - <p class="file-info prepend-top-10"> + <p + v-if="renderInfo" + class="file-info prepend-top-10"> <template v-if="fileSize>0"> {{ fileSizeReadable }} </template> <template v-if="fileSize>0 && width && height"> - - + | </template> <template v-if="width && height"> - {{ width }} x {{ height }} + W: {{ width }} | H: {{ height }} </template> </p> </div> diff --git a/app/assets/javascripts/vue_shared/components/deprecated_modal.vue b/app/assets/javascripts/vue_shared/components/deprecated_modal.vue index 424af5a0293..9c1e5c68649 100644 --- a/app/assets/javascripts/vue_shared/components/deprecated_modal.vue +++ b/app/assets/javascripts/vue_shared/components/deprecated_modal.vue @@ -86,8 +86,8 @@ <div class="modal-open"> <div :id="id" - class="modal" :class="id ? '' : 'd-block'" + class="modal" role="dialog" tabindex="-1" > @@ -105,9 +105,9 @@ <button type="button" class="close float-right" - @click="emitCancel($event)" data-dismiss="modal" aria-label="Close" + @click="emitCancel($event)" > <span aria-hidden="true">×</span> </button> @@ -115,22 +115,22 @@ </div> <div class="modal-body"> <slot - name="body" :text="text" + name="body" > <p>{{ text }}</p> </slot> </div> <div - class="modal-footer" v-if="!hideFooter" + class="modal-footer" > <button + :class="btnCancelKindClass" type="button" class="btn" - :class="btnCancelKindClass" - @click="emitCancel($event)" data-dismiss="modal" + @click="emitCancel($event)" > {{ closeButtonLabel }} </button> @@ -151,12 +151,12 @@ <button v-if="primaryButtonLabel" - type="button" - class="btn js-primary-button" :disabled="submitDisabled" :class="btnKindClass" - @click="emitSubmit($event)" + type="button" + class="btn js-primary-button" data-dismiss="modal" + @click="emitSubmit($event)" > {{ primaryButtonLabel }} </button> diff --git a/app/assets/javascripts/vue_shared/components/diff_viewer/constants.js b/app/assets/javascripts/vue_shared/components/diff_viewer/constants.js new file mode 100644 index 00000000000..6c1840361af --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/diff_viewer/constants.js @@ -0,0 +1,12 @@ +export const diffModes = { + replaced: 'replaced', + new: 'new', + deleted: 'deleted', + renamed: 'renamed', +}; + +export const imageViewMode = { + twoup: 'twoup', + swipe: 'swipe', + onion: 'onion', +}; diff --git a/app/assets/javascripts/vue_shared/components/diff_viewer/diff_viewer.vue b/app/assets/javascripts/vue_shared/components/diff_viewer/diff_viewer.vue new file mode 100644 index 00000000000..2c47f5b9b35 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/diff_viewer/diff_viewer.vue @@ -0,0 +1,70 @@ +<script> +import { viewerInformationForPath } from '../content_viewer/lib/viewer_utils'; +import ImageDiffViewer from './viewers/image_diff_viewer.vue'; +import DownloadDiffViewer from './viewers/download_diff_viewer.vue'; + +export default { + props: { + diffMode: { + type: String, + required: true, + }, + newPath: { + type: String, + required: true, + }, + newSha: { + type: String, + required: true, + }, + oldPath: { + type: String, + required: true, + }, + oldSha: { + type: String, + required: true, + }, + projectPath: { + type: String, + required: false, + default: '', + }, + }, + computed: { + viewer() { + if (!this.newPath) return null; + + const previewInfo = viewerInformationForPath(this.newPath); + if (!previewInfo) return DownloadDiffViewer; + + switch (previewInfo.id) { + case 'image': + return ImageDiffViewer; + default: + return DownloadDiffViewer; + } + }, + fullOldPath() { + return `${gon.relative_url_root}/${this.projectPath}/raw/${this.oldSha}/${this.oldPath}`; + }, + fullNewPath() { + return `${gon.relative_url_root}/${this.projectPath}/raw/${this.newSha}/${this.newPath}`; + }, + }, +}; +</script> + +<template> + <div + v-if="viewer" + class="diff-file preview-container"> + <component + :is="viewer" + :diff-mode="diffMode" + :new-path="fullNewPath" + :old-path="fullOldPath" + :project-path="projectPath" + /> + </div> +</template> diff --git a/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/download_diff_viewer.vue b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/download_diff_viewer.vue new file mode 100644 index 00000000000..50389b6ae63 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/download_diff_viewer.vue @@ -0,0 +1,69 @@ +<script> +import DownloadViewer from '../../content_viewer/viewers/download_viewer.vue'; +import { diffModes } from '../constants'; + +export default { + components: { + DownloadViewer, + }, + props: { + diffMode: { + type: String, + required: true, + }, + newPath: { + type: String, + required: true, + }, + oldPath: { + type: String, + required: true, + }, + projectPath: { + type: String, + required: false, + default: '', + }, + }, + diffModes, +}; +</script> + +<template> + <div class="diff-file-container"> + <div class="diff-viewer"> + <div + v-if="diffMode === $options.diffModes.replaced" + class="two-up view row"> + <div class="col-sm-6 deleted"> + <download-viewer + :path="oldPath" + :project-path="projectPath" + /> + </div> + <div class="col-sm-6 added"> + <download-viewer + :path="newPath" + :project-path="projectPath" + /> + </div> + </div> + <div + v-else-if="diffMode === $options.diffModes.new" + class="added"> + <download-viewer + :path="newPath" + :project-path="projectPath" + /> + </div> + <div + v-else + class="deleted"> + <download-viewer + :path="oldPath" + :project-path="projectPath" + /> + </div> + </div> + </div> +</template> diff --git a/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/onion_skin_viewer.vue b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/onion_skin_viewer.vue new file mode 100644 index 00000000000..38e881d17a2 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/onion_skin_viewer.vue @@ -0,0 +1,160 @@ +<script> +import { pixeliseValue } from '../../../lib/utils/dom_utils'; +import ImageViewer from '../../../content_viewer/viewers/image_viewer.vue'; + +export default { + components: { + ImageViewer, + }, + props: { + newPath: { + type: String, + required: true, + }, + oldPath: { + type: String, + required: true, + }, + projectPath: { + type: String, + required: false, + default: '', + }, + }, + data() { + return { + onionMaxWidth: undefined, + onionMaxHeight: undefined, + onionOldImgInfo: null, + onionNewImgInfo: null, + onionDraggerPos: 0, + onionOpacity: 1, + dragging: false, + }; + }, + computed: { + onionMaxPixelWidth() { + return pixeliseValue(this.onionMaxWidth); + }, + onionMaxPixelHeight() { + return pixeliseValue(this.onionMaxHeight); + }, + onionDraggerPixelPos() { + return pixeliseValue(this.onionDraggerPos); + }, + }, + beforeDestroy() { + document.body.removeEventListener('mouseup', this.stopDrag); + this.$refs.dragger.removeEventListener('mousedown', this.startDrag); + }, + methods: { + dragMove(e) { + if (!this.dragging) return; + const left = e.pageX - this.$refs.dragTrack.getBoundingClientRect().left; + const dragTrackWidth = + this.$refs.dragTrack.clientWidth - this.$refs.dragger.clientWidth || 100; + + let leftValue = left; + if (leftValue < 0) leftValue = 0; + if (leftValue > dragTrackWidth) leftValue = dragTrackWidth; + + this.onionOpacity = left / dragTrackWidth; + this.onionDraggerPos = leftValue; + }, + startDrag() { + this.dragging = true; + document.body.style.userSelect = 'none'; + document.body.addEventListener('mousemove', this.dragMove); + }, + stopDrag() { + this.dragging = false; + document.body.style.userSelect = ''; + document.body.removeEventListener('mousemove', this.dragMove); + }, + prepareOnionSkin() { + if (this.onionOldImgInfo && this.onionNewImgInfo) { + this.onionMaxWidth = Math.max( + this.onionOldImgInfo.renderedWidth, + this.onionNewImgInfo.renderedWidth, + ); + this.onionMaxHeight = Math.max( + this.onionOldImgInfo.renderedHeight, + this.onionNewImgInfo.renderedHeight, + ); + + this.onionOpacity = 1; + this.onionDraggerPos = + this.$refs.dragTrack.clientWidth - this.$refs.dragger.clientWidth || 100; + + document.body.addEventListener('mouseup', this.stopDrag); + } + }, + onionNewImgLoaded(imgInfo) { + this.onionNewImgInfo = imgInfo; + this.prepareOnionSkin(); + }, + onionOldImgLoaded(imgInfo) { + this.onionOldImgInfo = imgInfo; + this.prepareOnionSkin(); + }, + }, +}; +</script> + +<template> + <div class="onion-skin view"> + <div + :style="{ + 'width': onionMaxPixelWidth, + 'height': onionMaxPixelHeight, + 'user-select': dragging === true ? 'none' : '', + }" + class="onion-skin-frame"> + <div + :style="{ + 'width': onionMaxPixelWidth, + 'height': onionMaxPixelHeight, + }" + class="frame deleted"> + <image-viewer + key="onionOldImg" + :render-info="false" + :path="oldPath" + :project-path="projectPath" + @imgLoaded="onionOldImgLoaded" + /> + </div> + <div + ref="addedFrame" + :style="{ + 'opacity': onionOpacity, + 'width': onionMaxPixelWidth, + 'height': onionMaxPixelHeight, + }" + class="added frame"> + <image-viewer + key="onionNewImg" + :render-info="false" + :path="newPath" + :project-path="projectPath" + @imgLoaded="onionNewImgLoaded" + /> + </div> + <div class="controls"> + <div class="transparent"></div> + <div + ref="dragTrack" + class="drag-track" + @mousedown="startDrag" + @mouseup="stopDrag"> + <div + ref="dragger" + :style="{ 'left': onionDraggerPixelPos }" + class="dragger"> + </div> + </div> + <div class="opaque"></div> + </div> + </div> + </div> +</template> diff --git a/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/swipe_viewer.vue b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/swipe_viewer.vue new file mode 100644 index 00000000000..86366c799a2 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/swipe_viewer.vue @@ -0,0 +1,158 @@ +<script> +import _ from 'underscore'; +import { pixeliseValue } from '../../../lib/utils/dom_utils'; +import ImageViewer from '../../../content_viewer/viewers/image_viewer.vue'; + +export default { + components: { + ImageViewer, + }, + props: { + newPath: { + type: String, + required: true, + }, + oldPath: { + type: String, + required: true, + }, + projectPath: { + type: String, + required: false, + default: '', + }, + }, + data() { + return { + dragging: false, + swipeOldImgInfo: null, + swipeNewImgInfo: null, + swipeMaxWidth: undefined, + swipeMaxHeight: undefined, + swipeBarPos: 1, + swipeWrapWidth: undefined, + }; + }, + computed: { + swipeMaxPixelWidth() { + return pixeliseValue(this.swipeMaxWidth); + }, + swipeMaxPixelHeight() { + return pixeliseValue(this.swipeMaxHeight); + }, + swipeWrapPixelWidth() { + return pixeliseValue(this.swipeWrapWidth); + }, + swipeBarPixelPos() { + return pixeliseValue(this.swipeBarPos); + }, + }, + beforeDestroy() { + window.removeEventListener('resize', this.resizeThrottled, false); + document.body.removeEventListener('mouseup', this.stopDrag); + document.body.removeEventListener('mousemove', this.dragMove); + }, + mounted() { + window.addEventListener('resize', this.resize, false); + }, + methods: { + dragMove(e) { + if (!this.dragging) return; + + let leftValue = e.pageX - this.$refs.swipeFrame.getBoundingClientRect().left; + const spaceLeft = 20; + const { clientWidth } = this.$refs.swipeFrame; + if (leftValue <= 0) { + leftValue = 0; + } else if (leftValue > clientWidth - spaceLeft) { + leftValue = clientWidth - spaceLeft; + } + + this.swipeWrapWidth = this.swipeMaxWidth - leftValue; + this.swipeBarPos = leftValue; + }, + startDrag() { + this.dragging = true; + document.body.style.userSelect = 'none'; + document.body.addEventListener('mousemove', this.dragMove); + }, + stopDrag() { + this.dragging = false; + document.body.style.userSelect = ''; + document.body.removeEventListener('mousemove', this.dragMove); + }, + prepareSwipe() { + if (this.swipeOldImgInfo && this.swipeNewImgInfo) { + // Add 2 for border width + this.swipeMaxWidth = + Math.max(this.swipeOldImgInfo.renderedWidth, this.swipeNewImgInfo.renderedWidth) + 2; + this.swipeWrapWidth = this.swipeMaxWidth; + this.swipeMaxHeight = + Math.max(this.swipeOldImgInfo.renderedHeight, this.swipeNewImgInfo.renderedHeight) + 2; + + document.body.addEventListener('mouseup', this.stopDrag); + } + }, + swipeNewImgLoaded(imgInfo) { + this.swipeNewImgInfo = imgInfo; + this.prepareSwipe(); + }, + swipeOldImgLoaded(imgInfo) { + this.swipeOldImgInfo = imgInfo; + this.prepareSwipe(); + }, + resize: _.throttle(function throttledResize() { + this.swipeBarPos = 0; + }, 400), + }, +}; +</script> + +<template> + <div class="swipe view"> + <div + ref="swipeFrame" + :style="{ + 'width': swipeMaxPixelWidth, + 'height': swipeMaxPixelHeight, + }" + class="swipe-frame"> + <div class="frame deleted"> + <image-viewer + key="swipeOldImg" + ref="swipeOldImg" + :render-info="false" + :path="oldPath" + :project-path="projectPath" + @imgLoaded="swipeOldImgLoaded" + /> + </div> + <div + ref="swipeWrap" + :style="{ + 'width': swipeWrapPixelWidth, + 'height': swipeMaxPixelHeight, + }" + class="swipe-wrap"> + <div class="frame added"> + <image-viewer + key="swipeNewImg" + :render-info="false" + :path="newPath" + :project-path="projectPath" + @imgLoaded="swipeNewImgLoaded" + /> + </div> + </div> + <span + ref="swipeBar" + :style="{ 'left': swipeBarPixelPos }" + class="swipe-bar" + @mousedown="startDrag" + @mouseup="stopDrag"> + <span class="top-handle"></span> + <span class="bottom-handle"></span> + </span> + </div> + </div> +</template> diff --git a/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/two_up_viewer.vue b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/two_up_viewer.vue new file mode 100644 index 00000000000..9c19266ecdf --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff/two_up_viewer.vue @@ -0,0 +1,41 @@ +<script> +import ImageViewer from '../../../content_viewer/viewers/image_viewer.vue'; + +export default { + components: { + ImageViewer, + }, + props: { + newPath: { + type: String, + required: true, + }, + oldPath: { + type: String, + required: true, + }, + projectPath: { + type: String, + required: false, + default: '', + }, + }, +}; +</script> + +<template> + <div class="two-up view row"> + <div class="col-sm-6 frame deleted"> + <image-viewer + :path="oldPath" + :project-path="projectPath" + /> + </div> + <div class="col-sm-6 frame added"> + <image-viewer + :path="newPath" + :project-path="projectPath" + /> + </div> + </div> +</template> diff --git a/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff_viewer.vue b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff_viewer.vue new file mode 100644 index 00000000000..1af85283277 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/image_diff_viewer.vue @@ -0,0 +1,109 @@ +<script> +import ImageViewer from '../../content_viewer/viewers/image_viewer.vue'; +import TwoUpViewer from './image_diff/two_up_viewer.vue'; +import SwipeViewer from './image_diff/swipe_viewer.vue'; +import OnionSkinViewer from './image_diff/onion_skin_viewer.vue'; +import { diffModes, imageViewMode } from '../constants'; + +export default { + components: { + ImageViewer, + TwoUpViewer, + SwipeViewer, + OnionSkinViewer, + }, + props: { + diffMode: { + type: String, + required: true, + }, + newPath: { + type: String, + required: true, + }, + oldPath: { + type: String, + required: true, + }, + projectPath: { + type: String, + required: false, + default: '', + }, + }, + data() { + return { + mode: imageViewMode.twoup, + }; + }, + methods: { + changeMode(newMode) { + this.mode = newMode; + }, + }, + diffModes, + imageViewMode, +}; +</script> + +<template> + <div class="diff-file-container"> + <div + v-if="diffMode === $options.diffModes.replaced" + class="diff-viewer"> + <div class="image js-replaced-image"> + <two-up-viewer + v-if="mode === $options.imageViewMode.twoup" + v-bind="$props"/> + <swipe-viewer + v-else-if="mode === $options.imageViewMode.swipe" + v-bind="$props"/> + <onion-skin-viewer + v-else-if="mode === $options.imageViewMode.onion" + v-bind="$props"/> + </div> + <div class="view-modes"> + <ul class="view-modes-menu"> + <li + :class="{ + active: mode === $options.imageViewMode.twoup + }" + @click="changeMode($options.imageViewMode.twoup)"> + {{ s__('ImageDiffViewer|2-up') }} + </li> + <li + :class="{ + active: mode === $options.imageViewMode.swipe + }" + @click="changeMode($options.imageViewMode.swipe)"> + {{ s__('ImageDiffViewer|Swipe') }} + </li> + <li + :class="{ + active: mode === $options.imageViewMode.onion + }" + @click="changeMode($options.imageViewMode.onion)"> + {{ s__('ImageDiffViewer|Onion skin') }} + </li> + </ul> + </div> + <div class="note-container"></div> + </div> + <div + v-else-if="diffMode === $options.diffModes.new" + class="diff-viewer added"> + <image-viewer + :path="newPath" + :project-path="projectPath" + /> + </div> + <div + v-else + class="diff-viewer deleted"> + <image-viewer + :path="oldPath" + :project-path="projectPath" + /> + </div> + </div> +</template> diff --git a/app/assets/javascripts/vue_shared/components/dropdown/dropdown_button.vue b/app/assets/javascripts/vue_shared/components/dropdown/dropdown_button.vue index c159333d89a..3cba0c5e633 100644 --- a/app/assets/javascripts/vue_shared/components/dropdown/dropdown_button.vue +++ b/app/assets/javascripts/vue_shared/components/dropdown/dropdown_button.vue @@ -28,11 +28,11 @@ export default { <template> <button + :disabled="isDisabled || isLoading" class="dropdown-menu-toggle dropdown-menu-full-width" type="button" data-toggle="dropdown" aria-expanded="false" - :disabled="isDisabled || isLoading" > <loading-icon v-show="isLoading" @@ -42,8 +42,8 @@ export default { {{ toggleText }} </span> <span - class="dropdown-toggle-icon" v-show="!isLoading" + class="dropdown-toggle-icon" > <i class="fa fa-chevron-down" diff --git a/app/assets/javascripts/vue_shared/components/dropdown/dropdown_hidden_input.vue b/app/assets/javascripts/vue_shared/components/dropdown/dropdown_hidden_input.vue index 1fe27eb97ab..b7a4613bdd2 100644 --- a/app/assets/javascripts/vue_shared/components/dropdown/dropdown_hidden_input.vue +++ b/app/assets/javascripts/vue_shared/components/dropdown/dropdown_hidden_input.vue @@ -15,8 +15,8 @@ export default { <template> <input - type="hidden" :name="name" :value="value" + type="hidden" /> </template> diff --git a/app/assets/javascripts/vue_shared/components/dropdown/dropdown_search_input.vue b/app/assets/javascripts/vue_shared/components/dropdown/dropdown_search_input.vue index c2145a26e64..7f1912f6405 100644 --- a/app/assets/javascripts/vue_shared/components/dropdown/dropdown_search_input.vue +++ b/app/assets/javascripts/vue_shared/components/dropdown/dropdown_search_input.vue @@ -23,10 +23,10 @@ export default { <template> <div class="dropdown-input"> <input - class="dropdown-input-field" - type="search" v-model="searchQuery" :placeholder="placeholderText" + class="dropdown-input-field" + type="search" autocomplete="off" /> <i diff --git a/app/assets/javascripts/vue_shared/components/expand_button.vue b/app/assets/javascripts/vue_shared/components/expand_button.vue index 9295be3e2b2..0fdea651130 100644 --- a/app/assets/javascripts/vue_shared/components/expand_button.vue +++ b/app/assets/javascripts/vue_shared/components/expand_button.vue @@ -32,10 +32,10 @@ export default { <template> <span> <button - type="button" v-show="isCollapsed" - class="text-expander btn-blank" :aria-label="ariaLabel" + type="button" + class="text-expander btn-blank" @click="onClick"> ... </button> diff --git a/app/assets/javascripts/vue_shared/components/file_icon.vue b/app/assets/javascripts/vue_shared/components/file_icon.vue index be2755452e2..878c805ada5 100644 --- a/app/assets/javascripts/vue_shared/components/file_icon.vue +++ b/app/assets/javascripts/vue_shared/components/file_icon.vue @@ -73,8 +73,8 @@ export default { <template> <span> <svg - :class="[iconSizeClass, cssClasses]" v-if="!loading && !folder" + :class="[iconSizeClass, cssClasses]" > <use v-bind="{ 'xlink:href':spriteHref }" /> </svg> diff --git a/app/assets/javascripts/vue_shared/components/file_icon/file_icon_map.js b/app/assets/javascripts/vue_shared/components/file_icon/file_icon_map.js index 9ffbaae3ea5..9bca1993ccc 100644 --- a/app/assets/javascripts/vue_shared/components/file_icon/file_icon_map.js +++ b/app/assets/javascripts/vue_shared/components/file_icon/file_icon_map.js @@ -513,7 +513,7 @@ const fileNameIcons = { 'credits.md': 'credits', 'credits.md.rendered': 'credits', '.flowconfig': 'flow', - 'favicon.ico': 'favicon', + 'favicon.png': 'favicon', 'karma.conf.js': 'karma', 'karma.conf.ts': 'karma', 'karma.conf.coffee': 'karma', diff --git a/app/assets/javascripts/vue_shared/components/gl_modal.vue b/app/assets/javascripts/vue_shared/components/gl_modal.vue index d5d5a7d3798..b298b989203 100644 --- a/app/assets/javascripts/vue_shared/components/gl_modal.vue +++ b/app/assets/javascripts/vue_shared/components/gl_modal.vue @@ -1,15 +1,21 @@ <script> const buttonVariants = ['danger', 'primary', 'success', 'warning']; +const sizeVariants = ['sm', 'md', 'lg', 'xl']; export default { name: 'GlModal', - props: { id: { type: String, required: false, default: null, }, + modalSize: { + type: String, + required: false, + default: 'md', + validator: value => sizeVariants.includes(value), + }, headerTitleText: { type: String, required: false, @@ -27,7 +33,11 @@ export default { default: '', }, }, - + computed: { + modalSizeClass() { + return this.modalSize === 'md' ? '' : `modal-${this.modalSize}`; + }, + }, methods: { emitCancel(event) { this.$emit('cancel', event); @@ -47,6 +57,7 @@ export default { role="dialog" > <div + :class="modalSizeClass" class="modal-dialog" role="document" > @@ -59,10 +70,10 @@ export default { </slot> </h4> <button + :aria-label="s__('Modal|Close')" type="button" class="close js-modal-close-action" data-dismiss="modal" - :aria-label="s__('Modal|Close')" @click="emitCancel($event)" > <span aria-hidden="true">×</span> @@ -85,9 +96,9 @@ export default { {{ s__('Modal|Cancel') }} </button> <button + :class="`btn-${footerPrimaryButtonVariant}`" type="button" class="btn js-modal-primary-action" - :class="`btn-${footerPrimaryButtonVariant}`" data-dismiss="modal" @click="emitSubmit($event)" > diff --git a/app/assets/javascripts/vue_shared/components/header_ci_component.vue b/app/assets/javascripts/vue_shared/components/header_ci_component.vue index ca17fa06a00..62d35f6547d 100644 --- a/app/assets/javascripts/vue_shared/components/header_ci_component.vue +++ b/app/assets/javascripts/vue_shared/components/header_ci_component.vue @@ -117,8 +117,8 @@ export default { </section> <section - class="header-action-buttons" v-if="actions.length" + class="header-action-buttons" > <template v-for="(action, i) in actions" @@ -135,21 +135,21 @@ export default { <a v-else-if="action.type === 'ujs-link'" :href="action.path" - data-method="post" - rel="nofollow" :class="action.cssClass" :key="i" + data-method="post" + rel="nofollow" > {{ action.label }} </a> <button v-else-if="action.type === 'button'" - @click="onClickAction(action)" :disabled="action.isLoading" :class="action.cssClass" - type="button" :key="i" + type="button" + @click="onClickAction(action)" > {{ action.label }} <i @@ -162,11 +162,11 @@ export default { </template> <button v-if="hasSidebarButton" + id="toggleSidebar" type="button" class="btn btn-default d-block d-sm-none d-md-none sidebar-toggle-btn js-sidebar-build-toggle js-sidebar-build-toggle-header" aria-label="Toggle Sidebar" - id="toggleSidebar" > <i class="fa fa-angle-double-left" diff --git a/app/assets/javascripts/vue_shared/components/identicon.vue b/app/assets/javascripts/vue_shared/components/identicon.vue index 23010f40f26..4ffc811e714 100644 --- a/app/assets/javascripts/vue_shared/components/identicon.vue +++ b/app/assets/javascripts/vue_shared/components/identicon.vue @@ -43,9 +43,9 @@ export default { <template> <div - class="avatar identicon" :class="sizeClass" - :style="identiconStyles"> + :style="identiconStyles" + class="avatar identicon"> {{ identiconTitle }} </div> </template> diff --git a/app/assets/javascripts/vue_shared/components/issue/issue_warning.vue b/app/assets/javascripts/vue_shared/components/issue/issue_warning.vue index 3d39b3ab173..ca8ce554588 100644 --- a/app/assets/javascripts/vue_shared/components/issue/issue_warning.vue +++ b/app/assets/javascripts/vue_shared/components/issue/issue_warning.vue @@ -33,11 +33,11 @@ <template> <div class="issuable-note-warning"> <icon + v-if="!isLockedAndConfidential" :name="warningIcon" :size="16" class="icon inline" aria-hidden="true" - v-if="!isLockedAndConfidential" /> <span v-if="isLockedAndConfidential"> diff --git a/app/assets/javascripts/vue_shared/components/lib/utils/dom_utils.js b/app/assets/javascripts/vue_shared/components/lib/utils/dom_utils.js new file mode 100644 index 00000000000..02f28da8bb0 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/lib/utils/dom_utils.js @@ -0,0 +1,5 @@ +export function pixeliseValue(val) { + return val ? `${val}px` : ''; +} + +export default {}; diff --git a/app/assets/javascripts/vue_shared/components/loading_button.vue b/app/assets/javascripts/vue_shared/components/loading_button.vue index 88c13a1f340..2ff0c056b9c 100644 --- a/app/assets/javascripts/vue_shared/components/loading_button.vue +++ b/app/assets/javascripts/vue_shared/components/loading_button.vue @@ -54,19 +54,19 @@ <template> <button - @click="onClick" - type="button" :class="containerClass" :disabled="loading || disabled" + type="button" + @click="onClick" > <transition name="fade"> <loading-icon v-if="loading" :inline="true" - class="js-loading-button-icon" :class="{ 'append-right-5': label }" + class="js-loading-button-icon" /> </transition> <transition name="fade"> diff --git a/app/assets/javascripts/vue_shared/components/loading_icon.vue b/app/assets/javascripts/vue_shared/components/loading_icon.vue index 12a75e016d7..db22c5f02cd 100644 --- a/app/assets/javascripts/vue_shared/components/loading_icon.vue +++ b/app/assets/javascripts/vue_shared/components/loading_icon.vue @@ -35,10 +35,10 @@ :is="rootElementType" class="loading-container text-center"> <i - class="fa fa-spin fa-spinner" :class="cssClass" - aria-hidden="true" :aria-label="label" + class="fa fa-spin fa-spinner" + aria-hidden="true" > </i> </component> diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue index 12c7d125062..05e8ed2da2c 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/field.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue @@ -117,17 +117,17 @@ <template> <div - class="md-area js-vue-markdown-field" + ref="gl-form" :class="{ 'prepend-top-default append-bottom-default': addSpacingClasses }" - ref="gl-form"> + class="md-area js-vue-markdown-field"> <markdown-header :preview-markdown="previewMarkdown" @preview-markdown="showPreviewTab" @write-markdown="showWriteTab" /> <div - class="md-write-holder" v-show="!previewMarkdown" + class="md-write-holder" > <div class="zen-backdrop"> <slot name="textarea"></slot> @@ -137,8 +137,8 @@ aria-label="Enter zen mode" > <icon - name="screen-normal" :size="32" + name="screen-normal" /> </a> <markdown-toolbar @@ -149,8 +149,8 @@ </div> </div> <div - class="md md-preview-holder md-preview" v-show="previewMarkdown" + class="md md-preview-holder md-preview" > <div ref="markdown-preview" @@ -164,8 +164,8 @@ <template v-if="previewMarkdown && !markdownPreviewLoading"> <div v-if="referencedCommands" - v-html="referencedCommands" class="referenced-commands" + v-html="referencedCommands" > </div> <div diff --git a/app/assets/javascripts/vue_shared/components/markdown/header.vue b/app/assets/javascripts/vue_shared/components/markdown/header.vue index db453c30576..ee3628b1e3f 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/header.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/header.vue @@ -54,8 +54,8 @@ <div class="md-header"> <ul class="nav-links clearfix"> <li - class="md-header-tab" :class="{ active: !previewMarkdown }" + class="md-header-tab" > <a class="js-write-link" @@ -67,8 +67,8 @@ </a> </li> <li - class="md-header-tab" :class="{ active: previewMarkdown }" + class="md-header-tab" > <a class="js-preview-link" @@ -80,8 +80,8 @@ </a> </li> <li - class="md-header-toolbar" :class="{ active: !previewMarkdown }" + class="md-header-toolbar" > <toolbar-button tag="**" @@ -94,8 +94,8 @@ icon="italic" /> <toolbar-button - tag="> " :prepend="true" + tag="> " button-title="Insert a quote" icon="quote" /> @@ -106,20 +106,20 @@ icon="code" /> <toolbar-button - tag="* " :prepend="true" + tag="* " button-title="Add a bullet list" icon="list-bulleted" /> <toolbar-button - tag="1. " :prepend="true" + tag="1. " button-title="Add a numbered list" icon="list-numbered" /> <toolbar-button - tag="* [ ] " :prepend="true" + tag="* [ ] " button-title="Add a task list" icon="task-done" /> diff --git a/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue b/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue index 2d2d69ebeb2..9f1e009efdd 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue @@ -39,15 +39,15 @@ <template> <button v-tooltip - type="button" - class="toolbar-btn js-md" - tabindex="-1" - data-container="body" :data-md-tag="tag" :data-md-block="tagBlock" :data-md-prepend="prepend" :title="buttonTitle" :aria-label="buttonTitle" + type="button" + class="toolbar-btn js-md" + tabindex="-1" + data-container="body" > <icon :name="icon" diff --git a/app/assets/javascripts/vue_shared/components/memory_graph.vue b/app/assets/javascripts/vue_shared/components/memory_graph.vue index b07f6b07afe..522091ea889 100644 --- a/app/assets/javascripts/vue_shared/components/memory_graph.vue +++ b/app/assets/javascripts/vue_shared/components/memory_graph.vue @@ -113,19 +113,19 @@ export default { <template> <div class="memory-graph-container"> <svg - class="has-tooltip" :title="getFormattedMedian" :width="width" :height="height" + class="has-tooltip" xmlns="http://www.w3.org/2000/svg"> <path :d="pathD" :viewBox="pathViewBox" /> <circle - r="1.5" :cx="dotX" :cy="dotY" + r="1.5" tranform="translate(0 -1)" /> </svg> diff --git a/app/assets/javascripts/vue_shared/components/navigation_tabs.vue b/app/assets/javascripts/vue_shared/components/navigation_tabs.vue index 08d4936f480..99d61b5639d 100644 --- a/app/assets/javascripts/vue_shared/components/navigation_tabs.vue +++ b/app/assets/javascripts/vue_shared/components/navigation_tabs.vue @@ -59,9 +59,9 @@ export default { }" > <a + :class="`js-${scope}-tab-${tab.scope}`" role="button" @click="onTabClick(tab)" - :class="`js-${scope}-tab-${tab.scope}`" > {{ tab.name }} diff --git a/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue b/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue index eccba61a8c0..38115f268bb 100644 --- a/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue +++ b/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue @@ -54,7 +54,7 @@ <div class="note-header"> <div class="note-header-info"> <a :href="getUserData.path"> - <span class="d-none d-sm-block">{{ getUserData.name }}</span> + <span class="d-none d-sm-inline-block">{{ getUserData.name }}</span> <span class="note-headline-light">@{{ getUserData.username }}</span> </a> </div> diff --git a/app/assets/javascripts/vue_shared/components/panel_resizer.vue b/app/assets/javascripts/vue_shared/components/panel_resizer.vue index abbe9a22717..8c2dcc2d902 100644 --- a/app/assets/javascripts/vue_shared/components/panel_resizer.vue +++ b/app/assets/javascripts/vue_shared/components/panel_resizer.vue @@ -82,9 +82,9 @@ <template> <div - class="dragHandle" :class="className" :style="cursorStyle" + class="dragHandle" @mousedown="startDrag" @dblclick="resetSize" ></div> diff --git a/app/assets/javascripts/vue_shared/components/project_avatar/image.vue b/app/assets/javascripts/vue_shared/components/project_avatar/image.vue index 279cc1de5bb..97ca4d93bd7 100644 --- a/app/assets/javascripts/vue_shared/components/project_avatar/image.vue +++ b/app/assets/javascripts/vue_shared/components/project_avatar/image.vue @@ -85,7 +85,6 @@ <template> <img v-tooltip - class="avatar" :class="{ lazy: lazy, [avatarSizeClass]: true, @@ -99,5 +98,6 @@ :data-container="tooltipContainer" :data-placement="tooltipPlacement" :title="tooltipText" + class="avatar" /> </template> diff --git a/app/assets/javascripts/vue_shared/components/recaptcha_modal.vue b/app/assets/javascripts/vue_shared/components/recaptcha_modal.vue index 21ffdc1dc86..a2a9a5e6987 100644 --- a/app/assets/javascripts/vue_shared/components/recaptcha_modal.vue +++ b/app/assets/javascripts/vue_shared/components/recaptcha_modal.vue @@ -66,10 +66,10 @@ <template> <deprecated-modal - kind="warning" - class="recaptcha-modal js-recaptcha-modal" :hide-footer="true" :title="__('Please solve the reCAPTCHA')" + kind="warning" + class="recaptcha-modal js-recaptcha-modal" @cancel="close" > <div slot="body"> diff --git a/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue b/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue index 71ec34f2c7a..74998a4787d 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue @@ -97,8 +97,8 @@ <template> <div - class="block" :class="blockClass" + class="block" > <div class="issuable-sidebar-header"> <toggle-sidebar @@ -107,8 +107,8 @@ /> </div> <collapsed-calendar-icon - class="sidebar-collapsed-icon" :text="collapsedText" + class="sidebar-collapsed-icon" /> <div class="title"> {{ label }} diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue index f155ac2be02..a3fc358130f 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue @@ -143,8 +143,8 @@ export default { :value="label.id" /> <div - class="dropdown" ref="dropdown" + class="dropdown" > <dropdown-button :ability-name="abilityName" diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button.vue index 47497c1de98..48d2f16f554 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button.vue @@ -53,10 +53,7 @@ export default { <template> <button - type="button" ref="dropdownButton" - class="dropdown-menu-toggle wide js-label-select js-multiselect js-context-config-modal" - data-toggle="dropdown" :class="{ 'js-extra-options': showExtraOptions }" :data-ability-name="abilityName" :data-field-name="fieldName" @@ -64,6 +61,9 @@ export default { :data-labels="labelsPath" :data-namespace-path="namespace" :data-show-any="showExtraOptions" + type="button" + class="dropdown-menu-toggle wide js-label-select js-multiselect js-context-config-modal" + data-toggle="dropdown" > <span class="dropdown-toggle-text"> {{ dropdownToggleText }} diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_create_label.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_create_label.vue index 3c400afdc1d..fe895136ccc 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_create_label.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_create_label.vue @@ -19,9 +19,9 @@ export default { <div class="dropdown-page-two dropdown-new-label"> <div class="dropdown-title"> <button + :aria-label="__('Go back')" type="button" class="dropdown-title-button dropdown-menu-back" - :aria-label="__('Go back')" > <i aria-hidden="true" @@ -32,9 +32,9 @@ export default { </button> {{ headerTitle }} <button + :aria-label="__('Close')" type="button" class="dropdown-title-button dropdown-menu-close" - :aria-label="__('Close')" > <i aria-hidden="true" @@ -48,19 +48,19 @@ export default { <div class="dropdown-labels-error js-label-error"></div> <input id="new_label_name" + :placeholder="__('Name new label')" type="text" class="default-dropdown-input" - :placeholder="__('Name new label')" /> <div class="suggest-colors suggest-colors-dropdown"> <a v-for="(color, index) in suggestedColors" - href="#" :key="index" :data-color="color" :style="{ backgroundColor: color, }" + href="#" > </a> @@ -69,9 +69,9 @@ export default { <div class="dropdown-label-color-preview js-dropdown-label-color-preview"></div> <input id="new_label_color" + :placeholder="__('Assign custom color like #FF0000')" type="text" class="default-dropdown-input" - :placeholder="__('Assign custom color like #FF0000')" /> </div> <div class="clearfix"> diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_footer.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_footer.vue index 5f61e9fbe80..d64ad016f9b 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_footer.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_footer.vue @@ -34,9 +34,9 @@ export default { </li> <li> <a + :href="labelsWebUrl" data-is-link="true" class="dropdown-external-link" - :href="labelsWebUrl" > {{ manageLabelsTitle }} </a> diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_header.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_header.vue index 7664acdf19c..e98b6392827 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_header.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_header.vue @@ -6,9 +6,9 @@ export default {}; <div class="dropdown-title"> <span>{{ __('Assign labels') }}</span> <button + :aria-label="__('Close')" type="button" class="dropdown-title-button dropdown-menu-close" - :aria-label="__('Close')" > <i aria-hidden="true" diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_search_input.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_search_input.vue index ae633460c95..80d65a2a534 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_search_input.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_search_input.vue @@ -5,10 +5,10 @@ export default {}; <template> <div class="dropdown-input"> <input + :placeholder="__('Search')" autocomplete="off" class="dropdown-input-field" type="search" - :placeholder="__('Search')" /> <i aria-hidden="true" diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value.vue index 69d588eb25d..10e990f8a80 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value.vue @@ -35,7 +35,12 @@ export default { </script> <template> - <div class="hide-collapsed value issuable-show-labels js-value"> + <div + :class="{ + 'has-labels':!isEmpty, + }" + class="hide-collapsed value issuable-show-labels js-value" + > <span v-if="isEmpty" class="text-secondary" @@ -43,18 +48,18 @@ export default { <slot>{{ __('None') }}</slot> </span> <a - v-else v-for="label in labels" + v-else :key="label.id" :href="labelFilterUrl(label)" > <span v-tooltip - class="label color-label" - data-placement="bottom" - data-container="body" :style="labelStyle(label)" :title="label.description" + class="badge color-label" + data-placement="bottom" + data-container="body" > {{ label.title }} </span> diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue index 68fa2ab8d01..af297f3c408 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue @@ -37,10 +37,10 @@ export default { <template> <div v-tooltip + :title="labelsList" class="sidebar-collapsed-icon" data-placement="left" data-container="body" - :title="labelsList" @click="handleClick" > <i diff --git a/app/assets/javascripts/vue_shared/components/sidebar/toggle_sidebar.vue b/app/assets/javascripts/vue_shared/components/sidebar/toggle_sidebar.vue index de6f8c32e74..ac2e99abe77 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/toggle_sidebar.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/toggle_sidebar.vue @@ -28,21 +28,21 @@ export default { <template> <button + v-tooltip + :title="tooltipLabel" type="button" class="btn btn-blank gutter-toggle btn-sidebar-action" - @click="toggle" - v-tooltip data-container="body" data-placement="left" - :title="tooltipLabel" + @click="toggle" > <i - aria-label="toggle collapse" - class="fa" :class="{ 'fa-angle-double-right': !collapsed, 'fa-angle-double-left': collapsed }" + aria-label="toggle collapse" + class="fa" > </i> </button> diff --git a/app/assets/javascripts/vue_shared/components/skeleton_loading_container.vue b/app/assets/javascripts/vue_shared/components/skeleton_loading_container.vue index 16304e4815d..4a5ffbe5d5a 100644 --- a/app/assets/javascripts/vue_shared/components/skeleton_loading_container.vue +++ b/app/assets/javascripts/vue_shared/components/skeleton_loading_container.vue @@ -22,10 +22,10 @@ <template> <div - class="animation-container" :class="{ 'animation-container-small': small, }" + class="animation-container" > <div v-for="(css, index) in lineClasses" diff --git a/app/assets/javascripts/vue_shared/components/stacked_progress_bar.vue b/app/assets/javascripts/vue_shared/components/stacked_progress_bar.vue index 86f06c8d266..b1c2df54ef6 100644 --- a/app/assets/javascripts/vue_shared/components/stacked_progress_bar.vue +++ b/app/assets/javascripts/vue_shared/components/stacked_progress_bar.vue @@ -84,8 +84,8 @@ export default { <template> <div - class="stacked-progress-bar" :class="cssClass" + class="stacked-progress-bar" > <span v-if="!totalCount" @@ -96,30 +96,30 @@ export default { <span v-tooltip v-if="successPercent" - class="status-green" - data-placement="bottom" :title="successTooltip" :style="successBarStyle" + class="status-green" + data-placement="bottom" > {{ successPercent }}% </span> <span v-tooltip v-if="neutralPercent" - class="status-neutral" - data-placement="bottom" :title="neutralTooltip" :style="neutralBarStyle" + class="status-neutral" + data-placement="bottom" > {{ neutralPercent }}% </span> <span v-tooltip v-if="failurePercent" - class="status-red" - data-placement="bottom" :title="failureTooltip" :style="failureBarStyle" + class="status-red" + data-placement="bottom" > {{ failurePercent }}% </span> diff --git a/app/assets/javascripts/vue_shared/components/table_pagination.vue b/app/assets/javascripts/vue_shared/components/table_pagination.vue index 22fc5757447..2370e59d017 100644 --- a/app/assets/javascripts/vue_shared/components/table_pagination.vue +++ b/app/assets/javascripts/vue_shared/components/table_pagination.vue @@ -124,15 +124,18 @@ break; } }, + hideOnSmallScreen(item) { + return !item.first && !item.last && !item.next && !item.prev && !item.active; + }, }, }; </script> <template> <div v-if="showPagination" - class="gl-pagination" + class="gl-pagination prepend-top-default" > - <ul class="pagination clearfix"> + <ul class="pagination justify-content-center"> <li v-for="(item, index) in getItems" :key="index" @@ -142,12 +145,17 @@ 'js-next-button': item.next, 'js-last-button': item.last, 'js-first-button': item.first, + 'd-none d-md-block': hideOnSmallScreen(item), separator: item.separator, active: item.active, - disabled: item.disabled + disabled: item.disabled || item.separator }" + class="page-item" > - <a @click.prevent="changePage(item.title, item.disabled)"> + <a + class="page-link" + @click.prevent="changePage(item.title, item.disabled)" + > {{ item.title }} </a> </li> diff --git a/app/assets/javascripts/vue_shared/components/tabs/tab.vue b/app/assets/javascripts/vue_shared/components/tabs/tab.vue index 2a35d6bc151..1c6011dcfd0 100644 --- a/app/assets/javascripts/vue_shared/components/tabs/tab.vue +++ b/app/assets/javascripts/vue_shared/components/tabs/tab.vue @@ -26,15 +26,20 @@ export default { created() { this.isTab = true; }, + updated() { + if (this.$parent) { + this.$parent.$forceUpdate(); + } + }, }; </script> <template> <div - class="tab-pane" :class="{ active: localActive }" + class="tab-pane" role="tabpanel" > <slot></slot> diff --git a/app/assets/javascripts/vue_shared/components/tabs/tabs.js b/app/assets/javascripts/vue_shared/components/tabs/tabs.js index 4362264caa5..9b9e4bb47bd 100644 --- a/app/assets/javascripts/vue_shared/components/tabs/tabs.js +++ b/app/assets/javascripts/vue_shared/components/tabs/tabs.js @@ -1,4 +1,11 @@ export default { + props: { + stopPropagation: { + type: Boolean, + required: false, + default: false, + }, + }, data() { return { currentIndex: 0, @@ -13,7 +20,12 @@ export default { this.tabs = this.$children.filter(child => child.isTab); this.currentIndex = this.tabs.findIndex(tab => tab.localActive); }, - setTab(index) { + setTab(e, index) { + if (this.stopPropagation) { + e.stopPropagation(); + e.preventDefault(); + } + this.tabs[this.currentIndex].localActive = false; this.tabs[index].localActive = true; @@ -36,7 +48,7 @@ export default { href: '#', }, on: { - click: () => this.setTab(i), + click: e => this.setTab(e, i), }, }, tab.$slots.title || tab.title, diff --git a/app/assets/javascripts/vue_shared/components/toggle_button.vue b/app/assets/javascripts/vue_shared/components/toggle_button.vue index 09031d3ffa1..a897300b62b 100644 --- a/app/assets/javascripts/vue_shared/components/toggle_button.vue +++ b/app/assets/javascripts/vue_shared/components/toggle_button.vue @@ -63,26 +63,26 @@ <label class="toggle-wrapper"> <input v-if="name" - type="hidden" :name="name" :value="value" + type="hidden" /> <button - type="button" - class="project-feature-toggle" :aria-label="ariaLabel" :class="{ 'is-checked': value, 'is-disabled': disabledInput, 'is-loading': isLoading }" + type="button" + class="project-feature-toggle" @click="toggleFeature" > <loadingIcon class="loading-icon" /> <span class="toggle-icon"> <icon - css-classes="toggle-icon-svg" - :name="toggleIcon"/> + :name="toggleIcon" + css-classes="toggle-icon-svg"/> </span> </button> </label> diff --git a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue index cc9cc46bb4c..3a413c74410 100644 --- a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue +++ b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue @@ -85,7 +85,6 @@ export default { <template> <img v-tooltip - class="avatar" :class="{ lazy: lazy, [avatarSizeClass]: true, @@ -99,5 +98,7 @@ export default { :data-container="tooltipContainer" :data-placement="tooltipPlacement" :title="tooltipText" + class="avatar" + data-boundary="window" /> </template> diff --git a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue index 6955d164def..01c36fec41a 100644 --- a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue +++ b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue @@ -84,8 +84,8 @@ export default { <template> <a - class="user-avatar-link" - :href="linkHref"> + :href="linkHref" + class="user-avatar-link"> <user-avatar-image :img-src="imgSrc" :img-alt="imgAlt" @@ -94,8 +94,8 @@ export default { :tooltip-text="avatarTooltipText" :tooltip-placement="tooltipPlacement" /><span - v-if="shouldShowUsername" v-tooltip + v-if="shouldShowUsername" :title="tooltipText" :tooltip-placement="tooltipPlacement" >{{ username }}</span> diff --git a/app/assets/javascripts/vue_shared/models/assignee.js b/app/assets/javascripts/vue_shared/models/assignee.js new file mode 100644 index 00000000000..4a29b0d0581 --- /dev/null +++ b/app/assets/javascripts/vue_shared/models/assignee.js @@ -0,0 +1,13 @@ +export default class ListAssignee { + constructor(obj, defaultAvatar) { + this.id = obj.id; + this.name = obj.name; + this.username = obj.username; + this.avatar = obj.avatar_url || obj.avatar || defaultAvatar; + this.path = obj.path; + this.state = obj.state; + this.webUrl = obj.web_url || obj.webUrl; + } +} + +window.ListAssignee = ListAssignee; diff --git a/app/assets/stylesheets/bootstrap_migration.scss b/app/assets/stylesheets/bootstrap_migration.scss index e24f8b1d4e8..6017e5554d8 100644 --- a/app/assets/stylesheets/bootstrap_migration.scss +++ b/app/assets/stylesheets/bootstrap_migration.scss @@ -24,16 +24,54 @@ html { font-size: 14px; } +legend { + border-bottom: 1px solid $border-color; + margin-bottom: 20px; +} + button, html [type="button"], [type="reset"], -[type="submit"] { +[type="submit"], +[role="button"] { // Override bootstrap reboot -webkit-appearance: inherit; + cursor: pointer; } -[role="button"] { - cursor: pointer; +h1, +h2, +h3, +h4, +h5, +h6 { + color: $gl-text-color; + font-weight: 600; +} + +h1, +.h1, +h2, +.h2, +h3, +.h3 { + margin-top: 20px; + margin-bottom: 10px; +} + +h4, +.h4, +h5, +.h5, +h6, +.h6 { + margin-top: 10px; + margin-bottom: 10px; +} + +h5, +.h5 { + font-size: $gl-font-size; } input[type="file"] { @@ -51,7 +89,12 @@ a { color: $gl-link-color; } +hr { + overflow: hidden; +} + .form-group.row .col-form-label { + padding-top: 0; // Bootstrap 4 aligns labels to the left // for horizontal forms @include media-breakpoint-up(md) { @@ -59,10 +102,25 @@ a { } } +kbd { + display: inline-block; +} + code { padding: 2px 4px; + color: $red-600; background-color: $red-100; border-radius: 3px; + + .code > & { + background-color: inherit; + padding: unset; + } + + .build-trace & { + background-color: inherit; + padding: inherit; + } } table { @@ -70,11 +128,6 @@ table { border-spacing: 0; } -.tooltip { - // Fix bootstrap4 bug whereby tooltips flicker when they are hovered over their borders - pointer-events: none; -} - .popover { font-size: 14px; } @@ -93,6 +146,16 @@ table { color: $gl-text-color-secondary !important; } +.bg-success, +.bg-primary, +.bg-info, +.bg-danger, +.bg-warning { + .card-header { + color: $white-light; + } +} + // Polyfill deprecated selectors .hidden { @@ -145,6 +208,19 @@ table { &:not(:last-of-type) { border-bottom: 1px solid $well-inner-border; } + + p, + ol, + ul, + .form-group { + &:last-of-type { + margin-bottom: 0; + } + } + } + + .badge.badge-gray { + background-color: $well-expand-item; } } @@ -166,9 +242,21 @@ table { } } +.card-header { + h3.card-title, + h4.card-title { + margin-top: 0; + } +} + .nav-tabs { + // Override bootstrap's default border + border-bottom: 0; + .nav-link { - border: 0; + border-top: 0; + border-left: 0; + border-right: 0; } .nav-item { @@ -179,3 +267,60 @@ table { pre code { white-space: pre-wrap; } + +.alert, +.flash-notice { + border-radius: 0; +} + +.alert-success { + background-color: $green-500; + border-color: $green-500; +} + +.alert-info { + background-color: $blue-500; + border-color: $blue-500; +} + +.alert-warning { + background-color: $orange-500; + border-color: $orange-500; +} + +.alert-danger { + background-color: $red-500; + border-color: $red-500; +} + +.alert-success, +.alert-info, +.alert-warning, +.alert-danger, +.flash-notice { + color: $white-light; + + h4, + a, + .alert-link { + color: $white-light; + } +} + +input[type=color].form-control { + height: $input-height; +} + +.toggle-sidebar-button { + .collapse-text, + .icon-angle-double-left, + .icon-angle-double-right { + color: $gl-text-color-secondary; + } +} + +.project-templates-buttons { + .btn { + vertical-align: unset; + } +} diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss index c5be27f2d29..0de05548c68 100644 --- a/app/assets/stylesheets/framework/blocks.scss +++ b/app/assets/stylesheets/framework/blocks.scss @@ -16,6 +16,7 @@ .click-to-expand { cursor: pointer; + vertical-align: initial; } } } diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss index 0115f542c88..523fcb05a87 100644 --- a/app/assets/stylesheets/framework/buttons.scss +++ b/app/assets/stylesheets/framework/buttons.scss @@ -121,10 +121,6 @@ &.btn-sm { margin-left: $btn-sm-side-margin; } - - &.btn-xs { - margin-left: $btn-xs-side-margin; - } } @mixin btn-svg { @@ -150,10 +146,6 @@ line-height: 18px; } - &.btn-xs { - padding: 2px 5px; - } - &.btn-success, &.btn-new, &.btn-create, @@ -497,6 +489,10 @@ fieldset[disabled] .btn, } } +[readonly] { + cursor: default; +} + .btn-no-padding { padding: 0; } diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index 1e7b9534275..326499125fc 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -305,14 +305,6 @@ img.emoji { margin-bottom: 10px; } -.btn-sign-in { - text-shadow: none; - - @include media-breakpoint-up(sm) { - margin-top: 8px; - } -} - .side-filters { fieldset { margin-bottom: 15px; diff --git a/app/assets/stylesheets/framework/contextual_sidebar.scss b/app/assets/stylesheets/framework/contextual_sidebar.scss index 1a415e1b852..9cbaaa5dc8d 100644 --- a/app/assets/stylesheets/framework/contextual_sidebar.scss +++ b/app/assets/stylesheets/framework/contextual_sidebar.scss @@ -26,19 +26,25 @@ margin-right: 2px; width: $contextual-sidebar-width; - a { + > a, + > button { transition: padding $sidebar-transition-duration; font-weight: $gl-font-weight-bold; display: flex; + width: 100%; align-items: center; padding: 10px 16px 10px 10px; color: $gl-text-color; - } + background-color: transparent; + border: 0; + text-align: left; - &:hover, - a:hover { - background-color: $link-hover-background; - color: $gl-text-color; + &:hover, + &:focus { + background-color: $link-hover-background; + color: $gl-text-color; + outline: 0; + } } .avatar-container { diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index b91d579cae6..74475daae14 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -35,6 +35,12 @@ @include media-breakpoint-down(xs) { width: 100%; } + + &.projects-dropdown-menu { + padding: 0; + overflow-y: initial; + max-height: initial; + } } .dropdown-toggle, diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss index f77ec4b6a2c..f060254777c 100644 --- a/app/assets/stylesheets/framework/files.scss +++ b/app/assets/stylesheets/framework/files.scss @@ -400,3 +400,51 @@ span.idiff { color: $common-gray-light; border: 1px solid $common-gray-light; } + +.preview-container { + height: 100%; + overflow: auto; + + .file-container { + background-color: $gray-darker; + display: flex; + height: 100%; + align-items: center; + justify-content: center; + + text-align: center; + + .file-content { + padding: $gl-padding; + max-width: 100%; + max-height: 100%; + + img { + max-width: 90%; + max-height: 70vh; + } + + .is-zoomable { + cursor: pointer; + cursor: zoom-in; + + &.is-zoomed { + cursor: pointer; + cursor: zoom-out; + max-width: none; + max-height: none; + margin-right: $gl-padding; + } + } + } + + .file-info { + font-size: $label-font-size; + color: $diff-image-info-color; + } + } + + .md-previewer { + padding: $gl-padding; + } +} diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss index 0ee5748952a..551a7e852ae 100644 --- a/app/assets/stylesheets/framework/filters.scss +++ b/app/assets/stylesheets/framework/filters.scss @@ -299,6 +299,7 @@ height: 14px; width: 14px; vertical-align: middle; + margin-bottom: 4px; } .dropdown-toggle-text { diff --git a/app/assets/stylesheets/framework/flash.scss b/app/assets/stylesheets/framework/flash.scss index 52c3f18a682..a6e324036ae 100644 --- a/app/assets/stylesheets/framework/flash.scss +++ b/app/assets/stylesheets/framework/flash.scss @@ -10,6 +10,20 @@ @extend .alert; background-color: $blue-500; margin: 0; + + &.flash-notice-persistent { + background-color: $blue-100; + color: $gl-text-color; + + a { + color: $gl-link-color; + + &:hover { + color: $gl-link-hover-color; + text-decoration: none; + } + } + } } .flash-warning { diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss index c76ea532912..03520f42997 100644 --- a/app/assets/stylesheets/framework/forms.scss +++ b/app/assets/stylesheets/framework/forms.scss @@ -170,7 +170,7 @@ label { } .form-control::-webkit-input-placeholder { - color: $gl-text-color-secondary; + color: $placeholder-text-color; } .input-group { diff --git a/app/assets/stylesheets/framework/gfm.scss b/app/assets/stylesheets/framework/gfm.scss index e378e84ca1b..1cf12b1a015 100644 --- a/app/assets/stylesheets/framework/gfm.scss +++ b/app/assets/stylesheets/framework/gfm.scss @@ -19,6 +19,7 @@ .gfm-color_chip { display: inline-block; + line-height: 1; margin: 0 0 2px 4px; vertical-align: middle; border-radius: 3px; diff --git a/app/assets/stylesheets/framework/gitlab_theme.scss b/app/assets/stylesheets/framework/gitlab_theme.scss index 11e21edfc1b..b40d02f381a 100644 --- a/app/assets/stylesheets/framework/gitlab_theme.scss +++ b/app/assets/stylesheets/framework/gitlab_theme.scss @@ -3,26 +3,26 @@ */ @mixin gitlab-theme( - $color-100, - $color-200, - $color-500, - $color-700, - $color-800, - $color-900, + $location-badge-color, + $search-and-nav-links, + $active-tab-border, + $border-and-box-shadow, + $sidebar-text, + $nav-svg-color, $color-alternate ) { // Header .navbar-gitlab { - background-color: $color-900; + background-color: $nav-svg-color; .navbar-collapse { - color: $color-200; + color: $search-and-nav-links; } .container-fluid { .navbar-toggler { - border-left: 1px solid lighten($color-700, 10%); + border-left: 1px solid lighten($border-and-box-shadow, 10%); } } @@ -31,40 +31,40 @@ > li { > a:hover, > a:focus { - background-color: rgba($color-200, 0.2); + background-color: rgba($search-and-nav-links, 0.2); } &.active > a, - &.dropdown.open > a { - color: $color-900; + &.dropdown.show > a { + color: $nav-svg-color; background-color: $color-alternate; } &.line-separator { - border-left: 1px solid rgba($color-200, 0.2); + border-left: 1px solid rgba($search-and-nav-links, 0.2); } } } .navbar-sub-nav { - color: $color-200; + color: $search-and-nav-links; } .nav { > li { - color: $color-200; + color: $search-and-nav-links; > a { &.header-user-dropdown-toggle { .header-user-avatar { - border-color: $color-200; + border-color: $search-and-nav-links; } } &:hover, &:focus { @include media-breakpoint-up(sm) { - background-color: rgba($color-200, 0.2); + background-color: rgba($search-and-nav-links, 0.2); } svg { @@ -74,13 +74,13 @@ } &.active > a, - &.dropdown.open > a { - color: $color-900; + &.dropdown.show > a { + color: $nav-svg-color; background-color: $color-alternate; &:hover { svg { - fill: $color-900; + fill: $nav-svg-color; } } } @@ -88,7 +88,7 @@ .impersonated-user, .impersonated-user:hover { svg { - fill: $color-900; + fill: $nav-svg-color; } } } @@ -99,34 +99,34 @@ > a { &:hover, &:focus { - background-color: rgba($color-200, 0.2); + background-color: rgba($search-and-nav-links, 0.2); } } } .search { form { - background-color: rgba($color-200, 0.2); + background-color: rgba($search-and-nav-links, 0.2); &:hover { - background-color: rgba($color-200, 0.3); + background-color: rgba($search-and-nav-links, 0.3); } } .location-badge { - color: $color-100; - background-color: rgba($color-200, 0.1); - border-right: 1px solid $color-800; + color: $location-badge-color; + background-color: rgba($search-and-nav-links, 0.1); + border-right: 1px solid $sidebar-text; } .search-input::placeholder { - color: rgba($color-200, 0.8); + color: rgba($search-and-nav-links, 0.8); } .search-input-wrap { .search-icon, .clear-icon { - fill: rgba($color-200, 0.8); + fill: rgba($search-and-nav-links, 0.8); } } @@ -141,38 +141,34 @@ .search-input-wrap { .search-icon { - fill: rgba($color-200, 0.8); + fill: rgba($search-and-nav-links, 0.8); } } } } - .btn-sign-in { - background-color: $color-100; - color: $color-900; - } // Sidebar .nav-sidebar li.active { - box-shadow: inset 4px 0 0 $color-700; + box-shadow: inset 4px 0 0 $border-and-box-shadow; > a { - color: $color-800; + color: $sidebar-text; } svg { - fill: $color-800; + fill: $sidebar-text; } } .sidebar-top-level-items > li.active .badge.badge-pill { - color: $color-800; + color: $sidebar-text; } .nav-links li { &.active a, a.active { - border-bottom: 2px solid $color-500; + border-bottom: 2px solid $active-tab-border; .badge.badge-pill { font-weight: $gl-font-weight-bold; @@ -181,27 +177,27 @@ } .branch-header-title { - color: $color-700; + color: $border-and-box-shadow; } .ide-file-list .file.file-active { - color: $color-700; + color: $border-and-box-shadow; } .ide-sidebar-link { &.active { - color: $color-700; - box-shadow: inset 3px 0 $color-700; + color: $border-and-box-shadow; + box-shadow: inset 3px 0 $border-and-box-shadow; &.is-right { - box-shadow: inset -3px 0 $color-700; + box-shadow: inset -3px 0 $border-and-box-shadow; } } } } body { - &.ui_indigo { + &.ui-indigo { @include gitlab-theme( $indigo-100, $indigo-200, @@ -213,19 +209,19 @@ body { ); } - &.ui_dark { + &.ui-light-indigo { @include gitlab-theme( - $theme-gray-100, - $theme-gray-200, - $theme-gray-500, - $theme-gray-700, - $theme-gray-800, - $theme-gray-900, + $indigo-100, + $indigo-200, + $indigo-500, + $indigo-500, + $indigo-700, + $indigo-700, $white-light ); } - &.ui_blue { + &.ui-blue { @include gitlab-theme( $theme-blue-100, $theme-blue-200, @@ -237,7 +233,19 @@ body { ); } - &.ui_green { + &.ui-light-blue { + @include gitlab-theme( + $theme-light-blue-100, + $theme-light-blue-200, + $theme-light-blue-500, + $theme-light-blue-500, + $theme-light-blue-700, + $theme-light-blue-700, + $white-light + ); + } + + &.ui-green { @include gitlab-theme( $theme-green-100, $theme-green-200, @@ -249,7 +257,55 @@ body { ); } - &.ui_light { + &.ui-light-green { + @include gitlab-theme( + $theme-green-100, + $theme-green-200, + $theme-green-500, + $theme-green-500, + $theme-light-green-700, + $theme-light-green-700, + $white-light + ); + } + + &.ui-red { + @include gitlab-theme( + $theme-red-100, + $theme-red-200, + $theme-red-500, + $theme-red-700, + $theme-red-800, + $theme-red-900, + $white-light + ); + } + + &.ui-light-red { + @include gitlab-theme( + $theme-light-red-100, + $theme-light-red-200, + $theme-light-red-500, + $theme-light-red-500, + $theme-light-red-700, + $theme-light-red-700, + $white-light + ); + } + + &.ui-dark { + @include gitlab-theme( + $theme-gray-100, + $theme-gray-200, + $theme-gray-500, + $theme-gray-700, + $theme-gray-800, + $theme-gray-900, + $white-light + ); + } + + &.ui-light { @include gitlab-theme( $theme-gray-900, $theme-gray-700, diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index 2085e5646ef..db59c91e375 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -139,6 +139,8 @@ } .nav { + flex-wrap: nowrap; + > li:not(.d-none) a { @include media-breakpoint-down(xs) { margin-left: 0; @@ -158,11 +160,12 @@ } .navbar-toggler { + position: relative; right: -10px; border-radius: 0; min-width: 45px; padding: 0; - margin-right: -7px; + margin: $gl-padding-8 -7px $gl-padding-8 0; font-size: 14px; text-align: center; color: currentColor; @@ -186,6 +189,7 @@ display: -webkit-flex; display: flex; padding-right: 10px; + flex-direction: row; } li { @@ -264,6 +268,8 @@ .navbar-sub-nav, .navbar-nav { + align-items: center; + > li { > a:hover, > a:focus { @@ -290,6 +296,10 @@ margin: 8px; } } + + .dropdown-menu { + position: absolute; + } } .navbar-sub-nav { @@ -297,12 +307,6 @@ display: flex; margin: 0 0 0 6px; - .projects-dropdown-menu { - padding: 0; - overflow-y: initial; - max-height: initial; - } - .dropdown-chevron { position: relative; top: -1px; @@ -443,12 +447,18 @@ } .btn-sign-in { - margin-top: 3px; + background-color: $indigo-100; + color: $indigo-900; font-weight: $gl-font-weight-bold; + line-height: 18px; &:hover { background-color: $white-light; } + + @include media-breakpoint-down(xs) { + margin-top: $gl-padding-4; + } } .navbar-nav { diff --git a/app/assets/stylesheets/framework/layout.scss b/app/assets/stylesheets/framework/layout.scss index 0536c39cee7..52b5f059f20 100644 --- a/app/assets/stylesheets/framework/layout.scss +++ b/app/assets/stylesheets/framework/layout.scss @@ -54,10 +54,6 @@ body { &.limit-container-width { max-width: $limited-layout-width; } - - &.limit-container-width-sm { - max-width: $limited-layout-width-sm; - } } .alert-wrapper { @@ -115,9 +111,3 @@ body { .with-performance-bar .layout-page { margin-top: $header-height + $performance-bar-height; } - -.vertical-center { - min-height: 100vh; - display: flex; - align-items: center; -} diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss index b893151e4fe..7290a174668 100644 --- a/app/assets/stylesheets/framework/markdown_area.scss +++ b/app/assets/stylesheets/framework/markdown_area.scss @@ -61,10 +61,6 @@ padding-top: 0; line-height: 19px; - &.btn.btn-xs { - padding: 2px 5px; - } - &:focus { margin-top: -10px; padding-top: 10px; diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss index d76cf8f8182..0b645eb811b 100644 --- a/app/assets/stylesheets/framework/mixins.scss +++ b/app/assets/stylesheets/framework/mixins.scss @@ -186,6 +186,7 @@ overflow-y: hidden; -webkit-overflow-scrolling: touch; display: flex; + flex-wrap: nowrap; &::-webkit-scrollbar { display: none; diff --git a/app/assets/stylesheets/framework/modal.scss b/app/assets/stylesheets/framework/modal.scss index a7896cc3fc3..ffb40166c15 100644 --- a/app/assets/stylesheets/framework/modal.scss +++ b/app/assets/stylesheets/framework/modal.scss @@ -1,3 +1,7 @@ +.modal-xl { + max-width: 98%; +} + .modal-header { background-color: $modal-body-bg; diff --git a/app/assets/stylesheets/framework/pagination.scss b/app/assets/stylesheets/framework/pagination.scss index d3e013590b6..61d02511ff4 100644 --- a/app/assets/stylesheets/framework/pagination.scss +++ b/app/assets/stylesheets/framework/pagination.scss @@ -1,91 +1,14 @@ .gl-pagination { - text-align: center; - border-top: 1px solid $border-color; - margin: 0; - margin-top: 0; - - .pagination { - padding: 0; - margin: 20px 0; - - a { - cursor: pointer; - } - - .separator, - .separator:hover { - a { - cursor: default; - background-color: $gray-light; - padding: $gl-vert-padding; - } - } + a { + color: inherit; + text-decoration: none; } - - - .gap, - .gap:hover { - background-color: $gray-light; - padding: $gl-vert-padding; - cursor: default; - } -} - -.card > .gl-pagination { - margin: 0; } -/** - * Extra-small screen pagination. - */ -@media (max-width: 320px) { - .gl-pagination { - .first, - .last { - display: none; - } - - .page-item { - display: none; - - &.active { - display: inline; - } - } - } -} - -/** - * Small screen pagination - */ -@include media-breakpoint-down(xs) { - .gl-pagination { - .pagination li a { - padding: 6px 10px; - } - - .page-item { - display: none; - - &.active { - display: inline; - } - } - } -} - -/** - * Medium screen pagination - */ -@media (min-width: map-get($grid-breakpoints, xs)) and (max-width: map-get($grid-breakpoints, sm)) { - .gl-pagination { - .page-item { - display: none; - - &.active, - &.sibling { - display: inline; - } +.page-item { + &.active { + .page-link { + z-index: 3; } } } diff --git a/app/assets/stylesheets/framework/secondary_navigation_elements.scss b/app/assets/stylesheets/framework/secondary_navigation_elements.scss index 847fc8c0792..9dbb04e5443 100644 --- a/app/assets/stylesheets/framework/secondary_navigation_elements.scss +++ b/app/assets/stylesheets/framework/secondary_navigation_elements.scss @@ -197,7 +197,7 @@ flex-flow: row wrap; .nav-controls { - $controls-margin: $btn-xs-side-margin - 2px; + $controls-margin: $btn-margin-5 - 2px; flex: 0 0 100%; &.controls-flex { @@ -230,6 +230,8 @@ } .scrolling-tabs-container { + position: relative; + .merge-request-tabs-container & { overflow: hidden; } @@ -345,7 +347,7 @@ .empty-state .project-item-select-holder.btn-group { float: none; - display: inline-block; + justify-content: center; .btn { // overrides styles applied to plain `.empty-state .btn` diff --git a/app/assets/stylesheets/framework/tables.scss b/app/assets/stylesheets/framework/tables.scss index a10bd1544c5..339388392df 100644 --- a/app/assets/stylesheets/framework/tables.scss +++ b/app/assets/stylesheets/framework/tables.scss @@ -1,5 +1,6 @@ .table-holder { margin: 0; + overflow: auto; } table { @@ -38,6 +39,11 @@ table { &.wide { width: 55%; } + + &.table-th-transparent { + background: none; + color: $gl-text-color-secondary; + } } td { @@ -45,9 +51,91 @@ table { } } } + + &.responsive-table { + @include media-breakpoint-down(sm) { + thead { + display: none; + } + + &, + tbody, + td { + display: block; + } + + td { + color: $gl-text-color-secondary; + } + + tbody td.responsive-table-cell { + padding: $gl-padding 0; + width: 100%; + display: flex; + text-align: right; + align-items: center; + justify-content: space-between; + + &[data-column]::before { + content: attr(data-column); + display: block; + text-align: left; + padding-right: $gl-padding; + color: $gl-text-color-secondary; + } + + &:not([data-column]) { + flex-direction: row-reverse; + } + } + + tr.responsive-table-border-start, + tr.responsive-table-border-end { + display: block; + border: solid $gl-text-color-quaternary; + padding-left: 0; + padding-right: 0; + + > td { + border-color: $gl-text-color-quaternary; + + &, + &:last-child { + padding-left: $gl-padding; + padding-right: $gl-padding; + } + } + } + + tr.responsive-table-border-start { + border-width: 1px 1px 0; + border-radius: $border-radius-default $border-radius-default 0 0; + padding-top: 0; + padding-bottom: 0; + + > td:first-child { + border-top: 0; // always have the <table> top border + } + + > td:last-child { + border-bottom: 1px solid $gl-text-color-quaternary; + } + } + + tr.responsive-table-border-end { + border-width: 0 1px 1px; + border-radius: 0 0 $border-radius-default $border-radius-default; + margin-bottom: 2 * $gl-padding; + + > :last-child { + border-bottom: 0; + } + } + } + } } -.responsive-table { +.responsive-table:not(table) { @include media-breakpoint-down(sm) { th { width: 100%; diff --git a/app/assets/stylesheets/framework/terms.scss b/app/assets/stylesheets/framework/terms.scss index 744fd0ff796..7cda674e5c8 100644 --- a/app/assets/stylesheets/framework/terms.scss +++ b/app/assets/stylesheets/framework/terms.scss @@ -11,15 +11,15 @@ padding-top: $gl-padding; } - .panel { - .panel-heading { + .card { + .card-header { display: -webkit-flex; display: flex; align-items: center; justify-content: space-between; line-height: $line-height-base; - .title { + .card-title { display: flex; align-items: center; @@ -34,6 +34,8 @@ .navbar-collapse { padding-right: 0; + flex-grow: 0; + flex-basis: auto; .navbar-nav { margin: 0; diff --git a/app/assets/stylesheets/framework/timeline.scss b/app/assets/stylesheets/framework/timeline.scss index 75c11590547..dfb145debe7 100644 --- a/app/assets/stylesheets/framework/timeline.scss +++ b/app/assets/stylesheets/framework/timeline.scss @@ -4,7 +4,7 @@ padding: 0; &::before { - @include notes-media('max', map-get($grid-breakpoints, xs)) { + @include notes-media('max', map-get($grid-breakpoints, sm)) { background: none; } } @@ -34,7 +34,7 @@ .timeline-entry-inner { position: relative; - @include notes-media('max', map-get($grid-breakpoints, xs)) { + @include notes-media('max', map-get($grid-breakpoints, sm)) { .timeline-icon { display: none; } diff --git a/app/assets/stylesheets/framework/toggle.scss b/app/assets/stylesheets/framework/toggle.scss index d5cc78a6680..20394cc1e52 100644 --- a/app/assets/stylesheets/framework/toggle.scss +++ b/app/assets/stylesheets/framework/toggle.scss @@ -42,6 +42,10 @@ background: none; } + &:focus { + outline: none; + } + .toggle-icon { position: relative; display: block; diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss index 97b821e0cb9..9e77ea03a24 100644 --- a/app/assets/stylesheets/framework/typography.scss +++ b/app/assets/stylesheets/framework/typography.scss @@ -114,26 +114,27 @@ font-size: 0.95em; } + blockquote, .blockquote { color: $gl-grayish-blue; font-size: inherit; padding: 8px 24px; margin: 16px 0; border-left: 3px solid $white-dark; - } - .blockquote:dir(rtl) { - border-left: 0; - border-right: 3px solid $white-dark; - } + &:dir(rtl) { + border-left: 0; + border-right: 3px solid $white-dark; + } - .blockquote p { - color: $gl-grayish-blue !important; - font-size: inherit; - line-height: 1.5; + p { + color: $gl-grayish-blue !important; + font-size: inherit; + line-height: 1.5; - &:last-child { - margin: 0; + &:last-child { + margin: 0; + } } } diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 946223cfff0..f30f296d41f 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -99,7 +99,7 @@ $theme-gray-200: #dfdfdf; $theme-gray-300: #cccccc; $theme-gray-400: #bababa; $theme-gray-500: #a7a7a7; -$theme-gray-600: #949494; +$theme-gray-600: #919191; $theme-gray-700: #707070; $theme-gray-800: #4f4f4f; $theme-gray-900: #2e2e2e; @@ -117,6 +117,15 @@ $theme-blue-800: #25496e; $theme-blue-900: #1a3652; $theme-blue-950: #0f2235; +$theme-light-blue-50: #f2f7fc; +$theme-light-blue-100: #ebf1f7; +$theme-light-blue-200: #c9dcf2; +$theme-light-blue-300: #83abd4; +$theme-light-blue-400: #4d86bf; +$theme-light-blue-500: #367cc2; +$theme-light-blue-600: #3771ab; +$theme-light-blue-700: #2261a1; + $theme-green-50: #f2faf6; $theme-green-100: #e4f3ea; $theme-green-200: #c0dfcd; @@ -129,6 +138,29 @@ $theme-green-800: #145d33; $theme-green-900: #0d4524; $theme-green-950: #072d16; +$theme-light-green-700: #156b39; + +$theme-red-50: #fcf4f2; +$theme-red-100: #fae9e6; +$theme-red-200: #ebcac5; +$theme-red-300: #d99b91; +$theme-red-400: #b0655a; +$theme-red-500: #ad4a3b; +$theme-red-600: #9e4133; +$theme-red-700: #912f20; +$theme-red-800: #78291d; +$theme-red-900: #691a16; +$theme-red-950: #36140f; + +$theme-light-red-50: #fff6f5; +$theme-light-red-100: #fae2de; +$theme-light-red-200: #f7d5d0; +$theme-light-red-300: #d9796a; +$theme-light-red-400: #cf604e; +$theme-light-red-500: #c24b38; +$theme-light-red-600: #b03927; +$theme-light-red-700: #a62e21; + $black: #000; $black-transparent: rgba(0, 0, 0, 0.3); $almost-black: #242424; @@ -142,11 +174,6 @@ $border-gray-normal-dashed: darken($gray-normal, $darken-border-dashed-factor); $border-gray-dark: darken($white-normal, $darken-border-factor); /* - * Override Bootstrap 4 variables - */ -$secondary: $gray-light; - -/* * UI elements */ $border-color: #e5e5e5; @@ -164,7 +191,7 @@ $gl-font-weight-normal: 400; $gl-font-weight-bold: 600; $gl-text-color: #2e2e2e; $gl-text-color-secondary: #707070; -$gl-text-color-tertiary: #949494; +$gl-text-color-tertiary: #919191; $gl-text-color-quaternary: #d6d6d6; $gl-text-color-inverted: rgba(255, 255, 255, 1); $gl-text-color-secondary-inverted: rgba(255, 255, 255, 0.85); @@ -217,10 +244,11 @@ $tooltip-font-size: 12px; /* * Padding */ -$gl-padding-24: 24px; -$gl-padding: 16px; -$gl-padding-8: 8px; $gl-padding-4: 4px; +$gl-padding-8: 8px; +$gl-padding: 16px; +$gl-padding-24: 24px; +$gl-padding-32: 32px; $gl-col-padding: 15px; $gl-input-padding: 10px; $gl-vert-padding: 6px; @@ -238,7 +266,6 @@ $header-height: 40px; $ide-statusbar-height: 25px; $fixed-layout-width: 1280px; $limited-layout-width: 990px; -$limited-layout-width-sm: 790px; $container-text-max-width: 540px; $gl-avatar-size: 40px; $error-exclamation-point: $red-500; @@ -251,7 +278,7 @@ $active-item-blue: $blue-500; $layout-link-gray: #7e7c7c; $btn-side-margin: 10px; $btn-sm-side-margin: 7px; -$btn-xs-side-margin: 5px; +$btn-margin-5: 5px; $issue-status-expired: $orange-500; $issuable-sidebar-color: $gl-text-color-secondary; $sidebar-block-hover-color: #ebebeb; @@ -408,6 +435,22 @@ $badge-bg: rgba(0, 0, 0, 0.07); $badge-color: $gl-text-color-secondary; /* +* Pagination +*/ +$pagination-padding-y: 6px; +$pagination-padding-x: 16px; +$pagination-line-height: 20px; +$pagination-border-color: $border-color; +$pagination-active-bg: $blue-600; +$pagination-active-border-color: $blue-600; +$pagination-hover-bg: $blue-50; +$pagination-hover-border-color: $border-color; +$pagination-hover-color: $gl-text-color; +$pagination-disabled-color: #cdcdcd; +$pagination-disabled-bg: $gray-light; +$pagination-disabled-border-color: $border-color; + +/* * Status icons */ $status-icon-size: 22px; @@ -776,3 +819,19 @@ $modal-body-height: 134px; Prometheus */ $prometheus-table-row-highlight-color: $theme-gray-100; + +$priority-label-empty-state-width: 114px; + +/* + * Override Bootstrap 4 variables + */ + +$secondary: $gray-light; +$input-disabled-bg: $gray-light; +$input-border-color: $theme-gray-200; +$input-color: $gl-text-color; +$font-family-sans-serif: $regular_font; +$font-family-monospace: $monospace_font; +$input-line-height: 20px; +$btn-line-height: 20px; +$table-accent-bg: $gray-light; diff --git a/app/assets/stylesheets/mailers/highlighted_diff_email.scss b/app/assets/stylesheets/mailers/highlighted_diff_email.scss index b5eda79e5ed..1835c4364d3 100644 --- a/app/assets/stylesheets/mailers/highlighted_diff_email.scss +++ b/app/assets/stylesheets/mailers/highlighted_diff_email.scss @@ -138,6 +138,7 @@ pre { margin: 0; } +blockquote, .blockquote { color: $gl-grayish-blue; padding: 0 0 0 15px; diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss index 1c3d312f7ac..7c1d1626f1c 100644 --- a/app/assets/stylesheets/pages/boards.scss +++ b/app/assets/stylesheets/pages/boards.scss @@ -80,6 +80,7 @@ overflow-x: scroll; white-space: nowrap; min-height: 200px; + display: flex; @include media-breakpoint-only(sm) { height: calc(100vh - #{$issue-board-list-difference-sm}); @@ -110,17 +111,15 @@ .board { display: inline-block; - width: calc(85vw - 15px); + flex: 1; + min-width: 300px; + max-width: 400px; height: 100%; padding-right: ($gl-padding / 2); padding-left: ($gl-padding / 2); white-space: normal; vertical-align: top; - @include media-breakpoint-up(sm) { - width: 400px; - } - &.is-expandable { .board-header { cursor: pointer; @@ -128,6 +127,8 @@ } &.is-collapsed { + flex: none; + min-width: 0; width: 50px; .board-header { @@ -282,9 +283,6 @@ box-shadow: 0 1px 2px $issue-boards-card-shadow; list-style: none; - // as a fallback, hide overflow content so that dragging and dropping still works - overflow: hidden; - &:not(:last-child) { margin-bottom: 5px; } diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss index 9ee02ca1d83..f030189af06 100644 --- a/app/assets/stylesheets/pages/builds.scss +++ b/app/assets/stylesheets/pages/builds.scss @@ -12,26 +12,22 @@ @keyframes blinking-dots { 0% { background-color: rgba($white-light, 1); - box-shadow: 12px 0 0 0 rgba($white-light, 0.2), - 24px 0 0 0 rgba($white-light, 0.2); + box-shadow: 12px 0 0 0 rgba($white-light, 0.2), 24px 0 0 0 rgba($white-light, 0.2); } 25% { background-color: rgba($white-light, 0.4); - box-shadow: 12px 0 0 0 rgba($white-light, 2), - 24px 0 0 0 rgba($white-light, 0.2); + box-shadow: 12px 0 0 0 rgba($white-light, 2), 24px 0 0 0 rgba($white-light, 0.2); } 75% { background-color: rgba($white-light, 0.4); - box-shadow: 12px 0 0 0 rgba($white-light, 0.2), - 24px 0 0 0 rgba($white-light, 1); + box-shadow: 12px 0 0 0 rgba($white-light, 0.2), 24px 0 0 0 rgba($white-light, 1); } 100% { background-color: rgba($white-light, 1); - box-shadow: 12px 0 0 0 rgba($white-light, 0.2), - 24px 0 0 0 rgba($white-light, 0.2); + box-shadow: 12px 0 0 0 rgba($white-light, 0.2), 24px 0 0 0 rgba($white-light, 0.2); } } @@ -71,10 +67,15 @@ .bash { display: block; } + + &.build-trace-rounded { + border-radius: $border-radius-base; + } } .top-bar { height: 35px; + min-height: 35px; background: $gray-light; border: 1px solid $border-color; color: $gl-text-color; diff --git a/app/assets/stylesheets/pages/clusters.scss b/app/assets/stylesheets/pages/clusters.scss index 3e4d123242c..56beb7718a4 100644 --- a/app/assets/stylesheets/pages/clusters.scss +++ b/app/assets/stylesheets/pages/clusters.scss @@ -13,6 +13,10 @@ max-width: 100%; } +.clusters-error-alert { + width: 100%; +} + .clusters-container { .nav-bar-right { padding: $gl-padding-top $gl-padding; diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss index a4ca82de90e..dc8842212e0 100644 --- a/app/assets/stylesheets/pages/commits.scss +++ b/app/assets/stylesheets/pages/commits.scss @@ -193,6 +193,10 @@ display: inline-flex; } + .ci-status-icon svg { + vertical-align: text-bottom; + } + > .ci-status-link, > .btn, > .commit-sha-group { diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index f06c9dcdf8c..fbc97ec0c95 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -189,8 +189,22 @@ img { border: 1px solid $white-light; - background-image: linear-gradient(45deg, $border-color 25%, transparent 25%, transparent 75%, $border-color 75%, $border-color 100%), - linear-gradient(45deg, $border-color 25%, transparent 25%, transparent 75%, $border-color 75%, $border-color 100%); + background-image: linear-gradient( + 45deg, + $border-color 25%, + transparent 25%, + transparent 75%, + $border-color 75%, + $border-color 100% + ), + linear-gradient( + 45deg, + $border-color 25%, + transparent 25%, + transparent 75%, + $border-color 75%, + $border-color 100% + ); background-size: 10px 10px; background-position: 0 0, 5px 5px; max-width: 100%; @@ -395,6 +409,69 @@ .line_content { white-space: pre-wrap; } + + .diff-file-container { + .frame.deleted { + border: 0; + background-color: inherit; + + .image_file img { + border: 1px solid $deleted; + } + } + + .frame.added { + border: 0; + background-color: inherit; + + .image_file img { + border: 1px solid $added; + } + } + + .swipe.view, + .onion-skin.view { + .swipe-wrap { + top: 0; + right: 0; + } + + .frame.deleted { + top: 0; + right: 0; + } + + .swipe-bar { + top: 0; + + .top-handle { + top: -14px; + left: -7px; + } + + .bottom-handle { + bottom: -14px; + left: -7px; + } + } + + .file-container { + display: inline-block; + + .file-content { + padding: 0; + + img { + max-width: none; + } + } + } + } + + .onion-skin.view .controls { + bottom: -25px; + } + } } .file-content .diff-file { @@ -536,7 +613,7 @@ margin-right: 0; border-color: $white-light; cursor: pointer; - transition: all .1s ease-out; + transition: all 0.1s ease-out; @for $i from 1 through 4 { &:nth-child(#{$i}) { @@ -563,7 +640,7 @@ height: 24px; border-radius: 50%; padding: 0; - transition: transform .1s ease-out; + transition: transform 0.1s ease-out; z-index: 100; .collapse-icon { @@ -708,11 +785,35 @@ width: 100%; height: 10px; background-color: $white-light; - background-image: linear-gradient(45deg, transparent, transparent 73%, $diff-jagged-border-gradient-color 75%, $white-light 80%), - linear-gradient(225deg, transparent, transparent 73%, $diff-jagged-border-gradient-color 75%, $white-light 80%), - linear-gradient(135deg, transparent, transparent 73%, $diff-jagged-border-gradient-color 75%, $white-light 80%), - linear-gradient(-45deg, transparent, transparent 73%, $diff-jagged-border-gradient-color 75%, $white-light 80%); - background-position: 5px 5px,0 5px,0 5px,5px 5px; + background-image: linear-gradient( + 45deg, + transparent, + transparent 73%, + $diff-jagged-border-gradient-color 75%, + $white-light 80% + ), + linear-gradient( + 225deg, + transparent, + transparent 73%, + $diff-jagged-border-gradient-color 75%, + $white-light 80% + ), + linear-gradient( + 135deg, + transparent, + transparent 73%, + $diff-jagged-border-gradient-color 75%, + $white-light 80% + ), + linear-gradient( + -45deg, + transparent, + transparent 73%, + $diff-jagged-border-gradient-color 75%, + $white-light 80% + ); + background-position: 5px 5px, 0 5px, 0 5px, 5px 5px; background-size: 10px 10px; background-repeat: repeat; } @@ -750,11 +851,16 @@ .frame.click-to-comment { position: relative; cursor: image-url('illustrations/image_comment_light_cursor.svg') - $image-comment-cursor-left-offset $image-comment-cursor-top-offset, auto; + $image-comment-cursor-left-offset $image-comment-cursor-top-offset, + auto; // Retina cursor - cursor: -webkit-image-set(image-url('illustrations/image_comment_light_cursor.svg') 1x, image-url('illustrations/image_comment_light_cursor@2x.svg') 2x) - $image-comment-cursor-left-offset $image-comment-cursor-top-offset, auto; + cursor: -webkit-image-set( + image-url('illustrations/image_comment_light_cursor.svg') 1x, + image-url('illustrations/image_comment_light_cursor@2x.svg') 2x + ) + $image-comment-cursor-left-offset $image-comment-cursor-top-offset, + auto; .comment-indicator { position: absolute; @@ -840,7 +946,7 @@ .diff-notes-collapse, .note, - .discussion-reply-holder, { + .discussion-reply-holder { display: none; } diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss index cd0d67613c3..06f08ae2215 100644 --- a/app/assets/stylesheets/pages/environments.scss +++ b/app/assets/stylesheets/pages/environments.scss @@ -23,7 +23,6 @@ } .btn-group { - > a { color: $gl-text-color-secondary; } @@ -245,6 +244,7 @@ .prometheus-graph { flex: 1 0 auto; min-width: 450px; + max-width: 100%; padding: $gl-padding / 2; h5 { @@ -256,6 +256,17 @@ } } +.prometheus-graph-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: $gl-padding-8; + + h5 { + margin: 0; + } +} + .prometheus-graph-cursor { position: absolute; background: $theme-gray-600; diff --git a/app/assets/stylesheets/pages/groups.scss b/app/assets/stylesheets/pages/groups.scss index c2b42e02eee..05bf5596fb3 100644 --- a/app/assets/stylesheets/pages/groups.scss +++ b/app/assets/stylesheets/pages/groups.scss @@ -425,7 +425,7 @@ margin-left: 5px; > .btn { - margin-right: $btn-xs-side-margin; + margin-right: $btn-margin-5; } } } diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 10a0e076cb4..f9fd9f1ab8b 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -485,6 +485,15 @@ .sidebar-collapsed-user { padding-bottom: 0; margin-bottom: 10px; + + .author_link { + padding-left: 0; + + .avatar { + position: static; + margin: 0; + } + } } .issuable-header-btn { diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss index e178371d21f..79cac7f4ff0 100644 --- a/app/assets/stylesheets/pages/labels.scss +++ b/app/assets/stylesheets/pages/labels.scss @@ -57,67 +57,6 @@ border-bottom-left-radius: $border-radius-base; } -.label-row { - .label-name { - display: inline-block; - margin-bottom: 10px; - - @include media-breakpoint-up(sm) { - width: 200px; - margin-left: $gl-padding * 2; - margin-bottom: 0; - } - - .badge { - overflow: hidden; - text-overflow: ellipsis; - max-width: 100%; - } - } - - .label-type { - display: block; - margin-bottom: 10px; - margin-left: 50px; - - @include media-breakpoint-up(sm) { - display: inline-block; - width: 100px; - margin-left: 10px; - margin-bottom: 0; - vertical-align: top; - } - } - - .label-description { - display: block; - margin-bottom: 10px; - - .description-text { - margin-bottom: $gl-padding; - } - - a { - color: $blue-600; - } - - @include media-breakpoint-up(sm) { - display: inline-block; - max-width: 50%; - margin-left: 10px; - margin-bottom: 0; - vertical-align: top; - } - } - - .badge { - padding: 4px $grid-size; - font-size: $label-font-size; - position: relative; - top: ($grid-size / 2); - } -} - .color-label { padding: 0 $grid-size; line-height: 16px; @@ -133,26 +72,29 @@ } .manage-labels-list { - @media(min-width: map-get($grid-breakpoints, md)) { - &.content-list li { - padding: $gl-padding 0; - } - } - > li:not(.empty-message):not(.is-not-draggable) { background-color: $white-light; - cursor: move; - cursor: -webkit-grab; - cursor: -moz-grab; - - &:active { - cursor: -webkit-grabbing; - cursor: -moz-grabbing; - } + margin-bottom: 5px; + display: flex; + justify-content: space-between; + padding: $gl-padding; + border-radius: $border-radius-default; &.sortable-ghost { opacity: 0.3; } + + .prioritized-labels & { + box-shadow: 0 1px 2px $issue-boards-card-shadow; + cursor: move; + cursor: -webkit-grab; + cursor: -moz-grab; + + &:active { + cursor: -webkit-grabbing; + cursor: -moz-grabbing; + } + } } .btn-action { @@ -171,28 +113,11 @@ } } - .dropdown { - @include media-breakpoint-up(sm) { - float: right; - } - } - - @include media-breakpoint-down(xs) { - .dropdown-menu { - min-width: 100%; - } + .color-label { + padding: $gl-padding-4 $grid-size; } } -.draggable-handler { - display: inline-block; - vertical-align: top; - margin: 5px 0; - opacity: 0; - transition: opacity .3s; - color: $gray-darkest; -} - .prioritized-labels { margin-bottom: 30px; @@ -215,22 +140,6 @@ } } -.toggle-priority { - display: inline-block; - vertical-align: top; - - button { - border-color: transparent; - padding: 5px 8px; - vertical-align: top; - font-size: 14px; - - &:hover { - border-color: transparent; - } - } -} - .filtered-labels { font-size: 0; padding: 12px 16px; @@ -284,10 +193,8 @@ } .label-subscribe-button { - @media(min-width: map-get($grid-breakpoints, md)) { - min-width: 105px; - margin-left: $gl-padding; - } + width: 105px; + font-weight: 200; .label-subscribe-button-icon { &[disabled] { @@ -313,7 +220,7 @@ .label-link { display: inline-flex; - vertical-align: top; + vertical-align: text-bottom; &:hover .color-label { text-decoration: underline; @@ -324,3 +231,95 @@ font-size: $label-font-size; } } + +.labels-container { + background-color: $gray-light; + border-radius: $border-radius-default; + padding: $gl-padding $gl-padding-8; +} + +.label-actions-list { + list-style: none; + flex-shrink: 0; + padding: 0; +} + +.label-badge { + color: $theme-gray-900; + font-weight: $gl-font-weight-normal; + padding: $gl-padding-4 $gl-padding-8; + border-radius: $border-radius-default; + font-size: $label-font-size; +} + +.label-badge-blue { + background-color: $theme-blue-100; +} + +.label-badge-gray { + background-color: $theme-gray-100; +} + +.label-links { + list-style: none; + padding: 0; + white-space: nowrap; +} + +.label-link-item { + padding: 0; +} + +.label-list-item { + .content-list &::before, + .content-list &::after { + content: none; + } + + .label-name { + width: 150px; + flex-shrink: 0; + + .badge { + overflow: hidden; + text-overflow: ellipsis; + max-width: 100%; + } + } + + .label-description { + flex-grow: 1; + + a { + color: $blue-600; + } + } + + .label { + padding: 4px $grid-size; + font-size: $label-font-size; + position: relative; + top: $gl-padding-4; + } + + .label-action { + color: $theme-gray-800; + cursor: pointer; + + svg { + fill: $theme-gray-800; + } + + &:hover { + color: $blue-600; + + svg { + fill: $blue-600; + } + } + } +} + +.priority-labels-empty-state .svg-content img { + max-width: $priority-label-empty-state-width; +} diff --git a/app/assets/stylesheets/pages/members.scss b/app/assets/stylesheets/pages/members.scss index 9914555d309..5fdb2b4a90a 100644 --- a/app/assets/stylesheets/pages/members.scss +++ b/app/assets/stylesheets/pages/members.scss @@ -121,10 +121,6 @@ background: transparent; border: 0; outline: 0; - - @include media-breakpoint-up(sm) { - right: 160px; - } } .flex-project-members-panel { diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index 9eceb3e9a33..99fe4a578be 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -51,12 +51,6 @@ opacity: 0.3; } - &.btn-xs { - line-height: 1; - padding: 5px 10px; - margin-top: 1px; - } - &.dropdown-toggle { .fa { color: inherit; @@ -678,6 +672,7 @@ .merge-request-tabs { display: flex; + flex-wrap: nowrap; margin-bottom: 0; padding: 0; } diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss index 3b037d066dc..3849a04db5d 100644 --- a/app/assets/stylesheets/pages/note_form.scss +++ b/app/assets/stylesheets/pages/note_form.scss @@ -129,7 +129,7 @@ .icon svg { position: relative; top: 2px; - margin-right: $btn-xs-side-margin; + margin-right: $btn-margin-5; width: $gl-font-size; height: $gl-font-size; fill: $orange-600; diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index f85f66b9c0b..52332ac97dd 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -36,6 +36,7 @@ } .table-holder { + overflow: unset; width: 100%; } @@ -321,18 +322,17 @@ } .build-failures { + th { + border-top: 0; + } + .build-state { padding: 20px 2px; .build-name { - float: right; font-weight: $gl-font-weight-normal; } - .ci-status-icon-failed svg { - vertical-align: middle; - } - .stage { color: $gl-text-color-secondary; font-weight: $gl-font-weight-normal; @@ -344,6 +344,81 @@ border: 0; line-height: initial; } + + .build-trace-row td { + border-top: 0; + border-bottom-width: 1px; + border-bottom-style: solid; + padding-top: 0; + } + + .build-trace { + width: 100%; + text-align: left; + margin-top: $gl-padding; + } + + .build-name { + width: 196px; + + a { + font-weight: $gl-font-weight-bold; + color: $gl-text-color; + text-decoration: none; + + &:focus, + &:hover { + text-decoration: underline; + } + } + } + + .build-actions { + width: 70px; + text-align: right; + } + + .build-stage { + width: 140px; + } + + .ci-status-icon-failed { + padding: 10px 0 10px 12px; + width: 12px + 24px; // padding-left + svg width + } + + .build-icon svg { + width: 24px; + height: 24px; + vertical-align: middle; + } + + .build-state, + .build-trace-row { + > td:last-child { + padding-right: 0; + } + } + + @include media-breakpoint-down(sm) { + td:empty { + display: none; + } + + .ci-table { + margin-top: 2 * $gl-padding; + } + + .build-trace-container { + padding-top: $gl-padding; + padding-bottom: $gl-padding; + } + + .build-trace { + margin-bottom: 0; + margin-top: 0; + } + } } .pipeline-tab-content { @@ -926,10 +1001,10 @@ button.mini-pipeline-graph-dropdown-toggle { /** * Center dropdown menu in mini graph */ - &.dropdown-menu { + .dropdown &.dropdown-menu { transform: translate(-80%, 0); - @media(min-width: map-get($grid-breakpoints, md)) { + @media (min-width: map-get($grid-breakpoints, md)) { transform: translate(-50%, 0); right: auto; left: 50%; diff --git a/app/assets/stylesheets/pages/profiles/preferences.scss b/app/assets/stylesheets/pages/profiles/preferences.scss index 68d40b56133..a353f301d07 100644 --- a/app/assets/stylesheets/pages/profiles/preferences.scss +++ b/app/assets/stylesheets/pages/profiles/preferences.scss @@ -1,25 +1,3 @@ -@mixin application-theme-preview($color-1, $color-2, $color-3, $color-4) { - .one { - background-color: $color-1; - border-top-left-radius: $border-radius-default; - } - - .two { - background-color: $color-2; - border-top-right-radius: $border-radius-default; - } - - .three { - background-color: $color-3; - border-bottom-left-radius: $border-radius-default; - } - - .four { - background-color: $color-4; - border-bottom-right-radius: $border-radius-default; - } -} - .multi-file-editor-options { label { margin-right: 20px; @@ -38,49 +16,67 @@ .application-theme { label { - margin-right: 20px; + margin: 0 $gl-padding-32 $gl-padding 0; text-align: center; } .preview { font-size: 0; - margin-bottom: 10px; + height: 48px; + border-radius: 4px; + min-width: 112px; + margin-bottom: $gl-padding-8; + + &.ui-indigo { + background-color: $indigo-900; + } - &.indigo { - @include application-theme-preview($indigo-900, $indigo-700, $indigo-800, $indigo-500); + &.ui-light-indigo { + background-color: $indigo-700; } - &.dark { - @include application-theme-preview($theme-gray-900, $theme-gray-700, $theme-gray-800, $theme-gray-600); + &.ui-blue { + background-color: $theme-blue-900; } - &.light { - @include application-theme-preview($theme-gray-600, $theme-gray-200, $theme-gray-400, $theme-gray-100); + &.ui-light-blue { + background-color: $theme-light-blue-700; } - &.blue { - @include application-theme-preview($theme-blue-900, $theme-blue-700, $theme-blue-800, $theme-blue-500); + &.ui-green { + background-color: $theme-green-900; } - &.green { - @include application-theme-preview($theme-green-900, $theme-green-700, $theme-green-800, $theme-green-500); + &.ui-light-green { + background-color: $theme-light-green-700; + } + + &.ui-red { + background-color: $theme-red-900; + } + + &.ui-light-red { + background-color: $theme-light-red-700; + } + + &.ui-dark { + background-color: $theme-gray-900; + } + + &.ui-light { + background-color: $theme-gray-200; } } .preview-row { display: block; } - - .quadrant { - display: inline-block; - height: 50px; - width: 80px; - } } .syntax-theme { label { - margin-right: 20px; + margin-right: $gl-padding-32; + margin-bottom: $gl-padding; text-align: center; .preview { @@ -89,7 +85,6 @@ img { border-radius: 4px; - max-width: 100%; } } diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 22964163e95..aa83e5bdebc 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -354,12 +354,6 @@ min-width: 200px; } -.deploy-keys { - .scrolling-tabs-container { - position: relative; - } -} - .deploy-key { // Ensure that the fingerprint does not overflow on small screens .fingerprint { @@ -503,6 +497,12 @@ &:not(:first-child) { border-top: 1px solid $border-color; } + + .btn-template-icon { + position: absolute; + left: $gl-padding; + top: $gl-padding; + } } .template-title { @@ -520,12 +520,6 @@ } } - svg { - position: absolute; - left: $gl-padding; - top: $gl-padding; - } - .project-fields-form { display: none; @@ -536,34 +530,23 @@ } .template-input-group { - position: relative; - - @include media-breakpoint-up(sm) { - display: flex; - } - - .input-group-prepend, - .input-group-append { + .input-group-prepend { flex: 1; - text-align: left; - padding-left: ($gl-padding * 3); - background-color: $white-light; } - .selected-template { - line-height: 20px; + .input-group-text { + width: 100%; + background-color: $white-light; } .selected-icon { + padding-right: $gl-padding; + svg { display: none; top: 7px; height: 20px; width: 20px; - - &.active { - display: block; - } } } } @@ -875,7 +858,6 @@ pre.light-well { .git-clone-holder { width: 380px; - height: 28px; .btn-clipboard { border: 1px solid $border-color; diff --git a/app/assets/stylesheets/pages/repo.scss b/app/assets/stylesheets/pages/repo.scss index 6bbcb15329c..0a56153203c 100644 --- a/app/assets/stylesheets/pages/repo.scss +++ b/app/assets/stylesheets/pages/repo.scss @@ -183,7 +183,7 @@ svg { position: relative; - top: -1px; + top: -2px; } .ide-file-changed-icon { @@ -335,7 +335,6 @@ img { max-width: 90%; - max-height: 90%; } .isZoomable { @@ -458,9 +457,9 @@ width: auto; margin-right: 0; - a:hover, - a:focus { - text-decoration: none; + > a, + > button { + height: 60px; } } @@ -541,32 +540,12 @@ margin-right: -$grid-size; min-height: 60px; - .multi-file-commit-list-item { - margin-left: 0; - margin-right: 0; - } - &.form-text.text-muted { margin-left: 0; right: 0; } } -.multi-file-commit-list-item { - .multi-file-discard-btn { - display: none; - margin-top: -2px; - margin-left: auto; - color: $gl-link-color; - } - - &:hover { - .multi-file-discard-btn { - display: flex; - } - } -} - .multi-file-addition, .multi-file-addition-solid { color: $green-500; @@ -596,7 +575,7 @@ } } -.multi-file-commit-list-item, +.multi-file-commit-list-path, .ide-file-list .file { display: flex; align-items: center; @@ -613,11 +592,9 @@ } .multi-file-commit-list-path { - padding: 0; - background: none; - border: 0; - text-align: left; - width: 100%; + &.is-active { + background-color: $white-normal; + } &:hover, &:focus { @@ -632,7 +609,7 @@ } .multi-file-commit-list-file-path { - @include str-truncated(100%); + @include str-truncated(calc(100% - 30px)); &:hover { text-decoration: underline; @@ -643,6 +620,16 @@ } } +.multi-file-discard-btn { + top: 4px; + right: 8px; + bottom: 4px; + + svg { + top: 0; + } +} + .multi-file-commit-form { position: relative; background-color: $white-light; @@ -718,9 +705,17 @@ } .ide-new-btn { + .btn { + padding-top: 3px; + padding-bottom: 3px; + } + + .dropdown { + display: flex; + } + .dropdown-toggle svg { - margin-top: -2px; - margin-bottom: 2px; + top: 0; } .dropdown-menu { @@ -829,18 +824,20 @@ } .ide-staged-action-btn { - margin-left: auto; - line-height: 22px; + width: 22px; + margin-left: -1px; + border-top-left-radius: 0; + border-bottom-left-radius: 0; + + > svg { + top: 0; + } } .ide-commit-file-count { min-width: 22px; - margin-left: auto; background-color: $gray-light; - border-radius: $border-radius-default; border: 1px solid $white-dark; - line-height: 20px; - text-align: center; } .ide-commit-radios { @@ -877,6 +874,7 @@ border-top: 1px solid transparent; border-bottom: 1px solid transparent; outline: 0; + cursor: pointer; svg { margin: 0 auto; @@ -1120,7 +1118,12 @@ .ide-context-header { .avatar { - flex: 0 0 40px; + flex: 0 0 38px; + } + + .ide-merge-requests-dropdown.dropdown-menu { + width: 385px; + max-height: initial; } } @@ -1130,11 +1133,20 @@ .sidebar-context-title { white-space: nowrap; } + + .ide-sidebar-branch-title { + min-width: 50px; + } } .ide-external-link { + position: relative; + svg { display: none; + position: absolute; + top: 2px; + right: -$gl-padding; } &:hover, @@ -1165,6 +1177,8 @@ display: flex; flex-direction: column; height: 100%; + margin-top: -$grid-size; + margin-bottom: -$grid-size; .empty-state { margin-top: auto; @@ -1181,6 +1195,17 @@ margin: 0; } } + + .build-trace, + .top-bar { + margin-left: -$gl-padding; + } + + &.build-page .top-bar { + top: 0; + font-size: 12px; + border-top-right-radius: $border-radius-default; + } } .ide-pipeline-list { @@ -1189,7 +1214,7 @@ } .ide-pipeline-header { - min-height: 50px; + min-height: 55px; padding-left: $gl-padding; padding-right: $gl-padding; @@ -1209,8 +1234,7 @@ .ci-status-icon { display: flex; justify-content: center; - height: 20px; - margin-top: -2px; + min-width: 24px; overflow: hidden; } } @@ -1240,3 +1264,56 @@ overflow: hidden; text-overflow: ellipsis; } + +.ide-job-header { + min-height: 60px; +} + +.ide-merge-requests-dropdown { + .nav-links li { + width: 50%; + padding-left: 0; + padding-right: 0; + + a { + text-align: center; + + &:not(.active) { + background-color: $gray-light; + } + } + } + + .dropdown-input { + padding-left: $gl-padding; + padding-right: $gl-padding; + + .fa { + right: 26px; + } + } + + .btn-link { + padding-top: $gl-padding; + padding-bottom: $gl-padding; + } +} + +.ide-merge-request-current-icon { + min-width: 18px; +} + +.ide-merge-requests-empty { + height: 230px; +} + +.ide-merge-requests-dropdown-content { + min-height: 230px; + max-height: 470px; +} + +.ide-merge-request-project-path { + font-size: 12px; + line-height: 16px; + color: $gl-text-color-secondary; +} diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss index a35c4ff7c80..765c926751a 100644 --- a/app/assets/stylesheets/pages/search.scss +++ b/app/assets/stylesheets/pages/search.scss @@ -18,7 +18,8 @@ .file-finder-input:hover, .issuable-search-form:hover, .search-text-input:hover, -.form-control:hover { +.form-control:hover, +:not[readonly] { border-color: lighten($dropdown-input-focus-border, 20%); box-shadow: 0 0 4px lighten($search-input-focus-shadow-color, 20%); } @@ -28,7 +29,7 @@ input[type="checkbox"]:hover { } .search { - margin: 4px 8px 0; + margin: 0 8px; form { @extend .form-control; diff --git a/app/assets/stylesheets/pages/settings.scss b/app/assets/stylesheets/pages/settings.scss index 2b3773eebad..33a974e0176 100644 --- a/app/assets/stylesheets/pages/settings.scss +++ b/app/assets/stylesheets/pages/settings.scss @@ -52,7 +52,7 @@ .settings-content { max-height: 1px; - overflow-y: scroll; + overflow-y: hidden; padding-right: 110px; animation: collapseMaxHeight 300ms ease-out; // Keep the section from expanding when we scroll over it @@ -102,10 +102,6 @@ .form-text.text-muted { margin-top: 0; } - - .label-light { - margin-bottom: 0; - } } .settings-list-icon { @@ -131,12 +127,16 @@ color: $gl-danger; } -.service-settings .form-control-label { - padding-top: 0; +.service-settings { + input[type="radio"], + input[type="checkbox"] { + margin-top: 10px; + } } .integration-settings-form { - .card.card-body { + .card.card-body, + .info-well { padding: $gl-padding / 2; box-shadow: none; } @@ -174,7 +174,7 @@ .option-description, .option-disabled-reason { - margin-left: 45px; + margin-left: 30px; color: $project-option-descr-color; } @@ -296,7 +296,8 @@ } .btn-clipboard { - margin-left: 5px; + background-color: $white-light; + border: 1px solid $theme-gray-200; } .deploy-token-help-block { diff --git a/app/assets/stylesheets/pages/settings_ci_cd.scss b/app/assets/stylesheets/pages/settings_ci_cd.scss index a355e2dee24..777fdb3581e 100644 --- a/app/assets/stylesheets/pages/settings_ci_cd.scss +++ b/app/assets/stylesheets/pages/settings_ci_cd.scss @@ -16,3 +16,12 @@ .registry-placeholder { min-height: 60px; } + +.auto-devops-card { + margin-bottom: $gl-vert-padding; + + > .card-body { + border-radius: $card-border-radius; + padding: $gl-padding $gl-padding-24; + } +} diff --git a/app/assets/stylesheets/performance_bar.scss b/app/assets/stylesheets/performance_bar.scss index 06ef58531d7..5127ddfde6e 100644 --- a/app/assets/stylesheets/performance_bar.scss +++ b/app/assets/stylesheets/performance_bar.scss @@ -15,6 +15,7 @@ color: $perf-bar-text; select { + color: $perf-bar-text; width: 200px; } @@ -106,12 +107,12 @@ } .performance-bar-modal { - .modal-footer { - display: none; + .modal-body { + padding: 0; } - .modal-dialog { - width: 860px; + .modal-footer { + display: none; } } } diff --git a/app/assets/stylesheets/print.scss b/app/assets/stylesheets/print.scss index 90ccd4abd90..bb10928a037 100644 --- a/app/assets/stylesheets/print.scss +++ b/app/assets/stylesheets/print.scss @@ -22,9 +22,9 @@ header, nav, -nav.main-nav, nav.navbar-collapse, nav.navbar-collapse.collapse, +.nav-sidebar, .profiler-results, .tree-ref-holder, .tree-holder .breadcrumb, @@ -38,7 +38,8 @@ ul.notes-form, .edit-link, .note-action-button, .right-sidebar, -.flash-container { +.flash-container, +#js-peek { display: none !important; } diff --git a/app/controllers/admin/appearances_controller.rb b/app/controllers/admin/appearances_controller.rb index ea302f17d16..9aaec905734 100644 --- a/app/controllers/admin/appearances_controller.rb +++ b/app/controllers/admin/appearances_controller.rb @@ -41,6 +41,13 @@ class Admin::AppearancesController < Admin::ApplicationController redirect_to admin_appearances_path, notice: 'Header logo was succesfully removed.' end + def favicon + @appearance.remove_favicon! + @appearance.save + + redirect_to admin_appearances_path, notice: 'Favicon was succesfully removed.' + end + private # Use callbacks to share common setup or constraints between actions. @@ -61,6 +68,8 @@ class Admin::AppearancesController < Admin::ApplicationController logo_cache header_logo header_logo_cache + favicon + favicon_cache new_project_guidelines updated_by ] diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index db8a8cdc0d2..56312f801fb 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -91,6 +91,10 @@ class ApplicationController < ActionController::Base payload[:user_id] = logged_user.try(:id) payload[:username] = logged_user.try(:username) end + + if response.status == 422 && response.body.present? && response.content_type == 'application/json'.freeze + payload[:response] = response.body + end end # Controllers such as GitHttpController may use alternative methods @@ -130,12 +134,17 @@ class ApplicationController < ActionController::Base end def access_denied!(message = nil) + # If we display a custom access denied message to the user, we don't want to + # hide existence of the resource, rather tell them they cannot access it using + # the provided message + status = message.present? ? :forbidden : :not_found + respond_to do |format| - format.any { head :not_found } + format.any { head status } format.html do render "errors/access_denied", layout: "errors", - status: 404, + status: status, locals: { message: message } end end @@ -275,8 +284,10 @@ class ApplicationController < ActionController::Base return unless current_user return if current_user.terms_accepted? + message = _("Please accept the Terms of Service before continuing.") + if sessionless_user? - render_403 + access_denied!(message) else # Redirect to the destination if the request is a get. # Redirect to the source if it was a post, so the user can re-submit after @@ -287,7 +298,7 @@ class ApplicationController < ActionController::Base URI(request.referer).path if request.referer end - flash[:notice] = _("Please accept the Terms of Service before continuing.") + flash[:notice] = message redirect_to terms_path(redirect: redirect_path), status: :found end end diff --git a/app/controllers/boards/lists_controller.rb b/app/controllers/boards/lists_controller.rb index 381fd4d7508..e8b5934f2a9 100644 --- a/app/controllers/boards/lists_controller.rb +++ b/app/controllers/boards/lists_controller.rb @@ -56,8 +56,12 @@ module Boards private + def list_creation_attrs + %i[label_id] + end + def list_params - params.require(:list).permit(:label_id) + params.require(:list).permit(list_creation_attrs) end def move_params @@ -65,11 +69,15 @@ module Boards end def serialize_as_json(resource) - resource.as_json( + resource.as_json(serialization_attrs) + end + + def serialization_attrs + { only: [:id, :list_type, :position], methods: [:title], label: true - ) + } end end end diff --git a/app/controllers/concerns/internal_redirect.rb b/app/controllers/concerns/internal_redirect.rb index 7409b2e89a5..10b9852e329 100644 --- a/app/controllers/concerns/internal_redirect.rb +++ b/app/controllers/concerns/internal_redirect.rb @@ -23,6 +23,10 @@ module InternalRedirect nil end + def sanitize_redirect(url_or_path) + safe_redirect_path(url_or_path) || safe_redirect_path_for_url(url_or_path) + end + def host_allowed?(uri) uri.host == request.host && uri.port == request.port diff --git a/app/controllers/concerns/issuable_actions.rb b/app/controllers/concerns/issuable_actions.rb index c925b4aada5..d04eb192129 100644 --- a/app/controllers/concerns/issuable_actions.rb +++ b/app/controllers/concerns/issuable_actions.rb @@ -7,6 +7,19 @@ module IssuableActions before_action :authorize_admin_issuable!, only: :bulk_update end + def permitted_keys + [ + :issuable_ids, + :assignee_id, + :milestone_id, + :state_event, + :subscription_event, + label_ids: [], + add_label_ids: [], + remove_label_ids: [] + ] + end + def show respond_to do |format| format.html @@ -140,24 +153,15 @@ module IssuableActions end def bulk_update_params - permitted_keys = [ - :issuable_ids, - :assignee_id, - :milestone_id, - :state_event, - :subscription_event, - label_ids: [], - add_label_ids: [], - remove_label_ids: [] - ] + permitted_keys_array = permitted_keys.dup if resource_name == 'issue' - permitted_keys << { assignee_ids: [] } + permitted_keys_array << { assignee_ids: [] } else - permitted_keys.unshift(:assignee_id) + permitted_keys_array.unshift(:assignee_id) end - params.require(:update).permit(permitted_keys) + params.require(:update).permit(permitted_keys_array) end def resource_name diff --git a/app/controllers/concerns/issuable_collections.rb b/app/controllers/concerns/issuable_collections.rb index ca1b80a36a0..2ef2ee76855 100644 --- a/app/controllers/concerns/issuable_collections.rb +++ b/app/controllers/concerns/issuable_collections.rb @@ -95,12 +95,7 @@ module IssuableCollections elsif @group @filter_params[:group_id] = @group.id @filter_params[:include_subgroups] = true - else - # TODO: this filter ignore issues/mr created in public or - # internal repos where you are not a member. Enable this filter - # or improve current implementation to filter only issues you - # created or assigned or mentioned - # @filter_params[:authorized_only] = true + @filter_params[:use_cte_for_search] = true end @filter_params.permit(finder_type.valid_params) diff --git a/app/controllers/concerns/issues_action.rb b/app/controllers/concerns/issues_action.rb index b6eb7d292fc..9d58656773d 100644 --- a/app/controllers/concerns/issues_action.rb +++ b/app/controllers/concerns/issues_action.rb @@ -1,6 +1,7 @@ module IssuesAction extend ActiveSupport::Concern include IssuableCollections + include IssuesCalendar # rubocop:disable Gitlab/ModuleWithInstanceVariables def issues @@ -17,18 +18,9 @@ module IssuesAction end # rubocop:enable Gitlab/ModuleWithInstanceVariables - # rubocop:disable Gitlab/ModuleWithInstanceVariables def issues_calendar - @issues = issuables_collection - .non_archived - .with_due_date - .limit(100) - - respond_to do |format| - format.ics { response.headers['Content-Disposition'] = 'inline' } - end + render_issues_calendar(issuables_collection) end - # rubocop:enable Gitlab/ModuleWithInstanceVariables private diff --git a/app/controllers/concerns/issues_calendar.rb b/app/controllers/concerns/issues_calendar.rb new file mode 100644 index 00000000000..671a204621d --- /dev/null +++ b/app/controllers/concerns/issues_calendar.rb @@ -0,0 +1,24 @@ +module IssuesCalendar + extend ActiveSupport::Concern + + # rubocop:disable Gitlab/ModuleWithInstanceVariables + def render_issues_calendar(issuables) + @issues = issuables + .non_archived + .with_due_date + .limit(100) + + respond_to do |format| + format.ics do + # NOTE: with text/calendar as Content-Type, the browser always downloads + # the content as a file (even ignoring the Content-Disposition + # header). We want to display the content inline when accessed + # from GitLab, similarly to the RSS feed. + if request.referer&.start_with?(::Settings.gitlab.base_url) + response.headers['Content-Type'] = 'text/plain' + end + end + end + end + # rubocop:enable Gitlab/ModuleWithInstanceVariables +end diff --git a/app/controllers/concerns/uploads_actions.rb b/app/controllers/concerns/uploads_actions.rb index b9b9b6e4e88..170bca8b56f 100644 --- a/app/controllers/concerns/uploads_actions.rb +++ b/app/controllers/concerns/uploads_actions.rb @@ -2,7 +2,7 @@ module UploadsActions include Gitlab::Utils::StrongMemoize include SendFileUpload - UPLOAD_MOUNTS = %w(avatar attachment file logo header_logo).freeze + UPLOAD_MOUNTS = %w(avatar attachment file logo header_logo favicon).freeze def create link_to_file = UploadService.new(model, params[:file], uploader_class).execute @@ -31,6 +31,11 @@ module UploadsActions disposition = uploader.image_or_video? ? 'inline' : 'attachment' + uploaders = [uploader, *uploader.versions.values] + uploader = uploaders.find { |version| version.filename == params[:filename] } + + return render_404 unless uploader + send_upload(uploader, attachment: uploader.filename, disposition: disposition) end diff --git a/app/controllers/graphql_controller.rb b/app/controllers/graphql_controller.rb new file mode 100644 index 00000000000..0a1cf169aca --- /dev/null +++ b/app/controllers/graphql_controller.rb @@ -0,0 +1,45 @@ +class GraphqlController < ApplicationController + # Unauthenticated users have access to the API for public data + skip_before_action :authenticate_user! + + before_action :check_graphql_feature_flag! + + def execute + variables = Gitlab::Graphql::Variables.new(params[:variables]).to_h + query = params[:query] + operation_name = params[:operationName] + context = { + current_user: current_user + } + result = GitlabSchema.execute(query, variables: variables, context: context, operation_name: operation_name) + render json: result + end + + rescue_from StandardError do |exception| + log_exception(exception) + + render_error("Internal server error") + end + + rescue_from Gitlab::Graphql::Variables::Invalid do |exception| + render_error(exception.message, status: :unprocessable_entity) + end + + private + + # Overridden from the ApplicationController to make the response look like + # a GraphQL response. That is nicely picked up in Graphiql. + def render_404 + render_error("Not found!", status: :not_found) + end + + def render_error(message, status: 500) + error = { errors: [message: message] } + + render json: error, status: status + end + + def check_graphql_feature_flag! + render_404 unless Feature.enabled?(:graphql) + end +end diff --git a/app/controllers/groups/group_members_controller.rb b/app/controllers/groups/group_members_controller.rb index ef3eba80154..ef5d5e5c742 100644 --- a/app/controllers/groups/group_members_controller.rb +++ b/app/controllers/groups/group_members_controller.rb @@ -3,8 +3,12 @@ class Groups::GroupMembersController < Groups::ApplicationController include MembersPresentation include SortingHelper + def self.admin_not_required_endpoints + %i[index leave request_access] + end + # Authorize - before_action :authorize_admin_group_member!, except: [:index, :leave, :request_access] + before_action :authorize_admin_group_member!, except: admin_not_required_endpoints skip_cross_project_access_check :index, :create, :update, :destroy, :request_access, :approve_access_request, :leave, :resend_invite, diff --git a/app/controllers/groups/labels_controller.rb b/app/controllers/groups/labels_controller.rb index 58be330f466..863f50e8e66 100644 --- a/app/controllers/groups/labels_controller.rb +++ b/app/controllers/groups/labels_controller.rb @@ -2,6 +2,7 @@ class Groups::LabelsController < Groups::ApplicationController include ToggleSubscriptionAction before_action :label, only: [:edit, :update, :destroy] + before_action :available_labels, only: [:index] before_action :authorize_admin_labels!, only: [:new, :create, :edit, :update, :destroy] before_action :save_previous_label_path, only: [:edit] @@ -12,17 +13,8 @@ class Groups::LabelsController < Groups::ApplicationController format.html do @labels = @group.labels.page(params[:page]) end - format.json do - available_labels = LabelsFinder.new( - current_user, - group_id: @group.id, - only_group_labels: params[:only_group_labels], - include_ancestor_groups: params[:include_ancestor_groups], - include_descendant_groups: params[:include_descendant_groups] - ).execute - - render json: LabelSerializer.new.represent_appearance(available_labels) + render json: LabelSerializer.new.represent_appearance(@available_labels) end end end @@ -113,4 +105,15 @@ class Groups::LabelsController < Groups::ApplicationController def save_previous_label_path session[:previous_labels_path] = URI(request.referer || '').path end + + def available_labels + @available_labels ||= + LabelsFinder.new( + current_user, + group_id: @group.id, + only_group_labels: params[:only_group_labels], + include_ancestor_groups: params[:include_ancestor_groups], + include_descendant_groups: params[:include_descendant_groups] + ).execute + end end diff --git a/app/controllers/groups/milestones_controller.rb b/app/controllers/groups/milestones_controller.rb index 5903689dc62..9bd51de7e97 100644 --- a/app/controllers/groups/milestones_controller.rb +++ b/app/controllers/groups/milestones_controller.rb @@ -76,12 +76,15 @@ class Groups::MilestonesController < Groups::ApplicationController def milestones milestones = MilestonesFinder.new(search_params).execute - legacy_milestones = GroupMilestone.build_collection(group, group_projects, params) @sort = params[:sort] || 'due_date_asc' MilestoneArray.sort(milestones + legacy_milestones, @sort) end + def legacy_milestones + GroupMilestone.build_collection(group, group_projects, params) + end + def milestone @milestone = if params[:title] diff --git a/app/controllers/groups/shared_projects_controller.rb b/app/controllers/groups/shared_projects_controller.rb index f2f835767e0..7dec1f5f402 100644 --- a/app/controllers/groups/shared_projects_controller.rb +++ b/app/controllers/groups/shared_projects_controller.rb @@ -24,7 +24,9 @@ module Groups # Make the `search` param consistent for the frontend, # which will be using `filter`. params[:search] ||= params[:filter] if params[:filter] - params.permit(:sort, :search) + # Don't show archived projects + params[:non_archived] = true + params.permit(:sort, :search, :non_archived) end end end diff --git a/app/controllers/import/base_controller.rb b/app/controllers/import/base_controller.rb index 663269a0f92..5766c6924cd 100644 --- a/app/controllers/import/base_controller.rb +++ b/app/controllers/import/base_controller.rb @@ -25,4 +25,8 @@ class Import::BaseController < ApplicationController current_user.namespace end + + def project_save_error(project) + project.errors.full_messages.join(', ') + end end diff --git a/app/controllers/import/bitbucket_controller.rb b/app/controllers/import/bitbucket_controller.rb index 77af5fb9c4f..fa31933e778 100644 --- a/app/controllers/import/bitbucket_controller.rb +++ b/app/controllers/import/bitbucket_controller.rb @@ -55,7 +55,7 @@ class Import::BitbucketController < Import::BaseController if project.persisted? render json: ProjectSerializer.new.represent(project) else - render json: { errors: project.errors.full_messages }, status: :unprocessable_entity + render json: { errors: project_save_error(project) }, status: :unprocessable_entity end else render json: { errors: 'This namespace has already been taken! Please choose another one.' }, status: :unprocessable_entity diff --git a/app/controllers/import/fogbugz_controller.rb b/app/controllers/import/fogbugz_controller.rb index 25ec13b8075..2d665e05ac3 100644 --- a/app/controllers/import/fogbugz_controller.rb +++ b/app/controllers/import/fogbugz_controller.rb @@ -66,7 +66,7 @@ class Import::FogbugzController < Import::BaseController if project.persisted? render json: ProjectSerializer.new.represent(project) else - render json: { errors: project.errors.full_messages }, status: :unprocessable_entity + render json: { errors: project_save_error(project) }, status: :unprocessable_entity end end diff --git a/app/controllers/import/github_controller.rb b/app/controllers/import/github_controller.rb index f67ec4c248b..c9870332c0f 100644 --- a/app/controllers/import/github_controller.rb +++ b/app/controllers/import/github_controller.rb @@ -48,7 +48,7 @@ class Import::GithubController < Import::BaseController if project.persisted? render json: ProjectSerializer.new.represent(project) else - render json: { errors: project.errors.full_messages }, status: :unprocessable_entity + render json: { errors: project_save_error(project) }, status: :unprocessable_entity end else render json: { errors: 'This namespace has already been taken! Please choose another one.' }, status: :unprocessable_entity diff --git a/app/controllers/import/gitlab_controller.rb b/app/controllers/import/gitlab_controller.rb index 39e2e9e094b..fccbdbca0f6 100644 --- a/app/controllers/import/gitlab_controller.rb +++ b/app/controllers/import/gitlab_controller.rb @@ -32,7 +32,7 @@ class Import::GitlabController < Import::BaseController if project.persisted? render json: ProjectSerializer.new.represent(project) else - render json: { errors: project.errors.full_messages }, status: :unprocessable_entity + render json: { errors: project_save_error(project) }, status: :unprocessable_entity end else render json: { errors: 'This namespace has already been taken! Please choose another one.' }, status: :unprocessable_entity diff --git a/app/controllers/import/google_code_controller.rb b/app/controllers/import/google_code_controller.rb index 9b26a00f7c7..3bce27e810a 100644 --- a/app/controllers/import/google_code_controller.rb +++ b/app/controllers/import/google_code_controller.rb @@ -92,7 +92,7 @@ class Import::GoogleCodeController < Import::BaseController if project.persisted? render json: ProjectSerializer.new.represent(project) else - render json: { errors: project.errors.full_messages }, status: :unprocessable_entity + render json: { errors: project_save_error(project) }, status: :unprocessable_entity end end diff --git a/app/controllers/projects/artifacts_controller.rb b/app/controllers/projects/artifacts_controller.rb index abc283d7aa9..6484a713f8e 100644 --- a/app/controllers/projects/artifacts_controller.rb +++ b/app/controllers/projects/artifacts_controller.rb @@ -7,6 +7,7 @@ class Projects::ArtifactsController < Projects::ApplicationController before_action :authorize_read_build! before_action :authorize_update_build!, only: [:keep] before_action :extract_ref_name_and_path + before_action :set_request_format, only: [:file] before_action :validate_artifacts! before_action :entry, only: [:file] @@ -101,4 +102,12 @@ class Projects::ArtifactsController < Projects::ApplicationController render_404 unless @entry.exists? end + + def set_request_format + request.format = :html if set_request_format? + end + + def set_request_format? + request.format != :json + end end diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb index 0c1c286a0a4..a8c0a68fc17 100644 --- a/app/controllers/projects/blob_controller.rb +++ b/app/controllers/projects/blob_controller.rb @@ -7,6 +7,7 @@ class Projects::BlobController < Projects::ApplicationController prepend_before_action :authenticate_user!, only: [:edit] + before_action :set_request_format, only: [:edit, :show, :update] before_action :require_non_empty_project, except: [:new, :create] before_action :authorize_download_code! @@ -188,6 +189,18 @@ class Projects::BlobController < Projects::ApplicationController .last_for_path(@repository, @ref, @path).sha end + # In Rails 4.2 if params[:format] is empty, Rails set it to :html + # But since Rails 5.0 the framework now looks for an extension. + # E.g. for `blob/master/CHANGELOG.md` in Rails 4 the format would be `:html`, but in Rails 5 on it'd be `:md` + # This before_action explicitly sets the `:html` format for all requests unless `:format` is set by a client e.g. by JS for XHR requests. + def set_request_format + request.format = :html if set_request_format? + end + + def set_request_format? + params[:id].present? && params[:format].blank? && request.format != "json" + end + def show_html environment_params = @repository.branch_exists?(@ref) ? { ref: @ref } : { commit: @commit } @environment = EnvironmentsFinder.new(@project, current_user, environment_params).execute.last @@ -197,15 +210,14 @@ class Projects::BlobController < Projects::ApplicationController end def show_json - json = blob_json(@blob) - return render_404 unless json - + set_last_commit_sha path_segments = @path.split('/') path_segments.pop tree_path = path_segments.join('/') - render json: json.merge( + json = { id: @blob.id, + last_commit_sha: @last_commit_sha, path: blob.path, name: blob.name, extension: blob.extension, @@ -221,6 +233,10 @@ class Projects::BlobController < Projects::ApplicationController commits_path: project_commits_path(project, @id), tree_path: project_tree_path(project, File.join(@ref, tree_path)), permalink: project_blob_path(project, File.join(@commit.id, @path)) - ) + } + + json.merge!(blob_json(@blob) || {}) unless params[:viewer] == 'none' + + render json: json end end diff --git a/app/controllers/projects/clusters/applications_controller.rb b/app/controllers/projects/clusters/applications_controller.rb index 4d758402850..a5c82caa897 100644 --- a/app/controllers/projects/clusters/applications_controller.rb +++ b/app/controllers/projects/clusters/applications_controller.rb @@ -42,6 +42,6 @@ class Projects::Clusters::ApplicationsController < Projects::ApplicationControll owner: current_user } - Applications::CreateService.new(current_user, oauth_application_params).execute + Applications::CreateService.new(current_user, oauth_application_params).execute(request) end end diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 35c36c725e2..7c897b2d86c 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -4,6 +4,7 @@ class Projects::IssuesController < Projects::ApplicationController include IssuableActions include ToggleAwardEmoji include IssuableCollections + include IssuesCalendar include SpammableActions prepend_before_action :authenticate_user!, only: [:new] @@ -40,14 +41,7 @@ class Projects::IssuesController < Projects::ApplicationController end def calendar - @issues = @issuables - .non_archived - .with_due_date - .limit(100) - - respond_to do |format| - format.ics { response.headers['Content-Disposition'] = 'inline' } - end + render_issues_calendar(@issuables) end def new diff --git a/app/controllers/projects/lfs_storage_controller.rb b/app/controllers/projects/lfs_storage_controller.rb index 43d8867a536..45c98d60822 100644 --- a/app/controllers/projects/lfs_storage_controller.rb +++ b/app/controllers/projects/lfs_storage_controller.rb @@ -18,7 +18,7 @@ class Projects::LfsStorageController < Projects::GitHttpClientController def upload_authorize set_workhorse_internal_api_content_type - authorized = LfsObjectUploader.workhorse_authorize + authorized = LfsObjectUploader.workhorse_authorize(has_length: true) authorized.merge!(LfsOid: oid, LfsSize: size) render json: authorized diff --git a/app/controllers/projects/merge_requests/application_controller.rb b/app/controllers/projects/merge_requests/application_controller.rb index 29632bef7e5..8e4aeec16dc 100644 --- a/app/controllers/projects/merge_requests/application_controller.rb +++ b/app/controllers/projects/merge_requests/application_controller.rb @@ -15,7 +15,7 @@ class Projects::MergeRequests::ApplicationController < Projects::ApplicationCont def merge_request_params_attributes [ - :allow_maintainer_to_push, + :allow_collaboration, :assignee_id, :description, :force_remove_source_branch, diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index ecea6e1b2bf..38918b3cd52 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -28,15 +28,14 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo end def show - validates_merge_request - close_merge_request_without_source_project - check_if_can_be_merged - - # Return if the response has already been rendered - return if response_body + close_merge_request_if_no_source_project + mark_merge_request_mergeable respond_to do |format| format.html do + # use next to appease Rubocop + next render('invalid') if target_branch_missing? + # Build a note object for comment form @note = @project.notes.new(noteable: @merge_request) @@ -116,7 +115,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo end format.json do - render json: @merge_request.to_json(include: { milestone: {}, assignee: { only: [:name, :username], methods: [:avatar_url] }, labels: { methods: :text_color } }, methods: [:task_status, :task_status_short]) + render json: serializer.represent(@merge_request, serializer: 'basic') end end rescue ActiveRecord::StaleObjectError @@ -234,20 +233,6 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo alias_method :issuable, :merge_request alias_method :awardable, :merge_request - def validates_merge_request - # Show git not found page - # if there is no saved commits between source & target branch - if @merge_request.has_no_commits? - # and if target branch doesn't exist - return invalid_mr unless @merge_request.target_branch_exists? - end - end - - def invalid_mr - # Render special view for MR with removed target branch - render 'invalid' - end - def merge_params params.permit(merge_params_attributes) end @@ -261,7 +246,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo @merge_request.head_pipeline && @merge_request.head_pipeline.active? end - def close_merge_request_without_source_project + def close_merge_request_if_no_source_project if !@merge_request.source_project && @merge_request.open? @merge_request.close end @@ -269,7 +254,11 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo private - def check_if_can_be_merged + def target_branch_missing? + @merge_request.has_no_commits? && !@merge_request.target_branch_exists? + end + + def mark_merge_request_mergeable @merge_request.check_if_can_be_merged end diff --git a/app/controllers/projects/milestones_controller.rb b/app/controllers/projects/milestones_controller.rb index c5a044541f1..f85dcfe6bfc 100644 --- a/app/controllers/projects/milestones_controller.rb +++ b/app/controllers/projects/milestones_controller.rb @@ -1,4 +1,5 @@ class Projects::MilestonesController < Projects::ApplicationController + include Gitlab::Utils::StrongMemoize include MilestoneActions before_action :check_issuables_available! @@ -103,7 +104,7 @@ class Projects::MilestonesController < Projects::ApplicationController protected def milestones - @milestones ||= begin + strong_memoize(:milestones) do MilestonesFinder.new(search_params).execute end end @@ -121,10 +122,10 @@ class Projects::MilestonesController < Projects::ApplicationController end def search_params - if @project.group && can?(current_user, :read_group, @project.group) - group = @project.group + if request.format.json? && @project.group && can?(current_user, :read_group, @project.group) + groups = @project.group.self_and_ancestors_ids end - params.permit(:state).merge(project_ids: @project.id, group_ids: group&.id) + params.permit(:state).merge(project_ids: @project.id, group_ids: groups) end end diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb index 6b40fc2fe68..768595ceeb4 100644 --- a/app/controllers/projects/pipelines_controller.rb +++ b/app/controllers/projects/pipelines_controller.rb @@ -23,8 +23,6 @@ class Projects::PipelinesController < Projects::ApplicationController @finished_count = limited_pipelines_count(project, 'finished') @pipelines_count = limited_pipelines_count(project) - Gitlab::Ci::Pipeline::Preloader.preload(@pipelines) - respond_to do |format| format.html format.json do @@ -34,7 +32,7 @@ class Projects::PipelinesController < Projects::ApplicationController pipelines: PipelineSerializer .new(project: @project, current_user: @current_user) .with_pagination(request, response) - .represent(@pipelines, disable_coverage: true), + .represent(@pipelines, disable_coverage: true, preload: true), count: { all: @pipelines_count, running: @running_count, diff --git a/app/controllers/projects/settings/ci_cd_controller.rb b/app/controllers/projects/settings/ci_cd_controller.rb index 1d850baf012..fb3f6eec2bd 100644 --- a/app/controllers/projects/settings/ci_cd_controller.rb +++ b/app/controllers/projects/settings/ci_cd_controller.rb @@ -41,7 +41,7 @@ module Projects :runners_token, :builds_enabled, :build_allow_git_fetch, :build_timeout_human_readable, :build_coverage_regex, :public_builds, :auto_cancel_pending_pipelines, :ci_config_path, - auto_devops_attributes: [:id, :domain, :enabled] + auto_devops_attributes: [:id, :domain, :enabled, :deploy_strategy] ) end diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb index f5a222b3a48..e6d6965036e 100644 --- a/app/controllers/registrations_controller.rb +++ b/app/controllers/registrations_controller.rb @@ -3,6 +3,9 @@ class RegistrationsController < Devise::RegistrationsController include AcceptsPendingInvitations before_action :whitelist_query_limiting, only: [:destroy] + before_action :ensure_terms_accepted, + if: -> { Gitlab::CurrentSettings.current_application_settings.enforce_terms? }, + only: [:create] def new redirect_to(new_user_session_path) @@ -18,7 +21,9 @@ class RegistrationsController < Devise::RegistrationsController if !Gitlab::Recaptcha.load_configurations! || verify_recaptcha accept_pending_invitations - super + super do |new_user| + persist_accepted_terms_if_required(new_user) + end else flash[:alert] = 'There was an error with the reCAPTCHA. Please solve the reCAPTCHA again.' flash.delete :recaptcha_error @@ -40,6 +45,16 @@ class RegistrationsController < Devise::RegistrationsController protected + def persist_accepted_terms_if_required(new_user) + return unless new_user.persisted? + return unless Gitlab::CurrentSettings.current_application_settings.enforce_terms? + + if terms_accepted? + terms = ApplicationSetting::Term.latest + Users::RespondToTermsService.new(new_user, terms).execute(accepted: true) + end + end + def destroy_confirmation_valid? if current_user.confirm_deletion_with_password? current_user.valid_password?(params[:password]) @@ -91,4 +106,14 @@ class RegistrationsController < Devise::RegistrationsController def whitelist_query_limiting Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42380') end + + def ensure_terms_accepted + return if terms_accepted? + + redirect_to new_user_session_path, alert: _('You must accept our Terms of Service and privacy policy in order to register an account') + end + + def terms_accepted? + Gitlab::Utils.to_boolean(params[:terms_opt_in]) + end end diff --git a/app/controllers/users/terms_controller.rb b/app/controllers/users/terms_controller.rb index ab685b9106e..1b1560a2a00 100644 --- a/app/controllers/users/terms_controller.rb +++ b/app/controllers/users/terms_controller.rb @@ -2,6 +2,7 @@ module Users class TermsController < ApplicationController include InternalRedirect + skip_before_action :authenticate_user! skip_before_action :enforce_terms! skip_before_action :check_password_expiration skip_before_action :check_two_factor_requirement @@ -13,6 +14,10 @@ module Users def index @redirect = redirect_path + + if current_user && @term.accepted_by_user?(current_user) + flash.now[:notice] = "You have already accepted the Terms of Service as #{current_user.to_reference}" + end end def accept diff --git a/app/finders/group_members_finder.rb b/app/finders/group_members_finder.rb index 067aff408df..2a656c0d31c 100644 --- a/app/finders/group_members_finder.rb +++ b/app/finders/group_members_finder.rb @@ -3,17 +3,29 @@ class GroupMembersFinder @group = group end - def execute + def execute(include_descendants: false) group_members = @group.members + wheres = [] - return group_members unless @group.parent + return group_members unless @group.parent || include_descendants - parents_members = GroupMember.non_request - .where(source_id: @group.ancestors.select(:id)) - .where.not(user_id: @group.users.select(:id)) + wheres << "members.id IN (#{group_members.select(:id).to_sql})" - wheres = ["members.id IN (#{group_members.select(:id).to_sql})"] - wheres << "members.id IN (#{parents_members.select(:id).to_sql})" + if @group.parent + parents_members = GroupMember.non_request + .where(source_id: @group.ancestors.select(:id)) + .where.not(user_id: @group.users.select(:id)) + + wheres << "members.id IN (#{parents_members.select(:id).to_sql})" + end + + if include_descendants + descendant_members = GroupMember.non_request + .where(source_id: @group.descendants.select(:id)) + .where.not(user_id: @group.users.select(:id)) + + wheres << "members.id IN (#{descendant_members.select(:id).to_sql})" + end GroupMember.where(wheres.join(' OR ')) end diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index c6ef79ce15e..5d5f72c4d86 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -23,6 +23,7 @@ # created_before: datetime # updated_after: datetime # updated_before: datetime +# use_cte_for_search: boolean # class IssuableFinder prepend FinderWithCrossProjectAccess @@ -54,6 +55,7 @@ class IssuableFinder sort state include_subgroups + use_cte_for_search ] end @@ -74,19 +76,21 @@ class IssuableFinder items = init_collection items = filter_items(items) - # Filtering by project HAS TO be the last because we use the project IDs yielded by the issuable query thus far - items = by_project(items) + # This has to be last as we may use a CTE as an optimization fence by + # passing the use_cte_for_search param + # https://www.postgresql.org/docs/current/static/queries-with.html + items = by_search(items) sort(items) end def filter_items(items) + items = by_project(items) items = by_scope(items) items = by_created_at(items) items = by_updated_at(items) items = by_state(items) items = by_group(items) - items = by_search(items) items = by_assignee(items) items = by_author(items) items = by_non_archived(items) @@ -107,7 +111,6 @@ class IssuableFinder # def count_by_state count_params = params.merge(state: nil, sort: nil) - labels_count = label_names.any? ? label_names.count : 1 finder = self.class.new(current_user, count_params) counts = Hash.new(0) @@ -116,6 +119,11 @@ class IssuableFinder # per issuable, so we have to count those in Ruby - which is bad, but still # better than performing multiple queries. # + # This does not apply when we are using a CTE for the search, as the labels + # GROUP BY is inside the subquery in that case, so we set labels_count to 1. + labels_count = label_names.any? ? label_names.count : 1 + labels_count = 1 if use_cte_for_search? + finder.execute.reorder(nil).group(:state).count.each do |key, value| counts[Array(key).last.to_sym] += value / labels_count end @@ -159,10 +167,7 @@ class IssuableFinder finder_options = { include_subgroups: params[:include_subgroups], only_owned: true } GroupProjectsFinder.new(group: group, current_user: current_user, options: finder_options).execute else - opts = { current_user: current_user } - opts[:project_ids_relation] = item_project_ids(items) if items - - ProjectsFinder.new(opts).execute + ProjectsFinder.new(current_user: current_user).execute end @projects = projects.with_feature_available_for_user(klass, current_user).reorder(nil) @@ -329,8 +334,24 @@ class IssuableFinder items end + def use_cte_for_search? + return false unless search + return false unless Gitlab::Database.postgresql? + + params[:use_cte_for_search] + end + def by_search(items) - search ? items.full_search(search) : items + return items unless search + + if use_cte_for_search? + cte = Gitlab::SQL::RecursiveCTE.new(klass.table_name) + cte << items + + items = klass.with(cte.to_arel).from(klass.table_name) + end + + items.full_search(search) end def by_iids(items) diff --git a/app/finders/issues_finder.rb b/app/finders/issues_finder.rb index 3626670d141..24a6b9349a0 100644 --- a/app/finders/issues_finder.rb +++ b/app/finders/issues_finder.rb @@ -136,8 +136,4 @@ class IssuesFinder < IssuableFinder items end end - - def item_project_ids(items) - items&.reorder(nil)&.select(:project_id) - end end diff --git a/app/finders/members_finder.rb b/app/finders/members_finder.rb index 4734d97b8c7..4c893ae2de6 100644 --- a/app/finders/members_finder.rb +++ b/app/finders/members_finder.rb @@ -7,12 +7,12 @@ class MembersFinder @group = project.group end - def execute + def execute(include_descendants: false) project_members = project.project_members project_members = project_members.non_invite unless can?(current_user, :admin_project, project) if group - group_members = GroupMembersFinder.new(group).execute + group_members = GroupMembersFinder.new(group).execute(include_descendants: include_descendants) group_members = group_members.non_invite union = Gitlab::SQL::Union.new([project_members, group_members], remove_duplicates: false) diff --git a/app/finders/merge_requests_finder.rb b/app/finders/merge_requests_finder.rb index e2240e5e0d8..8d84ed4bdfb 100644 --- a/app/finders/merge_requests_finder.rb +++ b/app/finders/merge_requests_finder.rb @@ -56,8 +56,4 @@ class MergeRequestsFinder < IssuableFinder items.where(target_branch: target_branch) end - - def item_project_ids(items) - items&.reorder(nil)&.select(:target_project_id) - end end diff --git a/app/finders/snippets_finder.rb b/app/finders/snippets_finder.rb index d498a2d6d11..9d3772d7541 100644 --- a/app/finders/snippets_finder.rb +++ b/app/finders/snippets_finder.rb @@ -54,7 +54,10 @@ class SnippetsFinder < UnionFinder end def authorized_snippets - Snippet.where(feature_available_projects.or(not_project_related)) + # This query was intentionally converted to a raw one to get it work in Rails 5.0. + # In Rails 5.0 and 5.1 there's a bug: https://github.com/rails/arel/issues/531 + # Please convert it back when on rails 5.2 as it works again as expected since 5.2. + Snippet.where("#{feature_available_projects} OR #{not_project_related}") .public_or_visible_to_user(current_user) end @@ -86,18 +89,20 @@ class SnippetsFinder < UnionFinder def feature_available_projects # Don't return any project related snippets if the user cannot read cross project - return table[:id].eq(nil) unless Ability.allowed?(current_user, :read_cross_project) + return table[:id].eq(nil).to_sql unless Ability.allowed?(current_user, :read_cross_project) projects = projects_for_user do |part| part.with_feature_available_for_user(:snippets, current_user) end.select(:id) - arel_query = Arel::Nodes::SqlLiteral.new(projects.to_sql) - table[:project_id].in(arel_query) + # This query was intentionally converted to a raw one to get it work in Rails 5.0. + # In Rails 5.0 and 5.1 there's a bug: https://github.com/rails/arel/issues/531 + # Please convert it back when on rails 5.2 as it works again as expected since 5.2. + "snippets.project_id IN (#{projects.to_sql})" end def not_project_related - table[:project_id].eq(nil) + table[:project_id].eq(nil).to_sql end def table diff --git a/app/graphql/functions/base_function.rb b/app/graphql/functions/base_function.rb new file mode 100644 index 00000000000..42fb8f99acc --- /dev/null +++ b/app/graphql/functions/base_function.rb @@ -0,0 +1,4 @@ +module Functions + class BaseFunction < GraphQL::Function + end +end diff --git a/app/graphql/functions/echo.rb b/app/graphql/functions/echo.rb new file mode 100644 index 00000000000..e5bf109b8d7 --- /dev/null +++ b/app/graphql/functions/echo.rb @@ -0,0 +1,13 @@ +module Functions + class Echo < BaseFunction + argument :text, GraphQL::STRING_TYPE + + description "Testing endpoint to validate the API with" + + def call(obj, args, ctx) + username = ctx[:current_user]&.username + + "#{username.inspect} says: #{args[:text]}" + end + end +end diff --git a/app/graphql/gitlab_schema.rb b/app/graphql/gitlab_schema.rb new file mode 100644 index 00000000000..de4fc1d8e32 --- /dev/null +++ b/app/graphql/gitlab_schema.rb @@ -0,0 +1,8 @@ +class GitlabSchema < GraphQL::Schema + use BatchLoader::GraphQL + use Gitlab::Graphql::Authorize + use Gitlab::Graphql::Present + + query(Types::QueryType) + # mutation(Types::MutationType) +end diff --git a/app/graphql/mutations/.keep b/app/graphql/mutations/.keep new file mode 100644 index 00000000000..e69de29bb2d --- /dev/null +++ b/app/graphql/mutations/.keep diff --git a/app/graphql/resolvers/base_resolver.rb b/app/graphql/resolvers/base_resolver.rb new file mode 100644 index 00000000000..89b7f9dad6f --- /dev/null +++ b/app/graphql/resolvers/base_resolver.rb @@ -0,0 +1,4 @@ +module Resolvers + class BaseResolver < GraphQL::Schema::Resolver + end +end diff --git a/app/graphql/resolvers/full_path_resolver.rb b/app/graphql/resolvers/full_path_resolver.rb new file mode 100644 index 00000000000..4eb28aaed6c --- /dev/null +++ b/app/graphql/resolvers/full_path_resolver.rb @@ -0,0 +1,19 @@ +module Resolvers + module FullPathResolver + extend ActiveSupport::Concern + + prepended do + argument :full_path, GraphQL::ID_TYPE, + required: true, + description: 'The full path of the project or namespace, e.g., "gitlab-org/gitlab-ce"' + end + + def model_by_full_path(model, full_path) + BatchLoader.for(full_path).batch(key: "#{model.model_name.param_key}:full_path") do |full_paths, loader| + # `with_route` avoids an N+1 calculating full_path + results = model.where_full_path_in(full_paths).with_route + results.each { |project| loader.call(project.full_path, project) } + end + end + end +end diff --git a/app/graphql/resolvers/merge_request_resolver.rb b/app/graphql/resolvers/merge_request_resolver.rb new file mode 100644 index 00000000000..9f2d348e95f --- /dev/null +++ b/app/graphql/resolvers/merge_request_resolver.rb @@ -0,0 +1,20 @@ +module Resolvers + class MergeRequestResolver < BaseResolver + argument :iid, GraphQL::ID_TYPE, + required: true, + description: 'The IID of the merge request, e.g., "1"' + + type Types::MergeRequestType, null: true + + alias_method :project, :object + + def resolve(iid:) + return unless project.present? + + BatchLoader.for(iid.to_s).batch(key: project.id) do |iids, loader| + results = project.merge_requests.where(iid: iids) + results.each { |mr| loader.call(mr.iid.to_s, mr) } + end + end + end +end diff --git a/app/graphql/resolvers/project_resolver.rb b/app/graphql/resolvers/project_resolver.rb new file mode 100644 index 00000000000..ec115bad896 --- /dev/null +++ b/app/graphql/resolvers/project_resolver.rb @@ -0,0 +1,11 @@ +module Resolvers + class ProjectResolver < BaseResolver + prepend FullPathResolver + + type Types::ProjectType, null: true + + def resolve(full_path:) + model_by_full_path(Project, full_path) + end + end +end diff --git a/app/graphql/types/base_enum.rb b/app/graphql/types/base_enum.rb new file mode 100644 index 00000000000..b45a845f74f --- /dev/null +++ b/app/graphql/types/base_enum.rb @@ -0,0 +1,4 @@ +module Types + class BaseEnum < GraphQL::Schema::Enum + end +end diff --git a/app/graphql/types/base_field.rb b/app/graphql/types/base_field.rb new file mode 100644 index 00000000000..c5740a334d7 --- /dev/null +++ b/app/graphql/types/base_field.rb @@ -0,0 +1,5 @@ +module Types + class BaseField < GraphQL::Schema::Field + prepend Gitlab::Graphql::Authorize + end +end diff --git a/app/graphql/types/base_input_object.rb b/app/graphql/types/base_input_object.rb new file mode 100644 index 00000000000..309e336e6c8 --- /dev/null +++ b/app/graphql/types/base_input_object.rb @@ -0,0 +1,4 @@ +module Types + class BaseInputObject < GraphQL::Schema::InputObject + end +end diff --git a/app/graphql/types/base_interface.rb b/app/graphql/types/base_interface.rb new file mode 100644 index 00000000000..69e72dc5808 --- /dev/null +++ b/app/graphql/types/base_interface.rb @@ -0,0 +1,5 @@ +module Types + module BaseInterface + include GraphQL::Schema::Interface + end +end diff --git a/app/graphql/types/base_object.rb b/app/graphql/types/base_object.rb new file mode 100644 index 00000000000..e033ef96ce9 --- /dev/null +++ b/app/graphql/types/base_object.rb @@ -0,0 +1,7 @@ +module Types + class BaseObject < GraphQL::Schema::Object + prepend Gitlab::Graphql::Present + + field_class Types::BaseField + end +end diff --git a/app/graphql/types/base_scalar.rb b/app/graphql/types/base_scalar.rb new file mode 100644 index 00000000000..c0aa38be239 --- /dev/null +++ b/app/graphql/types/base_scalar.rb @@ -0,0 +1,4 @@ +module Types + class BaseScalar < GraphQL::Schema::Scalar + end +end diff --git a/app/graphql/types/base_union.rb b/app/graphql/types/base_union.rb new file mode 100644 index 00000000000..36337fc6ee5 --- /dev/null +++ b/app/graphql/types/base_union.rb @@ -0,0 +1,4 @@ +module Types + class BaseUnion < GraphQL::Schema::Union + end +end diff --git a/app/graphql/types/merge_request_type.rb b/app/graphql/types/merge_request_type.rb new file mode 100644 index 00000000000..d5d24952984 --- /dev/null +++ b/app/graphql/types/merge_request_type.rb @@ -0,0 +1,47 @@ +module Types + class MergeRequestType < BaseObject + present_using MergeRequestPresenter + + graphql_name 'MergeRequest' + + field :id, GraphQL::ID_TYPE, null: false + field :iid, GraphQL::ID_TYPE, null: false + field :title, GraphQL::STRING_TYPE, null: false + field :description, GraphQL::STRING_TYPE, null: true + field :state, GraphQL::STRING_TYPE, null: true + field :created_at, Types::TimeType, null: false + field :updated_at, Types::TimeType, null: false + field :source_project, Types::ProjectType, null: true + field :target_project, Types::ProjectType, null: false + # Alias for target_project + field :project, Types::ProjectType, null: false + field :project_id, GraphQL::INT_TYPE, null: false, method: :target_project_id + field :source_project_id, GraphQL::INT_TYPE, null: true + field :target_project_id, GraphQL::INT_TYPE, null: false + field :source_branch, GraphQL::STRING_TYPE, null: false + field :target_branch, GraphQL::STRING_TYPE, null: false + field :work_in_progress, GraphQL::BOOLEAN_TYPE, method: :work_in_progress?, null: false + field :merge_when_pipeline_succeeds, GraphQL::BOOLEAN_TYPE, null: true + field :diff_head_sha, GraphQL::STRING_TYPE, null: true + field :merge_commit_sha, GraphQL::STRING_TYPE, null: true + field :user_notes_count, GraphQL::INT_TYPE, null: true + field :should_remove_source_branch, GraphQL::BOOLEAN_TYPE, method: :should_remove_source_branch?, null: true + field :force_remove_source_branch, GraphQL::BOOLEAN_TYPE, method: :force_remove_source_branch?, null: true + field :merge_status, GraphQL::STRING_TYPE, null: true + field :in_progress_merge_commit_sha, GraphQL::STRING_TYPE, null: true + field :merge_error, GraphQL::STRING_TYPE, null: true + field :allow_collaboration, GraphQL::BOOLEAN_TYPE, null: true + field :should_be_rebased, GraphQL::BOOLEAN_TYPE, method: :should_be_rebased?, null: false + field :rebase_commit_sha, GraphQL::STRING_TYPE, null: true + field :rebase_in_progress, GraphQL::BOOLEAN_TYPE, method: :rebase_in_progress?, null: false + field :diff_head_sha, GraphQL::STRING_TYPE, null: true + field :merge_commit_message, GraphQL::STRING_TYPE, null: true + field :merge_ongoing, GraphQL::BOOLEAN_TYPE, method: :merge_ongoing?, null: false + field :source_branch_exists, GraphQL::BOOLEAN_TYPE, method: :source_branch_exists?, null: false + field :mergeable_discussions_state, GraphQL::BOOLEAN_TYPE, null: true + field :web_url, GraphQL::STRING_TYPE, null: true + field :upvotes, GraphQL::INT_TYPE, null: false + field :downvotes, GraphQL::INT_TYPE, null: false + field :subscribed, GraphQL::BOOLEAN_TYPE, method: :subscribed?, null: false + end +end diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb new file mode 100644 index 00000000000..06ed91c1658 --- /dev/null +++ b/app/graphql/types/mutation_type.rb @@ -0,0 +1,7 @@ +module Types + class MutationType < BaseObject + graphql_name "Mutation" + + # TODO: Add Mutations as fields + end +end diff --git a/app/graphql/types/project_type.rb b/app/graphql/types/project_type.rb new file mode 100644 index 00000000000..d9058ae7431 --- /dev/null +++ b/app/graphql/types/project_type.rb @@ -0,0 +1,72 @@ +module Types + class ProjectType < BaseObject + graphql_name 'Project' + + field :id, GraphQL::ID_TYPE, null: false + + field :full_path, GraphQL::ID_TYPE, null: false + field :path, GraphQL::STRING_TYPE, null: false + + field :name_with_namespace, GraphQL::STRING_TYPE, null: false + field :name, GraphQL::STRING_TYPE, null: false + + field :description, GraphQL::STRING_TYPE, null: true + + field :default_branch, GraphQL::STRING_TYPE, null: true + field :tag_list, GraphQL::STRING_TYPE, null: true + + field :ssh_url_to_repo, GraphQL::STRING_TYPE, null: true + field :http_url_to_repo, GraphQL::STRING_TYPE, null: true + field :web_url, GraphQL::STRING_TYPE, null: true + + field :star_count, GraphQL::INT_TYPE, null: false + field :forks_count, GraphQL::INT_TYPE, null: false + + field :created_at, Types::TimeType, null: true + field :last_activity_at, Types::TimeType, null: true + + field :archived, GraphQL::BOOLEAN_TYPE, null: true + + field :visibility, GraphQL::STRING_TYPE, null: true + + field :container_registry_enabled, GraphQL::BOOLEAN_TYPE, null: true + field :shared_runners_enabled, GraphQL::BOOLEAN_TYPE, null: true + field :lfs_enabled, GraphQL::BOOLEAN_TYPE, null: true + field :merge_requests_ff_only_enabled, GraphQL::BOOLEAN_TYPE, null: true + + field :avatar_url, GraphQL::STRING_TYPE, null: true, resolve: -> (project, args, ctx) do + project.avatar_url(only_path: false) + end + + %i[issues merge_requests wiki snippets].each do |feature| + field "#{feature}_enabled", GraphQL::BOOLEAN_TYPE, null: true, resolve: -> (project, args, ctx) do + project.feature_available?(feature, ctx[:current_user]) + end + end + + field :jobs_enabled, GraphQL::BOOLEAN_TYPE, null: true, resolve: -> (project, args, ctx) do + project.feature_available?(:builds, ctx[:current_user]) + end + + field :public_jobs, GraphQL::BOOLEAN_TYPE, method: :public_builds, null: true + + field :open_issues_count, GraphQL::INT_TYPE, null: true, resolve: -> (project, args, ctx) do + project.open_issues_count if project.feature_available?(:issues, ctx[:current_user]) + end + + field :import_status, GraphQL::STRING_TYPE, null: true + field :ci_config_path, GraphQL::STRING_TYPE, null: true + + field :only_allow_merge_if_pipeline_succeeds, GraphQL::BOOLEAN_TYPE, null: true + field :request_access_enabled, GraphQL::BOOLEAN_TYPE, null: true + field :only_allow_merge_if_all_discussions_are_resolved, GraphQL::BOOLEAN_TYPE, null: true + field :printing_merge_request_link_enabled, GraphQL::BOOLEAN_TYPE, null: true + + field :merge_request, + Types::MergeRequestType, + null: true, + resolver: Resolvers::MergeRequestResolver do + authorize :read_merge_request + end + end +end diff --git a/app/graphql/types/query_type.rb b/app/graphql/types/query_type.rb new file mode 100644 index 00000000000..010ec2d7942 --- /dev/null +++ b/app/graphql/types/query_type.rb @@ -0,0 +1,14 @@ +module Types + class QueryType < BaseObject + graphql_name 'Query' + + field :project, Types::ProjectType, + null: true, + resolver: Resolvers::ProjectResolver, + description: "Find a project" do + authorize :read_project + end + + field :echo, GraphQL::STRING_TYPE, null: false, function: Functions::Echo.new + end +end diff --git a/app/graphql/types/time_type.rb b/app/graphql/types/time_type.rb new file mode 100644 index 00000000000..2333d82ad1e --- /dev/null +++ b/app/graphql/types/time_type.rb @@ -0,0 +1,14 @@ +module Types + class TimeType < BaseScalar + graphql_name 'Time' + description 'Time represented in ISO 8601' + + def self.coerce_input(value, ctx) + Time.parse(value) + end + + def self.coerce_result(value, ctx) + value.iso8601 + end + end +end diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb index adc423af9e1..ef1bf283d0c 100644 --- a/app/helpers/application_settings_helper.rb +++ b/app/helpers/application_settings_helper.rb @@ -36,7 +36,7 @@ module ApplicationSettingsHelper # Return a group of checkboxes that use Bootstrap's button plugin for a # toggle button effect. - def restricted_level_checkboxes(help_block_id, checkbox_name) + def restricted_level_checkboxes(help_block_id, checkbox_name, options = {}) Gitlab::VisibilityLevel.values.map do |level| checked = restricted_visibility_levels(true).include?(level) css_class = checked ? 'active' : '' @@ -46,6 +46,7 @@ module ApplicationSettingsHelper check_box_tag(checkbox_name, level, checked, autocomplete: 'off', 'aria-describedby' => help_block_id, + 'class' => options[:class], id: tag_name) + visibility_level_icon(level) + visibility_level_label(level) end end @@ -53,7 +54,7 @@ module ApplicationSettingsHelper # Return a group of checkboxes that use Bootstrap's button plugin for a # toggle button effect. - def import_sources_checkboxes(help_block_id) + def import_sources_checkboxes(help_block_id, options = {}) Gitlab::ImportSources.options.map do |name, source| checked = Gitlab::CurrentSettings.import_sources.include?(source) css_class = checked ? 'active' : '' @@ -63,6 +64,7 @@ module ApplicationSettingsHelper check_box_tag(checkbox_name, source, checked, autocomplete: 'off', 'aria-describedby' => help_block_id, + 'class' => options[:class], id: name.tr(' ', '_')) + name end end diff --git a/app/helpers/favicon_helper.rb b/app/helpers/favicon_helper.rb new file mode 100644 index 00000000000..3a5342a8d9d --- /dev/null +++ b/app/helpers/favicon_helper.rb @@ -0,0 +1,7 @@ +module FaviconHelper + def favicon_extension_whitelist + FaviconUploader::EXTENSION_WHITELIST + .map { |extension| "'.#{extension}'"} + .to_sentence + end +end diff --git a/app/helpers/icons_helper.rb b/app/helpers/icons_helper.rb index 58372edff3c..2f304b040c7 100644 --- a/app/helpers/icons_helper.rb +++ b/app/helpers/icons_helper.rb @@ -60,7 +60,7 @@ module IconsHelper def spinner(text = nil, visible = false) css_class = 'loading' - css_class << ' hidden' unless visible + css_class << ' hide' unless visible content_tag :div, class: css_class do icon('spinner spin') + text diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index 52d6356e567..9f501ea55fb 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -165,7 +165,7 @@ module IssuablesHelper output << content_tag(:span, (issuable_first_contribution_icon if issuable.first_contribution?), class: 'has-tooltip', title: _('1st contribution!')) output << content_tag(:span, (issuable.task_status if issuable.tasks?), id: "task_status", class: "d-none d-sm-none d-md-inline-block") - output << content_tag(:span, (issuable.task_status_short if issuable.tasks?), id: "task_status_short", class: "d-md-none d-lg-none d-xl-inline-block") + output << content_tag(:span, (issuable.task_status_short if issuable.tasks?), id: "task_status_short", class: "d-md-none") output.html_safe end diff --git a/app/helpers/labels_helper.rb b/app/helpers/labels_helper.rb index e1b0e7a4a3e..c7df25cecef 100644 --- a/app/helpers/labels_helper.rb +++ b/app/helpers/labels_helper.rb @@ -211,6 +211,14 @@ module LabelsHelper end end + def label_status_tooltip(label, status) + type = label.is_a?(ProjectLabel) ? 'project' : 'group' + level = status.unsubscribed? ? type : status.sub('-level', '') + action = status.unsubscribed? ? 'Subscribe' : 'Unsubscribe' + + "#{action} at #{level} level" + end + # Required for Banzai::Filter::LabelReferenceFilter module_function :render_colored_label, :text_color_for_bg, :escape_once end diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb index 74251c260f0..5ff06b3e0fc 100644 --- a/app/helpers/merge_requests_helper.rb +++ b/app/helpers/merge_requests_helper.rb @@ -126,8 +126,8 @@ module MergeRequestsHelper link_to(url[merge_request.project, merge_request], data: data_attrs, &block) end - def allow_maintainer_push_unavailable_reason(merge_request) - return if merge_request.can_allow_maintainer_to_push?(current_user) + def allow_collaboration_unavailable_reason(merge_request) + return if merge_request.can_allow_collaboration?(current_user) minimum_visibility = [merge_request.target_project.visibility_level, merge_request.source_project.visibility_level].min diff --git a/app/helpers/page_layout_helper.rb b/app/helpers/page_layout_helper.rb index a8397b03d63..68d892393ef 100644 --- a/app/helpers/page_layout_helper.rb +++ b/app/helpers/page_layout_helper.rb @@ -39,10 +39,7 @@ module PageLayoutHelper end def favicon - return 'favicon-yellow.ico' if Gitlab::Utils.to_boolean(ENV['CANARY']) - return 'favicon-blue.ico' if Rails.env.development? - - 'favicon.ico' + Gitlab::Favicon.main end def page_image diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 55078e1a2d2..daad829faa2 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -171,11 +171,12 @@ module ProjectsHelper key = [ project.route.cache_key, project.cache_key, + project.last_activity_date, controller.controller_name, controller.action_name, Gitlab::CurrentSettings.cache_key, "cross-project:#{can?(current_user, :read_cross_project)}", - 'v2.5' + 'v2.6' ] key << pipeline_status_cache_key(project.pipeline_status) if project.pipeline_status.has_status? @@ -238,6 +239,14 @@ module ProjectsHelper "git push --set-upstream #{repository_url}/$(git rev-parse --show-toplevel | xargs basename).git $(git rev-parse --abbrev-ref HEAD)" end + def show_xcode_link?(project = @project) + browser.platform.mac? && project.repository.xcode_project? + end + + def xcode_uri_to_repo(project = @project) + "xcode://clone?repo=#{CGI.escape(default_url_to_repo(project))}" + end + private def get_project_nav_tabs(project, current_user) @@ -381,11 +390,11 @@ module ProjectsHelper def project_status_css_class(status) case status when "started" - "active" + "table-active" when "failed" - "danger" + "table-danger" when "finished" - "success" + "table-success" end end @@ -404,7 +413,10 @@ module ProjectsHelper exports_path = File.join(Settings.shared['path'], 'tmp/project_exports') filtered_message = message.strip.gsub(exports_path, "[REPO EXPORT PATH]") - disk_path = Gitlab.config.repositories.storages[project.repository_storage].legacy_disk_path + disk_path = Gitlab::GitalyClient::StorageSettings.allow_disk_access do + Gitlab.config.repositories.storages[project.repository_storage].legacy_disk_path + end + filtered_message.gsub(disk_path.chomp('/'), "[REPOS PATH]") end diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb index 761c1252fc8..f7dafca7834 100644 --- a/app/helpers/search_helper.rb +++ b/app/helpers/search_helper.rb @@ -25,14 +25,22 @@ module SearchHelper return unless collection.count > 0 from = collection.offset_value + 1 - to = collection.offset_value + collection.length + to = collection.offset_value + collection.count count = collection.total_count "Showing #{from} - #{to} of #{count} #{scope.humanize(capitalize: false)} for \"#{term}\"" end + def find_project_for_result_blob(result) + @project + end + def parse_search_result(result) - Gitlab::ProjectSearchResults.parse_search_result(result) + result + end + + def search_blob_title(project, filename) + filename end private diff --git a/app/helpers/workhorse_helper.rb b/app/helpers/workhorse_helper.rb index 9f78b80c71d..a82271ce0ee 100644 --- a/app/helpers/workhorse_helper.rb +++ b/app/helpers/workhorse_helper.rb @@ -6,7 +6,7 @@ module WorkhorseHelper headers.store(*Gitlab::Workhorse.send_git_blob(repository, blob)) headers['Content-Disposition'] = 'inline' headers['Content-Type'] = safe_content_type(blob) - head :ok # 'render nothing: true' messes up the Content-Type + render plain: "" end # Send a Git diff through Workhorse diff --git a/app/models/appearance.rb b/app/models/appearance.rb index 67cc84a9140..b770aadef0e 100644 --- a/app/models/appearance.rb +++ b/app/models/appearance.rb @@ -14,6 +14,7 @@ class Appearance < ActiveRecord::Base mount_uploader :logo, AttachmentUploader mount_uploader :header_logo, AttachmentUploader + mount_uploader :favicon, FaviconUploader # Overrides CacheableAttributes.current_without_cache def self.current_without_cache diff --git a/app/models/application_setting/term.rb b/app/models/application_setting/term.rb index e8ce0ccbb71..3b1dfe7e4ef 100644 --- a/app/models/application_setting/term.rb +++ b/app/models/application_setting/term.rb @@ -1,6 +1,7 @@ class ApplicationSetting class Term < ActiveRecord::Base include CacheMarkdownField + has_many :term_agreements validates :terms, presence: true @@ -9,5 +10,10 @@ class ApplicationSetting def self.latest order(:id).last end + + def accepted_by_user?(user) + user.accepted_term_id == id || + term_agreements.accepted.where(user: user).exists? + end end end diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 75fd55a8f7b..41446946a5e 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -55,12 +55,18 @@ module Ci where('(artifacts_file IS NOT NULL AND artifacts_file <> ?) OR EXISTS (?)', '', Ci::JobArtifact.select(1).where('ci_builds.id = ci_job_artifacts.job_id').archive) end + + scope :without_archived_trace, ->() do + where('NOT EXISTS (?)', Ci::JobArtifact.select(1).where('ci_builds.id = ci_job_artifacts.job_id').trace) + end + scope :with_artifacts_stored_locally, -> { with_artifacts_archive.where(artifacts_file_store: [nil, LegacyArtifactUploader::Store::LOCAL]) } scope :with_artifacts_not_expired, ->() { with_artifacts_archive.where('artifacts_expire_at IS NULL OR artifacts_expire_at > ?', Time.now) } scope :with_expired_artifacts, ->() { with_artifacts_archive.where('artifacts_expire_at < ?', Time.now) } scope :last_month, ->() { where('created_at > ?', Date.today - 1.month) } scope :manual_actions, ->() { where(when: :manual, status: COMPLETED_STATUSES + [:manual]) } scope :ref_protected, -> { where(protected: true) } + scope :with_live_trace, -> { where('EXISTS (?)', Ci::BuildTraceChunk.where('ci_builds.id = ci_build_trace_chunks.build_id').select(1)) } scope :matches_tag_ids, -> (tag_ids) do matcher = ::ActsAsTaggableOn::Tagging @@ -144,6 +150,7 @@ module Ci after_transition any => [:success] do |build| build.run_after_commit do BuildSuccessWorker.perform_async(id) + PagesWorker.perform_async(:deploy, id) if build.pages_generator? end end @@ -183,6 +190,11 @@ module Ci pipeline.manual_actions.where.not(name: name) end + def pages_generator? + Gitlab.config.pages.enabled && + self.name == 'pages' + end + def playable? action? && (manual? || retryable?) end @@ -402,8 +414,6 @@ module Ci build_data = Gitlab::DataBuilder::Build.build(self) project.execute_hooks(build_data.dup, :job_hooks) project.execute_services(build_data.dup, :job_hooks) - PagesService.new(build_data).execute - project.running_or_pending_build_count(force: true) end def browsable_artifacts? @@ -601,6 +611,7 @@ module Ci variables .concat(pipeline.persisted_variables) .append(key: 'CI_JOB_ID', value: id.to_s) + .append(key: 'CI_JOB_URL', value: Gitlab::Routing.url_helpers.project_job_url(project, self)) .append(key: 'CI_JOB_TOKEN', value: token, public: false) .append(key: 'CI_BUILD_ID', value: id.to_s) .append(key: 'CI_BUILD_TOKEN', value: token, public: false) diff --git a/app/models/ci/group.rb b/app/models/ci/group.rb index 87898b086c6..9c1046e8715 100644 --- a/app/models/ci/group.rb +++ b/app/models/ci/group.rb @@ -31,6 +31,14 @@ module Ci end end + def self.fabricate(stage) + stage.statuses.ordered.latest + .sort_by(&:sortable_name).group_by(&:group_name) + .map do |group_name, grouped_statuses| + self.new(stage, name: group_name, jobs: grouped_statuses) + end + end + private def commit_statuses diff --git a/app/models/ci/legacy_stage.rb b/app/models/ci/legacy_stage.rb index 9b536af672b..ce691875e42 100644 --- a/app/models/ci/legacy_stage.rb +++ b/app/models/ci/legacy_stage.rb @@ -16,11 +16,7 @@ module Ci end def groups - @groups ||= statuses.ordered.latest - .sort_by(&:sortable_name).group_by(&:group_name) - .map do |group_name, grouped_statuses| - Ci::Group.new(self, name: group_name, jobs: grouped_statuses) - end + @groups ||= Ci::Group.fabricate(self) end def to_param diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 53af87a271a..f430f18ca9a 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -7,13 +7,19 @@ module Ci include Presentable include Gitlab::OptimisticLocking include Gitlab::Utils::StrongMemoize + include AtomicInternalId + include EnumWithNil belongs_to :project, inverse_of: :pipelines belongs_to :user belongs_to :auto_canceled_by, class_name: 'Ci::Pipeline' belongs_to :pipeline_schedule, class_name: 'Ci::PipelineSchedule' - has_many :stages + has_internal_id :iid, scope: :project, presence: false, init: ->(s) do + s&.project&.pipelines&.maximum(:iid) || s&.project&.pipelines&.count + end + + has_many :stages, -> { order(position: :asc) }, inverse_of: :pipeline has_many :statuses, class_name: 'CommitStatus', foreign_key: :commit_id, inverse_of: :pipeline has_many :builds, foreign_key: :commit_id, inverse_of: :pipeline has_many :trigger_requests, dependent: :destroy, foreign_key: :commit_id # rubocop:disable Cop/ActiveRecordDependent @@ -49,7 +55,7 @@ module Ci after_create :keep_around_commits, unless: :importing? - enum source: { + enum_with_nil source: { unknown: nil, push: 1, web: 2, @@ -59,7 +65,7 @@ module Ci external: 6 } - enum config_source: { + enum_with_nil config_source: { unknown_source: nil, repository_source: 1, auto_devops_source: 2 @@ -249,6 +255,20 @@ module Ci stage unless stage.statuses_count.zero? end + ## + # TODO We do not completely switch to persisted stages because of + # race conditions with setting statuses gitlab-ce#23257. + # + def ordered_stages + return legacy_stages unless complete? + + if Feature.enabled?('ci_pipeline_persisted_stages') + stages + else + legacy_stages + end + end + def legacy_stages # TODO, this needs refactoring, see gitlab-ce#26481. @@ -411,7 +431,7 @@ module Ci def number_of_warnings BatchLoader.for(id).batch(default_value: 0) do |pipeline_ids, loader| - Build.where(commit_id: pipeline_ids) + ::Ci::Build.where(commit_id: pipeline_ids) .latest .failed_but_allowed .group(:commit_id) @@ -503,7 +523,8 @@ module Ci def update_status retry_optimistic_lock(self) do - case latest_builds_status + case latest_builds_status.to_s + when 'created' then nil when 'pending' then enqueue when 'running' then run when 'success' then succeed @@ -511,6 +532,9 @@ module Ci when 'canceled' then cancel when 'skipped' then skip when 'manual' then block + else + raise HasStatus::UnknownStatusError, + "Unknown status `#{latest_builds_status}`" end end end @@ -525,12 +549,16 @@ module Ci def persisted_variables Gitlab::Ci::Variables::Collection.new.tap do |variables| - variables.append(key: 'CI_PIPELINE_ID', value: id.to_s) if persisted? + break variables unless persisted? + + variables.append(key: 'CI_PIPELINE_ID', value: id.to_s) + variables.append(key: 'CI_PIPELINE_URL', value: Gitlab::Routing.url_helpers.project_pipeline_url(project, self)) end end def predefined_variables Gitlab::Ci::Variables::Collection.new + .append(key: 'CI_PIPELINE_IID', value: iid.to_s) .append(key: 'CI_CONFIG_PATH', value: ci_yaml_file_path) .append(key: 'CI_PIPELINE_SOURCE', value: source.to_s) .append(key: 'CI_COMMIT_MESSAGE', value: git_commit_message) @@ -575,17 +603,6 @@ module Ci @latest_builds_with_artifacts ||= builds.latest.with_artifacts_archive.to_a end - # Rails 5.0 autogenerated question mark enum methods return wrong result if enum value is nil. - # They always return `false`. - # These methods overwrite autogenerated ones to return correct results. - def unknown? - Gitlab.rails5? ? source.nil? : super - end - - def unknown_source? - Gitlab.rails5? ? config_source.nil? : super - end - private def ci_yaml_from_repo diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb index 57edd6a4956..8c9aacca8de 100644 --- a/app/models/ci/runner.rb +++ b/app/models/ci/runner.rb @@ -219,10 +219,8 @@ module Ci cache_attributes(values) - if persist_cached_data? - self.assign_attributes(values) - self.save if self.changed? - end + # We save data without validation, it will always change due to `contacted_at` + self.update_columns(values) if persist_cached_data? end def pick_build!(build) diff --git a/app/models/ci/stage.rb b/app/models/ci/stage.rb index 5a1eeb966aa..ea07f37e6c1 100644 --- a/app/models/ci/stage.rb +++ b/app/models/ci/stage.rb @@ -68,16 +68,44 @@ module Ci def update_status retry_optimistic_lock(self) do case statuses.latest.status + when 'created' then nil when 'pending' then enqueue when 'running' then run when 'success' then succeed when 'failed' then drop when 'canceled' then cancel when 'manual' then block - when 'skipped' then skip - else skip + when 'skipped', nil then skip + else + raise HasStatus::UnknownStatusError, + "Unknown status `#{statuses.latest.status}`" end end end + + def groups + @groups ||= Ci::Group.fabricate(self) + end + + def has_warnings? + number_of_warnings.positive? + end + + def number_of_warnings + BatchLoader.for(id).batch(default_value: 0) do |stage_ids, loader| + ::Ci::Build.where(stage_id: stage_ids) + .latest + .failed_but_allowed + .group(:stage_id) + .count + .each { |id, amount| loader.call(id, amount) } + end + end + + def detailed_status(current_user) + Gitlab::Ci::Status::Stage::Factory + .new(self, current_user) + .fabricate! + end end end diff --git a/app/models/clusters/platforms/kubernetes.rb b/app/models/clusters/platforms/kubernetes.rb index 25eac5160f1..36631d57ad1 100644 --- a/app/models/clusters/platforms/kubernetes.rb +++ b/app/models/clusters/platforms/kubernetes.rb @@ -11,12 +11,12 @@ module Clusters attr_encrypted :password, mode: :per_attribute_iv, - key: Settings.attr_encrypted_db_key_base, + key: Settings.attr_encrypted_db_key_base_truncated, algorithm: 'aes-256-cbc' attr_encrypted :token, mode: :per_attribute_iv, - key: Settings.attr_encrypted_db_key_base, + key: Settings.attr_encrypted_db_key_base_truncated, algorithm: 'aes-256-cbc' before_validation :enforce_namespace_to_lower_case diff --git a/app/models/clusters/providers/gcp.rb b/app/models/clusters/providers/gcp.rb index eb2e42fd3fe..4db1bb35c12 100644 --- a/app/models/clusters/providers/gcp.rb +++ b/app/models/clusters/providers/gcp.rb @@ -11,7 +11,7 @@ module Clusters attr_encrypted :access_token, mode: :per_attribute_iv, - key: Settings.attr_encrypted_db_key_base, + key: Settings.attr_encrypted_db_key_base_truncated, algorithm: 'aes-256-cbc' validates :gcp_project_id, diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index a7d05722287..97516079b66 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -3,6 +3,7 @@ class CommitStatus < ActiveRecord::Base include Importable include AfterCommitQueue include Presentable + include EnumWithNil self.table_name = 'ci_builds' @@ -39,7 +40,7 @@ class CommitStatus < ActiveRecord::Base scope :retried_ordered, -> { retried.ordered.includes(project: :namespace) } scope :after_stage, -> (index) { where('stage_idx > ?', index) } - enum failure_reason: { + enum_with_nil failure_reason: { unknown_failure: nil, script_failure: 1, api_failure: 2, @@ -190,11 +191,4 @@ class CommitStatus < ActiveRecord::Base v =~ /\d+/ ? v.to_i : v end end - - # Rails 5.0 autogenerated question mark enum methods return wrong result if enum value is nil. - # They always return `false`. - # This method overwrites the autogenerated one to return correct result. - def unknown_failure? - Gitlab.rails5? ? failure_reason.nil? : super - end end diff --git a/app/models/concerns/atomic_internal_id.rb b/app/models/concerns/atomic_internal_id.rb index 22f516a172f..164c704260e 100644 --- a/app/models/concerns/atomic_internal_id.rb +++ b/app/models/concerns/atomic_internal_id.rb @@ -25,9 +25,13 @@ module AtomicInternalId extend ActiveSupport::Concern module ClassMethods - def has_internal_id(column, scope:, init:) # rubocop:disable Naming/PredicateName - before_validation(on: :create) do + def has_internal_id(column, scope:, init:, presence: true) # rubocop:disable Naming/PredicateName + before_validation :"ensure_#{scope}_#{column}!", on: :create + validates column, presence: presence + + define_method("ensure_#{scope}_#{column}!") do scope_value = association(scope).reader + if read_attribute(column).blank? && scope_value scope_attrs = { scope_value.class.table_name.singularize.to_sym => scope_value } usage = self.class.table_name.to_sym @@ -35,13 +39,9 @@ module AtomicInternalId new_iid = InternalId.generate_next(self, scope_attrs, usage, init) write_attribute(column, new_iid) end - end - validates column, presence: true, numericality: true + read_attribute(column) + end end end - - def to_param - iid.to_s - end end diff --git a/app/models/concerns/avatarable.rb b/app/models/concerns/avatarable.rb index 13246a774e3..095897b08e3 100644 --- a/app/models/concerns/avatarable.rb +++ b/app/models/concerns/avatarable.rb @@ -4,11 +4,14 @@ module Avatarable included do prepend ShadowMethods include ObjectStorage::BackgroundMove + include Gitlab::Utils::StrongMemoize validate :avatar_type, if: ->(user) { user.avatar.present? && user.avatar_changed? } validates :avatar, file_size: { maximum: 200.kilobytes.to_i } mount_uploader :avatar, AvatarUploader + + after_initialize :add_avatar_to_batch end module ShadowMethods @@ -18,6 +21,17 @@ module Avatarable avatar_path(only_path: args.fetch(:only_path, true)) || super end + + def retrieve_upload(identifier, paths) + upload = retrieve_upload_from_batch(identifier) + + # This fallback is needed when deleting an upload, because we may have + # already been removed from the DB. We have to check an explicit `#nil?` + # because it's a BatchLoader instance. + upload = super if upload.nil? + + upload + end end def avatar_type @@ -52,4 +66,37 @@ module Avatarable url_base + avatar.local_url end + + # Path that is persisted in the tracking Upload model. Used to fetch the + # upload from the model. + def upload_paths(identifier) + avatar_mounter.blank_uploader.store_dirs.map { |store, path| File.join(path, identifier) } + end + + private + + def retrieve_upload_from_batch(identifier) + BatchLoader.for(identifier: identifier, model: self).batch(key: self.class) do |upload_params, loader, args| + model_class = args[:key] + paths = upload_params.flat_map do |params| + params[:model].upload_paths(params[:identifier]) + end + + Upload.where(uploader: AvatarUploader, path: paths).find_each do |upload| + model = model_class.instantiate('id' => upload.model_id) + + loader.call({ model: model, identifier: File.basename(upload.path) }, upload) + end + end + end + + def add_avatar_to_batch + return unless avatar_mounter + + avatar_mounter.read_identifiers.each { |identifier| retrieve_upload_from_batch(identifier) } + end + + def avatar_mounter + strong_memoize(:avatar_mounter) { _mounter(:avatar) } + end end diff --git a/app/models/concerns/cache_markdown_field.rb b/app/models/concerns/cache_markdown_field.rb index db8cf322ef7..9f6358cecbe 100644 --- a/app/models/concerns/cache_markdown_field.rb +++ b/app/models/concerns/cache_markdown_field.rb @@ -114,7 +114,7 @@ module CacheMarkdownField end def latest_cached_markdown_version - return CacheMarkdownField::CACHE_REDCARPET_VERSION unless cached_markdown_version + return CacheMarkdownField::CACHE_COMMONMARK_VERSION unless cached_markdown_version if cached_markdown_version < CacheMarkdownField::CACHE_COMMONMARK_VERSION_START CacheMarkdownField::CACHE_REDCARPET_VERSION diff --git a/app/models/concerns/enum_with_nil.rb b/app/models/concerns/enum_with_nil.rb new file mode 100644 index 00000000000..6b37903da20 --- /dev/null +++ b/app/models/concerns/enum_with_nil.rb @@ -0,0 +1,33 @@ +module EnumWithNil + extend ActiveSupport::Concern + + included do + def self.enum_with_nil(definitions) + # use original `enum` to auto-define all methods + enum(definitions) + + # override auto-defined methods only for the + # key which uses nil value + definitions.each do |name, values| + next unless key_with_nil = values.key(nil) + + # E.g. for enum_with_nil failure_reason: { unknown_failure: nil } + # this overrides auto-generated method `unknown_failure?` + define_method("#{key_with_nil}?") do + Gitlab.rails5? ? self[name].nil? : super() + end + + # E.g. for enum_with_nil failure_reason: { unknown_failure: nil } + # this overrides auto-generated method `failure_reason` + define_method(name) do + orig = super() + + return orig unless Gitlab.rails5? + return orig unless orig.nil? + + self.class.public_send(name.to_s.pluralize).key(nil) # rubocop:disable GitlabSecurity/PublicSend + end + end + end + end +end diff --git a/app/models/concerns/has_status.rb b/app/models/concerns/has_status.rb index 7c3ed96bc28..72c236a0fc7 100644 --- a/app/models/concerns/has_status.rb +++ b/app/models/concerns/has_status.rb @@ -11,6 +11,8 @@ module HasStatus STATUSES_ENUM = { created: 0, pending: 1, running: 2, success: 3, failed: 4, canceled: 5, skipped: 6, manual: 7 }.freeze + UnknownStatusError = Class.new(StandardError) + class_methods do def status_sql scope_relevant = respond_to?(:exclude_ignored) ? exclude_ignored : all diff --git a/app/models/concerns/iid_routes.rb b/app/models/concerns/iid_routes.rb new file mode 100644 index 00000000000..246748cf52c --- /dev/null +++ b/app/models/concerns/iid_routes.rb @@ -0,0 +1,9 @@ +module IidRoutes + ## + # This automagically enforces all related routes to use `iid` instead of `id` + # If you want to use `iid` for some routes and `id` for other routes, this module should not to be included, + # instead you should define `iid` or `id` explictly at each route generators. e.g. pipeline_path(project.id, pipeline.iid) + def to_param + iid.to_s + end +end diff --git a/app/models/concerns/protected_ref_access.rb b/app/models/concerns/protected_ref_access.rb index bfda5b1678b..e3a7f2d5498 100644 --- a/app/models/concerns/protected_ref_access.rb +++ b/app/models/concerns/protected_ref_access.rb @@ -8,8 +8,8 @@ module ProtectedRefAccess ].freeze HUMAN_ACCESS_LEVELS = { - Gitlab::Access::MASTER => "Masters".freeze, - Gitlab::Access::DEVELOPER => "Developers + Masters".freeze, + Gitlab::Access::MASTER => "Maintainers".freeze, + Gitlab::Access::DEVELOPER => "Developers + Maintainers".freeze, Gitlab::Access::NO_ACCESS => "No one".freeze }.freeze diff --git a/app/models/concerns/with_uploads.rb b/app/models/concerns/with_uploads.rb index e7cfffb775b..4245d083a49 100644 --- a/app/models/concerns/with_uploads.rb +++ b/app/models/concerns/with_uploads.rb @@ -36,4 +36,8 @@ module WithUploads upload.destroy end end + + def retrieve_upload(_identifier, paths) + uploads.find_by(path: paths) + end end diff --git a/app/models/deployment.rb b/app/models/deployment.rb index 254764eefde..ac86e9e8de0 100644 --- a/app/models/deployment.rb +++ b/app/models/deployment.rb @@ -1,5 +1,6 @@ class Deployment < ActiveRecord::Base include AtomicInternalId + include IidRoutes belongs_to :project, required: true belongs_to :environment, required: true diff --git a/app/models/group.rb b/app/models/group.rb index 8fb77a7869d..9c171de7fc3 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -11,6 +11,7 @@ class Group < Namespace include GroupDescendant include TokenAuthenticatable include WithUploads + include Gitlab::Utils::StrongMemoize has_many :group_members, -> { where(requested_at: nil) }, dependent: :destroy, as: :source # rubocop:disable Cop/ActiveRecordDependent alias_method :members, :group_members @@ -26,7 +27,11 @@ class Group < Namespace has_many :milestones has_many :project_group_links, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_many :shared_projects, through: :project_group_links, source: :project + + # Overridden on another method + # Left here just to be dependent: :destroy has_many :notification_settings, dependent: :destroy, as: :source # rubocop:disable Cop/ActiveRecordDependent + has_many :labels, class_name: 'GroupLabel' has_many :variables, class_name: 'Ci::GroupVariable' has_many :custom_attributes, class_name: 'GroupCustomAttribute' @@ -88,6 +93,15 @@ class Group < Namespace end end + # Overrides notification_settings has_many association + # This allows to apply notification settings from parent groups + # to child groups and projects. + def notification_settings + source_type = self.class.base_class.name + + NotificationSetting.where(source_type: source_type, source_id: self_and_ancestors_ids) + end + def to_reference(_from = nil, full: nil) "#{self.class.reference_prefix}#{full_path}" end @@ -141,13 +155,14 @@ class Group < Namespace ) end - def add_user(user, access_level, current_user: nil, expires_at: nil) + def add_user(user, access_level, current_user: nil, expires_at: nil, ldap: false) GroupMember.add_user( self, user, access_level, current_user: current_user, - expires_at: expires_at + expires_at: expires_at, + ldap: ldap ) end @@ -195,6 +210,10 @@ class Group < Namespace owners.include?(user) && owners.size == 1 end + def ldap_synced? + false + end + def post_create_hook Gitlab::AppLogger.info("Group \"#{name}\" was created") @@ -220,6 +239,12 @@ class Group < Namespace members_with_parents.pluck(:user_id) end + def self_and_ancestors_ids + strong_memoize(:self_and_ancestors_ids) do + self_and_ancestors.pluck(:id) + end + end + def members_with_parents # Avoids an unnecessary SELECT when the group has no parents source_ids = diff --git a/app/models/internal_id.rb b/app/models/internal_id.rb index f7f930e86ed..f50f28deffe 100644 --- a/app/models/internal_id.rb +++ b/app/models/internal_id.rb @@ -14,7 +14,7 @@ class InternalId < ActiveRecord::Base belongs_to :project belongs_to :namespace - enum usage: { issues: 0, merge_requests: 1, deployments: 2, milestones: 3, epics: 4 } + enum usage: { issues: 0, merge_requests: 1, deployments: 2, milestones: 3, epics: 4, ci_pipelines: 5 } validates :usage, presence: true diff --git a/app/models/issue.rb b/app/models/issue.rb index 41a290f34b4..d136700836d 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -2,6 +2,7 @@ require 'carrierwave/orm/activerecord' class Issue < ActiveRecord::Base include AtomicInternalId + include IidRoutes include Issuable include Noteable include Referable diff --git a/app/models/label.rb b/app/models/label.rb index de7f1d56c64..1cf04976602 100644 --- a/app/models/label.rb +++ b/app/models/label.rb @@ -137,6 +137,10 @@ class Label < ActiveRecord::Base priority.try(:priority) end + def priority? + priorities.present? + end + def template? template end diff --git a/app/models/list.rb b/app/models/list.rb index 5daf35ef845..4edcfa78835 100644 --- a/app/models/list.rb +++ b/app/models/list.rb @@ -2,17 +2,27 @@ class List < ActiveRecord::Base belongs_to :board belongs_to :label - enum list_type: { backlog: 0, label: 1, closed: 2 } + enum list_type: { backlog: 0, label: 1, closed: 2, assignee: 3 } validates :board, :list_type, presence: true validates :label, :position, presence: true, if: :label? validates :label_id, uniqueness: { scope: :board_id }, if: :label? - validates :position, numericality: { only_integer: true, greater_than_or_equal_to: 0 }, if: :label? + validates :position, numericality: { only_integer: true, greater_than_or_equal_to: 0 }, if: :movable? before_destroy :can_be_destroyed - scope :destroyable, -> { where(list_type: list_types[:label]) } - scope :movable, -> { where(list_type: list_types[:label]) } + scope :destroyable, -> { where(list_type: list_types.slice(*destroyable_types).values) } + scope :movable, -> { where(list_type: list_types.slice(*movable_types).values) } + + class << self + def destroyable_types + [:label] + end + + def movable_types + [:label] + end + end def destroyable? label? diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 79fc155fd3c..324065c1162 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -1,5 +1,6 @@ class MergeRequest < ActiveRecord::Base include AtomicInternalId + include IidRoutes include Issuable include Noteable include Referable @@ -1124,21 +1125,24 @@ class MergeRequest < ActiveRecord::Base project.merge_requests.merged.where(author_id: author_id).empty? end - def allow_maintainer_to_push - maintainer_push_possible? && super + # TODO: remove once production database rename completes + alias_attribute :allow_collaboration, :allow_maintainer_to_push + + def allow_collaboration + collaborative_push_possible? && allow_maintainer_to_push end - alias_method :allow_maintainer_to_push?, :allow_maintainer_to_push + alias_method :allow_collaboration?, :allow_collaboration - def maintainer_push_possible? + def collaborative_push_possible? source_project.present? && for_fork? && target_project.visibility_level > Gitlab::VisibilityLevel::PRIVATE && source_project.visibility_level > Gitlab::VisibilityLevel::PRIVATE && !ProtectedBranch.protected?(source_project, source_branch) end - def can_allow_maintainer_to_push?(user) - maintainer_push_possible? && + def can_allow_collaboration?(user) + collaborative_push_possible? && Ability.allowed?(user, :push_code, source_project) end diff --git a/app/models/milestone.rb b/app/models/milestone.rb index d14e3a4ded5..d05dcfd083a 100644 --- a/app/models/milestone.rb +++ b/app/models/milestone.rb @@ -9,6 +9,7 @@ class Milestone < ActiveRecord::Base include CacheMarkdownField include AtomicInternalId + include IidRoutes include Sortable include Referable include StripAttribute diff --git a/app/models/note.rb b/app/models/note.rb index 02f7a9b1e4f..41c04ae0571 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -435,6 +435,10 @@ class Note < ActiveRecord::Base super.merge(noteable: noteable) end + def retrieve_upload(_identifier, paths) + Upload.find_by(model: self, path: paths) + end + private def keep_around_commit diff --git a/app/models/notification_recipient.rb b/app/models/notification_recipient.rb index 2c3580bbdc6..1a03dd9df56 100644 --- a/app/models/notification_recipient.rb +++ b/app/models/notification_recipient.rb @@ -1,4 +1,6 @@ class NotificationRecipient + include Gitlab::Utils::StrongMemoize + attr_reader :user, :type, :reason def initialize(user, type, **opts) unless NotificationSetting.levels.key?(type) || type == :subscription @@ -64,7 +66,7 @@ class NotificationRecipient return false unless @target return false unless @target.respond_to?(:subscriptions) - subscription = @target.subscriptions.find_by_user_id(@user.id) + subscription = @target.subscriptions.find { |subscription| subscription.user_id == @user.id } subscription && !subscription.subscribed end @@ -142,10 +144,33 @@ class NotificationRecipient return project_setting unless project_setting.nil? || project_setting.global? - group_setting = @group && user.notification_settings_for(@group) + group_setting = closest_non_global_group_notification_settting - return group_setting unless group_setting.nil? || group_setting.global? + return group_setting unless group_setting.nil? user.global_notification_setting end + + # Returns the notificaton_setting of the lowest group in hierarchy with non global level + def closest_non_global_group_notification_settting + return unless @group + return if indexed_group_notification_settings.empty? + + notification_setting = nil + + @group.self_and_ancestors_ids.each do |id| + notification_setting = indexed_group_notification_settings[id] + break if notification_setting + end + + notification_setting + end + + def indexed_group_notification_settings + strong_memoize(:indexed_group_notification_settings) do + @group.notification_settings.where(user_id: user.id) + .where.not(level: NotificationSetting.levels[:global]) + .index_by(&:source_id) + end + end end diff --git a/app/models/personal_snippet.rb b/app/models/personal_snippet.rb index 82c1c4de3a0..355624fd552 100644 --- a/app/models/personal_snippet.rb +++ b/app/models/personal_snippet.rb @@ -1,2 +1,3 @@ class PersonalSnippet < Snippet + include WithUploads end diff --git a/app/models/project.rb b/app/models/project.rb index 32298fc7f5c..e5fa1c4db7b 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -68,7 +68,7 @@ class Project < ActiveRecord::Base add_authentication_token_field :runners_token - before_validation :mark_remote_mirrors_for_removal, if: -> { ActiveRecord::Base.connection.table_exists?(:remote_mirrors) } + before_validation :mark_remote_mirrors_for_removal, if: -> { RemoteMirror.table_exists? } before_save :ensure_runners_token @@ -228,6 +228,7 @@ class Project < ActiveRecord::Base has_many :commit_statuses has_many :pipelines, class_name: 'Ci::Pipeline', inverse_of: :project + has_many :stages, class_name: 'Ci::Stage', inverse_of: :project # Ci::Build objects store data on the file system such as artifact files and # build traces. Currently there's no efficient way of removing this data in @@ -291,6 +292,7 @@ class Project < ActiveRecord::Base validates :name, uniqueness: { scope: :namespace_id } validates :import_url, url: { protocols: %w(http https ssh git), allow_localhost: false, + enforce_user: true, ports: VALID_IMPORT_PORTS }, if: [:external_import?, :import_url_changed?] validates :star_count, numericality: { greater_than_or_equal_to: 0 } validate :check_limit, on: :create @@ -674,6 +676,12 @@ class Project < ActiveRecord::Base end end + def human_import_status_name + ensure_import_state + + import_state.human_status_name + end + def import_schedule ensure_import_state(force: true) @@ -1425,8 +1433,14 @@ class Project < ActiveRecord::Base Ci::Runner.from("(#{union.to_sql}) ci_runners") end + def active_runners + strong_memoize(:active_runners) do + all_runners.active + end + end + def any_runners?(&block) - all_runners.active.any?(&block) + active_runners.any?(&block) end def valid_runners_token?(token) @@ -1602,6 +1616,7 @@ class Project < ActiveRecord::Base def after_import repository.after_import + wiki.repository.after_import import_finish remove_import_jid update_project_counter_caches @@ -1649,12 +1664,6 @@ class Project < ActiveRecord::Base import_state.update_column(:jid, nil) end - def running_or_pending_build_count(force: false) - Rails.cache.fetch(['projects', id, 'running_or_pending_build_count'], force: force) do - builds.running_or_pending.count(:all) - end - end - # Lazy loading of the `pipeline_status` attribute def pipeline_status @pipeline_status ||= Gitlab::Cache::Ci::ProjectPipelineStatus.load_for_project(self) @@ -1974,18 +1983,18 @@ class Project < ActiveRecord::Base .limit(1) .select(1) source_of_merge_requests.opened - .where(allow_maintainer_to_push: true) + .where(allow_collaboration: true) .where('EXISTS (?)', developer_access_exists) end - def branch_allows_maintainer_push?(user, branch_name) + def branch_allows_collaboration?(user, branch_name) return false unless user cache_key = "user:#{user.id}:#{branch_name}:branch_allows_push" - memoized_results = strong_memoize(:branch_allows_maintainer_push) do + memoized_results = strong_memoize(:branch_allows_collaboration) do Hash.new do |result, cache_key| - result[cache_key] = fetch_branch_allows_maintainer_push?(user, branch_name) + result[cache_key] = fetch_branch_allows_collaboration?(user, branch_name) end end @@ -2127,18 +2136,22 @@ class Project < ActiveRecord::Base raise ex end - def fetch_branch_allows_maintainer_push?(user, branch_name) + def fetch_branch_allows_collaboration?(user, branch_name) check_access = -> do next false if empty_repo? - merge_request = source_of_merge_requests.opened - .where(allow_maintainer_to_push: true) - .find_by(source_branch: branch_name) - merge_request&.can_be_merged_by?(user) + merge_requests = source_of_merge_requests.opened + .where(allow_collaboration: true) + + if branch_name + merge_requests.find_by(source_branch: branch_name)&.can_be_merged_by?(user) + else + merge_requests.any? { |merge_request| merge_request.can_be_merged_by?(user) } + end end if RequestStore.active? - RequestStore.fetch("project-#{id}:branch-#{branch_name}:user-#{user.id}:branch_allows_maintainer_push") do + RequestStore.fetch("project-#{id}:branch-#{branch_name}:user-#{user.id}:branch_allows_collaboration") do check_access.call end else diff --git a/app/models/project_auto_devops.rb b/app/models/project_auto_devops.rb index ed6c1eddbc1..d7d6aaceb27 100644 --- a/app/models/project_auto_devops.rb +++ b/app/models/project_auto_devops.rb @@ -1,11 +1,18 @@ class ProjectAutoDevops < ActiveRecord::Base belongs_to :project + enum deploy_strategy: { + continuous: 0, + manual: 1 + } + scope :enabled, -> { where(enabled: true) } scope :disabled, -> { where(enabled: false) } validates :domain, allow_blank: true, hostname: { allow_numeric_hostname: true } + after_save :create_gitlab_deploy_token, if: :needs_to_create_deploy_token? + def instance_domain Gitlab::CurrentSettings.auto_devops_domain end @@ -20,6 +27,30 @@ class ProjectAutoDevops < ActiveRecord::Base variables.append(key: 'AUTO_DEVOPS_DOMAIN', value: domain.presence || instance_domain) end + + if manual? + variables.append(key: 'STAGING_ENABLED', value: 1) + variables.append(key: 'INCREMENTAL_ROLLOUT_ENABLED', value: 1) + end end end + + private + + def create_gitlab_deploy_token + project.deploy_tokens.create!( + name: DeployToken::GITLAB_DEPLOY_TOKEN_NAME, + read_registry: true + ) + end + + def needs_to_create_deploy_token? + auto_devops_enabled? && + !project.public? && + !project.deploy_tokens.find_by(name: DeployToken::GITLAB_DEPLOY_TOKEN_NAME).present? + end + + def auto_devops_enabled? + Gitlab::CurrentSettings.auto_devops_enabled? || enabled? + end end diff --git a/app/models/project_services/chat_message/base_message.rb b/app/models/project_services/chat_message/base_message.rb index 22a65b5145e..f710fa85b5d 100644 --- a/app/models/project_services/chat_message/base_message.rb +++ b/app/models/project_services/chat_message/base_message.rb @@ -26,13 +26,18 @@ module ChatMessage end end - def pretext + def summary return message if markdown format(message) end + def pretext + summary + end + def fallback + format(message) end def attachments diff --git a/app/models/project_services/chat_message/pipeline_message.rb b/app/models/project_services/chat_message/pipeline_message.rb index 2135122278a..96fd23aede3 100644 --- a/app/models/project_services/chat_message/pipeline_message.rb +++ b/app/models/project_services/chat_message/pipeline_message.rb @@ -23,10 +23,6 @@ module ChatMessage '' end - def fallback - format(message) - end - def attachments return message if markdown diff --git a/app/models/project_services/gemnasium_service.rb b/app/models/project_services/gemnasium_service.rb index 84248f9590b..8a6b0ed1a5f 100644 --- a/app/models/project_services/gemnasium_service.rb +++ b/app/models/project_services/gemnasium_service.rb @@ -43,13 +43,18 @@ class GemnasiumService < Service def execute(data) return unless supported_events.include?(data[:object_kind]) + # Gitaly: this class will be removed https://gitlab.com/gitlab-org/gitlab-ee/issues/6010 + repo_path = Gitlab::GitalyClient::StorageSettings.allow_disk_access do + project.repository.path_to_repo + end + Gemnasium::GitlabService.execute( ref: data[:ref], before: data[:before], after: data[:after], token: token, api_key: api_key, - repo: project.repository.path_to_repo # Gitaly: fixed by https://gitlab.com/gitlab-org/security-products/gemnasium-migration/issues/9 + repo: repo_path ) end end diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb index eb3261c902f..412d62388f0 100644 --- a/app/models/project_services/jira_service.rb +++ b/app/models/project_services/jira_service.rb @@ -265,7 +265,7 @@ class JiraService < IssueTrackerService title: title, status: status, icon: { - title: 'GitLab', url16x16: asset_url('favicon.ico', host: gitlab_config.url) + title: 'GitLab', url16x16: asset_url(Gitlab::Favicon.main, host: gitlab_config.url) } } } diff --git a/app/models/project_services/microsoft_teams_service.rb b/app/models/project_services/microsoft_teams_service.rb index 2facff53e26..99500caec0e 100644 --- a/app/models/project_services/microsoft_teams_service.rb +++ b/app/models/project_services/microsoft_teams_service.rb @@ -44,7 +44,7 @@ class MicrosoftTeamsService < ChatNotificationService def notify(message, opts) MicrosoftTeams::Notifier.new(webhook).ping( title: message.project_name, - pretext: message.pretext, + summary: message.summary, activity: message.activity, attachments: message.attachments ) diff --git a/app/models/project_wiki.rb b/app/models/project_wiki.rb index f799a0b4227..a6f94b3e3b0 100644 --- a/app/models/project_wiki.rb +++ b/app/models/project_wiki.rb @@ -140,10 +140,6 @@ class ProjectWiki [title, title_array.join("/")] end - def search_files(query) - repository.search_files_by_content(query, default_branch) - end - def repository @repository ||= Repository.new(full_path, @project, disk_path: disk_path, is_wiki: true) end diff --git a/app/models/protected_branch.rb b/app/models/protected_branch.rb index cb361a66591..dff99cfca35 100644 --- a/app/models/protected_branch.rb +++ b/app/models/protected_branch.rb @@ -5,7 +5,7 @@ class ProtectedBranch < ActiveRecord::Base protected_ref_access_levels :merge, :push def self.protected_ref_accessible_to?(ref, user, project:, action:, protected_refs: nil) - # Masters, owners and admins are allowed to create the default branch + # Maintainers, owners and admins are allowed to create the default branch if default_branch_protected? && project.empty_repo? return true if user.admin? || project.team.max_member_access(user.id) > Gitlab::Access::DEVELOPER end diff --git a/app/models/remote_mirror.rb b/app/models/remote_mirror.rb index 5cd222e18a4..c4b5dd2dc96 100644 --- a/app/models/remote_mirror.rb +++ b/app/models/remote_mirror.rb @@ -16,7 +16,7 @@ class RemoteMirror < ActiveRecord::Base belongs_to :project, inverse_of: :remote_mirrors - validates :url, presence: true, url: { protocols: %w(ssh git http https), allow_blank: true } + validates :url, presence: true, url: { protocols: %w(ssh git http https), allow_blank: true, enforce_user: true } before_save :set_new_remote_name, if: :mirror_url_changed? diff --git a/app/models/repository.rb b/app/models/repository.rb index 82cf47ba04e..e4202505634 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -270,6 +270,16 @@ class Repository end end + def archive_metadata(ref, storage_path, format = "tar.gz", append_sha:) + raw_repository.archive_metadata( + ref, + storage_path, + project.path, + format, + append_sha: append_sha + ) + end + def expire_tags_cache expire_method_caches(%i(tag_names tag_count)) @tags = nil @@ -946,6 +956,10 @@ class Repository blob_data_at(sha, path) end + def lfsconfig_for(sha) + blob_data_at(sha, '.lfsconfig') + end + def fetch_ref(source_repository, source_ref:, target_ref:) raw_repository.fetch_ref(source_repository.raw_repository, source_ref: source_ref, target_ref: target_ref) end diff --git a/app/models/term_agreement.rb b/app/models/term_agreement.rb index 8458a231bbd..c317bd0c90b 100644 --- a/app/models/term_agreement.rb +++ b/app/models/term_agreement.rb @@ -2,5 +2,7 @@ class TermAgreement < ActiveRecord::Base belongs_to :term, class_name: 'ApplicationSetting::Term' belongs_to :user + scope :accepted, -> { where(accepted: true) } + validates :user, :term, presence: true end diff --git a/app/models/timelog.rb b/app/models/timelog.rb index f4c5c581a11..659146f43e4 100644 --- a/app/models/timelog.rb +++ b/app/models/timelog.rb @@ -19,4 +19,9 @@ class Timelog < ActiveRecord::Base errors.add(:base, 'Issue or Merge Request ID is required') end end + + # Rails5 defaults to :touch_later, overwrite for normal touch + def belongs_to_touch_method + :touch + end end diff --git a/app/models/user.rb b/app/models/user.rb index e219ab800ad..8e0dc91b2a7 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1038,7 +1038,10 @@ class User < ActiveRecord::Base def notification_settings_for(source) if notification_settings.loaded? - notification_settings.find { |notification| notification.source == source } + notification_settings.find do |notification| + notification.source_type == source.class.base_class.name && + notification.source_id == source.id + end else notification_settings.find_or_initialize_by(source: source) end diff --git a/app/policies/ci/build_policy.rb b/app/policies/ci/build_policy.rb index 8b65758f3e8..1c0cc7425ec 100644 --- a/app/policies/ci/build_policy.rb +++ b/app/policies/ci/build_policy.rb @@ -14,8 +14,8 @@ module Ci @subject.triggered_by?(@user) end - condition(:branch_allows_maintainer_push) do - @subject.project.branch_allows_maintainer_push?(@user, @subject.ref) + condition(:branch_allows_collaboration) do + @subject.project.branch_allows_collaboration?(@user, @subject.ref) end rule { protected_ref }.policy do @@ -25,7 +25,7 @@ module Ci rule { can?(:admin_build) | (can?(:update_build) & owner_of_job) }.enable :erase_build - rule { can?(:public_access) & branch_allows_maintainer_push }.policy do + rule { can?(:public_access) & branch_allows_collaboration }.policy do enable :update_build enable :update_commit_status end diff --git a/app/policies/ci/pipeline_policy.rb b/app/policies/ci/pipeline_policy.rb index 540e4235299..b81329d0625 100644 --- a/app/policies/ci/pipeline_policy.rb +++ b/app/policies/ci/pipeline_policy.rb @@ -4,13 +4,13 @@ module Ci condition(:protected_ref) { ref_protected?(@user, @subject.project, @subject.tag?, @subject.ref) } - condition(:branch_allows_maintainer_push) do - @subject.project.branch_allows_maintainer_push?(@user, @subject.ref) + condition(:branch_allows_collaboration) do + @subject.project.branch_allows_collaboration?(@user, @subject.ref) end rule { protected_ref }.prevent :update_pipeline - rule { can?(:public_access) & branch_allows_maintainer_push }.policy do + rule { can?(:public_access) & branch_allows_collaboration }.policy do enable :update_pipeline end diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb index 99a0d7118f2..8ea5435d740 100644 --- a/app/policies/project_policy.rb +++ b/app/policies/project_policy.rb @@ -45,7 +45,7 @@ class ProjectPolicy < BasePolicy desc "User has developer access" condition(:developer) { team_access_level >= Gitlab::Access::DEVELOPER } - desc "User has master access" + desc "User has maintainer access" condition(:master) { team_access_level >= Gitlab::Access::MASTER } desc "Project is public" diff --git a/app/presenters/merge_request_presenter.rb b/app/presenters/merge_request_presenter.rb index ad839d9840a..8d466c33510 100644 --- a/app/presenters/merge_request_presenter.rb +++ b/app/presenters/merge_request_presenter.rb @@ -179,6 +179,25 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated .can_push_to_branch?(source_branch) end + def mergeable_discussions_state + # This avoids calling MergeRequest#mergeable_discussions_state without + # considering the state of the MR first. If a MR isn't mergeable, we can + # safely short-circuit it. + if merge_request.mergeable_state?(skip_ci_check: true, skip_discussions_check: true) + merge_request.mergeable_discussions_state? + else + false + end + end + + def web_url + Gitlab::UrlBuilder.build(merge_request) + end + + def subscribed? + merge_request.subscribed?(current_user, merge_request.target_project) + end + private def cached_can_be_reverted? diff --git a/app/serializers/merge_request_basic_entity.rb b/app/serializers/merge_request_basic_entity.rb index e4aec977f01..1c06691026d 100644 --- a/app/serializers/merge_request_basic_entity.rb +++ b/app/serializers/merge_request_basic_entity.rb @@ -5,4 +5,8 @@ class MergeRequestBasicEntity < IssuableSidebarEntity expose :state expose :source_branch_exists?, as: :source_branch_exists expose :rebase_in_progress?, as: :rebase_in_progress + expose :milestone, using: API::Entities::Milestone + expose :labels, using: LabelEntity + expose :assignee, using: API::Entities::UserBasic + expose :task_status, :task_status_short end diff --git a/app/serializers/merge_request_widget_entity.rb b/app/serializers/merge_request_widget_entity.rb index 141070aef45..8260c6c7b84 100644 --- a/app/serializers/merge_request_widget_entity.rb +++ b/app/serializers/merge_request_widget_entity.rb @@ -13,7 +13,7 @@ class MergeRequestWidgetEntity < IssuableEntity expose :squash expose :target_branch expose :target_project_id - expose :allow_maintainer_to_push + expose :allow_collaboration expose :should_be_rebased?, as: :should_be_rebased expose :ff_only_enabled do |merge_request| diff --git a/app/serializers/pipeline_details_entity.rb b/app/serializers/pipeline_details_entity.rb index 130968a44c1..8ba9cac53c4 100644 --- a/app/serializers/pipeline_details_entity.rb +++ b/app/serializers/pipeline_details_entity.rb @@ -1,6 +1,6 @@ class PipelineDetailsEntity < PipelineEntity expose :details do - expose :legacy_stages, as: :stages, using: StageEntity + expose :ordered_stages, as: :stages, using: StageEntity expose :artifacts, using: BuildArtifactEntity expose :manual_actions, using: BuildActionEntity end diff --git a/app/serializers/pipeline_serializer.rb b/app/serializers/pipeline_serializer.rb index 7181f8a6b04..17a022539bb 100644 --- a/app/serializers/pipeline_serializer.rb +++ b/app/serializers/pipeline_serializer.rb @@ -1,14 +1,11 @@ class PipelineSerializer < BaseSerializer include WithPagination - - InvalidResourceError = Class.new(StandardError) - entity PipelineDetailsEntity def represent(resource, opts = {}) if resource.is_a?(ActiveRecord::Relation) - resource = resource.preload([ + :stages, :retryable_builds, :cancelable_statuses, :trigger_requests, @@ -20,10 +17,14 @@ class PipelineSerializer < BaseSerializer end if paginated? - super(@paginator.paginate(resource), opts) - else - super(resource, opts) + resource = paginator.paginate(resource) end + + if opts.delete(:preload) + resource = Gitlab::Ci::Pipeline::Preloader.preload!(resource) + end + + super(resource, opts) end def represent_status(resource) @@ -36,7 +37,7 @@ class PipelineSerializer < BaseSerializer def represent_stages(resource) return {} unless resource.present? - data = represent(resource, { only: [{ details: [:stages] }] }) + data = represent(resource, { only: [{ details: [:stages] }], preload: true }) data.dig(:details, :stages) || [] end end diff --git a/app/serializers/status_entity.rb b/app/serializers/status_entity.rb index 8e8bda2f9df..47df7f9dcf9 100644 --- a/app/serializers/status_entity.rb +++ b/app/serializers/status_entity.rb @@ -7,16 +7,7 @@ class StatusEntity < Grape::Entity expose :details_path expose :favicon do |status| - dir = - if Gitlab::Utils.to_boolean(ENV['CANARY']) - File.join('ci_favicons', 'canary') - elsif Rails.env.development? - File.join('ci_favicons', 'dev') - else - 'ci_favicons' - end - - ActionController::Base.helpers.image_path(File.join(dir, "#{status.favicon}.ico")) + Gitlab::Favicon.status_overlay(status.favicon) end expose :action, if: -> (status, _) { status.has_action? } do diff --git a/app/services/application_settings/update_service.rb b/app/services/application_settings/update_service.rb index e70445cfb67..7bcb8f49d0d 100644 --- a/app/services/application_settings/update_service.rb +++ b/app/services/application_settings/update_service.rb @@ -1,5 +1,7 @@ module ApplicationSettings class UpdateService < ApplicationSettings::BaseService + attr_reader :params, :application_setting + def execute update_terms(@params.delete(:terms)) diff --git a/app/services/applications/create_service.rb b/app/services/applications/create_service.rb index 35d45f25a71..94a434b95dd 100644 --- a/app/services/applications/create_service.rb +++ b/app/services/applications/create_service.rb @@ -2,11 +2,10 @@ module Applications class CreateService def initialize(current_user, params) @current_user = current_user - @params = params - @ip_address = @params.delete(:ip_address) + @params = params.except(:ip_address) end - def execute(request = nil) + def execute(request) Doorkeeper::Application.create(@params) end end diff --git a/app/services/base_service.rb b/app/services/base_service.rb index 6883ba36c71..3519b7c5e7d 100644 --- a/app/services/base_service.rb +++ b/app/services/base_service.rb @@ -3,7 +3,7 @@ class BaseService attr_accessor :project, :current_user, :params - def initialize(project, user, params = {}) + def initialize(project, user = nil, params = {}) @project, @current_user, @params = project, user, params.dup end diff --git a/app/services/boards/issues/list_service.rb b/app/services/boards/issues/list_service.rb index 5a961ac89e4..b1dbe73cdf7 100644 --- a/app/services/boards/issues/list_service.rb +++ b/app/services/boards/issues/list_service.rb @@ -3,13 +3,18 @@ module Boards class ListService < Boards::BaseService def execute issues = IssuesFinder.new(current_user, filter_params).execute - issues = without_board_labels(issues) unless movable_list? || closed_list? - issues = with_list_label(issues) if movable_list? + issues = filter(issues) issues.order_by_position_and_priority end private + def filter(issues) + issues = without_board_labels(issues) unless list&.movable? || list&.closed? + issues = with_list_label(issues) if list&.label? + issues + end + def board @board ||= parent.boards.find(params[:board_id]) end @@ -20,18 +25,6 @@ module Boards @list = board.lists.find(params[:id]) if params.key?(:id) end - def movable_list? - return @movable_list if defined?(@movable_list) - - @movable_list = list.present? && list.movable? - end - - def closed_list? - return @closed_list if defined?(@closed_list) - - @closed_list = list.present? && list.closed? - end - def filter_params set_parent set_state diff --git a/app/services/boards/issues/move_service.rb b/app/services/boards/issues/move_service.rb index 3ceab209f3f..ee3112c7571 100644 --- a/app/services/boards/issues/move_service.rb +++ b/app/services/boards/issues/move_service.rb @@ -3,7 +3,7 @@ module Boards class MoveService < Boards::BaseService def execute(issue) return false unless can?(current_user, :update_issue, issue) - return false if issue_params.empty? + return false if issue_params(issue).empty? update(issue) end @@ -28,10 +28,10 @@ module Boards end def update(issue) - ::Issues::UpdateService.new(issue.project, current_user, issue_params).execute(issue) + ::Issues::UpdateService.new(issue.project, current_user, issue_params(issue)).execute(issue) end - def issue_params + def issue_params(issue) attrs = {} if move_between_lists? diff --git a/app/services/boards/lists/create_service.rb b/app/services/boards/lists/create_service.rb index 02f1c709374..6fd9885d4f3 100644 --- a/app/services/boards/lists/create_service.rb +++ b/app/services/boards/lists/create_service.rb @@ -1,16 +1,28 @@ module Boards module Lists class CreateService < Boards::BaseService + include Gitlab::Utils::StrongMemoize + def execute(board) List.transaction do - label = available_labels_for(board).find(params[:label_id]) + target = target(board) position = next_position(board) - create_list(board, label, position) + create_list(board, type, target, position) end end private + def type + :label + end + + def target(board) + strong_memoize(:target) do + available_labels_for(board).find(params[:label_id]) + end + end + def available_labels_for(board) options = { include_ancestor_groups: true } @@ -28,8 +40,8 @@ module Boards max_position.nil? ? 0 : max_position.succ end - def create_list(board, label, position) - board.lists.create(label: label, list_type: :label, position: position) + def create_list(board, type, target, position) + board.lists.create(type => target, list_type: type, position: position) end end end diff --git a/app/services/ci/register_job_service.rb b/app/services/ci/register_job_service.rb index 925775aea0b..9bdbb2c0d99 100644 --- a/app/services/ci/register_job_service.rb +++ b/app/services/ci/register_job_service.rb @@ -25,14 +25,12 @@ module Ci valid = true - if Feature.enabled?('ci_job_request_with_tags_matcher') - # pick builds that does not have other tags than runner's one - builds = builds.matches_tag_ids(runner.tags.ids) + # pick builds that does not have other tags than runner's one + builds = builds.matches_tag_ids(runner.tags.ids) - # pick builds that have at least one tag - unless runner.run_untagged? - builds = builds.with_any_tags - end + # pick builds that have at least one tag + unless runner.run_untagged? + builds = builds.with_any_tags end builds.find do |build| diff --git a/app/services/commits/create_service.rb b/app/services/commits/create_service.rb index f96f2931508..4d0578becbe 100644 --- a/app/services/commits/create_service.rb +++ b/app/services/commits/create_service.rb @@ -17,7 +17,7 @@ module Commits new_commit = create_commit! success(result: new_commit) - rescue ValidationError, ChangeError, Gitlab::Git::Index::IndexError, Gitlab::Git::CommitError, Gitlab::Git::HooksService::PreReceiveError => ex + rescue ValidationError, ChangeError, Gitlab::Git::Index::IndexError, Gitlab::Git::CommitError, Gitlab::Git::PreReceiveError => ex error(ex.message) end diff --git a/app/services/concerns/exclusive_lease_guard.rb b/app/services/concerns/exclusive_lease_guard.rb index 30be6accc32..f45436370c1 100644 --- a/app/services/concerns/exclusive_lease_guard.rb +++ b/app/services/concerns/exclusive_lease_guard.rb @@ -47,6 +47,6 @@ module ExclusiveLeaseGuard end def log_error(message, extra_args = {}) - logger.error(message) + Rails.logger.error(message) end end diff --git a/app/services/create_branch_service.rb b/app/services/create_branch_service.rb index 0ba6a0ac6b5..9b1a4d960e2 100644 --- a/app/services/create_branch_service.rb +++ b/app/services/create_branch_service.rb @@ -14,7 +14,7 @@ class CreateBranchService < BaseService else error('Invalid reference name') end - rescue Gitlab::Git::HooksService::PreReceiveError => ex + rescue Gitlab::Git::PreReceiveError => ex error(ex.message) end diff --git a/app/services/delete_branch_service.rb b/app/services/delete_branch_service.rb index 1f059c31944..e1499dcee64 100644 --- a/app/services/delete_branch_service.rb +++ b/app/services/delete_branch_service.rb @@ -16,7 +16,7 @@ class DeleteBranchService < BaseService else error('Failed to remove branch') end - rescue Gitlab::Git::HooksService::PreReceiveError => ex + rescue Gitlab::Git::PreReceiveError => ex error(ex.message) end diff --git a/app/services/lfs/unlock_file_service.rb b/app/services/lfs/unlock_file_service.rb index 7eb89339a92..7e3edf21d54 100644 --- a/app/services/lfs/unlock_file_service.rb +++ b/app/services/lfs/unlock_file_service.rb @@ -24,7 +24,7 @@ module Lfs success(lock: lock, http_status: :ok) elsif forced - error(_('You must have master access to force delete a lock'), 403) + error(_('You must have maintainer access to force delete a lock'), 403) else error(_("%{lock_path} is locked by GitLab User %{lock_user_id}") % { lock_path: lock.path, lock_user_id: lock.user_id }, 403) end diff --git a/app/services/merge_requests/base_service.rb b/app/services/merge_requests/base_service.rb index 231ab76fde4..4c420b38258 100644 --- a/app/services/merge_requests/base_service.rb +++ b/app/services/merge_requests/base_service.rb @@ -38,8 +38,8 @@ module MergeRequests def filter_params(merge_request) super - unless merge_request.can_allow_maintainer_to_push?(current_user) - params.delete(:allow_maintainer_to_push) + unless merge_request.can_allow_collaboration?(current_user) + params.delete(:allow_collaboration) end end diff --git a/app/services/merge_requests/create_from_issue_service.rb b/app/services/merge_requests/create_from_issue_service.rb index cf687b71d16..3407b312700 100644 --- a/app/services/merge_requests/create_from_issue_service.rb +++ b/app/services/merge_requests/create_from_issue_service.rb @@ -41,7 +41,9 @@ module MergeRequests end def ref - @ref || project.default_branch || 'master' + return @ref if project.repository.branch_exists?(@ref) + + project.default_branch || 'master' end def merge_request diff --git a/app/services/merge_requests/ff_merge_service.rb b/app/services/merge_requests/ff_merge_service.rb index ba6853b835a..bffc09c34f0 100644 --- a/app/services/merge_requests/ff_merge_service.rb +++ b/app/services/merge_requests/ff_merge_service.rb @@ -13,7 +13,7 @@ module MergeRequests source, merge_request.target_branch, merge_request: merge_request) - rescue Gitlab::Git::HooksService::PreReceiveError => e + rescue Gitlab::Git::PreReceiveError => e raise MergeError, e.message rescue StandardError => e raise MergeError, "Something went wrong during merge: #{e.message}" diff --git a/app/services/merge_requests/merge_service.rb b/app/services/merge_requests/merge_service.rb index 126da891c78..3d587f97906 100644 --- a/app/services/merge_requests/merge_service.rb +++ b/app/services/merge_requests/merge_service.rb @@ -79,7 +79,7 @@ module MergeRequests message = params[:commit_message] || merge_request.merge_commit_message repository.merge(current_user, source, merge_request, message) - rescue Gitlab::Git::HooksService::PreReceiveError => e + rescue Gitlab::Git::PreReceiveError => e handle_merge_error(log_message: e.message) raise MergeError, 'Something went wrong during merge pre-receive hook' rescue => e diff --git a/app/services/pages_service.rb b/app/services/pages_service.rb deleted file mode 100644 index 446eeb34d3b..00000000000 --- a/app/services/pages_service.rb +++ /dev/null @@ -1,15 +0,0 @@ -class PagesService - attr_reader :data - - def initialize(data) - @data = data - end - - def execute - return unless Settings.pages.enabled - return unless data[:build_name] == 'pages' - return unless data[:build_status] == 'success' - - PagesWorker.perform_async(:deploy, data[:build_id]) - end -end diff --git a/app/services/projects/autocomplete_service.rb b/app/services/projects/autocomplete_service.rb index 346971138b1..aa60661f7f2 100644 --- a/app/services/projects/autocomplete_service.rb +++ b/app/services/projects/autocomplete_service.rb @@ -11,7 +11,7 @@ module Projects order: { due_date: :asc, title: :asc } } - finder_params[:group_ids] = [@project.group.id] if @project.group + finder_params[:group_ids] = @project.group.self_and_ancestors_ids if @project.group MilestonesFinder.new(finder_params).execute.select([:iid, :title]) end diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb index d16ecdb7b9b..a02a9052fb2 100644 --- a/app/services/projects/create_service.rb +++ b/app/services/projects/create_service.rb @@ -46,6 +46,9 @@ module Projects yield(@project) if block_given? + # If the block added errors, don't try to save the project + return @project if @project.errors.any? + @project.creator = current_user if forked_from_project_id @@ -63,6 +66,7 @@ module Projects message = "Unable to save #{e.record.type}: #{e.record.errors.full_messages.join(", ")} " fail(error: message) rescue => e + @project.errors.add(:base, e.message) if @project fail(error: e.message) end @@ -141,7 +145,6 @@ module Projects Rails.logger.error(log_message) if @project - @project.errors.add(:base, message) @project.mark_import_as_failed(message) if @project.persisted? && @project.import? end diff --git a/app/services/projects/destroy_service.rb b/app/services/projects/destroy_service.rb index de0125ed0dd..02769e72229 100644 --- a/app/services/projects/destroy_service.rb +++ b/app/services/projects/destroy_service.rb @@ -135,6 +135,7 @@ module Projects raise_error('Failed to remove some tags in project container registry. Please try again or contact administrator.') end + log_destroy_event trash_repositories! # Rails attempts to load all related records into memory before @@ -148,6 +149,10 @@ module Projects end end + def log_destroy_event + log_info("Attempting to destroy #{project.full_path} (#{project.id})") + end + ## # This method makes sure that we correctly remove registry tags # for legacy image repository (when repository path equals project path). diff --git a/app/services/projects/import_service.rb b/app/services/projects/import_service.rb index 00080717600..1781a01cbd4 100644 --- a/app/services/projects/import_service.rb +++ b/app/services/projects/import_service.rb @@ -17,6 +17,8 @@ module Projects def execute add_repository_to_project + download_lfs_objects + import_data success @@ -37,7 +39,7 @@ module Projects # We should skip the repository for a GitHub import or GitLab project import, # because these importers fetch the project repositories for us. - return if has_importer? && importer_class.try(:imports_repository?) + return if importer_imports_repository? if unknown_url? # In this case, we only want to import issues, not a repository. @@ -73,6 +75,27 @@ module Projects end end + def download_lfs_objects + # In this case, we only want to import issues + return if unknown_url? + + # If it has its own repository importer, it has to implements its own lfs import download + return if importer_imports_repository? + + return unless project.lfs_enabled? + + oids_to_download = Projects::LfsPointers::LfsImportService.new(project).execute + download_service = Projects::LfsPointers::LfsDownloadService.new(project) + + oids_to_download.each do |oid, link| + download_service.execute(oid, link) + end + rescue => e + # Right now, to avoid aborting the importing process, we silently fail + # if any exception raises. + Rails.logger.error("The Lfs import process failed. #{e.message}") + end + def import_data return unless has_importer? @@ -98,5 +121,9 @@ module Projects def unknown_url? project.import_url == Project::UNKNOWN_IMPORT_URL end + + def importer_imports_repository? + has_importer? && importer_class.try(:imports_repository?) + end end end diff --git a/app/services/projects/lfs_pointers/lfs_download_link_list_service.rb b/app/services/projects/lfs_pointers/lfs_download_link_list_service.rb new file mode 100644 index 00000000000..d9fb74b090e --- /dev/null +++ b/app/services/projects/lfs_pointers/lfs_download_link_list_service.rb @@ -0,0 +1,93 @@ +# This service lists the download link from a remote source based on the +# oids provided +module Projects + module LfsPointers + class LfsDownloadLinkListService < BaseService + DOWNLOAD_ACTION = 'download'.freeze + + DownloadLinksError = Class.new(StandardError) + DownloadLinkNotFound = Class.new(StandardError) + + attr_reader :remote_uri + + def initialize(project, remote_uri: nil) + super(project) + + @remote_uri = remote_uri + end + + # This method accepts two parameters: + # - oids: hash of oids to query. The structure is { lfs_file_oid => lfs_file_size } + # + # Returns a hash with the structure { lfs_file_oids => download_link } + def execute(oids) + return {} unless project&.lfs_enabled? && remote_uri && oids.present? + + get_download_links(oids) + end + + private + + def get_download_links(oids) + response = Gitlab::HTTP.post(remote_uri, + body: request_body(oids), + headers: headers) + + raise DownloadLinksError, response.message unless response.success? + + parse_response_links(response['objects']) + end + + def parse_response_links(objects_response) + objects_response.each_with_object({}) do |entry, link_list| + begin + oid = entry['oid'] + link = entry.dig('actions', DOWNLOAD_ACTION, 'href') + + raise DownloadLinkNotFound unless link + + link_list[oid] = add_credentials(link) + rescue DownloadLinkNotFound, URI::InvalidURIError + Rails.logger.error("Link for Lfs Object with oid #{oid} not found or invalid.") + end + end + end + + def request_body(oids) + { + operation: DOWNLOAD_ACTION, + objects: oids.map { |oid, size| { oid: oid, size: size } } + }.to_json + end + + def headers + { + 'Accept' => LfsRequest::CONTENT_TYPE, + 'Content-Type' => LfsRequest::CONTENT_TYPE + }.freeze + end + + def add_credentials(link) + uri = URI.parse(link) + + if should_add_credentials?(uri) + uri.user = remote_uri.user + uri.password = remote_uri.password + end + + uri.to_s + end + + # The download link can be a local url or an object storage url + # If the download link has the some host as the import url then + # we add the same credentials because we may need them + def should_add_credentials?(link_uri) + url_credentials? && link_uri.host == remote_uri.host + end + + def url_credentials? + remote_uri.user.present? || remote_uri.password.present? + end + end + end +end diff --git a/app/services/projects/lfs_pointers/lfs_download_service.rb b/app/services/projects/lfs_pointers/lfs_download_service.rb new file mode 100644 index 00000000000..6ea43561d61 --- /dev/null +++ b/app/services/projects/lfs_pointers/lfs_download_service.rb @@ -0,0 +1,58 @@ +# This service downloads and links lfs objects from a remote URL +module Projects + module LfsPointers + class LfsDownloadService < BaseService + def execute(oid, url) + return unless project&.lfs_enabled? && oid.present? && url.present? + + return if LfsObject.exists?(oid: oid) + + sanitized_uri = Gitlab::UrlSanitizer.new(url) + + with_tmp_file(oid) do |file| + size = download_and_save_file(file, sanitized_uri) + lfs_object = LfsObject.new(oid: oid, size: size, file: file) + + project.all_lfs_objects << lfs_object + end + rescue StandardError => e + Rails.logger.error("LFS file with oid #{oid} could't be downloaded from #{sanitized_uri.sanitized_url}: #{e.message}") + end + + private + + def download_and_save_file(file, sanitized_uri) + IO.copy_stream(open(sanitized_uri.sanitized_url, headers(sanitized_uri)), file) + end + + def headers(sanitized_uri) + {}.tap do |headers| + credentials = sanitized_uri.credentials + + if credentials[:user].present? || credentials[:password].present? + # Using authentication headers in the request + headers[:http_basic_authentication] = [credentials[:user], credentials[:password]] + end + end + end + + def with_tmp_file(oid) + create_tmp_storage_dir + + File.open(File.join(tmp_storage_dir, oid), 'w') { |file| yield file } + end + + def create_tmp_storage_dir + FileUtils.makedirs(tmp_storage_dir) unless Dir.exist?(tmp_storage_dir) + end + + def tmp_storage_dir + @tmp_storage_dir ||= File.join(storage_dir, 'tmp', 'download') + end + + def storage_dir + @storage_dir ||= Gitlab.config.lfs.storage_path + end + end + end +end diff --git a/app/services/projects/lfs_pointers/lfs_import_service.rb b/app/services/projects/lfs_pointers/lfs_import_service.rb new file mode 100644 index 00000000000..b6b0dec142f --- /dev/null +++ b/app/services/projects/lfs_pointers/lfs_import_service.rb @@ -0,0 +1,92 @@ +# This service manages the whole worflow of discovering the Lfs files in a +# repository, linking them to the project and downloading (and linking) the non +# existent ones. +module Projects + module LfsPointers + class LfsImportService < BaseService + include Gitlab::Utils::StrongMemoize + + HEAD_REV = 'HEAD'.freeze + LFS_ENDPOINT_PATTERN = /^\t?url\s*=\s*(.+)$/.freeze + LFS_BATCH_API_ENDPOINT = '/info/lfs/objects/batch'.freeze + + LfsImportError = Class.new(StandardError) + + def execute + return {} unless project&.lfs_enabled? + + if external_lfs_endpoint? + # If the endpoint host is different from the import_url it means + # that the repo is using a third party service for storing the LFS files. + # In this case, we have to disable lfs in the project + disable_lfs! + + return {} + end + + get_download_links + rescue LfsDownloadLinkListService::DownloadLinksError => e + raise LfsImportError, "The LFS objects download list couldn't be imported. Error: #{e.message}" + end + + private + + def external_lfs_endpoint? + lfsconfig_endpoint_uri && lfsconfig_endpoint_uri.host != import_uri.host + end + + def disable_lfs! + project.update(lfs_enabled: false) + end + + def get_download_links + existent_lfs = LfsListService.new(project).execute + linked_oids = LfsLinkService.new(project).execute(existent_lfs.keys) + + # Retrieving those oids not linked and which we need to download + not_linked_lfs = existent_lfs.except(*linked_oids) + + LfsDownloadLinkListService.new(project, remote_uri: current_endpoint_uri).execute(not_linked_lfs) + end + + def lfsconfig_endpoint_uri + strong_memoize(:lfsconfig_endpoint_uri) do + # Retrieveing the blob data from the .lfsconfig file + data = project.repository.lfsconfig_for(HEAD_REV) + # Parsing the data to retrieve the url + parsed_data = data&.match(LFS_ENDPOINT_PATTERN) + + if parsed_data + URI.parse(parsed_data[1]).tap do |endpoint| + endpoint.user ||= import_uri.user + endpoint.password ||= import_uri.password + end + end + end + rescue URI::InvalidURIError + raise LfsImportError, 'Invalid URL in .lfsconfig file' + end + + def import_uri + @import_uri ||= URI.parse(project.import_url) + rescue URI::InvalidURIError + raise LfsImportError, 'Invalid project import URL' + end + + def current_endpoint_uri + (lfsconfig_endpoint_uri || default_endpoint_uri) + end + + # The import url must end with '.git' here we ensure it is + def default_endpoint_uri + @default_endpoint_uri ||= begin + import_uri.dup.tap do |uri| + path = uri.path.gsub(%r(/$), '') + path += '.git' unless path.ends_with?('.git') + uri.path = path + LFS_BATCH_API_ENDPOINT + end + end + end + end + end +end diff --git a/app/services/projects/lfs_pointers/lfs_link_service.rb b/app/services/projects/lfs_pointers/lfs_link_service.rb new file mode 100644 index 00000000000..d20bdf86c58 --- /dev/null +++ b/app/services/projects/lfs_pointers/lfs_link_service.rb @@ -0,0 +1,29 @@ +# Given a list of oids, this services links the existent Lfs Objects to the project +module Projects + module LfsPointers + class LfsLinkService < BaseService + # Accept an array of oids to link + # + # Returns a hash with the same structure with oids linked + def execute(oids) + return {} unless project&.lfs_enabled? + + # Search and link existing LFS Object + link_existing_lfs_objects(oids) + end + + private + + def link_existing_lfs_objects(oids) + existent_lfs_objects = LfsObject.where(oid: oids) + + return [] unless existent_lfs_objects.any? + + not_linked_lfs_objects = existent_lfs_objects.where.not(id: project.all_lfs_objects) + project.all_lfs_objects << not_linked_lfs_objects + + existent_lfs_objects.pluck(:oid) + end + end + end +end diff --git a/app/services/projects/lfs_pointers/lfs_list_service.rb b/app/services/projects/lfs_pointers/lfs_list_service.rb new file mode 100644 index 00000000000..b770982cbc0 --- /dev/null +++ b/app/services/projects/lfs_pointers/lfs_list_service.rb @@ -0,0 +1,19 @@ +# This service list all existent Lfs objects in a repository +module Projects + module LfsPointers + class LfsListService < BaseService + REV = 'HEAD'.freeze + + # Retrieve all lfs blob pointers and returns a hash + # with the structure { lfs_file_oid => lfs_file_size } + def execute + return {} unless project&.lfs_enabled? + + Gitlab::Git::LfsChanges.new(project.repository, REV) + .all_pointers + .map! { |blob| [blob.lfs_oid, blob.lfs_size] } + .to_h + end + end + end +end diff --git a/app/services/projects/update_service.rb b/app/services/projects/update_service.rb index 679f4a9cb62..d8250cd8102 100644 --- a/app/services/projects/update_service.rb +++ b/app/services/projects/update_service.rb @@ -17,6 +17,11 @@ module Projects ensure_wiki_exists if enabling_wiki? + yield if block_given? + + # If the block added errors, don't try to save the project + return validation_failed! if project.errors.any? + if project.update_attributes(params.except(:default_branch)) if project.previous_changes.include?('path') project.rename_repo @@ -28,21 +33,25 @@ module Projects success else - model_errors = project.errors.full_messages.to_sentence - error_message = model_errors.presence || 'Project could not be updated!' - - error(error_message) + validation_failed! end end def run_auto_devops_pipeline? - return false if project.repository.gitlab_ci_yml || !project.auto_devops.previous_changes.include?('enabled') + return false if project.repository.gitlab_ci_yml || !project.auto_devops&.previous_changes&.include?('enabled') project.auto_devops.enabled? || (project.auto_devops.enabled.nil? && Gitlab::CurrentSettings.auto_devops_enabled?) end private + def validation_failed! + model_errors = project.errors.full_messages.to_sentence + error_message = model_errors.presence || 'Project could not be updated!' + + error(error_message) + end + def renaming_project_with_container_registry_tags? new_path = params[:path] @@ -53,8 +62,8 @@ module Projects def changing_default_branch? new_branch = params[:default_branch] - project.repository.exists? && - new_branch && new_branch != project.default_branch + new_branch && project.repository.exists? && + new_branch != project.default_branch end def enabling_wiki? diff --git a/app/services/quick_actions/interpret_service.rb b/app/services/quick_actions/interpret_service.rb index 0215994b1a7..9ac8fdb4cff 100644 --- a/app/services/quick_actions/interpret_service.rb +++ b/app/services/quick_actions/interpret_service.rb @@ -561,6 +561,17 @@ module QuickActions end end + desc 'Make issue confidential.' + explanation do + 'Makes this issue confidential' + end + condition do + issuable.is_a?(Issue) && current_user.can?(:"admin_#{issuable.to_ability_name}", issuable) + end + command :confidential do + @updates[:confidential] = true + end + def extract_users(params) return [] if params.nil? diff --git a/app/services/tags/create_service.rb b/app/services/tags/create_service.rb index cc76d0df3a1..3cc88d77ba1 100644 --- a/app/services/tags/create_service.rb +++ b/app/services/tags/create_service.rb @@ -13,7 +13,7 @@ module Tags new_tag = repository.add_tag(current_user, tag_name, target, message) rescue Gitlab::Git::Repository::TagExistsError return error("Tag #{tag_name} already exists") - rescue Gitlab::Git::HooksService::PreReceiveError => ex + rescue Gitlab::Git::PreReceiveError => ex return error(ex.message) end diff --git a/app/services/tags/destroy_service.rb b/app/services/tags/destroy_service.rb index d3d46064729..b84b061e460 100644 --- a/app/services/tags/destroy_service.rb +++ b/app/services/tags/destroy_service.rb @@ -21,7 +21,7 @@ module Tags else error('Failed to remove tag') end - rescue Gitlab::Git::HooksService::PreReceiveError => ex + rescue Gitlab::Git::PreReceiveError => ex error(ex.message) end diff --git a/app/services/test_hooks/project_service.rb b/app/services/test_hooks/project_service.rb index 01d5d774cd5..65183e84cce 100644 --- a/app/services/test_hooks/project_service.rb +++ b/app/services/test_hooks/project_service.rb @@ -1,11 +1,13 @@ module TestHooks class ProjectService < TestHooks::BaseService - private + attr_writer :project def project @project ||= hook.project end + private + def push_events_data throw(:validation_error, 'Ensure the project has at least one commit.') if project.empty_repo? diff --git a/app/services/validate_new_branch_service.rb b/app/services/validate_new_branch_service.rb index 7d1ed768ee8..643f2ce1481 100644 --- a/app/services/validate_new_branch_service.rb +++ b/app/services/validate_new_branch_service.rb @@ -13,7 +13,7 @@ class ValidateNewBranchService < BaseService end success - rescue Gitlab::Git::HooksService::PreReceiveError => ex + rescue Gitlab::Git::PreReceiveError => ex error(ex.message) end end diff --git a/app/uploaders/favicon_uploader.rb b/app/uploaders/favicon_uploader.rb new file mode 100644 index 00000000000..3639375d474 --- /dev/null +++ b/app/uploaders/favicon_uploader.rb @@ -0,0 +1,13 @@ +class FaviconUploader < AttachmentUploader + EXTENSION_WHITELIST = %w[png ico].freeze + + def extension_whitelist + EXTENSION_WHITELIST + end + + private + + def filename_for_different_format(filename, format) + filename.chomp(File.extname(filename)) + ".#{format}" + end +end diff --git a/app/uploaders/file_uploader.rb b/app/uploaders/file_uploader.rb index 133fdf6684d..36bc0a4575a 100644 --- a/app/uploaders/file_uploader.rb +++ b/app/uploaders/file_uploader.rb @@ -65,10 +65,10 @@ class FileUploader < GitlabUploader SecureRandom.hex end - def upload_paths(filename) + def upload_paths(identifier) [ - File.join(secret, filename), - File.join(base_dir(Store::REMOTE), secret, filename) + File.join(secret, identifier), + File.join(base_dir(Store::REMOTE), secret, identifier) ] end diff --git a/app/uploaders/object_storage.rb b/app/uploaders/object_storage.rb index 5bdca26a584..b8ecfc4ee2b 100644 --- a/app/uploaders/object_storage.rb +++ b/app/uploaders/object_storage.rb @@ -10,8 +10,17 @@ module ObjectStorage UnknownStoreError = Class.new(StandardError) ObjectStorageUnavailable = Class.new(StandardError) - DIRECT_UPLOAD_TIMEOUT = 4.hours - DIRECT_UPLOAD_EXPIRE_OFFSET = 15.minutes + class ExclusiveLeaseTaken < StandardError + def initialize(lease_key) + @lease_key = lease_key + end + + def message + *lease_key_group, _ = *@lease_key.split(":") + "Exclusive lease for #{lease_key_group.join(':')} is already taken." + end + end + TMP_UPLOAD_PATH = 'tmp/uploads'.freeze module Store @@ -24,18 +33,18 @@ module ObjectStorage module RecordsUploads extend ActiveSupport::Concern - def prepended(base) + prepended do |base| raise "#{base} must include ObjectStorage::Concern to use extensions." unless base < Concern - base.include(RecordsUploads::Concern) + base.include(::RecordsUploads::Concern) end def retrieve_from_store!(identifier) - paths = store_dirs.map { |store, path| File.join(path, identifier) } + paths = upload_paths(identifier) unless current_upload_satisfies?(paths, model) # the upload we already have isn't right, find the correct one - self.upload = uploads.find_by(model: model, path: paths) + self.upload = model&.retrieve_upload(identifier, paths) end super @@ -48,7 +57,7 @@ module ObjectStorage end def upload=(upload) - return unless upload + return if upload.nil? self.object_store = upload.store super @@ -64,6 +73,15 @@ module ObjectStorage upload.id) end + def exclusive_lease_key + # For FileUploaders, model may have many uploaders. In that case + # we want to use exclusive key per upload, not per model to allow + # parallel migration + key_object = upload || model + + "object_storage_migrate:#{key_object.class}:#{key_object.id}" + end + private def current_upload_satisfies?(paths, model) @@ -157,9 +175,9 @@ module ObjectStorage model_class.uploader_options.dig(mount_point, :mount_on) || mount_point end - def workhorse_authorize + def workhorse_authorize(has_length:, maximum_size: nil) { - RemoteObject: workhorse_remote_upload_options, + RemoteObject: workhorse_remote_upload_options(has_length: has_length, maximum_size: maximum_size), TempPath: workhorse_local_upload_path }.compact end @@ -168,23 +186,16 @@ module ObjectStorage File.join(self.root, TMP_UPLOAD_PATH) end - def workhorse_remote_upload_options + def workhorse_remote_upload_options(has_length:, maximum_size: nil) return unless self.object_store_enabled? return unless self.direct_upload_enabled? id = [CarrierWave.generate_cache_id, SecureRandom.hex].join('-') upload_path = File.join(TMP_UPLOAD_PATH, id) - connection = ::Fog::Storage.new(self.object_store_credentials) - expire_at = Time.now + DIRECT_UPLOAD_TIMEOUT + DIRECT_UPLOAD_EXPIRE_OFFSET - options = { 'Content-Type' => 'application/octet-stream' } + direct_upload = ObjectStorage::DirectUpload.new(self.object_store_credentials, remote_store_path, upload_path, + has_length: has_length, maximum_size: maximum_size) - { - ID: id, - Timeout: DIRECT_UPLOAD_TIMEOUT, - GetURL: connection.get_object_url(remote_store_path, upload_path, expire_at), - DeleteURL: connection.delete_object_url(remote_store_path, upload_path, expire_at), - StoreURL: connection.put_object_url(remote_store_path, upload_path, expire_at, options) - } + direct_upload.to_hash.merge(ID: id) end end @@ -270,7 +281,7 @@ module ObjectStorage end def delete_migrated_file(migrated_file) - migrated_file.delete if exists? + migrated_file.delete end def exists? @@ -288,6 +299,13 @@ module ObjectStorage } end + # Returns all the possible paths for an upload. + # the `upload.path` is a lookup parameter, and it may change + # depending on the `store` param. + def upload_paths(identifier) + store_dirs.map { |store, path| File.join(path, identifier) } + end + def cache!(new_file = sanitized_file) # We intercept ::UploadedFile which might be stored on remote storage # We use that for "accelerated" uploads, where we store result on remote storage @@ -307,6 +325,10 @@ module ObjectStorage super end + def exclusive_lease_key + "object_storage_migrate:#{model.class}:#{model.id}" + end + private def schedule_background_upload? @@ -373,17 +395,14 @@ module ObjectStorage end end - def exclusive_lease_key - "object_storage_migrate:#{model.class}:#{model.id}" - end - def with_exclusive_lease - uuid = Gitlab::ExclusiveLease.new(exclusive_lease_key, timeout: 1.hour.to_i).try_obtain - raise 'exclusive lease already taken' unless uuid + lease_key = exclusive_lease_key + uuid = Gitlab::ExclusiveLease.new(lease_key, timeout: 1.hour.to_i).try_obtain + raise ExclusiveLeaseTaken.new(lease_key) unless uuid yield uuid ensure - Gitlab::ExclusiveLease.cancel(exclusive_lease_key, uuid) + Gitlab::ExclusiveLease.cancel(lease_key, uuid) end # diff --git a/app/uploaders/records_uploads.rb b/app/uploaders/records_uploads.rb index 89c74a78835..301f4681fcd 100644 --- a/app/uploaders/records_uploads.rb +++ b/app/uploaders/records_uploads.rb @@ -22,7 +22,7 @@ module RecordsUploads Upload.transaction do uploads.where(path: upload_path).delete_all - upload.destroy! if upload + upload.delete if upload self.upload = build_upload.tap(&:save!) end diff --git a/app/uploaders/uploader_helper.rb b/app/uploaders/uploader_helper.rb index fd446d31092..207928b61d0 100644 --- a/app/uploaders/uploader_helper.rb +++ b/app/uploaders/uploader_helper.rb @@ -1,6 +1,6 @@ # Extra methods for uploader module UploaderHelper - IMAGE_EXT = %w[png jpg jpeg gif bmp tiff].freeze + IMAGE_EXT = %w[png jpg jpeg gif bmp tiff ico].freeze # We recommend using the .mp4 format over .mov. Videos in .mov format can # still be used but you really need to make sure they are served with the # proper MIME type video/mp4 and not video/quicktime or your videos won't play diff --git a/app/validators/url_validator.rb b/app/validators/url_validator.rb index 8648c4c75e3..6854fec582e 100644 --- a/app/validators/url_validator.rb +++ b/app/validators/url_validator.rb @@ -18,6 +18,13 @@ # This validator can also block urls pointing to localhost or the local network to # protect against Server-side Request Forgery (SSRF), or check for the right port. # +# The available options are: +# - protocols: Allowed protocols. Default: http and https +# - allow_localhost: Allow urls pointing to localhost. Default: true +# - allow_local_network: Allow urls pointing to private network addresses. Default: true +# - ports: Allowed ports. Default: all. +# - enforce_user: Validate user format. Default: false +# # Example: # class User < ActiveRecord::Base # validates :personal_url, url: { allow_localhost: false, allow_local_network: false} @@ -35,7 +42,7 @@ class UrlValidator < ActiveModel::EachValidator if value.present? value.strip! else - record.errors.add(attribute, "must be a valid URL") + record.errors.add(attribute, 'must be a valid URL') end Gitlab::UrlBlocker.validate!(value, blocker_args) @@ -51,7 +58,8 @@ class UrlValidator < ActiveModel::EachValidator protocols: DEFAULT_PROTOCOLS, ports: [], allow_localhost: true, - allow_local_network: true + allow_local_network: true, + enforce_user: false } end @@ -64,7 +72,7 @@ class UrlValidator < ActiveModel::EachValidator end def blocker_args - current_options.slice(:allow_localhost, :allow_local_network, :protocols, :ports).tap do |args| + current_options.slice(*default_options.keys).tap do |args| if allow_setting_local_requests? args[:allow_localhost] = args[:allow_local_network] = true end diff --git a/app/views/admin/appearances/_form.html.haml b/app/views/admin/appearances/_form.html.haml index 5e08837255f..a0861870ba4 100644 --- a/app/views/admin/appearances/_form.html.haml +++ b/app/views/admin/appearances/_form.html.haml @@ -11,13 +11,32 @@ = image_tag @appearance.header_logo_url, class: 'appearance-light-logo-preview' - if @appearance.persisted? %br - = link_to 'Remove header logo', header_logos_admin_appearances_path, data: { confirm: "Header logo will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-sm remove-logo" + = link_to 'Remove header logo', header_logos_admin_appearances_path, data: { confirm: "Header logo will be removed. Are you sure?"}, method: :delete, class: "btn btn-inverted btn-remove btn-sm remove-logo" %hr = f.hidden_field :header_logo_cache = f.file_field :header_logo, class: "" .hint Maximum file size is 1MB. Pages are optimized for a 28px tall header logo + %fieldset.app_logo + %legend + Favicon: + .form-group.row + = f.label :favicon, 'Favicon', class: 'col-sm-2 col-form-label' + .col-sm-10 + - if @appearance.favicon? + = image_tag @appearance.favicon_url, class: 'appearance-light-logo-preview' + - if @appearance.persisted? + %br + = link_to 'Remove favicon', favicon_admin_appearances_path, data: { confirm: "Favicon will be removed. Are you sure?"}, method: :delete, class: "btn btn-inverted btn-remove btn-sm remove-logo" + %hr + = f.hidden_field :favicon_cache + = f.file_field :favicon, class: '' + .hint + Maximum file size is 1MB. Image size must be 32x32px. Allowed image formats are #{favicon_extension_whitelist}. + %br + Images with incorrect dimensions are not resized automatically, and may result in unexpected behavior. + %fieldset.sign-in %legend Sign in/Sign up pages: @@ -38,7 +57,7 @@ = image_tag @appearance.logo_url, class: 'appearance-logo-preview' - if @appearance.persisted? %br - = link_to 'Remove logo', logo_admin_appearances_path, data: { confirm: "Logo will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-sm remove-logo" + = link_to 'Remove logo', logo_admin_appearances_path, data: { confirm: "Logo will be removed. Are you sure?"}, method: :delete, class: "btn btn-inverted btn-remove btn-sm remove-logo" %hr = f.hidden_field :logo_cache = f.file_field :logo, class: "" diff --git a/app/views/admin/application_settings/_abuse.html.haml b/app/views/admin/application_settings/_abuse.html.haml index 6b9b2a17dd9..91993838fc8 100644 --- a/app/views/admin/application_settings/_abuse.html.haml +++ b/app/views/admin/application_settings/_abuse.html.haml @@ -2,11 +2,10 @@ = form_errors(@application_setting) %fieldset - .form-group.row - = f.label :admin_notification_email, 'Abuse reports notification email', class: 'col-form-label col-sm-2' - .col-sm-10 - = f.text_field :admin_notification_email, class: 'form-control' - .form-text.text-muted - Abuse reports will be sent to this address if it is set. Abuse reports are always available in the admin area. + .form-group + = f.label :admin_notification_email, 'Abuse reports notification email', class: 'label-light' + = f.text_field :admin_notification_email, class: 'form-control' + .form-text.text-muted + Abuse reports will be sent to this address if it is set. Abuse reports are always available in the admin area. = f.submit 'Save changes', class: "btn btn-success" diff --git a/app/views/admin/application_settings/_account_and_limit.html.haml b/app/views/admin/application_settings/_account_and_limit.html.haml index 07f9ea0865b..f40939747f4 100644 --- a/app/views/admin/application_settings/_account_and_limit.html.haml +++ b/app/views/admin/application_settings/_account_and_limit.html.haml @@ -2,38 +2,32 @@ = form_errors(@application_setting) %fieldset - .form-group.row - .offset-sm-2.col-sm-10 - .form-check - = f.check_box :gravatar_enabled, class: 'form-check-input' - = f.label :gravatar_enabled, class: 'form-check-label' do - Gravatar enabled - .form-group.row - = f.label :default_projects_limit, class: 'col-form-label col-sm-2' - .col-sm-10 - = f.number_field :default_projects_limit, class: 'form-control' - .form-group.row - = f.label :max_attachment_size, 'Maximum attachment size (MB)', class: 'col-form-label col-sm-2' - .col-sm-10 - = f.number_field :max_attachment_size, class: 'form-control' - .form-group.row - = f.label :session_expire_delay, 'Session duration (minutes)', class: 'col-form-label col-sm-2' - .col-sm-10 - = f.number_field :session_expire_delay, class: 'form-control' - %span.form-text.text-muted#session_expire_delay_help_block GitLab restart is required to apply changes - .form-group.row - = f.label :user_oauth_applications, 'User OAuth applications', class: 'col-form-label col-sm-2' - .col-sm-10 - .form-check - = f.check_box :user_oauth_applications, class: 'form-check-input' - = f.label :user_oauth_applications, class: 'form-check-label' do - Allow users to register any application to use GitLab as an OAuth provider - .form-group.row - = f.label :user_default_external, 'New users set to external', class: 'col-form-label col-sm-2' - .col-sm-10 - .form-check - = f.check_box :user_default_external, class: 'form-check-input' - = f.label :user_default_external, class: 'form-check-label' do - Newly registered users will by default be external + .form-group + .form-check + = f.check_box :gravatar_enabled, class: 'form-check-input' + = f.label :gravatar_enabled, class: 'form-check-label' do + Gravatar enabled + .form-group + = f.label :default_projects_limit, class: 'label-light' + = f.number_field :default_projects_limit, class: 'form-control' + .form-group + = f.label :max_attachment_size, 'Maximum attachment size (MB)', class: 'label-light' + = f.number_field :max_attachment_size, class: 'form-control' + .form-group + = f.label :session_expire_delay, 'Session duration (minutes)', class: 'label-light' + = f.number_field :session_expire_delay, class: 'form-control' + %span.form-text.text-muted#session_expire_delay_help_block GitLab restart is required to apply changes + .form-group + = f.label :user_oauth_applications, 'User OAuth applications', class: 'label-light' + .form-check + = f.check_box :user_oauth_applications, class: 'form-check-input' + = f.label :user_oauth_applications, class: 'form-check-label' do + Allow users to register any application to use GitLab as an OAuth provider + .form-group + = f.label :user_default_external, 'New users set to external', class: 'label-light' + .form-check + = f.check_box :user_default_external, class: 'form-check-input' + = f.label :user_default_external, class: 'form-check-label' do + Newly registered users will by default be external = f.submit 'Save changes', class: 'btn btn-success' diff --git a/app/views/admin/application_settings/_background_jobs.html.haml b/app/views/admin/application_settings/_background_jobs.html.haml index fc5df02242a..fd8e695ed49 100644 --- a/app/views/admin/application_settings/_background_jobs.html.haml +++ b/app/views/admin/application_settings/_background_jobs.html.haml @@ -6,25 +6,22 @@ These settings require a = link_to 'restart', help_page_path('administration/restart_gitlab') to take effect. - .form-group.row - .offset-sm-2.col-sm-10 - .form-check - = f.check_box :sidekiq_throttling_enabled, class: 'form-check-input' - = f.label :sidekiq_throttling_enabled, class: 'form-check-label' do - Enable Sidekiq Job Throttling - .form-text.text-muted - Limit the amount of resources slow running jobs are assigned. - .form-group.row - = f.label :sidekiq_throttling_queues, 'Sidekiq queues to throttle', class: 'col-form-label col-sm-2' - .col-sm-10 - = f.select :sidekiq_throttling_queues, sidekiq_queue_options_for_select, { include_hidden: false }, multiple: true, class: 'select2 select-wide', data: { field: 'sidekiq_throttling_queues' } + .form-group + .form-check + = f.check_box :sidekiq_throttling_enabled, class: 'form-check-input' + = f.label :sidekiq_throttling_enabled, class: 'form-check-label' do + Enable Sidekiq Job Throttling .form-text.text-muted - Choose which queues you wish to throttle. - .form-group.row - = f.label :sidekiq_throttling_factor, 'Throttling Factor', class: 'col-form-label col-sm-2' - .col-sm-10 - = f.number_field :sidekiq_throttling_factor, class: 'form-control', min: '0.01', max: '0.99', step: '0.01' - .form-text.text-muted - The factor by which the queues should be throttled. A value between 0.0 and 1.0, exclusive. + Limit the amount of resources slow running jobs are assigned. + .form-group + = f.label :sidekiq_throttling_queues, 'Sidekiq queues to throttle', class: 'label-light' + = f.select :sidekiq_throttling_queues, sidekiq_queue_options_for_select, { include_hidden: false }, multiple: true, class: 'select2 select-wide', data: { field: 'sidekiq_throttling_queues' } + .form-text.text-muted + Choose which queues you wish to throttle. + .form-group + = f.label :sidekiq_throttling_factor, 'Throttling Factor', class: 'label-light' + = f.number_field :sidekiq_throttling_factor, class: 'form-control', min: '0.01', max: '0.99', step: '0.01' + .form-text.text-muted + The factor by which the queues should be throttled. A value between 0.0 and 1.0, exclusive. = f.submit 'Save changes', class: "btn btn-success" diff --git a/app/views/admin/application_settings/_ci_cd.html.haml b/app/views/admin/application_settings/_ci_cd.html.haml index 233821818e6..7c16cafe13f 100644 --- a/app/views/admin/application_settings/_ci_cd.html.haml +++ b/app/views/admin/application_settings/_ci_cd.html.haml @@ -2,46 +2,40 @@ = form_errors(@application_setting) %fieldset - .form-group.row - .offset-sm-2.col-sm-10 - .form-check - = f.check_box :auto_devops_enabled, class: 'form-check-input' - = f.label :auto_devops_enabled, class: 'form-check-label' do - Enabled Auto DevOps for projects by default - .form-text.text-muted - It will automatically build, test, and deploy applications based on a predefined CI/CD configuration - = link_to icon('question-circle'), help_page_path('topics/autodevops/index.md') - .form-group.row - = f.label :auto_devops_domain, class: 'col-form-label col-sm-2' - .col-sm-10 - = f.text_field :auto_devops_domain, class: 'form-control', placeholder: 'domain.com' - .form-text.text-muted - = s_("AdminSettings|Specify a domain to use by default for every project's Auto Review Apps and Auto Deploy stages.") - .form-group.row - .offset-sm-2.col-sm-10 - .form-check - = f.check_box :shared_runners_enabled, class: 'form-check-input' - = f.label :shared_runners_enabled, class: 'form-check-label' do - Enable shared runners for new projects - .form-group.row - = f.label :shared_runners_text, class: 'col-form-label col-sm-2' - .col-sm-10 - = f.text_area :shared_runners_text, class: 'form-control', rows: 4 - .form-text.text-muted Markdown enabled - .form-group.row - = f.label :max_artifacts_size, 'Maximum artifacts size (MB)', class: 'col-form-label col-sm-2' - .col-sm-10 - = f.number_field :max_artifacts_size, class: 'form-control' - .form-text.text-muted - Set the maximum file size for each job's artifacts - = link_to icon('question-circle'), help_page_path('user/admin_area/settings/continuous_integration', anchor: 'maximum-artifacts-size') - .form-group.row - = f.label :default_artifacts_expire_in, 'Default artifacts expiration', class: 'col-form-label col-sm-2' - .col-sm-10 - = f.text_field :default_artifacts_expire_in, class: 'form-control' - .form-text.text-muted - Set the default expiration time for each job's artifacts. - 0 for unlimited. - = link_to icon('question-circle'), help_page_path('user/admin_area/settings/continuous_integration', anchor: 'default-artifacts-expiration') + .form-group + .form-check + = f.check_box :auto_devops_enabled, class: 'form-check-input' + = f.label :auto_devops_enabled, class: 'form-check-label' do + Enabled Auto DevOps for projects by default + .form-text.text-muted + It will automatically build, test, and deploy applications based on a predefined CI/CD configuration + = link_to icon('question-circle'), help_page_path('topics/autodevops/index.md') + .form-group + = f.label :auto_devops_domain, class: 'label-light' + = f.text_field :auto_devops_domain, class: 'form-control', placeholder: 'domain.com' + .form-text.text-muted + = s_("AdminSettings|Specify a domain to use by default for every project's Auto Review Apps and Auto Deploy stages.") + .form-group + .form-check + = f.check_box :shared_runners_enabled, class: 'form-check-input' + = f.label :shared_runners_enabled, class: 'form-check-label' do + Enable shared runners for new projects + .form-group + = f.label :shared_runners_text, class: 'label-light' + = f.text_area :shared_runners_text, class: 'form-control', rows: 4 + .form-text.text-muted Markdown enabled + .form-group + = f.label :max_artifacts_size, 'Maximum artifacts size (MB)', class: 'label-light' + = f.number_field :max_artifacts_size, class: 'form-control' + .form-text.text-muted + Set the maximum file size for each job's artifacts + = link_to icon('question-circle'), help_page_path('user/admin_area/settings/continuous_integration', anchor: 'maximum-artifacts-size') + .form-group + = f.label :default_artifacts_expire_in, 'Default artifacts expiration', class: 'label-light' + = f.text_field :default_artifacts_expire_in, class: 'form-control' + .form-text.text-muted + Set the default expiration time for each job's artifacts. + 0 for unlimited. + = link_to icon('question-circle'), help_page_path('user/admin_area/settings/continuous_integration', anchor: 'default-artifacts-expiration') = f.submit 'Save changes', class: "btn btn-success" diff --git a/app/views/admin/application_settings/_email.html.haml b/app/views/admin/application_settings/_email.html.haml index 01be5878a60..99e44ffa741 100644 --- a/app/views/admin/application_settings/_email.html.haml +++ b/app/views/admin/application_settings/_email.html.haml @@ -2,25 +2,23 @@ = form_errors(@application_setting) %fieldset - .form-group.row - .offset-sm-2.col-sm-10 - .form-check - = f.check_box :email_author_in_body, class: 'form-check-input' - = f.label :email_author_in_body, class: 'form-check-label' do - Include author name in notification email body - .form-text.text-muted - Some email servers do not support overriding the email sender name. - Enable this option to include the name of the author of the issue, - merge request or comment in the email body instead. - .form-group.row - .offset-sm-2.col-sm-10 - .form-check - = f.check_box :html_emails_enabled, class: 'form-check-input' - = f.label :html_emails_enabled, class: 'form-check-label' do - Enable HTML emails - .form-text.text-muted - By default GitLab sends emails in HTML and plain text formats so mail - clients can choose what format to use. Disable this option if you only - want to send emails in plain text format. + .form-group + .form-check + = f.check_box :email_author_in_body, class: 'form-check-input' + = f.label :email_author_in_body, class: 'form-check-label' do + Include author name in notification email body + .form-text.text-muted + Some email servers do not support overriding the email sender name. + Enable this option to include the name of the author of the issue, + merge request or comment in the email body instead. + .form-group + .form-check + = f.check_box :html_emails_enabled, class: 'form-check-input' + = f.label :html_emails_enabled, class: 'form-check-label' do + Enable HTML emails + .form-text.text-muted + By default GitLab sends emails in HTML and plain text formats so mail + clients can choose what format to use. Disable this option if you only + want to send emails in plain text format. = f.submit 'Save changes', class: "btn btn-success" diff --git a/app/views/admin/application_settings/_gitaly.html.haml b/app/views/admin/application_settings/_gitaly.html.haml index 859a1c6f45c..0b4001c0824 100644 --- a/app/views/admin/application_settings/_gitaly.html.haml +++ b/app/views/admin/application_settings/_gitaly.html.haml @@ -2,26 +2,23 @@ = form_errors(@application_setting) %fieldset - .form-group.row - = f.label :gitaly_timeout_default, 'Default Timeout Period', class: 'col-form-label col-sm-2' - .col-sm-10 - = f.number_field :gitaly_timeout_default, class: 'form-control' - .form-text.text-muted - Timeout for Gitaly calls from the GitLab application (in seconds). This timeout is not enforced - for git fetch/push operations or Sidekiq jobs. - .form-group.row - = f.label :gitaly_timeout_fast, 'Fast Timeout Period', class: 'col-form-label col-sm-2' - .col-sm-10 - = f.number_field :gitaly_timeout_fast, class: 'form-control' - .form-text.text-muted - Fast operation timeout (in seconds). Some Gitaly operations are expected to be fast. - If they exceed this threshold, there may be a problem with a storage shard and 'failing fast' - can help maintain the stability of the GitLab instance. - .form-group.row - = f.label :gitaly_timeout_medium, 'Medium Timeout Period', class: 'col-form-label col-sm-2' - .col-sm-10 - = f.number_field :gitaly_timeout_medium, class: 'form-control' - .form-text.text-muted - Medium operation timeout (in seconds). This should be a value between the Fast and the Default timeout. + .form-group + = f.label :gitaly_timeout_default, 'Default Timeout Period', class: 'label-light' + = f.number_field :gitaly_timeout_default, class: 'form-control' + .form-text.text-muted + Timeout for Gitaly calls from the GitLab application (in seconds). This timeout is not enforced + for git fetch/push operations or Sidekiq jobs. + .form-group + = f.label :gitaly_timeout_fast, 'Fast Timeout Period', class: 'label-light' + = f.number_field :gitaly_timeout_fast, class: 'form-control' + .form-text.text-muted + Fast operation timeout (in seconds). Some Gitaly operations are expected to be fast. + If they exceed this threshold, there may be a problem with a storage shard and 'failing fast' + can help maintain the stability of the GitLab instance. + .form-group + = f.label :gitaly_timeout_medium, 'Medium Timeout Period', class: 'label-light' + = f.number_field :gitaly_timeout_medium, class: 'form-control' + .form-text.text-muted + Medium operation timeout (in seconds). This should be a value between the Fast and the Default timeout. = f.submit 'Save changes', class: "btn btn-success" diff --git a/app/views/admin/application_settings/_help_page.html.haml b/app/views/admin/application_settings/_help_page.html.haml index 1f6c52d8b1a..1f402fcb786 100644 --- a/app/views/admin/application_settings/_help_page.html.haml +++ b/app/views/admin/application_settings/_help_page.html.haml @@ -2,21 +2,18 @@ = form_errors(@application_setting) %fieldset - .form-group.row - = f.label :help_page_text, class: 'col-form-label col-sm-2' - .col-sm-10 - = f.text_area :help_page_text, class: 'form-control', rows: 4 - .form-text.text-muted Markdown enabled - .form-group.row - .offset-sm-2.col-sm-10 - .form-check - = f.check_box :help_page_hide_commercial_content, class: 'form-check-input' - = f.label :help_page_hide_commercial_content, class: 'form-check-label' do - Hide marketing-related entries from help - .form-group.row - = f.label :help_page_support_url, 'Support page URL', class: 'col-form-label col-sm-2' - .col-sm-10 - = f.text_field :help_page_support_url, class: 'form-control', placeholder: 'http://company.example.com/getting-help', :'aria-describedby' => 'support_help_block' - %span.form-text.text-muted#support_help_block Alternate support URL for help page + .form-group + = f.label :help_page_text, class: 'label-light' + = f.text_area :help_page_text, class: 'form-control', rows: 4 + .form-text.text-muted Markdown enabled + .form-group + .form-check + = f.check_box :help_page_hide_commercial_content, class: 'form-check-input' + = f.label :help_page_hide_commercial_content, class: 'form-check-label' do + Hide marketing-related entries from help + .form-group + = f.label :help_page_support_url, 'Support page URL', class: 'label-light' + = f.text_field :help_page_support_url, class: 'form-control', placeholder: 'http://company.example.com/getting-help', :'aria-describedby' => 'support_help_block' + %span.form-text.text-muted#support_help_block Alternate support URL for help page = f.submit 'Save changes', class: "btn btn-success" diff --git a/app/views/admin/application_settings/_influx.html.haml b/app/views/admin/application_settings/_influx.html.haml index b40a714ed8f..61e8e3199a9 100644 --- a/app/views/admin/application_settings/_influx.html.haml +++ b/app/views/admin/application_settings/_influx.html.haml @@ -8,61 +8,53 @@ = link_to 'restart', help_page_path('administration/restart_gitlab') to take effect. = link_to icon('question-circle'), help_page_path('administration/monitoring/performance/introduction') - .form-group.row - .offset-sm-2.col-sm-10 - .form-check - = f.check_box :metrics_enabled, class: 'form-check-input' - = f.label :metrics_enabled, class: 'form-check-label' do - Enable InfluxDB Metrics - .form-group.row - = f.label :metrics_host, 'InfluxDB host', class: 'col-form-label col-sm-2' - .col-sm-10 - = f.text_field :metrics_host, class: 'form-control', placeholder: 'influxdb.example.com' - .form-group.row - = f.label :metrics_port, 'InfluxDB port', class: 'col-form-label col-sm-2' - .col-sm-10 - = f.text_field :metrics_port, class: 'form-control', placeholder: '8089' - .form-text.text-muted - The UDP port to use for connecting to InfluxDB. InfluxDB requires that - your server configuration specifies a database to store data in when - sending messages to this port, without it metrics data will not be - saved. - .form-group.row - = f.label :metrics_pool_size, 'Connection pool size', class: 'col-form-label col-sm-2' - .col-sm-10 - = f.number_field :metrics_pool_size, class: 'form-control' - .form-text.text-muted - The amount of InfluxDB connections to open. Connections are opened - lazily. Users using multi-threaded application servers should ensure - enough connections are available (at minimum the amount of application - server threads). - .form-group.row - = f.label :metrics_timeout, 'Connection timeout', class: 'col-form-label col-sm-2' - .col-sm-10 - = f.number_field :metrics_timeout, class: 'form-control' - .form-text.text-muted - The amount of seconds after which an InfluxDB connection will time - out. - .form-group.row - = f.label :metrics_method_call_threshold, 'Method Call Threshold (ms)', class: 'col-form-label col-sm-2' - .col-sm-10 - = f.number_field :metrics_method_call_threshold, class: 'form-control' - .form-text.text-muted - A method call is only tracked when it takes longer to complete than - the given amount of milliseconds. - .form-group.row - = f.label :metrics_sample_interval, 'Sampler Interval (sec)', class: 'col-form-label col-sm-2' - .col-sm-10 - = f.number_field :metrics_sample_interval, class: 'form-control' - .form-text.text-muted - The sampling interval in seconds. Sampled data includes memory usage, - retained Ruby objects, file descriptors and so on. - .form-group.row - = f.label :metrics_packet_size, 'Metrics per packet', class: 'col-form-label col-sm-2' - .col-sm-10 - = f.number_field :metrics_packet_size, class: 'form-control' - .form-text.text-muted - The amount of points to store in a single UDP packet. More points - results in fewer but larger UDP packets being sent. + .form-group + .form-check + = f.check_box :metrics_enabled, class: 'form-check-input' + = f.label :metrics_enabled, class: 'form-check-label' do + Enable InfluxDB Metrics + .form-group + = f.label :metrics_host, 'InfluxDB host', class: 'label-light' + = f.text_field :metrics_host, class: 'form-control', placeholder: 'influxdb.example.com' + .form-group + = f.label :metrics_port, 'InfluxDB port', class: 'label-light' + = f.text_field :metrics_port, class: 'form-control', placeholder: '8089' + .form-text.text-muted + The UDP port to use for connecting to InfluxDB. InfluxDB requires that + your server configuration specifies a database to store data in when + sending messages to this port, without it metrics data will not be + saved. + .form-group + = f.label :metrics_pool_size, 'Connection pool size', class: 'label-light' + = f.number_field :metrics_pool_size, class: 'form-control' + .form-text.text-muted + The amount of InfluxDB connections to open. Connections are opened + lazily. Users using multi-threaded application servers should ensure + enough connections are available (at minimum the amount of application + server threads). + .form-group + = f.label :metrics_timeout, 'Connection timeout', class: 'label-light' + = f.number_field :metrics_timeout, class: 'form-control' + .form-text.text-muted + The amount of seconds after which an InfluxDB connection will time + out. + .form-group + = f.label :metrics_method_call_threshold, 'Method Call Threshold (ms)', class: 'label-light' + = f.number_field :metrics_method_call_threshold, class: 'form-control' + .form-text.text-muted + A method call is only tracked when it takes longer to complete than + the given amount of milliseconds. + .form-group + = f.label :metrics_sample_interval, 'Sampler Interval (sec)', class: 'label-light' + = f.number_field :metrics_sample_interval, class: 'form-control' + .form-text.text-muted + The sampling interval in seconds. Sampled data includes memory usage, + retained Ruby objects, file descriptors and so on. + .form-group + = f.label :metrics_packet_size, 'Metrics per packet', class: 'label-light' + = f.number_field :metrics_packet_size, class: 'form-control' + .form-text.text-muted + The amount of points to store in a single UDP packet. More points + results in fewer but larger UDP packets being sent. = f.submit 'Save changes', class: "btn btn-success" diff --git a/app/views/admin/application_settings/_ip_limits.html.haml b/app/views/admin/application_settings/_ip_limits.html.haml index 320dd52ffc2..73d570a5fee 100644 --- a/app/views/admin/application_settings/_ip_limits.html.haml +++ b/app/views/admin/application_settings/_ip_limits.html.haml @@ -2,53 +2,44 @@ = form_errors(@application_setting) %fieldset - .form-group.row - .offset-sm-2.col-sm-10 - .form-check - = f.check_box :throttle_unauthenticated_enabled, class: 'form-check-input' - = f.label :throttle_unauthenticated_enabled, class: 'form-check-label' do - Enable unauthenticated request rate limit - %span.form-text.text-muted - Helps reduce request volume (e.g. from crawlers or abusive bots) - .form-group.row - = f.label :throttle_unauthenticated_requests_per_period, 'Max requests per period per IP', class: 'col-form-label col-sm-2' - .col-sm-10 - = f.number_field :throttle_unauthenticated_requests_per_period, class: 'form-control' - .form-group.row - = f.label :throttle_unauthenticated_period_in_seconds, 'Rate limit period in seconds', class: 'col-form-label col-sm-2' - .col-sm-10 - = f.number_field :throttle_unauthenticated_period_in_seconds, class: 'form-control' - .form-group.row - .offset-sm-2.col-sm-10 - .form-check - = f.check_box :throttle_authenticated_api_enabled, class: 'form-check-input' - = f.label :throttle_authenticated_api_enabled, class: 'form-check-label' do - Enable authenticated API request rate limit - %span.form-text.text-muted - Helps reduce request volume (e.g. from crawlers or abusive bots) - .form-group.row - = f.label :throttle_authenticated_api_requests_per_period, 'Max requests per period per user', class: 'col-form-label col-sm-2' - .col-sm-10 - = f.number_field :throttle_authenticated_api_requests_per_period, class: 'form-control' - .form-group.row - = f.label :throttle_authenticated_api_period_in_seconds, 'Rate limit period in seconds', class: 'col-form-label col-sm-2' - .col-sm-10 - = f.number_field :throttle_authenticated_api_period_in_seconds, class: 'form-control' - .form-group.row - .offset-sm-2.col-sm-10 - .form-check - = f.check_box :throttle_authenticated_web_enabled, class: 'form-check-input' - = f.label :throttle_authenticated_web_enabled, class: 'form-check-label' do - Enable authenticated web request rate limit - %span.form-text.text-muted - Helps reduce request volume (e.g. from crawlers or abusive bots) - .form-group.row - = f.label :throttle_authenticated_web_requests_per_period, 'Max requests per period per user', class: 'col-form-label col-sm-2' - .col-sm-10 - = f.number_field :throttle_authenticated_web_requests_per_period, class: 'form-control' - .form-group.row - = f.label :throttle_authenticated_web_period_in_seconds, 'Rate limit period in seconds', class: 'col-form-label col-sm-2' - .col-sm-10 - = f.number_field :throttle_authenticated_web_period_in_seconds, class: 'form-control' + .form-group + .form-check + = f.check_box :throttle_unauthenticated_enabled, class: 'form-check-input' + = f.label :throttle_unauthenticated_enabled, class: 'form-check-label' do + Enable unauthenticated request rate limit + %span.form-text.text-muted + Helps reduce request volume (e.g. from crawlers or abusive bots) + .form-group + = f.label :throttle_unauthenticated_requests_per_period, 'Max requests per period per IP', class: 'label-light' + = f.number_field :throttle_unauthenticated_requests_per_period, class: 'form-control' + .form-group + = f.label :throttle_unauthenticated_period_in_seconds, 'Rate limit period in seconds', class: 'label-light' + = f.number_field :throttle_unauthenticated_period_in_seconds, class: 'form-control' + .form-group + .form-check + = f.check_box :throttle_authenticated_api_enabled, class: 'form-check-input' + = f.label :throttle_authenticated_api_enabled, class: 'form-check-label' do + Enable authenticated API request rate limit + %span.form-text.text-muted + Helps reduce request volume (e.g. from crawlers or abusive bots) + .form-group + = f.label :throttle_authenticated_api_requests_per_period, 'Max requests per period per user', class: 'label-light' + = f.number_field :throttle_authenticated_api_requests_per_period, class: 'form-control' + .form-group + = f.label :throttle_authenticated_api_period_in_seconds, 'Rate limit period in seconds', class: 'label-light' + = f.number_field :throttle_authenticated_api_period_in_seconds, class: 'form-control' + .form-group + .form-check + = f.check_box :throttle_authenticated_web_enabled, class: 'form-check-input' + = f.label :throttle_authenticated_web_enabled, class: 'form-check-label' do + Enable authenticated web request rate limit + %span.form-text.text-muted + Helps reduce request volume (e.g. from crawlers or abusive bots) + .form-group + = f.label :throttle_authenticated_web_requests_per_period, 'Max requests per period per user', class: 'label-light' + = f.number_field :throttle_authenticated_web_requests_per_period, class: 'form-control' + .form-group + = f.label :throttle_authenticated_web_period_in_seconds, 'Rate limit period in seconds', class: 'label-light' + = f.number_field :throttle_authenticated_web_period_in_seconds, class: 'form-control' = f.submit 'Save changes', class: "btn btn-success" diff --git a/app/views/admin/application_settings/_koding.html.haml b/app/views/admin/application_settings/_koding.html.haml index 341c7641fcc..ae60f68f5fe 100644 --- a/app/views/admin/application_settings/_koding.html.haml +++ b/app/views/admin/application_settings/_koding.html.haml @@ -2,23 +2,21 @@ = form_errors(@application_setting) %fieldset - .form-group.row - .offset-sm-2.col-sm-10 - .form-check - = f.check_box :koding_enabled, class: 'form-check-input' - = f.label :koding_enabled, class: 'form-check-label' do - Enable Koding - .form-text.text-muted - Koding integration has been deprecated since GitLab 10.0. If you disable your Koding integration, you will not be able to enable it again. - .form-group.row - = f.label :koding_url, 'Koding URL', class: 'col-form-label col-sm-2' - .col-sm-10 - = f.text_field :koding_url, class: 'form-control', placeholder: 'http://gitlab.your-koding-instance.com:8090' - .form-text.text-muted - Koding has integration enabled out of the box for the - %strong gitlab - team, and you need to provide that team's URL here. Learn more in the - = succeed "." do - = link_to "Koding administration documentation", help_page_path("administration/integration/koding") + .form-group + .form-check + = f.check_box :koding_enabled, class: 'form-check-input' + = f.label :koding_enabled, class: 'form-check-label' do + Enable Koding + .form-text.text-muted + Koding integration has been deprecated since GitLab 10.0. If you disable your Koding integration, you will not be able to enable it again. + .form-group + = f.label :koding_url, 'Koding URL', class: 'label-light' + = f.text_field :koding_url, class: 'form-control', placeholder: 'http://gitlab.your-koding-instance.com:8090' + .form-text.text-muted + Koding has integration enabled out of the box for the + %strong gitlab + team, and you need to provide that team's URL here. Learn more in the + = succeed "." do + = link_to "Koding administration documentation", help_page_path("administration/integration/koding") = f.submit 'Save changes', class: "btn btn-success" diff --git a/app/views/admin/application_settings/_logging.html.haml b/app/views/admin/application_settings/_logging.html.haml index f5c1e126c70..a6e549cd1f0 100644 --- a/app/views/admin/application_settings/_logging.html.haml +++ b/app/views/admin/application_settings/_logging.html.haml @@ -2,35 +2,31 @@ = form_errors(@application_setting) %fieldset - .form-group.row - .offset-sm-2.col-sm-10 - .form-check - = f.check_box :sentry_enabled, class: 'form-check-input' - = f.label :sentry_enabled, class: 'form-check-label' do - Enable Sentry - .form-text.text-muted - %p This setting requires a restart to take effect. - Sentry is an error reporting and logging tool which is currently not shipped with GitLab, get it here: - %a{ href: 'https://getsentry.com', target: '_blank', rel: 'noopener noreferrer' } https://getsentry.com + .form-group + .form-check + = f.check_box :sentry_enabled, class: 'form-check-input' + = f.label :sentry_enabled, class: 'form-check-label' do + Enable Sentry + .form-text.text-muted + %p This setting requires a restart to take effect. + Sentry is an error reporting and logging tool which is currently not shipped with GitLab, get it here: + %a{ href: 'https://getsentry.com', target: '_blank', rel: 'noopener noreferrer' } https://getsentry.com - .form-group.row - = f.label :sentry_dsn, 'Sentry DSN', class: 'col-form-label col-sm-2' - .col-sm-10 - = f.text_field :sentry_dsn, class: 'form-control' + .form-group + = f.label :sentry_dsn, 'Sentry DSN', class: 'label-light' + = f.text_field :sentry_dsn, class: 'form-control' - .form-group.row - .offset-sm-2.col-sm-10 - .form-check - = f.check_box :clientside_sentry_enabled, class: 'form-check-input' - = f.label :clientside_sentry_enabled, class: 'form-check-label' do - Enable Clientside Sentry - .form-text.text-muted - Sentry can also be used for reporting and logging clientside exceptions. - %a{ href: 'https://sentry.io/for/javascript/', target: '_blank', rel: 'noopener noreferrer' } https://sentry.io/for/javascript/ + .form-group + .form-check + = f.check_box :clientside_sentry_enabled, class: 'form-check-input' + = f.label :clientside_sentry_enabled, class: 'form-check-label' do + Enable Clientside Sentry + .form-text.text-muted + Sentry can also be used for reporting and logging clientside exceptions. + %a{ href: 'https://sentry.io/for/javascript/', target: '_blank', rel: 'noopener noreferrer' } https://sentry.io/for/javascript/ - .form-group.row - = f.label :clientside_sentry_dsn, 'Clientside Sentry DSN', class: 'col-form-label col-sm-2' - .col-sm-10 - = f.text_field :clientside_sentry_dsn, class: 'form-control' + .form-group + = f.label :clientside_sentry_dsn, 'Clientside Sentry DSN', class: 'label-light' + = f.text_field :clientside_sentry_dsn, class: 'form-control' = f.submit 'Save changes', class: "btn btn-success" diff --git a/app/views/admin/application_settings/_outbound.html.haml b/app/views/admin/application_settings/_outbound.html.haml index 5dadb7b814b..e046242bee0 100644 --- a/app/views/admin/application_settings/_outbound.html.haml +++ b/app/views/admin/application_settings/_outbound.html.haml @@ -2,11 +2,10 @@ = form_errors(@application_setting) %fieldset - .form-group.row - .offset-sm-2.col-sm-10 - .form-check - = f.check_box :allow_local_requests_from_hooks_and_services, class: 'form-check-input' - = f.label :allow_local_requests_from_hooks_and_services, class: 'form-check-label' do - Allow requests to the local network from hooks and services + .form-group + .form-check + = f.check_box :allow_local_requests_from_hooks_and_services, class: 'form-check-input' + = f.label :allow_local_requests_from_hooks_and_services, class: 'form-check-label' do + Allow requests to the local network from hooks and services = f.submit 'Save changes', class: "btn btn-success" diff --git a/app/views/admin/application_settings/_pages.html.haml b/app/views/admin/application_settings/_pages.html.haml index f1889c3105f..f168ec62ffd 100644 --- a/app/views/admin/application_settings/_pages.html.haml +++ b/app/views/admin/application_settings/_pages.html.haml @@ -2,21 +2,19 @@ = form_errors(@application_setting) %fieldset - .form-group.row - = f.label :max_pages_size, 'Maximum size of pages (MB)', class: 'col-form-label col-sm-2' - .col-sm-10 - = f.number_field :max_pages_size, class: 'form-control' - .form-text.text-muted 0 for unlimited - .form-group.row - .offset-sm-2.col-sm-10 - .form-check - = f.check_box :pages_domain_verification_enabled, class: 'form-check-input' - = f.label :pages_domain_verification_enabled, class: 'form-check-label' do - Require users to prove ownership of custom domains - .form-text.text-muted - Domain verification is an essential security measure for public GitLab - sites. Users are required to demonstrate they control a domain before - it is enabled - = link_to icon('question-circle'), help_page_path('user/project/pages/getting_started_part_three.md', anchor: 'dns-txt-record') + .form-group + = f.label :max_pages_size, 'Maximum size of pages (MB)', class: 'label-light' + = f.number_field :max_pages_size, class: 'form-control' + .form-text.text-muted 0 for unlimited + .form-group + .form-check + = f.check_box :pages_domain_verification_enabled, class: 'form-check-input' + = f.label :pages_domain_verification_enabled, class: 'form-check-label' do + Require users to prove ownership of custom domains + .form-text.text-muted + Domain verification is an essential security measure for public GitLab + sites. Users are required to demonstrate they control a domain before + it is enabled + = link_to icon('question-circle'), help_page_path('user/project/pages/getting_started_part_three.md', anchor: 'dns-txt-record') = f.submit 'Save changes', class: "btn btn-success" diff --git a/app/views/admin/application_settings/_performance.html.haml b/app/views/admin/application_settings/_performance.html.haml index 57c22ce563f..ffa25af77ed 100644 --- a/app/views/admin/application_settings/_performance.html.haml +++ b/app/views/admin/application_settings/_performance.html.haml @@ -2,18 +2,17 @@ = form_errors(@application_setting) %fieldset - .form-group.row - .offset-sm-2.col-sm-10 - .form-check - = f.check_box :authorized_keys_enabled, class: 'form-check-input' - = f.label :authorized_keys_enabled, class: 'form-check-label' do - Write to "authorized_keys" file - .form-text.text-muted - By default, we write to the "authorized_keys" file to support Git - over SSH without additional configuration. GitLab can be optimized - to authenticate SSH keys via the database file. Only uncheck this - if you have configured your OpenSSH server to use the - AuthorizedKeysCommand. Click on the help icon for more details. - = link_to icon('question-circle'), help_page_path('administration/operations/fast_ssh_key_lookup') + .form-group + .form-check + = f.check_box :authorized_keys_enabled, class: 'form-check-input' + = f.label :authorized_keys_enabled, class: 'form-check-label' do + Write to "authorized_keys" file + .form-text.text-muted + By default, we write to the "authorized_keys" file to support Git + over SSH without additional configuration. GitLab can be optimized + to authenticate SSH keys via the database file. Only uncheck this + if you have configured your OpenSSH server to use the + AuthorizedKeysCommand. Click on the help icon for more details. + = link_to icon('question-circle'), help_page_path('administration/operations/fast_ssh_key_lookup') = f.submit 'Save changes', class: "btn btn-success" diff --git a/app/views/admin/application_settings/_performance_bar.html.haml b/app/views/admin/application_settings/_performance_bar.html.haml index ed4de2234f7..ddbfcc6b77b 100644 --- a/app/views/admin/application_settings/_performance_bar.html.haml +++ b/app/views/admin/application_settings/_performance_bar.html.haml @@ -2,15 +2,13 @@ = form_errors(@application_setting) %fieldset - .form-group.row - .offset-sm-2.col-sm-10 - .form-check - = f.check_box :performance_bar_enabled, class: 'form-check-input' - = f.label :performance_bar_enabled, class: 'form-check-label' do - Enable the Performance Bar - .form-group.row - = f.label :performance_bar_allowed_group_path, 'Allowed group', class: 'col-form-label col-sm-2' - .col-sm-10 - = f.text_field :performance_bar_allowed_group_path, class: 'form-control', placeholder: 'my-org/my-group', value: @application_setting.performance_bar_allowed_group&.full_path + .form-group + .form-check + = f.check_box :performance_bar_enabled, class: 'form-check-input' + = f.label :performance_bar_enabled, class: 'form-check-label' do + Enable the Performance Bar + .form-group + = f.label :performance_bar_allowed_group_path, 'Allowed group', class: 'label-light' + = f.text_field :performance_bar_allowed_group_path, class: 'form-control', placeholder: 'my-org/my-group', value: @application_setting.performance_bar_allowed_group&.full_path = f.submit 'Save changes', class: "btn btn-success" diff --git a/app/views/admin/application_settings/_plantuml.html.haml b/app/views/admin/application_settings/_plantuml.html.haml index e0dc058762e..259f18b3b96 100644 --- a/app/views/admin/application_settings/_plantuml.html.haml +++ b/app/views/admin/application_settings/_plantuml.html.haml @@ -2,19 +2,17 @@ = form_errors(@application_setting) %fieldset - .form-group.row - .offset-sm-2.col-sm-10 - .form-check - = f.check_box :plantuml_enabled, class: 'form-check-input' - = f.label :plantuml_enabled, class: 'form-check-label' do - Enable PlantUML - .form-group.row - = f.label :plantuml_url, 'PlantUML URL', class: 'col-form-label col-sm-2' - .col-sm-10 - = f.text_field :plantuml_url, class: 'form-control', placeholder: 'http://gitlab.your-plantuml-instance.com:8080' - .form-text.text-muted - Allow rendering of - = link_to "PlantUML", "http://plantuml.com" - diagrams in Asciidoc documents using an external PlantUML service. + .form-group + .form-check + = f.check_box :plantuml_enabled, class: 'form-check-input' + = f.label :plantuml_enabled, class: 'form-check-label' do + Enable PlantUML + .form-group + = f.label :plantuml_url, 'PlantUML URL', class: 'label-light' + = f.text_field :plantuml_url, class: 'form-control', placeholder: 'http://gitlab.your-plantuml-instance.com:8080' + .form-text.text-muted + Allow rendering of + = link_to "PlantUML", "http://plantuml.com" + diagrams in Asciidoc documents using an external PlantUML service. = f.submit 'Save changes', class: "btn btn-success" diff --git a/app/views/admin/application_settings/_prometheus.html.haml b/app/views/admin/application_settings/_prometheus.html.haml index d3c3656e96a..ad92b18b2c9 100644 --- a/app/views/admin/application_settings/_prometheus.html.haml +++ b/app/views/admin/application_settings/_prometheus.html.haml @@ -11,18 +11,17 @@ = link_to 'restart', help_page_path('administration/restart_gitlab') to take effect. = link_to icon('question-circle'), help_page_path('administration/monitoring/prometheus/index') - .form-group.row - .offset-sm-2.col-sm-10 - .form-check - = f.check_box :prometheus_metrics_enabled, class: 'form-check-input' - = f.label :prometheus_metrics_enabled, class: 'form-check-label' do - Enable Prometheus Metrics - - unless Gitlab::Metrics.metrics_folder_present? - .form-text.text-muted - %strong.cred WARNING: - Environment variable - %code prometheus_multiproc_dir - does not exist or is not pointing to a valid directory. - = link_to icon('question-circle'), help_page_path('administration/monitoring/prometheus/gitlab_metrics', anchor: 'metrics-shared-directory') + .form-group + .form-check + = f.check_box :prometheus_metrics_enabled, class: 'form-check-input' + = f.label :prometheus_metrics_enabled, class: 'form-check-label' do + Enable Prometheus Metrics + - unless Gitlab::Metrics.metrics_folder_present? + .form-text.text-muted + %strong.cred WARNING: + Environment variable + %code prometheus_multiproc_dir + does not exist or is not pointing to a valid directory. + = link_to icon('question-circle'), help_page_path('administration/monitoring/prometheus/gitlab_metrics', anchor: 'metrics-shared-directory') = f.submit 'Save changes', class: "btn btn-success" diff --git a/app/views/admin/application_settings/_realtime.html.haml b/app/views/admin/application_settings/_realtime.html.haml index 63a592cc2fd..120cf4909b2 100644 --- a/app/views/admin/application_settings/_realtime.html.haml +++ b/app/views/admin/application_settings/_realtime.html.haml @@ -2,18 +2,17 @@ = form_errors(@application_setting) %fieldset - .form-group.row - = f.label :polling_interval_multiplier, 'Polling interval multiplier', class: 'col-form-label col-sm-2' - .col-sm-10 - = f.text_field :polling_interval_multiplier, class: 'form-control' - .form-text.text-muted - Change this value to influence how frequently the GitLab UI polls for updates. - If you set the value to 2 all polling intervals are multiplied - by 2, which means that polling happens half as frequently. - The multiplier can also have a decimal value. - The default value (1) is a reasonable choice for the majority of GitLab - installations. Set to 0 to completely disable polling. - = link_to icon('question-circle'), help_page_path('administration/polling') + .form-group + = f.label :polling_interval_multiplier, 'Polling interval multiplier', class: 'label-light' + = f.text_field :polling_interval_multiplier, class: 'form-control' + .form-text.text-muted + Change this value to influence how frequently the GitLab UI polls for updates. + If you set the value to 2 all polling intervals are multiplied + by 2, which means that polling happens half as frequently. + The multiplier can also have a decimal value. + The default value (1) is a reasonable choice for the majority of GitLab + installations. Set to 0 to completely disable polling. + = link_to icon('question-circle'), help_page_path('administration/polling') = f.submit 'Save changes', class: "btn btn-success" diff --git a/app/views/admin/application_settings/_registry.html.haml b/app/views/admin/application_settings/_registry.html.haml index 8524cbfe4d9..beac70482e5 100644 --- a/app/views/admin/application_settings/_registry.html.haml +++ b/app/views/admin/application_settings/_registry.html.haml @@ -2,9 +2,8 @@ = form_errors(@application_setting) %fieldset - .form-group.row - = f.label :container_registry_token_expire_delay, 'Authorization token duration (minutes)', class: 'col-form-label col-sm-2' - .col-sm-10 - = f.number_field :container_registry_token_expire_delay, class: 'form-control' + .form-group + = f.label :container_registry_token_expire_delay, 'Authorization token duration (minutes)', class: 'label-light' + = f.number_field :container_registry_token_expire_delay, class: 'form-control' = f.submit 'Save changes', class: "btn btn-success" diff --git a/app/views/admin/application_settings/_repository_check.html.haml b/app/views/admin/application_settings/_repository_check.html.haml index 1311f17ecda..57facc380eb 100644 --- a/app/views/admin/application_settings/_repository_check.html.haml +++ b/app/views/admin/application_settings/_repository_check.html.haml @@ -4,59 +4,53 @@ %fieldset .sub-section %h4 Repository checks - .form-group.row - .offset-sm-2.col-sm-10 - .form-check - = f.check_box :repository_checks_enabled, class: 'form-check-input' - = f.label :repository_checks_enabled, class: 'form-check-label' do - Enable Repository Checks - .form-text.text-muted - GitLab will periodically run - %a{ href: 'https://git-scm.com/docs/git-fsck', target: 'blank' } 'git fsck' - in all project and wiki repositories to look for silent disk corruption issues. - .form-group.row - .offset-sm-2.col-sm-10 - = link_to 'Clear all repository checks', clear_repository_check_states_admin_application_settings_path, data: { confirm: 'This will clear repository check states for ALL projects in the database. This cannot be undone. Are you sure?' }, method: :put, class: "btn btn-sm btn-remove" + .form-group + .form-check + = f.check_box :repository_checks_enabled, class: 'form-check-input' + = f.label :repository_checks_enabled, class: 'form-check-label' do + Enable Repository Checks .form-text.text-muted - If you got a lot of false alarms from repository checks you can choose to clear all repository check information from the database. + GitLab will periodically run + %a{ href: 'https://git-scm.com/docs/git-fsck', target: 'blank' } 'git fsck' + in all project and wiki repositories to look for silent disk corruption issues. + .form-group + = link_to 'Clear all repository checks', clear_repository_check_states_admin_application_settings_path, data: { confirm: 'This will clear repository check states for ALL projects in the database. This cannot be undone. Are you sure?' }, method: :put, class: "btn btn-sm btn-remove" + .form-text.text-muted + If you got a lot of false alarms from repository checks you can choose to clear all repository check information from the database. .sub-section %h4 Housekeeping - .form-group.row - .offset-sm-2.col-sm-10 - .form-check - = f.check_box :housekeeping_enabled, class: 'form-check-input' - = f.label :housekeeping_enabled, class: 'form-check-label' do - Enable automatic repository housekeeping (git repack, git gc) - .form-text.text-muted - If you keep automatic housekeeping disabled for a long time Git - repository access on your GitLab server will become slower and your - repositories will use more disk space. We recommend to always leave - this enabled. - .form-check - = f.check_box :housekeeping_bitmaps_enabled, class: 'form-check-input' - = f.label :housekeeping_bitmaps_enabled, class: 'form-check-label' do - Enable Git pack file bitmap creation - .form-text.text-muted - Creating pack file bitmaps makes housekeeping take a little longer but - bitmaps should accelerate 'git clone' performance. - .form-group.row - = f.label :housekeeping_incremental_repack_period, 'Incremental repack period', class: 'col-form-label col-sm-2' - .col-sm-10 - = f.number_field :housekeeping_incremental_repack_period, class: 'form-control' + .form-group + .form-check + = f.check_box :housekeeping_enabled, class: 'form-check-input' + = f.label :housekeeping_enabled, class: 'form-check-label' do + Enable automatic repository housekeeping (git repack, git gc) .form-text.text-muted - Number of Git pushes after which an incremental 'git repack' is run. - .form-group.row - = f.label :housekeeping_full_repack_period, 'Full repack period', class: 'col-form-label col-sm-2' - .col-sm-10 - = f.number_field :housekeeping_full_repack_period, class: 'form-control' + If you keep automatic housekeeping disabled for a long time Git + repository access on your GitLab server will become slower and your + repositories will use more disk space. We recommend to always leave + this enabled. + .form-check + = f.check_box :housekeeping_bitmaps_enabled, class: 'form-check-input' + = f.label :housekeeping_bitmaps_enabled, class: 'form-check-label' do + Enable Git pack file bitmap creation .form-text.text-muted - Number of Git pushes after which a full 'git repack' is run. - .form-group.row - = f.label :housekeeping_gc_period, 'Git GC period', class: 'col-form-label col-sm-2' - .col-sm-10 - = f.number_field :housekeeping_gc_period, class: 'form-control' - .form-text.text-muted - Number of Git pushes after which 'git gc' is run. + Creating pack file bitmaps makes housekeeping take a little longer but + bitmaps should accelerate 'git clone' performance. + .form-group + = f.label :housekeeping_incremental_repack_period, 'Incremental repack period', class: 'label-light' + = f.number_field :housekeeping_incremental_repack_period, class: 'form-control' + .form-text.text-muted + Number of Git pushes after which an incremental 'git repack' is run. + .form-group + = f.label :housekeeping_full_repack_period, 'Full repack period', class: 'label-light' + = f.number_field :housekeeping_full_repack_period, class: 'form-control' + .form-text.text-muted + Number of Git pushes after which a full 'git repack' is run. + .form-group + = f.label :housekeeping_gc_period, 'Git GC period', class: 'label-light' + = f.number_field :housekeeping_gc_period, class: 'form-control' + .form-text.text-muted + Number of Git pushes after which 'git gc' is run. = f.submit 'Save changes', class: "btn btn-success" diff --git a/app/views/admin/application_settings/_repository_mirrors_form.html.haml b/app/views/admin/application_settings/_repository_mirrors_form.html.haml index 187c6c28bb1..beeb5169361 100644 --- a/app/views/admin/application_settings/_repository_mirrors_form.html.haml +++ b/app/views/admin/application_settings/_repository_mirrors_form.html.haml @@ -2,15 +2,14 @@ = form_errors(@application_setting) %fieldset - .form-group.row - = f.label :mirror_available, 'Enable mirror configuration', class: 'control-label col-sm-4' - .col-sm-8 - .form-check - = f.check_box :mirror_available, class: 'form-check-input' - = f.label :mirror_available, class: 'form-check-label' do - Allow mirrors to be setup for projects - %span.form-text.text-muted - If disabled, only admins will be able to setup mirrors in projects. - = link_to icon('question-circle'), help_page_path('workflow/repository_mirroring') + .form-group + = f.label :mirror_available, 'Enable mirror configuration', class: 'label-light' + .form-check + = f.check_box :mirror_available, class: 'form-check-input' + = f.label :mirror_available, class: 'form-check-label' do + Allow mirrors to be setup for projects + %span.form-text.text-muted + If disabled, only admins will be able to setup mirrors in projects. + = link_to icon('question-circle'), help_page_path('workflow/repository_mirroring') = f.submit 'Save changes', class: "btn btn-success" diff --git a/app/views/admin/application_settings/_repository_storage.html.haml b/app/views/admin/application_settings/_repository_storage.html.haml index 89d2c114b22..5a303666353 100644 --- a/app/views/admin/application_settings/_repository_storage.html.haml +++ b/app/views/admin/application_settings/_repository_storage.html.haml @@ -3,56 +3,49 @@ %fieldset .sub-section - .form-group.row - .offset-sm-2.col-sm-10 - .form-check - = f.check_box :hashed_storage_enabled, class: 'form-check-input' - = f.label :hashed_storage_enabled, class: 'form-check-label' do - Create new projects using hashed storage paths - .form-text.text-muted - Enable immutable, hash-based paths and repository names to store repositories on disk. This prevents - repositories from having to be moved or renamed when the Project URL changes and may improve disk I/O performance. - %em (EXPERIMENTAL) - .form-group.row - = f.label :repository_storages, 'Storage paths for new projects', class: 'col-form-label col-sm-2' - .col-sm-10 - = f.select :repository_storages, repository_storages_options_for_select(@application_setting.repository_storages), - {include_hidden: false}, multiple: true, class: 'form-control' + .form-group + .form-check + = f.check_box :hashed_storage_enabled, class: 'form-check-input' + = f.label :hashed_storage_enabled, class: 'form-check-label' do + Create new projects using hashed storage paths .form-text.text-muted - Manage repository storage paths. Learn more in the - = succeed "." do - = link_to "repository storages documentation", help_page_path("administration/repository_storage_paths") + Enable immutable, hash-based paths and repository names to store repositories on disk. This prevents + repositories from having to be moved or renamed when the Project URL changes and may improve disk I/O performance. + %em (EXPERIMENTAL) + .form-group + = f.label :repository_storages, 'Storage paths for new projects', class: 'label-light' + = f.select :repository_storages, repository_storages_options_for_select(@application_setting.repository_storages), + {include_hidden: false}, multiple: true, class: 'form-control' + .form-text.text-muted + Manage repository storage paths. Learn more in the + = succeed "." do + = link_to "repository storages documentation", help_page_path("administration/repository_storage_paths") .sub-section %h4 Circuit breaker - .form-group.row - = f.label :circuitbreaker_check_interval, _('Check interval'), class: 'col-form-label col-sm-2' - .col-sm-10 - = f.number_field :circuitbreaker_check_interval, class: 'form-control' - .form-text.text-muted - = circuitbreaker_check_interval_help_text - .form-group.row - = f.label :circuitbreaker_access_retries, _('Number of access attempts'), class: 'col-form-label col-sm-2' - .col-sm-10 - = f.number_field :circuitbreaker_access_retries, class: 'form-control' - .form-text.text-muted - = circuitbreaker_access_retries_help_text - .form-group.row - = f.label :circuitbreaker_storage_timeout, _('Seconds to wait for a storage access attempt'), class: 'col-form-label col-sm-2' - .col-sm-10 - = f.number_field :circuitbreaker_storage_timeout, class: 'form-control' - .form-text.text-muted - = circuitbreaker_storage_timeout_help_text - .form-group.row - = f.label :circuitbreaker_failure_count_threshold, _('Maximum git storage failures'), class: 'col-form-label col-sm-2' - .col-sm-10 - = f.number_field :circuitbreaker_failure_count_threshold, class: 'form-control' - .form-text.text-muted - = circuitbreaker_failure_count_help_text - .form-group.row - = f.label :circuitbreaker_failure_reset_time, _('Seconds before reseting failure information'), class: 'col-form-label col-sm-2' - .col-sm-10 - = f.number_field :circuitbreaker_failure_reset_time, class: 'form-control' - .form-text.text-muted - = circuitbreaker_failure_reset_time_help_text + .form-group + = f.label :circuitbreaker_check_interval, _('Check interval'), class: 'label-light' + = f.number_field :circuitbreaker_check_interval, class: 'form-control' + .form-text.text-muted + = circuitbreaker_check_interval_help_text + .form-group + = f.label :circuitbreaker_access_retries, _('Number of access attempts'), class: 'label-light' + = f.number_field :circuitbreaker_access_retries, class: 'form-control' + .form-text.text-muted + = circuitbreaker_access_retries_help_text + .form-group + = f.label :circuitbreaker_storage_timeout, _('Seconds to wait for a storage access attempt'), class: 'label-light' + = f.number_field :circuitbreaker_storage_timeout, class: 'form-control' + .form-text.text-muted + = circuitbreaker_storage_timeout_help_text + .form-group + = f.label :circuitbreaker_failure_count_threshold, _('Maximum git storage failures'), class: 'label-light' + = f.number_field :circuitbreaker_failure_count_threshold, class: 'form-control' + .form-text.text-muted + = circuitbreaker_failure_count_help_text + .form-group + = f.label :circuitbreaker_failure_reset_time, _('Seconds before reseting failure information'), class: 'label-light' + = f.number_field :circuitbreaker_failure_reset_time, class: 'form-control' + .form-text.text-muted + = circuitbreaker_failure_reset_time_help_text = f.submit 'Save changes', class: "btn btn-success" diff --git a/app/views/admin/application_settings/_signin.html.haml b/app/views/admin/application_settings/_signin.html.haml index 2ba26158162..69d1a43c511 100644 --- a/app/views/admin/application_settings/_signin.html.haml +++ b/app/views/admin/application_settings/_signin.html.haml @@ -2,59 +2,51 @@ = form_errors(@application_setting) %fieldset - .form-group.row - .offset-sm-2.col-sm-10 - .form-check - = f.check_box :password_authentication_enabled_for_web, class: 'form-check-input' - = f.label :password_authentication_enabled_for_web, class: 'form-check-label' do - Password authentication enabled for web interface - .form-text.text-muted - When disabled, an external authentication provider must be used. - .form-group.row - .offset-sm-2.col-sm-10 - .form-check - = f.check_box :password_authentication_enabled_for_git, class: 'form-check-input' - = f.label :password_authentication_enabled_for_git, class: 'form-check-label' do - Password authentication enabled for Git over HTTP(S) - .form-text.text-muted - When disabled, a Personal Access Token - - if Gitlab::Auth::LDAP::Config.enabled? - or LDAP password - must be used to authenticate. + .form-group + .form-check + = f.check_box :password_authentication_enabled_for_web, class: 'form-check-input' + = f.label :password_authentication_enabled_for_web, class: 'form-check-label' do + Password authentication enabled for web interface + .form-text.text-muted + When disabled, an external authentication provider must be used. + .form-group + .form-check + = f.check_box :password_authentication_enabled_for_git, class: 'form-check-input' + = f.label :password_authentication_enabled_for_git, class: 'form-check-label' do + Password authentication enabled for Git over HTTP(S) + .form-text.text-muted + When disabled, a Personal Access Token + - if Gitlab::Auth::LDAP::Config.enabled? + or LDAP password + must be used to authenticate. - if omniauth_enabled? && button_based_providers.any? - .form-group.row - = f.label :enabled_oauth_sign_in_sources, 'Enabled OAuth sign-in sources', class: 'col-form-label col-sm-2' + .form-group + = f.label :enabled_oauth_sign_in_sources, 'Enabled OAuth sign-in sources', class: 'label-light' = hidden_field_tag 'application_setting[enabled_oauth_sign_in_sources][]' - .col-sm-10 - .btn-group{ data: { toggle: 'buttons' } } - - oauth_providers_checkboxes.each do |source| - = source - .form-group.row - = f.label :two_factor_authentication, 'Two-factor authentication', class: 'col-form-label col-sm-2' - .col-sm-10 - .form-check - = f.check_box :require_two_factor_authentication, class: 'form-check-input' - = f.label :require_two_factor_authentication, class: 'form-check-label' do - Require all users to setup Two-factor authentication - .form-group.row - = f.label :two_factor_authentication, 'Two-factor grace period (hours)', class: 'col-form-label col-sm-2' - .col-sm-10 - = f.number_field :two_factor_grace_period, min: 0, class: 'form-control', placeholder: '0' - .form-text.text-muted Amount of time (in hours) that users are allowed to skip forced configuration of two-factor authentication - .form-group.row - = f.label :home_page_url, 'Home page URL', class: 'col-form-label col-sm-2' - .col-sm-10 - = f.text_field :home_page_url, class: 'form-control', placeholder: 'http://company.example.com', :'aria-describedby' => 'home_help_block' - %span.form-text.text-muted#home_help_block We will redirect non-logged in users to this page - .form-group.row - = f.label :after_sign_out_path, class: 'col-form-label col-sm-2' - .col-sm-10 - = f.text_field :after_sign_out_path, class: 'form-control', placeholder: 'http://company.example.com', :'aria-describedby' => 'after_sign_out_path_help_block' - %span.form-text.text-muted#after_sign_out_path_help_block We will redirect users to this page after they sign out - .form-group.row - = f.label :sign_in_text, class: 'col-form-label col-sm-2' - .col-sm-10 - = f.text_area :sign_in_text, class: 'form-control', rows: 4 - .form-text.text-muted Markdown enabled + .btn-group{ data: { toggle: 'buttons' } } + - oauth_providers_checkboxes.each do |source| + = source + .form-group + = f.label :two_factor_authentication, 'Two-factor authentication', class: 'label-light' + .form-check + = f.check_box :require_two_factor_authentication, class: 'form-check-input' + = f.label :require_two_factor_authentication, class: 'form-check-label' do + Require all users to setup Two-factor authentication + .form-group + = f.label :two_factor_authentication, 'Two-factor grace period (hours)', class: 'label-light' + = f.number_field :two_factor_grace_period, min: 0, class: 'form-control', placeholder: '0' + .form-text.text-muted Amount of time (in hours) that users are allowed to skip forced configuration of two-factor authentication + .form-group + = f.label :home_page_url, 'Home page URL', class: 'label-light' + = f.text_field :home_page_url, class: 'form-control', placeholder: 'http://company.example.com', :'aria-describedby' => 'home_help_block' + %span.form-text.text-muted#home_help_block We will redirect non-logged in users to this page + .form-group + = f.label :after_sign_out_path, class: 'label-light' + = f.text_field :after_sign_out_path, class: 'form-control', placeholder: 'http://company.example.com', :'aria-describedby' => 'after_sign_out_path_help_block' + %span.form-text.text-muted#after_sign_out_path_help_block We will redirect users to this page after they sign out + .form-group + = f.label :sign_in_text, class: 'label-light' + = f.text_area :sign_in_text, class: 'form-control', rows: 4 + .form-text.text-muted Markdown enabled = f.submit 'Save changes', class: "btn btn-success" diff --git a/app/views/admin/application_settings/_signup.html.haml b/app/views/admin/application_settings/_signup.html.haml index 279f96389e9..b9ba9128cc9 100644 --- a/app/views/admin/application_settings/_signup.html.haml +++ b/app/views/admin/application_settings/_signup.html.haml @@ -2,57 +2,49 @@ = form_errors(@application_setting) %fieldset - .form-group.row - .offset-sm-2.col-sm-10 - .form-check - = f.check_box :signup_enabled, class: 'form-check-input' - = f.label :signup_enabled, class: 'form-check-label' do - Sign-up enabled - .form-group.row - .offset-sm-2.col-sm-10 - .form-check - = f.check_box :send_user_confirmation_email, class: 'form-check-input' - = f.label :send_user_confirmation_email, class: 'form-check-label' do - Send confirmation email on sign-up - .form-group.row - = f.label :domain_whitelist, 'Whitelisted domains for sign-ups', class: 'col-form-label col-sm-2' - .col-sm-10 - = f.text_area :domain_whitelist_raw, placeholder: 'domain.com', class: 'form-control', rows: 8 - .form-text.text-muted ONLY users with e-mail addresses that match these domain(s) will be able to sign-up. Wildcards allowed. Use separate lines for multiple entries. Ex: domain.com, *.domain.com - .form-group.row - = f.label :domain_blacklist_enabled, 'Domain Blacklist', class: 'col-form-label col-sm-2' - .col-sm-10 - .form-check - = f.check_box :domain_blacklist_enabled, class: 'form-check-input' - = f.label :domain_blacklist_enabled, class: 'form-check-label' do - Enable domain blacklist for sign ups - .form-group.row - .offset-sm-2.col-sm-10 - .form-check - = radio_button_tag :blacklist_type, :file, class: 'form-check-input' - = label_tag :blacklist_type_file, class: 'form-check-label' do - .option-title - Upload blacklist file - .form-check - = radio_button_tag :blacklist_type, :raw, @application_setting.domain_blacklist.present? || @application_setting.domain_blacklist.blank?, class: 'form-check-input' - = label_tag :blacklist_type_raw, class: 'form-check-label' do - .option-title - Enter blacklist manually - .form-group.row.blacklist-file - = f.label :domain_blacklist_file, 'Blacklist file', class: 'col-form-label col-sm-2' - .col-sm-10 - = f.file_field :domain_blacklist_file, class: 'form-control', accept: '.txt,.conf' - .form-text.text-muted Users with e-mail addresses that match these domain(s) will NOT be able to sign-up. Wildcards allowed. Use separate lines or commas for multiple entries. - .form-group.row.blacklist-raw - = f.label :domain_blacklist, 'Blacklisted domains for sign-ups', class: 'col-form-label col-sm-2' - .col-sm-10 - = f.text_area :domain_blacklist_raw, placeholder: 'domain.com', class: 'form-control', rows: 8 - .form-text.text-muted Users with e-mail addresses that match these domain(s) will NOT be able to sign-up. Wildcards allowed. Use separate lines for multiple entries. Ex: domain.com, *.domain.com + .form-group + .form-check + = f.check_box :signup_enabled, class: 'form-check-input' + = f.label :signup_enabled, class: 'form-check-label' do + Sign-up enabled + .form-group + .form-check + = f.check_box :send_user_confirmation_email, class: 'form-check-input' + = f.label :send_user_confirmation_email, class: 'form-check-label' do + Send confirmation email on sign-up + .form-group + = f.label :domain_whitelist, 'Whitelisted domains for sign-ups', class: 'label-light' + = f.text_area :domain_whitelist_raw, placeholder: 'domain.com', class: 'form-control', rows: 8 + .form-text.text-muted ONLY users with e-mail addresses that match these domain(s) will be able to sign-up. Wildcards allowed. Use separate lines for multiple entries. Ex: domain.com, *.domain.com + .form-group + = f.label :domain_blacklist_enabled, 'Domain Blacklist', class: 'label-light' + .form-check + = f.check_box :domain_blacklist_enabled, class: 'form-check-input' + = f.label :domain_blacklist_enabled, class: 'form-check-label' do + Enable domain blacklist for sign ups + .form-group + .form-check + = radio_button_tag :blacklist_type, :file, false, class: 'form-check-input' + = label_tag :blacklist_type_file, class: 'form-check-label' do + .option-title + Upload blacklist file + .form-check + = radio_button_tag :blacklist_type, :raw, @application_setting.domain_blacklist.present? || @application_setting.domain_blacklist.blank?, class: 'form-check-input' + = label_tag :blacklist_type_raw, class: 'form-check-label' do + .option-title + Enter blacklist manually + .form-group.blacklist-file + = f.label :domain_blacklist_file, 'Blacklist file', class: 'label-light' + = f.file_field :domain_blacklist_file, class: 'form-control', accept: '.txt,.conf' + .form-text.text-muted Users with e-mail addresses that match these domain(s) will NOT be able to sign-up. Wildcards allowed. Use separate lines or commas for multiple entries. + .form-group.blacklist-raw + = f.label :domain_blacklist, 'Blacklisted domains for sign-ups', class: 'label-light' + = f.text_area :domain_blacklist_raw, placeholder: 'domain.com', class: 'form-control', rows: 8 + .form-text.text-muted Users with e-mail addresses that match these domain(s) will NOT be able to sign-up. Wildcards allowed. Use separate lines for multiple entries. Ex: domain.com, *.domain.com - .form-group.row - = f.label :after_sign_up_text, class: 'col-form-label col-sm-2' - .col-sm-10 - = f.text_area :after_sign_up_text, class: 'form-control', rows: 4 - .form-text.text-muted Markdown enabled + .form-group + = f.label :after_sign_up_text, class: 'label-light' + = f.text_area :after_sign_up_text, class: 'form-control', rows: 4 + .form-text.text-muted Markdown enabled = f.submit 'Save changes', class: "btn btn-success" diff --git a/app/views/admin/application_settings/_spam.html.haml b/app/views/admin/application_settings/_spam.html.haml index fb38e4ae922..8f0dce962a9 100644 --- a/app/views/admin/application_settings/_spam.html.haml +++ b/app/views/admin/application_settings/_spam.html.haml @@ -2,64 +2,56 @@ = form_errors(@application_setting) %fieldset - .form-group.row - .offset-sm-2.col-sm-10 - .form-check - = f.check_box :recaptcha_enabled, class: 'form-check-input' - = f.label :recaptcha_enabled, class: 'form-check-label' do - Enable reCAPTCHA - %span.form-text.text-muted#recaptcha_help_block Helps prevent bots from creating accounts - - .form-group.row - = f.label :recaptcha_site_key, 'reCAPTCHA Site Key', class: 'col-form-label col-sm-2' - .col-sm-10 - = f.text_field :recaptcha_site_key, class: 'form-control' - .form-text.text-muted - Generate site and private keys at - %a{ href: 'http://www.google.com/recaptcha', target: 'blank' } http://www.google.com/recaptcha - - .form-group.row - = f.label :recaptcha_private_key, 'reCAPTCHA Private Key', class: 'col-form-label col-sm-2' - .col-sm-10 - = f.text_field :recaptcha_private_key, class: 'form-control' - - .form-group.row - .offset-sm-2.col-sm-10 - .form-check - = f.check_box :akismet_enabled, class: 'form-check-input' - = f.label :akismet_enabled, class: 'form-check-label' do - Enable Akismet - %span.form-text.text-muted#akismet_help_block Helps prevent bots from creating issues - - .form-group.row - = f.label :akismet_api_key, 'Akismet API Key', class: 'col-form-label col-sm-2' - .col-sm-10 - = f.text_field :akismet_api_key, class: 'form-control' - .form-text.text-muted - Generate API key at - %a{ href: 'http://www.akismet.com', target: 'blank' } http://www.akismet.com - - .form-group.row - .offset-sm-2.col-sm-10 - .form-check - = f.check_box :unique_ips_limit_enabled, class: 'form-check-input' - = f.label :unique_ips_limit_enabled, class: 'form-check-label' do - Limit sign in from multiple ips - %span.form-text.text-muted#unique_ip_help_block - Helps prevent malicious users hide their activity - - .form-group.row - = f.label :unique_ips_limit_per_user, 'IPs per user', class: 'col-form-label col-sm-2' - .col-sm-10 - = f.number_field :unique_ips_limit_per_user, class: 'form-control' - .form-text.text-muted - Maximum number of unique IPs per user - - .form-group.row - = f.label :unique_ips_limit_time_window, 'IP expiration time', class: 'col-form-label col-sm-2' - .col-sm-10 - = f.number_field :unique_ips_limit_time_window, class: 'form-control' - .form-text.text-muted - How many seconds an IP will be counted towards the limit + .form-group + .form-check + = f.check_box :recaptcha_enabled, class: 'form-check-input' + = f.label :recaptcha_enabled, class: 'form-check-label' do + Enable reCAPTCHA + %span.form-text.text-muted#recaptcha_help_block Helps prevent bots from creating accounts + + .form-group + = f.label :recaptcha_site_key, 'reCAPTCHA Site Key', class: 'label-light' + = f.text_field :recaptcha_site_key, class: 'form-control' + .form-text.text-muted + Generate site and private keys at + %a{ href: 'http://www.google.com/recaptcha', target: 'blank' } http://www.google.com/recaptcha + + .form-group + = f.label :recaptcha_private_key, 'reCAPTCHA Private Key', class: 'label-light' + = f.text_field :recaptcha_private_key, class: 'form-control' + + .form-group + .form-check + = f.check_box :akismet_enabled, class: 'form-check-input' + = f.label :akismet_enabled, class: 'form-check-label' do + Enable Akismet + %span.form-text.text-muted#akismet_help_block Helps prevent bots from creating issues + + .form-group + = f.label :akismet_api_key, 'Akismet API Key', class: 'label-light' + = f.text_field :akismet_api_key, class: 'form-control' + .form-text.text-muted + Generate API key at + %a{ href: 'http://www.akismet.com', target: 'blank' } http://www.akismet.com + + .form-group + .form-check + = f.check_box :unique_ips_limit_enabled, class: 'form-check-input' + = f.label :unique_ips_limit_enabled, class: 'form-check-label' do + Limit sign in from multiple ips + %span.form-text.text-muted#unique_ip_help_block + Helps prevent malicious users hide their activity + + .form-group + = f.label :unique_ips_limit_per_user, 'IPs per user', class: 'label-light' + = f.number_field :unique_ips_limit_per_user, class: 'form-control' + .form-text.text-muted + Maximum number of unique IPs per user + + .form-group + = f.label :unique_ips_limit_time_window, 'IP expiration time', class: 'label-light' + = f.number_field :unique_ips_limit_time_window, class: 'form-control' + .form-text.text-muted + How many seconds an IP will be counted towards the limit = f.submit 'Save changes', class: "btn btn-success" diff --git a/app/views/admin/application_settings/_terminal.html.haml b/app/views/admin/application_settings/_terminal.html.haml index ae02d07e556..543628ff0ee 100644 --- a/app/views/admin/application_settings/_terminal.html.haml +++ b/app/views/admin/application_settings/_terminal.html.haml @@ -2,12 +2,11 @@ = form_errors(@application_setting) %fieldset - .form-group.row - = f.label :terminal_max_session_time, 'Max session time', class: 'col-form-label col-sm-2' - .col-sm-10 - = f.number_field :terminal_max_session_time, class: 'form-control' - .form-text.text-muted - Maximum time for web terminal websocket connection (in seconds). - 0 for unlimited. + .form-group + = f.label :terminal_max_session_time, 'Max session time', class: 'label-light' + = f.number_field :terminal_max_session_time, class: 'form-control' + .form-text.text-muted + Maximum time for web terminal websocket connection (in seconds). + 0 for unlimited. = f.submit 'Save changes', class: "btn btn-success" diff --git a/app/views/admin/application_settings/_terms.html.haml b/app/views/admin/application_settings/_terms.html.haml index 257565ce193..d3dc8659d1b 100644 --- a/app/views/admin/application_settings/_terms.html.haml +++ b/app/views/admin/application_settings/_terms.html.haml @@ -1,22 +1,19 @@ -= form_for @application_setting, url: admin_application_settings_path do |f| += form_for @application_setting, url: admin_application_settings_path, html: { class: 'fieldset-form' } do |f| = form_errors(@application_setting) %fieldset - .form-group.row - .col-sm-12 - .form-check - = f.check_box :enforce_terms, class: 'form-check-input' - = f.label :enforce_terms, class: 'form-check-label' do - = _("Require all users to accept Terms of Service when they access GitLab.") - .form-text.text-muted - = _("When enabled, users cannot use GitLab until the terms have been accepted.") - .form-group.row - .col-sm-12 - = f.label :terms do - = _("Terms of Service Agreement") - .col-sm-12 - = f.text_area :terms, class: 'form-control', rows: 8 + .form-group + .form-check + = f.check_box :enforce_terms, class: 'form-check-input' + = f.label :enforce_terms, class: 'form-check-label' do + = _("Require all users to accept Terms of Service and Privacy Policy when they access GitLab.") .form-text.text-muted - = _("Markdown enabled") + = _("When enabled, users cannot use GitLab until the terms have been accepted.") + .form-group + = f.label :terms do + = _("Terms of Service Agreement and Privacy Policy") + = f.text_area :terms, class: 'form-control', rows: 8 + .form-text.text-muted + = _("Markdown enabled") = f.submit _("Save changes"), class: "btn btn-success" diff --git a/app/views/admin/application_settings/_usage.html.haml b/app/views/admin/application_settings/_usage.html.haml index c110fd4d60d..49a3ee33a85 100644 --- a/app/views/admin/application_settings/_usage.html.haml +++ b/app/views/admin/application_settings/_usage.html.haml @@ -2,36 +2,34 @@ = form_errors(@application_setting) %fieldset - .form-group.row - .offset-sm-2.col-sm-10 - .form-check - = f.check_box :version_check_enabled, class: 'form-check-input' - = f.label :version_check_enabled, class: 'form-check-label' do - Enable version check - .form-text.text-muted - GitLab will inform you if a new version is available. - = link_to 'Learn more', help_page_path("user/admin_area/settings/usage_statistics", anchor: "version-check") - about what information is shared with GitLab Inc. - .form-group.row - .offset-sm-2.col-sm-10 - - can_be_configured = @application_setting.usage_ping_can_be_configured? - .form-check - = f.check_box :usage_ping_enabled, disabled: !can_be_configured, class: 'form-check-input' - = f.label :usage_ping_enabled, class: 'form-check-label' do - Enable usage ping - .form-text.text-muted - - if can_be_configured - To help improve GitLab and its user experience, GitLab will - periodically collect usage information. - = link_to 'Learn more', help_page_path("user/admin_area/settings/usage_statistics", anchor: "usage-ping") - about what information is shared with GitLab Inc. Visit - = link_to 'Cohorts', admin_cohorts_path(anchor: 'usage-ping') - to see the JSON payload sent. - - else - The usage ping is disabled, and cannot be configured through this - form. For more information, see the documentation on - = succeed '.' do - = link_to 'deactivating the usage ping', help_page_path('user/admin_area/settings/usage_statistics', anchor: 'deactivate-the-usage-ping') + .form-group + .form-check + = f.check_box :version_check_enabled, class: 'form-check-input' + = f.label :version_check_enabled, class: 'form-check-label' do + Enable version check + .form-text.text-muted + GitLab will inform you if a new version is available. + = link_to 'Learn more', help_page_path("user/admin_area/settings/usage_statistics", anchor: "version-check") + about what information is shared with GitLab Inc. + .form-group + - can_be_configured = @application_setting.usage_ping_can_be_configured? + .form-check + = f.check_box :usage_ping_enabled, disabled: !can_be_configured, class: 'form-check-input' + = f.label :usage_ping_enabled, class: 'form-check-label' do + Enable usage ping + .form-text.text-muted + - if can_be_configured + To help improve GitLab and its user experience, GitLab will + periodically collect usage information. + = link_to 'Learn more', help_page_path("user/admin_area/settings/usage_statistics", anchor: "usage-ping") + about what information is shared with GitLab Inc. Visit + = link_to 'Cohorts', admin_cohorts_path(anchor: 'usage-ping') + to see the JSON payload sent. + - else + The usage ping is disabled, and cannot be configured through this + form. For more information, see the documentation on + = succeed '.' do + = link_to 'deactivating the usage ping', help_page_path('user/admin_area/settings/usage_statistics', anchor: 'deactivate-the-usage-ping') = f.submit 'Save changes', class: "btn btn-success" diff --git a/app/views/admin/application_settings/_visibility_and_access.html.haml b/app/views/admin/application_settings/_visibility_and_access.html.haml index 2b4d3bab54d..4cc3e6a7d03 100644 --- a/app/views/admin/application_settings/_visibility_and_access.html.haml +++ b/app/views/admin/application_settings/_visibility_and_access.html.haml @@ -2,66 +2,57 @@ = form_errors(@application_setting) %fieldset - .form-group.row - = f.label :default_branch_protection, class: 'col-form-label col-sm-2' - .col-sm-10 - = f.select :default_branch_protection, options_for_select(Gitlab::Access.protection_options, @application_setting.default_branch_protection), {}, class: 'form-control' - .form-group.row.visibility-level-setting - = f.label :default_project_visibility, class: 'col-form-label col-sm-2' - .col-sm-10 - = render('shared/visibility_radios', model_method: :default_project_visibility, form: f, selected_level: @application_setting.default_project_visibility, form_model: Project.new) - .form-group.row.visibility-level-setting - = f.label :default_snippet_visibility, class: 'col-form-label col-sm-2' - .col-sm-10 - = render('shared/visibility_radios', model_method: :default_snippet_visibility, form: f, selected_level: @application_setting.default_snippet_visibility, form_model: ProjectSnippet.new) - .form-group.row.visibility-level-setting - = f.label :default_group_visibility, class: 'col-form-label col-sm-2' - .col-sm-10 - = render('shared/visibility_radios', model_method: :default_group_visibility, form: f, selected_level: @application_setting.default_group_visibility, form_model: Group.new) - .form-group.row - = f.label :restricted_visibility_levels, class: 'col-form-label col-sm-2' - .col-sm-10 - - checkbox_name = 'application_setting[restricted_visibility_levels][]' - = hidden_field_tag(checkbox_name) - - restricted_level_checkboxes('restricted-visibility-help', checkbox_name).each do |level| - .form-check - = level - %span.form-text.text-muted#restricted-visibility-help - Selected levels cannot be used by non-admin users for groups, projects or snippets. - If the public level is restricted, user profiles are only visible to logged in users. - .form-group.row - = f.label :import_sources, class: 'col-form-label col-sm-2' - .col-sm-10 - = hidden_field_tag 'application_setting[import_sources][]' - - import_sources_checkboxes('import-sources-help').each do |source| - .form-check= source - %span.form-text.text-muted#import-sources-help - Enabled sources for code import during project creation. OmniAuth must be configured for GitHub - = link_to "(?)", help_page_path("integration/github") - , Bitbucket - = link_to "(?)", help_page_path("integration/bitbucket") - and GitLab.com - = link_to "(?)", help_page_path("integration/gitlab") - - .form-group.row - .offset-sm-2.col-sm-10 + .form-group + = f.label :default_branch_protection, class: 'label-light' + = f.select :default_branch_protection, options_for_select(Gitlab::Access.protection_options, @application_setting.default_branch_protection), {}, class: 'form-control' + .form-group.visibility-level-setting + = f.label :default_project_visibility, class: 'label-light' + = render('shared/visibility_radios', model_method: :default_project_visibility, form: f, selected_level: @application_setting.default_project_visibility, form_model: Project.new) + .form-group.visibility-level-setting + = f.label :default_snippet_visibility, class: 'label-light' + = render('shared/visibility_radios', model_method: :default_snippet_visibility, form: f, selected_level: @application_setting.default_snippet_visibility, form_model: ProjectSnippet.new) + .form-group.visibility-level-setting + = f.label :default_group_visibility, class: 'label-light' + = render('shared/visibility_radios', model_method: :default_group_visibility, form: f, selected_level: @application_setting.default_group_visibility, form_model: Group.new) + .form-group + = f.label :restricted_visibility_levels, class: 'label-light' + - checkbox_name = 'application_setting[restricted_visibility_levels][]' + = hidden_field_tag(checkbox_name) + - restricted_level_checkboxes('restricted-visibility-help', checkbox_name, class: 'form-check-input').each do |level| .form-check - = f.check_box :project_export_enabled, class: 'form-check-input' - = f.label :project_export_enabled, class: 'form-check-label' do - Project export enabled + = level + %span.form-text.text-muted#restricted-visibility-help + Selected levels cannot be used by non-admin users for groups, projects or snippets. + If the public level is restricted, user profiles are only visible to logged in users. + .form-group + = f.label :import_sources, class: 'label-light' + = hidden_field_tag 'application_setting[import_sources][]' + - import_sources_checkboxes('import-sources-help', class: 'form-check-input').each do |source| + .form-check= source + %span.form-text.text-muted#import-sources-help + Enabled sources for code import during project creation. OmniAuth must be configured for GitHub + = link_to "(?)", help_page_path("integration/github") + , Bitbucket + = link_to "(?)", help_page_path("integration/bitbucket") + and GitLab.com + = link_to "(?)", help_page_path("integration/gitlab") + + .form-group + .form-check + = f.check_box :project_export_enabled, class: 'form-check-input' + = f.label :project_export_enabled, class: 'form-check-label' do + Project export enabled - .form-group.row - %label.col-form-label.col-sm-2 Enabled Git access protocols - .col-sm-10 - = select(:application_setting, :enabled_git_access_protocol, [['Both SSH and HTTP(S)', nil], ['Only SSH', 'ssh'], ['Only HTTP(S)', 'http']], {}, class: 'form-control') - %span.form-text.text-muted#clone-protocol-help - Allow only the selected protocols to be used for Git access. + .form-group + %label.label-light Enabled Git access protocols + = select(:application_setting, :enabled_git_access_protocol, [['Both SSH and HTTP(S)', nil], ['Only SSH', 'ssh'], ['Only HTTP(S)', 'http']], {}, class: 'form-control') + %span.form-text.text-muted#clone-protocol-help + Allow only the selected protocols to be used for Git access. - ApplicationSetting::SUPPORTED_KEY_TYPES.each do |type| - field_name = :"#{type}_key_restriction" - .form-group.row - = f.label field_name, "#{type.upcase} SSH keys", class: 'col-form-label col-sm-2' - .col-sm-10 - = f.select field_name, key_restriction_options_for_select(type), {}, class: 'form-control' + .form-group + = f.label field_name, "#{type.upcase} SSH keys", class: 'label-light' + = f.select field_name, key_restriction_options_for_select(type), {}, class: 'form-control' = f.submit 'Save changes', class: "btn btn-success" diff --git a/app/views/admin/application_settings/show.html.haml b/app/views/admin/application_settings/show.html.haml index 3f440c76ee0..38607ffca1c 100644 --- a/app/views/admin/application_settings/show.html.haml +++ b/app/views/admin/application_settings/show.html.haml @@ -17,7 +17,7 @@ %section.settings.as-account-limit.no-animate#js-account-settings{ class: ('expanded' if expanded) } .settings-header %h4 - = _('Account and limit settings') + = _('Account and limit') %button.btn.js-settings-toggle{ type: 'button' } = expanded ? _('Collapse') : _('Expand') %p @@ -50,11 +50,11 @@ %section.settings.as-terms.no-animate#js-terms-settings{ class: ('expanded' if expanded) } .settings-header %h4 - = _('Terms of Service') + = _('Terms of Service and Privacy Policy') %button.btn.btn-default.js-settings-toggle{ type: 'button' } = expanded ? _('Collapse') : _('Expand') %p - = _('Include a Terms of Service agreement that all users must accept.') + = _('Include a Terms of Service agreement and Privacy Policy that all users must accept.') .settings-content = render 'terms' @@ -169,7 +169,7 @@ .settings-content = render 'logging' -%section.settings.as-repository-storage.no-animate#js-repository-storage-settings{ class: ('expanded' if expanded) } +%section.qa-repository-storage-settings.settings.as-repository-storage.no-animate#js-repository-storage-settings{ class: ('expanded' if expanded) } .settings-header %h4 = _('Repository storage') @@ -317,7 +317,7 @@ %section.settings.as-mirror.no-animate#js-mirror-settings{ class: ('expanded' if expanded) } .settings-header %h4 - = _('Repository mirror settings') + = _('Repository mirror') %button.btn.js-settings-toggle{ type: 'button' } = expanded ? 'Collapse' : 'Expand' %p diff --git a/app/views/admin/gitaly_servers/index.html.haml b/app/views/admin/gitaly_servers/index.html.haml index d0cf5761726..9b24f411a75 100644 --- a/app/views/admin/gitaly_servers/index.html.haml +++ b/app/views/admin/gitaly_servers/index.html.haml @@ -6,10 +6,10 @@ - if @gitaly_servers.any? .table-holder %table.table.responsive-table - %thead.d-none.d-sm-none.d-md-block + %thead %tr %th= _("Storage") - %th= n_("Gitaly|Address") + %th= s_("Gitaly|Address") %th= _("Server version") %th= _("Git version") %th= _("Up to date") diff --git a/app/views/admin/groups/_form.html.haml b/app/views/admin/groups/_form.html.haml index dc4dccc9e0d..c8008771236 100644 --- a/app/views/admin/groups/_form.html.haml +++ b/app/views/admin/groups/_form.html.haml @@ -2,6 +2,9 @@ = form_errors(@group) = render 'shared/group_form', f: f + = render_if_exists 'shared/repository_size_limit_setting', form: f, type: :group + = render_if_exists 'admin/namespace_plan', f: f + .form-group.row.group-description-holder = f.label :avatar, "Group avatar", class: 'col-form-label col-sm-2' .col-sm-10 @@ -15,6 +18,8 @@ = render 'groups/group_admin_settings', f: f + = render_if_exists 'namespaces/shared_runners_minutes_settings', group: @group, form: f + - if @group.new_record? .form-group.row .offset-sm-2.col-sm-10 @@ -28,3 +33,5 @@ .form-actions = f.submit 'Save changes', class: "btn btn-save" = link_to 'Cancel', admin_group_path(@group), class: "btn btn-cancel" + += render_if_exists 'ldap_group_links/ldap_syncrhonizations', group: @group diff --git a/app/views/admin/groups/_group.html.haml b/app/views/admin/groups/_group.html.haml index e7c70a6f187..3f96988c203 100644 --- a/app/views/admin/groups/_group.html.haml +++ b/app/views/admin/groups/_group.html.haml @@ -1,3 +1,4 @@ +- group = local_assigns.fetch(:group) - css_class = 'no-description' if group.description.blank? %li.group-row{ class: css_class } @@ -8,6 +9,8 @@ %span.badge.badge-pill = storage_counter(group.storage_size) + = render_if_exists 'admin/namespace_plan_badge', namespace: group + %span = icon('bookmark') = number_with_delimiter(group.projects.count) diff --git a/app/views/admin/groups/show.html.haml b/app/views/admin/groups/show.html.haml index 6d75ccd5add..a40f98ad24f 100644 --- a/app/views/admin/groups/show.html.haml +++ b/app/views/admin/groups/show.html.haml @@ -40,6 +40,8 @@ %strong = @group.created_at.to_s(:medium) + = render_if_exists 'admin/namespace_plan_info', namespace: @group + %li %span.light Storage: %strong= storage_counter(@group.storage_size) @@ -58,6 +60,10 @@ = group_lfs_status(@group) = link_to icon('question-circle'), help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs') + = render_if_exists 'namespaces/shared_runner_status', namespace: @group + + = render_if_exists 'ldap_group_links/ldap_group_links_show', group: @group + .card .card-header %h3.card-title @@ -104,7 +110,7 @@ = form_tag admin_group_members_update_path(@group), id: "new_project_member", class: "bulk_import", method: :put do %div - = users_select_tag(:user_ids, multiple: true, email_user: true, scope: :all) + = users_select_tag(:user_ids, multiple: true, email_user: true, skip_ldap: @group.ldap_synced?, scope: :all) .prepend-top-10 = select_tag :access_level, options_for_select(GroupMember.access_level_roles), class: "project-access-select select2" %hr diff --git a/app/views/admin/projects/show.html.haml b/app/views/admin/projects/show.html.haml index 0a22a142858..ccba1c461fc 100644 --- a/app/views/admin/projects/show.html.haml +++ b/app/views/admin/projects/show.html.haml @@ -116,8 +116,8 @@ .card-body = form_for @project, url: transfer_admin_project_path(@project), method: :put do |f| .form-group.row - = f.label :new_namespace_id, "Namespace", class: 'col-form-label col-sm-2' - .col-sm-10 + = f.label :new_namespace_id, "Namespace", class: 'col-form-label col-sm-3' + .col-sm-9 .dropdown = dropdown_toggle('Search for Namespace', { toggle: 'dropdown', field_name: 'new_namespace_id' }, { toggle_class: 'js-namespace-select large' }) .dropdown-menu.dropdown-select @@ -127,7 +127,7 @@ = dropdown_loading .form-group.row - .offset-sm-2.col-sm-10 + .offset-sm-3.col-sm-9 = f.submit 'Transfer', class: 'btn btn-primary' .card.repository-check diff --git a/app/views/admin/runners/index.html.haml b/app/views/admin/runners/index.html.haml index f38aeb151df..8dfd176f1b7 100644 --- a/app/views/admin/runners/index.html.haml +++ b/app/views/admin/runners/index.html.haml @@ -67,7 +67,7 @@ %th Projects %th Jobs %th Tags - %th= link_to 'Last contact', admin_runners_path(params.slice(:search).merge(sort: 'contacted_asc')) + %th= link_to 'Last contact', admin_runners_path(safe_params.slice(:search).merge(sort: 'contacted_asc')) %th - @runners.each do |runner| diff --git a/app/views/admin/services/_form.html.haml b/app/views/admin/services/_form.html.haml index 144dceacbdd..993006e8745 100644 --- a/app/views/admin/services/_form.html.haml +++ b/app/views/admin/services/_form.html.haml @@ -7,5 +7,4 @@ = render 'shared/service_settings', form: form, subject: @service .footer-block.row-content-block - .form-actions - = form.submit 'Save', class: 'btn btn-save' + = form.submit 'Save', class: 'btn btn-save' diff --git a/app/views/admin/users/_access_levels.html.haml b/app/views/admin/users/_access_levels.html.haml index 35a331283ab..04acc5f8423 100644 --- a/app/views/admin/users/_access_levels.html.haml +++ b/app/views/admin/users/_access_levels.html.haml @@ -1,26 +1,26 @@ %fieldset %legend Access - .form-group - = f.label :projects_limit, class: 'col-form-label' + .form-group.row + = f.label :projects_limit, class: 'col-form-label col-sm-2' .col-sm-10= f.number_field :projects_limit, min: 0, max: Gitlab::Database::MAX_INT_VALUE, class: 'form-control' - .form-group - = f.label :can_create_group, class: 'col-form-label' + .form-group.row + = f.label :can_create_group, class: 'col-form-label col-sm-2' .col-sm-10= f.check_box :can_create_group - .form-group - = f.label :access_level, class: 'col-form-label' + .form-group.row + = f.label :access_level, class: 'col-form-label col-sm-2' .col-sm-10 - editing_current_user = (current_user == @user) = f.radio_button :access_level, :regular, disabled: editing_current_user - = label_tag :regular do + = label_tag :regular, class: 'font-weight-bold' do Regular %p.light Regular users have access to their groups and projects = f.radio_button :access_level, :admin, disabled: editing_current_user - = label_tag :admin do + = label_tag :admin, class: 'font-weight-bold' do Admin %p.light Administrators have access to all groups, projects and users and can manage all features in this installation @@ -28,8 +28,8 @@ %p.light You cannot remove your own admin rights. - .form-group - = f.label :external, class: 'col-form-label' + .form-group.row + = f.label :external, class: 'col-form-label col-sm-2' .col-sm-10 = f.check_box :external do External diff --git a/app/views/admin/users/_form.html.haml b/app/views/admin/users/_form.html.haml index 010cb2ac354..58be07fc83e 100644 --- a/app/views/admin/users/_form.html.haml +++ b/app/views/admin/users/_form.html.haml @@ -56,7 +56,7 @@ = f.label :linkedin, class: 'col-form-label col-sm-2' .col-sm-10= f.text_field :linkedin, class: 'form-control' .form-group.row - = f.label :twitter, class: 'col-form-label' + = f.label :twitter, class: 'col-form-label col-sm-2' .col-sm-10= f.text_field :twitter, class: 'form-control' .form-group.row = f.label :website_url, 'Website', class: 'col-form-label col-sm-2' diff --git a/app/views/admin/users/_user.html.haml b/app/views/admin/users/_user.html.haml index 5e176b61d68..b2163ee85fa 100644 --- a/app/views/admin/users/_user.html.haml +++ b/app/views/admin/users/_user.html.haml @@ -38,7 +38,7 @@ %li.divider - if user.can_be_removed? %li - %button.delete-user-button.btn.btn-danger{ data: { toggle: 'modal', + %button.delete-user-button.btn.text-danger{ data: { toggle: 'modal', target: '#delete-user-modal', delete_user_url: admin_user_path(user), block_user_url: block_admin_user_path(user), @@ -47,7 +47,7 @@ = s_('AdminUsers|Delete user') %li - %button.delete-user-button.btn.btn-danger{ data: { toggle: 'modal', + %button.delete-user-button.btn.text-danger{ data: { toggle: 'modal', target: '#delete-user-modal', delete_user_url: admin_user_path(user, hard_delete: true), block_user_url: block_admin_user_path(user), diff --git a/app/views/award_emoji/_awards_block.html.haml b/app/views/award_emoji/_awards_block.html.haml index 4b3c52af16a..8ca9fb4512e 100644 --- a/app/views/award_emoji/_awards_block.html.haml +++ b/app/views/award_emoji/_awards_block.html.haml @@ -12,9 +12,9 @@ - if can?(current_user, :award_emoji, awardable) .award-menu-holder.js-award-holder %button.btn.award-control.has-tooltip.js-add-award{ type: 'button', - 'aria-label': 'Add reaction', + 'aria-label': _('Add reaction'), class: ("js-user-authored" if user_authored), - data: { title: 'Add reaction', placement: "bottom" } } + data: { title: _('Add reaction'), placement: "bottom" } } %span{ class: "award-control-icon award-control-icon-neutral" }= custom_icon('emoji_slightly_smiling_face') %span{ class: "award-control-icon award-control-icon-positive" }= custom_icon('emoji_smiley') %span{ class: "award-control-icon award-control-icon-super-positive" }= custom_icon('emoji_smile') diff --git a/app/views/ci/variables/_variable_row.html.haml b/app/views/ci/variables/_variable_row.html.haml index 571eb28f195..6ee55836dd2 100644 --- a/app/views/ci/variables/_variable_row.html.haml +++ b/app/views/ci/variables/_variable_row.html.haml @@ -43,5 +43,6 @@ %span.toggle-icon = sprite_icon('status_success_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-checked') = sprite_icon('status_failed_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-unchecked') + = render_if_exists 'ci/variables/environment_scope', form_field: form_field, variable: variable %button.js-row-remove-button.ci-variable-row-remove-button{ type: 'button', 'aria-label': s_('CiVariables|Remove variable row') } = icon('minus-circle') diff --git a/app/views/devise/shared/_signup_box.html.haml b/app/views/devise/shared/_signup_box.html.haml index 2554b2688bb..ee7369f54a9 100644 --- a/app/views/devise/shared/_signup_box.html.haml +++ b/app/views/devise/shared/_signup_box.html.haml @@ -22,6 +22,13 @@ = f.label :password = f.password_field :password, class: "form-control bottom", required: true, pattern: ".{#{@minimum_password_length},}", title: "Minimum length is #{@minimum_password_length} characters." %p.gl-field-hint Minimum length is #{@minimum_password_length} characters + - if Gitlab::CurrentSettings.current_application_settings.enforce_terms? + .form-group + = check_box_tag :terms_opt_in, '1', false, required: true + = label_tag :terms_opt_in do + - terms_link = link_to s_("I accept the|Terms of Service and Privacy Policy"), terms_path, target: "_blank" + - accept_terms_label = _("I accept the %{terms_link}") % { terms_link: terms_link } + = accept_terms_label.html_safe %div - if Gitlab::Recaptcha.enabled? = recaptcha_tags diff --git a/app/views/errors/access_denied.html.haml b/app/views/errors/access_denied.html.haml index 227c7884915..8ae29b9d337 100644 --- a/app/views/errors/access_denied.html.haml +++ b/app/views/errors/access_denied.html.haml @@ -1,4 +1,4 @@ -- message = local_assigns.fetch(:message) +- message = local_assigns.fetch(:message, nil) - content_for(:title, 'Access Denied') = image_tag('illustrations/error-403.svg', alt: '403', lazy: false) diff --git a/app/views/explore/projects/_filter.html.haml b/app/views/explore/projects/_filter.html.haml index 845f4046d0d..6abb56ba6d2 100644 --- a/app/views/explore/projects/_filter.html.haml +++ b/app/views/explore/projects/_filter.html.haml @@ -1,6 +1,6 @@ - if current_user .dropdown - %button.dropdown-toggle{ href: '#', "data-toggle" => "dropdown" } + %button.dropdown-toggle{ href: '#', "data-toggle" => "dropdown", 'data-display' => 'static' } = icon('globe') %span.light Visibility: - if params[:visibility_level].present? diff --git a/app/views/groups/_activities.html.haml b/app/views/groups/_activities.html.haml index fd6e7111f38..577c63503a8 100644 --- a/app/views/groups/_activities.html.haml +++ b/app/views/groups/_activities.html.haml @@ -1,4 +1,4 @@ -.nav-block +.nav-block.activities .controls = link_to group_path(@group, rss_url_options), class: 'btn rss-btn has-tooltip' , title: 'Subscribe' do %i.fa.fa-rss diff --git a/app/views/groups/group_members/index.html.haml b/app/views/groups/group_members/index.html.haml index 6a0321bcd2b..13d584f5f1d 100644 --- a/app/views/groups/group_members/index.html.haml +++ b/app/views/groups/group_members/index.html.haml @@ -25,9 +25,10 @@ %span.badge= @members.total_count = form_tag group_group_members_path(@group), method: :get, class: 'form-inline member-search-form flex-project-members-form' do .form-group - = search_field_tag :search, params[:search], { placeholder: 'Find existing members by name', class: 'form-control', spellcheck: false } - %button.member-search-btn{ type: "submit", "aria-label" => "Submit search" } - = icon("search") + .position-relative.append-right-8 + = search_field_tag :search, params[:search], { placeholder: 'Find existing members by name', class: 'form-control', spellcheck: false } + %button.member-search-btn{ type: "submit", "aria-label" => "Submit search" } + = icon("search") - if can_manage_members = render 'shared/members/filter_2fa_dropdown' = render 'shared/members/sort_dropdown' diff --git a/app/views/groups/labels/index.html.haml b/app/views/groups/labels/index.html.haml index ac7e12fcd0b..db7eaff6658 100644 --- a/app/views/groups/labels/index.html.haml +++ b/app/views/groups/labels/index.html.haml @@ -1,21 +1,32 @@ -- page_title 'Labels' - +- @no_container = true +- page_title "Labels" +- can_admin_label = can?(current_user, :admin_label, @group) +- hide_class = '' +- hide = @available_labels.empty? || (params[:page].present? && params[:page] != '1') - issuables = ['issues', 'merge requests'] -.top-area.adjust - .nav-text - = _("Labels can be applied to %{features}. Group labels are available for any project within the group.") % { features: issuables.to_sentence } +- if can_admin_label + - content_for(:header_content) do + .nav-controls + = link_to _('New label'), new_group_label_path(@group), class: "btn btn-new" + +- if @labels.exists? + #promote-label-modal + %div{ class: container_class } + .top-area.adjust + .nav-text + = _('Labels can be applied to %{features}. Group labels are available for any project within the group.') % { features: issuables.to_sentence } - .nav-controls - - if can?(current_user, :admin_label, @group) - = link_to "New label", new_group_label_path(@group), class: "btn btn-new" + .labels-container.prepend-top-5 + .other-labels + - if can_admin_label + %h5{ class: ('hide' if hide) } Labels + %ul.content-list.manage-labels-list.js-other-labels + = render partial: 'shared/label', subject: @group, collection: @labels, as: :label, locals: { use_label_priority: false } + = paginate @labels, theme: 'gitlab' +- else + = render 'shared/empty_states/labels' -.labels - .other-labels - - if @labels.present? - %ul.content-list.manage-labels-list.js-other-labels - = render partial: 'shared/label', subject: @group, collection: @labels, as: :label - = paginate @labels, theme: 'gitlab' - - else - .nothing-here-block - = _("No labels created yet.") +%template#js-badge-item-template + %li.label-link-item.js-priority-badge.inline.prepend-left-10 + .label-badge.label-badge-blue= _('Prioritized label') diff --git a/app/views/groups/settings/ci_cd/show.html.haml b/app/views/groups/settings/ci_cd/show.html.haml index 383d955d71f..647948c7dff 100644 --- a/app/views/groups/settings/ci_cd/show.html.haml +++ b/app/views/groups/settings/ci_cd/show.html.haml @@ -7,7 +7,7 @@ .settings-header %h4 = _('Variables') - = link_to icon('question-circle'), help_page_path('ci/variables/README', anchor: 'secret-variables'), target: '_blank', rel: 'noopener noreferrer' + = link_to icon('question-circle'), help_page_path('ci/variables/README', anchor: 'variables'), target: '_blank', rel: 'noopener noreferrer' %button.btn.btn-default.js-settings-toggle{ type: "button" } = expanded ? _('Collapse') : _('Expand') %p.append-bottom-0 @@ -18,7 +18,7 @@ %section.settings#runners-settings.no-animate{ class: ('expanded' if expanded) } .settings-header %h4 - = _('Runners settings') + = _('Runners') %button.btn.btn-default.js-settings-toggle{ type: "button" } = expanded ? _('Collapse') : _('Expand') %p diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml index 8b0ef3cd87a..5a88619f769 100644 --- a/app/views/groups/show.html.haml +++ b/app/views/groups/show.html.haml @@ -18,7 +18,7 @@ - if can_create_subgroups .btn-group.new-project-subgroup.droplab-dropdown.js-new-project-subgroup{ data: { project_path: new_project_path(namespace_id: @group.id), subgroup_path: new_group_path(parent_id: @group.id) } } %input.btn.btn-success.dropdown-primary.js-new-group-child{ type: "button", value: new_project_label, data: { action: "new-project" } } - %button.btn.btn-success.dropdown-toggle.js-dropdown-toggle{ type: "button", data: { "dropdown-trigger" => "#new-project-or-subgroup-dropdown" } } + %button.btn.btn-success.dropdown-toggle.js-dropdown-toggle{ type: "button", data: { "dropdown-trigger" => "#new-project-or-subgroup-dropdown", 'display' => 'static' } } = icon("caret-down", class: "dropdown-btn-icon") %ul#new-project-or-subgroup-dropdown.dropdown-menu.dropdown-menu-right{ data: { dropdown: true } } %li.droplab-item-selected{ role: "button", data: { value: "new-project", text: new_project_label } } diff --git a/app/views/help/_shortcuts.html.haml b/app/views/help/_shortcuts.html.haml index 29db29235c1..c23fe0b5c49 100644 --- a/app/views/help/_shortcuts.html.haml +++ b/app/views/help/_shortcuts.html.haml @@ -18,71 +18,71 @@ %th Global Shortcuts %tr %td.shortcut - .key s + %kbd s %td Focus Search %tr %td.shortcut - .key f + %kbd f %td Focus Filter - if performance_bar_enabled? %tr %td.shortcut - .key p b + %kbd p b %td Show/hide the Performance Bar %tr %td.shortcut - .key ? + %kbd ? %td Show/hide this dialog %tr %td.shortcut - if browser.platform.mac? - .key ⌘ shift p + %kbd ⌘ shift p - else - .key ctrl shift p + %kbd ctrl shift p %td Toggle Markdown preview %tr %td.shortcut - .key + %kbd %i.fa.fa-arrow-up %td Edit last comment (when focused on an empty textarea) %tr %td.shortcut - .key shift t + %kbd shift t %td Go to todos %tr %td.shortcut - .key shift a + %kbd shift a %td Go to the activity feed %tr %td.shortcut - .key shift p + %kbd shift p %td Go to projects %tr %td.shortcut - .key shift i + %kbd shift i %td Go to issues %tr %td.shortcut - .key shift m + %kbd shift m %td Go to merge requests %tr %td.shortcut - .key shift g + %kbd shift g %td Go to groups %tr %td.shortcut - .key shift l + %kbd shift l %td Go to milestones %tr %td.shortcut - .key shift s + %kbd shift s %td Go to snippets %tbody @@ -91,21 +91,21 @@ %th Finding Project File %tr %td.shortcut - .key + %kbd %i.fa.fa-arrow-up %td Move selection up %tr %td.shortcut - .key + %kbd %i.fa.fa-arrow-down %td Move selection down %tr %td.shortcut - .key enter + %kbd enter %td Open Selection %tr %td.shortcut - .key esc + %kbd esc %td Go back .col-lg-4 %table.shortcut-mappings @@ -115,95 +115,95 @@ %th Project %tr %td.shortcut - .key g - .key p + %kbd g + %kbd p %td Go to the project's overview page %tr %td.shortcut - .key g - .key v + %kbd g + %kbd v %td Go to the project's activity feed %tr %td.shortcut - .key g - .key f + %kbd g + %kbd f %td Go to files %tr %td.shortcut - .key g - .key c + %kbd g + %kbd c %td Go to commits %tr %td.shortcut - .key g - .key j + %kbd g + %kbd j %td Go to jobs %tr %td.shortcut - .key g - .key n + %kbd g + %kbd n %td Go to network graph %tr %td.shortcut - .key g - .key d + %kbd g + %kbd d %td Go to repository charts %tr %td.shortcut - .key g - .key i + %kbd g + %kbd i %td Go to issues %tr %td.shortcut - .key g - .key b + %kbd g + %kbd b %td Go to issue boards %tr %td.shortcut - .key g - .key m + %kbd g + %kbd m %td Go to merge requests %tr %td.shortcut - .key g - .key e + %kbd g + %kbd e %td Go to environments %tr %td.shortcut - .key g - .key k + %kbd g + %kbd k %td Go to kubernetes %tr %td.shortcut - .key g - .key s + %kbd g + %kbd s %td Go to snippets %tr %td.shortcut - .key g - .key w + %kbd g + %kbd w %td Go to wiki %tr %td.shortcut - .key t + %kbd t %td Go to finding file %tr %td.shortcut - .key i + %kbd i %td New issue %tbody @@ -212,17 +212,17 @@ %th Project Files browsing %tr %td.shortcut - .key + %kbd %i.fa.fa-arrow-up %td Move selection up %tr %td.shortcut - .key + %kbd %i.fa.fa-arrow-down %td Move selection down %tr %td.shortcut - .key enter + %kbd enter %td Open Selection %tbody %tr @@ -230,7 +230,7 @@ %th Project File %tr %td.shortcut - .key y + %kbd y %td Go to file permalink %tbody %tr @@ -239,115 +239,115 @@ %tr %td.shortcut - if browser.platform.mac? - .key ⌘ p + %kbd ⌘ p - else - .key ctrl p + %kbd ctrl p %td Go to file .col-lg-4 %table.shortcut-mappings - %tbody.hidden-shortcut.network{ style: 'display:none' } + %tbody.hidden-shortcut{ style: 'display:none' } %tr %th %th Network Graph %tr %td.shortcut - .key + %kbd %i.fa.fa-arrow-left \/ - .key h + %kbd h %td Scroll left %tr %td.shortcut - .key + %kbd %i.fa.fa-arrow-right \/ - .key l + %kbd l %td Scroll right %tr %td.shortcut - .key + %kbd %i.fa.fa-arrow-up \/ - .key k + %kbd k %td Scroll up %tr %td.shortcut - .key + %kbd %i.fa.fa-arrow-down \/ - .key j + %kbd j %td Scroll down %tr %td.shortcut - .key + %kbd shift %i.fa.fa-arrow-up \/ - .key + %kbd shift k %td Scroll to top %tr %td.shortcut - .key + %kbd shift %i.fa.fa-arrow-down \/ - .key + %kbd shift j %td Scroll to bottom - %tbody.hidden-shortcut.issues{ style: 'display:none' } + %tbody.hidden-shortcut{ style: 'display:none' } %tr %th %th Issues %tr %td.shortcut - .key a + %kbd a %td Change assignee %tr %td.shortcut - .key m + %kbd m %td Change milestone %tr %td.shortcut - .key r + %kbd r %td Reply (quoting selected text) %tr %td.shortcut - .key e + %kbd e %td Edit issue %tr %td.shortcut - .key l + %kbd l %td Change Label - %tbody.hidden-shortcut.merge_requests{ style: 'display:none' } + %tbody.hidden-shortcut{ style: 'display:none' } %tr %th %th Merge Requests %tr %td.shortcut - .key a + %kbd a %td Change assignee %tr %td.shortcut - .key m + %kbd m %td Change milestone %tr %td.shortcut - .key r + %kbd r %td Reply (quoting selected text) %tr %td.shortcut - .key e + %kbd e %td Edit merge request %tr %td.shortcut - .key l + %kbd l %td Change Label - %tbody.hidden-shortcut.wiki{ style: 'display:none' } + %tbody.hidden-shortcut{ style: 'display:none' } %tr %th %th Wiki pages %tr %td.shortcut - .key e + %kbd e %td Edit wiki page diff --git a/app/views/help/ui.html.haml b/app/views/help/ui.html.haml index de8369ed7b9..b32b602ceb3 100644 --- a/app/views/help/ui.html.haml +++ b/app/views/help/ui.html.haml @@ -443,8 +443,6 @@ .col-md-6 .alert.alert-success = lorem - .alert.alert-primary - = lorem .alert.alert-info = lorem .col-md-6 diff --git a/app/views/import/gitea/new.html.haml b/app/views/import/gitea/new.html.haml index eb9790c7903..581576a8a3d 100644 --- a/app/views/import/gitea/new.html.haml +++ b/app/views/import/gitea/new.html.haml @@ -12,11 +12,11 @@ = form_tag personal_access_token_import_gitea_path do .form-group.row - = label_tag :gitea_host_url, 'Gitea Host URL', class: 'col-form-label col-sm-8' + = label_tag :gitea_host_url, 'Gitea Host URL', class: 'col-form-label col-sm-2' .col-sm-4 = text_field_tag :gitea_host_url, nil, placeholder: 'https://try.gitea.io', class: 'form-control' .form-group.row - = label_tag :personal_access_token, 'Personal Access Token', class: 'col-form-label col-sm-8' + = label_tag :personal_access_token, 'Personal Access Token', class: 'col-form-label col-sm-2' .col-sm-4 = text_field_tag :personal_access_token, nil, class: 'form-control' .form-actions diff --git a/app/views/import/github/new.html.haml b/app/views/import/github/new.html.haml index c63cf2b31cb..b9ebb1a39d9 100644 --- a/app/views/import/github/new.html.haml +++ b/app/views/import/github/new.html.haml @@ -19,7 +19,7 @@ = form_tag personal_access_token_import_github_path, method: :post, class: 'form-inline' do .form-group - = text_field_tag :personal_access_token, '', class: 'form-control', placeholder: _('Personal Access Token'), size: 40 + = text_field_tag :personal_access_token, '', class: 'form-control append-right-8', placeholder: _('Personal Access Token'), size: 40 = submit_tag _('List your GitHub repositories'), class: 'btn btn-success' - unless github_import_configured? diff --git a/app/views/import/gitlab_projects/new.html.haml b/app/views/import/gitlab_projects/new.html.haml index 2d059e78490..cc672a5ea7c 100644 --- a/app/views/import/gitlab_projects/new.html.haml +++ b/app/views/import/gitlab_projects/new.html.haml @@ -20,7 +20,7 @@ - else .input-group-prepend.static-namespace.has-tooltip{ title: user_url(current_user.username) + '/' } - .input-group-text + .input-group-text.border-0 #{user_url(current_user.username)}/ = hidden_field_tag :namespace_id, value: current_user.namespace_id .form-group.col-12.col-sm-6.project-path @@ -37,6 +37,6 @@ .form-group = file_field_tag :file, class: '' .row - .form-actions + .form-actions.col-sm-12 = submit_tag 'Import project', class: 'btn btn-create' = link_to 'Cancel', new_project_path, class: 'btn btn-cancel' diff --git a/app/views/kaminari/gitlab/_first_page.html.haml b/app/views/kaminari/gitlab/_first_page.html.haml index 369165da02a..3b7d4a1c578 100644 --- a/app/views/kaminari/gitlab/_first_page.html.haml +++ b/app/views/kaminari/gitlab/_first_page.html.haml @@ -5,5 +5,5 @@ -# total_pages: total number of pages -# per_page: number of items to fetch per page -# remote: data-remote -%li.first.page-item +%li.page-item.js-first-button = link_to_unless current_page.first?, raw(t 'views.pagination.first'), url, remote: remote, class: 'page-link' diff --git a/app/views/kaminari/gitlab/_gap.html.haml b/app/views/kaminari/gitlab/_gap.html.haml index 6eec30212d1..849f92fdc95 100644 --- a/app/views/kaminari/gitlab/_gap.html.haml +++ b/app/views/kaminari/gitlab/_gap.html.haml @@ -4,5 +4,5 @@ -# total_pages: total number of pages -# per_page: number of items to fetch per page -# remote: data-remote -%li.page-item.disabled +%li.page-item.disabled.d-none.d-md-block = link_to raw(t 'views.pagination.truncate'), '#', class: 'page-link' diff --git a/app/views/kaminari/gitlab/_last_page.html.haml b/app/views/kaminari/gitlab/_last_page.html.haml index 8b49db58281..7836e17f877 100644 --- a/app/views/kaminari/gitlab/_last_page.html.haml +++ b/app/views/kaminari/gitlab/_last_page.html.haml @@ -5,5 +5,5 @@ -# total_pages: total number of pages -# per_page: number of items to fetch per page -# remote: data-remote -%li.last.page-item +%li.page-item.js-last-button = link_to_unless current_page.last?, raw(t 'views.pagination.last'), url, {remote: remote, class: 'page-link'} diff --git a/app/views/kaminari/gitlab/_next_page.html.haml b/app/views/kaminari/gitlab/_next_page.html.haml index 05f151555ad..a7fa1a21a6c 100644 --- a/app/views/kaminari/gitlab/_next_page.html.haml +++ b/app/views/kaminari/gitlab/_next_page.html.haml @@ -8,5 +8,5 @@ - page_url = current_page.last? ? '#' : url -%li.page-item{ class: ('disabled' if current_page.last?) } +%li.page-item.js-next-button{ class: ('disabled' if current_page.last?) } = link_to raw(t 'views.pagination.next'), page_url, rel: 'next', remote: remote, class: 'page-link' diff --git a/app/views/kaminari/gitlab/_page.html.haml b/app/views/kaminari/gitlab/_page.html.haml index 8a40e13a537..d0dc1784540 100644 --- a/app/views/kaminari/gitlab/_page.html.haml +++ b/app/views/kaminari/gitlab/_page.html.haml @@ -6,5 +6,5 @@ -# total_pages: total number of pages -# per_page: number of items to fetch per page -# remote: data-remote -%li.page-item.js-pagination-page{ class: [active_when(page.current?), ('sibling' if page.next? || page.prev?)] } +%li.page-item.js-pagination-page{ class: [active_when(page.current?), ('sibling' if page.next? || page.prev?), ('d-none d-md-block' if !page.current?) ] } = link_to page, url, { remote: remote, rel: page.next? ? 'next' : page.prev? ? 'prev' : nil, class: 'page-link' } diff --git a/app/views/kaminari/gitlab/_paginator.html.haml b/app/views/kaminari/gitlab/_paginator.html.haml index a6435deb4bf..ac9e274dbc7 100644 --- a/app/views/kaminari/gitlab/_paginator.html.haml +++ b/app/views/kaminari/gitlab/_paginator.html.haml @@ -6,7 +6,7 @@ -# remote: data-remote -# paginator: the paginator that renders the pagination tags inside = paginator.render do - .gl-pagination + .gl-pagination.prepend-top-default %ul.pagination.justify-content-center - unless current_page.first? = first_page_tag unless total_pages < 5 # As kaminari will always show the first 5 pages diff --git a/app/views/kaminari/gitlab/_prev_page.html.haml b/app/views/kaminari/gitlab/_prev_page.html.haml index f4a11a449b7..12b0e106a62 100644 --- a/app/views/kaminari/gitlab/_prev_page.html.haml +++ b/app/views/kaminari/gitlab/_prev_page.html.haml @@ -8,5 +8,5 @@ - page_url = current_page.first? ? '#' : url -%li.page-item{ class: ('disabled' if current_page.first?) } +%li.page-item.js-previous-button{ class: ('disabled' if current_page.first?) } = link_to raw(t 'views.pagination.previous'), page_url, rel: 'prev', remote: remote, class: 'page-link' diff --git a/app/views/kaminari/gitlab/_without_count.html.haml b/app/views/kaminari/gitlab/_without_count.html.haml index 1425a809052..f780400ebcb 100644 --- a/app/views/kaminari/gitlab/_without_count.html.haml +++ b/app/views/kaminari/gitlab/_without_count.html.haml @@ -1,5 +1,5 @@ -.gl-pagination - %ul.pagination.clearfix +.gl-pagination.prepend-top-default + %ul.pagination.justify-content-center - if previous_path %li.page-item.prev = link_to(t('views.pagination.previous'), previous_path, rel: 'prev', class: 'page-link') diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml index 02bdfe9aa3c..9253a0652da 100644 --- a/app/views/layouts/_head.html.haml +++ b/app/views/layouts/_head.html.haml @@ -25,7 +25,7 @@ %title= page_title(site_name) %meta{ name: "description", content: page_description } - = favicon_link_tag favicon, id: 'favicon' + = favicon_link_tag favicon, id: 'favicon', data: { original_href: favicon }, type: 'image/png' = stylesheet_link_tag "application", media: "all" = stylesheet_link_tag "print", media: "print" diff --git a/app/views/layouts/devise.html.haml b/app/views/layouts/devise.html.haml index 82ca7252424..81f35615555 100644 --- a/app/views/layouts/devise.html.haml +++ b/app/views/layouts/devise.html.haml @@ -1,7 +1,7 @@ !!! 5 %html.devise-layout-html{ class: system_message_class } = render "layouts/head" - %body.ui_indigo.login-page.application.navless{ data: { page: body_data_page } } + %body.ui-indigo.login-page.application.navless{ data: { page: body_data_page } } .page-wrap = render "layouts/header/empty" .login-page-broadcast diff --git a/app/views/layouts/devise_empty.html.haml b/app/views/layouts/devise_empty.html.haml index adf90cb7667..52805e0da73 100644 --- a/app/views/layouts/devise_empty.html.haml +++ b/app/views/layouts/devise_empty.html.haml @@ -1,7 +1,7 @@ !!! 5 %html{ lang: "en", class: system_message_class } = render "layouts/head" - %body.ui_indigo.login-page.application.navless + %body.ui-indigo.login-page.application.navless = render "layouts/header/empty" = render "layouts/broadcast" .container.navless-container diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml index 1bca837a311..5cec443e969 100644 --- a/app/views/layouts/header/_default.html.haml +++ b/app/views/layouts/header/_default.html.haml @@ -61,7 +61,7 @@ - if header_link?(:sign_in) %li.nav-item %div - = link_to "Sign in / Register", new_session_path(:user, redirect_to_referer: 'yes'), class: 'nav-link btn btn-sign-in' + = link_to "Sign in / Register", new_session_path(:user, redirect_to_referer: 'yes'), class: 'btn btn-sign-in' %button.navbar-toggler.d-block.d-sm-none{ type: 'button' } %span.sr-only Toggle navigation diff --git a/app/views/layouts/header/_new_dropdown.haml b/app/views/layouts/header/_new_dropdown.haml index db8137cc248..d35df706036 100644 --- a/app/views/layouts/header/_new_dropdown.haml +++ b/app/views/layouts/header/_new_dropdown.haml @@ -1,5 +1,5 @@ %li.header-new.dropdown - = link_to new_project_path, class: "header-new-dropdown-toggle has-tooltip qa-new-menu-toggle", title: "New...", ref: 'tooltip', aria: { label: "New..." }, data: { toggle: 'dropdown', placement: 'bottom', container: 'body' } do + = link_to new_project_path, class: "header-new-dropdown-toggle has-tooltip qa-new-menu-toggle", title: "New...", ref: 'tooltip', aria: { label: "New..." }, data: { toggle: 'dropdown', placement: 'bottom', container: 'body', display: 'static' } do = sprite_icon('plus-square', size: 16) = sprite_icon('angle-down', css_class: 'caret-down') .dropdown-menu.dropdown-menu-right diff --git a/app/views/layouts/terms.html.haml b/app/views/layouts/terms.html.haml index a8964b19ba1..977eb350365 100644 --- a/app/views/layouts/terms.html.haml +++ b/app/views/layouts/terms.html.haml @@ -14,9 +14,9 @@ %div{ class: "#{container_class} limit-container-width" } .content{ id: "content-body" } - .panel.panel-default - .panel-heading - .title + .card + .card-header + .card-title = brand_header_logo - logo_text = brand_header_logo_type - if logo_text.present? diff --git a/app/views/profiles/preferences/show.html.haml b/app/views/profiles/preferences/show.html.haml index ce312943154..8f1078bd41d 100644 --- a/app/views/profiles/preferences/show.html.haml +++ b/app/views/profiles/preferences/show.html.haml @@ -4,18 +4,12 @@ = form_for @user, url: profile_preferences_path, remote: true, method: :put, html: { class: 'row prepend-top-default js-preferences-form' } do |f| .col-lg-4.application-theme %h4.prepend-top-0 - s_('Preferences|Navigation theme') + = s_('Preferences|Navigation theme') %p Customize the appearance of the application header and navigation sidebar. .col-lg-8.application-theme - Gitlab::Themes.each do |theme| = label_tag do - .preview{ class: theme.name.downcase } - .preview-row - .quadrant.one - .quadrant.two - .preview-row - .quadrant.three - .quadrant.four + .preview{ class: theme.css_class } = f.radio_button :theme_id, theme.id, checked: Gitlab::Themes.for_user(@user).id == theme.id = theme.name diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml index 075badb9e56..89940512bc6 100644 --- a/app/views/projects/_home_panel.html.haml +++ b/app/views/projects/_home_panel.html.haml @@ -42,6 +42,10 @@ .project-clone-holder = render "shared/clone_panel" + - if show_xcode_link?(@project) + .project-action-button.project-xcode.inline + = render "projects/buttons/xcode_link" + - if current_user - if can?(current_user, :download_code, @project) = render 'projects/buttons/download', project: @project, ref: @ref diff --git a/app/views/projects/_new_project_fields.html.haml b/app/views/projects/_new_project_fields.html.haml index cfbd0459e3e..6f957533287 100644 --- a/app/views/projects/_new_project_fields.html.haml +++ b/app/views/projects/_new_project_fields.html.haml @@ -16,7 +16,7 @@ - else .input-group-prepend.static-namespace.has-tooltip{ title: user_url(current_user.username) + '/' } - .input-group-text + .input-group-text.border-0 #{user_url(current_user.username)}/ = f.hidden_field :namespace_id, value: current_user.namespace_id .form-group.project-path.col-sm-6 diff --git a/app/views/projects/_project_templates.html.haml b/app/views/projects/_project_templates.html.haml index 9d27f51926e..d08807b5135 100644 --- a/app/views/projects/_project_templates.html.haml +++ b/app/views/projects/_project_templates.html.haml @@ -10,16 +10,18 @@ %a.btn.btn-default{ href: template.preview, rel: 'noopener noreferrer', target: '_blank' } Preview .project-fields-form - .form-group - %label.label-light - Template - .input-group.template-input-group - .input-group-prepend - .input-group-text - .selected-icon - - Gitlab::ProjectTemplate.all.each do |template| - = custom_icon(template.logo) - .selected-template - %button.btn.btn-default.change-template{ type: "button" } Change template + .row + .form-group.col-sm-12 + %label.label-light + Template + .input-group.template-input-group + .input-group-prepend + .input-group-text + .selected-icon + - Gitlab::ProjectTemplate.all.each do |template| + = custom_icon(template.logo) + .selected-template + .input-group-append + %button.btn.btn-default.change-template{ type: "button" } Change template = render 'new_project_fields', f: f, project_name_id: "template-project-name" diff --git a/app/views/projects/_stat_anchor_list.html.haml b/app/views/projects/_stat_anchor_list.html.haml index 8bffd1396ae..15ec58289e3 100644 --- a/app/views/projects/_stat_anchor_list.html.haml +++ b/app/views/projects/_stat_anchor_list.html.haml @@ -5,4 +5,4 @@ - anchors.each do |anchor| %li.nav-item = link_to_if anchor.link, anchor.label, anchor.link, class: anchor.enabled ? 'nav-link stat-link' : "nav-link btn btn-#{anchor.class_modifier || 'missing'}" do - %span.stat-text= anchor.label + .stat-text= anchor.label diff --git a/app/views/projects/artifacts/browse.html.haml b/app/views/projects/artifacts/browse.html.haml index 7ff7466e561..87b165e581a 100644 --- a/app/views/projects/artifacts/browse.html.haml +++ b/app/views/projects/artifacts/browse.html.haml @@ -9,10 +9,10 @@ .tree-holder .nav-block %ul.breadcrumb.repo-breadcrumb - %li + %li.breadcrumb-item = link_to 'Artifacts', browse_project_job_artifacts_path(@project, @build) - path_breadcrumbs do |title, path| - %li + %li.breadcrumb-item = link_to truncate(title, length: 40), browse_project_job_artifacts_path(@project, @build, path) .tree-controls diff --git a/app/views/projects/blob/viewers/_download.html.haml b/app/views/projects/blob/viewers/_download.html.haml index f9b1da05a00..fda4b9c92cd 100644 --- a/app/views/projects/blob/viewers/_download.html.haml +++ b/app/views/projects/blob/viewers/_download.html.haml @@ -1,5 +1,5 @@ .file-content.blob_file.blob-no-preview - .center.render-error.vertical-center + .center.render-error = link_to blob_raw_path do %h1.light = sprite_icon('download') diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml index f641d7bc51a..88f9b7dfc9f 100644 --- a/app/views/projects/branches/_branch.html.haml +++ b/app/views/projects/branches/_branch.html.haml @@ -72,7 +72,7 @@ - else %button{ class: "btn btn-remove remove-row js-ajax-loading-spinner has-tooltip disabled", disabled: true, - title: s_('Branches|Only a project master or owner can delete a protected branch') } + title: s_('Branches|Only a project maintainer or owner can delete a protected branch') } = icon("trash-o") - else = link_to project_branch_path(@project, branch.name), diff --git a/app/views/projects/buttons/_download.html.haml b/app/views/projects/buttons/_download.html.haml index c75093c4c24..f7551434d47 100644 --- a/app/views/projects/buttons/_download.html.haml +++ b/app/views/projects/buttons/_download.html.haml @@ -3,7 +3,7 @@ - if !project.empty_repo? && can?(current_user, :download_code, project) - archive_prefix = "#{project.path}-#{ref.tr('/', '-')}" .project-action-button.dropdown.inline> - %button.btn.has-tooltip{ title: s_('DownloadSource|Download'), 'data-toggle' => 'dropdown', 'aria-label' => s_('DownloadSource|Download') } + %button.btn.has-tooltip{ title: s_('DownloadSource|Download'), 'data-toggle' => 'dropdown', 'aria-label' => s_('DownloadSource|Download'), 'data-display' => 'static' } = sprite_icon('download') = icon("caret-down") %span.sr-only= _('Select Archive Format') diff --git a/app/views/projects/buttons/_dropdown.html.haml b/app/views/projects/buttons/_dropdown.html.haml index 84245d72f4a..8b9c52f0802 100644 --- a/app/views/projects/buttons/_dropdown.html.haml +++ b/app/views/projects/buttons/_dropdown.html.haml @@ -8,7 +8,7 @@ - if show_menu .project-action-button.dropdown.inline - %a.btn.dropdown-toggle.has-tooltip{ href: '#', title: _('Create new...'), 'data-toggle' => 'dropdown', 'data-container' => 'body', 'aria-label' => _('Create new...') } + %a.btn.dropdown-toggle.has-tooltip{ href: '#', title: _('Create new...'), 'data-toggle' => 'dropdown', 'data-container' => 'body', 'aria-label' => _('Create new...'), 'data-display' => 'static' } = icon('plus') = icon("caret-down") %ul.dropdown-menu.dropdown-menu-right.project-home-dropdown diff --git a/app/views/projects/buttons/_xcode_link.html.haml b/app/views/projects/buttons/_xcode_link.html.haml new file mode 100644 index 00000000000..a8b32fb0ef5 --- /dev/null +++ b/app/views/projects/buttons/_xcode_link.html.haml @@ -0,0 +1,2 @@ +%a.btn.btn-default{ href: xcode_uri_to_repo(@project) } + = _("Open in Xcode") diff --git a/app/views/projects/clusters/_advanced_settings.html.haml b/app/views/projects/clusters/_advanced_settings.html.haml index e9bdc54364b..243e8cd9ba0 100644 --- a/app/views/projects/clusters/_advanced_settings.html.haml +++ b/app/views/projects/clusters/_advanced_settings.html.haml @@ -7,8 +7,8 @@ - link_gke = link_to(s_('ClusterIntegration|Google Kubernetes Engine'), @cluster.gke_cluster_url, target: '_blank', rel: 'noopener noreferrer') = s_('ClusterIntegration|Manage your Kubernetes cluster by visiting %{link_gke}').html_safe % { link_gke: link_gke } - .card.form-group - %label.text-danger + .sub-section.form-group + %h4.text-danger = s_('ClusterIntegration|Remove Kubernetes cluster integration') %p = s_("ClusterIntegration|Remove this Kubernetes cluster's configuration from this project. This will not delete your actual Kubernetes cluster.") diff --git a/app/views/projects/clusters/_sidebar.html.haml b/app/views/projects/clusters/_sidebar.html.haml index 73cd7c50922..3d10348212f 100644 --- a/app/views/projects/clusters/_sidebar.html.haml +++ b/app/views/projects/clusters/_sidebar.html.haml @@ -1,7 +1,9 @@ +- clusters_help_url = help_page_path('user/project/clusters/index.md') +- help_link_start = "<a href=\"%{url}\" target=\"_blank\" rel=\"noopener noreferrer\">".html_safe +- help_link_end = '</a>'.html_safe %h4.prepend-top-0 = s_('ClusterIntegration|Kubernetes cluster integration') %p = s_('ClusterIntegration|With a Kubernetes cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way.') %p - - link = link_to(_('Kubernetes'), help_page_path('user/project/clusters/index'), target: '_blank', rel: 'noopener noreferrer') - = s_('ClusterIntegration|Learn more about %{link_to_documentation}').html_safe % { link_to_documentation: link } + = s_('ClusterIntegration|Learn more about %{help_link_start}Kubernetes%{help_link_end}.').html_safe % { help_link_start: help_link_start % { url: clusters_help_url }, help_link_end: help_link_end } diff --git a/app/views/projects/clusters/gcp/_form.html.haml b/app/views/projects/clusters/gcp/_form.html.haml index ca7a6d5a886..b8e40b0a38b 100644 --- a/app/views/projects/clusters/gcp/_form.html.haml +++ b/app/views/projects/clusters/gcp/_form.html.haml @@ -1,4 +1,10 @@ = javascript_include_tag 'https://apis.google.com/js/api.js' +- external_link_icon = icon('external-link') +- zones_link_url = 'https://cloud.google.com/compute/docs/regions-zones/regions-zones' +- machine_type_link_url = 'https://cloud.google.com/compute/docs/machine-types' +- pricing_link_url = 'https://cloud.google.com/compute/pricing#machinetype' +- help_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe +- help_link_end = ' %{external_link_icon}</a>'.html_safe % { external_link_icon: external_link_icon } %p - link_to_help_page = link_to(s_('ClusterIntegration|help page'), help_page_path('user/project/clusters/index'), target: '_blank', rel: 'noopener noreferrer') @@ -7,15 +13,15 @@ = form_for @cluster, html: { class: 'js-gke-cluster-creation prepend-top-20', data: { token: token_in_session } }, url: gcp_namespace_project_clusters_path(@project.namespace, @project), as: :cluster do |field| = form_errors(@cluster) .form-group - = field.label :name, s_('ClusterIntegration|Kubernetes cluster name') + = field.label :name, s_('ClusterIntegration|Kubernetes cluster name'), class: 'label-light' = field.text_field :name, class: 'form-control', placeholder: s_('ClusterIntegration|Kubernetes cluster name') .form-group - = field.label :environment_scope, s_('ClusterIntegration|Environment scope') + = field.label :environment_scope, s_('ClusterIntegration|Environment scope'), class: 'label-light' = field.text_field :environment_scope, class: 'form-control', readonly: !has_multiple_clusters?(@project), placeholder: s_('ClusterIntegration|Environment scope') = field.fields_for :provider_gcp, @cluster.provider_gcp do |provider_gcp_field| .form-group - = provider_gcp_field.label :gcp_project_id, s_('ClusterIntegration|Google Cloud Platform project ID') + = provider_gcp_field.label :gcp_project_id, s_('ClusterIntegration|Google Cloud Platform project'), class: 'label-light' .js-gcp-project-id-dropdown-entry-point{ data: { docsUrl: 'https://console.cloud.google.com/home/dashboard' } } = provider_gcp_field.hidden_field :gcp_project_id .dropdown @@ -26,8 +32,7 @@ %span.form-text.text-muted .form-group - = provider_gcp_field.label :zone, s_('ClusterIntegration|Zone') - = link_to(s_('ClusterIntegration|See zones'), 'https://cloud.google.com/compute/docs/regions-zones/regions-zones', target: '_blank', rel: 'noopener noreferrer') + = provider_gcp_field.label :zone, s_('ClusterIntegration|Zone'), class: 'label-light' .js-gcp-zone-dropdown-entry-point = provider_gcp_field.hidden_field :zone .dropdown @@ -35,13 +40,15 @@ %span.dropdown-toggle-text = _('Select project to choose zone') = icon('chevron-down') + %p.form-text.text-muted + = s_('ClusterIntegration|Learn more about %{help_link_start}zones%{help_link_end}.').html_safe % { help_link_start: help_link_start % { url: zones_link_url }, help_link_end: help_link_end } .form-group - = provider_gcp_field.label :num_nodes, s_('ClusterIntegration|Number of nodes') + = provider_gcp_field.label :num_nodes, s_('ClusterIntegration|Number of nodes'), class: 'label-light' = provider_gcp_field.text_field :num_nodes, class: 'form-control', placeholder: '3' .form-group - = provider_gcp_field.label :machine_type, s_('ClusterIntegration|Machine type') + = provider_gcp_field.label :machine_type, s_('ClusterIntegration|Machine type'), class: 'label-light' .js-gcp-machine-type-dropdown-entry-point = provider_gcp_field.hidden_field :machine_type .dropdown @@ -49,6 +56,8 @@ %span.dropdown-toggle-text = _('Select project and zone to choose machine type') = icon('chevron-down') + %p.form-text.text-muted + = s_('ClusterIntegration|Learn more about %{help_link_start_machine_type}machine types%{help_link_end} and %{help_link_start_pricing}pricing%{help_link_end}.').html_safe % { help_link_start_machine_type: help_link_start % { url: machine_type_link_url }, help_link_start_pricing: help_link_start % { url: pricing_link_url }, help_link_end: help_link_end } .form-group = field.submit s_('ClusterIntegration|Create Kubernetes cluster'), class: 'js-gke-cluster-creation-submit btn btn-success', disabled: true diff --git a/app/views/projects/clusters/user/_form.html.haml b/app/views/projects/clusters/user/_form.html.haml index 2e92524ce8f..db57da99ec7 100644 --- a/app/views/projects/clusters/user/_form.html.haml +++ b/app/views/projects/clusters/user/_form.html.haml @@ -1,27 +1,27 @@ = form_for @cluster, url: user_namespace_project_clusters_path(@project.namespace, @project), as: :cluster do |field| = form_errors(@cluster) .form-group - = field.label :name, s_('ClusterIntegration|Kubernetes cluster name') + = field.label :name, s_('ClusterIntegration|Kubernetes cluster name'), class: 'label-light' = field.text_field :name, class: 'form-control', placeholder: s_('ClusterIntegration|Kubernetes cluster name') .form-group - = field.label :environment_scope, s_('ClusterIntegration|Environment scope') + = field.label :environment_scope, s_('ClusterIntegration|Environment scope'), class: 'label-light' = field.text_field :environment_scope, class: 'form-control', readonly: !has_multiple_clusters?(@project), placeholder: s_('ClusterIntegration|Environment scope') = field.fields_for :platform_kubernetes, @cluster.platform_kubernetes do |platform_kubernetes_field| .form-group - = platform_kubernetes_field.label :api_url, s_('ClusterIntegration|API URL') + = platform_kubernetes_field.label :api_url, s_('ClusterIntegration|API URL'), class: 'label-light' = platform_kubernetes_field.text_field :api_url, class: 'form-control', placeholder: s_('ClusterIntegration|API URL') .form-group - = platform_kubernetes_field.label :ca_cert, s_('ClusterIntegration|CA Certificate') + = platform_kubernetes_field.label :ca_cert, s_('ClusterIntegration|CA Certificate'), class: 'label-light' = platform_kubernetes_field.text_area :ca_cert, class: 'form-control', placeholder: s_('ClusterIntegration|Certificate Authority bundle (PEM format)') .form-group - = platform_kubernetes_field.label :token, s_('ClusterIntegration|Token') + = platform_kubernetes_field.label :token, s_('ClusterIntegration|Token'), class: 'label-light' = platform_kubernetes_field.text_field :token, class: 'form-control', placeholder: s_('ClusterIntegration|Service token'), autocomplete: 'off' .form-group - = platform_kubernetes_field.label :namespace, s_('ClusterIntegration|Project namespace (optional, unique)') + = platform_kubernetes_field.label :namespace, s_('ClusterIntegration|Project namespace (optional, unique)'), class: 'label-light' = platform_kubernetes_field.text_field :namespace, class: 'form-control', placeholder: s_('ClusterIntegration|Project namespace') .form-group diff --git a/app/views/projects/clusters/user/_show.html.haml b/app/views/projects/clusters/user/_show.html.haml index 77d7a055474..4d117f435dc 100644 --- a/app/views/projects/clusters/user/_show.html.haml +++ b/app/views/projects/clusters/user/_show.html.haml @@ -1,20 +1,20 @@ = form_for @cluster, url: namespace_project_cluster_path(@project.namespace, @project, @cluster), as: :cluster do |field| = form_errors(@cluster) .form-group - = field.label :name, s_('ClusterIntegration|Kubernetes cluster name') + = field.label :name, s_('ClusterIntegration|Kubernetes cluster name'), class: 'label-light' = field.text_field :name, class: 'form-control', placeholder: s_('ClusterIntegration|Kubernetes cluster name') = field.fields_for :platform_kubernetes, @cluster.platform_kubernetes do |platform_kubernetes_field| .form-group - = platform_kubernetes_field.label :api_url, s_('ClusterIntegration|API URL') + = platform_kubernetes_field.label :api_url, s_('ClusterIntegration|API URL'), class: 'label-light' = platform_kubernetes_field.text_field :api_url, class: 'form-control', placeholder: s_('ClusterIntegration|API URL') .form-group - = platform_kubernetes_field.label :ca_cert, s_('ClusterIntegration|CA Certificate') + = platform_kubernetes_field.label :ca_cert, s_('ClusterIntegration|CA Certificate'), class: 'label-light' = platform_kubernetes_field.text_area :ca_cert, class: 'form-control', placeholder: s_('ClusterIntegration|Certificate Authority bundle (PEM format)') .form-group - = platform_kubernetes_field.label :token, s_('ClusterIntegration|Token') + = platform_kubernetes_field.label :token, s_('ClusterIntegration|Token'), class: 'label-light' .input-group = platform_kubernetes_field.text_field :token, class: 'form-control js-cluster-token', type: 'password', placeholder: s_('ClusterIntegration|Token'), autocomplete: 'off' %span.input-group-append.clipboard-addon @@ -23,7 +23,7 @@ = s_('ClusterIntegration|Show') .form-group - = platform_kubernetes_field.label :namespace, s_('ClusterIntegration|Project namespace (optional, unique)') + = platform_kubernetes_field.label :namespace, s_('ClusterIntegration|Project namespace (optional, unique)'), class: 'label-light' = platform_kubernetes_field.text_field :namespace, class: 'form-control', placeholder: s_('ClusterIntegration|Project namespace') .form-group diff --git a/app/views/projects/commit/_change.html.haml b/app/views/projects/commit/_change.html.haml index 30605927fd1..3d97e93c9e9 100644 --- a/app/views/projects/commit/_change.html.haml +++ b/app/views/projects/commit/_change.html.haml @@ -20,18 +20,18 @@ %span{ "aria-hidden": true } × .modal-body - if description - %p.append-bottom-20= description + %p= description = form_tag [type.underscore, @project.namespace.becomes(Namespace), @project, commit], method: :post, remote: false, class: "js-#{type}-form js-requires-input" do - .form-group.row.branch - = label_tag 'start_branch', branch_label, class: 'col-form-label col-sm-2' - .col-sm-10 - = hidden_field_tag :start_branch, @project.default_branch, id: 'start_branch' - = dropdown_tag(@project.default_branch, options: { title: s_("BranchSwitcherTitle|Switch branch"), filter: true, placeholder: s_("BranchSwitcherPlaceholder|Search branches"), toggle_class: 'js-project-refs-dropdown dynamic', dropdown_class: 'dropdown-menu-selectable', data: { field_name: "start_branch", selected: @project.default_branch, start_branch: @project.default_branch, refs_url: project_branches_path(@project), submit_form_on_click: false } }) + .form-group.branch + = label_tag 'start_branch', branch_label, class: 'label-light' - - if can?(current_user, :push_code, @project) - = render 'shared/new_merge_request_checkbox' - - else - = hidden_field_tag 'create_merge_request', 1, id: nil + = hidden_field_tag :start_branch, @project.default_branch, id: 'start_branch' + = dropdown_tag(@project.default_branch, options: { title: s_("BranchSwitcherTitle|Switch branch"), filter: true, placeholder: s_("BranchSwitcherPlaceholder|Search branches"), toggle_class: 'js-project-refs-dropdown dynamic', dropdown_class: 'dropdown-menu-selectable', data: { field_name: "start_branch", selected: @project.default_branch, start_branch: @project.default_branch, refs_url: project_branches_path(@project), submit_form_on_click: false } }) + + - if can?(current_user, :push_code, @project) + = render 'shared/new_merge_request_checkbox' + - else + = hidden_field_tag 'create_merge_request', 1, id: nil .form-actions = submit_tag label, class: 'btn btn-create' = link_to _("Cancel"), '#', class: "btn btn-cancel", "data-dismiss" => "modal" diff --git a/app/views/projects/commit/branches.html.haml b/app/views/projects/commit/branches.html.haml index a91e31afc2b..0b8e5105bc0 100644 --- a/app/views/projects/commit/branches.html.haml +++ b/app/views/projects/commit/branches.html.haml @@ -6,7 +6,7 @@ - if @branches.any? || @tags.any? || @tags_limit_exceeded %span - = link_to "#", class: "js-details-expand label label-gray ref-name" do + = link_to "#", class: "js-details-expand badge badge-gray ref-name" do = sprite_icon('ellipsis_h', size: 12, css_class: 'vertical-align-middle') %span.js-details-content.hide = commit_branches_links(@project, @branches) diff --git a/app/views/projects/deploy_keys/_index.html.haml b/app/views/projects/deploy_keys/_index.html.haml index 6af57d3ab26..fb1ea471dec 100644 --- a/app/views/projects/deploy_keys/_index.html.haml +++ b/app/views/projects/deploy_keys/_index.html.haml @@ -1,5 +1,5 @@ - expanded = Rails.env.test? -%section.settings.no-animate{ class: ('expanded' if expanded) } +%section.qa-deploy-keys-settings.settings.no-animate{ class: ('expanded' if expanded) } .settings-header %h4 Deploy Keys diff --git a/app/views/projects/deploy_tokens/_index.html.haml b/app/views/projects/deploy_tokens/_index.html.haml index 50e5950ced4..725720d2222 100644 --- a/app/views/projects/deploy_tokens/_index.html.haml +++ b/app/views/projects/deploy_tokens/_index.html.haml @@ -10,9 +10,8 @@ .settings-content - if @new_deploy_token.persisted? = render 'projects/deploy_tokens/new_deploy_token', deploy_token: @new_deploy_token - - else - %h5.prepend-top-0 - = s_('DeployTokens|Add a deploy token') - = render 'projects/deploy_tokens/form', project: @project, token: @new_deploy_token, presenter: @deploy_tokens - %hr + %h5.prepend-top-0 + = s_('DeployTokens|Add a deploy token') + = render 'projects/deploy_tokens/form', project: @project, token: @new_deploy_token, presenter: @deploy_tokens + %hr = render 'projects/deploy_tokens/table', project: @project, active_tokens: @deploy_tokens diff --git a/app/views/projects/deploy_tokens/_new_deploy_token.html.haml b/app/views/projects/deploy_tokens/_new_deploy_token.html.haml index 1e715681e59..5dd9ffba074 100644 --- a/app/views/projects/deploy_tokens/_new_deploy_token.html.haml +++ b/app/views/projects/deploy_tokens/_new_deploy_token.html.haml @@ -1,14 +1,18 @@ -.created-deploy-token-container - %h5.prepend-top-0 - = s_('DeployTokens|Your New Deploy Token') +.created-deploy-token-container.info-well + .well-segment + %h5.prepend-top-0 + = s_('DeployTokens|Your New Deploy Token') - .form-group - = text_field_tag 'deploy-token-user', deploy_token.username, readonly: true, class: 'deploy-token-field form-control js-select-on-focus' - = clipboard_button(text: deploy_token.username, title: s_('DeployTokens|Copy username to clipboard'), placement: 'left') - %span.deploy-token-help-block.prepend-top-5.text-success= s_("DeployTokens|Use this username as a login.") + .form-group + .input-group + = text_field_tag 'deploy-token-user', deploy_token.username, readonly: true, class: 'deploy-token-field form-control js-select-on-focus' + .input-group-append + = clipboard_button(text: deploy_token.username, title: s_('DeployTokens|Copy username to clipboard'), placement: 'left') + %span.deploy-token-help-block.prepend-top-5.text-success= s_("DeployTokens|Use this username as a login.") - .form-group - = text_field_tag 'deploy-token', deploy_token.token, readonly: true, class: 'deploy-token-field form-control js-select-on-focus' - = clipboard_button(text: deploy_token.token, title: s_('DeployTokens|Copy deploy token to clipboard'), placement: 'left') - %span.deploy-token-help-block.prepend-top-5.text-danger= s_("DeployTokens|Use this token as a password. Make sure you save it - you won't be able to access it again.") -%hr + .form-group + .input-group + = text_field_tag 'deploy-token', deploy_token.token, readonly: true, class: 'deploy-token-field form-control js-select-on-focus' + .input-group-append + = clipboard_button(text: deploy_token.token, title: s_('DeployTokens|Copy deploy token to clipboard'), placement: 'left') + %span.deploy-token-help-block.prepend-top-5.text-danger= s_("DeployTokens|Use this token as a password. Make sure you save it - you won't be able to access it again.") diff --git a/app/views/projects/diffs/_collapsed.html.haml b/app/views/projects/diffs/_collapsed.html.haml index 5762f4d86d7..9bd1255fe00 100644 --- a/app/views/projects/diffs/_collapsed.html.haml +++ b/app/views/projects/diffs/_collapsed.html.haml @@ -2,4 +2,4 @@ - url = url_for(safe_params.merge(action: :diff_for_path, old_path: diff_file.old_path, new_path: diff_file.new_path, file_identifier: diff_file.file_identifier)) .nothing-here-block.diff-collapsed{ data: { diff_for_path: url } } This diff is collapsed. - %a.click-to-expand Click to expand it. + %button.click-to-expand.btn.btn-link Click to expand it. diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index 77ba422e87e..9f175d2376f 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -7,7 +7,7 @@ %section.settings.general-settings.no-animate{ class: ('expanded' if expanded) } .settings-header %h4 - General project settings + General project %button.btn.js-settings-toggle{ type: 'button' } = expanded ? 'Collapse' : 'Expand' %p @@ -82,10 +82,10 @@ = render_if_exists 'projects/issues_settings' - %section.settings.merge-requests-feature.no-animate{ class: [('expanded' if expanded), ('hidden' if @project.project_feature.send(:merge_requests_access_level) == 0)] } + %section.qa-merge-request-settings.settings.merge-requests-feature.no-animate{ class: [('expanded' if expanded), ('hidden' if @project.project_feature.send(:merge_requests_access_level) == 0)] } .settings-header %h4 - Merge request settings + Merge request %button.btn.js-settings-toggle{ type: 'button' } = expanded ? 'Collapse' : 'Expand' %p @@ -101,10 +101,10 @@ = render 'export', project: @project - %section.settings.advanced-settings.no-animate{ class: ('expanded' if expanded) } + %section.qa-advanced-settings.settings.advanced-settings.no-animate{ class: ('expanded' if expanded) } .settings-header %h4 - Advanced settings + Advanced %button.btn.js-settings-toggle{ type: 'button' } = expanded ? 'Collapse' : 'Expand' %p diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml index bf6fc8af12d..d47dc3d8143 100644 --- a/app/views/projects/empty.html.haml +++ b/app/views/projects/empty.html.haml @@ -1,3 +1,4 @@ +- @content_class = "limit-container-width" unless fluid_layout - @no_container = true - breadcrumb_title _("Details") @@ -6,7 +7,7 @@ = render "home_panel" .project-empty-note-panel - %div{ class: [container_class, ("limit-container-width-sm" unless fluid_layout)] } + %div{ class: [container_class, ("limit-container-width" unless fluid_layout)] } .prepend-top-20 %h4 = _('The repository for this project is empty') @@ -36,7 +37,7 @@ = render 'stat_anchor_list', anchors: @project.empty_repo_statistics_buttons - if can?(current_user, :push_code, @project) - %div{ class: [container_class, ("limit-container-width-sm" unless fluid_layout)] } + %div{ class: [container_class, ("limit-container-width" unless fluid_layout)] } .prepend-top-20 .empty_wrapper %h3#repo-command-line-instructions.page-title-empty @@ -44,14 +45,14 @@ .git-empty %fieldset %h5 Git global setup - %pre.card.bg-light + %pre.bg-light :preserve git config --global user.name "#{h git_user_name}" git config --global user.email "#{h git_user_email}" %fieldset %h5 Create a new repository - %pre.card.bg-light + %pre.bg-light :preserve git clone #{ content_tag(:span, default_url_to_repo, class: 'clone')} cd #{h @project.path} @@ -64,7 +65,7 @@ %fieldset %h5 Existing folder - %pre.card.bg-light + %pre.bg-light :preserve cd existing_folder git init @@ -77,7 +78,7 @@ %fieldset %h5 Existing Git repository - %pre.card.bg-light + %pre.bg-light :preserve cd existing_repo git remote rename origin old-origin diff --git a/app/views/projects/issues/_new_branch.html.haml b/app/views/projects/issues/_new_branch.html.haml index 76438fae663..acb1a446ec4 100644 --- a/app/views/projects/issues/_new_branch.html.haml +++ b/app/views/projects/issues/_new_branch.html.haml @@ -18,7 +18,7 @@ %button.btn.js-create-merge-request.btn-success.btn-inverted{ type: 'button', data: { action: data_action } } = value - %button.btn.create-merge-request-dropdown-toggle.dropdown-toggle.btn-success.btn-inverted.js-dropdown-toggle{ type: 'button', data: { dropdown: { trigger: '#create-merge-request-dropdown' } } } + %button.btn.create-merge-request-dropdown-toggle.dropdown-toggle.btn-success.btn-inverted.js-dropdown-toggle{ type: 'button', data: { dropdown: { trigger: '#create-merge-request-dropdown' }, display: 'static' } } = icon('caret-down') .droplab-dropdown diff --git a/app/views/projects/jobs/show.html.haml b/app/views/projects/jobs/show.html.haml index ec9a04c0eab..1f33bb3a129 100644 --- a/app/views/projects/jobs/show.html.haml +++ b/app/views/projects/jobs/show.html.haml @@ -86,9 +86,7 @@ %button.js-scroll-down.btn-scroll.btn-transparent.btn-blank{ type: 'button', disabled: true } = custom_icon('scroll_down') - %pre.build-trace#build-trace - %code.bash.js-build-output - .build-loader-animation.js-build-refresh + = render 'shared/builds/build_output' - else = render "empty_states" diff --git a/app/views/projects/labels/index.html.haml b/app/views/projects/labels/index.html.haml index 9c78bade254..fb5b0fc15c9 100644 --- a/app/views/projects/labels/index.html.haml +++ b/app/views/projects/labels/index.html.haml @@ -1,40 +1,44 @@ - @no_container = true - page_title "Labels" -- hide_class = '' - can_admin_label = can?(current_user, :admin_label, @project) +- hide_class = '' + +- if can_admin_label + - content_for(:header_content) do + .nav-controls + = link_to _('New label'), new_project_label_path(@project), class: "btn btn-new" - if @labels.exists? || @prioritized_labels.exists? #promote-label-modal %div{ class: container_class } .top-area.adjust .nav-text - Labels can be applied to issues and merge requests. + = _('Labels can be applied to issues and merge requests.') - if can_admin_label - Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging. + = _('Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging.') - - if can_admin_label - .nav-controls - = link_to new_project_label_path(@project), class: "btn btn-new" do - New label - - .labels + .labels-container.prepend-top-5 - if can_admin_label -# Only show it in the first page - hide = @available_labels.empty? || (params[:page].present? && params[:page] != '1') .prioritized-labels{ class: ('hide' if hide) } - %h5 Prioritized Labels - %ul.content-list.manage-labels-list.js-prioritized-labels{ "data-url" => set_priorities_project_labels_path(@project) } - #js-priority-labels-empty-state{ class: "#{'hidden' unless @prioritized_labels.empty?}" } + %h5.prepend-top-10= _('Prioritized Labels') + .content-list.manage-labels-list.js-prioritized-labels{ "data-url" => set_priorities_project_labels_path(@project) } + #js-priority-labels-empty-state.priority-labels-empty-state{ class: "#{'hidden' unless @prioritized_labels.empty?}" } = render 'shared/empty_states/priority_labels' - if @prioritized_labels.present? - = render partial: 'shared/label', subject: @project, collection: @prioritized_labels, as: :label + = render partial: 'shared/label', subject: @project, collection: @prioritized_labels, as: :label, locals: { force_priority: true } - if @labels.present? .other-labels - if can_admin_label - %h5{ class: ('hide' if hide) } Other Labels - %ul.content-list.manage-labels-list.js-other-labels + %h5{ class: ('hide' if hide) }= _('Other Labels') + .content-list.manage-labels-list.js-other-labels = render partial: 'shared/label', subject: @project, collection: @labels, as: :label = paginate @labels, theme: 'gitlab' - else = render 'shared/empty_states/labels' + +%template#js-badge-item-template + %li.label-link-item.js-priority-badge.inline.prepend-left-10 + .label-badge.label-badge-blue= _('Prioritized label') diff --git a/app/views/projects/merge_requests/creations/_new_submit.html.haml b/app/views/projects/merge_requests/creations/_new_submit.html.haml index ebcd99f2a9b..b2eacabc21a 100644 --- a/app/views/projects/merge_requests/creations/_new_submit.html.haml +++ b/app/views/projects/merge_requests/creations/_new_submit.html.haml @@ -25,7 +25,7 @@ = custom_icon ('illustration_no_commits') - else %ul.merge-request-tabs.nav.nav-tabs.nav-links.no-top.no-bottom - %li.commits-tab.active + %li.commits-tab = link_to url_for(safe_params), data: {target: 'div#commits', action: 'new', toggle: 'tab'} do Commits %span.badge.badge-pill= @commits.size diff --git a/app/views/projects/merge_requests/diffs/_version_controls.html.haml b/app/views/projects/merge_requests/diffs/_version_controls.html.haml index 1c26f0405d2..52bf584d550 100644 --- a/app/views/projects/merge_requests/diffs/_version_controls.html.haml +++ b/app/views/projects/merge_requests/diffs/_version_controls.html.haml @@ -3,7 +3,7 @@ .mr-version-menus-container.content-block Changes between %span.dropdown.inline.mr-version-dropdown - %a.dropdown-toggle.btn.btn-default{ data: {toggle: :dropdown} } + %a.dropdown-toggle.btn.btn-default{ data: { toggle: :dropdown, display: 'static' } } %span - if @merge_request_diff.latest? latest version @@ -36,7 +36,7 @@ - if @merge_request_diff.base_commit_sha and %span.dropdown.inline.mr-version-compare-dropdown - %a.btn.btn-default.dropdown-toggle{ data: {toggle: :dropdown} } + %a.btn.btn-default.dropdown-toggle{ data: { toggle: :dropdown, display: 'static' } } - if @start_version version #{version_index(@start_version)} - else diff --git a/app/views/projects/merge_requests/show.html.haml b/app/views/projects/merge_requests/show.html.haml index 01e38ffee20..2f1877a15c2 100644 --- a/app/views/projects/merge_requests/show.html.haml +++ b/app/views/projects/merge_requests/show.html.haml @@ -32,26 +32,25 @@ .scrolling-tabs-container.inner-page-scroll-tabs.is-smaller .fade-left= icon('angle-left') .fade-right= icon('angle-right') - .nav-links.scrolling-tabs.nav.nav-tabs - %ul.merge-request-tabs.nav-tabs.nav - %li.notes-tab - = tab_link_for @merge_request, :show, force_link: @commit.present? do - Discussion - %span.badge.badge-pill= @merge_request.related_notes.user.count - - if @merge_request.source_project - %li.commits-tab - = tab_link_for @merge_request, :commits do - Commits - %span.badge.badge-pill= @commits_count - - if @pipelines.any? - %li.pipelines-tab - = tab_link_for @merge_request, :pipelines do - Pipelines - %span.badge.badge-pill.js-pipelines-mr-count= @pipelines.size - %li.diffs-tab - = tab_link_for @merge_request, :diffs do - Changes - %span.badge.badge-pill= @merge_request.diff_size + %ul.merge-request-tabs.nav-tabs.nav.nav-links.scrolling-tabs + %li.notes-tab + = tab_link_for @merge_request, :show, force_link: @commit.present? do + Discussion + %span.badge.badge-pill= @merge_request.related_notes.user.count + - if @merge_request.source_project + %li.commits-tab + = tab_link_for @merge_request, :commits do + Commits + %span.badge.badge-pill= @commits_count + - if @pipelines.any? + %li.pipelines-tab + = tab_link_for @merge_request, :pipelines do + Pipelines + %span.badge.badge-pill.js-pipelines-mr-count= @pipelines.size + %li.diffs-tab + = tab_link_for @merge_request, :diffs do + Changes + %span.badge.badge-pill= @merge_request.diff_size - if has_vue_discussions_cookie? #js-vue-discussion-counter diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml index 35a09f06bfa..5bb1bfb7059 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -29,16 +29,16 @@ .col-lg-9.js-toggle-container %ul.nav.nav-tabs.nav-links.gitlab-tabs{ role: 'tablist' } - %li{ class: active_when(active_tab == 'blank'), role: 'presentation' } - %a{ href: '#blank-project-pane', id: 'blank-project-tab', data: { toggle: 'tab' }, role: 'tab' } + %li.nav-item{ role: 'presentation' } + %a.nav-link.active{ href: '#blank-project-pane', id: 'blank-project-tab', data: { toggle: 'tab' }, role: 'tab' } %span.d-none.d-sm-block Blank project %span.d-block.d-sm-none Blank - %li{ class: active_when(active_tab == 'template'), role: 'presentation' } - %a{ href: '#create-from-template-pane', id: 'create-from-template-tab', data: { toggle: 'tab' }, role: 'tab' } + %li.nav-item{ role: 'presentation' } + %a.nav-link{ href: '#create-from-template-pane', id: 'create-from-template-tab', data: { toggle: 'tab' }, role: 'tab' } %span.d-none.d-sm-block Create from template %span.d-block.d-sm-none Template - %li{ class: active_when(active_tab == 'import'), role: 'presentation' } - %a{ href: '#import-project-pane', id: 'import-project-tab', data: { toggle: 'tab' }, role: 'tab' } + %li.nav-item{ role: 'presentation' } + %a.nav-link{ href: '#import-project-pane', id: 'import-project-tab', data: { toggle: 'tab' }, role: 'tab' } %span.d-none.d-sm-block Import project %span.d-block.d-sm-none Import diff --git a/app/views/projects/pages/_destroy.haml b/app/views/projects/pages/_destroy.haml index 4ada19a1368..9b77c4e3494 100644 --- a/app/views/projects/pages/_destroy.haml +++ b/app/views/projects/pages/_destroy.haml @@ -5,7 +5,7 @@ .errors-holder .card-body %p - Removing the pages will prevent from exposing them to outside world. + Removing pages will prevent them from being exposed to the outside world. .form-actions = link_to 'Remove pages', project_pages_path(@project), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn btn-remove" - else diff --git a/app/views/projects/pipelines/_with_tabs.html.haml b/app/views/projects/pipelines/_with_tabs.html.haml index 118391aac64..951f80b378d 100644 --- a/app/views/projects/pipelines/_with_tabs.html.haml +++ b/app/views/projects/pipelines/_with_tabs.html.haml @@ -1,7 +1,7 @@ .tabs-holder %ul.pipelines-tabs.nav-links.no-top.no-bottom.mobile-separator.nav.nav-tabs %li.js-pipeline-tab-link - = link_to project_pipeline_path(@project, @pipeline), data: { target: '#js-tab-pipeline', action: 'pipelines', toggle: 'tab' }, class: 'pipeline-tab' do + = link_to project_pipeline_path(@project, @pipeline), data: { target: '#js-tab-pipeline', action: 'pipelines', toggle: 'tab' }, class: 'pipeline-tab' do = _("Pipeline") %li.js-builds-tab-link = link_to builds_project_pipeline_path(@project, @pipeline), data: { target: '#js-tab-builds', action: 'builds', toggle: 'tab' }, class: 'builds-tab' do @@ -43,12 +43,36 @@ = render partial: "projects/stage/stage", collection: pipeline.legacy_stages, as: :stage - if @pipeline.failed_builds.present? - #js-tab-failures.build-failures.tab-pane - - @pipeline.failed_builds.each_with_index do |build, index| - .build-state - %span.ci-status-icon-failed= custom_icon('icon_status_failed') - %span.stage - = build.stage.titleize - %span.build-name - = link_to build.name, pipeline_job_url(pipeline, build) - %pre.build-log= build_summary(build, skip: index >= 10) + #js-tab-failures.build-failures.tab-pane.build-page + %table.table.responsive-table.ci-table.responsive-table-sm-rounded + %thead + %th.table-th-transparent + %th.table-th-transparent= _("Name") + %th.table-th-transparent= _("Stage") + %th.table-th-transparent= _("Failure") + + %tbody + - @pipeline.failed_builds.each_with_index do |build, index| + - job = build.present(current_user: current_user) + %tr.build-state.responsive-table-border-start + %td.responsive-table-cell.ci-status-icon-failed{ data: { column: "Status"} } + .d-none.d-md-block.build-icon + = custom_icon("icon_status_#{build.status}") + .d-md-none.build-badge + = render "ci/status/badge", link: false, status: job.detailed_status(current_user) + %td.responsive-table-cell.build-name{ data: { column: _("Name")} } + = link_to build.name, pipeline_job_url(pipeline, build) + %td.responsive-table-cell.build-stage{ data: { column: _("Stage")} } + = build.stage.titleize + %td.responsive-table-cell.build-failure{ data: { column: _("Failure")} } + = build.present.callout_failure_message + %td.responsive-table-cell.build-actions + = link_to retry_project_job_path(build.project, build, return_to: request.original_url), method: :post, title: _('Retry'), class: 'btn btn-build' do + = icon('repeat') + %tr.build-trace-row.responsive-table-border-end + %td + %td.responsive-table-cell.build-trace-container{ colspan: 4 } + %pre.build-trace.build-trace-rounded + %code.bash.js-build-output + = build_summary(build) + diff --git a/app/views/projects/project_members/_groups.html.haml b/app/views/projects/project_members/_groups.html.haml index 128f52ff648..3f05e06b0c6 100644 --- a/app/views/projects/project_members/_groups.html.haml +++ b/app/views/projects/project_members/_groups.html.haml @@ -3,5 +3,5 @@ Groups with access to %strong= @project.name %span.badge.badge-pill= group_links.size - %ul.content-list + %ul.content-list.members-list = render partial: 'shared/members/group', collection: group_links, as: :group_link diff --git a/app/views/projects/project_members/_team.html.haml b/app/views/projects/project_members/_team.html.haml index a30870a241c..0c5a187f208 100644 --- a/app/views/projects/project_members/_team.html.haml +++ b/app/views/projects/project_members/_team.html.haml @@ -9,9 +9,10 @@ %span.badge.badge-pill= members.total_count = form_tag project_project_members_path(project), method: :get, class: 'form-inline member-search-form flex-project-members-form' do .form-group - = search_field_tag :search, params[:search], { placeholder: 'Find existing members by name', class: 'form-control', spellcheck: false } - %button.member-search-btn{ type: "submit", "aria-label" => "Submit search" } - = icon("search") + .position-relative + = search_field_tag :search, params[:search], { placeholder: 'Find existing members by name', class: 'form-control', spellcheck: false } + %button.member-search-btn{ type: "submit", "aria-label" => "Submit search" } + = icon("search") = render 'shared/members/sort_dropdown' %ul.content-list.members-list = render partial: 'shared/members/member', collection: members, as: :member diff --git a/app/views/projects/project_members/index.html.haml b/app/views/projects/project_members/index.html.haml index a56023e98cd..9716322f8a1 100644 --- a/app/views/projects/project_members/index.html.haml +++ b/app/views/projects/project_members/index.html.haml @@ -12,17 +12,17 @@ - else %p Members can be added by project - %i Masters + %i Maintainers or %i Owners .light - if can?(current_user, :admin_project_member, @project) %ul.nav-links.nav.nav-tabs.gitlab-tabs{ role: 'tablist' } - %li.active{ role: 'presentation' } - %a{ href: '#add-member-pane', id: 'add-member-tab', data: { toggle: 'tab' }, role: 'tab' } Add member + %li.nav-tab{ role: 'presentation' } + %a.nav-link.active{ href: '#add-member-pane', id: 'add-member-tab', data: { toggle: 'tab' }, role: 'tab' } Add member - if @project.allowed_to_share_with_group? - %li{ role: 'presentation' } - %a{ href: '#share-with-group-pane', id: 'share-with-group-tab', data: { toggle: 'tab' }, role: 'tab' } Share with group + %li.nav-tab{ role: 'presentation' } + %a.nav-link{ href: '#share-with-group-pane', id: 'share-with-group-tab', data: { toggle: 'tab' }, role: 'tab' } Share with group .tab-content.gitlab-tab-content .tab-pane.active{ id: 'add-member-pane', role: 'tabpanel' } diff --git a/app/views/projects/protected_branches/shared/_branches_list.html.haml b/app/views/projects/protected_branches/shared/_branches_list.html.haml index a2cd7752fc4..9a06eca89bb 100644 --- a/app/views/projects/protected_branches/shared/_branches_list.html.haml +++ b/app/views/projects/protected_branches/shared/_branches_list.html.haml @@ -1,7 +1,7 @@ .protected-branches-list.js-protected-branches-list.qa-protected-branches-list - if @protected_branches.empty? - .card-header - %h3.card-title + .card-header.bg-white + %h3.card-title.mb-0 Protected branch (#{@protected_branches_count}) %p.settings-message.text-center There are currently no protected branches, protect a branch with the form above. diff --git a/app/views/projects/protected_branches/shared/_index.html.haml b/app/views/projects/protected_branches/shared/_index.html.haml index fd5c1aa342a..4f1c6c92484 100644 --- a/app/views/projects/protected_branches/shared/_index.html.haml +++ b/app/views/projects/protected_branches/shared/_index.html.haml @@ -1,6 +1,6 @@ - expanded = Rails.env.test? -%section.settings.no-animate{ class: ('expanded' if expanded) } +%section.qa-protected-branches-settings.settings.no-animate{ class: ('expanded' if expanded) } .settings-header %h4 Protected Branches @@ -12,8 +12,8 @@ %p By default, protected branches are designed to: %ul - %li prevent their creation, if not already created, from everybody except Masters - %li prevent pushes from everybody except Masters + %li prevent their creation, if not already created, from everybody except Maintainers + %li prevent pushes from everybody except Maintainers %li prevent <strong>anyone</strong> from force pushing to the branch %li prevent <strong>anyone</strong> from deleting the branch %p Read more about #{link_to "protected branches", help_page_path("user/project/protected_branches")} and #{link_to "project permissions", help_page_path("user/permissions")}. diff --git a/app/views/projects/protected_tags/shared/_index.html.haml b/app/views/projects/protected_tags/shared/_index.html.haml index c33723d8072..fe2903b456f 100644 --- a/app/views/projects/protected_tags/shared/_index.html.haml +++ b/app/views/projects/protected_tags/shared/_index.html.haml @@ -12,7 +12,7 @@ %p By default, protected tags are designed to: %ul - %li Prevent tag creation by everybody except Masters + %li Prevent tag creation by everybody except Maintainers %li Prevent <strong>anyone</strong> from updating the tag %li Prevent <strong>anyone</strong> from deleting the tag diff --git a/app/views/projects/releases/edit.html.haml b/app/views/projects/releases/edit.html.haml index b4787032966..d6f758608a0 100644 --- a/app/views/projects/releases/edit.html.haml +++ b/app/views/projects/releases/edit.html.haml @@ -13,7 +13,7 @@ = form_for(@release, method: :put, url: project_tag_release_path(@project, @tag.name), html: { class: 'common-note-form release-form js-quick-submit' }) do |f| = render layout: 'projects/md_preview', locals: { url: preview_markdown_path(@project), referenced_users: true } do - = render 'projects/zen', f: f, attr: :description, classes: 'note-textarea', placeholder: "Write your release notes or drag files here..." + = render 'projects/zen', f: f, attr: :description, classes: 'note-textarea', placeholder: "Write your release notes or drag files here…" = render 'shared/notes/hints' .error-alert .prepend-top-default diff --git a/app/views/projects/runners/_group_runners.html.haml b/app/views/projects/runners/_group_runners.html.haml index dfed0553f84..86de71c732b 100644 --- a/app/views/projects/runners/_group_runners.html.haml +++ b/app/views/projects/runners/_group_runners.html.haml @@ -26,9 +26,9 @@ - if can?(current_user, :admin_pipeline, @project.group) - group_link = link_to _('Group CI/CD settings'), group_settings_ci_cd_path(@project.group) - = _('Group masters can register group runners in the %{link}').html_safe % { link: group_link } + = _('Group maintainers can register group runners in the %{link}').html_safe % { link: group_link } - else - = _('Ask your group master to setup a group Runner.') + = _('Ask your group maintainer to setup a group Runner.') - else %h4.underlined-title diff --git a/app/views/projects/services/mattermost_slash_commands/_help.html.haml b/app/views/projects/services/mattermost_slash_commands/_help.html.haml index 62bef77be97..b20614dc88f 100644 --- a/app/views/projects/services/mattermost_slash_commands/_help.html.haml +++ b/app/views/projects/services/mattermost_slash_commands/_help.html.haml @@ -1,18 +1,19 @@ - enabled = Gitlab.config.mattermost.enabled -.card - %p - This service allows users to perform common operations on this - project by entering slash commands in Mattermost. - = link_to help_page_path('user/project/integrations/mattermost_slash_commands.md'), target: '_blank' do - View documentation - = icon('external-link') - %p.inline - See list of available commands in Mattermost after setting up this service, - by entering - %kbd.inline /<trigger> help - - unless enabled || @service.template? - = render 'projects/services/mattermost_slash_commands/detailed_help', subject: @service +.info-well + .well-segment + %p + This service allows users to perform common operations on this + project by entering slash commands in Mattermost. + = link_to help_page_path('user/project/integrations/mattermost_slash_commands.md'), target: '_blank' do + View documentation + = icon('external-link') + %p.inline + See list of available commands in Mattermost after setting up this service, + by entering + %kbd.inline /<trigger> help + - unless enabled || @service.template? + = render 'projects/services/mattermost_slash_commands/detailed_help', subject: @service - if enabled && !@service.template? = render 'projects/services/mattermost_slash_commands/installation_info', subject: @service diff --git a/app/views/projects/services/prometheus/_configuration_banner.html.haml b/app/views/projects/services/prometheus/_configuration_banner.html.haml index 2cc2a6b2b5b..898b55e4b39 100644 --- a/app/views/projects/services/prometheus/_configuration_banner.html.haml +++ b/app/views/projects/services/prometheus/_configuration_banner.html.haml @@ -2,7 +2,7 @@ = s_('PrometheusService|Auto configuration') - if service.manual_configuration? - .well + .info-well = s_('PrometheusService|To enable the installation of Prometheus on your clusters, deactivate the manual configuration below') - else .container-fluid diff --git a/app/views/projects/services/prometheus/_help.html.haml b/app/views/projects/services/prometheus/_help.html.haml index 15e7362c2ba..35d655e4b32 100644 --- a/app/views/projects/services/prometheus/_help.html.haml +++ b/app/views/projects/services/prometheus/_help.html.haml @@ -5,5 +5,5 @@ = s_('PrometheusService|Manual configuration') - unless @service.editable? - .card + .info-well = s_('PrometheusService|To enable manual configuration, uninstall Prometheus from your clusters') diff --git a/app/views/projects/services/slack_slash_commands/_help.html.haml b/app/views/projects/services/slack_slash_commands/_help.html.haml index 44f58ad05e5..9d045d84b52 100644 --- a/app/views/projects/services/slack_slash_commands/_help.html.haml +++ b/app/views/projects/services/slack_slash_commands/_help.html.haml @@ -1,99 +1,100 @@ - pretty_name = defined?(@project) ? @project.full_name : 'namespace / path' - run_actions_text = "Perform common operations on GitLab project: #{pretty_name}" -.card - %p - This service allows users to perform common operations on this - project by entering slash commands in Slack. - = link_to help_page_path('user/project/integrations/slack_slash_commands.md'), target: '_blank' do - View documentation - = icon('external-link') - %p.inline - See list of available commands in Slack after setting up this service, - by entering - %kbd.inline /<command> help - - unless @service.template? - %p To setup this service: - %ul.list-unstyled.indent-list - %li - 1. - = link_to 'https://my.slack.com/services/new/slash-commands', target: '_blank', rel: 'noreferrer noopener nofollow' do - Add a slash command - = icon('external-link') - in your Slack team with these options: +.info-well + .well-segment + %p + This service allows users to perform common operations on this + project by entering slash commands in Slack. + = link_to help_page_path('user/project/integrations/slack_slash_commands.md'), target: '_blank' do + View documentation + = icon('external-link') + %p.inline + See list of available commands in Slack after setting up this service, + by entering + %kbd.inline /<command> help + - unless @service.template? + %p To setup this service: + %ul.list-unstyled.indent-list + %li + 1. + = link_to 'https://my.slack.com/services/new/slash-commands', target: '_blank', rel: 'noreferrer noopener nofollow' do + Add a slash command + = icon('external-link') + in your Slack team with these options: - %hr + %hr - .help-form - .form-group - = label_tag nil, 'Command', class: 'col-sm-2 col-12 col-form-label' - .col-sm-10.col-12.text-block - %p Fill in the word that works best for your team. - %p - Suggestions: - %code= 'gitlab' - %code= @project.path # Path contains no spaces, but dashes - %code= @project.full_path + .help-form + .form-group + = label_tag nil, 'Command', class: 'col-sm-2 col-12 col-form-label' + .col-sm-10.col-12.text-block + %p Fill in the word that works best for your team. + %p + Suggestions: + %code= 'gitlab' + %code= @project.path # Path contains no spaces, but dashes + %code= @project.full_path - .form-group - = label_tag :url, 'URL', class: 'col-sm-2 col-12 col-form-label' - .col-sm-10.col-12.input-group - = text_field_tag :url, service_trigger_url(subject), class: 'form-control form-control-sm', readonly: 'readonly' - .input-group-append - = clipboard_button(target: '#url', class: 'input-group-text') + .form-group + = label_tag :url, 'URL', class: 'col-sm-2 col-12 col-form-label' + .col-sm-10.col-12.input-group + = text_field_tag :url, service_trigger_url(subject), class: 'form-control form-control-sm', readonly: 'readonly' + .input-group-append + = clipboard_button(target: '#url', class: 'input-group-text') - .form-group - = label_tag nil, 'Method', class: 'col-sm-2 col-12 col-form-label' - .col-sm-10.col-12.text-block POST + .form-group + = label_tag nil, 'Method', class: 'col-sm-2 col-12 col-form-label' + .col-sm-10.col-12.text-block POST - .form-group - = label_tag :customize_name, 'Customize name', class: 'col-sm-2 col-12 col-form-label' - .col-sm-10.col-12.input-group - = text_field_tag :customize_name, 'GitLab', class: 'form-control form-control-sm', readonly: 'readonly' - .input-group-append - = clipboard_button(target: '#customize_name', class: 'input-group-text') + .form-group + = label_tag :customize_name, 'Customize name', class: 'col-sm-2 col-12 col-form-label' + .col-sm-10.col-12.input-group + = text_field_tag :customize_name, 'GitLab', class: 'form-control form-control-sm', readonly: 'readonly' + .input-group-append + = clipboard_button(target: '#customize_name', class: 'input-group-text') - .form-group - = label_tag nil, 'Customize icon', class: 'col-sm-2 col-12 col-form-label' - .col-sm-10.col-12.text-block - = image_tag(asset_url('slash-command-logo.png'), width: 36, height: 36) - = link_to('Download image', asset_url('gitlab_logo.png'), class: 'btn btn-sm', target: '_blank', rel: 'noopener noreferrer') + .form-group + = label_tag nil, 'Customize icon', class: 'col-sm-2 col-12 col-form-label' + .col-sm-10.col-12.text-block + = image_tag(asset_url('slash-command-logo.png'), width: 36, height: 36) + = link_to('Download image', asset_url('gitlab_logo.png'), class: 'btn btn-sm', target: '_blank', rel: 'noopener noreferrer') - .form-group - = label_tag nil, 'Autocomplete', class: 'col-sm-2 col-12 col-form-label' - .col-sm-10.col-12.text-block Show this command in the autocomplete list + .form-group + = label_tag nil, 'Autocomplete', class: 'col-sm-2 col-12 col-form-label' + .col-sm-10.col-12.text-block Show this command in the autocomplete list - .form-group - = label_tag :autocomplete_description, 'Autocomplete description', class: 'col-sm-2 col-12 col-form-label' - .col-sm-10.col-12.input-group - = text_field_tag :autocomplete_description, run_actions_text, class: 'form-control form-control-sm', readonly: 'readonly' - .input-group-append - = clipboard_button(target: '#autocomplete_description', class: 'input-group-text') + .form-group + = label_tag :autocomplete_description, 'Autocomplete description', class: 'col-sm-2 col-12 col-form-label' + .col-sm-10.col-12.input-group + = text_field_tag :autocomplete_description, run_actions_text, class: 'form-control form-control-sm', readonly: 'readonly' + .input-group-append + = clipboard_button(target: '#autocomplete_description', class: 'input-group-text') - .form-group - = label_tag :autocomplete_usage_hint, 'Autocomplete usage hint', class: 'col-sm-2 col-12 col-form-label' - .col-sm-10.col-12.input-group - = text_field_tag :autocomplete_usage_hint, '[help]', class: 'form-control form-control-sm', readonly: 'readonly' - .input-group-append - = clipboard_button(target: '#autocomplete_usage_hint', class: 'input-group-text') + .form-group + = label_tag :autocomplete_usage_hint, 'Autocomplete usage hint', class: 'col-sm-2 col-12 col-form-label' + .col-sm-10.col-12.input-group + = text_field_tag :autocomplete_usage_hint, '[help]', class: 'form-control form-control-sm', readonly: 'readonly' + .input-group-append + = clipboard_button(target: '#autocomplete_usage_hint', class: 'input-group-text') - .form-group - = label_tag :descriptive_label, 'Descriptive label', class: 'col-sm-2 col-12 col-form-label' - .col-sm-10.col-12.input-group - = text_field_tag :descriptive_label, 'Perform common operations on GitLab project', class: 'form-control form-control-sm', readonly: 'readonly' - .input-group-append - = clipboard_button(target: '#descriptive_label', class: 'input-group-text') + .form-group + = label_tag :descriptive_label, 'Descriptive label', class: 'col-sm-2 col-12 col-form-label' + .col-sm-10.col-12.input-group + = text_field_tag :descriptive_label, 'Perform common operations on GitLab project', class: 'form-control form-control-sm', readonly: 'readonly' + .input-group-append + = clipboard_button(target: '#descriptive_label', class: 'input-group-text') - %hr + %hr - %ul.list-unstyled.indent-list - %li - 2. Paste the - %strong Token - into the field below - %li - 3. Select the - %strong Active - checkbox, press - %strong Save changes - and start using GitLab inside Slack! + %ul.list-unstyled.indent-list + %li + 2. Paste the + %strong Token + into the field below + %li + 3. Select the + %strong Active + checkbox, press + %strong Save changes + and start using GitLab inside Slack! diff --git a/app/views/projects/settings/ci_cd/_autodevops_form.html.haml b/app/views/projects/settings/ci_cd/_autodevops_form.html.haml index bbabb98dafe..4359362bb05 100644 --- a/app/views/projects/settings/ci_cd/_autodevops_form.html.haml +++ b/app/views/projects/settings/ci_cd/_autodevops_form.html.haml @@ -1,43 +1,66 @@ -.row.prepend-top-default +.row .col-lg-12 = form_for @project, url: project_settings_ci_cd_path(@project) do |f| = form_errors(@project) - %fieldset.builds-feature + %fieldset.builds-feature.js-auto-devops-settings .form-group - message = auto_devops_warning_message(@project) - ci_file_formatted = '<code>.gitlab-ci.yml</code>'.html_safe - if message - %p.settings-message.text-center + %p.auto-devops-warning-message.settings-message.text-center = message.html_safe = f.fields_for :auto_devops_attributes, @auto_devops do |form| - .form-check - = form.radio_button :enabled, 'true', class: 'form-check-input' - = form.label :enabled_true, class: 'form-check-label' do - %strong= s_('CICD|Enable Auto DevOps') - %br - = s_('CICD|The Auto DevOps pipeline configuration will be used when there is no %{ci_file} in the project.').html_safe % { ci_file: ci_file_formatted } + .card.auto-devops-card + .card-body + .form-check + = form.radio_button :enabled, 'true', class: 'form-check-input js-toggle-extra-settings' + = form.label :enabled_true, class: 'form-check-label' do + %strong= s_('CICD|Enable Auto DevOps') + .form-text.text-muted + = s_('CICD|The Auto DevOps pipeline configuration will be used when there is no %{ci_file} in the project.').html_safe % { ci_file: ci_file_formatted } - .form-check - = form.radio_button :enabled, 'false', class: 'form-check-input' - = form.label :enabled_false, class: 'form-check-label' do - %strong= s_('CICD|Disable Auto DevOps') - %br - = s_('CICD|An explicit %{ci_file} needs to be specified before you can begin using Continuous Integration and Delivery.').html_safe % { ci_file: ci_file_formatted } + .card.auto-devops-card + .card-body + .form-check + = form.radio_button :enabled, '', class: 'form-check-input js-toggle-extra-settings' + = form.label :enabled_, class: 'form-check-label' do + %strong= s_('CICD|Instance default (%{state})') % { state: "#{Gitlab::CurrentSettings.auto_devops_enabled? ? _('enabled') : _('disabled')}" } + .form-text.text-muted + = s_('CICD|Follow the instance default to either have Auto DevOps enabled or disabled when there is no project specific %{ci_file}.').html_safe % { ci_file: ci_file_formatted } - .form-check - = form.radio_button :enabled, '', class: 'form-check-input' - = form.label :enabled_, class: 'form-check-label' do - %strong= s_('CICD|Instance default (%{state})') % { state: "#{Gitlab::CurrentSettings.auto_devops_enabled? ? _('enabled') : _('disabled')}" } - %br - = s_('CICD|Follow the instance default to either have Auto DevOps enabled or disabled when there is no project specific %{ci_file}.').html_safe % { ci_file: ci_file_formatted } + .card.auto-devops-card.js-extra-settings{ class: form.object&.enabled == false ? 'hidden' : nil } + .card-body.bg-light + = form.label :domain do + %strong= _('Domain') + = form.text_field :domain, class: 'form-control', placeholder: 'domain.com' + .form-text.text-muted + = s_('CICD|You need to specify a domain if you want to use Auto Review Apps and Auto Deploy stages.') + - if cluster_ingress_ip = cluster_ingress_ip(@project) + = s_('%{nip_domain} can be used as an alternative to a custom domain.').html_safe % { nip_domain: "<code>#{cluster_ingress_ip}.nip.io</code>".html_safe } + = link_to icon('question-circle'), help_page_path('topics/autodevops/index.md', anchor: 'auto-devops-base-domain'), target: '_blank' - = form.label :domain, class:"prepend-top-10" do - = _('Domain') - = form.text_field :domain, class: 'form-control', placeholder: 'domain.com' - .form-text.text-muted - = s_('CICD|A domain is required to use Auto Review Apps and Auto Deploy Stages.') - - if cluster_ingress_ip = cluster_ingress_ip(@project) - = s_('%{nip_domain} can be used as an alternative to a custom domain.').html_safe % { nip_domain: "<code>#{cluster_ingress_ip}.nip.io</code>".html_safe } - = link_to icon('question-circle'), help_page_path('topics/autodevops/index.md', anchor: 'auto-devops-base-domain'), target: '_blank' + %label.prepend-top-10 + %strong= s_('CICD|Deployment strategy') + %p.settings-message.text-center + = s_('CICD|Deployment strategy needs a domain name to work correctly.') + .form-check + = form.radio_button :deploy_strategy, 'continuous', class: 'form-check-input' + = form.label :deploy_strategy_continuous, class: 'form-check-label' do + %strong= s_('CICD|Continuous deployment to production') + = link_to icon('question-circle'), help_page_path('topics/autodevops/index.md', anchor: 'auto-deploy'), target: '_blank' + .form-check + = form.radio_button :deploy_strategy, 'manual', class: 'form-check-input' + = form.label :deploy_strategy_manual, class: 'form-check-label' do + %strong= s_('CICD|Automatic deployment to staging, manual deployment to production') + = link_to icon('question-circle'), help_page_path('ci/environments.md', anchor: 'manually-deploying-to-environments'), target: '_blank' + + .card.auto-devops-card + .card-body + .form-check + = form.radio_button :enabled, 'false', class: 'form-check-input js-toggle-extra-settings', data: { hide_extra_settings: true } + = form.label :enabled_false, class: 'form-check-label' do + %strong= s_('CICD|Disable Auto DevOps') + .form-text.text-muted + = s_('CICD|An explicit %{ci_file} needs to be specified before you can begin using Continuous Integration and Delivery.').html_safe % { ci_file: ci_file_formatted } = f.submit 'Save changes', class: "btn btn-success prepend-top-15" diff --git a/app/views/projects/settings/ci_cd/_form.html.haml b/app/views/projects/settings/ci_cd/_form.html.haml index e93b240a007..7142a9d635e 100644 --- a/app/views/projects/settings/ci_cd/_form.html.haml +++ b/app/views/projects/settings/ci_cd/_form.html.haml @@ -7,7 +7,7 @@ = f.label :runners_token, "Runner token", class: 'label-light' .form-control.js-secret-value-placeholder = '*' * 20 - = f.text_field :runners_token, class: "form-control hidden js-secret-value", placeholder: 'xEeFCaDAB89' + = f.text_field :runners_token, class: "form-control hide js-secret-value", placeholder: 'xEeFCaDAB89' %p.form-text.text-muted The secure token used by the Runner to checkout the project %button.btn.btn-info.prepend-top-10.js-secret-value-reveal-button{ type: 'button', data: { secret_reveal_status: 'false' } } = _('Reveal value') diff --git a/app/views/projects/settings/ci_cd/show.html.haml b/app/views/projects/settings/ci_cd/show.html.haml index ed17bd4f7dc..56c175f5649 100644 --- a/app/views/projects/settings/ci_cd/show.html.haml +++ b/app/views/projects/settings/ci_cd/show.html.haml @@ -8,7 +8,7 @@ %section.settings#js-general-pipeline-settings.no-animate{ class: ('expanded' if general_expanded) } .settings-header %h4 - General pipelines settings + General pipelines %button.btn.js-settings-toggle{ type: 'button' } = expanded ? 'Collapse' : 'Expand' %p @@ -16,7 +16,7 @@ .settings-content = render 'form' -%section.settings#autodevops-settings.no-animate{ class: ('expanded' if expanded) } +%section.qa-autodevops-settings.settings#autodevops-settings.no-animate{ class: ('expanded' if expanded) } .settings-header %h4 = s_('CICD|Auto DevOps') @@ -28,10 +28,10 @@ .settings-content = render 'autodevops_form' -%section.settings.no-animate{ class: ('expanded' if expanded) } +%section.qa-runners-settings.settings.no-animate{ class: ('expanded' if expanded) } .settings-header %h4 - Runners settings + Runners %button.btn.js-settings-toggle{ type: 'button' } = expanded ? 'Collapse' : 'Expand' %p @@ -39,11 +39,11 @@ .settings-content = render 'projects/runners/index' -%section.settings.no-animate{ class: ('expanded' if expanded) } +%section.qa-variables-settings.settings.no-animate{ class: ('expanded' if expanded) } .settings-header %h4 = _('Variables') - = link_to icon('question-circle'), help_page_path('ci/variables/README', anchor: 'secret-variables'), target: '_blank', rel: 'noopener noreferrer' + = link_to icon('question-circle'), help_page_path('ci/variables/README', anchor: 'variables'), target: '_blank', rel: 'noopener noreferrer' %button.btn.js-settings-toggle{ type: 'button' } = expanded ? 'Collapse' : 'Expand' %p.append-bottom-0 diff --git a/app/views/projects/tags/new.html.haml b/app/views/projects/tags/new.html.haml index b596748ca5f..da822ac5675 100644 --- a/app/views/projects/tags/new.html.haml +++ b/app/views/projects/tags/new.html.haml @@ -36,7 +36,7 @@ = label_tag :release_description, s_('TagsPage|Release notes'), class: 'col-form-label col-sm-2' .col-sm-10 = render layout: 'projects/md_preview', locals: { url: preview_markdown_path(@project), referenced_users: true } do - = render 'projects/zen', attr: :release_description, classes: 'note-textarea', placeholder: s_('TagsPage|Write your release notes or drag files here...'), current_text: @release_description + = render 'projects/zen', attr: :release_description, classes: 'note-textarea', placeholder: s_('TagsPage|Write your release notes or drag files here…'), current_text: @release_description = render 'shared/notes/hints' .form-text.text-muted = s_('TagsPage|Optionally, add release notes to the tag. They will be stored in the GitLab database and displayed on the tags page.') diff --git a/app/views/projects/tree/_tree_header.html.haml b/app/views/projects/tree/_tree_header.html.haml index 4fc1a284693..9d196075bf1 100644 --- a/app/views/projects/tree/_tree_header.html.haml +++ b/app/views/projects/tree/_tree_header.html.haml @@ -6,7 +6,7 @@ = render 'shared/ref_switcher', destination: 'tree', path: @path, show_create: true - if on_top_of_branch? - - addtotree_toggle_attributes = { href: '#', 'data-toggle': 'dropdown', 'data-target': '.add-to-tree-dropdown' } + - addtotree_toggle_attributes = { href: '#', 'data-toggle': 'dropdown', 'data-target': '.add-to-tree-dropdown', 'data-boundary': 'window' } - else - addtotree_toggle_attributes = { title: _("You can only add files when you are on a branch"), data: { container: 'body' }, class: 'disabled has-tooltip' } diff --git a/app/views/projects/wikis/_form.html.haml b/app/views/projects/wikis/_form.html.haml index bcceb69954a..26fe1de31fe 100644 --- a/app/views/projects/wikis/_form.html.haml +++ b/app/views/projects/wikis/_form.html.haml @@ -25,7 +25,7 @@ .col-sm-12= f.label :content, class: 'control-label-full-width' .col-sm-12 = render layout: 'projects/md_preview', locals: { url: project_wiki_preview_markdown_path(@project, @page.slug) } do - = render 'projects/zen', f: f, attr: :content, classes: 'note-textarea', placeholder: s_("WikiPage|Write your content or drag files here...") + = render 'projects/zen', f: f, attr: :content, classes: 'note-textarea', placeholder: s_("WikiPage|Write your content or drag files here…") = render 'shared/notes/hints' .clearfix diff --git a/app/views/projects/wikis/edit.html.haml b/app/views/projects/wikis/edit.html.haml index 35c7dc2984a..d80d2957466 100644 --- a/app/views/projects/wikis/edit.html.haml +++ b/app/views/projects/wikis/edit.html.haml @@ -1,4 +1,4 @@ -- @content_class = "limit-container-width limit-container-width-sm" unless fluid_layout +- @content_class = "limit-container-width" unless fluid_layout - page_title _("Edit"), @page.title.capitalize, _("Wiki") = wiki_page_errors(@error) diff --git a/app/views/projects/wikis/git_access.html.haml b/app/views/projects/wikis/git_access.html.haml index 909987d8090..8c2cbd495a0 100644 --- a/app/views/projects/wikis/git_access.html.haml +++ b/app/views/projects/wikis/git_access.html.haml @@ -1,4 +1,4 @@ -- @content_class = "limit-container-width limit-container-width-sm" unless fluid_layout +- @content_class = "limit-container-width" unless fluid_layout - page_title s_("WikiClone|Git Access"), _("Wiki") .wiki-page-header.has-sidebar-toggle diff --git a/app/views/projects/wikis/show.html.haml b/app/views/projects/wikis/show.html.haml index ff72c8bb75d..a08973c7f32 100644 --- a/app/views/projects/wikis/show.html.haml +++ b/app/views/projects/wikis/show.html.haml @@ -1,4 +1,4 @@ -- @content_class = "limit-container-width limit-container-width-sm" unless fluid_layout +- @content_class = "limit-container-width" unless fluid_layout - breadcrumb_title @page.title.capitalize - wiki_breadcrumb_dropdown_links(@page.slug) - page_title @page.title.capitalize, _("Wiki") diff --git a/app/views/search/results/_blob.html.haml b/app/views/search/results/_blob.html.haml index de473c23d66..fdcd126e7a3 100644 --- a/app/views/search/results/_blob.html.haml +++ b/app/views/search/results/_blob.html.haml @@ -1,13 +1,5 @@ -- file_name, blob = blob -.blob-result - .file-holder - .js-file-title.file-title - - ref = @search_results.repository_ref - - blob_link = project_blob_path(@project, tree_join(ref, file_name)) - = link_to blob_link do - %i.fa.fa-file - %strong - = file_name - - if blob - .file-content.code.term - = render 'shared/file_highlight', blob: blob, first_line_number: blob.startline, blob_link: blob_link +- project = find_project_for_result_blob(blob) +- file_name, blob = parse_search_result(blob) +- blob_link = project_blob_path(project, tree_join(blob.ref, file_name)) + += render partial: 'search/results/blob_data', locals: { blob: blob, project: project, file_name: file_name, blob_link: blob_link } diff --git a/app/views/search/results/_blob_data.html.haml b/app/views/search/results/_blob_data.html.haml new file mode 100644 index 00000000000..143e9f91ca3 --- /dev/null +++ b/app/views/search/results/_blob_data.html.haml @@ -0,0 +1,10 @@ +.blob-result + .file-holder + .js-file-title.file-title + = link_to blob_link do + %i.fa.fa-file + %strong + = search_blob_title(project, file_name) + - if blob.data + .file-content.code.term + = render 'shared/file_highlight', blob: blob, first_line_number: blob.startline diff --git a/app/views/search/results/_commit.html.haml b/app/views/search/results/_commit.html.haml index f34eaf89027..ed5a3badf11 100644 --- a/app/views/search/results/_commit.html.haml +++ b/app/views/search/results/_commit.html.haml @@ -1 +1 @@ -= render 'projects/commits/commit', project: @project, commit: commit, ref: nil += render 'projects/commits/commit', project: commit.project, commit: commit, ref: nil, show_project_name: @project.nil? diff --git a/app/views/search/results/_wiki_blob.html.haml b/app/views/search/results/_wiki_blob.html.haml index 16a0e432d62..4346217c230 100644 --- a/app/views/search/results/_wiki_blob.html.haml +++ b/app/views/search/results/_wiki_blob.html.haml @@ -1,10 +1,5 @@ -- wiki_blob = parse_search_result(wiki_blob) -.blob-result - .file-holder - .js-file-title.file-title - = link_to project_wiki_path(@project, wiki_blob.basename) do - %i.fa.fa-file - %strong - = wiki_blob.basename - .file-content.code.term - = render 'shared/file_highlight', blob: wiki_blob, first_line_number: wiki_blob.startline +- project = find_project_for_result_blob(wiki_blob) +- file_name, wiki_blob = parse_search_result(wiki_blob) +- wiki_blob_link = project_wiki_path(project, wiki_blob.basename) + += render partial: 'search/results/blob_data', locals: { blob: wiki_blob, project: project, file_name: file_name, blob_link: wiki_blob_link } diff --git a/app/views/shared/_clone_panel.html.haml b/app/views/shared/_clone_panel.html.haml index 5fc02ba3160..3655c2a1d42 100644 --- a/app/views/shared/_clone_panel.html.haml +++ b/app/views/shared/_clone_panel.html.haml @@ -11,7 +11,7 @@ %span = default_clone_protocol.upcase = icon('caret-down') - %ul.dropdown-menu.dropdown-menu-selectable.dropdown-menu-right.clone-options-dropdown + %ul.dropdown-menu.dropdown-menu-selectable.clone-options-dropdown %li = ssh_clone_button(project) %li diff --git a/app/views/shared/_field.html.haml b/app/views/shared/_field.html.haml index 0c8d90d92f5..b89045e726a 100644 --- a/app/views/shared/_field.html.haml +++ b/app/views/shared/_field.html.haml @@ -9,11 +9,11 @@ - help = field[:help] - disabled = disable_fields_service?(@service) -.form-group +.form-group.row - if type == "password" && value.present? - = form.label name, "Enter new #{title.downcase}", class: "col-form-label" + = form.label name, "Enter new #{title.downcase}", class: "col-form-label col-sm-2" - else - = form.label name, title, class: "col-form-label" + = form.label name, title, class: "col-form-label col-sm-2" .col-sm-10 - if type == 'text' = form.text_field name, class: "form-control", placeholder: placeholder, required: required, disabled: disabled diff --git a/app/views/shared/_label.html.haml b/app/views/shared/_label.html.haml index ba5b65a209d..5eec7b02b54 100644 --- a/app/views/shared/_label.html.haml +++ b/app/views/shared/_label.html.haml @@ -1,93 +1,70 @@ - label_css_id = dom_id(label) - status = label_subscription_status(label, @project).inquiry if current_user - subject = local_assigns[:subject] +- use_label_priority = local_assigns.fetch(:use_label_priority, false) +- force_priority = local_assigns.fetch(:force_priority, use_label_priority ? label.priority.present? : false) - toggle_subscription_path = toggle_subscription_label_path(label, @project) if current_user - show_label_merge_requests_link = show_label_issuables_link?(label, :merge_requests, project: @project) - show_label_issues_link = show_label_issuables_link?(label, :issues, project: @project) +- tooltip_title = label_status_tooltip(label, status) if status %li.label-list-item{ id: label_css_id, data: { id: label.id } } - = render "shared/label_row", label: label - - .d-inline-block.d-sm-none.dropdown - %button.btn.btn-default.label-options-toggle{ type: 'button', data: { toggle: "dropdown" } } - Options - = icon('caret-down') - .dropdown-menu.dropdown-menu-right - %ul - - if show_label_merge_requests_link - %li - = link_to_label(label, subject: subject, type: :merge_request) do - View merge requests - - if show_label_issues_link - %li - = link_to_label(label, subject: subject) do - View open issues - - if current_user - %li.label-subscription - - if can_subscribe_to_label_in_different_levels?(label) - %a.js-unsubscribe-button.label-subscribe-button{ role: 'button', href: '#', class: ('hidden' if status.unsubscribed?), data: { url: toggle_subscription_path } } - %span Unsubscribe - %a.js-subscribe-button.label-subscribe-button{ role: 'button', href: '#', class: ('hidden' unless status.unsubscribed?), data: { url: toggle_subscription_project_label_path(@project, label) } } - %span Subscribe at project level - %a.js-subscribe-button.label-subscribe-button{ role: 'button', href: '#', class: ('hidden' unless status.unsubscribed?), data: { url: toggle_subscription_group_label_path(label.group, label) } } - %span Subscribe at group level - - else - %a.js-subscribe-button.label-subscribe-button{ role: 'button', href: '#', data: { status: status, url: toggle_subscription_path } } - %span= label_subscription_toggle_button_text(label, @project) - - - if can?(current_user, :admin_label, label) - %li - = link_to 'Edit', edit_label_path(label) - %li - = link_to 'Delete', - destroy_label_path(label), - title: 'Delete', - method: :delete, - data: {confirm: 'Remove this label? Are you sure?'}, - class: 'text-danger' - - .float-right.d-none.d-sm-none.d-md-block - - if can?(current_user, :admin_label, label) - - if label.is_a?(ProjectLabel) && label.project.group && can?(current_user, :admin_label, label.project.group) - %button.js-promote-project-label-button.btn.btn-transparent.btn-action.has-tooltip{ title: _('Promote to Group Label'), - disabled: true, - type: 'button', - data: { url: promote_project_label_path(label.project, label), - label_title: label.title, - label_color: label.color, - label_text_color: label.text_color, - group_name: label.project.group.name, - target: '#promote-label-modal', - container: 'body', - toggle: 'modal' } } - = sprite_icon('level-up') - = link_to edit_label_path(label), title: "Edit", class: 'btn btn-transparent btn-action', data: {toggle: "tooltip"} do - %span.sr-only Edit - = sprite_icon('pencil') - %span{ data: { toggle: 'modal', target: "#modal-delete-label-#{label.id}" } } - = link_to "#", title: "Delete", class: 'btn btn-transparent btn-action remove-row', data: { toggle: "tooltip" } do - %span.sr-only Delete - = sprite_icon('remove') + = render "shared/label_row", label: label, subject: subject, force_priority: force_priority + %ul.label-actions-list + - if @project + %li.inline + .label-badge.label-badge-gray= label.model_name.human.capitalize + - if can?(current_user, :admin_label, @project) + %li.inline.js-toggle-priority{ data: { url: remove_priority_project_label_path(@project, label), + dom_id: dom_id(label), type: label.type } } + %button.label-action.add-priority.btn.btn-transparent.has-tooltip{ title: _('Prioritize'), type: 'button', data: { placement: 'top' }, aria_label: _('Prioritize label') } + = sprite_icon('star-o') + %button.label-action.remove-priority.btn.btn-transparent.has-tooltip{ title: _('Remove priority'), type: 'button', data: { placement: 'top' }, aria_label: _('Deprioritize label') } + = sprite_icon('star') + %li.inline + = link_to edit_label_path(label), class: 'btn btn-transparent label-action', aria_label: 'Edit label' do + = sprite_icon('pencil') + %li.inline + .dropdown + %button{ type: 'button', class: 'btn btn-transparent js-label-options-dropdown label-action', data: { toggle: 'dropdown' }, aria_label: _('Label actions dropdown') } + = sprite_icon('ellipsis_v') + .dropdown-menu.dropdown-open-left + %ul + - if label.is_a?(ProjectLabel) && label.project.group && can?(current_user, :admin_label, label.project.group) + %li + %button.js-promote-project-label-button.btn.btn-transparent.btn-action{ disabled: true, type: 'button', + data: { url: promote_project_label_path(label.project, label), + label_title: label.title, + label_color: label.color, + label_text_color: label.text_color, + group_name: label.project.group.name, + target: '#promote-label-modal', + container: 'body', + toggle: 'modal' } } + = _('Promote to group label') + %li + %span{ data: { toggle: 'modal', target: "#modal-delete-label-#{label.id}" } } + %button.text-danger.remove-row{ type: 'button' }= _('Delete') - if current_user - .label-subscription.inline + %li.inline.label-subscription - if can_subscribe_to_label_in_different_levels?(label) - %button.js-unsubscribe-button.label-subscribe-button.btn.btn-default{ type: 'button', class: ('hidden' if status.unsubscribed?), data: { url: toggle_subscription_path } } - %span Unsubscribe - = icon('spinner spin', class: 'label-subscribe-button-loading') - + %button.js-unsubscribe-button.label-subscribe-button.btn.btn-default{ class: ('hidden' if status.unsubscribed?), data: { url: toggle_subscription_path, toggle: 'tooltip' }, title: tooltip_title } + %span= _('Unsubscribe') .dropdown.dropdown-group-label{ class: ('hidden' unless status.unsubscribed?) } - %button.dropdown-menu-toggle{ type: 'button', 'data-toggle' => 'dropdown' } - %span Subscribe - = icon('chevron-down') - %ul.dropdown-menu - %li - %a.js-subscribe-button{ class: ('hidden' unless status.unsubscribed?), data: { url: toggle_subscription_project_label_path(@project, label) } } - Project level - %a.js-subscribe-button{ class: ('hidden' unless status.unsubscribed?), data: { url: toggle_subscription_group_label_path(label.group, label) } } - Group level + %button.label-subscribe-button.btn.btn-default{ data: { toggle: 'dropdown' } } + %span + = _('Subscribe') + = sprite_icon('chevron-down') + .dropdown-menu.dropdown-open-left + %ul + %li + %button.js-subscribe-button.label-subscribe-button.btn.btn-default{ class: ('hidden' unless status.unsubscribed?), data: { status: status, url: toggle_subscription_project_label_path(@project, label) } } + %span= _('Subscribe at project level') + %li + %button.js-subscribe-button.js-group-level.label-subscribe-button.btn.btn-default{ class: ('hidden' unless status.unsubscribed?), data: { status: status, url: toggle_subscription_group_label_path(label.group, label) } } + %span= _('Subscribe at group level') - else - %button.js-subscribe-button.label-subscribe-button.btn.btn-default{ type: 'button', data: { status: status, url: toggle_subscription_path } } + %button.js-subscribe-button.label-subscribe-button.btn.btn-default{ data: { status: status, url: toggle_subscription_path, toggle: 'tooltip' }, title: tooltip_title } %span= label_subscription_toggle_button_text(label, @project) - = icon('spinner spin', class: 'label-subscribe-button-loading') = render 'shared/delete_label_modal', label: label diff --git a/app/views/shared/_label_row.html.haml b/app/views/shared/_label_row.html.haml index f1c1ca9b2c9..0ae3ab8f090 100644 --- a/app/views/shared/_label_row.html.haml +++ b/app/views/shared/_label_row.html.haml @@ -1,30 +1,23 @@ - subject = local_assigns[:subject] +- force_priority = local_assigns.fetch(:force_priority, false) - show_label_issues_link = show_label_issuables_link?(label, :issues, project: @project) - show_label_merge_requests_link = show_label_issuables_link?(label, :merge_requests, project: @project) -%span.label-row - - if can?(current_user, :admin_label, @project) - .draggable-handler - = icon('bars') - .js-toggle-priority.toggle-priority{ data: { url: remove_priority_project_label_path(@project, label), - dom_id: dom_id(label), type: label.type } } - %button.add-priority.btn.has-tooltip{ title: 'Prioritize', type: 'button', :'data-placement' => 'top' } - = icon('star-o') - %button.remove-priority.btn.has-tooltip{ title: 'Remove priority', type: 'button', :'data-placement' => 'top' } - = icon('star') - %span.label-name - = link_to_label(label, subject: @project, tooltip: false) - - if defined?(@project) && @project.group.present? - %span.label-type - = label.model_name.human.titleize - - %span.label-description +.label-name + = link_to_label(label, subject: @project, tooltip: false) +.label-description + .append-right-default.prepend-left-default - if label.description.present? - .description-text + .description-text.append-bottom-10 = markdown_field(label, :description) - .d-none.d-sm-none.d-md-block + %ul.label-links - if show_label_issues_link - = link_to_label(label, subject: subject) { 'Issues' } + %li.label-link-item.inline + = link_to_label(label, subject: subject) { 'Issues' } - if show_label_merge_requests_link · - = link_to_label(label, subject: subject, type: :merge_request) { 'Merge requests' } + %li.label-link-item.inline + = link_to_label(label, subject: subject, type: :merge_request) { _('Merge requests') } + - if force_priority + %li.label-link-item.js-priority-badge.inline.prepend-left-10 + .label-badge.label-badge-blue= _('Prioritized label') diff --git a/app/views/shared/_new_merge_request_checkbox.html.haml b/app/views/shared/_new_merge_request_checkbox.html.haml index 165109b6b70..24c0dfe247f 100644 --- a/app/views/shared/_new_merge_request_checkbox.html.haml +++ b/app/views/shared/_new_merge_request_checkbox.html.haml @@ -1,4 +1,4 @@ -.form-check +.form-check.prepend-top-8 - nonce = SecureRandom.hex = check_box_tag 'create_merge_request', 1, true, class: 'js-create-merge-request form-check-input', id: "create_merge_request-#{nonce}" = label_tag "create_merge_request-#{nonce}", class: 'form-check-label' do diff --git a/app/views/shared/_service_settings.html.haml b/app/views/shared/_service_settings.html.haml index 0ebf365c7bd..6fa61c15493 100644 --- a/app/views/shared/_service_settings.html.haml +++ b/app/views/shared/_service_settings.html.haml @@ -3,8 +3,9 @@ - if lookup_context.template_exists?('help', "projects/services/#{@service.to_param}", true) = render "projects/services/#{@service.to_param}/help", subject: subject - elsif @service.help.present? - .card - = markdown @service.help + .info-well + .well-segment + = markdown @service.help .service-settings - if @service.show_active_box? @@ -15,25 +16,24 @@ - if @service.configurable_events.present? .form-group.row - = form.label :url, "Trigger", class: 'col-form-label col-sm-2' + .col-sm-2.text-right Trigger .col-sm-10 - @service.configurable_events.each do |event| - %div - = form.check_box service_event_field_name(event), class: 'float-left' - .prepend-left-20 - = form.label service_event_field_name(event), class: 'list-label' do + .form-group + .form-check + = form.check_box service_event_field_name(event), class: 'form-check-input' + = form.label service_event_field_name(event), class: 'form-check-label' do %strong = event.humanize - - field = @service.event_field(event) + - field = @service.event_field(event) - - if field - %p + - if field = form.text_field field[:name], class: "form-control", placeholder: field[:placeholder] - %p.light - = @service.class.event_description(event) + %p.text-muted + = @service.class.event_description(event) - @service.global_fields.each do |field| - type = field[:type] diff --git a/app/views/shared/_visibility_level.html.haml b/app/views/shared/_visibility_level.html.haml index d67409ffe14..01ce1225b8d 100644 --- a/app/views/shared/_visibility_level.html.haml +++ b/app/views/shared/_visibility_level.html.haml @@ -1,11 +1,11 @@ - with_label = local_assigns.fetch(:with_label, true) -.form-group.visibility-level-setting +.form-group.row.visibility-level-setting - if with_label = f.label :visibility_level, class: 'col-form-label col-sm-2' do Visibility Level = link_to icon('question-circle'), help_page_path("public_access/public_access") - %div{ :class => ("col-sm-10" if with_label) } + %div{ :class => (with_label ? "col-sm-10" : "col-sm-12") } - if can_change_visibility_level = render('shared/visibility_radios', model_method: :visibility_level, form: f, selected_level: visibility_level, form_model: form_model) - else diff --git a/app/views/shared/boards/components/_board.html.haml b/app/views/shared/boards/components/_board.html.haml index 67476a3f573..76843ce7cc0 100644 --- a/app/views/shared/boards/components/_board.html.haml +++ b/app/views/shared/boards/components/_board.html.haml @@ -1,4 +1,4 @@ -.board{ ":class" => '{ "is-draggable": !list.preset, "is-expandable": list.isExpandable, "is-collapsed": !list.isExpanded }', +.board{ ":class" => '{ "is-draggable": !list.preset, "is-expandable": list.isExpandable, "is-collapsed": !list.isExpanded, "board-type-assignee": list.type === "assignee" }', ":data-id" => "list.id" } .board-inner %header.board-header{ ":class" => '{ "has-border": list.label && list.label.color }', ":style" => "{ borderTopColor: (list.label && list.label.color ? list.label.color : null) }", "@click" => "toggleExpanded($event)" } @@ -7,10 +7,18 @@ ":class": "{ \"fa-caret-down\": list.isExpanded, \"fa-caret-right\": !list.isExpanded }", "aria-hidden": "true" } + %a.user-avatar-link.js-no-trigger{ "v-if": "list.type === \"assignee\"", ":href": "list.assignee.path" } + -# haml-lint:disable AltText + %img.avatar.s20.has-tooltip{ height: "20", width: "20", ":src": "list.assignee.avatar", ":alt": "list.assignee.name" } + %span.board-title-text.has-tooltip{ "v-if": "list.type !== \"label\"", - ":title" => '(list.label ? list.label.description : "")', data: { container: "body" } } + ":title" => '((list.label && list.label.description) || list.title || "")', data: { container: "body" } } {{ list.title }} + %span.board-title-sub-text.prepend-left-5.has-tooltip{ "v-if": "list.type === \"assignee\"", + ":title" => '(list.assignee && list.assignee.username || "")' } + @{{ list.assignee.username }} + %span.has-tooltip{ "v-if": "list.type === \"label\"", ":title" => '(list.label ? list.label.description : "")', data: { container: "body", placement: "bottom" }, diff --git a/app/views/shared/boards/components/sidebar/_labels.html.haml b/app/views/shared/boards/components/sidebar/_labels.html.haml index a112a9f1f7e..daee691e358 100644 --- a/app/views/shared/boards/components/sidebar/_labels.html.haml +++ b/app/views/shared/boards/components/sidebar/_labels.html.haml @@ -9,7 +9,7 @@ None %a{ href: "#", "v-for" => "label in issue.labels" } - %span.badge.color-label.has-tooltip{ ":style" => "{ backgroundColor: label.color, color: label.textColor }" } + .badge.color-label.has-tooltip{ ":style" => "{ backgroundColor: label.color, color: label.textColor }" } {{ label.title }} - if can_admin_issue? .selectbox diff --git a/app/views/shared/builds/_build_output.html.haml b/app/views/shared/builds/_build_output.html.haml new file mode 100644 index 00000000000..0e18128a8f1 --- /dev/null +++ b/app/views/shared/builds/_build_output.html.haml @@ -0,0 +1,3 @@ +%pre.build-trace#build-trace + %code.bash.js-build-output + .build-loader-animation.js-build-refresh diff --git a/app/views/shared/empty_states/_wikis.html.haml b/app/views/shared/empty_states/_wikis.html.haml index fabb1f39a34..f1a41074c28 100644 --- a/app/views/shared/empty_states/_wikis.html.haml +++ b/app/views/shared/empty_states/_wikis.html.haml @@ -8,7 +8,7 @@ %h4 = s_('WikiEmpty|The wiki lets you write documentation for your project') %p.text-left - = s_("WikiEmpty|A wiki is where you can store all the details about your project. This can include why you've created it, it's principles, how to use it, and so on.") + = s_("WikiEmpty|A wiki is where you can store all the details about your project. This can include why you've created it, its principles, how to use it, and so on.") = create_link - elsif can?(current_user, :read_issue, @project) diff --git a/app/views/shared/form_elements/_description.html.haml b/app/views/shared/form_elements/_description.html.haml index e5dfa7dbf71..25df2fe5cd6 100644 --- a/app/views/shared/form_elements/_description.html.haml +++ b/app/views/shared/form_elements/_description.html.haml @@ -16,7 +16,7 @@ = render layout: 'projects/md_preview', locals: { url: preview_url, referenced_users: true } do = render 'projects/zen', f: form, attr: :description, classes: 'note-textarea qa-issuable-form-description', - placeholder: "Write a comment or drag your files here...", + placeholder: "Write a comment or drag your files here…", supports_quick_actions: supports_quick_actions = render 'shared/notes/hints', supports_quick_actions: supports_quick_actions .clearfix diff --git a/app/views/shared/issuable/_board_create_list_dropdown.html.haml b/app/views/shared/issuable/_board_create_list_dropdown.html.haml new file mode 100644 index 00000000000..23b2e1b91e5 --- /dev/null +++ b/app/views/shared/issuable/_board_create_list_dropdown.html.haml @@ -0,0 +1,8 @@ +.dropdown.prepend-left-10#js-add-list + %button.btn.btn-create.btn-inverted.js-new-board-list{ type: "button", data: board_list_data } + Add list + .dropdown-menu.dropdown-menu-paging.dropdown-menu-right.dropdown-menu-issues-board-new.dropdown-menu-selectable.js-tab-container-labels + = render partial: "shared/issuable/label_page_default", locals: { show_footer: true, show_create: true, show_boards_content: true, title: "Add list" } + - if can?(current_user, :admin_label, board.parent) + = render partial: "shared/issuable/label_page_create" + = dropdown_loading diff --git a/app/views/shared/issuable/_label_page_create.html.haml b/app/views/shared/issuable/_label_page_create.html.haml index 9f4021802df..55edaa7eda4 100644 --- a/app/views/shared/issuable/_label_page_create.html.haml +++ b/app/views/shared/issuable/_label_page_create.html.haml @@ -1,6 +1,7 @@ +- show_close = local_assigns.fetch(:show_close, true) - subject = @project || @group .dropdown-page-two.dropdown-new-label - = dropdown_title(create_label_title(subject), options: { back: true }) + = dropdown_title(create_label_title(subject), options: { back: true, close: show_close }) = dropdown_content do .dropdown-labels-error.js-label-error %input#new_label_name.default-dropdown-input{ type: "text", placeholder: _('Name new label') } diff --git a/app/views/shared/issuable/_label_page_default.html.haml b/app/views/shared/issuable/_label_page_default.html.haml index 2bd922bca2b..aa4a5f0e0d3 100644 --- a/app/views/shared/issuable/_label_page_default.html.haml +++ b/app/views/shared/issuable/_label_page_default.html.haml @@ -1,15 +1,18 @@ - title = local_assigns.fetch(:title, _('Assign labels')) +- content_title = local_assigns.fetch(:content_title, _('Create lists from labels. Issues with that label appear in that list.')) +- show_title = local_assigns.fetch(:show_title, true) - show_create = local_assigns.fetch(:show_create, true) - show_footer = local_assigns.fetch(:show_footer, true) - filter_placeholder = local_assigns.fetch(:filter_placeholder, 'Search') - show_boards_content = local_assigns.fetch(:show_boards_content, false) - subject = @project || @group .dropdown-page-one - = dropdown_title(title) + - if show_title + = dropdown_title(title) - if show_boards_content .issue-board-dropdown-content %p - = _('Create lists from labels. Issues with that label appear in that list.') + = content_title = dropdown_filter(filter_placeholder) = dropdown_content - if current_board_parent && show_footer diff --git a/app/views/shared/issuable/_milestone_dropdown.html.haml b/app/views/shared/issuable/_milestone_dropdown.html.haml index 955b8866c2c..37625a4a163 100644 --- a/app/views/shared/issuable/_milestone_dropdown.html.haml +++ b/app/views/shared/issuable/_milestone_dropdown.html.haml @@ -1,6 +1,8 @@ - project = @target_project || @project - extra_class = extra_class || '' - show_menu_above = show_menu_above || false +- selected = local_assigns.fetch(:selected, nil) + - selected_text = selected.try(:title) || params[:milestone_title] - dropdown_title = local_assigns.fetch(:dropdown_title, "Filter by milestone") - if selected.present? || params[:milestone_title].present? diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml index 644f7c4dd28..ef9ea2194ee 100644 --- a/app/views/shared/issuable/_search_bar.html.haml +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -104,14 +104,7 @@ .filter-dropdown-container - if type == :boards - if can?(current_user, :admin_list, board.parent) - .dropdown.prepend-left-10#js-add-list - %button.btn.btn-create.btn-inverted.js-new-board-list{ type: "button", data: board_list_data } - Add list - .dropdown-menu.dropdown-menu-paging.dropdown-menu-right.dropdown-menu-issues-board-new.dropdown-menu-selectable - = render partial: "shared/issuable/label_page_default", locals: { show_footer: true, show_create: true, show_boards_content: true, title: "Add list" } - - if can?(current_user, :admin_label, board.parent) - = render partial: "shared/issuable/label_page_create" - = dropdown_loading + = render_if_exists 'shared/issuable/board_create_list_dropdown', board: board - if @project #js-add-issues-btn.prepend-left-10{ data: { can_admin_list: can?(current_user, :admin_list, @project) } } - elsif type != :boards_modal diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index 9e50e888b35..0ca35ea1298 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -43,7 +43,7 @@ .selectbox.hide-collapsed = f.hidden_field 'milestone_id', value: issuable.milestone_id, id: nil - = dropdown_tag('Milestone', options: { title: _('Assign milestone'), toggle_class: 'js-milestone-select js-extra-options', filter: true, dropdown_class: 'dropdown-menu-selectable', placeholder: _('Search milestones'), data: { show_no: true, field_name: "#{issuable.to_ability_name}[milestone_id]", project_id: @project.id, issuable_id: issuable.id, milestones: project_milestones_path(@project, :json), ability_name: issuable.to_ability_name, issue_update: issuable_json_path(issuable), use_id: true, default_no: true, selected: (issuable.milestone.name if issuable.milestone), null_default: true }}) + = dropdown_tag('Milestone', options: { title: _('Assign milestone'), toggle_class: 'js-milestone-select js-extra-options', filter: true, dropdown_class: 'dropdown-menu-selectable', placeholder: _('Search milestones'), data: { show_no: true, field_name: "#{issuable.to_ability_name}[milestone_id]", project_id: @project.id, issuable_id: issuable.id, milestones: project_milestones_path(@project, :json), ability_name: issuable.to_ability_name, issue_update: issuable_json_path(issuable), use_id: true, default_no: true, selected: (issuable.milestone.name if issuable.milestone), null_default: true, display: 'static' }}) - if issuable.has_attribute?(:time_estimate) #issuable-time-tracker.block // Fallback while content is loading @@ -77,7 +77,7 @@ .selectbox.hide-collapsed = f.hidden_field :due_date, value: issuable.due_date.try(:strftime, 'yy-mm-dd') .dropdown - %button.dropdown-menu-toggle.js-due-date-select{ type: 'button', data: { toggle: 'dropdown', field_name: "#{issuable.to_ability_name}[due_date]", ability_name: issuable.to_ability_name, issue_update: issuable_json_path(issuable) } } + %button.dropdown-menu-toggle.js-due-date-select{ type: 'button', data: { toggle: 'dropdown', field_name: "#{issuable.to_ability_name}[due_date]", ability_name: issuable.to_ability_name, issue_update: issuable_json_path(issuable), display: 'static' } } %span.dropdown-toggle-text = _('Due date') = icon('chevron-down', 'aria-hidden': 'true') @@ -109,7 +109,7 @@ - selected_labels.each do |label| = hidden_field_tag "#{issuable.to_ability_name}[label_names][]", label.id, id: nil .dropdown - %button.dropdown-menu-toggle.js-label-select.js-multiselect.js-label-sidebar-dropdown{ type: "button", data: {toggle: "dropdown", default_label: "Labels", field_name: "#{issuable.to_ability_name}[label_names][]", ability_name: issuable.to_ability_name, show_no: "true", show_any: "true", namespace_path: @project.try(:namespace).try(:full_path), project_path: @project.try(:path), issue_update: issuable_json_path(issuable), labels: (labels_filter_path(false) if @project) } } + %button.dropdown-menu-toggle.js-label-select.js-multiselect.js-label-sidebar-dropdown{ type: "button", data: {toggle: "dropdown", default_label: "Labels", field_name: "#{issuable.to_ability_name}[label_names][]", ability_name: issuable.to_ability_name, show_no: "true", show_any: "true", namespace_path: @project.try(:namespace).try(:full_path), project_path: @project.try(:path), issue_update: issuable_json_path(issuable), labels: (labels_filter_path(false) if @project), display: 'static' } } %span.dropdown-toggle-text{ class: ("is-default" if selected_labels.empty?) } = multi_label_name(selected_labels, "Labels") = icon('chevron-down', 'aria-hidden': 'true') diff --git a/app/views/shared/issuable/_sidebar_assignees.html.haml b/app/views/shared/issuable/_sidebar_assignees.html.haml index e1cde527ad7..8a13c7a3b83 100644 --- a/app/views/shared/issuable/_sidebar_assignees.html.haml +++ b/app/views/shared/issuable/_sidebar_assignees.html.haml @@ -37,7 +37,7 @@ - issuable.assignees.each do |assignee| = hidden_field_tag "#{issuable.to_ability_name}[assignee_ids][]", assignee.id, id: nil, data: { avatar_url: assignee.avatar_url, name: assignee.name, username: assignee.username } - - options = { toggle_class: 'js-user-search js-author-search', title: _('Assign to'), filter: true, dropdown_class: 'dropdown-menu-user dropdown-menu-selectable dropdown-menu-author', placeholder: _('Search users'), data: { first_user: current_user&.username, current_user: true, project_id: @project&.id, author_id: issuable.author_id, field_name: "#{issuable.to_ability_name}[assignee_ids][]", issue_update: issuable_json_path(issuable), ability_name: issuable.to_ability_name, null_user: true } } + - options = { toggle_class: 'js-user-search js-author-search', title: _('Assign to'), filter: true, dropdown_class: 'dropdown-menu-user dropdown-menu-selectable dropdown-menu-author', placeholder: _('Search users'), data: { first_user: current_user&.username, current_user: true, project_id: @project&.id, author_id: issuable.author_id, field_name: "#{issuable.to_ability_name}[assignee_ids][]", issue_update: issuable_json_path(issuable), ability_name: issuable.to_ability_name, null_user: true, display: 'static' } } - title = _('Select assignee') - if issuable.is_a?(Issue) @@ -50,7 +50,7 @@ - data[:multi_select] = true - data['dropdown-title'] = title - data['dropdown-header'] = dropdown_options[:data][:'dropdown-header'] - - data['max-select'] = dropdown_options[:data][:'max-select'] + - data['max-select'] = dropdown_options[:data][:'max-select'] if dropdown_options[:data][:'max-select'] - options[:data].merge!(data) = dropdown_tag(title, options: options) diff --git a/app/views/shared/issuable/form/_contribution.html.haml b/app/views/shared/issuable/form/_contribution.html.haml index b34549240e0..bc9a1edc39c 100644 --- a/app/views/shared/issuable/form/_contribution.html.haml +++ b/app/views/shared/issuable/form/_contribution.html.haml @@ -7,14 +7,14 @@ %hr -.form-group - .col-form-label +.form-group.row + %label.col-form-label.col-sm-2 = _('Contribution') .col-sm-10 - .form-check - = form.check_box :allow_maintainer_to_push, disabled: !issuable.can_allow_maintainer_to_push?(current_user), class: 'form-check-input' - = form.label :allow_maintainer_to_push, class: 'form-check-label' do - = _('Allow edits from maintainers.') - = link_to 'About this feature', help_page_path('user/project/merge_requests/maintainer_access') + .form-check.prepend-top-5 + = form.check_box :allow_collaboration, disabled: !issuable.can_allow_collaboration?(current_user), class: 'form-check-input' + = form.label :allow_collaboration, class: 'form-check-label' do + = _('Allow commits from members who can merge to the target branch.') + = link_to 'About this feature', help_page_path('user/project/merge_requests/allow_collaboration') .form-text.text-muted - = allow_maintainer_push_unavailable_reason(issuable) + = allow_collaboration_unavailable_reason(issuable) diff --git a/app/views/shared/issuable/form/_metadata.html.haml b/app/views/shared/issuable/form/_metadata.html.haml index 1e27253aaeb..bd87bb38e77 100644 --- a/app/views/shared/issuable/form/_metadata.html.haml +++ b/app/views/shared/issuable/form/_metadata.html.haml @@ -15,17 +15,20 @@ - else = render "shared/issuable/form/metadata_merge_request_assignee", issuable: issuable, form: form, has_due_date: has_due_date .form-group.row.issue-milestone - = form.label :milestone_id, "Milestone", class: "col-form-label #{has_due_date ? "col-md-2 col-lg-4" : "col-2"}" - .col-10{ class: ("col-md-8" if has_due_date) } + = form.label :milestone_id, "Milestone", class: "col-form-label #{has_due_date ? "col-md-2 col-lg-4" : "col-sm-2"}" + .col-sm-10{ class: ("col-md-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, show_started: false, extra_class: "js-issuable-form-dropdown js-dropdown-keep-input", dropdown_title: "Select milestone" .form-group.row - has_labels = @labels && @labels.any? - = form.label :label_ids, "Labels", class: "col-form-label #{has_due_date ? "col-md-2 col-lg-4" : "col-2"}" + = form.label :label_ids, "Labels", class: "col-form-label #{has_due_date ? "col-md-2 col-lg-4" : "col-sm-2"}" = form.hidden_field :label_ids, multiple: true, value: '' - .col-10{ class: "#{"col-md-8" if has_due_date} #{'issuable-form-padding-top' if !has_labels}" } + .col-sm-10{ class: "#{"col-md-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/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_if_exists "shared/issuable/form/weight", issuable: issuable, form: form + - if has_due_date .col-lg-6 .form-group.row diff --git a/app/views/shared/members/_group.html.haml b/app/views/shared/members/_group.html.haml index 67b8843a27f..d0b492b43f3 100644 --- a/app/views/shared/members/_group.html.haml +++ b/app/views/shared/members/_group.html.haml @@ -5,16 +5,16 @@ %li.member.group_member{ id: dom_id } %span.list-item-name = group_icon(group, class: "avatar s40", alt: '') - %strong - = link_to group.full_name, group_path(group) - .cgray - Given access #{time_ago_with_tooltip(group_link.created_at)} - - if group_link.expires? - · - %span{ class: ('text-warning' if group_link.expires_soon?) } - Expires in #{distance_of_time_in_words_to_now(group_link.expires_at)} + .user-info + = link_to group.full_name, group_path(group), class: 'member' + .cgray + Given access #{time_ago_with_tooltip(group_link.created_at)} + - if group_link.expires? + · + %span{ class: ('text-warning' if group_link.expires_soon?) } + Expires in #{distance_of_time_in_words_to_now(group_link.expires_at)} .controls.member-controls - = form_tag project_group_link_path(@project, group_link), method: :put, remote: true, class: 'js-edit-member-form' do + = form_tag project_group_link_path(@project, group_link), method: :put, remote: true, class: 'js-edit-member-form form-group row append-right-5' do = hidden_field_tag "group_link[group_access]", group_link.group_access .member-form-control.dropdown.append-right-5 %button.dropdown-menu-toggle.js-member-permissions-dropdown{ type: "button", diff --git a/app/views/shared/notes/_edit_form.html.haml b/app/views/shared/notes/_edit_form.html.haml index 8923e5602a4..71a5b94e958 100644 --- a/app/views/shared/notes/_edit_form.html.haml +++ b/app/views/shared/notes/_edit_form.html.haml @@ -3,7 +3,7 @@ = hidden_field_tag :target_id, '', class: 'js-form-target-id' = hidden_field_tag :target_type, '', class: 'js-form-target-type' = render layout: 'projects/md_preview', locals: { url: preview_markdown_path(project), referenced_users: true } do - = render 'projects/zen', attr: 'note[note]', classes: 'note-textarea js-note-text js-task-list-field', placeholder: "Write a comment or drag your files here..." + = render 'projects/zen', attr: 'note[note]', classes: 'note-textarea js-note-text js-task-list-field', placeholder: "Write a comment or drag your files here…" = render 'shared/notes/hints' .note-form-actions.clearfix diff --git a/app/views/shared/notes/_form.html.haml b/app/views/shared/notes/_form.html.haml index 71c0d740bc8..c360f1ffe2a 100644 --- a/app/views/shared/notes/_form.html.haml +++ b/app/views/shared/notes/_form.html.haml @@ -29,7 +29,7 @@ = render 'projects/zen', f: f, attr: :note, classes: 'note-textarea js-note-text', - placeholder: "Write a comment or drag your files here...", + placeholder: "Write a comment or drag your files here…", supports_quick_actions: supports_quick_actions, supports_autocomplete: supports_autocomplete = render 'shared/notes/hints', supports_quick_actions: supports_quick_actions diff --git a/app/views/shared/notifications/_button.html.haml b/app/views/shared/notifications/_button.html.haml index e99d8d0973f..09ddf732ada 100644 --- a/app/views/shared/notifications/_button.html.haml +++ b/app/views/shared/notifications/_button.html.haml @@ -6,14 +6,14 @@ .js-notification-toggle-btns %div{ class: ("btn-group" if notification_setting.custom?) } - if notification_setting.custom? - %button.dropdown-new.btn.btn-default.has-tooltip.notifications-btn#notifications-button{ type: "button", title: "Notification setting", "aria-label" => "Notification setting: #{notification_title(notification_setting.level)}", data: { container: "body", toggle: "modal", target: "#" + notifications_menu_identifier("modal", notification_setting) } } + %button.dropdown-new.btn.btn-default.has-tooltip.notifications-btn#notifications-button{ type: "button", title: "Notification setting", "aria-label" => "Notification setting: #{notification_title(notification_setting.level)}", data: { container: "body", toggle: "modal", target: "#" + notifications_menu_identifier("modal", notification_setting), display: 'static' } } = icon("bell", class: "js-notification-loading") = notification_title(notification_setting.level) %button.btn.dropdown-toggle{ data: { toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting) } } = icon('caret-down') .sr-only Toggle dropdown - else - %button.dropdown-new.btn.btn-default.has-tooltip.notifications-btn#notifications-button{ type: "button", title: "Notification setting", "aria-label" => "Notification setting: #{notification_title(notification_setting.level)}", data: { container: "body", toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting) } } + %button.dropdown-new.btn.btn-default.has-tooltip.notifications-btn#notifications-button{ type: "button", title: "Notification setting", "aria-label" => "Notification setting: #{notification_title(notification_setting.level)}", data: { container: "body", toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting), display: 'static' } } = icon("bell", class: "js-notification-loading") = notification_title(notification_setting.level) = icon("caret-down") diff --git a/app/views/shared/projects/_edit_information.html.haml b/app/views/shared/projects/_edit_information.html.haml index ec9dc8f62c2..9230e045a81 100644 --- a/app/views/shared/projects/_edit_information.html.haml +++ b/app/views/shared/projects/_edit_information.html.haml @@ -1,6 +1,6 @@ - unless can?(current_user, :push_code, @project) .inline.prepend-left-10 - - if @project.branch_allows_maintainer_push?(current_user, selected_branch) + - if @project.branch_allows_collaboration?(current_user, selected_branch) = commit_in_single_accessible_branch - else = commit_in_fork_help diff --git a/app/views/shared/tokens/_scopes_form.html.haml b/app/views/shared/tokens/_scopes_form.html.haml index ae437dd16d6..2d0bb722189 100644 --- a/app/views/shared/tokens/_scopes_form.html.haml +++ b/app/views/shared/tokens/_scopes_form.html.haml @@ -5,6 +5,6 @@ - scopes.each do |scope| %fieldset = check_box_tag "#{prefix}[scopes][]", scope, token.scopes.include?(scope), id: "#{prefix}_scopes_#{scope}" - = label_tag ("#{prefix}_scopes_#{scope}"), scope + = label_tag ("#{prefix}_scopes_#{scope}"), scope, class: "label-light" %span= t(scope, scope: [:doorkeeper, :scopes]) .scope-description= t scope, scope: [:doorkeeper, :scope_desc] diff --git a/app/views/users/terms/index.html.haml b/app/views/users/terms/index.html.haml index e0fe551cf36..a869cf9cdee 100644 --- a/app/views/users/terms/index.html.haml +++ b/app/views/users/terms/index.html.haml @@ -1,13 +1,18 @@ - redirect_params = { redirect: @redirect } if @redirect -.panel-content.rendered-terms +.card-body.rendered-terms = markdown_field(@term, :terms) -.row-content-block.footer-block.clearfix - - if can?(current_user, :accept_terms, @term) - .float-right - = button_to accept_term_path(@term, redirect_params), class: 'btn btn-success prepend-left-8' do - = _('Accept terms') - - if can?(current_user, :decline_terms, @term) - .float-right - = button_to decline_term_path(@term, redirect_params), class: 'btn btn-default prepend-left-8' do - = _('Decline and sign out') +- if current_user + .card-footer.footer-block.clearfix + - if can?(current_user, :accept_terms, @term) + .float-right + = button_to accept_term_path(@term, redirect_params), class: 'btn btn-success prepend-left-8' do + = _('Accept terms') + - else + .pull-right + = link_to root_path, class: 'btn btn-success prepend-left-8' do + = _('Continue') + - if can?(current_user, :decline_terms, @term) + .float-right + = button_to decline_term_path(@term, redirect_params), class: 'btn btn-default prepend-left-8' do + = _('Decline and sign out') diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml index 93e57512edb..30b6796a7d6 100644 --- a/app/workers/all_queues.yml +++ b/app/workers/all_queues.yml @@ -17,6 +17,7 @@ - cronjob:stuck_ci_jobs - cronjob:stuck_import_jobs - cronjob:stuck_merge_jobs +- cronjob:ci_archive_traces_cron - cronjob:trending_projects - cronjob:issue_due_scheduler @@ -30,12 +31,14 @@ - github_importer:github_import_import_diff_note - github_importer:github_import_import_issue - github_importer:github_import_import_note +- github_importer:github_import_import_lfs_object - github_importer:github_import_import_pull_request - github_importer:github_import_refresh_import_jid - github_importer:github_import_stage_finish_import - github_importer:github_import_stage_import_base_data - github_importer:github_import_stage_import_issues_and_diff_notes - github_importer:github_import_stage_import_notes +- github_importer:github_import_stage_import_lfs_objects - github_importer:github_import_stage_import_pull_requests - github_importer:github_import_stage_import_repository diff --git a/app/workers/ci/archive_traces_cron_worker.rb b/app/workers/ci/archive_traces_cron_worker.rb new file mode 100644 index 00000000000..2ac65f41f4e --- /dev/null +++ b/app/workers/ci/archive_traces_cron_worker.rb @@ -0,0 +1,26 @@ +module Ci + class ArchiveTracesCronWorker + include ApplicationWorker + include CronjobQueue + + def perform + # Archive stale live traces which still resides in redis or database + # This could happen when ArchiveTraceWorker sidekiq jobs were lost by receiving SIGKILL + # More details in https://gitlab.com/gitlab-org/gitlab-ce/issues/36791 + Ci::Build.finished.with_live_trace.find_each(batch_size: 100) do |build| + begin + build.trace.archive! + rescue => e + failed_archive_counter.increment + Rails.logger.error "Failed to archive stale live trace. id: #{build.id} message: #{e.message}" + end + end + end + + private + + def failed_archive_counter + @failed_archive_counter ||= Gitlab::Metrics.counter(:job_trace_archive_failed_total, "Counter of failed attempts of traces archiving") + end + end +end diff --git a/app/workers/git_garbage_collect_worker.rb b/app/workers/git_garbage_collect_worker.rb index be4203bc7ad..ae5c5fac834 100644 --- a/app/workers/git_garbage_collect_worker.rb +++ b/app/workers/git_garbage_collect_worker.rb @@ -6,12 +6,6 @@ class GitGarbageCollectWorker # Timeout set to 24h LEASE_TIMEOUT = 86400 - GITALY_MIGRATED_TASKS = { - gc: :garbage_collect, - full_repack: :repack_full, - incremental_repack: :repack_incremental - }.freeze - def perform(project_id, task = :gc, lease_key = nil, lease_uuid = nil) project = Project.find(project_id) active_uuid = get_lease_uuid(lease_key) @@ -27,21 +21,7 @@ class GitGarbageCollectWorker end task = task.to_sym - cmd = command(task) - - gitaly_migrate(GITALY_MIGRATED_TASKS[task]) do |is_enabled| - if is_enabled - gitaly_call(task, project.repository.raw_repository) - else - repo_path = project.repository.path_to_repo - description = "'#{cmd.join(' ')}' in #{repo_path}" - Gitlab::GitLogger.info(description) - - output, status = Gitlab::Popen.popen(cmd, repo_path) - - Gitlab::GitLogger.error("#{description} failed:\n#{output}") unless status.zero? - end - end + gitaly_call(task, project.repository.raw_repository) # Refresh the branch cache in case garbage collection caused a ref lookup to fail flush_ref_caches(project) if task == :gc @@ -82,21 +62,12 @@ class GitGarbageCollectWorker when :incremental_repack client.repack_incremental end - end - - def command(task) - case task - when :gc - git(write_bitmaps: bitmaps_enabled?) + %w[gc] - when :full_repack - git(write_bitmaps: bitmaps_enabled?) + %w[repack -A -d --pack-kept-objects] - when :incremental_repack - # Normal git repack fails when bitmaps are enabled. It is impossible to - # create a bitmap here anyway. - git(write_bitmaps: false) + %w[repack -d] - else - raise "Invalid gc task: #{task.inspect}" - end + rescue GRPC::NotFound => e + Gitlab::GitLogger.error("#{method} failed:\nRepository not found") + raise Gitlab::Git::Repository::NoRepository.new(e) + rescue GRPC::BadStatus => e + Gitlab::GitLogger.error("#{method} failed:\n#{e}") + raise Gitlab::Git::CommandError.new(e) end def flush_ref_caches(project) @@ -108,19 +79,4 @@ class GitGarbageCollectWorker def bitmaps_enabled? Gitlab::CurrentSettings.housekeeping_bitmaps_enabled end - - def git(write_bitmaps:) - config_value = write_bitmaps ? 'true' : 'false' - %W[git -c repack.writeBitmaps=#{config_value}] - end - - def gitaly_migrate(method, &block) - Gitlab::GitalyClient.migrate(method, &block) - rescue GRPC::NotFound => e - Gitlab::GitLogger.error("#{method} failed:\nRepository not found") - raise Gitlab::Git::Repository::NoRepository.new(e) - rescue GRPC::BadStatus => e - Gitlab::GitLogger.error("#{method} failed:\n#{e}") - raise Gitlab::Git::CommandError.new(e) - end end diff --git a/app/workers/gitlab/github_import/advance_stage_worker.rb b/app/workers/gitlab/github_import/advance_stage_worker.rb index 8d708e15a66..be0b6c180b0 100644 --- a/app/workers/gitlab/github_import/advance_stage_worker.rb +++ b/app/workers/gitlab/github_import/advance_stage_worker.rb @@ -21,6 +21,7 @@ module Gitlab STAGES = { issues_and_diff_notes: Stage::ImportIssuesAndDiffNotesWorker, notes: Stage::ImportNotesWorker, + lfs_objects: Stage::ImportLfsObjectsWorker, finish: Stage::FinishImportWorker }.freeze diff --git a/app/workers/gitlab/github_import/import_lfs_object_worker.rb b/app/workers/gitlab/github_import/import_lfs_object_worker.rb new file mode 100644 index 00000000000..520c5cb091a --- /dev/null +++ b/app/workers/gitlab/github_import/import_lfs_object_worker.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module Gitlab + module GithubImport + class ImportLfsObjectWorker + include ObjectImporter + + def representation_class + Representation::LfsObject + end + + def importer_class + Importer::LfsObjectImporter + end + + def counter_name + :github_importer_imported_lfs_objects + end + + def counter_description + 'The number of imported GitHub Lfs Objects' + end + end + end +end diff --git a/app/workers/gitlab/github_import/stage/import_lfs_objects_worker.rb b/app/workers/gitlab/github_import/stage/import_lfs_objects_worker.rb new file mode 100644 index 00000000000..29257603a9d --- /dev/null +++ b/app/workers/gitlab/github_import/stage/import_lfs_objects_worker.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +module Gitlab + module GithubImport + module Stage + class ImportLfsObjectsWorker + include ApplicationWorker + include GithubImport::Queue + include StageMethods + + def perform(project_id) + return unless (project = find_project(project_id)) + + import(project) + end + + # project - An instance of Project. + def import(project) + waiter = Importer::LfsObjectsImporter + .new(project, nil) + .execute + + AdvanceStageWorker.perform_async( + project.id, + { waiter.key => waiter.jobs_remaining }, + :finish + ) + end + end + end + end +end diff --git a/app/workers/gitlab/github_import/stage/import_notes_worker.rb b/app/workers/gitlab/github_import/stage/import_notes_worker.rb index 5f4678a595f..ccf0013180d 100644 --- a/app/workers/gitlab/github_import/stage/import_notes_worker.rb +++ b/app/workers/gitlab/github_import/stage/import_notes_worker.rb @@ -18,7 +18,7 @@ module Gitlab AdvanceStageWorker.perform_async( project.id, { waiter.key => waiter.jobs_remaining }, - :finish + :lfs_objects ) end end diff --git a/app/workers/repository_fork_worker.rb b/app/workers/repository_fork_worker.rb index 08b1c3a7d7a..db48bb7e8b8 100644 --- a/app/workers/repository_fork_worker.rb +++ b/app/workers/repository_fork_worker.rb @@ -23,10 +23,11 @@ class RepositoryForkWorker source_repository_storage_path, source_disk_path = *args - source_repository_storage_name = Gitlab.config.repositories.storages.find do |_, info| - info.legacy_disk_path == source_repository_storage_path - end&.first || raise("no shard found for path '#{source_repository_storage_path}'") - + source_repository_storage_name = Gitlab::GitalyClient::StorageSettings.allow_disk_access do + Gitlab.config.repositories.storages.find do |_, info| + info.legacy_disk_path == source_repository_storage_path + end&.first || raise("no shard found for path '#{source_repository_storage_path}'") + end fork_repository(target_project, source_repository_storage_name, source_disk_path) end end diff --git a/app/workers/storage_migrator_worker.rb b/app/workers/storage_migrator_worker.rb index f92421a667d..0aff0c4c7c6 100644 --- a/app/workers/storage_migrator_worker.rb +++ b/app/workers/storage_migrator_worker.rb @@ -1,29 +1,8 @@ class StorageMigratorWorker include ApplicationWorker - BATCH_SIZE = 100 - def perform(start, finish) - projects = build_relation(start, finish) - - projects.with_route.find_each(batch_size: BATCH_SIZE) do |project| - Rails.logger.info "Starting storage migration of #{project.full_path} (ID=#{project.id})..." - - begin - project.migrate_to_hashed_storage! - rescue => err - Rails.logger.error("#{err.message} migrating storage of #{project.full_path} (ID=#{project.id}), trace - #{err.backtrace}") - end - end - end - - def build_relation(start, finish) - relation = Project - table = Project.arel_table - - relation = relation.where(table[:id].gteq(start)) if start - relation = relation.where(table[:id].lteq(finish)) if finish - - relation + migrator = Gitlab::HashedStorage::Migrator.new + migrator.bulk_migrate(start, finish) end end diff --git a/changelogs/unreleased/25045-add-variables-to-post-pipeline-api.yml b/changelogs/unreleased/25045-add-variables-to-post-pipeline-api.yml new file mode 100644 index 00000000000..1e648b75248 --- /dev/null +++ b/changelogs/unreleased/25045-add-variables-to-post-pipeline-api.yml @@ -0,0 +1,5 @@ +--- +title: Add variables to POST api/v4/projects/:id/pipeline +merge_request: 19124 +author: Jacopo Beschi @jacopo-beschi +type: added diff --git a/changelogs/unreleased/35158-snippets-api-visibility.yml b/changelogs/unreleased/35158-snippets-api-visibility.yml new file mode 100644 index 00000000000..f06015dda46 --- /dev/null +++ b/changelogs/unreleased/35158-snippets-api-visibility.yml @@ -0,0 +1,5 @@ +--- +title: Expose visibility via Snippets API +merge_request: 19620 +author: Jan Beckmann +type: added diff --git a/changelogs/unreleased/36862-subgroup-milestones.yml b/changelogs/unreleased/36862-subgroup-milestones.yml new file mode 100644 index 00000000000..98b9dc41cb1 --- /dev/null +++ b/changelogs/unreleased/36862-subgroup-milestones.yml @@ -0,0 +1,5 @@ +--- +title: Include milestones from parent groups when assigning a milestone to an issue or merge request +merge_request: +author: +type: changed diff --git a/changelogs/unreleased/38542-application-control-panel-in-settings-page.yml b/changelogs/unreleased/38542-application-control-panel-in-settings-page.yml new file mode 100644 index 00000000000..0654456ea45 --- /dev/null +++ b/changelogs/unreleased/38542-application-control-panel-in-settings-page.yml @@ -0,0 +1,5 @@ +--- +title: Add deploy strategies to the Auto DevOps settings +merge_request: 19172 +author: +type: added diff --git a/changelogs/unreleased/39549-label-list-page-redesign-with-draggable-labels.yml b/changelogs/unreleased/39549-label-list-page-redesign-with-draggable-labels.yml new file mode 100644 index 00000000000..fb4fbf80575 --- /dev/null +++ b/changelogs/unreleased/39549-label-list-page-redesign-with-draggable-labels.yml @@ -0,0 +1,5 @@ +--- +title: Label list page redesign +merge_request: 18466 +author: +type: changed diff --git a/changelogs/unreleased/42342-teams-pipeline-notifications.yml b/changelogs/unreleased/42342-teams-pipeline-notifications.yml new file mode 100644 index 00000000000..4ef3a35465b --- /dev/null +++ b/changelogs/unreleased/42342-teams-pipeline-notifications.yml @@ -0,0 +1,5 @@ +--- +title: Fixes Microsoft Teams notifications for pipeline events +merge_request: 19632 +author: Jeff Brown +type: fixed diff --git a/changelogs/unreleased/42751-rename-master-to-maintainer.yml b/changelogs/unreleased/42751-rename-master-to-maintainer.yml new file mode 100644 index 00000000000..d7f499ecd52 --- /dev/null +++ b/changelogs/unreleased/42751-rename-master-to-maintainer.yml @@ -0,0 +1,5 @@ +--- +title: Rename the Master role to Maintainer +merge_request: 19080 +author: +type: changed diff --git a/changelogs/unreleased/42751-rename-mr-maintainer-push.yml b/changelogs/unreleased/42751-rename-mr-maintainer-push.yml new file mode 100644 index 00000000000..aa29f6ed4b7 --- /dev/null +++ b/changelogs/unreleased/42751-rename-mr-maintainer-push.yml @@ -0,0 +1,5 @@ +--- +title: Rephrasing Merge Request's 'allow edits from maintainer' functionality +merge_request: 19061 +author: +type: deprecated diff --git a/changelogs/unreleased/43597-new-navigation-themes.yml b/changelogs/unreleased/43597-new-navigation-themes.yml new file mode 100644 index 00000000000..de703e46b3c --- /dev/null +++ b/changelogs/unreleased/43597-new-navigation-themes.yml @@ -0,0 +1,5 @@ +--- +title: Add additional theme color options +merge_request: +author: +type: changed diff --git a/changelogs/unreleased/44267-improve-failed-jobs-tab.yml b/changelogs/unreleased/44267-improve-failed-jobs-tab.yml new file mode 100644 index 00000000000..9743704e23d --- /dev/null +++ b/changelogs/unreleased/44267-improve-failed-jobs-tab.yml @@ -0,0 +1,5 @@ +--- +title: Improve Failed Jobs tab in the Pipeline detail page +merge_request: +author: +type: changed diff --git a/changelogs/unreleased/44674-use-one-column-form-layout-on-admin-area-settings-page.yml b/changelogs/unreleased/44674-use-one-column-form-layout-on-admin-area-settings-page.yml new file mode 100644 index 00000000000..69733889d5a --- /dev/null +++ b/changelogs/unreleased/44674-use-one-column-form-layout-on-admin-area-settings-page.yml @@ -0,0 +1,5 @@ +--- +title: Use one column form layout on Admin Area Settings page +merge_request: +author: +type: changed diff --git a/changelogs/unreleased/44790-disabled-emails-logging.yml b/changelogs/unreleased/44790-disabled-emails-logging.yml new file mode 100644 index 00000000000..90125dc0300 --- /dev/null +++ b/changelogs/unreleased/44790-disabled-emails-logging.yml @@ -0,0 +1,5 @@ +--- +title: Stop logging email information when emails are disabled +merge_request: 18521 +author: Marc Shaw +type: fixed diff --git a/changelogs/unreleased/45400-automatically-created-mr-uses-wrong-target-branch-when-branching-from-tag.yml b/changelogs/unreleased/45400-automatically-created-mr-uses-wrong-target-branch-when-branching-from-tag.yml new file mode 100644 index 00000000000..5aba62435ed --- /dev/null +++ b/changelogs/unreleased/45400-automatically-created-mr-uses-wrong-target-branch-when-branching-from-tag.yml @@ -0,0 +1,5 @@ +--- +title: Set MR target branch to default branch if target branch is not valid +merge_request: 19067 +author: +type: fixed diff --git a/changelogs/unreleased/45505-lograge_formatter_encoding.yml b/changelogs/unreleased/45505-lograge_formatter_encoding.yml new file mode 100644 index 00000000000..02f4c152966 --- /dev/null +++ b/changelogs/unreleased/45505-lograge_formatter_encoding.yml @@ -0,0 +1,6 @@ +--- +title: Enforce UTF-8 encoding on user input in LogrageWithTimestamp formatter and + filter out file content from logs +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/45557-machine-type-help-links.yml b/changelogs/unreleased/45557-machine-type-help-links.yml new file mode 100644 index 00000000000..870a650e10b --- /dev/null +++ b/changelogs/unreleased/45557-machine-type-help-links.yml @@ -0,0 +1,6 @@ +--- +title: Add machine type and pricing documentation links, add class to labels to make + bold +merge_request: +author: +type: changed diff --git a/changelogs/unreleased/45575-invalid-characters-signup.yml b/changelogs/unreleased/45575-invalid-characters-signup.yml new file mode 100644 index 00000000000..679bd13e59b --- /dev/null +++ b/changelogs/unreleased/45575-invalid-characters-signup.yml @@ -0,0 +1,5 @@ +--- +title: 'Fix username validation order on signup, resolves #45575' +merge_request: 19610 +author: Jan Beckmann +type: fixed diff --git a/changelogs/unreleased/45702-fix-hashed-storage-repository-archive.yml b/changelogs/unreleased/45702-fix-hashed-storage-repository-archive.yml new file mode 100644 index 00000000000..0f85ced06a9 --- /dev/null +++ b/changelogs/unreleased/45702-fix-hashed-storage-repository-archive.yml @@ -0,0 +1,5 @@ +--- +title: Fix repository archive generation when hashed storage is enabled +merge_request: 19441 +author: +type: fixed diff --git a/changelogs/unreleased/45820-add-xcode-link.yml b/changelogs/unreleased/45820-add-xcode-link.yml new file mode 100644 index 00000000000..9e61703ee10 --- /dev/null +++ b/changelogs/unreleased/45820-add-xcode-link.yml @@ -0,0 +1,5 @@ +--- +title: Add Open in Xcode link for xcode repositories +merge_request: +author: +type: added diff --git a/changelogs/unreleased/45821-avatar_api.yml b/changelogs/unreleased/45821-avatar_api.yml new file mode 100644 index 00000000000..e16b28c36a2 --- /dev/null +++ b/changelogs/unreleased/45821-avatar_api.yml @@ -0,0 +1,5 @@ +--- +title: Add Avatar API +merge_request: 19121 +author: Imre Farkas +type: added diff --git a/changelogs/unreleased/46075-automatically-provide-deploy-token-when-autodevops-is-enabled.yml b/changelogs/unreleased/46075-automatically-provide-deploy-token-when-autodevops-is-enabled.yml new file mode 100644 index 00000000000..6974be07716 --- /dev/null +++ b/changelogs/unreleased/46075-automatically-provide-deploy-token-when-autodevops-is-enabled.yml @@ -0,0 +1,5 @@ +--- +title: Automatize Deploy Token creation for Auto Devops +merge_request: 19507 +author: +type: added diff --git a/changelogs/unreleased/46429-creating-a-deploy-token-doesn-t-bring-back-to-the-creation-page.yml b/changelogs/unreleased/46429-creating-a-deploy-token-doesn-t-bring-back-to-the-creation-page.yml new file mode 100644 index 00000000000..b564fb0174f --- /dev/null +++ b/changelogs/unreleased/46429-creating-a-deploy-token-doesn-t-bring-back-to-the-creation-page.yml @@ -0,0 +1,5 @@ +--- +title: Allows you to create another deploy token dimmediately after creating one +merge_request: 19639 +author: +type: changed diff --git a/changelogs/unreleased/46452-nomethoderror-undefined-method-previous_changes-for-nil-nilclass.yml b/changelogs/unreleased/46452-nomethoderror-undefined-method-previous_changes-for-nil-nilclass.yml new file mode 100644 index 00000000000..89dee65f5a8 --- /dev/null +++ b/changelogs/unreleased/46452-nomethoderror-undefined-method-previous_changes-for-nil-nilclass.yml @@ -0,0 +1,5 @@ +--- +title: Check for nil AutoDevOps when saving project CI/CD settings. +merge_request: 19190 +author: +type: fixed diff --git a/changelogs/unreleased/46585-gdpr-terms-acceptance.yml b/changelogs/unreleased/46585-gdpr-terms-acceptance.yml new file mode 100644 index 00000000000..84853846b0e --- /dev/null +++ b/changelogs/unreleased/46585-gdpr-terms-acceptance.yml @@ -0,0 +1,6 @@ +--- +title: Add flash notice if user has already accepted terms and allow users to continue + to root path +merge_request: 19156 +author: +type: changed diff --git a/changelogs/unreleased/46648-timeout-searching-group-issues.yml b/changelogs/unreleased/46648-timeout-searching-group-issues.yml new file mode 100644 index 00000000000..54401edf5cc --- /dev/null +++ b/changelogs/unreleased/46648-timeout-searching-group-issues.yml @@ -0,0 +1,5 @@ +--- +title: Improve performance of group issues filtering on GitLab.com +merge_request: 19429 +author: +type: performance diff --git a/changelogs/unreleased/46845-update-email_spec-to-2-2-0.yml b/changelogs/unreleased/46845-update-email_spec-to-2-2-0.yml new file mode 100644 index 00000000000..bf501340769 --- /dev/null +++ b/changelogs/unreleased/46845-update-email_spec-to-2-2-0.yml @@ -0,0 +1,5 @@ +--- +title: Update email_spec to 2.2.0 +merge_request: 19164 +author: Takuya Noguchi +type: other diff --git a/changelogs/unreleased/46922-hashed-storage-single-project.yml b/changelogs/unreleased/46922-hashed-storage-single-project.yml new file mode 100644 index 00000000000..c293238a5a4 --- /dev/null +++ b/changelogs/unreleased/46922-hashed-storage-single-project.yml @@ -0,0 +1,5 @@ +--- +title: 'Hashed Storage: migration rake task now can be executed to specific project' +merge_request: 19268 +author: +type: changed diff --git a/changelogs/unreleased/47050-quick-actions-case-insensitive.yml b/changelogs/unreleased/47050-quick-actions-case-insensitive.yml new file mode 100644 index 00000000000..176aba627b9 --- /dev/null +++ b/changelogs/unreleased/47050-quick-actions-case-insensitive.yml @@ -0,0 +1,5 @@ +--- +title: Make quick commands case insensitive +merge_request: 19614 +author: Jan Beckmann +type: fixed diff --git a/changelogs/unreleased/47145-quick-actions-confidential.yml b/changelogs/unreleased/47145-quick-actions-confidential.yml new file mode 100644 index 00000000000..7ae4e2268af --- /dev/null +++ b/changelogs/unreleased/47145-quick-actions-confidential.yml @@ -0,0 +1,5 @@ +--- +title: Add /confidential quick action +merge_request: +author: Jan Beckmann +type: added diff --git a/changelogs/unreleased/47182-use-the-default-strings-of-timeago-js.yml b/changelogs/unreleased/47182-use-the-default-strings-of-timeago-js.yml new file mode 100644 index 00000000000..010b1db5aac --- /dev/null +++ b/changelogs/unreleased/47182-use-the-default-strings-of-timeago-js.yml @@ -0,0 +1,5 @@ +--- +title: Use the default strings of timeago.js for timeago +merge_request: 19350 +author: Takuya Noguchi +type: other diff --git a/changelogs/unreleased/47183-update-selenium-webdriver-to-3-12-0.yml b/changelogs/unreleased/47183-update-selenium-webdriver-to-3-12-0.yml new file mode 100644 index 00000000000..b0d51d810f2 --- /dev/null +++ b/changelogs/unreleased/47183-update-selenium-webdriver-to-3-12-0.yml @@ -0,0 +1,5 @@ +--- +title: Update selenium-webdriver to 3.12.0 +merge_request: 19351 +author: Takuya Noguchi +type: other diff --git a/changelogs/unreleased/47189-github_import_visibility.yml b/changelogs/unreleased/47189-github_import_visibility.yml new file mode 100644 index 00000000000..a2a727a3227 --- /dev/null +++ b/changelogs/unreleased/47189-github_import_visibility.yml @@ -0,0 +1,6 @@ +--- +title: Use Github repo visibility during import while respecting restricted visibility + levels +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/47208-human-import-status-name-not-working.yml b/changelogs/unreleased/47208-human-import-status-name-not-working.yml new file mode 100644 index 00000000000..e1f603f988e --- /dev/null +++ b/changelogs/unreleased/47208-human-import-status-name-not-working.yml @@ -0,0 +1,5 @@ +--- +title: Showing project import_status in a humanized form no longer gives an error +merge_request: 19470 +author: +type: fixed diff --git a/changelogs/unreleased/47408-migrateuploadsworker-is-doing-n-1-queries-on-migration.yml b/changelogs/unreleased/47408-migrateuploadsworker-is-doing-n-1-queries-on-migration.yml new file mode 100644 index 00000000000..c0df82f35f1 --- /dev/null +++ b/changelogs/unreleased/47408-migrateuploadsworker-is-doing-n-1-queries-on-migration.yml @@ -0,0 +1,5 @@ +--- +title: Optimize the upload migration proces +merge_request: 15947 +author: +type: fixed diff --git a/changelogs/unreleased/47513-upload-migration-lease-key-is-incorrect-for-non-mounted-uploaders.yml b/changelogs/unreleased/47513-upload-migration-lease-key-is-incorrect-for-non-mounted-uploaders.yml new file mode 100644 index 00000000000..010c4e9bce7 --- /dev/null +++ b/changelogs/unreleased/47513-upload-migration-lease-key-is-incorrect-for-non-mounted-uploaders.yml @@ -0,0 +1,5 @@ +--- +title: Use upload ID for creating lease key for file uploaders. +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/47604-avatars-and-system-icons-for-mobile.yml b/changelogs/unreleased/47604-avatars-and-system-icons-for-mobile.yml new file mode 100644 index 00000000000..ff66385375f --- /dev/null +++ b/changelogs/unreleased/47604-avatars-and-system-icons-for-mobile.yml @@ -0,0 +1,5 @@ +--- +title: Make avatars/icons hidden on mobile +merge_request: 19585 +author: Takuya Noguchi +type: fixed diff --git a/changelogs/unreleased/47646-ui-glitch.yml b/changelogs/unreleased/47646-ui-glitch.yml new file mode 100644 index 00000000000..384df4e2cc9 --- /dev/null +++ b/changelogs/unreleased/47646-ui-glitch.yml @@ -0,0 +1,5 @@ +--- +title: Line height fixed +merge_request: +author: Murat Dogan +type: fixed diff --git a/changelogs/unreleased/47672-set_inline_content_type_for_ics.yml b/changelogs/unreleased/47672-set_inline_content_type_for_ics.yml new file mode 100644 index 00000000000..4bc883d41bd --- /dev/null +++ b/changelogs/unreleased/47672-set_inline_content_type_for_ics.yml @@ -0,0 +1,5 @@ +--- +title: Render calendar feed inline when accessed from GitLab +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/47679-fix-failed-jobs-tab-ie11.yml b/changelogs/unreleased/47679-fix-failed-jobs-tab-ie11.yml new file mode 100644 index 00000000000..48f3bc87eee --- /dev/null +++ b/changelogs/unreleased/47679-fix-failed-jobs-tab-ie11.yml @@ -0,0 +1,5 @@ +--- +title: Fix overflowing Failed Jobs table in sm viewports on IE11 +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/47871-new-mr-tab-active-state.yml b/changelogs/unreleased/47871-new-mr-tab-active-state.yml new file mode 100644 index 00000000000..c3fc8672d82 --- /dev/null +++ b/changelogs/unreleased/47871-new-mr-tab-active-state.yml @@ -0,0 +1,5 @@ +--- +title: Fix active tab highlight when creating new merge request +merge_request: 19781 +author: Jan Beckmann +type: fixed diff --git a/changelogs/unreleased/add-background-migrations-for-not-archived-traces.yml b/changelogs/unreleased/add-background-migrations-for-not-archived-traces.yml new file mode 100644 index 00000000000..b1b23c477df --- /dev/null +++ b/changelogs/unreleased/add-background-migrations-for-not-archived-traces.yml @@ -0,0 +1,5 @@ +--- +title: Add background migrations for archiving legacy job traces +merge_request: 19194 +author: +type: performance diff --git a/changelogs/unreleased/add-new-arg-to-git-rev-list-call.yml b/changelogs/unreleased/add-new-arg-to-git-rev-list-call.yml new file mode 100644 index 00000000000..86680b6b117 --- /dev/null +++ b/changelogs/unreleased/add-new-arg-to-git-rev-list-call.yml @@ -0,0 +1,5 @@ +--- +title: Improve performance of LFS integrity check +merge_request: 19494 +author: +type: performance diff --git a/changelogs/unreleased/author-doc-fix.yml b/changelogs/unreleased/author-doc-fix.yml new file mode 100644 index 00000000000..83521543239 --- /dev/null +++ b/changelogs/unreleased/author-doc-fix.yml @@ -0,0 +1,5 @@ +--- +title: Fix fields for author & assignee in MR API docs. +merge_request: 19798 +author: gfyoung +type: fixed diff --git a/changelogs/unreleased/blackst0ne-add-gemfile-rails5-lock-check.yml b/changelogs/unreleased/blackst0ne-add-gemfile-rails5-lock-check.yml new file mode 100644 index 00000000000..69d49f3e3e0 --- /dev/null +++ b/changelogs/unreleased/blackst0ne-add-gemfile-rails5-lock-check.yml @@ -0,0 +1,5 @@ +--- +title: Add CI job to check Gemfile.rails5.lock +merge_request: 19605 +author: "@blackst0ne" +type: other diff --git a/changelogs/unreleased/blackst0ne-bump-grape-path-helpers-gem-to-1-0-5.yml b/changelogs/unreleased/blackst0ne-bump-grape-path-helpers-gem-to-1-0-5.yml new file mode 100644 index 00000000000..9d975ff81bf --- /dev/null +++ b/changelogs/unreleased/blackst0ne-bump-grape-path-helpers-gem-to-1-0-5.yml @@ -0,0 +1,5 @@ +--- +title: Bump grape-path-helpers to 1.0.5 +merge_request: 19604 +author: "@blackst0ne" +type: other diff --git a/changelogs/unreleased/blackst0ne-rails5-expected-search-search-seed_project-got-nil.yml b/changelogs/unreleased/blackst0ne-rails5-expected-search-search-seed_project-got-nil.yml new file mode 100644 index 00000000000..e7bb2703b03 --- /dev/null +++ b/changelogs/unreleased/blackst0ne-rails5-expected-search-search-seed_project-got-nil.yml @@ -0,0 +1,5 @@ +--- +title: "[Rails5] Fix sessions_controller_spec" +merge_request: 19936 +author: "@blackst0ne" +type: fixed diff --git a/changelogs/unreleased/blackst0ne-rails5-expected-the-response-to-have-status-code-ok-but-it-was-404.yml b/changelogs/unreleased/blackst0ne-rails5-expected-the-response-to-have-status-code-ok-but-it-was-404.yml new file mode 100644 index 00000000000..fad15de2dd5 --- /dev/null +++ b/changelogs/unreleased/blackst0ne-rails5-expected-the-response-to-have-status-code-ok-but-it-was-404.yml @@ -0,0 +1,5 @@ +--- +title: "[Rails5] Set request.format for artifacts_controller" +merge_request: 19937 +author: "@blackst0ne" +type: fixed diff --git a/changelogs/unreleased/blackst0ne-rails5-fix-blob-requests-format.yml b/changelogs/unreleased/blackst0ne-rails5-fix-blob-requests-format.yml new file mode 100644 index 00000000000..a83aa03606a --- /dev/null +++ b/changelogs/unreleased/blackst0ne-rails5-fix-blob-requests-format.yml @@ -0,0 +1,5 @@ +--- +title: "[Rails5] Explicitly set request.format for blob_controller" +merge_request: 19876 +author: "@blackst0ne" +type: fixed diff --git a/changelogs/unreleased/blackst0ne-rails5-fix-optimistic-lock-values.yml b/changelogs/unreleased/blackst0ne-rails5-fix-optimistic-lock-values.yml new file mode 100644 index 00000000000..1915dff73ab --- /dev/null +++ b/changelogs/unreleased/blackst0ne-rails5-fix-optimistic-lock-values.yml @@ -0,0 +1,5 @@ +--- +title: "[Rails5] Fix optimistic lock value" +merge_request: 19878 +author: "@blackst0ne" +type: fixed diff --git a/changelogs/unreleased/blackst0ne-rails5-fix-pipeline-schedules-controller-spec.yml b/changelogs/unreleased/blackst0ne-rails5-fix-pipeline-schedules-controller-spec.yml new file mode 100644 index 00000000000..7a2b19ad681 --- /dev/null +++ b/changelogs/unreleased/blackst0ne-rails5-fix-pipeline-schedules-controller-spec.yml @@ -0,0 +1,5 @@ +--- +title: "[Rails5] Fix pipeline_schedules_controller_spec" +merge_request: 19919 +author: "@blackst0ne" +type: fixed diff --git a/changelogs/unreleased/blackst0ne-rails5-fix-snippets-finder.yml b/changelogs/unreleased/blackst0ne-rails5-fix-snippets-finder.yml new file mode 100644 index 00000000000..597b85de26f --- /dev/null +++ b/changelogs/unreleased/blackst0ne-rails5-fix-snippets-finder.yml @@ -0,0 +1,5 @@ +--- +title: "[Rails5] Fix snippets_finder arel queries" +merge_request: 19796 +author: "@blackst0ne" +type: fixed diff --git a/changelogs/unreleased/blackst0ne-rails5-invalid-single-table-inheritance-type-group-is-not-a-subclass-of-namespace.yml b/changelogs/unreleased/blackst0ne-rails5-invalid-single-table-inheritance-type-group-is-not-a-subclass-of-namespace.yml new file mode 100644 index 00000000000..92e6ce35941 --- /dev/null +++ b/changelogs/unreleased/blackst0ne-rails5-invalid-single-table-inheritance-type-group-is-not-a-subclass-of-namespace.yml @@ -0,0 +1,6 @@ +--- +title: "[Rails5] Invalid single-table inheritance type: Group is not a subclass of + Namespace" +merge_request: 19918 +author: "@blackst0ne" +type: fixed diff --git a/changelogs/unreleased/bootstrap-changelog.yml b/changelogs/unreleased/bootstrap-changelog.yml new file mode 100644 index 00000000000..fd58447769d --- /dev/null +++ b/changelogs/unreleased/bootstrap-changelog.yml @@ -0,0 +1,5 @@ +--- +title: Upgrade GitLab from Bootstrap 3 to 4 +merge_request: +author: +type: other diff --git a/changelogs/unreleased/bvl-bump-gitlab-shell-7-1-3.yml b/changelogs/unreleased/bvl-bump-gitlab-shell-7-1-3.yml new file mode 100644 index 00000000000..76bb25bc7d7 --- /dev/null +++ b/changelogs/unreleased/bvl-bump-gitlab-shell-7-1-3.yml @@ -0,0 +1,5 @@ +--- +title: Include username in output when testing SSH to GitLab +merge_request: 19358 +author: +type: other diff --git a/changelogs/unreleased/bvl-fix-maintainer-push-rejected.yml b/changelogs/unreleased/bvl-fix-maintainer-push-rejected.yml new file mode 100644 index 00000000000..54154ad2449 --- /dev/null +++ b/changelogs/unreleased/bvl-fix-maintainer-push-rejected.yml @@ -0,0 +1,6 @@ +--- +title: Fix bug where maintainer would not be allowed to push to forks with merge requests + that have `Allow maintainer edits` enabled. +merge_request: 18968 +author: +type: fixed diff --git a/changelogs/unreleased/bvl-graphql-nested-merge-request.yml b/changelogs/unreleased/bvl-graphql-nested-merge-request.yml new file mode 100644 index 00000000000..f0f0488d31a --- /dev/null +++ b/changelogs/unreleased/bvl-graphql-nested-merge-request.yml @@ -0,0 +1,5 @@ +--- +title: Allow querying a single merge request within a project +merge_request: 19853 +author: +type: changed diff --git a/changelogs/unreleased/bvl-graphql-start-34754.yml b/changelogs/unreleased/bvl-graphql-start-34754.yml new file mode 100644 index 00000000000..a31f46d3a61 --- /dev/null +++ b/changelogs/unreleased/bvl-graphql-start-34754.yml @@ -0,0 +1,5 @@ +--- +title: Setup graphql with initial project & merge request query +merge_request: 19008 +author: +type: added diff --git a/changelogs/unreleased/bvl-terms-on-registration.yml b/changelogs/unreleased/bvl-terms-on-registration.yml new file mode 100644 index 00000000000..3e6e499dd02 --- /dev/null +++ b/changelogs/unreleased/bvl-terms-on-registration.yml @@ -0,0 +1,5 @@ +--- +title: Users can accept terms during registration +merge_request: 19583 +author: +type: other diff --git a/changelogs/unreleased/bw-enable-commonmark.yml b/changelogs/unreleased/bw-enable-commonmark.yml new file mode 100644 index 00000000000..89252e5063d --- /dev/null +++ b/changelogs/unreleased/bw-enable-commonmark.yml @@ -0,0 +1,5 @@ +--- +title: Use CommonMark syntax and rendering for new Markdown content +merge_request: 19331 +author: +type: added diff --git a/changelogs/unreleased/cache-doc-fix.yml b/changelogs/unreleased/cache-doc-fix.yml new file mode 100644 index 00000000000..db4726a92e9 --- /dev/null +++ b/changelogs/unreleased/cache-doc-fix.yml @@ -0,0 +1,5 @@ +--- +title: 'Remove incorrect CI doc re: PowerShell' +merge_request: 19622 +author: gfyoung +type: fixed diff --git a/changelogs/unreleased/commits_api_with_stats.yml b/changelogs/unreleased/commits_api_with_stats.yml new file mode 100644 index 00000000000..4357f1a6305 --- /dev/null +++ b/changelogs/unreleased/commits_api_with_stats.yml @@ -0,0 +1,5 @@ +--- +title: Added with_statsoption for GET /projects/:id/repository/commits +merge_request: +author: +type: added diff --git a/changelogs/unreleased/da-port-cte-to-ce.yml b/changelogs/unreleased/da-port-cte-to-ce.yml new file mode 100644 index 00000000000..6fa759fcf7d --- /dev/null +++ b/changelogs/unreleased/da-port-cte-to-ce.yml @@ -0,0 +1,5 @@ +--- +title: Add Gitlab::SQL:CTE for easily building CTE statements +merge_request: +author: +type: added diff --git a/changelogs/unreleased/dm-blockquote-trailing-whitespace.yml b/changelogs/unreleased/dm-blockquote-trailing-whitespace.yml new file mode 100644 index 00000000000..98ecdde4f4c --- /dev/null +++ b/changelogs/unreleased/dm-blockquote-trailing-whitespace.yml @@ -0,0 +1,5 @@ +--- +title: Allow trailing whitespace on blockquote fence lines +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/expose-ci-url.yml b/changelogs/unreleased/expose-ci-url.yml new file mode 100644 index 00000000000..b6ad7d18e0d --- /dev/null +++ b/changelogs/unreleased/expose-ci-url.yml @@ -0,0 +1,5 @@ +--- +title: Add CI_PIPELINE_URL and CI_JOB_URL +merge_request: 19618 +author: +type: added diff --git a/changelogs/unreleased/feature-customizable-favicon.yml b/changelogs/unreleased/feature-customizable-favicon.yml new file mode 100644 index 00000000000..0e5afc17c9e --- /dev/null +++ b/changelogs/unreleased/feature-customizable-favicon.yml @@ -0,0 +1,5 @@ +--- +title: Allow changing the default favicon to a custom icon. +merge_request: 14497 +author: Alexis Reigel +type: added diff --git a/changelogs/unreleased/fix-avatars-n-plus-one.yml b/changelogs/unreleased/fix-avatars-n-plus-one.yml new file mode 100644 index 00000000000..c5b42071f2b --- /dev/null +++ b/changelogs/unreleased/fix-avatars-n-plus-one.yml @@ -0,0 +1,5 @@ +--- +title: Fix an N+1 when loading user avatars +merge_request: +author: +type: performance diff --git a/changelogs/unreleased/fix-br-decode.yml b/changelogs/unreleased/fix-br-decode.yml new file mode 100644 index 00000000000..66ecc3deb35 --- /dev/null +++ b/changelogs/unreleased/fix-br-decode.yml @@ -0,0 +1,5 @@ +--- +title: mergeError message has been binded using v-html directive +merge_request: 19058 +author: Murat Dogan +type: fixed diff --git a/changelogs/unreleased/fix-web-ide-disable-markdown-autocomplete.yml b/changelogs/unreleased/fix-web-ide-disable-markdown-autocomplete.yml new file mode 100644 index 00000000000..6a4d9b6c8c4 --- /dev/null +++ b/changelogs/unreleased/fix-web-ide-disable-markdown-autocomplete.yml @@ -0,0 +1,5 @@ +--- +title: Disabled Web IDE autocomplete suggestions for Markdown files. +merge_request: +author: Isaac Smith +type: fixed diff --git a/changelogs/unreleased/fj-34526-enabling-wiki-search-by-title.yml b/changelogs/unreleased/fj-34526-enabling-wiki-search-by-title.yml new file mode 100644 index 00000000000..2ae2cf8a23e --- /dev/null +++ b/changelogs/unreleased/fj-34526-enabling-wiki-search-by-title.yml @@ -0,0 +1,5 @@ +--- +title: Added ability to search by wiki titles +merge_request: 19112 +author: +type: added diff --git a/changelogs/unreleased/fj-40401-support-import-lfs-objects.yml b/changelogs/unreleased/fj-40401-support-import-lfs-objects.yml new file mode 100644 index 00000000000..a8abdd943ba --- /dev/null +++ b/changelogs/unreleased/fj-40401-support-import-lfs-objects.yml @@ -0,0 +1,5 @@ +--- +title: Added support for LFS Download in the importing process +merge_request: 18871 +author: +type: fixed diff --git a/changelogs/unreleased/fj-bumping-gollum-lib-and-gollum-rugged-adapter.yml b/changelogs/unreleased/fj-bumping-gollum-lib-and-gollum-rugged-adapter.yml new file mode 100644 index 00000000000..3b4d429707f --- /dev/null +++ b/changelogs/unreleased/fj-bumping-gollum-lib-and-gollum-rugged-adapter.yml @@ -0,0 +1,5 @@ +--- +title: Fixed bug that allowed to remove other wiki pages if the title had wildcard characters +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/fj-relax-url-validator-rules-for-user.yml b/changelogs/unreleased/fj-relax-url-validator-rules-for-user.yml new file mode 100644 index 00000000000..4c72e4c5c1c --- /dev/null +++ b/changelogs/unreleased/fj-relax-url-validator-rules-for-user.yml @@ -0,0 +1,5 @@ +--- +title: Avoid checking the user format in every url validation +merge_request: 19575 +author: +type: changed diff --git a/changelogs/unreleased/fj-restore-users-v3-endpoint.yml b/changelogs/unreleased/fj-restore-users-v3-endpoint.yml new file mode 100644 index 00000000000..c5f952dfa88 --- /dev/null +++ b/changelogs/unreleased/fj-restore-users-v3-endpoint.yml @@ -0,0 +1,5 @@ +--- +title: Restore API v3 user endpoint +merge_request: +author: +type: changed diff --git a/changelogs/unreleased/gh-importer-transactions.yml b/changelogs/unreleased/gh-importer-transactions.yml new file mode 100644 index 00000000000..1489d60a3fb --- /dev/null +++ b/changelogs/unreleased/gh-importer-transactions.yml @@ -0,0 +1,5 @@ +--- +title: Move PR IO operations out of a transaction +merge_request: +author: +type: performance diff --git a/changelogs/unreleased/gitaly-commit-count-opt-out.yml b/changelogs/unreleased/gitaly-commit-count-opt-out.yml new file mode 100644 index 00000000000..fd8298b1d7b --- /dev/null +++ b/changelogs/unreleased/gitaly-commit-count-opt-out.yml @@ -0,0 +1,5 @@ +--- +title: Move some Gitaly RPC's to opt-out +merge_request: 19591 +author: +type: other diff --git a/changelogs/unreleased/gitaly-opt-out-branch-tag.yml b/changelogs/unreleased/gitaly-opt-out-branch-tag.yml new file mode 100644 index 00000000000..750fc863eed --- /dev/null +++ b/changelogs/unreleased/gitaly-opt-out-branch-tag.yml @@ -0,0 +1,5 @@ +--- +title: Move Gitaly branch/tag/ref RPC's to opt-out +merge_request: 19644 +author: +type: other diff --git a/changelogs/unreleased/ide-commit-actions-update.yml b/changelogs/unreleased/ide-commit-actions-update.yml new file mode 100644 index 00000000000..35bee94e156 --- /dev/null +++ b/changelogs/unreleased/ide-commit-actions-update.yml @@ -0,0 +1,5 @@ +--- +title: Improve Web IDE commit flow +merge_request: +author: +type: changed diff --git a/changelogs/unreleased/ide-url-util-relative-url-fix.yml b/changelogs/unreleased/ide-url-util-relative-url-fix.yml new file mode 100644 index 00000000000..9f0f4a0f7be --- /dev/null +++ b/changelogs/unreleased/ide-url-util-relative-url-fix.yml @@ -0,0 +1,6 @@ +--- +title: Fixes Web IDE button on merge requests when GitLab is installed with relative + URL +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/introduce-job-keep-alive-api-endpoint.yml b/changelogs/unreleased/introduce-job-keep-alive-api-endpoint.yml new file mode 100644 index 00000000000..0789fc34f27 --- /dev/null +++ b/changelogs/unreleased/introduce-job-keep-alive-api-endpoint.yml @@ -0,0 +1,5 @@ +--- +title: Make CI job update entrypoint to work as keep-alive endpoint +merge_request: 19543 +author: +type: changed diff --git a/changelogs/unreleased/issue_44230.yml b/changelogs/unreleased/issue_44230.yml new file mode 100644 index 00000000000..2c6dba6c0fb --- /dev/null +++ b/changelogs/unreleased/issue_44230.yml @@ -0,0 +1,5 @@ +--- +title: Apply notification settings level of groups to all child objects +merge_request: +author: +type: changed diff --git a/changelogs/unreleased/jivl-smarter-system-notes.yml b/changelogs/unreleased/jivl-smarter-system-notes.yml new file mode 100644 index 00000000000..e640981de9a --- /dev/null +++ b/changelogs/unreleased/jivl-smarter-system-notes.yml @@ -0,0 +1,5 @@ +--- +title: Add support for smarter system notes +merge_request: 17164 +author: +type: changed diff --git a/changelogs/unreleased/jprovazn-uploader-migration.yml b/changelogs/unreleased/jprovazn-uploader-migration.yml new file mode 100644 index 00000000000..1db67e9ace2 --- /dev/null +++ b/changelogs/unreleased/jprovazn-uploader-migration.yml @@ -0,0 +1,5 @@ +--- +title: Migrate any remaining jobs from deprecated `object_storage_upload` queue. +merge_request: +author: +type: deprecated diff --git a/changelogs/unreleased/live-trace-v2-persist-data.yml b/changelogs/unreleased/live-trace-v2-persist-data.yml new file mode 100644 index 00000000000..3ac47b04218 --- /dev/null +++ b/changelogs/unreleased/live-trace-v2-persist-data.yml @@ -0,0 +1,5 @@ +--- +title: Add a cronworker to rescue stale live traces +merge_request: 18680 +author: +type: performance diff --git a/changelogs/unreleased/mk-rake-task-verify-remote-files.yml b/changelogs/unreleased/mk-rake-task-verify-remote-files.yml new file mode 100644 index 00000000000..772aa11d89b --- /dev/null +++ b/changelogs/unreleased/mk-rake-task-verify-remote-files.yml @@ -0,0 +1,5 @@ +--- +title: Add support for verifying remote uploads, artifacts, and LFS objects in check rake tasks +merge_request: 19501 +author: +type: added diff --git a/changelogs/unreleased/n-plus-one-notification-recipients.yml b/changelogs/unreleased/n-plus-one-notification-recipients.yml new file mode 100644 index 00000000000..91c31e4c930 --- /dev/null +++ b/changelogs/unreleased/n-plus-one-notification-recipients.yml @@ -0,0 +1,5 @@ +--- +title: Fix some sources of excessive query counts when calculating notification recipients +merge_request: +author: +type: performance diff --git a/changelogs/unreleased/optimise-pages-service-calling.yml b/changelogs/unreleased/optimise-pages-service-calling.yml new file mode 100644 index 00000000000..e017e6b01f1 --- /dev/null +++ b/changelogs/unreleased/optimise-pages-service-calling.yml @@ -0,0 +1,5 @@ +--- +title: Optimise PagesWorker usage +merge_request: +author: +type: performance diff --git a/changelogs/unreleased/optimise-paused-runners.yml b/changelogs/unreleased/optimise-paused-runners.yml new file mode 100644 index 00000000000..13097e507d3 --- /dev/null +++ b/changelogs/unreleased/optimise-paused-runners.yml @@ -0,0 +1,5 @@ +--- +title: Optimise paused runners to reduce amount of used requests +merge_request: +author: +type: performance diff --git a/changelogs/unreleased/optimise-runner-update-cached-info.yml b/changelogs/unreleased/optimise-runner-update-cached-info.yml new file mode 100644 index 00000000000..15fb9bcdf41 --- /dev/null +++ b/changelogs/unreleased/optimise-runner-update-cached-info.yml @@ -0,0 +1,5 @@ +--- +title: Update runner cached informations without performing validations +merge_request: +author: +type: performance diff --git a/changelogs/unreleased/osw-ignore-diff-header-when-persisting-diff-hunk.yml b/changelogs/unreleased/osw-ignore-diff-header-when-persisting-diff-hunk.yml new file mode 100644 index 00000000000..ef66deaa0ef --- /dev/null +++ b/changelogs/unreleased/osw-ignore-diff-header-when-persisting-diff-hunk.yml @@ -0,0 +1,5 @@ +--- +title: Adjust insufficient diff hunks being persisted on NoteDiffFile +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/per-project-pipeline-iid.yml b/changelogs/unreleased/per-project-pipeline-iid.yml new file mode 100644 index 00000000000..78a513a9986 --- /dev/null +++ b/changelogs/unreleased/per-project-pipeline-iid.yml @@ -0,0 +1,5 @@ +--- +title: Add per-project pipeline id +merge_request: 18558 +author: +type: added diff --git a/changelogs/unreleased/pr-importer-io-extra-error-handling.yml b/changelogs/unreleased/pr-importer-io-extra-error-handling.yml new file mode 100644 index 00000000000..2f7121b2840 --- /dev/null +++ b/changelogs/unreleased/pr-importer-io-extra-error-handling.yml @@ -0,0 +1,5 @@ +--- +title: Ensure MR diffs always exist in the PR importer +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/presigned-multipart-uploads.yml b/changelogs/unreleased/presigned-multipart-uploads.yml new file mode 100644 index 00000000000..52fae6534fd --- /dev/null +++ b/changelogs/unreleased/presigned-multipart-uploads.yml @@ -0,0 +1,5 @@ +--- +title: Support direct_upload with S3 Multipart uploads +merge_request: +author: +type: added diff --git a/changelogs/unreleased/rails5-fix-46236.yml b/changelogs/unreleased/rails5-fix-46236.yml new file mode 100644 index 00000000000..9203b448bed --- /dev/null +++ b/changelogs/unreleased/rails5-fix-46236.yml @@ -0,0 +1,5 @@ +--- +title: Support rails5 in postgres indexes function and fix some migrations +merge_request: 19400 +author: Jasper Maes +type: fixed diff --git a/changelogs/unreleased/rails5-fix-46281.yml b/changelogs/unreleased/rails5-fix-46281.yml new file mode 100644 index 00000000000..ee0b8531988 --- /dev/null +++ b/changelogs/unreleased/rails5-fix-46281.yml @@ -0,0 +1,5 @@ +--- +title: Rails5 fix arel from +merge_request: 19340 +author: Jasper Maes +type: fixed diff --git a/changelogs/unreleased/rails5-fix-47366.yml b/changelogs/unreleased/rails5-fix-47366.yml new file mode 100644 index 00000000000..7ea03d2b95e --- /dev/null +++ b/changelogs/unreleased/rails5-fix-47366.yml @@ -0,0 +1,5 @@ +--- +title: Rails5 fix expected `issuable.reload.updated_at` to have changed +merge_request: 19733 +author: Jasper Maes +type: fixed diff --git a/changelogs/unreleased/rails5-fix-47368.yml b/changelogs/unreleased/rails5-fix-47368.yml new file mode 100644 index 00000000000..81bb1adabff --- /dev/null +++ b/changelogs/unreleased/rails5-fix-47368.yml @@ -0,0 +1,6 @@ +--- +title: 'Rails 5 fix unknown keywords: changes, key_id, project, gl_repository, action, + secret_token, protocol' +merge_request: 19466 +author: Jasper Maes +type: fixed diff --git a/changelogs/unreleased/rails5-fix-47370.yml b/changelogs/unreleased/rails5-fix-47370.yml new file mode 100644 index 00000000000..90c19593b7d --- /dev/null +++ b/changelogs/unreleased/rails5-fix-47370.yml @@ -0,0 +1,5 @@ +--- +title: Use same gem versions for rails5 as for rails4 where possible +merge_request: 19498 +author: Jasper Maes +type: fixed diff --git a/changelogs/unreleased/rails5-fix-47376.yml b/changelogs/unreleased/rails5-fix-47376.yml new file mode 100644 index 00000000000..ac9950e908e --- /dev/null +++ b/changelogs/unreleased/rails5-fix-47376.yml @@ -0,0 +1,5 @@ +--- +title: Rails 5 fix glob spec +merge_request: 19469 +author: Jasper Maes +type: fixed diff --git a/changelogs/unreleased/rails5-fix-47804.yml b/changelogs/unreleased/rails5-fix-47804.yml new file mode 100644 index 00000000000..3332ed3bbaa --- /dev/null +++ b/changelogs/unreleased/rails5-fix-47804.yml @@ -0,0 +1,5 @@ +--- +title: Rails5 fix stack level too deep +merge_request: 19762 +author: Jasper Maes +type: fixed diff --git a/changelogs/unreleased/rails5-fix-47805.yml b/changelogs/unreleased/rails5-fix-47805.yml new file mode 100644 index 00000000000..8bd8ad5488c --- /dev/null +++ b/changelogs/unreleased/rails5-fix-47805.yml @@ -0,0 +1,6 @@ +--- +title: 'Rails5 ActionController::ParameterMissing: param is missing or the value is + empty: application_setting' +merge_request: 19763 +author: Jasper Maes +type: fixed diff --git a/changelogs/unreleased/rails5-fix-47835.yml b/changelogs/unreleased/rails5-fix-47835.yml new file mode 100644 index 00000000000..fe9cbf1a03a --- /dev/null +++ b/changelogs/unreleased/rails5-fix-47835.yml @@ -0,0 +1,6 @@ +--- +title: Rails5 fix no implicit conversion of Hash into String. ActionController::Parameters + no longer returns an hash in Rails 5 +merge_request: 19792 +author: Jasper Maes +type: fixed diff --git a/changelogs/unreleased/rails5-fix-47836.yml b/changelogs/unreleased/rails5-fix-47836.yml new file mode 100644 index 00000000000..2aef2db607a --- /dev/null +++ b/changelogs/unreleased/rails5-fix-47836.yml @@ -0,0 +1,6 @@ +--- +title: Rails5 fix passing Group objects array into for_projects_and_groups milestone + scope +merge_request: 19863 +author: Jasper Maes +type: fixed diff --git a/changelogs/unreleased/rails5-fix-47960.yml b/changelogs/unreleased/rails5-fix-47960.yml new file mode 100644 index 00000000000..2229511ccd6 --- /dev/null +++ b/changelogs/unreleased/rails5-fix-47960.yml @@ -0,0 +1,5 @@ +--- +title: Rails5 fix update_attribute usage not causing a save +merge_request: 19881 +author: Jasper Maes +type: fixed diff --git a/changelogs/unreleased/rails5-fix-48009.yml b/changelogs/unreleased/rails5-fix-48009.yml new file mode 100644 index 00000000000..7ade9ef2b7d --- /dev/null +++ b/changelogs/unreleased/rails5-fix-48009.yml @@ -0,0 +1,5 @@ +--- +title: Rails5 update Gemfile.rails5.lock +merge_request: 19921 +author: Jasper Maes +type: fixed diff --git a/changelogs/unreleased/rails5-fix-48012.yml b/changelogs/unreleased/rails5-fix-48012.yml new file mode 100644 index 00000000000..953ccbd8af7 --- /dev/null +++ b/changelogs/unreleased/rails5-fix-48012.yml @@ -0,0 +1,6 @@ +--- +title: Rails5 fix passing Group objects array into for_projects_and_groups milestone + scope +merge_request: 19920 +author: Jasper Maes +type: fixed diff --git a/changelogs/unreleased/rails5-fix-db-check.yml b/changelogs/unreleased/rails5-fix-db-check.yml new file mode 100644 index 00000000000..ccac4619ea7 --- /dev/null +++ b/changelogs/unreleased/rails5-fix-db-check.yml @@ -0,0 +1,5 @@ +--- +title: Rails5 fix connection execute return integer instead of string +merge_request: 19901 +author: Jasper Maes +type: fixed diff --git a/changelogs/unreleased/rd-33733-showing-created-date-instead-of-updated-date-in-project-lists.yml b/changelogs/unreleased/rd-33733-showing-created-date-instead-of-updated-date-in-project-lists.yml new file mode 100644 index 00000000000..3934381b0cf --- /dev/null +++ b/changelogs/unreleased/rd-33733-showing-created-date-instead-of-updated-date-in-project-lists.yml @@ -0,0 +1,5 @@ +--- +title: Invalidate cache with project details when repository is updated +merge_request: 19774 +author: +type: fixed diff --git a/changelogs/unreleased/rd-44364-deprecate-support-for-dsa-keys.yml b/changelogs/unreleased/rd-44364-deprecate-support-for-dsa-keys.yml new file mode 100644 index 00000000000..1a52ffaaf79 --- /dev/null +++ b/changelogs/unreleased/rd-44364-deprecate-support-for-dsa-keys.yml @@ -0,0 +1,5 @@ +--- +title: Add migration to disable the usage of DSA keys +merge_request: 19299 +author: +type: other diff --git a/changelogs/unreleased/remove-ci_job_request_with_tags_matcher.yml b/changelogs/unreleased/remove-ci_job_request_with_tags_matcher.yml new file mode 100644 index 00000000000..b86512445d5 --- /dev/null +++ b/changelogs/unreleased/remove-ci_job_request_with_tags_matcher.yml @@ -0,0 +1,5 @@ +--- +title: Remove the ci_job_request_with_tags_matcher +merge_request: +author: +type: performance diff --git a/changelogs/unreleased/remove-link-label-vertical-alignment-property.yml b/changelogs/unreleased/remove-link-label-vertical-alignment-property.yml new file mode 100644 index 00000000000..40ec3998b05 --- /dev/null +++ b/changelogs/unreleased/remove-link-label-vertical-alignment-property.yml @@ -0,0 +1,5 @@ +--- +title: Change label link vertical alignment property +merge_request: 18777 +author: George Tsiolis +type: changed diff --git a/changelogs/unreleased/remove-small-container-width.yml b/changelogs/unreleased/remove-small-container-width.yml new file mode 100644 index 00000000000..1af8aafa87e --- /dev/null +++ b/changelogs/unreleased/remove-small-container-width.yml @@ -0,0 +1,5 @@ +--- +title: Remove small container width +merge_request: 19893 +author: George Tsiolis +type: changed diff --git a/changelogs/unreleased/remove-unused-query-in-hooks.yml b/changelogs/unreleased/remove-unused-query-in-hooks.yml new file mode 100644 index 00000000000..ef40b2db5a9 --- /dev/null +++ b/changelogs/unreleased/remove-unused-query-in-hooks.yml @@ -0,0 +1,5 @@ +--- +title: Remove unused running_or_pending_build_count +merge_request: +author: +type: performance diff --git a/changelogs/unreleased/rosulk-patch-12.yml b/changelogs/unreleased/rosulk-patch-12.yml new file mode 100644 index 00000000000..9637c88d1a4 --- /dev/null +++ b/changelogs/unreleased/rosulk-patch-12.yml @@ -0,0 +1,5 @@ +--- +title: Flex issue board columns +merge_request: 19250 +author: Roman Rosluk +type: changed diff --git a/changelogs/unreleased/safari-scrollbar-bug.yml b/changelogs/unreleased/safari-scrollbar-bug.yml new file mode 100644 index 00000000000..792a66d1ada --- /dev/null +++ b/changelogs/unreleased/safari-scrollbar-bug.yml @@ -0,0 +1,5 @@ +--- +title: Remove scrollbar in Safari in repo settings page +merge_request: 19809 +author: gfyoung +type: fixed diff --git a/changelogs/unreleased/sh-add-uncached-query-limiter.yml b/changelogs/unreleased/sh-add-uncached-query-limiter.yml new file mode 100644 index 00000000000..4318338c229 --- /dev/null +++ b/changelogs/unreleased/sh-add-uncached-query-limiter.yml @@ -0,0 +1,5 @@ +--- +title: Remove N+1 query for author in issues API +merge_request: +author: +type: performance diff --git a/changelogs/unreleased/sh-expire-content-cache-after-import.yml b/changelogs/unreleased/sh-expire-content-cache-after-import.yml new file mode 100644 index 00000000000..8876a487b86 --- /dev/null +++ b/changelogs/unreleased/sh-expire-content-cache-after-import.yml @@ -0,0 +1,5 @@ +--- +title: Expire Wiki content cache after importing a repository +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/sh-fix-events-nplus-one.yml b/changelogs/unreleased/sh-fix-events-nplus-one.yml new file mode 100644 index 00000000000..e5a974bef30 --- /dev/null +++ b/changelogs/unreleased/sh-fix-events-nplus-one.yml @@ -0,0 +1,5 @@ +--- +title: Eliminate N+1 queries with authors and push_data_payload in Events API +merge_request: +author: +type: performance diff --git a/changelogs/unreleased/sh-fix-pipeline-jobs-nplus-one.yml b/changelogs/unreleased/sh-fix-pipeline-jobs-nplus-one.yml new file mode 100644 index 00000000000..eac00f4fca6 --- /dev/null +++ b/changelogs/unreleased/sh-fix-pipeline-jobs-nplus-one.yml @@ -0,0 +1,5 @@ +--- +title: Eliminate N+1 queries for CI job artifacts in /api/prjoects/:id/pipelines/:pipeline_id/jobs +merge_request: +author: +type: performance diff --git a/changelogs/unreleased/sh-fix-secrets-not-working.yml b/changelogs/unreleased/sh-fix-secrets-not-working.yml new file mode 100644 index 00000000000..044a873ecd9 --- /dev/null +++ b/changelogs/unreleased/sh-fix-secrets-not-working.yml @@ -0,0 +1,5 @@ +--- +title: Fix attr_encryption key settings +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/sh-fix-source-project-nplus-one.yml b/changelogs/unreleased/sh-fix-source-project-nplus-one.yml new file mode 100644 index 00000000000..9d78ad6408c --- /dev/null +++ b/changelogs/unreleased/sh-fix-source-project-nplus-one.yml @@ -0,0 +1,5 @@ +--- +title: Fix N+1 with source_projects in merge requests API +merge_request: +author: +type: performance diff --git a/changelogs/unreleased/sh-improve-import-status-error.yml b/changelogs/unreleased/sh-improve-import-status-error.yml new file mode 100644 index 00000000000..6523280f9e6 --- /dev/null +++ b/changelogs/unreleased/sh-improve-import-status-error.yml @@ -0,0 +1,5 @@ +--- +title: Show a more helpful error for import status +merge_request: +author: +type: other diff --git a/changelogs/unreleased/sh-log-422-responses.yml b/changelogs/unreleased/sh-log-422-responses.yml new file mode 100644 index 00000000000..c7dfdbb703b --- /dev/null +++ b/changelogs/unreleased/sh-log-422-responses.yml @@ -0,0 +1,6 @@ +--- +title: Log response body to production_json.log when a controller responds with a + 422 status +merge_request: +author: +type: other diff --git a/changelogs/unreleased/sql-buckets.yml b/changelogs/unreleased/sql-buckets.yml new file mode 100644 index 00000000000..afb13d5cb20 --- /dev/null +++ b/changelogs/unreleased/sql-buckets.yml @@ -0,0 +1,5 @@ +--- +title: Adjust SQL and transaction Prometheus buckets +merge_request: +author: +type: other diff --git a/changelogs/unreleased/tz-diff-blob-image-viewer.yml b/changelogs/unreleased/tz-diff-blob-image-viewer.yml new file mode 100644 index 00000000000..81d87bc71f5 --- /dev/null +++ b/changelogs/unreleased/tz-diff-blob-image-viewer.yml @@ -0,0 +1,5 @@ +--- +title: Web IDE supports now Image + Download Diff Viewing +merge_request: 18768 +author: +type: added diff --git a/changelogs/unreleased/unify-views-search-results.yml b/changelogs/unreleased/unify-views-search-results.yml new file mode 100644 index 00000000000..81ad3616648 --- /dev/null +++ b/changelogs/unreleased/unify-views-search-results.yml @@ -0,0 +1,5 @@ +--- +title: Hide project name if searching against a project +merge_request: 19595 +author: +type: changed diff --git a/changelogs/unreleased/update-help-integration-screenshot.yml b/changelogs/unreleased/update-help-integration-screenshot.yml new file mode 100644 index 00000000000..b1f76a346e4 --- /dev/null +++ b/changelogs/unreleased/update-help-integration-screenshot.yml @@ -0,0 +1,5 @@ +--- +title: Update screenshot in Gitlab.com integration documentation +merge_request: 19433 +author: Tuğçe Nur Taş +type: other diff --git a/changelogs/unreleased/upgrade-gitlab-markup.yml b/changelogs/unreleased/upgrade-gitlab-markup.yml new file mode 100644 index 00000000000..9b0eaa7068d --- /dev/null +++ b/changelogs/unreleased/upgrade-gitlab-markup.yml @@ -0,0 +1,5 @@ +--- +title: Fix extra blank line at start of rendered reStructuredText code block +merge_request: 19596 +author: +type: fixed diff --git a/changelogs/unreleased/use-tooltip-component-in-mr-widget-author-time-component.yml b/changelogs/unreleased/use-tooltip-component-in-mr-widget-author-time-component.yml new file mode 100644 index 00000000000..4ab9b6dadc0 --- /dev/null +++ b/changelogs/unreleased/use-tooltip-component-in-mr-widget-author-time-component.yml @@ -0,0 +1,5 @@ +--- +title: Use Tooltip component in MrWidgetAuthorTime vue comonent +merge_request: 19635 +author: George Tsiolis +type: performance diff --git a/changelogs/unreleased/winh-new-branch-url-encode.yml b/changelogs/unreleased/winh-new-branch-url-encode.yml new file mode 100644 index 00000000000..f3554d0d4a1 --- /dev/null +++ b/changelogs/unreleased/winh-new-branch-url-encode.yml @@ -0,0 +1,5 @@ +--- +title: Fix branch name encoding for dropdown on issue page +merge_request: 19634 +author: +type: fixed diff --git a/config/application.rb b/config/application.rb index 1b575f1325d..95f6d2c9af1 100644 --- a/config/application.rb +++ b/config/application.rb @@ -12,6 +12,7 @@ module Gitlab require_dependency Rails.root.join('lib/gitlab/redis/shared_state') require_dependency Rails.root.join('lib/gitlab/request_context') require_dependency Rails.root.join('lib/gitlab/current_settings') + require_dependency Rails.root.join('lib/gitlab/middleware/read_only') # Settings in config/environments/* take precedence over those specified here. # Application configuration should go into files in config/initializers @@ -70,6 +71,7 @@ module Gitlab # - Webhook URLs (:hook) # - Sentry DSN (:sentry_dsn) # - Deploy keys (:key) + # - File content from Web Editor (:content) config.filter_parameters += [/token$/, /password/, /secret/] config.filter_parameters += %i( certificate @@ -81,6 +83,7 @@ module Gitlab sentry_dsn trace variables + content ) # Enable escaping HTML in JSON. @@ -173,7 +176,7 @@ module Gitlab ENV['GIT_TERMINAL_PROMPT'] = '0' # Gitlab Read-only middleware support - config.middleware.insert_after ActionDispatch::Flash, '::Gitlab::Middleware::ReadOnly' + config.middleware.insert_after ActionDispatch::Flash, ::Gitlab::Middleware::ReadOnly config.generators do |g| g.factory_bot false diff --git a/config/dependency_decisions.yml b/config/dependency_decisions.yml index 6616b85129e..21c20cd5e93 100644 --- a/config/dependency_decisions.yml +++ b/config/dependency_decisions.yml @@ -534,3 +534,9 @@ :why: https://github.com/squaremo/bitsyntax-js/blob/master/LICENSE-MIT :versions: [] :when: 2018-02-20 22:20:25.958123000 Z +- - :approve + - "@webassemblyjs/ieee754" + - :who: Mike Greiling + :why: https://github.com/xtuc/webassemblyjs/blob/master/LICENSE + :versions: [] + :when: 2018-06-08 05:30:56.764116000 Z diff --git a/config/environments/test.rb b/config/environments/test.rb index 1849c984351..af1011a1ab1 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -1,7 +1,7 @@ Rails.application.configure do # Make sure the middleware is inserted first in middleware chain - config.middleware.insert_before('ActionDispatch::Static', 'Gitlab::Testing::RequestBlockerMiddleware') - config.middleware.insert_before('ActionDispatch::Static', 'Gitlab::Testing::RequestInspectorMiddleware') + config.middleware.insert_before(ActionDispatch::Static, Gitlab::Testing::RequestBlockerMiddleware) + config.middleware.insert_before(ActionDispatch::Static, Gitlab::Testing::RequestInspectorMiddleware) # Settings specified here will take precedence over those in config/application.rb diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index 7eb44b8059e..489dc8840e5 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -78,10 +78,15 @@ production: &base # username_changing_enabled: false # default: true - User can change her username/namespace ## Default theme ID ## 1 - Indigo - ## 2 - Dark - ## 3 - Light - ## 4 - Blue + ## 2 - Light Indigo + ## 3 - Blue + ## 4 - Light Blue ## 5 - Green + ## 6 - Light Green + ## 7 - Red + ## 8 - Light Red + ## 9 - Dark + ## 10 - Light # default_theme: 1 # default: 1 ## Automatic issue closing diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index a0e3ab0d343..12d09150127 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -289,6 +289,9 @@ Settings.cron_jobs['repository_archive_cache_worker']['job_class'] = 'Repository Settings.cron_jobs['import_export_project_cleanup_worker'] ||= Settingslogic.new({}) Settings.cron_jobs['import_export_project_cleanup_worker']['cron'] ||= '0 * * * *' Settings.cron_jobs['import_export_project_cleanup_worker']['job_class'] = 'ImportExportProjectCleanupWorker' +Settings.cron_jobs['ci_archive_traces_cron_worker'] ||= Settingslogic.new({}) +Settings.cron_jobs['ci_archive_traces_cron_worker']['cron'] ||= '17 * * * *' +Settings.cron_jobs['ci_archive_traces_cron_worker']['job_class'] = 'Ci::ArchiveTracesCronWorker' Settings.cron_jobs['requests_profiles_worker'] ||= Settingslogic.new({}) Settings.cron_jobs['requests_profiles_worker']['cron'] ||= '0 0 * * *' Settings.cron_jobs['requests_profiles_worker']['job_class'] = 'RequestsProfilesWorker' diff --git a/config/initializers/active_record_locking.rb b/config/initializers/active_record_locking.rb index 3e7111fd063..0861544c5a4 100644 --- a/config/initializers/active_record_locking.rb +++ b/config/initializers/active_record_locking.rb @@ -1,73 +1,80 @@ # rubocop:disable Lint/RescueException -# Remove this entire initializer when we are at rails 5.0. -# This file fixes the bug (see below) which has been fixed in the upstream. -unless Gitlab.rails5? - # This patch fixes https://github.com/rails/rails/issues/26024 - # TODO: Remove it when it's no longer necessary - - module ActiveRecord - module Locking - module Optimistic - # We overwrite this method because we don't want to have default value - # for newly created records - def _create_record(attribute_names = self.attribute_names, *) # :nodoc: - super - end +# Remove this monkey-patch when all lock_version values are converted from NULLs to zeros. +# See https://gitlab.com/gitlab-org/gitlab-ce/issues/25228 +module ActiveRecord + module Locking + module Optimistic + # We overwrite this method because we don't want to have default value + # for newly created records + def _create_record(attribute_names = self.attribute_names, *) # :nodoc: + super + end - def _update_record(attribute_names = self.attribute_names) #:nodoc: - return super unless locking_enabled? - return 0 if attribute_names.empty? + def _update_record(attribute_names = self.attribute_names) #:nodoc: + return super unless locking_enabled? + return 0 if attribute_names.empty? - lock_col = self.class.locking_column + lock_col = self.class.locking_column - previous_lock_value = send(lock_col).to_i # rubocop:disable GitlabSecurity/PublicSend + previous_lock_value = send(lock_col).to_i # rubocop:disable GitlabSecurity/PublicSend - # This line is added as a patch - previous_lock_value = nil if previous_lock_value == '0' || previous_lock_value == 0 + # This line is added as a patch + previous_lock_value = nil if previous_lock_value == '0' || previous_lock_value == 0 - increment_lock + increment_lock - attribute_names += [lock_col] - attribute_names.uniq! + attribute_names += [lock_col] + attribute_names.uniq! - begin - relation = self.class.unscoped + begin + relation = self.class.unscoped - affected_rows = relation.where( - self.class.primary_key => id, - lock_col => previous_lock_value - ).update_all( - attributes_for_update(attribute_names).map do |name| - [name, _read_attribute(name)] - end.to_h - ) + affected_rows = relation.where( + self.class.primary_key => id, + lock_col => previous_lock_value + ).update_all( + attributes_for_update(attribute_names).map do |name| + [name, _read_attribute(name)] + end.to_h + ) - unless affected_rows == 1 - raise ActiveRecord::StaleObjectError.new(self, "update") - end + unless affected_rows == 1 + raise ActiveRecord::StaleObjectError.new(self, "update") + end - affected_rows + affected_rows - # If something went wrong, revert the version. - rescue Exception - send(lock_col + '=', previous_lock_value) # rubocop:disable GitlabSecurity/PublicSend - raise - end + # If something went wrong, revert the version. + rescue Exception + send(lock_col + '=', previous_lock_value) # rubocop:disable GitlabSecurity/PublicSend + raise end + end - # This is patched because we need it to query `lock_version IS NULL` - # rather than `lock_version = 0` whenever lock_version is NULL. - def relation_for_destroy - return super unless locking_enabled? + # This is patched because we need it to query `lock_version IS NULL` + # rather than `lock_version = 0` whenever lock_version is NULL. + def relation_for_destroy + return super unless locking_enabled? - column_name = self.class.locking_column - super.where(self.class.arel_table[column_name].eq(self[column_name])) - end + column_name = self.class.locking_column + super.where(self.class.arel_table[column_name].eq(self[column_name])) end + end + + # This is patched because we want `lock_version` default to `NULL` + # rather than `0` + if Gitlab.rails5? + class LockingType + def deserialize(value) + super + end - # This is patched because we want `lock_version` default to `NULL` - # rather than `0` + def serialize(value) + super + end + end + else class LockingType < SimpleDelegator def type_cast_from_database(value) super diff --git a/config/initializers/active_record_migration.rb b/config/initializers/active_record_migration.rb new file mode 100644 index 00000000000..04c06be7834 --- /dev/null +++ b/config/initializers/active_record_migration.rb @@ -0,0 +1,10 @@ +require 'active_record/migration' + +module ActiveRecord + class Migration + # data_source_exists? is not available in 4.2.10, table_exists deprecated in 5.0 + def table_exists?(table_name) + ActiveRecord::Base.connection.data_source_exists?(table_name) + end + end +end diff --git a/config/initializers/artifacts_direct_upload_support.rb b/config/initializers/artifacts_direct_upload_support.rb deleted file mode 100644 index d2bc35ea613..00000000000 --- a/config/initializers/artifacts_direct_upload_support.rb +++ /dev/null @@ -1,7 +0,0 @@ -artifacts_object_store = Gitlab.config.artifacts.object_store - -if artifacts_object_store.enabled && - artifacts_object_store.direct_upload && - artifacts_object_store.connection&.provider.to_s != 'Google' - raise "Only 'Google' is supported as a object storage provider when 'direct_upload' of artifacts is used" -end diff --git a/config/initializers/direct_upload_support.rb b/config/initializers/direct_upload_support.rb new file mode 100644 index 00000000000..32fc8c8bc69 --- /dev/null +++ b/config/initializers/direct_upload_support.rb @@ -0,0 +1,19 @@ +class DirectUploadsValidator + SUPPORTED_DIRECT_UPLOAD_PROVIDERS = %w(Google AWS).freeze + + ValidationError = Class.new(StandardError) + + def verify!(object_store) + return unless object_store.enabled + return unless object_store.direct_upload + return if SUPPORTED_DIRECT_UPLOAD_PROVIDERS.include?(object_store.connection&.provider.to_s) + + raise ValidationError, "Only #{SUPPORTED_DIRECT_UPLOAD_PROVIDERS.join(',')} are supported as a object storage provider when 'direct_upload' is used" + end +end + +DirectUploadsValidator.new.tap do |validator| + [Gitlab.config.artifacts, Gitlab.config.uploads, Gitlab.config.lfs].each do |uploader| + validator.verify!(uploader.object_store) + end +end diff --git a/config/initializers/disable_email_interceptor.rb b/config/initializers/disable_email_interceptor.rb index c76a6b8b19f..e8770c8d460 100644 --- a/config/initializers/disable_email_interceptor.rb +++ b/config/initializers/disable_email_interceptor.rb @@ -1,2 +1,5 @@ # Interceptor in lib/disable_email_interceptor.rb -ActionMailer::Base.register_interceptor(DisableEmailInterceptor) unless Gitlab.config.gitlab.email_enabled +unless Gitlab.config.gitlab.email_enabled + ActionMailer::Base.register_interceptor(DisableEmailInterceptor) + ActionMailer::Base.logger = nil +end diff --git a/config/initializers/lograge.rb b/config/initializers/lograge.rb index 114c1cb512f..1cf8a24e98c 100644 --- a/config/initializers/lograge.rb +++ b/config/initializers/lograge.rb @@ -27,6 +27,7 @@ unless Sidekiq.server? gitaly_calls = Gitlab::GitalyClient.get_request_count payload[:gitaly_calls] = gitaly_calls if gitaly_calls > 0 + payload[:response] = event.payload[:response] if event.payload[:response] payload end diff --git a/config/initializers/mime_types.rb b/config/initializers/mime_types.rb index e9326653cbe..acbdf8de5a6 100644 --- a/config/initializers/mime_types.rb +++ b/config/initializers/mime_types.rb @@ -15,3 +15,5 @@ Mime::Type.register "video/ogg", :ogv Mime::Type.unregister :json Mime::Type.register 'application/json', :json, [LfsRequest::CONTENT_TYPE, 'application/json'] + +Mime::Type.register 'image/x-icon', :ico diff --git a/config/initializers/mini_magick.rb b/config/initializers/mini_magick.rb new file mode 100644 index 00000000000..db0e7bbaaa3 --- /dev/null +++ b/config/initializers/mini_magick.rb @@ -0,0 +1,3 @@ +MiniMagick.configure do |config| + config.cli = :graphicsmagick +end diff --git a/config/initializers/postgresql_opclasses_support.rb b/config/initializers/postgresql_opclasses_support.rb index c2f3023b330..7b8afc78817 100644 --- a/config/initializers/postgresql_opclasses_support.rb +++ b/config/initializers/postgresql_opclasses_support.rb @@ -41,7 +41,12 @@ module ActiveRecord # Abstract representation of an index definition on a table. Instances of # this type are typically created and returned by methods in database # adapters. e.g. ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter#indexes - class IndexDefinition < Struct.new(:table, :name, :unique, :columns, :lengths, :orders, :where, :type, :using, :opclasses) #:nodoc: + attrs = [:table, :name, :unique, :columns, :lengths, :orders, :where, :type, :using, :opclasses] + + # In Rails 5 the second last attribute is newly `:comment` + attrs.insert(-2, :comment) if Gitlab.rails5? + + class IndexDefinition < Struct.new(*attrs) #:nodoc: end end end @@ -107,8 +112,15 @@ module ActiveRecord result.map do |row| index_name = row[0] - unique = row[1] == 't' + unique = if Gitlab.rails5? + row[1] + else + row[1] == 't' + end indkey = row[2].split(" ") + if Gitlab.rails5? + indkey = indkey.map(&:to_i) + end inddef = row[3] oid = row[4] diff --git a/config/initializers/rack_attack_global.rb b/config/initializers/rack_attack_global.rb index a90516eee7d..45963831c41 100644 --- a/config/initializers/rack_attack_global.rb +++ b/config/initializers/rack_attack_global.rb @@ -26,7 +26,7 @@ class Rack::Attack throttle('throttle_unauthenticated', Gitlab::Throttle.unauthenticated_options) do |req| Gitlab::Throttle.settings.throttle_unauthenticated_enabled && req.unauthenticated? && - !req.api_internal_request? && + !req.should_be_skipped? && req.ip end @@ -59,6 +59,10 @@ class Rack::Attack path =~ %r{^/api/v\d+/internal/} end + def should_be_skipped? + api_internal_request? + end + def web_request? !api_request? end diff --git a/config/locales/carrierwave.en.yml b/config/locales/carrierwave.en.yml new file mode 100644 index 00000000000..12619226460 --- /dev/null +++ b/config/locales/carrierwave.en.yml @@ -0,0 +1,14 @@ +en: + errors: + messages: + carrierwave_processing_error: failed to be processed + carrierwave_integrity_error: is not of an allowed file type + carrierwave_download_error: could not be downloaded + extension_whitelist_error: "You are not allowed to upload %{extension} files, allowed types: %{allowed_types}" + extension_blacklist_error: "You are not allowed to upload %{extension} files, prohibited types: %{prohibited_types}" + content_type_whitelist_error: "You are not allowed to upload %{content_type} files" + content_type_blacklist_error: "You are not allowed to upload %{content_type} files" + rmagick_processing_error: "Failed to manipulate with rmagick, maybe it is not an image?" + mini_magick_processing_error: "Failed to manipulate with MiniMagick, maybe it is not an image? Original Error: %{e}" + min_size_error: "File size should be greater than %{min_size}" + max_size_error: "File size should be less than %{max_size}" diff --git a/config/prometheus/additional_metrics.yml b/config/prometheus/additional_metrics.yml index 13732384953..c994bad7865 100644 --- a/config/prometheus/additional_metrics.yml +++ b/config/prometheus/additional_metrics.yml @@ -29,14 +29,14 @@ label: Pod average unit: ms - title: "HTTP Error Rate" - y_label: "HTTP 500 Errors / Sec" + y_label: "HTTP Errors" required_metrics: - nginx_upstream_responses_total weight: 1 queries: - - query_range: 'sum(rate(nginx_upstream_responses_total{status_code="5xx", upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"}[2m]))' - label: HTTP Errors - unit: "errors / sec" + - query_range: 'sum(rate(nginx_upstream_responses_total{status_code="5xx", upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"}[2m])) / sum(rate(nginx_upstream_responses_total{upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"}[2m])) * 100' + label: 5xx Errors + unit: "%" - group: Response metrics (HA Proxy) priority: 10 metrics: diff --git a/config/routes.rb b/config/routes.rb index 52726f94753..e0a9139b1b4 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -11,6 +11,12 @@ Rails.application.routes.draw do post :toggle_award_emoji, on: :member end + favicon_redirect = redirect do |_params, _request| + ActionController::Base.helpers.asset_url(Gitlab::Favicon.main) + end + get 'favicon.png', to: favicon_redirect + get 'favicon.ico', to: favicon_redirect + draw :sherlock draw :development draw :ci diff --git a/config/routes/admin.rb b/config/routes/admin.rb index 3cca1210e39..ff27ceb50dc 100644 --- a/config/routes/admin.rb +++ b/config/routes/admin.rb @@ -102,6 +102,7 @@ namespace :admin do get :preview_sign_in delete :logo delete :header_logos + delete :favicon end end diff --git a/config/routes/api.rb b/config/routes/api.rb index ce7a7c88900..b1aebf4d606 100644 --- a/config/routes/api.rb +++ b/config/routes/api.rb @@ -1,2 +1,7 @@ +constraints(::Constraints::FeatureConstrainer.new(:graphql)) do + post '/api/graphql', to: 'graphql#execute' + mount GraphiQL::Rails::Engine, at: '/-/graphql-explorer', graphql_path: '/api/graphql' +end + API::API.logger Rails.logger mount API::API => '/' diff --git a/config/routes/uploads.rb b/config/routes/uploads.rb index 6370645bcb9..6becadd57ae 100644 --- a/config/routes/uploads.rb +++ b/config/routes/uploads.rb @@ -17,7 +17,7 @@ scope path: :uploads do # Appearance get "-/system/:model/:mounted_as/:id/:filename", to: "uploads#show", - constraints: { model: /appearance/, mounted_as: /logo|header_logo/, filename: /.+/ } + constraints: { model: /appearance/, mounted_as: /logo|header_logo|favicon/, filename: /.+/ } # Project markdown uploads get ":namespace_id/:project_id/:secret/:filename", diff --git a/config/routes/wiki.rb b/config/routes/wiki.rb index c2da84ff6f2..cd3828b743c 100644 --- a/config/routes/wiki.rb +++ b/config/routes/wiki.rb @@ -6,7 +6,7 @@ scope(controller: :wikis) do post '/', to: 'wikis#create' end - scope(path: 'wikis/*id', as: :wiki, format: false) do + scope(path: 'wikis/*id', as: :wiki, format: false, defaults: { format: :html }) do get :edit get :history post :preview_markdown diff --git a/config/settings.rb b/config/settings.rb index 58f38d103ea..3f3481bb65d 100644 --- a/config/settings.rb +++ b/config/settings.rb @@ -85,17 +85,24 @@ class Settings < Settingslogic File.expand_path(path, Rails.root) end - # Returns a 256-bit key for attr_encrypted - def attr_encrypted_db_key_base - # Ruby 2.4+ requires passing in the exact required length for OpenSSL keys - # (https://github.com/ruby/ruby/commit/ce635262f53b760284d56bb1027baebaaec175d1). - # Previous versions quietly truncated the input. - # - # The default mode for the attr_encrypted gem is to use a 256-bit key. - # We truncate the 128-byte string to 32 bytes. + # Ruby 2.4+ requires passing in the exact required length for OpenSSL keys + # (https://github.com/ruby/ruby/commit/ce635262f53b760284d56bb1027baebaaec175d1). + # Previous versions quietly truncated the input. + # + # Use this when using :per_attribute_iv mode for attr_encrypted. + # We have to truncate the string to 32 bytes for a 256-bit cipher. + def attr_encrypted_db_key_base_truncated Gitlab::Application.secrets.db_key_base[0..31] end + # This should be used for :per_attribute_salt_and_iv mode. There is no + # need to truncate the key because the encryptor will use the salt to + # generate a hash of the password: + # https://github.com/attr-encrypted/encryptor/blob/c3a62c4a9e74686dd95e0548f9dc2a361fdc95d1/lib/encryptor.rb#L77 + def attr_encrypted_db_key_base + Gitlab::Application.secrets.db_key_base + end + private def base_url(config) diff --git a/config/unicorn.rb.example b/config/unicorn.rb.example index cc10da2bd88..220a0191160 100644 --- a/config/unicorn.rb.example +++ b/config/unicorn.rb.example @@ -81,6 +81,17 @@ GC.respond_to?(:copy_on_write_friendly=) and # fast LAN. check_client_connection false +before_exec do |server| + # The following is necessary to ensure stale Prometheus metrics don't + # accumulate over time. It needs to be done in this hook as opposed to + # inside an init script to ensure metrics files aren't deleted after new + # unicorn workers start after a SIGUSR2 is received. + if ENV['prometheus_multiproc_dir'] + old_metrics = Dir[File.join(ENV['prometheus_multiproc_dir'], '*.db')] + FileUtils.rm_rf(old_metrics) + end +end + before_fork do |server, worker| # the following is highly recommended for Rails + "preload_app true" # as there's no need for the master process to hold a connection diff --git a/config/webpack.config.js b/config/webpack.config.js index d6ab32972fb..b1e378f6c27 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -1,11 +1,10 @@ -const fs = require('fs'); const path = require('path'); const glob = require('glob'); const webpack = require('webpack'); const VueLoaderPlugin = require('vue-loader/lib/plugin'); const StatsWriterPlugin = require('webpack-stats-plugin').StatsWriterPlugin; -const CopyWebpackPlugin = require('copy-webpack-plugin'); const CompressionPlugin = require('compression-webpack-plugin'); +const MonacoWebpackPlugin = require('monaco-editor-webpack-plugin'); const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; const ROOT_PATH = path.resolve(__dirname, '..'); @@ -168,15 +167,7 @@ module.exports = { name: '[name].[hash:8].[ext]', }, }, - { - test: /monaco-editor\/\w+\/vs\/loader\.js$/, - use: [ - { loader: 'exports-loader', options: 'l.global' }, - { loader: 'imports-loader', options: 'l=>{},this=>l,AMDLoader=>this,module=>undefined' }, - ], - }, ], - noParse: [/monaco-editor\/\w+\/vs\//], }, optimization: { @@ -226,6 +217,9 @@ module.exports = { // enable vue-loader to use existing loader rules for other module types new VueLoaderPlugin(), + // automatically configure monaco editor web workers + new MonacoWebpackPlugin(), + // prevent pikaday from including moment.js new webpack.IgnorePlugin(/moment/, /pikaday/), @@ -235,29 +229,6 @@ module.exports = { jQuery: 'jquery', }), - // copy pre-compiled vendor libraries verbatim - new CopyWebpackPlugin([ - { - from: path.join( - ROOT_PATH, - `node_modules/monaco-editor/${IS_PRODUCTION ? 'min' : 'dev'}/vs` - ), - to: 'monaco-editor/vs', - transform: function(content, path) { - if (/\.js$/.test(path) && !/worker/i.test(path) && !/typescript/i.test(path)) { - return ( - '(function(){\n' + - 'var define = this.define, require = this.require;\n' + - 'window.define = define; window.require = require;\n' + - content + - '\n}.call(window.__monaco_context__ || (window.__monaco_context__ = {})));' - ); - } - return content; - }, - }, - ]), - // compression can require a lot of compute time and is disabled in CI IS_PRODUCTION && !NO_COMPRESSION && new CompressionPlugin(), diff --git a/db/fixtures/development/04_project.rb b/db/fixtures/development/04_project.rb index 213c8bca639..51e69879c79 100644 --- a/db/fixtures/development/04_project.rb +++ b/db/fixtures/development/04_project.rb @@ -67,6 +67,10 @@ Sidekiq::Testing.inline! do skip_disk_validation: true } + if i % 2 == 0 + params[:storage_version] = Project::LATEST_STORAGE_VERSION + end + project = Projects::CreateService.new(User.first, params).execute # Seed-Fu runs this entire fixture in a transaction, so the `after_commit` # hook won't run until after the fixture is loaded. That is too late diff --git a/db/fixtures/development/08_settings.rb b/db/fixtures/development/08_settings.rb new file mode 100644 index 00000000000..141465c06cf --- /dev/null +++ b/db/fixtures/development/08_settings.rb @@ -0,0 +1,7 @@ +# We want to enable hashed storage for every new project in development +# Details https://gitlab.com/gitlab-org/gitlab-ce/issues/46241 +Gitlab::Seeder.quiet do + ApplicationSetting.create_from_defaults unless ApplicationSetting.current_without_cache + ApplicationSetting.current_without_cache.update!(hashed_storage_enabled: true) + print '.' +end diff --git a/db/migrate/20160226114608_add_trigram_indexes_for_searching.rb b/db/migrate/20160226114608_add_trigram_indexes_for_searching.rb index 375e389e07a..7aa79bf5e02 100644 --- a/db/migrate/20160226114608_add_trigram_indexes_for_searching.rb +++ b/db/migrate/20160226114608_add_trigram_indexes_for_searching.rb @@ -37,7 +37,12 @@ class AddTrigramIndexesForSearching < ActiveRecord::Migration res = execute("SELECT true AS enabled FROM pg_available_extensions WHERE name = 'pg_trgm' AND installed_version IS NOT NULL;") row = res.first - row && row['enabled'] == 't' ? true : false + check = if Gitlab.rails5? + true + else + 't' + end + row && row['enabled'] == check ? true : false end def create_trigrams_extension diff --git a/db/migrate/20161124141322_migrate_process_commit_worker_jobs.rb b/db/migrate/20161124141322_migrate_process_commit_worker_jobs.rb index a96ea7d9db4..dc16d5c5169 100644 --- a/db/migrate/20161124141322_migrate_process_commit_worker_jobs.rb +++ b/db/migrate/20161124141322_migrate_process_commit_worker_jobs.rb @@ -12,7 +12,9 @@ class MigrateProcessCommitWorkerJobs < ActiveRecord::Migration end def repository_storage_path - Gitlab.config.repositories.storages[repository_storage].legacy_disk_path + Gitlab::GitalyClient::StorageSettings.allow_disk_access do + Gitlab.config.repositories.storages[repository_storage].legacy_disk_path + end end def repository_path diff --git a/db/migrate/20161207231620_fixup_environment_name_uniqueness.rb b/db/migrate/20161207231620_fixup_environment_name_uniqueness.rb index f73e4f6c99b..1db8c68626a 100644 --- a/db/migrate/20161207231620_fixup_environment_name_uniqueness.rb +++ b/db/migrate/20161207231620_fixup_environment_name_uniqueness.rb @@ -1,4 +1,5 @@ class FixupEnvironmentNameUniqueness < ActiveRecord::Migration + include Gitlab::Database::ArelMethods include Gitlab::Database::MigrationHelpers DOWNTIME = true @@ -41,7 +42,7 @@ class FixupEnvironmentNameUniqueness < ActiveRecord::Migration conflicts.each do |id, name| update_sql = - Arel::UpdateManager.new(ActiveRecord::Base) + arel_update_manager .table(environments) .set(environments[:name] => name + "-" + id.to_s) .where(environments[:id].eq(id)) diff --git a/db/migrate/20161207231626_add_environment_slug.rb b/db/migrate/20161207231626_add_environment_slug.rb index 83cdd484c4c..162f82a01cb 100644 --- a/db/migrate/20161207231626_add_environment_slug.rb +++ b/db/migrate/20161207231626_add_environment_slug.rb @@ -2,6 +2,7 @@ # for more information on how to write migrations for GitLab. class AddEnvironmentSlug < ActiveRecord::Migration + include Gitlab::Database::ArelMethods include Gitlab::Database::MigrationHelpers DOWNTIME = true @@ -19,7 +20,7 @@ class AddEnvironmentSlug < ActiveRecord::Migration finder = environments.project(:id, :name) connection.exec_query(finder.to_sql).rows.each do |id, name| - updater = Arel::UpdateManager.new(ActiveRecord::Base) + updater = arel_update_manager .table(environments) .set(environments[:slug] => generate_slug(name)) .where(environments[:id].eq(id)) diff --git a/db/migrate/20161226122833_remove_dot_git_from_usernames.rb b/db/migrate/20161226122833_remove_dot_git_from_usernames.rb index 8986cd8cb4b..133435523e1 100644 --- a/db/migrate/20161226122833_remove_dot_git_from_usernames.rb +++ b/db/migrate/20161226122833_remove_dot_git_from_usernames.rb @@ -64,7 +64,9 @@ class RemoveDotGitFromUsernames < ActiveRecord::Migration # we rename suffix instead of removing it path = path.sub(/\.git\z/, '_git') - check_routes(path.dup, 0, path) + Gitlab::GitalyClient::StorageSettings.allow_disk_access do + check_routes(path.dup, 0, path) + end end def check_routes(base, counter, path) diff --git a/db/migrate/20170622135728_add_unique_constraint_to_ci_variables.rb b/db/migrate/20170622135728_add_unique_constraint_to_ci_variables.rb index 8b2cc40ee59..787022b7bfe 100644 --- a/db/migrate/20170622135728_add_unique_constraint_to_ci_variables.rb +++ b/db/migrate/20170622135728_add_unique_constraint_to_ci_variables.rb @@ -2,12 +2,13 @@ class AddUniqueConstraintToCiVariables < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers DOWNTIME = false + INDEX_NAME = 'index_ci_variables_on_project_id_and_key_and_environment_scope' disable_ddl_transaction! def up unless this_index_exists? - add_concurrent_index(:ci_variables, columns, name: index_name, unique: true) + add_concurrent_index(:ci_variables, columns, name: INDEX_NAME, unique: true) end end @@ -18,21 +19,17 @@ class AddUniqueConstraintToCiVariables < ActiveRecord::Migration add_concurrent_index(:ci_variables, :project_id) end - remove_concurrent_index(:ci_variables, columns, name: index_name) + remove_concurrent_index(:ci_variables, columns, name: INDEX_NAME) end end private def this_index_exists? - index_exists?(:ci_variables, columns, name: index_name) + index_exists?(:ci_variables, columns, name: INDEX_NAME) end def columns @columns ||= [:project_id, :key, :environment_scope] end - - def index_name - 'index_ci_variables_on_project_id_and_key_and_environment_scope' - end end diff --git a/db/migrate/20170925184228_add_favicon_to_appearances.rb b/db/migrate/20170925184228_add_favicon_to_appearances.rb new file mode 100644 index 00000000000..65083733afb --- /dev/null +++ b/db/migrate/20170925184228_add_favicon_to_appearances.rb @@ -0,0 +1,7 @@ +class AddFaviconToAppearances < ActiveRecord::Migration + DOWNTIME = false + + def change + add_column :appearances, :favicon, :string + end +end diff --git a/db/migrate/20171106155656_turn_issues_due_date_index_to_partial_index.rb b/db/migrate/20171106155656_turn_issues_due_date_index_to_partial_index.rb index e4bed778695..08784de4043 100644 --- a/db/migrate/20171106155656_turn_issues_due_date_index_to_partial_index.rb +++ b/db/migrate/20171106155656_turn_issues_due_date_index_to_partial_index.rb @@ -20,9 +20,7 @@ class TurnIssuesDueDateIndexToPartialIndex < ActiveRecord::Migration name: NEW_INDEX_NAME ) - # We set the column name to nil as otherwise Rails will ignore the custom - # index name and remove the wrong index. - remove_concurrent_index(:issues, nil, name: OLD_INDEX_NAME) + remove_concurrent_index_by_name(:issues, OLD_INDEX_NAME) end def down @@ -32,6 +30,6 @@ class TurnIssuesDueDateIndexToPartialIndex < ActiveRecord::Migration name: OLD_INDEX_NAME ) - remove_concurrent_index(:issues, nil, name: NEW_INDEX_NAME) + remove_concurrent_index_by_name(:issues, NEW_INDEX_NAME) end end diff --git a/db/migrate/20180201110056_add_foreign_keys_to_todos.rb b/db/migrate/20180201110056_add_foreign_keys_to_todos.rb index b7c40f8c01a..020b0550321 100644 --- a/db/migrate/20180201110056_add_foreign_keys_to_todos.rb +++ b/db/migrate/20180201110056_add_foreign_keys_to_todos.rb @@ -31,7 +31,7 @@ class AddForeignKeysToTodos < ActiveRecord::Migration end def down - remove_foreign_key :todos, :users + remove_foreign_key :todos, column: :user_id remove_foreign_key :todos, column: :author_id remove_foreign_key :todos, :notes end diff --git a/db/migrate/20180424160449_add_pipeline_iid_to_ci_pipelines.rb b/db/migrate/20180424160449_add_pipeline_iid_to_ci_pipelines.rb new file mode 100644 index 00000000000..e8f0c91d612 --- /dev/null +++ b/db/migrate/20180424160449_add_pipeline_iid_to_ci_pipelines.rb @@ -0,0 +1,13 @@ +class AddPipelineIidToCiPipelines < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def up + add_column :ci_pipelines, :iid, :integer + end + + def down + remove_column :ci_pipelines, :iid, :integer + end +end diff --git a/db/migrate/20180425205249_add_index_constraints_to_pipeline_iid.rb b/db/migrate/20180425205249_add_index_constraints_to_pipeline_iid.rb new file mode 100644 index 00000000000..3fa59b44d5d --- /dev/null +++ b/db/migrate/20180425205249_add_index_constraints_to_pipeline_iid.rb @@ -0,0 +1,15 @@ +class AddIndexConstraintsToPipelineIid < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + add_concurrent_index :ci_pipelines, [:project_id, :iid], unique: true, where: 'iid IS NOT NULL' + end + + def down + remove_concurrent_index :ci_pipelines, [:project_id, :iid] + end +end diff --git a/db/migrate/20180523042841_rename_merge_requests_allow_maintainer_to_push.rb b/db/migrate/20180523042841_rename_merge_requests_allow_maintainer_to_push.rb new file mode 100644 index 00000000000..41bc7b71694 --- /dev/null +++ b/db/migrate/20180523042841_rename_merge_requests_allow_maintainer_to_push.rb @@ -0,0 +1,17 @@ +class RenameMergeRequestsAllowMaintainerToPush < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + # NOOP + end + + def down + if column_exists?(:merge_requests, :allow_collaboration) + cleanup_concurrent_column_rename :merge_requests, :allow_collaboration, :allow_maintainer_to_push + end + end +end diff --git a/db/migrate/20180530135500_add_index_to_stages_position.rb b/db/migrate/20180530135500_add_index_to_stages_position.rb new file mode 100644 index 00000000000..61150f33a25 --- /dev/null +++ b/db/migrate/20180530135500_add_index_to_stages_position.rb @@ -0,0 +1,15 @@ +class AddIndexToStagesPosition < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + add_concurrent_index :ci_stages, [:pipeline_id, :position] + end + + def down + remove_concurrent_index :ci_stages, [:pipeline_id, :position] + end +end diff --git a/db/migrate/20180531220618_change_default_value_for_dsa_key_restriction.rb b/db/migrate/20180531220618_change_default_value_for_dsa_key_restriction.rb new file mode 100644 index 00000000000..d0dcacc5b66 --- /dev/null +++ b/db/migrate/20180531220618_change_default_value_for_dsa_key_restriction.rb @@ -0,0 +1,16 @@ +class ChangeDefaultValueForDsaKeyRestriction < ActiveRecord::Migration + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + + def up + change_column :application_settings, :dsa_key_restriction, :integer, null: false, + default: -1 + + execute("UPDATE application_settings SET dsa_key_restriction = -1") + end + + def down + change_column :application_settings, :dsa_key_restriction, :integer, null: false, + default: 0 + end +end diff --git a/db/migrate/20180601213245_add_deploy_strategy_to_project_auto_devops.rb b/db/migrate/20180601213245_add_deploy_strategy_to_project_auto_devops.rb new file mode 100644 index 00000000000..6f50d428965 --- /dev/null +++ b/db/migrate/20180601213245_add_deploy_strategy_to_project_auto_devops.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 AddDeployStrategyToProjectAutoDevops < 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 :project_auto_devops, :deploy_strategy, :integer, default: 0, allow_null: false + end + + def down + remove_column :project_auto_devops, :deploy_strategy + end +end diff --git a/db/migrate/20180608110058_rename_merge_requests_allow_collaboration.rb b/db/migrate/20180608110058_rename_merge_requests_allow_collaboration.rb new file mode 100644 index 00000000000..dcbbef9bd4a --- /dev/null +++ b/db/migrate/20180608110058_rename_merge_requests_allow_collaboration.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 RenameMergeRequestsAllowCollaboration < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + + disable_ddl_transaction! + + def up + if column_exists?(:merge_requests, :allow_collaboration) + rename_column_concurrently :merge_requests, :allow_collaboration, :allow_maintainer_to_push + end + end + + def down + # NOOP + end +end diff --git a/db/post_migrate/20161109150329_fix_project_records_with_invalid_visibility.rb b/db/post_migrate/20161109150329_fix_project_records_with_invalid_visibility.rb index 69007b8e8ed..f058e85c1ec 100644 --- a/db/post_migrate/20161109150329_fix_project_records_with_invalid_visibility.rb +++ b/db/post_migrate/20161109150329_fix_project_records_with_invalid_visibility.rb @@ -1,4 +1,5 @@ class FixProjectRecordsWithInvalidVisibility < ActiveRecord::Migration + include Gitlab::Database::ArelMethods include Gitlab::Database::MigrationHelpers BATCH_SIZE = 500 @@ -33,7 +34,7 @@ class FixProjectRecordsWithInvalidVisibility < ActiveRecord::Migration end updates.each do |visibility_level, project_ids| - updater = Arel::UpdateManager.new(ActiveRecord::Base) + updater = arel_update_manager .table(projects) .set(projects[:visibility_level] => visibility_level) .where(projects[:id].in(project_ids)) diff --git a/db/post_migrate/20170324160416_migrate_user_activities_to_users_last_activity_on.rb b/db/post_migrate/20170324160416_migrate_user_activities_to_users_last_activity_on.rb index 78413a608f1..392fa00b1ba 100644 --- a/db/post_migrate/20170324160416_migrate_user_activities_to_users_last_activity_on.rb +++ b/db/post_migrate/20170324160416_migrate_user_activities_to_users_last_activity_on.rb @@ -1,5 +1,6 @@ # rubocop:disable Migration/UpdateLargeTable class MigrateUserActivitiesToUsersLastActivityOn < ActiveRecord::Migration + include Gitlab::Database::ArelMethods include Gitlab::Database::MigrationHelpers disable_ddl_transaction! @@ -39,7 +40,7 @@ class MigrateUserActivitiesToUsersLastActivityOn < ActiveRecord::Migration activities = activities(day.at_beginning_of_day, day.at_end_of_day, page: page) update_sql = - Arel::UpdateManager.new(ActiveRecord::Base) + arel_update_manager .table(users_table) .set(users_table[:last_activity_on] => day.to_date) .where(users_table[:username].in(activities.map(&:first))) diff --git a/db/post_migrate/20171124104327_migrate_kubernetes_service_to_new_clusters_architectures.rb b/db/post_migrate/20171124104327_migrate_kubernetes_service_to_new_clusters_architectures.rb index 1586a7eb92f..a957f107405 100644 --- a/db/post_migrate/20171124104327_migrate_kubernetes_service_to_new_clusters_architectures.rb +++ b/db/post_migrate/20171124104327_migrate_kubernetes_service_to_new_clusters_architectures.rb @@ -48,7 +48,7 @@ class MigrateKubernetesServiceToNewClustersArchitectures < ActiveRecord::Migrati attr_encrypted :token, mode: :per_attribute_iv, - key: Settings.attr_encrypted_db_key_base, + key: Settings.attr_encrypted_db_key_base_truncated, algorithm: 'aes-256-cbc' end diff --git a/db/post_migrate/20180523125103_cleanup_merge_requests_allow_maintainer_to_push_rename.rb b/db/post_migrate/20180523125103_cleanup_merge_requests_allow_maintainer_to_push_rename.rb new file mode 100644 index 00000000000..7301bcf2c6c --- /dev/null +++ b/db/post_migrate/20180523125103_cleanup_merge_requests_allow_maintainer_to_push_rename.rb @@ -0,0 +1,17 @@ +class CleanupMergeRequestsAllowMaintainerToPushRename < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + # NOOP + end + + def down + if column_exists?(:merge_requests, :allow_collaboration) + rename_column_concurrently :merge_requests, :allow_collaboration, :allow_maintainer_to_push + end + end +end diff --git a/db/post_migrate/20180529152628_schedule_to_archive_legacy_traces.rb b/db/post_migrate/20180529152628_schedule_to_archive_legacy_traces.rb new file mode 100644 index 00000000000..965cd3a8714 --- /dev/null +++ b/db/post_migrate/20180529152628_schedule_to_archive_legacy_traces.rb @@ -0,0 +1,35 @@ +class ScheduleToArchiveLegacyTraces < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + BATCH_SIZE = 5000 + BACKGROUND_MIGRATION_CLASS = 'ArchiveLegacyTraces' + + disable_ddl_transaction! + + class Build < ActiveRecord::Base + include EachBatch + self.table_name = 'ci_builds' + self.inheritance_column = :_type_disabled # Disable STI + + scope :type_build, -> { where(type: 'Ci::Build') } + + scope :finished, -> { where(status: [:success, :failed, :canceled]) } + + scope :without_archived_trace, -> do + where('NOT EXISTS (SELECT 1 FROM ci_job_artifacts WHERE ci_builds.id = ci_job_artifacts.job_id AND ci_job_artifacts.file_type = 3)') + end + end + + def up + queue_background_migration_jobs_by_range_at_intervals( + ::ScheduleToArchiveLegacyTraces::Build.type_build.finished.without_archived_trace, + BACKGROUND_MIGRATION_CLASS, + 5.minutes, + batch_size: BATCH_SIZE) + end + + def down + # noop + end +end diff --git a/db/post_migrate/20180603190921_migrate_object_storage_upload_sidekiq_queue.rb b/db/post_migrate/20180603190921_migrate_object_storage_upload_sidekiq_queue.rb new file mode 100644 index 00000000000..57bee6269b9 --- /dev/null +++ b/db/post_migrate/20180603190921_migrate_object_storage_upload_sidekiq_queue.rb @@ -0,0 +1,16 @@ +class MigrateObjectStorageUploadSidekiqQueue < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + sidekiq_queue_migrate 'object_storage_upload', to: 'object_storage:object_storage_background_move' + end + + def down + # do not migrate any jobs back because we would migrate also + # jobs which were not part of the 'object_storage_upload' + end +end diff --git a/db/post_migrate/20180608201435_cleanup_merge_requests_allow_collaboration_rename.rb b/db/post_migrate/20180608201435_cleanup_merge_requests_allow_collaboration_rename.rb new file mode 100644 index 00000000000..3f3edb8ea3d --- /dev/null +++ b/db/post_migrate/20180608201435_cleanup_merge_requests_allow_collaboration_rename.rb @@ -0,0 +1,20 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class CleanupMergeRequestsAllowCollaborationRename < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + if column_exists?(:merge_requests, :allow_collaboration) + cleanup_concurrent_column_rename :merge_requests, :allow_collaboration, :allow_maintainer_to_push + end + end + + def down + # NOOP + end +end diff --git a/db/schema.rb b/db/schema.rb index 97247387bc7..d05c6afbb9f 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: 20180529093006) do +ActiveRecord::Schema.define(version: 20180608201435) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -38,6 +38,7 @@ ActiveRecord::Schema.define(version: 20180529093006) do t.integer "cached_markdown_version" t.text "new_project_guidelines" t.text "new_project_guidelines_html" + t.string "favicon" end create_table "application_setting_terms", force: :cascade do |t| @@ -110,7 +111,7 @@ ActiveRecord::Schema.define(version: 20180529093006) do t.text "shared_runners_text_html" t.text "after_sign_up_text_html" t.integer "rsa_key_restriction", default: 0, null: false - t.integer "dsa_key_restriction", default: 0, null: false + t.integer "dsa_key_restriction", default: -1, null: false t.integer "ecdsa_key_restriction", default: 0, null: false t.integer "ed25519_key_restriction", default: 0, null: false t.boolean "housekeeping_enabled", default: true, null: false @@ -451,10 +452,12 @@ ActiveRecord::Schema.define(version: 20180529093006) do t.integer "config_source" t.boolean "protected" t.integer "failure_reason" + t.integer "iid" end add_index "ci_pipelines", ["auto_canceled_by_id"], name: "index_ci_pipelines_on_auto_canceled_by_id", using: :btree add_index "ci_pipelines", ["pipeline_schedule_id"], name: "index_ci_pipelines_on_pipeline_schedule_id", using: :btree + add_index "ci_pipelines", ["project_id", "iid"], name: "index_ci_pipelines_on_project_id_and_iid", unique: true, where: "(iid IS NOT NULL)", using: :btree add_index "ci_pipelines", ["project_id", "ref", "status", "id"], name: "index_ci_pipelines_on_project_id_and_ref_and_status_and_id", using: :btree add_index "ci_pipelines", ["project_id", "sha"], name: "index_ci_pipelines_on_project_id_and_sha", using: :btree add_index "ci_pipelines", ["project_id"], name: "index_ci_pipelines_on_project_id", using: :btree @@ -518,6 +521,7 @@ ActiveRecord::Schema.define(version: 20180529093006) do end add_index "ci_stages", ["pipeline_id", "name"], name: "index_ci_stages_on_pipeline_id_and_name", unique: true, using: :btree + add_index "ci_stages", ["pipeline_id", "position"], name: "index_ci_stages_on_pipeline_id_and_position", using: :btree add_index "ci_stages", ["pipeline_id"], name: "index_ci_stages_on_pipeline_id", using: :btree add_index "ci_stages", ["project_id"], name: "index_ci_stages_on_project_id", using: :btree @@ -1227,8 +1231,8 @@ ActiveRecord::Schema.define(version: 20180529093006) do t.boolean "discussion_locked" t.integer "latest_merge_request_diff_id" t.string "rebase_commit_sha" - t.boolean "allow_maintainer_to_push" t.boolean "squash", default: false, null: false + t.boolean "allow_maintainer_to_push" end add_index "merge_requests", ["assignee_id"], name: "index_merge_requests_on_assignee_id", using: :btree @@ -1490,6 +1494,7 @@ ActiveRecord::Schema.define(version: 20180529093006) do t.datetime_with_timezone "updated_at", null: false t.boolean "enabled" t.string "domain" + t.integer "deploy_strategy", default: 0, null: false end add_index "project_auto_devops", ["project_id"], name: "index_project_auto_devops_on_project_id", unique: true, using: :btree diff --git a/doc/README.md b/doc/README.md index ff8dd3fab8a..fee920f2012 100644 --- a/doc/README.md +++ b/doc/README.md @@ -162,7 +162,7 @@ configuration. Then customize everything from buildpacks to CI/CD. - [Auto DevOps](topics/autodevops/index.md) - [Deployment of Helm, Ingress, and Prometheus on Kubernetes](user/project/clusters/index.md#installing-applications) -- [Protected secret variables](ci/variables/README.md#protected-secret-variables) +- [Protected variables](ci/variables/README.md#protected-variables) - [Easy creation of Kubernetes clusters on GKE](user/project/clusters/index.md#adding-and-creating-a-new-gke-cluster-via-gitlab) ### Monitor @@ -191,7 +191,7 @@ instant how code changes impact your production environment. - [User account](user/profile/index.md): Manage your account - [Authentication](topics/authentication/index.md): Account security with two-factor authentication, setup your ssh keys and deploy keys for secure access to your projects. - [Profile settings](user/profile/index.md#profile-settings): Manage your profile settings, two factor authentication and more. -- [User permissions](user/permissions.md): Learn what each role in a project (external/guest/reporter/developer/master/owner) can do. +- [User permissions](user/permissions.md): Learn what each role in a project (external/guest/reporter/developer/maintainer/owner) can do. ### Git and GitLab @@ -215,7 +215,7 @@ 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. +- [Writing documentation](development/documentation/index.md): Contributing to GitLab Docs. ## GitLab subscriptions diff --git a/doc/administration/index.md b/doc/administration/index.md index df935095e61..0e65f9a9963 100644 --- a/doc/administration/index.md +++ b/doc/administration/index.md @@ -49,6 +49,7 @@ Learn how to install, configure, update, and maintain your GitLab instance. #### Customizing GitLab's appearance - [Header logo](../customization/branded_page_and_email_header.md): Change the logo on all pages and email headers. +- [Favicon](../customization/favicon.md): Change the default favicon to your own logo. - [Branded login page](../customization/branded_login_page.md): Customize the login page with your own logo, title, and description. - [Welcome message](../customization/welcome_message.md): Add a custom welcome message to the sign-in page. - ["New Project" page](../customization/new_project_page.md): Customize the text to be displayed on the page that opens whenever your users create a new project. diff --git a/doc/administration/integration/koding.md b/doc/administration/integration/koding.md index 67f9f01efb8..6c1ec3028cc 100644 --- a/doc/administration/integration/koding.md +++ b/doc/administration/integration/koding.md @@ -100,14 +100,14 @@ As it's pointed out before, you will need public access to this machine that you've installed Koding and GitLab on. Better to use a domain but a static IP is also fine. -For IP based installation you can use [xip.io](https://xip.io) service which is +For IP based installation you can use [nip.io](https://nip.io) service which is free and provides DNS resolution to IP based requests like following; - - 127.0.0.1.xip.io -> resolves to 127.0.0.1 - - foo.bar.baz.127.0.0.1.xip.io -> resolves to 127.0.0.1 + - 127.0.0.1.nip.io -> resolves to 127.0.0.1 + - foo.bar.baz.127.0.0.1.nip.io -> resolves to 127.0.0.1 - and so on... -As Koding needs subdomains for team names; `foo.127.0.0.1.xip.io` requests for +As Koding needs subdomains for team names; `foo.127.0.0.1.nip.io` requests for a running koding instance on `127.0.0.1` server will be handled as `foo` team requests. @@ -127,8 +127,8 @@ your Koding installation. Team called `gitlab` has integration on Koding out of the box, so if you didn't change anything your team on Koding should be `gitlab`. -So, if your Koding is running on `http://1.2.3.4.xip.io:8090` your URL needs -to be `http://gitlab.1.2.3.4.xip.io:8090`. You need to provide the same host +So, if your Koding is running on `http://1.2.3.4.nip.io:8090` your URL needs +to be `http://gitlab.1.2.3.4.nip.io:8090`. You need to provide the same host with your Koding installation here. @@ -192,7 +192,7 @@ cd koding docker-compose run \ --service-ports backend \ /opt/koding/scripts/bootstrap-container build \ - --host=**YOUR_IP**.xip.io \ + --host=**YOUR_IP**.nip.io \ --gitlabHost=**GITLAB_IP** \ --gitlabPort=**GITLAB_PORT** \ --gitlabToken=**SECRET_TOKEN** \ @@ -224,7 +224,7 @@ cd koding docker-compose run \ --service-ports backend \ /opt/koding/scripts/bootstrap-container build \ - --host=**YOUR_IP**.xip.io \ + --host=**YOUR_IP**.nip.io \ ``` #### Enable Single Sign On @@ -233,7 +233,7 @@ Once you restarted your Koding and logged in with your username and password you need to activate oauth authentication for your user. To do that - Navigate to Dashboard on Koding from; - `http://gitlab.**YOUR_IP**.xip.io:8090/Home/my-account` + `http://gitlab.**YOUR_IP**.nip.io:8090/Home/my-account` - Scroll down to Integrations section - Click on toggle to turn On integration in GitLab integration section diff --git a/doc/administration/integration/terminal.md b/doc/administration/integration/terminal.md index 32ad63c3706..e11ed58eb91 100644 --- a/doc/administration/integration/terminal.md +++ b/doc/administration/integration/terminal.md @@ -2,7 +2,7 @@ > [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7690) -in GitLab 8.15. Only project masters and owners can access web terminals. +in GitLab 8.15. Only project maintainers and owners can access web terminals. With the introduction of the [Kubernetes integration](../../user/project/clusters/index.md), GitLab gained the ability to store and use credentials for a Kubernetes cluster. diff --git a/doc/administration/job_artifacts.md b/doc/administration/job_artifacts.md index 77fe4d561a1..e59ab5a72e1 100644 --- a/doc/administration/job_artifacts.md +++ b/doc/administration/job_artifacts.md @@ -94,6 +94,7 @@ _The artifacts are stored by default in > Available in [GitLab Premium](https://about.gitlab.com/products/) and [GitLab.com Silver](https://about.gitlab.com/gitlab-com/). > Since version 10.6, available in [GitLab CE](https://about.gitlab.com/products/) +> Since version 11.0, we support direct_upload to S3. If you don't want to use the local disk where GitLab is installed to store the artifacts, you can use an object storage like AWS S3 instead. @@ -108,7 +109,7 @@ For source installations the following settings are nested under `artifacts:` an |---------|-------------|---------| | `enabled` | Enable/disable object storage | `false` | | `remote_directory` | The bucket name where Artifacts will be stored| | -| `direct_upload` | Set to true to enable direct upload of Artifacts without the need of local shared storage. Option may be removed once we decide to support only single storage for all files. Currently only `Google` provider is supported | `false` | +| `direct_upload` | Set to true to enable direct upload of Artifacts without the need of local shared storage. Option may be removed once we decide to support only single storage for all files. | `false` | | `background_upload` | Set to false to disable automatic upload. Option may be removed once upload is direct to S3 | `true` | | `proxy_download` | Set to true to enable proxying all files served. Option allows to reduce egress traffic as this allows clients to download directly from remote storage instead of proxying all data | `false` | | `connection` | Various connection options described below | | diff --git a/doc/administration/pages/index.md b/doc/administration/pages/index.md index c0221533f13..9b1297ca4ba 100644 --- a/doc/administration/pages/index.md +++ b/doc/administration/pages/index.md @@ -83,12 +83,12 @@ you need to add a [wildcard DNS A record][wiki-wildcard-dns] pointing to the host that GitLab runs. For example, an entry would look like this: ``` -*.example.io. 1800 IN A 1.1.1.1 +*.example.io. 1800 IN A 192.0.2.1 *.example.io. 1800 IN AAAA 2001::1 ``` where `example.io` is the domain under which GitLab Pages will be served -and `1.1.1.1` is the IPv4 address of your GitLab instance and `2001::1` is the +and `192.0.2.1` is the IPv4 address of your GitLab instance and `2001::1` is the IPv6 address. If you don't have IPv6, you can omit the AAAA record. > **Note:** @@ -193,13 +193,13 @@ world. Custom domains are supported, but no TLS. ```shell pages_external_url "http://example.io" - nginx['listen_addresses'] = ['1.1.1.1'] + nginx['listen_addresses'] = ['192.0.2.1'] pages_nginx['enable'] = false - gitlab_pages['external_http'] = ['1.1.1.2:80', '[2001::2]:80'] + gitlab_pages['external_http'] = ['192.0.2.2:80', '[2001::2]:80'] ``` - where `1.1.1.1` is the primary IP address that GitLab is listening to and - `1.1.1.2` and `2001::2` are the secondary IPs the GitLab Pages daemon + where `192.0.2.1` is the primary IP address that GitLab is listening to and + `192.0.2.2` and `2001::2` are the secondary IPs the GitLab Pages daemon listens on. If you don't have IPv6, you can omit the IPv6 address. 1. [Reconfigure GitLab][reconfigure] @@ -228,16 +228,16 @@ world. Custom domains and TLS are supported. ```shell pages_external_url "https://example.io" - nginx['listen_addresses'] = ['1.1.1.1'] + nginx['listen_addresses'] = ['192.0.2.1'] pages_nginx['enable'] = false gitlab_pages['cert'] = "/etc/gitlab/ssl/example.io.crt" gitlab_pages['cert_key'] = "/etc/gitlab/ssl/example.io.key" - gitlab_pages['external_http'] = ['1.1.1.2:80', '[2001::2]:80'] - gitlab_pages['external_https'] = ['1.1.1.2:443', '[2001::2]:443'] + gitlab_pages['external_http'] = ['192.0.2.2:80', '[2001::2]:80'] + gitlab_pages['external_https'] = ['192.0.2.2:443', '[2001::2]:443'] ``` - where `1.1.1.1` is the primary IP address that GitLab is listening to and - `1.1.1.2` and `2001::2` are the secondary IPs where the GitLab Pages daemon + where `192.0.2.1` is the primary IP address that GitLab is listening to and + `192.0.2.2` and `2001::2` are the secondary IPs where the GitLab Pages daemon listens on. If you don't have IPv6, you can omit the IPv6 address. 1. [Reconfigure GitLab][reconfigure] diff --git a/doc/administration/pages/source.md b/doc/administration/pages/source.md index a45c3306457..4e40a7cb18d 100644 --- a/doc/administration/pages/source.md +++ b/doc/administration/pages/source.md @@ -67,11 +67,11 @@ you need to add a [wildcard DNS A record][wiki-wildcard-dns] pointing to the host that GitLab runs. For example, an entry would look like this: ``` -*.example.io. 1800 IN A 1.1.1.1 +*.example.io. 1800 IN A 192.0.2.1 ``` where `example.io` is the domain under which GitLab Pages will be served -and `1.1.1.1` is the IP address of your GitLab instance. +and `192.0.2.1` is the IP address of your GitLab instance. > **Note:** You should not use the GitLab domain to serve user pages. For more information @@ -253,7 +253,7 @@ world. Custom domains are supported, but no TLS. port: 80 https: false - external_http: 1.1.1.2:80 + external_http: 192.0.2.2:80 ``` 1. Edit `/etc/default/gitlab` and set `gitlab_pages_enabled` to `true` in @@ -263,7 +263,7 @@ world. Custom domains are supported, but no TLS. ``` gitlab_pages_enabled=true - gitlab_pages_options="-pages-domain example.io -pages-root $app_root/shared/pages -listen-proxy 127.0.0.1:8090 -listen-http 1.1.1.2:80" + gitlab_pages_options="-pages-domain example.io -pages-root $app_root/shared/pages -listen-proxy 127.0.0.1:8090 -listen-http 192.0.2.2:80" ``` 1. Copy the `gitlab-pages-ssl` Nginx configuration file: @@ -274,7 +274,7 @@ world. Custom domains are supported, but no TLS. ``` 1. Edit all GitLab related configs in `/etc/nginx/site-available/` and replace - `0.0.0.0` with `1.1.1.1`, where `1.1.1.1` the primary IP where GitLab + `0.0.0.0` with `192.0.2.1`, where `192.0.2.1` the primary IP where GitLab listens to. 1. Restart NGINX 1. [Restart GitLab][restart] @@ -320,8 +320,8 @@ world. Custom domains and TLS are supported. port: 443 https: true - external_http: 1.1.1.2:80 - external_https: 1.1.1.2:443 + external_http: 192.0.2.2:80 + external_https: 192.0.2.2:443 ``` 1. Edit `/etc/default/gitlab` and set `gitlab_pages_enabled` to `true` in @@ -333,7 +333,7 @@ world. Custom domains and TLS are supported. ``` gitlab_pages_enabled=true - gitlab_pages_options="-pages-domain example.io -pages-root $app_root/shared/pages -listen-proxy 127.0.0.1:8090 -listen-http 1.1.1.2:80 -listen-https 1.1.1.2:443 -root-cert /path/to/example.io.crt -root-key /path/to/example.io.key + gitlab_pages_options="-pages-domain example.io -pages-root $app_root/shared/pages -listen-proxy 127.0.0.1:8090 -listen-http 192.0.2.2:80 -listen-https 192.0.2.2:443 -root-cert /path/to/example.io.crt -root-key /path/to/example.io.key ``` 1. Copy the `gitlab-pages-ssl` Nginx configuration file: @@ -344,7 +344,7 @@ world. Custom domains and TLS are supported. ``` 1. Edit all GitLab related configs in `/etc/nginx/site-available/` and replace - `0.0.0.0` with `1.1.1.1`, where `1.1.1.1` the primary IP where GitLab + `0.0.0.0` with `192.0.2.1`, where `192.0.2.1` the primary IP where GitLab listens to. 1. Restart NGINX 1. [Restart GitLab][restart] diff --git a/doc/administration/raketasks/check.md b/doc/administration/raketasks/check.md index 7d34d35e7d1..2649bf61d74 100644 --- a/doc/administration/raketasks/check.md +++ b/doc/administration/raketasks/check.md @@ -78,9 +78,10 @@ Example output: ## Uploaded Files Integrity -Various types of file can be uploaded to a GitLab installation by users. -Checksums are generated and stored in the database upon upload, and integrity -checks using those checksums can be run. These checks also detect missing files. +Various types of files can be uploaded to a GitLab installation by users. +These integrity checks can detect missing files. Additionally, for locally +stored files, checksums are generated and stored in the database upon upload, +and these checks will verify them against current files. Currently, integrity checks are supported for the following types of file: diff --git a/doc/administration/raketasks/storage.md b/doc/administration/raketasks/storage.md index cfd601b8866..7ad38abe4f5 100644 --- a/doc/administration/raketasks/storage.md +++ b/doc/administration/raketasks/storage.md @@ -17,13 +17,21 @@ This task will schedule all your existing projects and attachments associated wi **Omnibus Installation** ```bash -gitlab-rake gitlab:storage:migrate_to_hashed +sudo gitlab-rake gitlab:storage:migrate_to_hashed ``` **Source Installation** ```bash -rake gitlab:storage:migrate_to_hashed +sudo -u git -H bundle exec rake gitlab:storage:migrate_to_hashed RAILS_ENV=production +``` + +They both also accept a range as environment variable: + +```bash +# to migrate any non migrated project from ID 20 to 50. +export ID_FROM=20 +export ID_TO=50 ``` You can monitor the progress in the _Admin > Monitoring > Background jobs_ screen. @@ -44,13 +52,13 @@ To have a simple summary of projects using **Legacy** storage: **Omnibus Installation** ```bash -gitlab-rake gitlab:storage:legacy_projects +sudo gitlab-rake gitlab:storage:legacy_projects ``` **Source Installation** ```bash -rake gitlab:storage:legacy_projects +sudo -u git -H bundle exec rake gitlab:storage:legacy_projects RAILS_ENV=production ``` ------ @@ -60,13 +68,13 @@ To list projects using **Legacy** storage: **Omnibus Installation** ```bash -gitlab-rake gitlab:storage:list_legacy_projects +sudo gitlab-rake gitlab:storage:list_legacy_projects ``` **Source Installation** ```bash -rake gitlab:storage:list_legacy_projects +sudo -u git -H bundle exec rake gitlab:storage:list_legacy_projects RAILS_ENV=production ``` @@ -77,13 +85,13 @@ To have a simple summary of projects using **Hashed** storage: **Omnibus Installation** ```bash -gitlab-rake gitlab:storage:hashed_projects +sudo gitlab-rake gitlab:storage:hashed_projects ``` **Source Installation** ```bash -rake gitlab:storage:hashed_projects +sudo -u git -H bundle exec rake gitlab:storage:hashed_projects RAILS_ENV=production ``` ------ @@ -93,14 +101,13 @@ To list projects using **Hashed** storage: **Omnibus Installation** ```bash -gitlab-rake gitlab:storage:list_hashed_projects +sudo gitlab-rake gitlab:storage:list_hashed_projects ``` **Source Installation** ```bash -rake gitlab:storage:list_hashed_projects - +sudo -u git -H bundle exec rake gitlab:storage:list_hashed_projects RAILS_ENV=production ``` ## List attachments on Legacy storage @@ -110,13 +117,13 @@ To have a simple summary of project attachments using **Legacy** storage: **Omnibus Installation** ```bash -gitlab-rake gitlab:storage:legacy_attachments +sudo gitlab-rake gitlab:storage:legacy_attachments ``` **Source Installation** ```bash -rake gitlab:storage:legacy_attachments +sudo -u git -H bundle exec rake gitlab:storage:legacy_attachments RAILS_ENV=production ``` ------ @@ -126,13 +133,13 @@ To list project attachments using **Legacy** storage: **Omnibus Installation** ```bash -gitlab-rake gitlab:storage:list_legacy_attachments +sudo gitlab-rake gitlab:storage:list_legacy_attachments ``` **Source Installation** ```bash -rake gitlab:storage:list_legacy_attachments +sudo -u git -H bundle exec rake gitlab:storage:list_legacy_attachments RAILS_ENV=production ``` ## List attachments on Hashed storage @@ -142,13 +149,13 @@ To have a simple summary of project attachments using **Hashed** storage: **Omnibus Installation** ```bash -gitlab-rake gitlab:storage:hashed_attachments +sudo gitlab-rake gitlab:storage:hashed_attachments ``` **Source Installation** ```bash -rake gitlab:storage:hashed_attachments +sudo -u git -H bundle exec rake gitlab:storage:hashed_attachments RAILS_ENV=production ``` ------ @@ -158,13 +165,13 @@ To list project attachments using **Hashed** storage: **Omnibus Installation** ```bash -gitlab-rake gitlab:storage:list_hashed_attachments +sudo gitlab-rake gitlab:storage:list_hashed_attachments ``` **Source Installation** ```bash -rake gitlab:storage:list_hashed_attachments +sudo -u git -H bundle exec rake gitlab:storage:list_hashed_attachments RAILS_ENV=production ``` [storage-types]: ../repository_storage_types.md diff --git a/doc/api/README.md b/doc/api/README.md index 1c756dc855f..6267618d3bc 100644 --- a/doc/api/README.md +++ b/doc/api/README.md @@ -104,7 +104,7 @@ with a major point release of GitLab itself. All deprecations and changes between two versions should be listed in the documentation. For the changes between v3 and v4; please read the [v3 to v4 documentation](v3_to_v4.md) -#### Current status +### Current status Currently only API version v4 is available. Version v3 was removed in [GitLab 11.0](https://gitlab.com/gitlab-org/gitlab-ce/issues/36819). diff --git a/doc/api/access_requests.md b/doc/api/access_requests.md index 603fa4a8194..4b2014ca843 100644 --- a/doc/api/access_requests.md +++ b/doc/api/access_requests.md @@ -10,7 +10,7 @@ 10 => Guest access 20 => Reporter access 30 => Developer access -40 => Master access +40 => Maintainer access 50 => Owner access # Only valid for groups ``` diff --git a/doc/api/avatar.md b/doc/api/avatar.md new file mode 100644 index 00000000000..7faed893066 --- /dev/null +++ b/doc/api/avatar.md @@ -0,0 +1,33 @@ +# Avatar API + +> [Introduced][ce-19121] in GitLab 11.0 + +## Get a single avatar URL + +Get a single avatar URL for a given email addres. If user with matching public +email address is not found, results from external avatar services are returned. +This endpoint can be accessed without authentication. In case public visibility +is restricted, response will be `403 Forbidden` when unauthenticated. + +``` +GET /avatar?email=admin@example.com +``` + +| Attribute | Type | Required | Description | +| --------- | ------- | -------- | --------------------- | +| `email` | string | yes | Public email address of the user | +| `size` | integer | no | Single pixel dimension (since images are squares). Only used for avatar lookups at `Gravatar` or at the configured `Libravatar` server | + +```bash +curl https://gitlab.example.com/api/v4/avatar?email=admin@example.com +``` + +Example response: + +```json +{ + "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon" +} +``` + +[ce-19121]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/19121 diff --git a/doc/api/commits.md b/doc/api/commits.md index d1584cf64de..d07b9d5614a 100644 --- a/doc/api/commits.md +++ b/doc/api/commits.md @@ -16,6 +16,7 @@ GET /projects/:id/repository/commits | `until` | string | no | Only commits before or on this date will be returned in ISO 8601 format YYYY-MM-DDTHH:MM:SSZ | | `path` | string | no | The file path | | `all` | boolean | no | Retrieve every commit from the repository | +| `with_stats` | boolean | no | Stats about each commit will be added to the response | ```bash diff --git a/doc/api/graphql/index.md b/doc/api/graphql/index.md new file mode 100644 index 00000000000..59e27922f64 --- /dev/null +++ b/doc/api/graphql/index.md @@ -0,0 +1,40 @@ +# GraphQL API (Beta) + +> [Introduced][ce-19008] in GitLab 11.0. + +[GraphQL](https://graphql.org/) is a query language for APIs that +allows clients to request exactly the data they need, making it +possible to get all required data in a limited number of requests. + +The GraphQL data (fields) can be described in the form of types, +allowing clients to use [clientside GraphQL +libraries](https://graphql.org/code/#graphql-clients) to consume the +API and avoid manual parsing. + +Since there's no fixed endpoints and datamodel, new abilities can be +added to the API without creating breaking changes. This allows us to +have a versionless API as described in [the GraphQL +documentation](https://graphql.org/learn/best-practices/#versioning). + +## Enabling the GraphQL feature + +The GraphQL API itself is currently in Alpha, and therefore hidden behind a +feature flag. You can enable the feature using the [features api][features-api] on a self-hosted instance. + +For example: + +```shell +curl --data "value=100" --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/features/graphql +``` + +## Available queries + +A first iteration of a GraphQL API includes a query for: `project`. Within a project it is also possible to fetch a `mergeRequest` by IID. + +## GraphiQL + +The API can be explored by using the GraphiQL IDE, it is available on your +instance on `gitlab.example.com/-/graphql-explorer`. + +[ce-19008]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/19008 +[features-api]: ../features.md diff --git a/doc/api/members.md b/doc/api/members.md index 3234f833eae..1a10aa75ac0 100644 --- a/doc/api/members.md +++ b/doc/api/members.md @@ -8,7 +8,7 @@ The access levels are defined in the `Gitlab::Access` module. Currently, these l 10 => Guest access 20 => Reporter access 30 => Developer access -40 => Master access +40 => Maintainer access 50 => Owner access # Only valid for groups ``` diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md index 051d2a10bc6..da74045b702 100644 --- a/doc/api/merge_requests.md +++ b/doc/api/merge_requests.md @@ -70,18 +70,18 @@ Parameters: "author": { "id": 1, "username": "admin", - "email": "admin@example.com", "name": "Administrator", "state": "active", - "created_at": "2012-04-29T08:46:00Z" + "avatar_url": null, + "web_url" : "https://gitlab.example.com/admin" }, "assignee": { "id": 1, "username": "admin", - "email": "admin@example.com", "name": "Administrator", "state": "active", - "created_at": "2012-04-29T08:46:00Z" + "avatar_url": null, + "web_url" : "https://gitlab.example.com/admin" }, "source_project_id": 2, "target_project_id": 3, @@ -190,18 +190,18 @@ Parameters: "author": { "id": 1, "username": "admin", - "email": "admin@example.com", "name": "Administrator", "state": "active", - "created_at": "2012-04-29T08:46:00Z" + "avatar_url": null, + "web_url" : "https://gitlab.example.com/admin" }, "assignee": { "id": 1, "username": "admin", - "email": "admin@example.com", "name": "Administrator", "state": "active", - "created_at": "2012-04-29T08:46:00Z" + "avatar_url": null, + "web_url" : "https://gitlab.example.com/admin" }, "source_project_id": 2, "target_project_id": 3, @@ -297,18 +297,18 @@ Parameters: "author": { "id": 1, "username": "admin", - "email": "admin@example.com", "name": "Administrator", "state": "active", - "created_at": "2012-04-29T08:46:00Z" + "avatar_url": null, + "web_url" : "https://gitlab.example.com/admin" }, "assignee": { "id": 1, "username": "admin", - "email": "admin@example.com", "name": "Administrator", "state": "active", - "created_at": "2012-04-29T08:46:00Z" + "avatar_url": null, + "web_url" : "https://gitlab.example.com/admin" }, "source_project_id": 2, "target_project_id": 3, @@ -548,14 +548,16 @@ Parameters: "username": "jarrett", "id": 5, "state": "active", - "avatar_url": "http://www.gravatar.com/avatar/b95567800f828948baf5f4160ebb2473?s=40&d=identicon" + "avatar_url": "http://www.gravatar.com/avatar/b95567800f828948baf5f4160ebb2473?s=40&d=identicon", + "web_url" : "https://gitlab.example.com/jarrett" }, "assignee": { "name": "Administrator", "username": "root", "id": 1, "state": "active", - "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40&d=identicon" + "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40&d=identicon", + "web_url" : "https://gitlab.example.com/root" }, "source_project_id": 4, "target_project_id": 4, @@ -582,6 +584,7 @@ Parameters: "changes_count": "1", "should_remove_source_branch": true, "force_remove_source_branch": false, + "squash": false, "web_url": "http://example.com/example/example/merge_requests/1", "discussion_locked": false, "time_stats": { @@ -650,7 +653,8 @@ POST /projects/:id/merge_requests | `labels` | string | no | Labels for MR as a comma-separated list | | `milestone_id` | integer | no | The global ID of a milestone | | `remove_source_branch` | boolean | no | Flag indicating if a merge request should remove the source branch when merging | -| `allow_maintainer_to_push` | boolean | no | Whether or not a maintainer of the target project can push to the source branch | +| `allow_collaboration` | boolean | no | Allow commits from members who can merge to the target branch | +| `allow_maintainer_to_push` | boolean | no | Deprecated, see allow_collaboration | | `squash` | boolean | no | Squash commits into a single commit when merging | ```json @@ -667,18 +671,18 @@ POST /projects/:id/merge_requests "author": { "id": 1, "username": "admin", - "email": "admin@example.com", "name": "Administrator", "state": "active", - "created_at": "2012-04-29T08:46:00Z" + "avatar_url": null, + "web_url" : "https://gitlab.example.com/admin" }, "assignee": { "id": 1, "username": "admin", - "email": "admin@example.com", "name": "Administrator", "state": "active", - "created_at": "2012-04-29T08:46:00Z" + "avatar_url": null, + "web_url" : "https://gitlab.example.com/admin" }, "source_project_id": 3, "target_project_id": 4, @@ -708,6 +712,7 @@ POST /projects/:id/merge_requests "squash": false, "web_url": "http://example.com/example/example/merge_requests/1", "discussion_locked": false, + "allow_collaboration": false, "allow_maintainer_to_push": false, "time_stats": { "time_estimate": 0, @@ -740,7 +745,8 @@ PUT /projects/:id/merge_requests/:merge_request_iid | `remove_source_branch` | boolean | no | Flag indicating if a merge request should remove the source branch when merging | | `squash` | boolean | no | Squash commits into a single commit when merging | | `discussion_locked` | boolean | no | Flag indicating if the merge request's discussion is locked. If the discussion is locked only project members can add, edit or resolve comments. | -| `allow_maintainer_to_push` | boolean | no | Whether or not a maintainer of the target project can push to the source branch | +| `allow_collaboration` | boolean | no | Allow commits from members who can merge to the target branch | +| `allow_maintainer_to_push` | boolean | no | Deprecated, see allow_collaboration | Must include at least one non-required attribute from above. @@ -757,18 +763,18 @@ Must include at least one non-required attribute from above. "author": { "id": 1, "username": "admin", - "email": "admin@example.com", "name": "Administrator", "state": "active", - "created_at": "2012-04-29T08:46:00Z" + "avatar_url": null, + "web_url" : "https://gitlab.example.com/admin" }, "assignee": { "id": 1, "username": "admin", - "email": "admin@example.com", "name": "Administrator", "state": "active", - "created_at": "2012-04-29T08:46:00Z" + "avatar_url": null, + "web_url" : "https://gitlab.example.com/admin" }, "source_project_id": 3, "target_project_id": 4, @@ -798,6 +804,7 @@ Must include at least one non-required attribute from above. "squash": false, "web_url": "http://example.com/example/example/merge_requests/1", "discussion_locked": false, + "allow_collaboration": false, "allow_maintainer_to_push": false, "time_stats": { "time_estimate": 0, @@ -865,18 +872,18 @@ Parameters: "author": { "id": 1, "username": "admin", - "email": "admin@example.com", "name": "Administrator", "state": "active", - "created_at": "2012-04-29T08:46:00Z" + "avatar_url": null, + "web_url" : "https://gitlab.example.com/admin" }, "assignee": { "id": 1, "username": "admin", - "email": "admin@example.com", "name": "Administrator", "state": "active", - "created_at": "2012-04-29T08:46:00Z" + "avatar_url": null, + "web_url" : "https://gitlab.example.com/admin" }, "source_project_id": 4, "target_project_id": 4, @@ -944,18 +951,18 @@ Parameters: "author": { "id": 1, "username": "admin", - "email": "admin@example.com", "name": "Administrator", "state": "active", - "created_at": "2012-04-29T08:46:00Z" + "avatar_url": null, + "web_url" : "https://gitlab.example.com/admin" }, "assignee": { "id": 1, "username": "admin", - "email": "admin@example.com", "name": "Administrator", "state": "active", - "created_at": "2012-04-29T08:46:00Z" + "avatar_url": null, + "web_url" : "https://gitlab.example.com/admin" }, "source_project_id": 4, "target_project_id": 4, diff --git a/doc/api/namespaces.md b/doc/api/namespaces.md index 1f0dc700640..656bf07bb55 100644 --- a/doc/api/namespaces.md +++ b/doc/api/namespaces.md @@ -54,7 +54,7 @@ Example response: ] ``` -**Note**: `members_count_with_descendants` are presented only for group masters/owners. +**Note**: `members_count_with_descendants` are presented only for group maintainers/owners. ## Search for namespace diff --git a/doc/api/pipelines.md b/doc/api/pipelines.md index 899f5da6647..ebae68fe389 100644 --- a/doc/api/pipelines.md +++ b/doc/api/pipelines.md @@ -102,6 +102,7 @@ POST /projects/:id/pipeline |------------|---------|----------|---------------------| | `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | | `ref` | string | yes | Reference to commit | +| `variables` | array | no | An array containing the variables available in the pipeline, matching the structure [{ 'key' => 'UPLOAD_TO_S3', 'value' => 'true' }] | ``` curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/pipeline?ref=master" diff --git a/doc/api/protected_branches.md b/doc/api/protected_branches.md index 950ead52560..f6813f27dc0 100644 --- a/doc/api/protected_branches.md +++ b/doc/api/protected_branches.md @@ -8,7 +8,7 @@ The access levels are defined in the `ProtectedRefAccess::ALLOWED_ACCESS_LEVELS` ``` 0 => No access 30 => Developer access -40 => Master access +40 => Maintainer access ``` ## List protected branches @@ -36,13 +36,13 @@ Example response: "push_access_levels": [ { "access_level": 40, - "access_level_description": "Masters" + "access_level_description": "Maintainers" } ], "merge_access_levels": [ { "access_level": 40, - "access_level_description": "Masters" + "access_level_description": "Maintainers" } ] }, @@ -75,13 +75,13 @@ Example response: "push_access_levels": [ { "access_level": 40, - "access_level_description": "Masters" + "access_level_description": "Maintainers" } ], "merge_access_levels": [ { "access_level": 40, - "access_level_description": "Masters" + "access_level_description": "Maintainers" } ] } @@ -104,8 +104,8 @@ curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" 'https://gitl | --------- | ---- | -------- | ----------- | | `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | | `name` | string | yes | The name of the branch or wildcard | -| `push_access_level` | string | no | Access levels allowed to push (defaults: `40`, master access level) | -| `merge_access_level` | string | no | Access levels allowed to merge (defaults: `40`, master access level) | +| `push_access_level` | string | no | Access levels allowed to push (defaults: `40`, maintainer access level) | +| `merge_access_level` | string | no | Access levels allowed to merge (defaults: `40`, maintainer access level) | Example response: @@ -115,13 +115,13 @@ Example response: "push_access_levels": [ { "access_level": 30, - "access_level_description": "Developers + Masters" + "access_level_description": "Developers + Maintainers" } ], "merge_access_levels": [ { "access_level": 30, - "access_level_description": "Developers + Masters" + "access_level_description": "Developers + Maintainers" } ] } diff --git a/doc/api/services.md b/doc/api/services.md index f23303ef836..aeb48ccc36c 100644 --- a/doc/api/services.md +++ b/doc/api/services.md @@ -1,6 +1,6 @@ # Services API ->**Note:** This API requires an access token with Master or Owner permissions +>**Note:** This API requires an access token with Maintainer or Owner permissions ## Asana diff --git a/doc/api/settings.md b/doc/api/settings.md index 36a0782d8f2..e6b207d8746 100644 --- a/doc/api/settings.md +++ b/doc/api/settings.md @@ -81,7 +81,7 @@ PUT /application/settings | `clientside_sentry_enabled` | boolean | no | Enable Sentry error reporting for the client side | | `container_registry_token_expire_delay` | integer | no | Container Registry token duration in minutes | | `default_artifacts_expire_in` | string | no | Set the default expiration time for each job's artifacts | -| `default_branch_protection` | integer | no | Determine if developers can push to master. Can take `0` _(not protected, both developers and masters can push new commits, force push, or delete the branch)_, `1` _(partially protected, developers and masters can push new commits, but cannot force push or delete the branch)_ or `2` _(fully protected, developers cannot push new commits, but masters can; no-one can force push or delete the branch)_ as a parameter. Default is `2`. | +| `default_branch_protection` | integer | no | Determine if developers can push to master. Can take `0` _(not protected, both developers and maintainers can push new commits, force push, or delete the branch)_, `1` _(partially protected, developers and maintainers can push new commits, but cannot force push or delete the branch)_ or `2` _(fully protected, developers cannot push new commits, but maintainers can; no-one can force push or delete the branch)_ as a parameter. Default is `2`. | | `default_group_visibility` | string | no | What visibility level new groups receive. Can take `private`, `internal` and `public` as a parameter. Default is `private`. | | `default_project_visibility` | string | no | What visibility level new projects receive. Can take `private`, `internal` and `public` as a parameter. Default is `private`. | | `default_projects_limit` | integer | no | Project limit per user. Default is `100000` | diff --git a/doc/api/snippets.md b/doc/api/snippets.md index 42b760c107d..7892866cd8e 100644 --- a/doc/api/snippets.md +++ b/doc/api/snippets.md @@ -49,6 +49,7 @@ Example response: "title": "test", "file_name": "add.rb", "description": "Ruby test snippet", + "visibility": "private", "author": { "id": 1, "username": "john_smith", @@ -99,6 +100,7 @@ Example response: "title": "This is a snippet", "file_name": "test.txt", "description": "Hello World snippet", + "visibility": "internal", "author": { "id": 1, "username": "john_smith", @@ -150,6 +152,7 @@ Example response: "title": "test", "file_name": "add.rb", "description": "description of snippet", + "visibility": "internal", "author": { "id": 1, "username": "john_smith", @@ -238,7 +241,8 @@ Example response: "raw_url": "http://localhost:3000/snippets/48/raw", "title": "Minus similique nesciunt vel fugiat qui ullam sunt.", "updated_at": "2016-11-25T16:53:34.479Z", - "web_url": "http://localhost:3000/snippets/48" + "web_url": "http://localhost:3000/snippets/48", + "visibility": "public" } ] ``` diff --git a/doc/articles/index.md b/doc/articles/index.md index 9f85533ea94..87ee17bb6de 100644 --- a/doc/articles/index.md +++ b/doc/articles/index.md @@ -4,7 +4,7 @@ comments: false # Technical articles list (deprecated) -[Technical articles](../development/writing_documentation.md#technical-articles) are +[Technical articles](../development/documentation/index.md#technical-articles) are topic-related documentation, written with an user-friendly approach and language, aiming to provide the community with guidance on specific processes to achieve certain objectives. diff --git a/doc/ci/docker/using_docker_images.md b/doc/ci/docker/using_docker_images.md index 7c0f837ea9c..71f1d69cdf4 100644 --- a/doc/ci/docker/using_docker_images.md +++ b/doc/ci/docker/using_docker_images.md @@ -496,7 +496,7 @@ To configure access for `registry.example.com`, follow these steps: bXlfdXNlcm5hbWU6bXlfcGFzc3dvcmQ= ``` -1. Create a [secret variable] `DOCKER_AUTH_CONFIG` with the content of the +1. Create a [variable] `DOCKER_AUTH_CONFIG` with the content of the Docker configuration file as the value: ```json @@ -632,7 +632,7 @@ creation. [postgres-hub]: https://hub.docker.com/r/_/postgres/ [mysql-hub]: https://hub.docker.com/r/_/mysql/ [runner-priv-reg]: http://docs.gitlab.com/runner/configuration/advanced-configuration.html#using-a-private-container-registry -[secret variable]: ../variables/README.md#secret-variables +[variable]: ../variables/README.md#variables [entrypoint]: https://docs.docker.com/engine/reference/builder/#entrypoint [cmd]: https://docs.docker.com/engine/reference/builder/#cmd [register]: https://docs.gitlab.com/runner/register/ diff --git a/doc/ci/environments.md b/doc/ci/environments.md index 7f034409580..8ea2e0a81dc 100644 --- a/doc/ci/environments.md +++ b/doc/ci/environments.md @@ -114,7 +114,7 @@ Let's now see how that information is exposed within GitLab. ## Viewing the current status of an environment -The environment list under your project's **Pipelines ➔ Environments**, is +The environment list under your project's **Operations > Environments**, is where you can find information of the last deployment status of an environment. Here's how the Environments page looks so far. @@ -167,7 +167,7 @@ that works. You can't control everything, so sometimes things go wrong. When that unfortunate time comes GitLab has you covered. Simply by clicking the **Rollback** button that can be found in the deployments page -(**Pipelines ➔ Environments ➔ `environment name`**) you can relaunch the +(**Operations > Environments > `environment name`**) you can relaunch the job with the commit associated with it. >**Note:** @@ -249,7 +249,7 @@ the basis of [Review apps](review_apps/index.md). NOTE: **Note:** The `name` and `url` parameters can use most of the CI/CD variables, including [predefined](variables/README.md#predefined-variables-environment-variables), -[secret](variables/README.md#secret-variables) and +[project/group ones](variables/README.md#variables) and [`.gitlab-ci.yml` variables](yaml/README.md#variables). You however cannot use variables defined under `script` or on the Runner's side. There are also other variables that are unsupported in the context of `environment:name`. You can read more about @@ -593,7 +593,7 @@ version of the app, all without leaving GitLab. >**Note:** Web terminals were added in GitLab 8.15 and are only available to project -masters and owners. +maintainers and owners. If you deploy to your environments with the help of a deployment service (e.g., the [Kubernetes integration][kube]), GitLab can open diff --git a/doc/ci/examples/README.md b/doc/ci/examples/README.md index de60cd27cd1..aa31e172641 100644 --- a/doc/ci/examples/README.md +++ b/doc/ci/examples/README.md @@ -19,7 +19,9 @@ There's also a collection of repositories with [example projects](https://gitlab - [How to test and deploy Laravel/PHP applications with GitLab CI/CD and Envoy](laravel_with_gitlab_and_envoy/index.md) - **Ruby**: [Test and deploy a Ruby application to Heroku](test-and-deploy-ruby-application-to-heroku.md) - **Python**: [Test and deploy a Python application to Heroku](test-and-deploy-python-application-to-heroku.md) -- **Java**: [Continuous Delivery of a Spring Boot application with GitLab CI and Kubernetes](https://about.gitlab.com/2016/12/14/continuous-delivery-of-a-spring-boot-application-with-gitlab-ci-and-kubernetes/) +- **Java**: + - [Deploy a Spring Boot application to Cloud Foundry with GitLab CI/CD](deploy_spring_boot_to_cloud_foundry/index.md) + - [Continuous Delivery of a Spring Boot application with GitLab CI and Kubernetes](https://about.gitlab.com/2016/12/14/continuous-delivery-of-a-spring-boot-application-with-gitlab-ci-and-kubernetes/) - **Scala**: [Test a Scala application](test-scala-application.md) - **Clojure**: [Test a Clojure application](test-clojure-application.md) - **Elixir**: diff --git a/doc/ci/examples/artifactory_and_gitlab/index.md b/doc/ci/examples/artifactory_and_gitlab/index.md index d931c9a77f4..9657f52159e 100644 --- a/doc/ci/examples/artifactory_and_gitlab/index.md +++ b/doc/ci/examples/artifactory_and_gitlab/index.md @@ -58,7 +58,7 @@ The application is ready to use, but you need some additional steps to deploy it 1. Log in to Artifactory with your user's credentials. 1. From the main screen, click on the `libs-release-local` item in the **Set Me Up** panel. 1. Copy to clipboard the configuration snippet under the **Deploy** paragraph. -1. Change the `url` value in order to have it configurable via secret variables. +1. Change the `url` value in order to have it configurable via variables. 1. Copy the snippet in the `pom.xml` file for your project, just after the `dependencies` section. The snippet should look like this: @@ -98,7 +98,7 @@ parameter in `.gitlab-ci.yml` to use the custom location instead of the default </settings> ``` - Username and password will be replaced by the correct values using secret variables. + Username and password will be replaced by the correct values using variables. ### Configure GitLab CI/CD for `simple-maven-dep` @@ -107,8 +107,8 @@ Now it's time we set up [GitLab CI/CD](https://about.gitlab.com/features/gitlab- GitLab CI/CD uses a file in the root of the repo, named `.gitlab-ci.yml`, to read the definitions for jobs that will be executed by the configured GitLab Runners. You can read more about this file in the [GitLab Documentation](https://docs.gitlab.com/ee/ci/yaml/). -First of all, remember to set up secret variables for your deployment. Navigate to your project's **Settings > CI/CD** page -and add the following secret variables (replace them with your current values, of course): +First of all, remember to set up variables for your deployment. Navigate to your project's **Settings > CI/CD > Variables** page +and add the following ones (replace them with your current values, of course): - **MAVEN_REPO_URL**: `http://artifactory.example.com:8081/artifactory` (your Artifactory URL) - **MAVEN_REPO_USER**: `gitlab` (your Artifactory username) @@ -156,7 +156,7 @@ by running all Maven phases in a sequential order, therefore, executing `mvn tes Both `build` and `test` jobs leverage the `mvn` command to compile the application and to test it as defined in the test suite that is part of the application. -Deploy to Artifactory is done as defined by the secret variables we have just set up. +Deploy to Artifactory is done as defined by the variables we have just set up. The deployment occurs only if we're pushing or merging to `master` branch, so that the development versions are tested but not published. Done! Now you have all the changes in the GitLab repo, and a pipeline has already been started for this commit. In the **Pipelines** tab you can see what's happening. diff --git a/doc/ci/examples/deploy_spring_boot_to_cloud_foundry/img/cloud_foundry_secret_variables.png b/doc/ci/examples/deploy_spring_boot_to_cloud_foundry/img/cloud_foundry_secret_variables.png Binary files differnew file mode 100644 index 00000000000..5b5d91ec07a --- /dev/null +++ b/doc/ci/examples/deploy_spring_boot_to_cloud_foundry/img/cloud_foundry_secret_variables.png diff --git a/doc/ci/examples/deploy_spring_boot_to_cloud_foundry/img/create_from_template.png b/doc/ci/examples/deploy_spring_boot_to_cloud_foundry/img/create_from_template.png Binary files differnew file mode 100644 index 00000000000..f3761632556 --- /dev/null +++ b/doc/ci/examples/deploy_spring_boot_to_cloud_foundry/img/create_from_template.png diff --git a/doc/ci/examples/deploy_spring_boot_to_cloud_foundry/index.md b/doc/ci/examples/deploy_spring_boot_to_cloud_foundry/index.md new file mode 100644 index 00000000000..b88761be56b --- /dev/null +++ b/doc/ci/examples/deploy_spring_boot_to_cloud_foundry/index.md @@ -0,0 +1,142 @@ +--- +author: Dylan Griffith +author_gitlab: DylanGriffith +level: intermediary +article_type: tutorial +date: 2018-06-07 +description: "Continuous Deployment of a Spring Boot application to Cloud Foundry with GitLab CI/CD" +--- + +# Deploy a Spring Boot application to Cloud Foundry with GitLab CI/CD + +## Introduction + +In this article, we'll demonstrate how to deploy a [Spring +Boot](https://projects.spring.io/spring-boot/) application to [Cloud +Foundry (CF)](https://www.cloudfoundry.org/) with GitLab CI/CD using the [Continuous +Deployment](https://about.gitlab.com/2016/08/05/continuous-integration-delivery-and-deployment-with-gitlab/#continuous-deployment) +method. + +All the code for this project can be found in this [GitLab +repo](https://gitlab.com/gitlab-examples/spring-gitlab-cf-deploy-demo). + +In case you're interested in deploying Spring Boot applications to Kubernetes +using GitLab CI/CD, read through the blog post [Continuous Delivery of a Spring Boot application with GitLab CI and Kubernetes](https://about.gitlab.com/2016/12/14/continuous-delivery-of-a-spring-boot-application-with-gitlab-ci-and-kubernetes/). + +## Requirements + +_We assume you are familiar with Java, GitLab, Cloud Foundry, and GitLab CI/CD._ + +To follow along with this tutorial you will need the following: + +- An account on [Pivotal Web Services (PWS)](https://run.pivotal.io/) or any + other Cloud Foundry instance +- An account on GitLab + +NOTE: **Note:** +You will need to replace the `api.run.pivotal.io` URL in the all below +commands with the [API +URL](https://docs.cloudfoundry.org/running/cf-api-endpoint.html) of your CF +instance if you're not deploying to PWS. + +## Create your project + +To create your Spring Boot application you can use the Spring template in +GitLab when creating a new project: + +![New Project From Template](img/create_from_template.png) + +## Configure the deployment to Cloud Foundry + +To deploy to Cloud Foundry we need to add a `manifest.yml` file. This +is the configuration for the CF CLI we will use to deploy the application. We +will create this in the root directory of our project with the following +content: + +```yaml +--- +applications: +- name: gitlab-hello-world + random-route: true + memory: 1G + path: target/demo-0.0.1-SNAPSHOT.jar +``` + +## Configure GitLab CI/CD to deploy your application + +Now we need to add the the GitLab CI/CD configuration file +([`.gitlab-ci.yml`](../../yaml/README.md)) to our +project's root. This is how GitLab figures out what commands need to be run whenever +code is pushed to our repository. We will add the following `.gitlab-ci.yml` +file to the root directory of the repository, GitLab will detect it +automatically and run the steps defined once we push our code: + +```yaml +image: java:8 + +stages: + - build + - deploy + +build: + stage: build + script: ./mvnw package + artifacts: + paths: + - target/demo-0.0.1-SNAPSHOT.jar + +production: + stage: deploy + script: + - curl --location "https://cli.run.pivotal.io/stable?release=linux64-binary&source=github" | tar zx + - ./cf login -u $CF_USERNAME -p $CF_PASSWORD -a api.run.pivotal.io + - ./cf push + only: + - master +``` + +We've used the `java:8` [docker +image](../../docker/using_docker_images.md) to build +our application as it provides the up-to-date Java 8 JDK on [Docker +Hub](https://hub.docker.com/). We've also added the [`only` +clause](../../yaml/README.md#only-and-except-simplified) +to ensure our deployments only happen when we push to the master branch. + +Now, since the steps defined in `.gitlab-ci.yml` require credentials to login +to CF, you'll need to add your CF credentials as [environment +variables](../../variables/README.md#predefined-variables-environment-variables) +on GitLab CI/CD. To set the environment variables, navigate to your project's +**Settings > CI/CD** and expand **Secret Variables**. Name the variables +`CF_USERNAME` and `CF_PASSWORD` and set them to the correct values. + +![Secret Variable Settings in GitLab](img/cloud_foundry_secret_variables.png) + +Once set up, GitLab CI/CD will deploy your app to CF at every push to your +repository's deafult branch. To see the build logs or watch your builds running +live, navigate to **CI/CD > Pipelines**. + +CAUTION: **Caution:** +It is considered best practice for security to create a separate deploy +user for your application and add its credentials to GitLab instead of using +a developer's credentials. + +To start a manual deployment in GitLab go to **CI/CD > Pipelines** then click +on **Run Pipeline**. Once the app is finished deploying it will display the URL +of your application in the logs for the `production` job like: + +```shell +requested state: started +instances: 1/1 +usage: 1G x 1 instances +urls: gitlab-hello-world-undissembling-hotchpot.cfapps.io +last uploaded: Mon Nov 6 10:02:25 UTC 2017 +stack: cflinuxfs2 +buildpack: client-certificate-mapper=1.2.0_RELEASE container-security-provider=1.8.0_RELEASE java-buildpack=v4.5-offline-https://github.com/cloudfoundry/java-buildpack.git#ffeefb9 java-main java-opts jvmkill-agent=1.10.0_RELEASE open-jdk-like-jre=1.8.0_1... + + state since cpu memory disk details +#0 running 2017-11-06 09:03:22 PM 120.4% 291.9M of 1G 137.6M of 1G +``` + +You can then visit your deployed application (for this example, +https://gitlab-hello-world-undissembling-hotchpot.cfapps.io/) and you should +see the "Spring is here!" message. diff --git a/doc/ci/examples/deployment/README.md b/doc/ci/examples/deployment/README.md index 2dcdc2d41ec..bd60d641493 100644 --- a/doc/ci/examples/deployment/README.md +++ b/doc/ci/examples/deployment/README.md @@ -111,7 +111,7 @@ We also use two secure variables: ## Storing API keys Secure Variables can added by going to your project's -**Settings ➔ CI / CD ➔ Secret variables**. The variables that are defined +**Settings ➔ CI / CD ➔ Variables**. The variables that are defined in the project settings are sent along with the build script to the Runner. The secure variables are stored out of the repository. Never store secrets in your project's `.gitlab-ci.yml`. It is also important that the secret's value diff --git a/doc/ci/examples/devops_and_game_dev_with_gitlab_ci_cd/index.md b/doc/ci/examples/devops_and_game_dev_with_gitlab_ci_cd/index.md index 3d21c0cc306..c226b5bfb71 100644 --- a/doc/ci/examples/devops_and_game_dev_with_gitlab_ci_cd/index.md +++ b/doc/ci/examples/devops_and_game_dev_with_gitlab_ci_cd/index.md @@ -406,7 +406,7 @@ and further delves into the principles of GitLab CI/CD than discussed in this ar We need to be able to deploy to AWS with our AWS account credentials, but we certainly don't want to put secrets into source code. Luckily GitLab provides a solution for this -with [Secret Variables](../../../ci/variables/README.md). This can get complicated +with [Variables](../../../ci/variables/README.md). This can get complicated due to [IAM](https://aws.amazon.com/iam/) management. As a best practice, you shouldn't use root security credentials. Proper IAM credential management is beyond the scope of this article, but AWS will remind you that using root credentials is unadvised and against their @@ -428,7 +428,7 @@ fully understand [IAM Best Practices in AWS](http://docs.aws.amazon.com/IAM/late To deploy our build artifacts, we need to install the [AWS CLI](https://aws.amazon.com/cli/) on the Shared Runner. The Shared Runner also needs to be able to authenticate with your AWS account to deploy the artifacts. By convention, AWS CLI will look for `AWS_ACCESS_KEY_ID` -and `AWS_SECRET_ACCESS_KEY`. GitLab's CI gives us a way to pass the secret variables we +and `AWS_SECRET_ACCESS_KEY`. GitLab's CI gives us a way to pass the variables we set up in the prior section using the `variables` portion of the `deploy` job. At the end, we add directives to ensure deployment `only` happens on pushes to `master`. This way, every single branch still runs through CI, and only merging (or committing directly) to master will diff --git a/doc/ci/examples/laravel_with_gitlab_and_envoy/index.md b/doc/ci/examples/laravel_with_gitlab_and_envoy/index.md index 1f9b9d53fc1..39c65399332 100644 --- a/doc/ci/examples/laravel_with_gitlab_and_envoy/index.md +++ b/doc/ci/examples/laravel_with_gitlab_and_envoy/index.md @@ -116,11 +116,11 @@ cat ~/.ssh/id_rsa.pub >> ~/.ssh/authorized_keys cat ~/.ssh/id_rsa ``` -Now, let's add it to your GitLab project as a [secret variable](../../variables/README.md#secret-variables). -Secret variables are user-defined variables and are stored out of `.gitlab-ci.yml`, for security purposes. +Now, let's add it to your GitLab project as a [variable](../../variables/README.md#variables). +Variables are user-defined variables and are stored out of `.gitlab-ci.yml`, for security purposes. They can be added per project by navigating to the project's **Settings** > **CI/CD**. -![secret variables page](img/secret_variables_page.png) +![variables page](img/secret_variables_page.png) To the field **KEY**, add the name `SSH_PRIVATE_KEY`, and to the **VALUE** field, paste the private key you've copied earlier. We'll use this variable in the `.gitlab-ci.yml` later, to easily connect to our remote server as the deployer user without entering its password. diff --git a/doc/ci/pipelines.md b/doc/ci/pipelines.md index b16cbc61d14..4e964af97f5 100644 --- a/doc/ci/pipelines.md +++ b/doc/ci/pipelines.md @@ -258,7 +258,7 @@ on that specific branch: - trigger **manual actions** on existing pipelines - **retry/cancel** existing jobs (using Web UI or Pipelines API) -**Secret variables** marked as **protected** are accessible only to jobs that +**Variables** marked as **protected** are accessible only to jobs that run on protected branches, avoiding untrusted users to get unintended access to sensitive information like deployment credentials and tokens. diff --git a/doc/ci/runners/README.md b/doc/ci/runners/README.md index 703a7f030ed..8f1ff190804 100644 --- a/doc/ci/runners/README.md +++ b/doc/ci/runners/README.md @@ -84,7 +84,7 @@ visit the project you want to make the Runner work for in GitLab: ## Registering a group Runner -Creating a group Runner requires Master permissions for the group. To create a +Creating a group Runner requires Maintainer permissions for the group. To create a group Runner visit the group you want to make the Runner work for in GitLab: 1. Go to **Settings > CI/CD** to obtain the token @@ -120,9 +120,9 @@ To lock/unlock a Runner: ## Assigning a Runner to another project -If you are Master on a project where a specific Runner is assigned to, and the +If you are Maintainer on a project where a specific Runner is assigned to, and the Runner is not [locked only to that project](#locking-a-specific-runner-from-being-enabled-for-other-projects), -you can enable the Runner also on any other project where you have Master permissions. +you can enable the Runner also on any other project where you have Maintainer permissions. To enable/disable a Runner in your project: @@ -132,7 +132,7 @@ To enable/disable a Runner in your project: > **Note**: Consider that if you don't lock your specific Runner to a specific project, any -user with Master role in you project can assign your Runner to another arbitrary +user with Maintainer role in you project can assign your Runner to another arbitrary project without requiring your authorization, so use it with caution. An admin can enable/disable a specific Runner for projects: diff --git a/doc/ci/ssh_keys/README.md b/doc/ci/ssh_keys/README.md index 693c8e9ef18..4cb05509e7b 100644 --- a/doc/ci/ssh_keys/README.md +++ b/doc/ci/ssh_keys/README.md @@ -25,7 +25,7 @@ with any type of [executor](https://docs.gitlab.com/runner/executors/) ## How it works 1. Create a new SSH key pair locally with [ssh-keygen](http://linux.die.net/man/1/ssh-keygen) -1. Add the private key as a [secret variable](../variables/README.md) to +1. Add the private key as a [variable](../variables/README.md) to your project 1. Run the [ssh-agent](http://linux.die.net/man/1/ssh-agent) during job to load the private key. @@ -49,7 +49,7 @@ to access it. This is where an SSH key pair comes in handy. **Do not** add a passphrase to the SSH key, or the `before_script` will\ prompt for it. -1. Create a new [secret variable](../variables/README.md#secret-variables). +1. Create a new [variable](../variables/README.md#variables). As **Key** enter the name `SSH_PRIVATE_KEY` and in the **Value** field paste the content of your _private_ key that you created earlier. @@ -157,7 +157,7 @@ ssh-keyscan example.com ssh-keyscan 1.2.3.4 ``` -Create a new [secret variable](../variables/README.md#secret-variables) with +Create a new [variable](../variables/README.md#variables) with `SSH_KNOWN_HOSTS` as "Key", and as a "Value" add the output of `ssh-keyscan`. NOTE: **Note:** @@ -165,7 +165,7 @@ If you need to connect to multiple servers, all the server host keys need to be collected in the **Value** of the variable, one key per line. TIP: **Tip:** -By using a secret variable instead of `ssh-keyscan` directly inside +By using a variable instead of `ssh-keyscan` directly inside `.gitlab-ci.yml`, it has the benefit that you don't have to change `.gitlab-ci.yml` if the host domain name changes for some reason. Also, the values are predefined by you, meaning that if the host keys suddenly change, the CI/CD job will fail, diff --git a/doc/ci/triggers/README.md b/doc/ci/triggers/README.md index 47a576fdf5f..c507036aa6a 100644 --- a/doc/ci/triggers/README.md +++ b/doc/ci/triggers/README.md @@ -53,7 +53,7 @@ The action is irreversible. it will not trigger a job. - If your project is public, passing the token in plain text is probably not the wisest idea, so you might want to use a - [secret variable](../variables/README.md#secret-variables) for that purpose. + [variable](../variables/README.md#variables) for that purpose. To trigger a job you need to send a `POST` request to GitLab's API endpoint: diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md index f10423b92cf..a3da6515a19 100644 --- a/doc/ci/variables/README.md +++ b/doc/ci/variables/README.md @@ -10,17 +10,17 @@ The variables can be overwritten and they take precedence over each other in this order: 1. [Trigger variables][triggers] or [scheduled pipeline variables](../../user/project/pipelines/schedules.md#making-use-of-scheduled-pipeline-variables) (take precedence over all) -1. Project-level [secret variables](#secret-variables) or [protected secret variables](#protected-secret-variables) -1. Group-level [secret variables](#secret-variables) or [protected secret variables](#protected-secret-variables) +1. Project-level [variables](#variables) or [protected variables](#protected-variables) +1. Group-level [variables](#variables) or [protected variables](#protected-variables) 1. YAML-defined [job-level variables](../yaml/README.md#variables) 1. YAML-defined [global variables](../yaml/README.md#variables) 1. [Deployment variables](#deployment-variables) 1. [Predefined variables](#predefined-variables-environment-variables) (are the lowest in the chain) -For example, if you define `API_TOKEN=secure` as a secret variable and +For example, if you define `API_TOKEN=secure` as a project variable and `API_TOKEN=yaml` in your `.gitlab-ci.yml`, the `API_TOKEN` will take the value -`secure` as the secret variables are higher in the chain. +`secure` as the project variables are higher in the chain. ## Unsupported variables @@ -64,6 +64,7 @@ future GitLab releases.** | **CI_JOB_NAME** | 9.0 | 0.5 | The name of the job as defined in `.gitlab-ci.yml` | | **CI_JOB_STAGE** | 9.0 | 0.5 | The name of the stage as defined in `.gitlab-ci.yml` | | **CI_JOB_TOKEN** | 9.0 | 1.2 | Token used for authenticating with the GitLab Container Registry | +| **CI_JOB_URL** | 11.0 | 0.5 | Job details URL | | **CI_REPOSITORY_URL** | 9.0 | all | The URL to clone the Git repository | | **CI_RUNNER_DESCRIPTION** | 8.10 | 0.5 | The description of the runner as saved in GitLab | | **CI_RUNNER_ID** | 8.10 | 0.5 | The unique id of runner being used | @@ -72,6 +73,7 @@ future GitLab releases.** | **CI_RUNNER_REVISION** | all | 10.6 | GitLab Runner revision that is executing the current job | | **CI_RUNNER_EXECUTABLE_ARCH** | all | 10.6 | The OS/architecture of the GitLab Runner executable (note that this is not necessarily the same as the environment of the executor) | | **CI_PIPELINE_ID** | 8.10 | 0.5 | The unique id of the current pipeline that GitLab CI uses internally | +| **CI_PIPELINE_IID** | 11.0 | all | The unique id of the current pipeline scoped to project | | **CI_PIPELINE_TRIGGERED** | all | all | The flag to indicate that job was [triggered] | | **CI_PIPELINE_SOURCE** | 10.0 | all | Indicates how the pipeline was triggered. Possible options are: `push`, `web`, `trigger`, `schedule`, `api`, and `pipeline`. For pipelines created before GitLab 9.5, this will show as `unknown` | | **CI_PROJECT_DIR** | all | all | The full path where the repository is cloned and where the job is run | @@ -80,6 +82,7 @@ future GitLab releases.** | **CI_PROJECT_NAMESPACE** | 8.10 | 0.5 | The project namespace (username or groupname) that is currently being built | | **CI_PROJECT_PATH** | 8.10 | 0.5 | The namespace with project name | | **CI_PROJECT_PATH_SLUG** | 9.3 | all | `$CI_PROJECT_PATH` lowercased and with everything except `0-9` and `a-z` replaced with `-`. Use in URLs and domain names. | +| **CI_PIPELINE_URL** | 11.0 | 0.5 | Pipeline details URL | | **CI_PROJECT_URL** | 8.10 | 0.5 | The HTTP address to access project | | **CI_PROJECT_VISIBILITY** | 10.3 | all | The project visibility (internal, private, public) | | **CI_REGISTRY** | 8.10 | 0.5 | If the Container Registry is enabled it returns the address of GitLab's Container Registry | @@ -164,49 +167,49 @@ script: - 'eval $LS_CMD' # will execute 'ls -al $TMP_DIR' ``` -## Secret variables +## Variables NOTE: **Note:** -Group-level secret variables were added in GitLab 9.4. +Group-level variables were added in GitLab 9.4. CAUTION: **Important:** -Be aware that secret variables are not masked, and their values can be shown +Be aware that variables are not masked, and their values can be shown in the job logs if explicitly asked to do so. If your project is public or internal, you can set the pipelines private from your [project's Pipelines settings](../../user/project/pipelines/settings.md#visibility-of-pipelines). -Follow the discussion in issue [#13784][ce-13784] for masking the secret variables. +Follow the discussion in issue [#13784][ce-13784] for masking the variables. -GitLab CI allows you to define per-project or per-group secret variables -that are set in the pipeline environment. The secret variables are stored out of +GitLab CI allows you to define per-project or per-group variables +that are set in the pipeline environment. The variables are stored out of the repository (not in `.gitlab-ci.yml`) and are securely passed to GitLab Runner making them available during a pipeline run. It's the recommended method to use for storing things like passwords, SSH keys and credentials. -Project-level secret variables can be added by going to your project's -**Settings > CI/CD**, then finding the section called **Secret variables**. +Project-level variables can be added by going to your project's +**Settings > CI/CD**, then finding the section called **Variables**. -Likewise, group-level secret variables can be added by going to your group's -**Settings > CI/CD**, then finding the section called **Secret variables**. +Likewise, group-level variables can be added by going to your group's +**Settings > CI/CD**, then finding the section called **Variables**. Any variables of [subgroups] will be inherited recursively. -![Secret variables](img/secret_variables.png) +![Variables](img/secret_variables.png) Once you set them, they will be available for all subsequent pipelines. You can also -[protect your variables](#protected-secret-variables). +[protect your variables](#protected-variables). -### Protected secret variables +### Protected variables >**Notes:** This feature requires GitLab 9.3 or higher. -Secret variables could be protected. Whenever a secret variable is +Variables could be protected. Whenever a variable is protected, it would only be securely passed to pipelines running on the [protected branches] or [protected tags]. The other pipelines would not get any protected variables. Protected variables can be added by going to your project's **Settings > CI/CD**, then finding the section called -**Secret variables**, and check "Protected". +**Variables**, and check "Protected". Once you set them, they will be available for all subsequent pipelines. @@ -230,7 +233,7 @@ An example project service that defines deployment variables is the CAUTION: **Warning:** Enabling debug tracing can have severe security implications. The -output **will** contain the content of all your secret variables and any other +output **will** contain the content of all your variables and any other secrets! The output **will** be uploaded to the GitLab server and made visible in job traces! @@ -352,6 +355,8 @@ Running on runner-8a2f473d-project-1796893-concurrent-0 via runner-8a2f473d-mach ++ CI_PROJECT_URL=https://example.com/gitlab-examples/ci-debug-trace ++ export CI_PIPELINE_ID=52666 ++ CI_PIPELINE_ID=52666 +++ export CI_PIPELINE_IID=123 +++ CI_PIPELINE_IID=123 ++ export CI_RUNNER_ID=1337 ++ CI_RUNNER_ID=1337 ++ export CI_RUNNER_DESCRIPTION=shared-runners-manager-1.example.com @@ -416,7 +421,7 @@ job_name: ``` You can also list all environment variables with the `export` command, -but be aware that this will also expose the values of all the secret variables +but be aware that this will also expose the values of all the variables you set, in the job log: ```yaml @@ -439,6 +444,7 @@ export CI_JOB_MANUAL="true" export CI_JOB_TRIGGERED="true" export CI_JOB_TOKEN="abcde-1234ABCD5678ef" export CI_PIPELINE_ID="1000" +export CI_PIPELINE_IID="10" export CI_PROJECT_ID="34" export CI_PROJECT_DIR="/builds/gitlab-org/gitlab-ce" export CI_PROJECT_NAME="gitlab-ce" @@ -468,7 +474,7 @@ It is possible to use variables expressions with only / except policies in `.gitlab-ci.yml`. By using this approach you can limit what jobs are going to be created within a pipeline after pushing a code to GitLab. -This is particularly useful in combination with secret variables and triggered +This is particularly useful in combination with variables and triggered pipeline variables. ```yaml @@ -546,7 +552,7 @@ Below you can find supported syntax reference: Pattern matching is case-sensitive by default. Use `i` flag modifier, like `/pattern/i` to make a pattern case-insensitive. -[ce-13784]: https://gitlab.com/gitlab-org/gitlab-ce/issues/13784 "Simple protection of CI secret variables" +[ce-13784]: https://gitlab.com/gitlab-org/gitlab-ce/issues/13784 "Simple protection of CI variables" [eep]: https://about.gitlab.com/products/ "Available only in GitLab Premium" [envs]: ../environments.md [protected branches]: ../../user/project/protected_branches.md diff --git a/doc/ci/variables/where_variables_can_be_used.md b/doc/ci/variables/where_variables_can_be_used.md index 9800784d918..b2b4a26bdda 100644 --- a/doc/ci/variables/where_variables_can_be_used.md +++ b/doc/ci/variables/where_variables_can_be_used.md @@ -17,7 +17,7 @@ There are basically two places where you can use any defined variables: | Definition | Can be expanded? | Expansion place | Description | |--------------------------------------|-------------------|-----------------|--------------| -| `environment:url` | yes | GitLab | The variable expansion is made by GitLab's [internal variable expansion mechanism](#gitlab-internal-variable-expansion-mechanism).<ul><li>**Supported:** all variables defined for a job (secret variables, variables from `.gitlab-ci.yml`, variables from triggers, variables from pipeline schedules)</li><li>**Not suported:** variables defined in Runner's `config.toml` and variables created in job's `script`</li></ul> | +| `environment:url` | yes | GitLab | The variable expansion is made by GitLab's [internal variable expansion mechanism](#gitlab-internal-variable-expansion-mechanism).<ul><li>**Supported:** all variables defined for a job (project/group variables, variables from `.gitlab-ci.yml`, variables from triggers, variables from pipeline schedules)</li><li>**Not suported:** variables defined in Runner's `config.toml` and variables created in job's `script`</li></ul> | | `environment:name` | yes | GitLab | Similar to `environment:url`, but the variables expansion **doesn't support**: <ul><li>variables that are based on the environment's name (`CI_ENVIRONMENT_NAME`, `CI_ENVIRONMENT_SLUG`)</li><li>any other variables related to environment (currently only `CI_ENVIRONMENT_URL`)</li><li>[persisted variables](#persisted-variables)</li></ul> | | `variables` | yes | Runner | The variable expansion is made by GitLab Runner's [internal variable expansion mechanism](#gitlab-runner-internal-variable-expansion-mechanism) | | `image` | yes | Runner | The variable expansion is made by GitLab Runner's [internal variable expansion mechanism](#gitlab-runner-internal-variable-expansion-mechanism) | @@ -55,7 +55,7 @@ since the expansion is done in GitLab before any Runner will get the job. ### GitLab Runner internal variable expansion mechanism -- **Supported:** secret variables, `.gitlab-ci.yml` variables, `config.toml` variables, and +- **Supported:** project/group variables, `.gitlab-ci.yml` variables, `config.toml` variables, and variables from triggers and pipeline schedules - **Not supported:** variables defined inside of scripts (e.g., `export MY_VARIABLE="test"`) @@ -76,7 +76,7 @@ are using a different variables syntax. **Supported:** - The `script` may use all available variables that are default for the shell (e.g., `$PATH` which - should be present in all bash/sh shells) and all variables defined by GitLab CI/CD (secret variables, + should be present in all bash/sh shells) and all variables defined by GitLab CI/CD (project/group variables, `.gitlab-ci.yml` variables, `config.toml` variables, and variables from triggers and pipeline schedules). - The `script` may also use all variables defined in the lines before. So, for example, if you define a variable `export MY_VARIABLE="test"`: diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index 3e77a6f58b7..e11eb840265 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -327,7 +327,7 @@ Refs strategy equals to simplified only/except configuration, whereas kubernetes strategy accepts only `active` keyword. `variables` keyword is used to define variables expressions. In other words -you can use predefined variables / secret variables / project / group or +you can use predefined variables / project / group or environment-scoped variables to define an expression GitLab is going to evaluate in order to decide whether a job should be created or not. @@ -799,16 +799,6 @@ cache: - binaries/ ``` -If you use **Windows PowerShell** to run your shell scripts you need to replace -`$` with `$env:`: - -```yaml -cache: - key: "$env:CI_COMMIT_REF_SLUG" - paths: - - binaries/ -``` - ### `cache:untracked` Set `untracked: true` to cache all files that are untracked in your Git @@ -1249,7 +1239,7 @@ Runner itself](../variables/README.md#predefined-variables-environment-variables One example would be `CI_COMMIT_REF_NAME` which has the value of the branch or tag name for which project is built. Apart from the variables you can set in `.gitlab-ci.yml`, there are also the so called -[secret variables](../variables/README.md#secret-variables) +[Variables](../variables/README.md#variables) which can be set in GitLab's UI. [Learn more about variables and their priority.][variables] @@ -1416,6 +1406,43 @@ variables: You can set it globally or per-job in the [`variables`](#variables) section. +### Custom build directories + +> [Introduced][gitlab-runner-876] in Gitlab Runner 11.1 + +NOTE: **Note:** +This can only be used when `custom_build_dir` is set to true in the [Runner's +configuration](https://docs.gitlab.com/runner/configuration/advanced-configuration.html). + +By default, GitLab Runner clones the repository in the `/builds` directory, +but sometimes your project might require to have the code in a specific +directory, like the GO projects for example. In that case, you can specify +the `CI_PROJECT_DIR` variable to tell the Runner in which directory to clone +the repository: + +```yml +image: golang:1.10-alpine3.7 + +variables: + CI_PROJECT_DIR: /go/src/gitlab.com/namespace/project-name + +stages: + - test + +dir: + stage: test + script: + - pwd # /go/src/gitlab.com/namespace/project-name +``` + +The following executors may use this feature only when +[concurrent](https://docs.gitlab.com/runner/configuration/advanced-configuration.html#the-global-section) +is set to `1`: + +- `shell` +- `ssh` +- `docker`, `docker+machine` when the job's working directory is mounted as a host volume. + ## Special YAML features It's possible to use special YAML features like anchors (`&`), aliases (`*`) @@ -1609,5 +1636,6 @@ CI with various languages. [ce-7983]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7983 [ce-7447]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7447 [ce-12909]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/12909 +[gitlab-runner-876]: https://gitlab.com/gitlab-org/gitlab-runner/merge_requests/876 [schedules]: ../../user/project/pipelines/schedules.md [variables-expressions]: ../variables/README.md#variables-expressions diff --git a/doc/customization/favicon.md b/doc/customization/favicon.md new file mode 100644 index 00000000000..45a18159b5e --- /dev/null +++ b/doc/customization/favicon.md @@ -0,0 +1,16 @@ +# Changing the favicon + +> [Introduced][ce-14497] in GitLab 11.0. + +[ce-14497]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/14497 + +Navigate to the **Admin** area and go to the **Appearance** page. + +Upload the custom favicon (**Favicon**) in the section **Favicon**. + +![appearance](favicon/appearance.png) + +After saving the page, the new favicon will be shown in the browser. The main +favicon as well as the CI status icons will show the custom icon: + +![custom_favicon](favicon/custom_favicon.png) diff --git a/doc/customization/favicon/appearance.png b/doc/customization/favicon/appearance.png Binary files differnew file mode 100644 index 00000000000..6c41a05fc1f --- /dev/null +++ b/doc/customization/favicon/appearance.png diff --git a/doc/customization/favicon/custom_favicon.png b/doc/customization/favicon/custom_favicon.png Binary files differnew file mode 100644 index 00000000000..fa1b8827a36 --- /dev/null +++ b/doc/customization/favicon/custom_favicon.png diff --git a/doc/development/README.md b/doc/development/README.md index 898c60e96c0..5d6fed5bc72 100644 --- a/doc/development/README.md +++ b/doc/development/README.md @@ -32,6 +32,8 @@ description: 'Learn how to contribute to GitLab.' - [GitLab utilities](utilities.md) - [API styleguide](api_styleguide.md) Use this styleguide if you are contributing to the API. +- [GraphQL API styleguide](api_graphql_styleguide.md) Use this + styleguide if you are contribution to the [GraphQL API](../api/graphql/index.md) - [Sidekiq guidelines](sidekiq_style_guide.md) for working with Sidekiq workers - [Working with Gitaly](gitaly.md) - [Manage feature flags](feature_flags.md) @@ -86,8 +88,8 @@ description: 'Learn how to contribute to GitLab.' ## Documentation guides -- [Writing documentation](writing_documentation.md) -- [Documentation styleguide](doc_styleguide.md) +- [Writing documentation](documentation/index.md) +- [Documentation styleguide](documentation/styleguide.md) - [Markdown](../user/markdown.md) ## Internationalization (i18n) guides diff --git a/doc/development/api_graphql_styleguide.md b/doc/development/api_graphql_styleguide.md new file mode 100644 index 00000000000..f74e4f0bd7e --- /dev/null +++ b/doc/development/api_graphql_styleguide.md @@ -0,0 +1,81 @@ +# GraphQL API + +## Authentication + +Authentication happens through the `GraphqlController`, right now this +uses the same authentication as the Rails application. So the session +can be shared. + +It is also possible to add a `private_token` to the querystring, or +add a `HTTP_PRIVATE_TOKEN` header. + +### Authorization + +Fields can be authorized using the same abilities used in the Rails +app. This can be done using the `authorize` helper: + +```ruby +module Types + class QueryType < BaseObject + graphql_name 'Query' + + field :project, Types::ProjectType, null: true, resolver: Resolvers::ProjectResolver do + authorize :read_project + end + end +``` + +The object found by the resolve call is used for authorization. + +This works for authorizing a single record, for authorizing +collections, we should only load what the currently authenticated user +is allowed to view. Preferably we use our existing finders for that. + +## Types + +When exposing a model through the GraphQL API, we do so by creating a +new type in `app/graphql/types`. + +When exposing properties in a type, make sure to keep the logic inside +the definition as minimal as possible. Instead, consider moving any +logic into a presenter: + +```ruby +class Types::MergeRequestType < BaseObject + present_using MergeRequestPresenter + + name 'MergeRequest' +end +``` + +An existing presenter could be used, but it is also possible to create +a new presenter specifically for GraphQL. + +The presenter is initialized using the object resolved by a field, and +the context. + +## Resolvers + +To find objects to display in a field, we can add resolvers to +`app/graphql/resolvers`. + +Arguments can be defined within the resolver, those arguments will be +made available to the fields using the resolver. + +We already have a `FullPathLoader` that can be included in other +resolvers to quickly find Projects and Namespaces which will have a +lot of dependant objects. + +To limit the amount of queries performed, we can use `BatchLoader`. + +## Testing + +_full stack_ tests for a graphql query or mutation live in +`spec/requests/api/graphql`. + +When adding a query, the `a working graphql query` shared example can +be used to test if the query renders valid results. + +Using the `GraphqlHelpers#all_graphql_fields_for`-helper, a query +including all available fields can be constructed. This makes it easy +to add a test rendering all possible fields for a query. diff --git a/doc/development/changelog.md b/doc/development/changelog.md index a9fa5ae834f..9e0c81b3d60 100644 --- a/doc/development/changelog.md +++ b/doc/development/changelog.md @@ -45,6 +45,8 @@ the `author` field. GitLab team members **should not**. a changelog entry regardless of these guidelines if the contributor wants one. Example: "Fixed a typo on the search results page. (Jane Smith)" - Performance improvements **should** have a changelog entry. +- Any change that introduces a database migration **must** have a + changelog entry. ## Writing good changelog entries diff --git a/doc/development/doc_styleguide.md b/doc/development/doc_styleguide.md index 5d595c33915..7d84f8ca86a 100644 --- a/doc/development/doc_styleguide.md +++ b/doc/development/doc_styleguide.md @@ -1,499 +1,3 @@ --- -description: 'Writing styles, markup, formatting, and reusing regular expressions throughout the GitLab Documentation.' +redirect_to: 'documentation/styleguide.md' --- - -# Documentation style guidelines - -The documentation style guide defines the markup structure used in -GitLab documentation. Check the -[documentation guidelines](writing_documentation.md) for general development instructions. - -Check the GitLab handbook for the [writing styles guidelines](https://about.gitlab.com/handbook/communication/#writing-style-guidelines). - -## Text - -- Split up long lines (wrap text), this makes it much easier to review and edit. Only - double line breaks are shown as a full line break in [GitLab markdown][gfm]. - 80-100 characters is a good line length -- Make sure that the documentation is added in the correct - [directory](writing_documentation.md#documentation-directory-structure) and that - there's a link to it somewhere useful -- Do not duplicate information -- Be brief and clear -- Unless there's a logical reason not to, add documents in alphabetical order -- Write in US English -- Use [single spaces][] instead of double spaces -- Jump a line between different markups (e.g., after every paragraph, header, list, etc) -- Capitalize "G" and "L" in GitLab -- Use sentence case for titles, headings, labels, menu items, and buttons. -- Use title case when referring to [features](https://about.gitlab.com/features/) or [products](https://about.gitlab.com/pricing/), and methods. Note that some features are also objects (e.g. "Merge Requests" and "merge requests"). E.g.: GitLab Runner, Geo, Issue Boards, Git, Prometheus, Continuous Integration. - -## Formatting - -- Use double asterisks (`**`) to mark a word or text in bold (`**bold**`) -- Use undescore (`_`) for text in italics (`_italic_`) -- Jump a line between different markups, for example: - - ```md - ## Header - - Paragraph. - - - List item - - List item - ``` - -### Punctuation - -For punctuation rules, please refer to the [GitLab UX guide](https://design.gitlab.com/content/punctuation/). - -### Ordered and unordered lists - -- Use dashes (`-`) for unordered lists instead of asterisks (`*`) -- Use the number one (`1`) for ordered lists -- For punctuation in bullet lists, please refer to the [GitLab UX guide](https://design.gitlab.com/content/punctuation/) - -## Headings - -- Add **only one H1** in each document, by adding `#` at the beginning of - it (when using markdown). The `h1` will be the document `<title>`. -- For subheadings, use `##`, `###` and so on -- Avoid putting numbers in headings. Numbers shift, hence documentation anchor - links shift too, which eventually leads to dead links. If you think it is - compelling to add numbers in headings, make sure to at least discuss it with - someone in the Merge Request -- [Avoid using symbols and special chars](https://gitlab.com/gitlab-com/gitlab-docs/issues/84) - in headers. Whenever possible, they should be plain and short text. -- Avoid adding things that show ephemeral statuses. For example, if a feature is - considered beta or experimental, put this info in a note, not in the heading. -- When introducing a new document, be careful for the headings to be - grammatically and syntactically correct. Mention one or all - of the following GitLab members for a review: `@axil` or `@marcia`. - This is to ensure that no document with wrong heading is going - live without an audit, thus preventing dead links and redirection issues when - corrected -- Leave exactly one newline after a heading - -## Links - -- Use the regular inline link markdown markup `[Text](https://example.com)`. - It's easier to read, review, and maintain. -- If there's a link that repeats several times through the same document, - you can use `[Text][identifier]` and at the bottom of the section or the - document add: `[identifier]: https://example.com`, in which case, we do - encourage you to also add an alternative text: `[identifier]: https://example.com "Alternative text"` that appears when hovering your mouse on a link. -- To link to internal documentation, use relative links, not full URLs. Use `../` to - navigate tp high-level directories, and always add the file name `file.md` at the - end of the link with the `.md` extension, not `.html`. - Example: instead of `[text](../../merge_requests/)`, use - `[text](../../merge_requests/index.md)` or, `[text](../../ci/README.md)`, or, - for anchor links, `[text](../../ci/README.md#examples)`. - Using the markdown extension is necessary for the [`/help`](writing_documentation.md#gitlab-help) - section of GitLab. -- To link from CE to EE-only documentation, use the EE-only doc full URL. -- Use [meaningful anchor texts](https://www.futurehosting.com/blog/links-should-have-meaningful-anchor-text-heres-why/). - E.g., instead of writing something like `Read more about GitLab Issue Boards [here](LINK)`, - write `Read more about [GitLab Issue Boards](LINK)`. - -## Images - -- Place images in a separate directory named `img/` in the same directory where - the `.md` document that you're working on is located. Always prepend their - names with the name of the document that they will be included in. For - example, if there is a document called `twitter.md`, then a valid image name - could be `twitter_login_screen.png`. [**Exception**: images for - [articles](writing_documentation.md#technical-articles) should be - put in a directory called `img` underneath `/articles/article_title/img/`, therefore, - there's no need to prepend the document name to their filenames.] -- Images should have a specific, non-generic name that will differentiate them. -- Keep all file names in lower case. -- Consider using PNG images instead of JPEG. -- Compress all images with <https://tinypng.com/> or similar tool. -- Compress gifs with <https://ezgif.com/optimize> or similar tool. -- Images should be used (only when necessary) to _illustrate_ the description -of a process, not to _replace_ it. - -Inside the document: - -- The Markdown way of using an image inside a document is: - `![Proper description what the image is about](img/document_image_title.png)` -- Always use a proper description for what the image is about. That way, when a - browser fails to show the image, this text will be used as an alternative - description -- If there are consecutive images with little text between them, always add - three dashes (`---`) between the image and the text to create a horizontal - line for better clarity -- If a heading is placed right after an image, always add three dashes (`---`) - between the image and the heading - -## Alert boxes - -Whenever you want to call the attention to a particular sentence, -use the following markup for highlighting. - -_Note that the alert boxes only work for one paragraph only. Multiple paragraphs, -lists, headers, etc will not render correctly._ - -### Note - -```md -NOTE: **Note:** -This is something to note. -``` - -How it renders in docs.gitlab.com: - -NOTE: **Note:** -This is something to note. - -### Tip - -```md -TIP: **Tip:** -This is a tip. -``` - -How it renders in docs.gitlab.com: - -TIP: **Tip:** -This is a tip. - -### Caution - -```md -CAUTION: **Caution:** -This is something to be cautious about. -``` - -How it renders in docs.gitlab.com: - -CAUTION: **Caution:** -This is something to be cautious about. - -### Danger - -```md -DANGER: **Danger:** -This is a breaking change, a bug, or something very important to note. -``` - -How it renders in docs.gitlab.com: - -DANGER: **Danger:** -This is a breaking change, a bug, or something very important to note. - -## Blockquotes - -For highlighting a text within a blue blockquote, use this format: - -```md -> This is a blockquote. -``` - -which renders in docs.gitlab.com to: - -> This is a blockquote. - -If the text spans across multiple lines it's OK to split the line. - -## Specific sections and terms - -To mention and/or reference specific terms in GitLab, please follow the styles -below. - -### GitLab versions and tiers - -- Every piece of documentation that comes with a new feature should declare the - GitLab version that feature got introduced. Right below the heading add a - note: - - ```md - > Introduced in GitLab 8.3. - ``` - -- Whenever possible, every feature should have a link to the MR, issue, or epic that introduced it. - The above note would be then transformed to: - - ```md - > [Introduced][ce-1242] in GitLab 8.3. - ``` - - , where the [link identifier](#links) is named after the repository (CE) and - the MR number. - -- If the feature is only available in GitLab Enterprise Edition, don't forget to mention - the [paid tier](https://about.gitlab.com/handbook/marketing/product-marketing/#tiers) - the feature is available in: - - ```md - > [Introduced][ee-1234] in [GitLab Starter](https://about.gitlab.com/pricing/) 8.3. - ``` - -### Product badges - -When a feature is available in EE-only tiers, add the corresponding tier according to the -feature availability: - -- For GitLab Starter and GitLab.com Bronze: `**[STARTER]**` -- For GitLab Premium and GitLab.com Silver: `**[PREMIUM]**` -- For GitLab Ultimate and GitLab.com Gold: `**[ULTIMATE]**` -- For GitLab Core and GitLab.com Free: `**[CORE]**` - -To exclude GitLab.com tiers (when the feature is not available in GitLab.com), add the -keyword "only": - -- For GitLab Starter: `**[STARTER ONLY]**` -- For GitLab Premium: `**[PREMIUM ONLY]**` -- For GitLab Ultimate: `**[ULTIMATE ONLY]**` -- For GitLab Core: `**[CORE ONLY]**` - -The tier should be ideally added to headers, so that the full badge will be displayed. -But it can be also mentioned from paragraphs, list items, and table cells. For these cases, -the tier mention will be represented by an orange question mark. -E.g., `**[STARTER]**` renders **[STARTER]**, `**[STARTER ONLY]**` renders **[STARTER ONLY]**. - -The absence of tiers' mentions mean that the feature is available in GitLab Core, -GitLab.com Free, and higher tiers. - -#### How it works - -Introduced by [!244](https://gitlab.com/gitlab-com/gitlab-docs/merge_requests/244), -the special markup `**[STARTER]**` will generate a `span` element to trigger the -badges and tooltips (`<span class="badge-trigger starter">`). When the keyword -"only" is added, the corresponding GitLab.com badge will not be displayed. - -### GitLab Restart - -There are many cases that a restart/reconfigure of GitLab is required. To -avoid duplication, link to the special document that can be found in -[`doc/administration/restart_gitlab.md`][doc-restart]. Usually the text will -read like: - - ``` - Save the file and [reconfigure GitLab](../administration/restart_gitlab.md) - for the changes to take effect. - ``` - -If the document you are editing resides in a place other than the GitLab CE/EE -`doc/` directory, instead of the relative link, use the full path: -`http://docs.gitlab.com/ce/administration/restart_gitlab.html`. -Replace `reconfigure` with `restart` where appropriate. - -### Installation guide - -**Ruby:** -In [step 2 of the installation guide](../install/installation.md#2-ruby), -we install Ruby from source. Whenever there is a new version that needs to -be updated, remember to change it throughout the codeblock and also replace -the sha256sum (it can be found in the [downloads page][ruby-dl] of the Ruby -website). - -[ruby-dl]: https://www.ruby-lang.org/en/downloads/ "Ruby download website" - -### Configuration documentation for source and Omnibus installations - -GitLab currently officially supports two installation methods: installations -from source and Omnibus packages installations. - -Whenever there is a setting that is configurable for both installation methods, -prefer to document it in the CE docs to avoid duplication. - -Configuration settings include: - -- settings that touch configuration files in `config/` -- NGINX settings and settings in `lib/support/` in general - -When there is a list of steps to perform, usually that entails editing the -configuration file and reconfiguring/restarting GitLab. In such case, follow -the style below as a guide: - -```md -**For Omnibus installations** - -1. Edit `/etc/gitlab/gitlab.rb`: - - ```ruby - external_url "https://gitlab.example.com" - ``` - -1. Save the file and [reconfigure] GitLab for the changes to take effect. - ---- - -**For installations from source** - -1. Edit `config/gitlab.yml`: - - ```yaml - gitlab: - host: "gitlab.example.com" - ``` - -1. Save the file and [restart] GitLab for the changes to take effect. - - -[reconfigure]: path/to/administration/restart_gitlab.md#omnibus-gitlab-reconfigure -[restart]: path/to/administration/restart_gitlab.md#installations-from-source -``` - -In this case: - -- before each step list the installation method is declared in bold -- three dashes (`---`) are used to create a horizontal line and separate the - two methods -- the code blocks are indented one or more spaces under the list item to render - correctly -- different highlighting languages are used for each config in the code block -- the [references](#references) guide is used for reconfigure/restart - -### Fake tokens - -There may be times where a token is needed to demonstrate an API call using -cURL or a secret variable used in CI. It is strongly advised not to use real -tokens in documentation even if the probability of a token being exploited is -low. - -You can use the following fake tokens as examples. - -| **Token type** | **Token value** | -| --------------------- | --------------------------------- | -| Private user token | `9koXpg98eAheJpvBs5tK` | -| Personal access token | `n671WNGecHugsdEDPsyo` | -| Application ID | `2fcb195768c39e9a94cec2c2e32c59c0aad7a3365c10892e8116b5d83d4096b6` | -| Application secret | `04f294d1eaca42b8692017b426d53bbc8fe75f827734f0260710b83a556082df` | -| Secret CI variable | `Li8j-mLUVA3eZYjPfd_H` | -| Specific Runner token | `yrnZW46BrtBFqM7xDzE7dddd` | -| Shared Runner token | `6Vk7ZsosqQyfreAxXTZr` | -| Trigger token | `be20d8dcc028677c931e04f3871a9b` | -| Webhook secret token | `6XhDroRcYPM5by_h-HLY` | -| Health check token | `Tu7BgjR9qeZTEyRzGG2P` | -| Request profile token | `7VgpS4Ax5utVD2esNstz` | - -### API - -Here is a list of must-have items. Use them in the exact order that appears -on this document. Further explanation is given below. - -- Every method must have the REST API request. For example: - - ``` - GET /projects/:id/repository/branches - ``` - -- Every method must have a detailed - [description of the parameters](#method-description). -- Every method must have a cURL example. -- Every method must have a response body (in JSON format). - -#### Method description - -Use the following table headers to describe the methods. Attributes should -always be in code blocks using backticks (``` ` ```). - -``` -| Attribute | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -``` - -Rendered example: - -| Attribute | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `user` | string | yes | The GitLab username | - -#### cURL commands - -- Use `https://gitlab.example.com/api/v4/` as an endpoint. -- Wherever needed use this personal access token: `9koXpg98eAheJpvBs5tK`. -- Always put the request first. `GET` is the default so you don't have to - include it. -- Use double quotes to the URL when it includes additional parameters. -- Prefer to use examples using the personal access token and don't pass data of - username and password. - -| Methods | Description | -| ------- | ----------- | -| `-H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK"` | Use this method as is, whenever authentication needed | -| `-X POST` | Use this method when creating new objects | -| `-X PUT` | Use this method when updating existing objects | -| `-X DELETE` | Use this method when removing existing objects | - -#### cURL Examples - -Below is a set of [cURL][] examples that you can use in the API documentation. - -##### Simple cURL command - -Get the details of a group: - -```bash -curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/gitlab-org -``` - -##### cURL example with parameters passed in the URL - -Create a new project under the authenticated user's namespace: - -```bash -curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects?name=foo" -``` - -##### Post data using cURL's --data - -Instead of using `-X POST` and appending the parameters to the URI, you can use -cURL's `--data` option. The example below will create a new project `foo` under -the authenticated user's namespace. - -```bash -curl --data "name=foo" --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects" -``` - -##### Post data using JSON content - -> **Note:** In this example we create a new group. Watch carefully the single -and double quotes. - -```bash -curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --header "Content-Type: application/json" --data '{"path": "my-group", "name": "My group"}' https://gitlab.example.com/api/v4/groups -``` - -##### Post data using form-data - -Instead of using JSON or urlencode you can use multipart/form-data which -properly handles data encoding: - -```bash -curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --form "title=ssh-key" --form "key=ssh-rsa AAAAB3NzaC1yc2EA..." https://gitlab.example.com/api/v4/users/25/keys -``` - -The above example is run by and administrator and will add an SSH public key -titled ssh-key to user's account which has an id of 25. - -##### Escape special characters - -Spaces or slashes (`/`) may sometimes result to errors, thus it is recommended -to escape them when possible. In the example below we create a new issue which -contains spaces in its title. Observe how spaces are escaped using the `%20` -ASCII code. - -```bash -curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/42/issues?title=Hello%20Dude" -``` - -Use `%2F` for slashes (`/`). - -##### Pass arrays to API calls - -The GitLab API sometimes accepts arrays of strings or integers. For example, to -restrict the sign-up e-mail domains of a GitLab instance to `*.example.com` and -`example.net`, you would do something like this: - -```bash -curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --data "domain_whitelist[]=*.example.com" --data "domain_whitelist[]=example.net" https://gitlab.example.com/api/v4/application/settings -``` - -[cURL]: http://curl.haxx.se/ "cURL website" -[single spaces]: http://www.slate.com/articles/technology/technology/2011/01/space_invaders.html -[gfm]: http://docs.gitlab.com/ce/user/markdown.html#newlines "GitLab flavored markdown documentation" -[ce-1242]: https://gitlab.com/gitlab-org/gitlab-ce/issues/1242 -[doc-restart]: ../administration/restart_gitlab.md "GitLab restart documentation" diff --git a/doc/development/img/manual_build_docs.png b/doc/development/documentation/img/manual_build_docs.png Binary files differindex 615facabb5f..615facabb5f 100644 --- a/doc/development/img/manual_build_docs.png +++ b/doc/development/documentation/img/manual_build_docs.png diff --git a/doc/development/documentation/index.md b/doc/development/documentation/index.md new file mode 100644 index 00000000000..48e1685082a --- /dev/null +++ b/doc/development/documentation/index.md @@ -0,0 +1,558 @@ +--- +description: Learn how to contribute to GitLab Documentation. +--- + +# GitLab Documentation guidelines + + - **General Documentation**: written by the [developers responsible by creating features](#contributing-to-docs). Should be submitted in the same merge request containing code. Feature proposals (by GitLab contributors) should also be accompanied by its respective documentation. They can be later improved by PMs and Technical Writers. + - **[Technical Articles](#technical-articles)**: written by any [GitLab Team](https://about.gitlab.com/team/) member, GitLab contributors, or [Community Writers](https://about.gitlab.com/handbook/product/technical-writing/community-writers/). + - **Indexes per topic**: initially prepared by the Technical Writing Team, and kept up-to-date by developers and PMs in the same merge request containing code. They gather all resources for that topic in a single page (user and admin documentation, articles, and third-party docs). + +## Contributing to docs + +Whenever a feature is changed, updated, introduced, or deprecated, the merge +request introducing these changes must be accompanied by the documentation +(either updating existing ones or creating new ones). This is also valid when +changes are introduced to the UI. + +The one responsible for writing the first piece of documentation is the developer who +wrote the code. It's the job of the Product Manager to ensure all features are +shipped with its docs, whether is a small or big change. At the pace GitLab evolves, +this is the only way to keep the docs up-to-date. If you have any questions about it, +ask a Technical Writer. Otherwise, when your content is ready, assign one of +them to review it for you. + +We use the [monthly release blog post](https://about.gitlab.com/handbook/marketing/blog/release-posts/#monthly-releases) as a changelog checklist to ensure everything +is documented. + +Whenever you submit a merge request for the documentation, use the documentation MR description template. + +Please check the [documentation workflow](https://about.gitlab.com/handbook/product/technical-writing/workflow/) before getting started. + +## Documentation structure + +- Overview and use cases: what it is, why it is necessary, why one would use it +- Requirements: what do we need to get started +- Tutorial: how to set it up, how to use it + +Always link a new document from its topic-related index, otherwise, it will +not be included it in the documentation site search. + +_Note: to be extended._ + +### Feature overview and use cases + +Every major feature (regardless if present in GitLab Community or Enterprise editions) +should present, at the beginning of the document, two main sections: **overview** and +**use cases**. Every GitLab EE-only feature should also contain these sections. + +**Overview**: as the name suggests, the goal here is to provide an overview of the feature. +Describe what is it, what it does, why it is important/cool/nice-to-have, +what problem it solves, and what you can do with this feature that you couldn't +do before. + +**Use cases**: provide at least two, ideally three, use cases for every major feature. +You should answer this question: what can you do with this feature/change? Use cases +are examples of how this feature or change can be used in real life. + +Examples: +- CE and EE: [Issues](../user/project/issues/index.md#use-cases) +- CE and EE: [Merge Requests](../user/project/merge_requests/index.md#overview) +- EE-only: [Geo](https://docs.gitlab.com/ee/gitlab-geo/README.html#overview) +- EE-only: [Jenkins integration](https://docs.gitlab.com/ee/integration/jenkins.md#overview) + +Note that if you don't have anything to add between the doc title (`<h1>`) and +the header `## Overview`, you can omit the header, but keep the content of the +overview there. + +> **Overview** and **use cases** are required to **every** Enterprise Edition feature, +and for every **major** feature present in Community Edition. + +## Markdown and styles + +Currently GitLab docs use Redcarpet as [markdown](../user/markdown.md) engine, but there's an [open discussion](https://gitlab.com/gitlab-com/gitlab-docs/issues/50) for implementing Kramdown in the near future. + +All the docs follow the [documentation style guidelines](styleguide.md). + +## Documentation directory structure + +The documentation is structured based on the GitLab UI structure itself, +separated by [`user`](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/user), +[`administrator`](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/administration), and [`contributor`](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/development). + +In order to have a [solid site structure](https://searchengineland.com/seo-benefits-developing-solid-site-structure-277456) for our documentation, +all docs should be linked. Every new document should be cross-linked to its related documentation, and linked from its topic-related index, when existent. + +The directories `/workflow/`, `/gitlab-basics/`, `/university/`, and `/articles/` have +been deprecated and the majority their docs have been moved to their correct location +in small iterations. Please don't create new docs in these folders. + +### Location and naming documents + +The documentation hierarchy can be vastly improved by providing a better layout +and organization of directories. + +Having a structured document layout, we will be able to have meaningful URLs +like `docs.gitlab.com/user/project/merge_requests/index.html`. With this pattern, +you can immediately tell that you are navigating a user related documentation +and is about the project and its merge requests. + +Do not create summaries of similar types of content (e.g. an index of all articles, videos, etc.), +rather organize content by its subject (e.g. everything related to CI goes together) +and cross-link between any related content. + +The table below shows what kind of documentation goes where. + +| Directory | What belongs here | +| --------- | -------------- | +| `doc/user/` | User related documentation. Anything that can be done within the GitLab UI goes here including `/admin`. | +| `doc/administration/` | Documentation that requires the user to have access to the server where GitLab is installed. The admin settings that can be accessed via GitLab's interface go under `doc/user/admin_area/`. | +| `doc/api/` | API related documentation. | +| `doc/development/` | Documentation related to the development of GitLab. Any styleguides should go here. | +| `doc/legal/` | Legal documents about contributing to GitLab. | +| `doc/install/`| Probably the most visited directory, since `installation.md` is there. Ideally this should go under `doc/administration/`, but it's best to leave it as-is in order to avoid confusion (still debated though). | +| `doc/update/` | Same with `doc/install/`. Should be under `administration/`, but this is a well known location, better leave as-is, at least for now. | +| `doc/topics/` | Indexes per Topic (`doc/topics/topic-name/index.md`): all resources for that topic (user and admin documentation, articles, and third-party docs) | + +--- + +**General rules:** + +1. The correct naming and location of a new document, is a combination + of the relative URL of the document in question and the GitLab Map design + that is used for UX purposes ([source][graffle], [image][gitlab-map]). +1. When creating a new document and it has more than one word in its name, + make sure to use underscores instead of spaces or dashes (`-`). For example, + a proper naming would be `import_projects_from_github.md`. The same rule + applies to images. +1. Start a new directory with an `index.md` file. +1. There are four main directories, `user`, `administration`, `api` and `development`. +1. The `doc/user/` directory has five main subdirectories: `project/`, `group/`, + `profile/`, `dashboard/` and `admin_area/`. + 1. `doc/user/project/` should contain all project related documentation. + 1. `doc/user/group/` should contain all group related documentation. + 1. `doc/user/profile/` should contain all profile related documentation. + Every page you would navigate under `/profile` should have its own document, + i.e. `account.md`, `applications.md`, `emails.md`, etc. + 1. `doc/user/dashboard/` should contain all dashboard related documentation. + 1. `doc/user/admin_area/` should contain all admin related documentation + describing what can be achieved by accessing GitLab's admin interface + (_not to be confused with `doc/administration` where server access is + required_). + 1. Every category under `/admin/application_settings` should have its + own document located at `doc/user/admin_area/settings/`. For example, + the **Visibility and Access Controls** category should have a document + located at `doc/user/admin_area/settings/visibility_and_access_controls.md`. +1. The `doc/topics/` directory holds topic-related technical content. Create + `doc/topics/topic-name/subtopic-name/index.md` when subtopics become necessary. + General user- and admin- related documentation, should be placed accordingly. + +If you are unsure where a document should live, you can ping `@axil` or `@marcia` in your +merge request. + +### Changing document location + +Changing a document's location is not to be taken lightly. Remember that the +documentation is available to all installations under `help/` and not only to +GitLab.com or http://docs.gitlab.com. Make sure this is discussed with the +Documentation team beforehand. + +If you indeed need to change a document's location, do NOT remove the old +document, but rather replace all of its contents with a new line: + +``` +This document was moved to [another location](path/to/new_doc.md). +``` + +where `path/to/new_doc.md` is the relative path to the root directory `doc/`. + +--- + +For example, if you were to move `doc/workflow/lfs/lfs_administration.md` to +`doc/administration/lfs.md`, then the steps would be: + +1. Copy `doc/workflow/lfs/lfs_administration.md` to `doc/administration/lfs.md` +1. Replace the contents of `doc/workflow/lfs/lfs_administration.md` with: + + ``` + This document was moved to [another location](../../administration/lfs.md). + ``` + +1. Find and replace any occurrences of the old location with the new one. + A quick way to find them is to use `git grep`. First go to the root directory + where you cloned the `gitlab-ce` repository and then do: + + ``` + git grep -n "workflow/lfs/lfs_administration" + git grep -n "lfs/lfs_administration" + ``` + +NOTE: **Note:** +If the document being moved has any Disqus comments on it, there are extra steps +to follow documented just [below](#redirections-for-pages-with-disqus-comments). + +Things to note: + +- Since we also use inline documentation, except for the documentation itself, + the document might also be referenced in the views of GitLab (`app/`) which will + render when visiting `/help`, and sometimes in the testing suite (`spec/`). +- The above `git grep` command will search recursively in the directory you run + it in for `workflow/lfs/lfs_administration` and `lfs/lfs_administration` + and will print the file and the line where this file is mentioned. + You may ask why the two greps. Since we use relative paths to link to + documentation, sometimes it might be useful to search a path deeper. +- The `*.md` extension is not used when a document is linked to GitLab's + built-in help page, that's why we omit it in `git grep`. +- Use the checklist on the documentation MR description template. + +#### Alternative redirection method + +Alternatively to the method described above, you can simply replace the content +of the old file with a frontmatter containing a redirect link: + +```yaml +--- +redirect_to: '../path/to/file/README.md' +--- +``` + +It supports both full and relative URLs, e.g. `https://docs.gitlab.com/ee/path/to/file.html`, `../path/to/file.html`, `path/to/file.md`. Note that any `*.md` paths will be compiled to `*.html`. + +### Redirections for pages with Disqus comments + +If the documentation page being relocated already has any Disqus comments, +we need to preserve the Disqus thread. + +Disqus uses an identifier per page, and for docs.gitlab.com, the page identifier +is configured to be the page URL. Therefore, when we change the document location, +we need to preserve the old URL as the same Disqus identifier. + +To do that, add to the frontmatter the variable `redirect_from`, +using the old URL as value. For example, let's say I moved the document +available under `https://docs.gitlab.com/my-old-location/README.html` to a new location, +`https://docs.gitlab.com/my-new-location/index.html`. + +Into the **new document** frontmatter add the following: + +```yaml +--- +redirect_from: 'https://docs.gitlab.com/my-old-location/README.html' +--- +``` + +Note: it is necessary to include the file name in the `redirect_from` URL, +even if it's `index.html` or `README.html`. + +## Testing + +We treat documentation as code, thus have implemented some testing. +Currently, the following tests are in place: + +1. `docs lint`: Check that all internal (relative) links work correctly and + that all cURL examples in API docs use the full switches. It's recommended + to [check locally](#previewing-locally) before pushing to GitLab by executing the command + `bundle exec nanoc check internal_links` on your local + [`gitlab-docs`](https://gitlab.com/gitlab-com/gitlab-docs) directory. +1. [`ee_compat_check`](../automatic_ce_ee_merge.md#avoiding-ce-gt-ee-merge-conflicts-beforehand) (runs on CE only): + When you submit a merge request to GitLab Community Edition (CE), + there is this additional job that runs against Enterprise Edition (EE) + and checks if your changes can apply cleanly to the EE codebase. + If that job fails, read the instructions in the job log for what to do next. + As CE is merged into EE once a day, it's important to avoid merge conflicts. + Submitting an EE-equivalent merge request cherry-picking all commits from CE to EE is + essential to avoid them. + +## Branch naming + +If your contribution contains **only** documentation changes, you can speed up +the CI process by following some branch naming conventions. You have three +choices: + +| Branch name | Valid example | +| ----------- | ------------- | +| Starting with `docs/` | `docs/update-api-issues` | +| Starting with `docs-` | `docs-update-api-issues` | +| Ending in `-docs` | `123-update-api-issues-docs` | + +If your branch name matches any of the above, it will run only the docs +tests. If it doesn't, the whole test suite will run (including docs). + +## Merge requests for GitLab documentation + +Before getting started, make sure you read the introductory section +"[contributing to docs](#contributing-to-docs)" above and the +[tech writing workflow](https://about.gitlab.com/handbook/product/technical-writing/workflow/) +for GitLab Team members. + +- Use the current [merge request description template](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/.gitlab/merge_request_templates/Documentation.md) +- Use the correct [branch name](#branch-naming) +- Label the MR `Documentation` +- Assign the correct milestone (see note below) + + +NOTE: **Note:** +If the release version you want to add the documentation to has already been +frozen or released, use the label `Pick into X.Y` to get it merged into +the correct release. Avoid picking into a past release as much as you can, as +it increases the work of the release managers. + +### Cherry-picking from CE to EE + +As we have the `master` branch of CE merged into EE once a day, it's common to +run into merge conflicts. To avoid them, we [test for merge conflicts against EE](#testing) +with the `ee-compat-check` job, and use the following method of creating equivalent +branches for CE and EE. + +Follow this [method for cherry-picking from CE to EE](../automatic_ce_ee_merge.md#cherry-picking-from-ce-to-ee), with a few adjustments: + +- Create the [CE branch](#branch-naming) starting with `docs-`, + e.g.: `git checkout -b docs-example` +- Create the EE-equivalent branch ending with `-ee`, e.g., + `git checkout -b docs-example-ee` +- Once all the jobs are passing in CE and EE, and you've addressed the +feedback from your own team, assign the CE MR to a technical writer for review +- When both MRs are ready, the EE merge request will be merged first, and the +CE-equivalent will be merged next. +- Note that the review will occur only in the CE MR, as the EE MR +contains the same commits as the CE MR. +- If you have a few more changes that apply to the EE-version only, you can submit +a couple more commits to the EE branch, but ask the reviewer to review the EE merge request +additionally to the CE MR. If there are many EE-only changes though, start a new MR +to EE only. + +## Previewing the changes live + +To preview your changes to documentation locally, please follow +this [development guide](https://gitlab.com/gitlab-com/gitlab-docs/blob/master/README.md#development). + +If you want to preview the doc changes of your merge request live, you can use +the manual `review-docs-deploy` job in your merge request. You will need at +least Maintainer permissions to be able to run it and is currently enabled for the +following projects: + +- https://gitlab.com/gitlab-org/gitlab-ce +- https://gitlab.com/gitlab-org/gitlab-ee + +NOTE: **Note:** +You will need to push a branch to those repositories, it doesn't work for forks. + +TIP: **Tip:** +If your branch contains only documentation changes, you can use +[special branch names](#branch-naming) to avoid long running pipelines. + +In the mini pipeline graph, you should see an `>>` icon. Clicking on it will +reveal the `review-docs-deploy` job. Hit the play button for the job to start. + +![Manual trigger a docs build](img/manual_build_docs.png) + +This job will: + +1. Create a new branch in the [gitlab-docs](https://gitlab.com/gitlab-com/gitlab-docs) + project named after the scheme: `preview-<branch-slug>` +1. Trigger a cross project pipeline and build the docs site with your changes + +After a few minutes, the Review App will be deployed and you will be able to +preview the changes. The docs URL can be found in two places: + +- In the merge request widget +- In the output of the `review-docs-deploy` job, which also includes the + triggered pipeline so that you can investigate whether something went wrong + +In case the Review App URL returns 404, follow these steps to debug: + +1. **Did you follow the URL from the merge request widget?** If yes, then check if + the link is the same as the one in the job output. It can happen that if the + branch name slug is longer than 35 characters, it is automatically + truncated. That means that the merge request widget will not show the proper + URL due to a limitation of how `environment: url` works, but you can find the + real URL from the output of the `review-docs-deploy` job. +1. **Did you follow the URL from the job output?** If yes, then it means that + either the site is not yet deployed or something went wrong with the remote + pipeline. Give it a few minutes and it should appear online, otherwise you + can check the status of the remote pipeline from the link in the job output. + If the pipeline failed or got stuck, drop a line in the `#docs` chat channel. + +TIP: **Tip:** +Someone that has no merge rights to the CE/EE projects (think of forks from +contributors) will not be able to run the manual job. In that case, you can +ask someone from the GitLab team who has the permissions to do that for you. + +NOTE: **Note:** +Make sure that you always delete the branch of the merge request you were +working on. If you don't, the remote docs branch won't be removed either, +and the server where the Review Apps are hosted will eventually be out of +disk space. + +### Technical aspects + +If you want to know the hot details, here's what's really happening: + +1. You manually run the `review-docs-deploy` job in a CE/EE merge request. +1. The job runs the [`scripts/trigger-build-docs`](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/scripts/trigger-build-docs) + script with the `deploy` flag, which in turn: + 1. Takes your branch name and applies the following: + - The slug of the branch name is used to avoid special characters since + ultimately this will be used by NGINX. + - The `preview-` prefix is added to avoid conflicts if there's a remote branch + with the same name that you created in the merge request. + - The final branch name is truncated to 42 characters to avoid filesystem + limitations with long branch names (> 63 chars). + 1. The remote branch is then created if it doesn't exist (meaning you can + re-run the manual job as many times as you want and this step will be skipped). + 1. A new cross-project pipeline is triggered in the docs project. + 1. The preview URL is shown both at the job output and in the merge request + widget. You also get the link to the remote pipeline. +1. In the docs project, the pipeline is created and it + [skips the test jobs](https://gitlab.com/gitlab-com/gitlab-docs/blob/8d5d5c750c602a835614b02f9db42ead1c4b2f5e/.gitlab-ci.yml#L50-55) + to lower the build time. +1. Once the docs site is built, the HTML files are uploaded as artifacts. +1. A specific Runner tied only to the docs project, runs the Review App job + that downloads the artifacts and uses `rsync` to transfer the files over + to a location where NGINX serves them. + +The following GitLab features are used among others: + +- [Manual actions](../../ci/yaml/README.md#manual-actions) +- [Multi project pipelines](https://docs.gitlab.com/ee/ci/multi_project_pipeline_graphs.html) +- [Review Apps](../../ci/review_apps/index.md) +- [Artifacts](../../ci/yaml/README.md#artifacts) +- [Specific Runner](../../ci/runners/README.md#locking-a-specific-runner-from-being-enabled-for-other-projects) + +## GitLab `/help` + +Every GitLab instance includes the documentation, which is available from `/help` +(`http://my-instance.com/help`), e.g., <https://gitlab.com/help>. + +When you're building a new feature, you may need to link the documentation +from GitLab, the application. This is normally done in files inside the +`app/views/` directory with the help of the `help_page_path` helper method. + +In its simplest form, the HAML code to generate a link to the `/help` page is: + +```haml += link_to 'Help page', help_page_path('user/permissions') +``` + +The `help_page_path` contains the path to the document you want to link to with +the following conventions: + +- it is relative to the `doc/` directory in the GitLab repository +- the `.md` extension must be omitted +- it must not end with a slash (`/`) + +Below are some special cases where should be used depending on the context. +You can combine one or more of the following: + +1. **Linking to an anchor link.** Use `anchor` as part of the `help_page_path` + method: + + ```haml + = link_to 'Help page', help_page_path('user/permissions', anchor: 'anchor-link') + ``` + +1. **Opening links in a new tab.** This should be the default behavior: + + ```haml + = link_to 'Help page', help_page_path('user/permissions'), target: '_blank' + ``` + +1. **Linking to a circle icon.** Usually used in settings where a long + description cannot be used, like near checkboxes. You can basically use + any font awesome icon, but prefer the `question-circle`: + + ```haml + = link_to icon('question-circle'), help_page_path('user/permissions') + ``` + +1. **Using a button link.** Useful in places where text would be out of context + with the rest of the page layout: + + ```haml + = link_to 'Help page', help_page_path('user/permissions'), class: 'btn btn-info' + ``` + +1. **Using links inline of some text.** + + ```haml + Description to #{link_to 'Help page', help_page_path('user/permissions')}. + ``` + +1. **Adding a period at the end of the sentence.** Useful when you don't want + the period to be part of the link: + + ```haml + = succeed '.' do + Learn more in the + = link_to 'Help page', help_page_path('user/permissions') + ``` + +## General Documentation vs Technical Articles + +### General documentation + +General documentation is categorized by _User_, _Admin_, and _Contributor_, and describe what that feature is, what it does, and its available settings. + +### Technical Articles + +Technical articles replace technical content that once lived in the [GitLab Blog](https://about.gitlab.com/blog/), where they got out-of-date and weren't easily found. + +They are topic-related documentation, written with an user-friendly approach and language, aiming to provide the community with guidance on specific processes to achieve certain objectives. + +A technical article guides users and/or admins to achieve certain objectives (within guides and tutorials), or provide an overview of that particular topic or feature (within technical overviews). It can also describe the use, implementation, or integration of third-party tools with GitLab. + +They should be placed in a new directory named `/article-title/index.md` under a topic-related folder, and their images should be placed in `/article-title/img/`. For example, a new article on GitLab Pages should be placed in `doc/user/project/pages/article-title/` and a new article on GitLab CI/CD should be placed in `doc/ci/examples/article-title/`. + +#### Types of Technical Articles + +- **User guides**: technical content to guide regular users from point A to point B +- **Admin guides**: technical content to guide administrators of GitLab instances from point A to point B +- **Technical Overviews**: technical content describing features, solutions, and third-party integrations +- **Tutorials**: technical content provided step-by-step on how to do things, or how to reach very specific objectives + +#### Understanding guides, tutorials, and technical overviews + +Suppose there's a process to go from point A to point B in 5 steps: `(A) 1 > 2 > 3 > 4 > 5 (B)`. + +A **guide** can be understood as a description of certain processes to achieve a particular objective. A guide brings you from A to B describing the characteristics of that process, but not necessarily going over each step. It can mention, for example, steps 2 and 3, but does not necessarily explain how to accomplish them. + +- Live example: "[Static sites and GitLab Pages domains (Part 1)](../user/project/pages/getting_started_part_one.md) to [Creating and Tweaking GitLab CI/CD for GitLab Pages (Part 4)](../../user/project/pages/getting_started_part_four.md)" + +A **tutorial** requires a clear **step-by-step** guidance to achieve a singular objective. It brings you from A to B, describing precisely all the necessary steps involved in that process, showing each of the 5 steps to go from A to B. +It does not only describes steps 2 and 3, but also shows you how to accomplish them. + +- Live example (on the blog): [Hosting on GitLab.com with GitLab Pages](https://about.gitlab.com/2016/04/07/gitlab-pages-setup/) + +A **technical overview** is a description of what a certain feature is, and what it does, but does not walk +through the process of how to use it systematically. + +- Live example (on the blog): [GitLab Workflow, an overview](https://about.gitlab.com/2016/10/25/gitlab-workflow-an-overview/) + +#### Special format + +Every **Technical Article** contains a frontmatter at the beginning of the doc +with the following information: + +- **Type of article** (user guide, admin guide, technical overview, tutorial) +- **Knowledge level** expected from the reader to be able to follow through (beginner, intermediate, advanced) +- **Author's name** and **GitLab.com handle** +- **Publication date** (ISO format YYYY-MM-DD) + +For example: + + +```yaml +--- +author: John Doe +author_gitlab: johnDoe +level: beginner +article_type: user guide +date: 2017-02-01 +--- +``` + +#### Technical Articles - Writing Method + +Use the [writing method](https://about.gitlab.com/handbook/product/technical-writing/#writing-method) defined by the Technical Writing team. + +[gitlab-map]: https://gitlab.com/gitlab-org/gitlab-design/raw/master/production/resources/gitlab-map.png +[graffle]: https://gitlab.com/gitlab-org/gitlab-design/blob/d8d39f4a87b90fb9ae89ca12dc565347b4900d5e/production/resources/gitlab-map.graffle diff --git a/doc/development/documentation/styleguide.md b/doc/development/documentation/styleguide.md new file mode 100644 index 00000000000..e7ffba635c9 --- /dev/null +++ b/doc/development/documentation/styleguide.md @@ -0,0 +1,499 @@ +--- +description: 'Writing styles, markup, formatting, and reusing regular expressions throughout the GitLab Documentation.' +--- + +# Documentation style guidelines + +The documentation style guide defines the markup structure used in +GitLab documentation. Check the +[documentation guidelines](index.md) for general development instructions. + +Check the GitLab handbook for the [writing styles guidelines](https://about.gitlab.com/handbook/communication/#writing-style-guidelines). + +## Text + +- Split up long lines (wrap text), this makes it much easier to review and edit. Only + double line breaks are shown as a full line break in [GitLab markdown][gfm]. + 80-100 characters is a good line length +- Make sure that the documentation is added in the correct + [directory](index.md#documentation-directory-structure) and that + there's a link to it somewhere useful +- Do not duplicate information +- Be brief and clear +- Unless there's a logical reason not to, add documents in alphabetical order +- Write in US English +- Use [single spaces][] instead of double spaces +- Jump a line between different markups (e.g., after every paragraph, header, list, etc) +- Capitalize "G" and "L" in GitLab +- Use sentence case for titles, headings, labels, menu items, and buttons. +- Use title case when referring to [features](https://about.gitlab.com/features/) or [products](https://about.gitlab.com/pricing/), and methods. Note that some features are also objects (e.g. "Merge Requests" and "merge requests"). E.g.: GitLab Runner, Geo, Issue Boards, Git, Prometheus, Continuous Integration. + +## Formatting + +- Use double asterisks (`**`) to mark a word or text in bold (`**bold**`) +- Use undescore (`_`) for text in italics (`_italic_`) +- Jump a line between different markups, for example: + + ```md + ## Header + + Paragraph. + + - List item + - List item + ``` + +### Punctuation + +For punctuation rules, please refer to the [GitLab UX guide](https://design.gitlab.com/content/punctuation/). + +### Ordered and unordered lists + +- Use dashes (`-`) for unordered lists instead of asterisks (`*`) +- Use the number one (`1`) for ordered lists +- For punctuation in bullet lists, please refer to the [GitLab UX guide](https://design.gitlab.com/content/punctuation/) + +## Headings + +- Add **only one H1** in each document, by adding `#` at the beginning of + it (when using markdown). The `h1` will be the document `<title>`. +- For subheadings, use `##`, `###` and so on +- Avoid putting numbers in headings. Numbers shift, hence documentation anchor + links shift too, which eventually leads to dead links. If you think it is + compelling to add numbers in headings, make sure to at least discuss it with + someone in the Merge Request +- [Avoid using symbols and special chars](https://gitlab.com/gitlab-com/gitlab-docs/issues/84) + in headers. Whenever possible, they should be plain and short text. +- Avoid adding things that show ephemeral statuses. For example, if a feature is + considered beta or experimental, put this info in a note, not in the heading. +- When introducing a new document, be careful for the headings to be + grammatically and syntactically correct. Mention one or all + of the following GitLab members for a review: `@axil` or `@marcia`. + This is to ensure that no document with wrong heading is going + live without an audit, thus preventing dead links and redirection issues when + corrected +- Leave exactly one newline after a heading + +## Links + +- Use the regular inline link markdown markup `[Text](https://example.com)`. + It's easier to read, review, and maintain. +- If there's a link that repeats several times through the same document, + you can use `[Text][identifier]` and at the bottom of the section or the + document add: `[identifier]: https://example.com`, in which case, we do + encourage you to also add an alternative text: `[identifier]: https://example.com "Alternative text"` that appears when hovering your mouse on a link. +- To link to internal documentation, use relative links, not full URLs. Use `../` to + navigate tp high-level directories, and always add the file name `file.md` at the + end of the link with the `.md` extension, not `.html`. + Example: instead of `[text](../../merge_requests/)`, use + `[text](../../merge_requests/index.md)` or, `[text](../../ci/README.md)`, or, + for anchor links, `[text](../../ci/README.md#examples)`. + Using the markdown extension is necessary for the [`/help`](index.md#gitlab-help) + section of GitLab. +- To link from CE to EE-only documentation, use the EE-only doc full URL. +- Use [meaningful anchor texts](https://www.futurehosting.com/blog/links-should-have-meaningful-anchor-text-heres-why/). + E.g., instead of writing something like `Read more about GitLab Issue Boards [here](LINK)`, + write `Read more about [GitLab Issue Boards](LINK)`. + +## Images + +- Place images in a separate directory named `img/` in the same directory where + the `.md` document that you're working on is located. Always prepend their + names with the name of the document that they will be included in. For + example, if there is a document called `twitter.md`, then a valid image name + could be `twitter_login_screen.png`. [**Exception**: images for + [articles](index.md#technical-articles) should be + put in a directory called `img` underneath `/articles/article_title/img/`, therefore, + there's no need to prepend the document name to their filenames.] +- Images should have a specific, non-generic name that will differentiate them. +- Keep all file names in lower case. +- Consider using PNG images instead of JPEG. +- Compress all images with <https://tinypng.com/> or similar tool. +- Compress gifs with <https://ezgif.com/optimize> or similar tool. +- Images should be used (only when necessary) to _illustrate_ the description +of a process, not to _replace_ it. + +Inside the document: + +- The Markdown way of using an image inside a document is: + `![Proper description what the image is about](img/document_image_title.png)` +- Always use a proper description for what the image is about. That way, when a + browser fails to show the image, this text will be used as an alternative + description +- If there are consecutive images with little text between them, always add + three dashes (`---`) between the image and the text to create a horizontal + line for better clarity +- If a heading is placed right after an image, always add three dashes (`---`) + between the image and the heading + +## Alert boxes + +Whenever you want to call the attention to a particular sentence, +use the following markup for highlighting. + +_Note that the alert boxes only work for one paragraph only. Multiple paragraphs, +lists, headers, etc will not render correctly._ + +### Note + +```md +NOTE: **Note:** +This is something to note. +``` + +How it renders in docs.gitlab.com: + +NOTE: **Note:** +This is something to note. + +### Tip + +```md +TIP: **Tip:** +This is a tip. +``` + +How it renders in docs.gitlab.com: + +TIP: **Tip:** +This is a tip. + +### Caution + +```md +CAUTION: **Caution:** +This is something to be cautious about. +``` + +How it renders in docs.gitlab.com: + +CAUTION: **Caution:** +This is something to be cautious about. + +### Danger + +```md +DANGER: **Danger:** +This is a breaking change, a bug, or something very important to note. +``` + +How it renders in docs.gitlab.com: + +DANGER: **Danger:** +This is a breaking change, a bug, or something very important to note. + +## Blockquotes + +For highlighting a text within a blue blockquote, use this format: + +```md +> This is a blockquote. +``` + +which renders in docs.gitlab.com to: + +> This is a blockquote. + +If the text spans across multiple lines it's OK to split the line. + +## Specific sections and terms + +To mention and/or reference specific terms in GitLab, please follow the styles +below. + +### GitLab versions and tiers + +- Every piece of documentation that comes with a new feature should declare the + GitLab version that feature got introduced. Right below the heading add a + note: + + ```md + > Introduced in GitLab 8.3. + ``` + +- Whenever possible, every feature should have a link to the MR, issue, or epic that introduced it. + The above note would be then transformed to: + + ```md + > [Introduced][ce-1242] in GitLab 8.3. + ``` + + , where the [link identifier](#links) is named after the repository (CE) and + the MR number. + +- If the feature is only available in GitLab Enterprise Edition, don't forget to mention + the [paid tier](https://about.gitlab.com/handbook/marketing/product-marketing/#tiers) + the feature is available in: + + ```md + > [Introduced][ee-1234] in [GitLab Starter](https://about.gitlab.com/pricing/) 8.3. + ``` + +### Product badges + +When a feature is available in EE-only tiers, add the corresponding tier according to the +feature availability: + +- For GitLab Starter and GitLab.com Bronze: `**[STARTER]**` +- For GitLab Premium and GitLab.com Silver: `**[PREMIUM]**` +- For GitLab Ultimate and GitLab.com Gold: `**[ULTIMATE]**` +- For GitLab Core and GitLab.com Free: `**[CORE]**` + +To exclude GitLab.com tiers (when the feature is not available in GitLab.com), add the +keyword "only": + +- For GitLab Starter: `**[STARTER ONLY]**` +- For GitLab Premium: `**[PREMIUM ONLY]**` +- For GitLab Ultimate: `**[ULTIMATE ONLY]**` +- For GitLab Core: `**[CORE ONLY]**` + +The tier should be ideally added to headers, so that the full badge will be displayed. +But it can be also mentioned from paragraphs, list items, and table cells. For these cases, +the tier mention will be represented by an orange question mark. +E.g., `**[STARTER]**` renders **[STARTER]**, `**[STARTER ONLY]**` renders **[STARTER ONLY]**. + +The absence of tiers' mentions mean that the feature is available in GitLab Core, +GitLab.com Free, and higher tiers. + +#### How it works + +Introduced by [!244](https://gitlab.com/gitlab-com/gitlab-docs/merge_requests/244), +the special markup `**[STARTER]**` will generate a `span` element to trigger the +badges and tooltips (`<span class="badge-trigger starter">`). When the keyword +"only" is added, the corresponding GitLab.com badge will not be displayed. + +### GitLab Restart + +There are many cases that a restart/reconfigure of GitLab is required. To +avoid duplication, link to the special document that can be found in +[`doc/administration/restart_gitlab.md`][doc-restart]. Usually the text will +read like: + + ``` + Save the file and [reconfigure GitLab](../../administration/restart_gitlab.md) + for the changes to take effect. + ``` + +If the document you are editing resides in a place other than the GitLab CE/EE +`doc/` directory, instead of the relative link, use the full path: +`http://docs.gitlab.com/ce/administration/restart_gitlab.html`. +Replace `reconfigure` with `restart` where appropriate. + +### Installation guide + +**Ruby:** +In [step 2 of the installation guide](../../install/installation.md#2-ruby), +we install Ruby from source. Whenever there is a new version that needs to +be updated, remember to change it throughout the codeblock and also replace +the sha256sum (it can be found in the [downloads page][ruby-dl] of the Ruby +website). + +[ruby-dl]: https://www.ruby-lang.org/en/downloads/ "Ruby download website" + +### Configuration documentation for source and Omnibus installations + +GitLab currently officially supports two installation methods: installations +from source and Omnibus packages installations. + +Whenever there is a setting that is configurable for both installation methods, +prefer to document it in the CE docs to avoid duplication. + +Configuration settings include: + +- settings that touch configuration files in `config/` +- NGINX settings and settings in `lib/support/` in general + +When there is a list of steps to perform, usually that entails editing the +configuration file and reconfiguring/restarting GitLab. In such case, follow +the style below as a guide: + +```md +**For Omnibus installations** + +1. Edit `/etc/gitlab/gitlab.rb`: + + ```ruby + external_url "https://gitlab.example.com" + ``` + +1. Save the file and [reconfigure] GitLab for the changes to take effect. + +--- + +**For installations from source** + +1. Edit `config/gitlab.yml`: + + ```yaml + gitlab: + host: "gitlab.example.com" + ``` + +1. Save the file and [restart] GitLab for the changes to take effect. + + +[reconfigure]: path/to/administration/restart_gitlab.md#omnibus-gitlab-reconfigure +[restart]: path/to/administration/restart_gitlab.md#installations-from-source +``` + +In this case: + +- before each step list the installation method is declared in bold +- three dashes (`---`) are used to create a horizontal line and separate the + two methods +- the code blocks are indented one or more spaces under the list item to render + correctly +- different highlighting languages are used for each config in the code block +- the [references](#references) guide is used for reconfigure/restart + +### Fake tokens + +There may be times where a token is needed to demonstrate an API call using +cURL or a variable used in CI. It is strongly advised not to use real +tokens in documentation even if the probability of a token being exploited is +low. + +You can use the following fake tokens as examples. + +| **Token type** | **Token value** | +| --------------------- | --------------------------------- | +| Private user token | `9koXpg98eAheJpvBs5tK` | +| Personal access token | `n671WNGecHugsdEDPsyo` | +| Application ID | `2fcb195768c39e9a94cec2c2e32c59c0aad7a3365c10892e8116b5d83d4096b6` | +| Application secret | `04f294d1eaca42b8692017b426d53bbc8fe75f827734f0260710b83a556082df` | +| Secret CI variable | `Li8j-mLUVA3eZYjPfd_H` | +| Specific Runner token | `yrnZW46BrtBFqM7xDzE7dddd` | +| Shared Runner token | `6Vk7ZsosqQyfreAxXTZr` | +| Trigger token | `be20d8dcc028677c931e04f3871a9b` | +| Webhook secret token | `6XhDroRcYPM5by_h-HLY` | +| Health check token | `Tu7BgjR9qeZTEyRzGG2P` | +| Request profile token | `7VgpS4Ax5utVD2esNstz` | + +### API + +Here is a list of must-have items. Use them in the exact order that appears +on this document. Further explanation is given below. + +- Every method must have the REST API request. For example: + + ``` + GET /projects/:id/repository/branches + ``` + +- Every method must have a detailed + [description of the parameters](#method-description). +- Every method must have a cURL example. +- Every method must have a response body (in JSON format). + +#### Method description + +Use the following table headers to describe the methods. Attributes should +always be in code blocks using backticks (``` ` ```). + +``` +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +``` + +Rendered example: + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `user` | string | yes | The GitLab username | + +#### cURL commands + +- Use `https://gitlab.example.com/api/v4/` as an endpoint. +- Wherever needed use this personal access token: `9koXpg98eAheJpvBs5tK`. +- Always put the request first. `GET` is the default so you don't have to + include it. +- Use double quotes to the URL when it includes additional parameters. +- Prefer to use examples using the personal access token and don't pass data of + username and password. + +| Methods | Description | +| ------- | ----------- | +| `-H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK"` | Use this method as is, whenever authentication needed | +| `-X POST` | Use this method when creating new objects | +| `-X PUT` | Use this method when updating existing objects | +| `-X DELETE` | Use this method when removing existing objects | + +#### cURL Examples + +Below is a set of [cURL][] examples that you can use in the API documentation. + +##### Simple cURL command + +Get the details of a group: + +```bash +curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/gitlab-org +``` + +##### cURL example with parameters passed in the URL + +Create a new project under the authenticated user's namespace: + +```bash +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects?name=foo" +``` + +##### Post data using cURL's --data + +Instead of using `-X POST` and appending the parameters to the URI, you can use +cURL's `--data` option. The example below will create a new project `foo` under +the authenticated user's namespace. + +```bash +curl --data "name=foo" --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects" +``` + +##### Post data using JSON content + +> **Note:** In this example we create a new group. Watch carefully the single +and double quotes. + +```bash +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --header "Content-Type: application/json" --data '{"path": "my-group", "name": "My group"}' https://gitlab.example.com/api/v4/groups +``` + +##### Post data using form-data + +Instead of using JSON or urlencode you can use multipart/form-data which +properly handles data encoding: + +```bash +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --form "title=ssh-key" --form "key=ssh-rsa AAAAB3NzaC1yc2EA..." https://gitlab.example.com/api/v4/users/25/keys +``` + +The above example is run by and administrator and will add an SSH public key +titled ssh-key to user's account which has an id of 25. + +##### Escape special characters + +Spaces or slashes (`/`) may sometimes result to errors, thus it is recommended +to escape them when possible. In the example below we create a new issue which +contains spaces in its title. Observe how spaces are escaped using the `%20` +ASCII code. + +```bash +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/42/issues?title=Hello%20Dude" +``` + +Use `%2F` for slashes (`/`). + +##### Pass arrays to API calls + +The GitLab API sometimes accepts arrays of strings or integers. For example, to +restrict the sign-up e-mail domains of a GitLab instance to `*.example.com` and +`example.net`, you would do something like this: + +```bash +curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --data "domain_whitelist[]=*.example.com" --data "domain_whitelist[]=example.net" https://gitlab.example.com/api/v4/application/settings +``` + +[cURL]: http://curl.haxx.se/ "cURL website" +[single spaces]: http://www.slate.com/articles/technology/technology/2011/01/space_invaders.html +[gfm]: http://docs.gitlab.com/ce/user/markdown.html#newlines "GitLab flavored markdown documentation" +[ce-1242]: https://gitlab.com/gitlab-org/gitlab-ce/issues/1242 +[doc-restart]: ../../administration/restart_gitlab.md "GitLab restart documentation" diff --git a/doc/development/fe_guide/style_guide_scss.md b/doc/development/fe_guide/style_guide_scss.md index 655d94793dd..48eb6d0a7d6 100644 --- a/doc/development/fe_guide/style_guide_scss.md +++ b/doc/development/fe_guide/style_guide_scss.md @@ -216,12 +216,12 @@ If you want a line or set of lines to be ignored by the linter, you can use `// scss-lint:disable RuleName` ([more info][disabling-linters]): ```scss -// This lint rule is disabled because the class name comes from a gem. -// scss-lint:disable SelectorFormat -.ui_indigo { - background-color: #333; +// This lint rule is disabled because it is supported only in Chrome/Safari +// scss-lint:disable PropertySpelling +body { + text-decoration-skip: ink; } -// scss-lint:enable SelectorFormat +// scss-lint:enable PropertySpelling ``` Make sure a comment is added on the line above the `disable` rule, otherwise the diff --git a/doc/development/i18n/externalization.md b/doc/development/i18n/externalization.md index 0edcb23c7c5..4ba9958e2c6 100644 --- a/doc/development/i18n/externalization.md +++ b/doc/development/i18n/externalization.md @@ -233,7 +233,7 @@ This makes use of [`Intl.DateTimeFormat`]. Please never split a sentence as that would assume the sentence grammar and structure is the same in all languages. -For instance, the following +For instance, the following: ```js {{ s__("mrWidget|Set by") }} @@ -247,6 +247,27 @@ should be externalized as follows: {{ sprintf(s__("mrWidget|Set by %{author} to be merged automatically when the pipeline succeeds"), { author: author.name }) }} ``` +#### Avoid splitting sentences when adding links + +This also applies when using links in between translated sentences, otherwise these texts are not translatable in certain languages. + +Instead of: + +```haml +- zones_link = link_to(s_('ClusterIntegration|zones'), 'https://cloud.google.com/compute/docs/regions-zones/regions-zones', target: '_blank', rel: 'noopener noreferrer') += s_('ClusterIntegration|Learn more about %{zones_link}').html_safe % { zones_link: zones_link } +``` + +Set the link starting and ending HTML fragments as variables like so: + +```haml +- zones_link_url = 'https://cloud.google.com/compute/docs/regions-zones/regions-zones' +- zones_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: zones_link_url } += s_('ClusterIntegration|Learn more about %{zones_link_start}zones%{zones_link_end}').html_safe % { zones_link_start: zones_link_start, zones_link_end: '</a>'.html_safe } +``` + +The reasoning behind this is that in some languages words change depending on context. For example in Japanese は is added to the subject of a sentence and を to the object. This is impossible to translate correctly if we extract individual words from the sentence. + When in doubt, try to follow the best practices described in this [Mozilla Developer documentation][mdn]. diff --git a/doc/development/i18n/proofreader.md b/doc/development/i18n/proofreader.md index 5185d843ccb..9a677bf09b2 100644 --- a/doc/development/i18n/proofreader.md +++ b/doc/development/i18n/proofreader.md @@ -16,7 +16,7 @@ are very appreciative of the work done by translators and proofreaders! - Dutch - Esperanto - French - - Rémy Coutable - [GitLab](https://gitlab.com/rymai), [Crowdin](https://crowdin.com/profile/rymai) + - Davy Defaud - [GitLab](https://gitlab.com/DevDef), [Crowdin](https://crowdin.com/profile/DevDef) - German - Indonesian - Ahmad Naufal Mukhtar - [GitLab](https://gitlab.com/anaufalm), [Crowdin](https://crowdin.com/profile/anaufalm) diff --git a/doc/development/new_fe_guide/development/testing.md b/doc/development/new_fe_guide/development/testing.md index c359bd83ed1..e1e13474b75 100644 --- a/doc/development/new_fe_guide/development/testing.md +++ b/doc/development/new_fe_guide/development/testing.md @@ -1,3 +1,135 @@ -# Testing +# Overview of Frontend Testing -> TODO: Add content +## Types of tests in our codebase + +* **RSpec** + * **[Ruby unit tests](#ruby-unit-tests-spec-rb)** for models, controllers, helpers, etc. (`/spec/**/*.rb`) + * **[Full feature tests](#full-feature-tests-spec-features-rb)** (`/spec/features/**/*.rb`) +* **[Karma](#karma-tests-spec-javascripts-js)** (`/spec/javascripts/**/*.js`) +* ~~Spinach~~ — These have been removed from our codebase in May 2018. (`/features/`) + +## RSpec: Ruby unit tests `/spec/**/*.rb` + +These tests are meant to unit test the ruby models, controllers and helpers. + +### When do we write/update these tests? + +Whenever we create or modify any Ruby models, controllers or helpers we add/update corresponding tests. + +--- + +## RSpec: Full feature tests `/spec/features/**/*.rb` + +Full feature tests will load a full app environment and allow us to test things like rendering DOM, interacting with links and buttons, testing the outcome of those interactions through multiple pages if necessary. These are also called end-to-end tests but should not be confused with QA end-to-end tests (`package-and-qa` manual pipeline job). + +### When do we write/update these tests? + +When we add a new feature, we write at least two tests covering the success and the failure scenarios. + +### Relevant notes + +A `:js` flag is added to the test to make sure the full environment is loaded. + +``` +scenario 'successfully', :js do + sign_in(create(:admin)) +end +``` + +The steps of each test are written using capybara methods ([documentation](http://www.rubydoc.info/gems/capybara/2.15.1)). + +Bear in mind <abbr title="XMLHttpRequest">XHR</abbr> calls might require you to use `wait_for_requests` in between steps, like so: + +```rspec +find('.form-control').native.send_keys(:enter) + +wait_for_requests + +expect(page).not_to have_selector('.card') +``` + +--- + +## Karma tests `/spec/javascripts/**/*.js` + +These are the more frontend-focused, at the moment. They're **faster** than `rspec` and make for very quick testing of frontend components. + +### When do we write/update these tests? + +When we add/update a method/action/mutation to Vue or Vuex, we write karma tests to ensure the logic we wrote doesn't break. We should, however, refrain from writing tests that double-test Vue's internal features. + +### Relevant notes + +Karma tests are run against a virtual DOM. + +To populate the DOM, we can use fixtures to fake the generation of HTML instead of having Rails do that. + +Be sure to check the [best practices for karma tests](../../testing_guide/frontend_testing.html#best-practices). + +### Vue and Vuex + +Test as much as possible without double-testing Vue's internal features, as mentioned above. + +Make sure to test computedProperties, mutations, actions. Run the action and test that the proper mutations are committed. + +Also check these [notes on testing Vue components](../../fe_guide/vue.html#testing-vue-components). + +#### Vuex Helper: `testAction` + +We have a helper available to make testing actions easier, as per [official documentation](https://vuex.vuejs.org/en/testing.html): + +``` +testAction( + actions.actionName, // action + { }, // params to be passed to action + state, // state + [ + { type: types.MUTATION}, + { type: types.MUTATION_1, payload: {}}, + ], // mutations committed + [ + { type: 'actionName', payload: {}}, + { type: 'actionName1', payload: {}}, + ] // actions dispatched + done, +); +``` + +Check an example in [spec/javascripts/ide/stores/actions_spec.jsspec/javascripts/ide/stores/actions_spec.js](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/spec/javascripts/ide/stores/actions_spec.js). + +#### Vue Helper: `mountComponent` + +To make mounting a Vue component easier and more readable, we have a few helpers available in `spec/helpers/vue_mount_component_helper`. + +* `createComponentWithStore` +* `mountComponentWithStore` + +Examples of usage: + +``` +beforeEach(() => { + vm = createComponentWithStore(Component, store); + + vm.$store.state.currentBranchId = 'master'; + + vm.$mount(); +}, +``` + +``` +beforeEach(() => { + vm = mountComponentWithStore(Component, { + el: '#dummy-element', + store, + props: { badge }, + }); +}, +``` + +Don't forget to clean up: + +``` +afterEach(() => { + vm.$destroy(); +}); +``` diff --git a/doc/development/new_fe_guide/style/prettier.md b/doc/development/new_fe_guide/style/prettier.md index eb18189282b..6395af6f815 100644 --- a/doc/development/new_fe_guide/style/prettier.md +++ b/doc/development/new_fe_guide/style/prettier.md @@ -43,3 +43,17 @@ yarn prettier-all-save Formats all files in the repository with Prettier. (This should only be used to test global rule updates otherwise you would end up with huge MR's). The source of these Yarn scripts can be found in `/scripts/frontend/prettier.js`. + +### Scripts during Conversion period + +``` +node ./scripts/frontend/prettier.js check ./vendor/ +``` + +This will go over all files in a specific folder check it. + +``` +node ./scripts/frontend/prettier.js save ./vendor/ +``` + +This will go over all files in a specific folder and save it. diff --git a/doc/development/query_recorder.md b/doc/development/query_recorder.md index 26d3355e94d..61e5e1afede 100644 --- a/doc/development/query_recorder.md +++ b/doc/development/query_recorder.md @@ -22,6 +22,19 @@ As an example you might create 5 issues in between counts, which would cause the > **Note:** In some cases the query count might change slightly between runs for unrelated reasons. In this case you might need to test `exceed_query_limit(control_count + acceptable_change)`, but this should be avoided if possible. +## Cached queries + +By default, QueryRecorder will ignore cached queries in the count. However, it may be better to count +all queries to avoid introducing an N+1 query that may be masked by the statement cache. To do this, +pass the `skip_cached` variable to `QueryRecorder` and use the `exceed_all_query_limit` matcher: + +it "avoids N+1 database queries" do + control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) { visit_some_page }.count + create_list(:issue, 5) + expect { visit_some_page }.not_to exceed_all_query_limit(control_count) +end +``` + ## Finding the source of the query It may be useful to identify the source of the queries by looking at the call backtrace. diff --git a/doc/development/rake_tasks.md b/doc/development/rake_tasks.md index 31addcaf675..fc51b74da1d 100644 --- a/doc/development/rake_tasks.md +++ b/doc/development/rake_tasks.md @@ -176,3 +176,20 @@ git push -u origin update-project-templates ``` Now create a merge request and merge that to master. + +## Generate route lists + +To see the full list of API routes, you can run: + +```shell +bundle exec rake grape:path_helpers +``` + +For the Rails controllers, run: + +```shell +bundle exec rake routes +``` + +Since these take some time to create, it's often helpful to save the output to +a file for quick reference. diff --git a/doc/development/writing_documentation.md b/doc/development/writing_documentation.md index 1c41fc7611f..038a4b1e6ea 100644 --- a/doc/development/writing_documentation.md +++ b/doc/development/writing_documentation.md @@ -1,558 +1,3 @@ --- -description: Learn how to contribute to GitLab Documentation. +redirect_to: 'documentation/index.md' --- - -# GitLab Documentation guidelines - - - **General Documentation**: written by the [developers responsible by creating features](#contributing-to-docs). Should be submitted in the same merge request containing code. Feature proposals (by GitLab contributors) should also be accompanied by its respective documentation. They can be later improved by PMs and Technical Writers. - - **[Technical Articles](#technical-articles)**: written by any [GitLab Team](https://about.gitlab.com/team/) member, GitLab contributors, or [Community Writers](https://about.gitlab.com/handbook/product/technical-writing/community-writers/). - - **Indexes per topic**: initially prepared by the Technical Writing Team, and kept up-to-date by developers and PMs in the same merge request containing code. They gather all resources for that topic in a single page (user and admin documentation, articles, and third-party docs). - -## Contributing to docs - -Whenever a feature is changed, updated, introduced, or deprecated, the merge -request introducing these changes must be accompanied by the documentation -(either updating existing ones or creating new ones). This is also valid when -changes are introduced to the UI. - -The one responsible for writing the first piece of documentation is the developer who -wrote the code. It's the job of the Product Manager to ensure all features are -shipped with its docs, whether is a small or big change. At the pace GitLab evolves, -this is the only way to keep the docs up-to-date. If you have any questions about it, -ask a Technical Writer. Otherwise, when your content is ready, assign one of -them to review it for you. - -We use the [monthly release blog post](https://about.gitlab.com/handbook/marketing/blog/release-posts/#monthly-releases) as a changelog checklist to ensure everything -is documented. - -Whenever you submit a merge request for the documentation, use the documentation MR description template. - -Please check the [documentation workflow](https://about.gitlab.com/handbook/product/technical-writing/workflow/) before getting started. - -## Documentation structure - -- Overview and use cases: what it is, why it is necessary, why one would use it -- Requirements: what do we need to get started -- Tutorial: how to set it up, how to use it - -Always link a new document from its topic-related index, otherwise, it will -not be included it in the documentation site search. - -_Note: to be extended._ - -### Feature overview and use cases - -Every major feature (regardless if present in GitLab Community or Enterprise editions) -should present, at the beginning of the document, two main sections: **overview** and -**use cases**. Every GitLab EE-only feature should also contain these sections. - -**Overview**: as the name suggests, the goal here is to provide an overview of the feature. -Describe what is it, what it does, why it is important/cool/nice-to-have, -what problem it solves, and what you can do with this feature that you couldn't -do before. - -**Use cases**: provide at least two, ideally three, use cases for every major feature. -You should answer this question: what can you do with this feature/change? Use cases -are examples of how this feature or change can be used in real life. - -Examples: -- CE and EE: [Issues](../user/project/issues/index.md#use-cases) -- CE and EE: [Merge Requests](../user/project/merge_requests/index.md#overview) -- EE-only: [Geo](https://docs.gitlab.com/ee/gitlab-geo/README.html#overview) -- EE-only: [Jenkins integration](https://docs.gitlab.com/ee/integration/jenkins.md#overview) - -Note that if you don't have anything to add between the doc title (`<h1>`) and -the header `## Overview`, you can omit the header, but keep the content of the -overview there. - -> **Overview** and **use cases** are required to **every** Enterprise Edition feature, -and for every **major** feature present in Community Edition. - -## Markdown and styles - -Currently GitLab docs use Redcarpet as [markdown](../user/markdown.md) engine, but there's an [open discussion](https://gitlab.com/gitlab-com/gitlab-docs/issues/50) for implementing Kramdown in the near future. - -All the docs follow the [documentation style guidelines](doc_styleguide.md). - -## Documentation directory structure - -The documentation is structured based on the GitLab UI structure itself, -separated by [`user`](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/user), -[`administrator`](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/administration), and [`contributor`](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/development). - -In order to have a [solid site structure](https://searchengineland.com/seo-benefits-developing-solid-site-structure-277456) for our documentation, -all docs should be linked. Every new document should be cross-linked to its related documentation, and linked from its topic-related index, when existent. - -The directories `/workflow/`, `/gitlab-basics/`, `/university/`, and `/articles/` have -been deprecated and the majority their docs have been moved to their correct location -in small iterations. Please don't create new docs in these folders. - -### Location and naming documents - -The documentation hierarchy can be vastly improved by providing a better layout -and organization of directories. - -Having a structured document layout, we will be able to have meaningful URLs -like `docs.gitlab.com/user/project/merge_requests/index.html`. With this pattern, -you can immediately tell that you are navigating a user related documentation -and is about the project and its merge requests. - -Do not create summaries of similar types of content (e.g. an index of all articles, videos, etc.), -rather organize content by its subject (e.g. everything related to CI goes together) -and cross-link between any related content. - -The table below shows what kind of documentation goes where. - -| Directory | What belongs here | -| --------- | -------------- | -| `doc/user/` | User related documentation. Anything that can be done within the GitLab UI goes here including `/admin`. | -| `doc/administration/` | Documentation that requires the user to have access to the server where GitLab is installed. The admin settings that can be accessed via GitLab's interface go under `doc/user/admin_area/`. | -| `doc/api/` | API related documentation. | -| `doc/development/` | Documentation related to the development of GitLab. Any styleguides should go here. | -| `doc/legal/` | Legal documents about contributing to GitLab. | -| `doc/install/`| Probably the most visited directory, since `installation.md` is there. Ideally this should go under `doc/administration/`, but it's best to leave it as-is in order to avoid confusion (still debated though). | -| `doc/update/` | Same with `doc/install/`. Should be under `administration/`, but this is a well known location, better leave as-is, at least for now. | -| `doc/topics/` | Indexes per Topic (`doc/topics/topic-name/index.md`): all resources for that topic (user and admin documentation, articles, and third-party docs) | - ---- - -**General rules:** - -1. The correct naming and location of a new document, is a combination - of the relative URL of the document in question and the GitLab Map design - that is used for UX purposes ([source][graffle], [image][gitlab-map]). -1. When creating a new document and it has more than one word in its name, - make sure to use underscores instead of spaces or dashes (`-`). For example, - a proper naming would be `import_projects_from_github.md`. The same rule - applies to images. -1. Start a new directory with an `index.md` file. -1. There are four main directories, `user`, `administration`, `api` and `development`. -1. The `doc/user/` directory has five main subdirectories: `project/`, `group/`, - `profile/`, `dashboard/` and `admin_area/`. - 1. `doc/user/project/` should contain all project related documentation. - 1. `doc/user/group/` should contain all group related documentation. - 1. `doc/user/profile/` should contain all profile related documentation. - Every page you would navigate under `/profile` should have its own document, - i.e. `account.md`, `applications.md`, `emails.md`, etc. - 1. `doc/user/dashboard/` should contain all dashboard related documentation. - 1. `doc/user/admin_area/` should contain all admin related documentation - describing what can be achieved by accessing GitLab's admin interface - (_not to be confused with `doc/administration` where server access is - required_). - 1. Every category under `/admin/application_settings` should have its - own document located at `doc/user/admin_area/settings/`. For example, - the **Visibility and Access Controls** category should have a document - located at `doc/user/admin_area/settings/visibility_and_access_controls.md`. -1. The `doc/topics/` directory holds topic-related technical content. Create - `doc/topics/topic-name/subtopic-name/index.md` when subtopics become necessary. - General user- and admin- related documentation, should be placed accordingly. - -If you are unsure where a document should live, you can ping `@axil` or `@marcia` in your -merge request. - -### Changing document location - -Changing a document's location is not to be taken lightly. Remember that the -documentation is available to all installations under `help/` and not only to -GitLab.com or http://docs.gitlab.com. Make sure this is discussed with the -Documentation team beforehand. - -If you indeed need to change a document's location, do NOT remove the old -document, but rather replace all of its contents with a new line: - -``` -This document was moved to [another location](path/to/new_doc.md). -``` - -where `path/to/new_doc.md` is the relative path to the root directory `doc/`. - ---- - -For example, if you were to move `doc/workflow/lfs/lfs_administration.md` to -`doc/administration/lfs.md`, then the steps would be: - -1. Copy `doc/workflow/lfs/lfs_administration.md` to `doc/administration/lfs.md` -1. Replace the contents of `doc/workflow/lfs/lfs_administration.md` with: - - ``` - This document was moved to [another location](../../administration/lfs.md). - ``` - -1. Find and replace any occurrences of the old location with the new one. - A quick way to find them is to use `git grep`. First go to the root directory - where you cloned the `gitlab-ce` repository and then do: - - ``` - git grep -n "workflow/lfs/lfs_administration" - git grep -n "lfs/lfs_administration" - ``` - -NOTE: **Note:** -If the document being moved has any Disqus comments on it, there are extra steps -to follow documented just [below](#redirections-for-pages-with-disqus-comments). - -Things to note: - -- Since we also use inline documentation, except for the documentation itself, - the document might also be referenced in the views of GitLab (`app/`) which will - render when visiting `/help`, and sometimes in the testing suite (`spec/`). -- The above `git grep` command will search recursively in the directory you run - it in for `workflow/lfs/lfs_administration` and `lfs/lfs_administration` - and will print the file and the line where this file is mentioned. - You may ask why the two greps. Since we use relative paths to link to - documentation, sometimes it might be useful to search a path deeper. -- The `*.md` extension is not used when a document is linked to GitLab's - built-in help page, that's why we omit it in `git grep`. -- Use the checklist on the documentation MR description template. - -#### Alternative redirection method - -Alternatively to the method described above, you can simply replace the content -of the old file with a frontmatter containing a redirect link: - -```yaml ---- -redirect_to: '../path/to/file/README.md' ---- -``` - -It supports both full and relative URLs, e.g. `https://docs.gitlab.com/ee/path/to/file.html`, `../path/to/file.html`, `path/to/file.md`. Note that any `*.md` paths will be compiled to `*.html`. - -### Redirections for pages with Disqus comments - -If the documentation page being relocated already has any Disqus comments, -we need to preserve the Disqus thread. - -Disqus uses an identifier per page, and for docs.gitlab.com, the page identifier -is configured to be the page URL. Therefore, when we change the document location, -we need to preserve the old URL as the same Disqus identifier. - -To do that, add to the frontmatter the variable `redirect_from`, -using the old URL as value. For example, let's say I moved the document -available under `https://docs.gitlab.com/my-old-location/README.html` to a new location, -`https://docs.gitlab.com/my-new-location/index.html`. - -Into the **new document** frontmatter add the following: - -```yaml ---- -redirect_from: 'https://docs.gitlab.com/my-old-location/README.html' ---- -``` - -Note: it is necessary to include the file name in the `redirect_from` URL, -even if it's `index.html` or `README.html`. - -## Testing - -We treat documentation as code, thus have implemented some testing. -Currently, the following tests are in place: - -1. `docs lint`: Check that all internal (relative) links work correctly and - that all cURL examples in API docs use the full switches. It's recommended - to [check locally](#previewing-locally) before pushing to GitLab by executing the command - `bundle exec nanoc check internal_links` on your local - [`gitlab-docs`](https://gitlab.com/gitlab-com/gitlab-docs) directory. -1. [`ee_compat_check`](https://docs.gitlab.com/ee/development/automatic_ce_ee_merge.html#avoiding-ce-gt-ee-merge-conflicts-beforehand) (runs on CE only): - When you submit a merge request to GitLab Community Edition (CE), - there is this additional job that runs against Enterprise Edition (EE) - and checks if your changes can apply cleanly to the EE codebase. - If that job fails, read the instructions in the job log for what to do next. - As CE is merged into EE once a day, it's important to avoid merge conflicts. - Submitting an EE-equivalent merge request cherry-picking all commits from CE to EE is - essential to avoid them. - -## Branch naming - -If your contribution contains **only** documentation changes, you can speed up -the CI process by following some branch naming conventions. You have three -choices: - -| Branch name | Valid example | -| ----------- | ------------- | -| Starting with `docs/` | `docs/update-api-issues` | -| Starting with `docs-` | `docs-update-api-issues` | -| Ending in `-docs` | `123-update-api-issues-docs` | - -If your branch name matches any of the above, it will run only the docs -tests. If it doesn't, the whole test suite will run (including docs). - -## Merge requests for GitLab documentation - -Before getting started, make sure you read the introductory section -"[contributing to docs](#contributing-to-docs)" above and the -[tech writing workflow](https://about.gitlab.com/handbook/product/technical-writing/workflow/) -for GitLab Team members. - -- Use the current [merge request description template](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/.gitlab/merge_request_templates/Documentation.md) -- Use the correct [branch name](#branch-naming) -- Label the MR `Documentation` -- Assign the correct milestone (see note below) - - -NOTE: **Note:** -If the release version you want to add the documentation to has already been -frozen or released, use the label `Pick into X.Y` to get it merged into -the correct release. Avoid picking into a past release as much as you can, as -it increases the work of the release managers. - -### Cherry-picking from CE to EE - -As we have the `master` branch of CE merged into EE once a day, it's common to -run into merge conflicts. To avoid them, we [test for merge conflicts against EE](#testing) -with the `ee-compat-check` job, and use the following method of creating equivalent -branches for CE and EE. - -Follow this [method for cherry-picking from CE to EE](automatic_ce_ee_merge.md#cherry-picking-from-ce-to-ee), with a few adjustments: - -- Create the [CE branch](#branch-naming) starting with `docs-`, - e.g.: `git checkout -b docs-example` -- Create the EE-equivalent branch ending with `-ee`, e.g., - `git checkout -b docs-example-ee` -- Once all the jobs are passing in CE and EE, and you've addressed the -feedback from your own team, assign the CE MR to a technical writer for review -- When both MRs are ready, the EE merge request will be merged first, and the -CE-equivalent will be merged next. -- Note that the review will occur only in the CE MR, as the EE MR -contains the same commits as the CE MR. -- If you have a few more changes that apply to the EE-version only, you can submit -a couple more commits to the EE branch, but ask the reviewer to review the EE merge request -additionally to the CE MR. If there are many EE-only changes though, start a new MR -to EE only. - -## Previewing the changes live - -To preview your changes to documentation locally, please follow -this [development guide](https://gitlab.com/gitlab-com/gitlab-docs/blob/master/README.md#development). - -If you want to preview the doc changes of your merge request live, you can use -the manual `review-docs-deploy` job in your merge request. You will need at -least Master permissions to be able to run it and is currently enabled for the -following projects: - -- https://gitlab.com/gitlab-org/gitlab-ce -- https://gitlab.com/gitlab-org/gitlab-ee - -NOTE: **Note:** -You will need to push a branch to those repositories, it doesn't work for forks. - -TIP: **Tip:** -If your branch contains only documentation changes, you can use -[special branch names](#branch-naming) to avoid long running pipelines. - -In the mini pipeline graph, you should see an `>>` icon. Clicking on it will -reveal the `review-docs-deploy` job. Hit the play button for the job to start. - -![Manual trigger a docs build](img/manual_build_docs.png) - -This job will: - -1. Create a new branch in the [gitlab-docs](https://gitlab.com/gitlab-com/gitlab-docs) - project named after the scheme: `preview-<branch-slug>` -1. Trigger a cross project pipeline and build the docs site with your changes - -After a few minutes, the Review App will be deployed and you will be able to -preview the changes. The docs URL can be found in two places: - -- In the merge request widget -- In the output of the `review-docs-deploy` job, which also includes the - triggered pipeline so that you can investigate whether something went wrong - -In case the Review App URL returns 404, follow these steps to debug: - -1. **Did you follow the URL from the merge request widget?** If yes, then check if - the link is the same as the one in the job output. It can happen that if the - branch name slug is longer than 35 characters, it is automatically - truncated. That means that the merge request widget will not show the proper - URL due to a limitation of how `environment: url` works, but you can find the - real URL from the output of the `review-docs-deploy` job. -1. **Did you follow the URL from the job output?** If yes, then it means that - either the site is not yet deployed or something went wrong with the remote - pipeline. Give it a few minutes and it should appear online, otherwise you - can check the status of the remote pipeline from the link in the job output. - If the pipeline failed or got stuck, drop a line in the `#docs` chat channel. - -TIP: **Tip:** -Someone that has no merge rights to the CE/EE projects (think of forks from -contributors) will not be able to run the manual job. In that case, you can -ask someone from the GitLab team who has the permissions to do that for you. - -NOTE: **Note:** -Make sure that you always delete the branch of the merge request you were -working on. If you don't, the remote docs branch won't be removed either, -and the server where the Review Apps are hosted will eventually be out of -disk space. - -### Technical aspects - -If you want to know the hot details, here's what's really happening: - -1. You manually run the `review-docs-deploy` job in a CE/EE merge request. -1. The job runs the [`scripts/trigger-build-docs`](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/scripts/trigger-build-docs) - script with the `deploy` flag, which in turn: - 1. Takes your branch name and applies the following: - - The slug of the branch name is used to avoid special characters since - ultimately this will be used by NGINX. - - The `preview-` prefix is added to avoid conflicts if there's a remote branch - with the same name that you created in the merge request. - - The final branch name is truncated to 42 characters to avoid filesystem - limitations with long branch names (> 63 chars). - 1. The remote branch is then created if it doesn't exist (meaning you can - re-run the manual job as many times as you want and this step will be skipped). - 1. A new cross-project pipeline is triggered in the docs project. - 1. The preview URL is shown both at the job output and in the merge request - widget. You also get the link to the remote pipeline. -1. In the docs project, the pipeline is created and it - [skips the test jobs](https://gitlab.com/gitlab-com/gitlab-docs/blob/8d5d5c750c602a835614b02f9db42ead1c4b2f5e/.gitlab-ci.yml#L50-55) - to lower the build time. -1. Once the docs site is built, the HTML files are uploaded as artifacts. -1. A specific Runner tied only to the docs project, runs the Review App job - that downloads the artifacts and uses `rsync` to transfer the files over - to a location where NGINX serves them. - -The following GitLab features are used among others: - -- [Manual actions](../ci/yaml/README.md#manual-actions) -- [Multi project pipelines](https://docs.gitlab.com/ee/ci/multi_project_pipeline_graphs.html) -- [Review Apps](../ci/review_apps/index.md) -- [Artifacts](../ci/yaml/README.md#artifacts) -- [Specific Runner](../ci/runners/README.md#locking-a-specific-runner-from-being-enabled-for-other-projects) - -## GitLab `/help` - -Every GitLab instance includes the documentation, which is available from `/help` -(`http://my-instance.com/help`), e.g., <https://gitlab.com/help>. - -When you're building a new feature, you may need to link the documentation -from GitLab, the application. This is normally done in files inside the -`app/views/` directory with the help of the `help_page_path` helper method. - -In its simplest form, the HAML code to generate a link to the `/help` page is: - -```haml -= link_to 'Help page', help_page_path('user/permissions') -``` - -The `help_page_path` contains the path to the document you want to link to with -the following conventions: - -- it is relative to the `doc/` directory in the GitLab repository -- the `.md` extension must be omitted -- it must not end with a slash (`/`) - -Below are some special cases where should be used depending on the context. -You can combine one or more of the following: - -1. **Linking to an anchor link.** Use `anchor` as part of the `help_page_path` - method: - - ```haml - = link_to 'Help page', help_page_path('user/permissions', anchor: 'anchor-link') - ``` - -1. **Opening links in a new tab.** This should be the default behavior: - - ```haml - = link_to 'Help page', help_page_path('user/permissions'), target: '_blank' - ``` - -1. **Linking to a circle icon.** Usually used in settings where a long - description cannot be used, like near checkboxes. You can basically use - any font awesome icon, but prefer the `question-circle`: - - ```haml - = link_to icon('question-circle'), help_page_path('user/permissions') - ``` - -1. **Using a button link.** Useful in places where text would be out of context - with the rest of the page layout: - - ```haml - = link_to 'Help page', help_page_path('user/permissions'), class: 'btn btn-info' - ``` - -1. **Using links inline of some text.** - - ```haml - Description to #{link_to 'Help page', help_page_path('user/permissions')}. - ``` - -1. **Adding a period at the end of the sentence.** Useful when you don't want - the period to be part of the link: - - ```haml - = succeed '.' do - Learn more in the - = link_to 'Help page', help_page_path('user/permissions') - ``` - -## General Documentation vs Technical Articles - -### General documentation - -General documentation is categorized by _User_, _Admin_, and _Contributor_, and describe what that feature is, what it does, and its available settings. - -### Technical Articles - -Technical articles replace technical content that once lived in the [GitLab Blog](https://about.gitlab.com/blog/), where they got out-of-date and weren't easily found. - -They are topic-related documentation, written with an user-friendly approach and language, aiming to provide the community with guidance on specific processes to achieve certain objectives. - -A technical article guides users and/or admins to achieve certain objectives (within guides and tutorials), or provide an overview of that particular topic or feature (within technical overviews). It can also describe the use, implementation, or integration of third-party tools with GitLab. - -They should be placed in a new directory named `/article-title/index.md` under a topic-related folder, and their images should be placed in `/article-title/img/`. For example, a new article on GitLab Pages should be placed in `doc/user/project/pages/article-title/` and a new article on GitLab CI/CD should be placed in `doc/ci/examples/article-title/`. - -#### Types of Technical Articles - -- **User guides**: technical content to guide regular users from point A to point B -- **Admin guides**: technical content to guide administrators of GitLab instances from point A to point B -- **Technical Overviews**: technical content describing features, solutions, and third-party integrations -- **Tutorials**: technical content provided step-by-step on how to do things, or how to reach very specific objectives - -#### Understanding guides, tutorials, and technical overviews - -Suppose there's a process to go from point A to point B in 5 steps: `(A) 1 > 2 > 3 > 4 > 5 (B)`. - -A **guide** can be understood as a description of certain processes to achieve a particular objective. A guide brings you from A to B describing the characteristics of that process, but not necessarily going over each step. It can mention, for example, steps 2 and 3, but does not necessarily explain how to accomplish them. - -- Live example: "[Static sites and GitLab Pages domains (Part 1)](../user/project/pages/getting_started_part_one.md) to [Creating and Tweaking GitLab CI/CD for GitLab Pages (Part 4)](../user/project/pages/getting_started_part_four.md)" - -A **tutorial** requires a clear **step-by-step** guidance to achieve a singular objective. It brings you from A to B, describing precisely all the necessary steps involved in that process, showing each of the 5 steps to go from A to B. -It does not only describes steps 2 and 3, but also shows you how to accomplish them. - -- Live example (on the blog): [Hosting on GitLab.com with GitLab Pages](https://about.gitlab.com/2016/04/07/gitlab-pages-setup/) - -A **technical overview** is a description of what a certain feature is, and what it does, but does not walk -through the process of how to use it systematically. - -- Live example (on the blog): [GitLab Workflow, an overview](https://about.gitlab.com/2016/10/25/gitlab-workflow-an-overview/) - -#### Special format - -Every **Technical Article** contains a frontmatter at the beginning of the doc -with the following information: - -- **Type of article** (user guide, admin guide, technical overview, tutorial) -- **Knowledge level** expected from the reader to be able to follow through (beginner, intermediate, advanced) -- **Author's name** and **GitLab.com handle** -- **Publication date** (ISO format YYYY-MM-DD) - -For example: - - -```yaml ---- -author: John Doe -author_gitlab: johnDoe -level: beginner -article_type: user guide -date: 2017-02-01 ---- -``` - -#### Technical Articles - Writing Method - -Use the [writing method](https://about.gitlab.com/handbook/product/technical-writing/#writing-method) defined by the Technical Writing team. - -[gitlab-map]: https://gitlab.com/gitlab-org/gitlab-design/raw/master/production/resources/gitlab-map.png -[graffle]: https://gitlab.com/gitlab-org/gitlab-design/blob/d8d39f4a87b90fb9ae89ca12dc565347b4900d5e/production/resources/gitlab-map.graffle diff --git a/doc/downgrade_ee_to_ce/README.md b/doc/downgrade_ee_to_ce/README.md index 236408762e3..a187b3cbb07 100644 --- a/doc/downgrade_ee_to_ce/README.md +++ b/doc/downgrade_ee_to_ce/README.md @@ -57,7 +57,7 @@ $ sudo gitlab-rails runner "Service.where(type: ['JenkinsService', 'JenkinsDepre $ bundle exec rails runner "Service.where(type: ['JenkinsService', 'JenkinsDeprecatedService', 'GithubService']).delete_all" production ``` -### Secret variables environment scopes +### Variables environment scopes If you're using this feature and there are variables sharing the same key, but they have different scopes in a project, then you might want to diff --git a/doc/gitlab-basics/command-line-commands.md b/doc/gitlab-basics/command-line-commands.md index c9766040234..4666511d747 100644 --- a/doc/gitlab-basics/command-line-commands.md +++ b/doc/gitlab-basics/command-line-commands.md @@ -7,17 +7,19 @@ In Git, when you copy a project you say you "clone" it. To work on a git project When you are on your Dashboard, click on the project that you'd like to clone. To work in the project, you can copy a link to the Git repository through a SSH or a HTTPS protocol. SSH is easier to use after it's been -[setup](create-your-ssh-keys.md). While you are at the **Project** tab, select -HTTPS or SSH from the dropdown menu and copy the link using the 'Copy to clipboard' +[set up](create-your-ssh-keys.md). While you are at the **Project** tab, select +HTTPS or SSH from the dropdown menu and copy the link using the _Copy URL to clipboard_ button (you'll have to paste it on your shell in the next step). ![Copy the HTTPS or SSH](img/project_clone_url.png) ## On the command line +This section has examples of some basic shell commands that you might find useful. For more information, search the web for _bash commands_. + ### Clone your project -Go to your computer's shell and type the following command: +Go to your computer's shell and type the following command with your SSH or HTTPS URL: ``` git clone PASTE HTTPS OR SSH HERE @@ -25,33 +27,45 @@ git clone PASTE HTTPS OR SSH HERE A clone of the project will be created in your computer. ->**Note:** If you clone your project via an URL that contains special characters, make sure that they are URL-encoded. +>**Note:** If you clone your project via a URL that contains special characters, make sure that characters are URL-encoded. -### Go into a project, directory or file to work in it +### Go into a project directory to work in it ``` -cd NAME-OF-PROJECT-OR-FILE +cd NAME-OF-PROJECT ``` -### Go back one directory or file +### Go back one directory ``` -cd ../ +cd .. ``` -### View what’s in the directory that you are in +### List what’s in the current directory ``` ls ``` -### Create a directory +### List what’s in the current directory that starts with `a` + +``` +ls a* +``` + +### List what’s in the current directory that ends with `.md` + +``` +ls *.md +``` + +### Create a new directory ``` mkdir NAME-OF-YOUR-DIRECTORY ``` -### Create a README.md or file in directory +### Create a README.md file in the current directory ``` touch README.md @@ -62,6 +76,12 @@ nano README.md #### Press: enter ``` +### Show the contents of the README.md file + +``` +cat README.md +``` + ### Remove a file ``` @@ -74,12 +94,18 @@ rm NAME-OF-FILE rm -r NAME-OF-DIRECTORY ``` -### View history in the command line +### View command history ``` history ``` +### Execute command 123 from history + +``` +!123 +``` + ### Carry out commands for which the account you are using lacks authority You will be asked for an administrator’s password. @@ -88,8 +114,14 @@ You will be asked for an administrator’s password. sudo ``` -### Tell where you are +### Show which directory I am in ``` pwd ``` + +### Clear the shell window + +``` +clear +``` diff --git a/doc/gitlab-basics/start-using-git.md b/doc/gitlab-basics/start-using-git.md index 42cd8bb3e48..0d9994c9925 100644 --- a/doc/gitlab-basics/start-using-git.md +++ b/doc/gitlab-basics/start-using-git.md @@ -17,112 +17,197 @@ Depending on your operating system, you will need to use a shell of your prefere Git is usually preinstalled on Mac and Linux. Type the following command and then press enter: -``` + +```bash git --version ``` -You should receive a message that will tell you which Git version you have on your computer. If you don’t receive a "Git version" message, it means that you need to [download Git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git). +You should receive a message that tells you which Git version you have on your computer. If you don’t receive a "Git version" message, it means that you need to [download Git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git). If Git doesn't automatically download, there's an option on the website to [download manually](https://git-scm.com/downloads). Then follow the steps on the installation window. -After you are finished installing, open a new shell and type "git --version" again to verify that it was correctly installed. +After you are finished installing Git, open a new shell and type `git --version` again to verify that it was correctly installed. ## Add your Git username and set your email -It is important to configure your Git username and email address as every Git commit will use this information to identify you as the author. +It is important to configure your Git username and email address, since every Git commit will use this information to identify you as the author. On your shell, type the following command to add your username: -``` + +```bash git config --global user.name "YOUR_USERNAME" ``` Then verify that you have the correct username: -``` + +```bash git config --global user.name ``` To set your email address, type the following command: -``` + +```bash git config --global user.email "your_email_address@example.com" ``` To verify that you entered your email correctly, type: -``` + +```bash git config --global user.email ``` -You'll need to do this only once as you are using the `--global` option. It tells Git to always use this information for anything you do on that system. If you want to override this with a different username or email address for specific projects, you can run the command without the `--global` option when you’re in that project. +You'll need to do this only once, since you are using the `--global` option. It tells Git to always use this information for anything you do on that system. If you want to override this with a different username or email address for specific projects, you can run the command without the `--global` option when you’re in that project. ## Check your information -To view the information that you entered, type: -``` +To view the information that you entered, along with other global options, type: + +```bash git config --global --list ``` + ## Basic Git commands ### Go to the master branch to pull the latest changes from there -``` +```bash git checkout master ``` ### Download the latest changes in the project -This is for you to work on an up-to-date copy (it is important to do every time you work on a project), while you setup tracking branches. + +This is for you to work on an up-to-date copy (it is important to do this every time you start working on a project), while you set up tracking branches. You pull from remote repositories to get all the changes made by users since the last time you cloned or pulled the project. Later, you can push your local commits to the remote repositories. + +```bash +git pull REMOTE NAME-OF-BRANCH ``` -git pull REMOTE NAME-OF-BRANCH -u + +When you first clone a repository, REMOTE is typically "origin". This is where the repository came from, and it indicates the SSH or HTTPS URL of the repository on the remote server. NAME-OF-BRANCH is usually "master", but it may be any existing branch. + +### View your remote repositories + +To view your remote repositories, type: + +```bash +git remote -v ``` -(REMOTE: origin) (NAME-OF-BRANCH: could be "master" or an existing branch) ### Create a branch -Spaces won't be recognized, so you will need to use a hyphen or underscore. -``` + +To create a branch, type the following (spaces won't be recognized in the branch name, so you will need to use a hyphen or underscore): + +```bash git checkout -b NAME-OF-BRANCH ``` -### Work on a branch that has already been created -``` +### Work on an existing branch + +To switch to an existing branch, so you can work on it: + +```bash git checkout NAME-OF-BRANCH ``` ### View the changes you've made -It's important to be aware of what's happening and what's the status of your changes. -``` + +It's important to be aware of what's happening and the status of your changes. When you add, change, or delete files/folders, Git knows about it. To check the status of your changes: + +```bash git status ``` -### Add changes to commit -You'll see your changes in red when you type "git status". +### View differences + +To view the differences between your local, unstaged changes and the repository versions that you cloned or pulled, type: + +```bash +git diff +``` + +### Add and commit local changes + +You'll see your local changes in red when you type `git status`. These changes may be new, modified, or deleted files/folders. Use `git add` to stage a local file/folder for committing. Then use `git commit` to commit the staged files: + +```bash +git add FILE OR FOLDER +git commit -m "COMMENT TO DESCRIBE THE INTENTION OF THE COMMIT" ``` -git add CHANGES IN RED -git commit -m "DESCRIBE THE INTENTION OF THE COMMIT" + +### Add all changes to commit + +To add and commit all local changes in one command: + +```bash +git add . +git commit -m "COMMENT TO DESCRIBE THE INTENTION OF THE COMMIT" ``` +NOTE: **Note:** +The `.` character typically means _all_ in Git. + ### Send changes to gitlab.com -``` + +To push all local commits to the remote repository: + +```bash git push REMOTE NAME-OF-BRANCH ``` -### Delete all changes in the Git repository, but leave unstaged things +For example, to push your local commits to the _master_ branch of the _origin_ remote: + +```bash +git push origin master ``` + +### Delete all changes in the Git repository + +To delete all local changes in the repository that have not been added to the staging area, and leave unstaged files/folders, type: + +```bash git checkout . ``` -### Delete all changes in the Git repository, including untracked files -``` +### Delete all untracked changes in the Git repository + +```bash git clean -f ``` +### Unstage all changes that have been added to the staging area + +To undo the most recent add, but not committed, files/folders: + +```bash +git reset . +``` + +### Undo most recent commit + +To undo the most recent commit, type: + +```bash +git reset HEAD~1 +``` + +This leaves the files and folders unstaged in your local repository. + +CAUTION: **Warning:** +A Git commit is mostly irreversible, particularly if you already pushed it to the remote repository. Although you can undo a commit, the best option is to avoid the situation altogether. + ### Merge created branch with master branch + You need to be in the created branch. -``` + +```bash git checkout NAME-OF-BRANCH git merge master ``` ### Merge master branch with created branch + You need to be in the master branch. -``` + +```bash git checkout master git merge NAME-OF-BRANCH ``` diff --git a/doc/install/installation.md b/doc/install/installation.md index 34268c67140..6cd1fb4c2d7 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -301,9 +301,9 @@ sudo usermod -aG redis git ### Clone the Source # Clone GitLab repository - sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 10-8-stable gitlab + sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 11-0-stable gitlab -**Note:** You can change `10-8-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server! +**Note:** You can change `11-0-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server! ### Configure It diff --git a/doc/install/kubernetes/gitlab_omnibus.md b/doc/install/kubernetes/gitlab_omnibus.md index 98af87455ec..852a58a9afc 100644 --- a/doc/install/kubernetes/gitlab_omnibus.md +++ b/doc/install/kubernetes/gitlab_omnibus.md @@ -120,7 +120,7 @@ gitlabConfigStorageSize: 1Gi Ingress routing and SSL are automatically configured within this Chart. An NGINX ingress is provisioned and configured, and will route traffic to any service. SSL certificates are automatically created and configured by [kube-lego](https://github.com/kubernetes/charts/tree/master/stable/kube-lego). > **Note:** -Let's Encrypt limits a single TLD to five certificate requests within a single week. This means that common DNS wildcard services like [xip.io](http://xip.io) and [nip.io](http://nip.io) are unlikely to work. +Let's Encrypt limits a single TLD to five certificate requests within a single week. This means that common DNS wildcard services like [nip.io](http://nip.io) and [nip.io](http://nip.io) are unlikely to work. ## Installing GitLab using the Helm Chart > **Note:** @@ -144,7 +144,7 @@ helm install --name gitlab -f values.yaml gitlab/gitlab-omnibus or passing them on the command line: ```bash -helm install --name gitlab --set baseDomain=gitlab.io,baseIP=1.1.1.1,gitlab=ee,gitlabEELicense=$LICENSE,legoEmail=email@gitlab.com gitlab/gitlab-omnibus +helm install --name gitlab --set baseDomain=gitlab.io,baseIP=192.0.2.1,gitlab=ee,gitlabEELicense=$LICENSE,legoEmail=email@gitlab.com gitlab/gitlab-omnibus ``` ## Updating GitLab using the Helm Chart diff --git a/doc/install/openshift_and_gitlab/index.md b/doc/install/openshift_and_gitlab/index.md index 1ced1fb513d..60e1e2b5f8a 100644 --- a/doc/install/openshift_and_gitlab/index.md +++ b/doc/install/openshift_and_gitlab/index.md @@ -307,10 +307,10 @@ hostname** and use greater values for the volume sizes. If you don't provide a password for PostgreSQL, it will be created automatically. >**Note:** -The `gitlab.apps.10.2.2.2.xip.io` hostname that is used by default will +The `gitlab.apps.10.2.2.2.nip.io` hostname that is used by default will resolve to the host with IP `10.2.2.2` which is the IP our VM uses. It is a trick to have distinct FQDNs pointing to services that are on our local network. -Read more on how this works in <http://xip.io>. +Read more on how this works in <http://nip.io>. Now that we configured this, let's see how to manage and scale GitLab. @@ -347,7 +347,7 @@ Navigate back to the **Overview** and hopefully all pods will be up and running. ![GitLab running](img/gitlab-running.png) Congratulations! You can now navigate to your new shinny GitLab instance by -visiting <http://gitlab.apps.10.2.2.2.xip.io> where you will be asked to +visiting <http://gitlab.apps.10.2.2.2.nip.io> where you will be asked to change the root user password. Login using `root` as username and providing the password you just set, and start using GitLab! @@ -521,4 +521,4 @@ PaaS and managing your applications with the ease of containers. [autoscaling]: https://docs.openshift.org/latest/dev_guide/pod_autoscaling.html "Documentation - Autoscale" [basic-cli]: https://docs.openshift.org/latest/cli_reference/basic_cli_operations.html "Documentation - Basic CLI operations" [openshift-docs]: https://docs.openshift.org "OpenShift documentation" -[scc]: https://docs.openshift.org/latest/admin_guide/manage_scc.html "Documentation - Managing Security Context Constraints"
\ No newline at end of file +[scc]: https://docs.openshift.org/latest/admin_guide/manage_scc.html "Documentation - Managing Security Context Constraints" diff --git a/doc/integration/github.md b/doc/integration/github.md index 23bb8ef9303..680712f9e01 100644 --- a/doc/integration/github.md +++ b/doc/integration/github.md @@ -110,7 +110,7 @@ On the sign in page there should now be a GitHub icon below the regular sign in Click the icon to begin the authentication process. GitHub will ask the user to sign in and authorize the GitLab application. If everything goes well the user will be returned to GitLab and will be signed in. -### GitHub Enterprise with Self-Signed Certificate +## GitHub Enterprise with self-signed Certificate If you are attempting to import projects from GitHub Enterprise with a self-signed certificate and the imports are failing, you will need to disable SSL verification. diff --git a/doc/integration/gitlab.md b/doc/integration/gitlab.md index eec40a9b8f1..70087576678 100644 --- a/doc/integration/gitlab.md +++ b/doc/integration/gitlab.md @@ -7,13 +7,11 @@ GitLab.com will generate an application ID and secret key for you to use. 1. Sign in to GitLab.com -1. Navigate to your profile settings. +1. On the upper right corner, click on your avatar and go to your **Settings**. -1. Select "Applications" in the left menu. +1. Select **Applications** in the left menu. -1. Select "New application". - -1. Provide the required details. +1. Provide the required details for **Add new application**. - Name: This can be anything. Consider something like `<Organization>'s GitLab` or `<Your Name>'s GitLab` or something else descriptive. - Redirect URI: @@ -24,9 +22,9 @@ GitLab.com will generate an application ID and secret key for you to use. The first link is required for the importer and second for the authorization. -1. Select "Submit". +1. Select **Save application**. -1. You should now see a Client ID and Client Secret near the top right of the page (see screenshot). +1. You should now see a **Application Id** and **Secret** near the top right of the page (see screenshot). Keep this page open as you continue configuration. ![GitLab app](img/gitlab_app.png) diff --git a/doc/integration/google.md b/doc/integration/google.md index ae1d848f439..8906f91b6b4 100644 --- a/doc/integration/google.md +++ b/doc/integration/google.md @@ -35,7 +35,12 @@ In Google's side: 1. You should now be able to see a Client ID and Client secret. Note them down or keep this page open as you will need them later. -1. From the **Dashboard** select **ENABLE APIS AND SERVICES > Compute > Google Kubernetes Engine API > Enable** +1. From the **Dashboard** select **ENABLE APIS AND SERVICES > Compute > Google+ API > Enable** +1. To enable projects to access [Google Kubernetes Engine](../user/project/clusters/index.md), you must also + enable these APIs: + - Google Kubernetes Engine API + - Cloud Resource Manager API + - Cloud Billing API On your GitLab server: diff --git a/doc/integration/img/gitlab_app.png b/doc/integration/img/gitlab_app.png Binary files differindex b4958581a9b..8d6a4456fc4 100644 --- a/doc/integration/img/gitlab_app.png +++ b/doc/integration/img/gitlab_app.png diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md index 5faae7ca2d6..95221d8b6b1 100644 --- a/doc/raketasks/backup_restore.md +++ b/doc/raketasks/backup_restore.md @@ -492,8 +492,8 @@ directory (repositories, uploads). To restore a backup, you will also need to restore `/etc/gitlab/gitlab-secrets.json` (for Omnibus packages) or `/home/git/gitlab/.secret` (for installations from source). This file contains the database encryption key, -[CI secret variables](../ci/variables/README.md#secret-variables), and -secret variables used for [two-factor authentication](../user/profile/account/two_factor_authentication.md). +[CI/CD variables](../ci/variables/README.md#variables), and +variables used for [two-factor authentication](../user/profile/account/two_factor_authentication.md). If you fail to restore this encryption key file along with the application data backup, users with two-factor authentication enabled and GitLab Runners will lose access to your GitLab server. diff --git a/doc/security/information_exclusivity.md b/doc/security/information_exclusivity.md index f8e7fc3fd0e..22756232025 100644 --- a/doc/security/information_exclusivity.md +++ b/doc/security/information_exclusivity.md @@ -2,7 +2,7 @@ Git is a distributed version control system (DVCS). This means that everyone that works with the source code has a local copy of the complete repository. -In GitLab every project member that is not a guest (so reporters, developers and masters) can clone the repository to get a local copy. +In GitLab every project member that is not a guest (so reporters, developers and maintainers) can clone the repository to get a local copy. After obtaining this local copy the user can upload the full repository anywhere, including another project under their control or another server. The consequence is that you can't build access controls that prevent the intentional sharing of source code by users that have access to the source code. This is an inherent feature of a DVCS and all git management systems have this limitation. diff --git a/doc/security/webhooks.md b/doc/security/webhooks.md index a573445ab5b..b17b0a4bc4a 100644 --- a/doc/security/webhooks.md +++ b/doc/security/webhooks.md @@ -2,7 +2,7 @@ If you have non-GitLab web services running on your GitLab server or within its local network, these may be vulnerable to exploitation via Webhooks. -With [Webhooks](../user/project/integrations/webhooks.md), you and your project masters and owners can set up URLs to be triggered when specific things happen to projects. Normally, these requests are sent to external web services specifically set up for this purpose, that process the request and its attached data in some appropriate way. +With [Webhooks](../user/project/integrations/webhooks.md), you and your project maintainers and owners can set up URLs to be triggered when specific things happen to projects. Normally, these requests are sent to external web services specifically set up for this purpose, that process the request and its attached data in some appropriate way. Things get hairy, however, when a Webhook is set up with a URL that doesn't point to an external, but to an internal service, that may do something completely unintended when the webhook is triggered and the POST request is sent. diff --git a/doc/ssh/README.md b/doc/ssh/README.md index b71e9bf3000..bab196e7609 100644 --- a/doc/ssh/README.md +++ b/doc/ssh/README.md @@ -171,7 +171,7 @@ This is really useful for cloning repositories to your Continuous Integration (CI) server. By using deploy keys, you don't have to set up a dummy user account. -If you are a project master or owner, you can add a deploy key in the +If you are a project maintainer or owner, you can add a deploy key in the project settings under the section 'Repository'. Specify a title for the new deploy key and paste a public SSH key. After this, the machine that uses the corresponding private SSH key has read-only or read-write (if enabled) @@ -196,7 +196,7 @@ This is really useful for integrating repositories to secured, shared Continuous Integration (CI) services or other shared services. GitLab administrators can set up the Global Shared Deploy key in GitLab and add the private key to any shared systems. Individual repositories opt into -exposing their repository using these keys when a project masters (or higher) +exposing their repository using these keys when a project maintainers (or higher) authorizes a Global Shared Deploy key to be used with their project. Global Shared Keys can provide greater security compared to Per-Project Deploy @@ -205,7 +205,7 @@ who needs to know and configure the private key. GitLab administrators set up Global Deploy keys in the Admin area under the section **Deploy Keys**. Ensure keys have a meaningful title as that will be -the primary way for project masters and owners to identify the correct Global +the primary way for project maintainers and owners to identify the correct Global Deploy key to add. For instance, if the key gives access to a SaaS CI instance, use the name of that service in the key name if that is all it is used for. When creating Global Shared Deploy keys, give some thought to the granularity @@ -213,7 +213,7 @@ of keys - they could be of very narrow usage such as just a specific service or of broader usage for something like "Anywhere you need to give read access to your repository". -Once a GitLab administrator adds the Global Deployment key, project masters +Once a GitLab administrator adds the Global Deployment key, project maintainers and owners can add it in project's **Settings > Repository** section by expanding the **Deploy Key** section and clicking **Enable** next to the appropriate key listed under **Public deploy keys available to any project**. diff --git a/doc/system_hooks/system_hooks.md b/doc/system_hooks/system_hooks.md index 9ba05c7815b..cce02d218c2 100644 --- a/doc/system_hooks/system_hooks.md +++ b/doc/system_hooks/system_hooks.md @@ -138,7 +138,7 @@ Please refer to `group_rename` and `user_rename` for that case. "created_at": "2012-07-21T07:30:56Z", "updated_at": "2012-07-21T07:38:22Z", "event_name": "user_add_to_team", - "project_access": "Master", + "project_access": "Maintainer", "project_id": 74, "project_name": "StoreCloud", "project_path": "storecloud", @@ -158,7 +158,7 @@ Please refer to `group_rename` and `user_rename` for that case. "created_at": "2012-07-21T07:30:56Z", "updated_at": "2012-07-21T07:38:22Z", "event_name": "user_remove_from_team", - "project_access": "Master", + "project_access": "Maintainer", "project_id": 74, "project_name": "StoreCloud", "project_path": "storecloud", @@ -318,7 +318,7 @@ If the user is blocked via LDAP, `state` will be `ldap_blocked`. "created_at": "2012-07-21T07:30:56Z", "updated_at": "2012-07-21T07:38:22Z", "event_name": "user_add_to_group", - "group_access": "Master", + "group_access": "Maintainer", "group_id": 78, "group_name": "StoreCloud", "group_path": "storecloud", @@ -335,7 +335,7 @@ If the user is blocked via LDAP, `state` will be `ldap_blocked`. "created_at": "2012-07-21T07:30:56Z", "updated_at": "2012-07-21T07:38:22Z", "event_name": "user_remove_from_group", - "group_access": "Master", + "group_access": "Maintainer", "group_id": 78, "group_name": "StoreCloud", "group_path": "storecloud", diff --git a/doc/topics/autodevops/img/autodevops_domain_variables.png b/doc/topics/autodevops/img/autodevops_domain_variables.png Binary files differnew file mode 100644 index 00000000000..b6f8864796f --- /dev/null +++ b/doc/topics/autodevops/img/autodevops_domain_variables.png diff --git a/doc/topics/autodevops/img/autodevops_multiple_clusters.png b/doc/topics/autodevops/img/autodevops_multiple_clusters.png Binary files differnew file mode 100644 index 00000000000..f4d101ca921 --- /dev/null +++ b/doc/topics/autodevops/img/autodevops_multiple_clusters.png diff --git a/doc/topics/autodevops/index.md b/doc/topics/autodevops/index.md index efec365042a..103836e59d0 100644 --- a/doc/topics/autodevops/index.md +++ b/doc/topics/autodevops/index.md @@ -41,6 +41,7 @@ project in an easy and automatic way: 1. [Auto Code Quality](#auto-code-quality) 1. [Auto SAST (Static Application Security Testing)](#auto-sast) 1. [Auto Dependency Scanning](#auto-dependency-scanning) +1. [Auto License Management](#auto-license-management) 1. [Auto Container Scanning](#auto-container-scanning) 1. [Auto Review Apps](#auto-review-apps) 1. [Auto DAST (Dynamic Application Security Testing)](#auto-dast) @@ -62,7 +63,7 @@ Auto DevOps provides great defaults for all the stages; you can, however, For an overview on the creation of Auto DevOps, read the blog post [From 2/3 of the Self-Hosted Git Market, to the Next-Generation CI System, to Auto DevOps](https://about.gitlab.com/2017/06/29/whats-next-for-gitlab-ci/). -## Prerequisites +## Requirements TIP: **Tip:** For self-hosted installations, the easiest way to make use of Auto DevOps is to @@ -112,30 +113,31 @@ NOTE: **Note:** If you do not have Kubernetes or Prometheus installed, then Auto Review Apps, Auto Deploy, and Auto Monitoring will be silently skipped. -### Auto DevOps base domain +## Auto DevOps base domain The Auto DevOps base domain is required if you want to make use of [Auto -Review Apps](#auto-review-apps) and [Auto Deploy](#auto-deploy). It is defined -either under the project's CI/CD settings while -[enabling Auto DevOps](#enabling-auto-devops) or in instance-wide settings in -the CI/CD section. -It can also be set at the project or group level as a variable, `AUTO_DEVOPS_DOMAIN`. +Review Apps](#auto-review-apps) and [Auto Deploy](#auto-deploy). It can be defined +in three places: -A wildcard DNS A record matching the base domain is required, for example, +- either under the project's CI/CD settings while [enabling Auto DevOps](#enabling-auto-devops) +- or in instance-wide settings in the **admin area > Settings** under the "Continuous Integration and Delivery" section +- or at the project or group level as a variable: `AUTO_DEVOPS_DOMAIN` (required if you want to use [multiple clusters](#using-multiple-kubernetes-clusters)) + +A wildcard DNS A record matching the base domain(s) is required, for example, given a base domain of `example.com`, you'd need a DNS entry like: ``` *.example.com 3600 A 1.2.3.4 ``` -where `example.com` is the domain name under which the deployed apps will be served, +In this case, `example.com` is the domain name under which the deployed apps will be served, and `1.2.3.4` is the IP address of your load balancer; generally NGINX -([see prerequisites](#prerequisites)). How to set up the DNS record is beyond +([see requirements](#requirements)). How to set up the DNS record is beyond the scope of this document; you should check with your DNS provider. -Alternatively you can use free public services like [xip.io](http://xip.io) or +Alternatively you can use free public services like [nip.io](http://nip.io) or [nip.io](http://nip.io) which provide automatic wildcard DNS without any -configuration. Just set the Auto DevOps base domain to `1.2.3.4.xip.io` or +configuration. Just set the Auto DevOps base domain to `1.2.3.4.nip.io` or `1.2.3.4.nip.io`. Once set up, all requests will hit the load balancer, which in turn will route @@ -146,6 +148,56 @@ If GitLab is installed using the [GitLab Omnibus Helm Chart], there are two options: provide a static IP, or have one assigned. For more information see the relevant docs on the [network prerequisites](../../install/kubernetes/gitlab_omnibus.md#networking-prerequisites). +## Using multiple Kubernetes clusters **[PREMIUM]** + +When using Auto DevOps, you may want to deploy different environments to +different Kubernetes clusters. This is possible due to the 1:1 connection that +[exists between them](../../user/project/clusters/index.md#multiple-kubernetes-clusters). + +In the [Auto DevOps template](https://gitlab.com/gitlab-org/gitlab-ci-yml/blob/master/Auto-DevOps.gitlab-ci.yml) +(used behind the scenes by Auto DevOps), there are currently 3 defined environment names that you need to know: + +- `review/` (every environment starting with `review/`) +- `staging` +- `production` + +Those environments are tied to jobs that use [Auto Deploy](#auto-deploy), so +except for the environment scope, they would also need to have a different +domain they would be deployed to. This is why you need to define a separate +`AUTO_DEVOPS_DOMAIN` variable for all the above +[based on the environment](../../ci/variables/README.md#limiting-environment-scopes-of-variables). + +The following table is an example of how the three different clusters would +be configured. + +| Cluster name | Cluster environment scope | `AUTO_DEVOPS_DOMAIN` variable value | Variable environment scope | Notes | +| ------------ | -------------- | ----------------------------- | ------------- | ------ | +| review | `review/*` | `review.example.com` | `review/*` | The review cluster which will run all [Review Apps](../../ci/review_apps/index.md). `*` is a wildcard, which means it will be used by every environment name starting with `review/`. | +| staging | `staging` | `staging.example.com` | `staging` | (Optional) The staging cluster which will run the deployments of the staging environments. You need to [enable it first](#deploy-policy-for-staging-and-production-environments). | +| production | `production` | `example.com` | `production` | The production cluster which will run the deployments of the production environment. You can use [incremental rollouts](#incremental-rollout-to-production). | + +To add a different cluster for each environment: + +1. Navigate to your project's **Operations > Kubernetes** and create the Kubernetes clusters + with their respective environment scope as described from the table above. + + ![Auto DevOps multiple clusters](img/autodevops_multiple_clusters.png) + +1. After the clusters are created, navigate to each one and install Helm Tiller + and Ingress. +1. Make sure you have [configured your DNS](#auto-devops-base-domain) with the + specified Auto DevOps domains. +1. Navigate to your project's **Settings > CI/CD > Variables** and add + the `AUTO_DEVOPS_DOMAIN` variables with their respective environment + scope. + + ![Auto DevOps domain variables](img/autodevops_domain_variables.png) + +Now that all is configured, you can test your setup by creating a merge request +and verifying that your app is deployed as a review app in the Kubernetes +cluster with the `review/*` environment scope. Similarly, you can check the +other environments. + ## Quick start If you are using GitLab.com, see our [quick start guide](quick_start_guide.md) @@ -154,18 +206,18 @@ Google Cloud. ## Enabling Auto DevOps -If you haven't done already, read the [prerequisites](#prerequisites) to make +If you haven't done already, read the [requirements](#requirements) to make full use of Auto DevOps. If this is your fist time, we recommend you follow the -[quick start guide](#quick-start). +[quick start guide](quick_start_guide.md). To enable Auto DevOps to your project: -1. Check that your project doesn't have a `.gitlab-ci.yml`, and remove it otherwise -1. Go to your project's **Settings > CI/CD > General pipelines settings** and - find the Auto DevOps section +1. Check that your project doesn't have a `.gitlab-ci.yml`, or remove it otherwise +1. Go to your project's **Settings > CI/CD > Auto DevOps** 1. Select "Enable Auto DevOps" 1. Optionally, but recommended, add in the [base domain](#auto-devops-base-domain) - that will be used by Kubernetes to deploy your application + that will be used by Kubernetes to [deploy your application](#auto-deploy) + and choose the [deployment strategy](#deployment-strategy) 1. Hit **Save changes** for the changes to take effect Once saved, an Auto DevOps pipeline will be triggered on the default branch. @@ -182,6 +234,24 @@ in **Admin Area > Settings > Continuous Integration and Deployment**. Doing that all the projects that haven't explicitly set an option will have Auto DevOps enabled by default. +### Deployment strategy + +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/38542) in GitLab 11.0. + +You can change the deployment strategy used by Auto DevOps by going to your +project's **Settings > CI/CD > Auto DevOps**. + +The available options are: + +- **Continuous deployment to production** - enables [Auto Deploy](#auto-deploy) + by setting the [`STAGING_ENABLED`](#deploy-policy-for-staging-and-production-environments) and + [`INCREMENTAL_ROLLOUT_ENABLED`](#incremental-rollout-to-production) variables + to false. +- **Automatic deployment to staging, manual deployment to production** - sets the + [`STAGING_ENABLED`](#deploy-policy-for-staging-and-production-environments) and + [`INCREMENTAL_ROLLOUT_ENABLED`](#incremental-rollout-to-production) variables + to true, and the user is responsible for manually deploying to staging and production. + ## Stages of Auto DevOps The following sections describe the stages of Auto DevOps. Read them carefully @@ -230,7 +300,7 @@ In GitLab Starter, differences between the source and target branches are also [shown in the merge request widget](https://docs.gitlab.com/ee/user/project/merge_requests/code_quality_diff.html). -### Auto SAST +### Auto SAST **[ULTIMATE]** > Introduced in [GitLab Ultimate][ee] 10.3. @@ -241,9 +311,9 @@ report is created, it's uploaded as an artifact which you can later download and check out. In GitLab Ultimate, any security warnings are also -[shown in the merge request widget](https://docs.gitlab.com/ee/user/project/merge_requests/sast.html). +[shown in the merge request widget](https://docs.gitlab.com/ee//user/project/merge_requests/sast.html). -### Auto Dependency Scanning +### Auto Dependency Scanning **[ULTIMATE]** > Introduced in [GitLab Ultimate][ee] 10.7. @@ -254,7 +324,20 @@ report is created, it's uploaded as an artifact which you can later download and check out. In GitLab Ultimate, any security warnings are also -[shown in the merge request widget](https://docs.gitlab.com/ee/user/project/merge_requests/dependency_scanning.html). +[shown in the merge request widget](https://docs.gitlab.com/ee//user/project/merge_requests/dependency_scanning.html). + +### Auto License Management **[ULTIMATE]** + +> Introduced in [GitLab Ultimate][ee] 11.0. + +License Management uses the +[License Management Docker image](https://gitlab.com/gitlab-org/security-products/license_management) +to search the project dependencies for their license. Once the +report is created, it's uploaded as an artifact which you can later download and +check out. + +In GitLab Ultimate, any licenses are also +[shown in the merge request widget](https://docs.gitlab.com/ee//user/project/merge_requests/license_management.html). ### Auto Container Scanning @@ -267,13 +350,13 @@ created, it's uploaded as an artifact which you can later download and check out. In GitLab Ultimate, any security warnings are also -[shown in the merge request widget](https://docs.gitlab.com/ee/user/project/merge_requests/container_scanning.html). +[shown in the merge request widget](https://docs.gitlab.com/ee//user/project/merge_requests/container_scanning.html). ### Auto Review Apps NOTE: **Note:** This is an optional step, since many projects do not have a Kubernetes cluster -available. If the [prerequisites](#prerequisites) are not met, the job will +available. If the [requirements](#requirements) are not met, the job will silently be skipped. CAUTION: **Caution:** @@ -295,7 +378,7 @@ up in the merge request widget for easy discovery. When the branch is deleted, for example after the merge request is merged, the Review App will automatically be deleted. -### Auto DAST +### Auto DAST **[ULTIMATE]** > Introduced in [GitLab Ultimate][ee] 10.4. @@ -306,9 +389,9 @@ issues. Once the report is created, it's uploaded as an artifact which you can later download and check out. In GitLab Ultimate, any security warnings are also -[shown in the merge request widget](https://docs.gitlab.com/ee/user/project/merge_requests/dast.html). +[shown in the merge request widget](https://docs.gitlab.com/ee//user/project/merge_requests/dast.html). -### Auto Browser Performance Testing +### Auto Browser Performance Testing **[PREMIUM]** > Introduced in [GitLab Premium][ee] 10.4. @@ -320,13 +403,14 @@ Auto Browser Performance Testing utilizes the [Sitespeed.io container](https://h /direction ``` -In GitLab Premium, performance differences between the source and target branches are [shown in the merge request widget](https://docs.gitlab.com/ee/user/project/merge_requests/browser_performance_testing.html). +In GitLab Premium, performance differences between the source +and target branches are [shown in the merge request widget](https://docs.gitlab.com/ee//user/project/merge_requests/browser_performance_testing.html). ### Auto Deploy NOTE: **Note:** This is an optional step, since many projects do not have a Kubernetes cluster -available. If the [prerequisites](#prerequisites) are not met, the job will +available. If the [requirements](#requirements) are not met, the job will silently be skipped. CAUTION: **Caution:** @@ -360,10 +444,19 @@ no longer be valid as soon as the deployment job finishes. This means that Kubernetes can run the application, but in case it should be restarted or executed somewhere else, it cannot be accessed again. +> [Introduced][ce-19507] in GitLab 11.0. + +For internal and private projects a [GitLab Deploy Token](../../user/project/deploy_tokens/index.md###gitlab-deploy-token) +will be automatically created, when Auto DevOps is enabled and the Auto DevOps settings are saved. This Deploy Token +can be used for permanent access to the registry. + +Note: **Note** +When the GitLab Deploy Token has been manually revoked, it won't be automatically created. + ### Auto Monitoring NOTE: **Note:** -Check the [prerequisites](#prerequisites) for Auto Monitoring to make this stage +Check the [requirements](#requirements) for Auto Monitoring to make this stage work. Once your application is deployed, Auto Monitoring makes it possible to monitor @@ -493,6 +586,8 @@ also be customized, and you can easily use a [custom buildpack](#custom-buildpac | `POSTGRES_PASSWORD` | The PostgreSQL password; defaults to `testing-password`. Set it to use a custom password. | | `POSTGRES_DB` | The PostgreSQL database name; defaults to the value of [`$CI_ENVIRONMENT_SLUG`](../../ci/variables/README.md#predefined-variables-environment-variables). Set it to use a custom database name. | | `BUILDPACK_URL` | The buildpack's full URL. It can point to either Git repositories or a tarball URL. For Git repositories, it is possible to point to a specific `ref`, for example `https://github.com/heroku/heroku-buildpack-ruby.git#v142` | +| `SAST_CONFIDENCE_LEVEL` | The minimum confidence level of security issues you want to be reported; `1` for Low, `2` for Medium, `3` for High; defaults to `3`.| +| `DEP_SCAN_DISABLE_REMOTE_CHECKS` | Whether remote Dependency Scanning checks are disabled; defaults to `"false"`. Set to `"true"` to disable checks that send data to GitLab central servers. [Read more about remote checks](https://gitlab.com/gitlab-org/security-products/dependency-scanning#remote-checks).| | `STAGING_ENABLED` | From GitLab 10.8, this variable can be used to define a [deploy policy for staging and production environments](#deploy-policy-for-staging-and-production-environments). | | `CANARY_ENABLED` | From GitLab 11.0, this variable can be used to define a [deploy policy for canary environments](#deploy-policy-for-canary-environments). | | `INCREMENTAL_ROLLOUT_ENABLED`| From GitLab 10.8, this variable can be used to enable an [incremental rollout](#incremental-rollout-to-production) of your application for the production environment. | @@ -575,6 +670,9 @@ service: > [Introduced](https://gitlab.com/gitlab-org/gitlab-ci-yml/merge_requests/160) in GitLab 10.8. +TIP: **Tip:** +You can also set this inside your [project's settings](#deployment-strategy). + The normal behavior of Auto DevOps is to use Continuous Deployment, pushing automatically to the `production` environment every time a new pipeline is run on the default branch. However, there are cases where you might want to use a @@ -605,6 +703,9 @@ If `CANARY_ENABLED` is defined in your project (e.g., set `CANARY_ENABLED` to > [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/5415) in GitLab 10.8. +TIP: **Tip:** +You can also set this inside your [project's settings](#deployment-strategy). + When you have a new version of your app to deploy in production, you may want to use an incremental rollout to replace just a few pods with the latest code. This will allow you to first check how the app is behaving, and later manually @@ -741,3 +842,4 @@ curl --data "value=true" --header "PRIVATE-TOKEN: personal_access_token" https:/ [Auto DevOps template]: https://gitlab.com/gitlab-org/gitlab-ci-yml/blob/master/Auto-DevOps.gitlab-ci.yml [GitLab Omnibus Helm Chart]: ../../install/kubernetes/gitlab_omnibus.md [ee]: https://about.gitlab.com/products/ +[ce-19507]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/19507 diff --git a/doc/topics/git/index.md b/doc/topics/git/index.md index 2ca2bf743fb..7707d56764e 100644 --- a/doc/topics/git/index.md +++ b/doc/topics/git/index.md @@ -17,7 +17,7 @@ We've gathered some resources to help you to get the best from Git with GitLab. - [How to install Git](how_to_install_git/index.md) - [Start using Git on the command line](../../gitlab-basics/start-using-git.md) - [Command Line basic commands](../../gitlab-basics/command-line-commands.md) -- [GitLab Git Cheat Sheet (download)](https://gitlab.com/gitlab-com/marketing/raw/master/design/print/git-cheatsheet/print-pdf/git-cheatsheet.pdf) +- [GitLab Git Cheat Sheet (download)](https://about.gitlab.com/images/press/git-cheat-sheet.pdf) - Commits - [Revert a commit](../../user/project/merge_requests/revert_changes.md#reverting-a-commit) - [Cherry-picking a commit](../../user/project/merge_requests/cherry_pick_changes.md#cherry-picking-a-commit) diff --git a/doc/university/README.md b/doc/university/README.md index aa68c841f92..595fc480887 100644 --- a/doc/university/README.md +++ b/doc/university/README.md @@ -15,11 +15,11 @@ Would you like to contribute to GitLab University? Then please take a look at ou The curriculum is composed of GitLab videos, screencasts, presentations, projects and external GitLab content hosted on other services and has been organized into the following sections. -1. [GitLab Beginner](#beginner) -1. [GitLab Intermediate](#intermediate) -1. [GitLab Advanced](#advanced) -1. [External Articles](#external) -1. [Resources for GitLab Team Members](#team) +1. [GitLab Beginner](#1-gitlab-beginner) +1. [GitLab Intermediate](#2-gitlab-intermediate) +1. [GitLab Advanced](#3-gitlab-advanced) +1. [External Articles](#4-external-articles) +1. [Resources for GitLab Team Members](#5-resources-for-gitlab-team-members) --- @@ -126,7 +126,7 @@ The curriculum is composed of GitLab videos, screencasts, presentations, project 1. [IBM: Continuous Delivery vs Continuous Deployment - Video](https://www.youtube.com/watch?v=igwFj8PPSnw) 1. [Amazon: Transition to Continuous Delivery - Video](https://www.youtube.com/watch?v=esEFaY0FDKc) 2. [TechBeacon: Doing continuous delivery? Focus first on reducing release cycle times](https://techbeacon.com/doing-continuous-delivery-focus-first-reducing-release-cycle-times) -1. See **[Integrations](#integrations)** for integrations with other CI services. +1. See **[Integrations](#39-integrations)** for integrations with other CI services. #### 2.4. Workflow diff --git a/doc/university/glossary/README.md b/doc/university/glossary/README.md index 945d6a578b0..89516dba60b 100644 --- a/doc/university/glossary/README.md +++ b/doc/university/glossary/README.md @@ -557,10 +557,6 @@ Software that is hosted centrally and accessed on-demand (i.e. whenever you want This term is often used by people when they mean "Version Control." -#### SCLAU - -Abbreviation for SQO Count [Large And Up](https://about.gitlab.com/handbook/sales/#market-segmentation). This is the number of opportunities in large and strategic organizations passed from marketing to sales. - ### Scrum An Agile [framework](https://www.scrum.org/Resources/What-is-Scrum) designed to typically help complete complex software projects. It's made up of several parts: product requirements backlog, sprint planning, sprint (development), sprint review, and retrospec (analyzing the sprint). The goal is to end up with potentially shippable products. diff --git a/doc/university/high-availability/aws/README.md b/doc/university/high-availability/aws/README.md index f340164b882..dc045961ed7 100644 --- a/doc/university/high-availability/aws/README.md +++ b/doc/university/high-availability/aws/README.md @@ -2,6 +2,10 @@ comments: false --- +DANGER: This guide exists for reference of how an AWS deployment could work. +We are currently seeing very slow EFS access performance which causes GitLab to +be 5-10x slower than using NFS or Local disk. We _do not_ recommend follow this +guide at this time. # High Availability on AWS diff --git a/doc/update/10.6-to-10.7.md b/doc/update/10.6-to-10.7.md index 4a76ae14d2e..4efbb8c65cf 100644 --- a/doc/update/10.6-to-10.7.md +++ b/doc/update/10.6-to-10.7.md @@ -80,8 +80,8 @@ More information can be found on the [yarn website](https://yarnpkg.com/en/docs/ ### 5. Update Go -NOTE: GitLab 9.2 and higher only supports Go 1.8.3 and dropped support for Go -1.5.x through 1.7.x. Be sure to upgrade your installation if necessary. +NOTE: GitLab 9.2 and higher only supports Go 1.9 and dropped support for Go +1.5.x through 1.8.x. Be sure to upgrade your installation if necessary. You can check which version you are running with `go version`. @@ -91,11 +91,11 @@ Download and install Go: # Remove former Go installation folder sudo rm -rf /usr/local/go -curl --remote-name --progress https://storage.googleapis.com/golang/go1.8.3.linux-amd64.tar.gz -echo '1862f4c3d3907e59b04a757cfda0ea7aa9ef39274af99a784f5be843c80c6772 go1.8.3.linux-amd64.tar.gz' | shasum -a256 -c - && \ - sudo tar -C /usr/local -xzf go1.8.3.linux-amd64.tar.gz +curl --remote-name --progress https://storage.googleapis.com/golang/go1.9.linux-amd64.tar.gz +echo 'd70eadefce8e160638a9a6db97f7192d8463069ab33138893ad3bf31b0650a79 go1.9.linux-amd64.tar.gz' | shasum -a256 -c - && \ + sudo tar -C /usr/local -xzf go1.9.linux-amd64.tar.gz sudo ln -sf /usr/local/go/bin/{go,godoc,gofmt} /usr/local/bin/ -rm go1.8.3.linux-amd64.tar.gz +rm go1.9.linux-amd64.tar.gz ``` ### 6. Get latest code diff --git a/doc/update/10.8-to-11.0.md b/doc/update/10.8-to-11.0.md new file mode 100644 index 00000000000..78a47ab939f --- /dev/null +++ b/doc/update/10.8-to-11.0.md @@ -0,0 +1,362 @@ +--- +comments: false +--- + +# From 10.8 to 11.0 + +Make sure you view this update guide from the tag (version) of GitLab you would +like to install. In most cases this should be the highest numbered production +tag (without rc in it). You can select the tag in the version dropdown at the +top left corner of GitLab (below the menu bar). + +If the highest number stable branch is unclear please check the +[GitLab Blog](https://about.gitlab.com/blog/archives.html) for installation +guide links by version. + +### 1. Stop server + +```bash +sudo service gitlab stop +``` + +### 2. Backup + +NOTE: If you installed GitLab from source, make sure `rsync` is installed. + +```bash +cd /home/git/gitlab + +sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production +``` + +### 3. Update Ruby + +NOTE: GitLab 11.0 and higher only support Ruby 2.4.x and dropped support for Ruby 2.3.x. Be +sure to upgrade your interpreter if necessary. + +You can check which version you are running with `ruby -v`. + +Download Ruby and compile it: + +```bash +mkdir /tmp/ruby && cd /tmp/ruby +curl --remote-name --progress https://cache.ruby-lang.org/pub/ruby/2.4/ruby-2.4.4.tar.gz +echo 'ec82b0d53bd0adad9b19e6b45e44d54e9ec3f10c ruby-2.4.4.tar.gz' | shasum -c - && tar xzf ruby-2.4.4.tar.gz +cd ruby-2.4.4 + +./configure --disable-install-rdoc +make +sudo make install +``` + +Install Bundler: + +```bash +sudo gem install bundler --no-ri --no-rdoc +``` + +### 4. Update Node + +GitLab utilizes [webpack](http://webpack.js.org) to compile frontend assets. +This requires a minimum version of node v6.0.0. + +You can check which version you are running with `node -v`. If you are running +a version older than `v6.0.0` you will need to update to a newer version. You +can find instructions to install from community maintained packages or compile +from source at the nodejs.org website. + +<https://nodejs.org/en/download/> + +GitLab also requires the use of yarn `>= v1.2.0` to manage JavaScript +dependencies. + +```bash +curl --silent --show-error https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add - +echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list +sudo apt-get update +sudo apt-get install yarn +``` + +More information can be found on the [yarn website](https://yarnpkg.com/en/docs/install). + +### 5. Update Go + +NOTE: GitLab 9.2 and higher only supports Go 1.8.3 and dropped support for Go +1.5.x through 1.7.x. Be sure to upgrade your installation if necessary. + +You can check which version you are running with `go version`. + +Download and install Go: + +```bash +# Remove former Go installation folder +sudo rm -rf /usr/local/go + +curl --remote-name --progress https://storage.googleapis.com/golang/go1.8.3.linux-amd64.tar.gz +echo '1862f4c3d3907e59b04a757cfda0ea7aa9ef39274af99a784f5be843c80c6772 go1.8.3.linux-amd64.tar.gz' | shasum -a256 -c - && \ + sudo tar -C /usr/local -xzf go1.8.3.linux-amd64.tar.gz +sudo ln -sf /usr/local/go/bin/{go,godoc,gofmt} /usr/local/bin/ +rm go1.8.3.linux-amd64.tar.gz +``` + +### 6. Get latest code + +```bash +cd /home/git/gitlab + +sudo -u git -H git fetch --all --prune +sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically +sudo -u git -H git checkout -- locale +``` + +For GitLab Community Edition: + +```bash +cd /home/git/gitlab + +sudo -u git -H git checkout 11-0-stable +``` + +OR + +For GitLab Enterprise Edition: + +```bash +cd /home/git/gitlab + +sudo -u git -H git checkout 11-0-stable-ee +``` + +### 7. Update gitlab-shell + +```bash +cd /home/git/gitlab-shell + +sudo -u git -H git fetch --all --tags --prune +sudo -u git -H git checkout v$(</home/git/gitlab/GITLAB_SHELL_VERSION) +sudo -u git -H bin/compile +``` + +### 8. Update gitlab-workhorse + +Install and compile gitlab-workhorse. GitLab-Workhorse uses +[GNU Make](https://www.gnu.org/software/make/). +If you are not using Linux you may have to run `gmake` instead of +`make` below. + +```bash +cd /home/git/gitlab-workhorse + +sudo -u git -H git fetch --all --tags --prune +sudo -u git -H git checkout v$(</home/git/gitlab/GITLAB_WORKHORSE_VERSION) +sudo -u git -H make +``` + +### 9. Update Gitaly + +#### New Gitaly configuration options required + +In order to function Gitaly needs some additional configuration information. Below we assume you installed Gitaly in `/home/git/gitaly` and GitLab Shell in `/home/git/gitlab-shell`. + +```shell +echo ' +[gitaly-ruby] +dir = "/home/git/gitaly/ruby" + +[gitlab-shell] +dir = "/home/git/gitlab-shell" +' | sudo -u git tee -a /home/git/gitaly/config.toml +``` + +#### Check Gitaly configuration + +Due to a bug in the `rake gitlab:gitaly:install` script your Gitaly +configuration file may contain syntax errors. The block name +`[[storages]]`, which may occur more than once in your `config.toml` +file, should be `[[storage]]` instead. + +```shell +sudo -u git -H sed -i.pre-10.1 's/\[\[storages\]\]/[[storage]]/' /home/git/gitaly/config.toml +``` + +#### Compile Gitaly + +```shell +cd /home/git/gitaly +sudo -u git -H git fetch --all --tags --prune +sudo -u git -H git checkout v$(</home/git/gitlab/GITALY_SERVER_VERSION) +sudo -u git -H make +``` + +### 10. Update MySQL permissions + +If you are using MySQL you need to grant the GitLab user the necessary +permissions on the database: + +```bash +mysql -u root -p -e "GRANT TRIGGER ON \`gitlabhq_production\`.* TO 'git'@'localhost';" +``` + +If you use MySQL with replication, or just have MySQL configured with binary logging, +you will need to also run the following on all of your MySQL servers: + +```bash +mysql -u root -p -e "SET GLOBAL log_bin_trust_function_creators = 1;" +``` + +You can make this setting permanent by adding it to your `my.cnf`: + +``` +log_bin_trust_function_creators=1 +``` + +### 11. Update configuration files + +#### New configuration options for `gitlab.yml` + +There might be configuration options available for [`gitlab.yml`][yaml]. View them with the command below and apply them manually to your current `gitlab.yml`: + +```sh +cd /home/git/gitlab + +git diff origin/10-8-stable:config/gitlab.yml.example origin/11-0-stable:config/gitlab.yml.example +``` + +#### Nginx configuration + +Ensure you're still up-to-date with the latest NGINX configuration changes: + +```sh +cd /home/git/gitlab + +# For HTTPS configurations +git diff origin/10-8-stable:lib/support/nginx/gitlab-ssl origin/11-0-stable:lib/support/nginx/gitlab-ssl + +# For HTTP configurations +git diff origin/10-8-stable:lib/support/nginx/gitlab origin/11-0-stable:lib/support/nginx/gitlab +``` + +If you are using Strict-Transport-Security in your installation to continue using it you must enable it in your Nginx +configuration as GitLab application no longer handles setting it. + +If you are using Apache instead of NGINX please see the updated [Apache templates]. +Also note that because Apache does not support upstreams behind Unix sockets you +will need to let gitlab-workhorse listen on a TCP port. You can do this +via [/etc/default/gitlab]. + +[Apache templates]: https://gitlab.com/gitlab-org/gitlab-recipes/tree/master/web-server/apache +[/etc/default/gitlab]: https://gitlab.com/gitlab-org/gitlab-ce/blob/11-0-stable/lib/support/init.d/gitlab.default.example#L38 + +#### SMTP configuration + +If you're installing from source and use SMTP to deliver mail, you will need to add the following line +to config/initializers/smtp_settings.rb: + +```ruby +ActionMailer::Base.delivery_method = :smtp +``` + +See [smtp_settings.rb.sample] as an example. + +[smtp_settings.rb.sample]: https://gitlab.com/gitlab-org/gitlab-ce/blob/11-0-stable/config/initializers/smtp_settings.rb.sample#L13 + +#### Init script + +There might be new configuration options available for [`gitlab.default.example`][gl-example]. View them with the command below and apply them manually to your current `/etc/default/gitlab`: + +```sh +cd /home/git/gitlab + +git diff origin/10-8-stable:lib/support/init.d/gitlab.default.example origin/11-0-stable:lib/support/init.d/gitlab.default.example +``` + +Ensure you're still up-to-date with the latest init script changes: + +```bash +cd /home/git/gitlab + +sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab +``` + +For Ubuntu 16.04.1 LTS: + +```bash +sudo systemctl daemon-reload +``` + +### 12. Install libs, migrations, etc. + +```bash +cd /home/git/gitlab + +# MySQL installations (note: the line below states '--without postgres') +sudo -u git -H bundle install --without postgres development test --deployment + +# PostgreSQL installations (note: the line below states '--without mysql') +sudo -u git -H bundle install --without mysql development test --deployment + +# Optional: clean up old gems +sudo -u git -H bundle clean + +# Run database migrations +sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production + +# Compile GetText PO files + +sudo -u git -H bundle exec rake gettext:compile RAILS_ENV=production + +# Update node dependencies and recompile assets +sudo -u git -H bundle exec rake yarn:install gitlab:assets:clean gitlab:assets:compile RAILS_ENV=production NODE_ENV=production + +# Clean up cache +sudo -u git -H bundle exec rake cache:clear RAILS_ENV=production +``` + +**MySQL installations**: Run through the `MySQL strings limits` and `Tables and data conversion to utf8mb4` [tasks](../install/database_mysql.md). + +### 13. Start application + +```bash +sudo service gitlab start +sudo service nginx restart +``` + +### 14. Check application status + +Check if GitLab and its environment are configured correctly: + +```bash +cd /home/git/gitlab + +sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production +``` + +To make sure you didn't miss anything run a more thorough check: + +```bash +cd /home/git/gitlab + +sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production +``` + +If all items are green, then congratulations, the upgrade is complete! + +## Things went south? Revert to previous version (10.8) + +### 1. Revert the code to the previous version + +Follow the [upgrade guide from 10.7 to 10.8](10.7-to-10.8.md), except for the +database migration (the backup is already migrated to the previous version). + +### 2. Restore from the backup + +```bash +cd /home/git/gitlab + +sudo -u git -H bundle exec rake gitlab:backup:restore RAILS_ENV=production +``` + +If you have more than one backup `*.tar` file(s) please add `BACKUP=timestamp_of_backup` to the command above. + +[yaml]: https://gitlab.com/gitlab-org/gitlab-ce/blob/11-0-stable/config/gitlab.yml.example +[gl-example]: https://gitlab.com/gitlab-org/gitlab-ce/blob/11-0-stable/lib/support/init.d/gitlab.default.example diff --git a/doc/user/admin_area/settings/img/enforce_terms.png b/doc/user/admin_area/settings/img/enforce_terms.png Binary files differindex e5f0a2683b5..3d93e1cc891 100644 --- a/doc/user/admin_area/settings/img/enforce_terms.png +++ b/doc/user/admin_area/settings/img/enforce_terms.png diff --git a/doc/user/admin_area/settings/img/sign_up_terms.png b/doc/user/admin_area/settings/img/sign_up_terms.png Binary files differnew file mode 100644 index 00000000000..4cac9d426c9 --- /dev/null +++ b/doc/user/admin_area/settings/img/sign_up_terms.png diff --git a/doc/user/admin_area/settings/terms.md b/doc/user/admin_area/settings/terms.md index 8e1fb982aba..aa817c9a209 100644 --- a/doc/user/admin_area/settings/terms.md +++ b/doc/user/admin_area/settings/terms.md @@ -20,6 +20,19 @@ When an admin enables this feature, they will automattically be directed to the page to accept the terms themselves. After they accept, they will be directed back to the settings page. +## New registrations + +When this feature is enabled, a checkbox will be available in the +sign-up form. + +![Sign up form](img/sign_up_terms.png) + +This checkbox will be required during sign up. + +Users can review the terms entered in the admin panel before +accepting. The page will be opened in a new window so they can +continue their registration afterwards. + ## Accepting terms When this feature was enabled, the users that have not accepted the diff --git a/doc/user/discussions/index.md b/doc/user/discussions/index.md index 159109e8954..9b0ff02f227 100644 --- a/doc/user/discussions/index.md +++ b/doc/user/discussions/index.md @@ -11,7 +11,7 @@ You can leave a comment in the following places: - commit diffs The comment area supports [Markdown] and [quick actions]. One can edit their -own comment at any time, and anyone with [Master access level][permissions] or +own comment at any time, and anyone with [Maintainer access level][permissions] or higher can also edit a comment made by someone else. You could also reply to the notification email in order to reply to a comment, @@ -253,7 +253,7 @@ to newer issues or merge requests. - The people participating in the discussion are trolling, abusive, or otherwise being unproductive. -In these cases, a user with Master permissions or higher in the project can lock (and unlock) +In these cases, a user with Maintainer permissions or higher in the project can lock (and unlock) an issue or a merge request, using the "Lock" section in the sidebar: | Unlock | Lock | diff --git a/doc/user/gitlab_com/index.md b/doc/user/gitlab_com/index.md index 0c1cd113686..d054561d5f3 100644 --- a/doc/user/gitlab_com/index.md +++ b/doc/user/gitlab_com/index.md @@ -60,6 +60,7 @@ Below are the current settings regarding [GitLab CI/CD](../../ci/README.md). | Setting | GitLab.com | Default | | ----------- | ----------------- | ------------- | | Artifacts maximum size | 1G | 100M | +| Artifacts [expiry time](../../ci/yaml/README.md#artifacts-expire_in) | kept forever | deleted after 30 days unless otherwise specified | ## Repository size limit diff --git a/doc/user/group/index.md b/doc/user/group/index.md index 30761a66563..e6bf32a2dc5 100644 --- a/doc/user/group/index.md +++ b/doc/user/group/index.md @@ -125,7 +125,7 @@ side of your screen. --- -Group owners and masters will be notified of your request and will be able to approve or +Group owners and maintainers will be notified of your request and will be able to approve or decline it on the members page. ![Manage access requests](img/access_requests_management.png) diff --git a/doc/user/group/subgroups/index.md b/doc/user/group/subgroups/index.md index 02f8ef08117..08849ac1df4 100644 --- a/doc/user/group/subgroups/index.md +++ b/doc/user/group/subgroups/index.md @@ -145,8 +145,8 @@ permissions. For example, if User0 was first added to group `group-1/group-1-1` with Developer permissions, then they will inherit those permissions in every other subgroup -of `group-1/group-1-1`. To give them Master access to `group-1/group-1-1/group1-1-1`, -you would add them again in that group as Master. Removing them from that group, +of `group-1/group-1-1`. To give them Maintainer access to `group-1/group-1-1/group1-1-1`, +you would add them again in that group as Maintainer. Removing them from that group, the permissions will fallback to those of the ancestor group. ## Mentioning subgroups diff --git a/doc/user/index.md b/doc/user/index.md index a50e5e8fbf8..edf50019c2f 100644 --- a/doc/user/index.md +++ b/doc/user/index.md @@ -23,7 +23,7 @@ documentation. GitLab is a fully integrated software development platform that enables you and your team to work cohesively, faster, transparently, and effectively, since the discussion of a new idea until taking that idea to production all -all the way through, from within the same platform. +the way through, from within the same platform. Please check this page for an overview on [GitLab's features](https://about.gitlab.com/features/). @@ -110,7 +110,7 @@ personal access tokens, authorized applications, etc. - [Authentication](../topics/authentication/index.md): Read through the authentication methods available in GitLab. - [Permissions](permissions.md): Learn the different set of permissions levels for each -user type (guest, reporter, developer, master, owner). +user type (guest, reporter, developer, maintainer, owner). - [Feature highlight](feature_highlight.md): Learn more about the little blue dots around the app that explain certain features @@ -161,13 +161,13 @@ such as Trello, JIRA, etc. ## Webhooks -Configure [webhooks](project/integrations/webhooks.html) to listen for +Configure [webhooks](project/integrations/webhooks.md) to listen for specific events like pushes, issues or merge requests. GitLab will send a POST request with data to the webhook URL. ## API -Automate GitLab via [API](../api/README.html). +Automate GitLab via [API](../api/README.md). ## Git and GitLab diff --git a/doc/user/markdown.md b/doc/user/markdown.md index 650d60f1585..8e87c896a72 100644 --- a/doc/user/markdown.md +++ b/doc/user/markdown.md @@ -3,13 +3,15 @@ ## GitLab Flavored Markdown (GFM) > **Note:** -Not all of the GitLab-specific extensions to Markdown that are described in -this document currently work on our documentation website. +> Not all of the GitLab-specific extensions to Markdown that are described in +> this document currently work on our documentation website. > -For the best result, we encourage you to check this document out as rendered +> For the best result, we encourage you to check this document out as rendered by GitLab: [markdown.md] -_GitLab uses the [Redcarpet Ruby library][redcarpet] for Markdown processing._ +_GitLab uses (as of 11.1) the [CommonMark Ruby Library][commonmarker] for Markdown processing of all new issues, merge requests, comments, and other Markdown content in the GitLab system. Previous content and Markdown files `.md` in the repositories are still processed using the [Redcarpet Ruby library][redcarpet]._ + +_Where there are significant differences, we will try to call them out in this document._ GitLab uses "GitLab Flavored Markdown" (GFM). It extends the standard Markdown in a few significant ways to add some useful functionality. It was inspired by [GitHub Flavored Markdown](https://help.github.com/articles/basic-writing-and-formatting-syntax/). @@ -21,7 +23,7 @@ You can use GFM in the following areas: - milestones - snippets (the snippet must be named with a `.md` extension) - wiki pages -- markdown documents inside the repository +- markdown documents inside the repository (currently only rendered by Redcarpet) You can also use other rich text files in GitLab. You might have to install a dependency to do so. Please see the [github-markup gem readme](https://github.com/gitlabhq/markup#markups) for more information. @@ -34,7 +36,7 @@ https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#newline GFM honors the markdown specification in how [paragraphs and line breaks are handled](https://daringfireball.net/projects/markdown/syntax#p). A paragraph is simply one or more consecutive lines of text, separated by one or more blank lines. -Line-breaks, or softreturns, are rendered if you end a line with two or more spaces: +Line-breaks, or soft returns, are rendered if you end a line with two or more spaces: [//]: # (Do *NOT* remove the two ending whitespaces in the following line.) [//]: # (They are needed for the Markdown text to render correctly.) @@ -197,7 +199,7 @@ https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#inline- With inline diffs tags you can display {+ additions +} or [- deletions -]. -The wrapping tags can be either curly braces or square brackets [+ additions +] or {- deletions -}. +The wrapping tags can be either curly braces or square brackets: [+ additions +] or {- deletions -}. Examples: @@ -228,7 +230,7 @@ https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#emoji You can use it to point out a :bug: or warn about :speak_no_evil: patches. And if someone improves your really :snail: code, send them some :birthday:. People will :heart: you for that. - If you are new to this, don't be :fearful:. You can easily join the emoji :family:. All you need to do is to look up on the supported codes. + If you are new to this, don't be :fearful:. You can easily join the emoji :family:. All you need to do is to look up one of the supported codes. Consult the [Emoji Cheat Sheet](https://www.emojicopy.com) for a list of all supported emoji codes. :thumbsup: @@ -238,7 +240,7 @@ Sometimes you want to :monkey: around a bit and add some :star2: to your :speech You can use it to point out a :bug: or warn about :speak_no_evil: patches. And if someone improves your really :snail: code, send them some :birthday:. People will :heart: you for that. -If you are new to this, don't be :fearful:. You can easily join the emoji :family:. All you need to do is to look up on the supported codes. +If you are new to this, don't be :fearful:. You can easily join the emoji :family:. All you need to do is to look up one of the supported codes. Consult the [Emoji Cheat Sheet](https://www.emojicopy.com) for a list of all supported emoji codes. :thumbsup: @@ -394,17 +396,17 @@ Color written inside backticks will be followed by a color "chip". Examples: - `#F00` - `#F00A` - `#FF0000` - `#FF0000AA` - `RGB(0,255,0)` - `RGB(0%,100%,0%)` - `RGBA(0,255,0,0.7)` - `HSL(540,70%,50%)` + `#F00` + `#F00A` + `#FF0000` + `#FF0000AA` + `RGB(0,255,0)` + `RGB(0%,100%,0%)` + `RGBA(0,255,0,0.7)` + `HSL(540,70%,50%)` `HSLA(540,70%,50%,0.7)` -Becomes: +Become: `#F00` `#F00A` @@ -414,7 +416,7 @@ Becomes: `RGB(0%,100%,0%)` `RGBA(0,255,0,0.7)` `HSL(540,70%,50%)` -`HSLA(540,70%,50%,0.7)` +`HSLA(540,70%,50%,0.7)` #### Supported formats: @@ -481,14 +483,14 @@ Alt-H2 All Markdown-rendered headers automatically get IDs, except in comments. -On hover a link to those IDs becomes visible to make it easier to copy the link to the header to give it to someone else. +On hover, a link to those IDs becomes visible to make it easier to copy the link to the header to give it to someone else. The IDs are generated from the content of the header according to the following rules: -1. All text is converted to lowercase -1. All non-word text (e.g., punctuation, HTML) is removed -1. All spaces are converted to hyphens -1. Two or more hyphens in a row are converted to one +1. All text is converted to lowercase. +1. All non-word text (e.g., punctuation, HTML) is removed. +1. All spaces are converted to hyphens. +1. Two or more hyphens in a row are converted to one. 1. If a header with the same ID has already been generated, a unique incrementing number is appended, starting at 1. @@ -500,6 +502,7 @@ For example: # This header has Unicode in it: 한글 ## This header has spaces in it ### This header has spaces in it +## This header has 3.5 in it (and parentheses) ``` Would generate the following link IDs: @@ -509,11 +512,14 @@ Would generate the following link IDs: 1. `this-header-has-unicode-in-it-한글` 1. `this-header-has-spaces-in-it` 1. `this-header-has-spaces-in-it-1` +1. `this-header-has-3-5-in-it-and-parentheses` Note that the Emoji processing happens before the header IDs are generated, so the Emoji is converted to an image which then gets removed from the ID. ### Emphasis +Examples: + ```no-highlight Emphasis, aka italics, with *asterisks* or _underscores_. @@ -524,6 +530,8 @@ Combined emphasis with **asterisks and _underscores_**. Strikethrough uses two tildes. ~~Scratch this.~~ ``` +Become: + Emphasis, aka italics, with *asterisks* or _underscores_. Strong emphasis, aka bold, with **asterisks** or __underscores__. @@ -534,12 +542,14 @@ Strikethrough uses two tildes. ~~Scratch this.~~ ### Lists +Examples: + ```no-highlight 1. First ordered list item 2. Another item - * Unordered sub-list. + * Unordered sub-list. 1. Actual numbers don't matter, just that it's a number - 1. Ordered sub-list + 1. Ordered sub-list 4. And another item. * Unordered list can use asterisks @@ -547,11 +557,13 @@ Strikethrough uses two tildes. ~~Scratch this.~~ + Or pluses ``` +Become: + 1. First ordered list item 2. Another item - * Unordered sub-list. + * Unordered sub-list. 1. Actual numbers don't matter, just that it's a number - 1. Ordered sub-list + 1. Ordered sub-list 4. And another item. * Unordered list can use asterisks @@ -559,33 +571,45 @@ Strikethrough uses two tildes. ~~Scratch this.~~ + Or pluses If a list item contains multiple paragraphs, -each subsequent paragraph should be indented with four spaces. +each subsequent paragraph should be indented to the same level as the start of the list item text (_Redcarpet: paragraph should be indented with four spaces._) + +Example: ```no-highlight -1. First ordered list item +1. First ordered list item - Second paragraph of first item. -2. Another item + Second paragraph of first item. + +2. Another item ``` +Becomes: + 1. First ordered list item - Second paragraph of first item. + Paragraph of first item. + 2. Another item -If the second paragraph isn't indented with four spaces, -the second list item will be incorrectly labeled as `1`. +If the paragraph of the first item is not indented with the proper number of spaces, +the paragraph will appear outside the list, instead of properly indented under the list item. + +Example: ```no-highlight 1. First ordered list item - Second paragraph of first item. + Paragraph of first item. + 2. Another item ``` +Becomes: + 1. First ordered list item - Second paragraph of first item. + Paragraph of first item. + 2. Another item ### Links @@ -620,6 +644,8 @@ will point the link to `wikis/style` when the link is inside of a wiki markdown ### Images +Examples: + Here's our logo (hover to see the title text): Inline-style: @@ -630,6 +656,8 @@ will point the link to `wikis/style` when the link is inside of a wiki markdown [logo]: img/markdown_logo.png +Become: + Here's our logo: Inline-style: @@ -644,6 +672,8 @@ Reference-style: ### Blockquotes +Examples: + ```no-highlight > Blockquotes are very handy in email to emulate reply text. > This line is part of the same quote. @@ -653,6 +683,8 @@ Quote break. > This is a very long line that will still be quoted properly when it wraps. Oh boy let's keep writing to make sure this is long enough to actually wrap for everyone. Oh, you can *put* **Markdown** into a blockquote. ``` +Become: + > Blockquotes are very handy in email to emulate reply text. > This line is part of the same quote. @@ -666,6 +698,8 @@ You can also use raw HTML in your Markdown, and it'll mostly work pretty well. See the documentation for HTML::Pipeline's [SanitizationFilter](http://www.rubydoc.info/gems/html-pipeline/1.11.0/HTML/Pipeline/SanitizationFilter#WHITELIST-constant) class for the list of allowed HTML tags and attributes. In addition to the default `SanitizationFilter` whitelist, GitLab allows `span`, `abbr`, `details` and `summary` elements. +Examples: + ```no-highlight <dl> <dt>Definition list</dt> @@ -676,6 +710,8 @@ See the documentation for HTML::Pipeline's [SanitizationFilter](http://www.rubyd </dl> ``` +Become: + <dl> <dt>Definition list</dt> <dd>Is something people use sometimes.</dd> @@ -691,25 +727,31 @@ Content can be collapsed using HTML's [`<details>`](https://developer.mozilla.or <p> <details> <summary>Click me to collapse/fold.</summary> -These details will remain hidden until expanded. + +These details <em>will</em> remain <strong>hidden</strong> until expanded. <pre><code>PASTE LOGS HERE</code></pre> + </details> </p> -**Note:** Unfortunately Markdown is not supported inside these tags, as described by the [markdown specification](https://daringfireball.net/projects/markdown/syntax#html). You can work around this by using HTML, for example you can use `<pre><code>` tags instead of [code fences](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#code-and-syntax-highlighting). +**Note:** Markdown inside these tags is supported, as long as you have a blank link after the `</summary>` tag and before the `</details>` tag, as shown in the example. _Redcarpet does not support Markdown inside these tags. You can work around this by using HTML, for example you can use `<pre><code>` tags instead of [code fences](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#code-and-syntax-highlighting)._ ```html <details> <summary>Click me to collapse/fold.</summary> -These details will remain hidden until expanded. -<pre><code>PASTE LOGS HERE</code></pre> +These details _will_ remain **hidden** until expanded. + + PASTE LOGS HERE + </details> ``` ### Horizontal Rule +Examples: + ``` Three or more... @@ -726,6 +768,8 @@ ___ Underscores ``` +Become: + Three or more... --- @@ -742,10 +786,12 @@ Underscores ### Line Breaks -My basic recommendation for learning how line breaks work is to experiment and discover -- hit <Enter> once (i.e., insert one newline), then hit it twice (i.e., insert two newlines), see what happens. You'll soon learn to get what you want. "Markdown Toggle" is your friend. +A good way to learn how line breaks work is to experiment and discover -- hit <kbd>Enter</kbd> once (i.e., insert one newline), then hit it twice (i.e., insert two newlines), see what happens. You'll soon learn to get what you want. The "Preview" tab is your friend. Here are some things to try out: +Examples: + ``` Here's a line for us to start with. @@ -760,6 +806,8 @@ This line is *on its own line*, because the previous line ends with two spaces. spaces. ``` +Become: + Here's a line for us to start with. This line is separated from the one above by two newlines, so it will be a *separate paragraph*. @@ -774,7 +822,9 @@ spaces. ### Tables -Tables aren't part of the core Markdown spec, but they are part of GFM and Markdown Here supports them. +Tables aren't part of the core Markdown spec, but they are part of GFM. + +Example: ``` | header 1 | header 2 | @@ -783,18 +833,18 @@ Tables aren't part of the core Markdown spec, but they are part of GFM and Markd | cell 3 | cell 4 | ``` -Code above produces next output: +Becomes: | header 1 | header 2 | | -------- | -------- | | cell 1 | cell 2 | | cell 3 | cell 4 | -**Note** +**Note:** The row of dashes between the table header and body must have at least three dashes in each column. -The row of dashes between the table header and body must have at least three dashes in each column. +By including colons in the header row, you can align the text within that column. -By including colons in the header row, you can align the text within that column: +Example: ``` | Left Aligned | Centered | Right Aligned | Left Aligned | Centered | Right Aligned | @@ -803,6 +853,8 @@ By including colons in the header row, you can align the text within that column | Cell 7 | Cell 8 | Cell 9 | Cell 10 | Cell 11 | Cell 12 | ``` +Becomes: + | Left Aligned | Centered | Right Aligned | Left Aligned | Centered | Right Aligned | | :----------- | :------: | ------------: | :----------- | :------: | ------------: | | Cell 1 | Cell 2 | Cell 3 | Cell 4 | Cell 5 | Cell 6 | @@ -810,13 +862,29 @@ By including colons in the header row, you can align the text within that column ### Footnotes +Example: + ``` You can add footnotes to your text as follows.[^2] [^2]: This is my awesome footnote. ``` +Becomes: + You can add footnotes to your text as follows.[^2] +### Superscripts / Subscripts + +CommonMark and GFM currently do not support the superscript syntax ( `x^2` ) that Redcarpet does. You can use the standard HTML syntax for superscripts and subscripts. + +``` +The formula for water is H<sub>2</sub>O +while the equation for the theory of relativity is E = mc<sup>2</sup>. +``` + +The formula for water is H<sub>2</sub>O while the equation for the theory of relativity is E = mc<sup>2</sup>. + + ## Wiki-specific Markdown The following examples show how links inside wikis behave. @@ -908,3 +976,4 @@ A link starting with a `/` is relative to the wiki root. [katex]: https://github.com/Khan/KaTeX "KaTeX website" [katex-subset]: https://github.com/Khan/KaTeX/wiki/Function-Support-in-KaTeX "Macros supported by KaTeX" [asciidoctor-manual]: http://asciidoctor.org/docs/user-manual/#activating-stem-support "Asciidoctor user manual" +[commonmarker]: https://github.com/gjtorikian/commonmarker diff --git a/doc/user/permissions.md b/doc/user/permissions.md index 61dd0fbaed1..b36b0b4f757 100644 --- a/doc/user/permissions.md +++ b/doc/user/permissions.md @@ -27,7 +27,7 @@ See our [product handbook on permissions](https://about.gitlab.com/handbook/prod The following table depicts the various user permission levels in a project. -| Action | Guest | Reporter | Developer | Master | Owner | +| Action | Guest | Reporter | Developer |Maintainer| Owner | |---------------------------------------|---------|------------|-------------|----------|--------| | Create new issue | ✓ [^1] | ✓ | ✓ | ✓ | ✓ | | Create confidential issue | ✓ [^1] | ✓ | ✓ | ✓ | ✓ | @@ -51,6 +51,9 @@ The following table depicts the various user permission levels in a project. | See a container registry | | ✓ | ✓ | ✓ | ✓ | | See environments | | ✓ | ✓ | ✓ | ✓ | | See a list of merge requests | | ✓ | ✓ | ✓ | ✓ | +| Manage related issues **[STARTER]** | | ✓ | ✓ | ✓ | ✓ | +| Lock issue discussions | | ✓ | ✓ | ✓ | ✓ | +| Lock merge request discussions | | | ✓ | ✓ | ✓ | | Create new environments | | | ✓ | ✓ | ✓ | | Stop environments | | | ✓ | ✓ | ✓ | | Manage/Accept merge requests | | | ✓ | ✓ | ✓ | @@ -76,11 +79,12 @@ The following table depicts the various user permission levels in a project. | Edit project | | | | ✓ | ✓ | | Add deploy keys to project | | | | ✓ | ✓ | | Configure project hooks | | | | ✓ | ✓ | -| Manage runners | | | | ✓ | ✓ | +| Manage Runners | | | | ✓ | ✓ | | Manage job triggers | | | | ✓ | ✓ | | Manage variables | | | | ✓ | ✓ | -| Manage pages | | | | ✓ | ✓ | -| Manage pages domains and certificates | | | | ✓ | ✓ | +| Manage GitLab Pages | | | | ✓ | ✓ | +| Manage GitLab Pages domains and certificates | | | | ✓ | ✓ | +| Remove GitLab Pages | | | | | ✓ | | Manage clusters | | | | ✓ | ✓ | | Edit comments (posted by any user) | | | | ✓ | ✓ | | Switch visibility level | | | | | ✓ | @@ -90,6 +94,7 @@ The following table depicts the various user permission levels in a project. | Remove pages | | | | | ✓ | | Force push to protected branches [^4] | | | | | | | Remove protected branches [^4] | | | | | | +| View project Audit Events | | | | ✓ | ✓ | ## Project features permissions @@ -109,7 +114,7 @@ review, we've created protected branches. Read through the documentation on [protected branches](project/protected_branches.md) to learn more. -Additionally, you can allow or forbid users with Master and/or +Additionally, you can allow or forbid users with Maintainer and/or Developer permissions to push to a protected branch. Read through the documentation on [Allowed to Merge and Allowed to Push settings](project/protected_branches.md#using-the-allowed-to-merge-and-allowed-to-push-settings) to learn more. @@ -127,17 +132,12 @@ and drag issues around. Read though the [documentation on Issue Boards permissions](project/issue_board.md#permissions) to learn more. -### File Locking permissions - -> Available in [GitLab Premium](https://about.gitlab.com/products/). +### File Locking permissions **[PREMIUM]** The user that locks a file or directory is the only one that can edit and push their changes back to the repository where the locked objects are located. Read through the documentation on [permissions for File Locking](https://docs.gitlab.com/ee/user/project/file_lock.html#permissions-on-file-locking) to learn more. -File Locking is available in -[GitLab Premium](https://about.gitlab.com/products/) only. - ### Confidential Issues permissions Confidential issues can be accessed by reporters and higher permission levels, @@ -150,7 +150,7 @@ Any user can remove themselves from a group, unless they are the last Owner of the group. The following table depicts the various user permission levels in a group. -| Action | Guest | Reporter | Developer | Master | Owner | +| Action | Guest | Reporter | Developer | Maintainer | Owner | |-------------------------|-------|----------|-----------|--------|-------| | Browse group | ✓ | ✓ | ✓ | ✓ | ✓ | | Edit group | | | | | ✓ | @@ -160,6 +160,12 @@ group. | Remove group | | | | | ✓ | | Manage group labels | | ✓ | ✓ | ✓ | ✓ | | Create/edit/delete group milestones | | | ✓ | ✓ | ✓ | +| View private group epic **[ULTIMATE]** | | ✓ | ✓ | ✓ | ✓ | +| View internal group epic **[ULTIMATE]** | ✓ | ✓ | ✓ | ✓ | ✓ | +| View public group epic **[ULTIMATE]** | ✓ | ✓ | ✓ | ✓ | ✓ | +| Create/edit group epic **[ULTIMATE]** | | ✓ | ✓ | ✓ | ✓ | +| Delete group epic **[ULTIMATE]** | | | | | ✓ | +| View group Audit Events | | | | | ✓ | ### Subgroup permissions @@ -194,13 +200,34 @@ will find the option to flag the user as external. By default new users are not set as external users. This behavior can be changed by an administrator under **Admin > Application Settings**. +## Auditor users **[PREMIUM ONLY]** + +>[Introduced][ee-998] in [GitLab Premium][eep] 8.17. + +Auditor users are given read-only access to all projects, groups, and other +resources on the GitLab instance. + +An Auditor user should be able to access all projects and groups of a GitLab instance +with the permissions described on the documentation on [auditor users permissions](https://docs.gitlab.com/ee/administration/auditor_users.html#permissions-and-restrictions-of-an-auditor-user). + +[Read more about Auditor users.](https://docs.gitlab.com/ee/administration/auditor_users.html) + +## Project features + +Project features like wiki and issues can be hidden from users depending on +which visibility level you select on project settings. + +- Disabled: disabled for everyone +- Only team members: only team members will see even if your project is public or internal +- Everyone with access: everyone can see depending on your project visibility level + ## GitLab CI/CD permissions GitLab CI/CD permissions rely on the role the user has in GitLab. There are four permission levels in total: - admin -- master +- maintainer - developer - guest/reporter @@ -208,7 +235,7 @@ The admin user can perform any action on GitLab CI/CD in scope of the GitLab instance and project. In addition, all admins can use the admin interface under `/admin/runners`. -| Action | Guest, Reporter | Developer | Master | Admin | +| Action | Guest, Reporter | Developer |Maintainer| Admin | |---------------------------------------|-----------------|-------------|----------|--------| | See commits and jobs | ✓ | ✓ | ✓ | ✓ | | Retry or cancel job | | ✓ | ✓ | ✓ | @@ -230,7 +257,7 @@ Read all about the [new model and its implications][new-mod]. This table shows granted privileges for jobs triggered by specific types of users: -| Action | Guest, Reporter | Developer | Master | Admin | +| Action | Guest, Reporter | Developer |Maintainer| Admin | |---------------------------------------------|-----------------|-------------|----------|--------| | Run CI job | | ✓ | ✓ | ✓ | | Clone source and LFS from current project | | ✓ | ✓ | ✓ | @@ -263,23 +290,15 @@ for details about the pipelines security model. Since GitLab 8.15, LDAP user permissions can now be manually overridden by an admin user. Read through the documentation on [LDAP users permissions](https://docs.gitlab.com/ee/articles/how_to_configure_ldap_gitlab_ee/index.html#updating-user-permissions-new-feature) to learn more. -## Auditor users permissions - -> Available in [GitLab Premium](https://about.gitlab.com/products/). - -An Auditor user should be able to access all projects and groups of a GitLab instance -with the permissions described on the documentation on [auditor users permissions](https://docs.gitlab.com/ee/administration/auditor_users.html#permissions-and-restrictions-of-an-auditor-user). - -Auditor users are available in [GitLab Premium](https://about.gitlab.com/products/) -only. - [^1]: On public and internal projects, all users are able to perform this action [^2]: Guest users can only view the confidential issues they created themselves [^3]: If **Public pipelines** is enabled in **Project Settings > CI/CD** -[^4]: Not allowed for Guest, Reporter, Developer, Master, or Owner +[^4]: Not allowed for Guest, Reporter, Developer, Maintainer, or Owner [^5]: Only if the job was triggered by the user [^6]: Only if user is not external one [^7]: Only if user is a member of the project [ce-18994]: https://gitlab.com/gitlab-org/gitlab-ce/issues/18994 [new-mod]: project/new_ci_build_permissions_model.md +[ee-998]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/998 +[eep]: https://about.gitlab.com/products/
\ No newline at end of file diff --git a/doc/user/profile/img/profil-preferences-navigation-theme.png b/doc/user/profile/img/profil-preferences-navigation-theme.png Binary files differnew file mode 100644 index 00000000000..7adaec33b60 --- /dev/null +++ b/doc/user/profile/img/profil-preferences-navigation-theme.png diff --git a/doc/user/profile/img/profile-preferences-syntax-themes.png b/doc/user/profile/img/profile-preferences-syntax-themes.png Binary files differnew file mode 100644 index 00000000000..719df9410fc --- /dev/null +++ b/doc/user/profile/img/profile-preferences-syntax-themes.png diff --git a/doc/user/profile/img/profile_settings_dropdown.png b/doc/user/profile/img/profile_settings_dropdown.png Binary files differindex a2c620642e2..99b06a1bf58 100644 --- a/doc/user/profile/img/profile_settings_dropdown.png +++ b/doc/user/profile/img/profile_settings_dropdown.png diff --git a/doc/user/profile/preferences.md b/doc/user/profile/preferences.md index 930e506802a..e028861a419 100644 --- a/doc/user/profile/preferences.md +++ b/doc/user/profile/preferences.md @@ -7,6 +7,34 @@ To navigate to your profile's preferences, click your avatar icon in the top right corner and select **Settings**. From there on, choose the **Preferences** tab. +![Profile preferences settings](img/profile_settings_dropdown.png) + +## Navigation theme + +>**Note:** +Navigation themes have been re-introduced with [GitLab 10.0](https://about.gitlab.com/2017/09/22/gitlab-10-0-released/). + +The GitLab navigation theme setting allows you to personalize your GitLab experience. +You can choose from several color themes that add unique colors to the top navigation +and left side navigation. +Using individual color themes might help you differentiate between your different +GitLab instances. + +The default palette is Indigo. You can choose between 10 different themes: + +- Indigo +- Light Indigo +- Blue +- Light Blue +- Green +- Light Green +- Red +- Light Red +- Dark +- Light + +![Profile preferences syntax highlighting themes](img/profile-preferences-syntax-themes.png) + ## Syntax highlighting theme >**Note:** @@ -16,7 +44,7 @@ list of supported languages visit the rouge website. Changing this setting allows you to customize the color theme when viewing any syntax highlighted code on GitLab. -The default one is **White**, and you can choose among 5 different colors: +The default syntax theme is White, and you can choose among 5 different colors: - White - Dark @@ -24,6 +52,8 @@ The default one is **White**, and you can choose among 5 different colors: - Solarized dark - Monokai +![Profile preferences navigation themes](img/profil-preferences-navigation-theme.png) + ## Behavior The following settings allow you to customize the behavior of GitLab's layout @@ -52,16 +82,16 @@ You have 8 options here that you can use for your default dashboard view: - Assigned Issues - Assigned Merge Requests -### Project home page content +### Project overview content -The project home page content setting allows you to choose what content you want to +The project overview content setting allows you to choose what content you want to see on a project’s home page. You can choose between 3 options: -- Show the files and the readme (default) -- Show the readme -- Show the project’s activity +- Files and Readme (default) +- Readme +- Activity [rouge]: http://rouge.jneen.net/ "Rouge website" [todos]: ../../workflow/todos.md diff --git a/doc/user/project/clusters/eks_and_gitlab/img/add_cluster.png b/doc/user/project/clusters/eks_and_gitlab/img/add_cluster.png Binary files differnew file mode 100644 index 00000000000..9a0559a19d4 --- /dev/null +++ b/doc/user/project/clusters/eks_and_gitlab/img/add_cluster.png diff --git a/doc/user/project/clusters/eks_and_gitlab/img/create_dns.png b/doc/user/project/clusters/eks_and_gitlab/img/create_dns.png Binary files differnew file mode 100644 index 00000000000..657ab0d9fa9 --- /dev/null +++ b/doc/user/project/clusters/eks_and_gitlab/img/create_dns.png diff --git a/doc/user/project/clusters/eks_and_gitlab/img/create_project.png b/doc/user/project/clusters/eks_and_gitlab/img/create_project.png Binary files differnew file mode 100644 index 00000000000..f3446131419 --- /dev/null +++ b/doc/user/project/clusters/eks_and_gitlab/img/create_project.png diff --git a/doc/user/project/clusters/eks_and_gitlab/img/deploy_apps.png b/doc/user/project/clusters/eks_and_gitlab/img/deploy_apps.png Binary files differnew file mode 100644 index 00000000000..d6c3b1b3a94 --- /dev/null +++ b/doc/user/project/clusters/eks_and_gitlab/img/deploy_apps.png diff --git a/doc/user/project/clusters/eks_and_gitlab/img/environment.png b/doc/user/project/clusters/eks_and_gitlab/img/environment.png Binary files differnew file mode 100644 index 00000000000..77d711ba8f6 --- /dev/null +++ b/doc/user/project/clusters/eks_and_gitlab/img/environment.png diff --git a/doc/user/project/clusters/eks_and_gitlab/img/new_project.png b/doc/user/project/clusters/eks_and_gitlab/img/new_project.png Binary files differnew file mode 100644 index 00000000000..d401c4ac2bf --- /dev/null +++ b/doc/user/project/clusters/eks_and_gitlab/img/new_project.png diff --git a/doc/user/project/clusters/eks_and_gitlab/img/pipeline.png b/doc/user/project/clusters/eks_and_gitlab/img/pipeline.png Binary files differnew file mode 100644 index 00000000000..5f9c9815c24 --- /dev/null +++ b/doc/user/project/clusters/eks_and_gitlab/img/pipeline.png diff --git a/doc/user/project/clusters/eks_and_gitlab/index.md b/doc/user/project/clusters/eks_and_gitlab/index.md new file mode 100644 index 00000000000..2d8fdf0d1da --- /dev/null +++ b/doc/user/project/clusters/eks_and_gitlab/index.md @@ -0,0 +1,135 @@ +--- +author: Joshua Lambert +author_gitlab: joshlambert +level: intermediate +article_type: tutorial +date: 2018-06-05 +--- + +# Connecting and deploying to an Amazon EKS cluster + +## Introduction + +In this tutorial, we will show how easy it is to integrate an [Amazon EKS](https://aws.amazon.com/eks/) cluster with GitLab, and begin deploying applications. + +For an end-to-end walkthrough we will: +1. Start with a new project based on the sample Ruby on Rails template +1. Integrate an EKS cluster +1. Utilize [Auto DevOps](../../../../topics/autodevops/) to build, test, and deploy our application + +You will need: +1. An account on GitLab, like [GitLab.com](https://gitlab.com) +1. An Amazon EKS cluster +1. `kubectl` [installed and configured for access to the EKS cluster](https://docs.aws.amazon.com/eks/latest/userguide/getting-started.html#get-started-kubectl) + +If you don't have an Amazon EKS cluster, one can be created by following [the EKS getting started guide](https://docs.aws.amazon.com/eks/latest/userguide/getting-started.html). + +## Creating a new project + +On GitLab, create a new project by clicking on the `+` icon in the top navigation bar, and selecting `New project`. + +![New Project](img/new_project.png) + +On the new project screen, click on the `Create from template` tab, and select `Use template` for the Ruby on Rails sample project. + +Give the project a name, and then select `Create project`. + +![Create Project](img/create_project.png) + +## Connecting the EKS cluster + +From the left side bar, hover over `Operations` and select `Kubernetes`, then click on `Add Kubernetes cluster`, and finally `Add an existing Kubernetes cluster`. + +A few details from the EKS cluster will be required to connect it to GitLab. + +1. A valid Kubernetes certificate and token are needed to authenticate to the EKS cluster. A pair is created by default, which can be used. Open a shell and use `kubectl` to retrieve them: + * List the secrets with `kubectl get secrets`, and one should named similar to `default-token-xxxxx`. Copy that token name for use below. + * Get the certificate with `kubectl get secret <secret name> -o jsonpath="{['data']['ca\.crt']}" | base64 -D` + * Retrieve the token with `kubectl get secret <secret name> -o jsonpath="{['data']['token']}" | base64 -D`. +1. The API server endpoint is also required, so GitLab can connect to the cluster. This is displayed on the AWS EKS console, when viewing the EKS cluster details. + +You now have all the information needed to connect the EKS cluster: +* Kubernetes cluster name: Provide a name for the cluster to identify it within GitLab. +* Environment scope: Leave this as `*` for now, since we are only connecting a single cluster. +* API URL: Paste in the API server endpoint retrieved above. +* CA Certificate: Paste the certificate data from the earlier step, as-is. +* Paste the token value. Note on some versions of Kubernetes a trailing `%` is output, do not include it. +* Project namespace: This can be left blank to accept the default namespace, based on the project name. + +![Add Cluster](img/add_cluster.png) + +Click on `Add Kubernetes cluster`, the cluster is now connected to GitLab. At this point, [Kubernetes deployment variables](../#deployment-variables) will automatically be available during CI jobs, making it easy to interact with the cluster. + +If you would like to utilize your own CI/CD scripts to deploy to the cluster, you can stop here. + +## Disable Role Based-Access Control (RBAC) + +Presently, Auto DevOps and one-click app installs do not support [Kubernetes role-based access control](https://kubernetes.io/docs/reference/access-authn-authz/rbac/). Support is [being worked on](https://gitlab.com/groups/gitlab-org/-/epics/136), but in the interim RBAC must be disabled to utilize for these features. + +> **Note**: Disabling RBAC means that any application running in the cluster, or user who can authenticate to the cluster, has full API access. This is a [security concern](https://docs.gitlab.com/ee/user/project/clusters/#security-implications), and may not be desirable. + +To effectively disable RBAC, global permissions can be applied granting full access: + +```bash +kubectl create clusterrolebinding permissive-binding \ + --clusterrole=cluster-admin \ + --user=admin \ + --user=kubelet \ + --group=system:serviceaccounts +``` + +## Deploy services to the cluster + +GitLab supports one-click deployment of helpful services to the cluster, many of which support Auto DevOps. Back on the Kubernetes cluster screen in GitLab, a list of applications is now available to deploy. + +First install Helm Tiller, a package manager for Kubernetes. This enables deployment of the other applications. + +![Deploy Apps](img/deploy_apps.png) + +### Deploying NGINX Ingress (optional) + +Next, if you would like the deployed app to be reachable on the internet, deploy the Ingress. Note that this will also cause an [Elastic Load Balancer](https://aws.amazon.com/documentation/elastic-load-balancing/) to be created, which will incur additional AWS costs. + +Once installed, you may see a `?` for `Ingress IP Address`. This is because the created ELB is available at a DNS name, not an IP address. To get the DNS name, run: `kubectl get service ingress-nginx-ingress-controller -n gitlab-managed-apps -o jsonpath="{.status.loadBalancer.ingress[0].hostname}"`. Note, you may see a trailing `%` on some Kubernetes versions, do not include it. + +The Ingress is now available at this address, and will route incoming requests to the proper service based on the DNS name in the request. To support this, a wildcard DNS CNAME record should be created for the desired domain name. For example `*.myekscluster.com` would point to the Ingress hostname obtained earlier. + +![Create DNS](img/create_dns.png) + +### Deploying the GitLab Runner (optional) + +If the project is on GitLab.com, free shared runners are available and you do not have to deploy one. If a project specific runner is desired, or there are no shared runners, it is easy to deploy one. + +Simply click on the `Install` button for the GitLab Runner. It is important to note that the runner deployed is set as **privileged**, which means it essentially has root access to the underlying machine. This is required to build docker images, and so is on by default. + +### Deploying Prometheus (optional) + +GitLab is able to monitor applications automatically, utilizing [Prometheus](../../integrations/prometheus.html). Kubernetes container CPU and memory metrics are automatically collected, and response metrics are retrieved from NGINX Ingress as well. + +To enable monitoring, simply install Prometheus into the cluster with the `Install` button. + +## Create a default Storage Class + +Amazon EKS does not have a default Storage Class out of the box, which means requests for persistent volumes will not be automatically fulfilled. As part of Auto DevOps, the deployed Postgres instance requests persistent storage, and without a default storage class it will fail to start. + +If a default Storage Class does not already exist and is desired, follow Amazon's [short guide](https://docs.aws.amazon.com/eks/latest/userguide/storage-classes.html) to create one. + +Alternatively, disable Postgres by setting the project variable [`POSTGRES_ENABLED`](../../../../topics/autodevops/#environment-variables) to `false`. + +## Deploy the app to EKS + +With RBAC disabled and services deployed, [Auto DevOps](https://docs.gitlab.com/ee/topics/autodevops/) can now be leveraged to build, test, and deploy the app. To enable, click on `Settings` in the left sidebar, then `CI/CD`. You will see a section for `Auto DevOps`, expand it. Click on the radio button to `Enable Auto DevOps`. + +If a wildcard DNS entry was created resolving to the Load Balancer, enter it in the `domain` field. Otherwise, the deployed app will not be externally available outside of the cluster. To save, click `Save changes`. + +![Deploy Pipeline](img/pipeline.png) + +A new pipeline will automatically be created, which will begin to build, test, and deploy the app. + +After the pipeline has finished, your app will be running in EKS and available to users. Click on `CI/CD` tab in the left navigation bar, and choose `Environments`. + +![Deployed Environment](img/environment.png) + +You will see a list of the environments and their deploy status, as well as options to browse to the app, view monitoring metrics, and even access a shell on the running pod. + +To learn more about Auto DevOps, review our [documentation](../../../../topics/autodevops/). diff --git a/doc/user/project/clusters/index.md b/doc/user/project/clusters/index.md index 65cdece8d3d..48bb2e543c1 100644 --- a/doc/user/project/clusters/index.md +++ b/doc/user/project/clusters/index.md @@ -19,7 +19,7 @@ or provide the credentials to an [existing Kubernetes cluster](#adding-an-existi ## Adding and creating a new GKE cluster via GitLab NOTE: **Note:** -You need Master [permissions] and above to access the Kubernetes page. +You need Maintainer [permissions] and above to access the Kubernetes page. Before proceeding, make sure the following requirements are met: @@ -30,7 +30,7 @@ Before proceeding, make sure the following requirements are met: clusters on GKE. That would mean that a [billing account](https://cloud.google.com/billing/docs/how-to/manage-billing-account) must be set up and that you have to have permissions to access it. -- You must have Master [permissions] in order to be able to access the +- You must have Maintainer [permissions] in order to be able to access the **Kubernetes** page. - You must have [Cloud Billing API](https://cloud.google.com/billing/) enabled - You must have [Resource Manager @@ -39,22 +39,22 @@ Before proceeding, make sure the following requirements are met: If all of the above requirements are met, you can proceed to create and add a new Kubernetes cluster that will be hosted on GKE to your project: -1. Navigate to your project's **CI/CD > Kubernetes** page. +1. Navigate to your project's **Operations > Kubernetes** page. 1. Click on **Add Kubernetes cluster**. -1. Click on **Create with GKE**. +1. Click on **Create with Google Kubernetes Engine**. 1. Connect your Google account if you haven't done already by clicking the **Sign in with Google** button. 1. Fill in the requested values: - - **Cluster name** (required) - The name you wish to give the cluster. - - **GCP project ID** (required) - The ID of the project you created in your GCP + - **Kubernetes cluster name** - The name you wish to give the cluster. + - **Environment scope** - The [associated environment](#setting-the-environment-scope) to this cluster. + - **Google Cloud Platform project** - The project you created in your GCP console that will host the Kubernetes cluster. This must **not** be confused - with the project name. Learn more about [Google Cloud Platform projects](https://cloud.google.com/resource-manager/docs/creating-managing-projects). + with the project ID. Learn more about [Google Cloud Platform projects](https://cloud.google.com/resource-manager/docs/creating-managing-projects). - **Zone** - The [zone](https://cloud.google.com/compute/docs/regions-zones/) under which the cluster will be created. - **Number of nodes** - The number of nodes you wish the cluster to have. - **Machine type** - The [machine type](https://cloud.google.com/compute/docs/machine-types) of the Virtual Machine instance that the cluster will be based on. - - **Environment scope** - The [associated environment](#setting-the-environment-scope) to this cluster. 1. Finally, click the **Create Kubernetes cluster** button. After a few moments, your cluster should be created. If something goes wrong, @@ -66,11 +66,11 @@ enable the Cluster integration. ## Adding an existing Kubernetes cluster NOTE: **Note:** -You need Master [permissions] and above to access the Kubernetes page. +You need Maintainer [permissions] and above to access the Kubernetes page. To add an existing Kubernetes cluster to your project: -1. Navigate to your project's **CI/CD > Kubernetes** page. +1. Navigate to your project's **Operations > Kubernetes** page. 1. Click on **Add Kubernetes cluster**. 1. Click on **Add an existing Kubernetes cluster** and fill in the details: - **Kubernetes cluster name** (required) - The name you wish to give the cluster. @@ -201,6 +201,11 @@ Otherwise, you can list the IP addresses of all load balancers: kubectl get svc --all-namespaces -o jsonpath='{range.items[?(@.status.loadBalancer.ingress)]}{.status.loadBalancer.ingress[*].ip} ' ``` +> **Note**: Some Kubernetes clusters return a hostname instead, like [Amazon EKS](https://aws.amazon.com/eks/). For these platforms, run: +> ```bash +> kubectl get service ingress-nginx-ingress-controller -n gitlab-managed-apps -o jsonpath="{.status.loadBalancer.ingress[0].hostname}"`. +> ``` + The output is the external IP address of your cluster. This information can then be used to set up DNS entries and forwarding rules that allow external access to your deployed applications. @@ -233,7 +238,7 @@ When adding more than one Kubernetes clusters to your project, you need to differentiate them with an environment scope. The environment scope associates clusters and [environments](../../../ci/environments.md) in an 1:1 relationship similar to how the -[environment-specific variables](../../../ci/variables/README.md#limiting-environment-scopes-of-secret-variables) +[environment-specific variables](../../../ci/variables/README.md#limiting-environment-scopes-of-variables) work. The default environment scope is `*`, which means all jobs, regardless of their @@ -325,7 +330,7 @@ To disable the Kubernetes cluster integration, follow the same procedure. ## Removing the Kubernetes cluster integration NOTE: **Note:** -You need Master [permissions] and above to remove a Kubernetes cluster integration. +You need Maintainer [permissions] and above to remove a Kubernetes cluster integration. NOTE: **Note:** When you remove a cluster, you only remove its relation to GitLab, not the @@ -382,7 +387,7 @@ you will need the Kubernetes project integration enabled. ### Web terminals NOTE: **Note:** -Introduced in GitLab 8.15. You must be the project owner or have `master` permissions +Introduced in GitLab 8.15. You must be the project owner or have `maintainer` permissions to use terminals. Support is limited to the first container in the first pod of your environment. @@ -393,6 +398,10 @@ containers. To use this integration, you should deploy to Kubernetes using the deployment variables above, ensuring any pods you create are labelled with `app=$CI_ENVIRONMENT_SLUG`. GitLab will do the rest! +## Read more + +- [Connecting and deploying to an Amazon EKS cluster](eks_and_gitlab/index.md) + [permissions]: ../../permissions.md [ee]: https://about.gitlab.com/products/ [Auto DevOps]: ../../../topics/autodevops/index.md diff --git a/doc/user/project/container_registry.md b/doc/user/project/container_registry.md index 9c5e3509046..03302b3815d 100644 --- a/doc/user/project/container_registry.md +++ b/doc/user/project/container_registry.md @@ -143,6 +143,24 @@ docker login registry.example.com -u <your_username> -p <your_access_token> for errors (e.g. `/var/log/gitlab/gitlab-rails/production.log`). You may be able to find clues there. +#### Enable the registry debug server + +The optional debug server can be enabled by setting the registry debug address +in your `gitlab.rb` configuration. + +```ruby +registry['debug_addr'] = "localhost:5001" +``` + +After adding the setting, [reconfigure] GitLab to apply the change. + +Use curl to request debug output from the debug server: + +```bash +curl localhost:5001/debug/health +curl localhost:5001/debug/vars +``` + ### Advanced Troubleshooting >**NOTE:** The following section is only recommended for experts. @@ -275,3 +293,4 @@ Once the right permissions were set, the error will go away. [docker-docs]: https://docs.docker.com/engine/userguide/intro/ [pat]: ../profile/personal_access_tokens.md [pdt]: ../project/deploy_tokens/index.md +[reconfigure]: ../../administration/restart_gitlab.md#omnibus-gitlab-reconfigure
\ No newline at end of file diff --git a/doc/user/project/deploy_tokens/index.md b/doc/user/project/deploy_tokens/index.md index c09d5aeba8e..0b9b49f326f 100644 --- a/doc/user/project/deploy_tokens/index.md +++ b/doc/user/project/deploy_tokens/index.md @@ -5,7 +5,7 @@ Deploy tokens allow to download (through `git clone`), or read the container registry images of a project without the need of having a user and a password. Please note, that the expiration of deploy tokens happens on the date you define, -at midnight UTC and that they can be only managed by [masters](https://docs.gitlab.com/ee/user/permissions.html). +at midnight UTC and that they can be only managed by [maintainers](https://docs.gitlab.com/ee/user/permissions.html). ## Creating a Deploy Token diff --git a/doc/user/project/import/github.md b/doc/user/project/import/github.md index 8c639bd5343..fcd6192e82f 100644 --- a/doc/user/project/import/github.md +++ b/doc/user/project/import/github.md @@ -1,154 +1,145 @@ # Import your project from GitHub to GitLab -Import your projects from GitHub to GitLab with minimal effort. +Using the importer, you can import your GitHub repositories to GitLab.com or to +your self-hosted GitLab instance. ## Overview ->**Note:** -If you are an administrator you can enable the [GitHub integration][gh-import] -in your GitLab instance sitewide. This configuration is optional, users will -still be able to import their GitHub repositories with a -[personal access token][gh-token]. - ->**Note:** -Administrators of a GitLab instance (Community or Enterprise Edition) can also -use the [GitHub rake task][gh-rake] to import projects from GitHub without the -constrains of a Sidekiq worker. - -- At its current state, GitHub importer can import: - - the repository description (GitLab 7.7+) - - the Git repository data (GitLab 7.7+) - - the issues (GitLab 7.7+) - - the pull requests (GitLab 8.4+) - - the wiki pages (GitLab 8.4+) - - the milestones (GitLab 8.7+) - - the labels (GitLab 8.7+) - - the release note descriptions (GitLab 8.12+) - - the pull request review comments (GitLab 10.2+) - - the regular issue and pull request comments -- References to pull requests and issues are preserved (GitLab 8.7+) -- Repository public access is retained. If a repository is private in GitHub - it will be created as private in GitLab as well. +NOTE: **Note:** +While these instructions will always work for users on GitLab.com, if you are an +administrator of a self-hosted GitLab instance, you will need to enable the +[GitHub integration][gh-import] in order for users to follow the preferred +import method described on this page. If this is not enabled, users can alternatively import their +GitHub repositories using a [personal access token](#using-a-github-token) from GitHub, +but this method will not be able to associate all user activity (such as issues and pull requests) +with matching GitLab users. As an administrator of a self-hosted GitLab instance, you can also use +the [GitHub rake task](../../../administration/raketasks/github_import.md) to import projects from +GitHub without the constraints of a Sidekiq worker. + +The following aspects of a project are imported: + * Repository description (GitLab.com & 7.7+) + * Git repository data (GitLab.com & 7.7+) + * Issues (GitLab.com & 7.7+) + * Pull requests (GitLab.com & 8.4+) + * Wiki pages (GitLab.com & 8.4+) + * Milestones (GitLab.com & 8.7+) + * Labels (GitLab.com & 8.7+) + * Release note descriptions (GitLab.com & 8.12+) + * Pull request review comments (GitLab.com & 10.2+) + * Regular issue and pull request comments + +References to pull requests and issues are preserved (GitLab.com & 8.7+), and +each imported repository maintains visibility level unless that [visibility +level is restricted](../../../public_access/public_access.md#restricting-the-use-of-public-or-internal-projects), +in which case it defaults to the default project visibility. ## How it works -When issues/pull requests are being imported, the GitHub importer tries to find -the GitHub author/assignee in GitLab's database using the GitHub ID. For this -to work, the GitHub author/assignee should have signed in beforehand in GitLab -and **associated their GitHub account**. If the user is not -found in GitLab's database, the project creator (most of the times the current -user that started the import process) is set as the author, but a reference on -the issue about the original GitHub author is kept. +When issues and pull requests are being imported, the importer attempts to find their GitHub authors and +assignees in the database of the GitLab instance (note that pull requests are called "merge requests" in GitLab). -The importer will create any new namespaces (groups) if they don't exist or in -the case the namespace is taken, the repository will be imported under the user's -namespace that started the import process. +For this association to succeed, prior to the import, each GitHub author and assignee in the repository must +have either previously logged in to a GitLab account using the GitHub icon **or** have a GitHub account with +a [public email address](https://help.github.com/articles/setting-your-commit-email-address-on-github/) that +matches their GitLab account's email address. -The importer will also import branches on forks of projects related to open pull -requests. These branches will be imported with a naming scheme similar to -GH-SHA-Username/Pull-Request-number/fork-name/branch. This may lead to a discrepancy -in branches compared to the GitHub Repository. +If a user referenced in the project is not found in GitLab's database, the project creator (typically the user +that initiated the import process) is set as the author/assignee, but a note on the issue mentioning the original +GitHub author is added. -For a more technical description and an overview of the architecture you can -refer to [Working with the GitHub importer][gh-import-dev-docs]. +The importer creates any new namespaces (groups) if they do not exist, or, if the namespace is taken, the +repository is imported under the namespace of the user who initiated the import process. The namespace/repository +name can also be edited, with the proper permissions. -## Importing your GitHub repositories +The importer will also import branches on forks of projects related to open pull requests. These branches will be +imported with a naming scheme similar to `GH-SHA-username/pull-request-number/fork-name/branch`. This may lead to +a discrepancy in branches compared to those of the GitHub repository. -The importer page is visible when you create a new project. +For additional technical details, you can refer to the +[GitHub Importer](../../../development/github_importer.md "Working with the GitHub importer") +developer documentation. -![New project page on GitLab](img/import_projects_from_new_project_page.png) +## Import your GitHub repository into GitLab -Click on the **GitHub** link and the import authorization process will start. -There are two ways to authorize access to your GitHub repositories: +### Using the GitHub integration -1. [Using the GitHub integration][gh-integration] (if it's enabled by your - GitLab administrator). This is the preferred way as it's possible to - preserve the GitHub authors/assignees. Read more in the [How it works](#how-it-works) - section. -1. [Using a personal access token][gh-token] provided by GitHub. +Before you begin, ensure that any GitHub users who you want to map to GitLab users have either: -![Select authentication method](img/import_projects_from_github_select_auth_method.png) +1. A GitLab account that has logged in using the GitHub icon +\- or - +2. A GitLab account with an email address that matches the [public email address](https://help.github.com/articles/setting-your-commit-email-address-on-github/) of the GitHub user -### Authorize access to your repositories using the GitHub integration +User-matching attempts occur in that order, and if a user is not identified either way, the activity is associated with +the user account that is performing the import. -If the [GitHub integration][gh-import] is enabled by your GitLab administrator, -you can use it instead of the personal access token. +NOTE: **Note:** +If you are using a self-hosted GitLab instance, this process requires that you have configured the +[GitHub integration][gh-import]. -1. First you may want to connect your GitHub account to GitLab in order for - the username mapping to be correct. -1. Once you connect GitHub, click the **List your GitHub repositories** button - and you will be redirected to GitHub for permission to access your projects. -1. After accepting, you'll be automatically redirected to the importer. +1. From the top navigation bar, click **+** and select **New project**. +2. Select the **Import project** tab and then select **GitHub**. +3. Select the first button to **List your GitHub repositories**. You are redirected to a page on github.com to authorize the GitLab application. +4. Click **Authorize gitlabhq**. You are redirected back to GitLab's Import page and all of your GitHub repositories are listed. +5. Continue on to [selecting which repositories to import](#selecting-which-repositories-to-import). -You can now go on and [select which repositories to import](#select-which-repositories-to-import). +### Using a GitHub token -### Authorize access to your repositories using a personal access token +NOTE: **Note:** +For a proper author/assignee mapping for issues and pull requests, the [GitHub integration method (above)](#using-the-github-integration) +should be used instead of the personal access token. If you are using GitLab.com or a self-hosted GitLab instance with the GitHub +integration enabled, that should be the preferred method to import your repositories. Read more in the [How it works](#how-it-works) section. ->**Note:** -For a proper author/assignee mapping for issues and pull requests, the -[GitHub integration][gh-integration] should be used instead of the -[personal access token][gh-token]. If the GitHub integration is enabled by your -GitLab administrator, it should be the preferred method to import your repositories. -Read more in the [How it works](#how-it-works) section. +If you are not using the GitHub integration, you can still perform an authorization with GitHub to grant GitLab access your repositories: -If you are not using the GitHub integration, you can still perform a one-off -authorization with GitHub to grant GitLab access your repositories: +1. Go to https://github.com/settings/tokens/new +2. Enter a token description. +3. Select the repo scope. +4. Click **Generate token**. +5. Copy the token hash. +6. Go back to GitLab and provide the token to the GitHub importer. +7. Hit the **List Your GitHub Repositories** button and wait while GitLab reads your repositories' information. + Once done, you'll be taken to the importer page to select the repositories to import. -1. Go to <https://github.com/settings/tokens/new>. -1. Enter a token description. -1. Check the `repo` scope. -1. Click **Generate token**. -1. Copy the token hash. -1. Go back to GitLab and provide the token to the GitHub importer. -1. Hit the **List Your GitHub Repositories** button and wait while GitLab reads - your repositories' information. Once done, you'll be taken to the importer - page to select the repositories to import. +### Selecting which repositories to import -### Select which repositories to import +After you have authorized access to your GitHub repositories, you are redirected to the GitHub importer page and +your GitHub repositories are listed. -After you've authorized access to your GitHub repositories, you will be -redirected to the GitHub importer page. +1. By default, the proposed repository namespaces match the names as they exist in GitHub, but based on your permissions, + you can choose to edit these names before you proceed to import any of them. +2. Select the **Import** button next to any number of repositories, or select **Import all repositories**. +3. The **Status** column shows the import status of each repository. You can choose to leave the page open and it will + update in realtime or you can return to it later. +4. Once a repository has been imported, click its GitLab path to open its GitLab URL. -From there, you can see the import statuses of your GitHub repositories. +## Mirroring and pipeline status sharing -- Those that are being imported will show a _started_ status, -- those already successfully imported will be green with a _done_ status, -- whereas those that are not yet imported will have an **Import** button on the - right side of the table. +Depending your GitLab tier, [project mirroring](../../../workflow/repository_mirroring.md) can be set up to keep +your imported project in sync with its GitHub copy. -If you want, you can import all your GitHub projects in one go by hitting -**Import all projects** in the upper left corner. +Additionally, you can configure GitLab to send pipeline status updates back GitHub with the +[GitHub Project Integration](https://docs.gitlab.com/ee/user/project/integrations/github.html). **[PREMIUM]** -![GitHub importer page](img/import_projects_from_github_importer.png) +If you import your project using [CI/CD for external repo](https://docs.gitlab.com/ee/ci/ci_cd_for_external_repos/), then both +of the above are automatically configured. **[PREMIUM]** ---- +## Improving the speed of imports on self-hosted instances -You can also choose a different name for the project and a different namespace, -if you have the privileges to do so. +NOTE: **Note:** +Admin access to the GitLab server is required. -## Making the import process go faster - -For large projects it may take a while to import all data. To reduce the time -necessary you can increase the number of Sidekiq workers that process the -following queues: +For large projects it may take a while to import all data. To reduce the time necessary, you can increase the number of +Sidekiq workers that process the following queues: * `github_importer` * `github_importer_advance_stage` -For an optimal experience we recommend having at least 4 Sidekiq processes (each -running a number of threads equal to the number of CPU cores) that _only_ -process these queues. We also recommend that these processes run on separate -servers. For 4 servers with 8 cores this means you can import up to 32 objects -(e.g. issues) in parallel. +For an optimal experience, it's recommended having at least 4 Sidekiq processes (each running a number of threads equal +to the number of CPU cores) that *only* process these queues. It's also recommended that these processes run on separate +servers. For 4 servers with 8 cores this means you can import up to 32 objects (e.g., issues) in parallel. -Reducing the time spent in cloning a repository can be done by increasing -network throughput, CPU capacity, and disk performance (e.g. by using high -performance SSDs) of the disks that store the Git repositories (for your GitLab -instance). Increasing the number of Sidekiq workers will _not_ reduce the time -spent cloning repositories. +Reducing the time spent in cloning a repository can be done by increasing network throughput, CPU capacity, and disk +performance (e.g., by using high performance SSDs) of the disks that store the Git repositories (for your GitLab instance). +Increasing the number of Sidekiq workers will *not* reduce the time spent cloning repositories. [gh-import]: ../../../integration/github.md "GitHub integration" -[gh-rake]: ../../../administration/raketasks/github_import.md "GitHub rake task" -[gh-integration]: #authorize-access-to-your-repositories-using-the-github-integration -[gh-token]: #authorize-access-to-your-repositories-using-a-personal-access-token -[gh-import-dev-docs]: ../../../development/github_importer.md "Working with the GitHub importer" diff --git a/doc/user/project/import/img/import_projects_from_repo_url.png b/doc/user/project/import/img/import_projects_from_repo_url.png Binary files differindex ec867da1087..c453c7e558a 100644 --- a/doc/user/project/import/img/import_projects_from_repo_url.png +++ b/doc/user/project/import/img/import_projects_from_repo_url.png diff --git a/doc/user/project/integrations/index.md b/doc/user/project/integrations/index.md index e384ed57de9..363e994f36d 100644 --- a/doc/user/project/integrations/index.md +++ b/doc/user/project/integrations/index.md @@ -2,7 +2,7 @@ You can find the available integrations under your project's **Settings ➔ Integrations** page. You need to have at least -[master permission][permissions] on the project. +[maintainer permission][permissions] on the project. ## Project services diff --git a/doc/user/project/integrations/prometheus.md b/doc/user/project/integrations/prometheus.md index fa7e504c4aa..f687027e8c8 100644 --- a/doc/user/project/integrations/prometheus.md +++ b/doc/user/project/integrations/prometheus.md @@ -30,7 +30,7 @@ GitLab can seamlessly deploy and manage Prometheus on a [connected Kubernetes cl Once you have a connected Kubernetes cluster with Helm installed, deploying a managed Prometheus is as easy as a single click. -1. Go to the `CI/CD > Kubernetes` page, to view your connected clusters +1. Go to the `Operations > Kubernetes` page, to view your connected clusters 1. Select the cluster you would like to deploy Prometheus to 1. Click the **Install** button to deploy Prometheus to the cluster diff --git a/doc/user/project/integrations/prometheus_library/nginx_ingress.md b/doc/user/project/integrations/prometheus_library/nginx_ingress.md index 590b1c4275a..a1db79538a4 100644 --- a/doc/user/project/integrations/prometheus_library/nginx_ingress.md +++ b/doc/user/project/integrations/prometheus_library/nginx_ingress.md @@ -14,7 +14,7 @@ GitLab has support for automatically detecting and monitoring the Kubernetes NGI | ---- | ----- | | Throughput (req/sec) | sum(rate(nginx_upstream_responses_total{upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"}[2m])) by (status_code) | | Latency (ms) | avg(nginx_upstream_response_msecs_avg{upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"}) | -| HTTP Error Rate (HTTP Errors / sec) | sum(rate(nginx_upstream_responses_total{status_code="5xx", upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"}[2m])) | +| HTTP Error Rate (%) | sum(rate(nginx_upstream_responses_total{status_code="5xx", upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"}[2m])) / sum(rate(nginx_upstream_responses_total{upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"}[2m])) * 100 | ## Configuring NGINX ingress monitoring diff --git a/doc/user/project/issue_board.md b/doc/user/project/issue_board.md index 7eab825fa32..9ca1e6226c5 100644 --- a/doc/user/project/issue_board.md +++ b/doc/user/project/issue_board.md @@ -237,23 +237,25 @@ Issue Board, that is create/delete lists and drag issues around. ## Group Issue Board ->Introduced in GitLab 10.6 +> Introduced in [GitLab 10.6](https://about.gitlab.com/2018/03/22/gitlab-10-6-released/#single-group-issue-board-in-core-and-free) Group issue board is analogous to project-level issue board and it is accessible at the group navigation level. A group-level issue board allows you to view all issues from all projects in that group or descendant subgroups. Similarly, you can only filter by group labels for these boards. When updating milestones and labels for an issue through the sidebar update mechanism, again only group-level objects are available. +One group issue board per group was made available in GitLab 10.6 Core after multiple group issue boards were originally introduced in [GitLab 10.0 Premium](https://about.gitlab.com/2017/09/22/gitlab-10-0-released/#group-issue-boards). + ## Features per tier Different issue board features are available in different [GitLab tiers](https://about.gitlab.com/pricing/), as shown in the following table: -| Tier | Number of project issue boards | Board with configuration in project issue boards | Number of group issue boards | Board with configuration in group issue boards | -| --- | --- | --- | --- | --- | -| Core | 1 | No | 1 | No | -| Starter | Multiple | Yes | 1 | No | -| Premium | Multiple | Yes | Multiple | Yes | -| Ultimate | Multiple | Yes | Multiple | Yes | +| Tier | Number of Project Issue Boards | Number of Group Issue Boards | Configurable Project Issue Boards | Configurable Group Issue Boards | Assignee Lists +| --- | --- | --- | --- | --- | --- | +| Core | 1 | 1 | No | No | No | +| Starter | Multiple | 1 | Yes | No | No | +| Premium | Multiple | Multiple | Yes | Yes | Yes | +| Ultimate | Multiple | Multiple | Yes | Yes | Yes | ## Tips diff --git a/doc/user/project/issues/confidential_issues.md b/doc/user/project/issues/confidential_issues.md index 0bf1f396f9d..8eada25234f 100644 --- a/doc/user/project/issues/confidential_issues.md +++ b/doc/user/project/issues/confidential_issues.md @@ -71,10 +71,10 @@ least [Reporter access][permissions]. However, a guest user can also create confidential issues, but can only view the ones that they created themselves. Confidential issues are also hidden in search results for unprivileged users. -For example, here's what a user with Master and Guest access sees in the +For example, here's what a user with Maintainer and Guest access sees in the project's search results respectively. -| Master access | Guest access | +| Maintainer access | Guest access | | :-----------: | :----------: | | ![Confidential issues search master](img/confidential_issues_search_master.png) | ![Confidential issues search guest](img/confidential_issues_search_guest.png) | diff --git a/doc/user/project/members/index.md b/doc/user/project/members/index.md index 43713855e26..2c2e8e2d556 100644 --- a/doc/user/project/members/index.md +++ b/doc/user/project/members/index.md @@ -4,7 +4,7 @@ You can manage the groups and users and their access levels in all of your projects. You can also personalize the access level you give each user, per-project. -You should have `master` or `owner` [permissions](../../permissions.md) to add +You should have Maintainer or Owner [permissions](../../permissions.md) to add or import a new user to your project. To view, edit, add, and remove project's members, go to your @@ -43,7 +43,7 @@ level to the project. You can import another project's users in your own project by hitting the **Import members** button on the upper right corner of the **Members** menu. -In the dropdown menu, you can see only the projects you are Master on. +In the dropdown menu, you can see only the projects you are Maintainer on. ![Import members from another project](img/add_user_import_members_from_another_project.png) @@ -99,7 +99,7 @@ side of your screen. --- -Project owners & masters will be notified of your request and will be able to approve or +Project owners & maintainers will be notified of your request and will be able to approve or decline it on the members page. ![Manage access requests](img/access_requests_management.png) diff --git a/doc/user/project/members/share_project_with_groups.md b/doc/user/project/members/share_project_with_groups.md index 5d819998dd9..611ff0e6bfb 100644 --- a/doc/user/project/members/share_project_with_groups.md +++ b/doc/user/project/members/share_project_with_groups.md @@ -42,7 +42,7 @@ Admins are able to share projects with any group in the system. ## Maximum access level -In the example above, the maximum access level of 'Developer' for members from 'Engineering' means that users with higher access levels in 'Engineering' ('Master' or 'Owner') will only have 'Developer' access to 'Project Acme'. +In the example above, the maximum access level of 'Developer' for members from 'Engineering' means that users with higher access levels in 'Engineering' ('Maintainer' or 'Owner') will only have 'Developer' access to 'Project Acme'. ## Share project with group lock diff --git a/doc/user/project/merge_requests/allow_collaboration.md b/doc/user/project/merge_requests/allow_collaboration.md new file mode 100644 index 00000000000..859ac92ef89 --- /dev/null +++ b/doc/user/project/merge_requests/allow_collaboration.md @@ -0,0 +1,20 @@ +# Allow collaboration on merge requests across forks + +> [Introduced][ce-17395] in GitLab 10.6. + +This feature is available for merge requests across forked projects that are +publicly accessible. It makes it easier for members of projects to +collaborate on merge requests across forks. + +When enabled for a merge request, members with merge access to the target +branch of the project will be granted write permissions to the source branch +of the merge request. + +The feature can only be enabled by users who already have push access to the +source project, and only lasts while the merge request is open. + +Enable this functionality while creating or editing a merge request: + +![Enable collaboration](./img/allow_collaboration.png) + +[ce-17395]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/17395 diff --git a/doc/user/project/merge_requests/authorization_for_merge_requests.md b/doc/user/project/merge_requests/authorization_for_merge_requests.md index 59b3fe7242c..79444ee5682 100644 --- a/doc/user/project/merge_requests/authorization_for_merge_requests.md +++ b/doc/user/project/merge_requests/authorization_for_merge_requests.md @@ -9,7 +9,7 @@ There are two main ways to have a merge request flow with GitLab: With the protected branch flow everybody works within the same GitLab project. -The project maintainers get Master access and the regular developers get +The project maintainers get Maintainer access and the regular developers get Developer access. The maintainers mark the authoritative branches as 'Protected'. @@ -18,7 +18,7 @@ The developers push feature branches to the project and create merge requests to have their feature branches reviewed and merged into one of the protected branches. -By default, only users with Master access can merge changes into a protected +By default, only users with Maintainer access can merge changes into a protected branch. **Advantages** @@ -32,7 +32,7 @@ branch. ## Forking workflow -With the forking workflow the maintainers get Master access and the regular +With the forking workflow the maintainers get Maintainer access and the regular developers get Reporter access to the authoritative repository, which prohibits them from pushing any changes to it. diff --git a/doc/user/project/merge_requests/img/allow_collaboration.png b/doc/user/project/merge_requests/img/allow_collaboration.png Binary files differnew file mode 100644 index 00000000000..75596e7d9ad --- /dev/null +++ b/doc/user/project/merge_requests/img/allow_collaboration.png diff --git a/doc/user/project/merge_requests/img/allow_maintainer_push.png b/doc/user/project/merge_requests/img/allow_maintainer_push.png Binary files differdeleted file mode 100644 index 91cc399f4ff..00000000000 --- a/doc/user/project/merge_requests/img/allow_maintainer_push.png +++ /dev/null diff --git a/doc/user/project/merge_requests/index.md b/doc/user/project/merge_requests/index.md index b75bcacc9d7..5e2e0c3d171 100644 --- a/doc/user/project/merge_requests/index.md +++ b/doc/user/project/merge_requests/index.md @@ -28,7 +28,7 @@ With GitLab merge requests, you can: - Enable [fast-forward merge requests](#fast-forward-merge-requests) - Enable [semi-linear history merge requests](#semi-linear-history-merge-requests) as another security layer to guarantee the pipeline is passing in the target branch - [Create new merge requests by email](#create-new-merge-requests-by-email) -- Allow maintainers of the target project to push directly to the fork by [allowing edits from maintainers](maintainer_access.md) +- [Allow collaboration](allow_collaboration.md) so members of the target project can push directly to the fork - [Squash and merge](squash_and_merge.md) for a cleaner commit history With **[GitLab Enterprise Edition][ee]**, you can also: @@ -85,7 +85,7 @@ request is merged. This option is also visible in an existing merge request next to the merge request button and can be selected/deselected before merging. It's only visible -to users with [Master permissions](../../permissions.md) in the source project. +to users with [Maintainer permissions](../../permissions.md) in the source project. If the user viewing the merge request does not have the correct permissions to remove the source branch and the source branch is set for removal, the merge diff --git a/doc/user/project/merge_requests/maintainer_access.md b/doc/user/project/merge_requests/maintainer_access.md index 89f71e16a50..d59afecd375 100644 --- a/doc/user/project/merge_requests/maintainer_access.md +++ b/doc/user/project/merge_requests/maintainer_access.md @@ -1,20 +1 @@ -# Allow maintainer pushes for merge requests across forks - -> [Introduced][ce-17395] in GitLab 10.6. - -This feature is available for merge requests across forked projects that are -publicly accessible. It makes it easier for maintainers of projects to -collaborate on merge requests across forks. - -When enabled for a merge request, members with merge access to the target -branch of the project will be granted write permissions to the source branch -of the merge request. - -The feature can only be enabled by users who already have push access to the -source project, and only lasts while the merge request is open. - -Enable this functionality while creating a merge request: - -![Enable maintainer edits](./img/allow_maintainer_push.png) - -[ce-17395]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/17395 +This document was moved to [another location](allow_collaboration.md). diff --git a/doc/user/project/milestones/index.md b/doc/user/project/milestones/index.md index 64bb33be547..632253db94c 100644 --- a/doc/user/project/milestones/index.md +++ b/doc/user/project/milestones/index.md @@ -10,7 +10,6 @@ Milestones allow you to organize issues and merge requests into a cohesive group - **Project milestones** can be assigned to issues or merge requests in that project only. - **Group milestones** can be assigned to any issue or merge request of any project in that group. -- In the [future](https://gitlab.com/gitlab-org/gitlab-ce/issues/36862), you will be able to assign group milestones to issues and merge requests of projects in [subgroups](../../group/subgroups/index.md). ## Creating milestones diff --git a/doc/user/project/pages/index.md b/doc/user/project/pages/index.md index 862e4a3b466..205f0283107 100644 --- a/doc/user/project/pages/index.md +++ b/doc/user/project/pages/index.md @@ -42,7 +42,7 @@ to secure them. Your files live in a project [repository](../repository/index.md) on GitLab. [GitLab CI](../../../ci/README.md) picks up those files and makes them available at, typically, -`http://<username>.gitlab.io/<projectname>`. Please read through the docs on +`https://<username>.gitlab.io/<projectname>`. Please read through the docs on [GitLab Pages domains](getting_started_part_one.md#gitlab-pages-domain) for more info. ## Explore GitLab Pages diff --git a/doc/user/project/protected_branches.md b/doc/user/project/protected_branches.md index 0cbb0c878c2..3bf63a22963 100644 --- a/doc/user/project/protected_branches.md +++ b/doc/user/project/protected_branches.md @@ -10,8 +10,8 @@ created protected branches. By default, a protected branch does four simple things: - it prevents its creation, if not already created, from everybody except users - with Master permission -- it prevents pushes from everybody except users with Master permission + with Maintainer permission +- it prevents pushes from everybody except users with Maintainer permission - it prevents **anyone** from force pushing to the branch - it prevents **anyone** from deleting the branch @@ -24,7 +24,7 @@ See the [Changelog](#changelog) section for changes over time. ## Configuring protected branches -To protect a branch, you need to have at least Master permission level. Note +To protect a branch, you need to have at least Maintainer permission level. Note that the `master` branch is protected by default. 1. Navigate to your project's **Settings ➔ Repository** @@ -45,19 +45,19 @@ that the `master` branch is protected by default. Since GitLab 8.11, we added another layer of branch protection which provides more granular management of protected branches. The "Developers can push" option was replaced by an "Allowed to push" setting which can be set to -allow/prohibit Masters and/or Developers to push to a protected branch. +allow/prohibit Maintainers and/or Developers to push to a protected branch. Using the "Allowed to push" and "Allowed to merge" settings, you can control the actions that different roles can perform with the protected branch. For example, you could set "Allowed to push" to "No one", and "Allowed to merge" -to "Developers + Masters", to require _everyone_ to submit a merge request for +to "Developers + Maintainers", to require _everyone_ to submit a merge request for changes going into the protected branch. This is compatible with workflows like the [GitLab workflow](../../workflow/gitlab_flow.md). However, there are workflows where that is not needed, and only protecting from force pushes and branch removal is useful. For those workflows, you can allow everyone with write access to push to a protected branch by setting -"Allowed to push" to "Developers + Masters". +"Allowed to push" to "Developers + Maintainers". You can set the "Allowed to push" and "Allowed to merge" options while creating a protected branch or afterwards by selecting the option you want from the @@ -66,7 +66,7 @@ dropdown list in the "Already protected" area. ![Developers can push](img/protected_branches_devs_can_push.png) If you don't choose any of those options while creating a protected branch, -they are set to "Masters" by default. +they are set to "Maintainers" by default. ## Wildcard protected branches @@ -101,7 +101,7 @@ all matching branches: From time to time, it may be required to delete or clean up branches that are protected. -User with [Master permissions][perm] and up can manually delete protected +User with [Maintainer permissions][perm] and up can manually delete protected branches via GitLab's web interface: 1. Visit **Repository > Branches** diff --git a/doc/user/project/protected_tags.md b/doc/user/project/protected_tags.md index 0cb7aefdb2f..a5eaf2e9835 100644 --- a/doc/user/project/protected_tags.md +++ b/doc/user/project/protected_tags.md @@ -8,12 +8,12 @@ This feature evolved out of [Protected Branches](protected_branches.md) ## Overview -Protected tags will prevent anyone from updating or deleting the tag, as and will prevent creation of matching tags based on the permissions you have selected. By default, anyone without Master permission will be prevented from creating tags. +Protected tags will prevent anyone from updating or deleting the tag, as and will prevent creation of matching tags based on the permissions you have selected. By default, anyone without Maintainer permission will be prevented from creating tags. ## Configuring protected tags -To protect a tag, you need to have at least Master permission level. +To protect a tag, you need to have at least Maintainer permission level. 1. Navigate to the project's Settings -> Repository page diff --git a/doc/user/project/quick_actions.md b/doc/user/project/quick_actions.md index 2f4ed3493c2..0ef8eddad20 100644 --- a/doc/user/project/quick_actions.md +++ b/doc/user/project/quick_actions.md @@ -42,3 +42,4 @@ do. | `/tableflip` | Append the comment with `(╯°□°)╯︵ ┻━┻` | | `/shrug` | Append the comment with `¯\_(ツ)_/¯` | | <code>/copy_metadata #issue | !merge_request</code> | Copy labels and milestone from other issue or merge request | +| `/confidential` | Makes the issue confidential |
\ No newline at end of file diff --git a/doc/user/project/settings/import_export.md b/doc/user/project/settings/import_export.md index b8bac01959e..9034a9b5179 100644 --- a/doc/user/project/settings/import_export.md +++ b/doc/user/project/settings/import_export.md @@ -19,7 +19,7 @@ > - The exports are stored in a temporary [shared directory][tmp] and are deleted > every 24 hours by a specific worker. > - Group members will get exported as project members, as long as the user has -> master or admin access to the group where the exported project lives. An admin +> maintainer or admin access to the group where the exported project lives. An admin > in the import side is required to map the users, based on email or username. > Otherwise, a supplementary comment is left to mention the original author and > the MRs, notes or issues will be owned by the importer. diff --git a/doc/user/project/settings/index.md b/doc/user/project/settings/index.md index c9d2f8dc32d..212e271ce6f 100644 --- a/doc/user/project/settings/index.md +++ b/doc/user/project/settings/index.md @@ -1,7 +1,7 @@ # Project settings NOTE: **Note:** -Only project Masters and Admin users have the [permissions] to access a project +Only project Maintainers and Admin users have the [permissions] to access a project settings. You can adjust your [project](../index.md) settings by navigating @@ -74,7 +74,7 @@ To archive a project: #### Renaming a repository NOTE: **Note:** -Only project Masters and Admin users have the [permissions] to rename a +Only project Maintainers and Admin users have the [permissions] to rename a repository. Not to be confused with a project's name where it can also be changed from the [general project settings](#general-project-settings). @@ -98,7 +98,7 @@ Only project Owners and Admin users have the [permissions] to transfer a project You can transfer an existing project into a [group](../../group/index.md) if: -1. you have at least **Master** [permissions] to that group +1. you have at least **Maintainer** [permissions] to that group 1. you are an **Owner** of the project. Similarly, if you are an owner of a group, you can transfer any of its projects diff --git a/doc/user/project/web_ide/index.md b/doc/user/project/web_ide/index.md index 105d8a6ab61..b0143e45ab6 100644 --- a/doc/user/project/web_ide/index.md +++ b/doc/user/project/web_ide/index.md @@ -42,5 +42,26 @@ list. An additional review mode is available when you open a merge request, which shows you a preview of the merge request diff if you commit your changes. +## View CI job logs + +> [Introduced in](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/19279) [GitLab Core][ce] 11.0. + +The Web IDE can be used to quickly fix failing tests by opening the branch or +merge request in the Web IDE and opening the logs of the failed job. The status +of all jobs for the most recent pipeline and job traces for the current commit +can be accessed by clicking the **Pipelines** button in the top right. + +The pipeline status is also shown at all times in the status bar in the bottom +left. + +## Switching merge requests + +> [Introduced in](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/19318) [GitLab Core][ce] 11.0. + +Switching between your authored and assigned merge requests can be done without +leaving the Web IDE. Click the project name in the top left to open a list of +merge requests. You will need to commit or discard all your changes before +switching to a different merge request. + [ce]: https://about.gitlab.com/pricing/ [ee]: https://about.gitlab.com/pricing/ diff --git a/doc/user/reserved_names.md b/doc/user/reserved_names.md index 50ec99be48b..918daee5d9f 100644 --- a/doc/user/reserved_names.md +++ b/doc/user/reserved_names.md @@ -59,6 +59,7 @@ Currently the following names are reserved as top level groups: - deploy.html - explore - favicon.ico +- favicon.png - groups - header_logo_dark.png - header_logo_light.png diff --git a/doc/workflow/gitlab_flow.md b/doc/workflow/gitlab_flow.md index 23b67310d25..a7313082fac 100644 --- a/doc/workflow/gitlab_flow.md +++ b/doc/workflow/gitlab_flow.md @@ -131,7 +131,7 @@ There is room for more feedback and after the assigned person feels comfortable If the assigned person does not feel comfortable they can close the merge request without merging. In GitLab it is common to protect the long-lived branches (e.g. the master branch) so that normal developers [can't modify these protected branches](http://docs.gitlab.com/ce/permissions/permissions.html). -So if you want to merge it into a protected branch you assign it to someone with master authorizations. +So if you want to merge it into a protected branch you assign it to someone with maintainer authorizations. ## Issue tracking with GitLab flow diff --git a/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md b/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md index 0d592a6d43e..ae161e43233 100644 --- a/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md +++ b/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md @@ -144,7 +144,7 @@ git lfs unlock --id=123 ``` If for some reason you need to unlock a file that was not locked by you, -you can use the `--force` flag as long as you have a `master` access on +you can use the `--force` flag as long as you have a `maintainer` access on the project: ```bash diff --git a/doc/workflow/notifications.md b/doc/workflow/notifications.md index fc592b99860..edb0c6bdc30 100644 --- a/doc/workflow/notifications.md +++ b/doc/workflow/notifications.md @@ -34,9 +34,14 @@ anything that is set at Global Settings. ![notification settings](img/notification_group_settings.png) -Group Settings are taking precedence over Global Settings but are on a level below Project Settings. +Group Settings are taking precedence over Global Settings but are on a level below Project or Subgroup Settings: + +``` +Group < Subgroup < Project +``` + This means that you can set a different level of notifications per group while still being able -to have a finer level setting per project. +to have a finer level setting per project or subgroup. Organization like this is suitable for users that belong to different groups but don't have the same need for being notified for every group they are member of. These settings can be configured on group page under the name of the group. It will be the dropdown with the bell icon. They can also be configured on the user profile notifications dropdown. diff --git a/doc/workflow/repository_mirroring.md b/doc/workflow/repository_mirroring.md index dbe63144e38..8c4e6ea8eab 100644 --- a/doc/workflow/repository_mirroring.md +++ b/doc/workflow/repository_mirroring.md @@ -1,6 +1,6 @@ # Repository mirroring -Repository Mirroring is a way to mirror repositories from external sources. +Repository mirroring is a way to mirror repositories from external sources. It can be used to mirror all branches, tags, and commits that you have in your repository. @@ -34,14 +34,201 @@ A few things/limitations to consider: - The Git LFS objects will not be synced. You'll need to push/pull them manually. -## Use-case +## Use cases +- You migrated to GitLab but still need to keep your project in another source. + In that case, you can simply set it up to mirror to GitLab (pull) and all the + essential history of commits, tags and branches will be available in your + GitLab instance. - You have old projects in another source that you don't use actively anymore, but don't want to remove for archiving purposes. In that case, you can create a push mirror so that your active GitLab repository can push its changes to the old location. -## Pushing to a remote repository **[STARTER]** +## Pulling from a remote repository **[STARTER]** + +>[Introduced][ee-51] in GitLab Enterprise Edition 8.2. + +You can set up a repository to automatically have its branches, tags, and commits +updated from an upstream repository. This is useful when a repository you're +interested in is located on a different server, and you want to be able to +browse its content and its activity using the familiar GitLab interface. + +When creating a new project, you can enable repository mirroring when you choose +to import the repository from "Any repo by URL". Enter the full URL of the Git +repository to pull from and click on the **Mirror repository** checkbox. + +![New project](repository_mirroring/repository_mirroring_new_project.png) + +For an existing project, you can set up mirror pulling by visiting your project's +**Settings ➔ Repository** and searching for the "Pull from a remote repository" +section. Check the "Mirror repository" box and hit **Save changes** at the bottom. +You have a few options to choose from one being the user who will be the author +of all events in the activity feed that are the result of an update. This user +needs to have at least [master access][perms] to the project. Another option is +whether you want to trigger builds for mirror updates. + +![Pull settings](repository_mirroring/repository_mirroring_pull_settings.png) + +Since the repository on GitLab functions as a mirror of the upstream repository, +you are advised not to push commits directly to the repository on GitLab. +Instead, any commits should be pushed to the upstream repository, and will end +up in the GitLab repository automatically within a certain period of time +or when a [forced update](#forcing-an-update) is initiated. + +If you do manually update a branch in the GitLab repository, the branch will +become diverged from upstream, and GitLab will no longer automatically update +this branch to prevent any changes from being lost. + +![Diverged branch](repository_mirroring/repository_mirroring_diverged_branch.png) + +### Trigger update using API **[STARTER]** + +>[Introduced][ee-3453] in GitLab Enterprise Edition 10.3. + +Pull mirroring uses polling to detect new branches and commits added upstream, +often many minutes afterwards. If you notify GitLab by [API][pull-api], updates +will be pulled immediately. + +Read the [Pull Mirror Trigger API docs][pull-api]. + +### Pull only protected branches **[STARTER]** + +>[Introduced][ee-3326] in GitLab Enterprise Edition 10.3. + +You can choose to only pull the protected branches from your remote repository to GitLab. + +To use this option go to your project's repository settings page under pull mirror. + +### Overwrite diverged branches **[STARTER]** + +>[Introduced][ee-4559] in GitLab Enterprise Edition 10.6. + +You can choose to always update your local branch with the remote version even +if your local version has diverged from the remote. + +To use this option go to your project's repository settings page under pull mirror. + +### Hard failure **[STARTER]** + +>[Introduced][ee-3117] in GitLab Enterprise Edition 10.2. + +Once a mirror gets retried 14 times in a row, it will get marked as hard failed, +this will become visible in either the project main dashboard or in the +pull mirror settings page. + +![Hard failed mirror main notice](repository_mirroring/repository_mirroring_hard_failed_main.png) + +![Hard failed mirror settings notice](repository_mirroring/repository_mirroring_hard_failed_settings.png) + +When a project is hard failed, it will no longer get picked up for mirroring. +A user can resume the project mirroring again by either [forcing an update](#forcing-an-update) +or by changing the import URL in repository settings. + +### SSH authentication **[STARTER]** + +> [Introduced][ee-2551] in GitLab Starter 9.5 + +If you're mirroring over SSH (i.e., an `ssh://` URL), you can authenticate using +password-based authentication, just as over HTTPS, but you can also use public +key authentication. This is often more secure than password authentication, +especially when the source repository supports [Deploy Keys][deploy-key]. + +To get started, navigate to **Settings ➔ Repository ➔ Pull from a remote repository**, +enable mirroring (if not already enabled) and enter an `ssh://` URL. + +> **NOTE**: SCP-style URLs, e.g., `git@example.com:group/project.git`, are not +supported at this time. + +Entering the URL adds two features to the page - `Fingerprints` and +`SSH public key authentication`: + +![Pull settings for SSH](repository_mirroring/repository_mirroring_pull_settings_for_ssh.png) + +SSH authentication is mutual. You have to prove to the server that you're +allowed to access the repository, but the server also has to prove to *you* that +it's who it claims to be. You provide your credentials as a password or public +key. The server that the source repository resides on provides its credentials +as a "host key", the fingerprint of which needs to be verified manually. + +Press the `Detect host keys` button. GitLab will fetch the host keys from the +server, and display the fingerprints to you: + +![Detect SSH host keys](repository_mirroring/repository_mirroring_detect_host_keys.png) + +You now need to verify that the fingerprints are those you expect. GitLab.com +and other code hosting sites publish their fingerprints in the open for you +to check: + +* [AWS CodeCommit](http://docs.aws.amazon.com/codecommit/latest/userguide/regions.html#regions-fingerprints) +* [Bitbucket](https://confluence.atlassian.com/bitbucket/use-the-ssh-protocol-with-bitbucket-cloud-221449711.html#UsetheSSHprotocolwithBitbucketCloud-KnownhostorBitbucket%27spublickeyfingerprints) +* [GitHub](https://help.github.com/articles/github-s-ssh-key-fingerprints/) +* [GitLab.com](https://about.gitlab.com/gitlab-com/settings/#ssh-host-keys-fingerprints) +* [Launchpad](https://help.launchpad.net/SSHFingerprints) +* [Savannah](http://savannah.gnu.org/maintenance/SshAccess/) +* [SourceForge](https://sourceforge.net/p/forge/documentation/SSH%20Key%20Fingerprints/) + +Other providers will vary. If you're running on-premises GitLab, or otherwise +have access to the source server, you can securely gather the key fingerprints: + +``` +$ cat /etc/ssh/ssh_host*pub | ssh-keygen -E md5 -l -f - +256 MD5:f4:28:9f:23:99:15:21:1b:bf:ed:1f:8e:a0:76:b2:9d root@example.com (ECDSA) +256 MD5:e6:eb:45:8a:3c:59:35:5f:e9:5b:80:12:be:7e:22:73 root@example.com (ED25519) +2048 MD5:3f:72:be:3d:62:03:5c:62:83:e8:6e:14:34:3a:85:1d root@example.com (RSA) +``` + +(You may need to exclude `-E md5` for some older versions of SSH). + +If you're an SSH expert and already have a `known_hosts` file you'd like to use +unaltered, then you can skip these steps. Just press the "Show advanced" button +and paste in the file contents: + +![Advanced SSH host key management](repository_mirroring/repository_mirroring_pull_advanced_host_keys.png) + +Once you've **carefully verified** that all the fingerprints match your trusted +source, you can press `Save changes`. This will record the host keys, along with +the person who verified them (you!) and the date: + +![SSH host keys submitted](repository_mirroring/repository_mirroring_ssh_host_keys_verified.png) + +When pulling changes from the source repository, GitLab will now check that at +least one of the stored host keys matches before connecting. This can prevent +malicious code from being injected into your mirror, or your password being +stolen! + +To use SSH public key authentication, you'll also need to choose that option +from the authentication methods dropdown. GitLab will generate a 4096-bit RSA +key and display the public component of that key to you: + +![SSH public key authentication](repository_mirroring/repository_mirroring_ssh_public_key_authentication.png) + +You then need to add the public SSH key to the source repository configuration. +If the source is hosted on GitLab, you should add it as a [Deploy Key][deploy-key]. +Other sources may require you to add the key to your user's `authorized_keys` +file - just paste the entire `ssh-rsa AAA.... user@host` block into the file on +its own line and save it. + +Once the public key is set up on the source repository, press `Save changes` and your +mirror will begin working. + +If you need to change the key at any time, you can press the `Regenerate key` +button to do so. You'll have to update the source repository with the new key +to keep the mirror running. + +### How it works + +Once you activate the pull mirroring feature, the mirror will be inserted into +a queue. A scheduler will start every minute and schedule a fixed amount of +mirrors for update, based on the configured maximum capacity. + +If the mirror successfully updates it will be enqueued once again with a small +backoff period. + +If the mirror fails (eg: branch diverged from upstream), the project's backoff +period will be penalized each time it fails up to a maximum amount of time. + +## Pushing to a remote repository >[Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/249) in GitLab Enterprise Edition 8.7. [Moved to GitLab Community Edition][ce-18715] in 10.8. @@ -83,7 +270,7 @@ To use this option go to your project's repository settings page under push mirr To set up a mirror from GitLab to GitHub, you need to follow these steps: 1. Create a [GitHub personal access token](https://help.github.com/articles/creating-a-personal-access-token-for-the-command-line/) with the "public_repo" box checked: - + ![edit personal access token GitHub](repository_mirroring/repository_mirroring_github_edit_personal_access_token.png) 1. Fill in the "Git repository URL" with the personal access token replacing the password `https://GitHubUsername:GitHubPersonalAccessToken@github.com/group/project.git`: @@ -94,7 +281,7 @@ To set up a mirror from GitLab to GitHub, you need to follow these steps: 1. And either wait or trigger the "Update Now" button: ![update now](repository_mirroring/repository_mirroring_gitlab_push_to_a_remote_repository_update_now.png) - + ## Forcing an update While mirrors are scheduled to update automatically, you can always force an update @@ -105,7 +292,60 @@ by using the **Update now** button which is exposed in various places: - in the tags page - in the **Mirror repository** settings page +## Bidirectional mirroring + +CAUTION: **Warning:** +There is no bidirectional support without conflicts. If you +configure a repository to pull and push to a second remote, there is no +guarantee that it will update correctly on both remotes. If you configure +a repository for bidirectional mirroring, you should consider when conflicts +occur who and how they will be resolved. + +Rewriting any mirrored commit on either remote will cause conflicts and +mirroring to fail. This can be prevented by [only pulling protected branches]( +#pull-only-protected-branches) and [only pushing protected branches]( +#push-only-protected-branches). You should protect the branches you wish to +mirror on both remotes to prevent conflicts caused by rewriting history. + +Bidirectional mirroring also creates a race condition where commits to the same +branch in close proximity will cause conflicts. The race condition can be +mitigated by reducing the mirroring delay by using a Push event webhook to +trigger an immediate pull to GitLab. Push mirroring from GitLab is rate limited +to once per minute when only push mirroring protected branches. + +It may be possible to implement a locking mechanism using the server-side +`pre-receive` hook to prevent the race condition. Read about [configuring +custom Git hooks][hooks] on the GitLab server. + +### Mirroring with Perforce via GitFusion + +CAUTION: **Warning:** +Bidirectional mirroring should not be used as a permanent +configuration. There is no bidirectional mirroring without conflicts. +Refer to [Migrating from Perforce Helix][perforce] for alternative migration +approaches. + +GitFusion provides a Git interface to Perforce which can be used by GitLab to +bidirectionally mirror projects with GitLab. This may be useful in some +situations when migrating from Perforce to GitLab where overlapping Perforce +workspaces cannot be migrated simultaneously to GitLab. + +If using mirroring with Perforce you should only mirror protected branches. +Perforce will reject any pushes that rewrite history. It is recommended that +only the fewest number of branches are mirrored due to the performance +limitations of GitFusion. + +[ee-51]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/51 +[ee-2551]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/2551 +[ee-3117]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/3117 +[ee-3326]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/3326 [ee-3350]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/3350 +[ee-3453]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/3453 +[ee-4559]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/4559 [ce-18715]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/18715 [perms]: ../user/permissions.md - +[hooks]: ../administration/custom_hooks.md +[deploy-key]: ../ssh/README.md#deploy-keys +[webhook]: ../user/project/integrations/webhooks.md#push-events +[pull-api]: ../api/projects.md#start-the-pull-mirroring-process-for-a-project +[perforce]: ../user/project/import/perforce.md diff --git a/doc/workflow/repository_mirroring/repository_mirroring_detect_host_keys.png b/doc/workflow/repository_mirroring/repository_mirroring_detect_host_keys.png Binary files differnew file mode 100644 index 00000000000..333648942f8 --- /dev/null +++ b/doc/workflow/repository_mirroring/repository_mirroring_detect_host_keys.png diff --git a/doc/workflow/repository_mirroring/repository_mirroring_diverged_branch.png b/doc/workflow/repository_mirroring/repository_mirroring_diverged_branch.png Binary files differnew file mode 100644 index 00000000000..45c9bce0889 --- /dev/null +++ b/doc/workflow/repository_mirroring/repository_mirroring_diverged_branch.png diff --git a/doc/workflow/repository_mirroring/repository_mirroring_hard_failed_main.png b/doc/workflow/repository_mirroring/repository_mirroring_hard_failed_main.png Binary files differnew file mode 100644 index 00000000000..99d429a1802 --- /dev/null +++ b/doc/workflow/repository_mirroring/repository_mirroring_hard_failed_main.png diff --git a/doc/workflow/repository_mirroring/repository_mirroring_hard_failed_settings.png b/doc/workflow/repository_mirroring/repository_mirroring_hard_failed_settings.png Binary files differnew file mode 100644 index 00000000000..0ab07afa3cc --- /dev/null +++ b/doc/workflow/repository_mirroring/repository_mirroring_hard_failed_settings.png diff --git a/doc/workflow/repository_mirroring/repository_mirroring_new_project.png b/doc/workflow/repository_mirroring/repository_mirroring_new_project.png Binary files differnew file mode 100644 index 00000000000..43bf304838f --- /dev/null +++ b/doc/workflow/repository_mirroring/repository_mirroring_new_project.png diff --git a/doc/workflow/repository_mirroring/repository_mirroring_pull_advanced_host_keys.png b/doc/workflow/repository_mirroring/repository_mirroring_pull_advanced_host_keys.png Binary files differnew file mode 100644 index 00000000000..5da5a7436bb --- /dev/null +++ b/doc/workflow/repository_mirroring/repository_mirroring_pull_advanced_host_keys.png diff --git a/doc/workflow/repository_mirroring/repository_mirroring_pull_settings.png b/doc/workflow/repository_mirroring/repository_mirroring_pull_settings.png Binary files differnew file mode 100644 index 00000000000..4b9085302a1 --- /dev/null +++ b/doc/workflow/repository_mirroring/repository_mirroring_pull_settings.png diff --git a/doc/workflow/repository_mirroring/repository_mirroring_pull_settings_for_ssh.png b/doc/workflow/repository_mirroring/repository_mirroring_pull_settings_for_ssh.png Binary files differnew file mode 100644 index 00000000000..8c2efdafa43 --- /dev/null +++ b/doc/workflow/repository_mirroring/repository_mirroring_pull_settings_for_ssh.png diff --git a/doc/workflow/repository_mirroring/repository_mirroring_ssh_host_keys_verified.png b/doc/workflow/repository_mirroring/repository_mirroring_ssh_host_keys_verified.png Binary files differnew file mode 100644 index 00000000000..93f3a532a0e --- /dev/null +++ b/doc/workflow/repository_mirroring/repository_mirroring_ssh_host_keys_verified.png diff --git a/doc/workflow/repository_mirroring/repository_mirroring_ssh_public_key_authentication.png b/doc/workflow/repository_mirroring/repository_mirroring_ssh_public_key_authentication.png Binary files differnew file mode 100644 index 00000000000..6997ad511d9 --- /dev/null +++ b/doc/workflow/repository_mirroring/repository_mirroring_ssh_public_key_authentication.png diff --git a/doc_styleguide.md b/doc_styleguide.md deleted file mode 100644 index 05ff46323ac..00000000000 --- a/doc_styleguide.md +++ /dev/null @@ -1,3 +0,0 @@ -# Documentation styleguide - -Moved to [development/doc_styleguide](doc/development/doc_styleguide.md). diff --git a/lib/api/api.rb b/lib/api/api.rb index 7ea575a9661..e2ad3c5f4e3 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -83,6 +83,7 @@ module API # Keep in alphabetical order mount ::API::AccessRequests mount ::API::Applications + mount ::API::Avatar mount ::API::AwardEmoji mount ::API::Badges mount ::API::Boards diff --git a/lib/api/avatar.rb b/lib/api/avatar.rb new file mode 100644 index 00000000000..70219bc8ea0 --- /dev/null +++ b/lib/api/avatar.rb @@ -0,0 +1,21 @@ +module API + class Avatar < Grape::API + resource :avatar do + desc 'Return avatar url for a user' do + success Entities::Avatar + end + params do + requires :email, type: String, desc: 'Public email address of the user' + optional :size, type: Integer, desc: 'Single pixel dimension for Gravatar images' + end + get do + forbidden!('Unauthorized access') unless can?(current_user, :read_users_list) + + user = User.find_by_public_email(params[:email]) + user ||= User.new(email: params[:email]) + + present user, with: Entities::Avatar, size: params[:size] + end + end + end +end diff --git a/lib/api/commits.rb b/lib/api/commits.rb index 684955a1b24..964780cba6a 100644 --- a/lib/api/commits.rb +++ b/lib/api/commits.rb @@ -15,19 +15,21 @@ module API end params do optional :ref_name, type: String, desc: 'The name of a repository branch or tag, if not given the default branch is used' - optional :since, type: DateTime, desc: 'Only commits after or on this date will be returned' - optional :until, type: DateTime, desc: 'Only commits before or on this date will be returned' - optional :path, type: String, desc: 'The file path' - optional :all, type: Boolean, desc: 'Every commit will be returned' + optional :since, type: DateTime, desc: 'Only commits after or on this date will be returned' + optional :until, type: DateTime, desc: 'Only commits before or on this date will be returned' + optional :path, type: String, desc: 'The file path' + optional :all, type: Boolean, desc: 'Every commit will be returned' + optional :with_stats, type: Boolean, desc: 'Stats about each commit will be added to the response' use :pagination end get ':id/repository/commits' do - path = params[:path] + path = params[:path] before = params[:until] - after = params[:since] - ref = params[:ref_name] || user_project.try(:default_branch) || 'master' unless params[:all] + after = params[:since] + ref = params[:ref_name] || user_project.try(:default_branch) || 'master' unless params[:all] offset = (params[:page] - 1) * params[:per_page] - all = params[:all] + all = params[:all] + with_stats = params[:with_stats] commits = user_project.repository.commits(ref, path: path, @@ -47,7 +49,9 @@ module API paginated_commits = Kaminari.paginate_array(commits, total_count: commit_count) - present paginate(paginated_commits), with: Entities::Commit + serializer = with_stats ? Entities::CommitWithStats : Entities::Commit + + present paginate(paginated_commits), with: serializer end desc 'Commit multiple file changes as one commit' do diff --git a/lib/api/entities.rb b/lib/api/entities.rb index c4537036a3a..1cc8fcb8408 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -308,6 +308,10 @@ module API expose :additions, :deletions, :total end + class CommitWithStats < Commit + expose :stats, using: Entities::CommitStats + end + class CommitDetail < Commit expose :stats, using: Entities::CommitStats, if: :stats expose :status @@ -358,7 +362,7 @@ module API end class Snippet < Grape::Entity - expose :id, :title, :file_name, :description + expose :id, :title, :file_name, :description, :visibility expose :author, using: Entities::UserBasic expose :updated_at, :created_at expose :project_id @@ -412,6 +416,10 @@ module API expose :state, :created_at, :updated_at expose :due_date expose :start_date + + expose :web_url do |milestone, _options| + Gitlab::UrlBuilder.build(milestone) + end end class IssueBasic < ProjectEntity @@ -559,7 +567,9 @@ module API expose :discussion_locked expose :should_remove_source_branch?, as: :should_remove_source_branch expose :force_remove_source_branch?, as: :force_remove_source_branch - expose :allow_maintainer_to_push, if: -> (merge_request, _) { merge_request.for_fork? } + expose :allow_collaboration, if: -> (merge_request, _) { merge_request.for_fork? } + # Deprecated + expose :allow_collaboration, as: :allow_maintainer_to_push, if: -> (merge_request, _) { merge_request.for_fork? } expose :web_url do |merge_request, options| Gitlab::UrlBuilder.build(merge_request) @@ -692,6 +702,12 @@ module API expose :notes, using: Entities::Note end + class Avatar < Grape::Entity + expose :avatar_url do |avatarable, options| + avatarable.avatar_url(only_path: false, size: options[:size]) + end + end + class AwardEmoji < Grape::Entity expose :id expose :name diff --git a/lib/api/events.rb b/lib/api/events.rb index b0713ff1d54..fc4ba5a3188 100644 --- a/lib/api/events.rb +++ b/lib/api/events.rb @@ -17,6 +17,7 @@ module API def present_events(events) events = events.reorder(created_at: params[:sort]) + .with_associations present paginate(events), with: Entities::Event end diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index 2ed331d4fd2..9c53b7c3fe7 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -272,7 +272,8 @@ module API attrs[key] = params_hash[key] end end - ActionController::Parameters.new(attrs).permit! + permitted_attrs = ActionController::Parameters.new(attrs).permit! + Gitlab.rails5? ? permitted_attrs.to_h : permitted_attrs end def filter_by_iid(items, iid) diff --git a/lib/api/helpers/runner.rb b/lib/api/helpers/runner.rb index 35ac0b4cbca..61eb88d3331 100644 --- a/lib/api/helpers/runner.rb +++ b/lib/api/helpers/runner.rb @@ -59,6 +59,11 @@ module API def max_artifacts_size Gitlab::CurrentSettings.max_artifacts_size.megabytes.to_i end + + def job_forbidden!(job, reason) + header 'Job-Status', job.status + forbidden!(reason) + end end end end diff --git a/lib/api/issues.rb b/lib/api/issues.rb index b64f465ce56..25185d6edc8 100644 --- a/lib/api/issues.rb +++ b/lib/api/issues.rb @@ -16,7 +16,7 @@ module API args[:scope] = args[:scope].underscore if args[:scope] issues = IssuesFinder.new(current_user, args).execute - .preload(:assignees, :labels, :notes, :timelogs, :project) + .preload(:assignees, :labels, :notes, :timelogs, :project, :author) issues.reorder(args[:order_by] => args[:sort]) end diff --git a/lib/api/jobs.rb b/lib/api/jobs.rb index 54d1acbd412..e95b0dd5267 100644 --- a/lib/api/jobs.rb +++ b/lib/api/jobs.rb @@ -54,6 +54,7 @@ module API pipeline = user_project.pipelines.find(params[:pipeline_id]) builds = pipeline.builds builds = filter_builds(builds, params[:scope]) + builds = builds.preload(:job_artifacts_archive) present paginate(builds), with: Entities::Job end diff --git a/lib/api/markdown.rb b/lib/api/markdown.rb index b9ed68aa584..5d55224c1a7 100644 --- a/lib/api/markdown.rb +++ b/lib/api/markdown.rb @@ -10,9 +10,7 @@ module API detail "This feature was introduced in GitLab 11.0." end post do - # Explicitly set CommonMark as markdown engine to use. - # Remove this set when https://gitlab.com/gitlab-org/gitlab-ce/issues/43011 is done. - context = { markdown_engine: :common_mark, only_path: false } + context = { only_path: false } if params[:project] project = Project.find_by_full_path(params[:project]) diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb index b1e510d72de..af7d2471b34 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -38,7 +38,7 @@ module API merge_requests = MergeRequestsFinder.new(current_user, args).execute .reorder(args[:order_by] => args[:sort]) merge_requests = paginate(merge_requests) - .preload(:target_project) + .preload(:source_project, :target_project) return merge_requests if args[:view] == 'simple' @@ -162,7 +162,8 @@ module API optional :milestone_id, type: Integer, desc: 'The ID of a milestone to assign the merge request' optional :labels, type: String, desc: 'Comma-separated list of label names' optional :remove_source_branch, type: Boolean, desc: 'Remove source branch when merging' - optional :allow_maintainer_to_push, type: Boolean, desc: 'Whether a maintainer of the target project can push to the source project' + optional :allow_collaboration, type: Boolean, desc: 'Allow commits from members who can merge to the target branch' + optional :allow_maintainer_to_push, type: Boolean, as: :allow_collaboration, desc: '[deprecated] See allow_collaboration' optional :squash, type: Grape::API::Boolean, desc: 'When true, the commits will be squashed into a single commit on merge' use :optional_params_ee diff --git a/lib/api/pipelines.rb b/lib/api/pipelines.rb index 735591fedd5..8374a57edfa 100644 --- a/lib/api/pipelines.rb +++ b/lib/api/pipelines.rb @@ -41,15 +41,20 @@ module API end params do requires :ref, type: String, desc: 'Reference' + optional :variables, Array, desc: 'Array of variables available in the pipeline' end post ':id/pipeline' do Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42124') authorize! :create_pipeline, user_project + pipeline_params = declared_params(include_missing: false) + .merge(variables_attributes: params[:variables]) + .except(:variables) + new_pipeline = Ci::CreatePipelineService.new(user_project, current_user, - declared_params(include_missing: false)) + pipeline_params) .execute(:api, ignore_skip_ci: true, save_on_errors: false) if new_pipeline.persisted? diff --git a/lib/api/protected_branches.rb b/lib/api/protected_branches.rb index aa7cab4a741..a30eb46c220 100644 --- a/lib/api/protected_branches.rb +++ b/lib/api/protected_branches.rb @@ -41,10 +41,10 @@ module API requires :name, type: String, desc: 'The name of the protected branch' optional :push_access_level, type: Integer, values: ProtectedRefAccess::ALLOWED_ACCESS_LEVELS, - desc: 'Access levels allowed to push (defaults: `40`, master access level)' + desc: 'Access levels allowed to push (defaults: `40`, maintainer access level)' optional :merge_access_level, type: Integer, values: ProtectedRefAccess::ALLOWED_ACCESS_LEVELS, - desc: 'Access levels allowed to merge (defaults: `40`, master access level)' + desc: 'Access levels allowed to merge (defaults: `40`, maintainer access level)' end post ':id/protected_branches' do protected_branch = user_project.protected_branches.find_by(name: params[:name]) diff --git a/lib/api/runner.rb b/lib/api/runner.rb index e9886c76870..96a02914faa 100644 --- a/lib/api/runner.rb +++ b/lib/api/runner.rb @@ -84,7 +84,11 @@ module API end post '/request' do authenticate_runner! - no_content! unless current_runner.active? + + unless current_runner.active? + header 'X-GitLab-Last-Update', current_runner.ensure_runner_queue_value + break no_content! + end if current_runner.runner_queue_value_latest?(params[:last_update]) header 'X-GitLab-Last-Update', params[:last_update] @@ -125,7 +129,7 @@ module API end put '/:id' do job = authenticate_job! - forbidden!('Job is not running') unless job.running? + job_forbidden!(job, 'Job is not running') unless job.running? job.trace.set(params[:trace]) if params[:trace] @@ -133,6 +137,8 @@ module API project: job.project.full_path) case params[:state].to_s + when 'running' + job.touch if job.needs_touch? when 'success' job.success! when 'failed' @@ -152,7 +158,7 @@ module API end patch '/:id/trace' do job = authenticate_job! - forbidden!('Job is not running') unless job.running? + job_forbidden!(job, 'Job is not running') unless job.running? error!('400 Missing header Content-Range', 400) unless request.headers.key?('Content-Range') content_range = request.headers['Content-Range'] @@ -205,7 +211,7 @@ module API status 200 content_type Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE - JobArtifactUploader.workhorse_authorize + JobArtifactUploader.workhorse_authorize(has_length: false, maximum_size: max_artifacts_size) end desc 'Upload artifacts for job' do diff --git a/lib/api/search.rb b/lib/api/search.rb index 5d9ec617cb7..37fbabe419c 100644 --- a/lib/api/search.rb +++ b/lib/api/search.rb @@ -34,9 +34,7 @@ module API def process_results(results) case params[:scope] - when 'wiki_blobs' - paginate(results).map { |blob| Gitlab::ProjectSearchResults.parse_search_result(blob, user_project) } - when 'blobs' + when 'blobs', 'wiki_blobs' paginate(results).map { |blob| blob[1] } else paginate(results) diff --git a/lib/api/users.rb b/lib/api/users.rb index 14b8a796c8e..e8df2c5a74a 100644 --- a/lib/api/users.rb +++ b/lib/api/users.rb @@ -531,18 +531,22 @@ module API authenticate! end - desc 'Get the currently authenticated user' do - success Entities::UserPublic - end - get do - entity = - if current_user.admin? - Entities::UserWithAdmin - else - Entities::UserPublic - end + # Enabling /user endpoint for the v3 version to allow oauth + # authentication through this endpoint. + version %w(v3 v4), using: :path do + desc 'Get the currently authenticated user' do + success Entities::UserPublic + end + get do + entity = + if current_user.admin? + Entities::UserWithAdmin + else + Entities::UserPublic + end - present current_user, with: entity + present current_user, with: entity + end end desc "Get the currently authenticated user's SSH keys" do diff --git a/lib/backup.rb b/lib/backup.rb new file mode 100644 index 00000000000..e2c62af23ae --- /dev/null +++ b/lib/backup.rb @@ -0,0 +1,3 @@ +module Backup + Error = Class.new(StandardError) +end diff --git a/lib/backup/database.rb b/lib/backup/database.rb index 1608f7ad02d..086ca5986bd 100644 --- a/lib/backup/database.rb +++ b/lib/backup/database.rb @@ -44,7 +44,7 @@ module Backup end report_success(success) - abort 'Backup failed' unless success + raise Backup::Error, 'Backup failed' unless success end def restore @@ -72,7 +72,7 @@ module Backup end report_success(success) - abort 'Restore failed' unless success + abort Backup::Error, 'Restore failed' unless success end protected diff --git a/lib/backup/files.rb b/lib/backup/files.rb index 9895db9e451..e287aa1e392 100644 --- a/lib/backup/files.rb +++ b/lib/backup/files.rb @@ -26,20 +26,29 @@ module Backup unless status.zero? puts output - abort 'Backup failed' + raise Backup::Error, 'Backup failed' end - run_pipeline!([%W(tar --exclude=lost+found -C #{@backup_files_dir} -cf - .), %w(gzip -c -1)], out: [backup_tarball, 'w', 0600]) + run_pipeline!([%W(#{tar} --exclude=lost+found -C #{@backup_files_dir} -cf - .), %w(gzip -c -1)], out: [backup_tarball, 'w', 0600]) FileUtils.rm_rf(@backup_files_dir) else - run_pipeline!([%W(tar --exclude=lost+found -C #{app_files_dir} -cf - .), %w(gzip -c -1)], out: [backup_tarball, 'w', 0600]) + run_pipeline!([%W(#{tar} --exclude=lost+found -C #{app_files_dir} -cf - .), %w(gzip -c -1)], out: [backup_tarball, 'w', 0600]) end end def restore backup_existing_files_dir - run_pipeline!([%w(gzip -cd), %W(tar --unlink-first --recursive-unlink -C #{app_files_dir} -xf -)], in: backup_tarball) + run_pipeline!([%w(gzip -cd), %W(#{tar} --unlink-first --recursive-unlink -C #{app_files_dir} -xf -)], in: backup_tarball) + end + + def tar + if system(*%w[gtar --version], out: '/dev/null') + # It looks like we can get GNU tar by running 'gtar' + 'gtar' + else + 'tar' + end end def backup_existing_files_dir @@ -61,7 +70,7 @@ module Backup def run_pipeline!(cmd_list, options = {}) status_list = Open3.pipeline(*cmd_list, options) - abort 'Backup failed' unless status_list.compact.all?(&:success?) + raise Backup::Error, 'Backup failed' unless status_list.compact.all?(&:success?) end end end diff --git a/lib/backup/manager.rb b/lib/backup/manager.rb index a8da0c7edef..a3641505196 100644 --- a/lib/backup/manager.rb +++ b/lib/backup/manager.rb @@ -27,7 +27,7 @@ module Backup progress.puts "done".color(:green) else puts "creating archive #{tar_file} failed".color(:red) - abort 'Backup failed' + raise Backup::Error, 'Backup failed' end upload @@ -52,7 +52,7 @@ module Backup progress.puts "done".color(:green) else puts "uploading backup to #{remote_directory} failed".color(:red) - abort 'Backup failed' + raise Backup::Error, 'Backup failed' end end @@ -66,7 +66,7 @@ module Backup progress.puts "done".color(:green) else puts "deleting tmp directory '#{dir}' failed".color(:red) - abort 'Backup failed' + raise Backup::Error, 'Backup failed' end end end diff --git a/lib/backup/repository.rb b/lib/backup/repository.rb index 84670d6582e..0119c5d6851 100644 --- a/lib/backup/repository.rb +++ b/lib/backup/repository.rb @@ -17,7 +17,10 @@ module Backup Project.find_each(batch_size: 1000) do |project| progress.print " * #{display_repo_path(project)} ... " - path_to_project_repo = path_to_repo(project) + + path_to_project_repo = Gitlab::GitalyClient::StorageSettings.allow_disk_access do + path_to_repo(project) + end path_to_project_bundle = path_to_bundle(project) # Create namespace dir or hashed path if missing @@ -51,7 +54,9 @@ module Backup end wiki = ProjectWiki.new(project) - path_to_wiki_repo = path_to_repo(wiki) + path_to_wiki_repo = Gitlab::GitalyClient::StorageSettings.allow_disk_access do + path_to_repo(wiki) + end path_to_wiki_bundle = path_to_bundle(wiki) if File.exist?(path_to_wiki_repo) @@ -107,16 +112,31 @@ module Backup end end + def local_restore_custom_hooks(project, dir) + path_to_project_repo = Gitlab::GitalyClient::StorageSettings.allow_disk_access do + path_to_repo(project) + end + cmd = %W(tar -xf #{path_to_tars(project, dir)} -C #{path_to_project_repo} #{dir}) + output, status = Gitlab::Popen.popen(cmd) + unless status.zero? + progress_warn(project, cmd.join(' '), output) + end + end + + def gitaly_restore_custom_hooks(project, dir) + custom_hooks_path = path_to_tars(project, dir) + Gitlab::GitalyClient::RepositoryService.new(project.repository) + .restore_custom_hooks(custom_hooks_path) + end + def restore_custom_hooks(project) - # TODO: Need to find a way to do this for gitaly - # Gitaly migration issue: https://gitlab.com/gitlab-org/gitaly/issues/1195 in_path(path_to_tars(project)) do |dir| - path_to_project_repo = path_to_repo(project) - cmd = %W(tar -xf #{path_to_tars(project, dir)} -C #{path_to_project_repo} #{dir}) - - output, status = Gitlab::Popen.popen(cmd) - unless status.zero? - progress_warn(project, cmd.join(' '), output) + gitaly_migrate(:restore_custom_hooks) do |is_enabled| + if is_enabled + local_restore_custom_hooks(project, dir) + else + gitaly_restore_custom_hooks(project, dir) + end end end end diff --git a/lib/banzai/filter/blockquote_fence_filter.rb b/lib/banzai/filter/blockquote_fence_filter.rb index d2c4b1e4d76..fbfcd72c916 100644 --- a/lib/banzai/filter/blockquote_fence_filter.rb +++ b/lib/banzai/filter/blockquote_fence_filter.rb @@ -10,7 +10,7 @@ module Banzai ^``` .+? - \n```$ + \n```\ *$ ) | (?<html> @@ -19,9 +19,9 @@ module Banzai # Anything, including `>>>` blocks which are ignored by this filter # </tag> - ^<[^>]+?>\n + ^<[^>]+?>\ *\n .+? - \n<\/[^>]+?>$ + \n<\/[^>]+?>\ *$ ) | (?: @@ -30,14 +30,14 @@ module Banzai # Anything, including code and HTML blocks # >>> - ^>>>\n + ^>>>\ *\n (?<quote> (?: # Any character that doesn't introduce a code or HTML block (?! ^``` | - ^<[^>]+?>\n + ^<[^>]+?>\ *\n ) . | @@ -48,7 +48,7 @@ module Banzai \g<html> )+? ) - \n>>>$ + \n>>>\ *$ ) }mx.freeze diff --git a/lib/banzai/filter/markdown_filter.rb b/lib/banzai/filter/markdown_filter.rb index c1e2b680240..944363f17d3 100644 --- a/lib/banzai/filter/markdown_filter.rb +++ b/lib/banzai/filter/markdown_filter.rb @@ -14,7 +14,7 @@ module Banzai private - DEFAULT_ENGINE = :redcarpet + DEFAULT_ENGINE = :common_mark def engine(engine_from_context) engine_from_context ||= DEFAULT_ENGINE diff --git a/lib/banzai/filter/milestone_reference_filter.rb b/lib/banzai/filter/milestone_reference_filter.rb index b144bd8cf54..af8448937b3 100644 --- a/lib/banzai/filter/milestone_reference_filter.rb +++ b/lib/banzai/filter/milestone_reference_filter.rb @@ -65,7 +65,7 @@ module Banzai # We don't support IID lookups for group milestones, because IIDs can # clash between group and project milestones. if project.group && !params[:iid] - finder_params[:group_ids] = [project.group.id] + finder_params[:group_ids] = project.group.self_and_ancestors_ids end MilestonesFinder.new(finder_params).find_by(params) diff --git a/lib/constraints/feature_constrainer.rb b/lib/constraints/feature_constrainer.rb new file mode 100644 index 00000000000..05d48b0f25a --- /dev/null +++ b/lib/constraints/feature_constrainer.rb @@ -0,0 +1,13 @@ +module Constraints + class FeatureConstrainer + attr_reader :feature + + def initialize(feature) + @feature = feature + end + + def matches?(_request) + Feature.enabled?(feature) + end + end +end diff --git a/lib/gitlab.rb b/lib/gitlab.rb index a129746e2c6..b9a148f35bf 100644 --- a/lib/gitlab.rb +++ b/lib/gitlab.rb @@ -33,6 +33,7 @@ module Gitlab APP_DIRS_PATTERN = %r{^/?(app|config|ee|lib|spec|\(\w*\))} SUBDOMAIN_REGEX = %r{\Ahttps://[a-z0-9]+\.gitlab\.com\z} VERSION = File.read(root.join("VERSION")).strip.freeze + INSTALLATION_TYPE = File.read(root.join("INSTALLATION_TYPE")).strip.freeze def self.com? # Check `gl_subdomain?` as well to keep parity with gitlab.com diff --git a/lib/gitlab/access.rb b/lib/gitlab/access.rb index 7127948cf00..87e377de4d3 100644 --- a/lib/gitlab/access.rb +++ b/lib/gitlab/access.rb @@ -29,10 +29,10 @@ module Gitlab def options { - "Guest" => GUEST, - "Reporter" => REPORTER, - "Developer" => DEVELOPER, - "Master" => MASTER + "Guest" => GUEST, + "Reporter" => REPORTER, + "Developer" => DEVELOPER, + "Maintainer" => MASTER } end @@ -57,10 +57,10 @@ module Gitlab def protection_options { - "Not protected: Both developers and masters can push new commits, force push, or delete the branch." => PROTECTION_NONE, - "Protected against pushes: Developers cannot push new commits, but are allowed to accept merge requests to the branch. Masters can push to the branch." => PROTECTION_DEV_CAN_MERGE, - "Partially protected: Both developers and masters can push new commits, but cannot force push or delete the branch." => PROTECTION_DEV_CAN_PUSH, - "Fully protected: Developers cannot push new commits, but masters can. No-one can force push or delete the branch." => PROTECTION_FULL + "Not protected: Both developers and maintainers can push new commits, force push, or delete the branch." => PROTECTION_NONE, + "Protected against pushes: Developers cannot push new commits, but are allowed to accept merge requests to the branch. Maintainers can push to the branch." => PROTECTION_DEV_CAN_MERGE, + "Partially protected: Both developers and maintainers can push new commits, but cannot force push or delete the branch." => PROTECTION_DEV_CAN_PUSH, + "Fully protected: Developers cannot push new commits, but maintainers can. No-one can force push or delete the branch." => PROTECTION_FULL } end diff --git a/lib/gitlab/auth.rb b/lib/gitlab/auth.rb index 0f7a7b0ce8d..7de66539848 100644 --- a/lib/gitlab/auth.rb +++ b/lib/gitlab/auth.rb @@ -240,7 +240,7 @@ module Gitlab return unless login == 'gitlab-ci-token' return unless password - build = ::Ci::Build.running.find_by_token(password) + build = find_build_by_token(password) return unless build return unless build.project.builds_enabled? @@ -301,6 +301,12 @@ module Gitlab REGISTRY_SCOPES end + + private + + def find_build_by_token(token) + ::Ci::Build.running.find_by_token(token) + end end end end diff --git a/lib/gitlab/background_migration/archive_legacy_traces.rb b/lib/gitlab/background_migration/archive_legacy_traces.rb new file mode 100644 index 00000000000..5a4e5b2c471 --- /dev/null +++ b/lib/gitlab/background_migration/archive_legacy_traces.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true +# rubocop:disable Metrics/AbcSize +# rubocop:disable Style/Documentation + +module Gitlab + module BackgroundMigration + class ArchiveLegacyTraces + def perform(start_id, stop_id) + # This background migration directly refers to ::Ci::Build model which is defined in application code. + # In general, migration code should be isolated as much as possible in order to be idempotent. + # However, `archive!` method is too complicated to be replicated by coping its subsequent code. + # So we chose a way to use ::Ci::Build directly and we don't change the `archive!` method until 11.1 + ::Ci::Build.finished.without_archived_trace + .where(id: start_id..stop_id).find_each do |build| + begin + build.trace.archive! + rescue => e + Rails.logger.error "Failed to archive live trace. id: #{build.id} message: #{e.message}" + end + 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 index 914a9e48a2f..522c69a0bb1 100644 --- a/lib/gitlab/background_migration/prepare_untracked_uploads.rb +++ b/lib/gitlab/background_migration/prepare_untracked_uploads.rb @@ -54,7 +54,8 @@ module Gitlab def ensure_temporary_tracking_table_exists table_name = :untracked_files_for_uploads - unless UntrackedFile.connection.table_exists?(table_name) + + unless ActiveRecord::Base.connection.data_source_exists?(table_name) UntrackedFile.connection.create_table table_name do |t| t.string :path, limit: 600, null: false t.index :path, unique: true diff --git a/lib/gitlab/checks/change_access.rb b/lib/gitlab/checks/change_access.rb index 51ba09aa129..f76a6fb5f17 100644 --- a/lib/gitlab/checks/change_access.rb +++ b/lib/gitlab/checks/change_access.rb @@ -5,7 +5,7 @@ module Gitlab push_code: 'You are not allowed to push code to this project.', delete_default_branch: 'The default branch of a project cannot be deleted.', force_push_protected_branch: 'You are not allowed to force push code to a protected branch on this project.', - non_master_delete_protected_branch: 'You are not allowed to delete protected branches from this project. Only a project master or owner can delete a protected branch.', + non_master_delete_protected_branch: 'You are not allowed to delete protected branches from this project. Only a project maintainer or owner can delete a protected branch.', non_web_delete_protected_branch: 'You can only delete protected branches using the web interface.', merge_protected_branch: 'You are not allowed to merge code into protected branches on this project.', push_protected_branch: 'You are not allowed to push code to protected branches on this project.', diff --git a/lib/gitlab/ci/pipeline/chain/populate.rb b/lib/gitlab/ci/pipeline/chain/populate.rb index 69b8a8fc68f..f34c11ca3c2 100644 --- a/lib/gitlab/ci/pipeline/chain/populate.rb +++ b/lib/gitlab/ci/pipeline/chain/populate.rb @@ -8,6 +8,9 @@ module Gitlab PopulateError = Class.new(StandardError) def perform! + # Allocate next IID. This operation must be outside of transactions of pipeline creations. + pipeline.ensure_project_iid! + ## # Populate pipeline with block argument of CreatePipelineService#execute. # diff --git a/lib/gitlab/ci/pipeline/preloader.rb b/lib/gitlab/ci/pipeline/preloader.rb index e7a2e5511cf..db0a1ea4dab 100644 --- a/lib/gitlab/ci/pipeline/preloader.rb +++ b/lib/gitlab/ci/pipeline/preloader.rb @@ -5,23 +5,47 @@ module Gitlab module Pipeline # Class for preloading data associated with pipelines such as commit # authors. - module Preloader - def self.preload(pipelines) - # This ensures that all the pipeline commits are eager loaded before we - # start using them. + class Preloader + def self.preload!(pipelines) + ## + # This preloads all commits at once, because `Ci::Pipeline#commit` is + # using a lazy batch loading, what results in only one batched Gitaly + # call. + # pipelines.each(&:commit) pipelines.each do |pipeline| - # This preloads the author of every commit. We're using "lazy_author" - # here since "author" immediately loads the data on the first call. - pipeline.commit.try(:lazy_author) - - # This preloads the number of warnings for every pipeline, ensuring - # that Ci::Pipeline#has_warnings? doesn't execute any additional - # queries. - pipeline.number_of_warnings + self.new(pipeline).tap do |preloader| + preloader.preload_commit_authors + preloader.preload_pipeline_warnings + preloader.preload_stages_warnings + end end end + + def initialize(pipeline) + @pipeline = pipeline + end + + def preload_commit_authors + # This also preloads the author of every commit. We're using "lazy_author" + # here since "author" immediately loads the data on the first call. + @pipeline.commit.try(:lazy_author) + end + + def preload_pipeline_warnings + # This preloads the number of warnings for every pipeline, ensuring + # that Ci::Pipeline#has_warnings? doesn't execute any additional + # queries. + @pipeline.number_of_warnings + end + + def preload_stages_warnings + # This preloads the number of warnings for every stage, ensuring + # that Ci::Stage#has_warnings? doesn't execute any additional + # queries. + @pipeline.stages.each { |stage| stage.number_of_warnings } + end end end end diff --git a/lib/gitlab/ci/status/stage/common.rb b/lib/gitlab/ci/status/stage/common.rb index bc99d925347..f60a7662075 100644 --- a/lib/gitlab/ci/status/stage/common.rb +++ b/lib/gitlab/ci/status/stage/common.rb @@ -8,7 +8,9 @@ module Gitlab end def details_path - project_pipeline_path(subject.project, subject.pipeline, anchor: subject.name) + project_pipeline_path(subject.pipeline.project, + subject.pipeline, + anchor: subject.name) end def has_action? diff --git a/lib/gitlab/ci/trace.rb b/lib/gitlab/ci/trace.rb index fe15fabc2e8..a52d71225bb 100644 --- a/lib/gitlab/ci/trace.rb +++ b/lib/gitlab/ci/trace.rb @@ -1,6 +1,10 @@ module Gitlab module Ci class Trace + include ExclusiveLeaseGuard + + LEASE_TIMEOUT = 1.hour + ArchiveError = Class.new(StandardError) attr_reader :job @@ -105,6 +109,14 @@ module Gitlab end def archive! + try_obtain_lease do + unsafe_archive! + end + end + + private + + def unsafe_archive! raise ArchiveError, 'Already archived' if trace_artifact raise ArchiveError, 'Job is not finished yet' unless job.complete? @@ -126,8 +138,6 @@ module Gitlab end end - private - def archive_stream!(stream) clone_file!(stream, JobArtifactUploader.workhorse_upload_path) do |clone_path| create_build_trace!(job, clone_path) @@ -206,6 +216,16 @@ module Gitlab def trace_artifact job.job_artifacts_trace end + + # For ExclusiveLeaseGuard concern + def lease_key + @lease_key ||= "trace:archive:#{job.id}" + end + + # For ExclusiveLeaseGuard concern + def lease_timeout + LEASE_TIMEOUT + end end end end diff --git a/lib/gitlab/current_settings.rb b/lib/gitlab/current_settings.rb index 6cf7aa1bf0d..3cf35f499cd 100644 --- a/lib/gitlab/current_settings.rb +++ b/lib/gitlab/current_settings.rb @@ -24,7 +24,22 @@ module Gitlab private def ensure_application_settings! + cached_application_settings || uncached_application_settings + end + + def cached_application_settings return in_memory_application_settings if ENV['IN_MEMORY_APPLICATION_SETTINGS'] == 'true' + + begin + ::ApplicationSetting.cached + rescue + # In case Redis isn't running + # or the Redis UNIX socket file is not available + # or the DB is not running (we use migrations in the cache key) + end + end + + def uncached_application_settings return fake_application_settings unless connect_to_db? current_settings = ::ApplicationSetting.current diff --git a/lib/gitlab/cycle_analytics/summary/commit.rb b/lib/gitlab/cycle_analytics/summary/commit.rb index 0a88e052f60..550c1755a71 100644 --- a/lib/gitlab/cycle_analytics/summary/commit.rb +++ b/lib/gitlab/cycle_analytics/summary/commit.rb @@ -7,9 +7,7 @@ module Gitlab end def value - Gitlab::GitalyClient::StorageSettings.allow_disk_access do - @value ||= count_commits - end + @value ||= count_commits end private @@ -21,19 +19,11 @@ module Gitlab def count_commits return unless ref - repository = @project.repository.raw_repository - sha = @project.repository.commit(ref).sha - - cmd = %W(git --git-dir=#{repository.path} log) - cmd << '--format=%H' - cmd << "--after=#{@from.iso8601}" - cmd << sha - - output, status = Gitlab::Popen.popen(cmd) - - raise IOError, output unless status.zero? + gitaly_commit_client.commit_count(ref, after: @from) + end - output.lines.count + def gitaly_commit_client + Gitlab::GitalyClient::CommitService.new(@project.repository.raw_repository) end def ref diff --git a/lib/gitlab/database.rb b/lib/gitlab/database.rb index d49d055c3f2..4ad106e7b0a 100644 --- a/lib/gitlab/database.rb +++ b/lib/gitlab/database.rb @@ -188,8 +188,11 @@ module Gitlab end def self.cached_table_exists?(table_name) - # Rails 5 uses data_source_exists? instead of table_exists? - connection.schema_cache.table_exists?(table_name) + if Gitlab.rails5? + connection.schema_cache.data_source_exists?(table_name) + else + connection.schema_cache.table_exists?(table_name) + end end private_class_method :connection diff --git a/lib/gitlab/database/median.rb b/lib/gitlab/database/median.rb index 74fed447289..3cac007a42c 100644 --- a/lib/gitlab/database/median.rb +++ b/lib/gitlab/database/median.rb @@ -143,8 +143,13 @@ module Gitlab .order(arel_table[column_sym]) ).as('row_id') - count = arel_table.from(arel_table.alias) - .project('COUNT(*)') + arel_from = if Gitlab.rails5? + arel_table.from.from(arel_table.alias) + else + arel_table.from(arel_table.alias) + end + + count = arel_from.project('COUNT(*)') .where(arel_table[partition_column].eq(arel_table.alias[partition_column])) .as('ct') diff --git a/lib/gitlab/database/rename_reserved_paths_migration/v1/migration_classes.rb b/lib/gitlab/database/rename_reserved_paths_migration/v1/migration_classes.rb index 62d4d0a92a6..26ae6966746 100644 --- a/lib/gitlab/database/rename_reserved_paths_migration/v1/migration_classes.rb +++ b/lib/gitlab/database/rename_reserved_paths_migration/v1/migration_classes.rb @@ -37,6 +37,7 @@ module Gitlab class Namespace < ActiveRecord::Base include MigrationClasses::Routable self.table_name = 'namespaces' + self.inheritance_column = :_type_disabled belongs_to :parent, class_name: "#{MigrationClasses.name}::Namespace" has_one :route, as: :source diff --git a/lib/gitlab/diff/file.rb b/lib/gitlab/diff/file.rb index 765fb0289a8..2820293ad5c 100644 --- a/lib/gitlab/diff/file.rb +++ b/lib/gitlab/diff/file.rb @@ -78,9 +78,12 @@ module Gitlab # Returns the raw diff content up to the given line index def diff_hunk(diff_line) - # Adding 2 because of the @@ diff header and Enum#take should consider - # an extra line, because we're passing an index. - raw_diff.each_line.take(diff_line.index + 2).join + diff_line_index = diff_line.index + # @@ (match) header is not kept if it's found in the top of the file, + # therefore we should keep an extra line on this scenario. + diff_line_index += 1 unless diff_lines.first.match? + + diff_lines.select { |line| line.index <= diff_line_index }.map(&:text).join("\n") end def old_sha diff --git a/lib/gitlab/diff/line.rb b/lib/gitlab/diff/line.rb index 0603141e441..a1e904cfef4 100644 --- a/lib/gitlab/diff/line.rb +++ b/lib/gitlab/diff/line.rb @@ -53,6 +53,10 @@ module Gitlab %w[match new-nonewline old-nonewline].include?(type) end + def match? + type == :match + end + def discussable? !meta? end diff --git a/lib/gitlab/favicon.rb b/lib/gitlab/favicon.rb new file mode 100644 index 00000000000..faf7016d73a --- /dev/null +++ b/lib/gitlab/favicon.rb @@ -0,0 +1,47 @@ +module Gitlab + class Favicon + class << self + def main + return appearance_favicon.url if appearance_favicon.exists? + + image_name = + if Gitlab::Utils.to_boolean(ENV['CANARY']) + 'favicon-yellow.png' + elsif Rails.env.development? + 'favicon-blue.png' + else + 'favicon.png' + end + + ActionController::Base.helpers.image_path(image_name) + end + + def status_overlay(status_name) + path = File.join( + 'ci_favicons', + "#{status_name}.png" + ) + + ActionController::Base.helpers.image_path(path) + end + + def available_status_names + @available_status_names ||= begin + Dir.glob(Rails.root.join('app', 'assets', 'images', 'ci_favicons', '*.png')) + .map { |file| File.basename(file, '.png') } + .sort + end + end + + private + + def appearance + RequestStore.store[:appearance] ||= (Appearance.current || Appearance.new) + end + + def appearance_favicon + appearance.favicon + end + end + end +end diff --git a/lib/gitlab/file_finder.rb b/lib/gitlab/file_finder.rb index 8c082c0c336..f42088f980e 100644 --- a/lib/gitlab/file_finder.rb +++ b/lib/gitlab/file_finder.rb @@ -32,17 +32,13 @@ module Gitlab end def find_by_filename(query, except: []) - filenames = repository.search_files_by_name(query, ref).first(BATCH_SIZE) - filenames.delete_if { |filename| except.include?(filename) } unless except.empty? + filenames = search_filenames(query, except) - blob_refs = filenames.map { |filename| [ref, filename] } - blobs = Gitlab::Git::Blob.batch(repository, blob_refs, blob_size_limit: 1024) - - blobs.map do |blob| + blobs(filenames).map do |blob| Gitlab::SearchResults::FoundBlob.new( id: blob.id, filename: blob.path, - basename: File.basename(blob.path), + basename: File.basename(blob.path, File.extname(blob.path)), ref: ref, startline: 1, data: blob.data, @@ -50,5 +46,21 @@ module Gitlab ) end end + + def search_filenames(query, except) + filenames = repository.search_files_by_name(query, ref).first(BATCH_SIZE) + + filenames.delete_if { |filename| except.include?(filename) } unless except.empty? + + filenames + end + + def blob_refs(filenames) + filenames.map { |filename| [ref, filename] } + end + + def blobs(filenames) + Gitlab::Git::Blob.batch(repository, blob_refs(filenames), blob_size_limit: 1024) + end end end diff --git a/lib/gitlab/git.rb b/lib/gitlab/git.rb index e85e87a54af..55236a1122f 100644 --- a/lib/gitlab/git.rb +++ b/lib/gitlab/git.rb @@ -62,7 +62,7 @@ module Gitlab end def version - Gitlab::VersionInfo.parse(Gitlab::Popen.popen(%W(#{Gitlab.config.git.bin_path} --version)).first) + Gitlab::Git::Version.git_version end def check_namespace!(*objects) diff --git a/lib/gitlab/git/blame.rb b/lib/gitlab/git/blame.rb index 4158d50cd9e..e25e15f5c80 100644 --- a/lib/gitlab/git/blame.rb +++ b/lib/gitlab/git/blame.rb @@ -22,24 +22,9 @@ module Gitlab private def load_blame - raw_output = @repo.gitaly_migrate(:blame) do |is_enabled| - if is_enabled - load_blame_by_gitaly - else - load_blame_by_shelling_out - end - end - - output = encode_utf8(raw_output) - process_raw_blame output - end - - def load_blame_by_gitaly - @repo.gitaly_commit_client.raw_blame(@sha, @path) - end + output = encode_utf8(@repo.gitaly_commit_client.raw_blame(@sha, @path)) - def load_blame_by_shelling_out - @repo.shell_blame(@sha, @path) + process_raw_blame(output) end def process_raw_blame(output) diff --git a/lib/gitlab/git/commit.rb b/lib/gitlab/git/commit.rb index 89f761dd515..c9806cdb85f 100644 --- a/lib/gitlab/git/commit.rb +++ b/lib/gitlab/git/commit.rb @@ -60,6 +60,9 @@ module Gitlab # Some weird thing? return nil unless commit_id.is_a?(String) + # This saves us an RPC round trip. + return nil if commit_id.include?(':') + commit = repo.gitaly_migrate(:find_commit) do |is_enabled| if is_enabled repo.gitaly_commit_client.find_commit(commit_id) diff --git a/lib/gitlab/git/committer_with_hooks.rb b/lib/gitlab/git/committer_with_hooks.rb index a8a59f998cd..4198be7c9c9 100644 --- a/lib/gitlab/git/committer_with_hooks.rb +++ b/lib/gitlab/git/committer_with_hooks.rb @@ -20,7 +20,7 @@ module Gitlab end result[:newrev] - rescue Gitlab::Git::HooksService::PreReceiveError => e + rescue Gitlab::Git::PreReceiveError => e message = "Custom Hook failed: #{e.message}" raise Gitlab::Git::Wiki::OperationError, message end diff --git a/lib/gitlab/git/gitlab_projects.rb b/lib/gitlab/git/gitlab_projects.rb index 00c943fdb25..8475645971e 100644 --- a/lib/gitlab/git/gitlab_projects.rb +++ b/lib/gitlab/git/gitlab_projects.rb @@ -53,24 +53,11 @@ module Gitlab # Import project via git clone --bare # URL must be publicly cloneable def import_project(source, timeout) - Gitlab::GitalyClient.migrate(:import_repository, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled| - if is_enabled - gitaly_import_repository(source) - else - git_import_repository(source, timeout) - end - end + git_import_repository(source, timeout) end def fork_repository(new_shard_name, new_repository_relative_path) - Gitlab::GitalyClient.migrate(:fork_repository, - status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled| - if is_enabled - gitaly_fork_repository(new_shard_name, new_repository_relative_path) - else - git_fork_repository(new_shard_name, new_repository_relative_path) - end - end + git_fork_repository(new_shard_name, new_repository_relative_path) end def fetch_remote(name, timeout, force:, tags:, ssh_key: nil, known_hosts: nil, prune: true) @@ -241,16 +228,6 @@ module Gitlab true end - def gitaly_import_repository(source) - raw_repository = Gitlab::Git::Repository.new(shard_name, repository_relative_path, nil) - - Gitlab::GitalyClient::RepositoryService.new(raw_repository).import_repository(source) - true - rescue GRPC::BadStatus => e - @output << e.message - false - end - def git_fork_repository(new_shard_name, new_repository_relative_path) from_path = repository_absolute_path new_shard_path = Gitlab.config.repositories.storages.fetch(new_shard_name).legacy_disk_path @@ -270,16 +247,6 @@ module Gitlab run(cmd, nil) && Gitlab::Git::Repository.create_hooks(to_path, global_hooks_path) end - - def gitaly_fork_repository(new_shard_name, new_repository_relative_path) - target_repository = Gitlab::Git::Repository.new(new_shard_name, new_repository_relative_path, nil) - raw_repository = Gitlab::Git::Repository.new(shard_name, repository_relative_path, nil) - - Gitlab::GitalyClient::RepositoryService.new(target_repository).fork_repository(raw_repository) - rescue GRPC::BadStatus => e - logger.error "fork-repository failed: #{e.message}" - false - end end end end diff --git a/lib/gitlab/git/hook.rb b/lib/gitlab/git/hook.rb index 7c201c6169b..94ff5b4980a 100644 --- a/lib/gitlab/git/hook.rb +++ b/lib/gitlab/git/hook.rb @@ -11,7 +11,7 @@ module Gitlab def initialize(name, repository) @name = name @repository = repository - @path = File.join(repo_path.strip, 'hooks', name) + @path = File.join(repo_path, 'hooks', name) end def repo_path @@ -95,13 +95,13 @@ module Gitlab args = [ref, oldrev, newrev] stdout, stderr, status = Open3.capture3(env, path, *args, options) - [status.success?, Gitlab::Utils.nlbr(stderr.presence || stdout)] + [status.success?, stderr.presence || stdout] end def retrieve_error_message(stderr, stdout) err_message = stderr.read err_message = err_message.blank? ? stdout.read : err_message - Gitlab::Utils.nlbr(err_message) + err_message end end end diff --git a/lib/gitlab/git/hooks_service.rb b/lib/gitlab/git/hooks_service.rb index f302b852b35..e67cacdb95a 100644 --- a/lib/gitlab/git/hooks_service.rb +++ b/lib/gitlab/git/hooks_service.rb @@ -1,8 +1,6 @@ module Gitlab module Git class HooksService - PreReceiveError = Class.new(StandardError) - attr_accessor :oldrev, :newrev, :ref def execute(pusher, repository, oldrev, newrev, ref) diff --git a/lib/gitlab/git/lfs_changes.rb b/lib/gitlab/git/lfs_changes.rb index b9e5cf258f4..f3cc388ea41 100644 --- a/lib/gitlab/git/lfs_changes.rb +++ b/lib/gitlab/git/lfs_changes.rb @@ -30,7 +30,7 @@ module Gitlab def git_new_pointers(object_limit, not_in) @new_pointers ||= begin - rev_list.new_objects(not_in: not_in, require_path: true) do |object_ids| + rev_list.new_objects(rev_list_params(not_in: not_in)) do |object_ids| object_ids = object_ids.take(object_limit) if object_limit Gitlab::Git::Blob.batch_lfs_pointers(@repository, object_ids) @@ -39,7 +39,12 @@ module Gitlab end def git_all_pointers - rev_list.all_objects(require_path: true) do |object_ids| + params = {} + if rev_list_supports_new_options? + params[:options] = ["--filter=blob:limit=#{Gitlab::Git::Blob::LFS_POINTER_MAX_SIZE}"] + end + + rev_list.all_objects(rev_list_params(params)) do |object_ids| Gitlab::Git::Blob.batch_lfs_pointers(@repository, object_ids) end end @@ -47,6 +52,23 @@ module Gitlab def rev_list Gitlab::Git::RevList.new(@repository, newrev: @newrev) end + + # We're passing the `--in-commit-order` arg to ensure we don't wait + # for git to traverse all commits before returning pointers. + # This is required in order to improve the performance of LFS integrity check + def rev_list_params(params = {}) + params[:options] ||= [] + params[:options] << "--in-commit-order" if rev_list_supports_new_options? + params[:require_path] = true + + params + end + + def rev_list_supports_new_options? + return @option_supported if defined?(@option_supported) + + @option_supported = Gitlab::Git.version >= Gitlab::VersionInfo.parse('2.16.0') + end end end end diff --git a/lib/gitlab/git/pre_receive_error.rb b/lib/gitlab/git/pre_receive_error.rb new file mode 100644 index 00000000000..ac1ab7c39d5 --- /dev/null +++ b/lib/gitlab/git/pre_receive_error.rb @@ -0,0 +1,21 @@ +module Gitlab + module Git + # + # PreReceiveError is special because its message gets displayed to users + # in the web UI. To prevent XSS we sanitize the message on + # initialization. + class PreReceiveError < StandardError + def initialize(msg = '') + super(nlbr(msg)) + end + + private + + # In gitaly-ruby we override this method to do nothing, so that + # sanitization happens in gitlab-rails only. + def nlbr(str) + Gitlab::Utils.nlbr(str) + end + end + end +end diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb index 4cbf20bfe76..eb5d6318dcb 100644 --- a/lib/gitlab/git/repository.rb +++ b/lib/gitlab/git/repository.rb @@ -109,7 +109,7 @@ module Gitlab end def ==(other) - path == other.path + [storage, relative_path] == [other.storage, other.relative_path] end def path @@ -120,13 +120,11 @@ module Gitlab # Default branch in the repository def root_ref - @root_ref ||= gitaly_migrate(:root_ref) do |is_enabled| - if is_enabled - gitaly_ref_client.default_branch_name - else - discover_default_branch - end - end + gitaly_ref_client.default_branch_name + rescue GRPC::NotFound => e + raise NoRepository.new(e.message) + rescue GRPC::Unknown => e + raise Gitlab::Git::CommandError.new(e.message) end def rugged @@ -152,23 +150,15 @@ module Gitlab # Returns an Array of branch names # sorted by name ASC def branch_names - gitaly_migrate(:branch_names) do |is_enabled| - if is_enabled - gitaly_ref_client.branch_names - else - branches.map(&:name) - end + wrapped_gitaly_errors do + gitaly_ref_client.branch_names end end # Returns an Array of Branches def branches - gitaly_migrate(:branches) do |is_enabled| - if is_enabled - gitaly_ref_client.branches - else - branches_filter - end + wrapped_gitaly_errors do + gitaly_ref_client.branches end end @@ -200,12 +190,8 @@ module Gitlab end def local_branches(sort_by: nil) - gitaly_migrate(:local_branches) do |is_enabled| - if is_enabled - gitaly_ref_client.local_branches(sort_by: sort_by) - else - branches_filter(filter: :local, sort_by: sort_by) - end + wrapped_gitaly_errors do + gitaly_ref_client.local_branches(sort_by: sort_by) end end @@ -245,18 +231,6 @@ module Gitlab # 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 - ref.name && ref.target # ensures the branch is valid - - true - rescue Rugged::ReferenceError - false - end - end - end - # Returns the number of valid tags def tag_count gitaly_migrate(:tag_names) do |is_enabled| @@ -270,12 +244,8 @@ module Gitlab # Returns an Array of tag names def tag_names - gitaly_migrate(:tag_names) do |is_enabled| - if is_enabled - gitaly_ref_client.tag_names - else - rugged.tags.map { |t| t.name } - end + wrapped_gitaly_errors do + gitaly_ref_client.tag_names end end @@ -283,12 +253,8 @@ module Gitlab # # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/390 def tags - gitaly_migrate(:tags) do |is_enabled| - if is_enabled - tags_from_gitaly - else - tags_from_rugged - end + wrapped_gitaly_errors do + gitaly_ref_client.tags end end @@ -310,7 +276,7 @@ module Gitlab # # name - The name of the tag as a String. def tag_exists?(name) - gitaly_migrate(:ref_exists_tags) do |is_enabled| + gitaly_migrate(:ref_exists_tags, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled| if is_enabled gitaly_ref_exists?("refs/tags/#{name}") else @@ -323,7 +289,7 @@ module Gitlab # # name - The name of the branch as a String. def branch_exists?(name) - gitaly_migrate(:ref_exists_branches) do |is_enabled| + gitaly_migrate(:ref_exists_branches, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled| if is_enabled gitaly_ref_exists?("refs/heads/#{name}") else @@ -364,43 +330,18 @@ module Gitlab end.map(&:name) end - # Discovers the default branch based on the repository's available branches - # - # - If no branches are present, returns nil - # - If one branch is present, returns its name - # - If two or more branches are present, returns current HEAD or master or first branch - def discover_default_branch - names = branch_names - - return if names.empty? - - return names[0] if names.length == 1 - - if rugged_head - extracted_name = Ref.extract_branch_name(rugged_head.name) - - return extracted_name if names.include?(extracted_name) - end - - if names.include?('master') - 'master' - else - names[0] - end - end - def rugged_head rugged.head rescue Rugged::ReferenceError nil end - def archive_metadata(ref, storage_path, format = "tar.gz", append_sha:) + def archive_metadata(ref, storage_path, project_path, format = "tar.gz", append_sha:) ref ||= root_ref commit = Gitlab::Git::Commit.find(self, ref) return {} if commit.nil? - prefix = archive_prefix(ref, commit.id, append_sha: append_sha) + prefix = archive_prefix(ref, commit.id, project_path, append_sha: append_sha) { 'ArchivePrefix' => prefix, @@ -412,16 +353,12 @@ module Gitlab # This is both the filename of the archive (missing the extension) and the # name of the top-level member of the archive under which all files go - # - # FIXME: The generated prefix is incorrect for projects with hashed - # storage enabled - def archive_prefix(ref, sha, append_sha:) + def archive_prefix(ref, sha, project_path, append_sha:) append_sha = (ref != sha) if append_sha.nil? - project_name = self.name.chomp('.git') formatted_ref = ref.tr('/', '-') - prefix_segments = [project_name, formatted_ref] + prefix_segments = [project_path, formatted_ref] prefix_segments << sha if append_sha prefix_segments.join('-') @@ -537,7 +474,7 @@ module Gitlab def count_commits(options) count_commits_options = process_count_commits_options(options) - gitaly_migrate(:count_commits) do |is_enabled| + gitaly_migrate(:count_commits, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled| if is_enabled count_commits_by_gitaly(count_commits_options) else @@ -742,7 +679,7 @@ module Gitlab # # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/330 def commit_count(ref) - gitaly_migrate(:commit_count) do |is_enabled| + gitaly_migrate(:commit_count, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled| if is_enabled gitaly_commit_client.commit_count(ref) else @@ -1185,18 +1122,18 @@ module Gitlab end def compare_source_branch(target_branch_name, source_repository, source_branch_name, straight:) - Gitlab::GitalyClient::StorageSettings.allow_disk_access do - with_repo_branch_commit(source_repository, source_branch_name) do |commit| - break unless commit + tmp_ref = "refs/tmp/#{SecureRandom.hex}" - Gitlab::Git::Compare.new( - self, - target_branch_name, - commit.sha, - straight: straight - ) - end - end + return unless fetch_source_branch!(source_repository, source_branch_name, tmp_ref) + + Gitlab::Git::Compare.new( + self, + target_branch_name, + tmp_ref, + straight: straight + ) + ensure + delete_refs(tmp_ref) end def write_ref(ref_path, ref, old_ref: nil, shell: true) @@ -1397,6 +1334,11 @@ module Gitlab def write_config(full_path:) return unless full_path.present? + # This guard avoids Gitaly log/error spam + unless exists? + raise NoRepository, 'repository does not exist' + end + gitaly_migrate(:write_config) do |is_enabled| if is_enabled gitaly_repository_client.write_config(full_path: full_path) @@ -1452,6 +1394,16 @@ module Gitlab raise CommandError.new(e) end + def wrapped_gitaly_errors(&block) + yield block + rescue GRPC::NotFound => e + raise NoRepository.new(e) + rescue GRPC::InvalidArgument => e + raise ArgumentError.new(e) + rescue GRPC::BadStatus => e + raise CommandError.new(e) + end + def clean_stale_repository_files gitaly_migrate(:repository_cleanup, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled| gitaly_repository_client.cleanup if is_enabled && exists? @@ -1542,7 +1494,7 @@ module Gitlab end end - def rev_list(including: [], excluding: [], objects: false, &block) + def rev_list(including: [], excluding: [], options: [], objects: false, &block) args = ['rev-list'] args.push(*rev_list_param(including)) @@ -1555,6 +1507,10 @@ module Gitlab args.push('--objects') if objects + if options.any? + args.push(*options) + end + run_git!(args, lazy_block: block) end @@ -1601,12 +1557,8 @@ module Gitlab private def uncached_has_local_branches? - gitaly_migrate(:has_local_branches, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled| - if is_enabled - gitaly_repository_client.has_local_branches? - else - has_local_branches_rugged? - end + wrapped_gitaly_errors do + gitaly_repository_client.has_local_branches? end end @@ -1726,20 +1678,6 @@ module Gitlab } end - # Gitaly note: JV: Trying to get rid of the 'filter' option so we can implement this with 'git'. - def branches_filter(filter: nil, sort_by: nil) - branches = rugged.branches.each(filter).map do |rugged_ref| - begin - target_commit = Gitlab::Git::Commit.find(self, rugged_ref.target) - Gitlab::Git::Branch.new(self, rugged_ref.name, rugged_ref.target, target_commit) - rescue Rugged::ReferenceError - # Omit invalid branch - end - end.compact - - sort_branches(branches, sort_by) - end - def git_merged_branch_names(branch_names, root_sha) git_arguments = %W[branch --merged #{root_sha} @@ -1951,37 +1889,11 @@ module Gitlab end end - def tags_from_rugged - rugged.references.each("refs/tags/*").map do |ref| - message = nil - - if ref.target.is_a?(Rugged::Tag::Annotation) - tag_message = ref.target.message - - if tag_message.respond_to?(:chomp) - message = tag_message.chomp - end - end - - target_commit = Gitlab::Git::Commit.find(self, ref.target) - Gitlab::Git::Tag.new(self, { - name: ref.name, - target: ref.target, - target_commit: target_commit, - message: message - }) - end.sort_by(&:name) - end - def last_commit_for_path_by_rugged(sha, path) sha = last_commit_id_for_path_by_shelling_out(sha, path) commit(sha) end - def tags_from_gitaly - gitaly_ref_client.tags - end - def size_by_shelling_out popen(%w(du -sk), path).first.strip.to_i end diff --git a/lib/gitlab/git/rev_list.rb b/lib/gitlab/git/rev_list.rb index 38c3a55f96f..4e661eceffb 100644 --- a/lib/gitlab/git/rev_list.rb +++ b/lib/gitlab/git/rev_list.rb @@ -27,9 +27,10 @@ module Gitlab # # When given a block it will yield objects as a lazy enumerator so # the caller can limit work done instead of processing megabytes of data - def new_objects(require_path: nil, not_in: nil, &lazy_block) + def new_objects(options: [], require_path: nil, not_in: nil, &lazy_block) opts = { including: newrev, + options: options, excluding: not_in.nil? ? :all : not_in, require_path: require_path } @@ -37,8 +38,11 @@ module Gitlab get_objects(opts, &lazy_block) end - def all_objects(require_path: nil, &lazy_block) - get_objects(including: :all, require_path: require_path, &lazy_block) + def all_objects(options: [], require_path: nil, &lazy_block) + get_objects(including: :all, + options: options, + require_path: require_path, + &lazy_block) end # This methods returns an array of missed references @@ -54,8 +58,8 @@ module Gitlab repository.rev_list(args).split("\n") end - def get_objects(including: [], excluding: [], require_path: nil) - opts = { including: including, excluding: excluding, objects: true } + def get_objects(including: [], excluding: [], options: [], require_path: nil) + opts = { including: including, excluding: excluding, options: options, objects: true } repository.rev_list(opts) do |lazy_output| objects = objects_from_output(lazy_output, require_path: require_path) diff --git a/lib/gitlab/git/version.rb b/lib/gitlab/git/version.rb new file mode 100644 index 00000000000..11184ca3457 --- /dev/null +++ b/lib/gitlab/git/version.rb @@ -0,0 +1,11 @@ +module Gitlab + module Git + module Version + extend Gitlab::Git::Popen + + def self.git_version + Gitlab::VersionInfo.parse(popen(%W(#{Gitlab.config.git.bin_path} --version), nil).first) + end + end + end +end diff --git a/lib/gitlab/git/wiki.rb b/lib/gitlab/git/wiki.rb index 1ab8c4e0229..8ee46b59830 100644 --- a/lib/gitlab/git/wiki.rb +++ b/lib/gitlab/git/wiki.rb @@ -27,63 +27,38 @@ module Gitlab end def write_page(name, format, content, commit_details) - @repository.gitaly_migrate(:wiki_write_page) do |is_enabled| - if is_enabled - gitaly_write_page(name, format, content, commit_details) - else - gollum_write_page(name, format, content, commit_details) - end + @repository.wrapped_gitaly_errors do + gitaly_write_page(name, format, content, commit_details) end end def delete_page(page_path, commit_details) - @repository.gitaly_migrate(:wiki_delete_page) do |is_enabled| - if is_enabled - gitaly_delete_page(page_path, commit_details) - else - gollum_delete_page(page_path, commit_details) - end + @repository.wrapped_gitaly_errors do + gitaly_delete_page(page_path, commit_details) end end def update_page(page_path, title, format, content, commit_details) - @repository.gitaly_migrate(:wiki_update_page) do |is_enabled| - if is_enabled - gitaly_update_page(page_path, title, format, content, commit_details) - else - gollum_update_page(page_path, title, format, content, commit_details) - end + @repository.wrapped_gitaly_errors do + gitaly_update_page(page_path, title, format, content, commit_details) end end def pages(limit: nil) - @repository.gitaly_migrate(:wiki_get_all_pages) do |is_enabled| - if is_enabled - gitaly_get_all_pages - else - gollum_get_all_pages(limit: limit) - end + @repository.wrapped_gitaly_errors do + gitaly_get_all_pages end end def page(title:, version: nil, dir: nil) - @repository.gitaly_migrate(:wiki_find_page, - status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled| - if is_enabled - gitaly_find_page(title: title, version: version, dir: dir) - else - gollum_find_page(title: title, version: version, dir: dir) - end + @repository.wrapped_gitaly_errors do + gitaly_find_page(title: title, version: version, dir: dir) end end def file(name, version) - @repository.gitaly_migrate(:wiki_find_file) do |is_enabled| - if is_enabled - gitaly_find_file(name, version) - else - gollum_find_file(name, version) - end + @repository.wrapped_gitaly_errors do + gitaly_find_file(name, version) end end @@ -92,24 +67,15 @@ module Gitlab # :per_page - The number of items per page. # :limit - Total number of items to return. def page_versions(page_path, options = {}) - @repository.gitaly_migrate(:wiki_page_versions) do |is_enabled| - if is_enabled - versions = gitaly_wiki_client.page_versions(page_path, options) - - # Gitaly uses gollum-lib to get the versions. Gollum defaults to 20 - # per page, but also fetches 20 if `limit` or `per_page` < 20. - # Slicing returns an array with the expected number of items. - slice_bound = options[:limit] || options[:per_page] || Gollum::Page.per_page - versions[0..slice_bound] - else - current_page = gollum_page_by_path(page_path) - - commits_from_page(current_page, options).map do |gitlab_git_commit| - gollum_page = gollum_wiki.page(current_page.title, gitlab_git_commit.id) - Gitlab::Git::WikiPageVersion.new(gitlab_git_commit, gollum_page&.format) - end - end + versions = @repository.wrapped_gitaly_errors do + gitaly_wiki_client.page_versions(page_path, options) end + + # Gitaly uses gollum-lib to get the versions. Gollum defaults to 20 + # per page, but also fetches 20 if `limit` or `per_page` < 20. + # Slicing returns an array with the expected number of items. + slice_bound = options[:limit] || options[:per_page] || Gollum::Page.per_page + versions[0..slice_bound] end def count_page_versions(page_path) @@ -131,46 +97,13 @@ module Gitlab def page_formatted_data(title:, dir: nil, version: nil) version = version&.id - @repository.gitaly_migrate(:wiki_page_formatted_data, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled| - if is_enabled - gitaly_wiki_client.get_formatted_data(title: title, dir: dir, version: version) - else - # We don't use #page because if wiki_find_page feature is enabled, we would - # get a page without formatted_data. - gollum_find_page(title: title, dir: dir, version: version)&.formatted_data - end + @repository.wrapped_gitaly_errors do + gitaly_wiki_client.get_formatted_data(title: title, dir: dir, version: version) end end - def gollum_wiki - @gollum_wiki ||= Gollum::Wiki.new(@repository.path) - end - private - # options: - # :page - The Integer page number. - # :per_page - The number of items per page. - # :limit - Total number of items to return. - def commits_from_page(gollum_page, options = {}) - unless options[:limit] - options[:offset] = ([1, options.delete(:page).to_i].max - 1) * Gollum::Page.per_page - options[:limit] = (options.delete(:per_page) || Gollum::Page.per_page).to_i - end - - @repository.log(ref: gollum_page.last_version.id, - path: gollum_page.path, - limit: options[:limit], - offset: options[:offset]) - end - - def gollum_page_by_path(page_path) - page_name = Gollum::Page.canonicalize_filename(page_path) - page_dir = File.split(page_path).first - - gollum_wiki.paged(page_name, page_dir) - end - def new_page(gollum_page) Gitlab::Git::WikiPage.new(gollum_page, new_version(gollum_page, gollum_page.version.id)) end @@ -199,65 +132,6 @@ module Gitlab @gitaly_wiki_client ||= Gitlab::GitalyClient::WikiService.new(@repository) end - def gollum_write_page(name, format, content, commit_details) - assert_type!(format, Symbol) - assert_type!(commit_details, CommitDetails) - - with_committer_with_hooks(commit_details) do |committer| - filename = File.basename(name) - dir = (tmp_dir = File.dirname(name)) == '.' ? '' : tmp_dir - - gollum_wiki.write_page(filename, format, content, { committer: committer }, dir) - end - rescue Gollum::DuplicatePageError => e - raise Gitlab::Git::Wiki::DuplicatePageError, e.message - end - - def gollum_delete_page(page_path, commit_details) - assert_type!(commit_details, CommitDetails) - - with_committer_with_hooks(commit_details) do |committer| - gollum_wiki.delete_page(gollum_page_by_path(page_path), committer: committer) - end - end - - def gollum_update_page(page_path, title, format, content, commit_details) - assert_type!(format, Symbol) - assert_type!(commit_details, CommitDetails) - - with_committer_with_hooks(commit_details) do |committer| - page = gollum_page_by_path(page_path) - # Instead of performing two renames if the title has changed, - # the update_page will only update the format and content and - # the rename_page will do anything related to moving/renaming - gollum_wiki.update_page(page, page.name, format, content, committer: committer) - gollum_wiki.rename_page(page, title, committer: committer) - end - end - - def gollum_find_page(title:, version: nil, dir: nil) - if version - version = Gitlab::Git::Commit.find(@repository, version).id - end - - gollum_page = gollum_wiki.page(title, version, dir) - return unless gollum_page - - new_page(gollum_page) - end - - def gollum_find_file(name, version) - version ||= self.class.default_ref - gollum_file = gollum_wiki.file(name, version) - return unless gollum_file - - Gitlab::Git::WikiFile.new(gollum_file) - end - - def gollum_get_all_pages(limit: nil) - gollum_wiki.pages(limit: limit).map { |gollum_page| new_page(gollum_page) } - end - def gitaly_write_page(name, format, content, commit_details) gitaly_wiki_client.write_page(name, format, content, commit_details) end diff --git a/lib/gitlab/gitaly_client.rb b/lib/gitlab/gitaly_client.rb index 550294916a4..620362b52a9 100644 --- a/lib/gitlab/gitaly_client.rb +++ b/lib/gitlab/gitaly_client.rb @@ -33,11 +33,6 @@ module Gitlab MAXIMUM_GITALY_CALLS = 35 CLIENT_NAME = (Sidekiq.server? ? 'gitlab-sidekiq' : 'gitlab-web').freeze - # We have a mechanism to let GitLab automatically opt in to all Gitaly - # features. We want to be able to exclude some features from automatic - # opt-in. That is what EXPLICIT_OPT_IN_REQUIRED is for. - EXPLICIT_OPT_IN_REQUIRED = [Gitlab::GitalyClient::StorageSettings::DISK_ACCESS_DENIED_FLAG].freeze - MUTEX = Mutex.new class << self @@ -191,6 +186,8 @@ module Gitlab metadata['call_site'] = feature.to_s if feature metadata['gitaly-servers'] = address_metadata(remote_storage) if remote_storage + metadata.merge!(server_feature_flags) + result = { metadata: metadata } # nil timeout indicates that we should use the default @@ -209,6 +206,14 @@ module Gitlab result end + SERVER_FEATURE_FLAGS = %w[gogit_findcommit].freeze + + def self.server_feature_flags + SERVER_FEATURE_FLAGS.map do |f| + ["gitaly-feature-#{f.tr('_', '-')}", feature_enabled?(f).to_s] + end.to_h + end + def self.token(storage) params = Gitlab.config.repositories.storages[storage] raise "storage not found: #{storage.inspect}" if params.nil? @@ -239,10 +244,21 @@ module Gitlab when MigrationStatus::OPT_OUT true when MigrationStatus::OPT_IN - opt_into_all_features? && !EXPLICIT_OPT_IN_REQUIRED.include?(feature_name) + opt_into_all_features? && !explicit_opt_in_required.include?(feature_name) else false end + rescue => ex + # During application startup feature lookups in SQL can fail + Rails.logger.warn "exception while checking Gitaly feature status for #{feature_name}: #{ex}" + false + end + + # We have a mechanism to let GitLab automatically opt in to all Gitaly + # features. We want to be able to exclude some features from automatic + # opt-in. This function has an override in EE. + def self.explicit_opt_in_required + [] end # opt_into_all_features? returns true when the current environment diff --git a/lib/gitlab/gitaly_client/commit_service.rb b/lib/gitlab/gitaly_client/commit_service.rb index a4cc64de80d..7f2e6441f16 100644 --- a/lib/gitlab/gitaly_client/commit_service.rb +++ b/lib/gitlab/gitaly_client/commit_service.rb @@ -179,6 +179,8 @@ module Gitlab end def list_commits_by_oid(oids) + return [] if oids.empty? + request = Gitaly::ListCommitsByOidRequest.new(repository: @gitaly_repo, oid: oids) response = GitalyClient.call(@repository.storage, :commit_service, :list_commits_by_oid, request, timeout: GitalyClient.medium_timeout) diff --git a/lib/gitlab/gitaly_client/operation_service.rb b/lib/gitlab/gitaly_client/operation_service.rb index 44b0e517bf0..e9d4bb4c4b6 100644 --- a/lib/gitlab/gitaly_client/operation_service.rb +++ b/lib/gitlab/gitaly_client/operation_service.rb @@ -20,7 +20,7 @@ module Gitlab response = GitalyClient.call(@repository.storage, :operation_service, :user_delete_tag, request) if pre_receive_error = response.pre_receive_error.presence - raise Gitlab::Git::HooksService::PreReceiveError, pre_receive_error + raise Gitlab::Git::PreReceiveError, pre_receive_error end end @@ -35,7 +35,7 @@ module Gitlab response = GitalyClient.call(@repository.storage, :operation_service, :user_create_tag, request) if pre_receive_error = response.pre_receive_error.presence - raise Gitlab::Git::HooksService::PreReceiveError, pre_receive_error + raise Gitlab::Git::PreReceiveError, pre_receive_error elsif response.exists raise Gitlab::Git::Repository::TagExistsError end @@ -56,7 +56,7 @@ module Gitlab :user_create_branch, request) if response.pre_receive_error.present? - raise Gitlab::Git::HooksService::PreReceiveError.new(response.pre_receive_error) + raise Gitlab::Git::PreReceiveError.new(response.pre_receive_error) end branch = response.branch @@ -76,7 +76,7 @@ module Gitlab response = GitalyClient.call(@repository.storage, :operation_service, :user_delete_branch, request) if pre_receive_error = response.pre_receive_error.presence - raise Gitlab::Git::HooksService::PreReceiveError, pre_receive_error + raise Gitlab::Git::PreReceiveError, pre_receive_error end end @@ -106,7 +106,7 @@ module Gitlab second_response = response_enum.next if second_response.pre_receive_error.present? - raise Gitlab::Git::HooksService::PreReceiveError, second_response.pre_receive_error + raise Gitlab::Git::PreReceiveError, second_response.pre_receive_error end branch_update = second_response.branch_update @@ -175,7 +175,7 @@ module Gitlab ) if response.pre_receive_error.presence - raise Gitlab::Git::HooksService::PreReceiveError, response.pre_receive_error + raise Gitlab::Git::PreReceiveError, response.pre_receive_error elsif response.git_error.presence raise Gitlab::Git::Repository::GitError, response.git_error else @@ -242,7 +242,7 @@ module Gitlab :user_commit_files, req_enum, remote_storage: start_repository.storage) if (pre_receive_error = response.pre_receive_error.presence) - raise Gitlab::Git::HooksService::PreReceiveError, pre_receive_error + raise Gitlab::Git::PreReceiveError, pre_receive_error end if (index_error = response.index_error.presence) @@ -280,7 +280,7 @@ module Gitlab def handle_cherry_pick_or_revert_response(response) if response.pre_receive_error.presence - raise Gitlab::Git::HooksService::PreReceiveError, response.pre_receive_error + raise Gitlab::Git::PreReceiveError, response.pre_receive_error elsif response.commit_error.presence raise Gitlab::Git::CommitError, response.commit_error elsif response.create_tree_error.presence diff --git a/lib/gitlab/gitaly_client/repository_service.rb b/lib/gitlab/gitaly_client/repository_service.rb index ee01f5a5bd9..4340f779e53 100644 --- a/lib/gitlab/gitaly_client/repository_service.rb +++ b/lib/gitlab/gitaly_client/repository_service.rb @@ -213,25 +213,20 @@ module Gitlab end def create_from_bundle(bundle_path) - request = Gitaly::CreateRepositoryFromBundleRequest.new(repository: @gitaly_repo) - enum = Enumerator.new do |y| - File.open(bundle_path, 'rb') do |f| - while data = f.read(MAX_MSG_SIZE) - request.data = data - - y.yield request - - request = Gitaly::CreateRepositoryFromBundleRequest.new - end - end - end - - GitalyClient.call( - @storage, - :repository_service, + gitaly_repo_stream_request( + bundle_path, :create_repository_from_bundle, - enum, - timeout: GitalyClient.default_timeout + Gitaly::CreateRepositoryFromBundleRequest, + GitalyClient.default_timeout + ) + end + + def restore_custom_hooks(custom_hooks_path) + gitaly_repo_stream_request( + custom_hooks_path, + :restore_custom_hooks, + Gitaly::RestoreCustomHooksRequest, + GitalyClient.default_timeout ) end @@ -311,6 +306,30 @@ module Gitlab request = Gitaly::SearchFilesByContentRequest.new(repository: @gitaly_repo, ref: ref, query: query) GitalyClient.call(@storage, :repository_service, :search_files_by_content, request).flat_map(&:matches) end + + private + + def gitaly_repo_stream_request(file_path, rpc_name, request_class, timeout) + request = request_class.new(repository: @gitaly_repo) + enum = Enumerator.new do |y| + File.open(file_path, 'rb') do |f| + while data = f.read(MAX_MSG_SIZE) + request.data = data + + y.yield request + request = request_class.new + end + end + end + + GitalyClient.call( + @storage, + :repository_service, + rpc_name, + enum, + timeout: timeout + ) + end end end end diff --git a/lib/gitlab/github_import/importer/lfs_object_importer.rb b/lib/gitlab/github_import/importer/lfs_object_importer.rb new file mode 100644 index 00000000000..a88c17aaf82 --- /dev/null +++ b/lib/gitlab/github_import/importer/lfs_object_importer.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module Gitlab + module GithubImport + module Importer + class LfsObjectImporter + attr_reader :lfs_object, :project + + # lfs_object - An instance of `Gitlab::GithubImport::Representation::LfsObject`. + # project - An instance of `Project`. + def initialize(lfs_object, project, _) + @lfs_object = lfs_object + @project = project + end + + def execute + Projects::LfsPointers::LfsDownloadService + .new(project) + .execute(lfs_object.oid, lfs_object.download_link) + end + end + end + end +end diff --git a/lib/gitlab/github_import/importer/lfs_objects_importer.rb b/lib/gitlab/github_import/importer/lfs_objects_importer.rb new file mode 100644 index 00000000000..6046e30d4ef --- /dev/null +++ b/lib/gitlab/github_import/importer/lfs_objects_importer.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +module Gitlab + module GithubImport + module Importer + class LfsObjectsImporter + include ParallelScheduling + + def importer_class + LfsObjectImporter + end + + def representation_class + Representation::LfsObject + end + + def sidekiq_worker_class + ImportLfsObjectWorker + end + + def collection_method + :lfs_objects + end + + def each_object_to_import + lfs_objects = Projects::LfsPointers::LfsImportService.new(project).execute + + lfs_objects.each do |object| + yield object + end + rescue StandardError => e + Rails.logger.error("The Lfs import process failed. #{e.message}") + end + end + end + end +end diff --git a/lib/gitlab/github_import/importer/pull_request_importer.rb b/lib/gitlab/github_import/importer/pull_request_importer.rb index 49d859f9624..6b3688c4381 100644 --- a/lib/gitlab/github_import/importer/pull_request_importer.rb +++ b/lib/gitlab/github_import/importer/pull_request_importer.rb @@ -22,15 +22,22 @@ module Gitlab end def execute - if (mr_id = create_merge_request) - issuable_finder.cache_database_id(mr_id) + mr, already_exists = create_merge_request + + if mr + insert_git_data(mr, already_exists) + issuable_finder.cache_database_id(mr.id) end end # Creates the merge request and returns its ID. # # This method will return `nil` if the merge request could not be - # created. + # created, otherwise it will return an Array containing the following + # values: + # + # 1. A MergeRequest instance. + # 2. A boolean indicating if the MR already exists. def create_merge_request author_id, author_found = user_finder.author_id_for(pull_request) @@ -69,21 +76,43 @@ module Gitlab merge_request_id = GithubImport .insert_and_return_id(attributes, project.merge_requests) - merge_request = project.merge_requests.find(merge_request_id) - - # These fields are set so we can create the correct merge request - # diffs. - merge_request.source_branch_sha = pull_request.source_branch_sha - merge_request.target_branch_sha = pull_request.target_branch_sha - - merge_request.keep_around_commit - merge_request.merge_request_diffs.create - - merge_request.id + [project.merge_requests.find(merge_request_id), false] end rescue ActiveRecord::InvalidForeignKey # It's possible the project has been deleted since scheduling this # job. In this case we'll just skip creating the merge request. + [] + rescue ActiveRecord::RecordNotUnique + # It's possible we previously created the MR, but failed when updating + # the Git data. In this case we'll just continue working on the + # existing row. + [project.merge_requests.find_by(iid: pull_request.iid), true] + end + + def insert_git_data(merge_request, already_exists = false) + # These fields are set so we can create the correct merge request + # diffs. + merge_request.source_branch_sha = pull_request.source_branch_sha + merge_request.target_branch_sha = pull_request.target_branch_sha + + merge_request.keep_around_commit + + # MR diffs normally use an "after_save" hook to pull data from Git. + # All of this happens in the transaction started by calling + # create/save/etc. This in turn can lead to these transactions being + # held open for much longer than necessary. To work around this we + # first save the diff, then populate it. + diff = + if already_exists + merge_request.merge_request_diffs.take || + merge_request.merge_request_diffs.build + else + merge_request.merge_request_diffs.build + end + + diff.importing = true + diff.save + diff.save_git_content end end end diff --git a/lib/gitlab/github_import/parallel_importer.rb b/lib/gitlab/github_import/parallel_importer.rb index b02b123c98e..a77ac1e4fa6 100644 --- a/lib/gitlab/github_import/parallel_importer.rb +++ b/lib/gitlab/github_import/parallel_importer.rb @@ -15,6 +15,15 @@ module Gitlab true end + # This is a workaround for a Ruby 2.3.7 bug. rspec-mocks cannot restore + # the visibility of prepended modules. See + # https://github.com/rspec/rspec-mocks/issues/1231 for more details. + if Rails.env.test? + def self.requires_ci_cd_setup? + raise NotImplementedError + end + end + def initialize(project) @project = project end diff --git a/lib/gitlab/github_import/representation/lfs_object.rb b/lib/gitlab/github_import/representation/lfs_object.rb new file mode 100644 index 00000000000..debe0fa0baf --- /dev/null +++ b/lib/gitlab/github_import/representation/lfs_object.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +module Gitlab + module GithubImport + module Representation + class LfsObject + include ToHash + include ExposeAttribute + + attr_reader :attributes + + expose_attribute :oid, :download_link + + # Builds a lfs_object + def self.from_api_response(lfs_object) + new({ oid: lfs_object[0], download_link: lfs_object[1] }) + end + + # Builds a new lfs_object using a Hash that was built from a JSON payload. + def self.from_json_hash(raw_hash) + new(Representation.symbolize_hash(raw_hash)) + end + + # attributes - A Hash containing the raw lfs_object details. The keys of this + # Hash must be Symbols. + def initialize(attributes) + @attributes = attributes + end + end + end + end +end diff --git a/lib/gitlab/github_import/sequential_importer.rb b/lib/gitlab/github_import/sequential_importer.rb index 4f7324536a0..6a181caf65d 100644 --- a/lib/gitlab/github_import/sequential_importer.rb +++ b/lib/gitlab/github_import/sequential_importer.rb @@ -19,7 +19,8 @@ module Gitlab Importer::PullRequestsImporter, Importer::IssuesImporter, Importer::DiffNotesImporter, - Importer::NotesImporter + Importer::NotesImporter, + Importer::LfsObjectsImporter ].freeze # project - The project to import the data into. @@ -41,8 +42,6 @@ module Gitlab klass.new(project, client, parallel: false).execute end - project.repository.after_import - true end end diff --git a/lib/gitlab/gpg.rb b/lib/gitlab/gpg.rb index 413872d7e08..a4263369269 100644 --- a/lib/gitlab/gpg.rb +++ b/lib/gitlab/gpg.rb @@ -54,7 +54,11 @@ module Gitlab fingerprints = CurrentKeyChain.fingerprints_from_key(key) GPGME::Key.find(:public, fingerprints).flat_map do |raw_key| - raw_key.uids.map { |uid| { name: uid.name, email: uid.email.downcase } } + raw_key.uids.each_with_object([]) do |uid, arr| + name = uid.name.force_encoding('UTF-8') + email = uid.email.force_encoding('UTF-8') + arr << { name: name, email: email.downcase } if name.valid_encoding? && email.valid_encoding? + end end end end diff --git a/lib/gitlab/grape_logging/formatters/lograge_with_timestamp.rb b/lib/gitlab/grape_logging/formatters/lograge_with_timestamp.rb index 1e1fdabca93..0014ce2689b 100644 --- a/lib/gitlab/grape_logging/formatters/lograge_with_timestamp.rb +++ b/lib/gitlab/grape_logging/formatters/lograge_with_timestamp.rb @@ -2,8 +2,12 @@ module Gitlab module GrapeLogging module Formatters class LogrageWithTimestamp + include Gitlab::EncodingHelper + def call(severity, datetime, _, data) time = data.delete :time + data[:params] = utf8_encode_values(data[:params]) if data.has_key?(:params) + attributes = { time: datetime.utc.iso8601(3), severity: severity, @@ -13,6 +17,19 @@ module Gitlab }.merge(data) ::Lograge.formatter.call(attributes) + "\n" end + + private + + def utf8_encode_values(data) + case data + when Hash + data.merge(data) { |k, v| utf8_encode_values(v) } + when Array + data.map { |v| utf8_encode_values(v) } + when String + encode_utf8(data) + end + end end end end diff --git a/lib/gitlab/graphql.rb b/lib/gitlab/graphql.rb new file mode 100644 index 00000000000..04a89432230 --- /dev/null +++ b/lib/gitlab/graphql.rb @@ -0,0 +1,5 @@ +module Gitlab + module Graphql + StandardGraphqlError = Class.new(StandardError) + end +end diff --git a/lib/gitlab/graphql/authorize.rb b/lib/gitlab/graphql/authorize.rb new file mode 100644 index 00000000000..04f25c53e49 --- /dev/null +++ b/lib/gitlab/graphql/authorize.rb @@ -0,0 +1,21 @@ +module Gitlab + module Graphql + # Allow fields to declare permissions their objects must have. The field + # will be set to nil unless all required permissions are present. + module Authorize + extend ActiveSupport::Concern + + def self.use(schema_definition) + schema_definition.instrument(:field, Instrumentation.new) + end + + def required_permissions + @required_permissions ||= [] + end + + def authorize(*permissions) + required_permissions.concat(permissions) + end + end + end +end diff --git a/lib/gitlab/graphql/authorize/instrumentation.rb b/lib/gitlab/graphql/authorize/instrumentation.rb new file mode 100644 index 00000000000..6cb8e617f62 --- /dev/null +++ b/lib/gitlab/graphql/authorize/instrumentation.rb @@ -0,0 +1,45 @@ +module Gitlab + module Graphql + module Authorize + class Instrumentation + # Replace the resolver for the field with one that will only return the + # resolved object if the permissions check is successful. + # + # Collections are not supported. Apply permissions checks for those at the + # database level instead, to avoid loading superfluous data from the DB + def instrument(_type, field) + field_definition = field.metadata[:type_class] + return field unless field_definition.respond_to?(:required_permissions) + return field if field_definition.required_permissions.empty? + + old_resolver = field.resolve_proc + + new_resolver = -> (obj, args, ctx) do + resolved_obj = old_resolver.call(obj, args, ctx) + checker = build_checker(ctx[:current_user], field_definition.required_permissions) + + if resolved_obj.respond_to?(:then) + resolved_obj.then(&checker) + else + checker.call(resolved_obj) + end + end + + field.redefine do + resolve(new_resolver) + end + end + + private + + def build_checker(current_user, abilities) + proc do |obj| + # Load the elements if they weren't loaded by BatchLoader yet + obj = obj.sync if obj.respond_to?(:sync) + obj if abilities.all? { |ability| Ability.allowed?(current_user, ability, obj) } + end + end + end + end + end +end diff --git a/lib/gitlab/graphql/present.rb b/lib/gitlab/graphql/present.rb new file mode 100644 index 00000000000..2c7b64f1be9 --- /dev/null +++ b/lib/gitlab/graphql/present.rb @@ -0,0 +1,20 @@ +module Gitlab + module Graphql + module Present + extend ActiveSupport::Concern + prepended do + def self.present_using(kls) + @presenter_class = kls + end + + def self.presenter_class + @presenter_class + end + end + + def self.use(schema_definition) + schema_definition.instrument(:field, Instrumentation.new) + end + end + end +end diff --git a/lib/gitlab/graphql/present/instrumentation.rb b/lib/gitlab/graphql/present/instrumentation.rb new file mode 100644 index 00000000000..1688262974b --- /dev/null +++ b/lib/gitlab/graphql/present/instrumentation.rb @@ -0,0 +1,25 @@ +module Gitlab + module Graphql + module Present + class Instrumentation + def instrument(type, field) + presented_in = field.metadata[:type_class].owner + return field unless presented_in.respond_to?(:presenter_class) + return field unless presented_in.presenter_class + + old_resolver = field.resolve_proc + + resolve_with_presenter = -> (presented_type, args, context) do + object = presented_type.object + presenter = presented_in.presenter_class.new(object, **context.to_h) + old_resolver.call(presenter, args, context) + end + + field.redefine do + resolve(resolve_with_presenter) + end + end + end + end + end +end diff --git a/lib/gitlab/graphql/variables.rb b/lib/gitlab/graphql/variables.rb new file mode 100644 index 00000000000..ffbaf65b512 --- /dev/null +++ b/lib/gitlab/graphql/variables.rb @@ -0,0 +1,37 @@ +module Gitlab + module Graphql + class Variables + Invalid = Class.new(Gitlab::Graphql::StandardGraphqlError) + + def initialize(param) + @param = param + end + + def to_h + ensure_hash(@param) + end + + private + + # Handle form data, JSON body, or a blank value + def ensure_hash(ambiguous_param) + case ambiguous_param + when String + if ambiguous_param.present? + ensure_hash(JSON.parse(ambiguous_param)) + else + {} + end + when Hash, ActionController::Parameters + ambiguous_param + when nil + {} + else + raise Invalid, "Unexpected parameter: #{ambiguous_param}" + end + rescue JSON::ParserError => e + raise Invalid.new(e) + end + end + end +end diff --git a/lib/gitlab/hashed_storage/migrator.rb b/lib/gitlab/hashed_storage/migrator.rb new file mode 100644 index 00000000000..9251ed654cd --- /dev/null +++ b/lib/gitlab/hashed_storage/migrator.rb @@ -0,0 +1,57 @@ +module Gitlab + module HashedStorage + # Hashed Storage Migrator + # + # This is responsible for scheduling and flagging projects + # to be migrated from Legacy to Hashed storage, either one by one or in bulk. + class Migrator + BATCH_SIZE = 100 + + # Schedule a range of projects to be bulk migrated with #bulk_migrate asynchronously + # + # @param [Object] start first project id for the range + # @param [Object] finish last project id for the range + def bulk_schedule(start, finish) + StorageMigratorWorker.perform_async(start, finish) + end + + # Start migration of projects from specified range + # + # Flagging a project to be migrated is a synchronous action, + # but the migration runs through async jobs + # + # @param [Object] start first project id for the range + # @param [Object] finish last project id for the range + def bulk_migrate(start, finish) + projects = build_relation(start, finish) + + projects.with_route.find_each(batch_size: BATCH_SIZE) do |project| + migrate(project) + end + end + + # Flag a project to me migrated + # + # @param [Object] project that will be migrated + def migrate(project) + Rails.logger.info "Starting storage migration of #{project.full_path} (ID=#{project.id})..." + + project.migrate_to_hashed_storage! + rescue => err + Rails.logger.error("#{err.message} migrating storage of #{project.full_path} (ID=#{project.id}), trace - #{err.backtrace}") + end + + private + + def build_relation(start, finish) + relation = Project + table = Project.arel_table + + relation = relation.where(table[:id].gteq(start)) if start + relation = relation.where(table[:id].lteq(finish)) if finish + + relation + end + end + end +end diff --git a/lib/gitlab/hashed_storage/rake_helper.rb b/lib/gitlab/hashed_storage/rake_helper.rb index 8aba42ccfce..303b05e6a9a 100644 --- a/lib/gitlab/hashed_storage/rake_helper.rb +++ b/lib/gitlab/hashed_storage/rake_helper.rb @@ -9,8 +9,20 @@ module Gitlab ENV.fetch('LIMIT', 500).to_i end + def self.range_from + ENV['ID_FROM'] + end + + def self.range_to + ENV['ID_TO'] + end + + def self.range_single_item? + !range_from.nil? && range_from == range_to + end + def self.project_id_batches(&block) - Project.with_unmigrated_storage.in_batches(of: batch_size, start: ENV['ID_FROM'], finish: ENV['ID_TO']) do |relation| # rubocop: disable Cop/InBatches + Project.with_unmigrated_storage.in_batches(of: batch_size, start: range_from, finish: range_to) do |relation| # rubocop: disable Cop/InBatches ids = relation.pluck(:id) yield ids.min, ids.max diff --git a/lib/gitlab/health_checks/db_check.rb b/lib/gitlab/health_checks/db_check.rb index e27e16ddaf6..08495c0a59e 100644 --- a/lib/gitlab/health_checks/db_check.rb +++ b/lib/gitlab/health_checks/db_check.rb @@ -17,7 +17,7 @@ module Gitlab def check catch_timeout 10.seconds do if Gitlab::Database.postgresql? - ActiveRecord::Base.connection.execute('SELECT 1 as ping')&.first&.[]('ping') + ActiveRecord::Base.connection.execute('SELECT 1 as ping')&.first&.[]('ping')&.to_s else ActiveRecord::Base.connection.execute('SELECT 1 as ping')&.first&.first&.to_s end diff --git a/lib/gitlab/i18n.rb b/lib/gitlab/i18n.rb index 3772ef11c7f..343487bc361 100644 --- a/lib/gitlab/i18n.rb +++ b/lib/gitlab/i18n.rb @@ -21,7 +21,8 @@ module Gitlab 'nl_NL' => 'Nederlands', 'tr_TR' => 'Türkçe', 'id_ID' => 'Bahasa Indonesia', - 'fil_PH' => 'Filipino' + 'fil_PH' => 'Filipino', + 'pl_PL' => 'Polski' }.freeze def available_locales diff --git a/lib/gitlab/i18n/metadata_entry.rb b/lib/gitlab/i18n/metadata_entry.rb index 35d57459a3d..36fc1bcdcb7 100644 --- a/lib/gitlab/i18n/metadata_entry.rb +++ b/lib/gitlab/i18n/metadata_entry.rb @@ -3,16 +3,25 @@ module Gitlab class MetadataEntry attr_reader :entry_data + # Avoid testing too many plurals if `nplurals` was incorrectly set. + # Based on info on https://www.gnu.org/software/gettext/manual/html_node/Plural-forms.html + # which mentions special cases for numbers ending in 2 digits + MAX_FORMS_TO_TEST = 101 + def initialize(entry_data) @entry_data = entry_data end - def expected_plurals + def expected_forms return nil unless plural_information plural_information['nplurals'].to_i end + def forms_to_test + @forms_to_test ||= [expected_forms, MAX_FORMS_TO_TEST].compact.min + end + private def plural_information diff --git a/lib/gitlab/i18n/po_linter.rb b/lib/gitlab/i18n/po_linter.rb index 7d3ff8c7f58..d8e7269a2c2 100644 --- a/lib/gitlab/i18n/po_linter.rb +++ b/lib/gitlab/i18n/po_linter.rb @@ -1,6 +1,8 @@ module Gitlab module I18n class PoLinter + include Gitlab::Utils::StrongMemoize + attr_reader :po_path, :translation_entries, :metadata_entry, :locale VARIABLE_REGEX = /%{\w*}|%[a-z]/.freeze @@ -34,7 +36,7 @@ module Gitlab end @translation_entries = entries.map do |entry_data| - Gitlab::I18n::TranslationEntry.new(entry_data, metadata_entry.expected_plurals) + Gitlab::I18n::TranslationEntry.new(entry_data, metadata_entry.expected_forms) end nil @@ -48,7 +50,7 @@ module Gitlab translation_entries.each do |entry| errors_for_entry = validate_entry(entry) - errors[join_message(entry.msgid)] = errors_for_entry if errors_for_entry.any? + errors[entry.msgid] = errors_for_entry if errors_for_entry.any? end errors @@ -62,6 +64,7 @@ module Gitlab validate_newlines(errors, entry) validate_number_of_plurals(errors, entry) validate_unescaped_chars(errors, entry) + validate_translation(errors, entry) errors end @@ -81,35 +84,39 @@ module Gitlab end def validate_number_of_plurals(errors, entry) - return unless metadata_entry&.expected_plurals + return unless metadata_entry&.expected_forms return unless entry.translated? - if entry.has_plural? && entry.all_translations.size != metadata_entry.expected_plurals - errors << "should have #{metadata_entry.expected_plurals} "\ - "#{'translations'.pluralize(metadata_entry.expected_plurals)}" + if entry.has_plural? && entry.all_translations.size != metadata_entry.expected_forms + errors << "should have #{metadata_entry.expected_forms} "\ + "#{'translations'.pluralize(metadata_entry.expected_forms)}" end end def validate_newlines(errors, entry) - if entry.msgid_contains_newlines? + if entry.msgid_has_multiple_lines? errors << 'is defined over multiple lines, this breaks some tooling.' end - if entry.plural_id_contains_newlines? + if entry.plural_id_has_multiple_lines? errors << 'plural is defined over multiple lines, this breaks some tooling.' end - if entry.translations_contain_newlines? + if entry.translations_have_multiple_lines? errors << 'has translations defined over multiple lines, this breaks some tooling.' end end def validate_variables(errors, entry) if entry.has_singular_translation? + validate_variables_in_message(errors, entry.msgid, entry.msgid) + validate_variables_in_message(errors, entry.msgid, entry.singular_translation) end if entry.has_plural? + validate_variables_in_message(errors, entry.plural_id, entry.plural_id) + entry.plural_translations.each do |translation| validate_variables_in_message(errors, entry.plural_id, translation) end @@ -117,41 +124,98 @@ module Gitlab end def validate_variables_in_message(errors, message_id, message_translation) - message_id = join_message(message_id) required_variables = message_id.scan(VARIABLE_REGEX) validate_unnamed_variables(errors, required_variables) - validate_translation(errors, message_id, required_variables) validate_variable_usage(errors, message_translation, required_variables) end - def validate_translation(errors, message_id, used_variables) + def validate_translation(errors, entry) + Gitlab::I18n.with_locale(locale) do + if entry.has_plural? + translate_plural(entry) + else + translate_singular(entry) + end + end + + # `sprintf` could raise an `ArgumentError` when invalid passing something + # other than a Hash when using named variables + # + # `sprintf` could raise `TypeError` when passing a wrong type when using + # unnamed variables + # + # FastGettext::Translation could raise `RuntimeError` (raised as a string), + # or as subclassess `NoTextDomainConfigured` & `InvalidFormat` + # + # `FastGettext::Translation` could raise `ArgumentError` as subclassess + # `InvalidEncoding`, `IllegalSequence` & `InvalidCharacter` + rescue ArgumentError, TypeError, RuntimeError => e + errors << "Failure translating to #{locale}: #{e.message}" + end + + def translate_singular(entry) + used_variables = entry.msgid.scan(VARIABLE_REGEX) variables = fill_in_variables(used_variables) - begin - Gitlab::I18n.with_locale(locale) do - translated = if message_id.include?('|') - FastGettext::Translation.s_(message_id) - else - FastGettext::Translation._(message_id) - end + translation = if entry.msgid.include?('|') + FastGettext::Translation.s_(entry.msgid) + else + FastGettext::Translation._(entry.msgid) + end - translated % variables + translation % variables if used_variables.any? + end + + def translate_plural(entry) + used_variables = entry.plural_id.scan(VARIABLE_REGEX) + variables = fill_in_variables(used_variables) + + numbers_covering_all_plurals.map do |number| + translation = FastGettext::Translation.n_(entry.msgid, entry.plural_id, number) + + translation % variables if used_variables.any? + end + end + + def numbers_covering_all_plurals + @numbers_covering_all_plurals ||= calculate_numbers_covering_all_plurals + end + + def calculate_numbers_covering_all_plurals + required_numbers = [] + discovered_indexes = [] + counter = 0 + + while discovered_indexes.size < metadata_entry.forms_to_test && counter < Gitlab::I18n::MetadataEntry::MAX_FORMS_TO_TEST + index_for_count = index_for_pluralization(counter) + + unless discovered_indexes.include?(index_for_count) + discovered_indexes << index_for_count + required_numbers << counter end - # `sprintf` could raise an `ArgumentError` when invalid passing something - # other than a Hash when using named variables - # - # `sprintf` could raise `TypeError` when passing a wrong type when using - # unnamed variables - # - # FastGettext::Translation could raise `RuntimeError` (raised as a string), - # or as subclassess `NoTextDomainConfigured` & `InvalidFormat` - # - # `FastGettext::Translation` could raise `ArgumentError` as subclassess - # `InvalidEncoding`, `IllegalSequence` & `InvalidCharacter` - rescue ArgumentError, TypeError, RuntimeError => e - errors << "Failure translating to #{locale} with #{variables}: #{e.message}" + counter += 1 + end + + required_numbers + end + + def index_for_pluralization(counter) + # This calls the C function that defines the pluralization rule, it can + # return a boolean (`false` represents 0, `true` represents 1) or an integer + # that specifies the plural form to be used for the given number + pluralization_result = Gitlab::I18n.with_locale(locale) do + FastGettext.pluralisation_rule.call(counter) + end + + case pluralization_result + when false + 0 + when true + 1 + else + pluralization_result end end @@ -172,14 +236,18 @@ module Gitlab end def validate_unnamed_variables(errors, variables) - if variables.size > 1 && variables.any? { |variable_name| unnamed_variable?(variable_name) } + unnamed_variables, named_variables = variables.partition { |name| unnamed_variable?(name) } + + if unnamed_variables.any? && named_variables.any? + errors << 'is combining named variables with unnamed variables' + end + + if unnamed_variables.size > 1 errors << 'is combining multiple unnamed variables' end end def validate_variable_usage(errors, translation, required_variables) - translation = join_message(translation) - # We don't need to validate when the message is empty. # In this case we fall back to the default, which has all the the # required variables. @@ -205,10 +273,6 @@ module Gitlab def validate_flags(errors, entry) errors << "is marked #{entry.flag}" if entry.flag end - - def join_message(message) - Array(message).join - end end end end diff --git a/lib/gitlab/i18n/translation_entry.rb b/lib/gitlab/i18n/translation_entry.rb index e6c95afca7e..54adb98f42d 100644 --- a/lib/gitlab/i18n/translation_entry.rb +++ b/lib/gitlab/i18n/translation_entry.rb @@ -11,11 +11,11 @@ module Gitlab end def msgid - entry_data[:msgid] + @msgid ||= Array(entry_data[:msgid]).join end def plural_id - entry_data[:msgid_plural] + @plural_id ||= Array(entry_data[:msgid_plural]).join end def has_plural? @@ -23,12 +23,11 @@ module Gitlab end def singular_translation - all_translations.first if has_singular_translation? + all_translations.first.to_s if has_singular_translation? end def all_translations - @all_translations ||= entry_data.fetch_values(*translation_keys) - .reject(&:empty?) + @all_translations ||= translation_entries.map { |translation| Array(translation).join } end def translated? @@ -54,16 +53,16 @@ module Gitlab nplurals > 1 || !has_plural? end - def msgid_contains_newlines? - msgid.is_a?(Array) + def msgid_has_multiple_lines? + entry_data[:msgid].is_a?(Array) end - def plural_id_contains_newlines? - plural_id.is_a?(Array) + def plural_id_has_multiple_lines? + entry_data[:msgid_plural].is_a?(Array) end - def translations_contain_newlines? - all_translations.any? { |translation| translation.is_a?(Array) } + def translations_have_multiple_lines? + translation_entries.any? { |translation| translation.is_a?(Array) } end def msgid_contains_unescaped_chars? @@ -84,6 +83,11 @@ module Gitlab private + def translation_entries + @translation_entries ||= entry_data.fetch_values(*translation_keys) + .reject(&:empty?) + end + def translation_keys @translation_keys ||= entry_data.keys.select { |key| key.to_s =~ /\Amsgstr(\[\d+\])?\z/ } end diff --git a/lib/gitlab/import_export/repo_saver.rb b/lib/gitlab/import_export/repo_saver.rb index 695462c7dd2..0c224bd1971 100644 --- a/lib/gitlab/import_export/repo_saver.rb +++ b/lib/gitlab/import_export/repo_saver.rb @@ -26,10 +26,6 @@ module Gitlab @shared.error(e) false end - - def path_to_repo - @project.repository.path_to_repo - end end end end diff --git a/lib/gitlab/import_export/wiki_repo_saver.rb b/lib/gitlab/import_export/wiki_repo_saver.rb index 5fa2e101e29..2fd62c0fc7b 100644 --- a/lib/gitlab/import_export/wiki_repo_saver.rb +++ b/lib/gitlab/import_export/wiki_repo_saver.rb @@ -22,12 +22,8 @@ module Gitlab "project.wiki.bundle" end - def path_to_repo - @wiki.repository.path_to_repo - end - def wiki_repository_exists? - File.exist?(@wiki.repository.path_to_repo) && !@wiki.repository.empty? + @wiki.repository.exists? && !@wiki.repository.empty? end end end diff --git a/lib/gitlab/legacy_github_import/project_creator.rb b/lib/gitlab/legacy_github_import/project_creator.rb index 3ce245a8050..5e96eb16754 100644 --- a/lib/gitlab/legacy_github_import/project_creator.rb +++ b/lib/gitlab/legacy_github_import/project_creator.rb @@ -35,7 +35,10 @@ module Gitlab end def visibility_level - repo.private ? Gitlab::VisibilityLevel::PRIVATE : Gitlab::CurrentSettings.default_project_visibility + visibility_level = repo.private ? Gitlab::VisibilityLevel::PRIVATE : Gitlab::VisibilityLevel::PUBLIC + visibility_level = Gitlab::CurrentSettings.default_project_visibility if Gitlab::CurrentSettings.restricted_visibility_levels.include?(visibility_level) + + visibility_level end # diff --git a/lib/gitlab/metrics/subscribers/active_record.rb b/lib/gitlab/metrics/subscribers/active_record.rb index 4b3e8d0a6a0..38f119cf06d 100644 --- a/lib/gitlab/metrics/subscribers/active_record.rb +++ b/lib/gitlab/metrics/subscribers/active_record.rb @@ -20,7 +20,7 @@ module Gitlab define_histogram :gitlab_sql_duration_seconds do docstring 'SQL time' base_labels Transaction::BASE_LABELS - buckets [0.001, 0.01, 0.1, 1.0, 10.0] + buckets [0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0] end def current_transaction diff --git a/lib/gitlab/metrics/transaction.rb b/lib/gitlab/metrics/transaction.rb index f3e48083c19..9f903e96585 100644 --- a/lib/gitlab/metrics/transaction.rb +++ b/lib/gitlab/metrics/transaction.rb @@ -140,7 +140,7 @@ module Gitlab define_histogram :gitlab_transaction_duration_seconds do docstring 'Transaction duration' base_labels BASE_LABELS - buckets [0.001, 0.01, 0.1, 1.0, 10.0] + buckets [0.1, 0.25, 0.5, 1.0, 2.5, 5.0] end define_histogram :gitlab_transaction_allocated_memory_bytes do diff --git a/lib/gitlab/path_regex.rb b/lib/gitlab/path_regex.rb index 4dc38aae61e..61653044433 100644 --- a/lib/gitlab/path_regex.rb +++ b/lib/gitlab/path_regex.rb @@ -31,6 +31,7 @@ module Gitlab deploy.html explore favicon.ico + favicon.png files groups health_check diff --git a/lib/gitlab/profiler.rb b/lib/gitlab/profiler.rb index 18540e64d4c..ecff6ab5d5e 100644 --- a/lib/gitlab/profiler.rb +++ b/lib/gitlab/profiler.rb @@ -11,6 +11,7 @@ module Gitlab lib/gitlab/etag_caching/ lib/gitlab/metrics/ lib/gitlab/middleware/ + ee/lib/gitlab/middleware/ lib/gitlab/performance_bar/ lib/gitlab/request_profiler/ lib/gitlab/profiler.rb @@ -98,11 +99,7 @@ module Gitlab super - backtrace = Rails.backtrace_cleaner.clean(caller) - - backtrace.each do |caller_line| - next if caller_line.match(Regexp.union(IGNORE_BACKTRACES)) - + Gitlab::Profiler.clean_backtrace(caller).each do |caller_line| stripped_caller_line = caller_line.sub("#{Rails.root}/", '') super(" ↳ #{stripped_caller_line}") @@ -112,6 +109,12 @@ module Gitlab end end + def self.clean_backtrace(backtrace) + Array(Rails.backtrace_cleaner.clean(backtrace)).reject do |line| + line.match(Regexp.union(IGNORE_BACKTRACES)) + end + end + def self.with_custom_logger(logger) original_colorize_logging = ActiveSupport::LogSubscriber.colorize_logging original_activerecord_logger = ActiveRecord::Base.logger diff --git a/lib/gitlab/project_search_results.rb b/lib/gitlab/project_search_results.rb index 2e9b6e302f5..38bdc61d8ab 100644 --- a/lib/gitlab/project_search_results.rb +++ b/lib/gitlab/project_search_results.rb @@ -106,7 +106,8 @@ module Gitlab project_wiki = ProjectWiki.new(project) unless project_wiki.empty? - project_wiki.search_files(query) + ref = repository_ref || project.wiki.default_branch + Gitlab::WikiFileFinder.new(project, ref).find(query) else [] end diff --git a/lib/gitlab/quick_actions/extractor.rb b/lib/gitlab/quick_actions/extractor.rb index 075ff91700c..30c6806b68e 100644 --- a/lib/gitlab/quick_actions/extractor.rb +++ b/lib/gitlab/quick_actions/extractor.rb @@ -39,7 +39,7 @@ module Gitlab content.delete!("\r") content.gsub!(commands_regex) do if $~[:cmd] - commands << [$~[:cmd], $~[:arg]].reject(&:blank?) + commands << [$~[:cmd].downcase, $~[:arg]].reject(&:blank?) '' else $~[0] @@ -102,14 +102,14 @@ module Gitlab # /close ^\/ - (?<cmd>#{Regexp.union(names)}) + (?<cmd>#{Regexp.new(Regexp.union(names).source, Regexp::IGNORECASE)}) (?: [ ] (?<arg>[^\n]*) )? (?:\n|$) ) - }mx + }mix end def perform_substitutions(content, commands) @@ -120,7 +120,7 @@ module Gitlab end substitution_definitions.each do |substitution| - match_data = substitution.match(content) + match_data = substitution.match(content.downcase) if match_data command = [substitution.name.to_s] command << match_data[1] unless match_data[1].empty? diff --git a/lib/gitlab/quick_actions/substitution_definition.rb b/lib/gitlab/quick_actions/substitution_definition.rb index 032c49ed159..688056e5d73 100644 --- a/lib/gitlab/quick_actions/substitution_definition.rb +++ b/lib/gitlab/quick_actions/substitution_definition.rb @@ -15,7 +15,7 @@ module Gitlab return unless content all_names.each do |a_name| - content.gsub!(%r{/#{a_name} ?(.*)$}, execute_block(action_block, context, '\1')) + content.gsub!(%r{/#{a_name} ?(.*)$}i, execute_block(action_block, context, '\1')) end content end diff --git a/lib/gitlab/setup_helper.rb b/lib/gitlab/setup_helper.rb index e5c02dd8ecc..4a87f43597e 100644 --- a/lib/gitlab/setup_helper.rb +++ b/lib/gitlab/setup_helper.rb @@ -24,7 +24,9 @@ module Gitlab address = val['gitaly_address'] end - storages << { name: key, path: val.legacy_disk_path } + Gitlab::GitalyClient::StorageSettings.allow_disk_access do + storages << { name: key, path: val.legacy_disk_path } + end end if Rails.env.test? diff --git a/lib/gitlab/shell.rb b/lib/gitlab/shell.rb index 4a691d640b3..4b8aae4f5a2 100644 --- a/lib/gitlab/shell.rb +++ b/lib/gitlab/shell.rb @@ -106,10 +106,17 @@ module Gitlab raise Error.new("don't use disk paths with import_repository: #{url.inspect}") end - # The timeout ensures the subprocess won't hang forever - cmd = gitlab_projects(storage, "#{name}.git") - success = cmd.import_project(url, git_timeout) + relative_path = "#{name}.git" + cmd = gitaly_migrate(:import_repository, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled| + if is_enabled + GitalyGitlabProjects.new(storage, relative_path) + else + # The timeout ensures the subprocess won't hang forever + gitlab_projects(storage, relative_path) + end + end + success = cmd.import_project(url, git_timeout) raise Error, cmd.output unless success success @@ -165,8 +172,16 @@ module Gitlab # # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/817 def fork_repository(forked_from_storage, forked_from_disk_path, forked_to_storage, forked_to_disk_path) - gitlab_projects(forked_from_storage, "#{forked_from_disk_path}.git") - .fork_repository(forked_to_storage, "#{forked_to_disk_path}.git") + forked_from_relative_path = "#{forked_from_disk_path}.git" + fork_args = [forked_to_storage, "#{forked_to_disk_path}.git"] + + gitaly_migrate(:fork_repository, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled| + if is_enabled + GitalyGitlabProjects.new(forked_from_storage, forked_from_relative_path).fork_repository(*fork_args) + else + gitlab_projects(forked_from_storage, forked_from_relative_path).fork_repository(*fork_args) + end + end end # Removes a repository from file system, using rm_diretory which is an alias @@ -452,5 +467,39 @@ module Gitlab # need to do the same here... raise Error, e end + + class GitalyGitlabProjects + attr_reader :shard_name, :repository_relative_path, :output + + def initialize(shard_name, repository_relative_path) + @shard_name = shard_name + @repository_relative_path = repository_relative_path + @output = '' + end + + def import_project(source, _timeout) + raw_repository = Gitlab::Git::Repository.new(shard_name, repository_relative_path, nil) + + Gitlab::GitalyClient::RepositoryService.new(raw_repository).import_repository(source) + true + rescue GRPC::BadStatus => e + @output = e.message + false + end + + def fork_repository(new_shard_name, new_repository_relative_path) + target_repository = Gitlab::Git::Repository.new(new_shard_name, new_repository_relative_path, nil) + raw_repository = Gitlab::Git::Repository.new(shard_name, repository_relative_path, nil) + + Gitlab::GitalyClient::RepositoryService.new(target_repository).fork_repository(raw_repository) + rescue GRPC::BadStatus => e + logger.error "fork-repository failed: #{e.message}" + false + end + + def logger + Rails.logger + end + end end end diff --git a/lib/gitlab/slash_commands/command.rb b/lib/gitlab/slash_commands/command.rb index bb778f37096..c82320a6036 100644 --- a/lib/gitlab/slash_commands/command.rb +++ b/lib/gitlab/slash_commands/command.rb @@ -1,13 +1,15 @@ module Gitlab module SlashCommands class Command < BaseCommand - COMMANDS = [ - Gitlab::SlashCommands::IssueShow, - Gitlab::SlashCommands::IssueNew, - Gitlab::SlashCommands::IssueSearch, - Gitlab::SlashCommands::IssueMove, - Gitlab::SlashCommands::Deploy - ].freeze + def self.commands + [ + Gitlab::SlashCommands::IssueShow, + Gitlab::SlashCommands::IssueNew, + Gitlab::SlashCommands::IssueSearch, + Gitlab::SlashCommands::IssueMove, + Gitlab::SlashCommands::Deploy + ] + end def execute command, match = match_command @@ -37,7 +39,7 @@ module Gitlab private def available_commands - COMMANDS.select do |klass| + self.class.commands.keep_if do |klass| klass.available?(project) end end diff --git a/lib/gitlab/sql/cte.rb b/lib/gitlab/sql/cte.rb new file mode 100644 index 00000000000..f357829ba3f --- /dev/null +++ b/lib/gitlab/sql/cte.rb @@ -0,0 +1,50 @@ +module Gitlab + module SQL + # Class for easily building CTE statements. + # + # Example: + # + # cte = CTE.new(:my_cte_name) + # ns = Arel::Table.new(:namespaces) + # + # cte << Namespace. + # where(ns[:parent_id].eq(some_namespace_id)) + # + # Namespace + # with(cte.to_arel). + # from(cte.alias_to(ns)) + class CTE + attr_reader :table, :query + + # name - The name of the CTE as a String or Symbol. + def initialize(name, query) + @table = Arel::Table.new(name) + @query = query + end + + # Returns the Arel relation for this CTE. + def to_arel + sql = Arel::Nodes::SqlLiteral.new("(#{query.to_sql})") + + Arel::Nodes::As.new(table, sql) + end + + # Returns an "AS" statement that aliases the CTE name as the given table + # name. This allows one to trick ActiveRecord into thinking it's selecting + # from an actual table, when in reality it's selecting from a CTE. + # + # alias_table - The Arel table to use as the alias. + def alias_to(alias_table) + Arel::Nodes::As.new(table, alias_table) + end + + # Applies the CTE to the given relation, returning a new one that will + # query from it. + def apply_to(relation) + relation.except(:where) + .with(to_arel) + .from(alias_to(relation.model.arel_table)) + end + end + end +end diff --git a/lib/gitlab/task_helpers.rb b/lib/gitlab/task_helpers.rb index 42be301fd9b..922418966e9 100644 --- a/lib/gitlab/task_helpers.rb +++ b/lib/gitlab/task_helpers.rb @@ -128,17 +128,21 @@ module Gitlab end def all_repos - Gitlab.config.repositories.storages.each_value do |repository_storage| - IO.popen(%W(find #{repository_storage.legacy_disk_path} -mindepth 2 -type d -name *.git)) do |find| - find.each_line do |path| - yield path.chomp + Gitlab::GitalyClient::StorageSettings.allow_disk_access do + Gitlab.config.repositories.storages.each_value do |repository_storage| + IO.popen(%W(find #{repository_storage.legacy_disk_path} -mindepth 2 -type d -name *.git)) do |find| + find.each_line do |path| + yield path.chomp + end end end end end def repository_storage_paths_args - Gitlab.config.repositories.storages.values.map { |rs| rs.legacy_disk_path } + Gitlab::GitalyClient::StorageSettings.allow_disk_access do + Gitlab.config.repositories.storages.values.map { |rs| rs.legacy_disk_path } + end end def user_home diff --git a/lib/gitlab/themes.rb b/lib/gitlab/themes.rb index d43eff5ba4a..694b01b272c 100644 --- a/lib/gitlab/themes.rb +++ b/lib/gitlab/themes.rb @@ -12,11 +12,16 @@ module Gitlab # All available Themes THEMES = [ - Theme.new(1, 'Indigo', 'ui_indigo'), - Theme.new(2, 'Dark', 'ui_dark'), - Theme.new(3, 'Light', 'ui_light'), - Theme.new(4, 'Blue', 'ui_blue'), - Theme.new(5, 'Green', 'ui_green') + Theme.new(1, 'Indigo', 'ui-indigo'), + Theme.new(6, 'Light Indigo', 'ui-light-indigo'), + Theme.new(4, 'Blue', 'ui-blue'), + Theme.new(7, 'Light Blue', 'ui-light-blue'), + Theme.new(5, 'Green', 'ui-green'), + Theme.new(8, 'Light Green', 'ui-light-green'), + Theme.new(9, 'Red', 'ui-red'), + Theme.new(10, 'Light Red', 'ui-light-red'), + Theme.new(2, 'Dark', 'ui-dark'), + Theme.new(3, 'Light', 'ui-light') ].freeze # Convenience method to get a space-separated String of all the theme diff --git a/lib/gitlab/url_blocker.rb b/lib/gitlab/url_blocker.rb index 20be193ea0c..38be75b7482 100644 --- a/lib/gitlab/url_blocker.rb +++ b/lib/gitlab/url_blocker.rb @@ -5,7 +5,7 @@ module Gitlab BlockedUrlError = Class.new(StandardError) class << self - def validate!(url, allow_localhost: false, allow_local_network: true, ports: [], protocols: []) + def validate!(url, allow_localhost: false, allow_local_network: true, enforce_user: false, ports: [], protocols: []) return true if url.nil? begin @@ -20,7 +20,7 @@ module Gitlab port = uri.port || uri.default_port validate_protocol!(uri.scheme, protocols) validate_port!(port, ports) if ports.any? - validate_user!(uri.user) + validate_user!(uri.user) if enforce_user validate_hostname!(uri.hostname) begin diff --git a/lib/gitlab/url_builder.rb b/lib/gitlab/url_builder.rb index 824e2d7251f..e64033b0dba 100644 --- a/lib/gitlab/url_builder.rb +++ b/lib/gitlab/url_builder.rb @@ -26,6 +26,8 @@ module Gitlab project_snippet_url(object.project, object) when Snippet snippet_url(object) + when Milestone + milestone_url(object) else raise NotImplementedError.new("No URL builder defined for #{object.class}") end diff --git a/lib/gitlab/usage_data.rb b/lib/gitlab/usage_data.rb index e294f3c4ebc..59a222b086c 100644 --- a/lib/gitlab/usage_data.rb +++ b/lib/gitlab/usage_data.rb @@ -21,6 +21,7 @@ module Gitlab uuid: Gitlab::CurrentSettings.uuid, hostname: Gitlab.config.gitlab.host, version: Gitlab::VERSION, + installation_type: Gitlab::INSTALLATION_TYPE, active_user_count: User.active.count, recorded_at: Time.now, mattermost_enabled: Gitlab.config.mattermost.enabled, diff --git a/lib/gitlab/user_access.rb b/lib/gitlab/user_access.rb index 8cf5d636743..27560abfb96 100644 --- a/lib/gitlab/user_access.rb +++ b/lib/gitlab/user_access.rb @@ -65,7 +65,7 @@ module Gitlab return false unless can_access_git? return false unless project - return false if !user.can?(:push_code, project) && !project.branch_allows_maintainer_push?(user, ref) + return false if !user.can?(:push_code, project) && !project.branch_allows_collaboration?(user, ref) if protected?(ProtectedBranch, project, ref) protected_branch_accessible_to?(ref, action: :push) diff --git a/lib/gitlab/utils/override.rb b/lib/gitlab/utils/override.rb index 8bf6bcb1fe2..7b2a62fed48 100644 --- a/lib/gitlab/utils/override.rb +++ b/lib/gitlab/utils/override.rb @@ -87,18 +87,28 @@ module Gitlab end def included(base = nil) - return super if base.nil? # Rails concern, ignoring it + super + + queue_verification(base) + end + alias_method :prepended, :included + + def extended(mod) super + queue_verification(mod.singleton_class) + end + + def queue_verification(base) + return unless ENV['STATIC_VERIFICATION'] + if base.is_a?(Class) # We could check for Class in `override` # This could be `nil` if `override` was never called Override.extensions[self]&.add_class(base) end end - alias_method :prepended, :included - def self.extensions @extensions ||= {} end diff --git a/lib/gitlab/verify/batch_verifier.rb b/lib/gitlab/verify/batch_verifier.rb index 1ef369a4b67..167ba1b3149 100644 --- a/lib/gitlab/verify/batch_verifier.rb +++ b/lib/gitlab/verify/batch_verifier.rb @@ -7,13 +7,15 @@ module Gitlab @batch_size = batch_size @start = start @finish = finish + + fix_google_api_logger end # Yields a Range of IDs and a Hash of failed verifications (object => error) def run_batches(&blk) - relation.in_batches(of: batch_size, start: start, finish: finish) do |relation| # rubocop: disable Cop/InBatches - range = relation.first.id..relation.last.id - failures = run_batch(relation) + all_relation.in_batches(of: batch_size, start: start, finish: finish) do |batch| # rubocop: disable Cop/InBatches + range = batch.first.id..batch.last.id + failures = run_batch_for(batch) yield(range, failures) end @@ -29,24 +31,56 @@ module Gitlab private - def run_batch(relation) - relation.map { |upload| verify(upload) }.compact.to_h + def run_batch_for(batch) + batch.map { |upload| verify(upload) }.compact.to_h end def verify(object) + local?(object) ? verify_local(object) : verify_remote(object) + rescue => err + failure(object, err.inspect) + end + + def verify_local(object) expected = expected_checksum(object) actual = actual_checksum(object) - raise 'Checksum missing' unless expected.present? - raise 'Checksum mismatch' unless expected == actual + return failure(object, 'Checksum missing') unless expected.present? + return failure(object, 'Checksum mismatch') unless expected == actual + + success + end + # We don't calculate checksum for remote objects, so just check existence + def verify_remote(object) + return failure(object, 'Remote object does not exist') unless remote_object_exists?(object) + + success + end + + def success nil - rescue => err - [object, err] + end + + def failure(object, message) + [object, message] + end + + # It's already set to Logger::INFO, but acts as if it is set to + # Logger::DEBUG, and this fixes it... + def fix_google_api_logger + if Object.const_defined?('Google::Apis') + Google::Apis.logger.level = Logger::INFO + end end # This should return an ActiveRecord::Relation suitable for calling #in_batches on - def relation + def all_relation + raise NotImplementedError.new + end + + # Should return true if the object is stored locally + def local?(_object) raise NotImplementedError.new end @@ -59,6 +93,11 @@ module Gitlab def actual_checksum(_object) raise NotImplementedError.new end + + # Be sure to perform a hard check of the remote object (don't just check DB value) + def remote_object_exists?(object) + raise NotImplementedError.new + end end end end diff --git a/lib/gitlab/verify/job_artifacts.rb b/lib/gitlab/verify/job_artifacts.rb index 03500a61074..dbadfbde9e3 100644 --- a/lib/gitlab/verify/job_artifacts.rb +++ b/lib/gitlab/verify/job_artifacts.rb @@ -11,10 +11,14 @@ module Gitlab private - def relation + def all_relation ::Ci::JobArtifact.all end + def local?(artifact) + artifact.local_store? + end + def expected_checksum(artifact) artifact.file_sha256 end @@ -22,6 +26,10 @@ module Gitlab def actual_checksum(artifact) Digest::SHA256.file(artifact.file.path).hexdigest end + + def remote_object_exists?(artifact) + artifact.file.file.exists? + end end end end diff --git a/lib/gitlab/verify/lfs_objects.rb b/lib/gitlab/verify/lfs_objects.rb index 970e2a7b718..d3f58a73ac7 100644 --- a/lib/gitlab/verify/lfs_objects.rb +++ b/lib/gitlab/verify/lfs_objects.rb @@ -11,8 +11,12 @@ module Gitlab private - def relation - LfsObject.with_files_stored_locally + def all_relation + LfsObject.all + end + + def local?(lfs_object) + lfs_object.local_store? end def expected_checksum(lfs_object) @@ -22,6 +26,10 @@ module Gitlab def actual_checksum(lfs_object) LfsObject.calculate_oid(lfs_object.file.path) end + + def remote_object_exists?(lfs_object) + lfs_object.file.file.exists? + end end end end diff --git a/lib/gitlab/verify/rake_task.rb b/lib/gitlab/verify/rake_task.rb index dd138e6b92b..e190eaddc79 100644 --- a/lib/gitlab/verify/rake_task.rb +++ b/lib/gitlab/verify/rake_task.rb @@ -45,7 +45,7 @@ module Gitlab return unless verbose? failures.each do |object, error| - say " - #{verifier.describe(object)}: #{error.inspect}".color(:red) + say " - #{verifier.describe(object)}: #{error}".color(:red) end end end diff --git a/lib/gitlab/verify/uploads.rb b/lib/gitlab/verify/uploads.rb index 0ffa71a6d72..01f09ab8df7 100644 --- a/lib/gitlab/verify/uploads.rb +++ b/lib/gitlab/verify/uploads.rb @@ -11,8 +11,12 @@ module Gitlab private - def relation - Upload.with_files_stored_locally + def all_relation + Upload.all + end + + def local?(upload) + upload.local? end def expected_checksum(upload) @@ -22,6 +26,10 @@ module Gitlab def actual_checksum(upload) Upload.hexdigest(upload.absolute_path) end + + def remote_object_exists?(upload) + upload.build_uploader.file.exists? + end end end end diff --git a/lib/gitlab/wiki_file_finder.rb b/lib/gitlab/wiki_file_finder.rb new file mode 100644 index 00000000000..f97278f05cd --- /dev/null +++ b/lib/gitlab/wiki_file_finder.rb @@ -0,0 +1,23 @@ +module Gitlab + class WikiFileFinder < FileFinder + attr_reader :repository + + def initialize(project, ref) + @project = project + @ref = ref + @repository = project.wiki.repository + end + + private + + def search_filenames(query, except) + safe_query = Regexp.escape(query.tr(' ', '-')) + safe_query = Regexp.new(safe_query, Regexp::IGNORECASE) + filenames = repository.ls_files(ref) + + filenames.delete_if { |filename| except.include?(filename) } unless except.empty? + + filenames.grep(safe_query).first(BATCH_SIZE) + end + end +end diff --git a/lib/microsoft_teams/notifier.rb b/lib/microsoft_teams/notifier.rb index c08d3e933a8..226ee1373db 100644 --- a/lib/microsoft_teams/notifier.rb +++ b/lib/microsoft_teams/notifier.rb @@ -30,7 +30,7 @@ module MicrosoftTeams result = { 'sections' => [] } result['title'] = options[:title] - result['summary'] = options[:pretext] + result['summary'] = options[:summary] result['sections'] << MicrosoftTeams::Activity.new(options[:activity]).prepare attachments = options[:attachments] diff --git a/lib/object_storage/direct_upload.rb b/lib/object_storage/direct_upload.rb new file mode 100644 index 00000000000..61a69e7ffe4 --- /dev/null +++ b/lib/object_storage/direct_upload.rb @@ -0,0 +1,166 @@ +module ObjectStorage + # + # The DirectUpload c;ass generates a set of presigned URLs + # that can be used to upload data to object storage from untrusted component: Workhorse, Runner? + # + # For Google it assumes that the platform supports variable Content-Length. + # + # For AWS it initiates Multipart Upload and presignes a set of part uploads. + # Class calculates the best part size to be able to upload up to asked maximum size. + # The number of generated parts will never go above 100, + # but we will always try to reduce amount of generated parts. + # The part size is rounded-up to 5MB. + # + class DirectUpload + include Gitlab::Utils::StrongMemoize + + TIMEOUT = 4.hours + EXPIRE_OFFSET = 15.minutes + + MAXIMUM_MULTIPART_PARTS = 100 + MINIMUM_MULTIPART_SIZE = 5.megabytes + + attr_reader :credentials, :bucket_name, :object_name + attr_reader :has_length, :maximum_size + + def initialize(credentials, bucket_name, object_name, has_length:, maximum_size: nil) + unless has_length + raise ArgumentError, 'maximum_size has to be specified if length is unknown' unless maximum_size + end + + @credentials = credentials + @bucket_name = bucket_name + @object_name = object_name + @has_length = has_length + @maximum_size = maximum_size + end + + def to_hash + { + Timeout: TIMEOUT, + GetURL: get_url, + StoreURL: store_url, + DeleteURL: delete_url, + MultipartUpload: multipart_upload_hash + }.compact + end + + def multipart_upload_hash + return unless requires_multipart_upload? + + { + PartSize: rounded_multipart_part_size, + PartURLs: multipart_part_urls, + CompleteURL: multipart_complete_url, + AbortURL: multipart_abort_url + } + end + + def provider + credentials[:provider].to_s + end + + # Implements https://docs.aws.amazon.com/AmazonS3/latest/API/RESTObjectGET.html + def get_url + connection.get_object_url(bucket_name, object_name, expire_at) + end + + # Implements https://docs.aws.amazon.com/AmazonS3/latest/API/RESTObjectDELETE.html + def delete_url + connection.delete_object_url(bucket_name, object_name, expire_at) + end + + # Implements https://docs.aws.amazon.com/AmazonS3/latest/API/RESTObjectPUT.html + def store_url + connection.put_object_url(bucket_name, object_name, expire_at, upload_options) + end + + def multipart_part_urls + Array.new(number_of_multipart_parts) do |part_index| + multipart_part_upload_url(part_index + 1) + end + end + + # Implements https://docs.aws.amazon.com/AmazonS3/latest/API/mpUploadUploadPart.html + def multipart_part_upload_url(part_number) + connection.signed_url({ + method: 'PUT', + bucket_name: bucket_name, + object_name: object_name, + query: { uploadId: upload_id, partNumber: part_number }, + headers: upload_options + }, expire_at) + end + + # Implements https://docs.aws.amazon.com/AmazonS3/latest/API/mpUploadComplete.html + def multipart_complete_url + connection.signed_url({ + method: 'POST', + bucket_name: bucket_name, + object_name: object_name, + query: { uploadId: upload_id }, + headers: { 'Content-Type' => 'application/xml' } + }, expire_at) + end + + # Implements https://docs.aws.amazon.com/AmazonS3/latest/API/mpUploadAbort.html + def multipart_abort_url + connection.signed_url({ + method: 'DELETE', + bucket_name: bucket_name, + object_name: object_name, + query: { uploadId: upload_id } + }, expire_at) + end + + private + + def rounded_multipart_part_size + # round multipart_part_size up to minimum_mulitpart_size + (multipart_part_size + MINIMUM_MULTIPART_SIZE - 1) / MINIMUM_MULTIPART_SIZE * MINIMUM_MULTIPART_SIZE + end + + def multipart_part_size + maximum_size / number_of_multipart_parts + end + + def number_of_multipart_parts + [ + # round maximum_size up to minimum_mulitpart_size + (maximum_size + MINIMUM_MULTIPART_SIZE - 1) / MINIMUM_MULTIPART_SIZE, + MAXIMUM_MULTIPART_PARTS + ].min + end + + def aws? + provider == 'AWS' + end + + def requires_multipart_upload? + aws? && !has_length + end + + def upload_id + return unless requires_multipart_upload? + + strong_memoize(:upload_id) do + new_upload = connection.initiate_multipart_upload(bucket_name, object_name) + new_upload.body["UploadId"] + end + end + + def expire_at + strong_memoize(:expire_at) do + Time.now + TIMEOUT + EXPIRE_OFFSET + end + end + + def upload_options + { 'Content-Type' => 'application/octet-stream' } + end + + def connection + @connection ||= ::Fog::Storage.new(credentials) + end + end +end diff --git a/lib/peek/rblineprof/custom_controller_helpers.rb b/lib/peek/rblineprof/custom_controller_helpers.rb index da24a36603e..9beb442bfa3 100644 --- a/lib/peek/rblineprof/custom_controller_helpers.rb +++ b/lib/peek/rblineprof/custom_controller_helpers.rb @@ -41,7 +41,7 @@ module Peek ] end.sort_by{ |a,b,c,d,e,f| -f } - output = "<div class='modal-dialog modal-lg'><div class='modal-content'>" + output = "<div class='modal-dialog modal-xl'><div class='modal-content'>" output << "<div class='modal-header'>" output << "<h4>Line profiling: #{human_description(params[:lineprofiler])}</h4>" output << "<button class='close' type='button' data-dismiss='modal' aria-label='close'><span aria-hidden='true'>×</span></button>" diff --git a/lib/system_check/orphans/namespace_check.rb b/lib/system_check/orphans/namespace_check.rb index b5f443abe06..09b57c7b408 100644 --- a/lib/system_check/orphans/namespace_check.rb +++ b/lib/system_check/orphans/namespace_check.rb @@ -4,13 +4,15 @@ module SystemCheck set_name 'Orphaned namespaces:' def multi_check - Gitlab.config.repositories.storages.each do |storage_name, repository_storage| - $stdout.puts - $stdout.puts "* Storage: #{storage_name} (#{repository_storage.legacy_disk_path})".color(:yellow) - toplevel_namespace_dirs = disk_namespaces(repository_storage.legacy_disk_path) - - orphans = (toplevel_namespace_dirs - existing_namespaces) - print_orphans(orphans, storage_name) + Gitlab::GitalyClient::StorageSettings.allow_disk_access do + Gitlab.config.repositories.storages.each do |storage_name, repository_storage| + $stdout.puts + $stdout.puts "* Storage: #{storage_name} (#{repository_storage.legacy_disk_path})".color(:yellow) + toplevel_namespace_dirs = disk_namespaces(repository_storage.legacy_disk_path) + + orphans = (toplevel_namespace_dirs - existing_namespaces) + print_orphans(orphans, storage_name) + end end clear_namespaces! # releases memory when check finishes diff --git a/lib/system_check/orphans/repository_check.rb b/lib/system_check/orphans/repository_check.rb index 5ef0b93ad08..2695c658874 100644 --- a/lib/system_check/orphans/repository_check.rb +++ b/lib/system_check/orphans/repository_check.rb @@ -5,16 +5,18 @@ module SystemCheck attr_accessor :orphans def multi_check - Gitlab.config.repositories.storages.each do |storage_name, repository_storage| - storage_path = repository_storage.legacy_disk_path + Gitlab::GitalyClient::StorageSettings.allow_disk_access do + Gitlab.config.repositories.storages.each do |storage_name, repository_storage| + storage_path = repository_storage.legacy_disk_path - $stdout.puts - $stdout.puts "* Storage: #{storage_name} (#{storage_path})".color(:yellow) + $stdout.puts + $stdout.puts "* Storage: #{storage_name} (#{storage_path})".color(:yellow) - repositories = disk_repositories(storage_path) - orphans = (repositories - fetch_repositories(storage_name)) + repositories = disk_repositories(storage_path) + orphans = (repositories - fetch_repositories(storage_name)) - print_orphans(orphans, storage_name) + print_orphans(orphans, storage_name) + end end end diff --git a/lib/system_check/simple_executor.rb b/lib/system_check/simple_executor.rb index d268f501b4a..99c9e984107 100644 --- a/lib/system_check/simple_executor.rb +++ b/lib/system_check/simple_executor.rb @@ -43,7 +43,7 @@ module SystemCheck # # @param [SystemCheck::BaseCheck] check_klass def run_check(check_klass) - $stdout.print "#{check_klass.display_name} ... " + print_display_name(check_klass) check = check_klass.new @@ -60,18 +60,18 @@ module SystemCheck end if check.check? - $stdout.puts check_klass.check_pass.color(:green) + print_check_pass(check_klass) else - $stdout.puts check_klass.check_fail.color(:red) + print_check_failure(check_klass) if check.can_repair? $stdout.print 'Trying to fix error automatically. ...' if check.repair! - $stdout.puts 'Success'.color(:green) + print_success return else - $stdout.puts 'Failed'.color(:red) + print_failure end end @@ -83,6 +83,26 @@ module SystemCheck private + def print_display_name(check_klass) + $stdout.print "#{check_klass.display_name} ... " + end + + def print_check_pass(check_klass) + $stdout.puts check_klass.check_pass.color(:green) + end + + def print_check_failure(check_klass) + $stdout.puts check_klass.check_fail.color(:red) + end + + def print_success + $stdout.puts 'Success'.color(:green) + end + + def print_failure + $stdout.puts 'Failed'.color(:red) + end + # Prints header content for the series of checks to be executed for this component # # @param [String] component name of the component relative to the checks being executed diff --git a/lib/tasks/gettext.rake b/lib/tasks/gettext.rake index 247d7be7d78..d52c419d66b 100644 --- a/lib/tasks/gettext.rake +++ b/lib/tasks/gettext.rake @@ -50,6 +50,32 @@ namespace :gettext do end end + task :updated_check do + # Removing all pre-translated files speeds up `gettext:find` as the + # files don't need to be merged. + `rm locale/*/gitlab.po` + + # `gettext:find` writes touches to temp files to `stderr` which would cause + # `static-analysis` to report failures. We can ignore these + silence_stream(STDERR) { Rake::Task['gettext:find'].invoke } + + changed_files = `git diff --name-only`.lines.map(&:strip) + + # reset the locale folder for potential next tasks + `git checkout -- locale` + + if changed_files.include?('locale/gitlab.pot') + raise <<~MSG + Newly translated strings found, please add them to `gitlab.pot` by running: + + bundle exec rake gettext:find; git checkout -- locale/*/gitlab.po; + + Then commit and push the resulting changes to `locale/gitlab.pot`. + + MSG + end + end + def report_errors_for_file(file, errors_for_file) puts "Errors in `#{file}`:" diff --git a/lib/tasks/gitlab/check.rake b/lib/tasks/gitlab/check.rake index c04dae7446f..a8acafa9cd9 100644 --- a/lib/tasks/gitlab/check.rake +++ b/lib/tasks/gitlab/check.rake @@ -44,11 +44,13 @@ namespace :gitlab do start_checking "GitLab Shell" check_gitlab_shell - check_repo_base_exists - check_repo_base_is_not_symlink - check_repo_base_user_and_group - check_repo_base_permissions - check_repos_hooks_directory_is_link + Gitlab::GitalyClient::StorageSettings.allow_disk_access do + check_repo_base_exists + check_repo_base_is_not_symlink + check_repo_base_user_and_group + check_repo_base_permissions + check_repos_hooks_directory_is_link + end check_gitlab_shell_self_test finished_checking "GitLab Shell" diff --git a/lib/tasks/gitlab/cleanup.rake b/lib/tasks/gitlab/cleanup.rake index d6d15285489..52ae1330d7f 100644 --- a/lib/tasks/gitlab/cleanup.rake +++ b/lib/tasks/gitlab/cleanup.rake @@ -12,7 +12,7 @@ namespace :gitlab do namespaces = Namespace.pluck(:path) namespaces << HASHED_REPOSITORY_NAME # add so that it will be ignored Gitlab.config.repositories.storages.each do |name, repository_storage| - git_base_path = repository_storage.legacy_disk_path + git_base_path = Gitlab::GitalyClient::StorageSettings.allow_disk_access { repository_storage.legacy_disk_path } all_dirs = Dir.glob(git_base_path + '/*') puts git_base_path.color(:yellow) @@ -54,7 +54,8 @@ namespace :gitlab do move_suffix = "+orphaned+#{Time.now.to_i}" Gitlab.config.repositories.storages.each do |name, repository_storage| - repo_root = repository_storage.legacy_disk_path + repo_root = Gitlab::GitalyClient::StorageSettings.allow_disk_access { repository_storage.legacy_disk_path } + # Look for global repos (legacy, depth 1) and normal repos (depth 2) IO.popen(%W(find #{repo_root} -mindepth 1 -maxdepth 2 -name *.git)) do |find| find.each_line do |path| diff --git a/lib/tasks/gitlab/info.rake b/lib/tasks/gitlab/info.rake index 289aa5d9060..6de739e9515 100644 --- a/lib/tasks/gitlab/info.rake +++ b/lib/tasks/gitlab/info.rake @@ -67,8 +67,10 @@ namespace :gitlab do puts "GitLab Shell".color(:yellow) puts "Version:\t#{gitlab_shell_version || "unknown".color(:red)}" puts "Repository storage paths:" - Gitlab.config.repositories.storages.each do |name, repository_storage| - puts "- #{name}: \t#{repository_storage.legacy_disk_path}" + Gitlab::GitalyClient::StorageSettings.allow_disk_access do + Gitlab.config.repositories.storages.each do |name, repository_storage| + puts "- #{name}: \t#{repository_storage.legacy_disk_path}" + end end puts "Hooks:\t\t#{Gitlab.config.gitlab_shell.hooks_path}" puts "Git:\t\t#{Gitlab.config.git.bin_path}" diff --git a/lib/tasks/gitlab/storage.rake b/lib/tasks/gitlab/storage.rake index 68d6f9d7cb1..f539b1df955 100644 --- a/lib/tasks/gitlab/storage.rake +++ b/lib/tasks/gitlab/storage.rake @@ -2,9 +2,26 @@ namespace :gitlab do namespace :storage do desc 'GitLab | Storage | Migrate existing projects to Hashed Storage' task migrate_to_hashed: :environment do - legacy_projects_count = Project.with_unmigrated_storage.count + storage_migrator = Gitlab::HashedStorage::Migrator.new helper = Gitlab::HashedStorage::RakeHelper + if helper.range_single_item? + project = Project.with_unmigrated_storage.find_by(id: helper.range_from) + + unless project + puts "There are no projects requiring storage migration with ID=#{helper.range_from}" + + next + end + + puts "Enqueueing storage migration of #{project.full_path} (ID=#{project.id})..." + storage_migrator.migrate(project) + + next + end + + legacy_projects_count = Project.with_unmigrated_storage.count + if legacy_projects_count == 0 puts 'There are no projects requiring storage migration. Nothing to do!' @@ -14,7 +31,7 @@ namespace :gitlab do print "Enqueuing migration of #{legacy_projects_count} projects in batches of #{helper.batch_size}" helper.project_id_batches do |start, finish| - StorageMigratorWorker.perform_async(start, finish) + storage_migrator.bulk_schedule(start, finish) print '.' end diff --git a/lib/tasks/gitlab/traces.rake b/lib/tasks/gitlab/traces.rake index fd2a4f2d11a..ddcca69711f 100644 --- a/lib/tasks/gitlab/traces.rake +++ b/lib/tasks/gitlab/traces.rake @@ -8,9 +8,7 @@ namespace :gitlab do logger = Logger.new(STDOUT) logger.info('Archiving legacy traces') - Ci::Build.finished - .where('NOT EXISTS (?)', - Ci::JobArtifact.select(1).trace.where('ci_builds.id = ci_job_artifacts.job_id')) + Ci::Build.finished.without_archived_trace .order(id: :asc) .find_in_batches(batch_size: 1000) do |jobs| job_ids = jobs.map { |job| [job.id] } diff --git a/lib/tasks/import.rake b/lib/tasks/import.rake index aafbe52e5f8..fc59b3f937d 100644 --- a/lib/tasks/import.rake +++ b/lib/tasks/import.rake @@ -9,7 +9,10 @@ class GithubImport def initialize(token, gitlab_username, project_path, extras) @options = { token: token } @project_path = project_path - @current_user = User.find_by_username(gitlab_username) + @current_user = User.find_by(username: gitlab_username) + + raise "GitLab user #{gitlab_username} not found. Please specify a valid username." unless @current_user + @github_repo = extras.empty? ? nil : extras.first end @@ -50,7 +53,7 @@ class GithubImport end if import_success - @project.import_finish + @project.after_import puts "Import finished. Timings: #{timings}".color(:green) else puts "Import was not successful. Errors were as follows:" diff --git a/lib/tasks/lint.rake b/lib/tasks/lint.rake index 8b86a5c72a5..b5a9cddaacb 100644 --- a/lib/tasks/lint.rake +++ b/lib/tasks/lint.rake @@ -27,6 +27,7 @@ unless Rails.env.production? scss_lint flay gettext:lint + gettext:updated_check lint:static_verification ].each do |task| pid = Process.fork do diff --git a/lib/tasks/migrate/setup_postgresql.rake b/lib/tasks/migrate/setup_postgresql.rake index e7aab50e42a..f69d204c579 100644 --- a/lib/tasks/migrate/setup_postgresql.rake +++ b/lib/tasks/migrate/setup_postgresql.rake @@ -22,3 +22,18 @@ task setup_postgresql: :environment do ProjectNameLowerIndex.new.up AddPathIndexToRedirectRoutes.new.up end + +desc 'GitLab | Generate PostgreSQL Password Hash' +task :postgresql_md5_hash do + require 'digest' + username = ENV.fetch('USERNAME') do |missing| + puts "You must provide an username with '#{missing}' ENV variable" + exit(1) + end + password = ENV.fetch('PASSWORD') do |missing| + puts "You must provide a password with '#{missing}' ENV variable" + exit(1) + end + hash = Digest::MD5.hexdigest("#{password}#{username}") + puts "The MD5 hash of your database password for user: #{username} -> #{hash}" +end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 035a2275d9f..db5c183bac3 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -8,8 +8,8 @@ msgid "" msgstr "" "Project-Id-Version: gitlab 1.0.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-05-29 09:43-0500\n" -"PO-Revision-Date: 2018-05-29 09:43-0500\n" +"POT-Creation-Date: 2018-06-13 14:05+0200\n" +"PO-Revision-Date: 2018-06-13 14:05+0200\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Language-Team: LANGUAGE <LL@li.org>\n" "Language: \n" @@ -133,12 +133,12 @@ msgid "- show less" msgstr "" msgid "1 %{type} addition" -msgid_plural "%d %{type} additions" +msgid_plural "%{count} %{type} additions" msgstr[0] "" msgstr[1] "" msgid "1 %{type} modification" -msgid_plural "%d %{type} modifications" +msgid_plural "%{count} %{type} modifications" msgstr[0] "" msgstr[1] "" @@ -232,7 +232,7 @@ msgstr "" msgid "Account" msgstr "" -msgid "Account and limit settings" +msgid "Account and limit" msgstr "" msgid "Active" @@ -262,6 +262,9 @@ msgstr "" msgid "Add new directory" msgstr "" +msgid "Add reaction" +msgstr "" + msgid "Add todo" msgstr "" @@ -331,7 +334,7 @@ msgstr "" msgid "All features are enabled for blank projects, from templates, or when importing, but you can disable them afterward in the project settings." msgstr "" -msgid "Allow edits from maintainers." +msgid "Allow commits from members who can merge to the target branch." msgstr "" msgid "Allow rendering of PlantUML diagrams in Asciidoc documents." @@ -370,7 +373,7 @@ msgstr "" msgid "An error occurred while getting projects" msgstr "" -msgid "An error occurred while importing project" +msgid "An error occurred while importing project: ${details}" msgstr "" msgid "An error occurred while loading commits" @@ -439,7 +442,7 @@ msgstr "" msgid "Artifacts" msgstr "" -msgid "Ask your group master to setup a group Runner." +msgid "Ask your group maintainer to setup a group Runner." msgstr "" msgid "Assign custom color like #FF0000" @@ -463,6 +466,9 @@ msgstr "" msgid "Assigned to :name" msgstr "" +msgid "Assigned to me" +msgstr "" + msgid "Assignee" msgstr "" @@ -616,9 +622,6 @@ msgstr "" msgid "Begin with the selected commit" msgstr "" -msgid "Blame" -msgstr "" - msgid "Branch (%{branch_count})" msgid_plural "Branches (%{branch_count})" msgstr[0] "" @@ -696,7 +699,7 @@ msgstr "" msgid "Branches|Once you confirm and press %{delete_protected_branch}, it cannot be undone or recovered." msgstr "" -msgid "Branches|Only a project master or owner can delete a protected branch" +msgid "Branches|Only a project maintainer or owner can delete a protected branch" msgstr "" msgid "Branches|Overview" @@ -780,9 +783,6 @@ msgstr "" msgid "CI/CD settings" msgstr "" -msgid "CICD|A domain is required to use Auto Review Apps and Auto Deploy Stages." -msgstr "" - msgid "CICD|An explicit %{ci_file} needs to be specified before you can begin using Continuous Integration and Delivery." msgstr "" @@ -792,6 +792,18 @@ msgstr "" msgid "CICD|Auto DevOps will automatically build, test, and deploy your application based on a predefined Continuous Integration and Delivery configuration." msgstr "" +msgid "CICD|Automatic deployment to staging, manual deployment to production" +msgstr "" + +msgid "CICD|Continuous deployment to production" +msgstr "" + +msgid "CICD|Deployment strategy" +msgstr "" + +msgid "CICD|Deployment strategy needs a domain name to work correctly." +msgstr "" + msgid "CICD|Disable Auto DevOps" msgstr "" @@ -813,6 +825,9 @@ msgstr "" msgid "CICD|The Auto DevOps pipeline configuration will be used when there is no %{ci_file} in the project." msgstr "" +msgid "CICD|You need to specify a domain if you want to use Auto Review Apps and Auto Deploy stages." +msgstr "" + msgid "Can run untagged jobs" msgstr "" @@ -1041,6 +1056,9 @@ msgstr "" msgid "ClusterIntegration|Copy Ingress IP Address to clipboard" msgstr "" +msgid "ClusterIntegration|Copy Jupyter Hostname to clipboard" +msgstr "" + msgid "ClusterIntegration|Copy Kubernetes cluster name" msgstr "" @@ -1086,7 +1104,7 @@ msgstr "" msgid "ClusterIntegration|GitLab Runner" msgstr "" -msgid "ClusterIntegration|Google Cloud Platform project ID" +msgid "ClusterIntegration|Google Cloud Platform project" msgstr "" msgid "ClusterIntegration|Google Kubernetes Engine" @@ -1119,6 +1137,12 @@ msgstr "" msgid "ClusterIntegration|Integration status" msgstr "" +msgid "ClusterIntegration|Jupyter Hostname" +msgstr "" + +msgid "ClusterIntegration|JupyterHub" +msgstr "" + msgid "ClusterIntegration|Kubernetes cluster" msgstr "" @@ -1519,6 +1543,9 @@ msgstr "" msgid "ContainerRegistry|You can also use a %{deploy_token} for read-only access to the registry images." msgstr "" +msgid "Continue" +msgstr "" + msgid "Continuous Integration and Deployment" msgstr "" @@ -1558,6 +1585,9 @@ msgstr "" msgid "Copy reference to clipboard" msgstr "" +msgid "Copy to clipboard" +msgstr "" + msgid "Create" msgstr "" @@ -1627,6 +1657,9 @@ msgstr "" msgid "Created" msgstr "" +msgid "Created by me" +msgstr "" + msgid "Cron Timezone" msgstr "" @@ -1818,6 +1851,9 @@ msgstr "" msgid "DeployTokens|Your new project deploy token has been created." msgstr "" +msgid "Deprioritize label" +msgstr "" + msgid "Description" msgstr "" @@ -2010,6 +2046,9 @@ msgstr "" msgid "Error fetching contributors data." msgstr "" +msgid "Error fetching job trace" +msgstr "" + msgid "Error fetching labels." msgstr "" @@ -2028,6 +2067,9 @@ msgstr "" msgid "Error loading last commit." msgstr "" +msgid "Error loading merge requests." +msgstr "" + msgid "Error loading project data. Please try again." msgstr "" @@ -2106,6 +2148,9 @@ msgstr "" msgid "Failed to update issues, please try again." msgstr "" +msgid "Failure" +msgstr "" + msgid "Feb" msgstr "" @@ -2156,6 +2201,9 @@ msgstr "" msgid "Format" msgstr "" +msgid "Found errors in your .gitlab-ci.yml:" +msgstr "" + msgid "From %{provider_title}" msgstr "" @@ -2207,6 +2255,9 @@ msgstr "" msgid "Gitaly Servers" msgstr "" +msgid "Gitaly|Address" +msgstr "" + msgid "Go Back" msgstr "" @@ -2237,7 +2288,7 @@ msgstr "" msgid "Group Runners" msgstr "" -msgid "Group masters can register group runners in the %{link}" +msgid "Group maintainers can register group runners in the %{link}" msgstr "" msgid "GroupSettings|Prevent sharing a project within %{group} with other groups" @@ -2341,6 +2392,12 @@ msgstr "" msgid "Housekeeping successfully started" msgstr "" +msgid "I accept the %{terms_link}" +msgstr "" + +msgid "I accept the|Terms of Service and Privacy Policy" +msgstr "" + msgid "IDE|Commit" msgstr "" @@ -2350,6 +2407,9 @@ msgstr "" msgid "IDE|Go back" msgstr "" +msgid "IDE|Open in file view" +msgstr "" + msgid "IDE|Review" msgstr "" @@ -2359,6 +2419,15 @@ msgstr "" msgid "If your HTTP repository is not publicly accessible, add authentication information to the URL: <code>https://username:password@gitlab.company.com/group/project.git</code>." msgstr "" +msgid "ImageDiffViewer|2-up" +msgstr "" + +msgid "ImageDiffViewer|Onion skin" +msgstr "" + +msgid "ImageDiffViewer|Swipe" +msgstr "" + msgid "Import" msgstr "" @@ -2374,7 +2443,7 @@ msgstr "" msgid "Import repository" msgstr "" -msgid "Include a Terms of Service agreement that all users must accept." +msgid "Include a Terms of Service agreement and Privacy Policy that all users must accept." msgstr "" msgid "Install Runner on Kubernetes" @@ -2476,6 +2545,9 @@ msgstr "" msgid "Label" msgstr "" +msgid "Label actions dropdown" +msgstr "" + msgid "LabelSelect|%{firstLabelName} +%{remainingLabelCount} more" msgstr "" @@ -2491,6 +2563,9 @@ msgstr "" msgid "Labels can be applied to issues and merge requests to categorize them." msgstr "" +msgid "Labels can be applied to issues and merge requests." +msgstr "" + msgid "Labels|<span>Promote label</span> %{labelTitle} <span>to Group Label?</span>" msgstr "" @@ -2801,7 +2876,10 @@ msgstr "" msgid "No files found." msgstr "" -msgid "No labels created yet." +msgid "No merge requests found" +msgstr "" + +msgid "No messages were logged" msgstr "" msgid "No repository" @@ -2927,6 +3005,9 @@ msgstr "" msgid "Only project members can comment." msgstr "" +msgid "Open in Xcode" +msgstr "" + msgid "OpenedNDaysAgo|Opened" msgstr "" @@ -2939,6 +3020,9 @@ msgstr "" msgid "Options" msgstr "" +msgid "Other Labels" +msgstr "" + msgid "Otherwise it is recommended you start with one of the options below." msgstr "" @@ -2984,9 +3068,6 @@ msgstr "" msgid "Performance optimization" msgstr "" -msgid "Permalink" -msgstr "" - msgid "Permissions" msgstr "" @@ -3164,6 +3245,21 @@ msgstr "" msgid "Preferences" msgstr "" +msgid "Preferences|Navigation theme" +msgstr "" + +msgid "Prioritize" +msgstr "" + +msgid "Prioritize label" +msgstr "" + +msgid "Prioritized Labels" +msgstr "" + +msgid "Prioritized label" +msgstr "" + msgid "Private - Project access must be granted explicitly to each user." msgstr "" @@ -3389,10 +3485,10 @@ msgstr "" msgid "Promote these project milestones into a group milestone." msgstr "" -msgid "Promote to Group Label" +msgid "Promote to Group Milestone" msgstr "" -msgid "Promote to Group Milestone" +msgid "Promote to group label" msgstr "" msgid "Protip:" @@ -3416,9 +3512,6 @@ msgstr "" msgid "Quick actions can be used in the issues description and comment boxes." msgstr "" -msgid "Raw" -msgstr "" - msgid "Read more" msgstr "" @@ -3479,6 +3572,9 @@ msgstr "" msgid "Remove avatar" msgstr "" +msgid "Remove priority" +msgstr "" + msgid "Remove project" msgstr "" @@ -3488,7 +3584,7 @@ msgstr "" msgid "Repository maintenance" msgstr "" -msgid "Repository mirror settings" +msgid "Repository mirror" msgstr "" msgid "Repository storage" @@ -3497,7 +3593,7 @@ msgstr "" msgid "Request Access" msgstr "" -msgid "Require all users to accept Terms of Service when they access GitLab." +msgid "Require all users to accept Terms of Service and Privacy Policy when they access GitLab." msgstr "" msgid "Reset git storage health information" @@ -3556,9 +3652,6 @@ msgstr "" msgid "Runners can be placed on separate users, servers, and even on your local machine." msgstr "" -msgid "Runners settings" -msgstr "" - msgid "Running" msgstr "" @@ -3586,6 +3679,12 @@ msgstr "" msgid "Scheduling Pipelines" msgstr "" +msgid "Scroll to bottom" +msgstr "" + +msgid "Scroll to top" +msgstr "" + msgid "Search" msgstr "" @@ -3601,6 +3700,9 @@ msgstr "" msgid "Search for projects, issues, etc." msgstr "" +msgid "Search merge requests" +msgstr "" + msgid "Search milestones" msgstr "" @@ -3709,6 +3811,9 @@ msgstr "" msgid "Show command" msgstr "" +msgid "Show complete raw log" +msgstr "" + msgid "Show parent pages" msgstr "" @@ -3744,9 +3849,6 @@ msgstr "" msgid "Something went wrong when toggling the button" msgstr "" -msgid "Something went wrong while fetching the latest pipeline status." -msgstr "" - msgid "Something went wrong while fetching the projects." msgstr "" @@ -3870,6 +3972,12 @@ msgstr "" msgid "Specify the following URL during the Runner setup:" msgstr "" +msgid "Squash commits" +msgstr "" + +msgid "Stage" +msgstr "" + msgid "Stage all" msgstr "" @@ -3882,6 +3990,9 @@ msgstr "" msgid "Staged %{type}" msgstr "" +msgid "Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging." +msgstr "" + msgid "StarProject|Star" msgstr "" @@ -3921,6 +4032,15 @@ msgstr "" msgid "Subgroups" msgstr "" +msgid "Subscribe" +msgstr "" + +msgid "Subscribe at group level" +msgstr "" + +msgid "Subscribe at project level" +msgstr "" + msgid "Switch branch/tag" msgstr "" @@ -4001,7 +4121,7 @@ msgstr "" msgid "TagsPage|Use git tag command to add a new one:" msgstr "" -msgid "TagsPage|Write your release notes or drag files here..." +msgid "TagsPage|Write your release notes or drag files here…" msgstr "" msgid "TagsPage|protected" @@ -4016,10 +4136,10 @@ msgstr "" msgid "Team" msgstr "" -msgid "Terms of Service" +msgid "Terms of Service Agreement and Privacy Policy" msgstr "" -msgid "Terms of Service Agreement" +msgid "Terms of Service and Privacy Policy" msgstr "" msgid "The Issue Tracker is the place to add things that need to be improved or solved in a project" @@ -4247,6 +4367,9 @@ msgstr "" msgid "Timeago|%s days remaining" msgstr "" +msgid "Timeago|%s hours ago" +msgstr "" + msgid "Timeago|%s hours remaining" msgstr "" @@ -4262,6 +4385,9 @@ msgstr "" msgid "Timeago|%s months remaining" msgstr "" +msgid "Timeago|%s seconds ago" +msgstr "" + msgid "Timeago|%s seconds remaining" msgstr "" @@ -4277,46 +4403,43 @@ msgstr "" msgid "Timeago|%s years remaining" msgstr "" -msgid "Timeago|1 day remaining" +msgid "Timeago|1 day ago" msgstr "" -msgid "Timeago|1 hour remaining" +msgid "Timeago|1 day remaining" msgstr "" -msgid "Timeago|1 minute remaining" +msgid "Timeago|1 hour ago" msgstr "" -msgid "Timeago|1 month remaining" -msgstr "" - -msgid "Timeago|1 week remaining" +msgid "Timeago|1 hour remaining" msgstr "" -msgid "Timeago|1 year remaining" +msgid "Timeago|1 minute ago" msgstr "" -msgid "Timeago|Past due" +msgid "Timeago|1 minute remaining" msgstr "" -msgid "Timeago|a day ago" +msgid "Timeago|1 month ago" msgstr "" -msgid "Timeago|a month ago" +msgid "Timeago|1 month remaining" msgstr "" -msgid "Timeago|a week ago" +msgid "Timeago|1 week ago" msgstr "" -msgid "Timeago|a year ago" +msgid "Timeago|1 week remaining" msgstr "" -msgid "Timeago|about %s hours ago" +msgid "Timeago|1 year ago" msgstr "" -msgid "Timeago|about a minute ago" +msgid "Timeago|1 year remaining" msgstr "" -msgid "Timeago|about an hour ago" +msgid "Timeago|Past due" msgstr "" msgid "Timeago|in %s days" @@ -4358,7 +4481,7 @@ msgstr "" msgid "Timeago|in 1 year" msgstr "" -msgid "Timeago|less than a minute ago" +msgid "Timeago|just now" msgstr "" msgid "Timeago|right now" @@ -4464,6 +4587,15 @@ msgstr "" msgid "Unstar" msgstr "" +msgid "Unsubscribe" +msgstr "" + +msgid "Unsubscribe at group level" +msgstr "" + +msgid "Unsubscribe at project level" +msgstr "" + msgid "Unverified" msgstr "" @@ -4527,9 +4659,15 @@ msgstr "" msgid "View group labels" msgstr "" +msgid "View jobs" +msgstr "" + msgid "View labels" msgstr "" +msgid "View log" +msgstr "" + msgid "View open merge request" msgstr "" @@ -4608,7 +4746,7 @@ msgstr "" msgid "WikiEmptyIssueMessage|issue tracker" msgstr "" -msgid "WikiEmpty|A wiki is where you can store all the details about your project. This can include why you've created it, it's principles, how to use it, and so on." +msgid "WikiEmpty|A wiki is where you can store all the details about your project. This can include why you've created it, its principles, how to use it, and so on." msgstr "" msgid "WikiEmpty|Create your first page" @@ -4680,7 +4818,7 @@ msgstr "" msgid "WikiPage|Page slug" msgstr "" -msgid "WikiPage|Write your content or drag files here..." +msgid "WikiPage|Write your content or drag files here…" msgstr "" msgid "Wiki|Create Page" @@ -4740,6 +4878,9 @@ msgstr "" msgid "You can also star a label to make it a priority label." msgstr "" +msgid "You can also test your .gitlab-ci.yml in the %{linkStart}Lint%{linkEnd}" +msgstr "" + msgid "You can easily install a Runner on a Kubernetes cluster. %{link_to_help_page}" msgstr "" @@ -4758,13 +4899,22 @@ msgstr "" msgid "You cannot write to this read-only GitLab instance." msgstr "" +msgid "You do not have any assigned merge requests" +msgstr "" + msgid "You have no permissions" msgstr "" +msgid "You have not created any merge requests" +msgstr "" + msgid "You have reached your project limit" msgstr "" -msgid "You must have master access to force delete a lock" +msgid "You must accept our Terms of Service and privacy policy in order to register an account" +msgstr "" + +msgid "You must have maintainer access to force delete a lock" msgstr "" msgid "You must sign in to star a project" @@ -4894,7 +5044,7 @@ msgstr "" msgid "mrWidget|%{metricsLinkStart} Memory %{metricsLinkEnd} usage is %{emphasisStart} unchanged %{emphasisEnd} at %{memoryFrom}MB" msgstr "" -msgid "mrWidget|Allows edits from maintainers" +msgid "mrWidget|Allows commits from members who can merge to the target branch" msgstr "" msgid "mrWidget|Cancel automatic merge" @@ -5093,3 +5243,8 @@ msgstr "" msgid "with %{additions} additions, %{deletions} deletions." msgstr "" + +msgid "within %d minute " +msgid_plural "within %d minutes " +msgstr[0] "" +msgstr[1] "" diff --git a/package.json b/package.json index be3a6e4c9f6..6a52af10dc3 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,6 @@ "classlist-polyfill": "^1.2.0", "clipboard": "^1.7.1", "compression-webpack-plugin": "^1.1.11", - "copy-webpack-plugin": "^4.5.1", "core-js": "^2.4.1", "cropper": "^2.3.0", "css-loader": "^0.28.11", @@ -64,7 +63,8 @@ "jszip-utils": "^0.0.2", "katex": "^0.8.3", "marked": "^0.3.12", - "monaco-editor": "0.10.0", + "monaco-editor": "0.13.1", + "monaco-editor-webpack-plugin": "^1.2.1", "mousetrap": "^1.4.6", "pikaday": "^1.6.1", "popper.js": "^1.14.3", @@ -94,9 +94,9 @@ "vue-template-compiler": "^2.5.16", "vue-virtual-scroll-list": "^1.2.5", "vuex": "^3.0.1", - "webpack": "^4.7.0", + "webpack": "^4.11.1", "webpack-bundle-analyzer": "^2.11.1", - "webpack-cli": "^2.1.2", + "webpack-cli": "^3.0.2", "webpack-stats-plugin": "^0.2.1", "worker-loader": "^2.0.0" }, @@ -117,7 +117,7 @@ "eslint-plugin-import": "^2.12.0", "eslint-plugin-jasmine": "^2.1.0", "eslint-plugin-promise": "^3.8.0", - "eslint-plugin-vue": "^4.0.1", + "eslint-plugin-vue": "^4.5.0", "ignore": "^3.3.7", "istanbul": "^0.4.5", "jasmine-core": "^2.9.0", @@ -130,7 +130,7 @@ "karma-sourcemap-loader": "^0.3.7", "karma-webpack": "3.0.0", "nodemon": "^1.17.3", - "prettier": "1.11.1", + "prettier": "1.12.1", "webpack-dev-server": "^3.1.4" } } diff --git a/public/favicon.ico b/public/favicon.ico Binary files differdeleted file mode 100644 index 3479cbbb46f..00000000000 --- a/public/favicon.ico +++ /dev/null diff --git a/qa/Dockerfile b/qa/Dockerfile index 77cee9c5461..abf2184e1e2 100644 --- a/qa/Dockerfile +++ b/qa/Dockerfile @@ -28,6 +28,15 @@ RUN apt-get update -q && apt-get install -y google-chrome-stable && apt-get clea RUN wget -q https://chromedriver.storage.googleapis.com/$(wget -q -O - https://chromedriver.storage.googleapis.com/LATEST_RELEASE)/chromedriver_linux64.zip RUN unzip chromedriver_linux64.zip -d /usr/local/bin +## +# Install gcloud and kubectl CLI used in Auto DevOps test to create K8s +# clusters +# +RUN export CLOUD_SDK_REPO="cloud-sdk-$(lsb_release -c -s)" && \ + echo "deb http://packages.cloud.google.com/apt $CLOUD_SDK_REPO main" | tee -a /etc/apt/sources.list.d/google-cloud-sdk.list && \ + curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add - && \ + apt-get update -y && apt-get install google-cloud-sdk kubectl -y + WORKDIR /home/qa COPY ./Gemfile* ./ RUN bundle install @@ -12,7 +12,11 @@ module QA autoload :Browser, 'qa/runtime/browser' autoload :Env, 'qa/runtime/env' autoload :Address, 'qa/runtime/address' - autoload :API, 'qa/runtime/api' + + module API + autoload :Client, 'qa/runtime/api/client' + autoload :Request, 'qa/runtime/api/request' + end module Key autoload :Base, 'qa/runtime/key/base' @@ -41,6 +45,7 @@ module QA autoload :SecretVariable, 'qa/factory/resource/secret_variable' autoload :Runner, 'qa/factory/resource/runner' autoload :PersonalAccessToken, 'qa/factory/resource/personal_access_token' + autoload :KubernetesCluster, 'qa/factory/resource/kubernetes_cluster' end module Repository @@ -72,6 +77,7 @@ module QA module Integration autoload :LDAP, 'qa/scenario/test/integration/ldap' + autoload :Kubernetes, 'qa/scenario/test/integration/kubernetes' autoload :Mattermost, 'qa/scenario/test/integration/mattermost' end @@ -150,6 +156,15 @@ module QA autoload :Show, 'qa/page/project/issue/show' autoload :Index, 'qa/page/project/issue/index' end + + module Operations + module Kubernetes + autoload :Index, 'qa/page/project/operations/kubernetes/index' + autoload :Add, 'qa/page/project/operations/kubernetes/add' + autoload :AddExisting, 'qa/page/project/operations/kubernetes/add_existing' + autoload :Show, 'qa/page/project/operations/kubernetes/show' + end + end end module Profile @@ -195,6 +210,7 @@ module QA # module Service autoload :Shellout, 'qa/service/shellout' + autoload :KubernetesCluster, 'qa/service/kubernetes_cluster' autoload :Omnibus, 'qa/service/omnibus' autoload :Runner, 'qa/service/runner' end diff --git a/qa/qa/factory/repository/push.rb b/qa/qa/factory/repository/push.rb index 795f1f9cb1a..7c0d580c5ca 100644 --- a/qa/qa/factory/repository/push.rb +++ b/qa/qa/factory/repository/push.rb @@ -1,9 +1,11 @@ +require 'pathname' + module QA module Factory module Repository class Push < Factory::Base attr_accessor :file_name, :file_content, :commit_message, - :branch_name, :new_branch + :branch_name, :new_branch, :output attr_writer :remote_branch @@ -12,10 +14,14 @@ module QA project.description = 'Project with repository' end + product :output do |factory| + factory.output + end + def initialize @file_name = 'file.txt' @file_content = '# This is test project' - @commit_message = "Add #{@file_name}" + @commit_message = "This is a test commit" @branch_name = 'master' @new_branch = true end @@ -24,6 +30,12 @@ module QA @remote_branch ||= branch_name end + def directory=(dir) + raise "Must set directory as a Pathname" unless dir.is_a?(Pathname) + + @directory = dir + end + def fabricate! project.visit! @@ -43,9 +55,16 @@ module QA repository.checkout(branch_name) end - repository.add_file(file_name, file_content) + if @directory + @directory.each_child do |f| + repository.add_file(f.basename, f.read) if f.file? + end + else + repository.add_file(file_name, file_content) + end + repository.commit(commit_message) - repository.push_changes("#{branch_name}:#{remote_branch}") + @output = repository.push_changes("#{branch_name}:#{remote_branch}") end end end diff --git a/qa/qa/factory/resource/branch.rb b/qa/qa/factory/resource/branch.rb index 1785441f5a8..4cabe7eab45 100644 --- a/qa/qa/factory/resource/branch.rb +++ b/qa/qa/factory/resource/branch.rb @@ -46,7 +46,9 @@ module QA resource.remote_branch = @branch_name end - Page::Project::Show.act { wait_for_push } + Page::Project::Show.perform do |page| + page.wait { page.has_content?(branch_name) } + end # The upcoming process will make it access the Protected Branches page, # select the already created branch and protect it according @@ -62,13 +64,13 @@ module QA page.select_branch(branch_name) if allow_to_push - page.allow_devs_and_masters_to_push + page.allow_devs_and_maintainers_to_push else page.allow_no_one_to_push end if allow_to_merge - page.allow_devs_and_masters_to_merge + page.allow_devs_and_maintainers_to_merge else page.allow_no_one_to_merge end @@ -79,9 +81,13 @@ module QA page.protect_branch - # Wait for page load, which resets the expanded sections - page.wait(reload: false) do - !page.has_content?('Collapse') + # Avoid Selenium::WebDriver::Error::StaleElementReferenceError + # without sleeping. I.e. this completes fast on fast machines. + page.refresh + + # It is possible for the protected branch row to "disappear" at first + page.wait do + page.has_content?(branch_name) end end end diff --git a/qa/qa/factory/resource/kubernetes_cluster.rb b/qa/qa/factory/resource/kubernetes_cluster.rb new file mode 100644 index 00000000000..f32cf985e9d --- /dev/null +++ b/qa/qa/factory/resource/kubernetes_cluster.rb @@ -0,0 +1,55 @@ +require 'securerandom' + +module QA + module Factory + module Resource + class KubernetesCluster < Factory::Base + attr_writer :project, :cluster, + :install_helm_tiller, :install_ingress, :install_prometheus, :install_runner + + product :ingress_ip do + Page::Project::Operations::Kubernetes::Show.perform do |page| + page.ingress_ip + end + end + + def fabricate! + @project.visit! + + Page::Menu::Side.act { click_operations_kubernetes } + + Page::Project::Operations::Kubernetes::Index.perform do |page| + page.add_kubernetes_cluster + end + + Page::Project::Operations::Kubernetes::Add.perform do |page| + page.add_existing_cluster + end + + Page::Project::Operations::Kubernetes::AddExisting.perform do |page| + page.set_cluster_name(@cluster.cluster_name) + page.set_api_url(@cluster.api_url) + page.set_ca_certificate(@cluster.ca_certificate) + page.set_token(@cluster.token) + page.add_cluster! + end + + if @install_helm_tiller + Page::Project::Operations::Kubernetes::Show.perform do |page| + # Helm must be installed before everything else + page.install!(:helm) + page.await_installed(:helm) + + page.install!(:ingress) if @install_ingress + page.await_installed(:ingress) if @install_ingress + page.install!(:prometheus) if @install_prometheus + page.await_installed(:prometheus) if @install_prometheus + page.install!(:runner) if @install_runner + page.await_installed(:runner) if @install_runner + end + end + end + end + end + end +end diff --git a/qa/qa/fixtures/auto_devops_rack/Gemfile b/qa/qa/fixtures/auto_devops_rack/Gemfile new file mode 100644 index 00000000000..fc7514242d0 --- /dev/null +++ b/qa/qa/fixtures/auto_devops_rack/Gemfile @@ -0,0 +1,3 @@ +source 'https://rubygems.org' +gem 'rack' +gem 'rake' diff --git a/qa/qa/fixtures/auto_devops_rack/Gemfile.lock b/qa/qa/fixtures/auto_devops_rack/Gemfile.lock new file mode 100644 index 00000000000..09cf72c48ac --- /dev/null +++ b/qa/qa/fixtures/auto_devops_rack/Gemfile.lock @@ -0,0 +1,15 @@ +GEM + remote: https://rubygems.org/ + specs: + rack (2.0.4) + rake (12.3.0) + +PLATFORMS + ruby + +DEPENDENCIES + rack + rake + +BUNDLED WITH + 1.16.1 diff --git a/qa/qa/fixtures/auto_devops_rack/Rakefile b/qa/qa/fixtures/auto_devops_rack/Rakefile new file mode 100644 index 00000000000..c865c9aaac1 --- /dev/null +++ b/qa/qa/fixtures/auto_devops_rack/Rakefile @@ -0,0 +1,7 @@ +require 'rake/testtask' + +task default: %w[test] + +task :test do + puts "ok" +end diff --git a/qa/qa/fixtures/auto_devops_rack/config.ru b/qa/qa/fixtures/auto_devops_rack/config.ru new file mode 100644 index 00000000000..bde8e15488a --- /dev/null +++ b/qa/qa/fixtures/auto_devops_rack/config.ru @@ -0,0 +1 @@ +run lambda { |env| [200, { 'Content-Type' => 'text/plain' }, StringIO.new("Hello World!\n")] } diff --git a/qa/qa/git/repository.rb b/qa/qa/git/repository.rb index 1367671e3ca..fc753554fc4 100644 --- a/qa/qa/git/repository.rb +++ b/qa/qa/git/repository.rb @@ -7,8 +7,6 @@ module QA class Repository include Scenario::Actable - attr_reader :push_error - def self.perform(*args) Dir.mktmpdir do |dir| Dir.chdir(dir) { super } @@ -35,7 +33,7 @@ module QA end def clone(opts = '') - `git clone #{opts} #{@uri.to_s} ./ #{suppress_output}` + run_and_redact_credentials("git clone #{opts} #{@uri} ./") end def checkout(branch_name) @@ -71,8 +69,9 @@ module QA end def push_changes(branch = 'master') - # capture3 returns stdout, stderr and status. - _, @push_error, _ = Open3.capture3("git push #{@uri} #{branch} #{suppress_output}") + output, _ = run_and_redact_credentials("git push #{@uri} #{branch}") + + output end def commits @@ -81,12 +80,10 @@ module QA private - def suppress_output - # If we're running as the default user, it's probably a temporary - # instance and output can be useful for debugging - return if @username == Runtime::User.default_name - - "&> #{File::NULL}" + # Since the remote URL contains the credentials, and git occasionally + # outputs the URL. Note that stderr is redirected to stdout. + def run_and_redact_credentials(command) + Open3.capture2("#{command} 2>&1 | sed -E 's#://[^@]+@#://****@#g'") end end end diff --git a/qa/qa/page/admin/settings/main.rb b/qa/qa/page/admin/settings/main.rb index e7c1220c967..db3387b4557 100644 --- a/qa/qa/page/admin/settings/main.rb +++ b/qa/qa/page/admin/settings/main.rb @@ -6,11 +6,11 @@ module QA include QA::Page::Settings::Common view 'app/views/admin/application_settings/show.html.haml' do - element :advanced_settings_section, 'Repository storage' + element :repository_storage_settings end def expand_repository_storage(&block) - expand_section('Repository storage') do + expand_section(:repository_storage_settings) do RepositoryStorage.perform(&block) end end diff --git a/qa/qa/page/base.rb b/qa/qa/page/base.rb index 0a69af88570..30e35bf7abb 100644 --- a/qa/qa/page/base.rb +++ b/qa/qa/page/base.rb @@ -13,7 +13,7 @@ module QA visit current_url end - def wait(max: 60, time: 1, reload: true) + def wait(max: 60, time: 0.1, reload: true) start = Time.now while Time.now - start < max diff --git a/qa/qa/page/group/show.rb b/qa/qa/page/group/show.rb index 89125bd2e59..3e0eaa392f5 100644 --- a/qa/qa/page/group/show.rb +++ b/qa/qa/page/group/show.rb @@ -28,11 +28,9 @@ module QA def has_subgroup?(name) filter_by_name(name) - wait(reload: false) do - break false if page.has_content?('Sorry, no groups or projects matched your search') + page.has_text?(/#{name}|Sorry, no groups or projects matched your search/, wait: 60) - page.has_link?(name) - end + page.has_text?(name, wait: 0) end def go_to_new_subgroup diff --git a/qa/qa/page/menu/side.rb b/qa/qa/page/menu/side.rb index 7e028add2ef..3630b7e8568 100644 --- a/qa/qa/page/menu/side.rb +++ b/qa/qa/page/menu/side.rb @@ -7,9 +7,11 @@ module QA element :settings_link, 'link_to edit_project_path' element :repository_link, "title: 'Repository'" element :pipelines_settings_link, "title: 'CI / CD'" + element :operations_kubernetes_link, "title: _('Kubernetes')" element :issues_link, /link_to.*shortcuts-issues/ element :issues_link_text, "Issues" element :top_level_items, '.sidebar-top-level-items' + element :operations_section, "class: 'shortcuts-operations'" element :activity_link, "title: 'Activity'" end @@ -33,6 +35,14 @@ module QA end end + def click_operations_kubernetes + hover_operations do + within_submenu do + click_link('Kubernetes') + end + end + end + def click_ci_cd_pipelines within_sidebar do click_link('CI / CD') @@ -61,6 +71,14 @@ module QA end end + def hover_operations + within_sidebar do + find('.shortcuts-operations').hover + + yield + end + end + def within_sidebar page.within('.sidebar-top-level-items') do yield diff --git a/qa/qa/page/project/job/show.rb b/qa/qa/page/project/job/show.rb index 83bb224b5c3..228ffd9d381 100644 --- a/qa/qa/page/project/job/show.rb +++ b/qa/qa/page/project/job/show.rb @@ -4,8 +4,9 @@ module QA::Page COMPLETED_STATUSES = %w[passed failed canceled blocked skipped manual].freeze # excludes created, pending, running PASSED_STATUS = 'passed'.freeze - view 'app/views/projects/jobs/show.html.haml' do + view 'app/views/shared/builds/_build_output.html.haml' do element :build_output, '.js-build-output' + element :loading_animation, '.js-build-refresh' end view 'app/assets/javascripts/vue_shared/components/ci_badge_link.vue' do @@ -20,6 +21,10 @@ module QA::Page find('.ci-status').text == PASSED_STATUS end + def trace_loading? + has_css?('.js-build-refresh') + end + # Reminder: You may wish to wait for a particular job status before checking output def output find('.js-build-output').text diff --git a/qa/qa/page/project/operations/kubernetes/add.rb b/qa/qa/page/project/operations/kubernetes/add.rb new file mode 100644 index 00000000000..9b3c482fa6c --- /dev/null +++ b/qa/qa/page/project/operations/kubernetes/add.rb @@ -0,0 +1,19 @@ +module QA + module Page + module Project + module Operations + module Kubernetes + class Add < Page::Base + view 'app/views/projects/clusters/new.html.haml' do + element :add_kubernetes_cluster_button, "link_to s_('ClusterIntegration|Add an existing Kubernetes cluster')" + end + + def add_existing_cluster + click_on 'Add an existing Kubernetes cluster' + end + end + end + end + end + end +end diff --git a/qa/qa/page/project/operations/kubernetes/add_existing.rb b/qa/qa/page/project/operations/kubernetes/add_existing.rb new file mode 100644 index 00000000000..eef82b5f329 --- /dev/null +++ b/qa/qa/page/project/operations/kubernetes/add_existing.rb @@ -0,0 +1,39 @@ +module QA + module Page + module Project + module Operations + module Kubernetes + class AddExisting < Page::Base + view 'app/views/projects/clusters/user/_form.html.haml' do + element :cluster_name, 'text_field :name' + element :api_url, 'text_field :api_url' + element :ca_certificate, 'text_area :ca_cert' + element :token, 'text_field :token' + element :add_cluster_button, "submit s_('ClusterIntegration|Add Kubernetes cluster')" + end + + def set_cluster_name(name) + fill_in 'cluster_name', with: name + end + + def set_api_url(api_url) + fill_in 'cluster_platform_kubernetes_attributes_api_url', with: api_url + end + + def set_ca_certificate(ca_certificate) + fill_in 'cluster_platform_kubernetes_attributes_ca_cert', with: ca_certificate + end + + def set_token(token) + fill_in 'cluster_platform_kubernetes_attributes_token', with: token + end + + def add_cluster! + click_on 'Add Kubernetes cluster' + end + end + end + end + end + end +end diff --git a/qa/qa/page/project/operations/kubernetes/index.rb b/qa/qa/page/project/operations/kubernetes/index.rb new file mode 100644 index 00000000000..7261b5645da --- /dev/null +++ b/qa/qa/page/project/operations/kubernetes/index.rb @@ -0,0 +1,19 @@ +module QA + module Page + module Project + module Operations + module Kubernetes + class Index < Page::Base + view 'app/views/projects/clusters/_empty_state.html.haml' do + element :add_kubernetes_cluster_button, "link_to s_('ClusterIntegration|Add Kubernetes cluster')" + end + + def add_kubernetes_cluster + click_on 'Add Kubernetes cluster' + end + end + end + end + end + end +end diff --git a/qa/qa/page/project/operations/kubernetes/show.rb b/qa/qa/page/project/operations/kubernetes/show.rb new file mode 100644 index 00000000000..4923304133e --- /dev/null +++ b/qa/qa/page/project/operations/kubernetes/show.rb @@ -0,0 +1,39 @@ +module QA + module Page + module Project + module Operations + module Kubernetes + class Show < Page::Base + view 'app/assets/javascripts/clusters/components/application_row.vue' do + element :application_row, 'js-cluster-application-row-${this.id}' + element :install_button, "s__('ClusterIntegration|Install')" + element :installed_button, "s__('ClusterIntegration|Installed')" + end + + view 'app/assets/javascripts/clusters/components/applications.vue' do + element :ingress_ip_address, 'id="ingress-ip-address"' + end + + def install!(application_name) + within(".js-cluster-application-row-#{application_name}") do + click_on 'Install' + end + end + + def await_installed(application_name) + within(".js-cluster-application-row-#{application_name}") do + page.has_text?('Installed', wait: 300) + end + end + + def ingress_ip + # We need to wait longer since it can take some time before the + # ip address is assigned for the ingress controller + page.find('#ingress-ip-address', wait: 500).value + end + end + end + end + end + end +end diff --git a/qa/qa/page/project/pipeline/show.rb b/qa/qa/page/project/pipeline/show.rb index ec61c47b3bb..babc0079f3f 100644 --- a/qa/qa/page/project/pipeline/show.rb +++ b/qa/qa/page/project/pipeline/show.rb @@ -24,10 +24,10 @@ module QA::Page end end - def has_build?(name, status: :success) + def has_build?(name, status: :success, wait: nil) within('.pipeline-graph') do within('.ci-job-component', text: name) do - has_selector?(".ci-status-icon-#{status}") + has_selector?(".ci-status-icon-#{status}", { wait: wait }.compact) end end end diff --git a/qa/qa/page/project/settings/advanced.rb b/qa/qa/page/project/settings/advanced.rb index 5ef00504fdf..d7b2b66b587 100644 --- a/qa/qa/page/project/settings/advanced.rb +++ b/qa/qa/page/project/settings/advanced.rb @@ -4,9 +4,9 @@ module QA module Settings class Advanced < Page::Base view 'app/views/projects/edit.html.haml' do - element :project_path_field, 'f.text_field :path' - element :project_name_field, 'f.text_field :name' - element :rename_project_button, "f.submit 'Rename project'" + element :project_path_field, 'text_field :path' + element :project_name_field, 'text_field :name' + element :rename_project_button, "submit 'Rename project'" end def rename_to(path) diff --git a/qa/qa/page/project/settings/ci_cd.rb b/qa/qa/page/project/settings/ci_cd.rb index 145c3d3ddfa..1466bc2e0bf 100644 --- a/qa/qa/page/project/settings/ci_cd.rb +++ b/qa/qa/page/project/settings/ci_cd.rb @@ -6,21 +6,38 @@ module QA # rubocop:disable Naming/FileName include Common view 'app/views/projects/settings/ci_cd/show.html.haml' do - element :runners_settings, 'Runners settings' - element :secret_variables, 'Variables' + element :autodevops_settings + element :runners_settings + element :variables_settings + end + + view 'app/views/projects/settings/ci_cd/_autodevops_form.html.haml' do + element :enable_auto_devops_field, 'radio_button :enabled' + element :domain_field, 'text_field :domain' + element :enable_auto_devops_button, "%strong= s_('CICD|Enable Auto DevOps')" + element :domain_input, "%strong= _('Domain')" + element :save_changes_button, "submit 'Save changes'" end def expand_runners_settings(&block) - expand_section('Runners settings') do + expand_section(:runners_settings) do Settings::Runners.perform(&block) end end def expand_secret_variables(&block) - expand_section('Variables') do + expand_section(:variables_settings) do Settings::SecretVariables.perform(&block) end end + + def enable_auto_devops_with_domain(domain) + expand_section(:autodevops_settings) do + choose 'Enable Auto DevOps' + fill_in 'Domain', with: domain + click_on 'Save changes' + end + end end end end diff --git a/qa/qa/page/project/settings/main.rb b/qa/qa/page/project/settings/main.rb index 5d743f4c9c8..d8cf1d49dd2 100644 --- a/qa/qa/page/project/settings/main.rb +++ b/qa/qa/page/project/settings/main.rb @@ -6,11 +6,11 @@ module QA include Common view 'app/views/projects/edit.html.haml' do - element :advanced_settings_section, 'Advanced settings' + element :advanced_settings end def expand_advanced_settings(&block) - expand_section('Advanced settings') do + expand_section(:advanced_settings) do Advanced.perform(&block) end end diff --git a/qa/qa/page/project/settings/merge_request.rb b/qa/qa/page/project/settings/merge_request.rb index a88cd661016..d044d3715a9 100644 --- a/qa/qa/page/project/settings/merge_request.rb +++ b/qa/qa/page/project/settings/merge_request.rb @@ -5,17 +5,17 @@ module QA class MergeRequest < QA::Page::Base include Common - view 'app/views/projects/_merge_request_merge_method_settings.html.haml' do - element :radio_button_merge_ff - end - view 'app/views/projects/edit.html.haml' do - element :merge_request_settings, 'Merge request settings' + element :merge_request_settings element :save_merge_request_changes end + view 'app/views/projects/_merge_request_merge_method_settings.html.haml' do + element :radio_button_merge_ff + end + def enable_ff_only - expand_section('Merge request settings') do + expand_section(:merge_request_settings) do click_element :radio_button_merge_ff click_element :save_merge_request_changes end diff --git a/qa/qa/page/project/settings/protected_branches.rb b/qa/qa/page/project/settings/protected_branches.rb index 63bc3aaa2bc..0bd031e96b5 100644 --- a/qa/qa/page/project/settings/protected_branches.rb +++ b/qa/qa/page/project/settings/protected_branches.rb @@ -40,16 +40,16 @@ module QA click_allow(:push, 'No one') end - def allow_devs_and_masters_to_push - click_allow(:push, 'Developers + Masters') + def allow_devs_and_maintainers_to_push + click_allow(:push, 'Developers + Maintainers') end def allow_no_one_to_merge click_allow(:merge, 'No one') end - def allow_devs_and_masters_to_merge - click_allow(:merge, 'Developers + Masters') + def allow_devs_and_maintainers_to_merge + click_allow(:merge, 'Developers + Maintainers') end def protect_branch @@ -75,10 +75,6 @@ module QA within_element(:"allowed_to_#{action}_dropdown") do click_on text - - wait(reload: false) do - has_css?('.is-active') - end end end end diff --git a/qa/qa/page/project/settings/repository.rb b/qa/qa/page/project/settings/repository.rb index 30900e74e90..1ed5f455a85 100644 --- a/qa/qa/page/project/settings/repository.rb +++ b/qa/qa/page/project/settings/repository.rb @@ -6,17 +6,21 @@ module QA include Common view 'app/views/projects/deploy_keys/_index.html.haml' do - element :deploy_keys_section, 'Deploy Keys' + element :deploy_keys_settings + end + + view 'app/views/projects/protected_branches/shared/_index.html.haml' do + element :protected_branches_settings end def expand_deploy_keys(&block) - expand_section('Deploy Keys') do + expand_section(:deploy_keys_settings) do DeployKeys.perform(&block) end end def expand_protected_branches(&block) - expand_section('Protected Branches') do + expand_section(:protected_branches_settings) do ProtectedBranches.perform(&block) end end diff --git a/qa/qa/page/settings/common.rb b/qa/qa/page/settings/common.rb index a683a6829d5..f9f71aa4a72 100644 --- a/qa/qa/page/settings/common.rb +++ b/qa/qa/page/settings/common.rb @@ -4,19 +4,17 @@ module QA module Common # Click the Expand button present in the specified section # - # @param [String] name present in the container in the DOM - def expand_section(name) - page.within('#content-body') do - page.within('section', text: name) do - # Because it is possible to click the button before the JS toggle code is bound - wait(reload: false) do - click_button 'Expand' unless first('button', text: 'Collapse') + # @param [Symbol] and `element` name defined in a `view` block + def expand_section(element_name) + within_element(element_name) do + # Because it is possible to click the button before the JS toggle code is bound + wait(reload: false) do + click_button 'Expand' unless first('button', text: 'Collapse') - page.has_content?('Collapse') - end - - yield if block_given? + page.has_content?('Collapse') end + + yield if block_given? end end end diff --git a/qa/qa/runtime/api.rb b/qa/qa/runtime/api.rb deleted file mode 100644 index e2a096b971d..00000000000 --- a/qa/qa/runtime/api.rb +++ /dev/null @@ -1,82 +0,0 @@ -require 'airborne' - -module QA - module Runtime - module API - class Client - attr_reader :address - - def initialize(address = :gitlab) - @address = address - end - - def personal_access_token - @personal_access_token ||= get_personal_access_token - end - - def get_personal_access_token - # you can set the environment variable PERSONAL_ACCESS_TOKEN - # to use a specific access token rather than create one from the UI - if Runtime::Env.personal_access_token - Runtime::Env.personal_access_token - else - create_personal_access_token - end - end - - private - - def create_personal_access_token - Runtime::Browser.visit(@address, Page::Main::Login) do - Page::Main::Login.act { sign_in_using_credentials } - Factory::Resource::PersonalAccessToken.fabricate!.access_token - end - end - end - - class Request - API_VERSION = 'v4'.freeze - - def initialize(api_client, path, personal_access_token: nil) - personal_access_token ||= api_client.personal_access_token - request_path = request_path(path, personal_access_token: personal_access_token) - @session_address = Runtime::Address.new(api_client.address, request_path) - end - - def url - @session_address.address - end - - # Prepend a request path with the path to the API - # - # path - Path to append - # - # Examples - # - # >> request_path('/issues') - # => "/api/v4/issues" - # - # >> request_path('/issues', personal_access_token: 'sometoken) - # => "/api/v4/issues?private_token=..." - # - # Returns the relative path to the requested API resource - def request_path(path, version: API_VERSION, personal_access_token: nil, oauth_access_token: nil) - full_path = File.join('/api', version, path) - - if oauth_access_token - query_string = "access_token=#{oauth_access_token}" - elsif personal_access_token - query_string = "private_token=#{personal_access_token}" - end - - if query_string - full_path << (path.include?('?') ? '&' : '?') - full_path << query_string - end - - full_path - end - end - end - end -end diff --git a/qa/qa/runtime/api/client.rb b/qa/qa/runtime/api/client.rb new file mode 100644 index 00000000000..02015e23ad8 --- /dev/null +++ b/qa/qa/runtime/api/client.rb @@ -0,0 +1,39 @@ +require 'airborne' + +module QA + module Runtime + module API + class Client + attr_reader :address + + def initialize(address = :gitlab, personal_access_token: nil) + @address = address + @personal_access_token = personal_access_token + end + + def personal_access_token + @personal_access_token ||= get_personal_access_token + end + + def get_personal_access_token + # you can set the environment variable PERSONAL_ACCESS_TOKEN + # to use a specific access token rather than create one from the UI + if Runtime::Env.personal_access_token + Runtime::Env.personal_access_token + else + create_personal_access_token + end + end + + private + + def create_personal_access_token + Runtime::Browser.visit(@address, Page::Main::Login) do + Page::Main::Login.act { sign_in_using_credentials } + Factory::Resource::PersonalAccessToken.fabricate!.access_token + end + end + end + end + end +end diff --git a/qa/qa/runtime/api/request.rb b/qa/qa/runtime/api/request.rb new file mode 100644 index 00000000000..c33ada0de7a --- /dev/null +++ b/qa/qa/runtime/api/request.rb @@ -0,0 +1,43 @@ +module QA + module Runtime + module API + class Request + API_VERSION = 'v4'.freeze + + def initialize(api_client, path, **query_string) + query_string[:private_token] ||= api_client.personal_access_token unless query_string[:oauth_access_token] + request_path = request_path(path, **query_string) + @session_address = Runtime::Address.new(api_client.address, request_path) + end + + def url + @session_address.address + end + + # Prepend a request path with the path to the API + # + # path - Path to append + # + # Examples + # + # >> request_path('/issues') + # => "/api/v4/issues" + # + # >> request_path('/issues', private_token: 'sometoken) + # => "/api/v4/issues?private_token=..." + # + # Returns the relative path to the requested API resource + def request_path(path, version: API_VERSION, **query_string) + full_path = File.join('/api', version, path) + + if query_string.any? + full_path << (path.include?('?') ? '&' : '?') + full_path << query_string.map { |k, v| "#{k}=#{CGI.escape(v)}" }.join('&') + end + + full_path + end + end + end + end +end diff --git a/qa/qa/runtime/browser.rb b/qa/qa/runtime/browser.rb index a12d95683af..ecd273c6db8 100644 --- a/qa/qa/runtime/browser.rb +++ b/qa/qa/runtime/browser.rb @@ -102,19 +102,7 @@ module QA def perform(&block) visit(url) - yield if block_given? - rescue - raise if block.nil? - - # RSpec examples will take care of screenshots on their own - # - unless block.binding.receiver.is_a?(RSpec::Core::ExampleGroup) - screenshot_and_save_page - end - - raise - ensure - clear! if block_given? + yield.tap { clear! } if block_given? end ## diff --git a/qa/qa/runtime/env.rb b/qa/qa/runtime/env.rb index fe432edfa2a..81d00d45753 100644 --- a/qa/qa/runtime/env.rb +++ b/qa/qa/runtime/env.rb @@ -46,6 +46,18 @@ module QA def sandbox_name ENV['GITLAB_SANDBOX_NAME'] end + + def gcloud_account_key + ENV.fetch("GCLOUD_ACCOUNT_KEY") + end + + def gcloud_account_email + ENV.fetch("GCLOUD_ACCOUNT_EMAIL") + end + + def gcloud_zone + ENV.fetch('GCLOUD_ZONE') + end end end end diff --git a/qa/qa/scenario/test/integration/kubernetes.rb b/qa/qa/scenario/test/integration/kubernetes.rb new file mode 100644 index 00000000000..7479073e979 --- /dev/null +++ b/qa/qa/scenario/test/integration/kubernetes.rb @@ -0,0 +1,11 @@ +module QA + module Scenario + module Test + module Integration + class Kubernetes < Test::Instance + tags :kubernetes + end + end + end + end +end diff --git a/qa/qa/service/kubernetes_cluster.rb b/qa/qa/service/kubernetes_cluster.rb new file mode 100644 index 00000000000..7627c8c7ad9 --- /dev/null +++ b/qa/qa/service/kubernetes_cluster.rb @@ -0,0 +1,73 @@ +require 'securerandom' +require 'mkmf' + +module QA + module Service + class KubernetesCluster + include Service::Shellout + + attr_reader :api_url, :ca_certificate, :token + + def cluster_name + @cluster_name ||= "qa-cluster-#{SecureRandom.hex(4)}-#{Time.now.utc.strftime("%Y%m%d%H%M%S")}" + end + + def create! + validate_dependencies + login_if_not_already_logged_in + + shell <<~CMD.tr("\n", ' ') + gcloud container clusters + create #{cluster_name} + --enable-legacy-authorization + --zone #{Runtime::Env.gcloud_zone} + && gcloud container clusters + get-credentials + --zone #{Runtime::Env.gcloud_zone} + #{cluster_name} + CMD + + @api_url = `kubectl config view --minify -o jsonpath='{.clusters[].cluster.server}'` + @ca_certificate = Base64.decode64(`kubectl get secrets -o jsonpath="{.items[0].data['ca\\.crt']}"`) + @token = Base64.decode64(`kubectl get secrets -o jsonpath='{.items[0].data.token}'`) + self + end + + def remove! + shell <<~CMD.tr("\n", ' ') + gcloud container clusters delete + --zone #{Runtime::Env.gcloud_zone} + #{cluster_name} + --quiet --async + CMD + end + + private + + def validate_dependencies + find_executable('gcloud') || raise("You must first install `gcloud` executable to run these tests.") + find_executable('kubectl') || raise("You must first install `kubectl` executable to run these tests.") + end + + def login_if_not_already_logged_in + account = `gcloud auth list --filter=status:ACTIVE --format="value(account)"` + if account.empty? + attempt_login_with_env_vars + else + puts "gcloud account found. Using: #{account} for creating K8s cluster." + end + end + + def attempt_login_with_env_vars + puts "No gcloud account. Attempting to login from env vars GCLOUD_ACCOUNT_EMAIL and GCLOUD_ACCOUNT_KEY." + gcloud_account_key = Tempfile.new('gcloud-account-key') + gcloud_account_key.write(Runtime::Env.gcloud_account_key) + gcloud_account_key.close + gcloud_account_email = Runtime::Env.gcloud_account_email + shell("gcloud auth activate-service-account #{gcloud_account_email} --key-file #{gcloud_account_key.path}") + ensure + gcloud_account_key && gcloud_account_key.unlink + end + end + end +end diff --git a/qa/qa/service/runner.rb b/qa/qa/service/runner.rb index c0352e0467a..9417c707105 100644 --- a/qa/qa/service/runner.rb +++ b/qa/qa/service/runner.rb @@ -3,7 +3,6 @@ require 'securerandom' module QA module Service class Runner - include Scenario::Actable include Service::Shellout attr_accessor :token, :address, :tags, :image diff --git a/qa/qa/specs/features/api/basics_spec.rb b/qa/qa/specs/features/api/basics_spec.rb new file mode 100644 index 00000000000..1d7f9d6a03c --- /dev/null +++ b/qa/qa/specs/features/api/basics_spec.rb @@ -0,0 +1,61 @@ +require 'securerandom' + +module QA + feature 'API basics', :core do + before(:context) do + @api_client = Runtime::API::Client.new(:gitlab) + end + + let(:project_name) { "api-basics-#{SecureRandom.hex(8)}" } + let(:sanitized_project_path) { CGI.escape("#{Runtime::User.name}/#{project_name}") } + + scenario 'user creates a project with a file and deletes them afterwards' do + create_project_request = Runtime::API::Request.new(@api_client, '/projects') + post create_project_request.url, path: project_name, name: project_name + + expect_status(201) + expect(json_body).to match( + a_hash_including(name: project_name, path: project_name) + ) + + create_file_request = Runtime::API::Request.new(@api_client, "/projects/#{sanitized_project_path}/repository/files/README.md") + post create_file_request.url, branch: 'master', content: 'Hello world', commit_message: 'Add README.md' + + expect_status(201) + expect(json_body).to match( + a_hash_including(branch: 'master', file_path: 'README.md') + ) + + get_file_request = Runtime::API::Request.new(@api_client, "/projects/#{sanitized_project_path}/repository/files/README.md", ref: 'master') + get get_file_request.url + + expect_status(200) + expect(json_body).to match( + a_hash_including( + ref: 'master', + file_path: 'README.md', file_name: 'README.md', + encoding: 'base64', content: 'SGVsbG8gd29ybGQ=' + ) + ) + + delete_file_request = Runtime::API::Request.new(@api_client, "/projects/#{sanitized_project_path}/repository/files/README.md", branch: 'master', commit_message: 'Remove README.md') + delete delete_file_request.url + + expect_status(204) + + get_tree_request = Runtime::API::Request.new(@api_client, "/projects/#{sanitized_project_path}/repository/tree") + get get_tree_request.url + + expect_status(200) + expect(json_body).to eq([]) + + delete_project_request = Runtime::API::Request.new(@api_client, "/projects/#{sanitized_project_path}") + delete delete_project_request.url + + expect_status(202) + expect(json_body).to match( + a_hash_including(message: '202 Accepted') + ) + end + end +end diff --git a/qa/qa/specs/features/api/users_spec.rb b/qa/qa/specs/features/api/users_spec.rb index 38f4c497183..0aecf89e1b7 100644 --- a/qa/qa/specs/features/api/users_spec.rb +++ b/qa/qa/specs/features/api/users_spec.rb @@ -31,7 +31,7 @@ module QA end scenario 'submit request with an invalid token' do - request = Runtime::API::Request.new(@api_client, '/users', personal_access_token: 'invalid') + request = Runtime::API::Request.new(@api_client, '/users', private_token: 'invalid') get request.url diff --git a/qa/qa/specs/features/merge_request/create_spec.rb b/qa/qa/specs/features/merge_request/create_spec.rb index 0931e649e24..befbc0b281a 100644 --- a/qa/qa/specs/features/merge_request/create_spec.rb +++ b/qa/qa/specs/features/merge_request/create_spec.rb @@ -11,7 +11,7 @@ module QA expect(page).to have_content('This is a merge request') expect(page).to have_content('Great feature') - expect(page).to have_content(/Opened [\w\s]+ a minute ago/) + expect(page).to have_content(/Opened [\w\s]+ ago/) end end end diff --git a/qa/qa/specs/features/project/auto_devops_spec.rb b/qa/qa/specs/features/project/auto_devops_spec.rb new file mode 100644 index 00000000000..202a847d1a5 --- /dev/null +++ b/qa/qa/specs/features/project/auto_devops_spec.rb @@ -0,0 +1,57 @@ +require 'pathname' + +module QA + feature 'Auto Devops', :kubernetes do + after do + @cluster&.remove! + end + + scenario 'user creates a new project and runs auto devops' do + Runtime::Browser.visit(:gitlab, Page::Main::Login) + Page::Main::Login.act { sign_in_using_credentials } + + project = Factory::Resource::Project.fabricate! do |p| + p.name = 'project-with-autodevops' + p.description = 'Project with Auto Devops' + end + + # Create Auto Devops compatible repo + Factory::Repository::Push.fabricate! do |push| + push.project = project + push.directory = Pathname + .new(__dir__) + .join('../../../fixtures/auto_devops_rack') + push.commit_message = 'Create Auto DevOps compatible rack application' + end + + Page::Project::Show.act { wait_for_push } + + # Create and connect K8s cluster + @cluster = Service::KubernetesCluster.new.create! + kubernetes_cluster = Factory::Resource::KubernetesCluster.fabricate! do |cluster| + cluster.project = project + cluster.cluster = @cluster + cluster.install_helm_tiller = true + cluster.install_ingress = true + cluster.install_prometheus = true + cluster.install_runner = true + end + + project.visit! + Page::Menu::Side.act { click_ci_cd_settings } + Page::Project::Settings::CICD.perform do |p| + p.enable_auto_devops_with_domain("#{kubernetes_cluster.ingress_ip}.nip.io") + end + + project.visit! + Page::Menu::Side.act { click_ci_cd_pipelines } + Page::Project::Pipeline::Index.act { go_to_latest_pipeline } + + Page::Project::Pipeline::Show.perform do |pipeline| + expect(pipeline).to have_build('build', status: :success, wait: 600) + expect(pipeline).to have_build('test', status: :success, wait: 600) + expect(pipeline).to have_build('production', status: :success, wait: 600) + end + end + end +end diff --git a/qa/qa/specs/features/project/deploy_key_clone_spec.rb b/qa/qa/specs/features/project/deploy_key_clone_spec.rb index 442ac312b4d..46b3e38c1c5 100644 --- a/qa/qa/specs/features/project/deploy_key_clone_spec.rb +++ b/qa/qa/specs/features/project/deploy_key_clone_spec.rb @@ -92,7 +92,9 @@ module QA Page::Project::Pipeline::Show.act { go_to_first_job } Page::Project::Job::Show.perform do |job| - job.wait(reload: false) { job.completed? } + job.wait(reload: false) do + job.completed? && !job.trace_loading? + end expect(job.passed?).to be_truthy, "Job status did not become \"passed\"." expect(job.output).to include(sha1sum) diff --git a/qa/qa/specs/features/repository/protected_branches_spec.rb b/qa/qa/specs/features/repository/protected_branches_spec.rb index 406b2772b64..491675875b9 100644 --- a/qa/qa/specs/features/repository/protected_branches_spec.rb +++ b/qa/qa/specs/features/repository/protected_branches_spec.rb @@ -7,12 +7,6 @@ module QA resource.name = 'protected-branch-project' end end - given(:location) do - Page::Project::Show.act do - choose_repository_clone_http - repository_location - end - end before do Runtime::Browser.visit(:gitlab, Page::Main::Login) @@ -26,44 +20,49 @@ module QA Capybara.execute_script 'localStorage.clear()' end - scenario 'user is able to protect a branch' do - protected_branch = Factory::Resource::Branch.fabricate! do |resource| - resource.branch_name = branch_name - resource.project = project - resource.allow_to_push = true - resource.protected = true + context 'when developers and maintainers are allowed to push to a protected branch' do + let!(:protected_branch) { create_protected_branch(allow_to_push: true) } + + scenario 'user with push rights successfully pushes to the protected branch' do + expect(protected_branch.name).to have_content(branch_name) + expect(protected_branch.push_allowance).to have_content('Developers + Maintainers') + + push = push_new_file(branch_name) + + expect(push.output).to match(/remote: To create a merge request for protected-branch, visit/) end + end + + context 'when developers and maintainers are not allowed to push to a protected branch' do + scenario 'user without push rights fails to push to the protected branch' do + create_protected_branch(allow_to_push: false) + + push = push_new_file(branch_name) - expect(protected_branch.name).to have_content(branch_name) - expect(protected_branch.push_allowance).to have_content('Developers + Masters') + expect(push.output) + .to match(/remote\: GitLab\: You are not allowed to push code to protected branches on this project/) + expect(push.output) + .to match(/\[remote rejected\] #{branch_name} -> #{branch_name} \(pre-receive hook declined\)/) + end end - scenario 'users without authorization cannot push to protected branch' do + def create_protected_branch(allow_to_push:) Factory::Resource::Branch.fabricate! do |resource| resource.branch_name = branch_name resource.project = project - resource.allow_to_push = false + resource.allow_to_push = allow_to_push resource.protected = true end + end - project.visit! - - Git::Repository.perform do |repository| - repository.uri = location.uri - repository.use_default_credentials - - repository.act do - clone - configure_identity('GitLab QA', 'root@gitlab.com') - checkout('protected-branch') - commit_file('README.md', 'readme content', 'Add a readme') - push_changes('protected-branch') - end - - expect(repository.push_error) - .to match(/remote\: GitLab\: You are not allowed to push code to protected branches on this project/) - expect(repository.push_error) - .to match(/\[remote rejected\] #{branch_name} -> #{branch_name} \(pre-receive hook declined\)/) + def push_new_file(branch) + Factory::Repository::Push.fabricate! do |resource| + resource.project = project + resource.file_name = 'new_file.md' + resource.file_content = '# This is a new file' + resource.commit_message = 'Add new_file.md' + resource.branch_name = branch_name + resource.new_branch = false end end end diff --git a/qa/spec/git/repository_spec.rb b/qa/spec/git/repository_spec.rb new file mode 100644 index 00000000000..ee1f08da238 --- /dev/null +++ b/qa/spec/git/repository_spec.rb @@ -0,0 +1,40 @@ +describe QA::Git::Repository do + let(:repository) { described_class.new } + + before do + cd_empty_temp_directory + set_bad_uri + repository.use_default_credentials + end + + describe '#clone' do + it 'redacts credentials from the URI in output' do + output, _ = repository.clone + + expect(output).to include("fatal: unable to access 'http://****@foo/bar.git/'") + end + end + + describe '#push_changes' do + before do + `git init` # need a repo to push from + end + + it 'redacts credentials from the URI in output' do + output, _ = repository.push_changes + + expect(output).to include("error: failed to push some refs to 'http://****@foo/bar.git'") + end + end + + def cd_empty_temp_directory + tmp_dir = 'tmp/git-repository-spec/' + FileUtils.rm_r(tmp_dir) if File.exist?(tmp_dir) + FileUtils.mkdir_p tmp_dir + FileUtils.cd tmp_dir + end + + def set_bad_uri + repository.uri = 'http://foo/bar.git' + end +end diff --git a/qa/spec/runtime/api_client_spec.rb b/qa/spec/runtime/api/client_spec.rb index d497d8839b8..d497d8839b8 100644 --- a/qa/spec/runtime/api_client_spec.rb +++ b/qa/spec/runtime/api/client_spec.rb diff --git a/qa/spec/runtime/api/request_spec.rb b/qa/spec/runtime/api/request_spec.rb new file mode 100644 index 00000000000..80e3149f32d --- /dev/null +++ b/qa/spec/runtime/api/request_spec.rb @@ -0,0 +1,44 @@ +describe QA::Runtime::API::Request do + include Support::StubENV + + before do + stub_env('PERSONAL_ACCESS_TOKEN', 'a_token') + end + + let(:client) { QA::Runtime::API::Client.new('http://example.com') } + let(:request) { described_class.new(client, '/users') } + + describe '#url' do + it 'returns the full api request url' do + expect(request.url).to eq 'http://example.com/api/v4/users?private_token=a_token' + end + end + + describe '#request_path' do + it 'prepends the api path' do + expect(request.request_path('/users')).to eq '/api/v4/users' + end + + it 'adds the personal access token' do + expect(request.request_path('/users', private_token: 'token')) + .to eq '/api/v4/users?private_token=token' + end + + it 'adds the oauth access token' do + expect(request.request_path('/users', access_token: 'otoken')) + .to eq '/api/v4/users?access_token=otoken' + end + + it 'respects query parameters' do + expect(request.request_path('/users?page=1')).to eq '/api/v4/users?page=1' + expect(request.request_path('/users', private_token: 'token', foo: 'bar/baz')) + .to eq '/api/v4/users?private_token=token&foo=bar%2Fbaz' + expect(request.request_path('/users?page=1', private_token: 'token', foo: 'bar/baz')) + .to eq '/api/v4/users?page=1&private_token=token&foo=bar%2Fbaz' + end + + it 'uses a different api version' do + expect(request.request_path('/users', version: 'other_version')).to eq '/api/other_version/users' + end + end +end diff --git a/qa/spec/runtime/api_request_spec.rb b/qa/spec/runtime/api_request_spec.rb index 8cf4b040c24..e69de29bb2d 100644 --- a/qa/spec/runtime/api_request_spec.rb +++ b/qa/spec/runtime/api_request_spec.rb @@ -1,42 +0,0 @@ -describe QA::Runtime::API::Request do - include Support::StubENV - - before do - stub_env('PERSONAL_ACCESS_TOKEN', 'a_token') - end - - let(:client) { QA::Runtime::API::Client.new('http://example.com') } - let(:request) { described_class.new(client, '/users') } - - describe '#url' do - it 'returns the full api request url' do - expect(request.url).to eq 'http://example.com/api/v4/users?private_token=a_token' - end - end - - describe '#request_path' do - it 'prepends the api path' do - expect(request.request_path('/users')).to eq '/api/v4/users' - end - - it 'adds the personal access token' do - expect(request.request_path('/users', personal_access_token: 'token')) - .to eq '/api/v4/users?private_token=token' - end - - it 'adds the oauth access token' do - expect(request.request_path('/users', oauth_access_token: 'otoken')) - .to eq '/api/v4/users?access_token=otoken' - end - - it 'respects query parameters' do - expect(request.request_path('/users?page=1')).to eq '/api/v4/users?page=1' - expect(request.request_path('/users?page=1', personal_access_token: 'token')) - .to eq '/api/v4/users?page=1&private_token=token' - end - - it 'uses a different api version' do - expect(request.request_path('/users', version: 'other_version')).to eq '/api/other_version/users' - end - end -end diff --git a/qa/spec/support/stub_env.rb b/qa/spec/support/stub_env.rb index bc8f3a5e22e..044804cd599 100644 --- a/qa/spec/support/stub_env.rb +++ b/qa/spec/support/stub_env.rb @@ -32,6 +32,8 @@ module Support allow(ENV).to receive(:[]).and_call_original allow(ENV).to receive(:key?).and_call_original allow(ENV).to receive(:fetch).and_call_original + # Prevent secrets from leaking in CI + allow(ENV).to receive(:inspect).and_return([]) add_stubbed_value(STUBBED_KEY, true) end end diff --git a/scripts/frontend/prettier.js b/scripts/frontend/prettier.js index 39de77bc333..6e4e36b9b2d 100644 --- a/scripts/frontend/prettier.js +++ b/scripts/frontend/prettier.js @@ -9,6 +9,8 @@ const getStagedFiles = require('./frontend_script_utils').getStagedFiles; const mode = process.argv[2] || 'check'; const shouldSave = mode === 'save' || mode === 'save-all'; const allFiles = mode === 'check-all' || mode === 'save-all'; +let dirPath = process.argv[3] || ''; +if (dirPath && dirPath.charAt(dirPath.length - 1) !== '/') dirPath += '/'; const config = { patterns: ['**/*.js', '**/*.vue', '**/*.scss'], @@ -39,9 +41,10 @@ prettierIgnore.add( const availableExtensions = Object.keys(config.parsers); -console.log(`Loading ${allFiles ? 'All' : 'Staged'} Files ...`); +console.log(`Loading ${allFiles ? 'All' : 'Selected'} Files ...`); -const stagedFiles = allFiles ? null : getStagedFiles(availableExtensions.map(ext => `*.${ext}`)); +const stagedFiles = + allFiles || dirPath ? null : getStagedFiles(availableExtensions.map(ext => `*.${ext}`)); if (stagedFiles) { if (!stagedFiles.length || (stagedFiles.length === 1 && !stagedFiles[0])) { @@ -60,6 +63,13 @@ if (allFiles) { const patterns = config.patterns; const globPattern = patterns.length > 1 ? `{${patterns.join(',')}}` : `${patterns.join(',')}`; files = glob.sync(globPattern, { ignore }).filter(f => allFiles || stagedFiles.includes(f)); +} else if (dirPath) { + const ignore = config.ignore; + const patterns = config.patterns.map(item => { + return dirPath + item; + }); + const globPattern = patterns.length > 1 ? `{${patterns.join(',')}}` : `${patterns.join(',')}`; + files = glob.sync(globPattern, { ignore }); } else { files = stagedFiles.filter(f => availableExtensions.includes(f.split('.').pop())); } @@ -73,12 +83,11 @@ if (!files.length) { console.log(`${shouldSave ? 'Updating' : 'Checking'} ${files.length} file(s)`); -prettier - .resolveConfig('.') - .then(options => { - console.log('Found options : ', options); - files.forEach(file => { - try { +files.forEach(file => { + try { + prettier + .resolveConfig(file) + .then(options => { const fileExtension = file.split('.').pop(); Object.assign(options, { parser: config.parsers[fileExtension], @@ -101,17 +110,17 @@ prettier } console.log(`Prettify Manually : ${file}`); } - } catch (error) { - didError = true; - console.log(`\n\nError with ${file}: ${error.message}`); - } - }); - - if (didWarn || didError) { - process.exit(1); - } - }) - .catch(e => { - console.log(`Error on loading the Config File: ${e.message}`); - process.exit(1); - }); + }) + .catch(e => { + console.log(`Error on loading the Config File: ${e.message}`); + process.exit(1); + }); + } catch (error) { + didError = true; + console.log(`\n\nError with ${file}: ${error.message}`); + } +}); + +if (didWarn || didError) { + process.exit(1); +} diff --git a/scripts/rails5-gemfile-lock-check b/scripts/rails5-gemfile-lock-check new file mode 100755 index 00000000000..da6f1b7145e --- /dev/null +++ b/scripts/rails5-gemfile-lock-check @@ -0,0 +1,19 @@ +#!/usr/bin/env bash + +echo -e "=> Checking if Gemfile.rails5.lock is up-to-date...\\n" + +cp Gemfile.rails5.lock Gemfile.rails5.lock.orig +BUNDLE_GEMFILE=Gemfile.rails5 bundle install "$BUNDLE_INSTALL_FLAGS" +diff -u Gemfile.rails5.lock.orig Gemfile.rails5.lock >/dev/null 2>&1 + +if [ $? == 1 ] +then + diff -u Gemfile.rails5.lock.orig Gemfile.rails5.lock + + echo -e "\\n✖ ERROR: Gemfile.rails5.lock is not up-to-date! + Please run 'BUNDLE_GEMFILE=Gemfile.rails5 bundle install'\\n" >&2 + exit 1 +fi + +echo "✔ Gemfile.rails5.lock is up-to-date" +exit 0 diff --git a/scripts/trigger-build b/scripts/trigger-build new file mode 100755 index 00000000000..526f5164ede --- /dev/null +++ b/scripts/trigger-build @@ -0,0 +1,181 @@ +#!/usr/bin/env ruby + +require 'net/http' +require 'json' +require 'cgi' + +module Trigger + OMNIBUS_PROJECT_PATH = 'gitlab-org/omnibus-gitlab'.freeze + CNG_PROJECT_PATH = 'gitlab-org/build/CNG'.freeze + TOKEN = ENV['BUILD_TRIGGER_TOKEN'] + + def self.ee? + ENV['CI_PROJECT_NAME'] == 'gitlab-ee' || File.exist?('CHANGELOG-EE.md') + end + + class Omnibus + def initialize + @uri = URI("https://gitlab.com/api/v4/projects/#{CGI.escape(Trigger::OMNIBUS_PROJECT_PATH)}/trigger/pipeline") + @params = env_params.merge(file_params).merge(token: Trigger::TOKEN) + end + + def invoke! + res = Net::HTTP.post_form(@uri, @params) + id = JSON.parse(res.body)['id'] + project = Trigger::OMNIBUS_PROJECT_PATH + + if id + puts "Triggered https://gitlab.com/#{project}/pipelines/#{id}" + puts "Waiting for downstream pipeline status" + else + raise "Trigger failed! The response from the trigger is: #{res.body}" + end + + Trigger::Pipeline.new(project, id) + end + + private + + def env_params + { + "ref" => ENV["OMNIBUS_BRANCH"] || "master", + "variables[GITLAB_VERSION]" => ENV["CI_COMMIT_SHA"], + "variables[ALTERNATIVE_SOURCES]" => true, + "variables[ee]" => Trigger.ee? ? 'true' : 'false', + "variables[TRIGGERED_USER]" => ENV["GITLAB_USER_NAME"], + "variables[TRIGGER_SOURCE]" => "https://gitlab.com/gitlab-org/#{ENV['CI_PROJECT_NAME']}/-/jobs/#{ENV['CI_JOB_ID']}" + } + end + + def file_params + Hash.new.tap do |params| + Dir.glob("*_VERSION").each do |version_file| + params["variables[#{version_file}]"] = File.read(version_file).strip + end + end + end + end + + class CNG + def initialize + @uri = URI("https://gitlab.com/api/v4/projects/#{CGI.escape(Trigger::CNG_PROJECT_PATH)}/trigger/pipeline") + @ref_name = ENV['CI_COMMIT_REF_NAME'] + @username = ENV['GITLAB_USER_NAME'] + @project_name = ENV['CI_PROJECT_NAME'] + @job_id = ENV['CI_JOB_ID'] + @params = env_params.merge(file_params).merge(token: Trigger::TOKEN) + end + + # + # Trigger a pipeline + # + def invoke! + res = Net::HTTP.post_form(@uri, @params) + id = JSON.parse(res.body)['id'] + project = Trigger::CNG_PROJECT_PATH + + if id + puts "Triggered https://gitlab.com/#{project}/pipelines/#{id}" + puts "Waiting for downstream pipeline status" + else + raise "Trigger failed! The response from the trigger is: #{res.body}" + end + + Trigger::Pipeline.new(project, id) + end + + private + + def env_params + params = { + "ref" => ENV["CNG_BRANCH"] || "master", + "variables[TRIGGERED_USER]" => @username, + "variables[TRIGGER_SOURCE]" => "https://gitlab.com/gitlab-org/#{@project_name}/-/jobs/#{@job_id}" + } + + if Trigger.ee? + params["variables[GITLAB_EE_VERSION]"] = @ref_name + params["variables[EE_PIPELINE]"] = 'true' + else + params["variables[GITLAB_CE_VERSION]"] = @ref_name + params["variables[CE_PIPELINE]"] = 'true' + end + + params + end + + # Read version files from all components + def file_params + Dir.glob("*_VERSION").each_with_object({}) do |version_file, params| + raw_version = File.read(version_file).strip + # if the version matches semver format, treat it as a tag and prepend `v` + version = if raw_version =~ Regexp.compile(/^\d+\.\d+\.\d+(-rc\d+)?(-ee)?$/) + "v#{raw_version}" + else + raw_version + end + + params["variables[#{version_file}]"] = version + end + end + end + + class Pipeline + INTERVAL = 60 # seconds + MAX_DURATION = 3600 * 3 # 3 hours + + def initialize(project, id) + @start = Time.now.to_i + @uri = URI("https://gitlab.com/api/v4/projects/#{CGI.escape(project)}/pipelines/#{id}") + end + + def wait! + loop do + raise "Pipeline timed out after waiting for #{duration} minutes!" if timeout? + + case status + when :created, :pending, :running + print "." + sleep INTERVAL + when :success + puts "Pipeline succeeded in #{duration} minutes!" + break + else + raise "Pipeline did not succeed!" + end + + STDOUT.flush + end + end + + def timeout? + Time.now.to_i > (@start + MAX_DURATION) + end + + def duration + (Time.now.to_i - @start) / 60 + end + + def status + req = Net::HTTP::Get.new(@uri) + req['PRIVATE-TOKEN'] = ENV['GITLAB_QA_ACCESS_TOKEN'] + + res = Net::HTTP.start(@uri.hostname, @uri.port, use_ssl: true) do |http| + http.request(req) + end + + JSON.parse(res.body)['status'].to_s.to_sym + end + end +end + +case ARGV[0] +when 'omnibus' + Trigger::Omnibus.new.invoke!.wait! +when 'cng' + Trigger::CNG.new.invoke!.wait! +else + puts "Please provide a valid option: + omnibus - Triggers a pipeline that builds the omnibus-gitlab package + cng - Triggers a pipeline that builds images used by the GitLab helm chart" +end diff --git a/scripts/trigger-build-cloud-native b/scripts/trigger-build-cloud-native deleted file mode 100755 index b6ca75a588d..00000000000 --- a/scripts/trigger-build-cloud-native +++ /dev/null @@ -1,61 +0,0 @@ -#!/usr/bin/env ruby - -require 'gitlab' - -# -# Configure credentials to be used with gitlab gem -# -Gitlab.configure do |config| - config.endpoint = 'https://gitlab.com/api/v4' -end - -# -# The remote project -# -GITLAB_CNG_REPO = 'gitlab-org/build/CNG'.freeze - -def ee? - ENV['CI_PROJECT_NAME'] == 'gitlab-ee' || File.exist?('CHANGELOG-EE.md') -end - -def read_file_version(filename) - raw_version = File.read(filename).strip - - # if the version matches semver format, treat it as a tag and prepend `v` - if raw_version =~ Regexp.compile(/^\d+\.\d+\.\d+(-rc\d+)?(-ee)?$/) - "v#{raw_version}" - else - raw_version - end -end - -def params - params = { - 'GITLAB_SHELL_VERSION' => read_file_version('GITLAB_SHELL_VERSION'), - 'GITALY_VERSION' => read_file_version('GITALY_SERVER_VERSION'), - 'TRIGGERED_USER' => ENV['GITLAB_USER_NAME'], - 'TRIGGER_SOURCE' => "https://gitlab.com/gitlab-org/#{ENV['CI_PROJECT_NAME']}/-/jobs/#{ENV['CI_JOB_ID']}" - } - - if ee? - params['EE_PIPELINE'] = 'true' - params['GITLAB_EE_VERSION'] = ENV['CI_COMMIT_REF_NAME'] - else - params['CE_PIPELINE'] = 'true' - params['GITLAB_CE_VERSION'] = ENV['CI_COMMIT_REF_NAME'] - end - - params -end - -# -# Trigger a pipeline -# -def trigger_pipeline - # Create the cross project pipeline using CI_JOB_TOKEN - pipeline = Gitlab.run_trigger(GITLAB_CNG_REPO, ENV['CI_JOB_TOKEN'], 'master', params) - - puts "Triggered https://gitlab.com/#{GITLAB_CNG_REPO}/pipelines/#{pipeline.id}" -end - -trigger_pipeline diff --git a/scripts/trigger-build-omnibus b/scripts/trigger-build-omnibus deleted file mode 100755 index 95f35b44f5a..00000000000 --- a/scripts/trigger-build-omnibus +++ /dev/null @@ -1,108 +0,0 @@ -#!/usr/bin/env ruby - -require 'net/http' -require 'json' -require 'cgi' - -module Omnibus - PROJECT_PATH = 'gitlab-org/omnibus-gitlab'.freeze - - class Trigger - TOKEN = ENV['BUILD_TRIGGER_TOKEN'] - TRIGGERER = ENV['CI_PROJECT_NAME'] - - def initialize - @uri = URI("https://gitlab.com/api/v4/projects/#{CGI.escape(Omnibus::PROJECT_PATH)}/trigger/pipeline") - @params = env_params.merge(file_params).merge(token: TOKEN) - end - - def invoke! - res = Net::HTTP.post_form(@uri, @params) - id = JSON.parse(res.body)['id'] - - if id - puts "Triggered https://gitlab.com/#{Omnibus::PROJECT_PATH}/pipelines/#{id}" - puts "Waiting for downstream pipeline status" - else - raise "Trigger failed! The response from the trigger is: #{res.body}" - end - - Omnibus::Pipeline.new(id) - end - - private - - def ee? - TRIGGERER == 'gitlab-ee' || File.exist?('CHANGELOG-EE.md') - end - - def env_params - { - "ref" => ENV["OMNIBUS_BRANCH"] || "master", - "variables[GITLAB_VERSION]" => ENV["CI_COMMIT_SHA"], - "variables[ALTERNATIVE_SOURCES]" => true, - "variables[ee]" => ee? ? 'true' : 'false', - "variables[TRIGGERED_USER]" => ENV["GITLAB_USER_NAME"], - "variables[TRIGGER_SOURCE]" => "https://gitlab.com/gitlab-org/#{ENV['CI_PROJECT_NAME']}/-/jobs/#{ENV['CI_JOB_ID']}" - } - end - - def file_params - Hash.new.tap do |params| - Dir.glob("*_VERSION").each do |version_file| - params["variables[#{version_file}]"] = File.read(version_file).strip - end - end - end - end - - class Pipeline - INTERVAL = 60 # seconds - MAX_DURATION = 3600 * 3 # 3 hours - - def initialize(id) - @start = Time.now.to_i - @uri = URI("https://gitlab.com/api/v4/projects/#{CGI.escape(Omnibus::PROJECT_PATH)}/pipelines/#{id}") - end - - def wait! - loop do - raise "Pipeline timed out after waiting for #{duration} minutes!" if timeout? - - case status - when :created, :pending, :running - print "." - sleep INTERVAL - when :success - puts "Omnibus pipeline succeeded in #{duration} minutes!" - break - else - raise "Omnibus pipeline did not succeed!" - end - - STDOUT.flush - end - end - - def timeout? - Time.now.to_i > (@start + MAX_DURATION) - end - - def duration - (Time.now.to_i - @start) / 60 - end - - def status - req = Net::HTTP::Get.new(@uri) - req['PRIVATE-TOKEN'] = ENV['GITLAB_QA_ACCESS_TOKEN'] - - res = Net::HTTP.start(@uri.hostname, @uri.port, use_ssl: true) do |http| - http.request(req) - end - - JSON.parse(res.body)['status'].to_s.to_sym - end - end -end - -Omnibus::Trigger.new.invoke!.wait! diff --git a/spec/controllers/admin/application_settings_controller_spec.rb b/spec/controllers/admin/application_settings_controller_spec.rb index b4fc2aa326f..9d10d725ff3 100644 --- a/spec/controllers/admin/application_settings_controller_spec.rb +++ b/spec/controllers/admin/application_settings_controller_spec.rb @@ -73,7 +73,7 @@ describe Admin::ApplicationSettingsController do end it 'updates the restricted_visibility_levels when empty array is passed' do - put :update, application_setting: { restricted_visibility_levels: [] } + put :update, application_setting: { restricted_visibility_levels: [""] } expect(response).to redirect_to(admin_application_settings_path) expect(ApplicationSetting.current.restricted_visibility_levels).to be_empty diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb index b048da1991c..74f362fd7fc 100644 --- a/spec/controllers/application_controller_spec.rb +++ b/spec/controllers/application_controller_spec.rb @@ -1,3 +1,4 @@ +# coding: utf-8 require 'spec_helper' describe ApplicationController do @@ -457,6 +458,8 @@ describe ApplicationController do end context 'for sessionless users' do + render_views + before do sign_out user end @@ -467,6 +470,14 @@ describe ApplicationController do expect(response).to have_gitlab_http_status(403) end + it 'renders the error message when the format was html' do + get :index, + private_token: create(:personal_access_token, user: user).token, + format: :html + + expect(response.body).to have_content /accept the terms of service/i + end + it 'renders a 200 when the sessionless user accepted the terms' do accept_terms(user) @@ -477,4 +488,85 @@ describe ApplicationController do end end end + + describe '#append_info_to_payload' do + controller(described_class) do + attr_reader :last_payload + + def index + render text: 'authenticated' + end + + def append_info_to_payload(payload) + super + + @last_payload = payload + end + end + + it 'does not log errors with a 200 response' do + get :index + + expect(controller.last_payload.has_key?(:response)).to be_falsey + end + + context '422 errors' do + it 'logs a response with a string' do + response = spy(ActionDispatch::Response, status: 422, body: 'Hello world', content_type: 'application/json', cookies: {}) + allow(controller).to receive(:response).and_return(response) + get :index + + expect(controller.last_payload[:response]).to eq('Hello world') + end + + it 'logs a response with an array' do + body = ['I want', 'my hat back'] + response = spy(ActionDispatch::Response, status: 422, body: body, content_type: 'application/json', cookies: {}) + allow(controller).to receive(:response).and_return(response) + get :index + + expect(controller.last_payload[:response]).to eq(body) + end + + it 'does not log a string with an empty body' do + response = spy(ActionDispatch::Response, status: 422, body: nil, content_type: 'application/json', cookies: {}) + allow(controller).to receive(:response).and_return(response) + get :index + + expect(controller.last_payload.has_key?(:response)).to be_falsey + end + + it 'does not log an HTML body' do + response = spy(ActionDispatch::Response, status: 422, body: 'This is a test', content_type: 'application/html', cookies: {}) + allow(controller).to receive(:response).and_return(response) + get :index + + expect(controller.last_payload.has_key?(:response)).to be_falsey + end + end + end + + describe '#access_denied' do + controller(described_class) do + def index + access_denied!(params[:message]) + end + end + + before do + sign_in user + end + + it 'renders a 404 without a message' do + get :index + + expect(response).to have_gitlab_http_status(404) + end + + it 'renders a 403 when a message is passed to access denied' do + get :index, message: 'None shall pass' + + expect(response).to have_gitlab_http_status(403) + end + end end diff --git a/spec/controllers/boards/issues_controller_spec.rb b/spec/controllers/boards/issues_controller_spec.rb index dcb0faffbd4..e47ff8661a2 100644 --- a/spec/controllers/boards/issues_controller_spec.rb +++ b/spec/controllers/boards/issues_controller_spec.rb @@ -18,7 +18,7 @@ describe Boards::IssuesController do end describe 'GET index', :request_store do - let(:johndoe) { create(:user, avatar: fixture_file_upload(File.join(Rails.root, 'spec/fixtures/dk.png'))) } + let(:johndoe) { create(:user, avatar: fixture_file_upload(File.join('spec/fixtures/dk.png'))) } context 'with invalid board id' do it 'returns a not found 404 response' do diff --git a/spec/controllers/boards/lists_controller_spec.rb b/spec/controllers/boards/lists_controller_spec.rb index 71d45a22d91..57ccbf1d6b5 100644 --- a/spec/controllers/boards/lists_controller_spec.rb +++ b/spec/controllers/boards/lists_controller_spec.rb @@ -156,12 +156,18 @@ describe Boards::ListsController do def move(user:, board:, list:, position:) sign_in(user) - patch :update, namespace_id: project.namespace.to_param, - project_id: project, - board_id: board.to_param, - id: list.to_param, - list: { position: position }, - format: :json + params = { namespace_id: project.namespace.to_param, + project_id: project, + board_id: board.to_param, + id: list.to_param, + list: { position: position }, + format: :json } + + if Gitlab.rails5? + patch :update, params: params, as: :json + else + patch :update, params + end end end diff --git a/spec/controllers/concerns/controller_with_cross_project_access_check_spec.rb b/spec/controllers/concerns/controller_with_cross_project_access_check_spec.rb index 27f558e1b5d..d20471ef603 100644 --- a/spec/controllers/concerns/controller_with_cross_project_access_check_spec.rb +++ b/spec/controllers/concerns/controller_with_cross_project_access_check_spec.rb @@ -43,13 +43,13 @@ describe ControllerWithCrossProjectAccessCheck do end end - it 'renders a 404 with trying to access a cross project page' do + it 'renders a 403 with trying to access a cross project page' do message = "This page is unavailable because you are not allowed to read "\ "information across multiple projects." get :index - expect(response).to have_gitlab_http_status(404) + expect(response).to have_gitlab_http_status(403) expect(response.body).to match(/#{message}/) end @@ -119,7 +119,7 @@ describe ControllerWithCrossProjectAccessCheck do get :index - expect(response).to have_gitlab_http_status(404) + expect(response).to have_gitlab_http_status(403) end it 'is executed when the `unless` condition returns true' do @@ -127,19 +127,19 @@ describe ControllerWithCrossProjectAccessCheck do get :index - expect(response).to have_gitlab_http_status(404) + expect(response).to have_gitlab_http_status(403) end it 'does not skip the check on an action that is not skipped' do get :show, id: 'hello' - expect(response).to have_gitlab_http_status(404) + expect(response).to have_gitlab_http_status(403) end it 'does not skip the check on an action that was not defined to skip' do get :edit, id: 'hello' - expect(response).to have_gitlab_http_status(404) + expect(response).to have_gitlab_http_status(403) end end end diff --git a/spec/controllers/concerns/internal_redirect_spec.rb b/spec/controllers/concerns/internal_redirect_spec.rb index a0ee13b2352..7e23b56356e 100644 --- a/spec/controllers/concerns/internal_redirect_spec.rb +++ b/spec/controllers/concerns/internal_redirect_spec.rb @@ -54,6 +54,31 @@ describe InternalRedirect do end end + describe '#sanitize_redirect' do + let(:valid_path) { '/hello/world?hello=world' } + let(:valid_url) { "http://test.host#{valid_path}" } + + it 'returns `nil` for invalid paths' do + invalid_path = '//not/valid' + + expect(controller.sanitize_redirect(invalid_path)).to eq nil + end + + it 'returns `nil` for invalid urls' do + input = 'http://test.host:3000/invalid' + + expect(controller.sanitize_redirect(input)).to eq nil + end + + it 'returns input for valid paths' do + expect(controller.sanitize_redirect(valid_path)).to eq valid_path + end + + it 'returns path for valid urls' do + expect(controller.sanitize_redirect(valid_url)).to eq valid_path + end + end + describe '#host_allowed?' do it 'allows uris with the same host and port' do expect(controller.host_allowed?(URI('http://test.host/test'))).to be(true) diff --git a/spec/controllers/graphql_controller_spec.rb b/spec/controllers/graphql_controller_spec.rb new file mode 100644 index 00000000000..1449036e148 --- /dev/null +++ b/spec/controllers/graphql_controller_spec.rb @@ -0,0 +1,69 @@ +require 'spec_helper' + +describe GraphqlController do + describe 'execute' do + let(:user) { nil } + + before do + sign_in(user) if user + + run_test_query! + end + + subject { query_response } + + context 'graphql is disabled by feature flag' do + let(:user) { nil } + + before do + stub_feature_flags(graphql: false) + end + + it 'returns 404' do + run_test_query! + + expect(response).to have_gitlab_http_status(404) + end + end + + context 'signed out' do + let(:user) { nil } + + it 'runs the query with current_user: nil' do + is_expected.to eq('echo' => 'nil says: test success') + end + end + + context 'signed in' do + let(:user) { create(:user, username: 'Simon') } + + it 'runs the query with current_user set' do + is_expected.to eq('echo' => '"Simon" says: test success') + end + end + + context 'invalid variables' do + it 'returns an error' do + run_test_query!(variables: "This is not JSON") + + expect(response).to have_gitlab_http_status(422) + expect(json_response['errors'].first['message']).not_to be_nil + end + end + end + + # Chosen to exercise all the moving parts in GraphqlController#execute + def run_test_query!(variables: { 'text' => 'test success' }) + query = <<~QUERY + query Echo($text: String) { + echo(text: $text) + } + QUERY + + post :execute, query: query, operationName: 'Echo', variables: variables + end + + def query_response + json_response['data'] + end +end diff --git a/spec/controllers/groups/avatars_controller_spec.rb b/spec/controllers/groups/avatars_controller_spec.rb index 506aeee7d2a..7feecd0c380 100644 --- a/spec/controllers/groups/avatars_controller_spec.rb +++ b/spec/controllers/groups/avatars_controller_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' describe Groups::AvatarsController do let(:user) { create(:user) } - let(:group) { create(:group, avatar: fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png")) } + let(:group) { create(:group, avatar: fixture_file_upload("spec/fixtures/dk.png", "image/png")) } before do group.add_owner(user) diff --git a/spec/controllers/groups/shared_projects_controller_spec.rb b/spec/controllers/groups/shared_projects_controller_spec.rb index d8fa41abb18..003c8c262e7 100644 --- a/spec/controllers/groups/shared_projects_controller_spec.rb +++ b/spec/controllers/groups/shared_projects_controller_spec.rb @@ -38,7 +38,7 @@ describe Groups::SharedProjectsController do end it 'allows filtering shared projects' do - project = create(:project, :archived, namespace: user.namespace, name: "Searching for") + project = create(:project, namespace: user.namespace, name: "Searching for") share_project(project) get_shared_projects(filter: 'search') @@ -55,5 +55,14 @@ describe Groups::SharedProjectsController do expect(json_project_ids).to eq([second_project.id, shared_project.id]) end + + it 'does not include archived projects' do + archived_project = create(:project, :archived, namespace: user.namespace) + share_project(archived_project) + + get_shared_projects + + expect(json_project_ids).to contain_exactly(shared_project.id) + end end end diff --git a/spec/controllers/import/gitlab_projects_controller_spec.rb b/spec/controllers/import/gitlab_projects_controller_spec.rb index 8759d3c0b97..d624659bce9 100644 --- a/spec/controllers/import/gitlab_projects_controller_spec.rb +++ b/spec/controllers/import/gitlab_projects_controller_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe Import::GitlabProjectsController do set(:namespace) { create(:namespace) } set(:user) { namespace.owner } - let(:file) { fixture_file_upload(Rails.root + 'spec/fixtures/doc_sample.txt', 'text/plain') } + let(:file) { fixture_file_upload('spec/fixtures/doc_sample.txt', 'text/plain') } before do sign_in(user) diff --git a/spec/controllers/import/google_code_controller_spec.rb b/spec/controllers/import/google_code_controller_spec.rb index 4241db6e771..0763492d88a 100644 --- a/spec/controllers/import/google_code_controller_spec.rb +++ b/spec/controllers/import/google_code_controller_spec.rb @@ -4,7 +4,7 @@ describe Import::GoogleCodeController do include ImportSpecHelper let(:user) { create(:user) } - let(:dump_file) { fixture_file_upload(Rails.root + 'spec/fixtures/GoogleCodeProjectHosting.json', 'application/json') } + let(:dump_file) { fixture_file_upload('spec/fixtures/GoogleCodeProjectHosting.json', 'application/json') } before do sign_in(user) diff --git a/spec/controllers/profiles/avatars_controller_spec.rb b/spec/controllers/profiles/avatars_controller_spec.rb index 4fa0462ccdf..909709e1103 100644 --- a/spec/controllers/profiles/avatars_controller_spec.rb +++ b/spec/controllers/profiles/avatars_controller_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe Profiles::AvatarsController do - let(:user) { create(:user, avatar: fixture_file_upload(Rails.root + "spec/fixtures/dk.png")) } + let(:user) { create(:user, avatar: fixture_file_upload("spec/fixtures/dk.png")) } before do sign_in(user) diff --git a/spec/controllers/projects/avatars_controller_spec.rb b/spec/controllers/projects/avatars_controller_spec.rb index 6a41c4d23ea..acfa2730d94 100644 --- a/spec/controllers/projects/avatars_controller_spec.rb +++ b/spec/controllers/projects/avatars_controller_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe Projects::AvatarsController do - let(:project) { create(:project, :repository, avatar: fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png")) } + let(:project) { create(:project, :repository, avatar: fixture_file_upload("spec/fixtures/dk.png", "image/png")) } let(:user) { create(:user) } before do diff --git a/spec/controllers/projects/blob_controller_spec.rb b/spec/controllers/projects/blob_controller_spec.rb index 00a7df6ccc8..9e696e9cb29 100644 --- a/spec/controllers/projects/blob_controller_spec.rb +++ b/spec/controllers/projects/blob_controller_spec.rb @@ -55,6 +55,25 @@ describe Projects::BlobController do expect(json_response).to have_key 'raw_path' end end + + context "with viewer=none" do + let(:id) { 'master/README.md' } + + before do + get(:show, + namespace_id: project.namespace, + project_id: project, + id: id, + format: :json, + viewer: 'none') + end + + it do + expect(response).to be_ok + expect(json_response).not_to have_key 'html' + expect(json_response).to have_key 'raw_path' + end + end end context 'with tree path' do diff --git a/spec/controllers/projects/imports_controller_spec.rb b/spec/controllers/projects/imports_controller_spec.rb index 011843baffc..812833cc86b 100644 --- a/spec/controllers/projects/imports_controller_spec.rb +++ b/spec/controllers/projects/imports_controller_spec.rb @@ -29,7 +29,7 @@ describe Projects::ImportsController do context 'when import is in progress' do before do - project.update_attribute(:import_status, :started) + project.update_attributes(import_status: :started) end it 'renders template' do @@ -47,7 +47,7 @@ describe Projects::ImportsController do context 'when import failed' do before do - project.update_attribute(:import_status, :failed) + project.update_attributes(import_status: :failed) end it 'redirects to new_namespace_project_import_path' do @@ -59,7 +59,7 @@ describe Projects::ImportsController do context 'when import finished' do before do - project.update_attribute(:import_status, :finished) + project.update_attributes(import_status: :finished) end context 'when project is a fork' do @@ -108,7 +108,7 @@ describe Projects::ImportsController do context 'when import never happened' do before do - project.update_attribute(:import_status, :none) + project.update_attributes(import_status: :none) end it 'redirects to namespace_project_path' do diff --git a/spec/controllers/projects/jobs_controller_spec.rb b/spec/controllers/projects/jobs_controller_spec.rb index a08fcea27a5..06c8a432561 100644 --- a/spec/controllers/projects/jobs_controller_spec.rb +++ b/spec/controllers/projects/jobs_controller_spec.rb @@ -265,7 +265,7 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do expect(json_response['text']).to eq status.text expect(json_response['label']).to eq status.label expect(json_response['icon']).to eq status.icon - expect(json_response['favicon']).to match_asset_path "/assets/ci_favicons/#{status.favicon}.ico" + expect(json_response['favicon']).to match_asset_path "/assets/ci_favicons/#{status.favicon}.png" end end diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb index 6e8de6db9c3..a412e74581d 100644 --- a/spec/controllers/projects/merge_requests_controller_spec.rb +++ b/spec/controllers/projects/merge_requests_controller_spec.rb @@ -80,6 +80,16 @@ describe Projects::MergeRequestsController do )) end end + + context "that is invalid" do + let(:merge_request) { create(:invalid_merge_request, target_project: project, source_project: project) } + + it "renders merge request page" do + go(format: :html) + + expect(response).to be_success + end + end end describe 'as json' do @@ -106,6 +116,16 @@ describe Projects::MergeRequestsController do expect(response).to match_response_schema('entities/merge_request_widget') end end + + context "that is invalid" do + let(:merge_request) { create(:invalid_merge_request, target_project: project, source_project: project) } + + it "renders merge request page" do + go(format: :json) + + expect(response).to be_success + end + end end describe "as diff" do @@ -214,7 +234,7 @@ describe Projects::MergeRequestsController do body = JSON.parse(response.body) expect(body['assignee'].keys) - .to match_array(%w(name username avatar_url)) + .to match_array(%w(name username avatar_url id state web_url)) end end @@ -681,7 +701,7 @@ describe Projects::MergeRequestsController do expect(json_response['text']).to eq status.text expect(json_response['label']).to eq status.label expect(json_response['icon']).to eq status.icon - expect(json_response['favicon']).to match_asset_path "/assets/ci_favicons/#{status.favicon}.ico" + expect(json_response['favicon']).to match_asset_path "/assets/ci_favicons/#{status.favicon}.png" end end diff --git a/spec/controllers/projects/milestones_controller_spec.rb b/spec/controllers/projects/milestones_controller_spec.rb index 548c5ef36e7..02b30f9bc6d 100644 --- a/spec/controllers/projects/milestones_controller_spec.rb +++ b/spec/controllers/projects/milestones_controller_spec.rb @@ -57,19 +57,36 @@ describe Projects::MilestonesController do context "as json" do let!(:group) { create(:group, :public) } let!(:group_milestone) { create(:milestone, group: group) } - let!(:group_member) { create(:group_member, group: group, user: user) } - before do - project.update(namespace: group) - get :index, namespace_id: project.namespace.id, project_id: project.id, format: :json + context 'with a single group ancestor' do + before do + project.update(namespace: group) + get :index, namespace_id: project.namespace.id, project_id: project.id, format: :json + end + + it "queries projects milestones and groups milestones" do + milestones = assigns(:milestones) + + expect(milestones.count).to eq(2) + expect(milestones).to match_array([milestone, group_milestone]) + end end - it "queries projects milestones and groups milestones" do - milestones = assigns(:milestones) + context 'with nested groups', :nested_groups do + let!(:subgroup) { create(:group, :public, parent: group) } + let!(:subgroup_milestone) { create(:milestone, group: subgroup) } + + before do + project.update(namespace: subgroup) + get :index, namespace_id: project.namespace.id, project_id: project.id, format: :json + end + + it "queries projects milestones and all ancestors milestones" do + milestones = assigns(:milestones) - expect(milestones.count).to eq(2) - expect(milestones.where(project_id: nil).first).to eq(group_milestone) - expect(milestones.where(group_id: nil).first).to eq(milestone) + expect(milestones.count).to eq(3) + expect(milestones).to match_array([milestone, group_milestone, subgroup_milestone]) + end end end end diff --git a/spec/controllers/projects/pipeline_schedules_controller_spec.rb b/spec/controllers/projects/pipeline_schedules_controller_spec.rb index 3506305f755..4cdaa54e0bc 100644 --- a/spec/controllers/projects/pipeline_schedules_controller_spec.rb +++ b/spec/controllers/projects/pipeline_schedules_controller_spec.rb @@ -310,9 +310,19 @@ describe Projects::PipelineSchedulesController do end def go - put :update, namespace_id: project.namespace.to_param, - project_id: project, id: pipeline_schedule, - schedule: schedule + if Gitlab.rails5? + put :update, params: { namespace_id: project.namespace.to_param, + project_id: project, + id: pipeline_schedule, + schedule: schedule }, + as: :html + + else + put :update, namespace_id: project.namespace.to_param, + project_id: project, + id: pipeline_schedule, + schedule: schedule + end end end diff --git a/spec/controllers/projects/pipelines_controller_spec.rb b/spec/controllers/projects/pipelines_controller_spec.rb index 9e7bc20a6d1..9618a8417ec 100644 --- a/spec/controllers/projects/pipelines_controller_spec.rb +++ b/spec/controllers/projects/pipelines_controller_spec.rb @@ -17,44 +17,103 @@ describe Projects::PipelinesController do describe 'GET index.json' do before do - %w(pending running created success).each_with_index do |status, index| - sha = project.commit("HEAD~#{index}") - create(:ci_empty_pipeline, status: status, project: project, sha: sha) + %w(pending running success failed canceled).each_with_index do |status, index| + create_pipeline(status, project.commit("HEAD~#{index}")) end end - subject do - get :index, namespace_id: project.namespace, project_id: project, format: :json + context 'when using persisted stages', :request_store do + before do + stub_feature_flags(ci_pipeline_persisted_stages: true) + end + + it 'returns serialized pipelines', :request_store do + queries = ActiveRecord::QueryRecorder.new do + get_pipelines_index_json + end + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to match_response_schema('pipeline') + + expect(json_response).to include('pipelines') + expect(json_response['pipelines'].count).to eq 5 + expect(json_response['count']['all']).to eq '5' + expect(json_response['count']['running']).to eq '1' + expect(json_response['count']['pending']).to eq '1' + expect(json_response['count']['finished']).to eq '3' + + json_response.dig('pipelines', 0, 'details', 'stages').tap do |stages| + expect(stages.count).to eq 3 + end + + expect(queries.count).to be + end end - it 'returns JSON with serialized pipelines' do - subject + context 'when using legacy stages', :request_store do + before do + stub_feature_flags(ci_pipeline_persisted_stages: false) + end - expect(response).to have_gitlab_http_status(:ok) - expect(response).to match_response_schema('pipeline') + it 'returns JSON with serialized pipelines', :request_store do + queries = ActiveRecord::QueryRecorder.new do + get_pipelines_index_json + end + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to match_response_schema('pipeline') - expect(json_response).to include('pipelines') - expect(json_response['pipelines'].count).to eq 4 - expect(json_response['count']['all']).to eq '4' - expect(json_response['count']['running']).to eq '1' - expect(json_response['count']['pending']).to eq '1' - expect(json_response['count']['finished']).to eq '1' + expect(json_response).to include('pipelines') + expect(json_response['pipelines'].count).to eq 5 + expect(json_response['count']['all']).to eq '5' + expect(json_response['count']['running']).to eq '1' + expect(json_response['count']['pending']).to eq '1' + expect(json_response['count']['finished']).to eq '3' + + json_response.dig('pipelines', 0, 'details', 'stages').tap do |stages| + expect(stages.count).to eq 3 + end + + expect(queries.count).to be_within(3).of(30) + end end it 'does not include coverage data for the pipelines' do - subject + get_pipelines_index_json expect(json_response['pipelines'][0]).not_to include('coverage') end context 'when performing gitaly calls', :request_store do it 'limits the Gitaly requests' do - expect { subject }.to change { Gitlab::GitalyClient.get_request_count }.by(3) + expect { get_pipelines_index_json } + .to change { Gitlab::GitalyClient.get_request_count }.by(2) end end + + def get_pipelines_index_json + get :index, namespace_id: project.namespace, + project_id: project, + format: :json + end + + def create_pipeline(status, sha) + pipeline = create(:ci_empty_pipeline, status: status, + project: project, + sha: sha) + + create_build(pipeline, 'build', 1, 'build') + create_build(pipeline, 'test', 2, 'test') + create_build(pipeline, 'deploy', 3, 'deploy') + end + + def create_build(pipeline, stage, stage_idx, name) + status = %w[created running pending success failed canceled].sample + create(:ci_build, pipeline: pipeline, stage: stage, stage_idx: stage_idx, name: name, status: status) + end end - describe 'GET show JSON' do + describe 'GET show.json' do let(:pipeline) { create(:ci_pipeline_with_one_job, project: project) } it 'returns the pipeline' do @@ -67,6 +126,14 @@ describe Projects::PipelinesController do end context 'when the pipeline has multiple stages and groups', :request_store do + let(:project) { create(:project, :repository) } + + let(:pipeline) do + create(:ci_empty_pipeline, project: project, + user: user, + sha: project.commit.id) + end + before do create_build('build', 0, 'build') create_build('test', 1, 'rspec 0') @@ -74,11 +141,6 @@ describe Projects::PipelinesController do create_build('post deploy', 3, 'pages 0') end - let(:project) { create(:project, :repository) } - let(:pipeline) do - create(:ci_empty_pipeline, project: project, user: user, sha: project.commit.id) - end - it 'does not perform N + 1 queries' do control_count = ActiveRecord::QueryRecorder.new { get_pipeline_json }.count @@ -90,6 +152,7 @@ describe Projects::PipelinesController do create_build('post deploy', 3, 'pages 2') new_count = ActiveRecord::QueryRecorder.new { get_pipeline_json }.count + expect(new_count).to be_within(12).of(control_count) end end @@ -190,7 +253,7 @@ describe Projects::PipelinesController do expect(json_response['text']).to eq status.text expect(json_response['label']).to eq status.label expect(json_response['icon']).to eq status.icon - expect(json_response['favicon']).to match_asset_path("/assets/ci_favicons/#{status.favicon}.ico") + expect(json_response['favicon']).to match_asset_path("/assets/ci_favicons/#{status.favicon}.png") end end diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb index 994da3cd159..705b30f0130 100644 --- a/spec/controllers/projects_controller_spec.rb +++ b/spec/controllers/projects_controller_spec.rb @@ -6,8 +6,8 @@ describe ProjectsController do let(:project) { create(:project) } let(:public_project) { create(:project, :public) } 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') } + let(:jpg) { fixture_file_upload('spec/fixtures/rails_sample.jpg', 'image/jpg') } + let(:txt) { fixture_file_upload('spec/fixtures/doc_sample.txt', 'text/plain') } describe 'GET new' do context 'with an authenticated user' do @@ -296,16 +296,22 @@ describe ProjectsController do shared_examples_for 'updating a project' do context 'when only renaming a project path' do it "sets the repository to the right path after a rename" do - original_repository_path = project.repository.path + original_repository_path = Gitlab::GitalyClient::StorageSettings.allow_disk_access do + project.repository.path + end expect { update_project path: 'renamed_path' } .to change { project.reload.path } expect(project.path).to include 'renamed_path' + assign_repository_path = Gitlab::GitalyClient::StorageSettings.allow_disk_access do + assigns(:repository).path + end + if project.hashed_storage?(:repository) - expect(assigns(:repository).path).to eq(original_repository_path) + expect(assign_repository_path).to eq(original_repository_path) else - expect(assigns(:repository).path).to include(project.path) + expect(assign_repository_path).to include(project.path) end expect(response).to have_gitlab_http_status(302) diff --git a/spec/controllers/registrations_controller_spec.rb b/spec/controllers/registrations_controller_spec.rb index 346944fd5b0..898f3863008 100644 --- a/spec/controllers/registrations_controller_spec.rb +++ b/spec/controllers/registrations_controller_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe RegistrationsController do + include TermsHelper + describe '#create' do let(:user_params) { { user: { name: 'new_user', username: 'new_username', email: 'new@user.com', password: 'Any_password' } } } @@ -67,6 +69,25 @@ describe RegistrationsController do expect(flash[:notice]).to include 'Welcome! You have signed up successfully.' end end + + context 'when terms are enforced' do + before do + enforce_terms + end + + it 'redirects back with a notice when the checkbox was not checked' do + post :create, user_params + + expect(flash[:alert]).to match /you must accept our terms/i + end + + it 'creates the user with agreement when terms are accepted' do + post :create, user_params.merge(terms_opt_in: '1') + + expect(subject.current_user).to be_present + expect(subject.current_user.terms_accepted?).to be(true) + end + end end describe '#destroy' do diff --git a/spec/controllers/search_controller_spec.rb b/spec/controllers/search_controller_spec.rb index 30c06ddf744..416a09e1684 100644 --- a/spec/controllers/search_controller_spec.rb +++ b/spec/controllers/search_controller_spec.rb @@ -32,7 +32,7 @@ describe SearchController do it 'still blocks searches without a project_id' do get :show, search: 'hello' - expect(response).to have_gitlab_http_status(404) + expect(response).to have_gitlab_http_status(403) end it 'allows searches with a project_id' do diff --git a/spec/controllers/sessions_controller_spec.rb b/spec/controllers/sessions_controller_spec.rb index 555b186fe31..2b61e0d4a85 100644 --- a/spec/controllers/sessions_controller_spec.rb +++ b/spec/controllers/sessions_controller_spec.rb @@ -257,15 +257,15 @@ describe SessionsController do end end - describe '#new' do + describe "#new" do before do set_devise_mapping(context: @request) end - it 'redirects correctly for referer on same host with params' do - search_path = '/search?search=seed_project' - allow(controller.request).to receive(:referer) - .and_return('http://%{host}%{path}' % { host: 'test.host', path: search_path }) + it "redirects correctly for referer on same host with params" do + host = "test.host" + search_path = "/search?search=seed_project" + request.headers[:HTTP_REFERER] = "http://#{host}#{search_path}" get(:new, redirect_to_referer: :yes) diff --git a/spec/controllers/uploads_controller_spec.rb b/spec/controllers/uploads_controller_spec.rb index 376b229ffc9..eb94d395a9e 100644 --- a/spec/controllers/uploads_controller_spec.rb +++ b/spec/controllers/uploads_controller_spec.rb @@ -6,13 +6,13 @@ shared_examples 'content not cached without revalidation' do end describe UploadsController do - let!(:user) { create(:user, avatar: fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png")) } + let!(:user) { create(:user, avatar: fixture_file_upload("spec/fixtures/dk.png", "image/png")) } describe 'POST create' do let(:model) { 'personal_snippet' } let(:snippet) { create(:personal_snippet, :public) } - 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') } + let(:jpg) { fixture_file_upload('spec/fixtures/rails_sample.jpg', 'image/jpg') } + let(:txt) { fixture_file_upload('spec/fixtures/doc_sample.txt', 'text/plain') } context 'when a user does not have permissions to upload a file' do it "returns 401 when the user is not logged in" do @@ -136,7 +136,7 @@ describe UploadsController do context 'for PNG files' do it 'returns Content-Disposition: inline' do note = create(:note, :with_attachment, project: project) - get :show, model: 'note', mounted_as: 'attachment', id: note.id, filename: 'image.png' + get :show, model: 'note', mounted_as: 'attachment', id: note.id, filename: 'dk.png' expect(response['Content-Disposition']).to start_with('inline;') end @@ -145,7 +145,7 @@ describe UploadsController do context 'for SVG files' do it 'returns Content-Disposition: attachment' do note = create(:note, :with_svg_attachment, project: project) - get :show, model: 'note', mounted_as: 'attachment', id: note.id, filename: 'image.svg' + get :show, model: 'note', mounted_as: 'attachment', id: note.id, filename: 'unsanitized.svg' expect(response['Content-Disposition']).to start_with('attachment;') end @@ -164,7 +164,7 @@ describe UploadsController do end it "redirects to the sign in page" do - get :show, model: "user", mounted_as: "avatar", id: user.id, filename: "image.png" + get :show, model: "user", mounted_as: "avatar", id: user.id, filename: "dk.png" expect(response).to redirect_to(new_user_session_path) end @@ -172,14 +172,14 @@ describe UploadsController do context "when the user isn't blocked" do it "responds with status 200" do - get :show, model: "user", mounted_as: "avatar", id: user.id, filename: "image.png" + get :show, model: "user", mounted_as: "avatar", id: user.id, filename: "dk.png" expect(response).to have_gitlab_http_status(200) end it_behaves_like 'content not cached without revalidation' do subject do - get :show, model: 'user', mounted_as: 'avatar', id: user.id, filename: 'image.png' + get :show, model: 'user', mounted_as: 'avatar', id: user.id, filename: 'dk.png' response end @@ -189,14 +189,14 @@ describe UploadsController do context "when not signed in" do it "responds with status 200" do - get :show, model: "user", mounted_as: "avatar", id: user.id, filename: "image.png" + get :show, model: "user", mounted_as: "avatar", id: user.id, filename: "dk.png" expect(response).to have_gitlab_http_status(200) end it_behaves_like 'content not cached without revalidation' do subject do - get :show, model: 'user', mounted_as: 'avatar', id: user.id, filename: 'image.png' + get :show, model: 'user', mounted_as: 'avatar', id: user.id, filename: 'dk.png' response end @@ -205,7 +205,7 @@ describe UploadsController do end context "when viewing a project avatar" do - let!(:project) { create(:project, avatar: fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png")) } + let!(:project) { create(:project, avatar: fixture_file_upload("spec/fixtures/dk.png", "image/png")) } context "when the project is public" do before do @@ -214,14 +214,14 @@ describe UploadsController do context "when not signed in" do it "responds with status 200" do - get :show, model: "project", mounted_as: "avatar", id: project.id, filename: "image.png" + get :show, model: "project", mounted_as: "avatar", id: project.id, filename: "dk.png" expect(response).to have_gitlab_http_status(200) end it_behaves_like 'content not cached without revalidation' do subject do - get :show, model: 'project', mounted_as: 'avatar', id: project.id, filename: 'image.png' + get :show, model: 'project', mounted_as: 'avatar', id: project.id, filename: 'dk.png' response end @@ -234,14 +234,14 @@ describe UploadsController do end it "responds with status 200" do - get :show, model: "project", mounted_as: "avatar", id: project.id, filename: "image.png" + get :show, model: "project", mounted_as: "avatar", id: project.id, filename: "dk.png" expect(response).to have_gitlab_http_status(200) end it_behaves_like 'content not cached without revalidation' do subject do - get :show, model: 'project', mounted_as: 'avatar', id: project.id, filename: 'image.png' + get :show, model: 'project', mounted_as: 'avatar', id: project.id, filename: 'dk.png' response end @@ -256,7 +256,7 @@ describe UploadsController do context "when not signed in" do it "redirects to the sign in page" do - get :show, model: "project", mounted_as: "avatar", id: project.id, filename: "image.png" + get :show, model: "project", mounted_as: "avatar", id: project.id, filename: "dk.png" expect(response).to redirect_to(new_user_session_path) end @@ -279,7 +279,7 @@ describe UploadsController do end it "redirects to the sign in page" do - get :show, model: "project", mounted_as: "avatar", id: project.id, filename: "image.png" + get :show, model: "project", mounted_as: "avatar", id: project.id, filename: "dk.png" expect(response).to redirect_to(new_user_session_path) end @@ -287,14 +287,14 @@ describe UploadsController do context "when the user isn't blocked" do it "responds with status 200" do - get :show, model: "project", mounted_as: "avatar", id: project.id, filename: "image.png" + get :show, model: "project", mounted_as: "avatar", id: project.id, filename: "dk.png" expect(response).to have_gitlab_http_status(200) end it_behaves_like 'content not cached without revalidation' do subject do - get :show, model: 'project', mounted_as: 'avatar', id: project.id, filename: 'image.png' + get :show, model: 'project', mounted_as: 'avatar', id: project.id, filename: 'dk.png' response end @@ -304,7 +304,7 @@ describe UploadsController do context "when the user doesn't have access to the project" do it "responds with status 404" do - get :show, model: "project", mounted_as: "avatar", id: project.id, filename: "image.png" + get :show, model: "project", mounted_as: "avatar", id: project.id, filename: "dk.png" expect(response).to have_gitlab_http_status(404) end @@ -314,19 +314,19 @@ describe UploadsController do end context "when viewing a group avatar" do - let!(:group) { create(:group, avatar: fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png")) } + let!(:group) { create(:group, avatar: fixture_file_upload("spec/fixtures/dk.png", "image/png")) } context "when the group is public" do context "when not signed in" do it "responds with status 200" do - get :show, model: "group", mounted_as: "avatar", id: group.id, filename: "image.png" + get :show, model: "group", mounted_as: "avatar", id: group.id, filename: "dk.png" expect(response).to have_gitlab_http_status(200) end it_behaves_like 'content not cached without revalidation' do subject do - get :show, model: 'group', mounted_as: 'avatar', id: group.id, filename: 'image.png' + get :show, model: 'group', mounted_as: 'avatar', id: group.id, filename: 'dk.png' response end @@ -339,14 +339,14 @@ describe UploadsController do end it "responds with status 200" do - get :show, model: "group", mounted_as: "avatar", id: group.id, filename: "image.png" + get :show, model: "group", mounted_as: "avatar", id: group.id, filename: "dk.png" expect(response).to have_gitlab_http_status(200) end it_behaves_like 'content not cached without revalidation' do subject do - get :show, model: 'group', mounted_as: 'avatar', id: group.id, filename: 'image.png' + get :show, model: 'group', mounted_as: 'avatar', id: group.id, filename: 'dk.png' response end @@ -375,7 +375,7 @@ describe UploadsController do end it "redirects to the sign in page" do - get :show, model: "group", mounted_as: "avatar", id: group.id, filename: "image.png" + get :show, model: "group", mounted_as: "avatar", id: group.id, filename: "dk.png" expect(response).to redirect_to(new_user_session_path) end @@ -383,14 +383,14 @@ describe UploadsController do context "when the user isn't blocked" do it "responds with status 200" do - get :show, model: "group", mounted_as: "avatar", id: group.id, filename: "image.png" + get :show, model: "group", mounted_as: "avatar", id: group.id, filename: "dk.png" expect(response).to have_gitlab_http_status(200) end it_behaves_like 'content not cached without revalidation' do subject do - get :show, model: 'group', mounted_as: 'avatar', id: group.id, filename: 'image.png' + get :show, model: 'group', mounted_as: 'avatar', id: group.id, filename: 'dk.png' response end @@ -400,7 +400,7 @@ describe UploadsController do context "when the user doesn't have access to the project" do it "responds with status 404" do - get :show, model: "group", mounted_as: "avatar", id: group.id, filename: "image.png" + get :show, model: "group", mounted_as: "avatar", id: group.id, filename: "dk.png" expect(response).to have_gitlab_http_status(404) end @@ -420,14 +420,14 @@ describe UploadsController do context "when not signed in" do it "responds with status 200" do - get :show, model: "note", mounted_as: "attachment", id: note.id, filename: "image.png" + get :show, model: "note", mounted_as: "attachment", id: note.id, filename: "dk.png" expect(response).to have_gitlab_http_status(200) end it_behaves_like 'content not cached without revalidation' do subject do - get :show, model: 'note', mounted_as: 'attachment', id: note.id, filename: 'image.png' + get :show, model: 'note', mounted_as: 'attachment', id: note.id, filename: 'dk.png' response end @@ -440,14 +440,14 @@ describe UploadsController do end it "responds with status 200" do - get :show, model: "note", mounted_as: "attachment", id: note.id, filename: "image.png" + get :show, model: "note", mounted_as: "attachment", id: note.id, filename: "dk.png" expect(response).to have_gitlab_http_status(200) end it_behaves_like 'content not cached without revalidation' do subject do - get :show, model: 'note', mounted_as: 'attachment', id: note.id, filename: 'image.png' + get :show, model: 'note', mounted_as: 'attachment', id: note.id, filename: 'dk.png' response end @@ -462,7 +462,7 @@ describe UploadsController do context "when not signed in" do it "redirects to the sign in page" do - get :show, model: "note", mounted_as: "attachment", id: note.id, filename: "image.png" + get :show, model: "note", mounted_as: "attachment", id: note.id, filename: "dk.png" expect(response).to redirect_to(new_user_session_path) end @@ -485,7 +485,7 @@ describe UploadsController do end it "redirects to the sign in page" do - get :show, model: "note", mounted_as: "attachment", id: note.id, filename: "image.png" + get :show, model: "note", mounted_as: "attachment", id: note.id, filename: "dk.png" expect(response).to redirect_to(new_user_session_path) end @@ -493,14 +493,14 @@ describe UploadsController do context "when the user isn't blocked" do it "responds with status 200" do - get :show, model: "note", mounted_as: "attachment", id: note.id, filename: "image.png" + get :show, model: "note", mounted_as: "attachment", id: note.id, filename: "dk.png" expect(response).to have_gitlab_http_status(200) end it_behaves_like 'content not cached without revalidation' do subject do - get :show, model: 'note', mounted_as: 'attachment', id: note.id, filename: 'image.png' + get :show, model: 'note', mounted_as: 'attachment', id: note.id, filename: 'dk.png' response end @@ -510,7 +510,7 @@ describe UploadsController do context "when the user doesn't have access to the project" do it "responds with status 404" do - get :show, model: "note", mounted_as: "attachment", id: note.id, filename: "image.png" + get :show, model: "note", mounted_as: "attachment", id: note.id, filename: "dk.png" expect(response).to have_gitlab_http_status(404) end @@ -521,7 +521,7 @@ describe UploadsController do context 'Appearance' do context 'when viewing a custom header logo' do - let!(:appearance) { create :appearance, header_logo: fixture_file_upload(Rails.root.join('spec/fixtures/dk.png'), 'image/png') } + let!(:appearance) { create :appearance, header_logo: fixture_file_upload('spec/fixtures/dk.png', 'image/png') } context 'when not signed in' do it 'responds with status 200' do @@ -541,7 +541,7 @@ describe UploadsController do end context 'when viewing a custom logo' do - let!(:appearance) { create :appearance, logo: fixture_file_upload(Rails.root.join('spec/fixtures/dk.png'), 'image/png') } + let!(:appearance) { create :appearance, logo: fixture_file_upload('spec/fixtures/dk.png', 'image/png') } context 'when not signed in' do it 'responds with status 200' do @@ -560,5 +560,26 @@ describe UploadsController do end end end + + context 'original filename or a version filename must match' do + let!(:appearance) { create :appearance, favicon: fixture_file_upload('spec/fixtures/dk.png', 'image/png') } + + context 'has a valid filename on the original file' do + it 'successfully returns the file' do + get :show, model: 'appearance', mounted_as: 'favicon', id: appearance.id, filename: 'dk.png' + + expect(response).to have_gitlab_http_status(200) + expect(response.header['Content-Disposition']).to end_with 'filename="dk.png"' + end + end + + context 'has an invalid filename on the original file' do + it 'returns a 404' do + get :show, model: 'appearance', mounted_as: 'favicon', id: appearance.id, filename: 'bogus.png' + + expect(response).to have_gitlab_http_status(404) + end + end + end end end diff --git a/spec/controllers/users/terms_controller_spec.rb b/spec/controllers/users/terms_controller_spec.rb index a744463413c..0d77e91a67d 100644 --- a/spec/controllers/users/terms_controller_spec.rb +++ b/spec/controllers/users/terms_controller_spec.rb @@ -1,6 +1,7 @@ require 'spec_helper' describe Users::TermsController do + include TermsHelper let(:user) { create(:user) } let(:term) { create(:term) } @@ -15,10 +16,25 @@ describe Users::TermsController do expect(response).to have_gitlab_http_status(:redirect) end - it 'shows terms when they exist' do - term + context 'when terms exist' do + before do + stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false') + term + end + + it 'shows terms when they exist' do + get :index + + expect(response).to have_gitlab_http_status(:success) + end - expect(response).to have_gitlab_http_status(:success) + it 'shows a message when the user already accepted the terms' do + accept_terms(user) + + get :index + + expect(controller).to set_flash.now[:notice].to(/already accepted/) + end end end diff --git a/spec/factories/lfs_objects.rb b/spec/factories/lfs_objects.rb index eaf3a4ed497..c909471bb55 100644 --- a/spec/factories/lfs_objects.rb +++ b/spec/factories/lfs_objects.rb @@ -7,7 +7,7 @@ FactoryBot.define do end trait :with_file do - file { fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "`/png") } + file { fixture_file_upload("spec/fixtures/dk.png", "`/png") } end # The uniqueness constraint means we can't use the correct OID for all LFS diff --git a/spec/factories/merge_requests.rb b/spec/factories/merge_requests.rb index fab0ec22450..3441ce1b8cb 100644 --- a/spec/factories/merge_requests.rb +++ b/spec/factories/merge_requests.rb @@ -54,6 +54,11 @@ FactoryBot.define do state :opened end + trait :invalid do + source_branch "feature_one" + target_branch "feature_two" + end + trait :locked do state :locked end @@ -98,6 +103,7 @@ FactoryBot.define do factory :merged_merge_request, traits: [:merged] factory :closed_merge_request, traits: [:closed] factory :reopened_merge_request, traits: [:opened] + factory :invalid_merge_request, traits: [:invalid] factory :merge_request_with_diffs, traits: [:with_diffs] factory :merge_request_with_diff_notes do after(:create) do |mr| diff --git a/spec/factories/notes.rb b/spec/factories/notes.rb index 40f3fa7d69b..9fdc3e616a6 100644 --- a/spec/factories/notes.rb +++ b/spec/factories/notes.rb @@ -130,11 +130,11 @@ FactoryBot.define do end trait :with_attachment do - attachment { fixture_file_upload(Rails.root.join( "spec/fixtures/dk.png"), "image/png") } + attachment { fixture_file_upload("spec/fixtures/dk.png", "image/png") } end trait :with_svg_attachment do - attachment { fixture_file_upload(Rails.root.join("spec/fixtures/unsanitized.svg"), "image/svg+xml") } + attachment { fixture_file_upload("spec/fixtures/unsanitized.svg", "image/svg+xml") } end transient do diff --git a/spec/factories/project_auto_devops.rb b/spec/factories/project_auto_devops.rb index 5ce1988c76f..b77f702f9e1 100644 --- a/spec/factories/project_auto_devops.rb +++ b/spec/factories/project_auto_devops.rb @@ -3,5 +3,14 @@ FactoryBot.define do project enabled true domain "example.com" + deploy_strategy :continuous + + trait :manual do + deploy_strategy :manual + end + + trait :disabled do + enabled false + end end end diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb index 16e025618a6..f6b05bac0e8 100644 --- a/spec/factories/projects.rb +++ b/spec/factories/projects.rb @@ -151,11 +151,6 @@ FactoryBot.define do trait :empty_repo do after(:create) do |project| raise "Failed to create repository!" unless project.create_repository - - # We delete hooks so that gitlab-shell will not try to authenticate with - # an API that isn't running - project.gitlab_shell.rm_directory(project.repository_storage, - File.join("#{project.disk_path}.git", 'hooks')) end end @@ -180,13 +175,6 @@ FactoryBot.define do trait :wiki_repo do after(:create) do |project| raise 'Failed to create wiki repository!' unless project.create_wiki - - # We delete hooks so that gitlab-shell will not try to authenticate with - # an API that isn't running - project.gitlab_shell.rm_directory( - project.repository_storage, - File.join("#{project.wiki.repository.disk_path}.git", "hooks") - ) end end diff --git a/spec/factories/term_agreements.rb b/spec/factories/term_agreements.rb index 557599e663d..3c4eebd0196 100644 --- a/spec/factories/term_agreements.rb +++ b/spec/factories/term_agreements.rb @@ -3,4 +3,12 @@ FactoryBot.define do term user end + + trait :declined do + accepted false + end + + trait :accepted do + accepted true + end end diff --git a/spec/features/admin/admin_appearance_spec.rb b/spec/features/admin/admin_appearance_spec.rb index d91dcf76191..a5e0ac592b9 100644 --- a/spec/features/admin/admin_appearance_spec.rb +++ b/spec/features/admin/admin_appearance_spec.rb @@ -76,6 +76,26 @@ feature 'Admin Appearance' do expect(page).not_to have_css(header_logo_selector) end + scenario 'Favicon' do + sign_in(create(:admin)) + visit admin_appearances_path + + attach_file(:appearance_favicon, logo_fixture) + click_button 'Save' + + expect(page).to have_css('.appearance-light-logo-preview') + + click_link 'Remove favicon' + + expect(page).not_to have_css('.appearance-light-logo-preview') + + # allowed file types + attach_file(:appearance_favicon, Rails.root.join('spec', 'fixtures', 'sanitized.svg')) + click_button 'Save' + + expect(page).to have_content 'Favicon You are not allowed to upload "svg" files, allowed types: png, ico' + end + def expect_custom_sign_in_appearance(appearance) expect(page).to have_content appearance.title expect(page).to have_content appearance.description diff --git a/spec/features/admin/admin_settings_spec.rb b/spec/features/admin/admin_settings_spec.rb index dc025d82937..e7aca94db66 100644 --- a/spec/features/admin/admin_settings_spec.rb +++ b/spec/features/admin/admin_settings_spec.rb @@ -94,7 +94,7 @@ feature 'Admin updates settings' do accept_terms(admin) page.within('.as-terms') do - check 'Require all users to accept Terms of Service when they access GitLab.' + check 'Require all users to accept Terms of Service and Privacy Policy when they access GitLab.' fill_in 'Terms of Service Agreement', with: 'Be nice!' click_button 'Save changes' end diff --git a/spec/features/admin/admin_uses_repository_checks_spec.rb b/spec/features/admin/admin_uses_repository_checks_spec.rb index 90cf5a53787..7371a494d36 100644 --- a/spec/features/admin/admin_uses_repository_checks_spec.rb +++ b/spec/features/admin/admin_uses_repository_checks_spec.rb @@ -28,7 +28,7 @@ feature 'Admin uses repository checks' do visit_admin_project_page(project) page.within('.alert') do - expect(page.text).to match(/Last repository check \(.* ago\) failed/) + expect(page.text).to match(/Last repository check \(just now\) failed/) end end diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb index e414345ac23..f6e0dee28c6 100644 --- a/spec/features/boards/boards_spec.rb +++ b/spec/features/boards/boards_spec.rb @@ -150,7 +150,7 @@ describe 'Issue Boards', :js do click_button 'Add list' wait_for_requests - find('.dropdown-menu-close').click + find('.js-new-board-list').click page.within(find('.board:nth-child(2)')) do accept_confirm { find('.board-delete').click } diff --git a/spec/features/commits_spec.rb b/spec/features/commits_spec.rb index 62a2ec55b00..87fa3f60826 100644 --- a/spec/features/commits_spec.rb +++ b/spec/features/commits_spec.rb @@ -47,7 +47,7 @@ describe 'Commits' do context 'commit status is Ci Build' do let!(:build) { create(:ci_build, pipeline: pipeline) } - let(:artifacts_file) { fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif') } + let(:artifacts_file) { fixture_file_upload('spec/fixtures/banana_sample.gif', 'image/gif') } context 'when logged as developer' do before do diff --git a/spec/features/ics/dashboard_issues_spec.rb b/spec/features/ics/dashboard_issues_spec.rb index 5d6cd44ad1c..90d02f7e40f 100644 --- a/spec/features/ics/dashboard_issues_spec.rb +++ b/spec/features/ics/dashboard_issues_spec.rb @@ -11,13 +11,25 @@ describe 'Dashboard Issues Calendar Feed' do end context 'when authenticated' do - it 'renders calendar feed' do - sign_in user - visit issues_dashboard_path(:ics) + context 'with no referer' do + it 'renders calendar feed' do + sign_in user + visit issues_dashboard_path(:ics) - expect(response_headers['Content-Type']).to have_content('text/calendar') - expect(response_headers['Content-Disposition']).to have_content('inline') - expect(body).to have_text('BEGIN:VCALENDAR') + expect(response_headers['Content-Type']).to have_content('text/calendar') + expect(body).to have_text('BEGIN:VCALENDAR') + end + end + + context 'with GitLab as the referer' do + it 'renders calendar feed as text/plain' do + sign_in user + page.driver.header('Referer', issues_dashboard_url(host: Settings.gitlab.base_url)) + visit issues_dashboard_path(:ics) + + expect(response_headers['Content-Type']).to have_content('text/plain') + expect(body).to have_text('BEGIN:VCALENDAR') + end end end @@ -28,7 +40,6 @@ describe 'Dashboard Issues Calendar Feed' do visit issues_dashboard_path(:ics, private_token: personal_access_token.token) expect(response_headers['Content-Type']).to have_content('text/calendar') - expect(response_headers['Content-Disposition']).to have_content('inline') expect(body).to have_text('BEGIN:VCALENDAR') end end @@ -38,7 +49,6 @@ describe 'Dashboard Issues Calendar Feed' do visit issues_dashboard_path(:ics, feed_token: user.feed_token) expect(response_headers['Content-Type']).to have_content('text/calendar') - expect(response_headers['Content-Disposition']).to have_content('inline') expect(body).to have_text('BEGIN:VCALENDAR') end end diff --git a/spec/features/ics/group_issues_spec.rb b/spec/features/ics/group_issues_spec.rb index 0a049be2ffe..24de5b4b7c6 100644 --- a/spec/features/ics/group_issues_spec.rb +++ b/spec/features/ics/group_issues_spec.rb @@ -13,13 +13,25 @@ describe 'Group Issues Calendar Feed' do end context 'when authenticated' do - it 'renders calendar feed' do - sign_in user - visit issues_group_path(group, :ics) + context 'with no referer' do + it 'renders calendar feed' do + sign_in user + visit issues_group_path(group, :ics) - expect(response_headers['Content-Type']).to have_content('text/calendar') - expect(response_headers['Content-Disposition']).to have_content('inline') - expect(body).to have_text('BEGIN:VCALENDAR') + expect(response_headers['Content-Type']).to have_content('text/calendar') + expect(body).to have_text('BEGIN:VCALENDAR') + end + end + + context 'with GitLab as the referer' do + it 'renders calendar feed as text/plain' do + sign_in user + page.driver.header('Referer', issues_group_url(group, host: Settings.gitlab.base_url)) + visit issues_group_path(group, :ics) + + expect(response_headers['Content-Type']).to have_content('text/plain') + expect(body).to have_text('BEGIN:VCALENDAR') + end end end @@ -30,7 +42,6 @@ describe 'Group Issues Calendar Feed' do visit issues_group_path(group, :ics, private_token: personal_access_token.token) expect(response_headers['Content-Type']).to have_content('text/calendar') - expect(response_headers['Content-Disposition']).to have_content('inline') expect(body).to have_text('BEGIN:VCALENDAR') end end @@ -40,7 +51,6 @@ describe 'Group Issues Calendar Feed' do visit issues_group_path(group, :ics, feed_token: user.feed_token) expect(response_headers['Content-Type']).to have_content('text/calendar') - expect(response_headers['Content-Disposition']).to have_content('inline') expect(body).to have_text('BEGIN:VCALENDAR') end end diff --git a/spec/features/ics/project_issues_spec.rb b/spec/features/ics/project_issues_spec.rb index b99e9607f1d..2ca3d52a5be 100644 --- a/spec/features/ics/project_issues_spec.rb +++ b/spec/features/ics/project_issues_spec.rb @@ -12,13 +12,25 @@ describe 'Project Issues Calendar Feed' do end context 'when authenticated' do - it 'renders calendar feed' do - sign_in user - visit project_issues_path(project, :ics) + context 'with no referer' do + it 'renders calendar feed' do + sign_in user + visit project_issues_path(project, :ics) - expect(response_headers['Content-Type']).to have_content('text/calendar') - expect(response_headers['Content-Disposition']).to have_content('inline') - expect(body).to have_text('BEGIN:VCALENDAR') + expect(response_headers['Content-Type']).to have_content('text/calendar') + expect(body).to have_text('BEGIN:VCALENDAR') + end + end + + context 'with GitLab as the referer' do + it 'renders calendar feed as text/plain' do + sign_in user + page.driver.header('Referer', project_issues_url(project, host: Settings.gitlab.base_url)) + visit project_issues_path(project, :ics) + + expect(response_headers['Content-Type']).to have_content('text/plain') + expect(body).to have_text('BEGIN:VCALENDAR') + end end end @@ -29,7 +41,6 @@ describe 'Project Issues Calendar Feed' do visit project_issues_path(project, :ics, private_token: personal_access_token.token) expect(response_headers['Content-Type']).to have_content('text/calendar') - expect(response_headers['Content-Disposition']).to have_content('inline') expect(body).to have_text('BEGIN:VCALENDAR') end end @@ -39,7 +50,6 @@ describe 'Project Issues Calendar Feed' do visit project_issues_path(project, :ics, feed_token: user.feed_token) expect(response_headers['Content-Type']).to have_content('text/calendar') - expect(response_headers['Content-Disposition']).to have_content('inline') expect(body).to have_text('BEGIN:VCALENDAR') end end diff --git a/spec/features/issues/filtered_search/dropdown_assignee_spec.rb b/spec/features/issues/filtered_search/dropdown_assignee_spec.rb index cbd0949c192..c8115db9212 100644 --- a/spec/features/issues/filtered_search/dropdown_assignee_spec.rb +++ b/spec/features/issues/filtered_search/dropdown_assignee_spec.rb @@ -31,7 +31,7 @@ describe 'Dropdown assignee', :js do describe 'behavior' do it 'opens when the search bar has assignee:' do - filtered_search.set('assignee:') + input_filtered_search('assignee:', submit: false, extra_space: false) expect(page).to have_css(js_dropdown_assignee, visible: true) end @@ -44,6 +44,7 @@ describe 'Dropdown assignee', :js do it 'should show loading indicator when opened' do slow_requests do + # We aren't using `input_filtered_search` because we want to see the loading indicator filtered_search.set('assignee:') expect(page).to have_css('#js-dropdown-assignee .filter-dropdown-loading', visible: true) @@ -51,19 +52,19 @@ describe 'Dropdown assignee', :js do end it 'should hide loading indicator when loaded' do - filtered_search.set('assignee:') + input_filtered_search('assignee:', submit: false, extra_space: false) expect(find(js_dropdown_assignee)).not_to have_css('.filter-dropdown-loading') end it 'should load all the assignees when opened' do - filtered_search.set('assignee:') + input_filtered_search('assignee:', submit: false, extra_space: false) expect(dropdown_assignee_size).to eq(4) end it 'shows current user at top of dropdown' do - filtered_search.set('assignee:') + input_filtered_search('assignee:', submit: false, extra_space: false) expect(filter_dropdown.first('.filter-dropdown-item')).to have_content(user.name) end @@ -71,7 +72,7 @@ describe 'Dropdown assignee', :js do describe 'filtering' do before do - filtered_search.set('assignee:') + input_filtered_search('assignee:', submit: false, extra_space: false) expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_content(user_john.name) expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_content(user_jacob.name) @@ -79,23 +80,21 @@ describe 'Dropdown assignee', :js do end it 'filters by name' do - filtered_search.send_keys('j') + input_filtered_search('jac', submit: false, extra_space: false) - expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_content(user_john.name) expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_content(user_jacob.name) expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_no_content(user.name) end it 'filters by case insensitive name' do - filtered_search.send_keys('J') + input_filtered_search('JAC', submit: false, extra_space: false) - expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_content(user_john.name) expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_content(user_jacob.name) expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_no_content(user.name) end it 'filters by username with symbol' do - filtered_search.send_keys('@ot') + input_filtered_search('@ott', submit: false, extra_space: false) expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_content(user_jacob.name) expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_content(user.name) @@ -103,7 +102,7 @@ describe 'Dropdown assignee', :js do end it 'filters by case insensitive username with symbol' do - filtered_search.send_keys('@OT') + input_filtered_search('@OTT', submit: false, extra_space: false) expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_content(user_jacob.name) expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_content(user.name) @@ -111,7 +110,9 @@ describe 'Dropdown assignee', :js do end it 'filters by username without symbol' do - filtered_search.send_keys('ot') + input_filtered_search('ott', submit: false, extra_space: false) + + wait_for_requests expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_content(user_jacob.name) expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_content(user.name) @@ -119,7 +120,9 @@ describe 'Dropdown assignee', :js do end it 'filters by case insensitive username without symbol' do - filtered_search.send_keys('OT') + input_filtered_search('OTT', submit: false, extra_space: false) + + wait_for_requests expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_content(user_jacob.name) expect(find("#{js_dropdown_assignee} .filter-dropdown")).to have_content(user.name) @@ -129,7 +132,7 @@ describe 'Dropdown assignee', :js do describe 'selecting from dropdown' do before do - filtered_search.set('assignee:') + input_filtered_search('assignee:', submit: false, extra_space: false) end it 'fills in the assignee username when the assignee has not been filtered' do @@ -143,7 +146,7 @@ describe 'Dropdown assignee', :js do end it 'fills in the assignee username when the assignee has been filtered' do - filtered_search.send_keys('roo') + input_filtered_search('roo', submit: false, extra_space: false) click_assignee(user.name) wait_for_requests @@ -165,7 +168,7 @@ describe 'Dropdown assignee', :js do describe 'selecting from dropdown without Ajax call' do before do Gitlab::Testing::RequestBlockerMiddleware.block_requests! - filtered_search.set('assignee:') + input_filtered_search('assignee:', submit: false, extra_space: false) end after do @@ -183,31 +186,31 @@ describe 'Dropdown assignee', :js do describe 'input has existing content' do it 'opens assignee dropdown with existing search term' do - filtered_search.set('searchTerm assignee:') + input_filtered_search('searchTerm assignee:', submit: false, extra_space: false) expect(page).to have_css(js_dropdown_assignee, visible: true) end it 'opens assignee dropdown with existing author' do - filtered_search.set('author:@user assignee:') + input_filtered_search('author:@user assignee:', submit: false, extra_space: false) expect(page).to have_css(js_dropdown_assignee, visible: true) end it 'opens assignee dropdown with existing label' do - filtered_search.set('label:~bug assignee:') + input_filtered_search('label:~bug assignee:', submit: false, extra_space: false) expect(page).to have_css(js_dropdown_assignee, visible: true) end it 'opens assignee dropdown with existing milestone' do - filtered_search.set('milestone:%v1.0 assignee:') + input_filtered_search('milestone:%v1.0 assignee:', submit: false, extra_space: false) expect(page).to have_css(js_dropdown_assignee, visible: true) end it 'opens assignee dropdown with existing my-reaction' do - filtered_search.set('my-reaction:star assignee:') + input_filtered_search('my-reaction:star assignee:', submit: false, extra_space: false) expect(page).to have_css(js_dropdown_assignee, visible: true) end @@ -215,8 +218,7 @@ describe 'Dropdown assignee', :js do describe 'caching requests' do it 'caches requests after the first load' do - filtered_search.set('assignee') - filtered_search.send_keys(':') + input_filtered_search('assignee:', submit: false, extra_space: false) initial_size = dropdown_assignee_size expect(initial_size).to be > 0 @@ -224,8 +226,7 @@ describe 'Dropdown assignee', :js do new_user = create(:user) project.add_master(new_user) find('.filtered-search-box .clear-search').click - filtered_search.set('assignee') - filtered_search.send_keys(':') + input_filtered_search('assignee:', submit: false, extra_space: false) expect(dropdown_assignee_size).to eq(initial_size) end diff --git a/spec/features/issues/form_spec.rb b/spec/features/issues/form_spec.rb index 4625a50b8d9..2cb3ae08b0e 100644 --- a/spec/features/issues/form_spec.rb +++ b/spec/features/issues/form_spec.rb @@ -143,6 +143,9 @@ describe 'New/edit issue', :js do click_link label.title click_link label2.title end + + find('.js-issuable-form-dropdown.js-label-select').click + page.within '.js-label-select' do expect(page).to have_content label.title end diff --git a/spec/features/issues/user_uses_slash_commands_spec.rb b/spec/features/issues/user_uses_slash_commands_spec.rb index fd0aa6cf3a3..17818beb947 100644 --- a/spec/features/issues/user_uses_slash_commands_spec.rb +++ b/spec/features/issues/user_uses_slash_commands_spec.rb @@ -153,6 +153,42 @@ feature 'Issues > User uses quick actions', :js do end end + describe 'make issue confidential' do + let(:issue) { create(:issue, project: project) } + let(:original_issue) { create(:issue, project: project) } + + context 'when the current user can update issues' do + it 'does not create a note, and marks the issue as confidential' do + add_note("/confidential") + + expect(page).not_to have_content "/confidential" + expect(page).to have_content 'Commands applied' + expect(page).to have_content "made the issue confidential" + + expect(issue.reload).to be_confidential + end + end + + context 'when the current user cannot update the issue' do + let(:guest) { create(:user) } + before do + project.add_guest(guest) + gitlab_sign_out + sign_in(guest) + visit project_issue_path(project, issue) + end + + it 'does not create a note, and does not mark the issue as confidential' do + add_note("/confidential") + + expect(page).not_to have_content 'Commands applied' + expect(page).not_to have_content "made the issue confidential" + + expect(issue.reload).not_to be_confidential + end + end + end + describe 'move the issue to another project' do let(:issue) { create(:issue, project: project) } @@ -190,7 +226,9 @@ feature 'Issues > User uses quick actions', :js do it 'does not move the issue' do add_note("/move #{project_unauthorized.full_path}") - expect(page).not_to have_content 'Commands applied' + wait_for_requests + + expect(page).to have_content 'Commands applied' expect(issue.reload).to be_open end end diff --git a/spec/features/labels_hierarchy_spec.rb b/spec/features/labels_hierarchy_spec.rb index 4700ada1aae..5573148f8bc 100644 --- a/spec/features/labels_hierarchy_spec.rb +++ b/spec/features/labels_hierarchy_spec.rb @@ -34,7 +34,7 @@ feature 'Labels Hierarchy', :js, :nested_groups do wait_for_requests - expect(page).to have_selector('span.badge', text: label.title) + expect(page).to have_selector('.badge', text: label.title) end end @@ -45,7 +45,7 @@ feature 'Labels Hierarchy', :js, :nested_groups do wait_for_requests - expect(page).not_to have_selector('span.badge', text: child_group_label.title) + expect(page).not_to have_selector('.badge', text: child_group_label.title) end end diff --git a/spec/features/markdown/copy_as_gfm_spec.rb b/spec/features/markdown/copy_as_gfm_spec.rb index 4d897f09b57..05228e27963 100644 --- a/spec/features/markdown/copy_as_gfm_spec.rb +++ b/spec/features/markdown/copy_as_gfm_spec.rb @@ -502,6 +502,13 @@ describe 'Copy as GFM', :js do 1. Numbered lists GFM + # list item followed by an HR + <<-GFM.strip_heredoc, + - list item + + ----- + GFM + '# Heading', '## Heading', '### Heading', @@ -515,8 +522,6 @@ describe 'Copy as GFM', :js do '~~Strikethrough~~', - '2^2', - '-----', # table diff --git a/spec/features/markdown/markdown_spec.rb b/spec/features/markdown/markdown_spec.rb index f13d78d24e3..cac8a5068ec 100644 --- a/spec/features/markdown/markdown_spec.rb +++ b/spec/features/markdown/markdown_spec.rb @@ -24,7 +24,7 @@ require 'erb' # # See the MarkdownFeature class for setup details. -describe 'GitLab Markdown' do +describe 'GitLab Markdown', :aggregate_failures do include Capybara::Node::Matchers include MarkupHelper include MarkdownMatchers @@ -44,112 +44,106 @@ describe 'GitLab Markdown' do # Shared behavior that all pipelines should exhibit shared_examples 'all pipelines' do - describe 'Redcarpet extensions' do - it 'does not parse emphasis inside of words' do + it 'includes extensions' do + aggregate_failures 'does not parse emphasis inside of words' do expect(doc.to_html).not_to match('foo<em>bar</em>baz') end - it 'parses table Markdown' do - aggregate_failures do - expect(doc).to have_selector('th:contains("Header")') - expect(doc).to have_selector('th:contains("Row")') - expect(doc).to have_selector('th:contains("Example")') - end + aggregate_failures 'parses table Markdown' do + expect(doc).to have_selector('th:contains("Header")') + expect(doc).to have_selector('th:contains("Row")') + expect(doc).to have_selector('th:contains("Example")') end - it 'allows Markdown in tables' do + aggregate_failures 'allows Markdown in tables' do expect(doc.at_css('td:contains("Baz")').children.to_html) .to eq '<strong>Baz</strong>' end - it 'parses fenced code blocks' do - aggregate_failures do - expect(doc).to have_selector('pre.code.highlight.js-syntax-highlight.c') - expect(doc).to have_selector('pre.code.highlight.js-syntax-highlight.python') - end + aggregate_failures 'parses fenced code blocks' do + expect(doc).to have_selector('pre.code.highlight.js-syntax-highlight.c') + expect(doc).to have_selector('pre.code.highlight.js-syntax-highlight.python') end - it 'parses mermaid code block' do - aggregate_failures do - expect(doc).to have_selector('pre[lang=mermaid] > code.js-render-mermaid') - end + aggregate_failures 'parses mermaid code block' do + expect(doc).to have_selector('pre[lang=mermaid] > code.js-render-mermaid') end - it 'parses strikethroughs' do + aggregate_failures 'parses strikethroughs' do expect(doc).to have_selector(%{del:contains("and this text doesn't")}) end - - it 'parses superscript' do - expect(doc).to have_selector('sup', count: 2) - end end - describe 'SanitizationFilter' do - it 'permits b elements' do + it 'includes SanitizationFilter' do + aggregate_failures 'permits b elements' do expect(doc).to have_selector('b:contains("b tag")') end - it 'permits em elements' do + aggregate_failures 'permits em elements' do expect(doc).to have_selector('em:contains("em tag")') end - it 'permits code elements' do + aggregate_failures 'permits code elements' do expect(doc).to have_selector('code:contains("code tag")') end - it 'permits kbd elements' do + aggregate_failures 'permits kbd elements' do expect(doc).to have_selector('kbd:contains("s")') end - it 'permits strike elements' do + aggregate_failures 'permits strike elements' do expect(doc).to have_selector('strike:contains(Emoji)') end - it 'permits img elements' do + aggregate_failures 'permits img elements' do expect(doc).to have_selector('img[data-src*="smile.png"]') end - it 'permits br elements' do + aggregate_failures 'permits br elements' do expect(doc).to have_selector('br') end - it 'permits hr elements' do + aggregate_failures 'permits hr elements' do expect(doc).to have_selector('hr') end - it 'permits span elements' do + aggregate_failures 'permits span elements' do expect(doc).to have_selector('span:contains("span tag")') end - it 'permits details elements' do + aggregate_failures 'permits details elements' do expect(doc).to have_selector('details:contains("Hiding the details")') end - it 'permits summary elements' do + aggregate_failures 'permits summary elements' do expect(doc).to have_selector('details summary:contains("collapsible")') end - it 'permits style attribute in th elements' do - aggregate_failures do - expect(doc.at_css('th:contains("Header")')['style']).to eq 'text-align: center' - expect(doc.at_css('th:contains("Row")')['style']).to eq 'text-align: right' - expect(doc.at_css('th:contains("Example")')['style']).to eq 'text-align: left' - end + aggregate_failures 'permits align attribute in th elements' do + expect(doc.at_css('th:contains("Header")')['align']).to eq 'center' + expect(doc.at_css('th:contains("Row")')['align']).to eq 'right' + expect(doc.at_css('th:contains("Example")')['align']).to eq 'left' end - it 'permits style attribute in td elements' do - aggregate_failures do - expect(doc.at_css('td:contains("Foo")')['style']).to eq 'text-align: center' - expect(doc.at_css('td:contains("Bar")')['style']).to eq 'text-align: right' - expect(doc.at_css('td:contains("Baz")')['style']).to eq 'text-align: left' - end + aggregate_failures 'permits align attribute in td elements' do + expect(doc.at_css('td:contains("Foo")')['align']).to eq 'center' + expect(doc.at_css('td:contains("Bar")')['align']).to eq 'right' + expect(doc.at_css('td:contains("Baz")')['align']).to eq 'left' + end + + aggregate_failures 'permits superscript elements' do + expect(doc).to have_selector('sup', count: 2) + end + + aggregate_failures 'permits subscript elements' do + expect(doc).to have_selector('sub', count: 3) end - it 'removes `rel` attribute from links' do + aggregate_failures 'removes `rel` attribute from links' do expect(doc).not_to have_selector('a[rel="bookmark"]') end - it "removes `href` from `a` elements if it's fishy" do + aggregate_failures "removes `href` from `a` elements if it's fishy" do expect(doc).not_to have_selector('a[href*="javascript"]') end end @@ -176,26 +170,26 @@ describe 'GitLab Markdown' do end end - describe 'ExternalLinkFilter' do - it 'adds nofollow to external link' do + it 'includes ExternalLinkFilter' do + aggregate_failures 'adds nofollow to external link' do link = doc.at_css('a:contains("Google")') expect(link.attr('rel')).to include('nofollow') end - it 'adds noreferrer to external link' do + aggregate_failures 'adds noreferrer to external link' do link = doc.at_css('a:contains("Google")') expect(link.attr('rel')).to include('noreferrer') end - it 'adds _blank to target attribute for external links' do + aggregate_failures 'adds _blank to target attribute for external links' do link = doc.at_css('a:contains("Google")') expect(link.attr('target')).to match('_blank') end - it 'ignores internal link' do + aggregate_failures 'ignores internal link' do link = doc.at_css('a:contains("GitLab Root")') expect(link.attr('rel')).not_to match 'nofollow' @@ -219,24 +213,24 @@ describe 'GitLab Markdown' do it_behaves_like 'all pipelines' - it 'includes RelativeLinkFilter' do - expect(doc).to parse_relative_links - end + it 'includes custom filters' do + aggregate_failures 'RelativeLinkFilter' do + expect(doc).to parse_relative_links + end - it 'includes EmojiFilter' do - expect(doc).to parse_emoji - end + aggregate_failures 'EmojiFilter' do + expect(doc).to parse_emoji + end - it 'includes TableOfContentsFilter' do - expect(doc).to create_header_links - end + aggregate_failures 'TableOfContentsFilter' do + expect(doc).to create_header_links + end - it 'includes AutolinkFilter' do - expect(doc).to create_autolinks - end + aggregate_failures 'AutolinkFilter' do + expect(doc).to create_autolinks + end - it 'includes all reference filters' do - aggregate_failures do + aggregate_failures 'all reference filters' do expect(doc).to reference_users expect(doc).to reference_issues expect(doc).to reference_merge_requests @@ -246,22 +240,22 @@ describe 'GitLab Markdown' do expect(doc).to reference_labels expect(doc).to reference_milestones end - end - it 'includes TaskListFilter' do - expect(doc).to parse_task_lists - end + aggregate_failures 'TaskListFilter' do + expect(doc).to parse_task_lists + end - it 'includes InlineDiffFilter' do - expect(doc).to parse_inline_diffs - end + aggregate_failures 'InlineDiffFilter' do + expect(doc).to parse_inline_diffs + end - it 'includes VideoLinkFilter' do - expect(doc).to parse_video_links - end + aggregate_failures 'VideoLinkFilter' do + expect(doc).to parse_video_links + end - it 'includes ColorFilter' do - expect(doc).to parse_colors + aggregate_failures 'ColorFilter' do + expect(doc).to parse_colors + end end end @@ -280,24 +274,24 @@ describe 'GitLab Markdown' do it_behaves_like 'all pipelines' - it 'includes RelativeLinkFilter' do - expect(doc).not_to parse_relative_links - end + it 'includes custom filters' do + aggregate_failures 'RelativeLinkFilter' do + expect(doc).not_to parse_relative_links + end - it 'includes EmojiFilter' do - expect(doc).to parse_emoji - end + aggregate_failures 'EmojiFilter' do + expect(doc).to parse_emoji + end - it 'includes TableOfContentsFilter' do - expect(doc).to create_header_links - end + aggregate_failures 'TableOfContentsFilter' do + expect(doc).to create_header_links + end - it 'includes AutolinkFilter' do - expect(doc).to create_autolinks - end + aggregate_failures 'AutolinkFilter' do + expect(doc).to create_autolinks + end - it 'includes all reference filters' do - aggregate_failures do + aggregate_failures 'all reference filters' do expect(doc).to reference_users expect(doc).to reference_issues expect(doc).to reference_merge_requests @@ -307,26 +301,51 @@ describe 'GitLab Markdown' do expect(doc).to reference_labels expect(doc).to reference_milestones end - end - it 'includes TaskListFilter' do - expect(doc).to parse_task_lists - end + aggregate_failures 'TaskListFilter' do + expect(doc).to parse_task_lists + end - it 'includes GollumTagsFilter' do - expect(doc).to parse_gollum_tags - end + aggregate_failures 'GollumTagsFilter' do + expect(doc).to parse_gollum_tags + end + + aggregate_failures 'InlineDiffFilter' do + expect(doc).to parse_inline_diffs + end - it 'includes InlineDiffFilter' do - expect(doc).to parse_inline_diffs + aggregate_failures 'VideoLinkFilter' do + expect(doc).to parse_video_links + end + + aggregate_failures 'ColorFilter' do + expect(doc).to parse_colors + end end + end - it 'includes VideoLinkFilter' do - expect(doc).to parse_video_links + context 'Redcarpet documents' do + before do + allow_any_instance_of(Banzai::Filter::MarkdownFilter).to receive(:engine).and_return('Redcarpet') + @html = markdown(@feat.raw_markdown) end - it 'includes ColorFilter' do - expect(doc).to parse_colors + it 'processes certain elements differently' do + aggregate_failures 'parses superscript' do + expect(doc).to have_selector('sup', count: 3) + end + + aggregate_failures 'permits style attribute in th elements' do + expect(doc.at_css('th:contains("Header")')['style']).to eq 'text-align: center' + expect(doc.at_css('th:contains("Row")')['style']).to eq 'text-align: right' + expect(doc.at_css('th:contains("Example")')['style']).to eq 'text-align: left' + end + + aggregate_failures 'permits style attribute in td elements' do + expect(doc.at_css('td:contains("Foo")')['style']).to eq 'text-align: center' + expect(doc.at_css('td:contains("Bar")')['style']).to eq 'text-align: right' + expect(doc.at_css('td:contains("Baz")')['style']).to eq 'text-align: left' + end end end diff --git a/spec/features/merge_request/maintainer_edits_fork_spec.rb b/spec/features/merge_request/maintainer_edits_fork_spec.rb index a3323da1b1f..1808d0c0a0c 100644 --- a/spec/features/merge_request/maintainer_edits_fork_spec.rb +++ b/spec/features/merge_request/maintainer_edits_fork_spec.rb @@ -14,7 +14,7 @@ describe 'a maintainer edits files on a source-branch of an MR from a fork', :js source_branch: 'fix', target_branch: 'master', author: author, - allow_maintainer_to_push: true) + allow_collaboration: true) end before do diff --git a/spec/features/merge_request/user_allows_a_maintainer_to_push_spec.rb b/spec/features/merge_request/user_allows_commits_from_memebers_who_can_merge_spec.rb index eb41d7de8ed..0af37d76539 100644 --- a/spec/features/merge_request/user_allows_a_maintainer_to_push_spec.rb +++ b/spec/features/merge_request/user_allows_commits_from_memebers_who_can_merge_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe 'create a merge request that allows maintainers to push', :js do +describe 'create a merge request, allowing commits from members who can merge to the target branch', :js do include ProjectForksHelper let(:user) { create(:user) } let(:target_project) { create(:project, :public, :repository) } @@ -21,16 +21,16 @@ describe 'create a merge request that allows maintainers to push', :js do sign_in(user) end - it 'allows setting maintainer push possible' do + it 'allows setting possible' do visit_new_merge_request - check 'Allow edits from maintainers' + check 'Allow commits from members who can merge to the target branch' click_button 'Submit merge request' wait_for_requests - expect(page).to have_content('Allows edits from maintainers') + expect(page).to have_content('Allows commits from members who can merge to the target branch') end it 'shows a message when one of the projects is private' do @@ -57,12 +57,12 @@ describe 'create a merge request that allows maintainers to push', :js do visit_new_merge_request - expect(page).not_to have_content('Allows edits from maintainers') + expect(page).not_to have_content('Allows commits from members who can merge to the target branch') end end - context 'when a maintainer tries to edit the option' do - let(:maintainer) { create(:user) } + context 'when a member who can merge tries to edit the option' do + let(:member) { create(:user) } let(:merge_request) do create(:merge_request, source_project: source_project, @@ -71,15 +71,15 @@ describe 'create a merge request that allows maintainers to push', :js do end before do - target_project.add_master(maintainer) + target_project.add_master(member) - sign_in(maintainer) + sign_in(member) end - it 'it hides the option from maintainers' do + it 'it hides the option from members' do visit edit_project_merge_request_path(target_project, merge_request) - expect(page).not_to have_content('Allows edits from maintainers') + expect(page).not_to have_content('Allows commits from members who can merge to the target branch') end end end diff --git a/spec/features/merge_request/user_creates_image_diff_notes_spec.rb b/spec/features/merge_request/user_creates_image_diff_notes_spec.rb index 7c4fd25bb39..25c408516d1 100644 --- a/spec/features/merge_request/user_creates_image_diff_notes_spec.rb +++ b/spec/features/merge_request/user_creates_image_diff_notes_spec.rb @@ -12,7 +12,7 @@ feature 'Merge request > User creates image diff notes', :js do # Stub helper to return any blob file as image from public app folder. # This is necessary to run this specs since we don't display repo images in capybara. allow_any_instance_of(DiffHelper).to receive(:diff_file_blob_raw_url).and_return('/apple-touch-icon.png') - allow_any_instance_of(DiffHelper).to receive(:diff_file_old_blob_raw_url).and_return('/favicon.ico') + allow_any_instance_of(DiffHelper).to receive(:diff_file_old_blob_raw_url).and_return('/favicon.png') end context 'create commit diff notes' do diff --git a/spec/features/merge_request/user_posts_notes_spec.rb b/spec/features/merge_request/user_posts_notes_spec.rb index b54addce993..3bd9f5e2298 100644 --- a/spec/features/merge_request/user_posts_notes_spec.rb +++ b/spec/features/merge_request/user_posts_notes_spec.rb @@ -139,7 +139,7 @@ describe 'Merge request > User posts notes', :js do page.within("#note_#{note.id}") do is_expected.to have_css('.note_edited_ago') expect(find('.note_edited_ago').text) - .to match(/less than a minute ago/) + .to match(/just now/) end end end diff --git a/spec/features/merge_request/user_sees_mini_pipeline_graph_spec.rb b/spec/features/merge_request/user_sees_mini_pipeline_graph_spec.rb index fd1629746ef..d3104b448e0 100644 --- a/spec/features/merge_request/user_sees_mini_pipeline_graph_spec.rb +++ b/spec/features/merge_request/user_sees_mini_pipeline_graph_spec.rb @@ -23,8 +23,8 @@ describe 'Merge request < User sees mini pipeline graph', :js do end context 'as json' do - let(:artifacts_file1) { fixture_file_upload(Rails.root.join('spec/fixtures/banana_sample.gif'), 'image/gif') } - let(:artifacts_file2) { fixture_file_upload(Rails.root.join('spec/fixtures/dk.png'), 'image/png') } + let(:artifacts_file1) { fixture_file_upload(File.join('spec/fixtures/banana_sample.gif'), 'image/gif') } + let(:artifacts_file2) { fixture_file_upload(File.join('spec/fixtures/dk.png'), 'image/png') } before do create(:ci_build, :success, :trace_artifact, pipeline: pipeline, legacy_artifacts_file: artifacts_file1) diff --git a/spec/features/projects/deploy_keys_spec.rb b/spec/features/projects/deploy_keys_spec.rb index 43a23c42f83..1552a3512dd 100644 --- a/spec/features/projects/deploy_keys_spec.rb +++ b/spec/features/projects/deploy_keys_spec.rb @@ -22,7 +22,8 @@ describe 'Project deploy keys', :js do accept_confirm { find('.ic-remove').click() } - expect(page).not_to have_selector('.fa-spinner', count: 0) + wait_for_requests + expect(page).to have_selector('.deploy-key', count: 0) end end diff --git a/spec/features/projects/diffs/diff_show_spec.rb b/spec/features/projects/diffs/diff_show_spec.rb index c1307ab640f..9bfcb1e816a 100644 --- a/spec/features/projects/diffs/diff_show_spec.rb +++ b/spec/features/projects/diffs/diff_show_spec.rb @@ -166,8 +166,7 @@ feature 'Diff file viewer', :js do context 'expanding the diff' do before do - # We can't use `click_link` because the "link" doesn't have an `href`. - find('a.click-to-expand').click + click_button 'Click to expand it.' wait_for_requests end diff --git a/spec/features/projects/import_export/import_file_spec.rb b/spec/features/projects/import_export/import_file_spec.rb index 60fe30bd898..d0912e645bc 100644 --- a/spec/features/projects/import_export/import_file_spec.rb +++ b/spec/features/projects/import_export/import_file_spec.rb @@ -87,11 +87,13 @@ feature 'Import/Export - project import integration test', :js do def wiki_exists?(project) wiki = ProjectWiki.new(project) - File.exist?(wiki.repository.path_to_repo) && !wiki.repository.empty? + wiki.repository.exists? && !wiki.repository.empty? end def project_hook_exists?(project) - Gitlab::Git::Hook.new('post-receive', project.repository.raw_repository).exists? + Gitlab::GitalyClient::StorageSettings.allow_disk_access do + Gitlab::Git::Hook.new('post-receive', project.repository.raw_repository).exists? + end end def click_import_project_tab diff --git a/spec/features/projects/issues/user_comments_on_issue_spec.rb b/spec/features/projects/issues/user_comments_on_issue_spec.rb index c45fdc7642f..353f487485d 100644 --- a/spec/features/projects/issues/user_comments_on_issue_spec.rb +++ b/spec/features/projects/issues/user_comments_on_issue_spec.rb @@ -31,11 +31,14 @@ describe "User comments on issue", :js do end it "adds comment with code block" do - comment = "```\nCommand [1]: /usr/local/bin/git , see [text](doc/text)\n```" + code_block_content = "Command [1]: /usr/local/bin/git , see [text](doc/text)" + comment = "```\n#{code_block_content}\n```" add_note(comment) - expect(page).to have_content(comment) + wait_for_requests + + expect(page.find('pre code').text).to eq code_block_content end end diff --git a/spec/features/projects/jobs/permissions_spec.rb b/spec/features/projects/jobs/permissions_spec.rb index 31abadf9bd6..e9588daf37d 100644 --- a/spec/features/projects/jobs/permissions_spec.rb +++ b/spec/features/projects/jobs/permissions_spec.rb @@ -88,8 +88,7 @@ describe 'Project Jobs Permissions' do describe 'artifacts page' do context 'when recent job has artifacts available' do before do - artifacts = Rails.root.join('spec/fixtures/ci_build_artifacts.zip') - archive = fixture_file_upload(artifacts, 'application/zip') + archive = fixture_file_upload('spec/fixtures/ci_build_artifacts.zip') job.update_attributes(legacy_artifacts_file: archive) end diff --git a/spec/features/projects/jobs/user_browses_job_spec.rb b/spec/features/projects/jobs/user_browses_job_spec.rb index bff5bbe99af..ce0b38b7239 100644 --- a/spec/features/projects/jobs/user_browses_job_spec.rb +++ b/spec/features/projects/jobs/user_browses_job_spec.rb @@ -32,8 +32,6 @@ describe 'User browses a job', :js do page.within('.erased') do expect(page).to have_content('Job has been erased') end - - expect(build.project.running_or_pending_build_count).to eq(build.project.builds.running_or_pending.count(:all)) end context 'with a failed job' do diff --git a/spec/features/projects/jobs_spec.rb b/spec/features/projects/jobs_spec.rb index 9d1c4cbad8b..d2aaf60e72c 100644 --- a/spec/features/projects/jobs_spec.rb +++ b/spec/features/projects/jobs_spec.rb @@ -11,7 +11,7 @@ feature 'Jobs', :clean_gitlab_redis_shared_state do let(:job2) { create(:ci_build) } let(:artifacts_file) do - fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif') + fixture_file_upload('spec/fixtures/banana_sample.gif', 'image/gif') end before do diff --git a/spec/features/projects/labels/subscription_spec.rb b/spec/features/projects/labels/subscription_spec.rb index 70e8d436dcb..fafd338e448 100644 --- a/spec/features/projects/labels/subscription_spec.rb +++ b/spec/features/projects/labels/subscription_spec.rb @@ -36,7 +36,7 @@ feature 'Labels subscription' do within "#group_label_#{feature.id}" do expect(page).not_to have_button 'Unsubscribe' - click_link_on_dropdown('Group level') + click_link_on_dropdown('Subscribe at group level') expect(page).not_to have_selector('.dropdown-group-label') expect(page).to have_button 'Unsubscribe' @@ -45,7 +45,7 @@ feature 'Labels subscription' do expect(page).to have_selector('.dropdown-group-label') - click_link_on_dropdown('Project level') + click_link_on_dropdown('Subscribe at project level') expect(page).not_to have_selector('.dropdown-group-label') expect(page).to have_button 'Unsubscribe' @@ -68,7 +68,7 @@ feature 'Labels subscription' do find('.dropdown-group-label').click page.within('.dropdown-group-label') do - find('a.js-subscribe-button', text: text).click + find('.js-subscribe-button', text: text).click end end end diff --git a/spec/features/projects/labels/update_prioritization_spec.rb b/spec/features/projects/labels/update_prioritization_spec.rb index ae8b1364ec7..359381c391c 100644 --- a/spec/features/projects/labels/update_prioritization_spec.rb +++ b/spec/features/projects/labels/update_prioritization_spec.rb @@ -102,16 +102,16 @@ feature 'Prioritize labels' do drag_to(selector: '.label-list-item', from_index: 1, to_index: 2) page.within('.prioritized-labels') do - expect(first('li')).to have_content('feature') - expect(page.all('li').last).to have_content('bug') + expect(first('.label-list-item')).to have_content('feature') + expect(page.all('.label-list-item').last).to have_content('bug') end refresh wait_for_requests page.within('.prioritized-labels') do - expect(first('li')).to have_content('feature') - expect(page.all('li').last).to have_content('bug') + expect(first('.label-list-item')).to have_content('feature') + expect(page.all('.label-list-item').last).to have_content('bug') end end diff --git a/spec/features/projects/labels/user_removes_labels_spec.rb b/spec/features/projects/labels/user_removes_labels_spec.rb index f4fda6de465..efa74015c6e 100644 --- a/spec/features/projects/labels/user_removes_labels_spec.rb +++ b/spec/features/projects/labels/user_removes_labels_spec.rb @@ -17,8 +17,9 @@ describe "User removes labels" do end it "removes label" do - page.within(".labels") do + page.within(".other-labels") do page.first(".label-list-item") do + first('.js-label-options-dropdown').click first(".remove-row").click first(:link, "Delete label").click end @@ -36,17 +37,16 @@ describe "User removes labels" do end it "removes all labels" do - page.within(".labels") do - loop do - li = page.first(".label-list-item") - break unless li + loop do + li = page.first(".label-list-item") + break unless li - li.click_link("Delete") - click_link("Delete label") - end - - expect(page).to have_content("Generate a default set of labels").and have_content("New label") + li.find('.js-label-options-dropdown').click + li.click_button("Delete") + click_link("Delete label") end + + expect(page).to have_content("Generate a default set of labels").and have_content("New label") end end end diff --git a/spec/features/projects/pages_spec.rb b/spec/features/projects/pages_spec.rb index bdd49f731c7..a2899ec0f48 100644 --- a/spec/features/projects/pages_spec.rb +++ b/spec/features/projects/pages_spec.rb @@ -314,8 +314,8 @@ feature 'Pages' do project: project, pipeline: pipeline, ref: 'HEAD', - legacy_artifacts_file: fixture_file_upload(Rails.root.join('spec/fixtures/pages.zip')), - legacy_artifacts_metadata: fixture_file_upload(Rails.root.join('spec/fixtures/pages.zip.meta')) + legacy_artifacts_file: fixture_file_upload(File.join('spec/fixtures/pages.zip')), + legacy_artifacts_metadata: fixture_file_upload(File.join('spec/fixtures/pages.zip.meta')) ) end diff --git a/spec/features/projects/pipelines/pipeline_spec.rb b/spec/features/projects/pipelines/pipeline_spec.rb index 35776a5f23b..ecc7cf84138 100644 --- a/spec/features/projects/pipelines/pipeline_spec.rb +++ b/spec/features/projects/pipelines/pipeline_spec.rb @@ -344,6 +344,16 @@ describe 'Pipeline', :js do it 'shows build failure logs' do expect(page).to have_content('4 examples, 1 failure') end + + it 'shows the failure reason' do + expect(page).to have_content('There is an unknown failure, please try again') + end + + it 'shows retry button for failed build' do + page.within(find('.build-failures', match: :first)) do + expect(page).to have_link('Retry') + end + end end context 'when missing build logs' do diff --git a/spec/features/projects/settings/user_manages_group_links_spec.rb b/spec/features/projects/settings/user_manages_group_links_spec.rb index fdf42797091..92ce2ca83c7 100644 --- a/spec/features/projects/settings/user_manages_group_links_spec.rb +++ b/spec/features/projects/settings/user_manages_group_links_spec.rb @@ -30,7 +30,7 @@ describe 'Projects > Settings > User manages group links' do click_link('Share with group') select2(group_market.id, from: '#link_group_id') - select('Master', from: 'link_group_access') + select('Maintainer', from: 'link_group_access') click_button('Share') diff --git a/spec/features/projects/settings/user_manages_project_members_spec.rb b/spec/features/projects/settings/user_manages_project_members_spec.rb index 8af95522165..d3003753ae6 100644 --- a/spec/features/projects/settings/user_manages_project_members_spec.rb +++ b/spec/features/projects/settings/user_manages_project_members_spec.rb @@ -62,7 +62,7 @@ describe 'Projects > Settings > User manages project members' do page.within('.project-members-groups') do expect(page).to have_content('OpenSource') - expect(first('.group_member')).to have_content('Master') + expect(first('.group_member')).to have_content('Maintainer') end end end diff --git a/spec/features/protected_branches_spec.rb b/spec/features/protected_branches_spec.rb index 0c28a853b54..4c0f9971425 100644 --- a/spec/features/protected_branches_spec.rb +++ b/spec/features/protected_branches_spec.rb @@ -72,7 +72,7 @@ feature 'Protected Branches', :js do click_link 'No one' find(".js-allowed-to-push").click wait_for_requests - click_link 'Developers + Masters' + click_link 'Developers + Maintainers' end visit project_protected_branches_path(project) @@ -82,7 +82,7 @@ feature 'Protected Branches', :js do expect(page.find(".dropdown-toggle-text")).to have_content("No one") end page.within(".js-allowed-to-push") do - expect(page.find(".dropdown-toggle-text")).to have_content("Developers + Masters") + expect(page.find(".dropdown-toggle-text")).to have_content("Developers + Maintainers") end end end diff --git a/spec/features/runners_spec.rb b/spec/features/runners_spec.rb index 9ce7d538004..fe0b03a7e00 100644 --- a/spec/features/runners_spec.rb +++ b/spec/features/runners_spec.rb @@ -184,7 +184,7 @@ feature 'Runners' do given(:group) { create :group } - context 'as project and group master' do + context 'as project and group maintainer' do background do group.add_master(user) end @@ -197,13 +197,13 @@ feature 'Runners' do expect(page).to have_content 'This group does not provide any group Runners yet' - expect(page).to have_content 'Group masters can register group runners in the Group CI/CD settings' - expect(page).not_to have_content 'Ask your group master to setup a group Runner' + expect(page).to have_content 'Group maintainers can register group runners in the Group CI/CD settings' + expect(page).not_to have_content 'Ask your group maintainer to setup a group Runner' end end end - context 'as project master' do + context 'as project maintainer' do context 'project without a group' do given(:project) { create :project } @@ -223,8 +223,8 @@ feature 'Runners' do expect(page).to have_content 'This group does not provide any group Runners yet.' - expect(page).not_to have_content 'Group masters can register group runners in the Group CI/CD settings' - expect(page).to have_content 'Ask your group master to setup a group Runner.' + expect(page).not_to have_content 'Group maintainers can register group runners in the Group CI/CD settings' + expect(page).to have_content 'Ask your group maintainer to setup a group Runner.' end end diff --git a/spec/features/tags/master_deletes_tag_spec.rb b/spec/features/tags/master_deletes_tag_spec.rb index c0b4fa52526..9981bfa4609 100644 --- a/spec/features/tags/master_deletes_tag_spec.rb +++ b/spec/features/tags/master_deletes_tag_spec.rb @@ -38,7 +38,7 @@ feature 'Master deletes tag' do context 'when Gitaly operation_user_delete_tag feature is enabled' do before do allow_any_instance_of(Gitlab::GitalyClient::OperationService).to receive(:rm_tag) - .and_raise(Gitlab::Git::HooksService::PreReceiveError, 'Do not delete tags') + .and_raise(Gitlab::Git::PreReceiveError, 'Do not delete tags') end scenario 'shows the error message' do @@ -51,7 +51,7 @@ feature 'Master deletes tag' do context 'when Gitaly operation_user_delete_tag feature is disabled', :skip_gitaly_mock do before do allow_any_instance_of(Gitlab::Git::HooksService).to receive(:execute) - .and_raise(Gitlab::Git::HooksService::PreReceiveError, 'Do not delete tags') + .and_raise(Gitlab::Git::PreReceiveError, 'Do not delete tags') end scenario 'shows the error message' do diff --git a/spec/features/task_lists_spec.rb b/spec/features/task_lists_spec.rb index 2dc3c5e3927..f37d8998045 100644 --- a/spec/features/task_lists_spec.rb +++ b/spec/features/task_lists_spec.rb @@ -36,7 +36,7 @@ feature 'Task Lists' do MARKDOWN end - let(:nested_tasks_markdown) do + let(:nested_tasks_markdown_redcarpet) do <<-EOT.strip_heredoc - [ ] Task a - [x] Task a.1 @@ -49,6 +49,19 @@ feature 'Task Lists' do EOT end + let(:nested_tasks_markdown) do + <<-EOT.strip_heredoc + - [ ] Task a + - [x] Task a.1 + - [ ] Task a.2 + - [ ] Task b + + 1. [ ] Task 1 + 1. [ ] Task 1.1 + 1. [x] Task 1.2 + EOT + end + before do Warden.test_mode! @@ -141,13 +154,11 @@ feature 'Task Lists' do end end - describe 'nested tasks', :js do - let(:issue) { create(:issue, description: nested_tasks_markdown, author: user, project: project) } - + shared_examples 'shared nested tasks' do before do + allow(Banzai::Filter::MarkdownFilter).to receive(:engine).and_return('Redcarpet') visit_issue(project, issue) end - it 'renders' do expect(page).to have_selector('ul.task-list', count: 2) expect(page).to have_selector('li.task-list-item', count: 7) @@ -171,6 +182,30 @@ feature 'Task Lists' do expect(page).to have_content('marked the task Task 1.1 as complete') end end + + describe 'nested tasks', :js do + context 'with Redcarpet' do + let(:issue) { create(:issue, description: nested_tasks_markdown_redcarpet, author: user, project: project) } + + before do + allow_any_instance_of(Banzai::Filter::MarkdownFilter).to receive(:engine).and_return('Redcarpet') + visit_issue(project, issue) + end + + it_behaves_like 'shared nested tasks' + end + + context 'with CommonMark' do + let(:issue) { create(:issue, description: nested_tasks_markdown, author: user, project: project) } + + before do + allow_any_instance_of(Banzai::Filter::MarkdownFilter).to receive(:engine).and_return('CommonMark') + visit_issue(project, issue) + end + + it_behaves_like 'shared nested tasks' + end + end end describe 'for Notes' do diff --git a/spec/features/users/signup_spec.rb b/spec/features/users/signup_spec.rb index b5bd5c505f2..b51ca5d130b 100644 --- a/spec/features/users/signup_spec.rb +++ b/spec/features/users/signup_spec.rb @@ -140,7 +140,7 @@ describe 'Signup' do enforce_terms end - it 'asks the user to accept terms before going to the dashboard' do + it 'requires the user to check the checkbox' do visit root_path fill_in 'new_user_name', with: new_user.name @@ -148,11 +148,24 @@ describe 'Signup' do fill_in 'new_user_email', with: new_user.email fill_in 'new_user_email_confirmation', with: new_user.email fill_in 'new_user_password', with: new_user.password - click_button "Register" - expect_to_be_on_terms_page + click_button 'Register' + + expect(current_path).to eq new_user_session_path + expect(page).to have_content(/you must accept our terms of service/i) + end + + it 'asks the user to accept terms before going to the dashboard' do + visit root_path + + fill_in 'new_user_name', with: new_user.name + fill_in 'new_user_username', with: new_user.username + fill_in 'new_user_email', with: new_user.email + fill_in 'new_user_email_confirmation', with: new_user.email + fill_in 'new_user_password', with: new_user.password + check :terms_opt_in - click_button 'Accept terms' + click_button "Register" expect(current_path).to eq dashboard_projects_path end diff --git a/spec/features/users/terms_spec.rb b/spec/features/users/terms_spec.rb index 1efa5cd5490..5b2e7605c4d 100644 --- a/spec/features/users/terms_spec.rb +++ b/spec/features/users/terms_spec.rb @@ -3,12 +3,10 @@ require 'spec_helper' describe 'Users > Terms' do include TermsHelper - let(:user) { create(:user) } let!(:term) { create(:term, terms: 'By accepting, you promise to be nice!') } before do stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false') - sign_in(user) end it 'shows the terms' do @@ -17,86 +15,119 @@ describe 'Users > Terms' do expect(page).to have_content('By accepting, you promise to be nice!') end - context 'declining the terms' do - it 'returns the user to the app' do - visit terms_path + it 'does not show buttons to accept, decline or sign out', :aggregate_failures do + visit terms_path + + expect(page).not_to have_css('.footer-block') + expect(page).not_to have_content('Accept terms') + expect(page).not_to have_content('Decline and sign out') + expect(page).not_to have_content('Continue') + end - click_button 'Decline and sign out' + context 'when signed in' do + let(:user) { create(:user) } - expect(page).not_to have_content(term.terms) - expect(user.reload.terms_accepted?).to be(false) + before do + sign_in(user) end - end - context 'accepting the terms' do - it 'returns the user to the app' do - visit terms_path + context 'declining the terms' do + it 'returns the user to the app' do + visit terms_path - click_button 'Accept terms' + click_button 'Decline and sign out' - expect(page).not_to have_content(term.terms) - expect(user.reload.terms_accepted?).to be(true) + expect(page).not_to have_content(term.terms) + expect(user.reload.terms_accepted?).to be(false) + end end - end - context 'terms were enforced while session is active', :js do - let(:project) { create(:project) } + context 'accepting the terms' do + it 'returns the user to the app' do + visit terms_path - before do - project.add_developer(user) + click_button 'Accept terms' + + expect(page).not_to have_content(term.terms) + expect(user.reload.terms_accepted?).to be(true) + end end - it 'redirects to terms and back to where the user was going' do - visit project_path(project) + context 'when the user has already accepted the terms' do + before do + accept_terms(user) + end + + it 'allows the user to continue to the app' do + visit terms_path + + expect(page).to have_content "You have already accepted the Terms of Service as #{user.to_reference}" - enforce_terms + click_link 'Continue' - within('.nav-sidebar') do - click_link 'Issues' + expect(current_path).to eq(root_path) end + end + + context 'terms were enforced while session is active', :js do + let(:project) { create(:project) } - expect_to_be_on_terms_page + before do + project.add_developer(user) + end - click_button('Accept terms') + it 'redirects to terms and back to where the user was going' do + visit project_path(project) - expect(current_path).to eq(project_issues_path(project)) - end + enforce_terms - # Disabled until https://gitlab.com/gitlab-org/gitlab-ce/issues/37162 is solved properly - xit 'redirects back to the page the user was trying to save' do - visit new_project_issue_path(project) + within('.nav-sidebar') do + click_link 'Issues' + end - fill_in :issue_title, with: 'Hello world, a new issue' - fill_in :issue_description, with: "We don't want to lose what the user typed" + expect_to_be_on_terms_page - enforce_terms + click_button('Accept terms') - click_button 'Submit issue' + expect(current_path).to eq(project_issues_path(project)) + end - expect(current_path).to eq(terms_path) + # Disabled until https://gitlab.com/gitlab-org/gitlab-ce/issues/37162 is solved properly + xit 'redirects back to the page the user was trying to save' do + visit new_project_issue_path(project) - click_button('Accept terms') + fill_in :issue_title, with: 'Hello world, a new issue' + fill_in :issue_description, with: "We don't want to lose what the user typed" - expect(current_path).to eq(new_project_issue_path(project)) - expect(find_field('issue_title').value).to eq('Hello world, a new issue') - expect(find_field('issue_description').value).to eq("We don't want to lose what the user typed") - end - end + enforce_terms - context 'when the terms are enforced' do - before do - enforce_terms + click_button 'Submit issue' + + expect(current_path).to eq(terms_path) + + click_button('Accept terms') + + expect(current_path).to eq(new_project_issue_path(project)) + expect(find_field('issue_title').value).to eq('Hello world, a new issue') + expect(find_field('issue_description').value).to eq("We don't want to lose what the user typed") + end end - context 'signing out', :js do - it 'allows the user to sign out without a response' do - visit terms_path + context 'when the terms are enforced' do + before do + enforce_terms + end + + context 'signing out', :js do + it 'allows the user to sign out without a response' do + visit terms_path - find('.header-user-dropdown-toggle').click - click_link('Sign out') + find('.header-user-dropdown-toggle').click + click_link('Sign out') - expect(page).to have_content('Sign in') - expect(page).to have_content('Register') + expect(page).to have_content('Sign in') + expect(page).to have_content('Register') + end end end end diff --git a/spec/finders/group_members_finder_spec.rb b/spec/finders/group_members_finder_spec.rb index 9f285e28535..63e15b365a4 100644 --- a/spec/finders/group_members_finder_spec.rb +++ b/spec/finders/group_members_finder_spec.rb @@ -29,4 +29,16 @@ describe GroupMembersFinder, '#execute' do expect(result.to_a).to match_array([member1, member3, member4]) end + + it 'returns members for descendant groups if requested', :nested_groups do + member1 = group.add_master(user2) + member2 = group.add_master(user1) + nested_group.add_master(user2) + member3 = nested_group.add_master(user3) + member4 = nested_group.add_master(user4) + + result = described_class.new(group).execute(include_descendants: true) + + expect(result.to_a).to match_array([member1, member2, member3, member4]) + end end diff --git a/spec/finders/members_finder_spec.rb b/spec/finders/members_finder_spec.rb index 7bb1f45322e..2fc5299b0f4 100644 --- a/spec/finders/members_finder_spec.rb +++ b/spec/finders/members_finder_spec.rb @@ -19,4 +19,16 @@ describe MembersFinder, '#execute' do expect(result.to_a).to match_array([member1, member2, member3]) end + + it 'includes nested group members if asked', :nested_groups do + project = create(:project, namespace: group) + nested_group.request_access(user1) + member1 = group.add_master(user2) + member2 = nested_group.add_master(user3) + member3 = project.add_master(user4) + + result = described_class.new(project, user2).execute(include_descendants: true) + + expect(result.to_a).to match_array([member1, member2, member3]) + end end diff --git a/spec/fixtures/api/schemas/entities/merge_request_basic.json b/spec/fixtures/api/schemas/entities/merge_request_basic.json index 46031961cca..cf257ac00de 100644 --- a/spec/fixtures/api/schemas/entities/merge_request_basic.json +++ b/spec/fixtures/api/schemas/entities/merge_request_basic.json @@ -13,7 +13,22 @@ "assignee_id": { "type": ["integer", "null"] }, "subscribed": { "type": ["boolean", "null"] }, "participants": { "type": "array" }, - "allow_maintainer_to_push": { "type": "boolean"} + "allow_collaboration": { "type": "boolean"}, + "allow_maintainer_to_push": { "type": "boolean"}, + "assignee": { + "oneOf": [ + { "type": "null" }, + { "$ref": "user.json" } + ] + }, + "milestone": { + "type": [ "object", "null" ] + }, + "labels": { + "type": [ "array", "null" ] + }, + "task_status": { "type": "string" }, + "task_status_short": { "type": "string" } }, "additionalProperties": false } diff --git a/spec/fixtures/api/schemas/entities/merge_request_widget.json b/spec/fixtures/api/schemas/entities/merge_request_widget.json index 7be8c9e3e67..ee5588fa6c6 100644 --- a/spec/fixtures/api/schemas/entities/merge_request_widget.json +++ b/spec/fixtures/api/schemas/entities/merge_request_widget.json @@ -31,7 +31,7 @@ "source_project_id": { "type": "integer" }, "target_branch": { "type": "string" }, "target_project_id": { "type": "integer" }, - "allow_maintainer_to_push": { "type": "boolean"}, + "allow_collaboration": { "type": "boolean"}, "metrics": { "oneOf": [ { "type": "null" }, diff --git a/spec/fixtures/api/schemas/list.json b/spec/fixtures/api/schemas/list.json index 05922df6b81..b76ec115293 100644 --- a/spec/fixtures/api/schemas/list.json +++ b/spec/fixtures/api/schemas/list.json @@ -37,5 +37,5 @@ "title": { "type": "string" }, "position": { "type": ["integer", "null"] } }, - "additionalProperties": false + "additionalProperties": true } diff --git a/spec/fixtures/api/schemas/public_api/v4/commit/with_stats.json b/spec/fixtures/api/schemas/public_api/v4/commit/with_stats.json new file mode 100644 index 00000000000..3b5dd547e69 --- /dev/null +++ b/spec/fixtures/api/schemas/public_api/v4/commit/with_stats.json @@ -0,0 +1,14 @@ +{ + "type": "object", + "allOf": [ + { "$ref": "basic.json" }, + { + "required" : [ + "stats" + ], + "properties": { + "stats": { "$ref": "../commit_stats.json" } + } + } + ] +} diff --git a/spec/fixtures/api/schemas/public_api/v4/commits_with_stats.json b/spec/fixtures/api/schemas/public_api/v4/commits_with_stats.json new file mode 100644 index 00000000000..23511123ce4 --- /dev/null +++ b/spec/fixtures/api/schemas/public_api/v4/commits_with_stats.json @@ -0,0 +1,4 @@ +{ + "type": "array", + "items": { "$ref": "commit/with_stats.json" } +} diff --git a/spec/fixtures/api/schemas/public_api/v4/merge_requests.json b/spec/fixtures/api/schemas/public_api/v4/merge_requests.json index f97461ce9cc..f7adc4e0b91 100644 --- a/spec/fixtures/api/schemas/public_api/v4/merge_requests.json +++ b/spec/fixtures/api/schemas/public_api/v4/merge_requests.json @@ -82,6 +82,7 @@ "human_time_estimate": { "type": ["string", "null"] }, "human_total_time_spent": { "type": ["string", "null"] } }, + "allow_collaboration": { "type": ["boolean", "null"] }, "allow_maintainer_to_push": { "type": ["boolean", "null"] } }, "required": [ diff --git a/spec/fixtures/api/schemas/public_api/v4/milestones.json b/spec/fixtures/api/schemas/public_api/v4/milestones.json index c3c42b6ee60..448e97d6c85 100644 --- a/spec/fixtures/api/schemas/public_api/v4/milestones.json +++ b/spec/fixtures/api/schemas/public_api/v4/milestones.json @@ -13,7 +13,8 @@ "created_at": { "type": "date" }, "updated_at": { "type": "date" }, "start_date": { "type": "date" }, - "due_date": { "type": "date" } + "due_date": { "type": "date" }, + "web_url": { "type": "string" } }, "required": [ "id", "iid", "title", "description", "state", diff --git a/spec/fixtures/api/schemas/public_api/v4/snippets.json b/spec/fixtures/api/schemas/public_api/v4/snippets.json index e37e9704649..d13d703e063 100644 --- a/spec/fixtures/api/schemas/public_api/v4/snippets.json +++ b/spec/fixtures/api/schemas/public_api/v4/snippets.json @@ -8,6 +8,7 @@ "title": { "type": "string" }, "file_name": { "type": ["string", "null"] }, "description": { "type": ["string", "null"] }, + "visibility": { "type": "string" }, "web_url": { "type": "string" }, "created_at": { "type": "date" }, "updated_at": { "type": "date" }, diff --git a/spec/fixtures/markdown.md.erb b/spec/fixtures/markdown.md.erb index da32a46675f..e5d01c3bd03 100644 --- a/spec/fixtures/markdown.md.erb +++ b/spec/fixtures/markdown.md.erb @@ -43,8 +43,14 @@ This text says this, ~~and this text doesn't~~. ### Superscript -This is my 1^(st) time using superscript in Markdown. Now this is my -2^(nd). +This is my 1<sup>(st)</sup> time using superscript in Markdown. Now this is my +2<sup>(nd)</sup>. + +Redcarpet supports this superscript syntax ( x^2 ). + +### Subscript + +This (C<sub>6</sub>H<sub>12</sub>O<sub>6</sub>) is an example of subscripts in Markdown. ### Next step diff --git a/spec/graphql/gitlab_schema_spec.rb b/spec/graphql/gitlab_schema_spec.rb new file mode 100644 index 00000000000..b892f6b44ed --- /dev/null +++ b/spec/graphql/gitlab_schema_spec.rb @@ -0,0 +1,33 @@ +require 'spec_helper' + +describe GitlabSchema do + it 'uses batch loading' do + expect(field_instrumenters).to include(BatchLoader::GraphQL) + end + + it 'enables the preload instrumenter' do + expect(field_instrumenters).to include(BatchLoader::GraphQL) + end + + it 'enables the authorization instrumenter' do + expect(field_instrumenters).to include(instance_of(::Gitlab::Graphql::Authorize::Instrumentation)) + end + + it 'enables using presenters' do + expect(field_instrumenters).to include(instance_of(::Gitlab::Graphql::Present::Instrumentation)) + end + + it 'has the base mutation' do + pending('Adding an empty mutation breaks the documentation explorer') + + expect(described_class.mutation).to eq(::Types::MutationType.to_graphql) + end + + it 'has the base query' do + expect(described_class.query).to eq(::Types::QueryType.to_graphql) + end + + def field_instrumenters + described_class.instrumenters[:field] + end +end diff --git a/spec/graphql/resolvers/merge_request_resolver_spec.rb b/spec/graphql/resolvers/merge_request_resolver_spec.rb new file mode 100644 index 00000000000..73993b3a039 --- /dev/null +++ b/spec/graphql/resolvers/merge_request_resolver_spec.rb @@ -0,0 +1,45 @@ +require 'spec_helper' + +describe Resolvers::MergeRequestResolver do + include GraphqlHelpers + + set(:project) { create(:project, :repository) } + set(:merge_request_1) { create(:merge_request, :simple, source_project: project, target_project: project) } + set(:merge_request_2) { create(:merge_request, :rebased, source_project: project, target_project: project) } + + set(:other_project) { create(:project, :repository) } + set(:other_merge_request) { create(:merge_request, source_project: other_project, target_project: other_project) } + + let(:iid_1) { merge_request_1.iid } + let(:iid_2) { merge_request_2.iid } + + let(:other_iid) { other_merge_request.iid } + + describe '#resolve' do + it 'batch-resolves merge requests by target project full path and IID' do + result = batch(max_queries: 2) do + [resolve_mr(project, iid_1), resolve_mr(project, iid_2)] + end + + expect(result).to contain_exactly(merge_request_1, merge_request_2) + end + + it 'can batch-resolve merge requests from different projects' do + result = batch(max_queries: 3) do + [resolve_mr(project, iid_1), resolve_mr(project, iid_2), resolve_mr(other_project, other_iid)] + end + + expect(result).to contain_exactly(merge_request_1, merge_request_2, other_merge_request) + end + + it 'resolves an unknown iid to nil' do + result = batch { resolve_mr(project, -1) } + + expect(result).to be_nil + end + end + + def resolve_mr(project, iid) + resolve(described_class, obj: project, args: { iid: iid }) + end +end diff --git a/spec/graphql/resolvers/project_resolver_spec.rb b/spec/graphql/resolvers/project_resolver_spec.rb new file mode 100644 index 00000000000..d4990c6492c --- /dev/null +++ b/spec/graphql/resolvers/project_resolver_spec.rb @@ -0,0 +1,32 @@ +require 'spec_helper' + +describe Resolvers::ProjectResolver do + include GraphqlHelpers + + set(:project1) { create(:project) } + set(:project2) { create(:project) } + + set(:other_project) { create(:project) } + + describe '#resolve' do + it 'batch-resolves projects by full path' do + paths = [project1.full_path, project2.full_path] + + result = batch(max_queries: 1) do + paths.map { |path| resolve_project(path) } + end + + expect(result).to contain_exactly(project1, project2) + end + + it 'resolves an unknown full_path to nil' do + result = batch { resolve_project('unknown/project') } + + expect(result).to be_nil + end + end + + def resolve_project(full_path) + resolve(described_class, args: { full_path: full_path }) + end +end diff --git a/spec/graphql/types/project_type_spec.rb b/spec/graphql/types/project_type_spec.rb new file mode 100644 index 00000000000..b4eeca2e3f1 --- /dev/null +++ b/spec/graphql/types/project_type_spec.rb @@ -0,0 +1,14 @@ +require 'spec_helper' + +describe GitlabSchema.types['Project'] do + it { expect(described_class.graphql_name).to eq('Project') } + + describe 'nested merge request' do + it { expect(described_class).to have_graphql_field(:merge_request) } + + it 'authorizes the merge request' do + expect(described_class.fields['mergeRequest']) + .to require_graphql_authorizations(:read_merge_request) + end + end +end diff --git a/spec/graphql/types/query_type_spec.rb b/spec/graphql/types/query_type_spec.rb new file mode 100644 index 00000000000..e1df6f9811d --- /dev/null +++ b/spec/graphql/types/query_type_spec.rb @@ -0,0 +1,23 @@ +require 'spec_helper' + +describe GitlabSchema.types['Query'] do + it 'is called Query' do + expect(described_class.graphql_name).to eq('Query') + end + + it { is_expected.to have_graphql_fields(:project, :echo) } + + describe 'project field' do + subject { described_class.fields['project'] } + + it 'finds projects by full path' do + is_expected.to have_graphql_arguments(:full_path) + is_expected.to have_graphql_type(Types::ProjectType) + is_expected.to have_graphql_resolver(Resolvers::ProjectResolver) + end + + it 'authorizes with read_project' do + is_expected.to require_graphql_authorizations(:read_project) + end + end +end diff --git a/spec/graphql/types/time_type_spec.rb b/spec/graphql/types/time_type_spec.rb new file mode 100644 index 00000000000..4196d9d27d4 --- /dev/null +++ b/spec/graphql/types/time_type_spec.rb @@ -0,0 +1,16 @@ +require 'spec_helper' + +describe GitlabSchema.types['Time'] do + let(:iso) { "2018-06-04T15:23:50+02:00" } + let(:time) { Time.parse(iso) } + + it { expect(described_class.graphql_name).to eq('Time') } + + it 'coerces Time object into ISO 8601' do + expect(described_class.coerce_isolated_result(time)).to eq(iso) + end + + it 'coerces an ISO-time into Time object' do + expect(described_class.coerce_isolated_input(iso)).to eq(time) + end +end diff --git a/spec/helpers/emails_helper_spec.rb b/spec/helpers/emails_helper_spec.rb index 2390c1f3e5d..139387e0b24 100644 --- a/spec/helpers/emails_helper_spec.rb +++ b/spec/helpers/emails_helper_spec.rb @@ -47,9 +47,7 @@ describe EmailsHelper do describe '#header_logo' do context 'there is a brand item with a logo' do it 'returns the brand header logo' do - appearance = create :appearance, header_logo: fixture_file_upload( - Rails.root.join('spec/fixtures/dk.png') - ) + appearance = create :appearance, header_logo: fixture_file_upload('spec/fixtures/dk.png') expect(header_logo).to eq( %{<img style="height: 50px" src="/uploads/-/system/appearance/header_logo/#{appearance.id}/dk.png" alt="Dk" />} diff --git a/spec/helpers/groups_helper_spec.rb b/spec/helpers/groups_helper_spec.rb index b48c252acd3..6c94bd4e504 100644 --- a/spec/helpers/groups_helper_spec.rb +++ b/spec/helpers/groups_helper_spec.rb @@ -4,9 +4,9 @@ describe GroupsHelper do include ApplicationHelper describe 'group_icon' do - avatar_file_path = File.join(Rails.root, 'spec', 'fixtures', 'banana_sample.gif') - it 'returns an url for the avatar' do + avatar_file_path = File.join('spec', 'fixtures', 'banana_sample.gif') + group = create(:group) group.avatar = fixture_file_upload(avatar_file_path) group.save! @@ -17,9 +17,9 @@ describe GroupsHelper do end describe 'group_icon_url' do - avatar_file_path = File.join(Rails.root, 'spec', 'fixtures', 'banana_sample.gif') - it 'returns an url for the avatar' do + avatar_file_path = File.join('spec', 'fixtures', 'banana_sample.gif') + group = create(:group) group.avatar = fixture_file_upload(avatar_file_path) group.save! diff --git a/spec/helpers/markup_helper_spec.rb b/spec/helpers/markup_helper_spec.rb index c0dc9293397..1a720aae55c 100644 --- a/spec/helpers/markup_helper_spec.rb +++ b/spec/helpers/markup_helper_spec.rb @@ -298,7 +298,7 @@ describe MarkupHelper do it 'preserves code color scheme' do object = create_object("```ruby\ndef test\n 'hello world'\nend\n```") - expected = "\n<pre class=\"code highlight js-syntax-highlight ruby\">" \ + expected = "<pre class=\"code highlight js-syntax-highlight ruby\">" \ "<code><span class=\"line\"><span class=\"k\">def</span> <span class=\"nf\">test</span>...</span>\n" \ "</code></pre>" diff --git a/spec/helpers/page_layout_helper_spec.rb b/spec/helpers/page_layout_helper_spec.rb index b77114a8152..cf98eed27f1 100644 --- a/spec/helpers/page_layout_helper_spec.rb +++ b/spec/helpers/page_layout_helper_spec.rb @@ -40,23 +40,6 @@ describe PageLayoutHelper do end end - describe 'favicon' do - it 'defaults to favicon.ico' do - allow(Rails).to receive(:env).and_return(ActiveSupport::StringInquirer.new('production')) - expect(helper.favicon).to eq 'favicon.ico' - end - - it 'has blue favicon for development' do - allow(Rails).to receive(:env).and_return(ActiveSupport::StringInquirer.new('development')) - expect(helper.favicon).to eq 'favicon-blue.ico' - end - - it 'has yellow favicon for canary' do - stub_env('CANARY', 'true') - expect(helper.favicon).to eq 'favicon-yellow.ico' - end - end - describe 'page_image' do it 'defaults to the GitLab logo' do expect(helper.page_image).to match_asset_path 'assets/gitlab_logo.png' diff --git a/spec/helpers/preferences_helper_spec.rb b/spec/helpers/preferences_helper_spec.rb index c9d2ec8a4ae..363ebc88afd 100644 --- a/spec/helpers/preferences_helper_spec.rb +++ b/spec/helpers/preferences_helper_spec.rb @@ -33,7 +33,7 @@ describe PreferencesHelper do it "returns user's theme's css_class" do stub_user(theme_id: 3) - expect(helper.user_application_theme).to eq 'ui_light' + expect(helper.user_application_theme).to eq 'ui-light' end it 'returns the default when id is invalid' do @@ -41,7 +41,7 @@ describe PreferencesHelper do allow(Gitlab.config.gitlab).to receive(:default_theme).and_return(1) - expect(helper.user_application_theme).to eq 'ui_indigo' + expect(helper.user_application_theme).to eq 'ui-indigo' end end diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb index f8877b6d1aa..5cf9e9e8f12 100644 --- a/spec/helpers/projects_helper_spec.rb +++ b/spec/helpers/projects_helper_spec.rb @@ -5,9 +5,9 @@ describe ProjectsHelper do describe "#project_status_css_class" do it "returns appropriate class" do - expect(project_status_css_class("started")).to eq("active") - expect(project_status_css_class("failed")).to eq("danger") - expect(project_status_css_class("finished")).to eq("success") + expect(project_status_css_class("started")).to eq("table-active") + expect(project_status_css_class("failed")).to eq("table-danger") + expect(project_status_css_class("finished")).to eq("table-success") end end @@ -90,6 +90,10 @@ describe ProjectsHelper do expect(helper.project_list_cache_key(project)).to include(project.cache_key) end + it "includes the last activity date" do + expect(helper.project_list_cache_key(project)).to include(project.last_activity_date) + end + it "includes the controller name" do expect(helper.controller).to receive(:controller_name).and_return("testcontroller") @@ -276,7 +280,11 @@ describe ProjectsHelper do describe '#sanitizerepo_repo_path' do let(:project) { create(:project, :repository) } - let(:storage_path) { Gitlab.config.repositories.storages.default.legacy_disk_path } + let(:storage_path) do + Gitlab::GitalyClient::StorageSettings.allow_disk_access do + Gitlab.config.repositories.storages.default.legacy_disk_path + end + end before do allow(Settings.shared).to receive(:[]).with('path').and_return('/base/repo/export/path') @@ -435,4 +443,46 @@ describe ProjectsHelper do expect(helper.send(:git_user_name)).to eq('John \"A\" Doe53') end end + + describe 'show_xcode_link' do + let!(:project) { create(:project) } + let(:mac_ua) { 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.139 Safari/537.36' } + let(:ios_ua) { 'Mozilla/5.0 (iPad; CPU OS 5_1_1 like Mac OS X) AppleWebKit/534.46 (KHTML, like Gecko) Version/5.1 Mobile/9B206 Safari/7534.48.3' } + + context 'when the repository is xcode compatible' do + before do + allow(project.repository).to receive(:xcode_project?).and_return(true) + end + + it 'returns false if the visitor is not using macos' do + allow(helper).to receive(:browser).and_return(Browser.new(ios_ua)) + + expect(helper.show_xcode_link?(project)).to eq(false) + end + + it 'returns true if the visitor is using macos' do + allow(helper).to receive(:browser).and_return(Browser.new(mac_ua)) + + expect(helper.show_xcode_link?(project)).to eq(true) + end + end + + context 'when the repository is not xcode compatible' do + before do + allow(project.repository).to receive(:xcode_project?).and_return(false) + end + + it 'returns false if the visitor is not using macos' do + allow(helper).to receive(:browser).and_return(Browser.new(ios_ua)) + + expect(helper.show_xcode_link?(project)).to eq(false) + end + + it 'returns false if the visitor is using macos' do + allow(helper).to receive(:browser).and_return(Browser.new(mac_ua)) + + expect(helper.show_xcode_link?(project)).to eq(false) + end + end + end end diff --git a/spec/helpers/storage_helper_spec.rb b/spec/helpers/storage_helper_spec.rb index 4627a1e1872..c580b78c908 100644 --- a/spec/helpers/storage_helper_spec.rb +++ b/spec/helpers/storage_helper_spec.rb @@ -1,21 +1,25 @@ -require 'spec_helper' +require "spec_helper" describe StorageHelper do - describe '#storage_counter' do - it 'formats bytes to one decimal place' do - expect(helper.storage_counter(1.23.megabytes)).to eq '1.2 MB' + describe "#storage_counter" do + it "formats bytes to one decimal place" do + expect(helper.storage_counter(1.23.megabytes)).to eq("1.2 MB") end - it 'does not add decimals for sizes < 1 MB' do - expect(helper.storage_counter(23.5.kilobytes)).to eq '24 KB' + it "does not add decimals for sizes < 1 MB" do + expect(helper.storage_counter(23.5.kilobytes)).to eq("24 KB") end - it 'does not add decimals for zeroes' do - expect(helper.storage_counter(2.megabytes)).to eq '2 MB' + it "does not add decimals for zeroes" do + expect(helper.storage_counter(2.megabytes)).to eq("2 MB") end - it 'uses commas as thousands separator' do - expect(helper.storage_counter(100_000_000_000_000_000)).to eq '90,949.5 TB' + it "uses commas as thousands separator" do + if Gitlab.rails5? + expect(helper.storage_counter(100_000_000_000_000_000_000_000)).to eq("86,736.2 EB") + else + expect(helper.storage_counter(100_000_000_000_000_000)).to eq("90,949.5 TB") + end end end end diff --git a/spec/initializers/artifacts_direct_upload_support_spec.rb b/spec/initializers/artifacts_direct_upload_support_spec.rb deleted file mode 100644 index bfb71da3388..00000000000 --- a/spec/initializers/artifacts_direct_upload_support_spec.rb +++ /dev/null @@ -1,71 +0,0 @@ -require 'spec_helper' - -describe 'Artifacts direct upload support' do - subject do - load Rails.root.join('config/initializers/artifacts_direct_upload_support.rb') - end - - let(:connection) do - { provider: provider } - end - - before do - stub_artifacts_setting( - object_store: { - enabled: enabled, - direct_upload: direct_upload, - connection: connection - }) - end - - context 'when object storage is enabled' do - let(:enabled) { true } - - context 'when direct upload is enabled' do - let(:direct_upload) { true } - - context 'when provider is Google' do - let(:provider) { 'Google' } - - it 'succeeds' do - expect { subject }.not_to raise_error - end - end - - context 'when connection is empty' do - let(:connection) { nil } - - it 'raises an error' do - expect { subject }.to raise_error /object storage provider when 'direct_upload' of artifacts is used/ - end - end - - context 'when other provider is used' do - let(:provider) { 'AWS' } - - it 'raises an error' do - expect { subject }.to raise_error /object storage provider when 'direct_upload' of artifacts is used/ - end - end - end - - context 'when direct upload is disabled' do - let(:direct_upload) { false } - let(:provider) { 'AWS' } - - it 'succeeds' do - expect { subject }.not_to raise_error - end - end - end - - context 'when object storage is disabled' do - let(:enabled) { false } - let(:direct_upload) { false } - let(:provider) { 'AWS' } - - it 'succeeds' do - expect { subject }.not_to raise_error - end - end -end diff --git a/spec/initializers/direct_upload_support_spec.rb b/spec/initializers/direct_upload_support_spec.rb new file mode 100644 index 00000000000..e51d404e030 --- /dev/null +++ b/spec/initializers/direct_upload_support_spec.rb @@ -0,0 +1,90 @@ +require 'spec_helper' + +describe 'Direct upload support' do + subject do + load Rails.root.join('config/initializers/direct_upload_support.rb') + end + + where(:config_name) do + %w(lfs artifacts uploads) + end + + with_them do + let(:connection) do + { provider: provider } + end + + let(:object_store) do + { + enabled: enabled, + direct_upload: direct_upload, + connection: connection + } + end + + before do + allow(Gitlab.config).to receive_messages(to_settings(config_name => { + object_store: object_store + })) + end + + context 'when object storage is enabled' do + let(:enabled) { true } + + context 'when direct upload is enabled' do + let(:direct_upload) { true } + + context 'when provider is AWS' do + let(:provider) { 'AWS' } + + it 'succeeds' do + expect { subject }.not_to raise_error + end + end + + context 'when provider is Google' do + let(:provider) { 'Google' } + + it 'succeeds' do + expect { subject }.not_to raise_error + end + end + + context 'when connection is empty' do + let(:connection) { nil } + + it 'raises an error' do + expect { subject }.to raise_error /are supported as a object storage provider when 'direct_upload' is used/ + end + end + + context 'when other provider is used' do + let(:provider) { 'Rackspace' } + + it 'raises an error' do + expect { subject }.to raise_error /are supported as a object storage provider when 'direct_upload' is used/ + end + end + end + + context 'when direct upload is disabled' do + let(:direct_upload) { false } + let(:provider) { 'AWS' } + + it 'succeeds' do + expect { subject }.not_to raise_error + end + end + end + + context 'when object storage is disabled' do + let(:enabled) { false } + let(:direct_upload) { false } + let(:provider) { 'Rackspace' } + + it 'succeeds' do + expect { subject }.not_to raise_error + end + end + end +end diff --git a/spec/initializers/fog_google_https_private_urls_spec.rb b/spec/initializers/fog_google_https_private_urls_spec.rb index de3c157ab7b..08346b71fee 100644 --- a/spec/initializers/fog_google_https_private_urls_spec.rb +++ b/spec/initializers/fog_google_https_private_urls_spec.rb @@ -1,13 +1,13 @@ require 'spec_helper' -describe 'Fog::Storage::GoogleXML::File' do +describe 'Fog::Storage::GoogleXML::File', :fog_requests do let(:storage) do Fog.mock! - Fog::Storage.new({ - google_storage_access_key_id: "asdf", - google_storage_secret_access_key: "asdf", - provider: "Google" - }) + Fog::Storage.new( + google_storage_access_key_id: "asdf", + google_storage_secret_access_key: "asdf", + provider: "Google" + ) end let(:file) do diff --git a/spec/javascripts/blob/viewer/index_spec.js b/spec/javascripts/blob/viewer/index_spec.js index f920c4ca945..8b79624d9f4 100644 --- a/spec/javascripts/blob/viewer/index_spec.js +++ b/spec/javascripts/blob/viewer/index_spec.js @@ -32,7 +32,7 @@ describe('Blob viewer', () => { afterEach(() => { mock.restore(); - location.hash = ''; + window.location.hash = ''; }); it('loads source file after switching views', (done) => { @@ -49,7 +49,7 @@ describe('Blob viewer', () => { }); it('loads source file when line number is in hash', (done) => { - location.hash = '#L1'; + window.location.hash = '#L1'; new BlobViewer(); diff --git a/spec/javascripts/boards/board_card_spec.js b/spec/javascripts/boards/board_card_spec.js index 9b4db774b63..ad263791cd4 100644 --- a/spec/javascripts/boards/board_card_spec.js +++ b/spec/javascripts/boards/board_card_spec.js @@ -5,10 +5,10 @@ import Vue from 'vue'; import MockAdapter from 'axios-mock-adapter'; import axios from '~/lib/utils/axios_utils'; -import '~/boards/models/assignee'; import eventHub from '~/boards/eventhub'; import '~/vue_shared/models/label'; +import '~/vue_shared/models/assignee'; import '~/boards/models/list'; import '~/boards/stores/boards_store'; import boardCard from '~/boards/components/board_card.vue'; diff --git a/spec/javascripts/boards/boards_store_spec.js b/spec/javascripts/boards/boards_store_spec.js index 46fa10e1789..3f5ed4f3d07 100644 --- a/spec/javascripts/boards/boards_store_spec.js +++ b/spec/javascripts/boards/boards_store_spec.js @@ -7,9 +7,9 @@ import axios from '~/lib/utils/axios_utils'; import Cookies from 'js-cookie'; import '~/vue_shared/models/label'; +import '~/vue_shared/models/assignee'; import '~/boards/models/issue'; import '~/boards/models/list'; -import '~/boards/models/assignee'; import '~/boards/services/board_service'; import '~/boards/stores/boards_store'; import { listObj, listObjDuplicate, boardsMockInterceptor, mockBoardService } from './mock_data'; diff --git a/spec/javascripts/boards/issue_card_spec.js b/spec/javascripts/boards/issue_card_spec.js index abeef272c68..05acf903933 100644 --- a/spec/javascripts/boards/issue_card_spec.js +++ b/spec/javascripts/boards/issue_card_spec.js @@ -5,9 +5,9 @@ import Vue from 'vue'; import '~/vue_shared/models/label'; +import '~/vue_shared/models/assignee'; import '~/boards/models/issue'; import '~/boards/models/list'; -import '~/boards/models/assignee'; import '~/boards/stores/boards_store'; import '~/boards/components/issue_card_inner'; import { listObj } from './mock_data'; diff --git a/spec/javascripts/boards/issue_spec.js b/spec/javascripts/boards/issue_spec.js index d90f9a41231..db68096e3bd 100644 --- a/spec/javascripts/boards/issue_spec.js +++ b/spec/javascripts/boards/issue_spec.js @@ -3,9 +3,9 @@ import Vue from 'vue'; import '~/vue_shared/models/label'; +import '~/vue_shared/models/assignee'; import '~/boards/models/issue'; import '~/boards/models/list'; -import '~/boards/models/assignee'; import '~/boards/services/board_service'; import '~/boards/stores/boards_store'; import { mockBoardService } from './mock_data'; diff --git a/spec/javascripts/boards/list_spec.js b/spec/javascripts/boards/list_spec.js index d5d1139de15..ac8bbb8f2a8 100644 --- a/spec/javascripts/boards/list_spec.js +++ b/spec/javascripts/boards/list_spec.js @@ -6,9 +6,9 @@ import MockAdapter from 'axios-mock-adapter'; import axios from '~/lib/utils/axios_utils'; import _ from 'underscore'; import '~/vue_shared/models/label'; +import '~/vue_shared/models/assignee'; import '~/boards/models/issue'; import '~/boards/models/list'; -import '~/boards/models/assignee'; import '~/boards/services/board_service'; import '~/boards/stores/boards_store'; import { listObj, listObjDuplicate, boardsMockInterceptor, mockBoardService } from './mock_data'; diff --git a/spec/javascripts/boards/modal_store_spec.js b/spec/javascripts/boards/modal_store_spec.js index 797693a21aa..a234c81fadf 100644 --- a/spec/javascripts/boards/modal_store_spec.js +++ b/spec/javascripts/boards/modal_store_spec.js @@ -1,9 +1,9 @@ /* global ListIssue */ import '~/vue_shared/models/label'; +import '~/vue_shared/models/assignee'; import '~/boards/models/issue'; import '~/boards/models/list'; -import '~/boards/models/assignee'; import Store from '~/boards/stores/modal_store'; describe('Modal store', () => { diff --git a/spec/javascripts/bootstrap_linked_tabs_spec.js b/spec/javascripts/bootstrap_linked_tabs_spec.js index 93dc60d59fe..6f679369289 100644 --- a/spec/javascripts/bootstrap_linked_tabs_spec.js +++ b/spec/javascripts/bootstrap_linked_tabs_spec.js @@ -36,7 +36,7 @@ import LinkedTabs from '~/lib/utils/bootstrap_linked_tabs'; describe('on click', () => { it('should change the url according to the clicked tab', () => { - const historySpy = spyOn(history, 'replaceState').and.callFake(() => {}); + const historySpy = spyOn(window.history, 'replaceState').and.callFake(() => {}); const linkedTabs = new LinkedTabs({ action: 'show', diff --git a/spec/javascripts/clusters/stores/clusters_store_spec.js b/spec/javascripts/clusters/stores/clusters_store_spec.js index 6854b016852..9e43552f740 100644 --- a/spec/javascripts/clusters/stores/clusters_store_spec.js +++ b/spec/javascripts/clusters/stores/clusters_store_spec.js @@ -110,7 +110,7 @@ describe('Clusters Store', () => { expect( store.state.applications.jupyter.hostname, - ).toEqual(`jupyter.${store.state.applications.ingress.externalIp}.xip.io`); + ).toEqual(`jupyter.${store.state.applications.ingress.externalIp}.nip.io`); }); }); }); diff --git a/spec/javascripts/commits_spec.js b/spec/javascripts/commits_spec.js index 60d100e8544..28b89157bd3 100644 --- a/spec/javascripts/commits_spec.js +++ b/spec/javascripts/commits_spec.js @@ -56,7 +56,7 @@ describe('Commits List', () => { beforeEach(() => { commitsList.searchField.val(''); - spyOn(history, 'replaceState').and.stub(); + spyOn(window.history, 'replaceState').and.stub(); mock = new MockAdapter(axios); mock.onGet('/h5bp/html5-boilerplate/commits/master').reply(200, { diff --git a/spec/javascripts/create_merge_request_dropdown_spec.js b/spec/javascripts/create_merge_request_dropdown_spec.js new file mode 100644 index 00000000000..b229765a8c5 --- /dev/null +++ b/spec/javascripts/create_merge_request_dropdown_spec.js @@ -0,0 +1,67 @@ +import axios from '~/lib/utils/axios_utils'; +import MockAdapter from 'axios-mock-adapter'; +import CreateMergeRequestDropdown from '~/create_merge_request_dropdown'; +import { TEST_HOST } from 'spec/test_constants'; + +describe('CreateMergeRequestDropdown', () => { + let axiosMock; + let dropdown; + + beforeEach(() => { + axiosMock = new MockAdapter(axios); + + setFixtures(` + <div id="dummy-wrapper-element"> + <div class="available"></div> + <div class="unavailable"> + <div class="fa"></div> + <div class="text"></div> + </div> + <div class="js-ref"></div> + <div class="js-create-merge-request"></div> + <div class="js-create-target"></div> + <div class="js-dropdown-toggle"></div> + </div> + `); + + const dummyElement = document.getElementById('dummy-wrapper-element'); + dropdown = new CreateMergeRequestDropdown(dummyElement); + dropdown.refsPath = `${TEST_HOST}/dummy/refs?search=`; + }); + + afterEach(() => { + axiosMock.restore(); + }); + + describe('getRef', () => { + it('escapes branch names correctly', done => { + const endpoint = `${dropdown.refsPath}contains%23hash`; + spyOn(axios, 'get').and.callThrough(); + axiosMock.onGet(endpoint).replyOnce({}); + + dropdown + .getRef('contains#hash') + .then(() => { + expect(axios.get).toHaveBeenCalledWith(endpoint); + }) + .then(done) + .catch(done.fail); + }); + }); + + describe('updateCreatePaths', () => { + it('escapes branch names correctly', () => { + dropdown.createBranchPath = `${TEST_HOST}/branches?branch_name=some-branch&issue=42`; + dropdown.createMrPath = `${TEST_HOST}/create_merge_request?branch_name=some-branch&ref=master`; + + dropdown.updateCreatePaths('branch', 'contains#hash'); + + expect(dropdown.createBranchPath).toBe( + `${TEST_HOST}/branches?branch_name=contains%23hash&issue=42`, + ); + expect(dropdown.createMrPath).toBe( + `${TEST_HOST}/create_merge_request?branch_name=contains%23hash&ref=master`, + ); + }); + }); +}); diff --git a/spec/javascripts/datetime_utility_spec.js b/spec/javascripts/datetime_utility_spec.js index a8d09202154..e224ed46d18 100644 --- a/spec/javascripts/datetime_utility_spec.js +++ b/spec/javascripts/datetime_utility_spec.js @@ -149,23 +149,22 @@ describe('getSundays', () => { }); }); -describe('getTimeframeWindow', () => { - it('returns array of dates representing a timeframe based on provided length and date', () => { - const date = new Date(2018, 0, 1); +describe('getTimeframeWindowFrom', () => { + it('returns array of date objects upto provided length start with provided startDate', () => { + const startDate = new Date(2018, 0, 1); const mockTimeframe = [ - new Date(2017, 9, 1), - new Date(2017, 10, 1), - new Date(2017, 11, 1), new Date(2018, 0, 1), new Date(2018, 1, 1), - new Date(2018, 2, 31), + new Date(2018, 2, 1), + new Date(2018, 3, 1), + new Date(2018, 4, 31), ]; - const timeframe = datetimeUtility.getTimeframeWindow(6, date); - - expect(timeframe.length).toBe(6); + const timeframe = datetimeUtility.getTimeframeWindowFrom(startDate, 5); + expect(timeframe.length).toBe(5); timeframe.forEach((timeframeItem, index) => { - expect(timeframeItem.getFullYear() === mockTimeframe[index].getFullYear()).toBeTruthy(); - expect(timeframeItem.getMonth() === mockTimeframe[index].getMonth()).toBeTruthy(); + console.log(timeframeItem); + expect(timeframeItem.getFullYear() === mockTimeframe[index].getFullYear()).toBe(true); + expect(timeframeItem.getMonth() === mockTimeframe[index].getMonth()).toBe(true); expect(timeframeItem.getDate() === mockTimeframe[index].getDate()).toBeTruthy(); }); }); diff --git a/spec/javascripts/environments/environments_app_spec.js b/spec/javascripts/environments/environments_app_spec.js index 615168645b4..6968fbc7ce7 100644 --- a/spec/javascripts/environments/environments_app_spec.js +++ b/spec/javascripts/environments/environments_app_spec.js @@ -220,7 +220,7 @@ describe('Environment', () => { ); component = mountComponent(EnvironmentsComponent, mockData); - spyOn(history, 'pushState').and.stub(); + spyOn(window.history, 'pushState').and.stub(); }); describe('updateContent', () => { diff --git a/spec/javascripts/environments/folder/environments_folder_view_spec.js b/spec/javascripts/environments/folder/environments_folder_view_spec.js index f5ce4df0bfe..51d4213c38f 100644 --- a/spec/javascripts/environments/folder/environments_folder_view_spec.js +++ b/spec/javascripts/environments/folder/environments_folder_view_spec.js @@ -177,7 +177,7 @@ describe('Environments Folder View', () => { }); component = mountComponent(Component, mockData); - spyOn(history, 'pushState').and.stub(); + spyOn(window.history, 'pushState').and.stub(); }); describe('updateContent', () => { diff --git a/spec/javascripts/fixtures/images/green_box.png b/spec/javascripts/fixtures/images/green_box.png Binary files differnew file mode 100644 index 00000000000..cd1ff9f9ade --- /dev/null +++ b/spec/javascripts/fixtures/images/green_box.png diff --git a/spec/javascripts/fixtures/images/red_box.png b/spec/javascripts/fixtures/images/red_box.png Binary files differnew file mode 100644 index 00000000000..73b2927da0f --- /dev/null +++ b/spec/javascripts/fixtures/images/red_box.png diff --git a/spec/javascripts/gl_field_errors_spec.js b/spec/javascripts/gl_field_errors_spec.js index 4e93fd91751..108e0064c47 100644 --- a/spec/javascripts/gl_field_errors_spec.js +++ b/spec/javascripts/gl_field_errors_spec.js @@ -8,7 +8,9 @@ describe('GL Style Field Errors', function() { beforeEach(function() { loadFixtures('static/gl_field_errors.html.raw'); - const $form = this.$form = $('form.gl-show-field-errors'); + const $form = $('form.gl-show-field-errors'); + + this.$form = $form; this.fieldErrors = new GlFieldErrors($form); }); diff --git a/spec/javascripts/helpers/user_mock_data_helper.js b/spec/javascripts/helpers/user_mock_data_helper.js index 323fee3767e..f6c3ce5aecc 100644 --- a/spec/javascripts/helpers/user_mock_data_helper.js +++ b/spec/javascripts/helpers/user_mock_data_helper.js @@ -1,7 +1,7 @@ export default { createNumberRandomUsers(numberUsers) { const users = []; - for (let i = 0; i < numberUsers; i = i += 1) { + for (let i = 0; i < numberUsers; i += 1) { users.push( { avatar: 'https://gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', diff --git a/spec/javascripts/ide/components/commit_sidebar/list_item_spec.js b/spec/javascripts/ide/components/commit_sidebar/list_item_spec.js index cc7e0a3f26d..bf96170f703 100644 --- a/spec/javascripts/ide/components/commit_sidebar/list_item_spec.js +++ b/spec/javascripts/ide/components/commit_sidebar/list_item_spec.js @@ -19,6 +19,7 @@ describe('Multi-file editor commit sidebar list item', () => { vm = createComponentWithStore(Component, store, { file: f, actionComponent: 'stage-button', + activeFileKey: `staged-${f.key}`, }).$mount(); }); @@ -89,4 +90,20 @@ describe('Multi-file editor commit sidebar list item', () => { }); }); }); + + describe('is active', () => { + it('does not add active class when dont keys match', () => { + expect(vm.$el.querySelector('.is-active')).toBe(null); + }); + + it('adds active class when keys match', done => { + vm.keyPrefix = 'staged'; + + vm.$nextTick(() => { + expect(vm.$el.querySelector('.is-active')).not.toBe(null); + + done(); + }); + }); + }); }); diff --git a/spec/javascripts/ide/components/commit_sidebar/list_spec.js b/spec/javascripts/ide/components/commit_sidebar/list_spec.js index 54625ef90f8..b786be55019 100644 --- a/spec/javascripts/ide/components/commit_sidebar/list_spec.js +++ b/spec/javascripts/ide/components/commit_sidebar/list_spec.js @@ -16,7 +16,10 @@ describe('Multi-file editor commit sidebar list', () => { iconName: 'staged', action: 'stageAllChanges', actionBtnText: 'stage all', + actionBtnIcon: 'history', itemActionComponent: 'stage-button', + activeFileKey: 'staged-testing', + keyPrefix: 'staged', }); vm.$store.state.rightPanelCollapsed = false; @@ -40,7 +43,7 @@ describe('Multi-file editor commit sidebar list', () => { }); it('renders list', () => { - expect(vm.$el.querySelectorAll('li').length).toBe(1); + expect(vm.$el.querySelectorAll('.multi-file-commit-list > li').length).toBe(1); }); }); diff --git a/spec/javascripts/ide/components/commit_sidebar/stage_button_spec.js b/spec/javascripts/ide/components/commit_sidebar/stage_button_spec.js index 6bf8710bda7..a5b906da8a1 100644 --- a/spec/javascripts/ide/components/commit_sidebar/stage_button_spec.js +++ b/spec/javascripts/ide/components/commit_sidebar/stage_button_spec.js @@ -39,7 +39,7 @@ describe('IDE stage file button', () => { }); it('calls store with discard button', () => { - vm.$el.querySelectorAll('.btn')[1].click(); + vm.$el.querySelector('.dropdown-menu button').click(); expect(vm.discardFileChanges).toHaveBeenCalledWith(f.path); }); diff --git a/spec/javascripts/ide/components/jobs/detail/description_spec.js b/spec/javascripts/ide/components/jobs/detail/description_spec.js new file mode 100644 index 00000000000..9b715a41499 --- /dev/null +++ b/spec/javascripts/ide/components/jobs/detail/description_spec.js @@ -0,0 +1,28 @@ +import Vue from 'vue'; +import Description from '~/ide/components/jobs/detail/description.vue'; +import mountComponent from '../../../../helpers/vue_mount_component_helper'; +import { jobs } from '../../../mock_data'; + +describe('IDE job description', () => { + const Component = Vue.extend(Description); + let vm; + + beforeEach(() => { + vm = mountComponent(Component, { + job: jobs[0], + }); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('renders job details', () => { + expect(vm.$el.textContent).toContain('#1'); + expect(vm.$el.textContent).toContain('test'); + }); + + it('renders CI icon', () => { + expect(vm.$el.querySelector('.ci-status-icon .ic-status_passed_borderless')).not.toBe(null); + }); +}); diff --git a/spec/javascripts/ide/components/jobs/detail/scroll_button_spec.js b/spec/javascripts/ide/components/jobs/detail/scroll_button_spec.js new file mode 100644 index 00000000000..fff382a107f --- /dev/null +++ b/spec/javascripts/ide/components/jobs/detail/scroll_button_spec.js @@ -0,0 +1,59 @@ +import Vue from 'vue'; +import ScrollButton from '~/ide/components/jobs/detail/scroll_button.vue'; +import mountComponent from '../../../../helpers/vue_mount_component_helper'; + +describe('IDE job log scroll button', () => { + const Component = Vue.extend(ScrollButton); + let vm; + + beforeEach(() => { + vm = mountComponent(Component, { + direction: 'up', + disabled: false, + }); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('iconName', () => { + ['up', 'down'].forEach(direction => { + it(`returns icon name for ${direction}`, () => { + vm.direction = direction; + + expect(vm.iconName).toBe(`scroll_${direction}`); + }); + }); + }); + + describe('tooltipTitle', () => { + it('returns title for up', () => { + expect(vm.tooltipTitle).toBe('Scroll to top'); + }); + + it('returns title for down', () => { + vm.direction = 'down'; + + expect(vm.tooltipTitle).toBe('Scroll to bottom'); + }); + }); + + it('emits click event on click', () => { + spyOn(vm, '$emit'); + + vm.$el.querySelector('.btn-scroll').click(); + + expect(vm.$emit).toHaveBeenCalledWith('click'); + }); + + it('disables button when disabled is true', done => { + vm.disabled = true; + + vm.$nextTick(() => { + expect(vm.$el.querySelector('.btn-scroll').hasAttribute('disabled')).toBe(true); + + done(); + }); + }); +}); diff --git a/spec/javascripts/ide/components/jobs/detail_spec.js b/spec/javascripts/ide/components/jobs/detail_spec.js new file mode 100644 index 00000000000..8f8d4b9709e --- /dev/null +++ b/spec/javascripts/ide/components/jobs/detail_spec.js @@ -0,0 +1,185 @@ +import Vue from 'vue'; +import JobDetail from '~/ide/components/jobs/detail.vue'; +import { createStore } from '~/ide/stores'; +import { createComponentWithStore } from '../../../helpers/vue_mount_component_helper'; +import { jobs } from '../../mock_data'; + +describe('IDE jobs detail view', () => { + const Component = Vue.extend(JobDetail); + let vm; + + beforeEach(() => { + const store = createStore(); + + store.state.pipelines.detailJob = { + ...jobs[0], + isLoading: true, + output: 'testing', + rawPath: `${gl.TEST_HOST}/raw`, + }; + + vm = createComponentWithStore(Component, store); + + spyOn(vm, 'fetchJobTrace').and.returnValue(Promise.resolve()); + + vm = vm.$mount(); + + spyOn(vm.$refs.buildTrace, 'scrollTo'); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('calls fetchJobTrace on mount', () => { + expect(vm.fetchJobTrace).toHaveBeenCalled(); + }); + + it('scrolls to bottom on mount', done => { + setTimeout(() => { + expect(vm.$refs.buildTrace.scrollTo).toHaveBeenCalled(); + + done(); + }); + }); + + it('renders job output', () => { + expect(vm.$el.querySelector('.bash').textContent).toContain('testing'); + }); + + it('renders empty message output', done => { + vm.$store.state.pipelines.detailJob.output = ''; + + vm.$nextTick(() => { + expect(vm.$el.querySelector('.bash').textContent).toContain('No messages were logged'); + + done(); + }); + }); + + it('renders loading icon', () => { + expect(vm.$el.querySelector('.build-loader-animation')).not.toBe(null); + expect(vm.$el.querySelector('.build-loader-animation').style.display).toBe(''); + }); + + it('hides output when loading', () => { + expect(vm.$el.querySelector('.bash')).not.toBe(null); + expect(vm.$el.querySelector('.bash').style.display).toBe('none'); + }); + + it('hide loading icon when isLoading is false', done => { + vm.$store.state.pipelines.detailJob.isLoading = false; + + vm.$nextTick(() => { + expect(vm.$el.querySelector('.build-loader-animation').style.display).toBe('none'); + + done(); + }); + }); + + it('resets detailJob when clicking header button', () => { + spyOn(vm, 'setDetailJob'); + + vm.$el.querySelector('.btn').click(); + + expect(vm.setDetailJob).toHaveBeenCalledWith(null); + }); + + it('renders raw path link', () => { + expect(vm.$el.querySelector('.controllers-buttons').getAttribute('href')).toBe( + `${gl.TEST_HOST}/raw`, + ); + }); + + describe('scroll buttons', () => { + it('triggers scrollDown when clicking down button', done => { + spyOn(vm, 'scrollDown'); + + vm.$el.querySelectorAll('.btn-scroll')[1].click(); + + vm.$nextTick(() => { + expect(vm.scrollDown).toHaveBeenCalled(); + + done(); + }); + }); + + it('triggers scrollUp when clicking up button', done => { + spyOn(vm, 'scrollUp'); + + vm.scrollPos = 1; + + vm + .$nextTick() + .then(() => vm.$el.querySelector('.btn-scroll').click()) + .then(() => vm.$nextTick()) + .then(() => { + expect(vm.scrollUp).toHaveBeenCalled(); + }) + .then(done) + .catch(done.fail); + }); + }); + + describe('scrollDown', () => { + it('scrolls build trace to bottom', () => { + spyOnProperty(vm.$refs.buildTrace, 'scrollHeight').and.returnValue(1000); + + vm.scrollDown(); + + expect(vm.$refs.buildTrace.scrollTo).toHaveBeenCalledWith(0, 1000); + }); + }); + + describe('scrollUp', () => { + it('scrolls build trace to top', () => { + vm.scrollUp(); + + expect(vm.$refs.buildTrace.scrollTo).toHaveBeenCalledWith(0, 0); + }); + }); + + describe('scrollBuildLog', () => { + beforeEach(() => { + spyOnProperty(vm.$refs.buildTrace, 'offsetHeight').and.returnValue(100); + spyOnProperty(vm.$refs.buildTrace, 'scrollHeight').and.returnValue(200); + }); + + it('sets scrollPos to bottom when at the bottom', done => { + spyOnProperty(vm.$refs.buildTrace, 'scrollTop').and.returnValue(100); + + vm.scrollBuildLog(); + + setTimeout(() => { + expect(vm.scrollPos).toBe(1); + + done(); + }); + }); + + it('sets scrollPos to top when at the top', done => { + spyOnProperty(vm.$refs.buildTrace, 'scrollTop').and.returnValue(0); + vm.scrollPos = 1; + + vm.scrollBuildLog(); + + setTimeout(() => { + expect(vm.scrollPos).toBe(0); + + done(); + }); + }); + + it('resets scrollPos when not at top or bottom', done => { + spyOnProperty(vm.$refs.buildTrace, 'scrollTop').and.returnValue(10); + + vm.scrollBuildLog(); + + setTimeout(() => { + expect(vm.scrollPos).toBe(''); + + done(); + }); + }); + }); +}); diff --git a/spec/javascripts/ide/components/jobs/item_spec.js b/spec/javascripts/ide/components/jobs/item_spec.js index 7c1dd4e475c..79e07f00e7b 100644 --- a/spec/javascripts/ide/components/jobs/item_spec.js +++ b/spec/javascripts/ide/components/jobs/item_spec.js @@ -26,4 +26,14 @@ describe('IDE jobs item', () => { it('renders CI icon', () => { expect(vm.$el.querySelector('.ic-status_passed_borderless')).not.toBe(null); }); + + it('does not render view logs button if not started', done => { + vm.job.started = false; + + vm.$nextTick(() => { + expect(vm.$el.querySelector('.btn')).toBe(null); + + done(); + }); + }); }); diff --git a/spec/javascripts/ide/components/merge_requests/dropdown_spec.js b/spec/javascripts/ide/components/merge_requests/dropdown_spec.js new file mode 100644 index 00000000000..74884c9a362 --- /dev/null +++ b/spec/javascripts/ide/components/merge_requests/dropdown_spec.js @@ -0,0 +1,47 @@ +import Vue from 'vue'; +import { createStore } from '~/ide/stores'; +import Dropdown from '~/ide/components/merge_requests/dropdown.vue'; +import { createComponentWithStore } from '../../../helpers/vue_mount_component_helper'; +import { mergeRequests } from '../../mock_data'; + +describe('IDE merge requests dropdown', () => { + const Component = Vue.extend(Dropdown); + let vm; + + beforeEach(() => { + const store = createStore(); + + vm = createComponentWithStore(Component, store, { show: false }).$mount(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('does not render tabs when show is false', () => { + expect(vm.$el.querySelector('.nav-links')).toBe(null); + }); + + describe('when show is true', () => { + beforeEach(done => { + vm.show = true; + vm.$store.state.mergeRequests.assigned.mergeRequests.push(mergeRequests[0]); + + vm.$nextTick(done); + }); + + it('renders tabs', () => { + expect(vm.$el.querySelector('.nav-links')).not.toBe(null); + }); + + it('renders count for assigned & created data', () => { + expect(vm.$el.querySelector('.nav-links a').textContent).toContain('Created by me'); + expect(vm.$el.querySelector('.nav-links a .badge').textContent).toContain('0'); + + expect(vm.$el.querySelectorAll('.nav-links a')[1].textContent).toContain('Assigned to me'); + expect( + vm.$el.querySelectorAll('.nav-links a')[1].querySelector('.badge').textContent, + ).toContain('1'); + }); + }); +}); diff --git a/spec/javascripts/ide/components/merge_requests/item_spec.js b/spec/javascripts/ide/components/merge_requests/item_spec.js new file mode 100644 index 00000000000..51c4cddef2f --- /dev/null +++ b/spec/javascripts/ide/components/merge_requests/item_spec.js @@ -0,0 +1,61 @@ +import Vue from 'vue'; +import Item from '~/ide/components/merge_requests/item.vue'; +import mountCompontent from '../../../helpers/vue_mount_component_helper'; + +describe('IDE merge request item', () => { + const Component = Vue.extend(Item); + let vm; + + beforeEach(() => { + vm = mountCompontent(Component, { + item: { + iid: 1, + projectPathWithNamespace: 'gitlab-org/gitlab-ce', + title: 'Merge request title', + }, + currentId: '1', + currentProjectId: 'gitlab-org/gitlab-ce', + }); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('renders merge requests data', () => { + expect(vm.$el.textContent).toContain('Merge request title'); + expect(vm.$el.textContent).toContain('gitlab-org/gitlab-ce!1'); + }); + + it('renders icon if ID matches currentId', () => { + expect(vm.$el.querySelector('.ic-mobile-issue-close')).not.toBe(null); + }); + + it('does not render icon if ID does not match currentId', done => { + vm.currentId = '2'; + + vm.$nextTick(() => { + expect(vm.$el.querySelector('.ic-mobile-issue-close')).toBe(null); + + done(); + }); + }); + + it('does not render icon if project ID does not match', done => { + vm.currentProjectId = 'test/test'; + + vm.$nextTick(() => { + expect(vm.$el.querySelector('.ic-mobile-issue-close')).toBe(null); + + done(); + }); + }); + + it('emits click event on click', () => { + spyOn(vm, '$emit'); + + vm.$el.click(); + + expect(vm.$emit).toHaveBeenCalledWith('click', vm.item); + }); +}); diff --git a/spec/javascripts/ide/components/merge_requests/list_spec.js b/spec/javascripts/ide/components/merge_requests/list_spec.js new file mode 100644 index 00000000000..f4b393778dc --- /dev/null +++ b/spec/javascripts/ide/components/merge_requests/list_spec.js @@ -0,0 +1,126 @@ +import Vue from 'vue'; +import store from '~/ide/stores'; +import List from '~/ide/components/merge_requests/list.vue'; +import { createComponentWithStore } from '../../../helpers/vue_mount_component_helper'; +import { mergeRequests } from '../../mock_data'; +import { resetStore } from '../../helpers'; + +describe('IDE merge requests list', () => { + const Component = Vue.extend(List); + let vm; + + beforeEach(() => { + vm = createComponentWithStore(Component, store, { + type: 'created', + emptyText: 'empty text', + }); + + spyOn(vm, 'fetchMergeRequests'); + + vm.$mount(); + }); + + afterEach(() => { + vm.$destroy(); + + resetStore(vm.$store); + }); + + it('calls fetch on mounted', () => { + expect(vm.fetchMergeRequests).toHaveBeenCalledWith({ + type: 'created', + search: '', + }); + }); + + it('renders loading icon', done => { + vm.$store.state.mergeRequests.created.isLoading = true; + + vm.$nextTick(() => { + expect(vm.$el.querySelector('.loading-container')).not.toBe(null); + + done(); + }); + }); + + it('renders empty text when no merge requests exist', () => { + expect(vm.$el.textContent).toContain('empty text'); + }); + + it('renders no search results text when search is not empty', done => { + vm.search = 'testing'; + + vm.$nextTick(() => { + expect(vm.$el.textContent).toContain('No merge requests found'); + + done(); + }); + }); + + describe('with merge requests', () => { + beforeEach(done => { + vm.$store.state.mergeRequests.created.mergeRequests.push({ + ...mergeRequests[0], + projectPathWithNamespace: 'gitlab-org/gitlab-ce', + }); + + vm.$nextTick(done); + }); + + it('renders list', () => { + expect(vm.$el.querySelectorAll('li').length).toBe(1); + expect(vm.$el.querySelector('li').textContent).toContain(mergeRequests[0].title); + }); + + it('calls openMergeRequest when clicking merge request', done => { + spyOn(vm, 'openMergeRequest'); + vm.$el.querySelector('li button').click(); + + vm.$nextTick(() => { + expect(vm.openMergeRequest).toHaveBeenCalledWith({ + projectPath: 'gitlab-org/gitlab-ce', + id: 1, + }); + + done(); + }); + }); + }); + + describe('focusSearch', () => { + it('focuses search input when loading is false', done => { + spyOn(vm.$refs.searchInput, 'focus'); + + vm.$store.state.mergeRequests.created.isLoading = false; + vm.focusSearch(); + + vm.$nextTick(() => { + expect(vm.$refs.searchInput.focus).toHaveBeenCalled(); + + done(); + }); + }); + }); + + describe('searchMergeRequests', () => { + beforeEach(() => { + spyOn(vm, 'loadMergeRequests'); + + jasmine.clock().install(); + }); + + afterEach(() => { + jasmine.clock().uninstall(); + }); + + it('calls loadMergeRequests on input in search field', () => { + const event = new Event('input'); + + vm.$el.querySelector('input').dispatchEvent(event); + + jasmine.clock().tick(300); + + expect(vm.loadMergeRequests).toHaveBeenCalled(); + }); + }); +}); diff --git a/spec/javascripts/ide/components/pipelines/list_spec.js b/spec/javascripts/ide/components/pipelines/list_spec.js index 2bb5aa08c3b..68487733cb9 100644 --- a/spec/javascripts/ide/components/pipelines/list_spec.js +++ b/spec/javascripts/ide/components/pipelines/list_spec.js @@ -45,12 +45,15 @@ describe('IDE pipelines list', () => { setTimeout(done); }); - afterEach(() => { - vm.$store.dispatch('pipelines/stopPipelinePolling'); - vm.$store.dispatch('pipelines/clearEtagPoll'); - + afterEach(done => { vm.$destroy(); mock.restore(); + + vm.$store + .dispatch('pipelines/stopPipelinePolling') + .then(() => vm.$store.dispatch('pipelines/clearEtagPoll')) + .then(done) + .catch(done.fail); }); it('renders pipeline data', () => { diff --git a/spec/javascripts/ide/components/repo_commit_section_spec.js b/spec/javascripts/ide/components/repo_commit_section_spec.js index 5e3e00a180b..6bf309fb4bf 100644 --- a/spec/javascripts/ide/components/repo_commit_section_spec.js +++ b/spec/javascripts/ide/components/repo_commit_section_spec.js @@ -56,7 +56,7 @@ describe('RepoCommitSection', () => { vm.$store.state.entries[f.path] = f; }); - return vm.$mount(); + return vm; } beforeEach(done => { @@ -64,6 +64,10 @@ describe('RepoCommitSection', () => { vm = createComponent(); + spyOn(vm, 'openPendingTab').and.callThrough(); + + vm.$mount(); + spyOn(service, 'getTreeData').and.returnValue( Promise.resolve({ headers: { @@ -98,6 +102,7 @@ describe('RepoCommitSection', () => { store.state.noChangesStateSvgPath = 'nochangessvg'; store.state.committedStateSvgPath = 'svg'; + vm.$destroy(); vm = createComponentWithStore(Component, store).$mount(); expect(vm.$el.querySelector('.js-empty-state').textContent.trim()).toContain('No changes'); @@ -106,7 +111,7 @@ describe('RepoCommitSection', () => { }); it('renders a commit section', () => { - const changedFileElements = [...vm.$el.querySelectorAll('.multi-file-commit-list li')]; + const changedFileElements = [...vm.$el.querySelectorAll('.multi-file-commit-list > li')]; const allFiles = vm.$store.state.changedFiles.concat(vm.$store.state.stagedFiles); expect(changedFileElements.length).toEqual(4); @@ -135,22 +140,26 @@ describe('RepoCommitSection', () => { vm.$el.querySelector('.multi-file-discard-btn .btn').click(); Vue.nextTick(() => { - expect(vm.$el.querySelector('.ide-commit-list-container').querySelectorAll('li').length).toBe( - 1, - ); + expect( + vm.$el + .querySelector('.ide-commit-list-container') + .querySelectorAll('.multi-file-commit-list > li').length, + ).toBe(1); done(); }); }); it('discards a single file', done => { - vm.$el.querySelectorAll('.multi-file-discard-btn .btn')[1].click(); + vm.$el.querySelector('.multi-file-discard-btn .dropdown-menu button').click(); Vue.nextTick(() => { expect(vm.$el.querySelector('.ide-commit-list-container').textContent).not.toContain('file1'); - expect(vm.$el.querySelector('.ide-commit-list-container').querySelectorAll('li').length).toBe( - 1, - ); + expect( + vm.$el + .querySelector('.ide-commit-list-container') + .querySelectorAll('.multi-file-commit-list > li').length, + ).toBe(1); done(); }); @@ -176,5 +185,12 @@ describe('RepoCommitSection', () => { expect(store.state.openFiles.length).toBe(1); expect(store.state.openFiles[0].pending).toBe(true); }); + + it('calls openPendingTab', () => { + expect(vm.openPendingTab).toHaveBeenCalledWith({ + file: vm.lastOpenedFile, + keyPrefix: 'unstaged', + }); + }); }); }); diff --git a/spec/javascripts/ide/components/repo_editor_spec.js b/spec/javascripts/ide/components/repo_editor_spec.js index d3f80e6f9c0..2256deb7dac 100644 --- a/spec/javascripts/ide/components/repo_editor_spec.js +++ b/spec/javascripts/ide/components/repo_editor_spec.js @@ -3,7 +3,6 @@ import MockAdapter from 'axios-mock-adapter'; import axios from '~/lib/utils/axios_utils'; import store from '~/ide/stores'; import repoEditor from '~/ide/components/repo_editor.vue'; -import monacoLoader from '~/ide/monaco_loader'; import Editor from '~/ide/lib/editor'; import { activityBarViews } from '~/ide/constants'; import { createComponentWithStore } from '../../helpers/vue_mount_component_helper'; @@ -25,13 +24,10 @@ describe('RepoEditor', () => { f.tempFile = true; vm.$store.state.openFiles.push(f); Vue.set(vm.$store.state.entries, f.path, f); - vm.monaco = true; vm.$mount(); - monacoLoader(['vs/editor/editor.main'], () => { - setTimeout(done, 0); - }); + Vue.nextTick(() => setTimeout(done)); }); afterEach(() => { @@ -319,6 +315,17 @@ describe('RepoEditor', () => { done(); }); }); + + it('calls updateDimensions when rightPane is updated', done => { + vm.$store.state.rightPane = 'testing'; + + vm.$nextTick(() => { + expect(vm.editor.updateDimensions).toHaveBeenCalled(); + expect(vm.editor.updateDiffView).toHaveBeenCalled(); + + done(); + }); + }); }); describe('show tabs', () => { diff --git a/spec/javascripts/ide/lib/common/model_manager_spec.js b/spec/javascripts/ide/lib/common/model_manager_spec.js index c00d590c580..38ffa317e8e 100644 --- a/spec/javascripts/ide/lib/common/model_manager_spec.js +++ b/spec/javascripts/ide/lib/common/model_manager_spec.js @@ -1,18 +1,12 @@ -/* global monaco */ import eventHub from '~/ide/eventhub'; -import monacoLoader from '~/ide/monaco_loader'; import ModelManager from '~/ide/lib/common/model_manager'; import { file } from '../../helpers'; describe('Multi-file editor library model manager', () => { let instance; - beforeEach(done => { - monacoLoader(['vs/editor/editor.main'], () => { - instance = new ModelManager(monaco); - - done(); - }); + beforeEach(() => { + instance = new ModelManager(); }); afterEach(() => { diff --git a/spec/javascripts/ide/lib/common/model_spec.js b/spec/javascripts/ide/lib/common/model_spec.js index c278bf92b08..f096e06f43c 100644 --- a/spec/javascripts/ide/lib/common/model_spec.js +++ b/spec/javascripts/ide/lib/common/model_spec.js @@ -1,23 +1,17 @@ -/* global monaco */ import eventHub from '~/ide/eventhub'; -import monacoLoader from '~/ide/monaco_loader'; import Model from '~/ide/lib/common/model'; import { file } from '../../helpers'; describe('Multi-file editor library model', () => { let model; - beforeEach(done => { + beforeEach(() => { spyOn(eventHub, '$on').and.callThrough(); - monacoLoader(['vs/editor/editor.main'], () => { - const f = file('path'); - f.mrChange = { diff: 'ABC' }; - f.baseRaw = 'test'; - model = new Model(monaco, f); - - done(); - }); + const f = file('path'); + f.mrChange = { diff: 'ABC' }; + f.baseRaw = 'test'; + model = new Model(f); }); afterEach(() => { @@ -38,7 +32,7 @@ describe('Multi-file editor library model', () => { const f = file('path'); model.dispose(); - model = new Model(monaco, f, { + model = new Model(f, { ...f, content: '123 testing', }); diff --git a/spec/javascripts/ide/lib/decorations/controller_spec.js b/spec/javascripts/ide/lib/decorations/controller_spec.js index e1c4ca570b6..a112361e0d1 100644 --- a/spec/javascripts/ide/lib/decorations/controller_spec.js +++ b/spec/javascripts/ide/lib/decorations/controller_spec.js @@ -1,6 +1,4 @@ -/* global monaco */ -import monacoLoader from '~/ide/monaco_loader'; -import editor from '~/ide/lib/editor'; +import Editor from '~/ide/lib/editor'; import DecorationsController from '~/ide/lib/decorations/controller'; import Model from '~/ide/lib/common/model'; import { file } from '../../helpers'; @@ -10,16 +8,12 @@ describe('Multi-file editor library decorations controller', () => { let controller; let model; - beforeEach(done => { - monacoLoader(['vs/editor/editor.main'], () => { - editorInstance = editor.create(monaco); - editorInstance.createInstance(document.createElement('div')); + beforeEach(() => { + editorInstance = Editor.create(); + editorInstance.createInstance(document.createElement('div')); - controller = new DecorationsController(editorInstance); - model = new Model(monaco, file('path')); - - done(); - }); + controller = new DecorationsController(editorInstance); + model = new Model(file('path')); }); afterEach(() => { diff --git a/spec/javascripts/ide/lib/diff/controller_spec.js b/spec/javascripts/ide/lib/diff/controller_spec.js index fd8ab3b4f1d..96abd1dcd9e 100644 --- a/spec/javascripts/ide/lib/diff/controller_spec.js +++ b/spec/javascripts/ide/lib/diff/controller_spec.js @@ -1,6 +1,5 @@ -/* global monaco */ -import monacoLoader from '~/ide/monaco_loader'; -import editor from '~/ide/lib/editor'; +import { Range } from 'monaco-editor'; +import Editor from '~/ide/lib/editor'; import ModelManager from '~/ide/lib/common/model_manager'; import DecorationsController from '~/ide/lib/decorations/controller'; import DirtyDiffController, { getDiffChangeType, getDecorator } from '~/ide/lib/diff/controller'; @@ -14,20 +13,16 @@ describe('Multi-file editor library dirty diff controller', () => { let decorationsController; let model; - beforeEach(done => { - monacoLoader(['vs/editor/editor.main'], () => { - editorInstance = editor.create(monaco); - editorInstance.createInstance(document.createElement('div')); + beforeEach(() => { + editorInstance = Editor.create(); + editorInstance.createInstance(document.createElement('div')); - modelManager = new ModelManager(monaco); - decorationsController = new DecorationsController(editorInstance); + modelManager = new ModelManager(); + decorationsController = new DecorationsController(editorInstance); - model = modelManager.addModel(file('path')); + model = modelManager.addModel(file('path')); - controller = new DirtyDiffController(modelManager, decorationsController); - - done(); - }); + controller = new DirtyDiffController(modelManager, decorationsController); }); afterEach(() => { @@ -170,7 +165,7 @@ describe('Multi-file editor library dirty diff controller', () => { [], [ { - range: new monaco.Range(1, 1, 1, 1), + range: new Range(1, 1, 1, 1), options: { isWholeLine: true, linesDecorationsClassName: 'dirty-diff dirty-diff-modified', diff --git a/spec/javascripts/ide/lib/editor_spec.js b/spec/javascripts/ide/lib/editor_spec.js index b88a12264ca..c2cb964ea87 100644 --- a/spec/javascripts/ide/lib/editor_spec.js +++ b/spec/javascripts/ide/lib/editor_spec.js @@ -1,6 +1,5 @@ -/* global monaco */ -import monacoLoader from '~/ide/monaco_loader'; -import editor from '~/ide/lib/editor'; +import { editor as monacoEditor } from 'monaco-editor'; +import Editor from '~/ide/lib/editor'; import { file } from '../helpers'; describe('Multi-file editor library', () => { @@ -8,18 +7,14 @@ describe('Multi-file editor library', () => { let el; let holder; - beforeEach(done => { + beforeEach(() => { el = document.createElement('div'); holder = document.createElement('div'); el.appendChild(holder); document.body.appendChild(el); - monacoLoader(['vs/editor/editor.main'], () => { - instance = editor.create(monaco); - - done(); - }); + instance = Editor.create(); }); afterEach(() => { @@ -29,20 +24,20 @@ describe('Multi-file editor library', () => { }); it('creates instance of editor', () => { - expect(editor.editorInstance).not.toBeNull(); + expect(Editor.editorInstance).not.toBeNull(); }); it('creates instance returns cached instance', () => { - expect(editor.create(monaco)).toEqual(instance); + expect(Editor.create()).toEqual(instance); }); describe('createInstance', () => { it('creates editor instance', () => { - spyOn(instance.monaco.editor, 'create').and.callThrough(); + spyOn(monacoEditor, 'create').and.callThrough(); instance.createInstance(holder); - expect(instance.monaco.editor.create).toHaveBeenCalled(); + expect(monacoEditor.create).toHaveBeenCalled(); }); it('creates dirty diff controller', () => { @@ -60,11 +55,11 @@ describe('Multi-file editor library', () => { describe('createDiffInstance', () => { it('creates editor instance', () => { - spyOn(instance.monaco.editor, 'createDiffEditor').and.callThrough(); + spyOn(monacoEditor, 'createDiffEditor').and.callThrough(); instance.createDiffInstance(holder); - expect(instance.monaco.editor.createDiffEditor).toHaveBeenCalledWith(holder, { + expect(monacoEditor.createDiffEditor).toHaveBeenCalledWith(holder, { model: null, contextmenu: true, minimap: { @@ -268,4 +263,23 @@ describe('Multi-file editor library', () => { expect(instance.isDiffEditorType).toBe(false); }); }); + + it('sets quickSuggestions to false when language is markdown', () => { + instance.createInstance(holder); + + spyOn(instance.instance, 'updateOptions').and.callThrough(); + + const model = instance.createModel({ + ...file(), + key: 'index.md', + path: 'index.md', + }); + + instance.attachModel(model); + + expect(instance.instance.updateOptions).toHaveBeenCalledWith({ + readOnly: false, + quickSuggestions: false, + }); + }); }); diff --git a/spec/javascripts/ide/mock_data.js b/spec/javascripts/ide/mock_data.js index dcf857f7e04..dd87a43f370 100644 --- a/spec/javascripts/ide/mock_data.js +++ b/spec/javascripts/ide/mock_data.js @@ -75,6 +75,7 @@ export const jobs = [ }, stage: 'test', duration: 1, + started: new Date(), }, { id: 2, @@ -86,6 +87,7 @@ export const jobs = [ }, stage: 'test', duration: 1, + started: new Date(), }, { id: 3, @@ -97,6 +99,7 @@ export const jobs = [ }, stage: 'test', duration: 1, + started: new Date(), }, { id: 4, @@ -108,6 +111,7 @@ export const jobs = [ }, stage: 'build', duration: 1, + started: new Date(), }, ]; diff --git a/spec/javascripts/ide/monaco_loader_spec.js b/spec/javascripts/ide/monaco_loader_spec.js deleted file mode 100644 index 7ab315aa8c8..00000000000 --- a/spec/javascripts/ide/monaco_loader_spec.js +++ /dev/null @@ -1,15 +0,0 @@ -import monacoContext from 'monaco-editor/dev/vs/loader'; -import monacoLoader from '~/ide/monaco_loader'; - -describe('MonacoLoader', () => { - it('calls require.config and exports require', () => { - expect(monacoContext.require.getConfig()).toEqual( - jasmine.objectContaining({ - paths: { - vs: `${__webpack_public_path__}monaco-editor/vs`, // eslint-disable-line camelcase - }, - }), - ); - expect(monacoLoader).toBe(monacoContext.require); - }); -}); diff --git a/spec/javascripts/ide/stores/actions/file_spec.js b/spec/javascripts/ide/stores/actions/file_spec.js index 7bebc2288e3..5746683917e 100644 --- a/spec/javascripts/ide/stores/actions/file_spec.js +++ b/spec/javascripts/ide/stores/actions/file_spec.js @@ -166,12 +166,12 @@ describe('IDE store file actions', () => { }); it('resets location.hash for line highlighting', done => { - location.hash = 'test'; + window.location.hash = 'test'; store .dispatch('setFileActive', localFile.path) .then(() => { - expect(location.hash).not.toBe('test'); + expect(window.location.hash).not.toBe('test'); done(); }) diff --git a/spec/javascripts/ide/stores/modules/commit/actions_spec.js b/spec/javascripts/ide/stores/modules/commit/actions_spec.js index a2869ff378b..133ad627f34 100644 --- a/spec/javascripts/ide/stores/modules/commit/actions_spec.js +++ b/spec/javascripts/ide/stores/modules/commit/actions_spec.js @@ -108,77 +108,6 @@ describe('IDE commit module actions', () => { }); }); - describe('checkCommitStatus', () => { - beforeEach(() => { - store.state.currentProjectId = 'abcproject'; - store.state.currentBranchId = 'master'; - store.state.projects.abcproject = { - branches: { - master: { - workingReference: '1', - }, - }, - }; - }); - - it('calls service', done => { - spyOn(service, 'getBranchData').and.returnValue( - Promise.resolve({ - data: { - commit: { id: '123' }, - }, - }), - ); - - store - .dispatch('commit/checkCommitStatus') - .then(() => { - expect(service.getBranchData).toHaveBeenCalledWith('abcproject', 'master'); - - done(); - }) - .catch(done.fail); - }); - - it('returns true if current ref does not equal returned ID', done => { - spyOn(service, 'getBranchData').and.returnValue( - Promise.resolve({ - data: { - commit: { id: '123' }, - }, - }), - ); - - store - .dispatch('commit/checkCommitStatus') - .then(val => { - expect(val).toBeTruthy(); - - done(); - }) - .catch(done.fail); - }); - - it('returns false if current ref equals returned ID', done => { - spyOn(service, 'getBranchData').and.returnValue( - Promise.resolve({ - data: { - commit: { id: '1' }, - }, - }), - ); - - store - .dispatch('commit/checkCommitStatus') - .then(val => { - expect(val).toBeFalsy(); - - done(); - }) - .catch(done.fail); - }); - }); - describe('updateFilesAfterCommit', () => { const data = { id: '123', @@ -314,6 +243,7 @@ describe('IDE commit module actions', () => { ...file('changed'), type: 'blob', active: true, + lastCommitSha: '123456789', }; store.state.stagedFiles.push(f); store.state.changedFiles = [ @@ -366,6 +296,7 @@ describe('IDE commit module actions', () => { file_path: jasmine.anything(), content: jasmine.anything(), encoding: jasmine.anything(), + last_commit_id: undefined, }, ], start_branch: 'master', @@ -376,6 +307,32 @@ describe('IDE commit module actions', () => { .catch(done.fail); }); + it('sends lastCommit ID when not creating new branch', done => { + store.state.commit.commitAction = '1'; + + store + .dispatch('commit/commitChanges') + .then(() => { + expect(service.commit).toHaveBeenCalledWith('abcproject', { + branch: jasmine.anything(), + commit_message: 'testing 123', + actions: [ + { + action: 'update', + file_path: jasmine.anything(), + content: jasmine.anything(), + encoding: jasmine.anything(), + last_commit_id: '123456789', + }, + ], + start_branch: undefined, + }); + + done(); + }) + .catch(done.fail); + }); + it('sets last Commit Msg', done => { store .dispatch('commit/commitChanges') diff --git a/spec/javascripts/ide/stores/modules/merge_requests/actions_spec.js b/spec/javascripts/ide/stores/modules/merge_requests/actions_spec.js index b571cfb963a..fa4c18931e5 100644 --- a/spec/javascripts/ide/stores/modules/merge_requests/actions_spec.js +++ b/spec/javascripts/ide/stores/modules/merge_requests/actions_spec.js @@ -8,7 +8,9 @@ import actions, { receiveMergeRequestsSuccess, fetchMergeRequests, resetMergeRequests, + openMergeRequest, } from '~/ide/stores/modules/merge_requests/actions'; +import router from '~/ide/ide_router'; import { mergeRequests } from '../../../mock_data'; import testAction from '../../../../helpers/vuex_action_helper'; @@ -29,9 +31,9 @@ describe('IDE merge requests actions', () => { it('should should commit request', done => { testAction( requestMergeRequests, - null, + 'created', mockedState, - [{ type: types.REQUEST_MERGE_REQUESTS }], + [{ type: types.REQUEST_MERGE_REQUESTS, payload: 'created' }], [], done, ); @@ -48,16 +50,16 @@ describe('IDE merge requests actions', () => { it('should should commit error', done => { testAction( receiveMergeRequestsError, - null, + 'created', mockedState, - [{ type: types.RECEIVE_MERGE_REQUESTS_ERROR }], + [{ type: types.RECEIVE_MERGE_REQUESTS_ERROR, payload: 'created' }], [], done, ); }); it('creates flash message', () => { - receiveMergeRequestsError({ commit() {} }); + receiveMergeRequestsError({ commit() {} }, 'created'); expect(flashSpy).toHaveBeenCalled(); }); @@ -67,9 +69,14 @@ describe('IDE merge requests actions', () => { it('should commit received data', done => { testAction( receiveMergeRequestsSuccess, - 'data', + { type: 'created', data: 'data' }, mockedState, - [{ type: types.RECEIVE_MERGE_REQUESTS_SUCCESS, payload: 'data' }], + [ + { + type: types.RECEIVE_MERGE_REQUESTS_SUCCESS, + payload: { type: 'created', data: 'data' }, + }, + ], [], done, ); @@ -86,14 +93,14 @@ describe('IDE merge requests actions', () => { mock.onGet(/\/api\/v4\/merge_requests(.*)$/).replyOnce(200, mergeRequests); }); - it('calls API with params from state', () => { + it('calls API with params', () => { const apiSpy = spyOn(axios, 'get').and.callThrough(); - fetchMergeRequests({ dispatch() {}, state: mockedState }); + fetchMergeRequests({ dispatch() {}, state: mockedState }, { type: 'created' }); expect(apiSpy).toHaveBeenCalledWith(jasmine.anything(), { params: { - scope: 'assigned-to-me', + scope: 'created-by-me', state: 'opened', search: '', }, @@ -103,11 +110,14 @@ describe('IDE merge requests actions', () => { it('calls API with search', () => { const apiSpy = spyOn(axios, 'get').and.callThrough(); - fetchMergeRequests({ dispatch() {}, state: mockedState }, 'testing search'); + fetchMergeRequests( + { dispatch() {}, state: mockedState }, + { type: 'created', search: 'testing search' }, + ); expect(apiSpy).toHaveBeenCalledWith(jasmine.anything(), { params: { - scope: 'assigned-to-me', + scope: 'created-by-me', state: 'opened', search: 'testing search', }, @@ -117,7 +127,7 @@ describe('IDE merge requests actions', () => { it('dispatches request', done => { testAction( fetchMergeRequests, - null, + { type: 'created' }, mockedState, [], [ @@ -132,13 +142,16 @@ describe('IDE merge requests actions', () => { it('dispatches success with received data', done => { testAction( fetchMergeRequests, - null, + { type: 'created' }, mockedState, [], [ { type: 'requestMergeRequests' }, { type: 'resetMergeRequests' }, - { type: 'receiveMergeRequestsSuccess', payload: mergeRequests }, + { + type: 'receiveMergeRequestsSuccess', + payload: { type: 'created', data: mergeRequests }, + }, ], done, ); @@ -153,7 +166,7 @@ describe('IDE merge requests actions', () => { it('dispatches error', done => { testAction( fetchMergeRequests, - null, + { type: 'created' }, mockedState, [], [ @@ -171,12 +184,59 @@ describe('IDE merge requests actions', () => { it('commits reset', done => { testAction( resetMergeRequests, - null, + 'created', mockedState, - [{ type: types.RESET_MERGE_REQUESTS }], + [{ type: types.RESET_MERGE_REQUESTS, payload: 'created' }], [], done, ); }); }); + + describe('openMergeRequest', () => { + beforeEach(() => { + spyOn(router, 'push'); + }); + + it('commits reset mutations and actions', done => { + const commit = jasmine.createSpy(); + const dispatch = jasmine.createSpy().and.returnValue(Promise.resolve()); + openMergeRequest({ commit, dispatch }, { projectPath: 'gitlab-org/gitlab-ce', id: '1' }); + + setTimeout(() => { + expect(commit.calls.argsFor(0)).toEqual(['CLEAR_PROJECTS', null, { root: true }]); + expect(commit.calls.argsFor(1)).toEqual(['SET_CURRENT_MERGE_REQUEST', '1', { root: true }]); + expect(commit.calls.argsFor(2)).toEqual(['RESET_OPEN_FILES', null, { root: true }]); + + expect(dispatch.calls.argsFor(0)).toEqual(['setCurrentBranchId', '', { root: true }]); + expect(dispatch.calls.argsFor(1)).toEqual([ + 'pipelines/stopPipelinePolling', + null, + { root: true }, + ]); + expect(dispatch.calls.argsFor(2)).toEqual(['setRightPane', null, { root: true }]); + expect(dispatch.calls.argsFor(3)).toEqual([ + 'pipelines/resetLatestPipeline', + null, + { root: true }, + ]); + expect(dispatch.calls.argsFor(4)).toEqual([ + 'pipelines/clearEtagPoll', + null, + { root: true }, + ]); + + done(); + }); + }); + + it('pushes new route', () => { + openMergeRequest( + { commit() {}, dispatch: () => Promise.resolve() }, + { projectPath: 'gitlab-org/gitlab-ce', id: '1' }, + ); + + expect(router.push).toHaveBeenCalledWith('/project/gitlab-org/gitlab-ce/merge_requests/1'); + }); + }); }); diff --git a/spec/javascripts/ide/stores/modules/merge_requests/mutations_spec.js b/spec/javascripts/ide/stores/modules/merge_requests/mutations_spec.js index 664d3914564..ea03131d90d 100644 --- a/spec/javascripts/ide/stores/modules/merge_requests/mutations_spec.js +++ b/spec/javascripts/ide/stores/modules/merge_requests/mutations_spec.js @@ -12,26 +12,29 @@ describe('IDE merge requests mutations', () => { describe(types.REQUEST_MERGE_REQUESTS, () => { it('sets loading to true', () => { - mutations[types.REQUEST_MERGE_REQUESTS](mockedState); + mutations[types.REQUEST_MERGE_REQUESTS](mockedState, 'created'); - expect(mockedState.isLoading).toBe(true); + expect(mockedState.created.isLoading).toBe(true); }); }); describe(types.RECEIVE_MERGE_REQUESTS_ERROR, () => { it('sets loading to false', () => { - mutations[types.RECEIVE_MERGE_REQUESTS_ERROR](mockedState); + mutations[types.RECEIVE_MERGE_REQUESTS_ERROR](mockedState, 'created'); - expect(mockedState.isLoading).toBe(false); + expect(mockedState.created.isLoading).toBe(false); }); }); describe(types.RECEIVE_MERGE_REQUESTS_SUCCESS, () => { it('sets merge requests', () => { gon.gitlab_url = gl.TEST_HOST; - mutations[types.RECEIVE_MERGE_REQUESTS_SUCCESS](mockedState, mergeRequests); + mutations[types.RECEIVE_MERGE_REQUESTS_SUCCESS](mockedState, { + type: 'created', + data: mergeRequests, + }); - expect(mockedState.mergeRequests).toEqual([ + expect(mockedState.created.mergeRequests).toEqual([ { id: 1, iid: 1, @@ -47,9 +50,9 @@ describe('IDE merge requests mutations', () => { it('clears merge request array', () => { mockedState.mergeRequests = ['test']; - mutations[types.RESET_MERGE_REQUESTS](mockedState); + mutations[types.RESET_MERGE_REQUESTS](mockedState, 'created'); - expect(mockedState.mergeRequests).toEqual([]); + expect(mockedState.created.mergeRequests).toEqual([]); }); }); }); diff --git a/spec/javascripts/ide/stores/modules/pipelines/actions_spec.js b/spec/javascripts/ide/stores/modules/pipelines/actions_spec.js index f26eaf9c81f..f47e69d6e5b 100644 --- a/spec/javascripts/ide/stores/modules/pipelines/actions_spec.js +++ b/spec/javascripts/ide/stores/modules/pipelines/actions_spec.js @@ -13,9 +13,16 @@ import actions, { receiveJobsSuccess, fetchJobs, toggleStageCollapsed, + setDetailJob, + requestJobTrace, + receiveJobTraceError, + receiveJobTraceSuccess, + fetchJobTrace, + resetLatestPipeline, } from '~/ide/stores/modules/pipelines/actions'; import state from '~/ide/stores/modules/pipelines/state'; import * as types from '~/ide/stores/modules/pipelines/mutation_types'; +import { rightSidebarViews } from '~/ide/constants'; import testAction from '../../../../helpers/vuex_action_helper'; import { pipelines, jobs } from '../../../mock_data'; @@ -281,4 +288,149 @@ describe('IDE pipelines actions', () => { ); }); }); + + describe('setDetailJob', () => { + it('commits job', done => { + testAction( + setDetailJob, + 'job', + mockedState, + [{ type: types.SET_DETAIL_JOB, payload: 'job' }], + [{ type: 'setRightPane' }], + done, + ); + }); + + it('dispatches setRightPane as pipeline when job is null', done => { + testAction( + setDetailJob, + null, + mockedState, + [{ type: types.SET_DETAIL_JOB }], + [{ type: 'setRightPane', payload: rightSidebarViews.pipelines }], + done, + ); + }); + + it('dispatches setRightPane as job', done => { + testAction( + setDetailJob, + 'job', + mockedState, + [{ type: types.SET_DETAIL_JOB }], + [{ type: 'setRightPane', payload: rightSidebarViews.jobsDetail }], + done, + ); + }); + }); + + describe('requestJobTrace', () => { + it('commits request', done => { + testAction(requestJobTrace, null, mockedState, [{ type: types.REQUEST_JOB_TRACE }], [], done); + }); + }); + + describe('receiveJobTraceError', () => { + it('commits error', done => { + testAction( + receiveJobTraceError, + null, + mockedState, + [{ type: types.RECEIVE_JOB_TRACE_ERROR }], + [], + done, + ); + }); + + it('creates flash message', () => { + const flashSpy = spyOnDependency(actions, 'flash'); + + receiveJobTraceError({ commit() {} }); + + expect(flashSpy).toHaveBeenCalled(); + }); + }); + + describe('receiveJobTraceSuccess', () => { + it('commits data', done => { + testAction( + receiveJobTraceSuccess, + 'data', + mockedState, + [{ type: types.RECEIVE_JOB_TRACE_SUCCESS, payload: 'data' }], + [], + done, + ); + }); + }); + + describe('fetchJobTrace', () => { + beforeEach(() => { + mockedState.detailJob = { + path: `${gl.TEST_HOST}/project/builds`, + }; + }); + + describe('success', () => { + beforeEach(() => { + spyOn(axios, 'get').and.callThrough(); + mock.onGet(`${gl.TEST_HOST}/project/builds/trace`).replyOnce(200, { html: 'html' }); + }); + + it('dispatches request', done => { + testAction( + fetchJobTrace, + null, + mockedState, + [], + [ + { type: 'requestJobTrace' }, + { type: 'receiveJobTraceSuccess', payload: { html: 'html' } }, + ], + done, + ); + }); + + it('sends get request to correct URL', () => { + fetchJobTrace({ state: mockedState, dispatch() {} }); + + expect(axios.get).toHaveBeenCalledWith(`${gl.TEST_HOST}/project/builds/trace`, { + params: { format: 'json' }, + }); + }); + }); + + describe('error', () => { + beforeEach(() => { + mock.onGet(`${gl.TEST_HOST}/project/builds/trace`).replyOnce(500); + }); + + it('dispatches error', done => { + testAction( + fetchJobTrace, + null, + mockedState, + [], + [{ type: 'requestJobTrace' }, { type: 'receiveJobTraceError' }], + done, + ); + }); + }); + }); + + describe('resetLatestPipeline', () => { + it('commits reset mutations', done => { + testAction( + resetLatestPipeline, + null, + mockedState, + [ + { type: types.RECEIVE_LASTEST_PIPELINE_SUCCESS, payload: null }, + { type: types.SET_DETAIL_JOB, payload: null }, + ], + [], + done, + ); + }); + }); }); diff --git a/spec/javascripts/ide/stores/modules/pipelines/mutations_spec.js b/spec/javascripts/ide/stores/modules/pipelines/mutations_spec.js index 6285c01d483..eb7346bd5fc 100644 --- a/spec/javascripts/ide/stores/modules/pipelines/mutations_spec.js +++ b/spec/javascripts/ide/stores/modules/pipelines/mutations_spec.js @@ -147,6 +147,10 @@ describe('IDE pipelines mutations', () => { name: job.name, status: job.status, path: job.build_path, + rawPath: `${job.build_path}/raw`, + started: job.started, + isLoading: false, + output: '', })), ); }); @@ -171,4 +175,49 @@ describe('IDE pipelines mutations', () => { expect(mockedState.stages[0].isCollapsed).toBe(false); }); }); + + describe(types.SET_DETAIL_JOB, () => { + it('sets detail job', () => { + mutations[types.SET_DETAIL_JOB](mockedState, jobs[0]); + + expect(mockedState.detailJob).toEqual(jobs[0]); + }); + }); + + describe(types.REQUEST_JOB_TRACE, () => { + beforeEach(() => { + mockedState.detailJob = { ...jobs[0] }; + }); + + it('sets loading on detail job', () => { + mutations[types.REQUEST_JOB_TRACE](mockedState); + + expect(mockedState.detailJob.isLoading).toBe(true); + }); + }); + + describe(types.RECEIVE_JOB_TRACE_ERROR, () => { + beforeEach(() => { + mockedState.detailJob = { ...jobs[0], isLoading: true }; + }); + + it('sets loading to false on detail job', () => { + mutations[types.RECEIVE_JOB_TRACE_ERROR](mockedState); + + expect(mockedState.detailJob.isLoading).toBe(false); + }); + }); + + describe(types.RECEIVE_JOB_TRACE_SUCCESS, () => { + beforeEach(() => { + mockedState.detailJob = { ...jobs[0], isLoading: true }; + }); + + it('sets output on detail job', () => { + mutations[types.RECEIVE_JOB_TRACE_SUCCESS](mockedState, { html: 'html' }); + + expect(mockedState.detailJob.output).toBe('html'); + expect(mockedState.detailJob.isLoading).toBe(false); + }); + }); }); diff --git a/spec/javascripts/ide/stores/mutations/file_spec.js b/spec/javascripts/ide/stores/mutations/file_spec.js index e83961fcedc..52f83be8e8c 100644 --- a/spec/javascripts/ide/stores/mutations/file_spec.js +++ b/spec/javascripts/ide/stores/mutations/file_spec.js @@ -152,6 +152,53 @@ describe('IDE store file mutations', () => { expect(localFile.mrChange.diff).toBe('ABC'); }); + + it('has diffMode replaced by default', () => { + mutations.SET_FILE_MERGE_REQUEST_CHANGE(localState, { + file: localFile, + mrChange: { + diff: 'ABC', + }, + }); + + expect(localFile.mrChange.diffMode).toBe('replaced'); + }); + + it('has diffMode new', () => { + mutations.SET_FILE_MERGE_REQUEST_CHANGE(localState, { + file: localFile, + mrChange: { + diff: 'ABC', + new_file: true, + }, + }); + + expect(localFile.mrChange.diffMode).toBe('new'); + }); + + it('has diffMode deleted', () => { + mutations.SET_FILE_MERGE_REQUEST_CHANGE(localState, { + file: localFile, + mrChange: { + diff: 'ABC', + deleted_file: true, + }, + }); + + expect(localFile.mrChange.diffMode).toBe('deleted'); + }); + + it('has diffMode renamed', () => { + mutations.SET_FILE_MERGE_REQUEST_CHANGE(localState, { + file: localFile, + mrChange: { + diff: 'ABC', + renamed_file: true, + }, + }); + + expect(localFile.mrChange.diffMode).toBe('renamed'); + }); }); describe('DISCARD_FILE_CHANGES', () => { diff --git a/spec/javascripts/ide/stores/utils_spec.js b/spec/javascripts/ide/stores/utils_spec.js index f38ac6dd82f..a7bd443af51 100644 --- a/spec/javascripts/ide/stores/utils_spec.js +++ b/spec/javascripts/ide/stores/utils_spec.js @@ -1,4 +1,5 @@ import * as utils from '~/ide/stores/utils'; +import { file } from '../helpers'; describe('Multi-file store utils', () => { describe('setPageTitle', () => { @@ -63,4 +64,59 @@ describe('Multi-file store utils', () => { expect(foundEntry).toBeUndefined(); }); }); + + describe('createCommitPayload', () => { + it('returns API payload', () => { + const state = { + commitMessage: 'commit message', + }; + const rootState = { + stagedFiles: [ + { + ...file('staged'), + path: 'staged', + content: 'updated file content', + lastCommitSha: '123456789', + }, + { + ...file('newFile'), + path: 'added', + tempFile: true, + content: 'new file content', + base64: true, + lastCommitSha: '123456789', + }, + ], + currentBranchId: 'master', + }; + const payload = utils.createCommitPayload({ + branch: 'master', + newBranch: false, + state, + rootState, + }); + + expect(payload).toEqual({ + branch: 'master', + commit_message: 'commit message', + actions: [ + { + action: 'update', + file_path: 'staged', + content: 'updated file content', + encoding: 'text', + last_commit_id: '123456789', + }, + { + action: 'create', + file_path: 'added', + content: 'new file content', + encoding: 'base64', + last_commit_id: '123456789', + }, + ], + start_branch: undefined, + }); + }); + }); }); diff --git a/spec/javascripts/importer_status_spec.js b/spec/javascripts/importer_status_spec.js index 0575d02886d..63cdb3d5114 100644 --- a/spec/javascripts/importer_status_spec.js +++ b/spec/javascripts/importer_status_spec.js @@ -45,7 +45,25 @@ describe('Importer Status', () => { currentTarget: document.querySelector('.js-add-to-import'), }) .then(() => { - expect(document.querySelector('tr').classList.contains('active')).toEqual(true); + expect(document.querySelector('tr').classList.contains('table-active')).toEqual(true); + done(); + }) + .catch(done.fail); + }); + + it('shows error message after failed POST request', (done) => { + appendSetFixtures('<div class="flash-container"></div>'); + + mock.onPost(importUrl).reply(422, { + errors: 'You forgot your lunch', + }); + + instance.addToImport({ + currentTarget: document.querySelector('.js-add-to-import'), + }) + .then(() => { + const flashMessage = document.querySelector('.flash-text'); + expect(flashMessage.textContent.trim()).toEqual('An error occurred while importing project: You forgot your lunch'); done(); }) .catch(done.fail); diff --git a/spec/javascripts/issue_show/components/app_spec.js b/spec/javascripts/issue_show/components/app_spec.js index bf1f0c822fe..eb5e0bddb74 100644 --- a/spec/javascripts/issue_show/components/app_spec.js +++ b/spec/javascripts/issue_show/components/app_spec.js @@ -145,7 +145,7 @@ describe('Issuable output', () => { resolve({ data: { confidential: false, - web_url: location.pathname, + web_url: window.location.pathname, }, }); })); @@ -177,7 +177,7 @@ describe('Issuable output', () => { spyOn(vm.service, 'updateIssuable').and.callFake(() => new Promise((resolve) => { resolve({ data: { - web_url: location.pathname, + web_url: window.location.pathname, confidential: vm.isConfidential, }, }); diff --git a/spec/javascripts/jobs/header_spec.js b/spec/javascripts/jobs/header_spec.js index 4f861c39d3f..cef30a007db 100644 --- a/spec/javascripts/jobs/header_spec.js +++ b/spec/javascripts/jobs/header_spec.js @@ -13,6 +13,9 @@ describe('Job details header', () => { const threeWeeksAgo = new Date(); threeWeeksAgo.setDate(threeWeeksAgo.getDate() - 21); + const twoDaysAgo = new Date(); + twoDaysAgo.setDate(twoDaysAgo.getDate() - 2); + props = { job: { status: { @@ -31,7 +34,7 @@ describe('Job details header', () => { email: 'foo@bar.com', avatar_url: 'link', }, - started: '2018-01-08T09:48:27.319Z', + started: twoDaysAgo.toISOString(), new_issue_path: 'path', }, isLoading: false, @@ -69,7 +72,7 @@ describe('Job details header', () => { .querySelector('.header-main-content') .textContent.replace(/\s+/g, ' ') .trim(), - ).toEqual('failed Job #123 triggered 3 weeks ago by Foo'); + ).toEqual('failed Job #123 triggered 2 days ago by Foo'); }); it('should render new issue link', () => { diff --git a/spec/javascripts/jobs/mock_data.js b/spec/javascripts/jobs/mock_data.js index 25ca8eb6c0b..dd025255bd1 100644 --- a/spec/javascripts/jobs/mock_data.js +++ b/spec/javascripts/jobs/mock_data.js @@ -20,7 +20,7 @@ export default { group: 'success', has_details: true, details_path: '/root/ci-mock/-/jobs/4757', - favicon: '/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico', + favicon: '/assets/ci_favicons/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.png', action: { icon: 'retry', title: 'Retry', @@ -78,7 +78,7 @@ export default { group: 'success', has_details: true, details_path: '/root/ci-mock/pipelines/140', - favicon: '/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico', + favicon: '/assets/ci_favicons/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.png', }, duration: 6, finished_at: '2017-06-01T17:32:00.042Z', diff --git a/spec/javascripts/labels_select_spec.js b/spec/javascripts/labels_select_spec.js index a2b89c0aef5..386e00bfd0c 100644 --- a/spec/javascripts/labels_select_spec.js +++ b/spec/javascripts/labels_select_spec.js @@ -40,5 +40,9 @@ describe('LabelsSelect', () => { it('generated label item template has correct label styles', () => { expect($labelEl.find('span.label').attr('style')).toBe(`background-color: ${label.color}; color: ${label.text_color};`); }); + + it('generated label item has a badge class', () => { + expect($labelEl.find('span').hasClass('badge')).toEqual(true); + }); }); }); diff --git a/spec/javascripts/lib/utils/common_utils_spec.js b/spec/javascripts/lib/utils/common_utils_spec.js index 27f06573432..a9ec7f42a9d 100644 --- a/spec/javascripts/lib/utils/common_utils_spec.js +++ b/spec/javascripts/lib/utils/common_utils_spec.js @@ -2,6 +2,7 @@ import axios from '~/lib/utils/axios_utils'; import * as commonUtils from '~/lib/utils/common_utils'; import MockAdapter from 'axios-mock-adapter'; +import { faviconDataUrl, overlayDataUrl, faviconWithOverlayDataUrl } from './mock_data'; describe('common_utils', () => { describe('parseUrl', () => { @@ -39,13 +40,13 @@ describe('common_utils', () => { }); it('should decode params', () => { - history.pushState('', '', '?label_name%5B%5D=test'); + window.history.pushState('', '', '?label_name%5B%5D=test'); expect( commonUtils.getUrlParamsArray()[0], ).toBe('label_name[]=test'); - history.pushState('', '', '?'); + window.history.pushState('', '', '?'); }); }); @@ -395,6 +396,7 @@ describe('common_utils', () => { const favicon = document.createElement('link'); favicon.setAttribute('id', 'favicon'); favicon.setAttribute('href', 'default/favicon'); + favicon.setAttribute('data-default-href', 'default/favicon'); document.body.appendChild(favicon); }); @@ -413,7 +415,7 @@ describe('common_utils', () => { beforeEach(() => { const favicon = document.createElement('link'); favicon.setAttribute('id', 'favicon'); - favicon.setAttribute('href', 'default/favicon'); + favicon.setAttribute('data-original-href', 'default/favicon'); document.body.appendChild(favicon); }); @@ -421,12 +423,43 @@ describe('common_utils', () => { document.body.removeChild(document.getElementById('favicon')); }); - it('should reset page favicon to tanuki', () => { + it('should reset page favicon to the default icon', () => { + const favicon = document.getElementById('favicon'); + favicon.setAttribute('href', 'new/favicon'); commonUtils.resetFavicon(); expect(document.getElementById('favicon').getAttribute('href')).toEqual('default/favicon'); }); }); + describe('createOverlayIcon', () => { + it('should return the favicon with the overlay', (done) => { + commonUtils.createOverlayIcon(faviconDataUrl, overlayDataUrl).then((url) => { + expect(url).toEqual(faviconWithOverlayDataUrl); + done(); + }); + }); + }); + + describe('setFaviconOverlay', () => { + beforeEach(() => { + const favicon = document.createElement('link'); + favicon.setAttribute('id', 'favicon'); + favicon.setAttribute('data-original-href', faviconDataUrl); + document.body.appendChild(favicon); + }); + + afterEach(() => { + document.body.removeChild(document.getElementById('favicon')); + }); + + it('should set page favicon to provided favicon overlay', (done) => { + commonUtils.setFaviconOverlay(overlayDataUrl).then(() => { + expect(document.getElementById('favicon').getAttribute('href')).toEqual(faviconWithOverlayDataUrl); + done(); + }); + }); + }); + describe('setCiStatusFavicon', () => { const BUILD_URL = `${gl.TEST_HOST}/frontend-fixtures/builds-project/-/jobs/1/status.json`; let mock; @@ -434,6 +467,8 @@ describe('common_utils', () => { beforeEach(() => { const favicon = document.createElement('link'); favicon.setAttribute('id', 'favicon'); + favicon.setAttribute('href', 'null'); + favicon.setAttribute('data-original-href', faviconDataUrl); document.body.appendChild(favicon); mock = new MockAdapter(axios); }); @@ -449,7 +484,7 @@ describe('common_utils', () => { commonUtils.setCiStatusFavicon(BUILD_URL) .then(() => { const favicon = document.getElementById('favicon'); - expect(favicon.getAttribute('href')).toEqual('null'); + expect(favicon.getAttribute('href')).toEqual(faviconDataUrl); done(); }) // Error is already caught in catch() block of setCiStatusFavicon, @@ -458,16 +493,14 @@ describe('common_utils', () => { }); it('should set page favicon to CI status favicon based on provided status', (done) => { - const FAVICON_PATH = '//icon_status_success'; - mock.onGet(BUILD_URL).reply(200, { - favicon: FAVICON_PATH, + favicon: overlayDataUrl, }); commonUtils.setCiStatusFavicon(BUILD_URL) .then(() => { const favicon = document.getElementById('favicon'); - expect(favicon.getAttribute('href')).toEqual(FAVICON_PATH); + expect(favicon.getAttribute('href')).toEqual(faviconWithOverlayDataUrl); done(); }) .catch(done.fail); diff --git a/spec/javascripts/lib/utils/mock_data.js b/spec/javascripts/lib/utils/mock_data.js new file mode 100644 index 00000000000..fd0d62b751f --- /dev/null +++ b/spec/javascripts/lib/utils/mock_data.js @@ -0,0 +1,5 @@ +export const faviconDataUrl = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAACcFBMVEX////iQyniQyniQyniQyniQyniQyniQyniQynhRiriQyniQyniQyniQyniQyniQyniQyniQyniQyniQyniQyniQyniQyniQyniQyniQyniQyniRCniQyniQyniQyniQyniQyniQyniQyniQyniQyniQyniQyniQyniQyniQynhQiniQiniQiniQinhQinpUSjqUSjqTyjqTyjqTyjlSCniRCniQynjRCjqTyjsZSjrWyj8oib9kSb8pyb9pib8oyb8fyb3ZSb4Zib8fCb8oyb8oyb8oyb8pCb8cSbiQyn7bCb8cib8oyb8oSb8bSbtVSjpTij8nyb8oyb8oyb8lCb2Yyf3ZCf8mCb8oyb8oyb8oyb8iib8bSbiRCn8gyb8oyb8eCbpTinrUSj8oyb8oyb8oyb8pSb8bib4Zif0YCf8byb8oyb8oyb8oyb7oib8oyb8nCbjRSn9bib8ayb8nib8oyb8oyb8oyb8kSbpTyjpTyj8jib8oyb8oyb8oyb8fib0Xyf2ZSb8gCb8oyb6pSb8oyb8dib+cCbgQCnjRSn8cCb8oib8oyb8oyb8oybqUCjnSyn8bCb8oyb8oyb8oyb8myb2YyfyXyf8oyb8oyb8hibhQSn+bib8iSb8oyb8qCb+fSbmSSnqTyj8oib9pCb1YifxXyf7pSb8oCb8pCb+mCb0fCf8pSb7hSXvcSjiQyniQinqTyj9kCb9bib9byb+cCbqUSjiRCnsVCj+cSb8pib8bCb8bSbgQCn7bCb8bibjRSn8oyb8ayb8oib8aib8pCbjRCn8pybhQinhQSn8pSb7ayb7aSb6aib8eib///8IbM+7AAAAr3RSTlMBA3NtX2vT698HGQcRLwWLiXnv++3V+eEd/R8HE2V/Y5HjyefdFw99YWfJ+/3nwQP78/HvX1VTQ/kdA2HzbQXj9fX79/3DGf379/33T/v99/f7ba33+/f1+9/18/v59V339flzF/H9+fX3/fMhBwOh9/v5/fmvBV/z+fP3Awnp9/f38+UFgff7+/37+4c77/f7/flFz/f59dFr7/v98Wnr+/f3I5/197EDBU1ZAwUD8/kLUwAAAAFiS0dEAIgFHUgAAAAHdElNRQfhBQoLHiBV6/1lAAACHUlEQVQ4y41TZXsTQRCe4FAIUigN7m7FXY+iLRQKBG2x4g7BjhZ3Le7uMoEkFJprwyQk0CC/iZnNhUZaHt4vt6/szO7cHcD/wFKjZrJWq3YMq1M3eVc9rFzXR2yQkuA3RGxkjZLGiEk9miA2tURJs1RsnhhokYYtzaU13WZDbBVnW1sjo43J2vI6tZ0lLtFeAh1M0lECneI7dGYtrUtk3RUVIKaEJR25qw27yT0s3W0qEHuPlB4RradivXo7GX36xnbo51SQ+fWHARmCgYMGDxkaxbD3SssYPmIkwKgPLrfA87EETTg/fVaSa/SYsQDjSsd7DcGEsr+BieVKmaRNBsjUtClTfUI900y/5Mt05c8oJQKYSURZ2UqYFa0w283M588JEM2BuRwI5EqT8nmmXzZf4l8XsGNfCIv4QcHFklhiBpaqAsuC4tghj+ySyOdjeJYrP7RCCuR/E5tWAqxaLcmCNSyujdxjHZdbn8UHoA0bN/GoNm8hjQJb/ZzYpo6w3TB27JRduxxqrA7YzbWCezixN8RD2Oc2/Ptlfx7o5uT1A4XMiwzj4HfEikNe7+Ew0ZGjeuW70eEYaeHjxomTiKd++E4XnKGz8d+HDufOB3Ky3RcwdNF1qZiKLyf/B44r2tWf15wV143cwI2qfi8dbtKtX6Hbd+6G74EDqkTm/QcPH/0ufFyNLXjy9NnzF9Xb8BJevYY38C+8fZcg/AF3QTYemVkCwwAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAxNy0wNS0xMFQxMTozMDozMiswMjowMMzup8UAAAAldEVYdGRhdGU6bW9kaWZ5ADIwMTctMDUtMTBUMTE6MzA6MzIrMDI6MDC9sx95AAAAAElFTkSuQmCC'; + +export const overlayDataUrl = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAA85JREFUWAntVllIVGEUPv/9b46O41KplYN7PeRkti8TjQlhCUGh3MmeQugpIsGKAi2soIcIooiohxYKK2daqDAlIpIiWwxtQaJcaHE0d5tMrbn37z9XRqfR0TvVW56Hudf//uec72zfEWBCJjIwkYGJDPzvGSD/KgExN3Oi2Q+2DJgSDYQEMwItVGH1iZGmJw/Si1y+/PwVAMYYib22MYc/8hVQFgKDEfYoId0KYzagAQebsos/ewMZoeB9wdffcTYpQSaCTWHKoqSQaDk7zkIt0+aCUR8BelEHrf3dUNv9AcqbnsHtT5UKB/hTASh0SLYjnjb/CIDRJi0XiFAaJOpCD8zLpdb4NB66b1OfelthX815dtdRRfiti2aAXLvVLiMQ6olGyztGDkSo4JGGXk8/QFdGpYzpHG2GBQTDhtgVhPEaVbbVpvI6GJz22rv4TcAfrYI1x7Rj5MWWAppomKFVVb2302SFzUkZHAbkG+0b1+Gh77yNYjrmqnWTrLBLRxdvBWv8qlFujH/kYjJYyvLkj71t78zAUvzMAMnHhpN4zf9UREJhd8omyssxu1IgazQDwDnHUcNuH6vhPIE1fmuBzHt74Hn7W89jWGtcAjoaIDOFrdcMYJBkgOCoaRF0Lj0oglddDbCj6tRvKjphEpgjkzEQs2YAKsNxMzjn3nKurhzK+Ly7xe28ua8TwgMMcHJZnvvT0BPtEEKM4tDJ+C8GvIIk4ylINIXVZ0EUKJxYuh3mhCeokbudl6TtVc88dfBdLwbyaWB6zQCYQJpBYSrDGQxBQ/ZWRM2B+VNmQnVnHWx7elyNuL2/R336co7KyJR8CL9oLgEuFlREevWUkEl6uGwpVEG4FBm0OEf9N10NMgPlvWYAuNVwsWDKvcUNYsHUWTCZ13ysyFEXe6TO6aC8CUr9IiK+A05TQrc8yjwmxARHeeMAPlfQJw+AQRwu0YhL/GDXi9NwufG+S8dYkuYMqIb4SsWthotlNMOUCOM6r+G9cqXxPmd1dqrBav/o1zJy2l5/NUjJA/VORwYuFnOUaTQcPs9wMqwV++Xv8oADxKAcZ8nLPr8AoGW+xR6HSqYk3GodAz2QNj0V+Gr26dT9ASNH5239Pf0gktVNWZca8ZvfAFBprWS6hSu1pqt++Y0PD+WIwDAhIWQGtzvSHDbcodfFUFB9hg1Gjs5LXqIdFL+acFBl+FddqYwdxsWC3I70OvgfUaA65zhq2O2c8VxYcyIGFTVlXegYtvCXANCQZJMobjVcLMjtSK/IcEgyOOe8Ve5w7ryKDefp2P3+C/5ohv8HZmVLAAAAAElFTkSuQmCC'; + +export const faviconWithOverlayDataUrl = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAGt0lEQVRYR8WWf3DT5R3H35/vN0lDiCztSuiPAEnTFhSOUamIzFGokwMH55g0E845yjbP6+4qIoiHY6JjnHLeRI6h9jgQpQcD3ImH2u00eHBwjGthKLI1TSm26S8oKYVS0vT7/X52z7ckTUhqC/yx55/kks/zeb+ez6/nIWYm/B8XJQB4SGq6lL+CJA47vvRtvWs2D0nNl/Kf0qBZxx6u23arv0QAAIHivK8BynB4ffa7BgDQXJx/ngGnw+uThgTwP5ZnMocoJAxVxZDVZ+0L5n5WF75TkMafjLdJxpSg2E+gqW1X7zk3rbpaifhLiEBgTv4mEFbpBoTyu01DoDj/dQAv9rvjtdnp/k3Yx9rgAMV5QYCsAAwAgg6vL/1OTy/2BYrzzwLIBWACuNHhrXPG+otGoKaw0JA58kqGJtOFfgPS8yWT4sz88nzj7UIIfz+wd0mRdEZPLMnp2V/8R0+JrhLbBYFHJvwWzBUxYgqYNzhG+zfEhm24MIE5ectBtP0W+y0Or29FcoDifHFSRxwAcMrh9c0YrmisXaA4r0V0U8xvopgDDq9PpCQ+Ag0/zbEbNUNbMiG9fTwkDTsKHpJa2t1Zmiw1AqLg+tMZ+R6WVVtnZ2qP6Ib+FIjh05G3lsDrB4xjUIbRDeM+WZLJYZ4B1rKMzKPm/fdyzs9qg6WT225IMnPcuYjxbPZhn57qaA00zc4/QYT7b1b/wAZmDYSLjsN1WcmiM+6jXz7JTCs1aNPASBjrtrCGOXVBLK9ph72772bc0REZcsQlkEVoOxblhaFBH0Bxi6GBWFNC8gpV0XqYSe/hI85R9o1zxr/QaZbdbmuW9oRzljRrzBRkW9JhMaTgYugKzl35DlXNJ/Fp43FImoZnz7T0ln7bLihM0g85N627vkWPgLrbvYyCvAP1+rRIWETA5QsyQlcJYOCbMRasWpALtljwSsFyeJxFYsoNWqdN1y/ildM78Y/WGjxx8TL+ol3oluy8VupKe7cfoNLdCJkdqEUPOmBJ5ksJoae91mBps5lQ6pkIm20MPiz6A3KsmcNukDe/3Ye3zh3A77Q2XqcGjslLz88i/nB8pkpSoL8nAFSTBpUN4qSxS5KB5jOGUOniCebmzFQcevSN2xKP+Fp7ajt21f8TOxU/5i45JZFS6XwcTB9HxZgUnGTRNgk31x5jet+aGU7jWw+UweOcPeyTxxoqrGL25+UwdjehSvnmOVIqcz4C8y8GAABcQwjnYI5NheikhQWT+EZmDh2ev/l7cz4U2cGmYyg78TYqVH87Kbtd1wFY4hsVQAt14zu2RiDaTUZMf/BHWD35STx37wDv94k1dLeh7MRmvDZ1GR5Inxg17dX6MPnjZfh5X6tGSqXrV2B8ACIx98UNGOlV4CxCuA6zqIeq9FQ8c68bhx7ZiIK06CQdVF+Il3y1Hq03gnDfk4Uj8zbH2T51dCPOtlW39Q+iPTl2VSMfwKPiKw8aTuhgpl1Zdqxzj8PphRWwm21xZjv9VcgYkYb52dP132PFbSYr/la0DpNtrrg9a2oqsKfB2zlwG+4nSe1z7QDjaQBi2Eh6J4QRwimYt43LwOsuB2oX7YLVMCLqTAya3xx/EwZJxtYHy3WhyMkHExebXz3zAbbXfdo7AFBRaMAz1Ypa6XoaoPejKRGteZm6D3SlWVdOcOHo/Lfj2u9aXw+WHNmA00G/DiFEO0Jd+meyk0fIf/+vLfik6Xhj4qN0v7i5HCY1bBQPk+ij9GSzNbzYNdH03kMrscARfzvHQgiBocSFTVHVCrW+u+WrpK9iCIgS1rRK93oG/1GkRJVIup8KMNs1Sw/1rUtALD36ZzRca8XeJDmPtRc18vDn5SCJViYHENY3IZTK3JkE7RAYtpdkp3bAaJeOzN+CsSMTX+wqa7ih9sbVSLI2WV3znihAJYXZPThA7M6KQoM2MniyhUxTioxTpKLMadjx8Jqh5k3S//8d9GOh92XWmP/aXLKvfHgA0ZTklL0jj9m6UR6L5+9bjFWTPLcFIWbCY1+8pHb0drWybJ4aWLQrODyAWJndzoyylNyGg0hL+bV7Ll4rKIWB5CFBxMlLj21SL4W6QjDQjwOL9n4tNt0+AADPfo+UqgXPHJLSJrkso7F6ylLMy56OFMmYACIKblvtQext8Iqp0swyLYiI3zEAbs6Ml3cXv/p3Y+ryq5KcnSKb1Jmj75P7X0Rm/UV0tvO86r/WIhORwszvkmHEehH2WMo7ikDUQUWhoaIG+NNc96Os8eMEmklE2Qy2ANTO0OrA+CwFOFBfsq8pWZ7+B25aDBxvPp+QAAAAAElFTkSuQmCC'; diff --git a/spec/javascripts/lib/utils/url_utility_spec.js b/spec/javascripts/lib/utils/url_utility_spec.js new file mode 100644 index 00000000000..c7f4092911c --- /dev/null +++ b/spec/javascripts/lib/utils/url_utility_spec.js @@ -0,0 +1,29 @@ +import { webIDEUrl } from '~/lib/utils/url_utility'; + +describe('URL utility', () => { + describe('webIDEUrl', () => { + afterEach(() => { + gon.relative_url_root = ''; + }); + + describe('without relative_url_root', () => { + it('returns IDE path with route', () => { + expect(webIDEUrl('/gitlab-org/gitlab-ce/merge_requests/1')).toBe( + '/-/ide/project/gitlab-org/gitlab-ce/merge_requests/1', + ); + }); + }); + + describe('with relative_url_root', () => { + beforeEach(() => { + gon.relative_url_root = '/gitlab'; + }); + + it('returns IDE path with route', () => { + expect(webIDEUrl('/gitlab/gitlab-org/gitlab-ce/merge_requests/1')).toBe( + '/gitlab/-/ide/project/gitlab-org/gitlab-ce/merge_requests/1', + ); + }); + }); + }); +}); diff --git a/spec/javascripts/monitoring/graph_spec.js b/spec/javascripts/monitoring/graph_spec.js index 220228e5c08..a46a387a534 100644 --- a/spec/javascripts/monitoring/graph_spec.js +++ b/spec/javascripts/monitoring/graph_spec.js @@ -18,9 +18,7 @@ const createComponent = propsData => { }).$mount(); }; -const convertedMetrics = convertDatesMultipleSeries( - singleRowMetricsMultipleSeries, -); +const convertedMetrics = convertDatesMultipleSeries(singleRowMetricsMultipleSeries); describe('Graph', () => { beforeEach(() => { @@ -36,7 +34,7 @@ describe('Graph', () => { projectPath, }); - expect(component.$el.querySelector('.text-center').innerText.trim()).toBe( + expect(component.$el.querySelector('.prometheus-graph-title').innerText.trim()).toBe( component.graphData.title, ); }); @@ -52,9 +50,7 @@ describe('Graph', () => { }); const transformedHeight = `${component.graphHeight - 100}`; - expect(component.axisTransform.indexOf(transformedHeight)).not.toEqual( - -1, - ); + expect(component.axisTransform.indexOf(transformedHeight)).not.toEqual(-1); }); it('outerViewBox gets a width and height property based on the DOM size of the element', () => { diff --git a/spec/javascripts/notes/components/comment_form_spec.js b/spec/javascripts/notes/components/comment_form_spec.js index 224debbeff6..a7d1e4331eb 100644 --- a/spec/javascripts/notes/components/comment_form_spec.js +++ b/spec/javascripts/notes/components/comment_form_spec.js @@ -84,7 +84,7 @@ describe('issue_comment_form component', () => { it('should render textarea with placeholder', () => { expect( vm.$el.querySelector('.js-main-target-form textarea').getAttribute('placeholder'), - ).toEqual('Write a comment or drag your files here...'); + ).toEqual('Write a comment or drag your files here…'); }); it('should make textarea disabled while requesting', (done) => { diff --git a/spec/javascripts/notes/components/note_actions_spec.js b/spec/javascripts/notes/components/note_actions_spec.js index 1dfe890e05e..c9e549d2096 100644 --- a/spec/javascripts/notes/components/note_actions_spec.js +++ b/spec/javascripts/notes/components/note_actions_spec.js @@ -20,7 +20,7 @@ describe('issue_note_actions component', () => { beforeEach(() => { props = { - accessLevel: 'Master', + accessLevel: 'Maintainer', authorId: 26, canDelete: true, canEdit: true, @@ -67,7 +67,7 @@ describe('issue_note_actions component', () => { beforeEach(() => { store.dispatch('setUserData', {}); props = { - accessLevel: 'Master', + accessLevel: 'Maintainer', authorId: 26, canDelete: false, canEdit: false, diff --git a/spec/javascripts/notes/components/note_app_spec.js b/spec/javascripts/notes/components/note_app_spec.js index 0e792eee5e9..d494c63ff11 100644 --- a/spec/javascripts/notes/components/note_app_spec.js +++ b/spec/javascripts/notes/components/note_app_spec.js @@ -106,7 +106,7 @@ describe('note_app', () => { expect(vm.$el.querySelector('.js-main-target-form').tagName).toEqual('FORM'); expect( vm.$el.querySelector('.js-main-target-form textarea').getAttribute('placeholder'), - ).toEqual('Write a comment or drag your files here...'); + ).toEqual('Write a comment or drag your files here…'); }); it('should render form comment button as disabled', () => { @@ -129,7 +129,7 @@ describe('note_app', () => { expect(vm.$el.querySelector('.js-main-target-form').tagName).toEqual('FORM'); expect( vm.$el.querySelector('.js-main-target-form textarea').getAttribute('placeholder'), - ).toEqual('Write a comment or drag your files here...'); + ).toEqual('Write a comment or drag your files here…'); }); }); diff --git a/spec/javascripts/notes/components/note_form_spec.js b/spec/javascripts/notes/components/note_form_spec.js index f841a408d09..413d4f69434 100644 --- a/spec/javascripts/notes/components/note_form_spec.js +++ b/spec/javascripts/notes/components/note_form_spec.js @@ -49,7 +49,7 @@ describe('issue_note_form component', () => { it('should render text area with placeholder', () => { expect( vm.$el.querySelector('textarea').getAttribute('placeholder'), - ).toEqual('Write a comment or drag your files here...'); + ).toEqual('Write a comment or drag your files here…'); }); it('should link to markdown docs', () => { diff --git a/spec/javascripts/notes/mock_data.js b/spec/javascripts/notes/mock_data.js index bfe3a65feee..fa7adc32193 100644 --- a/spec/javascripts/notes/mock_data.js +++ b/spec/javascripts/notes/mock_data.js @@ -340,6 +340,79 @@ export const loggedOutnoteableData = { '/gitlab-org/gitlab-ce/preview_markdown?quick_actions_target_id=98&quick_actions_target_type=Issue', }; +export const collapseNotesMock = [ + { + expanded: true, + id: '0fb4e0e3f9276e55ff32eb4195add694aece4edd', + individual_note: true, + notes: [ + { + id: 1390, + attachment: null, + author: { + id: 1, + name: 'Root', + username: 'root', + state: 'active', + avatar_url: 'test', + path: '/root', + }, + created_at: '2018-02-26T18:07:41.071Z', + updated_at: '2018-02-26T18:07:41.071Z', + system: true, + system_note_icon_name: 'pencil', + noteable_id: 98, + noteable_type: 'Issue', + type: null, + human_access: 'Owner', + note: 'changed the description', + note_html: '<p dir="auto">changed the description</p>', + current_user: { can_edit: false }, + discussion_id: 'b97fb7bda470a65b3e009377a9032edec0a4dd05', + emoji_awardable: false, + path: '/h5bp/html5-boilerplate/notes/1057', + report_abuse_path: + '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fh5bp%2Fhtml5-boilerplate%2Fissues%2F10%23note_1057&user_id=1', + }, + ], + }, + { + expanded: true, + id: 'ffde43f25984ad7f2b4275135e0e2846875336c0', + individual_note: true, + notes: [ + { + id: 1391, + attachment: null, + author: { + id: 1, + name: 'Root', + username: 'root', + state: 'active', + avatar_url: 'test', + path: '/root', + }, + created_at: '2018-02-26T18:13:24.071Z', + updated_at: '2018-02-26T18:13:24.071Z', + system: true, + system_note_icon_name: 'pencil', + noteable_id: 99, + noteable_type: 'Issue', + type: null, + human_access: 'Owner', + note: 'changed the description', + note_html: '<p dir="auto">changed the description</p>', + current_user: { can_edit: false }, + discussion_id: '3eb958b4d81dec207ec3537a2f3bd8b9f271bb34', + emoji_awardable: false, + path: '/h5bp/html5-boilerplate/notes/1057', + report_abuse_path: + '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fh5bp%2Fhtml5-boilerplate%2Fissues%2F10%23note_1057&user_id=1', + }, + ], + }, +]; + export const INDIVIDUAL_NOTE_RESPONSE_MAP = { GET: { '/gitlab-org/gitlab-ce/issues/26/discussions.json': [ @@ -575,3 +648,508 @@ export function discussionNoteInterceptor(request, next) { }), ); } + +export const notesWithDescriptionChanges = [ + { + id: '39b271c2033e9ed43d8edb393702f65f7a830459', + reply_id: '39b271c2033e9ed43d8edb393702f65f7a830459', + expanded: true, + notes: [ + { + id: 901, + type: null, + attachment: null, + author: { + id: 1, + name: 'Administrator', + username: 'root', + state: 'active', + avatar_url: + 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', + path: '/root', + }, + created_at: '2018-05-29T12:05:36.117Z', + updated_at: '2018-05-29T12:05:36.117Z', + system: false, + noteable_id: 182, + noteable_type: 'Issue', + resolvable: false, + noteable_iid: 12, + note: + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.', + note_html: + '<p dir="auto">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p>', + current_user: { can_edit: true, can_award_emoji: true }, + resolved: false, + resolved_by: null, + discussion_id: '39b271c2033e9ed43d8edb393702f65f7a830459', + emoji_awardable: true, + award_emoji: [], + report_abuse_path: + '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-shell%2Fissues%2F12%23note_901&user_id=1', + human_access: 'Owner', + toggle_award_path: '/gitlab-org/gitlab-shell/notes/901/toggle_award_emoji', + path: '/gitlab-org/gitlab-shell/notes/901', + }, + ], + individual_note: true, + resolvable: false, + resolved: false, + diff_discussion: false, + }, + { + id: '4852335d7dc40b9ceb8fde1a2bb9c1b67e4c7795', + reply_id: '4852335d7dc40b9ceb8fde1a2bb9c1b67e4c7795', + expanded: true, + notes: [ + { + id: 902, + type: null, + attachment: null, + author: { + id: 1, + name: 'Administrator', + username: 'root', + state: 'active', + avatar_url: + 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', + path: '/root', + }, + created_at: '2018-05-29T12:05:58.694Z', + updated_at: '2018-05-29T12:05:58.694Z', + system: false, + noteable_id: 182, + noteable_type: 'Issue', + resolvable: false, + noteable_iid: 12, + note: + 'Varius vel pharetra vel turpis nunc eget lorem. Ipsum dolor sit amet consectetur adipiscing.', + note_html: + '<p dir="auto">Varius vel pharetra vel turpis nunc eget lorem. Ipsum dolor sit amet consectetur adipiscing.</p>', + current_user: { can_edit: true, can_award_emoji: true }, + resolved: false, + resolved_by: null, + discussion_id: '4852335d7dc40b9ceb8fde1a2bb9c1b67e4c7795', + emoji_awardable: true, + award_emoji: [], + report_abuse_path: + '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-shell%2Fissues%2F12%23note_902&user_id=1', + human_access: 'Owner', + toggle_award_path: '/gitlab-org/gitlab-shell/notes/902/toggle_award_emoji', + path: '/gitlab-org/gitlab-shell/notes/902', + }, + ], + individual_note: true, + resolvable: false, + resolved: false, + diff_discussion: false, + }, + { + id: '7f1feda384083eb31763366e6392399fde6f3f31', + reply_id: '7f1feda384083eb31763366e6392399fde6f3f31', + expanded: true, + notes: [ + { + id: 903, + type: null, + attachment: null, + author: { + id: 1, + name: 'Administrator', + username: 'root', + state: 'active', + avatar_url: + 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', + path: '/root', + }, + created_at: '2018-05-29T12:06:05.772Z', + updated_at: '2018-05-29T12:06:05.772Z', + system: true, + noteable_id: 182, + noteable_type: 'Issue', + resolvable: false, + noteable_iid: 12, + note: 'changed the description', + note_html: '<p dir="auto">changed the description</p>', + current_user: { can_edit: false, can_award_emoji: true }, + resolved: false, + resolved_by: null, + system_note_icon_name: 'pencil-square', + discussion_id: '7f1feda384083eb31763366e6392399fde6f3f31', + emoji_awardable: false, + report_abuse_path: + '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-shell%2Fissues%2F12%23note_903&user_id=1', + human_access: 'Owner', + path: '/gitlab-org/gitlab-shell/notes/903', + }, + ], + individual_note: true, + resolvable: false, + resolved: false, + diff_discussion: false, + }, + { + id: '091865fe3ae20f0045234a3d103e3b15e73405b5', + reply_id: '091865fe3ae20f0045234a3d103e3b15e73405b5', + expanded: true, + notes: [ + { + id: 904, + type: null, + attachment: null, + author: { + id: 1, + name: 'Administrator', + username: 'root', + state: 'active', + avatar_url: + 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', + path: '/root', + }, + created_at: '2018-05-29T12:06:16.112Z', + updated_at: '2018-05-29T12:06:16.112Z', + system: false, + noteable_id: 182, + noteable_type: 'Issue', + resolvable: false, + noteable_iid: 12, + note: 'Ullamcorper eget nulla facilisi etiam', + note_html: '<p dir="auto">Ullamcorper eget nulla facilisi etiam</p>', + current_user: { can_edit: true, can_award_emoji: true }, + resolved: false, + resolved_by: null, + discussion_id: '091865fe3ae20f0045234a3d103e3b15e73405b5', + emoji_awardable: true, + award_emoji: [], + report_abuse_path: + '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-shell%2Fissues%2F12%23note_904&user_id=1', + human_access: 'Owner', + toggle_award_path: '/gitlab-org/gitlab-shell/notes/904/toggle_award_emoji', + path: '/gitlab-org/gitlab-shell/notes/904', + }, + ], + individual_note: true, + resolvable: false, + resolved: false, + diff_discussion: false, + }, + { + id: 'a21cf2e804acc3c60d07e37d75e395f5a9a4d044', + reply_id: 'a21cf2e804acc3c60d07e37d75e395f5a9a4d044', + expanded: true, + notes: [ + { + id: 905, + type: null, + attachment: null, + author: { + id: 1, + name: 'Administrator', + username: 'root', + state: 'active', + avatar_url: + 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', + path: '/root', + }, + created_at: '2018-05-29T12:06:28.851Z', + updated_at: '2018-05-29T12:06:28.851Z', + system: true, + noteable_id: 182, + noteable_type: 'Issue', + resolvable: false, + noteable_iid: 12, + note: 'changed the description', + note_html: '<p dir="auto">changed the description</p>', + current_user: { can_edit: false, can_award_emoji: true }, + resolved: false, + resolved_by: null, + system_note_icon_name: 'pencil-square', + discussion_id: 'a21cf2e804acc3c60d07e37d75e395f5a9a4d044', + emoji_awardable: false, + report_abuse_path: + '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-shell%2Fissues%2F12%23note_905&user_id=1', + human_access: 'Owner', + path: '/gitlab-org/gitlab-shell/notes/905', + }, + ], + individual_note: true, + resolvable: false, + resolved: false, + diff_discussion: false, + }, + { + id: '70411b08cdfc01f24187a06d77daa33464cb2620', + reply_id: '70411b08cdfc01f24187a06d77daa33464cb2620', + expanded: true, + notes: [ + { + id: 906, + type: null, + attachment: null, + author: { + id: 1, + name: 'Administrator', + username: 'root', + state: 'active', + avatar_url: + 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', + path: '/root', + }, + created_at: '2018-05-29T12:20:02.925Z', + updated_at: '2018-05-29T12:20:02.925Z', + system: true, + noteable_id: 182, + noteable_type: 'Issue', + resolvable: false, + noteable_iid: 12, + note: 'changed the description', + note_html: '<p dir="auto">changed the description</p>', + current_user: { can_edit: false, can_award_emoji: true }, + resolved: false, + resolved_by: null, + system_note_icon_name: 'pencil-square', + discussion_id: '70411b08cdfc01f24187a06d77daa33464cb2620', + emoji_awardable: false, + report_abuse_path: + '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-shell%2Fissues%2F12%23note_906&user_id=1', + human_access: 'Owner', + path: '/gitlab-org/gitlab-shell/notes/906', + }, + ], + individual_note: true, + resolvable: false, + resolved: false, + diff_discussion: false, + }, +]; + +export const collapsedSystemNotes = [ + { + id: '39b271c2033e9ed43d8edb393702f65f7a830459', + reply_id: '39b271c2033e9ed43d8edb393702f65f7a830459', + expanded: true, + notes: [ + { + id: 901, + type: null, + attachment: null, + author: { + id: 1, + name: 'Administrator', + username: 'root', + state: 'active', + avatar_url: + 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', + path: '/root', + }, + created_at: '2018-05-29T12:05:36.117Z', + updated_at: '2018-05-29T12:05:36.117Z', + system: false, + noteable_id: 182, + noteable_type: 'Issue', + resolvable: false, + noteable_iid: 12, + note: + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.', + note_html: + '<p dir="auto">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p>', + current_user: { can_edit: true, can_award_emoji: true }, + resolved: false, + resolved_by: null, + discussion_id: '39b271c2033e9ed43d8edb393702f65f7a830459', + emoji_awardable: true, + award_emoji: [], + report_abuse_path: + '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-shell%2Fissues%2F12%23note_901&user_id=1', + human_access: 'Owner', + toggle_award_path: '/gitlab-org/gitlab-shell/notes/901/toggle_award_emoji', + path: '/gitlab-org/gitlab-shell/notes/901', + }, + ], + individual_note: true, + resolvable: false, + resolved: false, + diff_discussion: false, + }, + { + id: '4852335d7dc40b9ceb8fde1a2bb9c1b67e4c7795', + reply_id: '4852335d7dc40b9ceb8fde1a2bb9c1b67e4c7795', + expanded: true, + notes: [ + { + id: 902, + type: null, + attachment: null, + author: { + id: 1, + name: 'Administrator', + username: 'root', + state: 'active', + avatar_url: + 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', + path: '/root', + }, + created_at: '2018-05-29T12:05:58.694Z', + updated_at: '2018-05-29T12:05:58.694Z', + system: false, + noteable_id: 182, + noteable_type: 'Issue', + resolvable: false, + noteable_iid: 12, + note: + 'Varius vel pharetra vel turpis nunc eget lorem. Ipsum dolor sit amet consectetur adipiscing.', + note_html: + '<p dir="auto">Varius vel pharetra vel turpis nunc eget lorem. Ipsum dolor sit amet consectetur adipiscing.</p>', + current_user: { can_edit: true, can_award_emoji: true }, + resolved: false, + resolved_by: null, + discussion_id: '4852335d7dc40b9ceb8fde1a2bb9c1b67e4c7795', + emoji_awardable: true, + award_emoji: [], + report_abuse_path: + '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-shell%2Fissues%2F12%23note_902&user_id=1', + human_access: 'Owner', + toggle_award_path: '/gitlab-org/gitlab-shell/notes/902/toggle_award_emoji', + path: '/gitlab-org/gitlab-shell/notes/902', + }, + ], + individual_note: true, + resolvable: false, + resolved: false, + diff_discussion: false, + }, + { + id: '091865fe3ae20f0045234a3d103e3b15e73405b5', + reply_id: '091865fe3ae20f0045234a3d103e3b15e73405b5', + expanded: true, + notes: [ + { + id: 904, + type: null, + attachment: null, + author: { + id: 1, + name: 'Administrator', + username: 'root', + state: 'active', + avatar_url: + 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', + path: '/root', + }, + created_at: '2018-05-29T12:06:16.112Z', + updated_at: '2018-05-29T12:06:16.112Z', + system: false, + noteable_id: 182, + noteable_type: 'Issue', + resolvable: false, + noteable_iid: 12, + note: 'Ullamcorper eget nulla facilisi etiam', + note_html: '<p dir="auto">Ullamcorper eget nulla facilisi etiam</p>', + current_user: { can_edit: true, can_award_emoji: true }, + resolved: false, + resolved_by: null, + discussion_id: '091865fe3ae20f0045234a3d103e3b15e73405b5', + emoji_awardable: true, + award_emoji: [], + report_abuse_path: + '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-shell%2Fissues%2F12%23note_904&user_id=1', + human_access: 'Owner', + toggle_award_path: '/gitlab-org/gitlab-shell/notes/904/toggle_award_emoji', + path: '/gitlab-org/gitlab-shell/notes/904', + }, + ], + individual_note: true, + resolvable: false, + resolved: false, + diff_discussion: false, + }, + { + id: 'a21cf2e804acc3c60d07e37d75e395f5a9a4d044', + reply_id: 'a21cf2e804acc3c60d07e37d75e395f5a9a4d044', + expanded: true, + notes: [ + { + id: 905, + type: null, + attachment: null, + author: { + id: 1, + name: 'Administrator', + username: 'root', + state: 'active', + avatar_url: + 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', + path: '/root', + }, + created_at: '2018-05-29T12:06:28.851Z', + updated_at: '2018-05-29T12:06:28.851Z', + system: true, + noteable_id: 182, + noteable_type: 'Issue', + resolvable: false, + noteable_iid: 12, + note: 'changed the description', + note_html: '\n <p dir="auto">changed the description 2 times within 1 minute </p>', + current_user: { can_edit: false, can_award_emoji: true }, + resolved: false, + resolved_by: null, + system_note_icon_name: 'pencil-square', + discussion_id: 'a21cf2e804acc3c60d07e37d75e395f5a9a4d044', + emoji_awardable: false, + report_abuse_path: + '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-shell%2Fissues%2F12%23note_905&user_id=1', + human_access: 'Owner', + path: '/gitlab-org/gitlab-shell/notes/905', + times_updated: 2, + }, + ], + individual_note: true, + resolvable: false, + resolved: false, + diff_discussion: false, + }, + { + id: '70411b08cdfc01f24187a06d77daa33464cb2620', + reply_id: '70411b08cdfc01f24187a06d77daa33464cb2620', + expanded: true, + notes: [ + { + id: 906, + type: null, + attachment: null, + author: { + id: 1, + name: 'Administrator', + username: 'root', + state: 'active', + avatar_url: + 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', + path: '/root', + }, + created_at: '2018-05-29T12:20:02.925Z', + updated_at: '2018-05-29T12:20:02.925Z', + system: true, + noteable_id: 182, + noteable_type: 'Issue', + resolvable: false, + noteable_iid: 12, + note: 'changed the description', + note_html: '<p dir="auto">changed the description</p>', + current_user: { can_edit: false, can_award_emoji: true }, + resolved: false, + resolved_by: null, + system_note_icon_name: 'pencil-square', + discussion_id: '70411b08cdfc01f24187a06d77daa33464cb2620', + emoji_awardable: false, + report_abuse_path: + '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-shell%2Fissues%2F12%23note_906&user_id=1', + human_access: 'Owner', + path: '/gitlab-org/gitlab-shell/notes/906', + }, + ], + individual_note: true, + resolvable: false, + resolved: false, + diff_discussion: false, + }, +]; diff --git a/spec/javascripts/notes/stores/collapse_utils_spec.js b/spec/javascripts/notes/stores/collapse_utils_spec.js new file mode 100644 index 00000000000..06a6aab932a --- /dev/null +++ b/spec/javascripts/notes/stores/collapse_utils_spec.js @@ -0,0 +1,46 @@ +import { + isDescriptionSystemNote, + changeDescriptionNote, + getTimeDifferenceMinutes, + collapseSystemNotes, +} from '~/notes/stores/collapse_utils'; +import { + notesWithDescriptionChanges, + collapsedSystemNotes, +} from '../mock_data'; + +describe('Collapse utils', () => { + const mockSystemNote = { + note: 'changed the description', + note_html: '<p dir="auto">changed the description</p>', + system: true, + created_at: '2018-05-14T21:28:00.000Z', + }; + + it('checks if a system note is of a description type', () => { + expect(isDescriptionSystemNote(mockSystemNote)).toEqual(true); + }); + + it('returns false when a system note is not a description type', () => { + expect(isDescriptionSystemNote(Object.assign({}, mockSystemNote, { note: 'foo' }))).toEqual(false); + }); + + it('changes the description to contain the number of changed times', () => { + const changedNote = changeDescriptionNote(mockSystemNote, 3, 5); + + expect(changedNote.times_updated).toEqual(3); + expect(changedNote.note_html.trim()).toContain('<p dir="auto">changed the description 3 times within 5 minutes </p>'); + }); + + it('gets the time difference between two notes', () => { + const anotherSystemNote = { + created_at: '2018-05-14T21:33:00.000Z', + }; + + expect(getTimeDifferenceMinutes(mockSystemNote, anotherSystemNote)).toEqual(5); + }); + + it('collapses all description system notes made within 10 minutes or less from each other', () => { + expect(collapseSystemNotes(notesWithDescriptionChanges)).toEqual(collapsedSystemNotes); + }); +}); diff --git a/spec/javascripts/notes/stores/getters_spec.js b/spec/javascripts/notes/stores/getters_spec.js index 8b2a8d2cd7a..e5550580bf8 100644 --- a/spec/javascripts/notes/stores/getters_spec.js +++ b/spec/javascripts/notes/stores/getters_spec.js @@ -1,8 +1,9 @@ import * as getters from '~/notes/stores/getters'; -import { notesDataMock, userDataMock, noteableDataMock, individualNote } from '../mock_data'; +import { notesDataMock, userDataMock, noteableDataMock, individualNote, collapseNotesMock } from '../mock_data'; describe('Getters Notes Store', () => { let state; + beforeEach(() => { state = { notes: [individualNote], @@ -20,6 +21,22 @@ describe('Getters Notes Store', () => { }); }); + describe('Collapsed notes', () => { + const stateCollapsedNotes = { + notes: collapseNotesMock, + targetNoteHash: 'hash', + lastFetchedAt: 'timestamp', + + notesData: notesDataMock, + userData: userDataMock, + noteableData: noteableDataMock, + }; + + it('should return a single system note when a description was updated multiple times', () => { + expect(getters.notes(stateCollapsedNotes).length).toEqual(1); + }); + }); + describe('targetNoteHash', () => { it('should return `targetNoteHash`', () => { expect(getters.targetNoteHash(state)).toEqual('hash'); diff --git a/spec/javascripts/notes_spec.js b/spec/javascripts/notes_spec.js index 648fb3e9bd3..acbf23e2007 100644 --- a/spec/javascripts/notes_spec.js +++ b/spec/javascripts/notes_spec.js @@ -974,7 +974,7 @@ import timeoutPromise from './helpers/set_timeout_promise_helper'; ).toBeFalsy(); expect( $tempNoteHeader - .find('.d-none.d-sm-block') + .find('.d-none.d-sm-inline-block') .text() .trim(), ).toEqual(currentUserFullname); @@ -1020,7 +1020,7 @@ import timeoutPromise from './helpers/set_timeout_promise_helper'; const $tempNoteHeader = $tempNote.find('.note-header'); expect( $tempNoteHeader - .find('.d-none.d-sm-block') + .find('.d-none.d-sm-inline-block') .text() .trim(), ).toEqual('Foo <script>alert("XSS")</script>'); diff --git a/spec/javascripts/pipelines/graph/mock_data.js b/spec/javascripts/pipelines/graph/mock_data.js index 70eba98e939..9e25a4b3fed 100644 --- a/spec/javascripts/pipelines/graph/mock_data.js +++ b/spec/javascripts/pipelines/graph/mock_data.js @@ -20,7 +20,7 @@ export default { has_details: true, details_path: '/root/ci-mock/pipelines/123', favicon: - '/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico', + '/assets/ci_favicons/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.png', }, duration: 9, finished_at: '2017-04-19T14:30:27.542Z', @@ -40,7 +40,7 @@ export default { has_details: true, details_path: '/root/ci-mock/builds/4153', favicon: - '/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico', + '/assets/ci_favicons/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.png', action: { icon: 'retry', title: 'Retry', @@ -65,7 +65,7 @@ export default { has_details: true, details_path: '/root/ci-mock/builds/4153', favicon: - '/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico', + '/assets/ci_favicons/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.png', action: { icon: 'retry', title: 'Retry', @@ -85,7 +85,7 @@ export default { has_details: true, details_path: '/root/ci-mock/pipelines/123#test', favicon: - '/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico', + '/assets/ci_favicons/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.png', }, path: '/root/ci-mock/pipelines/123#test', dropdown_path: '/root/ci-mock/pipelines/123/stage.json?stage=test', @@ -105,7 +105,7 @@ export default { has_details: true, details_path: '/root/ci-mock/builds/4166', favicon: - '/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico', + '/assets/ci_favicons/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.png', action: { icon: 'retry', title: 'Retry', @@ -130,7 +130,7 @@ export default { has_details: true, details_path: '/root/ci-mock/builds/4166', favicon: - '/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico', + '/assets/ci_favicons/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.png', action: { icon: 'retry', title: 'Retry', @@ -152,7 +152,7 @@ export default { has_details: true, details_path: '/root/ci-mock/builds/4159', favicon: - '/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico', + '/assets/ci_favicons/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.png', action: { icon: 'retry', title: 'Retry', @@ -177,7 +177,7 @@ export default { has_details: true, details_path: '/root/ci-mock/builds/4159', favicon: - '/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico', + '/assets/ci_favicons/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.png', action: { icon: 'retry', title: 'Retry', @@ -197,7 +197,7 @@ export default { has_details: true, details_path: '/root/ci-mock/pipelines/123#deploy', favicon: - '/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico', + '/assets/ci_favicons/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.png', }, path: '/root/ci-mock/pipelines/123#deploy', dropdown_path: '/root/ci-mock/pipelines/123/stage.json?stage=deploy', diff --git a/spec/javascripts/pipelines/pipelines_spec.js b/spec/javascripts/pipelines/pipelines_spec.js index ff17602da2b..50141bd99b4 100644 --- a/spec/javascripts/pipelines/pipelines_spec.js +++ b/spec/javascripts/pipelines/pipelines_spec.js @@ -427,7 +427,7 @@ describe('Pipelines', () => { describe('methods', () => { beforeEach(() => { - spyOn(history, 'pushState').and.stub(); + spyOn(window.history, 'pushState').and.stub(); }); describe('updateContent', () => { diff --git a/spec/javascripts/profile/account/components/update_username_spec.js b/spec/javascripts/profile/account/components/update_username_spec.js index bac306edf5a..5311499fb73 100644 --- a/spec/javascripts/profile/account/components/update_username_spec.js +++ b/spec/javascripts/profile/account/components/update_username_spec.js @@ -90,7 +90,8 @@ describe('UpdateUsername component', () => { it('confirmation modal should escape usernames properly', done => { const { modalBody } = findElements(); - vm.username = vm.newUsername = '<i>Italic</i>'; + vm.username = '<i>Italic</i>'; + vm.newUsername = vm.username; Vue.nextTick() .then(() => { diff --git a/spec/javascripts/settings_panels_spec.js b/spec/javascripts/settings_panels_spec.js index 4fba36bd4de..c1a69bd7018 100644 --- a/spec/javascripts/settings_panels_spec.js +++ b/spec/javascripts/settings_panels_spec.js @@ -9,11 +9,11 @@ describe('Settings Panels', () => { describe('initSettingsPane', () => { afterEach(() => { - location.hash = ''; + window.location.hash = ''; }); it('should expand linked hash fragment panel', () => { - location.hash = '#autodevops-settings'; + window.location.hash = '#autodevops-settings'; const pipelineSettingsPanel = document.querySelector('#autodevops-settings'); // Our test environment automatically expands everything so we need to clear that out first diff --git a/spec/javascripts/shortcuts_issuable_spec.js b/spec/javascripts/shortcuts_issuable_spec.js index d73608ed0ed..b10d8be6781 100644 --- a/spec/javascripts/shortcuts_issuable_spec.js +++ b/spec/javascripts/shortcuts_issuable_spec.js @@ -66,7 +66,7 @@ describe('ShortcutsIssuable', function () { }); describe('with a multi-line selection', () => { it('quotes the selected lines as a group', () => { - stubSelection('<p>Selected line one.</p>\n\n<p>Selected line two.</p>\n\n<p>Selected line three.</p>'); + stubSelection('<p>Selected line one.</p>\n<p>Selected line two.</p>\n<p>Selected line three.</p>'); this.shortcut.replyWithSelectedText(true); expect($(this.selector).val()).toBe('> Selected line one.\n>\n> Selected line two.\n>\n> Selected line three.\n\n'); }); diff --git a/spec/javascripts/test_bundle.js b/spec/javascripts/test_bundle.js index 2411d33a496..994011b262a 100644 --- a/spec/javascripts/test_bundle.js +++ b/spec/javascripts/test_bundle.js @@ -39,7 +39,8 @@ jasmine.getJSONFixtures().fixturesPath = FIXTURES_PATH; beforeAll(() => jasmine.addMatchers(customMatchers)); // globalize common libraries -window.$ = window.jQuery = $; +window.$ = $; +window.jQuery = window.$; // stub expected globals window.gl = window.gl || {}; diff --git a/spec/javascripts/test_constants.js b/spec/javascripts/test_constants.js index df59195e9f6..a820dd2d09c 100644 --- a/spec/javascripts/test_constants.js +++ b/spec/javascripts/test_constants.js @@ -2,3 +2,6 @@ export const FIXTURES_PATH = '/base/spec/javascripts/fixtures'; export const TEST_HOST = 'http://test.host'; export const DUMMY_IMAGE_URL = `${FIXTURES_PATH}/one_white_pixel.png`; + +export const GREEN_BOX_IMAGE_URL = `${FIXTURES_PATH}/images/green_box.png`; +export const RED_BOX_IMAGE_URL = `${FIXTURES_PATH}/images/red_box.png`; diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_author_time_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_author_time_spec.js index 6784b498c29..10143402acf 100644 --- a/spec/javascripts/vue_mr_widget/components/mr_widget_author_time_spec.js +++ b/spec/javascripts/vue_mr_widget/components/mr_widget_author_time_spec.js @@ -1,12 +1,12 @@ import Vue from 'vue'; -import authorTimeComponent from '~/vue_merge_request_widget/components/mr_widget_author_time.vue'; +import MrWidgetAuthorTime from '~/vue_merge_request_widget/components/mr_widget_author_time.vue'; import mountComponent from 'spec/helpers/vue_mount_component_helper'; -describe('MRWidgetAuthorTime', () => { +describe('MrWidgetAuthorTime', () => { let vm; beforeEach(() => { - const Component = Vue.extend(authorTimeComponent); + const Component = Vue.extend(MrWidgetAuthorTime); vm = mountComponent(Component, { actionText: 'Merged by', @@ -34,7 +34,7 @@ describe('MRWidgetAuthorTime', () => { }); it('renders provided time', () => { - expect(vm.$el.querySelector('time').getAttribute('title')).toEqual('2017-03-23T23:02:00.807Z'); + expect(vm.$el.querySelector('time').getAttribute('data-original-title')).toEqual('2017-03-23T23:02:00.807Z'); expect(vm.$el.querySelector('time').textContent.trim()).toEqual('12 hours ago'); }); }); diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js index 9b9c9656979..3d36e46d863 100644 --- a/spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js +++ b/spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js @@ -12,6 +12,7 @@ describe('MRWidgetHeader', () => { afterEach(() => { vm.$destroy(); + gon.relative_url_root = ''; }); describe('computed', () => { @@ -145,7 +146,16 @@ describe('MRWidgetHeader', () => { const button = vm.$el.querySelector('.js-web-ide'); expect(button.textContent.trim()).toEqual('Web IDE'); - expect(button.getAttribute('href')).toEqual('undefined/-/ide/projectabc'); + expect(button.getAttribute('href')).toEqual('/-/ide/projectabc'); + }); + + it('renders web ide button with relative URL', () => { + gon.relative_url_root = '/gitlab'; + + const button = vm.$el.querySelector('.js-web-ide'); + + expect(button.textContent.trim()).toEqual('Web IDE'); + expect(button.getAttribute('href')).toEqual('/-/ide/projectabc'); }); it('renders download dropdown with links', () => { diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_failed_to_merge_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_failed_to_merge_spec.js index a0a74648328..8de99fd3c96 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_failed_to_merge_spec.js +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_failed_to_merge_spec.js @@ -6,6 +6,7 @@ import mountComponent from 'spec/helpers/vue_mount_component_helper'; describe('MRWidgetFailedToMerge', () => { const dummyIntervalId = 1337; let Component; + let mr; let vm; beforeEach(() => { @@ -13,10 +14,11 @@ describe('MRWidgetFailedToMerge', () => { spyOn(eventHub, '$emit'); spyOn(window, 'setInterval').and.returnValue(dummyIntervalId); spyOn(window, 'clearInterval').and.stub(); + mr = { + mergeError: 'Merge error happened', + }; vm = mountComponent(Component, { - mr: { - mergeError: 'Merge error happened.', - }, + mr, }); }); @@ -44,6 +46,19 @@ describe('MRWidgetFailedToMerge', () => { expect(vm.timerText).toEqual('Refreshing in a second to show the updated status...'); }); }); + + describe('mergeError', () => { + it('removes forced line breaks', done => { + mr.mergeError = 'contains<br />line breaks<br />'; + + Vue.nextTick() + .then(() => { + expect(vm.mergeError).toBe('contains line breaks'); + }) + .then(done) + .catch(done.fail); + }); + }); }); describe('created', () => { @@ -103,7 +118,7 @@ describe('MRWidgetFailedToMerge', () => { it('renders given error', () => { expect(vm.$el.querySelector('.has-error-message').textContent.trim()).toEqual( - 'Merge error happened..', + 'Merge error happened.', ); }); diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_merged_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_merged_spec.js index adeea03481f..3e2fd71b5b8 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_merged_spec.js +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_merged_spec.js @@ -186,7 +186,7 @@ describe('MRWidgetMerged', () => { it('should use mergedEvent mergedAt as tooltip title', () => { expect( - vm.$el.querySelector('time').getAttribute('title'), + vm.$el.querySelector('time').getAttribute('data-original-title'), ).toBe('Jan 24, 2018 1:02pm GMT+0000'); }); }); diff --git a/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js b/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js index 30918428da2..6342ea00436 100644 --- a/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js +++ b/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js @@ -5,6 +5,7 @@ import notify from '~/lib/utils/notify'; import { stateKey } from '~/vue_merge_request_widget/stores/state_maps'; import mountComponent from 'spec/helpers/vue_mount_component_helper'; import mockData from './mock_data'; +import { faviconDataUrl, overlayDataUrl, faviconWithOverlayDataUrl } from '../lib/utils/mock_data'; const returnPromise = data => new Promise((resolve) => { resolve({ @@ -273,6 +274,7 @@ describe('mrWidgetOptions', () => { beforeEach(() => { const favicon = document.createElement('link'); favicon.setAttribute('id', 'favicon'); + favicon.setAttribute('data-original-href', faviconDataUrl); document.body.appendChild(favicon); faviconElement = document.getElementById('favicon'); @@ -282,10 +284,13 @@ describe('mrWidgetOptions', () => { document.body.removeChild(document.getElementById('favicon')); }); - it('should call setFavicon method', () => { - vm.setFaviconHelper(); - - expect(faviconElement.getAttribute('href')).toEqual(vm.mr.ciStatusFaviconPath); + it('should call setFavicon method', (done) => { + vm.mr.ciStatusFaviconPath = overlayDataUrl; + vm.setFaviconHelper().then(() => { + expect(faviconElement.getAttribute('href')).toEqual(faviconWithOverlayDataUrl); + done(); + }) + .catch(done.fail); }); it('should not call setFavicon when there is no ciStatusFaviconPath', () => { diff --git a/spec/javascripts/vue_shared/components/content_viewer/content_viewer_spec.js b/spec/javascripts/vue_shared/components/content_viewer/content_viewer_spec.js index 383f0cd29ea..e2c34508b0d 100644 --- a/spec/javascripts/vue_shared/components/content_viewer/content_viewer_spec.js +++ b/spec/javascripts/vue_shared/components/content_viewer/content_viewer_spec.js @@ -3,6 +3,7 @@ import MockAdapter from 'axios-mock-adapter'; import axios from '~/lib/utils/axios_utils'; import contentViewer from '~/vue_shared/components/content_viewer/content_viewer.vue'; import mountComponent from 'spec/helpers/vue_mount_component_helper'; +import { GREEN_BOX_IMAGE_URL } from 'spec/test_constants'; describe('ContentViewer', () => { let vm; @@ -41,12 +42,12 @@ describe('ContentViewer', () => { it('renders image preview', done => { createComponent({ - path: 'test.jpg', + path: GREEN_BOX_IMAGE_URL, fileSize: 1024, }); setTimeout(() => { - expect(vm.$el.querySelector('.image_file img').getAttribute('src')).toBe('test.jpg'); + expect(vm.$el.querySelector('.image_file img').getAttribute('src')).toBe(GREEN_BOX_IMAGE_URL); done(); }); @@ -59,9 +60,8 @@ describe('ContentViewer', () => { }); setTimeout(() => { - expect(vm.$el.querySelector('.file-info').textContent.trim()).toContain( - 'test.abc (1.00 KiB)', - ); + expect(vm.$el.querySelector('.file-info').textContent.trim()).toContain('test.abc'); + expect(vm.$el.querySelector('.file-info').textContent.trim()).toContain('(1.00 KiB)'); expect(vm.$el.querySelector('.btn.btn-default').textContent.trim()).toContain('Download'); done(); diff --git a/spec/javascripts/vue_shared/components/diff_viewer/diff_viewer_spec.js b/spec/javascripts/vue_shared/components/diff_viewer/diff_viewer_spec.js new file mode 100644 index 00000000000..71d9145bf22 --- /dev/null +++ b/spec/javascripts/vue_shared/components/diff_viewer/diff_viewer_spec.js @@ -0,0 +1,70 @@ +import Vue from 'vue'; +import diffViewer from '~/vue_shared/components/diff_viewer/diff_viewer.vue'; +import mountComponent from 'spec/helpers/vue_mount_component_helper'; +import { GREEN_BOX_IMAGE_URL, RED_BOX_IMAGE_URL } from 'spec/test_constants'; + +describe('DiffViewer', () => { + let vm; + + function createComponent(props) { + const DiffViewer = Vue.extend(diffViewer); + vm = mountComponent(DiffViewer, props); + } + + afterEach(() => { + vm.$destroy(); + }); + + it('renders image diff', done => { + window.gon = { + relative_url_root: '', + }; + + createComponent({ + diffMode: 'replaced', + newPath: GREEN_BOX_IMAGE_URL, + newSha: 'ABC', + oldPath: RED_BOX_IMAGE_URL, + oldSha: 'DEF', + projectPath: '', + }); + + setTimeout(() => { + expect(vm.$el.querySelector('.deleted .image_file img').getAttribute('src')).toBe( + `//raw/DEF/${RED_BOX_IMAGE_URL}`, + ); + + expect(vm.$el.querySelector('.added .image_file img').getAttribute('src')).toBe( + `//raw/ABC/${GREEN_BOX_IMAGE_URL}`, + ); + + done(); + }); + }); + + it('renders fallback download diff display', done => { + createComponent({ + diffMode: 'replaced', + newPath: 'test.abc', + newSha: 'ABC', + oldPath: 'testold.abc', + oldSha: 'DEF', + }); + + setTimeout(() => { + expect(vm.$el.querySelector('.deleted .file-info').textContent.trim()).toContain( + 'testold.abc', + ); + expect(vm.$el.querySelector('.deleted .btn.btn-default').textContent.trim()).toContain( + 'Download', + ); + + expect(vm.$el.querySelector('.added .file-info').textContent.trim()).toContain('test.abc'); + expect(vm.$el.querySelector('.added .btn.btn-default').textContent.trim()).toContain( + 'Download', + ); + + done(); + }); + }); +}); diff --git a/spec/javascripts/vue_shared/components/diff_viewer/viewers/image_diff_viewer_spec.js b/spec/javascripts/vue_shared/components/diff_viewer/viewers/image_diff_viewer_spec.js new file mode 100644 index 00000000000..b878286ae3f --- /dev/null +++ b/spec/javascripts/vue_shared/components/diff_viewer/viewers/image_diff_viewer_spec.js @@ -0,0 +1,185 @@ +import Vue from 'vue'; +import imageDiffViewer from '~/vue_shared/components/diff_viewer/viewers/image_diff_viewer.vue'; +import mountComponent from 'spec/helpers/vue_mount_component_helper'; +import { GREEN_BOX_IMAGE_URL, RED_BOX_IMAGE_URL } from 'spec/test_constants'; + +describe('ImageDiffViewer', () => { + let vm; + + function createComponent(props) { + const ImageDiffViewer = Vue.extend(imageDiffViewer); + vm = mountComponent(ImageDiffViewer, props); + } + + const triggerEvent = (eventName, el = vm.$el, clientX = 0) => { + const event = document.createEvent('MouseEvents'); + event.initMouseEvent( + eventName, + true, + true, + window, + 1, + clientX, + 0, + clientX, + 0, + false, + false, + false, + false, + 0, + null, + ); + + el.dispatchEvent(event); + }; + + const dragSlider = (sliderElement, dragPixel = 20) => { + triggerEvent('mousedown', sliderElement); + triggerEvent('mousemove', document.body, dragPixel); + triggerEvent('mouseup', document.body); + }; + + afterEach(() => { + vm.$destroy(); + }); + + it('renders image diff for replaced', done => { + createComponent({ + diffMode: 'replaced', + newPath: GREEN_BOX_IMAGE_URL, + oldPath: RED_BOX_IMAGE_URL, + }); + + setTimeout(() => { + expect(vm.$el.querySelector('.added .image_file img').getAttribute('src')).toBe( + GREEN_BOX_IMAGE_URL, + ); + expect(vm.$el.querySelector('.deleted .image_file img').getAttribute('src')).toBe( + RED_BOX_IMAGE_URL, + ); + + expect(vm.$el.querySelector('.view-modes-menu li.active').textContent.trim()).toBe('2-up'); + expect(vm.$el.querySelector('.view-modes-menu li:nth-child(2)').textContent.trim()).toBe( + 'Swipe', + ); + expect(vm.$el.querySelector('.view-modes-menu li:nth-child(3)').textContent.trim()).toBe( + 'Onion skin', + ); + + done(); + }); + }); + + it('renders image diff for new', done => { + createComponent({ + diffMode: 'new', + newPath: GREEN_BOX_IMAGE_URL, + oldPath: '', + }); + + setTimeout(() => { + expect(vm.$el.querySelector('.added .image_file img').getAttribute('src')).toBe( + GREEN_BOX_IMAGE_URL, + ); + + done(); + }); + }); + + it('renders image diff for deleted', done => { + createComponent({ + diffMode: 'deleted', + newPath: '', + oldPath: RED_BOX_IMAGE_URL, + }); + + setTimeout(() => { + expect(vm.$el.querySelector('.deleted .image_file img').getAttribute('src')).toBe( + RED_BOX_IMAGE_URL, + ); + + done(); + }); + }); + + describe('swipeMode', () => { + beforeEach(done => { + createComponent({ + diffMode: 'replaced', + newPath: GREEN_BOX_IMAGE_URL, + oldPath: RED_BOX_IMAGE_URL, + }); + + setTimeout(() => { + done(); + }); + }); + + it('switches to Swipe Mode', done => { + vm.$el.querySelector('.view-modes-menu li:nth-child(2)').click(); + + vm.$nextTick(() => { + expect(vm.$el.querySelector('.view-modes-menu li.active').textContent.trim()).toBe('Swipe'); + done(); + }); + }); + + it('drag handler is working', done => { + vm.$el.querySelector('.view-modes-menu li:nth-child(2)').click(); + + vm.$nextTick(() => { + expect(vm.$el.querySelector('.swipe-bar').style.left).toBe('1px'); + expect(vm.$el.querySelector('.top-handle')).not.toBeNull(); + + dragSlider(vm.$el.querySelector('.swipe-bar'), 40); + + vm.$nextTick(() => { + expect(vm.$el.querySelector('.swipe-bar').style.left).toBe('-20px'); + done(); + }); + }); + }); + }); + + describe('onionSkin', () => { + beforeEach(done => { + createComponent({ + diffMode: 'replaced', + newPath: GREEN_BOX_IMAGE_URL, + oldPath: RED_BOX_IMAGE_URL, + }); + + setTimeout(() => { + done(); + }); + }); + + it('switches to Onion Skin Mode', done => { + vm.$el.querySelector('.view-modes-menu li:nth-child(3)').click(); + + vm.$nextTick(() => { + expect(vm.$el.querySelector('.view-modes-menu li.active').textContent.trim()).toBe( + 'Onion skin', + ); + done(); + }); + }); + + it('has working drag handler', done => { + vm.$el.querySelector('.view-modes-menu li:nth-child(3)').click(); + + vm.$nextTick(() => { + expect(vm.$el.querySelector('.dragger').style.left).toBe('100px'); + + dragSlider(vm.$el.querySelector('.dragger')); + + vm.$nextTick(() => { + expect(vm.$el.querySelector('.dragger').style.left).toBe('20px'); + expect(vm.$el.querySelector('.added.frame').style.opacity).toBe('0.2'); + done(); + }); + }); + }); + }); +}); diff --git a/spec/javascripts/vue_shared/components/gl_modal_spec.js b/spec/javascripts/vue_shared/components/gl_modal_spec.js index 85cb1b90fc6..e4737714312 100644 --- a/spec/javascripts/vue_shared/components/gl_modal_spec.js +++ b/spec/javascripts/vue_shared/components/gl_modal_spec.js @@ -190,4 +190,45 @@ describe('GlModal', () => { }); }); }); + + describe('handling sizes', () => { + it('should render modal-sm', () => { + vm = mountComponent(modalComponent, { + modalSize: 'sm', + }); + + expect(vm.$el.querySelector('.modal-dialog').classList.contains('modal-sm')).toEqual(true); + }); + + it('should render modal-lg', () => { + vm = mountComponent(modalComponent, { + modalSize: 'lg', + }); + + expect(vm.$el.querySelector('.modal-dialog').classList.contains('modal-lg')).toEqual(true); + }); + + it('should render modal-xl', () => { + vm = mountComponent(modalComponent, { + modalSize: 'xl', + }); + + expect(vm.$el.querySelector('.modal-dialog').classList.contains('modal-xl')).toEqual(true); + }); + + it('should not add modal size classes when md size is passed', () => { + vm = mountComponent(modalComponent, { + modalSize: 'md', + }); + + expect(vm.$el.querySelector('.modal-dialog').classList.contains('modal-md')).toEqual(false); + }); + + it('should not add modal size classes by default', () => { + vm = mountComponent(modalComponent, {}); + + expect(vm.$el.querySelector('.modal-dialog').classList.contains('modal-sm')).toEqual(false); + expect(vm.$el.querySelector('.modal-dialog').classList.contains('modal-lg')).toEqual(false); + }); + }); }); diff --git a/spec/javascripts/vue_shared/components/lib/utils/dom_utils_spec.js b/spec/javascripts/vue_shared/components/lib/utils/dom_utils_spec.js new file mode 100644 index 00000000000..2388660b0c2 --- /dev/null +++ b/spec/javascripts/vue_shared/components/lib/utils/dom_utils_spec.js @@ -0,0 +1,13 @@ +import * as domUtils from '~/vue_shared/components/lib/utils/dom_utils'; + +describe('domUtils', () => { + describe('pixeliseValue', () => { + it('should add px to a given Number', () => { + expect(domUtils.pixeliseValue(12)).toEqual('12px'); + }); + + it('should not add px to 0', () => { + expect(domUtils.pixeliseValue(0)).toEqual(''); + }); + }); +}); diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js index 4397b00acfa..370a296bd8f 100644 --- a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js +++ b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js @@ -82,7 +82,7 @@ describe('DropdownValueComponent', () => { }); it('renders label element with tooltip and styles based on label details', () => { - const labelEl = vm.$el.querySelector('a span.label.color-label'); + const labelEl = vm.$el.querySelector('a span.badge.color-label'); expect(labelEl).not.toBeNull(); expect(labelEl.dataset.placement).toBe('bottom'); expect(labelEl.dataset.container).toBe('body'); diff --git a/spec/javascripts/vue_shared/components/table_pagination_spec.js b/spec/javascripts/vue_shared/components/table_pagination_spec.js index c63f15e5880..c36b607a34e 100644 --- a/spec/javascripts/vue_shared/components/table_pagination_spec.js +++ b/spec/javascripts/vue_shared/components/table_pagination_spec.js @@ -51,7 +51,7 @@ describe('Pagination component', () => { expect( component.$el.querySelector('.js-previous-button').classList.contains('disabled'), - ).toEqual(true); + ).toEqual(true); component.$el.querySelector('.js-previous-button a').click(); diff --git a/spec/lib/backup/files_spec.rb b/spec/lib/backup/files_spec.rb index 99872211a4e..63f2298357f 100644 --- a/spec/lib/backup/files_spec.rb +++ b/spec/lib/backup/files_spec.rb @@ -46,7 +46,9 @@ describe Backup::Files do end it 'calls tar command with unlink' do - expect(subject).to receive(:run_pipeline!).with([%w(gzip -cd), %w(tar --unlink-first --recursive-unlink -C /var/gitlab-registry -xf -)], any_args) + expect(subject).to receive(:tar).and_return('blabla-tar') + + expect(subject).to receive(:run_pipeline!).with([%w(gzip -cd), %w(blabla-tar --unlink-first --recursive-unlink -C /var/gitlab-registry -xf -)], any_args) subject.restore end end diff --git a/spec/lib/backup/manager_spec.rb b/spec/lib/backup/manager_spec.rb index 23c04a1a101..ca319679e80 100644 --- a/spec/lib/backup/manager_spec.rb +++ b/spec/lib/backup/manager_spec.rb @@ -274,16 +274,13 @@ describe Backup::Manager do } ) - # the Fog mock only knows about directories we create explicitly Fog.mock! + + # the Fog mock only knows about directories we create explicitly connection = ::Fog::Storage.new(Gitlab.config.backup.upload.connection.symbolize_keys) connection.directories.create(key: Gitlab.config.backup.upload.remote_directory) end - after do - Fog.unmock! - end - context 'target path' do it 'uses the tar filename by default' do expect_any_instance_of(Fog::Collection).to receive(:create) diff --git a/spec/lib/backup/repository_spec.rb b/spec/lib/backup/repository_spec.rb index f583b2021a2..92a27e308d2 100644 --- a/spec/lib/backup/repository_spec.rb +++ b/spec/lib/backup/repository_spec.rb @@ -34,7 +34,9 @@ describe Backup::Repository do let(:timestamp) { Time.utc(2017, 3, 22) } let(:temp_dirs) do Gitlab.config.repositories.storages.map do |name, storage| - File.join(storage.legacy_disk_path, '..', 'repositories.old.' + timestamp.to_i.to_s) + Gitlab::GitalyClient::StorageSettings.allow_disk_access do + File.join(storage.legacy_disk_path, '..', 'repositories.old.' + timestamp.to_i.to_s) + end end end diff --git a/spec/lib/banzai/filter/blockquote_fence_filter_spec.rb b/spec/lib/banzai/filter/blockquote_fence_filter_spec.rb index 8224dc5a6b9..b645e49bd43 100644 --- a/spec/lib/banzai/filter/blockquote_fence_filter_spec.rb +++ b/spec/lib/banzai/filter/blockquote_fence_filter_spec.rb @@ -11,4 +11,8 @@ describe Banzai::Filter::BlockquoteFenceFilter do expect(output).to eq(expected) end + + it 'allows trailing whitespace on blockquote fence lines' do + expect(filter(">>> \ntest\n>>> ")).to eq("> test") + end end diff --git a/spec/lib/banzai/filter/image_lazy_load_filter_spec.rb b/spec/lib/banzai/filter/image_lazy_load_filter_spec.rb index c19de7b784a..41f957c4e00 100644 --- a/spec/lib/banzai/filter/image_lazy_load_filter_spec.rb +++ b/spec/lib/banzai/filter/image_lazy_load_filter_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Banzai::Filter::ImageLazyLoadFilter, lib: true do +describe Banzai::Filter::ImageLazyLoadFilter do include FilterSpecHelper def image(path) diff --git a/spec/lib/banzai/filter/markdown_filter_spec.rb b/spec/lib/banzai/filter/markdown_filter_spec.rb index 00c407d1b69..ab14d77d552 100644 --- a/spec/lib/banzai/filter/markdown_filter_spec.rb +++ b/spec/lib/banzai/filter/markdown_filter_spec.rb @@ -7,13 +7,13 @@ describe Banzai::Filter::MarkdownFilter do it 'adds language to lang attribute when specified' do result = filter("```html\nsome code\n```") - expect(result).to start_with("\n<pre><code lang=\"html\">") + expect(result).to start_with("<pre><code lang=\"html\">") end it 'does not add language to lang attribute when not specified' do result = filter("```\nsome code\n```") - expect(result).to start_with("\n<pre><code>") + expect(result).to start_with("<pre><code>") end end end diff --git a/spec/lib/banzai/filter/milestone_reference_filter_spec.rb b/spec/lib/banzai/filter/milestone_reference_filter_spec.rb index f8fa9b2d13d..91d4a60ba95 100644 --- a/spec/lib/banzai/filter/milestone_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/milestone_reference_filter_spec.rb @@ -3,7 +3,8 @@ require 'spec_helper' describe Banzai::Filter::MilestoneReferenceFilter do include FilterSpecHelper - let(:group) { create(:group, :public) } + let(:parent_group) { create(:group, :public) } + let(:group) { create(:group, :public, parent: parent_group) } let(:project) { create(:project, :public, group: group) } it 'requires project context' do @@ -340,6 +341,13 @@ describe Banzai::Filter::MilestoneReferenceFilter do expect(doc.css('a')).to be_empty end + + it 'supports parent group references', :nested_groups do + milestone.update!(group: parent_group) + + doc = reference_filter("See #{reference}") + expect(doc.css('a').first.text).to eq(milestone.name) + end end context 'group context' do diff --git a/spec/lib/gitlab/background_migration/archive_legacy_traces_spec.rb b/spec/lib/gitlab/background_migration/archive_legacy_traces_spec.rb new file mode 100644 index 00000000000..877c061d11b --- /dev/null +++ b/spec/lib/gitlab/background_migration/archive_legacy_traces_spec.rb @@ -0,0 +1,59 @@ +require 'spec_helper' + +describe Gitlab::BackgroundMigration::ArchiveLegacyTraces, :migration, schema: 20180529152628 do + include TraceHelpers + + let(:namespaces) { table(:namespaces) } + let(:projects) { table(:projects) } + let(:builds) { table(:ci_builds) } + let(:job_artifacts) { table(:ci_job_artifacts) } + + before do + namespaces.create!(id: 123, name: 'gitlab1', path: 'gitlab1') + projects.create!(id: 123, name: 'gitlab1', path: 'gitlab1', namespace_id: 123) + @build = builds.create!(id: 1, project_id: 123, status: 'success', type: 'Ci::Build') + end + + context 'when trace file exsits at the right place' do + before do + create_legacy_trace(@build, 'trace in file') + end + + it 'correctly archive legacy traces' do + expect(job_artifacts.count).to eq(0) + expect(File.exist?(legacy_trace_path(@build))).to be_truthy + + described_class.new.perform(1, 1) + + expect(job_artifacts.count).to eq(1) + expect(File.exist?(legacy_trace_path(@build))).to be_falsy + expect(File.read(archived_trace_path(job_artifacts.first))).to eq('trace in file') + end + end + + context 'when trace file does not exsits at the right place' do + it 'does not raise errors nor create job artifact' do + expect { described_class.new.perform(1, 1) }.not_to raise_error + + expect(job_artifacts.count).to eq(0) + end + end + + context 'when trace data exsits in database' do + before do + create_legacy_trace_in_db(@build, 'trace in db') + end + + it 'correctly archive legacy traces' do + expect(job_artifacts.count).to eq(0) + expect(@build.read_attribute(:trace)).not_to be_empty + + described_class.new.perform(1, 1) + + @build.reload + expect(job_artifacts.count).to eq(1) + expect(@build.read_attribute(:trace)).to be_nil + expect(File.read(archived_trace_path(job_artifacts.first))).to eq('trace in db') + 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 index 0d2074eed22..0dee683350f 100644 --- a/spec/lib/gitlab/background_migration/populate_untracked_uploads_spec.rb +++ b/spec/lib/gitlab/background_migration/populate_untracked_uploads_spec.rb @@ -114,7 +114,7 @@ describe Gitlab::BackgroundMigration::PopulateUntrackedUploads, :sidekiq, :migra 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 + expect(ActiveRecord::Base.connection.data_source_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 diff --git a/spec/lib/gitlab/bare_repository_import/importer_spec.rb b/spec/lib/gitlab/bare_repository_import/importer_spec.rb index 5c8a19a53bc..468f6ff6d24 100644 --- a/spec/lib/gitlab/bare_repository_import/importer_spec.rb +++ b/spec/lib/gitlab/bare_repository_import/importer_spec.rb @@ -20,6 +20,13 @@ describe Gitlab::BareRepositoryImport::Importer, repository: true do Rainbow.enabled = @rainbow end + around do |example| + # TODO migrate BareRepositoryImport https://gitlab.com/gitlab-org/gitaly/issues/953 + Gitlab::GitalyClient::StorageSettings.allow_disk_access do + example.run + end + end + shared_examples 'importing a repository' do describe '.execute' do it 'creates a project for a repository in storage' do diff --git a/spec/lib/gitlab/bare_repository_import/repository_spec.rb b/spec/lib/gitlab/bare_repository_import/repository_spec.rb index 1504826c7a5..afd8f5da39f 100644 --- a/spec/lib/gitlab/bare_repository_import/repository_spec.rb +++ b/spec/lib/gitlab/bare_repository_import/repository_spec.rb @@ -62,8 +62,10 @@ describe ::Gitlab::BareRepositoryImport::Repository do before do gitlab_shell.create_repository(repository_storage, hashed_path) - repository = Rugged::Repository.new(repo_path) - repository.config['gitlab.fullpath'] = 'to/repo' + Gitlab::GitalyClient::StorageSettings.allow_disk_access do + repository = Rugged::Repository.new(repo_path) + repository.config['gitlab.fullpath'] = 'to/repo' + end end after do diff --git a/spec/lib/gitlab/checks/change_access_spec.rb b/spec/lib/gitlab/checks/change_access_spec.rb index 48e9902027c..1cb8143a9e9 100644 --- a/spec/lib/gitlab/checks/change_access_spec.rb +++ b/spec/lib/gitlab/checks/change_access_spec.rb @@ -52,7 +52,7 @@ describe Gitlab::Checks::ChangeAccess do context 'with protected tag' do let!(:protected_tag) { create(:protected_tag, project: project, name: 'v*') } - context 'as master' do + context 'as maintainer' do before do project.add_master(user) end @@ -138,7 +138,7 @@ describe Gitlab::Checks::ChangeAccess do context 'if the user is not allowed to delete protected branches' do it 'raises an error' do - expect { subject.exec }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You are not allowed to delete protected branches from this project. Only a project master or owner can delete a protected branch.') + expect { subject.exec }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You are not allowed to delete protected branches from this project. Only a project maintainer or owner can delete a protected branch.') end end diff --git a/spec/lib/gitlab/ci/pipeline/chain/populate_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/populate_spec.rb index 4d7d6951a51..c5a4d9b4778 100644 --- a/spec/lib/gitlab/ci/pipeline/chain/populate_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/chain/populate_spec.rb @@ -42,6 +42,10 @@ describe Gitlab::Ci::Pipeline::Chain::Populate do it 'correctly assigns user' do expect(pipeline.builds).to all(have_attributes(user: user)) end + + it 'has pipeline iid' do + expect(pipeline.iid).to be > 0 + end end context 'when pipeline is empty' do @@ -68,6 +72,10 @@ describe Gitlab::Ci::Pipeline::Chain::Populate do expect(pipeline.errors.to_a) .to include 'No stages / jobs for this pipeline.' end + + it 'wastes pipeline iid' do + expect(InternalId.ci_pipelines.where(project_id: project.id).last.last_value).to be > 0 + end end context 'when pipeline has validation errors' do @@ -87,6 +95,10 @@ describe Gitlab::Ci::Pipeline::Chain::Populate do expect(pipeline.errors.to_a) .to include 'Failed to build the pipeline!' end + + it 'wastes pipeline iid' do + expect(InternalId.ci_pipelines.where(project_id: project.id).last.last_value).to be > 0 + end end context 'when there is a seed blocks present' do @@ -111,6 +123,12 @@ describe Gitlab::Ci::Pipeline::Chain::Populate do expect(pipeline.variables.first.key).to eq 'VAR' expect(pipeline.variables.first.value).to eq '123' end + + it 'has pipeline iid' do + step.perform! + + expect(pipeline.iid).to be > 0 + end end context 'when seeds block tries to persist some resources' do @@ -121,6 +139,12 @@ describe Gitlab::Ci::Pipeline::Chain::Populate do it 'raises exception' do expect { step.perform! }.to raise_error(ActiveRecord::RecordNotSaved) end + + it 'wastes pipeline iid' do + expect { step.perform! }.to raise_error + + expect(InternalId.ci_pipelines.where(project_id: project.id).last.last_value).to be > 0 + end end end @@ -132,22 +156,39 @@ describe Gitlab::Ci::Pipeline::Chain::Populate do end end - context 'when using only/except build policies' do - let(:config) do - { rspec: { script: 'rspec', stage: 'test', only: ['master'] }, - prod: { script: 'cap prod', stage: 'deploy', only: ['tags'] } } - end + context 'when variables policy is specified' do + shared_examples_for 'a correct pipeline' do + it 'populates pipeline according to used policies' do + step.perform! - let(:pipeline) do - build(:ci_pipeline, ref: 'master', config: config) + expect(pipeline.stages.size).to eq 1 + expect(pipeline.stages.first.builds.size).to eq 1 + expect(pipeline.stages.first.builds.first.name).to eq 'rspec' + end end - it 'populates pipeline according to used policies' do - step.perform! + context 'when using only/except build policies' do + let(:config) do + { rspec: { script: 'rspec', stage: 'test', only: ['master'] }, + prod: { script: 'cap prod', stage: 'deploy', only: ['tags'] } } + end + + let(:pipeline) do + build(:ci_pipeline, ref: 'master', config: config) + end - expect(pipeline.stages.size).to eq 1 - expect(pipeline.stages.first.builds.size).to eq 1 - expect(pipeline.stages.first.builds.first.name).to eq 'rspec' + it_behaves_like 'a correct pipeline' + + context 'when variables expression is specified' do + context 'when pipeline iid is the subject' do + let(:config) do + { rspec: { script: 'rspec', only: { variables: ["$CI_PIPELINE_IID == '1'"] } }, + prod: { script: 'cap prod', only: { variables: ["$CI_PIPELINE_IID == '1000'"] } } } + end + + it_behaves_like 'a correct pipeline' + end + end end end end diff --git a/spec/lib/gitlab/ci/pipeline/preloader_spec.rb b/spec/lib/gitlab/ci/pipeline/preloader_spec.rb index 477c7477df0..40dfd893465 100644 --- a/spec/lib/gitlab/ci/pipeline/preloader_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/preloader_spec.rb @@ -3,18 +3,47 @@ require 'spec_helper' describe Gitlab::Ci::Pipeline::Preloader do - describe '.preload' do - it 'preloads the author of every pipeline commit' do - commit = double(:commit) - pipeline = double(:pipeline, commit: commit) + let(:stage) { double(:stage) } + let(:commit) { double(:commit) } - expect(commit) - .to receive(:lazy_author) + let(:pipeline) do + double(:pipeline, commit: commit, stages: [stage]) + end + + describe '.preload!' do + context 'when preloading multiple commits' do + let(:project) { create(:project, :repository) } + + it 'preloads all commits once' do + expect(Commit).to receive(:decorate).once.and_call_original + + pipelines = [build_pipeline(ref: 'HEAD'), + build_pipeline(ref: 'HEAD~1')] + + described_class.preload!(pipelines) + end + + def build_pipeline(ref:) + build_stubbed(:ci_pipeline, project: project, sha: project.commit(ref).id) + end + end + + it 'preloads commit authors and number of warnings' do + expect(commit).to receive(:lazy_author) + expect(pipeline).to receive(:number_of_warnings) + expect(stage).to receive(:number_of_warnings) + + described_class.preload!([pipeline]) + end + + it 'returns original collection' do + allow(commit).to receive(:lazy_author) + allow(pipeline).to receive(:number_of_warnings) + allow(stage).to receive(:number_of_warnings) - expect(pipeline) - .to receive(:number_of_warnings) + pipelines = [pipeline, pipeline] - described_class.preload([pipeline]) + expect(described_class.preload!(pipelines)).to eq pipelines end end end diff --git a/spec/lib/gitlab/ci/trace_spec.rb b/spec/lib/gitlab/ci/trace_spec.rb index e9d755c2021..d6510649dba 100644 --- a/spec/lib/gitlab/ci/trace_spec.rb +++ b/spec/lib/gitlab/ci/trace_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Gitlab::Ci::Trace, :clean_gitlab_redis_cache do +describe Gitlab::Ci::Trace, :clean_gitlab_redis_shared_state do let(:build) { create(:ci_build) } let(:trace) { described_class.new(build) } diff --git a/spec/lib/gitlab/current_settings_spec.rb b/spec/lib/gitlab/current_settings_spec.rb index 19028495f52..55490f37ac7 100644 --- a/spec/lib/gitlab/current_settings_spec.rb +++ b/spec/lib/gitlab/current_settings_spec.rb @@ -5,6 +5,13 @@ describe Gitlab::CurrentSettings do stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false') end + shared_context 'with settings in cache' do + before do + create(:application_setting) + described_class.current_application_settings # warm the cache + end + end + describe '#current_application_settings', :use_clean_rails_memory_store_caching do it 'allows keys to be called directly' do db_settings = create(:application_setting, @@ -31,16 +38,29 @@ describe Gitlab::CurrentSettings do end context 'with DB unavailable' do - before do - # For some reason, `allow(described_class).to receive(:connect_to_db?).and_return(false)` causes issues - # during the initialization phase of the test suite, so instead let's mock the internals of it - allow(ActiveRecord::Base.connection).to receive(:active?).and_return(false) + context 'and settings in cache' do + include_context 'with settings in cache' + + it 'fetches the settings from cache without issuing any query' do + expect(ActiveRecord::QueryRecorder.new { described_class.current_application_settings }.count).to eq(0) + end end - it 'returns an in-memory ApplicationSetting object' do - expect(ApplicationSetting).not_to receive(:current) + context 'and no settings in cache' do + before do + # For some reason, `allow(described_class).to receive(:connect_to_db?).and_return(false)` causes issues + # during the initialization phase of the test suite, so instead let's mock the internals of it + allow(ActiveRecord::Base.connection).to receive(:active?).and_return(false) + expect(ApplicationSetting).not_to receive(:current) + end - expect(described_class.current_application_settings).to be_a(Gitlab::FakeApplicationSettings) + it 'returns an in-memory ApplicationSetting object' do + expect(described_class.current_application_settings).to be_a(Gitlab::FakeApplicationSettings) + end + + it 'does not issue any query' do + expect(ActiveRecord::QueryRecorder.new { described_class.current_application_settings }.count).to eq(0) + end end end @@ -52,73 +72,86 @@ describe Gitlab::CurrentSettings do ar_wrapped_defaults.slice(*::ApplicationSetting.defaults.keys) end - before do - # For some reason, `allow(described_class).to receive(:connect_to_db?).and_return(true)` causes issues - # during the initialization phase of the test suite, so instead let's mock the internals of it - allow(ActiveRecord::Base.connection).to receive(:active?).and_return(true) - allow(ActiveRecord::Base.connection).to receive(:cached_table_exists?).with('application_settings').and_return(true) - end + context 'and settings in cache' do + include_context 'with settings in cache' - it 'creates default ApplicationSettings if none are present' do - settings = described_class.current_application_settings - - expect(settings).to be_a(ApplicationSetting) - expect(settings).to be_persisted - expect(settings).to have_attributes(settings_from_defaults) + it 'fetches the settings from cache' do + # For some reason, `allow(described_class).to receive(:connect_to_db?).and_return(true)` causes issues + # during the initialization phase of the test suite, so instead let's mock the internals of it + expect(ActiveRecord::Base.connection).not_to receive(:active?) + expect(ActiveRecord::Base.connection).not_to receive(:cached_table_exists?) + expect(ActiveRecord::Migrator).not_to receive(:needs_migration?) + expect(ActiveRecord::QueryRecorder.new { described_class.current_application_settings }.count).to eq(0) + end end - context 'with migrations pending' do + context 'and no settings in cache' do before do - expect(ActiveRecord::Migrator).to receive(:needs_migration?).and_return(true) + allow(ActiveRecord::Base.connection).to receive(:active?).and_return(true) + allow(ActiveRecord::Base.connection).to receive(:cached_table_exists?).with('application_settings').and_return(true) end - it 'returns an in-memory ApplicationSetting object' do + it 'creates default ApplicationSettings if none are present' do settings = described_class.current_application_settings - expect(settings).to be_a(Gitlab::FakeApplicationSettings) - expect(settings.sign_in_enabled?).to eq(settings.sign_in_enabled) - expect(settings.sign_up_enabled?).to eq(settings.sign_up_enabled) + expect(settings).to be_a(ApplicationSetting) + expect(settings).to be_persisted + expect(settings).to have_attributes(settings_from_defaults) end - it 'uses the existing database settings and falls back to defaults' do - db_settings = create(:application_setting, - home_page_url: 'http://mydomain.com', - signup_enabled: false) - settings = described_class.current_application_settings - app_defaults = ApplicationSetting.last - - expect(settings).to be_a(Gitlab::FakeApplicationSettings) - expect(settings.home_page_url).to eq(db_settings.home_page_url) - expect(settings.signup_enabled?).to be_falsey - expect(settings.signup_enabled).to be_falsey - - # Check that unspecified values use the defaults - settings.reject! { |key, _| [:home_page_url, :signup_enabled].include? key } - settings.each { |key, _| expect(settings[key]).to eq(app_defaults[key]) } + context 'with migrations pending' do + before do + expect(ActiveRecord::Migrator).to receive(:needs_migration?).and_return(true) + end + + it 'returns an in-memory ApplicationSetting object' do + settings = described_class.current_application_settings + + expect(settings).to be_a(Gitlab::FakeApplicationSettings) + expect(settings.sign_in_enabled?).to eq(settings.sign_in_enabled) + expect(settings.sign_up_enabled?).to eq(settings.sign_up_enabled) + end + + it 'uses the existing database settings and falls back to defaults' do + db_settings = create(:application_setting, + home_page_url: 'http://mydomain.com', + signup_enabled: false) + settings = described_class.current_application_settings + app_defaults = ApplicationSetting.last + + expect(settings).to be_a(Gitlab::FakeApplicationSettings) + expect(settings.home_page_url).to eq(db_settings.home_page_url) + expect(settings.signup_enabled?).to be_falsey + expect(settings.signup_enabled).to be_falsey + + # Check that unspecified values use the defaults + settings.reject! { |key, _| [:home_page_url, :signup_enabled].include? key } + settings.each { |key, _| expect(settings[key]).to eq(app_defaults[key]) } + end end - end - context 'when ApplicationSettings.current is present' do - it 'returns the existing application settings' do - expect(ApplicationSetting).to receive(:current).and_return(:current_settings) + context 'when ApplicationSettings.current is present' do + it 'returns the existing application settings' do + expect(ApplicationSetting).to receive(:current).and_return(:current_settings) - expect(described_class.current_application_settings).to eq(:current_settings) + expect(described_class.current_application_settings).to eq(:current_settings) + end end - end - context 'when the application_settings table does not exists' do - it 'returns an in-memory ApplicationSetting object' do - expect(ApplicationSetting).to receive(:create_from_defaults).and_raise(ActiveRecord::StatementInvalid) + context 'when the application_settings table does not exists' do + it 'returns an in-memory ApplicationSetting object' do + expect(ApplicationSetting).to receive(:create_from_defaults).and_raise(ActiveRecord::StatementInvalid) - expect(described_class.current_application_settings).to be_a(Gitlab::FakeApplicationSettings) + expect(described_class.current_application_settings).to be_a(Gitlab::FakeApplicationSettings) + end end - end - context 'when the application_settings table is not fully migrated' do - it 'returns an in-memory ApplicationSetting object' do - expect(ApplicationSetting).to receive(:create_from_defaults).and_raise(ActiveRecord::UnknownAttributeError) + context 'when the application_settings table is not fully migrated' do + it 'returns an in-memory ApplicationSetting object' do + expect(ApplicationSetting).to receive(:create_from_defaults).and_raise(ActiveRecord::UnknownAttributeError) - expect(described_class.current_application_settings).to be_a(Gitlab::FakeApplicationSettings) + expect(described_class.current_application_settings).to be_a(Gitlab::FakeApplicationSettings) + end end end end diff --git a/spec/lib/gitlab/cycle_analytics/usage_data_spec.rb b/spec/lib/gitlab/cycle_analytics/usage_data_spec.rb index 56a316318cb..a785b17f682 100644 --- a/spec/lib/gitlab/cycle_analytics/usage_data_spec.rb +++ b/spec/lib/gitlab/cycle_analytics/usage_data_spec.rb @@ -3,7 +3,12 @@ require 'spec_helper' describe Gitlab::CycleAnalytics::UsageData do describe '#to_json' do before do - Timecop.freeze do + # Since git commits only have second precision, round up to the + # nearest second to ensure we have accurate median and standard + # deviation calculations. + current_time = Time.at(Time.now.to_i) + + Timecop.freeze(current_time) do user = create(:user, :admin) projects = create_list(:project, 2, :repository) @@ -37,13 +42,7 @@ describe Gitlab::CycleAnalytics::UsageData do expected_values.each_pair do |op, value| expect(stage_values).to have_key(op) - - if op == :missing - expect(stage_values[op]).to eq(value) - else - # delta is used because of git timings that Timecop does not stub - expect(stage_values[op].to_i).to be_within(5).of(value.to_i) - end + expect(stage_values[op]).to eq(value) end end end @@ -58,8 +57,8 @@ describe Gitlab::CycleAnalytics::UsageData do missing: 0 }, plan: { - average: 2, - sd: 2, + average: 1, + sd: 0, missing: 0 }, code: { diff --git a/spec/lib/gitlab/database_spec.rb b/spec/lib/gitlab/database_spec.rb index 8ac36ae8bab..8bb246aa4bd 100644 --- a/spec/lib/gitlab/database_spec.rb +++ b/spec/lib/gitlab/database_spec.rb @@ -314,8 +314,13 @@ describe Gitlab::Database do describe '.cached_table_exists?' do it 'only retrieves data once per table' do - expect(ActiveRecord::Base.connection).to receive(:table_exists?).with(:projects).once.and_call_original - expect(ActiveRecord::Base.connection).to receive(:table_exists?).with(:bogus_table_name).once.and_call_original + if Gitlab.rails5? + expect(ActiveRecord::Base.connection).to receive(:data_source_exists?).with(:projects).once.and_call_original + expect(ActiveRecord::Base.connection).to receive(:data_source_exists?).with(:bogus_table_name).once.and_call_original + else + expect(ActiveRecord::Base.connection).to receive(:table_exists?).with(:projects).once.and_call_original + expect(ActiveRecord::Base.connection).to receive(:table_exists?).with(:bogus_table_name).once.and_call_original + end 2.times do expect(described_class.cached_table_exists?(:projects)).to be_truthy diff --git a/spec/lib/gitlab/diff/file_spec.rb b/spec/lib/gitlab/diff/file_spec.rb index 0588fe935c3..5dfbb8e71f8 100644 --- a/spec/lib/gitlab/diff/file_spec.rb +++ b/spec/lib/gitlab/diff/file_spec.rb @@ -79,7 +79,9 @@ describe Gitlab::Diff::File do let(:diffs) { commit.diffs } before do - info_dir_path = File.join(project.repository.path_to_repo, 'info') + info_dir_path = Gitlab::GitalyClient::StorageSettings.allow_disk_access do + File.join(project.repository.path_to_repo, 'info') + end FileUtils.mkdir(info_dir_path) unless File.exist?(info_dir_path) File.write(File.join(info_dir_path, 'attributes'), "*.md -diff\n") @@ -470,56 +472,69 @@ describe Gitlab::Diff::File do end describe '#diff_hunk' do - let(:raw_diff) do - <<EOS -@@ -6,12 +6,18 @@ module Popen - - def popen(cmd, path=nil) - unless cmd.is_a?(Array) -- raise "System commands must be given as an array of strings" -+ raise RuntimeError, "System commands must be given as an array of strings" - end - - path ||= Dir.pwd -- vars = { "PWD" => path } -- options = { chdir: path } -+ -+ vars = { -+ "PWD" => path -+ } -+ -+ options = { -+ chdir: path -+ } - - unless File.directory?(path) - FileUtils.mkdir_p(path) -@@ -19,6 +25,7 @@ module Popen - - @cmd_output = "" - @cmd_status = 0 -+ - Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr| - @cmd_output << stdout.read - @cmd_output << stderr.read -EOS - end - - it 'returns raw diff up to given line index' do - allow(diff_file).to receive(:raw_diff) { raw_diff } - diff_line = instance_double(Gitlab::Diff::Line, index: 5) - - diff_hunk = <<EOS -@@ -6,12 +6,18 @@ module Popen - - def popen(cmd, path=nil) - unless cmd.is_a?(Array) -- raise "System commands must be given as an array of strings" -+ raise RuntimeError, "System commands must be given as an array of strings" - end -EOS - - expect(diff_file.diff_hunk(diff_line)).to eq(diff_hunk) + context 'when first line is a match' do + let(:raw_diff) do + <<~EOS + --- a/files/ruby/popen.rb + +++ b/files/ruby/popen.rb + @@ -6,12 +6,18 @@ module Popen + + def popen(cmd, path=nil) + unless cmd.is_a?(Array) + - raise "System commands must be given as an array of strings" + + raise RuntimeError, "System commands must be given as an array of strings" + end + EOS + end + + it 'returns raw diff up to given line index' do + allow(diff_file).to receive(:raw_diff) { raw_diff } + diff_line = instance_double(Gitlab::Diff::Line, index: 4) + + diff_hunk = <<~EOS + @@ -6,12 +6,18 @@ module Popen + + def popen(cmd, path=nil) + unless cmd.is_a?(Array) + - raise "System commands must be given as an array of strings" + + raise RuntimeError, "System commands must be given as an array of strings" + EOS + + expect(diff_file.diff_hunk(diff_line)).to eq(diff_hunk.strip) + end + end + + context 'when first line is not a match' do + let(:raw_diff) do + <<~EOS + @@ -1,4 +1,4 @@ + -Copyright (c) 2011-2017 GitLab B.V. + +Copyright (c) 2011-2019 GitLab B.V. + + With regard to the GitLab Software: + + @@ -9,17 +9,21 @@ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + EOS + end + + it 'returns raw diff up to given line index' do + allow(diff_file).to receive(:raw_diff) { raw_diff } + diff_line = instance_double(Gitlab::Diff::Line, index: 5) + + diff_hunk = <<~EOS + -Copyright (c) 2011-2017 GitLab B.V. + +Copyright (c) 2011-2019 GitLab B.V. + + With regard to the GitLab Software: + + @@ -9,17 +9,21 @@ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + EOS + + expect(diff_file.diff_hunk(diff_line)).to eq(diff_hunk.strip) + end end end end diff --git a/spec/lib/gitlab/favicon_spec.rb b/spec/lib/gitlab/favicon_spec.rb new file mode 100644 index 00000000000..f36111a4946 --- /dev/null +++ b/spec/lib/gitlab/favicon_spec.rb @@ -0,0 +1,52 @@ +require 'rails_helper' + +RSpec.describe Gitlab::Favicon, :request_store do + describe '.main' do + it 'defaults to favicon.png' do + allow(Rails).to receive(:env).and_return(ActiveSupport::StringInquirer.new('production')) + expect(described_class.main).to match_asset_path '/assets/favicon.png' + end + + it 'has blue favicon for development' do + allow(Rails).to receive(:env).and_return(ActiveSupport::StringInquirer.new('development')) + expect(described_class.main).to match_asset_path '/assets/favicon-blue.png' + end + + it 'has yellow favicon for canary' do + stub_env('CANARY', 'true') + expect(described_class.main).to match_asset_path 'favicon-yellow.png' + end + + it 'uses the custom favicon if a favicon appearance is present' do + create :appearance, favicon: fixture_file_upload('spec/fixtures/dk.png') + expect(described_class.main).to match %r{/uploads/-/system/appearance/favicon/\d+/dk.png} + end + end + + describe '.status_overlay' do + subject { described_class.status_overlay('favicon_status_created') } + + it 'returns the overlay for the status' do + expect(subject).to match_asset_path '/assets/ci_favicons/favicon_status_created.png' + end + end + + describe '.available_status_names' do + subject { described_class.available_status_names } + + it 'returns the available status names' do + expect(subject).to eq %w( + favicon_status_canceled + favicon_status_created + favicon_status_failed + favicon_status_manual + favicon_status_not_found + favicon_status_pending + favicon_status_running + favicon_status_skipped + favicon_status_success + favicon_status_warning + ) + end + end +end diff --git a/spec/lib/gitlab/file_finder_spec.rb b/spec/lib/gitlab/file_finder_spec.rb index 07cb10e563e..d6d9e4001a3 100644 --- a/spec/lib/gitlab/file_finder_spec.rb +++ b/spec/lib/gitlab/file_finder_spec.rb @@ -3,27 +3,11 @@ require 'spec_helper' describe Gitlab::FileFinder do describe '#find' do let(:project) { create(:project, :public, :repository) } - let(:finder) { described_class.new(project, project.default_branch) } - it 'finds by name' do - results = finder.find('files') - - filename, blob = results.find { |_, blob| blob.filename == 'files/images/wm.svg' } - expect(filename).to eq('files/images/wm.svg') - expect(blob).to be_a(Gitlab::SearchResults::FoundBlob) - expect(blob.ref).to eq(finder.ref) - expect(blob.data).not_to be_empty - end - - it 'finds by content' do - results = finder.find('files') - - filename, blob = results.find { |_, blob| blob.filename == 'CHANGELOG' } - - expect(filename).to eq('CHANGELOG') - expect(blob).to be_a(Gitlab::SearchResults::FoundBlob) - expect(blob.ref).to eq(finder.ref) - expect(blob.data).not_to be_empty + it_behaves_like 'file finder' do + subject { described_class.new(project, project.default_branch) } + let(:expected_file_by_name) { 'files/images/wm.svg' } + let(:expected_file_by_content) { 'CHANGELOG' } end end end diff --git a/spec/lib/gitlab/git/blame_spec.rb b/spec/lib/gitlab/git/blame_spec.rb index 793228701cf..ba790b717ae 100644 --- a/spec/lib/gitlab/git/blame_spec.rb +++ b/spec/lib/gitlab/git/blame_spec.rb @@ -7,7 +7,7 @@ describe Gitlab::Git::Blame, seed_helper: true do Gitlab::Git::Blame.new(repository, SeedRepo::Commit::ID, "CONTRIBUTING.md") end - shared_examples 'blaming a file' do + describe 'blaming a file' do context "each count" do it do data = [] @@ -68,12 +68,4 @@ describe Gitlab::Git::Blame, seed_helper: true do end end end - - context 'when Gitaly blame feature is enabled' do - it_behaves_like 'blaming a file' - end - - context 'when Gitaly blame feature is disabled', :skip_gitaly_mock do - it_behaves_like 'blaming a file' - end end diff --git a/spec/lib/gitlab/git/blob_spec.rb b/spec/lib/gitlab/git/blob_spec.rb index 94eaf86ef80..6015086f002 100644 --- a/spec/lib/gitlab/git/blob_spec.rb +++ b/spec/lib/gitlab/git/blob_spec.rb @@ -149,7 +149,9 @@ describe Gitlab::Git::Blob, seed_helper: true do it 'limits the size of a large file' do blob_size = Gitlab::Git::Blob::MAX_DATA_DISPLAY_SIZE + 1 buffer = Array.new(blob_size, 0) - rugged_blob = Rugged::Blob.from_buffer(repository.rugged, buffer.join('')) + rugged_blob = Gitlab::GitalyClient::StorageSettings.allow_disk_access do + Rugged::Blob.from_buffer(repository.rugged, buffer.join('')) + end blob = Gitlab::Git::Blob.raw(repository, rugged_blob) expect(blob.size).to eq(blob_size) @@ -164,7 +166,9 @@ describe Gitlab::Git::Blob, seed_helper: true do context 'when sha references a tree' do it 'returns nil' do - tree = repository.rugged.rev_parse('master^{tree}') + tree = Gitlab::GitalyClient::StorageSettings.allow_disk_access do + repository.rugged.rev_parse('master^{tree}') + end blob = Gitlab::Git::Blob.raw(repository, tree.oid) @@ -278,7 +282,11 @@ describe Gitlab::Git::Blob, seed_helper: true do end describe '.batch_lfs_pointers' do - let(:tree_object) { repository.rugged.rev_parse('master^{tree}') } + let(:tree_object) do + Gitlab::GitalyClient::StorageSettings.allow_disk_access do + repository.rugged.rev_parse('master^{tree}') + end + end let(:non_lfs_blob) do Gitlab::Git::Blob.find( diff --git a/spec/lib/gitlab/git/branch_spec.rb b/spec/lib/gitlab/git/branch_spec.rb index a19155ed5b0..ec1a684cfbc 100644 --- a/spec/lib/gitlab/git/branch_spec.rb +++ b/spec/lib/gitlab/git/branch_spec.rb @@ -69,7 +69,9 @@ describe Gitlab::Git::Branch, seed_helper: true do Gitlab::Git.committer_hash(email: user.email, name: user.name) end let(:params) do - parents = [repository.rugged.head.target] + parents = Gitlab::GitalyClient::StorageSettings.allow_disk_access do + [repository.rugged.head.target] + end tree = parents.first.tree { diff --git a/spec/lib/gitlab/git/commit_spec.rb b/spec/lib/gitlab/git/commit_spec.rb index 89be8a1b7f2..ae69a362dda 100644 --- a/spec/lib/gitlab/git/commit_spec.rb +++ b/spec/lib/gitlab/git/commit_spec.rb @@ -4,12 +4,15 @@ describe Gitlab::Git::Commit, seed_helper: true do let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '') } let(:commit) { described_class.find(repository, SeedRepo::Commit::ID) } let(:rugged_commit) do - repository.rugged.lookup(SeedRepo::Commit::ID) + Gitlab::GitalyClient::StorageSettings.allow_disk_access do + repository.rugged.lookup(SeedRepo::Commit::ID) + end end - describe "Commit info" do before do - repo = Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '').rugged + repo = Gitlab::GitalyClient::StorageSettings.allow_disk_access do + Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '').rugged + end @committer = { email: 'mike@smith.com', @@ -58,7 +61,9 @@ describe Gitlab::Git::Commit, seed_helper: true do after do # Erase the new commit so other tests get the original repo - repo = Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '').rugged + repo = Gitlab::GitalyClient::StorageSettings.allow_disk_access do + Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '').rugged + end repo.references.update("refs/heads/master", SeedRepo::LastCommit::ID) end end @@ -115,7 +120,9 @@ describe Gitlab::Git::Commit, seed_helper: true do describe '.find' do it "should return first head commit if without params" do expect(described_class.last(repository).id).to eq( - repository.rugged.head.target.oid + Gitlab::GitalyClient::StorageSettings.allow_disk_access do + repository.rugged.head.target.oid + end ) end @@ -414,6 +421,16 @@ describe Gitlab::Git::Commit, seed_helper: true do end end + describe '#batch_by_oid' do + context 'when oids is empty' do + it 'makes no Gitaly request' do + expect(Gitlab::GitalyClient).not_to receive(:call) + + described_class.batch_by_oid(repository, []) + end + end + end + shared_examples 'extracting commit signature' do context 'when the commit is signed' do let(:commit_id) { '0b4bc9a49b562e85de7cc9e834518ea6828729b9' } diff --git a/spec/lib/gitlab/git/committer_with_hooks_spec.rb b/spec/lib/gitlab/git/committer_with_hooks_spec.rb index 267056b96e6..2100690f873 100644 --- a/spec/lib/gitlab/git/committer_with_hooks_spec.rb +++ b/spec/lib/gitlab/git/committer_with_hooks_spec.rb @@ -1,154 +1,156 @@ require 'spec_helper' describe Gitlab::Git::CommitterWithHooks, seed_helper: true do - shared_examples 'calling wiki hooks' do - let(:project) { create(:project) } - let(:user) { project.owner } - let(:project_wiki) { ProjectWiki.new(project, user) } - let(:wiki) { project_wiki.wiki } - let(:options) do - { - id: user.id, - username: user.username, - name: user.name, - email: user.email, - message: 'commit message' - } - end - - subject { described_class.new(wiki, options) } + # TODO https://gitlab.com/gitlab-org/gitaly/issues/1234 + skip 'needs to be moved to gitaly-ruby test suite' do + shared_examples 'calling wiki hooks' do + let(:project) { create(:project) } + let(:user) { project.owner } + let(:project_wiki) { ProjectWiki.new(project, user) } + let(:wiki) { project_wiki.wiki } + let(:options) do + { + id: user.id, + username: user.username, + name: user.name, + email: user.email, + message: 'commit message' + } + end - before do - project_wiki.create_page('home', 'test content') - end + subject { described_class.new(wiki, options) } - shared_examples 'failing pre-receive hook' do before do - expect_any_instance_of(Gitlab::Git::HooksService).to receive(:run_hook).with('pre-receive').and_return([false, '']) - expect_any_instance_of(Gitlab::Git::HooksService).not_to receive(:run_hook).with('update') - expect_any_instance_of(Gitlab::Git::HooksService).not_to receive(:run_hook).with('post-receive') + project_wiki.create_page('home', 'test content') end - it 'raises exception' do - expect { subject.commit }.to raise_error(Gitlab::Git::Wiki::OperationError) - end + shared_examples 'failing pre-receive hook' do + before do + expect_any_instance_of(Gitlab::Git::HooksService).to receive(:run_hook).with('pre-receive').and_return([false, '']) + expect_any_instance_of(Gitlab::Git::HooksService).not_to receive(:run_hook).with('update') + expect_any_instance_of(Gitlab::Git::HooksService).not_to receive(:run_hook).with('post-receive') + end - it 'does not create a new commit inside the repository' do - current_rev = find_current_rev + it 'raises exception' do + expect { subject.commit }.to raise_error(Gitlab::Git::Wiki::OperationError) + end - expect { subject.commit }.to raise_error(Gitlab::Git::Wiki::OperationError) + it 'does not create a new commit inside the repository' do + current_rev = find_current_rev - expect(current_rev).to eq find_current_rev - end - end + expect { subject.commit }.to raise_error(Gitlab::Git::Wiki::OperationError) - shared_examples 'failing update hook' do - before do - expect_any_instance_of(Gitlab::Git::HooksService).to receive(:run_hook).with('pre-receive').and_return([true, '']) - expect_any_instance_of(Gitlab::Git::HooksService).to receive(:run_hook).with('update').and_return([false, '']) - expect_any_instance_of(Gitlab::Git::HooksService).not_to receive(:run_hook).with('post-receive') + expect(current_rev).to eq find_current_rev + end end - it 'raises exception' do - expect { subject.commit }.to raise_error(Gitlab::Git::Wiki::OperationError) - end + shared_examples 'failing update hook' do + before do + expect_any_instance_of(Gitlab::Git::HooksService).to receive(:run_hook).with('pre-receive').and_return([true, '']) + expect_any_instance_of(Gitlab::Git::HooksService).to receive(:run_hook).with('update').and_return([false, '']) + expect_any_instance_of(Gitlab::Git::HooksService).not_to receive(:run_hook).with('post-receive') + end - it 'does not create a new commit inside the repository' do - current_rev = find_current_rev + it 'raises exception' do + expect { subject.commit }.to raise_error(Gitlab::Git::Wiki::OperationError) + end - expect { subject.commit }.to raise_error(Gitlab::Git::Wiki::OperationError) + it 'does not create a new commit inside the repository' do + current_rev = find_current_rev - expect(current_rev).to eq find_current_rev - end - end + expect { subject.commit }.to raise_error(Gitlab::Git::Wiki::OperationError) - shared_examples 'failing post-receive hook' do - before do - expect_any_instance_of(Gitlab::Git::HooksService).to receive(:run_hook).with('pre-receive').and_return([true, '']) - expect_any_instance_of(Gitlab::Git::HooksService).to receive(:run_hook).with('update').and_return([true, '']) - expect_any_instance_of(Gitlab::Git::HooksService).to receive(:run_hook).with('post-receive').and_return([false, '']) + expect(current_rev).to eq find_current_rev + end end - it 'does not raise exception' do - expect { subject.commit }.not_to raise_error - end + shared_examples 'failing post-receive hook' do + before do + expect_any_instance_of(Gitlab::Git::HooksService).to receive(:run_hook).with('pre-receive').and_return([true, '']) + expect_any_instance_of(Gitlab::Git::HooksService).to receive(:run_hook).with('update').and_return([true, '']) + expect_any_instance_of(Gitlab::Git::HooksService).to receive(:run_hook).with('post-receive').and_return([false, '']) + end + + it 'does not raise exception' do + expect { subject.commit }.not_to raise_error + end - it 'creates the commit' do - current_rev = find_current_rev + it 'creates the commit' do + current_rev = find_current_rev - subject.commit + subject.commit - expect(current_rev).not_to eq find_current_rev + expect(current_rev).not_to eq find_current_rev + end end - end - shared_examples 'when hooks call succceeds' do - let(:hook) { double(:hook) } + shared_examples 'when hooks call succceeds' do + let(:hook) { double(:hook) } - it 'calls the three hooks' do - expect(Gitlab::Git::Hook).to receive(:new).exactly(3).times.and_return(hook) - expect(hook).to receive(:trigger).exactly(3).times.and_return([true, nil]) + it 'calls the three hooks' do + expect(Gitlab::Git::Hook).to receive(:new).exactly(3).times.and_return(hook) + expect(hook).to receive(:trigger).exactly(3).times.and_return([true, nil]) - subject.commit - end + subject.commit + end - it 'creates the commit' do - current_rev = find_current_rev + it 'creates the commit' do + current_rev = find_current_rev - subject.commit + subject.commit - expect(current_rev).not_to eq find_current_rev + expect(current_rev).not_to eq find_current_rev + end end - end - context 'when creating a page' do - before do - project_wiki.create_page('index', 'test content') + context 'when creating a page' do + before do + project_wiki.create_page('index', 'test content') + end + + it_behaves_like 'failing pre-receive hook' + it_behaves_like 'failing update hook' + it_behaves_like 'failing post-receive hook' + it_behaves_like 'when hooks call succceeds' end - it_behaves_like 'failing pre-receive hook' - it_behaves_like 'failing update hook' - it_behaves_like 'failing post-receive hook' - it_behaves_like 'when hooks call succceeds' - end + context 'when updating a page' do + before do + project_wiki.update_page(find_page('home'), content: 'some other content', format: :markdown) + end - context 'when updating a page' do - before do - project_wiki.update_page(find_page('home'), content: 'some other content', format: :markdown) + it_behaves_like 'failing pre-receive hook' + it_behaves_like 'failing update hook' + it_behaves_like 'failing post-receive hook' + it_behaves_like 'when hooks call succceeds' end - it_behaves_like 'failing pre-receive hook' - it_behaves_like 'failing update hook' - it_behaves_like 'failing post-receive hook' - it_behaves_like 'when hooks call succceeds' - end + context 'when deleting a page' do + before do + project_wiki.delete_page(find_page('home')) + end - context 'when deleting a page' do - before do - project_wiki.delete_page(find_page('home')) + it_behaves_like 'failing pre-receive hook' + it_behaves_like 'failing update hook' + it_behaves_like 'failing post-receive hook' + it_behaves_like 'when hooks call succceeds' end - it_behaves_like 'failing pre-receive hook' - it_behaves_like 'failing update hook' - it_behaves_like 'failing post-receive hook' - it_behaves_like 'when hooks call succceeds' - end + def find_current_rev + wiki.gollum_wiki.repo.commits.first&.sha + end - def find_current_rev - wiki.gollum_wiki.repo.commits.first&.sha + def find_page(name) + wiki.page(title: name) + end end - def find_page(name) - wiki.page(title: name) + context 'when Gitaly is enabled' do + it_behaves_like 'calling wiki hooks' end - end - - # TODO: Uncomment once Gitaly updates the ruby vendor code - # context 'when Gitaly is enabled' do - # it_behaves_like 'calling wiki hooks' - # end - context 'when Gitaly is disabled', :skip_gitaly_mock do - it_behaves_like 'calling wiki hooks' + context 'when Gitaly is disabled', :disable_gitaly do + it_behaves_like 'calling wiki hooks' + end end end diff --git a/spec/lib/gitlab/git/diff_spec.rb b/spec/lib/gitlab/git/diff_spec.rb index 4a7b06003fc..3bb0b5be15b 100644 --- a/spec/lib/gitlab/git/diff_spec.rb +++ b/spec/lib/gitlab/git/diff_spec.rb @@ -27,8 +27,10 @@ EOT too_large: false } - @rugged_diff = repository.rugged.diff("5937ac0a7beb003549fc5fd26fc247adbce4a52e^", "5937ac0a7beb003549fc5fd26fc247adbce4a52e", paths: - [".gitmodules"]).patches.first + @rugged_diff = Gitlab::GitalyClient::StorageSettings.allow_disk_access do + repository.rugged.diff("5937ac0a7beb003549fc5fd26fc247adbce4a52e^", "5937ac0a7beb003549fc5fd26fc247adbce4a52e", paths: + [".gitmodules"]).patches.first + end end describe '.new' do diff --git a/spec/lib/gitlab/git/gitlab_projects_spec.rb b/spec/lib/gitlab/git/gitlab_projects_spec.rb index 8b715d717c1..f5d8503c30c 100644 --- a/spec/lib/gitlab/git/gitlab_projects_spec.rb +++ b/spec/lib/gitlab/git/gitlab_projects_spec.rb @@ -5,6 +5,13 @@ describe Gitlab::Git::GitlabProjects do TestEnv.clean_test_path end + around do |example| + # TODO move this spec to gitaly-ruby. GitlabProjects is not used in gitlab-ce + Gitlab::GitalyClient::StorageSettings.allow_disk_access do + example.run + end + end + let(:project) { create(:project, :repository) } if $VERBOSE @@ -190,36 +197,30 @@ describe Gitlab::Git::GitlabProjects do end end - context 'when Gitaly import_repository feature is enabled' do - it_behaves_like 'importing repository' - end + describe 'logging' do + it 'imports a repo' do + message = "Importing project from <#{import_url}> to <#{tmp_repo_path}>." + expect(logger).to receive(:info).with(message) - context 'when Gitaly import_repository feature is disabled', :disable_gitaly do - describe 'logging' do - it 'imports a repo' do - message = "Importing project from <#{import_url}> to <#{tmp_repo_path}>." - expect(logger).to receive(:info).with(message) - - subject - end + subject end + end - context 'timeout' do - it 'does not import a repo' do - stub_spawn_timeout(cmd, timeout, nil) + context 'timeout' do + it 'does not import a repo' do + stub_spawn_timeout(cmd, timeout, nil) - message = "Importing project from <#{import_url}> to <#{tmp_repo_path}> failed." - expect(logger).to receive(:error).with(message) + message = "Importing project from <#{import_url}> to <#{tmp_repo_path}> failed." + expect(logger).to receive(:error).with(message) - is_expected.to be_falsy + is_expected.to be_falsy - expect(gl_projects.output).to eq("Timed out\n") - expect(File.exist?(File.join(tmp_repo_path, 'HEAD'))).to be_falsy - end + expect(gl_projects.output).to eq("Timed out\n") + expect(File.exist?(File.join(tmp_repo_path, 'HEAD'))).to be_falsy end - - it_behaves_like 'importing repository' end + + it_behaves_like 'importing repository' end describe '#fork_repository' do @@ -232,9 +233,6 @@ describe Gitlab::Git::GitlabProjects do before do FileUtils.mkdir_p(dest_repos_path) - - # Undo spec_helper stub that deletes hooks - allow_any_instance_of(described_class).to receive(:fork_repository).and_call_original end after do @@ -258,51 +256,45 @@ describe Gitlab::Git::GitlabProjects do end end - context 'when Gitaly fork_repository feature is enabled' do - it_behaves_like 'forking a repository' - end - - context 'when Gitaly fork_repository feature is disabled', :disable_gitaly do - it_behaves_like 'forking a repository' + it_behaves_like 'forking a repository' - # We seem to be stuck to having only one working Gitaly storage in tests, changing - # that is not very straight-forward so I'm leaving this test here for now till - # https://gitlab.com/gitlab-org/gitlab-ce/issues/41393 is fixed. - context 'different storages' do - let(:dest_repos) { 'alternative' } - let(:dest_repos_path) { File.join(File.dirname(tmp_repos_path), dest_repos) } + # We seem to be stuck to having only one working Gitaly storage in tests, changing + # that is not very straight-forward so I'm leaving this test here for now till + # https://gitlab.com/gitlab-org/gitlab-ce/issues/41393 is fixed. + context 'different storages' do + let(:dest_repos) { 'alternative' } + let(:dest_repos_path) { File.join(File.dirname(tmp_repos_path), dest_repos) } - before do - stub_storage_settings(dest_repos => { 'path' => dest_repos_path }) - end + before do + stub_storage_settings(dest_repos => { 'path' => dest_repos_path }) + end - it 'forks the repo' do - is_expected.to be_truthy + it 'forks the repo' do + is_expected.to be_truthy - expect(File.exist?(dest_repo)).to be_truthy - expect(File.exist?(File.join(dest_repo, 'hooks', 'pre-receive'))).to be_truthy - expect(File.exist?(File.join(dest_repo, 'hooks', 'post-receive'))).to be_truthy - end + expect(File.exist?(dest_repo)).to be_truthy + expect(File.exist?(File.join(dest_repo, 'hooks', 'pre-receive'))).to be_truthy + expect(File.exist?(File.join(dest_repo, 'hooks', 'post-receive'))).to be_truthy end + end - describe 'log messages' do - describe 'successful fork' do - it do - message = "Forking repository from <#{tmp_repo_path}> to <#{dest_repo}>." - expect(logger).to receive(:info).with(message) + describe 'log messages' do + describe 'successful fork' do + it do + message = "Forking repository from <#{tmp_repo_path}> to <#{dest_repo}>." + expect(logger).to receive(:info).with(message) - subject - end + subject end + end - describe 'failed fork due existing destination' do - it do - FileUtils.mkdir_p(dest_repo) - message = "fork-repository failed: destination repository <#{dest_repo}> already exists." - expect(logger).to receive(:error).with(message) + describe 'failed fork due existing destination' do + it do + FileUtils.mkdir_p(dest_repo) + message = "fork-repository failed: destination repository <#{dest_repo}> already exists." + expect(logger).to receive(:error).with(message) - subject - end + subject end end end diff --git a/spec/lib/gitlab/git/hook_spec.rb b/spec/lib/gitlab/git/hook_spec.rb index 2fe1f5603ce..a45c8510b15 100644 --- a/spec/lib/gitlab/git/hook_spec.rb +++ b/spec/lib/gitlab/git/hook_spec.rb @@ -8,25 +8,39 @@ describe Gitlab::Git::Hook do allow_any_instance_of(described_class).to receive(:trigger).and_call_original end + around do |example| + # TODO move hook tests to gitaly-ruby. Hook will disappear from gitlab-ce + Gitlab::GitalyClient::StorageSettings.allow_disk_access do + example.run + end + end + describe "#trigger" do - let(:project) { create(:project, :repository) } + set(:project) { create(:project, :repository) } let(:repository) { project.repository.raw_repository } let(:repo_path) { repository.path } + let(:hooks_dir) { File.join(repo_path, 'hooks') } let(:user) { create(:user) } let(:gl_id) { Gitlab::GlId.gl_id(user) } let(:gl_username) { user.username } def create_hook(name) - FileUtils.mkdir_p(File.join(repo_path, 'hooks')) - File.open(File.join(repo_path, 'hooks', name), 'w', 0755) do |f| - f.write('exit 0') + FileUtils.mkdir_p(hooks_dir) + hook_path = File.join(hooks_dir, name) + File.open(hook_path, 'w', 0755) do |f| + f.write(<<~HOOK) + #!/bin/sh + exit 0 + HOOK end end def create_failing_hook(name) - FileUtils.mkdir_p(File.join(repo_path, 'hooks')) - File.open(File.join(repo_path, 'hooks', name), 'w', 0755) do |f| - f.write(<<-HOOK) + FileUtils.mkdir_p(hooks_dir) + hook_path = File.join(hooks_dir, name) + File.open(hook_path, 'w', 0755) do |f| + f.write(<<~HOOK) + #!/bin/sh echo 'regular message from the hook' echo 'error message from the hook' 1>&2 echo 'error message from the hook line 2' 1>&2 @@ -38,7 +52,7 @@ describe Gitlab::Git::Hook do ['pre-receive', 'post-receive', 'update'].each do |hook_name| context "when triggering a #{hook_name} hook" do context "when the hook is successful" do - let(:hook_path) { File.join(repo_path, 'hooks', hook_name) } + let(:hook_path) { File.join(hooks_dir, hook_name) } let(:gl_repository) { Gitlab::GlRepository.gl_repository(project, false) } let(:env) do { @@ -76,7 +90,7 @@ describe Gitlab::Git::Hook do status, errors = hook.trigger(gl_id, gl_username, blank, blank, ref) expect(status).to be false - expect(errors).to eq("error message from the hook<br>error message from the hook line 2<br>") + expect(errors).to eq("error message from the hook\nerror message from the hook line 2\n") end end end diff --git a/spec/lib/gitlab/git/hooks_service_spec.rb b/spec/lib/gitlab/git/hooks_service_spec.rb index 3ed3feb4c74..9337aa39e13 100644 --- a/spec/lib/gitlab/git/hooks_service_spec.rb +++ b/spec/lib/gitlab/git/hooks_service_spec.rb @@ -26,24 +26,24 @@ describe Gitlab::Git::HooksService, seed_helper: true do context 'when pre-receive hook failed' do it 'does not call post-receive hook' do - expect(service).to receive(:run_hook).with('pre-receive').and_return([false, '']) + expect(service).to receive(:run_hook).with('pre-receive').and_return([false, 'hello world']) expect(service).not_to receive(:run_hook).with('post-receive') expect do service.execute(user, repository, blankrev, newrev, ref) - end.to raise_error(Gitlab::Git::HooksService::PreReceiveError) + end.to raise_error(Gitlab::Git::PreReceiveError, 'hello world') end end context 'when update hook failed' do it 'does not call post-receive hook' do expect(service).to receive(:run_hook).with('pre-receive').and_return([true, nil]) - expect(service).to receive(:run_hook).with('update').and_return([false, '']) + expect(service).to receive(:run_hook).with('update').and_return([false, 'hello world']) expect(service).not_to receive(:run_hook).with('post-receive') expect do service.execute(user, repository, blankrev, newrev, ref) - end.to raise_error(Gitlab::Git::HooksService::PreReceiveError) + end.to raise_error(Gitlab::Git::PreReceiveError, 'hello world') end end end diff --git a/spec/lib/gitlab/git/index_spec.rb b/spec/lib/gitlab/git/index_spec.rb index 73fbc6a6afa..16e6bd35449 100644 --- a/spec/lib/gitlab/git/index_spec.rb +++ b/spec/lib/gitlab/git/index_spec.rb @@ -8,6 +8,13 @@ describe Gitlab::Git::Index, seed_helper: true do index.read_tree(repository.lookup('master').tree) end + around do |example| + # TODO move these specs to gitaly-ruby. The Index class will disappear from gitlab-ce + Gitlab::GitalyClient::StorageSettings.allow_disk_access do + example.run + end + end + describe '#create' do let(:options) do { diff --git a/spec/lib/gitlab/git/pre_receive_error_spec.rb b/spec/lib/gitlab/git/pre_receive_error_spec.rb new file mode 100644 index 00000000000..1b8be62dec6 --- /dev/null +++ b/spec/lib/gitlab/git/pre_receive_error_spec.rb @@ -0,0 +1,9 @@ +require 'spec_helper' + +describe Gitlab::Git::PreReceiveError do + it 'makes its message HTML-friendly' do + ex = described_class.new("hello\nworld\n") + + expect(ex.message).to eq('hello<br>world<br>') + end +end diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index 7a9621d9c78..5bae99101e6 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -77,17 +77,6 @@ describe Gitlab::Git::Repository, seed_helper: true do end describe '#root_ref' do - context 'with gitaly disabled' do - before do - allow(Gitlab::GitalyClient).to receive(:feature_enabled?).and_return(false) - end - - it 'calls #discover_default_branch' do - expect(repository).to receive(:discover_default_branch) - repository.root_ref - end - end - it 'returns UTF-8' do expect(repository.root_ref).to be_utf8 end @@ -153,45 +142,6 @@ describe Gitlab::Git::Repository, seed_helper: true do end end - describe "#discover_default_branch" do - let(:master) { 'master' } - let(:feature) { 'feature' } - let(:feature2) { 'feature2' } - - around do |example| - Gitlab::GitalyClient::StorageSettings.allow_disk_access do - example.run - end - end - - it "returns 'master' when master exists" do - expect(repository).to receive(:branch_names).at_least(:once).and_return([feature, master]) - expect(repository.discover_default_branch).to eq('master') - end - - it "returns non-master when master exists but default branch is set to something else" do - File.write(File.join(repository_path, 'HEAD'), 'ref: refs/heads/feature') - expect(repository).to receive(:branch_names).at_least(:once).and_return([feature, master]) - expect(repository.discover_default_branch).to eq('feature') - File.write(File.join(repository_path, 'HEAD'), 'ref: refs/heads/master') - end - - it "returns a non-master branch when only one exists" do - expect(repository).to receive(:branch_names).at_least(:once).and_return([feature]) - expect(repository.discover_default_branch).to eq('feature') - end - - it "returns a non-master branch when more than one exists and master does not" do - expect(repository).to receive(:branch_names).at_least(:once).and_return([feature, feature2]) - expect(repository.discover_default_branch).to eq('feature') - end - - it "returns nil when no branch exists" do - expect(repository).to receive(:branch_names).at_least(:once).and_return([]) - expect(repository.discover_default_branch).to be_nil - end - end - describe '#branch_names' do subject { repository.branch_names } @@ -255,7 +205,7 @@ describe Gitlab::Git::Repository, seed_helper: true do let(:expected_path) { File.join(storage_path, cache_key, expected_filename) } let(:expected_prefix) { "gitlab-git-test-#{ref}-#{SeedRepo::LastCommit::ID}" } - subject(:metadata) { repository.archive_metadata(ref, storage_path, format, append_sha: append_sha) } + subject(:metadata) { repository.archive_metadata(ref, storage_path, 'gitlab-git-test', format, append_sha: append_sha) } it 'sets CommitId to the commit SHA' do expect(metadata['CommitId']).to eq(SeedRepo::LastCommit::ID) @@ -373,6 +323,7 @@ describe Gitlab::Git::Repository, seed_helper: true do context '#submodules' do around do |example| + # TODO #submodules will be removed, has been migrated to gitaly Gitlab::GitalyClient::StorageSettings.allow_disk_access do example.run end @@ -474,7 +425,7 @@ describe Gitlab::Git::Repository, seed_helper: true do end describe '#has_local_branches?' do - shared_examples 'check for local branches' do + context 'check for local branches' do it { expect(repository.has_local_branches?).to eq(true) } context 'mutable' do @@ -508,14 +459,6 @@ describe Gitlab::Git::Repository, seed_helper: true do end end end - - context 'with gitaly' do - it_behaves_like 'check for local branches' - end - - context 'without gitaly', :skip_gitaly_mock do - it_behaves_like 'check for local branches' - end end describe "#delete_branch" do @@ -1055,6 +998,7 @@ describe Gitlab::Git::Repository, seed_helper: true do describe "#rugged_commits_between" do around do |example| + # TODO #rugged_commits_between will be removed, has been migrated to gitaly Gitlab::GitalyClient::StorageSettings.allow_disk_access do example.run end @@ -1392,24 +1336,6 @@ describe Gitlab::Git::Repository, seed_helper: true do end end - # With Gitaly enabled, Gitaly just doesn't return deleted branches. - context 'with deleted branch with Gitaly disabled' do - before do - allow(Gitlab::GitalyClient).to receive(:feature_enabled?).and_return(false) - end - - it 'returns no results' do - ref = double() - allow(ref).to receive(:name) { 'bad-branch' } - allow(ref).to receive(:target) { raise Rugged::ReferenceError } - branches = double() - allow(branches).to receive(:each) { [ref].each } - allow(repository_rugged).to receive(:branches) { branches } - - expect(subject).to be_empty - end - end - it_behaves_like 'wrapping gRPC errors', Gitlab::GitalyClient::RefService, :branches end @@ -1703,6 +1629,7 @@ describe Gitlab::Git::Repository, seed_helper: true do let(:refs) { ['deadbeef', SeedRepo::RubyBlob::ID, '909e6157199'] } around do |example| + # TODO #batch_existence isn't used anywhere, can we remove it? Gitlab::GitalyClient::StorageSettings.allow_disk_access do example.run end @@ -2002,6 +1929,18 @@ describe Gitlab::Git::Repository, seed_helper: true do expect(config).to include("fullpath = #{repository_path}") end end + + context 'repository does not exist' do + it 'raises NoRepository and does not call Gitaly WriteConfig' do + repository = Gitlab::Git::Repository.new('default', 'does/not/exist.git', '') + + expect(repository.gitaly_repository_client).not_to receive(:write_config) + + expect do + repository.write_config(full_path: 'foo/bar.git') + end.to raise_error(Gitlab::Git::Repository::NoRepository) + end + end end context "when gitaly_write_config is enabled" do diff --git a/spec/lib/gitlab/git/rev_list_spec.rb b/spec/lib/gitlab/git/rev_list_spec.rb index 32ec1e029c8..95dc47e2a00 100644 --- a/spec/lib/gitlab/git/rev_list_spec.rb +++ b/spec/lib/gitlab/git/rev_list_spec.rb @@ -9,9 +9,11 @@ describe Gitlab::Git::RevList do end def stub_popen_rev_list(*additional_args, with_lazy_block: true, output:) + repo_path = Gitlab::GitalyClient::StorageSettings.allow_disk_access { repository.path } + params = [ args_for_popen(additional_args), - repository.path, + repo_path, {}, hash_including(lazy_block: with_lazy_block ? anything : nil) ] diff --git a/spec/lib/gitlab/git/wiki_spec.rb b/spec/lib/gitlab/git/wiki_spec.rb index 722d697c28e..b63658e1b3b 100644 --- a/spec/lib/gitlab/git/wiki_spec.rb +++ b/spec/lib/gitlab/git/wiki_spec.rb @@ -6,9 +6,7 @@ describe Gitlab::Git::Wiki do let(:project_wiki) { ProjectWiki.new(project, user) } subject { project_wiki.wiki } - # Remove skip_gitaly_mock flag when gitaly_find_page when - # https://gitlab.com/gitlab-org/gitlab-ce/issues/42039 is solved - describe '#page', :skip_gitaly_mock do + describe '#page' do before do create_page('page1', 'content') create_page('foo/page1', 'content foo/page1') @@ -25,6 +23,22 @@ describe Gitlab::Git::Wiki do end end + describe '#delete_page' do + after do + destroy_page('page1') + end + + it 'only removes the page with the same path' do + create_page('page1', 'content') + create_page('*', 'content') + + subject.delete_page('*', commit_details('whatever')) + + expect(subject.pages.count).to eq 1 + expect(subject.pages.first.title).to eq 'page1' + end + end + def create_page(name, content) subject.write_page(name, :markdown, content, commit_details(name)) end diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb index dfffea7797f..0d5f6a0b576 100644 --- a/spec/lib/gitlab/git_access_spec.rb +++ b/spec/lib/gitlab/git_access_spec.rb @@ -552,7 +552,7 @@ describe Gitlab::GitAccess do it 'returns not found' do project.add_guest(user) repo = project.repository - FileUtils.rm_rf(repo.path) + Gitlab::GitalyClient::StorageSettings.allow_disk_access { FileUtils.rm_rf(repo.path) } # Sanity check for rm_rf expect(repo.exists?).to eq(false) @@ -750,20 +750,22 @@ describe Gitlab::GitAccess do def merge_into_protected_branch @protected_branch_merge_commit ||= begin - stub_git_hooks - project.repository.add_branch(user, unprotected_branch, 'feature') - target_branch = project.repository.lookup('feature') - source_branch = project.repository.create_file( - user, - 'filename', - 'This is the file content', - message: 'This is a good commit message', - branch_name: unprotected_branch) - rugged = project.repository.rugged - author = { email: "email@example.com", time: Time.now, name: "Example Git User" } - - merge_index = rugged.merge_commits(target_branch, source_branch) - Rugged::Commit.create(rugged, author: author, committer: author, message: "commit message", parents: [target_branch, source_branch], tree: merge_index.write_tree(rugged)) + Gitlab::GitalyClient::StorageSettings.allow_disk_access do + stub_git_hooks + project.repository.add_branch(user, unprotected_branch, 'feature') + target_branch = project.repository.lookup('feature') + source_branch = project.repository.create_file( + user, + 'filename', + 'This is the file content', + message: 'This is a good commit message', + branch_name: unprotected_branch) + rugged = project.repository.rugged + author = { email: "email@example.com", time: Time.now, name: "Example Git User" } + + merge_index = rugged.merge_commits(target_branch, source_branch) + Rugged::Commit.create(rugged, author: author, committer: author, message: "commit message", parents: [target_branch, source_branch], tree: merge_index.write_tree(rugged)) + end end end diff --git a/spec/lib/gitlab/git_access_wiki_spec.rb b/spec/lib/gitlab/git_access_wiki_spec.rb index 730ede99fc9..9c6c9fe13bf 100644 --- a/spec/lib/gitlab/git_access_wiki_spec.rb +++ b/spec/lib/gitlab/git_access_wiki_spec.rb @@ -52,7 +52,9 @@ describe Gitlab::GitAccessWiki do context 'when the wiki repository does not exist' do it 'returns not found' do wiki_repo = project.wiki.repository - FileUtils.rm_rf(wiki_repo.path) + Gitlab::GitalyClient::StorageSettings.allow_disk_access do + FileUtils.rm_rf(wiki_repo.path) + end # Sanity check for rm_rf expect(wiki_repo.exists?).to eq(false) diff --git a/spec/lib/gitlab/gitaly_client/operation_service_spec.rb b/spec/lib/gitlab/gitaly_client/operation_service_spec.rb index 9fbdd73ee0e..9709f1f5646 100644 --- a/spec/lib/gitlab/gitaly_client/operation_service_spec.rb +++ b/spec/lib/gitlab/gitaly_client/operation_service_spec.rb @@ -48,7 +48,7 @@ describe Gitlab::GitalyClient::OperationService do .and_return(response) expect { subject }.to raise_error( - Gitlab::Git::HooksService::PreReceiveError, "something failed") + Gitlab::Git::PreReceiveError, "something failed") end end end @@ -85,7 +85,7 @@ describe Gitlab::GitalyClient::OperationService do .and_return(response) expect { subject }.to raise_error( - Gitlab::Git::HooksService::PreReceiveError, "something failed") + Gitlab::Git::PreReceiveError, "something failed") end end end diff --git a/spec/lib/gitlab/github_import/importer/issue_importer_spec.rb b/spec/lib/gitlab/github_import/importer/issue_importer_spec.rb index d34ca0b76b8..81fe97c1e49 100644 --- a/spec/lib/gitlab/github_import/importer/issue_importer_spec.rb +++ b/spec/lib/gitlab/github_import/importer/issue_importer_spec.rb @@ -180,12 +180,12 @@ describe Gitlab::GithubImport::Importer::IssueImporter, :clean_gitlab_redis_cach allow(importer.user_finder) .to receive(:user_id_for) - .ordered.with(issue.assignees[0]) + .with(issue.assignees[0]) .and_return(4) allow(importer.user_finder) .to receive(:user_id_for) - .ordered.with(issue.assignees[1]) + .with(issue.assignees[1]) .and_return(5) expect(Gitlab::Database) diff --git a/spec/lib/gitlab/github_import/importer/lfs_object_importer_spec.rb b/spec/lib/gitlab/github_import/importer/lfs_object_importer_spec.rb new file mode 100644 index 00000000000..4857f2afbe2 --- /dev/null +++ b/spec/lib/gitlab/github_import/importer/lfs_object_importer_spec.rb @@ -0,0 +1,23 @@ +require 'spec_helper' + +describe Gitlab::GithubImport::Importer::LfsObjectImporter do + let(:project) { create(:project) } + let(:download_link) { "http://www.gitlab.com/lfs_objects/oid" } + + let(:github_lfs_object) do + Gitlab::GithubImport::Representation::LfsObject.new( + oid: 'oid', download_link: download_link + ) + end + + let(:importer) { described_class.new(github_lfs_object, project, nil) } + + describe '#execute' do + it 'calls the LfsDownloadService with the lfs object attributes' do + expect_any_instance_of(Projects::LfsPointers::LfsDownloadService) + .to receive(:execute).with('oid', download_link) + + importer.execute + end + end +end diff --git a/spec/lib/gitlab/github_import/importer/lfs_objects_importer_spec.rb b/spec/lib/gitlab/github_import/importer/lfs_objects_importer_spec.rb new file mode 100644 index 00000000000..5f5c6b803c0 --- /dev/null +++ b/spec/lib/gitlab/github_import/importer/lfs_objects_importer_spec.rb @@ -0,0 +1,94 @@ +require 'spec_helper' + +describe Gitlab::GithubImport::Importer::LfsObjectsImporter do + let(:project) { double(:project, id: 4, import_source: 'foo/bar') } + let(:client) { double(:client) } + let(:download_link) { "http://www.gitlab.com/lfs_objects/oid" } + + let(:github_lfs_object) { ['oid', download_link] } + + describe '#parallel?' do + it 'returns true when running in parallel mode' do + importer = described_class.new(project, client) + expect(importer).to be_parallel + end + + it 'returns false when running in sequential mode' do + importer = described_class.new(project, client, parallel: false) + expect(importer).not_to be_parallel + end + end + + describe '#execute' do + context 'when running in parallel mode' do + it 'imports lfs objects in parallel' do + importer = described_class.new(project, client) + + expect(importer).to receive(:parallel_import) + + importer.execute + end + end + + context 'when running in sequential mode' do + it 'imports lfs objects in sequence' do + importer = described_class.new(project, client, parallel: false) + + expect(importer).to receive(:sequential_import) + + importer.execute + end + end + end + + describe '#sequential_import' do + it 'imports each lfs object in sequence' do + importer = described_class.new(project, client, parallel: false) + lfs_object_importer = double(:lfs_object_importer) + + allow(importer) + .to receive(:each_object_to_import) + .and_yield(['oid', download_link]) + + expect(Gitlab::GithubImport::Importer::LfsObjectImporter) + .to receive(:new) + .with( + an_instance_of(Gitlab::GithubImport::Representation::LfsObject), + project, + client + ) + .and_return(lfs_object_importer) + + expect(lfs_object_importer).to receive(:execute) + + importer.sequential_import + end + end + + describe '#parallel_import' do + it 'imports each lfs object in parallel' do + importer = described_class.new(project, client) + + allow(importer) + .to receive(:each_object_to_import) + .and_yield(github_lfs_object) + + expect(Gitlab::GithubImport::ImportLfsObjectWorker) + .to receive(:perform_async) + .with(project.id, an_instance_of(Hash), an_instance_of(String)) + + waiter = importer.parallel_import + + expect(waiter).to be_an_instance_of(Gitlab::JobWaiter) + expect(waiter.jobs_remaining).to eq(1) + end + end + + describe '#collection_options' do + it 'returns an empty Hash' do + importer = described_class.new(project, client) + + expect(importer.collection_options).to eq({}) + end + end +end diff --git a/spec/lib/gitlab/github_import/importer/pull_request_importer_spec.rb b/spec/lib/gitlab/github_import/importer/pull_request_importer_spec.rb index 35f3fdf8304..3422a1e82fc 100644 --- a/spec/lib/gitlab/github_import/importer/pull_request_importer_spec.rb +++ b/spec/lib/gitlab/github_import/importer/pull_request_importer_spec.rb @@ -40,13 +40,19 @@ describe Gitlab::GithubImport::Importer::PullRequestImporter, :clean_gitlab_redi describe '#execute' do it 'imports the pull request' do + mr = double(:merge_request, id: 10) + expect(importer) .to receive(:create_merge_request) - .and_return(10) + .and_return([mr, false]) + + expect(importer) + .to receive(:insert_git_data) + .with(mr, false) expect_any_instance_of(Gitlab::GithubImport::IssuableFinder) .to receive(:cache_database_id) - .with(10) + .with(mr.id) importer.execute end @@ -99,18 +105,11 @@ describe Gitlab::GithubImport::Importer::PullRequestImporter, :clean_gitlab_redi importer.create_merge_request end - it 'returns the ID of the created merge request' do - id = importer.create_merge_request - - expect(id).to be_a_kind_of(Numeric) - end - - it 'creates the merge request diffs' do - importer.create_merge_request - - mr = project.merge_requests.take + it 'returns the created merge request' do + mr, exists = importer.create_merge_request - expect(mr.merge_request_diffs.exists?).to eq(true) + expect(mr).to be_instance_of(MergeRequest) + expect(exists).to eq(false) end end @@ -217,5 +216,77 @@ describe Gitlab::GithubImport::Importer::PullRequestImporter, :clean_gitlab_redi expect { importer.create_merge_request }.not_to raise_error end end + + context 'when the merge request already exists' do + before do + allow(importer.user_finder) + .to receive(:author_id_for) + .with(pull_request) + .and_return([user.id, true]) + + allow(importer.user_finder) + .to receive(:assignee_id_for) + .with(pull_request) + .and_return(user.id) + end + + it 'returns the existing merge request' do + mr1, exists1 = importer.create_merge_request + mr2, exists2 = importer.create_merge_request + + expect(mr2).to eq(mr1) + expect(exists1).to eq(false) + expect(exists2).to eq(true) + end + end + end + + describe '#insert_git_data' do + before do + allow(importer.milestone_finder) + .to receive(:id_for) + .with(pull_request) + .and_return(milestone.id) + + allow(importer.user_finder) + .to receive(:author_id_for) + .with(pull_request) + .and_return([user.id, true]) + + allow(importer.user_finder) + .to receive(:assignee_id_for) + .with(pull_request) + .and_return(user.id) + end + + it 'creates the merge request diffs' do + mr, exists = importer.create_merge_request + + importer.insert_git_data(mr, exists) + + expect(mr.merge_request_diffs.exists?).to eq(true) + end + + it 'creates the merge request diff commits' do + mr, exists = importer.create_merge_request + + importer.insert_git_data(mr, exists) + + diff = mr.merge_request_diffs.take + + expect(diff.merge_request_diff_commits.exists?).to eq(true) + end + + context 'when the merge request exists' do + it 'creates the merge request diffs if they do not yet exist' do + mr, _ = importer.create_merge_request + + mr.merge_request_diffs.delete_all + + importer.insert_git_data(mr, true) + + expect(mr.merge_request_diffs.exists?).to eq(true) + end + end end end diff --git a/spec/lib/gitlab/github_import/importer/repository_importer_spec.rb b/spec/lib/gitlab/github_import/importer/repository_importer_spec.rb index cc9e4b67e72..d8f01dcb76b 100644 --- a/spec/lib/gitlab/github_import/importer/repository_importer_spec.rb +++ b/spec/lib/gitlab/github_import/importer/repository_importer_spec.rb @@ -14,7 +14,8 @@ describe Gitlab::GithubImport::Importer::RepositoryImporter do disk_path: 'foo', repository: repository, create_wiki: true, - import_state: import_state + import_state: import_state, + lfs_enabled?: true ) end diff --git a/spec/lib/gitlab/github_import/sequential_importer_spec.rb b/spec/lib/gitlab/github_import/sequential_importer_spec.rb index 6089b0b751f..05d3243f806 100644 --- a/spec/lib/gitlab/github_import/sequential_importer_spec.rb +++ b/spec/lib/gitlab/github_import/sequential_importer_spec.rb @@ -30,7 +30,6 @@ describe Gitlab::GithubImport::SequentialImporter do expect(instance).to receive(:execute) end - expect(repository).to receive(:after_import) expect(importer.execute).to eq(true) end end diff --git a/spec/lib/gitlab/gpg_spec.rb b/spec/lib/gitlab/gpg_spec.rb index ab9a166db00..47f37cae98f 100644 --- a/spec/lib/gitlab/gpg_spec.rb +++ b/spec/lib/gitlab/gpg_spec.rb @@ -74,6 +74,19 @@ describe Gitlab::Gpg do email: 'nannie.bernhard@example.com' }]) end + + it 'rejects non UTF-8 names and addresses' do + public_key = double(:key) + fingerprints = double(:fingerprints) + email = "\xEEch@test.com".force_encoding('ASCII-8BIT') + uid = double(:uid, name: 'Test User', email: email) + raw_key = double(:raw_key, uids: [uid]) + allow(Gitlab::Gpg::CurrentKeyChain).to receive(:fingerprints_from_key).with(public_key).and_return(fingerprints) + allow(GPGME::Key).to receive(:find).with(:public, anything).and_return([raw_key]) + + user_infos = described_class.user_infos_from_key(public_key) + expect(user_infos).to eq([]) + end end describe '.current_home_dir' do diff --git a/spec/lib/gitlab/hashed_storage/migrator_spec.rb b/spec/lib/gitlab/hashed_storage/migrator_spec.rb new file mode 100644 index 00000000000..813ae43b4d3 --- /dev/null +++ b/spec/lib/gitlab/hashed_storage/migrator_spec.rb @@ -0,0 +1,75 @@ +require 'spec_helper' + +describe Gitlab::HashedStorage::Migrator do + describe '#bulk_schedule' do + it 'schedules job to StorageMigratorWorker' do + Sidekiq::Testing.fake! do + expect { subject.bulk_schedule(1, 5) }.to change(StorageMigratorWorker.jobs, :size).by(1) + end + end + end + + describe '#bulk_migrate' do + let(:projects) { create_list(:project, 2, :legacy_storage) } + let(:ids) { projects.map(&:id) } + + it 'enqueue jobs to ProjectMigrateHashedStorageWorker' do + Sidekiq::Testing.fake! do + expect { subject.bulk_migrate(ids.min, ids.max) }.to change(ProjectMigrateHashedStorageWorker.jobs, :size).by(2) + end + end + + it 'sets projects as read only' do + allow(ProjectMigrateHashedStorageWorker).to receive(:perform_async).twice + subject.bulk_migrate(ids.min, ids.max) + + projects.each do |project| + expect(project.reload.repository_read_only?).to be_truthy + end + end + + it 'rescues and log exceptions' do + allow_any_instance_of(Project).to receive(:migrate_to_hashed_storage!).and_raise(StandardError) + expect { subject.bulk_migrate(ids.min, ids.max) }.not_to raise_error + end + + it 'delegates each project in specified range to #migrate' do + projects.each do |project| + expect(subject).to receive(:migrate).with(project) + end + + subject.bulk_migrate(ids.min, ids.max) + end + end + + describe '#migrate' do + let(:project) { create(:project, :legacy_storage, :empty_repo) } + + it 'enqueues job to ProjectMigrateHashedStorageWorker' do + Sidekiq::Testing.fake! do + expect { subject.migrate(project) }.to change(ProjectMigrateHashedStorageWorker.jobs, :size).by(1) + end + end + + it 'rescues and log exceptions' do + allow(project).to receive(:migrate_to_hashed_storage!).and_raise(StandardError) + + expect { subject.migrate(project) }.not_to raise_error + end + + it 'sets project as read only' do + allow(ProjectMigrateHashedStorageWorker).to receive(:perform_async) + subject.migrate(project) + + expect(project.reload.repository_read_only?).to be_truthy + end + + it 'migrate project' do + Sidekiq::Testing.inline! do + subject.migrate(project) + end + + expect(project.reload.hashed_storage?(:attachments)).to be_truthy + end + end +end diff --git a/spec/lib/gitlab/i18n/metadata_entry_spec.rb b/spec/lib/gitlab/i18n/metadata_entry_spec.rb index ab71d6454a9..a399517cc04 100644 --- a/spec/lib/gitlab/i18n/metadata_entry_spec.rb +++ b/spec/lib/gitlab/i18n/metadata_entry_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe Gitlab::I18n::MetadataEntry do - describe '#expected_plurals' do + describe '#expected_forms' do it 'returns the number of plurals' do data = { msgid: "", @@ -22,7 +22,7 @@ describe Gitlab::I18n::MetadataEntry do } entry = described_class.new(data) - expect(entry.expected_plurals).to eq(2) + expect(entry.expected_forms).to eq(2) end it 'returns 0 for the POT-metadata' do @@ -45,7 +45,7 @@ describe Gitlab::I18n::MetadataEntry do } entry = described_class.new(data) - expect(entry.expected_plurals).to eq(0) + expect(entry.expected_forms).to eq(0) end end end diff --git a/spec/lib/gitlab/i18n/po_linter_spec.rb b/spec/lib/gitlab/i18n/po_linter_spec.rb index 3a962ba7f22..3dbc23d2aaf 100644 --- a/spec/lib/gitlab/i18n/po_linter_spec.rb +++ b/spec/lib/gitlab/i18n/po_linter_spec.rb @@ -1,10 +1,31 @@ require 'spec_helper' require 'simple_po_parser' +# Disabling this cop to allow for multi-language examples in comments +# rubocop:disable Style/AsciiComments describe Gitlab::I18n::PoLinter do let(:linter) { described_class.new(po_path) } let(:po_path) { 'spec/fixtures/valid.po' } + def fake_translation(msgid:, translation:, plural_id: nil, plurals: []) + data = { msgid: msgid, msgid_plural: plural_id } + + if plural_id + [translation, *plurals].each_with_index do |plural, index| + allow(FastGettext::Translation).to receive(:n_).with(msgid, plural_id, index).and_return(plural) + data.merge!("msgstr[#{index}]" => plural) + end + else + allow(FastGettext::Translation).to receive(:_).with(msgid).and_return(translation) + data[:msgstr] = translation + end + + Gitlab::I18n::TranslationEntry.new( + data, + plurals.size + 1 + ) + end + describe '#errors' do it 'only calls validation once' do expect(linter).to receive(:validate_po).once.and_call_original @@ -155,9 +176,8 @@ describe Gitlab::I18n::PoLinter do describe '#validate_entries' do it 'keeps track of errors for entries' do - fake_invalid_entry = Gitlab::I18n::TranslationEntry.new( - { msgid: "Hello %{world}", msgstr: "Bonjour %{monde}" }, 2 - ) + fake_invalid_entry = fake_translation(msgid: "Hello %{world}", + translation: "Bonjour %{monde}") allow(linter).to receive(:translation_entries) { [fake_invalid_entry] } expect(linter).to receive(:validate_entry) @@ -177,6 +197,7 @@ describe Gitlab::I18n::PoLinter do expect(linter).to receive(:validate_newlines).with([], fake_entry) expect(linter).to receive(:validate_number_of_plurals).with([], fake_entry) expect(linter).to receive(:validate_unescaped_chars).with([], fake_entry) + expect(linter).to receive(:validate_translation).with([], fake_entry) linter.validate_entry(fake_entry) end @@ -185,7 +206,7 @@ describe Gitlab::I18n::PoLinter do describe '#validate_number_of_plurals' do it 'validates when there are an incorrect number of translations' do fake_metadata = double - allow(fake_metadata).to receive(:expected_plurals).and_return(2) + allow(fake_metadata).to receive(:expected_forms).and_return(2) allow(linter).to receive(:metadata_entry).and_return(fake_metadata) fake_entry = Gitlab::I18n::TranslationEntry.new( @@ -201,13 +222,16 @@ describe Gitlab::I18n::PoLinter do end describe '#validate_variables' do - it 'validates both signular and plural in a pluralized string when the entry has a singular' do - pluralized_entry = Gitlab::I18n::TranslationEntry.new( - { msgid: 'Hello %{world}', - msgid_plural: 'Hello all %{world}', - 'msgstr[0]' => 'Bonjour %{world}', - 'msgstr[1]' => 'Bonjour tous %{world}' }, - 2 + before do + allow(linter).to receive(:validate_variables_in_message).and_call_original + end + + it 'validates both singular and plural in a pluralized string when the entry has a singular' do + pluralized_entry = fake_translation( + msgid: 'Hello %{world}', + translation: 'Bonjour %{world}', + plural_id: 'Hello all %{world}', + plurals: ['Bonjour tous %{world}'] ) expect(linter).to receive(:validate_variables_in_message) @@ -221,11 +245,10 @@ describe Gitlab::I18n::PoLinter do end it 'only validates plural when there is no separate singular' do - pluralized_entry = Gitlab::I18n::TranslationEntry.new( - { msgid: 'Hello %{world}', - msgid_plural: 'Hello all %{world}', - 'msgstr[0]' => 'Bonjour %{world}' }, - 1 + pluralized_entry = fake_translation( + msgid: 'Hello %{world}', + translation: 'Bonjour %{world}', + plural_id: 'Hello all %{world}' ) expect(linter).to receive(:validate_variables_in_message) @@ -235,37 +258,65 @@ describe Gitlab::I18n::PoLinter do end it 'validates the message variables' do - entry = Gitlab::I18n::TranslationEntry.new( - { msgid: 'Hello', msgstr: 'Bonjour' }, - 2 - ) + entry = fake_translation(msgid: 'Hello', translation: 'Bonjour') expect(linter).to receive(:validate_variables_in_message) .with([], 'Hello', 'Bonjour') linter.validate_variables([], entry) end + + it 'validates variable usage in message ids' do + entry = fake_translation( + msgid: 'Hello %{world}', + translation: 'Bonjour %{world}', + plural_id: 'Hello all %{world}', + plurals: ['Bonjour tous %{world}'] + ) + + expect(linter).to receive(:validate_variables_in_message) + .with([], 'Hello %{world}', 'Hello %{world}') + .and_call_original + expect(linter).to receive(:validate_variables_in_message) + .with([], 'Hello all %{world}', 'Hello all %{world}') + .and_call_original + + linter.validate_variables([], entry) + end end describe '#validate_variables_in_message' do it 'detects when a variables are used incorrectly' do errors = [] - expected_errors = ['<hello %{world} %d> is missing: [%{hello}]', - '<hello %{world} %d> is using unknown variables: [%{world}]', - 'is combining multiple unnamed variables'] + expected_errors = ['<%d hello %{world} %s> is missing: [%{hello}]', + '<%d hello %{world} %s> is using unknown variables: [%{world}]', + 'is combining multiple unnamed variables', + 'is combining named variables with unnamed variables'] - linter.validate_variables_in_message(errors, '%{hello} world %d', 'hello %{world} %d') + linter.validate_variables_in_message(errors, '%d %{hello} world %s', '%d hello %{world} %s') expect(errors).to include(*expected_errors) end + + it 'does not allow combining 1 `%d` unnamed variable with named variables' do + errors = [] + + linter.validate_variables_in_message(errors, + '%{type} detected %d vulnerability', + '%{type} detecteerde %d kwetsbaarheid') + + expect(errors).not_to be_empty + end end describe '#validate_translation' do + let(:entry) { fake_translation(msgid: 'Hello %{world}', translation: 'Bonjour %{world}') } + it 'succeeds with valid variables' do errors = [] - linter.validate_translation(errors, 'Hello %{world}', ['%{world}']) + linter.validate_translation(errors, entry) expect(errors).to be_empty end @@ -275,43 +326,80 @@ describe Gitlab::I18n::PoLinter do expect(FastGettext::Translation).to receive(:_) { raise 'broken' } - linter.validate_translation(errors, 'Hello', []) + linter.validate_translation(errors, entry) - expect(errors).to include('Failure translating to en with []: broken') + expect(errors).to include('Failure translating to en: broken') end it 'adds an error message when translating fails when translating with context' do + entry = fake_translation(msgid: 'Tests|Hello', translation: 'broken') errors = [] expect(FastGettext::Translation).to receive(:s_) { raise 'broken' } - linter.validate_translation(errors, 'Tests|Hello', []) + linter.validate_translation(errors, entry) - expect(errors).to include('Failure translating to en with []: broken') + expect(errors).to include('Failure translating to en: broken') end it "adds an error when trying to translate with incorrect variables when using unnamed variables" do + entry = fake_translation(msgid: 'Hello %s', translation: 'Hello %d') errors = [] - linter.validate_translation(errors, 'Hello %d', ['%s']) + linter.validate_translation(errors, entry) - expect(errors.first).to start_with("Failure translating to en with") + expect(errors.first).to start_with("Failure translating to en") end it "adds an error when trying to translate with named variables when unnamed variables are expected" do + entry = fake_translation(msgid: 'Hello %s', translation: 'Hello %{thing}') errors = [] - linter.validate_translation(errors, 'Hello %d', ['%{world}']) + linter.validate_translation(errors, entry) - expect(errors.first).to start_with("Failure translating to en with") + expect(errors.first).to start_with("Failure translating to en") end - it 'adds an error when translated with incorrect variables using named variables' do - errors = [] + it 'tests translation for all given forms' do + # Fake a language that has 3 forms to translate + fake_metadata = double + allow(fake_metadata).to receive(:forms_to_test).and_return(3) + allow(linter).to receive(:metadata_entry).and_return(fake_metadata) + entry = fake_translation( + msgid: '%d exception', + translation: '%d uitzondering', + plural_id: '%d exceptions', + plurals: ['%d uitzonderingen', '%d uitzonderingetjes'] + ) + + # Make each count use a different index + allow(linter).to receive(:index_for_pluralization).with(0).and_return(0) + allow(linter).to receive(:index_for_pluralization).with(1).and_return(1) + allow(linter).to receive(:index_for_pluralization).with(2).and_return(2) + + expect(FastGettext::Translation).to receive(:n_).with('%d exception', '%d exceptions', 0).and_call_original + expect(FastGettext::Translation).to receive(:n_).with('%d exception', '%d exceptions', 1).and_call_original + expect(FastGettext::Translation).to receive(:n_).with('%d exception', '%d exceptions', 2).and_call_original + + linter.validate_translation([], entry) + end + end + + describe '#numbers_covering_all_plurals' do + it 'can correctly find all required numbers to translate to Polish' do + # Polish used as an example with 3 different forms: + # 0, all plurals except the ones ending in 2,3,4: Kotów + # 1: Kot + # 2-3-4: Koty + # So translating with [0, 1, 2] will give us all different posibilities + fake_metadata = double + allow(fake_metadata).to receive(:forms_to_test).and_return(4) + allow(linter).to receive(:metadata_entry).and_return(fake_metadata) + allow(linter).to receive(:locale).and_return('pl_PL') - linter.validate_translation(errors, 'Hello %{thing}', ['%d']) + numbers = linter.numbers_covering_all_plurals - expect(errors.first).to start_with("Failure translating to en with") + expect(numbers).to contain_exactly(0, 1, 2) end end @@ -336,3 +424,4 @@ describe Gitlab::I18n::PoLinter do end end end +# rubocop:enable Style/AsciiComments diff --git a/spec/lib/gitlab/i18n/translation_entry_spec.rb b/spec/lib/gitlab/i18n/translation_entry_spec.rb index f68bc8feff9..b301e6ea443 100644 --- a/spec/lib/gitlab/i18n/translation_entry_spec.rb +++ b/spec/lib/gitlab/i18n/translation_entry_spec.rb @@ -109,7 +109,7 @@ describe Gitlab::I18n::TranslationEntry do data = { msgid: %w(hello world) } entry = described_class.new(data, 2) - expect(entry.msgid_contains_newlines?).to be_truthy + expect(entry.msgid_has_multiple_lines?).to be_truthy end end @@ -118,7 +118,7 @@ describe Gitlab::I18n::TranslationEntry do data = { msgid_plural: %w(hello world) } entry = described_class.new(data, 2) - expect(entry.plural_id_contains_newlines?).to be_truthy + expect(entry.plural_id_has_multiple_lines?).to be_truthy end end @@ -127,7 +127,7 @@ describe Gitlab::I18n::TranslationEntry do data = { msgstr: %w(hello world) } entry = described_class.new(data, 2) - expect(entry.translations_contain_newlines?).to be_truthy + expect(entry.translations_have_multiple_lines?).to be_truthy end end diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index a129855dbd8..2ea66479c1b 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -257,6 +257,7 @@ project: - import_data - commit_statuses - pipelines +- stages - builds - runner_projects - runners diff --git a/spec/lib/gitlab/import_export/avatar_saver_spec.rb b/spec/lib/gitlab/import_export/avatar_saver_spec.rb index f40d4bc2d08..2223f163177 100644 --- a/spec/lib/gitlab/import_export/avatar_saver_spec.rb +++ b/spec/lib/gitlab/import_export/avatar_saver_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe Gitlab::ImportExport::AvatarSaver do let(:shared) { project.import_export_shared } let(:export_path) { "#{Dir.tmpdir}/project_tree_saver_spec" } - let(:project_with_avatar) { create(:project, avatar: fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png")) } + let(:project_with_avatar) { create(:project, avatar: fixture_file_upload("spec/fixtures/dk.png", "image/png")) } let(:project) { create(:project) } before do diff --git a/spec/lib/gitlab/import_export/fork_spec.rb b/spec/lib/gitlab/import_export/fork_spec.rb index 17e06a6a83f..71fd5a51c3b 100644 --- a/spec/lib/gitlab/import_export/fork_spec.rb +++ b/spec/lib/gitlab/import_export/fork_spec.rb @@ -41,8 +41,10 @@ describe 'forked project import' do after do FileUtils.rm_rf(export_path) - FileUtils.rm_rf(project_with_repo.repository.path_to_repo) - FileUtils.rm_rf(project.repository.path_to_repo) + Gitlab::GitalyClient::StorageSettings.allow_disk_access do + FileUtils.rm_rf(project_with_repo.repository.path_to_repo) + FileUtils.rm_rf(project.repository.path_to_repo) + end end it 'can access the MR' do diff --git a/spec/lib/gitlab/import_export/merge_request_parser_spec.rb b/spec/lib/gitlab/import_export/merge_request_parser_spec.rb index b793636c4d6..68eaa70e6b6 100644 --- a/spec/lib/gitlab/import_export/merge_request_parser_spec.rb +++ b/spec/lib/gitlab/import_export/merge_request_parser_spec.rb @@ -19,7 +19,9 @@ describe Gitlab::ImportExport::MergeRequestParser do end after do - FileUtils.rm_rf(project.repository.path_to_repo) + Gitlab::GitalyClient::StorageSettings.allow_disk_access do + FileUtils.rm_rf(project.repository.path_to_repo) + end end it 'has a source branch' do diff --git a/spec/lib/gitlab/import_export/repo_restorer_spec.rb b/spec/lib/gitlab/import_export/repo_restorer_spec.rb index dc806d036ff..013b8895f67 100644 --- a/spec/lib/gitlab/import_export/repo_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/repo_restorer_spec.rb @@ -23,8 +23,10 @@ describe Gitlab::ImportExport::RepoRestorer do after do FileUtils.rm_rf(export_path) - FileUtils.rm_rf(project_with_repo.repository.path_to_repo) - FileUtils.rm_rf(project.repository.path_to_repo) + Gitlab::GitalyClient::StorageSettings.allow_disk_access do + FileUtils.rm_rf(project_with_repo.repository.path_to_repo) + FileUtils.rm_rf(project.repository.path_to_repo) + end end it 'restores the repo successfully' do @@ -34,7 +36,9 @@ describe Gitlab::ImportExport::RepoRestorer do it 'has the webhooks' do restorer.restore - expect(Gitlab::Git::Hook.new('post-receive', project.repository.raw_repository)).to exist + Gitlab::GitalyClient::StorageSettings.allow_disk_access do + expect(Gitlab::Git::Hook.new('post-receive', project.repository.raw_repository)).to exist + end end end end diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml index 74e7a45fd6c..0a1e3eb83d3 100644 --- a/spec/lib/gitlab/import_export/safe_model_attributes.yml +++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml @@ -242,6 +242,7 @@ Ci::Pipeline: - config_source - failure_reason - protected +- iid Ci::Stage: - id - name @@ -539,6 +540,7 @@ ProjectAutoDevops: - id - enabled - domain +- deploy_strategy - project_id - created_at - updated_at diff --git a/spec/lib/gitlab/import_export/uploads_saver_spec.rb b/spec/lib/gitlab/import_export/uploads_saver_spec.rb index 1304d8fabfc..095687fa89d 100644 --- a/spec/lib/gitlab/import_export/uploads_saver_spec.rb +++ b/spec/lib/gitlab/import_export/uploads_saver_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe Gitlab::ImportExport::UploadsSaver do describe 'bundle a project Git repo' do let(:export_path) { "#{Dir.tmpdir}/uploads_saver_spec" } - let(:file) { fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif') } + let(:file) { fixture_file_upload('spec/fixtures/banana_sample.gif', 'image/gif') } let(:shared) { project.import_export_shared } before do diff --git a/spec/lib/gitlab/import_sources_spec.rb b/spec/lib/gitlab/import_sources_spec.rb index f2fa315e3ec..10341486512 100644 --- a/spec/lib/gitlab/import_sources_spec.rb +++ b/spec/lib/gitlab/import_sources_spec.rb @@ -91,4 +91,23 @@ describe Gitlab::ImportSources do end end end + + describe 'imports_repository? checker' do + let(:allowed_importers) { %w[github gitlab_project] } + + it 'fails if any importer other than the allowed ones implements this method' do + current_importers = described_class.values.select { |kind| described_class.importer(kind).try(:imports_repository?) } + not_allowed_importers = current_importers - allowed_importers + + expect(not_allowed_importers).to be_empty, failure_message(not_allowed_importers) + end + + def failure_message(importers_class_names) + <<-MSG + It looks like the #{importers_class_names.join(', ')} importers implements its own way to import the repository. + That means that the lfs object download must be handled for each of them. You can use 'LfsImportService' and + 'LfsDownloadService' to implement it. After that, add the importer name to the list of allowed importers in this spec. + MSG + end + end end diff --git a/spec/lib/gitlab/legacy_github_import/project_creator_spec.rb b/spec/lib/gitlab/legacy_github_import/project_creator_spec.rb index eb1b13704ea..972b17d5b12 100644 --- a/spec/lib/gitlab/legacy_github_import/project_creator_spec.rb +++ b/spec/lib/gitlab/legacy_github_import/project_creator_spec.rb @@ -44,16 +44,44 @@ describe Gitlab::LegacyGithubImport::ProjectCreator do end context 'when GitHub project is public' do - before do - allow_any_instance_of(ApplicationSetting).to receive(:default_project_visibility).and_return(Gitlab::VisibilityLevel::INTERNAL) - end - - it 'sets project visibility to the default project visibility' do + it 'sets project visibility to public' do repo.private = false project = service.execute - expect(project.visibility_level).to eq(Gitlab::VisibilityLevel::INTERNAL) + expect(project.visibility_level).to eq(Gitlab::VisibilityLevel::PUBLIC) + end + end + + context 'when visibility level is restricted' do + context 'when GitHub project is private' do + before do + stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PRIVATE]) + allow_any_instance_of(ApplicationSetting).to receive(:default_project_visibility).and_return(Gitlab::VisibilityLevel::INTERNAL) + end + + it 'sets project visibility to the default project visibility' do + repo.private = true + + project = service.execute + + expect(project.visibility_level).to eq(Gitlab::VisibilityLevel::INTERNAL) + end + end + + context 'when GitHub project is public' do + before do + stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC]) + allow_any_instance_of(ApplicationSetting).to receive(:default_project_visibility).and_return(Gitlab::VisibilityLevel::INTERNAL) + end + + it 'sets project visibility to the default project visibility' do + repo.private = false + + project = service.execute + + expect(project.visibility_level).to eq(Gitlab::VisibilityLevel::INTERNAL) + end end end diff --git a/spec/lib/gitlab/path_regex_spec.rb b/spec/lib/gitlab/path_regex_spec.rb index a40330d853f..e90e0aba0a4 100644 --- a/spec/lib/gitlab/path_regex_spec.rb +++ b/spec/lib/gitlab/path_regex_spec.rb @@ -90,11 +90,13 @@ describe Gitlab::PathRegex do let(:routes_not_starting_in_wildcard) { routes_without_format.select { |p| p !~ %r{^/[:*]} } } let(:top_level_words) do - words = routes_not_starting_in_wildcard.map do |route| - route.split('/')[1] - end.compact - - (words + ee_top_level_words + files_in_public + Array(API::API.prefix.to_s)).uniq + routes_not_starting_in_wildcard + .map { |route| route.split('/')[1] } + .concat(ee_top_level_words) + .concat(files_in_public) + .concat(Array(API::API.prefix.to_s)) + .compact + .uniq end let(:ee_top_level_words) do diff --git a/spec/lib/gitlab/profiler_spec.rb b/spec/lib/gitlab/profiler_spec.rb index 548eb28fe4d..4059188fba1 100644 --- a/spec/lib/gitlab/profiler_spec.rb +++ b/spec/lib/gitlab/profiler_spec.rb @@ -135,6 +135,51 @@ describe Gitlab::Profiler do end end + describe '.clean_backtrace' do + it 'uses the Rails backtrace cleaner' do + backtrace = [] + + expect(Rails.backtrace_cleaner).to receive(:clean).with(backtrace) + + described_class.clean_backtrace(backtrace) + end + + it 'removes lines from IGNORE_BACKTRACES' do + backtrace = [ + "lib/gitlab/gitaly_client.rb:294:in `block (2 levels) in migrate'", + "lib/gitlab/gitaly_client.rb:331:in `allow_n_plus_1_calls'", + "lib/gitlab/gitaly_client.rb:280:in `block in migrate'", + "lib/gitlab/metrics/influx_db.rb:103:in `measure'", + "lib/gitlab/gitaly_client.rb:278:in `migrate'", + "lib/gitlab/git/repository.rb:1451:in `gitaly_migrate'", + "lib/gitlab/git/commit.rb:66:in `find'", + "app/models/repository.rb:1047:in `find_commit'", + "lib/gitlab/metrics/instrumentation.rb:159:in `block in find_commit'", + "lib/gitlab/metrics/method_call.rb:36:in `measure'", + "lib/gitlab/metrics/instrumentation.rb:159:in `find_commit'", + "app/models/repository.rb:113:in `commit'", + "lib/gitlab/i18n.rb:50:in `with_locale'", + "lib/gitlab/middleware/multipart.rb:95:in `call'", + "lib/gitlab/request_profiler/middleware.rb:14:in `call'", + "ee/lib/gitlab/database/load_balancing/rack_middleware.rb:37:in `call'", + "ee/lib/gitlab/jira/middleware.rb:15:in `call'" + ] + + expect(described_class.clean_backtrace(backtrace)) + .to eq([ + "lib/gitlab/gitaly_client.rb:294:in `block (2 levels) in migrate'", + "lib/gitlab/gitaly_client.rb:331:in `allow_n_plus_1_calls'", + "lib/gitlab/gitaly_client.rb:280:in `block in migrate'", + "lib/gitlab/gitaly_client.rb:278:in `migrate'", + "lib/gitlab/git/repository.rb:1451:in `gitaly_migrate'", + "lib/gitlab/git/commit.rb:66:in `find'", + "app/models/repository.rb:1047:in `find_commit'", + "app/models/repository.rb:113:in `commit'", + "ee/lib/gitlab/jira/middleware.rb:15:in `call'" + ]) + end + end + describe '.with_custom_logger' do context 'when the logger is set' do it 'uses the replacement logger for the duration of the block' do diff --git a/spec/lib/gitlab/project_search_results_spec.rb b/spec/lib/gitlab/project_search_results_spec.rb index e3f705d2299..50224bde722 100644 --- a/spec/lib/gitlab/project_search_results_spec.rb +++ b/spec/lib/gitlab/project_search_results_spec.rb @@ -22,47 +22,57 @@ describe Gitlab::ProjectSearchResults do it { expect(results.query).to eq('hello world') } end - describe 'blob search' do - let(:project) { create(:project, :public, :repository) } - - subject(:results) { described_class.new(user, project, 'files').objects('blobs') } - - context 'when repository is disabled' do - let(:project) { create(:project, :public, :repository, :repository_disabled) } + shared_examples 'general blob search' do |entity_type, blob_kind| + let(:query) { 'files' } + subject(:results) { described_class.new(user, project, query).objects(blob_type) } - it 'hides blobs from members' do + context "when #{entity_type} is disabled" do + let(:project) { disabled_project } + it "hides #{blob_kind} from members" do project.add_reporter(user) is_expected.to be_empty end - it 'hides blobs from non-members' do + it "hides #{blob_kind} from non-members" do is_expected.to be_empty end end - context 'when repository is internal' do - let(:project) { create(:project, :public, :repository, :repository_private) } + context "when #{entity_type} is internal" do + let(:project) { private_project } - it 'finds blobs for members' do + it "finds #{blob_kind} for members" do project.add_reporter(user) is_expected.not_to be_empty end - it 'hides blobs from non-members' do + it "hides #{blob_kind} from non-members" do is_expected.to be_empty end end it 'finds by name' do - expect(results.map(&:first)).to include('files/images/wm.svg') + expect(results.map(&:first)).to include(expected_file_by_name) end it 'finds by content' do - blob = results.select { |result| result.first == "CHANGELOG" }.flatten.last + blob = results.select { |result| result.first == expected_file_by_content }.flatten.last - expect(blob.filename).to eq("CHANGELOG") + expect(blob.filename).to eq(expected_file_by_content) + end + end + + describe 'blob search' do + let(:project) { create(:project, :public, :repository) } + + it_behaves_like 'general blob search', 'repository', 'blobs' do + let(:blob_type) { 'blobs' } + let(:disabled_project) { create(:project, :public, :repository, :repository_disabled) } + let(:private_project) { create(:project, :public, :repository, :repository_private) } + let(:expected_file_by_name) { 'files/images/wm.svg' } + let(:expected_file_by_content) { 'CHANGELOG' } end describe 'parsing results' do @@ -189,40 +199,18 @@ describe Gitlab::ProjectSearchResults do describe 'wiki search' do let(:project) { create(:project, :public, :wiki_repo) } let(:wiki) { build(:project_wiki, project: project) } - let!(:wiki_page) { wiki.create_page('Title', 'Content') } - - subject(:results) { described_class.new(user, project, 'Content').objects('wiki_blobs') } - - context 'when wiki is disabled' do - let(:project) { create(:project, :public, :wiki_repo, :wiki_disabled) } - it 'hides wiki blobs from members' do - project.add_reporter(user) - - is_expected.to be_empty - end - - it 'hides wiki blobs from non-members' do - is_expected.to be_empty - end - end - - context 'when wiki is internal' do - let(:project) { create(:project, :public, :wiki_repo, :wiki_private) } - - it 'finds wiki blobs for guest' do - project.add_guest(user) - - is_expected.not_to be_empty - end - - it 'hides wiki blobs from non-members' do - is_expected.to be_empty - end + before do + wiki.create_page('Files/Title', 'Content') + wiki.create_page('CHANGELOG', 'Files example') end - it 'finds by content' do - expect(results).to include("master:Title.md\x001\x00Content\n") + it_behaves_like 'general blob search', 'wiki', 'wiki blobs' do + let(:blob_type) { 'wiki_blobs' } + let(:disabled_project) { create(:project, :public, :wiki_repo, :wiki_disabled) } + let(:private_project) { create(:project, :public, :wiki_repo, :wiki_private) } + let(:expected_file_by_name) { 'Files/Title.md' } + let(:expected_file_by_content) { 'CHANGELOG.md' } end end diff --git a/spec/lib/gitlab/quick_actions/extractor_spec.rb b/spec/lib/gitlab/quick_actions/extractor_spec.rb index f7c288f2393..0166f6c2ee0 100644 --- a/spec/lib/gitlab/quick_actions/extractor_spec.rb +++ b/spec/lib/gitlab/quick_actions/extractor_spec.rb @@ -182,6 +182,14 @@ describe Gitlab::QuickActions::Extractor do expect(msg).to eq "hello\nworld" end + it 'extracts command case insensitive' do + msg = %(hello\n/PoWer @user.name %9.10 ~"bar baz.2"\nworld) + msg, commands = extractor.extract_commands(msg) + + expect(commands).to eq [['power', '@user.name %9.10 ~"bar baz.2"']] + expect(msg).to eq "hello\nworld" + end + it 'does not extract noop commands' do msg = %(hello\nworld\n/reopen\n/noop_command) msg, commands = extractor.extract_commands(msg) @@ -206,6 +214,14 @@ describe Gitlab::QuickActions::Extractor do expect(msg).to eq "hello\nworld\nthis is great? SHRUG" end + it 'extracts and performs substitution commands case insensitive' do + msg = %(hello\nworld\n/reOpen\n/sHRuG this is great?) + msg, commands = extractor.extract_commands(msg) + + expect(commands).to eq [['reopen'], ['shrug', 'this is great?']] + expect(msg).to eq "hello\nworld\nthis is great? SHRUG" + end + it 'extracts and performs substitution commands with comments' do msg = %(hello\nworld\n/reopen\n/substitution wow this is a thing.) msg, commands = extractor.extract_commands(msg) diff --git a/spec/lib/gitlab/shell_spec.rb b/spec/lib/gitlab/shell_spec.rb index bf6ee4b0b59..155e1663298 100644 --- a/spec/lib/gitlab/shell_spec.rb +++ b/spec/lib/gitlab/shell_spec.rb @@ -405,7 +405,11 @@ describe Gitlab::Shell do describe '#create_repository' do shared_examples '#create_repository' do let(:repository_storage) { 'default' } - let(:repository_storage_path) { Gitlab.config.repositories.storages[repository_storage].legacy_disk_path } + let(:repository_storage_path) do + Gitlab::GitalyClient::StorageSettings.allow_disk_access do + Gitlab.config.repositories.storages[repository_storage].legacy_disk_path + end + end let(:repo_name) { 'project/path' } let(:created_path) { File.join(repository_storage_path, repo_name + '.git') } @@ -494,16 +498,34 @@ describe Gitlab::Shell do ) end - it 'returns true when the command succeeds' do - expect(gitlab_projects).to receive(:fork_repository).with('nfs-file05', 'fork/path.git') { true } + context 'with gitaly' do + it 'returns true when the command succeeds' do + expect_any_instance_of(Gitlab::GitalyClient::RepositoryService).to receive(:fork_repository) + .with(repository.raw_repository) { :gitaly_response_object } + + is_expected.to be_truthy + end + + it 'return false when the command fails' do + expect_any_instance_of(Gitlab::GitalyClient::RepositoryService).to receive(:fork_repository) + .with(repository.raw_repository) { raise GRPC::BadStatus, 'bla' } - is_expected.to be_truthy + is_expected.to be_falsy + end end - it 'return false when the command fails' do - expect(gitlab_projects).to receive(:fork_repository).with('nfs-file05', 'fork/path.git') { false } + context 'without gitaly', :disable_gitaly do + it 'returns true when the command succeeds' do + expect(gitlab_projects).to receive(:fork_repository).with('nfs-file05', 'fork/path.git') { true } + + is_expected.to be_truthy + end + + it 'return false when the command fails' do + expect(gitlab_projects).to receive(:fork_repository).with('nfs-file05', 'fork/path.git') { false } - is_expected.to be_falsy + is_expected.to be_falsy + end end end @@ -658,21 +680,43 @@ describe Gitlab::Shell do describe '#import_repository' do let(:import_url) { 'https://gitlab.com/gitlab-org/gitlab-ce.git' } - it 'returns true when the command succeeds' do - expect(gitlab_projects).to receive(:import_project).with(import_url, timeout) { true } + context 'with gitaly' do + it 'returns true when the command succeeds' do + expect_any_instance_of(Gitlab::GitalyClient::RepositoryService).to receive(:import_repository).with(import_url) - result = gitlab_shell.import_repository(project.repository_storage, project.disk_path, import_url) + result = gitlab_shell.import_repository(project.repository_storage, project.disk_path, import_url) - expect(result).to be_truthy + expect(result).to be_truthy + end + + it 'raises an exception when the command fails' do + expect_any_instance_of(Gitlab::GitalyClient::RepositoryService).to receive(:import_repository) + .with(import_url) { raise GRPC::BadStatus, 'bla' } + expect_any_instance_of(Gitlab::Shell::GitalyGitlabProjects).to receive(:output) { 'error'} + + expect do + gitlab_shell.import_repository(project.repository_storage, project.disk_path, import_url) + end.to raise_error(Gitlab::Shell::Error, "error") + end end - it 'raises an exception when the command fails' do - allow(gitlab_projects).to receive(:output) { 'error' } - expect(gitlab_projects).to receive(:import_project) { false } + context 'without gitaly', :disable_gitaly do + it 'returns true when the command succeeds' do + expect(gitlab_projects).to receive(:import_project).with(import_url, timeout) { true } - expect do - gitlab_shell.import_repository(project.repository_storage, project.disk_path, import_url) - end.to raise_error(Gitlab::Shell::Error, "error") + result = gitlab_shell.import_repository(project.repository_storage, project.disk_path, import_url) + + expect(result).to be_truthy + end + + it 'raises an exception when the command fails' do + allow(gitlab_projects).to receive(:output) { 'error' } + expect(gitlab_projects).to receive(:import_project) { false } + + expect do + gitlab_shell.import_repository(project.repository_storage, project.disk_path, import_url) + end.to raise_error(Gitlab::Shell::Error, "error") + end end end end diff --git a/spec/lib/gitlab/sql/cte_spec.rb b/spec/lib/gitlab/sql/cte_spec.rb new file mode 100644 index 00000000000..d6763c7b2e1 --- /dev/null +++ b/spec/lib/gitlab/sql/cte_spec.rb @@ -0,0 +1,42 @@ +require 'spec_helper' + +describe Gitlab::SQL::CTE, :postgresql do + describe '#to_arel' do + it 'generates an Arel relation for the CTE body' do + relation = User.where(id: 1) + cte = described_class.new(:cte_name, relation) + sql = cte.to_arel.to_sql + name = ActiveRecord::Base.connection.quote_table_name(:cte_name) + + sql1 = ActiveRecord::Base.connection.unprepared_statement do + relation.except(:order).to_sql + end + + expect(sql).to eq("#{name} AS (#{sql1})") + end + end + + describe '#alias_to' do + it 'returns an alias for the CTE' do + cte = described_class.new(:cte_name, nil) + table = Arel::Table.new(:kittens) + + source_name = ActiveRecord::Base.connection.quote_table_name(:cte_name) + alias_name = ActiveRecord::Base.connection.quote_table_name(:kittens) + + expect(cte.alias_to(table).to_sql).to eq("#{source_name} AS #{alias_name}") + end + end + + describe '#apply_to' do + it 'applies a CTE to an ActiveRecord::Relation' do + user = create(:user) + cte = described_class.new(:cte_name, User.where(id: user.id)) + + relation = cte.apply_to(User.all) + + expect(relation.to_sql).to match(/WITH .+cte_name/) + expect(relation.to_a).to eq(User.where(id: user.id).to_a) + end + end +end diff --git a/spec/lib/gitlab/sql/glob_spec.rb b/spec/lib/gitlab/sql/glob_spec.rb index f0bb4294d62..1cf8935bfe3 100644 --- a/spec/lib/gitlab/sql/glob_spec.rb +++ b/spec/lib/gitlab/sql/glob_spec.rb @@ -35,8 +35,9 @@ describe Gitlab::SQL::Glob do value = query("SELECT #{quote(string)} LIKE #{pattern}") .rows.flatten.first + check = Gitlab.rails5? ? true : 't' case value - when 't', 1 + when check, 1 true else false diff --git a/spec/lib/gitlab/themes_spec.rb b/spec/lib/gitlab/themes_spec.rb index ecacea6bb35..a8213988f70 100644 --- a/spec/lib/gitlab/themes_spec.rb +++ b/spec/lib/gitlab/themes_spec.rb @@ -5,9 +5,9 @@ describe Gitlab::Themes, lib: true do it 'returns a space-separated list of class names' do css = described_class.body_classes - expect(css).to include('ui_indigo') - expect(css).to include(' ui_dark ') - expect(css).to include(' ui_blue') + expect(css).to include('ui-indigo') + expect(css).to include('ui-dark') + expect(css).to include('ui-blue') end end diff --git a/spec/lib/gitlab/url_blocker_spec.rb b/spec/lib/gitlab/url_blocker_spec.rb index 81dbbb962dd..6f5f9938eca 100644 --- a/spec/lib/gitlab/url_blocker_spec.rb +++ b/spec/lib/gitlab/url_blocker_spec.rb @@ -58,20 +58,6 @@ describe Gitlab::UrlBlocker do end end - it 'returns true for a non-alphanumeric username' do - stub_resolv - - aggregate_failures do - expect(described_class).to be_blocked_url('ssh://-oProxyCommand=whoami@example.com/a') - - # The leading character here is a Unicode "soft hyphen" - expect(described_class).to be_blocked_url('ssh://oProxyCommand=whoami@example.com/a') - - # Unicode alphanumerics are allowed - expect(described_class).not_to be_blocked_url('ssh://ğitlab@example.com/a') - end - end - it 'returns true for invalid URL' do expect(described_class.blocked_url?('http://:8080')).to be true end @@ -120,6 +106,38 @@ describe Gitlab::UrlBlocker do allow(Addrinfo).to receive(:getaddrinfo).and_call_original end end + + context 'when enforce_user is' do + before do + stub_resolv + end + + context 'false (default)' do + it 'does not block urls with a non-alphanumeric username' do + expect(described_class).not_to be_blocked_url('ssh://-oProxyCommand=whoami@example.com/a') + + # The leading character here is a Unicode "soft hyphen" + expect(described_class).not_to be_blocked_url('ssh://oProxyCommand=whoami@example.com/a') + + # Unicode alphanumerics are allowed + expect(described_class).not_to be_blocked_url('ssh://ğitlab@example.com/a') + end + end + + context 'true' do + it 'blocks urls with a non-alphanumeric username' do + aggregate_failures do + expect(described_class).to be_blocked_url('ssh://-oProxyCommand=whoami@example.com/a', enforce_user: true) + + # The leading character here is a Unicode "soft hyphen" + expect(described_class).to be_blocked_url('ssh://oProxyCommand=whoami@example.com/a', enforce_user: true) + + # Unicode alphanumerics are allowed + expect(described_class).not_to be_blocked_url('ssh://ğitlab@example.com/a', enforce_user: true) + end + end + end + end end # Resolv does not support resolving UTF-8 domain names diff --git a/spec/lib/gitlab/url_builder_spec.rb b/spec/lib/gitlab/url_builder_spec.rb index b81749cf428..9f495a5d50b 100644 --- a/spec/lib/gitlab/url_builder_spec.rb +++ b/spec/lib/gitlab/url_builder_spec.rb @@ -22,6 +22,31 @@ describe Gitlab::UrlBuilder do end end + context 'when passing a Milestone' do + let(:group) { create(:group) } + let(:project) { create(:project, :public, namespace: group) } + + context 'belonging to a project' do + it 'returns a proper URL' do + milestone = create(:milestone, project: project) + + url = described_class.build(milestone) + + expect(url).to eq "#{Settings.gitlab['url']}/#{milestone.project.full_path}/milestones/#{milestone.iid}" + end + end + + context 'belonging to a group' do + it 'returns a proper URL' do + milestone = create(:milestone, group: group) + + url = described_class.build(milestone) + + expect(url).to eq "#{Settings.gitlab['url']}/groups/#{milestone.group.full_path}/-/milestones/#{milestone.iid}" + end + end + end + context 'when passing a MergeRequest' do it 'returns a proper URL' do merge_request = build_stubbed(:merge_request, iid: 42) diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb index a716e6f5434..22d921716aa 100644 --- a/spec/lib/gitlab/usage_data_spec.rb +++ b/spec/lib/gitlab/usage_data_spec.rb @@ -32,6 +32,7 @@ describe Gitlab::UsageData do mattermost_enabled edition version + installation_type uuid hostname signup @@ -156,6 +157,7 @@ describe Gitlab::UsageData do it "gathers license data" do expect(subject[:uuid]).to eq(Gitlab::CurrentSettings.uuid) expect(subject[:version]).to eq(Gitlab::VERSION) + expect(subject[:installation_type]).to eq(Gitlab::INSTALLATION_TYPE) expect(subject[:active_user_count]).to eq(User.active.count) expect(subject[:recorded_at]).to be_a(Time) end diff --git a/spec/lib/gitlab/user_access_spec.rb b/spec/lib/gitlab/user_access_spec.rb index 97b6069f64d..0469d984a40 100644 --- a/spec/lib/gitlab/user_access_spec.rb +++ b/spec/lib/gitlab/user_access_spec.rb @@ -142,7 +142,7 @@ describe Gitlab::UserAccess do target_project: canonical_project, source_project: project, source_branch: 'awesome-feature', - allow_maintainer_to_push: true + allow_collaboration: true ) end diff --git a/spec/lib/gitlab/utils/override_spec.rb b/spec/lib/gitlab/utils/override_spec.rb index 7c97cee982a..fc08ebcfc6d 100644 --- a/spec/lib/gitlab/utils/override_spec.rb +++ b/spec/lib/gitlab/utils/override_spec.rb @@ -1,7 +1,13 @@ -require 'spec_helper' +require 'fast_spec_helper' describe Gitlab::Utils::Override do - let(:base) { Struct.new(:good) } + let(:base) do + Struct.new(:good) do + def self.good + 0 + end + end + end let(:derived) { Class.new(base).tap { |m| m.extend described_class } } let(:extension) { Module.new.tap { |m| m.extend described_class } } @@ -9,6 +15,14 @@ describe Gitlab::Utils::Override do let(:prepending_class) { base.tap { |m| m.prepend extension } } let(:including_class) { base.tap { |m| m.include extension } } + let(:prepending_class_methods) do + base.tap { |m| m.singleton_class.prepend extension } + end + + let(:extending_class_methods) do + base.tap { |m| m.extend extension } + end + let(:klass) { subject } def good(mod) @@ -36,7 +50,7 @@ describe Gitlab::Utils::Override do shared_examples 'checking as intended' do it 'checks ok for overriding method' do good(subject) - result = klass.new(0).good + result = instance.good expect(result).to eq(1) described_class.verify! @@ -45,7 +59,25 @@ describe Gitlab::Utils::Override do it 'raises NotImplementedError when it is not overriding anything' do expect do bad(subject) - klass.new(0).bad + instance.bad + described_class.verify! + end.to raise_error(NotImplementedError) + end + end + + shared_examples 'checking as intended, nothing was overridden' do + it 'raises NotImplementedError because it is not overriding it' do + expect do + good(subject) + instance.good + described_class.verify! + end.to raise_error(NotImplementedError) + end + + it 'raises NotImplementedError when it is not overriding anything' do + expect do + bad(subject) + instance.bad described_class.verify! end.to raise_error(NotImplementedError) end @@ -54,7 +86,7 @@ describe Gitlab::Utils::Override do shared_examples 'nothing happened' do it 'does not complain when it is overriding something' do good(subject) - result = klass.new(0).good + result = instance.good expect(result).to eq(1) described_class.verify! @@ -62,7 +94,7 @@ describe Gitlab::Utils::Override do it 'does not complain when it is not overriding anything' do bad(subject) - result = klass.new(0).bad + result = instance.bad expect(result).to eq(true) described_class.verify! @@ -75,83 +107,97 @@ describe Gitlab::Utils::Override do end describe '#override' do - context 'when STATIC_VERIFICATION is set' do - before do - stub_env('STATIC_VERIFICATION', 'true') - end + context 'when instance is klass.new(0)' do + let(:instance) { klass.new(0) } - context 'when subject is a class' do - subject { derived } + context 'when STATIC_VERIFICATION is set' do + before do + stub_env('STATIC_VERIFICATION', 'true') + end - it_behaves_like 'checking as intended' - end + context 'when subject is a class' do + subject { derived } + + it_behaves_like 'checking as intended' + end + + context 'when subject is a module, and class is prepending it' do + subject { extension } + let(:klass) { prepending_class } + + it_behaves_like 'checking as intended' + end - context 'when subject is a module, and class is prepending it' do - subject { extension } - let(:klass) { prepending_class } + context 'when subject is a module, and class is including it' do + subject { extension } + let(:klass) { including_class } - it_behaves_like 'checking as intended' + it_behaves_like 'checking as intended, nothing was overridden' + end end - context 'when subject is a module, and class is including it' do - subject { extension } - let(:klass) { including_class } + context 'when STATIC_VERIFICATION is not set' do + before do + stub_env('STATIC_VERIFICATION', nil) + end - it 'raises NotImplementedError because it is not overriding it' do - expect do - good(subject) - klass.new(0).good - described_class.verify! - end.to raise_error(NotImplementedError) + context 'when subject is a class' do + subject { derived } + + it_behaves_like 'nothing happened' end - it 'raises NotImplementedError when it is not overriding anything' do - expect do - bad(subject) - klass.new(0).bad - described_class.verify! - end.to raise_error(NotImplementedError) + context 'when subject is a module, and class is prepending it' do + subject { extension } + let(:klass) { prepending_class } + + it_behaves_like 'nothing happened' end - end - end - end - context 'when STATIC_VERIFICATION is not set' do - before do - stub_env('STATIC_VERIFICATION', nil) - end + context 'when subject is a module, and class is including it' do + subject { extension } + let(:klass) { including_class } - context 'when subject is a class' do - subject { derived } + it 'does not complain when it is overriding something' do + good(subject) + result = instance.good - it_behaves_like 'nothing happened' - end + expect(result).to eq(0) + described_class.verify! + end - context 'when subject is a module, and class is prepending it' do - subject { extension } - let(:klass) { prepending_class } + it 'does not complain when it is not overriding anything' do + bad(subject) + result = instance.bad - it_behaves_like 'nothing happened' + expect(result).to eq(true) + described_class.verify! + end + end + end end - context 'when subject is a module, and class is including it' do - subject { extension } - let(:klass) { including_class } + context 'when instance is klass' do + let(:instance) { klass } - it 'does not complain when it is overriding something' do - good(subject) - result = klass.new(0).good + context 'when STATIC_VERIFICATION is set' do + before do + stub_env('STATIC_VERIFICATION', 'true') + end - expect(result).to eq(0) - described_class.verify! - end + context 'when subject is a module, and class is prepending it' do + subject { extension } + let(:klass) { prepending_class_methods } - it 'does not complain when it is not overriding anything' do - bad(subject) - result = klass.new(0).bad + it_behaves_like 'checking as intended' + end - expect(result).to eq(true) - described_class.verify! + context 'when subject is a module, and class is extending it' do + subject { extension } + let(:klass) { extending_class_methods } + + it_behaves_like 'checking as intended, nothing was overridden' + end end end end diff --git a/spec/lib/gitlab/verify/job_artifacts_spec.rb b/spec/lib/gitlab/verify/job_artifacts_spec.rb index ec490bdfde2..6e916a56564 100644 --- a/spec/lib/gitlab/verify/job_artifacts_spec.rb +++ b/spec/lib/gitlab/verify/job_artifacts_spec.rb @@ -21,15 +21,38 @@ describe Gitlab::Verify::JobArtifacts do FileUtils.rm_f(artifact.file.path) expect(failures.keys).to contain_exactly(artifact) - expect(failure).to be_a(Errno::ENOENT) - expect(failure.to_s).to include(artifact.file.path) + expect(failure).to include('No such file or directory') + expect(failure).to include(artifact.file.path) end it 'fails artifacts with a mismatched checksum' do File.truncate(artifact.file.path, 0) expect(failures.keys).to contain_exactly(artifact) - expect(failure.to_s).to include('Checksum mismatch') + expect(failure).to include('Checksum mismatch') + end + + context 'with remote files' do + let(:file) { double(:file) } + + before do + stub_artifacts_object_storage + artifact.update!(file_store: ObjectStorage::Store::REMOTE) + expect(CarrierWave::Storage::Fog::File).to receive(:new).and_return(file) + end + + it 'passes artifacts in object storage that exist' do + expect(file).to receive(:exists?).and_return(true) + + expect(failures).to eq({}) + end + + it 'fails artifacts in object storage that do not exist' do + expect(file).to receive(:exists?).and_return(false) + + expect(failures.keys).to contain_exactly(artifact) + expect(failure).to include('Remote object does not exist') + end end end end diff --git a/spec/lib/gitlab/verify/lfs_objects_spec.rb b/spec/lib/gitlab/verify/lfs_objects_spec.rb index 0f890e2c7ce..2feaedd6f14 100644 --- a/spec/lib/gitlab/verify/lfs_objects_spec.rb +++ b/spec/lib/gitlab/verify/lfs_objects_spec.rb @@ -21,30 +21,37 @@ describe Gitlab::Verify::LfsObjects do FileUtils.rm_f(lfs_object.file.path) expect(failures.keys).to contain_exactly(lfs_object) - expect(failure).to be_a(Errno::ENOENT) - expect(failure.to_s).to include(lfs_object.file.path) + expect(failure).to include('No such file or directory') + expect(failure).to include(lfs_object.file.path) end it 'fails LFS objects with a mismatched oid' do File.truncate(lfs_object.file.path, 0) expect(failures.keys).to contain_exactly(lfs_object) - expect(failure.to_s).to include('Checksum mismatch') + expect(failure).to include('Checksum mismatch') end context 'with remote files' do + let(:file) { double(:file) } + before do stub_lfs_object_storage + lfs_object.update!(file_store: ObjectStorage::Store::REMOTE) + expect(CarrierWave::Storage::Fog::File).to receive(:new).and_return(file) end - it 'skips LFS objects in object storage' do - local_failure = create(:lfs_object) - create(:lfs_object, :object_storage) + it 'passes LFS objects in object storage that exist' do + expect(file).to receive(:exists?).and_return(true) + + expect(failures).to eq({}) + end - failures = {} - described_class.new(batch_size: 10).run_batches { |_, failed| failures.merge!(failed) } + it 'fails LFS objects in object storage that do not exist' do + expect(file).to receive(:exists?).and_return(false) - expect(failures.keys).to contain_exactly(local_failure) + expect(failures.keys).to contain_exactly(lfs_object) + expect(failure).to include('Remote object does not exist') end end end diff --git a/spec/lib/gitlab/verify/uploads_spec.rb b/spec/lib/gitlab/verify/uploads_spec.rb index 85768308edc..296866d3319 100644 --- a/spec/lib/gitlab/verify/uploads_spec.rb +++ b/spec/lib/gitlab/verify/uploads_spec.rb @@ -23,37 +23,44 @@ describe Gitlab::Verify::Uploads do FileUtils.rm_f(upload.absolute_path) expect(failures.keys).to contain_exactly(upload) - expect(failure).to be_a(Errno::ENOENT) - expect(failure.to_s).to include(upload.absolute_path) + expect(failure).to include('No such file or directory') + expect(failure).to include(upload.absolute_path) end it 'fails uploads with a mismatched checksum' do upload.update!(checksum: 'something incorrect') expect(failures.keys).to contain_exactly(upload) - expect(failure.to_s).to include('Checksum mismatch') + expect(failure).to include('Checksum mismatch') end it 'fails uploads with a missing precalculated checksum' do upload.update!(checksum: '') expect(failures.keys).to contain_exactly(upload) - expect(failure.to_s).to include('Checksum missing') + expect(failure).to include('Checksum missing') end context 'with remote files' do + let(:file) { double(:file) } + before do stub_uploads_object_storage(AvatarUploader) + upload.update!(store: ObjectStorage::Store::REMOTE) + expect(CarrierWave::Storage::Fog::File).to receive(:new).and_return(file) end - it 'skips uploads in object storage' do - local_failure = create(:upload) - create(:upload, :object_storage) + it 'passes uploads in object storage that exist' do + expect(file).to receive(:exists?).and_return(true) + + expect(failures).to eq({}) + end - failures = {} - described_class.new(batch_size: 10).run_batches { |_, failed| failures.merge!(failed) } + it 'fails uploads in object storage that do not exist' do + expect(file).to receive(:exists?).and_return(false) - expect(failures.keys).to contain_exactly(local_failure) + expect(failures.keys).to contain_exactly(upload) + expect(failure).to include('Remote object does not exist') end end end diff --git a/spec/lib/gitlab/wiki_file_finder_spec.rb b/spec/lib/gitlab/wiki_file_finder_spec.rb new file mode 100644 index 00000000000..025d1203dc5 --- /dev/null +++ b/spec/lib/gitlab/wiki_file_finder_spec.rb @@ -0,0 +1,20 @@ +require 'spec_helper' + +describe Gitlab::WikiFileFinder do + describe '#find' do + let(:project) { create(:project, :public, :wiki_repo) } + let(:wiki) { build(:project_wiki, project: project) } + + before do + wiki.create_page('Files/Title', 'Content') + wiki.create_page('CHANGELOG', 'Files example') + end + + it_behaves_like 'file finder' do + subject { described_class.new(project, project.wiki.default_branch) } + + let(:expected_file_by_name) { 'Files/Title.md' } + let(:expected_file_by_content) { 'CHANGELOG.md' } + end + end +end diff --git a/spec/lib/microsoft_teams/notifier_spec.rb b/spec/lib/microsoft_teams/notifier_spec.rb index 3035693812f..c9756544bd6 100644 --- a/spec/lib/microsoft_teams/notifier_spec.rb +++ b/spec/lib/microsoft_teams/notifier_spec.rb @@ -8,7 +8,7 @@ describe MicrosoftTeams::Notifier do let(:options) do { title: 'JohnDoe4/project2', - pretext: '[[JohnDoe4/project2](http://localhost/namespace2/gitlabhq)] Issue [#1 Awesome issue](http://localhost/namespace2/gitlabhq/issues/1) opened by user6', + summary: '[[JohnDoe4/project2](http://localhost/namespace2/gitlabhq)] Issue [#1 Awesome issue](http://localhost/namespace2/gitlabhq/issues/1) opened by user6', activity: { title: 'Issue opened by user6', subtitle: 'in [JohnDoe4/project2](http://localhost/namespace2/gitlabhq)', diff --git a/spec/lib/object_storage/direct_upload_spec.rb b/spec/lib/object_storage/direct_upload_spec.rb new file mode 100644 index 00000000000..e0569218d78 --- /dev/null +++ b/spec/lib/object_storage/direct_upload_spec.rb @@ -0,0 +1,168 @@ +require 'spec_helper' + +describe ObjectStorage::DirectUpload do + let(:credentials) do + { + provider: 'AWS', + aws_access_key_id: 'AWS_ACCESS_KEY_ID', + aws_secret_access_key: 'AWS_SECRET_ACCESS_KEY' + } + end + + let(:storage_url) { 'https://uploads.s3.amazonaws.com/' } + + let(:bucket_name) { 'uploads' } + let(:object_name) { 'tmp/uploads/my-file' } + let(:maximum_size) { 1.gigabyte } + + let(:direct_upload) { described_class.new(credentials, bucket_name, object_name, has_length: has_length, maximum_size: maximum_size) } + + before do + Fog.unmock! + end + + describe '#has_length' do + context 'is known' do + let(:has_length) { true } + let(:maximum_size) { nil } + + it "maximum size is not required" do + expect { direct_upload }.not_to raise_error + end + end + + context 'is unknown' do + let(:has_length) { false } + + context 'and maximum size is specified' do + let(:maximum_size) { 1.gigabyte } + + it "does not raise an error" do + expect { direct_upload }.not_to raise_error + end + end + + context 'and maximum size is not specified' do + let(:maximum_size) { nil } + + it "raises an error" do + expect { direct_upload }.to raise_error /maximum_size has to be specified if length is unknown/ + end + end + end + end + + describe '#to_hash' do + subject { direct_upload.to_hash } + + shared_examples 'a valid upload' do + it "returns valid structure" do + expect(subject).to have_key(:Timeout) + expect(subject[:GetURL]).to start_with(storage_url) + expect(subject[:StoreURL]).to start_with(storage_url) + expect(subject[:DeleteURL]).to start_with(storage_url) + end + end + + shared_examples 'a valid upload with multipart data' do + before do + stub_object_storage_multipart_init(storage_url, "myUpload") + end + + it_behaves_like 'a valid upload' + + it "returns valid structure" do + expect(subject).to have_key(:MultipartUpload) + expect(subject[:MultipartUpload]).to have_key(:PartSize) + expect(subject[:MultipartUpload][:PartURLs]).to all(start_with(storage_url)) + expect(subject[:MultipartUpload][:PartURLs]).to all(include('uploadId=myUpload')) + expect(subject[:MultipartUpload][:CompleteURL]).to start_with(storage_url) + expect(subject[:MultipartUpload][:CompleteURL]).to include('uploadId=myUpload') + expect(subject[:MultipartUpload][:AbortURL]).to start_with(storage_url) + expect(subject[:MultipartUpload][:AbortURL]).to include('uploadId=myUpload') + end + end + + shared_examples 'a valid upload without multipart data' do + it_behaves_like 'a valid upload' + + it "returns valid structure" do + expect(subject).not_to have_key(:MultipartUpload) + end + end + + context 'when AWS is used' do + context 'when length is known' do + let(:has_length) { true } + + it_behaves_like 'a valid upload without multipart data' + end + + context 'when length is unknown' do + let(:has_length) { false } + + it_behaves_like 'a valid upload with multipart data' do + context 'when maximum upload size is 10MB' do + let(:maximum_size) { 10.megabyte } + + it 'returns only 2 parts' do + expect(subject[:MultipartUpload][:PartURLs].length).to eq(2) + end + + it 'part size is mimimum, 5MB' do + expect(subject[:MultipartUpload][:PartSize]).to eq(5.megabyte) + end + end + + context 'when maximum upload size is 12MB' do + let(:maximum_size) { 12.megabyte } + + it 'returns only 3 parts' do + expect(subject[:MultipartUpload][:PartURLs].length).to eq(3) + end + + it 'part size is rounded-up to 5MB' do + expect(subject[:MultipartUpload][:PartSize]).to eq(5.megabyte) + end + end + + context 'when maximum upload size is 49GB' do + let(:maximum_size) { 49.gigabyte } + + it 'returns maximum, 100 parts' do + expect(subject[:MultipartUpload][:PartURLs].length).to eq(100) + end + + it 'part size is rounded-up to 5MB' do + expect(subject[:MultipartUpload][:PartSize]).to eq(505.megabyte) + end + end + end + end + end + + context 'when Google is used' do + let(:credentials) do + { + provider: 'Google', + google_storage_access_key_id: 'GOOGLE_ACCESS_KEY_ID', + google_storage_secret_access_key: 'GOOGLE_SECRET_ACCESS_KEY' + } + end + + let(:storage_url) { 'https://storage.googleapis.com/uploads/' } + + context 'when length is known' do + let(:has_length) { true } + + it_behaves_like 'a valid upload without multipart data' + end + + context 'when length is unknown' do + let(:has_length) { false } + + it_behaves_like 'a valid upload without multipart data' + end + end + end +end diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb index 69eafbe4bbe..775ca4ba0eb 100644 --- a/spec/mailers/notify_spec.rb +++ b/spec/mailers/notify_spec.rb @@ -68,7 +68,7 @@ describe Notify do end it 'contains the description' do - is_expected.to have_html_escaped_body_text issue.description + is_expected.to have_body_text issue.description end it 'does not add a reason header' do @@ -89,7 +89,7 @@ describe Notify do end it 'contains a link to note author' do - is_expected.to have_html_escaped_body_text(issue.author_name) + is_expected.to have_body_text(issue.author_name) is_expected.to have_body_text 'created an issue:' end end @@ -115,8 +115,8 @@ describe Notify do it 'has the correct subject and body' do aggregate_failures do is_expected.to have_referable_subject(issue, reply: true) - is_expected.to have_html_escaped_body_text(previous_assignee.name) - is_expected.to have_html_escaped_body_text(assignee.name) + is_expected.to have_body_text(previous_assignee.name) + is_expected.to have_body_text(assignee.name) is_expected.to have_body_text(project_issue_path(project, issue)) end end @@ -190,7 +190,7 @@ describe Notify do aggregate_failures do is_expected.to have_referable_subject(issue, reply: true) is_expected.to have_body_text(status) - is_expected.to have_html_escaped_body_text(current_user.name) + is_expected.to have_body_text(current_user.name) is_expected.to have_body_text(project_issue_path project, issue) end end @@ -243,7 +243,7 @@ describe Notify do end it 'contains the description' do - is_expected.to have_html_escaped_body_text merge_request.description + is_expected.to have_body_text merge_request.description end context 'when sent with a reason' do @@ -260,7 +260,7 @@ describe Notify do end it 'contains a link to note author' do - is_expected.to have_html_escaped_body_text merge_request.author_name + is_expected.to have_body_text merge_request.author_name is_expected.to have_body_text 'created a merge request:' end end @@ -286,9 +286,9 @@ describe Notify do it 'has the correct subject and body' do aggregate_failures do is_expected.to have_referable_subject(merge_request, reply: true) - is_expected.to have_html_escaped_body_text(previous_assignee.name) + is_expected.to have_body_text(previous_assignee.name) is_expected.to have_body_text(project_merge_request_path(project, merge_request)) - is_expected.to have_html_escaped_body_text(assignee.name) + is_expected.to have_body_text(assignee.name) end end @@ -358,7 +358,7 @@ describe Notify do aggregate_failures do is_expected.to have_referable_subject(merge_request, reply: true) is_expected.to have_body_text(status) - is_expected.to have_html_escaped_body_text(current_user.name) + is_expected.to have_body_text(current_user.name) is_expected.to have_body_text(project_merge_request_path(project, merge_request)) end end @@ -526,7 +526,7 @@ describe Notify do it 'has the correct subject and body' do is_expected.to have_referable_subject(project_snippet, reply: true) - is_expected.to have_html_escaped_body_text project_snippet_note.note + is_expected.to have_body_text project_snippet_note.note end end @@ -539,7 +539,7 @@ describe Notify do it 'has the correct subject and body' do is_expected.to have_subject("#{project.name} | Project was moved") - is_expected.to have_html_escaped_body_text project.full_name + is_expected.to have_body_text project.full_name is_expected.to have_body_text(project.ssh_url_to_repo) end end @@ -566,7 +566,7 @@ describe Notify do expect(to_emails).to eq([recipient.notification_email]) is_expected.to have_subject "Request to join the #{project.full_name} project" - is_expected.to have_html_escaped_body_text project.full_name + is_expected.to have_body_text project.full_name is_expected.to have_body_text project_project_members_url(project) is_expected.to have_body_text project_member.human_access end @@ -586,7 +586,7 @@ describe Notify do it 'contains all the useful information' do is_expected.to have_subject "Access to the #{project.full_name} project was denied" - is_expected.to have_html_escaped_body_text project.full_name + is_expected.to have_body_text project.full_name is_expected.to have_body_text project.web_url end end @@ -603,7 +603,7 @@ describe Notify do it 'contains all the useful information' do is_expected.to have_subject "Access to the #{project.full_name} project was granted" - is_expected.to have_html_escaped_body_text project.full_name + is_expected.to have_body_text project.full_name is_expected.to have_body_text project.web_url is_expected.to have_body_text project_member.human_access end @@ -633,7 +633,7 @@ describe Notify do it 'contains all the useful information' do is_expected.to have_subject "Invitation to join the #{project.full_name} project" - is_expected.to have_html_escaped_body_text project.full_name + is_expected.to have_body_text project.full_name is_expected.to have_body_text project.full_name is_expected.to have_body_text project_member.human_access is_expected.to have_body_text project_member.invite_token @@ -657,10 +657,10 @@ describe Notify do it 'contains all the useful information' do is_expected.to have_subject 'Invitation accepted' - is_expected.to have_html_escaped_body_text project.full_name + is_expected.to have_body_text project.full_name is_expected.to have_body_text project.web_url is_expected.to have_body_text project_member.invite_email - is_expected.to have_html_escaped_body_text invited_user.name + is_expected.to have_body_text invited_user.name end end @@ -680,7 +680,7 @@ describe Notify do it 'contains all the useful information' do is_expected.to have_subject 'Invitation declined' - is_expected.to have_html_escaped_body_text project.full_name + is_expected.to have_body_text project.full_name is_expected.to have_body_text project.web_url is_expected.to have_body_text project_member.invite_email end @@ -932,7 +932,7 @@ describe Notify do end it 'contains the message from the note' do - is_expected.to have_html_escaped_body_text note.note + is_expected.to have_body_text note.note end it 'contains an introduction' do @@ -991,7 +991,7 @@ describe Notify do expect(to_emails).to eq([recipient.notification_email]) is_expected.to have_subject "Request to join the #{group.name} group" - is_expected.to have_html_escaped_body_text group.name + is_expected.to have_body_text group.name is_expected.to have_body_text group_group_members_url(group) is_expected.to have_body_text group_member.human_access end @@ -1010,7 +1010,7 @@ describe Notify do it 'contains all the useful information' do is_expected.to have_subject "Access to the #{group.name} group was denied" - is_expected.to have_html_escaped_body_text group.name + is_expected.to have_body_text group.name is_expected.to have_body_text group.web_url end end @@ -1026,7 +1026,7 @@ describe Notify do it 'contains all the useful information' do is_expected.to have_subject "Access to the #{group.name} group was granted" - is_expected.to have_html_escaped_body_text group.name + is_expected.to have_body_text group.name is_expected.to have_body_text group.web_url is_expected.to have_body_text group_member.human_access end @@ -1056,7 +1056,7 @@ describe Notify do it 'contains all the useful information' do is_expected.to have_subject "Invitation to join the #{group.name} group" - is_expected.to have_html_escaped_body_text group.name + is_expected.to have_body_text group.name is_expected.to have_body_text group.web_url is_expected.to have_body_text group_member.human_access is_expected.to have_body_text group_member.invite_token @@ -1080,10 +1080,10 @@ describe Notify do it 'contains all the useful information' do is_expected.to have_subject 'Invitation accepted' - is_expected.to have_html_escaped_body_text group.name + is_expected.to have_body_text group.name is_expected.to have_body_text group.web_url is_expected.to have_body_text group_member.invite_email - is_expected.to have_html_escaped_body_text invited_user.name + is_expected.to have_body_text invited_user.name end end @@ -1103,7 +1103,7 @@ describe Notify do it 'contains all the useful information' do is_expected.to have_subject 'Invitation declined' - is_expected.to have_html_escaped_body_text group.name + is_expected.to have_body_text group.name is_expected.to have_body_text group.web_url is_expected.to have_body_text group_member.invite_email end @@ -1396,7 +1396,7 @@ describe Notify do it 'has the correct subject and body' do is_expected.to have_referable_subject(personal_snippet, reply: true) - is_expected.to have_html_escaped_body_text personal_snippet_note.note + is_expected.to have_body_text personal_snippet_note.note end end end diff --git a/spec/migrations/active_record/schema_spec.rb b/spec/migrations/active_record/schema_spec.rb index e132529d8d8..9d35b3cd642 100644 --- a/spec/migrations/active_record/schema_spec.rb +++ b/spec/migrations/active_record/schema_spec.rb @@ -5,7 +5,11 @@ require 'spec_helper' describe ActiveRecord::Schema do let(:latest_migration_timestamp) do - migrations = Dir[Rails.root.join('db', 'migrate', '*'), Rails.root.join('db', 'post_migrate', '*')] + migrations_paths = %w[db ee/db] + .product(%w[migrate post_migrate]) + .map { |path| Rails.root.join(*path, '*') } + + migrations = Dir[*migrations_paths] migrations.map { |migration| File.basename(migration).split('_').first.to_i }.max end diff --git a/spec/migrations/change_default_value_for_dsa_key_restriction_spec.rb b/spec/migrations/change_default_value_for_dsa_key_restriction_spec.rb new file mode 100644 index 00000000000..7e61ab9b52e --- /dev/null +++ b/spec/migrations/change_default_value_for_dsa_key_restriction_spec.rb @@ -0,0 +1,33 @@ +require 'spec_helper' +require Rails.root.join('db', 'migrate', '20180531220618_change_default_value_for_dsa_key_restriction.rb') + +describe ChangeDefaultValueForDsaKeyRestriction, :migration do + let(:application_settings) { table(:application_settings) } + + before do + application_settings.create! + end + + it 'changes the default value for dsa_key_restriction' do + expect(application_settings.first.dsa_key_restriction).to eq(0) + + migrate! + + application_settings.reset_column_information + new_setting = application_settings.create! + + expect(application_settings.count).to eq(2) + expect(new_setting.dsa_key_restriction).to eq(-1) + end + + it 'changes the existing setting' do + setting = application_settings.last + + expect(setting.dsa_key_restriction).to eq(0) + + migrate! + + expect(application_settings.count).to eq(1) + expect(setting.reload.dsa_key_restriction).to eq(-1) + end +end diff --git a/spec/migrations/migrate_object_storage_upload_sidekiq_queue_spec.rb b/spec/migrations/migrate_object_storage_upload_sidekiq_queue_spec.rb new file mode 100644 index 00000000000..1ee6c440cf4 --- /dev/null +++ b/spec/migrations/migrate_object_storage_upload_sidekiq_queue_spec.rb @@ -0,0 +1,33 @@ +require 'spec_helper' +require Rails.root.join('db', 'post_migrate', '20180603190921_migrate_object_storage_upload_sidekiq_queue.rb') + +describe MigrateObjectStorageUploadSidekiqQueue, :sidekiq, :redis do + include Gitlab::Database::MigrationHelpers + + context 'when there are jobs in the queue' do + it 'correctly migrates queue when migrating up' do + Sidekiq::Testing.disable! do + stubbed_worker(queue: 'object_storage_upload').perform_async('Something', [1]) + stubbed_worker(queue: 'object_storage:object_storage_background_move').perform_async('Something', [1]) + + described_class.new.up + + expect(sidekiq_queue_length('object_storage_upload')).to eq 0 + expect(sidekiq_queue_length('object_storage:object_storage_background_move')).to eq 2 + end + end + end + + context 'when there are no jobs in the queues' do + it 'does not raise error when migrating up' do + expect { described_class.new.up }.not_to raise_error + end + end + + def stubbed_worker(queue:) + Class.new do + include Sidekiq::Worker + sidekiq_options queue: queue + end + end +end diff --git a/spec/migrations/migrate_process_commit_worker_jobs_spec.rb b/spec/migrations/migrate_process_commit_worker_jobs_spec.rb index 4ee1d255fbd..ac34efa4f9d 100644 --- a/spec/migrations/migrate_process_commit_worker_jobs_spec.rb +++ b/spec/migrations/migrate_process_commit_worker_jobs_spec.rb @@ -6,7 +6,11 @@ require Rails.root.join('db', 'migrate', '20161124141322_migrate_process_commit_ describe MigrateProcessCommitWorkerJobs do let(:project) { create(:project, :legacy_storage, :repository) } # rubocop:disable RSpec/FactoriesInMigrationSpecs let(:user) { create(:user) } # rubocop:disable RSpec/FactoriesInMigrationSpecs - let(:commit) { project.commit.raw.rugged_commit } + let(:commit) do + Gitlab::GitalyClient::StorageSettings.allow_disk_access do + project.commit.raw.rugged_commit + end + end describe 'Project' do describe 'find_including_path' do diff --git a/spec/migrations/schedule_to_archive_legacy_traces_spec.rb b/spec/migrations/schedule_to_archive_legacy_traces_spec.rb new file mode 100644 index 00000000000..d3eac3c45ea --- /dev/null +++ b/spec/migrations/schedule_to_archive_legacy_traces_spec.rb @@ -0,0 +1,45 @@ +require 'spec_helper' +require Rails.root.join('db', 'post_migrate', '20180529152628_schedule_to_archive_legacy_traces') + +describe ScheduleToArchiveLegacyTraces, :migration do + include TraceHelpers + + let(:namespaces) { table(:namespaces) } + let(:projects) { table(:projects) } + let(:builds) { table(:ci_builds) } + let(:job_artifacts) { table(:ci_job_artifacts) } + + before do + namespaces.create!(id: 123, name: 'gitlab1', path: 'gitlab1') + projects.create!(id: 123, name: 'gitlab1', path: 'gitlab1', namespace_id: 123) + @build_success = builds.create!(id: 1, project_id: 123, status: 'success', type: 'Ci::Build') + @build_failed = builds.create!(id: 2, project_id: 123, status: 'failed', type: 'Ci::Build') + @builds_canceled = builds.create!(id: 3, project_id: 123, status: 'canceled', type: 'Ci::Build') + @build_running = builds.create!(id: 4, project_id: 123, status: 'running', type: 'Ci::Build') + + create_legacy_trace(@build_success, 'This job is done') + create_legacy_trace(@build_failed, 'This job is done') + create_legacy_trace(@builds_canceled, 'This job is done') + create_legacy_trace(@build_running, 'This job is not done yet') + end + + it 'correctly archive legacy traces' do + expect(job_artifacts.count).to eq(0) + expect(File.exist?(legacy_trace_path(@build_success))).to be_truthy + expect(File.exist?(legacy_trace_path(@build_failed))).to be_truthy + expect(File.exist?(legacy_trace_path(@builds_canceled))).to be_truthy + expect(File.exist?(legacy_trace_path(@build_running))).to be_truthy + + migrate! + + expect(job_artifacts.count).to eq(3) + expect(File.exist?(legacy_trace_path(@build_success))).to be_falsy + expect(File.exist?(legacy_trace_path(@build_failed))).to be_falsy + expect(File.exist?(legacy_trace_path(@builds_canceled))).to be_falsy + expect(File.exist?(legacy_trace_path(@build_running))).to be_truthy + expect(File.exist?(archived_trace_path(job_artifacts.where(job_id: @build_success.id).first))).to be_truthy + expect(File.exist?(archived_trace_path(job_artifacts.where(job_id: @build_failed.id).first))).to be_truthy + expect(File.exist?(archived_trace_path(job_artifacts.where(job_id: @builds_canceled.id).first))).to be_truthy + expect(job_artifacts.where(job_id: @build_running.id)).not_to be_exist + end +end diff --git a/spec/migrations/turn_nested_groups_into_regular_groups_for_mysql_spec.rb b/spec/migrations/turn_nested_groups_into_regular_groups_for_mysql_spec.rb index 560409f08de..5f5ba426d69 100644 --- a/spec/migrations/turn_nested_groups_into_regular_groups_for_mysql_spec.rb +++ b/spec/migrations/turn_nested_groups_into_regular_groups_for_mysql_spec.rb @@ -49,10 +49,14 @@ describe TurnNestedGroupsIntoRegularGroupsForMysql do end it 'renames the repository of any projects' do - expect(updated_project.repository.path) + repo_path = Gitlab::GitalyClient::StorageSettings.allow_disk_access do + updated_project.repository.path + end + + expect(repo_path) .to end_with("#{parent_group.name}-#{child_group.name}/#{updated_project.path}.git") - expect(File.directory?(updated_project.repository.path)).to eq(true) + expect(File.directory?(repo_path)).to eq(true) end it 'creates a redirect route for renamed projects' do diff --git a/spec/models/application_setting/term_spec.rb b/spec/models/application_setting/term_spec.rb index 1eddf3c56ff..aa49594f4d1 100644 --- a/spec/models/application_setting/term_spec.rb +++ b/spec/models/application_setting/term_spec.rb @@ -12,4 +12,41 @@ describe ApplicationSetting::Term do expect(described_class.latest).to eq(terms) end end + + describe '#accepted_by_user?' do + let(:user) { create(:user) } + let(:term) { create(:term) } + + it 'is true when the user accepted the terms' do + accept_terms(term, user) + + expect(term.accepted_by_user?(user)).to be(true) + end + + it 'is false when the user declined the terms' do + decline_terms(term, user) + + expect(term.accepted_by_user?(user)).to be(false) + end + + it 'does not cause a query when the user accepted the current terms' do + accept_terms(term, user) + + expect { term.accepted_by_user?(user) }.not_to exceed_query_limit(0) + end + + it 'returns false if the currently accepted terms are different' do + accept_terms(create(:term), user) + + expect(term.accepted_by_user?(user)).to be(false) + end + + def accept_terms(term, user) + Users::RespondToTermsService.new(user, term).execute(accepted: true) + end + + def decline_terms(term, user) + Users::RespondToTermsService.new(user, term).execute(accepted: false) + end + end end diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb index 968267a6d24..3e6656e0f12 100644 --- a/spec/models/application_setting_spec.rb +++ b/spec/models/application_setting_spec.rb @@ -110,10 +110,9 @@ describe ApplicationSetting do # Upgraded databases will have this sort of content context 'repository_storages is a String, not an Array' do before do - setting.__send__(:raw_write_attribute, :repository_storages, 'default') + described_class.where(id: setting.id).update_all(repository_storages: 'default') end - it { expect(setting.repository_storages_before_type_cast).to eq('default') } it { expect(setting.repository_storages).to eq(['default']) } end diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index b5eac913639..51b9b518117 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -116,6 +116,26 @@ describe Ci::Build do end end + describe '.with_live_trace' do + subject { described_class.with_live_trace } + + context 'when build has live trace' do + let!(:build) { create(:ci_build, :success, :trace_live) } + + it 'selects the build' do + is_expected.to eq([build]) + end + end + + context 'when build does not have live trace' do + let!(:build) { create(:ci_build, :success, :trace_artifact) } + + it 'does not select the build' do + is_expected.to be_empty + end + end + end + describe '#actionize' do context 'when build is a created' do before do @@ -1528,7 +1548,9 @@ describe Ci::Build do let(:predefined_variables) do [ { key: 'CI_PIPELINE_ID', value: pipeline.id.to_s, public: true }, + { key: 'CI_PIPELINE_URL', value: project.web_url + "/pipelines/#{pipeline.id}", public: true }, { key: 'CI_JOB_ID', value: build.id.to_s, public: true }, + { key: 'CI_JOB_URL', value: project.web_url + "/-/jobs/#{build.id}", public: true }, { key: 'CI_JOB_TOKEN', value: build.token, public: false }, { key: 'CI_BUILD_ID', value: build.id.to_s, public: true }, { key: 'CI_BUILD_TOKEN', value: build.token, public: false }, @@ -1559,6 +1581,7 @@ describe Ci::Build do { key: 'CI_PROJECT_NAMESPACE', value: project.namespace.full_path, public: true }, { key: 'CI_PROJECT_URL', value: project.web_url, public: true }, { key: 'CI_PROJECT_VISIBILITY', value: 'private', public: true }, + { key: 'CI_PIPELINE_IID', value: pipeline.iid.to_s, public: true }, { key: 'CI_CONFIG_PATH', value: pipeline.ci_yaml_file_path, public: true }, { key: 'CI_PIPELINE_SOURCE', value: pipeline.source, public: true }, { key: 'CI_COMMIT_MESSAGE', value: pipeline.git_commit_message, public: true }, @@ -2150,6 +2173,7 @@ describe Ci::Build do it 'does not return prohibited variables' do keys = %w[CI_JOB_ID + CI_JOB_URL CI_JOB_TOKEN CI_BUILD_ID CI_BUILD_TOKEN @@ -2505,4 +2529,76 @@ describe Ci::Build do end end end + + describe 'pages deployments' do + set(:build) { create(:ci_build, project: project, user: user) } + + context 'when job is "pages"' do + before do + build.name = 'pages' + end + + context 'when pages are enabled' do + before do + allow(Gitlab.config.pages).to receive_messages(enabled: true) + end + + it 'is marked as pages generator' do + expect(build).to be_pages_generator + end + + context 'job succeeds' do + it "calls pages worker" do + expect(PagesWorker).to receive(:perform_async).with(:deploy, build.id) + + build.success! + end + end + + context 'job fails' do + it "does not call pages worker" do + expect(PagesWorker).not_to receive(:perform_async) + + build.drop! + end + end + end + + context 'when pages are disabled' do + before do + allow(Gitlab.config.pages).to receive_messages(enabled: false) + end + + it 'is not marked as pages generator' do + expect(build).not_to be_pages_generator + end + + context 'job succeeds' do + it "does not call pages worker" do + expect(PagesWorker).not_to receive(:perform_async) + + build.success! + end + end + end + end + + context 'when job is not "pages"' do + before do + build.name = 'other-job' + end + + it 'is not marked as pages generator' do + expect(build).not_to be_pages_generator + end + + context 'job succeeds' do + it "does not call pages worker" do + expect(PagesWorker).not_to receive(:perform_async) + + build.success + end + end + end + end end diff --git a/spec/models/ci/build_trace_chunk_spec.rb b/spec/models/ci/build_trace_chunk_spec.rb index cbcf1e55979..b5a6d959ccb 100644 --- a/spec/models/ci/build_trace_chunk_spec.rb +++ b/spec/models/ci/build_trace_chunk_spec.rb @@ -54,14 +54,6 @@ describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do it { is_expected.to eq('Sample data in db') } end - - context 'when data_store is others' do - before do - build_trace_chunk.send(:write_attribute, :data_store, -1) - end - - it { expect { subject }.to raise_error('Unsupported data store') } - end end describe '#set_data' do diff --git a/spec/models/ci/group_spec.rb b/spec/models/ci/group_spec.rb index 51123e73fe6..838fa63cb1f 100644 --- a/spec/models/ci/group_spec.rb +++ b/spec/models/ci/group_spec.rb @@ -41,4 +41,55 @@ describe Ci::Group do end end end + + describe '.fabricate' do + let(:pipeline) { create(:ci_empty_pipeline) } + let(:stage) { create(:ci_stage_entity, pipeline: pipeline) } + + before do + create_build(:ci_build, name: 'rspec 0 2') + create_build(:ci_build, name: 'rspec 0 1') + create_build(:ci_build, name: 'spinach 0 1') + create_build(:commit_status, name: 'aaaaa') + end + + it 'returns an array of three groups' do + expect(stage.groups).to be_a Array + expect(stage.groups).to all(be_a described_class) + expect(stage.groups.size).to eq 3 + end + + it 'returns groups with correctly ordered statuses' do + expect(stage.groups.first.jobs.map(&:name)) + .to eq ['aaaaa'] + expect(stage.groups.second.jobs.map(&:name)) + .to eq ['rspec 0 1', 'rspec 0 2'] + expect(stage.groups.third.jobs.map(&:name)) + .to eq ['spinach 0 1'] + end + + it 'returns groups with correct names' do + expect(stage.groups.map(&:name)) + .to eq %w[aaaaa rspec spinach] + end + + context 'when a name is nil on legacy pipelines' do + before do + pipeline.builds.first.update_attribute(:name, nil) + end + + it 'returns an array of three groups' do + expect(stage.groups.map(&:name)) + .to eq ['', 'aaaaa', 'rspec', 'spinach'] + end + end + + def create_build(type, status: 'success', **opts) + create(type, pipeline: pipeline, + stage: stage.name, + status: status, + stage_id: stage.id, + **opts) + end + end end diff --git a/spec/models/ci/job_artifact_spec.rb b/spec/models/ci/job_artifact_spec.rb index a3e119cbc27..efddd4e7662 100644 --- a/spec/models/ci/job_artifact_spec.rb +++ b/spec/models/ci/job_artifact_spec.rb @@ -76,12 +76,12 @@ describe Ci::JobArtifact do context 'updating the artifact file' do it 'updates the artifact size' do - artifact.update!(file: fixture_file_upload(File.join(Rails.root, 'spec/fixtures/dk.png'))) + artifact.update!(file: fixture_file_upload('spec/fixtures/dk.png')) expect(artifact.size).to eq(1062) end it 'updates the project statistics' do - expect { artifact.update!(file: fixture_file_upload(File.join(Rails.root, 'spec/fixtures/dk.png'))) } + expect { artifact.update!(file: fixture_file_upload('spec/fixtures/dk.png')) } .to change { artifact.project.statistics.reload.build_artifacts_size } .by(1062 - 106365) end diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index f5295bec65b..a41657b53b7 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -35,6 +35,16 @@ describe Ci::Pipeline, :mailer do end end + describe 'modules' do + it_behaves_like 'AtomicInternalId', validate_presence: false do + let(:internal_id_attribute) { :iid } + let(:instance) { build(:ci_pipeline) } + let(:scope) { :project } + let(:scope_attrs) { { project: instance.project } } + let(:usage) { :ci_pipelines } + end + end + describe '#source' do context 'when creating new pipeline' do let(:pipeline) do @@ -184,7 +194,7 @@ describe Ci::Pipeline, :mailer do it 'does contains persisted variables' do keys = subject.map { |variable| variable[:key] } - expect(keys).to eq %w[CI_PIPELINE_ID] + expect(keys).to eq %w[CI_PIPELINE_ID CI_PIPELINE_URL] end end end @@ -195,7 +205,8 @@ describe Ci::Pipeline, :mailer do it 'includes all predefined variables in a valid order' do keys = subject.map { |variable| variable[:key] } - expect(keys).to eq %w[CI_CONFIG_PATH + expect(keys).to eq %w[CI_PIPELINE_IID + CI_CONFIG_PATH CI_PIPELINE_SOURCE CI_COMMIT_MESSAGE CI_COMMIT_TITLE @@ -526,6 +537,87 @@ describe Ci::Pipeline, :mailer do end end end + + describe '#stages' do + before do + create(:ci_stage_entity, project: project, + pipeline: pipeline, + name: 'build') + end + + it 'returns persisted stages' do + expect(pipeline.stages).not_to be_empty + expect(pipeline.stages).to all(be_persisted) + end + end + + describe '#ordered_stages' do + before do + create(:ci_stage_entity, project: project, + pipeline: pipeline, + position: 4, + name: 'deploy') + + create(:ci_build, project: project, + pipeline: pipeline, + stage: 'test', + stage_idx: 3, + name: 'test') + + create(:ci_build, project: project, + pipeline: pipeline, + stage: 'build', + stage_idx: 2, + name: 'build') + + create(:ci_stage_entity, project: project, + pipeline: pipeline, + position: 1, + name: 'sanity') + + create(:ci_stage_entity, project: project, + pipeline: pipeline, + position: 5, + name: 'cleanup') + end + + subject { pipeline.ordered_stages } + + context 'when using legacy stages' do + before do + stub_feature_flags(ci_pipeline_persisted_stages: false) + end + + it 'returns legacy stages in valid order' do + expect(subject.map(&:name)).to eq %w[build test] + end + end + + context 'when using persisted stages' do + before do + stub_feature_flags(ci_pipeline_persisted_stages: true) + end + + context 'when pipelines is not complete' do + it 'still returns legacy stages' do + expect(subject).to all(be_a Ci::LegacyStage) + expect(subject.map(&:name)).to eq %w[build test] + end + end + + context 'when pipeline is complete' do + before do + pipeline.succeed! + end + + it 'returns stages in valid order' do + expect(subject).to all(be_a Ci::Stage) + expect(subject.map(&:name)) + .to eq %w[sanity build test deploy cleanup] + end + end + end + end end describe 'state machine' do @@ -1170,6 +1262,43 @@ describe Ci::Pipeline, :mailer do end end + describe '#update_status' do + context 'when pipeline is empty' do + it 'updates does not change pipeline status' do + expect(pipeline.statuses.latest.status).to be_nil + + expect { pipeline.update_status } + .to change { pipeline.reload.status }.to 'skipped' + end + end + + context 'when updating status to pending' do + before do + allow(pipeline) + .to receive_message_chain(:statuses, :latest, :status) + .and_return(:running) + end + + it 'updates pipeline status to running' do + expect { pipeline.update_status } + .to change { pipeline.reload.status }.to 'running' + end + end + + context 'when statuses status was not recognized' do + before do + allow(pipeline) + .to receive(:latest_builds_status) + .and_return(:unknown) + end + + it 'raises an exception' do + expect { pipeline.update_status } + .to raise_error(HasStatus::UnknownStatusError) + end + end + end + describe '#detailed_status' do subject { pipeline.detailed_status(user) } diff --git a/spec/models/ci/runner_spec.rb b/spec/models/ci/runner_spec.rb index 0f072aa1719..f6433234573 100644 --- a/spec/models/ci/runner_spec.rb +++ b/spec/models/ci/runner_spec.rb @@ -549,7 +549,7 @@ describe Ci::Runner do end describe '#update_cached_info' do - let(:runner) { create(:ci_runner) } + let(:runner) { create(:ci_runner, :project) } subject { runner.update_cached_info(architecture: '18-bit') } @@ -570,17 +570,22 @@ describe Ci::Runner do runner.contacted_at = 2.hours.ago end - it 'updates database' do - expect_redis_update + context 'with invalid runner' do + before do + runner.projects = [] + end + + it 'still updates redis cache and database' do + expect(runner).to be_invalid - expect { subject }.to change { runner.reload.read_attribute(:contacted_at) } - .and change { runner.reload.read_attribute(:architecture) } + expect_redis_update + does_db_update + end end - it 'updates cache' do + it 'updates redis cache and database' do expect_redis_update - - subject + does_db_update end end @@ -590,6 +595,11 @@ describe Ci::Runner do expect(redis).to receive(:set).with(redis_key, anything, any_args) end end + + def does_db_update + expect { subject }.to change { runner.reload.read_attribute(:contacted_at) } + .and change { runner.reload.read_attribute(:architecture) } + end end describe '#destroy' do diff --git a/spec/models/ci/stage_spec.rb b/spec/models/ci/stage_spec.rb index a00db1d2bfc..22a4556c10c 100644 --- a/spec/models/ci/stage_spec.rb +++ b/spec/models/ci/stage_spec.rb @@ -65,7 +65,31 @@ describe Ci::Stage, :models do end end - context 'when stage is skipped' do + context 'when stage has only created builds' do + let(:stage) { create(:ci_stage_entity, status: :created) } + + before do + create(:ci_build, :created, stage_id: stage.id) + end + + it 'updates status to skipped' do + expect(stage.reload.status).to eq 'created' + end + end + + context 'when stage is skipped because of skipped builds' do + before do + create(:ci_build, :skipped, stage_id: stage.id) + end + + it 'updates status to skipped' do + expect { stage.update_status } + .to change { stage.reload.status } + .to 'skipped' + end + end + + context 'when stage is skipped because is empty' do it 'updates status to skipped' do expect { stage.update_status } .to change { stage.reload.status } @@ -86,9 +110,85 @@ describe Ci::Stage, :models do expect(stage.reload).to be_failed end end + + context 'when statuses status was not recognized' do + before do + allow(stage) + .to receive_message_chain(:statuses, :latest, :status) + .and_return(:unknown) + end + + it 'raises an exception' do + expect { stage.update_status } + .to raise_error(HasStatus::UnknownStatusError) + end + end + end + + describe '#detailed_status' do + using RSpec::Parameterized::TableSyntax + + let(:user) { create(:user) } + let(:stage) { create(:ci_stage_entity, status: :created) } + subject { stage.detailed_status(user) } + + where(:statuses, :label) do + %w[created] | :created + %w[success] | :passed + %w[pending] | :pending + %w[skipped] | :skipped + %w[canceled] | :canceled + %w[success failed] | :failed + %w[running pending] | :running + end + + with_them do + before do + statuses.each do |status| + create(:commit_status, project: stage.project, + pipeline: stage.pipeline, + stage_id: stage.id, + status: status) + + stage.update_status + end + end + + it 'has a correct label' do + expect(subject.label).to eq label.to_s + end + end + + context 'when stage has warnings' do + before do + create(:ci_build, project: stage.project, + pipeline: stage.pipeline, + stage_id: stage.id, + status: :failed, + allow_failure: true) + + stage.update_status + end + + it 'is passed with warnings' do + expect(subject.label).to eq 'passed with warnings' + end + end end - describe '#index' do + describe '#groups' do + before do + create(:ci_build, stage_id: stage.id, name: 'rspec 0 1') + create(:ci_build, stage_id: stage.id, name: 'rspec 0 2') + end + + it 'groups stage builds by name' do + expect(stage.groups).to be_one + expect(stage.groups.first.name).to eq 'rspec' + end + end + + describe '#position' do context 'when stage has been imported and does not have position index set' do before do stage.update_column(:position, nil) @@ -119,4 +219,42 @@ describe Ci::Stage, :models do end end end + + context 'when stage has warnings' do + before do + create(:ci_build, :failed, :allowed_to_fail, stage_id: stage.id) + end + + describe '#has_warnings?' do + it 'returns true' do + expect(stage).to have_warnings + end + end + + describe '#number_of_warnings' do + it 'returns a lazy stage warnings counter' do + lazy_queries = ActiveRecord::QueryRecorder.new do + stage.number_of_warnings + end + + synced_queries = ActiveRecord::QueryRecorder.new do + stage.number_of_warnings.to_i + end + + expect(lazy_queries.count).to eq 0 + expect(synced_queries.count).to eq 1 + + expect(stage.number_of_warnings.inspect).to include 'BatchLoader' + expect(stage.number_of_warnings).to eq 1 + end + end + end + + context 'when stage does not have warnings' do + describe '#has_warnings?' do + it 'returns false' do + expect(stage).not_to have_warnings + end + end + end end diff --git a/spec/models/concerns/cache_markdown_field_spec.rb b/spec/models/concerns/cache_markdown_field_spec.rb index b3797c1fb46..2d75422ee68 100644 --- a/spec/models/concerns/cache_markdown_field_spec.rb +++ b/spec/models/concerns/cache_markdown_field_spec.rb @@ -156,7 +156,7 @@ describe CacheMarkdownField do end it { expect(thing.foo_html).to eq(updated_html) } - it { expect(thing.cached_markdown_version).to eq(CacheMarkdownField::CACHE_REDCARPET_VERSION) } + it { expect(thing.cached_markdown_version).to eq(CacheMarkdownField::CACHE_COMMONMARK_VERSION) } end describe '#cached_html_up_to_date?' do @@ -234,7 +234,7 @@ describe CacheMarkdownField do it 'returns default version when version is nil' do thing.cached_markdown_version = nil - is_expected.to eq(CacheMarkdownField::CACHE_REDCARPET_VERSION) + is_expected.to eq(CacheMarkdownField::CACHE_COMMONMARK_VERSION) end end @@ -261,7 +261,7 @@ describe CacheMarkdownField do thing.cached_markdown_version = nil thing.refresh_markdown_cache - expect(thing.cached_markdown_version).to eq(CacheMarkdownField::CACHE_REDCARPET_VERSION) + expect(thing.cached_markdown_version).to eq(CacheMarkdownField::CACHE_COMMONMARK_VERSION) end end @@ -346,7 +346,7 @@ describe CacheMarkdownField do expect(thing.foo_html).to eq(updated_html) expect(thing.baz_html).to eq(updated_html) - expect(thing.cached_markdown_version).to eq(CacheMarkdownField::CACHE_REDCARPET_VERSION) + expect(thing.cached_markdown_version).to eq(CacheMarkdownField::CACHE_COMMONMARK_VERSION) end end @@ -366,7 +366,7 @@ describe CacheMarkdownField do expect(thing.foo_html).to eq(updated_html) expect(thing.baz_html).to eq(updated_html) - expect(thing.cached_markdown_version).to eq(CacheMarkdownField::CACHE_REDCARPET_VERSION) + expect(thing.cached_markdown_version).to eq(CacheMarkdownField::CACHE_COMMONMARK_VERSION) end end end diff --git a/spec/models/deployment_spec.rb b/spec/models/deployment_spec.rb index aee70bcfb29..e01906f4b6c 100644 --- a/spec/models/deployment_spec.rb +++ b/spec/models/deployment_spec.rb @@ -20,6 +20,7 @@ describe Deployment do it_behaves_like 'AtomicInternalId' do let(:internal_id_attribute) { :iid } let(:instance) { build(:deployment) } + let(:scope) { :project } let(:scope_attrs) { { project: instance.project } } let(:usage) { :deployments } end diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb index f83b52e8975..9fe1186a8c9 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -67,6 +67,30 @@ describe Group do end end + describe '#notification_settings', :nested_groups do + let(:user) { create(:user) } + let(:group) { create(:group) } + let(:sub_group) { create(:group, parent_id: group.id) } + + before do + group.add_developer(user) + sub_group.add_developer(user) + end + + it 'also gets notification settings from parent groups' do + expect(sub_group.notification_settings.size).to eq(2) + expect(sub_group.notification_settings).to include(group.notification_settings.first) + end + + context 'when sub group is deleted' do + it 'does not delete parent notification settings' do + expect do + sub_group.destroy + end.to change { NotificationSetting.count }.by(-1) + end + end + end + describe '#visibility_level_allowed_by_parent' do let(:parent) { create(:group, :internal) } let(:sub_group) { build(:group, parent_id: parent.id) } @@ -240,7 +264,7 @@ describe Group do it "is false if avatar is html page" do group.update_attribute(:avatar, 'uploads/avatar.html') - expect(group.avatar_type).to eq(["file format is not supported. Please try one of the following supported formats: png, jpg, jpeg, gif, bmp, tiff"]) + expect(group.avatar_type).to eq(["file format is not supported. Please try one of the following supported formats: png, jpg, jpeg, gif, bmp, tiff, ico"]) end end diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb index 128acf83686..e818fbeb9cf 100644 --- a/spec/models/issue_spec.rb +++ b/spec/models/issue_spec.rb @@ -17,6 +17,7 @@ describe Issue do it_behaves_like 'AtomicInternalId' do let(:internal_id_attribute) { :iid } let(:instance) { build(:issue) } + let(:scope) { :project } let(:scope_attrs) { { project: instance.project } } let(:usage) { :issues } end diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 9ffa91fc265..3f028b3bd5c 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -16,7 +16,11 @@ describe MergeRequest do describe '#squash_in_progress?' do shared_examples 'checking whether a squash is in progress' do - let(:repo_path) { subject.source_project.repository.path } + let(:repo_path) do + Gitlab::GitalyClient::StorageSettings.allow_disk_access do + subject.source_project.repository.path + end + end let(:squash_path) { File.join(repo_path, "gitlab-worktree", "squash-#{subject.id}") } before do @@ -84,6 +88,7 @@ describe MergeRequest do it_behaves_like 'AtomicInternalId' do let(:internal_id_attribute) { :iid } let(:instance) { build(:merge_request) } + let(:scope) { :target_project } let(:scope_attrs) { { project: instance.target_project } } let(:usage) { :merge_requests } end @@ -2196,7 +2201,11 @@ describe MergeRequest do describe '#rebase_in_progress?' do shared_examples 'checking whether a rebase is in progress' do - let(:repo_path) { subject.source_project.repository.path } + let(:repo_path) do + Gitlab::GitalyClient::StorageSettings.allow_disk_access do + subject.source_project.repository.path + end + end let(:rebase_path) { File.join(repo_path, "gitlab-worktree", "rebase-#{subject.id}") } before do @@ -2236,25 +2245,25 @@ describe MergeRequest do end end - describe '#allow_maintainer_to_push' do + describe '#allow_collaboration' do let(:merge_request) do - build(:merge_request, source_branch: 'fixes', allow_maintainer_to_push: true) + build(:merge_request, source_branch: 'fixes', allow_collaboration: true) end it 'is false when pushing by a maintainer is not possible' do - expect(merge_request).to receive(:maintainer_push_possible?) { false } + expect(merge_request).to receive(:collaborative_push_possible?) { false } - expect(merge_request.allow_maintainer_to_push).to be_falsy + expect(merge_request.allow_collaboration).to be_falsy end it 'is true when pushing by a maintainer is possible' do - expect(merge_request).to receive(:maintainer_push_possible?) { true } + expect(merge_request).to receive(:collaborative_push_possible?) { true } - expect(merge_request.allow_maintainer_to_push).to be_truthy + expect(merge_request.allow_collaboration).to be_truthy end end - describe '#maintainer_push_possible?' do + describe '#collaborative_push_possible?' do let(:merge_request) do build(:merge_request, source_branch: 'fixes') end @@ -2266,14 +2275,14 @@ describe MergeRequest do it 'does not allow maintainer to push if the source project is the same as the target' do merge_request.target_project = merge_request.source_project = create(:project, :public) - expect(merge_request.maintainer_push_possible?).to be_falsy + expect(merge_request.collaborative_push_possible?).to be_falsy end it 'allows maintainer to push when both source and target are public' do merge_request.target_project = build(:project, :public) merge_request.source_project = build(:project, :public) - expect(merge_request.maintainer_push_possible?).to be_truthy + expect(merge_request.collaborative_push_possible?).to be_truthy end it 'is not available for protected branches' do @@ -2284,11 +2293,11 @@ describe MergeRequest do .with(merge_request.source_project, 'fixes') .and_return(true) - expect(merge_request.maintainer_push_possible?).to be_falsy + expect(merge_request.collaborative_push_possible?).to be_falsy end end - describe '#can_allow_maintainer_to_push?' do + describe '#can_allow_collaboration?' do let(:target_project) { create(:project, :public) } let(:source_project) { fork_project(target_project) } let(:merge_request) do @@ -2300,17 +2309,17 @@ describe MergeRequest do let(:user) { create(:user) } before do - allow(merge_request).to receive(:maintainer_push_possible?) { true } + allow(merge_request).to receive(:collaborative_push_possible?) { true } end it 'is false if the user does not have push access to the source project' do - expect(merge_request.can_allow_maintainer_to_push?(user)).to be_falsy + expect(merge_request.can_allow_collaboration?(user)).to be_falsy end it 'is true when the user has push access to the source project' do source_project.add_developer(user) - expect(merge_request.can_allow_maintainer_to_push?(user)).to be_truthy + expect(merge_request.can_allow_collaboration?(user)).to be_truthy end end diff --git a/spec/models/milestone_spec.rb b/spec/models/milestone_spec.rb index 4bb9717d33e..204d6b47832 100644 --- a/spec/models/milestone_spec.rb +++ b/spec/models/milestone_spec.rb @@ -6,6 +6,7 @@ describe Milestone do it_behaves_like 'AtomicInternalId' do let(:internal_id_attribute) { :iid } let(:instance) { build(:milestone, project: build(:project), group: nil) } + let(:scope) { :project } let(:scope_attrs) { { project: instance.project } } let(:usage) { :milestones } end @@ -15,6 +16,7 @@ describe Milestone do it_behaves_like 'AtomicInternalId' do let(:internal_id_attribute) { :iid } let(:instance) { build(:milestone, project: nil, group: build(:group)) } + let(:scope) { :group } let(:scope_attrs) { { namespace: instance.group } } let(:usage) { :milestones } end diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb index 6f702d8d95e..18b01c3e6b7 100644 --- a/spec/models/namespace_spec.rb +++ b/spec/models/namespace_spec.rb @@ -301,12 +301,18 @@ describe Namespace do end def project_rugged(project) - project.repository.rugged + Gitlab::GitalyClient::StorageSettings.allow_disk_access do + project.repository.rugged + end end end describe '#rm_dir', 'callback' do - let(:repository_storage_path) { Gitlab.config.repositories.storages.default.legacy_disk_path } + let(:repository_storage_path) do + Gitlab::GitalyClient::StorageSettings.allow_disk_access do + Gitlab.config.repositories.storages.default.legacy_disk_path + end + end let(:path_in_dir) { File.join(repository_storage_path, namespace.full_path) } let(:deleted_path) { namespace.full_path.gsub(namespace.path, "#{namespace.full_path}+#{namespace.id}+deleted") } let(:deleted_path_in_dir) { File.join(repository_storage_path, deleted_path) } diff --git a/spec/models/notification_recipient_spec.rb b/spec/models/notification_recipient_spec.rb index eda0e1da835..13fe47799ed 100644 --- a/spec/models/notification_recipient_spec.rb +++ b/spec/models/notification_recipient_spec.rb @@ -13,4 +13,48 @@ describe NotificationRecipient do expect(recipient.has_access?).to be_falsy end + + context '#notification_setting' do + context 'for child groups', :nested_groups do + let!(:moved_group) { create(:group) } + let(:group) { create(:group) } + let(:sub_group_1) { create(:group, parent: group) } + let(:sub_group_2) { create(:group, parent: sub_group_1) } + let(:project) { create(:project, namespace: moved_group) } + + before do + sub_group_2.add_owner(user) + moved_group.add_owner(user) + Groups::TransferService.new(moved_group, user).execute(sub_group_2) + + moved_group.reload + end + + context 'when notification setting is global' do + before do + user.notification_settings_for(group).global! + user.notification_settings_for(sub_group_1).mention! + user.notification_settings_for(sub_group_2).global! + user.notification_settings_for(moved_group).global! + end + + it 'considers notification setting from the first parent without global setting' do + expect(subject.notification_setting.source).to eq(sub_group_1) + end + end + + context 'when notification setting is not global' do + before do + user.notification_settings_for(group).global! + user.notification_settings_for(sub_group_1).mention! + user.notification_settings_for(sub_group_2).watch! + user.notification_settings_for(moved_group).disabled! + end + + it 'considers notification setting from lowest group member in hierarchy' do + expect(subject.notification_setting.source).to eq(moved_group) + end + end + end + end end diff --git a/spec/models/project_auto_devops_spec.rb b/spec/models/project_auto_devops_spec.rb index 7545c0797e9..749b2094787 100644 --- a/spec/models/project_auto_devops_spec.rb +++ b/spec/models/project_auto_devops_spec.rb @@ -5,6 +5,8 @@ describe ProjectAutoDevops do it { is_expected.to belong_to(:project) } + it { is_expected.to define_enum_for(:deploy_strategy) } + it { is_expected.to respond_to(:created_at) } it { is_expected.to respond_to(:updated_at) } @@ -67,8 +69,127 @@ describe ProjectAutoDevops do end end + context 'when deploy_strategy is manual' do + let(:domain) { 'example.com' } + + before do + auto_devops.deploy_strategy = 'manual' + end + + it do + expect(auto_devops.predefined_variables.map { |var| var[:key] }) + .to include("STAGING_ENABLED", "INCREMENTAL_ROLLOUT_ENABLED") + end + end + + context 'when deploy_strategy is continuous' do + let(:domain) { 'example.com' } + + before do + auto_devops.deploy_strategy = 'continuous' + end + + it do + expect(auto_devops.predefined_variables.map { |var| var[:key] }) + .not_to include("STAGING_ENABLED", "INCREMENTAL_ROLLOUT_ENABLED") + end + end + def domain_variable { key: 'AUTO_DEVOPS_DOMAIN', value: 'example.com', public: true } end end + + describe '#set_gitlab_deploy_token' do + let(:auto_devops) { build(:project_auto_devops, project: project) } + + context 'when the project is public' do + let(:project) { create(:project, :repository, :public) } + + it 'should not create a gitlab deploy token' do + expect do + auto_devops.save + end.not_to change { DeployToken.count } + end + end + + context 'when the project is internal' do + let(:project) { create(:project, :repository, :internal) } + + it 'should create a gitlab deploy token' do + expect do + auto_devops.save + end.to change { DeployToken.count }.by(1) + end + end + + context 'when the project is private' do + let(:project) { create(:project, :repository, :private) } + + it 'should create a gitlab deploy token' do + expect do + auto_devops.save + end.to change { DeployToken.count }.by(1) + end + end + + context 'when autodevops is enabled at project level' do + let(:project) { create(:project, :repository, :internal) } + let(:auto_devops) { build(:project_auto_devops, project: project) } + + it 'should create a deploy token' do + expect do + auto_devops.save + end.to change { DeployToken.count }.by(1) + end + end + + context 'when autodevops is enabled at instancel level' do + let(:project) { create(:project, :repository, :internal) } + let(:auto_devops) { build(:project_auto_devops, :disabled, project: project) } + + it 'should create a deploy token' do + allow(Gitlab::CurrentSettings).to receive(:auto_devops_enabled?).and_return(true) + + expect do + auto_devops.save + end.to change { DeployToken.count }.by(1) + end + end + + context 'when autodevops is disabled' do + let(:project) { create(:project, :repository, :internal) } + let(:auto_devops) { build(:project_auto_devops, :disabled, project: project) } + + it 'should not create a deploy token' do + expect do + auto_devops.save + end.not_to change { DeployToken.count } + end + end + + context 'when the project already has an active gitlab-deploy-token' do + let(:project) { create(:project, :repository, :internal) } + let!(:deploy_token) { create(:deploy_token, :gitlab_deploy_token, projects: [project]) } + let(:auto_devops) { build(:project_auto_devops, project: project) } + + it 'should not create a deploy token' do + expect do + auto_devops.save + end.not_to change { DeployToken.count } + end + end + + context 'when the project already has a revoked gitlab-deploy-token' do + let(:project) { create(:project, :repository, :internal) } + let!(:deploy_token) { create(:deploy_token, :gitlab_deploy_token, :expired, projects: [project]) } + let(:auto_devops) { build(:project_auto_devops, project: project) } + + it 'should not create a deploy token' do + expect do + auto_devops.save + end.not_to change { DeployToken.count } + end + end + end end diff --git a/spec/models/project_services/jira_service_spec.rb b/spec/models/project_services/jira_service_spec.rb index 54ef0be67ff..6c637533c6b 100644 --- a/spec/models/project_services/jira_service_spec.rb +++ b/spec/models/project_services/jira_service_spec.rb @@ -2,6 +2,7 @@ require 'spec_helper' describe JiraService do include Gitlab::Routing + include AssetsHelpers describe '#options' do let(:service) do @@ -164,6 +165,8 @@ describe JiraService do it "creates Remote Link reference in JIRA for comment" do @jira_service.close_issue(merge_request, ExternalIssue.new("JIRA-123", project)) + favicon_path = "http://localhost/assets/#{find_asset('favicon.png').digest_path}" + # Creates comment expect(WebMock).to have_requested(:post, @comment_url) # Creates Remote Link in JIRA issue fields @@ -173,7 +176,7 @@ describe JiraService do object: { url: "#{Gitlab.config.gitlab.url}/#{project.full_path}/commit/#{merge_request.diff_head_sha}", title: "GitLab: Solved by commit #{merge_request.diff_head_sha}.", - icon: { title: "GitLab", url16x16: "http://localhost/favicon.ico" }, + icon: { title: "GitLab", url16x16: favicon_path }, status: { resolved: true } } ) @@ -464,4 +467,18 @@ describe JiraService do end end end + + describe 'favicon urls', :request_store do + it 'includes the standard favicon' do + props = described_class.new.send(:build_remote_link_props, url: 'http://example.com', title: 'title') + expect(props[:object][:icon][:url16x16]).to match %r{^http://localhost/assets/favicon(?:-\h+).png$} + end + + it 'includes returns the custom favicon' do + create :appearance, favicon: fixture_file_upload('spec/fixtures/dk.png') + + props = described_class.new.send(:build_remote_link_props, url: 'http://example.com', title: 'title') + expect(props[:object][:icon][:url16x16]).to match %r{^http://localhost/uploads/-/system/appearance/favicon/\d+/dk.png$} + end + end end diff --git a/spec/models/project_services/microsoft_teams_service_spec.rb b/spec/models/project_services/microsoft_teams_service_spec.rb index 8d9ee96227f..3351c6280b4 100644 --- a/spec/models/project_services/microsoft_teams_service_spec.rb +++ b/spec/models/project_services/microsoft_teams_service_spec.rb @@ -225,10 +225,15 @@ describe MicrosoftTeamsService do it 'calls Microsoft Teams API for pipeline events' do data = Gitlab::DataBuilder::Pipeline.build(pipeline) + data[:markdown] = true chat_service.execute(data) - expect(WebMock).to have_requested(:post, webhook_url).once + message = ChatMessage::PipelineMessage.new(data) + + expect(WebMock).to have_requested(:post, webhook_url) + .with(body: hash_including({ summary: message.summary })) + .once end end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 9a76452a808..bc9cce6b0c3 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -238,20 +238,27 @@ describe Project do expect(project2.import_data).to be_nil end - it "does not allow blocked import_url localhost" do + it "does not allow import_url pointing to localhost" do project2 = build(:project, import_url: 'http://localhost:9000/t.git') expect(project2).to be_invalid expect(project2.errors[:import_url].first).to include('Requests to localhost are not allowed') end - it "does not allow blocked import_url port" do + it "does not allow import_url with invalid ports" do project2 = build(:project, import_url: 'http://github.com:25/t.git') expect(project2).to be_invalid expect(project2.errors[:import_url].first).to include('Only allowed ports are 22, 80, 443') end + it "does not allow import_url with invalid user" do + project2 = build(:project, import_url: 'http://$user:password@github.com/t.git') + + expect(project2).to be_invalid + expect(project2.errors[:import_url].first).to include('Username needs to start with an alphanumeric character') + end + describe 'project pending deletion' do let!(:project_pending_deletion) do create(:project, @@ -960,7 +967,7 @@ describe Project do it 'is false if avatar is html page' do project.update_attribute(:avatar, 'uploads/avatar.html') - expect(project.avatar_type).to eq(['file format is not supported. Please try one of the following supported formats: png, jpg, jpeg, gif, bmp, tiff']) + expect(project.avatar_type).to eq(['file format is not supported. Please try one of the following supported formats: png, jpg, jpeg, gif, bmp, tiff, ico']) end end @@ -1693,6 +1700,31 @@ describe Project do end end + describe '#human_import_status_name' do + context 'when import_state exists' do + it 'returns the humanized status name' do + project = create(:project) + create(:import_state, :started, project: project) + + expect(project.human_import_status_name).to eq("started") + end + end + + context 'when import_state was not created yet' do + let(:project) { create(:project, :import_started) } + + it 'ensures import_state is created and returns humanized status name' do + expect do + project.human_import_status_name + end.to change { ProjectImportState.count }.from(0).to(1) + end + + it 'returns humanized status name' do + expect(project.human_import_status_name).to eq("started") + end + end + end + describe 'Project import job' do let(:project) { create(:project, import_url: generate(:url)) } @@ -1701,7 +1733,11 @@ describe Project do .with(project.repository_storage, project.disk_path, project.import_url) .and_return(true) - expect_any_instance_of(Repository).to receive(:after_import) + # Works around https://github.com/rspec/rspec-mocks/issues/910 + allow(described_class).to receive(:find).with(project.id).and_return(project) + expect(project.repository).to receive(:after_import) + .and_call_original + expect(project.wiki.repository).to receive(:after_import) .and_call_original end @@ -2907,7 +2943,7 @@ describe Project do project.rename_repo - expect(project.repository.rugged.config['gitlab.fullpath']).to eq(project.full_path) + expect(rugged_config['gitlab.fullpath']).to eq(project.full_path) end end @@ -3068,7 +3104,7 @@ describe Project do it 'updates project full path in .git/config' do project.rename_repo - expect(project.repository.rugged.config['gitlab.fullpath']).to eq(project.full_path) + expect(rugged_config['gitlab.fullpath']).to eq(project.full_path) end end @@ -3366,10 +3402,11 @@ describe Project do end describe '#after_import' do - let(:project) { build(:project) } + let(:project) { create(:project) } it 'runs the correct hooks' do expect(project.repository).to receive(:after_import) + expect(project.wiki.repository).to receive(:after_import) expect(project).to receive(:import_finish) expect(project).to receive(:update_project_counter_caches) expect(project).to receive(:remove_import_jid) @@ -3488,13 +3525,13 @@ describe Project do it 'writes full path in .git/config when key is missing' do project.write_repository_config - expect(project.repository.rugged.config['gitlab.fullpath']).to eq project.full_path + expect(rugged_config['gitlab.fullpath']).to eq project.full_path end it 'updates full path in .git/config when key is present' do project.write_repository_config(gl_full_path: 'old/path') - expect { project.write_repository_config }.to change { project.repository.rugged.config['gitlab.fullpath'] }.from('old/path').to(project.full_path) + expect { project.write_repository_config }.to change { rugged_config['gitlab.fullpath'] }.from('old/path').to(project.full_path) end it 'does not raise an error with an empty repository' do @@ -3583,7 +3620,7 @@ describe Project do target_branch: 'target-branch', source_project: project, source_branch: 'awesome-feature-1', - allow_maintainer_to_push: true + allow_collaboration: true ) end @@ -3620,9 +3657,14 @@ describe Project do end end - describe '#branch_allows_maintainer_push?' do + describe '#branch_allows_collaboration_push?' do it 'allows access if the user can merge the merge request' do - expect(project.branch_allows_maintainer_push?(user, 'awesome-feature-1')) + expect(project.branch_allows_collaboration?(user, 'awesome-feature-1')) + .to be_truthy + end + + it 'allows access when there are merge requests open but no branch name is given' do + expect(project.branch_allows_collaboration?(user, nil)) .to be_truthy end @@ -3630,7 +3672,7 @@ describe Project do guest = create(:user) target_project.add_guest(guest) - expect(project.branch_allows_maintainer_push?(guest, 'awesome-feature-1')) + expect(project.branch_allows_collaboration?(guest, 'awesome-feature-1')) .to be_falsy end @@ -3640,31 +3682,31 @@ describe Project do target_branch: 'target-branch', source_project: project, source_branch: 'rejected-feature-1', - allow_maintainer_to_push: true) + allow_collaboration: true) - expect(project.branch_allows_maintainer_push?(user, 'rejected-feature-1')) + expect(project.branch_allows_collaboration?(user, 'rejected-feature-1')) .to be_falsy end it 'does not allow access if the user cannot merge the merge request' do create(:protected_branch, :masters_can_push, project: target_project, name: 'target-branch') - expect(project.branch_allows_maintainer_push?(user, 'awesome-feature-1')) + expect(project.branch_allows_collaboration?(user, 'awesome-feature-1')) .to be_falsy end it 'caches the result' do - control = ActiveRecord::QueryRecorder.new { project.branch_allows_maintainer_push?(user, 'awesome-feature-1') } + control = ActiveRecord::QueryRecorder.new { project.branch_allows_collaboration?(user, 'awesome-feature-1') } - expect { 3.times { project.branch_allows_maintainer_push?(user, 'awesome-feature-1') } } + expect { 3.times { project.branch_allows_collaboration?(user, 'awesome-feature-1') } } .not_to exceed_query_limit(control) end context 'when the requeststore is active', :request_store do it 'only queries per project across instances' do - control = ActiveRecord::QueryRecorder.new { project.branch_allows_maintainer_push?(user, 'awesome-feature-1') } + control = ActiveRecord::QueryRecorder.new { project.branch_allows_collaboration?(user, 'awesome-feature-1') } - expect { 2.times { described_class.find(project.id).branch_allows_maintainer_push?(user, 'awesome-feature-1') } } + expect { 2.times { described_class.find(project.id).branch_allows_collaboration?(user, 'awesome-feature-1') } } .not_to exceed_query_limit(control).with_threshold(2) end end @@ -3775,4 +3817,10 @@ describe Project do let(:uploader_class) { AttachmentUploader } end end + + def rugged_config + Gitlab::GitalyClient::StorageSettings.allow_disk_access do + project.repository.rugged.config + end + end end diff --git a/spec/models/project_team_spec.rb b/spec/models/project_team_spec.rb index e07c522800a..9978f3e9566 100644 --- a/spec/models/project_team_spec.rb +++ b/spec/models/project_team_spec.rb @@ -179,14 +179,14 @@ describe ProjectTeam do end describe "#human_max_access" do - it 'returns Master role' do + it 'returns Maintainer role' do user = create(:user) group = create(:group) project = create(:project, namespace: group) group.add_master(user) - expect(project.team.human_max_access(user.id)).to eq 'Master' + expect(project.team.human_max_access(user.id)).to eq 'Maintainer' end it 'returns Owner role' do diff --git a/spec/models/project_wiki_spec.rb b/spec/models/project_wiki_spec.rb index f1142832f1a..a3c20b3b3c1 100644 --- a/spec/models/project_wiki_spec.rb +++ b/spec/models/project_wiki_spec.rb @@ -188,7 +188,11 @@ describe ProjectWiki do before do subject.wiki # Make sure the wiki repo exists - BareRepoOperations.new(subject.repository.path_to_repo).commit_file(image, 'image.png') + repo_path = Gitlab::GitalyClient::StorageSettings.allow_disk_access do + subject.repository.path_to_repo + end + + BareRepoOperations.new(repo_path).commit_file(image, 'image.png') end it 'returns the latest version of the file if it exists' do diff --git a/spec/models/remote_mirror_spec.rb b/spec/models/remote_mirror_spec.rb index 1d94abe4195..3597b080021 100644 --- a/spec/models/remote_mirror_spec.rb +++ b/spec/models/remote_mirror_spec.rb @@ -15,6 +15,13 @@ describe RemoteMirror do expect(remote_mirror).not_to be_valid end + + it 'does not allow url with an invalid user' do + remote_mirror = build(:remote_mirror, url: 'http://$user:password@invalid.invalid') + + expect(remote_mirror).to be_invalid + expect(remote_mirror.errors[:url].first).to include('Username needs to start with an alphanumeric character') + end end end @@ -67,7 +74,9 @@ describe RemoteMirror do mirror.update_attribute(:url, 'http://foo:baz@test.com') - config = repo.raw_repository.rugged.config + config = Gitlab::GitalyClient::StorageSettings.allow_disk_access do + repo.raw_repository.rugged.config + end expect(config["remote.#{mirror.remote_name}.url"]).to eq('http://foo:baz@test.com') end diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 0ccf55bd895..b6df048d4ca 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -136,7 +136,10 @@ describe Repository do before do options = { message: 'test tag message\n', tagger: { name: 'John Smith', email: 'john@gmail.com' } } - repository.rugged.tags.create(annotated_tag_name, 'a48e4fc218069f68ef2e769dd8dfea3991362175', options) + + Gitlab::GitalyClient::StorageSettings.allow_disk_access do + repository.rugged.tags.create(annotated_tag_name, 'a48e4fc218069f68ef2e769dd8dfea3991362175', options) + end double_first = double(committed_date: Time.now - 1.second) double_last = double(committed_date: Time.now) @@ -1048,6 +1051,13 @@ describe Repository do let(:target_project) { project } let(:target_repository) { target_project.repository } + around do |example| + # TODO Gitlab::Git::OperationService will be moved to gitaly-ruby and disappear from this repo + Gitlab::GitalyClient::StorageSettings.allow_disk_access do + example.run + end + end + context 'when pre hooks were successful' do before do service = Gitlab::Git::HooksService.new @@ -1185,7 +1195,7 @@ describe Repository do Gitlab::Git::OperationService.new(git_user, repository.raw_repository).with_branch('feature') do new_rev end - end.to raise_error(Gitlab::Git::HooksService::PreReceiveError) + end.to raise_error(Gitlab::Git::PreReceiveError) end end @@ -1309,6 +1319,13 @@ describe Repository do end describe '#update_autocrlf_option' do + around do |example| + # TODO Gitlab::Git::OperationService will be moved to gitaly-ruby and disappear from this repo + Gitlab::GitalyClient::StorageSettings.allow_disk_access do + example.run + end + end + describe 'when autocrlf is not already set to :input' do before do repository.raw_repository.autocrlf = true @@ -1802,7 +1819,9 @@ describe Repository do expect(repository.branch_count).to be_an(Integer) # NOTE: Until rugged goes away, make sure rugged and gitaly are in sync - rugged_count = repository.raw_repository.rugged.branches.count + rugged_count = Gitlab::GitalyClient::StorageSettings.allow_disk_access do + repository.raw_repository.rugged.branches.count + end expect(repository.branch_count).to eq(rugged_count) end @@ -1813,7 +1832,9 @@ describe Repository do expect(repository.tag_count).to be_an(Integer) # NOTE: Until rugged goes away, make sure rugged and gitaly are in sync - rugged_count = repository.raw_repository.rugged.tags.count + rugged_count = Gitlab::GitalyClient::StorageSettings.allow_disk_access do + repository.raw_repository.rugged.tags.count + end expect(repository.tag_count).to eq(rugged_count) end @@ -1917,13 +1938,13 @@ describe Repository do context 'when pre hooks failed' do before do allow_any_instance_of(Gitlab::GitalyClient::OperationService) - .to receive(:user_delete_branch).and_raise(Gitlab::Git::HooksService::PreReceiveError) + .to receive(:user_delete_branch).and_raise(Gitlab::Git::PreReceiveError) end it 'gets an error and does not delete the branch' do expect do repository.rm_branch(user, 'feature') - end.to raise_error(Gitlab::Git::HooksService::PreReceiveError) + end.to raise_error(Gitlab::Git::PreReceiveError) expect(repository.find_branch('feature')).not_to be_nil end @@ -1959,7 +1980,7 @@ describe Repository do expect do repository.rm_branch(user, 'feature') - end.to raise_error(Gitlab::Git::HooksService::PreReceiveError) + end.to raise_error(Gitlab::Git::PreReceiveError) end it 'does not delete the branch' do @@ -1967,7 +1988,7 @@ describe Repository do expect do repository.rm_branch(user, 'feature') - end.to raise_error(Gitlab::Git::HooksService::PreReceiveError) + end.to raise_error(Gitlab::Git::PreReceiveError) expect(repository.find_branch('feature')).not_to be_nil end end @@ -2073,7 +2094,10 @@ describe Repository do it "attempting to call keep_around on truncated ref does not fail" do repository.keep_around(sample_commit.id) ref = repository.send(:keep_around_ref_name, sample_commit.id) - path = File.join(repository.path, ref) + + path = Gitlab::GitalyClient::StorageSettings.allow_disk_access do + File.join(repository.path, ref) + end # Corrupt the reference File.truncate(path, 0) @@ -2088,6 +2112,13 @@ describe Repository do end describe '#update_ref' do + around do |example| + # TODO Gitlab::Git::OperationService will be moved to gitaly-ruby and disappear from this repo + Gitlab::GitalyClient::StorageSettings.allow_disk_access do + example.run + end + end + it 'can create a ref' do Gitlab::Git::OperationService.new(nil, repository.raw_repository).send(:update_ref, 'refs/heads/foobar', 'refs/heads/master', Gitlab::Git::BLANK_SHA) @@ -2372,7 +2403,9 @@ describe Repository do end def create_remote_branch(remote_name, branch_name, target) - rugged = repository.rugged + rugged = Gitlab::GitalyClient::StorageSettings.allow_disk_access do + repository.rugged + end rugged.references.create("refs/remotes/#{remote_name}/#{branch_name}", target.id) end @@ -2410,6 +2443,32 @@ describe Repository do end end + describe '#archive_metadata' do + let(:ref) { 'master' } + let(:storage_path) { '/tmp' } + + let(:prefix) { [project.path, ref].join('-') } + let(:filename) { prefix + '.tar.gz' } + + subject(:result) { repository.archive_metadata(ref, storage_path, append_sha: false) } + + context 'with hashed storage disabled' do + let(:project) { create(:project, :repository, :legacy_storage) } + + it 'uses the project path to generate the filename' do + expect(result['ArchivePrefix']).to eq(prefix) + expect(File.basename(result['ArchivePath'])).to eq(filename) + end + end + + context 'with hashed storage enabled' do + it 'uses the project path to generate the filename' do + expect(result['ArchivePrefix']).to eq(prefix) + expect(File.basename(result['ArchivePath'])).to eq(filename) + end + end + end + describe 'commit cache' do set(:project) { create(:project, :repository) } diff --git a/spec/models/term_agreement_spec.rb b/spec/models/term_agreement_spec.rb index a59bf119692..950dfa09a6a 100644 --- a/spec/models/term_agreement_spec.rb +++ b/spec/models/term_agreement_spec.rb @@ -5,4 +5,13 @@ describe TermAgreement do it { is_expected.to validate_presence_of(:term) } it { is_expected.to validate_presence_of(:user) } end + + describe '.accepted' do + it 'only includes accepted terms' do + accepted = create(:term_agreement, :accepted) + create(:term_agreement, :declined) + + expect(described_class.accepted).to contain_exactly(accepted) + end + end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 09dfeae6377..097144d04ce 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -1260,7 +1260,7 @@ describe User do it 'is false if avatar is html page' do user.update_attribute(:avatar, 'uploads/avatar.html') - expect(user.avatar_type).to eq(['file format is not supported. Please try one of the following supported formats: png, jpg, jpeg, gif, bmp, tiff']) + expect(user.avatar_type).to eq(['file format is not supported. Please try one of the following supported formats: png, jpg, jpeg, gif, bmp, tiff, ico']) end end diff --git a/spec/policies/ci/build_policy_spec.rb b/spec/policies/ci/build_policy_spec.rb index 9ca156deaa0..eead55d33ca 100644 --- a/spec/policies/ci/build_policy_spec.rb +++ b/spec/policies/ci/build_policy_spec.rb @@ -101,7 +101,7 @@ describe Ci::BuildPolicy do it 'enables update_build if user is maintainer' do allow_any_instance_of(Project).to receive(:empty_repo?).and_return(false) - allow_any_instance_of(Project).to receive(:branch_allows_maintainer_push?).and_return(true) + allow_any_instance_of(Project).to receive(:branch_allows_collaboration?).and_return(true) expect(policy).to be_allowed :update_build expect(policy).to be_allowed :update_commit_status diff --git a/spec/policies/ci/pipeline_policy_spec.rb b/spec/policies/ci/pipeline_policy_spec.rb index a5e509cfa0f..bd32faf06ef 100644 --- a/spec/policies/ci/pipeline_policy_spec.rb +++ b/spec/policies/ci/pipeline_policy_spec.rb @@ -69,7 +69,7 @@ describe Ci::PipelinePolicy, :models do it 'enables update_pipeline if user is maintainer' do allow_any_instance_of(Project).to receive(:empty_repo?).and_return(false) - allow_any_instance_of(Project).to receive(:branch_allows_maintainer_push?).and_return(true) + allow_any_instance_of(Project).to receive(:branch_allows_collaboration?).and_return(true) expect(policy).to be_allowed :update_pipeline end diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb index 6609f5f7afd..6ac151f92f3 100644 --- a/spec/policies/project_policy_spec.rb +++ b/spec/policies/project_policy_spec.rb @@ -400,7 +400,7 @@ describe ProjectPolicy do :merge_request, target_project: target_project, source_project: project, - allow_maintainer_to_push: true + allow_collaboration: true ) end let(:maintainer_abilities) do diff --git a/spec/requests/api/avatar_spec.rb b/spec/requests/api/avatar_spec.rb new file mode 100644 index 00000000000..26e0435a6d5 --- /dev/null +++ b/spec/requests/api/avatar_spec.rb @@ -0,0 +1,106 @@ +require 'spec_helper' + +describe API::Avatar do + let(:gravatar_service) { double('GravatarService') } + + describe 'GET /avatar' do + context 'avatar uploaded to GitLab' do + context 'user with matching public email address' do + let(:user) { create(:user, :with_avatar, email: 'public@example.com', public_email: 'public@example.com') } + + before do + user + end + + it 'returns the avatar url' do + get api('/avatar'), { email: 'public@example.com' } + + expect(response.status).to eq 200 + expect(json_response['avatar_url']).to eql("#{::Settings.gitlab.base_url}#{user.avatar.local_url}") + end + end + + context 'no user with matching public email address' do + before do + expect(GravatarService).to receive(:new).and_return(gravatar_service) + expect(gravatar_service).to( + receive(:execute) + .with('private@example.com', nil, 2, { username: nil }) + .and_return('https://gravatar')) + end + + it 'returns the avatar url from Gravatar' do + get api('/avatar'), { email: 'private@example.com' } + + expect(response.status).to eq 200 + expect(json_response['avatar_url']).to eq('https://gravatar') + end + end + end + + context 'avatar uploaded to Gravatar' do + context 'user with matching public email address' do + let(:user) { create(:user, email: 'public@example.com', public_email: 'public@example.com') } + + before do + user + + expect(GravatarService).to receive(:new).and_return(gravatar_service) + expect(gravatar_service).to( + receive(:execute) + .with('public@example.com', nil, 2, { username: user.username }) + .and_return('https://gravatar')) + end + + it 'returns the avatar url from Gravatar' do + get api('/avatar'), { email: 'public@example.com' } + + expect(response.status).to eq 200 + expect(json_response['avatar_url']).to eq('https://gravatar') + end + end + + context 'no user with matching public email address' do + before do + expect(GravatarService).to receive(:new).and_return(gravatar_service) + expect(gravatar_service).to( + receive(:execute) + .with('private@example.com', nil, 2, { username: nil }) + .and_return('https://gravatar')) + end + + it 'returns the avatar url from Gravatar' do + get api('/avatar'), { email: 'private@example.com' } + + expect(response.status).to eq 200 + expect(json_response['avatar_url']).to eq('https://gravatar') + end + end + + context 'public visibility level restricted' do + let(:user) { create(:user, :with_avatar, email: 'public@example.com', public_email: 'public@example.com') } + + before do + user + + stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC]) + end + + context 'when authenticated' do + it 'returns the avatar url' do + get api('/avatar', user), { email: 'public@example.com' } + + expect(response.status).to eq 200 + expect(json_response['avatar_url']).to eql("#{::Settings.gitlab.base_url}#{user.avatar.local_url}") + end + end + + context 'when unauthenticated' do + it_behaves_like '403 response' do + let(:request) { get api('/avatar'), { email: 'public@example.com' } } + end + end + end + end + end +end diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb index 8ad19e3f0f5..e73d1a252f5 100644 --- a/spec/requests/api/commits_spec.rb +++ b/spec/requests/api/commits_spec.rb @@ -18,14 +18,14 @@ describe API::Commits do describe 'GET /projects/:id/repository/commits' do let(:route) { "/projects/#{project_id}/repository/commits" } - shared_examples_for 'project commits' do + shared_examples_for 'project commits' do |schema: 'public_api/v4/commits'| it "returns project commits" do commit = project.repository.commit get api(route, current_user) expect(response).to have_gitlab_http_status(200) - expect(response).to match_response_schema('public_api/v4/commits') + expect(response).to match_response_schema(schema) expect(json_response.first['id']).to eq(commit.id) expect(json_response.first['committer_name']).to eq(commit.committer_name) expect(json_response.first['committer_email']).to eq(commit.committer_email) @@ -161,6 +161,23 @@ describe API::Commits do end end + context 'with_stats optional parameter' do + let(:project) { create(:project, :public, :repository) } + + it_behaves_like 'project commits', schema: 'public_api/v4/commits_with_stats' do + let(:route) { "/projects/#{project_id}/repository/commits?with_stats=true" } + + it 'include commits details' do + commit = project.repository.commit + get api(route, current_user) + + expect(json_response.first['stats']['additions']).to eq(commit.stats.additions) + expect(json_response.first['stats']['deletions']).to eq(commit.stats.deletions) + expect(json_response.first['stats']['total']).to eq(commit.stats.total) + end + end + end + context 'with pagination params' do let(:page) { 1 } let(:per_page) { 5 } @@ -247,6 +264,19 @@ describe API::Commits do ] } end + let!(:valid_utf8_c_params) do + { + branch: 'master', + commit_message: message, + actions: [ + { + action: 'create', + file_path: 'foo/bar/baz.txt', + content: 'puts 🦊' + } + ] + } + end it 'a new file in project repo' do post api(url, user), valid_c_params @@ -257,6 +287,15 @@ describe API::Commits do expect(json_response['committer_email']).to eq(user.email) end + it 'a new file with utf8 chars in project repo' do + post api(url, user), valid_utf8_c_params + + expect(response).to have_gitlab_http_status(201) + expect(json_response['title']).to eq(message) + expect(json_response['committer_name']).to eq(user.name) + expect(json_response['committer_email']).to eq(user.email) + end + it 'returns a 400 bad request if file exists' do post api(url, user), invalid_c_params diff --git a/spec/requests/api/events_spec.rb b/spec/requests/api/events_spec.rb index 962c845f36d..e6a61fdcf39 100644 --- a/spec/requests/api/events_spec.rb +++ b/spec/requests/api/events_spec.rb @@ -176,7 +176,7 @@ describe API::Events do end it 'avoids N+1 queries' do - control_count = ActiveRecord::QueryRecorder.new do + control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) do get api("/projects/#{private_project.id}/events", user), target_type: :merge_request end.count @@ -184,7 +184,7 @@ describe API::Events do expect do get api("/projects/#{private_project.id}/events", user), target_type: :merge_request - end.not_to exceed_query_limit(control_count) + end.not_to exceed_all_query_limit(control_count) expect(response).to have_gitlab_http_status(200) expect(response).to include_pagination_headers diff --git a/spec/requests/api/graphql/project_query_spec.rb b/spec/requests/api/graphql/project_query_spec.rb new file mode 100644 index 00000000000..796ffc9d569 --- /dev/null +++ b/spec/requests/api/graphql/project_query_spec.rb @@ -0,0 +1,88 @@ +require 'spec_helper' + +describe 'getting project information' do + include GraphqlHelpers + + let(:project) { create(:project, :repository) } + let(:current_user) { create(:user) } + + let(:query) do + graphql_query_for('project', 'fullPath' => project.full_path) + end + + context 'when the user has access to the project' do + before do + project.add_developer(current_user) + end + + it 'includes the project' do + post_graphql(query, current_user: current_user) + + expect(graphql_data['project']).not_to be_nil + end + + it_behaves_like 'a working graphql query' do + before do + post_graphql(query, current_user: current_user) + end + end + + context 'when requesting a nested merge request' do + let(:merge_request) { create(:merge_request, source_project: project) } + let(:merge_request_graphql_data) { graphql_data['project']['mergeRequest'] } + + let(:query) do + graphql_query_for( + 'project', + { 'fullPath' => project.full_path }, + query_graphql_field('mergeRequest', iid: merge_request.iid) + ) + end + + it_behaves_like 'a working graphql query' do + before do + post_graphql(query, current_user: current_user) + end + end + + it 'contains merge request information' do + post_graphql(query, current_user: current_user) + + expect(merge_request_graphql_data).not_to be_nil + end + + # This is a field coming from the `MergeRequestPresenter` + it 'includes a web_url' do + post_graphql(query, current_user: current_user) + + expect(merge_request_graphql_data['webUrl']).to be_present + end + + context 'when the user does not have access to the merge request' do + let(:project) { create(:project, :public, :repository) } + + it 'returns nil' do + project.project_feature.update!(merge_requests_access_level: ProjectFeature::PRIVATE) + + post_graphql(query) + + expect(merge_request_graphql_data).to be_nil + end + end + end + end + + context 'when the user does not have access to the project' do + it 'returns an empty field' do + post_graphql(query, current_user: current_user) + + expect(graphql_data['project']).to be_nil + end + + it_behaves_like 'a working graphql query' do + before do + post_graphql(query, current_user: current_user) + end + end + end +end diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb index 5dc3ddd4b36..a56b913198c 100644 --- a/spec/requests/api/internal_spec.rb +++ b/spec/requests/api/internal_spec.rb @@ -522,7 +522,6 @@ describe API::Internal do context 'the project path was changed' do let(:project) { create(:project, :repository, :legacy_storage) } - let!(:old_path_to_repo) { project.repository.path_to_repo } let!(:repository) { project.repository } before do @@ -835,8 +834,7 @@ describe API::Internal do end def push(key, project, protocol = 'ssh', env: nil) - post( - api("/internal/allowed"), + params = { changes: 'd14d6c0abdd253381df51a723d58691b2ee1ab08 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/master', key_id: key.id, project: project.full_path, @@ -845,7 +843,19 @@ describe API::Internal do secret_token: secret_token, protocol: protocol, env: env - ) + } + + if Gitlab.rails5? + post( + api("/internal/allowed"), + params: params + ) + else + post( + api("/internal/allowed"), + params + ) + end end def archive(key, project) diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb index 4181f4ebbbe..a15d60aafe0 100644 --- a/spec/requests/api/issues_spec.rb +++ b/spec/requests/api/issues_spec.rb @@ -630,15 +630,17 @@ describe API::Issues do end it 'avoids N+1 queries' do - control_count = ActiveRecord::QueryRecorder.new do + get api("/projects/#{project.id}/issues", user) + + control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) do get api("/projects/#{project.id}/issues", user) end.count - create(:issue, author: user, project: project) + create_list(:issue, 3, project: project) expect do get api("/projects/#{project.id}/issues", user) - end.not_to exceed_query_limit(control_count) + end.not_to exceed_all_query_limit(control_count) end it 'returns 404 when project does not exist' do diff --git a/spec/requests/api/jobs_spec.rb b/spec/requests/api/jobs_spec.rb index 45082e644ca..50d6f4b4d99 100644 --- a/spec/requests/api/jobs_spec.rb +++ b/spec/requests/api/jobs_spec.rb @@ -177,6 +177,18 @@ describe API::Jobs do json_response.each { |job| expect(job['pipeline']['id']).to eq(pipeline.id) } end end + + it 'avoids N+1 queries' do + control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) do + get api("/projects/#{project.id}/pipelines/#{pipeline.id}/jobs", api_user), query + end.count + + 3.times { create(:ci_build, :artifacts, pipeline: pipeline) } + + expect do + get api("/projects/#{project.id}/pipelines/#{pipeline.id}/jobs", api_user), query + end.not_to exceed_all_query_limit(control_count) + end end context 'unauthorized user' do diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index 605761867bf..d4ebfc3f782 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -386,12 +386,13 @@ describe API::MergeRequests do source_project: forked_project, target_project: project, source_branch: 'fixes', - allow_maintainer_to_push: true) + allow_collaboration: true) end - it 'includes the `allow_maintainer_to_push` field' do + it 'includes the `allow_collaboration` field' do get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user) + expect(json_response['allow_collaboration']).to be_truthy expect(json_response['allow_maintainer_to_push']).to be_truthy end end @@ -654,11 +655,12 @@ describe API::MergeRequests do expect(response).to have_gitlab_http_status(400) end - it 'allows setting `allow_maintainer_to_push`' do + it 'allows setting `allow_collaboration`' do post api("/projects/#{forked_project.id}/merge_requests", user2), - title: 'Test merge_request', source_branch: "feature_conflict", target_branch: "master", - author: user2, target_project_id: project.id, allow_maintainer_to_push: true + title: 'Test merge_request', source_branch: "feature_conflict", target_branch: "master", + author: user2, target_project_id: project.id, allow_collaboration: true expect(response).to have_gitlab_http_status(201) + expect(json_response['allow_collaboration']).to be_truthy expect(json_response['allow_maintainer_to_push']).to be_truthy end diff --git a/spec/requests/api/pipelines_spec.rb b/spec/requests/api/pipelines_spec.rb index 0736329f9fd..78ea77cb3bb 100644 --- a/spec/requests/api/pipelines_spec.rb +++ b/spec/requests/api/pipelines_spec.rb @@ -285,6 +285,15 @@ describe API::Pipelines do end describe 'POST /projects/:id/pipeline ' do + def expect_variables(variables, expected_variables) + variables.each_with_index do |variable, index| + expected_variable = expected_variables[index] + + expect(variable.key).to eq(expected_variable['key']) + expect(variable.value).to eq(expected_variable['value']) + end + end + context 'authorized user' do context 'with gitlab-ci.yml' do before do @@ -294,13 +303,62 @@ describe API::Pipelines do it 'creates and returns a new pipeline' do expect do post api("/projects/#{project.id}/pipeline", user), ref: project.default_branch - end.to change { Ci::Pipeline.count }.by(1) + end.to change { project.pipelines.count }.by(1) expect(response).to have_gitlab_http_status(201) expect(json_response).to be_a Hash expect(json_response['sha']).to eq project.commit.id end + context 'variables given' do + let(:variables) { [{ 'key' => 'UPLOAD_TO_S3', 'value' => 'true' }] } + + it 'creates and returns a new pipeline using the given variables' do + expect do + post api("/projects/#{project.id}/pipeline", user), ref: project.default_branch, variables: variables + end.to change { project.pipelines.count }.by(1) + expect_variables(project.pipelines.last.variables, variables) + + expect(response).to have_gitlab_http_status(201) + expect(json_response).to be_a Hash + expect(json_response['sha']).to eq project.commit.id + expect(json_response).not_to have_key('variables') + end + end + + describe 'using variables conditions' do + let(:variables) { [{ 'key' => 'STAGING', 'value' => 'true' }] } + + before do + config = YAML.dump(test: { script: 'test', only: { variables: ['$STAGING'] } }) + stub_ci_pipeline_yaml_file(config) + end + + it 'creates and returns a new pipeline using the given variables' do + expect do + post api("/projects/#{project.id}/pipeline", user), ref: project.default_branch, variables: variables + end.to change { project.pipelines.count }.by(1) + expect_variables(project.pipelines.last.variables, variables) + + expect(response).to have_gitlab_http_status(201) + expect(json_response).to be_a Hash + expect(json_response['sha']).to eq project.commit.id + expect(json_response).not_to have_key('variables') + end + + context 'condition unmatch' do + let(:variables) { [{ 'key' => 'STAGING', 'value' => 'false' }] } + + it "doesn't create a job" do + expect do + post api("/projects/#{project.id}/pipeline", user), ref: project.default_branch + end.not_to change { project.pipelines.count } + + expect(response).to have_gitlab_http_status(400) + end + end + end + it 'fails when using an invalid ref' do post api("/projects/#{project.id}/pipeline", user), ref: 'invalid_ref' diff --git a/spec/requests/api/project_import_spec.rb b/spec/requests/api/project_import_spec.rb index f8c64f063af..97dffdc9233 100644 --- a/spec/requests/api/project_import_spec.rb +++ b/spec/requests/api/project_import_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe API::ProjectImport do let(:export_path) { "#{Dir.tmpdir}/project_export_spec" } let(:user) { create(:user) } - let(:file) { File.join(Rails.root, 'spec', 'features', 'projects', 'import_export', 'test_project_export.tar.gz') } + let(:file) { File.join('spec', 'features', 'projects', 'import_export', 'test_project_export.tar.gz') } let(:namespace) { create(:group) } before do allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path) diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index 9b7c3205c1f..99103039f77 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -518,7 +518,7 @@ describe API::Projects do end it 'uploads avatar for project a project' do - project = attributes_for(:project, avatar: fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif')) + project = attributes_for(:project, avatar: fixture_file_upload('spec/fixtures/banana_sample.gif', 'image/gif')) post api('/projects', user), project @@ -777,7 +777,7 @@ describe API::Projects do end it "uploads the file and returns its info" do - post api("/projects/#{project.id}/uploads", user), file: fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png") + post api("/projects/#{project.id}/uploads", user), file: fixture_file_upload("spec/fixtures/dk.png", "image/png") expect(response).to have_gitlab_http_status(201) expect(json_response['alt']).to eq("dk") diff --git a/spec/requests/api/repositories_spec.rb b/spec/requests/api/repositories_spec.rb index 9e6d69e3874..cd135dfc32a 100644 --- a/spec/requests/api/repositories_spec.rb +++ b/spec/requests/api/repositories_spec.rb @@ -220,11 +220,10 @@ describe API::Repositories do expect(response).to have_gitlab_http_status(200) - repo_name = project.repository.name.gsub("\.git", "") type, params = workhorse_send_data expect(type).to eq('git-archive') - expect(params['ArchivePath']).to match(/#{repo_name}\-[^\.]+\.tar.gz/) + expect(params['ArchivePath']).to match(/#{project.path}\-[^\.]+\.tar.gz/) end it 'returns the repository archive archive.zip' do @@ -232,11 +231,10 @@ describe API::Repositories do expect(response).to have_gitlab_http_status(200) - repo_name = project.repository.name.gsub("\.git", "") type, params = workhorse_send_data expect(type).to eq('git-archive') - expect(params['ArchivePath']).to match(/#{repo_name}\-[^\.]+\.zip/) + expect(params['ArchivePath']).to match(/#{project.path}\-[^\.]+\.zip/) end it 'returns the repository archive archive.tar.bz2' do @@ -244,11 +242,10 @@ describe API::Repositories do expect(response).to have_gitlab_http_status(200) - repo_name = project.repository.name.gsub("\.git", "") type, params = workhorse_send_data expect(type).to eq('git-archive') - expect(params['ArchivePath']).to match(/#{repo_name}\-[^\.]+\.tar.bz2/) + expect(params['ArchivePath']).to match(/#{project.path}\-[^\.]+\.tar.bz2/) end context 'when sha does not exist' do diff --git a/spec/requests/api/runner_spec.rb b/spec/requests/api/runner_spec.rb index 319ac389083..e7639599874 100644 --- a/spec/requests/api/runner_spec.rb +++ b/spec/requests/api/runner_spec.rb @@ -351,11 +351,13 @@ describe API::Runner, :clean_gitlab_redis_shared_state do context 'when valid token is provided' do context 'when Runner is not active' do let(:runner) { create(:ci_runner, :inactive) } + let(:update_value) { runner.ensure_runner_queue_value } it 'returns 204 error' do request_job - expect(response).to have_gitlab_http_status 204 + expect(response).to have_gitlab_http_status(204) + expect(response.header['X-GitLab-Last-Update']).to eq(update_value) end end @@ -816,6 +818,18 @@ describe API::Runner, :clean_gitlab_redis_shared_state do expect(job.reload.trace.raw).to eq 'BUILD TRACE' end + + context 'when running state is sent' do + it 'updates update_at value' do + expect { update_job_after_time }.to change { job.reload.updated_at } + end + end + + context 'when other state is sent' do + it "doesn't update update_at value" do + expect { update_job_after_time(20.minutes, state: 'success') }.not_to change { job.reload.updated_at } + end + end end context 'when job has been erased' do @@ -838,6 +852,7 @@ describe API::Runner, :clean_gitlab_redis_shared_state do update_job(state: 'success', trace: 'BUILD TRACE UPDATED') expect(response).to have_gitlab_http_status(403) + expect(response.header['Job-Status']).to eq 'failed' expect(job.trace.raw).to eq 'Job failed' expect(job).to be_failed end @@ -847,6 +862,12 @@ describe API::Runner, :clean_gitlab_redis_shared_state do new_params = params.merge(token: token) put api("/jobs/#{job.id}"), new_params end + + def update_job_after_time(update_interval = 20.minutes, state = 'running') + Timecop.travel(job.updated_at + update_interval) do + update_job(job.token, state: state) + end + end end describe 'PATCH /api/v4/jobs/:id/trace' do @@ -979,6 +1000,17 @@ describe API::Runner, :clean_gitlab_redis_shared_state do end end end + + context 'when the job is canceled' do + before do + job.cancel + patch_the_trace + end + + it 'receives status in header' do + expect(response.header['Job-Status']).to eq 'canceled' + end + end end context 'when Runner makes a force-patch' do @@ -1055,8 +1087,8 @@ describe API::Runner, :clean_gitlab_redis_shared_state do let(:jwt_token) { JWT.encode({ 'iss' => 'gitlab-workhorse' }, Gitlab::Workhorse.secret, 'HS256') } let(:headers) { { 'GitLab-Workhorse' => '1.0', Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER => jwt_token } } let(:headers_with_token) { headers.merge(API::Helpers::Runner::JOB_TOKEN_HEADER => job.token) } - let(:file_upload) { fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif') } - let(:file_upload2) { fixture_file_upload(Rails.root + 'spec/fixtures/dk.png', 'image/gif') } + let(:file_upload) { fixture_file_upload('spec/fixtures/banana_sample.gif', 'image/gif') } + let(:file_upload2) { fixture_file_upload('spec/fixtures/dk.png', 'image/gif') } before do stub_artifacts_object_storage @@ -1101,6 +1133,7 @@ describe API::Runner, :clean_gitlab_redis_shared_state do expect(json_response['RemoteObject']).to have_key('GetURL') expect(json_response['RemoteObject']).to have_key('StoreURL') expect(json_response['RemoteObject']).to have_key('DeleteURL') + expect(json_response['RemoteObject']).to have_key('MultipartUpload') end end diff --git a/spec/requests/api/settings_spec.rb b/spec/requests/api/settings_spec.rb index aead8978dd4..57adc3ca7a6 100644 --- a/spec/requests/api/settings_spec.rb +++ b/spec/requests/api/settings_spec.rb @@ -35,7 +35,9 @@ describe API::Settings, 'Settings' do context "custom repository storage type set in the config" do before do - storages = { 'custom' => 'tmp/tests/custom_repositories' } + # Add a possible storage to the config + storages = Gitlab.config.repositories.storages + .merge({ 'custom' => 'tmp/tests/custom_repositories' }) allow(Gitlab.config.repositories).to receive(:storages).and_return(storages) end diff --git a/spec/requests/api/snippets_spec.rb b/spec/requests/api/snippets_spec.rb index b3e253befc6..c5456977b60 100644 --- a/spec/requests/api/snippets_spec.rb +++ b/spec/requests/api/snippets_spec.rb @@ -20,6 +20,7 @@ describe API::Snippets do private_snippet.id) expect(json_response.last).to have_key('web_url') expect(json_response.last).to have_key('raw_url') + expect(json_response.last).to have_key('visibility') end it 'hides private snippets from regular user' do @@ -112,6 +113,7 @@ describe API::Snippets do expect(json_response['title']).to eq(snippet.title) expect(json_response['description']).to eq(snippet.description) expect(json_response['file_name']).to eq(snippet.file_name) + expect(json_response['visibility']).to eq(snippet.visibility) end it 'returns 404 for invalid snippet id' do @@ -142,6 +144,7 @@ describe API::Snippets do expect(json_response['title']).to eq(params[:title]) expect(json_response['description']).to eq(params[:description]) expect(json_response['file_name']).to eq(params[:file_name]) + expect(json_response['visibility']).to eq(params[:visibility]) end it 'returns 400 for missing parameters' do diff --git a/spec/requests/api/tags_spec.rb b/spec/requests/api/tags_spec.rb index e2b19ad59f9..969710d6613 100644 --- a/spec/requests/api/tags_spec.rb +++ b/spec/requests/api/tags_spec.rb @@ -287,7 +287,10 @@ describe API::Tags do context 'annotated tag' do it 'creates a new annotated tag' do # Identity must be set in .gitconfig to create annotated tag. - repo_path = project.repository.path_to_repo + repo_path = Gitlab::GitalyClient::StorageSettings.allow_disk_access do + project.repository.path_to_repo + end + system(*%W(#{Gitlab.config.git.bin_path} --git-dir=#{repo_path} config user.name #{user.name})) system(*%W(#{Gitlab.config.git.bin_path} --git-dir=#{repo_path} config user.email #{user.email})) diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index 05637eb0729..a97c3f3461a 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -512,7 +512,7 @@ describe API::Users do end it 'updates user with avatar' do - put api("/users/#{user.id}", admin), { avatar: fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif') } + put api("/users/#{user.id}", admin), { avatar: fixture_file_upload('spec/fixtures/banana_sample.gif', 'image/gif') } user.reload @@ -1123,58 +1123,63 @@ describe API::Users do describe "GET /user" do let(:personal_access_token) { create(:personal_access_token, user: user).token } - context 'with regular user' do - context 'with personal access token' do - it 'returns 403 without private token when sudo is defined' do - get api("/user?private_token=#{personal_access_token}&sudo=123") + shared_examples 'get user info' do |version| + context 'with regular user' do + context 'with personal access token' do + it 'returns 403 without private token when sudo is defined' do + get api("/user?private_token=#{personal_access_token}&sudo=123", version: version) - expect(response).to have_gitlab_http_status(403) + expect(response).to have_gitlab_http_status(403) + end end - end - it 'returns current user without private token when sudo not defined' do - get api("/user", user) + it 'returns current user without private token when sudo not defined' do + get api("/user", user, version: version) - expect(response).to have_gitlab_http_status(200) - expect(response).to match_response_schema('public_api/v4/user/public') - expect(json_response['id']).to eq(user.id) - end + expect(response).to have_gitlab_http_status(200) + expect(response).to match_response_schema('public_api/v4/user/public') + expect(json_response['id']).to eq(user.id) + end - context "scopes" do - let(:path) { "/user" } - let(:api_call) { method(:api) } + context "scopes" do + let(:path) { "/user" } + let(:api_call) { method(:api) } - include_examples 'allows the "read_user" scope' + include_examples 'allows the "read_user" scope', version + end end - end - context 'with admin' do - let(:admin_personal_access_token) { create(:personal_access_token, user: admin).token } + context 'with admin' do + let(:admin_personal_access_token) { create(:personal_access_token, user: admin).token } - context 'with personal access token' do - it 'returns 403 without private token when sudo defined' do - get api("/user?private_token=#{admin_personal_access_token}&sudo=#{user.id}") + context 'with personal access token' do + it 'returns 403 without private token when sudo defined' do + get api("/user?private_token=#{admin_personal_access_token}&sudo=#{user.id}", version: version) - expect(response).to have_gitlab_http_status(403) - end + expect(response).to have_gitlab_http_status(403) + end - it 'returns initial current user without private token but with is_admin when sudo not defined' do - get api("/user?private_token=#{admin_personal_access_token}") + it 'returns initial current user without private token but with is_admin when sudo not defined' do + get api("/user?private_token=#{admin_personal_access_token}", version: version) - expect(response).to have_gitlab_http_status(200) - expect(response).to match_response_schema('public_api/v4/user/admin') - expect(json_response['id']).to eq(admin.id) + expect(response).to have_gitlab_http_status(200) + expect(response).to match_response_schema('public_api/v4/user/admin') + expect(json_response['id']).to eq(admin.id) + end end end - end - context 'with unauthenticated user' do - it "returns 401 error if user is unauthenticated" do - get api("/user") + context 'with unauthenticated user' do + it "returns 401 error if user is unauthenticated" do + get api("/user", version: version) - expect(response).to have_gitlab_http_status(401) + expect(response).to have_gitlab_http_status(401) + end end end + + it_behaves_like 'get user info', 'v3' + it_behaves_like 'get user info', 'v4' end describe "GET /user/keys" do diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb index 2514dab1714..92fcfb65269 100644 --- a/spec/requests/git_http_spec.rb +++ b/spec/requests/git_http_spec.rb @@ -1,6 +1,7 @@ require "spec_helper" describe 'Git HTTP requests' do + include ProjectForksHelper include TermsHelper include GitHttpHelpers include WorkhorseHelpers @@ -305,6 +306,22 @@ describe 'Git HTTP requests' do expect(response.body).to eq(change_access_error(:push_code)) end end + + context 'when merge requests are open that allow maintainer access' do + let(:canonical_project) { create(:project, :public, :repository) } + let(:project) { fork_project(canonical_project, nil, repository: true) } + + before do + canonical_project.add_master(user) + create(:merge_request, + source_project: project, + target_project: canonical_project, + source_branch: 'fixes', + allow_collaboration: true) + end + + it_behaves_like 'pushes are allowed' + end end end diff --git a/spec/requests/lfs_http_spec.rb b/spec/requests/lfs_http_spec.rb index 79672fe1cc5..4d30b99262e 100644 --- a/spec/requests/lfs_http_spec.rb +++ b/spec/requests/lfs_http_spec.rb @@ -1021,6 +1021,7 @@ describe 'Git LFS API and storage' do expect(json_response['RemoteObject']).to have_key('GetURL') expect(json_response['RemoteObject']).to have_key('StoreURL') expect(json_response['RemoteObject']).to have_key('DeleteURL') + expect(json_response['RemoteObject']).not_to have_key('MultipartUpload') expect(json_response['LfsOid']).to eq(sample_oid) expect(json_response['LfsSize']).to eq(sample_size) end diff --git a/spec/requests/openid_connect_spec.rb b/spec/requests/openid_connect_spec.rb index be286c490fe..bcb8d6c2bfc 100644 --- a/spec/requests/openid_connect_spec.rb +++ b/spec/requests/openid_connect_spec.rb @@ -61,7 +61,7 @@ describe 'OpenID Connect requests' do email: private_email.email, public_email: public_email.email, website_url: 'https://example.com', - avatar: fixture_file_upload(Rails.root + "spec/fixtures/dk.png") + avatar: fixture_file_upload('spec/fixtures/dk.png') ) end diff --git a/spec/routing/api_routing_spec.rb b/spec/routing/api_routing_spec.rb new file mode 100644 index 00000000000..5fde4bd885b --- /dev/null +++ b/spec/routing/api_routing_spec.rb @@ -0,0 +1,31 @@ +require 'spec_helper' + +describe 'api', 'routing' do + context 'when graphql is disabled' do + before do + stub_feature_flags(graphql: false) + end + + it 'does not route to the GraphqlController' do + expect(get('/api/graphql')).not_to route_to('graphql#execute') + end + + it 'does not expose graphiql' do + expect(get('/-/graphql-explorer')).not_to route_to('graphiql/rails/editors#show') + end + end + + context 'when graphql is disabled' do + before do + stub_feature_flags(graphql: true) + end + + it 'routes to the GraphqlController' do + expect(get('/api/graphql')).not_to route_to('graphql#execute') + end + + it 'exposes graphiql' do + expect(get('/-/graphql-explorer')).not_to route_to('graphiql/rails/editors#show') + end + end +end diff --git a/spec/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb index e1b4e618092..56d93095a85 100644 --- a/spec/routing/project_routing_spec.rb +++ b/spec/routing/project_routing_spec.rb @@ -36,33 +36,36 @@ describe 'project routing' do shared_examples 'RESTful project resources' do let(:actions) { [:index, :create, :new, :edit, :show, :update, :destroy] } let(:controller_path) { controller } + let(:id) { { id: '1' } } + let(:format) { {} } # response format, e.g. { format: :html } + let(:params) { { namespace_id: 'gitlab', project_id: 'gitlabhq' } } it 'to #index' do - expect(get("/gitlab/gitlabhq/#{controller_path}")).to route_to("projects/#{controller}#index", namespace_id: 'gitlab', project_id: 'gitlabhq') if actions.include?(:index) + expect(get("/gitlab/gitlabhq/#{controller_path}")).to route_to("projects/#{controller}#index", params) if actions.include?(:index) end it 'to #create' do - expect(post("/gitlab/gitlabhq/#{controller_path}")).to route_to("projects/#{controller}#create", namespace_id: 'gitlab', project_id: 'gitlabhq') if actions.include?(:create) + expect(post("/gitlab/gitlabhq/#{controller_path}")).to route_to("projects/#{controller}#create", params) if actions.include?(:create) end it 'to #new' do - expect(get("/gitlab/gitlabhq/#{controller_path}/new")).to route_to("projects/#{controller}#new", namespace_id: 'gitlab', project_id: 'gitlabhq') if actions.include?(:new) + expect(get("/gitlab/gitlabhq/#{controller_path}/new")).to route_to("projects/#{controller}#new", params) if actions.include?(:new) end it 'to #edit' do - expect(get("/gitlab/gitlabhq/#{controller_path}/1/edit")).to route_to("projects/#{controller}#edit", namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') if actions.include?(:edit) + expect(get("/gitlab/gitlabhq/#{controller_path}/1/edit")).to route_to("projects/#{controller}#edit", params.merge(**id, **format)) if actions.include?(:edit) end it 'to #show' do - expect(get("/gitlab/gitlabhq/#{controller_path}/1")).to route_to("projects/#{controller}#show", namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') if actions.include?(:show) + expect(get("/gitlab/gitlabhq/#{controller_path}/1")).to route_to("projects/#{controller}#show", params.merge(**id, **format)) if actions.include?(:show) end it 'to #update' do - expect(put("/gitlab/gitlabhq/#{controller_path}/1")).to route_to("projects/#{controller}#update", namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') if actions.include?(:update) + expect(put("/gitlab/gitlabhq/#{controller_path}/1")).to route_to("projects/#{controller}#update", params.merge(id)) if actions.include?(:update) end it 'to #destroy' do - expect(delete("/gitlab/gitlabhq/#{controller_path}/1")).to route_to("projects/#{controller}#destroy", namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') if actions.include?(:destroy) + expect(delete("/gitlab/gitlabhq/#{controller_path}/1")).to route_to("projects/#{controller}#destroy", params.merge(**id, **format)) if actions.include?(:destroy) end end @@ -150,12 +153,13 @@ describe 'project routing' do end it 'to #history' do - expect(get('/gitlab/gitlabhq/wikis/1/history')).to route_to('projects/wikis#history', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') + expect(get('/gitlab/gitlabhq/wikis/1/history')).to route_to('projects/wikis#history', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1', format: :html) end it_behaves_like 'RESTful project resources' do let(:actions) { [:create, :edit, :show, :destroy] } let(:controller) { 'wikis' } + let(:format) { { format: :html } } end end diff --git a/spec/serializers/build_serializer_spec.rb b/spec/serializers/build_serializer_spec.rb index 98cd15e248b..52459cd369d 100644 --- a/spec/serializers/build_serializer_spec.rb +++ b/spec/serializers/build_serializer_spec.rb @@ -39,7 +39,7 @@ describe BuildSerializer do expect(subject[:label]).to eq('failed') expect(subject[:tooltip]).to eq('failed <br> (unknown failure)') expect(subject[:icon]).to eq(status.icon) - expect(subject[:favicon]).to match_asset_path("/assets/ci_favicons/#{status.favicon}.ico") + expect(subject[:favicon]).to match_asset_path("/assets/ci_favicons/#{status.favicon}.png") end end @@ -54,7 +54,7 @@ describe BuildSerializer do expect(subject[:label]).to eq('passed') expect(subject[:tooltip]).to eq('passed') expect(subject[:icon]).to eq(status.icon) - expect(subject[:favicon]).to match_asset_path("/assets/ci_favicons/#{status.favicon}.ico") + expect(subject[:favicon]).to match_asset_path("/assets/ci_favicons/#{status.favicon}.png") end end end diff --git a/spec/serializers/pipeline_serializer_spec.rb b/spec/serializers/pipeline_serializer_spec.rb index b741308e2c5..eb4235e3ee6 100644 --- a/spec/serializers/pipeline_serializer_spec.rb +++ b/spec/serializers/pipeline_serializer_spec.rb @@ -8,6 +8,10 @@ describe PipelineSerializer do described_class.new(current_user: user) end + before do + stub_feature_flags(ci_pipeline_persisted_stages: true) + end + subject { serializer.represent(resource) } describe '#represent' do @@ -99,7 +103,8 @@ describe PipelineSerializer do end end - context 'number of queries' do + describe 'number of queries when preloaded' do + subject { serializer.represent(resource, preload: true) } let(:resource) { Ci::Pipeline.all } before do @@ -107,7 +112,7 @@ describe PipelineSerializer do # gitaly calls in this block # Issue: https://gitlab.com/gitlab-org/gitlab-ce/issues/37772 Gitlab::GitalyClient.allow_n_plus_1_calls do - Ci::Pipeline::AVAILABLE_STATUSES.each do |status| + Ci::Pipeline::COMPLETED_STATUSES.each do |status| create_pipeline(status) end end @@ -120,7 +125,7 @@ describe PipelineSerializer do it 'verifies number of queries', :request_store do recorded = ActiveRecord::QueryRecorder.new { subject } - expect(recorded.count).to be_within(1).of(44) + expect(recorded.count).to be_within(2).of(27) expect(recorded.cached_count).to eq(0) end end @@ -139,7 +144,7 @@ describe PipelineSerializer do # pipeline. With the same ref this check is cached but if refs are # different then there is an extra query per ref # https://gitlab.com/gitlab-org/gitlab-ce/issues/46368 - expect(recorded.count).to be_within(1).of(51) + expect(recorded.count).to be_within(2).of(30) expect(recorded.cached_count).to eq(0) end end @@ -174,7 +179,7 @@ describe PipelineSerializer do expect(subject[:text]).to eq(status.text) expect(subject[:label]).to eq(status.label) expect(subject[:icon]).to eq(status.icon) - expect(subject[:favicon]).to match_asset_path("/assets/ci_favicons/#{status.favicon}.ico") + expect(subject[:favicon]).to match_asset_path("/assets/ci_favicons/#{status.favicon}.png") end end end diff --git a/spec/serializers/status_entity_spec.rb b/spec/serializers/status_entity_spec.rb index 559475e571c..0b010ebd507 100644 --- a/spec/serializers/status_entity_spec.rb +++ b/spec/serializers/status_entity_spec.rb @@ -18,17 +18,7 @@ describe StatusEntity do it 'contains status details' do expect(subject).to include :text, :icon, :favicon, :label, :group, :tooltip expect(subject).to include :has_details, :details_path - expect(subject[:favicon]).to match_asset_path('/assets/ci_favicons/favicon_status_success.ico') - end - - it 'contains a dev namespaced favicon if dev env' do - allow(Rails.env).to receive(:development?) { true } - expect(entity.as_json[:favicon]).to match_asset_path('/assets/ci_favicons/dev/favicon_status_success.ico') - end - - it 'contains a canary namespaced favicon if canary env' do - stub_env('CANARY', 'true') - expect(entity.as_json[:favicon]).to match_asset_path('/assets/ci_favicons/canary/favicon_status_success.ico') + expect(subject[:favicon]).to match_asset_path('/assets/ci_favicons/favicon_status_success.png') end end end diff --git a/spec/services/ci/retry_pipeline_service_spec.rb b/spec/services/ci/retry_pipeline_service_spec.rb index a73bd7a0268..688d3b8c038 100644 --- a/spec/services/ci/retry_pipeline_service_spec.rb +++ b/spec/services/ci/retry_pipeline_service_spec.rb @@ -280,12 +280,12 @@ describe Ci::RetryPipelineService, '#execute' do source_project: forked_project, target_project: project, source_branch: 'fixes', - allow_maintainer_to_push: true) + allow_collaboration: true) create_build('rspec 1', :failed, 1) end it 'allows to retry failed pipeline' do - allow_any_instance_of(Project).to receive(:fetch_branch_allows_maintainer_push?).and_return(true) + allow_any_instance_of(Project).to receive(:fetch_branch_allows_collaboration?).and_return(true) allow_any_instance_of(Project).to receive(:empty_repo?).and_return(false) service.execute(pipeline) diff --git a/spec/services/git_tag_push_service_spec.rb b/spec/services/git_tag_push_service_spec.rb index 33405d7a7ec..92159e1e372 100644 --- a/spec/services/git_tag_push_service_spec.rb +++ b/spec/services/git_tag_push_service_spec.rb @@ -118,7 +118,9 @@ describe GitTagPushService do before do # Create the lightweight tag - project.repository.raw_repository.rugged.tags.create(tag_name, newrev) + Gitlab::GitalyClient::StorageSettings.allow_disk_access do + project.repository.raw_repository.rugged.tags.create(tag_name, newrev) + end # Clear tag list cache project.repository.expire_tags_cache diff --git a/spec/services/lfs/unlock_file_service_spec.rb b/spec/services/lfs/unlock_file_service_spec.rb index 4bea112b9c6..948401d7bdc 100644 --- a/spec/services/lfs/unlock_file_service_spec.rb +++ b/spec/services/lfs/unlock_file_service_spec.rb @@ -80,12 +80,12 @@ describe Lfs::UnlockFileService do result = subject.execute expect(result[:status]).to eq(:error) - expect(result[:message]).to match(/You must have master access/) + expect(result[:message]).to match(/You must have maintainer access/) expect(result[:http_status]).to eq(403) end end - context 'by a master user' do + context 'by a maintainer user' do let(:current_user) { master } let(:params) do { id: lock.id, diff --git a/spec/services/merge_requests/create_from_issue_service_spec.rb b/spec/services/merge_requests/create_from_issue_service_spec.rb index 38d84cf0ceb..b1882df732d 100644 --- a/spec/services/merge_requests/create_from_issue_service_spec.rb +++ b/spec/services/merge_requests/create_from_issue_service_spec.rb @@ -125,9 +125,14 @@ describe MergeRequests::CreateFromIssueService do end context 'when ref branch does not exist' do - it 'does not create a merge request' do - expect { described_class.new(project, user, issue_iid: issue.iid, ref: 'nobr').execute } - .not_to change { project.merge_requests.count } + subject { described_class.new(project, user, issue_iid: issue.iid, ref: 'no-such-branch').execute } + + it 'creates a merge request' do + expect { subject }.to change(project.merge_requests, :count).by(1) + end + + it 'sets the merge request target branch to the project default branch' do + expect(subject[:merge_request].target_branch).to eq(project.default_branch) end end end diff --git a/spec/services/merge_requests/ff_merge_service_spec.rb b/spec/services/merge_requests/ff_merge_service_spec.rb index 5ef6365fcc9..28f56d19657 100644 --- a/spec/services/merge_requests/ff_merge_service_spec.rb +++ b/spec/services/merge_requests/ff_merge_service_spec.rb @@ -72,7 +72,7 @@ describe MergeRequests::FfMergeService do it 'logs and saves error if there is an PreReceiveError exception' do error_message = 'error message' - allow(service).to receive(:repository).and_raise(Gitlab::Git::HooksService::PreReceiveError, error_message) + allow(service).to receive(:repository).and_raise(Gitlab::Git::PreReceiveError, error_message) allow(service).to receive(:execute_hooks) service.execute(merge_request) diff --git a/spec/services/merge_requests/merge_service_spec.rb b/spec/services/merge_requests/merge_service_spec.rb index dc30a9bccc1..ef2738ef504 100644 --- a/spec/services/merge_requests/merge_service_spec.rb +++ b/spec/services/merge_requests/merge_service_spec.rb @@ -226,7 +226,7 @@ describe MergeRequests::MergeService do it 'logs and saves error if there is an PreReceiveError exception' do error_message = 'error message' - allow(service).to receive(:repository).and_raise(Gitlab::Git::HooksService::PreReceiveError, error_message) + allow(service).to receive(:repository).and_raise(Gitlab::Git::PreReceiveError, error_message) allow(service).to receive(:execute_hooks) service.execute(merge_request) diff --git a/spec/services/merge_requests/squash_service_spec.rb b/spec/services/merge_requests/squash_service_spec.rb index bd884787425..ded17fa92a4 100644 --- a/spec/services/merge_requests/squash_service_spec.rb +++ b/spec/services/merge_requests/squash_service_spec.rb @@ -63,7 +63,9 @@ describe MergeRequests::SquashService do end it 'has the same diff as the merge request, but a different SHA' do - rugged = project.repository.rugged + rugged = Gitlab::GitalyClient::StorageSettings.allow_disk_access do + project.repository.rugged + end mr_diff = rugged.diff(merge_request.diff_base_sha, merge_request.diff_head_sha) squash_diff = rugged.diff(merge_request.diff_start_sha, squash_sha) diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb index bd2e91f1f7a..443dcd92a8b 100644 --- a/spec/services/merge_requests/update_service_spec.rb +++ b/spec/services/merge_requests/update_service_spec.rb @@ -547,7 +547,7 @@ describe MergeRequests::UpdateService, :mailer do let(:closed_issuable) { create(:closed_merge_request, source_project: project) } end - context 'setting `allow_maintainer_to_push`' do + context 'setting `allow_collaboration`' do let(:target_project) { create(:project, :public) } let(:source_project) { fork_project(target_project) } let(:user) { create(:user) } @@ -562,23 +562,23 @@ describe MergeRequests::UpdateService, :mailer do allow(ProtectedBranch).to receive(:protected?).with(source_project, 'fixes') { false } end - it 'does not allow a maintainer of the target project to set `allow_maintainer_to_push`' do + it 'does not allow a maintainer of the target project to set `allow_collaboration`' do target_project.add_developer(user) - update_merge_request(allow_maintainer_to_push: true, title: 'Updated title') + update_merge_request(allow_collaboration: true, title: 'Updated title') expect(merge_request.title).to eq('Updated title') - expect(merge_request.allow_maintainer_to_push).to be_falsy + expect(merge_request.allow_collaboration).to be_falsy end it 'is allowed by a user that can push to the source and can update the merge request' do merge_request.update!(assignee: user) source_project.add_developer(user) - update_merge_request(allow_maintainer_to_push: true, title: 'Updated title') + update_merge_request(allow_collaboration: true, title: 'Updated title') expect(merge_request.title).to eq('Updated title') - expect(merge_request.allow_maintainer_to_push).to be_truthy + expect(merge_request.allow_collaboration).to be_truthy end end end diff --git a/spec/services/notification_recipient_service_spec.rb b/spec/services/notification_recipient_service_spec.rb new file mode 100644 index 00000000000..7f536ce4e68 --- /dev/null +++ b/spec/services/notification_recipient_service_spec.rb @@ -0,0 +1,36 @@ +require 'spec_helper' + +describe NotificationRecipientService do + let(:service) { described_class } + let(:assignee) { create(:user) } + let(:project) { create(:project, :public) } + let(:other_projects) { create_list(:project, 5, :public) } + + describe '#build_new_note_recipients' do + let(:issue) { create(:issue, project: project, assignees: [assignee]) } + let(:note) { create(:note_on_issue, noteable: issue, project_id: issue.project_id) } + + def create_watcher + watcher = create(:user) + create(:notification_setting, project: project, user: watcher, level: :watch) + + other_projects.each do |other_project| + create(:notification_setting, project: other_project, user: watcher, level: :watch) + end + end + + it 'avoids N+1 queries', :request_store do + Gitlab::GitalyClient.allow_n_plus_1_calls { create_watcher } + + service.build_new_note_recipients(note) + + control_count = ActiveRecord::QueryRecorder.new do + service.build_new_note_recipients(note) + end + + Gitlab::GitalyClient.allow_n_plus_1_calls { create_watcher } + + expect { service.build_new_note_recipients(note) }.not_to exceed_query_limit(control_count) + end + end +end diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb index 831678b949d..0eadc83bfe3 100644 --- a/spec/services/notification_service_spec.rb +++ b/spec/services/notification_service_spec.rb @@ -59,6 +59,20 @@ describe NotificationService, :mailer do should_email(participant) end + + context 'for subgroups', :nested_groups do + before do + build_group(project) + end + + it 'emails the participant' do + create(:note_on_issue, noteable: issuable, project_id: project.id, note: 'anything', author: @pg_participant) + + notification_trigger + + should_email_nested_group_user(@pg_participant) + end + end end shared_examples 'participating by assignee notification' do @@ -239,34 +253,56 @@ describe NotificationService, :mailer do end describe 'new note on issue in project that belongs to a group' do - let(:group) { create(:group) } - before do note.project.namespace_id = group.id - note.project.group.add_user(@u_watcher, GroupMember::MASTER) - note.project.group.add_user(@u_custom_global, GroupMember::MASTER) + group.add_user(@u_watcher, GroupMember::MASTER) + group.add_user(@u_custom_global, GroupMember::MASTER) note.project.save @u_watcher.notification_settings_for(note.project).participating! - @u_watcher.notification_settings_for(note.project.group).global! + @u_watcher.notification_settings_for(group).global! update_custom_notification(:new_note, @u_custom_global) reset_delivered_emails! end - it do - notification.new_note(note) + shared_examples 'new note notifications' do + it do + notification.new_note(note) + + should_email(note.noteable.author) + should_email(note.noteable.assignees.first) + should_email(@u_mentioned) + should_email(@u_custom_global) + should_not_email(@u_guest_custom) + should_not_email(@u_guest_watcher) + should_not_email(@u_watcher) + should_not_email(note.author) + should_not_email(@u_participating) + should_not_email(@u_disabled) + should_not_email(@u_lazy_participant) + end + end - should_email(note.noteable.author) - should_email(note.noteable.assignees.first) - should_email(@u_mentioned) - should_email(@u_custom_global) - should_not_email(@u_guest_custom) - should_not_email(@u_guest_watcher) - should_not_email(@u_watcher) - should_not_email(note.author) - should_not_email(@u_participating) - should_not_email(@u_disabled) - should_not_email(@u_lazy_participant) + let(:group) { create(:group) } + + it_behaves_like 'new note notifications' + + context 'which is a subgroup', :nested_groups do + let!(:parent) { create(:group) } + let!(:group) { create(:group, parent: parent) } + + it_behaves_like 'new note notifications' + + it 'overrides child objects with global level' do + user = create(:user) + parent.add_developer(user) + user.notification_settings_for(parent).watch! + reset_delivered_emails! + + notification.new_note(note) + + should_email(user) + end end end end @@ -301,6 +337,31 @@ describe NotificationService, :mailer do should_email(member) should_email(admin) end + + context 'on project that belongs to subgroup', :nested_groups do + let(:group_reporter) { create(:user) } + let(:group_guest) { create(:user) } + let(:parent_group) { create(:group) } + let(:child_group) { create(:group, parent: parent_group) } + let(:project) { create(:project, namespace: child_group) } + + context 'when user is group guest member' do + before do + parent_group.add_reporter(group_reporter) + parent_group.add_guest(group_guest) + group_guest.notification_settings_for(parent_group).watch! + group_reporter.notification_settings_for(parent_group).watch! + reset_delivered_emails! + end + + it 'does not email guest user' do + notification.new_note(note) + + should_email(group_reporter) + should_not_email(group_guest) + end + end + end end context 'issue note mention' do @@ -311,6 +372,7 @@ describe NotificationService, :mailer do before do build_team(note.project) + build_group(note.project) note.project.add_master(note.author) add_users_with_subscription(note.project, issue) reset_delivered_emails! @@ -336,10 +398,20 @@ describe NotificationService, :mailer do should_email(@u_guest_watcher) should_email(note.noteable.author) should_email(note.noteable.assignees.first) - should_not_email(note.author) + should_email_nested_group_user(@pg_watcher) should_email(@u_mentioned) - should_not_email(@u_disabled) should_email(@u_not_mentioned) + should_not_email(note.author) + should_not_email(@u_disabled) + should_not_email_nested_group_user(@pg_disabled) + end + + it 'notifies parent group members with mention level', :nested_groups do + note = create(:note_on_issue, noteable: issue, project_id: issue.project_id, note: "@#{@pg_mention.username}") + + notification.new_note(note) + + should_email_nested_group_user(@pg_mention) end it 'filters out "mentioned in" notes' do @@ -352,17 +424,18 @@ describe NotificationService, :mailer do end context 'project snippet note' do - let(:project) { create(:project, :public) } + let!(:project) { create(:project, :public) } let(:snippet) { create(:project_snippet, project: project, author: create(:user)) } - let(:note) { create(:note_on_project_snippet, noteable: snippet, project_id: snippet.project.id, note: '@all mentioned') } + let(:note) { create(:note_on_project_snippet, noteable: snippet, project_id: project.id, note: '@all mentioned') } before do - build_team(note.project) + build_team(project) + build_group(project) # make sure these users can read the project snippet! project.add_guest(@u_guest_watcher) project.add_guest(@u_guest_custom) - + add_member_for_parent_group(@pg_watcher, project) note.project.add_master(note.author) reset_delivered_emails! end @@ -370,7 +443,6 @@ describe NotificationService, :mailer do describe '#new_note' do it 'notifies the team members' do notification.new_note(note) - # Notify all team members note.project.team.members.each do |member| # User with disabled notification should not be notified @@ -449,6 +521,7 @@ describe NotificationService, :mailer do before do build_team(note.project) + build_group(project) reset_delivered_emails! allow(note.noteable).to receive(:author).and_return(@u_committer) update_custom_notification(:new_note, @u_guest_custom, resource: project) @@ -463,11 +536,13 @@ describe NotificationService, :mailer do should_email(@u_guest_custom) should_email(@u_committer) should_email(@u_watcher) + should_email_nested_group_user(@pg_watcher) should_not_email(@u_mentioned) should_not_email(note.author) should_not_email(@u_participating) should_not_email(@u_disabled) should_not_email(@u_lazy_participant) + should_not_email_nested_group_user(@pg_disabled) end it do @@ -478,10 +553,12 @@ describe NotificationService, :mailer do should_email(@u_committer) should_email(@u_watcher) should_email(@u_mentioned) + should_email_nested_group_user(@pg_watcher) should_not_email(note.author) should_not_email(@u_participating) should_not_email(@u_disabled) should_not_email(@u_lazy_participant) + should_not_email_nested_group_user(@pg_disabled) end it do @@ -548,10 +625,13 @@ describe NotificationService, :mailer do should_email(@g_global_watcher) should_email(@g_watcher) should_email(@unsubscribed_mentioned) + should_email_nested_group_user(@pg_watcher) should_not_email(@u_mentioned) should_not_email(@u_participating) should_not_email(@u_disabled) should_not_email(@u_lazy_participant) + should_not_email_nested_group_user(@pg_disabled) + should_not_email_nested_group_user(@pg_mention) end it do @@ -1922,19 +2002,69 @@ describe NotificationService, :mailer do # Users in the project's group but not part of project's team # with different notification settings def build_group(project) - group = create(:group, :public) - project.group = group + group = create_nested_group + project.update(namespace_id: group.id) # Group member: global=disabled, group=watch - @g_watcher = create_user_with_notification(:watch, 'group_watcher', project.group) + @g_watcher ||= create_user_with_notification(:watch, 'group_watcher', project.group) @g_watcher.notification_settings_for(nil).disabled! # Group member: global=watch, group=global - @g_global_watcher = create_global_setting_for(create(:user), :watch) + @g_global_watcher ||= create_global_setting_for(create(:user), :watch) group.add_users([@g_watcher, @g_global_watcher], :master) + group end + # Creates a nested group only if supported + # to avoid errors on MySQL + def create_nested_group + if Group.supports_nested_groups? + parent_group = create(:group, :public) + child_group = create(:group, :public, parent: parent_group) + + # Parent group member: global=disabled, parent_group=watch, child_group=global + @pg_watcher ||= create_user_with_notification(:watch, 'parent_group_watcher', parent_group) + @pg_watcher.notification_settings_for(nil).disabled! + + # Parent group member: global=global, parent_group=disabled, child_group=global + @pg_disabled ||= create_user_with_notification(:disabled, 'parent_group_disabled', parent_group) + @pg_disabled.notification_settings_for(nil).global! + + # Parent group member: global=global, parent_group=mention, child_group=global + @pg_mention ||= create_user_with_notification(:mention, 'parent_group_mention', parent_group) + @pg_mention.notification_settings_for(nil).global! + + # Parent group member: global=global, parent_group=participating, child_group=global + @pg_participant ||= create_user_with_notification(:participating, 'parent_group_participant', parent_group) + @pg_mention.notification_settings_for(nil).global! + + child_group + else + create(:group, :public) + end + end + + def add_member_for_parent_group(user, project) + return unless Group.supports_nested_groups? + + project.reload + + project.group.parent.add_master(user) + end + + def should_email_nested_group_user(user, times: 1, recipients: email_recipients) + return unless Group.supports_nested_groups? + + should_email(user, times: 1, recipients: email_recipients) + end + + def should_not_email_nested_group_user(user, recipients: email_recipients) + return unless Group.supports_nested_groups? + + should_not_email(user, recipients: email_recipients) + end + def add_users_with_subscription(project, issuable) @subscriber = create :user @unsubscriber = create :user diff --git a/spec/services/pages_service_spec.rb b/spec/services/pages_service_spec.rb deleted file mode 100644 index f8db6900a0a..00000000000 --- a/spec/services/pages_service_spec.rb +++ /dev/null @@ -1,53 +0,0 @@ -require 'spec_helper' - -describe PagesService do - let(:build) { create(:ci_build) } - let(:data) { Gitlab::DataBuilder::Build.build(build) } - let(:service) { described_class.new(data) } - - before do - allow(Gitlab.config.pages).to receive(:enabled).and_return(true) - end - - context 'execute asynchronously for pages job' do - before do - build.name = 'pages' - end - - context 'on success' do - before do - build.success - end - - it 'executes worker' do - expect(PagesWorker).to receive(:perform_async) - service.execute - end - end - - %w(pending running failed canceled).each do |status| - context "on #{status}" do - before do - build.status = status - end - - it 'does not execute worker' do - expect(PagesWorker).not_to receive(:perform_async) - service.execute - end - end - end - end - - context 'for other jobs' do - before do - build.name = 'other job' - build.success - end - - it 'does not execute worker' do - expect(PagesWorker).not_to receive(:perform_async) - service.execute - end - end -end diff --git a/spec/services/projects/after_import_service_spec.rb b/spec/services/projects/after_import_service_spec.rb index c6678fc1f5c..cd52bc88f4c 100644 --- a/spec/services/projects/after_import_service_spec.rb +++ b/spec/services/projects/after_import_service_spec.rb @@ -32,7 +32,7 @@ describe Projects::AfterImportService do end it 'removes refs/pull/**/*' do - expect(repository.rugged.references.map(&:name)) + expect(rugged.references.map(&:name)) .not_to include(%r{\Arefs/pull/}) end end @@ -46,10 +46,14 @@ describe Projects::AfterImportService do end it "does not remove refs/#{name}/tmp" do - expect(repository.rugged.references.map(&:name)) + expect(rugged.references.map(&:name)) .to include("refs/#{name}/tmp") end end end + + def rugged + Gitlab::GitalyClient::StorageSettings.allow_disk_access { repository.rugged } + end end end diff --git a/spec/services/projects/autocomplete_service_spec.rb b/spec/services/projects/autocomplete_service_spec.rb index f7ff8b80bd7..6fd73a50511 100644 --- a/spec/services/projects/autocomplete_service_spec.rb +++ b/spec/services/projects/autocomplete_service_spec.rb @@ -115,5 +115,20 @@ describe Projects::AutocompleteService do expect(milestone_titles).to eq([group_milestone2.title, group_milestone1.title]) end + + context 'with nested groups', :nested_groups do + let(:subgroup) { create(:group, :public, parent: group) } + let!(:subgroup_milestone) { create(:milestone, group: subgroup) } + + before do + project.update(namespace: subgroup) + end + + it 'includes project milestones and all acestors milestones' do + expect(milestone_titles).to match_array( + [project_milestone.title, group_milestone2.title, group_milestone1.title, subgroup_milestone.title] + ) + end + end end end diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb index a8f003b1073..e8cbf84e3be 100644 --- a/spec/services/projects/create_service_spec.rb +++ b/spec/services/projects/create_service_spec.rb @@ -272,8 +272,11 @@ describe Projects::CreateService, '#execute' do it 'writes project full path to .git/config' do project = create_project(user, opts) + rugged = Gitlab::GitalyClient::StorageSettings.allow_disk_access do + project.repository.rugged + end - expect(project.repository.rugged.config['gitlab.fullpath']).to eq project.full_path + expect(rugged.config['gitlab.fullpath']).to eq project.full_path end def create_project(user, opts) diff --git a/spec/services/projects/destroy_service_spec.rb b/spec/services/projects/destroy_service_spec.rb index b63f409579e..38660ad7a01 100644 --- a/spec/services/projects/destroy_service_spec.rb +++ b/spec/services/projects/destroy_service_spec.rb @@ -5,7 +5,11 @@ describe Projects::DestroyService do let!(:user) { create(:user) } let!(:project) { create(:project, :repository, namespace: user.namespace) } - let!(:path) { project.repository.path_to_repo } + let!(:path) do + Gitlab::GitalyClient::StorageSettings.allow_disk_access do + project.repository.path_to_repo + end + end let!(:remove_path) { path.sub(/\.git\Z/, "+#{project.id}+deleted.git") } let!(:async) { false } # execute or async_execute diff --git a/spec/services/projects/fork_service_spec.rb b/spec/services/projects/fork_service_spec.rb index a93f6f1ddc2..c15f5120b8a 100644 --- a/spec/services/projects/fork_service_spec.rb +++ b/spec/services/projects/fork_service_spec.rb @@ -8,7 +8,7 @@ describe Projects::ForkService do before do @from_user = create(:user) @from_namespace = @from_user.namespace - avatar = fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png") + avatar = fixture_file_upload("spec/fixtures/dk.png", "image/png") @from_project = create(:project, :repository, creator_id: @from_user.id, diff --git a/spec/services/projects/gitlab_projects_import_service_spec.rb b/spec/services/projects/gitlab_projects_import_service_spec.rb index ee1a886f5d6..0a898e9b89b 100644 --- a/spec/services/projects/gitlab_projects_import_service_spec.rb +++ b/spec/services/projects/gitlab_projects_import_service_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe Projects::GitlabProjectsImportService do set(:namespace) { create(:namespace) } let(:path) { 'test-path' } - let(:file) { fixture_file_upload(Rails.root + 'spec/fixtures/doc_sample.txt', 'text/plain') } + let(:file) { fixture_file_upload('spec/fixtures/doc_sample.txt', 'text/plain') } let(:overwrite) { false } let(:import_params) { { namespace_id: namespace.id, path: path, file: file, overwrite: overwrite } } subject { described_class.new(namespace.owner, import_params) } diff --git a/spec/services/projects/hashed_storage/migrate_repository_service_spec.rb b/spec/services/projects/hashed_storage/migrate_repository_service_spec.rb index 7dca81eb59e..ed4930313c5 100644 --- a/spec/services/projects/hashed_storage/migrate_repository_service_spec.rb +++ b/spec/services/projects/hashed_storage/migrate_repository_service_spec.rb @@ -37,7 +37,11 @@ describe Projects::HashedStorage::MigrateRepositoryService do it 'writes project full path to .git/config' do service.execute - expect(project.repository.rugged.config['gitlab.fullpath']).to eq project.full_path + rugged_config = Gitlab::GitalyClient::StorageSettings.allow_disk_access do + project.repository.rugged.config['gitlab.fullpath'] + end + + expect(rugged_config).to eq project.full_path end end diff --git a/spec/services/projects/housekeeping_service_spec.rb b/spec/services/projects/housekeeping_service_spec.rb index b7b5de07380..1cf373d1d72 100644 --- a/spec/services/projects/housekeeping_service_spec.rb +++ b/spec/services/projects/housekeeping_service_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' describe Projects::HousekeepingService do subject { described_class.new(project) } - let(:project) { create(:project, :repository) } + set(:project) { create(:project, :repository) } before do project.reset_pushes_since_gc @@ -16,12 +16,12 @@ describe Projects::HousekeepingService do it 'enqueues a sidekiq job' do expect(subject).to receive(:try_obtain_lease).and_return(:the_uuid) expect(subject).to receive(:lease_key).and_return(:the_lease_key) - expect(subject).to receive(:task).and_return(:the_task) - expect(GitGarbageCollectWorker).to receive(:perform_async).with(project.id, :the_task, :the_lease_key, :the_uuid) + expect(subject).to receive(:task).and_return(:incremental_repack) + expect(GitGarbageCollectWorker).to receive(:perform_async).with(project.id, :incremental_repack, :the_lease_key, :the_uuid).and_call_original - subject.execute - - expect(project.reload.pushes_since_gc).to eq(0) + Sidekiq::Testing.fake! do + expect { subject.execute }.to change(GitGarbageCollectWorker.jobs, :size).by(1) + end end it 'yields the block if given' do @@ -30,6 +30,16 @@ describe Projects::HousekeepingService do end.to yield_with_no_args end + it 'resets counter after execution' do + expect(subject).to receive(:try_obtain_lease).and_return(:the_uuid) + allow(subject).to receive(:gc_period).and_return(1) + project.increment_pushes_since_gc + + Sidekiq::Testing.inline! do + expect { subject.execute }.to change { project.pushes_since_gc }.to(0) + end + end + context 'when no lease can be obtained' do before do expect(subject).to receive(:try_obtain_lease).and_return(false) @@ -54,6 +64,30 @@ describe Projects::HousekeepingService do end.not_to yield_with_no_args end end + + context 'task type' do + it 'goes through all three housekeeping tasks, executing only the highest task when there is overlap' do + allow(subject).to receive(:try_obtain_lease).and_return(:the_uuid) + allow(subject).to receive(:lease_key).and_return(:the_lease_key) + + # At push 200 + expect(GitGarbageCollectWorker).to receive(:perform_async).with(project.id, :gc, :the_lease_key, :the_uuid) + .exactly(1).times + # At push 50, 100, 150 + expect(GitGarbageCollectWorker).to receive(:perform_async).with(project.id, :full_repack, :the_lease_key, :the_uuid) + .exactly(3).times + # At push 10, 20, ... (except those above) + expect(GitGarbageCollectWorker).to receive(:perform_async).with(project.id, :incremental_repack, :the_lease_key, :the_uuid) + .exactly(16).times + + 201.times do + subject.increment! + subject.execute if subject.needed? + end + + expect(project.pushes_since_gc).to eq(1) + end + end end describe '#needed?' do @@ -69,31 +103,7 @@ describe Projects::HousekeepingService do describe '#increment!' do it 'increments the pushes_since_gc counter' do - expect do - subject.increment! - end.to change { project.pushes_since_gc }.from(0).to(1) + expect { subject.increment! }.to change { project.pushes_since_gc }.by(1) end end - - it 'goes through all three housekeeping tasks, executing only the highest task when there is overlap' do - allow(subject).to receive(:try_obtain_lease).and_return(:the_uuid) - allow(subject).to receive(:lease_key).and_return(:the_lease_key) - - # At push 200 - expect(GitGarbageCollectWorker).to receive(:perform_async).with(project.id, :gc, :the_lease_key, :the_uuid) - .exactly(1).times - # At push 50, 100, 150 - expect(GitGarbageCollectWorker).to receive(:perform_async).with(project.id, :full_repack, :the_lease_key, :the_uuid) - .exactly(3).times - # At push 10, 20, ... (except those above) - expect(GitGarbageCollectWorker).to receive(:perform_async).with(project.id, :incremental_repack, :the_lease_key, :the_uuid) - .exactly(16).times - - 201.times do - subject.increment! - subject.execute if subject.needed? - end - - expect(project.pushes_since_gc).to eq(1) - end end diff --git a/spec/services/projects/import_service_spec.rb b/spec/services/projects/import_service_spec.rb index 30c89ebd821..b3815045792 100644 --- a/spec/services/projects/import_service_spec.rb +++ b/spec/services/projects/import_service_spec.rb @@ -3,9 +3,17 @@ require 'spec_helper' describe Projects::ImportService do let!(:project) { create(:project) } let(:user) { project.creator } + let(:import_url) { 'http://www.gitlab.com/demo/repo.git' } + let(:oid_download_links) { { 'oid1' => "#{import_url}/gitlab-lfs/objects/oid1", 'oid2' => "#{import_url}/gitlab-lfs/objects/oid2" } } subject { described_class.new(project, user) } + before do + allow(project).to receive(:lfs_enabled?).and_return(true) + allow_any_instance_of(Projects::LfsPointers::LfsDownloadService).to receive(:execute) + allow_any_instance_of(Projects::LfsPointers::LfsImportService).to receive(:execute).and_return(oid_download_links) + end + describe '#async?' do it 'returns true for an asynchronous importer' do importer_class = double(:importer, async?: true) @@ -63,6 +71,15 @@ describe Projects::ImportService do expect(result[:status]).to eq :error expect(result[:message]).to eq "Error importing repository #{project.import_url} into #{project.full_path} - The repository could not be created." end + + context 'when repository creation succeeds' do + it 'does not download lfs files' do + expect_any_instance_of(Projects::LfsPointers::LfsImportService).not_to receive(:execute) + expect_any_instance_of(Projects::LfsPointers::LfsDownloadService).not_to receive(:execute) + + subject.execute + end + end end context 'with known url' do @@ -91,6 +108,15 @@ describe Projects::ImportService do expect(result[:status]).to eq :error end + + context 'when repository import scheduled' do + it 'does not download lfs objects' do + expect_any_instance_of(Projects::LfsPointers::LfsImportService).not_to receive(:execute) + expect_any_instance_of(Projects::LfsPointers::LfsDownloadService).not_to receive(:execute) + + subject.execute + end + end end context 'with a non Github repository' do @@ -99,9 +125,10 @@ describe Projects::ImportService do project.import_type = 'bitbucket' end - it 'succeeds if repository import is successfully' do + it 'succeeds if repository import is successfull' do expect_any_instance_of(Gitlab::Shell).to receive(:import_repository).and_return(true) expect_any_instance_of(Gitlab::BitbucketImport::Importer).to receive(:execute).and_return(true) + expect_any_instance_of(Projects::LfsPointers::LfsImportService).to receive(:execute).and_return({}) result = subject.execute @@ -116,6 +143,29 @@ describe Projects::ImportService do expect(result[:status]).to eq :error expect(result[:message]).to eq "Error importing repository #{project.import_url} into #{project.full_path} - Failed to import the repository" end + + context 'when repository import scheduled' do + before do + allow_any_instance_of(Gitlab::Shell).to receive(:import_repository).and_return(true) + allow(subject).to receive(:import_data) + end + + it 'downloads lfs objects if lfs_enabled is enabled for project' do + allow(project).to receive(:lfs_enabled?).and_return(true) + expect_any_instance_of(Projects::LfsPointers::LfsImportService).to receive(:execute).and_return(oid_download_links) + expect_any_instance_of(Projects::LfsPointers::LfsDownloadService).to receive(:execute).twice + + subject.execute + end + + it 'does not download lfs objects if lfs_enabled is not enabled for project' do + allow(project).to receive(:lfs_enabled?).and_return(false) + expect_any_instance_of(Projects::LfsPointers::LfsImportService).not_to receive(:execute) + expect_any_instance_of(Projects::LfsPointers::LfsDownloadService).not_to receive(:execute) + + subject.execute + end + end end end @@ -147,6 +197,26 @@ describe Projects::ImportService do expect(result[:status]).to eq :error end + + context 'when importer' do + it 'has a custom repository importer it does not download lfs objects' do + allow(Gitlab::GithubImport::ParallelImporter).to receive(:imports_repository?).and_return(true) + + expect_any_instance_of(Projects::LfsPointers::LfsImportService).not_to receive(:execute) + expect_any_instance_of(Projects::LfsPointers::LfsDownloadService).not_to receive(:execute) + + subject.execute + end + + it 'does not have a custom repository importer downloads lfs objects' do + allow(Gitlab::GithubImport::ParallelImporter).to receive(:imports_repository?).and_return(false) + + expect_any_instance_of(Projects::LfsPointers::LfsImportService).to receive(:execute).and_return(oid_download_links) + expect_any_instance_of(Projects::LfsPointers::LfsDownloadService).to receive(:execute) + + subject.execute + end + end end context 'with blocked import_URL' do diff --git a/spec/services/projects/lfs_pointers/lfs_download_link_list_service_spec.rb b/spec/services/projects/lfs_pointers/lfs_download_link_list_service_spec.rb new file mode 100644 index 00000000000..d7a2829d5f8 --- /dev/null +++ b/spec/services/projects/lfs_pointers/lfs_download_link_list_service_spec.rb @@ -0,0 +1,102 @@ +require 'spec_helper' + +describe Projects::LfsPointers::LfsDownloadLinkListService do + let(:import_url) { 'http://www.gitlab.com/demo/repo.git' } + let(:lfs_endpoint) { "#{import_url}/info/lfs/objects/batch" } + let!(:project) { create(:project, import_url: import_url) } + let(:new_oids) { { 'oid1' => 123, 'oid2' => 125 } } + let(:remote_uri) { URI.parse(lfs_endpoint) } + + let(:objects_response) do + body = new_oids.map do |oid, size| + { + 'oid' => oid, + 'size' => size, + 'actions' => { + 'download' => { 'href' => "#{import_url}/gitlab-lfs/objects/#{oid}" } + } + } + end + + Struct.new(:success?, :objects).new(true, body) + end + + let(:invalid_object_response) do + [ + 'oid' => 'whatever', + 'size' => 123 + ] + end + + subject { described_class.new(project, remote_uri: remote_uri) } + + before do + allow(project).to receive(:lfs_enabled?).and_return(true) + allow(Gitlab::HTTP).to receive(:post).and_return(objects_response) + end + + describe '#execute' do + it 'retrieves each download link of every non existent lfs object' do + subject.execute(new_oids).each do |oid, link| + expect(link).to eq "#{import_url}/gitlab-lfs/objects/#{oid}" + end + end + + context 'credentials' do + context 'when the download link and the lfs_endpoint have the same host' do + context 'when lfs_endpoint has credentials' do + let(:import_url) { 'http://user:password@www.gitlab.com/demo/repo.git' } + + it 'adds credentials to the download_link' do + result = subject.execute(new_oids) + + result.each do |oid, link| + expect(link.starts_with?('http://user:password@')).to be_truthy + end + end + end + + context 'when lfs_endpoint does not have any credentials' do + it 'does not add any credentials' do + result = subject.execute(new_oids) + + result.each do |oid, link| + expect(link.starts_with?('http://user:password@')).to be_falsey + end + end + end + end + + context 'when the download link and the lfs_endpoint have different hosts' do + let(:import_url_with_credentials) { 'http://user:password@www.otherdomain.com/demo/repo.git' } + let(:lfs_endpoint) { "#{import_url_with_credentials}/info/lfs/objects/batch" } + + it 'downloads without any credentials' do + result = subject.execute(new_oids) + + result.each do |oid, link| + expect(link.starts_with?('http://user:password@')).to be_falsey + end + end + end + end + end + + describe '#get_download_links' do + it 'raise errorif request fails' do + allow(Gitlab::HTTP).to receive(:post).and_return(Struct.new(:success?, :message).new(false, 'Failed request')) + + expect { subject.send(:get_download_links, new_oids) }.to raise_error(described_class::DownloadLinksError) + end + end + + describe '#parse_response_links' do + it 'does not add oid entry if href not found' do + expect(Rails.logger).to receive(:error).with("Link for Lfs Object with oid whatever not found or invalid.") + + result = subject.send(:parse_response_links, invalid_object_response) + + expect(result).to be_empty + end + end +end diff --git a/spec/services/projects/lfs_pointers/lfs_download_service_spec.rb b/spec/services/projects/lfs_pointers/lfs_download_service_spec.rb new file mode 100644 index 00000000000..6af5bfc7689 --- /dev/null +++ b/spec/services/projects/lfs_pointers/lfs_download_service_spec.rb @@ -0,0 +1,69 @@ +require 'spec_helper' + +describe Projects::LfsPointers::LfsDownloadService do + let(:project) { create(:project) } + let(:oid) { '9e548e25631dd9ce6b43afd6359ab76da2819d6a5b474e66118c7819e1d8b3e8' } + let(:download_link) { "http://gitlab.com/#{oid}" } + let(:lfs_content) do + <<~HEREDOC + whatever + HEREDOC + end + + subject { described_class.new(project) } + + before do + allow(project).to receive(:lfs_enabled?).and_return(true) + WebMock.stub_request(:get, download_link).to_return(body: lfs_content) + end + + describe '#execute' do + context 'when file download succeeds' do + it 'a new lfs object is created' do + expect { subject.execute(oid, download_link) }.to change { LfsObject.count }.from(0).to(1) + end + + it 'has the same oid' do + subject.execute(oid, download_link) + + expect(LfsObject.first.oid).to eq oid + end + + it 'stores the content' do + subject.execute(oid, download_link) + + expect(File.read(LfsObject.first.file.file.file)).to eq lfs_content + end + end + + context 'when file download fails' do + it 'no lfs object is created' do + expect { subject.execute(oid, download_link) }.to change { LfsObject.count } + end + end + + context 'when credentials present' do + let(:download_link_with_credentials) { "http://user:password@gitlab.com/#{oid}" } + + before do + WebMock.stub_request(:get, download_link).with(headers: { 'Authorization' => 'Basic dXNlcjpwYXNzd29yZA==' }).to_return(body: lfs_content) + end + + it 'the request adds authorization headers' do + subject.execute(oid, download_link_with_credentials) + end + end + + context 'when an lfs object with the same oid already exists' do + before do + create(:lfs_object, oid: 'oid') + end + + it 'does not download the file' do + expect(subject).not_to receive(:download_and_save_file) + + subject.execute('oid', download_link) + end + end + end +end diff --git a/spec/services/projects/lfs_pointers/lfs_import_service_spec.rb b/spec/services/projects/lfs_pointers/lfs_import_service_spec.rb new file mode 100644 index 00000000000..5a75fb38dec --- /dev/null +++ b/spec/services/projects/lfs_pointers/lfs_import_service_spec.rb @@ -0,0 +1,146 @@ +require 'spec_helper' + +describe Projects::LfsPointers::LfsImportService do + let(:import_url) { 'http://www.gitlab.com/demo/repo.git' } + let(:default_endpoint) { "#{import_url}/info/lfs/objects/batch"} + let(:group) { create(:group, lfs_enabled: true)} + let!(:project) { create(:project, namespace: group, import_url: import_url, lfs_enabled: true) } + let!(:lfs_objects_project) { create_list(:lfs_objects_project, 2, project: project) } + let!(:existing_lfs_objects) { LfsObject.pluck(:oid, :size).to_h } + let(:oids) { { 'oid1' => 123, 'oid2' => 125 } } + let(:oid_download_links) { { 'oid1' => "#{import_url}/gitlab-lfs/objects/oid1", 'oid2' => "#{import_url}/gitlab-lfs/objects/oid2" } } + let(:all_oids) { existing_lfs_objects.merge(oids) } + let(:remote_uri) { URI.parse(lfs_endpoint) } + + subject { described_class.new(project) } + + before do + allow(project.repository).to receive(:lfsconfig_for).and_return(nil) + allow(Gitlab.config.lfs).to receive(:enabled).and_return(true) + allow_any_instance_of(Projects::LfsPointers::LfsListService).to receive(:execute).and_return(all_oids) + end + + describe '#execute' do + context 'when no lfs pointer is linked' do + before do + allow_any_instance_of(Projects::LfsPointers::LfsLinkService).to receive(:execute).and_return([]) + allow_any_instance_of(Projects::LfsPointers::LfsDownloadLinkListService).to receive(:execute).and_return(oid_download_links) + expect(Projects::LfsPointers::LfsDownloadLinkListService).to receive(:new).with(project, remote_uri: URI.parse(default_endpoint)).and_call_original + end + + it 'retrieves all lfs pointers in the project repository' do + expect_any_instance_of(Projects::LfsPointers::LfsListService).to receive(:execute) + + subject.execute + end + + it 'links existent lfs objects to the project' do + expect_any_instance_of(Projects::LfsPointers::LfsLinkService).to receive(:execute) + + subject.execute + end + + it 'retrieves the download links of non existent objects' do + expect_any_instance_of(Projects::LfsPointers::LfsDownloadLinkListService).to receive(:execute).with(all_oids) + + subject.execute + end + end + + context 'when some lfs objects are linked' do + before do + allow_any_instance_of(Projects::LfsPointers::LfsLinkService).to receive(:execute).and_return(existing_lfs_objects.keys) + allow_any_instance_of(Projects::LfsPointers::LfsDownloadLinkListService).to receive(:execute).and_return(oid_download_links) + end + + it 'retrieves the download links of non existent objects' do + expect_any_instance_of(Projects::LfsPointers::LfsDownloadLinkListService).to receive(:execute).with(oids) + + subject.execute + end + end + + context 'when all lfs objects are linked' do + before do + allow_any_instance_of(Projects::LfsPointers::LfsLinkService).to receive(:execute).and_return(all_oids.keys) + allow_any_instance_of(Projects::LfsPointers::LfsDownloadLinkListService).to receive(:execute) + end + + it 'retrieves no download links' do + expect_any_instance_of(Projects::LfsPointers::LfsDownloadLinkListService).to receive(:execute).with({}).and_call_original + + expect(subject.execute).to be_empty + end + end + + context 'when lfsconfig file exists' do + before do + allow(project.repository).to receive(:lfsconfig_for).and_return("[lfs]\n\turl = #{lfs_endpoint}\n") + end + + context 'when url points to the same import url host' do + let(:lfs_endpoint) { "#{import_url}/different_endpoint" } + let(:service) { double } + + before do + allow(service).to receive(:execute) + end + it 'downloads lfs object using the new endpoint' do + expect(Projects::LfsPointers::LfsDownloadLinkListService).to receive(:new).with(project, remote_uri: remote_uri).and_return(service) + + subject.execute + end + + context 'when import url has credentials' do + let(:import_url) { 'http://user:password@www.gitlab.com/demo/repo.git'} + + it 'adds the credentials to the new endpoint' do + expect(Projects::LfsPointers::LfsDownloadLinkListService) + .to receive(:new).with(project, remote_uri: URI.parse("http://user:password@www.gitlab.com/demo/repo.git/different_endpoint")) + .and_return(service) + + subject.execute + end + + context 'when url has its own credentials' do + let(:lfs_endpoint) { "http://user1:password1@www.gitlab.com/demo/repo.git/different_endpoint" } + + it 'does not add the import url credentials' do + expect(Projects::LfsPointers::LfsDownloadLinkListService) + .to receive(:new).with(project, remote_uri: remote_uri) + .and_return(service) + + subject.execute + end + end + end + end + + context 'when url points to a third party service' do + let(:lfs_endpoint) { 'http://third_party_service.com/info/lfs/objects/' } + + it 'disables lfs from the project' do + expect(project.lfs_enabled?).to be_truthy + + subject.execute + + expect(project.lfs_enabled?).to be_falsey + end + + it 'does not download anything' do + expect_any_instance_of(Projects::LfsPointers::LfsListService).not_to receive(:execute) + + subject.execute + end + end + end + end + + describe '#default_endpoint_uri' do + let(:import_url) { 'http://www.gitlab.com/demo/repo' } + + it 'adds suffix .git if the url does not have it' do + expect(subject.send(:default_endpoint_uri).path).to match(/repo.git/) + end + end +end diff --git a/spec/services/projects/lfs_pointers/lfs_link_service_spec.rb b/spec/services/projects/lfs_pointers/lfs_link_service_spec.rb new file mode 100644 index 00000000000..b7b153655db --- /dev/null +++ b/spec/services/projects/lfs_pointers/lfs_link_service_spec.rb @@ -0,0 +1,33 @@ +require 'spec_helper' + +describe Projects::LfsPointers::LfsLinkService do + let!(:project) { create(:project, lfs_enabled: true) } + let!(:lfs_objects_project) { create_list(:lfs_objects_project, 2, project: project) } + let(:new_oids) { { 'oid1' => 123, 'oid2' => 125 } } + let(:all_oids) { LfsObject.pluck(:oid, :size).to_h.merge(new_oids) } + let(:new_lfs_object) { create(:lfs_object) } + let(:new_oid_list) { all_oids.merge(new_lfs_object.oid => new_lfs_object.size) } + + subject { described_class.new(project) } + + before do + allow(project).to receive(:lfs_enabled?).and_return(true) + end + + describe '#execute' do + it 'links existing lfs objects to the project' do + expect(project.all_lfs_objects.count).to eq 2 + + linked = subject.execute(new_oid_list.keys) + + expect(project.all_lfs_objects.count).to eq 3 + expect(linked.size).to eq 3 + end + + it 'returns linked oids' do + linked = lfs_objects_project.map(&:lfs_object).map(&:oid) << new_lfs_object.oid + + expect(subject.execute(new_oid_list.keys)).to eq linked + end + end +end diff --git a/spec/services/projects/participants_service_spec.rb b/spec/services/projects/participants_service_spec.rb index 0d18ceb8ff8..6040f9100f8 100644 --- a/spec/services/projects/participants_service_spec.rb +++ b/spec/services/projects/participants_service_spec.rb @@ -4,7 +4,7 @@ describe Projects::ParticipantsService do describe '#groups' do describe 'avatar_url' do let(:project) { create(:project, :public) } - let(:group) { create(:group, avatar: fixture_file_upload(Rails.root + 'spec/fixtures/dk.png')) } + let(:group) { create(:group, avatar: fixture_file_upload('spec/fixtures/dk.png')) } let(:user) { create(:user) } let!(:group_member) { create(:group_member, group: group, user: user) } diff --git a/spec/services/projects/transfer_service_spec.rb b/spec/services/projects/transfer_service_spec.rb index 3e6483d7e28..5100987c2fe 100644 --- a/spec/services/projects/transfer_service_spec.rb +++ b/spec/services/projects/transfer_service_spec.rb @@ -64,7 +64,7 @@ describe Projects::TransferService do it 'updates project full path in .git/config' do transfer_project(project, user, group) - expect(project.repository.rugged.config['gitlab.fullpath']).to eq "#{group.full_path}/#{project.path}" + expect(rugged_config['gitlab.fullpath']).to eq "#{group.full_path}/#{project.path}" end end @@ -84,7 +84,9 @@ describe Projects::TransferService do end def project_path(project) - project.repository.path_to_repo + Gitlab::GitalyClient::StorageSettings.allow_disk_access do + project.repository.path_to_repo + end end def current_path @@ -101,7 +103,7 @@ describe Projects::TransferService do it 'rolls back project full path in .git/config' do attempt_project_transfer - expect(project.repository.rugged.config['gitlab.fullpath']).to eq project.full_path + expect(rugged_config['gitlab.fullpath']).to eq project.full_path end it "doesn't send move notifications" do @@ -264,4 +266,10 @@ describe Projects::TransferService do transfer_project(project, owner, group) end end + + def rugged_config + Gitlab::GitalyClient::StorageSettings.allow_disk_access do + project.repository.rugged.config + end + end end diff --git a/spec/services/projects/update_pages_service_spec.rb b/spec/services/projects/update_pages_service_spec.rb index 347ac13828c..1bffeee6790 100644 --- a/spec/services/projects/update_pages_service_spec.rb +++ b/spec/services/projects/update_pages_service_spec.rb @@ -4,13 +4,13 @@ describe Projects::UpdatePagesService do set(:project) { create(:project, :repository) } set(:pipeline) { create(:ci_pipeline, project: project, sha: project.commit('HEAD').sha) } set(:build) { create(:ci_build, pipeline: pipeline, ref: 'HEAD') } - let(:invalid_file) { fixture_file_upload(Rails.root + 'spec/fixtures/dk.png') } + let(:invalid_file) { fixture_file_upload('spec/fixtures/dk.png') } let(:extension) { 'zip' } - let(:file) { fixture_file_upload(Rails.root + "spec/fixtures/pages.#{extension}") } - let(:empty_file) { fixture_file_upload(Rails.root + "spec/fixtures/pages_empty.#{extension}") } + let(:file) { fixture_file_upload("spec/fixtures/pages.#{extension}") } + let(:empty_file) { fixture_file_upload("spec/fixtures/pages_empty.#{extension}") } let(:metadata) do - filename = Rails.root + "spec/fixtures/pages.#{extension}.meta" + filename = "spec/fixtures/pages.#{extension}.meta" fixture_file_upload(filename) if File.exist?(filename) end @@ -196,8 +196,8 @@ describe Projects::UpdatePagesService do let(:metadata) { spy('metadata') } before do - file = fixture_file_upload(Rails.root + 'spec/fixtures/pages.zip') - metafile = fixture_file_upload(Rails.root + 'spec/fixtures/pages.zip.meta') + file = fixture_file_upload('spec/fixtures/pages.zip') + metafile = fixture_file_upload('spec/fixtures/pages.zip.meta') build.update_attributes(legacy_artifacts_file: file) build.update_attributes(legacy_artifacts_metadata: metafile) diff --git a/spec/services/projects/update_service_spec.rb b/spec/services/projects/update_service_spec.rb index 3e6073b9861..ecf1ba05618 100644 --- a/spec/services/projects/update_service_spec.rb +++ b/spec/services/projects/update_service_spec.rb @@ -125,7 +125,7 @@ describe Projects::UpdateService do context 'when we update project but not enabling a wiki' do it 'does not try to create an empty wiki' do - FileUtils.rm_rf(project.wiki.repository.path) + Gitlab::Shell.new.rm_directory(project.repository_storage, project.wiki.path) result = update_project(project, user, { name: 'test1' }) @@ -146,7 +146,7 @@ describe Projects::UpdateService do context 'when enabling a wiki' do it 'creates a wiki' do project.project_feature.update(wiki_access_level: ProjectFeature::DISABLED) - FileUtils.rm_rf(project.wiki.repository.path) + Gitlab::Shell.new.rm_directory(project.repository_storage, project.wiki.path) result = update_project(project, user, project_feature_attributes: { wiki_access_level: ProjectFeature::ENABLED }) @@ -275,6 +275,10 @@ describe Projects::UpdateService do it { is_expected.to eq(false) } end + context 'when auto devops is nil' do + it { is_expected.to eq(false) } + end + context 'when auto devops is explicitly enabled' do before do project.create_auto_devops!(enabled: true) diff --git a/spec/services/quick_actions/interpret_service_spec.rb b/spec/services/quick_actions/interpret_service_spec.rb index bd835a1fca6..743e281183e 100644 --- a/spec/services/quick_actions/interpret_service_spec.rb +++ b/spec/services/quick_actions/interpret_service_spec.rb @@ -323,6 +323,14 @@ describe QuickActions::InterpretService do end end + shared_examples 'confidential command' do + it 'marks issue as confidential if content contains /confidential' do + _, updates = service.execute(content, issuable) + + expect(updates).to eq(confidential: true) + end + end + shared_examples 'shrug command' do it 'appends ¯\_(ツ)_/¯ to the comment' do new_content, _ = service.execute(content, issuable) @@ -774,6 +782,11 @@ describe QuickActions::InterpretService do let(:issuable) { issue } end + it_behaves_like 'confidential command' do + let(:content) { '/confidential' } + let(:issuable) { issue } + end + context '/copy_metadata command' do let(:todo_label) { create(:label, project: project, title: 'To Do') } let(:inreview_label) { create(:label, project: project, title: 'In Review') } @@ -919,6 +932,11 @@ describe QuickActions::InterpretService do end it_behaves_like 'empty command' do + let(:content) { '/confidential' } + let(:issuable) { issue } + end + + it_behaves_like 'empty command' do let(:content) { '/duplicate #{issue.to_reference}' } let(:issuable) { issue } end diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb index e28b0ea5cf2..57d081cffb3 100644 --- a/spec/services/system_note_service_spec.rb +++ b/spec/services/system_note_service_spec.rb @@ -3,6 +3,7 @@ require 'spec_helper' describe SystemNoteService do include Gitlab::Routing include RepoHelpers + include AssetsHelpers set(:group) { create(:group) } set(:project) { create(:project, :repository, group: group) } @@ -769,6 +770,8 @@ describe SystemNoteService do end describe "new reference" do + let(:favicon_path) { "http://localhost/assets/#{find_asset('favicon.png').digest_path}" } + before do allow(JIRA::Resource::Remotelink).to receive(:all).and_return([]) end @@ -789,7 +792,7 @@ describe SystemNoteService do object: { url: project_commit_url(project, commit), title: "GitLab: Mentioned on commit - #{commit.title}", - icon: { title: "GitLab", url16x16: "http://localhost/favicon.ico" }, + icon: { title: "GitLab", url16x16: favicon_path }, status: { resolved: false } } ) @@ -815,7 +818,7 @@ describe SystemNoteService do object: { url: project_issue_url(project, issue), title: "GitLab: Mentioned on issue - #{issue.title}", - icon: { title: "GitLab", url16x16: "http://localhost/favicon.ico" }, + icon: { title: "GitLab", url16x16: favicon_path }, status: { resolved: false } } ) @@ -841,7 +844,7 @@ describe SystemNoteService do object: { url: project_snippet_url(project, snippet), title: "GitLab: Mentioned on snippet - #{snippet.title}", - icon: { title: "GitLab", url16x16: "http://localhost/favicon.ico" }, + icon: { title: "GitLab", url16x16: favicon_path }, status: { resolved: false } } ) diff --git a/spec/services/tags/create_service_spec.rb b/spec/services/tags/create_service_spec.rb index e7e9080b6b0..0cbe57352be 100644 --- a/spec/services/tags/create_service_spec.rb +++ b/spec/services/tags/create_service_spec.rb @@ -41,7 +41,7 @@ describe Tags::CreateService do it 'returns an error' do expect(repository).to receive(:add_tag) .with(user, 'v1.1.0', 'master', 'Foo') - .and_raise(Gitlab::Git::HooksService::PreReceiveError, 'something went wrong') + .and_raise(Gitlab::Git::PreReceiveError, 'something went wrong') response = service.execute('v1.1.0', 'master', 'Foo') diff --git a/spec/services/test_hooks/project_service_spec.rb b/spec/services/test_hooks/project_service_spec.rb index 962b9f40c4f..19e1c5ff3b2 100644 --- a/spec/services/test_hooks/project_service_spec.rb +++ b/spec/services/test_hooks/project_service_spec.rb @@ -6,13 +6,19 @@ describe TestHooks::ProjectService do describe '#execute' do let(:project) { create(:project, :repository) } let(:hook) { create(:project_hook, project: project) } + let(:trigger) { 'not_implemented_events' } let(:service) { described_class.new(hook, current_user, trigger) } let(:sample_data) { { data: 'sample' } } let(:success_result) { { status: :success, http_status: 200, message: 'ok' } } - context 'hook with not implemented test' do - let(:trigger) { 'not_implemented_events' } + it 'allows to set a custom project' do + project = double + service.project = project + + expect(service.project).to eq(project) + end + context 'hook with not implemented test' do it 'returns error message' do expect(hook).not_to receive(:execute) expect(service.execute).to include({ status: :error, message: 'Testing not available for this hook' }) diff --git a/spec/services/upload_service_spec.rb b/spec/services/upload_service_spec.rb index 24f3a5c5ff0..9b232a52efa 100644 --- a/spec/services/upload_service_spec.rb +++ b/spec/services/upload_service_spec.rb @@ -9,7 +9,7 @@ describe UploadService do context 'for valid gif file' do before do - gif = fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif') + gif = fixture_file_upload('spec/fixtures/banana_sample.gif', 'image/gif') @link_to_file = upload_file(@project, gif) end @@ -21,7 +21,7 @@ describe UploadService do context 'for valid png file' do before do - png = fixture_file_upload(Rails.root + 'spec/fixtures/dk.png', + png = fixture_file_upload('spec/fixtures/dk.png', 'image/png') @link_to_file = upload_file(@project, png) end @@ -34,7 +34,7 @@ describe UploadService do context 'for valid jpg file' do before do - jpg = fixture_file_upload(Rails.root + 'spec/fixtures/rails_sample.jpg', 'image/jpg') + jpg = fixture_file_upload('spec/fixtures/rails_sample.jpg', 'image/jpg') @link_to_file = upload_file(@project, jpg) end @@ -46,7 +46,7 @@ describe UploadService do context 'for txt file' do before do - txt = fixture_file_upload(Rails.root + 'spec/fixtures/doc_sample.txt', 'text/plain') + txt = fixture_file_upload('spec/fixtures/doc_sample.txt', 'text/plain') @link_to_file = upload_file(@project, txt) end @@ -58,7 +58,7 @@ describe UploadService do context 'for too large a file' do before do - txt = fixture_file_upload(Rails.root + 'spec/fixtures/doc_sample.txt', 'text/plain') + txt = fixture_file_upload('spec/fixtures/doc_sample.txt', 'text/plain') allow(txt).to receive(:size) { 1000.megabytes.to_i } @link_to_file = upload_file(@project, txt) end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index d3de2331244..dac609e2545 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -87,6 +87,7 @@ RSpec.configure do |config| config.include LiveDebugger, :js config.include MigrationsHelpers, :migration config.include RedisHelpers + config.include Rails.application.routes.url_helpers, type: :routing if ENV['CI'] # This includes the first try, i.e. tests will be run 4 times before failing. @@ -107,19 +108,6 @@ RSpec.configure do |config| end config.before(:example) do - # Skip pre-receive hook check so we can use the web editor and merge. - allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([true, nil]) - - allow_any_instance_of(Gitlab::Git::GitlabProjects).to receive(:fork_repository).and_wrap_original do |m, *args| - m.call(*args) - - shard_name, repository_relative_path = args - # We can't leave the hooks in place after a fork, as those would fail in tests - # The "internal" API is not available - Gitlab::Shell.new.rm_directory(shard_name, - File.join(repository_relative_path, 'hooks')) - end - # Enable all features by default for testing allow(Feature).to receive(:enabled?) { true } end @@ -133,6 +121,10 @@ RSpec.configure do |config| RequestStore.clear! end + config.after(:example) do + Fog.unmock! if Fog.mock? + end + config.before(:example, :mailer) do reset_delivered_emails! end diff --git a/spec/support/api/scopes/read_user_shared_examples.rb b/spec/support/api/scopes/read_user_shared_examples.rb index 06ae8792c61..d7cef137989 100644 --- a/spec/support/api/scopes/read_user_shared_examples.rb +++ b/spec/support/api/scopes/read_user_shared_examples.rb @@ -1,10 +1,12 @@ -shared_examples_for 'allows the "read_user" scope' do +shared_examples_for 'allows the "read_user" scope' do |api_version| + let(:version) { api_version || 'v4' } + context 'for personal access tokens' do context 'when the requesting token has the "api" scope' do let(:token) { create(:personal_access_token, scopes: ['api'], user: user) } it 'returns a "200" response' do - get api_call.call(path, user, personal_access_token: token) + get api_call.call(path, user, personal_access_token: token, version: version) expect(response).to have_gitlab_http_status(200) end @@ -14,7 +16,7 @@ shared_examples_for 'allows the "read_user" scope' do let(:token) { create(:personal_access_token, scopes: ['read_user'], user: user) } it 'returns a "200" response' do - get api_call.call(path, user, personal_access_token: token) + get api_call.call(path, user, personal_access_token: token, version: version) expect(response).to have_gitlab_http_status(200) end @@ -28,7 +30,7 @@ shared_examples_for 'allows the "read_user" scope' do end it 'returns a "403" response' do - get api_call.call(path, user, personal_access_token: token) + get api_call.call(path, user, personal_access_token: token, version: version) expect(response).to have_gitlab_http_status(403) end diff --git a/spec/support/controllers/githubish_import_controller_shared_examples.rb b/spec/support/controllers/githubish_import_controller_shared_examples.rb index 368439aa5b0..1c1b68c12a2 100644 --- a/spec/support/controllers/githubish_import_controller_shared_examples.rb +++ b/spec/support/controllers/githubish_import_controller_shared_examples.rb @@ -118,14 +118,19 @@ shared_examples 'a GitHub-ish import controller: POST create' do expect(response).to have_gitlab_http_status(200) end - it 'returns 422 response when the project could not be imported' do + it 'returns 422 response with the base error when the project could not be imported' do + project = build(:project) + project.errors.add(:name, 'is invalid') + project.errors.add(:path, 'is old') + allow(Gitlab::LegacyGithubImport::ProjectCreator) .to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, access_params, type: provider) - .and_return(double(execute: build(:project))) + .and_return(double(execute: project)) post :create, format: :json expect(response).to have_gitlab_http_status(422) + expect(json_response['errors']).to eq('Name is invalid, Path is old') end context "when the repository owner is the provider user" do diff --git a/spec/support/features/reportable_note_shared_examples.rb b/spec/support/features/reportable_note_shared_examples.rb index 836e5e7be23..b4c71d69119 100644 --- a/spec/support/features/reportable_note_shared_examples.rb +++ b/spec/support/features/reportable_note_shared_examples.rb @@ -1,6 +1,7 @@ require 'spec_helper' shared_examples 'reportable note' do |type| + include MobileHelpers include NotesHelper let(:comment) { find("##{ActionView::RecordIdentifier.dom_id(note)}") } @@ -39,6 +40,9 @@ shared_examples 'reportable note' do |type| end def open_dropdown(dropdown) + # make window wide enough that tooltip doesn't trigger horizontal scrollbar + resize_window(1200, 800) + dropdown.find('.more-actions-toggle').click dropdown.find('.dropdown-menu li', match: :first) end diff --git a/spec/support/gitaly.rb b/spec/support/gitaly.rb index 5a1dd44bc9d..614aaa73693 100644 --- a/spec/support/gitaly.rb +++ b/spec/support/gitaly.rb @@ -9,7 +9,7 @@ RSpec.configure do |config| # Use 'and_wrap_original' to make sure the arguments are valid allow(Gitlab::GitalyClient).to receive(:feature_enabled?).and_wrap_original do |m, *args| m.call(*args) - !Gitlab::GitalyClient::EXPLICIT_OPT_IN_REQUIRED.include?(args.first) + !Gitlab::GitalyClient.explicit_opt_in_required.include?(args.first) end end end diff --git a/spec/support/helpers/assets_helpers.rb b/spec/support/helpers/assets_helpers.rb new file mode 100644 index 00000000000..09bbf451671 --- /dev/null +++ b/spec/support/helpers/assets_helpers.rb @@ -0,0 +1,15 @@ +module AssetsHelpers + # In a CI environment the assets are not compiled, as there is a CI job + # `compile-assets` that compiles them in the prepare stage for all following + # specs. + # Locally the assets are precompiled dynamically. + # + # Sprockets doesn't provide one method to access an asset for both cases. + def find_asset(asset_name) + if ENV['CI'] + Sprockets::Railtie.build_environment(Rails.application, true)[asset_name] + else + Rails.application.assets.find_asset(asset_name) + end + end +end diff --git a/spec/support/helpers/cycle_analytics_helpers.rb b/spec/support/helpers/cycle_analytics_helpers.rb index 55359d36597..32d9807f06a 100644 --- a/spec/support/helpers/cycle_analytics_helpers.rb +++ b/spec/support/helpers/cycle_analytics_helpers.rb @@ -4,12 +4,12 @@ module CycleAnalyticsHelpers create_commit("Commit for ##{issue.iid}", issue.project, user, branch_name) end - def create_commit(message, project, user, branch_name, count: 1) + def create_commit(message, project, user, branch_name, count: 1, commit_time: nil, skip_push_handler: false) repository = project.repository - oldrev = repository.commit(branch_name).sha + oldrev = repository.commit(branch_name)&.sha || Gitlab::Git::BLANK_SHA if Timecop.frozen? && Gitlab::GitalyClient.feature_enabled?(:operation_user_commit_files) - mock_gitaly_multi_action_dates(repository.raw) + mock_gitaly_multi_action_dates(repository.raw, commit_time) end commit_shas = Array.new(count) do |index| @@ -19,6 +19,8 @@ module CycleAnalyticsHelpers commit_sha end + return if skip_push_handler + GitPushService.new(project, user, oldrev: oldrev, @@ -44,13 +46,11 @@ module CycleAnalyticsHelpers project.repository.add_branch(user, source_branch, 'master') end - sha = project.repository.create_file( - user, - generate(:branch), - 'content', - message: commit_message, - branch_name: source_branch) - project.repository.commit(sha) + # Cycle analytic specs often test with frozen times, which causes metrics to be + # pinned to the current time. For example, in the plan stage, we assume that an issue + # milestone has been created before any code has been written. We add a second + # to ensure that the plan time is positive. + create_commit(commit_message, project, user, source_branch, commit_time: Time.now + 1.second, skip_push_handler: true) opts = { title: 'Awesome merge_request', @@ -116,14 +116,18 @@ module CycleAnalyticsHelpers protected: false) end - def mock_gitaly_multi_action_dates(raw_repository) + def mock_gitaly_multi_action_dates(raw_repository, commit_time) allow(raw_repository).to receive(:multi_action).and_wrap_original do |m, *args| - new_date = Time.now + new_date = commit_time || Time.now branch_update = m.call(*args) if branch_update.newrev _, opts = args - commit = raw_repository.commit(branch_update.newrev).rugged_commit + + commit = Gitlab::GitalyClient::StorageSettings.allow_disk_access do + raw_repository.commit(branch_update.newrev).rugged_commit + end + branch_update.newrev = commit.amend( update_ref: "#{Gitlab::Git::BRANCH_REF_PREFIX}#{opts[:branch_name]}", author: commit.author.merge(time: new_date), diff --git a/spec/support/helpers/graphql_helpers.rb b/spec/support/helpers/graphql_helpers.rb new file mode 100644 index 00000000000..0930b9da368 --- /dev/null +++ b/spec/support/helpers/graphql_helpers.rb @@ -0,0 +1,99 @@ +module GraphqlHelpers + # makes an underscored string look like a fieldname + # "merge_request" => "mergeRequest" + def self.fieldnamerize(underscored_field_name) + graphql_field_name = underscored_field_name.to_s.camelize + graphql_field_name[0] = graphql_field_name[0].downcase + + graphql_field_name + end + + # Run a loader's named resolver + def resolve(resolver_class, obj: nil, args: {}, ctx: {}) + resolver_class.new(object: obj, context: ctx).resolve(args) + end + + # Runs a block inside a BatchLoader::Executor wrapper + def batch(max_queries: nil, &blk) + wrapper = proc do + begin + BatchLoader::Executor.ensure_current + yield + ensure + BatchLoader::Executor.clear_current + end + end + + if max_queries + result = nil + expect { result = wrapper.call }.not_to exceed_query_limit(max_queries) + result + else + wrapper.call + end + end + + def graphql_query_for(name, attributes = {}, fields = nil) + <<~QUERY + { + #{query_graphql_field(name, attributes, fields)} + } + QUERY + end + + def query_graphql_field(name, attributes = {}, fields = nil) + fields ||= all_graphql_fields_for(name.classify) + attributes = attributes_to_graphql(attributes) + <<~QUERY + #{name}(#{attributes}) { + #{fields} + } + QUERY + end + + def all_graphql_fields_for(class_name) + type = GitlabSchema.types[class_name.to_s] + return "" unless type + + type.fields.map do |name, field| + # We can't guess arguments, so skip fields that require them + next if field.arguments.any? + + if scalar?(field) + name + else + "#{name} { #{all_graphql_fields_for(field_type(field))} }" + end + end.compact.join("\n") + end + + def attributes_to_graphql(attributes) + attributes.map do |name, value| + "#{GraphqlHelpers.fieldnamerize(name.to_s)}: \"#{value}\"" + end.join(", ") + end + + def post_graphql(query, current_user: nil) + post api('/', current_user, version: 'graphql'), query: query + end + + def graphql_data + json_response['data'] + end + + def graphql_errors + json_response['data'] + end + + def scalar?(field) + field_type(field).kind.scalar? + end + + def field_type(field) + if field.type.respond_to?(:of_type) + field.type.of_type + else + field.type + end + end +end diff --git a/spec/support/helpers/migrations_helpers.rb b/spec/support/helpers/migrations_helpers.rb index 84abec75c26..0bc235701eb 100644 --- a/spec/support/helpers/migrations_helpers.rb +++ b/spec/support/helpers/migrations_helpers.rb @@ -10,10 +10,6 @@ module MigrationsHelpers ActiveRecord::Migrator.migrations_paths end - def table_exists?(name) - ActiveRecord::Base.connection.table_exists?(name) - end - def migrations ActiveRecord::Migrator.migrations(migrations_paths) end diff --git a/spec/support/helpers/query_recorder.rb b/spec/support/helpers/query_recorder.rb index 28536bbef5e..7ce63375d34 100644 --- a/spec/support/helpers/query_recorder.rb +++ b/spec/support/helpers/query_recorder.rb @@ -1,10 +1,11 @@ module ActiveRecord class QueryRecorder - attr_reader :log, :cached + attr_reader :log, :skip_cached, :cached - def initialize(&block) + def initialize(skip_cached: true, &block) @log = [] @cached = [] + @skip_cached = skip_cached ActiveSupport::Notifications.subscribed(method(:callback), 'sql.active_record', &block) end @@ -16,7 +17,7 @@ module ActiveRecord def callback(name, start, finish, message_id, values) show_backtrace(values) if ENV['QUERY_RECORDER_DEBUG'] - if values[:name]&.include?("CACHE") + if values[:name]&.include?("CACHE") && skip_cached @cached << values[:sql] elsif !values[:name]&.include?("SCHEMA") @log << values[:sql] diff --git a/spec/support/helpers/stub_object_storage.rb b/spec/support/helpers/stub_object_storage.rb index 19d744b959a..bceaf8277ee 100644 --- a/spec/support/helpers/stub_object_storage.rb +++ b/spec/support/helpers/stub_object_storage.rb @@ -45,4 +45,16 @@ module StubObjectStorage remote_directory: 'uploads', **params) end + + def stub_object_storage_multipart_init(endpoint, upload_id = "upload_id") + stub_request(:post, %r{\A#{endpoint}tmp/uploads/[a-z0-9-]*\?uploads\z}) + .to_return status: 200, body: <<-EOS.strip_heredoc + <?xml version="1.0" encoding="UTF-8"?> + <InitiateMultipartUploadResult xmlns="http://s3.amazonaws.com/doc/2006-03-01/"> + <Bucket>example-bucket</Bucket> + <Key>example-object</Key> + <UploadId>#{upload_id}</UploadId> + </InitiateMultipartUploadResult> + EOS + end end diff --git a/spec/support/helpers/test_env.rb b/spec/support/helpers/test_env.rb index 1fef50a52ec..05a8e6206ae 100644 --- a/spec/support/helpers/test_env.rb +++ b/spec/support/helpers/test_env.rb @@ -135,6 +135,16 @@ module TestEnv install_dir: Gitlab.config.gitlab_shell.path, version: Gitlab::Shell.version_required, task: 'gitlab:shell:install') + + create_fake_git_hooks + end + + def create_fake_git_hooks + # gitlab-shell hooks don't work in our test environment because they try to make internal API calls + hooks_dir = File.join(Gitlab.config.gitlab_shell.path, 'hooks') + %w[pre-receive post-receive update].each do |hook| + File.open(File.join(hooks_dir, hook), 'w', 0755) { |f| f.puts '#!/bin/sh' } + end end def setup_gitaly diff --git a/spec/support/matchers/email_matchers.rb b/spec/support/matchers/email_matchers.rb deleted file mode 100644 index d9d59ec12ec..00000000000 --- a/spec/support/matchers/email_matchers.rb +++ /dev/null @@ -1,5 +0,0 @@ -RSpec::Matchers.define :have_html_escaped_body_text do |expected| - match do |actual| - expect(actual).to have_body_text(ERB::Util.html_escape(expected)) - end -end diff --git a/spec/support/matchers/exceed_query_limit.rb b/spec/support/matchers/exceed_query_limit.rb index 88d22a3ddd9..cd042401f3a 100644 --- a/spec/support/matchers/exceed_query_limit.rb +++ b/spec/support/matchers/exceed_query_limit.rb @@ -1,17 +1,4 @@ -RSpec::Matchers.define :exceed_query_limit do |expected| - supports_block_expectations - - match do |block| - @subject_block = block - actual_count > expected_count + threshold - end - - failure_message_when_negated do |actual| - threshold_message = threshold > 0 ? " (+#{@threshold})" : '' - counts = "#{expected_count}#{threshold_message}" - "Expected a maximum of #{counts} queries, got #{actual_count}:\n\n#{log_message}" - end - +module ExceedQueryLimitHelpers def with_threshold(threshold) @threshold = threshold self @@ -43,7 +30,7 @@ RSpec::Matchers.define :exceed_query_limit do |expected| end def recorder - @recorder ||= ActiveRecord::QueryRecorder.new(&@subject_block) + @recorder ||= ActiveRecord::QueryRecorder.new(skip_cached: skip_cached, &@subject_block) end def count_queries(queries) @@ -61,4 +48,52 @@ RSpec::Matchers.define :exceed_query_limit do |expected| @recorder.log_message end end + + def skip_cached + true + end + + def verify_count(&block) + @subject_block = block + actual_count > expected_count + threshold + end + + def failure_message + threshold_message = threshold > 0 ? " (+#{@threshold})" : '' + counts = "#{expected_count}#{threshold_message}" + "Expected a maximum of #{counts} queries, got #{actual_count}:\n\n#{log_message}" + end +end + +RSpec::Matchers.define :exceed_all_query_limit do |expected| + supports_block_expectations + + include ExceedQueryLimitHelpers + + match do |block| + verify_count(&block) + end + + failure_message_when_negated do |actual| + failure_message + end + + def skip_cached + false + end +end + +# Excludes cached methods from the query count +RSpec::Matchers.define :exceed_query_limit do |expected| + supports_block_expectations + + include ExceedQueryLimitHelpers + + match do |block| + verify_count(&block) + end + + failure_message_when_negated do |actual| + failure_message + end end diff --git a/spec/support/matchers/graphql_matchers.rb b/spec/support/matchers/graphql_matchers.rb new file mode 100644 index 00000000000..d23cbaf4beb --- /dev/null +++ b/spec/support/matchers/graphql_matchers.rb @@ -0,0 +1,46 @@ +RSpec::Matchers.define :require_graphql_authorizations do |*expected| + match do |field| + field_definition = field.metadata[:type_class] + expect(field_definition).to respond_to(:required_permissions) + expect(field_definition.required_permissions).to contain_exactly(*expected) + end +end + +RSpec::Matchers.define :have_graphql_fields do |*expected| + match do |kls| + field_names = expected.map { |name| GraphqlHelpers.fieldnamerize(name) } + expect(kls.fields.keys).to contain_exactly(*field_names) + end +end + +RSpec::Matchers.define :have_graphql_field do |field_name| + match do |kls| + expect(kls.fields.keys).to include(GraphqlHelpers.fieldnamerize(field_name)) + end +end + +RSpec::Matchers.define :have_graphql_arguments do |*expected| + include GraphqlHelpers + + match do |field| + argument_names = expected.map { |name| GraphqlHelpers.fieldnamerize(name) } + expect(field.arguments.keys).to contain_exactly(*argument_names) + end +end + +RSpec::Matchers.define :have_graphql_type do |expected| + match do |field| + expect(field.type).to eq(expected.to_graphql) + end +end + +RSpec::Matchers.define :have_graphql_resolver do |expected| + match do |field| + case expected + when Method + expect(field.metadata[:type_class].resolve_proc).to eq(expected) + else + expect(field.metadata[:type_class].resolver).to eq(expected) + end + end +end diff --git a/spec/support/rspec.rb b/spec/support/rspec.rb index dffab22d8b5..54b8df7aa19 100644 --- a/spec/support/rspec.rb +++ b/spec/support/rspec.rb @@ -9,4 +9,6 @@ RSpec.configure do |config| config.include StubConfiguration config.include StubObjectStorage config.include StubENV + + config.fixture_path = Rails.root if defined?(Rails) end diff --git a/spec/support/shared_examples/ci_trace_shared_examples.rb b/spec/support/shared_examples/ci_trace_shared_examples.rb index 21c6f3c829f..6dbe0f6f980 100644 --- a/spec/support/shared_examples/ci_trace_shared_examples.rb +++ b/spec/support/shared_examples/ci_trace_shared_examples.rb @@ -227,6 +227,42 @@ shared_examples_for 'common trace features' do end end end + + describe '#archive!' do + subject { trace.archive! } + + context 'when build status is success' do + let!(:build) { create(:ci_build, :success, :trace_live) } + + it 'does not have an archived trace yet' do + expect(build.job_artifacts_trace).to be_nil + end + + context 'when archives' do + it 'has an archived trace' do + subject + + build.reload + expect(build.job_artifacts_trace).to be_exist + end + + context 'when another process has already been archiving', :clean_gitlab_redis_shared_state do + before do + Gitlab::ExclusiveLease.new("trace:archive:#{trace.job.id}", timeout: 1.hour).try_obtain + end + + it 'blocks concurrent archiving' do + expect(Rails.logger).to receive(:error).with('Cannot obtain an exclusive lease. There must be another instance already in execution.') + + subject + + build.reload + expect(build.job_artifacts_trace).to be_nil + end + end + end + end + end end shared_examples_for 'trace with disabled live trace feature' do diff --git a/spec/support/shared_examples/controllers/uploads_actions_shared_examples.rb b/spec/support/shared_examples/controllers/uploads_actions_shared_examples.rb index ea7dbade171..bbbad86dcd5 100644 --- a/spec/support/shared_examples/controllers/uploads_actions_shared_examples.rb +++ b/spec/support/shared_examples/controllers/uploads_actions_shared_examples.rb @@ -1,7 +1,7 @@ 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') } + let(:jpg) { fixture_file_upload('spec/fixtures/rails_sample.jpg', 'image/jpg') } + let(:txt) { fixture_file_upload('spec/fixtures/doc_sample.txt', 'text/plain') } let(:secret) { FileUploader.generate_secret } let(:uploader_class) { FileUploader } diff --git a/spec/support/shared_examples/file_finder.rb b/spec/support/shared_examples/file_finder.rb new file mode 100644 index 00000000000..ef144bdf61c --- /dev/null +++ b/spec/support/shared_examples/file_finder.rb @@ -0,0 +1,21 @@ +shared_examples 'file finder' do + let(:query) { 'files' } + let(:search_results) { subject.find(query) } + + it 'finds by name' do + filename, blob = search_results.find { |_, blob| blob.filename == expected_file_by_name } + expect(filename).to eq(expected_file_by_name) + expect(blob).to be_a(Gitlab::SearchResults::FoundBlob) + expect(blob.ref).to eq(subject.ref) + expect(blob.data).not_to be_empty + end + + it 'finds by content' do + filename, blob = search_results.find { |_, blob| blob.filename == expected_file_by_content } + + expect(filename).to eq(expected_file_by_content) + expect(blob).to be_a(Gitlab::SearchResults::FoundBlob) + expect(blob.ref).to eq(subject.ref) + expect(blob.data).not_to be_empty + end +end diff --git a/spec/support/shared_examples/models/atomic_internal_id_spec.rb b/spec/support/shared_examples/models/atomic_internal_id_spec.rb index 6a6e13418a9..7ab1041d17c 100644 --- a/spec/support/shared_examples/models/atomic_internal_id_spec.rb +++ b/spec/support/shared_examples/models/atomic_internal_id_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -shared_examples_for 'AtomicInternalId' do +shared_examples_for 'AtomicInternalId' do |validate_presence: true| describe '.has_internal_id' do describe 'Module inclusion' do subject { described_class } @@ -9,14 +9,31 @@ shared_examples_for 'AtomicInternalId' do end describe 'Validation' do - subject { instance } - before do - allow(InternalId).to receive(:generate_next).and_return(nil) + allow_any_instance_of(described_class).to receive(:"ensure_#{scope}_#{internal_id_attribute}!") + + instance.valid? end - it { is_expected.to validate_presence_of(internal_id_attribute) } - it { is_expected.to validate_numericality_of(internal_id_attribute) } + context 'when presence validation is required' do + before do + skip unless validate_presence + end + + it 'validates presence' do + expect(instance.errors[internal_id_attribute]).to include("can't be blank") + end + end + + context 'when presence validation is not required' do + before do + skip if validate_presence + end + + it 'does not validate presence' do + expect(instance.errors[internal_id_attribute]).to be_empty + end + end end describe 'Creating an instance' do diff --git a/spec/support/shared_examples/notify_shared_examples.rb b/spec/support/shared_examples/notify_shared_examples.rb index 43fdaddf545..d176d3fa425 100644 --- a/spec/support/shared_examples/notify_shared_examples.rb +++ b/spec/support/shared_examples/notify_shared_examples.rb @@ -212,7 +212,7 @@ shared_examples 'a note email' do end it 'contains the message from the note' do - is_expected.to have_html_escaped_body_text note.note + is_expected.to have_body_text note.note end it 'does not contain note author' do @@ -225,7 +225,7 @@ shared_examples 'a note email' do end it 'contains a link to note author' do - is_expected.to have_html_escaped_body_text note.author_name + is_expected.to have_body_text note.author_name end end end diff --git a/spec/support/shared_examples/requests/graphql_shared_examples.rb b/spec/support/shared_examples/requests/graphql_shared_examples.rb new file mode 100644 index 00000000000..9b2b74593a5 --- /dev/null +++ b/spec/support/shared_examples/requests/graphql_shared_examples.rb @@ -0,0 +1,11 @@ +require 'spec_helper' + +shared_examples 'a working graphql query' do + include GraphqlHelpers + + it 'is returns a successfull response', :aggregate_failures do + expect(response).to be_success + expect(graphql_errors['errors']).to be_nil + expect(json_response.keys).to include('data') + end +end diff --git a/spec/support/shared_examples/uploaders/object_storage_shared_examples.rb b/spec/support/shared_examples/uploaders/object_storage_shared_examples.rb index 6352f1527cd..19800c6638f 100644 --- a/spec/support/shared_examples/uploaders/object_storage_shared_examples.rb +++ b/spec/support/shared_examples/uploaders/object_storage_shared_examples.rb @@ -76,26 +76,24 @@ shared_examples "migrates" do |to_store:, from_store: nil| end context 'when migrate! is occupied by another process' do - let(:exclusive_lease_key) { "object_storage_migrate:#{subject.model.class}:#{subject.model.id}" } - before do - @uuid = Gitlab::ExclusiveLease.new(exclusive_lease_key, timeout: 1.hour.to_i).try_obtain + @uuid = Gitlab::ExclusiveLease.new(subject.exclusive_lease_key, timeout: 1.hour.to_i).try_obtain end it 'does not execute migrate!' do expect(subject).not_to receive(:unsafe_migrate!) - expect { migrate(to) }.to raise_error('exclusive lease already taken') + expect { migrate(to) }.to raise_error(ObjectStorage::ExclusiveLeaseTaken) end it 'does not execute use_file' do expect(subject).not_to receive(:unsafe_use_file) - expect { subject.use_file }.to raise_error('exclusive lease already taken') + expect { subject.use_file }.to raise_error(ObjectStorage::ExclusiveLeaseTaken) end after do - Gitlab::ExclusiveLease.cancel(exclusive_lease_key, @uuid) + Gitlab::ExclusiveLease.cancel(subject.exclusive_lease_key, @uuid) end end diff --git a/spec/support/shoulda/matchers/rails_shim.rb b/spec/support/shoulda/matchers/rails_shim.rb new file mode 100644 index 00000000000..8d70598beb5 --- /dev/null +++ b/spec/support/shoulda/matchers/rails_shim.rb @@ -0,0 +1,27 @@ +# monkey patch which fixes serialization matcher in Rails 5 +# https://github.com/thoughtbot/shoulda-matchers/issues/913 +# This can be removed when a new version of shoulda-matchers +# is released +module Shoulda + module Matchers + class RailsShim + def self.serialized_attributes_for(model) + if defined?(::ActiveRecord::Type::Serialized) + # Rails 5+ + serialized_columns = model.columns.select do |column| + model.type_for_attribute(column.name).is_a?( + ::ActiveRecord::Type::Serialized + ) + end + + serialized_columns.inject({}) do |hash, column| # rubocop:disable Style/EachWithObject + hash[column.name.to_s] = model.type_for_attribute(column.name).coder + hash + end + else + model.serialized_attributes + end + end + end + end +end diff --git a/spec/support/trace/trace_helpers.rb b/spec/support/trace/trace_helpers.rb new file mode 100644 index 00000000000..c7802bbcb94 --- /dev/null +++ b/spec/support/trace/trace_helpers.rb @@ -0,0 +1,27 @@ +module TraceHelpers + def create_legacy_trace(build, content) + File.open(legacy_trace_path(build), 'wb') { |stream| stream.write(content) } + end + + def create_legacy_trace_in_db(build, content) + build.update_column(:trace, content) + end + + def legacy_trace_path(build) + legacy_trace_dir = File.join(Settings.gitlab_ci.builds_path, + build.created_at.utc.strftime("%Y_%m"), + build.project_id.to_s) + + FileUtils.mkdir_p(legacy_trace_dir) + + File.join(legacy_trace_dir, "#{build.id}.log") + end + + def archived_trace_path(job_artifact) + disk_hash = Digest::SHA2.hexdigest(job_artifact.project_id.to_s) + creation_date = job_artifact.created_at.utc.strftime('%Y_%m_%d') + + File.join(Gitlab.config.artifacts.path, disk_hash[0..1], disk_hash[2..3], disk_hash, + creation_date, job_artifact.job_id.to_s, job_artifact.id.to_s, 'job.log') + end +end diff --git a/spec/tasks/gitlab/backup_rake_spec.rb b/spec/tasks/gitlab/backup_rake_spec.rb index c9252bebb2e..93a436cb2b5 100644 --- a/spec/tasks/gitlab/backup_rake_spec.rb +++ b/spec/tasks/gitlab/backup_rake_spec.rb @@ -101,7 +101,9 @@ describe 'gitlab:app namespace rake task' do before do stub_env('SKIP', 'db') - path = File.join(project.repository.path_to_repo, 'custom_hooks') + path = Gitlab::GitalyClient::StorageSettings.allow_disk_access do + File.join(project.repository.path_to_repo, 'custom_hooks') + end FileUtils.mkdir_p(path) FileUtils.touch(File.join(path, "dummy.txt")) end @@ -122,7 +124,10 @@ describe 'gitlab:app namespace rake task' do expect { run_rake_task('gitlab:backup:create') }.to output.to_stdout expect { run_rake_task('gitlab:backup:restore') }.to output.to_stdout - expect(Dir.entries(File.join(project.repository.path, 'custom_hooks'))).to include("dummy.txt") + repo_path = Gitlab::GitalyClient::StorageSettings.allow_disk_access do + project.repository.path + end + expect(Dir.entries(File.join(repo_path, 'custom_hooks'))).to include("dummy.txt") end end @@ -243,10 +248,12 @@ describe 'gitlab:app namespace rake task' do FileUtils.mkdir_p(b_storage_dir) # Even when overriding the storage, we have to move it there, so it exists - FileUtils.mv( - File.join(Settings.absolute(storages['default'].legacy_disk_path), project_b.repository.disk_path + '.git'), - Rails.root.join(storages['test_second_storage'].legacy_disk_path, project_b.repository.disk_path + '.git') - ) + Gitlab::GitalyClient::StorageSettings.allow_disk_access do + FileUtils.mv( + File.join(Settings.absolute(storages['default'].legacy_disk_path), project_b.repository.disk_path + '.git'), + Rails.root.join(storages['test_second_storage'].legacy_disk_path, project_b.repository.disk_path + '.git') + ) + end expect { run_rake_task('gitlab:backup:create') }.to output.to_stdout diff --git a/spec/tasks/gitlab/gitaly_rake_spec.rb b/spec/tasks/gitlab/gitaly_rake_spec.rb index 1e507c0236e..4545226d78c 100644 --- a/spec/tasks/gitlab/gitaly_rake_spec.rb +++ b/spec/tasks/gitlab/gitaly_rake_spec.rb @@ -134,7 +134,9 @@ describe 'gitlab:gitaly namespace rake task' do parsed_output = TomlRB.parse(expected_output) config.each do |name, params| - expect(parsed_output['storage']).to include({ 'name' => name, 'path' => params.legacy_disk_path }) + Gitlab::GitalyClient::StorageSettings.allow_disk_access do + expect(parsed_output['storage']).to include({ 'name' => name, 'path' => params.legacy_disk_path }) + end end end end diff --git a/spec/tasks/gitlab/shell_rake_spec.rb b/spec/tasks/gitlab/shell_rake_spec.rb index 4a756c5742d..0ed5d3e27b9 100644 --- a/spec/tasks/gitlab/shell_rake_spec.rb +++ b/spec/tasks/gitlab/shell_rake_spec.rb @@ -7,11 +7,17 @@ describe 'gitlab:shell rake tasks' do stub_warn_user_is_not_gitlab end + after do + TestEnv.create_fake_git_hooks + end + describe 'install task' do it 'invokes create_hooks task' do expect(Rake::Task['gitlab:shell:create_hooks']).to receive(:invoke) - storages = Gitlab.config.repositories.storages.values.map(&:legacy_disk_path) + storages = Gitlab::GitalyClient::StorageSettings.allow_disk_access do + Gitlab.config.repositories.storages.values.map(&:legacy_disk_path) + end expect(Kernel).to receive(:system).with('bin/install', *storages).and_call_original expect(Kernel).to receive(:system).with('bin/compile').and_call_original diff --git a/spec/tasks/gitlab/storage_rake_spec.rb b/spec/tasks/gitlab/storage_rake_spec.rb index 35e451b2f9a..233076ad6fa 100644 --- a/spec/tasks/gitlab/storage_rake_spec.rb +++ b/spec/tasks/gitlab/storage_rake_spec.rb @@ -1,6 +1,6 @@ require 'rake_helper' -describe 'gitlab:storage:*' do +describe 'rake gitlab:storage:*' do before do Rake.application.rake_require 'tasks/gitlab/storage' @@ -44,16 +44,18 @@ describe 'gitlab:storage:*' do end describe 'gitlab:storage:migrate_to_hashed' do + let(:task) { 'gitlab:storage:migrate_to_hashed' } + context '0 legacy projects' do it 'does nothing' do expect(StorageMigratorWorker).not_to receive(:perform_async) - run_rake_task('gitlab:storage:migrate_to_hashed') + run_rake_task(task) end end context '3 legacy projects' do - let(:projects) { create_list(:project, 3, storage_version: 0) } + let(:projects) { create_list(:project, 3, :legacy_storage) } context 'in batches of 1' do before do @@ -65,7 +67,7 @@ describe 'gitlab:storage:*' do expect(StorageMigratorWorker).to receive(:perform_async).with(project.id, project.id) end - run_rake_task('gitlab:storage:migrate_to_hashed') + run_rake_task(task) end end @@ -80,23 +82,48 @@ describe 'gitlab:storage:*' do expect(StorageMigratorWorker).to receive(:perform_async).with(first, last) end - run_rake_task('gitlab:storage:migrate_to_hashed') + run_rake_task(task) end end end + + context 'with same id in range' do + it 'displays message when project cant be found' do + stub_env('ID_FROM', 99999) + stub_env('ID_TO', 99999) + + expect { run_rake_task(task) }.to output(/There are no projects requiring storage migration with ID=99999/).to_stdout + end + + it 'displays a message when project exists but its already migrated' do + project = create(:project) + stub_env('ID_FROM', project.id) + stub_env('ID_TO', project.id) + + expect { run_rake_task(task) }.to output(/There are no projects requiring storage migration with ID=#{project.id}/).to_stdout + end + + it 'enqueues migration when project can be found' do + project = create(:project, :legacy_storage) + stub_env('ID_FROM', project.id) + stub_env('ID_TO', project.id) + + expect { run_rake_task(task) }.to output(/Enqueueing storage migration .* \(ID=#{project.id}\)/).to_stdout + end + end end describe 'gitlab:storage:legacy_projects' do it_behaves_like 'rake entities summary', 'projects', 'Legacy' do let(:task) { 'gitlab:storage:legacy_projects' } - let(:create_collection) { create_list(:project, 3, storage_version: 0) } + let(:create_collection) { create_list(:project, 3, :legacy_storage) } end end describe 'gitlab:storage:list_legacy_projects' do it_behaves_like 'rake listing entities', 'projects', 'Legacy' do let(:task) { 'gitlab:storage:list_legacy_projects' } - let(:create_collection) { create_list(:project, 3, storage_version: 0) } + let(:create_collection) { create_list(:project, 3, :legacy_storage) } end end @@ -133,7 +160,7 @@ describe 'gitlab:storage:*' do describe 'gitlab:storage:hashed_attachments' do it_behaves_like 'rake entities summary', 'attachments', 'Hashed' do let(:task) { 'gitlab:storage:hashed_attachments' } - let(:project) { create(:project, storage_version: 2) } + let(:project) { create(:project) } let(:create_collection) { create_list(:upload, 3, model: project) } end end @@ -141,7 +168,7 @@ describe 'gitlab:storage:*' do describe 'gitlab:storage:list_hashed_attachments' do it_behaves_like 'rake listing entities', 'attachments', 'Hashed' do let(:task) { 'gitlab:storage:list_hashed_attachments' } - let(:project) { create(:project, storage_version: 2) } + let(:project) { create(:project) } let(:create_collection) { create_list(:upload, 3, model: project) } end end diff --git a/spec/uploaders/attachment_uploader_spec.rb b/spec/uploaders/attachment_uploader_spec.rb index d302c14efb9..a9415854d25 100644 --- a/spec/uploaders/attachment_uploader_spec.rb +++ b/spec/uploaders/attachment_uploader_spec.rb @@ -26,7 +26,7 @@ describe AttachmentUploader do describe "#migrate!" do before do - uploader.store!(fixture_file_upload(Rails.root.join('spec/fixtures/doc_sample.txt'))) + uploader.store!(fixture_file_upload(File.join('spec/fixtures/doc_sample.txt'))) stub_uploads_object_storage end diff --git a/spec/uploaders/file_mover_spec.rb b/spec/uploaders/file_mover_spec.rb index 68b7e24776d..de29d0c943f 100644 --- a/spec/uploaders/file_mover_spec.rb +++ b/spec/uploaders/file_mover_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' describe FileMover do let(:filename) { 'banana_sample.gif' } - let(:file) { fixture_file_upload(Rails.root.join('spec', 'fixtures', filename)) } + let(:file) { fixture_file_upload(File.join('spec', 'fixtures', filename)) } let(:temp_file_path) { File.join('uploads/-/system/temp', 'secret55', filename) } let(:temp_description) do diff --git a/spec/uploaders/file_uploader_spec.rb b/spec/uploaders/file_uploader_spec.rb index db2810bbe1d..59013a02938 100644 --- a/spec/uploaders/file_uploader_spec.rb +++ b/spec/uploaders/file_uploader_spec.rb @@ -89,7 +89,7 @@ describe FileUploader do describe "#migrate!" do before do - uploader.store!(fixture_file_upload(Rails.root.join('spec/fixtures/dk.png'))) + uploader.store!(fixture_file_upload('spec/fixtures/dk.png')) stub_uploads_object_storage end diff --git a/spec/uploaders/gitlab_uploader_spec.rb b/spec/uploaders/gitlab_uploader_spec.rb index 4fba122cce1..362f89424d4 100644 --- a/spec/uploaders/gitlab_uploader_spec.rb +++ b/spec/uploaders/gitlab_uploader_spec.rb @@ -62,7 +62,7 @@ describe GitlabUploader do expect(FileUtils).to receive(:mv).with(anything, /^#{subject.work_dir}/).and_call_original expect(FileUtils).to receive(:mv).with(/^#{subject.work_dir}/, /#{subject.cache_dir}/).and_call_original - fixture = Rails.root.join('spec', 'fixtures', 'rails_sample.jpg') + fixture = File.join('spec', 'fixtures', 'rails_sample.jpg') subject.cache!(fixture_file_upload(fixture)) expect(subject.file.path).to match(/#{subject.cache_dir}/) diff --git a/spec/uploaders/job_artifact_uploader_spec.rb b/spec/uploaders/job_artifact_uploader_spec.rb index 42036d67f3d..026e4356ed6 100644 --- a/spec/uploaders/job_artifact_uploader_spec.rb +++ b/spec/uploaders/job_artifact_uploader_spec.rb @@ -29,8 +29,7 @@ describe JobArtifactUploader do context 'when trace is stored in File storage' do context 'when file exists' do let(:file) do - fixture_file_upload( - Rails.root.join('spec/fixtures/trace/sample_trace'), 'text/plain') + fixture_file_upload('spec/fixtures/trace/sample_trace', 'text/plain') end before do @@ -63,8 +62,7 @@ describe JobArtifactUploader do context 'file is stored in valid local_path' do let(:file) do - fixture_file_upload( - Rails.root.join('spec/fixtures/ci_build_artifacts.zip'), 'application/zip') + fixture_file_upload('spec/fixtures/ci_build_artifacts.zip', 'application/zip') end before do @@ -81,7 +79,7 @@ describe JobArtifactUploader do describe "#migrate!" do before do - uploader.store!(fixture_file_upload(Rails.root.join('spec/fixtures/trace/sample_trace'))) + uploader.store!(fixture_file_upload('spec/fixtures/trace/sample_trace')) stub_artifacts_object_storage end diff --git a/spec/uploaders/legacy_artifact_uploader_spec.rb b/spec/uploaders/legacy_artifact_uploader_spec.rb index eeb6fd90c9d..0589563b502 100644 --- a/spec/uploaders/legacy_artifact_uploader_spec.rb +++ b/spec/uploaders/legacy_artifact_uploader_spec.rb @@ -44,8 +44,7 @@ describe LegacyArtifactUploader do context 'file is stored in valid path' do let(:file) do - fixture_file_upload( - Rails.root.join('spec/fixtures/ci_build_artifacts.zip'), 'application/zip') + fixture_file_upload('spec/fixtures/ci_build_artifacts.zip', 'application/zip') end before do diff --git a/spec/uploaders/namespace_file_uploader_spec.rb b/spec/uploaders/namespace_file_uploader_spec.rb index a8ba01d70b8..71fe2c353c0 100644 --- a/spec/uploaders/namespace_file_uploader_spec.rb +++ b/spec/uploaders/namespace_file_uploader_spec.rb @@ -28,7 +28,7 @@ describe NamespaceFileUploader do describe "#migrate!" do before do - uploader.store!(fixture_file_upload(Rails.root.join('spec/fixtures/doc_sample.txt'))) + uploader.store!(fixture_file_upload(File.join('spec/fixtures/doc_sample.txt'))) stub_uploads_object_storage end diff --git a/spec/uploaders/object_storage_spec.rb b/spec/uploaders/object_storage_spec.rb index 2dd0925a8e6..c7f5694ff43 100644 --- a/spec/uploaders/object_storage_spec.rb +++ b/spec/uploaders/object_storage_spec.rb @@ -321,7 +321,7 @@ describe ObjectStorage do when_file_is_in_use do expect(uploader).not_to receive(:unsafe_migrate!) - expect { uploader.migrate!(described_class::Store::REMOTE) }.to raise_error('exclusive lease already taken') + expect { uploader.migrate!(described_class::Store::REMOTE) }.to raise_error(ObjectStorage::ExclusiveLeaseTaken) end end @@ -329,7 +329,19 @@ describe ObjectStorage do when_file_is_in_use do expect(uploader).not_to receive(:unsafe_use_file) - expect { uploader.use_file }.to raise_error('exclusive lease already taken') + expect { uploader.use_file }.to raise_error(ObjectStorage::ExclusiveLeaseTaken) + end + end + + it 'can still migrate other files of the same model' do + uploader2 = uploader_class.new(object, :file) + uploader2.upload = create(:upload) + uploader.upload = create(:upload) + + when_file_is_in_use do + expect(uploader2).to receive(:unsafe_migrate!) + + uploader2.migrate!(described_class::Store::REMOTE) end end end @@ -355,14 +367,10 @@ describe ObjectStorage do end describe '.workhorse_authorize' do - subject { uploader_class.workhorse_authorize } + let(:has_length) { true } + let(:maximum_size) { nil } - before do - # ensure that we use regular Fog libraries - # other tests might call `Fog.mock!` and - # it will make tests to fail - Fog.unmock! - end + subject { uploader_class.workhorse_authorize(has_length: has_length, maximum_size: maximum_size) } shared_examples 'uses local storage' do it "returns temporary path" do @@ -371,10 +379,6 @@ describe ObjectStorage do expect(subject[:TempPath]).to start_with(uploader_class.root) expect(subject[:TempPath]).to include(described_class::TMP_UPLOAD_PATH) end - - it "does not return remote store" do - is_expected.not_to have_key('RemoteObject') - end end shared_examples 'uses remote storage' do @@ -383,7 +387,7 @@ describe ObjectStorage do expect(subject[:RemoteObject]).to have_key(:ID) expect(subject[:RemoteObject]).to include(Timeout: a_kind_of(Integer)) - expect(subject[:RemoteObject][:Timeout]).to be(ObjectStorage::DIRECT_UPLOAD_TIMEOUT) + expect(subject[:RemoteObject][:Timeout]).to be(ObjectStorage::DirectUpload::TIMEOUT) expect(subject[:RemoteObject]).to have_key(:GetURL) expect(subject[:RemoteObject]).to have_key(:DeleteURL) expect(subject[:RemoteObject]).to have_key(:StoreURL) @@ -391,9 +395,31 @@ describe ObjectStorage do expect(subject[:RemoteObject][:DeleteURL]).to include(described_class::TMP_UPLOAD_PATH) expect(subject[:RemoteObject][:StoreURL]).to include(described_class::TMP_UPLOAD_PATH) end + end - it "does not return local store" do - is_expected.not_to have_key('TempPath') + shared_examples 'uses remote storage with multipart uploads' do + it_behaves_like 'uses remote storage' do + it "returns multipart upload" do + is_expected.to have_key(:RemoteObject) + + expect(subject[:RemoteObject]).to have_key(:MultipartUpload) + expect(subject[:RemoteObject][:MultipartUpload]).to have_key(:PartSize) + expect(subject[:RemoteObject][:MultipartUpload]).to have_key(:PartURLs) + expect(subject[:RemoteObject][:MultipartUpload]).to have_key(:CompleteURL) + expect(subject[:RemoteObject][:MultipartUpload]).to have_key(:AbortURL) + expect(subject[:RemoteObject][:MultipartUpload][:PartURLs]).to all(include(described_class::TMP_UPLOAD_PATH)) + expect(subject[:RemoteObject][:MultipartUpload][:CompleteURL]).to include(described_class::TMP_UPLOAD_PATH) + expect(subject[:RemoteObject][:MultipartUpload][:AbortURL]).to include(described_class::TMP_UPLOAD_PATH) + end + end + end + + shared_examples 'uses remote storage without multipart uploads' do + it_behaves_like 'uses remote storage' do + it "does not return multipart upload" do + is_expected.to have_key(:RemoteObject) + expect(subject[:RemoteObject]).not_to have_key(:MultipartUpload) + end end end @@ -416,6 +442,8 @@ describe ObjectStorage do end context 'uses AWS' do + let(:storage_url) { "https://uploads.s3-eu-central-1.amazonaws.com/" } + before do expect(uploader_class).to receive(:object_store_credentials) do { provider: "AWS", @@ -425,18 +453,40 @@ describe ObjectStorage do end end - it_behaves_like 'uses remote storage' do - let(:storage_url) { "https://uploads.s3-eu-central-1.amazonaws.com/" } + context 'for known length' do + it_behaves_like 'uses remote storage without multipart uploads' do + it 'returns links for S3' do + expect(subject[:RemoteObject][:GetURL]).to start_with(storage_url) + expect(subject[:RemoteObject][:DeleteURL]).to start_with(storage_url) + expect(subject[:RemoteObject][:StoreURL]).to start_with(storage_url) + end + end + end + + context 'for unknown length' do + let(:has_length) { false } + let(:maximum_size) { 1.gigabyte } + + before do + stub_object_storage_multipart_init(storage_url) + end - it 'returns links for S3' do - expect(subject[:RemoteObject][:GetURL]).to start_with(storage_url) - expect(subject[:RemoteObject][:DeleteURL]).to start_with(storage_url) - expect(subject[:RemoteObject][:StoreURL]).to start_with(storage_url) + it_behaves_like 'uses remote storage with multipart uploads' do + it 'returns links for S3' do + expect(subject[:RemoteObject][:GetURL]).to start_with(storage_url) + expect(subject[:RemoteObject][:DeleteURL]).to start_with(storage_url) + expect(subject[:RemoteObject][:StoreURL]).to start_with(storage_url) + expect(subject[:RemoteObject][:MultipartUpload][:PartURLs]).to all(start_with(storage_url)) + expect(subject[:RemoteObject][:MultipartUpload][:CompleteURL]).to start_with(storage_url) + expect(subject[:RemoteObject][:MultipartUpload][:AbortURL]).to start_with(storage_url) + end end end end context 'uses Google' do + let(:storage_url) { "https://storage.googleapis.com/uploads/" } + before do expect(uploader_class).to receive(:object_store_credentials) do { provider: "Google", @@ -445,36 +495,71 @@ describe ObjectStorage do end end - it_behaves_like 'uses remote storage' do - let(:storage_url) { "https://storage.googleapis.com/uploads/" } + context 'for known length' do + it_behaves_like 'uses remote storage without multipart uploads' do + it 'returns links for Google Cloud' do + expect(subject[:RemoteObject][:GetURL]).to start_with(storage_url) + expect(subject[:RemoteObject][:DeleteURL]).to start_with(storage_url) + expect(subject[:RemoteObject][:StoreURL]).to start_with(storage_url) + end + end + end + + context 'for unknown length' do + let(:has_length) { false } + let(:maximum_size) { 1.gigabyte } - it 'returns links for Google Cloud' do - expect(subject[:RemoteObject][:GetURL]).to start_with(storage_url) - expect(subject[:RemoteObject][:DeleteURL]).to start_with(storage_url) - expect(subject[:RemoteObject][:StoreURL]).to start_with(storage_url) + it_behaves_like 'uses remote storage without multipart uploads' do + it 'returns links for Google Cloud' do + expect(subject[:RemoteObject][:GetURL]).to start_with(storage_url) + expect(subject[:RemoteObject][:DeleteURL]).to start_with(storage_url) + expect(subject[:RemoteObject][:StoreURL]).to start_with(storage_url) + end end end end context 'uses GDK/minio' do + let(:storage_url) { "http://minio:9000/uploads/" } + before do expect(uploader_class).to receive(:object_store_credentials) do { provider: "AWS", aws_access_key_id: "AWS_ACCESS_KEY_ID", aws_secret_access_key: "AWS_SECRET_ACCESS_KEY", - endpoint: 'http://127.0.0.1:9000', + endpoint: 'http://minio:9000', path_style: true, region: "gdk" } end end - it_behaves_like 'uses remote storage' do - let(:storage_url) { "http://127.0.0.1:9000/uploads/" } + context 'for known length' do + it_behaves_like 'uses remote storage without multipart uploads' do + it 'returns links for S3' do + expect(subject[:RemoteObject][:GetURL]).to start_with(storage_url) + expect(subject[:RemoteObject][:DeleteURL]).to start_with(storage_url) + expect(subject[:RemoteObject][:StoreURL]).to start_with(storage_url) + end + end + end + + context 'for unknown length' do + let(:has_length) { false } + let(:maximum_size) { 1.gigabyte } + + before do + stub_object_storage_multipart_init(storage_url) + end - it 'returns links for S3' do - expect(subject[:RemoteObject][:GetURL]).to start_with(storage_url) - expect(subject[:RemoteObject][:DeleteURL]).to start_with(storage_url) - expect(subject[:RemoteObject][:StoreURL]).to start_with(storage_url) + it_behaves_like 'uses remote storage with multipart uploads' do + it 'returns links for S3' do + expect(subject[:RemoteObject][:GetURL]).to start_with(storage_url) + expect(subject[:RemoteObject][:DeleteURL]).to start_with(storage_url) + expect(subject[:RemoteObject][:StoreURL]).to start_with(storage_url) + expect(subject[:RemoteObject][:MultipartUpload][:PartURLs]).to all(start_with(storage_url)) + expect(subject[:RemoteObject][:MultipartUpload][:CompleteURL]).to start_with(storage_url) + expect(subject[:RemoteObject][:MultipartUpload][:AbortURL]).to start_with(storage_url) + end end end end @@ -498,7 +583,7 @@ describe ObjectStorage do context 'when local file is used' do context 'when valid file is used' do let(:uploaded_file) do - fixture_file_upload(Rails.root + 'spec/fixtures/rails_sample.jpg', 'image/jpg') + fixture_file_upload('spec/fixtures/rails_sample.jpg', 'image/jpg') end it "properly caches the file" do @@ -659,4 +744,26 @@ describe ObjectStorage do end end end + + describe '#retrieve_from_store!' do + [:group, :project, :user].each do |model| + context "for #{model}s" do + let(:models) { create_list(model, 3, :with_avatar).map(&:reload) } + let(:avatars) { models.map(&:avatar) } + + it 'batches fetching uploads from the database' do + # Ensure that these are all created and fully loaded before we start + # running queries for avatars + models + + expect { avatars }.not_to exceed_query_limit(1) + end + + it 'fetches a unique upload for each model' do + expect(avatars.map(&:url).uniq).to eq(avatars.map(&:url)) + expect(avatars.map(&:upload).uniq).to eq(avatars.map(&:upload)) + end + end + end + end end diff --git a/spec/uploaders/personal_file_uploader_spec.rb b/spec/uploaders/personal_file_uploader_spec.rb index c70521d90dc..7700b14ce6b 100644 --- a/spec/uploaders/personal_file_uploader_spec.rb +++ b/spec/uploaders/personal_file_uploader_spec.rb @@ -45,7 +45,7 @@ describe PersonalFileUploader do describe "#migrate!" do before do - uploader.store!(fixture_file_upload(Rails.root.join('spec/fixtures/doc_sample.txt'))) + uploader.store!(fixture_file_upload('spec/fixtures/doc_sample.txt')) stub_uploads_object_storage end diff --git a/spec/uploaders/records_uploads_spec.rb b/spec/uploaders/records_uploads_spec.rb index 9a3e5d83e01..3592a11360d 100644 --- a/spec/uploaders/records_uploads_spec.rb +++ b/spec/uploaders/records_uploads_spec.rb @@ -16,7 +16,7 @@ describe RecordsUploads do end def upload_fixture(filename) - fixture_file_upload(Rails.root.join('spec', 'fixtures', filename)) + fixture_file_upload(File.join('spec', 'fixtures', filename)) end describe 'callbacks' do diff --git a/spec/uploaders/uploader_helper_spec.rb b/spec/uploaders/uploader_helper_spec.rb index c47f09adb6d..33da93cc9d0 100644 --- a/spec/uploaders/uploader_helper_spec.rb +++ b/spec/uploaders/uploader_helper_spec.rb @@ -12,7 +12,7 @@ describe UploaderHelper do end def upload_fixture(filename) - fixture_file_upload(Rails.root.join('spec', 'fixtures', filename)) + fixture_file_upload(File.join('spec', 'fixtures', filename)) end describe '#image_or_video?' do diff --git a/spec/uploaders/workers/object_storage/background_move_worker_spec.rb b/spec/uploaders/workers/object_storage/background_move_worker_spec.rb index b34f427fd8a..95813d15e52 100644 --- a/spec/uploaders/workers/object_storage/background_move_worker_spec.rb +++ b/spec/uploaders/workers/object_storage/background_move_worker_spec.rb @@ -125,8 +125,10 @@ describe ObjectStorage::BackgroundMoveWorker do it "migrates file to remote storage" do perform + project.reload + BatchLoader::Executor.clear_current - expect(project.reload.avatar.file_storage?).to be_falsey + expect(project.avatar).not_to be_file_storage end end @@ -137,7 +139,7 @@ describe ObjectStorage::BackgroundMoveWorker do it "migrates file to remote storage" do perform - expect(project.reload.avatar.file_storage?).to be_falsey + expect(project.reload.avatar).not_to be_file_storage end end end diff --git a/spec/uploaders/workers/object_storage/migrate_uploads_worker_spec.rb b/spec/uploaders/workers/object_storage/migrate_uploads_worker_spec.rb index aed62f97448..da490cb02af 100644 --- a/spec/uploaders/workers/object_storage/migrate_uploads_worker_spec.rb +++ b/spec/uploaders/workers/object_storage/migrate_uploads_worker_spec.rb @@ -11,6 +11,12 @@ describe ObjectStorage::MigrateUploadsWorker, :sidekiq do let(:uploads) { Upload.all } let(:to_store) { ObjectStorage::Store::REMOTE } + def perform(uploads) + described_class.new.perform(uploads.ids, model_class.to_s, mounted_as, to_store) + rescue ObjectStorage::MigrateUploadsWorker::Report::MigrationFailures + # swallow + end + shared_examples "uploads migration worker" do describe '.enqueue!' do def enqueue! @@ -69,12 +75,6 @@ describe ObjectStorage::MigrateUploadsWorker, :sidekiq do end describe '#perform' do - def perform - described_class.new.perform(uploads.ids, model_class.to_s, mounted_as, to_store) - rescue ObjectStorage::MigrateUploadsWorker::Report::MigrationFailures - # swallow - end - shared_examples 'outputs correctly' do |success: 0, failures: 0| total = success + failures @@ -82,7 +82,7 @@ describe ObjectStorage::MigrateUploadsWorker, :sidekiq do it 'outputs the reports' do expect(Rails.logger).to receive(:info).with(%r{Migrated #{success}/#{total} files}) - perform + perform(uploads) end end @@ -90,7 +90,7 @@ describe ObjectStorage::MigrateUploadsWorker, :sidekiq do it 'outputs upload failures' do expect(Rails.logger).to receive(:warn).with(/Error .* I am a teapot/) - perform + perform(uploads) end end end @@ -98,7 +98,7 @@ describe ObjectStorage::MigrateUploadsWorker, :sidekiq do it_behaves_like 'outputs correctly', success: 10 it 'migrates files' do - perform + perform(uploads) expect(Upload.where(store: ObjectStorage::Store::LOCAL).count).to eq(0) end @@ -123,6 +123,17 @@ describe ObjectStorage::MigrateUploadsWorker, :sidekiq do end it_behaves_like "uploads migration worker" + + describe "limits N+1 queries" do + it "to N*5" do + query_count = ActiveRecord::QueryRecorder.new { perform(uploads) } + + more_projects = create_list(:project, 3, :with_avatar) + + expected_queries_per_migration = 5 * more_projects.count + expect { perform(Upload.all) }.not_to exceed_query_limit(query_count).with_threshold(expected_queries_per_migration) + end + end end context "for FileUploader" do @@ -130,15 +141,29 @@ describe ObjectStorage::MigrateUploadsWorker, :sidekiq do let(:secret) { SecureRandom.hex } let(:mounted_as) { nil } + def upload_file(project) + uploader = FileUploader.new(project) + uploader.store!(fixture_file_upload('spec/fixtures/doc_sample.txt')) + end + before do stub_uploads_object_storage(FileUploader) - projects.map do |project| - uploader = FileUploader.new(project) - uploader.store!(fixture_file_upload('spec/fixtures/doc_sample.txt')) - end + projects.map(&method(:upload_file)) end it_behaves_like "uploads migration worker" + + describe "limits N+1 queries" do + it "to N*5" do + query_count = ActiveRecord::QueryRecorder.new { perform(uploads) } + + more_projects = create_list(:project, 3) + more_projects.map(&method(:upload_file)) + + expected_queries_per_migration = 5 * more_projects.count + expect { perform(Upload.all) }.not_to exceed_query_limit(query_count).with_threshold(expected_queries_per_migration) + end + end end end diff --git a/spec/validators/url_validator_spec.rb b/spec/validators/url_validator_spec.rb index 2d719263fc8..93fe013d11c 100644 --- a/spec/validators/url_validator_spec.rb +++ b/spec/validators/url_validator_spec.rb @@ -50,13 +50,56 @@ describe UrlValidator do end end - context 'when ports is set' do - let(:validator) { described_class.new(attributes: [:link_url], ports: [443]) } + context 'when ports is' do + let(:validator) { described_class.new(attributes: [:link_url], ports: ports) } - it 'blocks urls with a different port' do - subject + context 'empty' do + let(:ports) { [] } - expect(badge.errors.empty?).to be false + it 'does not block any port' do + subject + + expect(badge.errors.empty?).to be true + end + end + + context 'set' do + let(:ports) { [443] } + + it 'blocks urls with a different port' do + subject + + expect(badge.errors.empty?).to be false + end + end + end + + context 'when enforce_user is' do + let(:url) { 'http://$user@example.com'} + let(:validator) { described_class.new(attributes: [:link_url], enforce_user: enforce_user) } + + context 'true' do + let(:enforce_user) { true } + + it 'checks user format' do + badge.link_url = url + + subject + + expect(badge.errors.empty?).to be false + end + end + + context 'false (default)' do + let(:enforce_user) { false } + + it 'does not check user format' do + badge.link_url = url + + subject + + expect(badge.errors.empty?).to be true + end end end end diff --git a/spec/views/errors/access_denied.html.haml_spec.rb b/spec/views/errors/access_denied.html.haml_spec.rb new file mode 100644 index 00000000000..bde2f6f0169 --- /dev/null +++ b/spec/views/errors/access_denied.html.haml_spec.rb @@ -0,0 +1,7 @@ +require 'spec_helper' + +describe 'errors/access_denied' do + it 'does not fail to render when there is no message provided' do + expect { render }.not_to raise_error + end +end diff --git a/spec/views/projects/settings/ci_cd/_autodevops_form.html.haml_spec.rb b/spec/views/projects/settings/ci_cd/_autodevops_form.html.haml_spec.rb index d15391911c1..cb1b9e6f5fb 100644 --- a/spec/views/projects/settings/ci_cd/_autodevops_form.html.haml_spec.rb +++ b/spec/views/projects/settings/ci_cd/_autodevops_form.html.haml_spec.rb @@ -12,7 +12,7 @@ describe 'projects/settings/ci_cd/_autodevops_form' do it 'shows warning message' do render - expect(rendered).to have_css('.settings-message') + expect(rendered).to have_css('.auto-devops-warning-message') expect(rendered).to have_text('Auto Review Apps and Auto Deploy need a domain name and a') expect(rendered).to have_link('Kubernetes cluster') end @@ -26,7 +26,7 @@ describe 'projects/settings/ci_cd/_autodevops_form' do it 'shows warning message' do render - expect(rendered).to have_css('.settings-message') + expect(rendered).to have_css('.auto-devops-warning-message') expect(rendered).to have_text('Auto Review Apps and Auto Deploy need a') expect(rendered).to have_link('Kubernetes cluster') end @@ -42,7 +42,7 @@ describe 'projects/settings/ci_cd/_autodevops_form' do it 'shows warning message' do render - expect(rendered).to have_css('.settings-message') + expect(rendered).to have_css('.auto-devops-warning-message') expect(rendered).to have_text('Auto Review Apps and Auto Deploy need a domain name to work correctly.') end end @@ -55,7 +55,7 @@ describe 'projects/settings/ci_cd/_autodevops_form' do it 'does not show warning message' do render - expect(rendered).not_to have_css('.settings-message') + expect(rendered).not_to have_css('.auto-devops-warning-message') end end end diff --git a/spec/workers/ci/archive_traces_cron_worker_spec.rb b/spec/workers/ci/archive_traces_cron_worker_spec.rb new file mode 100644 index 00000000000..9af51b7d4d8 --- /dev/null +++ b/spec/workers/ci/archive_traces_cron_worker_spec.rb @@ -0,0 +1,59 @@ +require 'spec_helper' + +describe Ci::ArchiveTracesCronWorker do + subject { described_class.new.perform } + + before do + stub_feature_flags(ci_enable_live_trace: true) + end + + shared_examples_for 'archives trace' do + it do + subject + + build.reload + expect(build.job_artifacts_trace).to be_exist + end + end + + shared_examples_for 'does not archive trace' do + it do + subject + + build.reload + expect(build.job_artifacts_trace).to be_nil + end + end + + context 'when a job was succeeded' do + let!(:build) { create(:ci_build, :success, :trace_live) } + + it_behaves_like 'archives trace' + + context 'when archive raised an exception' do + let!(:build) { create(:ci_build, :success, :trace_artifact, :trace_live) } + let!(:build2) { create(:ci_build, :success, :trace_live) } + + it 'archives valid targets' do + expect(Rails.logger).to receive(:error).with("Failed to archive stale live trace. id: #{build.id} message: Already archived") + + subject + + build2.reload + expect(build2.job_artifacts_trace).to be_exist + end + end + end + + context 'when a job was cancelled' do + let!(:build) { create(:ci_build, :canceled, :trace_live) } + + it_behaves_like 'archives trace' + end + + context 'when a job is running' do + let!(:build) { create(:ci_build, :running, :trace_live) } + + it_behaves_like 'does not archive trace' + end +end diff --git a/spec/workers/every_sidekiq_worker_spec.rb b/spec/workers/every_sidekiq_worker_spec.rb index 9e3b99b3502..2106959e23c 100644 --- a/spec/workers/every_sidekiq_worker_spec.rb +++ b/spec/workers/every_sidekiq_worker_spec.rb @@ -13,7 +13,7 @@ describe 'Every Sidekiq worker' do file_worker_queues = Gitlab::SidekiqConfig.worker_queues.to_set worker_queues = Gitlab::SidekiqConfig.workers.map(&:queue).to_set - worker_queues << ActionMailer::DeliveryJob.queue_name + worker_queues << ActionMailer::DeliveryJob.new.queue_name worker_queues << 'default' missing_from_file = worker_queues - file_worker_queues diff --git a/spec/workers/git_garbage_collect_worker_spec.rb b/spec/workers/git_garbage_collect_worker_spec.rb index 74539a7e493..e39dec556fc 100644 --- a/spec/workers/git_garbage_collect_worker_spec.rb +++ b/spec/workers/git_garbage_collect_worker_spec.rb @@ -11,36 +11,63 @@ describe GitGarbageCollectWorker do subject { described_class.new } describe "#perform" do - shared_examples 'flushing ref caches' do |gitaly| - context 'with active lease_uuid' do + context 'with active lease_uuid' do + before do + allow(subject).to receive(:get_lease_uuid).and_return(lease_uuid) + end + + it "flushes ref caches when the task if 'gc'" do + expect(subject).to receive(:renew_lease).with(lease_key, lease_uuid).and_call_original + expect_any_instance_of(Gitlab::GitalyClient::RepositoryService).to receive(:garbage_collect) + .and_return(nil) + expect_any_instance_of(Repository).to receive(:after_create_branch).and_call_original + expect_any_instance_of(Repository).to receive(:branch_names).and_call_original + expect_any_instance_of(Repository).to receive(:has_visible_content?).and_call_original + expect_any_instance_of(Gitlab::Git::Repository).to receive(:has_visible_content?).and_call_original + + subject.perform(project.id, :gc, lease_key, lease_uuid) + end + end + + context 'with different lease than the active one' do + before do + allow(subject).to receive(:get_lease_uuid).and_return(SecureRandom.uuid) + end + + it 'returns silently' do + expect_any_instance_of(Repository).not_to receive(:after_create_branch).and_call_original + expect_any_instance_of(Repository).not_to receive(:branch_names).and_call_original + expect_any_instance_of(Repository).not_to receive(:has_visible_content?).and_call_original + + subject.perform(project.id, :gc, lease_key, lease_uuid) + end + end + + context 'with no active lease' do + before do + allow(subject).to receive(:get_lease_uuid).and_return(false) + end + + context 'when is able to get the lease' do before do - allow(subject).to receive(:get_lease_uuid).and_return(lease_uuid) + allow(subject).to receive(:try_obtain_lease).and_return(SecureRandom.uuid) end it "flushes ref caches when the task if 'gc'" do - expect(subject).to receive(:renew_lease).with(lease_key, lease_uuid).and_call_original - expect(subject).to receive(:command).with(:gc).and_return([:the, :command]) - - if gitaly - expect_any_instance_of(Gitlab::GitalyClient::RepositoryService).to receive(:garbage_collect) - .and_return(nil) - else - expect(Gitlab::Popen).to receive(:popen) - .with([:the, :command], project.repository.path_to_repo).and_return(["", 0]) - end - + expect_any_instance_of(Gitlab::GitalyClient::RepositoryService).to receive(:garbage_collect) + .and_return(nil) expect_any_instance_of(Repository).to receive(:after_create_branch).and_call_original expect_any_instance_of(Repository).to receive(:branch_names).and_call_original expect_any_instance_of(Repository).to receive(:has_visible_content?).and_call_original expect_any_instance_of(Gitlab::Git::Repository).to receive(:has_visible_content?).and_call_original - subject.perform(project.id, :gc, lease_key, lease_uuid) + subject.perform(project.id) end end - context 'with different lease than the active one' do + context 'when no lease can be obtained' do before do - allow(subject).to receive(:get_lease_uuid).and_return(SecureRandom.uuid) + expect(subject).to receive(:try_obtain_lease).and_return(false) end it 'returns silently' do @@ -49,65 +76,11 @@ describe GitGarbageCollectWorker do expect_any_instance_of(Repository).not_to receive(:branch_names).and_call_original expect_any_instance_of(Repository).not_to receive(:has_visible_content?).and_call_original - subject.perform(project.id, :gc, lease_key, lease_uuid) - end - end - - context 'with no active lease' do - before do - allow(subject).to receive(:get_lease_uuid).and_return(false) - end - - context 'when is able to get the lease' do - before do - allow(subject).to receive(:try_obtain_lease).and_return(SecureRandom.uuid) - end - - it "flushes ref caches when the task if 'gc'" do - expect(subject).to receive(:command).with(:gc).and_return([:the, :command]) - - if gitaly - expect_any_instance_of(Gitlab::GitalyClient::RepositoryService).to receive(:garbage_collect) - .and_return(nil) - else - expect(Gitlab::Popen).to receive(:popen) - .with([:the, :command], project.repository.path_to_repo).and_return(["", 0]) - end - - expect_any_instance_of(Repository).to receive(:after_create_branch).and_call_original - expect_any_instance_of(Repository).to receive(:branch_names).and_call_original - expect_any_instance_of(Repository).to receive(:has_visible_content?).and_call_original - expect_any_instance_of(Gitlab::Git::Repository).to receive(:has_visible_content?).and_call_original - - subject.perform(project.id) - end - end - - context 'when no lease can be obtained' do - before do - expect(subject).to receive(:try_obtain_lease).and_return(false) - end - - it 'returns silently' do - expect(subject).not_to receive(:command) - expect_any_instance_of(Repository).not_to receive(:after_create_branch).and_call_original - expect_any_instance_of(Repository).not_to receive(:branch_names).and_call_original - expect_any_instance_of(Repository).not_to receive(:has_visible_content?).and_call_original - - subject.perform(project.id) - end + subject.perform(project.id) end end end - context "with Gitaly turned on" do - it_should_behave_like 'flushing ref caches', true - end - - context "with Gitaly turned off", :skip_gitaly_mock do - it_should_behave_like 'flushing ref caches', false - end - context "repack_full" do before do expect(subject).to receive(:get_lease_uuid).and_return(lease_uuid) @@ -218,7 +191,9 @@ describe GitGarbageCollectWorker do # Create a new commit on a random new branch def create_objects(project) - rugged = project.repository.rugged + rugged = Gitlab::GitalyClient::StorageSettings.allow_disk_access do + project.repository.rugged + end old_commit = rugged.branches.first.target new_commit_sha = Rugged::Commit.create( rugged, @@ -237,7 +212,9 @@ describe GitGarbageCollectWorker do end def packs(project) - Dir["#{project.repository.path_to_repo}/objects/pack/*.pack"] + Gitlab::GitalyClient::StorageSettings.allow_disk_access do + Dir["#{project.repository.path_to_repo}/objects/pack/*.pack"] + end end def packed_refs(project) diff --git a/spec/workers/gitlab/github_import/stage/import_lfs_objects_worker_spec.rb b/spec/workers/gitlab/github_import/stage/import_lfs_objects_worker_spec.rb new file mode 100644 index 00000000000..b19884d7991 --- /dev/null +++ b/spec/workers/gitlab/github_import/stage/import_lfs_objects_worker_spec.rb @@ -0,0 +1,28 @@ +require 'spec_helper' + +describe Gitlab::GithubImport::Stage::ImportLfsObjectsWorker do + let(:project) { create(:project) } + let(:worker) { described_class.new } + + describe '#import' do + it 'imports all the lfs objects' do + importer = double(:importer) + waiter = Gitlab::JobWaiter.new(2, '123') + + expect(Gitlab::GithubImport::Importer::LfsObjectsImporter) + .to receive(:new) + .with(project, nil) + .and_return(importer) + + expect(importer) + .to receive(:execute) + .and_return(waiter) + + expect(Gitlab::GithubImport::AdvanceStageWorker) + .to receive(:perform_async) + .with(project.id, { '123' => 2 }, :finish) + + worker.import(project) + end + end +end diff --git a/spec/workers/gitlab/github_import/stage/import_notes_worker_spec.rb b/spec/workers/gitlab/github_import/stage/import_notes_worker_spec.rb index 098d2d55386..94cff9e4e80 100644 --- a/spec/workers/gitlab/github_import/stage/import_notes_worker_spec.rb +++ b/spec/workers/gitlab/github_import/stage/import_notes_worker_spec.rb @@ -21,7 +21,7 @@ describe Gitlab::GithubImport::Stage::ImportNotesWorker do expect(Gitlab::GithubImport::AdvanceStageWorker) .to receive(:perform_async) - .with(project.id, { '123' => 2 }, :finish) + .with(project.id, { '123' => 2 }, :lfs_objects) worker.import(client, project) end diff --git a/spec/workers/project_destroy_worker_spec.rb b/spec/workers/project_destroy_worker_spec.rb index f19c9dff941..42e1d86e3bb 100644 --- a/spec/workers/project_destroy_worker_spec.rb +++ b/spec/workers/project_destroy_worker_spec.rb @@ -2,7 +2,11 @@ require 'spec_helper' describe ProjectDestroyWorker do let(:project) { create(:project, :repository, pending_delete: true) } - let(:path) { project.repository.path_to_repo } + let(:path) do + Gitlab::GitalyClient::StorageSettings.allow_disk_access do + project.repository.path_to_repo + end + end subject { described_class.new } diff --git a/spec/workers/repository_check/single_repository_worker_spec.rb b/spec/workers/repository_check/single_repository_worker_spec.rb index a021235aed6..22fc64c1536 100644 --- a/spec/workers/repository_check/single_repository_worker_spec.rb +++ b/spec/workers/repository_check/single_repository_worker_spec.rb @@ -88,7 +88,9 @@ describe RepositoryCheck::SingleRepositoryWorker do end def break_wiki(project) - break_repo(wiki_path(project)) + Gitlab::GitalyClient::StorageSettings.allow_disk_access do + break_repo(wiki_path(project)) + end end def wiki_path(project) @@ -96,7 +98,9 @@ describe RepositoryCheck::SingleRepositoryWorker do end def break_project(project) - break_repo(project.repository.path_to_repo) + Gitlab::GitalyClient::StorageSettings.allow_disk_access do + break_repo(project.repository.path_to_repo) + end end def break_repo(repo) diff --git a/spec/workers/repository_fork_worker_spec.rb b/spec/workers/repository_fork_worker_spec.rb index 4b3c1736ea0..5d83397e8df 100644 --- a/spec/workers/repository_fork_worker_spec.rb +++ b/spec/workers/repository_fork_worker_spec.rb @@ -55,10 +55,15 @@ describe RepositoryForkWorker do it 'flushes various caches' do expect_fork_repository.and_return(true) - expect_any_instance_of(Repository).to receive(:expire_emptiness_caches) + # Works around https://github.com/rspec/rspec-mocks/issues/910 + expect(Project).to receive(:find).with(fork_project.id).and_return(fork_project) + expect(fork_project.repository).to receive(:expire_emptiness_caches) .and_call_original - - expect_any_instance_of(Repository).to receive(:expire_exists_cache) + expect(fork_project.repository).to receive(:expire_exists_cache) + .and_call_original + expect(fork_project.wiki.repository).to receive(:expire_emptiness_caches) + .and_call_original + expect(fork_project.wiki.repository).to receive(:expire_exists_cache) .and_call_original perform! @@ -89,6 +94,9 @@ describe RepositoryForkWorker do it_behaves_like 'RepositoryForkWorker performing' it 'logs a message about forking with old-style arguments' do + allow(subject).to receive(:gitlab_shell).and_return(shell) + expect(shell).to receive(:fork_repository) { true } + allow(Rails.logger).to receive(:info).with(anything) # To compensate for other logs expect(Rails.logger).to receive(:info).with("Project #{fork_project.id} is being forked using old-style arguments.") diff --git a/spec/workers/repository_import_worker_spec.rb b/spec/workers/repository_import_worker_spec.rb index 84d1b38ef19..f0884ad0aff 100644 --- a/spec/workers/repository_import_worker_spec.rb +++ b/spec/workers/repository_import_worker_spec.rb @@ -22,8 +22,11 @@ describe RepositoryImportWorker do expect_any_instance_of(Projects::ImportService).to receive(:execute) .and_return({ status: :ok }) - expect_any_instance_of(Repository).to receive(:expire_emptiness_caches) - expect_any_instance_of(Project).to receive(:import_finish) + # Works around https://github.com/rspec/rspec-mocks/issues/910 + expect(Project).to receive(:find).with(project.id).and_return(project) + expect(project.repository).to receive(:expire_emptiness_caches) + expect(project.wiki.repository).to receive(:expire_emptiness_caches) + expect(project).to receive(:import_finish) subject.perform(project.id) end @@ -34,9 +37,11 @@ describe RepositoryImportWorker do expect_any_instance_of(Projects::ImportService).to receive(:execute) .and_return({ status: :ok }) - expect_any_instance_of(Project).to receive(:after_import).and_call_original - expect_any_instance_of(Repository).to receive(:expire_emptiness_caches) - expect_any_instance_of(Project).to receive(:import_finish) + # Works around https://github.com/rspec/rspec-mocks/issues/910 + expect(Project).to receive(:find).with(project.id).and_return(project) + expect(project.repository).to receive(:expire_emptiness_caches) + expect(project.wiki.repository).to receive(:expire_emptiness_caches) + expect(project).to receive(:import_finish) subject.perform(project.id) end diff --git a/spec/workers/repository_remove_remote_worker_spec.rb b/spec/workers/repository_remove_remote_worker_spec.rb index f22d7c1d073..5968c5da3c9 100644 --- a/spec/workers/repository_remove_remote_worker_spec.rb +++ b/spec/workers/repository_remove_remote_worker_spec.rb @@ -44,7 +44,9 @@ describe RepositoryRemoveRemoteWorker do end def create_remote_branch(remote_name, branch_name, target) - rugged = project.repository.rugged + rugged = Gitlab::GitalyClient::StorageSettings.allow_disk_access do + project.repository.rugged + end rugged.references.create("refs/remotes/#{remote_name}/#{branch_name}", target.id) end end diff --git a/spec/workers/storage_migrator_worker_spec.rb b/spec/workers/storage_migrator_worker_spec.rb index ff625164142..815432aacce 100644 --- a/spec/workers/storage_migrator_worker_spec.rb +++ b/spec/workers/storage_migrator_worker_spec.rb @@ -2,29 +2,24 @@ require 'spec_helper' describe StorageMigratorWorker do subject(:worker) { described_class.new } - let(:projects) { create_list(:project, 2, :legacy_storage) } + let(:projects) { create_list(:project, 2, :legacy_storage, :empty_repo) } + let(:ids) { projects.map(&:id) } describe '#perform' do - let(:ids) { projects.map(&:id) } + it 'delegates to MigratorService' do + expect_any_instance_of(Gitlab::HashedStorage::Migrator).to receive(:bulk_migrate).with(5, 10) - it 'enqueue jobs to ProjectMigrateHashedStorageWorker' do - expect(ProjectMigrateHashedStorageWorker).to receive(:perform_async).twice - - worker.perform(ids.min, ids.max) + worker.perform(5, 10) end - it 'sets projects as read only' do - allow(ProjectMigrateHashedStorageWorker).to receive(:perform_async).twice - worker.perform(ids.min, ids.max) + it 'migrates projects in the specified range' do + Sidekiq::Testing.inline! do + worker.perform(ids.min, ids.max) + end projects.each do |project| - expect(project.reload.repository_read_only?).to be_truthy + expect(project.reload.hashed_storage?(:attachments)).to be_truthy end end - - it 'rescues and log exceptions' do - allow_any_instance_of(Project).to receive(:migrate_to_hashed_storage!).and_raise(StandardError) - expect { worker.perform(ids.min, ids.max) }.not_to raise_error - end end end diff --git a/spec/workers/stuck_ci_jobs_worker_spec.rb b/spec/workers/stuck_ci_jobs_worker_spec.rb index bdc64c6785b..2605c14334f 100644 --- a/spec/workers/stuck_ci_jobs_worker_spec.rb +++ b/spec/workers/stuck_ci_jobs_worker_spec.rb @@ -132,8 +132,10 @@ describe StuckCiJobsWorker do end it 'cancels exclusive lease after worker perform' do - expect(Gitlab::ExclusiveLease).to receive(:cancel).with(described_class::EXCLUSIVE_LEASE_KEY, exclusive_lease_uuid) worker.perform + + expect(Gitlab::ExclusiveLease.new(described_class::EXCLUSIVE_LEASE_KEY, timeout: 1.hour)) + .not_to be_exists end end end diff --git a/vendor/gitignore/Dart.gitignore b/vendor/gitignore/Dart.gitignore index 7bf00e82cc9..dbef116d224 100644 --- a/vendor/gitignore/Dart.gitignore +++ b/vendor/gitignore/Dart.gitignore @@ -3,7 +3,6 @@ # Files and directories created by pub .dart_tool/ .packages -.pub/ build/ # If you're building an application, you may want to check-in your pubspec.lock pubspec.lock @@ -11,3 +10,12 @@ pubspec.lock # Directory created by dartdoc # If you don't generate documentation locally you can remove this line. doc/api/ + +# Avoid committing generated Javascript files: +*.dart.js +*.info.json # Produced by the --dump-info flag. +*.js # When generated by dart2js. Don't specify *.js if your + # project includes source files written in JavaScript. +*.js_ +*.js.deps +*.js.map diff --git a/vendor/gitignore/Global/JetBrains.gitignore b/vendor/gitignore/Global/JetBrains.gitignore index b09cb3dbc04..4d5117a1d9d 100644 --- a/vendor/gitignore/Global/JetBrains.gitignore +++ b/vendor/gitignore/Global/JetBrains.gitignore @@ -14,6 +14,7 @@ .idea/**/sqlDataSources.xml .idea/**/dynamic.xml .idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml # Gradle .idea/**/gradle.xml diff --git a/vendor/gitignore/Node.gitignore b/vendor/gitignore/Node.gitignore index ad46b30886f..4454ba1b5bf 100644 --- a/vendor/gitignore/Node.gitignore +++ b/vendor/gitignore/Node.gitignore @@ -59,3 +59,9 @@ typings/ # next.js build output .next + +# vuepress build output +.vuepress/dist + +# Serverless directories +.serverless diff --git a/vendor/gitignore/Sass.gitignore b/vendor/gitignore/Sass.gitignore index 486b32ce90c..159f515170b 100644 --- a/vendor/gitignore/Sass.gitignore +++ b/vendor/gitignore/Sass.gitignore @@ -1,2 +1,4 @@ .sass-cache/ *.css.map +*.sass.map +*.scss.map diff --git a/vendor/gitignore/TeX.gitignore b/vendor/gitignore/TeX.gitignore index e6598ba1727..3d12d3f90ad 100644 --- a/vendor/gitignore/TeX.gitignore +++ b/vendor/gitignore/TeX.gitignore @@ -40,6 +40,10 @@ *.synctex.gz(busy) *.pdfsync +## Build tool directories for auxiliary files +# latexrun +latex.out/ + ## Auxiliary and intermediate files from other packages: # algorithms *.alg diff --git a/vendor/gitignore/Terraform.gitignore b/vendor/gitignore/Terraform.gitignore index 1fef4ab91e1..d9397e2d7d9 100644 --- a/vendor/gitignore/Terraform.gitignore +++ b/vendor/gitignore/Terraform.gitignore @@ -1,9 +1,15 @@ -# Local .terraform directories +# Local .terraform directories **/.terraform/* # .tfstate files *.tfstate *.tfstate.* -# .tfvars files -*.tfvars +# Crash log files +crash.log + +# Ignore any .tfvars files that are generated automatically for each Terraform run. Most +# .tfvars files are managed as part of configuration and so should be included in +# version control. +# +# example.tfvars diff --git a/vendor/gitignore/VisualStudio.gitignore b/vendor/gitignore/VisualStudio.gitignore index 3e759b75bf4..1e9abf78d69 100644 --- a/vendor/gitignore/VisualStudio.gitignore +++ b/vendor/gitignore/VisualStudio.gitignore @@ -52,7 +52,6 @@ BenchmarkDotNet.Artifacts/ project.lock.json project.fragment.lock.json artifacts/ -**/Properties/launchSettings.json # StyleCop StyleCopReport.xml diff --git a/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml b/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml index 589ebcf1414..0d58a00482a 100644 --- a/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml +++ b/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml @@ -7,6 +7,18 @@ # * creating a review app for each topic branch, # * and continuous deployment to production # +# Test jobs may be disabled by setting environment variables: +# * test: TEST_DISABLED +# * code_quality: CODE_QUALITY_DISABLED +# * license_management: LICENSE_MANAGEMENT_DISABLED +# * performance: PERFORMANCE_DISABLED +# * sast: SAST_DISABLED +# * dependency_scanning: DEPENDENCY_SCANNING_DISABLED +# * container_scanning: CONTAINER_SCANNING_DISABLED +# * dast: DAST_DISABLED +# * review: REVIEW_DISABLED +# * stop_review: REVIEW_DISABLED +# # In order to deploy, you must have a Kubernetes cluster configured either # via a project integration, or via group/project variables. # AUTO_DEVOPS_DOMAIN must also be set as a variable at the group or project @@ -15,7 +27,7 @@ # Continuous deployment to production is enabled by default. # If you want to deploy to staging first, or enable incremental rollouts, # set STAGING_ENABLED or INCREMENTAL_ROLLOUT_ENABLED environment variables. -# If you want to use canary deployments, uncomment the canary job. +# If you want to use canary deployments, set CANARY_ENABLED environment variable. # # If Auto DevOps fails to detect the proper buildpack, or if you want to # specify a custom buildpack, set a project variable `BUILDPACK_URL` to the @@ -76,8 +88,12 @@ test: - /bin/herokuish buildpack test only: - branches + except: + variables: + - $TEST_DISABLED code_quality: + stage: test image: docker:stable variables: DOCKER_DRIVER: overlay2 @@ -89,6 +105,25 @@ code_quality: - code_quality artifacts: paths: [gl-code-quality-report.json] + except: + variables: + - $CODE_QUALITY_DISABLED + +license_management: + image: docker:stable + variables: + DOCKER_DRIVER: overlay2 + allow_failure: true + services: + - docker:stable-dind + script: + - setup_docker + - license_management + artifacts: + paths: [gl-license-management-report.json] + except: + variables: + - $LICENSE_MANAGEMENT_DISABLED performance: stage: performance @@ -109,8 +144,12 @@ performance: refs: - branches kubernetes: active + except: + variables: + - $PERFORMANCE_DISABLED sast: + stage: test image: docker:stable variables: DOCKER_DRIVER: overlay2 @@ -122,8 +161,12 @@ sast: - sast artifacts: paths: [gl-sast-report.json] + except: + variables: + - $SAST_DISABLED dependency_scanning: + stage: test image: docker:stable variables: DOCKER_DRIVER: overlay2 @@ -135,8 +178,12 @@ dependency_scanning: - dependency_scanning artifacts: paths: [gl-dependency-scanning-report.json] + except: + variables: + - $DEPENDENCY_SCANNING_DISABLED container_scanning: + stage: test image: docker:stable variables: DOCKER_DRIVER: overlay2 @@ -148,6 +195,9 @@ container_scanning: - container_scanning artifacts: paths: [gl-container-scanning-report.json] + except: + variables: + - $CONTAINER_SCANNING_DISABLED dast: stage: dast @@ -164,7 +214,10 @@ dast: - branches kubernetes: active except: - - master + refs: + - master + variables: + - $DAST_DISABLED review: stage: review @@ -188,7 +241,10 @@ review: - branches kubernetes: active except: - - master + refs: + - master + variables: + - $REVIEW_DISABLED stop_review: stage: cleanup @@ -207,7 +263,10 @@ stop_review: - branches kubernetes: active except: - - master + refs: + - master + variables: + - $REVIEW_DISABLED # Keys that start with a dot (.) will not be processed by GitLab CI. # Staging and canary jobs are disabled by default, to enable them @@ -240,10 +299,11 @@ staging: variables: - $STAGING_ENABLED -# Canaries are disabled by default, but if you want them, -# and know what the downsides are, enable this job by removing the dot (.). +# Canaries are also disabled by default, but if you want them, +# and know what the downsides are, you can enable this by setting +# CANARY_ENABLED. -.canary: +canary: stage: canary script: - check_kube_domain @@ -261,6 +321,8 @@ staging: refs: - master kubernetes: active + variables: + - $CANARY_ENABLED .production: &production_template stage: production @@ -290,6 +352,7 @@ production: except: variables: - $STAGING_ENABLED + - $CANARY_ENABLED - $INCREMENTAL_ROLLOUT_ENABLED production_manual: @@ -416,6 +479,18 @@ rollout 100%: "registry.gitlab.com/gitlab-org/security-products/codequality:$SP_VERSION" /code } + function license_management() { + if echo $GITLAB_FEATURES |grep license_management > /dev/null ; then + # Extract "MAJOR.MINOR" from CI_SERVER_VERSION and generate "MAJOR-MINOR-stable" + LICENSE_MANAGEMENT_VERSION=$(echo "$CI_SERVER_VERSION" | sed 's/^\([0-9]*\)\.\([0-9]*\).*/\1-\2-stable/') + + docker run --volume "$PWD:/code" \ + "registry.gitlab.com/gitlab-org/security-products/license-management:$LICENSE_MANAGEMENT_VERSION" analyze /code + else + echo "License management is not available in your subscription" + fi + } + function sast() { case "$CI_SERVER_VERSION" in *-ee) @@ -615,7 +690,7 @@ rollout 100%: function check_kube_domain() { if [ -z ${AUTO_DEVOPS_DOMAIN+x} ]; then echo "In order to deploy or use Review Apps, AUTO_DEVOPS_DOMAIN variable must be set" - echo "You can do it in Auto DevOps project settings or defining a secret variable at group or project level" + echo "You can do it in Auto DevOps project settings or defining a variable at group or project level" echo "You can also manually add it in .gitlab-ci.yml" false else @@ -624,7 +699,6 @@ rollout 100%: } function build() { - if [[ -n "$CI_REGISTRY_USER" ]]; then echo "Logging to GitLab Container Registry with CI credentials..." docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" "$CI_REGISTRY" @@ -636,7 +710,7 @@ rollout 100%: docker build -t "$CI_APPLICATION_REPOSITORY:$CI_APPLICATION_TAG" . else echo "Building Heroku-based application using gliderlabs/herokuish docker image..." - docker run -i --name="$CI_CONTAINER_NAME" -v "$(pwd):/tmp/app:ro" gliderlabs/herokuish /bin/herokuish buildpack build + docker run -i -e BUILDPACK_URL --name="$CI_CONTAINER_NAME" -v "$(pwd):/tmp/app:ro" gliderlabs/herokuish /bin/herokuish buildpack build docker commit "$CI_CONTAINER_NAME" "$CI_APPLICATION_REPOSITORY:$CI_APPLICATION_TAG" docker rm "$CI_CONTAINER_NAME" >/dev/null echo "" diff --git a/vendor/gitlab-ci-yml/autodeploy/Kubernetes-with-canary.gitlab-ci.yml b/vendor/gitlab-ci-yml/autodeploy/Kubernetes-with-canary.gitlab-ci.yml deleted file mode 100644 index 6e5fe97cf6d..00000000000 --- a/vendor/gitlab-ci-yml/autodeploy/Kubernetes-with-canary.gitlab-ci.yml +++ /dev/null @@ -1,87 +0,0 @@ -# This template has been DEPRECATED. Consider using Auto DevOps instead: -# https://docs.gitlab.com/ee/topics/autodevops - -# Explanation on the scripts: -# https://gitlab.com/gitlab-examples/kubernetes-deploy/blob/master/README.md -image: registry.gitlab.com/gitlab-examples/kubernetes-deploy - -variables: - # Application deployment domain - KUBE_DOMAIN: domain.example.com - -stages: - - build - - test - - review - - staging - - canary - - production - - cleanup - -build: - stage: build - script: - - command build - only: - - branches - -canary: - stage: canary - script: - - command canary - environment: - name: production - url: http://$CI_PROJECT_PATH_SLUG.$KUBE_DOMAIN - when: manual - only: - - master - -production: - stage: production - script: - - command deploy - environment: - name: production - url: http://$CI_PROJECT_PATH_SLUG.$KUBE_DOMAIN - when: manual - only: - - master - -staging: - stage: staging - script: - - command deploy - environment: - name: staging - url: http://$CI_PROJECT_PATH_SLUG-staging.$KUBE_DOMAIN - only: - - master - -review: - stage: review - script: - - command deploy - environment: - name: review/$CI_COMMIT_REF_NAME - url: http://$CI_PROJECT_PATH_SLUG-$CI_ENVIRONMENT_SLUG.$KUBE_DOMAIN - on_stop: stop_review - only: - - branches - except: - - master - -stop_review: - stage: cleanup - variables: - GIT_STRATEGY: none - script: - - command destroy - environment: - name: review/$CI_COMMIT_REF_NAME - action: stop - when: manual - allow_failure: true - only: - - branches - except: - - master diff --git a/vendor/gitlab-ci-yml/autodeploy/Kubernetes.gitlab-ci.yml b/vendor/gitlab-ci-yml/autodeploy/Kubernetes.gitlab-ci.yml deleted file mode 100644 index 019a4d4cd7d..00000000000 --- a/vendor/gitlab-ci-yml/autodeploy/Kubernetes.gitlab-ci.yml +++ /dev/null @@ -1,74 +0,0 @@ -# This template has been DEPRECATED. Consider using Auto DevOps instead: -# https://docs.gitlab.com/ee/topics/autodevops - -# Explanation on the scripts: -# https://gitlab.com/gitlab-examples/kubernetes-deploy/blob/master/README.md -image: registry.gitlab.com/gitlab-examples/kubernetes-deploy - -variables: - # Application deployment domain - KUBE_DOMAIN: domain.example.com - -stages: - - build - - test - - review - - staging - - production - - cleanup - -build: - stage: build - script: - - command build - only: - - branches - -production: - stage: production - script: - - command deploy - environment: - name: production - url: http://$CI_PROJECT_PATH_SLUG.$KUBE_DOMAIN - when: manual - only: - - master - -staging: - stage: staging - script: - - command deploy - environment: - name: staging - url: http://$CI_PROJECT_PATH_SLUG-staging.$KUBE_DOMAIN - only: - - master - -review: - stage: review - script: - - command deploy - environment: - name: review/$CI_COMMIT_REF_NAME - url: http://$CI_PROJECT_PATH_SLUG-$CI_ENVIRONMENT_SLUG.$KUBE_DOMAIN - on_stop: stop_review - only: - - branches - except: - - master - -stop_review: - stage: cleanup - variables: - GIT_STRATEGY: none - script: - - command destroy - environment: - name: review/$CI_COMMIT_REF_NAME - action: stop - when: manual - only: - - branches - except: - - master diff --git a/vendor/gitlab-ci-yml/autodeploy/OpenShift.gitlab-ci.yml b/vendor/gitlab-ci-yml/autodeploy/OpenShift.gitlab-ci.yml deleted file mode 100644 index 60a9430a839..00000000000 --- a/vendor/gitlab-ci-yml/autodeploy/OpenShift.gitlab-ci.yml +++ /dev/null @@ -1,74 +0,0 @@ -# This template has been DEPRECATED. Consider using Auto DevOps instead: -# https://docs.gitlab.com/ee/topics/autodevops - -# Explanation on the scripts: -# https://gitlab.com/gitlab-examples/openshift-deploy/blob/master/README.md -image: registry.gitlab.com/gitlab-examples/openshift-deploy - -variables: - # Application deployment domain - KUBE_DOMAIN: domain.example.com - -stages: - - build - - test - - review - - staging - - production - - cleanup - -build: - stage: build - script: - - command build - only: - - branches - -production: - stage: production - script: - - command deploy - environment: - name: production - url: http://$CI_PROJECT_PATH_SLUG.$KUBE_DOMAIN - when: manual - only: - - master - -staging: - stage: staging - script: - - command deploy - environment: - name: staging - url: http://$CI_PROJECT_PATH_SLUG-staging.$KUBE_DOMAIN - only: - - master - -review: - stage: review - script: - - command deploy - environment: - name: review/$CI_COMMIT_REF_NAME - url: http://$CI_PROJECT_PATH_SLUG-$CI_ENVIRONMENT_SLUG.$KUBE_DOMAIN - on_stop: stop_review - only: - - branches - except: - - master - -stop_review: - stage: cleanup - variables: - GIT_STRATEGY: none - script: - - command destroy - environment: - name: review/$CI_COMMIT_REF_NAME - action: stop - when: manual - only: - - branches - except: - - master diff --git a/vendor/licenses.csv b/vendor/licenses.csv index 07861631a07..dada0da0b31 100644 --- a/vendor/licenses.csv +++ b/vendor/licenses.csv @@ -1,20 +1,41 @@ -@babel/code-frame,7.0.0-beta.32,MIT -@babel/helper-function-name,7.0.0-beta.32,MIT -@babel/helper-get-function-arity,7.0.0-beta.32,MIT -@babel/template,7.0.0-beta.32,MIT -@babel/traverse,7.0.0-beta.32,MIT -@babel/types,7.0.0-beta.32,MIT -@gitlab-org/gitlab-svgs,1.20.0,SEE LICENSE IN LICENSE -@mrmlnc/readdir-enhanced,2.2.1,MIT +@babel/code-frame,7.0.0-beta.44,MIT +@babel/generator,7.0.0-beta.44,MIT +@babel/helper-function-name,7.0.0-beta.44,MIT +@babel/helper-get-function-arity,7.0.0-beta.44,MIT +@babel/helper-split-export-declaration,7.0.0-beta.44,MIT +@babel/highlight,7.0.0-beta.44,MIT +@babel/template,7.0.0-beta.44,MIT +@babel/traverse,7.0.0-beta.44,MIT +@babel/types,7.0.0-beta.44,MIT +@gitlab-org/gitlab-svgs,1.23.0,SEE LICENSE IN LICENSE @sindresorhus/is,0.7.0,MIT @types/jquery,2.0.48,MIT +@vue/component-compiler-utils,1.2.1,MIT +@webassemblyjs/ast,1.5.10,MIT +@webassemblyjs/floating-point-hex-parser,1.5.10,MIT +@webassemblyjs/helper-api-error,1.5.10,MIT +@webassemblyjs/helper-buffer,1.5.10,MIT +@webassemblyjs/helper-code-frame,1.5.10,MIT +@webassemblyjs/helper-fsm,1.5.10,ISC +@webassemblyjs/helper-module-context,1.5.10,MIT +@webassemblyjs/helper-wasm-bytecode,1.5.10,MIT +@webassemblyjs/helper-wasm-section,1.5.10,MIT +@webassemblyjs/ieee754,1.5.10,Unknown +@webassemblyjs/leb128,1.5.10,Apache 2.0 +@webassemblyjs/utf8,1.5.10,MIT +@webassemblyjs/wasm-edit,1.5.10,MIT +@webassemblyjs/wasm-gen,1.5.10,MIT +@webassemblyjs/wasm-opt,1.5.10,MIT +@webassemblyjs/wasm-parser,1.5.10,MIT +@webassemblyjs/wast-parser,1.5.10,MIT +@webassemblyjs/wast-printer,1.5.10,MIT RedCloth,4.3.2,MIT abbrev,1.0.9,ISC abbrev,1.1.1,ISC accepts,1.3.4,MIT ace-rails-ap,4.1.2,MIT acorn,3.3.0,MIT -acorn,5.4.1,MIT +acorn,5.5.3,MIT acorn-dynamic-import,3.0.0,MIT acorn-jsx,3.0.1,MIT actionmailer,4.2.10,MIT @@ -33,7 +54,7 @@ agent-base,2.1.1,MIT ajv,4.11.8,MIT ajv,5.5.2,MIT ajv,6.1.1,MIT -ajv-keywords,1.5.1,MIT +ajv-keywords,2.1.1,MIT ajv-keywords,3.1.0,MIT akismet,2.0.0,MIT align-text,0.1.4,MIT @@ -42,15 +63,12 @@ alphanum-sort,1.0.2,MIT amdefine,1.0.1,BSD-3-Clause OR MIT amqplib,0.5.2,MIT ansi-align,2.0.0,ISC -ansi-escapes,1.4.0,MIT ansi-escapes,3.0.0,MIT ansi-html,0.0.7,Apache 2.0 ansi-regex,2.1.1,MIT ansi-regex,3.0.0,MIT -ansi-styles,1.0.0,MIT ansi-styles,2.2.1,MIT ansi-styles,3.2.1,MIT -any-observable,0.2.0,MIT anymatch,1.3.2,ISC anymatch,2.0.0,ISC append-transform,0.4.0,MIT @@ -62,7 +80,6 @@ arr-diff,2.0.0,MIT arr-diff,4.0.0,MIT arr-flatten,1.1.0,MIT arr-union,3.1.0,MIT -array-differ,1.0.0,MIT array-find,1.0.0,MIT array-find-index,1.0.2,MIT array-flatten,1.1.1,MIT @@ -85,12 +102,9 @@ assert-plus,0.2.0,MIT assert-plus,1.0.0,MIT asset_sync,2.4.0,MIT assign-symbols,1.0.0,MIT -ast-types,0.10.1,MIT -ast-types,0.11.1,MIT ast-types,0.11.3,MIT async,1.5.2,MIT async,2.1.5,MIT -async,2.4.1,MIT async,2.6.0,MIT async-each,1.0.1,MIT async-limiter,1.0.0,MIT @@ -100,7 +114,6 @@ atomic,1.1.99,Apache 2.0 attr_encrypted,3.1.0,MIT attr_required,1.0.0,MIT autoprefixer,6.7.7,MIT -autoprefixer-rails,6.2.3,MIT autosize,4.0.0,MIT aws-sign2,0.6.0,Apache 2.0 aws-sign2,0.7.0,Apache 2.0 @@ -108,10 +121,10 @@ aws4,1.6.0,MIT axiom-types,0.1.1,MIT axios,0.15.3,MIT axios,0.17.1,MIT -axios-mock-adapter,1.10.0,MIT +axios-mock-adapter,1.15.0,MIT babel-code-frame,6.26.0,MIT babel-core,6.26.3,MIT -babel-eslint,8.0.2,MIT +babel-eslint,8.2.3,MIT babel-generator,6.26.0,MIT babel-helper-bindify-decorators,6.24.1,MIT babel-helper-builder-binary-assignment-operator-visitor,6.24.1,MIT @@ -134,18 +147,14 @@ babel-plugin-istanbul,4.1.6,New BSD babel-plugin-rewire,1.1.0,ISC babel-plugin-syntax-async-functions,6.13.0,MIT babel-plugin-syntax-async-generators,6.13.0,MIT -babel-plugin-syntax-class-constructor-call,6.18.0,MIT babel-plugin-syntax-class-properties,6.13.0,MIT babel-plugin-syntax-decorators,6.13.0,MIT babel-plugin-syntax-dynamic-import,6.18.0,MIT babel-plugin-syntax-exponentiation-operator,6.13.0,MIT -babel-plugin-syntax-export-extensions,6.13.0,MIT -babel-plugin-syntax-flow,6.18.0,MIT babel-plugin-syntax-object-rest-spread,6.13.0,MIT babel-plugin-syntax-trailing-function-commas,6.22.0,MIT babel-plugin-transform-async-generator-functions,6.24.1,MIT babel-plugin-transform-async-to-generator,6.24.1,MIT -babel-plugin-transform-class-constructor-call,6.24.1,MIT babel-plugin-transform-class-properties,6.24.1,MIT babel-plugin-transform-decorators,6.24.1,MIT babel-plugin-transform-define,1.3.0,MIT @@ -172,8 +181,6 @@ babel-plugin-transform-es2015-template-literals,6.22.0,MIT babel-plugin-transform-es2015-typeof-symbol,6.23.0,MIT babel-plugin-transform-es2015-unicode-regex,6.24.1,MIT babel-plugin-transform-exponentiation-operator,6.24.1,MIT -babel-plugin-transform-export-extensions,6.22.0,MIT -babel-plugin-transform-flow-strip-types,6.22.0,MIT babel-plugin-transform-object-rest-spread,6.23.0,MIT babel-plugin-transform-regenerator,6.26.0,MIT babel-plugin-transform-strict-mode,6.24.1,MIT @@ -181,7 +188,6 @@ babel-preset-es2015,6.24.1,MIT babel-preset-es2016,6.24.1,MIT babel-preset-es2017,6.24.1,MIT babel-preset-latest,6.24.1,MIT -babel-preset-stage-1,6.24.1,MIT babel-preset-stage-2,6.24.1,MIT babel-preset-stage-3,6.24.1,MIT babel-register,6.26.0,MIT @@ -191,7 +197,6 @@ babel-traverse,6.26.0,MIT babel-types,6.26.0,MIT babosa,1.0.2,MIT babylon,6.18.0,MIT -babylon,7.0.0-beta.32,MIT babylon,7.0.0-beta.44,MIT backo2,1.0.2,MIT balanced-match,0.4.2,MIT @@ -210,9 +215,8 @@ better-assert,1.0.2,MIT bfj-node4,5.2.1,MIT big.js,3.1.3,MIT binary-extensions,1.11.0,MIT -binaryextensions,2.1.1,MIT bindata,2.4.3,ruby -bitsyntax,0.0.4,UNKNOWN +bitsyntax,0.0.4,Unknown bl,1.1.2,MIT blackst0ne-mermaid,7.1.0-fixed,MIT blob,0.0.4,MIT* @@ -224,7 +228,7 @@ bonjour,3.5.0,MIT boom,2.10.1,New BSD boom,4.3.1,New BSD boom,5.2.0,New BSD -bootstrap-sass,3.3.6,MIT +bootstrap,4.1.1,MIT bootstrap_form,2.7.0,MIT boxen,1.3.0,MIT brace-expansion,1.1.11,MIT @@ -238,9 +242,10 @@ browserify-cipher,1.0.0,MIT browserify-des,1.0.0,MIT browserify-rsa,4.0.1,MIT browserify-sign,4.0.4,ISC -browserify-zlib,0.1.4,MIT +browserify-zlib,0.2.0,MIT browserslist,1.7.7,MIT buffer,4.9.1,MIT +buffer-from,1.0.0,MIT buffer-indexof,1.1.0,MIT buffer-more-ints,0.0.2,MIT buffer-xor,1.0.3,MIT @@ -252,8 +257,8 @@ bytes,2.5.0,MIT bytes,3.0.0,MIT cacache,10.0.4,ISC cache-base,1.0.1,MIT +cache-loader,1.2.2,MIT cacheable-request,2.1.4,MIT -call-me-maybe,1.0.1,MIT caller-path,0.1.0,MIT callsite,1.0.0,MIT* callsites,0.2.0,MIT @@ -269,9 +274,7 @@ caseless,0.11.0,Apache 2.0 caseless,0.12.0,Apache 2.0 cause,0.1,MIT center-align,0.1.3,MIT -chalk,0.4.0,MIT chalk,1.1.3,MIT -chalk,2.4.0,MIT chalk,2.4.1,MIT chardet,0.4.2,MIT charenc,0.0.2,New BSD @@ -293,23 +296,13 @@ clap,1.1.3,MIT class-utils,0.3.6,MIT classlist-polyfill,1.2.0,Unlicense cli-boxes,1.0.0,MIT -cli-cursor,1.0.2,MIT cli-cursor,2.1.0,MIT -cli-spinners,0.1.2,MIT -cli-table,0.3.1,MIT -cli-truncate,0.2.1,MIT cli-width,2.1.0,ISC clipboard,1.7.1,MIT cliui,2.1.0,ISC cliui,4.0.0,ISC -clone,1.0.2,MIT clone,1.0.3,MIT -clone,2.1.1,MIT -clone-buffer,1.0.0,MIT clone-response,1.0.2,MIT -clone-stats,0.0.1,MIT -clone-stats,1.0.0,MIT -cloneable-readable,1.0.0,MIT co,3.0.6,MIT co,4.6.0,MIT coa,1.0.1,MIT @@ -321,7 +314,6 @@ color-convert,1.9.1,MIT color-name,1.1.2,MIT color-string,0.3.0,MIT colormin,1.1.2,MIT -colors,1.0.3,MIT colors,1.1.2,MIT combine-lists,1.0.1,MIT combined-stream,1.0.6,MIT @@ -336,7 +328,7 @@ compressible,2.0.11,MIT compression,1.7.0,MIT compression-webpack-plugin,1.1.11,MIT concat-map,0.0.1,MIT -concat-stream,1.6.0,MIT +concat-stream,1.6.2,MIT concurrent-ruby-ext,1.0.5,MIT configstore,3.1.1,Simplified BSD connect,3.6.6,MIT @@ -344,7 +336,7 @@ connect-history-api-fallback,1.3.0,MIT connection_pool,2.2.1,MIT console-browserify,1.1.0,MIT console-control-strings,1.1.0,ISC -consolidate,0.14.5,MIT +consolidate,0.15.1,MIT constants-browserify,1.0.0,MIT contains-path,0.1.0,MIT content-disposition,0.5.2,MIT @@ -354,11 +346,9 @@ cookie,0.3.1,MIT cookie-signature,1.0.6,MIT copy-concurrently,1.0.5,ISC copy-descriptor,0.1.1,MIT -copy-webpack-plugin,4.5.1,MIT core-js,2.3.0,MIT core-js,2.5.3,MIT core-util-is,1.0.2,MIT -cosmiconfig,2.2.2,MIT crack,0.4.3,MIT crass,1.0.4,MIT create-ecdh,4.0.0,MIT @@ -384,8 +374,6 @@ csso,2.3.2,MIT currently-unhandled,0.4.1,MIT custom-event,1.0.1,MIT cyclist,0.2.2,MIT* -d,0.1.1,MIT -d,1.0.0,MIT d3,3.5.17,New BSD d3-array,1.2.1,New BSD d3-axis,1.0.8,New BSD @@ -405,16 +393,12 @@ d3-time,1.0.8,New BSD d3-time-format,2.1.1,New BSD d3-timer,1.0.7,New BSD d3-transition,1.1.1,New BSD -d3_rails,3.5.11,MIT dagre-d3-renderer,0.4.24,MIT dagre-layout,0.8.0,MIT -dargs,5.1.0,MIT dashdash,1.14.1,MIT data-uri-to-buffer,1.2.0,MIT -date-fns,1.29.0,MIT date-format,1.2.0,MIT date-now,0.1.4,MIT -dateformat,3.0.3,MIT de-indent,1.0.2,MIT debug,2.2.0,MIT debug,2.6.8,MIT @@ -429,7 +413,6 @@ decode-uri-component,0.2.0,MIT decompress-response,3.3.0,MIT deep-equal,1.0.1,MIT deep-extend,0.4.2,MIT -deep-extend,0.5.1,MIT deep-is,0.1.3,MIT default-require-extensions,1.0.0,MIT default_value_for,3.0.2,MIT @@ -448,7 +431,6 @@ depd,1.1.1,MIT des.js,1.0.0,MIT descendants_tracker,0.0.4,MIT destroy,1.0.4,MIT -detect-conflict,1.0.1,MIT detect-indent,4.0.0,MIT detect-libc,1.0.3,Apache 2.0 detect-node,2.0.3,ISC @@ -456,28 +438,26 @@ device_detector,1.0.0,LGPL devise,4.4.3,MIT devise-two-factor,3.0.0,MIT di,0.0.1,MIT -diff,3.4.0,New BSD diff,3.5.0,New BSD diff-lcs,1.3,"MIT,Artistic-2.0,GPL-2.0+" diffie-hellman,5.0.2,MIT diffy,3.1.0,MIT -dir-glob,2.0.0,MIT dns-equal,1.0.0,MIT dns-packet,1.2.2,MIT dns-txt,2.0.2,MIT doctrine,1.5.0,Simplified BSD -doctrine,2.0.0,Apache 2.0 +doctrine,2.1.0,Apache 2.0 document-register-element,1.3.0,MIT dom-serialize,2.2.1,MIT dom-serializer,0.1.0,MIT domain-browser,1.1.7,MIT -domain_name,0.5.20170404,"Simplified BSD,New BSD,Mozilla Public License 2.0" +domain_name,0.5.20180417,"Simplified BSD,New BSD,Mozilla Public License 2.0" domelementtype,1.1.3,Simplified BSD domelementtype,1.3.0,Simplified BSD domhandler,2.4.1,Simplified BSD domutils,1.6.2,Simplified BSD doorkeeper,4.3.2,MIT -doorkeeper-openid_connect,1.3.0,MIT +doorkeeper-openid_connect,1.4.0,MIT dot-prop,4.2.0,MIT double-ended-queue,2.1.0-0,MIT dropzone,4.2.0,MIT @@ -486,12 +466,10 @@ duplexer,0.1.1,MIT duplexer3,0.1.4,New BSD duplexify,3.5.3,MIT ecc-jsbn,0.1.1,MIT -editions,1.3.4,MIT +ed25519,1.2.4,MIT ee-first,1.1.1,MIT -ejs,2.5.7,Apache 2.0 ejs,2.5.9,Apache 2.0 electron-to-chromium,1.3.3,ISC -elegant-spinner,1.0.1,MIT elliptic,6.4.0,MIT email_reply_trimmer,0.1.6,MIT emoji-unicode-version,0.2.1,MIT @@ -506,43 +484,34 @@ enhanced-resolve,0.9.1,MIT enhanced-resolve,4.0.0,MIT ent,2.2.0,MIT entities,1.1.1,Simplified BSD -envinfo,4.4.2,MIT equalizer,0.0.11,MIT errno,0.1.4,MIT errno,0.1.7,MIT -error,7.0.2,MIT error-ex,1.3.0,MIT -error-ex,1.3.1,MIT erubis,2.7.0,MIT es-abstract,1.10.0,MIT es-to-primitive,1.1.1,MIT -es5-ext,0.10.24,MIT -es6-iterator,2.0.1,MIT -es6-map,0.1.5,MIT es6-promise,3.0.2,MIT -es6-set,0.1.5,MIT -es6-symbol,3.1.1,MIT -es6-weak-map,2.0.1,MIT escape-html,1.0.3,MIT escape-string-regexp,1.0.5,MIT escape_utils,1.1.1,MIT escodegen,1.8.1,Simplified BSD escodegen,1.9.0,Simplified BSD -escope,3.6.0,Simplified BSD -eslint,3.19.0,MIT -eslint-config-airbnb-base,10.0.1,MIT -eslint-import-resolver-node,0.2.3,MIT -eslint-import-resolver-webpack,0.8.3,MIT -eslint-module-utils,2.0.0,MIT -eslint-plugin-filenames,1.1.0,MIT -eslint-plugin-html,2.0.1,ISC -eslint-plugin-import,2.2.0,MIT +eslint,4.12.1,MIT +eslint-config-airbnb-base,12.1.0,MIT +eslint-import-resolver-node,0.3.2,MIT +eslint-import-resolver-webpack,0.10.0,MIT +eslint-module-utils,2.2.0,MIT +eslint-plugin-filenames,1.2.0,MIT +eslint-plugin-html,4.0.3,ISC +eslint-plugin-import,2.12.0,MIT eslint-plugin-jasmine,2.2.0,MIT -eslint-plugin-promise,3.5.0,ISC +eslint-plugin-promise,3.8.0,ISC eslint-plugin-vue,4.0.1,MIT +eslint-restricted-globals,0.1.1,MIT eslint-scope,3.7.1,Simplified BSD eslint-visitor-keys,1.0.0,Apache 2.0 -espree,3.5.2,Simplified BSD +espree,3.5.4,Simplified BSD esprima,2.7.3,Simplified BSD esprima,3.1.3,Simplified BSD esprima,4.0.0,Simplified BSD @@ -555,7 +524,6 @@ esutils,2.0.2,Simplified BSD et-orbi,1.0.3,MIT etag,1.8.1,MIT eve-raphael,0.5.0,Apache 2.0 -event-emitter,0.3.5,MIT event-stream,3.3.4,MIT eventemitter3,1.2.0,MIT events,1.1.1,MIT @@ -564,20 +532,17 @@ evp_bytestokey,1.0.3,MIT excon,0.62.0,MIT execa,0.7.0,MIT execjs,2.6.0,MIT -exit-hook,1.1.1,MIT expand-braces,0.1.2,MIT expand-brackets,0.1.5,MIT expand-brackets,2.1.4,MIT expand-range,0.1.1,MIT expand-range,1.8.2,MIT -expand-tilde,2.0.2,MIT exports-loader,0.7.0,MIT express,4.16.2,MIT expression_parser,0.9.0,MIT extend,3.0.1,MIT extend-shallow,2.0.1,MIT extend-shallow,3.0.2,MIT -external-editor,2.1.0,MIT external-editor,2.2.0,MIT extglob,0.3.2,MIT extglob,2.0.4,MIT @@ -587,7 +552,6 @@ faraday,0.12.2,MIT faraday_middleware,0.12.2,MIT faraday_middleware-multi_json,0.0.6,MIT fast-deep-equal,1.0.0,MIT -fast-glob,2.2.1,MIT fast-json-stable-stringify,2.0.0,MIT fast-levenshtein,2.0.6,MIT fast_blank,1.0.0,MIT @@ -596,7 +560,6 @@ fastparse,1.1.1,MIT faye-websocket,0.10.0,MIT faye-websocket,0.11.1,MIT ffi,1.9.18,New BSD -figures,1.7.0,MIT figures,2.0.0,MIT file-entry-cache,2.0.0,MIT file-loader,1.1.11,MIT @@ -608,16 +571,14 @@ fill-range,2.2.3,MIT fill-range,4.0.0,MIT finalhandler,1.1.0,MIT find-cache-dir,1.0.0,MIT -find-root,0.1.2,MIT +find-root,1.1.0,MIT find-up,1.1.2,MIT find-up,2.1.0,MIT -first-chunk-stream,2.0.0,MIT flat-cache,1.2.2,MIT flatten,1.0.2,MIT flipper,0.13.0,MIT flipper-active_record,0.13.0,MIT flipper-active_support_cache_store,0.13.0,MIT -flow-parser,0.66.0,MIT flowdock,0.7.1,MIT flush-write-stream,1.0.2,MIT fog-aliyun,0.2.0,MIT @@ -653,6 +614,7 @@ fstream,1.0.11,ISC fstream-ignore,1.0.5,ISC ftp,0.3.10,MIT function-bind,1.1.1,MIT +functional-red-black-tree,1.0.1,MIT fuzzaldrin-plus,0.5.0,MIT gauge,2.7.4,ISC gemnasium-gitlab-service,0.2.6,MIT @@ -668,11 +630,9 @@ get_process_mem,0.2.0,MIT getpass,0.1.7,MIT gettext_i18n_rails,1.8.0,MIT gettext_i18n_rails_js,1.3.0,MIT -gh-got,6.0.0,MIT -gitaly-proto,0.99.0,MIT +gitaly-proto,0.100.0,MIT github-linguist,5.3.3,MIT github-markup,1.7.0,MIT -github-username,4.1.0,MIT gitlab-flowdock-git-hook,1.0.1,MIT gitlab-gollum-lib,4.2.7.2,MIT gitlab-gollum-rugged_adapter,0.4.4,MIT @@ -680,41 +640,35 @@ gitlab-grit,2.8.2,MIT gitlab-markup,1.6.3,MIT gitlab_omniauth-ldap,2.0.4,MIT glob,5.0.15,ISC -glob,7.1.1,ISC glob,7.1.2,ISC -glob-all,3.1.0,MIT glob-base,0.3.0,MIT glob-parent,2.0.0,ISC glob-parent,3.1.0,ISC -glob-to-regexp,0.3.0,BSD global-dirs,0.1.1,MIT -global-modules,1.0.0,MIT -global-prefix,1.0.2,MIT +global-modules-path,2.1.0,Apache 2.0 globalid,0.4.1,MIT -globals,10.4.0,MIT +globals,11.5.0,MIT globals,9.18.0,MIT globby,5.0.0,MIT globby,6.1.0,MIT -globby,7.1.1,MIT -globby,8.0.1,MIT gollum-grit_adapter,1.0.1,MIT -gon,6.1.0,MIT +gon,6.2.0,MIT good-listener,1.2.2,MIT google-api-client,0.19.8,Apache 2.0 google-protobuf,3.5.1,New BSD googleapis-common-protos-types,1.0.1,Apache 2.0 googleauth,0.6.2,Apache 2.0 got,6.7.1,MIT -got,7.1.0,MIT got,8.3.0,MIT gpgme,2.0.13,LGPL-2.1+ graceful-fs,4.1.11,ISC -grape,1.0.2,MIT +grape,1.0.3,MIT grape-entity,0.7.1,MIT -grape-route-helpers,2.1.0,MIT +grape-path-helpers,1.0.4,MIT grape_logging,1.7.0,MIT +graphiql-rails,1.4.10,MIT graphlib,2.1.1,MIT -grouped-queue,0.3.3,MIT +graphql,1.8.1,MIT grpc,1.11.0,Apache 2.0 gzip-size,4.1.0,MIT hamlit,2.6.1,MIT @@ -728,10 +682,8 @@ har-validator,5.0.3,ISC has,1.0.1,MIT has-ansi,2.0.0,MIT has-binary2,1.0.2,MIT -has-color,0.1.7,MIT has-cors,1.1.0,MIT has-flag,1.0.0,MIT -has-flag,2.0.0,MIT has-flag,3.0.0,MIT has-symbol-support-x,1.3.0,MIT has-to-string-tag-x,1.3.0,MIT @@ -756,7 +708,6 @@ hmac-drbg,1.0.1,MIT hoek,2.16.3,New BSD hoek,4.2.1,New BSD home-or-tmp,2.0.0,MIT -homedir-polyfill,1.0.1,MIT hosted-git-info,2.2.0,ISC hpack.js,2.1.6,MIT html-comment-regex,1.1.1,MIT @@ -781,17 +732,19 @@ httparty,0.13.7,MIT httpclient,2.8.3,ruby httpntlm,1.6.1,MIT httpreq,0.4.24,MIT -https-browserify,0.0.1,MIT +https-browserify,1.0.0,MIT https-proxy-agent,1.0.0,MIT i18n,0.9.5,MIT +icalendar,2.4.1,ruby ice_nine,0.11.2,MIT iconv-lite,0.4.15,MIT iconv-lite,0.4.19,MIT icss-replace-symbols,1.1.0,ISC icss-utils,2.1.0,ISC +ieee754,1.1.11,New BSD ieee754,1.1.8,New BSD iferr,0.1.5,MIT -ignore,3.3.7,MIT +ignore,3.3.8,MIT ignore-by-default,1.0.1,ISC immediate,3.0.6,MIT import-lazy,2.1.0,MIT @@ -799,7 +752,6 @@ import-local,1.0.0,MIT imports-loader,0.8.0,MIT imurmurhash,0.1.4,MIT indent-string,2.1.0,MIT -indent-string,3.2.0,MIT indexes-of,1.0.1,MIT indexof,0.0.1,MIT* inflection,1.10.0,MIT @@ -809,11 +761,9 @@ influxdb,0.2.3,MIT inherits,2.0.1,ISC inherits,2.0.3,ISC ini,1.3.5,ISC -inquirer,0.12.0,MIT inquirer,3.3.0,MIT inquirer,5.2.0,MIT internal-ip,1.2.0,MIT -interpret,1.0.1,MIT interpret,1.1.0,MIT into-stream,3.1.0,MIT invariant,2.2.2,New BSD @@ -822,7 +772,6 @@ ip,1.0.1,MIT ip,1.1.5,MIT ipaddr.js,1.6.0,MIT ipaddress,0.8.3,MIT -is-absolute,0.2.6,MIT is-absolute-url,2.1.0,MIT is-accessor-descriptor,0.1.6,MIT is-accessor-descriptor,1.0.0,MIT @@ -836,7 +785,6 @@ is-data-descriptor,1.0.0,MIT is-date-object,1.0.1,MIT is-descriptor,0.1.6,MIT is-descriptor,1.0.2,MIT -is-directory,0.3.1,MIT is-dotfile,1.0.3,MIT is-equal-shallow,0.1.3,MIT is-extendable,0.1.1,MIT @@ -859,7 +807,6 @@ is-number,3.0.0,MIT is-number,4.0.0,MIT is-obj,1.0.1,MIT is-object,1.0.1,MIT -is-observable,0.2.0,MIT is-odd,2.0.0,MIT is-path-cwd,1.0.0,MIT is-path-in-cwd,1.0.0,MIT @@ -872,17 +819,13 @@ is-promise,2.1.0,MIT is-property,1.0.2,MIT is-redirect,1.0.0,MIT is-regex,1.0.4,MIT -is-relative,0.2.1,MIT is-resolvable,1.0.0,MIT is-retry-allowed,1.1.0,MIT -is-scoped,1.0.0,MIT is-stream,1.1.0,MIT is-svg,2.1.0,MIT is-symbol,1.0.1,MIT is-typedarray,1.0.0,MIT -is-unc-path,0.1.2,MIT is-utf8,0.2.1,MIT -is-windows,0.2.0,MIT is-windows,1.0.2,MIT is-wsl,1.1.0,MIT isarray,0.0.1,MIT @@ -899,11 +842,9 @@ istanbul-lib-coverage,1.1.1,New BSD istanbul-lib-coverage,1.2.0,New BSD istanbul-lib-hook,1.1.0,New BSD istanbul-lib-instrument,1.10.1,New BSD -istanbul-lib-instrument,1.9.1,New BSD istanbul-lib-report,1.1.2,New BSD istanbul-lib-source-maps,1.2.2,New BSD istanbul-reports,1.1.3,New BSD -istextorbinary,2.2.1,MIT isurl,1.0.0,MIT jasmine-core,2.9.0,MIT jasmine-jquery,2.1.1,MIT @@ -918,12 +859,10 @@ js-cookie,2.1.3,MIT js-tokens,3.0.2,MIT js-yaml,3.11.0,MIT js-yaml,3.7.0,MIT -js-yaml,3.9.1,MIT jsbn,0.1.1,MIT -jscodeshift,0.4.1,New BSD -jscodeshift,0.5.0,New BSD jsesc,0.5.0,MIT jsesc,1.3.0,MIT +jsesc,2.5.1,MIT json,1.8.6,ruby json-buffer,3.0.0,MIT json-jwt,1.9.2,MIT @@ -931,6 +870,7 @@ json-parse-better-errors,1.0.2,MIT json-schema,0.2.3,BSD json-schema-traverse,0.3.1,MIT json-stable-stringify,1.0.1,MIT +json-stable-stringify-without-jsonify,1.0.1,MIT json-stringify-safe,5.0.1,ISC json3,3.3.2,MIT json5,0.5.1,MIT @@ -959,62 +899,44 @@ kind-of,3.2.2,MIT kind-of,4.0.0,MIT kind-of,5.1.0,MIT kind-of,6.0.2,MIT -kubeclient,3.0.0,MIT +kubeclient,3.1.0,MIT latest-version,3.1.0,MIT lazy-cache,1.0.4,MIT lazy-cache,2.0.2,MIT lcid,1.0.0,MIT +leb,0.3.0,Apache 2.0 levn,0.3.0,MIT libbase64,0.1.0,MIT libmime,3.0.0,MIT libqp,1.1.0,MIT licensee,8.9.2,MIT lie,3.1.1,MIT -listr,0.13.0,MIT -listr-silent-renderer,1.1.1,MIT -listr-update-renderer,0.4.0,MIT -listr-verbose-renderer,0.4.1,MIT little-plugger,1.1.4,MIT load-json-file,1.1.0,MIT -load-json-file,4.0.0,MIT +load-json-file,2.0.0,MIT loader-runner,2.3.0,MIT loader-utils,1.1.0,MIT locale,2.1.2,"ruby,LGPLv3+" locate-path,2.0.0,MIT lodash,4.17.10,MIT lodash,4.17.4,MIT -lodash,4.17.5,MIT -lodash._baseget,3.7.2,MIT -lodash._topath,3.8.1,MIT -lodash.camelcase,4.1.1,MIT lodash.camelcase,4.3.0,MIT -lodash.capitalize,4.2.1,MIT lodash.clonedeep,4.5.0,MIT -lodash.cond,4.5.2,MIT -lodash.deburr,4.1.0,MIT -lodash.endswith,4.2.1,MIT lodash.escaperegexp,4.1.2,MIT -lodash.get,3.7.0,MIT -lodash.isarray,3.0.4,MIT -lodash.isfunction,3.0.9,MIT -lodash.isstring,4.0.1,MIT -lodash.kebabcase,4.0.1,MIT +lodash.kebabcase,4.1.1,MIT lodash.memoize,4.1.2,MIT lodash.mergewith,4.6.0,MIT -lodash.snakecase,4.0.1,MIT -lodash.startswith,4.2.1,MIT +lodash.snakecase,4.1.1,MIT lodash.uniq,4.5.0,MIT -lodash.words,4.2.0,MIT -log-symbols,1.0.2,MIT -log-symbols,2.1.0,MIT +lodash.upperfirst,4.3.1,MIT log-symbols,2.2.0,MIT -log-update,1.0.2,MIT log4js,2.5.3,Apache 2.0 logging,2.2.2,MIT loggly,1.1.1,MIT loglevel,1.4.1,MIT loglevelnext,1.0.3,MIT lograge,0.10.0,MIT +long,3.2.0,Apache 2.0 longest,1.0.1,MIT loofah,2.2.2,MIT loose-envify,1.3.1,MIT @@ -1022,17 +944,17 @@ loud-rejection,1.6.0,MIT lowercase-keys,1.0.0,MIT lru-cache,2.2.4,MIT lru-cache,2.6.5,ISC -lru-cache,4.1.1,ISC +lru-cache,4.1.3,ISC macaddress,0.2.8,MIT mail,2.7.0,MIT mail_room,0.9.1,MIT mailcomposer,4.0.1,MIT mailgun-js,0.7.15,MIT -make-dir,1.0.0,MIT make-dir,1.2.0,MIT +mamacro,0.0.3,MIT map-cache,0.2.2,MIT map-obj,1.0.1,MIT -map-stream,0.1.0,UNKNOWN +map-stream,0.1.0,Unknown map-visit,1.0.0,MIT marked,0.3.12,MIT match-at,0.1.1,MIT @@ -1040,24 +962,19 @@ math-expression-evaluator,1.2.16,MIT md5.js,1.3.4,MIT media-typer,0.3.0,MIT mem,1.1.0,MIT -mem-fs,1.1.3,MIT -mem-fs-editor,4.0.1,MIT memoist,0.16.0,MIT memory-fs,0.2.0,MIT memory-fs,0.4.1,MIT meow,3.7.0,MIT merge-descriptors,1.0.1,MIT -merge2,1.2.2,MIT +merge-source-map,1.1.0,MIT method_source,0.8.2,MIT methods,1.1.2,MIT micromatch,2.3.11,MIT micromatch,3.1.10,MIT -micromatch,3.1.6,MIT -micromatch,3.1.9,MIT miller-rabin,4.0.1,MIT mime,1.4.1,MIT mime,1.6.0,MIT -mime,2.2.0,MIT mime,2.3.1,MIT mime-db,1.33.0,MIT mime-types,2.1.18,MIT @@ -1066,6 +983,7 @@ mime-types-data,3.2016.0521,MIT mimemagic,0.3.0,MIT mimic-fn,1.1.0,MIT mimic-response,1.0.0,MIT +mini_magick,4.8.0,MIT mini_mime,1.0.0,MIT mini_portile2,2.3.0,MIT minimalistic-assert,1.0.0,ISC @@ -1073,13 +991,13 @@ minimalistic-crypto-utils,1.0.1,MIT minimatch,3.0.4,ISC minimist,0.0.10,MIT minimist,0.0.8,MIT -minimist,0.1.0,MIT minimist,1.2.0,MIT mississippi,2.0.0,Simplified BSD mixin-deep,1.3.1,MIT mkdirp,0.5.1,MIT moment,2.19.2,MIT -monaco-editor,0.10.0,MIT +monaco-editor,0.13.1,MIT +monaco-editor-webpack-plugin,1.2.1,MIT mousetrap,1.4.6,Apache 2.0 mousetrap-rails,1.4.6,"MIT,Apache" move-concurrently,1.0.1,ISC @@ -1089,11 +1007,9 @@ multi_json,1.13.1,MIT multi_xml,0.6.0,MIT multicast-dns,6.1.1,MIT multicast-dns-service-types,1.1.0,MIT -multimatch,2.1.0,MIT multipart-post,2.0.0,MIT mustermann,1.0.2,MIT mustermann-grape,1.0.0,MIT -mute-stream,0.0.5,ISC mute-stream,0.0.7,ISC mysql2,0.4.10,MIT nan,2.8.0,MIT @@ -1102,14 +1018,12 @@ natural-compare,1.4.0,MIT negotiator,0.6.1,MIT neo-async,2.5.0,MIT net-ldap,0.16.0,MIT -net-ssh,4.2.0,MIT +net-ssh,5.0.1,MIT netmask,1.0.6,MIT netrc,0.11.0,MIT nice-try,1.0.4,MIT -node-dir,0.1.8,MIT node-forge,0.6.33,New BSD -node-libs-browser,1.1.1,MIT -node-libs-browser,2.0.0,MIT +node-libs-browser,2.1.0,MIT node-pre-gyp,0.6.39,New BSD node-uuid,1.4.8,MIT nodemailer,2.7.2,MIT @@ -1121,7 +1035,6 @@ nodemailer-smtp-transport,2.7.2,MIT nodemailer-wellknown,0.1.10,MIT nodemon,1.17.3,MIT nokogiri,1.8.2,MIT -nomnom,1.8.1,MIT nopt,1.0.10,MIT nopt,3.0.6,ISC nopt,4.0.1,ISC @@ -1147,15 +1060,15 @@ object-visit,1.0.1,MIT object.omit,2.0.1,MIT object.pick,1.3.0,MIT obuf,1.1.1,MIT -octokit,4.8.0,MIT +octokit,4.9.0,MIT omniauth,1.8.1,MIT omniauth-auth0,2.0.0,MIT -omniauth-authentiq,0.3.1,MIT +omniauth-authentiq,0.3.3,MIT omniauth-azure-oauth2,0.0.9,MIT omniauth-cas3,1.1.4,MIT omniauth-facebook,4.0.0,MIT omniauth-github,1.3.0,MIT -omniauth-gitlab,1.0.2,MIT +omniauth-gitlab,1.0.3,MIT omniauth-google-oauth2,0.5.3,MIT omniauth-kerberos,0.3.0,MIT omniauth-multipassword,0.4.2,MIT @@ -1169,46 +1082,36 @@ omniauth_crowd,2.2.3,MIT on-finished,2.3.0,MIT on-headers,1.0.1,MIT once,1.4.0,ISC -onetime,1.1.0,MIT onetime,2.0.1,MIT opener,1.4.3,(WTFPL OR MIT) opn,5.2.0,MIT optimist,0.6.1,MIT optionator,0.8.2,MIT -ora,0.2.3,MIT org-ruby,0.9.12,MIT original,1.0.0,MIT orm_adapter,0.5.0,MIT os,0.9.6,MIT -os-browserify,0.2.1,MIT +os-browserify,0.3.0,MIT os-homedir,1.0.2,MIT os-locale,2.1.0,MIT os-tmpdir,1.0.2,MIT osenv,0.1.5,ISC -p-cancelable,0.3.0,MIT p-cancelable,0.4.1,MIT -p-each-series,1.0.0,MIT p-finally,1.0.0,MIT p-is-promise,1.1.0,MIT -p-lazy,1.0.0,MIT p-limit,1.2.0,MIT p-locate,2.0.0,MIT p-map,1.1.1,MIT -p-reduce,1.0.0,MIT -p-timeout,1.2.1,MIT p-timeout,2.0.1,MIT p-try,1.0.0,MIT pac-proxy-agent,1.1.0,MIT pac-resolver,2.0.0,MIT package-json,4.0.1,MIT -pako,0.2.9,MIT pako,1.0.6,(MIT AND Zlib) parallel-transform,1.1.0,MIT parse-asn1,5.1.0,ISC parse-glob,3.0.4,MIT parse-json,2.2.0,MIT -parse-json,4.0.0,MIT -parse-passwd,1.0.0,MIT parseqs,0.0.5,MIT parseuri,0.0.5,MIT parseurl,1.3.2,MIT @@ -1224,7 +1127,7 @@ path-parse,1.0.5,MIT path-proxy,1.0.0,MIT path-to-regexp,0.1.7,MIT path-type,1.1.0,MIT -path-type,3.0.0,MIT +path-type,2.0.0,MIT pause-stream,0.0.11,Apache 2.0 pbkdf2,3.0.14,MIT peek,1.0.1,MIT @@ -1244,15 +1147,14 @@ pinkie,2.0.4,MIT pinkie-promise,2.0.1,MIT pkg-dir,1.0.0,MIT pkg-dir,2.0.0,MIT -pkg-up,1.0.0,MIT -pluralize,1.2.1,MIT +pluralize,7.0.0,MIT po_to_json,1.0.1,MIT +popper.js,1.14.3,MIT portfinder,1.0.13,MIT posix-character-classes,0.1.1,MIT posix-spawn,0.3.13,MIT postcss,5.2.16,MIT -postcss,6.0.19,MIT -postcss,6.0.21,MIT +postcss,6.0.22,MIT postcss-calc,5.3.1,MIT postcss-colormin,2.2.2,MIT postcss-convert-values,2.6.1,MIT @@ -1262,9 +1164,6 @@ postcss-discard-empty,2.1.0,MIT postcss-discard-overridden,0.1.1,MIT postcss-discard-unused,2.2.3,MIT postcss-filter-plugins,2.0.2,MIT -postcss-load-config,1.2.0,MIT -postcss-load-options,1.2.0,MIT -postcss-load-plugins,2.3.0,MIT postcss-merge-idents,2.1.7,MIT postcss-merge-longhand,2.0.2,MIT postcss-merge-rules,2.1.2,MIT @@ -1284,6 +1183,7 @@ postcss-reduce-idents,2.4.0,MIT postcss-reduce-initial,1.0.1,MIT postcss-reduce-transforms,1.0.4,MIT postcss-selector-parser,2.2.3,MIT +postcss-selector-parser,3.1.1,MIT postcss-svgo,2.1.6,MIT postcss-unique-selectors,2.0.2,MIT postcss-value-parser,3.3.0,MIT @@ -1294,17 +1194,15 @@ premailer-rails,1.9.7,MIT prepend-http,1.0.4,MIT prepend-http,2.0.0,MIT preserve,0.2.0,MIT -prettier,1.10.2,MIT prettier,1.11.1,MIT -prettier,1.8.2,MIT -pretty-bytes,4.0.2,MIT +prettier,1.12.1,MIT prismjs,1.6.0,MIT private,0.1.8,MIT process,0.11.10,MIT process-nextick-args,1.0.7,MIT process-nextick-args,2.0.0,MIT -progress,1.1.8,MIT -prometheus-client-mmap,0.9.1,Apache 2.0 +progress,2.0.0,MIT +prometheus-client-mmap,0.9.3,Apache 2.0 promise-inflight,1.0.1,ISC proxy-addr,2.0.3,MIT proxy-agent,2.0.0,MIT @@ -1359,26 +1257,20 @@ raw-body,2.3.2,MIT raw-loader,0.5.1,MIT rb-fsevent,0.10.2,MIT rb-inotify,0.9.10,MIT -rbnacl,4.0.2,MIT -rbnacl-libsodium,1.0.11,MIT rc,1.2.5,(BSD-2-Clause OR MIT OR Apache-2.0) -rdoc,4.2.2,ruby +rdoc,6.0.4,ruby re2,1.1.1,New BSD -read-chunk,2.1.0,MIT read-pkg,1.1.0,MIT -read-pkg,3.0.0,MIT +read-pkg,2.0.0,MIT read-pkg-up,1.0.1,MIT -read-pkg-up,3.0.0,MIT +read-pkg-up,2.0.0,MIT readable-stream,1.1.14,MIT readable-stream,2.0.6,MIT readable-stream,2.3.4,MIT +readable-stream,2.3.6,MIT readdirp,2.1.0,MIT -readline2,1.0.1,MIT recaptcha,3.0.0,MIT -recast,0.12.9,MIT -recast,0.14.7,MIT -rechoir,0.6.2,MIT -recursive-open-struct,1.0.5,MIT +recursive-open-struct,1.1.0,MIT redcarpet,3.4.0,MIT redent,1.0.0,MIT redis,2.8.0,MIT @@ -1386,7 +1278,7 @@ redis,3.3.5,MIT redis-actionpack,5.0.2,MIT redis-activesupport,5.0.4,MIT redis-commands,1.3.1,MIT -redis-namespace,1.5.2,MIT +redis-namespace,1.6.0,MIT redis-parser,2.6.0,MIT redis-rack,2.0.4,MIT redis-rails,5.0.2,MIT @@ -1409,8 +1301,6 @@ repeat-element,1.1.2,MIT repeat-string,0.2.2,MIT repeat-string,1.6.1,MIT repeating,2.0.1,MIT -replace-ext,0.0.1,MIT -replace-ext,1.0.0,MIT representable,3.0.4,MIT request,2.75.0,Apache 2.0 request,2.81.0,Apache 2.0 @@ -1419,28 +1309,22 @@ request_store,1.3.1,MIT requestretry,1.13.0,MIT require-all,2.2.0,MIT require-directory,2.1.1,MIT -require-from-string,1.2.1,MIT require-main-filename,1.0.1,ISC require-uncached,1.0.3,MIT requires-port,1.0.0,MIT resolve,1.1.7,MIT -resolve,1.5.0,MIT resolve,1.7.1,MIT resolve-cwd,2.0.0,MIT -resolve-dir,1.0.1,MIT resolve-from,1.0.1,MIT resolve-from,3.0.0,MIT resolve-url,0.2.1,MIT responders,2.4.0,MIT responselike,1.0.2,MIT rest-client,2.0.2,MIT -restore-cursor,1.0.1,MIT restore-cursor,2.0.0,MIT ret,0.1.15,MIT retriable,3.1.1,MIT right-align,0.1.3,MIT -rimraf,2.2.8,MIT -rimraf,2.6.1,ISC rimraf,2.6.2,ISC rinku,2.0.0,ISC ripemd160,2.0.1,MIT @@ -1451,16 +1335,15 @@ rqrcode-rails3,0.1.7,MIT ruby-enum,0.7.2,MIT ruby-fogbugz,0.2.1,MIT ruby-prof,0.17.0,Simplified BSD +ruby-progressbar,1.9.0,MIT ruby-saml,1.7.2,MIT ruby_parser,3.9.0,MIT rubyntlm,0.6.2,MIT rubypants,0.2.0,BSD rufus-scheduler,3.4.0,MIT -rugged,0.27.0,MIT -run-async,0.1.0,MIT +rugged,0.27.1,MIT run-async,2.3.0,MIT run-queue,1.0.3,ISC -rx-lite,3.1.2,Apache 2.0 rx-lite,4.0.8,Apache 2.0 rx-lite-aggregates,4.0.8,Apache 2.0 rxjs,5.5.10,Apache 2.0 @@ -1475,7 +1358,6 @@ sass-rails,5.0.6,MIT sawyer,0.8.1,MIT sax,1.2.2,ISC schema-utils,0.4.5,MIT -scoped-regex,1.0.0,MIT securecompare,1.0.0,MIT seed-fu,2.3.7,MIT select,1.1.2,MIT @@ -1484,7 +1366,6 @@ select2,3.5.2-browserify,Apache* select2-rails,3.5.9.3,MIT selfsigned,1.10.1,MIT semver,5.0.3,ISC -semver,5.3.0,ISC semver,5.5.0,ISC semver-diff,2.1.0,MIT send,0.16.1,MIT @@ -1506,9 +1387,7 @@ sha.js,2.4.10,MIT sha1,1.1.1,New BSD shebang-command,1.2.0,MIT shebang-regex,1.0.0,MIT -shelljs,0.7.8,New BSD -shelljs,0.8.1,New BSD -sidekiq,5.0.5,LGPL +sidekiq,5.1.3,LGPL sidekiq-cron,0.6.0,MIT sidekiq-limit_fetch,3.4.0,MIT signal-exit,3.0.2,ISC @@ -1516,8 +1395,7 @@ signet,0.8.1,Apache 2.0 slack-node,0.2.0,MIT slack-notifier,1.5.1,MIT slash,1.0.0,MIT -slice-ansi,0.0.4,MIT -slide,1.1.6,ISC +slice-ansi,1.0.0,MIT smart-buffer,1.1.15,MIT smtp-connection,2.12.0,MIT snapdragon,0.8.1,MIT @@ -1536,6 +1414,7 @@ socks,1.1.9,MIT socks-proxy-agent,2.1.1,MIT sort-keys,1.1.2,MIT sort-keys,2.0.0,MIT +sortablejs,1.7.0,MIT source-list-map,2.0.0,MIT source-map,0.2.0,New BSD source-map,0.4.4,New BSD @@ -1567,53 +1446,46 @@ state_machines-activerecord,0.5.1,MIT static-extend,0.1.2,MIT statuses,1.3.1,MIT statuses,1.4.0,MIT +stickyfilljs,2.0.5,MIT stream-browserify,2.0.1,MIT stream-combiner,0.0.4,MIT stream-each,1.2.2,MIT -stream-http,2.8.0,MIT +stream-http,2.8.2,MIT stream-shift,1.0.0,MIT -stream-to-observable,0.2.0,MIT streamroller,0.7.0,MIT strict-uri-encode,1.1.0,MIT -string-template,0.2.1,MIT string-width,1.0.2,MIT string-width,2.1.1,MIT string_decoder,0.10.31,MIT string_decoder,1.0.3,MIT +string_decoder,1.1.1,MIT stringex,2.8.4,MIT stringstream,0.0.5,MIT -strip-ansi,0.1.1,MIT strip-ansi,3.0.1,MIT strip-ansi,4.0.0,MIT strip-bom,2.0.0,MIT strip-bom,3.0.0,MIT -strip-bom-stream,2.0.0,MIT strip-eof,1.0.0,MIT strip-indent,1.0.1,MIT strip-json-comments,2.0.1,MIT style-loader,0.21.0,MIT supports-color,2.0.0,MIT supports-color,3.2.3,MIT -supports-color,5.1.0,MIT -supports-color,5.2.0,MIT supports-color,5.4.0,MIT svg4everybody,2.1.9,CC0-1.0 svgo,0.7.2,MIT -symbol-observable,0.2.4,MIT symbol-observable,1.0.1,MIT sys-filesystem,1.1.6,Artistic 2.0 -table,3.8.3,New BSD +table,4.0.2,New BSD tapable,0.1.10,MIT tapable,1.0.0,MIT tar,2.2.1,ISC tar-pack,3.4.1,Simplified BSD -temp,0.8.3,MIT temple,0.7.7,MIT term-size,1.2.0,MIT test-exclude,4.2.1,ISC text,1.3.1,MIT text-table,0.2.0,MIT -textextensions,2.2.0,MIT thor,0.19.4,MIT thread_safe,0.3.6,Apache 2.0 three,0.84.0,MIT @@ -1626,8 +1498,7 @@ thunky,0.1.0,MIT* tilt,2.0.6,MIT timeago.js,3.0.2,MIT timed-out,4.0.1,MIT -timers-browserify,1.4.2,MIT -timers-browserify,2.0.4,MIT +timers-browserify,2.0.10,MIT timespan,2.3.0,MIT timfel-krb5-auth,0.8.3,LGPL tiny-emitter,2.0.2,MIT @@ -1637,7 +1508,6 @@ to-arraybuffer,1.0.1,MIT to-fast-properties,1.0.3,MIT to-fast-properties,2.0.0,MIT to-object-path,0.3.0,MIT -to-regex,3.0.1,MIT to-regex,3.0.2,MIT to-regex-range,2.1.1,MIT toml-rb,1.0.0,MIT @@ -1667,9 +1537,7 @@ uglify-to-browserify,1.0.2,MIT uglifyjs-webpack-plugin,1.2.5,MIT uid-number,0.0.6,ISC ultron,1.1.1,MIT -unc-path-regex,0.1.2,MIT undefsafe,2.0.2,MIT -underscore,1.6.0,MIT underscore,1.7.0,MIT underscore,1.9.0,MIT unf,0.1.4,BSD @@ -1685,9 +1553,8 @@ unique-slug,2.0.0,ISC unique-string,1.0.0,MIT unpipe,1.0.0,MIT unset-value,1.0.0,MIT -untildify,3.0.2,MIT unzip-response,2.0.1,MIT -upath,1.0.2,MIT +upath,1.0.5,MIT update-notifier,2.3.0,Simplified BSD urix,0.1.0,MIT url,0.11.0,MIT @@ -1701,14 +1568,13 @@ url-parse-lax,3.0.0,MIT url-to-options,1.0.1,MIT url_safe_base64,0.2.2,MIT use,2.0.2,MIT -user-home,2.0.0,MIT useragent,2.2.1,MIT util,0.10.3,MIT util-deprecate,1.0.2,MIT utils-merge,1.0.1,MIT uuid,3.2.1,MIT uws,9.14.0,Zlib -v8-compile-cache,1.1.2,MIT +v8-compile-cache,2.0.0,MIT validate-npm-package-license,3.0.1,Apache 2.0 validates_hostname,1.0.6,MIT vary,1.1.1,MIT @@ -1716,18 +1582,15 @@ vary,1.1.2,MIT vendors,1.0.1,MIT verror,1.10.0,MIT version_sorter,2.1.0,MIT -vinyl,1.2.0,MIT -vinyl,2.1.0,MIT -vinyl-file,2.0.0,MIT virtus,1.0.5,MIT visibilityjs,1.2.4,MIT vm-browserify,0.0.4,MIT vmstat,2.3.0,MIT void-elements,2.0.1,MIT vue,2.5.16,MIT -vue-eslint-parser,2.0.1,MIT +vue-eslint-parser,2.0.3,MIT vue-hot-reload-api,2.3.0,MIT -vue-loader,14.2.2,MIT +vue-loader,15.2.0,MIT vue-resource,1.5.0,MIT vue-router,3.0.1,MIT vue-style-loader,4.1.0,MIT @@ -1738,17 +1601,14 @@ vuex,3.0.1,MIT warden,1.2.7,MIT watchpack,1.5.0,MIT wbuf,1.7.2,MIT -webpack,4.7.0,MIT -webpack-addons,1.1.5,MIT +webpack,4.11.1,MIT webpack-bundle-analyzer,2.11.1,MIT -webpack-cli,2.1.2,MIT +webpack-cli,3.0.2,MIT webpack-dev-middleware,2.0.6,MIT webpack-dev-middleware,3.1.3,MIT webpack-dev-server,3.1.4,MIT -webpack-log,1.1.2,MIT webpack-log,1.2.0,MIT webpack-rails,0.9.10,MIT -webpack-sources,1.0.1,MIT webpack-sources,1.1.0,MIT webpack-stats-plugin,0.2.1,MIT websocket-driver,0.6.5,MIT @@ -1765,11 +1625,10 @@ wordwrap,0.0.2,MIT wordwrap,0.0.3,MIT wordwrap,1.0.0,MIT worker-farm,1.5.2,MIT -worker-loader,1.1.1,MIT +worker-loader,2.0.0,MIT wrap-ansi,2.1.0,MIT wrappy,1.0.2,ISC write,0.2.1,MIT -write-file-atomic,1.3.4,ISC write-file-atomic,2.3.0,ISC ws,3.3.3,MIT ws,4.0.0,MIT @@ -1781,12 +1640,8 @@ xtend,4.0.1,MIT y18n,3.2.1,ISC y18n,4.0.0,ISC yallist,2.1.2,ISC -yargs,1.2.6,MIT yargs,11.0.0,MIT yargs,11.1.0,MIT yargs,3.10.0,MIT yargs-parser,9.0.2,ISC yeast,0.1.2,MIT -yeoman-environment,2.0.5,Simplified BSD -yeoman-environment,2.0.6,Simplified BSD -yeoman-generator,2.0.5,Simplified BSD diff --git a/yarn.lock b/yarn.lock index 418fa4a2216..cefd7c9a62e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -82,13 +82,6 @@ version "1.23.0" resolved "https://registry.yarnpkg.com/@gitlab-org/gitlab-svgs/-/gitlab-svgs-1.23.0.tgz#42047aeedcc06bc12d417ed1efadad1749af9670" -"@mrmlnc/readdir-enhanced@^2.2.1": - version "2.2.1" - resolved "https://registry.yarnpkg.com/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz#524af240d1a360527b730475ecfa1344aa540dde" - dependencies: - call-me-maybe "^1.0.1" - glob-to-regexp "^0.3.0" - "@sindresorhus/is@^0.7.0": version "0.7.0" resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.7.0.tgz#9a06f4f137ee84d7df0460c1fdb1135ffa6c50fd" @@ -111,6 +104,141 @@ source-map "^0.5.6" vue-template-es2015-compiler "^1.6.0" +"@webassemblyjs/ast@1.5.10": + version "1.5.10" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.5.10.tgz#7f1e81149ca4e103c9e7cc321ea0dcb83a392512" + dependencies: + "@webassemblyjs/helper-module-context" "1.5.10" + "@webassemblyjs/helper-wasm-bytecode" "1.5.10" + "@webassemblyjs/wast-parser" "1.5.10" + debug "^3.1.0" + mamacro "^0.0.3" + +"@webassemblyjs/floating-point-hex-parser@1.5.10": + version "1.5.10" + resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.5.10.tgz#ae48705fd58927df62023f114520b8215330ff86" + +"@webassemblyjs/helper-api-error@1.5.10": + version "1.5.10" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.5.10.tgz#0baf9453ce2fd8db58f0fdb4fb2852557c71d5a7" + +"@webassemblyjs/helper-buffer@1.5.10": + version "1.5.10" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.5.10.tgz#abee4284161e9cd6ba7619785ca277bfcb8052ce" + dependencies: + debug "^3.1.0" + +"@webassemblyjs/helper-code-frame@1.5.10": + version "1.5.10" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-code-frame/-/helper-code-frame-1.5.10.tgz#4e23c05431665f16322104580af7c06253d4b4e0" + dependencies: + "@webassemblyjs/wast-printer" "1.5.10" + +"@webassemblyjs/helper-fsm@1.5.10": + version "1.5.10" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-fsm/-/helper-fsm-1.5.10.tgz#490bab613ea255a9272b764826d3cc9d15170676" + +"@webassemblyjs/helper-module-context@1.5.10": + version "1.5.10" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-module-context/-/helper-module-context-1.5.10.tgz#6fca93585228bf33e6da076d0a1373db1fdd6580" + dependencies: + mamacro "^0.0.3" + +"@webassemblyjs/helper-wasm-bytecode@1.5.10": + version "1.5.10" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.5.10.tgz#90f6da93c7a186bfb2f587de442982ff533c4b44" + +"@webassemblyjs/helper-wasm-section@1.5.10": + version "1.5.10" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.5.10.tgz#d64292a19f7f357c49719461065efdf7ec975d66" + dependencies: + "@webassemblyjs/ast" "1.5.10" + "@webassemblyjs/helper-buffer" "1.5.10" + "@webassemblyjs/helper-wasm-bytecode" "1.5.10" + "@webassemblyjs/wasm-gen" "1.5.10" + debug "^3.1.0" + +"@webassemblyjs/ieee754@1.5.10": + version "1.5.10" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.5.10.tgz#257cad440dd6c8a339402d31e035ba2e38e9c245" + dependencies: + ieee754 "^1.1.11" + +"@webassemblyjs/leb128@1.5.10": + version "1.5.10" + resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.5.10.tgz#a8e4fe5f4b16daadb241fcc44d9735e9f27b05a3" + dependencies: + leb "^0.3.0" + +"@webassemblyjs/utf8@1.5.10": + version "1.5.10" + resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.5.10.tgz#0b3b6bc86b7619c5dc7b2789db6665aa35689983" + +"@webassemblyjs/wasm-edit@1.5.10": + version "1.5.10" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.5.10.tgz#0fe80f19e57f669eab1caa8c1faf9690b259d5b9" + dependencies: + "@webassemblyjs/ast" "1.5.10" + "@webassemblyjs/helper-buffer" "1.5.10" + "@webassemblyjs/helper-wasm-bytecode" "1.5.10" + "@webassemblyjs/helper-wasm-section" "1.5.10" + "@webassemblyjs/wasm-gen" "1.5.10" + "@webassemblyjs/wasm-opt" "1.5.10" + "@webassemblyjs/wasm-parser" "1.5.10" + "@webassemblyjs/wast-printer" "1.5.10" + debug "^3.1.0" + +"@webassemblyjs/wasm-gen@1.5.10": + version "1.5.10" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.5.10.tgz#8b29ddd3651259408ae5d5c816a011fb3f3f3584" + dependencies: + "@webassemblyjs/ast" "1.5.10" + "@webassemblyjs/helper-wasm-bytecode" "1.5.10" + "@webassemblyjs/ieee754" "1.5.10" + "@webassemblyjs/leb128" "1.5.10" + "@webassemblyjs/utf8" "1.5.10" + +"@webassemblyjs/wasm-opt@1.5.10": + version "1.5.10" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.5.10.tgz#569e45ab1b2bf0a7706cdf6d1b51d1188e9e4c7b" + dependencies: + "@webassemblyjs/ast" "1.5.10" + "@webassemblyjs/helper-buffer" "1.5.10" + "@webassemblyjs/wasm-gen" "1.5.10" + "@webassemblyjs/wasm-parser" "1.5.10" + debug "^3.1.0" + +"@webassemblyjs/wasm-parser@1.5.10": + version "1.5.10" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.5.10.tgz#3e1017e49f833f46b840db7cf9d194d4f00037ff" + dependencies: + "@webassemblyjs/ast" "1.5.10" + "@webassemblyjs/helper-api-error" "1.5.10" + "@webassemblyjs/helper-wasm-bytecode" "1.5.10" + "@webassemblyjs/ieee754" "1.5.10" + "@webassemblyjs/leb128" "1.5.10" + "@webassemblyjs/wasm-parser" "1.5.10" + +"@webassemblyjs/wast-parser@1.5.10": + version "1.5.10" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-parser/-/wast-parser-1.5.10.tgz#1a3235926483c985a00ee8ebca856ffda9544934" + dependencies: + "@webassemblyjs/ast" "1.5.10" + "@webassemblyjs/floating-point-hex-parser" "1.5.10" + "@webassemblyjs/helper-api-error" "1.5.10" + "@webassemblyjs/helper-code-frame" "1.5.10" + "@webassemblyjs/helper-fsm" "1.5.10" + long "^3.2.0" + mamacro "^0.0.3" + +"@webassemblyjs/wast-printer@1.5.10": + version "1.5.10" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.5.10.tgz#adb38831ba45efd0a5c7971b666e179b64f68bba" + dependencies: + "@webassemblyjs/ast" "1.5.10" + "@webassemblyjs/wast-parser" "1.5.10" + long "^3.2.0" + abbrev@1: version "1.1.1" resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" @@ -143,8 +271,8 @@ acorn@^3.0.4: resolved "https://registry.yarnpkg.com/acorn/-/acorn-3.3.0.tgz#45e37fb39e8da3f25baee3ff5369e2bb5f22017a" acorn@^5.0.0, acorn@^5.3.0, acorn@^5.5.0: - version "5.5.3" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.5.3.tgz#f473dd47e0277a08e28e9bec5aeeb04751f0b8c9" + version "5.6.2" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.6.2.tgz#b1da1d7be2ac1b4a327fb9eab851702c5045b4e7" addressparser@1.0.1: version "1.0.1" @@ -225,10 +353,6 @@ ansi-align@^2.0.0: dependencies: string-width "^2.0.0" -ansi-escapes@^1.0.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-1.4.0.tgz#d3a8a83b319aa67793662b13e761c7911422306e" - ansi-escapes@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.0.0.tgz#ec3e8b4e9f8064fc02c3ac9b65f1c275bda8ef92" @@ -255,14 +379,6 @@ ansi-styles@^3.2.1: dependencies: color-convert "^1.9.0" -ansi-styles@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-1.0.0.tgz#cb102df1c56f5123eab8b67cd7b98027a0279178" - -any-observable@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/any-observable/-/any-observable-0.2.0.tgz#c67870058003579009083f54ac0abafb5c33d242" - anymatch@^1.3.0: version "1.3.2" resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-1.3.2.tgz#553dcb8f91e3c889845dfdba34c77721b90b9d7a" @@ -318,10 +434,6 @@ arr-union@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4" -array-differ@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/array-differ/-/array-differ-1.0.0.tgz#eff52e3758249d33be402b8bb8e564bb2b5d4031" - array-find-index@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/array-find-index/-/array-find-index-1.0.2.tgz#df010aa1287e164bbda6f9723b0a96a1ec4187a1" @@ -405,11 +517,7 @@ assign-symbols@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367" -ast-types@0.10.1: - version "0.10.1" - resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.10.1.tgz#f52fca9715579a14f841d67d7f8d25432ab6a3dd" - -ast-types@0.11.3, ast-types@0.x.x: +ast-types@0.x.x: version "0.11.3" resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.11.3.tgz#c20757fe72ee71278ea0ff3d87e5c2ca30d9edf8" @@ -421,11 +529,11 @@ async-limiter@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.0.tgz#78faed8c3d074ab81f22b4e985d79e8738f720f8" -async@1.x, async@^1.4.0, async@^1.5.0, async@^1.5.2: +async@1.x, async@^1.4.0, async@^1.5.2: version "1.5.2" resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a" -async@^2.0.0, async@^2.1.4, async@^2.6.0: +async@^2.0.0, async@^2.1.4: version "2.6.0" resolved "https://registry.yarnpkg.com/async/-/async-2.6.0.tgz#61a29abb6fcc026fea77e56d1c6ec53a795951f4" dependencies: @@ -706,10 +814,6 @@ babel-plugin-syntax-async-generators@^6.5.0: version "6.13.0" resolved "https://registry.yarnpkg.com/babel-plugin-syntax-async-generators/-/babel-plugin-syntax-async-generators-6.13.0.tgz#6bc963ebb16eccbae6b92b596eb7f35c342a8b9a" -babel-plugin-syntax-class-constructor-call@^6.18.0: - version "6.18.0" - resolved "https://registry.yarnpkg.com/babel-plugin-syntax-class-constructor-call/-/babel-plugin-syntax-class-constructor-call-6.18.0.tgz#9cb9d39fe43c8600bec8146456ddcbd4e1a76416" - babel-plugin-syntax-class-properties@^6.8.0: version "6.13.0" resolved "https://registry.yarnpkg.com/babel-plugin-syntax-class-properties/-/babel-plugin-syntax-class-properties-6.13.0.tgz#d7eb23b79a317f8543962c505b827c7d6cac27de" @@ -726,14 +830,6 @@ babel-plugin-syntax-exponentiation-operator@^6.8.0: version "6.13.0" resolved "https://registry.yarnpkg.com/babel-plugin-syntax-exponentiation-operator/-/babel-plugin-syntax-exponentiation-operator-6.13.0.tgz#9ee7e8337290da95288201a6a57f4170317830de" -babel-plugin-syntax-export-extensions@^6.8.0: - version "6.13.0" - resolved "https://registry.yarnpkg.com/babel-plugin-syntax-export-extensions/-/babel-plugin-syntax-export-extensions-6.13.0.tgz#70a1484f0f9089a4e84ad44bac353c95b9b12721" - -babel-plugin-syntax-flow@^6.18.0: - version "6.18.0" - resolved "https://registry.yarnpkg.com/babel-plugin-syntax-flow/-/babel-plugin-syntax-flow-6.18.0.tgz#4c3ab20a2af26aa20cd25995c398c4eb70310c8d" - babel-plugin-syntax-object-rest-spread@^6.13.0, babel-plugin-syntax-object-rest-spread@^6.8.0: version "6.13.0" resolved "https://registry.yarnpkg.com/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.13.0.tgz#fd6536f2bce13836ffa3a5458c4903a597bb3bf5" @@ -758,14 +854,6 @@ babel-plugin-transform-async-to-generator@^6.24.1: babel-plugin-syntax-async-functions "^6.8.0" babel-runtime "^6.22.0" -babel-plugin-transform-class-constructor-call@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-class-constructor-call/-/babel-plugin-transform-class-constructor-call-6.24.1.tgz#80dc285505ac067dcb8d6c65e2f6f11ab7765ef9" - dependencies: - babel-plugin-syntax-class-constructor-call "^6.18.0" - babel-runtime "^6.22.0" - babel-template "^6.24.1" - babel-plugin-transform-class-properties@^6.24.1: version "6.24.1" resolved "https://registry.yarnpkg.com/babel-plugin-transform-class-properties/-/babel-plugin-transform-class-properties-6.24.1.tgz#6a79763ea61d33d36f37b611aa9def81a81b46ac" @@ -968,20 +1056,6 @@ babel-plugin-transform-exponentiation-operator@^6.24.1: babel-plugin-syntax-exponentiation-operator "^6.8.0" babel-runtime "^6.22.0" -babel-plugin-transform-export-extensions@^6.22.0: - version "6.22.0" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-export-extensions/-/babel-plugin-transform-export-extensions-6.22.0.tgz#53738b47e75e8218589eea946cbbd39109bbe653" - dependencies: - babel-plugin-syntax-export-extensions "^6.8.0" - babel-runtime "^6.22.0" - -babel-plugin-transform-flow-strip-types@^6.8.0: - version "6.22.0" - resolved "https://registry.yarnpkg.com/babel-plugin-transform-flow-strip-types/-/babel-plugin-transform-flow-strip-types-6.22.0.tgz#84cb672935d43714fdc32bce84568d87441cf7cf" - dependencies: - babel-plugin-syntax-flow "^6.18.0" - babel-runtime "^6.22.0" - babel-plugin-transform-object-rest-spread@^6.22.0: version "6.23.0" resolved "https://registry.yarnpkg.com/babel-plugin-transform-object-rest-spread/-/babel-plugin-transform-object-rest-spread-6.23.0.tgz#875d6bc9be761c58a2ae3feee5dc4895d8c7f921" @@ -1002,7 +1076,7 @@ babel-plugin-transform-strict-mode@^6.24.1: babel-runtime "^6.22.0" babel-types "^6.24.1" -babel-preset-es2015@^6.24.1, babel-preset-es2015@^6.9.0: +babel-preset-es2015@^6.24.1: version "6.24.1" resolved "https://registry.yarnpkg.com/babel-preset-es2015/-/babel-preset-es2015-6.24.1.tgz#d44050d6bc2c9feea702aaf38d727a0210538939" dependencies: @@ -1052,14 +1126,6 @@ babel-preset-latest@^6.24.1: babel-preset-es2016 "^6.24.1" babel-preset-es2017 "^6.24.1" -babel-preset-stage-1@^6.5.0: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-preset-stage-1/-/babel-preset-stage-1-6.24.1.tgz#7692cd7dcd6849907e6ae4a0a85589cfb9e2bfb0" - dependencies: - babel-plugin-transform-class-constructor-call "^6.24.1" - babel-plugin-transform-export-extensions "^6.22.0" - babel-preset-stage-2 "^6.24.1" - babel-preset-stage-2@^6.24.1: version "6.24.1" resolved "https://registry.yarnpkg.com/babel-preset-stage-2/-/babel-preset-stage-2-6.24.1.tgz#d9e2960fb3d71187f0e64eec62bc07767219bdc1" @@ -1079,7 +1145,7 @@ babel-preset-stage-3@^6.24.1: babel-plugin-transform-exponentiation-operator "^6.24.1" babel-plugin-transform-object-rest-spread "^6.22.0" -babel-register@^6.26.0, babel-register@^6.9.0: +babel-register@^6.26.0: version "6.26.0" resolved "https://registry.yarnpkg.com/babel-register/-/babel-register-6.26.0.tgz#6ed021173e2fcb486d7acb45c6009a856f647071" dependencies: @@ -1131,11 +1197,11 @@ babel-types@^6.18.0, babel-types@^6.19.0, babel-types@^6.24.1, babel-types@^6.26 lodash "^4.17.4" to-fast-properties "^1.0.3" -babylon@7.0.0-beta.44, babylon@^7.0.0-beta.30: +babylon@7.0.0-beta.44: version "7.0.0-beta.44" resolved "https://registry.yarnpkg.com/babylon/-/babylon-7.0.0-beta.44.tgz#89159e15e6e30c5096e22d738d8c0af8a0e8ca1d" -babylon@^6.17.3, babylon@^6.18.0: +babylon@^6.18.0: version "6.18.0" resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.18.0.tgz#af2f3b88fa6f5c1e4c634d1a0f8eac4f55b395e3" @@ -1207,10 +1273,6 @@ binary-extensions@^1.0.0: version "1.11.0" resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.11.0.tgz#46aa1751fb6a2f93ee5e689bb1087d4b14c6c205" -binaryextensions@2: - version "2.1.1" - resolved "https://registry.yarnpkg.com/binaryextensions/-/binaryextensions-2.1.1.tgz#3209a51ca4a4ad541a3b8d3d6a6d5b83a2485935" - bitsyntax@~0.0.4: version "0.0.4" resolved "https://registry.yarnpkg.com/bitsyntax/-/bitsyntax-0.0.4.tgz#eb10cc6f82b8c490e3e85698f07e83d46e0cba82" @@ -1518,10 +1580,6 @@ cacheable-request@^2.1.1: normalize-url "2.0.1" responselike "1.0.2" -call-me-maybe@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/call-me-maybe/-/call-me-maybe-1.0.1.tgz#26d208ea89e37b5cbde60250a15f031c16a4d66b" - caller-path@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/caller-path/-/caller-path-0.1.0.tgz#94085ef63581ecd3daa92444a8fe94e82577751f" @@ -1587,7 +1645,7 @@ center-align@^0.1.1: align-text "^0.1.3" lazy-cache "^1.0.3" -chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3: +chalk@^1.1.1, chalk@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" dependencies: @@ -1597,7 +1655,7 @@ chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3: strip-ansi "^3.0.0" supports-color "^2.0.0" -chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.0, chalk@^2.3.2, chalk@^2.4.1: +chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.0, chalk@^2.4.1: version "2.4.1" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.1.tgz#18c49ab16a037b6eb0152cc83e3471338215b66e" dependencies: @@ -1605,14 +1663,6 @@ chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.0, chalk@^2.3.2, chalk@^2.4 escape-string-regexp "^1.0.5" supports-color "^5.3.0" -chalk@~0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-0.4.0.tgz#5199a3ddcd0c1efe23bc08c1b027b06176e0c64f" - dependencies: - ansi-styles "~1.0.0" - has-color "~0.1.0" - strip-ansi "~0.1.0" - chardet@^0.4.0: version "0.4.2" resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.4.2.tgz#b5473b33dc97c424e5d98dc87d55d4d8a29c8bf2" @@ -1708,35 +1758,12 @@ cli-boxes@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-1.0.0.tgz#4fa917c3e59c94a004cd61f8ee509da651687143" -cli-cursor@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-1.0.2.tgz#64da3f7d56a54412e59794bd62dc35295e8f2987" - dependencies: - restore-cursor "^1.0.1" - cli-cursor@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-2.1.0.tgz#b35dac376479facc3e94747d41d0d0f5238ffcb5" dependencies: restore-cursor "^2.0.0" -cli-spinners@^0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-0.1.2.tgz#bb764d88e185fb9e1e6a2a1f19772318f605e31c" - -cli-table@^0.3.1: - version "0.3.1" - resolved "https://registry.yarnpkg.com/cli-table/-/cli-table-0.3.1.tgz#f53b05266a8b1a0b934b3d0821e6e2dc5914ae23" - dependencies: - colors "1.0.3" - -cli-truncate@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/cli-truncate/-/cli-truncate-0.2.1.tgz#9f15cfbb0705005369216c626ac7d05ab90dd574" - dependencies: - slice-ansi "0.0.4" - string-width "^1.0.1" - cli-width@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.1.0.tgz#b234ca209b29ef66fc518d9b98d5847b00edf00a" @@ -1765,40 +1792,16 @@ cliui@^4.0.0: strip-ansi "^4.0.0" wrap-ansi "^2.0.0" -clone-buffer@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/clone-buffer/-/clone-buffer-1.0.0.tgz#e3e25b207ac4e701af721e2cb5a16792cac3dc58" - clone-response@1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/clone-response/-/clone-response-1.0.2.tgz#d1dc973920314df67fbeb94223b4ee350239e96b" dependencies: mimic-response "^1.0.0" -clone-stats@^0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/clone-stats/-/clone-stats-0.0.1.tgz#b88f94a82cf38b8791d58046ea4029ad88ca99d1" - -clone-stats@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/clone-stats/-/clone-stats-1.0.0.tgz#b3782dff8bb5474e18b9b6bf0fdfe782f8777680" - -clone@^1.0.0, clone@^1.0.2: +clone@^1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.3.tgz#298d7e2231660f40c003c2ed3140decf3f53085f" -clone@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.1.tgz#d217d1e961118e3ac9a4b8bba3285553bf647cdb" - -cloneable-readable@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/cloneable-readable/-/cloneable-readable-1.0.0.tgz#a6290d413f217a61232f95e458ff38418cfb0117" - dependencies: - inherits "^2.0.1" - process-nextick-args "^1.0.6" - through2 "^2.0.1" - co@^4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" @@ -1856,11 +1859,7 @@ colormin@^1.0.5: css-color-names "0.0.4" has "^1.0.1" -colors@1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/colors/-/colors-1.0.3.tgz#0433f44d809680fdeb60ed260f1b0c262e82a40b" - -colors@^1.1.0, colors@^1.1.2, colors@~1.1.2: +colors@^1.1.0, colors@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/colors/-/colors-1.1.2.tgz#168a4701756b6a7f51a12ce0c97bfa28c084ed63" @@ -2024,19 +2023,6 @@ copy-descriptor@^0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" -copy-webpack-plugin@^4.5.1: - version "4.5.1" - resolved "https://registry.yarnpkg.com/copy-webpack-plugin/-/copy-webpack-plugin-4.5.1.tgz#fc4f68f4add837cc5e13d111b20715793225d29c" - dependencies: - cacache "^10.0.4" - find-cache-dir "^1.0.0" - globby "^7.1.1" - is-glob "^4.0.0" - loader-utils "^1.1.0" - minimatch "^3.0.4" - p-limit "^1.0.0" - serialize-javascript "^1.4.0" - core-js@^2.2.0, core-js@^2.4.0, core-js@^2.4.1, core-js@^2.5.0: version "2.5.3" resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.3.tgz#8acc38345824f16d8365b7c9b4259168e8ed603e" @@ -2357,10 +2343,6 @@ dagre-layout@^0.8.0: graphlib "^2.1.1" lodash "^4.17.4" -dargs@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/dargs/-/dargs-5.1.0.tgz#ec7ea50c78564cd36c9d5ec18f66329fade27829" - dashdash@^1.12.0: version "1.14.1" resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" @@ -2371,10 +2353,6 @@ data-uri-to-buffer@1: version "1.2.0" resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-1.2.0.tgz#77163ea9c20d8641b4707e8f18abdf9a78f34835" -date-fns@^1.27.2: - version "1.29.0" - resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-1.29.0.tgz#12e609cdcb935127311d04d33334e2960a2a54e6" - date-format@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/date-format/-/date-format-1.2.0.tgz#615e828e233dd1ab9bb9ae0950e0ceccfa6ecad8" @@ -2383,10 +2361,6 @@ date-now@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/date-now/-/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b" -dateformat@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-3.0.3.tgz#a6e37499a4d9a9cf85ef5872044d62901c9889ae" - de-indent@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/de-indent/-/de-indent-1.0.2.tgz#b2038e846dc33baa5796128d0804b455b8c1e21d" @@ -2427,7 +2401,7 @@ decode-uri-component@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" -decompress-response@^3.2.0, decompress-response@^3.3.0: +decompress-response@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-3.3.0.tgz#80a4dd323748384bfa248083622aedec982adff3" dependencies: @@ -2437,10 +2411,6 @@ deep-equal@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5" -deep-extend@^0.5.1: - version "0.5.1" - resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.5.1.tgz#b894a9dd90d3023fbf1c55a394fb858eb2066f1f" - deep-extend@~0.4.0: version "0.4.2" resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.4.2.tgz#48b699c27e334bf89f10892be432f6e4c7d34a7f" @@ -2543,10 +2513,6 @@ destroy@~1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" -detect-conflict@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/detect-conflict/-/detect-conflict-1.0.1.tgz#088657a66a961c05019db7c4230883b1c6b4176e" - detect-indent@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-4.0.0.tgz#f76d064352cdf43a1cb6ce619c4ee3a9475de208" @@ -2565,7 +2531,7 @@ di@^0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/di/-/di-0.0.1.tgz#806649326ceaa7caa3306d75d985ea2748ba913c" -diff@^3.3.1, diff@^3.4.0, diff@^3.5.0: +diff@^3.4.0: version "3.5.0" resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12" @@ -2577,13 +2543,6 @@ diffie-hellman@^5.0.0: miller-rabin "^4.0.0" randombytes "^2.0.0" -dir-glob@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-2.0.0.tgz#0b205d2b6aef98238ca286598a8204d29d0a0034" - dependencies: - arrify "^1.0.1" - path-type "^3.0.0" - dns-equal@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/dns-equal/-/dns-equal-1.0.0.tgz#b39e7f1da6eb0a75ba9c17324b34753c47e0654d" @@ -2696,15 +2655,11 @@ ecc-jsbn@~0.1.1: dependencies: jsbn "~0.1.0" -editions@^1.3.3: - version "1.3.4" - resolved "https://registry.yarnpkg.com/editions/-/editions-1.3.4.tgz#3662cb592347c3168eb8e498a0ff73271d67f50b" - ee-first@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" -ejs@^2.5.7, ejs@^2.5.9: +ejs@^2.5.7: version "2.5.9" resolved "https://registry.yarnpkg.com/ejs/-/ejs-2.5.9.tgz#7ba254582a560d267437109a68354112475b0ce5" @@ -2712,10 +2667,6 @@ electron-to-chromium@^1.2.7: version "1.3.3" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.3.tgz#651eb63fe89f39db70ffc8dbd5d9b66958bc6a0e" -elegant-spinner@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/elegant-spinner/-/elegant-spinner-1.0.1.tgz#db043521c95d7e303fd8f345bedc3349cfb0729e" - elliptic@^6.0.0: version "6.4.0" resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.4.0.tgz#cac9af8762c85836187003c8dfe193e5e2eae5df" @@ -2809,10 +2760,6 @@ entities@^1.1.1, entities@~1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.1.tgz#6e5c2d0a5621b5dadaecef80b90edfb5cd7772f0" -envinfo@^4.4.2: - version "4.4.2" - resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-4.4.2.tgz#472c49f3a8b9bca73962641ce7cb692bf623cd1c" - errno@^0.1.3: version "0.1.4" resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.4.tgz#b896e23a9e5e8ba33871fc996abd3635fc9a1c7d" @@ -2831,19 +2778,6 @@ error-ex@^1.2.0: dependencies: is-arrayish "^0.2.1" -error-ex@^1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.1.tgz#f855a86ce61adc4e8621c3cda21e7a7612c3a8dc" - dependencies: - is-arrayish "^0.2.1" - -error@^7.0.2: - version "7.0.2" - resolved "https://registry.yarnpkg.com/error/-/error-7.0.2.tgz#a5f75fff4d9926126ddac0ea5dc38e689153cb02" - dependencies: - string-template "~0.2.1" - xtend "~4.0.0" - es-abstract@^1.7.0: version "1.10.0" resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.10.0.tgz#1ecb36c197842a00d8ee4c2dfd8646bb97d60864" @@ -2969,12 +2903,11 @@ eslint-plugin-promise@^3.8.0: version "3.8.0" resolved "https://registry.yarnpkg.com/eslint-plugin-promise/-/eslint-plugin-promise-3.8.0.tgz#65ebf27a845e3c1e9d6f6a5622ddd3801694b621" -eslint-plugin-vue@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/eslint-plugin-vue/-/eslint-plugin-vue-4.0.1.tgz#afda92cfd7e7363b1fbdb1a772dd63359a9ce96a" +eslint-plugin-vue@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-vue/-/eslint-plugin-vue-4.5.0.tgz#09d6597f4849e31a3846c2c395fccf17685b69c3" dependencies: - require-all "^2.2.0" - vue-eslint-parser "^2.0.1" + vue-eslint-parser "^2.0.3" eslint-restricted-globals@^0.1.1: version "0.1.1" @@ -3048,35 +2981,30 @@ esprima@3.x.x, esprima@^3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.1.3.tgz#fdca51cee6133895e3c88d535ce49dbff62a4633" -esprima@^4.0.0, esprima@~4.0.0: +esprima@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.0.tgz#4499eddcd1110e0b218bacf2fa7f7f59f55ca804" esquery@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.0.0.tgz#cfba8b57d7fba93f17298a8a006a04cda13d80fa" + version "1.0.1" + resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.0.1.tgz#406c51658b1f5991a5f9b62b1dc25b00e3e5c708" dependencies: estraverse "^4.0.0" esrecurse@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.1.0.tgz#4713b6536adf7f2ac4f327d559e7756bff648220" + version "4.2.1" + resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.2.1.tgz#007a3b9fdbc2b3bb87e4879ea19c92fdbd3942cf" dependencies: - estraverse "~4.1.0" - object-assign "^4.0.1" + estraverse "^4.1.0" estraverse@^1.9.1: version "1.9.3" resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-1.9.3.tgz#af67f2dc922582415950926091a4005d29c9bb44" -estraverse@^4.0.0, estraverse@^4.1.1, estraverse@^4.2.0: +estraverse@^4.0.0, estraverse@^4.1.0, estraverse@^4.1.1, estraverse@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.2.0.tgz#0dee3fed31fcd469618ce7342099fc1afa0bdb13" -estraverse@~4.1.0: - version "4.1.1" - resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.1.1.tgz#f6caca728933a850ef90661d0e17982ba47111a2" - esutils@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b" @@ -3134,10 +3062,6 @@ execa@^0.7.0: signal-exit "^3.0.0" strip-eof "^1.0.0" -exit-hook@^1.0.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/exit-hook/-/exit-hook-1.1.1.tgz#f05ca233b48c05d54fff07765df8507e95c02ff8" - expand-braces@^0.1.1: version "0.1.2" resolved "https://registry.yarnpkg.com/expand-braces/-/expand-braces-0.1.2.tgz#488b1d1d2451cb3d3a6b192cfc030f44c5855fea" @@ -3177,12 +3101,6 @@ expand-range@^1.8.1: dependencies: fill-range "^2.1.0" -expand-tilde@^2.0.0, expand-tilde@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/expand-tilde/-/expand-tilde-2.0.2.tgz#97e801aa052df02454de46b02bf621642cdc8502" - dependencies: - homedir-polyfill "^1.0.1" - exports-loader@^0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/exports-loader/-/exports-loader-0.7.0.tgz#84881c784dea6036b8e1cd1dac3da9b6409e21a5" @@ -3281,16 +3199,6 @@ fast-deep-equal@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz#96256a3bc975595eb36d82e9929d060d893439ff" -fast-glob@^2.0.2: - version "2.2.1" - resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-2.2.1.tgz#686c2345be88f3741e174add0be6f2e5b6078889" - dependencies: - "@mrmlnc/readdir-enhanced" "^2.2.1" - glob-parent "^3.1.0" - is-glob "^4.0.0" - merge2 "^1.2.1" - micromatch "^3.1.10" - fast-json-stable-stringify@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2" @@ -3315,13 +3223,6 @@ faye-websocket@~0.11.0: dependencies: websocket-driver ">=0.5.1" -figures@^1.7.0: - version "1.7.0" - resolved "https://registry.yarnpkg.com/figures/-/figures-1.7.0.tgz#cbe1e3affcf1cd44b80cadfed28dc793a9701d2e" - dependencies: - escape-string-regexp "^1.0.5" - object-assign "^4.1.0" - figures@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/figures/-/figures-2.0.0.tgz#3ab1a2d2a62c8bfb431a0c94cb797a2fce27c962" @@ -3417,12 +3318,6 @@ find-up@^2.0.0, find-up@^2.1.0: dependencies: locate-path "^2.0.0" -first-chunk-stream@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/first-chunk-stream/-/first-chunk-stream-2.0.0.tgz#1bdecdb8e083c0664b91945581577a43a9f31d70" - dependencies: - readable-stream "^2.0.2" - flat-cache@^1.2.1: version "1.2.2" resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-1.2.2.tgz#fa86714e72c21db88601761ecf2f555d1abc6b96" @@ -3436,10 +3331,6 @@ flatten@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/flatten/-/flatten-1.0.2.tgz#dae46a9d78fbe25292258cc1e780a41d95c03782" -flow-parser@^0.*: - version "0.66.0" - resolved "https://registry.yarnpkg.com/flow-parser/-/flow-parser-0.66.0.tgz#be583fefb01192aa5164415d31a6241b35718983" - flush-write-stream@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/flush-write-stream/-/flush-write-stream-1.0.2.tgz#c81b90d8746766f1a609a46809946c45dd8ae417" @@ -3644,26 +3535,6 @@ getpass@^0.1.1: dependencies: assert-plus "^1.0.0" -gh-got@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/gh-got/-/gh-got-6.0.0.tgz#d74353004c6ec466647520a10bd46f7299d268d0" - dependencies: - got "^7.0.0" - is-plain-obj "^1.1.0" - -github-username@^4.0.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/github-username/-/github-username-4.1.0.tgz#cbe280041883206da4212ae9e4b5f169c30bf417" - dependencies: - gh-got "^6.0.0" - -glob-all@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/glob-all/-/glob-all-3.1.0.tgz#8913ddfb5ee1ac7812656241b03d5217c64b02ab" - dependencies: - glob "^7.0.5" - yargs "~1.2.6" - glob-base@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/glob-base/-/glob-base-0.3.0.tgz#dbb164f6221b1c0b1ccf82aea328b497df0ea3c4" @@ -3684,10 +3555,6 @@ glob-parent@^3.1.0: is-glob "^3.1.0" path-dirname "^1.0.0" -glob-to-regexp@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz#8c5a1494d2066c570cc3bfe4496175acc4d502ab" - glob@^5.0.15: version "5.0.15" resolved "https://registry.yarnpkg.com/glob/-/glob-5.0.15.tgz#1bc936b9e02f4a603fcc222ecf7633d30b8b93b1" @@ -3698,7 +3565,7 @@ glob@^5.0.15: once "^1.3.0" path-is-absolute "^1.0.0" -glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.1.1, glob@^7.1.2: +glob@^7.0.3, glob@^7.0.5, glob@^7.1.1, glob@^7.1.2: version "7.1.2" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" dependencies: @@ -3715,23 +3582,9 @@ global-dirs@^0.1.0: dependencies: ini "^1.3.4" -global-modules@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-1.0.0.tgz#6d770f0eb523ac78164d72b5e71a8877265cc3ea" - dependencies: - global-prefix "^1.0.1" - is-windows "^1.0.1" - resolve-dir "^1.0.0" - -global-prefix@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/global-prefix/-/global-prefix-1.0.2.tgz#dbf743c6c14992593c655568cb66ed32c0122ebe" - dependencies: - expand-tilde "^2.0.2" - homedir-polyfill "^1.0.1" - ini "^1.3.4" - is-windows "^1.0.1" - which "^1.2.14" +global-modules-path@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/global-modules-path/-/global-modules-path-2.1.0.tgz#923ec524e8726bb0c1a4ed4b8e21e1ff80c88bbb" globals@^11.0.1, globals@^11.1.0: version "11.5.0" @@ -3762,29 +3615,6 @@ globby@^6.1.0: pify "^2.0.0" pinkie-promise "^2.0.0" -globby@^7.1.1: - version "7.1.1" - resolved "https://registry.yarnpkg.com/globby/-/globby-7.1.1.tgz#fb2ccff9401f8600945dfada97440cca972b8680" - dependencies: - array-union "^1.0.1" - dir-glob "^2.0.0" - glob "^7.1.2" - ignore "^3.3.5" - pify "^3.0.0" - slash "^1.0.0" - -globby@^8.0.0, globby@^8.0.1: - version "8.0.1" - resolved "https://registry.yarnpkg.com/globby/-/globby-8.0.1.tgz#b5ad48b8aa80b35b814fc1281ecc851f1d2b5b50" - dependencies: - array-union "^1.0.1" - dir-glob "^2.0.0" - fast-glob "^2.0.2" - glob "^7.1.2" - ignore "^3.3.5" - pify "^3.0.0" - slash "^1.0.0" - good-listener@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/good-listener/-/good-listener-1.2.2.tgz#d53b30cdf9313dffb7dc9a0d477096aa6d145c50" @@ -3807,26 +3637,7 @@ got@^6.7.1: unzip-response "^2.0.1" url-parse-lax "^1.0.0" -got@^7.0.0: - version "7.1.0" - resolved "https://registry.yarnpkg.com/got/-/got-7.1.0.tgz#05450fd84094e6bbea56f451a43a9c289166385a" - dependencies: - decompress-response "^3.2.0" - duplexer3 "^0.1.4" - get-stream "^3.0.0" - is-plain-obj "^1.1.0" - is-retry-allowed "^1.0.0" - is-stream "^1.0.0" - isurl "^1.0.0-alpha5" - lowercase-keys "^1.0.0" - p-cancelable "^0.3.0" - p-timeout "^1.1.1" - safe-buffer "^5.0.1" - timed-out "^4.0.0" - url-parse-lax "^1.0.0" - url-to-options "^1.0.1" - -got@^8.0.3, got@^8.2.0: +got@^8.0.3: version "8.3.0" resolved "https://registry.yarnpkg.com/got/-/got-8.3.0.tgz#6ba26e75f8a6cc4c6b3eb1fe7ce4fec7abac8533" dependencies: @@ -3858,12 +3669,6 @@ graphlib@^2.1.1: dependencies: lodash "^4.11.1" -grouped-queue@^0.3.3: - version "0.3.3" - resolved "https://registry.yarnpkg.com/grouped-queue/-/grouped-queue-0.3.3.tgz#c167d2a5319c5a0e0964ef6a25b7c2df8996c85c" - dependencies: - lodash "^4.17.2" - gzip-size@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/gzip-size/-/gzip-size-4.1.0.tgz#8ae096257eabe7d69c45be2b67c448124ffb517c" @@ -3928,10 +3733,6 @@ has-binary2@~1.0.2: dependencies: isarray "2.0.1" -has-color@~0.1.0: - version "0.1.7" - resolved "https://registry.yarnpkg.com/has-color/-/has-color-0.1.7.tgz#67144a5260c34fc3cca677d041daf52fe7b78b2f" - has-cors@1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/has-cors/-/has-cors-1.1.0.tgz#5e474793f7ea9843d1bb99c23eef49ff126fff39" @@ -4067,12 +3868,6 @@ home-or-tmp@^2.0.0: os-homedir "^1.0.0" os-tmpdir "^1.0.1" -homedir-polyfill@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/homedir-polyfill/-/homedir-polyfill-1.0.1.tgz#4c2bbc8a758998feebf5ed68580f76d46768b4bc" - dependencies: - parse-passwd "^1.0.0" - hosted-git-info@^2.1.4: version "2.2.0" resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.2.0.tgz#7a0d097863d886c0fabbdcd37bf1758d8becf8a5" @@ -4203,9 +3998,9 @@ icss-utils@^2.1.0: dependencies: postcss "^6.0.1" -ieee754@^1.1.4: - version "1.1.8" - resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.8.tgz#be33d40ac10ef1926701f6f08a2d86fbfd1ad3e4" +ieee754@^1.1.11, ieee754@^1.1.4: + version "1.1.11" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.11.tgz#c16384ffe00f5b7835824e67b6f2bd44a5229455" iferr@^0.1.5: version "0.1.5" @@ -4215,7 +4010,7 @@ ignore-by-default@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/ignore-by-default/-/ignore-by-default-1.0.1.tgz#48ca6d72f6c6a3af00a9ad4ae6876be3889e2b09" -ignore@^3.3.3, ignore@^3.3.5, ignore@^3.3.7: +ignore@^3.3.3, ignore@^3.3.7: version "3.3.8" resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.8.tgz#3f8e9c35d38708a3a7e0e9abb6c73e7ee7707b2b" @@ -4251,10 +4046,6 @@ indent-string@^2.1.0: dependencies: repeating "^2.0.0" -indent-string@^3.0.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-3.2.0.tgz#4a5fd6d27cc332f37e5419a504dbb837105c9289" - indexes-of@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/indexes-of/-/indexes-of-1.0.1.tgz#f30f716c8e2bd346c7b67d3df3915566a7c05607" @@ -4309,7 +4100,7 @@ inquirer@^3.0.6: strip-ansi "^4.0.0" through "^2.3.6" -inquirer@^5.1.0, inquirer@^5.2.0: +inquirer@^5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-5.2.0.tgz#db350c2b73daca77ff1243962e9f22f099685726" dependencies: @@ -4333,7 +4124,7 @@ internal-ip@1.2.0: dependencies: meow "^3.3.0" -interpret@^1.0.0, interpret@^1.0.4: +interpret@^1.0.0, interpret@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.1.0.tgz#7ed1b1410c6a0e0f78cf95d3b8440c63f78b8614" @@ -4553,12 +4344,6 @@ is-object@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/is-object/-/is-object-1.0.1.tgz#8952688c5ec2ffd6b03ecc85e769e02903083470" -is-observable@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/is-observable/-/is-observable-0.2.0.tgz#b361311d83c6e5d726cabf5e250b0237106f5ae2" - dependencies: - symbol-observable "^0.2.2" - is-odd@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/is-odd/-/is-odd-2.0.0.tgz#7646624671fd7ea558ccd9a2795182f2958f1b24" @@ -4581,7 +4366,7 @@ is-path-inside@^1.0.0: dependencies: path-is-inside "^1.0.1" -is-plain-obj@^1.0.0, is-plain-obj@^1.1.0: +is-plain-obj@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e" @@ -4627,12 +4412,6 @@ is-retry-allowed@^1.0.0, is-retry-allowed@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/is-retry-allowed/-/is-retry-allowed-1.1.0.tgz#11a060568b67339444033d0125a61a20d564fb34" -is-scoped@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-scoped/-/is-scoped-1.0.0.tgz#449ca98299e713038256289ecb2b540dc437cb30" - dependencies: - scoped-regex "^1.0.0" - is-stream@^1.0.0, is-stream@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" @@ -4655,7 +4434,7 @@ is-utf8@^0.2.0: version "0.2.1" resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72" -is-windows@^1.0.1, is-windows@^1.0.2: +is-windows@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" @@ -4675,7 +4454,7 @@ isarray@2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.1.tgz#a37d94ed9cda2d59865c9f76fe596ee1f338741e" -isbinaryfile@^3.0.0, isbinaryfile@^3.0.2: +isbinaryfile@^3.0.0: version "3.0.2" resolved "https://registry.yarnpkg.com/isbinaryfile/-/isbinaryfile-3.0.2.tgz#4a3e974ec0cba9004d3fc6cde7209ea69368a621" @@ -4783,14 +4562,6 @@ istanbul@^0.4.5: which "^1.1.1" wordwrap "^1.0.0" -istextorbinary@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/istextorbinary/-/istextorbinary-2.2.1.tgz#a5231a08ef6dd22b268d0895084cf8d58b5bec53" - dependencies: - binaryextensions "2" - editions "^1.3.3" - textextensions "2" - isurl@^1.0.0-alpha5: version "1.0.0" resolved "https://registry.yarnpkg.com/isurl/-/isurl-1.0.0.tgz#b27f4f49f3cdaa3ea44a0a5b7f3462e6edc39d67" @@ -4854,46 +4625,6 @@ jsbn@~0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" -jscodeshift@^0.4.0: - version "0.4.1" - resolved "https://registry.yarnpkg.com/jscodeshift/-/jscodeshift-0.4.1.tgz#da91a1c2eccfa03a3387a21d39948e251ced444a" - dependencies: - async "^1.5.0" - babel-plugin-transform-flow-strip-types "^6.8.0" - babel-preset-es2015 "^6.9.0" - babel-preset-stage-1 "^6.5.0" - babel-register "^6.9.0" - babylon "^6.17.3" - colors "^1.1.2" - flow-parser "^0.*" - lodash "^4.13.1" - micromatch "^2.3.7" - node-dir "0.1.8" - nomnom "^1.8.1" - recast "^0.12.5" - temp "^0.8.1" - write-file-atomic "^1.2.0" - -jscodeshift@^0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/jscodeshift/-/jscodeshift-0.5.0.tgz#bdb7b6cc20dd62c16aa728c3fa2d2fe66ca7c748" - dependencies: - babel-plugin-transform-flow-strip-types "^6.8.0" - babel-preset-es2015 "^6.9.0" - babel-preset-stage-1 "^6.5.0" - babel-register "^6.9.0" - babylon "^7.0.0-beta.30" - colors "^1.1.2" - flow-parser "^0.*" - lodash "^4.13.1" - micromatch "^2.3.7" - neo-async "^2.5.0" - node-dir "0.1.8" - nomnom "^1.8.1" - recast "^0.14.1" - temp "^0.8.1" - write-file-atomic "^1.2.0" - jsesc@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-1.3.0.tgz#46c3fec8c1892b12b0833db9bc7622176dbab34b" @@ -4910,7 +4641,7 @@ json-buffer@3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.0.tgz#5b1f397afc75d677bde8bcfc0e47e1f9a3d9a898" -json-parse-better-errors@^1.0.1: +json-parse-better-errors@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9" @@ -5108,6 +4839,10 @@ lcid@^1.0.0: dependencies: invert-kv "^1.0.0" +leb@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/leb/-/leb-0.3.0.tgz#32bee9fad168328d6aea8522d833f4180eed1da3" + levn@^0.3.0, levn@~0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee" @@ -5137,54 +4872,6 @@ lie@~3.1.0: dependencies: immediate "~3.0.5" -listr-silent-renderer@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/listr-silent-renderer/-/listr-silent-renderer-1.1.1.tgz#924b5a3757153770bf1a8e3fbf74b8bbf3f9242e" - -listr-update-renderer@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/listr-update-renderer/-/listr-update-renderer-0.4.0.tgz#344d980da2ca2e8b145ba305908f32ae3f4cc8a7" - dependencies: - chalk "^1.1.3" - cli-truncate "^0.2.1" - elegant-spinner "^1.0.1" - figures "^1.7.0" - indent-string "^3.0.0" - log-symbols "^1.0.2" - log-update "^1.0.2" - strip-ansi "^3.0.1" - -listr-verbose-renderer@^0.4.0: - version "0.4.1" - resolved "https://registry.yarnpkg.com/listr-verbose-renderer/-/listr-verbose-renderer-0.4.1.tgz#8206f4cf6d52ddc5827e5fd14989e0e965933a35" - dependencies: - chalk "^1.1.3" - cli-cursor "^1.0.2" - date-fns "^1.27.2" - figures "^1.7.0" - -listr@^0.13.0: - version "0.13.0" - resolved "https://registry.yarnpkg.com/listr/-/listr-0.13.0.tgz#20bb0ba30bae660ee84cc0503df4be3d5623887d" - dependencies: - chalk "^1.1.3" - cli-truncate "^0.2.1" - figures "^1.7.0" - indent-string "^2.1.0" - is-observable "^0.2.0" - is-promise "^2.1.0" - is-stream "^1.1.0" - listr-silent-renderer "^1.1.1" - listr-update-renderer "^0.4.0" - listr-verbose-renderer "^0.4.0" - log-symbols "^1.0.2" - log-update "^1.0.2" - ora "^0.2.3" - p-map "^1.1.1" - rxjs "^5.4.2" - stream-to-observable "^0.2.0" - strip-ansi "^3.0.1" - load-json-file@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0" @@ -5204,15 +4891,6 @@ load-json-file@^2.0.0: pify "^2.0.0" strip-bom "^3.0.0" -load-json-file@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-4.0.0.tgz#2f5f45ab91e33216234fd53adab668eb4ec0993b" - dependencies: - graceful-fs "^4.1.2" - parse-json "^4.0.0" - pify "^3.0.0" - strip-bom "^3.0.0" - loader-runner@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.3.0.tgz#f482aea82d543e07921700d5a46ef26fdac6b8a2" @@ -5272,29 +4950,16 @@ lodash@4.17.4: version "4.17.4" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae" -lodash@^4.0.0, lodash@^4.11.1, lodash@^4.13.1, lodash@^4.14.0, lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.2, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.0, lodash@^4.3.0, lodash@^4.5.0: +lodash@^4.0.0, lodash@^4.11.1, lodash@^4.14.0, lodash@^4.15.0, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.0, lodash@^4.3.0, lodash@^4.5.0: version "4.17.10" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.10.tgz#1b7793cf7259ea38fb3661d4d38b3260af8ae4e7" -log-symbols@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-1.0.2.tgz#376ff7b58ea3086a0f09facc74617eca501e1a18" - dependencies: - chalk "^1.0.0" - -log-symbols@^2.1.0, log-symbols@^2.2.0: +log-symbols@^2.1.0: version "2.2.0" resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-2.2.0.tgz#5740e1c5d6f0dfda4ad9323b5332107ef6b4c40a" dependencies: chalk "^2.0.1" -log-update@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/log-update/-/log-update-1.0.2.tgz#19929f64c4093d2d2e7075a1dad8af59c296b8d1" - dependencies: - ansi-escapes "^1.0.0" - cli-cursor "^1.0.2" - log4js@^2.3.9: version "2.5.3" resolved "https://registry.yarnpkg.com/log4js/-/log4js-2.5.3.tgz#38bb7bde5e9c1c181bd75e8bc128c5cd0409caf1" @@ -5330,6 +4995,10 @@ loglevelnext@^1.0.1: version "1.0.3" resolved "https://registry.yarnpkg.com/loglevelnext/-/loglevelnext-1.0.3.tgz#0f69277e73bbbf2cd61b94d82313216bf87ac66e" +long@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/long/-/long-3.2.0.tgz#d821b7138ca1cb581c172990ef14db200b5c474b" + longest@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/longest/-/longest-1.0.1.tgz#30a0b2da38f73770e8294a0d22e6625ed77d0097" @@ -5391,12 +5060,16 @@ mailgun-js@^0.7.0: q "~1.4.0" tsscmp "~1.0.0" -make-dir@^1.0.0, make-dir@^1.1.0: +make-dir@^1.0.0: version "1.2.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.2.0.tgz#6d6a49eead4aae296c53bbf3a1a008bd6c89469b" dependencies: pify "^3.0.0" +mamacro@^0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/mamacro/-/mamacro-0.0.3.tgz#ad2c9576197c9f1abf308d0787865bd975a3f3e4" + map-cache@^0.2.2: version "0.2.2" resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf" @@ -5438,30 +5111,6 @@ media-typer@0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" -mem-fs-editor@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/mem-fs-editor/-/mem-fs-editor-4.0.1.tgz#27e6b59df91b37248e9be2145b1bea84695103ed" - dependencies: - commondir "^1.0.1" - deep-extend "^0.5.1" - ejs "^2.5.9" - glob "^7.0.3" - globby "^8.0.0" - isbinaryfile "^3.0.2" - mkdirp "^0.5.0" - multimatch "^2.0.0" - rimraf "^2.2.8" - through2 "^2.0.0" - vinyl "^2.0.1" - -mem-fs@^1.1.0: - version "1.1.3" - resolved "https://registry.yarnpkg.com/mem-fs/-/mem-fs-1.1.3.tgz#b8ae8d2e3fcb6f5d3f9165c12d4551a065d989cc" - dependencies: - through2 "^2.0.0" - vinyl "^1.1.0" - vinyl-file "^2.0.0" - mem@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/mem/-/mem-1.1.0.tgz#5edd52b485ca1d900fe64895505399a0dfa45f76" @@ -5504,15 +5153,11 @@ merge-source-map@^1.1.0: dependencies: source-map "^0.6.1" -merge2@^1.2.1: - version "1.2.2" - resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.2.2.tgz#03212e3da8d86c4d8523cebd6318193414f94e34" - methods@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" -micromatch@^2.1.5, micromatch@^2.3.7: +micromatch@^2.1.5: version "2.3.11" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-2.3.11.tgz#86677c97d1720b363431d04d0d15293bd38c1565" dependencies: @@ -5530,7 +5175,7 @@ micromatch@^2.1.5, micromatch@^2.3.7: parse-glob "^3.0.4" regex-cache "^0.4.2" -micromatch@^3.1.10, micromatch@^3.1.4, micromatch@^3.1.8, micromatch@^3.1.9: +micromatch@^3.1.4, micromatch@^3.1.8, micromatch@^3.1.9: version "3.1.10" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23" dependencies: @@ -5603,10 +5248,6 @@ minimist@0.0.8: version "0.0.8" resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" -minimist@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.1.0.tgz#99df657a52574c21c9057497df742790b2b4c0de" - minimist@^1.1.3, minimist@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" @@ -5637,7 +5278,7 @@ mixin-deep@^1.2.0: for-in "^1.0.2" is-extendable "^1.0.1" -mkdirp@0.5.x, "mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.0, mkdirp@~0.5.1: +mkdirp@0.5.x, "mkdirp@>=0.5 0", mkdirp@^0.5.1, mkdirp@~0.5.0, mkdirp@~0.5.1: version "0.5.1" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" dependencies: @@ -5647,9 +5288,13 @@ moment@2.x, moment@^2.18.1: version "2.19.2" resolved "https://registry.yarnpkg.com/moment/-/moment-2.19.2.tgz#8a7f774c95a64550b4c7ebd496683908f9419dbe" -monaco-editor@0.10.0: - version "0.10.0" - resolved "https://registry.yarnpkg.com/monaco-editor/-/monaco-editor-0.10.0.tgz#6604932585fe9c1f993f000a503d0d20fbe5896a" +monaco-editor-webpack-plugin@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/monaco-editor-webpack-plugin/-/monaco-editor-webpack-plugin-1.2.1.tgz#577ed091420f422bb8f0ff3a8899dd82344da56d" + +monaco-editor@0.13.1: + version "0.13.1" + resolved "https://registry.yarnpkg.com/monaco-editor/-/monaco-editor-0.13.1.tgz#6b9ce20e4d1c945042d256825eb133cb23315a52" mousetrap@^1.4.6: version "1.4.6" @@ -5685,15 +5330,6 @@ multicast-dns@^6.0.1: dns-packet "^1.0.1" thunky "^0.1.0" -multimatch@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/multimatch/-/multimatch-2.1.0.tgz#9c7906a22fb4c02919e2f5f75161b4cdbd4b2a2b" - dependencies: - array-differ "^1.0.0" - array-union "^1.0.1" - arrify "^1.0.0" - minimatch "^3.0.0" - mute-stream@0.0.7: version "0.0.7" resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab" @@ -5739,10 +5375,6 @@ nice-try@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.4.tgz#d93962f6c52f2c1558c0fbda6d512819f1efe1c4" -node-dir@0.1.8: - version "0.1.8" - resolved "https://registry.yarnpkg.com/node-dir/-/node-dir-0.1.8.tgz#55fb8deb699070707fb67f91a460f0448294c77d" - node-forge@0.6.33: version "0.6.33" resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.6.33.tgz#463811879f573d45155ad6a9f43dc296e8e85ebc" @@ -5859,13 +5491,6 @@ nodemon@^1.17.3: undefsafe "^2.0.2" update-notifier "^2.3.0" -nomnom@^1.8.1: - version "1.8.1" - resolved "https://registry.yarnpkg.com/nomnom/-/nomnom-1.8.1.tgz#2151f722472ba79e50a76fc125bb8c8f2e4dc2a7" - dependencies: - chalk "~0.4.0" - underscore "~1.6.0" - nopt@3.x: version "3.0.6" resolved "https://registry.yarnpkg.com/nopt/-/nopt-3.0.6.tgz#c6465dbf08abcd4db359317f79ac68a646b28ff9" @@ -6011,10 +5636,6 @@ once@1.x, once@^1.3.0, once@^1.3.1, once@^1.3.3, once@^1.4.0: dependencies: wrappy "1" -onetime@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/onetime/-/onetime-1.1.0.tgz#a1f7838f8314c516f05ecefcbc4ccfe04b4ed789" - onetime@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/onetime/-/onetime-2.0.1.tgz#067428230fd67443b2794b22bba528b6867962d4" @@ -6049,15 +5670,6 @@ optionator@^0.8.1, optionator@^0.8.2: type-check "~0.3.2" wordwrap "~1.0.0" -ora@^0.2.3: - version "0.2.3" - resolved "https://registry.yarnpkg.com/ora/-/ora-0.2.3.tgz#37527d220adcd53c39b73571d754156d5db657a4" - dependencies: - chalk "^1.1.1" - cli-cursor "^1.0.2" - cli-spinners "^0.1.2" - object-assign "^4.0.1" - original@>=0.0.5: version "1.0.0" resolved "https://registry.yarnpkg.com/original/-/original-1.0.0.tgz#9147f93fa1696d04be61e01bd50baeaca656bd3b" @@ -6091,20 +5703,10 @@ osenv@^0.1.4: os-homedir "^1.0.0" os-tmpdir "^1.0.0" -p-cancelable@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-0.3.0.tgz#b9e123800bcebb7ac13a479be195b507b98d30fa" - p-cancelable@^0.4.0: version "0.4.1" resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-0.4.1.tgz#35f363d67d52081c8d9585e37bcceb7e0bbcb2a0" -p-each-series@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/p-each-series/-/p-each-series-1.0.0.tgz#930f3d12dd1f50e7434457a22cd6f04ac6ad7f71" - dependencies: - p-reduce "^1.0.0" - p-finally@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" @@ -6113,11 +5715,7 @@ p-is-promise@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/p-is-promise/-/p-is-promise-1.1.0.tgz#9c9456989e9f6588017b0434d56097675c3da05e" -p-lazy@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/p-lazy/-/p-lazy-1.0.0.tgz#ec53c802f2ee3ac28f166cc82d0b2b02de27a835" - -p-limit@^1.0.0, p-limit@^1.1.0: +p-limit@^1.1.0: version "1.2.0" resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.2.0.tgz#0e92b6bedcb59f022c13d0f1949dc82d15909f1c" dependencies: @@ -6133,16 +5731,6 @@ p-map@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/p-map/-/p-map-1.1.1.tgz#05f5e4ae97a068371bc2a5cc86bfbdbc19c4ae7a" -p-reduce@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/p-reduce/-/p-reduce-1.0.0.tgz#18c2b0dd936a4690a529f8231f58a0fdb6a47dfa" - -p-timeout@^1.1.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/p-timeout/-/p-timeout-1.2.1.tgz#5eb3b353b7fce99f101a1038880bb054ebbea386" - dependencies: - p-finally "^1.0.0" - p-timeout@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/p-timeout/-/p-timeout-2.0.1.tgz#d8dd1979595d2dc0139e1fe46b8b646cb3cdf038" @@ -6223,17 +5811,6 @@ parse-json@^2.2.0: dependencies: error-ex "^1.2.0" -parse-json@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-4.0.0.tgz#be35f5425be1f7f6c747184f98a788cb99477ee0" - dependencies: - error-ex "^1.3.1" - json-parse-better-errors "^1.0.1" - -parse-passwd@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/parse-passwd/-/parse-passwd-1.0.0.tgz#6d5b934a456993b23d37f40a382d6f1666a8e5c6" - parseqs@0.0.5: version "0.0.5" resolved "https://registry.yarnpkg.com/parseqs/-/parseqs-0.0.5.tgz#d5208a3738e46766e291ba2ea173684921a8b89d" @@ -6312,12 +5889,6 @@ path-type@^2.0.0: dependencies: pify "^2.0.0" -path-type@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/path-type/-/path-type-3.0.0.tgz#cef31dc8e0a1a3bb0d105c0cd97cf3bf47f4e36f" - dependencies: - pify "^3.0.0" - pause-stream@0.0.11: version "0.0.11" resolved "https://registry.yarnpkg.com/pause-stream/-/pause-stream-0.0.11.tgz#fe5a34b0cbce12b5aa6a2b403ee2e73b602f1445" @@ -6342,7 +5913,7 @@ performance-now@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" -pify@^2.0.0, pify@^2.3.0: +pify@^2.0.0: version "2.3.0" resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" @@ -6668,29 +6239,21 @@ preserve@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b" -prettier@1.11.1: - version "1.11.1" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.11.1.tgz#61e43fc4cd44e68f2b0dfc2c38cd4bb0fccdcc75" - -prettier@^1.11.1, prettier@^1.5.3: +prettier@1.12.1, prettier@^1.11.1: version "1.12.1" resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.12.1.tgz#c1ad20e803e7749faf905a409d2367e06bbe7325" -pretty-bytes@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-4.0.2.tgz#b2bf82e7350d65c6c33aa95aaa5a4f6327f61cd9" - prismjs@^1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.6.0.tgz#118d95fb7a66dba2272e343b345f5236659db365" optionalDependencies: clipboard "^1.5.5" -private@^0.1.6, private@^0.1.8, private@~0.1.5: +private@^0.1.6, private@^0.1.8: version "0.1.8" resolved "https://registry.yarnpkg.com/private/-/private-0.1.8.tgz#2381edb3689f7a53d653190060fcf822d2f368ff" -process-nextick-args@^1.0.6, process-nextick-args@~1.0.6: +process-nextick-args@~1.0.6: version "1.0.7" resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-1.0.7.tgz#150e20b756590ad3f91093f25a4f2ad8bff30ba3" @@ -6898,13 +6461,6 @@ rc@^1.0.1, rc@^1.1.6, rc@^1.1.7: minimist "^1.2.0" strip-json-comments "~2.0.1" -read-chunk@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/read-chunk/-/read-chunk-2.1.0.tgz#6a04c0928005ed9d42e1a6ac5600e19cbc7ff655" - dependencies: - pify "^3.0.0" - safe-buffer "^5.1.1" - read-pkg-up@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-1.0.1.tgz#9d63c13276c065918d57f002a57f40a1b643fb02" @@ -6919,13 +6475,6 @@ read-pkg-up@^2.0.0: find-up "^2.0.0" read-pkg "^2.0.0" -read-pkg-up@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-3.0.0.tgz#3ed496685dba0f8fe118d0691dc51f4a1ff96f07" - dependencies: - find-up "^2.0.0" - read-pkg "^3.0.0" - read-pkg@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-1.1.0.tgz#f5ffaa5ecd29cb31c0474bca7d756b6bb29e3f28" @@ -6942,14 +6491,6 @@ read-pkg@^2.0.0: normalize-package-data "^2.3.2" path-type "^2.0.0" -read-pkg@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-3.0.0.tgz#9cbc686978fee65d16c00e2b19c237fcf6e38389" - dependencies: - load-json-file "^4.0.0" - normalize-package-data "^2.3.2" - path-type "^3.0.0" - "readable-stream@1 || 2", readable-stream@2, readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.4, readable-stream@^2.0.6, readable-stream@^2.1.4, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.2.9, readable-stream@^2.3.0, readable-stream@^2.3.3: version "2.3.4" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.4.tgz#c946c3f47fa7d8eabc0b6150f4a12f69a4574071" @@ -7003,31 +6544,6 @@ readdirp@^2.0.0: readable-stream "^2.0.2" set-immediate-shim "^1.0.1" -recast@^0.12.5: - version "0.12.9" - resolved "https://registry.yarnpkg.com/recast/-/recast-0.12.9.tgz#e8e52bdb9691af462ccbd7c15d5a5113647a15f1" - dependencies: - ast-types "0.10.1" - core-js "^2.4.1" - esprima "~4.0.0" - private "~0.1.5" - source-map "~0.6.1" - -recast@^0.14.1: - version "0.14.7" - resolved "https://registry.yarnpkg.com/recast/-/recast-0.14.7.tgz#4f1497c2b5826d42a66e8e3c9d80c512983ff61d" - dependencies: - ast-types "0.11.3" - esprima "~4.0.0" - private "~0.1.5" - source-map "~0.6.1" - -rechoir@^0.6.2: - version "0.6.2" - resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384" - dependencies: - resolve "^1.1.6" - redent@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/redent/-/redent-1.0.0.tgz#cf916ab1fd5f1f16dfb20822dd6ec7f730c2afde" @@ -7155,14 +6671,6 @@ repeating@^2.0.0: dependencies: is-finite "^1.0.0" -replace-ext@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/replace-ext/-/replace-ext-0.0.1.tgz#29bbd92078a739f0bcce2b4ee41e837953522924" - -replace-ext@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/replace-ext/-/replace-ext-1.0.0.tgz#de63128373fcbf7c3ccfa4de5a480c45a67958eb" - request@2.75.x: version "2.75.0" resolved "https://registry.yarnpkg.com/request/-/request-2.75.0.tgz#d2b8268a286da13eaa5d01adf5d18cc90f657d93" @@ -7252,10 +6760,6 @@ requestretry@^1.2.2: request "^2.74.0" when "^3.7.7" -require-all@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/require-all/-/require-all-2.2.0.tgz#b4420c233ac0282d0ff49b277fb880a8b5de0894" - require-directory@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" @@ -7281,13 +6785,6 @@ resolve-cwd@^2.0.0: dependencies: resolve-from "^3.0.0" -resolve-dir@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/resolve-dir/-/resolve-dir-1.0.1.tgz#79a40644c362be82f26effe739c9bb5382046f43" - dependencies: - expand-tilde "^2.0.0" - global-modules "^1.0.0" - resolve-from@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-1.0.1.tgz#26cbfe935d1aeeeabb29bc3fe5aeb01e93d44226" @@ -7304,7 +6801,7 @@ resolve@1.1.x: version "1.1.7" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b" -resolve@^1.1.6, resolve@^1.4.0, resolve@^1.5.0, resolve@^1.6.0: +resolve@^1.4.0, resolve@^1.5.0, resolve@^1.6.0: version "1.7.1" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.7.1.tgz#aadd656374fd298aee895bc026b8297418677fd3" dependencies: @@ -7316,13 +6813,6 @@ responselike@1.0.2: dependencies: lowercase-keys "^1.0.0" -restore-cursor@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-1.0.1.tgz#34661f46886327fed2991479152252df92daa541" - dependencies: - exit-hook "^1.0.0" - onetime "^1.0.0" - restore-cursor@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-2.0.0.tgz#9f7ee287f82fd326d4fd162923d62129eee0dfaf" @@ -7346,10 +6836,6 @@ rimraf@2, rimraf@^2.2.8, rimraf@^2.5.1, rimraf@^2.5.4, rimraf@^2.6.0, rimraf@^2. dependencies: glob "^7.0.5" -rimraf@~2.2.6: - version "2.2.8" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.2.8.tgz#e439be2aaee327321952730f99a8929e4fc50582" - ripemd160@^2.0.0, ripemd160@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.1.tgz#0f4584295c53a3628af7e6d79aca21ce57d1c6e7" @@ -7357,7 +6843,7 @@ ripemd160@^2.0.0, ripemd160@^2.0.1: hash-base "^2.0.0" inherits "^2.0.1" -run-async@^2.0.0, run-async@^2.2.0: +run-async@^2.2.0: version "2.3.0" resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.3.0.tgz#0371ab4ae0bdd720d4166d7dfda64ff7a445a6c0" dependencies: @@ -7379,7 +6865,7 @@ rx-lite@*, rx-lite@^4.0.8: version "4.0.8" resolved "https://registry.yarnpkg.com/rx-lite/-/rx-lite-4.0.8.tgz#0b1e11af8bc44836f04a6407e92da42467b79444" -rxjs@^5.4.2, rxjs@^5.5.2: +rxjs@^5.5.2: version "5.5.10" resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-5.5.10.tgz#fde02d7a614f6c8683d0d1957827f492e09db045" dependencies: @@ -7418,10 +6904,6 @@ schema-utils@^0.4.0, schema-utils@^0.4.2, schema-utils@^0.4.3, schema-utils@^0.4 ajv "^6.1.0" ajv-keywords "^3.1.0" -scoped-regex@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/scoped-regex/-/scoped-regex-1.0.0.tgz#a346bb1acd4207ae70bd7c0c7ca9e566b6baddb8" - select-hose@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca" @@ -7565,14 +7047,6 @@ shebang-regex@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" -shelljs@^0.8.0: - version "0.8.1" - resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.8.1.tgz#729e038c413a2254c4078b95ed46e0397154a9f1" - dependencies: - glob "^7.0.0" - interpret "^1.0.0" - rechoir "^0.6.2" - signal-exit@^3.0.0, signal-exit@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" @@ -7587,20 +7061,12 @@ slash@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55" -slice-ansi@0.0.4: - version "0.0.4" - resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-0.0.4.tgz#edbf8903f66f7ce2f8eafd6ceed65e264c831b35" - slice-ansi@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-1.0.0.tgz#044f1a49d8842ff307aad6b505ed178bd950134d" dependencies: is-fullwidth-code-point "^2.0.0" -slide@^1.1.5: - version "1.1.6" - resolved "https://registry.yarnpkg.com/slide/-/slide-1.1.6.tgz#56eb027d65b4d2dce6cb2e2d32c4d4afc9e1d707" - smart-buffer@^1.0.13, smart-buffer@^1.0.4: version "1.1.15" resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-1.1.15.tgz#7f114b5b65fab3e2a35aa775bb12f0d1c649bf16" @@ -7937,12 +7403,6 @@ stream-shift@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.0.tgz#d5c752825e5367e786f78e18e445ea223a155952" -stream-to-observable@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/stream-to-observable/-/stream-to-observable-0.2.0.tgz#59d6ea393d87c2c0ddac10aa0d561bc6ba6f0e10" - dependencies: - any-observable "^0.2.0" - streamroller@^0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/streamroller/-/streamroller-0.7.0.tgz#a1d1b7cf83d39afb0d63049a5acbf93493bdf64b" @@ -7956,10 +7416,6 @@ strict-uri-encode@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713" -string-template@~0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/string-template/-/string-template-0.2.1.tgz#42932e598a352d01fc22ec3367d9d84eec6c9add" - string-width@^1.0.1, string-width@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" @@ -8007,17 +7463,6 @@ strip-ansi@^4.0.0: dependencies: ansi-regex "^3.0.0" -strip-ansi@~0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-0.1.1.tgz#39e8a98d044d150660abe4a6808acf70bb7bc991" - -strip-bom-stream@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/strip-bom-stream/-/strip-bom-stream-2.0.0.tgz#f87db5ef2613f6968aa545abfe1ec728b6a829ca" - dependencies: - first-chunk-stream "^2.0.0" - strip-bom "^2.0.0" - strip-bom@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-2.0.0.tgz#6219a85616520491f35788bdbf1447a99c7e6b0e" @@ -8085,10 +7530,6 @@ symbol-observable@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.0.1.tgz#8340fc4702c3122df5d22288f88283f513d3fdd4" -symbol-observable@^0.2.2: - version "0.2.4" - resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-0.2.4.tgz#95a83db26186d6af7e7a18dbd9760a2f86d08f40" - table@^4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/table/-/table-4.0.2.tgz#a33447375391e766ad34d3486e6e2aedc84d2e36" @@ -8129,13 +7570,6 @@ tar@^2.2.1: fstream "^1.0.2" inherits "2" -temp@^0.8.1: - version "0.8.3" - resolved "https://registry.yarnpkg.com/temp/-/temp-0.8.3.tgz#e0c6bc4d26b903124410e4fed81103014dfc1f59" - dependencies: - os-tmpdir "^1.0.0" - rimraf "~2.2.6" - term-size@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/term-size/-/term-size-1.2.0.tgz#458b83887f288fc56d6fffbfad262e26638efa69" @@ -8152,14 +7586,10 @@ test-exclude@^4.2.1: read-pkg-up "^1.0.1" require-main-filename "^1.0.1" -text-table@^0.2.0, text-table@~0.2.0: +text-table@~0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" -textextensions@2: - version "2.2.0" - resolved "https://registry.yarnpkg.com/textextensions/-/textextensions-2.2.0.tgz#38ac676151285b658654581987a0ce1a4490d286" - three-orbit-controls@^82.1.0: version "82.1.0" resolved "https://registry.yarnpkg.com/three-orbit-controls/-/three-orbit-controls-82.1.0.tgz#11a7f33d0a20ecec98f098b37780f6537374fab4" @@ -8172,7 +7602,7 @@ three@^0.84.0: version "0.84.0" resolved "https://registry.yarnpkg.com/three/-/three-0.84.0.tgz#95be85a55a0fa002aa625ed559130957dcffd918" -through2@^2.0.0, through2@^2.0.1: +through2@^2.0.0: version "2.0.3" resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.3.tgz#0004569b37c7c74ba39c43f3ced78d1ad94140be" dependencies: @@ -8381,10 +7811,6 @@ underscore@^1.9.0: version "1.9.0" resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.9.0.tgz#31dbb314cfcc88f169cd3692d9149d81a00a73e4" -underscore@~1.6.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.6.0.tgz#8b38b10cacdef63337b8b24e4ff86d45aea529a8" - underscore@~1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.7.0.tgz#6bbaf0877500d36be34ecaa584e0db9fef035209" @@ -8441,10 +7867,6 @@ unset-value@^1.0.0: has-value "^0.3.1" isobject "^3.0.0" -untildify@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/untildify/-/untildify-3.0.2.tgz#7f1f302055b3fea0f3e81dc78eb36766cb65e3f1" - unzip-response@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/unzip-response/-/unzip-response-2.0.1.tgz#d2f0f737d16b0615e72a6935ed04214572d56f97" @@ -8561,9 +7983,9 @@ uws@~9.14.0: version "9.14.0" resolved "https://registry.yarnpkg.com/uws/-/uws-9.14.0.tgz#fac8386befc33a7a3705cbd58dc47b430ca4dd95" -v8-compile-cache@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-1.1.2.tgz#8d32e4f16974654657e676e0e467a348e89b0dc4" +v8-compile-cache@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.0.0.tgz#526492e35fc616864284700b7043e01baee09f0a" validate-npm-package-license@^3.0.1: version "3.0.1" @@ -8592,36 +8014,6 @@ verror@1.10.0: core-util-is "1.0.2" extsprintf "^1.2.0" -vinyl-file@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/vinyl-file/-/vinyl-file-2.0.0.tgz#a7ebf5ffbefda1b7d18d140fcb07b223efb6751a" - dependencies: - graceful-fs "^4.1.2" - pify "^2.3.0" - pinkie-promise "^2.0.0" - strip-bom "^2.0.0" - strip-bom-stream "^2.0.0" - vinyl "^1.1.0" - -vinyl@^1.1.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/vinyl/-/vinyl-1.2.0.tgz#5c88036cf565e5df05558bfc911f8656df218884" - dependencies: - clone "^1.0.0" - clone-stats "^0.0.1" - replace-ext "0.0.1" - -vinyl@^2.0.1: - version "2.1.0" - resolved "https://registry.yarnpkg.com/vinyl/-/vinyl-2.1.0.tgz#021f9c2cf951d6b939943c89eb5ee5add4fd924c" - dependencies: - clone "^2.1.1" - clone-buffer "^1.0.0" - clone-stats "^1.0.0" - cloneable-readable "^1.0.0" - remove-trailing-separator "^1.0.1" - replace-ext "^1.0.0" - visibilityjs@^1.2.4: version "1.2.4" resolved "https://registry.yarnpkg.com/visibilityjs/-/visibilityjs-1.2.4.tgz#bff8663da62c8c10ad4ee5ae6a1ae6fac4259d63" @@ -8636,7 +8028,7 @@ void-elements@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-2.0.1.tgz#c066afb582bb1cb4128d60ea92392e94d5e9dbec" -vue-eslint-parser@^2.0.1: +vue-eslint-parser@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/vue-eslint-parser/-/vue-eslint-parser-2.0.3.tgz#c268c96c6d94cfe3d938a5f7593959b0ca3360d1" dependencies: @@ -8715,12 +8107,6 @@ wbuf@^1.1.0, wbuf@^1.7.2: dependencies: minimalistic-assert "^1.0.0" -webpack-addons@^1.1.5: - version "1.1.5" - resolved "https://registry.yarnpkg.com/webpack-addons/-/webpack-addons-1.1.5.tgz#2b178dfe873fb6e75e40a819fa5c26e4a9bc837a" - dependencies: - jscodeshift "^0.4.0" - webpack-bundle-analyzer@^2.11.1: version "2.11.1" resolved "https://registry.yarnpkg.com/webpack-bundle-analyzer/-/webpack-bundle-analyzer-2.11.1.tgz#b9fbfb6a32c0a8c1c3237223e90890796b950ab9" @@ -8738,36 +8124,21 @@ webpack-bundle-analyzer@^2.11.1: opener "^1.4.3" ws "^4.0.0" -webpack-cli@^2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-2.1.2.tgz#9c9a4b90584f7b8acaf591238ef0667e04c817f6" +webpack-cli@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-3.0.2.tgz#e48c5662aff8ed5aac3db5f82f51d7f32e50459e" dependencies: - chalk "^2.3.2" + chalk "^2.4.1" cross-spawn "^6.0.5" - diff "^3.5.0" enhanced-resolve "^4.0.0" - envinfo "^4.4.2" - glob-all "^3.1.0" - global-modules "^1.0.0" - got "^8.2.0" + global-modules-path "^2.1.0" import-local "^1.0.0" - inquirer "^5.1.0" - interpret "^1.0.4" - jscodeshift "^0.5.0" - listr "^0.13.0" + inquirer "^5.2.0" + interpret "^1.1.0" loader-utils "^1.1.0" - lodash "^4.17.5" - log-symbols "^2.2.0" - mkdirp "^0.5.1" - p-each-series "^1.0.0" - p-lazy "^1.0.0" - prettier "^1.5.3" - supports-color "^5.3.0" - v8-compile-cache "^1.1.2" - webpack-addons "^1.1.5" + supports-color "^5.4.0" + v8-compile-cache "^2.0.0" yargs "^11.1.0" - yeoman-environment "^2.0.0" - yeoman-generator "^2.0.4" webpack-dev-middleware@3.1.3: version "3.1.3" @@ -8846,10 +8217,15 @@ webpack-stats-plugin@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/webpack-stats-plugin/-/webpack-stats-plugin-0.2.1.tgz#1f5bac13fc25d62cbb5fd0ff646757dc802b8595" -webpack@^4.7.0: - version "4.7.0" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.7.0.tgz#a04f68dab86d5545fd0277d07ffc44e4078154c9" +webpack@^4.11.1: + version "4.11.1" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.11.1.tgz#1aa0b936f7ae93a52cf38d2ad0d0f46dcf3c2723" dependencies: + "@webassemblyjs/ast" "1.5.10" + "@webassemblyjs/helper-module-context" "1.5.10" + "@webassemblyjs/wasm-edit" "1.5.10" + "@webassemblyjs/wasm-opt" "1.5.10" + "@webassemblyjs/wasm-parser" "1.5.10" acorn "^5.0.0" acorn-dynamic-import "^3.0.0" ajv "^6.1.0" @@ -8857,6 +8233,7 @@ webpack@^4.7.0: chrome-trace-event "^0.1.1" enhanced-resolve "^4.0.0" eslint-scope "^3.7.1" + json-parse-better-errors "^1.0.2" loader-runner "^2.3.0" loader-utils "^1.1.0" memory-fs "~0.4.1" @@ -8892,7 +8269,7 @@ which-module@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" -which@^1.1.1, which@^1.2.1, which@^1.2.14, which@^1.2.9: +which@^1.1.1, which@^1.2.1, which@^1.2.9: version "1.3.0" resolved "https://registry.yarnpkg.com/which/-/which-1.3.0.tgz#ff04bdfc010ee547d780bec38e1ac1c2777d253a" dependencies: @@ -8951,14 +8328,6 @@ wrappy@1: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" -write-file-atomic@^1.2.0: - version "1.3.4" - resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-1.3.4.tgz#f807a4f0b1d9e913ae7a48112e6cc3af1991b45f" - dependencies: - graceful-fs "^4.1.11" - imurmurhash "^0.1.4" - slide "^1.1.5" - write-file-atomic@^2.0.0: version "2.3.0" resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-2.3.0.tgz#1ff61575c2e2a4e8e510d6fa4e243cce183999ab" @@ -9001,7 +8370,7 @@ xregexp@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/xregexp/-/xregexp-2.0.0.tgz#52a63e56ca0b84a7f3a5f3d61872f126ad7a5943" -xtend@^4.0.0, xtend@^4.0.1, xtend@~4.0.0, xtend@~4.0.1: +xtend@^4.0.0, xtend@^4.0.1, xtend@~4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af" @@ -9057,12 +8426,6 @@ yargs@^11.1.0: y18n "^3.2.1" yargs-parser "^9.0.2" -yargs@~1.2.6: - version "1.2.6" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-1.2.6.tgz#9c7b4a82fd5d595b2bf17ab6dcc43135432fe34b" - dependencies: - minimist "^0.1.0" - yargs@~3.10.0: version "3.10.0" resolved "https://registry.yarnpkg.com/yargs/-/yargs-3.10.0.tgz#f7ee7bd857dd7c1d2d38c0e74efbd681d1431fd1" @@ -9075,53 +8438,3 @@ yargs@~3.10.0: yeast@0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/yeast/-/yeast-0.1.2.tgz#008e06d8094320c372dbc2f8ed76a0ca6c8ac419" - -yeoman-environment@^2.0.0, yeoman-environment@^2.0.5: - version "2.1.1" - resolved "https://registry.yarnpkg.com/yeoman-environment/-/yeoman-environment-2.1.1.tgz#10a045f7fc4397873764882eae055a33e56ee1c5" - dependencies: - chalk "^2.1.0" - cross-spawn "^6.0.5" - debug "^3.1.0" - diff "^3.3.1" - escape-string-regexp "^1.0.2" - globby "^8.0.1" - grouped-queue "^0.3.3" - inquirer "^5.2.0" - is-scoped "^1.0.0" - lodash "^4.17.10" - log-symbols "^2.1.0" - mem-fs "^1.1.0" - strip-ansi "^4.0.0" - text-table "^0.2.0" - untildify "^3.0.2" - -yeoman-generator@^2.0.4: - version "2.0.5" - resolved "https://registry.yarnpkg.com/yeoman-generator/-/yeoman-generator-2.0.5.tgz#57b0b3474701293cc9ec965288f3400b00887c81" - dependencies: - async "^2.6.0" - chalk "^2.3.0" - cli-table "^0.3.1" - cross-spawn "^6.0.5" - dargs "^5.1.0" - dateformat "^3.0.3" - debug "^3.1.0" - detect-conflict "^1.0.0" - error "^7.0.2" - find-up "^2.1.0" - github-username "^4.0.0" - istextorbinary "^2.2.1" - lodash "^4.17.10" - make-dir "^1.1.0" - mem-fs-editor "^4.0.0" - minimist "^1.2.0" - pretty-bytes "^4.0.2" - read-chunk "^2.1.0" - read-pkg-up "^3.0.0" - rimraf "^2.6.2" - run-async "^2.0.0" - shelljs "^0.8.0" - text-table "^0.2.0" - through2 "^2.0.0" - yeoman-environment "^2.0.5" |