diff options
828 files changed, 10075 insertions, 3840 deletions
diff --git a/.gitignore b/.gitignore index 65f61e1fade..0696dd217af 100644 --- a/.gitignore +++ b/.gitignore @@ -80,3 +80,4 @@ eslint-report.html package-lock.json /junit_*.xml /coverage-frontend/ +jsdoc/ diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 45de5ce61c6..bca16f50245 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -427,15 +427,7 @@ setup-test-env: - vendor/gitaly-ruby # GitLab Review apps -.review-base: &review-base - <<: *dedicated-no-docs-no-db-pull-cache-job - image: registry.gitlab.com/gitlab-org/gitlab-build-images:gitlab-charts-build-base - stage: test - cache: {} - dependencies: [] - environment: &review-environment - name: review/${CI_COMMIT_REF_NAME} - url: https://gitlab-${CI_ENVIRONMENT_SLUG}.${REVIEW_APPS_DOMAIN} +.review-only: &review-only only: refs: - branches@gitlab-org/gitlab-ce @@ -445,6 +437,17 @@ setup-test-env: refs: - master - /(^docs[\/-].*|.*-docs$)/ + +.review-base: &review-base + <<: *dedicated-no-docs-no-db-pull-cache-job + <<: *review-only + image: registry.gitlab.com/gitlab-org/gitlab-build-images:gitlab-charts-build-base + stage: test + cache: {} + dependencies: [] + environment: &review-environment + name: review/${CI_COMMIT_REF_NAME} + url: https://gitlab-${CI_ENVIRONMENT_SLUG}.${REVIEW_APPS_DOMAIN} before_script: [] .review-docker: &review-docker @@ -499,6 +502,22 @@ rspec-mysql: <<: *rspec-metadata-mysql parallel: 50 +.rspec-quarantine: &rspec-quarantine + script: + - export CACHE_CLASSES=true + - scripts/gitaly-test-spawn + - bin/rspec --color --format documentation --tag quarantine spec/ + +rspec-pg-quarantine: + <<: *rspec-metadata-pg + <<: *rspec-quarantine + allow_failure: true + +rspec-mysql-quarantine: + <<: *rspec-metadata-mysql + <<: *rspec-quarantine + allow_failure: true + static-analysis: <<: *dedicated-no-docs-no-db-pull-cache-job dependencies: @@ -788,6 +807,7 @@ qa:selectors: - bundle exec bin/qa Test::Sanity::Selectors .qa-frontend-node: &qa-frontend-node + <<: *dedicated-no-docs-no-db-pull-cache-job stage: test variables: NODE_OPTIONS: --max_old_space_size=3584 @@ -802,11 +822,6 @@ qa:selectors: - yarn install --frozen-lockfile --cache-folder .yarn-cache - date - yarn run webpack-prod - <<: *except-docs - -qa-frontend-node:6: - <<: *qa-frontend-node - image: node:6-alpine qa-frontend-node:8: <<: *qa-frontend-node @@ -854,6 +869,21 @@ lint:javascript:report: paths: - eslint-report.html +jsdoc: + <<: *dedicated-no-docs-pull-cache-job + stage: post-test + dependencies: + - compile-assets + before_script: [] + script: + - date + - yarn run jsdoc || true # ignore exit code + artifacts: + name: jsdoc + expire_in: 31d + paths: + - jsdoc/ + pages: <<: *dedicated-no-docs-no-db-pull-cache-job before_script: [] @@ -863,6 +893,7 @@ pages: - karma - gitlab:assets:compile - lint:javascript:report + - jsdoc script: - mv public/ .public/ - mkdir public/ @@ -872,6 +903,7 @@ pages: - mv webpack-report/ public/webpack-report/ || true - cp .public/assets/application-*.css public/application.css || true - cp .public/assets/application-*.css.gz public/application.css.gz || true + - mv jsdoc/ public/jsdoc/ || true artifacts: paths: - public @@ -918,6 +950,21 @@ no_ee_check: - //@gitlab-org/gitlab-ce # GitLab Review apps +review-build-cng: + <<: *single-script-job + <<: *review-only + variables: + <<: *single-script-job-variables + SCRIPT_NAME: trigger-build + script: + - gem install gitlab --no-document + - apk add --update openssl curl jq + - wget $CI_PROJECT_URL/raw/$CI_COMMIT_SHA/scripts/review_apps/review-apps.sh + - chmod 755 review-apps.sh + - source ./review-apps.sh + - wait_for_job_to_be_done "gitlab:assets:compile" + - BUILD_TRIGGER_TOKEN=$REVIEW_APPS_BUILD_TRIGGER_TOKEN ./$SCRIPT_NAME cng + review-deploy: <<: *review-base retry: 2 @@ -932,15 +979,14 @@ review-deploy: <<: *review-environment on_stop: review-stop before_script: - - apk update && apk add jq - - gem install gitlab --no-document - script: - export GITLAB_SHELL_VERSION=$(<GITLAB_SHELL_VERSION) - export GITALY_VERSION=$(<GITALY_SERVER_VERSION) - export GITLAB_WORKHORSE_VERSION=$(<GITLAB_WORKHORSE_VERSION) + - apk update && apk add jq + - gem install gitlab --no-document - source ./scripts/review_apps/review-apps.sh - - wait_for_job_to_be_done "gitlab:assets:compile" - - BUILD_TRIGGER_TOKEN=$REVIEW_APPS_BUILD_TRIGGER_TOKEN ./scripts/trigger-build cng + script: + - wait_for_job_to_be_done "review-build-cng" - check_kube_domain - download_gitlab_chart - ensure_namespace @@ -951,7 +997,6 @@ review-deploy: .review-qa-base: &review-qa-base <<: *review-docker - retry: 2 allow_failure: true variables: <<: *review-docker-variables diff --git a/.gitlab/issue_templates/Add style proposal b/.gitlab/issue_templates/Add style proposal.md index 1a3be44bea0..1a3be44bea0 100644 --- a/.gitlab/issue_templates/Add style proposal +++ b/.gitlab/issue_templates/Add style proposal.md diff --git a/.gitlab/issue_templates/Feature proposal.md b/.gitlab/issue_templates/Feature proposal.md index 639a236631d..4d4d3bfda15 100644 --- a/.gitlab/issue_templates/Feature proposal.md +++ b/.gitlab/issue_templates/Feature proposal.md @@ -4,7 +4,7 @@ ### Target audience -<!--- For whom are we doing this? Include either a persona from https://design.gitlab.com/getting-started/personas or define a specific company role. e.a. "Release Manager" or "Security Analyst" --> +<!--- For whom are we doing this? Include either a persona from https://design.gitlab.com/getting-started/personas or define a specific company role. e.a. "Release Manager" or "Security Analyst". Use the persona labels as well https://gitlab.com/groups/gitlab-org/-/labels?utf8=%E2%9C%93&subscribed=&search=persona%3A --> ### Further details @@ -12,12 +12,12 @@ ### Proposal -<!--- How are we going to solve the problem? --> +<!--- How are we going to solve the problem? Try to include the user journey! --> ### 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 --> +<!--- Define both the success metrics and acceptance criteria. Note thet success metrics indicate the desired business outcomes, while acceptance criteria indicate when the solution is working correctly. If there is no way to measure success, link to an issue that will implement a way to measure this --> ### Links / references -/label ~"feature proposal" +/label ~feature diff --git a/.gitlab/issue_templates/Security Release.md b/.gitlab/issue_templates/Security Release.md new file mode 100644 index 00000000000..1734e915ad2 --- /dev/null +++ b/.gitlab/issue_templates/Security Release.md @@ -0,0 +1,69 @@ +<!-- +# Read me first! + +Set the title to: `Security Release: 11.4.X, 11.3.X, and 11.2.X` +--> + +## Releases tasks + +- https://gitlab.com/gitlab-org/release/docs/blob/master/general/security/release-manager.md +- https://gitlab.com/gitlab-org/release/docs/blob/master/general/security/developer.md +- https://gitlab.com/gitlab-org/release/docs/blob/master/general/security/security-engineer.md + +## Version issues: + +* 11.4.X: {release task link} +* 11.3.X: {release task link} +* 11.2.X: {release task link} + +## Security Issues: + +### CE + +* {https://gitlab.com/gitlab-org/gitlab-ce/issues link} + +### EE + +* {https://gitlab.com/gitlab-org/gitlab-ee/issues link} + +## Security Issues in dev.gitlab.org: + +### CE + +- {https://dev.gitlab.org/gitlab/gitlabhq/issues link} + +| Version | MR | Status| +|---------|----|-------| +| 11.4 | {https://dev.gitlab.org/gitlab/gitlabhq/merge_requests/ link} | | +| 11.3 | {https://dev.gitlab.org/gitlab/gitlabhq/merge_requests/ link} | | +| 11.2 | {https://dev.gitlab.org/gitlab/gitlabhq/merge_requests/ link} | | +| master | {https://dev.gitlab.org/gitlab/gitlabhq/merge_requests/ link} | | + + + +### EE + +* {https://dev.gitlab.org/gitlab/gitlabhq/issues/ link} + + +| Version | MR | Status| +|---------|----|-------| +| 11.4| {https://dev.gitlab.org/gitlab/gitlab-ee/merge_requests/ link} | | +| 11.3 | {https://dev.gitlab.org/gitlab/gitlab-ee/merge_requests/ link} | | +| 11.2 | {https://dev.gitlab.org/gitlab/gitlab-ee/merge_requests/ link} | | +| master | {https://dev.gitlab.org/gitlab/gitlab-ee/merge_requests/ link} | | + + +## QA +{QA issue link} + +## Blog post + +Dev: {https://dev.gitlab.org/gitlab/www-gitlab-com/merge_requests/ link}<br/> +gitlab.com: {https://gitlab.com/gitlab-com/www-gitlab-com/merge_requests/ link} + +## Email notification +{https://gitlab.com/gitlab-com/marketing/general/issues/ link} + +/label ~security +/confidential diff --git a/.rubocop.yml b/.rubocop.yml index e8e550fdbde..bcff67ded8c 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -143,6 +143,7 @@ Naming/FileName: - XMPP - XSRF - XSS + - GRPC # GitLab ################################################################### diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 847a0f74aa2..f2ba9fdb174 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -686,17 +686,6 @@ Style/TrailingUnderscoreVariable: - 'spec/lib/gitlab/etag_caching/middleware_spec.rb' - 'spec/services/quick_actions/interpret_service_spec.rb' -# Offense count: 5 -# Cop supports --auto-correct. -# Configuration parameters: ExactNameMatch, AllowPredicates, AllowDSLWriters, IgnoreClassMethods, Whitelist. -# Whitelist: to_ary, to_a, to_c, to_enum, to_h, to_hash, to_i, to_int, to_io, to_open, to_path, to_proc, to_r, to_regexp, to_str, to_s, to_sym -Style/TrivialAccessors: - Exclude: - - 'app/models/external_issue.rb' - - 'app/serializers/base_serializer.rb' - - 'lib/gitlab/auth/ldap/person.rb' - - 'lib/system_check/base_check.rb' - # Offense count: 4 # Cop supports --auto-correct. Style/UnlessElse: diff --git a/CHANGELOG.md b/CHANGELOG.md index e86c818298b..c1deab58d38 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,210 @@ documentation](doc/development/changelog.md) for instructions on adding your own entry. +## 11.7.0 (2019-01-22) + +### Security (14 changes, 1 of them is from the community) + +- Escape label and milestone titles to prevent XSS in GFM autocomplete. !2693 +- Bump Ruby on Rails to 5.0.7.1. !23396 (@blackst0ne) +- Delete confidential todos for user when downgraded to Guest. +- Project guests no longer are able to see refs page. +- Set URL rel attribute for broken URLs. +- Prevent leaking protected variables for ambiguous refs. +- Authorize before reading job information via API. +- Allow changing group CI/CD settings only for owners. +- Fix SSRF with import_url and remote mirror url. +- Don't expose cross project repositories through diffs when creating merge reqeusts. +- Validate bundle files before unpacking them. +- Issuable no longer is visible to users when project can't be viewed. +- Escape html entities in LabelReferenceFilter when no label found. +- Prevent private snippets from being embeddable. + +### Removed (3 changes, 1 of them is from the community) + +- Removes all instances of deprecated Gitlab Upgrader calls. !23603 (@jwolen) +- Removed discard draft comment button form notes. !24185 +- Remove migration to backfill project_repositories for legacy storage projects. !24299 + +### Fixed (42 changes, 7 of them are from the community) + +- Prevent awards emoji being updated when updating status. !23470 +- Allow merge after rebase without page refresh on FF repositories. !23572 +- Prevent admins from attempting hashed storage migration on read only DB. !23597 +- Correct the ordering of metrics on the performance dashboard. !23630 +- Display empty files properly on MR diffs. !23671 (Sean Nichols) +- Allow GitHub imports via token even if OAuth2 provider not configured. !23703 +- Update header navigation theme colors. !23734 (George Tsiolis) +- Fix login box bottom margins on signin page. !23739 (@gear54) +- Return an ApplicationSetting in CurrentSettings. !23766 +- Fix bug commenting on LFS images. !23812 +- Only prompt user once when navigating away from file editor. !23820 (Sam Bigelow) +- Display commit ID for discussions made on merge request commits. !23837 +- Stop autofocusing on diff comment after initial mount. !23849 +- Fix object storage not working properly with Google S3 compatibility. !23858 +- Fix project calendar feed when sorted by priority. !23870 +- Fix edit button disappearing in issue title. !23948 (Ruben Moya) +- Aligns build loader animation with the job log. !23959 +- Allow 'rake gitlab:cleanup:remote_upload_files' to read bucket files without having permissions to see all buckets. !23981 +- Correctly externalize pipeline tags. !24028 +- Fix error when creating labels in a new issue in the boards page. !24039 (Ruben Moya) +- Use 'parsePikadayDate' to parse due date string. !24045 +- Fix commit SHA not showing in merge request compare dropdown. !24084 +- Remove top margin in modal header titles. !24108 +- Drop Webhooks from project import/export config. !24121 +- Only validate project visibility when it has changed. !24142 +- Resolve About this feature link should open in new window. !24149 +- Add syntax highlighting to suggestion diff. !24156 +- Fix Bitbucket Server import only including first 25 pull requests. !24178 +- Enable caching for records which primary key is not `id`. !24245 +- Adjust applied suggestion reverting previous changes. !24250 +- Fix unexpected exception by failure of finding an actual head pipeline. !24257 +- Fix broken templated "Too many changes to show" text. !24282 +- Fix requests profiler in admin page not rendering HTML properly. !24291 +- Fix no avatar not showing in user selection box. !24346 +- Upgrade to gitaly 1.12.1. !24361 +- Fix runner eternal loop when update job result. !24481 +- Fix notification email for image diff notes. +- Fixed merge request diffs empty states. +- Fixed diff suggestions removing dashes. +- Don't hide CI dropdown behind diff summary. (gfyoung) +- Fix spacing on discussions. +- Fixes missing margin in releases block. + +### Changed (22 changes, 8 of them are from the community) + +- Show clusters of ancestors in cluster list page. !22996 +- Remove unnecessary line before reply holder. !23092 (George Tsiolis) +- Make the Pages permission setting more clear. !23146 +- Disable merging of labels with same names. !23265 +- Allow basic authentication on go get middleware. !23497 (Morty Choi @mortyccp) +- No longer require email subaddressing for issue creation by email. !23523 +- Adjust padding of .dropdown-title to comply with design specs. !23546 +- Make commit IDs in merge request discussion header monospace. !23562 +- Update environments breadcrumb. !23751 (George Tsiolis) +- Add date range in milestone change email notifications. !23762 +- Require Knative to be installed only on an RBAC kubernetes cluster. !23807 (Chris Baumbauer) +- Fix label and header styles in the job details sidebar. !23816 (Nathan Friend) +- Add % prefix to milestone reference links. !23928 +- Reorder sidebar menu item for group clusters. !24001 (George Tsiolis) +- Support CURD operation for Links as one of the Release assets. !24056 +- Upgrade Omniauth and JWT gems to switch away from Google+ API. !24068 +- Renames Milestone sort into Milestone due date. !24080 (Jacopo Beschi @jacopo-beschi) +- Discussion filter only displayed in discussions tab for merge requests. !24082 +- Make RBAC enabled default for new clusters. !24119 +- Hashed Storage: Only set as `read_only` when starting the per-project migration. !24128 +- Knative version bump 0.1.3 -> 0.2.2. (Chris Baumbauer) +- Show message on non-diff discussions. + +### Performance (7 changes) + +- Fix some N+1 queries related to Admin Dashboard, User Dashboards and Activity Stream. !23034 +- Add indexes to speed up CI query. !23188 +- Improve the loading time on merge request's discussion page by caching diff highlight. !23857 +- Cache avatar URLs and paths within a request. !23950 +- Improve snippet search performance by removing duplicate counts. !23952 +- Skip per-commit validations already evaluated. !23984 +- Fix timeout issues retrieving branches via API. !24034 + +### Added (29 changes, 6 of them are from the community) + +- Handle ci.skip push option. !15643 (Jonathon Reinhart) +- Add NGINX 0.16.0 and above metrics. !22133 +- Add project milestone link. !22552 +- Support tls communication in gitaly. !22602 +- Add option to make ci variables protected by default. !22744 (Alexis Reigel) +- Add project identifier as List-Id email Header to ease filtering. !22817 (Olivier Crête) +- Add markdown helper buttons to file editor. !23480 +- Allow to include templates in gitlab-ci.yml. !23495 +- Extend override check to also check arity. !23498 (Jacopo Beschi @jacopo-beschi) +- Add importing of issues from CSV file. !23532 +- Add submit feedback link to help dropdown. !23547 +- Send a notification email to project maintainers when a mirror update fails. !23595 +- Restore Object Pools when restoring an object pool. !23682 +- Creates component for release block. !23697 +- Configure Auto DevOps deployed applications with secrets from prefixed CI variables. !23719 +- Add name, author_id, and sha to releases table. !23763 +- Display a list of Sentry Issues in GitLab. !23770 +- Releases API. !23795 +- Creates frontend app for releases. !23796 +- Add new pipeline variable CI_COMMIT_SHORT_SHA. !23822 +- Create system notes on issue / MR creation when labels, milestone, or due date is set. !23859 +- Adds API documentation for releases. !23901 +- Add API Support for Kubernetes integration. !23922 +- Expose CI/CD predefined variable `CI_API_V4_URL`. !23936 +- Add Knative metrics to Prometheus. !23972 (Chris Baumbauer) +- Use reports syntax for Dependency scanning in Auto DevOps. !24081 +- Allow to include files from another projects in gitlab-ci.yml. !24101 +- User Popovers for Commit Infos, Member Lists and Snippets. !24132 +- Add no-color theme for syntax highlighting. (khm) + +### Other (45 changes, 30 of them are from the community) + +- Redesign project lists UI. !22682 +- [Rails5.1] Update functional specs to use new keyword format. !23095 (@blackst0ne) +- Update a condition to visibility a merge request collaboration message. !23104 (Harry Kiselev) +- Remove framework/mobile.scss. !23301 (Takuya Noguchi) +- Passing the separator argument as a positional parameter is deprecated. !23334 (Jasper Maes) +- Clarifies docs about CI `allow_failure`. !23367 (C.J. Jameson) +- Refactor issuable sidebar to use serializer. !23379 +- Refactor the logic of updating head pipelines for merge requests. !23502 +- Allow user to add Kubernetes cluster for clusterable when there are ancestor clusters. !23569 +- Adds explanatory text to input fields on user profile settings page. !23673 +- Externalize strings from `/app/views/shared/notes`. !23696 (Tao Wang) +- Remove rails 4 support in CI, Gemfiles, bin/ and config/. !23717 (Jasper Maes) +- Fix calendar events fetching error on private profile page. !23718 (Harry Kiselev) +- Update GitLab Workhorse to v8.0.0. !23740 +- Hide confidential events in the API. !23746 +- Changed Userpopover Fixtures and shadow color. !23768 +- Fix deprecation: Passing conditions to delete_all is deprecated. !23817 (Jasper Maes) +- Fix deprecation: Passing ActiveRecord::Base objects to sanitize_sql_hash_for_assignment. !23818 (Jasper Maes) +- Remove rails4 specific code. !23847 (Jasper Maes) +- Remove deprecated ActionDispatch::ParamsParser. !23848 (Jasper Maes) +- Fix deprecation: Comparing equality between ActionController::Parameters and a Hash is deprecated. !23855 (Jasper Maes) +- Fix deprecation: Directly inheriting from ActiveRecord::Migration is deprecated. !23884 (Jasper Maes) +- Fix deprecation: alias_method_chain is deprecated. Please, use Module#prepend instead. !23887 (Jasper Maes) +- Update specs to exclude possible false positive pass. !23893 (@blackst0ne) +- Passing an argument to force an association to reload is now deprecated. !23894 (Jasper Maes) +- ActiveRecord::Migration -> ActiveRecord::Migration[5.0]. !23910 (Jasper Maes) +- Split bio into individual line in extended user tooltips. !23940 +- Fix deprecation: redirect_to :back is deprecated. !23943 (Jasper Maes) +- Fix deprecation: insert_sql is deprecated and will be removed. !23944 (Jasper Maes) +- Upgrade @gitlab/ui to 1.16.2. !23946 +- convert specs in javascripts/ and support/ to new syntax. !23947 (Jasper Maes) +- Remove deprecated xhr from specs. !23949 (Jasper Maes) +- Remove app/views/shared/issuable/_filter.html.haml. !24008 (Takuya Noguchi) +- Fix deprecation: Using positional arguments in integration tests. !24009 (Jasper Maes) +- UI improvements for redesigned project lists. !24011 +- Update cert-manager chart from v0.5.0 to v0.5.2. !24025 (Takuya Noguchi) +- Hide spinner on empty activites list on user profile overview. !24063 +- Don't show Auto DevOps enabled banner for projects with CI file or CI disabled. !24067 +- Update GitLab Runner Helm Chart to 0.1.43. !24083 +- Fix navigation style in docs. !24090 (Takuya Noguchi) +- Remove gem install bundler from Docker-based Ruby environments. !24093 (Takuya Noguchi) +- Fix deprecation: Using positional arguments in integration tests. !24110 (Jasper Maes) +- Fix deprecation: returning false in Active Record and Active Model callbacks will not implicitly halt a callback chain. !24134 (Jasper Maes) +- ActiveRecord::Migration -> ActiveRecord::Migration[5.0] for AddIndexesToCiBuildsAndPipelines. !24167 (Jasper Maes) +- Update url placeholder for the sentry configuration page. !24338 + + +## 11.6.5 (2019-01-17) + +### Fixed (5 changes) + +- Add syntax highlighting to suggestion diff. !24156 +- Fix broken templated "Too many changes to show" text. !24282 +- Fix requests profiler in admin page not rendering HTML properly. !24291 +- Fix no avatar not showing in user selection box. !24346 +- Fixed diff suggestions removing dashes. + + +## 11.6.4 (2019-01-15) + +### Security (1 change) + +- Validate bundle files before unpacking them. + + ## 11.6.3 (2019-01-04) ### Fixed (1 change) diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index 0eed1a29efd..cd99d386a8d 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -1.12.0 +1.14.0
\ No newline at end of file diff --git a/GITLAB_PAGES_VERSION b/GITLAB_PAGES_VERSION index 3a3cd8cc8b0..88c5fb891dc 100644 --- a/GITLAB_PAGES_VERSION +++ b/GITLAB_PAGES_VERSION @@ -1 +1 @@ -1.3.1 +1.4.0 diff --git a/GITLAB_WORKHORSE_VERSION b/GITLAB_WORKHORSE_VERSION index ae9a76b9249..da156181014 100644 --- a/GITLAB_WORKHORSE_VERSION +++ b/GITLAB_WORKHORSE_VERSION @@ -1 +1 @@ -8.0.0 +8.1.0
\ No newline at end of file @@ -125,9 +125,9 @@ gem 'wikicloth', '0.8.1' gem 'asciidoctor', '~> 1.5.8' gem 'asciidoctor-plantuml', '0.0.8' gem 'rouge', '~> 3.1' -gem 'truncato', '~> 0.7.9' +gem 'truncato', '~> 0.7.11' gem 'bootstrap_form', '~> 2.7.0' -gem 'nokogiri', '~> 1.8.5' +gem 'nokogiri', '~> 1.10.1' gem 'escape_utils', '~> 1.1' # Calendar rendering @@ -160,12 +160,12 @@ gem 'acts-as-taggable-on', '~> 5.0' # Background jobs gem 'sidekiq', '~> 5.2.1' -gem 'sidekiq-cron', '~> 0.6.0' +gem 'sidekiq-cron', '~> 1.0' gem 'redis-namespace', '~> 1.6.0' gem 'gitlab-sidekiq-fetcher', '~> 0.4.0', require: 'sidekiq-reliable-fetch' # Cron Parser -gem 'rufus-scheduler', '~> 3.4' +gem 'fugit', '~> 1.1' # HTTP requests gem 'httparty', '~> 0.13.3' @@ -304,6 +304,12 @@ group :metrics do gem 'raindrops', '~> 0.18' end +group :tracing do + # OpenTracing + gem 'opentracing', '~> 0.4.3' + gem 'jaeger-client', '~> 0.10.0' +end + group :development do gem 'foreman', '~> 0.84.0' gem 'brakeman', '~> 4.2', require: false diff --git a/Gemfile.lock b/Gemfile.lock index b4602dbbf36..419a6831924 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -185,7 +185,7 @@ GEM erubi (1.7.1) erubis (2.7.0) escape_utils (1.2.1) - et-orbi (1.0.3) + et-orbi (1.1.7) tzinfo eventmachine (1.2.7) excon (0.62.0) @@ -206,7 +206,7 @@ GEM fast_blank (1.0.0) fast_gettext (1.6.0) ffaker (2.10.0) - ffi (1.9.25) + ffi (1.10.0) flipper (0.13.0) flipper-active_record (0.13.0) activerecord (>= 3.2, < 6) @@ -258,6 +258,9 @@ GEM foreman (0.84.0) thor (~> 0.19.1) formatador (0.2.5) + fugit (1.1.7) + et-orbi (~> 1.1, >= 1.1.7) + raabro (~> 1.1) fuubar (2.2.0) rspec-core (~> 3.0) ruby-progressbar (~> 1.4) @@ -282,7 +285,7 @@ GEM gitlab-markup (1.6.5) gitlab-sidekiq-fetcher (0.4.0) sidekiq (~> 5) - gitlab-styles (2.4.1) + gitlab-styles (2.5.1) rubocop (~> 0.54.0) rubocop-gitlab-security (~> 0.1.0) rubocop-rspec (~> 1.19) @@ -389,6 +392,9 @@ GEM cause json ipaddress (0.8.3) + jaeger-client (0.10.0) + opentracing (~> 0.3) + thrift jira-ruby (1.4.1) activesupport multipart-post @@ -462,9 +468,9 @@ GEM mimemagic (0.3.2) mini_magick (4.8.0) mini_mime (1.0.1) - mini_portile2 (2.3.0) + mini_portile2 (2.4.0) minitest (5.11.3) - msgpack (1.2.4) + msgpack (1.2.6) multi_json (1.13.1) multi_xml (0.6.0) multipart-post (2.0.0) @@ -477,8 +483,8 @@ GEM net-ssh (5.0.1) netrc (0.11.0) nio4r (2.3.1) - nokogiri (1.8.5) - mini_portile2 (~> 2.3.0) + nokogiri (1.10.1) + mini_portile2 (~> 2.4.0) nokogumbo (1.5.0) nokogiri numerizer (0.1.1) @@ -544,6 +550,8 @@ GEM activesupport nokogiri (>= 1.4.4) omniauth (~> 1.0) + opentracing (0.4.3) + optimist (3.0.0) org-ruby (0.9.12) rubypants (~> 0.2) orm_adapter (0.5.0) @@ -606,6 +614,7 @@ GEM get_process_mem (~> 0.2) puma (>= 2.7, < 4) pyu-ruby-sasl (0.0.3.3) + raabro (1.1.6) rack (2.0.6) rack-accept (0.4.5) rack (>= 0.4) @@ -664,10 +673,10 @@ GEM ffi (>= 0.5.0, < 2) rblineprof (0.3.6) debugger-ruby_core_source (~> 1.3) - rbtrace (0.4.10) + rbtrace (0.4.11) ffi (>= 1.0.6) msgpack (>= 0.4.3) - trollop (>= 1.16.2) + optimist (>= 3.0.0) rdoc (6.0.4) re2 (1.1.1) recaptcha (3.0.0) @@ -775,8 +784,6 @@ GEM rubyntlm (0.6.2) rubypants (0.2.0) rubyzip (1.2.2) - rufus-scheduler (3.4.0) - et-orbi (~> 1.0) rugged (0.27.5) safe_yaml (1.0.4) sanitize (4.6.6) @@ -820,8 +827,8 @@ GEM connection_pool (~> 2.2, >= 2.2.2) rack-protection (>= 1.5.0) redis (>= 3.3.5, < 5) - sidekiq-cron (0.6.0) - rufus-scheduler (>= 3.3.0) + sidekiq-cron (1.0.4) + fugit (~> 1.1) sidekiq (>= 4.2.1) signet (0.11.0) addressable (~> 2.3) @@ -868,6 +875,7 @@ GEM rack (>= 1, < 3) thor (0.19.4) thread_safe (0.3.6) + thrift (0.11.0.0) tilt (2.0.8) timecop (0.8.1) timfel-krb5-auth (0.8.3) @@ -875,10 +883,9 @@ GEM parslet (~> 1.8.0) toml-rb (1.0.0) citrus (~> 3.0, > 3.0) - trollop (2.1.3) - truncato (0.7.10) + truncato (0.7.11) htmlentities (~> 4.3.1) - nokogiri (~> 1.8.0, >= 1.7.0) + nokogiri (>= 1.7.0, <= 2.0) tzinfo (1.2.5) thread_safe (~> 0.1) u2f (0.2.1) @@ -1003,6 +1010,7 @@ DEPENDENCIES fog-rackspace (~> 0.1.1) font-awesome-rails (~> 4.7) foreman (~> 0.84.0) + fugit (~> 1.1) fuubar (~> 2.2.0) gemojione (~> 3.3) gettext (~> 3.2.2) @@ -1037,6 +1045,7 @@ DEPENDENCIES httparty (~> 0.13.3) icalendar influxdb (~> 0.2) + jaeger-client (~> 0.10.0) jira-ruby (~> 1.4) jquery-atwho-rails (~> 1.3.2) js_regex (~> 2.2.1) @@ -1059,7 +1068,7 @@ DEPENDENCIES nakayoshi_fork (~> 0.0.4) net-ldap net-ssh (~> 5.0) - nokogiri (~> 1.8.5) + nokogiri (~> 1.10.1) oauth2 (~> 1.4) octokit (~> 4.9) omniauth (~> 1.8) @@ -1077,6 +1086,7 @@ DEPENDENCIES omniauth-shibboleth (~> 1.3.0) omniauth-twitter (~> 1.4) omniauth_crowd (~> 2.2.0) + opentracing (~> 0.4.3) org-ruby (~> 0.9.12) peek (~> 1.0.1) peek-gc (~> 0.0.2) @@ -1127,7 +1137,6 @@ DEPENDENCIES ruby-prof (~> 0.17.0) ruby-progressbar ruby_parser (~> 3.8) - rufus-scheduler (~> 3.4) rugged (~> 0.27) sanitize (~> 4.6) sass (~> 3.5) @@ -1141,7 +1150,7 @@ DEPENDENCIES sham_rack (~> 1.3.6) shoulda-matchers (~> 3.1.2) sidekiq (~> 5.2.1) - sidekiq-cron (~> 0.6.0) + sidekiq-cron (~> 1.0) simple_po_parser (~> 1.1.2) simplecov (~> 0.14.0) slack-notifier (~> 1.5.1) @@ -1156,7 +1165,7 @@ DEPENDENCIES thin (~> 1.7.0) timecop (~> 0.8.0) toml-rb (~> 1.0.0) - truncato (~> 0.7.9) + truncato (~> 0.7.11) u2f (~> 0.2.1) uglifier (~> 2.7.2) unf (~> 0.1.4) diff --git a/PROCESS.md b/PROCESS.md index f2eed5544fc..7fdac098807 100644 --- a/PROCESS.md +++ b/PROCESS.md @@ -56,7 +56,7 @@ Below we describe the contributing process to GitLab for two reasons: Several people from the [GitLab team][team] are helping community members to get their contributions accepted by meeting our [Definition of done][done]. -What you can expect from them is described at https://about.gitlab.com/roles/merge-request-coach/. +What you can expect from them is described at https://about.gitlab.com/job-families/expert/merge-request-coach/. ### Milestones on community contribution issues @@ -1 +1 @@ -11.7.0-pre +11.8.0-pre diff --git a/app/assets/javascripts/badges/components/badge_list_row.vue b/app/assets/javascripts/badges/components/badge_list_row.vue index 9051be1e102..cad5611c8c5 100644 --- a/app/assets/javascripts/badges/components/badge_list_row.vue +++ b/app/assets/javascripts/badges/components/badge_list_row.vue @@ -55,7 +55,7 @@ export default { :disabled="badge.isDeleting" class="btn btn-default append-right-8" type="button" - @click="editBadge(badge);" + @click="editBadge(badge)" > <icon :size="16" :aria-label="__('Edit')" name="pencil" /> </button> @@ -65,7 +65,7 @@ export default { type="button" data-toggle="modal" data-target="#delete-badge-modal" - @click="updateBadgeInModal(badge);" + @click="updateBadgeInModal(badge)" > <icon :size="16" :aria-label="__('Delete')" name="remove" /> </button> diff --git a/app/assets/javascripts/boards/components/board_card.vue b/app/assets/javascripts/boards/components/board_card.vue index 30fbdb9e97f..f569322ab70 100644 --- a/app/assets/javascripts/boards/components/board_card.vue +++ b/app/assets/javascripts/boards/components/board_card.vue @@ -86,7 +86,7 @@ export default { class="board-card" @mousedown="mouseDown" @mousemove="mouseMove" - @mouseup="showIssue($event);" + @mouseup="showIssue($event)" > <issue-card-inner :list="list" diff --git a/app/assets/javascripts/boards/components/board_list.vue b/app/assets/javascripts/boards/components/board_list.vue index f3f341ece5c..a689dfc3768 100644 --- a/app/assets/javascripts/boards/components/board_list.vue +++ b/app/assets/javascripts/boards/components/board_list.vue @@ -221,7 +221,7 @@ export default { </script> <template> - <div class="board-list-component"> + <div class="board-list-component d-flex flex-column"> <div v-if="loading" class="board-list-loading text-center" aria-label="Loading issues"> <gl-loading-icon /> </div> diff --git a/app/assets/javascripts/boards/components/board_new_issue.vue b/app/assets/javascripts/boards/components/board_new_issue.vue index 93bcb4e129e..28d96dab605 100644 --- a/app/assets/javascripts/boards/components/board_new_issue.vue +++ b/app/assets/javascripts/boards/components/board_new_issue.vue @@ -96,7 +96,7 @@ export default { <template> <div class="board-new-issue-form"> <div class="board-card"> - <form @submit="submit($event);"> + <form @submit="submit($event)"> <div v-if="error" class="flash-container"> <div class="flash-alert">An error occurred. Please try again.</div> </div> diff --git a/app/assets/javascripts/boards/components/issue_card_inner.vue b/app/assets/javascripts/boards/components/issue_card_inner.vue index 0f581c3d37d..90ab3a76342 100644 --- a/app/assets/javascripts/boards/components/issue_card_inner.vue +++ b/app/assets/javascripts/boards/components/issue_card_inner.vue @@ -184,7 +184,7 @@ export default { :title="label.description" class="badge color-label append-right-4 prepend-top-4" type="button" - @click="filterByLabel(label);" + @click="filterByLabel(label)" > {{ label.title }} </button> diff --git a/app/assets/javascripts/boards/components/modal/empty_state.vue b/app/assets/javascripts/boards/components/modal/empty_state.vue index defd857b92c..2a0008467c4 100644 --- a/app/assets/javascripts/boards/components/modal/empty_state.vue +++ b/app/assets/javascripts/boards/components/modal/empty_state.vue @@ -58,7 +58,7 @@ export default { v-if="activeTab === 'selected'" class="btn btn-default" type="button" - @click="changeTab('all');" + @click="changeTab('all')" > Open issues </button> diff --git a/app/assets/javascripts/boards/components/modal/footer.vue b/app/assets/javascripts/boards/components/modal/footer.vue index b1bc7d87086..d4afd9d59da 100644 --- a/app/assets/javascripts/boards/components/modal/footer.vue +++ b/app/assets/javascripts/boards/components/modal/footer.vue @@ -71,7 +71,7 @@ export default { <span class="inline add-issues-footer-to-list"> to list </span> <lists-dropdown /> </div> - <button class="btn btn-default float-right" type="button" @click="toggleModal(false);"> + <button class="btn btn-default float-right" type="button" @click="toggleModal(false)"> Cancel </button> </footer> diff --git a/app/assets/javascripts/boards/components/modal/header.vue b/app/assets/javascripts/boards/components/modal/header.vue index d0e285a149e..1f0961e02d8 100644 --- a/app/assets/javascripts/boards/components/modal/header.vue +++ b/app/assets/javascripts/boards/components/modal/header.vue @@ -58,7 +58,7 @@ export default { class="close" data-dismiss="modal" aria-label="Close" - @click="toggleModal(false);" + @click="toggleModal(false)" > <span aria-hidden="true">×</span> </button> diff --git a/app/assets/javascripts/boards/components/modal/list.vue b/app/assets/javascripts/boards/components/modal/list.vue index 878bb002c6c..e9ed2de859d 100644 --- a/app/assets/javascripts/boards/components/modal/list.vue +++ b/app/assets/javascripts/boards/components/modal/list.vue @@ -130,7 +130,7 @@ export default { <div :class="{ 'is-active': issue.selected }" class="board-card" - @click="toggleIssue($event, issue);" + @click="toggleIssue($event, issue)" > <issue-card-inner :issue="issue" :issue-link-base="issueLinkBase" :root-path="rootPath" /> <icon diff --git a/app/assets/javascripts/boards/components/modal/lists_dropdown.vue b/app/assets/javascripts/boards/components/modal/lists_dropdown.vue index 820d0679df5..3fbe8fe1be7 100644 --- a/app/assets/javascripts/boards/components/modal/lists_dropdown.vue +++ b/app/assets/javascripts/boards/components/modal/lists_dropdown.vue @@ -38,7 +38,7 @@ export default { :class="{ 'is-active': list.id == selected.id }" href="#" role="button" - @click.prevent="modal.selectedList = list;" + @click.prevent="modal.selectedList = list" > <span :style="{ backgroundColor: list.label.color }" class="dropdown-label-box"> </span> {{ list.title }} diff --git a/app/assets/javascripts/boards/components/modal/tabs.vue b/app/assets/javascripts/boards/components/modal/tabs.vue index 7b800a6ab97..2d2920e312e 100644 --- a/app/assets/javascripts/boards/components/modal/tabs.vue +++ b/app/assets/javascripts/boards/components/modal/tabs.vue @@ -21,12 +21,12 @@ export default { <div class="top-area prepend-top-10 append-bottom-10"> <ul class="nav-links issues-state-filters"> <li :class="{ active: activeTab == 'all' }"> - <a href="#" role="button" @click.prevent="changeTab('all');"> + <a href="#" role="button" @click.prevent="changeTab('all')"> Open issues <span class="badge badge-pill"> {{ issuesCount }} </span> </a> </li> <li :class="{ active: activeTab == 'selected' }"> - <a href="#" role="button" @click.prevent="changeTab('selected');"> + <a href="#" role="button" @click.prevent="changeTab('selected')"> Selected issues <span class="badge badge-pill"> {{ selectedCount }} </span> </a> </li> diff --git a/app/assets/javascripts/boards/components/project_select.vue b/app/assets/javascripts/boards/components/project_select.vue index d899b7fbd8c..8274647744f 100644 --- a/app/assets/javascripts/boards/components/project_select.vue +++ b/app/assets/javascripts/boards/components/project_select.vue @@ -82,7 +82,7 @@ export default { <template> <div> <label class="label-bold prepend-top-10"> Project </label> - <div ref="projectsDropdown" class="dropdown"> + <div ref="projectsDropdown" class="dropdown dropdown-projects"> <button class="dropdown-menu-toggle wide" type="button" diff --git a/app/assets/javascripts/contextual_sidebar.js b/app/assets/javascripts/contextual_sidebar.js index 10f02739ec8..50efecb3475 100644 --- a/app/assets/javascripts/contextual_sidebar.js +++ b/app/assets/javascripts/contextual_sidebar.js @@ -13,6 +13,9 @@ export default class ContextualSidebar { initDomElements() { this.$page = $('.layout-page'); this.$sidebar = $('.nav-sidebar'); + + if (!this.$sidebar.length) return; + this.$innerScroll = $('.nav-sidebar-inner-scroll', this.$sidebar); this.$overlay = $('.mobile-overlay'); this.$openSidebar = $('.toggle-mobile-nav'); @@ -21,12 +24,14 @@ export default class ContextualSidebar { } bindEvents() { + if (!this.$sidebar.length) return; + document.addEventListener('click', e => { if ( !e.target.closest('.nav-sidebar') && (bp.getBreakpointSize() === 'sm' || bp.getBreakpointSize() === 'md') ) { - this.toggleCollapsedSidebar(true); + this.toggleCollapsedSidebar(true, true); } }); this.$openSidebar.on('click', () => this.toggleSidebarNav(true)); @@ -34,7 +39,7 @@ export default class ContextualSidebar { this.$overlay.on('click', () => this.toggleSidebarNav(false)); this.$sidebarToggle.on('click', () => { const value = !this.$sidebar.hasClass('sidebar-collapsed-desktop'); - this.toggleCollapsedSidebar(value); + this.toggleCollapsedSidebar(value, true); }); $(window).on('resize', () => _.debounce(this.render(), 100)); @@ -53,16 +58,19 @@ export default class ContextualSidebar { this.$sidebar.removeClass('sidebar-collapsed-desktop'); } - toggleCollapsedSidebar(collapsed) { + toggleCollapsedSidebar(collapsed, saveCookie) { const breakpoint = bp.getBreakpointSize(); if (this.$sidebar.length) { this.$sidebar.toggleClass('sidebar-collapsed-desktop', collapsed); this.$page.toggleClass('page-with-icon-sidebar', breakpoint === 'sm' ? true : collapsed); } - ContextualSidebar.setCollapsedCookie(collapsed); - this.toggleSidebarOverflow(); + if (saveCookie) { + ContextualSidebar.setCollapsedCookie(collapsed); + } + + requestIdleCallback(() => this.toggleSidebarOverflow()); } toggleSidebarOverflow() { @@ -74,13 +82,15 @@ export default class ContextualSidebar { } render() { + if (!this.$sidebar.length) return; + const breakpoint = bp.getBreakpointSize(); if (breakpoint === 'sm' || breakpoint === 'md') { - this.toggleCollapsedSidebar(true); + this.toggleCollapsedSidebar(true, false); } else if (breakpoint === 'lg') { const collapse = parseBoolean(Cookies.get('sidebar_collapsed')); - this.toggleCollapsedSidebar(collapse); + this.toggleCollapsedSidebar(collapse, false); } } } diff --git a/app/assets/javascripts/deploy_keys/components/key.vue b/app/assets/javascripts/deploy_keys/components/key.vue index f01e6f2a639..6ffb8c4e1c0 100644 --- a/app/assets/javascripts/deploy_keys/components/key.vue +++ b/app/assets/javascripts/deploy_keys/components/key.vue @@ -113,7 +113,7 @@ export default { <div class="gl-responsive-table-row deploy-key"> <div class="table-section section-40"> <div role="rowheader" class="table-mobile-header">{{ s__('DeployKeys|Deploy key') }}</div> - <div class="table-mobile-content"> + <div class="table-mobile-content qa-key"> <strong class="title qa-key-title"> {{ deployKey.title }} </strong> <div class="fingerprint qa-key-fingerprint">{{ deployKey.fingerprint }}</div> </div> diff --git a/app/assets/javascripts/diffs/components/compare_versions.vue b/app/assets/javascripts/diffs/components/compare_versions.vue index f75345d31f8..3770b5c8864 100644 --- a/app/assets/javascripts/diffs/components/compare_versions.vue +++ b/app/assets/javascripts/diffs/components/compare_versions.vue @@ -3,6 +3,7 @@ import { mapActions, mapGetters, mapState } from 'vuex'; import { GlTooltipDirective, GlLink, GlButton } from '@gitlab/ui'; import { __ } from '~/locale'; import { getParameterValues, mergeUrlParams } from '~/lib/utils/url_utility'; +import { polyfillSticky } from '~/lib/utils/sticky'; import Icon from '~/vue_shared/components/icon.vue'; import CompareVersionsDropdown from './compare_versions_dropdown.vue'; @@ -54,6 +55,18 @@ export default { showDropdowns() { return !this.commit && this.mergeRequestDiffs.length; }, + fileTreeIcon() { + return this.showTreeList ? 'collapse-left' : 'expand-left'; + }, + toggleFileBrowserTitle() { + return this.showTreeList ? __('Hide file browser') : __('Show file browser'); + }, + baseVersionPath() { + return this.mergeRequestDiff.base_version_path; + }, + }, + mounted() { + polyfillSticky(this.$el); }, methods: { ...mapActions('diffs', [ @@ -70,7 +83,7 @@ export default { </script> <template> - <div class="mr-version-controls"> + <div class="mr-version-controls" :class="{ 'is-fileTreeOpen': showTreeList }"> <div class="mr-version-menus-container content-block"> <button v-gl-tooltip.hover @@ -79,10 +92,10 @@ export default { :class="{ active: showTreeList, }" - :title="__('Toggle file browser')" + :title="toggleFileBrowserTitle" @click="toggleShowTreeList" > - <icon name="hamburger" /> + <icon :name="fileTreeIcon" /> </button> <div v-if="showDropdowns" class="d-flex align-items-center compare-versions-container"> Changes between @@ -95,6 +108,7 @@ export default { and <compare-versions-dropdown :other-versions="comparableDiffs" + :base-version-path="baseVersionPath" :start-version="startVersion" :target-branch="targetBranch" class="mr-version-compare-dropdown" @@ -104,7 +118,7 @@ export default { {{ __('Viewing commit') }} <gl-link :href="commit.commit_url" class="monospace">{{ commit.short_id }}</gl-link> </div> - <div class="inline-parallel-buttons d-none d-md-flex ml-auto"> + <div class="inline-parallel-buttons d-none d-lg-flex ml-auto"> <gl-button v-if="commit || startVersion" :href="latestVersionPath" diff --git a/app/assets/javascripts/diffs/components/compare_versions_dropdown.vue b/app/assets/javascripts/diffs/components/compare_versions_dropdown.vue index b9b1ee02697..80aec84f574 100644 --- a/app/assets/javascripts/diffs/components/compare_versions_dropdown.vue +++ b/app/assets/javascripts/diffs/components/compare_versions_dropdown.vue @@ -34,14 +34,13 @@ export default { required: false, default: false, }, + baseVersionPath: { + type: String, + required: false, + default: null, + }, }, computed: { - baseVersion() { - return { - name: 'hii', - versionIndex: -1, - }; - }, targetVersions() { if (this.mergeRequestVersion) { return this.otherVersions; @@ -62,6 +61,9 @@ export default { ); }, href(version) { + if (this.isBase(version)) { + return this.baseVersionPath; + } if (this.showCommitCount) { return version.version_path; } @@ -139,7 +141,7 @@ export default { <time-ago v-if="version.created_at" :time="version.created_at" - class="js-timeago js-timeago-render" + class="js-timeago" /> </small> </div> diff --git a/app/assets/javascripts/diffs/components/diff_content.vue b/app/assets/javascripts/diffs/components/diff_content.vue index ba6dcd63880..6dc2f5d3f68 100644 --- a/app/assets/javascripts/diffs/components/diff_content.vue +++ b/app/assets/javascripts/diffs/components/diff_content.vue @@ -127,7 +127,7 @@ export default { :save-button-title="__('Comment')" class="diff-comment-form new-note discussion-form discussion-form-container" @handleFormUpdate="handleSaveNote" - @cancelForm="closeDiffFileCommentForm(diffFile.file_hash);" + @cancelForm="closeDiffFileCommentForm(diffFile.file_hash)" /> </div> </diff-viewer> diff --git a/app/assets/javascripts/diffs/components/diff_discussions.vue b/app/assets/javascripts/diffs/components/diff_discussions.vue index b2021cd6061..4c73eea4049 100644 --- a/app/assets/javascripts/diffs/components/diff_discussions.vue +++ b/app/assets/javascripts/diffs/components/diff_discussions.vue @@ -68,7 +68,7 @@ export default { }" type="button" class="js-diff-notes-toggle" - @click="toggleDiscussion({ discussionId: discussion.id });" + @click="toggleDiscussion({ discussionId: discussion.id })" > <icon v-if="discussion.expanded" name="collapse" class="collapse-icon" /> <template v-else> diff --git a/app/assets/javascripts/diffs/components/diff_file_header.vue b/app/assets/javascripts/diffs/components/diff_file_header.vue index f75a01b023b..b58f704bebb 100644 --- a/app/assets/javascripts/diffs/components/diff_file_header.vue +++ b/app/assets/javascripts/diffs/components/diff_file_header.vue @@ -145,7 +145,7 @@ export default { <div ref="header" class="js-file-title file-title file-title-flex-parent" - @click="handleToggleFile($event, true);" + @click="handleToggleFile($event, true)" > <div class="file-header-content"> <icon diff --git a/app/assets/javascripts/diffs/components/diff_line_gutter_content.vue b/app/assets/javascripts/diffs/components/diff_line_gutter_content.vue index c0613d80d37..6709df48637 100644 --- a/app/assets/javascripts/diffs/components/diff_line_gutter_content.vue +++ b/app/assets/javascripts/diffs/components/diff_line_gutter_content.vue @@ -179,7 +179,7 @@ export default { v-if="lineNumber" :data-linenumber="lineNumber" :href="lineHref" - @click="setHighlightedRow(lineCode);" + @click="setHighlightedRow(lineCode)" > </a> <diff-gutter-avatars v-if="shouldShowAvatarsOnGutter" :discussions="line.discussions" /> diff --git a/app/assets/javascripts/diffs/components/diff_line_note_form.vue b/app/assets/javascripts/diffs/components/diff_line_note_form.vue index e7569ba7b84..18edbe286ba 100644 --- a/app/assets/javascripts/diffs/components/diff_line_note_form.vue +++ b/app/assets/javascripts/diffs/components/diff_line_note_form.vue @@ -28,6 +28,11 @@ export default { type: Object, required: true, }, + helpPagePath: { + type: String, + required: false, + default: '', + }, }, computed: { ...mapState({ @@ -95,6 +100,7 @@ export default { :is-editing="true" :line-code="line.line_code" :line="line" + :help-page-path="helpPagePath" save-button-title="Comment" class="diff-comment-form" @cancelForm="handleCancelCommentForm" diff --git a/app/assets/javascripts/diffs/components/image_diff_overlay.vue b/app/assets/javascripts/diffs/components/image_diff_overlay.vue index d30e64312aa..4a83c5a72a5 100644 --- a/app/assets/javascripts/diffs/components/image_diff_overlay.vue +++ b/app/assets/javascripts/diffs/components/image_diff_overlay.vue @@ -97,7 +97,7 @@ export default { v-if="canComment" type="button" class="btn-transparent position-absolute image-diff-overlay-add-comment w-100 h-100 js-add-image-diff-note-button" - @click="clickedImage($event.offsetX, $event.offsetY);" + @click="clickedImage($event.offsetX, $event.offsetY)" > <span class="sr-only"> {{ __('Add image comment') }} </span> </button> @@ -109,7 +109,7 @@ export default { :disabled="!shouldToggleDiscussion" class="js-image-badge" type="button" - @click="toggleDiscussion({ discussionId: discussion.id });" + @click="toggleDiscussion({ discussionId: discussion.id })" > <icon v-if="showCommentIcon" name="image-comment-dark" /> <template v-else> diff --git a/app/assets/javascripts/diffs/components/inline_diff_comment_row.vue b/app/assets/javascripts/diffs/components/inline_diff_comment_row.vue index 814ee0b7c02..69146f1f6fd 100644 --- a/app/assets/javascripts/diffs/components/inline_diff_comment_row.vue +++ b/app/assets/javascripts/diffs/components/inline_diff_comment_row.vue @@ -54,6 +54,7 @@ export default { :diff-file-hash="diffFileHash" :line="line" :note-target-line="line" + :help-page-path="helpPagePath" /> </div> </td> diff --git a/app/assets/javascripts/diffs/components/parallel_diff_comment_row.vue b/app/assets/javascripts/diffs/components/parallel_diff_comment_row.vue index a65cf025cde..370cb6e339a 100644 --- a/app/assets/javascripts/diffs/components/parallel_diff_comment_row.vue +++ b/app/assets/javascripts/diffs/components/parallel_diff_comment_row.vue @@ -101,6 +101,7 @@ export default { :diff-file-hash="diffFileHash" :line="line.left" :note-target-line="line.left" + :help-page-path="helpPagePath" line-position="left" /> </td> diff --git a/app/assets/javascripts/diffs/components/tree_list.vue b/app/assets/javascripts/diffs/components/tree_list.vue index eb8f274aff3..097587c5ac1 100644 --- a/app/assets/javascripts/diffs/components/tree_list.vue +++ b/app/assets/javascripts/diffs/components/tree_list.vue @@ -81,7 +81,7 @@ export default { :placeholder="s__('MergeRequest|Filter files')" type="search" class="form-control" - @focus="toggleFocusSearch(true);" + @focus="toggleFocusSearch(true)" @blur="blurSearch" /> <button @@ -104,7 +104,7 @@ export default { }" class="btn btn-default pt-0 pb-0 d-flex align-items-center" type="button" - @click="toggleRenderTreeList(false);" + @click="toggleRenderTreeList(false)" > <icon name="hamburger" /> </button> @@ -117,7 +117,7 @@ export default { }" class="btn btn-default pt-0 pb-0 d-flex align-items-center" type="button" - @click="toggleRenderTreeList(true);" + @click="toggleRenderTreeList(true)" > <icon name="file-tree" /> </button> diff --git a/app/assets/javascripts/diffs/constants.js b/app/assets/javascripts/diffs/constants.js index 78a39baa4cb..0af1ba13d36 100644 --- a/app/assets/javascripts/diffs/constants.js +++ b/app/assets/javascripts/diffs/constants.js @@ -32,3 +32,5 @@ export const LINES_TO_BE_RENDERED_DIRECTLY = 100; export const MAX_LINES_TO_BE_RENDERED = 2000; export const MR_TREE_SHOW_KEY = 'mr_tree_show'; + +export const TREE_TYPE = 'tree'; diff --git a/app/assets/javascripts/diffs/store/actions.js b/app/assets/javascripts/diffs/store/actions.js index 00a4bb6d3a3..196c9dfb1c2 100644 --- a/app/assets/javascripts/diffs/store/actions.js +++ b/app/assets/javascripts/diffs/store/actions.js @@ -5,6 +5,7 @@ import createFlash from '~/flash'; import { s__ } from '~/locale'; import { handleLocationHash, historyPushState, scrollToElement } from '~/lib/utils/common_utils'; import { mergeUrlParams, getLocationHash } from '~/lib/utils/url_utility'; +import TreeWorker from '../workers/tree_worker'; import eventHub from '../../notes/event_hub'; import { getDiffPositionByLineCode, getNoteFormData } from './utils'; import * as types from './mutation_types'; @@ -21,17 +22,29 @@ export const setBaseConfig = ({ commit }, options) => { }; export const fetchDiffFiles = ({ state, commit }) => { + const worker = new TreeWorker(); + commit(types.SET_LOADING, true); + worker.addEventListener('message', ({ data }) => { + commit(types.SET_TREE_DATA, data); + + worker.terminate(); + }); + return axios .get(state.endpoint) .then(res => { commit(types.SET_LOADING, false); commit(types.SET_MERGE_REQUEST_DIFFS, res.data.merge_request_diffs || []); commit(types.SET_DIFF_DATA, res.data); + + worker.postMessage(state.diffFiles); + return Vue.nextTick(); }) - .then(handleLocationHash); + .then(handleLocationHash) + .catch(() => worker.terminate()); }; export const setHighlightedRow = ({ commit }, lineCode) => { diff --git a/app/assets/javascripts/diffs/store/mutation_types.js b/app/assets/javascripts/diffs/store/mutation_types.js index 0338cde3658..6ed8c5709a8 100644 --- a/app/assets/javascripts/diffs/store/mutation_types.js +++ b/app/assets/javascripts/diffs/store/mutation_types.js @@ -18,3 +18,5 @@ export const OPEN_DIFF_FILE_COMMENT_FORM = 'OPEN_DIFF_FILE_COMMENT_FORM'; export const UPDATE_DIFF_FILE_COMMENT_FORM = 'UPDATE_DIFF_FILE_COMMENT_FORM'; export const CLOSE_DIFF_FILE_COMMENT_FORM = 'CLOSE_DIFF_FILE_COMMENT_FORM'; export const SET_HIGHLIGHTED_ROW = 'SET_HIGHLIGHTED_ROW'; + +export const SET_TREE_DATA = 'SET_TREE_DATA'; diff --git a/app/assets/javascripts/diffs/store/mutations.js b/app/assets/javascripts/diffs/store/mutations.js index ed4203cf5e0..00095997ba3 100644 --- a/app/assets/javascripts/diffs/store/mutations.js +++ b/app/assets/javascripts/diffs/store/mutations.js @@ -1,5 +1,4 @@ import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; -import { sortTree } from '~/ide/stores/utils'; import { findDiffFile, addLineReferences, @@ -7,7 +6,6 @@ import { addContextLines, prepareDiffData, isDiscussionApplicableToLine, - generateTreeList, } from './utils'; import * as types from './mutation_types'; @@ -23,12 +21,9 @@ export default { [types.SET_DIFF_DATA](state, data) { prepareDiffData(data); - const { tree, treeEntries } = generateTreeList(data.diff_files); Object.assign(state, { ...convertObjectPropsToCamelCase(data), - tree: sortTree(tree), - treeEntries, }); }, @@ -239,4 +234,8 @@ export default { [types.SET_HIGHLIGHTED_ROW](state, lineCode) { state.highlightedRow = lineCode; }, + [types.SET_TREE_DATA](state, { treeEntries, tree }) { + state.treeEntries = treeEntries; + state.tree = tree; + }, }; diff --git a/app/assets/javascripts/diffs/store/utils.js b/app/assets/javascripts/diffs/store/utils.js index f427367c11e..09afacc24df 100644 --- a/app/assets/javascripts/diffs/store/utils.js +++ b/app/assets/javascripts/diffs/store/utils.js @@ -11,6 +11,7 @@ import { MATCH_LINE_TYPE, LINES_TO_BE_RENDERED_DIRECTLY, MAX_LINES_TO_BE_RENDERED, + TREE_TYPE, } from '../constants'; export function findDiffFile(files, hash) { @@ -180,8 +181,6 @@ export function addContextLines(options) { export function trimFirstCharOfLineContent(line = {}) { // eslint-disable-next-line no-param-reassign delete line.text; - // eslint-disable-next-line no-param-reassign - line.discussions = []; const parsedLine = Object.assign({}, line); @@ -221,10 +220,12 @@ export function prepareDiffData(diffData) { line.line_code = getLineCode(line, u); if (line.left) { line.left = trimFirstCharOfLineContent(line.left); + line.left.discussions = []; line.left.hasForm = false; } if (line.right) { line.right = trimFirstCharOfLineContent(line.right); + line.right.discussions = []; line.right.hasForm = false; } } @@ -234,7 +235,11 @@ export function prepareDiffData(diffData) { const linesLength = file.highlighted_diff_lines.length; for (let u = 0; u < linesLength; u += 1) { const line = file.highlighted_diff_lines[u]; - Object.assign(line, { ...trimFirstCharOfLineContent(line), hasForm: false }); + Object.assign(line, { + ...trimFirstCharOfLineContent(line), + discussions: [], + hasForm: false, + }); } showingLines += file.parallel_diff_lines.length; } @@ -289,8 +294,63 @@ export function isDiscussionApplicableToLine({ discussion, diffPosition, latestD return latestDiff && discussion.active && line_code === discussion.line_code; } -export const generateTreeList = files => - files.reduce( +export const getLowestSingleFolder = folder => { + const getFolder = (blob, start = []) => + blob.tree.reduce( + (acc, file) => { + const shouldGetFolder = file.tree.length === 1 && file.tree[0].type === TREE_TYPE; + const currentFileTypeTree = file.type === TREE_TYPE; + const path = shouldGetFolder || currentFileTypeTree ? acc.path.concat(file.name) : acc.path; + const tree = shouldGetFolder || currentFileTypeTree ? acc.tree.concat(file) : acc.tree; + + if (shouldGetFolder) { + const firstFolder = getFolder(file); + + path.push(firstFolder.path); + tree.push(...firstFolder.tree); + } + + return { + ...acc, + path, + tree, + }; + }, + { path: start, tree: [] }, + ); + const { path, tree } = getFolder(folder, [folder.name]); + + return { + path: path.join('/'), + treeAcc: tree.length ? tree[tree.length - 1].tree : null, + }; +}; + +export const flattenTree = tree => { + const flatten = blobTree => + blobTree.reduce((acc, file) => { + const blob = file; + let treeToFlatten = blob.tree; + + if (file.type === TREE_TYPE && file.tree.length === 1) { + const { treeAcc, path } = getLowestSingleFolder(file); + + if (treeAcc) { + blob.name = path; + treeToFlatten = flatten(treeAcc); + } + } + + blob.tree = flatten(treeToFlatten); + + return acc.concat(blob); + }, []); + + return flatten(tree); +}; + +export const generateTreeList = files => { + const { treeEntries, tree } = files.reduce( (acc, file) => { const split = file.new_path.split('/'); @@ -335,6 +395,9 @@ export const generateTreeList = files => { treeEntries: {}, tree: [] }, ); + return { treeEntries, tree: flattenTree(tree) }; +}; + export const getDiffMode = diffFile => { const diffModeKey = Object.keys(diffModes).find(key => diffFile[`${key}_file`]); return ( diff --git a/app/assets/javascripts/diffs/workers/tree_worker.js b/app/assets/javascripts/diffs/workers/tree_worker.js new file mode 100644 index 00000000000..534d737c77e --- /dev/null +++ b/app/assets/javascripts/diffs/workers/tree_worker.js @@ -0,0 +1,14 @@ +import { sortTree } from '~/ide/stores/utils'; +import { generateTreeList } from '../store/utils'; + +// eslint-disable-next-line no-restricted-globals +self.addEventListener('message', e => { + const { data } = e; + const { treeEntries, tree } = generateTreeList(data); + + // eslint-disable-next-line no-restricted-globals + self.postMessage({ + treeEntries, + tree: sortTree(tree), + }); +}); diff --git a/app/assets/javascripts/dirty_submit/dirty_submit_form.js b/app/assets/javascripts/dirty_submit/dirty_submit_form.js index d8d0fa1fac4..00e41dd0301 100644 --- a/app/assets/javascripts/dirty_submit/dirty_submit_form.js +++ b/app/assets/javascripts/dirty_submit/dirty_submit_form.js @@ -25,15 +25,16 @@ class DirtySubmitForm { DirtySubmitForm.THROTTLE_DURATION, ); this.form.addEventListener('input', throttledUpdateDirtyInput); + this.form.addEventListener('change', throttledUpdateDirtyInput); this.form.addEventListener('submit', event => this.formSubmit(event)); } updateDirtyInput(event) { - const input = event.target; + const { target } = event; - if (!input.dataset.isDirtySubmitInput) return; + if (!target.dataset.isDirtySubmitInput) return; - this.updateDirtyInputs(input); + this.updateDirtyInputs(target); this.toggleSubmission(); } diff --git a/app/assets/javascripts/environments/components/environment_actions.vue b/app/assets/javascripts/environments/components/environment_actions.vue index 1f7dab9fbd2..208bd19f6b0 100644 --- a/app/assets/javascripts/environments/components/environment_actions.vue +++ b/app/assets/javascripts/environments/components/environment_actions.vue @@ -92,7 +92,7 @@ export default { :disabled="isActionDisabled(action)" type="button" class="js-manual-action-link no-btn btn d-flex align-items-center" - @click="onClickAction(action);" + @click="onClickAction(action)" > <span class="flex-fill"> {{ action.name }} </span> <span v-if="action.scheduledAt" class="text-secondary"> diff --git a/app/assets/javascripts/environments/components/environments_app.vue b/app/assets/javascripts/environments/components/environments_app.vue index ae9459a2482..87c1d44dd40 100644 --- a/app/assets/javascripts/environments/components/environments_app.vue +++ b/app/assets/javascripts/environments/components/environments_app.vue @@ -96,9 +96,9 @@ export default { <tabs :tabs="tabs" scope="environments" @onChangeTab="onChangeTab" /> <div v-if="canCreateEnvironment && !isLoading" class="nav-controls"> - <a :href="newEnvironmentPath" class="btn btn-success"> - {{ s__('Environments|New environment') }} - </a> + <a :href="newEnvironmentPath" class="btn btn-success">{{ + s__('Environments|New environment') + }}</a> </div> </div> diff --git a/app/assets/javascripts/environments/components/environments_table.vue b/app/assets/javascripts/environments/components/environments_table.vue index 533e90e2222..75bdf87137f 100644 --- a/app/assets/javascripts/environments/components/environments_table.vue +++ b/app/assets/javascripts/environments/components/environments_table.vue @@ -85,9 +85,9 @@ export default { <div :key="`sub-div-${i}`"> <div class="text-center prepend-top-10"> - <a :href="folderUrl(model)" class="btn btn-default"> - {{ s__('Environments|Show all') }} - </a> + <a :href="folderUrl(model)" class="btn btn-default">{{ + s__('Environments|Show all') + }}</a> </div> </div> </template> diff --git a/app/assets/javascripts/environments/folder/environments_folder_bundle.js b/app/assets/javascripts/environments/folder/environments_folder_bundle.js index 3cf6e4ad14d..982e550e73c 100644 --- a/app/assets/javascripts/environments/folder/environments_folder_bundle.js +++ b/app/assets/javascripts/environments/folder/environments_folder_bundle.js @@ -15,11 +15,11 @@ export default () => const environmentsData = document.querySelector(this.$options.el).dataset; return { - endpoint: environmentsData.endpoint, - folderName: environmentsData.folderName, + endpoint: environmentsData.environmentsDataEndpoint, + folderName: environmentsData.environmentsDataFolderName, cssContainerClass: environmentsData.cssClass, - canCreateDeployment: parseBoolean(environmentsData.canCreateDeployment), - canReadEnvironment: parseBoolean(environmentsData.canReadEnvironment), + canCreateDeployment: parseBoolean(environmentsData.environmentsDataCanCreateDeployment), + canReadEnvironment: parseBoolean(environmentsData.environmentsDataCanReadEnvironment), }; }, render(createElement) { diff --git a/app/assets/javascripts/error_tracking/index.js b/app/assets/javascripts/error_tracking/index.js index 808ae2c9a41..3d609448efe 100644 --- a/app/assets/javascripts/error_tracking/index.js +++ b/app/assets/javascripts/error_tracking/index.js @@ -4,10 +4,6 @@ import store from './store'; import ErrorTrackingList from './components/error_tracking_list.vue'; export default () => { - if (!gon.features.errorTracking) { - return; - } - // eslint-disable-next-line no-new new Vue({ el: '#js-error_tracking', 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 6b1a934d3fe..19bc3313373 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 @@ -66,7 +66,7 @@ export default { <button type="button" class="filtered-search-history-dropdown-item" - @click="onItemActivated(item.text);" + @click="onItemActivated(item.text)" > <span> <span @@ -88,7 +88,7 @@ export default { <button type="button" class="filtered-search-history-clear-button" - @click="onRequestClearRecentSearches($event);" + @click="onRequestClearRecentSearches($event)" > Clear recent searches </button> diff --git a/app/assets/javascripts/fly_out_nav.js b/app/assets/javascripts/fly_out_nav.js index 3ac00c51df4..2b6af9060d1 100644 --- a/app/assets/javascripts/fly_out_nav.js +++ b/app/assets/javascripts/fly_out_nav.js @@ -24,6 +24,9 @@ export const slope = (a, b) => (b.y - a.y) / (b.x - a.x); let headerHeight = 50; export const getHeaderHeight = () => headerHeight; +const setHeaderHeight = () => { + headerHeight = sidebar.offsetTop; +}; export const isSidebarCollapsed = () => sidebar && sidebar.classList.contains('sidebar-collapsed-desktop'); @@ -186,7 +189,7 @@ export default () => { }); } - headerHeight = document.querySelector('.nav-sidebar').offsetTop; + requestIdleCallback(setHeaderHeight); items.forEach(el => { const subItems = el.querySelector('.sidebar-sub-level-items'); diff --git a/app/assets/javascripts/frequent_items/components/app.vue b/app/assets/javascripts/frequent_items/components/app.vue index 63531f1f246..968e255e1fc 100644 --- a/app/assets/javascripts/frequent_items/components/app.vue +++ b/app/assets/javascripts/frequent_items/components/app.vue @@ -47,6 +47,12 @@ export default { } eventHub.$on(`${this.namespace}-dropdownOpen`, this.dropdownOpenHandler); + + // As we init it through requestIdleCallback it could be that the dropdown is already open + const namespaceDropdown = document.getElementById(`nav-${this.namespace}-dropdown`); + if (namespaceDropdown && namespaceDropdown.classList.contains('show')) { + this.dropdownOpenHandler(); + } }, beforeDestroy() { eventHub.$off(`${this.namespace}-dropdownOpen`, this.dropdownOpenHandler); diff --git a/app/assets/javascripts/frequent_items/index.js b/app/assets/javascripts/frequent_items/index.js index 5157ff211dc..6263acbab8e 100644 --- a/app/assets/javascripts/frequent_items/index.js +++ b/app/assets/javascripts/frequent_items/index.js @@ -17,7 +17,7 @@ const frequentItemDropdowns = [ }, ]; -document.addEventListener('DOMContentLoaded', () => { +const initFrequentItemDropdowns = () => { frequentItemDropdowns.forEach(dropdown => { const { namespace, key } = dropdown; const el = document.getElementById(`js-${namespace}-dropdown`); @@ -66,4 +66,8 @@ document.addEventListener('DOMContentLoaded', () => { }, }); }); +}; + +document.addEventListener('DOMContentLoaded', () => { + requestIdleCallback(initFrequentItemDropdowns); }); diff --git a/app/assets/javascripts/ide/components/activity_bar.vue b/app/assets/javascripts/ide/components/activity_bar.vue index a1f66ff764d..7c769ab7fa0 100644 --- a/app/assets/javascripts/ide/components/activity_bar.vue +++ b/app/assets/javascripts/ide/components/activity_bar.vue @@ -45,7 +45,7 @@ export default { data-placement="right" type="button" class="ide-sidebar-link js-ide-edit-mode" - @click.prevent="changedActivityView($event, $options.activityBarViews.edit);" + @click.prevent="changedActivityView($event, $options.activityBarViews.edit)" > <icon name="code" /> </button> @@ -62,7 +62,7 @@ export default { data-placement="right" type="button" class="ide-sidebar-link js-ide-review-mode" - @click.prevent="changedActivityView($event, $options.activityBarViews.review);" + @click.prevent="changedActivityView($event, $options.activityBarViews.review)" > <icon name="file-modified" /> </button> @@ -79,7 +79,7 @@ export default { data-placement="right" type="button" class="ide-sidebar-link js-ide-commit-mode" - @click.prevent="changedActivityView($event, $options.activityBarViews.commit);" + @click.prevent="changedActivityView($event, $options.activityBarViews.commit)" > <icon name="commit" /> </button> 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 6f1ded91753..00b2d236da3 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/message_field.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/message_field.vue @@ -111,8 +111,8 @@ export default { name="commit-message" @scroll="handleScroll" @input="onInput" - @focus="updateIsFocused(true);" - @blur="updateIsFocused(false);" + @focus="updateIsFocused(true)" + @blur="updateIsFocused(false)" @keydown.ctrl.enter="onCtrlEnter" @keydown.meta.enter="onCtrlEnter" > 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 3525084b1cb..2b44438f849 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue @@ -65,7 +65,7 @@ export default { :disabled="disabled" type="radio" name="commit-action" - @change="updateCommitAction($event.target.value);" + @change="updateCommitAction($event.target.value)" /> <span class="prepend-left-10"> <span v-if="label" class="ide-radio-label"> {{ label }} </span> <slot v-else></slot> @@ -76,7 +76,7 @@ export default { :placeholder="newBranchName" type="text" class="form-control monospace" - @input="updateBranchName($event.target.value);" + @input="updateBranchName($event.target.value)" /> </div> </fieldset> 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 02c2004d495..e054be86c1e 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/stage_button.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/stage_button.vue @@ -48,7 +48,7 @@ export default { data-container="body" data-boundary="viewport" data-placement="bottom" - @click.stop.prevent="stageChange(path);" + @click.stop.prevent="stageChange(path)" > <icon :size="16" name="mobile-issue-close" class="ml-auto mr-auto" /> </button> @@ -70,7 +70,7 @@ export default { :header-title-text="modalTitle" :footer-primary-button-text="__('Discard changes')" footer-primary-button-variant="danger" - @submit="discardFileChanges(path);" + @submit="discardFileChanges(path)" > {{ __("You will loose all changes you've made to this file. This action cannot be undone.") }} </gl-modal> 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 ce41fcdb087..0567ef54ff3 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/unstage_button.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/unstage_button.vue @@ -33,7 +33,7 @@ export default { data-container="body" data-boundary="viewport" data-placement="bottom" - @click.stop.prevent="unstageChange(path);" + @click.stop.prevent="unstageChange(path)" > <icon :size="16" name="redo" class="ml-auto mr-auto" /> </button> diff --git a/app/assets/javascripts/ide/components/editor_mode_dropdown.vue b/app/assets/javascripts/ide/components/editor_mode_dropdown.vue index 5f99261ec39..732fa0786b0 100644 --- a/app/assets/javascripts/ide/components/editor_mode_dropdown.vue +++ b/app/assets/javascripts/ide/components/editor_mode_dropdown.vue @@ -40,7 +40,7 @@ export default { 'is-active': viewer === $options.viewerTypes.mr, }" href="#" - @click.prevent="changeMode($options.viewerTypes.mr);" + @click.prevent="changeMode($options.viewerTypes.mr)" > <strong class="dropdown-menu-inner-title"> {{ mergeReviewLine }} </strong> <span class="dropdown-menu-inner-content"> @@ -54,7 +54,7 @@ export default { 'is-active': viewer === $options.viewerTypes.diff, }" href="#" - @click.prevent="changeMode($options.viewerTypes.diff);" + @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/file_finder/index.vue b/app/assets/javascripts/ide/components/file_finder/index.vue index bb391912572..0b0cd7b75eb 100644 --- a/app/assets/javascripts/ide/components/file_finder/index.vue +++ b/app/assets/javascripts/ide/components/file_finder/index.vue @@ -164,7 +164,7 @@ export default { </script> <template> - <div class="ide-file-finder-overlay" @mousedown.self="toggleFileFinder(false);"> + <div class="ide-file-finder-overlay" @mousedown.self="toggleFileFinder(false)"> <div class="dropdown-menu diff-file-changes ide-file-finder show"> <div class="dropdown-input"> <input @@ -174,8 +174,8 @@ export default { type="search" class="dropdown-input-field" autocomplete="off" - @keydown="onKeydown($event);" - @keyup="onKeyup($event);" + @keydown="onKeydown($event)" + @keyup="onKeyup($event)" /> <i :class="{ diff --git a/app/assets/javascripts/ide/components/file_templates/dropdown.vue b/app/assets/javascripts/ide/components/file_templates/dropdown.vue index 414ea9c7d4d..343e0cca672 100644 --- a/app/assets/javascripts/ide/components/file_templates/dropdown.vue +++ b/app/assets/javascripts/ide/components/file_templates/dropdown.vue @@ -91,7 +91,7 @@ export default { <gl-loading-icon v-if="showLoading" :size="2" /> <ul v-else> <li v-for="(item, index) in outputData" :key="index"> - <button type="button" @click="clickItem(item);">{{ item.name }}</button> + <button type="button" @click="clickItem(item)">{{ item.name }}</button> </li> </ul> </div> diff --git a/app/assets/javascripts/ide/components/ide_status_bar.vue b/app/assets/javascripts/ide/components/ide_status_bar.vue index e2e0acc22b1..f1d40586903 100644 --- a/app/assets/javascripts/ide/components/ide_status_bar.vue +++ b/app/assets/javascripts/ide/components/ide_status_bar.vue @@ -84,7 +84,7 @@ export default { <button type="button" class="p-0 border-0 h-50" - @click="openRightPane($options.rightSidebarViews.pipelines);" + @click="openRightPane($options.rightSidebarViews.pipelines)" > <ci-icon v-tooltip diff --git a/app/assets/javascripts/ide/components/ide_tree.vue b/app/assets/javascripts/ide/components/ide_tree.vue index 9fc21adae7c..f93496132a4 100644 --- a/app/assets/javascripts/ide/components/ide_tree.vue +++ b/app/assets/javascripts/ide/components/ide_tree.vue @@ -43,7 +43,7 @@ export default { :show-label="false" class="d-flex border-0 p-0 mr-3 qa-new-file" icon="doc-new" - @click="openNewEntryModal({ type: 'blob' });" + @click="openNewEntryModal({ type: 'blob' })" /> <upload :show-label="false" @@ -56,7 +56,7 @@ export default { :show-label="false" class="d-flex border-0 p-0" icon="folder-new" - @click="openNewEntryModal({ type: 'tree' });" + @click="openNewEntryModal({ type: 'tree' })" /> </div> </template> diff --git a/app/assets/javascripts/ide/components/jobs/detail.vue b/app/assets/javascripts/ide/components/jobs/detail.vue index e8fe5fc696d..7710bfb49ec 100644 --- a/app/assets/javascripts/ide/components/jobs/detail.vue +++ b/app/assets/javascripts/ide/components/jobs/detail.vue @@ -75,7 +75,7 @@ export default { <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);"> + <button class="btn btn-default btn-sm d-flex" @click="setDetailJob(null)"> <icon name="chevron-left" /> {{ __('View jobs') }} </button> </header> diff --git a/app/assets/javascripts/ide/components/merge_requests/list.vue b/app/assets/javascripts/ide/components/merge_requests/list.vue index ac2b0eddfb4..2d55ffb3c65 100644 --- a/app/assets/javascripts/ide/components/merge_requests/list.vue +++ b/app/assets/javascripts/ide/components/merge_requests/list.vue @@ -84,7 +84,7 @@ export default { :placeholder="__('Search merge requests')" @focus="onSearchFocus" @input="searchMergeRequests" - @removeToken="setSearchType(null);" + @removeToken="setSearchType(null)" /> <icon :size="18" name="search" class="input-icon" /> </div> @@ -102,7 +102,7 @@ export default { <button type="button" class="btn-link d-flex align-items-center" - @click.stop="setSearchType(searchType);" + @click.stop="setSearchType(searchType)" > <span class="d-flex append-right-default ide-search-list-current-icon"> <icon :size="18" name="search" /> diff --git a/app/assets/javascripts/ide/components/new_dropdown/index.vue b/app/assets/javascripts/ide/components/new_dropdown/index.vue index a50d729036f..d7a7b1b4d78 100644 --- a/app/assets/javascripts/ide/components/new_dropdown/index.vue +++ b/app/assets/javascripts/ide/components/new_dropdown/index.vue @@ -73,7 +73,7 @@ export default { :aria-label="__('Create new file or directory')" type="button" class="rounded border-0 d-flex ide-entry-dropdown-toggle" - @click.stop="openDropdown();" + @click.stop="openDropdown()" > <icon name="ellipsis_v" /> <icon name="arrow-down" /> </button> @@ -85,7 +85,7 @@ export default { class="d-flex" icon="doc-new" icon-classes="mr-2" - @click="createNewItem('blob');" + @click="createNewItem('blob')" /> </li> <li><upload :path="path" @create="createTempEntry" /></li> @@ -95,7 +95,7 @@ export default { class="d-flex" icon="folder-new" icon-classes="mr-2" - @click="createNewItem($options.modalTypes.tree);" + @click="createNewItem($options.modalTypes.tree)" /> </li> <li class="divider"></li> @@ -106,7 +106,7 @@ export default { class="d-flex" icon="pencil" icon-classes="mr-2" - @click="createNewItem($options.modalTypes.rename);" + @click="createNewItem($options.modalTypes.rename)" /> </li> <li> @@ -115,7 +115,7 @@ export default { class="d-flex" icon="remove" icon-classes="mr-2" - @click="deleteEntry(path);" + @click="deleteEntry(path)" /> </li> </ul> diff --git a/app/assets/javascripts/ide/components/new_dropdown/modal.vue b/app/assets/javascripts/ide/components/new_dropdown/modal.vue index 63cbf41b89b..04ecd4ba4e7 100644 --- a/app/assets/javascripts/ide/components/new_dropdown/modal.vue +++ b/app/assets/javascripts/ide/components/new_dropdown/modal.vue @@ -114,7 +114,7 @@ export default { <button type="button" class="btn btn-missing p-1 pr-2 pl-2" - @click="createFromTemplate(template);" + @click="createFromTemplate(template)" > {{ template.name }} </button> diff --git a/app/assets/javascripts/ide/components/panes/right.vue b/app/assets/javascripts/ide/components/panes/right.vue index 7a57ccf2dd3..2e6bd85feec 100644 --- a/app/assets/javascripts/ide/components/panes/right.vue +++ b/app/assets/javascripts/ide/components/panes/right.vue @@ -122,7 +122,7 @@ export default { data-placement="left" class="ide-sidebar-link is-right" type="button" - @click="clickTab($event, tab);" + @click="clickTab($event, tab)" > <icon :size="16" :name="tab.icon" /> </button> diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue index c13d3ec094b..94a9e87369c 100644 --- a/app/assets/javascripts/ide/components/repo_editor.vue +++ b/app/assets/javascripts/ide/components/repo_editor.vue @@ -219,7 +219,7 @@ export default { <a href="javascript:void(0);" role="button" - @click.prevent="setFileViewMode({ file, viewMode: 'editor' });" + @click.prevent="setFileViewMode({ file, viewMode: 'editor' })" > <template v-if="viewer === $options.viewerTypes.edit"> {{ __('Edit') }} @@ -233,7 +233,7 @@ export default { <a href="javascript:void(0);" role="button" - @click.prevent="setFileViewMode({ file, viewMode: 'preview' });" + @click.prevent="setFileViewMode({ file, viewMode: 'preview' })" > {{ file.previewMode.previewTitle }} </a> diff --git a/app/assets/javascripts/ide/components/repo_tab.vue b/app/assets/javascripts/ide/components/repo_tab.vue index 4b87b83db8a..f6aa2295844 100644 --- a/app/assets/javascripts/ide/components/repo_tab.vue +++ b/app/assets/javascripts/ide/components/repo_tab.vue @@ -74,7 +74,7 @@ export default { active: tab.active, disabled: tab.pending, }" - @click="clickFile(tab);" + @click="clickFile(tab)" @mouseover="mouseOverTab" @mouseout="mouseOutTab" > @@ -88,7 +88,7 @@ export default { :disabled="tab.pending" type="button" class="multi-file-tab-close" - @click.stop.prevent="closeFile(tab);" + @click.stop.prevent="closeFile(tab)" > <icon v-if="!showChangedIcon" :size="12" name="close" /> <changed-file-icon v-else :file="tab" /> diff --git a/app/assets/javascripts/ide/components/resizable_panel.vue b/app/assets/javascripts/ide/components/resizable_panel.vue index a89de56ab5c..7277fcb7617 100644 --- a/app/assets/javascripts/ide/components/resizable_panel.vue +++ b/app/assets/javascripts/ide/components/resizable_panel.vue @@ -78,8 +78,8 @@ export default { :min-size="minSize" :max-size="$options.maxSize" :side="side === 'right' ? 'left' : 'right'" - @resize-start="setResizingStatus(true);" - @resize-end="setResizingStatus(false);" + @resize-start="setResizingStatus(true)" + @resize-end="setResizingStatus(false)" /> </div> </template> diff --git a/app/assets/javascripts/ide/components/shared/tokened_input.vue b/app/assets/javascripts/ide/components/shared/tokened_input.vue index f58e08c2cc9..de3e71dad92 100644 --- a/app/assets/javascripts/ide/components/shared/tokened_input.vue +++ b/app/assets/javascripts/ide/components/shared/tokened_input.vue @@ -76,8 +76,8 @@ export default { <button class="selectable btn-blank" type="button" - @click.stop="removeToken(token);" - @keyup.delete="removeToken(token);" + @click.stop="removeToken(token)" + @keyup.delete="removeToken(token)" > <div class="value-container rounded"> <div class="value">{{ token.label }}</div> diff --git a/app/assets/javascripts/ide/index.js b/app/assets/javascripts/ide/index.js index 6351948f750..5a2b680c2f7 100644 --- a/app/assets/javascripts/ide/index.js +++ b/app/assets/javascripts/ide/index.js @@ -10,13 +10,20 @@ import { parseBoolean } from '../lib/utils/common_utils'; Vue.use(Translate); /** + * Function that receives the default store and returns an extended one. + * @callback extendStoreCallback + * @param {Vuex.Store} store + * @param {Element} el + */ + +/** * Initialize the IDE on the given element. * * @param {Element} el - The element that will contain the IDE. * @param {Object} options - Extra options for the IDE (Used by EE). * @param {Component} options.rootComponent - * Component that overrides the root component. - * @param {(store:Vuex.Store, el:Element) => Vuex.Store} options.extendStore - + * @param {extendStoreCallback} options.extendStore - * Function that receives the default store and returns an extended one. */ export function initIde(el, options = {}) { diff --git a/app/assets/javascripts/jobs/components/job_app.vue b/app/assets/javascripts/jobs/components/job_app.vue index d2b7ce18290..d473d6a482d 100644 --- a/app/assets/javascripts/jobs/components/job_app.vue +++ b/app/assets/javascripts/jobs/components/job_app.vue @@ -80,7 +80,6 @@ export default { 'hasError', ]), ...mapGetters([ - 'headerActions', 'headerTime', 'shouldRenderCalloutMessage', 'shouldRenderTriggeredLabel', @@ -202,7 +201,6 @@ export default { :item-id="job.id" :time="headerTime" :user="job.user" - :actions="headerActions" :has-sidebar-button="true" :should-render-triggered-label="shouldRenderTriggeredLabel" :item-name="__('Job')" diff --git a/app/assets/javascripts/jobs/components/sidebar.vue b/app/assets/javascripts/jobs/components/sidebar.vue index ad3e7dabc79..a2141dc3760 100644 --- a/app/assets/javascripts/jobs/components/sidebar.vue +++ b/app/assets/javascripts/jobs/components/sidebar.vue @@ -48,8 +48,7 @@ export default { return `${this.job.runner.description} (#${this.job.runner.id})`; }, retryButtonClass() { - let className = - 'js-retry-button float-right btn btn-retry d-none d-md-block d-lg-block d-xl-block'; + let className = 'js-retry-button btn btn-retry'; className += this.job.status && this.job.recoverable ? ' btn-primary' : ' btn-inverted-secondary'; return className; @@ -110,24 +109,27 @@ export default { <aside class="right-sidebar build-sidebar" data-offset-top="101" data-spy="affix"> <div class="sidebar-container"> <div class="blocks-container"> - <div class="block d-flex align-items-center"> - <h4 class="flex-grow-1 prepend-top-8 m-0">{{ job.name }}</h4> - <gl-link - v-if="job.retry_path" - :class="retryButtonClass" - :href="job.retry_path" - data-method="post" - rel="nofollow" - >{{ __('Retry') }}</gl-link - > - <gl-link - v-if="job.terminal_path" - :href="job.terminal_path" - class="js-terminal-link pull-right btn btn-primary btn-inverted visible-md-block visible-lg-block" - target="_blank" - > - {{ __('Debug') }} <icon name="external-link" /> - </gl-link> + <div class="block d-flex flex-nowrap align-items-center"> + <h4 class="my-0 mr-2">{{ job.name }}</h4> + <div class="flex-grow-1 flex-shrink-0 text-right"> + <gl-link + v-if="job.retry_path" + :class="retryButtonClass" + :href="job.retry_path" + data-method="post" + rel="nofollow" + >{{ __('Retry') }}</gl-link + > + <gl-link + v-if="job.cancel_path" + :href="job.cancel_path" + class="js-cancel-job btn btn-default" + data-method="post" + rel="nofollow" + >{{ __('Cancel') }}</gl-link + > + </div> + <gl-button :aria-label="__('Toggle Sidebar')" type="button" @@ -137,22 +139,24 @@ export default { <i aria-hidden="true" data-hidden="true" class="fa fa-angle-double-right"></i> </gl-button> </div> - <div v-if="job.retry_path || job.new_issue_path" class="block retry-link"> + + <div v-if="job.terminal_path || job.new_issue_path" class="block retry-link"> <gl-link v-if="job.new_issue_path" :href="job.new_issue_path" - class="js-new-issue btn btn-success btn-inverted" + class="js-new-issue btn btn-success btn-inverted float-left mr-2" >{{ __('New issue') }}</gl-link > <gl-link - v-if="job.retry_path" - :href="job.retry_path" - class="js-retry-job btn btn-inverted-secondary" - data-method="post" - rel="nofollow" - >{{ __('Retry') }}</gl-link + v-if="job.terminal_path" + :href="job.terminal_path" + class="js-terminal-link btn btn-primary btn-inverted visible-md-block visible-lg-block float-left" + target="_blank" > + {{ __('Debug') }} <icon name="external-link" :size="14" /> + </gl-link> </div> + <div :class="{ block: renderBlock }"> <detail-row v-if="job.duration" @@ -193,16 +197,6 @@ export default { tag }}</span> </p> - - <div v-if="job.cancel_path" class="btn-group prepend-top-5" role="group"> - <gl-link - :href="job.cancel_path" - class="js-cancel-job btn btn-sm btn-default" - data-method="post" - rel="nofollow" - >{{ __('Cancel') }}</gl-link - > - </div> </div> <artifacts-block v-if="hasArtifact" :artifact="job.artifact" /> diff --git a/app/assets/javascripts/jobs/components/stages_dropdown.vue b/app/assets/javascripts/jobs/components/stages_dropdown.vue index 7f79e92067f..91332c21b52 100644 --- a/app/assets/javascripts/jobs/components/stages_dropdown.vue +++ b/app/assets/javascripts/jobs/components/stages_dropdown.vue @@ -55,7 +55,7 @@ export default { <ul class="dropdown-menu"> <li v-for="stage in stages" :key="stage.name"> - <button type="button" class="js-stage-item stage-item" @click="onStageClick(stage);"> + <button type="button" class="js-stage-item stage-item" @click="onStageClick(stage)"> {{ stage.name }} </button> </li> diff --git a/app/assets/javascripts/jobs/store/getters.js b/app/assets/javascripts/jobs/store/getters.js index 35e92b0b5d9..98911717381 100644 --- a/app/assets/javascripts/jobs/store/getters.js +++ b/app/assets/javascripts/jobs/store/getters.js @@ -1,22 +1,6 @@ import _ from 'underscore'; -import { __ } from '~/locale'; import { isScrolledToBottom } from '~/lib/utils/scroll_utils'; -export const headerActions = state => { - if (state.job.new_issue_path) { - return [ - { - label: __('New issue'), - path: state.job.new_issue_path, - cssClass: - 'js-new-issue btn btn-success btn-inverted d-none d-md-block d-lg-block d-xl-block', - type: 'link', - }, - ]; - } - return []; -}; - export const headerTime = state => (state.job.started ? state.job.started : state.job.created_at); export const shouldRenderCalloutMessage = state => diff --git a/app/assets/javascripts/layout_nav.js b/app/assets/javascripts/layout_nav.js index b8c3c237eb3..4314e5e1afb 100644 --- a/app/assets/javascripts/layout_nav.js +++ b/app/assets/javascripts/layout_nav.js @@ -11,48 +11,53 @@ function hideEndFade($scrollingTabs) { }); } +function initDeferred() { + $(document).trigger('init.scrolling-tabs'); +} + export default function initLayoutNav() { const contextualSidebar = new ContextualSidebar(); contextualSidebar.bindEvents(); initFlyOutNav(); - $(document) - .on('init.scrolling-tabs', () => { - const $scrollingTabs = $('.scrolling-tabs').not('.is-initialized'); - $scrollingTabs.addClass('is-initialized'); + // We need to init it on DomContentLoaded as others could also call it + $(document).on('init.scrolling-tabs', () => { + const $scrollingTabs = $('.scrolling-tabs').not('.is-initialized'); + $scrollingTabs.addClass('is-initialized'); - $(window) - .on('resize.nav', () => { - hideEndFade($scrollingTabs); - }) - .trigger('resize.nav'); + $(window) + .on('resize.nav', () => { + hideEndFade($scrollingTabs); + }) + .trigger('resize.nav'); - $scrollingTabs.on('scroll', function tabsScrollEvent() { - const $this = $(this); - const currentPosition = $this.scrollLeft(); - const maxPosition = $this.prop('scrollWidth') - $this.outerWidth(); + $scrollingTabs.on('scroll', function tabsScrollEvent() { + const $this = $(this); + const currentPosition = $this.scrollLeft(); + const maxPosition = $this.prop('scrollWidth') - $this.outerWidth(); - $this.siblings('.fade-left').toggleClass('scrolling', currentPosition > 0); - $this.siblings('.fade-right').toggleClass('scrolling', currentPosition < maxPosition - 1); - }); + $this.siblings('.fade-left').toggleClass('scrolling', currentPosition > 0); + $this.siblings('.fade-right').toggleClass('scrolling', currentPosition < maxPosition - 1); + }); - $scrollingTabs.each(function scrollTabsEachLoop() { - const $this = $(this); - const scrollingTabWidth = $this.width(); - const $active = $this.find('.active'); - const activeWidth = $active.width(); + $scrollingTabs.each(function scrollTabsEachLoop() { + const $this = $(this); + const scrollingTabWidth = $this.width(); + const $active = $this.find('.active'); + const activeWidth = $active.width(); - if ($active.length) { - const offset = $active.offset().left + activeWidth; + if ($active.length) { + const offset = $active.offset().left + activeWidth; - if (offset > scrollingTabWidth - 30) { - const scrollLeft = offset - scrollingTabWidth / 2 - activeWidth / 2; + if (offset > scrollingTabWidth - 30) { + const scrollLeft = offset - scrollingTabWidth / 2 - activeWidth / 2; - $this.scrollLeft(scrollLeft); - } + $this.scrollLeft(scrollLeft); } - }); - }) - .trigger('init.scrolling-tabs'); + } + }); + }); + + requestIdleCallback(initDeferred); } diff --git a/app/assets/javascripts/lazy_loader.js b/app/assets/javascripts/lazy_loader.js index ee01a73a6e8..66f25b622e0 100644 --- a/app/assets/javascripts/lazy_loader.js +++ b/app/assets/javascripts/lazy_loader.js @@ -163,6 +163,7 @@ export default class LazyLoader { img.removeAttribute('data-src'); img.classList.remove('lazy'); img.classList.add('js-lazy-loaded'); + img.classList.add('qa-js-lazy-loaded'); } } } diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js index fc34d243dd7..3b6a57dad44 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js +++ b/app/assets/javascripts/lib/utils/common_utils.js @@ -1,3 +1,7 @@ +/** + * @module common-utils + */ + import $ from 'jquery'; import axios from './axios_utils'; import { getLocationHash } from './url_utility'; @@ -426,13 +430,14 @@ export const historyPushState = newUrl => { }; /** - * Returns true for a String "true" and false otherwise. - * This is the opposite of Boolean(...).toString() + * Returns true for a String value of "true" and false otherwise. + * This is the opposite of Boolean(...).toString(). + * `parseBoolean` is idempotent. * * @param {String} value * @returns {Boolean} */ -export const parseBoolean = value => value === 'true'; +export const parseBoolean = value => (value && value.toString()) === 'true'; /** * Converts permission provided as strings to booleans. @@ -450,10 +455,16 @@ export const convertPermissionToBoolean = permission => { }; /** + * @callback backOffCallback + * @param {Function} next + * @param {Function} stop + */ + +/** * Back Off exponential algorithm * backOff :: (Function<next, stop>, Number) -> Promise<Any, Error> * - * @param {Function<next, stop>} fn function to be called + * @param {backOffCallback} fn function to be called * @param {Number} timeout * @return {Promise<Any, Error>} * @example diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js index 01dbbb9dd16..d3fe8f77bd4 100644 --- a/app/assets/javascripts/lib/utils/datetime_utility.js +++ b/app/assets/javascripts/lib/utils/datetime_utility.js @@ -87,44 +87,67 @@ let timeagoInstance; */ export const getTimeago = () => { if (!timeagoInstance) { - const localeRemaining = (number, index) => - [ - [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|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|1 week ago'), s__('Timeago|1 week remaining')], - [s__('Timeago|%s weeks ago'), s__('Timeago|%s weeks remaining')], - [s__('Timeago|1 month ago'), s__('Timeago|1 month remaining')], - [s__('Timeago|%s months ago'), s__('Timeago|%s months remaining')], - [s__('Timeago|1 year ago'), s__('Timeago|1 year remaining')], - [s__('Timeago|%s years ago'), s__('Timeago|%s years remaining')], - ][index]; - - const locale = (number, index) => - [ - [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|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|1 week ago'), s__('Timeago|in 1 week')], - [s__('Timeago|%s weeks ago'), s__('Timeago|in %s weeks')], - [s__('Timeago|1 month ago'), s__('Timeago|in 1 month')], - [s__('Timeago|%s months ago'), s__('Timeago|in %s months')], - [s__('Timeago|1 year ago'), s__('Timeago|in 1 year')], - [s__('Timeago|%s years ago'), s__('Timeago|in %s years')], - ][index]; - - timeago.register(timeagoLanguageCode, locale); - timeago.register(`${timeagoLanguageCode}-remaining`, localeRemaining); + const memoizedLocaleRemaining = () => { + const cache = []; + + const timeAgoLocaleRemaining = [ + () => [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|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|1 week ago'), s__('Timeago|1 week remaining')], + () => [s__('Timeago|%s weeks ago'), s__('Timeago|%s weeks remaining')], + () => [s__('Timeago|1 month ago'), s__('Timeago|1 month remaining')], + () => [s__('Timeago|%s months ago'), s__('Timeago|%s months remaining')], + () => [s__('Timeago|1 year ago'), s__('Timeago|1 year remaining')], + () => [s__('Timeago|%s years ago'), s__('Timeago|%s years remaining')], + ]; + + return (number, index) => { + if (cache[index]) { + return cache[index]; + } + cache[index] = timeAgoLocaleRemaining[index] && timeAgoLocaleRemaining[index](); + return cache[index]; + }; + }; + + const memoizedLocale = () => { + const cache = []; + + const timeAgoLocale = [ + () => [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|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|1 week ago'), s__('Timeago|in 1 week')], + () => [s__('Timeago|%s weeks ago'), s__('Timeago|in %s weeks')], + () => [s__('Timeago|1 month ago'), s__('Timeago|in 1 month')], + () => [s__('Timeago|%s months ago'), s__('Timeago|in %s months')], + () => [s__('Timeago|1 year ago'), s__('Timeago|in 1 year')], + () => [s__('Timeago|%s years ago'), s__('Timeago|in %s years')], + ]; + + return (number, index) => { + if (cache[index]) { + return cache[index]; + } + cache[index] = timeAgoLocale[index] && timeAgoLocale[index](); + return cache[index]; + }; + }; + + timeago.register(timeagoLanguageCode, memoizedLocale()); + timeago.register(`${timeagoLanguageCode}-remaining`, memoizedLocaleRemaining()); + timeagoInstance = timeago(); } @@ -132,35 +155,28 @@ export const getTimeago = () => { }; /** - * For the given element, renders a timeago instance. - * @param {jQuery} $els - */ -export const renderTimeago = $els => { - const timeagoEls = $els || document.querySelectorAll('.js-timeago-render'); - - // timeago.js sets timeouts internally for each timeago value to be updated in real time - getTimeago().render(timeagoEls, timeagoLanguageCode); -}; - -/** * For the given elements, sets a tooltip with a formatted date. - * @param {jQuery} + * @param {JQuery} $timeagoEls * @param {Boolean} setTimeago */ export const localTimeAgo = ($timeagoEls, setTimeago = true) => { - $timeagoEls.each((i, el) => { - if (setTimeago) { + getTimeago().render($timeagoEls, timeagoLanguageCode); + + if (!setTimeago) { + return; + } + + function addTimeAgoTooltip() { + $timeagoEls.each((i, el) => { // Recreate with custom template $(el).tooltip({ template: '<div class="tooltip local-timeago" role="tooltip"><div class="arrow"></div><div class="tooltip-inner"></div></div>', }); - } - - el.classList.add('js-timeago-render'); - }); + }); + } - renderTimeago($timeagoEls); + requestIdleCallback(addTimeAgoTooltip); }; /** diff --git a/app/assets/javascripts/locale/sprintf.js b/app/assets/javascripts/locale/sprintf.js index 5246c49842e..68b64a3a16a 100644 --- a/app/assets/javascripts/locale/sprintf.js +++ b/app/assets/javascripts/locale/sprintf.js @@ -4,8 +4,8 @@ import _ from 'underscore'; Very limited implementation of sprintf supporting only named parameters. @param input (translated) text with parameters (e.g. '%{num_users} users use us') - @param parameters object mapping parameter names to values (e.g. { num_users: 5 }) - @param escapeParameters whether parameter values should be escaped (see http://underscorejs.org/#escape) + @param {Object} parameters object mapping parameter names to values (e.g. { num_users: 5 }) + @param {Boolean} escapeParameters whether parameter values should be escaped (see http://underscorejs.org/#escape) @returns {String} the text with parameters replaces (e.g. '5 users use us') @see https://ruby-doc.org/core-2.3.3/Kernel.html#method-i-sprintf diff --git a/app/assets/javascripts/monitoring/components/graph.vue b/app/assets/javascripts/monitoring/components/graph.vue index 64a1df80a8e..309b73f5a4d 100644 --- a/app/assets/javascripts/monitoring/components/graph.vue +++ b/app/assets/javascripts/monitoring/components/graph.vue @@ -257,8 +257,8 @@ export default { <template> <div class="prometheus-graph" - @mouseover="showFlagContent = true;" - @mouseleave="showFlagContent = false;" + @mouseover="showFlagContent = true" + @mouseleave="showFlagContent = false" > <div class="prometheus-graph-header"> <h5 class="prometheus-graph-title">{{ graphData.title }}</h5> @@ -300,7 +300,7 @@ export default { :height="graphHeight - 100" class="prometheus-graph-overlay" transform="translate(-5, 20)" - @mousemove="handleMouseOverGraph($event);" + @mousemove="handleMouseOverGraph($event)" /> </svg> <svg v-else :viewBox="innerViewBox" class="js-no-data-to-display"> diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue index 7c17147dd01..d669ba5a8fa 100644 --- a/app/assets/javascripts/notes/components/comment_form.vue +++ b/app/assets/javascripts/notes/components/comment_form.vue @@ -357,9 +357,9 @@ js-gfm-input js-autosize markdown-area js-vue-textarea qa-comment-input" data-supports-quick-actions="true" aria-label="Description" placeholder="Write a comment or drag your files here…" - @keydown.up="editCurrentUserLastNote();" - @keydown.meta.enter="handleSave();" - @keydown.ctrl.enter="handleSave();" + @keydown.up="editCurrentUserLastNote()" + @keydown.meta.enter="handleSave()" + @keydown.ctrl.enter="handleSave()" > </textarea> </markdown-field> @@ -373,7 +373,7 @@ append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown" class="btn btn-success js-comment-button js-comment-submit-button qa-comment-button" type="submit" - @click.prevent="handleSave();" + @click.prevent="handleSave()" > {{ __(commentButtonTitle) }} </button> @@ -394,7 +394,7 @@ append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown" <button type="button" class="btn btn-transparent" - @click.prevent="setNoteType('comment');" + @click.prevent="setNoteType('comment')" > <i aria-hidden="true" class="fa fa-check icon"> </i> <div class="description"> @@ -408,7 +408,7 @@ append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown" <button type="button" class="btn btn-transparent qa-discussion-option" - @click.prevent="setNoteType('discussion');" + @click.prevent="setNoteType('discussion')" > <i aria-hidden="true" class="fa fa-check icon"> </i> <div class="description"> @@ -429,7 +429,7 @@ append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown" ]" :disabled="isToggleStateButtonLoading || isSubmitting" :label="issueActionButtonTitle" - @click="handleSave(true);" + @click="handleSave(true)" /> </div> </form> diff --git a/app/assets/javascripts/notes/components/diff_with_note.vue b/app/assets/javascripts/notes/components/diff_with_note.vue index af821df0fd2..376d4114efd 100644 --- a/app/assets/javascripts/notes/components/diff_with_note.vue +++ b/app/assets/javascripts/notes/components/diff_with_note.vue @@ -6,8 +6,6 @@ import ImageDiffOverlay from '~/diffs/components/image_diff_overlay.vue'; import { GlSkeletonLoading } from '@gitlab/ui'; import { getDiffMode } from '~/diffs/store/utils'; -const FIRST_CHAR_REGEX = /^(\+|-| )/; - export default { components: { DiffFileHeader, @@ -54,9 +52,6 @@ export default { this.error = true; }); }, - trimChar(line) { - return line.replace(FIRST_CHAR_REGEX, ''); - }, }, userColorSchemeClass: window.gon.user_color_scheme, }; @@ -85,7 +80,7 @@ export default { > <td class="diff-line-num old_line">{{ line.old_line }}</td> <td class="diff-line-num new_line">{{ line.new_line }}</td> - <td :class="line.type" class="line_content" v-html="trimChar(line.rich_text)"></td> + <td :class="line.type" class="line_content" v-html="line.rich_text"></td> </tr> </template> <tr v-if="!hasTruncatedDiffLines" class="line_holder line-holder-placeholder"> diff --git a/app/assets/javascripts/notes/components/discussion_filter.vue b/app/assets/javascripts/notes/components/discussion_filter.vue index 2d7c04ea614..e03d6e9cd02 100644 --- a/app/assets/javascripts/notes/components/discussion_filter.vue +++ b/app/assets/javascripts/notes/components/discussion_filter.vue @@ -112,7 +112,7 @@ export default { :class="{ 'is-active': filter.value === currentValue }" class="qa-filter-options" type="button" - @click="selectFilter(filter.value);" + @click="selectFilter(filter.value)" > {{ filter.title }} </button> diff --git a/app/assets/javascripts/notes/components/note_awards_list.vue b/app/assets/javascripts/notes/components/note_awards_list.vue index bde00ea87ff..3efdd1c5c17 100644 --- a/app/assets/javascripts/notes/components/note_awards_list.vue +++ b/app/assets/javascripts/notes/components/note_awards_list.vue @@ -174,7 +174,7 @@ export default { data-placement="bottom" class="btn award-control" type="button" - @click="handleAward(awardName);" + @click="handleAward(awardName)" > <span v-html="getAwardHTML(awardName)"></span> <span class="award-control-text js-counter">{{ awardList.length }}</span> diff --git a/app/assets/javascripts/notes/components/note_form.vue b/app/assets/javascripts/notes/components/note_form.vue index db62ddb3ecd..269b4a4b117 100644 --- a/app/assets/javascripts/notes/components/note_form.vue +++ b/app/assets/javascripts/notes/components/note_form.vue @@ -6,6 +6,7 @@ import issueWarning from '../../vue_shared/components/issue/issue_warning.vue'; import markdownField from '../../vue_shared/components/markdown/field.vue'; import issuableStateMixin from '../mixins/issuable_state'; import resolvable from '../mixins/resolvable'; +import { __ } from '~/locale'; export default { name: 'NoteForm', @@ -33,7 +34,7 @@ export default { saveButtonTitle: { type: String, required: false, - default: 'Save comment', + default: __('Save comment'), }, discussion: { type: Object, @@ -219,10 +220,10 @@ export default { class="note-textarea js-gfm-input js-note-text js-autosize markdown-area js-vue-issue-note-form js-vue-textarea qa-reply-input" aria-label="Description" placeholder="Write a comment or drag your files here…" - @keydown.meta.enter="handleKeySubmit();" - @keydown.ctrl.enter="handleKeySubmit();" - @keydown.up="editMyLastNote();" - @keydown.esc="cancelHandler(true);" + @keydown.meta.enter="handleKeySubmit()" + @keydown.ctrl.enter="handleKeySubmit()" + @keydown.up="editMyLastNote()" + @keydown.esc="cancelHandler(true)" ></textarea> </markdown-field> <div class="note-form-actions clearfix"> @@ -230,21 +231,21 @@ export default { :disabled="isDisabled" type="button" class="js-vue-issue-save btn btn-success js-comment-button qa-reply-comment-button" - @click="handleUpdate();" + @click="handleUpdate()" > {{ saveButtonTitle }} </button> <button v-if="discussion.resolvable" class="btn btn-nr btn-default append-right-10 js-comment-resolve-button" - @click.prevent="handleUpdate(true);" + @click.prevent="handleUpdate(true)" > {{ resolveButtonTitle }} </button> <button class="btn btn-cancel note-edit-cancel js-close-discussion-note-form" type="button" - @click="cancelHandler();" + @click="cancelHandler()" > Cancel </button> diff --git a/app/assets/javascripts/notes/components/noteable_discussion.vue b/app/assets/javascripts/notes/components/noteable_discussion.vue index 7c3f5d00308..1a116161e3c 100644 --- a/app/assets/javascripts/notes/components/noteable_discussion.vue +++ b/app/assets/javascripts/notes/components/noteable_discussion.vue @@ -206,11 +206,15 @@ export default { return sprintf(text, { commitId, linkStart, linkEnd }, false); }, diffLine() { + if (this.line) { + return this.line; + } + if (this.discussion.diff_discussion && this.discussion.truncated_diff_lines) { return this.discussion.truncated_diff_lines.slice(-1)[0]; } - return this.line; + return null; }, }, watch: { @@ -440,7 +444,7 @@ Please check your network connection and try again.`; <button type="button" class="btn btn-default ml-sm-2" - @click="resolveHandler();" + @click="resolveHandler()" > <i v-if="isResolving" aria-hidden="true" class="fa fa-spinner fa-spin"></i> {{ resolveButtonTitle }} diff --git a/app/assets/javascripts/notes/stores/mutations.js b/app/assets/javascripts/notes/stores/mutations.js index 8992454be2e..33d39ad2ec9 100644 --- a/app/assets/javascripts/notes/stores/mutations.js +++ b/app/assets/javascripts/notes/stores/mutations.js @@ -105,7 +105,10 @@ export default { if (discussion.diff_file) { diffData.file_hash = discussion.diff_file.file_hash; - diffData.truncated_diff_lines = discussion.truncated_diff_lines || []; + + diffData.truncated_diff_lines = utils.prepareDiffLines( + discussion.truncated_diff_lines || [], + ); } // To support legacy notes, should be very rare case. @@ -243,7 +246,7 @@ export default { [types.SET_DISCUSSION_DIFF_LINES](state, { discussionId, diffLines }) { const discussion = utils.findNoteObjectById(state.discussions, discussionId); - discussion.truncated_diff_lines = diffLines; + discussion.truncated_diff_lines = utils.prepareDiffLines(diffLines); }, [types.DISABLE_COMMENTS](state, value) { diff --git a/app/assets/javascripts/notes/stores/utils.js b/app/assets/javascripts/notes/stores/utils.js index dd57539e4d8..4b0feb0f94d 100644 --- a/app/assets/javascripts/notes/stores/utils.js +++ b/app/assets/javascripts/notes/stores/utils.js @@ -1,4 +1,5 @@ import AjaxCache from '~/lib/utils/ajax_cache'; +import { trimFirstCharOfLineContent } from '~/diffs/store/utils'; const REGEX_QUICK_ACTIONS = /^\/\w+.*$/gm; @@ -28,3 +29,6 @@ export const getQuickActionText = note => { export const hasQuickActions = note => REGEX_QUICK_ACTIONS.test(note); export const stripQuickActions = note => note.replace(REGEX_QUICK_ACTIONS, '').trim(); + +export const prepareDiffLines = diffLines => + diffLines.map(line => ({ ...trimFirstCharOfLineContent(line) })); diff --git a/app/assets/javascripts/pages/admin/application_settings/show/index.js b/app/assets/javascripts/pages/admin/application_settings/show/index.js new file mode 100644 index 00000000000..5ec9688a6e4 --- /dev/null +++ b/app/assets/javascripts/pages/admin/application_settings/show/index.js @@ -0,0 +1,3 @@ +import initUserInternalRegexPlaceholder from '../../application_settings/account_and_limits'; + +document.addEventListener('DOMContentLoaded', initUserInternalRegexPlaceholder()); diff --git a/app/assets/javascripts/pages/admin/index.js b/app/assets/javascripts/pages/admin/index.js index 3aa793e47b9..8a32556f06c 100644 --- a/app/assets/javascripts/pages/admin/index.js +++ b/app/assets/javascripts/pages/admin/index.js @@ -1,7 +1,3 @@ import initAdmin from './admin'; -import initUserInternalRegexPlaceholder from './application_settings/account_and_limits'; -document.addEventListener('DOMContentLoaded', () => { - initAdmin(); - initUserInternalRegexPlaceholder(); -}); +document.addEventListener('DOMContentLoaded', initAdmin()); diff --git a/app/assets/javascripts/pages/groups/clusters/update/index.js b/app/assets/javascripts/pages/groups/clusters/edit/index.js index 8001d2dd1da..8001d2dd1da 100644 --- a/app/assets/javascripts/pages/groups/clusters/update/index.js +++ b/app/assets/javascripts/pages/groups/clusters/edit/index.js diff --git a/app/assets/javascripts/pages/projects/clusters/update/index.js b/app/assets/javascripts/pages/projects/clusters/edit/index.js index 8001d2dd1da..8001d2dd1da 100644 --- a/app/assets/javascripts/pages/projects/clusters/update/index.js +++ b/app/assets/javascripts/pages/projects/clusters/edit/index.js 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 db2a4041ec0..bd4309e47ad 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 @@ -70,7 +70,7 @@ export default { :checked="isEditable" class="label-bold" type="radio" - @click="toggleCustomInput(true);" + @click="toggleCustomInput(true)" /> <label for="custom"> {{ s__('PipelineSheduleIntervalPattern|Custom') }} </label> @@ -88,7 +88,7 @@ export default { :value="cronIntervalPresets.everyDay" class="label-bold" type="radio" - @click="toggleCustomInput(false);" + @click="toggleCustomInput(false)" /> <label class="label-bold" for="every-day"> {{ __('Every day (at 4:00am)') }} </label> @@ -102,7 +102,7 @@ export default { :value="cronIntervalPresets.everyWeek" class="label-bold" type="radio" - @click="toggleCustomInput(false);" + @click="toggleCustomInput(false)" /> <label class="label-bold" for="every-week"> @@ -118,7 +118,7 @@ export default { :value="cronIntervalPresets.everyMonth" class="label-bold" type="radio" - @click="toggleCustomInput(false);" + @click="toggleCustomInput(false)" /> <label class="label-bold" for="every-month"> diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component.vue b/app/assets/javascripts/pipelines/components/graph/graph_component.vue index 59cebaba717..a49dc311bd0 100644 --- a/app/assets/javascripts/pipelines/components/graph/graph_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/graph_component.vue @@ -1,59 +1,20 @@ <script> -import _ from 'underscore'; import { GlLoadingIcon } from '@gitlab/ui'; import StageColumnComponent from './stage_column_component.vue'; +import GraphMixin from '../../mixins/graph_component_mixin'; export default { components: { StageColumnComponent, GlLoadingIcon, }, - props: { - isLoading: { - type: Boolean, - required: true, - }, - pipeline: { - type: Object, - required: true, - }, - }, - computed: { - graph() { - return this.pipeline.details && this.pipeline.details.stages; - }, - }, - methods: { - capitalizeStageName(name) { - const escapedName = _.escape(name); - return escapedName.charAt(0).toUpperCase() + escapedName.slice(1); - }, - isFirstColumn(index) { - return index === 0; - }, - stageConnectorClass(index, stage) { - let className; - - // If it's the first stage column and only has one job - if (index === 0 && stage.groups.length === 1) { - className = 'no-margin'; - } else if (index > 0) { - // If it is not the first column - className = 'left-margin'; - } - - return className; - }, - refreshPipelineGraph() { - this.$emit('refreshPipelineGraph'); - }, - }, + mixins: [GraphMixin], }; </script> <template> <div class="build-content middle-block js-pipeline-graph"> <div class="pipeline-visualization pipeline-graph pipeline-tab-content"> - <div class="text-center"><gl-loading-icon v-if="isLoading" :size="3" /></div> + <div v-if="isLoading" class="m-auto"><gl-loading-icon :size="3" /></div> <ul v-if="!isLoading" class="stage-column-list"> <stage-column-component diff --git a/app/assets/javascripts/pipelines/components/pipelines_actions.vue b/app/assets/javascripts/pipelines/components/pipelines_actions.vue index 2e9f2519fcb..0152e2fbe04 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_actions.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_actions.vue @@ -77,7 +77,7 @@ export default { :class="{ disabled: isActionDisabled(action) }" :disabled="isActionDisabled(action)" class="js-pipeline-action-link no-btn btn" - @click="onClickAction(action);" + @click="onClickAction(action)" > {{ action.name }} <span v-if="action.scheduled_at" class="pull-right"> diff --git a/app/assets/javascripts/pipelines/components/stage.vue b/app/assets/javascripts/pipelines/components/stage.vue index 2d3f667e73e..7426936515a 100644 --- a/app/assets/javascripts/pipelines/components/stage.vue +++ b/app/assets/javascripts/pipelines/components/stage.vue @@ -172,8 +172,6 @@ export default { <span :aria-label="stage.title" aria-hidden="true" class="no-pointer-events"> <icon :name="borderlessIcon" /> </span> - - <i class="fa fa-caret-down" aria-hidden="true"> </i> </button> <div diff --git a/app/assets/javascripts/pipelines/mixins/graph_component_mixin.js b/app/assets/javascripts/pipelines/mixins/graph_component_mixin.js new file mode 100644 index 00000000000..66e9476dadf --- /dev/null +++ b/app/assets/javascripts/pipelines/mixins/graph_component_mixin.js @@ -0,0 +1,44 @@ +import _ from 'underscore'; + +export default { + props: { + isLoading: { + type: Boolean, + required: true, + }, + pipeline: { + type: Object, + required: true, + }, + }, + computed: { + graph() { + return this.pipeline.details && this.pipeline.details.stages; + }, + }, + methods: { + capitalizeStageName(name) { + const escapedName = _.escape(name); + return escapedName.charAt(0).toUpperCase() + escapedName.slice(1); + }, + isFirstColumn(index) { + return index === 0; + }, + stageConnectorClass(index, stage) { + let className; + + // If it's the first stage column and only has one job + if (index === 0 && stage.groups.length === 1) { + className = 'no-margin'; + } else if (index > 0) { + // If it is not the first column + className = 'left-margin'; + } + + return className; + }, + refreshPipelineGraph() { + this.$emit('refreshPipelineGraph'); + }, + }, +}; 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 21095fcba16..83811ab489a 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 @@ -108,9 +108,7 @@ export default { </span> </li> <li v-for="result in results" :key="result.id"> - <button type="button" @click.prevent="setItem(result.name);"> - {{ result.name }} - </button> + <button type="button" @click.prevent="setItem(result.name)">{{ result.name }}</button> </li> </ul> </div> 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 056584c8865..a2eb79af4f9 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 @@ -169,7 +169,7 @@ export default { </span> </li> <li v-for="result in results" :key="result.project_number"> - <button type="button" @click.prevent="setItem(result);">{{ result.name }}</button> + <button type="button" @click.prevent="setItem(result)">{{ result.name }}</button> </li> </ul> </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 728616a441f..5f8a4946f4a 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 @@ -82,9 +82,7 @@ export default { </span> </li> <li v-for="result in results" :key="result.id"> - <button type="button" @click.prevent="setItem(result.name);"> - {{ result.name }} - </button> + <button type="button" @click.prevent="setItem(result.name)">{{ result.name }}</button> </li> </ul> </div> diff --git a/app/assets/javascripts/registry/components/table_registry.vue b/app/assets/javascripts/registry/components/table_registry.vue index 2c19973a114..81fe0a48c06 100644 --- a/app/assets/javascripts/registry/components/table_registry.vue +++ b/app/assets/javascripts/registry/components/table_registry.vue @@ -106,7 +106,7 @@ export default { :aria-label="s__('ContainerRegistry|Remove tag')" variant="danger" class="js-delete-registry d-none d-sm-block float-right" - @click="handleDeleteRegistry(item);" + @click="handleDeleteRegistry(item)" > <icon name="remove" /> </gl-button> diff --git a/app/assets/javascripts/reports/components/modal_open_name.vue b/app/assets/javascripts/reports/components/modal_open_name.vue index 118e4b02c46..4f81cee2a38 100644 --- a/app/assets/javascripts/reports/components/modal_open_name.vue +++ b/app/assets/javascripts/reports/components/modal_open_name.vue @@ -26,7 +26,7 @@ export default { <button type="button" class="btn-link btn-blank text-left break-link vulnerability-name-button" - @click="handleIssueClick();" + @click="handleIssueClick()" > {{ issue.title }} </button> diff --git a/app/assets/javascripts/reports/components/test_issue_body.vue b/app/assets/javascripts/reports/components/test_issue_body.vue index 938e83de546..7700f49bf7d 100644 --- a/app/assets/javascripts/reports/components/test_issue_body.vue +++ b/app/assets/javascripts/reports/components/test_issue_body.vue @@ -30,7 +30,7 @@ export default { <button type="button" class="btn-link btn-blank text-left break-link vulnerability-name-button" - @click="openModal({ issue });" + @click="openModal({ issue })" > <div v-if="isNew" class="badge badge-danger append-right-5">{{ s__('New') }}</div> {{ issue.name }} diff --git a/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue b/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue index f04f7606976..7f86741ed29 100644 --- a/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue +++ b/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue @@ -219,7 +219,7 @@ export default { name="button" type="button" class="js-clear-user-status-button clear-user-status btn" - @click="clearStatusInputs();" + @click="clearStatusInputs()" > <icon name="close" /> </button> 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 faea64c9841..c5cfa92f3c8 100644 --- a/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue +++ b/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue @@ -104,9 +104,7 @@ export default { </div> <div class="title hide-collapsed"> - {{ - sprintf(__('Lock %{issuableDisplayName}'), { issuableDisplayName: issuableDisplayName }) - }} + {{ sprintf(__('Lock %{issuableDisplayName}'), { issuableDisplayName: issuableDisplayName }) }} <button v-if="isEditable" class="float-right lock-edit" 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 d3a4f9c81e0..c03b2a68c78 100644 --- a/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue +++ b/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue @@ -102,13 +102,13 @@ export default { /> <div class="title hide-collapsed"> {{ __('Time tracking') }} - <div v-if="!showHelpState" class="help-button float-right" @click="toggleHelpState(true);"> + <div v-if="!showHelpState" class="help-button float-right" @click="toggleHelpState(true)"> <i class="fa fa-question-circle" aria-hidden="true"> </i> </div> <div v-if="showHelpState" class="close-help-button float-right" - @click="toggleHelpState(false);" + @click="toggleHelpState(false)" > <i class="fa fa-close" aria-hidden="true"> </i> </div> diff --git a/app/assets/javascripts/terminal/terminal.js b/app/assets/javascripts/terminal/terminal.js index 560f50ebf8f..e5dd7a465ea 100644 --- a/app/assets/javascripts/terminal/terminal.js +++ b/app/assets/javascripts/terminal/terminal.js @@ -2,11 +2,13 @@ import _ from 'underscore'; import $ from 'jquery'; import { Terminal } from 'xterm'; import * as fit from 'xterm/lib/addons/fit/fit'; +import * as webLinks from 'xterm/lib/addons/webLinks/webLinks'; import { canScrollUp, canScrollDown } from '~/lib/utils/dom_utils'; const SCROLL_MARGIN = 5; Terminal.applyAddon(fit); +Terminal.applyAddon(webLinks); export default class GLTerminal { constructor(element, options = {}) { @@ -48,6 +50,7 @@ export default class GLTerminal { this.terminal.open(this.container); this.terminal.fit(); + this.terminal.webLinksInit(); this.terminal.focus(); this.socket.onopen = () => { 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 84c8a3464a5..0cafa73362e 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 @@ -12,7 +12,7 @@ export default { name: 'ReadyToMerge', components: { statusIcon, - 'squash-before-merge': SquashBeforeMerge, + SquashBeforeMerge, }, props: { mr: { type: Object, required: true }, @@ -28,6 +28,7 @@ export default { isMakingRequest: false, isMergingImmediately: false, commitMessage: this.mr.commitMessage, + squashBeforeMerge: this.mr.squash, successSvg, warningSvg, }; @@ -110,12 +111,6 @@ export default { return enableSquashBeforeMerge && commitsCount > 1; }, }, - created() { - eventHub.$on('MRWidgetUpdateSquash', this.handleUpdateSquash); - }, - beforeDestroy() { - eventHub.$off('MRWidgetUpdateSquash', this.handleUpdateSquash); - }, methods: { shouldShowMergeControls() { return this.mr.isMergeAllowed || this.shouldShowMergeWhenPipelineSucceedsText; @@ -143,7 +138,7 @@ export default { commit_message: this.commitMessage, merge_when_pipeline_succeeds: this.setToMergeWhenPipelineSucceeds, should_remove_source_branch: this.removeSourceBranch === true, - squash: this.mr.squash, + squash: this.squashBeforeMerge, }; this.isMakingRequest = true; @@ -166,9 +161,6 @@ export default { new Flash('Something went wrong. Please try again.'); // eslint-disable-line }); }, - handleUpdateSquash(val) { - this.mr.squash = val; - }, initiateMergePolling() { simplePoll((continuePolling, stopPolling) => { this.handleMergePolling(continuePolling, stopPolling); @@ -249,7 +241,7 @@ export default { :class="mergeButtonClass" type="button" class="qa-merge-button" - @click="handleMergeButtonClick();" + @click="handleMergeButtonClick()" > <i v-if="isMakingRequest" class="fa fa-spinner fa-spin" aria-hidden="true"></i> {{ mergeButtonText }} @@ -273,7 +265,7 @@ export default { <a class="merge_when_pipeline_succeeds qa-merge-when-pipeline-succeeds-option" href="#" - @click.prevent="handleMergeButtonClick(true);" + @click.prevent="handleMergeButtonClick(true)" > <span class="media"> <span class="merge-opt-icon" aria-hidden="true" v-html="successSvg"></span> @@ -285,7 +277,7 @@ export default { <a class="accept-merge-request qa-merge-immediately-option" href="#" - @click.prevent="handleMergeButtonClick(false, true);" + @click.prevent="handleMergeButtonClick(false, true)" > <span class="media"> <span class="merge-opt-icon" aria-hidden="true" v-html="warningSvg"></span> @@ -311,8 +303,9 @@ export default { <!-- Placeholder for EE extension of this component --> <squash-before-merge v-if="shouldShowSquashBeforeMerge" - :mr="mr" - :is-merge-button-disabled="isMergeButtonDisabled" + v-model="squashBeforeMerge" + :help-path="mr.squashBeforeMergeHelpPath" + :is-disabled="isMergeButtonDisabled" /> <span v-if="mr.ffOnlyEnabled" class="js-fast-forward-message"> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/squash_before_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/squash_before_merge.vue index e71acf0d7dd..b1f5655a15a 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/squash_before_merge.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/squash_before_merge.vue @@ -1,6 +1,5 @@ <script> import Icon from '~/vue_shared/components/icon.vue'; -import eventHub from '~/vue_merge_request_widget/event_hub'; import tooltip from '~/vue_shared/directives/tooltip'; export default { @@ -11,23 +10,19 @@ export default { tooltip, }, props: { - mr: { - type: Object, - required: true, - }, - isMergeButtonDisabled: { + value: { type: Boolean, required: true, }, - }, - data() { - return { - squashBeforeMerge: this.mr.squash, - }; - }, - methods: { - updateSquashModel() { - eventHub.$emit('MRWidgetUpdateSquash', this.squashBeforeMerge); + helpPath: { + type: String, + required: false, + default: '', + }, + isDisabled: { + type: Boolean, + required: false, + default: false, }, }, }; @@ -37,18 +32,19 @@ export default { <div class="accept-control inline"> <label class="merge-param-checkbox"> <input - v-model="squashBeforeMerge" - :disabled="isMergeButtonDisabled" + :checked="value" + :disabled="isDisabled" type="checkbox" name="squash" class="qa-squash-checkbox" - @change="updateSquashModel" + @change="$emit('input', $event.target.checked)" /> {{ __('Squash commits') }} </label> <a + v-if="helpPath" v-tooltip - :href="mr.squashBeforeMergeHelpPath" + :href="helpPath" data-title="About this feature" data-placement="bottom" target="_blank" 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 b7f12076958..5a9d86594b1 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 @@ -32,7 +32,6 @@ import MRWidgetStore from './stores/ee_switch_mr_widget_store'; import MRWidgetService from './services/ee_switch_mr_widget_service'; import eventHub from './event_hub'; import stateMaps from './stores/ee_switch_state_maps'; -import SquashBeforeMerge from './components/states/squash_before_merge.vue'; import notify from '~/lib/utils/notify'; import SourceBranchRemovalStatus from './components/source_branch_removal_status.vue'; import GroupedTestReportsApp from '../reports/components/grouped_test_reports_app.vue'; @@ -59,7 +58,6 @@ export default { 'mr-widget-missing-branch': MissingBranchState, 'mr-widget-ready-to-merge': ReadyToMergeState, 'sha-mismatch': ShaMismatchState, - 'mr-widget-squash-before-merge': SquashBeforeMerge, 'mr-widget-checking': CheckingState, 'mr-widget-unresolved-discussions': UnresolvedDiscussionsState, 'mr-widget-pipeline-blocked': PipelineBlockedState, diff --git a/app/assets/javascripts/vue_shared/components/bar_chart.vue b/app/assets/javascripts/vue_shared/components/bar_chart.vue index 4abf795f7bd..eabf5d4bf60 100644 --- a/app/assets/javascripts/vue_shared/components/bar_chart.vue +++ b/app/assets/javascripts/vue_shared/components/bar_chart.vue @@ -293,8 +293,8 @@ export default { :title="setTooltipTitle(data)" class="bar-rect" data-placement="top" - @mouseover="barHoveredIn(index);" - @mouseout="barHoveredOut(index);" + @mouseover="barHoveredIn(index)" + @mouseout="barHoveredOut(index)" /> </template> </g> diff --git a/app/assets/javascripts/vue_shared/components/deprecated_modal.vue b/app/assets/javascripts/vue_shared/components/deprecated_modal.vue index 2129f90d497..36b3ee05456 100644 --- a/app/assets/javascripts/vue_shared/components/deprecated_modal.vue +++ b/app/assets/javascripts/vue_shared/components/deprecated_modal.vue @@ -95,7 +95,7 @@ export default { class="close float-right" data-dismiss="modal" aria-label="Close" - @click="emitCancel($event);" + @click="emitCancel($event)" > <span aria-hidden="true">×</span> </button> @@ -112,7 +112,7 @@ export default { type="button" class="btn" data-dismiss="modal" - @click="emitCancel($event);" + @click="emitCancel($event)" > {{ closeButtonLabel }} </button> @@ -130,7 +130,7 @@ export default { type="button" class="btn js-primary-button" data-dismiss="modal" - @click="emitSubmit($event);" + @click="emitSubmit($event)" > {{ primaryButtonLabel }} </button> 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 index d5fda7e4ed3..cab92297ca7 100644 --- 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 @@ -75,7 +75,7 @@ export default { :class="{ active: mode === $options.imageViewMode.twoup, }" - @click="changeMode($options.imageViewMode.twoup);" + @click="changeMode($options.imageViewMode.twoup)" > {{ s__('ImageDiffViewer|2-up') }} </li> @@ -83,7 +83,7 @@ export default { :class="{ active: mode === $options.imageViewMode.swipe, }" - @click="changeMode($options.imageViewMode.swipe);" + @click="changeMode($options.imageViewMode.swipe)" > {{ s__('ImageDiffViewer|Swipe') }} </li> @@ -91,7 +91,7 @@ export default { :class="{ active: mode === $options.imageViewMode.onion, }" - @click="changeMode($options.imageViewMode.onion);" + @click="changeMode($options.imageViewMode.onion)" > {{ s__('ImageDiffViewer|Onion skin') }} </li> diff --git a/app/assets/javascripts/vue_shared/components/file_row.vue b/app/assets/javascripts/vue_shared/components/file_row.vue index 4c884c55a30..f54033efc54 100644 --- a/app/assets/javascripts/vue_shared/components/file_row.vue +++ b/app/assets/javascripts/vue_shared/components/file_row.vue @@ -139,8 +139,8 @@ export default { class="file-row" role="button" @click="clickFile" - @mouseover="toggleHover(true);" - @mouseout="toggleHover(false);" + @mouseover="toggleHover(true)" + @mouseout="toggleHover(false)" > <div class="file-row-name-container"> <span ref="textOutput" :style="levelIndentation" class="file-row-name str-truncated"> diff --git a/app/assets/javascripts/vue_shared/components/gl_modal.vue b/app/assets/javascripts/vue_shared/components/gl_modal.vue index faf4181bbaf..438851e5ac7 100644 --- a/app/assets/javascripts/vue_shared/components/gl_modal.vue +++ b/app/assets/javascripts/vue_shared/components/gl_modal.vue @@ -81,7 +81,7 @@ export default { type="button" class="close js-modal-close-action" data-dismiss="modal" - @click="emitCancel($event);" + @click="emitCancel($event)" > <span aria-hidden="true">×</span> </button> @@ -96,7 +96,7 @@ export default { type="button" class="btn js-modal-cancel-action qa-modal-cancel-button" data-dismiss="modal" - @click="emitCancel($event);" + @click="emitCancel($event)" > {{ s__('Modal|Cancel') }} </button> @@ -105,7 +105,7 @@ export default { type="button" class="btn js-modal-primary-action qa-modal-primary-button" data-dismiss="modal" - @click="emitSubmit($event);" + @click="emitSubmit($event)" > {{ footerPrimaryButtonText }} </button> 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 c830f5b49b6..3f45dc7853b 100644 --- a/app/assets/javascripts/vue_shared/components/header_ci_component.vue +++ b/app/assets/javascripts/vue_shared/components/header_ci_component.vue @@ -148,7 +148,7 @@ export default { :class="action.cssClass" container-class="d-inline" :label="action.label" - @click="onClickAction(action);" + @click="onClickAction(action)" /> </template> </section> diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue index 937a2847a58..cc07ef46064 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/field.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue @@ -182,9 +182,9 @@ export default { this.hasSuggestion = data.references.suggestions && data.references.suggestions.length; } - this.$nextTick(() => { - $(this.$refs['markdown-preview']).renderGFM(); - }); + this.$nextTick() + .then(() => $(this.$refs['markdown-preview']).renderGFM()) + .catch(() => new Flash(__('Error rendering markdown preview'))); }, versionedPreviewPath() { diff --git a/app/assets/javascripts/vue_shared/components/markdown/header.vue b/app/assets/javascripts/vue_shared/components/markdown/header.vue index bf4d42670ee..dbfa32cd0ce 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/header.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/header.vue @@ -78,12 +78,7 @@ export default { <div class="md-header"> <ul class="nav-links clearfix"> <li :class="{ active: !previewMarkdown }" class="md-header-tab"> - <button - class="js-write-link" - tabindex="-1" - type="button" - @click="writeMarkdownTab($event);" - > + <button class="js-write-link" tabindex="-1" type="button" @click="writeMarkdownTab($event)"> Write </button> </li> @@ -92,7 +87,7 @@ export default { class="js-preview-link js-md-preview-button" tabindex="-1" type="button" - @click="previewMarkdownTab($event);" + @click="previewMarkdownTab($event)" > Preview </button> diff --git a/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue b/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue index 563e2f94fcc..c5a2aa1f2af 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue @@ -42,7 +42,7 @@ export default { <div class="md-suggestion-header border-bottom-0 mt-2"> <div class="qa-suggestion-diff-header font-weight-bold"> {{ __('Suggested change') }} - <a v-if="helpPagePath" :href="helpPagePath" :aria-label="__('Help')"> + <a v-if="helpPagePath" :href="helpPagePath" :aria-label="__('Help')" class="js-help-btn"> <icon name="question-o" css-classes="link-highlight" /> </a> </div> diff --git a/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue b/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue index 721f0276ac8..dcda701f049 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue @@ -129,7 +129,7 @@ export default { <template> <div> - <div class="flash-container mt-3"></div> + <div class="flash-container js-suggestions-flash"></div> <div v-show="isRendered" ref="container" v-html="noteHtml"></div> </div> </template> diff --git a/app/assets/javascripts/vue_shared/components/navigation_tabs.vue b/app/assets/javascripts/vue_shared/components/navigation_tabs.vue index 09a64502819..f8983a3d29a 100644 --- a/app/assets/javascripts/vue_shared/components/navigation_tabs.vue +++ b/app/assets/javascripts/vue_shared/components/navigation_tabs.vue @@ -58,7 +58,7 @@ export default { active: tab.isActive, }" > - <a :class="`js-${scope}-tab-${tab.scope}`" role="button" @click="onTabClick(tab);"> + <a :class="`js-${scope}-tab-${tab.scope}`" role="button" @click="onTabClick(tab)"> {{ tab.name }} <span v-if="shouldRenderBadge(tab.count)" class="badge badge-pill"> {{ tab.count }} </span> diff --git a/app/assets/javascripts/vue_shared/components/notes/system_note.vue b/app/assets/javascripts/vue_shared/components/notes/system_note.vue index 31df26f7b05..b0af8399955 100644 --- a/app/assets/javascripts/vue_shared/components/notes/system_note.vue +++ b/app/assets/javascripts/vue_shared/components/notes/system_note.vue @@ -97,7 +97,7 @@ export default { v-html="note.note_html" ></div> <div v-if="hasMoreCommits" class="flex-list"> - <div class="system-note-commit-list-toggler flex-row" @click="expanded = !expanded;"> + <div class="system-note-commit-list-toggler flex-row" @click="expanded = !expanded"> <icon :name="toggleIcon" :size="8" class="append-right-5" /> <span>Toggle commit list</span> </div> diff --git a/app/assets/javascripts/vue_shared/components/recaptcha_modal.vue b/app/assets/javascripts/vue_shared/components/recaptcha_modal.vue index 1c6c3fc4734..df19906309c 100644 --- a/app/assets/javascripts/vue_shared/components/recaptcha_modal.vue +++ b/app/assets/javascripts/vue_shared/components/recaptcha_modal.vue @@ -19,7 +19,7 @@ export default { data() { return { script: {}, - scriptSrc: 'https://www.google.com/recaptcha/api.js', + scriptSrc: 'https://www.recaptcha.net/recaptcha/api.js', }; }, 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 82067129c57..6c0c7f15943 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue @@ -134,7 +134,7 @@ export default { <button type="button" class="btn-blank btn-link btn-secondary-hover-link" - @click="newDateSelected(null);" + @click="newDateSelected(null)" > remove </button> diff --git a/app/assets/javascripts/vue_shared/components/table_pagination.vue b/app/assets/javascripts/vue_shared/components/table_pagination.vue index 01e655d27e5..2a34b4630f2 100644 --- a/app/assets/javascripts/vue_shared/components/table_pagination.vue +++ b/app/assets/javascripts/vue_shared/components/table_pagination.vue @@ -149,7 +149,7 @@ export default { }" class="page-item" > - <a class="page-link" @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/user_avatar/user_avatar_list.vue b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_list.vue index 7361867edc5..8eaf8386b99 100644 --- a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_list.vue +++ b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_list.vue @@ -23,6 +23,11 @@ export default { required: false, default: 20, }, + emptyText: { + type: String, + required: false, + default: __('None'), + }, }, data() { return { @@ -65,7 +70,8 @@ export default { </script> <template> - <div> + <div v-if="!items.length">{{ emptyText }}</div> + <div v-else> <user-avatar-link v-for="item in visibleItems" :key="item.id" diff --git a/app/assets/javascripts/vuex_shared/modules/modal/actions.js b/app/assets/javascripts/vuex_shared/modules/modal/actions.js index 552237e05c5..7b209909f69 100644 --- a/app/assets/javascripts/vuex_shared/modules/modal/actions.js +++ b/app/assets/javascripts/vuex_shared/modules/modal/actions.js @@ -15,3 +15,6 @@ export const show = ({ commit }) => { export const hide = ({ commit }) => { commit(types.HIDE); }; + +// prevent babel-plugin-rewire from generating an invalid default during karma tests +export default () => {}; diff --git a/app/assets/stylesheets/bootstrap_migration.scss b/app/assets/stylesheets/bootstrap_migration.scss index 587127bb059..c8357f7751c 100644 --- a/app/assets/stylesheets/bootstrap_migration.scss +++ b/app/assets/stylesheets/bootstrap_migration.scss @@ -225,7 +225,7 @@ h3.popover-header { } .info-well { - background: $theme-gray-50; + background: $gray-50; color: $gl-text-color; border: 1px solid $border-color; border-radius: 4px; diff --git a/app/assets/stylesheets/components/related_items_list.scss b/app/assets/stylesheets/components/related_items_list.scss new file mode 100644 index 00000000000..048a5c0300c --- /dev/null +++ b/app/assets/stylesheets/components/related_items_list.scss @@ -0,0 +1,381 @@ +$item-path-max-width: 160px; +$item-milestone-max-width: 120px; +$item-weight-max-width: 48px; + +.related-items-list { + padding: $gl-padding-4; + + &, + .list-item:last-child { + margin-bottom: 0; + } +} + +.item-body { + display: flex; + position: relative; + align-items: center; + padding: $gl-padding-8; + line-height: $gl-line-height; + + .item-contents { + display: flex; + align-items: center; + flex-wrap: wrap; + flex-grow: 1; + } + + .issue-token-state-icon-open, + .issue-token-state-icon-closed, + .confidential-icon, + .item-milestone .icon, + .item-weight .board-card-info-icon { + min-width: $gl-padding; + cursor: help; + } + + .issue-token-state-icon-open, + .issue-token-state-icon-closed { + margin-right: $gl-padding-4; + } + + .confidential-icon { + align-self: baseline; + color: $orange-600; + margin-right: $gl-padding-4; + } + + .item-title { + flex-basis: 100%; + margin-bottom: $gl-padding-8; + font-size: $gl-font-size-small; + + &.mr-title { + font-weight: $gl-font-weight-bold; + } + + .sortable-link { + max-width: 85%; + } + + .issue-token-state-icon-open, + .issue-token-state-icon-closed { + display: none; + } + } + + .item-meta { + display: flex; + flex-wrap: wrap; + flex-basis: 100%; + font-size: $gl-font-size-small; + color: $gl-text-color-secondary; + + .item-meta-child { + order: 0; + display: flex; + flex-wrap: wrap; + flex-basis: 100%; + + .item-due-date, + .item-weight { + margin-left: $gl-padding-8; + } + + .item-milestone, + .item-weight { + cursor: help; + } + + .item-milestone { + text-decoration: none; + max-width: $item-milestone-max-width; + } + + .item-due-date { + margin-right: 0; + } + + .item-weight { + margin-right: 0; + max-width: $item-weight-max-width; + } + } + + .item-path-id .path-id-text, + .item-milestone .milestone-title, + .item-due-date, + .item-weight .board-card-info-text { + color: $gl-text-color-secondary; + display: inline-block; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + } + + .item-path-id { + margin-top: $gl-padding-4; + font-size: $gl-font-size-xs; + white-space: nowrap; + + .path-id-text { + font-weight: $gl-font-weight-bold; + max-width: $item-path-max-width; + } + + .issue-token-state-icon-open, + .issue-token-state-icon-closed { + display: block; + } + + &:not(.mr-item-path) { + order: 1; + } + } + + .item-milestone .ic-clock { + color: $gl-text-color-tertiary; + margin-right: $gl-padding-4; + } + + .item-assignees { + order: 2; + align-self: flex-end; + align-items: center; + margin-left: auto; + + .user-avatar-link { + margin-right: -$gl-padding-4; + + &:nth-of-type(1) { + z-index: 2; + } + + &:nth-of-type(2) { + z-index: 1; + } + + &:last-child { + margin-right: 0; + } + } + + .avatar { + height: $gl-padding; + width: $gl-padding; + margin-right: 0; + vertical-align: bottom; + } + + .avatar-counter { + height: $gl-padding; + border: 1px solid transparent; + background-color: $gl-text-color-tertiary; + font-weight: $gl-font-weight-bold; + padding: 0 $gl-padding-4; + line-height: $gl-padding; + } + } + } + + .btn-item-remove { + position: absolute; + right: 0; + top: $gl-padding-4 / 2; + padding: $gl-padding-4; + margin-right: $gl-padding-4 / 2; + line-height: 0; + border-color: transparent; + color: $gl-text-color-secondary; + + &:hover { + color: $gl-text-color; + } + } +} + +.mr-status-wrapper, +.mr-ci-status + { + line-height: 0; +} + +@include media-breakpoint-up(sm) { + .item-body { + .item-contents .item-title { + .mr-title-link, + .sortable-link { + max-width: 90%; + } + } + } +} + +/* Small devices (landscape phones, 768px and up) */ +@include media-breakpoint-up(md) { + .item-body { + .item-contents { + min-width: 0; + + .item-title { + flex-basis: unset; + // 95% because we compensate + // for remove button which is + // positioned absolutely + width: 95%; + margin-bottom: $gl-padding-4; + + .mr-title-link, + .sortable-link { + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + max-width: 100%; + } + } + + .item-meta { + .item-path-id { + order: 0; + margin-top: 0; + } + + .item-meta-child { + flex-basis: unset; + margin-left: auto; + margin-right: $gl-padding-4; + + ~ .item-assignees { + margin-left: $gl-padding-4; + } + } + + .item-assignees { + margin-bottom: 0; + margin-left: 0; + order: 2; + } + } + } + + .btn-item-remove { + order: 1; + } + } +} + +/* Medium devices (desktops, 992px and up) */ +@include media-breakpoint-up(lg) { + .item-body { + padding: $gl-padding; + + .item-title { + font-size: $gl-font-size; + } + + .item-meta .item-path-id { + font-size: inherit; // Base size given to `item-meta` is `$gl-font-size-small` + } + + .issue-token-state-icon-open, + .issue-token-state-icon-closed { + margin-right: $gl-padding-4; + } + } +} + +/* Large devices (large desktops, 1200px and up) */ +@include media-breakpoint-up(xl) { + .item-body { + padding: $gl-padding-8; + padding-left: $gl-padding; + + .item-contents { + flex-wrap: nowrap; + overflow: hidden; + + .item-title { + display: flex; + margin-bottom: 0; + min-width: 0; + width: auto; + flex-basis: unset; + font-weight: $gl-font-weight-normal; + + .mr-title-link, + .sortable-link { + display: block; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + } + + .issue-token-state-icon-open, + .issue-token-state-icon-closed { + display: block; + margin-right: $gl-padding-8; + } + + .confidential-icon { + align-self: auto; + margin-top: 0; + } + } + + .item-meta { + margin-top: 0; + justify-content: flex-end; + flex: 1; + flex-wrap: nowrap; + + .item-path-id { + order: 0; + margin-top: 0; + margin-left: $gl-padding-8; + margin-right: auto; + + .issue-token-state-icon-open, + .issue-token-state-icon-closed { + display: none; + } + } + + .item-meta-child { + margin-left: $gl-padding-8; + flex-wrap: nowrap; + } + + .item-assignees { + flex-grow: 0; + margin-top: 0; + margin-right: $gl-padding-4; + + .avatar { + height: $gl-padding-24; + width: $gl-padding-24; + } + + .avatar-counter { + height: $gl-padding-24; + min-width: $gl-padding-24; + line-height: $gl-padding-24; + border-radius: $gl-padding-24; + } + } + } + } + + .btn-item-remove { + position: relative; + align-self: center; + top: initial; + right: 0; + margin-right: 0; + padding: $btn-sm-side-margin; + + &:hover { + border-color: $border-color; + } + } + } +} diff --git a/app/assets/stylesheets/framework/animations.scss b/app/assets/stylesheets/framework/animations.scss index 43d4044033f..4fb787887a1 100644 --- a/app/assets/stylesheets/framework/animations.scss +++ b/app/assets/stylesheets/framework/animations.scss @@ -204,7 +204,7 @@ a { [class^="skeleton-line-"] { position: relative; - background-color: $theme-gray-100; + background-color: $gray-100; height: 10px; overflow: hidden; @@ -220,10 +220,10 @@ a { background-size: cover; background-image: linear-gradient( to right, - $theme-gray-100 0%, - $theme-gray-50 20%, - $theme-gray-100 40%, - $theme-gray-100 100% + $gray-100 0%, + $gray-50 20%, + $gray-100 40%, + $gray-100 100% ); height: 10px; } diff --git a/app/assets/stylesheets/framework/awards.scss b/app/assets/stylesheets/framework/awards.scss index 7a95db5976d..ad650d45314 100644 --- a/app/assets/stylesheets/framework/awards.scss +++ b/app/assets/stylesheets/framework/awards.scss @@ -176,7 +176,7 @@ &.user-authored { cursor: default; background-color: $gray-light; - border-color: $theme-gray-200; + border-color: $gray-200; color: $gl-text-color-disabled; gl-emoji { diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss index a4a9276c580..5d2cbdde8dc 100644 --- a/app/assets/stylesheets/framework/buttons.scss +++ b/app/assets/stylesheets/framework/buttons.scss @@ -483,7 +483,7 @@ // All disabled buttons, regardless of color, type, etc %disabled { background-color: $gray-light !important; - border-color: $theme-gray-200 !important; + border-color: $gray-200 !important; color: $gl-text-color-disabled !important; opacity: 1 !important; cursor: default !important; diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index cb01a41cb7e..b90db135b4a 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -129,7 +129,7 @@ @extend .dropdown-toggle; padding-right: 25px; position: relative; - width: 163px; + width: 160px; text-overflow: ellipsis; overflow: hidden; diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss index 0a0ef2071e9..cbf9ee24ec5 100644 --- a/app/assets/stylesheets/framework/forms.scss +++ b/app/assets/stylesheets/framework/forms.scss @@ -261,7 +261,7 @@ label { right: 0.8em; top: 50%; transform: translateY(-50%); - color: $theme-gray-600; + color: $gray-600; } } diff --git a/app/assets/stylesheets/framework/gitlab_theme.scss b/app/assets/stylesheets/framework/gitlab_theme.scss index 0ef50e139f2..418eafa153c 100644 --- a/app/assets/stylesheets/framework/gitlab_theme.scss +++ b/app/assets/stylesheets/framework/gitlab_theme.scss @@ -282,31 +282,31 @@ body { &.ui-dark { @include gitlab-theme( - $theme-gray-200, - $theme-gray-500, - $theme-gray-700, - $theme-gray-800, - $theme-gray-900, + $gray-200, + $gray-500, + $gray-700, + $gray-800, + $gray-900, $white-light ); } &.ui-light { @include gitlab-theme( - $theme-gray-700, - $theme-gray-800, - $theme-gray-700, - $theme-gray-700, - $theme-gray-100, - $theme-gray-700 + $gray-700, + $gray-800, + $gray-700, + $gray-700, + $gray-100, + $gray-700 ); .navbar-gitlab { - background-color: $theme-gray-100; + background-color: $gray-100; box-shadow: 0 1px 0 0 $border-color; .logo-text svg { - fill: $theme-gray-900; + fill: $gray-900; } .navbar-sub-nav, @@ -315,7 +315,7 @@ body { > a:hover, > a:focus, > button:hover { - color: $theme-gray-900; + color: $gray-900; } &.active > a, @@ -329,8 +329,8 @@ body { .container-fluid { .navbar-toggler, .navbar-toggler:hover { - color: $theme-gray-700; - border-left: 1px solid $theme-gray-200; + color: $gray-700; + border-left: 1px solid $gray-200; } } } @@ -348,7 +348,7 @@ body { .search-input-wrap { .search-icon { - fill: $theme-gray-200; + fill: $gray-200; } .search-input { @@ -359,16 +359,16 @@ body { .nav-sidebar li.active { > a { - color: $theme-gray-900; + color: $gray-900; } svg { - fill: $theme-gray-900; + fill: $gray-900; } } .sidebar-top-level-items > li.active .badge.badge-pill { - color: $theme-gray-900; + color: $gray-900; } } } diff --git a/app/assets/stylesheets/framework/icons.scss b/app/assets/stylesheets/framework/icons.scss index 8db7d63266e..49b9b7014ae 100644 --- a/app/assets/stylesheets/framework/icons.scss +++ b/app/assets/stylesheets/framework/icons.scss @@ -87,8 +87,8 @@ display: flex; align-items: center; justify-content: center; - border: $border-size solid $theme-gray-400; + border: $border-size solid $gray-400; border-radius: 50%; padding: $gl-padding-8 - $border-size; - color: $theme-gray-700; + color: $gray-700; } diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss index ce46d760d7b..679148ddf7b 100644 --- a/app/assets/stylesheets/framework/markdown_area.scss +++ b/app/assets/stylesheets/framework/markdown_area.scss @@ -158,7 +158,7 @@ max-height: calc(100vh - 100px); } - table { + table:not(.js-syntax-highlight) { @include markdown-table; } } diff --git a/app/assets/stylesheets/framework/stacked_progress_bar.scss b/app/assets/stylesheets/framework/stacked_progress_bar.scss index 29a2d5881f7..2255d3be75b 100644 --- a/app/assets/stylesheets/framework/stacked_progress_bar.scss +++ b/app/assets/stylesheets/framework/stacked_progress_bar.scss @@ -3,7 +3,7 @@ height: 16px; border-radius: 10px; overflow: hidden; - background-color: $theme-gray-100; + background-color: $gray-100; .status-unavailable, .status-green, @@ -24,7 +24,7 @@ .status-unavailable { padding: 0 10px; - color: $theme-gray-700; + color: $gray-700; } .status-green { @@ -36,11 +36,11 @@ } .status-neutral { - background-color: $theme-gray-200; + background-color: $gray-200; color: $gl-gray-dark; &:hover { - background-color: $theme-gray-300; + background-color: $gray-300; } } diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss index 0c81dc2e156..45dab036d35 100644 --- a/app/assets/stylesheets/framework/typography.scss +++ b/app/assets/stylesheets/framework/typography.scss @@ -143,7 +143,7 @@ margin: 0 0 16px; } - table { + table:not(.js-syntax-highlight) { @extend .table; @extend .table-bordered; margin: 16px 0; diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 242977e8543..e886a54dc99 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -97,6 +97,18 @@ $red-800: #8b2615; $red-900: #711e11; $red-950: #4b140b; +$gray-50: #fafafa; +$gray-100: #f2f2f2; +$gray-200: #dfdfdf; +$gray-300: #cccccc; +$gray-400: #bababa; +$gray-500: #a7a7a7; +$gray-600: #919191; +$gray-700: #707070; +$gray-800: #4f4f4f; +$gray-900: #2e2e2e; +$gray-950: #1f1f1f; + // GitLab themes $indigo-50: #f7f7ff; @@ -111,18 +123,6 @@ $indigo-800: #393982; $indigo-900: #292961; $indigo-950: #1a1a40; -$theme-gray-50: #fafafa; -$theme-gray-100: #f2f2f2; -$theme-gray-200: #dfdfdf; -$theme-gray-300: #cccccc; -$theme-gray-400: #bababa; -$theme-gray-500: #a7a7a7; -$theme-gray-600: #919191; -$theme-gray-700: #707070; -$theme-gray-800: #4f4f4f; -$theme-gray-900: #2e2e2e; -$theme-gray-950: #1f1f1f; - $theme-blue-50: #f4f8fc; $theme-blue-100: #e6edf5; $theme-blue-200: #c8d7e6; @@ -664,3 +664,8 @@ $priority-label-empty-state-width: 114px; Issues Analytics */ $issues-analytics-popover-boarder-color: rgba(0, 0, 0, 0.15); +/* +Merge Requests +*/ +$mr-tabs-height: 51px; +$mr-version-controls-height: 56px; diff --git a/app/assets/stylesheets/framework/variables_overrides.scss b/app/assets/stylesheets/framework/variables_overrides.scss index 069f45bff49..1dfe2a69a2f 100644 --- a/app/assets/stylesheets/framework/variables_overrides.scss +++ b/app/assets/stylesheets/framework/variables_overrides.scss @@ -5,7 +5,7 @@ $secondary: $gray-light; $input-disabled-bg: $gray-light; -$input-border-color: $theme-gray-200; +$input-border-color: $gray-200; $input-color: $gl-text-color; $font-family-sans-serif: $regular-font; $font-family-monospace: $monospace-font; @@ -20,7 +20,7 @@ $warning: $orange-500; $danger: $red-500; $zindex-modal-backdrop: 1040; $nav-divider-margin-y: ($grid-size / 2); -$dropdown-divider-bg: $theme-gray-200; +$dropdown-divider-bg: $gray-200; $dropdown-item-padding-y: 8px; $dropdown-item-padding-x: 12px; $popover-max-width: 300px; @@ -34,3 +34,12 @@ $h3-font-size: 14px * 1.75; $h4-font-size: 14px * 1.5; $h5-font-size: 14px * 1.25; $h6-font-size: 14px; +$spacer: $grid-size; +$spacers: ( + 0: 0, + 1: ($spacer * .5), + 2: ($spacer), + 3: ($spacer * 2), + 4: ($spacer * 3), + 5: ($spacer * 4) +); diff --git a/app/assets/stylesheets/page_bundles/ide.scss b/app/assets/stylesheets/page_bundles/ide.scss index 98d0a2d43ea..553cc44fe83 100644 --- a/app/assets/stylesheets/page_bundles/ide.scss +++ b/app/assets/stylesheets/page_bundles/ide.scss @@ -130,7 +130,7 @@ $ide-commit-header-height: 48px; background: none; border: 0; border-radius: $border-radius-default; - color: $theme-gray-900; + color: $gray-900; svg { position: relative; @@ -145,7 +145,7 @@ $ide-commit-header-height: 48px; } &:not([disabled]):hover { - background-color: $theme-gray-200; + background-color: $gray-200; } &:not([disabled]):focus { @@ -265,7 +265,7 @@ $ide-commit-header-height: 48px; .margin { background-color: $white-light; - border-right: 1px solid $theme-gray-100; + border-right: 1px solid $gray-100; .line-insert { border-right: 1px solid $line-added-dark; @@ -292,7 +292,7 @@ $ide-commit-header-height: 48px; .monaco-editor, .monaco-editor-background, .monaco-editor .inputarea.ime-input { - background-color: $theme-gray-50; + background-color: $gray-50; } } } @@ -527,7 +527,7 @@ $ide-commit-header-height: 48px; display: block; margin-left: auto; margin-right: auto; - color: $theme-gray-700; + color: $gray-700; } .file-status-icon { @@ -551,7 +551,7 @@ $ide-commit-header-height: 48px; &:hover, &:focus { - background: $theme-gray-100; + background: $gray-100; outline: 0; @@ -563,7 +563,7 @@ $ide-commit-header-height: 48px; } &:active { - background: $theme-gray-200; + background: $gray-200; } &.is-active { @@ -767,12 +767,12 @@ $ide-commit-header-height: 48px; &:hover { color: $gl-text-color; - background-color: $theme-gray-100; + background-color: $gray-100; } &:focus { color: $gl-text-color; - background-color: $theme-gray-200; + background-color: $gray-200; } &.active { @@ -1273,10 +1273,10 @@ $ide-commit-header-height: 48px; .ide-entry-dropdown-toggle { padding: $gl-padding-4; color: $gl-text-color; - background-color: $theme-gray-100; + background-color: $gray-100; &:hover { - background-color: $theme-gray-200; + background-color: $gray-200; } &:active, @@ -1331,7 +1331,7 @@ $ide-commit-header-height: 48px; &:focus { outline: 0; box-shadow: none; - border-color: $theme-gray-200; + border-color: $gray-200; } } @@ -1358,7 +1358,7 @@ $ide-commit-header-height: 48px; .ide-commit-editor-header { height: 65px; padding: 8px 16px; - background-color: $theme-gray-50; + background-color: $gray-50; box-shadow: inset 0 -1px $white-dark; } @@ -1370,7 +1370,7 @@ $ide-commit-header-height: 48px; .ide-file-icon-holder { display: flex; align-items: center; - color: $theme-gray-700; + color: $gray-700; } .file-row:hover, diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss index c5a0eaaf704..bc28ffb3a92 100644 --- a/app/assets/stylesheets/pages/boards.scss +++ b/app/assets/stylesheets/pages/boards.scss @@ -26,6 +26,12 @@ opacity: 0.3; } +.dropdown-projects { + .dropdown-content { + max-height: 200px; + } +} + .dropdown-menu-issues-board-new { width: 320px; @@ -167,6 +173,7 @@ background: $gray-light; border: 1px solid $border-color; border-radius: $border-radius-default; + flex: 1; } .board-header { @@ -228,9 +235,11 @@ } .board-blank-state { - height: calc(100% - 49px); padding: $gl-padding; background-color: $white-light; + flex: 1; + overflow-y: auto; + overflow-x: hidden; } .board-blank-state-list { @@ -252,9 +261,9 @@ } .board-list-component { - height: calc(100% - 49px); - overflow: hidden; position: relative; + flex: 1; + min-height: 0; // firefox fix } .board-list { @@ -277,7 +286,7 @@ padding: $gl-padding; background: $white-light; border-radius: $border-radius-default; - border: 1px solid $theme-gray-200; + border: 1px solid $gray-200; box-shadow: 0 1px 2px $issue-boards-card-shadow; list-style: none; line-height: $gl-padding; @@ -669,7 +678,7 @@ } .board-card-info-icon { - color: $theme-gray-600; + color: $gray-600; margin-right: $gl-padding-4; } diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss index 1352a004206..65f46e3852a 100644 --- a/app/assets/stylesheets/pages/builds.scss +++ b/app/assets/stylesheets/pages/builds.scss @@ -261,7 +261,7 @@ .trigger-variables-table-cell { font-size: $gl-font-size-small; line-height: $gl-line-height; - border: 1px solid $theme-gray-200; + border: 1px solid $gray-200; padding: $gl-padding-4 6px; width: 50%; vertical-align: top; @@ -272,7 +272,13 @@ } .retry-link { - display: none; + display: block; + + .btn { + i { + margin-left: 5px; + } + } .btn-inverted-secondary { color: $blue-500; @@ -281,16 +287,6 @@ color: $white-light; } } - - @include media-breakpoint-down(sm) { - display: block; - - .btn { - i { - margin-left: 5px; - } - } - } } .stage-item { diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index b78f11aadf1..4f804866886 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -9,7 +9,7 @@ @media (min-width: map-get($grid-breakpoints, md)) { position: -webkit-sticky; position: sticky; - top: $header-height + 51px; + top: $mr-version-controls-height + $header-height + $mr-tabs-height; margin-left: -1px; border-left: 1px solid $border-color; z-index: 102; @@ -19,6 +19,7 @@ .with-performance-bar & { top: $header-height + 36px + $performance-bar-height; + } } @@ -34,7 +35,7 @@ } .with-performance-bar & { - top: 127px; + top: $header-height + $performance-bar-height + $mr-version-controls-height + $mr-tabs-height; } } @@ -1026,8 +1027,9 @@ .tree-list-holder { position: -webkit-sticky; position: sticky; - top: 100px; - max-height: calc(100vh - 100px); + $top-pos: $header-height + $mr-tabs-height + $mr-version-controls-height + 10px; + top: $header-height + $mr-tabs-height + $mr-version-controls-height + 10px; + max-height: calc(100vh - $top-pos); padding-right: $gl-padding; .file-row { @@ -1036,8 +1038,9 @@ } .with-performance-bar & { - top: 135px; - max-height: calc(100vh - 135px); + $performance-bar-top-pos: $performance-bar-height + $top-pos; + top: $performance-bar-top-pos; + max-height: calc(100vh - $performance-bar-top-pos); } } diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss index 75166ffcada..b6abb792709 100644 --- a/app/assets/stylesheets/pages/environments.scss +++ b/app/assets/stylesheets/pages/environments.scss @@ -267,7 +267,7 @@ .prometheus-graph-cursor { position: absolute; - background: $theme-gray-600; + background: $gray-600; width: 1px; } @@ -309,7 +309,7 @@ > .arrow::after { border-top: 6px solid transparent; border-bottom: 6px solid transparent; - border-left: 4px solid $theme-gray-50; + border-left: 4px solid $gray-50; } .arrow-shadow { @@ -331,7 +331,7 @@ > .arrow::after { border-top: 6px solid transparent; border-bottom: 6px solid transparent; - border-right: 4px solid $theme-gray-50; + border-right: 4px solid $gray-50; } .arrow-shadow { @@ -364,7 +364,7 @@ } > .popover-title { - background-color: $theme-gray-50; + background-color: $gray-50; border-radius: $border-radius-default $border-radius-default 0 0; } } @@ -439,7 +439,7 @@ } > text { - fill: $theme-gray-600; + fill: $gray-600; font-size: 10px; } } @@ -482,5 +482,5 @@ } .prometheus-table-row-highlight { - background-color: $theme-gray-100; + background-color: $gray-100; } diff --git a/app/assets/stylesheets/pages/graph.scss b/app/assets/stylesheets/pages/graph.scss index 4fb1a956fab..83b1680512d 100644 --- a/app/assets/stylesheets/pages/graph.scss +++ b/app/assets/stylesheets/pages/graph.scss @@ -44,7 +44,7 @@ } .x-axis-text { - fill: $theme-gray-900; + fill: $gray-900; } .bar-rect { @@ -64,7 +64,7 @@ text { font-weight: bold; font-size: 12px; - fill: $theme-gray-800; + fill: $gray-800; } } } @@ -87,5 +87,5 @@ .animate-flicker { animation: flickerAnimation 1.5s infinite; - fill: $theme-gray-500; + fill: $gray-500; } diff --git a/app/assets/stylesheets/pages/groups.scss b/app/assets/stylesheets/pages/groups.scss index f0cb81e0bc3..ebbb5beed81 100644 --- a/app/assets/stylesheets/pages/groups.scss +++ b/app/assets/stylesheets/pages/groups.scss @@ -78,7 +78,7 @@ &:hover { background-color: $gray-darker; - color: $theme-gray-900; + color: $gray-900; } } diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index a1069aa9783..e0bdc1341b1 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -934,7 +934,7 @@ .sidebar-collapsed-divider { line-height: 5px; font-size: 12px; - color: $theme-gray-700; + color: $gray-700; + .sidebar-collapsed-icon { padding-top: 0; diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss index 6c847fc0d53..0037364978c 100644 --- a/app/assets/stylesheets/pages/issues.scss +++ b/app/assets/stylesheets/pages/issues.scss @@ -80,7 +80,6 @@ ul.related-merge-requests > li { } } -.merge-requests-title, .related-branches-title { font-size: 16px; font-weight: $gl-font-weight-bold; @@ -91,23 +90,16 @@ ul.related-merge-requests > li { } .merge-request-status { - font-size: 13px; - padding: 0 5px; - color: $white-light; - height: 20px; - border-radius: 3px; - line-height: 18px; - &.merged { - background: $blue-500; + color: $blue-500; } &.closed { - background: $red-500; + color: $red-500; } &.open { - background: $green-500; + color: $green-500; } } diff --git a/app/assets/stylesheets/pages/issues/issue_count_badge.scss b/app/assets/stylesheets/pages/issues/issue_count_badge.scss index 4fba89e956b..64ca61f7094 100644 --- a/app/assets/stylesheets/pages/issues/issue_count_badge.scss +++ b/app/assets/stylesheets/pages/issues/issue_count_badge.scss @@ -1,11 +1,13 @@ -.issue-count-badge { +.issue-count-badge, +.mr-count-badge { display: inline-flex; border-radius: $border-radius-base; border: 1px solid $border-color; padding: 5px $gl-padding-8; } -.issue-count-badge-count { +.issue-count-badge-count, +.mr-count-badge-count { display: inline-flex; align-items: center; } diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss index d2b9470be69..2372640277e 100644 --- a/app/assets/stylesheets/pages/labels.scss +++ b/app/assets/stylesheets/pages/labels.scss @@ -82,7 +82,7 @@ justify-content: space-between; padding: $gl-padding; border-radius: $border-radius-default; - border: 1px solid $theme-gray-100; + border: 1px solid $gray-100; &:last-child { margin-bottom: 0; @@ -257,7 +257,7 @@ } .label-badge { - color: $theme-gray-900; + color: $gray-900; font-weight: $gl-font-weight-normal; padding: $gl-padding-4 $gl-padding-8; border-radius: $border-radius-default; @@ -269,7 +269,7 @@ } .label-badge-gray { - background-color: $theme-gray-100; + background-color: $gray-100; } .label-links { @@ -326,11 +326,11 @@ } .label-action { - color: $theme-gray-800; + color: $gray-800; cursor: pointer; svg { - fill: $theme-gray-800; + fill: $gray-800; } &:hover { diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index 221b4e934ff..53afb182b54 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -64,7 +64,7 @@ &::before { content: ''; - border-left: 1px solid $theme-gray-200; + border-left: 1px solid $gray-200; position: absolute; left: 32px; top: -17px; @@ -708,6 +708,7 @@ .mr-version-controls { position: relative; + z-index: 103; background: $gray-light; color: $gl-text-color; @@ -755,13 +756,37 @@ color: $orange-500; padding-right: 5px; } + + @include media-breakpoint-up(md) { + position: -webkit-sticky; + position: sticky; + top: $header-height + $mr-tabs-height; + width: 100%; + border-top: 1px solid $border-color; + + &.is-fileTreeOpen { + margin-left: -16px; + width: calc(100% + 32px); + } + + .mr-version-menus-container { + flex-wrap: nowrap; + } + + .with-performance-bar & { + top: $header-height + $performance-bar-height + $mr-tabs-height; + } + } } .merge-request-tabs-holder { top: $header-height; z-index: 200; background-color: $white-light; - border-bottom: 1px solid $border-color; + + @include media-breakpoint-down(md) { + border-bottom: 1px solid $border-color; + } @include media-breakpoint-up(sm) { position: sticky; @@ -816,7 +841,7 @@ display: flex; justify-content: space-between; - @include media-breakpoint-down(md) { + @include media-breakpoint-down(sm) { flex-direction: column-reverse; } @@ -907,7 +932,7 @@ } .btn svg { - fill: $theme-gray-700; + fill: $gray-700; } .dropdown-menu { @@ -951,7 +976,7 @@ .coverage { font-size: 12px; - color: $theme-gray-700; + color: $gray-700; line-height: initial; } diff --git a/app/assets/stylesheets/pages/milestone.scss b/app/assets/stylesheets/pages/milestone.scss index 94bf32945fc..15f3a2ef4a8 100644 --- a/app/assets/stylesheets/pages/milestone.scss +++ b/app/assets/stylesheets/pages/milestone.scss @@ -8,7 +8,7 @@ $status-box-line-height: 26px; padding: $gl-padding-8; margin-top: $gl-padding-8; border-radius: $border-radius-default; - background-color: $theme-gray-100; + background-color: $gray-100; .milestone { border: 0; diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss index 86f571dd90d..51f755c67af 100644 --- a/app/assets/stylesheets/pages/note_form.scss +++ b/app/assets/stylesheets/pages/note_form.scss @@ -147,7 +147,7 @@ } .sidebar-item-value & { - fill: $theme-gray-700; + fill: $gray-700; } } diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index 69b7b80dbf4..23b9e4f9416 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -5,7 +5,7 @@ $note-form-margin-left: 72px; @mixin vertical-line($left) { &::before { content: ''; - border-left: 2px solid $theme-gray-100; + border-left: 2px solid $gray-100; position: absolute; top: 0; bottom: 0; diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index 7a47e0a2836..a28921592ec 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -256,14 +256,25 @@ } } - .mini-pipeline-graph-dropdown-toggle svg { - height: $ci-action-icon-size; - width: $ci-action-icon-size; - position: absolute; - top: -1px; - left: -1px; - z-index: 2; - overflow: visible; + .mini-pipeline-graph-dropdown-toggle { + svg { + height: $ci-action-icon-size; + width: $ci-action-icon-size; + position: absolute; + top: -1px; + left: -1px; + z-index: 2; + overflow: visible; + } + + &:hover, + &:active, + &:focus { + svg { + top: -2px; + left: -2px; + } + } } .stage-container { @@ -293,7 +304,7 @@ width: 7px; position: absolute; right: -7px; - top: 10px; + top: 11px; border-bottom: 2px solid $border-color; } } @@ -433,6 +444,7 @@ } .pipeline-tab-content { + display: flex; width: 100%; background-color: $gray-light; padding: $gl-padding; @@ -707,21 +719,43 @@ font-weight: $gl-font-weight-normal; } -@mixin mini-pipeline-graph-color($color-light, $color-main, $color-dark) { - border-color: $color-main; - color: $color-main; +@mixin mini-pipeline-graph-color( + $color-background-default, + $color-background-hover-focus, + $color-background-active, + $color-foreground-default, + $color-foreground-hover-focus, + $color-foreground-active +) { + background-color: $color-background-default; + border-color: $color-foreground-default; + + svg { + fill: $color-foreground-default; + } &:hover, - &:focus, + &:focus { + background-color: $color-background-hover-focus; + border-color: $color-foreground-hover-focus; + + svg { + fill: $color-foreground-hover-focus; + } + } + &:active { - background-color: $color-light; - border-color: $color-dark; - color: $color-dark; + background-color: $color-background-active; + border-color: $color-foreground-active; svg { - fill: $color-dark; + fill: $color-foreground-active; } } + + &:focus { + box-shadow: 0 0 4px 1px $blue-300; + } } @mixin mini-pipeline-item() { @@ -733,26 +767,32 @@ height: $ci-action-icon-size; margin: 0; padding: 0; - transition: all 0.2s linear; position: relative; vertical-align: middle; + &:hover, + &:active, + &:focus { + outline: none; + border-width: 2px; + } + // Dropdown button animation in mini pipeline graph &.ci-status-icon-success { - @include mini-pipeline-graph-color($green-100, $green-500, $green-600); + @include mini-pipeline-graph-color($white, $green-100, $green-200, $green-500, $green-600, $green-700); } &.ci-status-icon-failed { - @include mini-pipeline-graph-color($red-100, $red-500, $red-600); + @include mini-pipeline-graph-color($white, $red-100, $red-200, $red-500, $red-600, $red-700); } &.ci-status-icon-pending, &.ci-status-icon-success_with_warnings { - @include mini-pipeline-graph-color($orange-100, $orange-500, $orange-600); + @include mini-pipeline-graph-color($white, $orange-100, $orange-200, $orange-500, $orange-600, $orange-700); } &.ci-status-icon-running { - @include mini-pipeline-graph-color($blue-100, $blue-400, $blue-600); + @include mini-pipeline-graph-color($white, $blue-100, $blue-200, $blue-500, $blue-600, $blue-700); } &.ci-status-icon-canceled, @@ -760,42 +800,18 @@ &.ci-status-icon-disabled, &.ci-status-icon-not-found, &.ci-status-icon-manual { - @include mini-pipeline-graph-color(rgba($gl-text-color, 0.1), $gl-text-color, $gl-text-color); + @include mini-pipeline-graph-color($white, $gray-700, $gray-800, $gray-900, $gray-950, $black); } &.ci-status-icon-created, &.ci-status-icon-skipped { - @include mini-pipeline-graph-color(rgba($gray-darkest, 0.1), $gray-darkest, $gray-darkest); + @include mini-pipeline-graph-color($white, $gray-200, $gray-300, $gray-500, $gray-600, $gray-700); } } // Dropdown button in mini pipeline graph button.mini-pipeline-graph-dropdown-toggle { @include mini-pipeline-item(); - - > .fa.fa-caret-down { - position: absolute; - left: 20px; - top: 5px; - display: inline-block; - visibility: hidden; - opacity: 0; - color: inherit; - font-size: 12px; - transition: visibility 0.1s, opacity 0.1s linear; - } - - &:active, - &:focus, - &:hover { - outline: none; - width: 35px; - - .fa.fa-caret-down { - visibility: visible; - opacity: 1; - } - } } /** diff --git a/app/assets/stylesheets/pages/profiles/preferences.scss b/app/assets/stylesheets/pages/profiles/preferences.scss index a353f301d07..45e62913f37 100644 --- a/app/assets/stylesheets/pages/profiles/preferences.scss +++ b/app/assets/stylesheets/pages/profiles/preferences.scss @@ -60,11 +60,11 @@ } &.ui-dark { - background-color: $theme-gray-900; + background-color: $gray-900; } &.ui-light { - background-color: $theme-gray-200; + background-color: $gray-200; } } diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 2d28333689f..505f6e036e3 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -459,7 +459,7 @@ margin-right: $gl-padding-4; margin-bottom: $gl-padding-4; color: $gl-text-color-secondary; - background-color: $theme-gray-100; + background-color: $gray-100; line-height: $gl-btn-line-height; &:hover { @@ -914,7 +914,7 @@ } .repository-language-bar-tooltip-share { - color: $theme-gray-400; + color: $gray-400; } pre.light-well { @@ -1025,8 +1025,10 @@ pre.light-well { margin: 0; } - @include media-breakpoint-up(md) { - .description { + .description { + line-height: 1.5; + + @include media-breakpoint-up(md) { color: $gl-text-color; } } diff --git a/app/assets/stylesheets/pages/reports.scss b/app/assets/stylesheets/pages/reports.scss index ecd51aa06a4..f7619ccbd20 100644 --- a/app/assets/stylesheets/pages/reports.scss +++ b/app/assets/stylesheets/pages/reports.scss @@ -96,7 +96,7 @@ } &.neutral svg { - color: $theme-gray-700; + color: $gray-700; } .ci-status-icon { diff --git a/app/assets/stylesheets/pages/settings.scss b/app/assets/stylesheets/pages/settings.scss index ccfa4e00a5b..c5b9d1f6885 100644 --- a/app/assets/stylesheets/pages/settings.scss +++ b/app/assets/stylesheets/pages/settings.scss @@ -297,7 +297,7 @@ .btn-clipboard { background-color: $white-light; - border: 1px solid $theme-gray-200; + border: 1px solid $gray-200; } .deploy-token-help-block { diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index a8fc848c879..26cd5dc801f 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -177,11 +177,17 @@ class ApplicationController < ActionController::Base # hide existence of the resource, rather tell them they cannot access it using # the provided message status ||= message.present? ? :forbidden : :not_found + template = + if status == :not_found + "errors/not_found" + else + "errors/access_denied" + end respond_to do |format| format.any { head status } format.html do - render "errors/access_denied", + render template, layout: "errors", status: status, locals: { message: message } diff --git a/app/controllers/concerns/uploads_actions.rb b/app/controllers/concerns/uploads_actions.rb index c114e16edf8..4ec0e94df9a 100644 --- a/app/controllers/concerns/uploads_actions.rb +++ b/app/controllers/concerns/uploads_actions.rb @@ -29,7 +29,13 @@ module UploadsActions def show return render_404 unless uploader&.exists? - expires_in 0.seconds, must_revalidate: true, private: true + if cache_publicly? + # We need to reset caching from the applications controller to get rid of the no-store value + headers['Cache-Control'] = '' + expires_in 5.minutes, public: true, must_revalidate: false + else + expires_in 0.seconds, must_revalidate: true, private: true + end disposition = uploader.image_or_video? ? 'inline' : 'attachment' @@ -114,6 +120,10 @@ module UploadsActions nil end + def cache_publicly? + false + end + def model strong_memoize(:model) { find_model } end diff --git a/app/controllers/dashboard/projects_controller.rb b/app/controllers/dashboard/projects_controller.rb index f073b6de444..b1d224d026f 100644 --- a/app/controllers/dashboard/projects_controller.rb +++ b/app/controllers/dashboard/projects_controller.rb @@ -53,6 +53,9 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController # rubocop: disable CodeReuse/ActiveRecord def load_projects(finder_params) + @total_user_projects_count = ProjectsFinder.new(params: { non_public: true }, current_user: current_user).execute + @total_starred_projects_count = ProjectsFinder.new(params: { starred: true }, current_user: current_user).execute + projects = ProjectsFinder .new(params: finder_params, current_user: current_user) .execute diff --git a/app/controllers/explore/projects_controller.rb b/app/controllers/explore/projects_controller.rb index 778fdda8dbd..9f074690cbc 100644 --- a/app/controllers/explore/projects_controller.rb +++ b/app/controllers/explore/projects_controller.rb @@ -55,6 +55,9 @@ class Explore::ProjectsController < Explore::ApplicationController # rubocop: disable CodeReuse/ActiveRecord def load_projects + @total_user_projects_count = ProjectsFinder.new(params: { non_public: true }, current_user: current_user).execute + @total_starred_projects_count = ProjectsFinder.new(params: { starred: true }, current_user: current_user).execute + projects = ProjectsFinder.new(current_user: current_user, params: params) .execute .includes(:route, :creator, :group, namespace: [:route, :owner]) diff --git a/app/controllers/groups/children_controller.rb b/app/controllers/groups/children_controller.rb index d549f793ad7..236a19a8dc4 100644 --- a/app/controllers/groups/children_controller.rb +++ b/app/controllers/groups/children_controller.rb @@ -35,7 +35,7 @@ module Groups def setup_children(parent) @children = GroupDescendantsFinder.new(current_user: current_user, parent_group: parent, - params: params).execute + params: params.to_unsafe_h).execute @children = @children.page(params[:page]) end end diff --git a/app/controllers/import/base_controller.rb b/app/controllers/import/base_controller.rb index 042b6b1264f..9b45be6db99 100644 --- a/app/controllers/import/base_controller.rb +++ b/app/controllers/import/base_controller.rb @@ -18,6 +18,7 @@ class Import::BaseController < ApplicationController end # rubocop: enable CodeReuse/ActiveRecord + # deprecated: being replaced by app/services/import/base_service.rb def find_or_create_namespace(names, owner) names = params[:target_namespace].presence || names @@ -32,6 +33,7 @@ class Import::BaseController < ApplicationController current_user.namespace end + # deprecated: being replaced by app/services/import/base_service.rb def project_save_error(project) project.errors.full_messages.join(', ') end diff --git a/app/controllers/import/bitbucket_server_controller.rb b/app/controllers/import/bitbucket_server_controller.rb index 575c40d5f6f..87338488eba 100644 --- a/app/controllers/import/bitbucket_server_controller.rb +++ b/app/controllers/import/bitbucket_server_controller.rb @@ -40,7 +40,7 @@ class Import::BitbucketServerController < Import::BaseController else render json: { errors: 'This namespace has already been taken! Please choose another one.' }, status: :unprocessable_entity end - rescue BitbucketServer::Client::ServerError => e + rescue BitbucketServer::Connection::ConnectionError => e render json: { errors: "Unable to connect to server: #{e}" }, status: :unprocessable_entity end @@ -62,7 +62,7 @@ class Import::BitbucketServerController < Import::BaseController already_added_projects_names = @already_added_projects.pluck(:import_source) @repos.reject! { |repo| already_added_projects_names.include?(repo.browse_url) } - rescue BitbucketServer::Connection::ConnectionError, BitbucketServer::Client::ServerError => e + rescue BitbucketServer::Connection::ConnectionError => e flash[:alert] = "Unable to connect to server: #{e}" clear_session_data redirect_to new_import_bitbucket_server_path diff --git a/app/controllers/import/github_controller.rb b/app/controllers/import/github_controller.rb index d4c26fa0709..34c7dbdc2fe 100644 --- a/app/controllers/import/github_controller.rb +++ b/app/controllers/import/github_controller.rb @@ -39,28 +39,25 @@ class Import::GithubController < Import::BaseController end def create - repo = client.repo(params[:repo_id].to_i) - project_name = params[:new_name].presence || repo.name - namespace_path = params[:target_namespace].presence || current_user.namespace_path - target_namespace = find_or_create_namespace(namespace_path, current_user.namespace_path) - - if can?(current_user, :create_projects, target_namespace) - project = Gitlab::LegacyGithubImport::ProjectCreator - .new(repo, project_name, target_namespace, current_user, access_params, type: provider) - .execute(extra_project_attrs) - - if project.persisted? - render json: ProjectSerializer.new.represent(project) - else - render json: { errors: project_save_error(project) }, status: :unprocessable_entity - end + result = Import::GithubService.new(client, current_user, import_params).execute(access_params, provider) + + if result[:status] == :success + render json: ProjectSerializer.new.represent(result[:project]) else - render json: { errors: 'This namespace has already been taken! Please choose another one.' }, status: :unprocessable_entity + render json: { errors: result[:message] }, status: result[:http_status] end end private + def import_params + params.permit(permitted_import_params) + end + + def permitted_import_params + [:repo_id, :new_name, :target_namespace] + end + def client @client ||= Gitlab::LegacyGithubImport::Client.new(session[access_token_key], client_options) end @@ -124,10 +121,6 @@ class Import::GithubController < Import::BaseController {} end - def extra_project_attrs - {} - end - def extra_import_params {} end diff --git a/app/controllers/projects/badges_controller.rb b/app/controllers/projects/badges_controller.rb index c24bf211760..09a384e89ab 100644 --- a/app/controllers/projects/badges_controller.rb +++ b/app/controllers/projects/badges_controller.rb @@ -21,11 +21,22 @@ class Projects::BadgesController < Projects::ApplicationController private + def badge_layout + case params[:style] + when 'flat' + 'badge' + when 'flat-square' + 'badge_flat-square' + else + 'badge' + end + end + def render_badge(badge) respond_to do |format| format.html { render_404 } format.svg do - render 'badge', locals: { badge: badge.template } + render badge_layout, locals: { badge: badge.template } end end end diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb index ff286c0ccf0..77672e7d9fc 100644 --- a/app/controllers/projects/blob_controller.rb +++ b/app/controllers/projects/blob_controller.rb @@ -93,7 +93,7 @@ class Projects::BlobController < Projects::ApplicationController @blob.load_all_data! @lines = @blob.present.highlight.lines - @form = UnfoldForm.new(params) + @form = UnfoldForm.new(params.to_unsafe_h) @lines = @lines[@form.since - 1..@form.to - 1].map(&:html_safe) diff --git a/app/controllers/projects/error_tracking_controller.rb b/app/controllers/projects/error_tracking_controller.rb index 4596b6c91f2..9e403e1d25b 100644 --- a/app/controllers/projects/error_tracking_controller.rb +++ b/app/controllers/projects/error_tracking_controller.rb @@ -1,9 +1,7 @@ # frozen_string_literal: true class Projects::ErrorTrackingController < Projects::ApplicationController - before_action :check_feature_flag! before_action :authorize_read_sentry_issue! - before_action :push_feature_flag_to_frontend POLLING_INTERVAL = 10_000 @@ -43,12 +41,4 @@ class Projects::ErrorTrackingController < Projects::ApplicationController .new(project: project, user: current_user) .represent(errors) end - - def check_feature_flag! - render_404 unless Feature.enabled?(:error_tracking, project) - end - - def push_feature_flag_to_frontend - push_frontend_feature_flag(:error_tracking, current_user) - end end diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 21688e54481..e3e60665506 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -178,8 +178,6 @@ class Projects::IssuesController < Projects::ApplicationController end def import_csv - return render_404 unless Feature.enabled?(:issues_import_csv) - if uploader = UploadService.new(project, params[:file]).execute ImportIssuesCsvWorker.perform_async(current_user.id, project.id, uploader.upload.id) diff --git a/app/controllers/projects/jobs_controller.rb b/app/controllers/projects/jobs_controller.rb index bfbbcba883f..d5ce790e2d9 100644 --- a/app/controllers/projects/jobs_controller.rb +++ b/app/controllers/projects/jobs_controller.rb @@ -4,10 +4,10 @@ class Projects::JobsController < Projects::ApplicationController include SendFileUpload include ContinueParams - before_action :build, except: [:index, :cancel_all] + before_action :build, except: [:index] before_action :authorize_read_build! before_action :authorize_update_build!, - except: [:index, :show, :status, :raw, :trace, :cancel_all, :erase] + except: [:index, :show, :status, :raw, :trace, :erase] before_action :authorize_erase_build!, only: [:erase] before_action :authorize_use_build_terminal!, only: [:terminal, :terminal_websocket_authorize] before_action :verify_api_request!, only: :terminal_websocket_authorize @@ -39,16 +39,6 @@ class Projects::JobsController < Projects::ApplicationController end # rubocop: enable CodeReuse/ActiveRecord - def cancel_all - return access_denied! unless can?(current_user, :update_build, project) - - @project.builds.running_or_pending.each do |build| - build.cancel if can?(current_user, :update_build, build) - end - - redirect_to project_jobs_path(project) - end - # rubocop: disable CodeReuse/ActiveRecord def show @pipeline = @build.pipeline diff --git a/app/controllers/projects/lfs_locks_api_controller.rb b/app/controllers/projects/lfs_locks_api_controller.rb index fc67cd72faa..6aacb9d9a56 100644 --- a/app/controllers/projects/lfs_locks_api_controller.rb +++ b/app/controllers/projects/lfs_locks_api_controller.rb @@ -4,19 +4,19 @@ class Projects::LfsLocksApiController < Projects::GitHttpClientController include LfsRequest def create - @result = Lfs::LockFileService.new(project, user, params).execute + @result = Lfs::LockFileService.new(project, user, lfs_params).execute render_json(@result[:lock]) end def unlock - @result = Lfs::UnlockFileService.new(project, user, params).execute + @result = Lfs::UnlockFileService.new(project, user, lfs_params).execute render_json(@result[:lock]) end def index - @result = Lfs::LocksFinderService.new(project, user, params).execute + @result = Lfs::LocksFinderService.new(project, user, lfs_params).execute render_json(@result[:locks]) end @@ -69,4 +69,8 @@ class Projects::LfsLocksApiController < Projects::GitHttpClientController def upload_request? %w(create unlock verify).include?(params[:action]) end + + def lfs_params + params.permit(:id, :path, :force) + end end diff --git a/app/controllers/projects/milestones_controller.rb b/app/controllers/projects/milestones_controller.rb index 8e68014a30d..8bc59d8a305 100644 --- a/app/controllers/projects/milestones_controller.rb +++ b/app/controllers/projects/milestones_controller.rb @@ -144,7 +144,7 @@ class Projects::MilestonesController < Projects::ApplicationController def search_params if request.format.json? && project_group && can?(current_user, :read_group, project_group) - groups = project_group.self_and_ancestors_ids + groups = project_group.self_and_ancestors.select(:id) end params.permit(:state).merge(project_ids: @project.id, group_ids: groups) diff --git a/app/controllers/projects/pipelines_settings_controller.rb b/app/controllers/projects/pipelines_settings_controller.rb index 192e6d38f36..7f988110977 100644 --- a/app/controllers/projects/pipelines_settings_controller.rb +++ b/app/controllers/projects/pipelines_settings_controller.rb @@ -4,6 +4,6 @@ class Projects::PipelinesSettingsController < Projects::ApplicationController before_action :authorize_admin_pipeline! def show - redirect_to project_settings_ci_cd_path(@project, params: params) + redirect_to project_settings_ci_cd_path(@project, params: params.to_unsafe_h) end end diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 878816475b2..d3af35723ac 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -10,10 +10,10 @@ class ProjectsController < Projects::ApplicationController prepend_before_action(only: [:show]) { authenticate_sessionless_user!(:rss) } before_action :whitelist_query_limiting, only: [:create] - before_action :authenticate_user!, except: [:index, :show, :activity, :refs] + before_action :authenticate_user!, except: [:index, :show, :activity, :refs, :resolve] before_action :redirect_git_extension, only: [:show] - before_action :project, except: [:index, :new, :create] - before_action :repository, except: [:index, :new, :create] + before_action :project, except: [:index, :new, :create, :resolve] + before_action :repository, except: [:index, :new, :create, :resolve] before_action :assign_ref_vars, only: [:show], if: :repo_exists? before_action :tree, only: [:show], if: [:repo_exists?, :project_view_files?] before_action :lfs_blob_ids, only: [:show], if: [:repo_exists?, :project_view_files?] @@ -442,4 +442,14 @@ class ProjectsController < Projects::ApplicationController def present_project @project = @project.present(current_user: current_user) end + + def resolve + @project = Project.find(params[:id]) + + if can?(current_user, :read_project, @project) + redirect_to @project + else + render_404 + end + end end diff --git a/app/controllers/uploads_controller.rb b/app/controllers/uploads_controller.rb index fa5d84633b5..519e7439205 100644 --- a/app/controllers/uploads_controller.rb +++ b/app/controllers/uploads_controller.rb @@ -70,6 +70,10 @@ class UploadsController < ApplicationController end end + def cache_publicly? + User === model || Appearance === model + end + def upload_model_class MODEL_CLASSES[params[:model]] || raise(UnknownUploadModelError) end diff --git a/app/finders/milestones_finder.rb b/app/finders/milestones_finder.rb index 9c477978f60..fcd54b6106e 100644 --- a/app/finders/milestones_finder.rb +++ b/app/finders/milestones_finder.rb @@ -3,8 +3,8 @@ # Search for milestones # # params - Hash -# project_ids: Array of project ids or single project id. -# group_ids: Array of group ids or single group id. +# project_ids: Array of project ids or single project id or ActiveRecord relation. +# group_ids: Array of group ids or single group id or ActiveRecord relation. # order - Orders by field default due date asc. # title - filter by title. # state - filters by state. @@ -12,17 +12,13 @@ class MilestonesFinder include FinderMethods - attr_reader :params, :project_ids, :group_ids + attr_reader :params def initialize(params = {}) - @project_ids = Array(params[:project_ids]) - @group_ids = Array(params[:group_ids]) @params = params end def execute - return Milestone.none if project_ids.empty? && group_ids.empty? - items = Milestone.all items = by_groups_and_projects(items) items = by_title(items) @@ -34,7 +30,7 @@ class MilestonesFinder private def by_groups_and_projects(items) - items.for_projects_and_groups(project_ids, group_ids) + items.for_projects_and_groups(params[:project_ids], params[:group_ids]) end # rubocop: disable CodeReuse/ActiveRecord diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 82bb2d1a805..9efa84b02f0 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -268,6 +268,17 @@ module ApplicationHelper _('You are on a read-only GitLab instance.') end + def client_class_list + "gl-browser-#{browser.id} gl-platform-#{browser.platform.id}" + end + + def client_js_flags + { + "is#{browser.id.to_s.titlecase}": true, + "is#{browser.platform.id.to_s.titlecase}": true + } + end + def autocomplete_data_sources(object, noteable_type) return {} unless object && noteable_type diff --git a/app/helpers/environments_helper.rb b/app/helpers/environments_helper.rb index 7b22bc8f98f..b3935ae350d 100644 --- a/app/helpers/environments_helper.rb +++ b/app/helpers/environments_helper.rb @@ -7,6 +7,15 @@ module EnvironmentsHelper } end + def environments_folder_list_view_data + { + "endpoint" => folder_project_environments_path(@project, @folder, format: :json), + "folder-name" => @folder, + "can-create-deployment" => can?(current_user, :create_deployment, @project).to_s, + "can-read-environment" => can?(current_user, :read_environment, @project).to_s + } + end + def metrics_data(project, environment) { "settings-path" => edit_project_service_path(project, 'prometheus'), diff --git a/app/helpers/members_helper.rb b/app/helpers/members_helper.rb index 5a21403bc5e..ab4a1ccc0d1 100644 --- a/app/helpers/members_helper.rb +++ b/app/helpers/members_helper.rb @@ -32,7 +32,7 @@ module MembersHelper end def filter_group_project_member_path(options = {}) - options = params.slice(:search, :sort).merge(options) + options = params.slice(:search, :sort).merge(options).permit! "#{request.path}?#{options.to_param}" end end diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index ebbed08f78a..eceee054ede 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -285,7 +285,7 @@ module ProjectsHelper # overridden in EE def settings_operations_available? - Feature.enabled?(:error_tracking, @project) && can?(current_user, :read_environment, @project) + can?(current_user, :read_environment, @project) end private diff --git a/app/helpers/release_blog_post_helper.rb b/app/helpers/release_blog_post_helper.rb new file mode 100644 index 00000000000..31b5b7edc39 --- /dev/null +++ b/app/helpers/release_blog_post_helper.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +module ReleaseBlogPostHelper + def blog_post_url + Gitlab::ReleaseBlogPost.instance.blog_post_url + end +end diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index dc6f8ae1a7f..cfdb3c0d719 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -224,8 +224,15 @@ module Ci before_transition any => [:failed] do |build| next unless build.project + next unless build.deployment - build.deployment&.drop + begin + build.deployment.drop! + rescue => e + Gitlab::Sentry.track_exception(e, extra: { build_id: build.id }) + end + + true end after_transition any => [:failed] do |build| diff --git a/app/models/clusters/applications/ingress.rb b/app/models/clusters/applications/ingress.rb index 7799f069742..7c15aaa4825 100644 --- a/app/models/clusters/applications/ingress.rb +++ b/app/models/clusters/applications/ingress.rb @@ -3,7 +3,7 @@ module Clusters module Applications class Ingress < ActiveRecord::Base - VERSION = '0.23.0'.freeze + VERSION = '1.1.2'.freeze self.table_name = 'clusters_applications_ingress' diff --git a/app/models/clusters/applications/prometheus.rb b/app/models/clusters/applications/prometheus.rb index e25be522d68..26bf73f4dd8 100644 --- a/app/models/clusters/applications/prometheus.rb +++ b/app/models/clusters/applications/prometheus.rb @@ -5,7 +5,8 @@ module Clusters class Prometheus < ActiveRecord::Base include PrometheusAdapter - VERSION = '6.7.3'.freeze + VERSION = '6.7.3' + READY_STATUS = [:installed, :updating, :updated, :update_errored].freeze self.table_name = 'clusters_applications_prometheus' @@ -24,12 +25,8 @@ module Clusters end end - def ready_status - [:installed] - end - def ready? - ready_status.include?(status_name) + READY_STATUS.include?(status_name) end def chart @@ -55,6 +52,24 @@ module Clusters ) end + def upgrade_command(values) + ::Gitlab::Kubernetes::Helm::UpgradeCommand.new( + name, + version: VERSION, + chart: chart, + rbac: cluster.platform_kubernetes_rbac?, + files: files_with_replaced_values(values) + ) + end + + # Returns a copy of files where the values of 'values.yaml' + # are replaced by the argument. + # + # See #values for the data format required + def files_with_replaced_values(replaced_values) + files.merge('values.yaml': replaced_values) + end + def prometheus_client return unless kube_client diff --git a/app/models/clusters/applications/runner.rb b/app/models/clusters/applications/runner.rb index 0c0247da1fb..f17da0bb7b1 100644 --- a/app/models/clusters/applications/runner.rb +++ b/app/models/clusters/applications/runner.rb @@ -3,7 +3,7 @@ module Clusters module Applications class Runner < ActiveRecord::Base - VERSION = '0.1.43'.freeze + VERSION = '0.1.45'.freeze self.table_name = 'clusters_applications_runners' diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb index 6050955fbd8..a2c48973fa5 100644 --- a/app/models/clusters/cluster.rb +++ b/app/models/clusters/cluster.rb @@ -49,8 +49,9 @@ module Clusters validates :name, cluster_name: true validates :cluster_type, presence: true - validate :restrict_modification, on: :update + validates :domain, allow_nil: true, hostname: { allow_numeric_hostname: true, require_valid_tld: true } + validate :restrict_modification, on: :update validate :no_groups, unless: :group_type? validate :no_projects, unless: :project_type? diff --git a/app/models/clusters/concerns/application_status.rb b/app/models/clusters/concerns/application_status.rb index 0e74cce29b7..a556dd5ad8b 100644 --- a/app/models/clusters/concerns/application_status.rb +++ b/app/models/clusters/concerns/application_status.rb @@ -77,6 +77,10 @@ module Clusters def available? installed? || updated? end + + def update_in_progress? + updating? + end end end end diff --git a/app/models/concerns/cache_markdown_field.rb b/app/models/concerns/cache_markdown_field.rb index a8c9e54f00c..73a27326f6c 100644 --- a/app/models/concerns/cache_markdown_field.rb +++ b/app/models/concerns/cache_markdown_field.rb @@ -15,7 +15,7 @@ module CacheMarkdownField # Increment this number every time the renderer changes its output CACHE_REDCARPET_VERSION = 3 CACHE_COMMONMARK_VERSION_START = 10 - CACHE_COMMONMARK_VERSION = 12 + CACHE_COMMONMARK_VERSION = 13 # changes to these attributes cause the cache to be invalidates INVALIDATED_BY = %w[author project].freeze diff --git a/app/models/external_issue.rb b/app/models/external_issue.rb index 4f73beaafc5..68b2353556e 100644 --- a/app/models/external_issue.rb +++ b/app/models/external_issue.rb @@ -3,6 +3,8 @@ class ExternalIssue include Referable + attr_reader :project + def initialize(issue_identifier, project) @issue_identifier, @project = issue_identifier, project end @@ -32,12 +34,8 @@ class ExternalIssue [self.class, to_s].hash end - def project - @project - end - def project_id - @project.id + project.id end def to_reference(_from = nil, full: nil) diff --git a/app/models/group.rb b/app/models/group.rb index edac2444c4d..22814e35ca8 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -382,6 +382,10 @@ class Group < Namespace end end + def highest_group_member(user) + GroupMember.where(source_id: self_and_ancestors_ids, user_id: user.id).order(:access_level).last + end + def hashed_storage?(_feature) false end diff --git a/app/models/issue.rb b/app/models/issue.rb index b7e13bcbccf..5c4ecbfdf4e 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -230,7 +230,8 @@ class Issue < ActiveRecord::Base end def check_for_spam? - project.public? && (title_changed? || description_changed?) + publicly_visible? && + (title_changed? || description_changed? || confidential_changed?) end def as_json(options = {}) diff --git a/app/models/label.rb b/app/models/label.rb index 5d2d1afd1d9..1c3db3eb35d 100644 --- a/app/models/label.rb +++ b/app/models/label.rb @@ -214,6 +214,7 @@ class Label < ActiveRecord::Base super(options).tap do |json| json[:type] = self.try(:type) json[:priority] = priority(options[:project]) if options.key?(:project) + json[:textColor] = text_color end end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index a9d1ece0d7e..7206d858dae 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -560,15 +560,19 @@ class MergeRequest < ActiveRecord::Base end def diff_refs - if persisted? - merge_request_diff.diff_refs - else - Gitlab::Diff::DiffRefs.new( - base_sha: diff_base_sha, - start_sha: diff_start_sha, - head_sha: diff_head_sha - ) - end + persisted? ? merge_request_diff.diff_refs : repository_diff_refs + end + + # Instead trying to fetch the + # persisted diff_refs, this method goes + # straight to the repository to get the + # most recent data possible. + def repository_diff_refs + Gitlab::Diff::DiffRefs.new( + base_sha: branch_merge_base_sha, + start_sha: target_branch_sha, + head_sha: source_branch_sha + ) end def branch_merge_base_sha diff --git a/app/models/milestone.rb b/app/models/milestone.rb index 1ebcbcda0d8..b21edce3aad 100644 --- a/app/models/milestone.rb +++ b/app/models/milestone.rb @@ -45,7 +45,7 @@ class Milestone < ActiveRecord::Base groups = groups.compact if groups.is_a? Array groups = [] if groups.nil? - where(project: projects).or(where(group: groups)) + where(project_id: projects).or(where(group_id: groups)) end scope :order_by_name_asc, -> { order(Arel::Nodes::Ascending.new(arel_table[:title].lower)) } @@ -191,7 +191,7 @@ class Milestone < ActiveRecord::Base return STATE_COUNT_HASH unless projects || groups counts = Milestone - .for_projects_and_groups(projects&.map(&:id), groups&.map(&:id)) + .for_projects_and_groups(projects, groups) .reorder(nil) .group(:state) .count @@ -275,8 +275,7 @@ class Milestone < ActiveRecord::Base if project relation = Milestone.for_projects_and_groups([project_id], [project.group&.id]) elsif group - project_ids = group.projects.map(&:id) - relation = Milestone.for_projects_and_groups(project_ids, [group.id]) + relation = Milestone.for_projects_and_groups(group.projects.select(:id), [group.id]) end title_exists = relation.find_by_title(title) diff --git a/app/models/project.rb b/app/models/project.rb index 7ab2fc30c24..15465d9b356 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -331,7 +331,7 @@ class Project < ActiveRecord::Base ports: ->(project) { project.persisted? ? VALID_MIRROR_PORTS : VALID_IMPORT_PORTS }, enforce_user: true }, if: [:external_import?, :import_url_changed?] validates :star_count, numericality: { greater_than_or_equal_to: 0 } - validate :check_limit, on: :create + validate :check_personal_projects_limit, on: :create validate :check_repository_path_availability, on: :update, if: ->(project) { project.renamed? } validate :visibility_level_allowed_by_group, if: -> { changes.has_key?(:visibility_level) } validate :visibility_level_allowed_as_fork, if: -> { changes.has_key?(:visibility_level) } @@ -809,18 +809,22 @@ class Project < ActiveRecord::Base ::Gitlab::CurrentSettings.mirror_available end - def check_limit - unless creator.can_create_project? || namespace.kind == 'group' - projects_limit = creator.projects_limit + def check_personal_projects_limit + # Since this method is called as validation hook, `creator` might not be + # present. Since the validation for that will fail, we can just return + # early. + return if !creator || creator.can_create_project? || + namespace.kind == 'group' - if projects_limit == 0 - self.errors.add(:limit_reached, "Personal project creation is not allowed. Please contact your administrator with questions") + limit = creator.projects_limit + error = + if limit.zero? + _('Personal project creation is not allowed. Please contact your administrator with questions') else - self.errors.add(:limit_reached, "Your project limit is #{projects_limit} projects! Please contact your administrator to increase it") + _('Your project limit is %{limit} projects! Please contact your administrator to increase it') end - end - rescue - self.errors.add(:base, "Can't check your ability to create project") + + self.errors.add(:limit_reached, error % { limit: limit }) end def visibility_level_allowed_by_group @@ -1531,7 +1535,7 @@ class Project < ActiveRecord::Base end def pages_available? - Gitlab.config.pages.enabled && !namespace.subgroup? + Gitlab.config.pages.enabled end def remove_private_deploy_keys @@ -1602,24 +1606,7 @@ class Project < ActiveRecord::Base # rubocop: disable CodeReuse/ServiceClass def after_create_default_branch - return unless default_branch - - # Ensure HEAD points to the default branch in case it is not master - change_head(default_branch) - - if Gitlab::CurrentSettings.default_branch_protection != Gitlab::Access::PROTECTION_NONE && !ProtectedBranch.protected?(self, default_branch) - params = { - name: default_branch, - push_access_levels_attributes: [{ - access_level: Gitlab::CurrentSettings.default_branch_protection == Gitlab::Access::PROTECTION_DEV_CAN_PUSH ? Gitlab::Access::DEVELOPER : Gitlab::Access::MAINTAINER - }], - merge_access_levels_attributes: [{ - access_level: Gitlab::CurrentSettings.default_branch_protection == Gitlab::Access::PROTECTION_DEV_CAN_MERGE ? Gitlab::Access::DEVELOPER : Gitlab::Access::MAINTAINER - }] - } - - ProtectedBranches::CreateService.new(self, creator, params).execute(skip_authorization: true) - end + Projects::ProtectDefaultBranchService.new(self).execute end # rubocop: enable CodeReuse/ServiceClass @@ -2039,6 +2026,10 @@ class Project < ActiveRecord::Base pool_repository&.unlink_repository(repository) && update_column(:pool_repository_id, nil) end + def link_pool_repository + pool_repository&.link_repository(repository) + end + private def merge_requests_allowing_collaboration(source_branch = nil) diff --git a/app/models/project_import_data.rb b/app/models/project_import_data.rb index 525725034a5..aa0c121fe99 100644 --- a/app/models/project_import_data.rb +++ b/app/models/project_import_data.rb @@ -30,4 +30,8 @@ class ProjectImportData < ActiveRecord::Base def merge_credentials(hash) self.credentials = credentials.to_h.merge(hash) unless hash.empty? end + + def clear_credentials + self.credentials = {} + end end diff --git a/app/models/remote_mirror.rb b/app/models/remote_mirror.rb index a3fa67c72bf..5eba7ddd75c 100644 --- a/app/models/remote_mirror.rb +++ b/app/models/remote_mirror.rb @@ -61,7 +61,10 @@ class RemoteMirror < ActiveRecord::Base timestamp = Time.now remote_mirror.update!( - last_update_at: timestamp, last_successful_update_at: timestamp, last_error: nil + last_update_at: timestamp, + last_successful_update_at: timestamp, + last_error: nil, + error_notification_sent: false ) end @@ -179,6 +182,10 @@ class RemoteMirror < ActiveRecord::Base project.repository.add_remote(remote_name, remote_url) end + def after_sent_notification + update_column(:error_notification_sent, true) + end + private def store_credentials @@ -221,7 +228,8 @@ class RemoteMirror < ActiveRecord::Base last_error: nil, last_update_at: nil, last_successful_update_at: nil, - update_status: 'finished' + update_status: 'finished', + error_notification_sent: false ) end diff --git a/app/models/storage/hashed_project.rb b/app/models/storage/hashed_project.rb index 911fb7e9ce9..f5d0d6fab3b 100644 --- a/app/models/storage/hashed_project.rb +++ b/app/models/storage/hashed_project.rb @@ -31,7 +31,7 @@ module Storage gitlab_shell.add_namespace(repository_storage, base_dir) end - def rename_repo + def rename_repo(old_full_path: nil, new_full_path: nil) true end diff --git a/app/models/storage/legacy_project.rb b/app/models/storage/legacy_project.rb index 9f6f19acb41..76ac5c13c18 100644 --- a/app/models/storage/legacy_project.rb +++ b/app/models/storage/legacy_project.rb @@ -29,18 +29,19 @@ module Storage gitlab_shell.add_namespace(repository_storage, base_dir) end - def rename_repo - new_full_path = project.build_full_path + def rename_repo(old_full_path: nil, new_full_path: nil) + old_full_path ||= project.full_path_was + new_full_path ||= project.build_full_path - if gitlab_shell.mv_repository(repository_storage, project.full_path_was, new_full_path) + if gitlab_shell.mv_repository(repository_storage, old_full_path, new_full_path) # If repository moved successfully we need to send update instructions to users. # However we cannot allow rollback since we moved repository # So we basically we mute exceptions in next actions begin - gitlab_shell.mv_repository(repository_storage, "#{project.full_path_was}.wiki", "#{new_full_path}.wiki") + gitlab_shell.mv_repository(repository_storage, "#{old_full_path}.wiki", "#{new_full_path}.wiki") return true rescue => e - Rails.logger.error "Exception renaming #{project.full_path_was} -> #{new_full_path}: #{e}" + Rails.logger.error "Exception renaming #{old_full_path} -> #{new_full_path}: #{e}" # Returning false does not rollback after_* transaction but gives # us information about failing some of tasks return false diff --git a/app/models/suggestion.rb b/app/models/suggestion.rb index c76b8e71507..7eee4fbbe5f 100644 --- a/app/models/suggestion.rb +++ b/app/models/suggestion.rb @@ -5,8 +5,7 @@ class Suggestion < ApplicationRecord validates :note, presence: true validates :commit_id, presence: true, if: :applied? - delegate :original_position, :position, :diff_file, - :noteable, to: :note + delegate :original_position, :position, :noteable, to: :note def project noteable.source_project @@ -16,6 +15,15 @@ class Suggestion < ApplicationRecord noteable.source_branch end + def file_path + position.file_path + end + + def diff_file + repository = project.repository + position.diff_file(repository) + end + # For now, suggestions only serve as a way to send patches that # will change a single line (being able to apply multiple in the same place), # which explains `from_line` and `to_line` being the same line. diff --git a/app/serializers/base_serializer.rb b/app/serializers/base_serializer.rb index 7b65bd22f54..4744a7c1cc8 100644 --- a/app/serializers/base_serializer.rb +++ b/app/serializers/base_serializer.rb @@ -16,11 +16,11 @@ class BaseSerializer .as_json end - def self.entity(entity_class) - @entity_class ||= entity_class - end + class << self + attr_reader :entity_class - def self.entity_class - @entity_class + def entity(entity_class) + @entity_class ||= entity_class + end end end diff --git a/app/serializers/merge_request_diff_entity.rb b/app/serializers/merge_request_diff_entity.rb index 433bfe60474..7e3053e5881 100644 --- a/app/serializers/merge_request_diff_entity.rb +++ b/app/serializers/merge_request_diff_entity.rb @@ -24,6 +24,14 @@ class MergeRequestDiffEntity < Grape::Entity short_sha(merge_request_diff.head_commit_sha) end + expose :base_version_path do |merge_request_diff| + project = merge_request.target_project + + next unless project + + merge_request_version_path(project, merge_request, merge_request_diff) + end + expose :version_path do |merge_request_diff| start_sha = options[:start_sha] project = merge_request.target_project diff --git a/app/services/clusters/applications/base_helm_service.rb b/app/services/clusters/applications/base_helm_service.rb index e86ca8cf1d0..8a71730d5ec 100644 --- a/app/services/clusters/applications/base_helm_service.rb +++ b/app/services/clusters/applications/base_helm_service.rb @@ -45,6 +45,10 @@ module Clusters def install_command @install_command ||= app.install_command end + + def upgrade_command(new_values = "") + app.upgrade_command(new_values) + end end end end diff --git a/app/services/import/base_service.rb b/app/services/import/base_service.rb new file mode 100644 index 00000000000..2683c75e41f --- /dev/null +++ b/app/services/import/base_service.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +module Import + class BaseService < ::BaseService + def initialize(client, user, params) + @client = client + @current_user = user + @params = params + end + + private + + def find_or_create_namespace(namespace, owner) + namespace = params[:target_namespace].presence || namespace + + return current_user.namespace if namespace == owner + + group = Groups::NestedCreateService.new(current_user, group_path: namespace).execute + + group.errors.any? ? current_user.namespace : group + rescue => e + Gitlab::AppLogger.error(e) + + current_user.namespace + end + + def project_save_error(project) + project.errors.full_messages.join(', ') + end + + def success(project) + super().merge(project: project, status: :success) + end + end +end diff --git a/app/services/import/github_service.rb b/app/services/import/github_service.rb new file mode 100644 index 00000000000..a2533683da9 --- /dev/null +++ b/app/services/import/github_service.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +module Import + class GithubService < Import::BaseService + attr_accessor :client + attr_reader :params, :current_user + + def execute(access_params, provider) + unless authorized? + return error('This namespace has already been taken! Please choose another one.', :unprocessable_entity) + end + + project = Gitlab::LegacyGithubImport::ProjectCreator + .new(repo, project_name, target_namespace, current_user, access_params, type: provider) + .execute(extra_project_attrs) + + if project.persisted? + success(project) + else + error(project_save_error(project), :unprocessable_entity) + end + end + + def repo + @repo ||= client.repo(params[:repo_id].to_i) + end + + def project_name + @project_name ||= params[:new_name].presence || repo.name + end + + def namespace_path + @namespace_path ||= params[:target_namespace].presence || current_user.namespace_path + end + + def target_namespace + @target_namespace ||= find_or_create_namespace(namespace_path, current_user.namespace_path) + end + + def extra_project_attrs + {} + end + + def authorized? + can?(current_user, :create_projects, target_namespace) + end + end +end diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb index c7e7bb55e4b..805bb5b510d 100644 --- a/app/services/issuable_base_service.rb +++ b/app/services/issuable_base_service.rb @@ -61,10 +61,10 @@ class IssuableBaseService < BaseService return unless milestone_id params[:milestone_id] = '' if milestone_id == IssuableFinder::NONE - group_ids = project.group&.self_and_ancestors&.pluck(:id) + groups = project.group&.self_and_ancestors&.select(:id) milestone = - Milestone.for_projects_and_groups([project.id], group_ids).find_by_id(milestone_id) + Milestone.for_projects_and_groups([project.id], groups).find_by_id(milestone_id) params[:milestone_id] = '' unless milestone end diff --git a/app/services/labels/create_service.rb b/app/services/labels/create_service.rb index fe34be41ac1..db710bac900 100644 --- a/app/services/labels/create_service.rb +++ b/app/services/labels/create_service.rb @@ -3,7 +3,7 @@ module Labels class CreateService < Labels::BaseService def initialize(params = {}) - @params = params.dup.with_indifferent_access + @params = params.to_h.dup.with_indifferent_access end # returns the created label diff --git a/app/services/labels/update_service.rb b/app/services/labels/update_service.rb index c3a720a1c66..e563447c64c 100644 --- a/app/services/labels/update_service.rb +++ b/app/services/labels/update_service.rb @@ -3,7 +3,7 @@ module Labels class UpdateService < Labels::BaseService def initialize(params = {}) - @params = params.dup.with_indifferent_access + @params = params.to_h.dup.with_indifferent_access end # returns the updated label diff --git a/app/services/lfs/locks_finder_service.rb b/app/services/lfs/locks_finder_service.rb index 4a5b2a52921..192ce3d3c2a 100644 --- a/app/services/lfs/locks_finder_service.rb +++ b/app/services/lfs/locks_finder_service.rb @@ -12,7 +12,7 @@ module Lfs # rubocop: disable CodeReuse/ActiveRecord def find_locks - options = params.slice(:id, :path).compact.symbolize_keys + options = params.slice(:id, :path).to_h.compact.symbolize_keys project.lfs_file_locks.where(options) end diff --git a/app/services/milestones/promote_service.rb b/app/services/milestones/promote_service.rb index 39071b5dc14..cbe5996e8ca 100644 --- a/app/services/milestones/promote_service.rb +++ b/app/services/milestones/promote_service.rb @@ -82,11 +82,9 @@ module Milestones end # rubocop: enable CodeReuse/ActiveRecord - # rubocop: disable CodeReuse/ActiveRecord def group_project_ids - @group_project_ids ||= group.projects.pluck(:id) + group.projects.select(:id) end - # rubocop: enable CodeReuse/ActiveRecord def raise_error(message) raise PromoteMilestoneError, "Promotion failed - #{message}" diff --git a/app/services/projects/after_rename_service.rb b/app/services/projects/after_rename_service.rb index aa9b253eb20..c3cd9d1ea4a 100644 --- a/app/services/projects/after_rename_service.rb +++ b/app/services/projects/after_rename_service.rb @@ -12,22 +12,27 @@ module Projects # # Projects::AfterRenameService.new(project).execute class AfterRenameService - attr_reader :project, :full_path_before, :full_path_after, :path_before + # @return [String] The Project being renamed. + attr_reader :project - RenameFailedError = Class.new(StandardError) + # @return [String] The path slug the project was using, before the rename took place. + attr_reader :path_before - # @param [Project] project The Project of the repository to rename. - def initialize(project) - @project = project + # @return [String] The full path of the namespace + project, before the rename took place. + attr_reader :full_path_before - # The full path of the namespace + project, before the rename took place. - @full_path_before = project.full_path_was + # @return [String] The full path of the namespace + project, after the rename took place. + attr_reader :full_path_after - # The full path of the namespace + project, after the rename took place. - @full_path_after = project.build_full_path + RenameFailedError = Class.new(StandardError) - # The path of just the project, before the rename took place. - @path_before = project.path_was + # @param [Project] project The Project being renamed. + # @param [String] path_before The path slug the project was using, before the rename took place. + def initialize(project, path_before:, full_path_before:) + @project = project + @path_before = path_before + @full_path_before = full_path_before + @full_path_after = project.full_path end def execute @@ -61,7 +66,7 @@ module Projects .new(project, full_path_before) .execute else - project.storage.rename_repo + project.storage.rename_repo(old_full_path: full_path_before, new_full_path: full_path_after) end rename_failed! unless success diff --git a/app/services/projects/autocomplete_service.rb b/app/services/projects/autocomplete_service.rb index 61f6402a810..3dad90188cf 100644 --- a/app/services/projects/autocomplete_service.rb +++ b/app/services/projects/autocomplete_service.rb @@ -14,7 +14,7 @@ module Projects order: { due_date: :asc, title: :asc } } - finder_params[:group_ids] = @project.group.self_and_ancestors_ids if @project.group + finder_params[:group_ids] = @project.group.self_and_ancestors.select(:id) if @project.group MilestonesFinder.new(finder_params).execute.select([:iid, :title]) end diff --git a/app/services/projects/create_from_template_service.rb b/app/services/projects/create_from_template_service.rb index 8306d43ca7c..678bc0d24c3 100644 --- a/app/services/projects/create_from_template_service.rb +++ b/app/services/projects/create_from_template_service.rb @@ -5,7 +5,7 @@ module Projects include Gitlab::Utils::StrongMemoize def initialize(user, params) - @current_user, @params = user, params.dup + @current_user, @params = user, params.to_h.dup end def execute diff --git a/app/services/projects/destroy_service.rb b/app/services/projects/destroy_service.rb index 336d029d330..b14b31302f5 100644 --- a/app/services/projects/destroy_service.rb +++ b/app/services/projects/destroy_service.rb @@ -7,9 +7,16 @@ module Projects DestroyError = Class.new(StandardError) DELETED_FLAG = '+deleted'.freeze + REPO_REMOVAL_DELAY = 5.minutes.to_i def async_execute project.update_attribute(:pending_delete, true) + + # Ensure no repository +deleted paths are kept, + # regardless of any issue with the ProjectDestroyWorker + # job process. + schedule_stale_repos_removal + job_id = ProjectDestroyWorker.perform_async(project.id, current_user.id, params) Rails.logger.info("User #{current_user.id} scheduled destruction of project #{project.full_path} with job ID #{job_id}") end @@ -92,14 +99,23 @@ module Projects log_info(%Q{Repository "#{path}" moved to "#{new_path}" for project "#{project.full_path}"}) project.run_after_commit do - # self is now project - GitlabShellWorker.perform_in(5.minutes, :remove_repository, self.repository_storage, new_path) + GitlabShellWorker.perform_in(REPO_REMOVAL_DELAY, :remove_repository, self.repository_storage, new_path) end else false end end + def schedule_stale_repos_removal + repo_paths = [removal_path(repo_path), removal_path(wiki_path)] + + # Ideally it should wait until the regular removal phase finishes, + # so let's delay it a bit further. + repo_paths.each do |path| + GitlabShellWorker.perform_in(REPO_REMOVAL_DELAY * 2, :remove_repository, project.repository_storage, path) + end + end + def rollback_repository(old_path, new_path) # There is a possibility project does not have repository or wiki return true unless repo_exists?(old_path) @@ -113,13 +129,11 @@ module Projects end # rubocop: enable CodeReuse/ActiveRecord - # rubocop: disable CodeReuse/ActiveRecord def mv_repository(from_path, to_path) - return true unless gitlab_shell.exists?(project.repository_storage, from_path + '.git') + return true unless repo_exists?(from_path) gitlab_shell.mv_repository(project.repository_storage, from_path, to_path) end - # rubocop: enable CodeReuse/ActiveRecord def attempt_rollback(project, message) return unless project diff --git a/app/services/projects/protect_default_branch_service.rb b/app/services/projects/protect_default_branch_service.rb new file mode 100644 index 00000000000..245490791bf --- /dev/null +++ b/app/services/projects/protect_default_branch_service.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +module Projects + # Service class that can be used to execute actions necessary after creating a + # default branch. + class ProtectDefaultBranchService + attr_reader :project, :default_branch_protection + + # @param [Project] project + def initialize(project) + @project = project + + @default_branch_protection = Gitlab::Access::BranchProtection + .new(Gitlab::CurrentSettings.default_branch_protection) + end + + def execute + protect_default_branch if default_branch + end + + def protect_default_branch + # Ensure HEAD points to the default branch in case it is not master + project.change_head(default_branch) + + create_protected_branch if protect_branch? + end + + def create_protected_branch + params = { + name: default_branch, + push_access_levels_attributes: [{ access_level: push_access_level }], + merge_access_levels_attributes: [{ access_level: merge_access_level }] + } + + # The creator of the project is always allowed to create protected + # branches, so we skip the authorization check in this service class. + ProtectedBranches::CreateService + .new(project, project.creator, params) + .execute(skip_authorization: true) + end + + def protect_branch? + default_branch_protection.any? && + !ProtectedBranch.protected?(project, default_branch) + end + + def default_branch + project.default_branch + end + + def push_access_level + if default_branch_protection.developer_can_push? + Gitlab::Access::DEVELOPER + else + Gitlab::Access::MAINTAINER + end + end + + def merge_access_level + if default_branch_protection.developer_can_merge? + Gitlab::Access::DEVELOPER + else + Gitlab::Access::MAINTAINER + end + end + end +end diff --git a/app/services/projects/update_service.rb b/app/services/projects/update_service.rb index dd1b9680ece..6856009b395 100644 --- a/app/services/projects/update_service.rb +++ b/app/services/projects/update_service.rb @@ -67,7 +67,7 @@ module Projects end if project.previous_changes.include?('path') - AfterRenameService.new(project).execute + after_rename_service(project).execute else system_hook_service.execute_hooks_for(project, :update) end @@ -75,6 +75,13 @@ module Projects update_pages_config if changing_pages_related_config? end + def after_rename_service(project) + # The path slug the project was using, before the rename took place. + path_before = project.previous_changes['path'].first + + AfterRenameService.new(project, path_before: path_before, full_path_before: project.full_path_was) + end + def changing_pages_related_config? changing_pages_https_only? || changing_pages_access_level? end diff --git a/app/services/suggestions/apply_service.rb b/app/services/suggestions/apply_service.rb index d931d528c86..cc47b46b527 100644 --- a/app/services/suggestions/apply_service.rb +++ b/app/services/suggestions/apply_service.rb @@ -11,6 +11,10 @@ module Suggestions return error('Suggestion is not appliable') end + unless latest_diff_refs?(suggestion) + return error('The file has been changed') + end + params = file_update_params(suggestion) result = ::Files::UpdateService.new(suggestion.project, @current_user, params).execute @@ -19,30 +23,44 @@ module Suggestions end result + rescue Files::UpdateService::FileChangedError + error('The file has been changed') end private - def file_update_params(suggestion) - diff_file = suggestion.diff_file + # Checks whether the latest diff refs for the branch matches with + # the position refs we're using to update the file content. Since + # the persisted refs are updated async (for MergeRequest), + # it's more consistent to fetch this data directly from the repository. + def latest_diff_refs?(suggestion) + suggestion.position.diff_refs == suggestion.noteable.repository_diff_refs + end - file_path = diff_file.file_path - branch_name = suggestion.noteable.source_branch - file_content = new_file_content(suggestion) + def file_update_params(suggestion) + blob = suggestion.diff_file.new_blob + file_path = suggestion.file_path + branch_name = suggestion.branch + file_content = new_file_content(suggestion, blob) commit_message = "Apply suggestion to #{file_path}" + file_last_commit = + Gitlab::Git::Commit.last_for_path(suggestion.project.repository, + blob.commit_id, + blob.path) + { file_path: file_path, branch_name: branch_name, start_branch: branch_name, commit_message: commit_message, - file_content: file_content + file_content: file_content, + last_commit_sha: file_last_commit&.id } end - def new_file_content(suggestion) + def new_file_content(suggestion, blob) range = suggestion.from_line_index..suggestion.to_line_index - blob = suggestion.diff_file.new_blob blob.load_all_data! content = blob.data.lines diff --git a/app/services/users/update_service.rb b/app/services/users/update_service.rb index af4fe1aebb9..0b00bd135eb 100644 --- a/app/services/users/update_service.rb +++ b/app/services/users/update_service.rb @@ -55,7 +55,7 @@ module Users params.reject! { |key, _| read_only.include?(key.to_sym) } end - @user.assign_attributes(params) if params.any? + @user.assign_attributes(params) unless params.empty? end end end diff --git a/app/uploaders/personal_file_uploader.rb b/app/uploaders/personal_file_uploader.rb index 25474b494ff..272837aa6ce 100644 --- a/app/uploaders/personal_file_uploader.rb +++ b/app/uploaders/personal_file_uploader.rb @@ -6,8 +6,15 @@ class PersonalFileUploader < FileUploader options.storage_path end - def self.base_dir(model, _store = nil) - File.join(options.base_dir, model_path_segment(model)) + def self.base_dir(model, store = nil) + base_dirs(model)[store || Store::LOCAL] + end + + def self.base_dirs(model) + { + Store::LOCAL => File.join(options.base_dir, model_path_segment(model)), + Store::REMOTE => model_path_segment(model) + } end def self.model_path_segment(model) @@ -33,13 +40,6 @@ class PersonalFileUploader < FileUploader store_dirs[object_store] end - def store_dirs - { - Store::LOCAL => File.join(base_dir, dynamic_segment), - Store::REMOTE => File.join(self.class.model_path_segment(model), dynamic_segment) - } - end - private def secure_url diff --git a/app/uploaders/records_uploads.rb b/app/uploaders/records_uploads.rb index 0efca895a50..9a243e07936 100644 --- a/app/uploaders/records_uploads.rb +++ b/app/uploaders/records_uploads.rb @@ -23,13 +23,23 @@ module RecordsUploads return unless model return unless file && file.exists? - Upload.transaction do - uploads.where(path: upload_path).delete_all - upload.delete if upload - - self.upload = build_upload.tap(&:save!) + # MySQL InnoDB may encounter a deadlock if a deletion and an + # insert is in the same transaction due to its next-key locking + # algorithm, so we need to skip the transaction. + # https://gitlab.com/gitlab-org/gitlab-ce/issues/55161#note_131556351 + if Gitlab::Database.mysql? + readd_upload + else + Upload.transaction { readd_upload } end end + + def readd_upload + uploads.where(path: upload_path).delete_all + upload.delete if upload + + self.upload = build_upload.tap(&:save!) + end # rubocop: enable CodeReuse/ActiveRecord def upload_path diff --git a/app/views/clusters/clusters/_integration_form.html.haml b/app/views/clusters/clusters/_integration_form.html.haml index 5e451f60c9d..4c47e11927e 100644 --- a/app/views/clusters/clusters/_integration_form.html.haml +++ b/app/views/clusters/clusters/_integration_form.html.haml @@ -13,19 +13,19 @@ = sprite_icon('status_failed_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-unchecked') .form-text.text-muted= s_('ClusterIntegration|Enable or disable GitLab\'s connection to your Kubernetes cluster.') - - if has_multiple_clusters? - .form-group - %h5= s_('ClusterIntegration|Environment scope') + .form-group + %h5= s_('ClusterIntegration|Environment scope') + - if has_multiple_clusters? = field.text_field :environment_scope, class: 'col-md-6 form-control js-select-on-focus', placeholder: s_('ClusterIntegration|Environment scope') .form-text.text-muted= s_("ClusterIntegration|Choose which of your environments will use this cluster.") + - else + = text_field_tag :environment_scope, '*', class: 'col-md-6 form-control disabled', placeholder: s_('ClusterIntegration|Environment scope'), disabled: true + - environment_scope_url = 'https://docs.gitlab.com/ee/user/project/clusters/#setting-the-environment-scope-premium' + - environment_scope_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: environment_scope_url } + .form-text.text-muted + %code * + = s_("ClusterIntegration| is the default environment scope for this cluster. This means that all jobs, regardless of their environment, will use this cluster. %{environment_scope_start}More information%{environment_scope_end}").html_safe % { environment_scope_start: environment_scope_start, environment_scope_end: '</a>'.html_safe } - if can?(current_user, :update_cluster, @cluster) .form-group = field.submit _('Save changes'), class: 'btn btn-success' - - - unless has_multiple_clusters? - %h5= s_('ClusterIntegration|Environment scope') - %p - %code * - is the default environment scope for this cluster. This means that all jobs, regardless of their environment, will use this cluster. - = link_to 'More information', ('https://docs.gitlab.com/ee/user/project/clusters/#setting-the-environment-scope') diff --git a/app/views/dashboard/_projects_head.html.haml b/app/views/dashboard/_projects_head.html.haml index 1050945b15a..ae67192cbcd 100644 --- a/app/views/dashboard/_projects_head.html.haml +++ b/app/views/dashboard/_projects_head.html.haml @@ -15,9 +15,11 @@ = nav_link(page: [dashboard_projects_path, root_path]) do = link_to dashboard_projects_path, class: 'shortcuts-activity', data: {placement: 'right'} do Your projects + %span.badge.badge-pill= limited_counter_with_delimiter(@total_user_projects_count) = nav_link(page: starred_dashboard_projects_path) do = link_to starred_dashboard_projects_path, data: {placement: 'right'} do Starred projects + %span.badge.badge-pill= limited_counter_with_delimiter(@total_starred_projects_count) = nav_link(page: [explore_root_path, trending_explore_projects_path, starred_explore_projects_path, explore_projects_path]) do = link_to explore_root_path, data: {placement: 'right'} do Explore projects diff --git a/app/views/groups/labels/index.html.haml b/app/views/groups/labels/index.html.haml index 4df3d831942..5cf3193bc62 100644 --- a/app/views/groups/labels/index.html.haml +++ b/app/views/groups/labels/index.html.haml @@ -6,15 +6,10 @@ - subscribed = params[:subscribed] - labels_or_filters = @labels.exists? || search.present? || subscribed.present? -- if @labels.present? && can_admin_label - - content_for(:header_content) do - .nav-controls - = link_to _('New label'), new_group_label_path(@group), class: "btn btn-success" - - if labels_or_filters #promote-label-modal %div{ class: container_class } - = render 'shared/labels/nav' + = render 'shared/labels/nav', labels_or_filters: labels_or_filters, can_admin_label: can_admin_label .labels-container.prepend-top-5 - if @labels.any? diff --git a/app/views/layouts/_init_client_detection_flags.html.haml b/app/views/layouts/_init_client_detection_flags.html.haml new file mode 100644 index 00000000000..c729f8aa696 --- /dev/null +++ b/app/views/layouts/_init_client_detection_flags.html.haml @@ -0,0 +1,7 @@ +- client = client_js_flags + +- if client + -# haml-lint:disable InlineJavaScript + :javascript + gl = window.gl || {}; + gl.client = #{client.to_json}; diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index 1f4d24d996c..4373240001e 100644 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -1,8 +1,9 @@ !!! 5 %html{ lang: I18n.locale, class: page_class } = render "layouts/head" - %body{ class: "#{user_application_theme} #{@body_class}", data: { page: body_data_page, project: "#{@project.path if @project}", group: "#{@group.path if @group}", find_file: find_file_path } } + %body{ class: "#{user_application_theme} #{@body_class} #{client_class_list}", data: { page: body_data_page, project: "#{@project.path if @project}", group: "#{@group.path if @group}", find_file: find_file_path } } = render "layouts/init_auto_complete" if @gfm_form + = render "layouts/init_client_detection_flags" = render 'peek/bar' = render partial: "layouts/header/default", locals: { project: @project, group: @group } = render 'layouts/page', sidebar: sidebar, nav: nav diff --git a/app/views/layouts/header/_help_dropdown.html.haml b/app/views/layouts/header/_help_dropdown.html.haml index 04409408ce0..513902890af 100644 --- a/app/views/layouts/header/_help_dropdown.html.haml +++ b/app/views/layouts/header/_help_dropdown.html.haml @@ -1,9 +1,13 @@ +- show_blog_link = current_user_menu?(:help) && blog_post_url.present? %ul - if current_user_menu?(:help) %li = link_to _("Help"), help_path %li.divider + - if show_blog_link %li - = link_to _("Submit feedback"), "https://about.gitlab.com/submit-feedback" + = link_to _("What's new?"), blog_post_url + %li + = link_to _("Submit feedback"), "https://about.gitlab.com/submit-feedback" - if current_user_menu?(:help) || current_user_menu?(:settings) || current_user_menu?(:profile) = render 'shared/user_dropdown_contributing_link' diff --git a/app/views/layouts/nav/sidebar/_profile.html.haml b/app/views/layouts/nav/sidebar/_profile.html.haml index 69167edb1df..1e3bb8f1224 100644 --- a/app/views/layouts/nav/sidebar/_profile.html.haml +++ b/app/views/layouts/nav/sidebar/_profile.html.haml @@ -3,7 +3,7 @@ .context-header = link_to profile_path, title: _('Profile Settings') do .avatar-container.s40.settings-avatar - = sprite_icon('user', size: 24) + = image_tag avatar_icon_for_user(current_user, 40), class: "avatar s40 avatar-tile", alt: current_user.name .sidebar-context-title User Settings %ul.sidebar-top-level-items = nav_link(path: 'profiles#show', html_options: {class: 'home'}) do diff --git a/app/views/layouts/nav/sidebar/_project.html.haml b/app/views/layouts/nav/sidebar/_project.html.haml index d62cbc1684b..1eab7813865 100644 --- a/app/views/layouts/nav/sidebar/_project.html.haml +++ b/app/views/layouts/nav/sidebar/_project.html.haml @@ -43,7 +43,7 @@ - if project_nav_tab? :files = nav_link(controller: sidebar_repository_paths) do - = link_to project_tree_path(@project), class: 'shortcuts-tree' do + = link_to project_tree_path(@project), class: 'shortcuts-tree qa-project-menu-repo' do .nav-icon-container = sprite_icon('doc-text') %span.nav-item-name @@ -64,7 +64,7 @@ = _('Commits') = nav_link(html_options: {class: branches_tab_class}) do - = link_to project_branches_path(@project) do + = link_to project_branches_path(@project), class: 'qa-branches-link' do = _('Branches') = nav_link(controller: [:tags]) do @@ -201,7 +201,7 @@ - if project_nav_tab? :operations = nav_link(controller: sidebar_operations_paths) do - = link_to sidebar_operations_link_path, class: 'shortcuts-operations' do + = link_to sidebar_operations_link_path, class: 'shortcuts-operations qa-link-operations' do .nav-icon-container = sprite_icon('cloud-gear') %span.nav-item-name @@ -227,7 +227,7 @@ %span = _('Environments') - - if project_nav_tab?(:error_tracking) && Feature.enabled?(:error_tracking, @project) + - if project_nav_tab?(:error_tracking) = nav_link(controller: :error_tracking) do = link_to project_error_tracking_index_path(@project), title: _('Error Tracking'), class: 'shortcuts-tracking qa-operations-tracking-link' do %span diff --git a/app/views/profiles/accounts/show.html.haml b/app/views/profiles/accounts/show.html.haml index e167e094240..ee2c5a13b8a 100644 --- a/app/views/profiles/accounts/show.html.haml +++ b/app/views/profiles/accounts/show.html.haml @@ -1,37 +1,37 @@ -- page_title "Account" +- page_title _('Account') - @content_class = "limit-container-width" unless fluid_layout - if current_user.ldap_user? .alert.alert-info - Some options are unavailable for LDAP accounts + = s_('Profiles|Some options are unavailable for LDAP accounts') .row.prepend-top-default .col-lg-4.profile-settings-sidebar %h4.prepend-top-0 - Two-Factor Authentication + = s_('Profiles|Two-Factor Authentication') %p - Increase your account's security by enabling Two-Factor Authentication (2FA). + = s_("Profiles|Increase your account's security by enabling Two-Factor Authentication (2FA)") .col-lg-8 %p - Status: #{current_user.two_factor_enabled? ? 'Enabled' : 'Disabled'} + #{_('Status')}: #{current_user.two_factor_enabled? ? _('Enabled') : _('Disabled')} - if current_user.two_factor_enabled? - = link_to 'Manage two-factor authentication', profile_two_factor_auth_path, class: 'btn btn-info' + = link_to _('Manage two-factor authentication'), profile_two_factor_auth_path, class: 'btn btn-info' - else .append-bottom-10 - = link_to 'Enable two-factor authentication', profile_two_factor_auth_path, class: 'btn btn-success' + = link_to _('Enable two-factor authentication'), profile_two_factor_auth_path, class: 'btn btn-success' %hr - if display_providers_on_profile? .row.prepend-top-default .col-lg-4.profile-settings-sidebar %h4.prepend-top-0 - Social sign-in + = s_('Profiles|Social sign-in') %p - Activate signin with one of the following services + = s_('Profiles|Activate signin with one of the following services') .col-lg-8 %label.label-bold - Connected Accounts - %p Click on icon to activate signin with one of the following services + = s_('Profiles|Connected Accounts') + %p= s_('Profiles|Click on icon to activate signin with one of the following services') - button_based_providers.each do |provider| .provider-btn-group .provider-btn-image @@ -39,24 +39,24 @@ - if auth_active?(provider) - if unlink_allowed?(provider) = link_to unlink_profile_account_path(provider: provider), method: :delete, class: 'provider-btn' do - Disconnect + = s_('Profiles|Disconnect') - else %a.provider-btn - Active + = s_('Profiles|Active') - else = link_to omniauth_authorize_path(:user, provider), method: :post, class: 'provider-btn not-active' do - Connect + = s_('Profiles|Connect') = render_if_exists 'profiles/accounts/group_saml_unlink_buttons', group_saml_identities: local_assigns[:group_saml_identities] %hr - if current_user.can_change_username? .row.prepend-top-default .col-lg-4.profile-settings-sidebar %h4.prepend-top-0.warning-title - Change username + = s_('Profiles|Change username') %p - Changing your username can have unintended side effects. + = s_('Profiles|Changing your username can have unintended side effects.') = succeed '.' do - = link_to 'Learn more', help_page_path('user/profile/index', anchor: 'changing-your-username'), target: '_blank' + = link_to s_('Profiles|Learn more'), help_page_path('user/profile/index', anchor: 'changing-your-username'), target: '_blank' .col-lg-8 - data = { initial_username: current_user.username, root_url: root_url, action_url: update_username_profile_path(format: :json) } #update-username{ data: data } diff --git a/app/views/projects/_activity.html.haml b/app/views/projects/_activity.html.haml index 31f1cf560e2..12da62f4c64 100644 --- a/app/views/projects/_activity.html.haml +++ b/app/views/projects/_activity.html.haml @@ -1,5 +1,5 @@ %div{ class: container_class } - .nav-block.d-none.d-sm-block.activities + .nav-block.d-none.d-sm-flex.activities = render 'shared/event_filter' .controls = link_to project_path(@project, rss_url_options), title: s_("ProjectActivityRSS|Subscribe"), class: 'btn d-none d-sm-inline-block has-tooltip' do diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml index d2587af11dd..e8cc3d6bcf0 100644 --- a/app/views/projects/_home_panel.html.haml +++ b/app/views/projects/_home_panel.html.haml @@ -23,7 +23,7 @@ = sprite_icon('tag', size: 16, css_class: 'icon append-right-4') = @project.topics_to_show - if @project.has_extra_topics? - = _("+ %{count} more") % { count: @project.count_of_extra_tags_not_shown } + = _("+ %{count} more") % { count: @project.count_of_extra_topics_not_shown } .project-repo-buttons.col-md-12.col-lg-6.d-inline-flex.flex-wrap.justify-content-lg-end diff --git a/app/views/projects/artifacts/_tree_file.html.haml b/app/views/projects/artifacts/_tree_file.html.haml index cfb91568061..f42d5128715 100644 --- a/app/views/projects/artifacts/_tree_file.html.haml +++ b/app/views/projects/artifacts/_tree_file.html.haml @@ -14,4 +14,4 @@ = link_to path_to_file, class: 'str-truncated' do %span= blob.name %td - = number_to_human_size(blob.size, precision: 2) + = number_to_human_size(blob.size) diff --git a/app/views/projects/badges/badge_flat-square.svg.erb b/app/views/projects/badges/badge_flat-square.svg.erb new file mode 100644 index 00000000000..5b90da15ef5 --- /dev/null +++ b/app/views/projects/badges/badge_flat-square.svg.erb @@ -0,0 +1,17 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="<%= badge.width %>" height="20"> + <g shape-rendering="crispEdges"> + <path fill="<%= badge.key_color %>" d="M0 0 h<%= badge.key_width %> v20 H0 z"/> + <path fill="<%= badge.value_color %>" d="M<%= badge.key_width %> 0 h<%= badge.value_width %> v20 H<%= badge.key_width %> z"/> + </g> + + <g fill="#fff" text-anchor="middle"> + <g font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11"> + <text x="<%= badge.key_text_anchor %>" y="14"> + <%= badge.key_text %> + </text> + <text x="<%= badge.value_text_anchor %>" y="14"> + <%= badge.value_text %> + </text> + </g> + </g> +</svg> diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml index 88f9b7dfc9f..4b0ea15335e 100644 --- a/app/views/projects/branches/_branch.html.haml +++ b/app/views/projects/branches/_branch.html.haml @@ -76,7 +76,7 @@ = icon("trash-o") - else = link_to project_branch_path(@project, branch.name), - class: "btn btn-remove remove-row js-ajax-loading-spinner has-tooltip", + class: "btn btn-remove remove-row qa-remove-btn js-ajax-loading-spinner has-tooltip", title: s_('Branches|Delete branch'), method: :delete, data: { confirm: s_("Branches|Deleting the '%{branch_name}' branch cannot be undone. Are you sure?") % { branch_name: branch.name } }, diff --git a/app/views/projects/branches/_panel.html.haml b/app/views/projects/branches/_panel.html.haml index 0e4b119bb54..93061452e12 100644 --- a/app/views/projects/branches/_panel.html.haml +++ b/app/views/projects/branches/_panel.html.haml @@ -10,7 +10,7 @@ .card.prepend-top-10 .card-header = panel_title - %ul.content-list.all-branches + %ul.content-list.all-branches.qa-all-branches - branches.first(overview_max_branches).each do |branch| = render "projects/branches/branch", branch: branch, merged: project.repository.merged_to_root_ref?(branch) - if branches.size > overview_max_branches diff --git a/app/views/projects/branches/index.html.haml b/app/views/projects/branches/index.html.haml index ca867961f6b..43f1cd01b67 100644 --- a/app/views/projects/branches/index.html.haml +++ b/app/views/projects/branches/index.html.haml @@ -35,7 +35,7 @@ - if can? current_user, :push_code, @project = link_to project_merged_branches_path(@project), - class: 'btn btn-inverted btn-remove has-tooltip', + class: 'btn btn-inverted btn-remove has-tooltip qa-delete-merged-branches', title: s_("Branches|Delete all branches that are merged into '%{default_branch}'") % { default_branch: @project.repository.root_ref }, method: :delete, data: { confirm: s_('Branches|Deleting the merged branches cannot be undone. Are you sure?'), diff --git a/app/views/projects/environments/folder.html.haml b/app/views/projects/environments/folder.html.haml index b7e1cf85cb7..aebd176af9b 100644 --- a/app/views/projects/environments/folder.html.haml +++ b/app/views/projects/environments/folder.html.haml @@ -1,8 +1,5 @@ - @no_container = true - page_title _("Environments") -#environments-folder-list-view{ data: { endpoint: folder_project_environments_path(@project, @folder, format: :json), - "folder-name" => @folder, - "can-create-deployment" => can?(current_user, :create_deployment, @project).to_s, - "can-read-environment" => can?(current_user, :read_environment, @project).to_s, +#environments-folder-list-view{ data: { environments_data: environments_folder_list_view_data, "css-class" => container_class } } diff --git a/app/views/projects/issues/_merge_requests.html.haml b/app/views/projects/issues/_merge_requests.html.haml index 5c36d2202a6..eb46bf5c6a3 100644 --- a/app/views/projects/issues/_merge_requests.html.haml +++ b/app/views/projects/issues/_merge_requests.html.haml @@ -1,35 +1,35 @@ - if @merge_requests.any? - %h2.merge-requests-title - = pluralize(@merge_requests.count, 'Related Merge Request') - %ul.unstyled-list.related-merge-requests - - has_any_head_pipeline = @merge_requests.any?(&:head_pipeline_id) - - @merge_requests.each do |merge_request| - %li - %span.merge-request-ci-status - - if merge_request.head_pipeline - = render_pipeline_status(merge_request.head_pipeline) - - elsif has_any_head_pipeline - = icon('blank fw') - %span.merge-request-id - = merge_request.to_reference - %span.merge-request-info - %strong - = link_to merge_request.title, merge_request_path(merge_request), class: "row_title" - - unless @issue.project.id == merge_request.target_project.id - in - - project = merge_request.target_project - = link_to project.full_name, project_path(project) + .card-slim.mt-3 + .card-header + %h2.card-title.mt-0.mb-0.h5.merge-requests-title + %span.mr-1.bold + = _('Related merge requests') + .d-inline-flex.lh-100.align-middle + .mr-count-badge + .mr-count-badge-count + = sprite_icon('merge-request', size: 16, css_class: 'mr-1 text-secondary') + = @merge_requests.count + %ul.content-list.related-items-list + - has_any_head_pipeline = @merge_requests.any?(&:head_pipeline_id) + - @merge_requests.each do |merge_request| + %li.list-item.py-0.px-0 + .item-body.issuable-info-container.py-lg-3.px-lg-3.pl-md-3 + .item-contents + .item-title.d-flex.align-items-center.mr-title + = render partial: 'projects/issues/merge_requests_status', locals: { merge_request: merge_request, css_class: 'd-none d-xl-block append-right-8' } + = link_to merge_request.title, merge_request_path(merge_request), { class: 'mr-title-link'} + .item-meta + = render partial: 'projects/issues/merge_requests_status', locals: { merge_request: merge_request, css_class: 'd-xl-none d-lg-block append-right-5' } + %span.d-flex.align-items-center.append-right-8.mr-item-path.item-path-id.mt-0 + %span.path-id-text.bold.text-truncate{ data: { toggle: 'tooltip'}, title: merge_request.target_project.full_path } + = merge_request.target_project.full_path + = merge_request.to_reference + %span.mr-ci-status.flex-md-grow-1.justify-content-end.d-flex.ml-md-2 + - if merge_request.head_pipeline + = render_pipeline_status(merge_request.head_pipeline, tooltip_placement: 'bottom') + - elsif has_any_head_pipeline + = icon('blank fw') - - if merge_request.merged? - %span.merge-request-status.prepend-left-10.merged - Merged - - elsif merge_request.closed? - %span.merge-request-status.prepend-left-10.closed - Closed - - else - %span.merge-request-status.prepend-left-10.open - Open - - - if @closed_by_merge_requests.present? - %li - = render partial: 'projects/issues/closed_by_box', locals: {merge_request_count: @merge_requests.count} + - if @closed_by_merge_requests.present? + %p + = render partial: 'projects/issues/closed_by_box', locals: {merge_request_count: @merge_requests.count} diff --git a/app/views/projects/issues/_merge_requests_status.html.haml b/app/views/projects/issues/_merge_requests_status.html.haml new file mode 100644 index 00000000000..38126d6f0c6 --- /dev/null +++ b/app/views/projects/issues/_merge_requests_status.html.haml @@ -0,0 +1,22 @@ +- time_format = '%b %e, %Y %l:%M%P %Z%z' + +- if merge_request.merged? + - mr_status_date = merge_request.merged_at + - mr_status_title = _('Merged') + - mr_status_icon = 'merge' + - mr_status_class = 'merged' +- elsif merge_request.closed? + - mr_status_date = merge_request.closed_event&.created_at + - mr_status_title = _('Closed') + - mr_status_icon = 'issue-close' + - mr_status_class = 'closed' +- else + - mr_status_date = merge_request.created_at + - mr_status_title = _('Opened') + - mr_status_icon = 'issue-open-m' + - mr_status_class = 'open' + +- mr_status_tooltip = "<div><span class=\"bold\">#{mr_status_title}</span> #{time_ago_in_words(mr_status_date)} ago</div><span class=\"text-tertiary\">#{l(mr_status_date.to_time, format: time_format)}</span>" + +%span.mr-status-wrapper.suggestion-help-hover{ class: css_class, data: { toggle: 'tooltip', placement: 'bottom', html: 'true', title: mr_status_tooltip } } + = sprite_icon(mr_status_icon, size: 16, css_class: "merge-request-status #{mr_status_class}") diff --git a/app/views/projects/issues/_nav_btns.html.haml b/app/views/projects/issues/_nav_btns.html.haml index fd6559e37ba..329efa0cdbf 100644 --- a/app/views/projects/issues/_nav_btns.html.haml +++ b/app/views/projects/issues/_nav_btns.html.haml @@ -1,5 +1,5 @@ - show_feed_buttons = local_assigns.fetch(:show_feed_buttons, true) -- show_import_button = local_assigns.fetch(:show_import_button, true) && Feature.enabled?(:issues_import_csv) && can?(current_user, :import_issues, @project) +- show_import_button = local_assigns.fetch(:show_import_button, true) && can?(current_user, :import_issues, @project) - show_export_button = local_assigns.fetch(:show_export_button, true) .nav-controls.issues-nav-controls diff --git a/app/views/projects/jobs/index.html.haml b/app/views/projects/jobs/index.html.haml index 59592abcf6a..afea5268006 100644 --- a/app/views/projects/jobs/index.html.haml +++ b/app/views/projects/jobs/index.html.haml @@ -8,10 +8,6 @@ .nav-controls - if can?(current_user, :update_build, @project) - - if @all_builds.running_or_pending.limit(1).any? # rubocop: disable CodeReuse/ActiveRecord - = link_to 'Cancel running', cancel_all_project_jobs_path(@project), - data: { confirm: 'Are you sure?' }, class: 'btn btn-danger', method: :post - - unless @repository.gitlab_ci_yml = link_to 'Get started with Pipelines', help_page_path('ci/quick_start/README'), class: 'btn btn-info' diff --git a/app/views/projects/labels/index.html.haml b/app/views/projects/labels/index.html.haml index 56b06374d6d..bb7c297ba1f 100644 --- a/app/views/projects/labels/index.html.haml +++ b/app/views/projects/labels/index.html.haml @@ -5,15 +5,10 @@ - subscribed = params[:subscribed] - labels_or_filters = @labels.exists? || @prioritized_labels.exists? || search.present? || subscribed.present? -- if labels_or_filters && can_admin_label - - content_for(:header_content) do - .nav-controls - = link_to _('New label'), new_project_label_path(@project), class: "btn btn-success qa-label-create-new" - - if labels_or_filters #promote-label-modal %div{ class: container_class } - = render 'shared/labels/nav' + = render 'shared/labels/nav', labels_or_filters: labels_or_filters, can_admin_label: can_admin_label .labels-container.prepend-top-10 - if can_admin_label diff --git a/app/views/projects/project_members/_groups.html.haml b/app/views/projects/project_members/_groups.html.haml index 3f05e06b0c6..b5d397e3065 100644 --- a/app/views/projects/project_members/_groups.html.haml +++ b/app/views/projects/project_members/_groups.html.haml @@ -1,7 +1,6 @@ .card.project-members-groups .card-header - Groups with access to - %strong= @project.name + = _("Groups with access to <strong>%{project_name}</strong>").html_safe % { project_name: sanitize_project_name(@project.name) } %span.badge.badge-pill= group_links.size %ul.content-list.members-list = render partial: 'shared/members/group', collection: group_links, as: :group_link diff --git a/app/views/projects/project_members/_new_project_group.html.haml b/app/views/projects/project_members/_new_project_group.html.haml index 88e68f89024..079811e4e79 100644 --- a/app/views/projects/project_members/_new_project_group.html.haml +++ b/app/views/projects/project_members/_new_project_group.html.haml @@ -10,8 +10,9 @@ = select_tag :link_group_access, options_for_select(ProjectGroupLink.access_options, ProjectGroupLink.default_access), class: "form-control select-control" = icon('chevron-down') .form-text.text-muted.append-bottom-10 - = link_to _("Read more"), help_page_path("user/permissions") - about role permissions + - permissions_docs_path = help_page_path('user/permissions') + - link_start = %q{<a href="%{url}">}.html_safe % { url: permissions_docs_path } + = _("%{link_start}Read more%{link_end} about role permissions").html_safe % { link_start: link_start, link_end: '</a>'.html_safe } .form-group = label_tag :expires_at, _('Access expiration date'), class: 'label-bold' .clearable-input diff --git a/app/views/projects/project_members/_new_project_member.html.haml b/app/views/projects/project_members/_new_project_member.html.haml index 1de7d9c6957..0590578c3fe 100644 --- a/app/views/projects/project_members/_new_project_member.html.haml +++ b/app/views/projects/project_members/_new_project_member.html.haml @@ -2,20 +2,21 @@ .col-sm-12 = form_for @project_member, as: :project_member, url: project_project_members_path(@project), html: { class: 'users-project-form' } do |f| .form-group - = label_tag :user_ids, "Select members to invite", class: "label-bold" + = label_tag :user_ids, _("Select members to invite"), class: "label-bold" = users_select_tag(:user_ids, multiple: true, class: "input-clamp qa-member-select-input", scope: :all, email_user: true, placeholder: "Search for members to update or invite") .form-group - = label_tag :access_level, "Choose a role permission", class: "label-bold" + = label_tag :access_level, _("Choose a role permission"), class: "label-bold" .select-wrapper = select_tag :access_level, options_for_select(ProjectMember.access_level_roles, @project_member.access_level), class: "form-control project-access-select select-control" = icon('chevron-down') .form-text.text-muted.append-bottom-10 - = link_to "Read more", help_page_path("user/permissions") - about role permissions + - permissions_docs_path = help_page_path('user/permissions') + - link_start = %q{<a href="%{url}">}.html_safe % { url: permissions_docs_path } + = _("%{link_start}Read more%{link_end} about role permissions").html_safe % { link_start: link_start, link_end: '</a>'.html_safe } .form-group .clearable-input - = label_tag :expires_at, 'Access expiration date', class: 'label-bold' + = label_tag :expires_at, _('Access expiration date'), class: 'label-bold' = text_field_tag :expires_at, nil, class: 'form-control js-access-expiration-date', placeholder: 'Expiration date' %i.clear-icon.js-clear-input - = f.submit "Add to project", class: "btn btn-success qa-add-member-button" - = link_to "Import", import_project_project_members_path(@project), class: "btn btn-default", title: "Import members from another project" + = f.submit _("Add to project"), class: "btn btn-success qa-add-member-button" + = link_to _("Import"), import_project_project_members_path(@project), class: "btn btn-default", title: _("Import members from another project") diff --git a/app/views/projects/project_members/_team.html.haml b/app/views/projects/project_members/_team.html.haml index 9682f8ac922..b92ecf4412f 100644 --- a/app/views/projects/project_members/_team.html.haml +++ b/app/views/projects/project_members/_team.html.haml @@ -4,14 +4,13 @@ .card .card-header.flex-project-members-panel %span.flex-project-title - Members of - %strong= project.name + = _("Members of <strong>%{project_name}</strong>").html_safe % { project_name: sanitize_project_name(project.name) } %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 .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" } + = 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.qa-members-list diff --git a/app/views/projects/project_members/import.html.haml b/app/views/projects/project_members/import.html.haml index 8b93e81cd31..bcca943de6a 100644 --- a/app/views/projects/project_members/import.html.haml +++ b/app/views/projects/project_members/import.html.haml @@ -1,15 +1,15 @@ -- page_title "Import members" +- page_title _("Import members") %h3.page-title - Import members from another project + = _("Import members from another project") %p.light - Only project members will be imported. Group members will be skipped. + = _("Only project members will be imported. Group members will be skipped.") %hr = form_tag apply_import_project_project_members_path(@project), method: 'post' do .form-group.row - = label_tag :source_project_id, "Project", class: 'col-form-label col-sm-2' + = label_tag :source_project_id, _("Project"), class: 'col-form-label col-sm-2' .col-sm-10= select_tag(:source_project_id, options_from_collection_for_select(@projects, :id, :name_with_namespace), prompt: "Select project", class: "select2 lg", required: true) .form-actions - = button_tag 'Import project members', class: "btn btn-success" - = link_to "Cancel", project_project_members_path(@project), class: "btn btn-cancel" + = button_tag _('Import project members'), class: "btn btn-success" + = link_to _("Cancel"), project_project_members_path(@project), class: "btn btn-cancel" diff --git a/app/views/projects/project_members/index.html.haml b/app/views/projects/project_members/index.html.haml index 14ed3345765..242ff91f539 100644 --- a/app/views/projects/project_members/index.html.haml +++ b/app/views/projects/project_members/index.html.haml @@ -1,39 +1,34 @@ -- page_title "Members" +- page_title _("Members") .row.prepend-top-default .col-lg-12 %h4 - Project members + = _("Project members") - if can?(current_user, :admin_project_member, @project) %p - You can invite a new member to - %strong= @project.name - or invite another group. + = _("You can invite a new member to <strong>%{project_name}</strong> or invite another group.").html_safe % { project_name: sanitize_project_name(@project.name) } - else %p - Members can be added by project - %i Maintainers - or - %i Owners + = _("Members can be added by project <i>Maintainers</i> or <i>Owners</i>").html_safe .light - if can?(current_user, :admin_project_member, @project) %ul.nav-links.nav.nav-tabs.gitlab-tabs{ role: 'tablist' } %li.nav-tab{ role: 'presentation' } - %a.nav-link.active{ href: '#invite-member-pane', id: 'invite-member-tab', data: { toggle: 'tab' }, role: 'tab' } Invite member + %a.nav-link.active{ href: '#invite-member-pane', id: 'invite-member-tab', data: { toggle: 'tab' }, role: 'tab' }= _("Invite member") - if @project.allowed_to_share_with_group? %li.nav-tab{ role: 'presentation' } - %a.nav-link{ href: '#invite-group-pane', id: 'invite-group-tab', data: { toggle: 'tab' }, role: 'tab' } Invite group + %a.nav-link{ href: '#invite-group-pane', id: 'invite-group-tab', data: { toggle: 'tab' }, role: 'tab' }= _("Invite group") .tab-content.gitlab-tab-content .tab-pane.active{ id: 'invite-member-pane', role: 'tabpanel' } - = render 'projects/project_members/new_project_member', tab_title: 'Invite member' + = render 'projects/project_members/new_project_member', tab_title: _('Invite member') .tab-pane{ id: 'invite-group-pane', role: 'tabpanel' } - = render 'projects/project_members/new_project_group', tab_title: 'Invite group' + = render 'projects/project_members/new_project_group', tab_title: _('Invite group') = render 'shared/members/requests', membership_source: @project, requesters: @requesters .clearfix %h5.member.existing-title - Existing members and groups + = _("Existing members and groups") - if @group_links.any? = render 'projects/project_members/groups', group_links: @group_links diff --git a/app/views/projects/settings/operations/_error_tracking.html.haml b/app/views/projects/settings/operations/_error_tracking.html.haml index 871b60f05ba..4911e8d3770 100644 --- a/app/views/projects/settings/operations/_error_tracking.html.haml +++ b/app/views/projects/settings/operations/_error_tracking.html.haml @@ -1,4 +1,4 @@ -- return unless Feature.enabled?(:error_tracking, @project) && can?(current_user, :read_environment, @project) +- return unless can?(current_user, :read_environment, @project) - setting = error_tracking_setting diff --git a/app/views/search/_category.html.haml b/app/views/search/_category.html.haml index ff9a7b53a86..aaf9b973cda 100644 --- a/app/views/search/_category.html.haml +++ b/app/views/search/_category.html.haml @@ -6,75 +6,75 @@ - if project_search_tabs?(:blobs) %li{ class: active_when(@scope == 'blobs') } = link_to search_filter_path(scope: 'blobs') do - Code + = _("Code") %span.badge.badge-pill = @search_results.blobs_count - if project_search_tabs?(:issues) %li{ class: active_when(@scope == 'issues') } = link_to search_filter_path(scope: 'issues') do - Issues + = _("Issues") %span.badge.badge-pill = limited_count(@search_results.limited_issues_count) - if project_search_tabs?(:merge_requests) %li{ class: active_when(@scope == 'merge_requests') } = link_to search_filter_path(scope: 'merge_requests') do - Merge requests + = _("Merge requests") %span.badge.badge-pill = limited_count(@search_results.limited_merge_requests_count) - if project_search_tabs?(:milestones) %li{ class: active_when(@scope == 'milestones') } = link_to search_filter_path(scope: 'milestones') do - Milestones + = _("Milestones") %span.badge.badge-pill = limited_count(@search_results.limited_milestones_count) - if project_search_tabs?(:notes) %li{ class: active_when(@scope == 'notes') } = link_to search_filter_path(scope: 'notes') do - Comments + = _("Comments") %span.badge.badge-pill = limited_count(@search_results.limited_notes_count) - if project_search_tabs?(:wiki) %li{ class: active_when(@scope == 'wiki_blobs') } = link_to search_filter_path(scope: 'wiki_blobs') do - Wiki + = _("Wiki") %span.badge.badge-pill = @search_results.wiki_blobs_count - if project_search_tabs?(:commits) %li{ class: active_when(@scope == 'commits') } = link_to search_filter_path(scope: 'commits') do - Commits + = _("Commits") %span.badge.badge-pill = @search_results.commits_count - elsif @show_snippets %li{ class: active_when(@scope == 'snippet_blobs') } = link_to search_filter_path(scope: 'snippet_blobs', snippets: true, group_id: nil, project_id: nil) do - Snippet Contents + = _("Snippet Contents") %span.badge.badge-pill = @search_results.snippet_blobs_count %li{ class: active_when(@scope == 'snippet_titles') } = link_to search_filter_path(scope: 'snippet_titles', snippets: true, group_id: nil, project_id: nil) do - Titles and Filenames + = _("Titles and Filenames") %span.badge.badge-pill = @search_results.snippet_titles_count - else %li{ class: active_when(@scope == 'projects') } = link_to search_filter_path(scope: 'projects') do - Projects + = _("Projects") %span.badge.badge-pill = limited_count(@search_results.limited_projects_count) %li{ class: active_when(@scope == 'issues') } = link_to search_filter_path(scope: 'issues') do - Issues + = _("Issues") %span.badge.badge-pill = limited_count(@search_results.limited_issues_count) %li{ class: active_when(@scope == 'merge_requests') } = link_to search_filter_path(scope: 'merge_requests') do - Merge requests + = _("Merge requests") %span.badge.badge-pill = limited_count(@search_results.limited_merge_requests_count) %li{ class: active_when(@scope == 'milestones') } = link_to search_filter_path(scope: 'milestones') do - Milestones + = _("Milestones") %span.badge.badge-pill = limited_count(@search_results.limited_milestones_count) diff --git a/app/views/search/_filter.html.haml b/app/views/search/_filter.html.haml index 6837f64f132..c8b6a3258ab 100644 --- a/app/views/search/_filter.html.haml +++ b/app/views/search/_filter.html.haml @@ -3,31 +3,31 @@ - if params[:project_id].present? = hidden_field_tag :project_id, params[:project_id] .dropdown - %button.dropdown-menu-toggle.js-search-group-dropdown{ type: "button", data: { toggle: "dropdown", default_label: "Group:", group_id: params[:group_id] } } + %button.dropdown-menu-toggle.js-search-group-dropdown{ type: "button", data: { toggle: "dropdown", default_label: _('Group:'), group_id: params[:group_id] } } %span.dropdown-toggle-text - Group: + = _("Group:") - if @group.present? = @group.name - else - Any + = _("Any") = icon("chevron-down") .dropdown-menu.dropdown-select.dropdown-menu-selectable.dropdown-menu-right - = dropdown_title("Filter results by group") - = dropdown_filter("Search groups") + = dropdown_title(_("Filter results by group")) + = dropdown_filter(_("Search groups")) = dropdown_content = dropdown_loading .dropdown.project-filter - %button.dropdown-menu-toggle.js-search-project-dropdown{ type: "button", data: { toggle: "dropdown", default_label: "Project:" } } + %button.dropdown-menu-toggle.js-search-project-dropdown{ type: "button", data: { toggle: "dropdown", default_label: _('Project:') } } %span.dropdown-toggle-text - Project: + = _("Project:") - if @project.present? = @project.full_name - else - Any + = _("Any") = icon("chevron-down") .dropdown-menu.dropdown-select.dropdown-menu-selectable.dropdown-menu-right - = dropdown_title("Filter results by project") - = dropdown_filter("Search projects") + = dropdown_title(_("Filter results by project")) + = dropdown_filter(_("Search projects")) = dropdown_content = dropdown_loading diff --git a/app/views/search/_form.html.haml b/app/views/search/_form.html.haml index a4a5cec1314..4af0c6bf84a 100644 --- a/app/views/search/_form.html.haml +++ b/app/views/search/_form.html.haml @@ -4,12 +4,12 @@ .search-holder .search-field-holder - = search_field_tag :search, params[:search], placeholder: "Search for projects, issues etc", class: "form-control search-text-input js-search-input", id: "dashboard_search", autofocus: true, spellcheck: false + = search_field_tag :search, params[:search], placeholder: _("Search for projects, issues, etc."), class: "form-control search-text-input js-search-input", id: "dashboard_search", autofocus: true, spellcheck: false = icon("search", class: "search-icon") %button.search-clear.js-search-clear{ class: ("hidden" if !params[:search].present?), type: "button", tabindex: "-1" } = icon("times-circle") %span.sr-only - Clear search + = _("Clear search") - unless params[:snippets].eql? 'true' = render 'filter' - = button_tag "Search", class: "btn btn-success btn-search" + = button_tag _("Search"), class: "btn btn-success btn-search" diff --git a/app/views/search/_results.html.haml b/app/views/search/_results.html.haml index c4d52431d6e..be7a2436d16 100644 --- a/app/views/search/_results.html.haml +++ b/app/views/search/_results.html.haml @@ -6,9 +6,11 @@ = search_entries_info(@search_objects, @scope, @search_term) - unless @show_snippets - if @project - in project #{link_to @project.full_name, [@project.namespace.becomes(Namespace), @project]} + - link_to_project = link_to(@project.full_name, [@project.namespace.becomes(Namespace), @project]) + = _("in project %{link_to_project}").html_safe % { link_to_project: link_to_project } - elsif @group - in group #{link_to @group.name, @group} + - link_to_group = link_to(@group.name, @group) + = _("in group %{link_to_group}").html_safe % { link_to_group: link_to_group } .results.prepend-top-10 - if @scope == 'commits' diff --git a/app/views/search/results/_empty.html.haml b/app/views/search/results/_empty.html.haml index 821a39d61f5..9d15995bb51 100644 --- a/app/views/search/results/_empty.html.haml +++ b/app/views/search/results/_empty.html.haml @@ -2,5 +2,5 @@ .search_glyph %h4 = icon('search') - We couldn't find any results matching + = _("We couldn't find any results matching") %code= @search_term diff --git a/app/views/search/results/_issue.html.haml b/app/views/search/results/_issue.html.haml index c413c3d4abb..796782035f2 100644 --- a/app/views/search/results/_issue.html.haml +++ b/app/views/search/results/_issue.html.haml @@ -4,7 +4,7 @@ = link_to [issue.project.namespace.becomes(Namespace), issue.project, issue] do %span.term.str-truncated= issue.title - if issue.closed? - %span.badge.badge-danger.prepend-left-5 Closed + %span.badge.badge-danger.prepend-left-5= _("Closed") .float-right ##{issue.iid} - if issue.description.present? .description.term diff --git a/app/views/search/results/_merge_request.html.haml b/app/views/search/results/_merge_request.html.haml index 519176af108..f0e0af11f27 100644 --- a/app/views/search/results/_merge_request.html.haml +++ b/app/views/search/results/_merge_request.html.haml @@ -3,9 +3,9 @@ = link_to [merge_request.target_project.namespace.becomes(Namespace), merge_request.target_project, merge_request] do %span.term.str-truncated= merge_request.title - if merge_request.merged? - %span.badge.badge-primary.prepend-left-5 Merged + %span.badge.badge-primary.prepend-left-5= _("Merged") - elsif merge_request.closed? - %span.badge.badge-danger.prepend-left-5 Closed + %span.badge.badge-danger.prepend-left-5= _("Closed") .float-right= merge_request.to_reference - if merge_request.description.present? .description.term diff --git a/app/views/search/results/_note.html.haml b/app/views/search/results/_note.html.haml index e4ab7b0541f..6717939d034 100644 --- a/app/views/search/results/_note.html.haml +++ b/app/views/search/results/_note.html.haml @@ -6,14 +6,14 @@ %h5.note-search-caption.str-truncated %i.fa.fa-comment = link_to_member(project, note.author, avatar: false) - commented on - = link_to project.full_name, project + - link_to_project = link_to(project.full_name, project) + = _("commented on %{link_to_project}").html_safe % { link_to_project: link_to_project } · - if note.for_commit? - = link_to_if(noteable_identifier, "Commit #{truncate_sha(note.commit_id)}", note_url) do + = link_to_if(noteable_identifier, _("Commit %{commit_id}") % { commit_id: truncate_sha(note.commit_id) }, note_url) do = truncate_sha(note.commit_id) - %span.light Commit deleted + %span.light= _("Commit deleted") - else %span #{note.noteable_type.titleize} ##{noteable_identifier} diff --git a/app/views/search/results/_snippet_blob.html.haml b/app/views/search/results/_snippet_blob.html.haml index b4ecd7bbae9..e0130f9a4b5 100644 --- a/app/views/search/results/_snippet_blob.html.haml +++ b/app/views/search/results/_snippet_blob.html.haml @@ -24,7 +24,7 @@ = markup(snippet.file_name, chunk[:data], legacy_render_context(params)) - else .file-content.code - .nothing-here-block Empty file + .nothing-here-block= _("Empty file") - else .file-content.code.js-syntax-highlight .line-numbers @@ -42,4 +42,4 @@ = highlight(snippet.file_name, chunk[:data]) - else .file-content.code - .nothing-here-block Empty file + .nothing-here-block= _("Empty file") diff --git a/app/views/search/results/_snippet_title.html.haml b/app/views/search/results/_snippet_title.html.haml index 9c8afb2165b..1e01088d9e6 100644 --- a/app/views/search/results/_snippet_title.html.haml +++ b/app/views/search/results/_snippet_title.html.haml @@ -5,7 +5,7 @@ - if snippet_title.private? %span.badge.badge-gray %i.fa.fa-lock - private + = _("private") %span.cgray.monospace.tiny.float-right.term = snippet_title.file_name diff --git a/app/views/search/show.html.haml b/app/views/search/show.html.haml index 499697f2777..3260d05f509 100644 --- a/app/views/search/show.html.haml +++ b/app/views/search/show.html.haml @@ -1,5 +1,5 @@ - @hide_top_links = true -- breadcrumb_title "Search" +- breadcrumb_title _("Search") - page_title @search_term .prepend-top-default diff --git a/app/views/shared/_mini_pipeline_graph.html.haml b/app/views/shared/_mini_pipeline_graph.html.haml index 28b34e38b15..8607f87ce0b 100644 --- a/app/views/shared/_mini_pipeline_graph.html.haml +++ b/app/views/shared/_mini_pipeline_graph.html.haml @@ -7,7 +7,6 @@ .stage-container.dropdown{ class: klass } %button.mini-pipeline-graph-dropdown-toggle.has-tooltip.js-builds-dropdown-button{ class: "ci-status-icon-#{detailed_status.group}", type: 'button', data: { toggle: 'dropdown', title: "#{stage.name}: #{detailed_status.label}", placement: 'top', "stage-endpoint" => stage_ajax_project_pipeline_path(pipeline.project, pipeline, stage: stage.name) } } = sprite_icon(icon_status) - = icon('caret-down') %ul.dropdown-menu.mini-pipeline-graph-dropdown-menu.js-builds-dropdown-container %li.js-builds-dropdown-list.scrollable-menu diff --git a/app/views/shared/_personal_access_tokens_created_container.html.haml b/app/views/shared/_personal_access_tokens_created_container.html.haml index 3150d39b84a..a8d3de66418 100644 --- a/app/views/shared/_personal_access_tokens_created_container.html.haml +++ b/app/views/shared/_personal_access_tokens_created_container.html.haml @@ -6,7 +6,7 @@ = container_title .form-group .input-group - = text_field_tag 'created-personal-access-token', new_token_value, readonly: true, class: "form-control js-select-on-focus", 'aria-describedby' => "created-token-help-block" + = text_field_tag 'created-personal-access-token', new_token_value, readonly: true, class: "qa-created-personal-access-token form-control js-select-on-focus", 'aria-describedby' => "created-token-help-block" %span.input-group-append = clipboard_button(text: new_token_value, title: clipboard_button_title, placement: "left", class: "input-group-text btn-default btn-clipboard") %span#created-token-help-block.form-text.text-muted.text-danger Make sure you save it - you won't be able to access it again. diff --git a/app/views/shared/_personal_access_tokens_form.html.haml b/app/views/shared/_personal_access_tokens_form.html.haml index f4df7bdcd83..0891b3459ec 100644 --- a/app/views/shared/_personal_access_tokens_form.html.haml +++ b/app/views/shared/_personal_access_tokens_form.html.haml @@ -12,7 +12,7 @@ .row .form-group.col-md-6 = f.label :name, class: 'label-bold' - = f.text_field :name, class: "form-control", required: true + = f.text_field :name, class: "form-control qa-personal-access-token-name-field", required: true .row .form-group.col-md-6 @@ -26,4 +26,4 @@ = render 'shared/tokens/scopes_form', prefix: 'personal_access_token', token: token, scopes: scopes .prepend-top-default - = f.submit "Create #{type} token", class: "btn btn-success" + = f.submit "Create #{type} token", class: "btn btn-success qa-create-token-button" diff --git a/app/views/shared/_personal_access_tokens_table.html.haml b/app/views/shared/_personal_access_tokens_table.html.haml index 2efd03d4867..49f3aae0f98 100644 --- a/app/views/shared/_personal_access_tokens_table.html.haml +++ b/app/views/shared/_personal_access_tokens_table.html.haml @@ -29,7 +29,7 @@ %span.token-never-expires-label Never %td= token.scopes.present? ? token.scopes.join(", ") : "<no scopes selected>" - path = impersonation ? revoke_admin_user_impersonation_token_path(token.user, token) : revoke_profile_personal_access_token_path(token) - %td= link_to "Revoke", path, method: :put, class: "btn btn-danger float-right", data: { confirm: "Are you sure you want to revoke this #{type} Token? This action cannot be undone." } + %td= link_to "Revoke", path, method: :put, class: "btn btn-danger float-right qa-revoke-button", data: { confirm: "Are you sure you want to revoke this #{type} Token? This action cannot be undone." } - else .settings-message.text-center This user has no active #{type} Tokens. diff --git a/app/views/shared/boards/components/_board.html.haml b/app/views/shared/boards/components/_board.html.haml index c6c5cadc3f5..307a0919a4c 100644 --- a/app/views/shared/boards/components/_board.html.haml +++ b/app/views/shared/boards/components/_board.html.haml @@ -1,6 +1,6 @@ .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 + .board-inner.d-flex.flex-column %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)" } %h3.board-title.js-board-handle{ ":class" => '{ "user-can-drag": (!disabled && !list.preset) }' } %i.fa.fa-fw.board-title-expandable-toggle{ "v-if": "list.isExpandable", diff --git a/app/views/shared/empty_states/_issues.html.haml b/app/views/shared/empty_states/_issues.html.haml index 2691ec4cd46..9173b802dd4 100644 --- a/app/views/shared/empty_states/_issues.html.haml +++ b/app/views/shared/empty_states/_issues.html.haml @@ -1,6 +1,6 @@ - button_path = local_assigns.fetch(:button_path, false) - project_select_button = local_assigns.fetch(:project_select_button, false) -- show_import_button = local_assigns.fetch(:show_import_button, false) && Feature.enabled?(:issues_import_csv) && can?(current_user, :import_issues, @project) +- show_import_button = local_assigns.fetch(:show_import_button, false) && can?(current_user, :import_issues, @project) - has_button = button_path || project_select_button - closed_issues_count = issuables_count_for_state(:issues, :closed) - opened_issues_count = issuables_count_for_state(:issues, :opened) diff --git a/app/views/shared/empty_states/_labels.html.haml b/app/views/shared/empty_states/_labels.html.haml index bee26cd8312..a739103641e 100644 --- a/app/views/shared/empty_states/_labels.html.haml +++ b/app/views/shared/empty_states/_labels.html.haml @@ -1,6 +1,6 @@ .row.empty-state.labels .col-12 - .svg-content + .svg-content.qa-label-svg = image_tag 'illustrations/labels.svg' .col-12 .text-content diff --git a/app/views/shared/labels/_nav.html.haml b/app/views/shared/labels/_nav.html.haml index 98572db738b..e69246dd0eb 100644 --- a/app/views/shared/labels/_nav.html.haml +++ b/app/views/shared/labels/_nav.html.haml @@ -18,3 +18,7 @@ %button.btn.btn-default{ type: "submit", "aria-label" => _('Submit search') } = icon("search") = render 'shared/labels/sort_dropdown' + - if labels_or_filters && can_admin_label && @project + = link_to _('New label'), new_project_label_path(@project), class: "btn btn-success qa-label-create-new" + - if labels_or_filters && can_admin_label && @group + = link_to _('New label'), new_group_label_path(@group), class: "btn btn-success qa-label-create-new" diff --git a/app/views/shared/tokens/_scopes_form.html.haml b/app/views/shared/tokens/_scopes_form.html.haml index af9db5f59a8..a5d3e1c8de0 100644 --- a/app/views/shared/tokens/_scopes_form.html.haml +++ b/app/views/shared/tokens/_scopes_form.html.haml @@ -4,6 +4,6 @@ - scopes.each do |scope| %fieldset.form-group.form-check - = check_box_tag "#{prefix}[scopes][]", scope, token.scopes.include?(scope), id: "#{prefix}_scopes_#{scope}", class: 'form-check-input' + = check_box_tag "#{prefix}[scopes][]", scope, token.scopes.include?(scope), id: "#{prefix}_scopes_#{scope}", class: "form-check-input qa-#{scope}-radio" = label_tag ("#{prefix}_scopes_#{scope}"), scope, class: 'label-bold form-check-label' .text-secondary= t scope, scope: [:doorkeeper, :scope_desc] diff --git a/app/workers/build_finished_worker.rb b/app/workers/build_finished_worker.rb index 61d866b1f02..ae853ec9316 100644 --- a/app/workers/build_finished_worker.rb +++ b/app/workers/build_finished_worker.rb @@ -9,14 +9,26 @@ class BuildFinishedWorker # rubocop: disable CodeReuse/ActiveRecord def perform(build_id) Ci::Build.find_by(id: build_id).try do |build| - # We execute that in sync as this access the files in order to access local file, and reduce IO - BuildTraceSectionsWorker.new.perform(build.id) - BuildCoverageWorker.new.perform(build.id) - - # We execute that async as this are two independent operations that can be executed after TraceSections and Coverage - BuildHooksWorker.perform_async(build.id) - ArchiveTraceWorker.perform_async(build.id) + process_build(build) end end # rubocop: enable CodeReuse/ActiveRecord + + private + + # Processes a single CI build that has finished. + # + # This logic resides in a separate method so that EE can extend it more + # easily. + # + # @param [Ci::Build] build The build to process. + def process_build(build) + # We execute these in sync to reduce IO. + BuildTraceSectionsWorker.new.perform(build.id) + BuildCoverageWorker.new.perform(build.id) + + # We execute these async as these are independent operations. + BuildHooksWorker.perform_async(build.id) + ArchiveTraceWorker.perform_async(build.id) + end end diff --git a/app/workers/expire_pipeline_cache_worker.rb b/app/workers/expire_pipeline_cache_worker.rb index c96e8a0379b..148384600b6 100644 --- a/app/workers/expire_pipeline_cache_worker.rb +++ b/app/workers/expire_pipeline_cache_worker.rb @@ -11,16 +11,9 @@ class ExpirePipelineCacheWorker pipeline = Ci::Pipeline.find_by(id: pipeline_id) return unless pipeline - project = pipeline.project store = Gitlab::EtagCaching::Store.new - store.touch(project_pipelines_path(project)) - store.touch(project_pipeline_path(project, pipeline)) - store.touch(commit_pipelines_path(project, pipeline.commit)) unless pipeline.commit.nil? - store.touch(new_merge_request_pipelines_path(project)) - each_pipelines_merge_request_path(project, pipeline) do |path| - store.touch(path) - end + update_etag_cache(pipeline, store) Gitlab::Cache::Ci::ProjectPipelineStatus.update_for_pipeline(pipeline) end @@ -51,4 +44,23 @@ class ExpirePipelineCacheWorker yield(path) end end + + # Updates ETag caches of a pipeline. + # + # This logic resides in a separate method so that EE can more easily extend + # it. + # + # @param [Ci::Pipeline] pipeline + # @param [Gitlab::EtagCaching::Store] store + def update_etag_cache(pipeline, store) + project = pipeline.project + + store.touch(project_pipelines_path(project)) + store.touch(project_pipeline_path(project, pipeline)) + store.touch(commit_pipelines_path(project, pipeline.commit)) unless pipeline.commit.nil? + store.touch(new_merge_request_pipelines_path(project)) + each_pipelines_merge_request_path(project, pipeline) do |path| + store.touch(path) + end + end end diff --git a/app/workers/git_garbage_collect_worker.rb b/app/workers/git_garbage_collect_worker.rb index d3628b23189..b33e9b1f718 100644 --- a/app/workers/git_garbage_collect_worker.rb +++ b/app/workers/git_garbage_collect_worker.rb @@ -23,6 +23,7 @@ class GitGarbageCollectWorker end task = task.to_sym + project.link_pool_repository gitaly_call(task, project.repository.raw_repository) # Refresh the branch cache in case garbage collection caused a ref lookup to fail diff --git a/app/workers/object_pool/join_worker.rb b/app/workers/object_pool/join_worker.rb index 07676011b2a..9c5161fd55a 100644 --- a/app/workers/object_pool/join_worker.rb +++ b/app/workers/object_pool/join_worker.rb @@ -5,14 +5,13 @@ module ObjectPool include ApplicationWorker include ObjectPoolQueue - def perform(pool_id, project_id) - pool = PoolRepository.find_by_id(pool_id) - return unless pool&.joinable? - + # The use of pool id is deprecated. Keeping the argument allows old jobs to + # still be performed. + def perform(_pool_id, project_id) project = Project.find_by_id(project_id) - return unless project + return unless project&.pool_repository&.joinable? - pool.link_repository(project.repository) + project.link_pool_repository Projects::HousekeepingService.new(project).execute end diff --git a/app/workers/remote_mirror_notification_worker.rb b/app/workers/remote_mirror_notification_worker.rb index 70c2e857d09..5bafe8e2046 100644 --- a/app/workers/remote_mirror_notification_worker.rb +++ b/app/workers/remote_mirror_notification_worker.rb @@ -9,7 +9,10 @@ class RemoteMirrorNotificationWorker # We check again if there's an error because a newer run since this job was # fired could've completed successfully. return unless remote_mirror && remote_mirror.last_error.present? + return if remote_mirror.error_notification_sent? NotificationService.new.remote_mirror_update_failed(remote_mirror) + + remote_mirror.after_sent_notification end end diff --git a/bin/changelog b/bin/changelog index 758c036161e..328d9495b96 100755 --- a/bin/changelog +++ b/bin/changelog @@ -148,7 +148,7 @@ class ChangelogEntry def execute assert_feature_branch! - assert_title! + assert_title! unless editor assert_new_file! # Read type from $stdin unless is already set @@ -162,6 +162,10 @@ class ChangelogEntry write amend_commit if options.amend end + + if editor + system("#{editor} '#{file_path}'") + end end private @@ -180,6 +184,10 @@ class ChangelogEntry File.write(file_path, contents) end + def editor + ENV['EDITOR'] + end + def amend_commit fail_with "git add failed" unless system(*%W[git add #{file_path}]) diff --git a/changelogs/unreleased/18667-handle-push-opts.yml b/changelogs/unreleased/18667-handle-push-opts.yml deleted file mode 100644 index 204293476f6..00000000000 --- a/changelogs/unreleased/18667-handle-push-opts.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Handle ci.skip push option -merge_request: 15643 -author: Jonathon Reinhart -type: added diff --git a/changelogs/unreleased/23367-clarify-docs-allow-failure.yml b/changelogs/unreleased/23367-clarify-docs-allow-failure.yml deleted file mode 100644 index 221d9e83ffb..00000000000 --- a/changelogs/unreleased/23367-clarify-docs-allow-failure.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Clarifies docs about CI `allow_failure` -merge_request: 23367 -author: C.J. Jameson -type: other diff --git a/changelogs/unreleased/25341-add-what-s-new-menu-item-in-top-navigation.yml b/changelogs/unreleased/25341-add-what-s-new-menu-item-in-top-navigation.yml new file mode 100644 index 00000000000..da1777827cb --- /dev/null +++ b/changelogs/unreleased/25341-add-what-s-new-menu-item-in-top-navigation.yml @@ -0,0 +1,5 @@ +--- +title: Resolve Add What's new menu item in top navigation +merge_request: 23186 +author: +type: added diff --git a/changelogs/unreleased/26375-markdown-footnotes-not-working.yml b/changelogs/unreleased/26375-markdown-footnotes-not-working.yml new file mode 100644 index 00000000000..86adef84a2a --- /dev/null +++ b/changelogs/unreleased/26375-markdown-footnotes-not-working.yml @@ -0,0 +1,5 @@ +--- +title: Footnotes now render properly in markdown +merge_request: 24168 +author: +type: fixed diff --git a/changelogs/unreleased/27861-add-markdown-editing-buttons-to-the-file-editor.yml b/changelogs/unreleased/27861-add-markdown-editing-buttons-to-the-file-editor.yml deleted file mode 100644 index 00eb5223d58..00000000000 --- a/changelogs/unreleased/27861-add-markdown-editing-buttons-to-the-file-editor.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Add markdown helper buttons to file editor -merge_request: 23480 -author: -type: added diff --git a/changelogs/unreleased/29951-issue-creation-by-email-without-subaddressing.yml b/changelogs/unreleased/29951-issue-creation-by-email-without-subaddressing.yml deleted file mode 100644 index 4139099eac3..00000000000 --- a/changelogs/unreleased/29951-issue-creation-by-email-without-subaddressing.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: No longer require email subaddressing for issue creation by email -merge_request: 23523 -author: -type: changed diff --git a/changelogs/unreleased/30120-add-flat-square-badge-style.yml b/changelogs/unreleased/30120-add-flat-square-badge-style.yml new file mode 100644 index 00000000000..a542a58d3fc --- /dev/null +++ b/changelogs/unreleased/30120-add-flat-square-badge-style.yml @@ -0,0 +1,5 @@ +--- +title: Add flat-square badge style +merge_request: 24172 +author: Fabian Schneider @fabsrc +type: added diff --git a/changelogs/unreleased/34758-extend-can-create-cluster-logic.yml b/changelogs/unreleased/34758-extend-can-create-cluster-logic.yml deleted file mode 100644 index 65f5253a271..00000000000 --- a/changelogs/unreleased/34758-extend-can-create-cluster-logic.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Allow user to add Kubernetes cluster for clusterable when there are ancestor clusters -merge_request: 23569 -author: -type: other diff --git a/changelogs/unreleased/34758-list-ancestor-clusters.yml b/changelogs/unreleased/34758-list-ancestor-clusters.yml deleted file mode 100644 index 8fdba7ba90a..00000000000 --- a/changelogs/unreleased/34758-list-ancestor-clusters.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Show clusters of ancestors in cluster list page -merge_request: 22996 -author: -type: changed diff --git a/changelogs/unreleased/40270-remove-gitlab-upgrader-completely.yml b/changelogs/unreleased/40270-remove-gitlab-upgrader-completely.yml deleted file mode 100644 index 9ea2157bfb7..00000000000 --- a/changelogs/unreleased/40270-remove-gitlab-upgrader-completely.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Removes all instances of deprecated Gitlab Upgrader calls -merge_request: 23603 -author: '@jwolen' -type: removed diff --git a/changelogs/unreleased/40473-api-support-for-kubernetes-integration.yml b/changelogs/unreleased/40473-api-support-for-kubernetes-integration.yml deleted file mode 100644 index 5567aad6320..00000000000 --- a/changelogs/unreleased/40473-api-support-for-kubernetes-integration.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Add API Support for Kubernetes integration -merge_request: 23922 -author: -type: added diff --git a/changelogs/unreleased/41766-vue-component.yml b/changelogs/unreleased/41766-vue-component.yml deleted file mode 100644 index 12343c8ce84..00000000000 --- a/changelogs/unreleased/41766-vue-component.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Creates component for release block -merge_request: 23697 -author: -type: added diff --git a/changelogs/unreleased/41766-vuex-store.yml b/changelogs/unreleased/41766-vuex-store.yml deleted file mode 100644 index f20fc736a6f..00000000000 --- a/changelogs/unreleased/41766-vuex-store.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Creates frontend app for releases -merge_request: 23796 -author: -type: added diff --git a/changelogs/unreleased/42125-extend-override-check-to-also-check-arity.yml b/changelogs/unreleased/42125-extend-override-check-to-also-check-arity.yml deleted file mode 100644 index 9892466ca50..00000000000 --- a/changelogs/unreleased/42125-extend-override-check-to-also-check-arity.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Extend override check to also check arity -merge_request: 23498 -author: Jacopo Beschi @jacopo-beschi -type: added diff --git a/changelogs/unreleased/42769-remove-expansion-hover-animation-from-status-icon-buttons.yml b/changelogs/unreleased/42769-remove-expansion-hover-animation-from-status-icon-buttons.yml new file mode 100644 index 00000000000..5a4ff8b3358 --- /dev/null +++ b/changelogs/unreleased/42769-remove-expansion-hover-animation-from-status-icon-buttons.yml @@ -0,0 +1,5 @@ +--- +title: Remove expansion hover animation from pipeline status icon buttons +merge_request: 24268 +author: Nathan Friend +type: changed diff --git a/changelogs/unreleased/43623-add-submit-feedback-in-product-feedback-link.yml b/changelogs/unreleased/43623-add-submit-feedback-in-product-feedback-link.yml deleted file mode 100644 index f5d99e9a448..00000000000 --- a/changelogs/unreleased/43623-add-submit-feedback-in-product-feedback-link.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Add submit feedback link to help dropdown -merge_request: 23547 -author: -type: added diff --git a/changelogs/unreleased/44353-improve-snippet-search-performance.yml b/changelogs/unreleased/44353-improve-snippet-search-performance.yml deleted file mode 100644 index 2ecbcef8843..00000000000 --- a/changelogs/unreleased/44353-improve-snippet-search-performance.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Improve snippet search performance by removing duplicate counts -merge_request: 23952 -author: -type: performance diff --git a/changelogs/unreleased/44984-use-serializer-for-issuable-sidebar.yml b/changelogs/unreleased/44984-use-serializer-for-issuable-sidebar.yml deleted file mode 100644 index ba9edc8740d..00000000000 --- a/changelogs/unreleased/44984-use-serializer-for-issuable-sidebar.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Refactor issuable sidebar to use serializer -merge_request: 23379 -author: -type: other diff --git a/changelogs/unreleased/47007-related-merge-requests-in-issue-design-restyle.yml b/changelogs/unreleased/47007-related-merge-requests-in-issue-design-restyle.yml new file mode 100644 index 00000000000..28e2a4cc377 --- /dev/null +++ b/changelogs/unreleased/47007-related-merge-requests-in-issue-design-restyle.yml @@ -0,0 +1,5 @@ +--- +title: Redesigned related merge requests in issue page. +merge_request: 24270 +author: +type: changed diff --git a/changelogs/unreleased/47052-merge-button-does-not-appear-after-rebase-ing.yml b/changelogs/unreleased/47052-merge-button-does-not-appear-after-rebase-ing.yml deleted file mode 100644 index fd1e4605f2d..00000000000 --- a/changelogs/unreleased/47052-merge-button-does-not-appear-after-rebase-ing.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Allow merge after rebase without page refresh on FF repositories -merge_request: 23572 -author: -type: fixed diff --git a/changelogs/unreleased/47988-improve-milestone-queries-with-subqueries.yml b/changelogs/unreleased/47988-improve-milestone-queries-with-subqueries.yml new file mode 100644 index 00000000000..d1a80ab43cf --- /dev/null +++ b/changelogs/unreleased/47988-improve-milestone-queries-with-subqueries.yml @@ -0,0 +1,5 @@ +--- +title: Improve milestone queries using subqueries instead of separate queries for ids +merge_request: 24325 +author: +type: performance diff --git a/changelogs/unreleased/49056-configure-auto-devops-deployed-applications-with-secrets-that-aren-t-committed-to-the-repo.yml b/changelogs/unreleased/49056-configure-auto-devops-deployed-applications-with-secrets-that-aren-t-committed-to-the-repo.yml deleted file mode 100644 index 65efa85176b..00000000000 --- a/changelogs/unreleased/49056-configure-auto-devops-deployed-applications-with-secrets-that-aren-t-committed-to-the-repo.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Configure Auto DevOps deployed applications with secrets from prefixed CI variables -merge_request: 23719 -author: -type: added diff --git a/changelogs/unreleased/49231-import-issues-csv.yml b/changelogs/unreleased/49231-import-issues-csv.yml deleted file mode 100644 index c10bd8143b2..00000000000 --- a/changelogs/unreleased/49231-import-issues-csv.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Add importing of issues from CSV file -merge_request: 23532 -author: -type: added diff --git a/changelogs/unreleased/50013-add-browser-platform-flags.yml b/changelogs/unreleased/50013-add-browser-platform-flags.yml new file mode 100644 index 00000000000..6176b8b64a7 --- /dev/null +++ b/changelogs/unreleased/50013-add-browser-platform-flags.yml @@ -0,0 +1,5 @@ +--- +title: Add CSS & JS global flags to represent browser and platform +merge_request: 24017 +author: +type: other diff --git a/changelogs/unreleased/51485-new-issue-labels-note.yml b/changelogs/unreleased/51485-new-issue-labels-note.yml deleted file mode 100644 index a312d379ce2..00000000000 --- a/changelogs/unreleased/51485-new-issue-labels-note.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Create system notes on issue / MR creation when labels, milestone, or due date is set -merge_request: 23859 -author: -type: added diff --git a/changelogs/unreleased/51606-expanding-a-diff-while-having-an-open-comment-form-will-always-scroll-down-to-the-comment.yml b/changelogs/unreleased/51606-expanding-a-diff-while-having-an-open-comment-form-will-always-scroll-down-to-the-comment.yml deleted file mode 100644 index a845234b42f..00000000000 --- a/changelogs/unreleased/51606-expanding-a-diff-while-having-an-open-comment-form-will-always-scroll-down-to-the-comment.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Stop autofocusing on diff comment after initial mount -merge_request: 23849 -author: -type: fixed diff --git a/changelogs/unreleased/51944-redesign-project-lists-ui.yml b/changelogs/unreleased/51944-redesign-project-lists-ui.yml deleted file mode 100644 index 56f9a86a686..00000000000 --- a/changelogs/unreleased/51944-redesign-project-lists-ui.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Redesign project lists UI -merge_request: 22682 -author: -type: other diff --git a/changelogs/unreleased/51970-correct-ordering-of-metrics.yml b/changelogs/unreleased/51970-correct-ordering-of-metrics.yml deleted file mode 100644 index fbc7b58d901..00000000000 --- a/changelogs/unreleased/51970-correct-ordering-of-metrics.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Correct the ordering of metrics on the performance dashboard -merge_request: 23630 -author: -type: fixed diff --git a/changelogs/unreleased/51994-disable-merging-labels-in-dropdowns.yml b/changelogs/unreleased/51994-disable-merging-labels-in-dropdowns.yml deleted file mode 100644 index 2d54cf814b7..00000000000 --- a/changelogs/unreleased/51994-disable-merging-labels-in-dropdowns.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Disable merging of labels with same names -merge_request: 23265 -author: -type: changed diff --git a/changelogs/unreleased/52275-fix-master-to-be-hyperlink.yml b/changelogs/unreleased/52275-fix-master-to-be-hyperlink.yml new file mode 100644 index 00000000000..c1cde0ceff6 --- /dev/null +++ b/changelogs/unreleased/52275-fix-master-to-be-hyperlink.yml @@ -0,0 +1,5 @@ +--- +title: Resolve In Merge Request diff screen, master is not a hyperlink +merge_request: 23874 +author: +type: fixed diff --git a/changelogs/unreleased/52278-squash-checkbox-fix.yml b/changelogs/unreleased/52278-squash-checkbox-fix.yml new file mode 100644 index 00000000000..c81748ae419 --- /dev/null +++ b/changelogs/unreleased/52278-squash-checkbox-fix.yml @@ -0,0 +1,5 @@ +--- +title: Resolve When merging an MR, the squash checkbox isnt always supported +merge_request: 24296 +author: +type: fixed diff --git a/changelogs/unreleased/52363-modifies-environment-scope-field-on-cluster-page.yml b/changelogs/unreleased/52363-modifies-environment-scope-field-on-cluster-page.yml new file mode 100644 index 00000000000..07cb35e6529 --- /dev/null +++ b/changelogs/unreleased/52363-modifies-environment-scope-field-on-cluster-page.yml @@ -0,0 +1,5 @@ +--- +title: Modifies environment scope UI on cluster page +merge_request: 24376 +author: +type: other diff --git a/changelogs/unreleased/52446-hide-ado-project-banner-for-ci-file-or-ci-disabled.yml b/changelogs/unreleased/52446-hide-ado-project-banner-for-ci-file-or-ci-disabled.yml deleted file mode 100644 index bd8d0699bd1..00000000000 --- a/changelogs/unreleased/52446-hide-ado-project-banner-for-ci-file-or-ci-disabled.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Don't show Auto DevOps enabled banner for projects with CI file or CI disabled -merge_request: 24067 -author: -type: other diff --git a/changelogs/unreleased/52620-fix-loader-animation-alignment.yml b/changelogs/unreleased/52620-fix-loader-animation-alignment.yml deleted file mode 100644 index 5cfb7fc019f..00000000000 --- a/changelogs/unreleased/52620-fix-loader-animation-alignment.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Aligns build loader animation with the job log -merge_request: 23959 -author: -type: fixed diff --git a/changelogs/unreleased/52888-status-emoji-should-not-be-added-to-awards-section-on-issue-page-2.yml b/changelogs/unreleased/52888-status-emoji-should-not-be-added-to-awards-section-on-issue-page-2.yml deleted file mode 100644 index 501940d6da3..00000000000 --- a/changelogs/unreleased/52888-status-emoji-should-not-be-added-to-awards-section-on-issue-page-2.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Prevent awards emoji being updated when updating status -merge_request: 23470 -author: -type: fixed diff --git a/changelogs/unreleased/52971-merge-request-file-browser-should-always-be-possible-show-hide.yml b/changelogs/unreleased/52971-merge-request-file-browser-should-always-be-possible-show-hide.yml new file mode 100644 index 00000000000..b661c55957d --- /dev/null +++ b/changelogs/unreleased/52971-merge-request-file-browser-should-always-be-possible-show-hide.yml @@ -0,0 +1,5 @@ +--- +title: Make possible to toggle file tree while scrolling through diffs +merge_request: !24103 +author: +type: changed diff --git a/changelogs/unreleased/53020-user-specific-profile-page-settings-fields-don-t-have-help-text-placeholders.yml b/changelogs/unreleased/53020-user-specific-profile-page-settings-fields-don-t-have-help-text-placeholders.yml deleted file mode 100644 index 99da02dd31a..00000000000 --- a/changelogs/unreleased/53020-user-specific-profile-page-settings-fields-don-t-have-help-text-placeholders.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Adds explanatory text to input fields on user profile settings page -merge_request: 23673 -author: -type: other diff --git a/changelogs/unreleased/53493-list-id-email-header.yml b/changelogs/unreleased/53493-list-id-email-header.yml deleted file mode 100644 index 09a0639f6f5..00000000000 --- a/changelogs/unreleased/53493-list-id-email-header.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Add project identifier as List-Id email Header to ease filtering -merge_request: 22817 -author: Olivier Crête -type: added diff --git a/changelogs/unreleased/53671-redirect-projects-id-to-project-page.yml b/changelogs/unreleased/53671-redirect-projects-id-to-project-page.yml new file mode 100644 index 00000000000..08c5ded05d5 --- /dev/null +++ b/changelogs/unreleased/53671-redirect-projects-id-to-project-page.yml @@ -0,0 +1,5 @@ +--- +title: Redirect GET projects/:id to project page +merge_request: 24467 +author: +type: added diff --git a/changelogs/unreleased/53696-make-rbac-default.yml b/changelogs/unreleased/53696-make-rbac-default.yml deleted file mode 100644 index 4f1326cd874..00000000000 --- a/changelogs/unreleased/53696-make-rbac-default.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Make RBAC enabled default for new clusters -merge_request: 24119 -author: -type: changed diff --git a/changelogs/unreleased/53714-inconsistent-text-color-for-labels.yml b/changelogs/unreleased/53714-inconsistent-text-color-for-labels.yml new file mode 100644 index 00000000000..d804e2df2cd --- /dev/null +++ b/changelogs/unreleased/53714-inconsistent-text-color-for-labels.yml @@ -0,0 +1,5 @@ +--- +title: Fix foreground color for labels to ensure consistency of label appearance +merge_request: 23873 +author: Nathan Friend +type: fixed diff --git a/changelogs/unreleased/53796-discard-draft-comment-button-to-easy-to-accidentally-hit-on-mobile.yml b/changelogs/unreleased/53796-discard-draft-comment-button-to-easy-to-accidentally-hit-on-mobile.yml deleted file mode 100644 index 083b5f21a52..00000000000 --- a/changelogs/unreleased/53796-discard-draft-comment-button-to-easy-to-accidentally-hit-on-mobile.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Removed discard draft comment button form notes -merge_request: 24185 -author: -type: removed diff --git a/changelogs/unreleased/53856-changing-group-visibility-does-not-re-enable-save-button.yml b/changelogs/unreleased/53856-changing-group-visibility-does-not-re-enable-save-button.yml new file mode 100644 index 00000000000..1daa72fb9c4 --- /dev/null +++ b/changelogs/unreleased/53856-changing-group-visibility-does-not-re-enable-save-button.yml @@ -0,0 +1,6 @@ +--- +title: Fix suboptimal handling of checkbox and radio input events causing + group general settings submit button to stay disabled after changing its visibility +merge_request: 23022 +author: +type: fixed diff --git a/changelogs/unreleased/53907-improve-milestone-links.yml b/changelogs/unreleased/53907-improve-milestone-links.yml deleted file mode 100644 index 8e867e783cc..00000000000 --- a/changelogs/unreleased/53907-improve-milestone-links.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Add % prefix to milestone reference links -merge_request: 23928 -author: -type: changed diff --git a/changelogs/unreleased/53933-include-dates-in-milestone-change-email.yml b/changelogs/unreleased/53933-include-dates-in-milestone-change-email.yml deleted file mode 100644 index 5c40a1e900c..00000000000 --- a/changelogs/unreleased/53933-include-dates-in-milestone-change-email.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Add date range in milestone change email notifications -merge_request: 23762 -author: -type: changed diff --git a/changelogs/unreleased/53954-resolved-non-diff-discussions-on-merge-requests-no-longer-show-who-resolved-them-and-when-at-a-glance.yml b/changelogs/unreleased/53954-resolved-non-diff-discussions-on-merge-requests-no-longer-show-who-resolved-them-and-when-at-a-glance.yml deleted file mode 100644 index 0632c1992c7..00000000000 --- a/changelogs/unreleased/53954-resolved-non-diff-discussions-on-merge-requests-no-longer-show-who-resolved-them-and-when-at-a-glance.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Show message on non-diff discussions -merge_request: -author: -type: changed diff --git a/changelogs/unreleased/53966-hashed-storage-read-only.yml b/changelogs/unreleased/53966-hashed-storage-read-only.yml deleted file mode 100644 index 2b6c9c49c85..00000000000 --- a/changelogs/unreleased/53966-hashed-storage-read-only.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: 'Hashed Storage: Only set as `read_only` when starting the per-project migration' -merge_request: 24128 -author: -type: changed diff --git a/changelogs/unreleased/54142-pages-in-project-s-permission-should-be-named-pages-access-control.yml b/changelogs/unreleased/54142-pages-in-project-s-permission-should-be-named-pages-access-control.yml deleted file mode 100644 index b45ebaa1a02..00000000000 --- a/changelogs/unreleased/54142-pages-in-project-s-permission-should-be-named-pages-access-control.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Make the Pages permission setting more clear -merge_request: 23146 -author: -type: changed diff --git a/changelogs/unreleased/54146-fix-calendar-query.yml b/changelogs/unreleased/54146-fix-calendar-query.yml deleted file mode 100644 index dcac343108a..00000000000 --- a/changelogs/unreleased/54146-fix-calendar-query.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix project calendar feed when sorted by priority -merge_request: 23870 -author: -type: fixed diff --git a/changelogs/unreleased/54206-show-the-activity-filter-dropdown-in-discussion-tab-only.yml b/changelogs/unreleased/54206-show-the-activity-filter-dropdown-in-discussion-tab-only.yml deleted file mode 100644 index e29987b0935..00000000000 --- a/changelogs/unreleased/54206-show-the-activity-filter-dropdown-in-discussion-tab-only.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Discussion filter only displayed in discussions tab for merge requests -merge_request: 24082 -author: -type: changed diff --git a/changelogs/unreleased/54311-fix-board-add-label.yml b/changelogs/unreleased/54311-fix-board-add-label.yml deleted file mode 100644 index 8fd8f7a0381..00000000000 --- a/changelogs/unreleased/54311-fix-board-add-label.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix error when creating labels in a new issue in the boards page -merge_request: 24039 -author: Ruben Moya -type: fixed diff --git a/changelogs/unreleased/54386-integrate-mobile-css-framework-into-specific-frameworks.yml b/changelogs/unreleased/54386-integrate-mobile-css-framework-into-specific-frameworks.yml deleted file mode 100644 index e446d2a2781..00000000000 --- a/changelogs/unreleased/54386-integrate-mobile-css-framework-into-specific-frameworks.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Remove framework/mobile.scss -merge_request: 23301 -author: Takuya Noguchi -type: other diff --git a/changelogs/unreleased/54427-label-xss.yml b/changelogs/unreleased/54427-label-xss.yml deleted file mode 100644 index 090d1832af2..00000000000 --- a/changelogs/unreleased/54427-label-xss.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Escape html entities in LabelReferenceFilter when no label found -merge_request: -author: -type: security diff --git a/changelogs/unreleased/54736-sign-in-bottom-margin.yml b/changelogs/unreleased/54736-sign-in-bottom-margin.yml deleted file mode 100644 index 32b5b44fe35..00000000000 --- a/changelogs/unreleased/54736-sign-in-bottom-margin.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix login box bottom margins on signin page -merge_request: 23739 -author: '@gear54' -type: fixed diff --git a/changelogs/unreleased/54786-mr-empty-file-display.yml b/changelogs/unreleased/54786-mr-empty-file-display.yml deleted file mode 100644 index 5adf5744755..00000000000 --- a/changelogs/unreleased/54786-mr-empty-file-display.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Display empty files properly on MR diffs -merge_request: 23671 -author: Sean Nichols -type: fixed diff --git a/changelogs/unreleased/54814-sidebar-styling-updates.yml b/changelogs/unreleased/54814-sidebar-styling-updates.yml deleted file mode 100644 index 98e3836ee14..00000000000 --- a/changelogs/unreleased/54814-sidebar-styling-updates.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix label and header styles in the job details sidebar. -merge_request: 23816 -author: Nathan Friend -type: changed diff --git a/changelogs/unreleased/54844-report-syntax-dep-scan-ado.yml b/changelogs/unreleased/54844-report-syntax-dep-scan-ado.yml deleted file mode 100644 index 95fc5cb804d..00000000000 --- a/changelogs/unreleased/54844-report-syntax-dep-scan-ado.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Use reports syntax for Dependency scanning in Auto DevOps -merge_request: 24081 -author: -type: added diff --git a/changelogs/unreleased/54953-error-500-viewing-merge-request-due-to-nil-commit_email_hostname.yml b/changelogs/unreleased/54953-error-500-viewing-merge-request-due-to-nil-commit_email_hostname.yml deleted file mode 100644 index 8fd127acf2b..00000000000 --- a/changelogs/unreleased/54953-error-500-viewing-merge-request-due-to-nil-commit_email_hostname.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Return an ApplicationSetting in CurrentSettings -merge_request: 23766 -author: -type: fixed diff --git a/changelogs/unreleased/54981-extended-user-centric-tooltips-add-missing-cases.yml b/changelogs/unreleased/54981-extended-user-centric-tooltips-add-missing-cases.yml deleted file mode 100644 index 25ae6d88428..00000000000 --- a/changelogs/unreleased/54981-extended-user-centric-tooltips-add-missing-cases.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: User Popovers for Commit Infos, Member Lists and Snippets -merge_request: 24132 -author: -type: added diff --git a/changelogs/unreleased/55111-gitlab-api-does-not-manage-default_branch_protection-3-value.yml b/changelogs/unreleased/55111-gitlab-api-does-not-manage-default_branch_protection-3-value.yml new file mode 100644 index 00000000000..b609fc2d60b --- /dev/null +++ b/changelogs/unreleased/55111-gitlab-api-does-not-manage-default_branch_protection-3-value.yml @@ -0,0 +1,5 @@ +--- +title: 'API: Fix default_branch_protection admin setting' +merge_request: 24398 +author: Robert Schilling +type: fixed diff --git a/changelogs/unreleased/55191-update-workhorse.yml b/changelogs/unreleased/55191-update-workhorse.yml deleted file mode 100644 index d16518e673a..00000000000 --- a/changelogs/unreleased/55191-update-workhorse.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Update GitLab Workhorse to v8.0.0 -merge_request: 23740 -author: -type: other diff --git a/changelogs/unreleased/55192-about-link-in-new-window.yml b/changelogs/unreleased/55192-about-link-in-new-window.yml deleted file mode 100644 index b686150942b..00000000000 --- a/changelogs/unreleased/55192-about-link-in-new-window.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Resolve About this feature link should open in new window -merge_request: 24149 -author: -type: fixed diff --git a/changelogs/unreleased/55266-fix-incorrect-due-date-parsing.yml b/changelogs/unreleased/55266-fix-incorrect-due-date-parsing.yml deleted file mode 100644 index 62a57085192..00000000000 --- a/changelogs/unreleased/55266-fix-incorrect-due-date-parsing.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Use 'parsePikadayDate' to parse due date string -merge_request: 24045 -author: -type: fixed diff --git a/changelogs/unreleased/55293-split-bio-into-individual-line-in-extended-user-tooltips.yml b/changelogs/unreleased/55293-split-bio-into-individual-line-in-extended-user-tooltips.yml deleted file mode 100644 index c6ff52b0fa1..00000000000 --- a/changelogs/unreleased/55293-split-bio-into-individual-line-in-extended-user-tooltips.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Split bio into individual line in extended user tooltips -merge_request: 23940 -author: -type: other diff --git a/changelogs/unreleased/55344-only-prompt-user-once-when-navigating-away-from-file-editor.yml b/changelogs/unreleased/55344-only-prompt-user-once-when-navigating-away-from-file-editor.yml deleted file mode 100644 index 9c4d73c5323..00000000000 --- a/changelogs/unreleased/55344-only-prompt-user-once-when-navigating-away-from-file-editor.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Only prompt user once when navigating away from file editor -merge_request: 23820 -author: Sam Bigelow -type: fixed diff --git a/changelogs/unreleased/55369-update-milestone-sort-to-say-say-milestone-due-date.yml b/changelogs/unreleased/55369-update-milestone-sort-to-say-say-milestone-due-date.yml deleted file mode 100644 index 7476b9caa93..00000000000 --- a/changelogs/unreleased/55369-update-milestone-sort-to-say-say-milestone-due-date.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Renames Milestone sort into Milestone due date -merge_request: 24080 -author: Jacopo Beschi @jacopo-beschi -type: changed diff --git a/changelogs/unreleased/55484-fix-edit-button.yml b/changelogs/unreleased/55484-fix-edit-button.yml deleted file mode 100644 index c8998cba248..00000000000 --- a/changelogs/unreleased/55484-fix-edit-button.yml +++ /dev/null @@ -1,4 +0,0 @@ -title: Fix edit button disappearing in issue title -merge_request: 23948 -author: Ruben Moya -type: fixed diff --git a/changelogs/unreleased/55669-redesign-project-lists-ui-further-improvements.yml b/changelogs/unreleased/55669-redesign-project-lists-ui-further-improvements.yml deleted file mode 100644 index a51a08c892a..00000000000 --- a/changelogs/unreleased/55669-redesign-project-lists-ui-further-improvements.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: UI improvements for redesigned project lists -merge_request: 24011 -author: -type: other diff --git a/changelogs/unreleased/55670-remove-app-views-shared-issuable-_filter-html-haml.yml b/changelogs/unreleased/55670-remove-app-views-shared-issuable-_filter-html-haml.yml deleted file mode 100644 index 9d37f798250..00000000000 --- a/changelogs/unreleased/55670-remove-app-views-shared-issuable-_filter-html-haml.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Remove app/views/shared/issuable/_filter.html.haml -merge_request: 24008 -author: Takuya Noguchi -type: other diff --git a/changelogs/unreleased/55716-update-cert-manager-chart-from-v0-5-0-to-v0-5-2.yml b/changelogs/unreleased/55716-update-cert-manager-chart-from-v0-5-0-to-v0-5-2.yml deleted file mode 100644 index a25ace9d76d..00000000000 --- a/changelogs/unreleased/55716-update-cert-manager-chart-from-v0-5-0-to-v0-5-2.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Update cert-manager chart from v0.5.0 to v0.5.2 -merge_request: 24025 -author: Takuya Noguchi -type: other diff --git a/changelogs/unreleased/55721-externalization-for-pipeline-tags.yml b/changelogs/unreleased/55721-externalization-for-pipeline-tags.yml deleted file mode 100644 index 4062300e73f..00000000000 --- a/changelogs/unreleased/55721-externalization-for-pipeline-tags.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Correctly externalize pipeline tags -merge_request: 24028 -author: -type: fixed diff --git a/changelogs/unreleased/55755-user-activity-is-stuck-loading-when-there-is-none.yml b/changelogs/unreleased/55755-user-activity-is-stuck-loading-when-there-is-none.yml deleted file mode 100644 index 5362a781281..00000000000 --- a/changelogs/unreleased/55755-user-activity-is-stuck-loading-when-there-is-none.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Hide spinner on empty activites list on user profile overview -merge_request: 24063 -author: -type: other diff --git a/changelogs/unreleased/55836-docs-fix-navigation-style-in-docs.yml b/changelogs/unreleased/55836-docs-fix-navigation-style-in-docs.yml deleted file mode 100644 index 2ac3599175b..00000000000 --- a/changelogs/unreleased/55836-docs-fix-navigation-style-in-docs.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix navigation style in docs -merge_request: 24090 -author: Takuya Noguchi -type: other diff --git a/changelogs/unreleased/55838-remove-gem-install-bundler-from-docker-based-ruby-environments.yml b/changelogs/unreleased/55838-remove-gem-install-bundler-from-docker-based-ruby-environments.yml deleted file mode 100644 index 08f60d205df..00000000000 --- a/changelogs/unreleased/55838-remove-gem-install-bundler-from-docker-based-ruby-environments.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Remove gem install bundler from Docker-based Ruby environments -merge_request: 24093 -author: Takuya Noguchi -type: other diff --git a/changelogs/unreleased/55883-modal-header-titles-have-an-unnecessary-top-margin.yml b/changelogs/unreleased/55883-modal-header-titles-have-an-unnecessary-top-margin.yml deleted file mode 100644 index 7dc783cc2b8..00000000000 --- a/changelogs/unreleased/55883-modal-header-titles-have-an-unnecessary-top-margin.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Remove top margin in modal header titles -merge_request: 24108 -author: -type: fixed diff --git a/changelogs/unreleased/55945-suggested-change-highlight.yml b/changelogs/unreleased/55945-suggested-change-highlight.yml deleted file mode 100644 index 611854d36ab..00000000000 --- a/changelogs/unreleased/55945-suggested-change-highlight.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Add syntax highlighting to suggestion diff -merge_request: 24156 -author: -type: fixed diff --git a/changelogs/unreleased/55945-suggested-change-preview-highlight.yml b/changelogs/unreleased/55945-suggested-change-preview-highlight.yml new file mode 100644 index 00000000000..997290a5d50 --- /dev/null +++ b/changelogs/unreleased/55945-suggested-change-preview-highlight.yml @@ -0,0 +1,5 @@ +--- +title: Fix syntax highlighting for suggested changes preview +merge_request: 24358 +author: +type: fixed diff --git a/changelogs/unreleased/55958-inconsistent-spacing-between-note-and-user-avatar-in-discussions.yml b/changelogs/unreleased/55958-inconsistent-spacing-between-note-and-user-avatar-in-discussions.yml deleted file mode 100644 index 765398cda84..00000000000 --- a/changelogs/unreleased/55958-inconsistent-spacing-between-note-and-user-avatar-in-discussions.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix spacing on discussions -merge_request: !24197 -author: -type: fixed diff --git a/changelogs/unreleased/55966-when-ref-is-ambiguous-createpipelineservice-raises-an-error.yml b/changelogs/unreleased/55966-when-ref-is-ambiguous-createpipelineservice-raises-an-error.yml new file mode 100644 index 00000000000..01a162944d3 --- /dev/null +++ b/changelogs/unreleased/55966-when-ref-is-ambiguous-createpipelineservice-raises-an-error.yml @@ -0,0 +1,5 @@ +--- +title: Prevent checking protected_ref? for ambiguous refs. +merge_request: 24437 +author: +type: fixed diff --git a/changelogs/unreleased/56076-releases-margin.yml b/changelogs/unreleased/56076-releases-margin.yml deleted file mode 100644 index a3cae1e035f..00000000000 --- a/changelogs/unreleased/56076-releases-margin.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fixes missing margin in releases block -merge_request: -author: -type: fixed diff --git a/changelogs/unreleased/56363-inconsitent-file-size-indication-across-different-ci-pages.yml b/changelogs/unreleased/56363-inconsitent-file-size-indication-across-different-ci-pages.yml new file mode 100644 index 00000000000..7c923422534 --- /dev/null +++ b/changelogs/unreleased/56363-inconsitent-file-size-indication-across-different-ci-pages.yml @@ -0,0 +1,6 @@ +--- +title: Show CI artifact file size with 3 significant digits on 'browse job artifacts' + page +merge_request: 24387 +author: +type: fixed diff --git a/changelogs/unreleased/56371-don-t-check-confidential-issues-for-spam.yml b/changelogs/unreleased/56371-don-t-check-confidential-issues-for-spam.yml new file mode 100644 index 00000000000..fcfa29977d1 --- /dev/null +++ b/changelogs/unreleased/56371-don-t-check-confidential-issues-for-spam.yml @@ -0,0 +1,5 @@ +--- +title: Do not run spam checks on confidential issues +merge_request: 24453 +author: +type: fixed diff --git a/changelogs/unreleased/56389-remove-unwanted-suggestion-flash-margin.yml b/changelogs/unreleased/56389-remove-unwanted-suggestion-flash-margin.yml new file mode 100644 index 00000000000..3494feb9be1 --- /dev/null +++ b/changelogs/unreleased/56389-remove-unwanted-suggestion-flash-margin.yml @@ -0,0 +1,5 @@ +--- +title: Remove unwanted margin above suggested changes. +merge_request: 24419 +author: +type: fixed diff --git a/changelogs/unreleased/56507-sh-bump-katex-0.10.0.yml b/changelogs/unreleased/56507-sh-bump-katex-0.10.0.yml new file mode 100644 index 00000000000..671e204da21 --- /dev/null +++ b/changelogs/unreleased/56507-sh-bump-katex-0.10.0.yml @@ -0,0 +1,5 @@ +--- +title: Upgrade KaTeX to version 0.10.0 +merge_request: 24478 +author: Andrew Harmon +type: fixed
\ No newline at end of file diff --git a/changelogs/unreleased/56547-limit-sidekiq-logging-based-on-argument-size.yml b/changelogs/unreleased/56547-limit-sidekiq-logging-based-on-argument-size.yml new file mode 100644 index 00000000000..9ef274f3b49 --- /dev/null +++ b/changelogs/unreleased/56547-limit-sidekiq-logging-based-on-argument-size.yml @@ -0,0 +1,5 @@ +--- +title: Prevent Sidekiq arguments over 10 KB in size from being logged to JSON +merge_request: 24493 +author: +type: changed diff --git a/changelogs/unreleased/56622-admin-settings-cannot-read-property-addeventlistener-of-null.yml b/changelogs/unreleased/56622-admin-settings-cannot-read-property-addeventlistener-of-null.yml new file mode 100644 index 00000000000..52b2db0e999 --- /dev/null +++ b/changelogs/unreleased/56622-admin-settings-cannot-read-property-addeventlistener-of-null.yml @@ -0,0 +1,5 @@ +--- +title: Load initUserInternalRegexPlaceholder only when required +merge_request: 24522 +author: +type: fixed diff --git a/changelogs/unreleased/56636-hashed-storage-afterrenameservice.yml b/changelogs/unreleased/56636-hashed-storage-afterrenameservice.yml new file mode 100644 index 00000000000..1f808850554 --- /dev/null +++ b/changelogs/unreleased/56636-hashed-storage-afterrenameservice.yml @@ -0,0 +1,5 @@ +--- +title: 'Hashed Storage: `AfterRenameService` was receiving the wrong `old_path` under some circunstances' +merge_request: 24526 +author: +type: fixed diff --git a/changelogs/unreleased/8688-recursive-pipelines-ce-backport.yml b/changelogs/unreleased/8688-recursive-pipelines-ce-backport.yml new file mode 100644 index 00000000000..cd7b56a1e05 --- /dev/null +++ b/changelogs/unreleased/8688-recursive-pipelines-ce-backport.yml @@ -0,0 +1,5 @@ +--- +title: Creates mixin to reduce code duplication between CE and EE in graph component +merge_request: +author: +type: other diff --git a/changelogs/unreleased/Projects--dropdown-is-misaligned-on-issue-boards-page.yml b/changelogs/unreleased/Projects--dropdown-is-misaligned-on-issue-boards-page.yml new file mode 100644 index 00000000000..49511294c48 --- /dev/null +++ b/changelogs/unreleased/Projects--dropdown-is-misaligned-on-issue-boards-page.yml @@ -0,0 +1,5 @@ +--- +title: Proper align Projects dropdown on issue boards page +merge_request: 24277 +author: Johann Hubert Sonntagbauer +type: fixed diff --git a/changelogs/unreleased/ab-50763-persist-index.yml b/changelogs/unreleased/ab-50763-persist-index.yml deleted file mode 100644 index 0cf11923c5a..00000000000 --- a/changelogs/unreleased/ab-50763-persist-index.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Add indexes to speed up CI query. -merge_request: 23188 -author: -type: performance diff --git a/changelogs/unreleased/ac-pages-subgroups.yml b/changelogs/unreleased/ac-pages-subgroups.yml new file mode 100644 index 00000000000..ef5a0c1872e --- /dev/null +++ b/changelogs/unreleased/ac-pages-subgroups.yml @@ -0,0 +1,5 @@ +--- +title: Pages for subgroups +merge_request: 23505 +author: +type: added diff --git a/changelogs/unreleased/ac-releases-api-with-assets.yml b/changelogs/unreleased/ac-releases-api-with-assets.yml deleted file mode 100644 index b29319ae683..00000000000 --- a/changelogs/unreleased/ac-releases-api-with-assets.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Support CURD operation for Links as one of the Release assets -merge_request: 24056 -author: -type: changed diff --git a/changelogs/unreleased/ac-releases-api.yml b/changelogs/unreleased/ac-releases-api.yml deleted file mode 100644 index 87217cce371..00000000000 --- a/changelogs/unreleased/ac-releases-api.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Releases API -merge_request: 23795 -author: -type: added diff --git a/changelogs/unreleased/ac-releases-name-sha-author.yml b/changelogs/unreleased/ac-releases-name-sha-author.yml deleted file mode 100644 index e84b82847eb..00000000000 --- a/changelogs/unreleased/ac-releases-name-sha-author.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Add name, author_id, and sha to releases table -merge_request: 23763 -author: -type: added diff --git a/changelogs/unreleased/actioncontroller-parameters-deprecations.yml b/changelogs/unreleased/actioncontroller-parameters-deprecations.yml new file mode 100644 index 00000000000..ddd15c37542 --- /dev/null +++ b/changelogs/unreleased/actioncontroller-parameters-deprecations.yml @@ -0,0 +1,5 @@ +--- +title: Fix several ActionController::Parameters deprecations +merge_request: 24332 +author: Jasper Maes +type: other diff --git a/changelogs/unreleased/add-badge-count-to-projects-and-groups.yml b/changelogs/unreleased/add-badge-count-to-projects-and-groups.yml new file mode 100644 index 00000000000..e200bbaa806 --- /dev/null +++ b/changelogs/unreleased/add-badge-count-to-projects-and-groups.yml @@ -0,0 +1,5 @@ +--- +title: Add badge count to projects +merge_request: 18425 +author: George Tsiolis +type: added diff --git a/changelogs/unreleased/add-new-nginx-metrics.yml b/changelogs/unreleased/add-new-nginx-metrics.yml deleted file mode 100644 index 57221056d6e..00000000000 --- a/changelogs/unreleased/add-new-nginx-metrics.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Add NGINX 0.16.0 and above metrics -merge_request: 22133 -author: -type: added diff --git a/changelogs/unreleased/allow-basic-auth-on-go-get-middleware.yml b/changelogs/unreleased/allow-basic-auth-on-go-get-middleware.yml deleted file mode 100644 index fda3fdc28cf..00000000000 --- a/changelogs/unreleased/allow-basic-auth-on-go-get-middleware.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Allow basic authentication on go get middleware -merge_request: 23497 -author: Morty Choi @mortyccp -type: changed diff --git a/changelogs/unreleased/allow_collaboration_status_work.yml b/changelogs/unreleased/allow_collaboration_status_work.yml deleted file mode 100644 index 3cf8f13ffea..00000000000 --- a/changelogs/unreleased/allow_collaboration_status_work.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Update a condition to visibility a merge request collaboration message -merge_request: 23104 -author: Harry Kiselev -type: other diff --git a/changelogs/unreleased/an-dtracing-test-for-invalid-tracers.yml b/changelogs/unreleased/an-dtracing-test-for-invalid-tracers.yml new file mode 100644 index 00000000000..5365260cbae --- /dev/null +++ b/changelogs/unreleased/an-dtracing-test-for-invalid-tracers.yml @@ -0,0 +1,5 @@ +--- +title: Avoid overwriting default jaeger values with nil +merge_request: 24482 +author: +type: fixed diff --git a/changelogs/unreleased/an-gilab-process-name.yml b/changelogs/unreleased/an-gilab-process-name.yml new file mode 100644 index 00000000000..72d811ee21f --- /dev/null +++ b/changelogs/unreleased/an-gilab-process-name.yml @@ -0,0 +1,5 @@ +--- +title: Extract process_name from GitLab::Sentry +merge_request: 24422 +author: +type: other diff --git a/changelogs/unreleased/an-opentracing-factory.yml b/changelogs/unreleased/an-opentracing-factory.yml new file mode 100644 index 00000000000..c04736f3e63 --- /dev/null +++ b/changelogs/unreleased/an-opentracing-factory.yml @@ -0,0 +1,5 @@ +--- +title: Conditionally initialize the global opentracing tracer +merge_request: 24186 +author: +type: other diff --git a/changelogs/unreleased/an-opentracing-propagation.yml b/changelogs/unreleased/an-opentracing-propagation.yml new file mode 100644 index 00000000000..d9aa7cd0048 --- /dev/null +++ b/changelogs/unreleased/an-opentracing-propagation.yml @@ -0,0 +1,5 @@ +--- +title: Adds inter-service OpenTracing propagation +merge_request: 24239 +author: +type: other diff --git a/changelogs/unreleased/api-nested-group-permission.yml b/changelogs/unreleased/api-nested-group-permission.yml new file mode 100644 index 00000000000..3ec0df6893f --- /dev/null +++ b/changelogs/unreleased/api-nested-group-permission.yml @@ -0,0 +1,5 @@ +--- +title: Return the maximum group access level in the projects API +merge_request: 24403 +author: +type: changed diff --git a/changelogs/unreleased/api-tags-search.yml b/changelogs/unreleased/api-tags-search.yml new file mode 100644 index 00000000000..1501acd5a9e --- /dev/null +++ b/changelogs/unreleased/api-tags-search.yml @@ -0,0 +1,5 @@ +--- +title: 'API: Support searching for tags' +merge_request: 24385 +author: Robert Schilling +type: added diff --git a/changelogs/unreleased/api-wiki-dot-slug.yml b/changelogs/unreleased/api-wiki-dot-slug.yml new file mode 100644 index 00000000000..82c76fa7450 --- /dev/null +++ b/changelogs/unreleased/api-wiki-dot-slug.yml @@ -0,0 +1,5 @@ +--- +title: 'API: Support dots in wiki slugs' +merge_request: 24383 +author: Robert Schilling +type: fixed diff --git a/changelogs/unreleased/auto-devops-custom-domains.yml b/changelogs/unreleased/auto-devops-custom-domains.yml new file mode 100644 index 00000000000..37e8ee26a4d --- /dev/null +++ b/changelogs/unreleased/auto-devops-custom-domains.yml @@ -0,0 +1,5 @@ +--- +title: Added support for custom hosts/domains to Auto DevOps +merge_request: 24248 +author: walkafwalka +type: added diff --git a/changelogs/unreleased/auto-devops-kubectl-1-11-6.yml b/changelogs/unreleased/auto-devops-kubectl-1-11-6.yml new file mode 100644 index 00000000000..1a8cdead4ac --- /dev/null +++ b/changelogs/unreleased/auto-devops-kubectl-1-11-6.yml @@ -0,0 +1,5 @@ +--- +title: Bump kubectl in Auto DevOps to 1.11.6 +merge_request: 24176 +author: +type: other diff --git a/changelogs/unreleased/backup_restore_fix_issue_46891.yml b/changelogs/unreleased/backup_restore_fix_issue_46891.yml new file mode 100644 index 00000000000..b8fe3b1b861 --- /dev/null +++ b/changelogs/unreleased/backup_restore_fix_issue_46891.yml @@ -0,0 +1,5 @@ +--- +title: Modify file restore to rectify tar issue +merge_request: 24000 +author: +type: fixed diff --git a/changelogs/unreleased/blackst0ne-bump-rails-cve-2018-16476.yml b/changelogs/unreleased/blackst0ne-bump-rails-cve-2018-16476.yml deleted file mode 100644 index dfa94c69ce0..00000000000 --- a/changelogs/unreleased/blackst0ne-bump-rails-cve-2018-16476.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Bump Ruby on Rails to 5.0.7.1 -merge_request: 23396 -author: "@blackst0ne" -type: security diff --git a/changelogs/unreleased/blackst0ne-convert-specs-rails5-style.yml b/changelogs/unreleased/blackst0ne-convert-specs-rails5-style.yml deleted file mode 100644 index c29cfec075c..00000000000 --- a/changelogs/unreleased/blackst0ne-convert-specs-rails5-style.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: "[Rails5.1] Update functional specs to use new keyword format" -merge_request: 23095 -author: "@blackst0ne" -type: other diff --git a/changelogs/unreleased/blackst0ne-improve-encoding-helper-spec.yml b/changelogs/unreleased/blackst0ne-improve-encoding-helper-spec.yml deleted file mode 100644 index 09480499b87..00000000000 --- a/changelogs/unreleased/blackst0ne-improve-encoding-helper-spec.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Update specs to exclude possible false positive pass -merge_request: 23893 -author: "@blackst0ne" -type: other diff --git a/changelogs/unreleased/bump-ingress-chart-112.yml b/changelogs/unreleased/bump-ingress-chart-112.yml new file mode 100644 index 00000000000..8a46fedb4b0 --- /dev/null +++ b/changelogs/unreleased/bump-ingress-chart-112.yml @@ -0,0 +1,5 @@ +--- +title: Bump nginx-ingress chart to 1.1.2 +merge_request: 24203 +author: +type: other diff --git a/changelogs/unreleased/bvl-hide-confidential-events-take2.yml b/changelogs/unreleased/bvl-hide-confidential-events-take2.yml deleted file mode 100644 index a5abd496a9d..00000000000 --- a/changelogs/unreleased/bvl-hide-confidential-events-take2.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Hide confidential events in the API -merge_request: 23746 -author: -type: other diff --git a/changelogs/unreleased/ccr-49289_milestone_link.yml b/changelogs/unreleased/ccr-49289_milestone_link.yml deleted file mode 100644 index 14c09752a24..00000000000 --- a/changelogs/unreleased/ccr-49289_milestone_link.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Add project milestone link -merge_request: 22552 -author: -type: added diff --git a/changelogs/unreleased/ci-dropdown-hidden-bug.yml b/changelogs/unreleased/ci-dropdown-hidden-bug.yml deleted file mode 100644 index 6910f04a6d5..00000000000 --- a/changelogs/unreleased/ci-dropdown-hidden-bug.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Don't hide CI dropdown behind diff summary -merge_request: -author: gfyoung -type: fixed diff --git a/changelogs/unreleased/cleanup-leagcy-artifact-migration.yml b/changelogs/unreleased/cleanup-leagcy-artifact-migration.yml new file mode 100644 index 00000000000..6e8dac97249 --- /dev/null +++ b/changelogs/unreleased/cleanup-leagcy-artifact-migration.yml @@ -0,0 +1,5 @@ +--- +title: Cleanup legacy artifact background migration +merge_request: 24144 +author: +type: other diff --git a/changelogs/unreleased/depracated-migration-inheritance.yml b/changelogs/unreleased/depracated-migration-inheritance.yml deleted file mode 100644 index 1ea9b2df59c..00000000000 --- a/changelogs/unreleased/depracated-migration-inheritance.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: ActiveRecord::Migration -> ActiveRecord::Migration[5.0] -merge_request: 23910 -author: Jasper Maes -type: other diff --git a/changelogs/unreleased/deprecated-actiondispatch-paramsparser.yml b/changelogs/unreleased/deprecated-actiondispatch-paramsparser.yml deleted file mode 100644 index 9cfb00a9544..00000000000 --- a/changelogs/unreleased/deprecated-actiondispatch-paramsparser.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Remove deprecated ActionDispatch::ParamsParser -merge_request: 23848 -author: Jasper Maes -type: other diff --git a/changelogs/unreleased/deprecated-alias-method-chain.yml b/changelogs/unreleased/deprecated-alias-method-chain.yml deleted file mode 100644 index 76dd016e4cc..00000000000 --- a/changelogs/unreleased/deprecated-alias-method-chain.yml +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: 'Fix deprecation: alias_method_chain is deprecated. Please, use Module#prepend - instead' -merge_request: 23887 -author: Jasper Maes -type: other diff --git a/changelogs/unreleased/deprecated-callback-false.yml b/changelogs/unreleased/deprecated-callback-false.yml deleted file mode 100644 index 6ba01a75ab9..00000000000 --- a/changelogs/unreleased/deprecated-callback-false.yml +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: 'Fix deprecation: returning false in Active Record and Active Model callbacks - will not implicitly halt a callback chain' -merge_request: 24134 -author: Jasper Maes -type: other diff --git a/changelogs/unreleased/deprecated-comparing-actioncontroller-params-hash.yml b/changelogs/unreleased/deprecated-comparing-actioncontroller-params-hash.yml deleted file mode 100644 index a7b9d054a4c..00000000000 --- a/changelogs/unreleased/deprecated-comparing-actioncontroller-params-hash.yml +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: 'Fix deprecation: Comparing equality between ActionController::Parameters and - a Hash is deprecated' -merge_request: 23855 -author: Jasper Maes -type: other diff --git a/changelogs/unreleased/deprecated-delete-all-params.yml b/changelogs/unreleased/deprecated-delete-all-params.yml deleted file mode 100644 index e23fe92a738..00000000000 --- a/changelogs/unreleased/deprecated-delete-all-params.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: 'Fix deprecation: Passing conditions to delete_all is deprecated' -merge_request: 23817 -author: Jasper Maes -type: other diff --git a/changelogs/unreleased/deprecated-directly-inheriting-migration.yml b/changelogs/unreleased/deprecated-directly-inheriting-migration.yml deleted file mode 100644 index 2793cc0d44f..00000000000 --- a/changelogs/unreleased/deprecated-directly-inheriting-migration.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: 'Fix deprecation: Directly inheriting from ActiveRecord::Migration is deprecated.' -merge_request: 23884 -author: Jasper Maes -type: other diff --git a/changelogs/unreleased/deprecated-insert-sql.yml b/changelogs/unreleased/deprecated-insert-sql.yml deleted file mode 100644 index ad21fbd9dde..00000000000 --- a/changelogs/unreleased/deprecated-insert-sql.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: 'Fix deprecation: insert_sql is deprecated and will be removed' -merge_request: 23944 -author: Jasper Maes -type: other diff --git a/changelogs/unreleased/deprecated-migration-inheritance-2.yml b/changelogs/unreleased/deprecated-migration-inheritance-2.yml deleted file mode 100644 index 467a521dbd4..00000000000 --- a/changelogs/unreleased/deprecated-migration-inheritance-2.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: ActiveRecord::Migration -> ActiveRecord::Migration[5.0] for AddIndexesToCiBuildsAndPipelines -merge_request: 24167 -author: Jasper Maes -type: other diff --git a/changelogs/unreleased/deprecated-passing-activerecord-objects.yml b/changelogs/unreleased/deprecated-passing-activerecord-objects.yml deleted file mode 100644 index e58647186b8..00000000000 --- a/changelogs/unreleased/deprecated-passing-activerecord-objects.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: 'Fix deprecation: Passing ActiveRecord::Base objects to sanitize_sql_hash_for_assignment' -merge_request: 23818 -author: Jasper Maes -type: other diff --git a/changelogs/unreleased/deprecated-positional-seperator-parameter.yml b/changelogs/unreleased/deprecated-positional-seperator-parameter.yml deleted file mode 100644 index 0d952e0d5eb..00000000000 --- a/changelogs/unreleased/deprecated-positional-seperator-parameter.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Passing the separator argument as a positional parameter is deprecated -merge_request: 23334 -author: Jasper Maes -type: other diff --git a/changelogs/unreleased/deprecated-positional-spec-arguments.yml b/changelogs/unreleased/deprecated-positional-spec-arguments.yml deleted file mode 100644 index 8e541df1ad4..00000000000 --- a/changelogs/unreleased/deprecated-positional-spec-arguments.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: 'Fix deprecation: Using positional arguments in integration tests' -merge_request: 24110 -author: Jasper Maes -type: other diff --git a/changelogs/unreleased/deprecated-redirect-back.yml b/changelogs/unreleased/deprecated-redirect-back.yml deleted file mode 100644 index 7fc567fbdb5..00000000000 --- a/changelogs/unreleased/deprecated-redirect-back.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: 'Fix deprecation: redirect_to :back is deprecated' -merge_request: 23943 -author: Jasper Maes -type: other diff --git a/changelogs/unreleased/diff-empty-state-fixes.yml b/changelogs/unreleased/diff-empty-state-fixes.yml deleted file mode 100644 index 0d347dd17e4..00000000000 --- a/changelogs/unreleased/diff-empty-state-fixes.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fixed merge request diffs empty states -merge_request: -author: -type: fixed diff --git a/changelogs/unreleased/diff-tree-collapse-directories.yml b/changelogs/unreleased/diff-tree-collapse-directories.yml new file mode 100644 index 00000000000..6eae48f2352 --- /dev/null +++ b/changelogs/unreleased/diff-tree-collapse-directories.yml @@ -0,0 +1,5 @@ +--- +title: Collapse directory structure in merge request file tree +merge_request: +author: +type: changed diff --git a/changelogs/unreleased/dm-note-email-image-diff-discussion.yml b/changelogs/unreleased/dm-note-email-image-diff-discussion.yml deleted file mode 100644 index 6532052e132..00000000000 --- a/changelogs/unreleased/dm-note-email-image-diff-discussion.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix notification email for image diff notes -merge_request: -author: -type: fixed diff --git a/changelogs/unreleased/dm-trim-discussion-truncated-line-first-chars.yml b/changelogs/unreleased/dm-trim-discussion-truncated-line-first-chars.yml new file mode 100644 index 00000000000..1e1fa8295c3 --- /dev/null +++ b/changelogs/unreleased/dm-trim-discussion-truncated-line-first-chars.yml @@ -0,0 +1,5 @@ +--- +title: Fix bug that caused Suggestion Markdown toolbar button to insert snippet with leading +/-/<space> +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/docs-releases-api.yml b/changelogs/unreleased/docs-releases-api.yml deleted file mode 100644 index fba70c0006d..00000000000 --- a/changelogs/unreleased/docs-releases-api.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Adds API documentation for releases -merge_request: 23901 -author: -type: added diff --git a/changelogs/unreleased/error_tracking_feature_flag_fe.yml b/changelogs/unreleased/error_tracking_feature_flag_fe.yml deleted file mode 100644 index 607929eb6b8..00000000000 --- a/changelogs/unreleased/error_tracking_feature_flag_fe.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Display a list of Sentry Issues in GitLab -merge_request: 23770 -author: -type: added diff --git a/changelogs/unreleased/feature-gb-expose-ci-api-url-variable.yml b/changelogs/unreleased/feature-gb-expose-ci-api-url-variable.yml deleted file mode 100644 index 19dc615c5f8..00000000000 --- a/changelogs/unreleased/feature-gb-expose-ci-api-url-variable.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Expose CI/CD predefined variable `CI_API_V4_URL` -merge_request: 23936 -author: -type: added diff --git a/changelogs/unreleased/feature-option-to-make-variables-protected.yml b/changelogs/unreleased/feature-option-to-make-variables-protected.yml deleted file mode 100644 index c99c0481c35..00000000000 --- a/changelogs/unreleased/feature-option-to-make-variables-protected.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Add option to make ci variables protected by default -merge_request: 22744 -author: Alexis Reigel -type: added diff --git a/changelogs/unreleased/features-document-graphicsmagick-source-installation.yml b/changelogs/unreleased/features-document-graphicsmagick-source-installation.yml new file mode 100644 index 00000000000..b224cace4bf --- /dev/null +++ b/changelogs/unreleased/features-document-graphicsmagick-source-installation.yml @@ -0,0 +1,5 @@ +--- +title: Document graphicsmagick installation for source installation +merge_request: 24404 +author: Alexis Reigel +type: added diff --git a/changelogs/unreleased/fix-403-page-is-rendered-but-404-is-the-response.yml b/changelogs/unreleased/fix-403-page-is-rendered-but-404-is-the-response.yml new file mode 100644 index 00000000000..eda69b32094 --- /dev/null +++ b/changelogs/unreleased/fix-403-page-is-rendered-but-404-is-the-response.yml @@ -0,0 +1,5 @@ +--- +title: Show the correct error page when access is denied +merge_request: 23932 +author: +type: fixed diff --git a/changelogs/unreleased/fix-55448.yml b/changelogs/unreleased/fix-55448.yml deleted file mode 100644 index e0bdbb6eda4..00000000000 --- a/changelogs/unreleased/fix-55448.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Remove deprecated xhr from specs -merge_request: 23949 -author: Jasper Maes -type: other diff --git a/changelogs/unreleased/fix-56558-move-primary-button.yml b/changelogs/unreleased/fix-56558-move-primary-button.yml new file mode 100644 index 00000000000..4dcc896b327 --- /dev/null +++ b/changelogs/unreleased/fix-56558-move-primary-button.yml @@ -0,0 +1,5 @@ +--- +title: Moved primary button for labels to follow the design patterns used on rest of the site +merge_request: +author: Martin Hobert +type: fixed diff --git a/changelogs/unreleased/fix-calendar-events-fetching-error.yml b/changelogs/unreleased/fix-calendar-events-fetching-error.yml deleted file mode 100644 index ad4a40cd9a0..00000000000 --- a/changelogs/unreleased/fix-calendar-events-fetching-error.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix calendar events fetching error on private profile page -merge_request: 23718 -author: Harry Kiselev -type: other diff --git a/changelogs/unreleased/fix-n-plus-1-queries-projects.yml b/changelogs/unreleased/fix-n-plus-1-queries-projects.yml deleted file mode 100644 index cb625784267..00000000000 --- a/changelogs/unreleased/fix-n-plus-1-queries-projects.yml +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: Fix some N+1 queries related to Admin Dashboard, User Dashboards and Activity - Stream -merge_request: 23034 -author: -type: performance diff --git a/changelogs/unreleased/fix-udpate-head-pipeline-method.yml b/changelogs/unreleased/fix-udpate-head-pipeline-method.yml deleted file mode 100644 index 8dbb9f8e42b..00000000000 --- a/changelogs/unreleased/fix-udpate-head-pipeline-method.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix unexpected exception by failure of finding an actual head pipeline -merge_request: 24257 -author: -type: fixed diff --git a/changelogs/unreleased/fj-44679-skip-per-commit-validations.yml b/changelogs/unreleased/fj-44679-skip-per-commit-validations.yml deleted file mode 100644 index 3f9754409df..00000000000 --- a/changelogs/unreleased/fj-44679-skip-per-commit-validations.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Skip per-commit validations already evaluated -merge_request: 23984 -author: -type: performance diff --git a/changelogs/unreleased/fj-fix-lfs-image-comments-diffs.yml b/changelogs/unreleased/fj-fix-lfs-image-comments-diffs.yml deleted file mode 100644 index dc1fe5d7417..00000000000 --- a/changelogs/unreleased/fj-fix-lfs-image-comments-diffs.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix bug commenting on LFS images -merge_request: 23812 -author: -type: fixed diff --git a/changelogs/unreleased/force-redeploy-on-updated-secrets.yml b/changelogs/unreleased/force-redeploy-on-updated-secrets.yml new file mode 100644 index 00000000000..3b727c99dd5 --- /dev/null +++ b/changelogs/unreleased/force-redeploy-on-updated-secrets.yml @@ -0,0 +1,5 @@ +--- +title: Redeploy Auto DevOps deployment on variable updates +merge_request: 24498 +author: walkafwalka +type: added diff --git a/changelogs/unreleased/force-reload-arguments-2.yml b/changelogs/unreleased/force-reload-arguments-2.yml deleted file mode 100644 index 23ab9433b3d..00000000000 --- a/changelogs/unreleased/force-reload-arguments-2.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Passing an argument to force an association to reload is now deprecated -merge_request: 23894 -author: Jasper Maes -type: other diff --git a/changelogs/unreleased/gitaly-update-1-13-0.yml b/changelogs/unreleased/gitaly-update-1-13-0.yml new file mode 100644 index 00000000000..73de25a532d --- /dev/null +++ b/changelogs/unreleased/gitaly-update-1-13-0.yml @@ -0,0 +1,5 @@ +--- +title: Upgrade Gitaly to 1.13.0 +merge_request: 24429 +author: +type: other diff --git a/changelogs/unreleased/gitlab-workhorse-update-8.1.0.yml b/changelogs/unreleased/gitlab-workhorse-update-8.1.0.yml new file mode 100644 index 00000000000..1e0160c4d40 --- /dev/null +++ b/changelogs/unreleased/gitlab-workhorse-update-8.1.0.yml @@ -0,0 +1,5 @@ +--- +title: Upgrade gitlab-workhorse to 8.1.0 +merge_request: 24571 +author: +type: other diff --git a/changelogs/unreleased/gt-externalize-app-views-projects-project_members.yml b/changelogs/unreleased/gt-externalize-app-views-projects-project_members.yml new file mode 100644 index 00000000000..1acea10fcaa --- /dev/null +++ b/changelogs/unreleased/gt-externalize-app-views-projects-project_members.yml @@ -0,0 +1,5 @@ +--- +title: Externalize strings from `/app/views/projects/project_members` +merge_request: 23227 +author: Tao Wang +type: other diff --git a/changelogs/unreleased/gt-externalize-app-views-shared-notes.yml b/changelogs/unreleased/gt-externalize-app-views-shared-notes.yml deleted file mode 100644 index 39ca6b67a54..00000000000 --- a/changelogs/unreleased/gt-externalize-app-views-shared-notes.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Externalize strings from `/app/views/shared/notes` -merge_request: 23696 -author: Tao Wang -type: other diff --git a/changelogs/unreleased/gt-remove-unnecessary-line-before-reply-holder.yml b/changelogs/unreleased/gt-remove-unnecessary-line-before-reply-holder.yml deleted file mode 100644 index 142a9c1f2cc..00000000000 --- a/changelogs/unreleased/gt-remove-unnecessary-line-before-reply-holder.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Remove unnecessary line before reply holder -merge_request: 23092 -author: George Tsiolis -type: changed diff --git a/changelogs/unreleased/gt-rename-gray-theme-color-variables.yml b/changelogs/unreleased/gt-rename-gray-theme-color-variables.yml new file mode 100644 index 00000000000..b612bb3ee39 --- /dev/null +++ b/changelogs/unreleased/gt-rename-gray-theme-color-variables.yml @@ -0,0 +1,5 @@ +--- +title: Remove all `$theme-gray-{weight}` variables in favor of `$gray-{weight}` +merge_request: 24333 +author: George Tsiolis +type: other diff --git a/changelogs/unreleased/gt-reorder-group-sidebar-menu-items.yml b/changelogs/unreleased/gt-reorder-group-sidebar-menu-items.yml deleted file mode 100644 index b1ecf2bb1ed..00000000000 --- a/changelogs/unreleased/gt-reorder-group-sidebar-menu-items.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Reorder sidebar menu item for group clusters -merge_request: 24001 -author: George Tsiolis -type: changed diff --git a/changelogs/unreleased/gt-update-environment-breadcrumb.yml b/changelogs/unreleased/gt-update-environment-breadcrumb.yml deleted file mode 100644 index 53b9673a96c..00000000000 --- a/changelogs/unreleased/gt-update-environment-breadcrumb.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Update environments breadcrumb -merge_request: 23751 -author: George Tsiolis -type: changed diff --git a/changelogs/unreleased/gt-update-navigation-theme-colors.yml b/changelogs/unreleased/gt-update-navigation-theme-colors.yml deleted file mode 100644 index 749587a6343..00000000000 --- a/changelogs/unreleased/gt-update-navigation-theme-colors.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Update header navigation theme colors -merge_request: 23734 -author: George Tsiolis -type: fixed diff --git a/changelogs/unreleased/homepage-proj-descr-cutoff.yml b/changelogs/unreleased/homepage-proj-descr-cutoff.yml new file mode 100644 index 00000000000..837c01f6722 --- /dev/null +++ b/changelogs/unreleased/homepage-proj-descr-cutoff.yml @@ -0,0 +1,5 @@ +--- +title: Increase line height of project summaries +merge_request: +author: gfyoung +type: fixed diff --git a/changelogs/unreleased/include-project.yml b/changelogs/unreleased/include-project.yml deleted file mode 100644 index c63ac490d21..00000000000 --- a/changelogs/unreleased/include-project.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Allow to include files from another projects in gitlab-ci.yml -merge_request: 24101 -author: -type: added diff --git a/changelogs/unreleased/include-templates.yml b/changelogs/unreleased/include-templates.yml deleted file mode 100644 index 5601cd185e9..00000000000 --- a/changelogs/unreleased/include-templates.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Allow to include templates in gitlab-ci.yml -merge_request: 23495 -author: -type: added diff --git a/changelogs/unreleased/jivl-update-placeholder-sentry-config.yml b/changelogs/unreleased/jivl-update-placeholder-sentry-config.yml deleted file mode 100644 index eb860fd3905..00000000000 --- a/changelogs/unreleased/jivl-update-placeholder-sentry-config.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Update url placeholder for the sentry configuration page -merge_request: 24338 -author: -type: other diff --git a/changelogs/unreleased/jlenny-CI_COMMIT_SHORT_SHA.yml b/changelogs/unreleased/jlenny-CI_COMMIT_SHORT_SHA.yml deleted file mode 100644 index abece81a20d..00000000000 --- a/changelogs/unreleased/jlenny-CI_COMMIT_SHORT_SHA.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Add new pipeline variable CI_COMMIT_SHORT_SHA -merge_request: 23822 -author: -type: added diff --git a/changelogs/unreleased/knative-prometheus.yml b/changelogs/unreleased/knative-prometheus.yml deleted file mode 100644 index 606c5332474..00000000000 --- a/changelogs/unreleased/knative-prometheus.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Add Knative metrics to Prometheus -merge_request: 23972 -author: Chris Baumbauer -type: added diff --git a/changelogs/unreleased/knative-rbac-check.yml b/changelogs/unreleased/knative-rbac-check.yml deleted file mode 100644 index 0c40bb46e7f..00000000000 --- a/changelogs/unreleased/knative-rbac-check.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Require Knative to be installed only on an RBAC kubernetes cluster -merge_request: 23807 -author: Chris Baumbauer -type: changed diff --git a/changelogs/unreleased/mg-fix-bad-cluster-update-entrypoint.yml b/changelogs/unreleased/mg-fix-bad-cluster-update-entrypoint.yml new file mode 100644 index 00000000000..932850cc825 --- /dev/null +++ b/changelogs/unreleased/mg-fix-bad-cluster-update-entrypoint.yml @@ -0,0 +1,5 @@ +--- +title: Fix cluster page non-interactive on form validation error +merge_request: 24583 +author: +type: fixed diff --git a/changelogs/unreleased/mk-avoid-read-only-error.yml b/changelogs/unreleased/mk-avoid-read-only-error.yml deleted file mode 100644 index 8641f5db9f0..00000000000 --- a/changelogs/unreleased/mk-avoid-read-only-error.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Prevent admins from attempting hashed storage migration on read only DB -merge_request: 23597 -author: -type: fixed diff --git a/changelogs/unreleased/move-job-cancel-btn.yml b/changelogs/unreleased/move-job-cancel-btn.yml new file mode 100644 index 00000000000..41f8e1be5f8 --- /dev/null +++ b/changelogs/unreleased/move-job-cancel-btn.yml @@ -0,0 +1,5 @@ +--- +title: Move cancel & new issue button on job page +merge_request: 24074 +author: +type: changed diff --git a/changelogs/unreleased/none-syntax-highlighting.yml b/changelogs/unreleased/none-syntax-highlighting.yml deleted file mode 100644 index b373aac7c02..00000000000 --- a/changelogs/unreleased/none-syntax-highlighting.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Add no-color theme for syntax highlighting. -merge_request: !20170 -author: khm -type: added diff --git a/changelogs/unreleased/osw-cache-discussions-diff-highlighting.yml b/changelogs/unreleased/osw-cache-discussions-diff-highlighting.yml deleted file mode 100644 index 7abc7d85794..00000000000 --- a/changelogs/unreleased/osw-cache-discussions-diff-highlighting.yml +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: Improve the loading time on merge request's discussion page by caching diff - highlight -merge_request: 23857 -author: -type: performance diff --git a/changelogs/unreleased/osw-enforces-project-removal-with-past-failed-attempts.yml b/changelogs/unreleased/osw-enforces-project-removal-with-past-failed-attempts.yml new file mode 100644 index 00000000000..6a2a67e7aa8 --- /dev/null +++ b/changelogs/unreleased/osw-enforces-project-removal-with-past-failed-attempts.yml @@ -0,0 +1,5 @@ +--- +title: Cleanup stale +deleted repo paths on project removal (adjusts project removal bug) +merge_request: 24269 +author: +type: fixed diff --git a/changelogs/unreleased/pl-reactive-caching-primary_key.yml b/changelogs/unreleased/pl-reactive-caching-primary_key.yml deleted file mode 100644 index a72933c19b1..00000000000 --- a/changelogs/unreleased/pl-reactive-caching-primary_key.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Enable caching for records which primary key is not `id` -merge_request: 24245 -author: -type: fixed diff --git a/changelogs/unreleased/raise-on-unfiltered-params.yml b/changelogs/unreleased/raise-on-unfiltered-params.yml new file mode 100644 index 00000000000..531e9ba807e --- /dev/null +++ b/changelogs/unreleased/raise-on-unfiltered-params.yml @@ -0,0 +1,5 @@ +--- +title: Actually set raise_on_unfiltered_parameters to true +merge_request: 24443 +author: Jasper Maes +type: other diff --git a/changelogs/unreleased/remote-mirror-update-failed-notification.yml b/changelogs/unreleased/remote-mirror-update-failed-notification.yml deleted file mode 100644 index 50ec8624ae5..00000000000 --- a/changelogs/unreleased/remote-mirror-update-failed-notification.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Send a notification email to project maintainers when a mirror update fails -merge_request: 23595 -author: -type: added diff --git a/changelogs/unreleased/remove-cancel-all-button-in-job-list-view.yml b/changelogs/unreleased/remove-cancel-all-button-in-job-list-view.yml new file mode 100644 index 00000000000..06546bc5a8e --- /dev/null +++ b/changelogs/unreleased/remove-cancel-all-button-in-job-list-view.yml @@ -0,0 +1,5 @@ +--- +title: Remove Cancel all jobs button in general jobs list view +merge_request: +author: Jordi Llull +type: removed diff --git a/changelogs/unreleased/remove-rails4-specific-code.yml b/changelogs/unreleased/remove-rails4-specific-code.yml deleted file mode 100644 index c6c4c0a5d5b..00000000000 --- a/changelogs/unreleased/remove-rails4-specific-code.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Remove rails4 specific code -merge_request: 23847 -author: Jasper Maes -type: other diff --git a/changelogs/unreleased/remove-rails4-support.yml b/changelogs/unreleased/remove-rails4-support.yml deleted file mode 100644 index a05c913a70c..00000000000 --- a/changelogs/unreleased/remove-rails4-support.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Remove rails 4 support in CI, Gemfiles, bin/ and config/ -merge_request: 23717 -author: Jasper Maes -type: other diff --git a/changelogs/unreleased/s3-directories-get.yml b/changelogs/unreleased/s3-directories-get.yml deleted file mode 100644 index 9f76af2bb09..00000000000 --- a/changelogs/unreleased/s3-directories-get.yml +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: Allow 'rake gitlab:cleanup:remote_upload_files' to read bucket files without - having permissions to see all buckets. -merge_request: 23981 -author: -type: fixed diff --git a/changelogs/unreleased/security-2770-verify-bundle-import-files.yml b/changelogs/unreleased/security-2770-verify-bundle-import-files.yml new file mode 100644 index 00000000000..dea40dd1ef1 --- /dev/null +++ b/changelogs/unreleased/security-2770-verify-bundle-import-files.yml @@ -0,0 +1,5 @@ +--- +title: Validate bundle files before unpacking them +merge_request: +author: +type: security diff --git a/changelogs/unreleased/security-48259-private-snippet.yml b/changelogs/unreleased/security-48259-private-snippet.yml deleted file mode 100644 index 6cf1e5dc694..00000000000 --- a/changelogs/unreleased/security-48259-private-snippet.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Prevent private snippets from being embeddable -merge_request: -author: -type: security diff --git a/changelogs/unreleased/security-53543-user-keeps-access-to-mr-issue-when-removed-from-team.yml b/changelogs/unreleased/security-53543-user-keeps-access-to-mr-issue-when-removed-from-team.yml deleted file mode 100644 index ab12ba539c1..00000000000 --- a/changelogs/unreleased/security-53543-user-keeps-access-to-mr-issue-when-removed-from-team.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Issuable no longer is visible to users when project can't be viewed -merge_request: -author: -type: security diff --git a/changelogs/unreleased/security-54377-label-milestone-name-xss.yml b/changelogs/unreleased/security-54377-label-milestone-name-xss.yml deleted file mode 100644 index 76589b2eb4f..00000000000 --- a/changelogs/unreleased/security-54377-label-milestone-name-xss.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Escape label and milestone titles to prevent XSS in GFM autocomplete -merge_request: 2693 -author: -type: security diff --git a/changelogs/unreleased/security-bvl-fix-cross-project-mr-exposure.yml b/changelogs/unreleased/security-bvl-fix-cross-project-mr-exposure.yml deleted file mode 100644 index 11aae4428fb..00000000000 --- a/changelogs/unreleased/security-bvl-fix-cross-project-mr-exposure.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Don't expose cross project repositories through diffs when creating merge reqeusts -merge_request: -author: -type: security diff --git a/changelogs/unreleased/security-fix-ssrf-import-url-remote-mirror.yml b/changelogs/unreleased/security-fix-ssrf-import-url-remote-mirror.yml deleted file mode 100644 index 7ba7aa21090..00000000000 --- a/changelogs/unreleased/security-fix-ssrf-import-url-remote-mirror.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix SSRF with import_url and remote mirror url -merge_request: -author: -type: security diff --git a/changelogs/unreleased/security-master-group-cicd-settings-accessible-to-maintainer.yml b/changelogs/unreleased/security-master-group-cicd-settings-accessible-to-maintainer.yml deleted file mode 100644 index 5586fa6cd8e..00000000000 --- a/changelogs/unreleased/security-master-group-cicd-settings-accessible-to-maintainer.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Allow changing group CI/CD settings only for owners. -merge_request: -author: -type: security diff --git a/changelogs/unreleased/security-master-guests-jobs-api.yml b/changelogs/unreleased/security-master-guests-jobs-api.yml deleted file mode 100644 index 83022e91aca..00000000000 --- a/changelogs/unreleased/security-master-guests-jobs-api.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Authorize before reading job information via API. -merge_request: -author: -type: security diff --git a/changelogs/unreleased/security-master-secret-ci-variables-exposed.yml b/changelogs/unreleased/security-master-secret-ci-variables-exposed.yml deleted file mode 100644 index 702181065f5..00000000000 --- a/changelogs/unreleased/security-master-secret-ci-variables-exposed.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Prevent leaking protected variables for ambiguous refs. -merge_request: -author: -type: security diff --git a/changelogs/unreleased/security-master-url-rel.yml b/changelogs/unreleased/security-master-url-rel.yml deleted file mode 100644 index 75f599f6bcd..00000000000 --- a/changelogs/unreleased/security-master-url-rel.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Set URL rel attribute for broken URLs. -merge_request: -author: -type: security diff --git a/changelogs/unreleased/security-refs-available-to-project-guest.yml b/changelogs/unreleased/security-refs-available-to-project-guest.yml deleted file mode 100644 index eb6804c52d3..00000000000 --- a/changelogs/unreleased/security-refs-available-to-project-guest.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Project guests no longer are able to see refs page -merge_request: -author: -type: security diff --git a/changelogs/unreleased/security-todos_not_redacted_for_guests.yml b/changelogs/unreleased/security-todos_not_redacted_for_guests.yml deleted file mode 100644 index be0ae9a7193..00000000000 --- a/changelogs/unreleased/security-todos_not_redacted_for_guests.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Delete confidential todos for user when downgraded to Guest -merge_request: -author: -type: security diff --git a/changelogs/unreleased/sh-bump-omniauth-google-gem.yml b/changelogs/unreleased/sh-bump-omniauth-google-gem.yml deleted file mode 100644 index 2b31a55f8b2..00000000000 --- a/changelogs/unreleased/sh-bump-omniauth-google-gem.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Upgrade Omniauth and JWT gems to switch away from Google+ API -merge_request: 24068 -author: -type: changed diff --git a/changelogs/unreleased/sh-cache-avatar-paths.yml b/changelogs/unreleased/sh-cache-avatar-paths.yml deleted file mode 100644 index b59a4db413d..00000000000 --- a/changelogs/unreleased/sh-cache-avatar-paths.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Cache avatar URLs and paths within a request -merge_request: 23950 -author: -type: performance diff --git a/changelogs/unreleased/sh-carrierwave-patch-google-acl.yml b/changelogs/unreleased/sh-carrierwave-patch-google-acl.yml deleted file mode 100644 index 206253a100c..00000000000 --- a/changelogs/unreleased/sh-carrierwave-patch-google-acl.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix object storage not working properly with Google S3 compatibility -merge_request: 23858 -author: -type: fixed diff --git a/changelogs/unreleased/sh-drop-webhooks-project-export.yml b/changelogs/unreleased/sh-drop-webhooks-project-export.yml deleted file mode 100644 index 217373bce66..00000000000 --- a/changelogs/unreleased/sh-drop-webhooks-project-export.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Drop Webhooks from project import/export config -merge_request: 24121 -author: -type: fixed diff --git a/changelogs/unreleased/sh-fix-backfill-project-repo-migration.yml b/changelogs/unreleased/sh-fix-backfill-project-repo-migration.yml new file mode 100644 index 00000000000..d1d4412eb50 --- /dev/null +++ b/changelogs/unreleased/sh-fix-backfill-project-repo-migration.yml @@ -0,0 +1,5 @@ +--- +title: Fix duplicate project disk path in BackfillLegacyProjectRepositories +merge_request: 24213 +author: +type: changed diff --git a/changelogs/unreleased/sh-fix-bitbucket-server-error-handling.yml b/changelogs/unreleased/sh-fix-bitbucket-server-error-handling.yml new file mode 100644 index 00000000000..87405fa0a78 --- /dev/null +++ b/changelogs/unreleased/sh-fix-bitbucket-server-error-handling.yml @@ -0,0 +1,5 @@ +--- +title: Fix Bitbucket Server importer error handling +merge_request: 24343 +author: +type: fixed diff --git a/changelogs/unreleased/sh-fix-branches-api-timeout.yml b/changelogs/unreleased/sh-fix-branches-api-timeout.yml deleted file mode 100644 index 8cd29a7269d..00000000000 --- a/changelogs/unreleased/sh-fix-branches-api-timeout.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix timeout issues retrieving branches via API -merge_request: 24034 -author: -type: performance diff --git a/changelogs/unreleased/sh-fix-github-import-without-oauth2-config.yml b/changelogs/unreleased/sh-fix-github-import-without-oauth2-config.yml deleted file mode 100644 index ad548a6ff35..00000000000 --- a/changelogs/unreleased/sh-fix-github-import-without-oauth2-config.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Allow GitHub imports via token even if OAuth2 provider not configured -merge_request: 23703 -author: -type: fixed diff --git a/changelogs/unreleased/sh-fix-issue-55822.yml b/changelogs/unreleased/sh-fix-issue-55822.yml deleted file mode 100644 index 1267b2ace2f..00000000000 --- a/changelogs/unreleased/sh-fix-issue-55822.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix commit SHA not showing in merge request compare dropdown -merge_request: 24084 -author: -type: fixed diff --git a/changelogs/unreleased/sh-fix-issue-55914.yml b/changelogs/unreleased/sh-fix-issue-55914.yml deleted file mode 100644 index f6f372f59c7..00000000000 --- a/changelogs/unreleased/sh-fix-issue-55914.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix Bitbucket Server import only including first 25 pull requests -merge_request: 24178 -author: -type: fixed diff --git a/changelogs/unreleased/sh-fix-real-size-warnings.yml b/changelogs/unreleased/sh-fix-real-size-warnings.yml deleted file mode 100644 index 5062ffd677c..00000000000 --- a/changelogs/unreleased/sh-fix-real-size-warnings.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix broken templated "Too many changes to show" text -merge_request: 24282 -author: -type: fixed diff --git a/changelogs/unreleased/sh-fix-request-profiles-html.yml b/changelogs/unreleased/sh-fix-request-profiles-html.yml deleted file mode 100644 index 74e4115db8e..00000000000 --- a/changelogs/unreleased/sh-fix-request-profiles-html.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix requests profiler in admin page not rendering HTML properly -merge_request: 24291 -author: -type: fixed diff --git a/changelogs/unreleased/sh-fix-snippet-uploads-path-lookup.yml b/changelogs/unreleased/sh-fix-snippet-uploads-path-lookup.yml new file mode 100644 index 00000000000..414c8663049 --- /dev/null +++ b/changelogs/unreleased/sh-fix-snippet-uploads-path-lookup.yml @@ -0,0 +1,5 @@ +--- +title: Fix 404s with snippet uploads in object storage +merge_request: 24550 +author: +type: fixed diff --git a/changelogs/unreleased/sh-preload-associations-for-group-api.yml b/changelogs/unreleased/sh-preload-associations-for-group-api.yml new file mode 100644 index 00000000000..24e424b7efb --- /dev/null +++ b/changelogs/unreleased/sh-preload-associations-for-group-api.yml @@ -0,0 +1,5 @@ +--- +title: Eliminate N+1 queries in /api/groups/:id +merge_request: 24513 +author: +type: performance diff --git a/changelogs/unreleased/sh-skip-validation-visibility-changed.yml b/changelogs/unreleased/sh-skip-validation-visibility-changed.yml deleted file mode 100644 index 405be698b2b..00000000000 --- a/changelogs/unreleased/sh-skip-validation-visibility-changed.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Only validate project visibility when it has changed -merge_request: 24142 -author: -type: fixed diff --git a/changelogs/unreleased/shared_with_group_path.yml b/changelogs/unreleased/shared_with_group_path.yml new file mode 100644 index 00000000000..73ba9a9f30a --- /dev/null +++ b/changelogs/unreleased/shared_with_group_path.yml @@ -0,0 +1,5 @@ +--- +title: Add group full path to project's shared_with_groups +merge_request: 24052 +author: Mathieu Parent +type: added diff --git a/changelogs/unreleased/spec-positional-arguments.yml b/changelogs/unreleased/spec-positional-arguments.yml deleted file mode 100644 index 9dc114e5595..00000000000 --- a/changelogs/unreleased/spec-positional-arguments.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: 'Fix deprecation: Using positional arguments in integration tests' -merge_request: 24009 -author: Jasper Maes -type: other diff --git a/changelogs/unreleased/specs-positional-arguments.yml b/changelogs/unreleased/specs-positional-arguments.yml deleted file mode 100644 index 38b831bd72c..00000000000 --- a/changelogs/unreleased/specs-positional-arguments.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: convert specs in javascripts/ and support/ to new syntax -merge_request: 23947 -author: Jasper Maes -type: other diff --git a/changelogs/unreleased/suggestion-dashes.yml b/changelogs/unreleased/suggestion-dashes.yml deleted file mode 100644 index e99ab30b263..00000000000 --- a/changelogs/unreleased/suggestion-dashes.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fixed diff suggestions removing dashes -merge_request: -author: -type: fixed diff --git a/changelogs/unreleased/support-gitaly-tls.yml b/changelogs/unreleased/support-gitaly-tls.yml deleted file mode 100644 index 2a15500d6da..00000000000 --- a/changelogs/unreleased/support-gitaly-tls.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Support tls communication in gitaly -merge_request: 22602 -author: -type: added diff --git a/changelogs/unreleased/tc-remove-20181218192239-migration.yml b/changelogs/unreleased/tc-remove-20181218192239-migration.yml deleted file mode 100644 index 81e06a99c1f..00000000000 --- a/changelogs/unreleased/tc-remove-20181218192239-migration.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Remove migration to backfill project_repositories for legacy storage projects -merge_request: 24299 -author: -type: removed diff --git a/changelogs/unreleased/triggermesh-knative-version.yml b/changelogs/unreleased/triggermesh-knative-version.yml deleted file mode 100644 index 27f400962da..00000000000 --- a/changelogs/unreleased/triggermesh-knative-version.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Knative version bump 0.1.3 -> 0.2.2 -merge_request: -author: Chris Baumbauer -type: changed diff --git a/changelogs/unreleased/twang2218-gitlab-ce-i18n-extract-app-views-search.yml b/changelogs/unreleased/twang2218-gitlab-ce-i18n-extract-app-views-search.yml new file mode 100644 index 00000000000..1af1fe09f33 --- /dev/null +++ b/changelogs/unreleased/twang2218-gitlab-ce-i18n-extract-app-views-search.yml @@ -0,0 +1,5 @@ +--- +title: 'i18n: externalize strings from ''app/views/search''' +merge_request: 24297 +author: Tao Wang +type: other diff --git a/changelogs/unreleased/tz-user-popover-follow-up.yml b/changelogs/unreleased/tz-user-popover-follow-up.yml deleted file mode 100644 index d8f004beaa0..00000000000 --- a/changelogs/unreleased/tz-user-popover-follow-up.yml +++ /dev/null @@ -1,4 +0,0 @@ -title: Changed Userpopover Fixtures and shadow color -merge_request: 23768 -author: -type: other diff --git a/changelogs/unreleased/update-gitlab-runner-helm-chart-to-0-1-43.yml b/changelogs/unreleased/update-gitlab-runner-helm-chart-to-0-1-43.yml deleted file mode 100644 index 24471b028b1..00000000000 --- a/changelogs/unreleased/update-gitlab-runner-helm-chart-to-0-1-43.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Update GitLab Runner Helm Chart to 0.1.43 -merge_request: 24083 -author: -type: other diff --git a/changelogs/unreleased/update-gitlab-runner-helm-chart-to-0-1-45.yml b/changelogs/unreleased/update-gitlab-runner-helm-chart-to-0-1-45.yml new file mode 100644 index 00000000000..7d92929221f --- /dev/null +++ b/changelogs/unreleased/update-gitlab-runner-helm-chart-to-0-1-45.yml @@ -0,0 +1,5 @@ +--- +title: Update GitLab Runner Helm Chart to 0.1.45 +merge_request: 24564 +author: +type: other diff --git a/changelogs/unreleased/update-gitlab-styles.yml b/changelogs/unreleased/update-gitlab-styles.yml new file mode 100644 index 00000000000..379f0ad4486 --- /dev/null +++ b/changelogs/unreleased/update-gitlab-styles.yml @@ -0,0 +1,5 @@ +--- +title: Update gitlab-styles to 2.5.1 +merge_request: 24336 +author: Jasper Maes +type: other diff --git a/changelogs/unreleased/update-sidekiq-cron.yml b/changelogs/unreleased/update-sidekiq-cron.yml new file mode 100644 index 00000000000..edce32e3753 --- /dev/null +++ b/changelogs/unreleased/update-sidekiq-cron.yml @@ -0,0 +1,6 @@ +--- +title: Update sidekiq-cron to 1.0.4 and use fugit to replace rufus-scheduler to parse + cron syntax +merge_request: 24235 +author: +type: other diff --git a/changelogs/unreleased/user-update-head-pipeline-worker.yml b/changelogs/unreleased/user-update-head-pipeline-worker.yml deleted file mode 100644 index fd88697f239..00000000000 --- a/changelogs/unreleased/user-update-head-pipeline-worker.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Refactor the logic of updating head pipelines for merge requests -merge_request: 23502 -author: -type: other diff --git a/changelogs/unreleased/winh-dropdown-title-padding.yml b/changelogs/unreleased/winh-dropdown-title-padding.yml deleted file mode 100644 index 9d65175b536..00000000000 --- a/changelogs/unreleased/winh-dropdown-title-padding.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Adjust padding of .dropdown-title to comply with design specs -merge_request: 23546 -author: -type: changed diff --git a/changelogs/unreleased/winh-merge-request-commit-context.yml b/changelogs/unreleased/winh-merge-request-commit-context.yml deleted file mode 100644 index 9e12a926af4..00000000000 --- a/changelogs/unreleased/winh-merge-request-commit-context.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Display commit ID for discussions made on merge request commits -merge_request: 23837 -author: -type: fixed diff --git a/changelogs/unreleased/winh-princess-mononospace.yml b/changelogs/unreleased/winh-princess-mononospace.yml deleted file mode 100644 index e2d33de375e..00000000000 --- a/changelogs/unreleased/winh-princess-mononospace.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Make commit IDs in merge request discussion header monospace -merge_request: 23562 -author: -type: changed diff --git a/changelogs/unreleased/winh-upgrade-gitlab-ui.yml b/changelogs/unreleased/winh-upgrade-gitlab-ui.yml deleted file mode 100644 index b312a329f5d..00000000000 --- a/changelogs/unreleased/winh-upgrade-gitlab-ui.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Upgrade @gitlab/ui to 1.16.2 -merge_request: 23946 -author: -type: other diff --git a/changelogs/unreleased/yoginth-avatar-on-settings-sidebar.yml b/changelogs/unreleased/yoginth-avatar-on-settings-sidebar.yml new file mode 100644 index 00000000000..0ec76f9ce02 --- /dev/null +++ b/changelogs/unreleased/yoginth-avatar-on-settings-sidebar.yml @@ -0,0 +1,5 @@ +--- +title: Added Avatar in the settings sidebar +merge_request: 24515 +author: Yoginth +type: changed diff --git a/changelogs/unreleased/zj-backup-restore-object-pools.yml b/changelogs/unreleased/zj-backup-restore-object-pools.yml deleted file mode 100644 index 26e1d49aa04..00000000000 --- a/changelogs/unreleased/zj-backup-restore-object-pools.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Restore Object Pools when restoring an object pool -merge_request: 23682 -author: -type: added diff --git a/changelogs/unreleased/zj-feature-gate-set-project-path.yml b/changelogs/unreleased/zj-feature-gate-set-project-path.yml new file mode 100644 index 00000000000..b426a2f3fe7 --- /dev/null +++ b/changelogs/unreleased/zj-feature-gate-set-project-path.yml @@ -0,0 +1,5 @@ +--- +title: Allow setting of feature gates per project +merge_request: 24184 +author: +type: added diff --git a/config/application.rb b/config/application.rb index 349c7258852..92a3d031c63 100644 --- a/config/application.rb +++ b/config/application.rb @@ -162,6 +162,9 @@ module Gitlab config.action_view.sanitized_allowed_protocols = %w(smb) + # Can be removed once upgraded to Rails 5.1 or higher + config.action_controller.raise_on_unfiltered_parameters = true + # Nokogiri is significantly faster and uses less memory than REXML ActiveSupport::XmlMini.backend = 'Nokogiri' diff --git a/config/initializers/new_framework_defaults.rb b/config/initializers/new_framework_defaults.rb index a1e0667bc6f..115ee08dbb6 100644 --- a/config/initializers/new_framework_defaults.rb +++ b/config/initializers/new_framework_defaults.rb @@ -8,8 +8,6 @@ # # Read the Guide for Upgrading Ruby on Rails for more info on each option. -Rails.application.config.action_controller.raise_on_unfiltered_parameters = true - # Enable per-form CSRF tokens. Previous versions had false. Rails.application.config.action_controller.per_form_csrf_tokens = false diff --git a/config/initializers/sentry.rb b/config/initializers/sentry.rb index 2a6c5148f71..abc91c3ae51 100644 --- a/config/initializers/sentry.rb +++ b/config/initializers/sentry.rb @@ -19,7 +19,7 @@ def configure_sentry config.sanitize_fields = Rails.application.config.filter_parameters.map(&:to_s) # Sanitize authentication headers config.sanitize_http_headers = %w[Authorization Private-Token] - config.tags = { program: Gitlab::Sentry.program_context } + config.tags = { program: Gitlab.process_name } end end end diff --git a/config/initializers/tracing.rb b/config/initializers/tracing.rb new file mode 100644 index 00000000000..46e8125daf8 --- /dev/null +++ b/config/initializers/tracing.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +if Gitlab::Tracing.enabled? + require 'opentracing' + + Rails.application.configure do |config| + config.middleware.insert_after Gitlab::Middleware::CorrelationId, ::Gitlab::Tracing::RackMiddleware + end + + # Instrument the Sidekiq client + Sidekiq.configure_client do |config| + config.client_middleware do |chain| + chain.add Gitlab::Tracing::Sidekiq::ClientMiddleware + end + end + + # Instrument Sidekiq server calls when running Sidekiq server + if Sidekiq.server? + Sidekiq.configure_server do |config| + config.server_middleware do |chain| + chain.add Gitlab::Tracing::Sidekiq::ServerMiddleware + end + end + end + + # In multi-processed clustered architectures (puma, unicorn) don't + # start tracing until the worker processes are spawned. This works + # around issues when the opentracing implementation spawns threads + Gitlab::Cluster::LifecycleEvents.on_worker_start do + tracer = Gitlab::Tracing::Factory.create_tracer(Gitlab.process_name, Gitlab::Tracing.connection_string) + OpenTracing.global_tracer = tracer if tracer + end +end diff --git a/config/jsdocs.config.js b/config/jsdocs.config.js new file mode 100644 index 00000000000..52635b1ce13 --- /dev/null +++ b/config/jsdocs.config.js @@ -0,0 +1,14 @@ +module.exports = { + source: { + include: ['app/assets/javascripts/'], + }, + opts: { + template: 'node_modules/docdash', + destination: 'jsdoc/', + recurse: true, + }, + docdash: { + search: true, + static: true, + }, +}; diff --git a/config/routes/project.rb b/config/routes/project.rb index 797bf6de37b..21793e7756a 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -2,6 +2,8 @@ resources :projects, only: [:index, :new, :create] draw :git_http +get '/projects/:id' => 'projects#resolve' + constraints(::Constraints::ProjectUrlConstrainer.new) do # If the route has a wildcard segment, the segment has a regex constraint, # the segment is potentially followed by _another_ wildcard segment, and @@ -256,8 +258,6 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do resources :jobs, only: [:index, :show], constraints: { id: /\d+/ } do collection do - post :cancel_all - resources :artifacts, only: [] do collection do get :latest_succeeded, diff --git a/danger/commit_messages/Dangerfile b/danger/commit_messages/Dangerfile index 691dbbab184..c20c8b77e6a 100644 --- a/danger/commit_messages/Dangerfile +++ b/danger/commit_messages/Dangerfile @@ -2,6 +2,9 @@ require 'json' +URL_LIMIT_SUBJECT = "https://chris.beams.io/posts/git-commit/#limit-50" +URL_GIT_COMMIT = "https://chris.beams.io/posts/git-commit/" + # rubocop: disable Style/SignalException # rubocop: disable Metrics/CyclomaticComplexity # rubocop: disable Metrics/PerceivedComplexity @@ -14,9 +17,6 @@ class EmojiChecker DIGESTS = File.expand_path('../../fixtures/emojis/digests.json', __dir__) ALIASES = File.expand_path('../../fixtures/emojis/aliases.json', __dir__) - URL_LIMIT_SUBJECT = "https://chris.beams.io/posts/git-commit/#limit-50" - URL_GIT_COMMIT = "https://chris.beams.io/posts/git-commit/" - # A regex that indicates a piece of text _might_ include an Emoji. The regex # alone is not enough, as we'd match `:foo:bar:baz`. Instead, we use this # regex to save us from having to check for all possible emoji names when we diff --git a/danger/documentation/Dangerfile b/danger/documentation/Dangerfile index 52af837c261..188331cc87c 100644 --- a/danger/documentation/Dangerfile +++ b/danger/documentation/Dangerfile @@ -32,7 +32,7 @@ to be reviewed. | Tech writer | Stage(s) | | ------------ | ------------------------------------------------------------ | | `@marcia` | ~Create ~Release + ~"development guidelines" | -| `@axil` | ~Distribution ~Gitaly ~Gitter ~Monitoring ~Package ~Secure | +| `@axil` | ~Distribution ~Gitaly ~Gitter ~Monitor ~Package ~Secure | | `@eread` | ~Manage ~Configure ~Geo ~Verify | | `@mikelewis` | ~Plan | diff --git a/db/fixtures/development/10_merge_requests.rb b/db/fixtures/development/10_merge_requests.rb index 8bdc7c6556c..2051bcff8f0 100644 --- a/db/fixtures/development/10_merge_requests.rb +++ b/db/fixtures/development/10_merge_requests.rb @@ -27,6 +27,9 @@ Gitlab::Seeder.quiet do Sidekiq::Worker.skipping_transaction_check do MergeRequests::CreateService.new(project, developer, params).execute + rescue Repository::AmbiguousRefError + # Ignore pipelines creation errors for now, we can doing that after + # https://gitlab.com/gitlab-org/gitlab-ce/issues/55966. will be resolved. end print '.' end diff --git a/db/migrate/20190104182041_cleanup_legacy_artifact_migration.rb b/db/migrate/20190104182041_cleanup_legacy_artifact_migration.rb new file mode 100644 index 00000000000..11659846a06 --- /dev/null +++ b/db/migrate/20190104182041_cleanup_legacy_artifact_migration.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +class CleanupLegacyArtifactMigration < ActiveRecord::Migration[5.0] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + class Build < ActiveRecord::Base + include EachBatch + + self.table_name = 'ci_builds' + self.inheritance_column = :_type_disabled + + scope :with_legacy_artifacts, -> { where("artifacts_file <> ''") } + end + + def up + Gitlab::BackgroundMigration.steal('MigrateLegacyArtifacts') + + CleanupLegacyArtifactMigration::Build + .with_legacy_artifacts + .each_batch(of: 100) do |batch| + range = batch.pluck('MIN(id)', 'MAX(id)').first + + Gitlab::BackgroundMigration::MigrateLegacyArtifacts.new.perform(*range) + end + end + + def down + # no-op + end +end diff --git a/db/migrate/20190108192941_remove_partial_index_from_ci_builds_artifacts_file.rb b/db/migrate/20190108192941_remove_partial_index_from_ci_builds_artifacts_file.rb new file mode 100644 index 00000000000..073faf721ae --- /dev/null +++ b/db/migrate/20190108192941_remove_partial_index_from_ci_builds_artifacts_file.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +class RemovePartialIndexFromCiBuildsArtifactsFile < ActiveRecord::Migration[5.0] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + INDEX_NAME = 'partial_index_ci_builds_on_id_with_legacy_artifacts'.freeze + + disable_ddl_transaction! + + def up + remove_concurrent_index_by_name(:ci_builds, INDEX_NAME) + end + + def down + add_concurrent_index(:ci_builds, :id, where: "artifacts_file <> ''", name: INDEX_NAME) + end +end diff --git a/db/migrate/20190114172110_add_domain_to_cluster.rb b/db/migrate/20190114172110_add_domain_to_cluster.rb new file mode 100644 index 00000000000..58d7664b8c0 --- /dev/null +++ b/db/migrate/20190114172110_add_domain_to_cluster.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class AddDomainToCluster < ActiveRecord::Migration[5.0] + DOWNTIME = false + + def change + add_column :clusters, :domain, :string + end +end diff --git a/db/migrate/20190115054216_add_error_notification_sent_to_remote_mirrors.rb b/db/migrate/20190115054216_add_error_notification_sent_to_remote_mirrors.rb new file mode 100644 index 00000000000..d8f979a1848 --- /dev/null +++ b/db/migrate/20190115054216_add_error_notification_sent_to_remote_mirrors.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class AddErrorNotificationSentToRemoteMirrors < ActiveRecord::Migration[5.0] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def change + add_column :remote_mirrors, :error_notification_sent, :boolean + end +end diff --git a/db/post_migrate/20161221153951_rename_reserved_project_names.rb b/db/post_migrate/20161221153951_rename_reserved_project_names.rb index 50e1c8449ba..32579256299 100644 --- a/db/post_migrate/20161221153951_rename_reserved_project_names.rb +++ b/db/post_migrate/20161221153951_rename_reserved_project_names.rb @@ -113,7 +113,7 @@ class RenameReservedProjectNames < ActiveRecord::Migration[4.2] # Because project path update is quite complex operation we can't safely # copy-paste all code from GitLab. As exception we use Rails code here if rename_project_row(project, path) - Projects::AfterRenameService.new(project).execute + after_rename_service(project, path_was, namespace_path).execute end rescue Exception => e # rubocop: disable Lint/RescueException Rails.logger.error "Exception when renaming project #{id}: #{e.message}" @@ -126,4 +126,12 @@ class RenameReservedProjectNames < ActiveRecord::Migration[4.2] project.update(path: path) && defined?(Projects::AfterRenameService) end + + def after_rename_service(project, path_was, namespace_path) + AfterRenameService.new( + project, + path_before: path_was, + full_path_before: "#{namespace_path}/#{path_was}" + ).execute + end end diff --git a/db/post_migrate/20170313133418_rename_more_reserved_project_names.rb b/db/post_migrate/20170313133418_rename_more_reserved_project_names.rb index bef669b459d..85c97e3687e 100644 --- a/db/post_migrate/20170313133418_rename_more_reserved_project_names.rb +++ b/db/post_migrate/20170313133418_rename_more_reserved_project_names.rb @@ -55,7 +55,7 @@ class RenameMoreReservedProjectNames < ActiveRecord::Migration[4.2] # Because project path update is quite complex operation we can't safely # copy-paste all code from GitLab. As exception we use Rails code here if rename_project_row(project, path) - Projects::AfterRenameService.new(project).execute + after_rename_service(project, path_was, namespace_path).execute end rescue Exception => e # rubocop: disable Lint/RescueException Rails.logger.error "Exception when renaming project #{id}: #{e.message}" @@ -68,4 +68,12 @@ class RenameMoreReservedProjectNames < ActiveRecord::Migration[4.2] project.update(path: path) && defined?(Projects::AfterRenameService) end + + def after_rename_service(project, path_was, namespace_path) + AfterRenameService.new( + project, + path_before: path_was, + full_path_before: "#{namespace_path}/#{path_was}" + ).execute + end end diff --git a/db/schema.rb b/db/schema.rb index 87826881d58..cd502d06bf4 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20190103140724) do +ActiveRecord::Schema.define(version: 20190115054216) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -354,7 +354,6 @@ ActiveRecord::Schema.define(version: 20190103140724) do t.index ["commit_id", "status", "type"], name: "index_ci_builds_on_commit_id_and_status_and_type", using: :btree t.index ["commit_id", "type", "name", "ref"], name: "index_ci_builds_on_commit_id_and_type_and_name_and_ref", using: :btree t.index ["commit_id", "type", "ref"], name: "index_ci_builds_on_commit_id_and_type_and_ref", using: :btree - t.index ["id"], name: "partial_index_ci_builds_on_id_with_legacy_artifacts", where: "(artifacts_file <> ''::text)", using: :btree t.index ["project_id", "id"], name: "index_ci_builds_on_project_id_and_id", using: :btree t.index ["project_id", "status"], name: "index_ci_builds_project_id_and_status_for_live_jobs_partial2", where: "(((type)::text = 'Ci::Build'::text) AND ((status)::text = ANY (ARRAY[('running'::character varying)::text, ('pending'::character varying)::text, ('created'::character varying)::text])))", using: :btree t.index ["protected"], name: "index_ci_builds_on_protected", using: :btree @@ -648,6 +647,7 @@ ActiveRecord::Schema.define(version: 20190103140724) do t.string "name", null: false t.string "environment_scope", default: "*", null: false t.integer "cluster_type", limit: 2, default: 3, null: false + t.string "domain" t.index ["enabled"], name: "index_clusters_on_enabled", using: :btree t.index ["user_id"], name: "index_clusters_on_user_id", using: :btree end @@ -1849,6 +1849,7 @@ ActiveRecord::Schema.define(version: 20190103140724) do t.string "encrypted_credentials_salt" t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.boolean "error_notification_sent" t.index ["last_successful_update_at"], name: "index_remote_mirrors_on_last_successful_update_at", using: :btree t.index ["project_id"], name: "index_remote_mirrors_on_project_id", using: :btree end diff --git a/doc/administration/gitaly/index.md b/doc/administration/gitaly/index.md index cf37eaa0b61..abef7a6cd33 100644 --- a/doc/administration/gitaly/index.md +++ b/doc/administration/gitaly/index.md @@ -49,6 +49,25 @@ Starting with GitLab 11.4, Gitaly is a replacement for NFS except when the [Elastic Search indexer](https://gitlab.com/gitlab-org/gitlab-elasticsearch-indexer) is used. +### Network architecture + +- gitlab-rails shards repositories into "repository storages" +- gitlab-rails/config/gitlab.yml contains a map from storage names to + (Gitaly address, Gitaly token) pairs +- the `storage name` -\> `(Gitaly address, Gitaly token)` map in + gitlab.yml is the single source of truth for the Gitaly network + topology +- a (Gitaly address, Gitaly token) corresponds to a Gitaly server +- a Gitaly server hosts one or more storages +- Gitaly addresses must be specified in such a way that they resolve + correctly for ALL Gitaly clients +- Gitaly clients are: unicorn, sidekiq, gitlab-workhorse, + gitlab-shell, and Gitaly itself +- special case: a Gitaly server must be able to make RPC calls **to + itself** via its own (Gitaly address, Gitaly token) pair as + specified in gitlab-rails/config/gitlab.yml +- Gitaly servers must not be exposed to the public internet + Gitaly network traffic is unencrypted so you should use a firewall to restrict access to your Gitaly server. @@ -217,6 +236,8 @@ repository from your GitLab server over HTTP. ## TLS support +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/22602) in GitLab 11.7. + Gitaly supports TLS credentials for GRPC authentication. To be able to communicate with a gitaly instance that listens for secure connections you will need to use `tls://` url scheme in the `gitaly_address` of the corresponding storage entry in the gitlab configuration. diff --git a/doc/api/avatar.md b/doc/api/avatar.md index aa6f7c185ae..e55fffba4b2 100644 --- a/doc/api/avatar.md +++ b/doc/api/avatar.md @@ -1,33 +1,41 @@ # Avatar API -> [Introduced][ce-19121] in GitLab 11.0 +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/19121) in GitLab 11.0. ## Get a single avatar URL -Get a single avatar URL for a given email address. 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 a single [avatar](../user/profile/index.md#profile-settings) URL for a user with the given email address. -``` +If: + +- No user with the given public email address is found, results from external avatar services are + returned. +- Public visibility is restricted, response will be `403 Forbidden` when unauthenticated. + +NOTE: **Note:** +This endpoint can be accessed without authentication. + +```text 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 | +Parameters: -```bash -curl https://gitlab.example.com/api/v4/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. | + +Example request: + +```sh +curl https://gitlab.example.com/api/v4/avatar?email=admin@example.com&size=32 ``` Example response: ```json { - "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon" + "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=64&d=identicon" } ``` - -[ce-19121]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/19121 diff --git a/doc/api/award_emoji.md b/doc/api/award_emoji.md index 41e39c31069..92936a277ac 100644 --- a/doc/api/award_emoji.md +++ b/doc/api/award_emoji.md @@ -1,19 +1,26 @@ # Award Emoji API -> [Introduced][ce-4575] in GitLab 8.9, Snippet support in 8.12 +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/4575) in GitLab 8.9. Snippet support added in 8.12. +An [awarded emoji](../user/award_emojis.md) tells a thousand words. -An awarded emoji tells a thousand words, and can be awarded on issues, merge -requests, snippets, and notes/comments. Issues, merge requests, snippets, and notes are further called -`awardables`. +Emoji can be awarded on the following (known as "awardables"): + +- [Issues](../user/project/issues/index.md) ([API](issues.md)). +- [Merge requests](../user/project/merge_requests/index.md) ([API](merge_requests.md)). +- [Snippets](../user/snippets.md) ([API](snippets.md)). + +Emoji can also [be awarded](../user/award_emojis.html#award-emoji-for-comments) on comments (also known as notes). See also [Notes API](notes.md). ## Issues, merge requests, and snippets +See [Award Emoji on Comments](#award-emoji-on-comments) for information on using these endpoints with comments. + ### List an awardable's award emoji -Gets a list of all award emoji +Get a list of all award emoji for a specified awardable. -``` +```text GET /projects/:id/issues/:issue_iid/award_emoji GET /projects/:id/merge_requests/:merge_request_iid/award_emoji GET /projects/:id/snippets/:snippet_id/award_emoji @@ -21,16 +28,18 @@ GET /projects/:id/snippets/:snippet_id/award_emoji Parameters: -| Attribute | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | -| `awardable_id` | integer | yes | The ID (`iid` for merge requests/issues, `id` for snippets) of an awardable | +| Attribute | Type | Required | Description | +|:---------------|:---------------|:---------|:-----------------------------------------------------------------------------| +| `id` | integer/string | yes | ID or [URL-encoded path of the project](README.md#namespaced-path-encoding). | +| `awardable_id` | integer | yes | ID (`iid` for merge requests/issues, `id` for snippets) of an awardable. | -```bash +Example request: + +```sh curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/1/issues/80/award_emoji ``` -Example Response: +Example response: ```json [ @@ -71,9 +80,9 @@ Example Response: ### Get single award emoji -Gets a single award emoji from an issue, snippet, or merge request. +Get a single award emoji from an issue, snippet, or merge request. -``` +```text GET /projects/:id/issues/:issue_iid/award_emoji/:award_id GET /projects/:id/merge_requests/:merge_request_iid/award_emoji/:award_id GET /projects/:id/snippets/:snippet_id/award_emoji/:award_id @@ -81,17 +90,19 @@ GET /projects/:id/snippets/:snippet_id/award_emoji/:award_id Parameters: -| Attribute | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | -| `awardable_id` | integer | yes | The ID (`iid` for merge requests/issues, `id` for snippets) of an awardable | -| `award_id` | integer | yes | The ID of the award emoji | +| Attribute | Type | Required | Description | +|:---------------|:---------------|:---------|:-----------------------------------------------------------------------------| +| `id` | integer/string | yes | ID or [URL-encoded path of the project](README.md#namespaced-path-encoding). | +| `awardable_id` | integer | yes | ID (`iid` for merge requests/issues, `id` for snippets) of an awardable. | +| `award_id` | integer | yes | ID of the award emoji. | + +Example request: -```bash +```sh curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/1/issues/80/award_emoji/1 ``` -Example Response: +Example response: ```json { @@ -114,9 +125,9 @@ Example Response: ### Award a new emoji -This end point creates an award emoji on the specified resource +Create an award emoji on the specified awardable. -``` +```text POST /projects/:id/issues/:issue_iid/award_emoji POST /projects/:id/merge_requests/:merge_request_iid/award_emoji POST /projects/:id/snippets/:snippet_id/award_emoji @@ -124,13 +135,13 @@ POST /projects/:id/snippets/:snippet_id/award_emoji Parameters: -| Attribute | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | -| `awardable_id` | integer | yes | The ID (`iid` for merge requests/issues, `id` for snippets) of an awardable | -| `name` | string | yes | The name of the emoji, without colons | +| Attribute | Type | Required | Description | +|:---------------|:---------------|:---------|:-----------------------------------------------------------------------------| +| `id` | integer/string | yes | ID or [URL-encoded path of the project](README.md#namespaced-path-encoding). | +| `awardable_id` | integer | yes | ID (`iid` for merge requests/issues, `id` for snippets) of an awardable. | +| `name` | string | yes | Name of the emoji without colons. | -```bash +```sh curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/1/issues/80/award_emoji?name=blowfish ``` @@ -157,10 +168,12 @@ Example Response: ### Delete an award emoji -Sometimes its just not meant to be, and you'll have to remove your award. Only available to -admins or the author of the award. +Sometimes it's just not meant to be and you'll have to remove the award. -``` +NOTE: **Note:** +Only available to administrators or the author of the award. + +```text DELETE /projects/:id/issues/:issue_iid/award_emoji/:award_id DELETE /projects/:id/merge_requests/:merge_request_iid/award_emoji/:award_id DELETE /projects/:id/snippets/:snippet_id/award_emoji/:award_id @@ -168,43 +181,47 @@ DELETE /projects/:id/snippets/:snippet_id/award_emoji/:award_id Parameters: -| Attribute | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | -| `issue_iid` | integer | yes | The internal ID of an issue | -| `award_id` | integer | yes | The ID of an award_emoji | +| Attribute | Type | Required | Description | +|:---------------|:---------------|:---------|:-----------------------------------------------------------------------------| +| `id` | integer/string | yes | ID or [URL-encoded path of the project](README.md#namespaced-path-encoding). | +| `awardable_id` | integer | yes | ID (`iid` for merge requests/issues, `id` for snippets) of an awardable. | +| `award_id` | integer | yes | ID of an award emoji. | -```bash +```sh curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/1/issues/80/award_emoji/344 ``` -## Award Emoji on Notes +## Award Emoji on Comments -The endpoints documented above are available for Notes as well. Notes -are a sub-resource of Issues, Merge Requests, or Snippets. The examples below -describe working with Award Emoji on notes for an Issue, but can be -easily adapted for notes on a Merge Request. +Comments (also known as notes) are a sub-resource of issues, merge requests, and snippets. -### List a note's award emoji +NOTE: **Note:** +The examples below describe working with award emoji on comments for an issue, but can be +easily adapted for comments on a merge request. -``` +### List a comment's award emoji + +Get all award emoji for a comment (note). + +```text GET /projects/:id/issues/:issue_iid/notes/:note_id/award_emoji ``` Parameters: -| Attribute | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | -| `issue_iid` | integer | yes | The internal ID of an issue | -| `note_id` | integer | yes | The ID of a note | +| Attribute | Type | Required | Description | +|:------------|:---------------|:---------|:-----------------------------------------------------------------------------| +| `id` | integer/string | yes | ID or [URL-encoded path of the project](README.md#namespaced-path-encoding). | +| `issue_iid` | integer | yes | Internal ID of an issue. | +| `note_id` | integer | yes | ID of a comment (note). | +Example request: -```bash +```sh curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/1/issues/80/notes/1/award_emoji ``` -Example Response: +Example response: ```json [ @@ -227,26 +244,30 @@ Example Response: ] ``` -### Get single note's award emoji +### Get an award emoji for a comment -``` +Get a single award emoji for a comment (note). + +```text GET /projects/:id/issues/:issue_iid/notes/:note_id/award_emoji/:award_id ``` Parameters: -| Attribute | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | -| `issue_iid` | integer | yes | The internal ID of an issue | -| `note_id` | integer | yes | The ID of a note | -| `award_id` | integer | yes | The ID of the award emoji | +| Attribute | Type | Required | Description | +|:------------|:---------------|:---------|:-----------------------------------------------------------------------------| +| `id` | integer/string | yes | ID or [URL-encoded path of the project](README.md#namespaced-path-encoding). | +| `issue_iid` | integer | yes | Internal ID of an issue. | +| `note_id` | integer | yes | ID of a comment (note). | +| `award_id` | integer | yes | ID of the award emoji. | + +Example request: -```bash +```sh curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/1/issues/80/notes/1/award_emoji/2 ``` -Example Response: +Example response: ```json { @@ -267,26 +288,30 @@ Example Response: } ``` -### Award a new emoji on a note +### Award a new emoji on a comment -``` +Create an award emoji on the specified comment (note). + +```text POST /projects/:id/issues/:issue_iid/notes/:note_id/award_emoji ``` Parameters: -| Attribute | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | -| `issue_iid` | integer | yes | The internal ID of an issue | -| `note_id` | integer | yes | The ID of a note | -| `name` | string | yes | The name of the emoji, without colons | +| Attribute | Type | Required | Description | +|:------------|:---------------|:---------|:-----------------------------------------------------------------------------| +| `id` | integer/string | yes | ID or [URL-encoded path of the project](README.md#namespaced-path-encoding). | +| `issue_iid` | integer | yes | Internal ID of an issue. | +| `note_id` | integer | yes | ID of a comment (note). | +| `name` | string | yes | Name of the emoji without colons. | -```bash +Example request: + +```sh curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/1/issues/80/notes/1/award_emoji?name=rocket ``` -Example Response: +Example response: ```json { @@ -307,26 +332,28 @@ Example Response: } ``` -### Delete an award emoji +### Delete an award emoji from a comment -Sometimes its just not meant to be, and you'll have to remove your award. Only available to -admins or the author of the award. +Sometimes it's just not meant to be and you'll have to remove the award. -``` +NOTE: **Note:** +Only available to administrators or the author of the award. + +```text DELETE /projects/:id/issues/:issue_iid/notes/:note_id/award_emoji/:award_id ``` Parameters: -| Attribute | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | -| `issue_iid` | integer | yes | The internal ID of an issue | -| `note_id` | integer | yes | The ID of a note | -| `award_id` | integer | yes | The ID of an award_emoji | +| Attribute | Type | Required | Description | +|:------------|:---------------|:---------|:-----------------------------------------------------------------------------| +| `id` | integer/string | yes | ID or [URL-encoded path of the project](README.md#namespaced-path-encoding). | +| `issue_iid` | integer | yes | Internal ID of an issue. | +| `note_id` | integer | yes | ID of a comment (note). | +| `award_id` | integer | yes | ID of an award_emoji. | + +Example request: -```bash +```sh curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/1/issues/80/award_emoji/345 ``` - -[ce-4575]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/4575 diff --git a/doc/api/branches.md b/doc/api/branches.md index 3b55154887d..8d5f333ba77 100644 --- a/doc/api/branches.md +++ b/doc/api/branches.md @@ -1,21 +1,31 @@ # Branches API +This API operates on [repository branches](../user/project/repository/branches/index.md). + +TIP: **Tip:** +See also [Protected branches API](protected_branches.md). + ## List repository branches Get a list of repository branches from a project, sorted by name alphabetically. -This endpoint can be accessed without authentication if the repository is -publicly accessible. -``` +NOTE: **Note:** +This endpoint can be accessed without authentication if the repository is publicly accessible. + +```text GET /projects/:id/repository/branches ``` -| Attribute | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | -| `search` | string | no | Return list of branches matching the search criteria. | +Parameters: + +| Attribute | Type | Required | Description | +|:----------|:---------------|:---------|:-------------------------------------------------------------------------------------------------------------| +| `id` | integer/string | yes | ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user. | +| `search` | string | no | Return list of branches matching the search criteria. | + +Example request: -```bash +```sh curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/5/repository/branches ``` @@ -53,19 +63,25 @@ Example response: ## Get single repository branch -Get a single project repository branch. This endpoint can be accessed without -authentication if the repository is publicly accessible. +Get a single project repository branch. -``` +NOTE: **Note:** +This endpoint can be accessed without authentication if the repository is publicly accessible. + +```text GET /projects/:id/repository/branches/:branch ``` -| Attribute | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | -| `branch` | string | yes | The name of the branch | +Parameters: + +| Attribute | Type | Required | Description | +|:----------|:---------------|:---------|:-------------------------------------------------------------------------------------------------------------| +| `id` | integer/string | yes | ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user. | +| `branch` | string | yes | Name of the branch. | -```bash +Example request: + +```sh curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/5/repository/branches/master ``` @@ -100,120 +116,34 @@ Example response: ## Protect repository branch ->**Note:** This API endpoint is deprecated in favor of `POST /projects/:id/protected_branches`. - -Protects a single project repository branch. This is an idempotent function, -protecting an already protected repository branch still returns a `200 OK` -status code. - -``` -PUT /projects/:id/repository/branches/:branch/protect -``` - -```bash -curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/5/repository/branches/master/protect?developers_can_push=true&developers_can_merge=true -``` - -| Attribute | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | -| `branch` | string | yes | The name of the branch | -| `developers_can_push` | boolean | no | Flag if developers can push to the branch | -| `developers_can_merge` | boolean | no | Flag if developers can merge to the branch | - -Example response: - -```json -{ - "commit": { - "author_email": "john@example.com", - "author_name": "John Smith", - "authored_date": "2012-06-27T05:51:39-07:00", - "committed_date": "2012-06-28T03:44:20-07:00", - "committer_email": "john@example.com", - "committer_name": "John Smith", - "id": "7b5c3cc8be40ee161ae89a06bba6229da1032a0c", - "short_id": "7b5c3cc", - "title": "add projects API", - "message": "add projects API", - "parent_ids": [ - "4ad91d3c1144c406e50c7b33bae684bd6837faf8" - ] - }, - "name": "master", - "merged": false, - "protected": true, - "default": true, - "developers_can_push": true, - "developers_can_merge": true, - "can_push": true -} -``` +See [`POST /projects/:id/protected_branches`](protected_branches.md#protect-repository-branches) for +information on protecting repository branches. ## Unprotect repository branch ->**Note:** This API endpoint is deprecated in favor of `DELETE /projects/:id/protected_branches/:name` +See [`DELETE /projects/:id/protected_branches/:name`](protected_branches.md#unprotect-repository-branches) +for information on unprotecting repository branches. -Unprotects a single project repository branch. This is an idempotent function, -unprotecting an already unprotected repository branch still returns a `200 OK` -status code. - -``` -PUT /projects/:id/repository/branches/:branch/unprotect -``` - -```bash -curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/5/repository/branches/master/unprotect -``` - -| Attribute | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | -| `branch` | string | yes | The name of the branch | +## Create repository branch -Example response: +Create a new branch in the repository. -```json -{ - "commit": { - "author_email": "john@example.com", - "author_name": "John Smith", - "authored_date": "2012-06-27T05:51:39-07:00", - "committed_date": "2012-06-28T03:44:20-07:00", - "committer_email": "john@example.com", - "committer_name": "John Smith", - "id": "7b5c3cc8be40ee161ae89a06bba6229da1032a0c", - "short_id": "7b5c3cc", - "title": "add projects API", - "message": "add projects API", - "parent_ids": [ - "4ad91d3c1144c406e50c7b33bae684bd6837faf8" - ] - }, - "name": "master", - "merged": false, - "protected": false, - "default": true, - "developers_can_push": false, - "developers_can_merge": false, - "can_push": true -} +```text +POST /projects/:id/repository/branches ``` -## Create repository branch +Parameters: -``` -POST /projects/:id/repository/branches -``` +| Attribute | Type | Required | Description | +|:----------|:--------|:---------|:-------------------------------------------------------------------------------------------------------------| +| `id` | integer | yes | ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user. | +| `branch` | string | yes | Name of the branch. | +| `ref` | string | yes | Branch name or commit SHA to create branch from. | -| Attribute | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `id` | integer | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | -| `branch` | string | yes | The name of the branch | -| `ref` | string | yes | The branch name or commit SHA to create branch from | +Example request: -```bash -curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/repository/branches?branch=newbranch&ref=master" +```sh +curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/5/repository/branches?branch=newbranch&ref=master ``` Example response: @@ -247,36 +177,47 @@ Example response: ## Delete repository branch -``` +Delete a branch from the repository. + +NOTE: **Note:** +In the case of an error, an explanation message is provided. + +```text DELETE /projects/:id/repository/branches/:branch ``` -| Attribute | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | -| `branch` | string | yes | The name of the branch | +Parameters: + +| Attribute | Type | Required | Description | +|:----------|:---------------|:---------|:-------------------------------------------------------------------------------------------------------------| +| `id` | integer/string | yes | ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user. | +| `branch` | string | yes | Name of the branch. | -In case of an error, an explaining message is provided. +Example request: -```bash -curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/repository/branches/newbranch" +```sh +curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/5/repository/branches/newbranch ``` ## Delete merged branches Will delete all branches that are merged into the project's default branch. -Protected branches will not be deleted as part of this operation. +NOTE: **Note:** +[Protected branches](../user/project/protected_branches.md) will not be deleted as part of this operation. -``` +```text DELETE /projects/:id/repository/merged_branches ``` -| Attribute | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | +Parameters: + +| Attribute | Type | Required | Description | +|:----------|:---------------|:---------|:-------------------------------------------------------------------------------------------------------------| +| `id` | integer/string | yes | ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user. | +Example request: -```bash -curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/repository/merged_branches" +```sh +curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/5/repository/merged_branches ``` diff --git a/doc/api/features.md b/doc/api/features.md index 59f1005ef72..47f104e1f20 100644 --- a/doc/api/features.md +++ b/doc/api/features.md @@ -60,9 +60,10 @@ POST /features/:name | `value` | integer/string | yes | `true` or `false` to enable/disable, or an integer for percentage of time | | `feature_group` | string | no | A Feature group name | | `user` | string | no | A GitLab username | +| `project` | string | no | A projects path, for example 'gitlab-org/gitlab-ce' | -Note that you can enable or disable a feature for both a `feature_group` and a -`user` with a single API call. +Note that you can enable or disable a feature for a `feature_group`, a `user`, +and a `project` in a single API call. ```bash curl --data "value=30" --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/features/new_library diff --git a/doc/api/groups.md b/doc/api/groups.md index 2d9114c40ea..907b443d355 100644 --- a/doc/api/groups.md +++ b/doc/api/groups.md @@ -357,12 +357,14 @@ Example response: { "group_id": 4, "group_name": "Twitter", + "group_full_path": "twitter", "group_access_level": 30, "expires_at": null }, { "group_id": 3, "group_name": "Gitlab Org", + "group_full_path": "gitlab-org", "group_access_level": 10, "expires_at": "2018-08-14" } diff --git a/doc/api/import.md b/doc/api/import.md new file mode 100644 index 00000000000..9f8e0d232c6 --- /dev/null +++ b/doc/api/import.md @@ -0,0 +1,33 @@ +# Import API + +## Import repository from GitHub + +Import your projects from GitHub to GitLab via the API. + +``` +POST /import/github +``` + +| Attribute | Type | Required | Description | +|------------|---------|----------|---------------------| +| `personal_access_token` | string | yes | GitHub personal access token | +| `repo_id` | integer | yes | GitHub repository ID | +| `new_name` | string | no | New repo name | +| `target_namespace` | string | yes | Namespace to import repo into | + + +```bash +curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --data "personal_access_token=abc123&repo_id=12345&target_namespace=root" https://gitlab.example.com/api/v4/import/github +``` + +Example response: + +```json +{ + "id": 27, + "name": "my-repo", + "full_path": "/root/my-repo", + "full_name": "Administrator / my-repo" +} +``` + diff --git a/doc/api/oauth2.md b/doc/api/oauth2.md index 6786c0c5b5c..6e156a14b25 100644 --- a/doc/api/oauth2.md +++ b/doc/api/oauth2.md @@ -1,126 +1,165 @@ # GitLab as an OAuth2 provider -This document covers using the [OAuth2](https://oauth.net/2/) protocol to allow other services access GitLab resources on user's behalf. +This document covers using the [OAuth2](https://oauth.net/2/) protocol to allow +other services to access GitLab resources on user's behalf. -If you want GitLab to be an OAuth authentication service provider to sign into other services please see the [OAuth2 provider](../integration/oauth_provider.md) -documentation. +If you want GitLab to be an OAuth authentication service provider to sign into +other services, see the [OAuth2 provider](../integration/oauth_provider.md) +documentation. This functionality is based on the +[doorkeeper Ruby gem](https://github.com/doorkeeper-gem/doorkeeper). -This functionality is based on [doorkeeper gem](https://github.com/doorkeeper-gem/doorkeeper). +## Supported OAuth2 flows -## Supported OAuth2 Flows +GitLab currently supports the following authorization flows: -GitLab currently supports following authorization flows: +- **Web application flow:** Most secure and common type of flow, designed for + applications with secure server-side. +- **Implicit grant flow:** This flow is designed for user-agent only apps (e.g., single + page web application running on GitLab Pages). +- **Resource owner password credentials flow:** To be used **only** for securely + hosted, first-party services. -- *Web Application Flow* - Most secure and common type of flow, designed for the applications with secure server-side. -- *Implicit Flow* - This flow is designed for user-agent only apps (e.g. single page web application running on GitLab Pages). -- *Resource Owner Password Credentials Flow* - To be used **only** for securely hosted, first-party services. +Refer to the [OAuth RFC](https://tools.ietf.org/html/rfc6749) to find out +how all those flows work and pick the right one for your use case. -Please refer to [OAuth RFC](https://tools.ietf.org/html/rfc6749) to find out in details how all those flows work and pick the right one for your use case. +Both **web application** and **implicit grant** flows require `application` to be +registered first via the `/profile/applications` page in your user's account. +During registration, by enabling proper scopes, you can limit the range of +resources which the `application` can access. Upon creation, you'll obtain the +`application` credentials: _Application ID_ and _Client Secret_ - **keep them secure**. -Both *web application* and *implicit* flows require `application` to be registered first via `/profile/applications` page -in your user's account. During registration, by enabling proper scopes you can limit the range of resources which the `application` can access. Upon creation -you'll obtain `application` credentials: _Application ID_ and _Client Secret_ - **keep them secure**. +CAUTION: **Important:** +OAuth specification advises sending the `state` parameter with each request to +`/oauth/authorize`. We highly recommended sending a unique value with each request +and validate it against the one in the redirect request. This is important in +order to prevent [CSRF attacks](https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)). +The `state` parameter really should have been a requirement in the standard! ->**Important:** OAuth specification advises sending `state` parameter with each request to `/oauth/authorize`. We highly recommended to send a unique -value with each request and validate it against the one in redirect request. This is important to prevent [CSRF attacks]. The `state` param really should -have been a requirement in the standard! +In the following sections you will find detailed instructions on how to obtain +authorization with each flow. -In the following sections you will find detailed instructions on how to obtain authorization with each flow. +### Web application flow -### Web Application Flow +NOTE: **Note:** +Check the [RFC spec](https://tools.ietf.org/html/rfc6749#section-4.1) for a +detailed flow description. -Check [RFC spec](http://tools.ietf.org/html/rfc6749#section-4.1) for a detailed flow description +The web application flow is: -#### 1. Requesting authorization code +1. Request authorization code. To do that, you should redirect the user to the + `/oauth/authorize` endpoint with the following GET parameters: -To request the authorization code, you should redirect the user to the `/oauth/authorize` endpoint with following GET parameters: + ``` + https://gitlab.example.com/oauth/authorize?client_id=APP_ID&redirect_uri=REDIRECT_URI&response_type=code&state=YOUR_UNIQUE_STATE_HASH + ``` -``` -https://gitlab.example.com/oauth/authorize?client_id=APP_ID&redirect_uri=REDIRECT_URI&response_type=code&state=YOUR_UNIQUE_STATE_HASH -``` + This will ask the user to approve the applications access to their account and + then redirect back to the `REDIRECT_URI` you provided. The redirect will + include the GET `code` parameter, for example: -This will ask the user to approve the applications access to their account and then redirect back to the `REDIRECT_URI` you provided. The redirect will -include the GET `code` parameter, for example: + ``` + http://myapp.com/oauth/redirect?code=1234567890&state=YOUR_UNIQUE_STATE_HASH + ``` -`http://myapp.com/oauth/redirect?code=1234567890&state=YOUR_UNIQUE_STATE_HASH` + You should then use `code` to request an access token. -You should then use the `code` to request an access token. +1. Once you have the authorization code you can request an `access_token` using the + code. You can do that by using any HTTP client. In the following example, + we are using Ruby's `rest-client`: -#### 2. Requesting access token + ```ruby + parameters = 'client_id=APP_ID&client_secret=APP_SECRET&code=RETURNED_CODE&grant_type=authorization_code&redirect_uri=REDIRECT_URI' + RestClient.post 'http://gitlab.example.com/oauth/token', parameters + ``` -Once you have the authorization code you can request an `access_token` using the code, to do that you can use any HTTP client. In the following example, -we are using Ruby's `rest-client`: + Example response: -``` -parameters = 'client_id=APP_ID&client_secret=APP_SECRET&code=RETURNED_CODE&grant_type=authorization_code&redirect_uri=REDIRECT_URI' -RestClient.post 'http://gitlab.example.com/oauth/token', parameters + ```json + { + "access_token": "de6780bc506a0446309bd9362820ba8aed28aa506c71eedbe1c5c4f9dd350e54", + "token_type": "bearer", + "expires_in": 7200, + "refresh_token": "8257e65c97202ed1726cf9571600918f3bffb2544b26e00a61df9897668c33a1" + } + ``` -# The response will be -{ - "access_token": "de6780bc506a0446309bd9362820ba8aed28aa506c71eedbe1c5c4f9dd350e54", - "token_type": "bearer", - "expires_in": 7200, - "refresh_token": "8257e65c97202ed1726cf9571600918f3bffb2544b26e00a61df9897668c33a1" -} -``` ->**Note:** -The `redirect_uri` must match the `redirect_uri` used in the original authorization request. +NOTE: **Note:** +The `redirect_uri` must match the `redirect_uri` used in the original +authorization request. You can now make requests to the API with the access token returned. +### Implicit grant flow -### Implicit Grant - -Check [RFC spec](http://tools.ietf.org/html/rfc6749#section-4.2) for a detailed flow description. - -Unlike the web flow, the client receives an `access token` immediately as a result of the authorization request. The flow does not use client secret -or authorization code because all of the application code and storage is easily accessible, therefore __secrets__ can leak easily. +NOTE: **Note:** +Check the [RFC spec](https://tools.ietf.org/html/rfc6749#section-4.2) for a +detailed flow description. ->**Important:** Avoid using this flow for applications that store data outside of the GitLab instance. If you do, make sure to verify `application id` -associated with access token before granting access to the data +CAUTION: **Important:** +Avoid using this flow for applications that store data outside of the GitLab +instance. If you do, make sure to verify `application id` associated with the +access token before granting access to the data (see [/oauth/token/info](https://github.com/doorkeeper-gem/doorkeeper/wiki/API-endpoint-descriptions-and-examples#get----oauthtokeninfo)). +Unlike the web flow, the client receives an `access token` immediately as a +result of the authorization request. The flow does not use the client secret +or the authorization code because all of the application code and storage is +easily accessible, therefore secrets can leak easily. -#### 1. Requesting access token - -To request the access token, you should redirect the user to the `/oauth/authorize` endpoint using `token` response type: +To request the access token, you should redirect the user to the +`/oauth/authorize` endpoint using `token` response type: ``` https://gitlab.example.com/oauth/authorize?client_id=APP_ID&redirect_uri=REDIRECT_URI&response_type=token&state=YOUR_UNIQUE_STATE_HASH ``` -This will ask the user to approve the application's access to their account and then redirect back to the `REDIRECT_URI` you provided. The redirect -will include a fragment with `access_token` as well as token details in GET parameters, for example: +This will ask the user to approve the application's access to their account and +then redirect them back to the `REDIRECT_URI` you provided. The redirect +will include a fragment with `access_token` as well as token details in GET +parameters, for example: ``` http://myapp.com/oauth/redirect#access_token=ABCDExyz123&state=YOUR_UNIQUE_STATE_HASH&token_type=bearer&expires_in=3600 ``` -### Resource Owner Password Credentials +### Resource owner password credentials flow -Check [RFC spec](http://tools.ietf.org/html/rfc6749#section-4.3) for a detailed flow description. +NOTE: **Note:** +Check the [RFC spec](https://tools.ietf.org/html/rfc6749#section-4.3) for a +detailed flow description. -> **Deprecation notice:** Starting in GitLab 8.11, the Resource Owner Password Credentials has been *disabled* for users with two-factor authentication -turned on. These users can access the API using [personal access tokens] instead. +NOTE: **Note:** +The Resource Owner Password Credentials is disabled for users with [two-factor +authentication](../user/profile/account/two_factor_authentication.md) turned on. +These users can access the API using [personal access tokens](../user/profile/personal_access_tokens.md) +instead. -In this flow, a token is requested in exchange for the resource owner credentials (username and password). -The credentials should only be used when there is a high degree of trust between the resource owner and the client (e.g. the -client is part of the device operating system or a highly privileged application), and when other authorization grant types are not -available (such as an authorization code). +In this flow, a token is requested in exchange for the resource owner credentials +(username and password). ->**Important:** -Never store the user's credentials and only use this grant type when your client is deployed to a trusted environment, in 99% of cases [personal access tokens] -are a better choice. +The credentials should only be used when: -Even though this grant type requires direct client access to the resource owner credentials, the resource owner credentials are used -for a single request and are exchanged for an access token. This grant type can eliminate the need for the client to store the -resource owner credentials for future use, by exchanging the credentials with a long-lived access token or refresh token. +- There is a high degree of trust between the resource owner and the client. For + example, the client is part of the device operating system or a highly + privileged application. +- Other authorization grant types are not available (such as an authorization code). -#### 1. Requesting access token +CAUTION: **Important:** +Never store the user's credentials and only use this grant type when your client +is deployed to a trusted environment, in 99% of cases +[personal access tokens](../user/profile/personal_access_tokens.md) are a better +choice. -POST request to `/oauth/token` with parameters: +Even though this grant type requires direct client access to the resource owner +credentials, the resource owner credentials are used for a single request and +are exchanged for an access token. This grant type can eliminate the need for +the client to store the resource owner credentials for future use, by exchanging +the credentials with a long-lived access token or refresh token. -``` +To request an access token, you must make a POST request to `/oauth/token` with +the following parameters: + +```json { "grant_type" : "password", "username" : "user@example.com", @@ -128,6 +167,13 @@ POST request to `/oauth/token` with parameters: } ``` +Example cURL request: + +```sh +echo 'grant_type=password&username=<your_username>&password=<your_password>' > auth.txt +curl --data "@auth.txt" --request POST https://gitlab.example.com/oauth/token +``` + Then, you'll receive the access token back in the response: ``` @@ -138,7 +184,7 @@ Then, you'll receive the access token back in the response: } ``` -For testing you can use the oauth2 ruby gem: +For testing, you can use the `oauth2` Ruby gem: ``` client = OAuth2::Client.new('the_client_id', 'the_client_secret', :site => "http://example.com") @@ -148,7 +194,9 @@ puts access_token.token ## Access GitLab API with `access token` -The `access token` allows you to make requests to the API on a behalf of a user. You can pass the token either as GET parameter +The `access token` allows you to make requests to the API on behalf of a user. +You can pass the token either as GET parameter: + ``` GET https://gitlab.example.com/api/v4/user?access_token=OAUTH-TOKEN ``` @@ -159,5 +207,3 @@ or you can put the token to the Authorization header: curl --header "Authorization: Bearer OAUTH-TOKEN" https://gitlab.example.com/api/v4/user ``` -[personal access tokens]: ../user/profile/personal_access_tokens.md -[CSRF attacks]: http://www.oauthsecurity.com/#user-content-authorization-code-flow diff --git a/doc/api/projects.md b/doc/api/projects.md index 538cd34de43..1296b435792 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -525,11 +525,13 @@ GET /projects/:id { "group_id": 4, "group_name": "Twitter", + "group_full_path": "twitter", "group_access_level": 30 }, { "group_id": 3, "group_name": "Gitlab Org", + "group_full_path": "gitlab-org", "group_access_level": 10 } ], diff --git a/doc/api/tags.md b/doc/api/tags.md index fc86aaa6757..23dbf2d9ff7 100644 --- a/doc/api/tags.md +++ b/doc/api/tags.md @@ -17,6 +17,9 @@ Parameters: | `id` | integer/string| yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user| | `order_by` | string | no | Return tags ordered by `name` or `updated` fields. Default is `updated` | | `sort` | string | no | Return tags sorted in `asc` or `desc` order. Default is `desc` | +| `search` | string | no | Return list of tags matching the search criteria | + +> Support for `search` was [introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/54401) in GitLab 11.8. ```json [ diff --git a/doc/ci/docker/using_docker_build.md b/doc/ci/docker/using_docker_build.md index fef367051bf..cd1a6695fbd 100644 --- a/doc/ci/docker/using_docker_build.md +++ b/doc/ci/docker/using_docker_build.md @@ -521,11 +521,11 @@ stages: variables: DOCKER_HOST: tcp://docker:2375 DOCKER_DRIVER: overlay2 - CONTAINER_TEST_IMAGE: registry.example.com/my-group/my-project/my-image:$CI_COMMIT_REF_SLUG - CONTAINER_RELEASE_IMAGE: registry.example.com/my-group/my-project/my-image:latest + CONTAINER_TEST_IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG + CONTAINER_RELEASE_IMAGE: $CI_REGISTRY_IMAGE:latest before_script: - - docker login -u gitlab-ci-token -p $CI_JOB_TOKEN registry.example.com + - docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY build: stage: build diff --git a/doc/ci/environments.md b/doc/ci/environments.md index 010c579b83e..b9b5ceab7fb 100644 --- a/doc/ci/environments.md +++ b/doc/ci/environments.md @@ -416,81 +416,15 @@ and/or `production`) you can see this information in the merge request itself. ### Go directly from source files to public pages on the environment -> Introduced in GitLab 8.17. In GitLab 11.5 the file links -are surfaced to the merge request widget. - -You can specify a Route Map to get GitLab to show **View on ...** -buttons to go directly from a file to that file's representation on the -[deployed website via Review Apps](review_apps/index.md). - -To get this to work, you need to tell GitLab how the paths of files in your repository map to paths of pages on your website, using a Route Map. - -A Route Map is a file inside the repository at `.gitlab/route-map.yml`, which contains a YAML array that maps `source` paths (in the repository) to `public` paths (on the website). -Below is an example of a route map for [Middleman](https://middlemanapp.com) static websites -like <https://gitlab.com/gitlab-com/www-gitlab-com>: - -```yaml -# Team data -- source: 'data/team.yml' # data/team.yml - public: 'team/' # team/ - -# Blogposts -- source: /source\/posts\/([0-9]{4})-([0-9]{2})-([0-9]{2})-(.+?)\..*/ # source/posts/2017-01-30-around-the-world-in-6-releases.html.md.erb - public: '\1/\2/\3/\4/' # 2017/01/30/around-the-world-in-6-releases/ - -# HTML files -- source: /source\/(.+?\.html).*/ # source/index.html.haml - public: '\1' # index.html - -# Other files -- source: /source\/(.*)/ # source/images/blogimages/around-the-world-in-6-releases-cover.png - public: '\1' # images/blogimages/around-the-world-in-6-releases-cover.png -``` - -Mappings are defined as entries in the root YAML array, and are identified by a `-` prefix. Within an entry, we have a hash map with two keys: - -- `source` - - a string, starting and ending with `'`, for an exact match - - a regular expression, starting and ending with `/`, for a pattern match - - The regular expression needs to match the entire source path - `^` and `$` anchors are implied. - - Can include capture groups denoted by `()` that can be referred to in the `public` path. - - Slashes (`/`) can, but don't have to, be escaped as `\/`. - - Literal periods (`.`) should be escaped as `\.`. -- `public` - - a string, starting and ending with `'`. - - Can include `\N` expressions to refer to capture groups in the `source` regular expression in order of their occurrence, starting with `\1`. - -The public path for a source path is determined by finding the first `source` expression that matches it, and returning the corresponding `public` path, replacing the `\N` expressions with the values of the `()` capture groups if appropriate. - -In the example above, the fact that mappings are evaluated in order of their definition is used to ensure that `source/index.html.haml` will match `/source\/(.+?\.html).*/` instead of `/source\/(.*)/`, and will result in a public path of `index.html`, instead of `index.html.haml`. - ---- - -Once you have the route mapping set up, it will be exposed in a few places: - -- In the merge request widget. The **View app** button will take you to the - environment URL you have set up in `.gitlab-ci.yml`. The dropdown will render - the first 5 matched items from the route map, but you can filter them if more - than 5 are available. - - ![View app file list in merge request widget](img/view_on_mr_widget.png) - -- In the diff for a merge request, comparison, or commit. - - !["View on env" button in merge request diff](img/view_on_env_mr.png) - -- In the blob file view. - - !["View on env" button in file view](img/view_on_env_blob.png) | - ---- - -We now have a full development cycle, where our app is tested, built, deployed -as a Review app, deployed to a staging server once the merge request is merged, -and finally manually deployed to the production server. What we just described -is a single workflow, but imagine tens of developers working on a project -at the same time. They each push to their branches, and dynamic environments are -created all the time. In that case, we probably need to do some clean up. Read +With GitLab's [Route Maps](review_apps/index.md#route-maps) you can go directly +from source files to public pages on the environment set for Review Apps. + +From then on, you have a full development cycle, where your app is tested, built, deployed +as a Review App, deployed to a staging server once the merge request is merged, +and finally manually deployed to the production server. This is a simple workflow, +but when you have multiple developers working on a project +at the same time, each of them pushing to their own branches, dynamic environments are +created all the time. In which case, you probably want to do some clean up. Read next how environments can be stopped. ## Stopping an environment diff --git a/doc/ci/img/view_on_env_blob.png b/doc/ci/review_apps/img/view_on_env_blob.png Binary files differindex acc457fbb38..acc457fbb38 100644 --- a/doc/ci/img/view_on_env_blob.png +++ b/doc/ci/review_apps/img/view_on_env_blob.png diff --git a/doc/ci/img/view_on_env_mr.png b/doc/ci/review_apps/img/view_on_env_mr.png Binary files differindex 2c0bd25a4f2..2c0bd25a4f2 100644 --- a/doc/ci/img/view_on_env_mr.png +++ b/doc/ci/review_apps/img/view_on_env_mr.png diff --git a/doc/ci/img/view_on_mr_widget.png b/doc/ci/review_apps/img/view_on_mr_widget.png Binary files differindex efe023b07b5..efe023b07b5 100644 --- a/doc/ci/img/view_on_mr_widget.png +++ b/doc/ci/review_apps/img/view_on_mr_widget.png diff --git a/doc/ci/review_apps/index.md b/doc/ci/review_apps/index.md index 64be011008e..8b3a7b63e62 100644 --- a/doc/ci/review_apps/index.md +++ b/doc/ci/review_apps/index.md @@ -102,3 +102,88 @@ The following are example projects that use Review Apps with: - [OpenShift](https://gitlab.com/gitlab-examples/review-apps-openshift). See also the video [Demo: Cloud Native Development with GitLab](https://www.youtube.com/watch?v=jfIyQEwrocw), which includes a Review Apps example. + +## Route Maps + +> Introduced in GitLab 8.17. In GitLab 11.5 the file links +are surfaced to the merge request widget. + +Route Maps allows you to go directly from source files +to public pages on the [environment](../environments.md) defined for +Review Apps. Once set up, the review app link in the merge request +widget can take you directly to the pages changed, making it easier +and faster to preview proposed modifications. + +All you need to do is to tell GitLab how the paths of files +in your repository map to paths of pages on your website using a Route Map. +Once set, GitLab will display **View on ...** buttons, which will take you +to the pages changed directly from merge requests. + +To set up a route map, add a a file inside the repository at `.gitlab/route-map.yml`, +which contains a YAML array that maps `source` paths (in the repository) to `public` +paths (on the website). + +### Route Maps example + +Below there's an example of a route map for [Middleman](https://middlemanapp.com), +a static site generator (SSG) used to build [GitLab's website](https://about.gitlab.com), +deployed from its [project on GitLab.com](https://gitlab.com/gitlab-com/www-gitlab-com): + +```yaml +# Team data +- source: 'data/team.yml' # data/team.yml + public: 'team/' # team/ + +# Blogposts +- source: /source\/posts\/([0-9]{4})-([0-9]{2})-([0-9]{2})-(.+?)\..*/ # source/posts/2017-01-30-around-the-world-in-6-releases.html.md.erb + public: '\1/\2/\3/\4/' # 2017/01/30/around-the-world-in-6-releases/ + +# HTML files +- source: /source\/(.+?\.html).*/ # source/index.html.haml + public: '\1' # index.html + +# Other files +- source: /source\/(.*)/ # source/images/blogimages/around-the-world-in-6-releases-cover.png + public: '\1' # images/blogimages/around-the-world-in-6-releases-cover.png +``` + +Mappings are defined as entries in the root YAML array, and are identified by a `-` prefix. Within an entry, we have a hash map with two keys: + +- `source` + - a string, starting and ending with `'`, for an exact match + - a regular expression, starting and ending with `/`, for a pattern match + - The regular expression needs to match the entire source path - `^` and `$` anchors are implied. + - Can include capture groups denoted by `()` that can be referred to in the `public` path. + - Slashes (`/`) can, but don't have to, be escaped as `\/`. + - Literal periods (`.`) should be escaped as `\.`. +- `public` + - a string, starting and ending with `'`. + - Can include `\N` expressions to refer to capture groups in the `source` regular expression in order of their occurrence, starting with `\1`. + +The public path for a source path is determined by finding the first +`source` expression that matches it, and returning the corresponding +`public` path, replacing the `\N` expressions with the values of the +`()` capture groups if appropriate. + +In the example above, the fact that mappings are evaluated in order +of their definition is used to ensure that `source/index.html.haml` +will match `/source\/(.+?\.html).*/` instead of `/source\/(.*)/`, +and will result in a public path of `index.html`, instead of +`index.html.haml`. + +Once you have the route mapping set up, it will be exposed in a few places: + +- In the merge request widget. The **View app** button will take you to the + environment URL you have set up in `.gitlab-ci.yml`. The dropdown will render + the first 5 matched items from the route map, but you can filter them if more + than 5 are available. + + ![View app file list in merge request widget](img/view_on_mr_widget.png) + +- In the diff for a merge request, comparison, or commit. + + !["View on env" button in merge request diff](img/view_on_env_mr.png) + +- In the blob file view. + + !["View on env" button in file view](img/view_on_env_blob.png) diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index d4f0da52e53..fb69d888b94 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -78,118 +78,6 @@ A job is defined by a list of parameters that define the job behavior. | [retry](#retry) | no | Define when and how many times a job can be auto-retried in case of a failure | | [parallel](#parallel) | no | Defines how many instances of a job should be run in parallel | -### `extends` - -> Introduced in GitLab 11.3. - -`extends` defines an entry name that a job that uses `extends` is going to -inherit from. - -It is an alternative to using [YAML anchors](#anchors) and is a little -more flexible and readable: - -```yaml -.tests: - script: rake test - stage: test - only: - refs: - - branches - -rspec: - extends: .tests - script: rake rspec - only: - variables: - - $RSPEC -``` - -In the example above, the `rspec` job inherits from the `.tests` template job. -GitLab will perform a reverse deep merge based on the keys. GitLab will: - -- Merge the `rspec` contents into `.tests` recursively. -- Not merge the values of the keys. - -This results in the following `rspec` job: - -```yaml -rspec: - script: rake rspec - stage: test - only: - refs: - - branches - variables: - - $RSPEC -``` - -NOTE: **Note:** -Note that `script: rake test` has been overwritten by `script: rake rspec`. - -If you do want to include the `rake test`, have a look at [before_script-and-after_script](#before_script-and-after_script). - -`.tests` in this example is a [hidden key](#hidden-keys-jobs), but it's -possible to inherit from regular jobs as well. - -`extends` supports multi-level inheritance, however it is not recommended to -use more than three levels. The maximum nesting level that is supported is 10. -The following example has two levels of inheritance: - -```yaml -.tests: - only: - - pushes - -.rspec: - extends: .tests - script: rake rspec - -rspec 1: - variables: - RSPEC_SUITE: '1' - extends: .rspec - -rspec 2: - variables: - RSPEC_SUITE: '2' - extends: .rspec - -spinach: - extends: .tests - script: rake spinach -``` - -`extends` works across configuration files combined with [`include`](#include). - -### `pages` - -`pages` is a special job that is used to upload static content to GitLab that -can be used to serve your website. It has a special syntax, so the two -requirements below must be met: - -1. Any static content must be placed under a `public/` directory -1. `artifacts` with a path to the `public/` directory must be defined - -The example below simply moves all files from the root of the project to the -`public/` directory. The `.public` workaround is so `cp` doesn't also copy -`public/` to itself in an infinite loop: - -```yaml -pages: - stage: deploy - script: - - mkdir .public - - cp -r * .public - - mv .public public - artifacts: - paths: - - public - only: - - master -``` - -Read more on [GitLab Pages user documentation](../../user/project/pages/index.md). - ## `image` and `services` This allows to specify a custom Docker image and a list of services that can be @@ -260,7 +148,7 @@ There are also two edge cases worth mentioning: 1. If no `stages` are defined in `.gitlab-ci.yml`, then the `build`, `test` and `deploy` are allowed to be used as job's stage by default. -2. If a job doesn't specify a `stage`, the job is assigned the `test` stage. +1. If a job doesn't specify a `stage`, the job is assigned the `test` stage. ## `stage` @@ -328,7 +216,7 @@ a "key: value" pair. Be careful when using special characters: jobs are created: 1. `only` defines the names of branches and tags for which the job will run. -2. `except` defines the names of branches and tags for which the job will +1. `except` defines the names of branches and tags for which the job will **not** run. There are a few rules that apply to the usage of job policy: @@ -677,9 +565,9 @@ cleanup_job: The above script will: 1. Execute `cleanup_build_job` only when `build_job` fails. -2. Always execute `cleanup_job` as the last step in pipeline regardless of +1. Always execute `cleanup_job` as the last step in pipeline regardless of success or failure. -3. Allow you to manually execute `deploy_job` from GitLab's UI. +1. Allow you to manually execute `deploy_job` from GitLab's UI. ### `when:manual` @@ -1622,7 +1510,6 @@ Possible values for `when` are: - `missing_dependency_failure`: Retry if a dependency was missing. - `runner_unsupported`: Retry if the runner was unsupported. - ## `parallel` > [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/22631) in GitLab 11.5. @@ -1645,193 +1532,213 @@ test: ## `include` -> Introduced in [GitLab Premium](https://about.gitlab.com/pricing/) 10.5. -> Available for Starter, Premium and Ultimate since 10.6. -> Behaviour expanded in GitLab 10.8 to allow more flexible overriding. -> [Moved](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/21603) -to GitLab Core in 11.4 -> In GitLab 11.7, support for [including GitLab-supplied templates directly](https://gitlab.com/gitlab-org/gitlab-ce/issues/53445) and support for [including templates from another repository](https://gitlab.com/gitlab-org/gitlab-ce/issues/53903) was added. +> - Introduced in [GitLab Premium](https://about.gitlab.com/pricing/) 10.5. +> - Available for Starter, Premium and Ultimate since 10.6. +> - [Moved](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/21603) to GitLab Core in 11.4. Using the `include` keyword, you can allow the inclusion of external YAML files. +`include` requires the external YAML file to have the extensions `.yml` or `.yaml`, +otherwise the external file will not be included. -In the following example, the content of `.before-script-template.yml` will be -automatically fetched and evaluated along with the content of `.gitlab-ci.yml`: +The files defined in `include` are: -```yaml -# Content of https://gitlab.com/awesome-project/raw/master/.before-script-template.yml +- Deep merged with those in `.gitlab-ci.yml`. +- Always evaluated first and merged with the content of `.gitlab-ci.yml`, + regardless of the position of the `include` keyword. -before_script: - - apt-get update -qq && apt-get install -y -qq sqlite3 libsqlite3-dev nodejs - - gem install bundler --no-document - - bundle install --jobs $(nproc) "${FLAGS[@]}" -``` +TIP: **Tip:** +Use merging to customize and override included CI/CD configurations with local +definitions. -```yaml -# Content of .gitlab-ci.yml +Recursive includes are not supported. Your external files should not use the +`include` keyword as it will be ignored. -include: 'https://gitlab.com/awesome-project/raw/master/.before-script-template.yml' +NOTE: **Note:** +Using YAML aliases across different YAML files sourced by `include` is not +supported. You must only refer to aliases in the same file. Instead +of using YAML anchors, you can use the [`extends` keyword](#extends). -rspec: - script: - - bundle exec rspec -``` +`include` supports four include methods: -NOTE: **Note:** -`include` requires the external YAML files to have the extensions `.yml` or `.yaml`. -The external file will not be included if the extension is missing. +- [`local`](#includelocal) +- [`file`](#includefile) +- [`template`](#includetemplate) +- [`remote`](#includeremote) -You can include your extra YAML file either as a single string or -as an array of multiple values. You can also use full paths or -relative URLs. The following examples are both valid: +See [usage examples](#include-examples). -```yaml -# Single string +### `include:local` -include: '/templates/.after-script-template.yml' -``` +`include:local` includes a file from the same repository as `.gitlab-ci.yml`. +It's referenced using full paths relative to the root directory (`/`). -```yaml -# Single string +You can only use files that are currently tracked by Git on the same branch +your configuration file is on. In other words, when using a `include:local`, make +sure that both `.gitlab-ci.yml` and the local file are on the same branch. +NOTE: **Note:** +Including local files through Git submodules paths is not supported. + +Example: + +```yaml include: - file: '/templates/.after-script-template.yml' + - local: '/templates/.gitlab-ci-template.yml' ``` -```yaml -# Array +### `include:file` +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/53903) in GitLab 11.7. + +To include files from another private project under the same GitLab instance, +use `include:file`. This file is referenced using full paths relative to the +root directory (`/`). For example: + +```yaml include: - - 'https://gitlab.com/awesome-project/raw/master/.before-script-template.yml' - - '/templates/.after-script-template.yml' + - project: 'my-group/my-project' + file: '/templates/.gitlab-ci-template.yml' ``` -```yaml -# Array mixed syntax +You can also specify `ref`, with the default being the `HEAD` of the project: +```yaml include: - - 'https://gitlab.com/awesome-project/raw/master/.before-script-template.yml' - - '/templates/.after-script-template.yml' - - template: Auto-DevOps.gitlab-ci.yml + - project: 'my-group/my-project' + ref: master + file: '/templates/.gitlab-ci-template.yml' + + - project: 'my-group/my-project' + ref: v1.0.0 + file: '/templates/.gitlab-ci-template.yml' + + - project: 'my-group/my-project' + ref: 787123b47f14b552955ca2786bc9542ae66fee5b # Git SHA + file: '/templates/.gitlab-ci-template.yml' ``` -```yaml -# Array +### `include:template` + +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/53445) in GitLab 11.7. + +`include:template` can be used to include `.gitlab-ci.yml` templates that are +[shipped with GitLab](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/lib/gitlab/ci/templates). +For example: + +```yaml +# File sourced from GitLab's template collection include: - - remote: 'https://gitlab.com/awesome-project/raw/master/.before-script-template.yml' - - local: '/templates/.after-script-template.yml' - template: Auto-DevOps.gitlab-ci.yml ``` ---- +### `include:remote` -`include` supports four types of files: +`include:remote` can be used to include a file from a different location, +using HTTP/HTTPS, referenced by using the full URL. The remote file must be +publicly accessible through a simple GET request as authentication schemas +in the remote URL is not supported. For example: -- **local** to the same repository, referenced by using full paths in the same - repository, with `/` being the root directory. For example: +```yaml +include: + - remote: 'https://gitlab.com/awesome-project/raw/master/.gitlab-ci-template.yml' +``` - ```yaml - # Within the repository - include: '/templates/.gitlab-ci-template.yml' - ``` +NOTE: **Note for GitLab admins:** +In order to include files from another repository inside your local network, +you may need to enable the **Allow requests to the local network from hooks and services** checkbox +located in the **Admin area > Settings > Network > Outbound requests** section. - Or using: +### `include` examples - ```yaml - # Within the repository - include: - local: '/templates/.gitlab-ci-template.yml' - ``` +Here are a few more `include` examples. - NOTE: **Note:** - You can only use files that are currently tracked by Git on the same branch - your configuration file is. In other words, when using a **local file**, make - sure that both `.gitlab-ci.yml` and the local file are on the same branch. +#### Single string or array of multiple values - NOTE: **Note:** - We don't support the inclusion of local files through Git submodules paths. +You can include your extra YAML file(s) either as a single string or +an array of multiple values. The following examples are all valid. -- **file** from another repository, referenced by using full paths in the same - repository, with `/` being the root directory. For example: +Single string with the `include:local` method implied: - ```yaml - include: - project: 'my-group/my-project' - file: '/templates/.gitlab-ci-template.yml' - ``` +```yaml +include: '/templates/.after-script-template.yml' +``` - You can also specify `ref:`. The default `ref:` is the `HEAD` of the project: +Array with `include` method implied: - ```yaml - include: - - project: 'my-group/my-project' - ref: master - file: '/templates/.gitlab-ci-template.yml' +```yaml +include: + - 'https://gitlab.com/awesome-project/raw/master/.before-script-template.yml' + - '/templates/.after-script-template.yml' +``` - - project: 'my-group/my-project' - ref: v1.0.0 - file: '/templates/.gitlab-ci-template.yml' +Single string with `include` method specified explicitly: - - project: 'my-group/my-project' - ref: 787123b47f14b552955ca2786bc9542ae66fee5b # git sha - file: '/templates/.gitlab-ci-template.yml' - ``` +```yaml +include: + remote: 'https://gitlab.com/awesome-project/raw/master/.before-script-template.yml' +``` -- **remote** in a different location, accessed using HTTP/HTTPS, referenced - using the full URL. For example: +Array with `include:remote` being the single item: - ```yaml - # File sourced from outside repository - include: 'https://gitlab.com/awesome-project/raw/master/.gitlab-ci-template.yml' - ``` +```yaml +include: + - remote: 'https://gitlab.com/awesome-project/raw/master/.before-script-template.yml' +``` - Or using: +Array with multiple `include` methods specified explicitly: - ```yaml - # File sourced from outside repository - include: - remote: 'https://gitlab.com/awesome-project/raw/master/.gitlab-ci-template.yml' - ``` +```yaml +include: + - remote: 'https://gitlab.com/awesome-project/raw/master/.before-script-template.yml' + - local: '/templates/.after-script-template.yml' + - template: Auto-DevOps.gitlab-ci.yml +``` - NOTE: **Note:** - The remote file must be publicly accessible through a simple GET request, as we don't support authentication schemas in the remote URL. +Array mixed syntax: + +```yaml +include: + - 'https://gitlab.com/awesome-project/raw/master/.before-script-template.yml' + - '/templates/.after-script-template.yml' + - template: Auto-DevOps.gitlab-ci.yml + - project: 'my-group/my-project' + ref: master + file: '/templates/.gitlab-ci-template.yml' +``` - NOTE: **Note:** - In order to include files from another repository inside your local network, - you may need to enable the **Allow requests to the local network from hooks and services** checkbox - located in the **Settings > Network > Outbound requests** section within the **Admin area**. +#### Re-using a `before_script` template -- **template** included with GitLab. For example: +In the following example, the content of `.before-script-template.yml` will be +automatically fetched and evaluated along with the content of `.gitlab-ci.yml`. - ```yaml - # File sourced from GitLab's template collection - include: - template: Auto-DevOps.gitlab-ci.yml - ``` +Content of `https://gitlab.com/awesome-project/raw/master/.before-script-template.yml`: - NOTE: **Note:** - Templates included this way are sourced from [lib/gitlab/ci/templates](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/lib/gitlab/ci/templates). +```yaml +before_script: + - apt-get update -qq && apt-get install -y -qq sqlite3 libsqlite3-dev nodejs + - gem install bundler --no-document + - bundle install --jobs $(nproc) "${FLAGS[@]}" +``` ---- +Content of `.gitlab-ci.yml`: +```yaml +include: 'https://gitlab.com/awesome-project/raw/master/.before-script-template.yml' -Since GitLab 10.8 we are now deep merging the files defined in `include` -with those in `.gitlab-ci.yml`. Files defined by `include` are always -evaluated first and merged with the content of `.gitlab-ci.yml`, no -matter the position of the `include` keyword. You can take advantage of -merging to customize and override details in included CI -configurations with local definitions. +rspec: + script: + - bundle exec rspec +``` -NOTE: **Note:** -The recursive includes are not supported, meaning your external files -should not use the `include` keyword, as it will be ignored. +#### Overriding external template values The following example shows specific YAML-defined variables and details of the `production` job from an include file being customized in `.gitlab-ci.yml`. -```yaml -# Content of https://company.com/autodevops-template.yml +Content of `https://company.com/autodevops-template.yml`: +```yaml variables: POSTGRES_USER: user POSTGRES_PASSWORD: testing_password @@ -1849,9 +1756,9 @@ production: - master ``` -```yaml -# Content of .gitlab-ci.yml +Content of `.gitlab-ci.yml`: +```yaml include: 'https://company.com/autodevops-template.yml' image: alpine:latest @@ -1878,11 +1785,11 @@ with the environment url of the `production` job defined in The merging lets you extend and override dictionary mappings, but you cannot add or modify items to an included array. For example, to add an additional item to the production job script, you must repeat the -existing script items. +existing script items: -```yaml -# Content of https://company.com/autodevops-template.yml +Content of `https://company.com/autodevops-template.yml`: +```yaml production: stage: production script: @@ -1890,9 +1797,9 @@ production: - deploy ``` -```yaml -# Content of .gitlab-ci.yml +Content of `.gitlab-ci.yml`: +```yaml include: 'https://company.com/autodevops-template.yml' stages: @@ -1909,10 +1816,140 @@ In this case, if `install_dependencies` and `deploy` were not repeated in `.gitlab-ci.yml`, they would not be part of the script for the `production` job in the combined CI configuration. +## `extends` + +> Introduced in GitLab 11.3. + +`extends` defines an entry name that a job that uses `extends` is going to +inherit from. + +It is an alternative to using [YAML anchors](#anchors) and is a little +more flexible and readable: + +```yaml +.tests: + script: rake test + stage: test + only: + refs: + - branches + +rspec: + extends: .tests + script: rake rspec + only: + variables: + - $RSPEC +``` + +In the example above, the `rspec` job inherits from the `.tests` template job. +GitLab will perform a reverse deep merge based on the keys. GitLab will: + +- Merge the `rspec` contents into `.tests` recursively. +- Not merge the values of the keys. + +This results in the following `rspec` job: + +```yaml +rspec: + script: rake rspec + stage: test + only: + refs: + - branches + variables: + - $RSPEC +``` + NOTE: **Note:** -We currently do not support using YAML aliases across different YAML files -sourced by `include`. You must only refer to aliases in the same file. Instead -of using YAML anchors you can use [`extends` keyword](#extends). +Note that `script: rake test` has been overwritten by `script: rake rspec`. + +If you do want to include the `rake test`, see [`before_script` and `after_script`](#before_script-and-after_script). + +`.tests` in this example is a [hidden key](#hidden-keys-jobs), but it's +possible to inherit from regular jobs as well. + +`extends` supports multi-level inheritance, however it is not recommended to +use more than three levels. The maximum nesting level that is supported is 10. +The following example has two levels of inheritance: + +```yaml +.tests: + only: + - pushes + +.rspec: + extends: .tests + script: rake rspec + +rspec 1: + variables: + RSPEC_SUITE: '1' + extends: .rspec + +rspec 2: + variables: + RSPEC_SUITE: '2' + extends: .rspec + +spinach: + extends: .tests + script: rake spinach +``` + +## Using `extends` and `include` together + +`extends` works across configuration files combined with `include`. + +For example, if you have a local `included.yml` file: + +```yaml +.template: + script: + - echo Hello! +``` + +Then, in `.gitlab-ci.yml` you can use it like this: + +```yaml +include: included.yml + +useTemplate: + image: alpine + extends: .template +``` + +This will run a job called `useTemplate` that runs `echo Hello!` as defined in +the `.template` job, and uses the `alpine` Docker image as defined in the local job. + +## `pages` + +`pages` is a special job that is used to upload static content to GitLab that +can be used to serve your website. It has a special syntax, so the two +requirements below must be met: + +- Any static content must be placed under a `public/` directory. +- `artifacts` with a path to the `public/` directory must be defined. + +The example below simply moves all files from the root of the project to the +`public/` directory. The `.public` workaround is so `cp` doesn't also copy +`public/` to itself in an infinite loop: + +```yaml +pages: + stage: deploy + script: + - mkdir .public + - cp -r * .public + - mv .public public + artifacts: + paths: + - public + only: + - master +``` + +Read more on [GitLab Pages user documentation](../../user/project/pages/index.md). ## `variables` @@ -1951,9 +1988,9 @@ which can be set in GitLab's UI. ### Git strategy -> Introduced in GitLab 8.9 as an experimental feature. May change or be removed - completely in future releases. `GIT_STRATEGY=none` requires GitLab Runner - v1.7+. +> Introduced in GitLab 8.9 as an experimental feature. May change or be removed +> completely in future releases. `GIT_STRATEGY=none` requires GitLab Runner +> v1.7+. You can set the `GIT_STRATEGY` used for getting recent application code, either globally or per-job in the [`variables`](#variables) section. If left @@ -1989,6 +2026,11 @@ variables: GIT_STRATEGY: none ``` +NOTE: **Note:** `GIT_STRATEGY` is not supported for +[Kubernetes executor](https://docs.gitlab.com/runner/executors/kubernetes.html), +but may be in the future. See the [support Git strategy with Kubernetes executor feature proposal](https://gitlab.com/gitlab-org/gitlab-runner/issues/3847) +for updates. + ### Git submodule strategy > Requires GitLab Runner v1.10+. @@ -2283,8 +2325,9 @@ capitalization, the commit will be created but the pipeline will be skipped. Alternatively, one can pass the `ci.skip` [Git push option][push-option] if using Git 2.10 or newer: -``` -$ git push -o ci.skip + +```sh +git push -o ci.skip ``` ## Validate the .gitlab-ci.yml diff --git a/doc/development/README.md b/doc/development/README.md index f22dde32de9..05715274a81 100644 --- a/doc/development/README.md +++ b/doc/development/README.md @@ -47,6 +47,7 @@ description: 'Learn how to contribute to GitLab.' - [Avoid modules with instance variables](module_with_instance_variables.md) if possible - [How to dump production data to staging](db_dump.md) - [Working with the GitHub importer](github_importer.md) +- [Import/Export development documentation](import_export.md) - [Working with Merge Request diffs](diffs.md) - [Permissions](permissions.md) - [Prometheus metrics](prometheus_metrics.md) diff --git a/doc/development/automatic_ce_ee_merge.md b/doc/development/automatic_ce_ee_merge.md index 0cc083cefc0..fed772b9240 100644 --- a/doc/development/automatic_ce_ee_merge.md +++ b/doc/development/automatic_ce_ee_merge.md @@ -148,7 +148,7 @@ merge commit SHA is `138f5e2f20289bb376caffa0303adb0cac859ce1`: - To cherry-pick multiple commits, such as B and D in a range [A > B > C > D], use: ```shell - git cherry-pick commmit-B-SHA commit-D-SHA + git cherry-pick commit-B-SHA commit-D-SHA ``` For example, suppose commit B SHA = `4f5e4018c09ed797fdf446b3752f82e46f5af502`, @@ -213,7 +213,7 @@ being able to deploy. No, not if there is an EE merge request for every CE merge request that causes conflicts _and_ that EE merge request is merged first. In the past we may have been a bit more relaxed when it comes to enforcing EE merge requests, but to -enable automatic merging have to start requiring such merge requests even for +enable automatic merging we have to start requiring such merge requests even for the smallest conflicts. ### Some files I work with often conflict, how can I best deal with this? diff --git a/doc/development/contributing/issue_workflow.md b/doc/development/contributing/issue_workflow.md index 7c7da50a149..24feb1378a1 100644 --- a/doc/development/contributing/issue_workflow.md +++ b/doc/development/contributing/issue_workflow.md @@ -6,7 +6,7 @@ scheduling into milestones. Labelling is a task for everyone. Most issues will have labels for at least one of the following: -- Type: ~"feature proposal", ~bug, ~customer, etc. +- Type: ~feature, ~bug, ~customer, etc. - Subject: ~wiki, ~"container registry", ~ldap, ~api, ~frontend, etc. - Team: ~Plan, ~Manage, ~Quality, etc. - Stage: ~"devops:plan", ~"devops:create", etc. @@ -27,8 +27,8 @@ labels, you can _always_ add the team and type, and often also the subject. Type labels are very important. They define what kind of issue this is. Every issue should have one or more. -Examples of type labels are ~"feature proposal", ~bug, ~customer, ~security, -and ~"direction". +Examples of type labels are ~feature, ~bug, ~customer, ~security, +and ~direction. A number of type labels have a priority assigned to them, which automatically makes them float to the top, depending on their importance. @@ -67,7 +67,7 @@ The current team labels are: - ~Geo - ~Gitaly - ~Manage -- ~Monitoring +- ~Monitor - ~Plan - ~Quality - ~Release @@ -200,7 +200,7 @@ We add the ~"Accepting merge requests" label to: - Low priority ~bug issues (i.e. we do not add it to the bugs that we want to solve in the ~"Next Patch Release") -- Small ~"feature proposal" +- Small ~feature - Small ~"technical debt" issues After adding the ~"Accepting merge requests" label, we try to estimate the @@ -259,10 +259,10 @@ For feature proposals for EE, open an issue on the [issue tracker of EE][ee-tracker]. In order to help track the feature proposals, we have created a -[`feature proposal`][fpl] label. For the time being, users that are not members +[`feature`][fl] label. For the time being, users that are not members of the project cannot add labels. You can instead ask one of the [core team] -members to add the label ~"feature proposal" to the issue or add the following -code snippet right after your description in a new line: `~"feature proposal"`. +members to add the label ~feature to the issue or add the following +code snippet right after your description in a new line: `~feature`. Please keep feature proposals as small and simple as possible, complex ones might be edited to make them small and simple. @@ -276,7 +276,7 @@ need to ask one of the [core team] members to add the label, if you do not have If you want to create something yourself, consider opening an issue first to discuss whether it is interesting to include this in GitLab. -[fpl]: https://gitlab.com/gitlab-org/gitlab-ce/issues?label_name=feature+proposal +[fl]: https://gitlab.com/gitlab-org/gitlab-ce/issues?label_name=feature ## Issue tracker guidelines diff --git a/doc/development/documentation/styleguide.md b/doc/development/documentation/styleguide.md index 721e26f2de9..c188819560e 100644 --- a/doc/development/documentation/styleguide.md +++ b/doc/development/documentation/styleguide.md @@ -272,20 +272,26 @@ Inside the document: - For regular code blocks, always use a highlighting class corresponding to the language for better readability. Examples: - ```md - ```ruby - Ruby code - ``` - - ```js - JavaScript code - ``` - - ```md - Markdown code - ``` - ``` - + ````md + ```ruby + Ruby code + ``` + + ```js + JavaScript code + ``` + + ```md + Markdown code + ``` + + ```text + Code for which no specific highlighting class is available. + ``` + ```` + +- To display raw markdown instead of rendered markdown, use four backticks on their own lines around the + markdown to display. See [example](https://gitlab.com/gitlab-org/gitlab-ce/blob/8c1991b9bb7e3b8d606481fdea316d633cfa5eb7/doc/development/documentation/styleguide.md#L275-287). - For a complete reference on code blocks, check the [Kramdown guide](https://about.gitlab.com/handbook/product/technical-writing/markdown-guide/#code-blocks). ## Alert boxes @@ -383,7 +389,7 @@ Which renders to: > ### This is an `h3` >{:.no_toc} -## Specific sections and terms +## Terms To maintain consistency through GitLab documentation, the following guides documentation authors on agreed styles and usage of terms. @@ -412,7 +418,7 @@ The following are recommended verbs for specific uses. |:------------|:--------------------------------|:-------------------| | "go" | making a browser go to location | "navigate", "open" | -### GitLab versions and tiers +## 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 @@ -437,7 +443,7 @@ The following are recommended verbs for specific uses. > [Introduced](<link-to-issue>) in [GitLab Starter](https://about.gitlab.com/pricing/) 10.3. ``` -#### Early versions of EE +### Early versions of EE If the feature was created before GitLab 9.2 (before [different EE tiers were introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/1851)): @@ -450,7 +456,7 @@ For example: > [Introduced](<link-to-issue>) in GitLab Enterprise Edition 9.0. Available in [GitLab Premium](https://about.gitlab.com/pricing/). ``` -### Product badges +## Product badges When a feature is available in EE-only tiers, add the corresponding tier according to the feature availability: @@ -471,12 +477,16 @@ keyword "only": The tier should be ideally added to headers, so that the full badge will be displayed. However, 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 that will show the tiers on hover. -E.g., `**[STARTER]**` renders **[STARTER]**, `**[STARTER ONLY]**` renders **[STARTER ONLY]**. + +For example: + +- `**[STARTER]**` renders as **[STARTER]** +- `**[STARTER ONLY]**` renders as **[STARTER ONLY]** The absence of tiers' mentions mean that the feature is available in GitLab Core, GitLab.com Free, and all higher tiers. -#### How it works +### 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 diff --git a/doc/development/fe_guide/index.md b/doc/development/fe_guide/index.md index ad87ecf1b87..3a3cb77f592 100644 --- a/doc/development/fe_guide/index.md +++ b/doc/development/fe_guide/index.md @@ -21,8 +21,8 @@ We also use [Axios][axios] to handle all of our network requests. We also utilize [webpack][webpack] to handle the bundling, minification, and compression of our assets. -Working with our frontend assets requires Node (v6.0 or greater) and Yarn -(v1.2 or greater). You can find information on how to install these on our +Working with our frontend assets requires Node (v8.10.0 or greater) and Yarn +(v1.10.0 or greater). You can find information on how to install these on our [installation guide][install]. ### Browser Support diff --git a/doc/development/fe_guide/vuex.md b/doc/development/fe_guide/vuex.md index 65963b959f7..4b60ec80cb8 100644 --- a/doc/development/fe_guide/vuex.md +++ b/doc/development/fe_guide/vuex.md @@ -120,8 +120,8 @@ create: 1. An action `receiveSomethingError`, to handle the error callback 1. An action `fetchSomething` to make the request. 1. In case your application does more than a `GET` request you can use these as examples: - - `PUT`: `createSomething` - - `POST`: `updateSomething` + - `POST`: `createSomething` + - `PUT`: `updateSomething` - `DELETE`: `deleteSomething` The component MUST only dispatch the `fetchNamespace` action. Actions namespaced with `request` or `receive` should not be called from the component diff --git a/doc/development/import_export.md b/doc/development/import_export.md new file mode 100644 index 00000000000..71db1abb201 --- /dev/null +++ b/doc/development/import_export.md @@ -0,0 +1,352 @@ +# Import/Export development documentation + +Troubleshooing and general development guidelines and tips for the [Import/Export feature](../user/project/settings/import_export.md). + +<i class="fa fa-youtube-play youtube" aria-hidden="true"></i> This document is originally based on the [Import/Export 201 presentation available on YouTube](https://www.youtube.com/watch?v=V3i1OfExotE). + +## Troubleshooting commands + +Finds information about the status of the import and further logs using the JID: + +```ruby +# Rails console +Project.find_by_full_path('group/project').import_state.slice(:jid, :status, :last_error) +> {"jid"=>"414dec93f941a593ea1a6894", "status"=>"finished", "last_error"=>nil} +``` + +```bash +# Logs +grep JID /var/log/gitlab/sidekiq/current +grep "Import/Export error" /var/log/gitlab/sidekiq/current +grep "Import/Export backtrace" /var/log/gitlab/sidekiq/current +``` + +## Troubleshooting performance issues + +Read through the current performance problems using the Import/Export below. + +### OOM errors + +Out of memory (OOM) errors are normally caused by the [Sidekiq Memory Killer](https://docs.gitlab.com/ee/administration/operations/sidekiq_memory_killer.html): + +```bash +SIDEKIQ_MEMORY_KILLER_MAX_RSS = 2GB in GitLab.com +``` + +An import status `started`, and the following sidekiq logs will signal a memory issue: + +```bash +WARN: Work still in progress <struct with JID> +``` + +### Timeouts + +Timeout errors occur due to the `StuckImportJobsWorker` marking the process as failed: + +```ruby +class StuckImportJobsWorker + include ApplicationWorker + include CronjobQueue + + IMPORT_JOBS_EXPIRATION = 15.hours.to_i + + def perform + import_state_without_jid_count = mark_import_states_without_jid_as_failed! + import_state_with_jid_count = mark_import_states_with_jid_as_failed! + ... +``` + +```bash +Marked stuck import jobs as failed. JIDs: xyz +``` + +``` + +-----------+ +-----------------------------------+ + |Export Job |--->| Calls ActiveRecord `as_json` and | + +-----------+ | `to_json` on all project models | + +-----------------------------------+ + + +-----------+ +-----------------------------------+ + |Import Job |--->| Loads all JSON in memory, then | + +-----------+ | inserts into the DB in batches | + +-----------------------------------+ +``` + +### Problems and solutions + +| Problem | Possible solutions | +| -------- | -------- | +| [Slow JSON](https://gitlab.com/gitlab-org/gitlab-ce/issues/54084) loading/dumping models from the database | [split the worker](https://gitlab.com/gitlab-org/gitlab-ce/issues/54085) | +| | Batch export +| | Optimize SQL +| | Move away from `ActiveRecord` callbacks (difficult) +| High memory usage (see also some [analysis](https://gitlab.com/gitlab-org/gitlab-ce/issues/35389) | DB Commit sweet spot that uses less memory | +| | [Netflix Fast JSON API](https://github.com/Netflix/fast_jsonapi) may help | +| | Batch reading/writing to disk and any SQL + +### Temporary solutions + +While the performance problems are not tackled, there is a process to workaround +importing big projects, using a foreground import: + +[Foreground import](https://gitlab.com/gitlab-com/gl-infra/infrastructure/issues/5384) of big projects for customers. +(Using the import template in the [infrastructure tracker](https://gitlab.com/gitlab-com/gl-infra/infrastructure/)) + +## Security + +The Import/Export feature is constantly updated (adding new things to export), however +the code hasn't been refactored in a long time. We should perform a [code audit](https://gitlab.com/gitlab-org/gitlab-ce/issues/42135) +to make sure its dynamic nature does not increase the number of security concerns. + +### Security in the code + +Some of these classes provide a layer of security to the Import/Export. + +The `AttributeCleaner` removes any prohibited keys: + +```ruby +# AttributeCleaner +# Removes all `_ids` and other prohibited keys + class AttributeCleaner + ALLOWED_REFERENCES = RelationFactory::PROJECT_REFERENCES + RelationFactory::USER_REFERENCES + ['group_id'] + + def clean + @relation_hash.reject do |key, _value| + prohibited_key?(key) || !@relation_class.attribute_method?(key) || excluded_key?(key) + end.except('id') + end + + ... + +``` + +The `AttributeConfigurationSpec` checks and confirms the addition of new columns: + +```ruby +# AttributeConfigurationSpec +<<-MSG + It looks like #{relation_class}, which is exported using the project Import/Export, has new attributes: + + Please add the attribute(s) to SAFE_MODEL_ATTRIBUTES if you consider this can be exported. + Otherwise, please blacklist the attribute(s) in IMPORT_EXPORT_CONFIG by adding it to its correspondent + model in the +excluded_attributes+ section. + + SAFE_MODEL_ATTRIBUTES: #{File.expand_path(safe_attributes_file)} + IMPORT_EXPORT_CONFIG: #{Gitlab::ImportExport.config_file} +MSG +``` + +The `ModelConfigurationSpec` checks and confirms the addition of new models: + +```ruby +# ModelConfigurationSpec +<<-MSG + New model(s) <#{new_models.join(',')}> have been added, related to #{parent_model_name}, which is exported by + the Import/Export feature. + + If you think this model should be included in the export, please add it to `#{Gitlab::ImportExport.config_file}`. + + Definitely add it to `#{File.expand_path(ce_models_yml)}` + #{"or `#{File.expand_path(ee_models_yml)}` if the model/associations are EE-specific\n" if ee_models_hash.any?} + to signal that you've handled this error and to prevent it from showing up in the future. +MSG +``` + +The `ExportFileSpec` detects encrypted or sensitive columns: + +```ruby +# ExportFileSpec +<<-MSG + Found a new sensitive word <#{key_found}>, which is part of the hash #{parent.inspect} + If you think this information shouldn't get exported, please exclude the model or attribute in + IMPORT_EXPORT_CONFIG. + + Otherwise, please add the exception to +safe_list+ in CURRENT_SPEC using #{sensitive_word} as the + key and the correspondent hash or model as the value. + + Also, if the attribute is a generated unique token, please add it to RelationFactory::TOKEN_RESET_MODELS + if it needs to be reset (to prevent duplicate column problems while importing to the same instance). + + IMPORT_EXPORT_CONFIG: #{Gitlab::ImportExport.config_file} + CURRENT_SPEC: #{__FILE__} +MSG +``` + +## Versioning + +Import/Export does not use strict SemVer, since it has frequent constant changes +during a single GitLab release. It does require an update when there is a breaking change. + +```ruby +# ImportExport +module Gitlab + module ImportExport + extend self + + # For every version update, the version history in import_export.md has to be kept up to date. + VERSION = '0.2.4' +``` + +## Version history + +The [current version history](../user/project/settings/import_export.md) also displays the equivalent GitLab version +and it is useful for knowing which versions won't be compatible between them. + +| GitLab version | Import/Export version | +| ---------------- | --------------------- | +| 11.1 to current | 0.2.4 | +| 10.8 | 0.2.3 | +| 10.4 | 0.2.2 | +| ... | ... | +| 8.10.3 | 0.1.3 | +| 8.10.0 | 0.1.2 | +| 8.9.5 | 0.1.1 | +| 8.9.0 | 0.1.0 | + +### When to bump the version up + +We will have to bump the verision if we rename model/columns or perform any format +modifications in the JSON structure or the file structure of the archive file. + +We do not need to bump the version up in any of the following cases: + +- Add a new column or a model +- Remove a column or model (unless there is a DB constraint) +- Export new things (such as a new type of upload) + + +Every time we bump the version, the integration specs will fail and can be fixed with: + +```bash +bundle exec rake gitlab:import_export:bump_version +``` + +### Renaming columns or models + +This is a relatively common occurence that will require a version bump. + +There is also the _RC problem_ - GitLab.com runs an RC, prior to any customers, +meaning that we want to bump the version up in the next version (or patch release). + +For example: + +1. Add rename to `RelationRenameService` in X.Y +2. Remove it from `RelationRenameService` in X.Y + 1 +3. Bump Import/Export version in X.Y + 1 + +```ruby +module Gitlab + module ImportExport + class RelationRenameService + RENAMES = { + 'pipelines' => 'ci_pipelines' # Added in 11.6, remove in 11.7 + }.freeze +``` + +## A quick dive into the code + +### Import/Export configuration (`import_export.yml`) + +The main configuration `import_export.yml` defines what models can be exported/imported. + +Model relationships to be included in the project import/export: + +```yaml +project_tree: + - labels: + :priorities + - milestones: + - events: + - :push_event_payload + - issues: + - events: + - ... +``` + +Only include the following attributes for the models specified: + +```yaml +included_attributes: + user: + - :id + - :email + ... + +``` + +Do not include the following attributes for the models specified: + +```yaml +excluded_attributes: + project: + - :name + - :path + - ... +``` + +Extra methods to be called by the export: + +```yaml +# Methods +methods: + labels: + - :type + label: + - :type +``` + +### Import + +The import job status moves from `none` to `finished` or `failed` into different states: + +_import\_status_: none -> scheduled -> started -> finished/failed + +While the status is `started` the `Importer` code processes each step required for the import. + +```ruby +# ImportExport::Importer +module Gitlab + module ImportExport + class Importer + def execute + if import_file && check_version! && restorers.all?(&:restore) && overwrite_project + project_tree.restored_project + else + raise Projects::ImportService::Error.new(@shared.errors.join(', ')) + end + rescue => e + raise Projects::ImportService::Error.new(e.message) + ensure + remove_import_file + end + + def restorers + [repo_restorer, wiki_restorer, project_tree, avatar_restorer, + uploads_restorer, lfs_restorer, statistics_restorer] + end +``` + +The export service, is similar to the `Importer`, restoring data instead of saving it. + +### Export + +```ruby +# ImportExport::ExportService +module Projects + module ImportExport + class ExportService < BaseService + + def save_all! + if save_services + Gitlab::ImportExport::Saver.save(project: project, shared: @shared) + notify_success + else + cleanup_and_notify_error! + end + end + + def save_services + [version_saver, avatar_saver, project_tree_saver, uploads_saver, repo_saver, + wiki_repo_saver, lfs_saver].all?(&:save) + end +``` diff --git a/doc/development/sidekiq_debugging.md b/doc/development/sidekiq_debugging.md index 84b61bd7e61..2b3a9481b93 100644 --- a/doc/development/sidekiq_debugging.md +++ b/doc/development/sidekiq_debugging.md @@ -11,6 +11,11 @@ Example: gitlab_rails['env'] = {"SIDEKIQ_LOG_ARGUMENTS" => "1"} ``` -Please note: It is not recommend to enable this setting in production because some +Please note: It is not recommend to enable this setting in production because some Sidekiq jobs (such as sending a password reset email) take secret arguments (for -example the password reset token).
\ No newline at end of file +example the password reset token). + +When using [Sidekiq JSON logging](../administration/logs.md#sidekiqlog), +arguments logs are limited to a maximum size of 10 kilobytes of text; +any arguments after this limit will be discarded and replaced with a +single argument containing the string `"..."`. diff --git a/doc/development/testing_guide/flaky_tests.md b/doc/development/testing_guide/flaky_tests.md index bbb2313ea7b..58a2e53b486 100644 --- a/doc/development/testing_guide/flaky_tests.md +++ b/doc/development/testing_guide/flaky_tests.md @@ -5,6 +5,39 @@ It's a test that sometimes fails, but if you retry it enough times, it passes, eventually. +## Quarantined tests + +When a test frequently fails in `master`, +[a ~"broken master" issue](https://about.gitlab.com/handbook/engineering/workflow/#broken-master) +should be created. +If the test cannot be fixed in a timely fashion, there is an impact on the +productivity of all the developers, so it should be placed in quarantine by +assigning the `:quarantine` metadata. + +This means it will be skipped unless run with `--tag quarantine`: + +```shell +bin/rspec --tag quarantine +``` + +**Before putting a test in quarantine, you should make sure that a +~"broken master" issue exists for it so it won't stay in quarantine forever.** + +Once a test is in quarantine, there are 3 choices: + +- Should the test be fixed (i.e. get rid of its flakiness)? +- Should the test be moved to a lower level of testing? +- Should the test be removed entirely (e.g. because there's already a + lower-level test, or it's duplicating another same-level test, or it's testing + too much etc.)? + +### Quarantine tests on the CI + +Quarantined tests are run on the CI in dedicated jobs that are allowed to fail: + +- `rspec-pg-quarantine` and `rspec-mysql-quarantine` (CE & EE) +- `rspec-pg-quarantine-ee` and `rspec-mysql-quarantine-ee` (EE only) + ## Automatic retries and flaky tests detection On our CI, we use [rspec-retry] to automatically retry a failing example a few diff --git a/doc/install/installation.md b/doc/install/installation.md index 5653c59f576..b3ad1c5a91c 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -50,62 +50,85 @@ The GitLab installation consists of setting up the following components: `sudo` is not installed on Debian by default. Make sure your system is up-to-date and install it. - # run as root! - apt-get update -y - apt-get upgrade -y - apt-get install sudo -y +```sh +# run as root! +apt-get update -y +apt-get upgrade -y +apt-get install sudo -y +``` **Note:** During this installation some files will need to be edited manually. If you are familiar with vim set it as default editor with the commands below. If you are not familiar with vim please skip this and keep using the default editor. - # Install vim and set as default editor - sudo apt-get install -y vim - sudo update-alternatives --set editor /usr/bin/vim.basic +```sh +# Install vim and set as default editor +sudo apt-get install -y vim +sudo update-alternatives --set editor /usr/bin/vim.basic +``` Install the required packages (needed to compile Ruby and native extensions to Ruby gems): - sudo apt-get install -y build-essential zlib1g-dev libyaml-dev libssl-dev libgdbm-dev libre2-dev libreadline-dev libncurses5-dev libffi-dev curl openssh-server checkinstall libxml2-dev libxslt-dev libcurl4-openssl-dev libicu-dev logrotate rsync python-docutils pkg-config cmake +```sh +sudo apt-get install -y build-essential zlib1g-dev libyaml-dev libssl-dev libgdbm-dev libre2-dev \ + libreadline-dev libncurses5-dev libffi-dev curl openssh-server checkinstall libxml2-dev \ + libxslt-dev libcurl4-openssl-dev libicu-dev logrotate rsync python-docutils pkg-config cmake +``` Ubuntu 14.04 (Trusty Tahr) doesn't have the `libre2-dev` package available, but you can [install re2 manually](https://github.com/google/re2/wiki/Install). If you want to use Kerberos for user authentication, then install libkrb5-dev: - sudo apt-get install libkrb5-dev +```sh +sudo apt-get install libkrb5-dev +``` **Note:** If you don't know what Kerberos is, you can assume you don't need it. Make sure you have the right version of Git installed - # Install Git - sudo apt-get install -y git-core +```sh +# Install Git +sudo apt-get install -y git-core - # Make sure Git is version 2.18.0 or higher - git --version +# Make sure Git is version 2.18.0 or higher +git --version +``` Is the system packaged Git too old? Remove it and compile from source. - # Remove packaged Git - sudo apt-get remove git-core +```sh +# Remove packaged Git +sudo apt-get remove git-core + +# Install dependencies +sudo apt-get install -y libcurl4-openssl-dev libexpat1-dev gettext libz-dev libssl-dev build-essential + +# Download and compile from source +cd /tmp +curl --remote-name --progress https://www.kernel.org/pub/software/scm/git/git-2.18.0.tar.gz +echo '94faf2c0b02a7920b0b46f4961d8e9cad08e81418614102898a55f980fa3e7e4 git-2.18.0.tar.gz' | shasum -a256 -c - && tar -xzf git-2.18.0.tar.gz +cd git-2.18.0/ +./configure +make prefix=/usr/local all - # Install dependencies - sudo apt-get install -y libcurl4-openssl-dev libexpat1-dev gettext libz-dev libssl-dev build-essential +# Install into /usr/local/bin +sudo make prefix=/usr/local install - # Download and compile from source - cd /tmp - curl --remote-name --progress https://www.kernel.org/pub/software/scm/git/git-2.18.0.tar.gz - echo '94faf2c0b02a7920b0b46f4961d8e9cad08e81418614102898a55f980fa3e7e4 git-2.18.0.tar.gz' | shasum -a256 -c - && tar -xzf git-2.18.0.tar.gz - cd git-2.18.0/ - ./configure - make prefix=/usr/local all +# When editing config/gitlab.yml (Step 5), change the git -> bin_path to /usr/local/bin/git +``` - # Install into /usr/local/bin - sudo make prefix=/usr/local install +For the [Custom Favicon](../customization/favicon.md) to work, graphicsmagick +needs to be installed. - # When editing config/gitlab.yml (Step 5), change the git -> bin_path to /usr/local/bin/git +```sh +sudo apt-get install -y graphicsmagick +``` **Note:** In order to receive mail notifications, make sure to install a mail server. By default, Debian is shipped with exim4 but this [has problems](https://gitlab.com/gitlab-org/gitlab-ce/issues/12754) while Ubuntu does not ship with one. The recommended mail server is postfix and you can install it with: - sudo apt-get install -y postfix +```sh +sudo apt-get install -y postfix +``` Then select 'Internet Site' and press enter to confirm the hostname. @@ -127,22 +150,28 @@ instructions are designed to install Ruby from the official source code. Remove the old Ruby 1.8 if present: - sudo apt-get remove ruby1.8 +```sh +sudo apt-get remove ruby1.8 +``` Download Ruby and compile it: - mkdir /tmp/ruby && cd /tmp/ruby - curl --remote-name --progress https://cache.ruby-lang.org/pub/ruby/2.5/ruby-2.5.3.tar.gz - echo 'f919a9fbcdb7abecd887157b49833663c5c15fda ruby-2.5.3.tar.gz' | shasum -c - && tar xzf ruby-2.5.3.tar.gz - cd ruby-2.5.3 - - ./configure --disable-install-rdoc - make - sudo make install +```sh +mkdir /tmp/ruby && cd /tmp/ruby +curl --remote-name --progress https://cache.ruby-lang.org/pub/ruby/2.5/ruby-2.5.3.tar.gz +echo 'f919a9fbcdb7abecd887157b49833663c5c15fda ruby-2.5.3.tar.gz' | shasum -c - && tar xzf ruby-2.5.3.tar.gz +cd ruby-2.5.3 + +./configure --disable-install-rdoc +make +sudo make install +``` Then install the Bundler Gem: - sudo gem install bundler --no-document +```sh +sudo gem install bundler --no-document --version '< 2' +``` ## 3. Go @@ -151,31 +180,35 @@ GitLab we need a Go compiler. The instructions below assume you use 64-bit Linux. You can find downloads for other platforms at the [Go download page](https://golang.org/dl). - # Remove former Go installation folder - sudo rm -rf /usr/local/go - - curl --remote-name --progress https://dl.google.com/go/go1.10.3.linux-amd64.tar.gz - echo 'fa1b0e45d3b647c252f51f5e1204aba049cde4af177ef9f2181f43004f901035 go1.10.3.linux-amd64.tar.gz' | shasum -a256 -c - && \ - sudo tar -C /usr/local -xzf go1.10.3.linux-amd64.tar.gz - sudo ln -sf /usr/local/go/bin/{go,godoc,gofmt} /usr/local/bin/ - rm go1.10.3.linux-amd64.tar.gz +```sh +# Remove former Go installation folder +sudo rm -rf /usr/local/go + +curl --remote-name --progress https://dl.google.com/go/go1.10.3.linux-amd64.tar.gz +echo 'fa1b0e45d3b647c252f51f5e1204aba049cde4af177ef9f2181f43004f901035 go1.10.3.linux-amd64.tar.gz' | shasum -a256 -c - && \ + sudo tar -C /usr/local -xzf go1.10.3.linux-amd64.tar.gz +sudo ln -sf /usr/local/go/bin/{go,godoc,gofmt} /usr/local/bin/ +rm go1.10.3.linux-amd64.tar.gz +``` ## 4. Node Since GitLab 8.17, GitLab requires the use of Node to compile javascript assets, and Yarn to manage javascript dependencies. The current minimum -requirements for these are node >= v6.0.0 and yarn >= v1.2.0. In many distros +requirements for these are node >= v8.10.0 and yarn >= v1.10.0. In many distros the versions provided by the official package repositories are out of date, so we'll need to install through the following commands: - # install node v8.x - curl --location https://deb.nodesource.com/setup_8.x | sudo bash - - sudo apt-get install -y nodejs - - 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 +```sh +# install node v8.x +curl --location https://deb.nodesource.com/setup_8.x | sudo bash - +sudo apt-get install -y nodejs + +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 +``` Visit the official websites for [node](https://nodejs.org/en/download/package-manager/) and [yarn](https://yarnpkg.com/en/docs/install/) if you have any trouble with these steps. @@ -183,7 +216,9 @@ Visit the official websites for [node](https://nodejs.org/en/download/package-ma Create a `git` user for GitLab: - sudo adduser --disabled-login --gecos 'GitLab' git +```sh +sudo adduser --disabled-login --gecos 'GitLab' git +``` ## 6. Database @@ -195,37 +230,37 @@ you need at least PostgreSQL 9.2. 1. Install the database packages: - ```bash + ```sh sudo apt-get install -y postgresql postgresql-client libpq-dev postgresql-contrib ``` 1. Create a database user for GitLab: - ```bash + ```sh sudo -u postgres psql -d template1 -c "CREATE USER git CREATEDB;" ``` 1. Create the `pg_trgm` extension (required for GitLab 8.6+): - ```bash + ```sh sudo -u postgres psql -d template1 -c "CREATE EXTENSION IF NOT EXISTS pg_trgm;" ``` 1. Create the GitLab production database and grant all privileges on database: - ```bash + ```sh sudo -u postgres psql -d template1 -c "CREATE DATABASE gitlabhq_production OWNER git;" ``` 1. Try connecting to the new database with the new user: - ```bash + ```sh sudo -u git -H psql -d gitlabhq_production ``` 1. Check if the `pg_trgm` extension is enabled: - ```bash + ```sh SELECT true AS enabled FROM pg_available_extensions WHERE name = 'pg_trgm' @@ -243,7 +278,7 @@ you need at least PostgreSQL 9.2. 1. Quit the database session: - ```bash + ```sh gitlabhq_production> \q ``` @@ -262,7 +297,7 @@ If you are using Debian 7 or Ubuntu 12.04, follow the special documentation on [an alternate Redis installation](redis.md). Once done, follow the rest of the guide here. -``` +```sh # Configure redis to use sockets sudo cp /etc/redis/redis.conf /etc/redis/redis.conf.orig @@ -294,89 +329,95 @@ sudo usermod -aG redis git ## 8. GitLab - # We'll install GitLab into home directory of the user "git" - cd /home/git +```sh +# We'll install GitLab into home directory of the user "git" +cd /home/git +``` ### Clone the Source - # Clone GitLab repository - sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 11-7-stable gitlab +```sh +# Clone GitLab repository +sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 11-7-stable gitlab +``` **Note:** You can change `11-7-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server! ### Configure It - # Go to GitLab installation folder - cd /home/git/gitlab +```sh +# Go to GitLab installation folder +cd /home/git/gitlab - # Copy the example GitLab config - sudo -u git -H cp config/gitlab.yml.example config/gitlab.yml +# Copy the example GitLab config +sudo -u git -H cp config/gitlab.yml.example config/gitlab.yml - # Update GitLab config file, follow the directions at top of file - sudo -u git -H editor config/gitlab.yml +# Update GitLab config file, follow the directions at top of file +sudo -u git -H editor config/gitlab.yml - # Copy the example secrets file - sudo -u git -H cp config/secrets.yml.example config/secrets.yml - sudo -u git -H chmod 0600 config/secrets.yml +# Copy the example secrets file +sudo -u git -H cp config/secrets.yml.example config/secrets.yml +sudo -u git -H chmod 0600 config/secrets.yml - # Make sure GitLab can write to the log/ and tmp/ directories - sudo chown -R git log/ - sudo chown -R git tmp/ - sudo chmod -R u+rwX,go-w log/ - sudo chmod -R u+rwX tmp/ +# Make sure GitLab can write to the log/ and tmp/ directories +sudo chown -R git log/ +sudo chown -R git tmp/ +sudo chmod -R u+rwX,go-w log/ +sudo chmod -R u+rwX tmp/ - # Make sure GitLab can write to the tmp/pids/ and tmp/sockets/ directories - sudo chmod -R u+rwX tmp/pids/ - sudo chmod -R u+rwX tmp/sockets/ +# Make sure GitLab can write to the tmp/pids/ and tmp/sockets/ directories +sudo chmod -R u+rwX tmp/pids/ +sudo chmod -R u+rwX tmp/sockets/ - # Create the public/uploads/ directory - sudo -u git -H mkdir public/uploads/ +# Create the public/uploads/ directory +sudo -u git -H mkdir public/uploads/ - # Make sure only the GitLab user has access to the public/uploads/ directory - # now that files in public/uploads are served by gitlab-workhorse - sudo chmod 0700 public/uploads +# Make sure only the GitLab user has access to the public/uploads/ directory +# now that files in public/uploads are served by gitlab-workhorse +sudo chmod 0700 public/uploads - # Change the permissions of the directory where CI job traces are stored - sudo chmod -R u+rwX builds/ +# Change the permissions of the directory where CI job traces are stored +sudo chmod -R u+rwX builds/ - # Change the permissions of the directory where CI artifacts are stored - sudo chmod -R u+rwX shared/artifacts/ +# Change the permissions of the directory where CI artifacts are stored +sudo chmod -R u+rwX shared/artifacts/ - # Change the permissions of the directory where GitLab Pages are stored - sudo chmod -R ug+rwX shared/pages/ +# Change the permissions of the directory where GitLab Pages are stored +sudo chmod -R ug+rwX shared/pages/ - # Copy the example Unicorn config - sudo -u git -H cp config/unicorn.rb.example config/unicorn.rb +# Copy the example Unicorn config +sudo -u git -H cp config/unicorn.rb.example config/unicorn.rb - # Find number of cores - nproc +# Find number of cores +nproc - # Enable cluster mode if you expect to have a high load instance - # Set the number of workers to at least the number of cores - # Ex. change amount of workers to 3 for 2GB RAM server - sudo -u git -H editor config/unicorn.rb +# Enable cluster mode if you expect to have a high load instance +# Set the number of workers to at least the number of cores +# Ex. change amount of workers to 3 for 2GB RAM server +sudo -u git -H editor config/unicorn.rb - # Copy the example Rack attack config - sudo -u git -H cp config/initializers/rack_attack.rb.example config/initializers/rack_attack.rb +# Copy the example Rack attack config +sudo -u git -H cp config/initializers/rack_attack.rb.example config/initializers/rack_attack.rb - # Configure Git global settings for git user - # 'autocrlf' is needed for the web editor - sudo -u git -H git config --global core.autocrlf input +# Configure Git global settings for git user +# 'autocrlf' is needed for the web editor +sudo -u git -H git config --global core.autocrlf input - # Disable 'git gc --auto' because GitLab already runs 'git gc' when needed - sudo -u git -H git config --global gc.auto 0 +# Disable 'git gc --auto' because GitLab already runs 'git gc' when needed +sudo -u git -H git config --global gc.auto 0 - # Enable packfile bitmaps - sudo -u git -H git config --global repack.writeBitmaps true +# Enable packfile bitmaps +sudo -u git -H git config --global repack.writeBitmaps true - # Enable push options - sudo -u git -H git config --global receive.advertisePushOptions true +# Enable push options +sudo -u git -H git config --global receive.advertisePushOptions true - # Configure Redis connection settings - sudo -u git -H cp config/resque.yml.example config/resque.yml +# Configure Redis connection settings +sudo -u git -H cp config/resque.yml.example config/resque.yml - # Change the Redis socket path if you are not using the default Debian / Ubuntu configuration - sudo -u git -H editor config/resque.yml +# Change the Redis socket path if you are not using the default Debian / Ubuntu configuration +sudo -u git -H editor config/resque.yml +``` **Important Note:** Make sure to edit both `gitlab.yml` and `unicorn.rb` to match your setup. @@ -384,33 +425,37 @@ sudo usermod -aG redis git ### Configure GitLab DB Settings - # PostgreSQL only: - sudo -u git cp config/database.yml.postgresql config/database.yml - - # MySQL only: - sudo -u git cp config/database.yml.mysql config/database.yml - - # MySQL and remote PostgreSQL only: - # Update username/password in config/database.yml. - # You only need to adapt the production settings (first part). - # If you followed the database guide then please do as follows: - # Change 'secure password' with the value you have given to $password - # You can keep the double quotes around the password - sudo -u git -H editor config/database.yml - - # PostgreSQL and MySQL: - # Make config/database.yml readable to git only - sudo -u git -H chmod o-rwx config/database.yml +```sh +# PostgreSQL only: +sudo -u git cp config/database.yml.postgresql config/database.yml + +# MySQL only: +sudo -u git cp config/database.yml.mysql config/database.yml + +# MySQL and remote PostgreSQL only: +# Update username/password in config/database.yml. +# You only need to adapt the production settings (first part). +# If you followed the database guide then please do as follows: +# Change 'secure password' with the value you have given to $password +# You can keep the double quotes around the password +sudo -u git -H editor config/database.yml + +# PostgreSQL and MySQL: +# Make config/database.yml readable to git only +sudo -u git -H chmod o-rwx config/database.yml +``` ### Install Gems **Note:** As of bundler 1.5.2, you can invoke `bundle install -jN` (where `N` the number of your processor cores) and enjoy the parallel gems installation with measurable difference in completion time (~60% faster). Check the number of your cores with `nproc`. For more information check this [post](https://robots.thoughtbot.com/parallel-gem-installing-using-bundler). First make sure you have bundler >= 1.5.2 (run `bundle -v`) as it addresses some [issues](https://devcenter.heroku.com/changelog-items/411) that were [fixed](https://github.com/bundler/bundler/pull/2817) in 1.5.2. - # For PostgreSQL (note, the option says "without ... mysql") - sudo -u git -H bundle install --deployment --without development test mysql aws kerberos +```sh +# For PostgreSQL (note, the option says "without ... mysql") +sudo -u git -H bundle install --deployment --without development test mysql aws kerberos - # Or if you use MySQL (note, the option says "without ... postgres") - sudo -u git -H bundle install --deployment --without development test postgres aws kerberos +# Or if you use MySQL (note, the option says "without ... postgres") +sudo -u git -H bundle install --deployment --without development test postgres aws kerberos +``` **Note:** If you want to use Kerberos for user authentication, then omit `kerberos` in the `--without` option above. @@ -418,12 +463,14 @@ sudo usermod -aG redis git GitLab Shell is an SSH access and repository management software developed specially for GitLab. - # Run the installation task for gitlab-shell (replace `REDIS_URL` if needed): - sudo -u git -H bundle exec rake gitlab:shell:install REDIS_URL=unix:/var/run/redis/redis.sock RAILS_ENV=production SKIP_STORAGE_VALIDATION=true +```sh +# Run the installation task for gitlab-shell (replace `REDIS_URL` if needed): +sudo -u git -H bundle exec rake gitlab:shell:install REDIS_URL=unix:/var/run/redis/redis.sock RAILS_ENV=production SKIP_STORAGE_VALIDATION=true - # By default, the gitlab-shell config is generated from your main GitLab config. - # You can review (and modify) the gitlab-shell config as follows: - sudo -u git -H editor /home/git/gitlab-shell/config.yml +# By default, the gitlab-shell config is generated from your main GitLab config. +# You can review (and modify) the gitlab-shell config as follows: +sudo -u git -H editor /home/git/gitlab-shell/config.yml +``` **Note:** If you want to use HTTPS, see [Using HTTPS](#using-https) for the additional steps. @@ -441,57 +488,73 @@ GitLab-Workhorse uses [GNU Make](https://www.gnu.org/software/make/). The following command-line will install GitLab-Workhorse in `/home/git/gitlab-workhorse` which is the recommended location. - sudo -u git -H bundle exec rake "gitlab:workhorse:install[/home/git/gitlab-workhorse]" RAILS_ENV=production +```sh +sudo -u git -H bundle exec rake "gitlab:workhorse:install[/home/git/gitlab-workhorse]" RAILS_ENV=production +``` You can specify a different Git repository by providing it as an extra parameter: - sudo -u git -H bundle exec rake "gitlab:workhorse:install[/home/git/gitlab-workhorse,https://example.com/gitlab-workhorse.git]" RAILS_ENV=production +```sh +sudo -u git -H bundle exec rake "gitlab:workhorse:install[/home/git/gitlab-workhorse,https://example.com/gitlab-workhorse.git]" RAILS_ENV=production +``` ### Install gitlab-pages GitLab-Pages uses [GNU Make](https://www.gnu.org/software/make/). This step is optional and only needed if you wish to host static sites from within GitLab. The following commands will install GitLab-Pages in `/home/git/gitlab-pages`. For additional setup steps, please consult the [administration guide](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/administration/pages/source.md) for your version of GitLab as the GitLab Pages daemon can be ran several different ways. - cd /home/git - sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-pages.git - cd gitlab-pages - sudo -u git -H git checkout v$(</home/git/gitlab/GITLAB_PAGES_VERSION) - sudo -u git -H make +```sh +cd /home/git +sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-pages.git +cd gitlab-pages +sudo -u git -H git checkout v$(</home/git/gitlab/GITLAB_PAGES_VERSION) +sudo -u git -H make +``` ### Install Gitaly - # Fetch Gitaly source with Git and compile with Go - sudo -u git -H bundle exec rake "gitlab:gitaly:install[/home/git/gitaly,/home/git/repositories]" RAILS_ENV=production +```sh +# Fetch Gitaly source with Git and compile with Go +sudo -u git -H bundle exec rake "gitlab:gitaly:install[/home/git/gitaly,/home/git/repositories]" RAILS_ENV=production +``` You can specify a different Git repository by providing it as an extra parameter: - sudo -u git -H bundle exec rake "gitlab:gitaly:install[/home/git/gitaly,/home/git/repositories,https://example.com/gitaly.git]" RAILS_ENV=production +```sh +sudo -u git -H bundle exec rake "gitlab:gitaly:install[/home/git/gitaly,/home/git/repositories,https://example.com/gitaly.git]" RAILS_ENV=production +``` Next, make sure gitaly configured: - # Restrict Gitaly socket access - sudo chmod 0700 /home/git/gitlab/tmp/sockets/private - sudo chown git /home/git/gitlab/tmp/sockets/private +```sh +# Restrict Gitaly socket access +sudo chmod 0700 /home/git/gitlab/tmp/sockets/private +sudo chown git /home/git/gitlab/tmp/sockets/private - # If you are using non-default settings you need to update config.toml - cd /home/git/gitaly - sudo -u git -H editor config.toml +# If you are using non-default settings you need to update config.toml +cd /home/git/gitaly +sudo -u git -H editor config.toml +``` For more information about configuring Gitaly see [doc/administration/gitaly](../administration/gitaly). ### Initialize Database and Activate Advanced Features - sudo -u git -H bundle exec rake gitlab:setup RAILS_ENV=production - # Type 'yes' to create the database tables. +```sh +sudo -u git -H bundle exec rake gitlab:setup RAILS_ENV=production +# Type 'yes' to create the database tables. - # or you can skip the question by adding force=yes - sudo -u git -H bundle exec rake gitlab:setup RAILS_ENV=production force=yes +# or you can skip the question by adding force=yes +sudo -u git -H bundle exec rake gitlab:setup RAILS_ENV=production force=yes - # When done you see 'Administrator account created:' +# When done you see 'Administrator account created:' +``` **Note:** You can set the Administrator/root password and e-mail by supplying them in environmental variables, `GITLAB_ROOT_PASSWORD` and `GITLAB_ROOT_EMAIL` respectively, as seen below. If you don't set the password (and it is set to the default one) please wait with exposing GitLab to the public internet until the installation is done and you've logged into the server the first time. During the first login you'll be forced to change the default password. - sudo -u git -H bundle exec rake gitlab:setup RAILS_ENV=production GITLAB_ROOT_PASSWORD=yourpassword GITLAB_ROOT_EMAIL=youremail +```sh +sudo -u git -H bundle exec rake gitlab:setup RAILS_ENV=production GITLAB_ROOT_PASSWORD=yourpassword GITLAB_ROOT_EMAIL=youremail +``` ### Secure secrets.yml @@ -503,43 +566,58 @@ Otherwise your secrets are exposed if one of your backups is compromised. Download the init script (will be `/etc/init.d/gitlab`): - sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab +```sh +sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab +``` And if you are installing with a non-default folder or user copy and edit the defaults file: - sudo cp lib/support/init.d/gitlab.default.example /etc/default/gitlab +```sh +sudo cp lib/support/init.d/gitlab.default.example /etc/default/gitlab +``` If you installed GitLab in another directory or as a user other than the default you should change these settings in `/etc/default/gitlab`. Do not edit `/etc/init.d/gitlab` as it will be changed on upgrade. Make GitLab start on boot: - sudo update-rc.d gitlab defaults 21 +```sh +sudo update-rc.d gitlab defaults 21 +``` ### Set up Logrotate - sudo cp lib/support/logrotate/gitlab /etc/logrotate.d/gitlab +```sh +sudo cp lib/support/logrotate/gitlab /etc/logrotate.d/gitlab +``` ### Check Application Status Check if GitLab and its environment are configured correctly: - sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production - +```sh +sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production +``` ### Compile GetText PO files - sudo -u git -H bundle exec rake gettext:compile RAILS_ENV=production +```sh +sudo -u git -H bundle exec rake gettext:compile RAILS_ENV=production +``` ### Compile Assets - sudo -u git -H yarn install --production --pure-lockfile - sudo -u git -H bundle exec rake gitlab:assets:compile RAILS_ENV=production NODE_ENV=production +```sh +sudo -u git -H yarn install --production --pure-lockfile +sudo -u git -H bundle exec rake gitlab:assets:compile RAILS_ENV=production NODE_ENV=production +``` ### Start Your GitLab Instance - sudo service gitlab start - # or - sudo /etc/init.d/gitlab restart +```sh +sudo service gitlab start +# or +sudo /etc/init.d/gitlab restart +``` ## 9. Nginx @@ -547,27 +625,33 @@ Check if GitLab and its environment are configured correctly: ### Installation - sudo apt-get install -y nginx +```sh +sudo apt-get install -y nginx +``` ### Site Configuration Copy the example site config: - sudo cp lib/support/nginx/gitlab /etc/nginx/sites-available/gitlab - sudo ln -s /etc/nginx/sites-available/gitlab /etc/nginx/sites-enabled/gitlab +```sh +sudo cp lib/support/nginx/gitlab /etc/nginx/sites-available/gitlab +sudo ln -s /etc/nginx/sites-available/gitlab /etc/nginx/sites-enabled/gitlab +``` Make sure to edit the config file to match your setup. Also, ensure that you match your paths to GitLab, especially if installing for a user other than the 'git' user: - # Change YOUR_SERVER_FQDN to the fully-qualified - # domain name of your host serving GitLab. - # - # Remember to match your paths to GitLab, especially - # if installing for a user other than 'git'. - # - # If using Ubuntu default nginx install: - # either remove the default_server from the listen line - # or else sudo rm -f /etc/nginx/sites-enabled/default - sudo editor /etc/nginx/sites-available/gitlab +```sh +# Change YOUR_SERVER_FQDN to the fully-qualified +# domain name of your host serving GitLab. +# +# Remember to match your paths to GitLab, especially +# if installing for a user other than 'git'. +# +# If using Ubuntu default nginx install: +# either remove the default_server from the listen line +# or else sudo rm -f /etc/nginx/sites-enabled/default +sudo editor /etc/nginx/sites-available/gitlab +``` If you intend to enable GitLab pages, there is a separate Nginx config you need to use. Read all about the needed configuration at the @@ -579,13 +663,17 @@ to use. Read all about the needed configuration at the Validate your `gitlab` or `gitlab-ssl` Nginx config file with the following command: - sudo nginx -t +```sh +sudo nginx -t +``` You should receive `syntax is okay` and `test is successful` messages. If you receive errors check your `gitlab` or `gitlab-ssl` Nginx config file for typos, etc. as indicated in the error message given. ### Restart - sudo service nginx restart +```sh +sudo service nginx restart +``` ## Done! @@ -593,7 +681,9 @@ You should receive `syntax is okay` and `test is successful` messages. If you re To make sure you didn't miss anything run a more thorough check with: - sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production +```sh +sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production +``` If all items are green, then congratulations on successfully installing GitLab! @@ -680,31 +770,39 @@ for the changes to take effect. If you'd like to connect to a Redis server on a non-standard port or on a different host, you can configure its connection string via the `config/resque.yml` file. - # example - production: - url: redis://redis.example.tld:6379 +``` +# example +production: + url: redis://redis.example.tld:6379 +``` If you want to connect the Redis server via socket, then use the "unix:" URL scheme and the path to the Redis socket file in the `config/resque.yml` file. - # example - production: - url: unix:/path/to/redis/socket +``` +# example +production: + url: unix:/path/to/redis/socket +``` Also you can use environment variables in the `config/resque.yml` file: - # example - production: - url: <%= ENV.fetch('GITLAB_REDIS_URL') %> +``` +# example +production: + url: <%= ENV.fetch('GITLAB_REDIS_URL') %> +``` ### Custom SSH Connection If you are running SSH on a non-standard port, you must change the GitLab user's SSH config. - # Add to /home/git/.ssh/config - host localhost # Give your setup a name (here: override localhost) - user git # Your remote git user - port 2222 # Your port number - hostname 127.0.0.1; # Your server name or IP +``` +# Add to /home/git/.ssh/config +host localhost # Give your setup a name (here: override localhost) + user git # Your remote git user + port 2222 # Your port number + hostname 127.0.0.1; # Your server name or IP +``` You also need to change the corresponding options (e.g. `ssh_user`, `ssh_host`, `admin_uri`) in the `config\gitlab.yml` file. diff --git a/doc/security/webhooks.md b/doc/security/webhooks.md index b17b0a4bc4a..fb2b6768f0a 100644 --- a/doc/security/webhooks.md +++ b/doc/security/webhooks.md @@ -12,7 +12,7 @@ If a web service does not require authentication, Webhooks can be used to trigge To prevent this type of exploitation from happening, starting with GitLab 10.6, all Webhook requests to the current GitLab instance server address and/or in a private network will be forbidden by default. That means that all requests made to 127.0.0.1, ::1 and 0.0.0.0, as well as IPv4 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 and IPv6 site-local (ffc0::/10) addresses won't be allowed. -This behavior can be overridden by enabling the option *"Allow requests to the local network from hooks and services"* in the *"Outbound requests"* section inside the Admin area under **Settings** (`/admin/application_settings`): +This behavior can be overridden by enabling the option *"Allow requests to the local network from hooks and services"* in the *"Outbound requests"* section inside the Admin area under **Settings** (`/admin/application_settings/network`): ![Outbound requests admin settings](img/outbound_requests_section.png) diff --git a/doc/topics/autodevops/index.md b/doc/topics/autodevops/index.md index 780e9b8783e..68e50a61151 100644 --- a/doc/topics/autodevops/index.md +++ b/doc/topics/autodevops/index.md @@ -683,6 +683,8 @@ also be customized, and you can easily use a [custom buildpack](#custom-buildpac | `PRODUCTION_REPLICAS` | The number of replicas to deploy in the production environment. This takes precedence over `REPLICAS`; defaults to 1. | | `CANARY_REPLICAS` | The number of canary replicas to deploy for [Canary Deployments](https://docs.gitlab.com/ee/user/project/canary_deployments.html); defaults to 1 | | `CANARY_PRODUCTION_REPLICAS` | The number of canary replicas to deploy for [Canary Deployments](https://docs.gitlab.com/ee/user/project/canary_deployments.html) in the production environment. This takes precedence over `CANARY_REPLICAS`; defaults to 1 | +| `ADDITIONAL_HOSTS` | Fully qualified domain names specified as a comma-separated list that are added to the ingress hosts. | +| `<ENVIRONMENT>_ADDITIONAL_HOSTS` | For a specific environment, the fully qualified domain names specified as a comma-separated list that are added to the ingress hosts. This takes precedence over `ADDITIONAL_HOSTS`. | | `POSTGRES_ENABLED` | Whether PostgreSQL is enabled; defaults to `"true"`. Set to `false` to disable the automatic deployment of PostgreSQL. | | `POSTGRES_USER` | The PostgreSQL user; defaults to `user`. Set it to use a custom username. | | `POSTGRES_PASSWORD` | The PostgreSQL password; defaults to `testing-password`. Set it to use a custom password. | diff --git a/doc/update/10.0-to-10.1.md b/doc/update/10.0-to-10.1.md index 10cf02a984f..d4373ca3f23 100644 --- a/doc/update/10.0-to-10.1.md +++ b/doc/update/10.0-to-10.1.md @@ -49,7 +49,7 @@ sudo make install Install Bundler: ```bash -sudo gem install bundler --no-document +sudo gem install bundler --no-document --version '< 2' ``` ### 4. Update Node diff --git a/doc/update/10.1-to-10.2.md b/doc/update/10.1-to-10.2.md index 20895a05567..0705b58ed7a 100644 --- a/doc/update/10.1-to-10.2.md +++ b/doc/update/10.1-to-10.2.md @@ -49,7 +49,7 @@ sudo make install Install Bundler: ```bash -sudo gem install bundler --no-document +sudo gem install bundler --no-document --version '< 2' ``` ### 4. Update Node diff --git a/doc/update/10.2-to-10.3.md b/doc/update/10.2-to-10.3.md index 441a241d053..33a52d3e807 100644 --- a/doc/update/10.2-to-10.3.md +++ b/doc/update/10.2-to-10.3.md @@ -49,7 +49,7 @@ sudo make install Install Bundler: ```bash -sudo gem install bundler --no-document +sudo gem install bundler --no-document --version '< 2' ``` ### 4. Update Node diff --git a/doc/update/10.3-to-10.4.md b/doc/update/10.3-to-10.4.md index 9f3efdd790e..3ba96535965 100644 --- a/doc/update/10.3-to-10.4.md +++ b/doc/update/10.3-to-10.4.md @@ -51,7 +51,7 @@ sudo make install Install Bundler: ```bash -sudo gem install bundler --no-document +sudo gem install bundler --no-document --version '< 2' ``` ### 4. Update Node diff --git a/doc/update/10.4-to-10.5.md b/doc/update/10.4-to-10.5.md index 3766645a141..f00bbcaeaa6 100644 --- a/doc/update/10.4-to-10.5.md +++ b/doc/update/10.4-to-10.5.md @@ -51,7 +51,7 @@ sudo make install Install Bundler: ```bash -sudo gem install bundler --no-document +sudo gem install bundler --no-document --version '< 2' ``` ### 4. Update Node diff --git a/doc/update/10.5-to-10.6.md b/doc/update/10.5-to-10.6.md index 986ecbf5ef0..6c3f8b663cc 100644 --- a/doc/update/10.5-to-10.6.md +++ b/doc/update/10.5-to-10.6.md @@ -51,7 +51,7 @@ sudo make install Install Bundler: ```bash -sudo gem install bundler --no-document +sudo gem install bundler --no-document --version '< 2' ``` ### 4. Update Node diff --git a/doc/update/10.6-to-10.7.md b/doc/update/10.6-to-10.7.md index 10d29837bfb..9bd354a5bcd 100644 --- a/doc/update/10.6-to-10.7.md +++ b/doc/update/10.6-to-10.7.md @@ -51,7 +51,7 @@ sudo make install Install Bundler: ```bash -sudo gem install bundler --no-document +sudo gem install bundler --no-document --version '< 2' ``` ### 4. Update Node diff --git a/doc/update/10.7-to-10.8.md b/doc/update/10.7-to-10.8.md index 0cc46fc5aa9..9aafd3f269f 100644 --- a/doc/update/10.7-to-10.8.md +++ b/doc/update/10.7-to-10.8.md @@ -52,7 +52,7 @@ sudo make install Install Bundler: ```bash -sudo gem install bundler --no-document +sudo gem install bundler --no-document --version '< 2' ``` ### 4. Update Node diff --git a/doc/update/10.8-to-11.0.md b/doc/update/10.8-to-11.0.md index ad3305d8ebd..f6fdc342e3d 100644 --- a/doc/update/10.8-to-11.0.md +++ b/doc/update/10.8-to-11.0.md @@ -51,7 +51,7 @@ sudo make install Install Bundler: ```bash -sudo gem install bundler --no-document +sudo gem install bundler --no-document --version '< 2' ``` ### 4. Update Node diff --git a/doc/update/11.0-to-11.1.md b/doc/update/11.0-to-11.1.md index 5b2dd48a744..25a7c1cf929 100644 --- a/doc/update/11.0-to-11.1.md +++ b/doc/update/11.0-to-11.1.md @@ -51,7 +51,7 @@ sudo make install Install Bundler: ```bash -sudo gem install bundler --no-document +sudo gem install bundler --no-document --version '< 2' ``` ### 4. Update Node diff --git a/doc/update/11.1-to-11.2.md b/doc/update/11.1-to-11.2.md index cb09d0a2505..ced59c245b8 100644 --- a/doc/update/11.1-to-11.2.md +++ b/doc/update/11.1-to-11.2.md @@ -51,7 +51,7 @@ sudo make install Install Bundler: ```bash -sudo gem install bundler --no-document +sudo gem install bundler --no-document --version '< 2' ``` ### 4. Update Node diff --git a/doc/update/11.2-to-11.3.md b/doc/update/11.2-to-11.3.md index 228ff6cb70e..fa0c6872182 100644 --- a/doc/update/11.2-to-11.3.md +++ b/doc/update/11.2-to-11.3.md @@ -51,7 +51,7 @@ sudo make install Install Bundler: ```bash -sudo gem install bundler --no-document +sudo gem install bundler --no-document --version '< 2' ``` ### 4. Update Node diff --git a/doc/update/11.3-to-11.4.md b/doc/update/11.3-to-11.4.md index 5f64bf81127..18bbfe4747e 100644 --- a/doc/update/11.3-to-11.4.md +++ b/doc/update/11.3-to-11.4.md @@ -51,7 +51,7 @@ sudo make install Install Bundler: ```bash -sudo gem install bundler --no-document +sudo gem install bundler --no-document --version '< 2' ``` ### 4. Update Node diff --git a/doc/update/11.4-to-11.5.md b/doc/update/11.4-to-11.5.md index fd7a8e5c2ae..6ad58ae8781 100644 --- a/doc/update/11.4-to-11.5.md +++ b/doc/update/11.4-to-11.5.md @@ -51,7 +51,7 @@ sudo make install Install Bundler: ```bash -sudo gem install bundler --no-document +sudo gem install bundler --no-document --version '< 2' ``` ### 4. Update Node diff --git a/doc/update/11.5-to-11.6.md b/doc/update/11.5-to-11.6.md index 2e9ec5d71de..c890f7b6147 100644 --- a/doc/update/11.5-to-11.6.md +++ b/doc/update/11.5-to-11.6.md @@ -51,7 +51,7 @@ sudo make install Install Bundler: ```bash -sudo gem install bundler --no-document +sudo gem install bundler --no-document --version '< 2' ``` ### 4. Update Node @@ -317,11 +317,11 @@ sudo systemctl daemon-reload ```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 +sudo -u git -H bundle install --deployment --without development test mysql aws kerberos + +# MySQL installations (note: the line below states '--without postgres') +sudo -u git -H bundle install --deployment --without development test postgres aws kerberos # Optional: clean up old gems sudo -u git -H bundle clean diff --git a/doc/update/11.6-to-11.7.md b/doc/update/11.6-to-11.7.md index f5f671c1946..fd7c3697e80 100644 --- a/doc/update/11.6-to-11.7.md +++ b/doc/update/11.6-to-11.7.md @@ -51,7 +51,7 @@ sudo make install Install Bundler: ```bash -sudo gem install bundler --no-document +sudo gem install bundler --no-document --version '< 2' ``` ### 4. Update Node @@ -66,7 +66,7 @@ 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 +GitLab also requires the use of yarn `>= v1.10.0` to manage JavaScript dependencies. ```bash @@ -317,11 +317,11 @@ sudo systemctl daemon-reload ```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 +sudo -u git -H bundle install --deployment --without development test mysql aws kerberos + +# MySQL installations (note: the line below states '--without postgres') +sudo -u git -H bundle install --deployment --without development test postgres aws kerberos # Optional: clean up old gems sudo -u git -H bundle clean diff --git a/doc/update/11.7-to-11.8.md b/doc/update/11.7-to-11.8.md new file mode 100644 index 00000000000..82293ff40a8 --- /dev/null +++ b/doc/update/11.7-to-11.8.md @@ -0,0 +1,394 @@ +--- +comments: false +--- + +# From 11.7 to 11.8 + +Make sure you view this update guide from the branch (version) of GitLab you would +like to install (e.g., `11-8-stable`. You can select the branch 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: Beginning in GitLab 11.0, we only support Ruby 2.4 or higher, and dropped +support for Ruby 2.3. Be sure to upgrade 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.5/ruby-2.5.3.tar.gz +echo 'f919a9fbcdb7abecd887157b49833663c5c15fda ruby-2.5.3.tar.gz' | shasum -c - && tar xzf ruby-2.5.3.tar.gz +cd ruby-2.5.3 + +./configure --disable-install-rdoc +make +sudo make install +``` + +Install Bundler: + +```bash +sudo gem install bundler --no-document --version '< 2' +``` + +### 4. Update Node + +NOTE: Beginning in GitLab 11.8, we only support node 8 or higher, and dropped +support for node 6. Be sure to upgrade if necessary. + +GitLab utilizes [webpack](http://webpack.js.org) to compile frontend assets. +This requires a minimum version of node v8.10.0. + +You can check which version you are running with `node -v`. If you are running +a version older than `v8.10.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.10.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 11.4 and higher only supports Go 1.10.x and newer, and dropped support for Go +1.9.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://dl.google.com/go/go1.10.5.linux-amd64.tar.gz +echo 'a035d9beda8341b645d3f45a1b620cf2d8fb0c5eb409be36b389c0fd384ecc3a go1.10.5.linux-amd64.tar.gz' | shasum -a256 -c - && \ + sudo tar -C /usr/local -xzf go1.10.5.linux-amd64.tar.gz +sudo ln -sf /usr/local/go/bin/{go,godoc,gofmt} /usr/local/bin/ +rm go1.10.5.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-8-stable +``` + +OR + +For GitLab Enterprise Edition: + +```bash +cd /home/git/gitlab + +sudo -u git -H git checkout 11-8-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 + +#### 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 gitlab-pages + +#### Only needed if you use GitLab Pages + +Install and compile gitlab-pages. GitLab-Pages 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-pages + +sudo -u git -H git fetch --all --tags --prune +sudo -u git -H git checkout v$(</home/git/gitlab/GITLAB_PAGES_VERSION) +sudo -u git -H make +``` + +### 11. 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 +``` + +### 12. Update configuration files + +#### New `unicorn.rb` configuration + +We have made [changes](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/22372) to `unicorn.rb` to allow GitLab run with both Unicorn and Puma in future. + +Make `/home/git/gitlab/config/unicorn.rb` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/11-8-stable/config/unicorn.rb.example but with your settings. +In particular, make sure that `require_relative "/home/git/gitlab/lib/gitlab/cluster/lifecycle_events"` line exists and the `before_exec`, `before_fork`, and `after_fork` handlers are configured as shown below: + +```ruby +require_relative "/home/git/gitlab/lib/gitlab/cluster/lifecycle_events" + +before_exec do |server| + # Signal application hooks that we're about to restart + Gitlab::Cluster::LifecycleEvents.do_master_restart +end + +before_fork do |server, worker| + # Signal application hooks that we're about to fork + Gitlab::Cluster::LifecycleEvents.do_before_fork +end + +after_fork do |server, worker| + # Signal application hooks of worker start + Gitlab::Cluster::LifecycleEvents.do_worker_start +end +``` + +#### 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/11-7-stable:config/gitlab.yml.example origin/11-8-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/11-7-stable:lib/support/nginx/gitlab-ssl origin/11-8-stable:lib/support/nginx/gitlab-ssl + +# For HTTP configurations +git diff origin/11-7-stable:lib/support/nginx/gitlab origin/11-8-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-8-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-8-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/11-7-stable:lib/support/init.d/gitlab.default.example origin/11-8-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 +``` + +### 13. Install libs, migrations, etc. + +```bash +cd /home/git/gitlab + +# PostgreSQL installations (note: the line below states '--without mysql') +sudo -u git -H bundle install --deployment --without development test mysql aws kerberos + +# MySQL installations (note: the line below states '--without postgres') +sudo -u git -H bundle install --deployment --without development test postgres aws kerberos + + +# 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). + +### 14. Start application + +```bash +sudo service gitlab start +sudo service nginx restart +``` + +### 15. 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 (11.7) + +### 1. Revert the code to the previous version + +Follow the [upgrade guide from 11.6 to 11.7](11.6-to-11.7.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-8-stable/config/gitlab.yml.example +[gl-example]: https://gitlab.com/gitlab-org/gitlab-ce/blob/11-8-stable/lib/support/init.d/gitlab.default.example diff --git a/doc/update/6.9-to-7.0.md b/doc/update/6.9-to-7.0.md index 7f3abf74675..b54884c4682 100644 --- a/doc/update/6.9-to-7.0.md +++ b/doc/update/6.9-to-7.0.md @@ -47,7 +47,7 @@ sudo make install Install Bundler: ```bash -sudo gem install bundler --no-document +sudo gem install bundler --no-document --version '< 2' ``` ### 3. Get latest code diff --git a/doc/update/6.x-or-7.x-to-7.14.md b/doc/update/6.x-or-7.x-to-7.14.md index c20a72ce162..745df56bbb0 100644 --- a/doc/update/6.x-or-7.x-to-7.14.md +++ b/doc/update/6.x-or-7.x-to-7.14.md @@ -67,7 +67,7 @@ sudo make install Install Bundler: ```bash -sudo gem install bundler --no-document +sudo gem install bundler --no-document --version '< 2' ``` ## 3. Get latest code diff --git a/doc/update/7.0-to-7.1.md b/doc/update/7.0-to-7.1.md index fb4710faad5..8b69431dee1 100644 --- a/doc/update/7.0-to-7.1.md +++ b/doc/update/7.0-to-7.1.md @@ -47,7 +47,7 @@ sudo make install Install Bundler: ```bash -sudo gem install bundler --no-document +sudo gem install bundler --no-document --version '< 2' ``` ### 3. Get latest code diff --git a/doc/update/8.10-to-8.11.md b/doc/update/8.10-to-8.11.md index 12a465e1602..f8415b5159b 100644 --- a/doc/update/8.10-to-8.11.md +++ b/doc/update/8.10-to-8.11.md @@ -47,7 +47,7 @@ sudo make install Install Bundler: ```bash -sudo gem install bundler --no-document +sudo gem install bundler --no-document --version '< 2' ``` ### 4. Get latest code diff --git a/doc/update/8.11-to-8.12.md b/doc/update/8.11-to-8.12.md index b9a7986d5ba..07ac8129b4f 100644 --- a/doc/update/8.11-to-8.12.md +++ b/doc/update/8.11-to-8.12.md @@ -47,7 +47,7 @@ sudo make install Install Bundler: ```bash -sudo gem install bundler --no-document +sudo gem install bundler --no-document --version '< 2' ``` ### 4. Get latest code diff --git a/doc/update/8.12-to-8.13.md b/doc/update/8.12-to-8.13.md index 37e61794e7e..bf622deaba8 100644 --- a/doc/update/8.12-to-8.13.md +++ b/doc/update/8.12-to-8.13.md @@ -47,7 +47,7 @@ sudo make install Install Bundler: ```bash -sudo gem install bundler --no-document +sudo gem install bundler --no-document --version '< 2' ``` ### 4. Get latest code diff --git a/doc/update/8.13-to-8.14.md b/doc/update/8.13-to-8.14.md index 927f453b9bf..43b636ea958 100644 --- a/doc/update/8.13-to-8.14.md +++ b/doc/update/8.13-to-8.14.md @@ -47,7 +47,7 @@ sudo make install Install Bundler: ```bash -sudo gem install bundler --no-document +sudo gem install bundler --no-document --version '< 2' ``` ### 4. Get latest code diff --git a/doc/update/8.14-to-8.15.md b/doc/update/8.14-to-8.15.md index d98a60d31c8..eadf1743597 100644 --- a/doc/update/8.14-to-8.15.md +++ b/doc/update/8.14-to-8.15.md @@ -50,7 +50,7 @@ sudo make install Install Bundler: ```bash -sudo gem install bundler --no-document +sudo gem install bundler --no-document --version '< 2' ``` ### 4. Get latest code diff --git a/doc/update/8.15-to-8.16.md b/doc/update/8.15-to-8.16.md index 94b0102ed48..4e8d54d5010 100644 --- a/doc/update/8.15-to-8.16.md +++ b/doc/update/8.15-to-8.16.md @@ -50,7 +50,7 @@ sudo make install Install Bundler: ```bash -sudo gem install bundler --no-document +sudo gem install bundler --no-document --version '< 2' ``` ### 4. Get latest code diff --git a/doc/update/8.16-to-8.17.md b/doc/update/8.16-to-8.17.md index 5a4f620a164..cab28a4d1c6 100644 --- a/doc/update/8.16-to-8.17.md +++ b/doc/update/8.16-to-8.17.md @@ -50,7 +50,7 @@ sudo make install Install Bundler: ```bash -sudo gem install bundler --no-document +sudo gem install bundler --no-document --version '< 2' ``` ### 4. Update Node diff --git a/doc/update/8.17-to-9.0.md b/doc/update/8.17-to-9.0.md index 38f7d22437a..55cf0842df4 100644 --- a/doc/update/8.17-to-9.0.md +++ b/doc/update/8.17-to-9.0.md @@ -49,7 +49,7 @@ sudo make install Install Bundler: ```bash -sudo gem install bundler --no-document +sudo gem install bundler --no-document --version '< 2' ``` ### 4. Update Node diff --git a/doc/update/9.0-to-9.1.md b/doc/update/9.0-to-9.1.md index a4d2e7be23c..10214fd8aca 100644 --- a/doc/update/9.0-to-9.1.md +++ b/doc/update/9.0-to-9.1.md @@ -49,7 +49,7 @@ sudo make install Install Bundler: ```bash -sudo gem install bundler --no-document +sudo gem install bundler --no-document --version '< 2' ``` ### 4. Update Node diff --git a/doc/update/9.1-to-9.2.md b/doc/update/9.1-to-9.2.md index dd808c51985..79d92f05257 100644 --- a/doc/update/9.1-to-9.2.md +++ b/doc/update/9.1-to-9.2.md @@ -49,7 +49,7 @@ sudo make install Install Bundler: ```bash -sudo gem install bundler --no-document +sudo gem install bundler --no-document --version '< 2' ``` ### 4. Update Node diff --git a/doc/update/9.2-to-9.3.md b/doc/update/9.2-to-9.3.md index d2bcf45a28e..98443b8bfa6 100644 --- a/doc/update/9.2-to-9.3.md +++ b/doc/update/9.2-to-9.3.md @@ -49,7 +49,7 @@ sudo make install Install Bundler: ```bash -sudo gem install bundler --no-document +sudo gem install bundler --no-document --version '< 2' ``` ### 4. Update Node diff --git a/doc/update/9.3-to-9.4.md b/doc/update/9.3-to-9.4.md index dae2162a964..640b9c3997e 100644 --- a/doc/update/9.3-to-9.4.md +++ b/doc/update/9.3-to-9.4.md @@ -49,7 +49,7 @@ sudo make install Install Bundler: ```bash -sudo gem install bundler --no-document +sudo gem install bundler --no-document --version '< 2' ``` ### 4. Update Node diff --git a/doc/update/9.4-to-9.5.md b/doc/update/9.4-to-9.5.md index f2811e9471f..e6cfa70975e 100644 --- a/doc/update/9.4-to-9.5.md +++ b/doc/update/9.4-to-9.5.md @@ -49,7 +49,7 @@ sudo make install Install Bundler: ```bash -sudo gem install bundler --no-document +sudo gem install bundler --no-document --version '< 2' ``` ### 4. Update Node diff --git a/doc/update/9.5-to-10.0.md b/doc/update/9.5-to-10.0.md index 333a6e35714..8b565f67cb1 100644 --- a/doc/update/9.5-to-10.0.md +++ b/doc/update/9.5-to-10.0.md @@ -49,7 +49,7 @@ sudo make install Install Bundler: ```bash -sudo gem install bundler --no-document +sudo gem install bundler --no-document --version '< 2' ``` ### 4. Update Node diff --git a/doc/user/award_emojis.md b/doc/user/award_emojis.md index 93be3da44d4..e4fd08a582c 100644 --- a/doc/user/award_emojis.md +++ b/doc/user/award_emojis.md @@ -1,24 +1,24 @@ # Award emoji -> **Notes:** -> - First [introduced][1825] in GitLab 8.2. -> - GitLab 9.0 [introduced][ce-9570] the usage of native emojis if the platform +> - First [introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/1825) in GitLab 8.2. +> - GitLab 9.0 [introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9570) the usage of native emoji if the platform > supports them and falls back to images or CSS sprites. This change greatly -> improved the award emoji performance overall. +> improved award emoji performance overall. When you're collaborating online, you get fewer opportunities for high-fives -and thumbs-ups. Emoji can be awarded to issues, merge requests, snippets, and -virtually everywhere where you can have a discussion. +and thumbs-ups. Emoji can be awarded to [issues](project/issues/index.md), [merge requests](project/merge_requests/index.md), +[snippets](snippets.md), and anywhere you can have a discussion. ![Award emoji](img/award_emoji_select.png) Award emoji make it much easier to give and receive feedback without a long -comment thread. Comments that are only emoji will automatically become -award emoji. +comment thread. + +For information on the relevant API, see [Award Emoji API](../api/award_emoji.md). ## Sort issues and merge requests on vote count -> [Introduced][2871] in GitLab 8.5. +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/2781) in GitLab 8.5. You can quickly sort issues and merge requests by the number of votes they have received. The sort options can be found in the dropdown menu as "Most @@ -32,20 +32,16 @@ downvotes. ## Award emoji for comments -> [Introduced][4291] in GitLab 8.9. +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/4291) in GitLab 8.9. Award emoji can also be applied to individual comments when you want to celebrate an accomplishment or agree with an opinion. -To add an award emoji, click the smile in the top right of the comment and pick -an emoji from the dropdown. If you want to remove an award emoji, just click -the emoji again and the vote will be removed. +To: + +- Add an award emoji, click the smile in the top right of the comment and pick an emoji from the dropdown. +- Remove an award emoji, click the emoji again and the vote will be removed. ![Picking an emoji for a comment](img/award_emoji_comment_picker.png) ![An award emoji has been applied to a comment](img/award_emoji_comment_awarded.png) - -[2871]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/2781 -[1825]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/1825 -[4291]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/4291 -[ce-9570]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9570 diff --git a/doc/user/discussions/index.md b/doc/user/discussions/index.md index 9379d047fca..84f4b0b3922 100644 --- a/doc/user/discussions/index.md +++ b/doc/user/discussions/index.md @@ -280,7 +280,7 @@ Additionally locked issues can not be reopened. For issues with many comments like activity notes and user comments, sometimes finding useful information can be hard. There is a way to filter comments from single notes and discussions for merge requests and issues. -From a merge request's **Discussion** tab, or from an issue overview, find the filter's dropdown menu on the right side of the page, from which you can choose one of the following options: +From a merge request's **Discussion** tab, or from an epic/issue overview, find the filter's dropdown menu on the right side of the page, from which you can choose one of the following options: - **Show all activity**: displays all user comments and system notes (issue updates, mentions from other issues, changes to the description, etc). diff --git a/doc/user/group/subgroups/index.md b/doc/user/group/subgroups/index.md index 4d56b33f684..b6f8f55978b 100644 --- a/doc/user/group/subgroups/index.md +++ b/doc/user/group/subgroups/index.md @@ -164,9 +164,11 @@ and you can choose the group of people to be notified. Here's a list of what you can't do with subgroups: -- [GitLab Pages](../../project/pages/index.md) are not currently working for - projects hosted under a subgroup. That means that only projects hosted under - the first parent group will work. +- [GitLab Pages](../../project/pages/index.md) supports projects hosted under + a subgroup, but not subgroup websites. + That means that only the highest-level group supports + [group websites](../../project/pages/introduction.html#user-or-group-pages), + although you can have project websites under a subgroup. - It is not possible to share a project with a group that's an ancestor of the group the project is in. That means you can only share as you walk down the hierarchy. For example, `group/subgroup01/project` **cannot** be shared diff --git a/doc/user/markdown.md b/doc/user/markdown.md index 893658290e5..f2448f240ca 100644 --- a/doc/user/markdown.md +++ b/doc/user/markdown.md @@ -300,7 +300,7 @@ You can use it to point out a <img src="https://gitlab.com/gitlab-org/gitlab-ce/ If you are new to this, don't be <img src="https://gitlab.com/gitlab-org/gitlab-ce/raw/master/app/assets/images/emoji/fearful.png" width="20px" height="20px">. You can easily join the emoji <img src="https://gitlab.com/gitlab-org/gitlab-ce/raw/master/app/assets/images/emoji/family.png" width="20px" height="20px">. 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. <img src="https://gitlab.com/gitlab-org/gitlab-ce/raw/master/app/assets/images/emoji/thumbsup.png" width="20px" height="20px"> +Consult the [Emoji Cheat Sheet](https://www.webfx.com/tools/emoji-cheat-sheet/) for a list of all supported emoji codes. <img src="https://gitlab.com/gitlab-org/gitlab-ce/raw/master/app/assets/images/emoji/thumbsup.png" width="20px" height="20px"> Most emoji are natively supported on macOS, Windows, iOS, Android and will fallback to image-based emoji where there is lack of support. diff --git a/doc/user/permissions.md b/doc/user/permissions.md index 2a1c8cc5bc0..0c358390046 100644 --- a/doc/user/permissions.md +++ b/doc/user/permissions.md @@ -178,9 +178,7 @@ 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]** | ✓ | ✓ | ✓ | ✓ | ✓ | +| View group epic **[ULTIMATE]** | ✓ | ✓ | ✓ | ✓ | ✓ | | Create/edit group epic **[ULTIMATE]** | | ✓ | ✓ | ✓ | ✓ | | Delete group epic **[ULTIMATE]** | | | | | ✓ | | View group Audit Events | | | | | ✓ | diff --git a/doc/user/project/clusters/index.md b/doc/user/project/clusters/index.md index 6f334af4fb7..bb815695cb1 100644 --- a/doc/user/project/clusters/index.md +++ b/doc/user/project/clusters/index.md @@ -178,8 +178,11 @@ When creating a cluster in GitLab, you will be asked if you would like to create [Attribute-based access control (ABAC)](https://kubernetes.io/docs/admin/authorization/abac/) cluster, or a [Role-based access control (RBAC)](https://kubernetes.io/docs/admin/authorization/rbac/) one. -Whether ABAC or RBAC is enabled, GitLab will create the necessary -service accounts and privileges in order to install and run +NOTE: **Note:** +[RBAC](#role-based-access-control-rbac) is recommended and the GitLab default. + +Whether [ABAC](#attribute-based-access-control-abac) or [RBAC](#role-based-access-control-rbac) is enabled, +GitLab will create the necessary service accounts and privileges in order to install and run [GitLab managed applications](#installing-applications): - If GitLab is creating the cluster, a `gitlab` service account with diff --git a/doc/user/project/clusters/serverless/index.md b/doc/user/project/clusters/serverless/index.md index 9ecb109fa89..bebccf97987 100644 --- a/doc/user/project/clusters/serverless/index.md +++ b/doc/user/project/clusters/serverless/index.md @@ -121,9 +121,9 @@ In order to deploy functions to your Knative instance, the following files must runtime: https://gitlab.com/triggermesh/runtimes/raw/master/nodejs.yaml description: "echo function using node.js runtime" buildargs: - - DIRECTORY=echo - environment: - FUNCTION: echo + - DIRECTORY=echo + environment: + FUNCTION: echo ``` diff --git a/doc/user/project/index.md b/doc/user/project/index.md index d46ae31580a..6a1aadf058e 100644 --- a/doc/user/project/index.md +++ b/doc/user/project/index.md @@ -65,8 +65,8 @@ common actions on issues or merge requests browse, and download job artifacts - [Pipeline settings](pipelines/settings.md): Set up Git strategy (choose the default way your repository is fetched from GitLab in a job), timeout (defines the maximum amount of time in minutes that a job is able run), custom path for `.gitlab-ci.yml`, test coverage parsing, pipeline's visibility, and much more - - [GKE cluster integration](clusters/index.md): Connecting your GitLab project - with Google Kubernetes Engine + - [Kubernetes cluster integration](clusters/index.md): Connecting your GitLab project + with a Kubernetes cluster - [GitLab Pages](pages/index.md): Build, test, and deploy your static website with GitLab Pages @@ -149,3 +149,24 @@ When [renaming a user](../profile/index.md#changing-your-username), work after a rename, making any transition a lot smoother. - The redirects will be available as long as the original path is not claimed by another group, user or project. + +## Use your project as a Go package + +Any project can be used as a Go package including private projects in subgroups. To use packages +hosted in private projects with the `go get` command, use a [`.netrc` file](https://ec.haxx.se/usingcurl-netrc.html) +and a [personal access token](../profile/personal_access_tokens.md) in the password field. + +For example: + +```text +machine example.gitlab.com +login <gitlab_user_name> +password <personal_access_token> +``` + +## Access project page with project ID + +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/53671) in GitLab 11.8. + +To quickly access a project from the GitLab UI using the project ID, +visit the `/projects/:id` URL in your browser or other tool accessing the project. diff --git a/doc/user/project/integrations/prometheus_library/index.md b/doc/user/project/integrations/prometheus_library/index.md index 9b9b4f6c8ca..a79bc2bce06 100644 --- a/doc/user/project/integrations/prometheus_library/index.md +++ b/doc/user/project/integrations/prometheus_library/index.md @@ -10,7 +10,8 @@ Currently supported exporters are: - [Kubernetes](kubernetes.md) - [NGINX](nginx.md) -- [NGINX Ingress Controller](nginx_ingress.md) +- [NGINX Ingress Controller 0.9.0-0.15.x](nginx_ingress_vts.md) +- [NGINX Ingress Controller 0.16.0+](nginx_ingress.md) - [HAProxy](haproxy.md) - [Amazon Cloud Watch](cloudwatch.md) diff --git a/doc/user/project/integrations/prometheus_library/nginx_ingress.md b/doc/user/project/integrations/prometheus_library/nginx_ingress.md index d5f77d622be..b7601f26802 100644 --- a/doc/user/project/integrations/prometheus_library/nginx_ingress.md +++ b/doc/user/project/integrations/prometheus_library/nginx_ingress.md @@ -1,8 +1,10 @@ # Monitoring NGINX Ingress Controller -> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/13438) in GitLab 9.5. +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/22133) in GitLab 11.7. -GitLab has support for automatically detecting and monitoring the Kubernetes NGINX ingress controller. This is provided by leveraging the built in Prometheus metrics included in [version 0.9.0](https://github.com/kubernetes/ingress-nginx/blob/master/Changelog.md#09-beta1) and above of the ingress. +NOTE: **Note:** NGINX Ingress versions prior to 0.16.0 offer an included [VTS Prometheus metrics exporter](nginx_ingress_vts.md), which exports metrics different than the built-in metrics. + +GitLab has support for automatically detecting and monitoring the Kubernetes NGINX ingress controller. This is provided by leveraging the built-in Prometheus metrics included starting with [version 0.16.0](https://github.com/kubernetes/ingress-nginx/blob/master/Changelog.md#0160). ## Requirements @@ -12,9 +14,9 @@ GitLab has support for automatically detecting and monitoring the Kubernetes NGI | Name | Query | | ---- | ----- | -| 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 (%) | 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 | +| Throughput (req/sec) | sum(label_replace(rate(nginx_ingress_controller_requests{namespace="%{kube_namespace}",ingress=~".*%{ci_environment_slug}.*"}[2m]), "status_code", "${1}xx", "status", "(.)..")) by (status_code) | +| Latency (ms) | sum(rate(nginx_ingress_controller_ingress_upstream_latency_seconds_sum{namespace="%{kube_namespace}",ingress=~".*%{ci_environment_slug}.*"}[2m])) / sum(rate(nginx_ingress_controller_ingress_upstream_latency_seconds_count{namespace="%{kube_namespace}",ingress=~".*%{ci_environment_slug}.*"}[2m])) * 1000 | +| HTTP Error Rate (%) | sum(rate(nginx_ingress_controller_requests{status=~"5.*",namespace="%{kube_namespace}",ingress=~".*%{ci_environment_slug}.*"}[2m])) / sum(rate(nginx_ingress_controller_requests{namespace="%{kube_namespace}",ingress=~".*%{ci_environment_slug}.*"}[2m])) * 100 | ## Configuring NGINX ingress monitoring @@ -22,9 +24,9 @@ If you have deployed NGINX Ingress using GitLab's [Kubernetes cluster integratio For other deployments, there is [some configuration](#manually-setting-up-nginx-ingress-for-prometheus-monitoring) required depending on your installation: -- NGINX Ingress should be version 0.9.0 or above, with metrics enabled -- NGINX Ingress should be annotated for Prometheus monitoring -- Prometheus should be configured to monitor annotated pods +- NGINX Ingress should be version 0.16.0 or above, with metrics enabled. +- NGINX Ingress should be annotated for Prometheus monitoring. +- Prometheus should be configured to monitor annotated pods. ### About managed NGINX Ingress deployments @@ -32,9 +34,9 @@ NGINX Ingress is deployed into the `gitlab-managed-apps` namespace, using the [o NGINX is configured for Prometheus monitoring, by setting: -- `enable-vts-status: "true"`, to export Prometheus metrics -- `prometheus.io/scrape: "true"`, to enable automatic discovery -- `prometheus.io/port: "10254"`, to specify the metrics port +- `enable-vts-status: "true"`, to export Prometheus metrics. +- `prometheus.io/scrape: "true"`, to enable automatic discovery. +- `prometheus.io/port: "10254"`, to specify the metrics port. When used in conjunction with the GitLab deployed Prometheus service, response metrics will be automatically collected. @@ -51,6 +53,6 @@ Managing these settings depends on how NGINX ingress has been deployed. If you h ## Specifying the Environment label -In order to isolate and only display relevant metrics for a given environment, GitLab needs a method to detect which labels are associated. To do this, GitLab will search for metrics with appropriate labels. In this case, the `upstream` label must be of the form `<KUBE_NAMESPACE>-<CI_ENVIRONMENT_SLUG>-*`. +In order to isolate and only display relevant metrics for a given environment, GitLab needs a method to detect which labels are associated. To do this, GitLab will search for metrics with appropriate labels. In this case, the `ingress` label must `<CI_ENVIRONMENT_SLUG>`. If you have used [Auto Deploy](../../../../topics/autodevops/index.md#auto-deploy) to deploy your app, this format will be used automatically and metrics will be detected with no action on your part. diff --git a/doc/user/project/integrations/prometheus_library/nginx_ingress_vts.md b/doc/user/project/integrations/prometheus_library/nginx_ingress_vts.md new file mode 100644 index 00000000000..081eb8732ad --- /dev/null +++ b/doc/user/project/integrations/prometheus_library/nginx_ingress_vts.md @@ -0,0 +1,58 @@ +# Monitoring NGINX Ingress Controller with VTS metrics + +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/13438) in GitLab 9.5. + +NOTE: **Note:** [NGINX Ingress version 0.16](nginx_ingress.md) and above have built-in Prometheus metrics, which are different than the VTS based metrics. + +GitLab has support for automatically detecting and monitoring the Kubernetes NGINX ingress controller. This is provided by leveraging the included VTS Prometheus metrics exporter in [version 0.9.0](https://github.com/kubernetes/ingress-nginx/blob/master/Changelog.md#09-beta1) through [0.15.x](https://github.com/kubernetes/ingress-nginx/blob/master/Changelog.md#0150). + +## Requirements + +[Prometheus integration](../prometheus.md) must be active. + +## Metrics supported + +| Name | Query | +| ---- | ----- | +| 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 (%) | 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 + +If you have deployed NGINX Ingress using GitLab's [Kubernetes cluster integration](../../clusters/index.md#installing-applications), it will [automatically be monitored](#about-managed-nginx-ingress-deployments) by Prometheus. + +For other deployments, there is [some configuration](#manually-setting-up-nginx-ingress-for-prometheus-monitoring) required depending on your installation: + +- NGINX Ingress should be version 0.9.0 or above, with metrics enabled. +- NGINX Ingress should be annotated for Prometheus monitoring. +- Prometheus should be configured to monitor annotated pods. + +### About managed NGINX Ingress deployments + +NGINX Ingress is deployed into the `gitlab-managed-apps` namespace, using the [official Helm chart](https://github.com/kubernetes/charts/tree/master/stable/nginx-ingress). NGINX Ingress will be [externally reachable via the Load Balancer's IP](../../clusters/index.md#getting-the-external-ip-address). + +NGINX is configured for Prometheus monitoring, by setting: + +- `enable-vts-status: "true"`, to export Prometheus metrics. +- `prometheus.io/scrape: "true"`, to enable automatic discovery. +- `prometheus.io/port: "10254"`, to specify the metrics port. + +When used in conjunction with the GitLab deployed Prometheus service, response metrics will be automatically collected. + +### Manually setting up NGINX Ingress for Prometheus monitoring + +Version 0.9.0 and above of [NGINX ingress](https://github.com/kubernetes/ingress-nginx) have built-in support for exporting Prometheus metrics. To enable, a ConfigMap setting must be passed: `enable-vts-status: "true"`. Once enabled, a Prometheus metrics endpoint will start running on port 10254. + +Next, the ingress needs to be annotated for Prometheus monitoring. Two new annotations need to be added: + +- `prometheus.io/scrape: "true"` +- `prometheus.io/port: "10254"` + +Managing these settings depends on how NGINX ingress has been deployed. If you have deployed via the [official Helm chart](https://github.com/kubernetes/charts/tree/master/stable/nginx-ingress), metrics can be enabled with `controller.stats.enabled` along with the required annotations. Alternatively it is possible edit the NGINX ingress YML directly in the [Kubernetes dashboard](https://github.com/kubernetes/dashboard). + +## Specifying the Environment label + +In order to isolate and only display relevant metrics for a given environment, GitLab needs a method to detect which labels are associated. To do this, GitLab will search for metrics with appropriate labels. In this case, the `upstream` label must be of the form `<KUBE_NAMESPACE>-<CI_ENVIRONMENT_SLUG>-*`. + +If you have used [Auto Deploy](../../../../topics/autodevops/index.md#auto-deploy) to deploy your app, this format will be used automatically and metrics will be detected with no action on your part. diff --git a/doc/user/project/issues/csv_import.md b/doc/user/project/issues/csv_import.md index 001e0d303e9..032e3a73ad0 100644 --- a/doc/user/project/issues/csv_import.md +++ b/doc/user/project/issues/csv_import.md @@ -2,16 +2,30 @@ > [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/23532) in GitLab 11.7. -Issues can be imported by uploading a CSV file. The file will be processed in the background and a notification email -will be sent to you once the import is completed. +Issues can be imported to a project by uploading a CSV file. Supported fields are +`title` and `description`. + +The user uploading the CSV file will be set as the author of the imported issues. > **Note:** A permission level of `Developer` or higher is required to import issues. +To import issues: + +1. Ensure your CSV file meets the [file format](#csv-file-format) requirements. +1. Navigate to a project's Issues list page. +1. If existing issues are present, click the import icon at the top right, next to the **Edit issues** button. +1. For a project without any issues, click the button labeled **Import CSV** in the middle of the page. +1. Select the file and click the **Import issues** button. + +The file is processed in the background and a notification email is sent +to you once the import is completed. + ## CSV File Format ### Header row -CSV files must contain a header row with at least two columns: `title` and `description`, in that order. +CSV files must contain a header row beginning with at least two columns, `title` and `description`, in that order. +If additional columns are present, they will be ignored. ### Column separator @@ -33,7 +47,11 @@ a double-quote (`"`) within a quoted field, use two double-quote characters in s After the header row, succeeding rows must follow the same column order. The issue title is required while the description is optional. -The user uploading the CSV file will be set as the author of the imported issues. +### File size + +The limit depends on the configuration value of Max Attachment Size for the GitLab instance. + +For GitLab.com, it is set to 10 MB. ## Sample Data diff --git a/doc/user/project/issues/img/import_csv_button.png b/doc/user/project/issues/img/import_csv_button.png Binary files differdeleted file mode 100644 index ab100a95750..00000000000 --- a/doc/user/project/issues/img/import_csv_button.png +++ /dev/null diff --git a/doc/user/project/issues/index.md b/doc/user/project/issues/index.md index 40a1f60c4ab..5a3ac9c175b 100644 --- a/doc/user/project/issues/index.md +++ b/doc/user/project/issues/index.md @@ -144,12 +144,12 @@ create various boards per project with [Multiple Issue Boards](https://docs.gitl ### Import Issues from CSV -From the project-level issues list, you can find the import button near the "Edit issues" button in the upper-right -side. +You can import a CSV file containing issue titles and descriptions to create +a batch of issues simultaneously. -![Import CSV button](img/import_csv_button.png) +When you navigate to the Issues list page, an import button is displayed. -Learn more about [importing issues from CSV](csv_import.md) +For further details, see [Importing issues from CSV](csv_import.md) ### External Issue Tracker @@ -157,14 +157,14 @@ Alternatively to GitLab's built-in Issue Tracker, you can also use an [external tracker](../../../integration/external-issue-tracker.md) such as Jira, Redmine, or Bugzilla. -### Issue's API +### Issue API -Read through the [API documentation](../../../api/issues.md). +See the [API documentation](../../../api/issues.md). ### Bulk editing issues -Find out about [bulk editing issues](../../project/bulk_editing.md). +See the [bulk editing issues](../../project/bulk_editing.md) page. ### Similar issues -Find out about [similar issues](similar_issues.md). +See the [similar issues](similar_issues.md) page. diff --git a/doc/user/project/members/index.md b/doc/user/project/members/index.md index 2c2e8e2d556..2a490e3fd7f 100644 --- a/doc/user/project/members/index.md +++ b/doc/user/project/members/index.md @@ -77,7 +77,7 @@ GitLab users to the project. Once done, hit **Add users to project** and watch that there is a new member with the e-mail address we used above. From there on, you can resend the -invitation, change their access level or even delete them. +invitation, change their access level, or even delete them. ![Invite user members list](img/add_user_email_accept.png) diff --git a/doc/user/project/merge_requests/allow_collaboration.md b/doc/user/project/merge_requests/allow_collaboration.md index 859ac92ef89..da6e6b5fd3a 100644 --- a/doc/user/project/merge_requests/allow_collaboration.md +++ b/doc/user/project/merge_requests/allow_collaboration.md @@ -1,20 +1,72 @@ # Allow collaboration on merge requests across forks -> [Introduced][ce-17395] in GitLab 10.6. +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/17395) + in GitLab 10.6. + +When a user opens a merge request from a fork, they are given the option to allow +upstream members to collaborate with them on the source branch. This allows +the members of the upstream project to make small fixes or rebase branches +before merging, reducing the back and forth of accepting external contributions. 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. +publicly accessible. 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. +## Enabling commit edits from upstream members + 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. +source project and only lasts while the merge request is open. Once enabled, +upstream members will also be able to retry the pipelines and jobs of the +merge request: + +1. Enable the contribution while creating or editing a merge request. + + ![Enable contribution](img/allow_collaboration.png) + +1. Once the merge request is created, you'll see that commits from members who + can merge to the target branch are allowed. + + ![Check that contribution is enabled](img/allow_collaboration_after_save.png) + +## Pushing to the fork as the upstream member + +If the creator of the merge request has enabled contributions from upstream +members, you can push directly to the branch of the forked repository. + +Assuming that: + +- The forked project URL is `git@gitlab.com:thedude/awesome-project.git`. +- The branch of the merge request is `update-docs`. + +Here's how the process would look like: + +1. First, you need to get the changes that the merge request has introduced. + Click the **Check out branch** button that has some pre-populated + commands that you can run. + + ![Check out branch button](img/checkout_button.png) + +1. Use the copy to clipboard button to copy the first command and paste them + in your terminal: + + ```sh + git fetch git@gitlab.com:thedude/awesome-project.git update-docs + git checkout -b thedude-awesome-project-update-docs FETCH_HEAD + ``` + + This will fetch the branch of the forked project and then create a local branch + based off the fetched branch. -Enable this functionality while creating or editing a merge request: +1. Make any changes you want and commit. +1. Push to the forked project: -![Enable collaboration](./img/allow_collaboration.png) + ```sh + git push git@gitlab.com:thedude/awesome-project.git thedude-awesome-project-update-docs:update-docs + ``` -[ce-17395]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/17395 + Note the colon (`:`) between the two branches. The above command will push the + local branch `thedude-awesome-project-update-docs` to the + `update-docs` branch of the `git@gitlab.com:thedude/awesome-project.git` repository. diff --git a/doc/user/project/merge_requests/img/allow_collaboration.png b/doc/user/project/merge_requests/img/allow_collaboration.png Binary files differindex 3c81e4c27b8..e40e8a6b11c 100644 --- a/doc/user/project/merge_requests/img/allow_collaboration.png +++ b/doc/user/project/merge_requests/img/allow_collaboration.png diff --git a/doc/user/project/merge_requests/img/allow_collaboration_after_save.png b/doc/user/project/merge_requests/img/allow_collaboration_after_save.png Binary files differnew file mode 100644 index 00000000000..4ba4c84c8c5 --- /dev/null +++ b/doc/user/project/merge_requests/img/allow_collaboration_after_save.png diff --git a/doc/user/project/merge_requests/img/checkout_button.png b/doc/user/project/merge_requests/img/checkout_button.png Binary files differnew file mode 100644 index 00000000000..9850795c9b4 --- /dev/null +++ b/doc/user/project/merge_requests/img/checkout_button.png diff --git a/doc/user/project/merge_requests/index.md b/doc/user/project/merge_requests/index.md index f479f9e4ef6..2c8a590fc45 100644 --- a/doc/user/project/merge_requests/index.md +++ b/doc/user/project/merge_requests/index.md @@ -92,6 +92,15 @@ request widget will show the "Removes source branch" text. ![Remove source branch status](img/remove_source_branch_status.png) +## Allow collaboration on merge requests across forks + +When a user opens a merge request from a fork, they are given the option to allow +upstream maintainers to collaborate with them on the source branch. This allows +the maintainers of the upstream project to make small fixes or rebase branches +before merging, reducing the back and forth of accepting community contributions. + +[Learn more about allowing upstream members to push to forks.](allow_collaboration.md) + ## Authorization for merge requests There are two main ways to have a merge request flow with GitLab: @@ -275,7 +284,11 @@ you can preview the changes submitted to a feature-branch through a merge reques in a per-branch basis. No need to checkout the branch, install and preview locally; all your changes will be available to preview by anyone with the Review Apps link. -[Read more about Review Apps.](../../../ci/review_apps/index.md) +With GitLab's [Route Maps](../../../ci/review_apps/index.md#route-maps) set, the +merge request widget takes you directly to the pages changed, making it easier and +faster to preview proposed modifications. + +[Read more about Review Apps](../../../ci/review_apps/index.md). ## Pipelines for merge requests diff --git a/doc/user/project/operations/error_tracking.md b/doc/user/project/operations/error_tracking.md index 2b5abc7233f..c79fbad5765 100644 --- a/doc/user/project/operations/error_tracking.md +++ b/doc/user/project/operations/error_tracking.md @@ -1,6 +1,6 @@ # Error Tracking -> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/169) in GitLab 11.7. +> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/169) in GitLab 11.8. Error tracking allows developers to easily discover and view the errors that their application may be generating. By surfacing error information where the code is being developed, efficiency and awareness can be increased. diff --git a/doc/user/project/pages/getting_started_part_one.md b/doc/user/project/pages/getting_started_part_one.md index 290dfa5af84..595241b2cba 100644 --- a/doc/user/project/pages/getting_started_part_one.md +++ b/doc/user/project/pages/getting_started_part_one.md @@ -85,6 +85,12 @@ and a project within this group is called `blog`. Your project URL is `https://gitlab.com/websites/blog/`. Once you enable GitLab Pages for this project, the site will live under `https://websites.gitlab.io/blog/`. +- You created a group for your engineering department called `engineering`, +a subgroup for all your documentation websites called `docs`, +and a project within this subgroup is called `workflows`. Your project +URL is `https://gitlab.com/engineering/docs/workflows/`. Once you enable +GitLab Pages for this project, the site will live under +`https://engineering.gitlab.io/docs/workflows`. #### User and Group Websites @@ -97,9 +103,7 @@ will be published under `https://john.gitlab.io`. Once you enable GitLab Pages for your project, your website will be published under `https://websites.gitlab.io`. ->**Note:** -GitLab Pages [does **not** support subgroups](../../group/subgroups/index.md#limitations). -You can only create the highest level group website. +> Support for subgroup project's websites was [introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/30548) in GitLab 11.8. **General example:** diff --git a/doc/user/project/pages/introduction.md b/doc/user/project/pages/introduction.md index ed049e2e648..a7846b1ee18 100644 --- a/doc/user/project/pages/introduction.md +++ b/doc/user/project/pages/introduction.md @@ -38,6 +38,7 @@ be served on. | Group pages | `groupname.example.io` | `http(s)://groupname.example.io` | | Project pages owned by a user | `projectname` | `http(s)://username.example.io/projectname` | | Project pages owned by a group | `projectname` | `http(s)://groupname.example.io/projectname`| +| Project pages owned by a subgroup | `subgroup/projectname` | `http(s)://groupname.example.io/subgroup/projectname`| > **Warning:** > There are some known [limitations](#limitations) regarding namespaces served @@ -494,8 +495,8 @@ don't redirect HTTP to HTTPS. [rfc]: https://tools.ietf.org/html/rfc2818#section-3.1 "HTTP Over TLS RFC" -GitLab Pages [does **not** support subgroups](../../group/subgroups/index.md#limitations). -You can only create the highest level group website. +GitLab Pages [does **not** support group websites for subgroups](../../group/subgroups/index.md#limitations). +You can only create the highest-level group website. ## Redirects in GitLab Pages diff --git a/doc/user/project/pipelines/schedules.md b/doc/user/project/pipelines/schedules.md index 051277dfe02..ec8b8444d99 100644 --- a/doc/user/project/pipelines/schedules.md +++ b/doc/user/project/pipelines/schedules.md @@ -3,7 +3,7 @@ > **Notes**: > - This feature was introduced in 9.1 as [Trigger Schedule][ce-10533]. > - In 9.2, the feature was [renamed to Pipeline Schedule][ce-10853]. -> - Cron notation is parsed by [Rufus-Scheduler](https://github.com/jmettraux/rufus-scheduler). +> - Cron notation is parsed by [Fugit](https://github.com/floraison/fugit). Pipeline schedules can be used to run a pipeline at specific intervals, for example every month on the 22nd for a certain branch. diff --git a/doc/user/project/pipelines/settings.md b/doc/user/project/pipelines/settings.md index 88d745b0ce4..bb9b4238ee9 100644 --- a/doc/user/project/pipelines/settings.md +++ b/doc/user/project/pipelines/settings.md @@ -1,7 +1,7 @@ # Pipelines settings To reach the pipelines settings navigate to your project's -**Settings ➔ CI/CD**. +**Settings > CI/CD**. The following settings can be configured per project. @@ -10,14 +10,14 @@ The following settings can be configured per project. With Git strategy, you can choose the default way your repository is fetched from GitLab in a job. -There are two options: +There are two options. Using: -- Using `git clone` which is slower since it clones the repository from scratch +- `git clone`, which is slower since it clones the repository from scratch for every job, ensuring that the project workspace is always pristine. -- Using `git fetch` which is faster as it re-uses the project workspace (falling +- `git fetch`, which is faster as it re-uses the project workspace (falling back to clone if it doesn't exist). -The default Git strategy can be overridden by the [GIT_STRATEGY variable][var] +The default Git strategy can be overridden by the [GIT_STRATEGY variable](../../../ci/yaml/README.md#git-strategy) in `.gitlab-ci.yml`. ## Timeout @@ -29,14 +29,14 @@ if the job surpasses the threshold, it is marked as failed. ### Timeout overriding on Runner level -> - [Introduced][ce-17221] in GitLab 10.7. +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/17221) in GitLab 10.7. Project defined timeout (either specific timeout set by user or the default -60 minutes timeout) may be [overridden on Runner level][timeout overriding]. +60 minutes timeout) may be [overridden on Runner level](../../../ci/runners/README.html#setting-maximum-job-timeout-for-a-runner). ## Custom CI config path -> - [Introduced][ce-12509] in GitLab 9.4. +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/12509) in GitLab 9.4. By default we look for the `.gitlab-ci.yml` file in the project's root directory. If you require a different location **within** the repository, @@ -59,7 +59,7 @@ job log using a regular expression. In the pipelines settings, search for the ![Pipelines settings test coverage](img/pipelines_settings_test_coverage.png) Leave blank if you want to disable it or enter a ruby regular expression. You -can use http://rubular.com to test your regex. +can use <http://rubular.com> to test your regex. If the pipeline succeeds, the coverage is shown in the merge request widget and in the jobs table. @@ -79,28 +79,28 @@ project setting under your project's **Settings > CI/CD > General pipelines sett If **Public pipelines** is enabled (default): -- for **public** projects, anyone can view the pipelines and access the job details - (output logs and artifacts) -- for **internal** projects, any logged in user can view the pipelines +- For **public** projects, anyone can view the pipelines and access the job details + (output logs and artifacts). +- For **internal** projects, any logged in user can view the pipelines and access the job details - (output logs and artifacts) -- for **private** projects, any member (guest or higher) can view the pipelines + (output logs and artifacts). +- For **private** projects, any member (guest or higher) can view the pipelines and access the job details - (output logs and artifacts) + (output logs and artifacts). If **Public pipelines** is disabled: -- for **public** projects, anyone can view the pipelines, but only members - (reporter or higher) can access the job details (output logs and artifacts) -- for **internal** projects, any logged in user can view the pipelines, - but only members (reporter or higher) can access the job details (output logs - and artifacts) -- for **private** projects, only members (reporter or higher) - can view the pipelines and access the job details (output logs and artifacts) +- For **public** projects, anyone can view the pipelines, but only members + (reporter or higher) can access the job details (output logs and artifacts). +- For **internal** projects, any logged in user can view the pipelines. + However, only members (reporter or higher) can access the job details (output logs + and artifacts). +- For **private** projects, only members (reporter or higher) + can view the pipelines and access the job details (output logs and artifacts). ## Auto-cancel pending pipelines -> [Introduced][ce-9362] in GitLab 9.1. +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9362) in GitLab 9.1. If you want to auto-cancel all pending non-HEAD pipelines on branch, when new pipeline will be created (after your git push or manually from UI), @@ -132,19 +132,19 @@ Depending on the status of your job, a badge can have the following values: You can access a pipeline status badge image using the following link: -``` +```text https://example.gitlab.com/<namespace>/<project>/badges/<branch>/build.svg ``` ### Test coverage report badge -GitLab makes it possible to define the regular expression for [coverage report], +GitLab makes it possible to define the regular expression for [coverage report](#test-coverage-parsing), that each job log will be matched against. This means that each job in the pipeline can have the test coverage percentage value defined. The test coverage badge can be accessed using following link: -``` +```text https://example.gitlab.com/<namespace>/<project>/badges/<branch>/coverage.svg ``` @@ -157,13 +157,28 @@ into your `README.md`: ![coverage](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/coverage.svg?job=coverage) ``` -### Environment Variables +### Badge styles -[Environment variables](../../../ci/variables/README.html#variables) can be set in an environment to be available to a runner. +Pipeline badges can be rendered in different styles by adding the `style=style_name` parameter to the URL. Currently two styles are available: + +#### Flat (default) + +```text +https://example.gitlab.com/<namespace>/<project>/badges/<branch>/coverage.svg?style=flat +``` + +![Badge flat style](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/coverage.svg?job=coverage&style=flat) -[var]: ../../../ci/yaml/README.md#git-strategy -[coverage report]: #test-coverage-parsing -[timeout overriding]: ../../../ci/runners/README.html#setting-maximum-job-timeout-for-a-runner -[ce-9362]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9362 -[ce-12509]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/12509 -[ce-17221]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/17221 +#### Flat square + +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/30120) in GitLab 11.8. + +```text +https://example.gitlab.com/<namespace>/<project>/badges/<branch>/coverage.svg?style=flat-square +``` + +![Badge flat square style](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/coverage.svg?job=coverage&style=flat-square) + +## Environment Variables + +[Environment variables](../../../ci/variables/README.html#variables) can be set in an environment to be available to a runner. diff --git a/doc/user/project/repository/branches/index.md b/doc/user/project/repository/branches/index.md index 783081cec26..f05554ffc5b 100644 --- a/doc/user/project/repository/branches/index.md +++ b/doc/user/project/repository/branches/index.md @@ -2,16 +2,17 @@ Read through GiLab's branching documentation: -- [Create a branch](../web_editor.md#create-a-new-branch) -- [Default branch](#default-branch) -- [Protected branches](../../protected_branches.md#protected-branches) -- [Delete merged branches](#delete-merged-branches) -- [Branch filter search box](#branch-filter-search-box) +- [Create a branch](../web_editor.md#create-a-new-branch). +- [Default branch](#default-branch). +- [Protected branches](../../protected_branches.md#protected-branches). +- [Delete merged branches](#delete-merged-branches). +- [Branch filter search box](#branch-filter-search-box). See also: -- [GitLab Flow](../../../../university/training/gitlab_flow.md#gitlab-flow): use the best of GitLab for your branching strategies -- [Getting started with Git](../../../../topics/git/index.md) and GitLab +- [Branches API](../../../../api/branches.md), for information on operating on repository branches using the GitLab API. +- [GitLab Flow](../../../../university/training/gitlab_flow.md#gitlab-flow). Use the best of GitLab for your branching strategies. +- [Getting started with Git](../../../../topics/git/index.md) and GitLab. ## Default branch @@ -41,7 +42,6 @@ this operation. It's particularly useful to clean up old branches that were not deleted automatically when a merge request was merged. - ## Branch filter search box > [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/22166) in GitLab 11.5. diff --git a/lib/api/api.rb b/lib/api/api.rb index 59b67c67f9d..a768b78cda5 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -110,6 +110,7 @@ module API mount ::API::GroupMilestones mount ::API::Groups mount ::API::GroupVariables + mount ::API::ImportGithub mount ::API::Internal mount ::API::Issues mount ::API::JobArtifacts diff --git a/lib/api/entities.rb b/lib/api/entities.rb index a2a3c0a16d7..e0a48908122 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -115,6 +115,9 @@ module API expose :group_name do |group_link, options| group_link.group.name end + expose :group_full_path do |group_link, options| + group_link.group.full_path + end expose :group_access, as: :group_access_level expose :expires_at end @@ -277,7 +280,7 @@ module API # N+1 is solved then by using `subject.tags.map(&:name)` # MR describing the solution: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/20555 super(projects_relation).preload(:group) - .preload(project_group_links: :group, + .preload(project_group_links: { group: :route }, fork_network: :root_project, fork_network_member: :forked_from_project, forked_from_project: [:route, :forks, :tags, namespace: :route]) @@ -341,19 +344,23 @@ module API class GroupDetail < Group expose :projects, using: Entities::Project do |group, options| - GroupProjectsFinder.new( + projects = GroupProjectsFinder.new( group: group, current_user: options[:current_user], options: { only_owned: true } ).execute + + Entities::Project.prepare_relation(projects) end expose :shared_projects, using: Entities::Project do |group, options| - GroupProjectsFinder.new( + projects = GroupProjectsFinder.new( group: group, current_user: options[:current_user], options: { only_shared: true } ).execute + + Entities::Project.prepare_relation(projects) end end @@ -961,7 +968,7 @@ module API if options[:group_members] options[:group_members].find { |member| member.source_id == project.namespace_id } else - project.group.group_member(options[:current_user]) + project.group.highest_group_member(options[:current_user]) end end end diff --git a/lib/api/features.rb b/lib/api/features.rb index 1331248699f..835aac05905 100644 --- a/lib/api/features.rb +++ b/lib/api/features.rb @@ -16,15 +16,13 @@ module API end end - # rubocop: disable CodeReuse/ActiveRecord def gate_targets(params) - targets = [] - targets << Feature.group(params[:feature_group]) if params[:feature_group] - targets << UserFinder.new(params[:user]).find_by_username if params[:user] + Feature::Target.new(params).targets + end - targets + def gate_specified?(params) + Feature::Target.new(params).gate_specified? end - # rubocop: enable CodeReuse/ActiveRecord end resource :features do @@ -44,6 +42,7 @@ module API requires :value, type: String, desc: '`true` or `false` to enable/disable, an integer for percentage of time' optional :feature_group, type: String, desc: 'A Feature group name' optional :user, type: String, desc: 'A GitLab username' + optional :project, type: String, desc: 'A projects path, like gitlab-org/gitlab-ce' end post ':name' do feature = Feature.get(params[:name]) @@ -52,13 +51,13 @@ module API case value when true - if targets.present? + if gate_specified?(params) targets.each { |target| feature.enable(target) } else feature.enable end when false - if targets.present? + if gate_specified?(params) targets.each { |target| feature.disable(target) } else feature.disable diff --git a/lib/api/import_github.rb b/lib/api/import_github.rb new file mode 100644 index 00000000000..bb4e536cf57 --- /dev/null +++ b/lib/api/import_github.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +module API + class ImportGithub < Grape::API + rescue_from Octokit::Unauthorized, with: :provider_unauthorized + + helpers do + def client + @client ||= Gitlab::LegacyGithubImport::Client.new(params[:personal_access_token], client_options) + end + + def access_params + { github_access_token: params[:personal_access_token] } + end + + def client_options + {} + end + + def provider + :github + end + end + + desc 'Import a GitHub project' do + detail 'This feature was introduced in GitLab 11.3.4.' + success Entities::ProjectEntity + end + params do + requires :personal_access_token, type: String, desc: 'GitHub personal access token' + requires :repo_id, type: Integer, desc: 'GitHub repository ID' + optional :new_name, type: String, desc: 'New repo name' + requires :target_namespace, type: String, desc: 'Namespace to import repo into' + end + post 'import/github' do + result = Import::GithubService.new(client, current_user, params).execute(access_params, provider) + + if result[:status] == :success + present ProjectSerializer.new.represent(result[:project]) + else + status result[:http_status] + { errors: result[:message] } + end + end + end +end diff --git a/lib/api/settings.rb b/lib/api/settings.rb index f53ba0ab761..95371961398 100644 --- a/lib/api/settings.rb +++ b/lib/api/settings.rb @@ -35,7 +35,7 @@ module API end optional :container_registry_token_expire_delay, type: Integer, desc: 'Authorization token duration (minutes)' optional :default_artifacts_expire_in, type: String, desc: "Set the default expiration time for each job's artifacts" - optional :default_branch_protection, type: Integer, values: [0, 1, 2], desc: 'Determine if developers can push to master' + optional :default_branch_protection, type: Integer, values: Gitlab::Access.protection_values, desc: 'Determine if developers can push to master' optional :default_group_visibility, type: String, values: Gitlab::VisibilityLevel.string_values, desc: 'The default group visibility' optional :default_project_visibility, type: String, values: Gitlab::VisibilityLevel.string_values, desc: 'The default project visibility' optional :default_projects_limit, type: Integer, desc: 'The maximum number of personal projects' diff --git a/lib/api/tags.rb b/lib/api/tags.rb index aacdca3871a..f5359fd316c 100644 --- a/lib/api/tags.rb +++ b/lib/api/tags.rb @@ -20,12 +20,15 @@ module API desc: 'Return tags sorted in updated by `asc` or `desc` order.' optional :order_by, type: String, values: %w[name updated], default: 'updated', desc: 'Return tags ordered by `name` or `updated` fields.' + optional :search, type: String, desc: 'Return list of tags matching the search criteria' use :pagination end get ':id/repository/tags' do - tags = ::Kaminari.paginate_array(::TagsFinder.new(user_project.repository, sort: "#{params[:order_by]}_#{params[:sort]}").execute) + tags = ::TagsFinder.new(user_project.repository, + sort: "#{params[:order_by]}_#{params[:sort]}", + search: params[:search]).execute - present paginate(tags), with: Entities::Tag, project: user_project + present paginate(::Kaminari.paginate_array(tags)), with: Entities::Tag, project: user_project end desc 'Get a single repository tag' do diff --git a/lib/api/wikis.rb b/lib/api/wikis.rb index 302b2797a34..ef0e3decc2c 100644 --- a/lib/api/wikis.rb +++ b/lib/api/wikis.rb @@ -22,7 +22,9 @@ module API end end - resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do + WIKI_ENDPOINT_REQUIREMENTS = API::NAMESPACE_OR_PROJECT_REQUIREMENTS.merge(slug: API::NO_SLASH_URL_PART_REGEX) + + resource :projects, requirements: WIKI_ENDPOINT_REQUIREMENTS do desc 'Get a list of wiki pages' do success Entities::WikiPageBasic end @@ -103,7 +105,7 @@ module API requires :file, type: ::API::Validations::Types::SafeFile, desc: 'The attachment file to be uploaded' optional :branch, type: String, desc: 'The name of the branch' end - post ":id/wikis/attachments", requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do + post ":id/wikis/attachments" do authorize! :create_wiki, user_project result = ::Wikis::CreateAttachmentService.new(user_project, diff --git a/lib/backup/files.rb b/lib/backup/files.rb index 0032ae8f84b..2bac84846c5 100644 --- a/lib/backup/files.rb +++ b/lib/backup/files.rb @@ -71,8 +71,14 @@ module Backup end def run_pipeline!(cmd_list, options = {}) - status_list = Open3.pipeline(*cmd_list, options) - raise Backup::Error, 'Backup failed' unless status_list.compact.all?(&:success?) + err_r, err_w = IO.pipe + options[:err] = err_w + status = Open3.pipeline(*cmd_list, options) + err_w.close + return if status.compact.all?(&:success?) + + regex = /^g?tar: \.: Cannot mkdir: No such file or directory$/ + raise Backup::Error, 'Backup failed' unless err_r.read =~ regex end end end diff --git a/lib/banzai/filter/footnote_filter.rb b/lib/banzai/filter/footnote_filter.rb new file mode 100644 index 00000000000..97527976437 --- /dev/null +++ b/lib/banzai/filter/footnote_filter.rb @@ -0,0 +1,68 @@ +# frozen_string_literal: true + +module Banzai + module Filter + # HTML Filter for footnotes + # + # Footnotes are supported in CommonMark. However we were stripping + # the ids during sanitization. Those are now allowed. + # + # Footnotes are numbered the same - the first one has `id=fn1`, the + # second is `id=fn2`, etc. In order to allow footnotes when rendering + # multiple markdown blocks on a page, we need to make each footnote + # reference unique. + # + # This filter adds a random number to each footnote (the same number + # can be used for a single render). So you get `id=fn1-4335` and `id=fn2-4335`. + # + class FootnoteFilter < HTML::Pipeline::Filter + INTEGER_PATTERN = /\A\d+\z/.freeze + FOOTNOTE_ID_PREFIX = 'fn'.freeze + FOOTNOTE_LINK_ID_PREFIX = 'fnref'.freeze + FOOTNOTE_LI_REFERENCE_PATTERN = /\A#{FOOTNOTE_ID_PREFIX}\d+\z/.freeze + FOOTNOTE_LINK_REFERENCE_PATTERN = /\A#{FOOTNOTE_LINK_ID_PREFIX}\d+\z/.freeze + FOOTNOTE_START_NUMBER = 1 + + def call + return doc unless first_footnote = doc.at_css("ol > li[id=#{fn_id(FOOTNOTE_START_NUMBER)}]") + + # Sanitization stripped off the section wrapper - add it back in + first_footnote.parent.wrap('<section class="footnotes">') + rand_suffix = "-#{random_number}" + + doc.css('sup > a[id]').each do |link_node| + ref_num = link_node[:id].delete_prefix(FOOTNOTE_LINK_ID_PREFIX) + footnote_node = doc.at_css("li[id=#{fn_id(ref_num)}]") + backref_node = footnote_node.at_css("a[href=\"##{fnref_id(ref_num)}\"]") + + if ref_num =~ INTEGER_PATTERN && footnote_node && backref_node + link_node[:href] += rand_suffix + link_node[:id] += rand_suffix + footnote_node[:id] += rand_suffix + backref_node[:href] += rand_suffix + + # Sanitization stripped off class - add it back in + link_node.parent.append_class('footnote-ref') + backref_node.append_class('footnote-backref') + end + end + + doc + end + + private + + def random_number + @random_number ||= rand(10000) + end + + def fn_id(num) + "#{FOOTNOTE_ID_PREFIX}#{num}" + end + + def fnref_id(num) + "#{FOOTNOTE_LINK_ID_PREFIX}#{num}" + end + end + end +end diff --git a/lib/banzai/filter/milestone_reference_filter.rb b/lib/banzai/filter/milestone_reference_filter.rb index c70c3f0c04e..fce042e8946 100644 --- a/lib/banzai/filter/milestone_reference_filter.rb +++ b/lib/banzai/filter/milestone_reference_filter.rb @@ -101,9 +101,9 @@ module Banzai def self_and_ancestors_ids(parent) if group_context?(parent) - parent.self_and_ancestors_ids + parent.self_and_ancestors.select(:id) elsif project_context?(parent) - parent.group&.self_and_ancestors_ids + parent.group&.self_and_ancestors&.select(:id) end end diff --git a/lib/banzai/filter/sanitization_filter.rb b/lib/banzai/filter/sanitization_filter.rb index 8ba09290e6d..edc053638a8 100644 --- a/lib/banzai/filter/sanitization_filter.rb +++ b/lib/banzai/filter/sanitization_filter.rb @@ -8,8 +8,8 @@ module Banzai class SanitizationFilter < HTML::Pipeline::SanitizationFilter include Gitlab::Utils::StrongMemoize - UNSAFE_PROTOCOLS = %w(data javascript vbscript).freeze - TABLE_ALIGNMENT_PATTERN = /text-align: (?<alignment>center|left|right)/ + UNSAFE_PROTOCOLS = %w(data javascript vbscript).freeze + TABLE_ALIGNMENT_PATTERN = /text-align: (?<alignment>center|left|right)/.freeze def whitelist strong_memoize(:whitelist) do @@ -45,10 +45,9 @@ module Banzai whitelist[:attributes][:all].delete('name') whitelist[:attributes]['a'].push('name') - # Allow any protocol in `a` elements... + # Allow any protocol in `a` elements + # and then remove links with unsafe protocols whitelist[:protocols].delete('a') - - # ...but then remove links with unsafe protocols whitelist[:transformers].push(self.class.remove_unsafe_links) # Remove `rel` attribute from `a` elements @@ -57,6 +56,12 @@ module Banzai # Remove any `style` properties not required for table alignment whitelist[:transformers].push(self.class.remove_unsafe_table_style) + # Allow `id` in a and li elements for footnotes + # and remove any `id` properties not matching for footnotes + whitelist[:attributes]['a'].push('id') + whitelist[:attributes]['li'] = %w(id) + whitelist[:transformers].push(self.class.remove_non_footnote_ids) + whitelist end @@ -112,6 +117,20 @@ module Banzai end end end + + def remove_non_footnote_ids + lambda do |env| + node = env[:node] + + return unless node.name == 'a' || node.name == 'li' + return unless node.has_attribute?('id') + + return if node.name == 'a' && node['id'] =~ Banzai::Filter::FootnoteFilter::FOOTNOTE_LINK_REFERENCE_PATTERN + return if node.name == 'li' && node['id'] =~ Banzai::Filter::FootnoteFilter::FOOTNOTE_LI_REFERENCE_PATTERN + + node.remove_attribute('id') + end + end end end end diff --git a/lib/banzai/pipeline/gfm_pipeline.rb b/lib/banzai/pipeline/gfm_pipeline.rb index 5f13a6d6cde..d860dad0b6c 100644 --- a/lib/banzai/pipeline/gfm_pipeline.rb +++ b/lib/banzai/pipeline/gfm_pipeline.rb @@ -30,6 +30,7 @@ module Banzai Filter::AutolinkFilter, Filter::ExternalLinkFilter, Filter::SuggestionFilter, + Filter::FootnoteFilter, *reference_filters, diff --git a/lib/bitbucket_server/client.rb b/lib/bitbucket_server/client.rb index 83e8808db07..6a608058813 100644 --- a/lib/bitbucket_server/client.rb +++ b/lib/bitbucket_server/client.rb @@ -4,18 +4,6 @@ module BitbucketServer class Client attr_reader :connection - ServerError = Class.new(StandardError) - - SERVER_ERRORS = [SocketError, - OpenSSL::SSL::SSLError, - Errno::ECONNRESET, - Errno::ECONNREFUSED, - Errno::EHOSTUNREACH, - Net::OpenTimeout, - Net::ReadTimeout, - Gitlab::HTTP::BlockedUrlError, - BitbucketServer::Connection::ConnectionError].freeze - def initialize(options = {}) @connection = Connection.new(options) end @@ -64,8 +52,6 @@ module BitbucketServer def get_collection(path, type, page_offset: 0, limit: nil) paginator = BitbucketServer::Paginator.new(connection, Addressable::URI.escape(path), type, page_offset: page_offset, limit: limit) BitbucketServer::Collection.new(paginator) - rescue *SERVER_ERRORS => e - raise ServerError, e end end end diff --git a/lib/bitbucket_server/connection.rb b/lib/bitbucket_server/connection.rb index 7efcdcf8619..9c14b26c65a 100644 --- a/lib/bitbucket_server/connection.rb +++ b/lib/bitbucket_server/connection.rb @@ -7,6 +7,17 @@ module BitbucketServer DEFAULT_API_VERSION = '1.0' SEPARATOR = '/' + NETWORK_ERRORS = [ + SocketError, + OpenSSL::SSL::SSLError, + Errno::ECONNRESET, + Errno::ECONNREFUSED, + Errno::EHOSTUNREACH, + Net::OpenTimeout, + Net::ReadTimeout, + Gitlab::HTTP::BlockedUrlError + ].freeze + attr_reader :api_version, :base_uri, :username, :token ConnectionError = Class.new(StandardError) @@ -27,6 +38,8 @@ module BitbucketServer check_errors!(response) response.parsed_response + rescue *NETWORK_ERRORS => e + raise ConnectionError, e end def post(path, body) @@ -38,6 +51,8 @@ module BitbucketServer check_errors!(response) response.parsed_response + rescue *NETWORK_ERRORS => e + raise ConnectionError, e end # We need to support two different APIs for deletion: @@ -55,6 +70,8 @@ module BitbucketServer check_errors!(response) response.parsed_response + rescue *NETWORK_ERRORS => e + raise ConnectionError, e end private diff --git a/lib/feature.rb b/lib/feature.rb index e048a443abc..e59cd70f822 100644 --- a/lib/feature.rb +++ b/lib/feature.rb @@ -102,4 +102,42 @@ class Feature expires_in: 1.hour) end end + + class Target + attr_reader :params + + def initialize(params) + @params = params + end + + def gate_specified? + %i(user project feature_group).any? { |key| params.key?(key) } + end + + def targets + [feature_group, user, project].compact + end + + private + + # rubocop: disable CodeReuse/ActiveRecord + def feature_group + return unless params.key?(:feature_group) + + Feature.group(params[:feature_group]) + end + # rubocop: enable CodeReuse/ActiveRecord + + def user + return unless params.key?(:user) + + UserFinder.new(params[:user]).find_by_username! + end + + def project + return unless params.key?(:project) + + Project.find_by_full_path(params[:project]) + end + end end diff --git a/lib/gitlab.rb b/lib/gitlab.rb index 2ef54658a11..b91394f7f58 100644 --- a/lib/gitlab.rb +++ b/lib/gitlab.rb @@ -27,12 +27,52 @@ module Gitlab end end + def self.version_info + Gitlab::VersionInfo.parse(Gitlab::VERSION) + end + COM_URL = 'https://gitlab.com'.freeze 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.pre_release? + VERSION.include?('pre') + end + + def self.final_release? + !VERSION.include?('rc') && !pre_release? + end + + def self.minor_release + "#{version_info.major}.#{version_info.minor}" + end + + def self.prev_minor_release + "#{version_info.major}.#{version_info.minor - 1}" + end + + def self.prev_major_release + "#{version_info.major.to_i - 1}" + end + + def self.new_major_release? + version_info.minor.to_i.zero? + end + + def self.previous_release + if version_info.minor_version? + if version_info.patch_version? + minor_release + else + prev_minor_release + end + else + prev_major_release + end + end + def self.com? # Check `gl_subdomain?` as well to keep parity with gitlab.com Gitlab.config.gitlab.url == COM_URL || gl_subdomain? @@ -50,11 +90,11 @@ module Gitlab Rails.env.development? || org? || com? end - def self.pre_release? - VERSION.include?('pre') - end + def self.process_name + return 'sidekiq' if Sidekiq.server? + return 'console' if defined?(Rails::Console) + return 'test' if Rails.env.test? - def self.version_info - Gitlab::VersionInfo.parse(Gitlab::VERSION) + 'web' end end diff --git a/lib/gitlab/access/branch_protection.rb b/lib/gitlab/access/branch_protection.rb new file mode 100644 index 00000000000..f039e5c011f --- /dev/null +++ b/lib/gitlab/access/branch_protection.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +module Gitlab + module Access + # A wrapper around Integer based branch protection levels. + # + # This wrapper can be used to work with branch protection levels without + # having to directly refer to the constants. For example, instead of this: + # + # if access_level == Gitlab::Access::PROTECTION_DEV_CAN_PUSH + # ... + # end + # + # You can write this instead: + # + # protection = BranchProtection.new(access_level) + # + # if protection.developer_can_push? + # ... + # end + class BranchProtection + attr_reader :level + + # @param [Integer] level The branch protection level as an Integer. + def initialize(level) + @level = level + end + + def any? + level != PROTECTION_NONE + end + + def developer_can_push? + level == PROTECTION_DEV_CAN_PUSH + end + + def developer_can_merge? + level == PROTECTION_DEV_CAN_MERGE + end + end + end +end diff --git a/lib/gitlab/auth/ldap/person.rb b/lib/gitlab/auth/ldap/person.rb index a0244a3cea1..48d134f91b0 100644 --- a/lib/gitlab/auth/ldap/person.rb +++ b/lib/gitlab/auth/ldap/person.rb @@ -98,9 +98,7 @@ module Gitlab private - def entry - @entry - end + attr_reader :entry def config @config ||= Gitlab::Auth::LDAP::Config.new(provider) diff --git a/lib/gitlab/background_migration/backfill_project_repositories.rb b/lib/gitlab/background_migration/backfill_project_repositories.rb index aaf520d70f6..c8d83cc1803 100644 --- a/lib/gitlab/background_migration/backfill_project_repositories.rb +++ b/lib/gitlab/background_migration/backfill_project_repositories.rb @@ -83,7 +83,7 @@ module Gitlab extend ActiveSupport::Concern def full_path - @full_path ||= build_full_path + route&.path || build_full_path end def build_full_path @@ -99,7 +99,12 @@ module Gitlab end end - # Namespace model. + # Route model + class Route < ActiveRecord::Base + belongs_to :source, inverse_of: :route, polymorphic: true + end + + # Namespace model class Namespace < ActiveRecord::Base self.table_name = 'namespaces' self.inheritance_column = nil @@ -108,6 +113,8 @@ module Gitlab belongs_to :parent, class_name: 'Namespace', inverse_of: 'namespaces' + has_one :route, -> { where(source_type: 'Namespace') }, inverse_of: :source, foreign_key: :source_id + has_many :projects, inverse_of: :parent has_many :namespaces, inverse_of: :parent end @@ -134,6 +141,7 @@ module Gitlab belongs_to :parent, class_name: 'Namespace', foreign_key: :namespace_id, inverse_of: 'projects' + has_one :route, -> { where(source_type: 'Project') }, inverse_of: :source, foreign_key: :source_id has_one :project_repository, inverse_of: :project delegate :disk_path, to: :storage @@ -194,6 +202,8 @@ module Gitlab def project_repositories(start_id, stop_id) projects .without_project_repository + .includes(:route, parent: [:route]).references(:routes) + .includes(:parent).references(:namespaces) .where(id: start_id..stop_id) .map { |project| build_attributes_for_project(project) } .compact diff --git a/lib/gitlab/ci/config/entry/job.rb b/lib/gitlab/ci/config/entry/job.rb index 3239743bfff..1d8904f7b29 100644 --- a/lib/gitlab/ci/config/entry/job.rb +++ b/lib/gitlab/ci/config/entry/job.rb @@ -16,13 +16,6 @@ module Gitlab dependencies before_script after_script variables environment coverage retry parallel extends].freeze - DEFAULT_ONLY_POLICY = { - refs: %w(branches tags) - }.freeze - - DEFAULT_EXCEPT_POLICY = { - }.freeze - validations do validates :config, allowed_keys: ALLOWED_KEYS validates :config, presence: true @@ -73,7 +66,8 @@ module Gitlab description: 'Services that will be used to execute this job.' entry :only, Entry::Policy, - description: 'Refs policy this job will be executed for.' + description: 'Refs policy this job will be executed for.', + default: { refs: %w[branches tags] } entry :except, Entry::Policy, description: 'Refs policy this job will be executed for.' @@ -156,8 +150,8 @@ module Gitlab services: services_value, stage: stage_value, cache: cache_value, - only: DEFAULT_ONLY_POLICY.deep_merge(only_value.to_h), - except: DEFAULT_EXCEPT_POLICY.deep_merge(except_value.to_h), + only: only_value, + except: except_value, variables: variables_defined? ? variables_value : nil, environment: environment_defined? ? environment_value : nil, environment_name: environment_defined? ? environment_value[:name] : nil, diff --git a/lib/gitlab/ci/config/entry/policy.rb b/lib/gitlab/ci/config/entry/policy.rb index 998da1f6837..9c677bf6617 100644 --- a/lib/gitlab/ci/config/entry/policy.rb +++ b/lib/gitlab/ci/config/entry/policy.rb @@ -64,7 +64,8 @@ module Gitlab end end - def self.default + def value + default.to_h.deep_merge(subject.value.to_h) end end end diff --git a/lib/gitlab/ci/config/entry/retry.rb b/lib/gitlab/ci/config/entry/retry.rb index eaf8b38aa3c..e9cbcb31e21 100644 --- a/lib/gitlab/ci/config/entry/retry.rb +++ b/lib/gitlab/ci/config/entry/retry.rb @@ -82,9 +82,6 @@ module Gitlab 'retry config' end end - - def self.default - end end end end diff --git a/lib/gitlab/ci/config/entry/variables.rb b/lib/gitlab/ci/config/entry/variables.rb index 89d790ebfa6..c9d0c7cb568 100644 --- a/lib/gitlab/ci/config/entry/variables.rb +++ b/lib/gitlab/ci/config/entry/variables.rb @@ -14,7 +14,7 @@ module Gitlab validates :config, variables: true end - def self.default + def self.default(**) {} end diff --git a/lib/gitlab/ci/cron_parser.rb b/lib/gitlab/ci/cron_parser.rb index b1db9084662..94f4a4e36c9 100644 --- a/lib/gitlab/ci/cron_parser.rb +++ b/lib/gitlab/ci/cron_parser.rb @@ -35,7 +35,7 @@ module Gitlab # NOTE: # cron_timezone can only accept timezones listed in TZInfo::Timezone. # Aliases of Timezones from ActiveSupport::TimeZone are NOT accepted, - # because Rufus::Scheduler only supports TZInfo::Timezone. + # because Fugit::Cron only supports TZInfo::Timezone. # # For example, those codes have the same effect. # Time.zone = 'Pacific Time (US & Canada)' (ActiveSupport::TimeZone) @@ -47,10 +47,7 @@ module Gitlab # If you want to know more, please take a look # https://github.com/rails/rails/blob/master/activesupport/lib/active_support/values/time_zone.rb def try_parse_cron(cron, cron_timezone) - cron_line = Rufus::Scheduler.parse("#{cron} #{cron_timezone}") - cron_line if cron_line.is_a?(Rufus::Scheduler::CronLine) - rescue - # noop + Fugit::Cron.parse("#{cron} #{cron_timezone}") end end end diff --git a/lib/gitlab/ci/pipeline/chain/build.rb b/lib/gitlab/ci/pipeline/chain/build.rb index d33d1edfe35..41632211374 100644 --- a/lib/gitlab/ci/pipeline/chain/build.rb +++ b/lib/gitlab/ci/pipeline/chain/build.rb @@ -17,7 +17,6 @@ module Gitlab user: @command.current_user, pipeline_schedule: @command.schedule, merge_request: @command.merge_request, - protected: @command.protected_ref?, variables_attributes: Array(@command.variables_attributes) ) diff --git a/lib/gitlab/ci/pipeline/chain/populate.rb b/lib/gitlab/ci/pipeline/chain/populate.rb index 633d3cd4f6b..0405292a25b 100644 --- a/lib/gitlab/ci/pipeline/chain/populate.rb +++ b/lib/gitlab/ci/pipeline/chain/populate.rb @@ -13,6 +13,10 @@ module Gitlab # Allocate next IID. This operation must be outside of transactions of pipeline creations. pipeline.ensure_project_iid! + # Protect the pipeline. This is assigned in Populate instead of + # Build to prevent erroring out on ambiguous refs. + pipeline.protected = @command.protected_ref? + ## # Populate pipeline with block argument of CreatePipelineService#execute. # diff --git a/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml b/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml index e292641da2b..9362596eb60 100644 --- a/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml @@ -49,7 +49,7 @@ variables: POSTGRES_ENABLED: "true" POSTGRES_DB: $CI_ENVIRONMENT_SLUG - KUBERNETES_VERSION: 1.10.9 + KUBERNETES_VERSION: 1.11.6 HELM_VERSION: 2.11.0 DOCKER_DRIVER: overlay2 @@ -611,16 +611,16 @@ rollout 100%: track="${1-stable}" export APPLICATION_SECRET_NAME=$(application_secret_name "$track") - bash -c ' - function k8s_prefixed_variables() { - env | sed -n "s/^K8S_SECRET_\(.*\)$/\1/p" - } + env | sed -n "s/^K8S_SECRET_\(.*\)$/\1/p" > k8s_prefixed_variables + + kubectl create secret \ + -n "$KUBE_NAMESPACE" generic "$APPLICATION_SECRET_NAME" \ + --from-env-file k8s_prefixed_variables -o yaml --dry-run | + kubectl replace -n "$KUBE_NAMESPACE" --force -f - - kubectl create secret \ - -n "$KUBE_NAMESPACE" generic "$APPLICATION_SECRET_NAME" \ - --from-env-file <(k8s_prefixed_variables) -o yaml --dry-run | - kubectl replace -n "$KUBE_NAMESPACE" --force -f - - ' + export APPLICATION_SECRET_CHECKSUM=$(cat k8s_prefixed_variables | sha256sum | cut -d ' ' -f 1) + + rm k8s_prefixed_variables } function deploy_name() { @@ -667,6 +667,14 @@ rollout 100%: create_application_secret "$track" + env_slug=$(echo ${CI_ENVIRONMENT_SLUG//-/_} | tr -s '[:lower:]' '[:upper:]') + eval env_ADDITIONAL_HOSTS=\$${env_slug}_ADDITIONAL_HOSTS + if [ -n "$env_ADDITIONAL_HOSTS" ]; then + additional_hosts="{$env_ADDITIONAL_HOSTS}" + elif [ -n "$ADDITIONAL_HOSTS" ]; then + additional_hosts="{$ADDITIONAL_HOSTS}" + fi + if [[ -n "$DB_INITIALIZE" && -z "$(helm ls -q "^$name$")" ]]; then echo "Deploying first release with database initialization..." helm upgrade --install \ @@ -680,7 +688,9 @@ rollout 100%: --set application.track="$track" \ --set application.database_url="$DATABASE_URL" \ --set application.secretName="$APPLICATION_SECRET_NAME" \ + --set application.secretChecksum="$APPLICATION_SECRET_CHECKSUM" \ --set service.url="$CI_ENVIRONMENT_URL" \ + --set service.additionalHosts="$additional_hosts" \ --set replicaCount="$replicas" \ --set postgresql.enabled="$postgres_enabled" \ --set postgresql.nameOverride="postgres" \ @@ -713,7 +723,9 @@ rollout 100%: --set application.track="$track" \ --set application.database_url="$DATABASE_URL" \ --set application.secretName="$APPLICATION_SECRET_NAME" \ + --set application.secretChecksum="$APPLICATION_SECRET_CHECKSUM" \ --set service.url="$CI_ENVIRONMENT_URL" \ + --set service.additionalHosts="$additional_hosts" \ --set replicaCount="$replicas" \ --set postgresql.enabled="$postgres_enabled" \ --set postgresql.nameOverride="postgres" \ diff --git a/lib/gitlab/config/entry/configurable.rb b/lib/gitlab/config/entry/configurable.rb index afdb60b2cd5..37ba16dba25 100644 --- a/lib/gitlab/config/entry/configurable.rb +++ b/lib/gitlab/config/entry/configurable.rb @@ -56,6 +56,7 @@ module Gitlab def entry(key, entry, metadata) factory = ::Gitlab::Config::Entry::Factory.new(entry) .with(description: metadata[:description]) + .with(default: metadata[:default]) (@nodes ||= {}).merge!(key.to_sym => factory) end diff --git a/lib/gitlab/config/entry/factory.rb b/lib/gitlab/config/entry/factory.rb index 30d43c9f9a1..79f9ff32514 100644 --- a/lib/gitlab/config/entry/factory.rb +++ b/lib/gitlab/config/entry/factory.rb @@ -12,7 +12,7 @@ module Gitlab def initialize(entry) @entry = entry @metadata = {} - @attributes = {} + @attributes = { default: entry.default } end def value(value) @@ -21,12 +21,12 @@ module Gitlab end def metadata(metadata) - @metadata.merge!(metadata) + @metadata.merge!(metadata.compact) self end def with(attributes) - @attributes.merge!(attributes) + @attributes.merge!(attributes.compact) self end @@ -38,9 +38,7 @@ module Gitlab # See issue #18775. # if @value.nil? - Entry::Unspecified.new( - fabricate_unspecified - ) + Entry::Unspecified.new(fabricate_unspecified) else fabricate(@entry, @value) end @@ -53,10 +51,12 @@ module Gitlab # If entry has a default value we fabricate concrete node # with default value. # - if @entry.default.nil? + default = @attributes.fetch(:default) + + if default.nil? fabricate(Entry::Undefined) else - fabricate(@entry, @entry.default) + fabricate(@entry, default) end end @@ -64,6 +64,7 @@ module Gitlab entry.new(value, @metadata).tap do |node| node.key = @attributes[:key] node.parent = @attributes[:parent] + node.default = @attributes[:default] node.description = @attributes[:description] end end diff --git a/lib/gitlab/config/entry/node.rb b/lib/gitlab/config/entry/node.rb index 30357b2c95b..9999ab4ff95 100644 --- a/lib/gitlab/config/entry/node.rb +++ b/lib/gitlab/config/entry/node.rb @@ -10,7 +10,7 @@ module Gitlab InvalidError = Class.new(StandardError) attr_reader :config, :metadata - attr_accessor :key, :parent, :description + attr_accessor :key, :parent, :default, :description def initialize(config, **metadata) @config = config @@ -85,7 +85,7 @@ module Gitlab "#<#{self.class.name} #{unspecified}{#{key}: #{val.inspect}}>" end - def self.default + def self.default(**) end def self.aspects diff --git a/lib/gitlab/config/entry/simplifiable.rb b/lib/gitlab/config/entry/simplifiable.rb index 3e148fe2e91..5fbf7565e2a 100644 --- a/lib/gitlab/config/entry/simplifiable.rb +++ b/lib/gitlab/config/entry/simplifiable.rb @@ -6,6 +6,8 @@ module Gitlab class Simplifiable < SimpleDelegator EntryStrategy = Struct.new(:name, :condition) + attr_reader :subject + def initialize(config, **metadata) unless self.class.const_defined?(:UnknownStrategy) raise ArgumentError, 'UndefinedStrategy not available!' @@ -17,7 +19,7 @@ module Gitlab entry = self.class.entry_class(strategy) - super(entry.new(config, metadata)) + super(@subject = entry.new(config, metadata)) end def self.strategy(name, **opts) @@ -37,6 +39,9 @@ module Gitlab self::UnknownStrategy end end + + def self.default + end end end end diff --git a/lib/gitlab/git/bundle_file.rb b/lib/gitlab/git/bundle_file.rb new file mode 100644 index 00000000000..8384a436fcc --- /dev/null +++ b/lib/gitlab/git/bundle_file.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +module Gitlab + module Git + class BundleFile + # All git bundle files start with this string + # + # https://github.com/git/git/blob/v2.20.1/bundle.c#L15 + MAGIC = "# v2 git bundle\n" + + InvalidBundleError = Class.new(StandardError) + + attr_reader :filename + + def self.check!(filename) + new(filename).check! + end + + def initialize(filename) + @filename = filename + end + + def check! + data = File.open(filename, 'r') { |f| f.read(MAGIC.size) } + + raise InvalidBundleError, 'Invalid bundle file' unless data == MAGIC + end + end + end +end diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb index 5bbedc9d5e3..786c90f9272 100644 --- a/lib/gitlab/git/repository.rb +++ b/lib/gitlab/git/repository.rb @@ -789,6 +789,11 @@ module Gitlab end def create_from_bundle(bundle_path) + # It's important to check that the linked-to file is actually a valid + # .bundle file as it is passed to `git clone`, which may otherwise + # interpret it as a pointer to another repository + ::Gitlab::Git::BundleFile.check!(bundle_path) + gitaly_repository_client.create_from_bundle(bundle_path) end diff --git a/lib/gitlab/gitaly_client.rb b/lib/gitlab/gitaly_client.rb index 8bf8a3b53cd..85afbd85fe6 100644 --- a/lib/gitlab/gitaly_client.rb +++ b/lib/gitlab/gitaly_client.rb @@ -52,11 +52,18 @@ module Gitlab klass = stub_class(name) addr = stub_address(storage) creds = stub_creds(storage) - klass.new(addr, creds) + klass.new(addr, creds, interceptors: interceptors) end end end + def self.interceptors + return [] unless Gitlab::Tracing.enabled? + + [Gitlab::Tracing::GRPCInterceptor.instance] + end + private_class_method :interceptors + def self.stub_cert_paths cert_paths = Dir["#{OpenSSL::X509::DEFAULT_CERT_DIR}/*"] cert_paths << OpenSSL::X509::DEFAULT_CERT_FILE if File.exist? OpenSSL::X509::DEFAULT_CERT_FILE diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb index 15137140639..9b1794eec91 100644 --- a/lib/gitlab/gon_helper.rb +++ b/lib/gitlab/gon_helper.rb @@ -8,10 +8,7 @@ module Gitlab def add_gon_variables gon.api_version = 'v4' - gon.default_avatar_url = - Gitlab::Utils.append_path( - Gitlab.config.gitlab.url, - ActionController::Base.helpers.image_path('no_avatar.png')) + gon.default_avatar_url = default_avatar_url gon.max_file_size = Gitlab::CurrentSettings.max_attachment_size gon.asset_host = ActionController::Base.asset_host gon.webpack_public_path = webpack_public_path @@ -50,5 +47,15 @@ module Gitlab # use this method to push multiple feature flags. gon.push({ features: { var_name => enabled } }, true) end + + def default_avatar_url + # We can't use ActionController::Base.helpers.image_url because it + # doesn't return an actual URL because request is nil for some reason. + # + # We also can't use Gitlab::Utils.append_path because the image path + # may be an absolute URL. + URI.join(Gitlab.config.gitlab.url, + ActionController::Base.helpers.image_path('no_avatar.png')).to_s + end end end diff --git a/lib/gitlab/middleware/read_only/controller.rb b/lib/gitlab/middleware/read_only/controller.rb index f142f9da43d..817db12ac55 100644 --- a/lib/gitlab/middleware/read_only/controller.rb +++ b/lib/gitlab/middleware/read_only/controller.rb @@ -71,12 +71,16 @@ module Gitlab @route_hash ||= Rails.application.routes.recognize_path(request.url, { method: request.request_method }) rescue {} end + def relative_url + File.join('', Gitlab.config.gitlab.relative_url_root).chomp('/') + end + # Overridden in EE module def whitelisted_routes - grack_route || ReadOnly.internal_routes.any? { |path| request.path.include?(path) } || lfs_route || sidekiq_route + grack_route? || internal_route? || lfs_route? || sidekiq_route? end - def grack_route + def grack_route? # Calling route_hash may be expensive. Only do it if we think there's a possible match return false unless request.path.end_with?('.git/git-upload-pack', '.git/git-receive-pack') @@ -84,7 +88,11 @@ module Gitlab WHITELISTED_GIT_ROUTES[route_hash[:controller]]&.include?(route_hash[:action]) end - def lfs_route + def internal_route? + ReadOnly.internal_routes.any? { |path| request.path.include?(path) } + end + + def lfs_route? # Calling route_hash may be expensive. Only do it if we think there's a possible match unless request.path.end_with?('/info/lfs/objects/batch', '/info/lfs/locks', '/info/lfs/locks/verify') || @@ -95,8 +103,8 @@ module Gitlab WHITELISTED_GIT_LFS_ROUTES[route_hash[:controller]]&.include?(route_hash[:action]) end - def sidekiq_route - request.path.start_with?('/admin/sidekiq') + def sidekiq_route? + request.path.start_with?("#{relative_url}/admin/sidekiq") end end end diff --git a/lib/gitlab/release_blog_post.rb b/lib/gitlab/release_blog_post.rb new file mode 100644 index 00000000000..639aee61464 --- /dev/null +++ b/lib/gitlab/release_blog_post.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true +require 'singleton' + +module Gitlab + class ReleaseBlogPost + include Singleton + + RELEASE_RSS_URL = 'https://about.gitlab.com/releases.xml' + + def blog_post_url + @url ||= fetch_blog_post_url + end + + private + + def fetch_blog_post_url + installed_version = Gitlab.final_release? ? Gitlab.minor_release : Gitlab.previous_release + response = Gitlab::HTTP.get(RELEASE_RSS_URL, verify: false) + + return unless response.code == 200 + + blog_entry = find_installed_blog_entry(response, installed_version) + blog_entry['id'] if blog_entry + end + + def find_installed_blog_entry(response, installed_version) + response['feed']['entry'].find do |entry| + entry['release'] == installed_version || matches_previous_release_post(entry['release'], installed_version) + end + end + + def should_match_previous_release_post? + Gitlab.new_major_release? && !Gitlab.final_release? + end + + def matches_previous_release_post(rss_release_version, installed_version) + should_match_previous_release_post? && rss_release_version[/\d+/] == installed_version + end + end +end diff --git a/lib/gitlab/sentry.rb b/lib/gitlab/sentry.rb index 46d01964eac..956c16117f5 100644 --- a/lib/gitlab/sentry.rb +++ b/lib/gitlab/sentry.rb @@ -52,14 +52,6 @@ module Gitlab end end - def self.program_context - if Sidekiq.server? - 'sidekiq' - else - 'rails' - end - end - def self.should_raise_for_dev? Rails.env.development? || Rails.env.test? end diff --git a/lib/gitlab/sidekiq_logging/structured_logger.rb b/lib/gitlab/sidekiq_logging/structured_logger.rb index e86db8db3a1..fdc0d518c59 100644 --- a/lib/gitlab/sidekiq_logging/structured_logger.rb +++ b/lib/gitlab/sidekiq_logging/structured_logger.rb @@ -5,6 +5,7 @@ module Gitlab class StructuredLogger START_TIMESTAMP_FIELDS = %w[created_at enqueued_at].freeze DONE_TIMESTAMP_FIELDS = %w[started_at retried_at failed_at completed_at].freeze + MAXIMUM_JOB_ARGUMENTS_LENGTH = 10.kilobytes def call(job, queue) started_at = current_time @@ -64,6 +65,7 @@ module Gitlab job['pid'] = ::Process.pid job.delete('args') unless ENV['SIDEKIQ_LOG_ARGUMENTS'] + job['args'] = limited_job_args(job['args']) if job['args'] convert_to_iso8601(job, START_TIMESTAMP_FIELDS) @@ -93,6 +95,21 @@ module Gitlab Time.at(timestamp).utc.iso8601(3) end + + def limited_job_args(args) + return unless args.is_a?(Array) + + total_length = 0 + limited_args = args.take_while do |arg| + total_length += arg.to_json.length + + total_length <= MAXIMUM_JOB_ARGUMENTS_LENGTH + end + + limited_args.push('...') if total_length > MAXIMUM_JOB_ARGUMENTS_LENGTH + + limited_args + end end end end diff --git a/lib/gitlab/tracing.rb b/lib/gitlab/tracing.rb new file mode 100644 index 00000000000..3c4db42ac06 --- /dev/null +++ b/lib/gitlab/tracing.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Gitlab + module Tracing + # Only enable tracing when the `GITLAB_TRACING` env var is configured. Note that we avoid using ApplicationSettings since + # the same environment variable needs to be configured for Workhorse, Gitaly and any other components which + # emit tracing. Since other components may start before Rails, and may not have access to ApplicationSettings, + # an env var makes more sense. + def self.enabled? + connection_string.present? + end + + def self.connection_string + ENV['GITLAB_TRACING'] + end + end +end diff --git a/lib/gitlab/tracing/common.rb b/lib/gitlab/tracing/common.rb new file mode 100644 index 00000000000..5e2b12e3f90 --- /dev/null +++ b/lib/gitlab/tracing/common.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +module Gitlab + module Tracing + module Common + def tracer + OpenTracing.global_tracer + end + + # Convience method for running a block with a span + def in_tracing_span(operation_name:, tags:, child_of: nil) + scope = tracer.start_active_span( + operation_name, + child_of: child_of, + tags: tags + ) + span = scope.span + + # Add correlation details to the span if we have them + correlation_id = Gitlab::CorrelationId.current_id + if correlation_id + span.set_tag('correlation_id', correlation_id) + end + + begin + yield span + rescue => e + log_exception_on_span(span, e) + raise e + ensure + scope.close + end + end + + def log_exception_on_span(span, exception) + span.set_tag('error', true) + span.log_kv(kv_tags_for_exception(exception)) + end + + def kv_tags_for_exception(exception) + case exception + when Exception + { + 'event': 'error', + 'error.kind': exception.class.to_s, + 'message': Gitlab::UrlSanitizer.sanitize(exception.message), + 'stack': exception.backtrace.join("\n") + } + else + { + 'event': 'error', + 'error.kind': exception.class.to_s, + 'error.object': Gitlab::UrlSanitizer.sanitize(exception.to_s) + } + end + end + end + end +end diff --git a/lib/gitlab/tracing/factory.rb b/lib/gitlab/tracing/factory.rb new file mode 100644 index 00000000000..fc714164353 --- /dev/null +++ b/lib/gitlab/tracing/factory.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +require "cgi" + +module Gitlab + module Tracing + class Factory + OPENTRACING_SCHEME = "opentracing" + + def self.create_tracer(service_name, connection_string) + return unless connection_string.present? + + begin + opentracing_details = parse_connection_string(connection_string) + driver_name = opentracing_details[:driver_name] + + case driver_name + when "jaeger" + JaegerFactory.create_tracer(service_name, opentracing_details[:options]) + else + raise "Unknown driver: #{driver_name}" + end + rescue => e + # Can't create the tracer? Warn and continue sans tracer + warn "Unable to instantiate tracer: #{e}" + nil + end + end + + def self.parse_connection_string(connection_string) + parsed = URI.parse(connection_string) + + unless valid_uri?(parsed) + raise "Invalid tracing connection string" + end + + { + driver_name: parsed.host, + options: parse_query(parsed.query) + } + end + private_class_method :parse_connection_string + + def self.parse_query(query) + return {} unless query + + CGI.parse(query).symbolize_keys.transform_values(&:first) + end + private_class_method :parse_query + + def self.valid_uri?(uri) + return false unless uri + + uri.scheme == OPENTRACING_SCHEME && + uri.host.to_s =~ /^[a-z0-9_]+$/ && + uri.path.empty? + end + private_class_method :valid_uri? + end + end +end diff --git a/lib/gitlab/tracing/grpc_interceptor.rb b/lib/gitlab/tracing/grpc_interceptor.rb new file mode 100644 index 00000000000..6c2aab73125 --- /dev/null +++ b/lib/gitlab/tracing/grpc_interceptor.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +require 'opentracing' +require 'grpc' + +module Gitlab + module Tracing + class GRPCInterceptor < GRPC::ClientInterceptor + include Common + include Singleton + + def request_response(request:, call:, method:, metadata:) + wrap_with_tracing(method, 'unary', metadata) do + yield + end + end + + def client_streamer(requests:, call:, method:, metadata:) + wrap_with_tracing(method, 'client_stream', metadata) do + yield + end + end + + def server_streamer(request:, call:, method:, metadata:) + wrap_with_tracing(method, 'server_stream', metadata) do + yield + end + end + + def bidi_streamer(requests:, call:, method:, metadata:) + wrap_with_tracing(method, 'bidi_stream', metadata) do + yield + end + end + + private + + def wrap_with_tracing(method, grpc_type, metadata) + tags = { + 'component' => 'grpc', + 'span.kind' => 'client', + 'grpc.method' => method, + 'grpc.type' => grpc_type + } + + in_tracing_span(operation_name: "grpc:#{method}", tags: tags) do |span| + OpenTracing.inject(span.context, OpenTracing::FORMAT_TEXT_MAP, metadata) + + yield + end + end + end + end +end diff --git a/lib/gitlab/tracing/jaeger_factory.rb b/lib/gitlab/tracing/jaeger_factory.rb new file mode 100644 index 00000000000..2682007302a --- /dev/null +++ b/lib/gitlab/tracing/jaeger_factory.rb @@ -0,0 +1,97 @@ +# frozen_string_literal: true + +require 'jaeger/client' + +module Gitlab + module Tracing + class JaegerFactory + # When the probabilistic sampler is used, by default 0.1% of requests will be traced + DEFAULT_PROBABILISTIC_RATE = 0.001 + + # The default port for the Jaeger agent UDP listener + DEFAULT_UDP_PORT = 6831 + + # Reduce this from default of 10 seconds as the Ruby jaeger + # client doesn't have overflow control, leading to very large + # messages which fail to send over UDP (max packet = 64k) + # Flush more often, with smaller packets + FLUSH_INTERVAL = 5 + + def self.create_tracer(service_name, options) + kwargs = { + service_name: service_name, + sampler: get_sampler(options[:sampler], options[:sampler_param]), + reporter: get_reporter(service_name, options[:http_endpoint], options[:udp_endpoint]) + }.compact + + extra_params = options.except(:sampler, :sampler_param, :http_endpoint, :udp_endpoint, :strict_parsing, :debug) # rubocop: disable CodeReuse/ActiveRecord + if extra_params.present? + message = "jaeger tracer: invalid option: #{extra_params.keys.join(", ")}" + + if options[:strict_parsing] + raise message + else + warn message + end + end + + Jaeger::Client.build(kwargs) + end + + def self.get_sampler(sampler_type, sampler_param) + case sampler_type + when "probabilistic" + sampler_rate = sampler_param ? sampler_param.to_f : DEFAULT_PROBABILISTIC_RATE + Jaeger::Samplers::Probabilistic.new(rate: sampler_rate) + when "const" + const_value = sampler_param == "1" + Jaeger::Samplers::Const.new(const_value) + else + nil + end + end + private_class_method :get_sampler + + def self.get_reporter(service_name, http_endpoint, udp_endpoint) + encoder = Jaeger::Encoders::ThriftEncoder.new(service_name: service_name) + + if http_endpoint.present? + sender = get_http_sender(encoder, http_endpoint) + elsif udp_endpoint.present? + sender = get_udp_sender(encoder, udp_endpoint) + else + return nil + end + + Jaeger::Reporters::RemoteReporter.new( + sender: sender, + flush_interval: FLUSH_INTERVAL + ) + end + private_class_method :get_reporter + + def self.get_http_sender(encoder, address) + Jaeger::HttpSender.new( + url: address, + encoder: encoder, + logger: Logger.new(STDOUT) + ) + end + private_class_method :get_http_sender + + def self.get_udp_sender(encoder, address) + pair = address.split(":", 2) + host = pair[0] + port = pair[1] ? pair[1].to_i : DEFAULT_UDP_PORT + + Jaeger::UdpSender.new( + host: host, + port: port, + encoder: encoder, + logger: Logger.new(STDOUT) + ) + end + private_class_method :get_udp_sender + end + end +end diff --git a/lib/gitlab/tracing/rack_middleware.rb b/lib/gitlab/tracing/rack_middleware.rb new file mode 100644 index 00000000000..e6a31293f7b --- /dev/null +++ b/lib/gitlab/tracing/rack_middleware.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +require 'opentracing' + +module Gitlab + module Tracing + class RackMiddleware + include Common + + REQUEST_METHOD = 'REQUEST_METHOD' + + def initialize(app) + @app = app + end + + def call(env) + method = env[REQUEST_METHOD] + + context = tracer.extract(OpenTracing::FORMAT_RACK, env) + tags = { + 'component' => 'rack', + 'span.kind' => 'server', + 'http.method' => method, + 'http.url' => self.class.build_sanitized_url_from_env(env) + } + + in_tracing_span(operation_name: "http:#{method}", child_of: context, tags: tags) do |span| + @app.call(env).tap do |status_code, _headers, _body| + span.set_tag('http.status_code', status_code) + end + end + end + + # Generate a sanitized (safe) request URL from the rack environment + def self.build_sanitized_url_from_env(env) + request = ActionDispatch::Request.new(env) + + original_url = request.original_url + uri = URI.parse(original_url) + uri.query = request.filtered_parameters.to_query if uri.query.present? + + uri.to_s + end + end + end +end diff --git a/lib/gitlab/tracing/sidekiq/client_middleware.rb b/lib/gitlab/tracing/sidekiq/client_middleware.rb new file mode 100644 index 00000000000..2b71c1ea21e --- /dev/null +++ b/lib/gitlab/tracing/sidekiq/client_middleware.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +require 'opentracing' + +module Gitlab + module Tracing + module Sidekiq + class ClientMiddleware + include SidekiqCommon + + SPAN_KIND = 'client' + + def call(worker_class, job, queue, redis_pool) + in_tracing_span( + operation_name: "sidekiq:#{job['class']}", + tags: tags_from_job(job, SPAN_KIND)) do |span| + # Inject the details directly into the job + tracer.inject(span.context, OpenTracing::FORMAT_TEXT_MAP, job) + + yield + end + end + end + end + end +end diff --git a/lib/gitlab/tracing/sidekiq/server_middleware.rb b/lib/gitlab/tracing/sidekiq/server_middleware.rb new file mode 100644 index 00000000000..5b43c4310e6 --- /dev/null +++ b/lib/gitlab/tracing/sidekiq/server_middleware.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +require 'opentracing' + +module Gitlab + module Tracing + module Sidekiq + class ServerMiddleware + include SidekiqCommon + + SPAN_KIND = 'server' + + def call(worker, job, queue) + context = tracer.extract(OpenTracing::FORMAT_TEXT_MAP, job) + + in_tracing_span( + operation_name: "sidekiq:#{job['class']}", + child_of: context, + tags: tags_from_job(job, SPAN_KIND)) do |span| + yield + end + end + end + end + end +end diff --git a/lib/gitlab/tracing/sidekiq/sidekiq_common.rb b/lib/gitlab/tracing/sidekiq/sidekiq_common.rb new file mode 100644 index 00000000000..a911a29d773 --- /dev/null +++ b/lib/gitlab/tracing/sidekiq/sidekiq_common.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module Gitlab + module Tracing + module Sidekiq + module SidekiqCommon + include Gitlab::Tracing::Common + + def tags_from_job(job, kind) + { + 'component' => 'sidekiq', + 'span.kind' => kind, + 'sidekiq.queue' => job['queue'], + 'sidekiq.jid' => job['jid'], + 'sidekiq.retry' => job['retry'].to_s, + 'sidekiq.args' => job['args']&.join(", ") + } + end + end + end + end +end diff --git a/lib/gitlab/version_info.rb b/lib/gitlab/version_info.rb index aa6d5310161..142ead12c08 100644 --- a/lib/gitlab/version_info.rb +++ b/lib/gitlab/version_info.rb @@ -20,6 +20,14 @@ module Gitlab @patch = patch end + def minor_version? + minor.to_i > 0 + end + + def patch_version? + patch.to_i > 0 + end + def <=>(other) return unless other.is_a? VersionInfo return unless valid? && other.valid? diff --git a/lib/system_check/base_check.rb b/lib/system_check/base_check.rb index e06245294c4..46aad8aa885 100644 --- a/lib/system_check/base_check.rb +++ b/lib/system_check/base_check.rb @@ -70,18 +70,14 @@ module SystemCheck # multiple reasons why a check can fail # # @param [String] reason to be displayed - def skip_reason=(reason) - @skip_reason = reason - end + attr_writer :skip_reason # Skip reason defined during runtime # # This value have precedence over the one defined in the subclass # # @return [String] the reason - def skip_reason - @skip_reason - end + attr_reader :skip_reason # Does the check support automatically repair routine? # diff --git a/locale/gitlab.pot b/locale/gitlab.pot index e7aa7e453bd..55e2e82e13e 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -129,6 +129,9 @@ msgstr "" msgid "%{issuableType} will be removed! Are you sure?" msgstr "" +msgid "%{link_start}Read more%{link_end} about role permissions" +msgstr "" + msgid "%{loadingIcon} Started" msgstr "" @@ -399,6 +402,9 @@ msgstr "" msgid "Add reaction" msgstr "" +msgid "Add to project" +msgstr "" + msgid "Add todo" msgstr "" @@ -1305,6 +1311,9 @@ msgstr "" msgid "Choose a file" msgstr "" +msgid "Choose a role permission" +msgstr "" + msgid "Choose a template..." msgstr "" @@ -1464,6 +1473,9 @@ msgstr "" msgid "Closed" msgstr "" +msgid "ClusterIntegration| is the default environment scope for this cluster. This means that all jobs, regardless of their environment, will use this cluster. %{environment_scope_start}More information%{environment_scope_end}" +msgstr "" + msgid "ClusterIntegration|%{appList} was successfully installed on your Kubernetes cluster" msgstr "" @@ -1890,6 +1902,9 @@ msgstr "" msgid "ClusterIntegration|sign up" msgstr "" +msgid "Code" +msgstr "" + msgid "Cohorts" msgstr "" @@ -1928,9 +1943,15 @@ msgid_plural "Commits" msgstr[0] "" msgstr[1] "" +msgid "Commit %{commit_id}" +msgstr "" + msgid "Commit Message" msgstr "" +msgid "Commit deleted" +msgstr "" + msgid "Commit duration in minutes for last 30 commits" msgstr "" @@ -2770,6 +2791,9 @@ msgstr "" msgid "Enable the Performance Bar for a given group." msgstr "" +msgid "Enable two-factor authentication" +msgstr "" + msgid "Enable usage ping" msgstr "" @@ -2947,6 +2971,9 @@ msgstr "" msgid "Error occurred when toggling the notification subscription" msgstr "" +msgid "Error rendering markdown preview" +msgstr "" + msgid "Error saving label update." msgstr "" @@ -3010,6 +3037,9 @@ msgstr "" msgid "Existing folder" msgstr "" +msgid "Existing members and groups" +msgstr "" + msgid "Expand" msgstr "" @@ -3136,6 +3166,12 @@ msgstr "" msgid "Filter by two-factor authentication" msgstr "" +msgid "Filter results by group" +msgstr "" + +msgid "Filter results by project" +msgstr "" + msgid "Filter..." msgstr "" @@ -3145,6 +3181,9 @@ msgstr "" msgid "Find by path" msgstr "" +msgid "Find existing members by name" +msgstr "" + msgid "Find file" msgstr "" @@ -3397,6 +3436,9 @@ msgstr "" msgid "Group name" msgstr "" +msgid "Group:" +msgstr "" + msgid "Group: %{group_name}" msgstr "" @@ -3436,6 +3478,9 @@ msgstr "" msgid "Groups can also be nested by creating %{subgroup_docs_link_start}subgroups%{subgroup_docs_link_end}." msgstr "" +msgid "Groups with access to <strong>%{project_name}</strong>" +msgstr "" + msgid "GroupsDropdown|Frequently visited" msgstr "" @@ -3529,6 +3574,9 @@ msgstr "" msgid "Here is the public SSH key that needs to be added to the remote server. For more information, please refer to the documentation." msgstr "" +msgid "Hide file browser" +msgstr "" + msgid "Hide host keys manual input" msgstr "" @@ -3663,12 +3711,21 @@ msgstr "" msgid "Import issues" msgstr "" +msgid "Import members" +msgstr "" + +msgid "Import members from another project" +msgstr "" + msgid "Import multiple repositories by uploading a manifest file." msgstr "" msgid "Import project" msgstr "" +msgid "Import project members" +msgstr "" + msgid "Import projects from Bitbucket" msgstr "" @@ -3774,6 +3831,12 @@ msgstr "" msgid "Invite" msgstr "" +msgid "Invite group" +msgstr "" + +msgid "Invite member" +msgstr "" + msgid "Invoke Count" msgstr "" @@ -4114,6 +4177,9 @@ msgstr "" msgid "Manage project labels" msgstr "" +msgid "Manage two-factor authentication" +msgstr "" + msgid "Manifest" msgstr "" @@ -4195,6 +4261,12 @@ msgstr "" msgid "Members" msgstr "" +msgid "Members can be added by project <i>Maintainers</i> or <i>Owners</i>" +msgstr "" + +msgid "Members of <strong>%{project_name}</strong>" +msgstr "" + msgid "Merge Request" msgstr "" @@ -4742,6 +4814,9 @@ msgstr "" msgid "Only project members can comment." msgstr "" +msgid "Only project members will be imported. Group members will be skipped." +msgstr "" + msgid "Oops, are you sure?" msgstr "" @@ -4865,6 +4940,9 @@ msgstr "" msgid "Personal Access Token" msgstr "" +msgid "Personal project creation is not allowed. Please contact your administrator with questions" +msgstr "" + msgid "Pick a name" msgstr "" @@ -5144,6 +5222,12 @@ msgstr "" msgid "Profiles|Account scheduled for removal." msgstr "" +msgid "Profiles|Activate signin with one of the following services" +msgstr "" + +msgid "Profiles|Active" +msgstr "" + msgid "Profiles|Add key" msgstr "" @@ -5159,6 +5243,9 @@ msgstr "" msgid "Profiles|Change username" msgstr "" +msgid "Profiles|Changing your username can have unintended side effects." +msgstr "" + msgid "Profiles|Choose file..." msgstr "" @@ -5171,6 +5258,15 @@ msgstr "" msgid "Profiles|Clear status" msgstr "" +msgid "Profiles|Click on icon to activate signin with one of the following services" +msgstr "" + +msgid "Profiles|Connect" +msgstr "" + +msgid "Profiles|Connected Accounts" +msgstr "" + msgid "Profiles|Current path: %{path}" msgstr "" @@ -5189,6 +5285,9 @@ msgstr "" msgid "Profiles|Deleting an account has the following effects:" msgstr "" +msgid "Profiles|Disconnect" +msgstr "" + msgid "Profiles|Do not show on profile" msgstr "" @@ -5201,6 +5300,9 @@ msgstr "" msgid "Profiles|Enter your name, so people you know can recognize you" msgstr "" +msgid "Profiles|Increase your account's security by enabling Two-Factor Authentication (2FA)" +msgstr "" + msgid "Profiles|Invalid password" msgstr "" @@ -5237,6 +5339,9 @@ msgstr "" msgid "Profiles|Set new profile picture" msgstr "" +msgid "Profiles|Social sign-in" +msgstr "" + msgid "Profiles|Some options are unavailable for LDAP accounts" msgstr "" @@ -5264,6 +5369,9 @@ msgstr "" msgid "Profiles|This information will appear on your profile" msgstr "" +msgid "Profiles|Two-Factor Authentication" +msgstr "" + msgid "Profiles|Type your %{confirmationValue} to confirm:" msgstr "" @@ -5396,12 +5504,18 @@ msgstr "" msgid "Project export started. A download link will be sent by email." msgstr "" +msgid "Project members" +msgstr "" + msgid "Project name" msgstr "" msgid "Project slug" msgstr "" +msgid "Project:" +msgstr "" + msgid "ProjectActivityRSS|Subscribe" msgstr "" @@ -5964,6 +6078,9 @@ msgstr "" msgid "Search for projects, issues, etc." msgstr "" +msgid "Search groups" +msgstr "" + msgid "Search merge requests" msgstr "" @@ -5979,6 +6096,9 @@ msgstr "" msgid "Search project" msgstr "" +msgid "Search projects" +msgstr "" + msgid "Search users" msgstr "" @@ -6039,6 +6159,9 @@ msgstr "" msgid "Select branch/tag" msgstr "" +msgid "Select members to invite" +msgstr "" + msgid "Select project" msgstr "" @@ -6225,6 +6348,9 @@ msgstr "" msgid "Show complete raw log" msgstr "" +msgid "Show file browser" +msgstr "" + msgid "Show latest version" msgstr "" @@ -6272,6 +6398,9 @@ msgstr "" msgid "Slower but makes sure the project workspace is pristine as it clones the repository from scratch for every job" msgstr "" +msgid "Snippet Contents" +msgstr "" + msgid "Snippets" msgstr "" @@ -7218,6 +7347,9 @@ msgstr "" msgid "Title" msgstr "" +msgid "Titles and Filenames" +msgstr "" + msgid "To GitLab" msgstr "" @@ -7305,9 +7437,6 @@ msgstr "" msgid "Toggle discussion" msgstr "" -msgid "Toggle file browser" -msgstr "" - msgid "Toggle navigation" msgstr "" @@ -7641,6 +7770,9 @@ msgstr "" msgid "Want to see the data? Please ask an administrator for access." msgstr "" +msgid "We couldn't find any results matching" +msgstr "" + msgid "We detected potential spam in the %{humanized_resource_name}. Please solve the reCAPTCHA to proceed." msgstr "" @@ -7659,6 +7791,9 @@ msgstr "" msgid "Web terminal" msgstr "" +msgid "What's new?" +msgstr "" + msgid "When a runner is locked, it cannot be assigned to other projects" msgstr "" @@ -7863,6 +7998,9 @@ msgstr "" msgid "You can easily install a Runner on a Kubernetes cluster. %{link_to_help_page}" msgstr "" +msgid "You can invite a new member to <strong>%{project_name}</strong> or invite another group." +msgstr "" + msgid "You can move around the graph by using the arrow keys." msgstr "" @@ -8016,6 +8154,9 @@ msgstr "" msgid "Your name" msgstr "" +msgid "Your project limit is %{limit} projects! Please contact your administrator to increase it" +msgstr "" + msgid "Your projects" msgstr "" @@ -8040,6 +8181,9 @@ msgstr "" msgid "command line instructions" msgstr "" +msgid "commented on %{link_to_project}" +msgstr "" + msgid "confidentiality|You are going to turn off the confidentiality. This means <strong>everyone</strong> will be able to see and leave a comment on this issue." msgstr "" @@ -8113,6 +8257,12 @@ msgstr "" msgid "importing" msgstr "" +msgid "in group %{link_to_group}" +msgstr "" + +msgid "in project %{link_to_project}" +msgstr "" + msgid "issue boards" msgstr "" @@ -8365,6 +8515,9 @@ msgstr "" msgid "personal access token" msgstr "" +msgid "private" +msgstr "" + msgid "project" msgstr "" diff --git a/package.json b/package.json index 325341f1ca7..6c771e377b8 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "eslint-fix": "eslint --max-warnings 0 --report-unused-disable-directives --ext .js,.vue --fix .", "eslint-report": "eslint --max-warnings 0 --ext .js,.vue --format html --output-file ./eslint-report.html --no-inline-config .", "jest": "BABEL_ENV=jest jest", + "jsdoc": "jsdoc -c config/jsdocs.config.js", "karma": "BABEL_ENV=${BABEL_ENV:=karma} karma start --single-run true config/karma.config.js", "karma-coverage": "BABEL_ENV=coverage karma start --single-run true config/karma.config.js", "karma-start": "BABEL_ENV=karma karma start config/karma.config.js", @@ -20,12 +21,12 @@ }, "dependencies": { "@babel/core": "^7.2.2", - "@babel/plugin-proposal-class-properties": "^7.2.3", + "@babel/plugin-proposal-class-properties": "^7.3.0", "@babel/plugin-proposal-json-strings": "^7.2.0", - "@babel/plugin-proposal-private-methods": "^7.2.3", + "@babel/plugin-proposal-private-methods": "^7.3.0", "@babel/plugin-syntax-dynamic-import": "^7.2.0", "@babel/plugin-syntax-import-meta": "^7.2.0", - "@babel/preset-env": "^7.2.3", + "@babel/preset-env": "^7.3.1", "@gitlab/csslab": "^1.8.0", "@gitlab/svgs": "^1.47.0", "@gitlab/ui": "^1.20.0", @@ -75,7 +76,7 @@ "js-cookie": "^2.1.3", "jszip": "^3.1.3", "jszip-utils": "^0.0.2", - "katex": "^0.9.0", + "katex": "^0.10.0", "marked": "^0.3.12", "mermaid": "^8.0.0-rc.8", "monaco-editor": "^0.14.3", @@ -132,6 +133,7 @@ "babel-types": "^6.26.0", "chalk": "^2.4.1", "commander": "^2.18.0", + "docdash": "^1.0.2", "eslint": "~5.9.0", "eslint-import-resolver-jest": "^2.1.1", "eslint-import-resolver-webpack": "^0.10.1", @@ -148,6 +150,8 @@ "jasmine-jquery": "^2.1.1", "jest": "^23.6.0", "jest-junit": "^5.2.0", + "jsdoc": "^3.5.5", + "jsdoc-vue": "^1.0.0", "karma": "^3.0.0", "karma-chrome-launcher": "^2.2.0", "karma-coverage-istanbul-reporter": "^2.0.4", @@ -157,12 +161,14 @@ "karma-sourcemap-loader": "^0.3.7", "karma-webpack": "^4.0.0-beta.0", "nodemon": "^1.18.4", - "prettier": "1.15.2", + "pixelmatch": "^4.0.2", + "prettier": "1.16.1", "vue-jest": "^3.0.2", "webpack-dev-server": "^3.1.14", "yarn-deduplicate": "^1.0.5" }, "engines": { + "node": ">=8.10.0", "yarn": "^1.10.0" } } diff --git a/qa/Gemfile b/qa/Gemfile index 75ad7bd07af..873eac1013f 100644 --- a/qa/Gemfile +++ b/qa/Gemfile @@ -7,4 +7,4 @@ gem 'rake', '~> 12.3.0' gem 'rspec', '~> 3.7' gem 'selenium-webdriver', '~> 3.12' gem 'airborne', '~> 0.2.13' -gem 'nokogiri', '~> 1.8.5' +gem 'nokogiri', '~> 1.10.1' diff --git a/qa/Gemfile.lock b/qa/Gemfile.lock index 55f3211482b..419cacdb2af 100644 --- a/qa/Gemfile.lock +++ b/qa/Gemfile.lock @@ -44,11 +44,11 @@ GEM mime-types-data (~> 3.2015) mime-types-data (3.2016.0521) mini_mime (1.0.0) - mini_portile2 (2.3.0) + mini_portile2 (2.4.0) minitest (5.11.1) netrc (0.11.0) - nokogiri (1.8.5) - mini_portile2 (~> 2.3.0) + nokogiri (1.10.1) + mini_portile2 (~> 2.4.0) pry (0.11.3) coderay (~> 1.1.0) method_source (~> 0.9.0) @@ -97,7 +97,7 @@ DEPENDENCIES airborne (~> 0.2.13) capybara (~> 2.16.1) capybara-screenshot (~> 1.0.18) - nokogiri (~> 1.8.5) + nokogiri (~> 1.10.0) pry-byebug (~> 3.5.1) rake (~> 12.3.0) rspec (~> 3.7) diff --git a/qa/Rakefile b/qa/Rakefile new file mode 100644 index 00000000000..8df1cfdc174 --- /dev/null +++ b/qa/Rakefile @@ -0,0 +1,6 @@ +require_relative 'qa/tools/revoke_all_personal_access_tokens' + +desc "Revokes all personal access tokens" +task :revoke_personal_access_tokens do + QA::Tools::RevokeAllPersonalAccessTokens.new.run +end @@ -158,6 +158,10 @@ module QA autoload :Activity, 'qa/page/project/activity' autoload :Menu, 'qa/page/project/menu' + module Branches + autoload :Show, 'qa/page/project/branches/show' + end + module Commit autoload :Show, 'qa/page/project/commit/show' end @@ -191,6 +195,11 @@ module QA autoload :MirroringRepositories, 'qa/page/project/settings/mirroring_repositories' end + module SubMenus + autoload :Common, 'qa/page/project/sub_menus/common' + autoload :Repository, 'qa/page/project/sub_menus/repository' + end + module Issue autoload :New, 'qa/page/project/issue/new' autoload :Show, 'qa/page/project/issue/show' diff --git a/qa/qa/git/repository.rb b/qa/qa/git/repository.rb index 7f959441dac..ac8dcbf0d83 100644 --- a/qa/qa/git/repository.rb +++ b/qa/qa/git/repository.rb @@ -11,14 +11,15 @@ module QA class Repository include Scenario::Actable - attr_writer :password + attr_writer :password, :use_lfs attr_accessor :env_vars def initialize # We set HOME to the current working directory (which is a # temporary directory created in .perform()) so the temporarily dropped # .netrc can be utilised - self.env_vars = [%Q{HOME="#{File.dirname(netrc_file_path)}"}] + self.env_vars = [%Q{HOME="#{tmp_home_dir}"}] + @use_lfs = false end def self.perform(*args) @@ -33,25 +34,27 @@ module QA def username=(username) @username = username - @uri.user = username + # Only include the user in the URI if we're using HTTP as this breaks + # SSH authentication. + @uri.user = username unless ssh_key_set? end def use_default_credentials self.username, self.password = default_credentials - - add_credentials_to_netrc unless ssh_key_set? end def clone(opts = '') - run("git clone #{opts} #{uri} ./") - end + clone_result = run("git clone #{opts} #{uri} ./") + return clone_result.response unless clone_result.success + + enable_lfs_result = enable_lfs if use_lfs? - def checkout(branch_name) - run(%Q{git checkout "#{branch_name}"}) + clone_result.to_s + enable_lfs_result.to_s end - def checkout_new_branch(branch_name) - run(%Q{git checkout -b "#{branch_name}"}) + def checkout(branch_name, new_branch: false) + opts = new_branch ? '-b' : '' + run(%Q{git checkout #{opts} "#{branch_name}"}).to_s end def shallow_clone @@ -61,8 +64,6 @@ module QA def configure_identity(name, email) run(%Q{git config user.name #{name}}) run(%Q{git config user.email #{email}}) - - add_credentials_to_netrc end def commit_file(name, contents, message) @@ -73,19 +74,30 @@ module QA def add_file(name, contents) ::File.write(name, contents) - run(%Q{git add #{name}}) + if use_lfs? + git_lfs_track_result = run(%Q{git lfs track #{name} --lockable}) + return git_lfs_track_result.response unless git_lfs_track_result.success + end + + git_add_result = run(%Q{git add #{name}}) + + git_lfs_track_result.to_s + git_add_result.to_s end def commit(message) - run(%Q{git commit -m "#{message}"}) + run(%Q{git commit -m "#{message}"}).to_s end def push_changes(branch = 'master') - run("git push #{uri} #{branch}") + run("git push #{uri} #{branch}").to_s + end + + def merge(branch) + run("git merge #{branch}") end def commits - run('git log --oneline').split("\n") + run('git log --oneline').to_s.split("\n") end def use_ssh_key(key) @@ -97,7 +109,8 @@ module QA keyscan_params = ['-H'] keyscan_params << "-p #{uri.port}" if uri.port keyscan_params << uri.host - run("ssh-keyscan #{keyscan_params.join(' ')} >> #{known_hosts_file.path}") + res = run("ssh-keyscan #{keyscan_params.join(' ')} >> #{known_hosts_file.path}") + return res.response unless res.success? self.env_vars << %Q{GIT_SSH_COMMAND="ssh -i #{private_key_file.path} -o UserKnownHostsFile=#{known_hosts_file.path}"} end @@ -131,23 +144,66 @@ module QA output[/git< version (\d+)/, 1] || 'unknown' end + def try_add_credentials_to_netrc + return unless add_credentials? + return if netrc_already_contains_content? + + # Despite libcurl supporting a custom .netrc location through the + # CURLOPT_NETRC_FILE environment variable, git does not support it :( + # Info: https://curl.haxx.se/libcurl/c/CURLOPT_NETRC_FILE.html + # + # This will create a .netrc in the correct working directory, which is + # a temporary directory created in .perform() + # + FileUtils.mkdir_p(tmp_home_dir) + File.open(netrc_file_path, 'a') { |file| file.puts(netrc_content) } + File.chmod(0600, netrc_file_path) + end + private - attr_reader :uri, :username, :password, :known_hosts_file, :private_key_file + attr_reader :uri, :username, :password, :known_hosts_file, + :private_key_file, :use_lfs + + alias_method :use_lfs?, :use_lfs + + Result = Struct.new(:success, :response) do + alias_method :success?, :success + alias_method :to_s, :response + end + + def add_credentials? + return false if !username || !password + return true unless ssh_key_set? + return true if ssh_key_set? && use_lfs? + + false + end def ssh_key_set? !private_key_file.nil? end + def enable_lfs + # git lfs install *needs* a .gitconfig defined at ${HOME}/.gitconfig + FileUtils.mkdir_p(tmp_home_dir) + touch_gitconfig_result = run("touch #{tmp_home_dir}/.gitconfig") + return touch_gitconfig_result.response unless touch_gitconfig_result.success? + + git_lfs_install_result = run('git lfs install') + + touch_gitconfig_result.to_s + git_lfs_install_result.to_s + end + def run(command_str, *extra_env) command = [env_vars, *extra_env, command_str, '2>&1'].compact.join(' ') - Runtime::Logger.debug "Git: command=[#{command}]" + Runtime::Logger.debug "Git: pwd=[#{Dir.pwd}], command=[#{command}]" - output, _ = Open3.capture2(command) - output = output.chomp.gsub(/\s+$/, '') - Runtime::Logger.debug "Git: output=[#{output}]" + output, status = Open3.capture2e(command) + output.chomp! + Runtime::Logger.debug "Git: output=[#{output}], exitstatus=[#{status.exitstatus}]" - output + Result.new(status.exitstatus == 0, output) end def default_credentials @@ -158,12 +214,12 @@ module QA end end - def tmp_netrc_directory - @tmp_netrc_directory ||= File.join(Dir.tmpdir, "qa-netrc-credentials", $$.to_s) + def tmp_home_dir + @tmp_home_dir ||= File.join(Dir.tmpdir, "qa-netrc-credentials", $$.to_s) end def netrc_file_path - @netrc_file_path ||= File.join(tmp_netrc_directory, '.netrc') + @netrc_file_path ||= File.join(tmp_home_dir, '.netrc') end def netrc_content @@ -174,21 +230,6 @@ module QA File.exist?(netrc_file_path) && File.readlines(netrc_file_path).grep(/^#{netrc_content}$/).any? end - - def add_credentials_to_netrc - # Despite libcurl supporting a custom .netrc location through the - # CURLOPT_NETRC_FILE environment variable, git does not support it :( - # Info: https://curl.haxx.se/libcurl/c/CURLOPT_NETRC_FILE.html - # - # This will create a .netrc in the correct working directory, which is - # a temporary directory created in .perform() - # - return if netrc_already_contains_content? - - FileUtils.mkdir_p(tmp_netrc_directory) - File.open(netrc_file_path, 'a') { |file| file.puts(netrc_content) } - File.chmod(0600, netrc_file_path) - end end end end diff --git a/qa/qa/page/label/index.rb b/qa/qa/page/label/index.rb index 323acd57743..97ce8f0eba5 100644 --- a/qa/qa/page/label/index.rb +++ b/qa/qa/page/label/index.rb @@ -2,11 +2,26 @@ module QA module Page module Label class Index < Page::Base - view 'app/views/projects/labels/index.html.haml' do + view 'app/views/shared/labels/_nav.html.haml' do element :label_create_new end + view 'app/views/shared/empty_states/_labels.html.haml' do + element :label_svg + end + + view 'app/assets/javascripts/lazy_loader.js' do + element :js_lazy_loaded + end + def go_to_new_label + # The 'labels.svg' takes a fraction of a second to load after which the "New label" button shifts up a bit + # This can cause webdriver to miss the hit so we wait for the svg to load (implicitly with has_element?) + # before clicking the button. + within_element(:label_svg) do + has_element?(:js_lazy_loaded) + end + click_element :label_create_new end end diff --git a/qa/qa/page/profile/personal_access_tokens.rb b/qa/qa/page/profile/personal_access_tokens.rb index 9191dbe9cf3..8c12eff5cf1 100644 --- a/qa/qa/page/profile/personal_access_tokens.rb +++ b/qa/qa/page/profile/personal_access_tokens.rb @@ -3,29 +3,51 @@ module QA module Profile class PersonalAccessTokens < Page::Base view 'app/views/shared/_personal_access_tokens_form.html.haml' do - element :personal_access_token_name_field, 'text_field :name' # rubocop:disable QA/ElementWithPattern - element :create_token_button, 'submit "Create #{type} token"' # rubocop:disable QA/ElementWithPattern, Lint/InterpolationCheck - element :scopes_api_radios, "label :scopes" # rubocop:disable QA/ElementWithPattern + element :personal_access_token_name_field + element :create_token_button + end + + view 'app/views/shared/tokens/_scopes_form.html.haml' do + element :api_radio, 'qa-#{scope}-radio' # rubocop:disable QA/ElementWithPattern, Lint/InterpolationCheck end view 'app/views/shared/_personal_access_tokens_created_container.html.haml' do - element :create_token_field, "text_field_tag 'created-personal-access-token'" # rubocop:disable QA/ElementWithPattern + element :created_personal_access_token + end + view 'app/views/shared/_personal_access_tokens_table.html.haml' do + element :revoke_button end def fill_token_name(name) - fill_in 'personal_access_token_name', with: name + fill_element(:personal_access_token_name_field, name) end def check_api - check 'personal_access_token_scopes_api' + check_element(:api_radio) end def create_token - click_on 'Create personal access token' + click_element(:create_token_button) end def created_access_token - page.find('#created-personal-access-token').value + find_element(:created_personal_access_token, wait: 30).value + end + + def has_token_row_for_name?(token_name) + page.has_css?('tr', text: token_name, wait: 1.0) + end + + def first_token_row_for_name(token_name) + page.find('tr', text: token_name, match: :first, wait: 1.0) + end + + def revoke_first_token_with_name(token_name) + within first_token_row_for_name(token_name) do + accept_confirm do + click_element(:revoke_button) + end + end end end end diff --git a/qa/qa/page/project/branches/show.rb b/qa/qa/page/project/branches/show.rb new file mode 100644 index 00000000000..762a97e2088 --- /dev/null +++ b/qa/qa/page/project/branches/show.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +module QA + module Page + module Project + module Branches + class Show < Page::Base + view 'app/views/projects/branches/_branch.html.haml' do + element :remove_btn + end + view 'app/views/projects/branches/_panel.html.haml' do + element :all_branches + end + view 'app/views/projects/branches/index.html.haml' do + element :delete_merged_branches + end + + def delete_branch(branch_name) + within_element(:all_branches) do + within(".js-branch-#{branch_name}") do + accept_alert do + find_element(:remove_btn).click + end + end + end + end + + def has_branch_title?(branch_title) + within_element(:all_branches) do + within(".item-title") do + has_text?(branch_title) + end + end + end + + def has_branch_with_badge?(branch_name, badge) + within_element(:all_branches) do + within(".js-branch-#{branch_name} .badge") do + has_text?(badge) + end + end + end + + def delete_merged_branches + accept_alert do + click_element(:delete_merged_branches) + end + end + + def wait_for_texts_not_to_be_visible(texts) + text_not_visible = wait do + texts.all? do |text| + has_no_text?(text) + end + end + raise "Expected text(s) #{texts} not to be visible" unless text_not_visible + end + end + end + end + end +end diff --git a/qa/qa/page/project/menu.rb b/qa/qa/page/project/menu.rb index 835e1ed00b5..eb3426b1ac0 100644 --- a/qa/qa/page/project/menu.rb +++ b/qa/qa/page/project/menu.rb @@ -4,11 +4,14 @@ module QA module Page module Project class Menu < Page::Base + include SubMenus::Common + include SubMenus::Repository + view 'app/views/layouts/nav/sidebar/_project.html.haml' do element :settings_item element :settings_link, 'link_to edit_project_path' # rubocop:disable QA/ElementWithPattern - element :repository_link, "title: _('Repository')" # rubocop:disable QA/ElementWithPattern element :link_pipelines + element :link_operations element :link_members_settings element :pipelines_settings_link, "title: _('CI / CD')" # rubocop:disable QA/ElementWithPattern element :operations_kubernetes_link, "title: _('Kubernetes')" # rubocop:disable QA/ElementWithPattern @@ -18,7 +21,6 @@ module QA element :merge_requests_link, /link_to.*shortcuts-merge_requests/ # rubocop:disable QA/ElementWithPattern element :merge_requests_link_text, "Merge Requests" # rubocop:disable QA/ElementWithPattern element :top_level_items, '.sidebar-top-level-items' # rubocop:disable QA/ElementWithPattern - element :operations_section, "class: 'shortcuts-operations'" # rubocop:disable QA/ElementWithPattern element :activity_link, "title: _('Activity')" # rubocop:disable QA/ElementWithPattern element :wiki_link_text, "Wiki" # rubocop:disable QA/ElementWithPattern element :milestones_link @@ -85,12 +87,6 @@ module QA end end - def click_repository - within_sidebar do - click_link('Repository') - end - end - def click_repository_settings hover_settings do within_submenu do @@ -129,6 +125,7 @@ module QA def hover_issues within_sidebar do + scroll_to_element(:issues_item) find_element(:issues_item).hover yield @@ -137,7 +134,8 @@ module QA def hover_operations within_sidebar do - find('.shortcuts-operations').hover + scroll_to_element(:link_operations) + find_element(:link_operations).hover yield end @@ -145,20 +143,9 @@ module QA def hover_settings within_sidebar do - find('.qa-settings-item').hover - - yield - end - end - - def within_sidebar - page.within('.sidebar-top-level-items') do - yield - end - end + scroll_to_element(:settings_item) + find_element(:settings_item).hover - def within_submenu - page.within('.fly-out-list') do yield end end diff --git a/qa/qa/page/project/settings/deploy_keys.rb b/qa/qa/page/project/settings/deploy_keys.rb index 3c8c0cbdf7c..5da8d352e74 100644 --- a/qa/qa/page/project/settings/deploy_keys.rb +++ b/qa/qa/page/project/settings/deploy_keys.rb @@ -14,8 +14,13 @@ module QA end view 'app/assets/javascripts/deploy_keys/components/key.vue' do - element :key_title, /class=".*qa-key-title.*"/ # rubocop:disable QA/ElementWithPattern - element :key_fingerprint, /class=".*qa-key-fingerprint.*"/ # rubocop:disable QA/ElementWithPattern + element :key + element :key_title + element :key_fingerprint + end + + def add_key + click_on 'Add key' end def fill_key_title(title) @@ -26,31 +31,29 @@ module QA fill_in 'deploy_key_key', with: key end - def add_key - click_on 'Add key' - end - - def key_title + def find_fingerprint(title) within_project_deploy_keys do - find_element(:key_title).text + find_element(:key, title) + .find(element_selector_css(:key_fingerprint)).text end end - def key_fingerprint + def has_key?(title, fingerprint) within_project_deploy_keys do - find_element(:key_fingerprint).text + find_element(:key, title) + .has_css?(element_selector_css(:key_fingerprint), text: fingerprint) end end - def key_titles + def key_title within_project_deploy_keys do - all_elements(:key_title) + find_element(:key_title).text end end - def key_fingerprints + def key_fingerprint within_project_deploy_keys do - all_elements(:key_fingerprint) + find_element(:key_fingerprint).text end end @@ -58,7 +61,7 @@ module QA def within_project_deploy_keys wait(reload: false) do - has_css?(element_selector_css(:project_deploy_keys)) + has_element?(:project_deploy_keys) end within_element(:project_deploy_keys) do diff --git a/qa/qa/page/project/sub_menus/common.rb b/qa/qa/page/project/sub_menus/common.rb new file mode 100644 index 00000000000..c94e1e85256 --- /dev/null +++ b/qa/qa/page/project/sub_menus/common.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module QA + module Page + module Project + module SubMenus + module Common + def within_sidebar + within('.sidebar-top-level-items') do + yield + end + end + + def within_submenu + within('.fly-out-list') do + yield + end + end + end + end + end + end +end diff --git a/qa/qa/page/project/sub_menus/repository.rb b/qa/qa/page/project/sub_menus/repository.rb new file mode 100644 index 00000000000..29eaa9a74de --- /dev/null +++ b/qa/qa/page/project/sub_menus/repository.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +module QA + module Page + module Project + module SubMenus + module Repository + def self.included(base) + base.class_eval do + view 'app/views/layouts/nav/sidebar/_project.html.haml' do + element :project_menu_repo + element :branches_link + end + end + end + + def click_repository + within_sidebar do + click_element(:project_menu_repo) + end + end + + def click_repository_branches + hover_repository do + within_submenu do + click_element(:branches_link) + end + end + end + + private + + def hover_repository + within_sidebar do + find_element(:project_menu_repo).hover + + yield + end + end + end + end + end + end +end diff --git a/qa/qa/resource/base.rb b/qa/qa/resource/base.rb index f325162d1c0..ffe8633dd16 100644 --- a/qa/qa/resource/base.rb +++ b/qa/qa/resource/base.rb @@ -116,23 +116,13 @@ module QA end private_class_method :evaluator - def self.dynamic_attributes - const_get(:DynamicAttributes) - rescue NameError - mod = const_set(:DynamicAttributes, Module.new) - - include mod - - mod - end - class DSL def initialize(base) @base = base end def attribute(name, &block) - @base.dynamic_attributes.module_eval do + @base.module_eval do attr_writer(name) define_method(name) do diff --git a/qa/qa/resource/deploy_key.rb b/qa/qa/resource/deploy_key.rb index 9ed8fb7726e..9565598efb0 100644 --- a/qa/qa/resource/deploy_key.rb +++ b/qa/qa/resource/deploy_key.rb @@ -8,11 +8,7 @@ module QA attribute :fingerprint do Page::Project::Settings::Repository.perform do |setting| setting.expand_deploy_keys do |key| - key_offset = key.key_titles.index do |key_title| - key_title.text == title - end - - key.key_fingerprints[key_offset].text + key.find_fingerprint(title) end end end diff --git a/qa/qa/resource/fork.rb b/qa/qa/resource/fork.rb index 9fd66f3a36a..c6243ff43fa 100644 --- a/qa/qa/resource/fork.rb +++ b/qa/qa/resource/fork.rb @@ -3,6 +3,13 @@ module QA module Resource class Fork < Base + attribute :project do + Resource::Project.fabricate! do |resource| + resource.name = push.project.name + resource.path_with_namespace = "#{user.name}/#{push.project.name}" + end + end + attribute :push do Repository::ProjectPush.fabricate! end @@ -37,6 +44,8 @@ module QA Page::Layout::Banner.perform do |page| page.has_notice?('The project was successfully forked.') end + + populate(:project) end end end diff --git a/qa/qa/resource/merge_request_from_fork.rb b/qa/qa/resource/merge_request_from_fork.rb index f91ae299d76..5d20a6e9c75 100644 --- a/qa/qa/resource/merge_request_from_fork.rb +++ b/qa/qa/resource/merge_request_from_fork.rb @@ -11,7 +11,7 @@ module QA attribute :push do Repository::ProjectPush.fabricate! do |resource| - resource.project = fork + resource.project = fork.project resource.branch_name = fork_branch resource.file_name = 'file2.txt' resource.user = fork.user diff --git a/qa/qa/resource/project.rb b/qa/qa/resource/project.rb index 1fafbf5d73e..433e5a8f7c9 100644 --- a/qa/qa/resource/project.rb +++ b/qa/qa/resource/project.rb @@ -12,6 +12,10 @@ module QA Group.fabricate! end + attribute :path_with_namespace do + "#{group.sandbox.path}/#{group.path}/#{name}" if group + end + attribute :repository_ssh_location do Page::Project::Show.perform do |page| page.repository_clone_ssh_location @@ -46,8 +50,14 @@ module QA end end + def fabricate_via_api! + resource_web_url(api_get) + rescue ResourceNotFoundError + super + end + def api_get_path - "/projects/#{name}" + "/projects/#{CGI.escape(path_with_namespace)}" end def api_post_path diff --git a/qa/qa/resource/repository/push.rb b/qa/qa/resource/repository/push.rb index c14d97ff7fb..32f15547da2 100644 --- a/qa/qa/resource/repository/push.rb +++ b/qa/qa/resource/repository/push.rb @@ -8,7 +8,7 @@ module QA class Push < Base attr_accessor :file_name, :file_content, :commit_message, :branch_name, :new_branch, :output, :repository_http_uri, - :repository_ssh_uri, :ssh_key, :user + :repository_ssh_uri, :ssh_key, :user, :use_lfs attr_writer :remote_branch @@ -20,6 +20,7 @@ module QA @new_branch = true @repository_http_uri = "" @ssh_key = nil + @use_lfs = false end def remote_branch @@ -33,7 +34,9 @@ module QA end def files=(files) - if !files.is_a?(Array) || files.empty? + if !files.is_a?(Array) || + files.empty? || + files.any? { |file| !file.has_key?(:name) || !file.has_key?(:content) } raise ArgumentError, "Please provide an array of hashes e.g.: [{name: 'file1', content: 'foo'}]" end @@ -42,6 +45,8 @@ module QA def fabricate! Git::Repository.perform do |repository| + @output = '' + if ssh_key repository.uri = repository_ssh_uri repository.use_ssh_key(ssh_key) @@ -50,6 +55,8 @@ module QA repository.use_default_credentials unless user end + repository.use_lfs = use_lfs + username = 'GitLab QA' email = 'root@gitlab.com' @@ -60,29 +67,27 @@ module QA email = user.email end - repository.clone + repository.try_add_credentials_to_netrc + + @output += repository.clone repository.configure_identity(username, email) - if new_branch - repository.checkout_new_branch(branch_name) - else - repository.checkout(branch_name) - end + @output += repository.checkout(branch_name, new_branch: new_branch) if @directory @directory.each_child do |f| - repository.add_file(f.basename, f.read) if f.file? + @output += repository.add_file(f.basename, f.read) if f.file? end elsif @files @files.each do |f| repository.add_file(f[:name], f[:content]) end else - repository.add_file(file_name, file_content) + @output += repository.add_file(file_name, file_content) end - repository.commit(commit_message) - @output = repository.push_changes("#{branch_name}:#{remote_branch}") + @output += repository.commit(commit_message) + @output += repository.push_changes("#{branch_name}:#{remote_branch}") repository.delete_ssh_key end diff --git a/qa/qa/runtime/env.rb b/qa/qa/runtime/env.rb index 79b40223d84..23a2ace6a55 100644 --- a/qa/qa/runtime/env.rb +++ b/qa/qa/runtime/env.rb @@ -63,7 +63,7 @@ module QA # - "https://user:pass@somehost.com:443/wd/hub" # - "http://localhost:4444/wd/hub" - return unless ENV['QA_REMOTE_GRID'] + return if (ENV['QA_REMOTE_GRID'] || '').empty? "#{remote_grid_protocol}://#{remote_grid_credentials}#{ENV['QA_REMOTE_GRID']}/wd/hub" end diff --git a/qa/qa/specs/features/browser_ui/3_create/merge_request/merge_merge_request_from_fork_spec.rb b/qa/qa/specs/features/browser_ui/3_create/merge_request/merge_merge_request_from_fork_spec.rb index 6dcd74471fe..6ca7af8a3af 100644 --- a/qa/qa/specs/features/browser_ui/3_create/merge_request/merge_merge_request_from_fork_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/merge_request/merge_merge_request_from_fork_spec.rb @@ -5,18 +5,18 @@ module QA describe 'Merge request creation from fork' do it 'user forks a project, submits a merge request and maintainer merges it' do Runtime::Browser.visit(:gitlab, Page::Main::Login) - Page::Main::Login.act { sign_in_using_credentials } + Page::Main::Login.perform(&:sign_in_using_credentials) merge_request = Resource::MergeRequestFromFork.fabricate! do |merge_request| merge_request.fork_branch = 'feature-branch' end - Page::Main::Menu.perform { |main| main.sign_out } - Page::Main::Login.perform { |login| login.sign_in_using_credentials } + Page::Main::Menu.perform(&:sign_out) + Page::Main::Login.perform(&:sign_in_using_credentials) merge_request.visit! - Page::MergeRequest::Show.perform { |show| show.merge! } + Page::MergeRequest::Show.perform(&:merge!) expect(page).to have_content('The changes were merged') end diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/add_list_delete_branches_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/add_list_delete_branches_spec.rb new file mode 100644 index 00000000000..0f0c627d79a --- /dev/null +++ b/qa/qa/specs/features/browser_ui/3_create/repository/add_list_delete_branches_spec.rb @@ -0,0 +1,92 @@ +# frozen_string_literal: true + +module QA + context 'Create' do + describe 'Create, list, and delete branches via web' do + master_branch = 'master' + second_branch = 'second-branch' + third_branch = 'third-branch' + file_1_master = 'file.txt' + file_2_master = 'other-file.txt' + file_second_branch = 'file-2.txt' + file_third_branch = 'file-3.txt' + first_commit_message_of_master_branch = "Add #{file_1_master}" + second_commit_message_of_master_branch = "Add #{file_2_master}" + commit_message_of_second_branch = "Add #{file_second_branch}" + commit_message_of_third_branch = "Add #{file_third_branch}" + + before do + Runtime::Browser.visit(:gitlab, Page::Main::Login) + Page::Main::Login.perform(&:sign_in_using_credentials) + + project = Resource::Project.fabricate! do |proj| + proj.name = 'project-qa-test' + proj.description = 'project for qa test' + end + project.visit! + + Git::Repository.perform do |repository| + repository.uri = project.repository_http_location.uri + repository.use_default_credentials + + repository.act do + clone + configure_identity('GitLab QA', 'root@gitlab.com') + commit_file(file_1_master, 'Test file content', first_commit_message_of_master_branch) + push_changes + checkout(second_branch, new_branch: true) + commit_file(file_second_branch, 'File 2 content', commit_message_of_second_branch) + push_changes(second_branch) + checkout(master_branch) + # This second commit on master is needed for the master branch to be ahead + # of the second branch, and when the second branch is merged to master it will + # show the 'merged' badge on it. + # Refer to the below issue note: + # https://gitlab.com/gitlab-org/gitlab-ce/issues/55524#note_126100848 + commit_file(file_2_master, 'Other test file content', second_commit_message_of_master_branch) + push_changes + merge(second_branch) + push_changes + checkout(third_branch, new_branch: true) + commit_file(file_third_branch, 'File 3 content', commit_message_of_third_branch) + push_changes(third_branch) + end + end + Page::Project::Show.perform(&:wait_for_push) + end + + it 'branches are correctly listed after CRUD operations' do + Page::Project::Menu.perform(&:click_repository_branches) + + expect(page).to have_content(master_branch) + expect(page).to have_content(second_branch) + expect(page).to have_content(third_branch) + expect(page).to have_content("Merge branch 'second-branch'") + expect(page).to have_content(commit_message_of_second_branch) + expect(page).to have_content(commit_message_of_third_branch) + + Page::Project::Branches::Show.perform do |branches| + expect(branches).to have_branch_with_badge(second_branch, 'merged') + end + + Page::Project::Branches::Show.perform do |branches_view| + branches_view.delete_branch(third_branch) + end + + expect(page).not_to have_content(third_branch) + + Page::Project::Branches::Show.perform(&:delete_merged_branches) + + expect(page).to have_content( + 'Merged branches are being deleted. This can take some time depending on the number of branches. Please refresh the page to see changes.' + ) + + page.refresh + Page::Project::Branches::Show.perform do |branches_view| + branches_view.wait_for_texts_not_to_be_visible([commit_message_of_second_branch]) + expect(branches_view).not_to have_branch_title(second_branch) + end + end + end + end +end diff --git a/qa/qa/specs/features/browser_ui/6_release/deploy_key/add_deploy_key_spec.rb b/qa/qa/specs/features/browser_ui/6_release/deploy_key/add_deploy_key_spec.rb index 84757f25379..6f39a755392 100644 --- a/qa/qa/specs/features/browser_ui/6_release/deploy_key/add_deploy_key_spec.rb +++ b/qa/qa/specs/features/browser_ui/6_release/deploy_key/add_deploy_key_spec.rb @@ -5,7 +5,7 @@ module QA describe 'Deploy key creation' do it 'user adds a deploy key' do Runtime::Browser.visit(:gitlab, Page::Main::Login) - Page::Main::Login.act { sign_in_using_credentials } + Page::Main::Login.perform(&:sign_in_using_credentials) key = Runtime::Key::RSA.new deploy_key_title = 'deploy key title' @@ -16,7 +16,13 @@ module QA resource.key = deploy_key_value end - expect(deploy_key.fingerprint).to eq(key.fingerprint) + expect(deploy_key.fingerprint).to eq key.fingerprint + + Page::Project::Settings::Repository.perform do |setting| + setting.expand_deploy_keys do |keys| + expect(keys).to have_key(deploy_key_title, key.fingerprint) + end + end end end end diff --git a/qa/qa/tools/revoke_all_personal_access_tokens.rb b/qa/qa/tools/revoke_all_personal_access_tokens.rb new file mode 100644 index 00000000000..7484b633bf6 --- /dev/null +++ b/qa/qa/tools/revoke_all_personal_access_tokens.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +require_relative '../../qa' +require 'net/protocol.rb' +# This script revokes all personal access tokens with the name of 'api-test-token' on the host specified by GITLAB_ADDRESS +# Required environment variables: GITLAB_USERNAME, GITLAB_PASSWORD and GITLAB_ADDRESS +# Run `rake revoke_personal_access_tokens` + +module QA + module Tools + class RevokeAllPersonalAccessTokens + def run + do_run + rescue Net::ReadTimeout + STDOUT.puts 'Net::ReadTimeout during run. Trying again' + run + end + + private + + def do_run + raise ArgumentError, "Please provide GITLAB_USERNAME" unless ENV['GITLAB_USERNAME'] + raise ArgumentError, "Please provide GITLAB_PASSWORD" unless ENV['GITLAB_PASSWORD'] + raise ArgumentError, "Please provide GITLAB_ADDRESS" unless ENV['GITLAB_ADDRESS'] + + STDOUT.puts 'Running...' + + Runtime::Browser.visit(ENV['GITLAB_ADDRESS'], Page::Main::Login) + Page::Main::Login.perform(&:sign_in_using_credentials) + Page::Main::Menu.perform(&:go_to_profile_settings) + Page::Profile::Menu.perform(&:click_access_tokens) + + token_name = 'api-test-token' + + Page::Profile::PersonalAccessTokens.perform do |page| + while page.has_token_row_for_name?(token_name) + page.revoke_first_token_with_name(token_name) + print "\e[32m.\e[0m" + end + end + end + end + end +end diff --git a/qa/spec/resource/base_spec.rb b/qa/spec/resource/base_spec.rb index b8c406ae72a..a2a3ad01749 100644 --- a/qa/spec/resource/base_spec.rb +++ b/qa/spec/resource/base_spec.rb @@ -213,6 +213,42 @@ describe QA::Resource::Base do .to raise_error(described_class::NoValueError, "No value was computed for no_block of #{resource.class.name}.") end end + + context 'when multiple resources have the same attribute name' do + let(:base) do + Class.new(QA::Resource::Base) do + def fabricate! + 'any' + end + + def self.current_url + 'http://stub' + end + end + end + let(:first_resource) do + Class.new(base) do + attribute :test do + 'first block' + end + end + end + let(:second_resource) do + Class.new(base) do + attribute :test do + 'second block' + end + end + end + + it 'has unique attribute values' do + first_result = first_resource.fabricate!(resource: first_resource.new) + second_result = second_resource.fabricate!(resource: second_resource.new) + + expect(first_result.test).to eq 'first block' + expect(second_result.test).to eq 'second block' + end + end end describe '#web_url' do diff --git a/rubocop/cop/inject_enterprise_edition_module.rb b/rubocop/cop/inject_enterprise_edition_module.rb index c8b8aca51ab..1d37b1bd12d 100644 --- a/rubocop/cop/inject_enterprise_edition_module.rb +++ b/rubocop/cop/inject_enterprise_edition_module.rb @@ -11,9 +11,13 @@ module RuboCop METHODS = Set.new(%i[include extend prepend]).freeze - def_node_matcher :ee_const?, <<~PATTERN - (const (const _ :EE) _) - PATTERN + def ee_const?(node) + line = node.location.expression.source_line + + # We use `match?` here instead of RuboCop's AST matching, as this makes + # it far easier to handle nested constants such as `EE::Foo::Bar::Baz`. + line.match?(/(\s|\()(::)?EE::/) + end def on_send(node) return unless METHODS.include?(node.children[1]) diff --git a/rubocop/spec_helpers.rb b/rubocop/spec_helpers.rb index 9bf5f1e3b18..63c1b975a65 100644 --- a/rubocop/spec_helpers.rb +++ b/rubocop/spec_helpers.rb @@ -1,6 +1,7 @@ module RuboCop module SpecHelpers SPEC_HELPERS = %w[fast_spec_helper.rb rails_helper.rb spec_helper.rb].freeze + MIGRATION_SPEC_DIRECTORIES = ['spec/migrations', 'spec/lib/gitlab/background_migration'].freeze # Returns true if the given node originated from the spec directory. def in_spec?(node) @@ -10,14 +11,18 @@ module RuboCop path.start_with?(File.join(Dir.pwd, 'spec'), File.join(Dir.pwd, 'ee', 'spec')) end + def migration_directories + @migration_directories ||= MIGRATION_SPEC_DIRECTORIES.map do |dir| + [File.join(Dir.pwd, dir), File.join(Dir.pwd, 'ee', dir)] + end.flatten + end + # Returns true if the given node originated from a migration spec. def in_migration_spec?(node) path = node.location.expression.source_buffer.name in_spec?(node) && - path.start_with?( - File.join(Dir.pwd, 'spec', 'migrations'), - File.join(Dir.pwd, 'ee', 'spec', 'migrations')) + path.start_with?(*migration_directories) end end end diff --git a/scripts/review_apps/review-apps.sh b/scripts/review_apps/review-apps.sh index 4e1dbff7b80..1ee6f502b8e 100755 --- a/scripts/review_apps/review-apps.sh +++ b/scripts/review_apps/review-apps.sh @@ -346,7 +346,6 @@ function wait_for_job_to_be_done() { if [[ "${job_status}" == "failed" ]]; then echo "The '${job_name}' failed." - exit 1 elif [[ "${job_status}" == "manual" ]]; then echo "The '${job_name}' is manual." else diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb index 43f561f7a25..c290acb72aa 100644 --- a/spec/controllers/application_controller_spec.rb +++ b/spec/controllers/application_controller_spec.rb @@ -519,12 +519,14 @@ describe ApplicationController do get :index expect(response).to have_gitlab_http_status(404) + expect(response).to render_template('errors/not_found') end it 'renders a 403 when a message is passed to access denied' do get :index, params: { message: 'None shall pass' } expect(response).to have_gitlab_http_status(403) + expect(response).to render_template('errors/access_denied') end it 'renders a status passed to access denied' do diff --git a/spec/controllers/import/bitbucket_server_controller_spec.rb b/spec/controllers/import/bitbucket_server_controller_spec.rb index 73195463a50..bb282db5a41 100644 --- a/spec/controllers/import/bitbucket_server_controller_spec.rb +++ b/spec/controllers/import/bitbucket_server_controller_spec.rb @@ -78,7 +78,7 @@ describe Import::BitbucketServerController do end it "returns an error when the server can't be contacted" do - expect(client).to receive(:repo).with(project_key, repo_slug).and_raise(BitbucketServer::Client::ServerError) + expect(client).to receive(:repo).with(project_key, repo_slug).and_raise(::BitbucketServer::Connection::ConnectionError) post :create, params: { project: project_key, repository: repo_slug }, format: :json diff --git a/spec/controllers/omniauth_callbacks_controller_spec.rb b/spec/controllers/omniauth_callbacks_controller_spec.rb index 21936491ffc..59463462e5a 100644 --- a/spec/controllers/omniauth_callbacks_controller_spec.rb +++ b/spec/controllers/omniauth_callbacks_controller_spec.rb @@ -55,7 +55,7 @@ describe OmniauthCallbacksController, type: :controller do context 'when a redirect url is stored' do it 'redirects with fragment' do - post provider, nil, { user_return_to: '/fake/url' } + post provider, session: { user_return_to: '/fake/url' } expect(response).to redirect_to('/fake/url#L101') end @@ -63,7 +63,7 @@ describe OmniauthCallbacksController, type: :controller do context 'when a redirect url with a fragment is stored' do it 'redirects with the new fragment' do - post provider, nil, { user_return_to: '/fake/url#replaceme' } + post provider, session: { user_return_to: '/fake/url#replaceme' } expect(response).to redirect_to('/fake/url#L101') end diff --git a/spec/controllers/projects/badges_controller_spec.rb b/spec/controllers/projects/badges_controller_spec.rb index 2556bc3ae50..8eac3d9a459 100644 --- a/spec/controllers/projects/badges_controller_spec.rb +++ b/spec/controllers/projects/badges_controller_spec.rb @@ -22,7 +22,44 @@ describe Projects::BadgesController do expect(response).to have_gitlab_http_status(:ok) end - def get_badge(badge) - get badge, params: { namespace_id: project.namespace.to_param, project_id: project, ref: pipeline.ref }, format: :svg + it 'renders the `flat` badge layout by default' do + get_badge(:coverage) + + expect(response).to render_template('projects/badges/badge') + end + + context 'when style param is set to `flat`' do + it 'renders the `flat` badge layout' do + get_badge(:coverage, 'flat') + + expect(response).to render_template('projects/badges/badge') + end + end + + context 'when style param is set to an invalid type' do + it 'renders the `flat` (default) badge layout' do + get_badge(:coverage, 'xxx') + + expect(response).to render_template('projects/badges/badge') + end + end + + context 'when style param is set to `flat-square`' do + it 'renders the `flat-square` badge layout' do + get_badge(:coverage, 'flat-square') + + expect(response).to render_template('projects/badges/badge_flat-square') + end + end + + def get_badge(badge, style = nil) + params = { + namespace_id: project.namespace.to_param, + project_id: project, + ref: pipeline.ref, + style: style + } + + get badge, params: params, format: :svg end end diff --git a/spec/controllers/projects/error_tracking_controller_spec.rb b/spec/controllers/projects/error_tracking_controller_spec.rb index 729e71b87a6..6464398cea1 100644 --- a/spec/controllers/projects/error_tracking_controller_spec.rb +++ b/spec/controllers/projects/error_tracking_controller_spec.rb @@ -20,18 +20,6 @@ describe Projects::ErrorTrackingController do expect(response).to render_template(:index) end - context 'with feature flag disabled' do - before do - stub_feature_flags(error_tracking: false) - end - - it 'returns 404' do - get :index, params: project_params - - expect(response).to have_gitlab_http_status(:not_found) - end - end - context 'with insufficient permissions' do before do project.add_guest(user) diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb index df21dc7bc85..a2c3bb2919d 100644 --- a/spec/controllers/projects/issues_controller_spec.rb +++ b/spec/controllers/projects/issues_controller_spec.rb @@ -1030,19 +1030,6 @@ describe Projects::IssuesController do let(:project) { create(:project, :public) } let(:file) { fixture_file_upload('spec/fixtures/csv_comma.csv') } - context 'feature disabled' do - it 'returns 404' do - sign_in(user) - project.add_maintainer(user) - - stub_feature_flags(issues_import_csv: false) - - import_csv - - expect(response).to have_gitlab_http_status :not_found - end - end - context 'unauthorized' do it 'returns 404 for guests' do sign_out(:user) @@ -1086,9 +1073,9 @@ describe Projects::IssuesController do end def import_csv - post :import_csv, namespace_id: project.namespace.to_param, - project_id: project.to_param, - file: file + post :import_csv, params: { namespace_id: project.namespace.to_param, + project_id: project.to_param, + file: file } end end @@ -1131,6 +1118,7 @@ describe Projects::IssuesController do context 'when user is setting notes filters' do let(:issuable) { issue } + let(:issuable_parent) { project } let!(:discussion_note) { create(:discussion_note_on_issue, :system, noteable: issuable, project: project) } it_behaves_like 'issuable notes filter' diff --git a/spec/controllers/projects/jobs_controller_spec.rb b/spec/controllers/projects/jobs_controller_spec.rb index 7f65fe551e9..d8a331b3cf0 100644 --- a/spec/controllers/projects/jobs_controller_spec.rb +++ b/spec/controllers/projects/jobs_controller_spec.rb @@ -770,50 +770,6 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do end end - describe 'POST cancel_all' do - before do - project.add_developer(user) - sign_in(user) - end - - context 'when jobs are cancelable' do - before do - create_list(:ci_build, 2, :cancelable, pipeline: pipeline) - - post_cancel_all - end - - it 'redirects to a index page' do - expect(response).to have_gitlab_http_status(:found) - expect(response).to redirect_to(namespace_project_jobs_path) - end - - it 'transits to canceled' do - expect(Ci::Build.all).to all(be_canceled) - end - end - - context 'when jobs are not cancelable' do - before do - create_list(:ci_build, 2, :canceled, pipeline: pipeline) - - post_cancel_all - end - - it 'redirects to a index page' do - expect(response).to have_gitlab_http_status(:found) - expect(response).to redirect_to(namespace_project_jobs_path) - end - end - - def post_cancel_all - post :cancel_all, params: { - namespace_id: project.namespace, - project_id: project - } - end - end - describe 'POST erase' do let(:role) { :maintainer } diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb index 4f4d3ca226f..4451fd227e8 100644 --- a/spec/controllers/projects/merge_requests_controller_spec.rb +++ b/spec/controllers/projects/merge_requests_controller_spec.rb @@ -78,6 +78,7 @@ describe Projects::MergeRequestsController do context 'when user is setting notes filters' do let(:issuable) { merge_request } + let(:issuable_parent) { project } let!(:discussion_note) { create(:discussion_note_on_merge_request, :system, noteable: issuable, project: project) } let!(:discussion_comment) { create(:discussion_note_on_merge_request, noteable: issuable, project: project) } diff --git a/spec/controllers/projects/pages_controller_spec.rb b/spec/controllers/projects/pages_controller_spec.rb index 382c1b5d124..4b742a5d427 100644 --- a/spec/controllers/projects/pages_controller_spec.rb +++ b/spec/controllers/projects/pages_controller_spec.rb @@ -28,10 +28,10 @@ describe Projects::PagesController do let(:group) { create(:group, :nested) } let(:project) { create(:project, namespace: group) } - it 'returns a 404 status code' do + it 'returns a 200 status code' do get :show, params: request_params - expect(response).to have_gitlab_http_status(404) + expect(response).to have_gitlab_http_status(200) end end end diff --git a/spec/controllers/projects/settings/operations_controller_spec.rb b/spec/controllers/projects/settings/operations_controller_spec.rb index 810f5bb64ba..d989ec22481 100644 --- a/spec/controllers/projects/settings/operations_controller_spec.rb +++ b/spec/controllers/projects/settings/operations_controller_spec.rb @@ -41,18 +41,6 @@ describe Projects::Settings::OperationsController do end end - context 'with feature flag disabled' do - before do - stub_feature_flags(error_tracking: false) - end - - it 'renders 404' do - get :show, params: project_params(project) - - expect(response).to have_gitlab_http_status(:not_found) - end - end - context 'with insufficient permissions' do before do project.add_reporter(user) @@ -121,18 +109,6 @@ describe Projects::Settings::OperationsController do end end - context 'with feature flag disabled' do - before do - stub_feature_flags(error_tracking: false) - end - - it 'renders 404' do - patch :update, params: project_params(project) - - expect(response).to have_gitlab_http_status(:not_found) - end - end - context 'with insufficient permissions' do before do project.add_reporter(user) diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb index f84f069f4db..9801ed19957 100644 --- a/spec/controllers/projects_controller_spec.rb +++ b/spec/controllers/projects_controller_spec.rb @@ -955,6 +955,59 @@ describe ProjectsController do end end + describe 'GET resolve' do + shared_examples 'resolvable endpoint' do + it 'redirects to the project page' do + get :resolve, params: { id: project.id } + + expect(response).to have_gitlab_http_status(302) + expect(response).to redirect_to(project_path(project)) + end + end + + context 'with an authenticated user' do + before do + sign_in(user) + end + + context 'when user has access to the project' do + before do + project.add_developer(user) + end + + it_behaves_like 'resolvable endpoint' + end + + context 'when user has no access to the project' do + it 'gives 404 for existing project' do + get :resolve, params: { id: project.id } + + expect(response).to have_gitlab_http_status(404) + end + end + + it 'gives 404 for non-existing project' do + get :resolve, params: { id: '0' } + + expect(response).to have_gitlab_http_status(404) + end + end + + context 'non authenticated user' do + context 'with a public project' do + let(:project) { public_project } + + it_behaves_like 'resolvable endpoint' + end + + it 'gives 404 for private project' do + get :resolve, params: { id: project.id } + + expect(response).to have_gitlab_http_status(404) + end + end + end + def project_moved_message(redirect_route, project) "Project '#{redirect_route.path}' was moved to '#{project.full_path}'. Please update any links and bookmarks that may still have the old path." end diff --git a/spec/controllers/uploads_controller_spec.rb b/spec/controllers/uploads_controller_spec.rb index 19142aa1272..5fbb71eca96 100644 --- a/spec/controllers/uploads_controller_spec.rb +++ b/spec/controllers/uploads_controller_spec.rb @@ -12,6 +12,12 @@ shared_examples 'content not cached without revalidation and no-store' do end end +shared_examples 'content publicly cached' do + it 'ensures content is publicly cached' do + expect(subject['Cache-Control']).to eq('max-age=300, public') + end +end + describe UploadsController do let!(:user) { create(:user, avatar: fixture_file_upload("spec/fixtures/dk.png", "image/png")) } @@ -184,7 +190,7 @@ describe UploadsController do expect(response).to have_gitlab_http_status(200) end - it_behaves_like 'content not cached without revalidation and no-store' do + it_behaves_like 'content publicly cached' do subject do get :show, params: { model: 'user', mounted_as: 'avatar', id: user.id, filename: 'dk.png' } @@ -201,7 +207,7 @@ describe UploadsController do expect(response).to have_gitlab_http_status(200) end - it_behaves_like 'content not cached without revalidation' do + it_behaves_like 'content publicly cached' do subject do get :show, params: { model: 'user', mounted_as: 'avatar', id: user.id, filename: 'dk.png' } @@ -537,7 +543,7 @@ describe UploadsController do expect(response).to have_gitlab_http_status(200) end - it_behaves_like 'content not cached without revalidation' do + it_behaves_like 'content publicly cached' do subject do get :show, params: { model: 'appearance', mounted_as: 'header_logo', id: appearance.id, filename: 'dk.png' } @@ -557,7 +563,7 @@ describe UploadsController do expect(response).to have_gitlab_http_status(200) end - it_behaves_like 'content not cached without revalidation' do + it_behaves_like 'content publicly cached' do subject do get :show, params: { model: 'appearance', mounted_as: 'logo', id: appearance.id, filename: 'dk.png' } diff --git a/spec/factories/clusters/clusters.rb b/spec/factories/clusters/clusters.rb index 3e2c0df8afb..a2e5f4862db 100644 --- a/spec/factories/clusters/clusters.rb +++ b/spec/factories/clusters/clusters.rb @@ -59,5 +59,9 @@ FactoryBot.define do trait :with_installed_helm do application_helm factory: %i(clusters_applications_helm installed) end + + trait :with_domain do + domain 'example.com' + end end end diff --git a/spec/factories/wiki_pages.rb b/spec/factories/wiki_pages.rb index 2335b5118dd..ae257d769e8 100644 --- a/spec/factories/wiki_pages.rb +++ b/spec/factories/wiki_pages.rb @@ -5,7 +5,7 @@ FactoryBot.define do transient do attrs do { - title: 'Title', + title: 'Title.with.dot', content: 'Content for wiki page', format: 'markdown' } diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb index baa2b1d8af5..08c27354bd2 100644 --- a/spec/features/boards/boards_spec.rb +++ b/spec/features/boards/boards_spec.rb @@ -97,7 +97,7 @@ describe 'Issue Boards', :js do expect(find('.board:nth-child(4)')).to have_selector('.board-card') end - it 'shows description tooltip on list title' do + it 'shows description tooltip on list title', :quarantine do page.within('.board:nth-child(2)') do expect(find('.board-title span.has-tooltip')[:title]).to eq('Test') end @@ -411,7 +411,7 @@ describe 'Issue Boards', :js do wait_for_empty_boards((2..4)) end - it 'filters by label with space after reload' do + it 'filters by label with space after reload', :quarantine do set_filter("label", "\"#{accepting.title}") click_filter_link(accepting.title) submit_filter @@ -477,7 +477,7 @@ describe 'Issue Boards', :js do end end - it 'filters by multiple labels' do + it 'filters by multiple labels', :quarantine do set_filter("label", testing.title) click_filter_link(testing.title) diff --git a/spec/features/dashboard/help_spec.rb b/spec/features/dashboard/help_spec.rb index 68bfbf22736..fa12cecc984 100644 --- a/spec/features/dashboard/help_spec.rb +++ b/spec/features/dashboard/help_spec.rb @@ -5,13 +5,23 @@ RSpec.describe 'Dashboard Help' do sign_in(create(:user)) end - it 'renders correctly markdown' do - visit help_page_path("administration/raketasks/maintenance") + context 'help dropdown' do + it 'shows the "What\'s new?" menu item' do + visit root_dashboard_path - expect(page).to have_content('Gather information about GitLab and the system it runs on') + expect(page.find('.header-help .dropdown-menu')).to have_text("What's new?") + end + end + + context 'documentation' do + it 'renders correctly markdown' do + visit help_page_path("administration/raketasks/maintenance") + + expect(page).to have_content('Gather information about GitLab and the system it runs on') - node = find('.documentation h2 a#user-content-check-gitlab-configuration') - expect(node[:href]).to eq '#check-gitlab-configuration' - expect(find(:xpath, "#{node.path}/..").text).to eq 'Check GitLab configuration' + node = find('.documentation h2 a#user-content-check-gitlab-configuration') + expect(node[:href]).to eq '#check-gitlab-configuration' + expect(find(:xpath, "#{node.path}/..").text).to eq 'Check GitLab configuration' + end end end diff --git a/spec/features/dashboard/projects_spec.rb b/spec/features/dashboard/projects_spec.rb index 975b7944741..edca8f9df08 100644 --- a/spec/features/dashboard/projects_spec.rb +++ b/spec/features/dashboard/projects_spec.rb @@ -91,6 +91,7 @@ describe 'Dashboard Projects' do visit dashboard_projects_path expect(page).to have_content(project.name) + expect(find('.nav-links li:nth-child(1) .badge-pill')).to have_content(1) end it 'shows personal projects on personal projects tab', :js do @@ -121,6 +122,8 @@ describe 'Dashboard Projects' do expect(page).not_to have_content(project.name) expect(page).to have_content(project2.name) + expect(find('.nav-links li:nth-child(1) .badge-pill')).to have_content(1) + expect(find('.nav-links li:nth-child(2) .badge-pill')).to have_content(1) end end diff --git a/spec/features/dashboard/todos/todos_spec.rb b/spec/features/dashboard/todos/todos_spec.rb index 96b22a0f64b..2284ee925a0 100644 --- a/spec/features/dashboard/todos/todos_spec.rb +++ b/spec/features/dashboard/todos/todos_spec.rb @@ -332,7 +332,7 @@ describe 'Dashboard Todos' do it 'links to the pipelines for the merge request' do href = pipelines_project_merge_request_path(project, todo.target) - expect(page).to have_link "merge request #{todo.target.to_reference(full: true)}", href + expect(page).to have_link "merge request #{todo.target.to_reference(full: true)}", href: href end end end diff --git a/spec/features/groups_spec.rb b/spec/features/groups_spec.rb index d01fc04311a..00d81b26ce2 100644 --- a/spec/features/groups_spec.rb +++ b/spec/features/groups_spec.rb @@ -154,7 +154,7 @@ describe 'Group' do end describe 'group edit', :js do - let(:group) { create(:group) } + let(:group) { create(:group, :public) } let(:path) { edit_group_path(group) } let(:new_name) { 'new-name' } @@ -163,6 +163,8 @@ describe 'Group' do end it_behaves_like 'dirty submit form', [{ form: '.js-general-settings-form', input: 'input[name="group[name]"]' }, + { form: '.js-general-settings-form', input: '#group_visibility_level_0' }, + { form: '.js-general-permissions-form', input: '#group_request_access_enabled' }, { form: '.js-general-permissions-form', input: 'input[name="group[two_factor_grace_period]"]' }] it 'saves new settings' do diff --git a/spec/features/issuables/markdown_references/internal_references_spec.rb b/spec/features/issuables/markdown_references/internal_references_spec.rb index 9613e22bf24..23385ba65fc 100644 --- a/spec/features/issuables/markdown_references/internal_references_spec.rb +++ b/spec/features/issuables/markdown_references/internal_references_spec.rb @@ -64,11 +64,13 @@ describe "Internal references", :js do it "shows references" do page.within("#merge-requests .merge-requests-title") do - expect(page).to have_content("1 Related Merge Request") + expect(page).to have_content("Related merge requests") + expect(page).to have_css(".mr-count-badge") end page.within("#merge-requests ul") do expect(page).to have_content(private_project_merge_request.title) + expect(page).to have_css(".merge-request-status") end expect(page).to have_content("mentioned in merge request #{private_project_merge_request.to_reference(public_project)}") diff --git a/spec/features/issues/user_creates_branch_and_merge_request_spec.rb b/spec/features/issues/user_creates_branch_and_merge_request_spec.rb index 32bc851f00f..693ad89069c 100644 --- a/spec/features/issues/user_creates_branch_and_merge_request_spec.rb +++ b/spec/features/issues/user_creates_branch_and_merge_request_spec.rb @@ -141,7 +141,7 @@ describe 'User creates branch and merge request on issue page', :js do it 'disables the create branch button' do expect(page).to have_css('.create-mr-dropdown-wrap .unavailable:not(.hidden)') expect(page).to have_css('.create-mr-dropdown-wrap .available.hidden', visible: false) - expect(page).to have_content /1 Related Merge Request/ + expect(page).to have_content /Related merge requests/ end end diff --git a/spec/features/markdown/math_spec.rb b/spec/features/markdown/math_spec.rb index 6a23d6b78ab..678ce80b382 100644 --- a/spec/features/markdown/math_spec.rb +++ b/spec/features/markdown/math_spec.rb @@ -16,7 +16,7 @@ describe 'Math rendering', :js do visit project_issue_path(project, issue) - expect(page).to have_selector('.katex .mord.mathit', text: 'b') - expect(page).to have_selector('.katex-display .mord.mathit', text: 'b') + expect(page).to have_selector('.katex .mord.mathdefault', text: 'b') + expect(page).to have_selector('.katex-display .mord.mathdefault', text: 'b') end end diff --git a/spec/features/merge_request/user_merges_immediately_spec.rb b/spec/features/merge_request/user_merges_immediately_spec.rb index ea61f9675bc..84636ae355c 100644 --- a/spec/features/merge_request/user_merges_immediately_spec.rb +++ b/spec/features/merge_request/user_merges_immediately_spec.rb @@ -25,6 +25,8 @@ describe 'Merge requests > User merges immediately', :js do end it 'enables merge immediately' do + wait_for_requests + page.within '.mr-widget-body' do find('.dropdown-toggle').click 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 0959f1b12f3..5188dc3625f 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 @@ -51,22 +51,52 @@ describe 'Merge request < User sees mini pipeline graph', :js do first('.mini-pipeline-graph-dropdown-toggle') end - it 'expands when hovered' do + # Status icon button styles should update as described in + # https://gitlab.com/gitlab-org/gitlab-ce/issues/42769 + it 'has unique styles for default, :hover, :active, and :focus states' do find('.mini-pipeline-graph-dropdown-toggle') - before_width = evaluate_script("$('.mini-pipeline-graph-dropdown-toggle:visible').outerWidth();") + default_background_color = evaluate_script("$('.mini-pipeline-graph-dropdown-toggle:visible').css('background-color');") + default_foreground_color = evaluate_script("$('.mini-pipeline-graph-dropdown-toggle:visible svg').css('fill');") + default_box_shadow = evaluate_script("$('.mini-pipeline-graph-dropdown-toggle:visible').css('box-shadow');") toggle.hover find('.mini-pipeline-graph-dropdown-toggle') - after_width = evaluate_script("$('.mini-pipeline-graph-dropdown-toggle:visible').outerWidth();") + hover_background_color = evaluate_script("$('.mini-pipeline-graph-dropdown-toggle:visible').css('background-color');") + hover_foreground_color = evaluate_script("$('.mini-pipeline-graph-dropdown-toggle:visible svg').css('fill');") + hover_box_shadow = evaluate_script("$('.mini-pipeline-graph-dropdown-toggle:visible').css('box-shadow');") - expect(before_width).to be < after_width - end + page.driver.browser.action.click_and_hold(toggle.native).perform - it 'shows dropdown caret when hovered' do - toggle.hover + find('.mini-pipeline-graph-dropdown-toggle') + active_background_color = evaluate_script("$('.mini-pipeline-graph-dropdown-toggle:visible').css('background-color');") + active_foreground_color = evaluate_script("$('.mini-pipeline-graph-dropdown-toggle:visible svg').css('fill');") + active_box_shadow = evaluate_script("$('.mini-pipeline-graph-dropdown-toggle:visible').css('box-shadow');") + + page.driver.browser.action.release(toggle.native) + .move_by(100, 100) + .perform + + find('.mini-pipeline-graph-dropdown-toggle') + focus_background_color = evaluate_script("$('.mini-pipeline-graph-dropdown-toggle:visible').css('background-color');") + focus_foreground_color = evaluate_script("$('.mini-pipeline-graph-dropdown-toggle:visible svg').css('fill');") + focus_box_shadow = evaluate_script("$('.mini-pipeline-graph-dropdown-toggle:visible').css('box-shadow');") + + expect(default_background_color).not_to eq(hover_background_color) + expect(hover_background_color).not_to eq(active_background_color) + expect(default_background_color).not_to eq(active_background_color) + + expect(default_foreground_color).not_to eq(hover_foreground_color) + expect(hover_foreground_color).not_to eq(active_foreground_color) + expect(default_foreground_color).not_to eq(active_foreground_color) + + expect(focus_background_color).to eq(hover_background_color) + expect(focus_foreground_color).to eq(hover_foreground_color) - expect(toggle).to have_selector('.fa-caret-down') + expect(default_box_shadow).to eq('none') + expect(hover_box_shadow).to eq('none') + expect(active_box_shadow).not_to eq('none') + expect(focus_box_shadow).not_to eq('none') end it 'shows tooltip when hovered' do diff --git a/spec/features/projects/artifacts/user_browses_artifacts_spec.rb b/spec/features/projects/artifacts/user_browses_artifacts_spec.rb index 9ebbbaea911..5f630c9ffa4 100644 --- a/spec/features/projects/artifacts/user_browses_artifacts_spec.rb +++ b/spec/features/projects/artifacts/user_browses_artifacts_spec.rb @@ -25,8 +25,8 @@ describe "User browses artifacts" do page.within(".tree-table") do expect(page).to have_no_content("..") .and have_content("other_artifacts_0.1.2") - .and have_content("ci_artifacts.txt") - .and have_content("rails_sample.jpg") + .and have_content("ci_artifacts.txt 27 Bytes") + .and have_content("rails_sample.jpg 34.4 KB") end page.within(".build-header") do diff --git a/spec/features/projects/jobs_spec.rb b/spec/features/projects/jobs_spec.rb index 60f37f4b74a..8230396a4cc 100644 --- a/spec/features/projects/jobs_spec.rb +++ b/spec/features/projects/jobs_spec.rb @@ -28,7 +28,6 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do end it "shows Pending tab jobs" do - expect(page).to have_link 'Cancel running' expect(page).to have_selector('.nav-links li.active', text: 'Pending') expect(page).to have_content job.short_sha expect(page).to have_content job.ref @@ -44,7 +43,6 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do it "shows Running tab jobs" do expect(page).to have_selector('.nav-links li.active', text: 'Running') - expect(page).to have_link 'Cancel running' expect(page).to have_content job.short_sha expect(page).to have_content job.ref expect(page).to have_content job.name @@ -60,7 +58,6 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do it "shows Finished tab jobs" do expect(page).to have_selector('.nav-links li.active', text: 'Finished') expect(page).to have_content 'No jobs to show' - expect(page).to have_link 'Cancel running' end end @@ -75,7 +72,6 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do expect(page).to have_content job.short_sha expect(page).to have_content job.ref expect(page).to have_content job.name - expect(page).not_to have_link 'Cancel running' end end @@ -94,23 +90,6 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do end end - describe "POST /:project/jobs/:id/cancel_all" do - before do - job.run! - visit project_jobs_path(project) - click_link "Cancel running" - end - - it 'shows all necessary content' do - expect(page).to have_selector('.nav-links li.active', text: 'All') - expect(page).to have_content 'canceled' - expect(page).to have_content job.short_sha - expect(page).to have_content job.ref - expect(page).to have_content job.name - expect(page).not_to have_link 'Cancel running' - end - end - describe "GET /:project/jobs/:id" do context "Job from project" do let(:job) { create(:ci_build, :success, :trace_live, pipeline: pipeline) } @@ -191,7 +170,7 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do href = new_project_issue_path(project, options) - page.within('.header-action-buttons') do + page.within('.build-sidebar') do expect(find('.js-new-issue')['href']).to include(href) end end diff --git a/spec/features/projects/labels/update_prioritization_spec.rb b/spec/features/projects/labels/update_prioritization_spec.rb index 055a0c83a11..d36f043f880 100644 --- a/spec/features/projects/labels/update_prioritization_spec.rb +++ b/spec/features/projects/labels/update_prioritization_spec.rb @@ -125,7 +125,7 @@ describe 'Prioritize labels' do wait_for_requests end - page.within('.breadcrumbs-container') do + page.within('.top-area') do expect(page).to have_link('New label') end end diff --git a/spec/features/projects/pipelines/pipeline_spec.rb b/spec/features/projects/pipelines/pipeline_spec.rb index 4706c28bb3d..3192c9ffad4 100644 --- a/spec/features/projects/pipelines/pipeline_spec.rb +++ b/spec/features/projects/pipelines/pipeline_spec.rb @@ -477,10 +477,11 @@ describe 'Pipeline', :js do end context 'when accessing failed jobs page' do - it 'fails to access the page' do - subject + it 'renders a 404 page' do + requests = inspect_requests { subject } - expect(page).to have_title('Access Denied') + expect(page).to have_title('Not Found') + expect(requests.first.status_code).to eq(404) end end end diff --git a/spec/features/projects/settings/operations_settings_spec.rb b/spec/features/projects/settings/operations_settings_spec.rb index 1f2328a6dd8..06290c67c70 100644 --- a/spec/features/projects/settings/operations_settings_spec.rb +++ b/spec/features/projects/settings/operations_settings_spec.rb @@ -8,32 +8,16 @@ describe 'Projects > Settings > For a forked project', :js do let(:role) { :maintainer } before do - stub_feature_flags(error_tracking: true) sign_in(user) project.add_role(user, role) end describe 'Sidebar > Operations' do - context 'when sidebar feature flag enabled' do - it 'renders the settings link in the sidebar' do - visit project_path(project) - wait_for_requests + it 'renders the settings link in the sidebar' do + visit project_path(project) + wait_for_requests - expect(page).to have_selector('a[title="Operations"]', visible: false) - end - end - - context 'when sidebar feature flag disabled' do - before do - stub_feature_flags(error_tracking: false) - end - - it 'does not render the settings link in the sidebar' do - visit project_path(project) - wait_for_requests - - expect(page).not_to have_selector('a[title="Operations"]', visible: false) - end + expect(page).to have_selector('a[title="Operations"]', visible: false) end end end diff --git a/spec/features/projects_spec.rb b/spec/features/projects_spec.rb index b56bb272b46..eb70a3c41c1 100644 --- a/spec/features/projects_spec.rb +++ b/spec/features/projects_spec.rb @@ -99,6 +99,30 @@ describe 'Project' do end end + describe 'project topics' do + let(:project) { create(:project, :repository) } + let(:path) { project_path(project) } + + before do + sign_in(create(:admin)) + visit path + end + + it 'shows project topics' do + project.update_attribute(:tag_list, 'topic1') + visit path + expect(page).to have_css('.project-topic-list') + expect(page).to have_content('topic1') + end + + it 'shows up to 3 project tags' do + project.update_attribute(:tag_list, 'topic1, topic2, topic3, topic4') + visit path + expect(page).to have_css('.project-topic-list') + expect(page).to have_content('topic1, topic2, topic3 + 1 more') + end + end + describe 'copy clone URL to clipboard', :js do let(:project) { create(:project, :repository) } let(:path) { project_path(project) } diff --git a/spec/fixtures/malicious.bundle b/spec/fixtures/malicious.bundle new file mode 100644 index 00000000000..7ba47932906 --- /dev/null +++ b/spec/fixtures/malicious.bundle @@ -0,0 +1 @@ +gitdir: foo.git diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb index 4135f31e051..b81249a1e29 100644 --- a/spec/helpers/application_helper_spec.rb +++ b/spec/helpers/application_helper_spec.rb @@ -168,6 +168,21 @@ describe ApplicationHelper do end end + describe '#client_class_list' do + it 'returns string containing CSS classes representing client browser and platform' do + class_list = helper.client_class_list + expect(class_list).to eq('gl-browser-generic gl-platform-other') + end + end + + describe '#client_js_flags' do + it 'returns map containing JS flags representing client browser and platform' do + flags_list = helper.client_js_flags + expect(flags_list[:isGeneric]).to eq(true) + expect(flags_list[:isOther]).to eq(true) + end + end + describe '#autocomplete_data_sources' do let(:project) { create(:project) } let(:noteable_type) { Issue } diff --git a/spec/javascripts/diffs/components/compare_versions_dropdown_spec.js b/spec/javascripts/diffs/components/compare_versions_dropdown_spec.js index 7237274eb43..53b9ac22fc0 100644 --- a/spec/javascripts/diffs/components/compare_versions_dropdown_spec.js +++ b/spec/javascripts/diffs/components/compare_versions_dropdown_spec.js @@ -1 +1,34 @@ // TODO: https://gitlab.com/gitlab-org/gitlab-ce/issues/48034 +import { shallowMount, createLocalVue } from '@vue/test-utils'; +import CompareVersionsDropdown from '~/diffs/components/compare_versions_dropdown.vue'; +import diffsMockData from '../mock_data/merge_request_diffs'; + +describe('CompareVersionsDropdown', () => { + let wrapper; + const targetBranch = { branchName: 'tmp-wine-dev', versionIndex: -1 }; + + const factory = (options = {}) => { + const localVue = createLocalVue(); + + wrapper = shallowMount(CompareVersionsDropdown, { localVue, ...options }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + it('should render a correct base version link', () => { + factory({ + propsData: { + baseVersionPath: '/gnuwget/wget2/merge_requests/6/diffs?diff_id=37', + otherVersions: diffsMockData.slice(1), + targetBranch, + }, + }); + + const links = wrapper.findAll('a'); + const lastLink = links.wrappers[links.length - 1]; + + expect(lastLink.attributes('href')).toEqual(wrapper.props('baseVersionPath')); + }); +}); diff --git a/spec/javascripts/diffs/components/compare_versions_spec.js b/spec/javascripts/diffs/components/compare_versions_spec.js index 75c66e9ca82..a976c6b837f 100644 --- a/spec/javascripts/diffs/components/compare_versions_spec.js +++ b/spec/javascripts/diffs/components/compare_versions_spec.js @@ -22,10 +22,10 @@ describe('CompareVersions', () => { const treeListBtn = vm.$el.querySelector('.js-toggle-tree-list'); expect(treeListBtn).not.toBeNull(); - expect(treeListBtn.dataset.originalTitle).toBe('Toggle file browser'); + expect(treeListBtn.dataset.originalTitle).toBe('Hide file browser'); expect(treeListBtn.querySelectorAll('svg use').length).not.toBe(0); expect(treeListBtn.querySelector('svg use').getAttribute('xlink:href')).toContain( - '#hamburger', + '#collapse-left', ); }); @@ -100,6 +100,12 @@ describe('CompareVersions', () => { }); }); + describe('baseVersionPath', () => { + it('should be set correctly from mergeRequestDiff', () => { + expect(vm.baseVersionPath).toEqual(vm.mergeRequestDiff.base_version_path); + }); + }); + describe('isWhitespaceVisible', () => { const originalHref = window.location.href; diff --git a/spec/javascripts/diffs/mock_data/merge_request_diffs.js b/spec/javascripts/diffs/mock_data/merge_request_diffs.js index d72ad7818dd..4bbef146336 100644 --- a/spec/javascripts/diffs/mock_data/merge_request_diffs.js +++ b/spec/javascripts/diffs/mock_data/merge_request_diffs.js @@ -1,42 +1,46 @@ export default [ { - versionIndex: 4, - createdAt: '2018-10-23T11:49:16.611Z', - commitsCount: 4, + base_version_path: '/gnuwget/wget2/merge_requests/6/diffs?diff_id=37', + version_index: 4, + created_at: '2018-10-23T11:49:16.611Z', + commits_count: 4, latest: true, - shortCommitSha: 'de7a8f7f', - versionPath: '/gnuwget/wget2/merge_requests/6/diffs?diff_id=37', - comparePath: + short_commit_sha: 'de7a8f7f', + version_path: '/gnuwget/wget2/merge_requests/6/diffs?diff_id=37', + compare_path: '/gnuwget/wget2/merge_requests/6/diffs?diff_id=37&start_sha=de7a8f7f20c3ea2e0bef3ba01cfd41c21f6b4995', }, { - versionIndex: 3, - createdAt: '2018-10-23T11:46:40.617Z', - commitsCount: 3, + base_version_path: '/gnuwget/wget2/merge_requests/6/diffs?diff_id=36', + version_index: 3, + created_at: '2018-10-23T11:46:40.617Z', + commits_count: 3, latest: false, - shortCommitSha: 'e78fc18f', - versionPath: '/gnuwget/wget2/merge_requests/6/diffs?diff_id=36', - comparePath: + short_commit_sha: 'e78fc18f', + version_path: '/gnuwget/wget2/merge_requests/6/diffs?diff_id=36', + compare_path: '/gnuwget/wget2/merge_requests/6/diffs?diff_id=37&start_sha=e78fc18fa37acb2185c59ca94d4a964464feb50e', }, { - versionIndex: 2, - createdAt: '2018-10-04T09:57:39.648Z', - commitsCount: 2, + base_version_path: '/gnuwget/wget2/merge_requests/6/diffs?diff_id=35', + version_index: 2, + created_at: '2018-10-04T09:57:39.648Z', + commits_count: 2, latest: false, - shortCommitSha: '48da7e7e', - versionPath: '/gnuwget/wget2/merge_requests/6/diffs?diff_id=35', - comparePath: + short_commit_sha: '48da7e7e', + version_path: '/gnuwget/wget2/merge_requests/6/diffs?diff_id=35', + compare_path: '/gnuwget/wget2/merge_requests/6/diffs?diff_id=37&start_sha=48da7e7e9a99d41c852578bd9cb541ca4d864b3e', }, { - versionIndex: 1, - createdAt: '2018-09-25T20:30:39.493Z', - commitsCount: 1, + base_version_path: '/gnuwget/wget2/merge_requests/6/diffs?diff_id=20', + version_index: 1, + created_at: '2018-09-25T20:30:39.493Z', + commits_count: 1, latest: false, - shortCommitSha: '47bac2ed', - versionPath: '/gnuwget/wget2/merge_requests/6/diffs?diff_id=20', - comparePath: + short_commit_sha: '47bac2ed', + version_path: '/gnuwget/wget2/merge_requests/6/diffs?diff_id=20', + compare_path: '/gnuwget/wget2/merge_requests/6/diffs?diff_id=37&start_sha=47bac2ed972c5bee344c1cea159a22cd7f711dc0', }, ]; diff --git a/spec/javascripts/diffs/store/mutations_spec.js b/spec/javascripts/diffs/store/mutations_spec.js index d8733941181..c595c38ef55 100644 --- a/spec/javascripts/diffs/store/mutations_spec.js +++ b/spec/javascripts/diffs/store/mutations_spec.js @@ -628,4 +628,26 @@ describe('DiffsStoreMutations', () => { expect(file.parallel_diff_lines[1].right.hasForm).toBe(false); }); }); + + describe('SET_TREE_DATA', () => { + it('sets treeEntries and tree in state', () => { + const state = { + treeEntries: {}, + tree: [], + }; + + mutations[types.SET_TREE_DATA](state, { + treeEntries: { file: { name: 'index.js' } }, + tree: ['tree'], + }); + + expect(state.treeEntries).toEqual({ + file: { + name: 'index.js', + }, + }); + + expect(state.tree).toEqual(['tree']); + }); + }); }); diff --git a/spec/javascripts/diffs/store/utils_spec.js b/spec/javascripts/diffs/store/utils_spec.js index 036b320b314..3641946518b 100644 --- a/spec/javascripts/diffs/store/utils_spec.js +++ b/spec/javascripts/diffs/store/utils_spec.js @@ -251,45 +251,40 @@ describe('DiffsStoreUtils', () => { describe('trimFirstCharOfLineContent', () => { it('trims the line when it starts with a space', () => { expect(utils.trimFirstCharOfLineContent({ rich_text: ' diff' })).toEqual({ - discussions: [], rich_text: 'diff', }); }); it('trims the line when it starts with a +', () => { expect(utils.trimFirstCharOfLineContent({ rich_text: '+diff' })).toEqual({ - discussions: [], rich_text: 'diff', }); }); it('trims the line when it starts with a -', () => { expect(utils.trimFirstCharOfLineContent({ rich_text: '-diff' })).toEqual({ - discussions: [], rich_text: 'diff', }); }); it('does not trims the line when it starts with a letter', () => { expect(utils.trimFirstCharOfLineContent({ rich_text: 'diff' })).toEqual({ - discussions: [], rich_text: 'diff', }); }); it('does not modify the provided object', () => { const lineObj = { - discussions: [], rich_text: ' diff', }; utils.trimFirstCharOfLineContent(lineObj); - expect(lineObj).toEqual({ discussions: [], rich_text: ' diff' }); + expect(lineObj).toEqual({ rich_text: ' diff' }); }); it('handles a undefined or null parameter', () => { - expect(utils.trimFirstCharOfLineContent()).toEqual({ discussions: [] }); + expect(utils.trimFirstCharOfLineContent()).toEqual({}); }); }); @@ -601,4 +596,123 @@ describe('DiffsStoreUtils', () => { expect(utils.getDiffMode({})).toBe('replaced'); }); }); + + describe('getLowestSingleFolder', () => { + it('returns path and tree of lowest single folder tree', () => { + const folder = { + name: 'app', + type: 'tree', + tree: [ + { + name: 'javascripts', + type: 'tree', + tree: [ + { + type: 'blob', + name: 'index.js', + }, + ], + }, + ], + }; + const { path, treeAcc } = utils.getLowestSingleFolder(folder); + + expect(path).toEqual('app/javascripts'); + expect(treeAcc).toEqual([ + { + type: 'blob', + name: 'index.js', + }, + ]); + }); + + it('returns passed in folders path & tree when more than tree exists', () => { + const folder = { + name: 'app', + type: 'tree', + tree: [ + { + name: 'spec', + type: 'blob', + tree: [], + }, + ], + }; + const { path, treeAcc } = utils.getLowestSingleFolder(folder); + + expect(path).toEqual('app'); + expect(treeAcc).toBeNull(); + }); + }); + + describe('flattenTree', () => { + it('returns flattened directory structure', () => { + const tree = [ + { + type: 'tree', + name: 'app', + tree: [ + { + type: 'tree', + name: 'javascripts', + tree: [ + { + type: 'blob', + name: 'index.js', + tree: [], + }, + ], + }, + ], + }, + { + type: 'tree', + name: 'spec', + tree: [ + { + type: 'tree', + name: 'javascripts', + tree: [], + }, + { + type: 'blob', + name: 'index_spec.js', + tree: [], + }, + ], + }, + ]; + const flattened = utils.flattenTree(tree); + + expect(flattened).toEqual([ + { + type: 'tree', + name: 'app/javascripts', + tree: [ + { + type: 'blob', + name: 'index.js', + tree: [], + }, + ], + }, + { + type: 'tree', + name: 'spec', + tree: [ + { + type: 'tree', + name: 'javascripts', + tree: [], + }, + { + type: 'blob', + name: 'index_spec.js', + tree: [], + }, + ], + }, + ]); + }); + }); }); diff --git a/spec/javascripts/dirty_submit/dirty_submit_collection_spec.js b/spec/javascripts/dirty_submit/dirty_submit_collection_spec.js index 08ffc44605f..47be0b3ce9d 100644 --- a/spec/javascripts/dirty_submit/dirty_submit_collection_spec.js +++ b/spec/javascripts/dirty_submit/dirty_submit_collection_spec.js @@ -1,5 +1,5 @@ import DirtySubmitCollection from '~/dirty_submit/dirty_submit_collection'; -import { setInput, createForm } from './helper'; +import { setInputValue, createForm } from './helper'; describe('DirtySubmitCollection', () => { it('disables submits until there are changes', done => { @@ -14,11 +14,11 @@ describe('DirtySubmitCollection', () => { expect(submit.disabled).toBe(true); - return setInput(input, `${originalValue} changes`) + return setInputValue(input, `${originalValue} changes`) .then(() => { expect(submit.disabled).toBe(false); }) - .then(() => setInput(input, originalValue)) + .then(() => setInputValue(input, originalValue)) .then(() => { expect(submit.disabled).toBe(true); }) diff --git a/spec/javascripts/dirty_submit/dirty_submit_form_spec.js b/spec/javascripts/dirty_submit/dirty_submit_form_spec.js index 093fec97951..ae2a785de52 100644 --- a/spec/javascripts/dirty_submit/dirty_submit_form_spec.js +++ b/spec/javascripts/dirty_submit/dirty_submit_form_spec.js @@ -1,14 +1,14 @@ import DirtySubmitForm from '~/dirty_submit/dirty_submit_form'; -import { setInput, createForm } from './helper'; +import { getInputValue, setInputValue, createForm } from './helper'; function expectToToggleDisableOnDirtyUpdate(submit, input) { - const originalValue = input.value; + const originalValue = getInputValue(input); expect(submit.disabled).toBe(true); - return setInput(input, `${originalValue} changes`) + return setInputValue(input, `${originalValue} changes`) .then(() => expect(submit.disabled).toBe(false)) - .then(() => setInput(input, originalValue)) + .then(() => setInputValue(input, originalValue)) .then(() => expect(submit.disabled).toBe(true)); } @@ -33,4 +33,24 @@ describe('DirtySubmitForm', () => { .then(done) .catch(done.fail); }); + + it('disables submit until there are changes for radio inputs', done => { + const { form, input, submit } = createForm('radio'); + + new DirtySubmitForm(form); // eslint-disable-line no-new + + return expectToToggleDisableOnDirtyUpdate(submit, input) + .then(done) + .catch(done.fail); + }); + + it('disables submit until there are changes for checkbox inputs', done => { + const { form, input, submit } = createForm('checkbox'); + + new DirtySubmitForm(form); // eslint-disable-line no-new + + return expectToToggleDisableOnDirtyUpdate(submit, input) + .then(done) + .catch(done.fail); + }); }); diff --git a/spec/javascripts/dirty_submit/helper.js b/spec/javascripts/dirty_submit/helper.js index 6d1e643553c..b51783cb915 100644 --- a/spec/javascripts/dirty_submit/helper.js +++ b/spec/javascripts/dirty_submit/helper.js @@ -1,25 +1,42 @@ import DirtySubmitForm from '~/dirty_submit/dirty_submit_form'; import setTimeoutPromiseHelper from '../helpers/set_timeout_promise_helper'; -export function setInput(element, value) { - element.value = value; +function isCheckableType(type) { + return /^(radio|checkbox)$/.test(type); +} + +export function setInputValue(element, value) { + const { type } = element; + let eventType; + + if (isCheckableType(type)) { + element.checked = !element.checked; + eventType = 'change'; + } else { + element.value = value; + eventType = 'input'; + } element.dispatchEvent( - new Event('input', { + new Event(eventType, { bubbles: true, - cancelable: true, }), ); return setTimeoutPromiseHelper(DirtySubmitForm.THROTTLE_DURATION); } -export function createForm() { +export function getInputValue(input) { + return isCheckableType(input.type) ? input.checked : input.value; +} + +export function createForm(type = 'text') { const form = document.createElement('form'); form.innerHTML = ` - <input type="text" value="original" class="js-input" name="input" /> + <input type="${type}" name="${type}" class="js-input"/> <button type="submit" class="js-dirty-submit"></button> `; + const input = form.querySelector('.js-input'); const submit = form.querySelector('.js-dirty-submit'); diff --git a/spec/javascripts/jobs/components/sidebar_spec.js b/spec/javascripts/jobs/components/sidebar_spec.js index b0bc16d7c64..3a02351460c 100644 --- a/spec/javascripts/jobs/components/sidebar_spec.js +++ b/spec/javascripts/jobs/components/sidebar_spec.js @@ -28,7 +28,7 @@ describe('Sidebar details block', () => { store, }); - expect(vm.$el.querySelector('.js-retry-job')).toBeNull(); + expect(vm.$el.querySelector('.js-retry-button')).toBeNull(); }); }); @@ -70,7 +70,7 @@ describe('Sidebar details block', () => { }); it('should render link to retry job', () => { - expect(vm.$el.querySelector('.js-retry-job').getAttribute('href')).toEqual(job.retry_path); + expect(vm.$el.querySelector('.js-retry-button').getAttribute('href')).toEqual(job.retry_path); }); it('should render link to cancel job', () => { diff --git a/spec/javascripts/jobs/store/getters_spec.js b/spec/javascripts/jobs/store/getters_spec.js index 4195d9d3680..7931b2af79f 100644 --- a/spec/javascripts/jobs/store/getters_spec.js +++ b/spec/javascripts/jobs/store/getters_spec.js @@ -8,30 +8,6 @@ describe('Job Store Getters', () => { localState = state(); }); - describe('headerActions', () => { - describe('with new issue path', () => { - it('returns an array with action to create a new issue', () => { - localState.job.new_issue_path = 'issues/new'; - - expect(getters.headerActions(localState)).toEqual([ - { - label: 'New issue', - path: localState.job.new_issue_path, - cssClass: - 'js-new-issue btn btn-success btn-inverted d-none d-md-block d-lg-block d-xl-block', - type: 'link', - }, - ]); - }); - }); - - describe('without new issue path', () => { - it('returns an empty array', () => { - expect(getters.headerActions(localState)).toEqual([]); - }); - }); - }); - describe('headerTime', () => { describe('when the job has started key', () => { it('returns started key', () => { diff --git a/spec/javascripts/lib/utils/common_utils_spec.js b/spec/javascripts/lib/utils/common_utils_spec.js index f320f232687..121c4040212 100644 --- a/spec/javascripts/lib/utils/common_utils_spec.js +++ b/spec/javascripts/lib/utils/common_utils_spec.js @@ -3,6 +3,25 @@ import * as commonUtils from '~/lib/utils/common_utils'; import MockAdapter from 'axios-mock-adapter'; import { faviconDataUrl, overlayDataUrl, faviconWithOverlayDataUrl } from './mock_data'; +const PIXEL_TOLERANCE = 0.2; + +/** + * Loads a data URL as the src of an + * {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLImageElement/Image|Image} + * and resolves to that Image once loaded. + * + * @param url + * @returns {Promise} + */ +const urlToImage = url => + new Promise(resolve => { + const img = new Image(); + img.onload = function() { + resolve(img); + }; + img.src = url; + }); + describe('common_utils', () => { describe('parseUrl', () => { it('returns an anchor tag with url', () => { @@ -347,20 +366,31 @@ describe('common_utils', () => { }); describe('parseBoolean', () => { + const { parseBoolean } = commonUtils; + it('returns true for "true"', () => { - expect(commonUtils.parseBoolean('true')).toEqual(true); + expect(parseBoolean('true')).toEqual(true); }); it('returns false for "false"', () => { - expect(commonUtils.parseBoolean('false')).toEqual(false); + expect(parseBoolean('false')).toEqual(false); }); it('returns false for "something"', () => { - expect(commonUtils.parseBoolean('something')).toEqual(false); + expect(parseBoolean('something')).toEqual(false); }); it('returns false for null', () => { - expect(commonUtils.parseBoolean(null)).toEqual(false); + expect(parseBoolean(null)).toEqual(false); + }); + + it('is idempotent', () => { + const input = ['true', 'false', 'something', null]; + input.forEach(value => { + const result = parseBoolean(value); + + expect(parseBoolean(result)).toBe(result); + }); }); }); @@ -502,8 +532,9 @@ describe('common_utils', () => { it('should return the favicon with the overlay', done => { commonUtils .createOverlayIcon(faviconDataUrl, overlayDataUrl) - .then(url => { - expect(url).toEqual(faviconWithOverlayDataUrl); + .then(url => Promise.all([urlToImage(url), urlToImage(faviconWithOverlayDataUrl)])) + .then(([actual, expected]) => { + expect(actual).toImageDiffEqual(expected, PIXEL_TOLERANCE); done(); }) .catch(done.fail); @@ -525,10 +556,10 @@ describe('common_utils', () => { it('should set page favicon to provided favicon overlay', done => { commonUtils .setFaviconOverlay(overlayDataUrl) - .then(() => { - expect(document.getElementById('favicon').getAttribute('href')).toEqual( - faviconWithOverlayDataUrl, - ); + .then(() => document.getElementById('favicon').getAttribute('href')) + .then(url => Promise.all([urlToImage(url), urlToImage(faviconWithOverlayDataUrl)])) + .then(([actual, expected]) => { + expect(actual).toImageDiffEqual(expected, PIXEL_TOLERANCE); done(); }) .catch(done.fail); @@ -571,10 +602,10 @@ describe('common_utils', () => { commonUtils .setCiStatusFavicon(BUILD_URL) - .then(() => { - const favicon = document.getElementById('favicon'); - - expect(favicon.getAttribute('href')).toEqual(faviconWithOverlayDataUrl); + .then(() => document.getElementById('favicon').getAttribute('href')) + .then(url => Promise.all([urlToImage(url), urlToImage(faviconWithOverlayDataUrl)])) + .then(([actual, expected]) => { + expect(actual).toImageDiffEqual(expected, PIXEL_TOLERANCE); done(); }) .catch(done.fail); diff --git a/spec/javascripts/matchers.js b/spec/javascripts/matchers.js index 0d465510fd3..406527b08a3 100644 --- a/spec/javascripts/matchers.js +++ b/spec/javascripts/matchers.js @@ -1,3 +1,5 @@ +import pixelmatch from 'pixelmatch'; + export default { toContainText: () => ({ compare(vm, text) { @@ -54,4 +56,41 @@ export default { return result; }, }), + toImageDiffEqual: () => { + const getImageData = img => { + const canvas = document.createElement('canvas'); + canvas.width = img.width; + canvas.height = img.height; + canvas.getContext('2d').drawImage(img, 0, 0); + return canvas.getContext('2d').getImageData(0, 0, img.width, img.height).data; + }; + + return { + compare(actual, expected, threshold = 0.1) { + if (actual.height !== expected.height || actual.width !== expected.width) { + return { + pass: false, + message: `Expected image dimensions (h x w) of ${expected.height}x${expected.width}. + Received an image with ${actual.height}x${actual.width}`, + }; + } + + const { width, height } = actual; + const differentPixels = pixelmatch( + getImageData(actual), + getImageData(expected), + null, + width, + height, + { threshold }, + ); + + return { + pass: differentPixels < 20, + message: `${differentPixels} pixels differ more than ${threshold * + 100} percent between input and output.`, + }; + }, + }; + }, }; diff --git a/spec/javascripts/notes/stores/mutation_spec.js b/spec/javascripts/notes/stores/mutation_spec.js index 3fbae82f16c..b6b2c7d60a5 100644 --- a/spec/javascripts/notes/stores/mutation_spec.js +++ b/spec/javascripts/notes/stores/mutation_spec.js @@ -179,11 +179,11 @@ describe('Notes Store mutations', () => { diff_file: { file_hash: 'a', }, - truncated_diff_lines: ['a'], + truncated_diff_lines: [{ text: '+a', rich_text: '+<span>a</span>' }], }, ]); - expect(state.discussions[0].truncated_diff_lines).toEqual(['a']); + expect(state.discussions[0].truncated_diff_lines).toEqual([{ rich_text: '<span>a</span>' }]); }); it('adds empty truncated_diff_lines when not in discussion', () => { @@ -420,9 +420,12 @@ describe('Notes Store mutations', () => { ], }; - mutations.SET_DISCUSSION_DIFF_LINES(state, { discussionId: 1, diffLines: ['test'] }); + mutations.SET_DISCUSSION_DIFF_LINES(state, { + discussionId: 1, + diffLines: [{ text: '+a', rich_text: '+<span>a</span>' }], + }); - expect(state.discussions[0].truncated_diff_lines).toEqual(['test']); + expect(state.discussions[0].truncated_diff_lines).toEqual([{ rich_text: '<span>a</span>' }]); }); it('keeps reactivity of discussion', () => { @@ -435,7 +438,10 @@ describe('Notes Store mutations', () => { ]); const discussion = state.discussions[0]; - mutations.SET_DISCUSSION_DIFF_LINES(state, { discussionId: 1, diffLines: ['test'] }); + mutations.SET_DISCUSSION_DIFF_LINES(state, { + discussionId: 1, + diffLines: [{ rich_text: '<span>a</span>' }], + }); discussion.expanded = true; diff --git a/spec/javascripts/test_bundle.js b/spec/javascripts/test_bundle.js index 96c0844f83c..547379dabed 100644 --- a/spec/javascripts/test_bundle.js +++ b/spec/javascripts/test_bundle.js @@ -187,6 +187,7 @@ if (process.env.BABEL_ENV === 'coverage') { './terminal/terminal_bundle.js', './users/users_bundle.js', './issue_show/index.js', + './pages/admin/application_settings/show/index.js', ]; describe('Uncovered files', function() { diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js index 2119a3b927a..e387367d1a2 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js @@ -1,11 +1,12 @@ import Vue from 'vue'; import ReadyToMerge from '~/vue_merge_request_widget/components/states/ready_to_merge.vue'; +import SquashBeforeMerge from '~/vue_merge_request_widget/components/states/squash_before_merge.vue'; import eventHub from '~/vue_merge_request_widget/event_hub'; +import { createLocalVue, shallowMount } from '@vue/test-utils'; const commitMessage = 'This is the commit message'; const commitMessageWithDescription = 'This is the commit message description'; -const createComponent = (customConfig = {}) => { - const Component = Vue.extend(ReadyToMerge); +const createTestMr = customConfig => { const mr = { isPipelineActive: false, pipeline: null, @@ -16,6 +17,7 @@ const createComponent = (customConfig = {}) => { hasCI: false, ciStatus: null, sha: '12345678', + squash: false, commitMessage, commitMessageWithDescription, shouldRemoveSourceBranch: true, @@ -24,14 +26,23 @@ const createComponent = (customConfig = {}) => { Object.assign(mr, customConfig.mr); - const service = { - merge() {}, - poll() {}, - }; + return mr; +}; + +const createTestService = () => ({ + merge() {}, + poll() {}, +}); + +const createComponent = (customConfig = {}) => { + const Component = Vue.extend(ReadyToMerge); return new Component({ el: document.createElement('div'), - propsData: { mr, service }, + propsData: { + mr: createTestMr(customConfig), + service: createTestService(), + }, }); }; @@ -612,6 +623,47 @@ describe('ReadyToMerge', () => { }); }); + describe('Squash checkbox component', () => { + let wrapper; + const localVue = createLocalVue(); + + const createLocalComponent = (customConfig = {}) => { + wrapper = shallowMount(localVue.extend(ReadyToMerge), { + localVue, + propsData: { + mr: createTestMr(customConfig), + service: createTestService(), + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + const findCheckboxElement = () => wrapper.find(SquashBeforeMerge); + + it('should be rendered when squash before merge is enabled and there is more than 1 commit', () => { + createLocalComponent({ + mr: { commitsCount: 2, enableSquashBeforeMerge: true }, + }); + + expect(findCheckboxElement().exists()).toBeTruthy(); + }); + + it('should not be rendered when squash before merge is disabled', () => { + createLocalComponent({ mr: { commitsCount: 2, enableSquashBeforeMerge: false } }); + + expect(findCheckboxElement().exists()).toBeFalsy(); + }); + + it('should not be rendered when there is only 1 commit', () => { + createLocalComponent({ mr: { commitsCount: 1, enableSquashBeforeMerge: true } }); + + expect(findCheckboxElement().exists()).toBeFalsy(); + }); + }); + describe('Merge controls', () => { describe('when allowed to merge', () => { beforeEach(() => { diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_squash_before_merge_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_squash_before_merge_spec.js new file mode 100644 index 00000000000..d6d8eecfcb9 --- /dev/null +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_squash_before_merge_spec.js @@ -0,0 +1,100 @@ +import { createLocalVue, shallowMount } from '@vue/test-utils'; +import SquashBeforeMerge from '~/vue_merge_request_widget/components/states/squash_before_merge.vue'; + +const localVue = createLocalVue(); + +describe('Squash before merge component', () => { + let wrapper; + + const createComponent = props => { + wrapper = shallowMount(localVue.extend(SquashBeforeMerge), { + localVue, + sync: false, + propsData: { + ...props, + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + describe('checkbox', () => { + const findCheckbox = () => wrapper.find('.qa-squash-checkbox'); + + it('is unchecked if passed value prop is false', () => { + createComponent({ + value: false, + }); + + expect(findCheckbox().element.checked).toBeFalsy(); + }); + + it('is checked if passed value prop is true', () => { + createComponent({ + value: true, + }); + + expect(findCheckbox().element.checked).toBeTruthy(); + }); + + it('changes value on click', done => { + createComponent({ + value: false, + }); + + findCheckbox().element.checked = true; + + findCheckbox().trigger('change'); + + wrapper.vm.$nextTick(() => { + expect(findCheckbox().element.checked).toBeTruthy(); + done(); + }); + }); + + it('is disabled if isDisabled prop is true', () => { + createComponent({ + value: false, + isDisabled: true, + }); + + expect(findCheckbox().attributes('disabled')).toBeTruthy(); + }); + }); + + describe('about link', () => { + it('is not rendered if no help path is passed', () => { + createComponent({ + value: false, + }); + + const aboutLink = wrapper.find('a'); + + expect(aboutLink.exists()).toBeFalsy(); + }); + + it('is rendered if help path is passed', () => { + createComponent({ + value: false, + helpPath: 'test-path', + }); + + const aboutLink = wrapper.find('a'); + + expect(aboutLink.exists()).toBeTruthy(); + }); + + it('should have a correct help path if passed', () => { + createComponent({ + value: false, + helpPath: 'test-path', + }); + + const aboutLink = wrapper.find('a'); + + expect(aboutLink.attributes('href')).toEqual('test-path'); + }); + }); +}); diff --git a/spec/javascripts/vue_shared/components/markdown/suggestion_diff_header_spec.js b/spec/javascripts/vue_shared/components/markdown/suggestion_diff_header_spec.js index 8187b3204b1..12ee804f668 100644 --- a/spec/javascripts/vue_shared/components/markdown/suggestion_diff_header_spec.js +++ b/spec/javascripts/vue_shared/components/markdown/suggestion_diff_header_spec.js @@ -31,6 +31,12 @@ describe('Suggestion Diff component', () => { expect(header.innerHTML.includes('Suggested change')).toBe(true); }); + it('renders a help button', () => { + const helpBtn = vm.$el.querySelector('.js-help-btn'); + + expect(helpBtn).not.toBeNull(); + }); + it('renders an apply button', () => { const applyBtn = vm.$el.querySelector('.qa-apply-btn'); diff --git a/spec/javascripts/vue_shared/components/markdown/suggestions_spec.js b/spec/javascripts/vue_shared/components/markdown/suggestions_spec.js index 423cd6dee0f..33be63a3a1e 100644 --- a/spec/javascripts/vue_shared/components/markdown/suggestions_spec.js +++ b/spec/javascripts/vue_shared/components/markdown/suggestions_spec.js @@ -61,7 +61,7 @@ describe('Suggestion component', () => { describe('mounted', () => { it('renders a flash container', () => { - expect(vm.$el.querySelector('.flash-container')).not.toBeNull(); + expect(vm.$el.querySelector('.js-suggestions-flash')).not.toBeNull(); }); it('renders a container for suggestions', () => { diff --git a/spec/javascripts/vue_shared/components/user_avatar/user_avatar_list_spec.js b/spec/javascripts/vue_shared/components/user_avatar/user_avatar_list_spec.js index 64aa7e29718..96bc3b0cc17 100644 --- a/spec/javascripts/vue_shared/components/user_avatar/user_avatar_list_spec.js +++ b/spec/javascripts/vue_shared/components/user_avatar/user_avatar_list_spec.js @@ -6,6 +6,8 @@ import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link const TEST_IMAGE_SIZE = 7; const TEST_BREAKPOINT = 5; +const TEST_EMPTY_MESSAGE = 'Lorem ipsum empty'; +const DEFAULT_EMPTY_MESSAGE = 'None'; const createUser = id => ({ id, @@ -21,14 +23,19 @@ const createList = n => const localVue = createLocalVue(); describe('UserAvatarList', () => { - let propsData; + let props; let wrapper; - const factory = options => { + const factory = (options = {}) => { + const propsData = { + ...props, + ...options.propsData, + }; + wrapper = shallowMount(localVue.extend(UserAvatarList), { + ...options, localVue, propsData, - ...options, }); }; @@ -38,28 +45,47 @@ describe('UserAvatarList', () => { }; beforeEach(() => { - propsData = { imgSize: TEST_IMAGE_SIZE }; + props = { imgSize: TEST_IMAGE_SIZE }; }); afterEach(() => { wrapper.destroy(); }); + describe('empty text', () => { + it('shows when items are empty', () => { + factory({ propsData: { items: [] } }); + + expect(wrapper.text()).toContain(DEFAULT_EMPTY_MESSAGE); + }); + + it('does not show when items are not empty', () => { + factory({ propsData: { items: createList(1) } }); + + expect(wrapper.text()).not.toContain(DEFAULT_EMPTY_MESSAGE); + }); + + it('can be set in props', () => { + factory({ propsData: { items: [], emptyText: TEST_EMPTY_MESSAGE } }); + + expect(wrapper.text()).toContain(TEST_EMPTY_MESSAGE); + }); + }); + describe('with no breakpoint', () => { beforeEach(() => { - propsData.breakpoint = 0; + props.breakpoint = 0; }); it('renders avatars', () => { const items = createList(20); - propsData.items = items; - factory(); + factory({ propsData: { items } }); const links = wrapper.findAll(UserAvatarLink); const linkProps = links.wrappers.map(x => x.props()); expect(linkProps).toEqual( - propsData.items.map(x => + items.map(x => jasmine.objectContaining({ linkHref: x.web_url, imgSrc: x.avatar_url, @@ -74,8 +100,8 @@ describe('UserAvatarList', () => { describe('with breakpoint and length equal to breakpoint', () => { beforeEach(() => { - propsData.breakpoint = TEST_BREAKPOINT; - propsData.items = createList(TEST_BREAKPOINT); + props.breakpoint = TEST_BREAKPOINT; + props.items = createList(TEST_BREAKPOINT); }); it('renders all avatars if length is <= breakpoint', () => { @@ -83,7 +109,7 @@ describe('UserAvatarList', () => { const links = wrapper.findAll(UserAvatarLink); - expect(links.length).toEqual(propsData.items.length); + expect(links.length).toEqual(props.items.length); }); it('does not show button', () => { @@ -95,8 +121,8 @@ describe('UserAvatarList', () => { describe('with breakpoint and length greater than breakpoint', () => { beforeEach(() => { - propsData.breakpoint = TEST_BREAKPOINT; - propsData.items = createList(TEST_BREAKPOINT + 1); + props.breakpoint = TEST_BREAKPOINT; + props.items = createList(TEST_BREAKPOINT + 1); }); it('renders avatars up to breakpoint', () => { @@ -116,7 +142,7 @@ describe('UserAvatarList', () => { it('renders all avatars', () => { const links = wrapper.findAll(UserAvatarLink); - expect(links.length).toEqual(propsData.items.length); + expect(links.length).toEqual(props.items.length); }); it('with collapse clicked, it renders avatars up to breakpoint', () => { diff --git a/spec/lib/banzai/filter/footnote_filter_spec.rb b/spec/lib/banzai/filter/footnote_filter_spec.rb new file mode 100644 index 00000000000..2e50e4e2351 --- /dev/null +++ b/spec/lib/banzai/filter/footnote_filter_spec.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Banzai::Filter::FootnoteFilter do + include FilterSpecHelper + + # first[^1] and second[^second] + # [^1]: one + # [^second]: two + let(:footnote) do + <<~EOF + <p>first<sup><a href="#fn1" id="fnref1">1</a></sup> and second<sup><a href="#fn2" id="fnref2">2</a></sup></p> + <ol> + <li id="fn1"> + <p>one <a href="#fnref1">↩</a></p> + </li> + <li id="fn2"> + <p>two <a href="#fnref2">↩</a></p> + </li> + </ol> + EOF + end + + let(:filtered_footnote) do + <<~EOF + <p>first<sup class="footnote-ref"><a href="#fn1-#{identifier}" id="fnref1-#{identifier}">1</a></sup> and second<sup class="footnote-ref"><a href="#fn2-#{identifier}" id="fnref2-#{identifier}">2</a></sup></p> + <section class="footnotes"><ol> + <li id="fn1-#{identifier}"> + <p>one <a href="#fnref1-#{identifier}" class="footnote-backref">↩</a></p> + </li> + <li id="fn2-#{identifier}"> + <p>two <a href="#fnref2-#{identifier}" class="footnote-backref">↩</a></p> + </li> + </ol></section> + EOF + end + + context 'when footnotes exist' do + let(:doc) { filter(footnote) } + let(:link_node) { doc.css('sup > a').first } + let(:identifier) { link_node[:id].delete_prefix('fnref1-') } + + it 'properly adds the necessary ids and classes' do + expect(doc.to_html).to eq filtered_footnote + end + end +end diff --git a/spec/lib/banzai/filter/sanitization_filter_spec.rb b/spec/lib/banzai/filter/sanitization_filter_spec.rb index 0b3c2390304..836af18e0b6 100644 --- a/spec/lib/banzai/filter/sanitization_filter_spec.rb +++ b/spec/lib/banzai/filter/sanitization_filter_spec.rb @@ -246,7 +246,7 @@ describe Banzai::Filter::SanitizationFilter do 'protocol-based JS injection: spaces and entities' => { input: '<a href="  javascript:alert(\'XSS\');">foo</a>', - output: '<a href>foo</a>' + output: '<a href="">foo</a>' }, 'protocol whitespace' => { @@ -300,5 +300,48 @@ describe Banzai::Filter::SanitizationFilter do expect(act.to_html).to eq exp end + + describe 'footnotes' do + it 'allows correct footnote id property on links' do + exp = %q{<a href="#fn1" id="fnref1">foo/bar.md</a>} + act = filter(exp) + + expect(act.to_html).to eq exp + end + + it 'allows correct footnote id property on li element' do + exp = %q{<ol><li id="fn1">footnote</li></ol>} + act = filter(exp) + + expect(act.to_html).to eq exp + end + + it 'removes invalid id for footnote links' do + exp = %q{<a href="#fn1">link</a>} + + %w[fnrefx test xfnref1].each do |id| + act = filter(%Q{<a href="#fn1" id="#{id}">link</a>}) + + expect(act.to_html).to eq exp + end + end + + it 'removes invalid id for footnote li' do + exp = %q{<ol><li>footnote</li></ol>} + + %w[fnx test xfn1].each do |id| + act = filter(%Q{<ol><li id="#{id}">footnote</li></ol>}) + + expect(act.to_html).to eq exp + end + end + + it 'allows footnotes numbered higher than 9' do + exp = %q{<a href="#fn15" id="fnref15">link</a><ol><li id="fn15">footnote</li></ol>} + act = filter(exp) + + expect(act.to_html).to eq exp + end + end end end diff --git a/spec/lib/banzai/pipeline/full_pipeline_spec.rb b/spec/lib/banzai/pipeline/full_pipeline_spec.rb index e9c7a2f352e..3634655c6a5 100644 --- a/spec/lib/banzai/pipeline/full_pipeline_spec.rb +++ b/spec/lib/banzai/pipeline/full_pipeline_spec.rb @@ -25,4 +25,36 @@ describe Banzai::Pipeline::FullPipeline do expect(result).to include(%{data-original='\">bad things'}) end end + + describe 'footnotes' do + let(:project) { create(:project, :public) } + let(:html) { described_class.to_html(footnote_markdown, project: project) } + let(:identifier) { html[/.*fnref1-(\d+).*/, 1] } + let(:footnote_markdown) do + <<~EOF + first[^1] and second[^second] + [^1]: one + [^second]: two + EOF + end + + let(:filtered_footnote) do + <<~EOF + <p dir="auto">first<sup class="footnote-ref"><a href="#fn1-#{identifier}" id="fnref1-#{identifier}">1</a></sup> and second<sup class="footnote-ref"><a href="#fn2-#{identifier}" id="fnref2-#{identifier}">2</a></sup></p> + + <section class="footnotes"><ol> + <li id="fn1-#{identifier}"> + <p>one <a href="#fnref1-#{identifier}" class="footnote-backref"><gl-emoji title="leftwards arrow with hook" data-name="leftwards_arrow_with_hook" data-unicode-version="1.1">↩</gl-emoji></a></p> + </li> + <li id="fn2-#{identifier}"> + <p>two <a href="#fnref2-#{identifier}" class="footnote-backref"><gl-emoji title="leftwards arrow with hook" data-name="leftwards_arrow_with_hook" data-unicode-version="1.1">↩</gl-emoji></a></p> + </li> + </ol></section> + EOF + end + + it 'properly adds the necessary ids and classes' do + expect(html.lines.map(&:strip).join("\n")).to eq filtered_footnote + end + end end diff --git a/spec/lib/bitbucket_server/client_spec.rb b/spec/lib/bitbucket_server/client_spec.rb index 5de0a9a65b5..b021e69800a 100644 --- a/spec/lib/bitbucket_server/client_spec.rb +++ b/spec/lib/bitbucket_server/client_spec.rb @@ -17,12 +17,6 @@ describe BitbucketServer::Client do subject.pull_requests(project, repo_slug) end - - it 'throws an exception when connection fails' do - allow(BitbucketServer::Collection).to receive(:new).and_raise(OpenSSL::SSL::SSLError) - - expect { subject.pull_requests(project, repo_slug) }.to raise_error(described_class::ServerError) - end end describe '#activities' do diff --git a/spec/lib/bitbucket_server/connection_spec.rb b/spec/lib/bitbucket_server/connection_spec.rb index b5da4cb1a49..ab8a5b89608 100644 --- a/spec/lib/bitbucket_server/connection_spec.rb +++ b/spec/lib/bitbucket_server/connection_spec.rb @@ -26,6 +26,12 @@ describe BitbucketServer::Connection do expect { subject.get(url) }.to raise_error(described_class::ConnectionError) end + + it 'throws an exception upon a network error' do + WebMock.stub_request(:get, url).with(headers: { 'Accept' => 'application/json' }).to_raise(OpenSSL::SSL::SSLError) + + expect { subject.get(url) }.to raise_error(described_class::ConnectionError) + end end describe '#post' do @@ -42,6 +48,12 @@ describe BitbucketServer::Connection do expect { subject.post(url, payload) }.to raise_error(described_class::ConnectionError) end + + it 'throws an exception upon a network error' do + WebMock.stub_request(:post, url).with(headers: { 'Accept' => 'application/json' }).to_raise(OpenSSL::SSL::SSLError) + + expect { subject.post(url, payload) }.to raise_error(described_class::ConnectionError) + end end describe '#delete' do @@ -63,6 +75,12 @@ describe BitbucketServer::Connection do expect { subject.delete(:branches, branch_path, payload) }.to raise_error(described_class::ConnectionError) end + + it 'throws an exception upon a network error' do + WebMock.stub_request(:delete, branch_url).with(headers: headers).to_raise(OpenSSL::SSL::SSLError) + + expect { subject.delete(:branches, branch_path, payload) }.to raise_error(described_class::ConnectionError) + end end end end diff --git a/spec/lib/feature_spec.rb b/spec/lib/feature_spec.rb index 9d56c62ae57..630732614b2 100644 --- a/spec/lib/feature_spec.rb +++ b/spec/lib/feature_spec.rb @@ -182,4 +182,18 @@ describe Feature do expect(described_class.disabled?(:enabled_feature_flag)).to be_falsey end end + + describe Feature::Target do + describe '#targets' do + let(:project) { create(:project) } + let(:user_name) { project.owner.username } + + subject { described_class.new(user: user_name, project: project.full_path) } + + it 'returns all found targets' do + expect(subject.targets).to be_an(Array) + expect(subject.targets).to eq([project.owner, project]) + end + end + end end diff --git a/spec/lib/gitlab/access/branch_protection_spec.rb b/spec/lib/gitlab/access/branch_protection_spec.rb new file mode 100644 index 00000000000..7f2979e8e28 --- /dev/null +++ b/spec/lib/gitlab/access/branch_protection_spec.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Access::BranchProtection do + describe '#any?' do + using RSpec::Parameterized::TableSyntax + + where(:level, :result) do + Gitlab::Access::PROTECTION_NONE | false + Gitlab::Access::PROTECTION_DEV_CAN_PUSH | true + Gitlab::Access::PROTECTION_DEV_CAN_MERGE | true + Gitlab::Access::PROTECTION_FULL | true + end + + with_them do + it { expect(described_class.new(level).any?).to eq(result) } + end + end + + describe '#developer_can_push?' do + using RSpec::Parameterized::TableSyntax + + where(:level, :result) do + Gitlab::Access::PROTECTION_NONE | false + Gitlab::Access::PROTECTION_DEV_CAN_PUSH | true + Gitlab::Access::PROTECTION_DEV_CAN_MERGE | false + Gitlab::Access::PROTECTION_FULL | false + end + + with_them do + it do + expect(described_class.new(level).developer_can_push?).to eq(result) + end + end + end + + describe '#developer_can_merge?' do + using RSpec::Parameterized::TableSyntax + + where(:level, :result) do + Gitlab::Access::PROTECTION_NONE | false + Gitlab::Access::PROTECTION_DEV_CAN_PUSH | false + Gitlab::Access::PROTECTION_DEV_CAN_MERGE | true + Gitlab::Access::PROTECTION_FULL | false + end + + with_them do + it do + expect(described_class.new(level).developer_can_merge?).to eq(result) + end + end + end +end diff --git a/spec/lib/gitlab/background_migration/backfill_project_repositories_spec.rb b/spec/lib/gitlab/background_migration/backfill_project_repositories_spec.rb index 53c071f0268..510a0074554 100644 --- a/spec/lib/gitlab/background_migration/backfill_project_repositories_spec.rb +++ b/spec/lib/gitlab/background_migration/backfill_project_repositories_spec.rb @@ -2,6 +2,7 @@ require 'spec_helper' +# rubocop:disable RSpec/FactoriesInMigrationSpecs describe Gitlab::BackgroundMigration::BackfillProjectRepositories do let(:group) { create(:group, name: 'foo', path: 'foo') } @@ -34,6 +35,7 @@ describe Gitlab::BackgroundMigration::BackfillProjectRepositories do let!(:project_hashed_storage_2) { create(:project, name: 'bar', path: 'bar', namespace: group, storage_version: 2) } let!(:project_legacy_storage_3) { create(:project, name: 'baz', path: 'baz', namespace: group, storage_version: 0) } let!(:project_legacy_storage_4) { create(:project, name: 'zoo', path: 'zoo', namespace: group, storage_version: nil) } + let!(:project_legacy_storage_5) { create(:project, name: 'test', path: 'test', namespace: group, storage_version: nil) } describe '.on_hashed_storage' do it 'finds projects with repository on hashed storage' do @@ -47,7 +49,7 @@ describe Gitlab::BackgroundMigration::BackfillProjectRepositories do it 'finds projects with repository on legacy storage' do projects = described_class.on_legacy_storage.pluck(:id) - expect(projects).to match_array([project_legacy_storage_3.id, project_legacy_storage_4.id]) + expect(projects).to match_array([project_legacy_storage_3.id, project_legacy_storage_4.id, project_legacy_storage_5.id]) end end @@ -58,7 +60,7 @@ describe Gitlab::BackgroundMigration::BackfillProjectRepositories do projects = described_class.without_project_repository.pluck(:id) - expect(projects).to contain_exactly(project_hashed_storage_2.id, project_legacy_storage_4.id) + expect(projects).to contain_exactly(project_hashed_storage_2.id, project_legacy_storage_4.id, project_legacy_storage_5.id) end end @@ -78,17 +80,27 @@ describe Gitlab::BackgroundMigration::BackfillProjectRepositories do expect(project.disk_path).to eq(project_legacy_storage_3.disk_path) end + it 'returns the correct disk_path using the route entry' do + project_legacy_storage_5.route.update(path: 'zoo/new-test') + project = described_class.find(project_legacy_storage_5.id) + + expect(project.disk_path).to eq('zoo/new-test') + end + it 'raises OrphanedNamespaceError when any parent namespace does not exist' do subgroup = create(:group, parent: group) project_orphaned_namespace = create(:project, name: 'baz', path: 'baz', namespace: subgroup, storage_version: nil) subgroup.update_column(:parent_id, Namespace.maximum(:id).succ) project = described_class.find(project_orphaned_namespace.id) + project.route.destroy + subgroup.route.destroy - expect { project.disk_path } + expect { project.reload.disk_path } .to raise_error(Gitlab::BackgroundMigration::BackfillProjectRepositories::OrphanedNamespaceError) end end end end end +# rubocop:enable RSpec/FactoriesInMigrationSpecs diff --git a/spec/lib/gitlab/background_migration/create_gpg_key_subkeys_from_gpg_keys_spec.rb b/spec/lib/gitlab/background_migration/create_gpg_key_subkeys_from_gpg_keys_spec.rb index f92acf61682..f974dc8fda2 100644 --- a/spec/lib/gitlab/background_migration/create_gpg_key_subkeys_from_gpg_keys_spec.rb +++ b/spec/lib/gitlab/background_migration/create_gpg_key_subkeys_from_gpg_keys_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' describe Gitlab::BackgroundMigration::CreateGpgKeySubkeysFromGpgKeys, :migration, schema: 20171005130944 do context 'when GpgKey exists' do - let!(:gpg_key) { create(:gpg_key, key: GpgHelpers::User3.public_key) } + let!(:gpg_key) { create(:gpg_key, key: GpgHelpers::User3.public_key) } # rubocop:disable RSpec/FactoriesInMigrationSpecs before do GpgKeySubkey.destroy_all # rubocop: disable DestroyAll diff --git a/spec/lib/gitlab/background_migration/delete_diff_files_spec.rb b/spec/lib/gitlab/background_migration/delete_diff_files_spec.rb index 1969aed51da..27281333348 100644 --- a/spec/lib/gitlab/background_migration/delete_diff_files_spec.rb +++ b/spec/lib/gitlab/background_migration/delete_diff_files_spec.rb @@ -1,5 +1,6 @@ require 'spec_helper' +# rubocop:disable RSpec/FactoriesInMigrationSpecs describe Gitlab::BackgroundMigration::DeleteDiffFiles, :migration, :sidekiq, schema: 20180619121030 do describe '#perform' do context 'when diff files can be deleted' do @@ -71,3 +72,4 @@ describe Gitlab::BackgroundMigration::DeleteDiffFiles, :migration, :sidekiq, sch end end end +# rubocop:enable RSpec/FactoriesInMigrationSpecs diff --git a/spec/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits_spec.rb b/spec/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits_spec.rb index 5dce3fcbcb6..bc71a90605a 100644 --- a/spec/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits_spec.rb +++ b/spec/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits_spec.rb @@ -1,5 +1,6 @@ require 'spec_helper' +# rubocop:disable RSpec/FactoriesInMigrationSpecs describe Gitlab::BackgroundMigration::DeserializeMergeRequestDiffsAndCommits, :migration, schema: 20171114162227 do include GitHelpers @@ -324,3 +325,4 @@ describe Gitlab::BackgroundMigration::DeserializeMergeRequestDiffsAndCommits, :m end end end +# rubocop:enable RSpec/FactoriesInMigrationSpecs diff --git a/spec/lib/gitlab/background_migration/migrate_events_to_push_event_payloads_spec.rb b/spec/lib/gitlab/background_migration/migrate_events_to_push_event_payloads_spec.rb index 5432d270555..188969951a6 100644 --- a/spec/lib/gitlab/background_migration/migrate_events_to_push_event_payloads_spec.rb +++ b/spec/lib/gitlab/background_migration/migrate_events_to_push_event_payloads_spec.rb @@ -1,5 +1,6 @@ require 'spec_helper' +# rubocop:disable RSpec/FactoriesInMigrationSpecs describe Gitlab::BackgroundMigration::MigrateEventsToPushEventPayloads::Event, :migration, schema: 20170608152748 do describe '#commit_title' do it 'returns nil when there are no commits' do @@ -429,3 +430,4 @@ describe Gitlab::BackgroundMigration::MigrateEventsToPushEventPayloads, :migrati end end end +# rubocop:enable RSpec/FactoriesInMigrationSpecs diff --git a/spec/lib/gitlab/background_migration/migrate_system_uploads_to_new_folder_spec.rb b/spec/lib/gitlab/background_migration/migrate_system_uploads_to_new_folder_spec.rb index 021e1d14b18..ea8bdd48e72 100644 --- a/spec/lib/gitlab/background_migration/migrate_system_uploads_to_new_folder_spec.rb +++ b/spec/lib/gitlab/background_migration/migrate_system_uploads_to_new_folder_spec.rb @@ -1,5 +1,6 @@ require 'spec_helper' +# rubocop:disable RSpec/FactoriesInMigrationSpecs describe Gitlab::BackgroundMigration::MigrateSystemUploadsToNewFolder, :delete do let(:migration) { described_class.new } @@ -17,3 +18,4 @@ describe Gitlab::BackgroundMigration::MigrateSystemUploadsToNewFolder, :delete d end end end +# rubocop:enable RSpec/FactoriesInMigrationSpecs diff --git a/spec/lib/gitlab/background_migration/move_personal_snippet_files_spec.rb b/spec/lib/gitlab/background_migration/move_personal_snippet_files_spec.rb index 2e77e80ee46..593486fc56c 100644 --- a/spec/lib/gitlab/background_migration/move_personal_snippet_files_spec.rb +++ b/spec/lib/gitlab/background_migration/move_personal_snippet_files_spec.rb @@ -1,5 +1,6 @@ require 'spec_helper' +# rubocop:disable RSpec/FactoriesInMigrationSpecs describe Gitlab::BackgroundMigration::MovePersonalSnippetFiles do let(:test_dir) { File.join(Rails.root, 'tmp', 'tests', 'move_snippet_files_test') } let(:old_uploads_dir) { File.join('uploads', 'system', 'personal_snippet') } @@ -70,3 +71,4 @@ describe Gitlab::BackgroundMigration::MovePersonalSnippetFiles do "[an upload](#{path})" end end +# rubocop:enable RSpec/FactoriesInMigrationSpecs diff --git a/spec/lib/gitlab/background_migration/populate_cluster_kubernetes_namespace_table_spec.rb b/spec/lib/gitlab/background_migration/populate_cluster_kubernetes_namespace_table_spec.rb index 4f1b01eed41..812e0cc6947 100644 --- a/spec/lib/gitlab/background_migration/populate_cluster_kubernetes_namespace_table_spec.rb +++ b/spec/lib/gitlab/background_migration/populate_cluster_kubernetes_namespace_table_spec.rb @@ -3,90 +3,87 @@ require 'spec_helper' describe Gitlab::BackgroundMigration::PopulateClusterKubernetesNamespaceTable, :migration, schema: 20181022173835 do + include MigrationHelpers::ClusterHelpers + let(:migration) { described_class.new } - let(:clusters) { create_list(:cluster, 10, :project, :provided_by_gcp) } + let(:clusters_table) { table(:clusters) } + let(:cluster_projects_table) { table(:cluster_projects) } + let(:cluster_kubernetes_namespaces_table) { table(:clusters_kubernetes_namespaces) } + let(:projects_table) { table(:projects) } + let(:namespaces_table) { table(:namespaces) } + let(:provider_gcp_table) { table(:cluster_providers_gcp) } + let(:platform_kubernetes_table) { table(:cluster_platforms_kubernetes) } before do - clusters + create_cluster_project_list(10) end shared_examples 'consistent kubernetes namespace attributes' do it 'should populate namespace and service account information' do - subject + migration.perform clusters_with_namespace.each do |cluster| - project = cluster.project - cluster_project = cluster.cluster_projects.first + cluster_project = cluster_projects_table.find_by(cluster_id: cluster.id) + project = projects_table.find(cluster_project.project_id) + kubernetes_namespace = cluster_kubernetes_namespaces_table.find_by(cluster_id: cluster.id) namespace = "#{project.path}-#{project.id}" - kubernetes_namespace = cluster.reload.kubernetes_namespace expect(kubernetes_namespace).to be_present - expect(kubernetes_namespace.cluster_project).to eq(cluster_project) - expect(kubernetes_namespace.project).to eq(cluster_project.project) - expect(kubernetes_namespace.cluster).to eq(cluster_project.cluster) + expect(kubernetes_namespace.cluster_project_id).to eq(cluster_project.id) + expect(kubernetes_namespace.project_id).to eq(cluster_project.project_id) + expect(kubernetes_namespace.cluster_id).to eq(cluster_project.cluster_id) expect(kubernetes_namespace.namespace).to eq(namespace) expect(kubernetes_namespace.service_account_name).to eq("#{namespace}-service-account") end end end - subject { migration.perform } - context 'when no Clusters::Project has a Clusters::KubernetesNamespace' do - let(:cluster_projects) { Clusters::Project.all } + let(:cluster_projects) { cluster_projects_table.all } it 'should create a Clusters::KubernetesNamespace per Clusters::Project' do expect do - subject - end.to change(Clusters::KubernetesNamespace, :count).by(cluster_projects.count) + migration.perform + end.to change(Clusters::KubernetesNamespace, :count).by(cluster_projects_table.count) end it_behaves_like 'consistent kubernetes namespace attributes' do - let(:clusters_with_namespace) { clusters } + let(:clusters_with_namespace) { clusters_table.all } end end context 'when every Clusters::Project has Clusters::KubernetesNamespace' do before do - clusters.each do |cluster| - create(:cluster_kubernetes_namespace, - cluster_project: cluster.cluster_projects.first, - cluster: cluster, - project: cluster.project) - end + create_kubernetes_namespace(clusters_table.all) end it 'should not create any Clusters::KubernetesNamespace' do expect do - subject + migration.perform end.not_to change(Clusters::KubernetesNamespace, :count) end end context 'when only some Clusters::Project have Clusters::KubernetesNamespace related' do - let(:with_kubernetes_namespace) { clusters.first(6) } - let(:with_no_kubernetes_namespace) { clusters.last(4) } + let(:with_kubernetes_namespace) { clusters_table.first(6) } + let(:with_no_kubernetes_namespace) { clusters_table.last(4) } before do - with_kubernetes_namespace.each do |cluster| - create(:cluster_kubernetes_namespace, - cluster_project: cluster.cluster_projects.first, - cluster: cluster, - project: cluster.project) - end + create_kubernetes_namespace(with_kubernetes_namespace) end it 'creates limited number of Clusters::KubernetesNamespace' do expect do - subject + migration.perform end.to change(Clusters::KubernetesNamespace, :count).by(with_no_kubernetes_namespace.count) end it 'should not modify clusters with Clusters::KubernetesNamespace' do - subject + migration.perform with_kubernetes_namespace.each do |cluster| - expect(cluster.kubernetes_namespaces.count).to eq(1) + kubernetes_namespace = cluster_kubernetes_namespaces_table.where(cluster_id: cluster.id) + expect(kubernetes_namespace.count).to eq(1) end end diff --git a/spec/lib/gitlab/background_migration/populate_external_pipeline_source_spec.rb b/spec/lib/gitlab/background_migration/populate_external_pipeline_source_spec.rb index 6ab126ad39a..3e009fed0f1 100644 --- a/spec/lib/gitlab/background_migration/populate_external_pipeline_source_spec.rb +++ b/spec/lib/gitlab/background_migration/populate_external_pipeline_source_spec.rb @@ -2,6 +2,7 @@ require 'spec_helper' +# rubocop:disable RSpec/FactoriesInMigrationSpecs describe Gitlab::BackgroundMigration::PopulateExternalPipelineSource, :migration, schema: 20180916011959 do let(:migration) { described_class.new } @@ -65,3 +66,4 @@ describe Gitlab::BackgroundMigration::PopulateExternalPipelineSource, :migration it_behaves_like 'no changes' end end +# rubocop:enable RSpec/FactoriesInMigrationSpecs diff --git a/spec/lib/gitlab/background_migration/populate_merge_request_metrics_with_events_data_spec.rb b/spec/lib/gitlab/background_migration/populate_merge_request_metrics_with_events_data_spec.rb index e99257e3481..ff1bd9f7850 100644 --- a/spec/lib/gitlab/background_migration/populate_merge_request_metrics_with_events_data_spec.rb +++ b/spec/lib/gitlab/background_migration/populate_merge_request_metrics_with_events_data_spec.rb @@ -1,5 +1,6 @@ require 'rails_helper' +# rubocop:disable RSpec/FactoriesInMigrationSpecs describe Gitlab::BackgroundMigration::PopulateMergeRequestMetricsWithEventsData, :migration, schema: 20171128214150 do # commits_count attribute is added in a next migration before do @@ -128,3 +129,4 @@ describe Gitlab::BackgroundMigration::PopulateMergeRequestMetricsWithEventsData, end end end +# rubocop:enable RSpec/FactoriesInMigrationSpecs diff --git a/spec/lib/gitlab/ci/config/entry/global_spec.rb b/spec/lib/gitlab/ci/config/entry/global_spec.rb index 941ef33c8a4..7651f594a4c 100644 --- a/spec/lib/gitlab/ci/config/entry/global_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/global_spec.rb @@ -160,8 +160,7 @@ describe Gitlab::Ci::Config::Entry::Global do variables: { 'VAR' => 'value' }, ignore: false, after_script: ['make clean'], - only: { refs: %w[branches tags] }, - except: {} }, + only: { refs: %w[branches tags] } }, spinach: { name: :spinach, before_script: [], script: %w[spinach], @@ -172,8 +171,7 @@ describe Gitlab::Ci::Config::Entry::Global do variables: {}, ignore: false, after_script: ['make clean'], - only: { refs: %w[branches tags] }, - except: {} } + only: { refs: %w[branches tags] } } ) end end diff --git a/spec/lib/gitlab/ci/config/entry/job_spec.rb b/spec/lib/gitlab/ci/config/entry/job_spec.rb index 3d0b98eb238..0560eb42e4d 100644 --- a/spec/lib/gitlab/ci/config/entry/job_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/job_spec.rb @@ -258,8 +258,7 @@ describe Gitlab::Ci::Config::Entry::Job do stage: 'test', ignore: false, after_script: %w[cleanup], - only: { refs: %w[branches tags] }, - except: {}) + only: { refs: %w[branches tags] }) end end end diff --git a/spec/lib/gitlab/ci/config/entry/jobs_spec.rb b/spec/lib/gitlab/ci/config/entry/jobs_spec.rb index d97be76f0e0..271ee30df3c 100644 --- a/spec/lib/gitlab/ci/config/entry/jobs_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/jobs_spec.rb @@ -67,14 +67,12 @@ describe Gitlab::Ci::Config::Entry::Jobs do script: %w[rspec], ignore: false, stage: 'test', - only: { refs: %w[branches tags] }, - except: {} }, + only: { refs: %w[branches tags] } }, spinach: { name: :spinach, script: %w[spinach], ignore: false, stage: 'test', - only: { refs: %w[branches tags] }, - except: {} }) + only: { refs: %w[branches tags] } }) end end diff --git a/spec/lib/gitlab/ci/config/entry/policy_spec.rb b/spec/lib/gitlab/ci/config/entry/policy_spec.rb index 83001b7fdd8..1c987e13a9a 100644 --- a/spec/lib/gitlab/ci/config/entry/policy_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/policy_spec.rb @@ -168,8 +168,33 @@ describe Gitlab::Ci::Config::Entry::Policy do end end + describe '#value' do + context 'when default value has been provided' do + before do + entry.default = { refs: %w[branches tags] } + end + + context 'when user overrides default values' do + let(:config) { { refs: %w[feature], variables: %w[$VARIABLE] } } + + it 'does not include default values' do + expect(entry.value).to eq config + end + end + + context 'when default value has not been defined' do + let(:config) { { variables: %w[$VARIABLE] } } + + it 'includes default values' do + expect(entry.value).to eq(refs: %w[branches tags], + variables: %w[$VARIABLE]) + end + end + end + end + describe '.default' do - it 'does not have a default value' do + it 'does not have default policy' do expect(described_class.default).to be_nil 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 1b014ecfaa4..3459939267a 100644 --- a/spec/lib/gitlab/ci/pipeline/chain/populate_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/chain/populate_spec.rb @@ -79,6 +79,31 @@ describe Gitlab::Ci::Pipeline::Chain::Populate do end end + describe 'pipeline protect' do + subject { step.perform! } + + context 'when ref is protected' do + before do + allow(project).to receive(:protected_for?).with('master').and_return(true) + allow(project).to receive(:protected_for?).with('refs/heads/master').and_return(true) + end + + it 'does not protect the pipeline' do + subject + + expect(pipeline.protected).to eq(true) + end + end + + context 'when ref is not protected' do + it 'does not protect the pipeline' do + subject + + expect(pipeline.protected).to eq(false) + end + end + end + context 'when pipeline has validation errors' do let(:pipeline) do build(:ci_pipeline, project: project, ref: nil) diff --git a/spec/lib/gitlab/config/entry/configurable_spec.rb b/spec/lib/gitlab/config/entry/configurable_spec.rb index 85a7cf1d241..37e38e49c0d 100644 --- a/spec/lib/gitlab/config/entry/configurable_spec.rb +++ b/spec/lib/gitlab/config/entry/configurable_spec.rb @@ -7,6 +7,10 @@ describe Gitlab::Config::Entry::Configurable do end end + before do + allow(entry).to receive(:default) + end + describe 'validations' do context 'when entry is a hash' do let(:instance) { entry.new(key: 'value') } @@ -26,9 +30,11 @@ describe Gitlab::Config::Entry::Configurable do end describe 'configured entries' do + let(:entry_class) { double('entry_class', default: nil) } + before do - entry.class_eval do - entry :object, Object, description: 'test object' + entry.class_exec(entry_class) do |entry_class| + entry :object, entry_class, description: 'test object' end end diff --git a/spec/lib/gitlab/git/bundle_file_spec.rb b/spec/lib/gitlab/git/bundle_file_spec.rb new file mode 100644 index 00000000000..ff7c981dadd --- /dev/null +++ b/spec/lib/gitlab/git/bundle_file_spec.rb @@ -0,0 +1,26 @@ +require 'spec_helper' + +describe Gitlab::Git::BundleFile do + describe '.check!' do + let(:valid_bundle) { Tempfile.new } + let(:valid_bundle_path) { valid_bundle.path } + let(:invalid_bundle_path) { Rails.root.join('spec/fixtures/malicious.bundle') } + + after do + valid_bundle.close! + end + + it 'returns nil for a valid bundle' do + valid_bundle.write("# v2 git bundle\nfoo bar baz\n") + valid_bundle.close + + expect(described_class.check!(valid_bundle_path)).to be_nil + end + + it 'raises an exception for an invalid bundle' do + expect do + described_class.check!(invalid_bundle_path) + end.to raise_error(described_class::InvalidBundleError) + end + end +end diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index 852ee9c96af..a19e3e84f83 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -1753,22 +1753,23 @@ describe Gitlab::Git::Repository, :seed_helper do end describe '#create_from_bundle' do - let(:bundle_path) { File.join(Dir.tmpdir, "repo-#{SecureRandom.hex}.bundle") } + let(:valid_bundle_path) { File.join(Dir.tmpdir, "repo-#{SecureRandom.hex}.bundle") } + let(:malicious_bundle_path) { Rails.root.join('spec/fixtures/malicious.bundle') } let(:project) { create(:project) } let(:imported_repo) { project.repository.raw } before do - expect(repository.bundle_to_disk(bundle_path)).to be_truthy + expect(repository.bundle_to_disk(valid_bundle_path)).to be_truthy end after do - FileUtils.rm_rf(bundle_path) + FileUtils.rm_rf(valid_bundle_path) end it 'creates a repo from a bundle file' do expect(imported_repo).not_to exist - result = imported_repo.create_from_bundle(bundle_path) + result = imported_repo.create_from_bundle(valid_bundle_path) expect(result).to be_truthy expect(imported_repo).to exist @@ -1776,11 +1777,17 @@ describe Gitlab::Git::Repository, :seed_helper do end it 'creates a symlink to the global hooks dir' do - imported_repo.create_from_bundle(bundle_path) + imported_repo.create_from_bundle(valid_bundle_path) hooks_path = Gitlab::GitalyClient::StorageSettings.allow_disk_access { File.join(imported_repo.path, 'hooks') } expect(File.readlink(hooks_path)).to eq(Gitlab.config.gitlab_shell.hooks_path) end + + it 'raises an error if the bundle is an attempted malicious payload' do + expect do + imported_repo.create_from_bundle(malicious_bundle_path) + end.to raise_error(::Gitlab::Git::BundleFile::InvalidBundleError) + end end describe '#checksum' do diff --git a/spec/lib/gitlab/gon_helper_spec.rb b/spec/lib/gitlab/gon_helper_spec.rb index c6f09ca2112..1ff2334bacf 100644 --- a/spec/lib/gitlab/gon_helper_spec.rb +++ b/spec/lib/gitlab/gon_helper_spec.rb @@ -29,4 +29,13 @@ describe Gitlab::GonHelper do helper.push_frontend_feature_flag(:my_feature_flag, 10) end end + + describe '#default_avatar_url' do + it 'returns an absolute URL' do + url = helper.default_avatar_url + + expect(url).to match(/^http/) + expect(url).to match(/no_avatar.*png$/) + end + end end diff --git a/spec/lib/gitlab/middleware/read_only_spec.rb b/spec/lib/gitlab/middleware/read_only_spec.rb index bdb1f34d2f6..24d49a049b6 100644 --- a/spec/lib/gitlab/middleware/read_only_spec.rb +++ b/spec/lib/gitlab/middleware/read_only_spec.rb @@ -101,16 +101,36 @@ describe Gitlab::Middleware::ReadOnly do expect(subject).not_to disallow_request end - it 'expects requests to sidekiq admin to be allowed' do - response = request.post('/admin/sidekiq') + context 'sidekiq admin requests' do + where(:mounted_at) do + [ + '', + '/', + '/gitlab', + '/gitlab/', + '/gitlab/gitlab', + '/gitlab/gitlab/' + ] + end - expect(response).not_to be_redirect - expect(subject).not_to disallow_request + with_them do + before do + stub_config_setting(relative_url_root: mounted_at) + end - response = request.get('/admin/sidekiq') + it 'allows requests' do + path = File.join(mounted_at, 'admin/sidekiq') + response = request.post(path) - expect(response).not_to be_redirect - expect(subject).not_to disallow_request + expect(response).not_to be_redirect + expect(subject).not_to disallow_request + + response = request.get(path) + + expect(response).not_to be_redirect + expect(subject).not_to disallow_request + end + end end where(:description, :path) do diff --git a/spec/lib/gitlab/release_blog_post_spec.rb b/spec/lib/gitlab/release_blog_post_spec.rb new file mode 100644 index 00000000000..2c987df3767 --- /dev/null +++ b/spec/lib/gitlab/release_blog_post_spec.rb @@ -0,0 +1,97 @@ +require 'spec_helper' + +describe Gitlab::ReleaseBlogPost do + describe '.blog_post_url' do + let(:releases_xml) do + <<~EOS + <?xml version='1.0' encoding='utf-8' ?> + <feed xmlns='http://www.w3.org/2005/Atom'> + <entry> + <release>11.2</release> + <id>https://about.gitlab.com/2018/08/22/gitlab-11-2-released/</id> + </entry> + <entry> + <release>11.1</release> + <id>https://about.gitlab.com/2018/07/22/gitlab-11-1-released/</id> + </entry> + <entry> + <release>11.0</release> + <id>https://about.gitlab.com/2018/06/22/gitlab-11-0-released/</id> + </entry> + <entry> + <release>10.8</release> + <id>https://about.gitlab.com/2018/05/22/gitlab-10-8-released/</id> + </entry> + </feed> + EOS + end + + subject { described_class.send(:new).blog_post_url } + + before do + stub_request(:get, 'https://about.gitlab.com/releases.xml') + .to_return(status: 200, headers: { 'content-type' => ['text/xml'] }, body: releases_xml) + end + + context 'matches GitLab version to blog post url' do + it 'returns the correct url for major pre release' do + stub_const('Gitlab::VERSION', '11.0.0-pre') + + expect(subject).to eql('https://about.gitlab.com/2018/05/22/gitlab-10-8-released/') + end + + it 'returns the correct url for major release candidate' do + stub_const('Gitlab::VERSION', '11.0.0-rc3') + + expect(subject).to eql('https://about.gitlab.com/2018/05/22/gitlab-10-8-released/') + end + + it 'returns the correct url for major release' do + stub_const('Gitlab::VERSION', '11.0.0') + + expect(subject).to eql('https://about.gitlab.com/2018/06/22/gitlab-11-0-released/') + end + + it 'returns the correct url for minor pre release' do + stub_const('Gitlab::VERSION', '11.2.0-pre') + + expect(subject).to eql('https://about.gitlab.com/2018/07/22/gitlab-11-1-released/') + end + + it 'returns the correct url for minor release candidate' do + stub_const('Gitlab::VERSION', '11.2.0-rc3') + + expect(subject).to eql('https://about.gitlab.com/2018/07/22/gitlab-11-1-released/') + end + + it 'returns the correct url for minor release' do + stub_const('Gitlab::VERSION', '11.2.0') + + expect(subject).to eql('https://about.gitlab.com/2018/08/22/gitlab-11-2-released/') + end + + it 'returns the correct url for patch pre release' do + stub_const('Gitlab::VERSION', '11.2.1-pre') + expect(subject).to eql('https://about.gitlab.com/2018/08/22/gitlab-11-2-released/') + end + + it 'returns the correct url for patch release candidate' do + stub_const('Gitlab::VERSION', '11.2.1-rc3') + + expect(subject).to eql('https://about.gitlab.com/2018/08/22/gitlab-11-2-released/') + end + + it 'returns the correct url for patch release' do + stub_const('Gitlab::VERSION', '11.2.1') + + expect(subject).to eql('https://about.gitlab.com/2018/08/22/gitlab-11-2-released/') + end + + it 'returns nil when no blog post is matched' do + stub_const('Gitlab::VERSION', '9.0.0') + + expect(subject).to be(nil) + end + end + end +end diff --git a/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb b/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb index f773f370ee2..a9d15f1d522 100644 --- a/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb +++ b/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb @@ -82,15 +82,36 @@ describe Gitlab::SidekiqLogging::StructuredLogger do end.to raise_error(ArgumentError) end end + + context 'when the job args are bigger than the maximum allowed' do + it 'keeps args from the front until they exceed the limit' do + Timecop.freeze(timestamp) do + job['args'] = [ + 1, + 2, + 'a' * (described_class::MAXIMUM_JOB_ARGUMENTS_LENGTH / 2), + 'b' * (described_class::MAXIMUM_JOB_ARGUMENTS_LENGTH / 2), + 3 + ] + + expected_args = job['args'].take(3) + ['...'] + + expect(logger).to receive(:info).with(start_payload.merge('args' => expected_args)).ordered + expect(logger).to receive(:info).with(end_payload.merge('args' => expected_args)).ordered + expect(subject).to receive(:log_job_start).and_call_original + expect(subject).to receive(:log_job_done).and_call_original + + subject.call(job, 'test_queue') { } + end + end + end end context 'with SIDEKIQ_LOG_ARGUMENTS disabled' do - it 'logs start and end of job' do + it 'logs start and end of job without args' do Timecop.freeze(timestamp) do - start_payload.delete('args') - - expect(logger).to receive(:info).with(start_payload).ordered - expect(logger).to receive(:info).with(end_payload).ordered + expect(logger).to receive(:info).with(start_payload.except('args')).ordered + expect(logger).to receive(:info).with(end_payload.except('args')).ordered expect(subject).to receive(:log_job_start).and_call_original expect(subject).to receive(:log_job_done).and_call_original diff --git a/spec/lib/gitlab/tracing/factory_spec.rb b/spec/lib/gitlab/tracing/factory_spec.rb new file mode 100644 index 00000000000..945490f0988 --- /dev/null +++ b/spec/lib/gitlab/tracing/factory_spec.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' + +describe Gitlab::Tracing::Factory do + describe '.create_tracer' do + let(:service_name) { 'rspec' } + + context "when tracing is not configured" do + it 'ignores null connection strings' do + expect(described_class.create_tracer(service_name, nil)).to be_nil + end + + it 'ignores empty connection strings' do + expect(described_class.create_tracer(service_name, '')).to be_nil + end + + it 'ignores unknown implementations' do + expect(described_class.create_tracer(service_name, 'opentracing://invalid_driver')).to be_nil + end + + it 'ignores invalid connection strings' do + expect(described_class.create_tracer(service_name, 'open?tracing')).to be_nil + end + end + + context "when tracing is configured with jaeger" do + let(:mock_tracer) { double('tracer') } + + it 'processes default connections' do + expect(Gitlab::Tracing::JaegerFactory).to receive(:create_tracer).with(service_name, {}).and_return(mock_tracer) + + expect(described_class.create_tracer(service_name, 'opentracing://jaeger')).to be(mock_tracer) + end + + it 'processes connections with parameters' do + expect(Gitlab::Tracing::JaegerFactory).to receive(:create_tracer).with(service_name, { a: '1', b: '2', c: '3' }).and_return(mock_tracer) + + expect(described_class.create_tracer(service_name, 'opentracing://jaeger?a=1&b=2&c=3')).to be(mock_tracer) + end + end + end +end diff --git a/spec/lib/gitlab/tracing/grpc_interceptor_spec.rb b/spec/lib/gitlab/tracing/grpc_interceptor_spec.rb new file mode 100644 index 00000000000..7f5aecb7baa --- /dev/null +++ b/spec/lib/gitlab/tracing/grpc_interceptor_spec.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' + +describe Gitlab::Tracing::GRPCInterceptor do + subject { described_class.instance } + + shared_examples_for "a grpc interceptor method" do + let(:custom_error) { Class.new(StandardError) } + + it 'yields' do + expect { |b| method.call(kwargs, &b) }.to yield_control + end + + it 'propagates exceptions' do + expect { method.call(kwargs) { raise custom_error } }.to raise_error(custom_error) + end + end + + describe '#request_response' do + let(:method) { subject.method(:request_response) } + let(:kwargs) { { request: {}, call: {}, method: 'grc_method', metadata: {} } } + + it_behaves_like 'a grpc interceptor method' + end + + describe '#client_streamer' do + let(:method) { subject.method(:client_streamer) } + let(:kwargs) { { requests: [], call: {}, method: 'grc_method', metadata: {} } } + + it_behaves_like 'a grpc interceptor method' + end + + describe '#server_streamer' do + let(:method) { subject.method(:server_streamer) } + let(:kwargs) { { request: {}, call: {}, method: 'grc_method', metadata: {} } } + + it_behaves_like 'a grpc interceptor method' + end + + describe '#bidi_streamer' do + let(:method) { subject.method(:bidi_streamer) } + let(:kwargs) { { requests: [], call: {}, method: 'grc_method', metadata: {} } } + + it_behaves_like 'a grpc interceptor method' + end +end diff --git a/spec/lib/gitlab/tracing/jaeger_factory_spec.rb b/spec/lib/gitlab/tracing/jaeger_factory_spec.rb new file mode 100644 index 00000000000..3d6a007cfd9 --- /dev/null +++ b/spec/lib/gitlab/tracing/jaeger_factory_spec.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' + +describe Gitlab::Tracing::JaegerFactory do + describe '.create_tracer' do + let(:service_name) { 'rspec' } + + shared_examples_for 'a jaeger tracer' do + it 'responds to active_span methods' do + expect(tracer).to respond_to(:active_span) + end + + it 'yields control' do + expect { |b| tracer.start_active_span('operation_name', &b) }.to yield_control + end + end + + context 'processes default connections' do + it_behaves_like 'a jaeger tracer' do + let(:tracer) { described_class.create_tracer(service_name, {}) } + end + end + + context 'handles debug options' do + it_behaves_like 'a jaeger tracer' do + let(:tracer) { described_class.create_tracer(service_name, { debug: "1" }) } + end + end + + context 'handles const sampler' do + it_behaves_like 'a jaeger tracer' do + let(:tracer) { described_class.create_tracer(service_name, { sampler: "const", sampler_param: "1" }) } + end + end + + context 'handles probabilistic sampler' do + it_behaves_like 'a jaeger tracer' do + let(:tracer) { described_class.create_tracer(service_name, { sampler: "probabilistic", sampler_param: "0.5" }) } + end + end + + context 'handles http_endpoint configurations' do + it_behaves_like 'a jaeger tracer' do + let(:tracer) { described_class.create_tracer(service_name, { http_endpoint: "http://localhost:1234" }) } + end + end + + context 'handles udp_endpoint configurations' do + it_behaves_like 'a jaeger tracer' do + let(:tracer) { described_class.create_tracer(service_name, { udp_endpoint: "localhost:4321" }) } + end + end + + context 'ignores invalid parameters' do + it_behaves_like 'a jaeger tracer' do + let(:tracer) { described_class.create_tracer(service_name, { invalid: "true" }) } + end + end + + context 'accepts the debug parameter when strict_parser is set' do + it_behaves_like 'a jaeger tracer' do + let(:tracer) { described_class.create_tracer(service_name, { debug: "1", strict_parsing: "1" }) } + end + end + + it 'rejects invalid parameters when strict_parser is set' do + expect { described_class.create_tracer(service_name, { invalid: "true", strict_parsing: "1" }) }.to raise_error(StandardError) + end + end +end diff --git a/spec/lib/gitlab/tracing/rack_middleware_spec.rb b/spec/lib/gitlab/tracing/rack_middleware_spec.rb new file mode 100644 index 00000000000..13d4d8a89f7 --- /dev/null +++ b/spec/lib/gitlab/tracing/rack_middleware_spec.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Tracing::RackMiddleware do + using RSpec::Parameterized::TableSyntax + + describe '#call' do + context 'for normal middleware flow' do + let(:fake_app) { -> (env) { fake_app_response } } + subject { described_class.new(fake_app) } + let(:request) { } + + context 'for 200 responses' do + let(:fake_app_response) { [200, { 'Content-Type': 'text/plain' }, ['OK']] } + + it 'delegates correctly' do + expect(subject.call(Rack::MockRequest.env_for("/"))).to eq(fake_app_response) + end + end + + context 'for 500 responses' do + let(:fake_app_response) { [500, { 'Content-Type': 'text/plain' }, ['Error']] } + + it 'delegates correctly' do + expect(subject.call(Rack::MockRequest.env_for("/"))).to eq(fake_app_response) + end + end + end + + context 'when an application is raising an exception' do + let(:custom_error) { Class.new(StandardError) } + let(:fake_app) { ->(env) { raise custom_error } } + + subject { described_class.new(fake_app) } + + it 'delegates propagates exceptions correctly' do + expect { subject.call(Rack::MockRequest.env_for("/")) }.to raise_error(custom_error) + end + end + end + + describe '.build_sanitized_url_from_env' do + def env_for_url(url) + env = Rack::MockRequest.env_for(input_url) + env['action_dispatch.parameter_filter'] = [/token/] + + env + end + + where(:input_url, :output_url) do + '/gitlab-org/gitlab-ce' | 'http://example.org/gitlab-org/gitlab-ce' + '/gitlab-org/gitlab-ce?safe=1' | 'http://example.org/gitlab-org/gitlab-ce?safe=1' + '/gitlab-org/gitlab-ce?private_token=secret' | 'http://example.org/gitlab-org/gitlab-ce?private_token=%5BFILTERED%5D' + '/gitlab-org/gitlab-ce?mixed=1&private_token=secret' | 'http://example.org/gitlab-org/gitlab-ce?mixed=1&private_token=%5BFILTERED%5D' + end + + with_them do + it { expect(described_class.build_sanitized_url_from_env(env_for_url(input_url))).to eq(output_url) } + end + end +end diff --git a/spec/lib/gitlab/tracing/sidekiq/client_middleware_spec.rb b/spec/lib/gitlab/tracing/sidekiq/client_middleware_spec.rb new file mode 100644 index 00000000000..3755860b5ba --- /dev/null +++ b/spec/lib/gitlab/tracing/sidekiq/client_middleware_spec.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' + +describe Gitlab::Tracing::Sidekiq::ClientMiddleware do + describe '#call' do + let(:worker_class) { 'test_worker_class' } + let(:job) do + { + 'class' => "jobclass", + 'queue' => "jobqueue", + 'retry' => 0, + 'args' => %w{1 2 3} + } + end + let(:queue) { 'test_queue' } + let(:redis_pool) { double("redis_pool") } + let(:custom_error) { Class.new(StandardError) } + let(:span) { OpenTracing.start_span('test', ignore_active_scope: true) } + + subject { described_class.new } + + it 'yields' do + expect(subject).to receive(:in_tracing_span).with( + operation_name: "sidekiq:jobclass", + tags: { + "component" => "sidekiq", + "span.kind" => "client", + "sidekiq.queue" => "jobqueue", + "sidekiq.jid" => nil, + "sidekiq.retry" => "0", + "sidekiq.args" => "1, 2, 3" + } + ).and_yield(span) + + expect { |b| subject.call(worker_class, job, queue, redis_pool, &b) }.to yield_control + end + + it 'propagates exceptions' do + expect { subject.call(worker_class, job, queue, redis_pool) { raise custom_error } }.to raise_error(custom_error) + end + end +end diff --git a/spec/lib/gitlab/tracing/sidekiq/server_middleware_spec.rb b/spec/lib/gitlab/tracing/sidekiq/server_middleware_spec.rb new file mode 100644 index 00000000000..c3087de785a --- /dev/null +++ b/spec/lib/gitlab/tracing/sidekiq/server_middleware_spec.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' + +describe Gitlab::Tracing::Sidekiq::ServerMiddleware do + describe '#call' do + let(:worker_class) { 'test_worker_class' } + let(:job) do + { + 'class' => "jobclass", + 'queue' => "jobqueue", + 'retry' => 0, + 'args' => %w{1 2 3} + } + end + let(:queue) { 'test_queue' } + let(:custom_error) { Class.new(StandardError) } + let(:span) { OpenTracing.start_span('test', ignore_active_scope: true) } + subject { described_class.new } + + it 'yields' do + expect(subject).to receive(:in_tracing_span).with( + hash_including( + operation_name: "sidekiq:jobclass", + tags: { + "component" => "sidekiq", + "span.kind" => "server", + "sidekiq.queue" => "jobqueue", + "sidekiq.jid" => nil, + "sidekiq.retry" => "0", + "sidekiq.args" => "1, 2, 3" + } + ) + ).and_yield(span) + + expect { |b| subject.call(worker_class, job, queue, &b) }.to yield_control + end + + it 'propagates exceptions' do + expect { subject.call(worker_class, job, queue) { raise custom_error } }.to raise_error(custom_error) + end + end +end diff --git a/spec/lib/gitlab_spec.rb b/spec/lib/gitlab_spec.rb index d63f448883b..6ac3d115bc6 100644 --- a/spec/lib/gitlab_spec.rb +++ b/spec/lib/gitlab_spec.rb @@ -8,6 +8,7 @@ describe Gitlab do expect(described_class.root).to eq(Pathname.new(File.expand_path('../..', __dir__))) end end + describe '.revision' do let(:cmd) { %W[#{described_class.config.git.bin_path} log --pretty=format:%h -n 1] } @@ -69,6 +70,82 @@ describe Gitlab do end end + describe '.final_release?' do + subject { described_class.final_release? } + + context 'returns the corrent boolean value' do + it 'is false for a pre release' do + stub_const('Gitlab::VERSION', '11.0.0-pre') + + expect(subject).to be false + end + + it 'is false for a release candidate' do + stub_const('Gitlab::VERSION', '11.0.0-rc2') + + expect(subject).to be false + end + + it 'is true for a final release' do + stub_const('Gitlab::VERSION', '11.0.2') + + expect(subject).to be true + end + end + end + + describe '.minor_release' do + subject { described_class.minor_release } + + it 'returns the minor release of the full GitLab version' do + stub_const('Gitlab::VERSION', '11.0.1-rc3') + + expect(subject).to eql '11.0' + end + end + + describe '.previous_release' do + subject { described_class.previous_release } + + context 'it should return the previous release' do + it 'returns the previous major version when GitLab major version is not final' do + stub_const('Gitlab::VERSION', '11.0.1-pre') + + expect(subject).to eql '10' + end + + it 'returns the current minor version when the GitLab patch version is RC and > 0' do + stub_const('Gitlab::VERSION', '11.2.1-rc3') + + expect(subject).to eql '11.2' + end + + it 'returns the previous minor version when the GitLab patch version is RC and 0' do + stub_const('Gitlab::VERSION', '11.2.0-rc3') + + expect(subject).to eql '11.1' + end + end + end + + describe '.new_major_release?' do + subject { described_class.new_major_release? } + + context 'returns the corrent boolean value' do + it 'is true when the minor version is 0 and the patch is a pre release' do + stub_const('Gitlab::VERSION', '11.0.1-pre') + + expect(subject).to be true + end + + it 'is false when the minor version is above 0' do + stub_const('Gitlab::VERSION', '11.2.1-rc3') + + expect(subject).to be false + end + end + end + describe '.com?' do it 'is true when on GitLab.com' do stub_config_setting(url: 'https://gitlab.com') diff --git a/spec/migrations/README.md b/spec/migrations/README.md index 49760fa62b8..5df44dbc355 100644 --- a/spec/migrations/README.md +++ b/spec/migrations/README.md @@ -22,39 +22,33 @@ migrate the database **down** to the previous migration version. With this approach you can test a migration against a database schema that this migration has been written for. -Use `migrate!` helper to run the migration that is under test. - The `after` hook will migrate the database **up** and reinstitutes the latest schema version, so that the process does not affect subsequent specs and ensures proper isolation. -## Testing a class that is not an ActiveRecord::Migration - -In order to test a class that is not a migration itself, you will need to -manually provide a required schema version. Please add a `schema` tag to a -context that you want to switch the database schema within. - -Example: `describe SomeClass, :migration, schema: 20170608152748`. - ## Available helpers Use `table` helper to create a temporary `ActiveRecord::Base` derived model for a table. -Use `migrate!` helper to run the migration that is under test. It will not only +See `spec/support/helpers/migrations_helpers.rb` for all the available helpers. + +## Testing a class that is an ActiveRecord::Migration + +In order to test a class that is an `ActiveRecord::Migration`, you will need to +manually `require` the migration file because it is not autoloaded with Rails. + +Use `migrate!` helper to run the migration that is under test. It will not only run migration, but will also bump the schema version in the `schema_migrations` table. It is necessary because in the `after` hook we trigger the rest of the migrations, and we need to know where to start. -See `spec/support/migrations_helpers.rb` for all the available helpers. +### Example -## An example +This spec tests the [`db/post_migrate/20170526185842_migrate_pipeline_stages.rb`](https://gitlab.com/gitlab-org/gitlab-ce/blob/v11.6.5/db/post_migrate/20170526185842_migrate_pipeline_stages.rb) migration. You can find the complete spec on [`spec/migrations/migrate_pipeline_stages_spec.rb`](https://gitlab.com/gitlab-org/gitlab-ce/blob/v11.6.5/spec/migrations/migrate_pipeline_stages_spec.rb). ```ruby require 'spec_helper' - -# Load a migration class. - require Rails.root.join('db', 'post_migrate', '20170526185842_migrate_pipeline_stages.rb') describe MigratePipelineStages, :migration do @@ -86,6 +80,56 @@ describe MigratePipelineStages, :migration do end ``` +## Testing a class that is not an ActiveRecord::Migration + +To test a class that is not an `ActiveRecord::Migration` (a background migration), +you will need to manually provide a required schema version. Please add a +schema tag to a context that you want to switch the database schema within. + +Example: `describe SomeClass, :migration, schema: 20170608152748`. + +### Example + +This spec tests the [`lib/gitlab/background_migration/archive_legacy_traces.rb`](https://gitlab.com/gitlab-org/gitlab-ce/blob/v11.6.5/lib/gitlab/background_migration/archive_legacy_traces.rb) +background migration. You can find the complete spec on +[`spec/lib/gitlab/background_migration/archive_legacy_traces_spec.rb`](https://gitlab.com/gitlab-org/gitlab-ce/blob/v11.6.5/spec/lib/gitlab/background_migration/archive_legacy_traces_spec.rb) + +```ruby +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 +end +``` + ## Best practices 1. Note that this type of tests do not run within the transaction, we use diff --git a/spec/migrations/add_foreign_keys_to_todos_spec.rb b/spec/migrations/add_foreign_keys_to_todos_spec.rb index bf2fa5c0f56..efd87173b9c 100644 --- a/spec/migrations/add_foreign_keys_to_todos_spec.rb +++ b/spec/migrations/add_foreign_keys_to_todos_spec.rb @@ -3,9 +3,11 @@ require Rails.root.join('db', 'migrate', '20180201110056_add_foreign_keys_to_tod describe AddForeignKeysToTodos, :migration do let(:todos) { table(:todos) } + let(:users) { table(:users) } + let(:projects) { table(:projects) } - let(:project) { create(:project) } # rubocop:disable RSpec/FactoriesInMigrationSpecs - let(:user) { create(:user) } # rubocop:disable RSpec/FactoriesInMigrationSpecs + let(:project) { projects.create!(name: 'gitlab', path: 'gitlab-org/gitlab-ce', namespace_id: 1) } + let(:user) { users.create!(email: 'email@email.com', name: 'foo', username: 'foo', projects_limit: 0) } context 'add foreign key on user_id' do let!(:todo_with_user) { create_todo(user_id: user.id) } diff --git a/spec/migrations/cleanup_legacy_artifact_migration_spec.rb b/spec/migrations/cleanup_legacy_artifact_migration_spec.rb new file mode 100644 index 00000000000..dc269d32e5a --- /dev/null +++ b/spec/migrations/cleanup_legacy_artifact_migration_spec.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +require 'spec_helper' +require Rails.root.join('db', 'migrate', '20190104182041_cleanup_legacy_artifact_migration.rb') + +describe CleanupLegacyArtifactMigration, :migration, :sidekiq, :redis do + let(:migration) { spy('migration') } + + context 'when still legacy artifacts exist' do + let(:namespaces) { table(:namespaces) } + let(:projects) { table(:projects) } + let(:pipelines) { table(:ci_pipelines) } + let(:jobs) { table(:ci_builds) } + let(:job_artifacts) { table(:ci_job_artifacts) } + let(:namespace) { namespaces.create!(name: 'gitlab', path: 'gitlab-org') } + let(:project) { projects.create!(name: 'gitlab', path: 'gitlab-ce', namespace_id: namespace.id) } + let(:pipeline) { pipelines.create!(project_id: project.id, ref: 'master', sha: 'adf43c3a') } + let(:archive_file_type) { Gitlab::BackgroundMigration::MigrateLegacyArtifacts::ARCHIVE_FILE_TYPE } + let(:metadata_file_type) { Gitlab::BackgroundMigration::MigrateLegacyArtifacts::METADATA_FILE_TYPE } + let(:local_store) { ::ObjectStorage::Store::LOCAL } + let(:remote_store) { ::ObjectStorage::Store::REMOTE } + let(:legacy_location) { Gitlab::BackgroundMigration::MigrateLegacyArtifacts::LEGACY_PATH_FILE_LOCATION } + + before do + jobs.create!(id: 1, commit_id: pipeline.id, project_id: project.id, status: :success, artifacts_file: 'archive.zip') + jobs.create!(id: 2, commit_id: pipeline.id, project_id: project.id, status: :failed, artifacts_metadata: 'metadata.gz') + jobs.create!(id: 3, commit_id: pipeline.id, project_id: project.id, status: :failed, artifacts_file: 'archive.zip', artifacts_metadata: 'metadata.gz') + jobs.create!(id: 4, commit_id: pipeline.id, project_id: project.id, status: :running) + jobs.create!(id: 5, commit_id: pipeline.id, project_id: project.id, status: :success, artifacts_file: 'archive.zip', artifacts_file_store: remote_store, artifacts_metadata: 'metadata.gz') + jobs.create!(id: 6, commit_id: pipeline.id, project_id: project.id, status: :failed, artifacts_file: 'archive.zip', artifacts_metadata: 'metadata.gz') + end + + it 'steals sidekiq jobs from MigrateLegacyArtifacts background migration' do + expect(Gitlab::BackgroundMigration).to receive(:steal).with('MigrateLegacyArtifacts') + + migrate! + end + + it 'migrates legacy artifacts to ci_job_artifacts table' do + migrate! + + expect(job_artifacts.order(:job_id, :file_type).pluck('project_id, job_id, file_type, file_store, size, expire_at, file, file_sha256, file_location')) + .to eq([[project.id, 1, archive_file_type, local_store, nil, nil, 'archive.zip', nil, legacy_location], + [project.id, 3, archive_file_type, local_store, nil, nil, 'archive.zip', nil, legacy_location], + [project.id, 3, metadata_file_type, local_store, nil, nil, 'metadata.gz', nil, legacy_location], + [project.id, 5, archive_file_type, remote_store, nil, nil, 'archive.zip', nil, legacy_location], + [project.id, 5, metadata_file_type, local_store, nil, nil, 'metadata.gz', nil, legacy_location], + [project.id, 6, archive_file_type, local_store, nil, nil, 'archive.zip', nil, legacy_location], + [project.id, 6, metadata_file_type, local_store, nil, nil, 'metadata.gz', nil, legacy_location]]) + end + end +end diff --git a/spec/migrations/cleanup_namespaceless_pending_delete_projects_spec.rb b/spec/migrations/cleanup_namespaceless_pending_delete_projects_spec.rb index b5980cb9ddb..651341906c2 100644 --- a/spec/migrations/cleanup_namespaceless_pending_delete_projects_spec.rb +++ b/spec/migrations/cleanup_namespaceless_pending_delete_projects_spec.rb @@ -2,6 +2,8 @@ require 'spec_helper' require Rails.root.join('db', 'post_migrate', '20170502101023_cleanup_namespaceless_pending_delete_projects.rb') describe CleanupNamespacelessPendingDeleteProjects, :migration, schema: 20180222043024 do + let(:projects) { table(:projects) } + before do # Stub after_save callbacks that will fail when Project has no namespace allow_any_instance_of(Project).to receive(:ensure_storage_path_exists).and_return(nil) @@ -10,9 +12,9 @@ describe CleanupNamespacelessPendingDeleteProjects, :migration, schema: 20180222 describe '#up' do it 'only cleans up pending delete projects' do - create(:project) # rubocop:disable RSpec/FactoriesInMigrationSpecs - create(:project, pending_delete: true) # rubocop:disable RSpec/FactoriesInMigrationSpecs - project = build(:project, pending_delete: true, namespace_id: nil) # rubocop:disable RSpec/FactoriesInMigrationSpecs + projects.create!(name: 'gitlab', path: 'gitlab-org/gitlab-ce', namespace_id: 1) + projects.create!(name: 'gitlab', path: 'gitlab-org/gitlab-ee', namespace_id: 2, pending_delete: true) + project = Project.new(pending_delete: true, namespace_id: nil) project.save(validate: false) expect(NamespacelessProjectDestroyWorker).to receive(:bulk_perform_async).with([[project.id]]) @@ -21,8 +23,8 @@ describe CleanupNamespacelessPendingDeleteProjects, :migration, schema: 20180222 end it 'does nothing when no pending delete projects without namespace found' do - create(:project) # rubocop:disable RSpec/FactoriesInMigrationSpecs - create(:project, pending_delete: true) # rubocop:disable RSpec/FactoriesInMigrationSpecs + projects.create!(name: 'gitlab', path: 'gitlab-org/gitlab-ce', namespace_id: 1) + projects.create!(name: 'gitlab', path: 'gitlab-org/gitlab-ee', namespace_id: 2, pending_delete: true) expect(NamespacelessProjectDestroyWorker).not_to receive(:bulk_perform_async) diff --git a/spec/migrations/rename_more_reserved_project_names_spec.rb b/spec/migrations/rename_more_reserved_project_names_spec.rb index baf16c2ce53..80ae209e9d1 100644 --- a/spec/migrations/rename_more_reserved_project_names_spec.rb +++ b/spec/migrations/rename_more_reserved_project_names_spec.rb @@ -37,9 +37,8 @@ describe RenameMoreReservedProjectNames, :delete do .to receive(:execute) .and_raise(Projects::AfterRenameService::RenameFailedError) - allow(Projects::AfterRenameService) - .to receive(:new) - .with(project) + expect(migration) + .to receive(:after_rename_service) .and_return(service) end diff --git a/spec/migrations/rename_reserved_project_names_spec.rb b/spec/migrations/rename_reserved_project_names_spec.rb index 7818aa0d560..93e5c032287 100644 --- a/spec/migrations/rename_reserved_project_names_spec.rb +++ b/spec/migrations/rename_reserved_project_names_spec.rb @@ -41,9 +41,8 @@ describe RenameReservedProjectNames, :migration, schema: :latest do .to receive(:execute) .and_raise(Projects::AfterRenameService::RenameFailedError) - allow(Projects::AfterRenameService) - .to receive(:new) - .with(project) + expect(migration) + .to receive(:after_rename_service) .and_return(service) end diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index 1afc2436bb5..60d89313f07 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -3028,6 +3028,24 @@ describe Ci::Build do subject.drop! end end + + context 'when associated deployment failed to update its status' do + let(:build) { create(:ci_build, :running, pipeline: pipeline) } + let!(:deployment) { create(:deployment, deployable: build) } + + before do + allow_any_instance_of(Deployment) + .to receive(:drop!).and_raise('Unexpected error') + end + + it 'can drop the build' do + expect(Gitlab::Sentry).to receive(:track_exception) + + expect { build.drop! }.not_to raise_error + + expect(build).to be_failed + end + end end describe '.matches_tag_ids' do diff --git a/spec/models/clusters/applications/ingress_spec.rb b/spec/models/clusters/applications/ingress_spec.rb index de313a8ca36..52c347229c6 100644 --- a/spec/models/clusters/applications/ingress_spec.rb +++ b/spec/models/clusters/applications/ingress_spec.rb @@ -35,7 +35,7 @@ describe Clusters::Applications::Ingress do let(:application) { create(:clusters_applications_ingress, :scheduled, version: '0.22.0') } it 'updates the application version' do - expect(application.reload.version).to eq('0.23.0') + expect(application.reload.version).to eq('1.1.2') end end end @@ -90,7 +90,7 @@ describe Clusters::Applications::Ingress do it 'should be initialized with ingress arguments' do expect(subject.name).to eq('ingress') expect(subject.chart).to eq('stable/nginx-ingress') - expect(subject.version).to eq('0.23.0') + expect(subject.version).to eq('1.1.2') expect(subject).to be_rbac expect(subject.files).to eq(ingress.files) end @@ -107,7 +107,7 @@ describe Clusters::Applications::Ingress do let(:ingress) { create(:clusters_applications_ingress, :errored, version: 'nginx') } it 'should be initialized with the locked version' do - expect(subject.version).to eq('0.23.0') + expect(subject.version).to eq('1.1.2') end end end diff --git a/spec/models/clusters/applications/prometheus_spec.rb b/spec/models/clusters/applications/prometheus_spec.rb index de6b844023a..e50ba67c493 100644 --- a/spec/models/clusters/applications/prometheus_spec.rb +++ b/spec/models/clusters/applications/prometheus_spec.rb @@ -90,6 +90,24 @@ describe Clusters::Applications::Prometheus do expect(application).not_to be_ready end + + it 'returns true when updating' do + application = build(:clusters_applications_prometheus, :updating, cluster: cluster) + + expect(application).to be_ready + end + + it 'returns true when updated' do + application = build(:clusters_applications_prometheus, :updated, cluster: cluster) + + expect(application).to be_ready + end + + it 'returns true when errored' do + application = build(:clusters_applications_prometheus, :update_errored, cluster: cluster) + + expect(application).to be_ready + end end describe '#prometheus_client' do @@ -197,6 +215,46 @@ describe Clusters::Applications::Prometheus do end end + describe '#upgrade_command' do + let(:prometheus) { build(:clusters_applications_prometheus) } + let(:values) { prometheus.values } + + it 'returns an instance of Gitlab::Kubernetes::Helm::GetCommand' do + expect(prometheus.upgrade_command(values)).to be_an_instance_of(::Gitlab::Kubernetes::Helm::UpgradeCommand) + end + + it 'should be initialized with 3 arguments' do + command = prometheus.upgrade_command(values) + + expect(command.name).to eq('prometheus') + expect(command.chart).to eq('stable/prometheus') + expect(command.version).to eq('6.7.3') + expect(command.files).to eq(prometheus.files) + end + end + + describe '#update_in_progress?' do + context 'when app is updating' do + it 'returns true' do + cluster = create(:cluster) + prometheus_app = build(:clusters_applications_prometheus, :updating, cluster: cluster) + + expect(prometheus_app.update_in_progress?).to be true + end + end + end + + describe '#update_errored?' do + context 'when app errored' do + it 'returns true' do + cluster = create(:cluster) + prometheus_app = build(:clusters_applications_prometheus, :update_errored, cluster: cluster) + + expect(prometheus_app.update_errored?).to be true + end + end + end + describe '#files' do let(:application) { create(:clusters_applications_prometheus) } let(:values) { subject[:'values.yaml'] } @@ -211,4 +269,43 @@ describe Clusters::Applications::Prometheus do expect(values).to include('serverFiles') end end + + describe '#files_with_replaced_values' do + let(:application) { build(:clusters_applications_prometheus) } + let(:files) { application.files } + + subject { application.files_with_replaced_values({ hello: :world }) } + + it 'does not modify #files' do + expect(subject[:'values.yaml']).not_to eq(files) + expect(files[:'values.yaml']).to eq(application.values) + end + + it 'returns values.yaml with replaced values' do + expect(subject[:'values.yaml']).to eq({ hello: :world }) + end + + it 'should include cert files' do + expect(subject[:'ca.pem']).to be_present + expect(subject[:'ca.pem']).to eq(application.cluster.application_helm.ca_cert) + + expect(subject[:'cert.pem']).to be_present + expect(subject[:'key.pem']).to be_present + + cert = OpenSSL::X509::Certificate.new(subject[:'cert.pem']) + expect(cert.not_after).to be < 60.minutes.from_now + end + + context 'when the helm application does not have a ca_cert' do + before do + application.cluster.application_helm.ca_cert = nil + end + + it 'should not include cert files' do + expect(subject[:'ca.pem']).not_to be_present + expect(subject[:'cert.pem']).not_to be_present + expect(subject[:'key.pem']).not_to be_present + end + end + end end diff --git a/spec/models/clusters/applications/runner_spec.rb b/spec/models/clusters/applications/runner_spec.rb index 3d0735c6d0b..8ad41e997c2 100644 --- a/spec/models/clusters/applications/runner_spec.rb +++ b/spec/models/clusters/applications/runner_spec.rb @@ -18,7 +18,7 @@ describe Clusters::Applications::Runner do let(:application) { create(:clusters_applications_runner, :scheduled, version: '0.1.30') } it 'updates the application version' do - expect(application.reload.version).to eq('0.1.43') + expect(application.reload.version).to eq('0.1.45') end end end @@ -46,7 +46,7 @@ describe Clusters::Applications::Runner do it 'should be initialized with 4 arguments' do expect(subject.name).to eq('runner') expect(subject.chart).to eq('runner/gitlab-runner') - expect(subject.version).to eq('0.1.43') + expect(subject.version).to eq('0.1.45') expect(subject).to be_rbac expect(subject.repository).to eq('https://charts.gitlab.io') expect(subject.files).to eq(gitlab_runner.files) @@ -64,7 +64,7 @@ describe Clusters::Applications::Runner do let(:gitlab_runner) { create(:clusters_applications_runner, :errored, runner: ci_runner, version: '0.1.13') } it 'should be initialized with the locked version' do - expect(subject.version).to eq('0.1.43') + expect(subject.version).to eq('0.1.45') end end end diff --git a/spec/models/clusters/cluster_spec.rb b/spec/models/clusters/cluster_spec.rb index f447e64b029..0161db740ee 100644 --- a/spec/models/clusters/cluster_spec.rb +++ b/spec/models/clusters/cluster_spec.rb @@ -113,7 +113,7 @@ describe Clusters::Cluster do end end - describe 'validation' do + describe 'validations' do subject { cluster.valid? } context 'when validates name' do @@ -252,6 +252,31 @@ describe Clusters::Cluster do end end end + + describe 'domain validation' do + let(:cluster) { build(:cluster) } + + subject { cluster } + + context 'when cluster has domain' do + let(:cluster) { build(:cluster, :with_domain) } + + it { is_expected.to be_valid } + end + + context 'when cluster has an invalid domain' do + let(:cluster) { build(:cluster, domain: 'not-valid-domain') } + + it 'should add an error on domain' do + expect(subject).not_to be_valid + expect(subject.errors[:domain].first).to eq('is not a fully qualified domain name') + end + end + + context 'when cluster does not have a domain' do + it { is_expected.to be_valid } + end + end end describe '.ancestor_clusters_for_clusterable' do diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb index e63881242f6..9dc32a815d8 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -722,6 +722,42 @@ describe Group do end end + describe '#highest_group_member', :nested_groups do + let(:nested_group) { create(:group, parent: group) } + let(:nested_group_2) { create(:group, parent: nested_group) } + let(:user) { create(:user) } + + subject(:highest_group_member) { nested_group_2.highest_group_member(user) } + + context 'when the user is not a member of any group in the hierarchy' do + it 'returns nil' do + expect(highest_group_member).to be_nil + end + end + + context 'when the user is only a member of one group in the hierarchy' do + before do + nested_group.add_developer(user) + end + + it 'returns that group member' do + expect(highest_group_member.access_level).to eq(Gitlab::Access::DEVELOPER) + end + end + + context 'when the user is a member of several groups in the hierarchy' do + before do + group.add_owner(user) + nested_group.add_developer(user) + nested_group_2.add_maintainer(user) + end + + it 'returns the group member with the highest access level' do + expect(highest_group_member.access_level).to eq(Gitlab::Access::OWNER) + end + end + end + describe '#has_parent?' do context 'when the group has a parent' do it 'should be truthy' do diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb index 6f900a60213..5d18e085a6f 100644 --- a/spec/models/issue_spec.rb +++ b/spec/models/issue_spec.rb @@ -721,39 +721,28 @@ describe Issue do end end - describe '#check_for_spam' do - let(:project) { create :project, visibility_level: visibility_level } - let(:issue) { create :issue, project: project } + describe '#check_for_spam?' do + using RSpec::Parameterized::TableSyntax - subject do - issue.assign_attributes(description: description) - issue.check_for_spam? + where(:visibility_level, :confidential, :new_attributes, :check_for_spam?) do + Gitlab::VisibilityLevel::PUBLIC | false | { description: 'woo' } | true + Gitlab::VisibilityLevel::PUBLIC | false | { title: 'woo' } | true + Gitlab::VisibilityLevel::PUBLIC | true | { confidential: false } | true + Gitlab::VisibilityLevel::PUBLIC | true | { description: 'woo' } | false + Gitlab::VisibilityLevel::PUBLIC | false | { title: 'woo', confidential: true } | false + Gitlab::VisibilityLevel::PUBLIC | false | { description: 'original description' } | false + Gitlab::VisibilityLevel::INTERNAL | false | { description: 'woo' } | false + Gitlab::VisibilityLevel::PRIVATE | false | { description: 'woo' } | false end - context 'when project is public and spammable attributes changed' do - let(:visibility_level) { Gitlab::VisibilityLevel::PUBLIC } - let(:description) { 'woo' } + with_them do + it 'checks for spam on issues that can be seen anonymously' do + project = create(:project, visibility_level: visibility_level) + issue = create(:issue, project: project, confidential: confidential, description: 'original description') - it 'returns true' do - is_expected.to be_truthy - end - end - - context 'when project is private' do - let(:visibility_level) { Gitlab::VisibilityLevel::PRIVATE } - let(:description) { issue.description } - - it 'returns false' do - is_expected.to be_falsey - end - end - - context 'when spammable attributes have not changed' do - let(:visibility_level) { Gitlab::VisibilityLevel::PUBLIC } - let(:description) { issue.description } + issue.assign_attributes(new_attributes) - it 'returns false' do - is_expected.to be_falsey + expect(issue.check_for_spam?).to eq(check_for_spam?) end end end diff --git a/spec/models/milestone_spec.rb b/spec/models/milestone_spec.rb index 015db4d4e96..2e436f2cc8a 100644 --- a/spec/models/milestone_spec.rb +++ b/spec/models/milestone_spec.rb @@ -286,8 +286,8 @@ describe Milestone do end context 'relations as params' do - let(:projects) { Project.where(id: project.id) } - let(:groups) { Group.where(id: group.id) } + let(:projects) { Project.where(id: project.id).select(:id) } + let(:groups) { Group.where(id: group.id).select(:id) } it_behaves_like 'filters by projects and groups' end diff --git a/spec/models/project_import_data_spec.rb b/spec/models/project_import_data_spec.rb index e9910c0a5d1..fe47811f074 100644 --- a/spec/models/project_import_data_spec.rb +++ b/spec/models/project_import_data_spec.rb @@ -39,4 +39,15 @@ describe ProjectImportData do expect(row.credentials).to eq({ 'number' => 10, 'foo' => 'bar' }) end end + + describe '#clear_credentials' do + it 'clears out the Hash' do + row = described_class.new + + row.merge_credentials('number' => 10) + row.clear_credentials + + expect(row.credentials).to eq({}) + end + end end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 397b4d7c61f..7a8dc59039e 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -209,9 +209,14 @@ describe Project do it 'does not allow new projects beyond user limits' do project2 = build(:project) - allow(project2).to receive(:creator).and_return(double(can_create_project?: false, projects_limit: 0).as_null_object) + + allow(project2) + .to receive(:creator) + .and_return( + double(can_create_project?: false, projects_limit: 0).as_null_object + ) + expect(project2).not_to be_valid - expect(project2.errors[:limit_reached].first).to match(/Personal project creation is not allowed/) end describe 'wiki path conflict' do @@ -3087,7 +3092,7 @@ describe Project do context 'when the project is in a subgroup' do let(:group) { create(:group, :nested) } - it { is_expected.to be(false) } + it { is_expected.to be(true) } end end @@ -4431,6 +4436,75 @@ describe Project do end end + describe '#check_personal_projects_limit' do + context 'when creating a project for a group' do + it 'does nothing' do + creator = build(:user) + project = build(:project, namespace: build(:group), creator: creator) + + allow(creator) + .to receive(:can_create_project?) + .and_return(false) + + project.check_personal_projects_limit + + expect(project.errors).to be_empty + end + end + + context 'when the user is not allowed to create a personal project' do + let(:user) { build(:user) } + let(:project) { build(:project, creator: user) } + + before do + allow(user) + .to receive(:can_create_project?) + .and_return(false) + end + + context 'when the project limit is zero' do + it 'adds a validation error' do + allow(user) + .to receive(:projects_limit) + .and_return(0) + + project.check_personal_projects_limit + + expect(project.errors[:limit_reached].first) + .to match(/Personal project creation is not allowed/) + end + end + + context 'when the project limit is greater than zero' do + it 'adds a validation error' do + allow(user) + .to receive(:projects_limit) + .and_return(5) + + project.check_personal_projects_limit + + expect(project.errors[:limit_reached].first) + .to match(/Your project limit is 5 projects/) + end + end + end + + context 'when the user is allowed to create personal projects' do + it 'does nothing' do + user = build(:user) + project = build(:project, creator: user) + + allow(user) + .to receive(:can_create_project?) + .and_return(true) + + project.check_personal_projects_limit + + expect(project.errors).to be_empty + end + end + end + def rugged_config rugged_repo(project.repository).config end diff --git a/spec/models/remote_mirror_spec.rb b/spec/models/remote_mirror_spec.rb index 224bc9ed935..c06e9a08ab4 100644 --- a/spec/models/remote_mirror_spec.rb +++ b/spec/models/remote_mirror_spec.rb @@ -303,6 +303,25 @@ describe RemoteMirror, :mailer do end end + context '#url=' do + let(:remote_mirror) { create(:project, :repository, :remote_mirror).remote_mirrors.first } + + it 'resets all the columns when URL changes' do + remote_mirror.update(last_error: Time.now, + last_update_at: Time.now, + last_successful_update_at: Time.now, + update_status: 'started', + error_notification_sent: true) + + expect { remote_mirror.update_attribute(:url, 'http://new.example.com') } + .to change { remote_mirror.last_error }.to(nil) + .and change { remote_mirror.last_update_at }.to(nil) + .and change { remote_mirror.last_successful_update_at }.to(nil) + .and change { remote_mirror.update_status }.to('finished') + .and change { remote_mirror.error_notification_sent }.to(false) + end + end + context '#updated_since?' do let(:remote_mirror) { create(:project, :repository, :remote_mirror).remote_mirrors.first } let(:timestamp) { Time.now - 5.minutes } diff --git a/spec/requests/api/features_spec.rb b/spec/requests/api/features_spec.rb index 7d3eff7d32d..22a9e36ca31 100644 --- a/spec/requests/api/features_spec.rb +++ b/spec/requests/api/features_spec.rb @@ -129,6 +129,40 @@ describe API::Features do end end + context 'when enabling for a project by path' do + context 'when the project exists' do + let!(:project) { create(:project) } + + it 'sets the feature gate' do + post api("/features/#{feature_name}", admin), params: { value: 'true', project: project.full_path } + + expect(response).to have_gitlab_http_status(201) + expect(json_response).to eq( + 'name' => 'my_feature', + 'state' => 'conditional', + 'gates' => [ + { 'key' => 'boolean', 'value' => false }, + { 'key' => 'actors', 'value' => ["Project:#{project.id}"] } + ]) + end + end + + context 'when the project does not exist' do + it 'sets no new values' do + post api("/features/#{feature_name}", admin), params: { value: 'true', project: 'mep/to/the/mep/mep' } + + expect(response).to have_gitlab_http_status(201) + expect(json_response).to eq( + "name" => "my_feature", + "state" => "off", + "gates" => [ + { "key" => "boolean", "value" => false } + ] + ) + end + end + end + it 'creates a feature with the given percentage if passed an integer' do post api("/features/#{feature_name}", admin), params: { value: '50' } diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb index c9dfc5c4a7e..7176bc23e34 100644 --- a/spec/requests/api/groups_spec.rb +++ b/spec/requests/api/groups_spec.rb @@ -382,6 +382,20 @@ describe API::Groups do expect(response_project_ids(json_response, 'shared_projects')) .to contain_exactly(projects[:public].id, projects[:internal].id) end + + it 'avoids N+1 queries' do + get api("/groups/#{group1.id}", admin) + + control_count = ActiveRecord::QueryRecorder.new do + get api("/groups/#{group1.id}", admin) + end.count + + create(:project, namespace: group1) + + expect do + get api("/groups/#{group1.id}", admin) + end.not_to exceed_query_limit(control_count) + end end context "when authenticated as admin" do diff --git a/spec/requests/api/import_github_spec.rb b/spec/requests/api/import_github_spec.rb new file mode 100644 index 00000000000..aceff9b4aa6 --- /dev/null +++ b/spec/requests/api/import_github_spec.rb @@ -0,0 +1,56 @@ +require 'spec_helper' + +describe API::ImportGithub do + include ApiHelpers + + let(:token) { "asdasd12345" } + let(:provider) { :github } + let(:access_params) { { github_access_token: token } } + + describe "POST /import/github" do + let(:user) { create(:user) } + let(:project) { create(:project) } + let(:provider_username) { user.username } + let(:provider_user) { OpenStruct.new(login: provider_username) } + let(:provider_repo) do + OpenStruct.new( + name: 'vim', + full_name: "#{provider_username}/vim", + owner: OpenStruct.new(login: provider_username) + ) + end + + before do + Grape::Endpoint.before_each do |endpoint| + allow(endpoint).to receive(:client).and_return(double('client', user: provider_user, repo: provider_repo).as_null_object) + end + end + + it 'returns 201 response when the project is imported successfully' do + allow(Gitlab::LegacyGithubImport::ProjectCreator) + .to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, access_params, type: provider) + .and_return(double(execute: project)) + + post api("/import/github", user), params: { + target_namespace: user.namespace_path, + personal_access_token: token, + repo_id: 1234 + } + expect(response).to have_gitlab_http_status(201) + expect(json_response).to be_a Hash + expect(json_response['name']).to eq(project.name) + end + + it 'returns 422 response when user can not create projects in the chosen namespace' do + other_namespace = create(:group, name: 'other_namespace') + + post api("/import/github", user), params: { + target_namespace: other_namespace.name, + personal_access_token: token, + repo_id: 1234 + } + + expect(response).to have_gitlab_http_status(422) + end + end +end diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index ffe4512fa6f..7248908b494 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -948,6 +948,7 @@ describe API::Projects do expect(json_response['shared_with_groups'].length).to eq(1) expect(json_response['shared_with_groups'][0]['group_id']).to eq(group.id) expect(json_response['shared_with_groups'][0]['group_name']).to eq(group.name) + expect(json_response['shared_with_groups'][0]['group_full_path']).to eq(group.full_path) expect(json_response['shared_with_groups'][0]['group_access_level']).to eq(link.group_access) expect(json_response['shared_with_groups'][0]['expires_at']).to be_nil expect(json_response['only_allow_merge_if_pipeline_succeeds']).to eq(project.only_allow_merge_if_pipeline_succeeds) @@ -967,6 +968,7 @@ describe API::Projects do expect(json_response['shared_with_groups'].length).to eq(1) expect(json_response['shared_with_groups'][0]['group_id']).to eq(group.id) expect(json_response['shared_with_groups'][0]['group_name']).to eq(group.name) + expect(json_response['shared_with_groups'][0]['group_full_path']).to eq(group.full_path) expect(json_response['shared_with_groups'][0]['group_access_level']).to eq(link.group_access) expect(json_response['shared_with_groups'][0]['expires_at']).to eq(expires_at.to_s) end @@ -1145,6 +1147,40 @@ describe API::Projects do .to eq(Gitlab::Access::OWNER) end end + + context 'nested group project', :nested_groups do + let(:group) { create(:group) } + let(:nested_group) { create(:group, parent: group) } + let(:project2) { create(:project, group: nested_group) } + + before do + project2.group.parent.add_owner(user) + end + + it 'sets group access and return 200' do + get api("/projects/#{project2.id}", user) + + expect(response).to have_gitlab_http_status(200) + expect(json_response['permissions']['project_access']).to be_nil + expect(json_response['permissions']['group_access']['access_level']) + .to eq(Gitlab::Access::OWNER) + end + + context 'with various access levels across nested groups' do + before do + project2.group.add_maintainer(user) + end + + it 'sets the maximum group access and return 200' do + get api("/projects/#{project2.id}", user) + + expect(response).to have_gitlab_http_status(200) + expect(json_response['permissions']['project_access']).to be_nil + expect(json_response['permissions']['group_access']['access_level']) + .to eq(Gitlab::Access::OWNER) + end + end + end end end end diff --git a/spec/requests/api/settings_spec.rb b/spec/requests/api/settings_spec.rb index cfbda63bb30..45fb1562e84 100644 --- a/spec/requests/api/settings_spec.rb +++ b/spec/requests/api/settings_spec.rb @@ -63,7 +63,8 @@ describe API::Settings, 'Settings' do terms: 'Hello world!', performance_bar_allowed_group_path: group.full_path, instance_statistics_visibility_private: true, - diff_max_patch_bytes: 150_000 + diff_max_patch_bytes: 150_000, + default_branch_protection: Gitlab::Access::PROTECTION_DEV_CAN_MERGE } expect(response).to have_gitlab_http_status(200) @@ -88,6 +89,7 @@ describe API::Settings, 'Settings' do expect(json_response['performance_bar_allowed_group_id']).to eq(group.id) expect(json_response['instance_statistics_visibility_private']).to be(true) expect(json_response['diff_max_patch_bytes']).to eq(150_000) + expect(json_response['default_branch_protection']).to eq(Gitlab::Access::PROTECTION_DEV_CAN_MERGE) end end diff --git a/spec/requests/api/submodules_spec.rb b/spec/requests/api/submodules_spec.rb index c482a85c68f..064392fb185 100644 --- a/spec/requests/api/submodules_spec.rb +++ b/spec/requests/api/submodules_spec.rb @@ -64,7 +64,7 @@ describe API::Submodules do expect(response).to have_gitlab_http_status(400) end - it 'returns the commmit' do + it 'returns the commit' do head_commit = project.repository.commit.id put api(route(submodule), user), params: params @@ -81,7 +81,7 @@ describe API::Submodules do let(:branch) { 'submodule_inside_folder' } let(:encoded_submodule) { CGI.escape(submodule) } - it 'returns the commmit' do + it 'returns the commit' do expect(Submodules::UpdateService) .to receive(:new) .with(any_args, hash_including(submodule: submodule)) diff --git a/spec/requests/api/tags_spec.rb b/spec/requests/api/tags_spec.rb index d09b6fe72b1..fffe878ddbd 100644 --- a/spec/requests/api/tags_spec.rb +++ b/spec/requests/api/tags_spec.rb @@ -54,6 +54,18 @@ describe API::Tags do end end + context 'searching' do + it 'only returns searched tags' do + get api("#{route}", user), params: { search: 'v1.1.0' } + + expect(response).to have_gitlab_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response.size).to eq(1) + expect(json_response[0]['name']).to eq('v1.1.0') + end + end + shared_examples_for 'repository tags' do it 'returns the repository tags' do get api(route, current_user) diff --git a/spec/requests/api/wikis_spec.rb b/spec/requests/api/wikis_spec.rb index f5092e8e2b5..6109829aad1 100644 --- a/spec/requests/api/wikis_spec.rb +++ b/spec/requests/api/wikis_spec.rb @@ -22,7 +22,7 @@ describe API::Wikis do context 'when wiki has pages' do let!(:pages) do [create(:wiki_page, wiki: project_wiki, attrs: { title: 'page1', content: 'content of page1' }), - create(:wiki_page, wiki: project_wiki, attrs: { title: 'page2', content: 'content of page2' })] + create(:wiki_page, wiki: project_wiki, attrs: { title: 'page2.with.dot', content: 'content of page2' })] end it 'returns the list of wiki pages without content' do diff --git a/spec/requests/lfs_locks_api_spec.rb b/spec/requests/lfs_locks_api_spec.rb index 28cb90e450e..c63fbcdd84e 100644 --- a/spec/requests/lfs_locks_api_spec.rb +++ b/spec/requests/lfs_locks_api_spec.rb @@ -132,6 +132,17 @@ describe 'Git LFS File Locking API' do expect(json_response['lock'].keys).to match_array(%w(id path locked_at owner)) end + + context 'when a maintainer uses force' do + let(:authorization) { authorize_user(maintainer) } + + it 'deletes the lock' do + project.add_maintainer(maintainer) + post_lfs_json url, { force: true }, headers + + expect(response).to have_gitlab_http_status(200) + end + end end end diff --git a/spec/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb index 5c3b37ef11c..a0d01fc8263 100644 --- a/spec/routing/project_routing_spec.rb +++ b/spec/routing/project_routing_spec.rb @@ -122,6 +122,10 @@ describe 'project routing' do route_to('projects#preview_markdown', namespace_id: 'gitlab', id: 'gitlabhq') ) end + + it 'to #resolve' do + expect(get('/projects/1')).to route_to('projects#resolve', id: '1') + end end # members_namespace_project_autocomplete_sources_path GET /:project_id/autocomplete_sources/members(.:format) projects/autocomplete_sources#members diff --git a/spec/rubocop/cop/inject_enterprise_edition_module_spec.rb b/spec/rubocop/cop/inject_enterprise_edition_module_spec.rb index 08ffc3c3a53..0ff777388e5 100644 --- a/spec/rubocop/cop/inject_enterprise_edition_module_spec.rb +++ b/spec/rubocop/cop/inject_enterprise_edition_module_spec.rb @@ -19,6 +19,41 @@ describe RuboCop::Cop::InjectEnterpriseEditionModule do SOURCE end + it 'does not flag the use of `prepend EEFoo` in the middle of a file' do + expect_no_offenses(<<~SOURCE) + class Foo + prepend EEFoo + end + SOURCE + end + + it 'flags the use of `prepend EE::Foo::Bar` in the middle of a file' do + expect_offense(<<~SOURCE) + class Foo + prepend EE::Foo::Bar + ^^^^^^^^^^^^^^^^^^^^ Injecting EE modules must be done on the last line of this file, outside of any class or module definitions + end + SOURCE + end + + it 'flags the use of `prepend(EE::Foo::Bar)` in the middle of a file' do + expect_offense(<<~SOURCE) + class Foo + prepend(EE::Foo::Bar) + ^^^^^^^^^^^^^^^^^^^^^ Injecting EE modules must be done on the last line of this file, outside of any class or module definitions + end + SOURCE + end + + it 'flags the use of `prepend EE::Foo::Bar::Baz` in the middle of a file' do + expect_offense(<<~SOURCE) + class Foo + prepend EE::Foo::Bar::Baz + ^^^^^^^^^^^^^^^^^^^^^^^^^ Injecting EE modules must be done on the last line of this file, outside of any class or module definitions + end + SOURCE + end + it 'flags the use of `prepend ::EE` in the middle of a file' do expect_offense(<<~SOURCE) class Foo diff --git a/spec/services/projects/after_rename_service_spec.rb b/spec/services/projects/after_rename_service_spec.rb index 59c08b30f9f..bc5366a3339 100644 --- a/spec/services/projects/after_rename_service_spec.rb +++ b/spec/services/projects/after_rename_service_spec.rb @@ -4,53 +4,45 @@ require 'spec_helper' describe Projects::AfterRenameService do let(:rugged_config) { rugged_repo(project.repository).config } + let(:legacy_storage) { Storage::LegacyProject.new(project) } + let(:hashed_storage) { Storage::HashedProject.new(project) } + let!(:path_before_rename) { project.path } + let!(:full_path_before_rename) { project.full_path } + let!(:path_after_rename) { "#{project.path}-renamed" } + let!(:full_path_after_rename) { "#{project.full_path}-renamed" } describe '#execute' do context 'using legacy storage' do - let(:project) { create(:project, :repository, :legacy_storage) } - let(:gitlab_shell) { Gitlab::Shell.new } + let(:project) { create(:project, :repository, :wiki_repo, :legacy_storage) } let(:project_storage) { project.send(:storage) } + let(:gitlab_shell) { Gitlab::Shell.new } before do # Project#gitlab_shell returns a new instance of Gitlab::Shell on every # call. This makes testing a bit easier. allow(project).to receive(:gitlab_shell).and_return(gitlab_shell) - allow(project) - .to receive(:previous_changes) - .and_return('path' => ['foo']) - - allow(project) - .to receive(:path_was) - .and_return('foo') - stub_feature_flags(skip_hashed_storage_upgrade: false) end it 'renames a repository' do stub_container_registry_config(enabled: false) - expect(gitlab_shell).to receive(:mv_repository) - .ordered - .with(project.repository_storage, "#{project.namespace.full_path}/foo", "#{project.full_path}") - .and_return(true) - - expect(gitlab_shell).to receive(:mv_repository) - .ordered - .with(project.repository_storage, "#{project.namespace.full_path}/foo.wiki", "#{project.full_path}.wiki") - .and_return(true) - expect_any_instance_of(SystemHooksService) .to receive(:execute_hooks_for) .with(project, :rename) expect_any_instance_of(Gitlab::UploadsTransfer) .to receive(:rename_project) - .with('foo', project.path, project.namespace.full_path) + .with(path_before_rename, path_after_rename, project.namespace.full_path) - expect(project).to receive(:expire_caches_before_rename) + expect_repository_exist("#{full_path_before_rename}.git") + expect_repository_exist("#{full_path_before_rename}.wiki.git") - described_class.new(project).execute + service_execute + + expect_repository_exist("#{full_path_after_rename}.git") + expect_repository_exist("#{full_path_after_rename}.wiki.git") end context 'container registry with images' do @@ -63,8 +55,7 @@ describe Projects::AfterRenameService do end it 'raises a RenameFailedError' do - expect { described_class.new(project).execute } - .to raise_error(described_class::RenameFailedError) + expect { service_execute }.to raise_error(described_class::RenameFailedError) end end @@ -76,7 +67,7 @@ describe Projects::AfterRenameService do it 'moves pages folder to new location' do expect_any_instance_of(Gitlab::PagesTransfer).to receive(:rename_project) - described_class.new(project).execute + service_execute end end @@ -88,14 +79,12 @@ describe Projects::AfterRenameService do it 'moves uploads folder to new location' do expect_any_instance_of(Gitlab::UploadsTransfer).to receive(:rename_project) - described_class.new(project).execute + service_execute end end it 'updates project full path in .git/config' do - allow(project_storage).to receive(:rename_repo).and_return(true) - - described_class.new(project).execute + service_execute expect(rugged_config['gitlab.fullpath']).to eq(project.full_path) end @@ -103,13 +92,25 @@ describe Projects::AfterRenameService do it 'updates storage location' do allow(project_storage).to receive(:rename_repo).and_return(true) - described_class.new(project).execute + service_execute expect(project.project_repository).to have_attributes( disk_path: project.disk_path, shard_name: project.repository_storage ) end + + context 'with hashed storage upgrade when renaming enabled' do + it 'calls HashedStorageMigrationService with correct options' do + stub_application_setting(hashed_storage_enabled: true) + + expect_next_instance_of(::Projects::HashedStorageMigrationService) do |service| + expect(service).to receive(:execute).and_return(true) + end + + service_execute + end + end end context 'using hashed storage' do @@ -123,25 +124,11 @@ describe Projects::AfterRenameService do # Project#gitlab_shell returns a new instance of Gitlab::Shell on every # call. This makes testing a bit easier. allow(project).to receive(:gitlab_shell).and_return(gitlab_shell) - allow(project).to receive(:previous_changes).and_return('path' => ['foo']) stub_feature_flags(skip_hashed_storage_upgrade: false) stub_application_setting(hashed_storage_enabled: true) end - context 'migration to hashed storage' do - it 'calls HashedStorageMigrationService with correct options' do - project = create(:project, :repository, :legacy_storage) - allow(project).to receive(:previous_changes).and_return('path' => ['foo']) - - expect_next_instance_of(::Projects::HashedStorageMigrationService) do |service| - expect(service).to receive(:execute).and_return(true) - end - - described_class.new(project).execute - end - end - it 'renames a repository' do stub_container_registry_config(enabled: false) @@ -153,7 +140,7 @@ describe Projects::AfterRenameService do expect(project).to receive(:expire_caches_before_rename) - described_class.new(project).execute + service_execute end context 'container registry with images' do @@ -166,7 +153,7 @@ describe Projects::AfterRenameService do end it 'raises a RenameFailedError' do - expect { described_class.new(project).execute } + expect { service_execute } .to raise_error(described_class::RenameFailedError) end end @@ -175,38 +162,46 @@ describe Projects::AfterRenameService do it 'moves pages folder to new location' do expect_any_instance_of(Gitlab::PagesTransfer).to receive(:rename_project) - described_class.new(project).execute + service_execute end end context 'attachments' do + let(:uploader) { create(:upload, :issuable_upload, :with_file, model: project) } + let(:file_uploader) { build(:file_uploader, project: project) } + let(:legacy_storage_path) { File.join(file_uploader.root, legacy_storage.disk_path) } + let(:hashed_storage_path) { File.join(file_uploader.root, hashed_storage.disk_path) } + it 'keeps uploads folder location unchanged' do expect_any_instance_of(Gitlab::UploadsTransfer).not_to receive(:rename_project) - described_class.new(project).execute + service_execute end context 'when not rolled out' do let(:project) { create(:project, :repository, storage_version: 1, skip_disk_validation: true) } - it 'moves pages folder to hashed storage' do - expect_next_instance_of(Projects::HashedStorage::MigrateAttachmentsService) do |service| - expect(service).to receive(:execute) - end + it 'moves attachments folder to hashed storage' do + expect(File.directory?(legacy_storage_path)).to be_truthy + expect(File.directory?(hashed_storage_path)).to be_falsey - described_class.new(project).execute + service_execute + expect(project.reload.hashed_storage?(:attachments)).to be_truthy + + expect(File.directory?(legacy_storage_path)).to be_falsey + expect(File.directory?(hashed_storage_path)).to be_truthy end end end it 'updates project full path in .git/config' do - described_class.new(project).execute + service_execute expect(rugged_config['gitlab.fullpath']).to eq(project.full_path) end it 'updates storage location' do - described_class.new(project).execute + service_execute expect(project.project_repository).to have_attributes( disk_path: project.disk_path, @@ -215,4 +210,21 @@ describe Projects::AfterRenameService do end end end + + def service_execute + # AfterRenameService is called by UpdateService after a successful model.update + # the initialization will include before and after paths values + project.update(path: path_after_rename) + + described_class.new(project, path_before: path_before_rename, full_path_before: full_path_before_rename).execute + end + + def expect_repository_exist(full_path_with_extension) + expect( + gitlab_shell.exists?( + project.repository_storage, + full_path_with_extension + ) + ).to be_truthy + end end diff --git a/spec/services/projects/destroy_service_spec.rb b/spec/services/projects/destroy_service_spec.rb index 12ddf8447bd..dfbdfa2ab69 100644 --- a/spec/services/projects/destroy_service_spec.rb +++ b/spec/services/projects/destroy_service_spec.rb @@ -281,6 +281,40 @@ describe Projects::DestroyService do end end + context 'repository +deleted path removal' do + def removal_path(path) + "#{path}+#{project.id}#{described_class::DELETED_FLAG}" + end + + context 'regular phase' do + it 'schedules +deleted removal of existing repos' do + service = described_class.new(project, user, {}) + allow(service).to receive(:schedule_stale_repos_removal) + + expect(GitlabShellWorker).to receive(:perform_in) + .with(5.minutes, :remove_repository, project.repository_storage, removal_path(project.disk_path)) + + service.execute + end + end + + context 'stale cleanup' do + let!(:async) { true } + + it 'schedules +deleted wiki and repo removal' do + allow(ProjectDestroyWorker).to receive(:perform_async) + + expect(GitlabShellWorker).to receive(:perform_in) + .with(10.minutes, :remove_repository, project.repository_storage, removal_path(project.disk_path)) + + expect(GitlabShellWorker).to receive(:perform_in) + .with(10.minutes, :remove_repository, project.repository_storage, removal_path(project.wiki.disk_path)) + + destroy_project(project, user, {}) + end + end + end + context '#attempt_restore_repositories' do let(:path) { project.disk_path + '.git' } diff --git a/spec/services/projects/protect_default_branch_service_spec.rb b/spec/services/projects/protect_default_branch_service_spec.rb new file mode 100644 index 00000000000..c145b2c06c6 --- /dev/null +++ b/spec/services/projects/protect_default_branch_service_spec.rb @@ -0,0 +1,242 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Projects::ProtectDefaultBranchService do + let(:service) { described_class.new(project) } + let(:project) { instance_spy(Project) } + + describe '#execute' do + before do + allow(service) + .to receive(:protect_default_branch) + end + + context 'without a default branch' do + it 'does nothing' do + allow(service) + .to receive(:default_branch) + .and_return(nil) + + service.execute + + expect(service) + .not_to have_received(:protect_default_branch) + end + end + + context 'with a default branch' do + it 'protects the default branch' do + allow(service) + .to receive(:default_branch) + .and_return('master') + + service.execute + + expect(service) + .to have_received(:protect_default_branch) + end + end + end + + describe '#protect_default_branch' do + before do + allow(service) + .to receive(:default_branch) + .and_return('master') + + allow(project) + .to receive(:change_head) + .with('master') + + allow(service) + .to receive(:create_protected_branch) + end + + context 'when branch protection is needed' do + before do + allow(service) + .to receive(:protect_branch?) + .and_return(true) + + allow(service) + .to receive(:create_protected_branch) + end + + it 'changes the HEAD of the project' do + service.protect_default_branch + + expect(project) + .to have_received(:change_head) + end + + it 'protects the default branch' do + service.protect_default_branch + + expect(service) + .to have_received(:create_protected_branch) + end + end + + context 'when branch protection is not needed' do + before do + allow(service) + .to receive(:protect_branch?) + .and_return(false) + end + + it 'changes the HEAD of the project' do + service.protect_default_branch + + expect(project) + .to have_received(:change_head) + end + + it 'does not protect the default branch' do + service.protect_default_branch + + expect(service) + .not_to have_received(:create_protected_branch) + end + end + end + + describe '#create_protected_branch' do + it 'creates the protected branch' do + creator = instance_spy(User) + create_service = instance_spy(ProtectedBranches::CreateService) + access_level = Gitlab::Access::DEVELOPER + params = { + name: 'master', + push_access_levels_attributes: [{ access_level: access_level }], + merge_access_levels_attributes: [{ access_level: access_level }] + } + + allow(project) + .to receive(:creator) + .and_return(creator) + + allow(ProtectedBranches::CreateService) + .to receive(:new) + .with(project, creator, params) + .and_return(create_service) + + allow(service) + .to receive(:push_access_level) + .and_return(access_level) + + allow(service) + .to receive(:merge_access_level) + .and_return(access_level) + + allow(service) + .to receive(:default_branch) + .and_return('master') + + allow(create_service) + .to receive(:execute) + .with(skip_authorization: true) + + service.create_protected_branch + + expect(create_service) + .to have_received(:execute) + end + end + + describe '#protect_branch?' do + context 'when default branch protection is disabled' do + it 'returns false' do + allow(Gitlab::CurrentSettings) + .to receive(:default_branch_protection) + .and_return(Gitlab::Access::PROTECTION_NONE) + + expect(service.protect_branch?).to eq(false) + end + end + + context 'when default branch protection is enabled' do + before do + allow(Gitlab::CurrentSettings) + .to receive(:default_branch_protection) + .and_return(Gitlab::Access::PROTECTION_DEV_CAN_MERGE) + + allow(service) + .to receive(:default_branch) + .and_return('master') + end + + it 'returns false if the branch is already protected' do + allow(ProtectedBranch) + .to receive(:protected?) + .with(project, 'master') + .and_return(true) + + expect(service.protect_branch?).to eq(false) + end + + it 'returns true if the branch is not yet protected' do + allow(ProtectedBranch) + .to receive(:protected?) + .with(project, 'master') + .and_return(false) + + expect(service.protect_branch?).to eq(true) + end + end + end + + describe '#default_branch' do + it 'returns the default branch of the project' do + allow(project) + .to receive(:default_branch) + .and_return('master') + + expect(service.default_branch).to eq('master') + end + end + + describe '#push_access_level' do + context 'when developers can push' do + it 'returns the DEVELOPER access level' do + allow(Gitlab::CurrentSettings) + .to receive(:default_branch_protection) + .and_return(Gitlab::Access::PROTECTION_DEV_CAN_PUSH) + + expect(service.push_access_level).to eq(Gitlab::Access::DEVELOPER) + end + end + + context 'when developers can not push' do + it 'returns the MAINTAINER access level' do + allow(Gitlab::CurrentSettings) + .to receive(:default_branch_protection) + .and_return(Gitlab::Access::PROTECTION_DEV_CAN_MERGE) + + expect(service.push_access_level).to eq(Gitlab::Access::MAINTAINER) + end + end + end + + describe '#merge_access_level' do + context 'when developers can merge' do + it 'returns the DEVELOPER access level' do + allow(Gitlab::CurrentSettings) + .to receive(:default_branch_protection) + .and_return(Gitlab::Access::PROTECTION_DEV_CAN_MERGE) + + expect(service.merge_access_level).to eq(Gitlab::Access::DEVELOPER) + end + end + + context 'when developers can not merge' do + it 'returns the MAINTAINER access level' do + allow(Gitlab::CurrentSettings) + .to receive(:default_branch_protection) + .and_return(Gitlab::Access::PROTECTION_DEV_CAN_PUSH) + + expect(service.merge_access_level).to eq(Gitlab::Access::MAINTAINER) + end + end + end +end diff --git a/spec/services/suggestions/apply_service_spec.rb b/spec/services/suggestions/apply_service_spec.rb index 3a483717756..e5ca1c155ed 100644 --- a/spec/services/suggestions/apply_service_spec.rb +++ b/spec/services/suggestions/apply_service_spec.rb @@ -117,13 +117,184 @@ describe Suggestions::ApplyService do expect(commit.committer_email).to eq(user.commit_email) expect(commit.author_name).to eq(user.name) end + + context 'when it fails to apply because the file was changed' do + it 'returns error message' do + service = instance_double(Files::UpdateService) + + expect(Files::UpdateService).to receive(:new) + .and_return(service) + + allow(service).to receive(:execute) + .and_raise(Files::UpdateService::FileChangedError) + + result = subject.execute(suggestion) + + expect(result).to eq(message: 'The file has been changed', status: :error) + end + end + + context 'when diff ref from position is different from repo diff refs' do + it 'returns error message' do + outdated_refs = Gitlab::Diff::DiffRefs.new(base_sha: 'foo', start_sha: 'bar', head_sha: 'outdated') + + allow(suggestion).to receive(:appliable?) { true } + allow(suggestion.position).to receive(:diff_refs) { outdated_refs } + + result = subject.execute(suggestion) + + expect(result).to eq(message: 'The file has been changed', status: :error) + end + end + + context 'multiple suggestions applied' do + let(:expected_content) do + <<-CONTENT.strip_heredoc + require 'fileutils' + require 'open3' + + module Popen + extend self + + + def popen(cmd, path=nil) + unless cmd.is_a?(Array) + # v1 change + end + + path ||= Dir.pwd + # v1 change + vars = { + "PWD" => path + } + + options = { + chdir: path + } + # v2 change + unless File.directory?(path) + FileUtils.mkdir_p(path) + end + + @cmd_output = "" + # v2 change + + Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr| + @cmd_output << stdout.read + @cmd_output << stderr.read + @cmd_status = wait_thr.value.exitstatus + end + + return @cmd_output, @cmd_status + end + end + CONTENT + end + + let(:merge_request) do + create(:merge_request, source_project: project, + target_project: project) + end + + def create_suggestion(diff, old_line: nil, new_line: nil, from_content:, to_content:, path:) + position = Gitlab::Diff::Position.new(old_path: path, + new_path: path, + old_line: old_line, + new_line: new_line, + diff_refs: diff.diff_refs) + + suggestion_note = create(:diff_note_on_merge_request, noteable: merge_request, + original_position: position, + position: position, + project: project) + create(:suggestion, note: suggestion_note, + from_content: from_content, + to_content: to_content) + end + + def apply_suggestion(suggestion) + suggestion.note.reload + merge_request.reload + merge_request.clear_memoized_shas + + result = subject.execute(suggestion) + refresh = MergeRequests::RefreshService.new(project, user) + refresh.execute(merge_request.diff_head_sha, + suggestion.commit_id, + merge_request.source_branch_ref) + + result + end + + def fetch_raw_diff(suggestion) + project.reload.commit(suggestion.commit_id).diffs.diff_files.first.diff.diff + end + + it 'applies multiple suggestions in subsequent versions correctly' do + diff = merge_request.merge_request_diff + path = 'files/ruby/popen.rb' + + suggestion_1_changes = { old_line: nil, + new_line: 13, + from_content: "\n", + to_content: "# v1 change\n", + path: path } + + suggestion_2_changes = { old_line: 24, + new_line: 31, + from_content: " @cmd_output << stderr.read\n", + to_content: "# v2 change\n", + path: path } + + suggestion_1 = create_suggestion(diff, suggestion_1_changes) + suggestion_2 = create_suggestion(diff, suggestion_2_changes) + + apply_suggestion(suggestion_1) + + suggestion_1_diff = fetch_raw_diff(suggestion_1) + + # rubocop: disable Layout/TrailingWhitespace + expected_suggestion_1_diff = <<-CONTENT.strip_heredoc + @@ -10,7 +10,7 @@ module Popen + end + + path ||= Dir.pwd + - + +# v1 change + vars = { + "PWD" => path + } + CONTENT + # rubocop: enable Layout/TrailingWhitespace + + apply_suggestion(suggestion_2) + + suggestion_2_diff = fetch_raw_diff(suggestion_2) + + # rubocop: disable Layout/TrailingWhitespace + expected_suggestion_2_diff = <<-CONTENT.strip_heredoc + @@ -28,7 +28,7 @@ module Popen + + Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr| + @cmd_output << stdout.read + - @cmd_output << stderr.read + +# v2 change + @cmd_status = wait_thr.value.exitstatus + end + CONTENT + # rubocop: enable Layout/TrailingWhitespace + + expect(suggestion_1_diff.strip).to eq(expected_suggestion_1_diff.strip) + expect(suggestion_2_diff.strip).to eq(expected_suggestion_2_diff.strip) + end + end end context 'fork-project' do let(:project) { create(:project, :public, :repository) } let(:forked_project) do - fork_project_with_submodules(project, user) + fork_project_with_submodules(project, user, repository: project.repository) end let(:merge_request) do diff --git a/spec/sidekiq/cron/job_gem_dependency_spec.rb b/spec/sidekiq/cron/job_gem_dependency_spec.rb index 2e30cf025b0..2e7de75fd08 100644 --- a/spec/sidekiq/cron/job_gem_dependency_spec.rb +++ b/spec/sidekiq/cron/job_gem_dependency_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' describe Sidekiq::Cron::Job do describe 'cron jobs' do - context 'when rufus-scheduler depends on ZoTime or EoTime' do + context 'when Fugit depends on ZoTime or EoTime' do before do described_class .create(name: 'TestCronWorker', @@ -10,7 +10,7 @@ describe Sidekiq::Cron::Job do class: Settings.cron_jobs[:pipeline_schedule_worker]['job_class']) end - it 'does not get "Rufus::Scheduler::ZoTime/EtOrbi::EoTime into an exact number"' do + it 'does not get any errors' do expect { described_class.all.first.should_enque?(Time.now) }.not_to raise_error end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 89357056c93..72684caad32 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -22,7 +22,7 @@ if rspec_profiling_is_configured && (!ENV.key?('CI') || branch_can_be_profiled) require 'rspec_profiling/rspec' end -if ENV['CI'] && !ENV['NO_KNAPSACK'] +if ENV['CI'] && ENV['KNAPSACK_GENERATE_REPORT'] && !ENV['NO_KNAPSACK'] require 'knapsack' Knapsack::Adapters::RSpecAdapter.bind end @@ -127,6 +127,16 @@ RSpec.configure do |config| .and_return(false) end + config.before(:suite) do + # Set latest release blog post URL for "What's new?" link + Gitlab::ReleaseBlogPost.instance.instance_variable_set(:@url, 'https://about.gitlab.com') + end + + config.before(:example, :quarantine) do + # Skip tests in quarantine unless we explicitly focus on them. + skip('In quarantine') unless config.inclusion_filter[:quarantine] + end + config.before(:example, :request_store) do RequestStore.begin! end diff --git a/spec/support/migrations_helpers/cluster_helpers.rb b/spec/support/migrations_helpers/cluster_helpers.rb new file mode 100644 index 00000000000..b54af15c29e --- /dev/null +++ b/spec/support/migrations_helpers/cluster_helpers.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: true + +module MigrationHelpers + module ClusterHelpers + # Creates a list of cluster projects. + def create_cluster_project_list(quantity) + group = namespaces_table.create(name: 'gitlab-org', path: 'gitlab-org') + + quantity.times do |id| + create_cluster_project(group, id) + end + end + + # Creates dependencies for a cluster project: + # - Group + # - Project + # - Cluster + # - Project - cluster relationship + # - GCP provider + # - Platform Kubernetes + def create_cluster_project(group, id) + project = projects_table.create!( + name: "project-#{id}", + path: "project-#{id}", + namespace_id: group.id + ) + + cluster = clusters_table.create( + name: 'test-cluster', + cluster_type: 3, + provider_type: :gcp, + platform_type: :kubernetes + ) + + cluster_projects_table.create(project_id: project.id, cluster_id: cluster.id) + + provider_gcp_table.create!( + gcp_project_id: "test-gcp-project-#{id}", + endpoint: '111.111.111.111', + cluster_id: cluster.id, + status: 3, + num_nodes: 1, + zone: 'us-central1-a' + ) + + platform_kubernetes_table.create( + cluster_id: cluster.id, + api_url: 'https://kubernetes.example.com', + encrypted_token: 'a' * 40, + encrypted_token_iv: 'a' * 40 + ) + end + + # Creates a Kubernetes namespace for a list of clusters + def create_kubernetes_namespace(clusters) + clusters.each do |cluster| + cluster_project = cluster_projects_table.find_by(cluster_id: cluster.id) + project = projects_table.find(cluster_project.project_id) + namespace = "#{project.path}-#{project.id}" + + cluster_kubernetes_namespaces_table.create( + cluster_project_id: cluster_project.id, + cluster_id: cluster.id, + project_id: cluster_project.project_id, + namespace: namespace, + service_account_name: "#{namespace}-service-account" + ) + end + end + end +end diff --git a/spec/support/shared_examples/controllers/issuable_notes_filter_shared_examples.rb b/spec/support/shared_examples/controllers/issuable_notes_filter_shared_examples.rb index dbdca99b5aa..0acc9e2a836 100644 --- a/spec/support/shared_examples/controllers/issuable_notes_filter_shared_examples.rb +++ b/spec/support/shared_examples/controllers/issuable_notes_filter_shared_examples.rb @@ -1,8 +1,16 @@ shared_examples 'issuable notes filter' do + let(:params) do + if issuable_parent.is_a?(Project) + { namespace_id: issuable_parent.namespace, project_id: issuable_parent, id: issuable.iid } + else + { group_id: issuable_parent, id: issuable.to_param } + end + end + it 'sets discussion filter' do notes_filter = UserPreference::NOTES_FILTERS[:only_comments] - get :discussions, params: { namespace_id: project.namespace, project_id: project, id: issuable.iid, notes_filter: notes_filter } + get :discussions, params: params.merge(notes_filter: notes_filter) expect(user.reload.notes_filter_for(issuable)).to eq(notes_filter) expect(UserPreference.count).to eq(1) @@ -13,7 +21,7 @@ shared_examples 'issuable notes filter' do expect_any_instance_of(issuable.class).to receive(:expire_note_etag_cache) - get :discussions, params: { namespace_id: project.namespace, project_id: project, id: issuable.iid, notes_filter: notes_filter } + get :discussions, params: params.merge(notes_filter: notes_filter) end it 'does not expires notes e-tag cache for issuable if filter did not change' do @@ -22,14 +30,14 @@ shared_examples 'issuable notes filter' do expect_any_instance_of(issuable.class).not_to receive(:expire_note_etag_cache) - get :discussions, params: { namespace_id: project.namespace, project_id: project, id: issuable.iid, notes_filter: notes_filter } + get :discussions, params: params.merge(notes_filter: notes_filter) end it 'does not set notes filter when database is in read only mode' do allow(Gitlab::Database).to receive(:read_only?).and_return(true) notes_filter = UserPreference::NOTES_FILTERS[:only_comments] - get :discussions, params: { namespace_id: project.namespace, project_id: project, id: issuable.iid, notes_filter: notes_filter } + get :discussions, params: params.merge(notes_filter: notes_filter) expect(user.reload.notes_filter_for(issuable)).to eq(0) end @@ -37,7 +45,7 @@ shared_examples 'issuable notes filter' do it 'returns only user comments' do user.set_notes_filter(UserPreference::NOTES_FILTERS[:only_comments], issuable) - get :discussions, params: { namespace_id: project.namespace, project_id: project, id: issuable.iid } + get :discussions, params: params discussions = JSON.parse(response.body) expect(discussions.count).to eq(1) @@ -47,7 +55,7 @@ shared_examples 'issuable notes filter' do it 'returns only activity notes' do user.set_notes_filter(UserPreference::NOTES_FILTERS[:only_activity], issuable) - get :discussions, params: { namespace_id: project.namespace, project_id: project, id: issuable.iid } + get :discussions, params: params discussions = JSON.parse(response.body) expect(discussions.count).to eq(1) @@ -60,7 +68,7 @@ shared_examples 'issuable notes filter' do expect(ResourceEvents::MergeIntoNotesService).not_to receive(:new) - get :discussions, params: { namespace_id: project.namespace, project_id: project, id: issuable.iid } + get :discussions, params: params end end end diff --git a/spec/support/shared_examples/dirty_submit_form_shared_examples.rb b/spec/support/shared_examples/dirty_submit_form_shared_examples.rb index ba363593120..52a2ee49495 100644 --- a/spec/support/shared_examples/dirty_submit_form_shared_examples.rb +++ b/spec/support/shared_examples/dirty_submit_form_shared_examples.rb @@ -1,24 +1,36 @@ shared_examples 'dirty submit form' do |selector_args| selectors = selector_args.is_a?(Array) ? selector_args : [selector_args] + def expect_disabled_state(form, submit, is_disabled = true) + disabled_selector = is_disabled == true ? '[disabled]' : ':not([disabled])' + + form.find(".js-dirty-submit#{disabled_selector}", match: :first) + + expect(submit.disabled?).to be is_disabled + end + selectors.each do |selector| - it "disables #{selector[:form]} submit until there are changes", :js do + it "disables #{selector[:form]} submit until there are changes on #{selector[:input]}", :js do form = find(selector[:form]) submit = form.first('.js-dirty-submit') input = form.first(selector[:input]) + is_radio = input[:type] == 'radio' + is_checkbox = input[:type] == 'checkbox' + is_checkable = is_radio || is_checkbox original_value = input.value + original_checkable = form.find("input[name='#{input[:name]}'][checked]") if is_radio + original_checkable = input if is_checkbox expect(submit.disabled?).to be true + expect(input.checked?).to be false - input.set("#{original_value} changes") + is_checkable ? input.click : input.set("#{original_value} changes") - form.find('.js-dirty-submit:not([disabled])', match: :first) - expect(submit.disabled?).to be false + expect_disabled_state(form, submit, false) - input.set(original_value) + is_checkable ? original_checkable.click : input.set(original_value) - form.find('.js-dirty-submit[disabled]', match: :first) - expect(submit.disabled?).to be true + expect_disabled_state(form, submit) end end end diff --git a/spec/support/shared_examples/lib/gitlab/background_migration/backfill_project_repositories_examples.rb b/spec/support/shared_examples/lib/gitlab/background_migration/backfill_project_repositories_examples.rb index 1f688c0f9d3..dcf7c1a90c2 100644 --- a/spec/support/shared_examples/lib/gitlab/background_migration/backfill_project_repositories_examples.rb +++ b/spec/support/shared_examples/lib/gitlab/background_migration/backfill_project_repositories_examples.rb @@ -32,11 +32,13 @@ shared_examples 'backfill migration for project repositories' do |storage| it 'inserts rows in a single query' do projects.create!(name: 'foo', path: 'foo', namespace_id: group.id, storage_version: storage_version, repository_storage: shard.name) + group2 = namespaces.create!(name: 'gro', path: 'gro') control_count = ActiveRecord::QueryRecorder.new { described_class.new.perform(1, projects.last.id) } projects.create!(name: 'bar', path: 'bar', namespace_id: group.id, storage_version: storage_version, repository_storage: shard.name) - projects.create!(name: 'zoo', path: 'zoo', namespace_id: group.id, storage_version: storage_version, repository_storage: shard.name) + projects.create!(name: 'top', path: 'top', namespace_id: group.id, storage_version: storage_version, repository_storage: shard.name) + projects.create!(name: 'zoo', path: 'zoo', namespace_id: group2.id, storage_version: storage_version, repository_storage: shard.name) expect { described_class.new.perform(1, projects.last.id) }.not_to exceed_query_limit(control_count) end diff --git a/spec/uploaders/personal_file_uploader_spec.rb b/spec/uploaders/personal_file_uploader_spec.rb index 2896e9a112d..97758f0243e 100644 --- a/spec/uploaders/personal_file_uploader_spec.rb +++ b/spec/uploaders/personal_file_uploader_spec.rb @@ -4,19 +4,13 @@ describe PersonalFileUploader do let(:model) { create(:personal_snippet) } let(:uploader) { described_class.new(model) } let(:upload) { create(:upload, :personal_snippet_upload) } - let(:identifier) { %r{\h+/\S+} } subject { uploader } - it_behaves_like 'builds correct paths' do - let(:patterns) do - { - store_dir: %r[uploads/-/system/personal_snippet/\d+], - upload_path: identifier, - absolute_path: %r[#{CarrierWave.root}/uploads/-/system/personal_snippet/\d+/#{identifier}] - } - end - end + it_behaves_like 'builds correct paths', + store_dir: %r[uploads/-/system/personal_snippet/\d+], + upload_path: %r[\h+/\S+], + absolute_path: %r[#{CarrierWave.root}/uploads/-/system/personal_snippet\/\d+\/\h+\/\S+$] context "object_store is REMOTE" do before do @@ -25,13 +19,17 @@ describe PersonalFileUploader do include_context 'with storage', described_class::Store::REMOTE - it_behaves_like 'builds correct paths' do - let(:patterns) do - { - store_dir: %r[\d+/\h+], - upload_path: identifier - } - end + it_behaves_like 'builds correct paths', + store_dir: %r[\d+/\h+], + upload_path: %r[^personal_snippet\/\d+\/\h+\/<filename>] + end + + describe '#upload_paths' do + it 'builds correct paths for both local and remote storage' do + paths = uploader.upload_paths('test.jpg') + + expect(paths.first).to match(%r[\h+\/test.jpg]) + expect(paths.second).to match(%r[^personal_snippet\/\d+\/\h+\/test.jpg]) end end diff --git a/spec/views/help/instance_configuration.html.haml_spec.rb b/spec/views/help/instance_configuration.html.haml_spec.rb index f30b5881fde..ceb7e34a540 100644 --- a/spec/views/help/instance_configuration.html.haml_spec.rb +++ b/spec/views/help/instance_configuration.html.haml_spec.rb @@ -13,9 +13,9 @@ describe 'help/instance_configuration' do it 'has links to several sections' do render - expect(rendered).to have_link(nil, '#ssh-host-keys-fingerprints') if ssh_settings.any? - expect(rendered).to have_link(nil, '#gitlab-pages') - expect(rendered).to have_link(nil, '#gitlab-ci') + expect(rendered).to have_link(nil, href: '#ssh-host-keys-fingerprints') if ssh_settings.any? + expect(rendered).to have_link(nil, href: '#gitlab-pages') + expect(rendered).to have_link(nil, href: '#gitlab-ci') end it 'has several sections' do diff --git a/spec/views/projects/commit/show.html.haml_spec.rb b/spec/views/projects/commit/show.html.haml_spec.rb index a9c32122600..d07099489e5 100644 --- a/spec/views/projects/commit/show.html.haml_spec.rb +++ b/spec/views/projects/commit/show.html.haml_spec.rb @@ -54,9 +54,9 @@ describe 'projects/commit/show.html.haml' do end it 'shows that it is in the context of a merge request' do - merge_request_url = diffs_project_merge_request_url(project, merge_request, commit_id: commit.id) + merge_request_url = diffs_project_merge_request_path(project, merge_request, commit_id: commit.id) expect(rendered).to have_content("This commit is part of merge request") - expect(rendered).to have_link(merge_request.to_reference, merge_request_url) + expect(rendered).to have_link(merge_request.to_reference, href: merge_request_url) end end end diff --git a/spec/views/projects/settings/operations/show.html.haml_spec.rb b/spec/views/projects/settings/operations/show.html.haml_spec.rb index 752fd82c5e8..8e34521c7c8 100644 --- a/spec/views/projects/settings/operations/show.html.haml_spec.rb +++ b/spec/views/projects/settings/operations/show.html.haml_spec.rb @@ -13,8 +13,6 @@ describe 'projects/settings/operations/show' do describe 'Operations > Error Tracking' do before do - stub_feature_flags(error_tracking: true) - project.add_reporter(user) allow(view).to receive(:error_tracking_setting) diff --git a/spec/workers/git_garbage_collect_worker_spec.rb b/spec/workers/git_garbage_collect_worker_spec.rb index a159f24f876..4895a968d6e 100644 --- a/spec/workers/git_garbage_collect_worker_spec.rb +++ b/spec/workers/git_garbage_collect_worker_spec.rb @@ -71,6 +71,17 @@ describe GitGarbageCollectWorker do subject.perform(project.id) end + + context 'when the repository has joined a pool' do + let!(:pool) { create(:pool_repository, :ready) } + let(:project) { pool.source_project } + + it 'ensures the repositories are linked' do + expect_any_instance_of(PoolRepository).to receive(:link_repository).once + + subject.perform(project.id) + end + end end context 'when no lease can be obtained' do diff --git a/spec/workers/remote_mirror_notification_worker_spec.rb b/spec/workers/remote_mirror_notification_worker_spec.rb new file mode 100644 index 00000000000..e3db10ed645 --- /dev/null +++ b/spec/workers/remote_mirror_notification_worker_spec.rb @@ -0,0 +1,39 @@ +require 'spec_helper' + +describe RemoteMirrorNotificationWorker, :mailer do + set(:project) { create(:project, :repository, :remote_mirror) } + set(:mirror) { project.remote_mirrors.first } + + describe '#execute' do + it 'calls NotificationService#remote_mirror_update_failed when the mirror exists' do + mirror.update_column(:last_error, "There was a problem fetching") + + expect(NotificationService).to receive_message_chain(:new, :remote_mirror_update_failed) + + subject.perform(mirror.id) + + expect(mirror.reload.error_notification_sent?).to be_truthy + end + + it 'does nothing when the mirror has no errors' do + expect(NotificationService).not_to receive(:new) + + subject.perform(mirror.id) + end + + it 'does nothing when the mirror does not exist' do + expect(NotificationService).not_to receive(:new) + + subject.perform(RemoteMirror.maximum(:id).to_i.succ) + end + + it 'does nothing when a notification has already been sent' do + mirror.update_columns(last_error: "There was a problem fetching", + error_notification_sent: true) + + expect(NotificationService).not_to receive(:new) + + subject.perform(mirror.id) + end + end +end diff --git a/spec/workers/repository_update_remote_mirror_worker_spec.rb b/spec/workers/repository_update_remote_mirror_worker_spec.rb index d73b0b53713..b582a3650b6 100644 --- a/spec/workers/repository_update_remote_mirror_worker_spec.rb +++ b/spec/workers/repository_update_remote_mirror_worker_spec.rb @@ -22,6 +22,13 @@ describe RepositoryUpdateRemoteMirrorWorker do expect { subject.perform(remote_mirror.id, Time.now) }.to change { remote_mirror.reload.update_status }.to('finished') end + it 'resets the notification flag upon success' do + expect_any_instance_of(Projects::UpdateRemoteMirrorService).to receive(:execute).with(remote_mirror).and_return(status: :success) + remote_mirror.update_column(:error_notification_sent, true) + + expect { subject.perform(remote_mirror.id, Time.now) }.to change { remote_mirror.reload.error_notification_sent }.to(false) + end + it 'sets status as failed when update remote mirror service executes with errors' do error_message = 'fail!' diff --git a/yarn.lock b/yarn.lock index da00f335362..bb948ad703c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -29,18 +29,7 @@ semver "^5.4.1" source-map "^0.5.0" -"@babel/generator@^7.0.0": - version "7.1.2" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.1.2.tgz#fde75c072575ce7abbd97322e8fef5bae67e4630" - integrity sha512-70A9HWLS/1RHk3Ck8tNHKxOoKQuSKocYgwDN85Pyl/RBduss6AKxUR7RIZ/lzduQMSYfWEM4DDBu6A+XGbkFig== - dependencies: - "@babel/types" "^7.1.2" - jsesc "^2.5.1" - lodash "^4.17.10" - source-map "^0.5.0" - trim-right "^1.0.1" - -"@babel/generator@^7.2.2": +"@babel/generator@^7.0.0", "@babel/generator@^7.2.2": version "7.2.2" resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.2.2.tgz#18c816c70962640eab42fe8cae5f3947a5c65ccc" integrity sha512-I4o675J/iS8k+P38dvJ3IBGqObLXyQLTxtrR4u9cSUJOURvafeEWb/pFMOTwtNrmq73mJzyF6ueTbO1BtN0Zeg== @@ -75,10 +64,10 @@ "@babel/traverse" "^7.1.0" "@babel/types" "^7.0.0" -"@babel/helper-create-class-features-plugin@^7.2.3": - version "7.2.3" - resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.2.3.tgz#f6e719abb90cb7f4a69591e35fd5eb89047c4a7c" - integrity sha512-xO/3Gn+2C7/eOUeb0VRnSP1+yvWHNxlpAot1eMhtoKDCN7POsyQP5excuT5UsV5daHxMWBeIIOeI5cmB8vMRgQ== +"@babel/helper-create-class-features-plugin@^7.3.0": + version "7.3.0" + resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.3.0.tgz#2b01a81b3adc2b1287f9ee193688ef8dc71e718f" + integrity sha512-DUsQNS2CGLZZ7I3W3fvh0YpPDd6BuWJlDl+qmZZpABZHza2ErE3LxtEzLJFHFC1ZwtlAXvHhbFYbtM5o5B0WBw== dependencies: "@babel/helper-function-name" "^7.1.0" "@babel/helper-member-expression-to-functions" "^7.0.0" @@ -182,17 +171,7 @@ "@babel/traverse" "^7.1.0" "@babel/types" "^7.0.0" -"@babel/helper-replace-supers@^7.1.0": - version "7.1.0" - resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.1.0.tgz#5fc31de522ec0ef0899dc9b3e7cf6a5dd655f362" - integrity sha512-BvcDWYZRWVuDeXTYZWxekQNO5D4kO55aArwZOTFXw6rlLQA8ZaDicJR1sO47h+HrnCiDFiww0fSPV0d713KBGQ== - dependencies: - "@babel/helper-member-expression-to-functions" "^7.0.0" - "@babel/helper-optimise-call-expression" "^7.0.0" - "@babel/traverse" "^7.1.0" - "@babel/types" "^7.0.0" - -"@babel/helper-replace-supers@^7.2.3": +"@babel/helper-replace-supers@^7.1.0", "@babel/helper-replace-supers@^7.2.3": version "7.2.3" resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.2.3.tgz#19970020cf22677d62b3a689561dbd9644d8c5e5" integrity sha512-GyieIznGUfPXPWu0yLS6U55Mz67AZD9cUk0BfirOWlPrXlBcan9Gz+vHGz+cPfuoweZSnPzPIm67VtQM0OWZbA== @@ -245,12 +224,7 @@ esutils "^2.0.2" js-tokens "^4.0.0" -"@babel/parser@^7.0.0", "@babel/parser@^7.1.0", "@babel/parser@^7.1.2": - version "7.1.2" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.1.2.tgz#85c5c47af6d244fab77bce6b9bd830e38c978409" - integrity sha512-x5HFsW+E/nQalGMw7hu+fvPqnBeBaIr0lWJ2SG0PPL2j+Pm9lYvCrsZJGIgauPIENx0v10INIyFjmSNUD/gSqQ== - -"@babel/parser@^7.2.2", "@babel/parser@^7.2.3": +"@babel/parser@^7.0.0", "@babel/parser@^7.2.2", "@babel/parser@^7.2.3": version "7.2.3" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.2.3.tgz#32f5df65744b70888d17872ec106b02434ba1489" integrity sha512-0LyEcVlfCoFmci8mXx8A5oIkpkOgyo8dRHtxBnK9RRBwxO2+JZPNsqtVEZQ7mJFPxnXF9lfmU24mHOPI0qnlkA== @@ -264,12 +238,12 @@ "@babel/helper-remap-async-to-generator" "^7.1.0" "@babel/plugin-syntax-async-generators" "^7.2.0" -"@babel/plugin-proposal-class-properties@^7.2.3": - version "7.2.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.2.3.tgz#c9e1294363b346cff333007a92080f3203698461" - integrity sha512-FVuQngLoN2iDrpW7LmhPZ2sO4DJxf35FOcwidwB9Ru9tMvI5URthnkVHuG14IStV+TzkMTyLMoOUlSTtrdVwqw== +"@babel/plugin-proposal-class-properties@^7.3.0": + version "7.3.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.3.0.tgz#272636bc0fa19a0bc46e601ec78136a173ea36cd" + integrity sha512-wNHxLkEKTQ2ay0tnsam2z7fGZUi+05ziDJflEt3AZTP3oXLKHJp9HqhfroB/vdMvt3sda9fAbq7FsG8QPDrZBg== dependencies: - "@babel/helper-create-class-features-plugin" "^7.2.3" + "@babel/helper-create-class-features-plugin" "^7.3.0" "@babel/helper-plugin-utils" "^7.0.0" "@babel/plugin-proposal-json-strings@^7.2.0": @@ -280,10 +254,10 @@ "@babel/helper-plugin-utils" "^7.0.0" "@babel/plugin-syntax-json-strings" "^7.2.0" -"@babel/plugin-proposal-object-rest-spread@^7.2.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.2.0.tgz#88f5fec3e7ad019014c97f7ee3c992f0adbf7fb8" - integrity sha512-1L5mWLSvR76XYUQJXkd/EEQgjq8HHRP6lQuZTTg0VA4tTGPpGemmCdAfQIz1rzEuWAm+ecP8PyyEm30jC1eQCg== +"@babel/plugin-proposal-object-rest-spread@^7.3.1": + version "7.3.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.3.1.tgz#f69fb6a1ea6a4e1c503994a91d9cf76f3c4b36e8" + integrity sha512-Nmmv1+3LqxJu/V5jU9vJmxR/KIRWFk2qLHmbB56yRRRFhlaSuOVXscX3gUmhaKgUhzA3otOHVubbIEVYsZ0eZg== dependencies: "@babel/helper-plugin-utils" "^7.0.0" "@babel/plugin-syntax-object-rest-spread" "^7.2.0" @@ -296,12 +270,12 @@ "@babel/helper-plugin-utils" "^7.0.0" "@babel/plugin-syntax-optional-catch-binding" "^7.2.0" -"@babel/plugin-proposal-private-methods@^7.2.3": - version "7.2.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.2.3.tgz#aff0f5436df2c4365938c0309d551984e42c290c" - integrity sha512-jehrt1/TuLdLeBAVEv1VmTCNJcvSj+5Ozp7l21DN19Ylo0ATxpZ5bDk8i4WS9Ngvdgk/YTcqJCTp3uY2lwQoxw== +"@babel/plugin-proposal-private-methods@^7.3.0": + version "7.3.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.3.0.tgz#da373257a66525cb76544c37ab2ce4c611568841" + integrity sha512-j6luy/F0MX6kd71e9hz97my2tBXTa+czAz+sscJVCRmjB9e9g2D4JN+tyfcwMCXUM2afj/tYCjzNaxwWJ4SdYg== dependencies: - "@babel/helper-create-class-features-plugin" "^7.2.3" + "@babel/helper-create-class-features-plugin" "^7.3.0" "@babel/helper-plugin-utils" "^7.0.0" "@babel/plugin-proposal-unicode-property-regex@^7.2.0": @@ -493,6 +467,13 @@ "@babel/helper-module-transforms" "^7.1.0" "@babel/helper-plugin-utils" "^7.0.0" +"@babel/plugin-transform-named-capturing-groups-regex@^7.3.0": + version "7.3.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.3.0.tgz#140b52985b2d6ef0cb092ef3b29502b990f9cd50" + integrity sha512-NxIoNVhk9ZxS+9lSoAQ/LM0V2UEvARLttEHUrRDGKFaAxOYQcrkN/nLRE+BbbicCAvZPl7wMP0X60HsHE5DtQw== + dependencies: + regexp-tree "^0.1.0" + "@babel/plugin-transform-new-target@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.0.0.tgz#ae8fbd89517fa7892d20e6564e641e8770c3aa4a" @@ -570,19 +551,20 @@ "@babel/helper-regex" "^7.0.0" regexpu-core "^4.1.3" -"@babel/preset-env@^7.2.3": - version "7.2.3" - resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.2.3.tgz#948c8df4d4609c99c7e0130169f052ea6a7a8933" - integrity sha512-AuHzW7a9rbv5WXmvGaPX7wADxFkZIqKlbBh1dmZUQp4iwiPpkE/Qnrji6SC4UQCQzvWY/cpHET29eUhXS9cLPw== +"@babel/preset-env@^7.3.1": + version "7.3.1" + resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.3.1.tgz#389e8ca6b17ae67aaf9a2111665030be923515db" + integrity sha512-FHKrD6Dxf30e8xgHQO0zJZpUPfVZg+Xwgz5/RdSWCbza9QLNk4Qbp40ctRoqDxml3O8RMzB1DU55SXeDG6PqHQ== dependencies: "@babel/helper-module-imports" "^7.0.0" "@babel/helper-plugin-utils" "^7.0.0" "@babel/plugin-proposal-async-generator-functions" "^7.2.0" "@babel/plugin-proposal-json-strings" "^7.2.0" - "@babel/plugin-proposal-object-rest-spread" "^7.2.0" + "@babel/plugin-proposal-object-rest-spread" "^7.3.1" "@babel/plugin-proposal-optional-catch-binding" "^7.2.0" "@babel/plugin-proposal-unicode-property-regex" "^7.2.0" "@babel/plugin-syntax-async-generators" "^7.2.0" + "@babel/plugin-syntax-json-strings" "^7.2.0" "@babel/plugin-syntax-object-rest-spread" "^7.2.0" "@babel/plugin-syntax-optional-catch-binding" "^7.2.0" "@babel/plugin-transform-arrow-functions" "^7.2.0" @@ -602,6 +584,7 @@ "@babel/plugin-transform-modules-commonjs" "^7.2.0" "@babel/plugin-transform-modules-systemjs" "^7.2.0" "@babel/plugin-transform-modules-umd" "^7.2.0" + "@babel/plugin-transform-named-capturing-groups-regex" "^7.3.0" "@babel/plugin-transform-new-target" "^7.0.0" "@babel/plugin-transform-object-super" "^7.2.0" "@babel/plugin-transform-parameters" "^7.2.0" @@ -617,16 +600,7 @@ js-levenshtein "^1.1.3" semver "^5.3.0" -"@babel/template@^7.0.0", "@babel/template@^7.1.0", "@babel/template@^7.1.2": - version "7.1.2" - resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.1.2.tgz#090484a574fef5a2d2d7726a674eceda5c5b5644" - integrity sha512-SY1MmplssORfFiLDcOETrW7fCLl+PavlwMh92rrGcikQaRq4iWPVH0MpwPpY3etVMx6RnDjXtr6VZYr/IbP/Ag== - dependencies: - "@babel/code-frame" "^7.0.0" - "@babel/parser" "^7.1.2" - "@babel/types" "^7.1.2" - -"@babel/template@^7.2.2": +"@babel/template@^7.0.0", "@babel/template@^7.1.0", "@babel/template@^7.1.2", "@babel/template@^7.2.2": version "7.2.2" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.2.2.tgz#005b3fdf0ed96e88041330379e0da9a708eb2907" integrity sha512-zRL0IMM02AUDwghf5LMSSDEz7sBCO2YnNmpg3uWTZj/v1rcG2BmQUvaGU8GhU8BvfMh1k2KIAYZ7Ji9KXPUg7g== @@ -635,22 +609,7 @@ "@babel/parser" "^7.2.2" "@babel/types" "^7.2.2" -"@babel/traverse@^7.0.0", "@babel/traverse@^7.1.0": - version "7.1.0" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.1.0.tgz#503ec6669387efd182c3888c4eec07bcc45d91b2" - integrity sha512-bwgln0FsMoxm3pLOgrrnGaXk18sSM9JNf1/nHC/FksmNGFbYnPWY4GYCfLxyP1KRmfsxqkRpfoa6xr6VuuSxdw== - dependencies: - "@babel/code-frame" "^7.0.0" - "@babel/generator" "^7.0.0" - "@babel/helper-function-name" "^7.1.0" - "@babel/helper-split-export-declaration" "^7.0.0" - "@babel/parser" "^7.1.0" - "@babel/types" "^7.0.0" - debug "^3.1.0" - globals "^11.1.0" - lodash "^4.17.10" - -"@babel/traverse@^7.1.5", "@babel/traverse@^7.2.2", "@babel/traverse@^7.2.3": +"@babel/traverse@^7.0.0", "@babel/traverse@^7.1.0", "@babel/traverse@^7.1.5", "@babel/traverse@^7.2.2", "@babel/traverse@^7.2.3": version "7.2.3" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.2.3.tgz#7ff50cefa9c7c0bd2d81231fdac122f3957748d8" integrity sha512-Z31oUD/fJvEWVR0lNZtfgvVt512ForCTNKYcJBGbPb1QZfve4WGH8Wsy7+Mev33/45fhP/hwQtvgusNdcCMgSw== @@ -665,16 +624,7 @@ globals "^11.1.0" lodash "^4.17.10" -"@babel/types@^7.0.0", "@babel/types@^7.1.2": - version "7.1.2" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.1.2.tgz#183e7952cf6691628afdc2e2b90d03240bac80c0" - integrity sha512-pb1I05sZEKiSlMUV9UReaqsCPUpgbHHHu2n1piRm7JkuBkm6QxcaIzKu6FMnMtCbih/cEYTR+RGYYC96Yk9HAg== - dependencies: - esutils "^2.0.2" - lodash "^4.17.10" - to-fast-properties "^2.0.0" - -"@babel/types@^7.2.0", "@babel/types@^7.2.2": +"@babel/types@^7.0.0", "@babel/types@^7.2.0", "@babel/types@^7.2.2": version "7.2.2" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.2.2.tgz#44e10fc24e33af524488b716cdaee5360ea8ed1e" integrity sha512-fKCuD6UFUMkR541eDWL+2ih/xFZBXPOg/7EQFeTluMDebfqR4jrpaCjLhkWlQS4hT6nRa2PMEgXKbRB5/H2fpg== @@ -1654,6 +1604,11 @@ babel-types@^6.0.0, babel-types@^6.18.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.19: + version "7.0.0-beta.19" + resolved "https://registry.yarnpkg.com/babylon/-/babylon-7.0.0-beta.19.tgz#e928c7e807e970e0536b078ab3e0c48f9e052503" + integrity sha512-Vg0C9s/REX6/WIXN37UKpv5ZhRi6A4pjHlpkE34+8/a6c2W1Q692n3hmc+SZG5lKRnaExLUbxtJ1SVT+KaCQ/A== + babylon@^6.18.0: version "6.18.0" resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.18.0.tgz#af2f3b88fa6f5c1e4c634d1a0f8eac4f55b395e3" @@ -1746,7 +1701,7 @@ blob@0.0.4: resolved "https://registry.yarnpkg.com/blob/-/blob-0.0.4.tgz#bcf13052ca54463f30f9fc7e95b9a47630a94921" integrity sha1-vPEwUspURj8w+fx+lbmkdjCpSSE= -bluebird@^3.1.1, bluebird@^3.3.0, bluebird@^3.5.1, bluebird@^3.5.3: +bluebird@^3.1.1, bluebird@^3.3.0, bluebird@^3.5.1, bluebird@^3.5.3, bluebird@~3.5.0: version "3.5.3" resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.3.tgz#7d01c6f9616c9a51ab0f8c549a79dfe6ec33efa7" integrity sha512-/qKPUQlaW1OyR51WeCPBvRnAlnZFUJkCSG5HzGnuIqhgyJtF+T94lFnn33eiazjRm2LAHVy2guNnaq48X9SJuw== @@ -2099,6 +2054,13 @@ caseless@~0.12.0: resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= +catharsis@~0.8.9: + version "0.8.9" + resolved "https://registry.yarnpkg.com/catharsis/-/catharsis-0.8.9.tgz#98cc890ca652dd2ef0e70b37925310ff9e90fc8b" + integrity sha1-mMyJDKZS3S7w5ws3klMQ/56Q/Is= + dependencies: + underscore-contrib "~0.3.0" + chalk@1.1.3, chalk@^1.0.0, chalk@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" @@ -2226,6 +2188,16 @@ cli-cursor@^2.1.0: dependencies: restore-cursor "^2.0.0" +cli-table3@^0.5.0: + version "0.5.1" + resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.5.1.tgz#0252372d94dfc40dbd8df06005f48f31f656f202" + integrity sha512-7Qg2Jrep1S/+Q3EceiZtQcDPWxhAvBw+ERf1162v4sikJrvojMHFqXt8QIVha8UlH9rgU0BeWPytZ9/TzYqlUw== + dependencies: + object-assign "^4.1.0" + string-width "^2.1.1" + optionalDependencies: + colors "^1.1.2" + cli-width@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.1.0.tgz#b234ca209b29ef66fc518d9b98d5847b00edf00a" @@ -2315,6 +2287,11 @@ colors@^1.1.0: resolved "https://registry.yarnpkg.com/colors/-/colors-1.1.2.tgz#168a4701756b6a7f51a12ce0c97bfa28c084ed63" integrity sha1-FopHAXVran9RoSzgyXv6KMCE7WM= +colors@^1.1.2: + version "1.3.3" + resolved "https://registry.yarnpkg.com/colors/-/colors-1.3.3.tgz#39e005d546afe01e01f9c4ca8fa50f686a01205d" + integrity sha512-mmGt/1pZqYRjMxB1axhTo16/snVZ5krrKkcmMeVKxzECMMXoCgnvTPp10QgHfcbQZw8Dq2jMNG6je4JlWU0gWg== + combine-lists@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/combine-lists/-/combine-lists-1.0.1.tgz#458c07e09e0d900fc28b70a3fec2dacd1d2cb7f6" @@ -2329,7 +2306,7 @@ combined-stream@^1.0.6, combined-stream@~1.0.6: dependencies: delayed-stream "~1.0.0" -commander@2, commander@^2.10.0, commander@^2.18.0, commander@^2.19.0: +commander@2, commander@^2.10.0, commander@^2.16.0, commander@^2.18.0, commander@^2.19.0: version "2.19.0" resolved "https://registry.yarnpkg.com/commander/-/commander-2.19.0.tgz#f6198aa84e5b83c46054b94ddedbfed5ee9ff12a" integrity sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg== @@ -3246,6 +3223,11 @@ dns-txt@^2.0.2: dependencies: buffer-indexof "^1.0.0" +docdash@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/docdash/-/docdash-1.0.2.tgz#0449a8f6bb247f563020b78a5485dea95ae2e094" + integrity sha512-IEM57bWPLtVXpUeCKbiGvHsHtW9O9ZiiBPfeQDAZ7JdQiAF3aNWQoJ3e/+uJ63lHO009yn0tbJjtKpXJ2AURCQ== + doctrine@1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-1.5.0.tgz#379dce730f6166f76cefa4e6707a159b02c5a6fa" @@ -3554,7 +3536,7 @@ escape-html@~1.0.3: resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg= -escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: +escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5, escape-string-regexp@~1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= @@ -4639,7 +4621,7 @@ got@^8.0.3: url-parse-lax "^3.0.0" url-to-options "^1.0.1" -graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2: +graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.9: version "4.1.15" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.15.tgz#ffb703e1066e8a0eeaa4c8b80ba9253eeefbfb00" integrity sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA== @@ -6128,11 +6110,41 @@ js-yaml@3.x, js-yaml@^3.12.0, js-yaml@^3.7.0: argparse "^1.0.7" esprima "^4.0.0" +js2xmlparser@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/js2xmlparser/-/js2xmlparser-3.0.0.tgz#3fb60eaa089c5440f9319f51760ccd07e2499733" + integrity sha1-P7YOqgicVED5MZ9RdgzNB+JJlzM= + dependencies: + xmlcreate "^1.0.1" + jsbn@~0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM= +jsdoc-vue@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/jsdoc-vue/-/jsdoc-vue-1.0.0.tgz#ff3ac1ba6bc4a74079bb79058a7bf0066e346235" + integrity sha1-/zrBumvEp0B5u3kFinvwBm40YjU= + +jsdoc@^3.5.5: + version "3.5.5" + resolved "https://registry.yarnpkg.com/jsdoc/-/jsdoc-3.5.5.tgz#484521b126e81904d632ff83ec9aaa096708fa4d" + integrity sha512-6PxB65TAU4WO0Wzyr/4/YhlGovXl0EVYfpKbpSroSj0qBxT4/xod/l40Opkm38dRHRdQgdeY836M0uVnJQG7kg== + dependencies: + babylon "7.0.0-beta.19" + bluebird "~3.5.0" + catharsis "~0.8.9" + escape-string-regexp "~1.0.5" + js2xmlparser "~3.0.0" + klaw "~2.0.0" + marked "~0.3.6" + mkdirp "~0.5.1" + requizzle "~0.2.1" + strip-json-comments "~2.0.1" + taffydb "2.6.2" + underscore "~1.8.3" + jsdom@^11.5.1: version "11.12.0" resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-11.12.0.tgz#1a80d40ddd378a1de59656e9e6dc5a3ba8657bc8" @@ -6342,12 +6354,12 @@ karma@^3.0.0: tmp "0.0.33" useragent "2.2.1" -katex@^0.9.0: - version "0.9.0" - resolved "https://registry.yarnpkg.com/katex/-/katex-0.9.0.tgz#26a7d082c21d53725422d2d71da9b2d8455fbd4a" - integrity sha512-lp3x90LT1tDZBW2tjLheJ98wmRMRjUHwk4QpaswT9bhqoQZ+XA4cPcjcQBxgOQNwaOSt6ZeL/a6GKQ1of3LFxQ== +katex@^0.10.0: + version "0.10.0" + resolved "https://registry.yarnpkg.com/katex/-/katex-0.10.0.tgz#da562e5d0d5cc3aa602e27af8a9b8710bfbce765" + integrity sha512-/WRvx+L1eVBrLwX7QzKU1dQuaGnE7E8hDvx3VWfZh9HbMiCfsKWJNnYZ0S8ZMDAfAyDSofdyXIrH/hujF1fYXg== dependencies: - match-at "^0.1.1" + commander "^2.16.0" keyv@3.0.0: version "3.0.0" @@ -6385,6 +6397,13 @@ kind-of@^6.0.0, kind-of@^6.0.2: resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.2.tgz#01146b36a6218e64e58f3a8d66de5d7fc6f6d051" integrity sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA== +klaw@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/klaw/-/klaw-2.0.0.tgz#59c128e0dc5ce410201151194eeb9cbf858650f6" + integrity sha1-WcEo4Nxc5BAgEVEZTuucv4WGUPY= + dependencies: + graceful-fs "^4.1.9" + kleur@^2.0.1: version "2.0.2" resolved "https://registry.yarnpkg.com/kleur/-/kleur-2.0.2.tgz#b704f4944d95e255d038f0cb05fb8a602c55a300" @@ -6672,15 +6691,10 @@ map-visit@^1.0.0: dependencies: object-visit "^1.0.0" -marked@^0.3.12: - version "0.3.12" - resolved "https://registry.yarnpkg.com/marked/-/marked-0.3.12.tgz#7cf25ff2252632f3fe2406bde258e94eee927519" - integrity sha512-k4NaW+vS7ytQn6MgJn3fYpQt20/mOgYM5Ft9BYMfQJDz2QT6yEeS9XJ8k2Nw8JTeWK/znPPW2n3UJGzyYEiMoA== - -match-at@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/match-at/-/match-at-0.1.1.tgz#25d040d291777704d5e6556bbb79230ec2de0540" - integrity sha512-h4Yd392z9mST+dzc+yjuybOGFNOZjmXIPKWjxBd1Bb23r4SmDOsk2NYCU2BMUBGbSpZqwVsZYNq26QS3xfaT3Q== +marked@^0.3.12, marked@~0.3.6: + version "0.3.19" + resolved "https://registry.yarnpkg.com/marked/-/marked-0.3.19.tgz#5d47f709c4c9fc3c216b6d46127280f40b39d790" + integrity sha512-ea2eGWOqNxPcXv8dyERdSr/6FmzvWwzjMxpfGB/sbMccXoct+xY+YukPD+QTUZwyvK7BZwcr4m21WBOW41pAkg== math-random@^1.0.1: version "1.0.1" @@ -6916,7 +6930,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.1, mkdirp@~0.5.0: +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 "http://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" integrity sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM= @@ -7745,6 +7759,13 @@ pinkie@^2.0.0: resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" integrity sha1-clVrgM+g1IqXToDnckjoDtT3+HA= +pixelmatch@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/pixelmatch/-/pixelmatch-4.0.2.tgz#8f47dcec5011b477b67db03c243bc1f3085e8854" + integrity sha1-j0fc7FARtHe2fbA8JDvB8wheiFQ= + dependencies: + pngjs "^3.0.0" + pkg-dir@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-1.0.0.tgz#7a4b508a8d5bb2d629d447056ff4e9c9314cf3d4" @@ -7776,6 +7797,11 @@ pn@^1.1.0: resolved "https://registry.yarnpkg.com/pn/-/pn-1.1.0.tgz#e2f4cef0e219f463c179ab37463e4e1ecdccbafb" integrity sha512-2qHaIQr2VLRFoxe2nASzsV6ef4yOOH+Fi9FBOVH6cqeSgUnoyySPZkxzLuzd+RYOQTRpROA0ztTMqxROKSb/nA== +pngjs@^3.0.0: + version "3.3.3" + resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-3.3.3.tgz#85173703bde3edac8998757b96e5821d0966a21b" + integrity sha512-1n3Z4p3IOxArEs1VRXnZ/RXdfEniAUS9jb68g58FIXMNkPJeZd+Qh4Uq7/e0LVxAQGos1eIUrqrt4FpjdnEd+Q== + pofile@^1: version "1.0.11" resolved "https://registry.yarnpkg.com/pofile/-/pofile-1.0.11.tgz#35aff58c17491d127a07336d5522ebc9df57c954" @@ -7879,10 +7905,10 @@ prettier@1.13.7: resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.13.7.tgz#850f3b8af784a49a6ea2d2eaa7ed1428a34b7281" integrity sha512-KIU72UmYPGk4MujZGYMFwinB7lOf2LsDNGSOC8ufevsrPLISrZbNJlWstRi3m0AMuszbH+EFSQ/r6w56RSPK6w== -prettier@1.15.2: - version "1.15.2" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.15.2.tgz#d31abe22afa4351efa14c7f8b94b58bb7452205e" - integrity sha512-YgPLFFA0CdKL4Eg2IHtUSjzj/BWgszDHiNQAe0VAIBse34148whfdzLagRL+QiKS+YfK5ftB6X4v/MBw8yCoug== +prettier@1.16.1: + version "1.16.1" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.16.1.tgz#534c2c9d7853f8845e5e078384e71973bd74089f" + integrity sha512-XXUITwIkGb3CPJ2hforHah/zTINRyie5006Jd2HKy2qz7snEJXl0KLfsJZW/wst9g6R2rFvqba3VpNYdu1hDcA== pretty-format@^23.6.0: version "23.6.0" @@ -8284,6 +8310,15 @@ regex-not@^1.0.0, regex-not@^1.0.2: extend-shallow "^3.0.2" safe-regex "^1.1.0" +regexp-tree@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/regexp-tree/-/regexp-tree-0.1.0.tgz#a56ad7746097888ea16457479029ec9345b96ab0" + integrity sha512-rHQv+tzu+0l3KS/ERabas1yK49ahNVxuH40WcPg53CzP5p8TgmmyBgHELLyJcvjhTD0e5ahSY6C76LbEVtr7cg== + dependencies: + cli-table3 "^0.5.0" + colors "^1.1.2" + yargs "^10.0.3" + regexpp@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-2.0.1.tgz#8d19d31cf632482b589049f8281f93dbcba4d07f" @@ -8441,6 +8476,13 @@ requires-port@1.x.x, requires-port@^1.0.0: resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8= +requizzle@~0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/requizzle/-/requizzle-0.2.1.tgz#6943c3530c4d9a7e46f1cddd51c158fc670cdbde" + integrity sha1-aUPDUwxNmn5G8c3dUcFY/GcM294= + dependencies: + underscore "~1.6.0" + resolve-cwd@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a" @@ -9329,6 +9371,11 @@ table@^5.0.2: slice-ansi "2.0.0" string-width "^2.1.1" +taffydb@2.6.2: + version "2.6.2" + resolved "https://registry.yarnpkg.com/taffydb/-/taffydb-2.6.2.tgz#7cbcb64b5a141b6a2efc2c5d2c67b4e150b2a268" + integrity sha1-fLy2S1oUG2ou/CxdLGe04VCyomg= + tapable@^0.1.8: version "0.1.10" resolved "https://registry.yarnpkg.com/tapable/-/tapable-0.1.10.tgz#29c35707c2b70e50d07482b5d202e8ed446dafd4" @@ -9651,11 +9698,28 @@ undefsafe@^2.0.2: dependencies: debug "^2.2.0" +underscore-contrib@~0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/underscore-contrib/-/underscore-contrib-0.3.0.tgz#665b66c24783f8fa2b18c9f8cbb0e2c7d48c26c7" + integrity sha1-ZltmwkeD+PorGMn4y7Dix9SMJsc= + dependencies: + underscore "1.6.0" + +underscore@1.6.0, underscore@~1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.6.0.tgz#8b38b10cacdef63337b8b24e4ff86d45aea529a8" + integrity sha1-izixDKze9jM3uLJOT/htRa6lKag= + underscore@^1.9.0: version "1.9.0" resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.9.0.tgz#31dbb314cfcc88f169cd3692d9149d81a00a73e4" integrity sha512-4IV1DSSxC1QK48j9ONFK1MoIAKKkbE8i7u55w2R6IqBqbT7A/iG7aZBCR2Bi8piF0Uz+i/MG1aeqLwl/5vqF+A== +underscore@~1.8.3: + version "1.8.3" + resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.8.3.tgz#4f3fb53b106e6097fcf9cb4109f2a5e9bdfa5022" + integrity sha1-Tz+1OxBuYJf8+ctBCfKl6b36UCI= + unicode-canonical-property-names-ecmascript@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz#2619800c4c825800efdd8343af7dd9933cbe2818" @@ -10353,6 +10417,11 @@ xmlbuilder@8.2.2: resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-8.2.2.tgz#69248673410b4ba42e1a6136551d2922335aa773" integrity sha1-aSSGc0ELS6QuGmE2VR0pIjNap3M= +xmlcreate@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/xmlcreate/-/xmlcreate-1.0.2.tgz#fa6bf762a60a413fb3dd8f4b03c5b269238d308f" + integrity sha1-+mv3YqYKQT+z3Y9LA8WyaSONMI8= + xmlhttprequest-ssl@~1.5.4: version "1.5.5" resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.5.tgz#c2876b06168aadc40e57d97e81191ac8f4398b3e" @@ -10413,6 +10482,13 @@ yargs-parser@^11.1.1: camelcase "^5.0.0" decamelize "^1.2.0" +yargs-parser@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-8.1.0.tgz#f1376a33b6629a5d063782944da732631e966950" + integrity sha512-yP+6QqN8BmrgW2ggLtTbdrOyBNSI7zBa4IykmiV5R1wl1JWNxQvWhMfMdmzIYtKU7oP3OOInY/tl2ov3BDjnJQ== + dependencies: + camelcase "^4.1.0" + yargs-parser@^9.0.2: version "9.0.2" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-9.0.2.tgz#9ccf6a43460fe4ed40a9bb68f48d43b8a68cc077" @@ -10438,6 +10514,24 @@ yargs@12.0.2: y18n "^3.2.1 || ^4.0.0" yargs-parser "^10.1.0" +yargs@^10.0.3: + version "10.1.2" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-10.1.2.tgz#454d074c2b16a51a43e2fb7807e4f9de69ccb5c5" + integrity sha512-ivSoxqBGYOqQVruxD35+EyCFDYNEFL/Uo6FcOnz+9xZdZzK0Zzw4r4KhbrME1Oo2gOggwJod2MnsdamSG7H9ig== + dependencies: + cliui "^4.0.0" + decamelize "^1.1.1" + find-up "^2.1.0" + get-caller-file "^1.0.1" + os-locale "^2.0.0" + require-directory "^2.1.1" + require-main-filename "^1.0.1" + set-blocking "^2.0.0" + string-width "^2.0.0" + which-module "^2.0.0" + y18n "^3.2.1" + yargs-parser "^8.1.0" + yargs@^11.0.0: version "11.1.0" resolved "https://registry.yarnpkg.com/yargs/-/yargs-11.1.0.tgz#90b869934ed6e871115ea2ff58b03f4724ed2d77" |