diff options
1692 files changed, 20572 insertions, 9406 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 6cbc8e21c76..16c56747711 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,4 +1,4 @@ -image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.5.3-golang-1.9-git-2.18-chrome-69.0-node-10.x-yarn-1.12-postgresql-9.6-graphicsmagick-1.3.29" +image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.5.3-golang-1.11-git-2.18-chrome-71.0-node-10.x-yarn-1.12-postgresql-9.6-graphicsmagick-1.3.29" .dedicated-runner: &dedicated-runner retry: 1 @@ -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: @@ -527,7 +546,7 @@ docs lint: script: - scripts/lint-doc.sh - scripts/lint-changelog-yaml - - mv doc/ /tmp/gitlab-docs/content/ + - mv doc/ /tmp/gitlab-docs/content/$DOCS_GITLAB_REPO_SUFFIX - cd /tmp/gitlab-docs # Build HTML from Markdown - bundle exec nanoc @@ -608,7 +627,7 @@ gitlab:setup-mysql: # Frontend-related jobs gitlab:assets:compile: <<: *dedicated-no-docs-pull-cache-job - image: dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.5.3-git-2.18-chrome-69.0-node-8.x-yarn-1.12-graphicsmagick-1.3.29-docker-18.06.1 + image: dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.5.3-git-2.18-chrome-71.0-node-8.x-yarn-1.12-graphicsmagick-1.3.29-docker-18.06.1 dependencies: [] services: - docker:stable-dind @@ -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,7 +822,6 @@ qa:selectors: - yarn install --frozen-lockfile --cache-folder .yarn-cache - date - yarn run webpack-prod - <<: *except-docs qa-frontend-node:8: <<: *qa-frontend-node @@ -931,6 +950,22 @@ 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 + API_TOKEN: "${GITLAB_BOT_MULTI_PROJECT_PIPELINE_POLLING_TOKEN}" + 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 @@ -945,15 +980,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 @@ -964,7 +998,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.md b/.gitlab/issue_templates/Coding style proposal.md index 1a3be44bea0..1a3be44bea0 100644 --- a/.gitlab/issue_templates/Add style proposal.md +++ b/.gitlab/issue_templates/Coding style proposal.md diff --git a/.gitlab/issue_templates/Feature proposal.md b/.gitlab/issue_templates/Feature proposal.md index 639a236631d..0b22c7bc26b 100644 --- a/.gitlab/issue_templates/Feature proposal.md +++ b/.gitlab/issue_templates/Feature proposal.md @@ -4,7 +4,30 @@ ### 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 a [persona](https://design.gitlab.com/research/personas) +listed below, if applicable, along with its [label](https://gitlab.com/groups/gitlab-org/-/labels?utf8=%E2%9C%93&subscribed=&search=persona%3A), +or define a specific company role, e.g. "Release Manager". + +Existing personas are: (copy relevant personas out of this comment, and delete any persona that does not apply) + +- Parker, Product Manager, https://design.gitlab.com/research/personas#persona-parker +/label ~"Persona: Product Manager" + +- Delaney, Development Team Lead, https://design.gitlab.com/research/personas#persona-delaney +/label ~"Persona: Development Team Lead" + +- Sasha, Software Developer, https://design.gitlab.com/research/personas#persona-sasha +/label ~"Persona: Software developer" + +- Devon, DevOps Engineer, https://design.gitlab.com/research/personas#persona-devon +/label ~"Persona: DevOps Engineer" + +- Sidney, Systems Administrator, https://design.gitlab.com/research/personas#persona-sidney +/label ~"Persona: Systems Administrator" + +- Sam, Security Analyst, https://design.gitlab.com/research/personas#persona-sam +/label ~"Persona: Security Analyst" +--> ### Further details @@ -12,12 +35,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..ae469d3b125 --- /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 | +|---------|----| +| 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 | +|---------|----| +| 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/.gitlab/issue_templates/Security developer workflow.md b/.gitlab/issue_templates/Security developer workflow.md index f9bf700f809..4bc4215d21b 100644 --- a/.gitlab/issue_templates/Security developer workflow.md +++ b/.gitlab/issue_templates/Security developer workflow.md @@ -3,20 +3,17 @@ Create this issue under https://dev.gitlab.org/gitlab/gitlabhq -Set the title to: `[Security] Description of the original issue` +Set the title to: `Description of the original issue` --> -### Prior to the security release +### Prior to starting the security release work - [ ] Read the [security process for developers] if you are not familiar with it. - [ ] Link to the original issue adding it to the [links section](#links) - [ ] Run `scripts/security-harness` in the CE, EE, and/or Omnibus to prevent pushing to any remote besides `dev.gitlab.org` -- [ ] Create an MR targetting `org` `master`, prefixing your branch with `security-` -- [ ] Label your MR with the ~security label, prefix the title with `WIP: [master]` -- [ ] Add a link to the MR to the [links section](#links) -- [ ] Add a link to an EE MR if required -- [ ] Make sure the MR remains in-progress and gets approved after the review cycle, **but never merged**. -- [ ] Add a link to this issue on the original security issue. +- [ ] Create a new branch prefixing it with `security-` +- [ ] Create a MR targeting `dev.gitlab.org` `master` +- [ ] Add a link to this issue in the original security issue on `gitlab.com`. #### Backports diff --git a/.gitlab/merge_request_templates/Security Release.md b/.gitlab/merge_request_templates/Security Release.md new file mode 100644 index 00000000000..d72b4eb1cb6 --- /dev/null +++ b/.gitlab/merge_request_templates/Security Release.md @@ -0,0 +1,28 @@ +<!-- +# README first! +This MR should be created on `dev.gitlab.org`. + +See [the general developer security release guidelines](https://gitlab.com/gitlab-org/release/docs/blob/master/general/security/developer.md). + +--> +## Related issues + +<!-- Mention the issue(s) this MR is related to --> + +## Author's checklist + +- [ ] Link to the developer security workflow issue on `dev.gitlab.org` +- [ ] MR targets `master` or `security-X-Y` for backports +- [ ] Milestone is set for the version this MR applies to +- [ ] Title of this MR is the same as for all backports +- [ ] A [CHANGELOG entry](https://docs.gitlab.com/ee/development/changelog.html) is added without a `merge_request` value, with `type` set to `security` +- [ ] Add a link to this MR in the `links` section of related issue +- [ ] Add a link to an EE MR if required +- [ ] Assign to a reviewer + +## Reviewers checklist + +- [ ] Correct milestone is applied and the title is matching across all backports +- [ ] Assigned to `@gitlab-release-tools-bot` with passing CI pipelines + +/label ~security ~"Merge into Security" 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..c42d11a860e 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -15,12 +15,6 @@ Capybara/CurrentPathExpectation: Layout/EmptyLinesAroundArguments: Enabled: false -# Offense count: 253 -# Cop supports --auto-correct. -# Configuration parameters: AllowForAlignment, ForceEqualSignAlignment. -Layout/ExtraSpacing: - Enabled: false - # Offense count: 83 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, IndentationWidth. @@ -443,11 +437,6 @@ Style/LineEndConcatenation: - 'spec/lib/gitlab/gfm/reference_rewriter_spec.rb' - 'spec/lib/gitlab/incoming_email_spec.rb' -# Offense count: 39 -# Cop supports --auto-correct. -Style/MethodCallWithoutArgsParentheses: - Enabled: false - # Offense count: 18 Style/MethodMissing: Enabled: false @@ -686,17 +675,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..4985c607d57 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,251 @@ documentation](doc/development/changelog.md) for instructions on adding your own entry. +## 11.7.2 (2019-01-29) + +### Fixed (1 change) + +- Fix uninitialized constant with GitLab Pages. + + +## 11.7.1 (2019-01-28) + +### Security (24 changes) + +- Make potentially malicious links more visible in the UI and scrub RTLO chars from links. !2770 +- Don't process MR refs for guests in the notes. !2771 +- Sanitize user full name to clean up any URL to prevent mail clients from auto-linking URLs. !2828 +- Fixed XSS content in KaTex links. +- Disallows unauthorized users from accessing the pipelines section. +- Verify that LFS upload requests are genuine. +- Extract GitLab Pages using RubyZip. +- Prevent awarding emojis to notes whose parent is not visible to user. +- Prevent unauthorized replies when discussion is locked or confidential. +- Disable git v2 protocol temporarily. +- Fix showing ci status for guest users when public pipline are not set. +- Fix contributed projects info still visible when user enable private profile. +- Add subresources removal to member destroy service. +- Add more LFS validations to prevent forgery. +- Use common error for unauthenticated users when creating issues. +- Fix slow regex in project reference pattern. +- Fix private user email being visible in push (and tag push) webhooks. +- Fix wiki access rights when external wiki is enabled. +- Group guests are no longer able to see merge requests they don't have access to at group level. +- Fix path disclosure on project import error. +- Restrict project import visibility based on its group. +- Expose CI/CD trigger token only to the trigger owner. +- Notify only users who can access the project on project move. +- Alias GitHub and BitBucket OAuth2 callback URLs. + + +## 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.8 (2019-01-30) + +- No changes. + +## 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) @@ -324,6 +569,33 @@ entry. - Enable Rubocop on lib/gitlab. (gfyoung) +## 11.5.8 (2019-01-28) + +### Security (21 changes) + +- Make potentially malicious links more visible in the UI and scrub RTLO chars from links. !2770 +- Don't process MR refs for guests in the notes. !2771 +- Fixed XSS content in KaTex links. +- Verify that LFS upload requests are genuine. +- Extract GitLab Pages using RubyZip. +- Prevent awarding emojis to notes whose parent is not visible to user. +- Prevent unauthorized replies when discussion is locked or confidential. +- Disable git v2 protocol temporarily. +- Fix showing ci status for guest users when public pipline are not set. +- Fix contributed projects info still visible when user enable private profile. +- Disallows unauthorized users from accessing the pipelines section. +- Add more LFS validations to prevent forgery. +- Use common error for unauthenticated users when creating issues. +- Fix slow regex in project reference pattern. +- Fix private user email being visible in push (and tag push) webhooks. +- Fix wiki access rights when external wiki is enabled. +- Fix path disclosure on project import error. +- Restrict project import visibility based on its group. +- Expose CI/CD trigger token only to the trigger owner. +- Notify only users who can access the project on project move. +- Alias GitHub and BitBucket OAuth2 callback URLs. + + ## 11.5.5 (2018-12-20) ### Security (1 change) diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index 0eed1a29efd..63e799cf451 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -1.12.0 +1.14.1 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..0e79152459e 100644 --- a/GITLAB_WORKHORSE_VERSION +++ b/GITLAB_WORKHORSE_VERSION @@ -1 +1 @@ -8.0.0 +8.1.1 @@ -57,6 +57,7 @@ gem 'u2f', '~> 0.2.1' # GitLab Pages gem 'validates_hostname', '~> 1.0.6' +gem 'rubyzip', '~> 1.2.2', require: 'zip' # Browser detection gem 'browser', '~> 2.5' @@ -125,9 +126,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 +161,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' @@ -224,7 +225,7 @@ gem 'asana', '~> 0.8.1' gem 'ruby-fogbugz', '~> 0.2.1' # Kubernetes integration -gem 'kubeclient', '~> 4.0.0' +gem 'kubeclient', '~> 4.2.2' # Sanitize user input gem 'sanitize', '~> 4.6' @@ -304,6 +305,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 5098c6fb88e..1c28176ac62 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) @@ -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 @@ -419,7 +425,7 @@ GEM kgio (2.10.0) knapsack (1.17.0) rake - kubeclient (4.0.0) + kubeclient (4.2.2) http (~> 3.0) recursive-open-struct (~> 1.0, >= 1.0.4) rest-client (~> 2.0) @@ -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) @@ -618,7 +627,7 @@ GEM httpclient (>= 2.4) multi_json (>= 1.3.6) rack (>= 1.1) - rack-protection (2.0.4) + rack-protection (2.0.5) rack rack-proxy (0.6.0) rack @@ -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) @@ -816,12 +823,13 @@ GEM rack shoulda-matchers (3.1.2) activesupport (>= 4.0.0) - sidekiq (5.2.3) + sidekiq (5.2.5) connection_pool (~> 2.2, >= 2.2.2) + rack (>= 1.5.0) 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 +876,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 +884,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 +1011,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 +1046,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) @@ -1044,7 +1054,7 @@ DEPENDENCIES jwt (~> 2.1.0) kaminari (~> 1.0) knapsack (~> 1.17) - kubeclient (~> 4.0.0) + kubeclient (~> 4.2.2) letter_opener_web (~> 1.3.0) license_finder (~> 5.4) licensee (~> 8.9) @@ -1059,7 +1069,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 +1087,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 +1138,7 @@ DEPENDENCIES ruby-prof (~> 0.17.0) ruby-progressbar ruby_parser (~> 3.8) - rufus-scheduler (~> 3.4) + rubyzip (~> 1.2.2) rugged (~> 0.27) sanitize (~> 4.6) sass (~> 3.5) @@ -1141,7 +1152,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 +1167,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) @@ -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/behaviors/markdown/copy_as_gfm.js b/app/assets/javascripts/behaviors/markdown/copy_as_gfm.js index fe02096d903..947d019c725 100644 --- a/app/assets/javascripts/behaviors/markdown/copy_as_gfm.js +++ b/app/assets/javascripts/behaviors/markdown/copy_as_gfm.js @@ -1,320 +1,8 @@ -/* eslint-disable object-shorthand, no-unused-vars, no-use-before-define, no-restricted-syntax, guard-for-in, no-continue */ - import $ from 'jquery'; -import _ from 'underscore'; -import { insertText, getSelectedFragment, nodeMatchesSelector } from '~/lib/utils/common_utils'; -import { placeholderImage } from '~/lazy_loader'; - -const gfmRules = { - // The filters referenced in lib/banzai/pipeline/gfm_pipeline.rb convert - // GitLab Flavored Markdown (GFM) to HTML. - // These handlers consequently convert that same HTML to GFM to be copied to the clipboard. - // Every filter in lib/banzai/pipeline/gfm_pipeline.rb that generates HTML - // from GFM should have a handler here, in reverse order. - // The GFM-to-HTML-to-GFM cycle is tested in spec/features/copy_as_gfm_spec.rb. - InlineDiffFilter: { - 'span.idiff.addition'(el, text) { - return `{+${text}+}`; - }, - 'span.idiff.deletion'(el, text) { - return `{-${text}-}`; - }, - }, - TaskListFilter: { - 'input[type=checkbox].task-list-item-checkbox'(el) { - return `[${el.checked ? 'x' : ' '}]`; - }, - }, - ReferenceFilter: { - '.tooltip'(el) { - return ''; - }, - 'a.gfm:not([data-link=true])'(el, text) { - return el.dataset.original || text; - }, - }, - AutolinkFilter: { - a(el, text) { - // Fallback on the regular MarkdownFilter's `a` handler. - if (text !== el.getAttribute('href')) return false; - - return text; - }, - }, - TableOfContentsFilter: { - 'ul.section-nav'(el) { - return '[[_TOC_]]'; - }, - }, - EmojiFilter: { - 'img.emoji'(el) { - return el.getAttribute('alt'); - }, - 'gl-emoji'(el) { - return `:${el.getAttribute('data-name')}:`; - }, - }, - ImageLinkFilter: { - 'a.no-attachment-icon'(el, text) { - return text; - }, - }, - ImageLazyLoadFilter: { - img(el, text) { - return `![${el.getAttribute('alt')}](${el.getAttribute('src')})`; - }, - }, - VideoLinkFilter: { - '.video-container'(el) { - const videoEl = el.querySelector('video'); - if (!videoEl) return false; - - return CopyAsGFM.nodeToGFM(videoEl); - }, - video(el) { - return `![${el.dataset.title}](${el.getAttribute('src')})`; - }, - }, - MermaidFilter: { - 'svg.mermaid'(el, text) { - const sourceEl = el.querySelector('text.source'); - if (!sourceEl) return false; - - return `\`\`\`mermaid\n${CopyAsGFM.nodeToGFM(sourceEl)}\n\`\`\``; - }, - 'svg.mermaid style, svg.mermaid g'(el, text) { - // We don't want to include the content of these elements in the copied text. - return ''; - }, - }, - MathFilter: { - 'pre.code.math[data-math-style=display]'(el, text) { - return `\`\`\`math\n${text.trim()}\n\`\`\``; - }, - 'code.code.math[data-math-style=inline]'(el, text) { - return `$\`${text}\`$`; - }, - 'span.katex-display span.katex-mathml'(el) { - const mathAnnotation = el.querySelector('annotation[encoding="application/x-tex"]'); - if (!mathAnnotation) return false; - - return `\`\`\`math\n${CopyAsGFM.nodeToGFM(mathAnnotation)}\n\`\`\``; - }, - 'span.katex-mathml'(el) { - const mathAnnotation = el.querySelector('annotation[encoding="application/x-tex"]'); - if (!mathAnnotation) return false; - - return `$\`${CopyAsGFM.nodeToGFM(mathAnnotation)}\`$`; - }, - 'span.katex-html'(el) { - // We don't want to include the content of this element in the copied text. - return ''; - }, - 'annotation[encoding="application/x-tex"]'(el, text) { - return text.trim(); - }, - }, - SanitizationFilter: { - 'a[name]:not([href]):empty'(el) { - return el.outerHTML; - }, - dl(el, text) { - let lines = text - .replace(/\n\n/g, '\n') - .trim() - .split('\n'); - // Add two spaces to the front of subsequent list items lines, - // or leave the line entirely blank. - lines = lines.map(l => { - const line = l.trim(); - if (line.length === 0) return ''; - - return ` ${line}`; - }); - - return `<dl>\n${lines.join('\n')}\n</dl>\n`; - }, - 'dt, dd, summary, details'(el, text) { - const tag = el.nodeName.toLowerCase(); - return `<${tag}>${text}</${tag}>\n`; - }, - 'sup, sub, kbd, q, samp, var, ruby, rt, rp, abbr'(el, text) { - const tag = el.nodeName.toLowerCase(); - return `<${tag}>${text}</${tag}>`; - }, - }, - SyntaxHighlightFilter: { - 'pre.code.highlight'(el, t) { - const text = t.trimRight(); - - let lang = el.getAttribute('lang'); - if (!lang || lang === 'plaintext') { - lang = ''; - } - - // Prefixes lines with 4 spaces if the code contains triple backticks - if (lang === '' && text.match(/^```/gm)) { - return text - .split('\n') - .map(l => { - const line = l.trim(); - if (line.length === 0) return ''; - - return ` ${line}`; - }) - .join('\n'); - } - - return `\`\`\`${lang}\n${text}\n\`\`\``; - }, - 'pre > code'(el, text) { - // Don't wrap code blocks in `` - return text; - }, - }, - MarkdownFilter: { - br(el) { - // Two spaces at the end of a line are turned into a BR - return ' '; - }, - code(el, text) { - let backtickCount = 1; - const backtickMatch = text.match(/`+/); - if (backtickMatch) { - backtickCount = backtickMatch[0].length + 1; - } - - const backticks = Array(backtickCount + 1).join('`'); - const spaceOrNoSpace = backtickCount > 1 ? ' ' : ''; - - return backticks + spaceOrNoSpace + text.trim() + spaceOrNoSpace + backticks; - }, - blockquote(el, text) { - return text - .trim() - .split('\n') - .map(s => `> ${s}`.trim()) - .join('\n'); - }, - img(el) { - const imageSrc = el.src; - const imageUrl = imageSrc && imageSrc !== placeholderImage ? imageSrc : el.dataset.src || ''; - return `![${el.getAttribute('alt')}](${imageUrl})`; - }, - 'a.anchor'(el, text) { - // Don't render a Markdown link for the anchor link inside a heading - return text; - }, - a(el, text) { - return `[${text}](${el.getAttribute('href')})`; - }, - li(el, text) { - const lines = text.trim().split('\n'); - const firstLine = `- ${lines.shift()}`; - // Add four spaces to the front of subsequent list items lines, - // or leave the line entirely blank. - const nextLines = lines.map(s => { - if (s.trim().length === 0) return ''; - - return ` ${s}`; - }); - - return `${firstLine}\n${nextLines.join('\n')}`; - }, - ul(el, text) { - return text; - }, - ol(el, text) { - // LIs get a `- ` prefix by default, which we replace by `1. ` for ordered lists. - return text.replace(/^- /gm, '1. '); - }, - h1(el, text) { - return `# ${text.trim()}\n`; - }, - h2(el, text) { - return `## ${text.trim()}\n`; - }, - h3(el, text) { - return `### ${text.trim()}\n`; - }, - h4(el, text) { - return `#### ${text.trim()}\n`; - }, - h5(el, text) { - return `##### ${text.trim()}\n`; - }, - h6(el, text) { - return `###### ${text.trim()}\n`; - }, - strong(el, text) { - return `**${text}**`; - }, - em(el, text) { - return `_${text}_`; - }, - del(el, text) { - return `~~${text}~~`; - }, - hr(el) { - // extra leading \n is to ensure that there is a blank line between - // a list followed by an hr, otherwise this breaks old redcarpet rendering - return '\n-----\n'; - }, - p(el, text) { - return `${text.trim()}\n`; - }, - table(el) { - const theadEl = el.querySelector('thead'); - const tbodyEl = el.querySelector('tbody'); - if (!theadEl || !tbodyEl) return false; - - const theadText = CopyAsGFM.nodeToGFM(theadEl); - const tbodyText = CopyAsGFM.nodeToGFM(tbodyEl); - - return [theadText, tbodyText].join('\n'); - }, - thead(el, text) { - const cells = _.map(el.querySelectorAll('th'), cell => { - let chars = CopyAsGFM.nodeToGFM(cell).length + 2; - - let before = ''; - let after = ''; - const alignment = cell.align || cell.style.textAlign; - - switch (alignment) { - case 'center': - before = ':'; - after = ':'; - chars -= 2; - break; - case 'right': - after = ':'; - chars -= 1; - break; - default: - break; - } - - chars = Math.max(chars, 3); - - const middle = Array(chars + 1).join('-'); - - return before + middle + after; - }); - - const separatorRow = `|${cells.join('|')}|`; - - return [text, separatorRow].join('\n'); - }, - tr(el) { - const cellEls = el.querySelectorAll('td, th'); - if (cellEls.length === 0) return false; - - const cells = _.map(cellEls, cell => CopyAsGFM.nodeToGFM(cell)); - return `| ${cells.join(' | ')} |`; - }, - }, -}; +import { DOMParser } from 'prosemirror-model'; +import { getSelectedFragment } from '~/lib/utils/common_utils'; +import schema from './schema'; +import markdownSerializer from './serializer'; export class CopyAsGFM { constructor() { @@ -347,8 +35,13 @@ export class CopyAsGFM { e.preventDefault(); e.stopPropagation(); + const div = document.createElement('div'); + div.appendChild(el.cloneNode(true)); + const html = div.innerHTML; + clipboardData.setData('text/plain', el.textContent); clipboardData.setData('text/x-gfm', this.nodeToGFM(el)); + clipboardData.setData('text/html', html); } static pasteGFM(e) { @@ -361,7 +54,7 @@ export class CopyAsGFM { e.preventDefault(); - window.gl.utils.insertText(e.target, (textBefore, textAfter) => { + window.gl.utils.insertText(e.target, textBefore => { // If the text before the cursor contains an odd number of backticks, // we are either inside an inline code span that starts with 1 backtick // or a code block that starts with 3 backticks. @@ -443,75 +136,12 @@ export class CopyAsGFM { return codeElement; } - static nodeToGFM(node, respectWhitespaceParam = false) { - if (node.nodeType === Node.COMMENT_NODE) { - return ''; - } - - if (node.nodeType === Node.TEXT_NODE) { - return node.textContent; - } - - const respectWhitespace = - respectWhitespaceParam || (node.nodeName === 'PRE' || node.nodeName === 'CODE'); - - const text = this.innerGFM(node, respectWhitespace); - - if (node.nodeType === Node.DOCUMENT_FRAGMENT_NODE) { - return text; - } - - for (const filter in gfmRules) { - const rules = gfmRules[filter]; - - for (const selector in rules) { - const func = rules[selector]; - - if (!nodeMatchesSelector(node, selector)) continue; - - let result; - if (func.length === 2) { - // if `func` takes 2 arguments, it depends on text. - // if there is no text, we don't need to generate GFM for this node. - if (text.length === 0) continue; - - result = func(node, text); - } else { - result = func(node); - } - - if (result === false) continue; - - return result; - } - } - - return text; - } - - static innerGFM(parentNode, respectWhitespace = false) { - const nodes = parentNode.childNodes; - - const clonedParentNode = parentNode.cloneNode(true); - const clonedNodes = Array.prototype.slice.call(clonedParentNode.childNodes, 0); - - for (let i = 0; i < nodes.length; i += 1) { - const node = nodes[i]; - const clonedNode = clonedNodes[i]; - - const text = this.nodeToGFM(node, respectWhitespace); - - // `clonedNode.replaceWith(text)` is not yet widely supported - clonedNode.parentNode.replaceChild(document.createTextNode(text), clonedNode); - } - - let nodeText = clonedParentNode.innerText || clonedParentNode.textContent; - - if (!respectWhitespace) { - nodeText = nodeText.trim(); - } + static nodeToGFM(node) { + const wrapEl = document.createElement('div'); + wrapEl.appendChild(node.cloneNode(true)); + const doc = DOMParser.fromSchema(schema).parse(wrapEl); - return nodeText; + return markdownSerializer.serialize(doc); } } diff --git a/app/assets/javascripts/behaviors/markdown/editor_extensions.js b/app/assets/javascripts/behaviors/markdown/editor_extensions.js new file mode 100644 index 00000000000..47e5fc65c48 --- /dev/null +++ b/app/assets/javascripts/behaviors/markdown/editor_extensions.js @@ -0,0 +1,106 @@ +import Doc from './nodes/doc'; +import Paragraph from './nodes/paragraph'; +import Text from './nodes/text'; + +import Blockquote from './nodes/blockquote'; +import CodeBlock from './nodes/code_block'; +import HardBreak from './nodes/hard_break'; +import Heading from './nodes/heading'; +import HorizontalRule from './nodes/horizontal_rule'; +import Image from './nodes/image'; + +import Table from './nodes/table'; +import TableHead from './nodes/table_head'; +import TableBody from './nodes/table_body'; +import TableHeaderRow from './nodes/table_header_row'; +import TableRow from './nodes/table_row'; +import TableCell from './nodes/table_cell'; + +import Emoji from './nodes/emoji'; +import Reference from './nodes/reference'; + +import TableOfContents from './nodes/table_of_contents'; +import Video from './nodes/video'; + +import BulletList from './nodes/bullet_list'; +import OrderedList from './nodes/ordered_list'; +import ListItem from './nodes/list_item'; + +import DescriptionList from './nodes/description_list'; +import DescriptionTerm from './nodes/description_term'; +import DescriptionDetails from './nodes/description_details'; + +import TaskList from './nodes/task_list'; +import OrderedTaskList from './nodes/ordered_task_list'; +import TaskListItem from './nodes/task_list_item'; + +import Summary from './nodes/summary'; +import Details from './nodes/details'; + +import Bold from './marks/bold'; +import Italic from './marks/italic'; +import Strike from './marks/strike'; +import InlineDiff from './marks/inline_diff'; + +import Link from './marks/link'; +import Code from './marks/code'; +import MathMark from './marks/math'; +import InlineHTML from './marks/inline_html'; + +// The filters referenced in lib/banzai/pipeline/gfm_pipeline.rb transform +// GitLab Flavored Markdown (GFM) to HTML. +// The nodes and marks referenced here transform that same HTML to GFM to be copied to the clipboard. +// Every filter in lib/banzai/pipeline/gfm_pipeline.rb that generates HTML +// from GFM should have a node or mark here. +// The GFM-to-HTML-to-GFM cycle is tested in spec/features/copy_as_gfm_spec.rb. + +export default [ + new Doc(), + new Paragraph(), + new Text(), + + new Blockquote(), + new CodeBlock(), + new HardBreak(), + new Heading({ maxLevel: 6 }), + new HorizontalRule(), + new Image(), + + new Table(), + new TableHead(), + new TableBody(), + new TableHeaderRow(), + new TableRow(), + new TableCell(), + + new Emoji(), + new Reference(), + + new TableOfContents(), + new Video(), + + new BulletList(), + new OrderedList(), + new ListItem(), + + new DescriptionList(), + new DescriptionTerm(), + new DescriptionDetails(), + + new TaskList(), + new OrderedTaskList(), + new TaskListItem(), + + new Summary(), + new Details(), + + new Bold(), + new Italic(), + new Strike(), + new InlineDiff(), + + new Link(), + new Code(), + new MathMark(), + new InlineHTML(), +]; diff --git a/app/assets/javascripts/behaviors/markdown/marks/bold.js b/app/assets/javascripts/behaviors/markdown/marks/bold.js new file mode 100644 index 00000000000..b537954c1cb --- /dev/null +++ b/app/assets/javascripts/behaviors/markdown/marks/bold.js @@ -0,0 +1,11 @@ +/* eslint-disable class-methods-use-this */ + +import { Bold as BaseBold } from 'tiptap-extensions'; +import { defaultMarkdownSerializer } from 'prosemirror-markdown'; + +// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter +export default class Bold extends BaseBold { + get toMarkdown() { + return defaultMarkdownSerializer.marks.strong; + } +} diff --git a/app/assets/javascripts/behaviors/markdown/marks/code.js b/app/assets/javascripts/behaviors/markdown/marks/code.js new file mode 100644 index 00000000000..a760ee80dd0 --- /dev/null +++ b/app/assets/javascripts/behaviors/markdown/marks/code.js @@ -0,0 +1,11 @@ +/* eslint-disable class-methods-use-this */ + +import { Code as BaseCode } from 'tiptap-extensions'; +import { defaultMarkdownSerializer } from 'prosemirror-markdown'; + +// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter +export default class Code extends BaseCode { + get toMarkdown() { + return defaultMarkdownSerializer.marks.code; + } +} diff --git a/app/assets/javascripts/behaviors/markdown/marks/inline_diff.js b/app/assets/javascripts/behaviors/markdown/marks/inline_diff.js new file mode 100644 index 00000000000..ce425e80cd3 --- /dev/null +++ b/app/assets/javascripts/behaviors/markdown/marks/inline_diff.js @@ -0,0 +1,41 @@ +/* eslint-disable class-methods-use-this */ + +import { Mark } from 'tiptap'; + +// Transforms generated HTML back to GFM for Banzai::Filter::InlineDiffFilter +export default class InlineDiff extends Mark { + get name() { + return 'inline_diff'; + } + + get schema() { + return { + attrs: { + addition: { + default: true, + }, + }, + parseDOM: [ + { tag: 'span.idiff.addition', attrs: { addition: true } }, + { tag: 'span.idiff.deletion', attrs: { addition: false } }, + ], + toDOM: node => [ + 'span', + { class: `idiff left right ${node.attrs.addition ? 'addition' : 'deletion'}` }, + 0, + ], + }; + } + + get toMarkdown() { + return { + mixable: true, + open(state, mark) { + return mark.attrs.addition ? '{+' : '{-'; + }, + close(state, mark) { + return mark.attrs.addition ? '+}' : '-}'; + }, + }; + } +} diff --git a/app/assets/javascripts/behaviors/markdown/marks/inline_html.js b/app/assets/javascripts/behaviors/markdown/marks/inline_html.js new file mode 100644 index 00000000000..ebed8698e21 --- /dev/null +++ b/app/assets/javascripts/behaviors/markdown/marks/inline_html.js @@ -0,0 +1,46 @@ +/* eslint-disable class-methods-use-this */ + +import { Mark } from 'tiptap'; +import _ from 'underscore'; + +// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter +export default class InlineHTML extends Mark { + get name() { + return 'inline_html'; + } + + get schema() { + return { + excludes: '', + attrs: { + tag: {}, + title: { default: null }, + }, + parseDOM: [ + { + tag: 'sup, sub, kbd, q, samp, var', + getAttrs: el => ({ tag: el.nodeName.toLowerCase() }), + }, + { + tag: 'abbr', + getAttrs: el => ({ tag: 'abbr', title: el.getAttribute('title') }), + }, + ], + toDOM: node => [node.attrs.tag, { title: node.attrs.title }, 0], + }; + } + + get toMarkdown() { + return { + mixable: true, + open(state, mark) { + return `<${mark.attrs.tag}${ + mark.attrs.title ? ` title="${state.esc(_.escape(mark.attrs.title))}"` : '' + }>`; + }, + close(state, mark) { + return `</${mark.attrs.tag}>`; + }, + }; + } +} diff --git a/app/assets/javascripts/behaviors/markdown/marks/italic.js b/app/assets/javascripts/behaviors/markdown/marks/italic.js new file mode 100644 index 00000000000..44b35c97739 --- /dev/null +++ b/app/assets/javascripts/behaviors/markdown/marks/italic.js @@ -0,0 +1,11 @@ +/* eslint-disable class-methods-use-this */ + +import { Italic as BaseItalic } from 'tiptap-extensions'; +import { defaultMarkdownSerializer } from 'prosemirror-markdown'; + +// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter +export default class Italic extends BaseItalic { + get toMarkdown() { + return defaultMarkdownSerializer.marks.em; + } +} diff --git a/app/assets/javascripts/behaviors/markdown/marks/link.js b/app/assets/javascripts/behaviors/markdown/marks/link.js new file mode 100644 index 00000000000..5c23d6a5ceb --- /dev/null +++ b/app/assets/javascripts/behaviors/markdown/marks/link.js @@ -0,0 +1,21 @@ +/* eslint-disable class-methods-use-this */ + +import { Link as BaseLink } from 'tiptap-extensions'; +import { defaultMarkdownSerializer } from 'prosemirror-markdown'; + +// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter +export default class Link extends BaseLink { + get toMarkdown() { + return { + mixable: true, + open(state, mark, parent, index) { + const open = defaultMarkdownSerializer.marks.link.open(state, mark, parent, index); + return open === '<' ? '' : open; + }, + close(state, mark, parent, index) { + const close = defaultMarkdownSerializer.marks.link.close(state, mark, parent, index); + return close === '>' ? '' : close; + }, + }; + } +} diff --git a/app/assets/javascripts/behaviors/markdown/marks/math.js b/app/assets/javascripts/behaviors/markdown/marks/math.js new file mode 100644 index 00000000000..e582fb18f15 --- /dev/null +++ b/app/assets/javascripts/behaviors/markdown/marks/math.js @@ -0,0 +1,41 @@ +/* eslint-disable class-methods-use-this */ + +import { Mark } from 'tiptap'; +import { defaultMarkdownSerializer } from 'prosemirror-markdown'; + +// Transforms generated HTML back to GFM for Banzai::Filter::MathFilter +export default class MathMark extends Mark { + get name() { + return 'math'; + } + + get schema() { + return { + parseDOM: [ + // Matches HTML generated by Banzai::Filter::MathFilter + { + tag: 'code.code.math[data-math-style=inline]', + priority: 51, + }, + // Matches HTML after being transformed by app/assets/javascripts/behaviors/markdown/render_math.js + { + tag: 'span.katex', + contentElement: 'annotation[encoding="application/x-tex"]', + }, + ], + toDOM: () => ['code', { class: 'code math', 'data-math-style': 'inline' }, 0], + }; + } + + get toMarkdown() { + return { + escape: false, + open(state, mark, parent, index) { + return `$${defaultMarkdownSerializer.marks.code.open(state, mark, parent, index)}`; + }, + close(state, mark, parent, index) { + return `${defaultMarkdownSerializer.marks.code.close(state, mark, parent, index)}$`; + }, + }; + } +} diff --git a/app/assets/javascripts/behaviors/markdown/marks/strike.js b/app/assets/javascripts/behaviors/markdown/marks/strike.js new file mode 100644 index 00000000000..c2951a40a4b --- /dev/null +++ b/app/assets/javascripts/behaviors/markdown/marks/strike.js @@ -0,0 +1,15 @@ +/* eslint-disable class-methods-use-this */ + +import { Strike as BaseStrike } from 'tiptap-extensions'; + +// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter +export default class Strike extends BaseStrike { + get toMarkdown() { + return { + open: '~~', + close: '~~', + mixable: true, + expelEnclosingWhitespace: true, + }; + } +} diff --git a/app/assets/javascripts/behaviors/markdown/nodes/blockquote.js b/app/assets/javascripts/behaviors/markdown/nodes/blockquote.js new file mode 100644 index 00000000000..b0bc8f79643 --- /dev/null +++ b/app/assets/javascripts/behaviors/markdown/nodes/blockquote.js @@ -0,0 +1,13 @@ +/* eslint-disable class-methods-use-this */ + +import { Blockquote as BaseBlockquote } from 'tiptap-extensions'; +import { defaultMarkdownSerializer } from 'prosemirror-markdown'; + +// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter +export default class Blockquote extends BaseBlockquote { + toMarkdown(state, node) { + if (!node.childCount) return; + + defaultMarkdownSerializer.nodes.blockquote(state, node); + } +} diff --git a/app/assets/javascripts/behaviors/markdown/nodes/bullet_list.js b/app/assets/javascripts/behaviors/markdown/nodes/bullet_list.js new file mode 100644 index 00000000000..3b0792e1af8 --- /dev/null +++ b/app/assets/javascripts/behaviors/markdown/nodes/bullet_list.js @@ -0,0 +1,11 @@ +/* eslint-disable class-methods-use-this */ + +import { BulletList as BaseBulletList } from 'tiptap-extensions'; +import { defaultMarkdownSerializer } from 'prosemirror-markdown'; + +// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter +export default class BulletList extends BaseBulletList { + toMarkdown(state, node) { + defaultMarkdownSerializer.nodes.bullet_list(state, node); + } +} diff --git a/app/assets/javascripts/behaviors/markdown/nodes/code_block.js b/app/assets/javascripts/behaviors/markdown/nodes/code_block.js new file mode 100644 index 00000000000..1e0c05eff08 --- /dev/null +++ b/app/assets/javascripts/behaviors/markdown/nodes/code_block.js @@ -0,0 +1,99 @@ +/* eslint-disable class-methods-use-this */ + +import { CodeBlock as BaseCodeBlock } from 'tiptap-extensions'; + +const PLAINTEXT_LANG = 'plaintext'; + +// Transforms generated HTML back to GFM for: +// - Banzai::Filter::SyntaxHighlightFilter +// - Banzai::Filter::MathFilter +// - Banzai::Filter::MermaidFilter +// - Banzai::Filter::SuggestionFilter +export default class CodeBlock extends BaseCodeBlock { + get schema() { + return { + content: 'text*', + marks: '', + group: 'block', + code: true, + defining: true, + attrs: { + lang: { default: PLAINTEXT_LANG }, + }, + parseDOM: [ + // Matches HTML generated by Banzai::Filter::SyntaxHighlightFilter, Banzai::Filter::MathFilter, Banzai::Filter::MermaidFilter, or Banzai::Filter::SuggestionFilter + { + tag: 'pre.code.highlight', + preserveWhitespace: 'full', + getAttrs: el => { + const lang = el.getAttribute('lang'); + if (!lang || lang === '') return {}; + + return { lang }; + }, + }, + // Matches HTML generated by Banzai::Filter::MathFilter, + // after being transformed by app/assets/javascripts/behaviors/markdown/render_math.js + { + tag: 'span.katex-display', + preserveWhitespace: 'full', + contentElement: 'annotation[encoding="application/x-tex"]', + attrs: { lang: 'math' }, + }, + // Matches HTML generated by Banzai::Filter::MermaidFilter, + // after being transformed by app/assets/javascripts/behaviors/markdown/render_mermaid.js + { + tag: 'svg.mermaid', + preserveWhitespace: 'full', + contentElement: 'text.source', + attrs: { lang: 'mermaid' }, + }, + // Matches HTML generated by Banzai::Filter::SuggestionFilter, + // after being transformed by app/assets/javascripts/vue_shared/components/markdown/suggestions.vue + { + tag: '.md-suggestion', + skip: true, + }, + { + tag: '.md-suggestion-header', + ignore: true, + }, + { + tag: '.md-suggestion-diff', + preserveWhitespace: 'full', + getContent: (el, schema) => + [...el.querySelectorAll('.line_content.new span')].map(span => + schema.text(span.innerText), + ), + attrs: { lang: 'suggestion' }, + }, + ], + toDOM: node => ['pre', { class: 'code highlight', lang: node.attrs.lang }, ['code', 0]], + }; + } + + toMarkdown(state, node) { + if (!node.childCount) return; + + const { + textContent: text, + attrs: { lang }, + } = node; + + // Prefixes lines with 4 spaces if the code contains a line that starts with triple backticks + if (lang === PLAINTEXT_LANG && text.match(/^```/gm)) { + state.wrapBlock(' ', null, node, () => state.text(text, false)); + return; + } + + state.write('```'); + if (lang !== PLAINTEXT_LANG) state.write(lang); + + state.ensureNewLine(); + state.text(text, false); + state.ensureNewLine(); + + state.write('```'); + state.closeBlock(node); + } +} diff --git a/app/assets/javascripts/behaviors/markdown/nodes/description_details.js b/app/assets/javascripts/behaviors/markdown/nodes/description_details.js new file mode 100644 index 00000000000..a4451d8ce8d --- /dev/null +++ b/app/assets/javascripts/behaviors/markdown/nodes/description_details.js @@ -0,0 +1,28 @@ +/* eslint-disable class-methods-use-this */ + +import { Node } from 'tiptap'; + +// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter +export default class DescriptionDetails extends Node { + get name() { + return 'description_details'; + } + + get schema() { + return { + content: 'text*', + marks: '', + defining: true, + parseDOM: [{ tag: 'dd' }], + toDOM: () => ['dd', 0], + }; + } + + toMarkdown(state, node) { + state.flushClose(1); + state.write('<dd>'); + state.text(node.textContent, false); + state.write('</dd>'); + state.closeBlock(node); + } +} diff --git a/app/assets/javascripts/behaviors/markdown/nodes/description_list.js b/app/assets/javascripts/behaviors/markdown/nodes/description_list.js new file mode 100644 index 00000000000..6aa1aca29d7 --- /dev/null +++ b/app/assets/javascripts/behaviors/markdown/nodes/description_list.js @@ -0,0 +1,28 @@ +/* eslint-disable class-methods-use-this */ + +import { Node } from 'tiptap'; + +// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter +export default class DescriptionList extends Node { + get name() { + return 'description_list'; + } + + get schema() { + return { + content: '(description_term+ description_details+)+', + group: 'block', + parseDOM: [{ tag: 'dl' }], + toDOM: () => ['dl', 0], + }; + } + + toMarkdown(state, node) { + state.write('<dl>\n'); + state.wrapBlock(' ', null, node, () => state.renderContent(node)); + state.flushClose(1); + state.ensureNewLine(); + state.write('</dl>'); + state.closeBlock(node); + } +} diff --git a/app/assets/javascripts/behaviors/markdown/nodes/description_term.js b/app/assets/javascripts/behaviors/markdown/nodes/description_term.js new file mode 100644 index 00000000000..89057ec6444 --- /dev/null +++ b/app/assets/javascripts/behaviors/markdown/nodes/description_term.js @@ -0,0 +1,28 @@ +/* eslint-disable class-methods-use-this */ + +import { Node } from 'tiptap'; + +// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter +export default class DescriptionTerm extends Node { + get name() { + return 'description_term'; + } + + get schema() { + return { + content: 'text*', + marks: '', + defining: true, + parseDOM: [{ tag: 'dt' }], + toDOM: () => ['dt', 0], + }; + } + + toMarkdown(state, node) { + state.flushClose(state.closed && state.closed.type === node.type ? 1 : 2); + state.write('<dt>'); + state.text(node.textContent, false); + state.write('</dt>'); + state.closeBlock(node); + } +} diff --git a/app/assets/javascripts/behaviors/markdown/nodes/details.js b/app/assets/javascripts/behaviors/markdown/nodes/details.js new file mode 100644 index 00000000000..1c40dbb8168 --- /dev/null +++ b/app/assets/javascripts/behaviors/markdown/nodes/details.js @@ -0,0 +1,28 @@ +/* eslint-disable class-methods-use-this */ + +import { Node } from 'tiptap'; + +// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter +export default class Details extends Node { + get name() { + return 'details'; + } + + get schema() { + return { + content: 'summary block*', + group: 'block', + parseDOM: [{ tag: 'details' }], + toDOM: () => ['details', { open: true, onclick: 'return false', tabindex: '-1' }, 0], + }; + } + + toMarkdown(state, node) { + state.write('<details>\n'); + state.renderContent(node); + state.flushClose(1); + state.ensureNewLine(); + state.write('</details>'); + state.closeBlock(node); + } +} diff --git a/app/assets/javascripts/behaviors/markdown/nodes/doc.js b/app/assets/javascripts/behaviors/markdown/nodes/doc.js new file mode 100644 index 00000000000..88b16fd85da --- /dev/null +++ b/app/assets/javascripts/behaviors/markdown/nodes/doc.js @@ -0,0 +1,15 @@ +/* eslint-disable class-methods-use-this */ + +import { Node } from 'tiptap'; + +export default class Doc extends Node { + get name() { + return 'doc'; + } + + get schema() { + return { + content: 'block+', + }; + } +} diff --git a/app/assets/javascripts/behaviors/markdown/nodes/emoji.js b/app/assets/javascripts/behaviors/markdown/nodes/emoji.js new file mode 100644 index 00000000000..a7cc3e828f5 --- /dev/null +++ b/app/assets/javascripts/behaviors/markdown/nodes/emoji.js @@ -0,0 +1,41 @@ +/* eslint-disable class-methods-use-this */ + +import { Node } from 'tiptap'; + +// Transforms generated HTML back to GFM for Banzai::Filter::EmojiFilter +export default class Emoji extends Node { + get name() { + return 'emoji'; + } + + get schema() { + return { + inline: true, + group: 'inline', + attrs: { + name: {}, + title: {}, + moji: {}, + }, + parseDOM: [ + { + tag: 'gl-emoji', + getAttrs: el => ({ + name: el.dataset.name, + title: el.getAttribute('title'), + moji: el.textContent, + }), + }, + ], + toDOM: node => [ + 'gl-emoji', + { 'data-name': node.attrs.name, title: node.attrs.title }, + node.attrs.moji, + ], + }; + } + + toMarkdown(state, node) { + state.write(`:${node.attrs.name}:`); + } +} diff --git a/app/assets/javascripts/behaviors/markdown/nodes/hard_break.js b/app/assets/javascripts/behaviors/markdown/nodes/hard_break.js new file mode 100644 index 00000000000..59e5d8ab3e2 --- /dev/null +++ b/app/assets/javascripts/behaviors/markdown/nodes/hard_break.js @@ -0,0 +1,10 @@ +/* eslint-disable class-methods-use-this */ + +import { HardBreak as BaseHardBreak } from 'tiptap-extensions'; + +// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter +export default class HardBreak extends BaseHardBreak { + toMarkdown(state) { + if (!state.atBlank()) state.write(' \n'); + } +} diff --git a/app/assets/javascripts/behaviors/markdown/nodes/heading.js b/app/assets/javascripts/behaviors/markdown/nodes/heading.js new file mode 100644 index 00000000000..fec8608cf5d --- /dev/null +++ b/app/assets/javascripts/behaviors/markdown/nodes/heading.js @@ -0,0 +1,13 @@ +/* eslint-disable class-methods-use-this */ + +import { Heading as BaseHeading } from 'tiptap-extensions'; +import { defaultMarkdownSerializer } from 'prosemirror-markdown'; + +// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter +export default class Heading extends BaseHeading { + toMarkdown(state, node) { + if (!node.childCount) return; + + defaultMarkdownSerializer.nodes.heading(state, node); + } +} diff --git a/app/assets/javascripts/behaviors/markdown/nodes/horizontal_rule.js b/app/assets/javascripts/behaviors/markdown/nodes/horizontal_rule.js new file mode 100644 index 00000000000..695c7160bde --- /dev/null +++ b/app/assets/javascripts/behaviors/markdown/nodes/horizontal_rule.js @@ -0,0 +1,11 @@ +/* eslint-disable class-methods-use-this */ + +import { HorizontalRule as BaseHorizontalRule } from 'tiptap-extensions'; +import { defaultMarkdownSerializer } from 'prosemirror-markdown'; + +// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter +export default class HorizontalRule extends BaseHorizontalRule { + toMarkdown(state, node) { + defaultMarkdownSerializer.nodes.horizontal_rule(state, node); + } +} diff --git a/app/assets/javascripts/behaviors/markdown/nodes/image.js b/app/assets/javascripts/behaviors/markdown/nodes/image.js new file mode 100644 index 00000000000..c225a5ed876 --- /dev/null +++ b/app/assets/javascripts/behaviors/markdown/nodes/image.js @@ -0,0 +1,52 @@ +/* eslint-disable class-methods-use-this */ + +import { Image as BaseImage } from 'tiptap-extensions'; +import { placeholderImage } from '~/lazy_loader'; +import { defaultMarkdownSerializer } from 'prosemirror-markdown'; + +export default class Image extends BaseImage { + get schema() { + return { + attrs: { + src: {}, + alt: { + default: null, + }, + title: { + default: null, + }, + }, + group: 'inline', + inline: true, + draggable: true, + parseDOM: [ + // Matches HTML generated by Banzai::Filter::ImageLinkFilter + { + tag: 'a.no-attachment-icon', + priority: 51, + skip: true, + }, + // Matches HTML generated by Banzai::Filter::ImageLazyLoadFilter + { + tag: 'img[src]', + getAttrs: el => { + const imageSrc = el.src; + const imageUrl = + imageSrc && imageSrc !== placeholderImage ? imageSrc : el.dataset.src || ''; + + return { + src: imageUrl, + title: el.getAttribute('title'), + alt: el.getAttribute('alt'), + }; + }, + }, + ], + toDOM: node => ['img', node.attrs], + }; + } + + toMarkdown(state, node) { + defaultMarkdownSerializer.nodes.image(state, node); + } +} diff --git a/app/assets/javascripts/behaviors/markdown/nodes/list_item.js b/app/assets/javascripts/behaviors/markdown/nodes/list_item.js new file mode 100644 index 00000000000..4237637ed9a --- /dev/null +++ b/app/assets/javascripts/behaviors/markdown/nodes/list_item.js @@ -0,0 +1,11 @@ +/* eslint-disable class-methods-use-this */ + +import { ListItem as BaseListItem } from 'tiptap-extensions'; +import { defaultMarkdownSerializer } from 'prosemirror-markdown'; + +// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter +export default class ListItem extends BaseListItem { + toMarkdown(state, node) { + defaultMarkdownSerializer.nodes.list_item(state, node); + } +} diff --git a/app/assets/javascripts/behaviors/markdown/nodes/ordered_list.js b/app/assets/javascripts/behaviors/markdown/nodes/ordered_list.js new file mode 100644 index 00000000000..4c1542d14ea --- /dev/null +++ b/app/assets/javascripts/behaviors/markdown/nodes/ordered_list.js @@ -0,0 +1,10 @@ +/* eslint-disable class-methods-use-this */ + +import { OrderedList as BaseOrderedList } from 'tiptap-extensions'; + +// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter +export default class OrderedList extends BaseOrderedList { + toMarkdown(state, node) { + state.renderList(node, ' ', () => '1. '); + } +} diff --git a/app/assets/javascripts/behaviors/markdown/nodes/ordered_task_list.js b/app/assets/javascripts/behaviors/markdown/nodes/ordered_task_list.js new file mode 100644 index 00000000000..25c4976a1bc --- /dev/null +++ b/app/assets/javascripts/behaviors/markdown/nodes/ordered_task_list.js @@ -0,0 +1,28 @@ +/* eslint-disable class-methods-use-this */ + +import { Node } from 'tiptap'; + +// Transforms generated HTML back to GFM for Banzai::Filter::TaskListFilter +export default class OrderedTaskList extends Node { + get name() { + return 'ordered_task_list'; + } + + get schema() { + return { + group: 'block', + content: '(task_list_item|list_item)+', + parseDOM: [ + { + priority: 51, + tag: 'ol.task-list', + }, + ], + toDOM: () => ['ol', { class: 'task-list' }, 0], + }; + } + + toMarkdown(state, node) { + state.renderList(node, ' ', () => '1. '); + } +} diff --git a/app/assets/javascripts/behaviors/markdown/nodes/paragraph.js b/app/assets/javascripts/behaviors/markdown/nodes/paragraph.js new file mode 100644 index 00000000000..dec3207b1bb --- /dev/null +++ b/app/assets/javascripts/behaviors/markdown/nodes/paragraph.js @@ -0,0 +1,24 @@ +/* eslint-disable class-methods-use-this */ + +import { Node } from 'tiptap'; +import { defaultMarkdownSerializer } from 'prosemirror-markdown'; + +// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter +export default class Paragraph extends Node { + get name() { + return 'paragraph'; + } + + get schema() { + return { + content: 'inline*', + group: 'block', + parseDOM: [{ tag: 'p' }], + toDOM: () => ['p', 0], + }; + } + + toMarkdown(state, node) { + defaultMarkdownSerializer.nodes.paragraph(state, node); + } +} diff --git a/app/assets/javascripts/behaviors/markdown/nodes/reference.js b/app/assets/javascripts/behaviors/markdown/nodes/reference.js new file mode 100644 index 00000000000..5d6bbeca833 --- /dev/null +++ b/app/assets/javascripts/behaviors/markdown/nodes/reference.js @@ -0,0 +1,52 @@ +/* eslint-disable class-methods-use-this */ + +import { Node } from 'tiptap'; + +// Transforms generated HTML back to GFM for Banzai::Filter::ReferenceFilter and subclasses +export default class Reference extends Node { + get name() { + return 'reference'; + } + + get schema() { + return { + inline: true, + group: 'inline', + atom: true, + attrs: { + className: {}, + referenceType: {}, + originalText: { default: null }, + href: {}, + text: {}, + }, + parseDOM: [ + { + tag: 'a.gfm:not([data-link=true])', + priority: 51, + getAttrs: el => ({ + className: el.className, + referenceType: el.dataset.referenceType, + originalText: el.dataset.original, + href: el.getAttribute('href'), + text: el.textContent, + }), + }, + ], + toDOM: node => [ + 'a', + { + class: node.attrs.className, + href: node.attrs.href, + 'data-reference-type': node.attrs.referenceType, + 'data-original': node.attrs.originalText, + }, + node.attrs.text, + ], + }; + } + + toMarkdown(state, node) { + state.write(node.attrs.originalText || node.attrs.text); + } +} diff --git a/app/assets/javascripts/behaviors/markdown/nodes/summary.js b/app/assets/javascripts/behaviors/markdown/nodes/summary.js new file mode 100644 index 00000000000..2e36e316d71 --- /dev/null +++ b/app/assets/javascripts/behaviors/markdown/nodes/summary.js @@ -0,0 +1,27 @@ +/* eslint-disable class-methods-use-this */ + +import { Node } from 'tiptap'; + +// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter +export default class Summary extends Node { + get name() { + return 'summary'; + } + + get schema() { + return { + content: 'text*', + marks: '', + defining: true, + parseDOM: [{ tag: 'summary' }], + toDOM: () => ['summary', 0], + }; + } + + toMarkdown(state, node) { + state.write('<summary>'); + state.text(node.textContent, false); + state.write('</summary>'); + state.closeBlock(node); + } +} diff --git a/app/assets/javascripts/behaviors/markdown/nodes/table.js b/app/assets/javascripts/behaviors/markdown/nodes/table.js new file mode 100644 index 00000000000..a7fcb9227cd --- /dev/null +++ b/app/assets/javascripts/behaviors/markdown/nodes/table.js @@ -0,0 +1,25 @@ +/* eslint-disable class-methods-use-this */ + +import { Node } from 'tiptap'; + +// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter +export default class Table extends Node { + get name() { + return 'table'; + } + + get schema() { + return { + content: 'table_head table_body', + group: 'block', + isolating: true, + parseDOM: [{ tag: 'table' }], + toDOM: () => ['table', 0], + }; + } + + toMarkdown(state, node) { + state.renderContent(node); + state.closeBlock(node); + } +} diff --git a/app/assets/javascripts/behaviors/markdown/nodes/table_body.js b/app/assets/javascripts/behaviors/markdown/nodes/table_body.js new file mode 100644 index 00000000000..403556dc0c8 --- /dev/null +++ b/app/assets/javascripts/behaviors/markdown/nodes/table_body.js @@ -0,0 +1,24 @@ +/* eslint-disable class-methods-use-this */ + +import { Node } from 'tiptap'; + +// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter +export default class TableBody extends Node { + get name() { + return 'table_body'; + } + + get schema() { + return { + content: 'table_row+', + parseDOM: [{ tag: 'tbody' }], + toDOM: () => ['tbody', 0], + }; + } + + toMarkdown(state, node) { + state.flushClose(1); + state.renderContent(node); + state.closeBlock(node); + } +} diff --git a/app/assets/javascripts/behaviors/markdown/nodes/table_cell.js b/app/assets/javascripts/behaviors/markdown/nodes/table_cell.js new file mode 100644 index 00000000000..c63bfe10e39 --- /dev/null +++ b/app/assets/javascripts/behaviors/markdown/nodes/table_cell.js @@ -0,0 +1,35 @@ +/* eslint-disable class-methods-use-this */ + +import { Node } from 'tiptap'; + +// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter +export default class TableCell extends Node { + get name() { + return 'table_cell'; + } + + get schema() { + return { + attrs: { + header: { default: false }, + align: { default: null }, + }, + content: 'inline*', + isolating: true, + parseDOM: [ + { + tag: 'td, th', + getAttrs: el => ({ + header: el.tagName === 'TH', + align: el.getAttribute('align') || el.style.textAlign, + }), + }, + ], + toDOM: node => [node.attrs.header ? 'th' : 'td', { align: node.attrs.align }, 0], + }; + } + + toMarkdown(state, node) { + state.renderInline(node); + } +} diff --git a/app/assets/javascripts/behaviors/markdown/nodes/table_head.js b/app/assets/javascripts/behaviors/markdown/nodes/table_head.js new file mode 100644 index 00000000000..4cb94bf088c --- /dev/null +++ b/app/assets/javascripts/behaviors/markdown/nodes/table_head.js @@ -0,0 +1,24 @@ +/* eslint-disable class-methods-use-this */ + +import { Node } from 'tiptap'; + +// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter +export default class TableHead extends Node { + get name() { + return 'table_head'; + } + + get schema() { + return { + content: 'table_header_row', + parseDOM: [{ tag: 'thead' }], + toDOM: () => ['thead', 0], + }; + } + + toMarkdown(state, node) { + state.flushClose(1); + state.renderContent(node); + state.closeBlock(node); + } +} diff --git a/app/assets/javascripts/behaviors/markdown/nodes/table_header_row.js b/app/assets/javascripts/behaviors/markdown/nodes/table_header_row.js new file mode 100644 index 00000000000..e7eee636402 --- /dev/null +++ b/app/assets/javascripts/behaviors/markdown/nodes/table_header_row.js @@ -0,0 +1,43 @@ +/* eslint-disable class-methods-use-this */ + +import TableRow from './table_row'; + +const CENTER_ALIGN = 'center'; + +// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter +export default class TableHeaderRow extends TableRow { + get name() { + return 'table_header_row'; + } + + get schema() { + return { + content: 'table_cell+', + parseDOM: [ + { + tag: 'thead tr', + priority: 51, + }, + ], + toDOM: () => ['tr', 0], + }; + } + + toMarkdown(state, node) { + const cellWidths = super.toMarkdown(state, node); + + state.flushClose(1); + + state.write('|'); + node.forEach((cell, _, i) => { + if (i) state.write('|'); + + state.write(cell.attrs.align === CENTER_ALIGN ? ':' : '-'); + state.write(state.repeat('-', cellWidths[i])); + state.write(cell.attrs.align === CENTER_ALIGN || cell.attrs.align === 'right' ? ':' : '-'); + }); + state.write('|'); + + state.closeBlock(node); + } +} diff --git a/app/assets/javascripts/behaviors/markdown/nodes/table_of_contents.js b/app/assets/javascripts/behaviors/markdown/nodes/table_of_contents.js new file mode 100644 index 00000000000..20c7fa8a9ab --- /dev/null +++ b/app/assets/javascripts/behaviors/markdown/nodes/table_of_contents.js @@ -0,0 +1,33 @@ +/* eslint-disable class-methods-use-this */ + +import { Node } from 'tiptap'; + +// Transforms generated HTML back to GFM for Banzai::Filter::TableOfContentsFilter +export default class TableOfContents extends Node { + get name() { + return 'table_of_contents'; + } + + get schema() { + return { + group: 'block', + atom: true, + parseDOM: [ + { + tag: 'ul.section-nav', + priority: 51, + }, + { + tag: 'p.table-of-contents', + priority: 51, + }, + ], + toDOM: () => ['p', { class: 'table-of-contents' }, 'Table of Contents'], + }; + } + + toMarkdown(state, node) { + state.write('[[_TOC_]]'); + state.closeBlock(node); + } +} diff --git a/app/assets/javascripts/behaviors/markdown/nodes/table_row.js b/app/assets/javascripts/behaviors/markdown/nodes/table_row.js new file mode 100644 index 00000000000..5852502773a --- /dev/null +++ b/app/assets/javascripts/behaviors/markdown/nodes/table_row.js @@ -0,0 +1,38 @@ +/* eslint-disable class-methods-use-this */ + +import { Node } from 'tiptap'; + +// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter +export default class TableRow extends Node { + get name() { + return 'table_row'; + } + + get schema() { + return { + content: 'table_cell+', + parseDOM: [{ tag: 'tr' }], + toDOM: () => ['tr', 0], + }; + } + + toMarkdown(state, node) { + const cellWidths = []; + + state.flushClose(1); + + state.write('| '); + node.forEach((cell, _, i) => { + if (i) state.write(' | '); + + const { length } = state.out; + state.render(cell, node, i); + cellWidths.push(state.out.length - length); + }); + state.write(' |'); + + state.closeBlock(node); + + return cellWidths; + } +} diff --git a/app/assets/javascripts/behaviors/markdown/nodes/task_list.js b/app/assets/javascripts/behaviors/markdown/nodes/task_list.js new file mode 100644 index 00000000000..ab33bc21502 --- /dev/null +++ b/app/assets/javascripts/behaviors/markdown/nodes/task_list.js @@ -0,0 +1,28 @@ +/* eslint-disable class-methods-use-this */ + +import { Node } from 'tiptap'; + +// Transforms generated HTML back to GFM for Banzai::Filter::TaskListFilter +export default class TaskList extends Node { + get name() { + return 'task_list'; + } + + get schema() { + return { + group: 'block', + content: '(task_list_item|list_item)+', + parseDOM: [ + { + priority: 51, + tag: 'ul.task-list', + }, + ], + toDOM: () => ['ul', { class: 'task-list' }, 0], + }; + } + + toMarkdown(state, node) { + state.renderList(node, ' ', () => '* '); + } +} diff --git a/app/assets/javascripts/behaviors/markdown/nodes/task_list_item.js b/app/assets/javascripts/behaviors/markdown/nodes/task_list_item.js new file mode 100644 index 00000000000..d0ee7333d5e --- /dev/null +++ b/app/assets/javascripts/behaviors/markdown/nodes/task_list_item.js @@ -0,0 +1,49 @@ +/* eslint-disable class-methods-use-this */ + +import { Node } from 'tiptap'; + +// Transforms generated HTML back to GFM for Banzai::Filter::TaskListFilter +export default class TaskListItem extends Node { + get name() { + return 'task_list_item'; + } + + get schema() { + return { + attrs: { + done: { + default: false, + }, + }, + defining: true, + draggable: false, + content: 'paragraph block*', + parseDOM: [ + { + priority: 51, + tag: 'li.task-list-item', + getAttrs: el => { + const checkbox = el.querySelector('input[type=checkbox].task-list-item-checkbox'); + return { done: checkbox && checkbox.checked }; + }, + }, + ], + toDOM(node) { + return [ + 'li', + { class: 'task-list-item' }, + [ + 'input', + { type: 'checkbox', class: 'task-list-item-checkbox', checked: node.attrs.done }, + ], + ['div', { class: 'todo-content' }, 0], + ]; + }, + }; + } + + toMarkdown(state, node) { + state.write(`[${node.attrs.done ? 'x' : ' '}] `); + state.renderContent(node); + } +} diff --git a/app/assets/javascripts/behaviors/markdown/nodes/text.js b/app/assets/javascripts/behaviors/markdown/nodes/text.js new file mode 100644 index 00000000000..84838c14999 --- /dev/null +++ b/app/assets/javascripts/behaviors/markdown/nodes/text.js @@ -0,0 +1,20 @@ +/* eslint-disable class-methods-use-this */ + +import { Node } from 'tiptap'; +import { defaultMarkdownSerializer } from 'prosemirror-markdown'; + +export default class Text extends Node { + get name() { + return 'text'; + } + + get schema() { + return { + group: 'inline', + }; + } + + toMarkdown(state, node) { + defaultMarkdownSerializer.nodes.text(state, node); + } +} diff --git a/app/assets/javascripts/behaviors/markdown/nodes/video.js b/app/assets/javascripts/behaviors/markdown/nodes/video.js new file mode 100644 index 00000000000..516f983397d --- /dev/null +++ b/app/assets/javascripts/behaviors/markdown/nodes/video.js @@ -0,0 +1,54 @@ +/* eslint-disable class-methods-use-this */ + +import { Node } from 'tiptap'; +import { defaultMarkdownSerializer } from 'prosemirror-markdown'; + +// Transforms generated HTML back to GFM for Banzai::Filter::VideoLinkFilter +export default class Video extends Node { + get name() { + return 'video'; + } + + get schema() { + return { + attrs: { + src: {}, + alt: { + default: null, + }, + }, + group: 'block', + draggable: true, + parseDOM: [ + { + tag: '.video-container', + skip: true, + }, + { + tag: '.video-container p', + priority: 51, + ignore: true, + }, + { + tag: 'video[src]', + getAttrs: el => ({ src: el.getAttribute('src'), alt: el.dataset.title }), + }, + ], + toDOM: node => [ + 'video', + { + src: node.attrs.src, + width: '400', + controls: true, + 'data-setup': '{}', + 'data-title': node.attrs.alt, + }, + ], + }; + } + + toMarkdown(state, node) { + defaultMarkdownSerializer.nodes.image(state, node); + state.closeBlock(node); + } +} diff --git a/app/assets/javascripts/behaviors/markdown/schema.js b/app/assets/javascripts/behaviors/markdown/schema.js new file mode 100644 index 00000000000..163182ab778 --- /dev/null +++ b/app/assets/javascripts/behaviors/markdown/schema.js @@ -0,0 +1,24 @@ +import { Schema } from 'prosemirror-model'; +import editorExtensions from './editor_extensions'; + +const nodes = editorExtensions + .filter(extension => extension.type === 'node') + .reduce( + (ns, { name, schema }) => ({ + ...ns, + [name]: schema, + }), + {}, + ); + +const marks = editorExtensions + .filter(extension => extension.type === 'mark') + .reduce( + (ms, { name, schema }) => ({ + ...ms, + [name]: schema, + }), + {}, + ); + +export default new Schema({ nodes, marks }); diff --git a/app/assets/javascripts/behaviors/markdown/serializer.js b/app/assets/javascripts/behaviors/markdown/serializer.js new file mode 100644 index 00000000000..70dbd8bd206 --- /dev/null +++ b/app/assets/javascripts/behaviors/markdown/serializer.js @@ -0,0 +1,24 @@ +import { MarkdownSerializer } from 'prosemirror-markdown'; +import editorExtensions from './editor_extensions'; + +const nodes = editorExtensions + .filter(extension => extension.type === 'node') + .reduce( + (ns, { name, toMarkdown }) => ({ + ...ns, + [name]: toMarkdown, + }), + {}, + ); + +const marks = editorExtensions + .filter(extension => extension.type === 'mark') + .reduce( + (ms, { name, toMarkdown }) => ({ + ...ms, + [name]: toMarkdown, + }), + {}, + ); + +export default new MarkdownSerializer(nodes, marks); diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js index 2918e1486a7..0eb067d4963 100644 --- a/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js +++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js @@ -1,6 +1,5 @@ import $ from 'jquery'; import Mousetrap from 'mousetrap'; -import _ from 'underscore'; import Sidebar from '../../right_sidebar'; import Shortcuts from './shortcuts'; import { CopyAsGFM } from '../markdown/copy_as_gfm'; @@ -63,18 +62,18 @@ export default class ShortcutsIssuable extends Shortcuts { } const el = CopyAsGFM.transformGFMSelection(documentFragment.cloneNode(true)); - const selected = CopyAsGFM.nodeToGFM(el); + const blockquoteEl = document.createElement('blockquote'); + blockquoteEl.appendChild(el); + const text = CopyAsGFM.nodeToGFM(blockquoteEl); - if (selected.trim() === '') { + if (text.trim() === '') { return false; } - const quote = _.map(selected.split('\n'), val => `${`> ${val}`.trim()}\n`); - // If replyField already has some content, add a newline before our quote const separator = ($replyField.val().trim() !== '' && '\n\n') || ''; $replyField - .val((a, current) => `${current}${separator}${quote.join('')}\n`) + .val((a, current) => `${current}${separator}${text}\n\n`) .trigger('input') .trigger('change'); 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/clusters/clusters_bundle.js b/app/assets/javascripts/clusters/clusters_bundle.js index b1f992c03ff..fc4779632f9 100644 --- a/app/assets/javascripts/clusters/clusters_bundle.js +++ b/app/assets/javascripts/clusters/clusters_bundle.js @@ -6,7 +6,7 @@ import Flash from '../flash'; import Poll from '../lib/utils/poll'; import initSettingsPanels from '../settings_panels'; import eventHub from './event_hub'; -import { APPLICATION_STATUS, REQUEST_LOADING, REQUEST_SUCCESS, REQUEST_FAILURE } from './constants'; +import { APPLICATION_STATUS, REQUEST_SUBMITTED, REQUEST_FAILURE } from './constants'; import ClustersService from './services/clusters_service'; import ClustersStore from './stores/clusters_store'; import Applications from './components/applications.vue'; @@ -231,22 +231,18 @@ export default class Clusters { installApplication(data) { const appId = data.id; - this.store.updateAppProperty(appId, 'requestStatus', REQUEST_LOADING); + this.store.updateAppProperty(appId, 'requestStatus', REQUEST_SUBMITTED); this.store.updateAppProperty(appId, 'requestReason', null); - - this.service - .installApplication(appId, data.params) - .then(() => { - this.store.updateAppProperty(appId, 'requestStatus', REQUEST_SUCCESS); - }) - .catch(() => { - this.store.updateAppProperty(appId, 'requestStatus', REQUEST_FAILURE); - this.store.updateAppProperty( - appId, - 'requestReason', - s__('ClusterIntegration|Request to begin installing failed'), - ); - }); + this.store.updateAppProperty(appId, 'statusReason', null); + + this.service.installApplication(appId, data.params).catch(() => { + this.store.updateAppProperty(appId, 'requestStatus', REQUEST_FAILURE); + this.store.updateAppProperty( + appId, + 'requestReason', + s__('ClusterIntegration|Request to begin installing failed'), + ); + }); } destroy() { diff --git a/app/assets/javascripts/clusters/components/application_row.vue b/app/assets/javascripts/clusters/components/application_row.vue index d4354dcfebd..3c3ce1dec56 100644 --- a/app/assets/javascripts/clusters/components/application_row.vue +++ b/app/assets/javascripts/clusters/components/application_row.vue @@ -4,12 +4,7 @@ import { s__, sprintf } from '../../locale'; import eventHub from '../event_hub'; import identicon from '../../vue_shared/components/identicon.vue'; import loadingButton from '../../vue_shared/components/loading_button.vue'; -import { - APPLICATION_STATUS, - REQUEST_LOADING, - REQUEST_SUCCESS, - REQUEST_FAILURE, -} from '../constants'; +import { APPLICATION_STATUS, REQUEST_SUBMITTED, REQUEST_FAILURE } from '../constants'; export default { components: { @@ -72,6 +67,13 @@ export default { isKnownStatus() { return Object.values(APPLICATION_STATUS).includes(this.status); }, + isInstalling() { + return ( + this.status === APPLICATION_STATUS.SCHEDULED || + this.status === APPLICATION_STATUS.INSTALLING || + (this.requestStatus === REQUEST_SUBMITTED && !this.statusReason && !this.isInstalled) + ); + }, isInstalled() { return ( this.status === APPLICATION_STATUS.INSTALLED || @@ -79,6 +81,18 @@ export default { this.status === APPLICATION_STATUS.UPDATING ); }, + canInstall() { + if (this.isInstalling) { + return false; + } + + return ( + this.status === APPLICATION_STATUS.NOT_INSTALLABLE || + this.status === APPLICATION_STATUS.INSTALLABLE || + this.status === APPLICATION_STATUS.ERROR || + this.isUnknownStatus + ); + }, hasLogo() { return !!this.logoUrl; }, @@ -90,12 +104,7 @@ export default { return `js-cluster-application-row-${this.id}`; }, installButtonLoading() { - return ( - !this.status || - this.status === APPLICATION_STATUS.SCHEDULED || - this.status === APPLICATION_STATUS.INSTALLING || - this.requestStatus === REQUEST_LOADING - ); + return !this.status || this.status === APPLICATION_STATUS.SCHEDULED || this.isInstalling; }, installButtonDisabled() { // Avoid the potential for the real-time data to say APPLICATION_STATUS.INSTALLABLE but @@ -104,30 +113,17 @@ export default { return ( ((this.status !== APPLICATION_STATUS.INSTALLABLE && this.status !== APPLICATION_STATUS.ERROR) || - this.requestStatus === REQUEST_LOADING || - this.requestStatus === REQUEST_SUCCESS) && + this.isInstalling) && this.isKnownStatus ); }, installButtonLabel() { let label; - if ( - this.status === APPLICATION_STATUS.NOT_INSTALLABLE || - this.status === APPLICATION_STATUS.INSTALLABLE || - this.status === APPLICATION_STATUS.ERROR || - this.isUnknownStatus - ) { + if (this.canInstall) { label = s__('ClusterIntegration|Install'); - } else if ( - this.status === APPLICATION_STATUS.SCHEDULED || - this.status === APPLICATION_STATUS.INSTALLING - ) { + } else if (this.isInstalling) { label = s__('ClusterIntegration|Installing'); - } else if ( - this.status === APPLICATION_STATUS.INSTALLED || - this.status === APPLICATION_STATUS.UPDATED || - this.status === APPLICATION_STATUS.UPDATING - ) { + } else if (this.isInstalled) { label = s__('ClusterIntegration|Installed'); } @@ -140,7 +136,10 @@ export default { return s__('ClusterIntegration|Manage'); }, hasError() { - return this.status === APPLICATION_STATUS.ERROR || this.requestStatus === REQUEST_FAILURE; + return ( + !this.isInstalling && + (this.status === APPLICATION_STATUS.ERROR || this.requestStatus === REQUEST_FAILURE) + ); }, generalErrorDescription() { return sprintf(s__('ClusterIntegration|Something went wrong while installing %{title}'), { diff --git a/app/assets/javascripts/clusters/constants.js b/app/assets/javascripts/clusters/constants.js index e31afadf186..360511e8882 100644 --- a/app/assets/javascripts/clusters/constants.js +++ b/app/assets/javascripts/clusters/constants.js @@ -18,8 +18,7 @@ export const APPLICATION_STATUS = { }; // These are only used client-side -export const REQUEST_LOADING = 'request-loading'; -export const REQUEST_SUCCESS = 'request-success'; +export const REQUEST_SUBMITTED = 'request-submitted'; export const REQUEST_FAILURE = 'request-failure'; export const INGRESS = 'ingress'; export const JUPYTER = 'jupyter'; diff --git a/app/assets/javascripts/commons/jquery.js b/app/assets/javascripts/commons/jquery.js index a7ed175f7a4..009153d0703 100644 --- a/app/assets/javascripts/commons/jquery.js +++ b/app/assets/javascripts/commons/jquery.js @@ -7,4 +7,3 @@ import 'vendor/jquery.caret'; import 'vendor/jquery.atwho'; import 'vendor/jquery.scrollTo'; import 'jquery.waitforimages'; -import 'select2/select2'; diff --git a/app/assets/javascripts/contextual_sidebar.js b/app/assets/javascripts/contextual_sidebar.js index 42c3542b50b..50efecb3475 100644 --- a/app/assets/javascripts/contextual_sidebar.js +++ b/app/assets/javascripts/contextual_sidebar.js @@ -70,7 +70,7 @@ export default class ContextualSidebar { ContextualSidebar.setCollapsedCookie(collapsed); } - requestIdleCallback(this.toggleSidebarOverflow); + requestIdleCallback(() => this.toggleSidebarOverflow()); } toggleSidebarOverflow() { diff --git a/app/assets/javascripts/diffs/components/compare_versions.vue b/app/assets/javascripts/diffs/components/compare_versions.vue index f75345d31f8..3ef54752436 100644 --- a/app/assets/javascripts/diffs/components/compare_versions.vue +++ b/app/assets/javascripts/diffs/components/compare_versions.vue @@ -2,9 +2,10 @@ 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'; +import SettingsDropdown from './settings_dropdown.vue'; export default { components: { @@ -12,6 +13,7 @@ export default { Icon, GlLink, GlButton, + SettingsDropdown, }, directives: { GlTooltip: GlTooltipDirective, @@ -34,26 +36,25 @@ export default { }, computed: { ...mapState('diffs', ['commit', 'showTreeList', 'startVersion', 'latestVersionPath']), - ...mapGetters('diffs', ['isInlineView', 'isParallelView', 'hasCollapsedFile']), + ...mapGetters('diffs', ['hasCollapsedFile']), comparableDiffs() { return this.mergeRequestDiffs.slice(1); }, - toggleWhitespaceText() { - if (this.isWhitespaceVisible()) { - return __('Hide whitespace changes'); - } - return __('Show whitespace changes'); - }, - toggleWhitespacePath() { - if (this.isWhitespaceVisible()) { - return mergeUrlParams({ w: 1 }, window.location.href); - } - - return mergeUrlParams({ w: 0 }, window.location.href); - }, 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', [ @@ -62,15 +63,12 @@ export default { 'expandAllFiles', 'toggleShowTreeList', ]), - isWhitespaceVisible() { - return getParameterValues('w')[0] !== '1'; - }, }, }; </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 +77,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 +93,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" @@ -115,31 +114,7 @@ export default { <a v-show="hasCollapsedFile" class="btn btn-default append-right-8" @click="expandAllFiles"> {{ __('Expand all') }} </a> - <a :href="toggleWhitespacePath" class="btn btn-default qa-toggle-whitespace"> - {{ toggleWhitespaceText }} - </a> - <div class="btn-group prepend-left-8"> - <button - id="inline-diff-btn" - :class="{ active: isInlineView }" - type="button" - class="btn js-inline-diff-button" - data-view-type="inline" - @click="setInlineDiffViewType" - > - {{ __('Inline') }} - </button> - <button - id="parallel-diff-btn" - :class="{ active: isParallelView }" - type="button" - class="btn js-parallel-diff-button" - data-view-type="parallel" - @click="setParallelDiffViewType" - > - {{ __('Side-by-side') }} - </button> - </div> + <settings-dropdown /> </div> </div> </div> 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/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/settings_dropdown.vue b/app/assets/javascripts/diffs/components/settings_dropdown.vue new file mode 100644 index 00000000000..0129763161a --- /dev/null +++ b/app/assets/javascripts/diffs/components/settings_dropdown.vue @@ -0,0 +1,92 @@ +<script> +import { mapActions, mapGetters, mapState } from 'vuex'; +import { GlButton } from '@gitlab/ui'; +import Icon from '~/vue_shared/components/icon.vue'; + +export default { + components: { + GlButton, + Icon, + }, + computed: { + ...mapGetters('diffs', ['isInlineView', 'isParallelView']), + ...mapState('diffs', ['renderTreeList', 'showWhitespace']), + }, + methods: { + ...mapActions('diffs', [ + 'setInlineDiffViewType', + 'setParallelDiffViewType', + 'setRenderTreeList', + 'setShowWhitespace', + ]), + }, +}; +</script> + +<template> + <div class="dropdown"> + <button + type="button" + class="btn btn-default js-show-diff-settings" + data-toggle="dropdown" + data-display="static" + > + <icon name="settings" /> <icon name="arrow-down" /> + </button> + <div class="dropdown-menu dropdown-menu-right p-2 pt-3 pb-3"> + <div> + <span class="bold d-block mb-1">{{ __('File browser') }}</span> + <div class="btn-group d-flex"> + <gl-button + :class="{ active: !renderTreeList }" + class="w-100 js-list-view" + @click="setRenderTreeList(false)" + > + {{ __('List view') }} + </gl-button> + <gl-button + :class="{ active: renderTreeList }" + class="w-100 js-tree-view" + @click="setRenderTreeList(true)" + > + {{ __('Tree view') }} + </gl-button> + </div> + </div> + <div class="mt-2"> + <span class="bold d-block mb-1">{{ __('Compare changes') }}</span> + <div class="btn-group d-flex js-diff-view-buttons"> + <gl-button + id="inline-diff-btn" + :class="{ active: isInlineView }" + class="w-100 js-inline-diff-button" + data-view-type="inline" + @click="setInlineDiffViewType" + > + {{ __('Inline') }} + </gl-button> + <gl-button + id="parallel-diff-btn" + :class="{ active: isParallelView }" + class="w-100 js-parallel-diff-button" + data-view-type="parallel" + @click="setParallelDiffViewType" + > + {{ __('Side-by-side') }} + </gl-button> + </div> + </div> + <div class="mt-2"> + <label class="mb-0"> + <input + id="show-whitespace" + type="checkbox" + :checked="showWhitespace" + @change="setShowWhitespace({ showWhitespace: $event.target.checked, pushState: true })" + /> + {{ __('Show whitespace changes') }} + </label> + </div> + </div> + </div> +</template> diff --git a/app/assets/javascripts/diffs/components/tree_list.vue b/app/assets/javascripts/diffs/components/tree_list.vue index eb8f274aff3..0b3def3d29d 100644 --- a/app/assets/javascripts/diffs/components/tree_list.vue +++ b/app/assets/javascripts/diffs/components/tree_list.vue @@ -1,13 +1,10 @@ <script> import { mapActions, mapGetters, mapState } from 'vuex'; import { GlTooltipDirective } from '@gitlab/ui'; -import { parseBoolean } from '~/lib/utils/common_utils'; import Icon from '~/vue_shared/components/icon.vue'; import FileRow from '~/vue_shared/components/file_row.vue'; import FileRowStats from './file_row_stats.vue'; -const treeListStorageKey = 'mr_diff_tree_list'; - export default { directives: { GlTooltip: GlTooltipDirective, @@ -17,17 +14,12 @@ export default { FileRow, }, data() { - const treeListStored = localStorage.getItem(treeListStorageKey); - const renderTreeList = treeListStored !== null ? parseBoolean(treeListStored) : true; - return { search: '', - renderTreeList, - focusSearch: false, }; }, computed: { - ...mapState('diffs', ['tree', 'addedLines', 'removedLines']), + ...mapState('diffs', ['tree', 'addedLines', 'removedLines', 'renderTreeList']), ...mapGetters('diffs', ['allBlobs', 'diffFilesLength']), filteredTreeList() { const search = this.search.toLowerCase().trim(); @@ -52,19 +44,6 @@ export default { ...mapActions('diffs', ['toggleTreeOpen', 'scrollToFile']), clearSearch() { this.search = ''; - this.toggleFocusSearch(false); - }, - toggleRenderTreeList(toggle) { - this.renderTreeList = toggle; - localStorage.setItem(treeListStorageKey, this.renderTreeList); - }, - toggleFocusSearch(toggle) { - this.focusSearch = toggle; - }, - blurSearch() { - if (this.search.trim() === '') { - this.toggleFocusSearch(false); - } }, }, FileRowStats, @@ -81,8 +60,6 @@ export default { :placeholder="s__('MergeRequest|Filter files')" type="search" class="form-control" - @focus="toggleFocusSearch(true);" - @blur="blurSearch" /> <button v-show="search" @@ -94,34 +71,6 @@ export default { <icon name="close" /> </button> </div> - <div v-show="!focusSearch" class="btn-group prepend-left-8 tree-list-view-toggle"> - <button - v-gl-tooltip.hover - :aria-label="__('List view')" - :title="__('List view')" - :class="{ - active: !renderTreeList, - }" - class="btn btn-default pt-0 pb-0 d-flex align-items-center" - type="button" - @click="toggleRenderTreeList(false);" - > - <icon name="hamburger" /> - </button> - <button - v-gl-tooltip.hover - :aria-label="__('Tree view')" - :title="__('Tree view')" - :class="{ - active: renderTreeList, - }" - class="btn btn-default pt-0 pb-0 d-flex align-items-center" - type="button" - @click="toggleRenderTreeList(true);" - > - <icon name="file-tree" /> - </button> - </div> </div> <div :class="{ 'pt-0 tree-list-blobs': !renderTreeList }" class="tree-list-scroll"> <template v-if="filteredTreeList.length"> diff --git a/app/assets/javascripts/diffs/constants.js b/app/assets/javascripts/diffs/constants.js index 78a39baa4cb..bd188d9de9e 100644 --- a/app/assets/javascripts/diffs/constants.js +++ b/app/assets/javascripts/diffs/constants.js @@ -32,3 +32,7 @@ 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'; +export const TREE_LIST_STORAGE_KEY = 'mr_diff_tree_list'; +export const WHITESPACE_STORAGE_KEY = 'mr_show_whitespace'; diff --git a/app/assets/javascripts/diffs/index.js b/app/assets/javascripts/diffs/index.js index b130cedc24c..094e5cdea9c 100644 --- a/app/assets/javascripts/diffs/index.js +++ b/app/assets/javascripts/diffs/index.js @@ -1,6 +1,9 @@ import Vue from 'vue'; -import { mapState } from 'vuex'; +import { mapActions, mapState } from 'vuex'; +import { parseBoolean } from '~/lib/utils/common_utils'; +import { getParameterValues } from '~/lib/utils/url_utility'; import diffsApp from './components/app.vue'; +import { TREE_LIST_STORAGE_KEY } from './constants'; export default function initDiffsApp(store) { return new Vue({ @@ -26,6 +29,16 @@ export default function initDiffsApp(store) { activeTab: state => state.page.activeTab, }), }, + created() { + const treeListStored = localStorage.getItem(TREE_LIST_STORAGE_KEY); + const renderTreeList = treeListStored !== null ? parseBoolean(treeListStored) : true; + + this.setRenderTreeList(renderTreeList); + this.setShowWhitespace({ showWhitespace: getParameterValues('w')[0] !== '1' }); + }, + methods: { + ...mapActions('diffs', ['setRenderTreeList', 'setShowWhitespace']), + }, render(createElement) { return createElement('diffs-app', { props: { diff --git a/app/assets/javascripts/diffs/store/actions.js b/app/assets/javascripts/diffs/store/actions.js index 00a4bb6d3a3..2c5019fb652 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'; @@ -13,6 +14,8 @@ import { INLINE_DIFF_VIEW_TYPE, DIFF_VIEW_COOKIE_NAME, MR_TREE_SHOW_KEY, + TREE_LIST_STORAGE_KEY, + WHITESPACE_STORAGE_KEY, } from '../constants'; export const setBaseConfig = ({ commit }, options) => { @@ -21,17 +24,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) + .get(state.endpoint, { params: { w: state.showWhitespace ? null : '1' } }) .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) => { @@ -265,5 +280,21 @@ export const closeDiffFileCommentForm = ({ commit }, fileHash) => { commit(types.CLOSE_DIFF_FILE_COMMENT_FORM, fileHash); }; +export const setRenderTreeList = ({ commit }, renderTreeList) => { + commit(types.SET_RENDER_TREE_LIST, renderTreeList); + + localStorage.setItem(TREE_LIST_STORAGE_KEY, renderTreeList); +}; + +export const setShowWhitespace = ({ commit }, { showWhitespace, pushState = false }) => { + commit(types.SET_SHOW_WHITESPACE, showWhitespace); + + localStorage.setItem(WHITESPACE_STORAGE_KEY, showWhitespace); + + if (pushState) { + historyPushState(showWhitespace ? '?w=0' : '?w=1'); + } +}; + // prevent babel-plugin-rewire from generating an invalid default during karma tests export default () => {}; diff --git a/app/assets/javascripts/diffs/store/modules/diff_state.js b/app/assets/javascripts/diffs/store/modules/diff_state.js index 98e57d52d77..05b4c552f6e 100644 --- a/app/assets/javascripts/diffs/store/modules/diff_state.js +++ b/app/assets/javascripts/diffs/store/modules/diff_state.js @@ -27,4 +27,6 @@ export default () => ({ projectPath: '', commentForms: [], highlightedRow: null, + renderTreeList: true, + showWhitespace: true, }); diff --git a/app/assets/javascripts/diffs/store/mutation_types.js b/app/assets/javascripts/diffs/store/mutation_types.js index 0338cde3658..e760b4d1079 100644 --- a/app/assets/javascripts/diffs/store/mutation_types.js +++ b/app/assets/javascripts/diffs/store/mutation_types.js @@ -18,3 +18,7 @@ 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'; +export const SET_RENDER_TREE_LIST = 'SET_RENDER_TREE_LIST'; +export const SET_SHOW_WHITESPACE = 'SET_SHOW_WHITESPACE'; diff --git a/app/assets/javascripts/diffs/store/mutations.js b/app/assets/javascripts/diffs/store/mutations.js index ed4203cf5e0..4aeb393b29b 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,14 @@ export default { [types.SET_HIGHLIGHTED_ROW](state, lineCode) { state.highlightedRow = lineCode; }, + [types.SET_TREE_DATA](state, { treeEntries, tree }) { + state.treeEntries = treeEntries; + state.tree = tree; + }, + [types.SET_RENDER_TREE_LIST](state, renderTreeList) { + state.renderTreeList = renderTreeList; + }, + [types.SET_SHOW_WHITESPACE](state, showWhitespace) { + state.showWhitespace = showWhitespace; + }, }; diff --git a/app/assets/javascripts/diffs/store/utils.js b/app/assets/javascripts/diffs/store/utils.js index f427367c11e..effb6202327 100644 --- a/app/assets/javascripts/diffs/store/utils.js +++ b/app/assets/javascripts/diffs/store/utils.js @@ -1,5 +1,6 @@ import _ from 'underscore'; import { diffModes } from '~/ide/constants'; +import { truncatePathMiddleToLength } from '~/lib/utils/text_utility'; import { LINE_POSITION_LEFT, LINE_POSITION_RIGHT, @@ -11,6 +12,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 +182,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 +221,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 +236,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 +295,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: truncatePathMiddleToLength(path.join('/'), 40), + 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 +396,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/mixins/environments_mixin.js b/app/assets/javascripts/environments/mixins/environments_mixin.js index 96dc1f07cb9..e81a1525df0 100644 --- a/app/assets/javascripts/environments/mixins/environments_mixin.js +++ b/app/assets/javascripts/environments/mixins/environments_mixin.js @@ -143,7 +143,7 @@ export default { */ created() { this.service = new EnvironmentsService(this.endpoint); - this.requestData = { page: this.page, scope: this.scope }; + this.requestData = { page: this.page, scope: this.scope, nested: true }; this.poll = new Poll({ resource: this.service, diff --git a/app/assets/javascripts/environments/services/environments_service.js b/app/assets/javascripts/environments/services/environments_service.js index 4e07ccba91a..cb4ff6856db 100644 --- a/app/assets/javascripts/environments/services/environments_service.js +++ b/app/assets/javascripts/environments/services/environments_service.js @@ -7,8 +7,8 @@ export default class EnvironmentsService { } fetchEnvironments(options = {}) { - const { scope, page } = options; - return axios.get(this.environmentsEndpoint, { params: { scope, page } }); + const { scope, page, nested } = options; + return axios.get(this.environmentsEndpoint, { params: { scope, page, nested } }); } // eslint-disable-next-line class-methods-use-this diff --git a/app/assets/javascripts/environments/stores/environments_store.js b/app/assets/javascripts/environments/stores/environments_store.js index 5808a2d4afa..ac9a31c202c 100644 --- a/app/assets/javascripts/environments/stores/environments_store.js +++ b/app/assets/javascripts/environments/stores/environments_store.js @@ -20,7 +20,8 @@ export default class EnvironmentsStore { * * Stores the received environments. * - * In the main environments endpoint, each environment has the following schema + * In the main environments endpoint (with { nested: true } in params), each folder + * has the following schema: * { name: String, size: Number, latest: Object } * In the endpoint to retrieve environments from each folder, the environment does * not have the `latest` key and the data is all in the root level. 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/feature_highlight/feature_highlight.js b/app/assets/javascripts/feature_highlight/feature_highlight.js index 173fe7c69de..be55e6923c6 100644 --- a/app/assets/javascripts/feature_highlight/feature_highlight.js +++ b/app/assets/javascripts/feature_highlight/feature_highlight.js @@ -31,12 +31,14 @@ export function setupFeatureHighlightPopover(id, debounceTimeout = 300) { .removeAttr('disabled'); } +const getPriority = e => parseInt(e.dataset.highlightPriority, 10) || 0; + export function findHighestPriorityFeature() { let priorityFeature; const sortedFeatureEls = [].slice .call(document.querySelectorAll('.js-feature-highlight')) - .sort((a, b) => (a.dataset.highlightPriority || 0) < (b.dataset.highlightPriority || 0)); + .sort((a, b) => getPriority(b) - getPriority(a)); const [priorityFeatureEl] = sortedFeatureEls; if (priorityFeatureEl) { 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/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js index 4a2af02b40a..33c82778c79 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js @@ -593,7 +593,7 @@ export default class FilteredSearchManager { tokens.forEach(token => { const condition = this.filteredSearchTokenKeys.searchByConditionKeyValue( token.key, - token.value.toLowerCase(), + token.value, ); const tokenConfig = this.filteredSearchTokenKeys.searchByKey(token.key) || {}; const { param } = tokenConfig; diff --git a/app/assets/javascripts/filtered_search/filtered_search_token_keys.js b/app/assets/javascripts/filtered_search/filtered_search_token_keys.js index e01dedbb57c..b70da240833 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_token_keys.js +++ b/app/assets/javascripts/filtered_search/filtered_search_token_keys.js @@ -65,8 +65,10 @@ export default class FilteredSearchTokenKeys { searchByConditionKeyValue(key, value) { return ( - this.conditions.find(condition => condition.tokenKey === key && condition.value === value) || - null + this.conditions.find( + condition => + condition.tokenKey === key && condition.value.toLowerCase() === value.toLowerCase(), + ) || null ); } diff --git a/app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js b/app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js index b494b7e2de0..fd61030eb13 100644 --- a/app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js +++ b/app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js @@ -60,52 +60,52 @@ export const conditions = [ { url: 'assignee_id=None', tokenKey: 'assignee', - value: 'none', + value: 'None', }, { url: 'assignee_id=Any', tokenKey: 'assignee', - value: 'any', + value: 'Any', }, { url: 'milestone_title=None', tokenKey: 'milestone', - value: 'none', + value: 'None', }, { url: 'milestone_title=Any', tokenKey: 'milestone', - value: 'any', + value: 'Any', }, { url: 'milestone_title=%23upcoming', tokenKey: 'milestone', - value: 'upcoming', + value: 'Upcoming', }, { url: 'milestone_title=%23started', tokenKey: 'milestone', - value: 'started', + value: 'Started', }, { url: 'label_name[]=None', tokenKey: 'label', - value: 'none', + value: 'None', }, { url: 'label_name[]=Any', - tokenKey: 'any', - value: 'any', + tokenKey: 'label', + value: 'Any', }, { url: 'my_reaction_emoji=None', tokenKey: 'my-reaction', - value: 'none', + value: 'None', }, { url: 'my_reaction_emoji=Any', tokenKey: 'my-reaction', - value: 'any', + value: 'Any', }, ]; 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/groups_select.js b/app/assets/javascripts/groups_select.js index 2049760fe29..bdadbb1bb2a 100644 --- a/app/assets/javascripts/groups_select.js +++ b/app/assets/javascripts/groups_select.js @@ -4,93 +4,97 @@ import Api from './api'; import { normalizeHeaders } from './lib/utils/common_utils'; export default function groupsSelect() { - // Needs to be accessible in rspec - window.GROUP_SELECT_PER_PAGE = 20; - $('.ajax-groups-select').each(function setAjaxGroupsSelect2() { - const $select = $(this); - const allAvailable = $select.data('allAvailable'); - const skipGroups = $select.data('skipGroups') || []; - const parentGroupID = $select.data('parentId'); - const groupsPath = parentGroupID - ? Api.subgroupsPath.replace(':id', parentGroupID) - : Api.groupsPath; + import(/* webpackChunkName: 'select2' */ 'select2/select2') + .then(() => { + // Needs to be accessible in rspec + window.GROUP_SELECT_PER_PAGE = 20; + $('.ajax-groups-select').each(function setAjaxGroupsSelect2() { + const $select = $(this); + const allAvailable = $select.data('allAvailable'); + const skipGroups = $select.data('skipGroups') || []; + const parentGroupID = $select.data('parentId'); + const groupsPath = parentGroupID + ? Api.subgroupsPath.replace(':id', parentGroupID) + : Api.groupsPath; - $select.select2({ - placeholder: 'Search for a group', - allowClear: $select.hasClass('allowClear'), - multiple: $select.hasClass('multiselect'), - minimumInputLength: 0, - ajax: { - url: Api.buildUrl(groupsPath), - dataType: 'json', - quietMillis: 250, - transport(params) { - axios[params.type.toLowerCase()](params.url, { - params: params.data, - }) - .then(res => { - const results = res.data || []; - const headers = normalizeHeaders(res.headers); - const currentPage = parseInt(headers['X-PAGE'], 10) || 0; - const totalPages = parseInt(headers['X-TOTAL-PAGES'], 10) || 0; - const more = currentPage < totalPages; + $select.select2({ + placeholder: 'Search for a group', + allowClear: $select.hasClass('allowClear'), + multiple: $select.hasClass('multiselect'), + minimumInputLength: 0, + ajax: { + url: Api.buildUrl(groupsPath), + dataType: 'json', + quietMillis: 250, + transport(params) { + axios[params.type.toLowerCase()](params.url, { + params: params.data, + }) + .then(res => { + const results = res.data || []; + const headers = normalizeHeaders(res.headers); + const currentPage = parseInt(headers['X-PAGE'], 10) || 0; + const totalPages = parseInt(headers['X-TOTAL-PAGES'], 10) || 0; + const more = currentPage < totalPages; - params.success({ - results, - pagination: { - more, - }, - }); - }) - .catch(params.error); - }, - data(search, page) { - return { - search, - page, - per_page: window.GROUP_SELECT_PER_PAGE, - all_available: allAvailable, - }; - }, - results(data, page) { - if (data.length) return { results: [] }; + params.success({ + results, + pagination: { + more, + }, + }); + }) + .catch(params.error); + }, + data(search, page) { + return { + search, + page, + per_page: window.GROUP_SELECT_PER_PAGE, + all_available: allAvailable, + }; + }, + results(data, page) { + if (data.length) return { results: [] }; - const groups = data.length ? data : data.results || []; - const more = data.pagination ? data.pagination.more : false; - const results = groups.filter(group => skipGroups.indexOf(group.id) === -1); + const groups = data.length ? data : data.results || []; + const more = data.pagination ? data.pagination.more : false; + const results = groups.filter(group => skipGroups.indexOf(group.id) === -1); - return { - results, - page, - more, - }; - }, - }, - // eslint-disable-next-line consistent-return - initSelection(element, callback) { - const id = $(element).val(); - if (id !== '') { - return Api.group(id, callback); - } - }, - formatResult(object) { - return `<div class='group-result'> <div class='group-name'>${ - object.full_name - }</div> <div class='group-path'>${object.full_path}</div> </div>`; - }, - formatSelection(object) { - return object.full_name; - }, - dropdownCssClass: 'ajax-groups-dropdown select2-infinite', - // we do not want to escape markup since we are displaying html in results - escapeMarkup(m) { - return m; - }, - }); + return { + results, + page, + more, + }; + }, + }, + // eslint-disable-next-line consistent-return + initSelection(element, callback) { + const id = $(element).val(); + if (id !== '') { + return Api.group(id, callback); + } + }, + formatResult(object) { + return `<div class='group-result'> <div class='group-name'>${ + object.full_name + }</div> <div class='group-path'>${object.full_path}</div> </div>`; + }, + formatSelection(object) { + return object.full_name; + }, + dropdownCssClass: 'ajax-groups-dropdown select2-infinite', + // we do not want to escape markup since we are displaying html in results + escapeMarkup(m) { + return m; + }, + }); - $select.on('select2-loaded', () => { - const dropdown = document.querySelector('.select2-infinite .select2-results'); - dropdown.style.height = `${Math.floor(dropdown.scrollHeight)}px`; - }); - }); + $select.on('select2-loaded', () => { + const dropdown = document.querySelector('.select2-infinite .select2-results'); + dropdown.style.height = `${Math.floor(dropdown.scrollHeight)}px`; + }); + }); + }) + .catch(() => {}); } 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/list.vue b/app/assets/javascripts/ide/components/commit_sidebar/list.vue index a1094570275..4f1260de0bc 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/list.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/list.vue @@ -87,7 +87,7 @@ export default { }, }, discardModalText: __( - "You will loose all the unstaged changes you've made in this project. This action cannot be undone.", + "You will lose all the unstaged changes you've made in this project. This action cannot be undone.", ), }; </script> 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..09c9d135614 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,9 +70,9 @@ 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.") }} + {{ __("You will lose all changes you've made to this file. This action cannot be undone.") }} </gl-modal> </div> </template> diff --git a/app/assets/javascripts/ide/components/commit_sidebar/unstage_button.vue b/app/assets/javascripts/ide/components/commit_sidebar/unstage_button.vue index 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..ce577ae85b0 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 @@ -107,16 +107,23 @@ export default { class="commit-sha" >{{ lastCommit.short_id }}</a > - by {{ lastCommit.author_name }} + by + <user-avatar-image + css-classes="ide-status-avatar" + :size="18" + :img-src="latestPipeline && latestPipeline.commit.author_gravatar_url" + :img-alt="lastCommit.author_name" + :tooltip-text="lastCommit.author_name" + /> + {{ lastCommit.author_name }} <time v-tooltip :datetime="lastCommit.committed_date" :title="tooltipTitle(lastCommit.committed_date)" data-placement="top" data-container="body" + >{{ lastCommitFormatedAge }}</time > - {{ lastCommitFormatedAge }} - </time> </div> <div v-if="file" class="ide-status-file">{{ file.name }}</div> <div v-if="file" class="ide-status-file">{{ file.eol }}</div> 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/lib/common/model.js b/app/assets/javascripts/ide/lib/common/model.js index 60bddb34977..a15f04075d9 100644 --- a/app/assets/javascripts/ide/lib/common/model.js +++ b/app/assets/javascripts/ide/lib/common/model.js @@ -13,12 +13,12 @@ export default class Model { (this.originalModel = monacoEditor.createModel( head ? head.content : this.file.raw, undefined, - new Uri(false, false, `original/${this.path}`), + new Uri('gitlab', false, `original/${this.path}`), )), (this.model = monacoEditor.createModel( this.content, undefined, - new Uri(false, false, this.path), + new Uri('gitlab', false, this.path), )), ); if (this.file.mrChange) { @@ -26,7 +26,7 @@ export default class Model { (this.baseModel = monacoEditor.createModel( this.file.baseRaw, undefined, - new Uri(false, false, `target/${this.path}`), + new Uri('gitlab', false, `target/${this.path}`), )), ); } diff --git a/app/assets/javascripts/issuable/auto_width_dropdown_select.js b/app/assets/javascripts/issuable/auto_width_dropdown_select.js index 612c524ca1c..e0fb58ef195 100644 --- a/app/assets/javascripts/issuable/auto_width_dropdown_select.js +++ b/app/assets/javascripts/issuable/auto_width_dropdown_select.js @@ -11,10 +11,14 @@ class AutoWidthDropdownSelect { init() { const { dropdownClass } = this; - this.$selectElement.select2({ - dropdownCssClass: dropdownClass, - ...AutoWidthDropdownSelect.selectOptions(this.dropdownClass), - }); + import(/* webpackChunkName: 'select2' */ 'select2/select2') + .then(() => { + this.$selectElement.select2({ + dropdownCssClass: dropdownClass, + ...AutoWidthDropdownSelect.selectOptions(this.dropdownClass), + }); + }) + .catch(() => {}); return this; } diff --git a/app/assets/javascripts/issuable_context.js b/app/assets/javascripts/issuable_context.js index f3d722409b0..48e7ed1318d 100644 --- a/app/assets/javascripts/issuable_context.js +++ b/app/assets/javascripts/issuable_context.js @@ -7,10 +7,14 @@ export default class IssuableContext { constructor(currentUser) { this.userSelect = new UsersSelect(currentUser); - $('select.select2').select2({ - width: 'resolve', - dropdownAutoWidth: true, - }); + import(/* webpackChunkName: 'select2' */ 'select2/select2') + .then(() => { + $('select.select2').select2({ + width: 'resolve', + dropdownAutoWidth: true, + }); + }) + .catch(() => {}); $('.issuable-sidebar .inline-update').on('change', 'select', function onClickSelect() { return $(this).submit(); diff --git a/app/assets/javascripts/issuable_form.js b/app/assets/javascripts/issuable_form.js index c81a2230310..4d2533d01f1 100644 --- a/app/assets/javascripts/issuable_form.js +++ b/app/assets/javascripts/issuable_form.js @@ -120,35 +120,39 @@ export default class IssuableForm { } initTargetBranchDropdown() { - this.$targetBranchSelect.select2({ - ...AutoWidthDropdownSelect.selectOptions('js-target-branch-select'), - ajax: { - url: this.$targetBranchSelect.data('endpoint'), - dataType: 'JSON', - quietMillis: 250, - data(search) { - return { - search, - }; - }, - results(data) { - return { - // `data` keys are translated so we can't just access them with a string based key - results: data[Object.keys(data)[0]].map(name => ({ - id: name, - text: name, - })), - }; - }, - }, - initSelection(el, callback) { - const val = el.val(); - - callback({ - id: val, - text: val, + import(/* webpackChunkName: 'select2' */ 'select2/select2') + .then(() => { + this.$targetBranchSelect.select2({ + ...AutoWidthDropdownSelect.selectOptions('js-target-branch-select'), + ajax: { + url: this.$targetBranchSelect.data('endpoint'), + dataType: 'JSON', + quietMillis: 250, + data(search) { + return { + search, + }; + }, + results(data) { + return { + // `data` keys are translated so we can't just access them with a string based key + results: data[Object.keys(data)[0]].map(name => ({ + id: name, + text: name, + })), + }; + }, + }, + initSelection(el, callback) { + const val = el.val(); + + callback({ + id: val, + text: val, + }); + }, }); - }, - }); + }) + .catch(() => {}); } } diff --git a/app/assets/javascripts/issue_show/components/app.vue b/app/assets/javascripts/issue_show/components/app.vue index e4e2eab2acd..cd569eb3045 100644 --- a/app/assets/javascripts/issue_show/components/app.vue +++ b/app/assets/javascripts/issue_show/components/app.vue @@ -10,6 +10,7 @@ import descriptionComponent from './description.vue'; import editedComponent from './edited.vue'; import formComponent from './form.vue'; import recaptchaModalImplementor from '../../vue_shared/mixins/recaptcha_modal_implementor'; +import { __ } from '~/locale'; export default { components: { @@ -201,8 +202,8 @@ export default { methods: { handleBeforeUnloadEvent(e) { const event = e; - if (this.showForm && this.issueChanged) { - event.returnValue = 'Are you sure you want to lose your issue information?'; + if (this.showForm && this.issueChanged && !this.showRecaptcha) { + event.returnValue = __('Are you sure you want to lose your issue information?'); } return undefined; }, 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/label_manager.js b/app/assets/javascripts/label_manager.js index 062501d1d04..f134a54dd53 100644 --- a/app/assets/javascripts/label_manager.js +++ b/app/assets/javascripts/label_manager.js @@ -70,7 +70,18 @@ export default class LabelManager { const $detachedLabel = $label.detach(); this.toggleLabelPriorityBadge($detachedLabel, action); - $detachedLabel.appendTo($target); + + const $labelEls = $target.find('li.label-list-item'); + + /* + * If there is a label element in the target, we'd want to + * append the new label just right next to it. + */ + if ($labelEls.length) { + $labelEls.last().after($detachedLabel); + } else { + $detachedLabel.appendTo($target); + } if ($from.find('li').length) { $from.find('.empty-message').removeClass('hidden'); 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 3b6a57dad44..ae8b4b4d635 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js +++ b/app/assets/javascripts/lib/utils/common_utils.js @@ -614,10 +614,18 @@ export const spriteIcon = (icon, className = '') => { /** * This method takes in object with snake_case property names - * and returns new object with camelCase property names + * and returns a new object with camelCase property names * * Reasoning for this method is to ensure consistent property * naming conventions across JS code. + * + * This method also supports additional params in `options` object + * + * @param {Object} obj - Object to be converted. + * @param {Object} options - Object containing additional options. + * @param {boolean} options.deep - FLag to allow deep object converting + * @param {Array[]} dropKeys - List of properties to discard while building new object + * @param {Array[]} ignoreKeyNames - List of properties to leave intact (as snake_case) while building new object */ export const convertObjectPropsToCamelCase = (obj = {}, options = {}) => { if (obj === null) { @@ -625,12 +633,26 @@ export const convertObjectPropsToCamelCase = (obj = {}, options = {}) => { } const initial = Array.isArray(obj) ? [] : {}; + const { deep = false, dropKeys = [], ignoreKeyNames = [] } = options; return Object.keys(obj).reduce((acc, prop) => { const result = acc; const val = obj[prop]; - if (options.deep && (isObject(val) || Array.isArray(val))) { + // Drop properties from new object if + // there are any mentioned in options + if (dropKeys.indexOf(prop) > -1) { + return acc; + } + + // Skip converting properties in new object + // if there are any mentioned in options + if (ignoreKeyNames.indexOf(prop) > -1) { + result[prop] = obj[prop]; + return acc; + } + + if (deep && (isObject(val) || Array.isArray(val))) { result[convertToCamelCase(prop)] = convertObjectPropsToCamelCase(val, options); } else { result[convertToCamelCase(prop)] = obj[prop]; 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/main.js b/app/assets/javascripts/main.js index 4ba3543f9b2..8e10b3ad912 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -100,18 +100,24 @@ function deferredInitialisation() { }); // Initialize select2 selects - $('select.select2').select2({ - width: 'resolve', - dropdownAutoWidth: true, - }); - - // Close select2 on escape - $('.js-select2').on('select2-close', () => { - setTimeout(() => { - $('.select2-container-active').removeClass('select2-container-active'); - $(':focus').blur(); - }, 1); - }); + if ($('select.select2').length) { + import(/* webpackChunkName: 'select2' */ 'select2/select2') + .then(() => { + $('select.select2').select2({ + width: 'resolve', + dropdownAutoWidth: true, + }); + + // Close select2 on escape + $('.js-select2').on('select2-close', () => { + setTimeout(() => { + $('.select2-container-active').removeClass('select2-container-active'); + $(':focus').blur(); + }, 1); + }); + }) + .catch(() => {}); + } // Initialize tooltips $body.tooltip({ diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js index b0dc5697018..2f15da42271 100644 --- a/app/assets/javascripts/merge_request_tabs.js +++ b/app/assets/javascripts/merge_request_tabs.js @@ -428,7 +428,7 @@ export default class MergeRequestTabs { } diffViewType() { - return $('.inline-parallel-buttons button.active').data('viewType'); + return $('.js-diff-view-buttons button.active').data('viewType'); } isDiffAction(action) { diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue index cea5c1a56ca..0b4bb9cc686 100644 --- a/app/assets/javascripts/monitoring/components/dashboard.vue +++ b/app/assets/javascripts/monitoring/components/dashboard.vue @@ -6,15 +6,12 @@ import Flash from '../../flash'; import MonitoringService from '../services/monitoring_service'; import MonitorAreaChart from './charts/area.vue'; import GraphGroup from './graph_group.vue'; -import Graph from './graph.vue'; import EmptyState from './empty_state.vue'; import MonitoringStore from '../stores/monitoring_store'; -import eventHub from '../event_hub'; export default { components: { MonitorAreaChart, - Graph, GraphGroup, EmptyState, Icon, @@ -25,21 +22,11 @@ export default { required: false, default: true, }, - showLegend: { - type: Boolean, - required: false, - default: true, - }, showPanels: { type: Boolean, required: false, default: true, }, - forceSmallGraph: { - type: Boolean, - required: false, - default: false, - }, documentationPath: { type: String, required: true, @@ -99,14 +86,10 @@ export default { store: new MonitoringStore(), state: 'gettingStarted', showEmptyState: true, - hoverData: {}, elWidth: 0, }; }, computed: { - graphComponent() { - return gon.features && gon.features.areaChart ? MonitorAreaChart : Graph; - }, forceRedraw() { return this.elWidth; }, @@ -122,10 +105,8 @@ export default { childList: false, subtree: false, }; - eventHub.$on('hoverChanged', this.hoverChanged); }, beforeDestroy() { - eventHub.$off('hoverChanged', this.hoverChanged); window.removeEventListener('resize', this.resizeThrottled, false); this.sidebarMutationObserver.disconnect(); }, @@ -176,9 +157,6 @@ export default { resize() { this.elWidth = this.$el.clientWidth; }, - hoverChanged(data) { - this.hoverData = data; - }, }, }; </script> @@ -196,13 +174,13 @@ export default { class="dropdown-menu dropdown-menu-selectable dropdown-menu-drop-up" > <ul> - <li v-for="environment in store.environmentsData" :key="environment.latest.id"> + <li v-for="environment in store.environmentsData" :key="environment.id"> <a - :href="environment.latest.metrics_path" - :class="{ 'is-active': environment.latest.name == currentEnvironmentName }" + :href="environment.metrics_path" + :class="{ 'is-active': environment.name == currentEnvironmentName }" class="dropdown-item" > - {{ environment.latest.name }} + {{ environment.name }} </a> </li> </ul> @@ -215,23 +193,13 @@ export default { :name="groupData.group" :show-panels="showPanels" > - <component - :is="graphComponent" + <monitor-area-chart v-for="(graphData, graphIndex) in groupData.metrics" :key="graphIndex" :graph-data="graphData" - :hover-data="hoverData" - :deployment-data="store.deploymentData" - :project-path="projectPath" - :tags-path="tagsPath" - :show-legend="showLegend" - :small-graph="forceSmallGraph" :alert-data="getGraphAlerts(graphData.id)" group-id="monitor-area-chart" - > - <!-- EE content --> - {{ null }} - </component> + /> </graph-group> </div> <empty-state diff --git a/app/assets/javascripts/monitoring/components/graph.vue b/app/assets/javascripts/monitoring/components/graph.vue deleted file mode 100644 index 64a1df80a8e..00000000000 --- a/app/assets/javascripts/monitoring/components/graph.vue +++ /dev/null @@ -1,329 +0,0 @@ -<script> -import { scaleLinear, scaleTime } from 'd3-scale'; -import { axisLeft, axisBottom } from 'd3-axis'; -import _ from 'underscore'; -import { max, extent } from 'd3-array'; -import { select } from 'd3-selection'; -import GraphAxis from './graph/axis.vue'; -import GraphLegend from './graph/legend.vue'; -import GraphFlag from './graph/flag.vue'; -import GraphDeployment from './graph/deployment.vue'; -import GraphPath from './graph/path.vue'; -import MonitoringMixin from '../mixins/monitoring_mixins'; -import eventHub from '../event_hub'; -import measurements from '../utils/measurements'; -import { bisectDate, timeScaleFormat } from '../utils/date_time_formatters'; -import createTimeSeries from '../utils/multiple_time_series'; -import bp from '../../breakpoints'; - -const d3 = { scaleLinear, scaleTime, axisLeft, axisBottom, max, extent, select }; - -export default { - components: { - GraphAxis, - GraphFlag, - GraphDeployment, - GraphPath, - GraphLegend, - }, - mixins: [MonitoringMixin], - props: { - graphData: { - type: Object, - required: true, - }, - deploymentData: { - type: Array, - required: true, - }, - hoverData: { - type: Object, - required: false, - default: () => ({}), - }, - projectPath: { - type: String, - required: true, - }, - tagsPath: { - type: String, - required: true, - }, - showLegend: { - type: Boolean, - required: false, - default: true, - }, - smallGraph: { - type: Boolean, - required: false, - default: false, - }, - }, - data() { - return { - baseGraphHeight: 450, - baseGraphWidth: 600, - graphHeight: 450, - graphWidth: 600, - graphHeightOffset: 120, - margin: {}, - unitOfDisplay: '', - yAxisLabel: '', - legendTitle: '', - reducedDeploymentData: [], - measurements: measurements.large, - currentData: { - time: new Date(), - value: 0, - }, - currentXCoordinate: 0, - currentCoordinates: {}, - showFlag: false, - showFlagContent: false, - timeSeries: [], - graphDrawData: {}, - realPixelRatio: 1, - seriesUnderMouse: [], - }; - }, - computed: { - outerViewBox() { - return `0 0 ${this.baseGraphWidth} ${this.baseGraphHeight}`; - }, - innerViewBox() { - return `0 0 ${this.baseGraphWidth - 150} ${this.baseGraphHeight}`; - }, - axisTransform() { - return `translate(70, ${this.graphHeight - 100})`; - }, - paddingBottomRootSvg() { - return { - paddingBottom: `${Math.ceil(this.baseGraphHeight * 100) / this.baseGraphWidth || 0}%`, - }; - }, - deploymentFlagData() { - return this.reducedDeploymentData.find(deployment => deployment.showDeploymentFlag); - }, - shouldRenderData() { - return this.graphData.queries.filter(s => s.result.length > 0).length > 0; - }, - }, - watch: { - hoverData() { - this.positionFlag(); - }, - }, - mounted() { - this.draw(); - }, - methods: { - showDot(path) { - return this.showFlagContent && this.seriesUnderMouse.includes(path); - }, - draw() { - const breakpointSize = bp.getBreakpointSize(); - const svgWidth = this.$refs.baseSvg.getBoundingClientRect().width; - - this.margin = measurements.large.margin; - - if (this.smallGraph || breakpointSize === 'xs' || breakpointSize === 'sm') { - this.graphHeight = 300; - this.margin = measurements.small.margin; - this.measurements = measurements.small; - } - - this.yAxisLabel = this.graphData.y_label || 'Values'; - this.graphWidth = svgWidth - this.margin.left - this.margin.right; - this.graphHeight = this.graphHeight - this.margin.top - this.margin.bottom; - this.baseGraphHeight = this.graphHeight - 50; - this.baseGraphWidth = this.graphWidth; - - // pixel offsets inside the svg and outside are not 1:1 - this.realPixelRatio = svgWidth / this.baseGraphWidth; - - // set the legends on the axes - const [query] = this.graphData.queries; - this.legendTitle = query ? query.label : 'Average'; - this.unitOfDisplay = query ? query.unit : ''; - - if (this.shouldRenderData) { - this.renderAxesPaths(); - this.formatDeployments(); - } - }, - handleMouseOverGraph(e) { - let point = this.$refs.graphData.createSVGPoint(); - point.x = e.clientX; - point.y = e.clientY; - point = point.matrixTransform(this.$refs.graphData.getScreenCTM().inverse()); - point.x += 7; - - this.seriesUnderMouse = this.timeSeries.filter(series => { - const mouseX = series.timeSeriesScaleX.invert(point.x); - let minDistance = Infinity; - - const closestTickMark = Object.keys(this.allXAxisValues).reduce((closest, x) => { - const distance = Math.abs(Number(new Date(x)) - Number(mouseX)); - if (distance < minDistance) { - minDistance = distance; - return x; - } - return closest; - }); - - return series.values.find(v => v.time.toString() === closestTickMark); - }); - - const firstTimeSeries = this.seriesUnderMouse[0]; - const timeValueOverlay = firstTimeSeries.timeSeriesScaleX.invert(point.x); - const overlayIndex = bisectDate(firstTimeSeries.values, timeValueOverlay, 1); - const d0 = firstTimeSeries.values[overlayIndex - 1]; - const d1 = firstTimeSeries.values[overlayIndex]; - if (d0 === undefined || d1 === undefined) return; - const evalTime = timeValueOverlay - d0[0] > d1[0] - timeValueOverlay; - const hoveredDataIndex = evalTime ? overlayIndex : overlayIndex - 1; - const hoveredDate = firstTimeSeries.values[hoveredDataIndex].time; - const currentDeployXPos = this.mouseOverDeployInfo(point.x); - - eventHub.$emit('hoverChanged', { - hoveredDate, - currentDeployXPos, - }); - }, - renderAxesPaths() { - ({ timeSeries: this.timeSeries, graphDrawData: this.graphDrawData } = createTimeSeries( - this.graphData.queries, - this.graphWidth, - this.graphHeight, - this.graphHeightOffset, - )); - - if (_.findWhere(this.timeSeries, { renderCanary: true })) { - this.timeSeries = this.timeSeries.map(series => ({ ...series, renderCanary: true })); - } - - const axisXScale = d3.scaleTime().range([0, this.graphWidth - 70]); - const axisYScale = d3.scaleLinear().range([this.graphHeight - this.graphHeightOffset, 0]); - - const allValues = this.timeSeries.reduce((all, { values }) => all.concat(values), []); - axisXScale.domain(d3.extent(allValues, d => d.time)); - axisYScale.domain([0, d3.max(allValues.map(d => d.value))]); - - this.allXAxisValues = this.timeSeries.reduce((obj, series) => { - const seriesKeys = {}; - series.values.forEach(v => { - seriesKeys[v.time] = true; - }); - return { - ...obj, - ...seriesKeys, - }; - }, {}); - - const xAxis = d3 - .axisBottom() - .scale(axisXScale) - .ticks(this.graphWidth / 120) - .tickFormat(timeScaleFormat); - - const yAxis = d3 - .axisLeft() - .scale(axisYScale) - .ticks(measurements.yTicks); - - d3.select(this.$refs.baseSvg) - .select('.x-axis') - .call(xAxis); - - const width = this.graphWidth; - d3.select(this.$refs.baseSvg) - .select('.y-axis') - .call(yAxis) - .selectAll('.tick') - .each(function createTickLines(d, i) { - if (i > 0) { - d3.select(this) - .select('line') - .attr('x2', width) - .attr('class', 'axis-tick'); - } // Avoid adding the class to the first tick, to prevent coloring - }); // This will select all of the ticks once they're rendered - }, - }, -}; -</script> - -<template> - <div - class="prometheus-graph" - @mouseover="showFlagContent = true;" - @mouseleave="showFlagContent = false;" - > - <div class="prometheus-graph-header"> - <h5 class="prometheus-graph-title">{{ graphData.title }}</h5> - <div class="prometheus-graph-widgets"><slot></slot></div> - </div> - <div :style="paddingBottomRootSvg" class="prometheus-svg-container"> - <svg ref="baseSvg" :viewBox="outerViewBox"> - <g :transform="axisTransform" class="x-axis" /> - <g class="y-axis" transform="translate(70, 20)" /> - <graph-axis - :graph-width="graphWidth" - :graph-height="graphHeight" - :margin="margin" - :measurements="measurements" - :y-axis-label="yAxisLabel" - :unit-of-display="unitOfDisplay" - /> - <svg v-if="shouldRenderData" ref="graphData" :viewBox="innerViewBox" class="graph-data"> - <slot name="additionalSvgContent" :graphDrawData="graphDrawData" /> - <graph-path - v-for="(path, index) in timeSeries" - :key="index" - :generated-line-path="path.linePath" - :generated-area-path="path.areaPath" - :line-style="path.lineStyle" - :line-color="path.lineColor" - :area-color="path.areaColor" - :current-coordinates="currentCoordinates[path.metricTag]" - :show-dot="showDot(path)" - /> - <graph-deployment - :deployment-data="reducedDeploymentData" - :graph-height="graphHeight" - :graph-height-offset="graphHeightOffset" - /> - <rect - ref="graphOverlay" - :width="graphWidth - 70" - :height="graphHeight - 100" - class="prometheus-graph-overlay" - transform="translate(-5, 20)" - @mousemove="handleMouseOverGraph($event);" - /> - </svg> - <svg v-else :viewBox="innerViewBox" class="js-no-data-to-display"> - <text x="50%" y="50%" alignment-baseline="middle" text-anchor="middle"> - {{ s__('Metrics|No data to display') }} - </text> - </svg> - </svg> - <graph-flag - v-if="shouldRenderData" - :real-pixel-ratio="realPixelRatio" - :current-x-coordinate="currentXCoordinate" - :current-data="currentData" - :graph-height="graphHeight" - :graph-height-offset="graphHeightOffset" - :show-flag-content="showFlagContent" - :time-series="seriesUnderMouse" - :unit-of-display="unitOfDisplay" - :legend-title="legendTitle" - :deployment-flag-data="deploymentFlagData" - :current-coordinates="currentCoordinates" - /> - </div> - <graph-legend v-if="showLegend" :legend-title="legendTitle" :time-series="timeSeries" /> - </div> -</template> diff --git a/app/assets/javascripts/monitoring/components/graph/axis.vue b/app/assets/javascripts/monitoring/components/graph/axis.vue deleted file mode 100644 index 8f046857a20..00000000000 --- a/app/assets/javascripts/monitoring/components/graph/axis.vue +++ /dev/null @@ -1,118 +0,0 @@ -<script> -import { convertToSentenceCase } from '~/lib/utils/text_utility'; -import { s__ } from '~/locale'; - -export default { - props: { - graphWidth: { - type: Number, - required: true, - }, - graphHeight: { - type: Number, - required: true, - }, - margin: { - type: Object, - required: true, - }, - measurements: { - type: Object, - required: true, - }, - yAxisLabel: { - type: String, - required: true, - }, - unitOfDisplay: { - type: String, - required: true, - }, - }, - data() { - return { - yLabelWidth: 0, - yLabelHeight: 0, - }; - }, - computed: { - textTransform() { - const yCoordinate = - (this.graphHeight - this.margin.top + this.measurements.axisLabelLineOffset) / 2 || 0; - - return `translate(15, ${yCoordinate}) rotate(-90)`; - }, - - rectTransform() { - const yCoordinate = - (this.graphHeight - this.margin.top + this.measurements.axisLabelLineOffset) / 2 + - this.yLabelWidth / 2 || 0; - - return `translate(0, ${yCoordinate}) rotate(-90)`; - }, - - xPosition() { - return (this.graphWidth + this.measurements.axisLabelLineOffset) / 2 - this.margin.right || 0; - }, - - yPosition() { - return this.graphHeight - this.margin.top + this.measurements.axisLabelLineOffset || 0; - }, - - yAxisLabelSentenceCase() { - return `${convertToSentenceCase(this.yAxisLabel)} (${this.unitOfDisplay})`; - }, - - timeString() { - return s__('PrometheusDashboard|Time'); - }, - }, - mounted() { - this.$nextTick(() => { - const bbox = this.$refs.ylabel.getBBox(); - this.yLabelWidth = bbox.width + 10; // Added some padding - this.yLabelHeight = bbox.height + 5; - }); - }, -}; -</script> -<template> - <g class="axis-label-container"> - <line - :y1="yPosition" - :x2="graphWidth + 20" - :y2="yPosition" - class="label-x-axis-line" - stroke="#000000" - stroke-width="1" - x1="10" - /> - <line - :x2="10" - :y2="yPosition" - class="label-y-axis-line" - stroke="#000000" - stroke-width="1" - x1="10" - y1="0" - /> - <rect - :transform="rectTransform" - :width="yLabelWidth" - :height="yLabelHeight" - class="rect-axis-text" - /> - <text - ref="ylabel" - :transform="textTransform" - class="label-axis-text y-label-text" - text-anchor="middle" - > - {{ yAxisLabelSentenceCase }} - </text> - <rect :x="xPosition + 60" :y="graphHeight - 80" class="rect-axis-text" width="35" height="50" /> - <text :x="xPosition + 60" :y="yPosition" class="label-axis-text x-label-text" dy=".35em"> - {{ timeString }} - </text> - </g> -</template> diff --git a/app/assets/javascripts/monitoring/components/graph/deployment.vue b/app/assets/javascripts/monitoring/components/graph/deployment.vue deleted file mode 100644 index bee9784692c..00000000000 --- a/app/assets/javascripts/monitoring/components/graph/deployment.vue +++ /dev/null @@ -1,48 +0,0 @@ -<script> -export default { - props: { - deploymentData: { - type: Array, - required: true, - }, - graphHeight: { - type: Number, - required: true, - }, - graphHeightOffset: { - type: Number, - required: true, - }, - }, - computed: { - calculatedHeight() { - return this.graphHeight - this.graphHeightOffset; - }, - }, - methods: { - transformDeploymentGroup(deployment) { - return `translate(${Math.floor(deployment.xPos) - 5}, 20)`; - }, - }, -}; -</script> -<template> - <g class="deploy-info"> - <g - v-for="(deployment, index) in deploymentData" - :key="index" - :transform="transformDeploymentGroup(deployment)" - > - <rect :height="calculatedHeight" x="0" y="0" width="3" fill="url(#shadow-gradient)" /> - <line :y2="calculatedHeight" class="deployment-line" x1="0" y1="0" x2="0" stroke="#000" /> - </g> - <svg height="0" width="0"> - <defs> - <linearGradient id="shadow-gradient"> - <stop offset="0%" stop-color="#000" stop-opacity="0.4" /> - <stop offset="100%" stop-color="#000" stop-opacity="0" /> - </linearGradient> - </defs> - </svg> - </g> -</template> diff --git a/app/assets/javascripts/monitoring/components/graph/flag.vue b/app/assets/javascripts/monitoring/components/graph/flag.vue deleted file mode 100644 index 9d6d1caef80..00000000000 --- a/app/assets/javascripts/monitoring/components/graph/flag.vue +++ /dev/null @@ -1,151 +0,0 @@ -<script> -import { dateFormat, timeFormat } from '../../utils/date_time_formatters'; -import { formatRelevantDigits } from '../../../lib/utils/number_utils'; -import Icon from '../../../vue_shared/components/icon.vue'; -import TrackLine from './track_line.vue'; - -export default { - components: { - Icon, - TrackLine, - }, - props: { - currentXCoordinate: { - type: Number, - required: true, - }, - currentData: { - type: Object, - required: true, - }, - deploymentFlagData: { - type: Object, - required: false, - default: null, - }, - graphHeight: { - type: Number, - required: true, - }, - graphHeightOffset: { - type: Number, - required: true, - }, - realPixelRatio: { - type: Number, - required: true, - }, - showFlagContent: { - type: Boolean, - required: true, - }, - timeSeries: { - type: Array, - required: true, - }, - unitOfDisplay: { - type: String, - required: true, - }, - legendTitle: { - type: String, - required: true, - }, - currentCoordinates: { - type: Object, - required: true, - }, - }, - computed: { - formatTime() { - return this.deploymentFlagData - ? timeFormat(this.deploymentFlagData.time) - : timeFormat(this.currentData.time); - }, - formatDate() { - return this.deploymentFlagData - ? dateFormat(this.deploymentFlagData.time) - : dateFormat(this.currentData.time); - }, - cursorStyle() { - const xCoordinate = this.deploymentFlagData - ? this.deploymentFlagData.xPos - : this.currentXCoordinate; - - const offsetTop = 20 * this.realPixelRatio; - const offsetLeft = (70 + xCoordinate) * this.realPixelRatio; - const height = (this.graphHeight - this.graphHeightOffset) * this.realPixelRatio; - - return { - top: `${offsetTop}px`, - left: `${offsetLeft}px`, - height: `${height}px`, - }; - }, - flagOrientation() { - if (this.currentXCoordinate * this.realPixelRatio > 120) { - return 'left'; - } - return 'right'; - }, - }, - methods: { - seriesMetricValue(seriesIndex, series) { - const indexFromCoordinates = this.currentCoordinates[series.metricTag] - ? this.currentCoordinates[series.metricTag].currentDataIndex - : 0; - const index = this.deploymentFlagData - ? this.deploymentFlagData.seriesIndex - : indexFromCoordinates; - const value = series.values[index] && series.values[index].value; - if (Number.isNaN(value)) { - return '-'; - } - return `${formatRelevantDigits(value)}${this.unitOfDisplay}`; - }, - seriesMetricLabel(index, series) { - if (this.timeSeries.length < 2) { - return this.legendTitle; - } - if (series.metricTag) { - return series.metricTag; - } - return `series ${index + 1}`; - }, - }, -}; -</script> - -<template> - <div :style="cursorStyle" class="prometheus-graph-cursor"> - <div v-if="showFlagContent" :class="flagOrientation" class="prometheus-graph-flag popover"> - <div class="arrow-shadow"></div> - <div class="arrow"></div> - <div class="popover-title"> - <h5 v-if="deploymentFlagData">Deployed</h5> - {{ formatDate }} <strong>{{ formatTime }}</strong> - </div> - <div v-if="deploymentFlagData" class="popover-content deploy-meta-content"> - <div> - <icon :size="12" name="commit" /> - <a :href="deploymentFlagData.commitUrl"> {{ deploymentFlagData.sha.slice(0, 8) }} </a> - </div> - <div v-if="deploymentFlagData.tag"> - <icon :size="12" name="label" /> - <a :href="deploymentFlagData.tagUrl"> {{ deploymentFlagData.ref }} </a> - </div> - </div> - <div class="popover-content"> - <table class="prometheus-table"> - <tr v-for="(series, index) in timeSeries" :key="index"> - <track-line :track="series" /> - <td>{{ series.track }} {{ seriesMetricLabel(index, series) }}</td> - <td> - <strong>{{ seriesMetricValue(index, series) }}</strong> - </td> - </tr> - </table> - </div> - </div> - </div> -</template> diff --git a/app/assets/javascripts/monitoring/components/graph/legend.vue b/app/assets/javascripts/monitoring/components/graph/legend.vue deleted file mode 100644 index b5211c306a3..00000000000 --- a/app/assets/javascripts/monitoring/components/graph/legend.vue +++ /dev/null @@ -1,62 +0,0 @@ -<script> -import TrackLine from './track_line.vue'; -import TrackInfo from './track_info.vue'; - -export default { - components: { - TrackLine, - TrackInfo, - }, - props: { - legendTitle: { - type: String, - required: true, - }, - timeSeries: { - type: Array, - required: true, - }, - }, - methods: { - isStable(track) { - return { - 'prometheus-table-row-highlight': track.trackName !== 'Canary' && track.renderCanary, - }; - }, - }, -}; -</script> -<template> - <div class="prometheus-graph-legends prepend-left-10"> - <table class="prometheus-table"> - <tr - v-for="(series, index) in timeSeries" - v-if="series.shouldRenderLegend" - :key="index" - :class="isStable(series)" - > - <td> - <strong v-if="series.renderCanary">{{ series.trackName }}</strong> - </td> - <track-line :track="series" /> - <td v-if="timeSeries.length > 1" class="legend-metric-title"> - <track-info v-if="series.metricTag" :track="series" /> - <track-info v-else :track="series"> - <strong>{{ legendTitle }}</strong> series {{ index + 1 }} - </track-info> - </td> - <td v-else> - <track-info :track="series"> - <strong>{{ legendTitle }}</strong> - </track-info> - </td> - <template v-for="(track, trackIndex) in series.tracksLegend"> - <track-line :key="`track-line-${trackIndex}`" :track="track" /> - <td :key="`track-info-${trackIndex}`"> - <track-info :track="track" class="legend-metric-title" /> - </td> - </template> - </tr> - </table> - </div> -</template> diff --git a/app/assets/javascripts/monitoring/components/graph/path.vue b/app/assets/javascripts/monitoring/components/graph/path.vue deleted file mode 100644 index f2c237ec391..00000000000 --- a/app/assets/javascripts/monitoring/components/graph/path.vue +++ /dev/null @@ -1,65 +0,0 @@ -<script> -export default { - props: { - generatedLinePath: { - type: String, - required: true, - }, - generatedAreaPath: { - type: String, - required: true, - }, - lineStyle: { - type: String, - required: false, - default: '', - }, - lineColor: { - type: String, - required: true, - }, - areaColor: { - type: String, - required: true, - }, - currentCoordinates: { - type: Object, - required: false, - default: () => ({ currentX: 0, currentY: 0 }), - }, - showDot: { - type: Boolean, - required: true, - }, - }, - computed: { - strokeDashArray() { - if (this.lineStyle === 'dashed') return '3, 1'; - if (this.lineStyle === 'dotted') return '1, 1'; - return null; - }, - }, -}; -</script> -<template> - <g transform="translate(-5, 20)"> - <circle - v-if="showDot" - :cx="currentCoordinates.currentX" - :cy="currentCoordinates.currentY" - :fill="lineColor" - :stroke="lineColor" - class="circle-path" - r="3" - /> - <path :d="generatedAreaPath" :fill="areaColor" class="metric-area" /> - <path - :d="generatedLinePath" - :stroke="lineColor" - :stroke-dasharray="strokeDashArray" - class="metric-line" - fill="none" - stroke-width="1" - /> - </g> -</template> diff --git a/app/assets/javascripts/monitoring/components/graph/track_info.vue b/app/assets/javascripts/monitoring/components/graph/track_info.vue deleted file mode 100644 index 3464067834f..00000000000 --- a/app/assets/javascripts/monitoring/components/graph/track_info.vue +++ /dev/null @@ -1,28 +0,0 @@ -<script> -import { formatRelevantDigits } from '~/lib/utils/number_utils'; - -export default { - name: 'TrackInfo', - props: { - track: { - type: Object, - required: true, - }, - }, - computed: { - summaryMetrics() { - return `Avg: ${formatRelevantDigits(this.track.average)} · Max: ${formatRelevantDigits( - this.track.max, - )}`; - }, - }, -}; -</script> -<template> - <span> - <slot> - <strong> {{ track.metricTag }} </strong> - </slot> - {{ summaryMetrics }} - </span> -</template> diff --git a/app/assets/javascripts/monitoring/components/graph/track_line.vue b/app/assets/javascripts/monitoring/components/graph/track_line.vue deleted file mode 100644 index d2ed1ba113e..00000000000 --- a/app/assets/javascripts/monitoring/components/graph/track_line.vue +++ /dev/null @@ -1,33 +0,0 @@ -<script> -export default { - name: 'TrackLine', - props: { - track: { - type: Object, - required: true, - }, - }, - computed: { - stylizedLine() { - if (this.track.lineStyle === 'dashed') return '6, 3'; - if (this.track.lineStyle === 'dotted') return '3, 3'; - return null; - }, - }, -}; -</script> -<template> - <td> - <svg width="16" height="8"> - <line - :stroke-dasharray="stylizedLine" - :stroke="track.lineColor" - :x1="0" - :x2="16" - :y1="4" - :y2="4" - stroke-width="4" - /> - </svg> - </td> -</template> diff --git a/app/assets/javascripts/monitoring/event_hub.js b/app/assets/javascripts/monitoring/event_hub.js deleted file mode 100644 index 0948c2e5352..00000000000 --- a/app/assets/javascripts/monitoring/event_hub.js +++ /dev/null @@ -1,3 +0,0 @@ -import Vue from 'vue'; - -export default new Vue(); diff --git a/app/assets/javascripts/monitoring/mixins/monitoring_mixins.js b/app/assets/javascripts/monitoring/mixins/monitoring_mixins.js deleted file mode 100644 index 87c3d969de4..00000000000 --- a/app/assets/javascripts/monitoring/mixins/monitoring_mixins.js +++ /dev/null @@ -1,86 +0,0 @@ -import { bisectDate } from '../utils/date_time_formatters'; - -const mixins = { - methods: { - mouseOverDeployInfo(mouseXPos) { - if (!this.reducedDeploymentData) return false; - - let dataFound = false; - this.reducedDeploymentData = this.reducedDeploymentData.map(d => { - const deployment = d; - if (d.xPos >= mouseXPos - 10 && d.xPos <= mouseXPos + 10 && !dataFound) { - dataFound = d.xPos + 1; - - deployment.showDeploymentFlag = true; - } else { - deployment.showDeploymentFlag = false; - } - return deployment; - }); - - return dataFound; - }, - - formatDeployments() { - this.reducedDeploymentData = this.deploymentData.reduce((deploymentDataArray, deployment) => { - const time = new Date(deployment.created_at); - const xPos = Math.floor(this.timeSeries[0].timeSeriesScaleX(time)); - - time.setSeconds(this.timeSeries[0].values[0].time.getSeconds()); - - if (xPos >= 0) { - const seriesIndex = bisectDate(this.timeSeries[0].values, time, 1); - - deploymentDataArray.push({ - id: deployment.id, - time, - sha: deployment.sha, - commitUrl: `${this.projectPath}/commit/${deployment.sha}`, - tag: deployment.tag, - tagUrl: deployment.tag ? `${this.tagsPath}/${deployment.ref.name}` : null, - ref: deployment.ref.name, - xPos, - seriesIndex, - showDeploymentFlag: false, - }); - } - - return deploymentDataArray; - }, []); - }, - - positionFlag() { - const timeSeries = this.seriesUnderMouse[0]; - if (!timeSeries) { - return; - } - const hoveredDataIndex = bisectDate(timeSeries.values, this.hoverData.hoveredDate); - - this.currentData = timeSeries.values[hoveredDataIndex]; - this.currentXCoordinate = Math.floor(timeSeries.timeSeriesScaleX(this.currentData.time)); - - this.currentCoordinates = {}; - - this.seriesUnderMouse.forEach(series => { - const currentDataIndex = bisectDate(series.values, this.hoverData.hoveredDate); - const currentData = series.values[currentDataIndex]; - const currentX = Math.floor(series.timeSeriesScaleX(currentData.time)); - const currentY = Math.floor(series.timeSeriesScaleY(currentData.value)); - - this.currentCoordinates[series.metricTag] = { - currentX, - currentY, - currentDataIndex, - }; - }); - - if (this.hoverData.currentDeployXPos) { - this.showFlag = false; - } else { - this.showFlag = true; - } - }, - }, -}; - -export default mixins; diff --git a/app/assets/javascripts/monitoring/stores/monitoring_store.js b/app/assets/javascripts/monitoring/stores/monitoring_store.js index 8692c873a41..96ecc5ab8a8 100644 --- a/app/assets/javascripts/monitoring/stores/monitoring_store.js +++ b/app/assets/javascripts/monitoring/stores/monitoring_store.js @@ -66,9 +66,7 @@ export default class MonitoringStore { } storeEnvironmentsData(environmentsData = []) { - this.environmentsData = environmentsData.filter( - environment => !!environment.latest.last_deployment, - ); + this.environmentsData = environmentsData.filter(environment => !!environment.last_deployment); } getMetricsCount() { diff --git a/app/assets/javascripts/monitoring/utils/date_time_formatters.js b/app/assets/javascripts/monitoring/utils/date_time_formatters.js deleted file mode 100644 index d88c13609dc..00000000000 --- a/app/assets/javascripts/monitoring/utils/date_time_formatters.js +++ /dev/null @@ -1,42 +0,0 @@ -import { timeFormat as time } from 'd3-time-format'; -import { timeSecond, timeMinute, timeHour, timeDay, timeWeek, timeMonth, timeYear } from 'd3-time'; -import { bisector } from 'd3-array'; - -const d3 = { - time, - bisector, - timeSecond, - timeMinute, - timeHour, - timeDay, - timeWeek, - timeMonth, - timeYear, -}; - -export const dateFormat = d3.time('%d %b %Y, '); -export const timeFormat = d3.time('%-I:%M%p'); -export const dateFormatWithName = d3.time('%a, %b %-d'); -export const bisectDate = d3.bisector(d => d.time).left; - -export function timeScaleFormat(date) { - let formatFunction; - if (d3.timeSecond(date) < date) { - formatFunction = d3.time('.%L'); - } else if (d3.timeMinute(date) < date) { - formatFunction = d3.time(':%S'); - } else if (d3.timeHour(date) < date) { - formatFunction = d3.time('%-I:%M'); - } else if (d3.timeDay(date) < date) { - formatFunction = d3.time('%-I %p'); - } else if (d3.timeWeek(date) < date) { - formatFunction = d3.time('%a %d'); - } else if (d3.timeMonth(date) < date) { - formatFunction = d3.time('%b %d'); - } else if (d3.timeYear(date) < date) { - formatFunction = d3.time('%B'); - } else { - formatFunction = d3.time('%Y'); - } - return formatFunction(date); -} diff --git a/app/assets/javascripts/monitoring/utils/measurements.js b/app/assets/javascripts/monitoring/utils/measurements.js deleted file mode 100644 index 7c771f43eee..00000000000 --- a/app/assets/javascripts/monitoring/utils/measurements.js +++ /dev/null @@ -1,44 +0,0 @@ -export default { - small: { - // Covers both xs and sm screen sizes - margin: { - top: 40, - right: 40, - bottom: 50, - left: 40, - }, - legends: { - width: 15, - height: 3, - offsetX: 20, - offsetY: 32, - }, - backgroundLegend: { - width: 30, - height: 50, - }, - axisLabelLineOffset: -20, - }, - large: { - // This covers both md and lg screen sizes - margin: { - top: 80, - right: 80, - bottom: 100, - left: 80, - }, - legends: { - width: 15, - height: 3, - offsetX: 20, - offsetY: 34, - }, - backgroundLegend: { - width: 30, - height: 150, - }, - axisLabelLineOffset: 20, - }, - xTicks: 8, - yTicks: 3, -}; diff --git a/app/assets/javascripts/monitoring/utils/multiple_time_series.js b/app/assets/javascripts/monitoring/utils/multiple_time_series.js deleted file mode 100644 index 50ba14dfb2e..00000000000 --- a/app/assets/javascripts/monitoring/utils/multiple_time_series.js +++ /dev/null @@ -1,223 +0,0 @@ -import _ from 'underscore'; -import { scaleLinear, scaleTime } from 'd3-scale'; -import { line, area, curveLinear } from 'd3-shape'; -import { extent, max, sum } from 'd3-array'; -import { timeMinute, timeSecond } from 'd3-time'; -import { capitalizeFirstCharacter } from '~/lib/utils/text_utility'; - -const d3 = { - scaleLinear, - scaleTime, - line, - area, - curveLinear, - extent, - max, - timeMinute, - timeSecond, - sum, -}; - -const defaultColorPalette = { - blue: ['#1f78d1', '#8fbce8'], - orange: ['#fc9403', '#feca81'], - red: ['#db3b21', '#ed9d90'], - green: ['#1aaa55', '#8dd5aa'], - purple: ['#6666c4', '#d1d1f0'], -}; - -const defaultColorOrder = ['blue', 'orange', 'red', 'green', 'purple']; - -const defaultStyleOrder = ['solid', 'dashed', 'dotted']; - -function queryTimeSeries(query, graphDrawData, lineStyle) { - let usedColors = []; - let renderCanary = false; - const timeSeriesParsed = []; - - function pickColor(name) { - let pick; - if (name && defaultColorPalette[name]) { - pick = name; - } else { - const unusedColors = _.difference(defaultColorOrder, usedColors); - if (unusedColors.length > 0) { - [pick] = unusedColors; - } else { - usedColors = []; - [pick] = defaultColorOrder; - } - } - usedColors.push(pick); - return defaultColorPalette[pick]; - } - - function findByDate(series, time) { - const val = series.find(v => Math.abs(d3.timeSecond.count(time, v.time)) < 60); - if (val) { - return val.value; - } - return NaN; - } - - // The timeseries data may have gaps in it - // but we need a regularly-spaced set of time/value pairs - // this gives us a complete range of one minute intervals - // offset the same amount as the original data - const [minX, maxX] = graphDrawData.xDom; - const offset = d3.timeMinute(minX) - Number(minX); - const datesWithoutGaps = d3.timeSecond - .every(60) - .range(d3.timeMinute.offset(minX, -1), maxX) - .map(d => d - offset); - - query.result.forEach((timeSeries, timeSeriesNumber) => { - let metricTag = ''; - let lineColor = ''; - let areaColor = ''; - let shouldRenderLegend = true; - const timeSeriesValues = timeSeries.values.map(d => d.value); - const maximumValue = d3.max(timeSeriesValues); - const accum = d3.sum(timeSeriesValues); - const trackName = capitalizeFirstCharacter(query.track ? query.track : 'Stable'); - - if (trackName === 'Canary') { - renderCanary = true; - } - - const timeSeriesMetricLabel = timeSeries.metric[Object.keys(timeSeries.metric)[0]]; - const seriesCustomizationData = - query.series != null && _.findWhere(query.series[0].when, { value: timeSeriesMetricLabel }); - - if (seriesCustomizationData) { - metricTag = seriesCustomizationData.value || timeSeriesMetricLabel; - [lineColor, areaColor] = pickColor(seriesCustomizationData.color); - if (timeSeriesParsed.length > 0) { - shouldRenderLegend = false; - } else { - shouldRenderLegend = true; - } - } else { - metricTag = timeSeriesMetricLabel || query.label || `series ${timeSeriesNumber + 1}`; - [lineColor, areaColor] = pickColor(); - if (timeSeriesParsed.length > 1) { - shouldRenderLegend = false; - } - } - - const values = datesWithoutGaps.map(time => ({ - time, - value: findByDate(timeSeries.values, time), - })); - - timeSeriesParsed.push({ - linePath: graphDrawData.lineFunction(values), - areaPath: graphDrawData.areaBelowLine(values), - timeSeriesScaleX: graphDrawData.timeSeriesScaleX, - timeSeriesScaleY: graphDrawData.timeSeriesScaleY, - values: timeSeries.values, - max: maximumValue, - average: accum / timeSeries.values.length, - lineStyle, - lineColor, - areaColor, - metricTag, - trackName, - shouldRenderLegend, - renderCanary, - }); - - if (!shouldRenderLegend) { - if (!timeSeriesParsed[0].tracksLegend) { - timeSeriesParsed[0].tracksLegend = []; - } - timeSeriesParsed[0].tracksLegend.push({ - max: maximumValue, - average: accum / timeSeries.values.length, - lineStyle, - lineColor, - metricTag, - }); - } - }); - - return timeSeriesParsed; -} - -function xyDomain(queries) { - const allValues = queries.reduce( - (allQueryResults, query) => - allQueryResults.concat( - query.result.reduce((allResults, result) => allResults.concat(result.values), []), - ), - [], - ); - - const xDom = d3.extent(allValues, d => d.time); - const yDom = [0, d3.max(allValues.map(d => d.value))]; - - return { - xDom, - yDom, - }; -} - -export function generateGraphDrawData(queries, graphWidth, graphHeight, graphHeightOffset) { - const { xDom, yDom } = xyDomain(queries); - - const timeSeriesScaleX = d3.scaleTime().range([0, graphWidth - 70]); - const timeSeriesScaleY = d3.scaleLinear().range([graphHeight - graphHeightOffset, 0]); - - timeSeriesScaleX.domain(xDom); - timeSeriesScaleX.ticks(d3.timeMinute, 60); - timeSeriesScaleY.domain(yDom); - - const defined = d => !Number.isNaN(d.value) && d.value != null; - - const lineFunction = d3 - .line() - .defined(defined) - .curve(d3.curveLinear) // d3 v4 uses curbe instead of interpolate - .x(d => timeSeriesScaleX(d.time)) - .y(d => timeSeriesScaleY(d.value)); - - const areaBelowLine = d3 - .area() - .defined(defined) - .curve(d3.curveLinear) - .x(d => timeSeriesScaleX(d.time)) - .y0(graphHeight - graphHeightOffset) - .y1(d => timeSeriesScaleY(d.value)); - - const areaAboveLine = d3 - .area() - .defined(defined) - .curve(d3.curveLinear) - .x(d => timeSeriesScaleX(d.time)) - .y0(0) - .y1(d => timeSeriesScaleY(d.value)); - - return { - lineFunction, - areaBelowLine, - areaAboveLine, - xDom, - yDom, - timeSeriesScaleX, - timeSeriesScaleY, - }; -} - -export default function createTimeSeries(queries, graphWidth, graphHeight, graphHeightOffset) { - const graphDrawData = generateGraphDrawData(queries, graphWidth, graphHeight, graphHeightOffset); - - const timeSeries = queries.reduce((series, query, index) => { - const lineStyle = defaultStyleOrder[index % defaultStyleOrder.length]; - return series.concat(queryTimeSeries(query, graphDrawData, lineStyle)); - }, []); - - return { - timeSeries, - graphDrawData, - }; -} 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/discussion_jump_to_next_button.vue b/app/assets/javascripts/notes/components/discussion_jump_to_next_button.vue new file mode 100644 index 00000000000..07a5bda6bcb --- /dev/null +++ b/app/assets/javascripts/notes/components/discussion_jump_to_next_button.vue @@ -0,0 +1,28 @@ +<script> +import icon from '~/vue_shared/components/icon.vue'; +import { GlTooltipDirective } from '@gitlab/ui'; + +export default { + name: 'JumpToNextDiscussionButton', + components: { + icon, + }, + directives: { + GlTooltip: GlTooltipDirective, + }, +}; +</script> + +<template> + <div class="btn-group" role="group"> + <button + ref="button" + v-gl-tooltip + class="btn btn-default discussion-next-btn" + :title="s__('MergeRequests|Jump to next unresolved discussion')" + @click="$emit('onClick')" + > + <icon name="comment-next" /> + </button> + </div> +</template> diff --git a/app/assets/javascripts/notes/components/discussion_resolve_button.vue b/app/assets/javascripts/notes/components/discussion_resolve_button.vue new file mode 100644 index 00000000000..2b29d710236 --- /dev/null +++ b/app/assets/javascripts/notes/components/discussion_resolve_button.vue @@ -0,0 +1,28 @@ +<script> +export default { + name: 'ResolveDiscussionButton', + props: { + isResolving: { + type: Boolean, + required: false, + default: false, + }, + buttonTitle: { + type: String, + required: true, + }, + }, +}; +</script> + +<template> + <button ref="button" type="button" class="btn btn-default ml-sm-2" @click="$emit('onClick')"> + <i + v-if="isResolving" + ref="isResolvingIcon" + aria-hidden="true" + class="fa fa-spinner fa-spin" + ></i> + {{ buttonTitle }} + </button> +</template> 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..695efe3602f 100644 --- a/app/assets/javascripts/notes/components/noteable_discussion.vue +++ b/app/assets/javascripts/notes/components/noteable_discussion.vue @@ -12,6 +12,7 @@ import { SYSTEM_NOTE } from '../constants'; import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; import noteableNote from './noteable_note.vue'; import noteHeader from './note_header.vue'; +import resolveDiscussionButton from './discussion_resolve_button.vue'; import toggleRepliesWidget from './toggle_replies_widget.vue'; import noteSignedOutWidget from './note_signed_out_widget.vue'; import noteEditedText from './note_edited_text.vue'; @@ -23,6 +24,7 @@ import autosave from '../mixins/autosave'; import noteable from '../mixins/noteable'; import resolvable from '../mixins/resolvable'; import discussionNavigation from '../mixins/discussion_navigation'; +import jumpToNextDiscussionButton from './discussion_jump_to_next_button.vue'; export default { name: 'NoteableDiscussion', @@ -34,6 +36,8 @@ export default { noteSignedOutWidget, noteEditedText, noteForm, + resolveDiscussionButton, + jumpToNextDiscussionButton, toggleRepliesWidget, placeholderNote, placeholderSystemNote, @@ -206,11 +210,25 @@ 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; + }, + commit() { + if (!this.discussion.for_commit) { + return null; + } + + return { + id: this.discussion.commit_id, + url: this.discussion.discussion_path, + }; }, }, watch: { @@ -376,6 +394,7 @@ Please check your network connection and try again.`; :is="componentName(initialDiscussion)" :note="componentData(initialDiscussion)" :line="line" + :commit="commit" :help-page-path="helpPagePath" @handleDeleteNote="deleteNoteHandler" > @@ -436,16 +455,12 @@ Please check your network connection and try again.`; > Reply... </button> - <div v-if="discussion.resolvable"> - <button - type="button" - class="btn btn-default ml-sm-2" - @click="resolveHandler();" - > - <i v-if="isResolving" aria-hidden="true" class="fa fa-spinner fa-spin"></i> - {{ resolveButtonTitle }} - </button> - </div> + <resolve-discussion-button + v-if="discussion.resolvable" + :is-resolving="isResolving" + :button-title="resolveButtonTitle" + @onClick="resolveHandler" + /> <div v-if="discussion.resolvable" class="btn-group discussion-actions ml-sm-2" @@ -461,16 +476,10 @@ Please check your network connection and try again.`; <icon name="issue-new" /> </a> </div> - <div v-if="shouldShowJumpToNextDiscussion" class="btn-group" role="group"> - <button - v-gl-tooltip - class="btn btn-default discussion-next-btn" - title="Jump to next unresolved discussion" - @click="jumpToNextDiscussion" - > - <icon name="comment-next" /> - </button> - </div> + <jump-to-next-discussion-button + v-if="shouldShowJumpToNextDiscussion" + @onClick="jumpToNextDiscussion" + /> </div> </div> </template> diff --git a/app/assets/javascripts/notes/components/noteable_note.vue b/app/assets/javascripts/notes/components/noteable_note.vue index 4c02588127e..3c48d81ed05 100644 --- a/app/assets/javascripts/notes/components/noteable_note.vue +++ b/app/assets/javascripts/notes/components/noteable_note.vue @@ -2,7 +2,9 @@ import $ from 'jquery'; import { mapGetters, mapActions } from 'vuex'; import { escape } from 'underscore'; +import { truncateSha } from '~/lib/utils/text_utility'; import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue'; +import { s__, sprintf } from '../../locale'; import Flash from '../../flash'; import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; import noteHeader from './note_header.vue'; @@ -37,6 +39,11 @@ export default { required: false, default: '', }, + commit: { + type: Object, + required: false, + default: () => null, + }, }, data() { return { @@ -73,6 +80,21 @@ export default { isTarget() { return this.targetNoteHash === this.noteAnchorId; }, + actionText() { + if (!this.commit) { + return ''; + } + + // We need to do this to ensure we have the currect sentence order + // when translating this as the sentence order may change from one + // language to the next. See: + // https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/24427#note_133713771 + const { id, url } = this.commit; + const commitLink = `<a class="commit-sha monospace" href="${escape(url)}">${truncateSha( + id, + )}</a>`; + return sprintf(s__('MergeRequests|commented on commit %{commitLink}'), { commitLink }, false); + }, }, created() { @@ -200,13 +222,10 @@ export default { </div> <div class="timeline-content"> <div class="note-header"> - <note-header - v-once - :author="author" - :created-at="note.created_at" - :note-id="note.id" - action-text="commented" - /> + <note-header v-once :author="author" :created-at="note.created_at" :note-id="note.id"> + <span v-if="commit" v-html="actionText"></span> + <span v-else class="d-none d-sm-inline">·</span> + </note-header> <note-actions :author-id="author.id" :note-id="note.id" diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js index 65f85314fa0..2105a62cecb 100644 --- a/app/assets/javascripts/notes/stores/actions.js +++ b/app/assets/javascripts/notes/stores/actions.js @@ -415,12 +415,13 @@ export const submitSuggestion = ( commit(types.APPLY_SUGGESTION, { discussionId, noteId, suggestionId }); callback(); }) - .catch(() => { - Flash( - __('Something went wrong while applying the suggestion. Please try again.'), - 'alert', - flashContainer, + .catch(err => { + const defaultMessage = __( + 'Something went wrong while applying the suggestion. Please try again.', ); + const flashMessage = err.response.data ? `${err.response.data.message}.` : defaultMessage; + + Flash(__(flashMessage), 'alert', flashContainer); callback(); }); }; 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/explore/projects/index.js b/app/assets/javascripts/pages/explore/projects/index.js index 0c585e162cb..8f98be79640 100644 --- a/app/assets/javascripts/pages/explore/projects/index.js +++ b/app/assets/javascripts/pages/explore/projects/index.js @@ -1,3 +1,7 @@ import ProjectsList from '~/projects_list'; +import Star from '../../../star'; -document.addEventListener('DOMContentLoaded', () => new ProjectsList()); +document.addEventListener('DOMContentLoaded', () => { + new ProjectsList(); // eslint-disable-line no-new + new Star('.project-row'); // eslint-disable-line no-new +}); 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/pages/projects/tags/releases/index.js b/app/assets/javascripts/pages/projects/tags/releases/index.js new file mode 100644 index 00000000000..d6afc71fb03 --- /dev/null +++ b/app/assets/javascripts/pages/projects/tags/releases/index.js @@ -0,0 +1,8 @@ +import $ from 'jquery'; +import ZenMode from '~/zen_mode'; +import GLForm from '~/gl_form'; + +document.addEventListener('DOMContentLoaded', () => { + new ZenMode(); // eslint-disable-line no-new + new GLForm($('.release-form')); // eslint-disable-line no-new +}); 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/project_select.js b/app/assets/javascripts/project_select.js index a33835472bb..5ee510eb11d 100644 --- a/app/assets/javascripts/project_select.js +++ b/app/assets/javascripts/project_select.js @@ -5,97 +5,101 @@ import Api from './api'; import ProjectSelectComboButton from './project_select_combo_button'; export default function projectSelect() { - $('.ajax-project-select').each(function(i, select) { - var placeholder; - const simpleFilter = $(select).data('simpleFilter') || false; - this.groupId = $(select).data('groupId'); - this.includeGroups = $(select).data('includeGroups'); - this.allProjects = $(select).data('allProjects') || false; - this.orderBy = $(select).data('orderBy') || 'id'; - this.withIssuesEnabled = $(select).data('withIssuesEnabled'); - this.withMergeRequestsEnabled = $(select).data('withMergeRequestsEnabled'); - this.withShared = - $(select).data('withShared') === undefined ? true : $(select).data('withShared'); - this.includeProjectsInSubgroups = $(select).data('includeProjectsInSubgroups') || false; - this.allowClear = $(select).data('allowClear') || false; + import(/* webpackChunkName: 'select2' */ 'select2/select2') + .then(() => { + $('.ajax-project-select').each(function(i, select) { + var placeholder; + const simpleFilter = $(select).data('simpleFilter') || false; + this.groupId = $(select).data('groupId'); + this.includeGroups = $(select).data('includeGroups'); + this.allProjects = $(select).data('allProjects') || false; + this.orderBy = $(select).data('orderBy') || 'id'; + this.withIssuesEnabled = $(select).data('withIssuesEnabled'); + this.withMergeRequestsEnabled = $(select).data('withMergeRequestsEnabled'); + this.withShared = + $(select).data('withShared') === undefined ? true : $(select).data('withShared'); + this.includeProjectsInSubgroups = $(select).data('includeProjectsInSubgroups') || false; + this.allowClear = $(select).data('allowClear') || false; - placeholder = 'Search for project'; - if (this.includeGroups) { - placeholder += ' or group'; - } + placeholder = 'Search for project'; + if (this.includeGroups) { + placeholder += ' or group'; + } - $(select).select2({ - placeholder: placeholder, - minimumInputLength: 0, - query: (function(_this) { - return function(query) { - var finalCallback, projectsCallback; - finalCallback = function(projects) { - var data; - data = { - results: projects, - }; - return query.callback(data); - }; - if (_this.includeGroups) { - projectsCallback = function(projects) { - var groupsCallback; - groupsCallback = function(groups) { + $(select).select2({ + placeholder: placeholder, + minimumInputLength: 0, + query: (function(_this) { + return function(query) { + var finalCallback, projectsCallback; + finalCallback = function(projects) { var data; - data = groups.concat(projects); - return finalCallback(data); + data = { + results: projects, + }; + return query.callback(data); }; - return Api.groups(query.term, {}, groupsCallback); + if (_this.includeGroups) { + projectsCallback = function(projects) { + var groupsCallback; + groupsCallback = function(groups) { + var data; + data = groups.concat(projects); + return finalCallback(data); + }; + return Api.groups(query.term, {}, groupsCallback); + }; + } else { + projectsCallback = finalCallback; + } + if (_this.groupId) { + return Api.groupProjects( + _this.groupId, + query.term, + { + with_issues_enabled: _this.withIssuesEnabled, + with_merge_requests_enabled: _this.withMergeRequestsEnabled, + with_shared: _this.withShared, + include_subgroups: _this.includeProjectsInSubgroups, + }, + projectsCallback, + ); + } else { + return Api.projects( + query.term, + { + order_by: _this.orderBy, + with_issues_enabled: _this.withIssuesEnabled, + with_merge_requests_enabled: _this.withMergeRequestsEnabled, + membership: !_this.allProjects, + }, + projectsCallback, + ); + } }; - } else { - projectsCallback = finalCallback; - } - if (_this.groupId) { - return Api.groupProjects( - _this.groupId, - query.term, - { - with_issues_enabled: _this.withIssuesEnabled, - with_merge_requests_enabled: _this.withMergeRequestsEnabled, - with_shared: _this.withShared, - include_subgroups: _this.includeProjectsInSubgroups, - }, - projectsCallback, - ); - } else { - return Api.projects( - query.term, - { - order_by: _this.orderBy, - with_issues_enabled: _this.withIssuesEnabled, - with_merge_requests_enabled: _this.withMergeRequestsEnabled, - membership: !_this.allProjects, - }, - projectsCallback, - ); - } - }; - })(this), - id: function(project) { - if (simpleFilter) return project.id; - return JSON.stringify({ - name: project.name, - url: project.web_url, - }); - }, - text: function(project) { - return project.name_with_namespace || project.name; - }, + })(this), + id: function(project) { + if (simpleFilter) return project.id; + return JSON.stringify({ + name: project.name, + url: project.web_url, + }); + }, + text: function(project) { + return project.name_with_namespace || project.name; + }, - initSelection: function(el, callback) { - return Api.project(el.val()).then(({ data }) => callback(data)); - }, + initSelection: function(el, callback) { + return Api.project(el.val()).then(({ data }) => callback(data)); + }, - allowClear: this.allowClear, + allowClear: this.allowClear, - dropdownCssClass: 'ajax-project-dropdown', - }); - if (simpleFilter) return select; - return new ProjectSelectComboButton(select); - }); + dropdownCssClass: 'ajax-project-dropdown', + }); + if (simpleFilter) return select; + return new ProjectSelectComboButton(select); + }); + }) + .catch(() => {}); } diff --git a/app/assets/javascripts/project_select_combo_button.js b/app/assets/javascripts/project_select_combo_button.js index 3dbac3ff942..d3b5f532dc1 100644 --- a/app/assets/javascripts/project_select_combo_button.js +++ b/app/assets/javascripts/project_select_combo_button.js @@ -44,9 +44,13 @@ export default class ProjectSelectComboButton { // eslint-disable-next-line class-methods-use-this openDropdown(event) { - $(event.currentTarget) - .siblings('.project-item-select') - .select2('open'); + import(/* webpackChunkName: 'select2' */ 'select2/select2') + .then(() => { + $(event.currentTarget) + .siblings('.project-item-select') + .select2('open'); + }) + .catch(() => {}); } selectProject() { 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/users_select.js b/app/assets/javascripts/users_select.js index ce051582299..4017630d6ef 100644 --- a/app/assets/javascripts/users_select.js +++ b/app/assets/javascripts/users_select.js @@ -579,101 +579,109 @@ function UsersSelect(currentUser, els, options = {}) { }; })(this), ); - $('.ajax-users-select').each( - (function(_this) { - return function(i, select) { - var firstUser, showAnyUser, showEmailUser, showNullUser; - var options = {}; - options.skipLdap = $(select).hasClass('skip_ldap'); - options.projectId = $(select).data('projectId'); - options.groupId = $(select).data('groupId'); - options.showCurrentUser = $(select).data('currentUser'); - options.authorId = $(select).data('authorId'); - options.skipUsers = $(select).data('skipUsers'); - showNullUser = $(select).data('nullUser'); - showAnyUser = $(select).data('anyUser'); - showEmailUser = $(select).data('emailUser'); - firstUser = $(select).data('firstUser'); - return $(select).select2({ - placeholder: 'Search for a user', - multiple: $(select).hasClass('multiselect'), - minimumInputLength: 0, - query: function(query) { - return _this.users(query.term, options, function(users) { - var anyUser, data, emailUser, index, len, name, nullUser, obj, ref; - data = { - results: users, - }; - if (query.term.length === 0) { - if (firstUser) { - // Move current user to the front of the list - ref = data.results; - - for (index = 0, len = ref.length; index < len; index += 1) { - obj = ref[index]; - if (obj.username === firstUser) { - data.results.splice(index, 1); - data.results.unshift(obj); - break; + import(/* webpackChunkName: 'select2' */ 'select2/select2') + .then(() => { + $('.ajax-users-select').each( + (function(_this) { + return function(i, select) { + var firstUser, showAnyUser, showEmailUser, showNullUser; + var options = {}; + options.skipLdap = $(select).hasClass('skip_ldap'); + options.projectId = $(select).data('projectId'); + options.groupId = $(select).data('groupId'); + options.showCurrentUser = $(select).data('currentUser'); + options.authorId = $(select).data('authorId'); + options.skipUsers = $(select).data('skipUsers'); + showNullUser = $(select).data('nullUser'); + showAnyUser = $(select).data('anyUser'); + showEmailUser = $(select).data('emailUser'); + firstUser = $(select).data('firstUser'); + return $(select).select2({ + placeholder: 'Search for a user', + multiple: $(select).hasClass('multiselect'), + minimumInputLength: 0, + query: function(query) { + return _this.users(query.term, options, function(users) { + var anyUser, data, emailUser, index, len, name, nullUser, obj, ref; + data = { + results: users, + }; + if (query.term.length === 0) { + if (firstUser) { + // Move current user to the front of the list + ref = data.results; + + for (index = 0, len = ref.length; index < len; index += 1) { + obj = ref[index]; + if (obj.username === firstUser) { + data.results.splice(index, 1); + data.results.unshift(obj); + break; + } + } + } + if (showNullUser) { + nullUser = { + name: 'Unassigned', + id: 0, + }; + data.results.unshift(nullUser); + } + if (showAnyUser) { + name = showAnyUser; + if (name === true) { + name = 'Any User'; + } + anyUser = { + name: name, + id: null, + }; + data.results.unshift(anyUser); } } - } - if (showNullUser) { - nullUser = { - name: 'Unassigned', - id: 0, - }; - data.results.unshift(nullUser); - } - if (showAnyUser) { - name = showAnyUser; - if (name === true) { - name = 'Any User'; + if ( + showEmailUser && + data.results.length === 0 && + query.term.match(/^[^@]+@[^@]+$/) + ) { + var trimmed = query.term.trim(); + emailUser = { + name: 'Invite "' + trimmed + '" by email', + username: trimmed, + id: trimmed, + invite: true, + }; + data.results.unshift(emailUser); } - anyUser = { - name: name, - id: null, - }; - data.results.unshift(anyUser); - } - } - if (showEmailUser && data.results.length === 0 && query.term.match(/^[^@]+@[^@]+$/)) { - var trimmed = query.term.trim(); - emailUser = { - name: 'Invite "' + trimmed + '" by email', - username: trimmed, - id: trimmed, - invite: true, - }; - data.results.unshift(emailUser); - } - return query.callback(data); + return query.callback(data); + }); + }, + initSelection: function() { + var args; + args = 1 <= arguments.length ? [].slice.call(arguments, 0) : []; + return _this.initSelection.apply(_this, args); + }, + formatResult: function() { + var args; + args = 1 <= arguments.length ? [].slice.call(arguments, 0) : []; + return _this.formatResult.apply(_this, args); + }, + formatSelection: function() { + var args; + args = 1 <= arguments.length ? [].slice.call(arguments, 0) : []; + return _this.formatSelection.apply(_this, args); + }, + dropdownCssClass: 'ajax-users-dropdown', + // we do not want to escape markup since we are displaying html in results + escapeMarkup: function(m) { + return m; + }, }); - }, - initSelection: function() { - var args; - args = 1 <= arguments.length ? [].slice.call(arguments, 0) : []; - return _this.initSelection.apply(_this, args); - }, - formatResult: function() { - var args; - args = 1 <= arguments.length ? [].slice.call(arguments, 0) : []; - return _this.formatResult.apply(_this, args); - }, - formatSelection: function() { - var args; - args = 1 <= arguments.length ? [].slice.call(arguments, 0) : []; - return _this.formatSelection.apply(_this, args); - }, - dropdownCssClass: 'ajax-users-dropdown', - // we do not want to escape markup since we are displaying html in results - escapeMarkup: function(m) { - return m; - }, - }); - }; - })(this), - ); + }; + })(this), + ); + }) + .catch(() => {}); } UsersSelect.prototype.initSelection = function(element, callback) { diff --git a/app/assets/javascripts/vue_merge_request_widget/components/source_branch_removal_status.vue b/app/assets/javascripts/vue_merge_request_widget/components/source_branch_removal_status.vue index dd940548e30..780ecdcdac4 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/source_branch_removal_status.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/source_branch_removal_status.vue @@ -7,7 +7,7 @@ export default { tooltip, }, created() { - this.removesBranchText = __('<strong>Removes</strong> source branch'); + this.removesBranchText = __('<strong>Deletes</strong> source branch'); this.tooltipTitle = __('A user with write access to the source branch selected this option'); }, }; diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.vue index 02c76db4a50..1b3af2fccf2 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.vue @@ -106,11 +106,11 @@ export default { <a :href="mr.targetBranchPath" class="label-branch"> {{ mr.targetBranch }} </a> </p> <p v-if="mr.shouldRemoveSourceBranch"> - {{ s__('mrWidget|The source branch will be removed') }} + {{ s__('mrWidget|The source branch will be deleted') }} </p> <p v-else class="d-flex align-items-start"> <span class="append-right-10"> - {{ s__('mrWidget|The source branch will not be removed') }} + {{ s__('mrWidget|The source branch will not be deleted') }} </span> <a v-if="canRemoveSourceBranch" @@ -121,7 +121,7 @@ export default { @click.prevent="removeSourceBranch" > <i v-if="isRemovingSourceBranch" class="fa fa-spinner fa-spin" aria-hidden="true"> </i> - {{ s__('mrWidget|Remove source branch') }} + {{ s__('mrWidget|Delete source branch') }} </a> </p> </section> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue index fe83fe58b67..b9562fbc260 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue @@ -84,7 +84,7 @@ export default { .removeSourceBranch() .then(res => res.data) .then(data => { - if (data.message === 'Branch was removed') { + if (data.message === 'Branch was deleted') { eventHub.$emit('MRWidgetUpdateRequested', () => { this.isMakingRequest = false; }); @@ -174,22 +174,22 @@ export default { </template> </p> <p v-if="mr.sourceBranchRemoved"> - {{ s__('mrWidget|The source branch has been removed') }} + {{ s__('mrWidget|The source branch has been deleted') }} </p> <p v-if="shouldShowRemoveSourceBranch" class="space-children"> - <span>{{ s__('mrWidget|You can remove source branch now') }}</span> + <span>{{ s__('mrWidget|You can delete the source branch now') }}</span> <button :disabled="isMakingRequest" type="button" class="btn btn-sm btn-default js-remove-branch-button" @click="removeSourceBranch" > - {{ s__('mrWidget|Remove Source Branch') }} + {{ s__('mrWidget|Delete source branch') }} </button> </p> <p v-if="shouldShowSourceBranchRemoving"> <gl-loading-icon :inline="true" /> - <span> {{ s__('mrWidget|The source branch is being removed') }} </span> + <span> {{ s__('mrWidget|The source branch is being deleted') }} </span> </p> </section> </div> 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 5df891aebf3..b8f29649eb5 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 @@ -223,7 +223,7 @@ export default { } }) .catch(() => { - new Flash('Something went wrong while removing the source branch. Please try again.'); // eslint-disable-line + new Flash('Something went wrong while deleting the source branch. Please try again.'); // eslint-disable-line }); }, }, @@ -241,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 }} @@ -265,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> @@ -277,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> @@ -297,7 +297,7 @@ export default { class="js-remove-source-branch-checkbox" type="checkbox" /> - Remove source branch + Delete source branch </label> <!-- Placeholder for EE extension of this component --> 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 bbb9491d6cf..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 @@ -37,7 +37,7 @@ export default { type="checkbox" name="squash" class="qa-squash-checkbox" - @change="$emit('input', $event.target.checked);" + @change="$emit('input', $event.target.checked)" /> {{ __('Squash commits') }} </label> diff --git a/app/assets/javascripts/vue_merge_request_widget/ee_switch_mr_widget_options.js b/app/assets/javascripts/vue_merge_request_widget/ee_switch_mr_widget_options.js deleted file mode 100644 index 8780aa4bd1c..00000000000 --- a/app/assets/javascripts/vue_merge_request_widget/ee_switch_mr_widget_options.js +++ /dev/null @@ -1,3 +0,0 @@ -import MRWidgetOptions from './mr_widget_options.vue'; - -export default MRWidgetOptions; diff --git a/app/assets/javascripts/vue_merge_request_widget/index.js b/app/assets/javascripts/vue_merge_request_widget/index.js index 60cebbfc2b2..0cedbdbdfef 100644 --- a/app/assets/javascripts/vue_merge_request_widget/index.js +++ b/app/assets/javascripts/vue_merge_request_widget/index.js @@ -1,5 +1,5 @@ import Vue from 'vue'; -import MrWidgetOptions from './ee_switch_mr_widget_options'; +import MrWidgetOptions from 'ee_else_ce/vue_merge_request_widget/mr_widget_options.vue'; import Translate from '../vue_shared/translate'; Vue.use(Translate); 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 5a9d86594b1..0ce9d271845 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 @@ -3,6 +3,9 @@ import _ from 'underscore'; import { __ } from '~/locale'; import Project from '~/pages/projects/project'; import SmartInterval from '~/smart_interval'; +import MRWidgetStore from 'ee_else_ce/vue_merge_request_widget/stores/mr_widget_store'; +import MRWidgetService from 'ee_else_ce/vue_merge_request_widget/services/mr_widget_service'; +import stateMaps from 'ee_else_ce/vue_merge_request_widget/stores/state_maps'; import createFlash from '../flash'; import WidgetHeader from './components/mr_widget_header.vue'; import WidgetMergeHelp from './components/mr_widget_merge_help.vue'; @@ -28,10 +31,7 @@ import FailedToMerge from './components/states/mr_widget_failed_to_merge.vue'; import MergeWhenPipelineSucceedsState from './components/states/mr_widget_merge_when_pipeline_succeeds.vue'; import AutoMergeFailed from './components/states/mr_widget_auto_merge_failed.vue'; import CheckingState from './components/states/mr_widget_checking.vue'; -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 notify from '~/lib/utils/notify'; import SourceBranchRemovalStatus from './components/source_branch_removal_status.vue'; import GroupedTestReportsApp from '../reports/components/grouped_test_reports_app.vue'; diff --git a/app/assets/javascripts/vue_merge_request_widget/services/ee_switch_mr_widget_service.js b/app/assets/javascripts/vue_merge_request_widget/services/ee_switch_mr_widget_service.js deleted file mode 100644 index ea2aabb78fe..00000000000 --- a/app/assets/javascripts/vue_merge_request_widget/services/ee_switch_mr_widget_service.js +++ /dev/null @@ -1,3 +0,0 @@ -import MRWidgetService from './mr_widget_service'; - -export default MRWidgetService; diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/ee_switch_get_state_key.js b/app/assets/javascripts/vue_merge_request_widget/stores/ee_switch_get_state_key.js deleted file mode 100644 index ebef30e3eab..00000000000 --- a/app/assets/javascripts/vue_merge_request_widget/stores/ee_switch_get_state_key.js +++ /dev/null @@ -1,3 +0,0 @@ -import getStateKey from './get_state_key'; - -export default getStateKey; diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/ee_switch_mr_widget_store.js b/app/assets/javascripts/vue_merge_request_widget/stores/ee_switch_mr_widget_store.js deleted file mode 100644 index 92a07c53f2d..00000000000 --- a/app/assets/javascripts/vue_merge_request_widget/stores/ee_switch_mr_widget_store.js +++ /dev/null @@ -1,3 +0,0 @@ -import MergeRequestStore from './mr_widget_store'; - -export default MergeRequestStore; diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/ee_switch_state_maps.js b/app/assets/javascripts/vue_merge_request_widget/stores/ee_switch_state_maps.js deleted file mode 100644 index 50cf9503ea7..00000000000 --- a/app/assets/javascripts/vue_merge_request_widget/stores/ee_switch_state_maps.js +++ /dev/null @@ -1,3 +0,0 @@ -import stateMaps from './state_maps'; - -export default stateMaps; diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js index e5a52c6a7f6..ab194e84ab4 100644 --- a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js +++ b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js @@ -1,5 +1,5 @@ import Timeago from 'timeago.js'; -import getStateKey from './ee_switch_get_state_key'; +import getStateKey from 'ee_else_ce/vue_merge_request_widget/stores/get_state_key'; import { stateKey } from './state_maps'; import { formatDate } from '../../lib/utils/datetime_utility'; 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/filtered_search_dropdown.vue b/app/assets/javascripts/vue_shared/components/filtered_search_dropdown.vue index 834c39a5ee0..4e5dfbf3bf8 100644 --- a/app/assets/javascripts/vue_shared/components/filtered_search_dropdown.vue +++ b/app/assets/javascripts/vue_shared/components/filtered_search_dropdown.vue @@ -1,15 +1,21 @@ <script> import $ from 'jquery'; +import { GlButton } from '@gitlab/ui'; +import { __ } from '~/locale'; import Icon from '~/vue_shared/components/icon.vue'; /** * Renders a split dropdown with * an input that allows to search through the given * array of options. + * + * When there are no results and `showCreateMode` is true + * it renders a create button with the value typed. */ export default { name: 'FilteredSearchDropdown', components: { Icon, + GlButton, }, props: { title: { @@ -43,6 +49,16 @@ export default { type: String, required: true, }, + showCreateMode: { + type: Boolean, + required: false, + default: false, + }, + createButtonText: { + type: String, + required: false, + default: __('Create'), + }, }, data() { return { @@ -64,6 +80,12 @@ export default { return this.items.slice(0, this.visibleItems); }, + computedCreateButtonText() { + return `${this.createButtonText} ${this.filter}`; + }, + shouldRenderCreateButton() { + return this.showCreateMode && this.filteredResults.length === 0 && this.filter !== ''; + }, }, mounted() { /** @@ -112,10 +134,20 @@ export default { <div class="dropdown-content"> <ul> <li v-for="(result, i) in filteredResults" :key="i" class="js-filtered-dropdown-result"> - <slot name="result" :result="result"> {{ result[filterKey] }} </slot> + <slot name="result" :result="result">{{ result[filterKey] }}</slot> </li> </ul> </div> + + <div v-if="shouldRenderCreateButton" class="dropdown-footer"> + <slot name="footer" :filter="filter"> + <gl-button + class="js-dropdown-create-button btn-transparent" + @click="$emit('createItem', filter)" + >{{ computedCreateButtonText }}</gl-button + > + </slot> + </div> </div> </div> </div> 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/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.vue b/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff.vue index b9f884074d0..a351ca62c94 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff.vue @@ -42,7 +42,7 @@ export default { </script> <template> - <div> + <div class="md-suggestion"> <suggestion-diff-header class="qa-suggestion-diff-header" :can-apply="suggestion.appliable && suggestion.current_user.can_apply && !disabled" diff --git a/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue b/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue index 721f0276ac8..c33665c24f6 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 v-show="isRendered" ref="container" v-html="noteHtml"></div> + <div class="flash-container js-suggestions-flash"></div> + <div v-show="isRendered" ref="container" class="note-text md" 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_image.vue b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue index 95f4395ac13..a6c1737dcab 100644 --- a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue +++ b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue @@ -68,7 +68,8 @@ export default { sanitizedSource() { let baseSrc = this.imgSrc === '' || this.imgSrc === null ? defaultAvatarUrl : this.imgSrc; // Only adds the width to the URL if its not a base64 data image - if (!baseSrc.startsWith('data:') && !baseSrc.includes('?')) baseSrc += `?width=${this.size}`; + if (!(baseSrc.indexOf('data:') === 0) && !baseSrc.includes('?')) + baseSrc += `?width=${this.size}`; return baseSrc; }, resultantSrcAttribute() { 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/vue_shared/components/user_popover/user_popover.vue b/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue index d24fe1b547e..f9773622001 100644 --- a/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue +++ b/app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue @@ -28,10 +28,10 @@ export default { }, computed: { statusHtml() { - if (this.user.status.emoji && this.user.status.message) { - return `${glEmojiTag(this.user.status.emoji)} ${this.user.status.message}`; - } else if (this.user.status.message) { - return this.user.status.message; + if (this.user.status.emoji && this.user.status.message_html) { + return `${glEmojiTag(this.user.status.emoji)} ${this.user.status.message_html}`; + } else if (this.user.status.message_html) { + return this.user.status.message_html; } return ''; }, 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/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/buttons.scss b/app/assets/stylesheets/framework/buttons.scss index 5d2cbdde8dc..d164cc56e44 100644 --- a/app/assets/stylesheets/framework/buttons.scss +++ b/app/assets/stylesheets/framework/buttons.scss @@ -42,6 +42,10 @@ color: $text; border-color: $border; + &.btn-border-color { + border-color: $border-color; + } + > .icon { color: $text; } diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index a499a3a9f95..08d84f7748f 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -381,6 +381,7 @@ img.emoji { .inline { display: inline-block; } .center { text-align: center; } .vertical-align-middle { vertical-align: middle; } +.vertical-align-sub { vertical-align: sub; } .flex-align-self-center { align-self: center; } .flex-grow { flex-grow: 1; } .flex-no-shrink { flex-shrink: 0; } @@ -408,3 +409,20 @@ img.emoji { .gl-pr-3 { padding-right: #{2 * $grid-size}; } .gl-pr-4 { padding-right: #{3 * $grid-size}; } .gl-pr-5 { padding-right: #{4 * $grid-size}; } + +/** + * Removes browser specific clear icon from input fields in + * Internet Explorer 10, Internet Explorer 11, and Microsoft Edge. + * This is intended for elements which add a customized clear icon. + * + * see also https://developer.mozilla.org/en-US/docs/Web/CSS/::-ms-clear + */ +.ms-no-clear ::-ms-clear { + display: none; +} + +/** COMMON POSITIONING CLASSES */ +.position-bottom-0 { bottom: 0; } +.position-left-0 { left: 0; } +.position-right-0 { right: 0; } +.position-top-0 { top: 0; } 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/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss index 679148ddf7b..f708a26bb32 100644 --- a/app/assets/stylesheets/framework/markdown_area.scss +++ b/app/assets/stylesheets/framework/markdown_area.scss @@ -280,6 +280,8 @@ .md-suggestion-diff { display: table !important; border: 1px solid $border-color !important; + width: 100% !important; + font-family: $monospace-font !important; } .md-suggestion-header { diff --git a/app/assets/stylesheets/framework/responsive_tables.scss b/app/assets/stylesheets/framework/responsive_tables.scss index 29a9c076cdf..6bd44ee19bd 100644 --- a/app/assets/stylesheets/framework/responsive_tables.scss +++ b/app/assets/stylesheets/framework/responsive_tables.scss @@ -39,7 +39,7 @@ .table-section { white-space: nowrap; - $section-widths: 5 10 15 20 25 30 40 50 60 100; + $section-widths: 5 10 15 20 25 30 40 50 60 70 80 100; @each $width in $section-widths { &.section-#{$width} { flex: 0 0 #{$width + '%'}; diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss index 45dab036d35..a08639936c0 100644 --- a/app/assets/stylesheets/framework/typography.scss +++ b/app/assets/stylesheets/framework/typography.scss @@ -148,6 +148,7 @@ @extend .table-bordered; margin: 16px 0; color: $gl-text-color; + border: 0; th { background: $label-gray-bg; diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index c1666c728f3..9eae9a831fa 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -278,8 +278,8 @@ $performance-bar-height: 35px; $flash-height: 52px; $context-header-height: 60px; $breadcrumb-min-height: 48px; -$project-title-row-height: 64px; -$project-avatar-mobile-size: 24px; +$home-panel-title-row-height: 64px; +$home-panel-avatar-mobile-size: 24px; $gl-line-height: 16px; $gl-line-height-24: 24px; $gl-line-height-14: 14px; @@ -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 d5f8e3fb4ee..1dfe2a69a2f 100644 --- a/app/assets/stylesheets/framework/variables_overrides.scss +++ b/app/assets/stylesheets/framework/variables_overrides.scss @@ -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/highlight/none.scss b/app/assets/stylesheets/highlight/none.scss index 7d692a87e33..7ced4e82e66 100644 --- a/app/assets/stylesheets/highlight/none.scss +++ b/app/assets/stylesheets/highlight/none.scss @@ -38,7 +38,7 @@ $none-over-bg: #ded7fc; $none-expanded-border: #e0e0e0; - $none-expanded-bg: #f7f7f7; + $none-expanded-bg: #e0e0e0; .line_holder { @@ -50,18 +50,12 @@ .diff-line-num { &.old { - background-color: $line-number-old; - border-color: $line-removed-dark; - a { color: scale-color($line-number-old, $red: -30%, $green: -30%, $blue: -30%); } } &.new { - background-color: $line-number-new; - border-color: $line-added-dark; - a { color: scale-color($line-number-new, $red: -30%, $green: -30%, $blue: -30%); } @@ -78,8 +72,8 @@ } &.hll:not(.empty-cell) { - background-color: $line-number-select; - border-color: $line-select-yellow-dark; + background-color: $white-light; + border-color: $white-normal; } } @@ -101,26 +95,28 @@ .line_content { &.old { - background-color: $line-removed; + background-color: $white-normal; &::before { - color: scale-color($line-number-old, $red: -30%, $green: -30%, $blue: -30%); + color: $gl-text-color; } span.idiff { - background-color: $line-removed-dark; + background-color: $white-normal; + text-decoration: underline; } } &.new { - background-color: $line-added; + background-color: $white-normal; &::before { - color: scale-color($line-number-new, $red: -30%, $green: -30%, $blue: -30%); + color: $gl-text-color; } span.idiff { - background-color: $line-added-dark; + background-color: $white-normal; + text-decoration: underline; } } @@ -129,7 +125,7 @@ } &.hll:not(.empty-cell) { - background-color: $line-select-yellow; + background-color: $white-normal; } } } diff --git a/app/assets/stylesheets/page_bundles/ide.scss b/app/assets/stylesheets/page_bundles/ide.scss index 553cc44fe83..1f24b8dfa9e 100644 --- a/app/assets/stylesheets/page_bundles/ide.scss +++ b/app/assets/stylesheets/page_bundles/ide.scss @@ -395,6 +395,11 @@ $ide-commit-header-height: 48px; svg { vertical-align: sub; } + + .ide-status-avatar { + float: none; + margin: 0 0 1px; + } } .ide-status-file { diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss index e1d1e598da8..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 { diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index b78f11aadf1..02aac58a475 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}); } } @@ -1092,12 +1095,6 @@ } } -.tree-list-view-toggle { - svg { - top: 0; - } -} - .image-diff-overlay, .image-diff-overlay-add-comment { top: 0; diff --git a/app/assets/stylesheets/pages/groups.scss b/app/assets/stylesheets/pages/groups.scss index ebbb5beed81..8ade995525a 100644 --- a/app/assets/stylesheets/pages/groups.scss +++ b/app/assets/stylesheets/pages/groups.scss @@ -29,9 +29,7 @@ } } -.group-nav-container .group-search, .group-nav-container .nav-controls { - display: flex; align-items: flex-start; padding: $gl-padding-top 0 0; @@ -44,6 +42,52 @@ margin-top: 0; } + @include media-breakpoint-down(sm) { + .dropdown, + .dropdown .dropdown-toggle, + .btn-success { + display: block; + } + + .group-filter-form, + .dropdown { + margin-bottom: 10px; + margin-right: 0; + } + + &, + .group-filter-form, + .group-filter-form-field, + .dropdown, + .dropdown .dropdown-toggle, + .btn-success { + width: 100%; + } + + .dropdown .dropdown-toggle .fa-chevron-down { + position: absolute; + top: 11px; + right: 8px; + } + } +} + +.home-panel-buttons { + .home-panel-action-button { + vertical-align: top; + } + + + .notification-dropdown { + .dropdown-menu { + @extend .dropdown-menu-right; + } + + .icon { + fill: $gl-text-color-secondary; + } + } + .new-project-subgroup { .dropdown-primary { min-width: 115px; @@ -99,61 +143,29 @@ font-weight: $gl-font-weight-bold; } } - } - } - - @include media-breakpoint-down(sm) { - &, - .dropdown, - .dropdown .dropdown-toggle, - .btn-success { - display: block; - } - .group-filter-form, - .dropdown { - margin-bottom: 10px; - margin-right: 0; - } - - .group-filter-form, - .dropdown .dropdown-toggle, - .btn-success { - width: 100%; - } - - .dropdown .dropdown-toggle .fa-chevron-down { - position: absolute; - top: 11px; - right: 8px; - } - - .new-project-subgroup { - display: flex; - align-items: flex-start; + @include media-breakpoint-down(sm) { + display: flex; + align-items: flex-start; - .dropdown-primary { - flex: 1; - } + .dropdown-primary { + flex: 1; + } - .dropdown-toggle { - width: auto; - } + .dropdown-toggle { + width: auto; + } - .dropdown-menu { - width: 100%; - max-width: inherit; - min-width: inherit; + .dropdown-menu { + width: 100%; + max-width: inherit; + min-width: inherit; + } } } } } -.group-nav-container .group-search { - padding: $gl-padding 0; - border-bottom: 1px solid $border-color; -} - .groups-listing { .group-list-tree .group-row:first-child { border-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/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index 1e4b8d8b7e4..53afb182b54 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -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; } diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index 058b0ffef5f..e676d48c1f4 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; } } @@ -708,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() { @@ -734,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, @@ -761,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; - } - } } /** @@ -846,7 +861,7 @@ button.mini-pipeline-graph-dropdown-toggle { height: $ci-action-dropdown-svg-size; fill: $gl-text-color-secondary; position: relative; - top: 0; + top: 1px; vertical-align: initial; } } @@ -854,7 +869,7 @@ button.mini-pipeline-graph-dropdown-toggle { // SVGs in the commit widget and mr widget a.ci-action-icon-container.ci-action-icon-wrapper svg { - top: 2px; + top: 4px; } .scrollable-menu { diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index efd8b5f5ff3..2342c284a5e 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -140,73 +140,19 @@ } } -.project-home-panel, -.group-home-panel { - padding-top: 24px; - padding-bottom: 24px; - - .group-avatar { - float: none; - margin: 0 auto; - - &.identicon { - border-radius: 50%; - } - } - - .group-title { - margin-top: 10px; - margin-bottom: 10px; - font-size: 24px; - font-weight: $gl-font-weight-normal; - line-height: 1; - word-wrap: break-word; - - .fa { - margin-left: 2px; - font-size: 12px; - vertical-align: middle; - } - } - - .group-home-desc { - margin-left: auto; - margin-right: auto; - margin-bottom: 0; - max-width: 700px; - - > p { - margin-bottom: 0; - } - } - - .notifications-btn { - .fa-bell, - .fa-spinner { - margin-right: 6px; - } - - .fa-angle-down { - margin-left: 6px; - } - } -} - +.group-home-panel, .project-home-panel { padding-top: $gl-padding; padding-bottom: $gl-padding; - .project-avatar { - width: $project-title-row-height; - height: $project-title-row-height; + .home-panel-avatar { + width: $home-panel-title-row-height; + height: $home-panel-title-row-height; flex-shrink: 0; - flex-basis: $project-title-row-height; - margin: 0 $gl-padding 0 0; + flex-basis: $home-panel-title-row-height; } - .project-title { - margin-top: 8px; - margin-bottom: 5px; + .home-panel-title { font-size: 20px; line-height: $gl-line-height-24; font-weight: bold; @@ -215,11 +161,7 @@ font-size: $gl-font-size-large; } - .project-visibility { - color: $gl-text-color-secondary; - } - - .project-topic-list { + .home-panel-topic-list { font-size: $gl-font-size; font-weight: $gl-font-weight-normal; @@ -231,12 +173,12 @@ } } - .project-title-row { + .home-panel-title-row { @include media-breakpoint-down(sm) { - .project-avatar { - width: $project-avatar-mobile-size; - height: $project-avatar-mobile-size; - flex-basis: $project-avatar-mobile-size; + .home-panel-avatar { + width: $home-panel-avatar-mobile-size; + height: $home-panel-avatar-mobile-size; + flex-basis: $home-panel-avatar-mobile-size; .avatar { font-size: 20px; @@ -244,28 +186,26 @@ } } - .project-title { + .home-panel-title { margin-top: 4px; margin-bottom: 2px; font-size: $gl-font-size; line-height: $gl-font-size-large; } - .project-topic-list, - .project-metadata { + .home-panel-topic-list, + .home-panel-metadata { font-size: $gl-font-size-small; } } } - .project-metadata { + .home-panel-metadata { font-weight: normal; font-size: 14px; line-height: $gl-btn-line-height; - color: $gl-text-color-secondary; - - .project-license { + .home-panel-license { .btn { line-height: 0; border-width: 0; @@ -273,13 +213,13 @@ } .access-request-link, - .project-topic-list { + .home-panel-topic-list { padding-left: $gl-padding-8; border-left: 1px solid $gl-text-color-secondary; } } - .project-description { + .home-panel-description { @include media-breakpoint-up(md) { font-size: $gl-font-size-large; } @@ -292,12 +232,11 @@ } } -.nav > .project-repo-buttons { +.nav > .project-buttons { margin-top: 0; } -.project-repo-buttons, -.group-buttons { +.project-repo-buttons { .btn { &:last-child { margin-left: 0; @@ -318,8 +257,30 @@ margin-left: 0; } } + + .notifications-icon { + top: 1px; + margin-right: 0; + } } + .icon { + top: 0; + } + + .count-badge, + .btn-xs { + height: 24px; + } + + .dropdown-toggle, + .clone-dropdown-btn { + .fa { + color: unset; + } + } + + .home-panel-action-button, .project-action-button { margin: $gl-padding $gl-padding-8 0 0; vertical-align: top; @@ -385,31 +346,6 @@ } } -.project-repo-buttons { - .icon { - top: 0; - } - - .count-badge, - .btn-xs { - height: 24px; - } - - .dropdown-toggle, - .clone-dropdown-btn { - .fa { - color: unset; - } - } - - .btn { - .notifications-icon { - top: 1px; - margin-right: 0; - } - } -} - .split-one { display: inline-table; margin-right: 12px; @@ -772,9 +708,6 @@ .project-stats, .project-buttons { - font-size: 0; - text-align: center; - .scrolling-tabs-container { .scrolling-tabs { margin-top: $gl-padding-8; @@ -1025,8 +958,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/settings.scss b/app/assets/stylesheets/pages/settings.scss index c5b9d1f6885..811cc310a8f 100644 --- a/app/assets/stylesheets/pages/settings.scss +++ b/app/assets/stylesheets/pages/settings.scss @@ -167,12 +167,14 @@ font-weight: $gl-font-weight-normal; display: inline-block; color: $gl-text-color; + vertical-align: top; } .option-description, .option-disabled-reason { margin-left: 30px; color: $project-option-descr-color; + margin-top: -5px; } .option-disabled-reason { 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/boards_responses.rb b/app/controllers/concerns/boards_responses.rb index 3cdf4ddf8bb..8b191c86397 100644 --- a/app/controllers/concerns/boards_responses.rb +++ b/app/controllers/concerns/boards_responses.rb @@ -34,15 +34,11 @@ module BoardsResponses end def authorize_read_list - ability = board.group_board? ? :read_group : :read_list - - authorize_action_for!(board.parent, ability) + authorize_action_for!(board, :read_list) end def authorize_read_issue - ability = board.group_board? ? :read_group : :read_issue - - authorize_action_for!(board.parent, ability) + authorize_action_for!(board, :read_issue) end def authorize_update_issue @@ -57,7 +53,7 @@ module BoardsResponses end def authorize_admin_list - authorize_action_for!(board.parent, :admin_list) + authorize_action_for!(board, :admin_list) end def authorize_action_for!(resource, ability) diff --git a/app/controllers/concerns/issuable_collections.rb b/app/controllers/concerns/issuable_collections.rb index 789e0dc736e..07d0bf16d93 100644 --- a/app/controllers/concerns/issuable_collections.rb +++ b/app/controllers/concerns/issuable_collections.rb @@ -129,13 +129,13 @@ module IssuableCollections return sort_param if Gitlab::Database.read_only? if user_preference[issuable_sorting_field] != sort_param - user_preference.update_attribute(issuable_sorting_field, sort_param) + user_preference.update(issuable_sorting_field => sort_param) end sort_param end - # Implement default_sorting_field method on controllers + # Implement issuable_sorting_field method on controllers # to choose which column to store the sorting parameter. def issuable_sorting_field nil diff --git a/app/controllers/concerns/issues_action.rb b/app/controllers/concerns/issuable_collections_action.rb index a75590457d6..18ed4027eac 100644 --- a/app/controllers/concerns/issues_action.rb +++ b/app/controllers/concerns/issuable_collections_action.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -module IssuesAction +module IssuableCollectionsAction extend ActiveSupport::Concern include IssuableCollections include IssuesCalendar @@ -18,6 +18,12 @@ module IssuesAction format.atom { render layout: 'xml.atom' } end end + + def merge_requests + @merge_requests = issuables_collection.page(params[:page]) + + @issuable_meta_data = issuable_meta_data(@merge_requests, collection_type) + end # rubocop:enable Gitlab/ModuleWithInstanceVariables def issues_calendar @@ -26,8 +32,29 @@ module IssuesAction private + def issuable_sorting_field + case action_name + when 'issues' + Issue::SORTING_PREFERENCE_FIELD + when 'merge_requests' + MergeRequest::SORTING_PREFERENCE_FIELD + else + nil + end + end + def finder_type - (super if defined?(super)) || - (IssuesFinder if %w(issues issues_calendar).include?(action_name)) + case action_name + when 'issues', 'issues_calendar' + IssuesFinder + when 'merge_requests' + MergeRequestsFinder + else + nil + end + end + + def finder_options + super.merge(non_archived: true) end end diff --git a/app/controllers/concerns/membership_actions.rb b/app/controllers/concerns/membership_actions.rb index ca713192c9e..6402e01ddc0 100644 --- a/app/controllers/concerns/membership_actions.rb +++ b/app/controllers/concerns/membership_actions.rb @@ -35,7 +35,9 @@ module MembershipActions respond_to do |format| format.html do - message = "User was successfully removed from #{source_type}." + source = source_type == 'group' ? 'group and any subresources' : source_type + + message = "User was successfully removed from #{source}." redirect_to members_page_url, notice: message end diff --git a/app/controllers/concerns/merge_requests_action.rb b/app/controllers/concerns/merge_requests_action.rb deleted file mode 100644 index ed10f32512e..00000000000 --- a/app/controllers/concerns/merge_requests_action.rb +++ /dev/null @@ -1,25 +0,0 @@ -# frozen_string_literal: true - -module MergeRequestsAction - extend ActiveSupport::Concern - include IssuableCollections - - # rubocop:disable Gitlab/ModuleWithInstanceVariables - def merge_requests - @merge_requests = issuables_collection.page(params[:page]) - - @issuable_meta_data = issuable_meta_data(@merge_requests, collection_type) - end - # rubocop:enable Gitlab/ModuleWithInstanceVariables - - private - - def finder_type - (super if defined?(super)) || - (MergeRequestsFinder if action_name == 'merge_requests') - end - - def finder_options - super.merge(non_archived: true) - end -end 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/milestones_controller.rb b/app/controllers/dashboard/milestones_controller.rb index 3802aa5f40f..9484e4d30cd 100644 --- a/app/controllers/dashboard/milestones_controller.rb +++ b/app/controllers/dashboard/milestones_controller.rb @@ -27,7 +27,7 @@ class Dashboard::MilestonesController < Dashboard::ApplicationController def group_milestones groups = GroupsFinder.new(current_user, all_available: false).execute - DashboardGroupMilestone.build_collection(groups) + DashboardGroupMilestone.build_collection(groups, params) end # See [#39545](https://gitlab.com/gitlab-org/gitlab-ce/issues/39545) for info about the deprecation of dynamic milestones 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/dashboard_controller.rb b/app/controllers/dashboard_controller.rb index be2d9512c01..75329b05a6f 100644 --- a/app/controllers/dashboard_controller.rb +++ b/app/controllers/dashboard_controller.rb @@ -1,8 +1,7 @@ # frozen_string_literal: true class DashboardController < Dashboard::ApplicationController - include IssuesAction - include MergeRequestsAction + include IssuableCollectionsAction prepend_before_action(only: [:issues]) { authenticate_sessionless_user!(:rss) } prepend_before_action(only: [:issues_calendar]) { authenticate_sessionless_user!(:ics) } diff --git a/app/controllers/explore/projects_controller.rb b/app/controllers/explore/projects_controller.rb index 778fdda8dbd..f3d76c5a478 100644 --- a/app/controllers/explore/projects_controller.rb +++ b/app/controllers/explore/projects_controller.rb @@ -15,7 +15,7 @@ class Explore::ProjectsController < Explore::ApplicationController format.html format.json do render json: { - html: view_to_html_string("dashboard/projects/_projects", locals: { projects: @projects }) + html: view_to_html_string("explore/projects/_projects", locals: { projects: @projects }) } end end @@ -30,7 +30,7 @@ class Explore::ProjectsController < Explore::ApplicationController format.html format.json do render json: { - html: view_to_html_string("dashboard/projects/_projects", locals: { projects: @projects }) + html: view_to_html_string("explore/projects/_projects", locals: { projects: @projects }) } end end @@ -44,7 +44,7 @@ class Explore::ProjectsController < Explore::ApplicationController format.html format.json do render json: { - html: view_to_html_string("dashboard/projects/_projects", locals: { projects: @projects }) + html: view_to_html_string("explore/projects/_projects", locals: { projects: @projects }) } end end @@ -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/milestones_controller.rb b/app/controllers/groups/milestones_controller.rb index 868deea3f01..7ed4384089b 100644 --- a/app/controllers/groups/milestones_controller.rb +++ b/app/controllers/groups/milestones_controller.rb @@ -115,6 +115,6 @@ class Groups::MilestonesController < Groups::ApplicationController end def search_params - params.permit(:state).merge(group_ids: group.id) + params.permit(:state, :search_title).merge(group_ids: group.id) end end diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index c5d8ac2ed77..15aadf3f74b 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -2,8 +2,7 @@ class GroupsController < Groups::ApplicationController include API::Helpers::RelatedResourcesHelpers - include IssuesAction - include MergeRequestsAction + include IssuableCollectionsAction include ParamsBackwardCompatibility include PreviewMarkdown 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_controller.rb b/app/controllers/import/bitbucket_controller.rb index 1b30b4dda36..2b1395f364f 100644 --- a/app/controllers/import/bitbucket_controller.rb +++ b/app/controllers/import/bitbucket_controller.rb @@ -8,7 +8,7 @@ class Import::BitbucketController < Import::BaseController rescue_from Bitbucket::Error::Unauthorized, with: :bitbucket_unauthorized def callback - response = client.auth_code.get_token(params[:code], redirect_uri: callback_import_bitbucket_url) + response = client.auth_code.get_token(params[:code], redirect_uri: users_import_bitbucket_callback_url) session[:bitbucket_token] = response.token session[:bitbucket_expires_at] = response.expires_at @@ -89,7 +89,7 @@ class Import::BitbucketController < Import::BaseController end def go_to_bitbucket_for_permissions - redirect_to client.auth_code.authorize_url(redirect_uri: callback_import_bitbucket_url) + redirect_to client.auth_code.authorize_url(redirect_uri: users_import_bitbucket_callback_url) end def bitbucket_unauthorized diff --git a/app/controllers/import/bitbucket_server_controller.rb b/app/controllers/import/bitbucket_server_controller.rb index 87338488eba..f333e43b892 100644 --- a/app/controllers/import/bitbucket_server_controller.rb +++ b/app/controllers/import/bitbucket_server_controller.rb @@ -13,7 +13,10 @@ class Import::BitbucketServerController < Import::BaseController # Repository names are limited to 128 characters. They must start with a # letter or number and may contain spaces, hyphens, underscores, and periods. # (https://community.atlassian.com/t5/Answers-Developer-Questions/stash-repository-names/qaq-p/499054) - VALID_BITBUCKET_CHARS = /\A[\w\-_\.\s]+\z/ + # + # Bitbucket Server starts personal project names with a tilde. + VALID_BITBUCKET_PROJECT_CHARS = /\A~?[\w\-\.\s]+\z/ + VALID_BITBUCKET_CHARS = /\A[\w\-\.\s]+\z/ def new end @@ -91,7 +94,7 @@ class Import::BitbucketServerController < Import::BaseController return render_validation_error('Missing project key') unless @project_key.present? && @repo_slug.present? return render_validation_error('Missing repository slug') unless @repo_slug.present? - return render_validation_error('Invalid project key') unless @project_key =~ VALID_BITBUCKET_CHARS + return render_validation_error('Invalid project key') unless @project_key =~ VALID_BITBUCKET_PROJECT_CHARS return render_validation_error('Invalid repository slug') unless @repo_slug =~ VALID_BITBUCKET_CHARS end diff --git a/app/controllers/import/github_controller.rb b/app/controllers/import/github_controller.rb index d4c26fa0709..3fbc0817e95 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 @@ -86,7 +83,7 @@ class Import::GithubController < Import::BaseController end def callback_import_url - public_send("callback_import_#{provider}_url", extra_import_params) # rubocop:disable GitlabSecurity/PublicSend + public_send("users_import_#{provider}_callback_url", extra_import_params) # rubocop:disable GitlabSecurity/PublicSend end def provider_unauthorized @@ -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/notification_settings_controller.rb b/app/controllers/notification_settings_controller.rb index 384f308269a..43c4f4d220e 100644 --- a/app/controllers/notification_settings_controller.rb +++ b/app/controllers/notification_settings_controller.rb @@ -17,7 +17,8 @@ class NotificationSettingsController < ApplicationController @saved = @notification_setting.update(notification_setting_params_for(@notification_setting.source)) if params[:hide_label].present? - render_response("projects/buttons/_notifications") + btn_class = params[:project_id].present? ? 'btn-xs' : '' + render_response("shared/notifications/_new_button", btn_class) else render_response end @@ -41,9 +42,9 @@ class NotificationSettingsController < ApplicationController can?(current_user, ability_name, resource) end - def render_response(response_template = "shared/notifications/_button") + def render_response(response_template = "shared/notifications/_button", btn_class = nil) render json: { - html: view_to_html_string(response_template, notification_setting: @notification_setting), + html: view_to_html_string(response_template, notification_setting: @notification_setting, btn_class: btn_class), saved: @saved } end diff --git a/app/controllers/passwords_controller.rb b/app/controllers/passwords_controller.rb index 2912a22411e..28f113b5cbe 100644 --- a/app/controllers/passwords_controller.rb +++ b/app/controllers/passwords_controller.rb @@ -5,7 +5,7 @@ class PasswordsController < Devise::PasswordsController before_action :resource_from_email, only: [:create] before_action :check_password_authentication_available, only: [:create] - before_action :throttle_reset, only: [:create] + before_action :throttle_reset, only: [:create] # rubocop: disable CodeReuse/ActiveRecord def edit 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/commit_controller.rb b/app/controllers/projects/commit_controller.rb index 32fc5140366..b13c0ae3967 100644 --- a/app/controllers/projects/commit_controller.rb +++ b/app/controllers/projects/commit_controller.rb @@ -24,10 +24,10 @@ class Projects::CommitController < Projects::ApplicationController apply_diff_view_cookie! respond_to do |format| - format.html do + format.html do render end - format.diff do + format.diff do send_git_diff(@project.repository, @commit.diff_refs) end format.patch do diff --git a/app/controllers/projects/discussions_controller.rb b/app/controllers/projects/discussions_controller.rb index b62606067c0..028390c7e2a 100644 --- a/app/controllers/projects/discussions_controller.rb +++ b/app/controllers/projects/discussions_controller.rb @@ -40,7 +40,7 @@ class Projects::DiscussionsController < Projects::ApplicationController def render_json_with_discussions_serializer render json: - DiscussionSerializer.new(project: project, noteable: discussion.noteable, current_user: current_user, note_entity: ProjectNoteEntity) + DiscussionSerializer.new(project: project, noteable: discussion.noteable, current_user: current_user, note_entity: ProjectNoteEntity) .represent(discussion, context: self, render_truncated_diff_lines: true) end diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb index a63eea0ca0e..1a1b024d766 100644 --- a/app/controllers/projects/environments_controller.rb +++ b/app/controllers/projects/environments_controller.rb @@ -15,6 +15,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController push_frontend_feature_flag(:area_chart, project) end + # Returns all environments or all folders based on the :nested param def index @environments = project.environments .with_state(params[:scope] || :available) @@ -25,11 +26,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController Gitlab::PollingInterval.set_header(response, interval: 3_000) render json: { - environments: EnvironmentSerializer - .new(project: @project, current_user: @current_user) - .with_pagination(request, response) - .within_folders - .represent(@environments), + environments: serialize_environments(request, response, params[:nested]), available_count: project.environments.available.count, stopped_count: project.environments.stopped.count } @@ -37,6 +34,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController end end + # Returns all environments for a given folder # rubocop: disable CodeReuse/ActiveRecord def folder folder_environments = project.environments.where(environment_type: params[:id]) @@ -48,10 +46,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController format.html format.json do render json: { - environments: EnvironmentSerializer - .new(project: @project, current_user: @current_user) - .with_pagination(request, response) - .represent(@environments), + environments: serialize_environments(request, response), available_count: folder_environments.available.count, stopped_count: folder_environments.stopped.count } @@ -186,6 +181,14 @@ class Projects::EnvironmentsController < Projects::ApplicationController @environment ||= project.environments.find(params[:id]) end + def serialize_environments(request, response, nested = false) + serializer = EnvironmentSerializer + .new(project: @project, current_user: @current_user) + .with_pagination(request, response) + serializer = serializer.within_folders if nested + serializer.represent(@environments) + end + def authorize_stop_environment! access_denied! unless can?(current_user, :stop_environment, environment) end 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..69f983f7ccd 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -19,7 +19,7 @@ class Projects::IssuesController < Projects::ApplicationController prepend_before_action(only: [:index]) { authenticate_sessionless_user!(:rss) } prepend_before_action(only: [:calendar]) { authenticate_sessionless_user!(:ics) } - prepend_before_action :authenticate_new_issue!, only: [:new] + prepend_before_action :authenticate_user!, only: [:new] prepend_before_action :store_uri, only: [:new, :show] before_action :whitelist_query_limiting, only: [:create, :create_merge_request, :move, :bulk_update] @@ -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) @@ -193,6 +191,10 @@ class Projects::IssuesController < Projects::ApplicationController protected + def issuable_sorting_field + Issue::SORTING_PREFERENCE_FIELD + end + # rubocop: disable CodeReuse/ActiveRecord def issue return @issue if defined?(@issue) @@ -247,14 +249,6 @@ class Projects::IssuesController < Projects::ApplicationController ] + [{ label_ids: [], assignee_ids: [] }] end - def authenticate_new_issue! - return if current_user - - notice = "Please sign in to create the new issue." - - redirect_to new_user_session_path, notice: notice - end - def store_uri if request.get? && !request.xhr? store_location_for :user, request.fullpath 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/lfs_storage_controller.rb b/app/controllers/projects/lfs_storage_controller.rb index babeee48ef3..013e01b82aa 100644 --- a/app/controllers/projects/lfs_storage_controller.rb +++ b/app/controllers/projects/lfs_storage_controller.rb @@ -5,7 +5,7 @@ class Projects::LfsStorageController < Projects::GitHttpClientController include WorkhorseRequest include SendFileUpload - skip_before_action :verify_workhorse_api!, only: [:download, :upload_finalize] + skip_before_action :verify_workhorse_api!, only: :download def download lfs_object = LfsObject.find_by_oid(oid) diff --git a/app/controllers/projects/merge_requests/application_controller.rb b/app/controllers/projects/merge_requests/application_controller.rb index 368ee89ff5c..54ff7ded8e5 100644 --- a/app/controllers/projects/merge_requests/application_controller.rb +++ b/app/controllers/projects/merge_requests/application_controller.rb @@ -39,8 +39,11 @@ class Projects::MergeRequests::ApplicationController < Projects::ApplicationCont end def set_pipeline_variables - @pipelines = @merge_request.all_pipelines - @pipeline = @merge_request.head_pipeline - @statuses_count = @pipeline.present? ? @pipeline.statuses.relevant.count : 0 + @pipelines = + if can?(current_user, :read_pipeline, @project) + @merge_request.all_pipelines + else + Ci::Pipeline.none + end end end diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 162c2636641..bc0a3d3526d 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -55,7 +55,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo render json: serializer.represent(@merge_request, serializer: params[:serializer]) end - format.patch do + format.patch do break render_404 unless @merge_request.diff_refs send_git_patch @project.repository, @merge_request.diff_refs @@ -230,6 +230,10 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo alias_method :issuable, :merge_request alias_method :awardable, :merge_request + def issuable_sorting_field + MergeRequest::SORTING_PREFERENCE_FIELD + end + def merge_params params.permit(merge_params_attributes) end diff --git a/app/controllers/projects/milestones_controller.rb b/app/controllers/projects/milestones_controller.rb index 8e68014a30d..f6f61b6e5fb 100644 --- a/app/controllers/projects/milestones_controller.rb +++ b/app/controllers/projects/milestones_controller.rb @@ -144,9 +144,9 @@ 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) + params.permit(:state, :search_title).merge(project_ids: @project.id, group_ids: groups) end end diff --git a/app/controllers/projects/pages_controller.rb b/app/controllers/projects/pages_controller.rb index c1ad6707c97..d0e35bee986 100644 --- a/app/controllers/projects/pages_controller.rb +++ b/app/controllers/projects/pages_controller.rb @@ -18,7 +18,7 @@ class Projects::PagesController < Projects::ApplicationController project.pages_domains.destroy_all # rubocop: disable DestroyAll respond_to do |format| - format.html do + format.html do redirect_to project_pages_path(@project), status: 302, notice: 'Pages were removed' diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb index 67827b1d3bb..6a86f8ca729 100644 --- a/app/controllers/projects/pipelines_controller.rb +++ b/app/controllers/projects/pipelines_controller.rb @@ -4,6 +4,7 @@ class Projects::PipelinesController < Projects::ApplicationController before_action :whitelist_query_limiting, only: [:create, :retry] before_action :pipeline, except: [:index, :new, :create, :charts] before_action :authorize_read_pipeline! + before_action :authorize_read_build!, only: [:index] before_action :authorize_create_pipeline!, only: [:new, :create] before_action :authorize_update_pipeline!, only: [:retry, :cancel] @@ -69,7 +70,7 @@ class Projects::PipelinesController < Projects::ApplicationController render json: PipelineSerializer .new(project: @project, current_user: @current_user) - .represent(@pipeline, grouped: true) + .represent(@pipeline, show_represent_params) end end end @@ -157,6 +158,10 @@ class Projects::PipelinesController < Projects::ApplicationController end end + def show_represent_params + { grouped: true } + end + def create_params params.require(:pipeline).permit(:ref, variables_attributes: %i[key secret_value]) end diff --git a/app/controllers/projects/settings/ci_cd_controller.rb b/app/controllers/projects/settings/ci_cd_controller.rb index 75e590f3f33..f2f63e986bb 100644 --- a/app/controllers/projects/settings/ci_cd_controller.rb +++ b/app/controllers/projects/settings/ci_cd_controller.rb @@ -99,7 +99,9 @@ module Projects def define_triggers_variables @triggers = @project.triggers + .present(current_user: current_user) @trigger = ::Ci::Trigger.new + .present(current_user: current_user) end def define_badges_variables diff --git a/app/controllers/projects/triggers_controller.rb b/app/controllers/projects/triggers_controller.rb index f5fdfb8accc..c7b4ebb2b24 100644 --- a/app/controllers/projects/triggers_controller.rb +++ b/app/controllers/projects/triggers_controller.rb @@ -66,12 +66,11 @@ class Projects::TriggersController < Projects::ApplicationController end def trigger - @trigger ||= project.triggers.find(params[:id]) || render_404 + @trigger ||= project.triggers.find(params[:id]) + .present(current_user: current_user) end def trigger_params - params.require(:trigger).permit( - :description - ) + params.require(:trigger).permit(:description) 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/contributed_projects_finder.rb b/app/finders/contributed_projects_finder.rb index c1ef9dfefa7..f8c7f0c3167 100644 --- a/app/finders/contributed_projects_finder.rb +++ b/app/finders/contributed_projects_finder.rb @@ -14,6 +14,9 @@ class ContributedProjectsFinder < UnionFinder # Returns an ActiveRecord::Relation. # rubocop: disable CodeReuse/ActiveRecord def execute(current_user = nil) + # Do not show contributed projects if the user profile is private. + return Project.none unless can_read_profile?(current_user) + segments = all_projects(current_user) find_union(segments, Project).includes(:namespace).order_id_desc @@ -22,6 +25,10 @@ class ContributedProjectsFinder < UnionFinder private + def can_read_profile?(current_user) + Ability.allowed?(current_user, :read_user_profile, @user) + end + def all_projects(current_user) projects = [] diff --git a/app/finders/milestones_finder.rb b/app/finders/milestones_finder.rb index 9c477978f60..77b55cbb838 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,20 +12,17 @@ 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) + items = by_search_title(items) items = by_state(items) order(items) @@ -34,7 +31,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 @@ -47,6 +44,14 @@ class MilestonesFinder end # rubocop: enable CodeReuse/ActiveRecord + def by_search_title(items) + if params[:search_title].present? + items.search_title(params[:search_title]) + else + items + end + end + def by_state(items) Milestone.filter_by_state(items, params[:state]) end diff --git a/app/graphql/types/permission_types/project.rb b/app/graphql/types/permission_types/project.rb index ab37c282fe5..e9a4ea9157b 100644 --- a/app/graphql/types/permission_types/project.rb +++ b/app/graphql/types/permission_types/project.rb @@ -8,7 +8,7 @@ module Types abilities :change_namespace, :change_visibility_level, :rename_project, :remove_project, :archive_project, :remove_fork_project, :remove_pages, :read_project, :create_merge_request_in, - :read_wiki, :read_project_member, :create_issue, :upload_file, + :read_wiki, :read_project_member, :create_issue, :upload_file, :read_cycle_analytics, :download_code, :download_wiki_code, :fork_project, :create_project_snippet, :read_commit_status, :request_access, :create_pipeline, :create_pipeline_schedule, 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/auth_helper.rb b/app/helpers/auth_helper.rb index 654fb9d9987..2b1d6f49878 100644 --- a/app/helpers/auth_helper.rb +++ b/app/helpers/auth_helper.rb @@ -16,6 +16,13 @@ module AuthHelper PROVIDERS_WITH_ICONS.include?(name.to_s) end + def qa_class_for_provider(provider) + { + saml: 'qa-saml-login-button', + github: 'qa-github-login-button' + }[provider.to_sym] + end + def auth_providers Gitlab::Auth::OAuth::Provider.providers end @@ -31,7 +38,7 @@ module AuthHelper def form_based_provider_with_highest_priority @form_based_provider_with_highest_priority ||= begin form_based_provider_priority.each do |provider_regexp| - highest_priority = form_based_providers.find { |provider| provider.match?(provider_regexp) } + highest_priority = form_based_providers.find { |provider| provider.match?(provider_regexp) } break highest_priority unless highest_priority.nil? end end diff --git a/app/helpers/emails_helper.rb b/app/helpers/emails_helper.rb index fa5d3ae474a..dedc58f482b 100644 --- a/app/helpers/emails_helper.rb +++ b/app/helpers/emails_helper.rb @@ -36,6 +36,14 @@ module EmailsHelper nil end + def sanitize_name(name) + if name =~ URI::DEFAULT_PARSER.regexp[:URI_REF] + name.tr('.', '_') + else + name + end + end + def password_reset_token_valid_time valid_hours = Devise.reset_password_within / 60 / 60 if valid_hours >= 24 diff --git a/app/helpers/external_wiki_helper.rb b/app/helpers/external_wiki_helper.rb deleted file mode 100644 index e36d63b2946..00000000000 --- a/app/helpers/external_wiki_helper.rb +++ /dev/null @@ -1,12 +0,0 @@ -# frozen_string_literal: true - -module ExternalWikiHelper - def get_project_wiki_path(project) - external_wiki_service = project.external_wiki - if external_wiki_service - external_wiki_service.properties['external_wiki_url'] - else - project_wiki_path(project, :home) - end - end -end diff --git a/app/helpers/import_helper.rb b/app/helpers/import_helper.rb index 49171df1433..d3befd87ccc 100644 --- a/app/helpers/import_helper.rb +++ b/app/helpers/import_helper.rb @@ -8,7 +8,9 @@ module ImportHelper end def sanitize_project_name(name) - name.gsub(/[^\w\-]/, '-') + # For personal projects in Bitbucket in the form ~username, we can + # just drop that leading tilde. + name.gsub(/\A~+/, '').gsub(/[^\w\-]/, '-') end def import_project_target(owner, name) diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index 5f7147508c7..f8176facce9 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -209,7 +209,7 @@ module IssuablesHelper end def issuable_labels_tooltip(labels, limit: 5) - first, last = labels.partition.with_index { |_, i| i < limit } + first, last = labels.partition.with_index { |_, i| i < limit } if labels && labels.any? label_names = first.collect { |label| label.fetch(:title) } diff --git a/app/helpers/members_helper.rb b/app/helpers/members_helper.rb index 5a21403bc5e..11d5591d509 100644 --- a/app/helpers/members_helper.rb +++ b/app/helpers/members_helper.rb @@ -18,12 +18,13 @@ module MembersHelper "remove #{member.user.name} from" end - "#{text} #{action} the #{member.source.human_name} #{member.real_source_type.humanize(capitalize: false)}?" + "#{text} #{action} the #{member.source.human_name} #{source_text(member)}?" end def remove_member_title(member) action = member.request? ? 'Deny access request' : 'Remove user' - "#{action} from #{member.real_source_type.humanize(capitalize: false)}" + + "#{action} from #{source_text(member)}" end def leave_confirmation_message(member_source) @@ -32,7 +33,17 @@ 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 + + private + + def source_text(member) + type = member.real_source_type.humanize(capitalize: false) + + return type if member.request? || member.invite? || type != 'group' + + 'group and any subresources' + end end diff --git a/app/helpers/notes_helper.rb b/app/helpers/notes_helper.rb index 033686823a2..293dd20ad49 100644 --- a/app/helpers/notes_helper.rb +++ b/app/helpers/notes_helper.rb @@ -85,7 +85,7 @@ module NotesHelper diffs_project_merge_request_path(discussion.project, discussion.noteable, path_params) elsif discussion.for_commit? - anchor = discussion.line_code if discussion.diff_discussion? + anchor = discussion.diff_discussion? ? discussion.line_code : "note_#{discussion.first_note.id}" project_commit_path(discussion.project, discussion.noteable, anchor: anchor) end diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index ebbed08f78a..85248a16f50 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 @@ -305,7 +305,8 @@ module ProjectsHelper nav_tabs << :container_registry end - if project.builds_enabled? && can?(current_user, :read_pipeline, project) + # Pipelines feature is tied to presence of builds + if can?(current_user, :read_build, project) nav_tabs << :pipelines end @@ -313,19 +314,24 @@ module ProjectsHelper nav_tabs << :operations end - if project.external_issue_tracker - nav_tabs << :external_issue_tracker - end - tab_ability_map.each do |tab, ability| if can?(current_user, ability, project) nav_tabs << tab end end + nav_tabs << external_nav_tabs(project) + nav_tabs.flatten end + def external_nav_tabs(project) + [].tap do |tabs| + tabs << :external_issue_tracker if project.external_issue_tracker + tabs << :external_wiki if project.has_external_wiki? + end + end + def tab_ability_map { environments: :read_environment, diff --git a/app/helpers/release_blog_post_helper.rb b/app/helpers/release_blog_post_helper.rb deleted file mode 100644 index 31b5b7edc39..00000000000 --- a/app/helpers/release_blog_post_helper.rb +++ /dev/null @@ -1,7 +0,0 @@ -# frozen_string_literal: true - -module ReleaseBlogPostHelper - def blog_post_url - Gitlab::ReleaseBlogPost.instance.blog_post_url - end -end diff --git a/app/models/appearance.rb b/app/models/appearance.rb index e114c435b67..ff1ecfda684 100644 --- a/app/models/appearance.rb +++ b/app/models/appearance.rb @@ -44,7 +44,11 @@ class Appearance < ActiveRecord::Base private def logo_system_path(logo, mount_type) - return unless logo&.upload + # Legacy attachments may not have have an associated Upload record, + # so fallback to the AttachmentUploader#url if this is the + # case. AttachmentUploader#path doesn't work because for a local + # file, this is an absolute path to the file. + return logo&.url unless logo&.upload # If we're using a CDN, we need to use the full URL asset_host = ActionController::Base.asset_host diff --git a/app/models/application_record.rb b/app/models/application_record.rb index 71fbba5b328..29696ab276f 100644 --- a/app/models/application_record.rb +++ b/app/models/application_record.rb @@ -2,4 +2,8 @@ class ApplicationRecord < ActiveRecord::Base self.abstract_class = true + + def self.id_in(ids) + where(id: ids) + end end diff --git a/app/models/ci/bridge.rb b/app/models/ci/bridge.rb index 29aa00a66d9..5450d40ea95 100644 --- a/app/models/ci/bridge.rb +++ b/app/models/ci/bridge.rb @@ -2,11 +2,13 @@ module Ci class Bridge < CommitStatus + include Ci::Processable include Importable include AfterCommitQueue include Gitlab::Utils::StrongMemoize belongs_to :project + belongs_to :trigger_request validates :ref, presence: true def self.retry(bridge, current_user) @@ -23,6 +25,21 @@ module Ci .fabricate! end + def schedulable? + false + end + + def action? + false + end + + def artifacts? + false + end + + def expanded_environment_name + end + def predefined_variables raise NotImplementedError end @@ -30,5 +47,9 @@ module Ci def execute_hooks raise NotImplementedError end + + def to_partial_path + 'projects/generic_commit_statuses/generic_commit_status' + end end end diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index dc6f8ae1a7f..84010e40ef4 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -3,6 +3,8 @@ module Ci class Build < CommitStatus prepend ArtifactMigratable + include Ci::Processable + include Ci::Metadatable include TokenAuthenticatable include AfterCommitQueue include ObjectStorage::BackgroundMove @@ -36,12 +38,10 @@ module Ci has_one :"job_artifacts_#{key}", -> { where(file_type: value) }, class_name: 'Ci::JobArtifact', inverse_of: :job, foreign_key: :job_id end - has_one :metadata, class_name: 'Ci::BuildMetadata', autosave: true has_one :runner_session, class_name: 'Ci::BuildRunnerSession', validate: true, inverse_of: :build accepts_nested_attributes_for :runner_session - delegate :timeout, to: :metadata, prefix: true, allow_nil: true delegate :url, to: :runner_session, prefix: true, allow_nil: true delegate :terminal_specification, to: :runner_session, allow_nil: true delegate :gitlab_deploy_token, to: :project @@ -132,7 +132,6 @@ module Ci before_save :ensure_token before_destroy { unscoped_project } - before_create :ensure_metadata after_create unless: :importing? do |build| run_after_commit { BuildHooksWorker.perform_async(build.id) } end @@ -224,8 +223,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| @@ -253,10 +259,6 @@ module Ci end end - def ensure_metadata - metadata || build_metadata(project: project) - end - def detailed_status(current_user) Gitlab::Ci::Status::Build::Factory .new(self, current_user) @@ -276,18 +278,6 @@ module Ci self.name == 'pages' end - # degenerated build is one that cannot be run by Runner - def degenerated? - self.options.blank? - end - - def degenerate! - Build.transaction do - self.update!(options: nil, yaml_variables: nil) - self.metadata&.destroy - end - end - def archived? return true if degenerated? @@ -631,26 +621,6 @@ module Ci super || project.try(:build_coverage_regex) end - def when - read_attribute(:when) || 'on_success' - end - - def options - read_metadata_attribute(:options, :config_options, {}) - end - - def yaml_variables - read_metadata_attribute(:yaml_variables, :config_variables, []) - end - - def options=(value) - write_metadata_attribute(:options, :config_options, value) - end - - def yaml_variables=(value) - write_metadata_attribute(:yaml_variables, :config_variables, value) - end - def user_variables Gitlab::Ci::Variables::Collection.new.tap do |variables| break variables if user.blank? @@ -952,20 +922,5 @@ module Ci def project_destroyed? project.pending_delete? end - - def read_metadata_attribute(legacy_key, metadata_key, default_value = nil) - read_attribute(legacy_key) || metadata&.read_attribute(metadata_key) || default_value - end - - def write_metadata_attribute(legacy_key, metadata_key, value) - # save to metadata or this model depending on the state of feature flag - if Feature.enabled?(:ci_build_metadata_config) - ensure_metadata.write_attribute(metadata_key, value) - write_attribute(legacy_key, nil) - else - write_attribute(legacy_key, value) - metadata&.write_attribute(metadata_key, nil) - end - end end end diff --git a/app/models/ci/build_metadata.rb b/app/models/ci/build_metadata.rb index 38390f49217..cd8eb774cf5 100644 --- a/app/models/ci/build_metadata.rb +++ b/app/models/ci/build_metadata.rb @@ -10,7 +10,7 @@ module Ci self.table_name = 'ci_builds_metadata' - belongs_to :build, class_name: 'Ci::Build' + belongs_to :build, class_name: 'CommitStatus' belongs_to :project before_create :set_build_project diff --git a/app/models/ci/job_artifact.rb b/app/models/ci/job_artifact.rb index 11c88200c37..789bb293811 100644 --- a/app/models/ci/job_artifact.rb +++ b/app/models/ci/job_artifact.rb @@ -73,6 +73,8 @@ module Ci where(file_type: types) end + scope :expired, -> (limit) { where('expire_at < ?', Time.now).limit(limit) } + delegate :filename, :exists?, :open, to: :file enum file_type: { diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 30a957b4117..acef5d2e643 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -25,6 +25,8 @@ module Ci has_many :stages, -> { order(position: :asc) }, inverse_of: :pipeline has_many :statuses, class_name: 'CommitStatus', foreign_key: :commit_id, inverse_of: :pipeline + has_many :processables, -> { processables }, + class_name: 'CommitStatus', foreign_key: :commit_id, inverse_of: :pipeline has_many :builds, foreign_key: :commit_id, inverse_of: :pipeline has_many :trigger_requests, dependent: :destroy, foreign_key: :commit_id # rubocop:disable Cop/ActiveRecordDependent has_many :variables, class_name: 'Ci::PipelineVariable' diff --git a/app/models/ci/stage.rb b/app/models/ci/stage.rb index 58f3fe2460a..0389945191e 100644 --- a/app/models/ci/stage.rb +++ b/app/models/ci/stage.rb @@ -14,6 +14,7 @@ module Ci has_many :statuses, class_name: 'CommitStatus', foreign_key: :stage_id has_many :builds, foreign_key: :stage_id + has_many :bridges, foreign_key: :stage_id with_options unless: :importing? do validates :project, presence: true diff --git a/app/models/ci/trigger.rb b/app/models/ci/trigger.rb index 55db42162ca..637148c4ce4 100644 --- a/app/models/ci/trigger.rb +++ b/app/models/ci/trigger.rb @@ -4,6 +4,7 @@ module Ci class Trigger < ActiveRecord::Base extend Gitlab::Ci::Model include IgnorableColumn + include Presentable ignore_column :deleted_at @@ -29,7 +30,7 @@ module Ci end def short_token - token[0...4] + token[0...4] if token.present? end def legacy? 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/clusters/platforms/kubernetes.rb b/app/models/clusters/platforms/kubernetes.rb index 1cc170c8c4d..8f3424db295 100644 --- a/app/models/clusters/platforms/kubernetes.rb +++ b/app/models/clusters/platforms/kubernetes.rb @@ -154,7 +154,7 @@ module Clusters def build_kube_client! raise "Incomplete settings" unless api_url - raise "No namespace" if cluster.project_type? && actual_namespace.empty? # can probably remove this line once we remove #actual_namespace + raise "No namespace" if cluster.project_type? && actual_namespace.empty? # can probably remove this line once we remove #actual_namespace unless (username && password) || token raise "Either username/password or token is required to access API" diff --git a/app/models/commit.rb b/app/models/commit.rb index 01f4c58daa1..982e13e2845 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -11,6 +11,7 @@ class Commit include Mentionable include Referable include StaticModel + include Presentable include ::Gitlab::Utils::StrongMemoize attr_mentionable :safe_message, pipeline: :single_line @@ -304,7 +305,9 @@ class Commit end def last_pipeline - @last_pipeline ||= pipelines.last + strong_memoize(:last_pipeline) do + pipelines.last + end end def status(ref = nil) diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index 0f50bd39131..7f6562b63e5 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -41,6 +41,7 @@ class CommitStatus < ActiveRecord::Base scope :latest_ordered, -> { latest.ordered.includes(project: :namespace) } scope :retried_ordered, -> { retried.ordered.includes(project: :namespace) } scope :after_stage, -> (index) { where('stage_idx > ?', index) } + scope :processables, -> { where(type: %w[Ci::Build Ci::Bridge]) } # We use `CommitStatusEnums.failure_reasons` here so that EE can more easily # extend this `Hash` with new values. diff --git a/app/models/concerns/cache_markdown_field.rb b/app/models/concerns/cache_markdown_field.rb index a8c9e54f00c..002f3e17891 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 = 14 # changes to these attributes cause the cache to be invalidates INVALIDATED_BY = %w[author project].freeze diff --git a/app/models/concerns/ci/metadatable.rb b/app/models/concerns/ci/metadatable.rb new file mode 100644 index 00000000000..9eed9492b37 --- /dev/null +++ b/app/models/concerns/ci/metadatable.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true + +module Ci + ## + # This module implements methods that need to read and write + # metadata for CI/CD entities. + # + module Metadatable + extend ActiveSupport::Concern + + included do + has_one :metadata, class_name: 'Ci::BuildMetadata', + foreign_key: :build_id, + inverse_of: :build, + autosave: true + + delegate :timeout, to: :metadata, prefix: true, allow_nil: true + before_create :ensure_metadata + end + + def ensure_metadata + metadata || build_metadata(project: project) + end + + def degenerated? + self.options.blank? + end + + def degenerate! + self.class.transaction do + self.update!(options: nil, yaml_variables: nil) + self.metadata&.destroy + end + end + + def options + read_metadata_attribute(:options, :config_options, {}) + end + + def yaml_variables + read_metadata_attribute(:yaml_variables, :config_variables, []) + end + + def options=(value) + write_metadata_attribute(:options, :config_options, value) + end + + def yaml_variables=(value) + write_metadata_attribute(:yaml_variables, :config_variables, value) + end + + private + + def read_metadata_attribute(legacy_key, metadata_key, default_value = nil) + read_attribute(legacy_key) || metadata&.read_attribute(metadata_key) || default_value + end + + def write_metadata_attribute(legacy_key, metadata_key, value) + # save to metadata or this model depending on the state of feature flag + if Feature.enabled?(:ci_build_metadata_config) + ensure_metadata.write_attribute(metadata_key, value) + write_attribute(legacy_key, nil) + else + write_attribute(legacy_key, value) + metadata&.write_attribute(metadata_key, nil) + end + end + end +end diff --git a/app/models/concerns/ci/processable.rb b/app/models/concerns/ci/processable.rb new file mode 100644 index 00000000000..1c78b1413a8 --- /dev/null +++ b/app/models/concerns/ci/processable.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module Ci + ## + # This module implements methods that need to be implemented by CI/CD + # entities that are supposed to go through pipeline processing + # services. + # + # + module Processable + def schedulable? + raise NotImplementedError + end + + def action? + raise NotImplementedError + end + + def when + read_attribute(:when) || 'on_success' + end + + def expanded_environment_name + raise NotImplementedError + end + end +end diff --git a/app/models/concerns/has_status.rb b/app/models/concerns/has_status.rb index b92643f87f8..0d2be4c61ab 100644 --- a/app/models/concerns/has_status.rb +++ b/app/models/concerns/has_status.rb @@ -85,11 +85,11 @@ module HasStatus scope :running, -> { where(status: 'running') } scope :pending, -> { where(status: 'pending') } scope :success, -> { where(status: 'success') } - scope :failed, -> { where(status: 'failed') } - scope :canceled, -> { where(status: 'canceled') } - scope :skipped, -> { where(status: 'skipped') } - scope :manual, -> { where(status: 'manual') } - scope :scheduled, -> { where(status: 'scheduled') } + scope :failed, -> { where(status: 'failed') } + scope :canceled, -> { where(status: 'canceled') } + scope :skipped, -> { where(status: 'skipped') } + scope :manual, -> { where(status: 'manual') } + scope :scheduled, -> { where(status: 'scheduled') } scope :alive, -> { where(status: [:created, :pending, :running]) } scope :created_or_pending, -> { where(status: [:created, :pending]) } scope :running_or_pending, -> { where(status: [:running, :pending]) } diff --git a/app/models/container_repository.rb b/app/models/container_repository.rb index 2c08a8e1acf..cf057d774cf 100644 --- a/app/models/container_repository.rb +++ b/app/models/container_repository.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class ContainerRepository < ActiveRecord::Base + include Gitlab::Utils::StrongMemoize + belongs_to :project validates :name, length: { minimum: 0, allow_nil: false } @@ -8,6 +10,8 @@ class ContainerRepository < ActiveRecord::Base delegate :client, to: :registry + scope :ordered, -> { order(:name) } + # rubocop: disable CodeReuse/ServiceClass def registry @registry ||= begin @@ -39,11 +43,12 @@ class ContainerRepository < ActiveRecord::Base end def tags - return @tags if defined?(@tags) return [] unless manifest && manifest['tags'] - @tags = manifest['tags'].map do |tag| - ContainerRegistry::Tag.new(self, tag) + strong_memoize(:tags) do + manifest['tags'].sort.map do |tag| + ContainerRegistry::Tag.new(self, tag) + end end end diff --git a/app/models/dashboard_group_milestone.rb b/app/models/dashboard_group_milestone.rb index 9bcc95e35a5..74aa04ab7d0 100644 --- a/app/models/dashboard_group_milestone.rb +++ b/app/models/dashboard_group_milestone.rb @@ -11,11 +11,12 @@ class DashboardGroupMilestone < GlobalMilestone @group_name = milestone.group.full_name end - def self.build_collection(groups) - Milestone.of_groups(groups.select(:id)) + def self.build_collection(groups, params) + milestones = Milestone.of_groups(groups.select(:id)) .reorder_by_due_date_asc .order_by_name_asc .active - .map { |m| new(m) } + milestones = milestones.search_title(params[:search_title]) if params[:search_title].present? + milestones.map { |m| new(m) } end end diff --git a/app/models/email.rb b/app/models/email.rb index b6a977dfa22..3ce6e792fa8 100644 --- a/app/models/email.rb +++ b/app/models/email.rb @@ -15,7 +15,7 @@ class Email < ActiveRecord::Base after_commit :update_invalid_gpg_signatures, if: -> { previous_changes.key?('confirmed_at') } devise :confirmable - self.reconfirmable = false # currently email can't be changed, no need to reconfirm + self.reconfirmable = false # currently email can't be changed, no need to reconfirm delegate :username, to: :user 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/global_milestone.rb b/app/models/global_milestone.rb index 4e82f3fed27..fd17745b035 100644 --- a/app/models/global_milestone.rb +++ b/app/models/global_milestone.rb @@ -27,6 +27,7 @@ class GlobalMilestone items = Milestone.of_projects(projects) .reorder_by_due_date_asc .order_by_name_asc + items = items.search_title(params[:search_title]) if params[:search_title].present? Milestone.filter_by_state(items, params[:state]).map { |m| new(m) } end diff --git a/app/models/group.rb b/app/models/group.rb index edac2444c4d..52f503404af 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -98,7 +98,7 @@ class Group < Namespace def select_for_project_authorization if current_scope.joins_values.include?(:shared_projects) joins('INNER JOIN namespaces project_namespace ON project_namespace.id = projects.namespace_id') - .where('project_namespace.share_with_group_lock = ?', false) + .where('project_namespace.share_with_group_lock = ?', false) .select("projects.id AS project_id, LEAST(project_group_links.group_access, members.access_level) AS access_level") else super @@ -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/group_milestone.rb b/app/models/group_milestone.rb index a58537de319..97cb26c6ea9 100644 --- a/app/models/group_milestone.rb +++ b/app/models/group_milestone.rb @@ -5,9 +5,10 @@ class GroupMilestone < GlobalMilestone def self.build_collection(group, projects, params) params = - { state: params[:state] } + { state: params[:state], search_title: params[:search_title] } project_milestones = Milestone.of_projects(projects) + project_milestones = project_milestones.search_title(params[:search_title]) if params[:search_title].present? child_milestones = Milestone.filter_by_state(project_milestones, params[:state]) grouped_milestones = child_milestones.group_by(&:title) diff --git a/app/models/identity.rb b/app/models/identity.rb index d63dd432426..acdde4f296b 100644 --- a/app/models/identity.rb +++ b/app/models/identity.rb @@ -8,7 +8,7 @@ class Identity < ActiveRecord::Base validates :provider, presence: true validates :extern_uid, allow_blank: true, uniqueness: { scope: UniquenessScopes.scopes, case_sensitive: false } - validates :user_id, uniqueness: { scope: UniquenessScopes.scopes } + validates :user, uniqueness: { scope: UniquenessScopes.scopes } before_save :ensure_normalized_extern_uid, if: :extern_uid_changed? after_destroy :clear_user_synced_attributes, if: :user_synced_attributes_metadata_from_provider? diff --git a/app/models/internal_id.rb b/app/models/internal_id.rb index e7168d49db9..e75c6eb2331 100644 --- a/app/models/internal_id.rb +++ b/app/models/internal_id.rb @@ -66,6 +66,17 @@ class InternalId < ActiveRecord::Base InternalIdGenerator.new(subject, scope, usage, init).generate end + # Flushing records is generally safe in a sense that those + # records are going to be re-created when needed. + # + # A filter condition has to be provided to not accidentally flush + # records for all projects. + def flush_records!(filter) + raise ArgumentError, "filter cannot be empty" if filter.blank? + + where(filter).delete_all + end + def available? @available_flag ||= ActiveRecord::Migrator.current_version >= REQUIRED_SCHEMA_VERSION # rubocop:disable Gitlab/PredicateMemoization end @@ -111,7 +122,7 @@ class InternalId < ActiveRecord::Base # Generates next internal id and returns it def generate - InternalId.transaction do + subject.transaction do # Create a record in internal_ids if one does not yet exist # and increment its last value # @@ -125,7 +136,7 @@ class InternalId < ActiveRecord::Base # # Note this will acquire a ROW SHARE lock on the InternalId record def track_greatest(new_value) - InternalId.transaction do + subject.transaction do (lookup || create_record).track_greatest_and_save!(new_value) end end @@ -148,7 +159,7 @@ class InternalId < ActiveRecord::Base # violation. We can safely roll-back the nested transaction and perform # a lookup instead to retrieve the record. def create_record - InternalId.transaction(requires_new: true) do + subject.transaction(requires_new: true) do InternalId.create!( **scope, usage: usage_value, diff --git a/app/models/issue.rb b/app/models/issue.rb index b7e13bcbccf..182c5d3d4b0 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -26,6 +26,8 @@ class Issue < ActiveRecord::Base DueThisMonth = DueDateStruct.new('Due This Month', 'month').freeze DueNextMonthAndPreviousTwoWeeks = DueDateStruct.new('Due Next Month And Previous Two Weeks', 'next_month_and_previous_two_weeks').freeze + SORTING_PREFERENCE_FIELD = :issues_sort + belongs_to :project belongs_to :moved_to, class_name: 'Issue' belongs_to :closed_by, class_name: 'User' @@ -230,7 +232,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/lfs_download_object.rb b/app/models/lfs_download_object.rb new file mode 100644 index 00000000000..6383f95d546 --- /dev/null +++ b/app/models/lfs_download_object.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +class LfsDownloadObject + include ActiveModel::Validations + + attr_accessor :oid, :size, :link + delegate :sanitized_url, :credentials, to: :sanitized_uri + + validates :oid, format: { with: /\A\h{64}\z/ } + validates :size, numericality: { greater_than_or_equal_to: 0 } + validates :link, public_url: { protocols: %w(http https) } + + def initialize(oid:, size:, link:) + @oid = oid + @size = size + @link = link + end + + def sanitized_uri + @sanitized_uri ||= Gitlab::UrlSanitizer.new(link) + end +end diff --git a/app/models/member.rb b/app/models/member.rb index 9fc95ea00c3..8e071a8ff21 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -76,14 +76,17 @@ class Member < ActiveRecord::Base scope :maintainers, -> { active.where(access_level: MAINTAINER) } scope :masters, -> { maintainers } # @deprecated scope :owners, -> { active.where(access_level: OWNER) } - scope :owners_and_maintainers, -> { active.where(access_level: [OWNER, MAINTAINER]) } + scope :owners_and_maintainers, -> { active.where(access_level: [OWNER, MAINTAINER]) } scope :owners_and_masters, -> { owners_and_maintainers } # @deprecated + scope :with_user, -> (user) { where(user: user) } scope :order_name_asc, -> { left_join_users.reorder(Gitlab::Database.nulls_last_order('users.name', 'ASC')) } scope :order_name_desc, -> { left_join_users.reorder(Gitlab::Database.nulls_last_order('users.name', 'DESC')) } scope :order_recent_sign_in, -> { left_join_users.reorder(Gitlab::Database.nulls_last_order('users.last_sign_in_at', 'DESC')) } scope :order_oldest_sign_in, -> { left_join_users.reorder(Gitlab::Database.nulls_last_order('users.last_sign_in_at', 'ASC')) } + scope :on_project_and_ancestors, ->(project) { where(source: [project] + project.ancestors) } + before_validation :generate_invite_token, on: :create, if: -> (member) { member.invite_email.present? } after_create :send_invite, if: :invite?, unless: :importing? diff --git a/app/models/members/group_member.rb b/app/models/members/group_member.rb index fc49ee7ac8c..2c9e1ba1d80 100644 --- a/app/models/members/group_member.rb +++ b/app/models/members/group_member.rb @@ -12,6 +12,8 @@ class GroupMember < Member validates :source_type, format: { with: /\ANamespace\z/ } default_scope { where(source_type: SOURCE_TYPE) } + scope :in_groups, ->(groups) { where(source_id: groups.select(:id)) } + after_create :update_two_factor_requirement, unless: :invite? after_destroy :update_two_factor_requirement, unless: :invite? diff --git a/app/models/members/project_member.rb b/app/models/members/project_member.rb index 016c18ce6c8..5372c6084f4 100644 --- a/app/models/members/project_member.rb +++ b/app/models/members/project_member.rb @@ -12,6 +12,10 @@ class ProjectMember < Member default_scope { where(source_type: SOURCE_TYPE) } scope :in_project, ->(project) { where(source_id: project.id) } + scope :in_namespaces, ->(groups) do + joins('INNER JOIN projects ON projects.id = members.source_id') + .where('projects.namespace_id in (?)', groups.select(:id)) + end class << self # Add users to projects with passed access option diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 7206d858dae..84cb8e1c50b 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -21,6 +21,8 @@ class MergeRequest < ActiveRecord::Base self.reactive_cache_refresh_interval = 10.minutes self.reactive_cache_lifetime = 10.minutes + SORTING_PREFERENCE_FIELD = :merge_requests_sort + ignore_column :locked_at, :ref_fetched, :deleted_at diff --git a/app/models/milestone.rb b/app/models/milestone.rb index 1ebcbcda0d8..26cfdc5ef30 100644 --- a/app/models/milestone.rb +++ b/app/models/milestone.rb @@ -28,7 +28,7 @@ class Milestone < ActiveRecord::Base has_internal_id :iid, scope: :group, init: ->(s) { s&.group&.milestones&.maximum(:iid) } has_many :issues - has_many :labels, -> { distinct.reorder('labels.title') }, through: :issues + has_many :labels, -> { distinct.reorder('labels.title') }, through: :issues has_many :merge_requests has_many :events, as: :target, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent @@ -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)) } @@ -77,7 +77,7 @@ class Milestone < ActiveRecord::Base alias_attribute :name, :title class << self - # Searches for milestones matching the given query. + # Searches for milestones with a matching title or description. # # This method uses ILIKE on PostgreSQL and LIKE on MySQL. # @@ -88,6 +88,17 @@ class Milestone < ActiveRecord::Base fuzzy_search(query, [:title, :description]) end + # Searches for milestones with a matching title. + # + # This method uses ILIKE on PostgreSQL and LIKE on MySQL. + # + # query - The search query as a String + # + # Returns an ActiveRecord::Relation. + def search_title(query) + fuzzy_search(query, [:title]) + end + def filter_by_state(milestones, state) case state when 'closed' then milestones.closed @@ -191,7 +202,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 +286,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/namespace.rb b/app/models/namespace.rb index a0bebc5e9a2..f7592532c5b 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -class Namespace < ActiveRecord::Base +class Namespace < ApplicationRecord include CacheMarkdownField include Sortable include Gitlab::VisibilityLevel diff --git a/app/models/project.rb b/app/models/project.rb index 27be16720b5..b385b89449d 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) } @@ -377,8 +377,10 @@ class Project < ActiveRecord::Base # "enabled" here means "not disabled". It includes private features! scope :with_feature_enabled, ->(feature) { - access_level_attribute = ProjectFeature.access_level_attribute(feature) - with_project_feature.where(project_features: { access_level_attribute => [nil, ProjectFeature::PRIVATE, ProjectFeature::ENABLED, ProjectFeature::PUBLIC] }) + access_level_attribute = ProjectFeature.arel_table[ProjectFeature.access_level_attribute(feature)] + enabled_feature = access_level_attribute.gt(ProjectFeature::DISABLED).or(access_level_attribute.eq(nil)) + + with_project_feature.where(enabled_feature) } # Picks a feature where the level is exactly that given. @@ -465,7 +467,8 @@ class Project < ActiveRecord::Base # logged in users to more efficiently get private projects with the given # feature. def self.with_feature_available_for_user(feature, user) - visible = [nil, ProjectFeature::ENABLED, ProjectFeature::PUBLIC] + visible = [ProjectFeature::ENABLED, ProjectFeature::PUBLIC] + min_access_level = ProjectFeature.required_minimum_access_level(feature) if user&.admin? with_feature_enabled(feature) @@ -473,10 +476,15 @@ class Project < ActiveRecord::Base column = ProjectFeature.quoted_access_level_column(feature) with_project_feature - .where("#{column} IN (?) OR (#{column} = ? AND EXISTS (?))", - visible, - ProjectFeature::PRIVATE, - user.authorizations_for_projects) + .where( + "(projects.visibility_level > :private AND (#{column} IS NULL OR #{column} >= (:public_visible) OR (#{column} = :private_visible AND EXISTS(:authorizations))))"\ + " OR (projects.visibility_level = :private AND (#{column} IS NULL OR #{column} >= :private_visible) AND EXISTS(:authorizations))", + { + private: Gitlab::VisibilityLevel::PRIVATE, + public_visible: ProjectFeature::ENABLED, + private_visible: ProjectFeature::PRIVATE, + authorizations: user.authorizations_for_projects(min_access_level: min_access_level) + }) else with_feature_access_level(feature, visible) end @@ -530,6 +538,7 @@ class Project < ActiveRecord::Base def reference_pattern %r{ + (?<!#{Gitlab::PathRegex::PATH_START_CHAR}) ((?<namespace>#{Gitlab::PathRegex::FULL_NAMESPACE_FORMAT_REGEX})\/)? (?<project>#{Gitlab::PathRegex::PROJECT_PATH_FORMAT_REGEX}) }x @@ -569,6 +578,14 @@ class Project < ActiveRecord::Base end end + def all_pipelines + if builds_enabled? + super + else + super.external + end + end + # returns all ancestor-groups upto but excluding the given namespace # when no namespace is given, all ancestors upto the top are returned def ancestors_upto(top = nil, hierarchy_order: nil) @@ -809,18 +826,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 +1552,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 @@ -1581,6 +1602,13 @@ class Project < ActiveRecord::Base def after_import repository.after_import wiki.repository.after_import + + # The import assigns iid values on its own, e.g. by re-using GitHub ids. + # Flush existing InternalId records for this project for consistency reasons. + # Those records are going to be recreated with the next normal creation + # of a model instance (e.g. an Issue). + InternalId.flush_records!(project: self) + import_state.finish import_state.remove_jid update_project_counter_caches @@ -1602,24 +1630,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 @@ -1702,11 +1713,19 @@ class Project < ActiveRecord::Base .append(key: 'CI_PROJECT_NAMESPACE', value: namespace.full_path) .append(key: 'CI_PROJECT_URL', value: web_url) .append(key: 'CI_PROJECT_VISIBILITY', value: visibility) + .concat(pages_variables) .concat(container_registry_variables) .concat(auto_devops_variables) .concat(api_variables) end + def pages_variables + Gitlab::Ci::Variables::Collection.new.tap do |variables| + variables.append(key: 'CI_PAGES_DOMAIN', value: Gitlab.config.pages.host) + variables.append(key: 'CI_PAGES_URL', value: pages_url) + end + end + def api_variables Gitlab::Ci::Variables::Collection.new.tap do |variables| variables.append(key: 'CI_API_V4_URL', value: API::Helpers::Version.new('v4').root_url) diff --git a/app/models/project_feature.rb b/app/models/project_feature.rb index 39f2b8fe0de..f700090a493 100644 --- a/app/models/project_feature.rb +++ b/app/models/project_feature.rb @@ -23,11 +23,11 @@ class ProjectFeature < ActiveRecord::Base PUBLIC = 30 FEATURES = %i(issues merge_requests wiki snippets builds repository pages).freeze + PRIVATE_FEATURES_MIN_ACCESS_LEVEL = { merge_requests: Gitlab::Access::REPORTER }.freeze class << self def access_level_attribute(feature) - feature = feature.model_name.plural.to_sym if feature.respond_to?(:model_name) - raise ArgumentError, "invalid project feature: #{feature}" unless FEATURES.include?(feature) + feature = ensure_feature!(feature) "#{feature}_access_level".to_sym end @@ -38,6 +38,21 @@ class ProjectFeature < ActiveRecord::Base "#{table}.#{attribute}" end + + def required_minimum_access_level(feature) + feature = ensure_feature!(feature) + + PRIVATE_FEATURES_MIN_ACCESS_LEVEL.fetch(feature, Gitlab::Access::GUEST) + end + + private + + def ensure_feature!(feature) + feature = feature.model_name.plural.to_sym if feature.respond_to?(:model_name) + raise ArgumentError, "invalid project feature: #{feature}" unless FEATURES.include?(feature) + + feature + end end # Default scopes force us to unscope here since a service may need to check diff --git a/app/models/project_services/bamboo_service.rb b/app/models/project_services/bamboo_service.rb index a252052200a..71f5607dbdb 100644 --- a/app/models/project_services/bamboo_service.rb +++ b/app/models/project_services/bamboo_service.rb @@ -80,19 +80,27 @@ class BambooService < CiService private - def get_build_result_index - # When Bamboo returns multiple results for a given changeset, arbitrarily assume the most relevant result to be the last one. - -1 + def get_build_result(response) + return if response.code != 200 + + # May be nil if no result, a single result hash, or an array if multiple results for a given changeset. + result = response.dig('results', 'results', 'result') + + # In case of multiple results, arbitrarily assume the last one is the most relevant. + return result.last if result.is_a?(Array) + + result end def read_build_page(response) + result = get_build_result(response) key = - if response.code != 200 || response.dig('results', 'results', 'size') == '0' + if result.blank? # If actual build link can't be determined, send user to build summary page. build_key else # If actual build link is available, go to build result page. - response.dig('results', 'results', 'result', get_build_result_index, 'planResultKey', 'key') + result.dig('planResultKey', 'key') end build_url("browse/#{key}") @@ -101,11 +109,15 @@ class BambooService < CiService def read_commit_status(response) return :error unless response.code == 200 || response.code == 404 - status = if response.code == 404 || response.dig('results', 'results', 'size') == '0' - 'Pending' - else - response.dig('results', 'results', 'result', get_build_result_index, 'buildState') - end + result = get_build_result(response) + status = + if result.blank? + 'Pending' + else + result.dig('buildState') + end + + return :error unless status.present? if status.include?('Success') 'success' diff --git a/app/models/project_services/irker_service.rb b/app/models/project_services/irker_service.rb index a15780c14f9..83fd9a34438 100644 --- a/app/models/project_services/irker_service.rb +++ b/app/models/project_services/irker_service.rb @@ -59,7 +59,7 @@ class IrkerService < Service ' append "?key=secretpassword" to the URI (Note that due to a bug, if you ' \ ' want to use a password, you have to omit the "#" on the channel). If you ' \ ' specify a default IRC URI to prepend before each recipient, you can just ' \ - ' give a channel name.' }, + ' give a channel name.' }, { type: 'checkbox', name: 'colorize_messages' } ] end diff --git a/app/models/project_team.rb b/app/models/project_team.rb index 33bc6a561f9..aeba2843e5d 100644 --- a/app/models/project_team.rb +++ b/app/models/project_team.rb @@ -74,6 +74,14 @@ class ProjectTeam end alias_method :users, :members + # `members` method uses project_authorizations table which + # is updated asynchronously, on project move it still contains + # old members who may not have access to the new location, + # so we filter out only members of project or project's group + def members_in_project_and_ancestors + members.where(id: member_user_ids) + end + def guests @guests ||= fetch_members(Gitlab::Access::GUEST) end @@ -191,4 +199,8 @@ class ProjectTeam def group project.group end + + def member_user_ids + Member.on_project_and_ancestors(project).select(:user_id) + 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/snippet.rb b/app/models/snippet.rb index f9b23bbbf6c..f23ddd64fe3 100644 --- a/app/models/snippet.rb +++ b/app/models/snippet.rb @@ -50,11 +50,11 @@ class Snippet < ActiveRecord::Base validates :visibility_level, inclusion: { in: Gitlab::VisibilityLevel.values } # Scopes - scope :are_internal, -> { where(visibility_level: Snippet::INTERNAL) } + scope :are_internal, -> { where(visibility_level: Snippet::INTERNAL) } scope :are_private, -> { where(visibility_level: Snippet::PRIVATE) } scope :are_public, -> { where(visibility_level: Snippet::PUBLIC) } scope :public_and_internal, -> { where(visibility_level: [Snippet::PUBLIC, Snippet::INTERNAL]) } - scope :fresh, -> { order("created_at DESC") } + scope :fresh, -> { order("created_at DESC") } scope :inc_relations_for_view, -> { includes(author: :status) } participant :author 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/user.rb b/app/models/user.rb index 26fd2d903a1..691abe3175f 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -2,7 +2,7 @@ require 'carrierwave/orm/activerecord' -class User < ActiveRecord::Base +class User < ApplicationRecord extend Gitlab::ConfigHelper include Gitlab::ConfigHelper @@ -145,7 +145,7 @@ class User < ActiveRecord::Base has_many :issue_assignees has_many :assigned_issues, class_name: "Issue", through: :issue_assignees, source: :issue - has_many :assigned_merge_requests, dependent: :nullify, foreign_key: :assignee_id, class_name: "MergeRequest" # rubocop:disable Cop/ActiveRecordDependent + has_many :assigned_merge_requests, dependent: :nullify, foreign_key: :assignee_id, class_name: "MergeRequest" # rubocop:disable Cop/ActiveRecordDependent has_many :custom_attributes, class_name: 'UserCustomAttribute' has_many :callouts, class_name: 'UserCallout' @@ -754,8 +754,12 @@ class User < ActiveRecord::Base # # Example use: # `Project.where('EXISTS(?)', user.authorizations_for_projects)` - def authorizations_for_projects - project_authorizations.select(1).where('project_authorizations.project_id = projects.id') + def authorizations_for_projects(min_access_level: nil) + authorizations = project_authorizations.select(1).where('project_authorizations.project_id = projects.id') + + return authorizations unless min_access_level.present? + + authorizations.where('project_authorizations.access_level >= ?', min_access_level) end # Returns the projects this user has reporter (or greater) access to, limited diff --git a/app/policies/board_policy.rb b/app/policies/board_policy.rb new file mode 100644 index 00000000000..46db008421f --- /dev/null +++ b/app/policies/board_policy.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +class BoardPolicy < BasePolicy + delegate { @subject.parent } + + condition(:is_group_board) { @subject.group_board? } + + rule { is_group_board ? can?(:read_group) : can?(:read_project) }.enable :read_parent + + rule { is_group_board & can?(:read_group) }.policy do + enable :read_milestone + enable :read_issue + end +end diff --git a/app/policies/ci/pipeline_policy.rb b/app/policies/ci/pipeline_policy.rb index e42d78f47c5..2c90b8a73cd 100644 --- a/app/policies/ci/pipeline_policy.rb +++ b/app/policies/ci/pipeline_policy.rb @@ -10,6 +10,15 @@ module Ci @subject.project.branch_allows_collaboration?(@user, @subject.ref) end + condition(:external_pipeline, scope: :subject, score: 0) do + @subject.external? + end + + # Disallow users without permissions from accessing internal pipelines + rule { ~can?(:read_build) & ~external_pipeline }.policy do + prevent :read_pipeline + end + rule { protected_ref }.prevent :update_pipeline rule { can?(:public_access) & branch_allows_collaboration }.policy do diff --git a/app/policies/container_repository_policy.rb b/app/policies/container_repository_policy.rb new file mode 100644 index 00000000000..6781c845142 --- /dev/null +++ b/app/policies/container_repository_policy.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class ContainerRepositoryPolicy < BasePolicy + delegate { @subject.project } +end diff --git a/app/policies/issue_policy.rb b/app/policies/issue_policy.rb index a0706eaa46c..dd8c5d49cf4 100644 --- a/app/policies/issue_policy.rb +++ b/app/policies/issue_policy.rb @@ -18,6 +18,7 @@ class IssuePolicy < IssuablePolicy prevent :read_issue_iid prevent :update_issue prevent :admin_issue + prevent :create_note end rule { locked }.policy do diff --git a/app/policies/note_policy.rb b/app/policies/note_policy.rb index f22843b6463..8d23e3abed3 100644 --- a/app/policies/note_policy.rb +++ b/app/policies/note_policy.rb @@ -18,6 +18,7 @@ class NotePolicy < BasePolicy prevent :read_note prevent :admin_note prevent :resolve_note + prevent :award_emoji end rule { is_author }.policy do diff --git a/app/policies/personal_snippet_policy.rb b/app/policies/personal_snippet_policy.rb index 777f933cdcd..2b5cca76c20 100644 --- a/app/policies/personal_snippet_policy.rb +++ b/app/policies/personal_snippet_policy.rb @@ -28,5 +28,10 @@ class PersonalSnippetPolicy < BasePolicy rule { anonymous }.prevent :comment_personal_snippet - rule { can?(:comment_personal_snippet) }.enable :award_emoji + rule { can?(:comment_personal_snippet) }.policy do + enable :create_note + enable :award_emoji + end + + rule { full_private_access }.enable :read_personal_snippet end diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb index 12f9f29dcc1..cadbc5ae009 100644 --- a/app/policies/project_policy.rb +++ b/app/policies/project_policy.rb @@ -108,6 +108,10 @@ class ProjectPolicy < BasePolicy condition(:has_clusters, scope: :subject) { clusterable_has_clusters? } condition(:can_have_multiple_clusters) { multiple_clusters_available? } + condition(:internal_builds_disabled) do + !@subject.builds_enabled? + end + features = %w[ merge_requests issues @@ -196,7 +200,6 @@ class ProjectPolicy < BasePolicy enable :read_build enable :read_container_image enable :read_pipeline - enable :read_pipeline_schedule enable :read_environment enable :read_deployment enable :read_merge_request @@ -235,6 +238,7 @@ class ProjectPolicy < BasePolicy enable :update_build enable :create_pipeline enable :update_pipeline + enable :read_pipeline_schedule enable :create_pipeline_schedule enable :create_merge_request_from enable :create_wiki @@ -314,13 +318,12 @@ class ProjectPolicy < BasePolicy prevent(*create_read_update_admin_destroy(:project_snippet)) end - rule { wiki_disabled & ~has_external_wiki }.policy do + rule { wiki_disabled }.policy do prevent(*create_read_update_admin_destroy(:wiki)) prevent(:download_wiki_code) end rule { builds_disabled | repository_disabled }.policy do - prevent(*create_update_admin_destroy(:pipeline)) prevent(*create_read_update_admin_destroy(:build)) prevent(*create_read_update_admin_destroy(:pipeline_schedule)) prevent(*create_read_update_admin_destroy(:environment)) @@ -328,11 +331,22 @@ class ProjectPolicy < BasePolicy prevent(*create_read_update_admin_destroy(:deployment)) end + # There's two separate cases when builds_disabled is true: + # 1. When internal CI is disabled - builds_disabled && internal_builds_disabled + # - We do not prevent the user from accessing Pipelines to allow him to access external CI + # 2. When the user is not allowed to access CI - builds_disabled && ~internal_builds_disabled + # - We prevent the user from accessing Pipelines + rule { (builds_disabled & ~internal_builds_disabled) | repository_disabled }.policy do + prevent(*create_read_update_admin_destroy(:pipeline)) + prevent(*create_read_update_admin_destroy(:commit_status)) + end + rule { repository_disabled }.policy do prevent :push_code prevent :download_code prevent :fork_project prevent :read_commit_status + prevent :read_pipeline prevent(*create_read_update_admin_destroy(:release)) end @@ -359,7 +373,6 @@ class ProjectPolicy < BasePolicy enable :read_merge_request enable :read_note enable :read_pipeline - enable :read_pipeline_schedule enable :read_commit_status enable :read_container_image enable :download_code @@ -378,7 +391,6 @@ class ProjectPolicy < BasePolicy rule { public_builds & can?(:guest_access) }.policy do enable :read_pipeline - enable :read_pipeline_schedule end # These rules are included to allow maintainers of projects to push to certain @@ -393,7 +405,7 @@ class ProjectPolicy < BasePolicy end.enable :read_issue_iid rule do - (can?(:read_project_for_iids) & merge_requests_visible_to_user) | can?(:read_merge_request) + (~guest & can?(:read_project_for_iids) & merge_requests_visible_to_user) | can?(:read_merge_request) end.enable :read_merge_request_iid rule { ~can_have_multiple_clusters & has_clusters }.prevent :add_cluster diff --git a/app/policies/project_snippet_policy.rb b/app/policies/project_snippet_policy.rb index 7dafa33bb99..e5e005cee6d 100644 --- a/app/policies/project_snippet_policy.rb +++ b/app/policies/project_snippet_policy.rb @@ -43,4 +43,6 @@ class ProjectSnippetPolicy < BasePolicy enable :update_project_snippet enable :admin_project_snippet end + + rule { ~can?(:read_project_snippet) }.prevent :create_note end diff --git a/app/presenters/ci/trigger_presenter.rb b/app/presenters/ci/trigger_presenter.rb new file mode 100644 index 00000000000..605c8f328a4 --- /dev/null +++ b/app/presenters/ci/trigger_presenter.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module Ci + class TriggerPresenter < Gitlab::View::Presenter::Delegated + presents :trigger + + def has_token_exposed? + can?(current_user, :admin_trigger, trigger) + end + + def token + if has_token_exposed? + trigger.token + else + trigger.short_token + end + end + end +end diff --git a/app/presenters/commit_presenter.rb b/app/presenters/commit_presenter.rb new file mode 100644 index 00000000000..05adbe1d4f5 --- /dev/null +++ b/app/presenters/commit_presenter.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class CommitPresenter < Gitlab::View::Presenter::Simple + presents :commit + + def status_for(ref) + can?(current_user, :read_commit_status, commit.project) && commit.status(ref) + end + + def any_pipelines? + can?(current_user, :read_pipeline, commit.project) && commit.pipelines.any? + end +end diff --git a/app/presenters/merge_request_presenter.rb b/app/presenters/merge_request_presenter.rb index 44b6ca299ae..c59e73f824c 100644 --- a/app/presenters/merge_request_presenter.rb +++ b/app/presenters/merge_request_presenter.rb @@ -170,6 +170,10 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated source_branch_exists? && merge_request.can_remove_source_branch?(current_user) end + def can_read_pipeline? + pipeline && can?(current_user, :read_pipeline, pipeline) + end + def mergeable_discussions_state # This avoids calling MergeRequest#mergeable_discussions_state without # considering the state of the MR first. If a MR isn't mergeable, we can 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/cluster_application_entity.rb b/app/serializers/cluster_application_entity.rb index 7b1a0be75ca..62b23a889c8 100644 --- a/app/serializers/cluster_application_entity.rb +++ b/app/serializers/cluster_application_entity.rb @@ -4,6 +4,7 @@ class ClusterApplicationEntity < Grape::Entity expose :name expose :status_name, as: :status expose :status_reason + expose :version expose :external_ip, if: -> (e, _) { e.respond_to?(:external_ip) } expose :hostname, if: -> (e, _) { e.respond_to?(:hostname) } expose :email, if: -> (e, _) { e.respond_to?(:email) } diff --git a/app/serializers/container_repository_entity.rb b/app/serializers/container_repository_entity.rb index 59bf35f5aff..cc746698a05 100644 --- a/app/serializers/container_repository_entity.rb +++ b/app/serializers/container_repository_entity.rb @@ -3,7 +3,7 @@ class ContainerRepositoryEntity < Grape::Entity include RequestAwareEntity - expose :id, :path, :location + expose :id, :name, :path, :location, :created_at expose :tags_path do |repository| project_registry_repository_tags_path(project, repository, format: :json) diff --git a/app/serializers/container_tag_entity.rb b/app/serializers/container_tag_entity.rb index 637294877f8..361c073e22e 100644 --- a/app/serializers/container_tag_entity.rb +++ b/app/serializers/container_tag_entity.rb @@ -3,7 +3,7 @@ class ContainerTagEntity < Grape::Entity include RequestAwareEntity - expose :name, :location, :revision, :short_revision, :total_size, :created_at + expose :name, :path, :location, :digest, :revision, :short_revision, :total_size, :created_at expose :destroy_path, if: -> (*) { can_destroy? } do |tag| project_registry_repository_tag_path(project, tag.repository, tag.name) diff --git a/app/serializers/error_tracking/project_entity.rb b/app/serializers/error_tracking/project_entity.rb new file mode 100644 index 00000000000..405d87ca0d0 --- /dev/null +++ b/app/serializers/error_tracking/project_entity.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +module ErrorTracking + class ProjectEntity < Grape::Entity + expose(*Gitlab::ErrorTracking::Project::ACCESSORS) + end +end diff --git a/app/serializers/error_tracking/project_serializer.rb b/app/serializers/error_tracking/project_serializer.rb new file mode 100644 index 00000000000..b2406f4d631 --- /dev/null +++ b/app/serializers/error_tracking/project_serializer.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +module ErrorTracking + class ProjectSerializer < BaseSerializer + entity ProjectEntity + 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/serializers/merge_request_widget_entity.rb b/app/serializers/merge_request_widget_entity.rb index 9361c9f987b..f42abf06e1e 100644 --- a/app/serializers/merge_request_widget_entity.rb +++ b/app/serializers/merge_request_widget_entity.rb @@ -57,7 +57,7 @@ class MergeRequestWidgetEntity < IssuableEntity end expose :merge_commit_message - expose :actual_head_pipeline, with: PipelineDetailsEntity, as: :pipeline + expose :actual_head_pipeline, with: PipelineDetailsEntity, as: :pipeline, if: -> (mr, _) { presenter(mr).can_read_pipeline? } expose :merge_pipeline, with: PipelineDetailsEntity, if: ->(mr, _) { mr.merged? && can?(request.current_user, :read_pipeline, mr.target_project)} # Booleans diff --git a/app/serializers/pipeline_entity.rb b/app/serializers/pipeline_entity.rb index c9669e59199..29b1a6c244b 100644 --- a/app/serializers/pipeline_entity.rb +++ b/app/serializers/pipeline_entity.rb @@ -59,7 +59,7 @@ class PipelineEntity < Grape::Entity pipeline.present.failure_reason end - expose :retry_path, if: -> (*) { can_retry? } do |pipeline| + expose :retry_path, if: -> (*) { can_retry? } do |pipeline| retry_project_pipeline_path(pipeline.project, pipeline) end diff --git a/app/services/boards/issues/move_service.rb b/app/services/boards/issues/move_service.rb index 43a26f4264e..834baeb9643 100644 --- a/app/services/boards/issues/move_service.rb +++ b/app/services/boards/issues/move_service.rb @@ -50,7 +50,7 @@ module Boards if move_between_ids attrs[:move_between_ids] = move_between_ids - attrs[:board_group_id] = board.group&.id + attrs[:board_group_id] = board.group&.id end attrs diff --git a/app/services/boards/lists/destroy_service.rb b/app/services/boards/lists/destroy_service.rb index 609c430caed..e20805d0405 100644 --- a/app/services/boards/lists/destroy_service.rb +++ b/app/services/boards/lists/destroy_service.rb @@ -20,7 +20,7 @@ module Boards # rubocop: disable CodeReuse/ActiveRecord def decrement_higher_lists(list) - board.lists.movable.where('position > ?', list.position) + board.lists.movable.where('position > ?', list.position) .update_all('position = position - 1') end # rubocop: enable CodeReuse/ActiveRecord diff --git a/app/services/ci/destroy_expired_job_artifacts_service.rb b/app/services/ci/destroy_expired_job_artifacts_service.rb new file mode 100644 index 00000000000..7d2f5d33fed --- /dev/null +++ b/app/services/ci/destroy_expired_job_artifacts_service.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +module Ci + class DestroyExpiredJobArtifactsService + include ::Gitlab::ExclusiveLeaseHelpers + include ::Gitlab::LoopHelpers + + BATCH_SIZE = 100 + LOOP_TIMEOUT = 45.minutes + LOOP_LIMIT = 1000 + EXCLUSIVE_LOCK_KEY = 'expired_job_artifacts:destroy:lock' + LOCK_TIMEOUT = 50.minutes + + ## + # Destroy expired job artifacts on GitLab instance + # + # This destroy process cannot run for more than 45 minutes. This is for + # preventing multiple `ExpireBuildArtifactsWorker` CRON jobs run concurrently, + # which is scheduled at every hour. + def execute + in_lock(EXCLUSIVE_LOCK_KEY, ttl: LOCK_TIMEOUT, retries: 1) do + loop_until(timeout: LOOP_TIMEOUT, limit: LOOP_LIMIT) do + destroy_batch + end + end + end + + private + + def destroy_batch + artifacts = Ci::JobArtifact.expired(BATCH_SIZE).to_a + + return false if artifacts.empty? + + artifacts.each(&:destroy!) + end + end +end diff --git a/app/services/ci/process_pipeline_service.rb b/app/services/ci/process_pipeline_service.rb index 446188347df..4a7ce00b8e2 100644 --- a/app/services/ci/process_pipeline_service.rb +++ b/app/services/ci/process_pipeline_service.rb @@ -10,7 +10,7 @@ module Ci update_retried new_builds = - stage_indexes_of_created_builds.map do |index| + stage_indexes_of_created_processables.map do |index| process_stage(index) end @@ -27,7 +27,7 @@ module Ci return if HasStatus::BLOCKED_STATUS.include?(current_status) if HasStatus::COMPLETED_STATUSES.include?(current_status) - created_builds_in_stage(index).select do |build| + created_processables_in_stage(index).select do |build| Gitlab::OptimisticLocking.retry_lock(build) do |subject| Ci::ProcessBuildService.new(project, @user) .execute(build, current_status) @@ -43,19 +43,19 @@ module Ci # rubocop: enable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord - def stage_indexes_of_created_builds - created_builds.order(:stage_idx).pluck('distinct stage_idx') + def stage_indexes_of_created_processables + created_processables.order(:stage_idx).pluck('distinct stage_idx') end # rubocop: enable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord - def created_builds_in_stage(index) - created_builds.where(stage_idx: index) + def created_processables_in_stage(index) + created_processables.where(stage_idx: index) end # rubocop: enable CodeReuse/ActiveRecord - def created_builds - pipeline.builds.created + def created_processables + pipeline.processables.created end # This method is for compatibility and data consistency and should be removed with 9.3 version of GitLab 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/concerns/exclusive_lease_guard.rb b/app/services/concerns/exclusive_lease_guard.rb index f102e00d150..28879d2d67f 100644 --- a/app/services/concerns/exclusive_lease_guard.rb +++ b/app/services/concerns/exclusive_lease_guard.rb @@ -6,9 +6,14 @@ # # `#try_obtain_lease` takes a block which will be run if it was able to # obtain the lease. Implement `#lease_timeout` to configure the timeout -# for the exclusive lease. Optionally override `#lease_key` to set the +# for the exclusive lease. +# +# Optionally override `#lease_key` to set the # lease key, it defaults to the class name with underscores. # +# Optionally override `#lease_release?` to prevent the job to +# be re-executed more often than LEASE_TIMEOUT. +# module ExclusiveLeaseGuard extend ActiveSupport::Concern @@ -23,7 +28,7 @@ module ExclusiveLeaseGuard begin yield lease ensure - release_lease(lease) + release_lease(lease) if lease_release? end end @@ -40,6 +45,10 @@ module ExclusiveLeaseGuard "#{self.class.name} does not implement #{__method__}" end + def lease_release? + true + end + def release_lease(uuid) Gitlab::ExclusiveLease.cancel(lease_key, uuid) end diff --git a/app/services/delete_branch_service.rb b/app/services/delete_branch_service.rb index 44252f7b0a6..8322a3d74f4 100644 --- a/app/services/delete_branch_service.rb +++ b/app/services/delete_branch_service.rb @@ -14,7 +14,7 @@ class DeleteBranchService < BaseService end if repository.rm_branch(current_user, branch_name) - success('Branch was removed') + success('Branch was deleted') else error('Failed to remove branch') 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/promote_service.rb b/app/services/labels/promote_service.rb index 3c0e6196d4f..e73e6476c12 100644 --- a/app/services/labels/promote_service.rb +++ b/app/services/labels/promote_service.rb @@ -49,7 +49,7 @@ module Labels .new(current_user, title: new_label.title, group_id: project.group.id) .execute(skip_authorization: true) .where.not(id: new_label) - .select(:id) # Can't use pluck() to avoid object-creation because of the batching + .select(:id) # Can't use pluck() to avoid object-creation because of the batching end # rubocop: enable CodeReuse/ActiveRecord diff --git a/app/services/members/destroy_service.rb b/app/services/members/destroy_service.rb index ae0c644e6c0..f9717a9426b 100644 --- a/app/services/members/destroy_service.rb +++ b/app/services/members/destroy_service.rb @@ -2,9 +2,11 @@ module Members class DestroyService < Members::BaseService - def execute(member, skip_authorization: false) + def execute(member, skip_authorization: false, skip_subresources: false) raise Gitlab::Access::AccessDeniedError unless skip_authorization || can_destroy_member?(member) + @skip_auth = skip_authorization + return member if member.is_a?(GroupMember) && member.source.last_owner?(member.user) member.destroy @@ -15,6 +17,7 @@ module Members notification_service.decline_access_request(member) end + delete_subresources(member) unless skip_subresources enqueue_delete_todos(member) after_execute(member: member) @@ -24,6 +27,29 @@ module Members private + def delete_subresources(member) + return unless member.is_a?(GroupMember) && member.user && member.group + + delete_project_members(member) + delete_subgroup_members(member) if Group.supports_nested_objects? + end + + def delete_project_members(member) + groups = member.group.self_and_descendants + + ProjectMember.in_namespaces(groups).with_user(member.user).each do |project_member| + self.class.new(current_user).execute(project_member, skip_authorization: @skip_auth) + end + end + + def delete_subgroup_members(member) + groups = member.group.descendants + + GroupMember.in_groups(groups).with_user(member.user).each do |group_member| + self.class.new(current_user).execute(group_member, skip_authorization: @skip_auth, skip_subresources: true) + end + end + def can_destroy_member?(member) can?(current_user, destroy_member_permission(member), member) 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/notes/build_service.rb b/app/services/notes/build_service.rb index 7b92fe6fe14..bae98ede561 100644 --- a/app/services/notes/build_service.rb +++ b/app/services/notes/build_service.rb @@ -9,7 +9,7 @@ module Notes if in_reply_to_discussion_id.present? discussion = find_discussion(in_reply_to_discussion_id) - unless discussion + unless discussion && can?(current_user, :create_note, discussion.noteable) note = Note.new note.errors.add(:base, 'Discussion to reply to cannot be found') return note @@ -34,19 +34,8 @@ module Notes if project project.notes.find_discussion(discussion_id) else - discussion = Note.find_discussion(discussion_id) - noteable = discussion.noteable - - return nil unless noteable_without_project?(noteable) - - discussion + Note.find_discussion(discussion_id) end end - - def noteable_without_project?(noteable) - return true if noteable.is_a?(PersonalSnippet) && can?(current_user, :comment_personal_snippet, noteable) - - false - end end end diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index e1cf327209b..1a65561dd70 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -373,7 +373,8 @@ class NotificationService end def project_was_moved(project, old_path_with_namespace) - recipients = notifiable_users(project.team.members, :mention, project: project) + recipients = project.private? ? project.team.members_in_project_and_ancestors : project.team.members + recipients = notifiable_users(recipients, :mention, project: project) recipients.each do |recipient| mailer.project_was_moved_email( diff --git a/app/services/projects/after_rename_service.rb b/app/services/projects/after_rename_service.rb index aa9b253eb20..fafdecb3222 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 @@ -57,11 +62,11 @@ module Projects def rename_or_migrate_repository! success = if migrate_to_hashed_storage? - ::Projects::HashedStorageMigrationService + ::Projects::HashedStorage::MigrationService .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/container_repository/cleanup_tags_service.rb b/app/services/projects/container_repository/cleanup_tags_service.rb new file mode 100644 index 00000000000..488290db824 --- /dev/null +++ b/app/services/projects/container_repository/cleanup_tags_service.rb @@ -0,0 +1,94 @@ +# frozen_string_literal: true + +module Projects + module ContainerRepository + class CleanupTagsService < BaseService + def execute(container_repository) + return error('feature disabled') unless can_use? + return error('access denied') unless can_admin? + + tags = container_repository.tags + tags_by_digest = group_by_digest(tags) + + tags = without_latest(tags) + tags = filter_by_name(tags) + tags = with_manifest(tags) + tags = order_by_date(tags) + tags = filter_keep_n(tags) + tags = filter_by_older_than(tags) + + deleted_tags = delete_tags(tags, tags_by_digest) + + success(deleted: deleted_tags.map(&:name)) + end + + private + + def delete_tags(tags_to_delete, tags_by_digest) + deleted_digests = group_by_digest(tags_to_delete).select do |digest, tags| + delete_tag_digest(digest, tags, tags_by_digest[digest]) + end + + deleted_digests.values.flatten + end + + def delete_tag_digest(digest, tags, other_tags) + # Issue: https://gitlab.com/gitlab-org/gitlab-ce/issues/21405 + # we have to remove all tags due + # to Docker Distribution bug unable + # to delete single tag + return unless tags.count == other_tags.count + + # delete all tags + tags.map(&:delete) + end + + def group_by_digest(tags) + tags.group_by(&:digest) + end + + def without_latest(tags) + tags.reject(&:latest?) + end + + def with_manifest(tags) + tags.select(&:valid?) + end + + def order_by_date(tags) + now = DateTime.now + tags.sort_by { |tag| tag.created_at || now }.reverse + end + + def filter_by_name(tags) + regex = Gitlab::UntrustedRegexp.new("\\A#{params['name_regex']}\\z") + + tags.select do |tag| + regex.scan(tag.name).any? + end + end + + def filter_keep_n(tags) + tags.drop(params['keep_n'].to_i) + end + + def filter_by_older_than(tags) + return tags unless params['older_than'] + + older_than = ChronicDuration.parse(params['older_than']).seconds.ago + + tags.select do |tag| + tag.created_at && tag.created_at < older_than + end + end + + def can_admin? + can?(current_user, :admin_container_image, project) + end + + def can_use? + Feature.enabled?(:container_registry_cleanup, project, default_enabled: true) + end + end + end +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/hashed_storage/base_repository_service.rb b/app/services/projects/hashed_storage/base_repository_service.rb new file mode 100644 index 00000000000..761c81d776f --- /dev/null +++ b/app/services/projects/hashed_storage/base_repository_service.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +module Projects + module HashedStorage + # Returned when there is an error with the Hashed Storage migration + RepositoryMigrationError = Class.new(StandardError) + + # Returned when there is an error with the Hashed Storage rollback + RepositoryRollbackError = Class.new(StandardError) + + class BaseRepositoryService < BaseService + include Gitlab::ShellAdapter + + attr_reader :old_disk_path, :new_disk_path, :old_wiki_disk_path, :old_storage_version, :logger, :move_wiki + + def initialize(project, old_disk_path, logger: nil) + @project = project + @logger = logger || Gitlab::AppLogger + @old_disk_path = old_disk_path + @old_wiki_disk_path = "#{old_disk_path}.wiki" + @move_wiki = has_wiki? + end + + protected + + # rubocop: disable CodeReuse/ActiveRecord + def has_wiki? + gitlab_shell.exists?(project.repository_storage, "#{old_wiki_disk_path}.git") + end + # rubocop: enable CodeReuse/ActiveRecord + + # rubocop: disable CodeReuse/ActiveRecord + def move_repository(from_name, to_name) + from_exists = gitlab_shell.exists?(project.repository_storage, "#{from_name}.git") + to_exists = gitlab_shell.exists?(project.repository_storage, "#{to_name}.git") + + # If we don't find the repository on either original or target we should log that as it could be an issue if the + # project was not originally empty. + if !from_exists && !to_exists + logger.warn "Can't find a repository on either source or target paths for #{project.full_path} (ID=#{project.id}) ..." + return false + elsif !from_exists + # Repository have been moved already. + return true + end + + gitlab_shell.mv_repository(project.repository_storage, from_name, to_name) + end + # rubocop: enable CodeReuse/ActiveRecord + + def rollback_folder_move + move_repository(new_disk_path, old_disk_path) + move_repository("#{new_disk_path}.wiki", old_wiki_disk_path) + end + end + end +end diff --git a/app/services/projects/hashed_storage/migrate_attachments_service.rb b/app/services/projects/hashed_storage/migrate_attachments_service.rb index a1f0302aeb7..03e0685d2cd 100644 --- a/app/services/projects/hashed_storage/migrate_attachments_service.rb +++ b/app/services/projects/hashed_storage/migrate_attachments_service.rb @@ -12,6 +12,7 @@ module Projects @logger = logger || Rails.logger @old_disk_path = old_disk_path @new_disk_path = project.disk_path + @skipped = false end def execute @@ -32,24 +33,29 @@ module Projects result end + def skipped? + @skipped + end + private - def move_folder!(old_disk_path, new_disk_path) - unless File.directory?(old_disk_path) - logger.info("Skipped attachments migration from '#{old_disk_path}' to '#{new_disk_path}', source path doesn't exist or is not a directory (PROJECT_ID=#{project.id})") - return + def move_folder!(old_path, new_path) + unless File.directory?(old_path) + logger.info("Skipped attachments migration from '#{old_path}' to '#{new_path}', source path doesn't exist or is not a directory (PROJECT_ID=#{project.id})") + @skipped = true + return true end - if File.exist?(new_disk_path) - logger.error("Cannot migrate attachments from '#{old_disk_path}' to '#{new_disk_path}', target path already exist (PROJECT_ID=#{project.id})") - raise AttachmentMigrationError, "Target path '#{new_disk_path}' already exist" + if File.exist?(new_path) + logger.error("Cannot migrate attachments from '#{old_path}' to '#{new_path}', target path already exist (PROJECT_ID=#{project.id})") + raise AttachmentMigrationError, "Target path '#{new_path}' already exist" end # Create hashed storage base path folder - FileUtils.mkdir_p(File.dirname(new_disk_path)) + FileUtils.mkdir_p(File.dirname(new_path)) - FileUtils.mv(old_disk_path, new_disk_path) - logger.info("Migrated project attachments from '#{old_disk_path}' to '#{new_disk_path}' (PROJECT_ID=#{project.id})") + FileUtils.mv(old_path, new_path) + logger.info("Migrated project attachments from '#{old_path}' to '#{new_path}' (PROJECT_ID=#{project.id})") true end diff --git a/app/services/projects/hashed_storage/migrate_repository_service.rb b/app/services/projects/hashed_storage/migrate_repository_service.rb index 2d851866a18..9c672283c7e 100644 --- a/app/services/projects/hashed_storage/migrate_repository_service.rb +++ b/app/services/projects/hashed_storage/migrate_repository_service.rb @@ -2,21 +2,7 @@ module Projects module HashedStorage - RepositoryMigrationError = Class.new(StandardError) - - class MigrateRepositoryService < BaseService - include Gitlab::ShellAdapter - - attr_reader :old_disk_path, :new_disk_path, :old_wiki_disk_path, :old_storage_version, :logger, :move_wiki - - def initialize(project, old_disk_path, logger: nil) - @project = project - @logger = logger || Rails.logger - @old_disk_path = old_disk_path - @old_wiki_disk_path = "#{old_disk_path}.wiki" - @move_wiki = has_wiki? - end - + class MigrateRepositoryService < BaseRepositoryService def execute try_to_set_repository_read_only! @@ -61,36 +47,6 @@ module Projects raise RepositoryMigrationError, migration_error end end - - # rubocop: disable CodeReuse/ActiveRecord - def has_wiki? - gitlab_shell.exists?(project.repository_storage, "#{old_wiki_disk_path}.git") - end - # rubocop: enable CodeReuse/ActiveRecord - - # rubocop: disable CodeReuse/ActiveRecord - def move_repository(from_name, to_name) - from_exists = gitlab_shell.exists?(project.repository_storage, "#{from_name}.git") - to_exists = gitlab_shell.exists?(project.repository_storage, "#{to_name}.git") - - # If we don't find the repository on either original or target we should log that as it could be an issue if the - # project was not originally empty. - if !from_exists && !to_exists - logger.warn "Can't find a repository on either source or target paths for #{project.full_path} (ID=#{project.id}) ..." - return false - elsif !from_exists - # Repository have been moved already. - return true - end - - gitlab_shell.mv_repository(project.repository_storage, from_name, to_name) - end - # rubocop: enable CodeReuse/ActiveRecord - - def rollback_folder_move - move_repository(new_disk_path, old_disk_path) - move_repository("#{new_disk_path}.wiki", old_wiki_disk_path) - end end end end diff --git a/app/services/projects/hashed_storage/migration_service.rb b/app/services/projects/hashed_storage/migration_service.rb new file mode 100644 index 00000000000..f132dca61c9 --- /dev/null +++ b/app/services/projects/hashed_storage/migration_service.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +module Projects + module HashedStorage + class MigrationService < BaseService + attr_reader :logger, :old_disk_path + + def initialize(project, old_disk_path, logger: nil) + @project = project + @old_disk_path = old_disk_path + @logger = logger || Gitlab::AppLogger + end + + def execute + # Migrate repository from Legacy to Hashed Storage + unless project.hashed_storage?(:repository) + return false unless migrate_repository + end + + # Migrate attachments from Legacy to Hashed Storage + unless project.hashed_storage?(:attachments) + return false unless migrate_attachments + end + + true + end + + private + + def migrate_repository + HashedStorage::MigrateRepositoryService.new(project, old_disk_path, logger: logger).execute + end + + def migrate_attachments + HashedStorage::MigrateAttachmentsService.new(project, old_disk_path, logger: logger).execute + end + end + end +end diff --git a/app/services/projects/hashed_storage_migration_service.rb b/app/services/projects/hashed_storage_migration_service.rb deleted file mode 100644 index a0e734005f8..00000000000 --- a/app/services/projects/hashed_storage_migration_service.rb +++ /dev/null @@ -1,27 +0,0 @@ -# frozen_string_literal: true - -module Projects - class HashedStorageMigrationService < BaseService - attr_reader :logger, :old_disk_path - - def initialize(project, old_disk_path, logger: nil) - @project = project - @old_disk_path = old_disk_path - @logger = logger || Rails.logger - end - - def execute - # Migrate repository from Legacy to Hashed Storage - unless project.hashed_storage?(:repository) - return unless HashedStorage::MigrateRepositoryService.new(project, old_disk_path, logger: logger).execute - end - - # Migrate attachments from Legacy to Hashed Storage - unless project.hashed_storage?(:attachments) - HashedStorage::MigrateAttachmentsService.new(project, old_disk_path, logger: logger).execute - end - - true - end - end -end diff --git a/app/services/projects/import_error_filter.rb b/app/services/projects/import_error_filter.rb new file mode 100644 index 00000000000..a0fc5149bb4 --- /dev/null +++ b/app/services/projects/import_error_filter.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module Projects + # Used by project imports, it removes any potential paths + # included in an error message that could be stored in the DB + class ImportErrorFilter + ERROR_MESSAGE_FILTER = /[^\s]*#{File::SEPARATOR}[^\s]*(?=(\s|\z))/ + FILTER_MESSAGE = '[FILTERED]' + + def self.filter_message(message) + message.gsub(ERROR_MESSAGE_FILTER, FILTER_MESSAGE) + end + end +end diff --git a/app/services/projects/import_service.rb b/app/services/projects/import_service.rb index 0c426faa22d..5861b803996 100644 --- a/app/services/projects/import_service.rb +++ b/app/services/projects/import_service.rb @@ -24,8 +24,16 @@ module Projects import_data success - rescue => e + rescue Gitlab::UrlBlocker::BlockedUrlError => e + Gitlab::Sentry.track_acceptable_exception(e, extra: { project_path: project.full_path, importer: project.import_type }) + error("Error importing repository #{project.safe_import_url} into #{project.full_path} - #{e.message}") + rescue => e + message = Projects::ImportErrorFilter.filter_message(e.message) + + Gitlab::Sentry.track_acceptable_exception(e, extra: { project_path: project.full_path, importer: project.import_type }) + + error("Error importing repository #{project.safe_import_url} into #{project.full_path} - #{message}") end private @@ -35,7 +43,7 @@ module Projects begin Gitlab::UrlBlocker.validate!(project.import_url, ports: Project::VALID_IMPORT_PORTS) rescue Gitlab::UrlBlocker::BlockedUrlError => e - raise Error, "Blocked import URL: #{e.message}" + raise e, "Blocked import URL: #{e.message}" end end @@ -86,11 +94,11 @@ module Projects return unless project.lfs_enabled? - oids_to_download = Projects::LfsPointers::LfsImportService.new(project).execute - download_service = Projects::LfsPointers::LfsDownloadService.new(project) + lfs_objects_to_download = Projects::LfsPointers::LfsImportService.new(project).execute - oids_to_download.each do |oid, link| - download_service.execute(oid, link) + lfs_objects_to_download.each do |lfs_download_object| + Projects::LfsPointers::LfsDownloadService.new(project, lfs_download_object) + .execute end rescue => e # Right now, to avoid aborting the importing process, we silently fail diff --git a/app/services/projects/lfs_pointers/lfs_download_link_list_service.rb b/app/services/projects/lfs_pointers/lfs_download_link_list_service.rb index a837ea82e38..7998976b00a 100644 --- a/app/services/projects/lfs_pointers/lfs_download_link_list_service.rb +++ b/app/services/projects/lfs_pointers/lfs_download_link_list_service.rb @@ -41,16 +41,17 @@ module Projects end def parse_response_links(objects_response) - objects_response.each_with_object({}) do |entry, link_list| + objects_response.each_with_object([]) do |entry, link_list| begin - oid = entry['oid'] link = entry.dig('actions', DOWNLOAD_ACTION, 'href') raise DownloadLinkNotFound unless link - link_list[oid] = add_credentials(link) - rescue DownloadLinkNotFound, URI::InvalidURIError - Rails.logger.error("Link for Lfs Object with oid #{oid} not found or invalid.") + link_list << LfsDownloadObject.new(oid: entry['oid'], + size: entry['size'], + link: add_credentials(link)) + rescue DownloadLinkNotFound, Addressable::URI::InvalidURIError + log_error("Link for Lfs Object with oid #{entry['oid']} not found or invalid.") end end end @@ -70,7 +71,7 @@ module Projects end def add_credentials(link) - uri = URI.parse(link) + uri = Addressable::URI.parse(link) if should_add_credentials?(uri) uri.user = remote_uri.user diff --git a/app/services/projects/lfs_pointers/lfs_download_service.rb b/app/services/projects/lfs_pointers/lfs_download_service.rb index b5128443435..398f00a598d 100644 --- a/app/services/projects/lfs_pointers/lfs_download_service.rb +++ b/app/services/projects/lfs_pointers/lfs_download_service.rb @@ -4,68 +4,93 @@ module Projects module LfsPointers class LfsDownloadService < BaseService - VALID_PROTOCOLS = %w[http https].freeze + SizeError = Class.new(StandardError) + OidError = Class.new(StandardError) - # rubocop: disable CodeReuse/ActiveRecord - def execute(oid, url) - return unless project&.lfs_enabled? && oid.present? && url.present? + attr_reader :lfs_download_object + delegate :oid, :size, :credentials, :sanitized_url, to: :lfs_download_object, prefix: :lfs - return if LfsObject.exists?(oid: oid) + def initialize(project, lfs_download_object) + super(project) - sanitized_uri = sanitize_url!(url) + @lfs_download_object = lfs_download_object + end - with_tmp_file(oid) do |file| - download_and_save_file(file, sanitized_uri) - lfs_object = LfsObject.new(oid: oid, size: file.size, file: file) + # rubocop: disable CodeReuse/ActiveRecord + def execute + return unless project&.lfs_enabled? && lfs_download_object + return error("LFS file with oid #{lfs_oid} has invalid attributes") unless lfs_download_object.valid? + return if LfsObject.exists?(oid: lfs_oid) - project.all_lfs_objects << lfs_object + wrap_download_errors do + download_lfs_file! end - rescue Gitlab::UrlBlocker::BlockedUrlError => e - Rails.logger.error("LFS file with oid #{oid} couldn't be downloaded: #{e.message}") - rescue StandardError => e - Rails.logger.error("LFS file with oid #{oid} couldn't be downloaded from #{sanitized_uri.sanitized_url}: #{e.message}") end # rubocop: enable CodeReuse/ActiveRecord private - def sanitize_url!(url) - Gitlab::UrlSanitizer.new(url).tap do |sanitized_uri| - # Just validate that HTTP/HTTPS protocols are used. The - # subsequent Gitlab::HTTP.get call will do network checks - # based on the settings. - Gitlab::UrlBlocker.validate!(sanitized_uri.sanitized_url, - protocols: VALID_PROTOCOLS) + def wrap_download_errors(&block) + yield + rescue SizeError, OidError, StandardError => e + error("LFS file with oid #{lfs_oid} could't be downloaded from #{lfs_sanitized_url}: #{e.message}") + end + + def download_lfs_file! + with_tmp_file do |tmp_file| + download_and_save_file!(tmp_file) + project.all_lfs_objects << LfsObject.new(oid: lfs_oid, + size: lfs_size, + file: tmp_file) + + success end end - def download_and_save_file(file, sanitized_uri) - response = Gitlab::HTTP.get(sanitized_uri.sanitized_url, headers(sanitized_uri)) do |fragment| + def download_and_save_file!(file) + digester = Digest::SHA256.new + response = Gitlab::HTTP.get(lfs_sanitized_url, download_headers) do |fragment| + digester << fragment file.write(fragment) + + raise_size_error! if file.size > lfs_size end raise StandardError, "Received error code #{response.code}" unless response.success? - end - def headers(sanitized_uri) - query_options.tap do |headers| - credentials = sanitized_uri.credentials + raise_size_error! if file.size != lfs_size + raise_oid_error! if digester.hexdigest != lfs_oid + end - if credentials[:user].present? || credentials[:password].present? + def download_headers + { stream_body: true }.tap do |headers| + if lfs_credentials[:user].present? || lfs_credentials[:password].present? # Using authentication headers in the request - headers[:http_basic_authentication] = [credentials[:user], credentials[:password]] + headers[:basic_auth] = { username: lfs_credentials[:user], password: lfs_credentials[:password] } end end end - def query_options - { stream_body: true } - end - - def with_tmp_file(oid) + def with_tmp_file create_tmp_storage_dir - File.open(File.join(tmp_storage_dir, oid), 'wb') { |file| yield file } + File.open(tmp_filename, 'wb') do |file| + begin + yield file + rescue StandardError => e + # If the lfs file is successfully downloaded it will be removed + # when it is added to the project's lfs files. + # Nevertheless if any excetion raises the file would remain + # in the file system. Here we ensure to remove it + File.unlink(file) if File.exist?(file) + + raise e + end + end + end + + def tmp_filename + File.join(tmp_storage_dir, lfs_oid) end def create_tmp_storage_dir @@ -79,6 +104,20 @@ module Projects def storage_dir @storage_dir ||= Gitlab.config.lfs.storage_path end + + def raise_size_error! + raise SizeError, 'Size mistmatch' + end + + def raise_oid_error! + raise OidError, 'Oid mismatch' + end + + def error(message, http_status = nil) + log_error(message) + + super + end end end end 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_pages_service.rb b/app/services/projects/update_pages_service.rb index eb2478be3cf..5caeb4cfa5f 100644 --- a/app/services/projects/update_pages_service.rb +++ b/app/services/projects/update_pages_service.rb @@ -7,7 +7,11 @@ module Projects BLOCK_SIZE = 32.kilobytes MAX_SIZE = 1.terabyte - SITE_PATH = 'public/'.freeze + PUBLIC_DIR = 'public'.freeze + + # this has to be invalid group name, + # as it shares the namespace with groups + TMP_EXTRACT_PATH = '@pages.tmp'.freeze attr_reader :build @@ -27,12 +31,11 @@ module Projects raise InvalidStateError, 'pages are outdated' unless latest? # Create temporary directory in which we will extract the artifacts - FileUtils.mkdir_p(tmp_path) - Dir.mktmpdir(nil, tmp_path) do |archive_path| + make_secure_tmp_dir(tmp_path) do |archive_path| extract_archive!(archive_path) # Check if we did extract public directory - archive_public_path = File.join(archive_path, 'public') + archive_public_path = File.join(archive_path, PUBLIC_DIR) raise InvalidStateError, 'pages miss the public folder' unless Dir.exist?(archive_public_path) raise InvalidStateError, 'pages are outdated' unless latest? @@ -85,22 +88,18 @@ module Projects raise InvalidStateError, 'missing artifacts metadata' unless build.artifacts_metadata? # Calculate page size after extract - public_entry = build.artifacts_metadata_entry(SITE_PATH, recursive: true) + public_entry = build.artifacts_metadata_entry(PUBLIC_DIR + '/', recursive: true) if public_entry.total_size > max_size raise InvalidStateError, "artifacts for pages are too large: #{public_entry.total_size}" end - # Requires UnZip at least 6.00 Info-ZIP. - # -qq be (very) quiet - # -n never overwrite existing files - # We add * to end of SITE_PATH, because we want to extract SITE_PATH and all subdirectories - site_path = File.join(SITE_PATH, '*') build.artifacts_file.use_file do |artifacts_path| - unless system(*%W(unzip -n #{artifacts_path} #{site_path} -d #{temp_path})) - raise FailedToExtractError, 'pages failed to extract' - end + SafeZip::Extract.new(artifacts_path) + .extract(directories: [PUBLIC_DIR], to: temp_path) end + rescue SafeZip::Extract::Error => e + raise FailedToExtractError, e.message end def deploy_page!(archive_public_path) @@ -139,7 +138,7 @@ module Projects end def tmp_path - @tmp_path ||= File.join(::Settings.pages.path, 'tmp') + @tmp_path ||= File.join(::Settings.pages.path, TMP_EXTRACT_PATH) end def pages_path @@ -147,11 +146,11 @@ module Projects end def public_path - @public_path ||= File.join(pages_path, 'public') + @public_path ||= File.join(pages_path, PUBLIC_DIR) end def previous_public_path - @previous_public_path ||= File.join(pages_path, "public.#{SecureRandom.hex}") + @previous_public_path ||= File.join(pages_path, "#{PUBLIC_DIR}.#{SecureRandom.hex}") end def ref @@ -188,5 +187,15 @@ module Projects def pages_deployments_failed_total_counter @pages_deployments_failed_total_counter ||= Gitlab::Metrics.counter(:pages_deployments_failed_total, "Counter of GitLab Pages deployments which failed") end + + def make_secure_tmp_dir(tmp_path) + FileUtils.mkdir_p(tmp_path) + path = Dir.mktmpdir(nil, tmp_path) + begin + yield(path) + ensure + FileUtils.remove_entry_secure(path) + 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/protected_branches/api_service.rb b/app/services/protected_branches/api_service.rb index 4340d3e8260..9b85e13107b 100644 --- a/app/services/protected_branches/api_service.rb +++ b/app/services/protected_branches/api_service.rb @@ -6,8 +6,6 @@ module ProtectedBranches @push_params = AccessLevelParams.new(:push, params) @merge_params = AccessLevelParams.new(:merge, params) - verify_params! - protected_branch_params = { name: params[:name], push_access_levels_attributes: @push_params.access_levels, @@ -16,11 +14,5 @@ module ProtectedBranches ::ProtectedBranches::CreateService.new(@project, @current_user, protected_branch_params).execute end - - private - - def verify_params! - # EE-only - end end end diff --git a/app/services/suggestions/apply_service.rb b/app/services/suggestions/apply_service.rb index cc47b46b527..1f720fc835f 100644 --- a/app/services/suggestions/apply_service.rb +++ b/app/services/suggestions/apply_service.rb @@ -11,7 +11,7 @@ module Suggestions return error('Suggestion is not appliable') end - unless latest_diff_refs?(suggestion) + unless latest_source_head?(suggestion) return error('The file has been changed') end @@ -29,12 +29,13 @@ module Suggestions private - # 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 + # Checks whether the latest source branch HEAD matches with + # the position HEAD we're using to update the file content. Since + # the persisted HEAD is updated async (for MergeRequest), + # it's more consistent to fetch this data directly from the + # repository. + def latest_source_head?(suggestion) + suggestion.position.head_sha == suggestion.noteable.source_branch_sha end def file_update_params(suggestion) 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/views/admin/application_settings/_repository_storage.html.haml b/app/views/admin/application_settings/_repository_storage.html.haml index c6c29ed1f21..7a2bbfcdc4d 100644 --- a/app/views/admin/application_settings/_repository_storage.html.haml +++ b/app/views/admin/application_settings/_repository_storage.html.haml @@ -11,7 +11,6 @@ .form-text.text-muted Enable immutable, hash-based paths and repository names to store repositories on disk. This prevents repositories from having to be moved or renamed when the Project URL changes and may improve disk I/O performance. - %em (EXPERIMENTAL) .form-group = f.label :repository_storages, 'Storage paths for new projects', class: 'label-bold' = f.select :repository_storages, repository_storages_options_for_select(@application_setting.repository_storages), diff --git a/app/views/admin/groups/show.html.haml b/app/views/admin/groups/show.html.haml index da2ebb08405..93da87538bc 100644 --- a/app/views/admin/groups/show.html.haml +++ b/app/views/admin/groups/show.html.haml @@ -104,7 +104,7 @@ - link_to_help = link_to(_("here"), help_page_path("user/permissions")) = _('Read more about project permissions <strong>%{link_to_help}</strong>').html_safe % { link_to_help: link_to_help } - = form_tag admin_group_members_update_path(@group), id: "new_project_member", class: "bulk_import", method: :put do + = form_tag admin_group_members_update_path(@group), id: "new_project_member", class: "bulk_import", method: :put do %div = users_select_tag(:user_ids, multiple: true, email_user: true, skip_ldap: @group.ldap_synced?, scope: :all) .prepend-top-10 diff --git a/app/views/admin/runners/_runner.html.haml b/app/views/admin/runners/_runner.html.haml index 829d2c8949f..ec57eb1ed08 100644 --- a/app/views/admin/runners/_runner.html.haml +++ b/app/views/admin/runners/_runner.html.haml @@ -18,19 +18,19 @@ .table-mobile-content = link_to runner.short_sha, admin_runner_path(runner) - .table-section.section-15 + .table-section.section-20 .table-mobile-header{ role: 'rowheader' }= _('Description') .table-mobile-content.str-truncated.has-tooltip{ title: runner.description } = runner.description - .table-section.section-15 + .table-section.section-10 .table-mobile-header{ role: 'rowheader' }= _('Version') .table-mobile-content.str-truncated.has-tooltip{ title: runner.version } = runner.version .table-section.section-10 .table-mobile-header{ role: 'rowheader' }= _('IP Address') - .table-mobile-content + .table-mobile-content.str-truncated.has-tooltip{ title: runner.ip_address } = runner.ip_address .table-section.section-5 diff --git a/app/views/admin/runners/index.html.haml b/app/views/admin/runners/index.html.haml index e9e4e0847d3..81380587fd2 100644 --- a/app/views/admin/runners/index.html.haml +++ b/app/views/admin/runners/index.html.haml @@ -106,8 +106,8 @@ .gl-responsive-table-row.table-row-header{ role: 'row' } .table-section.section-10{ role: 'rowheader' }= _('Type') .table-section.section-10{ role: 'rowheader' }= _('Runner token') - .table-section.section-15{ role: 'rowheader' }= _('Description') - .table-section.section-15{ role: 'rowheader' }= _('Version') + .table-section.section-20{ role: 'rowheader' }= _('Description') + .table-section.section-10{ role: 'rowheader' }= _('Version') .table-section.section-10{ role: 'rowheader' }= _('IP Address') .table-section.section-5{ role: 'rowheader' }= _('Projects') .table-section.section-5{ role: 'rowheader' }= _('Jobs') diff --git a/app/views/ci/status/_dropdown_graph_badge.html.haml b/app/views/ci/status/_dropdown_graph_badge.html.haml index 9de9143e8b1..c787d7420b7 100644 --- a/app/views/ci/status/_dropdown_graph_badge.html.haml +++ b/app/views/ci/status/_dropdown_graph_badge.html.haml @@ -6,7 +6,7 @@ - tooltip = "#{subject.name} - #{status.status_tooltip}" - if status.has_details? - = link_to status.details_path, class: 'mini-pipeline-graph-dropdown-item', data: { toggle: 'tooltip', title: tooltip, container: 'body' } do + = link_to status.details_path, class: 'mini-pipeline-graph-dropdown-item', data: { toggle: 'tooltip', title: tooltip, container: 'body' } do %span{ class: klass }= sprite_icon(status.icon) %span.ci-build-text= subject.name @@ -16,5 +16,5 @@ %span.ci-build-text= subject.name - if status.has_action? - = link_to status.action_path, class: "ci-action-icon-container ci-action-icon-wrapper js-ci-action-icon", method: status.action_method, data: { toggle: 'tooltip', title: status.action_title, container: 'body' } do + = link_to status.action_path, class: "ci-action-icon-container ci-action-icon-wrapper js-ci-action-icon", method: status.action_method, data: { toggle: 'tooltip', title: status.action_title, container: 'body' } do = sprite_icon(status.action_icon, css_class: "icon-action-#{status.action_icon}") diff --git a/app/views/clusters/clusters/_integration_form.html.haml b/app/views/clusters/clusters/_form.html.haml index 5e451f60c9d..4c47e11927e 100644 --- a/app/views/clusters/clusters/_integration_form.html.haml +++ b/app/views/clusters/clusters/_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/clusters/clusters/_gcp_signup_offer_banner.html.haml b/app/views/clusters/clusters/_gcp_signup_offer_banner.html.haml index 85d1002243b..a9299af8d78 100644 --- a/app/views/clusters/clusters/_gcp_signup_offer_banner.html.haml +++ b/app/views/clusters/clusters/_gcp_signup_offer_banner.html.haml @@ -8,5 +8,5 @@ %h4= s_('ClusterIntegration|Did you know?') %p= s_('ClusterIntegration|Every new Google Cloud Platform (GCP) account receives $300 in credit upon %{sign_up_link}. In partnership with Google, GitLab is able to offer an additional $200 for both new and existing GCP accounts to get started with GitLab\'s Google Kubernetes Engine Integration.').html_safe % { sign_up_link: link } %a.btn.btn-default{ href: 'https://goo.gl/AaJzRW', target: '_blank', rel: 'noopener noreferrer' } - Apply for credit + = s_("ClusterIntegration|Apply for credit") diff --git a/app/views/clusters/clusters/gcp/_show.html.haml b/app/views/clusters/clusters/gcp/_show.html.haml deleted file mode 100644 index e9f05eaf453..00000000000 --- a/app/views/clusters/clusters/gcp/_show.html.haml +++ /dev/null @@ -1,50 +0,0 @@ -.form-group - %label.append-bottom-10{ for: 'cluster-name' } - = s_('ClusterIntegration|Kubernetes cluster name') - .input-group - %input.form-control.cluster-name.js-select-on-focus{ value: @cluster.name, readonly: true } - %span.input-group-append - = clipboard_button(text: @cluster.name, title: s_('ClusterIntegration|Copy Kubernetes cluster name'), class: 'input-group-text btn-default') - -= form_for @cluster, url: clusterable.cluster_path(@cluster), as: :cluster do |field| - = form_errors(@cluster) - - = field.fields_for :platform_kubernetes, @cluster.platform_kubernetes do |platform_kubernetes_field| - .form-group - = platform_kubernetes_field.label :api_url, s_('ClusterIntegration|API URL') - .input-group - = platform_kubernetes_field.text_field :api_url, class: 'form-control js-select-on-focus', placeholder: s_('ClusterIntegration|API URL'), readonly: true - %span.input-group-append - = clipboard_button(text: @cluster.platform_kubernetes.api_url, title: s_('ClusterIntegration|Copy API URL'), class: 'input-group-text btn-default') - - .form-group - = platform_kubernetes_field.label :ca_cert, s_('ClusterIntegration|CA Certificate') - .input-group - = platform_kubernetes_field.text_area :ca_cert, class: 'form-control js-select-on-focus', placeholder: s_('ClusterIntegration|Certificate Authority bundle (PEM format)'), readonly: true - %span.input-group-append.clipboard-addon - = clipboard_button(text: @cluster.platform_kubernetes.ca_cert, title: s_('ClusterIntegration|Copy CA Certificate'), class: 'input-group-text btn-blank') - - .form-group - = platform_kubernetes_field.label :token, s_('ClusterIntegration|Token') - .input-group - = platform_kubernetes_field.text_field :token, class: 'form-control js-cluster-token js-select-on-focus', type: 'password', placeholder: s_('ClusterIntegration|Token'), readonly: true - %span.input-group-append - %button.btn.btn-default.input-group-text.js-show-cluster-token{ type: 'button' } - = s_('ClusterIntegration|Show') - = clipboard_button(text: @cluster.platform_kubernetes.token, title: s_('ClusterIntegration|Copy Token'), class: 'btn-default') - - - if @cluster.allow_user_defined_namespace? - .form-group - = platform_kubernetes_field.label :namespace, s_('ClusterIntegration|Project namespace (optional, unique)') - = platform_kubernetes_field.text_field :namespace, class: 'form-control', placeholder: s_('ClusterIntegration|Project namespace') - - .form-group - .form-check - = platform_kubernetes_field.check_box :authorization_type, { class: 'form-check-input', disabled: true }, 'rbac', 'abac' - = platform_kubernetes_field.label :authorization_type, s_('ClusterIntegration|RBAC-enabled cluster'), class: 'form-check-label label-bold' - .form-text.text-muted - = s_('ClusterIntegration|Enable this setting if using role-based access control (RBAC).') - = s_('ClusterIntegration|This option will allow you to install applications on RBAC clusters.') - - .form-group - = field.submit s_('ClusterIntegration|Save changes'), class: 'btn btn-success' diff --git a/app/views/clusters/clusters/index.html.haml b/app/views/clusters/clusters/index.html.haml index 58d0a304363..9bab3bf56aa 100644 --- a/app/views/clusters/clusters/index.html.haml +++ b/app/views/clusters/clusters/index.html.haml @@ -1,5 +1,5 @@ -- breadcrumb_title 'Kubernetes' -- page_title "Kubernetes Clusters" +- breadcrumb_title _('Kubernetes') +- page_title _('Kubernetes Clusters') = render_gcp_signup_offer @@ -9,12 +9,12 @@ - else .top-area.adjust .nav-text - = s_("ClusterIntegration|Kubernetes clusters can be used to deploy applications and to provide Review Apps for this project") + = s_('ClusterIntegration|Kubernetes clusters can be used to deploy applications and to provide Review Apps for this project') = render 'clusters/clusters/buttons' - if @has_ancestor_clusters .bs-callout.bs-callout-info - = s_("ClusterIntegration|Clusters are utilized by selecting the nearest ancestor with a matching environment scope. For example, project clusters will override group clusters.") + = s_('ClusterIntegration|Clusters are utilized by selecting the nearest ancestor with a matching environment scope. For example, project clusters will override group clusters.') %strong = link_to _('More information'), help_page_path('user/group/clusters/', anchor: 'cluster-precedence') diff --git a/app/views/clusters/clusters/new.html.haml b/app/views/clusters/clusters/new.html.haml index eeeef6bd824..6a8af23e5e8 100644 --- a/app/views/clusters/clusters/new.html.haml +++ b/app/views/clusters/clusters/new.html.haml @@ -1,5 +1,5 @@ -- breadcrumb_title 'Kubernetes' -- page_title _("Kubernetes Cluster") +- breadcrumb_title _('Kubernetes') +- page_title _('Kubernetes Cluster') - active_tab = local_assigns.fetch(:active_tab, 'gcp') = javascript_include_tag 'https://apis.google.com/js/api.js' diff --git a/app/views/clusters/clusters/show.html.haml b/app/views/clusters/clusters/show.html.haml index 89a2dfdd69f..1ef76ef801e 100644 --- a/app/views/clusters/clusters/show.html.haml +++ b/app/views/clusters/clusters/show.html.haml @@ -1,7 +1,7 @@ - @content_class = "limit-container-width" unless fluid_layout -- add_to_breadcrumbs "Kubernetes Clusters", clusterable.index_path +- add_to_breadcrumbs _('Kubernetes Clusters'), clusterable.index_path - breadcrumb_title @cluster.name -- page_title _("Kubernetes Cluster") +- page_title _('Kubernetes Cluster') - manage_prometheus_path = edit_project_service_path(@cluster.project, 'prometheus') if @project - expanded = Rails.env.test? @@ -31,7 +31,7 @@ %section#cluster-integration %h4= @cluster.name = render 'banner' - = render 'integration_form' + = render 'form' .cluster-applications-table#js-cluster-applications @@ -39,19 +39,16 @@ .settings-header %h4= s_('ClusterIntegration|Kubernetes cluster details') %button.btn.js-settings-toggle{ type: 'button' } - = expanded ? 'Collapse' : 'Expand' + = expanded ? _('Collapse') : _('Expand') %p= s_('ClusterIntegration|See and edit the details for your Kubernetes cluster') .settings-content - - if @cluster.managed? - = render 'clusters/clusters/gcp/show' - - else - = render 'clusters/clusters/user/show' + = render 'clusters/platforms/kubernetes/form', cluster: @cluster, platform: @cluster.platform_kubernetes, update_cluster_url_path: clusterable.cluster_path(@cluster) %section.settings.no-animate#js-cluster-advanced-settings{ class: ('expanded' if expanded) } .settings-header %h4= _('Advanced settings') %button.btn.js-settings-toggle{ type: 'button' } - = expanded ? 'Collapse' : 'Expand' + = expanded ? _('Collapse') : _('Expand') %p= s_("ClusterIntegration|Advanced options on this Kubernetes cluster's integration") .settings-content = render 'advanced_settings' diff --git a/app/views/clusters/clusters/user/_show.html.haml b/app/views/clusters/clusters/user/_show.html.haml deleted file mode 100644 index cac8e72edd3..00000000000 --- a/app/views/clusters/clusters/user/_show.html.haml +++ /dev/null @@ -1,39 +0,0 @@ -= form_for @cluster, url: clusterable.cluster_path(@cluster), as: :cluster do |field| - = form_errors(@cluster) - .form-group - = field.label :name, s_('ClusterIntegration|Kubernetes cluster name'), class: 'label-bold' - = field.text_field :name, class: 'form-control', placeholder: s_('ClusterIntegration|Kubernetes cluster name') - - = field.fields_for :platform_kubernetes, @cluster.platform_kubernetes do |platform_kubernetes_field| - .form-group - = platform_kubernetes_field.label :api_url, s_('ClusterIntegration|API URL'), class: 'label-bold' - = platform_kubernetes_field.text_field :api_url, class: 'form-control', placeholder: s_('ClusterIntegration|API URL') - - .form-group - = platform_kubernetes_field.label :ca_cert, s_('ClusterIntegration|CA Certificate'), class: 'label-bold' - = platform_kubernetes_field.text_area :ca_cert, class: 'form-control', placeholder: s_('ClusterIntegration|Certificate Authority bundle (PEM format)') - - .form-group - = platform_kubernetes_field.label :token, s_('ClusterIntegration|Token'), class: 'label-bold' - .input-group - = platform_kubernetes_field.text_field :token, class: 'form-control js-cluster-token', type: 'password', placeholder: s_('ClusterIntegration|Token'), autocomplete: 'off' - %span.input-group-append.clipboard-addon - .input-group-text - %button.js-show-cluster-token.btn-blank{ type: 'button' } - = s_('ClusterIntegration|Show') - - - if @cluster.allow_user_defined_namespace? - .form-group - = platform_kubernetes_field.label :namespace, s_('ClusterIntegration|Project namespace (optional, unique)'), class: 'label-bold' - = platform_kubernetes_field.text_field :namespace, class: 'form-control', placeholder: s_('ClusterIntegration|Project namespace') - - .form-group - .form-check - = platform_kubernetes_field.check_box :authorization_type, { class: 'form-check-input', disabled: true }, 'rbac', 'abac' - = platform_kubernetes_field.label :authorization_type, s_('ClusterIntegration|RBAC-enabled cluster'), class: 'form-check-label label-bold' - .form-text.text-muted - = s_('ClusterIntegration|Enable this setting if using role-based access control (RBAC).') - = s_('ClusterIntegration|This option will allow you to install applications on RBAC clusters.') - - .form-group - = field.submit s_('ClusterIntegration|Save changes'), class: 'btn btn-success' diff --git a/app/views/clusters/platforms/kubernetes/_form.html.haml b/app/views/clusters/platforms/kubernetes/_form.html.haml new file mode 100644 index 00000000000..4a452b83112 --- /dev/null +++ b/app/views/clusters/platforms/kubernetes/_form.html.haml @@ -0,0 +1,58 @@ += form_for cluster, url: update_cluster_url_path, as: :cluster do |field| + = form_errors(cluster) + + .form-group + - if cluster.managed? + %label.append-bottom-10{ for: 'cluster-name' } + = s_('ClusterIntegration|Kubernetes cluster name') + .input-group + %input.form-control.cluster-name.js-select-on-focus{ value: cluster.name, readonly: true } + %span.input-group-append + = clipboard_button(text: cluster.name, title: s_('ClusterIntegration|Copy Kubernetes cluster name'), class: 'input-group-text btn-default') + - else + = field.label :name, s_('ClusterIntegration|Kubernetes cluster name'), class: 'label-bold' + .input-group + = field.text_field :name, class: 'form-control', placeholder: s_('ClusterIntegration|Kubernetes cluster name') + + = field.fields_for :platform_kubernetes, platform do |platform_field| + .form-group + = platform_field.label :api_url, s_('ClusterIntegration|API URL') + .input-group + = platform_field.text_field :api_url, class: 'form-control js-select-on-focus', placeholder: s_('ClusterIntegration|API URL'), readonly: cluster.managed? + - if cluster.managed? + %span.input-group-append + = clipboard_button(text: platform.api_url, title: s_('ClusterIntegration|Copy API URL'), class: 'input-group-text btn-default') + + .form-group + = platform_field.label :ca_cert, s_('ClusterIntegration|CA Certificate') + .input-group + = platform_field.text_area :ca_cert, class: 'form-control js-select-on-focus', placeholder: s_('ClusterIntegration|Certificate Authority bundle (PEM format)'), readonly: cluster.managed? + - if cluster.managed? + %span.input-group-append.clipboard-addon + = clipboard_button(text: platform.ca_cert, title: s_('ClusterIntegration|Copy CA Certificate'), class: 'input-group-text btn-blank') + + .form-group + = platform_field.label :token, s_('ClusterIntegration|Token') + .input-group + = platform_field.text_field :token, class: 'form-control js-cluster-token js-select-on-focus', type: 'password', placeholder: s_('ClusterIntegration|Token'), readonly: cluster.managed? + %span.input-group-append + %button.btn.btn-default.input-group-text.js-show-cluster-token{ type: 'button' } + = s_('ClusterIntegration|Show') + - if cluster.managed? + = clipboard_button(text: platform.token, title: s_('ClusterIntegration|Copy Token'), class: 'btn-default') + + - if cluster.allow_user_defined_namespace? + .form-group + = platform_field.label :namespace, s_('ClusterIntegration|Project namespace (optional, unique)') + = platform_field.text_field :namespace, class: 'form-control', placeholder: s_('ClusterIntegration|Project namespace') + + .form-group + .form-check + = platform_field.check_box :authorization_type, { class: 'form-check-input', disabled: true }, 'rbac', 'abac' + = platform_field.label :authorization_type, s_('ClusterIntegration|RBAC-enabled cluster'), class: 'form-check-label label-bold' + .form-text.text-muted + = s_('ClusterIntegration|Enable this setting if using role-based access control (RBAC).') + = s_('ClusterIntegration|This option will allow you to install applications on RBAC clusters.') + + .form-group + = field.submit s_('ClusterIntegration|Save changes'), class: 'btn btn-success' 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/dashboard/groups/index.html.haml b/app/views/dashboard/groups/index.html.haml index 2f7add600e4..19b06ba5cdd 100644 --- a/app/views/dashboard/groups/index.html.haml +++ b/app/views/dashboard/groups/index.html.haml @@ -1,6 +1,6 @@ - @hide_top_links = true - page_title "Groups" -- header_title "Groups", dashboard_groups_path +- header_title "Groups", dashboard_groups_path = render_if_exists "shared/gold_trial_callout" = render 'dashboard/groups_head' diff --git a/app/views/dashboard/milestones/index.html.haml b/app/views/dashboard/milestones/index.html.haml index ae0e38bf0ee..13822d36f15 100644 --- a/app/views/dashboard/milestones/index.html.haml +++ b/app/views/dashboard/milestones/index.html.haml @@ -13,6 +13,8 @@ .top-area = render 'shared/milestones_filter', counts: @milestone_states + .nav-controls + = render 'shared/milestones/search_form' .milestones %ul.content-list diff --git a/app/views/devise/mailer/_confirmation_instructions_secondary.html.haml b/app/views/devise/mailer/_confirmation_instructions_secondary.html.haml index 3d0a1f622a5..ccc3e734276 100644 --- a/app/views/devise/mailer/_confirmation_instructions_secondary.html.haml +++ b/app/views/devise/mailer/_confirmation_instructions_secondary.html.haml @@ -1,5 +1,5 @@ #content - = email_default_heading("#{@resource.user.name}, you've added an additional email!") + = email_default_heading("#{sanitize_name(@resource.user.name)}, you've added an additional email!") %p Click the link below to confirm your email address (#{@resource.email}) #cta = link_to 'Confirm your email address', confirmation_url(@resource, confirmation_token: @token) diff --git a/app/views/devise/passwords/edit.html.haml b/app/views/devise/passwords/edit.html.haml index 4b8ad5acd5b..dd1edb5fdc9 100644 --- a/app/views/devise/passwords/edit.html.haml +++ b/app/views/devise/passwords/edit.html.haml @@ -7,7 +7,7 @@ = f.hidden_field :reset_password_token .form-group = f.label 'New password', for: "user_password" - = f.password_field :password, class: "form-control top qa-password-field", required: true, title: 'This field is required' + = f.password_field :password, class: "form-control top qa-password-field", required: true, title: 'This field is required' .form-group = f.label 'Confirm new password', for: "user_password_confirmation" = f.password_field :password_confirmation, class: "form-control bottom qa-password-confirmation", title: 'This field is required', required: true diff --git a/app/views/devise/sessions/two_factor.html.haml b/app/views/devise/sessions/two_factor.html.haml index fefdf5f9531..f49cdfbf8da 100644 --- a/app/views/devise/sessions/two_factor.html.haml +++ b/app/views/devise/sessions/two_factor.html.haml @@ -7,7 +7,7 @@ - resource_params = params[resource_name].presence || params = f.hidden_field :remember_me, value: resource_params.fetch(:remember_me, 0) %div - = f.label 'Two-Factor Authentication code', name: :otp_attempt + = f.label 'Two-Factor Authentication code', name: :otp_attempt = f.text_field :otp_attempt, class: 'form-control', required: true, autofocus: true, autocomplete: 'off', title: 'This field is required.' %p.form-text.text-muted.hint Enter the code from the two-factor app on your mobile device. If you've lost your device, you may enter one of your recovery codes. .prepend-top-20 diff --git a/app/views/devise/shared/_omniauth_box.html.haml b/app/views/devise/shared/_omniauth_box.html.haml index 12271ee5adb..1b583ea85d6 100644 --- a/app/views/devise/shared/_omniauth_box.html.haml +++ b/app/views/devise/shared/_omniauth_box.html.haml @@ -5,7 +5,7 @@ .d-flex.justify-content-between.flex-wrap - providers.each do |provider| - has_icon = provider_has_icon?(provider) - = link_to omniauth_authorize_path(:user, provider), method: :post, class: 'btn d-flex align-items-center omniauth-btn text-left oauth-login qa-saml-login-button', id: "oauth-login-#{provider}" do + = link_to omniauth_authorize_path(:user, provider), method: :post, class: "btn d-flex align-items-center omniauth-btn text-left oauth-login #{qa_class_for_provider(provider)}", id: "oauth-login-#{provider}" do - if has_icon = provider_image_tag(provider) %span diff --git a/app/views/doorkeeper/authorizations/new.html.haml b/app/views/doorkeeper/authorizations/new.html.haml index 74791b81ccd..dae9a7acf6b 100644 --- a/app/views/doorkeeper/authorizations/new.html.haml +++ b/app/views/doorkeeper/authorizations/new.html.haml @@ -31,7 +31,7 @@ %strong= t scope, scope: [:doorkeeper, :scopes] .text-secondary= t scope, scope: [:doorkeeper, :scope_desc] .form-actions.text-right - = form_tag oauth_authorization_path, method: :delete, class: 'inline' do + = form_tag oauth_authorization_path, method: :delete, class: 'inline' do = hidden_field_tag :client_id, @pre_auth.client.uid = hidden_field_tag :redirect_uri, @pre_auth.redirect_uri = hidden_field_tag :state, @pre_auth.state diff --git a/app/views/events/event/_note.html.haml b/app/views/events/event/_note.html.haml index fb0d2c3b8b0..90ed8e41d32 100644 --- a/app/views/events/event/_note.html.haml +++ b/app/views/events/event/_note.html.haml @@ -21,6 +21,6 @@ = link_to note.attachment.url, target: '_blank' do = image_tag note.attachment.url, class: 'note-image-attach' - else - = link_to note.attachment.url, target: '_blank', class: 'note-file-attach' do + = link_to note.attachment.url, target: '_blank', class: 'note-file-attach' do %i.fa.fa-paperclip = note.attachment_identifier diff --git a/app/views/groups/_home_panel.html.haml b/app/views/groups/_home_panel.html.haml index 88e401081f4..3a8d95f44d1 100644 --- a/app/views/groups/_home_panel.html.haml +++ b/app/views/groups/_home_panel.html.haml @@ -1,17 +1,58 @@ -.group-home-panel.text-center.border-bottom - %div{ class: container_class } - .avatar-container.s70.group-avatar - = group_icon(@group, class: "avatar s70 avatar-tile") - %h1.group-title - = @group.name - %span.visibility-icon.has-tooltip{ data: { container: 'body' }, title: visibility_icon_description(@group) } - = visibility_level_icon(@group.visibility_level, fw: false) +- can_create_subgroups = can?(current_user, :create_subgroup, @group) - - if @group.description.present? - .group-home-desc - = markdown_field(@group, :description) +.group-home-panel + .row.mb-3 + .home-panel-title-row.col-md-12.col-lg-6.d-flex + .avatar-container.home-panel-avatar.append-right-default.float-none + = group_icon(@group, class: 'avatar avatar-tile s64', width: 64, height: 64) + .d-flex.flex-column.flex-wrap.align-items-baseline + .d-inline-flex.align-items-baseline + %h1.home-panel-title.prepend-top-8.append-bottom-5 + = @group.name + %span.visibility-icon.text-secondary.prepend-left-4.has-tooltip{ data: { container: 'body' }, title: visibility_icon_description(@group) } + = visibility_level_icon(@group.visibility_level, fw: false, options: {class: 'icon'}) + .home-panel-metadata.d-flex.align-items-center.text-secondary + %span + = _("Group") + - if current_user + %span.access-request-links.prepend-left-8 + = render 'shared/members/access_request_links', source: @group - - if current_user - .group-buttons.d-none.d-sm-block - = render 'shared/members/access_request_buttons', source: @group - = render 'shared/notifications/button', notification_setting: @notification_setting + .home-panel-buttons.col-md-12.col-lg-6.d-inline-flex.flex-wrap.justify-content-lg-end + - if current_user + .group-buttons + = render 'shared/notifications/new_button', notification_setting: @notification_setting, btn_class: 'btn' + - if can? current_user, :create_projects, @group + - new_project_label = _("New project") + - new_subgroup_label = _("New subgroup") + - if can_create_subgroups + .btn-group.new-project-subgroup.droplab-dropdown.home-panel-action-button.prepend-top-default.js-new-project-subgroup.qa-new-project-or-subgroup-dropdown{ data: { project_path: new_project_path(namespace_id: @group.id), subgroup_path: new_group_path(parent_id: @group.id) } } + %input.btn.btn-success.dropdown-primary.js-new-group-child.qa-new-in-group-button{ type: "button", value: new_project_label, data: { action: "new-project" } } + %button.btn.btn-success.dropdown-toggle.js-dropdown-toggle.qa-new-project-or-subgroup-dropdown-toggle{ type: "button", data: { "dropdown-trigger" => "#new-project-or-subgroup-dropdown", 'display' => 'static' } } + = sprite_icon("arrow-down", css_class: "icon dropdown-btn-icon") + %ul#new-project-or-subgroup-dropdown.dropdown-menu.dropdown-menu-right{ data: { dropdown: true } } + %li.droplab-item-selected.qa-new-project-option{ role: "button", data: { value: "new-project", text: new_project_label } } + .menu-item + .icon-container + = icon("check", class: "list-item-checkmark") + .description + %strong= new_project_label + %span= s_("GroupsTree|Create a project in this group.") + %li.divider.droplap-item-ignore + %li.qa-new-subgroup-option{ role: "button", data: { value: "new-subgroup", text: new_subgroup_label } } + .menu-item + .icon-container + = icon("check", class: "list-item-checkmark") + .description + %strong= new_subgroup_label + %span= s_("GroupsTree|Create a subgroup in this group.") + - else + = link_to new_project_label, new_project_path(namespace_id: @group.id), class: "btn btn-success" + + - if @group.description.present? + .group-home-desc.mt-1 + .home-panel-description + .home-panel-description-markdown.read-more-container + = markdown_field(@group, :description) + %button.btn.btn-blank.btn-link.js-read-more-trigger.d-lg-none{ type: "button" } + = _("Read more") diff --git a/app/views/groups/group_members/index.html.haml b/app/views/groups/group_members/index.html.haml index 13d584f5f1d..2af3e861587 100644 --- a/app/views/groups/group_members/index.html.haml +++ b/app/views/groups/group_members/index.html.haml @@ -23,7 +23,7 @@ Members with access to %strong= @group.name %span.badge= @members.total_count - = form_tag group_group_members_path(@group), method: :get, class: 'form-inline member-search-form flex-project-members-form' do + = form_tag group_group_members_path(@group), method: :get, class: 'form-inline member-search-form flex-project-members-form' do .form-group .position-relative.append-right-8 = search_field_tag :search, params[:search], { placeholder: 'Find existing members by name', class: 'form-control', spellcheck: false } 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/groups/merge_requests.html.haml b/app/views/groups/merge_requests.html.haml index a9ce2fe5ab0..808bb1309b1 100644 --- a/app/views/groups/merge_requests.html.haml +++ b/app/views/groups/merge_requests.html.haml @@ -7,7 +7,7 @@ = render 'shared/issuable/nav', type: :merge_requests - if current_user .nav-controls - = render 'shared/new_project_item_select', path: 'merge_requests/new', label: "New merge request", type: :merge_requests, with_feature_enabled: 'merge_requests', with_shared: false, include_projects_in_subgroups: true + = render 'shared/new_project_item_select', path: 'merge_requests/new', label: "New merge request", type: :merge_requests, with_feature_enabled: 'merge_requests', with_shared: false, include_projects_in_subgroups: true = render 'shared/issuable/search_bar', type: :merge_requests diff --git a/app/views/groups/milestones/_form.html.haml b/app/views/groups/milestones/_form.html.haml index 39e3af5f6d2..b0ba846f204 100644 --- a/app/views/groups/milestones/_form.html.haml +++ b/app/views/groups/milestones/_form.html.haml @@ -1,20 +1,20 @@ -= form_for [@group, @milestone], html: { class: 'milestone-form common-note-form js-quick-submit js-requires-input' } do |f| += form_for [@group, @milestone], html: { class: 'milestone-form common-note-form js-quick-submit js-requires-input' } do |f| + = form_errors(@milestone) .row - = form_errors(@milestone) - .col-md-6 .form-group.row - = f.label :title, "Title", class: "col-form-label col-sm-2" + .col-form-label.col-sm-2 + = f.label :title, "Title" .col-sm-10 = f.text_field :title, maxlength: 255, class: "form-control", required: true, autofocus: true .form-group.row.milestone-description - = f.label :description, "Description", class: "col-form-label col-sm-2" + .col-form-label.col-sm-2 + = f.label :description, "Description" .col-sm-10 = render layout: 'projects/md_preview', locals: { url: group_preview_markdown_path } do = render 'projects/zen', f: f, attr: :description, classes: 'note-textarea', placeholder: 'Write milestone description...', supports_autocomplete: false - .clearfix - .error-alert - + .clearfix + .error-alert = render "shared/milestones/form_dates", f: f .form-actions @@ -24,4 +24,3 @@ - else = f.submit 'Update milestone', class: "btn-success btn" = link_to "Cancel", group_milestone_path(@group, @milestone), class: "btn btn-cancel" - diff --git a/app/views/groups/milestones/index.html.haml b/app/views/groups/milestones/index.html.haml index af4fe8f2ef8..b6fb908c8f6 100644 --- a/app/views/groups/milestones/index.html.haml +++ b/app/views/groups/milestones/index.html.haml @@ -4,6 +4,7 @@ = render 'shared/milestones_filter', counts: @milestone_states .nav-controls + = render 'shared/milestones/search_form' = render 'shared/milestones_sort_dropdown' - if can?(current_user, :admin_milestone, @group) = link_to "New milestone", new_group_milestone_path(@group), class: "btn btn-success" diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml index cc294f6a931..77fe88dacb7 100644 --- a/app/views/groups/show.html.haml +++ b/app/views/groups/show.html.haml @@ -1,66 +1,41 @@ - @no_container = true - breadcrumb_title _("Details") -- can_create_subgroups = can?(current_user, :create_subgroup, @group) +- @content_class = "limit-container-width" unless fluid_layout = content_for :meta_tags do = auto_discovery_link_tag(:atom, group_url(@group, rss_url_options), title: "#{@group.name} activity") -= render 'groups/home_panel' - -.groups-listing{ class: container_class, data: { endpoints: { default: group_children_path(@group, format: :json), shared: group_shared_projects_path(@group, format: :json) } } } - .top-area.group-nav-container - .group-search - = render "shared/groups/search_form" - - if can? current_user, :create_projects, @group - - new_project_label = _("New project") - - new_subgroup_label = _("New subgroup") - - if can_create_subgroups - .btn-group.new-project-subgroup.droplab-dropdown.js-new-project-subgroup.qa-new-project-or-subgroup-dropdown{ data: { project_path: new_project_path(namespace_id: @group.id), subgroup_path: new_group_path(parent_id: @group.id) } } - %input.btn.btn-success.dropdown-primary.js-new-group-child.qa-new-in-group-button{ type: "button", value: new_project_label, data: { action: "new-project" } } - %button.btn.btn-success.dropdown-toggle.js-dropdown-toggle.qa-new-project-or-subgroup-dropdown-toggle{ type: "button", data: { "dropdown-trigger" => "#new-project-or-subgroup-dropdown", 'display' => 'static' } } - = icon("caret-down", class: "dropdown-btn-icon") - %ul#new-project-or-subgroup-dropdown.dropdown-menu.dropdown-menu-right{ data: { dropdown: true } } - %li.droplab-item-selected.qa-new-project-option{ role: "button", data: { value: "new-project", text: new_project_label } } - .menu-item - .icon-container - = icon("check", class: "list-item-checkmark") - .description - %strong= new_project_label - %span= s_("GroupsTree|Create a project in this group.") - %li.divider.droplap-item-ignore - %li.qa-new-subgroup-option{ role: "button", data: { value: "new-subgroup", text: new_subgroup_label } } - .menu-item - .icon-container - = icon("check", class: "list-item-checkmark") - .description - %strong= new_subgroup_label - %span= s_("GroupsTree|Create a subgroup in this group.") - - else - = link_to new_project_label, new_project_path(namespace_id: @group.id), class: "btn btn-success" - - .scrolling-tabs-container.inner-page-scroll-tabs - .fade-left= icon('angle-left') - .fade-right= icon('angle-right') - %ul.nav-links.scrolling-tabs.mobile-separator.nav.nav-tabs - %li.js-subgroups_and_projects-tab - = link_to group_path, data: { target: 'div#subgroups_and_projects', action: 'subgroups_and_projects', toggle: 'tab'} do - = _("Subgroups and projects") - %li.js-shared-tab - = link_to group_shared_path, data: { target: 'div#shared', action: 'shared', toggle: 'tab'} do - = _("Shared projects") - %li.js-archived-tab - = link_to group_archived_path, data: { target: 'div#archived', action: 'archived', toggle: 'tab'} do - = _("Archived projects") - - .nav-controls - = render "shared/groups/dropdown", options_hash: subgroups_sort_options_hash - - .tab-content - #subgroups_and_projects.tab-pane - = render "subgroups_and_projects", group: @group - - #shared.tab-pane - = render "shared_projects", group: @group - - #archived.tab-pane - = render "archived_projects", group: @group +%div{ class: [container_class, ("limit-container-width" unless fluid_layout)] } + = render 'groups/home_panel' + + .groups-listing{ data: { endpoints: { default: group_children_path(@group, format: :json), shared: group_shared_projects_path(@group, format: :json) } } } + .top-area.group-nav-container + .scrolling-tabs-container.inner-page-scroll-tabs + .fade-left= icon('angle-left') + .fade-right= icon('angle-right') + %ul.nav-links.scrolling-tabs.mobile-separator.nav.nav-tabs + %li.js-subgroups_and_projects-tab + = link_to group_path, data: { target: 'div#subgroups_and_projects', action: 'subgroups_and_projects', toggle: 'tab'} do + = _("Subgroups and projects") + %li.js-shared-tab + = link_to group_shared_path, data: { target: 'div#shared', action: 'shared', toggle: 'tab'} do + = _("Shared projects") + %li.js-archived-tab + = link_to group_archived_path, data: { target: 'div#archived', action: 'archived', toggle: 'tab'} do + = _("Archived projects") + + .nav-controls.d-block.d-md-flex + .group-search + = render "shared/groups/search_form" + + = render "shared/groups/dropdown", options_hash: subgroups_sort_options_hash + + .tab-content + #subgroups_and_projects.tab-pane + = render "subgroups_and_projects", group: @group + + #shared.tab-pane + = render "shared_projects", group: @group + + #archived.tab-pane + = render "archived_projects", group: @group diff --git a/app/views/ide/_show.html.haml b/app/views/ide/_show.html.haml index b24d6e27536..057225d021f 100644 --- a/app/views/ide/_show.html.haml +++ b/app/views/ide/_show.html.haml @@ -4,7 +4,7 @@ - content_for :page_specific_javascripts do = stylesheet_link_tag 'page_bundles/ide' -#ide.ide-loading{ data: ide_data() } +#ide.ide-loading{ data: ide_data } .text-center = icon('spinner spin 2x') %h2.clgray= _('Loading the GitLab IDE...') diff --git a/app/views/import/bitbucket_server/status.html.haml b/app/views/import/bitbucket_server/status.html.haml index ef69197e453..9280f12e187 100644 --- a/app/views/import/bitbucket_server/status.html.haml +++ b/app/views/import/bitbucket_server/status.html.haml @@ -56,7 +56,7 @@ .project-path.input-group-prepend - if current_user.can_select_namespace? - selected = params[:namespace_id] || :extra_group - - opts = current_user.can_create_group? ? { extra_group: Group.new(name: repo.project_key, path: repo.project_key) } : {} + - opts = current_user.can_create_group? ? { extra_group: Group.new(name: sanitize_project_name(repo.project_key), path: sanitize_project_name(repo.project_key)) } : {} = select_tag :namespace_id, namespaces_options(selected, opts.merge({ display_path: true })), { class: 'input-group-text select2 js-select-namespace', tabindex: 1 } - else = text_field_tag :path, current_user.namespace_path, class: "input-group-text input-large form-control", tabindex: 1, disabled: true diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml index 08a6359f777..0bb2363f65a 100644 --- a/app/views/layouts/_head.html.haml +++ b/app/views/layouts/_head.html.haml @@ -11,20 +11,20 @@ %meta{ 'http-equiv' => 'X-UA-Compatible', content: 'IE=edge' } -# Open Graph - http://ogp.me/ - %meta{ property: 'og:type', content: "object" } - %meta{ property: 'og:site_name', content: site_name } - %meta{ property: 'og:title', content: page_title } + %meta{ property: 'og:type', content: "object" } + %meta{ property: 'og:site_name', content: site_name } + %meta{ property: 'og:title', content: page_title } %meta{ property: 'og:description', content: page_description } - %meta{ property: 'og:image', content: page_image } + %meta{ property: 'og:image', content: page_image } %meta{ property: 'og:image:width', content: '64' } %meta{ property: 'og:image:height', content: '64' } - %meta{ property: 'og:url', content: request.base_url + request.fullpath } + %meta{ property: 'og:url', content: request.base_url + request.fullpath } -# Twitter Card - https://dev.twitter.com/cards/types/summary - %meta{ property: 'twitter:card', content: "summary" } - %meta{ property: 'twitter:title', content: page_title } - %meta{ property: 'twitter:description', content: page_description } - %meta{ property: 'twitter:image', content: page_image } + %meta{ property: 'twitter:card', content: "summary" } + %meta{ property: 'twitter:title', content: page_title } + %meta{ property: 'twitter:description', content: page_description } + %meta{ property: 'twitter:image', content: page_image } = page_card_meta_tags %title= page_title(site_name) @@ -33,8 +33,8 @@ = favicon_link_tag favicon, id: 'favicon', data: { original_href: favicon }, type: 'image/png' = stylesheet_link_tag "application", media: "all" - = stylesheet_link_tag "print", media: "print" - = stylesheet_link_tag "test", media: "all" if Rails.env.test? + = stylesheet_link_tag "print", media: "print" + = stylesheet_link_tag "test", media: "all" if Rails.env.test? = stylesheet_link_tag 'performance_bar' if performance_bar_enabled? = stylesheet_link_tag 'csslab' if Feature.enabled?(:csslab) @@ -61,10 +61,10 @@ %meta{ name: 'theme-color', content: '#474D57' } -# Apple Safari/iOS home screen icons - = favicon_link_tag 'touch-icon-iphone.png', rel: 'apple-touch-icon' - = favicon_link_tag 'touch-icon-ipad.png', rel: 'apple-touch-icon', sizes: '76x76' + = favicon_link_tag 'touch-icon-iphone.png', rel: 'apple-touch-icon' + = favicon_link_tag 'touch-icon-ipad.png', rel: 'apple-touch-icon', sizes: '76x76' = favicon_link_tag 'touch-icon-iphone-retina.png', rel: 'apple-touch-icon', sizes: '120x120' - = favicon_link_tag 'touch-icon-ipad-retina.png', rel: 'apple-touch-icon', sizes: '152x152' + = favicon_link_tag 'touch-icon-ipad-retina.png', rel: 'apple-touch-icon', sizes: '152x152' %link{ rel: 'mask-icon', href: image_path('logo.svg'), color: 'rgb(226, 67, 41)' } -# Windows 8 pinned site tile 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/group.html.haml b/app/views/layouts/group.html.haml index bfbfeee7c4b..1d40b78fa83 100644 --- a/app/views/layouts/group.html.haml +++ b/app/views/layouts/group.html.haml @@ -1,7 +1,7 @@ - page_title @group.name - page_description @group.description unless page_description - header_title group_title(@group) unless header_title -- nav "group" +- nav "group" - @left_sidebar = true - content_for :page_specific_javascripts do diff --git a/app/views/layouts/header/_help_dropdown.html.haml b/app/views/layouts/header/_help_dropdown.html.haml index 513902890af..cd9128c452b 100644 --- a/app/views/layouts/header/_help_dropdown.html.haml +++ b/app/views/layouts/header/_help_dropdown.html.haml @@ -1,12 +1,8 @@ -- 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 _("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) diff --git a/app/views/layouts/header/_new_dropdown.haml b/app/views/layouts/header/_new_dropdown.haml index e42251f9ec8..5a66b02c048 100644 --- a/app/views/layouts/header/_new_dropdown.haml +++ b/app/views/layouts/header/_new_dropdown.haml @@ -1,4 +1,4 @@ -%li.header-new.dropdown{ data: { track_label: "new_dropdown", track_event: "click_dropdown" } } +%li.header-new.dropdown{ data: { track_label: "new_dropdown", track_event: "click_dropdown" } } = link_to new_project_path, class: "header-new-dropdown-toggle has-tooltip qa-new-menu-toggle", title: _("New..."), ref: 'tooltip', aria: { label: _("New...") }, data: { toggle: 'dropdown', placement: 'bottom', container: 'body', display: 'static' } do = sprite_icon('plus-square', size: 16) = sprite_icon('angle-down', css_class: 'caret-down') diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml index ddd30efe062..f659c89dd30 100644 --- a/app/views/layouts/nav/_dashboard.html.haml +++ b/app/views/layouts/nav/_dashboard.html.haml @@ -10,7 +10,7 @@ = render "layouts/nav/projects_dropdown/show" - if dashboard_nav_link?(:groups) - = nav_link(controller: ['dashboard/groups', 'explore/groups'], html_options: { id: 'nav-groups-dropdown', class: "home dropdown header-groups qa-groups-dropdown", data: { track_label: "groups_dropdown", track_event: "click_dropdown" } }) do + = nav_link(controller: ['dashboard/groups', 'explore/groups'], html_options: { id: 'nav-groups-dropdown', class: "home dropdown header-groups qa-groups-dropdown", data: { track_label: "groups_dropdown", track_event: "click_dropdown" } }) do %button{ type: 'button', data: { toggle: "dropdown" } } = _('Groups') = sprite_icon('angle-down', css_class: 'caret-down') 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..dd7833647b7 100644 --- a/app/views/layouts/nav/sidebar/_project.html.haml +++ b/app/views/layouts/nav/sidebar/_project.html.haml @@ -26,7 +26,7 @@ %span= _('Details') = nav_link(path: 'projects#activity') do - = link_to activity_project_path(@project), title: _('Activity'), class: 'shortcuts-project-activity' do + = link_to activity_project_path(@project), title: _('Activity'), class: 'shortcuts-project-activity qa-activity-link' do %span= _('Activity') - if project_nav_tab?(:releases) @@ -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 @@ -146,7 +146,7 @@ - if project_nav_tab? :merge_requests = nav_link(controller: @project.issues_enabled? ? :merge_requests : [:merge_requests, :labels, :milestones]) do - = link_to project_merge_requests_path(@project), class: 'shortcuts-merge_requests' do + = link_to project_merge_requests_path(@project), class: 'shortcuts-merge_requests qa-merge-requests-link' do .nav-icon-container = sprite_icon('git-merge') %span.nav-item-name @@ -170,7 +170,7 @@ = _('CI / CD') %ul.sidebar-sub-level-items - = nav_link(controller: [:pipelines, :builds, :jobs, :pipeline_schedules, :artifacts], html_options: { class: "fly-out-top-item" } ) do + = nav_link(controller: [:pipelines, :builds, :jobs, :pipeline_schedules, :artifacts], html_options: { class: "fly-out-top-item" }) do = link_to project_pipelines_path(@project) do %strong.fly-out-top-item-name = _('CI / CD') @@ -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 @@ -281,19 +281,34 @@ %strong.fly-out-top-item-name = _('Registry') - - if project_nav_tab? :wiki + - if project_nav_tab?(:wiki) + - wiki_url = project_wiki_path(@project, :home) = nav_link(controller: :wikis) do - = link_to get_project_wiki_path(@project), class: 'shortcuts-wiki' do + = link_to wiki_url, class: 'shortcuts-wiki qa-wiki-link' do .nav-icon-container = sprite_icon('book') %span.nav-item-name = _('Wiki') %ul.sidebar-sub-level-items.is-fly-out-only = nav_link(controller: :wikis, html_options: { class: "fly-out-top-item" } ) do - = link_to get_project_wiki_path(@project) do + = link_to wiki_url do %strong.fly-out-top-item-name = _('Wiki') + - if project_nav_tab?(:external_wiki) + - external_wiki_url = @project.external_wiki.external_wiki_url + = nav_link do + = link_to external_wiki_url, class: 'shortcuts-external_wiki' do + .nav-icon-container + = sprite_icon('issue-external') + %span.nav-item-name + = _('External Wiki') + %ul.sidebar-sub-level-items.is-fly-out-only + = nav_link(html_options: { class: "fly-out-top-item" } ) do + = link_to external_wiki_url do + %strong.fly-out-top-item-name + = _('External Wiki') + - if project_nav_tab? :snippets = nav_link(controller: :snippets) do = link_to project_snippets_path(@project), class: 'shortcuts-snippets' do diff --git a/app/views/layouts/snippets.html.haml b/app/views/layouts/snippets.html.haml index 6418500d5d1..5f986c81ff4 100644 --- a/app/views/layouts/snippets.html.haml +++ b/app/views/layouts/snippets.html.haml @@ -1,4 +1,4 @@ -- header_title _("Snippets"), snippets_path +- header_title _("Snippets"), snippets_path - content_for :page_specific_javascripts do - if @snippet && current_user diff --git a/app/views/notify/_note_email.text.erb b/app/views/notify/_note_email.text.erb index 50209c46ed1..5a67214059c 100644 --- a/app/views/notify/_note_email.text.erb +++ b/app/views/notify/_note_email.text.erb @@ -3,7 +3,7 @@ <% discussion = note.discussion if note.part_of_discussion? -%> <% if discussion && !discussion.individual_note? -%> -<%= note.author_name -%> +<%= sanitize_name(note.author_name) -%> <% if discussion.new_discussion? -%> <%= " started a new discussion" -%> <% else -%> @@ -16,7 +16,7 @@ <% elsif Gitlab::CurrentSettings.email_author_in_body -%> -<%= "#{note.author_name} commented:" -%> +<%= "#{sanitize_name(note.author_name)} commented:" -%> <% end -%> diff --git a/app/views/notify/autodevops_disabled_email.text.erb b/app/views/notify/autodevops_disabled_email.text.erb index 695780c3145..bf863952478 100644 --- a/app/views/notify/autodevops_disabled_email.text.erb +++ b/app/views/notify/autodevops_disabled_email.text.erb @@ -3,7 +3,7 @@ Auto DevOps pipeline was disabled for <%= @project.name %> The Auto DevOps pipeline failed for pipeline <%= @pipeline.iid %> (<%= pipeline_url(@pipeline) %>) and has been disabled for <%= @project.name %>. In order to use the Auto DevOps pipeline with your project, please review the currently supported languagues (https://docs.gitlab.com/ee/topics/autodevops/#currently-supported-languages), adjust your project accordingly, and turn on the Auto DevOps pipeline within your CI/CD project settings (<%= project_settings_ci_cd_url(@project) %>). <% if @pipeline.user -%> - Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) triggered by <%= @pipeline.user.name %> ( <%= user_url(@pipeline.user) %> ) + Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) triggered by <%= sanitize_name(@pipeline.user.name) %> ( <%= user_url(@pipeline.user) %> ) <% else -%> Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) triggered by API <% end -%> diff --git a/app/views/notify/closed_issue_email.html.haml b/app/views/notify/closed_issue_email.html.haml index b7284dd819b..eb148d72da1 100644 --- a/app/views/notify/closed_issue_email.html.haml +++ b/app/views/notify/closed_issue_email.html.haml @@ -1,2 +1,2 @@ %p - Issue was closed by #{@updated_by.name} + Issue was closed by #{sanitize_name(@updated_by.name)} diff --git a/app/views/notify/closed_issue_email.text.haml b/app/views/notify/closed_issue_email.text.haml index b35d4b7502d..b1f0a3f37ec 100644 --- a/app/views/notify/closed_issue_email.text.haml +++ b/app/views/notify/closed_issue_email.text.haml @@ -1,3 +1,3 @@ -Issue was closed by #{@updated_by.name} +Issue was closed by #{sanitize_name(@updated_by.name)} Issue ##{@issue.iid}: #{project_issue_url(@issue.project, @issue)} diff --git a/app/views/notify/closed_merge_request_email.html.haml b/app/views/notify/closed_merge_request_email.html.haml index 44e018304e1..2aa753e0d55 100644 --- a/app/views/notify/closed_merge_request_email.html.haml +++ b/app/views/notify/closed_merge_request_email.html.haml @@ -1,2 +1,2 @@ %p - Merge Request #{@merge_request.to_reference} was closed by #{@updated_by.name} + Merge Request #{@merge_request.to_reference} was closed by #{sanitize_name(@updated_by.name)} diff --git a/app/views/notify/closed_merge_request_email.text.haml b/app/views/notify/closed_merge_request_email.text.haml index c4e06cb3cb1..1094d584a1c 100644 --- a/app/views/notify/closed_merge_request_email.text.haml +++ b/app/views/notify/closed_merge_request_email.text.haml @@ -1,8 +1,8 @@ -Merge Request #{@merge_request.to_reference} was closed by #{@updated_by.name} +Merge Request #{@merge_request.to_reference} was closed by #{sanitize_name(@updated_by.name)} Merge Request url: #{project_merge_request_url(@merge_request.target_project, @merge_request)} = merge_path_description(@merge_request, 'to') -Author: #{@merge_request.author_name} -Assignee: #{@merge_request.assignee_name} +Author: #{sanitize_name(@merge_request.author_name)} +Assignee: #{sanitize_name(@merge_request.assignee_name)} diff --git a/app/views/notify/issue_status_changed_email.html.haml b/app/views/notify/issue_status_changed_email.html.haml index b6051b11cea..66e73a9b03f 100644 --- a/app/views/notify/issue_status_changed_email.html.haml +++ b/app/views/notify/issue_status_changed_email.html.haml @@ -1,2 +1,2 @@ %p - Issue was #{@issue_status} by #{@updated_by.name} + Issue was #{@issue_status} by #{sanitize_name(@updated_by.name)} diff --git a/app/views/notify/issue_status_changed_email.text.erb b/app/views/notify/issue_status_changed_email.text.erb index 4200881f7e8..f38b09e9820 100644 --- a/app/views/notify/issue_status_changed_email.text.erb +++ b/app/views/notify/issue_status_changed_email.text.erb @@ -1,4 +1,4 @@ -Issue was <%= @issue_status %> by <%= @updated_by.name %> +Issue was <%= @issue_status %> by <%= sanitize_name(@updated_by.name) %> Issue <%= @issue.iid %>: <%= url_for(project_issue_url(@issue.project, @issue)) %> diff --git a/app/views/notify/member_access_requested_email.text.erb b/app/views/notify/member_access_requested_email.text.erb index 9c5ee0eaf26..ddb4a7b3d2c 100644 --- a/app/views/notify/member_access_requested_email.text.erb +++ b/app/views/notify/member_access_requested_email.text.erb @@ -1,3 +1,3 @@ -<%= member.user.name %> (<%= user_url(member.user) %>) requested <%= member.human_access %> access to the <%= member_source.human_name %> <%= member_source.model_name.singular %>. +<%= sanitize_name(member.user.name) %> (<%= user_url(member.user) %>) requested <%= member.human_access %> access to the <%= member_source.human_name %> <%= member_source.model_name.singular %>. <%= polymorphic_url([member_source, :members]) %> diff --git a/app/views/notify/member_invite_accepted_email.text.erb b/app/views/notify/member_invite_accepted_email.text.erb index cef87101427..c824533eac2 100644 --- a/app/views/notify/member_invite_accepted_email.text.erb +++ b/app/views/notify/member_invite_accepted_email.text.erb @@ -1,3 +1,3 @@ -<%= member.invite_email %>, now known as <%= member.user.name %>, has accepted your invitation to join the <%= member_source.human_name %> <%= member_source.model_name.singular %>. +<%= member.invite_email %>, now known as <%= sanitize_name(member.user.name) %>, has accepted your invitation to join the <%= member_source.human_name %> <%= member_source.model_name.singular %>. <%= member_source.web_url %> diff --git a/app/views/notify/member_invited_email.text.erb b/app/views/notify/member_invited_email.text.erb index 0a6393355be..d944c3b4a50 100644 --- a/app/views/notify/member_invited_email.text.erb +++ b/app/views/notify/member_invited_email.text.erb @@ -1,4 +1,4 @@ -You have been invited <%= "by #{member.created_by.name} " if member.created_by %>to join the <%= member_source.human_name %> <%= member_source.model_name.singular %> as <%= member.human_access %>. +You have been invited <%= "by #{sanitize_name(member.created_by.name)} " if member.created_by %>to join the <%= member_source.human_name %> <%= member_source.model_name.singular %> as <%= member.human_access %>. Accept invitation: <%= invite_url(@token) %> Decline invitation: <%= decline_invite_url(@token) %> diff --git a/app/views/notify/merge_request_status_email.html.haml b/app/views/notify/merge_request_status_email.html.haml index b487e26b122..ffb416abf72 100644 --- a/app/views/notify/merge_request_status_email.html.haml +++ b/app/views/notify/merge_request_status_email.html.haml @@ -1,2 +1,2 @@ %p - Merge Request #{@merge_request.to_reference} was #{@mr_status} by #{@updated_by.name} + Merge Request #{@merge_request.to_reference} was #{@mr_status} by #{sanitize_name(@updated_by.name)} diff --git a/app/views/notify/merge_request_status_email.text.haml b/app/views/notify/merge_request_status_email.text.haml index ae2a2933865..b9b9e0c3ad7 100644 --- a/app/views/notify/merge_request_status_email.text.haml +++ b/app/views/notify/merge_request_status_email.text.haml @@ -1,8 +1,8 @@ -Merge Request #{@merge_request.to_reference} was #{@mr_status} by #{@updated_by.name} +Merge Request #{@merge_request.to_reference} was #{@mr_status} by #{sanitize_name(@updated_by.name)} Merge Request url: #{project_merge_request_url(@merge_request.target_project, @merge_request)} = merge_path_description(@merge_request, 'to') -Author: #{@merge_request.author_name} -Assignee: #{@merge_request.assignee_name} +Author: #{sanitize_name(@merge_request.author_name)} +Assignee: #{sanitize_name(@merge_request.assignee_name)} diff --git a/app/views/notify/merge_request_unmergeable_email.text.haml b/app/views/notify/merge_request_unmergeable_email.text.haml index dcdd6db69d6..0c7bf1bb044 100644 --- a/app/views/notify/merge_request_unmergeable_email.text.haml +++ b/app/views/notify/merge_request_unmergeable_email.text.haml @@ -4,5 +4,5 @@ Merge Request url: #{project_merge_request_url(@merge_request.target_project, @m = merge_path_description(@merge_request, 'to') -Author: #{@merge_request.author_name} -Assignee: #{@merge_request.assignee_name} +Author: #{sanitize_name(@merge_request.author_name)} +Assignee: #{sanitize_name(@merge_request.assignee_name)} diff --git a/app/views/notify/merged_merge_request_email.text.haml b/app/views/notify/merged_merge_request_email.text.haml index 661c23bcbe2..045a43cbc84 100644 --- a/app/views/notify/merged_merge_request_email.text.haml +++ b/app/views/notify/merged_merge_request_email.text.haml @@ -4,5 +4,5 @@ Merge Request url: #{project_merge_request_url(@merge_request.target_project, @m = merge_path_description(@merge_request, 'to') -Author: #{@merge_request.author_name} -Assignee: #{@merge_request.assignee_name} +Author: #{sanitize_name(@merge_request.author_name)} +Assignee: #{sanitize_name(@merge_request.assignee_name)} diff --git a/app/views/notify/new_gpg_key_email.html.haml b/app/views/notify/new_gpg_key_email.html.haml index 4b9350c4e88..b857705e01f 100644 --- a/app/views/notify/new_gpg_key_email.html.haml +++ b/app/views/notify/new_gpg_key_email.html.haml @@ -1,5 +1,5 @@ %p - Hi #{@user.name}! + Hi #{sanitize_name(@user.name)}! %p A new GPG key was added to your account: %p diff --git a/app/views/notify/new_gpg_key_email.text.erb b/app/views/notify/new_gpg_key_email.text.erb index 80b5a1fd7ff..92ea851eee4 100644 --- a/app/views/notify/new_gpg_key_email.text.erb +++ b/app/views/notify/new_gpg_key_email.text.erb @@ -1,4 +1,4 @@ -Hi <%= @user.name %>! +Hi <%= sanitize_name(@user.name) %>! A new GPG key was added to your account: diff --git a/app/views/notify/new_issue_email.text.erb b/app/views/notify/new_issue_email.text.erb index 3c716f77296..58a2bcbe5eb 100644 --- a/app/views/notify/new_issue_email.text.erb +++ b/app/views/notify/new_issue_email.text.erb @@ -1,7 +1,7 @@ New Issue was created. Issue <%= @issue.iid %>: <%= url_for(project_issue_url(@issue.project, @issue)) %> -Author: <%= @issue.author_name %> +Author: <%= sanitize_name(@issue.author_name) %> Assignee: <%= @issue.assignee_list %> <%= @issue.description %> diff --git a/app/views/notify/new_mention_in_issue_email.text.erb b/app/views/notify/new_mention_in_issue_email.text.erb index 23213106c5b..173091e4a80 100644 --- a/app/views/notify/new_mention_in_issue_email.text.erb +++ b/app/views/notify/new_mention_in_issue_email.text.erb @@ -1,7 +1,7 @@ You have been mentioned in an issue. Issue <%= @issue.iid %>: <%= url_for(project_issue_url(@issue.project, @issue)) %> -Author: <%= @issue.author_name %> -Assignee: <%= @issue.assignee_list %> +Author: <%= sanitize_name(@issue.author_name) %> +Assignee: <%= sanitize_name(@issue.assignee_list) %> <%= @issue.description %> diff --git a/app/views/notify/new_mention_in_merge_request_email.text.erb b/app/views/notify/new_mention_in_merge_request_email.text.erb index 6fcebb22fc4..96a4f3f9eac 100644 --- a/app/views/notify/new_mention_in_merge_request_email.text.erb +++ b/app/views/notify/new_mention_in_merge_request_email.text.erb @@ -3,7 +3,7 @@ You have been mentioned in Merge Request <%= @merge_request.to_reference %> <%= url_for(project_merge_request_url(@merge_request.target_project, @merge_request)) %> <%= merge_path_description(@merge_request, 'to') %> -Author: <%= @merge_request.author_name %> -Assignee: <%= @merge_request.assignee_name %> +Author: <%= sanitize_name(@merge_request.author_name) %> +Assignee: <%= sanitize_name(@merge_request.assignee_name) %> <%= @merge_request.description %> diff --git a/app/views/notify/new_merge_request_email.html.haml b/app/views/notify/new_merge_request_email.html.haml index 5acd45b74a7..db23447dd39 100644 --- a/app/views/notify/new_merge_request_email.html.haml +++ b/app/views/notify/new_merge_request_email.html.haml @@ -7,7 +7,7 @@ - if @merge_request.assignee_id.present? %p - Assignee: #{@merge_request.assignee_name} + Assignee: #{sanitize_name(@merge_request.assignee_name)} = render_if_exists 'notify/merge_request_approvers', presenter: @mr_presenter diff --git a/app/views/notify/new_ssh_key_email.html.haml b/app/views/notify/new_ssh_key_email.html.haml index 63b0cbbd205..d031842be95 100644 --- a/app/views/notify/new_ssh_key_email.html.haml +++ b/app/views/notify/new_ssh_key_email.html.haml @@ -1,5 +1,5 @@ %p - Hi #{@user.name}! + Hi #{sanitize_name(@user.name)}! %p A new public key was added to your account: %p diff --git a/app/views/notify/new_ssh_key_email.text.erb b/app/views/notify/new_ssh_key_email.text.erb index 05b551c89a0..690357d69ed 100644 --- a/app/views/notify/new_ssh_key_email.text.erb +++ b/app/views/notify/new_ssh_key_email.text.erb @@ -1,4 +1,4 @@ -Hi <%= @user.name %>! +Hi <%= sanitize_name(@user.name) %>! A new public key was added to your account: diff --git a/app/views/notify/new_user_email.html.haml b/app/views/notify/new_user_email.html.haml index db4424a01f9..dfbb5c75bd3 100644 --- a/app/views/notify/new_user_email.html.haml +++ b/app/views/notify/new_user_email.html.haml @@ -1,5 +1,5 @@ %p - Hi #{@user['name']}! + Hi #{sanitize_name(@user['name'])}! %p - if Gitlab::CurrentSettings.allow_signup? Your account has been created successfully. diff --git a/app/views/notify/new_user_email.text.erb b/app/views/notify/new_user_email.text.erb index dd9b71e3b84..f3f20f3bfba 100644 --- a/app/views/notify/new_user_email.text.erb +++ b/app/views/notify/new_user_email.text.erb @@ -1,4 +1,4 @@ -Hi <%= @user.name %>! +Hi <%= sanitize_name(@user.name) %>! The Administrator created an account for you. Now you are a member of the company GitLab application. diff --git a/app/views/notify/pipeline_failed_email.text.erb b/app/views/notify/pipeline_failed_email.text.erb index 294238eee51..722eedf90be 100644 --- a/app/views/notify/pipeline_failed_email.text.erb +++ b/app/views/notify/pipeline_failed_email.text.erb @@ -10,20 +10,20 @@ Commit: <%= @pipeline.short_sha %> ( <%= commit_url(@pipeline) %> ) Commit Message: <%= @pipeline.git_commit_message.truncate(50) %> <% commit = @pipeline.commit -%> <% if commit.author -%> -Commit Author: <%= commit.author.name %> ( <%= user_url(commit.author) %> ) +Commit Author: <%= sanitize_name(commit.author.name) %> ( <%= user_url(commit.author) %> ) <% else -%> Commit Author: <%= commit.author_name %> <% end -%> <% if commit.different_committer? -%> <% if commit.committer -%> -Committed by: <%= commit.committer.name %> ( <%= user_url(commit.committer) %> ) +Committed by: <%= sanitize_name(commit.committer.name) %> ( <%= user_url(commit.committer) %> ) <% else -%> Committed by: <%= commit.committer_name %> <% end -%> <% end -%> <% if @pipeline.user -%> -Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) triggered by <%= @pipeline.user.name %> ( <%= user_url(@pipeline.user) %> ) +Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) triggered by <%= sanitize_name(@pipeline.user.name) %> ( <%= user_url(@pipeline.user) %> ) <% else -%> Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) triggered by API <% end -%> diff --git a/app/views/notify/pipeline_success_email.text.erb b/app/views/notify/pipeline_success_email.text.erb index 39622cf7f02..9aadf380f79 100644 --- a/app/views/notify/pipeline_success_email.text.erb +++ b/app/views/notify/pipeline_success_email.text.erb @@ -10,13 +10,13 @@ Commit: <%= @pipeline.short_sha %> ( <%= commit_url(@pipeline) %> ) Commit Message: <%= @pipeline.git_commit_message.truncate(50) %> <% commit = @pipeline.commit -%> <% if commit.author -%> -Commit Author: <%= commit.author.name %> ( <%= user_url(commit.author) %> ) +Commit Author: <%= sanitize_name(commit.author.name) %> ( <%= user_url(commit.author) %> ) <% else -%> Commit Author: <%= commit.author_name %> <% end -%> <% if commit.different_committer? -%> <% if commit.committer -%> -Committed by: <%= commit.committer.name %> ( <%= user_url(commit.committer) %> ) +Committed by: <%= sanitize_name(commit.committer.name) %> ( <%= user_url(commit.committer) %> ) <% else -%> Committed by: <%= commit.committer_name %> <% end -%> @@ -25,7 +25,7 @@ Committed by: <%= commit.committer_name %> <% job_count = @pipeline.total_size -%> <% stage_count = @pipeline.stages_count -%> <% if @pipeline.user -%> -Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) triggered by <%= @pipeline.user.name %> ( <%= user_url(@pipeline.user) %> ) +Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) triggered by <%= sanitize_name(@pipeline.user.name) %> ( <%= user_url(@pipeline.user) %> ) <% else -%> Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) triggered by API <% end -%> diff --git a/app/views/notify/push_to_merge_request_email.html.haml b/app/views/notify/push_to_merge_request_email.html.haml index 67744ec1cee..97258833cfc 100644 --- a/app/views/notify/push_to_merge_request_email.html.haml +++ b/app/views/notify/push_to_merge_request_email.html.haml @@ -1,5 +1,5 @@ %h3 - = @updated_by_user.name + = sanitize_name(@updated_by_user.name) pushed new commits to merge request = link_to(@merge_request.to_reference, project_merge_request_url(@merge_request.target_project, @merge_request)) diff --git a/app/views/notify/push_to_merge_request_email.text.haml b/app/views/notify/push_to_merge_request_email.text.haml index 95759d127e2..10c8e158846 100644 --- a/app/views/notify/push_to_merge_request_email.text.haml +++ b/app/views/notify/push_to_merge_request_email.text.haml @@ -1,4 +1,4 @@ -#{@updated_by_user.name} pushed new commits to merge request #{@merge_request.to_reference} +#{sanitize_name(@updated_by_user.name)} pushed new commits to merge request #{@merge_request.to_reference} \ #{url_for(project_merge_request_url(@merge_request.target_project, @merge_request))} \ diff --git a/app/views/notify/reassigned_issue_email.html.haml b/app/views/notify/reassigned_issue_email.html.haml index ee2f40e1683..6d25488a7e2 100644 --- a/app/views/notify/reassigned_issue_email.html.haml +++ b/app/views/notify/reassigned_issue_email.html.haml @@ -2,7 +2,7 @@ Assignee changed - if @previous_assignees.any? from - %strong= @previous_assignees.map(&:name).to_sentence + %strong= sanitize_name(@previous_assignees.map(&:name).to_sentence) to - if @issue.assignees.any? %strong= @issue.assignee_list diff --git a/app/views/notify/reassigned_issue_email.text.erb b/app/views/notify/reassigned_issue_email.text.erb index 6c357f1074a..7bf2e8e6ce3 100644 --- a/app/views/notify/reassigned_issue_email.text.erb +++ b/app/views/notify/reassigned_issue_email.text.erb @@ -2,5 +2,5 @@ Reassigned Issue <%= @issue.iid %> <%= url_for([@issue.project.namespace.becomes(Namespace), @issue.project, @issue, { only_path: false }]) %> -Assignee changed <%= "from #{@previous_assignees.map(&:name).to_sentence}" if @previous_assignees.any? -%> +Assignee changed <%= "from #{sanitize_name(@previous_assignees.map(&:name).to_sentence)}" if @previous_assignees.any? -%> to <%= "#{@issue.assignees.any? ? @issue.assignee_list : 'Unassigned'}" %> diff --git a/app/views/notify/reassigned_merge_request_email.html.haml b/app/views/notify/reassigned_merge_request_email.html.haml index 24c2b08810b..e4f19bc3200 100644 --- a/app/views/notify/reassigned_merge_request_email.html.haml +++ b/app/views/notify/reassigned_merge_request_email.html.haml @@ -2,9 +2,9 @@ Assignee changed - if @previous_assignee from - %strong= @previous_assignee.name + %strong= sanitize_name(@previous_assignee.name) to - if @merge_request.assignee_id - %strong= @merge_request.assignee_name + %strong= sanitize_name(@merge_request.assignee_name) - else %strong Unassigned diff --git a/app/views/notify/reassigned_merge_request_email.text.erb b/app/views/notify/reassigned_merge_request_email.text.erb index 998a40fefde..96c770b5219 100644 --- a/app/views/notify/reassigned_merge_request_email.text.erb +++ b/app/views/notify/reassigned_merge_request_email.text.erb @@ -2,5 +2,5 @@ Reassigned Merge Request <%= @merge_request.iid %> <%= url_for([@merge_request.project.namespace.becomes(Namespace), @merge_request.project, @merge_request, { only_path: false }]) %> -Assignee changed <%= "from #{@previous_assignee.name}" if @previous_assignee -%> - to <%= "#{@merge_request.assignee_id ? @merge_request.assignee_name : 'Unassigned'}" %> +Assignee changed <%= "from #{sanitize_name(@previous_assignee.name)}" if @previous_assignee -%> + to <%= "#{@merge_request.assignee_id ? sanitize_name(@merge_request.assignee_name) : 'Unassigned'}" %> diff --git a/app/views/notify/resolved_all_discussions_email.html.haml b/app/views/notify/resolved_all_discussions_email.html.haml index 522421b7cc3..502b8f21e35 100644 --- a/app/views/notify/resolved_all_discussions_email.html.haml +++ b/app/views/notify/resolved_all_discussions_email.html.haml @@ -1,2 +1,2 @@ %p - All discussions on Merge Request #{@merge_request.to_reference} were resolved by #{@resolved_by.name} + All discussions on Merge Request #{@merge_request.to_reference} were resolved by #{sanitize_name(@resolved_by.name)} diff --git a/app/views/notify/resolved_all_discussions_email.text.erb b/app/views/notify/resolved_all_discussions_email.text.erb index 2881f3e699e..c4b36bfe1a8 100644 --- a/app/views/notify/resolved_all_discussions_email.text.erb +++ b/app/views/notify/resolved_all_discussions_email.text.erb @@ -1,3 +1,3 @@ -All discussions on Merge Request <%= @merge_request.to_reference %> were resolved by <%= @resolved_by.name %> +All discussions on Merge Request <%= @merge_request.to_reference %> were resolved by <%= sanitize_name(@resolved_by.name) %> <%= url_for(project_merge_request_url(@merge_request.target_project, @merge_request)) %> 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/_export.html.haml b/app/views/projects/_export.html.haml index 91deffe07c1..409b6dba9ca 100644 --- a/app/views/projects/_export.html.haml +++ b/app/views/projects/_export.html.haml @@ -33,9 +33,9 @@ %p Once the exported file is ready, you will receive a notification email with a download link, or you can download it from this page. - if project.export_status == :finished - = link_to 'Download export', download_export_project_path(project), + = link_to 'Download export', download_export_project_path(project), rel: 'nofollow', download: '', method: :get, class: "btn btn-default" - = link_to 'Generate new export', generate_new_export_project_path(project), + = link_to 'Generate new export', generate_new_export_project_path(project), method: :post, class: "btn btn-default" - else = link_to 'Export project', export_project_path(project), diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml index e8cc3d6bcf0..7694217eb28 100644 --- a/app/views/projects/_home_panel.html.haml +++ b/app/views/projects/_home_panel.html.haml @@ -1,17 +1,17 @@ - empty_repo = @project.empty_repo? - show_auto_devops_callout = show_auto_devops_callout?(@project) .project-home-panel{ class: ("empty-project" if empty_repo) } - .project-header.row.append-bottom-8 - .project-title-row.col-md-12.col-lg-6.d-flex - .avatar-container.project-avatar.float-none + .row.append-bottom-8 + .home-panel-title-row.col-md-12.col-lg-6.d-flex + .avatar-container.home-panel-avatar.append-right-default.float-none = project_icon(@project, alt: @project.name, class: 'avatar avatar-tile s64', width: 64, height: 64) .d-flex.flex-column.flex-wrap.align-items-baseline .d-inline-flex.align-items-baseline - %h1.project-title.qa-project-name + %h1.home-panel-title.prepend-top-8.append-bottom-5.qa-project-name = @project.name - %span.project-visibility.prepend-left-8.d-inline-flex.align-items-baseline.visibility-icon.has-tooltip{ data: { container: 'body' }, title: visibility_icon_description(@project) } + %span.visibility-icon.text-secondary.prepend-left-4.has-tooltip{ data: { container: 'body' }, title: visibility_icon_description(@project) } = visibility_level_icon(@project.visibility_level, fw: false, options: {class: 'icon'}) - .project-metadata.d-flex.align-items-center + .home-panel-metadata.d-flex.align-items-center.text-secondary - if can?(current_user, :read_project, @project) %span.text-secondary = s_('ProjectPage|Project ID: %{project_id}') % { project_id: @project.id } @@ -19,17 +19,17 @@ %span.access-request-links.prepend-left-8 = render 'shared/members/access_request_links', source: @project - if @project.tag_list.present? - %span.project-topic-list.d-inline-flex.prepend-left-8.has-tooltip{ data: { container: 'body' }, title: @project.has_extra_topics? ? @project.tag_list.join(', ') : nil } + %span.home-panel-topic-list.d-inline-flex.prepend-left-8.has-tooltip{ data: { container: 'body' }, title: @project.has_extra_topics? ? @project.tag_list.join(', ') : nil } = 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_topics_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 - if current_user .d-inline-flex - = render 'projects/buttons/notifications', notification_setting: @notification_setting, btn_class: 'btn-xs' + = render 'shared/notifications/new_button', notification_setting: @notification_setting, btn_class: 'btn-xs' .count-buttons.d-inline-flex = render 'projects/buttons/star' @@ -44,13 +44,13 @@ - if can?(current_user, :download_code, @project) %nav.project-stats - .nav-links.quick-links.mt-3 + .nav-links.quick-links = render 'stat_anchor_list', anchors: @project.statistics_anchors(show_auto_devops_callout: show_auto_devops_callout) - .project-home-desc.mt-1 + .home-panel-home-desc.mt-1 - if @project.description.present? - .project-description - .project-description-markdown.read-more-container + .home-panel-description + .home-panel-description-markdown.read-more-container = markdown_field(@project, :description) %button.btn.btn-blank.btn-link.js-read-more-trigger.d-lg-none{ type: "button" } = _("Read more") diff --git a/app/views/projects/_issuable_by_email.html.haml b/app/views/projects/_issuable_by_email.html.haml index 147e73f047f..0b2d179456d 100644 --- a/app/views/projects/_issuable_by_email.html.haml +++ b/app/views/projects/_issuable_by_email.html.haml @@ -21,7 +21,7 @@ = clipboard_button(target: '#issuable_email', class: 'btn btn-clipboard input-group-text btn-transparent d-none d-sm-block') - if issuable_type == 'issue' - - enter_title_text = _('Enter the issue title') + - enter_title_text = _('Enter the issue title') - enter_description_text = _('Enter the issue description') - else - enter_title_text = _('Enter the merge request title') diff --git a/app/views/projects/_new_project_fields.html.haml b/app/views/projects/_new_project_fields.html.haml index ba7d3154326..276363df7da 100644 --- a/app/views/projects/_new_project_fields.html.haml +++ b/app/views/projects/_new_project_fields.html.haml @@ -43,7 +43,7 @@ = f.label :description, class: 'label-bold' do Project description %span (optional) - = f.text_area :description, placeholder: 'Description format', class: "form-control", rows: 3, maxlength: 250, data: { track_label: "#{track_label}", track_event: "activate_form_input", track_property: "project_description", track_value: "" } + = f.text_area :description, placeholder: 'Description format', class: "form-control", rows: 3, maxlength: 250, data: { track_label: "#{track_label}", track_event: "activate_form_input", track_property: "project_description", track_value: "" } = f.label :visibility_level, class: 'label-bold' do Visibility Level 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/blob/viewers/_readme.html.haml b/app/views/projects/blob/viewers/_readme.html.haml index d8492abc638..c2329a7aa66 100644 --- a/app/views/projects/blob/viewers/_readme.html.haml +++ b/app/views/projects/blob/viewers/_readme.html.haml @@ -1,4 +1,4 @@ = icon('info-circle fw') = succeed '.' do To learn more about this project, read - = link_to "the wiki", get_project_wiki_path(viewer.project) + = link_to "the wiki", project_wiki_path(viewer.project, :home) 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/buttons/_dropdown.html.haml b/app/views/projects/buttons/_dropdown.html.haml index 45515fb492f..bbe0a2c97fd 100644 --- a/app/views/projects/buttons/_dropdown.html.haml +++ b/app/views/projects/buttons/_dropdown.html.haml @@ -35,8 +35,8 @@ - elsif can_collaborate_with_project?(@project) %li= link_to _('New file'), project_new_blob_path(@project, @project.default_branch || 'master') - elsif create_mr_from_new_fork - - continue_params = { to: project_new_blob_path(@project, @project.default_branch || 'master'), - notice: edit_in_new_fork_notice, + - continue_params = { to: project_new_blob_path(@project, @project.default_branch || 'master'), + notice: edit_in_new_fork_notice, notice_now: edit_in_new_fork_notice_now } - fork_path = project_forks_path(@project, namespace_key: current_user.namespace.id, continue: continue_params) %li= link_to _('New file'), fork_path, method: :post diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml index 44e9cb84341..9d069c025ba 100644 --- a/app/views/projects/ci/builds/_build.html.haml +++ b/app/views/projects/ci/builds/_build.html.haml @@ -25,7 +25,7 @@ = job.tag? ? icon('tag') : sprite_icon('fork', css_class: 'sprite') = link_to job.ref, project_ref_path(job.project, job.ref), class: "ref-name" - else - .light none + .light= _('none') .icon-container.commit-icon = custom_icon("icon_commit") @@ -33,10 +33,10 @@ = link_to job.short_sha, project_commit_path(job.project, job.sha), class: "commit-sha" - if job.stuck? - = icon('warning', class: 'text-warning has-tooltip', title: 'Job is stuck. Check runners.') + = icon('warning', class: 'text-warning has-tooltip', title: _('Job is stuck. Check runners.')) - if retried - = icon('refresh', class: 'text-warning has-tooltip', title: 'Job was retried') + = icon('refresh', class: 'text-warning has-tooltip', title: _('Job was retried')) .label-container - if job.tags.any? @@ -44,13 +44,13 @@ %span.badge.badge-primary = tag - if job.try(:trigger_request) - %span.badge.badge-info triggered + %span.badge.badge-info= _('triggered') - if job.try(:allow_failure) - %span.badge.badge-danger allowed to fail + %span.badge.badge-danger= _('allowed to fail') - if job.schedulable? %span.badge.badge-info= s_('DelayedJobs|delayed') - elsif job.action? - %span.badge.badge-info manual + %span.badge.badge-info= _('manual') - if pipeline_link %td @@ -70,7 +70,7 @@ - if job.try(:runner) = runner_link(job.runner) - else - .light none + .light= _('none') - if stage %td @@ -97,11 +97,11 @@ %td .float-right - if can?(current_user, :read_build, job) && job.artifacts? - = link_to download_project_job_artifacts_path(job.project, job), rel: 'nofollow', download: '', title: 'Download artifacts', class: 'btn btn-build' do + = link_to download_project_job_artifacts_path(job.project, job), rel: 'nofollow', download: '', title: _('Download artifacts'), class: 'btn btn-build' do = sprite_icon('download') - if can?(current_user, :update_build, job) - if job.active? - = link_to cancel_project_job_path(job.project, job, continue: { to: request.fullpath }), method: :post, title: 'Cancel', class: 'btn btn-build' do + = link_to cancel_project_job_path(job.project, job, continue: { to: request.fullpath }), method: :post, title: _('Cancel'), class: 'btn btn-build' do = icon('remove', class: 'cred') - elsif job.scheduled? .btn-group @@ -123,8 +123,8 @@ = sprite_icon('time-out') - elsif allow_retry - if job.playable? && !admin && can?(current_user, :update_build, job) - = link_to play_project_job_path(job.project, job, return_to: request.original_url), method: :post, title: 'Play', class: 'btn btn-build' do + = link_to play_project_job_path(job.project, job, return_to: request.original_url), method: :post, title: _('Play'), class: 'btn btn-build' do = custom_icon('icon_play') - elsif job.retryable? - = link_to retry_project_job_path(job.project, job, return_to: request.original_url), method: :post, title: 'Retry', class: 'btn btn-build' do + = link_to retry_project_job_path(job.project, job, return_to: request.original_url), method: :post, title: _('Retry'), class: 'btn btn-build' do = icon('repeat') diff --git a/app/views/projects/ci/lints/_create.html.haml b/app/views/projects/ci/lints/_create.html.haml index b4c18374220..59b5b9f8a30 100644 --- a/app/views/projects/ci/lints/_create.html.haml +++ b/app/views/projects/ci/lints/_create.html.haml @@ -1,15 +1,15 @@ - if @status %p - %b Status: - syntax is correct + %b= _("Status:") + = _("syntax is correct") %i.fa.fa-ok.correct-syntax .table-holder %table.table.table-bordered %thead %tr - %th Parameter - %th Value + %th= _("Parameter") + %th= _("Value") %tbody - @stages.each do |stage| - @builds.select { |build| build[:stage] == stage }.each do |build| @@ -22,27 +22,27 @@ %pre= job[:after_script].to_a.join('\n') %br - %b Tag list: + %b= _("Tag list:") = build[:tag_list].to_a.join(", ") %br - %b Only policy: + %b= _("Only policy:") = job[:only].to_a.join(", ") %br - %b Except policy: + %b= _("Except policy:") = job[:except].to_a.join(", ") %br - %b Environment: + %b= _("Environment:") = build[:environment] %br - %b When: + %b= _("When:") = build[:when] - if build[:allow_failure] - %b Allowed to fail + %b= _("Allowed to fail") - else %p - %b Status: - syntax is incorrect + %b= _("Status:") + = _("syntax is incorrect") %i.fa.fa-remove.incorrect-syntax - %b Error: + %b= _("Error:") = @error diff --git a/app/views/projects/ci/lints/show.html.haml b/app/views/projects/ci/lints/show.html.haml index cbda6bf2107..7b87664961e 100644 --- a/app/views/projects/ci/lints/show.html.haml +++ b/app/views/projects/ci/lints/show.html.haml @@ -1,9 +1,9 @@ -- page_title "CI Lint" -- page_description "Validate your GitLab CI configuration file" +- page_title _("CI Lint") +- page_description _("Validate your GitLab CI configuration file") - content_for :library_javascripts do = page_specific_javascript_tag('lib/ace.js') -%h2.pt-3.pb-3 Check your .gitlab-ci.yml +%h2.pt-3.pb-3= _("Check your .gitlab-ci.yml") .project-ci-linter = form_tag project_ci_lint_path(@project), method: :post do @@ -11,14 +11,14 @@ .col-sm-12 .file-holder .js-file-title.file-title.clearfix - Content of .gitlab-ci.yml + = _("Contents of .gitlab-ci.yml") #ci-editor.ci-editor= @content = text_area_tag(:content, @content, class: 'hidden form-control span1', rows: 7, require: true) .col-sm-12 .float-left.prepend-top-10 - = submit_tag('Validate', class: 'btn btn-success submit-yml') + = submit_tag(_('Validate'), class: 'btn btn-success submit-yml') .float-right.prepend-top-10 - = button_tag('Clear', type: 'button', class: 'btn btn-default clear-yml') + = button_tag(_('Clear'), type: 'button', class: 'btn btn-default clear-yml') .row.prepend-top-20 .col-sm-12 diff --git a/app/views/projects/commit/_ci_menu.html.haml b/app/views/projects/commit/_ci_menu.html.haml index f6666921a25..8b6e3e42ea1 100644 --- a/app/views/projects/commit/_ci_menu.html.haml +++ b/app/views/projects/commit/_ci_menu.html.haml @@ -1,9 +1,11 @@ +- any_pipelines = @commit.present(current_user: current_user).any_pipelines? + %ul.nav-links.no-top.no-bottom.commit-ci-menu.nav.nav-tabs = nav_link(path: 'commit#show') do = link_to project_commit_path(@project, @commit.id) do Changes %span.badge.badge-pill= @diffs.size - - if can?(current_user, :read_pipeline, @project) + - if any_pipelines = nav_link(path: 'commit#pipelines') do = link_to pipelines_project_commit_path(@project, @commit.id) do Pipelines diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml index 2a919a767c0..90fee2d70be 100644 --- a/app/views/projects/commit/_commit_box.html.haml +++ b/app/views/projects/commit/_commit_box.html.haml @@ -49,7 +49,7 @@ #{ _('Download') } - unless @commit.parents.length > 1 %li= link_to s_("DownloadCommit|Email Patches"), project_commit_path(@project, @commit, format: :patch), class: "qa-email-patches" - %li= link_to s_("DownloadCommit|Plain Diff"), project_commit_path(@project, @commit, format: :diff), class: "qa-plain-diff" + %li= link_to s_("DownloadCommit|Plain Diff"), project_commit_path(@project, @commit, format: :diff), class: "qa-plain-diff" .commit-box{ data: { project_path: project_path(@project) } } %h3.commit-title @@ -74,8 +74,8 @@ %span.commit-info.merge-requests{ 'data-project-commit-path' => merge_requests_project_commit_path(@project, @commit.id, format: :json) } = icon('spinner spin') - - if @commit.last_pipeline - - last_pipeline = @commit.last_pipeline + - last_pipeline = @commit.last_pipeline + - if can?(current_user, :read_pipeline, last_pipeline) .well-segment.pipeline-info .status-icon-container = link_to project_pipeline_path(@project, last_pipeline.id), class: "ci-status-icon-#{last_pipeline.status}" do diff --git a/app/views/projects/commit/show.html.haml b/app/views/projects/commit/show.html.haml index 79e32949db9..06f0cd9675e 100644 --- a/app/views/projects/commit/show.html.haml +++ b/app/views/projects/commit/show.html.haml @@ -9,10 +9,7 @@ .container-fluid{ class: [limited_container_width, container_class] } = render "commit_box" - - if @commit.status - = render "ci_menu" - - else - .block-connector + = render "ci_menu" = render "projects/diffs/diffs", diffs: @diffs, environment: @environment, is_commit: true .limited-width-notes diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml index 1a74b120c26..0d3c6e7027c 100644 --- a/app/views/projects/commits/_commit.html.haml +++ b/app/views/projects/commits/_commit.html.haml @@ -6,6 +6,7 @@ - merge_request = local_assigns.fetch(:merge_request, nil) - project = local_assigns.fetch(:project) { merge_request&.project } - ref = local_assigns.fetch(:ref) { merge_request&.source_branch } +- commit_status = commit.present(current_user: current_user).status_for(ref) - link = commit_path(project, commit, merge_request: merge_request) %li.commit.flex-row.js-toggle-container{ id: "commit-#{commit.short_id}" } @@ -22,7 +23,7 @@ %span.commit-row-message.d-block.d-sm-none · = commit.short_id - - if commit.status(ref) + - if commit_status .d-block.d-sm-none = render_commit_status(commit, ref: ref) - if commit.description? @@ -45,7 +46,7 @@ - else = render partial: 'projects/commit/ajax_signature', locals: { commit: commit } - - if commit.status(ref) + - if commit_status = render_commit_status(commit, ref: ref) .js-commit-pipeline-status{ data: { endpoint: pipelines_project_commit_path(project, commit.id, ref: ref) } } diff --git a/app/views/projects/deploy_keys/_index.html.haml b/app/views/projects/deploy_keys/_index.html.haml index 062aa423bde..24d665761cc 100644 --- a/app/views/projects/deploy_keys/_index.html.haml +++ b/app/views/projects/deploy_keys/_index.html.haml @@ -3,7 +3,7 @@ .settings-header %h4 Deploy Keys - %button.btn.js-settings-toggle.qa-expand-deploy-keys{ type: 'button' } + %button.btn.js-settings-toggle{ type: 'button' } = expanded ? 'Collapse' : 'Expand' %p Deploy keys allow read-only or read-write (if enabled) access to your repository. Deploy keys can be used for CI, staging or production servers. You can create a deploy key or add an existing one. diff --git a/app/views/projects/issues/_merge_requests.html.haml b/app/views/projects/issues/_merge_requests.html.haml index 5c36d2202a6..310e339ac8d 100644 --- a/app/views/projects/issues/_merge_requests.html.haml +++ b/app/views/projects/issues/_merge_requests.html.haml @@ -1,35 +1,36 @@ - 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| + - merge_request = merge_request.present(current_user: current_user) + %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.can_read_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..43e4c8db93f --- /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/issues/_new_branch.html.haml b/app/views/projects/issues/_new_branch.html.haml index 5374f4a1de0..fbd70cd1906 100644 --- a/app/views/projects/issues/_new_branch.html.haml +++ b/app/views/projects/issues/_new_branch.html.haml @@ -30,7 +30,7 @@ = icon('check', class: 'icon') = _('Create merge request and branch') - %li{ class: [!can_create_merge_request && 'droplab-item-selected'], role: 'button', data: { value: 'create-branch', text: _('Create branch') } } + %li{ class: [!can_create_merge_request && 'droplab-item-selected'], role: 'button', data: { value: 'create-branch', text: _('Create branch') } } .menu-item = icon('check', class: 'icon') = _('Create branch') diff --git a/app/views/projects/issues/_related_branches.html.haml b/app/views/projects/issues/_related_branches.html.haml index 1df38db9fd4..ffdd96870ef 100644 --- a/app/views/projects/issues/_related_branches.html.haml +++ b/app/views/projects/issues/_related_branches.html.haml @@ -6,7 +6,7 @@ %li - target = @project.repository.find_branch(branch).dereferenced_target - pipeline = @project.pipeline_for(branch, target.sha) if target - - if pipeline + - if can?(current_user, :read_pipeline, pipeline) %span.related-branch-ci-status = render_pipeline_status(pipeline) %span.related-branch-info diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml index f048fb91304..653b7d4c6f3 100644 --- a/app/views/projects/issues/show.html.haml +++ b/app/views/projects/issues/show.html.haml @@ -15,7 +15,10 @@ .issuable-status-box.status-box.status-box-issue-closed{ class: issue_button_visibility(@issue, false) } = sprite_icon('mobile-issue-close', size: 16, css_class: 'd-block d-sm-none') %span.d-none.d-sm-block - Closed + - if @issue.moved? + = _("Closed (moved)") + - else + = _("Closed") .issuable-status-box.status-box.status-box-open{ class: issue_button_visibility(@issue, true) } = sprite_icon('issue-open-m', size: 16, css_class: 'd-block d-sm-none') %span.d-none.d-sm-block Open 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/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml index faa070d0389..ac29cd8f679 100644 --- a/app/views/projects/merge_requests/_merge_request.html.haml +++ b/app/views/projects/merge_requests/_merge_request.html.haml @@ -46,7 +46,7 @@ %li.issuable-status.d-none.d-sm-inline-block = icon('ban') CLOSED - - if merge_request.head_pipeline + - if can?(current_user, :read_pipeline, merge_request.head_pipeline) %li.issuable-pipeline-status.d-none.d-sm-inline-block = render_pipeline_status(merge_request.head_pipeline) - if merge_request.open? && merge_request.broken? @@ -56,6 +56,7 @@ - if merge_request.assignee %li = link_to_member(merge_request.source_project, merge_request.assignee, name: false, title: _('Assigned to :name')) + = render_if_exists 'projects/merge_requests/approvals_count', merge_request: merge_request = render 'shared/issuable_meta_data', issuable: merge_request diff --git a/app/views/projects/milestones/_form.html.haml b/app/views/projects/milestones/_form.html.haml index ebd3229e42b..19f5bba75c4 100644 --- a/app/views/projects/milestones/_form.html.haml +++ b/app/views/projects/milestones/_form.html.haml @@ -1,27 +1,29 @@ = form_for [@project.namespace.becomes(Namespace), @project, @milestone], - html: {class: 'milestone-form common-note-form js-quick-submit js-requires-input'}, - data: { markdown_version: @milestone.cached_markdown_version } do |f| + html: { class: 'milestone-form common-note-form js-quick-submit js-requires-input' }, + data: { markdown_version: @milestone.cached_markdown_version } do |f| = form_errors(@milestone) .row .col-md-6 .form-group.row - = f.label :title, "Title", class: "col-form-label col-sm-2" + .col-form-label.col-sm-2 + = f.label :title, _('Title') .col-sm-10 - = f.text_field :title, maxlength: 255, class: "qa-milestone-title form-control", required: true, autofocus: true + = f.text_field :title, maxlength: 255, class: 'qa-milestone-title form-control', required: true, autofocus: true .form-group.row.milestone-description - = f.label :description, "Description", class: "col-form-label col-sm-2" + .col-form-label.col-sm-2 + = f.label :description, _('Description') .col-sm-10 = render layout: 'projects/md_preview', locals: { url: preview_markdown_path(@project) } do - = render 'projects/zen', f: f, attr: :description, classes: 'qa-milestone-description note-textarea', placeholder: 'Write milestone description...' + = render 'projects/zen', f: f, attr: :description, classes: 'qa-milestone-description note-textarea', placeholder: _('Write milestone description...') = render 'shared/notes/hints' .clearfix .error-alert - = render "shared/milestones/form_dates", f: f + = render 'shared/milestones/form_dates', f: f .form-actions - if @milestone.new_record? - = f.submit 'Create milestone', class: "btn-success btn qa-milestone-create-button" - = link_to "Cancel", project_milestones_path(@project), class: "btn btn-cancel" + = f.submit _('Create milestone'), class: 'btn-create btn qa-milestone-create-button' + = link_to _('Cancel'), project_milestones_path(@project), class: 'btn btn-cancel' - else - = f.submit 'Save changes', class: "btn-success btn" - = link_to "Cancel", project_milestone_path(@project, @milestone), class: "btn btn-cancel" + = f.submit _('Save changes'), class: 'btn-success btn' + = link_to _('Cancel'), project_milestone_path(@project, @milestone), class: 'btn btn-cancel' diff --git a/app/views/projects/milestones/edit.html.haml b/app/views/projects/milestones/edit.html.haml index 4006a468792..aa564e00af9 100644 --- a/app/views/projects/milestones/edit.html.haml +++ b/app/views/projects/milestones/edit.html.haml @@ -1,14 +1,14 @@ - @no_container = true -- breadcrumb_title "Edit" -- add_to_breadcrumbs "Milestones", project_milestones_path(@project) -- page_title "Edit", @milestone.title, "Milestones" +- breadcrumb_title _('Edit') +- add_to_breadcrumbs _('Milestones'), project_milestones_path(@project) +- page_title _('Edit'), @milestone.title, _('Milestones') %div{ class: container_class } %h3.page-title - Edit Milestone + = _('Edit Milestone') %hr - = render "form" + = render 'form' diff --git a/app/views/projects/milestones/index.html.haml b/app/views/projects/milestones/index.html.haml index 57f3c640696..a3414c16d73 100644 --- a/app/views/projects/milestones/index.html.haml +++ b/app/views/projects/milestones/index.html.haml @@ -1,15 +1,16 @@ - @no_container = true -- page_title 'Milestones' +- page_title _('Milestones') %div{ class: container_class } .top-area = render 'shared/milestones_filter', counts: milestone_counts(@project.milestones) .nav-controls + = render 'shared/milestones/search_form' = render 'shared/milestones_sort_dropdown' - if can?(current_user, :admin_milestone, @project) - = link_to new_project_milestone_path(@project), class: "btn btn-success qa-new-project-milestone", title: 'New milestone' do - New milestone + = link_to new_project_milestone_path(@project), class: 'btn btn-success qa-new-project-milestone', title: _('New milestone') do + = _('New milestone') .milestones #delete-milestone-modal @@ -20,6 +21,6 @@ - if @milestones.blank? %li - .nothing-here-block No milestones to show + .nothing-here-block= _('No milestones to show') = paginate @milestones, theme: 'gitlab' diff --git a/app/views/projects/milestones/new.html.haml b/app/views/projects/milestones/new.html.haml index 01cc951e8c2..79207fd70b5 100644 --- a/app/views/projects/milestones/new.html.haml +++ b/app/views/projects/milestones/new.html.haml @@ -1,12 +1,12 @@ - @no_container = true -- add_to_breadcrumbs "Milestones", project_milestones_path(@project) -- breadcrumb_title "New" -- page_title "New Milestone" +- add_to_breadcrumbs _('Milestones'), project_milestones_path(@project) +- breadcrumb_title _('New') +- page_title _('New Milestone') %div{ class: container_class } %h3.page-title - New Milestone + = _('New Milestone') %hr - = render "form" + = render 'form' diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml index 5859de61d71..0542b349e44 100644 --- a/app/views/projects/milestones/show.html.haml +++ b/app/views/projects/milestones/show.html.haml @@ -1,30 +1,30 @@ - @no_container = true -- add_to_breadcrumbs "Milestones", project_milestones_path(@project) +- add_to_breadcrumbs _('Milestones'), project_milestones_path(@project) - breadcrumb_title @milestone.title -- page_title @milestone.title, "Milestones" +- page_title @milestone.title, _('Milestones') - page_description @milestone.description %div{ class: container_class } .detail-page-header.milestone-page-header .status-box{ class: status_box_class(@milestone) } - if @milestone.closed? - Closed + = _('Closed') - elsif @milestone.expired? - Past due + = _('Past due') - elsif @milestone.upcoming? - Upcoming + = _('Upcoming') - else - Open + = _('Open') .header-text-content %span.identifier %strong - Milestone + = _('Milestone') - if @milestone.due_date || @milestone.start_date = milestone_date_range(@milestone) .milestone-buttons - if can?(current_user, :admin_milestone, @project) - = link_to edit_project_milestone_path(@project, @milestone), class: "btn btn-grouped btn-nr" do - Edit + = link_to edit_project_milestone_path(@project, @milestone), class: 'btn btn-grouped btn-nr' do + = _('Edit') - if @project.group %button.js-promote-project-milestone-button.btn.btn-grouped{ data: { toggle: 'modal', @@ -39,13 +39,13 @@ #promote-milestone-modal - if @milestone.active? - = link_to 'Close milestone', project_milestone_path(@project, @milestone, milestone: {state_event: :close }), method: :put, class: "btn btn-close btn-nr btn-grouped" + = link_to _('Close milestone'), project_milestone_path(@project, @milestone, milestone: {state_event: :close }), method: :put, class: 'btn btn-close btn-nr btn-grouped' - else - = link_to 'Reopen milestone', project_milestone_path(@project, @milestone, milestone: {state_event: :activate }), method: :put, class: "btn btn-reopen btn-nr btn-grouped" + = link_to _('Reopen milestone'), project_milestone_path(@project, @milestone, milestone: {state_event: :activate }), method: :put, class: 'btn btn-reopen btn-nr btn-grouped' = render 'shared/milestones/delete_button' - %a.btn.btn-default.btn-grouped.float-right.d-block.d-sm-none.js-sidebar-toggle{ href: "#" } + %a.btn.btn-default.btn-grouped.float-right.d-block.d-sm-none.js-sidebar-toggle{ href: '#' } = icon('angle-double-left') .detail-page-description.milestone-detail @@ -62,10 +62,10 @@ - if can?(current_user, :read_issue, @project) && @milestone.total_items_count(current_user).zero? .alert.alert-success.prepend-top-default - %span Assign some issues to this milestone. + %span= _('Assign some issues to this milestone.') - elsif @milestone.complete?(current_user) && @milestone.active? .alert.alert-success.prepend-top-default - %span All issues for this milestone are closed. You may close this milestone now. + %span= _('All issues for this milestone are closed. You may close this milestone now.') = render 'deprecation_message' = render 'shared/milestones/tabs', milestone: @milestone diff --git a/app/views/projects/pages_domains/_form.html.haml b/app/views/projects/pages_domains/_form.html.haml index 0d848f7899c..b7b46c56c37 100644 --- a/app/views/projects/pages_domains/_form.html.haml +++ b/app/views/projects/pages_domains/_form.html.haml @@ -6,25 +6,24 @@ .form-group.row = f.label :domain, class: 'col-form-label col-sm-2' do - Domain + = _("Domain") .col-sm-10 = f.text_field :domain, required: true, autocomplete: 'off', class: 'form-control', disabled: @domain.persisted? - if Gitlab.config.pages.external_https .form-group.row = f.label :certificate, class: 'col-form-label col-sm-2' do - Certificate (PEM) + = _("Certificate (PEM)") .col-sm-10 = f.text_area :certificate, rows: 5, class: 'form-control' - %span.help-inline Upload a certificate for your domain with all intermediates + %span.help-inline= _("Upload a certificate for your domain with all intermediates") .form-group.row = f.label :key, class: 'col-form-label col-sm-2' do - Key (PEM) + = _("Key (PEM)") .col-sm-10 = f.text_area :key, rows: 5, class: 'form-control' - %span.help-inline Upload a private key for your certificate + %span.help-inline= _("Upload a private key for your certificate") - else .nothing-here-block - Support for custom certificates is disabled. - Ask your system's administrator to enable it. + = _("Support for custom certificates is disabled. Ask your system's administrator to enable it.") diff --git a/app/views/projects/pages_domains/edit.html.haml b/app/views/projects/pages_domains/edit.html.haml index 342b1482df7..e11387ae742 100644 --- a/app/views/projects/pages_domains/edit.html.haml +++ b/app/views/projects/pages_domains/edit.html.haml @@ -1,4 +1,4 @@ -- add_to_breadcrumbs "Pages", project_pages_path(@project) +- add_to_breadcrumbs _("Pages"), project_pages_path(@project) - breadcrumb_title @domain.domain - page_title @domain.domain %h3.page-title @@ -8,4 +8,4 @@ = form_for [@project.namespace.becomes(Namespace), @project, @domain], html: { class: 'fieldset-form' } do |f| = render 'form', { f: f } .form-actions - = f.submit 'Save Changes', class: "btn btn-success" + = f.submit _('Save Changes'), class: "btn btn-success" diff --git a/app/views/projects/pages_domains/new.html.haml b/app/views/projects/pages_domains/new.html.haml index 94ad1470052..c7cefa87c76 100644 --- a/app/views/projects/pages_domains/new.html.haml +++ b/app/views/projects/pages_domains/new.html.haml @@ -1,12 +1,12 @@ -- add_to_breadcrumbs "Pages", project_pages_path(@project) -- page_title 'New Pages Domain' +- add_to_breadcrumbs _("Pages"), project_pages_path(@project) +- page_title _('New Pages Domain') %h3.page-title - New Pages Domain + = _("New Pages Domain") %hr.clearfix %div = form_for [@project.namespace.becomes(Namespace), @project, @domain], html: { class: 'fieldset-form' } do |f| = render 'form', { f: f } .form-actions - = f.submit 'Create New Domain', class: "btn btn-success" + = f.submit _('Create New Domain'), class: "btn btn-success" .float-right = link_to _('Cancel'), project_pages_path(@project), class: 'btn btn-cancel' diff --git a/app/views/projects/pages_domains/show.html.haml b/app/views/projects/pages_domains/show.html.haml index a8484187493..82147568981 100644 --- a/app/views/projects/pages_domains/show.html.haml +++ b/app/views/projects/pages_domains/show.html.haml @@ -1,6 +1,6 @@ -- add_to_breadcrumbs "Pages", project_pages_path(@project) +- add_to_breadcrumbs _("Pages"), project_pages_path(@project) - breadcrumb_title @domain.domain -- page_title "#{@domain.domain}", 'Pages Domains' +- page_title "#{@domain.domain}", _('Pages Domains') - dns_record = "#{@domain.domain} CNAME #{@domain.project.pages_subdomain}.#{Settings.pages.host}." - verification_enabled = Gitlab::CurrentSettings.pages_domain_verification_enabled? @@ -9,37 +9,37 @@ = content_for :flash_message do .alert.alert-warning .container-fluid.container-limited - This domain is not verified. You will need to verify ownership before access is enabled. + = _("This domain is not verified. You will need to verify ownership before access is enabled.") %h3.page-title.with-button - = link_to 'Edit', edit_project_pages_domain_path(@project, @domain), class: 'btn btn-success float-right' - Pages Domain + = link_to _('Edit'), edit_project_pages_domain_path(@project, @domain), class: 'btn btn-success float-right' + = _("Pages Domain") .table-holder %table.table %tr %td - Domain + = _("Domain") %td = link_to @domain.url do = @domain.url = icon('external-link') %tr %td - DNS + = _("DNS") %td .input-group = text_field_tag :domain_dns, dns_record , class: "monospace js-select-on-focus form-control", readonly: true .input-group-append = clipboard_button(target: '#domain_dns', class: 'btn-default input-group-text d-none d-sm-block') %p.form-text.text-muted - To access this domain create a new DNS record + = _("To access this domain create a new DNS record") - if verification_enabled - verification_record = "#{@domain.verification_domain} TXT #{@domain.keyed_verification_code}" %tr %td - Verification status + = _("Verification status") %td = form_tag verify_project_pages_domain_path(@project, @domain) do .status-badge @@ -53,17 +53,16 @@ .input-group-append = clipboard_button(target: '#domain_verification', class: 'btn-default d-none d-sm-block') %p.form-text.text-muted - - help_link = help_page_path('user/project/pages/getting_started_part_three.md', anchor: 'dns-txt-record') - To #{link_to 'verify ownership', help_link} of your domain, - add the above key to a TXT record within to your DNS configuration. + - link_to_help = link_to(_('verify ownership'), help_page_path('user/project/pages/getting_started_part_three.md', anchor: 'dns-txt-record')) + = _("To %{link_to_help} of your domain, add the above key to a TXT record within to your DNS configuration.").html_safe % { link_to_help: link_to_help } %tr %td - Certificate + = _("Certificate") %td - if @domain.certificate_text %pre = @domain.certificate_text - else .light - missing + = _("missing") diff --git a/app/views/projects/pipeline_schedules/_form.html.haml b/app/views/projects/pipeline_schedules/_form.html.haml index 259979417e0..1121cf06b5c 100644 --- a/app/views/projects/pipeline_schedules/_form.html.haml +++ b/app/views/projects/pipeline_schedules/_form.html.haml @@ -34,7 +34,7 @@ = n_('Reveal value', 'Reveal values', @schedule.variables.size) .form-group.row .col-md-9 - = f.label :active, s_('PipelineSchedules|Activated'), class: 'label-bold' + = f.label :active, s_('PipelineSchedules|Activated'), class: 'label-bold' %div = f.check_box :active, required: false, value: @schedule.active? = _('Active') diff --git a/app/views/projects/pipelines/_info.html.haml b/app/views/projects/pipelines/_info.html.haml index 0f0114d513c..69a47faabed 100644 --- a/app/views/projects/pipelines/_info.html.haml +++ b/app/views/projects/pipelines/_info.html.haml @@ -6,23 +6,22 @@ = preserve(markdown(commit.description, pipeline: :single_line)) .info-well - - if commit.status - .well-segment.pipeline-info - .icon-container - = icon('clock-o') - = pluralize @pipeline.total_size, "job" - - if @pipeline.ref - from - - if @pipeline.ref_exists? - = link_to @pipeline.ref, project_ref_path(@project, @pipeline.ref), class: "ref-name" - - else - %span.ref-name - = @pipeline.ref - - if @pipeline.duration - in - = time_interval_in_words(@pipeline.duration) - - if @pipeline.queued_duration - = "(queued for #{time_interval_in_words(@pipeline.queued_duration)})" + .well-segment.pipeline-info + .icon-container + = icon('clock-o') + = pluralize @pipeline.total_size, "job" + - if @pipeline.ref + from + - if @pipeline.ref_exists? + = link_to @pipeline.ref, project_ref_path(@project, @pipeline.ref), class: "ref-name" + - else + %span.ref-name + = @pipeline.ref + - if @pipeline.duration + in + = time_interval_in_words(@pipeline.duration) + - if @pipeline.queued_duration + = "(queued for #{time_interval_in_words(@pipeline.queued_duration)})" .well-segment .icon-container diff --git a/app/views/projects/project_members/_team.html.haml b/app/views/projects/project_members/_team.html.haml index b92ecf4412f..e0dd386fc5d 100644 --- a/app/views/projects/project_members/_team.html.haml +++ b/app/views/projects/project_members/_team.html.haml @@ -6,7 +6,7 @@ %span.flex-project-title = _("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_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 } diff --git a/app/views/projects/project_templates/_built_in_templates.html.haml b/app/views/projects/project_templates/_built_in_templates.html.haml index 5b4d8927045..2a0ce4bd16b 100644 --- a/app/views/projects/project_templates/_built_in_templates.html.haml +++ b/app/views/projects/project_templates/_built_in_templates.html.haml @@ -12,6 +12,6 @@ %a.btn.btn-default.append-right-10{ href: template.preview, rel: 'noopener noreferrer', target: '_blank', data: { track_label: "create_from_template", track_property: "template_preview", track_event: "click_button", track_value: template.name } } = _("Preview") %label.btn.btn-success.template-button.choose-template.append-bottom-0{ for: template.name } - %input{ type: "radio", autocomplete: "off", name: "project[template_name]", id: template.name, value: template.name, data: { track_label: "create_from_template", track_property: "template_use", track_event: "click_button" } } + %input{ type: "radio", autocomplete: "off", name: "project[template_name]", id: template.name, value: template.name, data: { track_label: "create_from_template", track_property: "template_use", track_event: "click_button" } } %span = _("Use template") diff --git a/app/views/projects/protected_branches/shared/_protected_branch.html.haml b/app/views/projects/protected_branches/shared/_protected_branch.html.haml index 81b07af22ad..bb7998f739d 100644 --- a/app/views/projects/protected_branches/shared/_protected_branch.html.haml +++ b/app/views/projects/protected_branches/shared/_protected_branch.html.haml @@ -15,7 +15,7 @@ = link_to(commit.short_id, namespace_project_commit_path(@project.namespace, @project, commit.id), class: 'commit-sha') = time_ago_with_tooltip(commit.committed_date) - else - (branch was removed from repository) + (branch was deleted from repository) = yield diff --git a/app/views/projects/serverless/functions/index.html.haml b/app/views/projects/serverless/functions/index.html.haml index f650fa0f38f..635580eac5c 100644 --- a/app/views/projects/serverless/functions/index.html.haml +++ b/app/views/projects/serverless/functions/index.html.haml @@ -5,7 +5,7 @@ - status_path = project_serverless_functions_path(@project, format: :json) - clusters_path = project_clusters_path(@project) -.serverless-functions-page.js-serverless-functions-page{ data: { status_path: status_path, installed: @installed, clusters_path: clusters_path, help_path: help_page_path('user/project/clusters/serverless/index') } } +.serverless-functions-page.js-serverless-functions-page{ data: { status_path: status_path, installed: @installed, clusters_path: clusters_path, help_path: help_page_path('user/project/clusters/serverless/index') } } %div{ class: [container_class, ('limit-container-width' unless fluid_layout)] } .js-serverless-functions-notice diff --git a/app/views/projects/settings/ci_cd/_form.html.haml b/app/views/projects/settings/ci_cd/_form.html.haml index bb328f5344c..bfb275b9ef5 100644 --- a/app/views/projects/settings/ci_cd/_form.html.haml +++ b/app/views/projects/settings/ci_cd/_form.html.haml @@ -110,6 +110,9 @@ %li go test -cover (Go) %code coverage: \d+.\d+% of statements + %li + nyc npm test (NodeJS) - + %code All files[^|]*\|[^|]*\s+([\d\.]+) = f.submit _('Save changes'), class: "btn btn-success" 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/projects/tags/_tag.html.haml b/app/views/projects/tags/_tag.html.haml index f55202c2c5f..cc203cfad86 100644 --- a/app/views/projects/tags/_tag.html.haml +++ b/app/views/projects/tags/_tag.html.haml @@ -28,7 +28,7 @@ = render 'projects/buttons/download', project: @project, ref: tag.name, pipeline: @tags_pipelines[tag.name] - if can?(current_user, :push_code, @project) - = link_to edit_project_tag_release_path(@project, tag.name), class: 'btn has-tooltip', title: s_('TagsPage|Edit release notes'), data: { container: "body" } do + = link_to edit_project_tag_release_path(@project, tag.name), class: 'btn btn-edit has-tooltip', title: s_('TagsPage|Edit release notes'), data: { container: "body" } do = icon("pencil") - if can?(current_user, :admin_project, @project) diff --git a/app/views/projects/tags/show.html.haml b/app/views/projects/tags/show.html.haml index 15a960f81b8..feeaf799f51 100644 --- a/app/views/projects/tags/show.html.haml +++ b/app/views/projects/tags/show.html.haml @@ -20,7 +20,7 @@ .nav-controls.controls-flex - if can?(current_user, :push_code, @project) - = link_to edit_project_tag_release_path(@project, @tag.name), class: 'btn controls-item has-tooltip', title: s_('TagsPage|Edit release notes') do + = link_to edit_project_tag_release_path(@project, @tag.name), class: 'btn btn-edit controls-item has-tooltip', title: s_('TagsPage|Edit release notes') do = icon("pencil") = link_to project_tree_path(@project, @tag.name), class: 'btn controls-item has-tooltip', title: s_('TagsPage|Browse files') do = icon('files-o') diff --git a/app/views/projects/tree/_tree_header.html.haml b/app/views/projects/tree/_tree_header.html.haml index 4e9a119ac66..ec8e5234bd4 100644 --- a/app/views/projects/tree/_tree_header.html.haml +++ b/app/views/projects/tree/_tree_header.html.haml @@ -40,27 +40,24 @@ #{ _('New directory') } - elsif can?(current_user, :fork_project, @project) && can?(current_user, :create_merge_request_in, @project) %li - - continue_params = { to: project_new_blob_path(@project, @id), - notice: edit_in_new_fork_notice, + - continue_params = { to: project_new_blob_path(@project, @id), + notice: edit_in_new_fork_notice, notice_now: edit_in_new_fork_notice_now } - - fork_path = project_forks_path(@project, namespace_key: current_user.namespace.id, - continue: continue_params) + - fork_path = project_forks_path(@project, namespace_key: current_user.namespace.id, continue: continue_params) = link_to fork_path, method: :post do #{ _('New file') } %li - - continue_params = { to: request.fullpath, - notice: edit_in_new_fork_notice + " Try to upload a file again.", + - continue_params = { to: request.fullpath, + notice: edit_in_new_fork_notice + " Try to upload a file again.", notice_now: edit_in_new_fork_notice_now } - - fork_path = project_forks_path(@project, namespace_key: current_user.namespace.id, - continue: continue_params) + - fork_path = project_forks_path(@project, namespace_key: current_user.namespace.id, continue: continue_params) = link_to fork_path, method: :post do #{ _('Upload file') } %li - - continue_params = { to: request.fullpath, - notice: edit_in_new_fork_notice + " Try to create a new directory again.", + - continue_params = { to: request.fullpath, + notice: edit_in_new_fork_notice + " Try to create a new directory again.", notice_now: edit_in_new_fork_notice_now } - - fork_path = project_forks_path(@project, namespace_key: current_user.namespace.id, - continue: continue_params) + - fork_path = project_forks_path(@project, namespace_key: current_user.namespace.id, continue: continue_params) = link_to fork_path, method: :post do #{ _('New directory') } diff --git a/app/views/projects/triggers/_trigger.html.haml b/app/views/projects/triggers/_trigger.html.haml index 7e4618e1a88..6f6f1e5e0c5 100644 --- a/app/views/projects/triggers/_trigger.html.haml +++ b/app/views/projects/triggers/_trigger.html.haml @@ -1,6 +1,6 @@ %tr %td - - if can?(current_user, :admin_trigger, trigger) + - if trigger.has_token_exposed? %span= trigger.token = clipboard_button(text: trigger.token, title: "Copy trigger token to clipboard") - else diff --git a/app/views/projects/wikis/_form.html.haml b/app/views/projects/wikis/_form.html.haml index 7d8826e540c..d1556dbd077 100644 --- a/app/views/projects/wikis/_form.html.haml +++ b/app/views/projects/wikis/_form.html.haml @@ -16,7 +16,7 @@ .form-group.row .col-sm-12= f.label :title, class: 'control-label-full-width' .col-sm-12 - = f.text_field :title, class: 'form-control', value: @page.title + = f.text_field :title, class: 'form-control qa-wiki-title-textbox', value: @page.title - if @page.persisted? %span.edit-wiki-page-slug-tip = icon('lightbulb-o') @@ -31,7 +31,7 @@ .col-sm-12= f.label :content, class: 'control-label-full-width' .col-sm-12 = render layout: 'projects/md_preview', locals: { url: project_wiki_preview_markdown_path(@project, @page.slug) } do - = render 'projects/zen', f: f, attr: :content, classes: 'note-textarea', placeholder: s_("WikiPage|Write your content or drag files here…") + = render 'projects/zen', f: f, attr: :content, classes: 'note-textarea qa-wiki-content-textarea', placeholder: s_("WikiPage|Write your content or drag files here…") = render 'shared/notes/hints' .clearfix @@ -47,14 +47,14 @@ .form-group.row .col-sm-12= f.label :commit_message, class: 'control-label-full-width' - .col-sm-12= f.text_field :message, class: 'form-control', rows: 18, value: commit_message + .col-sm-12= f.text_field :message, class: 'form-control qa-wiki-message-textbox', rows: 18, value: commit_message .form-actions - if @page && @page.persisted? - = f.submit _("Save changes"), class: 'btn-success btn' + = f.submit _("Save changes"), class: 'btn-success btn qa-save-changes-button' .float-right = link_to _("Cancel"), project_wiki_path(@project, @page), class: 'btn btn-cancel btn-grouped' - else - = f.submit s_("Wiki|Create page"), class: 'btn-success btn' + = f.submit s_("Wiki|Create page"), class: 'btn-success btn qa-create-page-button' .float-right = link_to _("Cancel"), project_wiki_path(@project, :home), class: 'btn btn-cancel' diff --git a/app/views/projects/wikis/pages.html.haml b/app/views/projects/wikis/pages.html.haml index aeef64fd7eb..94267b6e0cf 100644 --- a/app/views/projects/wikis/pages.html.haml +++ b/app/views/projects/wikis/pages.html.haml @@ -1,5 +1,5 @@ - @no_container = true -- add_to_breadcrumbs "Wiki", get_project_wiki_path(@project) +- add_to_breadcrumbs "Wiki", project_wiki_path(@project, :home) - breadcrumb_title s_("Wiki|Pages") - page_title s_("Wiki|Pages"), _("Wiki") diff --git a/app/views/projects/wikis/show.html.haml b/app/views/projects/wikis/show.html.haml index 4d5fd55364c..8b348bb4e4f 100644 --- a/app/views/projects/wikis/show.html.haml +++ b/app/views/projects/wikis/show.html.haml @@ -2,7 +2,7 @@ - breadcrumb_title @page.human_title - wiki_breadcrumb_dropdown_links(@page.slug) - page_title @page.human_title, _("Wiki") -- add_to_breadcrumbs _("Wiki"), get_project_wiki_path(@project) +- add_to_breadcrumbs _("Wiki"), project_wiki_path(@project, :home) .wiki-page-header.has-sidebar-toggle %button.btn.btn-default.sidebar-toggle.js-sidebar-wiki-toggle{ role: "button", type: "button" } 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/sent_notifications/unsubscribe.html.haml b/app/views/sent_notifications/unsubscribe.html.haml index 7d3e243495f..ca392e1adfc 100644 --- a/app/views/sent_notifications/unsubscribe.html.haml +++ b/app/views/sent_notifications/unsubscribe.html.haml @@ -1,17 +1,16 @@ - noteable = @sent_notification.noteable - noteable_type = @sent_notification.noteable_type.titleize.downcase - noteable_text = %(#{noteable.title} (#{noteable.to_reference})) -- page_title "Unsubscribe", noteable_text, noteable_type.pluralize, @sent_notification.project.full_name +- page_title _("Unsubscribe"), noteable_text, noteable_type.pluralize, @sent_notification.project.full_name %h3.page-title - Unsubscribe from #{noteable_type} + = _("Unsubscribe from %{type}") % { type: noteable_type } %p - = succeed '?' do - Are you sure you want to unsubscribe from the #{noteable_type}: - = link_to noteable_text, url_for([@sent_notification.project.namespace.becomes(Namespace), @sent_notification.project, noteable]) + - link_to_noteable_text = link_to(noteable_text, url_for([@sent_notification.project.namespace.becomes(Namespace), @sent_notification.project, noteable])) + = _("Are you sure you want to unsubscribe from the %{type}: %{link_to_noteable_text}?").html_safe % { type: noteable_type, link_to_noteable_text: link_to_noteable_text } %p - = link_to 'Unsubscribe', unsubscribe_sent_notification_path(@sent_notification, force: true), + = link_to _('Unsubscribe'), unsubscribe_sent_notification_path(@sent_notification, force: true), class: 'btn btn-primary append-right-10' - = link_to 'Cancel', new_user_session_path, class: 'btn append-right-10' + = link_to _('Cancel'), new_user_session_path, class: 'btn append-right-10' 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/boards/components/_sidebar.html.haml b/app/views/shared/boards/components/_sidebar.html.haml index 1ff956649ed..c9ff63f8c45 100644 --- a/app/views/shared/boards/components/_sidebar.html.haml +++ b/app/views/shared/boards/components/_sidebar.html.haml @@ -1,4 +1,4 @@ -%board-sidebar{ "inline-template" => true, ":current-user" => (UserSerializer.new.represent(current_user) || {}).to_json } +%board-sidebar{ "inline-template" => true, ":current-user" => (UserSerializer.new.represent(current_user) || {}).to_json } %transition{ name: "boards-sidebar-slide" } %aside.right-sidebar.right-sidebar-expanded.issue-boards-sidebar{ "v-show" => "showSidebar" } .issuable-sidebar 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/empty_states/_wikis.html.haml b/app/views/shared/empty_states/_wikis.html.haml index df3308abe0d..73eedcc1dc9 100644 --- a/app/views/shared/empty_states/_wikis.html.haml +++ b/app/views/shared/empty_states/_wikis.html.haml @@ -2,7 +2,7 @@ - if can?(current_user, :create_wiki, @project) - create_path = project_wiki_path(@project, params[:id], { view: 'create' }) - - create_link = link_to s_('WikiEmpty|Create your first page'), create_path, class: 'btn btn-success', title: s_('WikiEmpty|Create your first page') + - create_link = link_to s_('WikiEmpty|Create your first page'), create_path, class: 'btn btn-success qa-create-first-page-link', title: s_('WikiEmpty|Create your first page') = render layout: layout_path, locals: { image_path: 'illustrations/wiki_login_empty.svg' } do %h4.text-left diff --git a/app/views/shared/empty_states/_wikis_layout.html.haml b/app/views/shared/empty_states/_wikis_layout.html.haml index a5f100e3469..d44017299b8 100644 --- a/app/views/shared/empty_states/_wikis_layout.html.haml +++ b/app/views/shared/empty_states/_wikis_layout.html.haml @@ -1,6 +1,6 @@ .row.empty-state .col-12 - .svg-content + .svg-content.qa-svg-content = image_tag image_path .col-12 .text-content.text-center diff --git a/app/views/shared/issuable/_bulk_update_sidebar.html.haml b/app/views/shared/issuable/_bulk_update_sidebar.html.haml index ca02424215c..909eb738f95 100644 --- a/app/views/shared/issuable/_bulk_update_sidebar.html.haml +++ b/app/views/shared/issuable/_bulk_update_sidebar.html.haml @@ -2,7 +2,7 @@ %aside.issues-bulk-update.js-right-sidebar.right-sidebar{ "aria-live" => "polite", data: { 'signed-in': current_user.present? } } .issuable-sidebar.hidden - = form_tag [:bulk_update, @project.namespace.becomes(Namespace), @project, type], method: :post, class: "bulk-update" do + = form_tag [:bulk_update, @project.namespace.becomes(Namespace), @project, type], method: :post, class: "bulk-update" do .block.issuable-sidebar-header .filter-item.inline.update-issues-btn.float-left = button_tag "Update all", class: "btn update-selected-issues btn-info", disabled: true @@ -50,4 +50,3 @@ = hidden_field_tag "update[issuable_ids]", [] = hidden_field_tag :state_event, params[:state_event] - diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml index 20847378495..588659c7e9c 100644 --- a/app/views/shared/issuable/_search_bar.html.haml +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -57,10 +57,10 @@ avatar: { lazy: true, url: '{{avatar_url}}' } #js-dropdown-assignee.filtered-search-input-dropdown-menu.dropdown-menu %ul{ data: { dropdown: true } } - %li.filter-dropdown-item{ data: { value: 'none' } } + %li.filter-dropdown-item{ data: { value: 'None' } } %button.btn.btn-link{ type: 'button' } = _('None') - %li.filter-dropdown-item{ data: { value: 'any' } } + %li.filter-dropdown-item{ data: { value: 'Any' } } %button.btn.btn-link{ type: 'button' } = _('Any') %li.divider.droplab-item-ignore @@ -73,16 +73,16 @@ avatar: { lazy: true, url: '{{avatar_url}}' } #js-dropdown-milestone.filtered-search-input-dropdown-menu.dropdown-menu %ul{ data: { dropdown: true } } - %li.filter-dropdown-item{ data: { value: 'none' } } + %li.filter-dropdown-item{ data: { value: 'None' } } %button.btn.btn-link{ type: 'button' } = _('None') - %li.filter-dropdown-item{ data: { value: 'any' } } + %li.filter-dropdown-item{ data: { value: 'Any' } } %button.btn.btn-link{ type: 'button' } = _('Any') - %li.filter-dropdown-item{ data: { value: 'upcoming' } } + %li.filter-dropdown-item{ data: { value: 'Upcoming' } } %button.btn.btn-link{ type: 'button' } = _('Upcoming') - %li.filter-dropdown-item{ data: { value: 'started' } } + %li.filter-dropdown-item{ data: { value: 'Started' } } %button.btn.btn-link{ type: 'button' } = _('Started') %li.divider.droplab-item-ignore @@ -92,10 +92,10 @@ {{title}} #js-dropdown-label.filtered-search-input-dropdown-menu.dropdown-menu %ul{ data: { dropdown: true } } - %li.filter-dropdown-item{ data: { value: 'none' } } + %li.filter-dropdown-item{ data: { value: 'None' } } %button.btn.btn-link{ type: 'button' } = _('None') - %li.filter-dropdown-item{ data: { value: 'any' } } + %li.filter-dropdown-item{ data: { value: 'Any' } } %button.btn.btn-link{ type: 'button' } = _('Any') %li.divider.droplab-item-ignore @@ -107,10 +107,10 @@ {{title}} #js-dropdown-my-reaction.filtered-search-input-dropdown-menu.dropdown-menu %ul{ data: { dropdown: true } } - %li.filter-dropdown-item{ data: { value: 'none' } } + %li.filter-dropdown-item{ data: { value: 'None' } } %button.btn.btn-link{ type: 'button' } = _('None') - %li.filter-dropdown-item{ data: { value: 'any' } } + %li.filter-dropdown-item{ data: { value: 'Any' } } %button.btn.btn-link{ type: 'button' } = _('Any') %li.divider.droplab-item-ignore diff --git a/app/views/shared/issuable/form/_merge_params.html.haml b/app/views/shared/issuable/form/_merge_params.html.haml index ca3141b2cc3..f0c4acdd07f 100644 --- a/app/views/shared/issuable/form/_merge_params.html.haml +++ b/app/views/shared/issuable/form/_merge_params.html.haml @@ -10,7 +10,7 @@ = hidden_field_tag 'merge_request[force_remove_source_branch]', '0', id: nil = check_box_tag 'merge_request[force_remove_source_branch]', '1', issuable.force_remove_source_branch?, class: 'form-check-input' = label_tag 'merge_request[force_remove_source_branch]', class: 'form-check-label' do - Remove source branch when merge request is accepted. + Delete source branch when merge request is accepted. .form-group.row .col-sm-10.offset-sm-2 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/members/_access_request_buttons.html.haml b/app/views/shared/members/_access_request_buttons.html.haml deleted file mode 100644 index ebae58f28ba..00000000000 --- a/app/views/shared/members/_access_request_buttons.html.haml +++ /dev/null @@ -1,20 +0,0 @@ -- model_name = source.model_name.to_s.downcase - -- if can?(current_user, :"destroy_#{model_name}_member", source.members.find_by(user_id: current_user.id)) # rubocop: disable CodeReuse/ActiveRecord - .project-action-button.inline - - link_text = source.is_a?(Group) ? _('Leave group') : _('Leave project') - = link_to link_text, polymorphic_path([:leave, source, :members]), - method: :delete, - data: { confirm: leave_confirmation_message(source) }, - class: 'btn' -- elsif requester = source.requesters.find_by(user_id: current_user.id) # rubocop: disable CodeReuse/ActiveRecord - .project-action-button.inline - = link_to _('Withdraw Access Request'), polymorphic_path([:leave, source, :members]), - method: :delete, - data: { confirm: remove_member_message(requester) }, - class: 'btn' -- elsif source.request_access_enabled && can?(current_user, :request_access, source) - .project-action-button.inline - = link_to _('Request Access'), polymorphic_path([:request_access, source, :members]), - method: :post, - class: 'btn' diff --git a/app/views/shared/milestones/_form_dates.html.haml b/app/views/shared/milestones/_form_dates.html.haml index 922805958a5..4de89d7c7a0 100644 --- a/app/views/shared/milestones/_form_dates.html.haml +++ b/app/views/shared/milestones/_form_dates.html.haml @@ -1,11 +1,13 @@ .col-md-6 .form-group.row - = f.label :start_date, "Start Date", class: "col-form-label col-sm-2" + .col-form-label.col-sm-2 + = f.label :start_date, "Start Date" .col-sm-10 = f.text_field :start_date, class: "datepicker form-control", placeholder: "Select start date", autocomplete: 'off' %a.inline.float-right.prepend-top-5.js-clear-start-date{ href: "#" } Clear start date .form-group.row - = f.label :due_date, "Due Date", class: "col-form-label col-sm-2" + .col-form-label.col-sm-2 + = f.label :due_date, "Due Date" .col-sm-10 = f.text_field :due_date, class: "datepicker form-control", placeholder: "Select due date", autocomplete: 'off' %a.inline.float-right.prepend-top-5.js-clear-due-date{ href: "#" } Clear due date diff --git a/app/views/shared/milestones/_search_form.html.haml b/app/views/shared/milestones/_search_form.html.haml new file mode 100644 index 00000000000..403a0224a85 --- /dev/null +++ b/app/views/shared/milestones/_search_form.html.haml @@ -0,0 +1,8 @@ += form_tag request.path, method: :get do |f| + = search_field_tag :search_title, params[:search_title], + placeholder: _('Filter by milestone name'), + class: 'form-control input-short', + spellcheck: false + = hidden_field_tag :state, params[:state] + = hidden_field_tag :sort, params[:sort] + diff --git a/app/views/shared/notifications/_button.html.haml b/app/views/shared/notifications/_button.html.haml index 30860988bbb..2ece7b7f701 100644 --- a/app/views/shared/notifications/_button.html.haml +++ b/app/views/shared/notifications/_button.html.haml @@ -1,7 +1,7 @@ - btn_class = local_assigns.fetch(:btn_class, nil) - if notification_setting - .js-notification-dropdown.notification-dropdown.project-action-button.dropdown.inline + .js-notification-dropdown.notification-dropdown.home-panel-action-button.dropdown.inline = form_for notification_setting, remote: true, html: { class: "inline notification-form" } do |f| = hidden_setting_source_input(notification_setting) = f.hidden_field :level, class: "notification_setting_level" diff --git a/app/views/projects/buttons/_notifications.html.haml b/app/views/shared/notifications/_new_button.html.haml index 745983ace7e..6d26dbebbc8 100644 --- a/app/views/projects/buttons/_notifications.html.haml +++ b/app/views/shared/notifications/_new_button.html.haml @@ -1,7 +1,7 @@ -- btn_class = local_assigns.fetch(:btn_class, "btn-xs") +- btn_class = local_assigns.fetch(:btn_class, nil) - if notification_setting - .js-notification-dropdown.notification-dropdown.project-action-button.dropdown.inline + .js-notification-dropdown.notification-dropdown.home-panel-action-button.prepend-top-default.append-right-8.dropdown.inline = form_for notification_setting, remote: true, html: { class: "inline notification-form no-label" } do |f| = hidden_setting_source_input(notification_setting) = hidden_field_tag "hide_label", true @@ -9,14 +9,14 @@ .js-notification-toggle-btns %div{ class: ("btn-group" if notification_setting.custom?) } - if notification_setting.custom? - %button.dropdown-new.btn.btn-default.has-tooltip.notifications-btn#notifications-button{ type: "button", title: _("Notification setting - %{notification_title}") % { notification_title: notification_title(notification_setting.level) }, class: "#{btn_class}", "aria-label" => _("Notification setting - %{notification_title}") % { notification_title: notification_title(notification_setting.level) }, data: { container: "body", toggle: "modal", target: "#" + notifications_menu_identifier("modal", notification_setting), display: 'static' } } + %button.dropdown-new.btn.btn-default.has-tooltip.notifications-btn#notifications-button{ type: "button", title: _("Notification setting - %{notification_title}") % { notification_title: notification_title(notification_setting.level) }, class: "#{btn_class}", "aria-label" => _("Notification setting - %{notification_title}") % { notification_title: notification_title(notification_setting.level) }, data: { container: "body", placement: 'top', toggle: "modal", target: "#" + notifications_menu_identifier("modal", notification_setting), display: 'static' } } = sprite_icon("notifications", css_class: "icon notifications-icon js-notifications-icon") %span.js-notification-loading.fa.hidden - %button.btn.dropdown-toggle{ data: { toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting), flip: "false" } } - = sprite_icon("arrow-down", css_class: "icon") + %button.btn.dropdown-toggle{ data: { toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting), flip: "false" }, class: "#{btn_class}" } + = sprite_icon("arrow-down", css_class: "icon mr-0") .sr-only Toggle dropdown - else - %button.dropdown-new.btn.btn-default.has-tooltip.notifications-btn#notifications-button{ type: "button", title: "Notification setting - #{notification_title(notification_setting.level)}", class: "#{btn_class}", "aria-label" => "Notification setting: #{notification_title(notification_setting.level)}", data: { container: "body", toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting), flip: "false" } } + %button.dropdown-new.btn.btn-default.has-tooltip.notifications-btn#notifications-button{ type: "button", title: "Notification setting - #{notification_title(notification_setting.level)}", class: "#{btn_class}", "aria-label" => "Notification setting: #{notification_title(notification_setting.level)}", data: { container: "body", placement: 'top', toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting), flip: "false" } } = sprite_icon("notifications", css_class: "icon notifications-icon js-notifications-icon") %span.js-notification-loading.fa.hidden = sprite_icon("arrow-down", css_class: "icon") diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml index fea7e17be3d..e1564d57426 100644 --- a/app/views/shared/projects/_project.html.haml +++ b/app/views/shared/projects/_project.html.haml @@ -84,7 +84,7 @@ title: _('Issues'), data: { container: 'body', placement: 'top' } do = sprite_icon('issues', size: 14, css_class: 'append-right-4') = number_with_delimiter(project.open_issues_count) - - if pipeline_status && can?(current_user, :read_cross_project) && project.pipeline_status.has_status? + - if pipeline_status && can?(current_user, :read_cross_project) && project.pipeline_status.has_status? && can?(current_user, :read_build, project) %span.icon-wrapper.pipeline-status = render_project_pipeline_status(project.pipeline_status, tooltip_placement: 'top') .updated-note 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/views/users/show.html.haml b/app/views/users/show.html.haml index 8da63a29ca6..211e3eafac6 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -74,11 +74,11 @@ = link_to @user.short_website_url, @user.full_website_url, class: 'text-link', target: '_blank', rel: 'me noopener noreferrer nofollow' - unless @user.location.blank? .profile-link-holder.middle-dot-divider - = icon('map-marker') + = sprite_icon('location', size: 16, css_class: 'vertical-align-sub') = @user.location - unless @user.organization.blank? .profile-link-holder.middle-dot-divider - = icon('briefcase') + = sprite_icon('work', size: 16, css_class: 'vertical-align-sub') = @user.organization - if @user.bio.present? diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml index 223ddc80c88..85c123c2704 100644 --- a/app/workers/all_queues.yml +++ b/app/workers/all_queues.yml @@ -45,6 +45,8 @@ - github_importer:github_import_stage_import_pull_requests - github_importer:github_import_stage_import_repository +- hashed_storage:hashed_storage_migrator + - mail_scheduler:mail_scheduler_issue_due - mail_scheduler:mail_scheduler_notification_service @@ -90,13 +92,15 @@ - object_pool:object_pool_join - object_pool:object_pool_destroy +- container_repository:delete_container_repository +- container_repository:cleanup_container_repository + - default - mailers # ActionMailer::DeliveryJob.queue_name - authorized_projects - background_migration - create_gpg_signature -- delete_container_repository - delete_merged_branches - delete_user - email_receiver @@ -129,7 +133,6 @@ - repository_fork - repository_import - repository_remove_remote -- storage_migrator - system_hook_push - update_merge_requests - upload_checksum 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/cleanup_container_repository_worker.rb b/app/workers/cleanup_container_repository_worker.rb new file mode 100644 index 00000000000..974ee8c8146 --- /dev/null +++ b/app/workers/cleanup_container_repository_worker.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +class CleanupContainerRepositoryWorker + include ApplicationWorker + include ExclusiveLeaseGuard + + queue_namespace :container_repository + + LEASE_TIMEOUT = 1.hour + + attr_reader :container_repository, :current_user + + def perform(current_user_id, container_repository_id, params) + @current_user = User.find_by_id(current_user_id) + @container_repository = ContainerRepository.find_by_id(container_repository_id) + + return unless valid? + + try_obtain_lease do + Projects::ContainerRepository::CleanupTagsService + .new(project, current_user, params) + .execute(container_repository) + end + end + + private + + def valid? + current_user && container_repository && project + end + + def project + container_repository&.project + end + + # For ExclusiveLeaseGuard concern + def lease_key + @lease_key ||= "container_repository:cleanup_tags:#{container_repository.id}" + end + + # For ExclusiveLeaseGuard concern + def lease_timeout + LEASE_TIMEOUT + end + + # For ExclusiveLeaseGuard concern + def lease_release? + # we don't allow to execute this worker + # more often than LEASE_TIMEOUT + # for given container repository + false + end +end diff --git a/app/workers/delete_container_repository_worker.rb b/app/workers/delete_container_repository_worker.rb index e8fe9d82797..42e66513ff1 100644 --- a/app/workers/delete_container_repository_worker.rb +++ b/app/workers/delete_container_repository_worker.rb @@ -4,6 +4,8 @@ class DeleteContainerRepositoryWorker include ApplicationWorker include ExclusiveLeaseGuard + queue_namespace :container_repository + LEASE_TIMEOUT = 1.hour attr_reader :container_repository diff --git a/app/workers/expire_build_artifacts_worker.rb b/app/workers/expire_build_artifacts_worker.rb index dce812d1ae2..251e95c68d5 100644 --- a/app/workers/expire_build_artifacts_worker.rb +++ b/app/workers/expire_build_artifacts_worker.rb @@ -4,8 +4,20 @@ class ExpireBuildArtifactsWorker include ApplicationWorker include CronjobQueue - # rubocop: disable CodeReuse/ActiveRecord def perform + if Feature.enabled?(:ci_new_expire_job_artifacts_service, default_enabled: true) + perform_efficient_artifacts_removal + else + perform_legacy_artifacts_removal + end + end + + def perform_efficient_artifacts_removal + Ci::DestroyExpiredJobArtifactsService.new.execute + end + + # rubocop: disable CodeReuse/ActiveRecord + def perform_legacy_artifacts_removal Rails.logger.info 'Scheduling removal of build artifacts' build_ids = Ci::Build.with_expired_artifacts.pluck(:id) 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/hashed_storage/migrator_worker.rb b/app/workers/hashed_storage/migrator_worker.rb new file mode 100644 index 00000000000..49e347d4060 --- /dev/null +++ b/app/workers/hashed_storage/migrator_worker.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module HashedStorage + class MigratorWorker + include ApplicationWorker + + queue_namespace :hashed_storage + + # @param [Integer] start initial ID of the batch + # @param [Integer] finish last ID of the batch + def perform(start, finish) + migrator = Gitlab::HashedStorage::Migrator.new + migrator.bulk_migrate(start: start, finish: finish) + end + end +end diff --git a/app/workers/namespaceless_project_destroy_worker.rb b/app/workers/namespaceless_project_destroy_worker.rb index 2965f3b1150..f6e98746055 100644 --- a/app/workers/namespaceless_project_destroy_worker.rb +++ b/app/workers/namespaceless_project_destroy_worker.rb @@ -17,7 +17,7 @@ class NamespacelessProjectDestroyWorker return end - return if project.namespace # Reject doing anything for projects that *do* have a namespace + return if project.namespace # Reject doing anything for projects that *do* have a namespace project.team.truncate diff --git a/app/workers/project_migrate_hashed_storage_worker.rb b/app/workers/project_migrate_hashed_storage_worker.rb index 4c6339f7701..1c8f313e6e9 100644 --- a/app/workers/project_migrate_hashed_storage_worker.rb +++ b/app/workers/project_migrate_hashed_storage_worker.rb @@ -4,21 +4,25 @@ class ProjectMigrateHashedStorageWorker include ApplicationWorker LEASE_TIMEOUT = 30.seconds.to_i + LEASE_KEY_SEGMENT = 'project_migrate_hashed_storage_worker'.freeze # rubocop: disable CodeReuse/ActiveRecord def perform(project_id, old_disk_path = nil) - project = Project.find_by(id: project_id) - return if project.nil? || project.pending_delete? - uuid = lease_for(project_id).try_obtain + if uuid - ::Projects::HashedStorageMigrationService.new(project, old_disk_path || project.full_path, logger: logger).execute + project = Project.find_by(id: project_id) + return if project.nil? || project.pending_delete? + + old_disk_path ||= project.disk_path + + ::Projects::HashedStorage::MigrationService.new(project, old_disk_path, logger: logger).execute else - false + return false end - rescue => ex + + ensure cancel_lease_for(project_id, uuid) if uuid - raise ex end # rubocop: enable CodeReuse/ActiveRecord @@ -29,7 +33,8 @@ class ProjectMigrateHashedStorageWorker private def lease_key(project_id) - "project_migrate_hashed_storage_worker:#{project_id}" + # we share the same lease key for both migration and rollback so they don't run simultaneously + "#{LEASE_KEY_SEGMENT}:#{project_id}" end def cancel_lease_for(project_id, uuid) 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/app/workers/storage_migrator_worker.rb b/app/workers/storage_migrator_worker.rb deleted file mode 100644 index fa76fbac55c..00000000000 --- a/app/workers/storage_migrator_worker.rb +++ /dev/null @@ -1,10 +0,0 @@ -# frozen_string_literal: true - -class StorageMigratorWorker - include ApplicationWorker - - def perform(start, finish) - migrator = Gitlab::HashedStorage::Migrator.new - migrator.bulk_migrate(start, finish) - 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/bin/secpick b/bin/secpick index 3d032f696a2..be120a304c9 100755 --- a/bin/secpick +++ b/bin/secpick @@ -57,8 +57,8 @@ module Secpick merge_request: { source_branch: source_branch, target_branch: security_branch, - title: "WIP: [#{@options[:version].tr('-', '.')}] ", - description: '/label ~security' + title: "[#{@options[:version].tr('-', '.')}] ", + description: '/label ~security ~"Merge into Security"' } } end 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/24680-support-bamboo-api-polymorphism.yml b/changelogs/unreleased/24680-support-bamboo-api-polymorphism.yml new file mode 100644 index 00000000000..5117195cd0c --- /dev/null +++ b/changelogs/unreleased/24680-support-bamboo-api-polymorphism.yml @@ -0,0 +1,5 @@ +--- +title: "Support bamboo api polymorphism" +merge_request: 24680 +author: Alex Lossent +type: fixed
\ No newline at end of file diff --git a/changelogs/unreleased/24875-label.yml b/changelogs/unreleased/24875-label.yml new file mode 100644 index 00000000000..1f9d2222edf --- /dev/null +++ b/changelogs/unreleased/24875-label.yml @@ -0,0 +1,5 @@ +--- +title: Append prioritized label before pagination +merge_request: 24815 +author: +type: fixed 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 deleted file mode 100644 index da1777827cb..00000000000 --- a/changelogs/unreleased/25341-add-what-s-new-menu-item-in-top-navigation.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Resolve Add What's new menu item in top navigation -merge_request: 23186 -author: -type: added diff --git a/changelogs/unreleased/25569-changing-wording-to-delete-when-referring-to-removing-a-branch.yml b/changelogs/unreleased/25569-changing-wording-to-delete-when-referring-to-removing-a-branch.yml new file mode 100644 index 00000000000..02a667073ca --- /dev/null +++ b/changelogs/unreleased/25569-changing-wording-to-delete-when-referring-to-removing-a-branch.yml @@ -0,0 +1,5 @@ +--- +title: Use delete instead of remove when referring to `git branch -D` +merge_request: !23966 +author: +type: changed 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/36445-better-indication-that-an-issue-has-been-moved-or-marked-as-duplicated.yml b/changelogs/unreleased/36445-better-indication-that-an-issue-has-been-moved-or-marked-as-duplicated.yml new file mode 100644 index 00000000000..70b561ccbf6 --- /dev/null +++ b/changelogs/unreleased/36445-better-indication-that-an-issue-has-been-moved-or-marked-as-duplicated.yml @@ -0,0 +1,5 @@ +--- +title: Indicate on Issue Status if an Issue was Moved +merge_request: 24470 +author: +type: added 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/40997-gitlab-pages-deploy-jobs-have-a-null-status.yml b/changelogs/unreleased/40997-gitlab-pages-deploy-jobs-have-a-null-status.yml new file mode 100644 index 00000000000..01036253151 --- /dev/null +++ b/changelogs/unreleased/40997-gitlab-pages-deploy-jobs-have-a-null-status.yml @@ -0,0 +1,5 @@ +--- +title: Fix empty labels of CI builds for gitlab-pages on pipeline page +merge_request: 24451 +author: +type: fixed 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/44698-recaptcha.yml b/changelogs/unreleased/44698-recaptcha.yml new file mode 100644 index 00000000000..e1760a6c635 --- /dev/null +++ b/changelogs/unreleased/44698-recaptcha.yml @@ -0,0 +1,5 @@ +--- +title: Prevent unload when Recaptcha is open +merge_request: 24625 +author: +type: fixed 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/45791-number-of-repositories-usage-ping.yml b/changelogs/unreleased/45791-number-of-repositories-usage-ping.yml new file mode 100644 index 00000000000..8d1f5df56ea --- /dev/null +++ b/changelogs/unreleased/45791-number-of-repositories-usage-ping.yml @@ -0,0 +1,5 @@ +--- +title: Add repositories count to usage ping data +merge_request: 24823 +author: +type: added 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/50352-sort-save.yml b/changelogs/unreleased/50352-sort-save.yml new file mode 100644 index 00000000000..cd046c8b785 --- /dev/null +++ b/changelogs/unreleased/50352-sort-save.yml @@ -0,0 +1,5 @@ +--- +title: Save issues/merge request sorting options to backend +merge_request: 24198 +author: +type: added 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/51754-admin-view-private-personal-snippets.yml b/changelogs/unreleased/51754-admin-view-private-personal-snippets.yml new file mode 100644 index 00000000000..cf3d73fce0c --- /dev/null +++ b/changelogs/unreleased/51754-admin-view-private-personal-snippets.yml @@ -0,0 +1,5 @@ +--- +title: Allow users with full private access to read private personal snippets. +merge_request: 24560 +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/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/52674-api-v4-projects-project_id-jobs-endpoint-hits-statement-timeout.yml b/changelogs/unreleased/52674-api-v4-projects-project_id-jobs-endpoint-hits-statement-timeout.yml new file mode 100644 index 00000000000..f79078c1fd9 --- /dev/null +++ b/changelogs/unreleased/52674-api-v4-projects-project_id-jobs-endpoint-hits-statement-timeout.yml @@ -0,0 +1,5 @@ +--- +title: "[API] Omit `X-Total` and `X-Total-Pages` headers when items count is more than 10,000" +merge_request: 23931 +author: +type: performance 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/53104-redesign-group-overview-ui-mvc.yml b/changelogs/unreleased/53104-redesign-group-overview-ui-mvc.yml new file mode 100644 index 00000000000..cb810b7ac7f --- /dev/null +++ b/changelogs/unreleased/53104-redesign-group-overview-ui-mvc.yml @@ -0,0 +1,5 @@ +--- +title: Refresh group overview to match project overview +merge_request: 23866 +author: +type: changed 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/53676-ip-address-of-gitlab-runner-is-wrong-in-the-runners-description.yml b/changelogs/unreleased/53676-ip-address-of-gitlab-runner-is-wrong-in-the-runners-description.yml new file mode 100644 index 00000000000..12a6509e6f7 --- /dev/null +++ b/changelogs/unreleased/53676-ip-address-of-gitlab-runner-is-wrong-in-the-runners-description.yml @@ -0,0 +1,5 @@ +--- +title: Get remote IP address of runner +merge_request: 24624 +author: +type: changed 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/53950-commit-comments-displayed-on-a-merge-request.yml b/changelogs/unreleased/53950-commit-comments-displayed-on-a-merge-request.yml new file mode 100644 index 00000000000..adaaed7f1aa --- /dev/null +++ b/changelogs/unreleased/53950-commit-comments-displayed-on-a-merge-request.yml @@ -0,0 +1,5 @@ +--- +title: Display "commented" only for commit discussions on merge requests +merge_request: 24427 +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/54213-standardize-token-value-capitalization-in-filter-bar.yml b/changelogs/unreleased/54213-standardize-token-value-capitalization-in-filter-bar.yml new file mode 100644 index 00000000000..37dea77b8d2 --- /dev/null +++ b/changelogs/unreleased/54213-standardize-token-value-capitalization-in-filter-bar.yml @@ -0,0 +1,5 @@ +--- +title: Standardize filter value capitlization in filter bar in both issues and boards pages +merge_request: 23846 +author: obahareth +type: changed diff --git a/changelogs/unreleased/54250-upstream-kubeclient-redirect-patch.yml b/changelogs/unreleased/54250-upstream-kubeclient-redirect-patch.yml new file mode 100644 index 00000000000..d1bdbccb20a --- /dev/null +++ b/changelogs/unreleased/54250-upstream-kubeclient-redirect-patch.yml @@ -0,0 +1,5 @@ +--- +title: Upgrade kubeclient to 4.2.2 and swap out monkey-patch to disallow redirects +merge_request: 24284 +author: +type: other 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/54905-milestone-search.yml b/changelogs/unreleased/54905-milestone-search.yml new file mode 100644 index 00000000000..88717242e7c --- /dev/null +++ b/changelogs/unreleased/54905-milestone-search.yml @@ -0,0 +1,5 @@ +--- +title: Adds milestone search +merge_request: 24265 +author: Jacopo Beschi @jacopo-beschi +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/55820-adds-common-name-chart-value.yml b/changelogs/unreleased/55820-adds-common-name-chart-value.yml new file mode 100644 index 00000000000..1871abbfc6b --- /dev/null +++ b/changelogs/unreleased/55820-adds-common-name-chart-value.yml @@ -0,0 +1,5 @@ +--- +title: Ensure Cert Manager works with Auto DevOps URLs greater than 64 bytes +merge_request: 24683 +author: +type: fixed 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/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/56334-runners-ipv6-address-overlaps-other-values.yml b/changelogs/unreleased/56334-runners-ipv6-address-overlaps-other-values.yml new file mode 100644 index 00000000000..8a6adef5dae --- /dev/null +++ b/changelogs/unreleased/56334-runners-ipv6-address-overlaps-other-values.yml @@ -0,0 +1,5 @@ +--- +title: Resolve Runners IPv6 address overlaps other values +merge_request: 24531 +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/56379-pipeline-stages-job-action-button-icon-is-not-aligned.yml b/changelogs/unreleased/56379-pipeline-stages-job-action-button-icon-is-not-aligned.yml new file mode 100644 index 00000000000..ec8a1d9d6ea --- /dev/null +++ b/changelogs/unreleased/56379-pipeline-stages-job-action-button-icon-is-not-aligned.yml @@ -0,0 +1,5 @@ +--- +title: Resolve Pipeline stages job action button icon is not aligned +merge_request: 24577 +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/56398-fix-cluster-installation-loading-state.yml b/changelogs/unreleased/56398-fix-cluster-installation-loading-state.yml new file mode 100644 index 00000000000..19ff408ddf4 --- /dev/null +++ b/changelogs/unreleased/56398-fix-cluster-installation-loading-state.yml @@ -0,0 +1,5 @@ +--- +title: Fix cluster installation processing spinner +merge_request: 24814 +author: +type: fixed diff --git a/changelogs/unreleased/56417-update-helm-to-2-12-2.yml b/changelogs/unreleased/56417-update-helm-to-2-12-2.yml new file mode 100644 index 00000000000..f01915c532f --- /dev/null +++ b/changelogs/unreleased/56417-update-helm-to-2-12-2.yml @@ -0,0 +1,5 @@ +--- +title: Update Helm to 2.12.2 to address Helm client vulnerability +merge_request: 24418 +author: Takuya Noguchi +type: security diff --git a/changelogs/unreleased/56424-fix-gl-form-init-tag-editing.yml b/changelogs/unreleased/56424-fix-gl-form-init-tag-editing.yml new file mode 100644 index 00000000000..b19b4d650fd --- /dev/null +++ b/changelogs/unreleased/56424-fix-gl-form-init-tag-editing.yml @@ -0,0 +1,5 @@ +--- +title: Fix form functionality for edit tag page +merge_request: 24645 +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/56556-fix-markdown-table-border.yml b/changelogs/unreleased/56556-fix-markdown-table-border.yml new file mode 100644 index 00000000000..7724f49d4e9 --- /dev/null +++ b/changelogs/unreleased/56556-fix-markdown-table-border.yml @@ -0,0 +1,5 @@ +--- +title: Fix markdown table border. +merge_request: 24601 +author: +type: fixed 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/56764-poor-ui-on-milestone-validation-error-page.yml b/changelogs/unreleased/56764-poor-ui-on-milestone-validation-error-page.yml new file mode 100644 index 00000000000..089ffd47321 --- /dev/null +++ b/changelogs/unreleased/56764-poor-ui-on-milestone-validation-error-page.yml @@ -0,0 +1,5 @@ +--- +title: Fix CSS grid on a new Project/Group Milestone +merge_request: 24614 +author: Takuya Noguchi +type: fixed 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/ab-54270-github-iid.yml b/changelogs/unreleased/ab-54270-github-iid.yml new file mode 100644 index 00000000000..1776b0aeb86 --- /dev/null +++ b/changelogs/unreleased/ab-54270-github-iid.yml @@ -0,0 +1,5 @@ +--- +title: Improve efficiency of GitHub importer by reducing amount of locks needed. +merge_request: 24102 +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/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/adrianmoisey-GITLAB_PAGES_PREDEFINED_VARIABLES.yml b/changelogs/unreleased/adrianmoisey-GITLAB_PAGES_PREDEFINED_VARIABLES.yml new file mode 100644 index 00000000000..a664c44e1d7 --- /dev/null +++ b/changelogs/unreleased/adrianmoisey-GITLAB_PAGES_PREDEFINED_VARIABLES.yml @@ -0,0 +1,5 @@ +--- +title: Add GitLab Pages predefined CI variables 'CI_PAGES_DOMAIN' and 'CI_PAGES_URL' +merge_request: 24504 +author: Adrian Moisey +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-active-record-tracing.yml b/changelogs/unreleased/an-opentracing-active-record-tracing.yml new file mode 100644 index 00000000000..59b480675ec --- /dev/null +++ b/changelogs/unreleased/an-opentracing-active-record-tracing.yml @@ -0,0 +1,5 @@ +--- +title: Adds tracing support for ActiveRecord notifications +merge_request: 24604 +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/an-opentracing-render-tracing.yml b/changelogs/unreleased/an-opentracing-render-tracing.yml new file mode 100644 index 00000000000..6ff7f1f3cf2 --- /dev/null +++ b/changelogs/unreleased/an-opentracing-render-tracing.yml @@ -0,0 +1,5 @@ +--- +title: Add OpenTracing instrumentation for Action View Render events +merge_request: 24728 +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/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/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/cluster_status_for_ugprading.yml b/changelogs/unreleased/cluster_status_for_ugprading.yml new file mode 100644 index 00000000000..ca1f8b3a786 --- /dev/null +++ b/changelogs/unreleased/cluster_status_for_ugprading.yml @@ -0,0 +1,5 @@ +--- +title: Expose version for each application in cluster_status JSON endpoint +merge_request: 24791 +author: +type: other diff --git a/changelogs/unreleased/container-repository-cleanup-api.yml b/changelogs/unreleased/container-repository-cleanup-api.yml new file mode 100644 index 00000000000..c2b23a9add0 --- /dev/null +++ b/changelogs/unreleased/container-repository-cleanup-api.yml @@ -0,0 +1,5 @@ +--- +title: Add Container Registry API with cleanup function +merge_request: 24303 +author: +type: added 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-copy-suggestion-as-gfm.yml b/changelogs/unreleased/dm-copy-suggestion-as-gfm.yml new file mode 100644 index 00000000000..96115e6ade1 --- /dev/null +++ b/changelogs/unreleased/dm-copy-suggestion-as-gfm.yml @@ -0,0 +1,5 @@ +--- +title: Allow suggestions to be copied and pasted as GFM +merge_request: +author: +type: fixed 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-push-mirror-GitLab-GitHub.yml b/changelogs/unreleased/docs-push-mirror-GitLab-GitHub.yml new file mode 100644 index 00000000000..4539a9b7985 --- /dev/null +++ b/changelogs/unreleased/docs-push-mirror-GitLab-GitHub.yml @@ -0,0 +1,5 @@ +--- +title: Updated docs for fields in pushing mirror from GitLab to GitHub +merge_request: 24566 +author: Joseph Yu +type: other 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/expire-job-artifacts-worker.yml b/changelogs/unreleased/expire-job-artifacts-worker.yml new file mode 100644 index 00000000000..cda6e9ff497 --- /dev/null +++ b/changelogs/unreleased/expire-job-artifacts-worker.yml @@ -0,0 +1,5 @@ +--- +title: Efficiently remove expired artifacts in `ExpireBuildArtifactsWorker` +merge_request: 24450 +author: +type: performance 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-39759-new-project-icon-vertical-align.yml b/changelogs/unreleased/fix-39759-new-project-icon-vertical-align.yml new file mode 100644 index 00000000000..3d87807dbc1 --- /dev/null +++ b/changelogs/unreleased/fix-39759-new-project-icon-vertical-align.yml @@ -0,0 +1,5 @@ +--- +title: Adjust vertical alignment for project visibility icons +merge_request: 24511 +author: Martin Hobert +type: fixed 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-49388.yml b/changelogs/unreleased/fix-49388.yml new file mode 100644 index 00000000000..f8b5e3e1943 --- /dev/null +++ b/changelogs/unreleased/fix-49388.yml @@ -0,0 +1,5 @@ +--- +title: Update metrics environment dropdown to show complete option set +merge_request: 24441 +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-55956-oversized-dropdown-button-custom-notifications.yml b/changelogs/unreleased/fix-55956-oversized-dropdown-button-custom-notifications.yml new file mode 100644 index 00000000000..e33699a2112 --- /dev/null +++ b/changelogs/unreleased/fix-55956-oversized-dropdown-button-custom-notifications.yml @@ -0,0 +1,5 @@ +--- +title: Fixed oversized custom project notification selector dropdown +merge_request: 24557 +author: +type: fixed 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-clusters.yml b/changelogs/unreleased/gt-externalize-app-views-clusters.yml new file mode 100644 index 00000000000..6d2284ead37 --- /dev/null +++ b/changelogs/unreleased/gt-externalize-app-views-clusters.yml @@ -0,0 +1,5 @@ +--- +title: Externalize strings from `/app/views/clusters` +merge_request: 24666 +author: George Tsiolis +type: other diff --git a/changelogs/unreleased/gt-externalize-app-views-projects-ci.yml b/changelogs/unreleased/gt-externalize-app-views-projects-ci.yml new file mode 100644 index 00000000000..ecc878ab892 --- /dev/null +++ b/changelogs/unreleased/gt-externalize-app-views-projects-ci.yml @@ -0,0 +1,5 @@ +--- +title: Externalize strings from `/app/views/projects/ci` +merge_request: 24617 +author: George Tsiolis +type: other diff --git a/changelogs/unreleased/gt-externalize-app-views-projects-milestones.yml b/changelogs/unreleased/gt-externalize-app-views-projects-milestones.yml new file mode 100644 index 00000000000..56aaac812bb --- /dev/null +++ b/changelogs/unreleased/gt-externalize-app-views-projects-milestones.yml @@ -0,0 +1,5 @@ +--- +title: Externalize strings from `/app/views/projects/milestones` +merge_request: 24726 +author: George Tsiolis +type: other diff --git a/changelogs/unreleased/gt-externalize-app-views-projects-pages_domains.yml b/changelogs/unreleased/gt-externalize-app-views-projects-pages_domains.yml new file mode 100644 index 00000000000..f60776a2ed8 --- /dev/null +++ b/changelogs/unreleased/gt-externalize-app-views-projects-pages_domains.yml @@ -0,0 +1,5 @@ +--- +title: Externalize strings from `/app/views/projects/pages_domains` +merge_request: 24723 +author: George Tsiolis +type: other diff --git a/changelogs/unreleased/gt-externalize-app-views-sent_notifications.yml b/changelogs/unreleased/gt-externalize-app-views-sent_notifications.yml new file mode 100644 index 00000000000..e77b5376fa8 --- /dev/null +++ b/changelogs/unreleased/gt-externalize-app-views-sent_notifications.yml @@ -0,0 +1,5 @@ +--- +title: Externalize strings from `/app/views/sent_notifications` +merge_request: 24576 +author: George Tsiolis +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-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/hnk-master-patch-61932.yml b/changelogs/unreleased/hnk-master-patch-61932.yml new file mode 100644 index 00000000000..8cc9d0057a9 --- /dev/null +++ b/changelogs/unreleased/hnk-master-patch-61932.yml @@ -0,0 +1,5 @@ +--- +title: Update runner admin page to make description field larger +merge_request: 23593 +author: Sascha Reynolds +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/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-adjusts-suggestions-unable-to-be-applied.yml b/changelogs/unreleased/osw-adjusts-suggestions-unable-to-be-applied.yml new file mode 100644 index 00000000000..3ba62b92413 --- /dev/null +++ b/changelogs/unreleased/osw-adjusts-suggestions-unable-to-be-applied.yml @@ -0,0 +1,5 @@ +--- +title: Adjusts suggestions unable to be applied +merge_request: 24603 +author: +type: fixed 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/osw-fix-quick-suggestion-application-being-reverted.yml b/changelogs/unreleased/osw-fix-quick-suggestion-application-being-reverted.yml deleted file mode 100644 index 1f80d7535a5..00000000000 --- a/changelogs/unreleased/osw-fix-quick-suggestion-application-being-reverted.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Adjust applied suggestion reverting previous changes -merge_request: 24250 -author: -type: fixed diff --git a/changelogs/unreleased/patch-38.yml b/changelogs/unreleased/patch-38.yml new file mode 100644 index 00000000000..9179fc6846e --- /dev/null +++ b/changelogs/unreleased/patch-38.yml @@ -0,0 +1,5 @@ +--- +title: fix display comment avatars issue in IE 11 +merge_request: 24777 +author: Gokhan Apaydin +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/refactor-56366-extract-resolve-discussion-button.yml b/changelogs/unreleased/refactor-56366-extract-resolve-discussion-button.yml new file mode 100644 index 00000000000..98859e8aa07 --- /dev/null +++ b/changelogs/unreleased/refactor-56366-extract-resolve-discussion-button.yml @@ -0,0 +1,5 @@ +--- +title: Refactored NoteableDiscussion by extracting ResolveDiscussionButton +merge_request: 24505 +author: Martin Hobert +type: other diff --git a/changelogs/unreleased/refactor-56369-extract-jump-to-next-discussion-button.yml b/changelogs/unreleased/refactor-56369-extract-jump-to-next-discussion-button.yml new file mode 100644 index 00000000000..9a0d16c2d70 --- /dev/null +++ b/changelogs/unreleased/refactor-56369-extract-jump-to-next-discussion-button.yml @@ -0,0 +1,5 @@ +--- +title: Extracted JumpToNextDiscussionButton to its own component +author: Martin Hobert +merge_request: 24506 +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-diff-coloring.yml b/changelogs/unreleased/remove-diff-coloring.yml new file mode 100644 index 00000000000..1ee1b525c35 --- /dev/null +++ b/changelogs/unreleased/remove-diff-coloring.yml @@ -0,0 +1,5 @@ +--- +title: 'remove red/green colors from diff view of no-color syntax theme' +merge_request: 24582 +author: khm +type: changed 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-22076-sanitize-url-in-names.yml b/changelogs/unreleased/security-22076-sanitize-url-in-names.yml new file mode 100644 index 00000000000..4e0ad4dd4c4 --- /dev/null +++ b/changelogs/unreleased/security-22076-sanitize-url-in-names.yml @@ -0,0 +1,6 @@ +--- +title: Sanitize user full name to clean up any URL to prevent mail clients from auto-linking + URLs +merge_request: 2793 +author: +type: security 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-55320-stored-xss-in-user-status.yml b/changelogs/unreleased/security-55320-stored-xss-in-user-status.yml new file mode 100644 index 00000000000..8ea9ae0ccdf --- /dev/null +++ b/changelogs/unreleased/security-55320-stored-xss-in-user-status.yml @@ -0,0 +1,5 @@ +--- +title: Use sanitized user status message for user popover +merge_request: +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-stored-xss-via-katex.yml b/changelogs/unreleased/security-stored-xss-via-katex.yml new file mode 100644 index 00000000000..a71ae1123f2 --- /dev/null +++ b/changelogs/unreleased/security-stored-xss-via-katex.yml @@ -0,0 +1,5 @@ +--- +title: Fixed XSS content in KaTex links +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-disable-nil-user-id-identity-validation.yml b/changelogs/unreleased/sh-disable-nil-user-id-identity-validation.yml new file mode 100644 index 00000000000..5af3bdce51b --- /dev/null +++ b/changelogs/unreleased/sh-disable-nil-user-id-identity-validation.yml @@ -0,0 +1,5 @@ +--- +title: Fix failed LDAP logins when nil user_id present +merge_request: 24749 +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-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-gon-helper-avatar.yml b/changelogs/unreleased/sh-fix-gon-helper-avatar.yml deleted file mode 100644 index c83273608ad..00000000000 --- a/changelogs/unreleased/sh-fix-gon-helper-avatar.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix no avatar not showing in user selection box -merge_request: 24346 -author: -type: fixed diff --git a/changelogs/unreleased/sh-fix-import-redirect-vulnerability.yml b/changelogs/unreleased/sh-fix-import-redirect-vulnerability.yml new file mode 100644 index 00000000000..addf327b69d --- /dev/null +++ b/changelogs/unreleased/sh-fix-import-redirect-vulnerability.yml @@ -0,0 +1,5 @@ +--- +title: Alias GitHub and BitBucket OAuth2 callback URLs +merge_request: +author: +type: security 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-issue-9357.yml b/changelogs/unreleased/sh-fix-issue-9357.yml new file mode 100644 index 00000000000..756cd6047b8 --- /dev/null +++ b/changelogs/unreleased/sh-fix-issue-9357.yml @@ -0,0 +1,5 @@ +--- +title: Fix 500 errors with legacy appearance logos +merge_request: 24615 +author: +type: fixed diff --git a/changelogs/unreleased/sh-fix-pages-zip-constant.yml b/changelogs/unreleased/sh-fix-pages-zip-constant.yml new file mode 100644 index 00000000000..fcd8aa45825 --- /dev/null +++ b/changelogs/unreleased/sh-fix-pages-zip-constant.yml @@ -0,0 +1,5 @@ +--- +title: Fix uninitialized constant with GitLab Pages +merge_request: +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-fix-upload-snippets-with-relative-url-root.yml b/changelogs/unreleased/sh-fix-upload-snippets-with-relative-url-root.yml new file mode 100644 index 00000000000..8bc1e4b4f8a --- /dev/null +++ b/changelogs/unreleased/sh-fix-upload-snippets-with-relative-url-root.yml @@ -0,0 +1,5 @@ +--- +title: Fix 404s for snippet uploads when relative URL root used +merge_request: 24588 +author: +type: fixed diff --git a/changelogs/unreleased/sh-issue-53419-fix.yml b/changelogs/unreleased/sh-issue-53419-fix.yml new file mode 100644 index 00000000000..ab8b65857e2 --- /dev/null +++ b/changelogs/unreleased/sh-issue-53419-fix.yml @@ -0,0 +1,5 @@ +--- +title: Fix Bitbucket Server import not allowing personal projects +merge_request: 23601 +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-remove-bitbucket-mirror-constant.yml b/changelogs/unreleased/sh-remove-bitbucket-mirror-constant.yml new file mode 100644 index 00000000000..8c0b000220f --- /dev/null +++ b/changelogs/unreleased/sh-remove-bitbucket-mirror-constant.yml @@ -0,0 +1,5 @@ +--- +title: Fix import handling errors in Bitbucket Server importer +merge_request: 24499 +author: +type: fixed 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/test-permissions.yml b/changelogs/unreleased/test-permissions.yml new file mode 100644 index 00000000000..cfb69fdcb1e --- /dev/null +++ b/changelogs/unreleased/test-permissions.yml @@ -0,0 +1,5 @@ +--- +title: Disallows unauthorized users from accessing the pipelines section. +merge_request: +author: +type: security 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-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/update-spriteicon-from-icon-on-profile.yml b/changelogs/unreleased/update-spriteicon-from-icon-on-profile.yml new file mode 100644 index 00000000000..32259bfacd4 --- /dev/null +++ b/changelogs/unreleased/update-spriteicon-from-icon-on-profile.yml @@ -0,0 +1,5 @@ +--- +title: Update to GitLab SVG icon from Font Awesome in profile for location and work +merge_request: 24671 +author: Yoginth +type: changed 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.ru b/config.ru index a5d055334dd..5cd79870d54 100644 --- a/config.ru +++ b/config.ru @@ -19,7 +19,7 @@ if defined?(Unicorn) Unicorn::StreamInput.send(:public, :eof?) # rubocop:disable GitlabSecurity/PublicSend end -require ::File.expand_path('../config/environment', __FILE__) +require ::File.expand_path('../config/environment', __FILE__) warmup do |app| client = Rack::MockRequest.new(app) 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/8_devise.rb b/config/initializers/8_devise.rb index 67eabb0b4fc..4683b02f300 100644 --- a/config/initializers/8_devise.rb +++ b/config/initializers/8_devise.rb @@ -178,7 +178,7 @@ Devise.setup do |config| # Configure the default scope given to Warden. By default it's the first # devise role declared in your routes (usually :user). - config.default_scope = :user # now have an :email scope as well, so set the default + config.default_scope = :user # now have an :email scope as well, so set the default # Configure sign_out behavior. # Sign_out action can be scoped (i.e. /users/sign_out affects only :user scope). diff --git a/config/initializers/doorkeeper_openid_connect.rb b/config/initializers/doorkeeper_openid_connect.rb index ae5d834a02c..e97c0fcbd6b 100644 --- a/config/initializers/doorkeeper_openid_connect.rb +++ b/config/initializers/doorkeeper_openid_connect.rb @@ -31,7 +31,7 @@ Doorkeeper::OpenidConnect.configure do o.claim(:name) { |user| user.name } o.claim(:nickname) { |user| user.username } - o.claim(:email) { |user| user.public_email } + o.claim(:email) { |user| user.public_email } o.claim(:email_verified) { |user| true if user.public_email? } o.claim(:website) { |user| user.full_website_url if user.website_url? } o.claim(:profile) { |user| Gitlab::Routing.url_helpers.user_url user } diff --git a/config/initializers/kaminari_active_record_relation_methods_with_limit.rb b/config/initializers/kaminari_active_record_relation_methods_with_limit.rb new file mode 100644 index 00000000000..cc20b83b234 --- /dev/null +++ b/config/initializers/kaminari_active_record_relation_methods_with_limit.rb @@ -0,0 +1,41 @@ +module Kaminari + # Active Record specific page scope methods implementations + module ActiveRecordRelationMethodsWithLimit + MAX_COUNT_LIMIT = 10_000 + + # This is a modified version of + # https://github.com/kaminari/kaminari/blob/c5186f5d9b7f23299d115408e62047447fd3189d/kaminari-activerecord/lib/kaminari/activerecord/active_record_relation_methods.rb#L17-L41 + # that limit the COUNT query to 10,000 to avoid query timeouts. + # rubocop: disable Gitlab/ModuleWithInstanceVariables + def total_count_with_limit(column_name = :all, _options = nil) #:nodoc: + return @total_count if defined?(@total_count) && @total_count + + # There are some cases that total count can be deduced from loaded records + if loaded? + # Total count has to be 0 if loaded records are 0 + return @total_count = 0 if (current_page == 1) && @records.empty? + # Total count is calculable at the last page + return @total_count = (current_page - 1) * limit_value + @records.length if @records.any? && (@records.length < limit_value) + end + + # #count overrides the #select which could include generated columns referenced in #order, so skip #order here, where it's irrelevant to the result anyway + c = except(:offset, :limit, :order) + # Remove includes only if they are irrelevant + c = c.except(:includes) unless references_eager_loaded_tables? + # .group returns an OrderedHash that responds to #count + # The following line was modified from `c = c.count(:all)` + c = c.limit(MAX_COUNT_LIMIT + 1).count(column_name) + @total_count = + if c.is_a?(Hash) || c.is_a?(ActiveSupport::OrderedHash) + c.count + elsif c.respond_to? :count + c.count(column_name) + else + c + end + end + # rubocop: enable Gitlab/ModuleWithInstanceVariables + + Kaminari::ActiveRecordRelationMethods.prepend(self) + end +end diff --git a/config/initializers/kubeclient.rb b/config/initializers/kubeclient.rb deleted file mode 100644 index f8fe1156aaa..00000000000 --- a/config/initializers/kubeclient.rb +++ /dev/null @@ -1,22 +0,0 @@ -class Kubeclient::Client - # Monkey patch to set `max_redirects: 0`, so that kubeclient - # does not follow redirects and expose internal services. - # See https://gitlab.com/gitlab-org/gitlab-ce/issues/53158 - def create_rest_client(path = nil) - path ||= @api_endpoint.path - options = { - ssl_ca_file: @ssl_options[:ca_file], - ssl_cert_store: @ssl_options[:cert_store], - verify_ssl: @ssl_options[:verify_ssl], - ssl_client_cert: @ssl_options[:client_cert], - ssl_client_key: @ssl_options[:client_key], - proxy: @http_proxy_uri, - user: @auth_options[:username], - password: @auth_options[:password], - open_timeout: @timeouts[:open], - read_timeout: @timeouts[:read], - max_redirects: 0 - } - RestClient::Resource.new(@api_endpoint.merge(path).to_s, options) - end -end 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/postgresql_cte.rb b/config/initializers/postgresql_cte.rb index 38a9cd68d57..56689bc8e74 100644 --- a/config/initializers/postgresql_cte.rb +++ b/config/initializers/postgresql_cte.rb @@ -108,7 +108,7 @@ module ActiveRecord when String with_value when Hash - with_value.map do |name, expression| + with_value.map do |name, expression| case expression when String select = Arel::Nodes::SqlLiteral.new "(#{expression})" 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..ddd91150c90 --- /dev/null +++ b/config/initializers/tracing.rb @@ -0,0 +1,37 @@ +# 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 + + # Instrument Rails + Gitlab::Tracing::Rails::ActiveRecordSubscriber.instrument + Gitlab::Tracing::Rails::ActionViewSubscriber.instrument + + # 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/routes/import.rb b/config/routes/import.rb index 3998d977c81..69df82611f2 100644 --- a/config/routes/import.rb +++ b/config/routes/import.rb @@ -1,3 +1,12 @@ +# Alias import callbacks under the /users/auth endpoint so that +# the OAuth2 callback URL can be restricted under http://example.com/users/auth +# instead of http://example.com. +Devise.omniauth_providers.each do |provider| + next if provider == 'ldapmain' + + get "/users/auth/-/import/#{provider}/callback", to: "import/#{provider}#callback", as: "users_import_#{provider}_callback" +end + namespace :import do resource :github, only: [:create, :new], controller: :github do post :personal_access_token diff --git a/config/routes/project.rb b/config/routes/project.rb index d9afb4e7bc8..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 diff --git a/config/routes/repository.rb b/config/routes/repository.rb index 96975759709..f5201b9ddbb 100644 --- a/config/routes/repository.rb +++ b/config/routes/repository.rb @@ -62,7 +62,7 @@ scope format: false do resources :protected_tags, only: [:index, :show, :create, :update, :destroy] end - scope constraints: { id: /.+/ } do + scope constraints: { id: /.+/ } do scope controller: :blob do get '/new/*id', action: :new, as: :new_blob post '/create/*id', action: :create, as: :create_blob diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml index 3e8c218052d..1e094c03171 100644 --- a/config/sidekiq_queues.yml +++ b/config/sidekiq_queues.yml @@ -47,7 +47,6 @@ - [project_service, 1] - [delete_user, 1] - [todos_destroyer, 1] - - [delete_container_repository, 1] - [delete_merged_branches, 1] - [authorized_projects, 1] - [expire_build_instance_artifacts, 1] @@ -69,7 +68,7 @@ - [background_migration, 1] - [gcp_cluster, 1] - [project_migrate_hashed_storage, 1] - - [storage_migrator, 1] + - [hashed_storage, 1] - [pages_domain_verification, 1] - [object_storage_upload, 1] - [object_storage, 1] @@ -81,6 +80,7 @@ - [delete_diff_files, 1] - [detect_repository_languages, 1] - [auto_devops, 2] + - [container_repository, 1] - [object_pool, 1] - [repository_cleanup, 1] - [delete_stored_files, 1] diff --git a/config/webpack.config.js b/config/webpack.config.js index b9044e13f50..fdf179b007a 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -94,6 +94,9 @@ module.exports = { vendor: path.join(ROOT_PATH, 'vendor/assets/javascripts'), vue$: 'vue/dist/vue.esm.js', spec: path.join(ROOT_PATH, 'spec/javascripts'), + + // the following resolves files which are different between CE and EE + ee_else_ce: path.join(ROOT_PATH, 'app/assets/javascripts'), }, }, 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/migrate/20170927095921_add_ci_builds_index_for_jobscontroller.rb b/db/migrate/20170927095921_add_ci_builds_index_for_jobscontroller.rb index 85aa78006db..3ee9c959fca 100644 --- a/db/migrate/20170927095921_add_ci_builds_index_for_jobscontroller.rb +++ b/db/migrate/20170927095921_add_ci_builds_index_for_jobscontroller.rb @@ -28,7 +28,7 @@ class AddCiBuildsIndexForJobscontroller < ActiveRecord::Migration[4.2] disable_ddl_transaction! def up - add_concurrent_index :ci_builds, [:project_id, :id] unless index_exists? :ci_builds, [:project_id, :id] + add_concurrent_index :ci_builds, [:project_id, :id] unless index_exists? :ci_builds, [:project_id, :id] remove_concurrent_index :ci_builds, :project_id if index_exists? :ci_builds, :project_id 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/migrate/20190116234221_add_sorting_fields_to_user_preference.rb b/db/migrate/20190116234221_add_sorting_fields_to_user_preference.rb new file mode 100644 index 00000000000..7bf581fe9b0 --- /dev/null +++ b/db/migrate/20190116234221_add_sorting_fields_to_user_preference.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddSortingFieldsToUserPreference < ActiveRecord::Migration[5.0] + include Gitlab::Database::MigrationHelpers + + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + + def up + add_column :user_preferences, :issues_sort, :string + add_column :user_preferences, :merge_requests_sort, :string + end + + def down + remove_column :user_preferences, :issues_sort + remove_column :user_preferences, :merge_requests_sort + 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/post_migrate/20170317162059_update_upload_paths_to_system.rb b/db/post_migrate/20170317162059_update_upload_paths_to_system.rb index a5a6f043e10..99cdca465e2 100644 --- a/db/post_migrate/20170317162059_update_upload_paths_to_system.rb +++ b/db/post_migrate/20170317162059_update_upload_paths_to_system.rb @@ -48,7 +48,7 @@ class UpdateUploadPathsToSystem < ActiveRecord::Migration[4.2] end def new_upload_dir - File.join(base_directory, "-", "system") + File.join(base_directory, "-", "system") end def arel_table diff --git a/db/post_migrate/20181219130552_update_project_import_visibility_level.rb b/db/post_migrate/20181219130552_update_project_import_visibility_level.rb new file mode 100644 index 00000000000..6209de88b31 --- /dev/null +++ b/db/post_migrate/20181219130552_update_project_import_visibility_level.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +class UpdateProjectImportVisibilityLevel < ActiveRecord::Migration[5.0] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + BATCH_SIZE = 100 + + PRIVATE = 0 + INTERNAL = 10 + + disable_ddl_transaction! + + class Namespace < ActiveRecord::Base + self.table_name = 'namespaces' + end + + class Project < ActiveRecord::Base + include EachBatch + + belongs_to :namespace + + IMPORT_TYPE = 'gitlab_project' + + scope :with_group_visibility, ->(visibility) do + joins(:namespace) + .where(namespaces: { type: 'Group', visibility_level: visibility }) + .where(import_type: IMPORT_TYPE) + .where('projects.visibility_level > namespaces.visibility_level') + end + + self.table_name = 'projects' + end + + def up + # Update project's visibility to be the same as the group + # if it is more restrictive than `PUBLIC`. + update_projects_visibility(PRIVATE) + update_projects_visibility(INTERNAL) + end + + def down + # no-op: unrecoverable data migration + end + + private + + def update_projects_visibility(visibility) + say_with_time("Updating project visibility to #{visibility} on #{Project::IMPORT_TYPE} imports.") do + Project.with_group_visibility(visibility).select(:id).each_batch(of: BATCH_SIZE) do |batch, _index| + batch_sql = Gitlab::Database.mysql? ? batch.pluck(:id).join(', ') : batch.select(:id).to_sql + + say("Updating #{batch.size} items.", true) + + execute("UPDATE projects SET visibility_level = '#{visibility}' WHERE id IN (#{batch_sql})") + end + end + end +end diff --git a/db/post_migrate/20190102152410_delete_inconsistent_internal_id_records2.rb b/db/post_migrate/20190102152410_delete_inconsistent_internal_id_records2.rb new file mode 100644 index 00000000000..ddcddcf72a3 --- /dev/null +++ b/db/post_migrate/20190102152410_delete_inconsistent_internal_id_records2.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true +class DeleteInconsistentInternalIdRecords2 < ActiveRecord::Migration[5.0] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + # This migration cleans up any inconsistent records in internal_ids. + # + # That is, it deletes records that track a `last_value` that is + # smaller than the maximum internal id (usually `iid`) found in + # the corresponding model records. + + def up + disable_statement_timeout do + delete_internal_id_records('milestones', 'project_id') + delete_internal_id_records('milestones', 'namespace_id', 'group_id') + end + end + + class InternalId < ActiveRecord::Base + self.table_name = 'internal_ids' + enum usage: { issues: 0, merge_requests: 1, deployments: 2, milestones: 3, epics: 4, ci_pipelines: 5 } + end + + private + + def delete_internal_id_records(base_table, scope_column_name, base_scope_column_name = scope_column_name) + sql = <<~SQL + SELECT id FROM ( -- workaround for MySQL + SELECT internal_ids.id FROM ( + SELECT #{base_scope_column_name} AS #{scope_column_name}, max(iid) as maximum_iid from #{base_table} GROUP BY #{scope_column_name} + ) maxima JOIN internal_ids USING (#{scope_column_name}) + WHERE internal_ids.usage=#{InternalId.usages.fetch(base_table)} AND maxima.maximum_iid > internal_ids.last_value + ) internal_ids + SQL + + InternalId.where("id IN (#{sql})").tap do |ids| # rubocop:disable GitlabSecurity/SqlInjection + say "Deleting internal_id records for #{base_table}: #{ids.map { |i| [i.project_id, i.last_value] }}" unless ids.empty? + end.delete_all + end +end diff --git a/db/post_migrate/20190115054215_migrate_delete_container_repository_worker.rb b/db/post_migrate/20190115054215_migrate_delete_container_repository_worker.rb new file mode 100644 index 00000000000..4fcee326b7e --- /dev/null +++ b/db/post_migrate/20190115054215_migrate_delete_container_repository_worker.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class MigrateDeleteContainerRepositoryWorker < ActiveRecord::Migration[5.0] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def up + sidekiq_queue_migrate('delete_container_repository', to: 'container_repository:delete_container_repository') + end + + def down + sidekiq_queue_migrate('container_repository:delete_container_repository', to: 'delete_container_repository') + end +end diff --git a/db/post_migrate/20190124200344_migrate_storage_migrator_sidekiq_queue.rb b/db/post_migrate/20190124200344_migrate_storage_migrator_sidekiq_queue.rb new file mode 100644 index 00000000000..193bd571831 --- /dev/null +++ b/db/post_migrate/20190124200344_migrate_storage_migrator_sidekiq_queue.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class MigrateStorageMigratorSidekiqQueue < ActiveRecord::Migration[5.0] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def up + sidekiq_queue_migrate 'storage_migrator', to: 'hashed_storage:hashed_storage_migrator' + end + + def down + sidekiq_queue_migrate 'hashed_storage:hashed_storage_migrator', to: 'storage_migrator' + end +end diff --git a/db/schema.rb b/db/schema.rb index 87826881d58..7c1733becb9 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: 20190124200344) 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 @@ -2145,6 +2146,8 @@ ActiveRecord::Schema.define(version: 20190103140724) do t.integer "merge_request_notes_filter", limit: 2, default: 0, null: false t.datetime_with_timezone "created_at", null: false t.datetime_with_timezone "updated_at", null: false + t.string "issues_sort" + t.string "merge_requests_sort" t.index ["user_id"], name: "index_user_preferences_on_user_id", unique: true, using: :btree end diff --git a/doc/README.md b/doc/README.md index b15c3a63d92..1a0359f9e2a 100644 --- a/doc/README.md +++ b/doc/README.md @@ -52,6 +52,11 @@ GitLab provides solutions for [all the stages of the DevOps lifecycle](https://a ![DevOps Stages](img/devops-stages.png) +GitLab is like a top-of-the-line kitchen for making software. As the executive +chef, you decide what software you want serve. Using your recipe, GitLab handles +all the prep work, cooking, and delivery, so you can turn around orders faster +than ever. + The following sections provide links to documentation for each DevOps stage: | DevOps Stage | Documentation for | diff --git a/doc/administration/auth/ldap.md b/doc/administration/auth/ldap.md index 54ded25291a..0ac73c55580 100644 --- a/doc/administration/auth/ldap.md +++ b/doc/administration/auth/ldap.md @@ -335,7 +335,7 @@ group, you can use the following syntax: ``` Find more information about this "LDAP_MATCHING_RULE_IN_CHAIN" filter at -https://msdn.microsoft.com/en-us/library/aa746475(v=vs.85).aspx. Support for +<https://docs.microsoft.com/en-us/windows/desktop/ADSI/search-filter-syntax>. Support for nested members in the user filter should not be confused with [group sync nested groups support (EE only)](https://docs.gitlab.com/ee/administration/auth/ldap-ee.html#supported-ldap-group-types-attributes). diff --git a/doc/administration/container_registry.md b/doc/administration/container_registry.md index 5b7a61ef8ff..db0b3e1270c 100644 --- a/doc/administration/container_registry.md +++ b/doc/administration/container_registry.md @@ -11,7 +11,7 @@ With the Container Registry integrated into GitLab, every project can have its own space to store its Docker images. You can read more about the Container Registry at -https://docs.docker.com/registry/introduction/. +<https://docs.docker.com/registry/introduction/>. ## Enable the Container Registry @@ -378,7 +378,7 @@ Read more about the individual driver's config options in the > **Warning** GitLab will not backup Docker images that are not stored on the filesystem. Remember to enable backups with your object storage provider if desired. -> +> > **Important** Enabling storage driver other than `filesystem` would mean that your Docker client needs to be able to access the storage backend directly. So you must use an address that resolves and is accessible outside GitLab server. @@ -606,15 +606,15 @@ information in [issue 18239][ce-18239]. ## Troubleshooting -When using AWS S3 with the GitLab registry, an error may occur when pushing +When using AWS S3 with the GitLab registry, an error may occur when pushing large images. Look in the Registry log for the following error: ``` -level=error msg="response completed with error" err.code=unknown err.detail="unexpected EOF" err.message="unknown error" +level=error msg="response completed with error" err.code=unknown err.detail="unexpected EOF" err.message="unknown error" ``` -To resolve the error specify a `chunksize` value in the Registry configuration. -Start with a value between `25000000` (25MB) and `50000000` (50MB). +To resolve the error specify a `chunksize` value in the Registry configuration. +Start with a value between `25000000` (25MB) and `50000000` (50MB). **For Omnibus installations** diff --git a/doc/administration/git_protocol.md b/doc/administration/git_protocol.md index 341a00009e5..11b2adeeeb8 100644 --- a/doc/administration/git_protocol.md +++ b/doc/administration/git_protocol.md @@ -5,6 +5,13 @@ description: "Set and configure Git protocol v2" # Configuring Git Protocol v2 > [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/46555) in GitLab 11.4. +> [Temporarily disabled](https://gitlab.com/gitlab-org/gitlab-ce/issues/55769) in GitLab 11.5.8, 11.6.6, 11.7.1, and 11.8+ + +NOTE: **Note:** +Git protocol v2 support has been [temporarily disabled](https://gitlab.com/gitlab-org/gitlab-ce/issues/55769), +as a feature used to hide certain internal references does not function when it +is enabled, and this has a security impact. Once this problem has been resolved, +protocol v2 support will be re-enabled. Git protocol v2 improves the v1 wire protocol in several ways and is enabled by default in GitLab for HTTP requests. In order to enable SSH, diff --git a/doc/administration/gitaly/index.md b/doc/administration/gitaly/index.md index 05c1923f0cb..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. diff --git a/doc/administration/index.md b/doc/administration/index.md index 89132cd95f0..0b673d61139 100644 --- a/doc/administration/index.md +++ b/doc/administration/index.md @@ -41,13 +41,14 @@ Learn how to install, configure, update, and maintain your GitLab instance. - [Polling](polling.md): Configure how often the GitLab UI polls for updates. - [GitLab Pages configuration](pages/index.md): Enable and configure GitLab Pages. - [GitLab Pages configuration for GitLab source installations](pages/source.md): Enable and configure GitLab Pages on -[source installations](../install/installation.md#installation-from-source). + [source installations](../install/installation.md#installation-from-source). - [Environment variables](environment_variables.md): Supported environment variables that can be used to override their defaults values in order to configure GitLab. - [Plugins](plugins.md): With custom plugins, GitLab administrators can introduce custom integrations without modifying GitLab's source code. - [Enforcing Terms of Service](../user/admin_area/settings/terms.md) - [Third party offers](../user/admin_area/settings/third_party_offers.md) - [Compliance](compliance.md): A collection of features from across the application that you may configure to help ensure that your GitLab instance and DevOps workflow meet compliance standards. - [Diff limits](../user/admin_area/diff_limits.md): Configure the diff rendering size limits of branch comparison pages. +- [Broadcast Messages](../user/admin_area/broadcast_messages.md): Send messages to GitLab users through the UI. #### Customizing GitLab's appearance @@ -80,7 +81,7 @@ Learn how to install, configure, update, and maintain your GitLab instance. - [Mattermost](https://docs.gitlab.com/omnibus/gitlab-mattermost/): Integrate with [Mattermost](https://about.mattermost.com/), an open source, private cloud workplace for web messaging. - [PlantUML](integration/plantuml.md): Create simple diagrams in AsciiDoc and Markdown documents -created in snippets, wikis, and repos. + created in snippets, wikis, and repos. - [Web terminals](integration/terminal.md): Provide terminal access to your applications deployed to Kubernetes from within GitLab's CI/CD [environments](../ci/environments.md#web-terminals). ## User settings and permissions @@ -88,7 +89,7 @@ created in snippets, wikis, and repos. - [Libravatar](../customization/libravatar.md): Use Libravatar instead of Gravatar for user avatars. - [Sign-up restrictions](../user/admin_area/settings/sign_up_restrictions.md): block email addresses of specific domains, or whitelist only specific domains. - [Access restrictions](../user/admin_area/settings/visibility_and_access_controls.md#enabled-git-access-protocols): Define which Git access protocols can be used to talk to GitLab (SSH, HTTP, HTTPS). -- [Authentication/Authorization](../topics/authentication/index.md#gitlab-administrators): Enforce 2FA, configure external authentication with LDAP, SAML, CAS and additional Omniauth providers. +- [Authentication and Authorization](auth/README.md): Configure external authentication with LDAP, SAML, CAS and additional providers. See also other [authentication](../topics/authentication/index.md#gitlab-administrators) topics (for example, enforcing 2FA). - [Incoming email](incoming_email.md): Configure incoming emails to allow users to [reply by email], create [issues by email] and [merge requests by email], and to enable [Service Desk]. diff --git a/doc/administration/issue_closing_pattern.md b/doc/administration/issue_closing_pattern.md index 35f25e55414..160da47c780 100644 --- a/doc/administration/issue_closing_pattern.md +++ b/doc/administration/issue_closing_pattern.md @@ -17,7 +17,7 @@ The default pattern can be located in [gitlab.yml.example] under the "Automatic issue closing" section. > **Tip:** -You are advised to use http://rubular.com to test the issue closing pattern. +You are advised to use <http://rubular.com> to test the issue closing pattern. Because Rubular doesn't understand `%{issue_ref}`, you can replace this by `#\d+` when testing your patterns, which matches only local issue references like `#123`. diff --git a/doc/administration/monitoring/performance/grafana_configuration.md b/doc/administration/monitoring/performance/grafana_configuration.md index 7947b0fedc4..1f431f8bd62 100644 --- a/doc/administration/monitoring/performance/grafana_configuration.md +++ b/doc/administration/monitoring/performance/grafana_configuration.md @@ -33,7 +33,7 @@ Test Connection to ensure the configuration is correct. - **Name**: InfluxDB - **Default**: Checked - **Type**: InfluxDB 0.9.x (Even if you're using InfluxDB 0.10.x) -- **Url**: https://localhost:8086 (Or the remote URL if you've installed InfluxDB +- **Url**: `https://localhost:8086` (Or the remote URL if you've installed InfluxDB on a separate server) - **Access**: proxy - **Database**: gitlab diff --git a/doc/administration/pages/index.md b/doc/administration/pages/index.md index cbd3032bd4e..10ae8c7dedf 100644 --- a/doc/administration/pages/index.md +++ b/doc/administration/pages/index.md @@ -332,6 +332,42 @@ The maximum size of the unpacked archive per project can be configured in the Admin area under the Application settings in the **Maximum size of pages (MB)**. The default is 100MB. +## Running GitLab Pages in a separate server + +You may want to run GitLab Pages daemon on a separate server in order to decrease the load on your main application server. +Follow the steps below to configure GitLab Pages in a separate server. + +1. Suppose you have the main GitLab application server named `app1`. Prepare +new Linux server (let's call it `app2`), create NFS share there and configure access to +this share from `app1`. Let's use the default GitLab Pages folder `/var/opt/gitlab/gitlab-rails/shared/pages` +as the shared folder on `app2` and mount it to `/mnt/pages` on `app1`. + +1. On `app2` install GitLab omnibus and modify `/etc/gitlab/gitlab.rb` this way: + + ```shell + external_url 'http://<ip-address-of-the-server>' + pages_external_url "http://<your-pages-domain>" + postgresql['enable'] = false + redis['enable'] = false + prometheus['enable'] = false + unicorn['enable'] = false + sidekiq['enable'] = false + gitlab_workhorse['enable'] = false + gitaly['enable'] = false + alertmanager['enable'] = false + node_exporter['enable'] = false + ``` +1. Run `sudo gitlab-ctl reconfigure`. +1. On `app1` apply the following changes to `/etc/gitlab/gitlab.rb`: + + ```shell + gitlab_pages['enable'] = false + pages_external_url "http://<your-pages-domain>" + gitlab_rails['pages_path'] = "/mnt/pages" + ``` + +1. Run `sudo gitlab-ctl reconfigure`. + ## Backup Pages are part of the [regular backup][backup] so there is nothing to configure. diff --git a/doc/administration/reply_by_email_postfix_setup.md b/doc/administration/reply_by_email_postfix_setup.md index 4c42cb7756a..d57fc67c83e 100644 --- a/doc/administration/reply_by_email_postfix_setup.md +++ b/doc/administration/reply_by_email_postfix_setup.md @@ -333,6 +333,6 @@ If all the tests were successful, Postfix is all set up and ready to receive ema --- -_This document was adapted from https://help.ubuntu.com/community/PostfixBasicSetupHowto, by contributors to the Ubuntu documentation wiki._ +_This document was adapted from <https://help.ubuntu.com/community/PostfixBasicSetupHowto>, by contributors to the Ubuntu documentation wiki._ [incoming email]: incoming_email.md diff --git a/doc/administration/troubleshooting/debug.md b/doc/administration/troubleshooting/debug.md index bd702dcc9ec..8f7280d5128 100644 --- a/doc/administration/troubleshooting/debug.md +++ b/doc/administration/troubleshooting/debug.md @@ -158,7 +158,7 @@ are concerned about affecting others during a production system, you can run a separate Rails process to debug the issue: 1. Log in to your GitLab account. -1. Copy the URL that is causing problems (e.g. https://gitlab.com/ABC). +1. Copy the URL that is causing problems (e.g. `https://gitlab.com/ABC`). 1. Create a Personal Access Token for your user (Profile Settings -> Access Tokens). 1. Bring up the GitLab Rails console. For omnibus users, run: diff --git a/doc/api/README.md b/doc/api/README.md index 6c5bb1c0940..692f63a400c 100644 --- a/doc/api/README.md +++ b/doc/api/README.md @@ -16,6 +16,7 @@ The following API resources are available: - [Broadcast messages](broadcast_messages.md) - [Code snippets](snippets.md) - [Commits](commits.md) +- [Container Registry](container_registry.md) - [Custom attributes](custom_attributes.md) - [Deploy keys](deploy_keys.md), and [deploy keys for multiple projects](deploy_key_multiple_projects.md) - [Deployments](deployments.md) @@ -438,6 +439,14 @@ Additional pagination headers are also sent back. | `X-Next-Page` | The index of the next page | | `X-Prev-Page` | The index of the previous page | +CAUTION: **Caution:** +For performance reasons since +[GitLab 11.8](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/23931) +and **behind the `api_kaminari_count_with_limit` +[feature flag](../development/feature_flags.md)**, if the number of resources is +more than 10,000, the `X-Total` and `X-Total-Pages` headers as well as the +`rel="last"` `Link` are not present in the response headers. + ## Namespaced path encoding If using namespaced API calls, make sure that the `NAMESPACE/PROJECT_NAME` is @@ -596,7 +605,7 @@ Content-Type: application/json ## Encoding `+` in ISO 8601 dates If you need to include a `+` in a query parameter, you may need to use `%2B` instead due -a [W3 recommendation](http://www.w3.org/Addressing/URL/4_URI_Recommentations.html) that +to a [W3 recommendation](http://www.w3.org/Addressing/URL/4_URI_Recommentations.html) that causes a `+` to be interpreted as a space. For example, in an ISO 8601 date, you may want to pass a time in Mountain Standard Time, such as: 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/container_registry.md b/doc/api/container_registry.md new file mode 100644 index 00000000000..b70854103e8 --- /dev/null +++ b/doc/api/container_registry.md @@ -0,0 +1,200 @@ +# Container Registry API + +This is the API docs of the [GitLab Container Registry](../user/project/container_registry.md). + +## List registry repositories + +Get a list of registry repositories in a project. + +``` +GET /projects/:id/registry/repositories +``` + +| 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. | + + +```bash +curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/registry/repositories" +``` + +Example response: + +```json +[ + { + "id": 1, + "name": "", + "path": "group/project", + "location": "gitlab.example.com:5000/group/project", + "created_at": "2019-01-10T13:38:57.391Z" + }, + { + "id": 2, + "name": "releases", + "path": "group/project/releases", + "location": "gitlab.example.com:5000/group/project/releases", + "created_at": "2019-01-10T13:39:08.229Z" + } +] +``` + +## Delete registry repository + +Get a list of repository commits in a project. + +This operation is executed asynchronously and might take some time to get executed. + +``` +DELETE /projects/:id/registry/repositories/:repository_id +``` + +| 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. | +| `repository_id` | integer | yes | The ID of registry repository. | + +```bash +curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/registry/repositories/2" +``` + +## List repository tags + +Get a list of tags for given registry repository. + +``` +GET /projects/:id/registry/repositories/:repository_id/tags +``` + +| 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. | +| `repository_id` | integer | yes | The ID of registry repository. | + +```bash +curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/registry/repositories/2/tags" +``` + +Example response: + +```json +[ + { + "name": "A", + "path": "group/project:A", + "location": "gitlab.example.com:5000/group/project:A" + }, + { + "name": "latest", + "path": "group/project:latest", + "location": "gitlab.example.com:5000/group/project:latest" + } +] +``` + +## Get details of a repository tag + +Get details of a registry repository tag. + +``` +GET /projects/:id/registry/repositories/:repository_id/tags/:tag_name +``` + +| 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. | +| `repository_id` | integer | yes | The ID of registry repository. | +| `tag_name` | string | yes | The name of tag. | + +```bash +curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/registry/repositories/2/tags/v10.0.0" +``` + +Example response: + +```json +{ + "name": "v10.0.0", + "path": "group/project:latest", + "location": "gitlab.example.com:5000/group/project:latest", + "revision": "e9ed9d87c881d8c2fd3a31b41904d01ba0b836e7fd15240d774d811a1c248181", + "short_revision": "e9ed9d87c", + "digest": "sha256:c3490dcf10ffb6530c1303522a1405dfaf7daecd8f38d3e6a1ba19ea1f8a1751", + "created_at": "2019-01-06T16:49:51.272+00:00", + "total_size": 350224384 +} +``` + +## Delete a repository tag + +Delete a registry repository tag. + +``` +DELETE /projects/:id/registry/repositories/:repository_id/tags/:tag_name +``` + +| 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. | +| `repository_id` | integer | yes | The ID of registry repository. | +| `tag_name` | string | yes | The name of tag. | + +```bash +curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/registry/repositories/2/tags/v10.0.0" +``` + +## Delete repository tags in bulk + +Delete repository tags in bulk based on given criteria. + +``` +DELETE /projects/:id/registry/repositories/:repository_id/tags +``` + +| 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. | +| `repository_id` | integer | yes | The ID of registry repository. | +| `name_regex` | string | yes | The regex of the name to delete. To delete all tags specify `.*`. | +| `keep_n` | integer | no | The amount of latest tags of given name to keep. | +| `older_than` | string | no | Tags to delete that are older than the given time, written in human readable form `1h`, `1d`, `1month`. | + +This API call performs the following operations: + +1. It orders all tags by creation date. The creation date is the time of the + manifest creation, not the time of tag push. +1. It removes only the tags matching the given `name_regex`. +1. It never removes the tag named `latest`. +1. It keeps N latest matching tags (if `keep_n` is specified). +1. It only removes tags that are older than X amount of time (if `older_than` is specified). +1. It schedules the asynchronous job to be executed in the background. + +These operations are executed asynchronously and it might +take time to get executed. You can run this at most +once an hour for a given container repository. + +NOTE: **Note:** +Due to a [Docker Distribution deficiency](https://gitlab.com/gitlab-org/gitlab-ce/issues/21405), +it doesn't remove tags whose manifest is shared by multiple tags. + +Examples: + +1. Remove tag names that are matching the regex (Git SHA), keep always at least 5, + and remove ones that are older than 2 days: + + ```bash + curl --request DELETE --data 'name_regex=[0-9a-z]{40}' --data 'keep_n=5' --data 'older_than=2d' --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/registry/repositories/2/tags" + ``` + +2. Remove all tags, but keep always the latest 5: + + ```bash + curl --request DELETE --data 'name_regex=.*' --data 'keep_n=5' --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/registry/repositories/2/tags" + ``` + +3. Remove all tags that are older than 1 month: + + ```bash + curl --request DELETE --data 'name_regex=.*' --data 'older_than=1month' --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/registry/repositories/2/tags" + ``` 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/issues.md b/doc/api/issues.md index fb06119063f..6d8683601f6 100644 --- a/doc/api/issues.md +++ b/doc/api/issues.md @@ -106,6 +106,8 @@ Example response: "created_at" : "2016-01-04T15:31:51.081Z", "iid" : 6, "labels" : [], + "upvotes": 4, + "downvotes": 0, "user_notes_count": 1, "due_date": "2016-07-22", "web_url": "http://example.com/example/example/issues/6", @@ -214,6 +216,8 @@ Example response: "name" : "Dr. Luella Kovacek" }, "labels" : [], + "upvotes": 4, + "downvotes": 0, "id" : 41, "title" : "Ut commodi ullam eos dolores perferendis nihil sunt.", "updated_at" : "2016-01-04T15:31:46.176Z", @@ -327,6 +331,8 @@ Example response: "name" : "Dr. Luella Kovacek" }, "labels" : [], + "upvotes": 4, + "downvotes": 0, "id" : 41, "title" : "Ut commodi ullam eos dolores perferendis nihil sunt.", "updated_at" : "2016-01-04T15:31:46.176Z", @@ -421,6 +427,8 @@ Example response: "name" : "Dr. Luella Kovacek" }, "labels" : [], + "upvotes": 4, + "downvotes": 0, "id" : 41, "title" : "Ut commodi ullam eos dolores perferendis nihil sunt.", "updated_at" : "2016-01-04T15:31:46.176Z", @@ -494,6 +502,8 @@ Example response: "labels" : [ "bug" ], + "upvotes": 4, + "downvotes": 0, "author" : { "name" : "Alexandra Bashirian", "avatar_url" : null, @@ -592,6 +602,8 @@ Example response: "labels" : [ "bug" ], + "upvotes": 4, + "downvotes": 0, "id" : 85, "assignees" : [], "assignee" : null, @@ -676,6 +688,8 @@ Example response: "closed_at": null, "closed_by": null, "labels": [], + "upvotes": 4, + "downvotes": 0, "milestone": null, "assignees": [{ "name": "Miss Monserrate Beier", @@ -758,6 +772,8 @@ Example response: "closed_at": null, "closed_by": null, "labels": [], + "upvotes": 4, + "downvotes": 0, "milestone": null, "assignees": [{ "name": "Miss Monserrate Beier", @@ -839,6 +855,8 @@ Example response: "created_at": "2016-04-05T21:41:45.217Z", "updated_at": "2016-04-07T13:02:37.905Z", "labels": [], + "upvotes": 4, + "downvotes": 0, "milestone": null, "assignee": { "name": "Edwardo Grady", diff --git a/doc/api/project_clusters.md b/doc/api/project_clusters.md index c51a3564211..8efb98fe1fc 100644 --- a/doc/api/project_clusters.md +++ b/doc/api/project_clusters.md @@ -76,7 +76,7 @@ Parameters: | Attribute | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `id` | integer | yes | The ID of the project owned by the authenticated user | -| `cluster_id` | integer | yes | The ID of the cluster | +| `cluster_id` | integer | yes | The ID of the cluster | Example request: @@ -157,12 +157,12 @@ Parameters: | --------- | ---- | -------- | ----------- | | `id` | integer | yes | The ID of the project owned by the authenticated user | | `name` | String | yes | The name of the cluster | -| `enabled` | Boolean | no | Determines if cluster is active or not, defaults to true | -| `platform_kubernetes_attributes[api_url]` | String | yes | The URL to access the Kubernetes API | +| `enabled` | Boolean | no | Determines if cluster is active or not, defaults to true | +| `platform_kubernetes_attributes[api_url]` | String | yes | The URL to access the Kubernetes API | | `platform_kubernetes_attributes[token]` | String | yes | The token to authenticate against Kubernetes | -| `platform_kubernetes_attributes[ca_cert]` | String | no | TLS certificate (needed if API is using a self-signed TLS certificate | -| `platform_kubernetes_attributes[namespace]` | String | no | The unique namespace related to the project | -| `platform_kubernetes_attributes[authorization_type]` | String | no | The cluster authorization type: `rbac`, `abac` or `unknown_authorization`. Defaults to `rbac`. | +| `platform_kubernetes_attributes[ca_cert]` | String | no | TLS certificate (needed if API is using a self-signed TLS certificate | +| `platform_kubernetes_attributes[namespace]` | String | no | The unique namespace related to the project | +| `platform_kubernetes_attributes[authorization_type]` | String | no | The cluster authorization type: `rbac`, `abac` or `unknown_authorization`. Defaults to `rbac`. | Example request: @@ -245,11 +245,12 @@ Parameters: | Attribute | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `id` | integer | yes | The ID of the project owned by the authenticated user | -| `name` | String | no | The name of the cluster | -| `platform_kubernetes_attributes[api_url]` | String | no | The URL to access the Kubernetes API | -| `platform_kubernetes_attributes[token]` | String | no | The token to authenticate against Kubernetes | -| `platform_kubernetes_attributes[ca_cert]` | String | no | TLS certificate (needed if API is using a self-signed TLS certificate | -| `platform_kubernetes_attributes[namespace]` | String | no | The unique namespace related to the project | +| `cluster_id` | integer | yes | The ID of the cluster | +| `name` | String | no | The name of the cluster | +| `platform_kubernetes_attributes[api_url]` | String | no | The URL to access the Kubernetes API | +| `platform_kubernetes_attributes[token]` | String | no | The token to authenticate against Kubernetes | +| `platform_kubernetes_attributes[ca_cert]` | String | no | TLS certificate (needed if API is using a self-signed TLS certificate | +| `platform_kubernetes_attributes[namespace]` | String | no | The unique namespace related to the project | NOTE: **Note:** `name`, `api_url`, `ca_cert` and `token` can only be updated if the cluster was added 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/repositories.md b/doc/api/repositories.md index 9f552a10589..104c64a89ce 100644 --- a/doc/api/repositories.md +++ b/doc/api/repositories.md @@ -112,7 +112,7 @@ GET /projects/:id/repository/archive[.format] ``` `format` is an optional suffix for the archive format. Default is -`tar.gz`. Options are `tar.gz`, `tar.bz2`, `tbz`, 'tbz2`, `tb2`, +`tar.gz`. Options are `tar.gz`, `tar.bz2`, `tbz`, `tbz2`, `tb2`, `bz2`, `tar`, and `zip`. For example, specifying `archive.zip` would send an archive in ZIP format. diff --git a/doc/api/settings.md b/doc/api/settings.md index 9998a93de03..c329e3cdf24 100644 --- a/doc/api/settings.md +++ b/doc/api/settings.md @@ -208,7 +208,7 @@ are listed in the descriptions of the relevant settings. | `rsa_key_restriction` | integer | no | The minimum allowed bit length of an uploaded RSA key. Default is `0` (no restriction). `-1` disables RSA keys. | | `send_user_confirmation_email` | boolean | no | Send confirmation email on sign-up. | | `sentry_dsn` | string | required by: `sentry_enabled` | Sentry Data Source Name. | -| `sentry_enabled` | boolean | no | (**If enabled, requires:** `sentry_dsn`) Sentry is an error reporting and logging tool which is currently not shipped with GitLab, available at https://getsentry.com. | +| `sentry_enabled` | boolean | no | (**If enabled, requires:** `sentry_dsn`) Sentry is an error reporting and logging tool which is currently not shipped with GitLab, available at <https://sentry.io>. | | `session_expire_delay` | integer | no | Session duration in minutes. GitLab restart is required to apply changes | | `shared_runners_enabled` | boolean | no | (**If enabled, requires:** `shared_runners_text`) Enable shared runners for new projects. | | `shared_runners_text` | string | required by: `shared_runners_enabled` | Shared runners text. | 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/caching/index.md b/doc/ci/caching/index.md index 495ec099111..8b2ce425cf5 100644 --- a/doc/ci/caching/index.md +++ b/doc/ci/caching/index.md @@ -29,7 +29,7 @@ needed to compile the project: Cache was designed to be used to speed up invocations of subsequent runs of a given job, by keeping things like dependencies (e.g., npm packages, Go vendor packages, etc.) so they don't have to be re-fetched from the public internet. - While the cache can be abused to pass intermediate build results between + While the cache can be abused to pass intermediate build results between stages, there may be cases where artifacts are a better fit. - `artifacts`: **Use for stage results that will be passed between stages.** Artifacts were designed to upload some compiled/generated bits of the build, @@ -40,10 +40,10 @@ needed to compile the project: comply to this rule trigger an unintuitive and illogical error message (an enhancement is discussed at [https://gitlab.com/gitlab-org/gitlab-ce/issues/15530](https://gitlab.com/gitlab-org/gitlab-ce/issues/15530) - ). Artifacts need to be uploaded to the GitLab instance (not only the GitLab - runner) before the next stage job(s) can start, so you need to evaluate - carefully whether your bandwidth allows you to profit from parallelization - with stages and shared artifacts before investing time in changes to the + ). Artifacts need to be uploaded to the GitLab instance (not only the GitLab + runner) before the next stage job(s) can start, so you need to evaluate + carefully whether your bandwidth allows you to profit from parallelization + with stages and shared artifacts before investing time in changes to the setup. @@ -90,7 +90,7 @@ cache, when declaring `cache` in your jobs, use one or a mix of the following: that will be only available to a particular project. - [Use a `key`](../yaml/README.md#cache-key) that fits your workflow (e.g., different caches on each branch). For that, you can take advantage of the - [CI/CD predefined variables](../variables/README.md#predefined-variables-environment-variables). + [CI/CD predefined variables](../variables/README.md#predefined-environment-variables). TIP: **Tip:** Using the same Runner for your pipeline, is the most simple and efficient way to diff --git a/doc/ci/docker/using_docker_build.md b/doc/ci/docker/using_docker_build.md index fef367051bf..a462c75f2f5 100644 --- a/doc/ci/docker/using_docker_build.md +++ b/doc/ci/docker/using_docker_build.md @@ -194,7 +194,7 @@ not without its own challenges: - docker run -v "$MOUNT_POINT:/mnt" my-docker-image ``` -An example project using this approach can be found here: https://gitlab.com/gitlab-examples/docker. +An example project using this approach can be found here: <https://gitlab.com/gitlab-examples/docker>. ### Use Docker socket binding @@ -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/docker/using_kaniko.md b/doc/ci/docker/using_kaniko.md index aa6b387bc58..f354cdb398e 100644 --- a/doc/ci/docker/using_kaniko.md +++ b/doc/ci/docker/using_kaniko.md @@ -40,7 +40,7 @@ In the following example, kaniko is used to build a Docker image and then push it to [GitLab Container Registry](../../user/project/container_registry.md). The job will run only when a tag is pushed. A `config.json` file is created under `/kaniko/.docker` with the needed GitLab Container Registry credentials taken from the -[environment variables](../variables/README.md#predefined-variables-environment-variables) +[environment variables](../variables/README.md#predefined-environment-variables) GitLab CI/CD provides. In the last step, kaniko uses the `Dockerfile` under the root directory of the project, builds the Docker image and pushes it to the project's Container Registry while tagging it with the Git tag: diff --git a/doc/ci/environments.md b/doc/ci/environments.md index 010c579b83e..6a9917f6430 100644 --- a/doc/ci/environments.md +++ b/doc/ci/environments.md @@ -249,7 +249,7 @@ the basis of [Review apps](review_apps/index.md). NOTE: **Note:** The `name` and `url` parameters can use most of the CI/CD variables, -including [predefined](variables/README.md#predefined-variables-environment-variables), +including [predefined](variables/README.md#predefined-environment-variables), [project/group ones](variables/README.md#variables) and [`.gitlab-ci.yml` variables](yaml/README.md#variables). You however cannot use variables defined under `script` or on the Runner's side. There are also other variables that @@ -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. +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. -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 +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/examples/artifactory_and_gitlab/index.md b/doc/ci/examples/artifactory_and_gitlab/index.md index 6aa0edd87b4..9e657275d50 100644 --- a/doc/ci/examples/artifactory_and_gitlab/index.md +++ b/doc/ci/examples/artifactory_and_gitlab/index.md @@ -16,8 +16,8 @@ to build a [Maven](https://maven.apache.org/) project, deploy it to [Artifactory You'll create two different projects: -- `simple-maven-dep`: the app built and deployed to Artifactory (available at https://gitlab.com/gitlab-examples/maven/simple-maven-dep ) -- `simple-maven-app`: the app using the previous one as a dependency (available at https://gitlab.com/gitlab-examples/maven/simple-maven-app ) +- `simple-maven-dep`: the app built and deployed to Artifactory (available at <https://gitlab.com/gitlab-examples/maven/simple-maven-dep>) +- `simple-maven-app`: the app using the previous one as a dependency (available at <https://gitlab.com/gitlab-examples/maven/simple-maven-app>) We assume that you already have a GitLab account on [GitLab.com](https://gitlab.com/), and that you know the basic usage of Git and [GitLab CI/CD](https://about.gitlab.com/features/gitlab-ci-cd/). We also assume that an Artifactory instance is available and reachable from the internet, and that you have valid credentials to deploy on it. @@ -107,7 +107,7 @@ Now it's time we set up [GitLab CI/CD](https://about.gitlab.com/features/gitlab- GitLab CI/CD uses a file in the root of the repo, named `.gitlab-ci.yml`, to read the definitions for jobs that will be executed by the configured GitLab Runners. You can read more about this file in the [GitLab Documentation](https://docs.gitlab.com/ee/ci/yaml/). -First of all, remember to set up variables for your deployment. Navigate to your project's **Settings > CI/CD > Variables** page +First of all, remember to set up variables for your deployment. Navigate to your project's **Settings > CI/CD > Environment variables** page and add the following ones (replace them with your current values, of course): - **MAVEN_REPO_URL**: `http://artifactory.example.com:8081/artifactory` (your Artifactory URL) diff --git a/doc/ci/examples/container_scanning.md b/doc/ci/examples/container_scanning.md index 68330261910..31c3df81fef 100644 --- a/doc/ci/examples/container_scanning.md +++ b/doc/ci/examples/container_scanning.md @@ -22,7 +22,7 @@ container_scanning: variables: DOCKER_DRIVER: overlay2 ## Define two new variables based on GitLab's CI/CD predefined variables - ## https://docs.gitlab.com/ee/ci/variables/#predefined-variables-environment-variables + ## https://docs.gitlab.com/ee/ci/variables/#predefined-environment-variables CI_APPLICATION_REPOSITORY: $CI_REGISTRY_IMAGE/$CI_COMMIT_REF_SLUG CI_APPLICATION_TAG: $CI_COMMIT_SHA allow_failure: true @@ -87,7 +87,7 @@ container_scanning: variables: DOCKER_DRIVER: overlay2 ## Define two new variables based on GitLab's CI/CD predefined variables - ## https://docs.gitlab.com/ee/ci/variables/#predefined-variables-environment-variables + ## https://docs.gitlab.com/ee/ci/variables/#predefined-environment-variables CI_APPLICATION_REPOSITORY: $CI_REGISTRY_IMAGE/$CI_COMMIT_REF_SLUG CI_APPLICATION_TAG: $CI_COMMIT_SHA allow_failure: true diff --git a/doc/ci/examples/deploy_spring_boot_to_cloud_foundry/index.md b/doc/ci/examples/deploy_spring_boot_to_cloud_foundry/index.md index 40ceef3d554..6499413baf0 100644 --- a/doc/ci/examples/deploy_spring_boot_to_cloud_foundry/index.md +++ b/doc/ci/examples/deploy_spring_boot_to_cloud_foundry/index.md @@ -104,7 +104,7 @@ to ensure our deployments only happen when we push to the master branch. Now, since the steps defined in `.gitlab-ci.yml` require credentials to login to CF, you'll need to add your CF credentials as [environment -variables](../../variables/README.md#predefined-variables-environment-variables) +variables](../../variables/README.md#predefined-environment-variables) on GitLab CI/CD. To set the environment variables, navigate to your project's **Settings > CI/CD** and expand **Variables**. Name the variables `CF_USERNAME` and `CF_PASSWORD` and set them to the correct values. @@ -138,5 +138,5 @@ buildpack: client-certificate-mapper=1.2.0_RELEASE container-security-provider=1 ``` You can then visit your deployed application (for this example, -https://gitlab-hello-world-undissembling-hotchpot.cfapps.io/) and you should +`https://gitlab-hello-world-undissembling-hotchpot.cfapps.io/`) and you should see the "Spring is here!" message. diff --git a/doc/ci/examples/deployment/README.md b/doc/ci/examples/deployment/README.md index 46effb76d71..010ba6b66a2 100644 --- a/doc/ci/examples/deployment/README.md +++ b/doc/ci/examples/deployment/README.md @@ -6,7 +6,7 @@ used with GitLab CI. >**Note:** We recommend to use Dpl if you're deploying to any of these services: -https://github.com/travis-ci/dpl#supported-providers. +<https://github.com/travis-ci/dpl#supported-providers>. ## Requirements @@ -34,7 +34,7 @@ The Dpl provides support for vast number of services, including: Heroku, Cloud F To use it simply define provider and any additional parameters required by the provider. For example if you want to use it to deploy your application to heroku, you need to specify `heroku` as provider, specify `api-key` and `app`. -There's more and all possible parameters can be found here: https://github.com/travis-ci/dpl#heroku +There's more and all possible parameters can be found here: <https://github.com/travis-ci/dpl#heroku>. ```yaml staging: diff --git a/doc/ci/examples/test-and-deploy-python-application-to-heroku.md b/doc/ci/examples/test-and-deploy-python-application-to-heroku.md index b59271e400f..61bf68fa0e8 100644 --- a/doc/ci/examples/test-and-deploy-python-application-to-heroku.md +++ b/doc/ci/examples/test-and-deploy-python-application-to-heroku.md @@ -47,7 +47,7 @@ This project has three jobs: ## Store API keys -You'll need to create two variables in **Settings > CI/CD > Variables** in your GitLab project: +You'll need to create two variables in **Settings > CI/CD > Environment variables** in your GitLab project: - `HEROKU_STAGING_API_KEY` - Heroku API key used to deploy staging app. - `HEROKU_PRODUCTION_API_KEY` - Heroku API key used to deploy production app. diff --git a/doc/ci/examples/test-and-deploy-ruby-application-to-heroku.md b/doc/ci/examples/test-and-deploy-ruby-application-to-heroku.md index 33a353f17f5..46e6efccaf8 100644 --- a/doc/ci/examples/test-and-deploy-ruby-application-to-heroku.md +++ b/doc/ci/examples/test-and-deploy-ruby-application-to-heroku.md @@ -43,7 +43,7 @@ This project has three jobs: ## Store API keys -You'll need to create two variables in your project's **Settings > CI/CD > Variables**: +You'll need to create two variables in your project's **Settings > CI/CD > Environment variables**: - `HEROKU_STAGING_API_KEY` - Heroku API key used to deploy staging app. - `HEROKU_PRODUCTION_API_KEY` - Heroku API key used to deploy production app. diff --git a/doc/ci/interactive_web_terminal/index.md b/doc/ci/interactive_web_terminal/index.md index 0cf9daed22f..2a4160f62b0 100644 --- a/doc/ci/interactive_web_terminal/index.md +++ b/doc/ci/interactive_web_terminal/index.md @@ -1,4 +1,4 @@ -# Interactive Web Terminals **[CORE ONLY]** +# Interactive Web Terminals > [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/50144) in GitLab 11.3. @@ -9,10 +9,11 @@ is deployed, some [security precautions](../../administration/integration/termin taken to protect the users. NOTE: **Note:** -GitLab.com does not support interactive web terminal at the moment – neither -using shared GitLab.com runners nor your own runners. Please follow -[this issue](https://gitlab.com/gitlab-org/gitlab-ce/issues/52611) for -progress. +[Shared runners on GitLab.com](../quick_start/README.md#shared-runners) do not +provide an interactive web terminal. Follow [this +issue](https://gitlab.com/gitlab-org/gitlab-ce/issues/52611) for progress on +adding support. For groups and projects hosted on GitLab.com, interactive web +terminals are available when using your own group or project runner. ## Configuration diff --git a/doc/ci/junit_test_reports.md b/doc/ci/junit_test_reports.md index 91a0ae327bf..cf18c6d9660 100644 --- a/doc/ci/junit_test_reports.md +++ b/doc/ci/junit_test_reports.md @@ -69,7 +69,7 @@ collects the JUnit test report from each job. After each job is executed, the XML reports are stored in GitLab as artifacts and their results are shown in the merge request widget. -NOTE: **Note:** +NOTE: **Note:** If you also want the ability to browse JUnit output files, include the [`artifacts:paths`](yaml/README.md#artifactspaths) keyword. @@ -151,7 +151,7 @@ There are a few tools that can produce JUnit reports in C/C++. #### GoogleTest In the following example, `gtest` is used to generate the test reports. -If there are multiple gtest executables created for different architectures (`x86`, `x64` or `arm`), +If there are multiple gtest executables created for different architectures (`x86`, `x64` or `arm`), you will be required to run each test providing a unique filename. The results will then be aggregated together. @@ -171,4 +171,4 @@ Currently, the following tools might not work because their XML formats are unsu |Case|Tool|Issue| |---|---|---| -|`<testcase>` does not have `classname` attribute|ESlint, sass-lint|https://gitlab.com/gitlab-org/gitlab-ce/issues/50964| +|`<testcase>` does not have `classname` attribute|ESlint, sass-lint|<https://gitlab.com/gitlab-org/gitlab-ce/issues/50964>| 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/triggers/README.md b/doc/ci/triggers/README.md index c9a60feb73f..61037360326 100644 --- a/doc/ci/triggers/README.md +++ b/doc/ci/triggers/README.md @@ -224,5 +224,5 @@ removed with one of the future versions of GitLab. You are advised to [ee-2017]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/2017 [ee]: https://about.gitlab.com/pricing/ [variables]: ../variables/README.md -[predef]: ../variables/README.md#predefined-variables-environment-variables +[predef]: ../variables/README.md#predefined-environment-variables [registry]: ../../user/project/container_registry.md diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md index 25d189afb24..97e133a2e2f 100644 --- a/doc/ci/variables/README.md +++ b/doc/ci/variables/README.md @@ -1,26 +1,36 @@ +--- +table_display_block: true +--- + # GitLab CI/CD Variables -When receiving a job from GitLab CI, the [Runner] prepares the build environment. -It starts by setting a list of **predefined variables** (environment variables) -and a list of **user-defined variables**. +When receiving a job from GitLab CI, the [Runner](https://docs.gitlab.com/runner/) prepares the build environment. +It starts by setting a list of: + +- [Predefined environment variables](#predefined-environment-variables). +- Other variables. ## Priority of variables -The variables can be overwritten and they take precedence over each other in -this order: +Variables of different types can take precedence over other variables, depending on where they are defined. + +The order of precedence for variables is (from highest to lowest): -1. [Trigger variables][triggers] or [scheduled pipeline variables](../../user/project/pipelines/schedules.md#making-use-of-scheduled-pipeline-variables) (take precedence over all) -1. Project-level [variables](#variables) or [protected variables](#protected-variables) -1. Group-level [variables](#variables) or [protected variables](#protected-variables) -1. YAML-defined [job-level variables](../yaml/README.md#variables) -1. YAML-defined [global variables](../yaml/README.md#variables) -1. [Deployment variables](#deployment-variables) -1. [Predefined variables](#predefined-variables-environment-variables) (are the - lowest in the chain) +1. [Trigger variables](../triggers/README.md#pass-job-variables-to-a-trigger) or [scheduled pipeline variables](../../user/project/pipelines/schedules.md#making-use-of-scheduled-pipeline-variables). +1. Project-level [variables](#variables) or [protected variables](#protected-variables). +1. Group-level [variables](#variables) or [protected variables](#protected-variables). +1. YAML-defined [job-level variables](../yaml/README.md#variables). +1. YAML-defined [global variables](../yaml/README.md#variables). +1. [Deployment variables](#deployment-variables). +1. [Predefined environment variables](#predefined-environment-variables). -For example, if you define `API_TOKEN=secure` as a project variable and -`API_TOKEN=yaml` in your `.gitlab-ci.yml`, the `API_TOKEN` will take the value -`secure` as the project variables are higher in the chain. +For example, you define: + +- `API_TOKEN=secure` as a project variable. +- `API_TOKEN=yaml` in your `.gitlab-ci.yml`. + +`API_TOKEN` will take the value `secure` as the project variables take precedence over those defined +in `.gitlab-ci.yml`. ## Unsupported variables @@ -28,10 +38,10 @@ There are cases where some variables cannot be used in the context of a `.gitlab-ci.yml` definition (for example under `script`). Read more about which variables are [not supported](where_variables_can_be_used.md). -## Predefined variables (Environment variables) +## Predefined environment variables Some of the predefined environment variables are available only if a minimum -version of [GitLab Runner][runner] is used. Consult the table below to find the +version of [GitLab Runner](https://docs.gitlab.com/runner/) is used. Consult the table below to find the version of Runner required. NOTE: **Note:** @@ -55,12 +65,12 @@ future GitLab releases.** | **CI_COMMIT_TITLE** | 10.8 | all | The title of the commit - the full first line of the message | | **CI_CONFIG_PATH** | 9.4 | 0.5 | The path to CI config file. Defaults to `.gitlab-ci.yml` | | **CI_DEBUG_TRACE** | all | 1.7 | Whether [debug tracing](#debug-tracing) is enabled | -| **CI_DEPLOY_PASSWORD** | 10.8 | all | Authentication password of the [GitLab Deploy Token][gitlab-deploy-token], only present if the Project has one related.| +| **CI_DEPLOY_PASSWORD** | 10.8 | all | Authentication password of the [GitLab Deploy Token][gitlab-deploy-token], only present if the Project has one related.| | **CI_DEPLOY_USER** | 10.8 | all | Authentication username of the [GitLab Deploy Token][gitlab-deploy-token], only present if the Project has one related.| | **CI_DISPOSABLE_ENVIRONMENT** | all | 10.1 | Marks that the job is executed in a disposable environment (something that is created only for this job and disposed of/destroyed after the execution - all executors except `shell` and `ssh`). If the environment is disposable, it is set to true, otherwise it is not defined at all. | -| **CI_ENVIRONMENT_NAME** | 8.15 | all | The name of the environment for this job | -| **CI_ENVIRONMENT_SLUG** | 8.15 | all | A simplified version of the environment name, suitable for inclusion in DNS, URLs, Kubernetes labels, etc. | -| **CI_ENVIRONMENT_URL** | 9.3 | all | The URL of the environment for this job | +| **CI_ENVIRONMENT_NAME** | 8.15 | all | The name of the environment for this job. Only present if [`environment:name`](../yaml/README.md#environmenturl) is set. | +| **CI_ENVIRONMENT_SLUG** | 8.15 | all | A simplified version of the environment name, suitable for inclusion in DNS, URLs, Kubernetes labels, etc. Only present if [`environment:name`](../yaml/README.md#environmentname) is set. | +| **CI_ENVIRONMENT_URL** | 9.3 | all | The URL of the environment for this job. Only present if [`environment:url`](../yaml/README.md#environmenturl) is set. | | **CI_JOB_ID** | 9.0 | all | The unique id of the current job that GitLab CI uses internally | | **CI_JOB_MANUAL** | 8.12 | all | The flag to indicate that job was manually started | | **CI_JOB_NAME** | 9.0 | 0.5 | The name of the job as defined in `.gitlab-ci.yml` | @@ -81,6 +91,8 @@ future GitLab releases.** | **CI_NODE_INDEX** | 11.5 | all | Index of the job in the job set. If the job is not parallelized, this variable is not set. | | **CI_NODE_TOTAL** | 11.5 | all | Total number of instances of this job running in parallel. If the job is not parallelized, this variable is set to `1`. | | **CI_API_V4_URL** | 11.7 | all | The GitLab API v4 root URL | +| **CI_PAGES_DOMAIN** | 11.8 | all | The configured domain that hosts GitLab Pages. | +| **CI_PAGES_URL** | 11.8 | all | URL to GitLab Pages-built pages. Always belongs to a subdomain of `CI_PAGES_DOMAIN`. | | **CI_PIPELINE_ID** | 8.10 | all | The unique id of the current pipeline that GitLab CI uses internally | | **CI_PIPELINE_IID** | 11.0 | all | The unique id of the current pipeline scoped to project | | **CI_PIPELINE_SOURCE** | 10.0 | all | Indicates how the pipeline was triggered. Possible options are: `push`, `web`, `trigger`, `schedule`, `api`, and `pipeline`. For pipelines created before GitLab 9.5, this will show as `unknown` | @@ -92,7 +104,7 @@ future GitLab releases.** | **CI_PROJECT_NAMESPACE** | 8.10 | 0.5 | The project namespace (username or groupname) that is currently being built | | **CI_PROJECT_PATH** | 8.10 | 0.5 | The namespace with project name | | **CI_PROJECT_PATH_SLUG** | 9.3 | all | `$CI_PROJECT_PATH` lowercased and with everything except `0-9` and `a-z` replaced with `-`. Use in URLs and domain names. | -| **CI_PROJECT_URL** | 8.10 | 0.5 | The HTTP address to access project | +| **CI_PROJECT_URL** | 8.10 | 0.5 | The HTTP(S) address to access project | | **CI_PROJECT_VISIBILITY** | 10.3 | all | The project visibility (internal, private, public) | | **CI_REGISTRY** | 8.10 | 0.5 | If the Container Registry is enabled it returns the address of GitLab's Container Registry | | **CI_REGISTRY_IMAGE** | 8.10 | 0.5 | If the Container Registry is enabled for the project it returns the address of the registry tied to the specific project | @@ -154,7 +166,7 @@ This feature requires GitLab Runner 0.5.0 or higher and GitLab 7.14 or higher. GitLab CI allows you to add to `.gitlab-ci.yml` variables that are set in the build environment. The variables are hence saved in the repository, and they -are meant to store non-sensitive project configuration, e.g., `RAILS_ENV` or +are meant to store non-sensitive project configuration. For example, `RAILS_ENV` or `DATABASE_URL`. For example, if you set the variable below globally (not inside a job), it will @@ -202,16 +214,18 @@ GitLab CI allows you to define per-project or per-group variables that are set in the pipeline environment. The variables are stored out of the repository (not in `.gitlab-ci.yml`) and are securely passed to GitLab Runner making them available during a pipeline run. It's the recommended method to -use for storing things like passwords, SSH keys and credentials. +use for storing things like passwords, SSH keys, and credentials. + +Project-level variables can be added by: -Project-level variables can be added by going to your project's -**Settings > CI/CD**, then finding the section called **Variables**. +1. Navigating to your project's **Settings > CI/CD** page. +1. Inputing variable keys and values in the **Environment variables** section. -Likewise, group-level variables can be added by going to your group's -**Settings > CI/CD**, then finding the section called **Variables**. -Any variables of [subgroups] will be inherited recursively. +Group-level variables can be added by: -![Variables](img/variables.png) +1. Navigating to your group's **Settings > CI/CD** page. +1. Inputing variable keys and values in the **Environment variables** section. Any variables of + [subgroups](../../user/group/subgroups/index.md) will be inherited recursively. Once you set them, they will be available for all subsequent pipelines. You can also [protect your variables](#protected-variables). @@ -391,6 +405,10 @@ Running on runner-8a2f473d-project-1796893-concurrent-0 via runner-8a2f473d-mach ++ CI_SERVER_VERSION=8.14.3-ee ++ export CI_SERVER_REVISION=82823 ++ CI_SERVER_REVISION=82823 +++ export CI_PAGES_DOMAIN=gitlab.io +++ CI_PAGES_DOMAIN=gitlab.io +++ export CI_PAGES_URL=https://gitlab-examples.gitlab.io/ci-debug-trace +++ CI_PAGES_URL=https://gitlab-examples.gitlab.io/ci-debug-trace ++ export CI_PROJECT_ID=17893 ++ CI_PROJECT_ID=17893 ++ export CI_PROJECT_NAME=ci-debug-trace @@ -494,6 +512,8 @@ export CI_JOB_TRIGGERED="true" export CI_JOB_TOKEN="abcde-1234ABCD5678ef" export CI_PIPELINE_ID="1000" export CI_PIPELINE_IID="10" +export CI_PAGES_DOMAIN="gitlab.io" +export CI_PAGES_URL="https://gitlab-org.gitlab.io/gitlab-ce" export CI_PROJECT_ID="34" export CI_PROJECT_DIR="/builds/gitlab-org/gitlab-ce" export CI_PROJECT_NAME="gitlab-ce" @@ -609,11 +629,8 @@ Below you can find supported syntax reference: [envs]: ../environments.md [protected branches]: ../../user/project/protected_branches.md [protected tags]: ../../user/project/protected_tags.md -[runner]: https://docs.gitlab.com/runner/ [shellexecutors]: https://docs.gitlab.com/runner/executors/ [triggered]: ../triggers/README.md -[triggers]: ../triggers/README.md#pass-job-variables-to-a-trigger -[subgroups]: ../../user/group/subgroups/index.md [builds-policies]: ../yaml/README.md#only-and-except-complex [gitlab-deploy-token]: ../../user/project/deploy_tokens/index.md#gitlab-deploy-token [registry]: ../../user/project/container_registry.md diff --git a/doc/ci/variables/img/variables.png b/doc/ci/variables/img/variables.png Binary files differdeleted file mode 100644 index 0795f7c888f..00000000000 --- a/doc/ci/variables/img/variables.png +++ /dev/null diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index d4f0da52e53..4c39b14b1d0 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. @@ -1633,7 +1520,7 @@ parallel. This value has to be greater than or equal to two (2) and less than or This creates N instances of the same job that run in parallel. They're named sequentially from `job_name 1/N` to `job_name N/N`. -For every job, `CI_NODE_INDEX` and `CI_NODE_TOTAL` [environment variables](../variables/README.html#predefined-variables-environment-variables) are set. +For every job, `CI_NODE_INDEX` and `CI_NODE_TOTAL` [environment variables](../variables/README.html#predefined-environment-variables) are set. A simple example: @@ -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` @@ -1940,7 +1977,7 @@ The YAML-defined variables are also set to all created service containers, thus allowing to fine tune them. Except for the user defined variables, there are also the ones [set up by the -Runner itself](../variables/README.md#predefined-variables-environment-variables). +Runner itself](../variables/README.md#predefined-environment-variables). One example would be `CI_COMMIT_REF_NAME` which has the value of the branch or tag name for which project is built. Apart from the variables you can set in `.gitlab-ci.yml`, there are also the so called @@ -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/api_styleguide.md b/doc/development/api_styleguide.md index ce444ebdde4..4fc38a460f8 100644 --- a/doc/development/api_styleguide.md +++ b/doc/development/api_styleguide.md @@ -14,7 +14,7 @@ Always use an [Entity] to present the endpoint's payload. ## Methods and parameters description Every method must be described using the [Grape DSL](https://github.com/ruby-grape/grape#describing-methods) -(see https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/api/environments.rb +(see <https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/api/environments.rb> for a good example): - `desc` for the method summary. You should pass it a block for additional @@ -49,14 +49,14 @@ end `params` block. It filters out the params that have been passed, but are not allowed. -– https://github.com/ruby-grape/grape#declared +– <https://github.com/ruby-grape/grape#declared> ### Exclude params from parent namespaces! > By default `declared(params) `includes parameters that were defined in all parent namespaces. -– https://github.com/ruby-grape/grape#include-parent-namespaces +– <https://github.com/ruby-grape/grape#include-parent-namespaces> In most cases you will want to exclude params from the parent namespaces: 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/contributing/merge_request_workflow.md b/doc/development/contributing/merge_request_workflow.md index 6bcee74a3dd..9bef0635e3f 100644 --- a/doc/development/contributing/merge_request_workflow.md +++ b/doc/development/contributing/merge_request_workflow.md @@ -171,21 +171,21 @@ the feature you contribute through all of these steps. 1. Added to [the website](https://gitlab.com/gitlab-com/www-gitlab-com/), if relevant 1. Community questions answered 1. Answers to questions radiated (in docs/wiki/support etc.) -1. [Black-box tests/end-to-end tests](../testing_guide/testing_levels.md#black-box-tests-or-end-to-end-tests) added if required. Please contact [the quality team](https://about.gitlab.com/handbook/engineering/quality/#teams) with any questions +1. [Black-box tests/end-to-end tests](../testing_guide/testing_levels.md#black-box-tests-or-end-to-end-tests) added if required. Please contact [the quality team](https://about.gitlab.com/handbook/engineering/quality/#teams) with any questions If you add a dependency in GitLab (such as an operating system package) please consider updating the following and note the applicability of each in your merge request: -1. Note the addition in the release blog post (create one if it doesn't exist yet) https://gitlab.com/gitlab-com/www-gitlab-com/merge_requests/ -1. Upgrade guide, for example https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/update/7.5-to-7.6.md -1. Installation guide https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/install/installation.md#1-packages-dependencies -1. GitLab Development Kit https://gitlab.com/gitlab-org/gitlab-development-kit -1. Test suite https://gitlab.com/gitlab-org/gitlab-ce/blob/master/scripts/prepare_build.sh -1. Omnibus package creator https://gitlab.com/gitlab-org/omnibus-gitlab +1. Note the addition in the release blog post (create one if it doesn't exist yet) <https://gitlab.com/gitlab-com/www-gitlab-com/merge_requests/> +1. Upgrade guide, for example <https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/update/7.5-to-7.6.md> +1. Installation guide <https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/install/installation.md#1-packages-dependencies> +1. GitLab Development Kit <https://gitlab.com/gitlab-org/gitlab-development-kit> +1. Test suite <https://gitlab.com/gitlab-org/gitlab-ce/blob/master/scripts/prepare_build.sh> +1. Omnibus package creator <https://gitlab.com/gitlab-org/omnibus-gitlab> [definition-of-done]: http://guide.agilealliance.org/guide/definition-of-done.html -[testing]: ../testing_guide/index.md +[testing]: ../testing_guide/index.md --- diff --git a/doc/development/documentation/site_architecture/index.md b/doc/development/documentation/site_architecture/index.md index 9d4d2d3a28b..0ce5825fd61 100644 --- a/doc/development/documentation/site_architecture/index.md +++ b/doc/development/documentation/site_architecture/index.md @@ -11,7 +11,7 @@ and deploy it to <https://docs.gitlab.com>. While the source of the documentation content is stored in GitLab's respective product repositories, the source that is used to build the documentation site _from that content_ -is located at https://gitlab.com/gitlab-com/gitlab-docs. See the README there for +is located at <https://gitlab.com/gitlab-com/gitlab-docs>. See the README there for detailed information. ## Assets diff --git a/doc/development/documentation/styleguide.md b/doc/development/documentation/styleguide.md index 092bbdac037..cda66447c2c 100644 --- a/doc/development/documentation/styleguide.md +++ b/doc/development/documentation/styleguide.md @@ -95,6 +95,20 @@ yield a useful result, and ensuring content is helpful and easy to consume. - List item 2 ``` +### Tables overlapping the ToC + +By default, all tables have a width of 100% on docs.gitlab.com. +In a few cases, the table will overlap the table of contents (ToC). +For these cases, add an entry to the document's frontmatter to +render them displaying block. This will make sure the table +is displayed behind the ToC, scrolling horizontally: + +```md +--- +table_display_block: true +--- +``` + ## Emphasis - Use double asterisks (`**`) to mark a word or text in bold (`**bold**`). @@ -222,6 +236,15 @@ For other punctuation rules, please refer to the E.g., instead of writing something like `Read more about GitLab Issue Boards [here](LINK)`, write `Read more about [GitLab Issue Boards](LINK)`. +### Unlinking emails + +By default, all email addresses will render in an email tag on docs.gitlab.com. +To escape the code block and unlink email addresses, use two backticks: + +```md +`` example@email.com `` +``` + ## Navigation To indicate the steps of navigation through the UI: @@ -262,6 +285,16 @@ Inside the document: - If a heading is placed right after an image, always add three dashes (`---`) between the image and the heading. +### Remove image shadow + +All images displayed on docs.gitlab.com have a box shadow by default. +To remove the box shadow, use the image class `.image-noshadow` applied +directly to an HTML `img` tag: + +```html +<img src="path/to/image.jpg" alt="Alt text (required)" class="image-noshadow"> +``` + ## Code blocks - Always wrap code added to a sentence in inline code blocks (``` ` ```). @@ -389,7 +422,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. @@ -418,7 +451,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 @@ -443,7 +476,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)): @@ -456,7 +489,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: @@ -477,12 +510,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/ee_features.md b/doc/development/ee_features.md index 790b1bf951b..e0985922443 100644 --- a/doc/development/ee_features.md +++ b/doc/development/ee_features.md @@ -839,6 +839,20 @@ For example there can be an `app/assets/javascripts/protected_branches/protected_branches_bundle.js` and an EE counterpart `ee/app/assets/javascripts/protected_branches/protected_branches_bundle.js`. +The corresponding import statement would then look like this: + +```javascript +// app/assets/javascripts/protected_branches/protected_branches_bundle.js +import bundle from '~/protected_branches/protected_branches_bundle.js'; + +// ee/app/assets/javascripts/protected_branches/protected_branches_bundle.js +// (only works in EE) +import bundle from 'ee/protected_branches/protected_branches_bundle.js'; + +// in CE: app/assets/javascripts/protected_branches/protected_branches_bundle.js +// in EE: ee/app/assets/javascripts/protected_branches/protected_branches_bundle.js +import bundle from 'ee_else_ce/protected_branches/protected_branches_bundle.js'; +``` See the frontend guide [performance section](./fe_guide/performance.md) for information on managing page-specific javascript within EE. diff --git a/doc/development/fe_guide/vue.md b/doc/development/fe_guide/vue.md index ccfd465531a..9c614e3468a 100644 --- a/doc/development/fe_guide/vue.md +++ b/doc/development/fe_guide/vue.md @@ -6,9 +6,9 @@ To get started with Vue, read through [their documentation][vue-docs]. What is described in the following sections can be found in these examples: -- web ide: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/app/assets/javascripts/ide/stores -- security products: https://gitlab.com/gitlab-org/gitlab-ee/tree/master/ee/app/assets/javascripts/vue_shared/security_reports -- registry: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/app/assets/javascripts/registry/stores +- web ide: <https://gitlab.com/gitlab-org/gitlab-ce/tree/master/app/assets/javascripts/ide/stores> +- security products: <https://gitlab.com/gitlab-org/gitlab-ee/tree/master/ee/app/assets/javascripts/vue_shared/security_reports> +- registry: <https://gitlab.com/gitlab-org/gitlab-ce/tree/master/app/assets/javascripts/registry/stores> ## Vue architecture 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/gitaly.md b/doc/development/gitaly.md index 32beafad307..d5fc403bf8b 100644 --- a/doc/development/gitaly.md +++ b/doc/development/gitaly.md @@ -128,7 +128,26 @@ to manually run `make` again. Note that CI tests will not use your locally modified version of Gitaly. To use a custom Gitaly version in CI you need to update GITALY_SERVER_VERSION. You can use the format `=revision` to use a -non-tagged commit from https://gitlab.com/gitlab-org/gitaly in CI. +non-tagged commit from <https://gitlab.com/gitlab-org/gitaly> in CI. + +To use a different Gitaly repository, e.g., if your changes are present +on a fork, you can specify a `GITALY_REPO_URL` environment variable when +running tests: + +```shell +GITALY_REPO_URL=https://gitlab.com/nick.thomas/gitaly bundle exec rspec spec/lib/gitlab/git/repository_spec.rb +``` + +If your fork of Gitaly is private, you can generate a [Deploy Token](../user/project/deploy_tokens/index.md) +and specify it in the URL: + +```shell +GITALY_REPO_URL=https://gitlab+deploy-token-1000:token-here@gitlab.com/nick.thomas/gitaly bundle exec rspec spec/lib/gitlab/git/repository_spec.rb +``` + +To use a custom Gitaly repository in CI, for instance if you want your +GitLab fork to always use your own Gitaly fork, set `GITALY_REPO_URL` +as a [CI environment variable](../ci/variables/README.md#variables). --- diff --git a/doc/development/i18n/externalization.md b/doc/development/i18n/externalization.md index 6323275426f..00db58a45a2 100644 --- a/doc/development/i18n/externalization.md +++ b/doc/development/i18n/externalization.md @@ -228,6 +228,16 @@ This makes use of [`Intl.DateTimeFormat`]. [`Intl.DateTimeFormat`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DateTimeFormat +- In Ruby/HAML, we have two ways of adding format to dates and times: + + 1. **Through the `l` helper**, i.e. `l(active_session.created_at, format: :short)`. We have some predefined formats for +[dates](https://gitlab.com/gitlab-org/gitlab-ce/blob/v11.7.0/config/locales/en.yml#L54) and [times](https://gitlab.com/gitlab-org/gitlab-ce/blob/v11.7.0/config/locales/en.yml#L261). + If you need to add a new format, because other parts of the code could benefit from it, + you'll need to add it to [en.yml](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/config/locales/en.yml) file. + 2. **Through `strftime`**, i.e. `milestone.start_date.strftime('%b %-d')`. We use `strftime` in case none of the formats + defined on [en.yml](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/config/locales/en.yml) matches the date/time + specifications we need, and if there is no need to add it as a new format because is very particular (i.e. it's only used in a single view). + ## Best practices ### Splitting sentences diff --git a/doc/development/new_fe_guide/development/testing.md b/doc/development/new_fe_guide/development/testing.md index f5dfb1a31e1..d74f141f08f 100644 --- a/doc/development/new_fe_guide/development/testing.md +++ b/doc/development/new_fe_guide/development/testing.md @@ -350,7 +350,7 @@ You can find the credentials on 1Password, under `frontendteam@gitlab.com`. #### macOS -You can download any older version of Firefox from the releases FTP server, https://ftp.mozilla.org/pub/firefox/releases/ +You can download any older version of Firefox from the releases FTP server, <https://ftp.mozilla.org/pub/firefox/releases/> 1. From the website, select a version, in this case `50.0.1`. 1. Go to the mac folder. diff --git a/doc/development/ordering_table_columns.md b/doc/development/ordering_table_columns.md index e9c6481635b..3e49a65f5ab 100644 --- a/doc/development/ordering_table_columns.md +++ b/doc/development/ordering_table_columns.md @@ -30,7 +30,7 @@ ideal column order would be the following: - `user_id` (integer, 4 bytes) - `name` (text, variable) -or +or - `name` (text, variable) - `id` (integer, 4 bytes) @@ -47,8 +47,7 @@ type size in descending order with variable sizes (`text`, `varchar`, arrays, ## Type Sizes -While the PostgreSQL documentation -(https://www.postgresql.org/docs/current/static/datatype.html) contains plenty +While the [PostgreSQL documentation](https://www.postgresql.org/docs/current/datatype.html) contains plenty of information we will list the sizes of common types here so it's easier to look them up. Here "word" refers to the word size, which is 4 bytes for a 32 bits platform and 8 bytes for a 64 bits platform. @@ -138,7 +137,7 @@ This would produce the following chunks: | variable | data | Here we only need 40 bytes per row excluding the variable sized data and 24-byte -tuple header. 8 bytes being saved may not sound like much, but for tables as +tuple header. 8 bytes being saved may not sound like much, but for tables as large as the `events` table it does begin to matter. For example, when storing 80 000 000 rows this translates to a space saving of at least 610 MB, all by just changing the order of a few columns. 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/best_practices.md b/doc/development/testing_guide/best_practices.md index 24f4d457d45..4cc3812b0f0 100644 --- a/doc/development/testing_guide/best_practices.md +++ b/doc/development/testing_guide/best_practices.md @@ -121,7 +121,7 @@ failure. In CI you can download these files as job artifacts. Also, you can manually take screenshots at any point in a test by adding the methods below. Be sure to remove them when they are no longer needed! See -https://github.com/mattheworiordan/capybara-screenshot#manual-screenshots for +<https://github.com/mattheworiordan/capybara-screenshot#manual-screenshots> for more. Add `screenshot_and_save_page` in a `:js` spec to screenshot what Capybara @@ -302,7 +302,7 @@ path, they will use the same repository on disk and lead to test environment pollution. Other files must be managed manually by the spec. If you run code that creates a -`tmp/test-file.csv` file, for instance, the spec must ensure that the file is +`tmp/test-file.csv` file, for instance, the spec must ensure that the file is removed as part of cleanup. #### Persistent in-memory application state diff --git a/doc/development/testing_guide/flaky_tests.md b/doc/development/testing_guide/flaky_tests.md index bbb2313ea7b..3d568c37fba 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 @@ -15,51 +48,51 @@ examples in a JSON report file on `master` (`retrieve-tests-metadata` and `updat is detected in any other branch (`flaky-examples-check` job). In the future, the `flaky-examples-check` job will not be allowed to fail. -This was originally implemented in: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/13021. +This was originally implemented in: <https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/13021>. [rspec-retry]: https://github.com/NoRedInk/rspec-retry [`spec/spec_helper.rb`]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/spec/spec_helper.rb ## Problems we had in the past at GitLab -- [`rspec-retry` is bitting us when some API specs fail](https://gitlab.com/gitlab-org/gitlab-ce/issues/29242): https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9825 -- [Sporadic RSpec failures due to `PG::UniqueViolation`](https://gitlab.com/gitlab-org/gitlab-ce/issues/28307#note_24958837): https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9846 - - Follow-up: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10688 - - [Capybara.reset_session! should be called before requests are blocked](https://gitlab.com/gitlab-org/gitlab-ce/issues/33779): https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/12224 +- [`rspec-retry` is bitting us when some API specs fail](https://gitlab.com/gitlab-org/gitlab-ce/issues/29242): <https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9825> +- [Sporadic RSpec failures due to `PG::UniqueViolation`](https://gitlab.com/gitlab-org/gitlab-ce/issues/28307#note_24958837): <https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9846> + - Follow-up: <https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10688> + - [Capybara.reset_session! should be called before requests are blocked](https://gitlab.com/gitlab-org/gitlab-ce/issues/33779): <https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/12224> - FFaker generates funky data that tests are not ready to handle (and tests should be predictable so that's bad!): - - [Make `spec/mailers/notify_spec.rb` more robust](https://gitlab.com/gitlab-org/gitlab-ce/issues/20121): https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10015 - - [Transient failure in spec/requests/api/commits_spec.rb](https://gitlab.com/gitlab-org/gitlab-ce/issues/27988#note_25342521): https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9944 - - [Replace FFaker factory data with sequences](https://gitlab.com/gitlab-org/gitlab-ce/issues/29643): https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10184 - - [Transient failure in spec/finders/issues_finder_spec.rb](https://gitlab.com/gitlab-org/gitlab-ce/issues/30211#note_26707685): https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10404 + - [Make `spec/mailers/notify_spec.rb` more robust](https://gitlab.com/gitlab-org/gitlab-ce/issues/20121): <https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10015> + - [Transient failure in spec/requests/api/commits_spec.rb](https://gitlab.com/gitlab-org/gitlab-ce/issues/27988#note_25342521): <https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9944> + - [Replace FFaker factory data with sequences](https://gitlab.com/gitlab-org/gitlab-ce/issues/29643): <https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10184> + - [Transient failure in spec/finders/issues_finder_spec.rb](https://gitlab.com/gitlab-org/gitlab-ce/issues/30211#note_26707685): <https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10404> ### Time-sensitive flaky tests -- https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10046 -- https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10306 +- <https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10046> +- <https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10306> ### Array order expectation -- https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10148 +- <https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10148> ### Feature tests -- [Be sure to create all the data the test need before starting exercize](https://gitlab.com/gitlab-org/gitlab-ce/issues/32622#note_31128195): https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/12059 -- [Bis](https://gitlab.com/gitlab-org/gitlab-ce/issues/34609#note_34048715): https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/12604 -- [Bis](https://gitlab.com/gitlab-org/gitlab-ce/issues/34698#note_34276286): https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/12664 -- [Assert against the underlying database state instead of against a page's content](https://gitlab.com/gitlab-org/gitlab-ce/issues/31437): https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10934 +- [Be sure to create all the data the test need before starting exercize](https://gitlab.com/gitlab-org/gitlab-ce/issues/32622#note_31128195): <https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/12059> +- [Bis](https://gitlab.com/gitlab-org/gitlab-ce/issues/34609#note_34048715): <https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/12604> +- [Bis](https://gitlab.com/gitlab-org/gitlab-ce/issues/34698#note_34276286): <https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/12664> +- [Assert against the underlying database state instead of against a page's content](https://gitlab.com/gitlab-org/gitlab-ce/issues/31437): <https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10934> #### Capybara viewport size related issues -- [Transient failure of spec/features/issues/filtered_search/filter_issues_spec.rb](https://gitlab.com/gitlab-org/gitlab-ce/issues/29241#note_26743936): https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10411 +- [Transient failure of spec/features/issues/filtered_search/filter_issues_spec.rb](https://gitlab.com/gitlab-org/gitlab-ce/issues/29241#note_26743936): <https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10411> #### Capybara JS driver related issues -- [Don't wait for AJAX when no AJAX request is fired](https://gitlab.com/gitlab-org/gitlab-ce/issues/30461): https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10454 -- [Bis](https://gitlab.com/gitlab-org/gitlab-ce/issues/34647): https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/12626 +- [Don't wait for AJAX when no AJAX request is fired](https://gitlab.com/gitlab-org/gitlab-ce/issues/30461): <https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10454> +- [Bis](https://gitlab.com/gitlab-org/gitlab-ce/issues/34647): <https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/12626> #### PhantomJS / WebKit related issues -- Memory is through the roof! (TL;DR: Load images but block images requests!): https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/12003 +- Memory is through the roof! (TL;DR: Load images but block images requests!): <https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/12003> ## Resources diff --git a/doc/development/testing_guide/img/review_apps_cicd_architecture.png b/doc/development/testing_guide/img/review_apps_cicd_architecture.png Binary files differnew file mode 100644 index 00000000000..87e472076f3 --- /dev/null +++ b/doc/development/testing_guide/img/review_apps_cicd_architecture.png diff --git a/doc/development/testing_guide/review_apps.md b/doc/development/testing_guide/review_apps.md index 309babb5f94..3af97717775 100644 --- a/doc/development/testing_guide/review_apps.md +++ b/doc/development/testing_guide/review_apps.md @@ -6,39 +6,73 @@ Review Apps are automatically deployed by each pipeline, both in ## How does it work? +### CD/CD architecture diagram + +![Review Apps CI/CD architecture](img/review_apps_cicd_architecture.png) + +<details> +<summary>Show mermaid source</summary> +<pre> +graph TD + B1 -.->|2. once gitlab:assets:compile is done,<br />triggers a CNG-mirror pipeline and wait for it to be done| A2 + C1 -.->|2. once review-build-cng is done,<br />Helm deploys the Review App using the Cloud<br/>Native images built by the CNG-mirror pipeline| A3 + +subgraph gitlab-ce/ee `test` stage + A1[gitlab:assets:compile] + B1[review-build-cng] -->|1. wait for| A1 + C1[review-deploy] -->|1. wait for| B1 + D1[review-qa-smoke] -->|1. wait for| C1 + D1[review-qa-smoke] -.->|2. once review-deploy is done| E1>gitlab-qa runs the smoke<br/>suite against the Review App] + end + +subgraph CNG-mirror pipeline + A2>Cloud Native images are built]; + end + +subgraph GCP `gitlab-review-apps` project + A3>"Cloud Native images are deployed to the<br />`review-apps-ce` or `review-apps-ee` Kubernetes (GKE) cluster"]; + end +</pre> +</details> + +### Detailed explanation + 1. On every [pipeline][gitlab-pipeline] during the `test` stage, the - [`review-deploy`][review-deploy-job] job is automatically started. -1. The `review-deploy` job: - 1. Waits for the `gitlab:assets:compile` job to finish since the - [`CNG-mirror`][cng-mirror] pipeline triggerred in the following step - depends on it. - 1. [Triggers a pipeline][cng-pipeline] in the [`CNG-mirror`][cng-mirror] - project. - - We use the `CNG-mirror` project so that the `CNG`, (**C**loud - **N**ative **G**itLab), project's registry is not overloaded with a - lot of transient Docker images. - - The `CNG-mirror` pipeline creates the Docker images of each component - (e.g. `gitlab-rails-ee`, `gitlab-shell`, `gitaly` etc.) based on the - commit from the [GitLab pipeline][gitlab-pipeline] and store them in - its [registry][cng-mirror-registry]. - 1. Once all images are built by [`CNG-mirror`][cng-mirror], the Review App - is deployed using [the official GitLab Helm chart][helm-chart] to the - [`review-apps-ce`][review-apps-ce] / [`review-apps-ee`][review-apps-ee] - Kubernetes cluster on GCP. - - The actual scripts used to deploy the Review App can be found at - [`scripts/review_apps/review-apps.sh`][review-apps.sh]. - - These scripts are basically - [our official Auto DevOps scripts][Auto-DevOps.gitlab-ci.yml] where the - default CNG images are overridden with the images built and stored in the - [`CNG-mirror` project's registry][cng-mirror-registry]. - - Since we're using [the official GitLab Helm chart][helm-chart], this means - you get a dedicated environment for your branch that's very close to what - it would look in production. -1. Once the `review-deploy` job succeeds, you should be able to use your Review - App thanks to the direct link to it from the MR widget. The default username - is `root` and its password can be found in the 1Password secure note named - **gitlab-{ce,ee} Review App's root password** (note that there's currently - [a bug where the default password seems to be overridden][password-bug]). + [`review-build-cng`][review-build-cng] and + [`review-deploy`][review-deploy] jobs are automatically started. + - The [`review-deploy`][review-deploy] job waits for the + [`review-build-cng`][review-build-cng] job to finish. + - The [`review-build-cng`][review-build-cng] job waits for the + [`gitlab:assets:compile`][gitlab:assets:compile] job to finish since the + [`CNG-mirror`][cng-mirror] pipeline triggered in the following step depends on it. +1. Once the [`gitlab:assets:compile`][gitlab:assets:compile] job is done, + [`review-build-cng`][review-build-cng] [triggers a pipeline][cng-pipeline] + in the [`CNG-mirror`][cng-mirror] project. + - The [`CNG-mirror`][cng-pipeline] pipeline creates the Docker images of + each component (e.g. `gitlab-rails-ee`, `gitlab-shell`, `gitaly` etc.) + based on the commit from the [GitLab pipeline][gitlab-pipeline] and store + them in its [registry][cng-mirror-registry]. + - We use the [`CNG-mirror`][cng-mirror] project so that the `CNG`, (**C**loud + **N**ative **G**itLab), project's registry is not overloaded with a + lot of transient Docker images. +1. Once the [`review-build-cng`][review-build-cng] job is done, the + [`review-deploy`][review-deploy] job deploys the Review App using + [the official GitLab Helm chart][helm-chart] to the + [`review-apps-ce`][review-apps-ce] / [`review-apps-ee`][review-apps-ee] + Kubernetes cluster on GCP. + - The actual scripts used to deploy the Review App can be found at + [`scripts/review_apps/review-apps.sh`][review-apps.sh]. + - These scripts are basically + [our official Auto DevOps scripts][Auto-DevOps.gitlab-ci.yml] where the + default CNG images are overridden with the images built and stored in the + [`CNG-mirror` project's registry][cng-mirror-registry]. + - Since we're using [the official GitLab Helm chart][helm-chart], this means + you get a dedicated environment for your branch that's very close to what + it would look in production. +1. Once the [`review-deploy`][review-deploy] job succeeds, you should be able to + use your Review App thanks to the direct link to it from the MR widget. The + default username is `root` and its password can be found in the 1Password + secure note named **gitlab-{ce,ee} Review App's root password**. **Additional notes:** @@ -120,10 +154,13 @@ find a way to limit it to only us.** > This isn't enabled for forks. -[gitlab-pipeline]: https://gitlab.com/gitlab-org/gitlab-ce/pipelines/35850709 -[review-deploy-job]: https://gitlab.com/gitlab-org/gitlab-ce/-/jobs/118076368 +[charts-1068]: https://gitlab.com/charts/gitlab/issues/1068 +[gitlab-pipeline]: https://gitlab.com/gitlab-org/gitlab-ce/pipelines/44362587 +[gitlab:assets:compile]: https://gitlab.com/gitlab-org/gitlab-ce/-/jobs/149511610 +[review-build-cng]: https://gitlab.com/gitlab-org/gitlab-ce/-/jobs/149511623 +[review-deploy]: https://gitlab.com/gitlab-org/gitlab-ce/-/jobs/149511624 [cng-mirror]: https://gitlab.com/gitlab-org/build/CNG-mirror -[cng-pipeline]: https://gitlab.com/gitlab-org/build/CNG-mirror/pipelines/35883435 +[cng-pipeline]: https://gitlab.com/gitlab-org/build/CNG-mirror/pipelines/44364657 [cng-mirror-registry]: https://gitlab.com/gitlab-org/build/CNG-mirror/container_registry [helm-chart]: https://gitlab.com/charts/gitlab/ [review-apps-ce]: https://console.cloud.google.com/kubernetes/clusters/details/us-central1-a/review-apps-ce?project=gitlab-review-apps diff --git a/doc/development/testing_guide/testing_levels.md b/doc/development/testing_guide/testing_levels.md index a8671fc3aa3..070b6477a7a 100644 --- a/doc/development/testing_guide/testing_levels.md +++ b/doc/development/testing_guide/testing_levels.md @@ -6,7 +6,7 @@ _This diagram demonstrates the relative priority of each test type we use. `e2e` ## Unit tests -Formal definition: https://en.wikipedia.org/wiki/Unit_testing +Formal definition: <https://en.wikipedia.org/wiki/Unit_testing> These kind of tests ensure that a single unit of code (a method) works as expected (given an input, it has a predictable output). These tests should be @@ -32,7 +32,7 @@ records should use stubs/doubles as much as possible. ## Integration tests -Formal definition: https://en.wikipedia.org/wiki/Integration_testing +Formal definition: <https://en.wikipedia.org/wiki/Integration_testing> These kind of tests ensure that individual parts of the application work well together, without the overhead of the actual app environment (i.e. the browser). @@ -75,8 +75,8 @@ of multiple components). Formal definitions: -- https://en.wikipedia.org/wiki/System_testing -- https://en.wikipedia.org/wiki/White-box_testing +- <https://en.wikipedia.org/wiki/System_testing> +- <https://en.wikipedia.org/wiki/White-box_testing> These kind of tests ensure the GitLab *Rails* application (i.e. `gitlab-ce`/`gitlab-ee`) works as expected from a *browser* point of view. @@ -135,8 +135,8 @@ The reasons why we should follow these best practices are as follows: Formal definitions: -- https://en.wikipedia.org/wiki/System_testing -- https://en.wikipedia.org/wiki/Black-box_testing +- <https://en.wikipedia.org/wiki/System_testing> +- <https://en.wikipedia.org/wiki/Black-box_testing> GitLab consists of [multiple pieces] such as [GitLab Shell], [GitLab Workhorse], [Gitaly], [GitLab Pages], [GitLab Runner], and GitLab Rails. All theses pieces diff --git a/doc/gitlab-basics/create-project.md b/doc/gitlab-basics/create-project.md index 33f46e8d4f3..978ffde84ad 100644 --- a/doc/gitlab-basics/create-project.md +++ b/doc/gitlab-basics/create-project.md @@ -1,27 +1,28 @@ -# How to create a project in GitLab +# Create a project -> **Notes:** -> - For a list of words that are not allowed to be used as project names see the -> [reserved names][reserved]. +[Projects](../user/project/index.md) combine many features of GitLab together. -1. In your dashboard, click the green **New project** button or use the plus - icon in the upper right corner of the navigation bar. +NOTE: **Note:** +For a list of words that cannot be used as project names see +[Reserved project and group names](../user/reserved_names.md). - ![Create a project](img/create_new_project_button.png) +To create a project in GitLab: -1. This opens the **New project** page. +1. In your dashboard, click the green **New project** button or use the plus + icon in the navigation bar. This opens the **New project** page. +1. On the **New project** page, choose if you want to: + - Create a [blank project](#blank-projects). + - Create a project using with one of the available [project templates](#project-templates). + - [Import a project](../user/project/import/index.md) from a different repository, + if enabled on your GitLab instance. Contact your GitLab admin if this + is unavailable. - ![Project information](img/create_new_project_info.png) +## Blank projects -1. Choose if you want start a blank project, or with one of the predefined - [Project Templates](https://gitlab.com/gitlab-org/project-templates): - this will kickstart your repository code and CI automatically. - Otherwise, if you have a project in a different repository, you can [import it] by - clicking on the **Import project** tab, provided this is enabled in - your GitLab instance. Ask your administrator if not. +To create a new blank project on the **New project** page: -1. Provide the following information: - - Enter the name of your project in the **Project name** field. You can't use +1. On the **Blank project** tab, provide the following information: + - The name of your project in the **Project name** field. You can't use special characters, but you can use spaces, hyphens, underscores or even emoji. - The **Project description (optional)** field enables you to enter a @@ -31,11 +32,64 @@ - Changing the **Visibility Level** modifies the project's [viewing and access rights](../public_access/public_access.md) for users. - Selecting the **Initialize repository with a README** option creates a - README so that the Git repository is initialized, has a default branch and + README file so that the Git repository is initialized, has a default branch, and can be cloned. - 1. Click **Create project**. +## Project templates + +Project templates can pre-populate your project with necessary files to get you started quickly. + +There are two types of project templates: + +- [Built-in templates](#builtin-templates), sourced from the [`project-templates`](https://gitlab.com/gitlab-org/project-templates) group. +- [Custom project templates](#custom-project-templates-premium-only), for custom templates configured by GitLab administrators and users. + +### Built-in templates + +Built-in templates are project templates that are: + +- Developed and maintained in the + [`project-templates`](https://gitlab.com/gitlab-org/project-templates) group. +- Released with GitLab. + +To use a built-in template on the **New project** page: + +1. On the **Create from template** tab. +1. From the list of available built-in templates, click the: + - **Preview** button to look at the template source itself. + - **Use template** button to start creating the project. +1. Finish creating the project by filling out the project's details. The process is the same as for + using a [blank project](#blank-projects). + +TIP: **Tip:** +You can improve the existing built-in templates or contribute new ones on the +[`project-templates`](https://gitlab.com/gitlab-org/project-templates) group. + +### Custom project templates **[PREMIUM ONLY]** + +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/6860) in +[GitLab Premium](https://about.gitlab.com/pricing) 11.2. + +Creating new projects based on custom project templates is a convenient option to bootstrap a project. + +Custom projects are available from the **Instance** or **Group** tabs under the **Create from template** tab, +depending on the type of template. + +To use a custom project template on the **New project** page: + +1. On the **Create from template** tab, select the **Instance** tab or the **Group** tab. +1. From the list of available custom templates, click the: + - **Preview** button to look at the template source itself. + - **Use template** button to start creating the project. +1. Finish creating the project by filling out the project's details. The process is the same as for + using a [blank project](#blank-projects). + +For information on configuring custom project templates, see: + +- [Custom instance-level project templates](../user/admin_area/custom_project_templates.md), for instance-level templates. +- [Custom group-level project templates](../user/group/custom_project_templates.md), for group-level templates. + ## Push to create a new project > [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/26388) in GitLab 10.5. @@ -46,20 +100,20 @@ GitLab to create the new project, all without leaving your terminal. If you have namespace, we will automatically create a new project under that GitLab namespace with its visibility set to Private by default (you can later change it in the [project's settings](../public_access/public_access.md#how-to-change-project-visibility)). -This can be done by using either SSH or HTTP: +This can be done by using either SSH or HTTPS: -``` +```sh ## Git push using SSH git push --set-upstream git@gitlab.example.com:namespace/nonexistent-project.git master -## Git push using HTTP +## Git push using HTTPS git push --set-upstream https://gitlab.example.com/namespace/nonexistent-project.git master ``` Once the push finishes successfully, a remote message will indicate the command to set the remote and the URL to the new project: -``` +```text remote: remote: The private project namespace/nonexistent-project was created. remote: @@ -70,6 +124,3 @@ remote: To view the project, visit: remote: https://gitlab.example.com/namespace/nonexistent-project remote: ``` - -[import it]: ../workflow/importing/README.md -[reserved]: ../user/reserved_names.md diff --git a/doc/gitlab-basics/img/create_new_project_button.png b/doc/gitlab-basics/img/create_new_project_button.png Binary files differdeleted file mode 100644 index 567f104880f..00000000000 --- a/doc/gitlab-basics/img/create_new_project_button.png +++ /dev/null diff --git a/doc/gitlab-basics/img/create_new_project_info.png b/doc/gitlab-basics/img/create_new_project_info.png Binary files differdeleted file mode 100644 index 2693a7f9a6d..00000000000 --- a/doc/gitlab-basics/img/create_new_project_info.png +++ /dev/null diff --git a/doc/gitlab-basics/start-using-git.md b/doc/gitlab-basics/start-using-git.md index 0d9994c9925..e30afdf8a40 100644 --- a/doc/gitlab-basics/start-using-git.md +++ b/doc/gitlab-basics/start-using-git.md @@ -68,6 +68,54 @@ git config --global --list ## Basic Git commands +Start using Git via the command line with the most basic +commands as described below. + +### Clone a repository + +To start working locally on an existing remote repository, +clone it with the command `git clone <repository path>`. +By cloning a repository, you'll download a copy of its +files into your local computer, preserving the Git +connection with the remote repository. + +You can either clone it via HTTPS or [SSH](../ssh/README.md). +If you chose to clone it via HTTPS, you'll have to enter your +credentials every time you pull and push. With SSH, you enter +your credentials once and can pull and push straightaway. + +You can find both paths (HTTPS and SSH) by navigating to +your project's landing page and clicking **Clone**. GitLab +will prompt you with both paths, from which you can copy +and paste in your command line. + +As an example, consider a repository path: + +- HTTPS: `https://gitlab.com/gitlab-org/gitlab-ce.git` +- SSH: `` git@gitlab.com:gitlab-org/gitlab-ce.git `` + +To get started, open a terminal window in the directory +you wish to clone the repository files into, and run one +of the following commands. + +Clone via HTTPS: + +```bash +git clone https://gitlab.com/gitlab-org/gitlab-ce.git +``` + +Clone via SSH: + +```bash +git clone git@gitlab.com:gitlab-org/gitlab-ce.git +``` + +Both commands will download a copy of the files in a +folder named after the project's name. + +You can then navigate to the directory and start working +on it locally. + ### Go to the master branch to pull the latest changes from there ```bash diff --git a/doc/install/aws/index.md b/doc/install/aws/index.md index ce61ace60a0..e209a00b38c 100644 --- a/doc/install/aws/index.md +++ b/doc/install/aws/index.md @@ -60,7 +60,7 @@ Here's a list of the AWS services we will use, with links to pricing information To minimize the permissions of the user, we'll create a new [IAM](https://docs.aws.amazon.com/IAM/latest/UserGuide/introduction.html) role with limited access: -1. Navigate to the IAM dashboard https://console.aws.amazon.com/iam/home and +1. Navigate to the IAM dashboard <https://console.aws.amazon.com/iam/home> and click **Create role**. 1. Create a new role by selecting **AWS service > EC2**, then click **Next: Permissions**. @@ -78,7 +78,7 @@ Internet Gateway. We'll now create a VPC, a virtual networking environment that you'll control: -1. Navigate to https://console.aws.amazon.com/vpc/home. +1. Navigate to <https://console.aws.amazon.com/vpc/home>. 1. Select **Your VPCs** from the left menu and then click **Create VPC**. At the "Name tag" enter `gitlab-vpc` and at the "IPv4 CIDR block" enter `10.0.0.0/16`. If you don't require dedicated hardware, you can leave diff --git a/doc/install/google_cloud_platform/index.md b/doc/install/google_cloud_platform/index.md index ab5f7507f24..aa4b3dccf7d 100644 --- a/doc/install/google_cloud_platform/index.md +++ b/doc/install/google_cloud_platform/index.md @@ -22,14 +22,14 @@ Once you have performed those two steps, you can [create a VM](#creating-the-vm) To deploy GitLab on GCP you need to follow five simple steps: -1. Go to https://console.cloud.google.com/compute/instances and login with your Google credentials. +1. Go to <https://console.cloud.google.com/compute/instances> and login with your Google credentials. 1. Click on **Create** ![Search for GitLab](img/launch_vm.png) 1. On the next page, you can select the type of VM as well as the - estimated costs. Provide the name of the instance, desired datacenter, and machine type. Note that GitLab recommends at least 2 vCPU's and 4GB of RAM. + estimated costs. Provide the name of the instance, desired datacenter, and machine type. Note that GitLab recommends at least 2 vCPU's and 4GB of RAM. ![Launch on Compute Engine](img/vm_details.png) @@ -51,7 +51,7 @@ After a few seconds, the instance will be created and available to log in. The n ![GitLab first sign in](img/ssh_terminal.png) -1. Next, follow the instructions for installing GitLab for the operating system you choose, at https://about.gitlab.com/installation/. You can use the IP address from the step above, as the hostname. +1. Next, follow the instructions for installing GitLab for the operating system you choose, at <https://about.gitlab.com/installation/>. You can use the IP address from the step above, as the hostname. 1. Congratulations! GitLab is now installed and you can access it via your browser. To finish installation, open the URL in your browser and provide the initial administrator password. The username for this account is `root`. diff --git a/doc/install/installation.md b/doc/install/installation.md index 678c306a484..1f65e3415d1 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -6,7 +6,8 @@ Since an installation from source is a lot of work and error prone we strongly r One reason the Omnibus package is more reliable is its use of Runit to restart any of the GitLab processes in case one crashes. On heavily used GitLab instances the memory usage of the Sidekiq background worker will grow over time. -Omnibus packages solve this by [letting the Sidekiq terminate gracefully](http://docs.gitlab.com/ce/operations/sidekiq_memory_killer.html) if it uses too much memory. + +Omnibus packages solve this by [letting the Sidekiq terminate gracefully](../administration/operations/sidekiq_memory_killer.md) if it uses too much memory. After this termination Runit will detect Sidekiq is not running and will start it. Since installations from source don't have Runit, Sidekiq can't be terminated and its memory usage will grow over time. @@ -15,19 +16,19 @@ Since installations from source don't have Runit, Sidekiq can't be terminated an Make sure you view [this installation guide](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/install/installation.md) from the branch (version) of GitLab you would like to install (e.g., `11-7-stable`). You can select the branch in the version dropdown in 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/) for installation guide links by version. +If the highest number stable branch is unclear, check the [GitLab blog](https://about.gitlab.com/blog/) for installation guide links by version. ## Important Notes This guide is long because it covers many cases and includes all commands you need, this is [one of the few installation scripts that actually works out of the box](https://twitter.com/robinvdvleuten/status/424163226532986880). -This installation guide was created for and tested on **Debian/Ubuntu** operating systems. Please read [requirements.md](requirements.md) for hardware and operating system requirements. If you want to install on RHEL/CentOS we recommend using the [Omnibus packages](https://about.gitlab.com/downloads/). +This installation guide was created for and tested on **Debian/Ubuntu** operating systems. Read [requirements.md](requirements.md) for hardware and operating system requirements. If you want to install on RHEL/CentOS, we recommend using the [Omnibus packages](https://about.gitlab.com/downloads/). -This is the official installation guide to set up a production server. To set up a **development installation** or for many other installation options please see [the installation section of the readme](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/README.md#installation). +This is the official installation guide to set up a production server. To set up a **development installation** or for many other installation options, see [the installation section of the README](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/README.md#installation). -The following steps have been known to work. Please **use caution when you deviate** from this guide. Make sure you don't violate any assumptions GitLab makes about its environment. For example many people run into permission problems because they changed the location of directories or run services as the wrong user. +The following steps have been known to work. **Use caution when you deviate** from this guide. Make sure you don't violate any assumptions GitLab makes about its environment. For example, many people run into permission problems because they changed the location of directories or run services as the wrong user. -If you find a bug/error in this guide please **submit a merge request** +If you find a bug/error in this guide, **submit a merge request** following the [contributing guide](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md). @@ -35,17 +36,17 @@ following the The GitLab installation consists of setting up the following components: -1. Packages / Dependencies -1. Ruby -1. Go -1. Node -1. System Users -1. Database -1. Redis -1. GitLab -1. Nginx +1. [Packages and dependencies](#1-packages-and-dependencies). +1. [Ruby](#2-ruby). +1. [Go](#3-go). +1. [Node](#4-node). +1. [System users](#5-system-users). +1. [Database](#6-database). +1. [Redis](#7-redis). +1. [GitLab](#8-gitlab). +1. [Nginx](#9-nginx). -## 1. Packages / Dependencies +## 1. Packages and dependencies `sudo` is not installed on Debian by default. Make sure your system is up-to-date and install it. @@ -57,7 +58,8 @@ 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. +NOTE: **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, skip this and keep using the default editor. ```sh # Install vim and set as default editor @@ -76,15 +78,16 @@ sudo apt-get install -y build-essential zlib1g-dev libyaml-dev libssl-dev libgdb 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: +If you want to use Kerberos for user authentication, 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. +NOTE: **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 +Make sure you have the right version of Git installed: ```sh # Install Git @@ -117,6 +120,13 @@ sudo make prefix=/usr/local install # When editing config/gitlab.yml (Step 5), change the git -> bin_path to /usr/local/bin/git ``` +For the [Custom Favicon](../customization/favicon.md) to work, GraphicsMagick +needs to be installed. + +```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: ```sh @@ -160,10 +170,10 @@ make sudo make install ``` -Then install the Bundler Gem: +Then install the Bundler gem (a version below 2.x): ```sh -sudo gem install bundler --no-document +sudo gem install bundler --no-document --version '< 2' ``` ## 3. Go @@ -186,9 +196,14 @@ 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 >= v8.10.0 and yarn >= v1.10.0. In many distros +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` >= v8.10.0. +- `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: @@ -205,7 +220,7 @@ 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. -## 5. System Users +## 5. System users Create a `git` user for GitLab: @@ -215,11 +230,10 @@ sudo adduser --disabled-login --gecos 'GitLab' git ## 6. Database -We recommend using a PostgreSQL database. For MySQL check the -[MySQL setup guide](database_mysql.md). +We recommend using a PostgreSQL database. For MySQL, see the [MySQL setup guide](database_mysql.md). -> **Note**: because we need to make use of extensions and concurrent index removal, -you need at least PostgreSQL 9.2. +NOTE: **Note:** +Because we need to make use of extensions and concurrent index removal, you need at least PostgreSQL 9.2. 1. Install the database packages: @@ -279,7 +293,7 @@ you need at least PostgreSQL 9.2. GitLab requires at least Redis 2.8. -If you are using Debian 8 or Ubuntu 14.04 and up, then you can simply install +If you are using Debian 8 or Ubuntu 14.04 and up, you can simply install Redis 2.8 with: ```sh @@ -334,7 +348,8 @@ cd /home/git 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! +CAUTION: **Caution:** +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 @@ -412,9 +427,11 @@ sudo -u git -H cp config/resque.yml.example config/resque.yml sudo -u git -H editor config/resque.yml ``` -**Important Note:** Make sure to edit both `gitlab.yml` and `unicorn.rb` to match your setup. +CAUTION: **Caution:** +Make sure to edit both `gitlab.yml` and `unicorn.rb` to match your setup. -**Note:** If you want to use HTTPS, see [Using HTTPS](#using-https) for the additional steps. +NOTE: **Note:** +If you want to use HTTPS, see [Using HTTPS](#using-https) for the additional steps. ### Configure GitLab DB Settings @@ -440,7 +457,13 @@ 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. +NOTE: **Note:** +As of Bundler 1.5.2, you can invoke `bundle install -jN` (where `N` is the number of your processor cores) and enjoy parallel gems installation with measurable difference in completion time (~60% faster). Check the number of your cores with `nproc`. For more information, see this [post](https://robots.thoughtbot.com/parallel-gem-installing-using-bundler). + +Make sure you have `bundle` (run `bundle -v`): + +- `>= 1.5.2`, because some [issues](https://devcenter.heroku.com/changelog-items/411) were [fixed](https://github.com/bundler/bundler/pull/2817) in 1.5.2. +- `< 2.x`. ```sh # For PostgreSQL (note, the option says "without ... mysql") @@ -450,7 +473,8 @@ sudo -u git -H bundle install --deployment --without development test mysql aws 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. +NOTE: **Note:** +If you want to use Kerberos for user authentication, omit `kerberos` in the `--without` option above. ### Install GitLab Shell @@ -465,11 +489,14 @@ sudo -u git -H bundle exec rake gitlab:shell:install REDIS_URL=unix:/var/run/red 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. +NOTE: **Note:** +If you want to use HTTPS, see [Using HTTPS](#using-https) for the additional steps. -**Note:** Make sure your hostname can be resolved on the machine itself by either a proper DNS record or an additional line in /etc/hosts ("127.0.0.1 hostname"). This might be necessary for example if you set up GitLab behind a reverse proxy. If the hostname cannot be resolved, the final installation check will fail with "Check GitLab API access: FAILED. code: 401" and pushing commits will be rejected with "[remote rejected] master -> master (hook declined)". +NOTE: **Note:** +Make sure your hostname can be resolved on the machine itself by either a proper DNS record or an additional line in `/etc/hosts` ("127.0.0.1 hostname"). This might be necessary, for example, if you set up GitLab behind a reverse proxy. If the hostname cannot be resolved, the final installation check will fail with "Check GitLab API access: FAILED. code: 401" and pushing commits will be rejected with "[remote rejected] master -> master (hook declined)". -**Note:** GitLab Shell application startup time can be greatly reduced by disabling RubyGems. This can be done in several manners: +NOTE: **Note:** +GitLab Shell application startup time can be greatly reduced by disabling RubyGems. This can be done in several ways: - Export `RUBYOPT=--disable-gems` environment variable for the processes. - Compile Ruby with `configure --disable-rubygems` to disable RubyGems by default. Not recommended for system-wide Ruby. @@ -491,9 +518,9 @@ 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 ``` -### Install gitlab-pages +### 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. +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, 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 run several different ways. ```sh cd /home/git @@ -543,7 +570,8 @@ sudo -u git -H bundle exec rake gitlab:setup RAILS_ENV=production force=yes # 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. +NOTE: **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), wait to expose 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. ```sh sudo -u git -H bundle exec rake gitlab:setup RAILS_ENV=production GITLAB_ROOT_PASSWORD=yourpassword GITLAB_ROOT_EMAIL=youremail @@ -569,7 +597,7 @@ And if you are installing with a non-default folder or user copy and edit the de 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. +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: @@ -614,7 +642,8 @@ sudo /etc/init.d/gitlab restart ## 9. Nginx -**Note:** Nginx is the officially supported web server for GitLab. If you cannot or do not want to use Nginx as your web server, have a look at the [GitLab recipes](https://gitlab.com/gitlab-org/gitlab-recipes/). +NOTE: **Note:** +Nginx is the officially supported web server for GitLab. If you cannot or do not want to use Nginx as your web server, see [GitLab recipes](https://gitlab.com/gitlab-org/gitlab-recipes/). ### Installation @@ -631,7 +660,7 @@ 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: +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: ```sh # Change YOUR_SERVER_FQDN to the fully-qualified @@ -678,7 +707,7 @@ 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 ``` -If all items are green, then congratulations on successfully installing GitLab! +If all items are green, congratulations on successfully installing GitLab! NOTE: Supply `SANITIZE=true` environment variable to `gitlab:check` to omit project names from the output of the check command. @@ -720,11 +749,11 @@ To use GitLab with HTTPS: 1. Update `ssl_certificate` and `ssl_certificate_key`. 1. Review the configuration file and consider applying other security and performance enhancing features. -Using a self-signed certificate is discouraged but if you must use it follow the normal directions then: +Using a self-signed certificate is discouraged but if you must use it, follow the normal directions. Then: 1. Generate a self-signed SSL certificate: - ``` + ```sh mkdir -p /etc/nginx/ssl/ cd /etc/nginx/ssl/ sudo openssl req -newkey rsa:2048 -x509 -nodes -days 3560 -out gitlab.crt -keyout gitlab.key @@ -738,16 +767,16 @@ See the ["Reply by email" documentation](../administration/reply_by_email.md) fo ### LDAP Authentication -You can configure LDAP authentication in `config/gitlab.yml`. Please restart GitLab after editing this file. +You can configure LDAP authentication in `config/gitlab.yml`. Restart GitLab after editing this file. ### Using Custom Omniauth Providers -See the [omniauth integration document](../integration/omniauth.md) +See the [omniauth integration document](../integration/omniauth.md). ### Build your projects -GitLab can build your projects. To enable that feature you need GitLab Runners to do that for you. -Checkout the [GitLab Runner section](https://about.gitlab.com/gitlab-ci/#gitlab-runner) to install it +GitLab can build your projects. To enable that feature, you need GitLab Runners to do that for you. +See the [GitLab Runner section](https://about.gitlab.com/product/continuous-integration/#gitlab-runner) to install it. ### Adding your Trusted Proxies @@ -769,7 +798,7 @@ 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. +If you want to connect the Redis server via socket, use the "unix:" URL scheme and the path to the Redis socket file in the `config/resque.yml` file. ``` # example @@ -801,7 +830,7 @@ You also need to change the corresponding options (e.g. `ssh_user`, `ssh_host`, ### Additional Markup Styles -Apart from the always supported markdown style there are other rich text files that GitLab can display. But you might have to install a dependency to do so. Please see the [github-markup gem readme](https://github.com/gitlabhq/markup#markups) for more information. +Apart from the always supported markdown style, there are other rich text files that GitLab can display. But you might have to install a dependency to do so. See the [github-markup gem README](https://github.com/gitlabhq/markup#markups) for more information. ## Troubleshooting diff --git a/doc/install/kubernetes/gitlab_omnibus.md b/doc/install/kubernetes/gitlab_omnibus.md index 2d9c7f15634..2ae5485869e 100644 --- a/doc/install/kubernetes/gitlab_omnibus.md +++ b/doc/install/kubernetes/gitlab_omnibus.md @@ -7,7 +7,7 @@ instead. A comparison of the two charts is available in [this video](https://you For more information on available GitLab Helm Charts, see the [charts overview](index.md#chart-overview). - This GitLab-Omnibus chart has been tested on Google Kubernetes Engine and Azure Container Service. -- This work is based partially on: https://github.com/lwolf/kubernetes-gitlab/. GitLab would like to thank Sergey Nuzhdin for his work. +- This work is based partially on: <https://github.com/lwolf/kubernetes-gitlab/>. GitLab would like to thank Sergey Nuzhdin for his work. ## Introduction diff --git a/doc/integration/akismet.md b/doc/integration/akismet.md index 200fe6f5206..35024a78fca 100644 --- a/doc/integration/akismet.md +++ b/doc/integration/akismet.md @@ -18,7 +18,7 @@ from happening. To use Akismet: -1. Go to the URL: https://akismet.com/account/ +1. Go to the URL: <https://akismet.com/account/> 1. Sign-in or create a new account. diff --git a/doc/integration/auth0.md b/doc/integration/auth0.md index bccaeec3706..e2ed7a4b1ab 100644 --- a/doc/integration/auth0.md +++ b/doc/integration/auth0.md @@ -21,12 +21,12 @@ configuration file. For example: - Client Secret: `KbveM3nqfjwCbrhaUy_gDu2dss8TIlHIdzlyf33pB7dEK5u_NyQdp65O_o02hXs2` 1. Fill in the Allowed Callback URLs: - - http://`YOUR_GITLAB_URL`/users/auth/auth0/callback (or) - - https://`YOUR_GITLAB_URL`/users/auth/auth0/callback + - `http://YOUR_GITLAB_URL/users/auth/auth0/callback` (or) + - `https://YOUR_GITLAB_URL/users/auth/auth0/callback` 1. Fill in the Allowed Origins (CORS): - - http://`YOUR_GITLAB_URL` (or) - - https://`YOUR_GITLAB_URL` + - `http://YOUR_GITLAB_URL` (or) + - `https://YOUR_GITLAB_URL` 1. On your GitLab server, open the configuration file. diff --git a/doc/integration/azure.md b/doc/integration/azure.md index 634dd952448..7a6d4bb143f 100644 --- a/doc/integration/azure.md +++ b/doc/integration/azure.md @@ -15,12 +15,12 @@ To enable the Microsoft Azure OAuth2 OmniAuth provider you must register your ap - Type: 'WEB APPLICATION AND/OR WEB API' 1. On the "App properties" page enter the needed URI's and click the "Complete" button. - - SIGN-IN URL: Enter the URL of your GitLab installation (e.g 'https://gitlab.mycompany.com/') - - APP ID URI: Enter the endpoint URL for Microsoft to use, just has to be unique (e.g 'https://mycompany.onmicrosoft.com/gitlab') + - SIGN-IN URL: Enter the URL of your GitLab installation (e.g `https://gitlab.mycompany.com/`) + - APP ID URI: Enter the endpoint URL for Microsoft to use, just has to be unique (e.g `https://mycompany.onmicrosoft.com/gitlab`) 1. Select "Configure" in the top menu. -1. Add a "Reply URL" pointing to the Azure OAuth callback of your GitLab installation (e.g. https://gitlab.mycompany.com/users/auth/azure_oauth2/callback). +1. Add a "Reply URL" pointing to the Azure OAuth callback of your GitLab installation (e.g. `https://gitlab.mycompany.com/users/auth/azure_oauth2/callback`). 1. Create a "Client secret" by selecting a duration, the secret will be generated as soon as you click the "Save" button in the bottom menu.. @@ -28,7 +28,7 @@ To enable the Microsoft Azure OAuth2 OmniAuth provider you must register your ap 1. Select "View endpoints" from the bottom menu. -1. You will see lots of endpoint URLs in the form 'https://login.microsoftonline.com/TENANT ID/...', note down the TENANT ID part of one of those endpoints. +1. You will see lots of endpoint URLs in the form `https://login.microsoftonline.com/TENANT ID/...`, note down the TENANT ID part of one of those endpoints. 1. On your GitLab server, open the configuration file. diff --git a/doc/integration/bitbucket.md b/doc/integration/bitbucket.md index a69db1d1a6e..68ec8c4b5c2 100644 --- a/doc/integration/bitbucket.md +++ b/doc/integration/bitbucket.md @@ -43,9 +43,13 @@ you to use. | :--- | :---------- | | **Name** | This can be anything. Consider something like `<Organization>'s GitLab` or `<Your Name>'s GitLab` or something else descriptive. | | **Application description** | Fill this in if you wish. | - | **Callback URL** | The URL to your GitLab installation, e.g., `https://gitlab.example.com`. | + | **Callback URL** | The URL to your GitLab installation, e.g., `https://gitlab.example.com/users/auth`. | | **URL** | The URL to your GitLab installation, e.g., `https://gitlab.example.com`. | + NOTE: Be sure to append `/users/auth` to the end of the callback URL + to prevent a [OAuth2 convert + redirect](http://tetraph.com/covert_redirect/) vulnerability. + NOTE: Starting in GitLab 8.15, you MUST specify a callback URL, or you will see an "Invalid redirect_uri" message. For more details, see [the Bitbucket documentation](https://confluence.atlassian.com/bitbucket/oauth-faq-338365710.html). diff --git a/doc/integration/github.md b/doc/integration/github.md index b8156b2b593..eca9aa16499 100644 --- a/doc/integration/github.md +++ b/doc/integration/github.md @@ -21,9 +21,13 @@ To get the credentials (a pair of Client ID and Client Secret), you must registe - Application name: This can be anything. Consider something like `<Organization>'s GitLab` or `<Your Name>'s GitLab` or something else descriptive. - Homepage URL: the URL to your GitLab installation. e.g., `https://gitlab.company.com` - Application description: Fill this in if you wish. - - Authorization callback URL: `http(s)://${YOUR_DOMAIN}`. Please make sure the port is included if your GitLab instance is not configured on default port. + - Authorization callback URL: `http(s)://${YOUR_DOMAIN}/users/auth`. Please make sure the port is included if your GitLab instance is not configured on default port. ![Register OAuth App](img/github_register_app.png) + NOTE: Be sure to append `/users/auth` to the end of the callback URL + to prevent a [OAuth2 convert + redirect](http://tetraph.com/covert_redirect/) vulnerability. + 1. Select **Register application**. 1. You should now see a pair of **Client ID** and **Client Secret** near the top right of the page (see screenshot). diff --git a/doc/integration/saml.md b/doc/integration/saml.md index a7470d27b4b..bb3cd9a005f 100644 --- a/doc/integration/saml.md +++ b/doc/integration/saml.md @@ -287,7 +287,7 @@ so you will not be able to sign in using local credentials. Make sure that at le of the SAML users has admin permissions. You may also bypass the auto signin feature by browsing to -https://gitlab.example.com/users/sign_in?auto_sign_in=false. +`https://gitlab.example.com/users/sign_in?auto_sign_in=false`. ### `attribute_statements` diff --git a/doc/integration/twitter.md b/doc/integration/twitter.md index d0976b6201e..1cbfd81dfa9 100644 --- a/doc/integration/twitter.md +++ b/doc/integration/twitter.md @@ -10,8 +10,8 @@ To enable the Twitter OmniAuth provider you must register your application with - Name: This can be anything. Consider something like `<Organization>'s GitLab` or `<Your Name>'s GitLab` or something else descriptive. - Description: Create a description. - - Website: The URL to your GitLab installation. 'https://gitlab.example.com' - - Callback URL: 'https://gitlab.example.com/users/auth/twitter/callback' + - Website: The URL to your GitLab installation. `https://gitlab.example.com` + - Callback URL: `https://gitlab.example.com/users/auth/twitter/callback` - Agree to the "Developer Agreement". ![Twitter App Details](img/twitter_app_details.png) diff --git a/doc/migrate_ci_to_ce/README.md b/doc/migrate_ci_to_ce/README.md index 9347a834510..4c4b423f40f 100644 --- a/doc/migrate_ci_to_ce/README.md +++ b/doc/migrate_ci_to_ce/README.md @@ -71,7 +71,7 @@ sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production SKIP=r ``` If this fails you need to fix it before upgrading to 8.0. Also see -https://about.gitlab.com/getting-help/ +<https://about.gitlab.com/get-help/> ### 2. Check source and target database types @@ -118,7 +118,7 @@ From this point on, GitLab CI will be unavailable for your end users. ### 1. Upgrade GitLab to 8.0 First upgrade your GitLab server to version 8.0: -https://about.gitlab.com/update/ +<https://about.gitlab.com/update/> ### 2. Disable CI on the GitLab server during the migration diff --git a/doc/policy/maintenance.md b/doc/policy/maintenance.md index 1b93cdb83ac..1d656574acd 100644 --- a/doc/policy/maintenance.md +++ b/doc/policy/maintenance.md @@ -3,21 +3,23 @@ ## Versioning GitLab follows the [Semantic Versioning](http://semver.org/) for its releases: -`(Major).(Minor).(Patch)` in a [pragmatic way]. - -- **Major version**: Whenever there is something significant or any backwards - incompatible changes are introduced to the public API. -- **Minor version**: When new, backwards compatible functionality is introduced - to the public API or a minor feature is introduced, or when a set of smaller - features is rolled out. -- **Patch number**: When backwards compatible bug fixes are introduced that fix - incorrect behavior. +`(Major).(Minor).(Patch)` in a [pragmatic way](https://gist.github.com/jashkenas/cbd2b088e20279ae2c8e). For example, for GitLab version 10.5.7: -- `10` represents major version -- `5` represents minor version -- `7` represents patch number +- `10` represents the major version. The major release was 10.0.0, but often referred to as 10.0. +- `5` represents the minor version. The minor release was 10.5.0, but often referred to as 10.5. +- `7` represents the patch number. + +Any part of the version number can increment into multiple digits, for example, 13.10.11. + +The following table describes the version types and their release cadence: + +| Version type | Description | Cadence | +|:-------------|:----------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------| +| Major | For significant changes, or when any backward-incompatible changes are introduced to the public API. | Yearly. The next major release is GitLab 12.0 on June 22, 2019. Subsequent major releases will be scheduled for May 22 each year, by default. | | +| Minor | For when new backward-compatible functionality is introduced to the public API, a minor feature is introduced, or when a set of smaller features is rolled out. | Monthly on the 22nd. | +| Patch | For backward-compatible bug fixes that fix incorrect behavior. See [Patch releases](#patch-releases). | As needed. | ## Patch releases @@ -68,7 +70,7 @@ We cannot guarantee that upgrading between major versions will be seamless. As p We recommend that you first upgrade to the latest available minor version within your major version. By doing this, you can address any deprecation messages -that could possibly change behaviour in the next major release. +that could change behavior in the next major release. Please see the table below for some examples: @@ -79,9 +81,5 @@ Please see the table below for some examples: | 11.3.4 | 8.13.4 | `8.13.4` -> `8.17.7` -> `9.5.10` -> `10.8.7` -> `11.3.4` | `8.17.7` is the last version in version `8`, `9.5.10` is the last version in version `9`, `10.8.7` is the last version in version `10` | More information about the release procedures can be found in our -[release-tools documentation][rel]. You may also want to read our -[Responsible Disclosure Policy][disclosure]. - -[rel]: https://gitlab.com/gitlab-org/release-tools/blob/master/doc/ -[disclosure]: https://about.gitlab.com/disclosure/ -[pragmatic way]: https://gist.github.com/jashkenas/cbd2b088e20279ae2c8e +[release documentation](https://gitlab.com/gitlab-org/release/docs). You may also want to read our +[Responsible Disclosure Policy](https://about.gitlab.com/security/disclosure/). diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md index bb28ca35a26..037b71a27b9 100644 --- a/doc/raketasks/backup_restore.md +++ b/doc/raketasks/backup_restore.md @@ -378,7 +378,7 @@ with the name of your bucket: If you want to use Google Cloud Storage to save backups, you'll have to create an access key from the Google console first: -1. Go to the storage settings page https://console.cloud.google.com/storage/settings +1. Go to the storage settings page <https://console.cloud.google.com/storage/settings> 1. Select "Interoperability" and create an access key 1. Make note of the "Access Key" and "Secret" and replace them in the configurations below diff --git a/doc/security/webhooks.md b/doc/security/webhooks.md index fb2b6768f0a..8c26bbac6a7 100644 --- a/doc/security/webhooks.md +++ b/doc/security/webhooks.md @@ -6,9 +6,9 @@ With [Webhooks](../user/project/integrations/webhooks.md), you and your project Things get hairy, however, when a Webhook is set up with a URL that doesn't point to an external, but to an internal service, that may do something completely unintended when the webhook is triggered and the POST request is sent. -Because Webhook requests are made by the GitLab server itself, these have complete access to everything running on the server (http://localhost:123) or within the server's local network (http://192.168.1.12:345), even if these services are otherwise protected and inaccessible from the outside world. +Because Webhook requests are made by the GitLab server itself, these have complete access to everything running on the server (`http://localhost:123`) or within the server's local network (`http://192.168.1.12:345`), even if these services are otherwise protected and inaccessible from the outside world. -If a web service does not require authentication, Webhooks can be used to trigger destructive commands by getting the GitLab server to make POST requests to endpoints like "http://localhost:123/some-resource/delete". +If a web service does not require authentication, Webhooks can be used to trigger destructive commands by getting the GitLab server to make POST requests to endpoints like `http://localhost:123/some-resource/delete`. 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. diff --git a/doc/ssh/README.md b/doc/ssh/README.md index e570627bfc1..09a97fcea07 100644 --- a/doc/ssh/README.md +++ b/doc/ssh/README.md @@ -318,7 +318,7 @@ not implicitly give any access just by setting them up. ### Eclipse -How to add your SSH key to Eclipse: https://wiki.eclipse.org/EGit/User_Guide#Eclipse_SSH_Configuration +How to add your SSH key to Eclipse: <https://wiki.eclipse.org/EGit/User_Guide#Eclipse_SSH_Configuration> ## SSH on the GitLab server diff --git a/doc/system_hooks/system_hooks.md b/doc/system_hooks/system_hooks.md index cce02d218c2..6d63906ea4d 100644 --- a/doc/system_hooks/system_hooks.md +++ b/doc/system_hooks/system_hooks.md @@ -272,7 +272,7 @@ If the user is blocked via LDAP, `state` will be `ldap_blocked`. } ``` -`owner_name` and `owner_email` are always `null`. Please see https://gitlab.com/gitlab-org/gitlab-ce/issues/39675. +`owner_name` and `owner_email` are always `null`. Please see <https://gitlab.com/gitlab-org/gitlab-ce/issues/39675>. **Group removed:** @@ -289,7 +289,7 @@ If the user is blocked via LDAP, `state` will be `ldap_blocked`. } ``` -`owner_name` and `owner_email` are always `null`. Please see https://gitlab.com/gitlab-org/gitlab-ce/issues/39675. +`owner_name` and `owner_email` are always `null`. Please see <https://gitlab.com/gitlab-org/gitlab-ce/issues/39675>. **Group renamed:** @@ -309,7 +309,7 @@ If the user is blocked via LDAP, `state` will be `ldap_blocked`. } ``` -`owner_name` and `owner_email` are always `null`. Please see https://gitlab.com/gitlab-org/gitlab-ce/issues/39675. +`owner_name` and `owner_email` are always `null`. Please see <https://gitlab.com/gitlab-org/gitlab-ce/issues/39675>. **New Group Member:** diff --git a/doc/topics/autodevops/index.md b/doc/topics/autodevops/index.md index 780e9b8783e..325de50cab0 100644 --- a/doc/topics/autodevops/index.md +++ b/doc/topics/autodevops/index.md @@ -193,7 +193,7 @@ To add a different cluster for each environment: and Ingress. 1. Make sure you have [configured your DNS](#auto-devops-base-domain) with the specified Auto DevOps domains. -1. Navigate to your project's **Settings > CI/CD > Variables** and add +1. Navigate to your project's **Settings > CI/CD > Environment variables** and add the `AUTO_DEVOPS_DOMAIN` variables with their respective environment scope. @@ -634,6 +634,11 @@ repo or by specifying a project variable: - **Project variable** - Create a [project variable](../../ci/variables/README.md#variables) `AUTO_DEVOPS_CHART` with the URL of a custom chart to use or create two project variables `AUTO_DEVOPS_CHART_REPOSITORY` with the URL of a custom chart repository and `AUTO_DEVOPS_CHART` with the path to the chart. +### Custom Helm chart per environment **[PREMIUM]** + +You can specify the use of a custom Helm chart per environment by scoping the environment variable +to the desired environment. See [Limiting environment scopes of variables](https://docs.gitlab.com/ee/ci/variables/#limiting-environment-scopes-of-variables-premium). + ### Customizing `.gitlab-ci.yml` If you want to modify the CI/CD pipeline used by Auto DevOps, you can copy the @@ -683,10 +688,12 @@ 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. | -| `POSTGRES_DB` | The PostgreSQL database name; defaults to the value of [`$CI_ENVIRONMENT_SLUG`](../../ci/variables/README.md#predefined-variables-environment-variables). Set it to use a custom database name. | +| `POSTGRES_DB` | The PostgreSQL database name; defaults to the value of [`$CI_ENVIRONMENT_SLUG`](../../ci/variables/README.md#predefined-environment-variables). Set it to use a custom database name. | | `BUILDPACK_URL` | The buildpack's full URL. It can point to either Git repositories or a tarball URL. For Git repositories, it is possible to point to a specific `ref`, for example `https://github.com/heroku/heroku-buildpack-ruby.git#v142` | | `SAST_CONFIDENCE_LEVEL` | The minimum confidence level of security issues you want to be reported; `1` for Low, `2` for Medium, `3` for High; defaults to `3`.| | `DEP_SCAN_DISABLE_REMOTE_CHECKS` | Whether remote Dependency Scanning checks are disabled; defaults to `"false"`. Set to `"true"` to disable checks that send data to GitLab central servers. [Read more about remote checks](https://gitlab.com/gitlab-org/security-products/dependency-scanning#remote-checks).| diff --git a/doc/topics/autodevops/quick_start_guide.md b/doc/topics/autodevops/quick_start_guide.md index 6326aadcdf2..9749bd63f2b 100644 --- a/doc/topics/autodevops/quick_start_guide.md +++ b/doc/topics/autodevops/quick_start_guide.md @@ -83,7 +83,7 @@ under which this application will be deployed. ![GitLab GKE cluster details](img/guide_gitlab_gke_details.png) 1. Once ready, click **Create Kubernetes cluster**. - + NOTE: **Note:** Do not select `f1-micro` from the **Machine type** dropdown. `f1-micro` machines cannot support a full GitLab installation. @@ -216,7 +216,7 @@ deployment and clicking a square will take you to the pod's logs page. TIP: **Tip:** There is only one pod hosting the application at the moment, but you can add more pods by defining the [`REPLICAS` variable](index.md#environment-variables) -under **Settings > CI/CD > Variables**. +under **Settings > CI/CD > Environment variables**. ### Working with branches diff --git a/doc/university/glossary/README.md b/doc/university/glossary/README.md index d34cd1bb1c3..404a686f1cf 100644 --- a/doc/university/glossary/README.md +++ b/doc/university/glossary/README.md @@ -423,7 +423,7 @@ A set of symbols that are used to organize objects of various kinds so that thes ### Nginx -A web [server](https://www.nginx.com/resources/wiki/) (pronounced "engine x"). [It can act]((https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/doc/settings/nginx.md) as a reverse proxy server for HTTP, HTTPS, SMTP, POP3, and IMAP protocols, as well as a load balancer and an HTTP cache. +A web [server](https://www.nginx.com/resources/wiki/) (pronounced "engine x"). [It can act](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/doc/settings/nginx.md) as a reverse proxy server for HTTP, HTTPS, SMTP, POP3, and IMAP protocols, as well as a load balancer and an HTTP cache. ### OAuth diff --git a/doc/university/training/end-user/README.md b/doc/university/training/end-user/README.md index 637e4e3c791..53578a34d1c 100644 --- a/doc/university/training/end-user/README.md +++ b/doc/university/training/end-user/README.md @@ -50,7 +50,7 @@ project. - Use the tools at your disposal when you get stuck. - Use `git help <command>` command - Use Google (i.e. StackOverflow, Google groups) - - Read documentation at https://git-scm.com + - Read documentation at <https://git-scm.com> --- @@ -62,7 +62,7 @@ Workshop Time! ### Setup - Windows: Install 'Git for Windows' - - https://git-for-windows.github.io + - <https://git-for-windows.github.io> - Mac: Type `git` in the Terminal application. - If it's not installed, it will prompt you to install it. - Linux @@ -142,7 +142,7 @@ cd ~/workspace - Sign in into your gitlab.com account - Create a project -- Choose to import from 'Any Repo by URL' and use https://gitlab.com/gitlab-org/training-examples.git +- Choose to import from 'Any Repo by URL' and use <https://gitlab.com/gitlab-org/training-examples.git> - On your machine clone the `training-examples` project --- diff --git a/doc/university/training/topics/env_setup.md b/doc/university/training/topics/env_setup.md index bdf805711e0..78ca30e0f55 100644 --- a/doc/university/training/topics/env_setup.md +++ b/doc/university/training/topics/env_setup.md @@ -8,7 +8,7 @@ comments: false ## Install - **Windows** - - Install 'Git for Windows' from https://git-for-windows.github.io + - Install 'Git for Windows' from <https://git-for-windows.github.io> - **Mac** - Type '`git`' in the Terminal application. diff --git a/doc/university/training/topics/git_intro.md b/doc/university/training/topics/git_intro.md index 7e502d6dad4..127323c292c 100644 --- a/doc/university/training/topics/git_intro.md +++ b/doc/university/training/topics/git_intro.md @@ -8,7 +8,7 @@ comments: false ## Intro -https://git-scm.com/about +<https://git-scm.com/about> - Distributed version control - Does not rely on connection to a central server @@ -25,4 +25,4 @@ Use the tools at your disposal when you get stuck. - Use '`git help <command>`' command - Use Google -- Read documentation at https://git-scm.com +- Read documentation at <https://git-scm.com> 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..8f588f8b2ae 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 @@ -218,7 +218,7 @@ log_bin_trust_function_creators=1 Note: 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-5-stable/config/unicorn.rb.example but with your settings. +- Make `/home/git/gitlab/config/unicorn.rb` the same as <https://gitlab.com/gitlab-org/gitlab-ce/blob/11-5-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 diff --git a/doc/update/11.5-to-11.6.md b/doc/update/11.5-to-11.6.md index 2e9ec5d71de..f95ce54650e 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 @@ -218,7 +218,7 @@ log_bin_trust_function_creators=1 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-6-stable/config/unicorn.rb.example but with your settings. +Make `/home/git/gitlab/config/unicorn.rb` the same as <https://gitlab.com/gitlab-org/gitlab-ce/blob/11-6-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 @@ -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..b4d830e8ce0 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 @@ -218,7 +218,7 @@ log_bin_trust_function_creators=1 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-7-stable/config/unicorn.rb.example but with your settings. +Make `/home/git/gitlab/config/unicorn.rb` the same as <https://gitlab.com/gitlab-org/gitlab-ce/blob/11-7-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 @@ -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 index 1587c310876..d5cd557d7b5 100644 --- a/doc/update/11.7-to-11.8.md +++ b/doc/update/11.7-to-11.8.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 @@ -221,7 +221,7 @@ log_bin_trust_function_creators=1 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. +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 @@ -320,11 +320,12 @@ sudo systemctl daemon-reload ```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 --without postgres development test --deployment +sudo -u git -H bundle install --deployment --without development test postgres aws kerberos -# PostgreSQL installations (note: the line below states '--without mysql') -sudo -u git -H bundle install --without mysql development test --deployment # Optional: clean up old gems sudo -u git -H bundle clean diff --git a/doc/update/5.1-to-5.2.md b/doc/update/5.1-to-5.2.md index 4faf5fa549e..bcc9058ff99 100644 --- a/doc/update/5.1-to-5.2.md +++ b/doc/update/5.1-to-5.2.md @@ -73,15 +73,15 @@ sudo -u git -H bundle exec rake assets:precompile RAILS_ENV=production ## 5. Update config files -- Make `/home/git/gitlab/config/gitlab.yml` same as https://gitlab.com/gitlab-org/gitlab-ce/blob/5-2-stable/config/gitlab.yml.example but with your settings. -- Make `/home/git/gitlab/config/puma.rb` same as https://gitlab.com/gitlab-org/gitlab-ce/blob/5-2-stable/config/puma.rb.example but with your settings. +- Make `/home/git/gitlab/config/gitlab.yml` same as <https://gitlab.com/gitlab-org/gitlab-ce/blob/5-2-stable/config/gitlab.yml.example> but with your settings. +- Make `/home/git/gitlab/config/puma.rb` same as <https://gitlab.com/gitlab-org/gitlab-ce/blob/5-2-stable/config/puma.rb.example> but with your settings. ## 6. Update Init script ```bash cd /home/git/gitlab sudo rm /etc/init.d/gitlab -sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab +sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab sudo chmod +x /etc/init.d/gitlab ``` diff --git a/doc/update/5.1-to-5.4.md b/doc/update/5.1-to-5.4.md index 212343bac3f..5767c9cc121 100644 --- a/doc/update/5.1-to-5.4.md +++ b/doc/update/5.1-to-5.4.md @@ -70,8 +70,8 @@ sudo -u git -H bundle exec rake assets:precompile RAILS_ENV=production ## 5. Update config files -- Make `/home/git/gitlab/config/gitlab.yml` same as https://gitlab.com/gitlab-org/gitlab-ce/blob/5-4-stable/config/gitlab.yml.example but with your settings. -- Make `/home/git/gitlab/config/puma.rb` same as https://gitlab.com/gitlab-org/gitlab-ce/blob/5-4-stable/config/puma.rb.example but with your settings. +- Make `/home/git/gitlab/config/gitlab.yml` same as <https://gitlab.com/gitlab-org/gitlab-ce/blob/5-4-stable/config/gitlab.yml.example> but with your settings. +- Make `/home/git/gitlab/config/puma.rb` same as <https://gitlab.com/gitlab-org/gitlab-ce/blob/5-4-stable/config/puma.rb.example> but with your settings. ## 6. Update Init script diff --git a/doc/update/5.1-to-6.0.md b/doc/update/5.1-to-6.0.md index 865d38e0ca4..4993d034b6e 100644 --- a/doc/update/5.1-to-6.0.md +++ b/doc/update/5.1-to-6.0.md @@ -185,8 +185,8 @@ sudo -u git -H git config --global core.autocrlf input Note: We switched from Puma in GitLab 5.x to unicorn in GitLab 6.0. -- Make `/home/git/gitlab/config/gitlab.yml` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/6-0-stable/config/gitlab.yml.example but with your settings. -- Make `/home/git/gitlab/config/unicorn.rb` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/6-0-stable/config/unicorn.rb.example but with your settings. +- Make `/home/git/gitlab/config/gitlab.yml` the same as <https://gitlab.com/gitlab-org/gitlab-ce/blob/6-0-stable/config/gitlab.yml.example> but with your settings. +- Make `/home/git/gitlab/config/unicorn.rb` the same as <https://gitlab.com/gitlab-org/gitlab-ce/blob/6-0-stable/config/unicorn.rb.example> but with your settings. ## 7. Update Init script diff --git a/doc/update/5.2-to-5.3.md b/doc/update/5.2-to-5.3.md index ed4f3ebdd53..c378d2798f4 100644 --- a/doc/update/5.2-to-5.3.md +++ b/doc/update/5.2-to-5.3.md @@ -64,8 +64,8 @@ sudo -u git -H bundle exec rake assets:precompile RAILS_ENV=production ## 4. Update config files -- Make `/home/git/gitlab/config/gitlab.yml` same as https://gitlab.com/gitlab-org/gitlab-ce/blob/5-3-stable/config/gitlab.yml.example but with your settings. -- Make `/home/git/gitlab/config/puma.rb` same as https://gitlab.com/gitlab-org/gitlab-ce/blob/5-3-stable/config/puma.rb.example but with your settings. +- Make `/home/git/gitlab/config/gitlab.yml` same as <https://gitlab.com/gitlab-org/gitlab-ce/blob/5-3-stable/config/gitlab.yml.example> but with your settings. +- Make `/home/git/gitlab/config/puma.rb` same as <https://gitlab.com/gitlab-org/gitlab-ce/blob/5-3-stable/config/puma.rb.example> but with your settings. ## 5. Update Init script diff --git a/doc/update/5.3-to-5.4.md b/doc/update/5.3-to-5.4.md index 7277250eb32..77b1e9e5329 100644 --- a/doc/update/5.3-to-5.4.md +++ b/doc/update/5.3-to-5.4.md @@ -68,8 +68,8 @@ sudo -u git -H bundle exec rake assets:precompile RAILS_ENV=production ## 5. Update config files -- Make `/home/git/gitlab/config/gitlab.yml` same as https://gitlab.com/gitlab-org/gitlab-ce/blob/5-4-stable/config/gitlab.yml.example but with your settings. -- Make `/home/git/gitlab/config/puma.rb` same as https://gitlab.com/gitlab-org/gitlab-ce/blob/5-4-stable/config/puma.rb.example but with your settings. +- Make `/home/git/gitlab/config/gitlab.yml` same as <https://gitlab.com/gitlab-org/gitlab-ce/blob/5-4-stable/config/gitlab.yml.example> but with your settings. +- Make `/home/git/gitlab/config/puma.rb` same as <https://gitlab.com/gitlab-org/gitlab-ce/blob/5-4-stable/config/puma.rb.example> but with your settings. ## 6. Update Init script diff --git a/doc/update/5.4-to-6.0.md b/doc/update/5.4-to-6.0.md index dacdf05cc9c..2d2da769b89 100644 --- a/doc/update/5.4-to-6.0.md +++ b/doc/update/5.4-to-6.0.md @@ -118,8 +118,8 @@ sudo -u git -H bundle exec rake assets:precompile RAILS_ENV=production Note: We switched from Puma in GitLab 5.4 to unicorn in GitLab 6.0. -- Make `/home/git/gitlab/config/gitlab.yml` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/master/config/gitlab.yml.example but with your settings. -- Make `/home/git/gitlab/config/unicorn.rb` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/master/config/unicorn.rb.example but with your settings. +- Make `/home/git/gitlab/config/gitlab.yml` the same as <https://gitlab.com/gitlab-org/gitlab-ce/blob/master/config/gitlab.yml.example> but with your settings. +- Make `/home/git/gitlab/config/unicorn.rb` the same as <https://gitlab.com/gitlab-org/gitlab-ce/blob/master/config/unicorn.rb.example> but with your settings. ## 7. Update Init script diff --git a/doc/update/6.0-to-6.1.md b/doc/update/6.0-to-6.1.md index a3c52a1cfb4..dd409175c27 100644 --- a/doc/update/6.0-to-6.1.md +++ b/doc/update/6.0-to-6.1.md @@ -85,8 +85,8 @@ sudo -u git -H bundle exec rake cache:clear RAILS_ENV=production ## 5. Update config files -- Make `/home/git/gitlab/config/gitlab.yml` same as https://gitlab.com/gitlab-org/gitlab-ce/blob/6-1-stable/config/gitlab.yml.example but with your settings. -- Make `/home/git/gitlab/config/unicorn.rb` same as https://gitlab.com/gitlab-org/gitlab-ce/blob/6-1-stable/config/unicorn.rb.example but with your settings. +- Make `/home/git/gitlab/config/gitlab.yml` same as <https://gitlab.com/gitlab-org/gitlab-ce/blob/6-1-stable/config/gitlab.yml.example> but with your settings. +- Make `/home/git/gitlab/config/unicorn.rb` same as <https://gitlab.com/gitlab-org/gitlab-ce/blob/6-1-stable/config/unicorn.rb.example> but with your settings. ## 6. Update Init script diff --git a/doc/update/6.1-to-6.2.md b/doc/update/6.1-to-6.2.md index 36a395bf01e..cace80c99b7 100644 --- a/doc/update/6.1-to-6.2.md +++ b/doc/update/6.1-to-6.2.md @@ -79,15 +79,15 @@ sudo -u git -H bundle exec rake cache:clear RAILS_ENV=production ## 6. Update config files -TIP: to see what changed in `gitlab.yml.example` in this release use next command: +TIP: to see what changed in `gitlab.yml.example` in this release use next command: ``` git diff 6-1-stable:config/gitlab.yml.example 6-2-stable:config/gitlab.yml.example ``` -- Make `/home/git/gitlab/config/gitlab.yml` same as https://gitlab.com/gitlab-org/gitlab-ce/blob/6-2-stable/config/gitlab.yml.example but with your settings. +- Make `/home/git/gitlab/config/gitlab.yml` same as <https://gitlab.com/gitlab-org/gitlab-ce/blob/6-2-stable/config/gitlab.yml.example> but with your settings. -- Make `/home/git/gitlab/config/unicorn.rb` same as https://gitlab.com/gitlab-org/gitlab-ce/blob/6-2-stable/config/unicorn.rb.example but with your settings. +- Make `/home/git/gitlab/config/unicorn.rb` same as <https://gitlab.com/gitlab-org/gitlab-ce/blob/6-2-stable/config/unicorn.rb.example> but with your settings. - Copy rack attack middleware config: diff --git a/doc/update/6.2-to-6.3.md b/doc/update/6.2-to-6.3.md index 02e87a08b8f..7205575942a 100644 --- a/doc/update/6.2-to-6.3.md +++ b/doc/update/6.2-to-6.3.md @@ -81,8 +81,8 @@ TIP: to see what changed in gitlab.yml.example in this release use next command: git diff 6-2-stable:config/gitlab.yml.example 6-3-stable:config/gitlab.yml.example ``` -- Make `/home/git/gitlab/config/gitlab.yml` same as https://gitlab.com/gitlab-org/gitlab-ce/blob/6-3-stable/config/gitlab.yml.example but with your settings. -- Make `/home/git/gitlab/config/unicorn.rb` same as https://gitlab.com/gitlab-org/gitlab-ce/blob/6-3-stable/config/unicorn.rb.example but with your settings. +- Make `/home/git/gitlab/config/gitlab.yml` same as <https://gitlab.com/gitlab-org/gitlab-ce/blob/6-3-stable/config/gitlab.yml.example> but with your settings. +- Make `/home/git/gitlab/config/unicorn.rb` same as <https://gitlab.com/gitlab-org/gitlab-ce/blob/6-3-stable/config/unicorn.rb.example> but with your settings. ```bash # Copy rack attack middleware config diff --git a/doc/update/6.9-to-7.0.md b/doc/update/6.9-to-7.0.md index 7f3abf74675..e1ca34305b4 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 @@ -110,8 +110,8 @@ There are new configuration options available for gitlab.yml. View them with the git diff origin/6-9-stable:config/gitlab.yml.example origin/7-0-stable:config/gitlab.yml.example ``` -- HTTP setups: Make `/etc/nginx/sites-available/nginx` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/7-0-stable/lib/support/nginx/gitlab but with your settings. -- HTTPS setups: Make `/etc/nginx/sites-available/nginx-ssl` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/7-0-stable/lib/support/nginx/gitlab-ssl but with your setting. +- HTTP setups: Make `/etc/nginx/sites-available/nginx` the same as <https://gitlab.com/gitlab-org/gitlab-ce/blob/7-0-stable/lib/support/nginx/gitlab> but with your settings. +- HTTPS setups: Make `/etc/nginx/sites-available/nginx-ssl` the same as <https://gitlab.com/gitlab-org/gitlab-ce/blob/7-0-stable/lib/support/nginx/gitlab-ssl> but with your setting. ### 7. Start application 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..674163091be 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 @@ -178,9 +178,9 @@ TIP: to see what changed in `gitlab.yml.example` in this release use next comman git diff 6-0-stable:config/gitlab.yml.example 7-14-stable:config/gitlab.yml.example ``` -- Make `/home/git/gitlab/config/gitlab.yml` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/7-14-stable/config/gitlab.yml.example but with your settings. -- Make `/home/git/gitlab/config/unicorn.rb` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/7-14-stable/config/unicorn.rb.example but with your settings. -- Make `/home/git/gitlab-shell/config.yml` the same as https://gitlab.com/gitlab-org/gitlab-shell/blob/v2.6.5/config.yml.example but with your settings. +- Make `/home/git/gitlab/config/gitlab.yml` the same as <https://gitlab.com/gitlab-org/gitlab-ce/blob/7-14-stable/config/gitlab.yml.example> but with your settings. +- Make `/home/git/gitlab/config/unicorn.rb` the same as <https://gitlab.com/gitlab-org/gitlab-ce/blob/7-14-stable/config/unicorn.rb.example> but with your settings. +- Make `/home/git/gitlab-shell/config.yml` the same as <https://gitlab.com/gitlab-org/gitlab-shell/blob/v2.6.5/config.yml.example> but with your settings. - Copy rack attack middleware config. ```bash @@ -195,8 +195,8 @@ sudo cp lib/support/logrotate/gitlab /etc/logrotate.d/gitlab ### Change Nginx settings -- HTTP setups: Make `/etc/nginx/sites-available/gitlab` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/7-14-stable/lib/support/nginx/gitlab but with your settings. -- HTTPS setups: Make `/etc/nginx/sites-available/gitlab-ssl` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/7-14-stable/lib/support/nginx/gitlab-ssl but with your settings. +- HTTP setups: Make `/etc/nginx/sites-available/gitlab` the same as <https://gitlab.com/gitlab-org/gitlab-ce/blob/7-14-stable/lib/support/nginx/gitlab> but with your settings. +- HTTPS setups: Make `/etc/nginx/sites-available/gitlab-ssl` the same as <https://gitlab.com/gitlab-org/gitlab-ce/blob/7-14-stable/lib/support/nginx/gitlab-ssl> but with your settings. - A new `location /uploads/` section has been added that needs to have the same content as the existing `location @gitlab` section. ### Check the version of /usr/local/bin/git 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/7.1-to-7.2.md b/doc/update/7.1-to-7.2.md index b69bd391241..44e5fc676b3 100644 --- a/doc/update/7.1-to-7.2.md +++ b/doc/update/7.1-to-7.2.md @@ -94,8 +94,8 @@ There are new configuration options available for `gitlab.yml`. View them with t git diff 7-1-stable:config/gitlab.yml.example 7-2-stable:config/gitlab.yml.example ``` -- HTTP setups: Make `/etc/nginx/sites-available/nginx` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/7-0-stable/lib/support/nginx/gitlab but with your settings. -- HTTPS setups: Make `/etc/nginx/sites-available/nginx-ssl` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/7-0-stable/lib/support/nginx/gitlab-ssl but with your setting. +- HTTP setups: Make `/etc/nginx/sites-available/nginx` the same as <https://gitlab.com/gitlab-org/gitlab-ce/blob/7-0-stable/lib/support/nginx/gitlab> but with your settings. +- HTTPS setups: Make `/etc/nginx/sites-available/nginx-ssl` the same as <https://gitlab.com/gitlab-org/gitlab-ce/blob/7-0-stable/lib/support/nginx/gitlab-ssl> but with your setting. Update rack attack middleware config diff --git a/doc/update/7.2-to-7.3.md b/doc/update/7.2-to-7.3.md index b69a9927f37..2625df2def8 100644 --- a/doc/update/7.2-to-7.3.md +++ b/doc/update/7.2-to-7.3.md @@ -108,8 +108,8 @@ git diff origin/7-2-stable:config/gitlab.yml.example origin/7-3-stable:config/gi sudo -u git -H sed -i 's/:backlog => 64/:backlog => 1024/' config/unicorn.rb ``` -- HTTP setups: Make `/etc/nginx/sites-available/nginx` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/7-3-stable/lib/support/nginx/gitlab but with your settings. -- HTTPS setups: Make `/etc/nginx/sites-available/nginx-ssl` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/7-3-stable/lib/support/nginx/gitlab-ssl but with your setting. +- HTTP setups: Make `/etc/nginx/sites-available/nginx` the same as <https://gitlab.com/gitlab-org/gitlab-ce/blob/7-3-stable/lib/support/nginx/gitlab> but with your settings. +- HTTPS setups: Make `/etc/nginx/sites-available/nginx-ssl` the same as <https://gitlab.com/gitlab-org/gitlab-ce/blob/7-3-stable/lib/support/nginx/gitlab-ssl> but with your setting. ### 7. Start application diff --git a/doc/update/7.3-to-7.4.md b/doc/update/7.3-to-7.4.md index 3786095bb8b..ad7930e8728 100644 --- a/doc/update/7.3-to-7.4.md +++ b/doc/update/7.3-to-7.4.md @@ -75,7 +75,7 @@ sudo -u git -H editor config/unicorn.rb #### Change Nginx HTTPS settings -- HTTPS setups: Make `/etc/nginx/sites-available/gitlab-ssl` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/7-4-stable/lib/support/nginx/gitlab-ssl but with your setting. +- HTTPS setups: Make `/etc/nginx/sites-available/gitlab-ssl` the same as <https://gitlab.com/gitlab-org/gitlab-ce/blob/7-4-stable/lib/support/nginx/gitlab-ssl> but with your setting. #### MySQL Databases: Update database.yml config file 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/update/upgrading_postgresql_using_slony.md b/doc/update/upgrading_postgresql_using_slony.md index 51178809b4c..d2e2cf439b5 100644 --- a/doc/update/upgrading_postgresql_using_slony.md +++ b/doc/update/upgrading_postgresql_using_slony.md @@ -57,7 +57,7 @@ server. ## Installing Slony Slony will be used to upgrade the database without requiring long downtimes. -Slony can be downloaded from http://www.slony.info/. If you have installed +Slony can be downloaded from <http://www.slony.info/>. If you have installed PostgreSQL using your operating system's package manager you may also be able to install Slony using said package manager. diff --git a/doc/user/admin_area/broadcast_messages.md b/doc/user/admin_area/broadcast_messages.md new file mode 100644 index 00000000000..51949088521 --- /dev/null +++ b/doc/user/admin_area/broadcast_messages.md @@ -0,0 +1,51 @@ +# Broadcast Messages + +GitLab can display messages to all users of a GitLab instance in a banner that appears in the UI. + +![Broadcast Message](img/broadcast_messages.png) + +NOTE: **Note:** +If more than one banner message is active at one time, they are displayed in a stack in order of creation. + +## Adding a broadcast message + +To display messages to users on your GitLab instance, add broadcast message. + +To add a broadcast message: + +1. Navigate to the **Admin Area > Messages** page. +1. Add the text for the message to the **Message** field. Markdown and emoji are supported. +1. If required, click the **Customize colors** link to edit the background color and font color of the message. +1. Select a date for the message to start and end. +1. Click the **Add broadcast message** button. + +NOTE: **Note:** +Once a broadcast message has expired, it is no longer displayed in the UI but is still listed in the +list of broadcast messages. + +## Editing a broadcast message + +If changes are required to a broadcast message, they can be edited. + +To edit a broadcast message: + +1. Navigate to the **Admin Area > Messages** page. +1. From the list of broadcast messages, click the appropriate button to edit the message. +1. After making the required changes, click the **Update broadcast message** button. + +TIP: **Tip:** +Expired messages can be made active again by changing their end date. + +## Deleting a broadcast message + +Broadcast messages that are no longer required can be deleted. + +To delete a broadcast message: + +1. Navigate to the **Admin Area > Messages** page. +1. From the list of broadcast messages, click the appropriate button to delete the message. + +Once deleted, the broadcast message is removed from the list of broadcast messages. + +NOTE: **Note:** +Broadcast messages can be deleted while active. diff --git a/doc/user/admin_area/custom_project_templates.md b/doc/user/admin_area/custom_project_templates.md index 5afbf9f2934..e34ba045c54 100644 --- a/doc/user/admin_area/custom_project_templates.md +++ b/doc/user/admin_area/custom_project_templates.md @@ -2,24 +2,25 @@ > [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/6860) in [GitLab Premium](https://about.gitlab.com/pricing) 11.2. -When you create a new project, creating it based on custom project templates is -a convenient option to bootstrap from an existing project boilerplate. -The administration setting to configure a GitLab group that serves as template -source can be found under **Admin > Settings > Custom project templates**. +When you create a new [project](../project/index.md), creating it based on custom project templates is +a convenient bootstrap option. + +GitLab administrators can configure a GitLab group that serves as template +source for an entire GitLab instance under **Admin area > Settings > Custom project templates**. + +NOTE: **Note:** +To set project templates at a group level, +see [Custom group-level project templates](../group/custom_project_templates.md). Within this section, you can configure the group where all the custom project templates are sourced. Every project directly under the group namespace will be available to the user if they have access to them. For example, every public -project in the group will be available to every logged user. However, -private projects will be available only if the user has view [permissions](../permissions.md) -in the project: +project in the group will be available to every logged in user. -- Project Owner, Maintainer, Developer, Reporter or Guest -- Is a member of the Group: Owner, Maintainer, Developer, Reporter or Guest +However, private projects will be available only if the user is a member of the project. +NOTE: **Note:** Projects below subgroups of the template group are **not** supported. Repository and database information that are copied over to each new project are identical to the data exported with [GitLab's Project Import/Export](../project/settings/import_export.md). - -If you would like to set project templates at a group level, please see [Custom group-level project templates](../group/custom_project_templates.md).
\ No newline at end of file diff --git a/doc/user/admin_area/img/broadcast_messages.png b/doc/user/admin_area/img/broadcast_messages.png Binary files differnew file mode 100644 index 00000000000..926d38ae049 --- /dev/null +++ b/doc/user/admin_area/img/broadcast_messages.png 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/gitlab_com/index.md b/doc/user/gitlab_com/index.md index 68a0f1a5837..c1c9b8bf43c 100644 --- a/doc/user/gitlab_com/index.md +++ b/doc/user/gitlab_com/index.md @@ -46,7 +46,7 @@ Below are the settings for [GitLab Pages]. | Setting | GitLab.com | Default | | ----------------------- | ---------------- | ------------- | | Domain name | `gitlab.io` | - | -| IP address | `52.167.214.135` | - | +| IP address | `35.185.44.232` | - | | Custom domains support | yes | no | | TLS certificates support| yes | no | diff --git a/doc/user/group/custom_project_templates.md b/doc/user/group/custom_project_templates.md index eaf0273050b..8e101407ac0 100644 --- a/doc/user/group/custom_project_templates.md +++ b/doc/user/group/custom_project_templates.md @@ -2,22 +2,25 @@ > [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/6861) in [GitLab Premium](https://about.gitlab.com/pricing) 11.6. -When you create a new project, creating it based on custom project templates is -a convenient option to bootstrap from an existing project boilerplate. -The group-level setting to configure a GitLab group that serves as template -source can be found under **Group > Settings > General > Custom project templates**. +When you create a new [project](../project/index.md), creating it based on custom project templates is +a convenient bootstrap option. + +Users can configure a GitLab group that serves as template +source under a group's **Settings > General > Custom project templates**. + +NOTE: **Note:** +GitLab administrators can +[set project templates for an entire GitLab instance](../admin_area/custom_project_templates.md). Within this section, you can configure the group where all the custom project templates are sourced. Every project directly under the group namespace will be available to the user if they have access to them. For example, every public -project in the group will be available to every logged in user. However, -private projects will be available only if the user has view [permissions](../permissions.md) -in the project. That is, users with Owner, Maintainer, Developer, Reporter or Guest roles for projects, -or for groups to which the project belongs. +project in the group will be available to every logged in user. +However, private projects will be available only if the user is a member of the project. + +NOTE: **Note:** Projects of nested subgroups of a selected template source cannot be used. Repository and database information that are copied over to each new project are identical to the data exported with [GitLab's Project Import/Export](../project/settings/import_export.md). - -If you would like to set project templates at an instance level, please see [Custom instance-level project templates](../admin_area/custom_project_templates.md).
\ No newline at end of file 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/permissions.md b/doc/user/permissions.md index 2a1c8cc5bc0..019652b2408 100644 --- a/doc/user/permissions.md +++ b/doc/user/permissions.md @@ -159,6 +159,13 @@ Confidential issues can be accessed by reporters and higher permission levels, as well as by guest users that create a confidential issue. To learn more, read through the documentation on [permissions and access to confidential issues](project/issues/confidential_issues.md#permissions-and-access-to-confidential-issues). +### Releases permissions + +[Project Releases](project/releases/index.md) can be read by all project +members (Reporters, Developers, Maintainers, Owners) **except Guests**. +Releases can be created, updated, or deleted via [Releases APIs](../api/releases/index.md) +by project Developers, Maintainers, and Owners. + ## Group members permissions NOTE: **Note:** @@ -178,9 +185,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/profile/index.md b/doc/user/profile/index.md index 2f989a26725..efb031b8239 100644 --- a/doc/user/profile/index.md +++ b/doc/user/profile/index.md @@ -73,7 +73,7 @@ which also covers the case where you have projects hosted with ## Private profile -The following information will be hidden from the user profile page (https://gitlab.example.com/username) if this feature is enabled: +The following information will be hidden from the user profile page (`https://gitlab.example.com/username`) if this feature is enabled: - Atom feed - Date when account is created 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/img/app-domain.png b/doc/user/project/clusters/serverless/img/app-domain.png Binary files differnew file mode 100644 index 00000000000..d113dfadd2e --- /dev/null +++ b/doc/user/project/clusters/serverless/img/app-domain.png diff --git a/doc/user/project/clusters/serverless/img/serverless-details.png b/doc/user/project/clusters/serverless/img/serverless-details.png Binary files differdeleted file mode 100644 index 61e0735199a..00000000000 --- a/doc/user/project/clusters/serverless/img/serverless-details.png +++ /dev/null diff --git a/doc/user/project/clusters/serverless/index.md b/doc/user/project/clusters/serverless/index.md index 9ecb109fa89..aa1e165e3a2 100644 --- a/doc/user/project/clusters/serverless/index.md +++ b/doc/user/project/clusters/serverless/index.md @@ -1,7 +1,9 @@ # Serverless > Introduced in GitLab 11.5. -> Serverless is currently in [alpha](https://about.gitlab.com/handbook/product/#alpha). + +CAUTION: **Caution:** +Serverless is currently in [alpha](https://about.gitlab.com/handbook/product/#alpha). Run serverless workloads on Kubernetes using [Knative](https://cloud.google.com/knative/). @@ -82,7 +84,15 @@ Currently the following [runtimes](https://gitlab.com/triggermesh/runtimes) are - node.js - kaniko -In order to deploy functions to your Knative instance, the following files must be present: +You can find all the files referenced in this doc in the [functions example project](https://gitlab.com/knative-examples/functions). + +Follow these steps to deploy a function using the Node.js runtime to your Knative instance: + +1. Create a directory that will house the function. In this example we will create a directory called `echo` at the root of the project. + +1. Create the file that will contain the function code. In this example, our file is called `echo.js` and is located inside the `echo` directory. If your project is: + - Public, continue to the next step. + - Private, you will need to [create a GitLab deploy token](../../deploy_tokens/index.md#creating-a-deploy-token) with `gitlab-deploy-token` as the name and the `read_registry` scope. 1. `.gitlab-ci.yml`: This template allows to define the stage, environment, and image to be used for your functions. It must be included at the root of your repository: @@ -94,10 +104,12 @@ In order to deploy functions to your Knative instance, the following files must functions: stage: deploy environment: test - image: gcr.io/triggermesh/tm:v0.0.7 + image: gcr.io/triggermesh/tm:v0.0.9 script: - - tm -n "$KUBE_NAMESPACE" set registry-auth gitlab-registry --registry "$CI_REGISTRY" --username "$CI_REGISTRY_USER" --password "$CI_JOB_TOKEN" - - tm -n "$KUBE_NAMESPACE" --registry-host "$CI_REGISTRY_IMAGE" deploy --wait + - tm -n "$KUBE_NAMESPACE" set registry-auth gitlab-registry --registry "$CI_REGISTRY" --username "$CI_REGISTRY_USER" --password "$CI_JOB_TOKEN" --push + - tm -n "$KUBE_NAMESPACE" set registry-auth gitlab-registry --registry "$CI_REGISTRY" --username "$CI_DEPLOY_USER" --password "$CI_DEPLOY_PASSWORD" --pull + - tm -n "$KUBE_NAMESPACE" deploy --wait + ``` The `gitlab-ci.yml` template creates a `Deploy` stage with a `functions` job that invokes the `tm` CLI with the required parameters. @@ -121,13 +133,15 @@ 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 ``` -The `serverless.yml` file is referencing both an `echo` directory (under `buildargs`) and an `echo` file (under `handler`) which is a reference to `echo.js` in the [repository](https://gitlab.com/knative-examples/functions). Additionally, it contains three sections with distinct parameters: +The `serverless.yml` file references both an `echo` directory (under `buildargs`) and an `echo` file (under `handler`), +which is a reference to `echo.js` in the [repository](https://gitlab.com/knative-examples/functions). Additionally, it +contains three sections with distinct parameters: ### `service` @@ -149,7 +163,6 @@ The `serverless.yml` file is referencing both an `echo` directory (under `builda In the `serverless.yml` example above, the function name is `echo` and the subsequent lines contain the function attributes. - | Parameter | Description | |-----------|-------------| | `handler` | The function's file name. In the example above, both the function name and the handler are the same. | @@ -158,9 +171,8 @@ In the `serverless.yml` example above, the function name is `echo` and the subse | `buildargs` | Pointer to the function file in the repo. In the sample the function is located in the `echo` directory. | | `environment` | Sets an environment variable for the specific function only. | -After the `gitlab-ci.yml` template has been added and the `serverless.yml` file has been -created, each function must be defined as a single file in your repository. Committing a -function to your project will result in a +After the `gitlab-ci.yml` template has been added and the `serverless.yml` file has been +created, pushing a commit to your project will result in a CI pipeline being executed which will deploy each function as a Knative service. Once the deploy stage has finished, additional details for the function will appear under **Operations > Serverless**. @@ -182,14 +194,6 @@ The sample function can now be triggered from any HTTP client using a simple `PO ![function exection](img/function-execution.png) -Currently, the Serverless page presents all functions available in all clusters registered for the project with Knative installed. - -Clicking on the function name will provide additional details such as the -function's URL as well as runtime statistics such as the number of active pods -available to service the request based on load. - -![serverless function details](img/serverless-details.png) - ## Deploying Serverless applications > Introduced in GitLab 11.5. @@ -227,14 +231,18 @@ deploy: - tm -n "$KUBE_NAMESPACE" --config "$KUBECONFIG" deploy service "$CI_PROJECT_NAME" --from-image "$CI_REGISTRY_IMAGE" --wait ``` -## Deploy the application with Knative +### Deploy the application with Knative With all the pieces in place, the next time a CI pipeline runs, the Knative application will be deployed. Navigate to **CI/CD > Pipelines** and click the most recent pipeline. -## Obtain the URL for the Knative deployment +### Obtain the URL for the Knative deployment + +Go to the **Operations > Serverless** page to find the URL for your deployment in the **Domain** column. + +![app domain](img/app-domain.png) -Use the CI/CD deployment job output to obtain the deployment URL. Once all the stages of the pipeline finish, click the **deploy** stage. +Alternatively, use the CI/CD deployment job output to obtain the deployment URL. Once all the stages of the pipeline finish, click the **deploy** stage. ![deploy stage](img/deploy-stage.png) diff --git a/doc/user/project/container_registry.md b/doc/user/project/container_registry.md index eb9e1cd85cd..638b73bfcb6 100644 --- a/doc/user/project/container_registry.md +++ b/doc/user/project/container_registry.md @@ -15,7 +15,7 @@ With the Docker Container Registry integrated into GitLab, every project can have its own space to store its Docker images. -You can read more about Docker Registry at https://docs.docker.com/registry/introduction/. +You can read more about Docker Registry at <https://docs.docker.com/registry/introduction/>. ## Enable the Container Registry for your project diff --git a/doc/user/project/import/github.md b/doc/user/project/import/github.md index b36373b4830..cf99dded5e2 100644 --- a/doc/user/project/import/github.md +++ b/doc/user/project/import/github.md @@ -92,7 +92,7 @@ integration enabled, that should be the preferred method to import your reposito If you are not using the GitHub integration, you can still perform an authorization with GitHub to grant GitLab access your repositories: -1. Go to https://github.com/settings/tokens/new +1. Go to <https://github.com/settings/tokens/new> 1. Enter a token description. 1. Select the repo scope. 1. Click **Generate token**. diff --git a/doc/user/project/import/index.md b/doc/user/project/import/index.md index 2f5efbe84d9..ca02e4e9e96 100644 --- a/doc/user/project/import/index.md +++ b/doc/user/project/import/index.md @@ -25,3 +25,10 @@ but issues and merge requests can't be imported. If you want to retain all metadata like issues and merge requests, you can use the [import/export feature](../settings/import_export.md). + +## Migrating between two self-hosted GitLab instances + +The best method for migrating a project from one GitLab instance to another, +perhaps from an old server to a new server for example, is to +[back up the project](../../../raketasks/backup_restore.md), +then restore it on the new server. diff --git a/doc/user/project/index.md b/doc/user/project/index.md index ce8bd2de61f..6a1aadf058e 100644 --- a/doc/user/project/index.md +++ b/doc/user/project/index.md @@ -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/bamboo.md b/doc/user/project/integrations/bamboo.md index 70c0d434f1f..48aabd02438 100644 --- a/doc/user/project/integrations/bamboo.md +++ b/doc/user/project/integrations/bamboo.md @@ -40,7 +40,7 @@ service in GitLab. 1. Navigate to the [Integrations page](project_services.md#accessing-the-project-services) 1. Click 'Atlassian Bamboo CI' 1. Select the 'Active' checkbox. -1. Enter the base URL of your Bamboo server. 'https://bamboo.example.com' +1. Enter the base URL of your Bamboo server. `https://bamboo.example.com` 1. Enter the build key from your Bamboo build plan. Build keys are typically made up from the Project Key and Plan Key that are set on project/plan creation and separated with a dash (`-`), for example **PROJ-PLAN**. This is a short, all diff --git a/doc/user/project/integrations/hipchat.md b/doc/user/project/integrations/hipchat.md index eee779c50d4..0fd847d415f 100644 --- a/doc/user/project/integrations/hipchat.md +++ b/doc/user/project/integrations/hipchat.md @@ -18,7 +18,7 @@ allow GitLab to send messages only to *one* room. ### Complete these steps in HipChat -1. Go to: https://admin.hipchat.com/admin +1. Go to: <https://admin.hipchat.com/admin> 1. Click on "Group Admin" -> "Integrations". 1. Find "Build Your Own!" and click "Create". 1. Select the desired room, name the integration "GitLab", and click "Create". diff --git a/doc/user/project/integrations/irker.md b/doc/user/project/integrations/irker.md index ecdd83ce8f0..f220fa8497a 100644 --- a/doc/user/project/integrations/irker.md +++ b/doc/user/project/integrations/irker.md @@ -4,12 +4,12 @@ GitLab provides a way to push update messages to an Irker server. When configured, pushes to a project will trigger the service to send data directly to the Irker server. -See the project homepage for further info: https://gitlab.com/esr/irker +See the project homepage for further info: <https://gitlab.com/esr/irker> ## Needed setup You will first need an Irker daemon. You can download the Irker code from its -repository on https://gitlab.com/esr/irker: +repository on <https://gitlab.com/esr/irker>: ``` git clone https://gitlab.com/esr/irker.git diff --git a/doc/user/project/integrations/jira_cloud_configuration.md b/doc/user/project/integrations/jira_cloud_configuration.md index cae66526175..849df707521 100644 --- a/doc/user/project/integrations/jira_cloud_configuration.md +++ b/doc/user/project/integrations/jira_cloud_configuration.md @@ -3,7 +3,7 @@ An API token is needed when integrating with JIRA Cloud, follow the steps below to create one: -1. Log in to https://id.atlassian.com with your email. +1. Log in to <https://id.atlassian.com> with your email. 1. **Click API tokens**, then **Create API token**. ![JIRA API token](img/jira_api_token_menu.png) diff --git a/doc/user/project/integrations/mattermost.md b/doc/user/project/integrations/mattermost.md index ed4367c1135..8c5461de42f 100644 --- a/doc/user/project/integrations/mattermost.md +++ b/doc/user/project/integrations/mattermost.md @@ -10,7 +10,7 @@ To enable Mattermost integration you must create an incoming webhook integration 1. Save it, copy the **Webhook URL**, we'll need this later for GitLab. There might be some cases that Incoming Webhooks are blocked by admin, ask your mattermost admin to enable -it on https://mattermost.example/admin_console/integrations/custom. +it on `https://mattermost.example/admin_console/integrations/custom`. Display name override is not enabled by default, you need to ask your admin to enable it on that same section. @@ -38,7 +38,7 @@ At the end, fill in your Mattermost details: | Field | Description | | ----- | ----------- | -| **Webhook** | The incoming webhook URL which you have to set up on Mattermost, it will be something like: http://mattermost.example/hooks/5xo… | +| **Webhook** | The incoming webhook URL which you have to set up on Mattermost, it will be something like: `http://mattermost.example/hooks/5xo…` | | **Username** | Optional username which can be on messages sent to Mattermost. Fill this in if you want to change the username of the bot. | | **Notify only broken pipelines** | If you choose to enable the **Pipeline** event and you want to be only notified about failed pipelines. | diff --git a/doc/user/project/integrations/prometheus.md b/doc/user/project/integrations/prometheus.md index d6ee678443f..a4698fd172a 100644 --- a/doc/user/project/integrations/prometheus.md +++ b/doc/user/project/integrations/prometheus.md @@ -89,7 +89,7 @@ to integrate with. Once configured, GitLab will attempt to retrieve performance metrics for any environment which has had a successful deployment. -GitLab will automatically scan the Prometheus server for metrics from known serves like Kubernetes and NGINX, and attempt to identify individual environment. The supported metrics and scan process is detailed in our [Prometheus Metric Library documentation](prometheus_library/index.md). +GitLab will automatically scan the Prometheus server for metrics from known serves like Kubernetes and NGINX, and attempt to identify individual environment. The supported metrics and scan process is detailed in our [Prometheus Metrics Library documentation](prometheus_library/index.md). You can view the performance dashboard for an environment by [clicking on the monitoring button](../../../ci/environments.md#monitoring-environments). @@ -132,7 +132,7 @@ If the "No data found" screen continues to appear, it could be due to: [prometheus-docker-image]: https://hub.docker.com/r/prom/prometheus/ [prometheus-yml]:samples/prometheus.yml [gitlab.com-ip-range]: https://gitlab.com/gitlab-com/infrastructure/issues/434 -[ci-environment-slug]: ../../../ci/variables/#predefined-variables-environment-variables +[ci-environment-slug]: ../../../ci/variables/#predefined-environment-variables [ce-8935]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8935 [ce-10408]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10408 [promgldocs]: ../../../administration/monitoring/prometheus/index.md diff --git a/doc/user/project/integrations/prometheus_library/index.md b/doc/user/project/integrations/prometheus_library/index.md index 9b9b4f6c8ca..f47884996d8 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) @@ -28,6 +29,6 @@ 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 that, GitLab uses the defined queries and fills in the environment specific variables. Typically this involves looking for the -[`$CI_ENVIRONMENT_SLUG`](../../../../ci/variables/README.md#predefined-variables-environment-variables), +[`$CI_ENVIRONMENT_SLUG`](../../../../ci/variables/README.md#predefined-environment-variables), but may also include other information such as the project's Kubernetes namespace. Each search query is defined in the [exporter specific documentation](#exporters). diff --git a/doc/user/project/integrations/prometheus_library/kubernetes.md b/doc/user/project/integrations/prometheus_library/kubernetes.md index 6b190deaa6c..7a45c87ada0 100644 --- a/doc/user/project/integrations/prometheus_library/kubernetes.md +++ b/doc/user/project/integrations/prometheus_library/kubernetes.md @@ -34,4 +34,4 @@ Prometheus needs to be deployed into the cluster and configured properly in orde In order to isolate and only display relevant CPU and Memory metrics for a given environment, GitLab needs a method to detect which containers it is running. Because these metrics are tracked at the container level, traditional Kubernetes labels are not available. -Instead, the [Deployment](https://kubernetes.io/docs/concepts/workloads/controllers/deployment/) or [DaemonSet](https://kubernetes.io/docs/concepts/workloads/controllers/daemonset/) name should begin with [CI_ENVIRONMENT_SLUG](../../../../ci/variables/README.md#predefined-variables-environment-variables). It can be followed by a `-` and additional content if desired. For example, a deployment name of `review-homepage-5620p5` would match the `review/homepage` environment. +Instead, the [Deployment](https://kubernetes.io/docs/concepts/workloads/controllers/deployment/) or [DaemonSet](https://kubernetes.io/docs/concepts/workloads/controllers/daemonset/) name should begin with [CI_ENVIRONMENT_SLUG](../../../../ci/variables/README.md#predefined-environment-variables). It can be followed by a `-` and additional content if desired. For example, a deployment name of `review-homepage-5620p5` would match the `review/homepage` environment. 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/integrations/slack.md b/doc/user/project/integrations/slack.md index af4ca35a215..bb8d276c2fc 100644 --- a/doc/user/project/integrations/slack.md +++ b/doc/user/project/integrations/slack.md @@ -23,4 +23,54 @@ The Slack Notifications Service allows your GitLab project to send events (e.g. Your Slack team will now start receiving GitLab event notifications as configured. -![Slack configuration](img/slack_configuration.png)
\ No newline at end of file +![Slack configuration](img/slack_configuration.png) + +## Troubleshooting + +If you're having trouble with the Slack integration not working, then start by +searching through the [Sidekiq logs](../../../administration/logs.md#sidekiqlog) +for errors relating to your Slack service. + +### Something went wrong on our end + +This is a generic error shown in the GitLab UI and doesn't mean much by itself. +You'll need to look in [the logs](../../../administration/logs.md#productionlog) to find +an error message and keep troubleshooting from there. + +### `certificate verify failed` + +You may see an entry similar to the following in your Sidekiq log: + +```text +2019-01-10_13:22:08.42572 2019-01-10T13:22:08.425Z 6877 TID-abcdefg ProjectServiceWorker JID-3bade5fb3dd47a85db6d78c5 ERROR: {:class=>"ProjectServiceWorker", :service_class=>"SlackService", :message=>"SSL_connect returned=1 errno=0 state=error: certificate verify failed"} +``` + +This is probably a problem either with GitLab communicating with Slack, or GitLab +communicating with itself. The former is less likely since Slack's security certificates +should _hopefully_ always be trusted. We can establish which we're dealing with by using +the below rails console script. + +```sh +# start a rails console: +sudo gitlab-rails console production + +# or for source installs: +bundle exec rails console production +``` + +```ruby +# run this in the Rails console +# replace <SLACK URL> with your actual Slack URL +result = Net::HTTP.get(URI('https://<SLACK URL>'));0 + +# replace <GITLAB URL> with your actual GitLab URL +result = Net::HTTP.get(URI('https://<GITLAB URL>'));0 +``` + +If it's an issue with GitLab not trusting HTTPS connections to itself, then you may simply +need to [add your certificate to GitLab's trusted certificates](https://docs.gitlab.com/omnibus/settings/ssl.html#install-custom-public-certificates). + +If it's an issue with GitLab not trusting connections to Slack, then the GitLab +OpenSSL trust store probably got messed up somehow. Typically this is from overriding +the trust store with `gitlab_rails['env'] = {"SSL_CERT_FILE" => "/path/to/file.pem"}` +or by accidentally modifying the default CA bundle `/opt/gitlab/embedded/ssl/certs/cacert.pem`. 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/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..b4f5a72e148 100644 --- a/doc/user/project/merge_requests/index.md +++ b/doc/user/project/merge_requests/index.md @@ -76,10 +76,10 @@ You can [search and filter the results](../../search/index.md#issues-and-merge-r ![Group Issues list view](img/group_merge_requests_list_view.png) -## Removing the source branch +## Deleting the source branch -When creating a merge request, select the "Remove source branch when merge -request accepted" option and the source branch will be removed when the merge +When creating a merge request, select the "Delete source branch when merge +request accepted" option and the source branch will be deleted when the merge request is merged. This option is also visible in an existing merge request next to the merge @@ -87,10 +87,19 @@ request button and can be selected/deselected before merging. It's only visible to users with [Maintainer permissions](../../permissions.md) in the source project. If the user viewing the merge request does not have the correct permissions to -remove the source branch and the source branch is set for removal, the merge -request widget will show the "Removes source branch" text. +delete the source branch and the source branch is set for deletion, the merge +request widget will show the "Deletes source branch" text. -![Remove source branch status](img/remove_source_branch_status.png) +![Delete 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 @@ -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/new_ci_build_permissions_model.md b/doc/user/project/new_ci_build_permissions_model.md index 9a53036b4d1..d7a1a69f29d 100644 --- a/doc/user/project/new_ci_build_permissions_model.md +++ b/doc/user/project/new_ci_build_permissions_model.md @@ -238,6 +238,6 @@ test: [triggers]: ../../ci/triggers/README.md [update-docs]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update [workhorse]: https://gitlab.com/gitlab-org/gitlab-workhorse -[jobenv]: ../../ci/variables/README.md#predefined-variables-environment-variables +[jobenv]: ../../ci/variables/README.md#predefined-environment-variables [2fa]: ../profile/account/two_factor_authentication.md [pat]: ../profile/personal_access_tokens.md diff --git a/doc/user/project/operations/error_tracking.md b/doc/user/project/operations/error_tracking.md index 2b5abc7233f..fe4b36062f7 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. @@ -10,7 +10,7 @@ Error tracking allows developers to easily discover and view the errors that the ### Deploying Sentry -You may sign up to the cloud hosted https://sentry.io or deploy your own [on-premise instance](https://docs.sentry.io/server/installation/). +You may sign up to the cloud hosted <https://sentry.io> or deploy your own [on-premise instance](https://docs.sentry.io/server/installation/). ### Enabling Sentry 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..2bb6fcd9d74 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 @@ -177,7 +178,7 @@ Supposed your repository contained the following files: ``` ├── index.html ├── css -│  └── main.css +│ └── main.css └── js └── main.js ``` @@ -332,7 +333,7 @@ public/ │ └ index.html.gz │ ├── css/ -│  └─┬ main.css +│ └─┬ main.css │ └ main.css.gz │ └── js/ @@ -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/doc/workflow/repository_mirroring.md b/doc/workflow/repository_mirroring.md index e259e6fe50c..ac26aeab137 100644 --- a/doc/workflow/repository_mirroring.md +++ b/doc/workflow/repository_mirroring.md @@ -80,10 +80,13 @@ mirror. To set up a mirror from GitLab to GitHub, you need to follow these steps: 1. Create a [GitHub personal access token](https://help.github.com/articles/creating-a-personal-access-token-for-the-command-line/) with the `public_repo` box checked. -1. Fill in the **Git repository URL** field, with the personal access token instead of a password. - For example: `https://<GitHubUsername>:<GitHubPersonalAccessToken>@github.com/group/project.git`. +1. Fill in the **Git repository URL** field using this format: `https://<your_github_username>@github.com/<your_github_group>/<your_github_project>.git`. +1. Fill in **Password** field with your GitHub personal access token. 1. Click the **Mirror repository** button. -1. Wait, or click the update button. + +The mirrored repository will be listed. For example, `https://*****:*****@github.com/<your_github_group>/<your_github_project>.git`. + +The repository will push soon. To force a push, click the appropriate button. ## Pulling from a remote repository **[STARTER]** @@ -137,8 +140,8 @@ increased each time it fails, up to a maximum amount of time. ### SSH authentication -> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/2551) for Push mirroring in [GitLab Starter](https://about.gitlab.com/pricing/) 9.5. -> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/22982) for Pull mirroring in [GitLab Core](https://about.gitlab.com/pricing/) 11.6 +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/2551) for Pull mirroring in [GitLab Starter](https://about.gitlab.com/pricing/) 9.5. +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/22982) for Push mirroring in [GitLab Core](https://about.gitlab.com/pricing/) 11.6 SSH authentication is mutual: diff --git a/lib/api/api.rb b/lib/api/api.rb index 8dd4e37ef7f..9cbfc0e35ff 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -101,6 +101,7 @@ module API mount ::API::CircuitBreakers mount ::API::Commits mount ::API::CommitStatuses + mount ::API::ContainerRegistry mount ::API::DeployKeys mount ::API::Deployments mount ::API::Environments @@ -111,6 +112,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/container_registry.rb b/lib/api/container_registry.rb new file mode 100644 index 00000000000..e4493910196 --- /dev/null +++ b/lib/api/container_registry.rb @@ -0,0 +1,143 @@ +# frozen_string_literal: true + +module API + class ContainerRegistry < Grape::API + include PaginationParams + + REGISTRY_ENDPOINT_REQUIREMENTS = API::NAMESPACE_OR_PROJECT_REQUIREMENTS.merge( + tag_name: API::NO_SLASH_URL_PART_REGEX) + + before { error!('404 Not Found', 404) unless Feature.enabled?(:container_registry_api, user_project, default_enabled: true) } + before { authorize_read_container_images! } + + params do + requires :id, type: String, desc: 'The ID of a project' + end + resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do + desc 'Get a project container repositories' do + detail 'This feature was introduced in GitLab 11.8.' + success Entities::ContainerRegistry::Repository + end + params do + use :pagination + end + get ':id/registry/repositories' do + repositories = user_project.container_repositories.ordered + + present paginate(repositories), with: Entities::ContainerRegistry::Repository + end + + desc 'Delete repository' do + detail 'This feature was introduced in GitLab 11.8.' + end + params do + requires :repository_id, type: Integer, desc: 'The ID of the repository' + end + delete ':id/registry/repositories/:repository_id', requirements: REGISTRY_ENDPOINT_REQUIREMENTS do + authorize_admin_container_image! + + DeleteContainerRepositoryWorker.perform_async(current_user.id, repository.id) + + status :accepted + end + + desc 'Get a list of repositories tags' do + detail 'This feature was introduced in GitLab 11.8.' + success Entities::ContainerRegistry::Tag + end + params do + requires :repository_id, type: Integer, desc: 'The ID of the repository' + use :pagination + end + get ':id/registry/repositories/:repository_id/tags', requirements: REGISTRY_ENDPOINT_REQUIREMENTS do + authorize_read_container_image! + + tags = Kaminari.paginate_array(repository.tags) + present paginate(tags), with: Entities::ContainerRegistry::Tag + end + + desc 'Delete repository tags (in bulk)' do + detail 'This feature was introduced in GitLab 11.8.' + end + params do + requires :repository_id, type: Integer, desc: 'The ID of the repository' + requires :name_regex, type: String, desc: 'The tag name regexp to delete, specify .* to delete all' + optional :keep_n, type: Integer, desc: 'Keep n of latest tags with matching name' + optional :older_than, type: String, desc: 'Delete older than: 1h, 1d, 1month' + end + delete ':id/registry/repositories/:repository_id/tags', requirements: REGISTRY_ENDPOINT_REQUIREMENTS do + authorize_admin_container_image! + + CleanupContainerRepositoryWorker.perform_async(current_user.id, repository.id, + declared_params.except(:repository_id)) # rubocop: disable CodeReuse/ActiveRecord + + status :accepted + end + + desc 'Get a details about repository tag' do + detail 'This feature was introduced in GitLab 11.8.' + success Entities::ContainerRegistry::TagDetails + end + params do + requires :repository_id, type: Integer, desc: 'The ID of the repository' + requires :tag_name, type: String, desc: 'The name of the tag' + end + get ':id/registry/repositories/:repository_id/tags/:tag_name', requirements: REGISTRY_ENDPOINT_REQUIREMENTS do + authorize_read_container_image! + validate_tag! + + present tag, with: Entities::ContainerRegistry::TagDetails + end + + desc 'Delete repository tag' do + detail 'This feature was introduced in GitLab 11.8.' + end + params do + requires :repository_id, type: Integer, desc: 'The ID of the repository' + requires :tag_name, type: String, desc: 'The name of the tag' + end + delete ':id/registry/repositories/:repository_id/tags/:tag_name', requirements: REGISTRY_ENDPOINT_REQUIREMENTS do + authorize_destroy_container_image! + validate_tag! + + tag.delete + + status :ok + end + end + + helpers do + def authorize_read_container_images! + authorize! :read_container_image, user_project + end + + def authorize_read_container_image! + authorize! :read_container_image, repository + end + + def authorize_update_container_image! + authorize! :update_container_image, repository + end + + def authorize_destroy_container_image! + authorize! :admin_container_image, repository + end + + def authorize_admin_container_image! + authorize! :admin_container_image, repository + end + + def repository + @repository ||= user_project.container_repositories.find(params[:repository_id]) + end + + def tag + @tag ||= repository.tag(params[:tag_name]) + end + + def validate_tag! + not_found!('Tag') unless tag.valid? + end + end + end +end diff --git a/lib/api/deployments.rb b/lib/api/deployments.rb index 8706a971a1a..eb45df31ff9 100644 --- a/lib/api/deployments.rb +++ b/lib/api/deployments.rb @@ -33,7 +33,7 @@ module API success Entities::Deployment end params do - requires :deployment_id, type: Integer, desc: 'The deployment ID' + requires :deployment_id, type: Integer, desc: 'The deployment ID' end get ':id/deployments/:deployment_id' do authorize! :read_deployment, user_project diff --git a/lib/api/entities.rb b/lib/api/entities.rb index a2a3c0a16d7..9f1394571d8 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 @@ -187,7 +190,7 @@ module API expose :custom_attributes, using: 'API::Entities::CustomAttribute', if: :with_custom_attributes # rubocop: disable CodeReuse/ActiveRecord - def self.preload_relation(projects_relation, options = {}) + def self.preload_relation(projects_relation, options = {}) # Preloading tags, should be done with using only `:tags`, # as `:tags` are defined as: `has_many :tags, through: :taggings` # N+1 is solved then by using `subject.tags.map(&:name)` @@ -271,13 +274,13 @@ module API expose :statistics, using: 'API::Entities::ProjectStatistics', if: :statistics # rubocop: disable CodeReuse/ActiveRecord - def self.preload_relation(projects_relation, options = {}) + def self.preload_relation(projects_relation, options = {}) # Preloading tags, should be done with using only `:tags`, # as `:tags` are defined as: `has_many :tags, through: :taggings` # 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 @@ -1216,8 +1223,11 @@ module API end class Trigger < Grape::Entity + include ::API::Helpers::Presentable + expose :id - expose :token, :description + expose :token + expose :description expose :created_at, :updated_at, :last_used expose :owner, using: Entities::UserBasic end diff --git a/lib/api/entities/container_registry.rb b/lib/api/entities/container_registry.rb new file mode 100644 index 00000000000..00833ca7480 --- /dev/null +++ b/lib/api/entities/container_registry.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module API + module Entities + module ContainerRegistry + class Repository < Grape::Entity + expose :id + expose :name + expose :path + expose :location + expose :created_at + end + + class Tag < Grape::Entity + expose :name + expose :path + expose :location + end + + class TagDetails < Tag + expose :revision + expose :short_revision + expose :digest + expose :created_at + expose :total_size + end + end + end +end diff --git a/lib/api/environments.rb b/lib/api/environments.rb index 633f24d3c9a..0278c6c54a5 100644 --- a/lib/api/environments.rb +++ b/lib/api/environments.rb @@ -74,7 +74,7 @@ module API success Entities::Environment end params do - requires :environment_id, type: Integer, desc: 'The environment ID' + requires :environment_id, type: Integer, desc: 'The environment ID' end delete ':id/environments/:environment_id' do authorize! :update_environment, user_project @@ -88,7 +88,7 @@ module API success Entities::Environment end params do - requires :environment_id, type: Integer, desc: 'The environment ID' + requires :environment_id, type: Integer, desc: 'The environment ID' end post ':id/environments/:environment_id/stop' do authorize! :read_environment, user_project 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/helpers/pagination.rb b/lib/api/helpers/pagination.rb index d311cbb5f7e..de59c915d66 100644 --- a/lib/api/helpers/pagination.rb +++ b/lib/api/helpers/pagination.rb @@ -146,7 +146,7 @@ module API end def add_default_pagination_headers - header 'X-Per-Page', per_page.to_s + header 'X-Per-Page', per_page.to_s end def add_navigation_links(next_page_params) @@ -178,15 +178,26 @@ module API end def paginate(relation) - relation = add_default_order(relation) - - relation.page(params[:page]).per(params[:per_page]).tap do |data| + paginate_with_limit_optimization(add_default_order(relation)).tap do |data| add_pagination_headers(data) end end private + def paginate_with_limit_optimization(relation) + pagination_data = relation.page(params[:page]).per(params[:per_page]) + return pagination_data unless pagination_data.is_a?(ActiveRecord::Relation) + return pagination_data unless Feature.enabled?(:api_kaminari_count_with_limit) + + limited_total_count = pagination_data.total_count_with_limit + if limited_total_count > Kaminari::ActiveRecordRelationMethods::MAX_COUNT_LIMIT + pagination_data.without_count + else + pagination_data + end + end + # rubocop: disable CodeReuse/ActiveRecord def add_default_order(relation) if relation.is_a?(ActiveRecord::Relation) && relation.order_values.empty? diff --git a/lib/api/helpers/presentable.rb b/lib/api/helpers/presentable.rb new file mode 100644 index 00000000000..973c2132efe --- /dev/null +++ b/lib/api/helpers/presentable.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module API + module Helpers + ## + # This module makes it possible to use `app/presenters` with + # Grape Entities. It instantiates model presenter and passes + # options defined in the API endpoint to the presenter itself. + # + # present object, with: Entities::Something, + # current_user: current_user, + # another_option: 'my options' + # + # Example above will make `current_user` and `another_option` + # values available in the subclass of `Gitlab::View::Presenter` + # thorough a separate method in the presenter. + # + # The model class needs to have `::Presentable` module mixed in + # if you want to use `API::Helpers::Presentable`. + # + module Presentable + extend ActiveSupport::Concern + + def initialize(object, options = {}) + super(object.present(options), options) + end + end + end +end diff --git a/lib/api/helpers/runner.rb b/lib/api/helpers/runner.rb index 45d0343bc89..16df8e830e1 100644 --- a/lib/api/helpers/runner.rb +++ b/lib/api/helpers/runner.rb @@ -26,7 +26,7 @@ module API end def get_runner_ip - { ip_address: request.ip } + { ip_address: request.env["HTTP_X_FORWARDED_FOR"] || request.ip } end def current_runner 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/issues.rb b/lib/api/issues.rb index dac700482b4..afa3ac80121 100644 --- a/lib/api/issues.rb +++ b/lib/api/issues.rb @@ -294,7 +294,7 @@ module API end # rubocop: enable CodeReuse/ActiveRecord - desc 'List merge requests that are related to the issue' do + desc 'List merge requests that are related to the issue' do success Entities::MergeRequestBasic end params do @@ -318,7 +318,7 @@ module API present paginate(merge_requests), with: Entities::MergeRequestBasic, current_user: current_user, project: user_project end - desc 'List merge requests closing issue' do + desc 'List merge requests closing issue' do success Entities::MergeRequestBasic end params do @@ -335,7 +335,7 @@ module API end # rubocop: enable CodeReuse/ActiveRecord - desc 'List participants for an issue' do + desc 'List participants for an issue' do success Entities::UserBasic end params do diff --git a/lib/api/jobs.rb b/lib/api/jobs.rb index 45c694b6448..59f0dbe8a9b 100644 --- a/lib/api/jobs.rb +++ b/lib/api/jobs.rb @@ -52,7 +52,7 @@ module API success Entities::Job end params do - requires :pipeline_id, type: Integer, desc: 'The pipeline ID' + requires :pipeline_id, type: Integer, desc: 'The pipeline ID' use :optional_scope use :pagination end diff --git a/lib/api/labels.rb b/lib/api/labels.rb index 2e676b0aa6b..d5eb2b94669 100644 --- a/lib/api/labels.rb +++ b/lib/api/labels.rb @@ -69,7 +69,7 @@ module API success Entities::Label end params do - requires :name, type: String, desc: 'The name of the label to be updated' + requires :name, type: String, desc: 'The name of the label to be updated' optional :new_name, type: String, desc: 'The new name of the label' optional :color, type: String, desc: "The new color of the label given in 6-digit hex notation with leading '#' sign (e.g. #FFAABB) or one of the allowed CSS color names" optional :description, type: String, desc: 'The new description of label' diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb index 8c1951cc535..132b19164d0 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -179,7 +179,7 @@ module API optional :assignee_id, type: Integer, desc: 'The ID of a user to assign the merge request' optional :milestone_id, type: Integer, desc: 'The ID of a milestone to assign the merge request' optional :labels, type: String, desc: 'Comma-separated list of label names' - optional :remove_source_branch, type: Boolean, desc: 'Remove source branch when merging' + optional :remove_source_branch, type: Boolean, desc: 'Delete source branch when merging' optional :allow_collaboration, type: Boolean, desc: 'Allow commits from members who can merge to the target branch' optional :allow_maintainer_to_push, type: Boolean, as: :allow_collaboration, desc: '[deprecated] See allow_collaboration' optional :squash, type: Grape::API::Boolean, desc: 'When true, the commits will be squashed into a single commit on merge' diff --git a/lib/api/pipeline_schedules.rb b/lib/api/pipeline_schedules.rb index 47b711917e2..c86b50d3736 100644 --- a/lib/api/pipeline_schedules.rb +++ b/lib/api/pipeline_schedules.rb @@ -32,7 +32,7 @@ module API success Entities::PipelineScheduleDetails end params do - requires :pipeline_schedule_id, type: Integer, desc: 'The pipeline schedule id' + requires :pipeline_schedule_id, type: Integer, desc: 'The pipeline schedule id' end get ':id/pipeline_schedules/:pipeline_schedule_id' do present pipeline_schedule, with: Entities::PipelineScheduleDetails @@ -87,7 +87,7 @@ module API success Entities::PipelineScheduleDetails end params do - requires :pipeline_schedule_id, type: Integer, desc: 'The pipeline schedule id' + requires :pipeline_schedule_id, type: Integer, desc: 'The pipeline schedule id' end post ':id/pipeline_schedules/:pipeline_schedule_id/take_ownership' do authorize! :update_pipeline_schedule, pipeline_schedule @@ -103,7 +103,7 @@ module API success Entities::PipelineScheduleDetails end params do - requires :pipeline_schedule_id, type: Integer, desc: 'The pipeline schedule id' + requires :pipeline_schedule_id, type: Integer, desc: 'The pipeline schedule id' end delete ':id/pipeline_schedules/:pipeline_schedule_id' do authorize! :admin_pipeline_schedule, pipeline_schedule diff --git a/lib/api/pipelines.rb b/lib/api/pipelines.rb index 7a7b23d2bbb..ac8fe98e55e 100644 --- a/lib/api/pipelines.rb +++ b/lib/api/pipelines.rb @@ -42,7 +42,7 @@ module API success Entities::Pipeline end params do - requires :ref, type: String, desc: 'Reference' + requires :ref, type: String, desc: 'Reference' optional :variables, Array, desc: 'Array of variables available in the pipeline' end # rubocop: disable CodeReuse/ActiveRecord @@ -76,7 +76,7 @@ module API requires :pipeline_id, type: Integer, desc: 'The pipeline ID' end get ':id/pipelines/:pipeline_id' do - authorize! :read_pipeline, user_project + authorize! :read_pipeline, pipeline present pipeline, with: Entities::Pipeline end @@ -101,10 +101,10 @@ module API success Entities::Pipeline end params do - requires :pipeline_id, type: Integer, desc: 'The pipeline ID' + requires :pipeline_id, type: Integer, desc: 'The pipeline ID' end post ':id/pipelines/:pipeline_id/retry' do - authorize! :update_pipeline, user_project + authorize! :update_pipeline, pipeline pipeline.retry_failed(current_user) @@ -116,10 +116,10 @@ module API success Entities::Pipeline end params do - requires :pipeline_id, type: Integer, desc: 'The pipeline ID' + requires :pipeline_id, type: Integer, desc: 'The pipeline ID' end post ':id/pipelines/:pipeline_id/cancel' do - authorize! :update_pipeline, user_project + authorize! :update_pipeline, pipeline pipeline.cancel_running diff --git a/lib/api/projects_relation_builder.rb b/lib/api/projects_relation_builder.rb index 8edcfea7c93..263468c9aa6 100644 --- a/lib/api/projects_relation_builder.rb +++ b/lib/api/projects_relation_builder.rb @@ -11,7 +11,7 @@ module API projects_relation end - def preload_relation(projects_relation, options = {}) + def preload_relation(projects_relation, options = {}) projects_relation end diff --git a/lib/api/releases.rb b/lib/api/releases.rb index 576fee51db0..cb85028f22c 100644 --- a/lib/api/releases.rb +++ b/lib/api/releases.rb @@ -97,7 +97,7 @@ module API success Entities::Release end params do - requires :tag_name, type: String, desc: 'The name of the tag', as: :tag + requires :tag_name, type: String, desc: 'The name of the tag', as: :tag end delete ':id/releases/:tag_name', requirements: RELEASE_ENDPOINT_REQUIREMETS do authorize_destroy_release! diff --git a/lib/api/services.rb b/lib/api/services.rb index d60f0f5f08d..637b5a8a89a 100644 --- a/lib/api/services.rb +++ b/lib/api/services.rb @@ -763,7 +763,7 @@ module API params do requires :id, type: String, desc: 'The ID of a project' end - resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do + resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do before { authenticate! } before { authorize_admin_project } @@ -842,7 +842,7 @@ module API params do requires :id, type: String, desc: 'The ID of a project' end - resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do + resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do desc "Trigger a slash command for #{service_slug}" do detail 'Added in GitLab 8.13' 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/todos.rb b/lib/api/todos.rb index d2c8cf7c1aa..64ac8ece56c 100644 --- a/lib/api/todos.rb +++ b/lib/api/todos.rb @@ -14,7 +14,7 @@ module API params do requires :id, type: String, desc: 'The ID of a project' end - resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do + resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do ISSUABLE_TYPES.each do |type, finder| type_id_str = "#{type.singularize}_iid".to_sym diff --git a/lib/api/triggers.rb b/lib/api/triggers.rb index 3ce1529f259..8fc7c7361e1 100644 --- a/lib/api/triggers.rb +++ b/lib/api/triggers.rb @@ -7,7 +7,7 @@ module API params do requires :id, type: String, desc: 'The ID of a project' end - resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do + resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do desc 'Trigger a GitLab project pipeline' do success Entities::Pipeline end @@ -51,7 +51,7 @@ module API triggers = user_project.triggers.includes(:trigger_requests) - present paginate(triggers), with: Entities::Trigger + present paginate(triggers), with: Entities::Trigger, current_user: current_user end # rubocop: enable CodeReuse/ActiveRecord @@ -59,7 +59,7 @@ module API success Entities::Trigger end params do - requires :trigger_id, type: Integer, desc: 'The trigger ID' + requires :trigger_id, type: Integer, desc: 'The trigger ID' end get ':id/triggers/:trigger_id' do authenticate! @@ -68,14 +68,14 @@ module API trigger = user_project.triggers.find(params.delete(:trigger_id)) break not_found!('Trigger') unless trigger - present trigger, with: Entities::Trigger + present trigger, with: Entities::Trigger, current_user: current_user end desc 'Create a trigger' do success Entities::Trigger end params do - requires :description, type: String, desc: 'The trigger description' + requires :description, type: String, desc: 'The trigger description' end post ':id/triggers' do authenticate! @@ -85,7 +85,7 @@ module API declared_params(include_missing: false).merge(owner: current_user)) if trigger.valid? - present trigger, with: Entities::Trigger + present trigger, with: Entities::Trigger, current_user: current_user else render_validation_error!(trigger) end @@ -106,7 +106,7 @@ module API break not_found!('Trigger') unless trigger if trigger.update(declared_params(include_missing: false)) - present trigger, with: Entities::Trigger + present trigger, with: Entities::Trigger, current_user: current_user else render_validation_error!(trigger) end @@ -116,7 +116,7 @@ module API success Entities::Trigger end params do - requires :trigger_id, type: Integer, desc: 'The trigger ID' + requires :trigger_id, type: Integer, desc: 'The trigger ID' end post ':id/triggers/:trigger_id/take_ownership' do authenticate! @@ -127,7 +127,7 @@ module API if trigger.update(owner: current_user) status :ok - present trigger, with: Entities::Trigger + present trigger, with: Entities::Trigger, current_user: current_user else render_validation_error!(trigger) end @@ -137,7 +137,7 @@ module API success Entities::Trigger end params do - requires :trigger_id, type: Integer, desc: 'The trigger ID' + requires :trigger_id, type: Integer, desc: 'The trigger ID' end delete ':id/triggers/:trigger_id' do authenticate! diff --git a/lib/api/variables.rb b/lib/api/variables.rb index f7cae2251c2..148deb86c4c 100644 --- a/lib/api/variables.rb +++ b/lib/api/variables.rb @@ -11,7 +11,7 @@ module API requires :id, type: String, desc: 'The ID of a project' end - resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do + resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do desc 'Get project variables' do success Entities::Variable end 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/autolink_filter.rb b/lib/banzai/filter/autolink_filter.rb index deda4b1872e..f3061bad4ff 100644 --- a/lib/banzai/filter/autolink_filter.rb +++ b/lib/banzai/filter/autolink_filter.rb @@ -8,6 +8,10 @@ module Banzai # # Based on HTML::Pipeline::AutolinkFilter # + # Note that our CommonMark parser, `commonmarker` (using the autolink extension) + # handles standard autolinking, like http/https. We detect additional + # schemes (smb, rdar, etc). + # # Context options: # :autolink - Boolean, skips all processing done by this filter when false # :link_attr - Hash of attributes for the generated links @@ -107,10 +111,13 @@ module Banzai end end - # match has come from node.to_html above, so we know it's encoded - # correctly. + # Since this came from a Text node, make sure the new href is encoded. + # `commonmarker` percent encodes the domains of links it handles, so + # do the same (instead of using `normalized_encode`). + href_safe = Addressable::URI.encode(match).html_safe + html_safe_match = match.html_safe - options = link_options.merge(href: html_safe_match) + options = link_options.merge(href: href_safe) content_tag(:a, html_safe_match, options) + dropped end diff --git a/lib/banzai/filter/emoji_filter.rb b/lib/banzai/filter/emoji_filter.rb index c87948a30bf..fa1690f73ad 100644 --- a/lib/banzai/filter/emoji_filter.rb +++ b/lib/banzai/filter/emoji_filter.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true +# Generated HTML is transformed back to GFM by app/assets/javascripts/behaviors/markdown/nodes/emoji.js module Banzai module Filter # HTML filter that replaces :emoji: and unicode with images. diff --git a/lib/banzai/filter/external_link_filter.rb b/lib/banzai/filter/external_link_filter.rb index 4f60b6f84c6..61ee3eac216 100644 --- a/lib/banzai/filter/external_link_filter.rb +++ b/lib/banzai/filter/external_link_filter.rb @@ -4,17 +4,29 @@ module Banzai module Filter # HTML Filter to modify the attributes of external links class ExternalLinkFilter < HTML::Pipeline::Filter - SCHEMES = ['http', 'https', nil].freeze + SCHEMES = ['http', 'https', nil].freeze + RTLO = "\u202E".freeze + ENCODED_RTLO = '%E2%80%AE'.freeze def call links.each do |node| - uri = uri(node['href'].to_s) - - node.set_attribute('href', uri.to_s) if uri + # URI.parse does stricter checking on the url than Addressable, + # such as on `mailto:` links. Since we've been using it, do an + # initial parse for validity and then use Addressable + # for IDN support, etc + uri = uri_strict(node['href'].to_s) + if uri + node.set_attribute('href', uri.to_s) + addressable_uri = addressable_uri(node['href']) + else + addressable_uri = nil + end - if SCHEMES.include?(uri&.scheme) && !internal_url?(uri) - node.set_attribute('rel', 'nofollow noreferrer noopener') - node.set_attribute('target', '_blank') + unless internal_url?(addressable_uri) + punycode_autolink_node!(addressable_uri, node) + sanitize_link_text!(node) + add_malicious_tooltip!(addressable_uri, node) + add_nofollow!(addressable_uri, node) end end @@ -23,12 +35,18 @@ module Banzai private - def uri(href) + def uri_strict(href) URI.parse(href) rescue URI::Error nil end + def addressable_uri(href) + Addressable::URI.parse(href) + rescue Addressable::URI::InvalidURIError + nil + end + def links query = 'descendant-or-self::a[@href and not(@href = "")]' doc.xpath(query) @@ -45,6 +63,57 @@ module Banzai def internal_url @internal_url ||= URI.parse(Gitlab.config.gitlab.url) end + + # Only replace an autolink with an IDN with it's punycode + # version if we need emailable links. Otherwise let it + # be shown normally and the tooltips will show the + # punycode version. + def punycode_autolink_node!(uri, node) + return unless uri + return unless context[:emailable_links] + + unencoded_uri_str = Addressable::URI.unencode(node['href']) + + if unencoded_uri_str == node.content && idn?(uri) + node.content = uri.normalize + end + end + + # escape any right-to-left (RTLO) characters in link text + def sanitize_link_text!(node) + node.inner_html = node.inner_html.gsub(RTLO, ENCODED_RTLO) + end + + # If the domain is an international domain name (IDN), + # let's expose with a tooltip in case it's intended + # to be malicious. This is particularly useful for links + # where the link text is not the same as the actual link. + # We will continue to show the unicode version of the domain + # in autolinked link text, which could contain emojis, etc. + # + # Also show the tooltip if the url contains the RTLO character, + # as this is an indicator of a malicious link + def add_malicious_tooltip!(uri, node) + if idn?(uri) || has_encoded_rtlo?(uri) + node.add_class('has-tooltip') + node.set_attribute('title', uri.normalize) + end + end + + def add_nofollow!(uri, node) + if SCHEMES.include?(uri&.scheme) + node.set_attribute('rel', 'nofollow noreferrer noopener') + node.set_attribute('target', '_blank') + end + end + + def idn?(uri) + uri&.normalized_host&.start_with?('xn--') + end + + def has_encoded_rtlo?(uri) + uri&.to_s&.include?(ENCODED_RTLO) + end 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/image_lazy_load_filter.rb b/lib/banzai/filter/image_lazy_load_filter.rb index afaee70f351..d8b9eb29cf5 100644 --- a/lib/banzai/filter/image_lazy_load_filter.rb +++ b/lib/banzai/filter/image_lazy_load_filter.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true +# Generated HTML is transformed back to GFM by app/assets/javascripts/behaviors/markdown/nodes/image.js module Banzai module Filter # HTML filter that moves the value of image `src` attributes to `data-src` diff --git a/lib/banzai/filter/image_link_filter.rb b/lib/banzai/filter/image_link_filter.rb index 884a94fb761..01237303c27 100644 --- a/lib/banzai/filter/image_link_filter.rb +++ b/lib/banzai/filter/image_link_filter.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true +# Generated HTML is transformed back to GFM by app/assets/javascripts/behaviors/markdown/nodes/image.js module Banzai module Filter # HTML filter that wraps links around inline images. diff --git a/lib/banzai/filter/inline_diff_filter.rb b/lib/banzai/filter/inline_diff_filter.rb index e9ddc6e0e3d..5a1c0bee32d 100644 --- a/lib/banzai/filter/inline_diff_filter.rb +++ b/lib/banzai/filter/inline_diff_filter.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true +# Generated HTML is transformed back to GFM by app/assets/javascripts/behaviors/markdown/marks/inline_diff.js module Banzai module Filter class InlineDiffFilter < HTML::Pipeline::Filter diff --git a/lib/banzai/filter/markdown_engines/common_mark.rb b/lib/banzai/filter/markdown_engines/common_mark.rb index e52c0d15b31..d3af776db05 100644 --- a/lib/banzai/filter/markdown_engines/common_mark.rb +++ b/lib/banzai/filter/markdown_engines/common_mark.rb @@ -32,8 +32,13 @@ module Banzai :DEFAULT # default rendering system. Nothing special. ].freeze - def initialize - @renderer = Banzai::Renderer::CommonMark::HTML.new(options: RENDER_OPTIONS) + RENDER_OPTIONS_SOURCEPOS = RENDER_OPTIONS + [ + :SOURCEPOS # enable embedding of source position information + ].freeze + + def initialize(context) + @context = context + @renderer = Banzai::Renderer::CommonMark::HTML.new(options: render_options) end def render(text) @@ -41,6 +46,12 @@ module Banzai @renderer.render(doc) end + + private + + def render_options + @context[:no_sourcepos] ? RENDER_OPTIONS : RENDER_OPTIONS_SOURCEPOS + end end end end diff --git a/lib/banzai/filter/markdown_engines/redcarpet.rb b/lib/banzai/filter/markdown_engines/redcarpet.rb index ec150d041ff..5b3f75096b1 100644 --- a/lib/banzai/filter/markdown_engines/redcarpet.rb +++ b/lib/banzai/filter/markdown_engines/redcarpet.rb @@ -20,7 +20,7 @@ module Banzai tables: true }.freeze - def initialize + def initialize(context = nil) html_renderer = Banzai::Renderer::Redcarpet::HTML.new @renderer = ::Redcarpet::Markdown.new(html_renderer, OPTIONS) end diff --git a/lib/banzai/filter/markdown_filter.rb b/lib/banzai/filter/markdown_filter.rb index cdf758472c1..242e39f5495 100644 --- a/lib/banzai/filter/markdown_filter.rb +++ b/lib/banzai/filter/markdown_filter.rb @@ -6,7 +6,7 @@ module Banzai def initialize(text, context = nil, result = nil) super(text, context, result) - @renderer = renderer(context[:markdown_engine]).new + @renderer = renderer(context[:markdown_engine]).new(context) @text = @text.delete("\r") end diff --git a/lib/banzai/filter/math_filter.rb b/lib/banzai/filter/math_filter.rb index 9d1bc3cf60c..8dd5a8979c8 100644 --- a/lib/banzai/filter/math_filter.rb +++ b/lib/banzai/filter/math_filter.rb @@ -2,6 +2,9 @@ require 'uri' +# Generated HTML is transformed back to GFM by: +# - app/assets/javascripts/behaviors/markdown/marks/math.js +# - app/assets/javascripts/behaviors/markdown/nodes/code_block.js module Banzai module Filter # HTML filter that adds class="code math" and removes the dollar sign in $`2+2`$. diff --git a/lib/banzai/filter/mermaid_filter.rb b/lib/banzai/filter/mermaid_filter.rb index 7c8b165a330..f0adb83af8a 100644 --- a/lib/banzai/filter/mermaid_filter.rb +++ b/lib/banzai/filter/mermaid_filter.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true +# Generated HTML is transformed back to GFM by app/assets/javascripts/behaviors/markdown/nodes/code_block.js module Banzai module Filter class MermaidFilter < HTML::Pipeline::Filter 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/reference_filter.rb b/lib/banzai/filter/reference_filter.rb index e5164e7f72a..42f9b3a689c 100644 --- a/lib/banzai/filter/reference_filter.rb +++ b/lib/banzai/filter/reference_filter.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true +# Generated HTML is transformed back to GFM by app/assets/javascripts/behaviors/markdown/nodes/reference.js module Banzai module Filter # Base class for GitLab Flavored Markdown reference filters. diff --git a/lib/banzai/filter/relative_link_filter.rb b/lib/banzai/filter/relative_link_filter.rb index 7acbc933adc..93e6d6470f1 100644 --- a/lib/banzai/filter/relative_link_filter.rb +++ b/lib/banzai/filter/relative_link_filter.rb @@ -58,6 +58,8 @@ module Banzai path_parts.unshift(relative_url_root, 'groups', group.full_path, '-') elsif project path_parts.unshift(relative_url_root, project.full_path) + else + path_parts.unshift(relative_url_root) end begin diff --git a/lib/banzai/filter/sanitization_filter.rb b/lib/banzai/filter/sanitization_filter.rb index 8ba09290e6d..a4a06eae7b7 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 @@ -41,14 +41,16 @@ module Banzai whitelist[:elements].push('abbr') whitelist[:attributes]['abbr'] = %w(title) + # Allow the 'data-sourcepos' from CommonMark on all elements + whitelist[:attributes][:all].push('data-sourcepos') + # Disallow `name` attribute globally, allow on `a` 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 +59,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 +120,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/filter/spaced_link_filter.rb b/lib/banzai/filter/spaced_link_filter.rb index c6a3a763c23..00dbf2d3130 100644 --- a/lib/banzai/filter/spaced_link_filter.rb +++ b/lib/banzai/filter/spaced_link_filter.rb @@ -73,7 +73,8 @@ module Banzai html = Banzai::Filter::MarkdownFilter.call(transform_markdown(match), context) # link is wrapped in a <p>, so strip that off - html.sub('<p>', '').chomp('</p>') + p_node = Nokogiri::HTML.fragment(html).at_css('p') + p_node ? p_node.children.to_html : html end def spaced_link_filter(text) diff --git a/lib/banzai/filter/suggestion_filter.rb b/lib/banzai/filter/suggestion_filter.rb index 307ea449140..9950db373d8 100644 --- a/lib/banzai/filter/suggestion_filter.rb +++ b/lib/banzai/filter/suggestion_filter.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true +# Generated HTML is transformed back to GFM by app/assets/javascripts/behaviors/markdown/nodes/code_block.js module Banzai module Filter class SuggestionFilter < HTML::Pipeline::Filter diff --git a/lib/banzai/filter/syntax_highlight_filter.rb b/lib/banzai/filter/syntax_highlight_filter.rb index 18e5e9185de..bcf77861f10 100644 --- a/lib/banzai/filter/syntax_highlight_filter.rb +++ b/lib/banzai/filter/syntax_highlight_filter.rb @@ -3,6 +3,7 @@ require 'rouge/plugins/common_mark' require 'rouge/plugins/redcarpet' +# Generated HTML is transformed back to GFM by app/assets/javascripts/behaviors/markdown/nodes/code_block.js module Banzai module Filter # HTML Filter to highlight fenced code blocks diff --git a/lib/banzai/filter/table_of_contents_filter.rb b/lib/banzai/filter/table_of_contents_filter.rb index c6d1e028eaa..f2ae17b44fa 100644 --- a/lib/banzai/filter/table_of_contents_filter.rb +++ b/lib/banzai/filter/table_of_contents_filter.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true +# Generated HTML is transformed back to GFM by app/assets/javascripts/behaviors/markdown/nodes/table_of_contents.js module Banzai module Filter # HTML filter that adds an anchor child element to all Headers in a diff --git a/lib/banzai/filter/task_list_filter.rb b/lib/banzai/filter/task_list_filter.rb index ef35a49edcb..c6b402575cb 100644 --- a/lib/banzai/filter/task_list_filter.rb +++ b/lib/banzai/filter/task_list_filter.rb @@ -2,6 +2,10 @@ require 'task_list/filter' +# Generated HTML is transformed back to GFM by: +# - app/assets/javascripts/behaviors/markdown/nodes/ordered_task_list.js +# - app/assets/javascripts/behaviors/markdown/nodes/task_list.js +# - app/assets/javascripts/behaviors/markdown/nodes/task_list_item.js module Banzai module Filter class TaskListFilter < TaskList::Filter diff --git a/lib/banzai/filter/video_link_filter.rb b/lib/banzai/filter/video_link_filter.rb index 0fb59c914c3..0fff104cf91 100644 --- a/lib/banzai/filter/video_link_filter.rb +++ b/lib/banzai/filter/video_link_filter.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true +# Generated HTML is transformed back to GFM by app/assets/javascripts/behaviors/markdown/nodes/video.js module Banzai module Filter # Find every image that isn't already wrapped in an `a` tag, and that has diff --git a/lib/banzai/pipeline/atom_pipeline.rb b/lib/banzai/pipeline/atom_pipeline.rb index 13a342351b6..c632910585d 100644 --- a/lib/banzai/pipeline/atom_pipeline.rb +++ b/lib/banzai/pipeline/atom_pipeline.rb @@ -6,7 +6,8 @@ module Banzai def self.transform_context(context) super(context).merge( only_path: false, - xhtml: true + xhtml: true, + no_sourcepos: true ) end end diff --git a/lib/banzai/pipeline/broadcast_message_pipeline.rb b/lib/banzai/pipeline/broadcast_message_pipeline.rb index a3d63e0aaf5..580b5b72474 100644 --- a/lib/banzai/pipeline/broadcast_message_pipeline.rb +++ b/lib/banzai/pipeline/broadcast_message_pipeline.rb @@ -14,6 +14,12 @@ module Banzai Filter::ExternalLinkFilter ] end + + def self.transform_context(context) + super(context).merge( + no_sourcepos: true + ) + end end end end diff --git a/lib/banzai/pipeline/email_pipeline.rb b/lib/banzai/pipeline/email_pipeline.rb index 2c08581ce0d..13e6a990407 100644 --- a/lib/banzai/pipeline/email_pipeline.rb +++ b/lib/banzai/pipeline/email_pipeline.rb @@ -11,7 +11,9 @@ module Banzai def self.transform_context(context) super(context).merge( - only_path: false + only_path: false, + emailable_links: true, + no_sourcepos: true ) end end diff --git a/lib/banzai/pipeline/gfm_pipeline.rb b/lib/banzai/pipeline/gfm_pipeline.rb index 5f13a6d6cde..30cafd11834 100644 --- a/lib/banzai/pipeline/gfm_pipeline.rb +++ b/lib/banzai/pipeline/gfm_pipeline.rb @@ -3,11 +3,11 @@ module Banzai module Pipeline class GfmPipeline < BasePipeline - # These filters convert GitLab Flavored Markdown (GFM) to HTML. - # The handlers defined in app/assets/javascripts/behaviors/markdown/copy_as_gfm.js - # consequently convert that same HTML to GFM to be copied to the clipboard. - # Every filter that generates HTML from GFM should have a handler in - # app/assets/javascripts/behaviors/markdown/copy_as_gfm.js, in reverse order. + # These filters transform GitLab Flavored Markdown (GFM) to HTML. + # The nodes and marks referenced in app/assets/javascripts/behaviors/markdown/editor_extensions.js + # consequently transform that same HTML to GFM to be copied to the clipboard. + # Every filter that generates HTML from GFM should have a node or mark in + # app/assets/javascripts/behaviors/markdown/editor_extensions.js. # The GFM-to-HTML-to-GFM cycle is tested in spec/features/copy_as_gfm_spec.rb. def self.filters @filters ||= FilterArray[ @@ -30,6 +30,7 @@ module Banzai Filter::AutolinkFilter, Filter::ExternalLinkFilter, Filter::SuggestionFilter, + Filter::FootnoteFilter, *reference_filters, diff --git a/lib/banzai/pipeline/single_line_pipeline.rb b/lib/banzai/pipeline/single_line_pipeline.rb index 61ff7b0bcce..72374207a8f 100644 --- a/lib/banzai/pipeline/single_line_pipeline.rb +++ b/lib/banzai/pipeline/single_line_pipeline.rb @@ -27,6 +27,12 @@ module Banzai Filter::CommitReferenceFilter ] end + + def self.transform_context(context) + super(context).merge( + no_sourcepos: true + ) + end end end end diff --git a/lib/container_registry/tag.rb b/lib/container_registry/tag.rb index 8633e764f90..ef41dc560c9 100644 --- a/lib/container_registry/tag.rb +++ b/lib/container_registry/tag.rb @@ -2,6 +2,8 @@ module ContainerRegistry class Tag + include Gitlab::Utils::StrongMemoize + attr_reader :repository, :name delegate :registry, :client, to: :repository @@ -15,6 +17,10 @@ module ContainerRegistry manifest.present? end + def latest? + name == "latest" + end + def v1? manifest && manifest['schemaVersion'] == 1 end @@ -24,7 +30,9 @@ module ContainerRegistry end def manifest - @manifest ||= client.repository_manifest(repository.path, name) + strong_memoize(:manifest) do + client.repository_manifest(repository.path, name) + end end def path @@ -42,36 +50,44 @@ module ContainerRegistry end def digest - @digest ||= client.repository_tag_digest(repository.path, name) + strong_memoize(:digest) do + client.repository_tag_digest(repository.path, name) + end end def config_blob - return @config_blob if defined?(@config_blob) return unless manifest && manifest['config'] - @config_blob = repository.blob(manifest['config']) + strong_memoize(:config_blob) do + repository.blob(manifest['config']) + end end def config - return unless config_blob + return unless config_blob&.data - @config ||= ContainerRegistry::Config.new(self, config_blob) if config_blob.data + strong_memoize(:config) do + ContainerRegistry::Config.new(self, config_blob) + end end def created_at return unless config - @created_at ||= DateTime.rfc3339(config['created']) + strong_memoize(:created_at) do + DateTime.rfc3339(config['created']) + end end def layers - return @layers if defined?(@layers) return unless manifest - layers = manifest['layers'] || manifest['fsLayers'] + strong_memoize(:layers) do + layers = manifest['layers'] || manifest['fsLayers'] - @layers = layers.map do |layer| - repository.blob(layer) + layers.map do |layer| + repository.blob(layer) + end end end 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 074d04fc32a..e073450283b 100644 --- a/lib/gitlab.rb +++ b/lib/gitlab.rb @@ -7,6 +7,14 @@ module Gitlab Pathname.new(File.expand_path('..', __dir__)) end + def self.version_info + Gitlab::VersionInfo.parse(Gitlab::VERSION) + end + + def self.pre_release? + VERSION.include?('pre') + end + def self.config Settings end @@ -27,52 +35,12 @@ 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? @@ -89,4 +57,12 @@ module Gitlab def self.dev_env_or_com? Rails.env.development? || org? || com? end + + def self.process_name + return 'sidekiq' if Sidekiq.server? + return 'console' if defined?(Rails::Console) + return 'test' if Rails.env.test? + + '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/auth/o_auth/user.rb b/lib/gitlab/auth/o_auth/user.rb index a4e8a41b246..f38c5d57c44 100644 --- a/lib/gitlab/auth/o_auth/user.rb +++ b/lib/gitlab/auth/o_auth/user.rb @@ -46,7 +46,7 @@ module Gitlab gl_user.block if block_after_save - log.info "(#{provider}) saving user #{auth_hash.email} from login with extern_uid => #{auth_hash.uid}" + log.info "(#{provider}) saving user #{auth_hash.email} from login with admin => #{gl_user.admin}, extern_uid => #{auth_hash.uid}" gl_user rescue ActiveRecord::RecordInvalid => e log.info "(#{provider}) Error saving user #{auth_hash.uid} (#{auth_hash.email}): #{gl_user.errors.full_messages}" 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/background_migration/migrate_stage_status.rb b/lib/gitlab/background_migration/migrate_stage_status.rb index 0e5c7f092f2..6a29a632577 100644 --- a/lib/gitlab/background_migration/migrate_stage_status.rb +++ b/lib/gitlab/background_migration/migrate_stage_status.rb @@ -16,10 +16,10 @@ module Gitlab scope :running, -> { where(status: 'running') } scope :pending, -> { where(status: 'pending') } scope :success, -> { where(status: 'success') } - scope :failed, -> { where(status: 'failed') } - scope :canceled, -> { where(status: 'canceled') } - scope :skipped, -> { where(status: 'skipped') } - scope :manual, -> { where(status: 'manual') } + scope :failed, -> { where(status: 'failed') } + scope :canceled, -> { where(status: 'canceled') } + scope :skipped, -> { where(status: 'skipped') } + scope :manual, -> { where(status: 'manual') } scope :failed_but_allowed, -> do where(allow_failure: true, status: [:failed, :canceled]) diff --git a/lib/gitlab/bitbucket_server_import/importer.rb b/lib/gitlab/bitbucket_server_import/importer.rb index 28cfb46e2d4..dbbedd5dcbe 100644 --- a/lib/gitlab/bitbucket_server_import/importer.rb +++ b/lib/gitlab/bitbucket_server_import/importer.rb @@ -132,7 +132,7 @@ module Gitlab project.repository.fetch_as_mirror(project.import_url, refmap: self.class.refmap, remote_name: REMOTE_NAME) log_info(stage: 'import_repository', message: 'finished import') - rescue Gitlab::Shell::Error, Gitlab::Git::RepositoryMirroring::RemoteError => e + rescue Gitlab::Shell::Error => e log_error(stage: 'import_repository', message: 'failed import', error: e.message) # Expire cache to prevent scenarios such as: @@ -140,7 +140,7 @@ module Gitlab # 2. Retried import, repo is broken or not imported but +exists?+ still returns true project.repository.expire_content_cache if project.repository_exists? - raise e.message + raise end # Bitbucket Server keeps tracks of references for open pull requests in diff --git a/lib/gitlab/ci/ansi2html.rb b/lib/gitlab/ci/ansi2html.rb index 974b5ad6877..4dcb3869d4f 100644 --- a/lib/gitlab/ci/ansi2html.rb +++ b/lib/gitlab/ci/ansi2html.rb @@ -31,7 +31,7 @@ module Gitlab end class Converter - def on_0(_) reset() end + def on_0(_) reset end def on_1(_) enable(STYLE_SWITCHES[:bold]) end @@ -177,7 +177,7 @@ module Gitlab end end - close_open_tags() + close_open_tags OpenStruct.new( html: @out.force_encoding(Encoding.default_external), @@ -194,7 +194,7 @@ module Gitlab action = scanner[1] timestamp = scanner[2] section = scanner[3] - line = scanner.matched()[0...-5] # strips \r\033[0K + line = scanner.matched[0...-5] # strips \r\033[0K @out << %{<div class="hidden" data-action="#{action}" data-timestamp="#{timestamp}" data-section="#{section}">#{line}</div>} end @@ -209,10 +209,10 @@ module Gitlab # sequence gets stripped (including stuff like "delete last line") return unless indicator == '[' && terminator == 'm' - close_open_tags() + close_open_tags - if commands.empty?() - reset() + if commands.empty? + reset return end @@ -222,7 +222,7 @@ module Gitlab end def evaluate_command_stack(stack) - return unless command = stack.shift() + return unless command = stack.shift if self.respond_to?("on_#{command}", true) self.__send__("on_#{command}", stack) # rubocop:disable GitlabSecurity/PublicSend @@ -333,8 +333,8 @@ module Gitlab return unless command_stack.length >= 2 return unless command_stack[0] == "5" - command_stack.shift() # ignore the "5" command - color_index = command_stack.shift().to_i + command_stack.shift # ignore the "5" command + color_index = command_stack.shift.to_i return unless color_index >= 0 return unless color_index <= 255 diff --git a/lib/gitlab/ci/config/entry/job.rb b/lib/gitlab/ci/config/entry/job.rb index 1d8904f7b29..290c9591b98 100644 --- a/lib/gitlab/ci/config/entry/job.rb +++ b/lib/gitlab/ci/config/entry/job.rb @@ -67,7 +67,7 @@ module Gitlab entry :only, Entry::Policy, description: 'Refs policy this job will be executed for.', - default: { refs: %w[branches tags] } + default: Entry::Policy::DEFAULT_ONLY entry :except, Entry::Policy, description: 'Refs policy this job will be executed for.' diff --git a/lib/gitlab/ci/config/entry/jobs.rb b/lib/gitlab/ci/config/entry/jobs.rb index 82b72e40404..9845c4af655 100644 --- a/lib/gitlab/ci/config/entry/jobs.rb +++ b/lib/gitlab/ci/config/entry/jobs.rb @@ -28,11 +28,15 @@ module Gitlab name.to_s.start_with?('.') end + def node_type(name) + hidden?(name) ? Entry::Hidden : Entry::Job + end + # rubocop: disable CodeReuse/ActiveRecord def compose!(deps = nil) super do @config.each do |name, config| - node = hidden?(name) ? Entry::Hidden : Entry::Job + node = node_type(name) factory = ::Gitlab::Config::Entry::Factory.new(node) .value(config || {}) diff --git a/lib/gitlab/ci/config/entry/policy.rb b/lib/gitlab/ci/config/entry/policy.rb index 9c677bf6617..adc3660d950 100644 --- a/lib/gitlab/ci/config/entry/policy.rb +++ b/lib/gitlab/ci/config/entry/policy.rb @@ -11,6 +11,8 @@ module Gitlab strategy :RefsPolicy, if: -> (config) { config.is_a?(Array) } strategy :ComplexPolicy, if: -> (config) { config.is_a?(Hash) } + DEFAULT_ONLY = { refs: %w[branches tags] }.freeze + class RefsPolicy < ::Gitlab::Config::Entry::Node include ::Gitlab::Config::Entry::Validatable 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/pipeline/seed/build.rb b/lib/gitlab/ci/pipeline/seed/build.rb index ef738a93bfe..d8296940a04 100644 --- a/lib/gitlab/ci/pipeline/seed/build.rb +++ b/lib/gitlab/ci/pipeline/seed/build.rb @@ -38,9 +38,17 @@ module Gitlab ) end + def bridge? + @attributes.to_h.dig(:options, :trigger).present? + end + def to_resource strong_memoize(:resource) do - ::Ci::Build.new(attributes) + if bridge? + ::Ci::Bridge.new(attributes) + else + ::Ci::Build.new(attributes) + end end end end diff --git a/lib/gitlab/ci/pipeline/seed/stage.rb b/lib/gitlab/ci/pipeline/seed/stage.rb index 4775ff15581..9c15064756a 100644 --- a/lib/gitlab/ci/pipeline/seed/stage.rb +++ b/lib/gitlab/ci/pipeline/seed/stage.rb @@ -39,7 +39,13 @@ module Gitlab def to_resource strong_memoize(:stage) do ::Ci::Stage.new(attributes).tap do |stage| - seeds.each { |seed| stage.builds << seed.to_resource } + seeds.each do |seed| + if seed.bridge? + stage.bridges << seed.to_resource + else + stage.builds << seed.to_resource + end + end end end end diff --git a/lib/gitlab/ci/status/bridge/common.rb b/lib/gitlab/ci/status/bridge/common.rb index c6cb620f7a0..4746195c618 100644 --- a/lib/gitlab/ci/status/bridge/common.rb +++ b/lib/gitlab/ci/status/bridge/common.rb @@ -18,7 +18,6 @@ module Gitlab end def details_path - raise NotImplementedError end end end diff --git a/lib/gitlab/ci/status/external/common.rb b/lib/gitlab/ci/status/external/common.rb index 4169f5b3210..cd772819293 100644 --- a/lib/gitlab/ci/status/external/common.rb +++ b/lib/gitlab/ci/status/external/common.rb @@ -6,7 +6,7 @@ module Gitlab module External module Common def label - subject.description + subject.description.presence || super end def has_details? diff --git a/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml b/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml index de4288fb532..75a5bf142d2 100644 --- a/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml @@ -50,7 +50,7 @@ variables: POSTGRES_DB: $CI_ENVIRONMENT_SLUG KUBERNETES_VERSION: 1.11.6 - HELM_VERSION: 2.11.0 + HELM_VERSION: 2.12.2 DOCKER_DRIVER: overlay2 @@ -116,7 +116,7 @@ code_quality: license_management: stage: test - image: + image: name: "registry.gitlab.com/gitlab-org/security-products/license-management:$CI_SERVER_VERSION_MAJOR-$CI_SERVER_VERSION_MINOR-stable" entrypoint: [""] allow_failure: true @@ -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,10 @@ 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.commonName="le.$AUTO_DEVOPS_DOMAIN" \ --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 +724,10 @@ 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.commonName="le.$AUTO_DEVOPS_DOMAIN" \ --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/ci/trace/stream.rb b/lib/gitlab/ci/trace/stream.rb index 0f23b95ba15..e61fb50a303 100644 --- a/lib/gitlab/ci/trace/stream.rb +++ b/lib/gitlab/ci/trace/stream.rb @@ -46,7 +46,7 @@ module Gitlab stream.seek(offset, IO::SEEK_SET) stream.write(data) stream.truncate(offset + data.bytesize) - stream.flush() + stream.flush end def set(data) diff --git a/lib/gitlab/ci/yaml_processor.rb b/lib/gitlab/ci/yaml_processor.rb index 0c48a6ab3ac..07ba6f83d47 100644 --- a/lib/gitlab/ci/yaml_processor.rb +++ b/lib/gitlab/ci/yaml_processor.rb @@ -33,7 +33,7 @@ module Gitlab { stage_idx: @stages.index(job[:stage]), stage: job[:stage], - tag_list: job[:tags] || [], + tag_list: job[:tags], name: job[:name].to_s, allow_failure: job[:ignore], when: job[:when] || 'on_success', @@ -53,8 +53,9 @@ module Gitlab retry: job[:retry], parallel: job[:parallel], instance: job[:instance], - start_in: job[:start_in] - }.compact } + start_in: job[:start_in], + trigger: job[:trigger] + }.compact }.compact end def stage_builds_attributes(stage) diff --git a/lib/gitlab/data_builder/push.rb b/lib/gitlab/data_builder/push.rb index 862127110b9..ea08b5f7eae 100644 --- a/lib/gitlab/data_builder/push.rb +++ b/lib/gitlab/data_builder/push.rb @@ -93,7 +93,7 @@ module Gitlab user_id: user.id, user_name: user.name, user_username: user.username, - user_email: user.email, + user_email: user.public_email, user_avatar: user.avatar_url(only_path: false), project_id: project.id, project: project.hook_attrs, diff --git a/lib/gitlab/email/handler/reply_processing.rb b/lib/gitlab/email/handler/reply_processing.rb index ba9730d2685..d8f4be8ada1 100644 --- a/lib/gitlab/email/handler/reply_processing.rb +++ b/lib/gitlab/email/handler/reply_processing.rb @@ -56,7 +56,7 @@ module Gitlab raise ProjectNotFound unless author.can?(:read_project, project) end - raise UserNotAuthorizedError unless author.can?(permission, project || noteable) + raise UserNotAuthorizedError unless author.can?(permission, try(:noteable) || project) end def verify_record!(record:, invalid_exception:, record_name:) diff --git a/lib/gitlab/error_tracking/project.rb b/lib/gitlab/error_tracking/project.rb new file mode 100644 index 00000000000..93e81da5034 --- /dev/null +++ b/lib/gitlab/error_tracking/project.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module Gitlab + module ErrorTracking + class Project + include ActiveModel::Model + + ACCESSORS = [ + :id, :name, :status, :slug, :organization_name, + :organization_id, :organization_slug + ].freeze + + attr_accessor(*ACCESSORS) + end + end +end diff --git a/lib/gitlab/gfm/reference_rewriter.rb b/lib/gitlab/gfm/reference_rewriter.rb index 08d7db49ad7..4d82acd9d87 100644 --- a/lib/gitlab/gfm/reference_rewriter.rb +++ b/lib/gitlab/gfm/reference_rewriter.rb @@ -93,7 +93,7 @@ module Gitlab end def markdown(text) - Banzai.render(text, project: @source_parent, no_original_data: true) + Banzai.render(text, project: @source_parent, no_original_data: true, no_sourcepos: true) 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..0ab53f8f706 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 @@ -126,7 +133,11 @@ module Gitlab end def self.address_metadata(storage) - Base64.strict_encode64(JSON.dump({ storage => { 'address' => address(storage), 'token' => token(storage) } })) + Base64.strict_encode64(JSON.dump(storage => connection_data(storage))) + end + + def self.connection_data(storage) + { 'address' => address(storage), 'token' => token(storage) } end # All Gitaly RPC call sites should use GitalyClient.call. This method diff --git a/lib/gitlab/github_import/bulk_importing.rb b/lib/gitlab/github_import/bulk_importing.rb index da2f96b5c4b..147597289cf 100644 --- a/lib/gitlab/github_import/bulk_importing.rb +++ b/lib/gitlab/github_import/bulk_importing.rb @@ -15,12 +15,10 @@ module Gitlab end # Bulk inserts the given rows into the database. - def bulk_insert(model, rows, batch_size: 100, pre_hook: nil) + def bulk_insert(model, rows, batch_size: 100) rows.each_slice(batch_size) do |slice| - pre_hook.call(slice) if pre_hook Gitlab::Database.bulk_insert(model.table_name, slice) end - rows end end end diff --git a/lib/gitlab/github_import/importer/issue_importer.rb b/lib/gitlab/github_import/importer/issue_importer.rb index 4226eee85cc..656d46b6a7d 100644 --- a/lib/gitlab/github_import/importer/issue_importer.rb +++ b/lib/gitlab/github_import/importer/issue_importer.rb @@ -57,11 +57,7 @@ module Gitlab updated_at: issue.updated_at } - insert_and_return_id(attributes, project.issues).tap do |id| - # We use .insert_and_return_id which effectively disables all callbacks. - # Trigger iid logic here to make sure we track internal id values consistently. - project.issues.find(id).ensure_project_iid! - end + insert_and_return_id(attributes, project.issues) rescue ActiveRecord::InvalidForeignKey # It's possible the project has been deleted since scheduling this # job. In this case we'll just skip creating the issue. diff --git a/lib/gitlab/github_import/importer/lfs_object_importer.rb b/lib/gitlab/github_import/importer/lfs_object_importer.rb index a88c17aaf82..195383fd3e9 100644 --- a/lib/gitlab/github_import/importer/lfs_object_importer.rb +++ b/lib/gitlab/github_import/importer/lfs_object_importer.rb @@ -13,10 +13,12 @@ module Gitlab @project = project end + def lfs_download_object + LfsDownloadObject.new(oid: lfs_object.oid, size: lfs_object.size, link: lfs_object.link) + end + def execute - Projects::LfsPointers::LfsDownloadService - .new(project) - .execute(lfs_object.oid, lfs_object.download_link) + Projects::LfsPointers::LfsDownloadService.new(project, lfs_download_object).execute end end end diff --git a/lib/gitlab/github_import/importer/milestones_importer.rb b/lib/gitlab/github_import/importer/milestones_importer.rb index 8d54b27374c..87cf2c8b598 100644 --- a/lib/gitlab/github_import/importer/milestones_importer.rb +++ b/lib/gitlab/github_import/importer/milestones_importer.rb @@ -19,20 +19,10 @@ module Gitlab # rubocop: enable CodeReuse/ActiveRecord def execute - # We insert records in bulk, by-passing any standard model callbacks. - # The pre_hook here makes sure we track internal ids consistently. - # Note this has to be called before performing an insert of a batch - # because we're outside a transaction scope here. - bulk_insert(Milestone, build_milestones, pre_hook: method(:track_greatest_iid)) + bulk_insert(Milestone, build_milestones) build_milestones_cache end - def track_greatest_iid(slice) - greatest_iid = slice.max { |e| e[:iid] }[:iid] - - InternalId.track_greatest(nil, { project: project }, :milestones, greatest_iid, ->(_) { project.milestones.maximum(:iid) }) - end - def build_milestones build_database_rows(each_milestone) end diff --git a/lib/gitlab/github_import/representation/lfs_object.rb b/lib/gitlab/github_import/representation/lfs_object.rb index debe0fa0baf..a4606173f49 100644 --- a/lib/gitlab/github_import/representation/lfs_object.rb +++ b/lib/gitlab/github_import/representation/lfs_object.rb @@ -9,11 +9,11 @@ module Gitlab attr_reader :attributes - expose_attribute :oid, :download_link + expose_attribute :oid, :link, :size # Builds a lfs_object def self.from_api_response(lfs_object) - new({ oid: lfs_object[0], download_link: lfs_object[1] }) + new({ oid: lfs_object.oid, link: lfs_object.link, size: lfs_object.size }) end # Builds a new lfs_object using a Hash that was built from a JSON payload. diff --git a/lib/gitlab/hashed_storage/migrator.rb b/lib/gitlab/hashed_storage/migrator.rb index 1f29cf10cad..bf463077dcc 100644 --- a/lib/gitlab/hashed_storage/migrator.rb +++ b/lib/gitlab/hashed_storage/migrator.rb @@ -11,21 +11,21 @@ module Gitlab # Schedule a range of projects to be bulk migrated with #bulk_migrate asynchronously # - # @param [Object] start first project id for the range - # @param [Object] finish last project id for the range - def bulk_schedule(start, finish) - StorageMigratorWorker.perform_async(start, finish) + # @param [Integer] start first project id for the range + # @param [Integer] finish last project id for the range + def bulk_schedule(start:, finish:) + ::HashedStorage::MigratorWorker.perform_async(start, finish) end # Start migration of projects from specified range # - # Flagging a project to be migrated is a synchronous action, + # Flagging a project to be migrated is a synchronous action # but the migration runs through async jobs # - # @param [Object] start first project id for the range - # @param [Object] finish last project id for the range + # @param [Integer] start first project id for the range + # @param [Integer] finish last project id for the range # rubocop: disable CodeReuse/ActiveRecord - def bulk_migrate(start, finish) + def bulk_migrate(start:, finish:) projects = build_relation(start, finish) projects.with_route.find_each(batch_size: BATCH_SIZE) do |project| @@ -34,9 +34,9 @@ module Gitlab end # rubocop: enable CodeReuse/ActiveRecord - # Flag a project to be migrated + # Flag a project to be migrated to Hashed Storage # - # @param [Object] project that will be migrated + # @param [Project] project that will be migrated def migrate(project) Rails.logger.info "Starting storage migration of #{project.full_path} (ID=#{project.id})..." @@ -45,6 +45,10 @@ module Gitlab Rails.logger.error("#{err.message} migrating storage of #{project.full_path} (ID=#{project.id}), trace - #{err.backtrace}") end + def rollback(project) + # TODO: implement rollback strategy + end + private # rubocop: disable CodeReuse/ActiveRecord diff --git a/lib/gitlab/import/merge_request_helpers.rb b/lib/gitlab/import/merge_request_helpers.rb index 9215067d973..fa3ff6c3f12 100644 --- a/lib/gitlab/import/merge_request_helpers.rb +++ b/lib/gitlab/import/merge_request_helpers.rb @@ -24,10 +24,6 @@ module Gitlab merge_request = project.merge_requests.reload.find(merge_request_id) - # We use .insert_and_return_id which effectively disables all callbacks. - # Trigger iid logic here to make sure we track internal id values consistently. - merge_request.ensure_target_project_iid! - [merge_request, false] end rescue ActiveRecord::InvalidForeignKey diff --git a/lib/gitlab/import_export/project_tree_restorer.rb b/lib/gitlab/import_export/project_tree_restorer.rb index a56ec65b9f1..51001750a6c 100644 --- a/lib/gitlab/import_export/project_tree_restorer.rb +++ b/lib/gitlab/import_export/project_tree_restorer.rb @@ -107,7 +107,7 @@ module Gitlab def project_params @project_params ||= begin - attrs = json_params.merge(override_params) + attrs = json_params.merge(override_params).merge(visibility_level) # Cleaning all imported and overridden params Gitlab::ImportExport::AttributeCleaner.clean(relation_hash: attrs, @@ -127,6 +127,13 @@ module Gitlab end end + def visibility_level + level = override_params['visibility_level'] || json_params['visibility_level'] || @project.visibility_level + level = @project.group.visibility_level if @project.group && level > @project.group.visibility_level + + { 'visibility_level' => level } + end + # Given a relation hash containing one or more models and its relationships, # loops through each model and each object from a model type and # and assigns its correspondent attributes hash from +tree_hash+ diff --git a/lib/gitlab/import_export/shared.rb b/lib/gitlab/import_export/shared.rb index c13e6c1d83b..947caaaefee 100644 --- a/lib/gitlab/import_export/shared.rb +++ b/lib/gitlab/import_export/shared.rb @@ -8,6 +8,7 @@ module Gitlab def initialize(project) @project = project @errors = [] + @logger = Gitlab::Import::Logger.build end def active_export_count @@ -23,19 +24,16 @@ module Gitlab end def error(error) - error_out(error.message, caller[0].dup) - add_error_message(error.message) + log_error(message: error.message, caller: caller[0].dup) + log_debug(backtrace: error.backtrace&.join("\n")) + + Gitlab::Sentry.track_acceptable_exception(error, extra: log_base_data) - # Debug: - if error.backtrace - Rails.logger.error("Import/Export backtrace: #{error.backtrace.join("\n")}") - else - Rails.logger.error("No backtrace found") - end + add_error_message(error.message) end - def add_error_message(error_message) - @errors << error_message + def add_error_message(message) + @errors << filtered_error_message(message) end def after_export_in_progress? @@ -52,8 +50,25 @@ module Gitlab @project.disk_path end - def error_out(message, caller) - Rails.logger.error("Import/Export error raised on #{caller}: #{message}") + def log_error(details) + @logger.error(log_base_data.merge(details)) + end + + def log_debug(details) + @logger.debug(log_base_data.merge(details)) + end + + def log_base_data + { + importer: 'Import/Export', + import_jid: @project&.import_state&.import_jid, + project_id: @project&.id, + project_path: @project&.full_path + } + end + + def filtered_error_message(message) + Projects::ImportErrorFilter.filter_message(message) end def after_export_lock_file diff --git a/lib/gitlab/kubernetes/helm.rb b/lib/gitlab/kubernetes/helm.rb index 03d38ec78fd..bbac15c7710 100644 --- a/lib/gitlab/kubernetes/helm.rb +++ b/lib/gitlab/kubernetes/helm.rb @@ -3,7 +3,7 @@ module Gitlab module Kubernetes module Helm - HELM_VERSION = '2.11.0'.freeze + HELM_VERSION = '2.12.2'.freeze KUBECTL_VERSION = '1.11.0'.freeze NAMESPACE = 'gitlab-managed-apps'.freeze SERVICE_ACCOUNT = 'tiller'.freeze diff --git a/lib/gitlab/kubernetes/kube_client.rb b/lib/gitlab/kubernetes/kube_client.rb index fe839940f74..624c2c67551 100644 --- a/lib/gitlab/kubernetes/kube_client.rb +++ b/lib/gitlab/kubernetes/kube_client.rb @@ -76,9 +76,12 @@ module Gitlab attr_reader :api_prefix, :kubeclient_options + # We disable redirects through 'http_max_redirects: 0', + # so that KubeClient does not follow redirects and + # expose internal services. def initialize(api_prefix, **kubeclient_options) @api_prefix = api_prefix - @kubeclient_options = kubeclient_options + @kubeclient_options = kubeclient_options.merge(http_max_redirects: 0) end def create_or_update_cluster_role_binding(resource) diff --git a/lib/gitlab/lfs_token.rb b/lib/gitlab/lfs_token.rb index c09d3ebc7be..26b81847d37 100644 --- a/lib/gitlab/lfs_token.rb +++ b/lib/gitlab/lfs_token.rb @@ -47,7 +47,7 @@ module Gitlab user? ? :lfs_token : :lfs_deploy_token end - private # rubocop:disable Lint/UselessAccessModifier + private # rubocop:disable Lint/UselessAccessModifier class HMACToken include LfsTokenHelper @@ -100,7 +100,7 @@ module Gitlab # class LegacyRedisDeviseToken TOKEN_LENGTH = 50 - DEFAULT_EXPIRY_TIME = 1800 * 1000 # 30 mins + DEFAULT_EXPIRY_TIME = 1800 * 1000 # 30 mins def initialize(actor) @actor = actor diff --git a/lib/gitlab/loop_helpers.rb b/lib/gitlab/loop_helpers.rb new file mode 100644 index 00000000000..3873156a3b0 --- /dev/null +++ b/lib/gitlab/loop_helpers.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module Gitlab + module LoopHelpers + ## + # This helper method repeats the same task until it's expired. + # + # Note: ExpiredLoopError does not happen until the given block finished. + # Please do not use this method for heavy or asynchronous operations. + def loop_until(timeout: nil, limit: 1_000_000) + raise ArgumentError unless limit + + start = Time.now + + limit.times do + return true unless yield + + return false if timeout && (Time.now - start) > timeout + end + + false + end + end +end diff --git a/lib/gitlab/metrics/influx_db.rb b/lib/gitlab/metrics/influx_db.rb index 1359e973590..0b04340fbb5 100644 --- a/lib/gitlab/metrics/influx_db.rb +++ b/lib/gitlab/metrics/influx_db.rb @@ -147,9 +147,7 @@ module Gitlab # # See `Gitlab::Metrics::Transaction#add_event` for more details. def add_event(*args) - trans = current_transaction - - trans&.add_event(*args) + current_transaction&.add_event(*args) end # Returns the prefix to use for the name of a series. diff --git a/lib/gitlab/middleware/go.rb b/lib/gitlab/middleware/go.rb index 72a788022ef..f9efef38825 100644 --- a/lib/gitlab/middleware/go.rb +++ b/lib/gitlab/middleware/go.rb @@ -111,7 +111,7 @@ module Gitlab def project_for_paths(paths, request) project = Project.where_full_path_in(paths).first - return unless Ability.allowed?(current_user(request, project), :read_project, project) + return unless Ability.allowed?(current_user(request, project), :read_project, project) project 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/pages_client.rb b/lib/gitlab/pages_client.rb index 3626e53f84c..d74fdba2241 100644 --- a/lib/gitlab/pages_client.rb +++ b/lib/gitlab/pages_client.rb @@ -103,7 +103,7 @@ module Gitlab end def write_token(new_token) - Tempfile.open(File.basename(token_path), File.dirname(token_path), encoding: 'ascii-8bit') do |f| + Tempfile.open(File.basename(token_path), File.dirname(token_path), encoding: 'ascii-8bit') do |f| f.write(new_token) f.close File.link(f.path, token_path) diff --git a/lib/gitlab/path_regex.rb b/lib/gitlab/path_regex.rb index fa68dead80b..3c888be0710 100644 --- a/lib/gitlab/path_regex.rb +++ b/lib/gitlab/path_regex.rb @@ -125,7 +125,8 @@ module Gitlab # allow non-regex validations, etc), `NAMESPACE_FORMAT_REGEX_JS` serves as a Javascript-compatible version of # `NAMESPACE_FORMAT_REGEX`, with the negative lookbehind assertion removed. This means that the client-side validation # will pass for usernames ending in `.atom` and `.git`, but will be caught by the server-side validation. - PATH_REGEX_STR = '[a-zA-Z0-9_\.][a-zA-Z0-9_\-\.]*'.freeze + PATH_START_CHAR = '[a-zA-Z0-9_\.]'.freeze + PATH_REGEX_STR = PATH_START_CHAR + '[a-zA-Z0-9_\-\.]*'.freeze NAMESPACE_FORMAT_REGEX_JS = PATH_REGEX_STR + '[a-zA-Z0-9_\-]|[a-zA-Z0-9_]'.freeze NO_SUFFIX_REGEX = /(?<!\.git|\.atom)/.freeze diff --git a/lib/gitlab/release_blog_post.rb b/lib/gitlab/release_blog_post.rb deleted file mode 100644 index 639aee61464..00000000000 --- a/lib/gitlab/release_blog_post.rb +++ /dev/null @@ -1,40 +0,0 @@ -# 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/sql/union.rb b/lib/gitlab/sql/union.rb index d24d5116167..f05592fc3a3 100644 --- a/lib/gitlab/sql/union.rb +++ b/lib/gitlab/sql/union.rb @@ -9,7 +9,7 @@ module Gitlab # # Example usage: # - # union = Gitlab::SQL::Union.new(user.personal_projects, user.projects) + # union = Gitlab::SQL::Union.new([user.personal_projects, user.projects]) # sql = union.to_sql # # Project.where("id IN (#{sql})") 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..3a08ede8138 --- /dev/null +++ b/lib/gitlab/tracing/common.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true + +require 'opentracing' + +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 postnotify_span(operation_name, start_time, end_time, tags: nil, child_of: nil, exception: nil) + span = OpenTracing.start_span(operation_name, start_time: start_time, tags: tags, child_of: child_of) + + log_exception_on_span(span, exception) if exception + + span.finish(end_time: end_time) + 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/rails/action_view_subscriber.rb b/lib/gitlab/tracing/rails/action_view_subscriber.rb new file mode 100644 index 00000000000..88816e1fb32 --- /dev/null +++ b/lib/gitlab/tracing/rails/action_view_subscriber.rb @@ -0,0 +1,75 @@ +# frozen_string_literal: true + +module Gitlab + module Tracing + module Rails + class ActionViewSubscriber + include RailsCommon + + COMPONENT_TAG = 'ActionView' + RENDER_TEMPLATE_NOTIFICATION_TOPIC = 'render_template.action_view' + RENDER_COLLECTION_NOTIFICATION_TOPIC = 'render_collection.action_view' + RENDER_PARTIAL_NOTIFICATION_TOPIC = 'render_partial.action_view' + + # Instruments Rails ActionView events for opentracing. + # Returns a lambda, which, when called will unsubscribe from the notifications + def self.instrument + subscriber = new + + subscriptions = [ + ActiveSupport::Notifications.subscribe(RENDER_TEMPLATE_NOTIFICATION_TOPIC) do |_, start, finish, _, payload| + subscriber.notify_render_template(start, finish, payload) + end, + ActiveSupport::Notifications.subscribe(RENDER_COLLECTION_NOTIFICATION_TOPIC) do |_, start, finish, _, payload| + subscriber.notify_render_collection(start, finish, payload) + end, + ActiveSupport::Notifications.subscribe(RENDER_PARTIAL_NOTIFICATION_TOPIC) do |_, start, finish, _, payload| + subscriber.notify_render_partial(start, finish, payload) + end + ] + + create_unsubscriber subscriptions + end + + # For more information on the payloads: https://guides.rubyonrails.org/active_support_instrumentation.html + def notify_render_template(start, finish, payload) + generate_span_for_notification("render_template", start, finish, payload, tags_for_render_template(payload)) + end + + def notify_render_collection(start, finish, payload) + generate_span_for_notification("render_collection", start, finish, payload, tags_for_render_collection(payload)) + end + + def notify_render_partial(start, finish, payload) + generate_span_for_notification("render_partial", start, finish, payload, tags_for_render_partial(payload)) + end + + private + + def tags_for_render_template(payload) + { + 'component' => COMPONENT_TAG, + 'template.id' => payload[:identifier], + 'template.layout' => payload[:layout] + } + end + + def tags_for_render_collection(payload) + { + 'component' => COMPONENT_TAG, + 'template.id' => payload[:identifier], + 'template.count' => payload[:count] || 0, + 'template.cache.hits' => payload[:cache_hits] || 0 + } + end + + def tags_for_render_partial(payload) + { + 'component' => COMPONENT_TAG, + 'template.id' => payload[:identifier] + } + end + end + end + end +end diff --git a/lib/gitlab/tracing/rails/active_record_subscriber.rb b/lib/gitlab/tracing/rails/active_record_subscriber.rb new file mode 100644 index 00000000000..32f5658e57e --- /dev/null +++ b/lib/gitlab/tracing/rails/active_record_subscriber.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +module Gitlab + module Tracing + module Rails + class ActiveRecordSubscriber + include RailsCommon + + ACTIVE_RECORD_NOTIFICATION_TOPIC = 'sql.active_record' + OPERATION_NAME_PREFIX = 'active_record:' + DEFAULT_OPERATION_NAME = 'sqlquery' + + # Instruments Rails ActiveRecord events for opentracing. + # Returns a lambda, which, when called will unsubscribe from the notifications + def self.instrument + subscriber = new + + subscription = ActiveSupport::Notifications.subscribe(ACTIVE_RECORD_NOTIFICATION_TOPIC) do |_, start, finish, _, payload| + subscriber.notify(start, finish, payload) + end + + create_unsubscriber [subscription] + end + + # For more information on the payloads: https://guides.rubyonrails.org/active_support_instrumentation.html + def notify(start, finish, payload) + generate_span_for_notification(notification_name(payload), start, finish, payload, tags_for_notification(payload)) + end + + private + + def notification_name(payload) + OPERATION_NAME_PREFIX + (payload[:name].presence || DEFAULT_OPERATION_NAME) + end + + def tags_for_notification(payload) + { + 'component' => 'ActiveRecord', + 'span.kind' => 'client', + 'db.type' => 'sql', + 'db.connection_id' => payload[:connection_id], + 'db.cached' => payload[:cached] || false, + 'db.statement' => payload[:sql] + } + end + end + end + end +end diff --git a/lib/gitlab/tracing/rails/rails_common.rb b/lib/gitlab/tracing/rails/rails_common.rb new file mode 100644 index 00000000000..88e914f62f8 --- /dev/null +++ b/lib/gitlab/tracing/rails/rails_common.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module Gitlab + module Tracing + module Rails + module RailsCommon + extend ActiveSupport::Concern + include Gitlab::Tracing::Common + + class_methods do + def create_unsubscriber(subscriptions) + -> { subscriptions.each { |subscriber| ActiveSupport::Notifications.unsubscribe(subscriber) } } + end + end + + def generate_span_for_notification(operation_name, start, finish, payload, tags) + exception = payload[:exception] + + postnotify_span(operation_name, start, finish, tags: tags, exception: exception) + end + 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/usage_data.rb b/lib/gitlab/usage_data.rb index 083c620267a..6bfcf83f388 100644 --- a/lib/gitlab/usage_data.rb +++ b/lib/gitlab/usage_data.rb @@ -81,6 +81,7 @@ module Gitlab pages_domains: count(PagesDomain), projects: count(Project), projects_imported_from_github: count(Project.where(import_type: 'github')), + projects_with_repositories_enabled: count(ProjectFeature.where('repository_access_level > ?', ProjectFeature::DISABLED)), protected_branches: count(ProtectedBranch), releases: count(Release), remote_mirrors: count(RemoteMirror), diff --git a/lib/gitlab/version_info.rb b/lib/gitlab/version_info.rb index 142ead12c08..aa6d5310161 100644 --- a/lib/gitlab/version_info.rb +++ b/lib/gitlab/version_info.rb @@ -20,14 +20,6 @@ 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/safe_zip/entry.rb b/lib/safe_zip/entry.rb new file mode 100644 index 00000000000..664e2f52f91 --- /dev/null +++ b/lib/safe_zip/entry.rb @@ -0,0 +1,97 @@ +# frozen_string_literal: true + +module SafeZip + class Entry + attr_reader :zip_archive, :zip_entry + attr_reader :path, :params + + def initialize(zip_archive, zip_entry, params) + @zip_archive = zip_archive + @zip_entry = zip_entry + @params = params + @path = ::File.expand_path(zip_entry.name, params.extract_path) + end + + def path_dir + ::File.dirname(path) + end + + def real_path_dir + ::File.realpath(path_dir) + end + + def exist? + ::File.exist?(path) + end + + def extract + # do not extract if file is not part of target directory + return false unless matching_target_directory + + # do not overwrite existing file + raise SafeZip::Extract::AlreadyExistsError, "File already exists #{zip_entry.name}" if exist? + + create_path_dir + + if zip_entry.file? + extract_file + elsif zip_entry.directory? + extract_dir + elsif zip_entry.symlink? + extract_symlink + else + raise SafeZip::Extract::UnsupportedEntryError, "File #{zip_entry.name} cannot be extracted" + end + rescue SafeZip::Extract::Error + raise + rescue => e + raise SafeZip::Extract::ExtractError, e.message + end + + private + + def extract_file + zip_archive.extract(zip_entry, path) + end + + def extract_dir + FileUtils.mkdir(path) + end + + def extract_symlink + source_path = read_symlink + real_source_path = expand_symlink(source_path) + + # ensure that source path of symlink is within target directories + unless real_source_path.start_with?(matching_target_directory) + raise SafeZip::Extract::PermissionDeniedError, "Symlink cannot be created targeting: #{source_path}" + end + + ::File.symlink(source_path, path) + end + + def create_path_dir + # Create all directories, but ignore permissions + FileUtils.mkdir_p(path_dir) + + # disallow to make path dirs to point to another directories + unless path_dir == real_path_dir + raise SafeZip::Extract::PermissionDeniedError, "Directory of #{zip_entry.name} points to another directory" + end + end + + def matching_target_directory + params.matching_target_directory(path) + end + + def read_symlink + zip_archive.read(zip_entry) + end + + def expand_symlink(source_path) + ::File.realpath(source_path, path_dir) + rescue + raise SafeZip::Extract::SymlinkSourceDoesNotExistError, "Symlink source #{source_path} does not exist" + end + end +end diff --git a/lib/safe_zip/extract.rb b/lib/safe_zip/extract.rb new file mode 100644 index 00000000000..679c021c730 --- /dev/null +++ b/lib/safe_zip/extract.rb @@ -0,0 +1,73 @@ +# frozen_string_literal: true + +module SafeZip + class Extract + Error = Class.new(StandardError) + PermissionDeniedError = Class.new(Error) + SymlinkSourceDoesNotExistError = Class.new(Error) + UnsupportedEntryError = Class.new(Error) + AlreadyExistsError = Class.new(Error) + NoMatchingError = Class.new(Error) + ExtractError = Class.new(Error) + + attr_reader :archive_path + + def initialize(archive_file) + @archive_path = archive_file + end + + def extract(opts = {}) + params = SafeZip::ExtractParams.new(**opts) + + if Feature.enabled?(:safezip_use_rubyzip, default_enabled: true) + extract_with_ruby_zip(params) + else + legacy_unsafe_extract_with_system_zip(params) + end + end + + private + + def extract_with_ruby_zip(params) + ::Zip::File.open(archive_path) do |zip_archive| + # Extract all files in the following order: + # 1. Directories first, + # 2. Files next, + # 3. Symlinks last (or anything else) + extracted = extract_all_entries(zip_archive, params, + zip_archive.lazy.select(&:directory?)) + + extracted += extract_all_entries(zip_archive, params, + zip_archive.lazy.select(&:file?)) + + extracted += extract_all_entries(zip_archive, params, + zip_archive.lazy.reject(&:directory?).reject(&:file?)) + + raise NoMatchingError, 'No entries extracted' unless extracted > 0 + end + end + + def extract_all_entries(zip_archive, params, entries) + entries.count do |zip_entry| + SafeZip::Entry.new(zip_archive, zip_entry, params) + .extract + end + end + + def legacy_unsafe_extract_with_system_zip(params) + # Requires UnZip at least 6.00 Info-ZIP. + # -n never overwrite existing files + args = %W(unzip -n -qq #{archive_path}) + + # We add * to end of directory, because we want to extract directory and all subdirectories + args += params.directories_wildcard + + # Target directory where we extract + args += %W(-d #{params.extract_path}) + + unless system(*args) + raise Error, 'archive failed to extract' + end + end + end +end diff --git a/lib/safe_zip/extract_params.rb b/lib/safe_zip/extract_params.rb new file mode 100644 index 00000000000..bd3b788bac9 --- /dev/null +++ b/lib/safe_zip/extract_params.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +module SafeZip + class ExtractParams + include Gitlab::Utils::StrongMemoize + + attr_reader :directories, :extract_path + + def initialize(directories:, to:) + @directories = directories + @extract_path = ::File.realpath(to) + end + + def matching_target_directory(path) + target_directories.find do |directory| + path.start_with?(directory) + end + end + + def target_directories + strong_memoize(:target_directories) do + directories.map do |directory| + ::File.join(::File.expand_path(directory, extract_path), '') + end + end + end + + def directories_wildcard + strong_memoize(:directories_wildcard) do + directories.map do |directory| + ::File.join(directory, '*') + end + end + end + end +end diff --git a/lib/sentry/client.rb b/lib/sentry/client.rb index 343f2c49a7f..4187014d49e 100644 --- a/lib/sentry/client.rb +++ b/lib/sentry/client.rb @@ -3,6 +3,7 @@ module Sentry class Client Error = Class.new(StandardError) + SentryError = Class.new(StandardError) attr_accessor :url, :token @@ -16,6 +17,13 @@ module Sentry map_to_errors(issues) end + def list_projects + projects = get_projects + map_to_projects(projects) + rescue KeyError => e + raise Client::SentryError, "Sentry API response is missing keys. #{e.message}" + end + private def request_params @@ -27,18 +35,23 @@ module Sentry } end - def get_issues(issue_status:, limit:) - resp = Gitlab::HTTP.get( - issues_api_url, - **request_params.merge(query: { - query: "is:#{issue_status}", - limit: limit - }) - ) + def http_get(url, params = {}) + resp = Gitlab::HTTP.get(url, **request_params.merge(params)) handle_response(resp) end + def get_issues(issue_status:, limit:) + http_get(issues_api_url, query: { + query: "is:#{issue_status}", + limit: limit + }) + end + + def get_projects + http_get(projects_api_url) + end + def handle_response(response) unless response.code == 200 raise Client::Error, "Sentry response error: #{response.code}" @@ -47,6 +60,13 @@ module Sentry response.as_json end + def projects_api_url + projects_url = URI(@url) + projects_url.path = '/api/0/projects/' + + projects_url + end + def issues_api_url issues_url = URI(@url + '/issues/') issues_url.path.squeeze!('/') @@ -55,9 +75,11 @@ module Sentry end def map_to_errors(issues) - issues.map do |issue| - map_to_error(issue) - end + issues.map(&method(:map_to_error)) + end + + def map_to_projects(projects) + projects.map(&method(:map_to_project)) end def issue_url(id) @@ -100,5 +122,19 @@ module Sentry project_slug: project.fetch('slug', nil) ) end + + def map_to_project(project) + organization = project.fetch('organization') + + Gitlab::ErrorTracking::Project.new( + id: project.fetch('id'), + name: project.fetch('name'), + slug: project.fetch('slug'), + status: project.dig('status'), + organization_name: organization.fetch('name'), + organization_id: organization.fetch('id'), + organization_slug: organization.fetch('slug') + ) + end end end 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/lib/tasks/gitlab/bulk_add_permission.rake b/lib/tasks/gitlab/bulk_add_permission.rake index 26cbf0740b6..c0d6cc8ca8e 100644 --- a/lib/tasks/gitlab/bulk_add_permission.rake +++ b/lib/tasks/gitlab/bulk_add_permission.rake @@ -14,7 +14,7 @@ namespace :gitlab do end desc "GitLab | Add a specific user to all projects (as a developer)" - task :user_to_projects, [:email] => :environment do |t, args| + task :user_to_projects, [:email] => :environment do |t, args| user = User.find_by(email: args.email) project_ids = Project.pluck(:id) puts "Importing #{user.email} users into #{project_ids.size} projects" @@ -22,7 +22,7 @@ namespace :gitlab do end desc "GitLab | Add all users to all groups (admin users are added as owners)" - task all_users_to_all_groups: :environment do |t, args| + task all_users_to_all_groups: :environment do |t, args| user_ids = User.where(admin: false).pluck(:id) admin_ids = User.where(admin: true).pluck(:id) groups = Group.all @@ -36,7 +36,7 @@ namespace :gitlab do end desc "GitLab | Add a specific user to all groups (as a developer)" - task :user_to_groups, [:email] => :environment do |t, args| + task :user_to_groups, [:email] => :environment do |t, args| user = User.find_by_email args.email groups = Group.all puts "Importing #{user.email} users into #{groups.size} groups" diff --git a/lib/tasks/gitlab/storage.rake b/lib/tasks/gitlab/storage.rake index 09dc3aa9882..f9ce3e1d338 100644 --- a/lib/tasks/gitlab/storage.rake +++ b/lib/tasks/gitlab/storage.rake @@ -37,7 +37,7 @@ namespace :gitlab do print "Enqueuing migration of #{legacy_projects_count} projects in batches of #{helper.batch_size}" helper.project_id_batches do |start, finish| - storage_migrator.bulk_schedule(start, finish) + storage_migrator.bulk_schedule(start: start, finish: finish) print '.' end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 329d258cd09..8caa876e6b0 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -291,7 +291,7 @@ msgstr "" msgid "<strong>%{group_name}</strong> group members" msgstr "" -msgid "<strong>Removes</strong> source branch" +msgid "<strong>Deletes</strong> source branch" msgstr "" msgid "A 'Runner' is a process which runs a job. You can set up as many Runners as you need." @@ -504,6 +504,9 @@ msgstr "" msgid "All features are enabled for blank projects, from templates, or when importing, but you can disable them afterward in the project settings." msgstr "" +msgid "All issues for this milestone are closed. You may close this milestone now." +msgstr "" + msgid "All users" msgstr "" @@ -528,6 +531,9 @@ msgstr "" msgid "Allow users to request access if visibility is public or internal." msgstr "" +msgid "Allowed to fail" +msgstr "" + msgid "Allows you to add and manage Kubernetes clusters." msgstr "" @@ -714,6 +720,9 @@ msgstr "" msgid "Are you sure you want to lose unsaved changes?" msgstr "" +msgid "Are you sure you want to lose your issue information?" +msgstr "" + msgid "Are you sure you want to regenerate the public key? You will have to update the public key on the remote server before mirroring will work again." msgstr "" @@ -735,6 +744,9 @@ msgstr "" msgid "Are you sure you want to stop this environment?" msgstr "" +msgid "Are you sure you want to unsubscribe from the %{type}: %{link_to_noteable_text}?" +msgstr "" + msgid "Are you sure?" msgstr "" @@ -756,6 +768,9 @@ msgstr "" msgid "Assign milestone" msgstr "" +msgid "Assign some issues to this milestone." +msgstr "" + msgid "Assign to" msgstr "" @@ -1176,6 +1191,9 @@ msgstr "" msgid "CI / CD Settings" msgstr "" +msgid "CI Lint" +msgstr "" + msgid "CI/CD" msgstr "" @@ -1245,6 +1263,12 @@ msgstr "" msgid "Cannot modify managed Kubernetes cluster" msgstr "" +msgid "Certificate" +msgstr "" + +msgid "Certificate (PEM)" +msgstr "" + msgid "Change permissions" msgstr "" @@ -1284,6 +1308,9 @@ msgstr "" msgid "Check the %{docs_link_start}documentation%{docs_link_end}." msgstr "" +msgid "Check your .gitlab-ci.yml" +msgstr "" + msgid "Checking %{text} availability…" msgstr "" @@ -1425,6 +1452,9 @@ msgstr "" msgid "CiVariable|Validation failed" msgstr "" +msgid "Clear" +msgstr "" + msgid "Clear search" msgstr "" @@ -1470,9 +1500,18 @@ msgstr "" msgid "Close" msgstr "" +msgid "Close milestone" +msgstr "" + msgid "Closed" msgstr "" +msgid "Closed (moved)" +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 "" @@ -1515,6 +1554,9 @@ msgstr "" msgid "ClusterIntegration|Applications" msgstr "" +msgid "ClusterIntegration|Apply for credit" +msgstr "" + msgid "ClusterIntegration|Are you sure you want to remove this Kubernetes cluster's integration? This will not delete your actual Kubernetes cluster." msgstr "" @@ -1899,6 +1941,9 @@ msgstr "" msgid "ClusterIntegration|sign up" msgstr "" +msgid "Code" +msgstr "" + msgid "Cohorts" msgstr "" @@ -1937,9 +1982,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 "" @@ -2003,6 +2054,9 @@ msgstr "" msgid "Compare Revisions" msgstr "" +msgid "Compare changes" +msgstr "" + msgid "Compare changes with the last commit" msgstr "" @@ -2102,6 +2156,9 @@ msgstr "" msgid "ContainerRegistry|You can also use a %{deploy_token} for read-only access to the registry images." msgstr "" +msgid "Contents of .gitlab-ci.yml" +msgstr "" + msgid "Continue" msgstr "" @@ -2201,6 +2258,9 @@ msgstr "" msgid "Create New Directory" msgstr "" +msgid "Create New Domain" +msgstr "" + msgid "Create a new branch" msgstr "" @@ -2246,6 +2306,9 @@ msgstr "" msgid "Create merge request and branch" msgstr "" +msgid "Create milestone" +msgstr "" + msgid "Create new branch" msgstr "" @@ -2351,6 +2414,9 @@ msgstr "" msgid "CycleAnalyticsStage|Test" msgstr "" +msgid "DNS" +msgstr "" + msgid "Dashboard" msgstr "" @@ -2659,6 +2725,9 @@ msgstr "" msgid "Download" msgstr "" +msgid "Download artifacts" +msgstr "" + msgid "Download asset" msgstr "" @@ -2701,6 +2770,9 @@ msgstr "" msgid "Edit Label" msgstr "" +msgid "Edit Milestone" +msgstr "" + msgid "Edit Pipeline Schedule %{id}" msgstr "" @@ -2779,6 +2851,9 @@ msgstr "" msgid "Enable the Performance Bar for a given group." msgstr "" +msgid "Enable two-factor authentication" +msgstr "" + msgid "Enable usage ping" msgstr "" @@ -2818,6 +2893,9 @@ msgstr "" msgid "Environment variables are configured by your administrator to be %{link_start}protected%{link_end} by default" msgstr "" +msgid "Environment:" +msgstr "" + msgid "Environments" msgstr "" @@ -2971,6 +3049,9 @@ msgstr "" msgid "Error while loading the merge request. Please try again." msgstr "" +msgid "Error:" +msgstr "" + msgid "Errors" msgstr "" @@ -3016,6 +3097,9 @@ msgstr "" msgid "Everyone can contribute" msgstr "" +msgid "Except policy:" +msgstr "" + msgid "Existing Git repository" msgstr "" @@ -3067,6 +3151,9 @@ msgstr "" msgid "External URL" msgstr "" +msgid "External Wiki" +msgstr "" + msgid "Facebook" msgstr "" @@ -3124,6 +3211,9 @@ msgstr "" msgid "File added" msgstr "" +msgid "File browser" +msgstr "" + msgid "File deleted" msgstr "" @@ -3148,9 +3238,18 @@ msgstr "" msgid "Filter by commit message" msgstr "" +msgid "Filter by milestone name" +msgstr "" + msgid "Filter by two-factor authentication" msgstr "" +msgid "Filter results by group" +msgstr "" + +msgid "Filter results by project" +msgstr "" + msgid "Filter..." msgstr "" @@ -3415,6 +3514,9 @@ msgstr "" msgid "Group name" msgstr "" +msgid "Group:" +msgstr "" + msgid "Group: %{group_name}" msgstr "" @@ -3550,6 +3652,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 "" @@ -3564,9 +3669,6 @@ msgstr[1] "" msgid "Hide values" msgstr "" -msgid "Hide whitespace changes" -msgstr "" - msgid "History" msgstr "" @@ -3855,6 +3957,12 @@ msgstr "" msgid "Job has been erased" msgstr "" +msgid "Job is stuck. Check runners." +msgstr "" + +msgid "Job was retried" +msgstr "" + msgid "Jobs" msgstr "" @@ -3912,12 +4020,18 @@ msgstr "" msgid "June" msgstr "" +msgid "Key (PEM)" +msgstr "" + msgid "Kubernetes" msgstr "" msgid "Kubernetes Cluster" msgstr "" +msgid "Kubernetes Clusters" +msgstr "" + msgid "Kubernetes cluster creation time exceeds timeout; %{timeout}" msgstr "" @@ -4150,6 +4264,9 @@ msgstr "" msgid "Manage project labels" msgstr "" +msgid "Manage two-factor authentication" +msgstr "" + msgid "Manifest" msgstr "" @@ -4255,6 +4372,9 @@ msgstr "" msgid "Merge requests are a place to propose changes you've made to a project and discuss those changes with others" msgstr "" +msgid "MergeRequests|Jump to next unresolved discussion" +msgstr "" + msgid "MergeRequests|Resolve this discussion in a new issue" msgstr "" @@ -4270,6 +4390,9 @@ msgstr "" msgid "MergeRequests|View replaced file @ %{commitId}" msgstr "" +msgid "MergeRequests|commented on commit %{commitLink}" +msgstr "" + msgid "MergeRequests|started a discussion" msgstr "" @@ -4324,9 +4447,6 @@ msgstr "" msgid "Metrics|Learn about environments" msgstr "" -msgid "Metrics|No data to display" -msgstr "" - msgid "Metrics|No deployed environments" msgstr "" @@ -4482,6 +4602,12 @@ msgstr[1] "" msgid "New Label" msgstr "" +msgid "New Milestone" +msgstr "" + +msgid "New Pages Domain" +msgstr "" + msgid "New Pipeline Schedule" msgstr "" @@ -4521,6 +4647,9 @@ msgstr "" msgid "New merge request" msgstr "" +msgid "New milestone" +msgstr "" + msgid "New pipelines will cancel older, pending pipelines on the same branch" msgstr "" @@ -4602,6 +4731,9 @@ msgstr "" msgid "No messages were logged" msgstr "" +msgid "No milestones to show" +msgstr "" + msgid "No other labels with such name or description" msgstr "" @@ -4781,6 +4913,9 @@ msgstr "" msgid "Only mirror protected branches" msgstr "" +msgid "Only policy:" +msgstr "" + msgid "Only project members can comment." msgstr "" @@ -4856,6 +4991,12 @@ msgstr "" msgid "Pages" msgstr "" +msgid "Pages Domain" +msgstr "" + +msgid "Pages Domains" +msgstr "" + msgid "Pagination|Last »" msgstr "" @@ -4868,12 +5009,18 @@ msgstr "" msgid "Pagination|« First" msgstr "" +msgid "Parameter" +msgstr "" + msgid "Part of merge request changes" msgstr "" msgid "Password" msgstr "" +msgid "Past due" +msgstr "" + msgid "Paste your public SSH key, which is usually contained in the file '~/.ssh/id_rsa.pub' and begins with 'ssh-rsa'. Don't use your private SSH key." msgstr "" @@ -4910,6 +5057,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 "" @@ -5189,6 +5339,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 "" @@ -5204,6 +5360,9 @@ msgstr "" msgid "Profiles|Change username" msgstr "" +msgid "Profiles|Changing your username can have unintended side effects." +msgstr "" + msgid "Profiles|Choose file..." msgstr "" @@ -5216,6 +5375,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 "" @@ -5234,6 +5402,9 @@ msgstr "" msgid "Profiles|Deleting an account has the following effects:" msgstr "" +msgid "Profiles|Disconnect" +msgstr "" + msgid "Profiles|Do not show on profile" msgstr "" @@ -5246,6 +5417,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 "" @@ -5282,6 +5456,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 "" @@ -5309,6 +5486,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 "" @@ -5450,6 +5630,9 @@ msgstr "" msgid "Project slug" msgstr "" +msgid "Project:" +msgstr "" + msgid "ProjectActivityRSS|Subscribe" msgstr "" @@ -5525,9 +5708,6 @@ msgstr "" msgid "ProjectsDropdown|This feature requires browser localStorage support" msgstr "" -msgid "PrometheusDashboard|Time" -msgstr "" - msgid "PrometheusService|%{exporters} with %{metrics} were found" msgstr "" @@ -5746,6 +5926,9 @@ msgstr "" msgid "Rename folder" msgstr "" +msgid "Reopen milestone" +msgstr "" + msgid "Reply to this email directly or %{view_it_on_gitlab}." msgstr "" @@ -5958,6 +6141,9 @@ msgstr "" msgid "Save" msgstr "" +msgid "Save Changes" +msgstr "" + msgid "Save application" msgstr "" @@ -6012,6 +6198,9 @@ msgstr "" msgid "Search for projects, issues, etc." msgstr "" +msgid "Search groups" +msgstr "" + msgid "Search merge requests" msgstr "" @@ -6027,6 +6216,9 @@ msgstr "" msgid "Search project" msgstr "" +msgid "Search projects" +msgstr "" + msgid "Search users" msgstr "" @@ -6276,6 +6468,9 @@ msgstr "" msgid "Show complete raw log" msgstr "" +msgid "Show file browser" +msgstr "" + msgid "Show latest version" msgstr "" @@ -6323,6 +6518,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 "" @@ -6578,6 +6776,9 @@ msgstr "" msgid "Status" msgstr "" +msgid "Status:" +msgstr "" + msgid "Stop environment" msgstr "" @@ -6635,6 +6836,9 @@ msgstr "" msgid "Suggested change" msgstr "" +msgid "Support for custom certificates is disabled. Ask your system's administrator to enable it." +msgstr "" + msgid "Switch branch/tag" msgstr "" @@ -6653,6 +6857,9 @@ msgstr "" msgid "Tag" msgstr "" +msgid "Tag list:" +msgstr "" + msgid "Tags" msgstr "" @@ -6944,6 +7151,9 @@ msgstr "" msgid "This directory" msgstr "" +msgid "This domain is not verified. You will need to verify ownership before access is enabled." +msgstr "" + msgid "This group" msgstr "" @@ -7269,9 +7479,18 @@ msgstr "" msgid "Title" msgstr "" +msgid "Titles and Filenames" +msgstr "" + +msgid "To %{link_to_help} of your domain, add the above key to a TXT record within to your DNS configuration." +msgstr "" + msgid "To GitLab" msgstr "" +msgid "To access this domain create a new DNS record" +msgstr "" + msgid "To add an SSH key you need to %{generate_link_start}generate one%{link_end} or use an %{existing_link_start}existing key%{link_end}." msgstr "" @@ -7356,9 +7575,6 @@ msgstr "" msgid "Toggle discussion" msgstr "" -msgid "Toggle file browser" -msgstr "" - msgid "Toggle navigation" msgstr "" @@ -7479,6 +7695,9 @@ msgstr "" msgid "Unsubscribe at project level" msgstr "" +msgid "Unsubscribe from %{type}" +msgstr "" + msgid "Unverified" msgstr "" @@ -7509,6 +7728,12 @@ msgstr "" msgid "Upload New File" msgstr "" +msgid "Upload a certificate for your domain with all intermediates" +msgstr "" + +msgid "Upload a private key for your certificate" +msgstr "" + msgid "Upload file" msgstr "" @@ -7608,6 +7833,15 @@ msgstr "" msgid "Users requesting access to" msgstr "" +msgid "Validate" +msgstr "" + +msgid "Validate your GitLab CI configuration file" +msgstr "" + +msgid "Value" +msgstr "" + msgid "Various container registry settings." msgstr "" @@ -7617,6 +7851,9 @@ msgstr "" msgid "Various settings that affect GitLab performance." msgstr "" +msgid "Verification status" +msgstr "" + msgid "Verified" msgstr "" @@ -7692,6 +7929,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 "" @@ -7710,15 +7950,15 @@ msgstr "" msgid "Web terminal" msgstr "" -msgid "What's new?" -msgstr "" - msgid "When a runner is locked, it cannot be assigned to other projects" msgstr "" msgid "When enabled, users cannot use GitLab until the terms have been accepted." msgstr "" +msgid "When:" +msgstr "" + msgid "Who can see this group?" msgstr "" @@ -7869,6 +8109,9 @@ msgstr "" msgid "Write a comment or drag your files here…" msgstr "" +msgid "Write milestone description..." +msgstr "" + msgid "Yes" msgstr "" @@ -7974,10 +8217,10 @@ msgstr "" msgid "You need to register a two-factor authentication app before you can set up a U2F device." msgstr "" -msgid "You will loose all changes you've made to this file. This action cannot be undone." +msgid "You will lose all changes you've made to this file. This action cannot be undone." msgstr "" -msgid "You will loose all the unstaged changes you've made in this project. This action cannot be undone." +msgid "You will lose all the unstaged changes you've made in this project. This action cannot be undone." msgstr "" msgid "You will not get any notifications via email" @@ -8073,6 +8316,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 "" @@ -8082,6 +8328,9 @@ msgstr "" msgid "ago" msgstr "" +msgid "allowed to fail" +msgstr "" + msgid "among other things" msgstr "" @@ -8097,6 +8346,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 "" @@ -8170,6 +8422,12 @@ msgstr "" msgid "importing" msgstr "" +msgid "in group %{link_to_group}" +msgstr "" + +msgid "in project %{link_to_project}" +msgstr "" + msgid "issue boards" msgstr "" @@ -8191,11 +8449,17 @@ msgstr "" msgid "latest version" msgstr "" +msgid "manual" +msgstr "" + msgid "merge request" msgid_plural "merge requests" msgstr[0] "" msgstr[1] "" +msgid "missing" +msgstr "" + msgid "mrWidget| Please restore it or use a different %{missingBranchName} branch" msgstr "" @@ -8241,6 +8505,9 @@ msgstr "" msgid "mrWidget|Create an issue to resolve them later" msgstr "" +msgid "mrWidget|Delete source branch" +msgstr "" + msgid "mrWidget|Deployment statistics are not available currently" msgstr "" @@ -8301,12 +8568,6 @@ msgstr "" msgid "mrWidget|Refreshing now" msgstr "" -msgid "mrWidget|Remove Source Branch" -msgstr "" - -msgid "mrWidget|Remove source branch" -msgstr "" - msgid "mrWidget|Request to merge" msgstr "" @@ -8340,19 +8601,19 @@ msgstr "" msgid "mrWidget|The source branch HEAD has recently changed. Please reload the page and review the changes before merging" msgstr "" -msgid "mrWidget|The source branch has been removed" +msgid "mrWidget|The source branch has been deleted" msgstr "" msgid "mrWidget|The source branch is %{commitsBehindLinkStart}%{commitsBehind}%{commitsBehindLinkEnd} the target branch" msgstr "" -msgid "mrWidget|The source branch is being removed" +msgid "mrWidget|The source branch is being deleted" msgstr "" -msgid "mrWidget|The source branch will be removed" +msgid "mrWidget|The source branch will be deleted" msgstr "" -msgid "mrWidget|The source branch will not be removed" +msgid "mrWidget|The source branch will not be deleted" msgstr "" msgid "mrWidget|There are merge conflicts" @@ -8376,10 +8637,10 @@ msgstr "" msgid "mrWidget|You are not allowed to edit this project directly. Please fork to make changes." msgstr "" -msgid "mrWidget|You can merge this merge request manually using the" +msgid "mrWidget|You can delete the source branch now" msgstr "" -msgid "mrWidget|You can remove source branch now" +msgid "mrWidget|You can merge this merge request manually using the" msgstr "" msgid "mrWidget|branch does not exist." @@ -8400,6 +8661,9 @@ msgstr "" msgid "new merge request" msgstr "" +msgid "none" +msgstr "" + msgid "notification emails" msgstr "" @@ -8422,6 +8686,9 @@ msgstr "" msgid "personal access token" msgstr "" +msgid "private" +msgstr "" + msgid "project" msgstr "" @@ -8469,9 +8736,18 @@ msgstr "" msgid "stuck" msgstr "" +msgid "syntax is correct" +msgstr "" + +msgid "syntax is incorrect" +msgstr "" + msgid "this document" msgstr "" +msgid "triggered" +msgstr "" + msgid "updated" msgstr "" @@ -8481,6 +8757,9 @@ msgstr "" msgid "uses Kubernetes clusters to deploy your code!" msgstr "" +msgid "verify ownership" +msgstr "" + msgid "view it on GitLab" msgstr "" diff --git a/package.json b/package.json index c4f749fb2c6..13c0527c4a3 100644 --- a/package.json +++ b/package.json @@ -21,15 +21,15 @@ }, "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", + "@gitlab/svgs": "^1.48.0", + "@gitlab/ui": "^1.22.1", "apollo-boost": "^0.1.20", "apollo-client": "^2.4.5", "autosize": "^4.0.0", @@ -76,15 +76,17 @@ "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", - "monaco-editor-webpack-plugin": "^1.5.4", + "monaco-editor": "^0.15.6", + "monaco-editor-webpack-plugin": "^1.7.0", "mousetrap": "^1.4.6", "pikaday": "^1.6.1", "popper.js": "^1.14.3", "prismjs": "^1.6.0", + "prosemirror-markdown": "^1.3.0", + "prosemirror-model": "^1.6.4", "raphael": "^2.2.7", "raven-js": "^3.22.1", "raw-loader": "^1.0.0", @@ -101,6 +103,9 @@ "three-orbit-controls": "^82.1.0", "three-stl-loader": "^1.0.4", "timeago.js": "^3.0.2", + "tiptap": "^1.8.0", + "tiptap-commands": "^1.4.0", + "tiptap-extensions": "^1.8.0", "underscore": "^1.9.0", "url-loader": "^1.1.2", "visibilityjs": "^1.2.4", @@ -112,7 +117,7 @@ "vue-template-compiler": "^2.5.21", "vue-virtual-scroll-list": "^1.2.5", "vuex": "^3.0.1", - "webpack": "^4.28.1", + "webpack": "^4.29.0", "webpack-bundle-analyzer": "^3.0.3", "webpack-cli": "^3.2.1", "webpack-stats-plugin": "^0.2.1", @@ -160,11 +165,12 @@ "karma-mocha-reporter": "^2.2.5", "karma-sourcemap-loader": "^0.3.7", "karma-webpack": "^4.0.0-beta.0", - "nodemon": "^1.18.4", - "prettier": "1.15.2", + "nodemon": "^1.18.9", + "pixelmatch": "^4.0.2", + "prettier": "1.16.1", "vue-jest": "^3.0.2", "webpack-dev-server": "^3.1.14", - "yarn-deduplicate": "^1.0.5" + "yarn-deduplicate": "^1.1.0" }, "engines": { "node": ">=8.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..9f84bdc3828 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.1) 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..9a7b9c6bb35 --- /dev/null +++ b/qa/Rakefile @@ -0,0 +1,12 @@ +require_relative 'qa/tools/revoke_all_personal_access_tokens' +require_relative 'qa/tools/delete_subgroups' + +desc "Revokes all personal access tokens" +task :revoke_personal_access_tokens do + QA::Tools::RevokeAllPersonalAccessTokens.new.run +end + +desc "Deletes subgroups within a provided group" +task :delete_subgroups do + QA::Tools::DeleteSubgroups.new.run +end @@ -99,6 +99,7 @@ module QA autoload :LDAPNoTLS, 'qa/scenario/test/integration/ldap_no_tls' autoload :LDAPTLS, 'qa/scenario/test/integration/ldap_tls' autoload :InstanceSAML, 'qa/scenario/test/integration/instance_saml' + autoload :OAuth, 'qa/scenario/test/integration/oauth' autoload :Kubernetes, 'qa/scenario/test/integration/kubernetes' autoload :Mattermost, 'qa/scenario/test/integration/mattermost' autoload :ObjectStorage, 'qa/scenario/test/integration/object_storage' @@ -158,6 +159,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 +196,15 @@ module QA autoload :MirroringRepositories, 'qa/page/project/settings/mirroring_repositories' end + module SubMenus + autoload :CiCd, 'qa/page/project/sub_menus/ci_cd' + autoload :Common, 'qa/page/project/sub_menus/common' + autoload :Issues, 'qa/page/project/sub_menus/issues' + autoload :Operations, 'qa/page/project/sub_menus/operations' + autoload :Repository, 'qa/page/project/sub_menus/repository' + autoload :Settings, 'qa/page/project/sub_menus/settings' + end + module Issue autoload :New, 'qa/page/project/issue/new' autoload :Show, 'qa/page/project/issue/show' @@ -277,6 +291,7 @@ module QA # module Component autoload :ClonePanel, 'qa/page/component/clone_panel' + autoload :LazyLoader, 'qa/page/component/lazy_loader' autoload :LegacyClonePanel, 'qa/page/component/legacy_clone_panel' autoload :Dropzone, 'qa/page/component/dropzone' autoload :GroupsFilter, 'qa/page/component/groups_filter' @@ -328,6 +343,13 @@ module QA autoload :Login, 'qa/vendor/saml_idp/page/login' end end + + module Github + module Page + autoload :Base, 'qa/vendor/github/page/base' + autoload :Login, 'qa/vendor/github/page/login' + end + end end # Classes that provide support to other parts of the framework. diff --git a/qa/qa/git/repository.rb b/qa/qa/git/repository.rb index 7f959441dac..0aa94101098 100644 --- a/qa/qa/git/repository.rb +++ b/qa/qa/git/repository.rb @@ -5,20 +5,25 @@ require 'uri' require 'open3' require 'fileutils' require 'tmpdir' +require 'tempfile' +require 'securerandom' module QA module Git class Repository include Scenario::Actable - attr_writer :password + attr_writer :use_lfs attr_accessor :env_vars + InvalidCredentialsError = Class.new(RuntimeError) + 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) @@ -27,31 +32,41 @@ module QA end end + def password=(password) + @password = password + + raise InvalidCredentialsError, "Please provide a username when setting a password" unless username + + try_add_credentials_to_netrc + end + def uri=(address) @uri = URI(address) end 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 - def checkout(branch_name) - run(%Q{git checkout "#{branch_name}"}) + enable_lfs_result = enable_lfs if use_lfs? + + 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 +76,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 +86,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 +121,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 +156,56 @@ 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? + + save_netrc_content + 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? + + 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,24 +216,11 @@ module QA end end - def tmp_netrc_directory - @tmp_netrc_directory ||= File.join(Dir.tmpdir, "qa-netrc-credentials", $$.to_s) + def read_netrc_content + File.exist?(netrc_file_path) ? File.readlines(netrc_file_path) : [] end - def netrc_file_path - @netrc_file_path ||= File.join(tmp_netrc_directory, '.netrc') - end - - def netrc_content - "machine #{uri.host} login #{username} password #{password}" - end - - def netrc_already_contains_content? - File.exist?(netrc_file_path) && - File.readlines(netrc_file_path).grep(/^#{netrc_content}$/).any? - end - - def add_credentials_to_netrc + def save_netrc_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 @@ -183,12 +228,26 @@ module QA # 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) + FileUtils.mkdir_p(tmp_home_dir) File.open(netrc_file_path, 'a') { |file| file.puts(netrc_content) } File.chmod(0600, netrc_file_path) end + + 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_home_dir, '.netrc') + end + + def netrc_content + "machine #{uri.host} login #{username} password #{password}" + end + + def netrc_already_contains_content? + read_netrc_content.grep(/^#{netrc_content}$/).any? + end end end end diff --git a/qa/qa/page/base.rb b/qa/qa/page/base.rb index c3c90f254b7..b1f27131207 100644 --- a/qa/qa/page/base.rb +++ b/qa/qa/page/base.rb @@ -128,6 +128,10 @@ module QA page.has_no_text? text end + def finished_loading? + has_no_css?('.fa-spinner', wait: Capybara.default_max_wait_time) + end + def within_element(name) page.within(element_selector_css(name)) do yield diff --git a/qa/qa/page/component/lazy_loader.rb b/qa/qa/page/component/lazy_loader.rb new file mode 100644 index 00000000000..6f74a4691ba --- /dev/null +++ b/qa/qa/page/component/lazy_loader.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module QA + module Page + module Component + module LazyLoader + def self.included(base) + base.view 'app/assets/javascripts/lazy_loader.js' do + element :js_lazy_loaded + end + end + end + end + end +end diff --git a/qa/qa/page/group/show.rb b/qa/qa/page/group/show.rb index 0f0ab81a4ef..6dd9ff997a4 100644 --- a/qa/qa/page/group/show.rb +++ b/qa/qa/page/group/show.rb @@ -6,7 +6,7 @@ module QA class Show < Page::Base include Page::Component::GroupsFilter - view 'app/views/groups/show.html.haml' do + view 'app/views/groups/_home_panel.html.haml' do element :new_project_or_subgroup_dropdown element :new_project_or_subgroup_dropdown_toggle element :new_project_option diff --git a/qa/qa/page/label/index.rb b/qa/qa/page/label/index.rb index 323acd57743..f0d323ca3b4 100644 --- a/qa/qa/page/label/index.rb +++ b/qa/qa/page/label/index.rb @@ -1,12 +1,27 @@ +# frozen_string_literal: true + module QA module Page module Label class Index < Page::Base - view 'app/views/projects/labels/index.html.haml' do + include Component::LazyLoader + + 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 + 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/main/login.rb b/qa/qa/page/main/login.rb index cb83ace20b6..d5377f1d1c1 100644 --- a/qa/qa/page/main/login.rb +++ b/qa/qa/page/main/login.rb @@ -31,8 +31,9 @@ module QA element :register_tab end - view 'app/views/devise/shared/_omniauth_box.html.haml' do + view 'app/helpers/auth_helper.rb' do element :saml_login_button + element :github_login_button end view 'app/views/layouts/devise.html.haml' do @@ -132,6 +133,16 @@ module QA click_element :standard_tab end + def sign_in_with_github + set_initial_password_if_present + click_element :github_login_button + end + + def sign_in_with_saml + set_initial_password_if_present + click_element :saml_login_button + end + private def sign_in_using_ldap_credentials @@ -142,11 +153,6 @@ module QA click_element :sign_in_button end - def sign_in_with_saml - set_initial_password_if_present - click_element :saml_login_button - end - def sign_in_using_gitlab_credentials(user) switch_to_sign_in_tab if has_sign_in_tab? switch_to_standard_tab if has_standard_tab? diff --git a/qa/qa/page/main/menu.rb b/qa/qa/page/main/menu.rb index 6804cc8fb20..616d50f47fc 100644 --- a/qa/qa/page/main/menu.rb +++ b/qa/qa/page/main/menu.rb @@ -57,8 +57,12 @@ module QA end def go_to_profile_settings - within_user_menu do - click_link 'Settings' + with_retry(reload: false) do + within_user_menu do + click_link 'Settings' + end + + has_text?('User Settings') end end diff --git a/qa/qa/page/merge_request/show.rb b/qa/qa/page/merge_request/show.rb index 4f21ed602d9..f54bea880a0 100644 --- a/qa/qa/page/merge_request/show.rb +++ b/qa/qa/page/merge_request/show.rb @@ -50,17 +50,17 @@ module QA end def fast_forward_possible? - !has_text?('Fast-forward merge is not possible') + has_no_text?('Fast-forward merge is not possible') end def has_merge_button? refresh - has_css?(element_selector_css(:merge_button)) + has_element?(:merge_button) end def has_merge_options? - has_css?(element_selector_css(:merge_moment_dropdown)) + has_element?(:merge_moment_dropdown) end def merge_immediately @@ -75,19 +75,21 @@ module QA def rebase! # The rebase button is disabled on load wait do - has_css?(element_selector_css(:mr_rebase_button)) + has_element?(:mr_rebase_button) end # The rebase button is enabled via JS wait(reload: false) do - !first(element_selector_css(:mr_rebase_button)).disabled? + !find_element(:mr_rebase_button).disabled? end click_element :mr_rebase_button - wait(reload: false) do + success = wait do has_text?('Fast-forward merge without a merge commit') end + + raise "Rebase did not appear to be successful" unless success end def has_assignee?(username) @@ -106,30 +108,32 @@ module QA def merge! # The merge button is disabled on load wait do - has_css?(element_selector_css(:merge_button)) + has_element?(:merge_button) end # The merge button is enabled via JS wait(reload: false) do - !first(element_selector_css(:merge_button)).disabled? + !find_element(:merge_button).disabled? end merge_immediately - wait(reload: false) do + success = wait do has_text?('The changes were merged into') end + + raise "Merge did not appear to be successful" unless success end def mark_to_squash # The squash checkbox is disabled on load wait do - has_css?(element_selector_css(:squash_checkbox)) + has_element?(:squash_checkbox) end # The squash checkbox is enabled via JS wait(reload: false) do - !first(element_selector_css(:squash_checkbox)).disabled? + !find_element(:squash_checkbox).disabled? end click_element :squash_checkbox @@ -145,7 +149,7 @@ module QA def add_comment_to_diff(text) wait(time: 5) do - page.has_text?("No newline at end of file") + has_text?("No newline at end of file") end all_elements(:new_diff_line).first.hover click_element :diff_comment 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..922a6ddb086 --- /dev/null +++ b/qa/qa/page/project/branches/show.rb @@ -0,0 +1,64 @@ +# 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 + click_element(:remove_btn) + end + end + end + + finished_loading? + 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..46dfe87fe25 100644 --- a/qa/qa/page/project/menu.rb +++ b/qa/qa/page/project/menu.rb @@ -4,162 +4,35 @@ module QA module Page module Project class Menu < Page::Base - 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_members_settings - element :pipelines_settings_link, "title: _('CI / CD')" # rubocop:disable QA/ElementWithPattern - element :operations_kubernetes_link, "title: _('Kubernetes')" # rubocop:disable QA/ElementWithPattern - element :operations_environments_link - element :issues_link, /link_to.*shortcuts-issues/ # rubocop:disable QA/ElementWithPattern - element :issues_link_text, "Issues" # rubocop:disable QA/ElementWithPattern - 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 - element :labels_link - end - - view 'app/assets/javascripts/fly_out_nav.js' do - element :fly_out, "classList.add('fly-out-list')" # rubocop:disable QA/ElementWithPattern - end - - def click_ci_cd_pipelines - within_sidebar do - click_element :link_pipelines - end - end + include SubMenus::Common - def click_ci_cd_settings - hover_settings do - within_submenu do - click_link('CI / CD') - end - end - end - - def click_issues - within_sidebar do - click_link('Issues') - end - end + include SubMenus::CiCd + include SubMenus::Issues + include SubMenus::Operations + include SubMenus::Repository + include SubMenus::Settings - def click_members_settings - hover_settings do - within_submenu do - click_element :link_members_settings - end - end + view 'app/views/layouts/nav/sidebar/_project.html.haml' do + element :activity_link + element :merge_requests_link + element :wiki_link end def click_merge_requests within_sidebar do - click_link('Merge Requests') - end - end - - def click_operations_environments - hover_operations do - within_submenu do - click_element(:operations_environments_link) - end - end - end - - def click_operations_kubernetes - hover_operations do - within_submenu do - click_link('Kubernetes') - end - end - end - - def click_milestones - within_sidebar do - click_element :milestones_link - end - end - - def click_repository - within_sidebar do - click_link('Repository') - end - end - - def click_repository_settings - hover_settings do - within_submenu do - click_link('Repository') - end + click_element(:merge_requests_link) end end def click_wiki within_sidebar do - click_link('Wiki') + click_element(:wiki_link) end end def go_to_activity within_sidebar do - click_on 'Activity' - end - end - - def go_to_labels - hover_issues do - within_submenu do - click_element(:labels_link) - end - end - end - - def go_to_settings - within_sidebar do - click_on 'Settings' - end - end - - private - - def hover_issues - within_sidebar do - find_element(:issues_item).hover - - yield - end - end - - def hover_operations - within_sidebar do - find('.shortcuts-operations').hover - - yield - end - end - - 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 - - def within_submenu - page.within('.fly-out-list') do - yield + click_element(:activity_link) end end end diff --git a/qa/qa/page/project/operations/kubernetes/show.rb b/qa/qa/page/project/operations/kubernetes/show.rb index 9e8f9ba79d7..98ac5c32d91 100644 --- a/qa/qa/page/project/operations/kubernetes/show.rb +++ b/qa/qa/page/project/operations/kubernetes/show.rb @@ -30,7 +30,7 @@ module QA def ingress_ip # We need to wait longer since it can take some time before the # ip address is assigned for the ingress controller - page.find('#ingress-ip-address', wait: 500).value + page.find('#ingress-ip-address', wait: 1200).value end end end diff --git a/qa/qa/page/project/sub_menus/ci_cd.rb b/qa/qa/page/project/sub_menus/ci_cd.rb new file mode 100644 index 00000000000..adae2ce08c4 --- /dev/null +++ b/qa/qa/page/project/sub_menus/ci_cd.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module QA + module Page + module Project + module SubMenus + module CiCd + def self.included(base) + base.class_eval do + view 'app/views/layouts/nav/sidebar/_project.html.haml' do + element :link_pipelines + end + end + end + + def click_ci_cd_pipelines + within_sidebar do + click_element :link_pipelines + end + end + end + end + end + end +end 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/issues.rb b/qa/qa/page/project/sub_menus/issues.rb new file mode 100644 index 00000000000..f81e4f34909 --- /dev/null +++ b/qa/qa/page/project/sub_menus/issues.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +module QA + module Page + module Project + module SubMenus + module Issues + def self.included(base) + base.class_eval do + view 'app/views/layouts/nav/sidebar/_project.html.haml' do + element :issues_item + element :labels_link + element :milestones_link + end + end + end + + def click_issues + within_sidebar do + click_link('Issues') + end + end + + def click_milestones + within_sidebar do + click_element :milestones_link + end + end + + def go_to_labels + hover_issues do + within_submenu do + click_element(:labels_link) + end + end + end + + private + + def hover_issues + within_sidebar do + scroll_to_element(:issues_item) + find_element(:issues_item).hover + + yield + end + end + end + end + end + end +end diff --git a/qa/qa/page/project/sub_menus/operations.rb b/qa/qa/page/project/sub_menus/operations.rb new file mode 100644 index 00000000000..cf9fc453565 --- /dev/null +++ b/qa/qa/page/project/sub_menus/operations.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +module QA + module Page + module Project + module SubMenus + module Operations + def self.included(base) + base.class_eval do + view 'app/views/layouts/nav/sidebar/_project.html.haml' do + element :link_operations + element :operations_environments_link + end + end + end + + def click_operations_environments + hover_operations do + within_submenu do + click_element(:operations_environments_link) + end + end + end + + def click_operations_kubernetes + hover_operations do + within_submenu do + click_link('Kubernetes') + end + end + end + + private + + def hover_operations + within_sidebar do + scroll_to_element(:link_operations) + find_element(:link_operations).hover + + 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/page/project/sub_menus/settings.rb b/qa/qa/page/project/sub_menus/settings.rb new file mode 100644 index 00000000000..62c594c0210 --- /dev/null +++ b/qa/qa/page/project/sub_menus/settings.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +module QA + module Page + module Project + module SubMenus + module Settings + def self.included(base) + base.class_eval do + view 'app/views/layouts/nav/sidebar/_project.html.haml' do + element :settings_item + element :link_members_settings + end + end + end + + def click_ci_cd_settings + hover_settings do + within_submenu do + click_link('CI / CD') + end + end + end + + def click_members_settings + hover_settings do + within_submenu do + click_element :link_members_settings + end + end + end + + def click_repository_settings + hover_settings do + within_submenu do + click_link('Repository') + end + end + end + + def go_to_settings + within_sidebar do + click_on 'Settings' + end + end + + private + + def hover_settings + within_sidebar do + scroll_to_element(:settings_item) + find_element(:settings_item).hover + + yield + end + end + end + end + end + end +end diff --git a/qa/qa/page/project/wiki/new.rb b/qa/qa/page/project/wiki/new.rb index 2498af8600c..b90e03be36a 100644 --- a/qa/qa/page/project/wiki/new.rb +++ b/qa/qa/page/project/wiki/new.rb @@ -1,42 +1,58 @@ +# frozen_string_literal: true + module QA module Page module Project module Wiki class New < Page::Base + include Component::LazyLoader + view 'app/views/projects/wikis/_form.html.haml' do - element :wiki_title_textbox, 'text_field :title' # rubocop:disable QA/ElementWithPattern - element :wiki_content_textarea, "render 'projects/zen', f: f, attr: :content" # rubocop:disable QA/ElementWithPattern - element :wiki_message_textbox, 'text_field :message' # rubocop:disable QA/ElementWithPattern - element :save_changes_button, 'submit _("Save changes")' # rubocop:disable QA/ElementWithPattern - element :create_page_button, 'submit s_("Wiki|Create page")' # rubocop:disable QA/ElementWithPattern + element :wiki_title_textbox + element :wiki_content_textarea + element :wiki_message_textbox + element :save_changes_button + element :create_page_button end view 'app/views/shared/empty_states/_wikis.html.haml' do - element :create_link, 'Create your first page' # rubocop:disable QA/ElementWithPattern + element :create_first_page_link + end + + view 'app/views/shared/empty_states/_wikis_layout.html.haml' do + element :svg_content end def go_to_create_first_page - click_link 'Create your first page' + # The svg takes a fraction of a second to load after which the + # "Create your first page" button shifts up a bit. This can cause + # webdriver to miss the hit so we wait for the svg to load before + # clicking the button. + within_element(:svg_content) do + has_element? :js_lazy_loaded + end + + click_element :create_first_page_link end def set_title(title) - fill_in 'wiki_title', with: title + fill_element :wiki_title_textbox, title end def set_content(content) - fill_in 'wiki_content', with: content + fill_element :wiki_content_textarea, content end def set_message(message) - fill_in 'wiki_message', with: message + fill_element :wiki_message_textbox, message end def save_changes - click_on 'Save changes' + click_element :save_changes_button end def create_new_page - click_on 'Create page' + click_element :create_page_button 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/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..a5827fb6e73 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,25 @@ module QA email = user.email end - repository.clone + @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/resource/user.rb b/qa/qa/resource/user.rb index b9580d81171..6c5e91b6488 100644 --- a/qa/qa/resource/user.rb +++ b/qa/qa/resource/user.rb @@ -17,11 +17,11 @@ module QA end def username - @username ||= "qa-user-#{unique_id}" + @username || "qa-user-#{unique_id}" end def password - @password ||= 'password' + @password || 'password' end def name @@ -29,7 +29,15 @@ module QA end def email - @email ||= api_resource&.dig(:email) || "#{username}@example.com" + @email ||= "#{username}@example.com" + end + + def public_email + @public_email ||= begin + api_public_email = api_resource&.dig(:public_email) + + api_public_email && api_public_email != '' ? api_public_email : Runtime::User.default_email + end end def credentials_given? diff --git a/qa/qa/runtime/env.rb b/qa/qa/runtime/env.rb index 79b40223d84..dd0ddbdbd6b 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 @@ -100,6 +100,14 @@ module QA ENV['GITLAB_ADMIN_PASSWORD'] end + def github_username + ENV['GITHUB_USERNAME'] + end + + def github_password + ENV['GITHUB_PASSWORD'] + end + def forker? !!(forker_username && forker_password) end diff --git a/qa/qa/runtime/user.rb b/qa/qa/runtime/user.rb index 5eb7a210fce..e8bcb8a9f50 100644 --- a/qa/qa/runtime/user.rb +++ b/qa/qa/runtime/user.rb @@ -7,6 +7,10 @@ module QA 'root' end + def default_email + 'admin@example.com' + end + def default_password '5iveL!fe' end diff --git a/qa/qa/scenario/test/integration/oauth.rb b/qa/qa/scenario/test/integration/oauth.rb new file mode 100644 index 00000000000..912156fbc29 --- /dev/null +++ b/qa/qa/scenario/test/integration/oauth.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module QA + module Scenario + module Test + module Integration + class OAuth < Test::Instance::All + tags :oauth + end + end + end + end +end diff --git a/qa/qa/specs/features/browser_ui/1_manage/login/login_via_oauth_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/login/login_via_oauth_spec.rb new file mode 100644 index 00000000000..a118176eb8a --- /dev/null +++ b/qa/qa/specs/features/browser_ui/1_manage/login/login_via_oauth_spec.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module QA + context 'Manage', :orchestrated, :oauth do + describe 'OAuth login' do + it 'User logs in to GitLab with GitHub OAuth' do + Runtime::Browser.visit(:gitlab, Page::Main::Login) + + Page::Main::Login.perform(&:sign_in_with_github) + Vendor::Github::Page::Login.perform(&:login) + + expect(page).to have_content('Welcome to GitLab') + end + end + end +end diff --git a/qa/qa/specs/features/browser_ui/3_create/merge_request/create_merge_request_spec.rb b/qa/qa/specs/features/browser_ui/3_create/merge_request/create_merge_request_spec.rb index 6ddd7dde2cf..c06f13ee204 100644 --- a/qa/qa/specs/features/browser_ui/3_create/merge_request/create_merge_request_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/merge_request/create_merge_request_spec.rb @@ -3,7 +3,7 @@ module QA context 'Create' do describe 'Merge request creation' do - it 'user creates a new merge request' do + it 'user creates a new merge request' do gitlab_account_username = "@#{Runtime::User.username}" Runtime::Browser.visit(:gitlab, Page::Main::Login) @@ -49,7 +49,7 @@ module QA end describe 'creates a merge request', :smoke do - it 'user creates a new merge request' do + it 'user creates a new merge request' do Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.act { sign_in_using_credentials } 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/merge_request/rebase_merge_request_spec.rb b/qa/qa/specs/features/browser_ui/3_create/merge_request/rebase_merge_request_spec.rb index e2d639fd150..3fbcd77dac6 100644 --- a/qa/qa/specs/features/browser_ui/3_create/merge_request/rebase_merge_request_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/merge_request/rebase_merge_request_spec.rb @@ -3,17 +3,17 @@ module QA context 'Create' do describe 'Merge request rebasing' do - it 'user rebases source branch of merge request' do + it 'user rebases source branch of merge request' do Runtime::Browser.visit(:gitlab, Page::Main::Login) - Page::Main::Login.act { sign_in_using_credentials } + Page::Main::Login.perform(&:sign_in_using_credentials) project = Resource::Project.fabricate! do |project| project.name = "only-fast-forward" end project.visit! - Page::Project::Menu.act { go_to_settings } - Page::Project::Settings::MergeRequest.act { enable_ff_only } + Page::Project::Menu.perform(&:go_to_settings) + Page::Project::Settings::MergeRequest.perform(&:enable_ff_only) merge_request = Resource::MergeRequest.fabricate! do |merge_request| merge_request.project = project @@ -38,7 +38,7 @@ module QA merge_request.rebase! expect(merge_request).to have_merge_button - expect(merge_request.fast_forward_possible?).to be_truthy + expect(merge_request).to be_fast_forward_possible end end end diff --git a/qa/qa/specs/features/browser_ui/3_create/merge_request/squash_merge_request_spec.rb b/qa/qa/specs/features/browser_ui/3_create/merge_request/squash_merge_request_spec.rb index 4126f967ee2..10cc0480794 100644 --- a/qa/qa/specs/features/browser_ui/3_create/merge_request/squash_merge_request_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/merge_request/squash_merge_request_spec.rb @@ -3,7 +3,7 @@ module QA context 'Create' do describe 'Merge request squashing' do - it 'user squashes commits while merging' do + it 'user squashes commits while merging' do Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.perform(&:sign_in_using_credentials) 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..3567ddca1a1 --- /dev/null +++ b/qa/qa/specs/features/browser_ui/3_create/repository/add_list_delete_branches_spec.rb @@ -0,0 +1,93 @@ +# 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.try_add_credentials_to_netrc + + 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/3_create/repository/push_http_private_token_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/push_http_private_token_spec.rb index a63b7dce8d6..3310a873a60 100644 --- a/qa/qa/specs/features/browser_ui/3_create/repository/push_http_private_token_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/repository/push_http_private_token_spec.rb @@ -3,7 +3,7 @@ module QA context 'Create' do describe 'Git push over HTTP', :ldap_no_tls do - it 'user using a personal access token pushes code to the repository' do + it 'user using a personal access token pushes code to the repository' do Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.perform(&:sign_in_using_credentials) diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/push_mirroring_over_http_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/push_mirroring_over_http_spec.rb index 2d0e281ab59..9d31a25ab35 100644 --- a/qa/qa/specs/features/browser_ui/3_create/repository/push_mirroring_over_http_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/repository/push_mirroring_over_http_spec.rb @@ -3,7 +3,7 @@ module QA context 'Create' do describe 'Push mirror a repository over HTTP' do - it 'configures and syncs a (push) mirrored repository' do + it 'configures and syncs a (push) mirrored repository' do Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.perform(&:sign_in_using_credentials) diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_spec.rb index ad6426df420..d10ad896b3b 100644 --- a/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_spec.rb @@ -3,7 +3,7 @@ module QA context 'Create' do describe 'Git push over HTTP', :ldap_no_tls do - it 'user pushes code to the repository' do + it 'user pushes code to the repository' do Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.act { sign_in_using_credentials } diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/user_views_raw_diff_patch_requests_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/user_views_raw_diff_patch_requests_spec.rb index 3a5d89e6b83..b862a7bd1ed 100644 --- a/qa/qa/specs/features/browser_ui/3_create/repository/user_views_raw_diff_patch_requests_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/repository/user_views_raw_diff_patch_requests_spec.rb @@ -2,7 +2,10 @@ module QA context 'Create' do - describe 'Commit data' do + # failure reported: https://gitlab.com/gitlab-org/quality/nightly/issues/42 + # also failing in staging until the fix is picked into the next release: + # https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/24533 + describe 'Commit data', :quarantine do before(:context) do Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.perform(&:sign_in_using_credentials) @@ -47,7 +50,7 @@ module QA Page::Project::Commit::Show.perform(&:select_email_patches) - expect(page).to have_content("From: #{user.name} <#{user.email}>") + expect(page).to have_content("From: #{user.name} <#{user.public_email}>") expect(page).to have_content('Subject: [PATCH] Add second file') expect(page).to have_content('diff --git a/second b/second') end diff --git a/qa/qa/specs/features/browser_ui/3_create/wiki/create_edit_clone_push_wiki_spec.rb b/qa/qa/specs/features/browser_ui/3_create/wiki/create_edit_clone_push_wiki_spec.rb index a7d0998d42c..29589ec870a 100644 --- a/qa/qa/specs/features/browser_ui/3_create/wiki/create_edit_clone_push_wiki_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/wiki/create_edit_clone_push_wiki_spec.rb @@ -3,22 +3,15 @@ module QA context 'Create' do describe 'Wiki management' do - def login - Runtime::Browser.visit(:gitlab, Page::Main::Login) - Page::Main::Login.act { sign_in_using_credentials } - end - def validate_content(content) expect(page).to have_content('Wiki was successfully updated') expect(page).to have_content(/#{content}/) end - before do - login - end + it 'user creates, edits, clones, and pushes to the wiki' do + Runtime::Browser.visit(:gitlab, Page::Main::Login) + Page::Main::Login.perform(&:sign_in_using_credentials) - # Failure reported: https://gitlab.com/gitlab-org/quality/nightly/issues/24 - it 'user creates, edits, clones, and pushes to the wiki', :quarantine do wiki = Resource::Wiki.fabricate! do |resource| resource.title = 'Home' resource.content = '# My First Wiki Content' @@ -27,7 +20,7 @@ module QA validate_content('My First Wiki Content') - Page::Project::Wiki::Edit.act { go_to_edit_page } + Page::Project::Wiki::Edit.perform(&:go_to_edit_page) Page::Project::Wiki::New.perform do |page| page.set_content("My Second Wiki Content") page.save_changes @@ -41,7 +34,7 @@ module QA push.file_content = '# My Third Wiki Content' push.commit_message = 'Update Home.md' end - Page::Project::Menu.act { click_wiki } + Page::Project::Menu.perform(&:click_wiki) expect(page).to have_content('My Third Wiki Content') end diff --git a/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb b/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb index 5147b17d7ab..553550eef8b 100644 --- a/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb +++ b/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb @@ -3,7 +3,8 @@ require 'pathname' module QA - context 'Configure', :orchestrated, :kubernetes do + # Transient failure issue: https://gitlab.com/gitlab-org/quality/nightly/issues/68 + context 'Configure', :orchestrated, :kubernetes, :quarantine do describe 'Auto DevOps support' do def login Runtime::Browser.visit(:gitlab, Page::Main::Login) diff --git a/qa/qa/support/api.rb b/qa/qa/support/api.rb index 1107d43161e..8aa7d6812ac 100644 --- a/qa/qa/support/api.rb +++ b/qa/qa/support/api.rb @@ -20,6 +20,24 @@ module QA e.response end + def delete(url) + RestClient::Request.execute( + method: :delete, + url: url, + verify_ssl: false) + rescue RestClient::ExceptionWithResponse => e + e.response + end + + def head(url) + RestClient::Request.execute( + method: :head, + url: url, + verify_ssl: false) + rescue RestClient::ExceptionWithResponse => e + e.response + end + def parse_body(response) JSON.parse(response.body, symbolize_names: true) end diff --git a/qa/qa/support/page/logging.rb b/qa/qa/support/page/logging.rb index e96756642c8..f2cd0194b6b 100644 --- a/qa/qa/support/page/logging.rb +++ b/qa/qa/support/page/logging.rb @@ -112,6 +112,17 @@ module QA found end + def finished_loading? + log('waiting for loading to complete...') + now = Time.now + + loaded = super + + log("loading complete after #{Time.now - now} seconds") + + loaded + end + def within_element(name) log("within element :#{name}") diff --git a/qa/qa/tools/delete_subgroups.rb b/qa/qa/tools/delete_subgroups.rb new file mode 100644 index 00000000000..c5c48e77ade --- /dev/null +++ b/qa/qa/tools/delete_subgroups.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true + +require_relative '../../qa' + +# This script deletes all subgroups of a group specified by ENV['GROUP_NAME_OR_PATH'] +# Required environment variables: PERSONAL_ACCESS_TOKEN and GITLAB_ADDRESS +# Optional environment variable: GROUP_NAME_OR_PATH (defaults to 'gitlab-qa-sandbox-group') +# Run `rake delete_subgroups` + +module QA + module Tools + class DeleteSubgroups + include Support::Api + + def initialize + raise ArgumentError, "Please provide GITLAB_ADDRESS" unless ENV['GITLAB_ADDRESS'] + raise ArgumentError, "Please provide PERSONAL_ACCESS_TOKEN" unless ENV['PERSONAL_ACCESS_TOKEN'] + + @api_client = Runtime::API::Client.new(ENV['GITLAB_ADDRESS'], personal_access_token: ENV['PERSONAL_ACCESS_TOKEN']) + end + + def run + STDOUT.puts 'Running...' + + # Fetch group's id + group_id = fetch_group_id + + sub_groups_head_response = head Runtime::API::Request.new(@api_client, "/groups/#{group_id}/subgroups", per_page: "100").url + total_sub_groups = sub_groups_head_response.headers[:x_total] + total_sub_group_pages = sub_groups_head_response.headers[:x_total_pages] + + STDOUT.puts "total_sub_groups: #{total_sub_groups}" + STDOUT.puts "total_sub_group_pages: #{total_sub_group_pages}" + + total_sub_group_pages.to_i.times do |page_no| + # Fetch all subgroups for the top level group + sub_groups_response = get Runtime::API::Request.new(@api_client, "/groups/#{group_id}/subgroups", per_page: "100").url + + sub_group_ids = JSON.parse(sub_groups_response.body).map { |subgroup| subgroup["id"] } + + if sub_group_ids.any? + STDOUT.puts "\n==== Current Page: #{page_no + 1} ====\n" + + delete_subgroups(sub_group_ids) + end + end + STDOUT.puts "\nDone" + end + + private + + def delete_subgroups(sub_group_ids) + sub_group_ids.each do |subgroup_id| + delete_response = delete Runtime::API::Request.new(@api_client, "/groups/#{subgroup_id}").url + dot_or_f = delete_response.code == 202 ? "\e[32m.\e[0m" : "\e[31mF\e[0m" + print dot_or_f + end + end + + def fetch_group_id + group_search_response = get Runtime::API::Request.new(@api_client, "/groups", search: ENV['GROUP_NAME_OR_PATH'] || 'gitlab-qa-sandbox-group').url + JSON.parse(group_search_response.body).first["id"] + 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/qa/vendor/github/page/base.rb b/qa/qa/vendor/github/page/base.rb new file mode 100644 index 00000000000..3b96180afe9 --- /dev/null +++ b/qa/qa/vendor/github/page/base.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module QA + module Vendor + module Github + module Page + class Base + include Capybara::DSL + include Scenario::Actable + end + end + end + end +end diff --git a/qa/qa/vendor/github/page/login.rb b/qa/qa/vendor/github/page/login.rb new file mode 100644 index 00000000000..6d8f9aa7c12 --- /dev/null +++ b/qa/qa/vendor/github/page/login.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require 'capybara/dsl' + +module QA + module Vendor + module Github + module Page + class Login < Page::Base + def login + fill_in 'login', with: QA::Runtime::Env.github_username + fill_in 'password', with: QA::Runtime::Env.github_password + click_on 'Sign in' + + unless has_no_text?("Authorize GitLab-OAuth") + click_on 'Authorize gitlab-qa' if has_button?('Authorize gitlab-qa') + end + end + end + end + end + end +end diff --git a/qa/spec/git/repository_spec.rb b/qa/spec/git/repository_spec.rb index faa154c78da..4a350cd6c42 100644 --- a/qa/spec/git/repository_spec.rb +++ b/qa/spec/git/repository_spec.rb @@ -1,69 +1,119 @@ describe QA::Git::Repository do include Support::StubENV - let(:repository) { described_class.new } + shared_context 'git directory' do + let(:repository) { described_class.new } + let(:tmp_git_dir) { Dir.mktmpdir } + let(:tmp_netrc_dir) { Dir.mktmpdir } - before do - stub_env('GITLAB_USERNAME', 'root') - cd_empty_temp_directory - set_bad_uri - repository.use_default_credentials - end + before do + stub_env('GITLAB_USERNAME', 'root') + cd_empty_temp_directory + set_bad_uri - describe '#clone' do - it 'is unable to resolve host' do - expect(repository.clone).to include("fatal: unable to access 'http://root@foo/bar.git/'") + allow(repository).to receive(:tmp_home_dir).and_return(tmp_netrc_dir) end - end - describe '#push_changes' do - before do - `git init` # need a repo to push from + after do + # Switch to a safe dir before deleting tmp dirs to avoid dir access errors + FileUtils.cd __dir__ + FileUtils.remove_entry_secure(tmp_git_dir, true) + FileUtils.remove_entry_secure(tmp_netrc_dir, true) end - it 'fails to push changes' do - expect(repository.push_changes).to include("error: failed to push some refs to 'http://root@foo/bar.git'") + def cd_empty_temp_directory + FileUtils.cd tmp_git_dir + end + + def set_bad_uri + repository.uri = 'http://foo/bar.git' end end - describe '#git_protocol=' do - [0, 1, 2].each do |version| - it "configures git to use protocol version #{version}" do - expect(repository).to receive(:run).with("git config protocol.version #{version}") - repository.git_protocol = version + context 'with default credentials' do + include_context 'git directory' do + before do + repository.use_default_credentials end end - it 'raises an error if the version is unsupported' do - expect { repository.git_protocol = 'foo' }.to raise_error(ArgumentError, "Please specify the protocol you would like to use: 0, 1, or 2") + describe '#clone' do + it 'is unable to resolve host' do + expect(repository.clone).to include("fatal: unable to access 'http://root@foo/bar.git/'") + end end - end - describe '#fetch_supported_git_protocol' do - it "reports the detected version" do - expect(repository).to receive(:run).and_return("packet: git< version 2") - expect(repository.fetch_supported_git_protocol).to eq('2') + describe '#push_changes' do + before do + `git init` # need a repo to push from + end + + it 'fails to push changes' do + expect(repository.push_changes).to include("error: failed to push some refs to 'http://root@foo/bar.git'") + end end - it 'reports unknown if version is unknown' do - expect(repository).to receive(:run).and_return("packet: git< version -1") - expect(repository.fetch_supported_git_protocol).to eq('unknown') + describe '#git_protocol=' do + [0, 1, 2].each do |version| + it "configures git to use protocol version #{version}" do + expect(repository).to receive(:run).with("git config protocol.version #{version}") + repository.git_protocol = version + end + end + + it 'raises an error if the version is unsupported' do + expect { repository.git_protocol = 'foo' }.to raise_error(ArgumentError, "Please specify the protocol you would like to use: 0, 1, or 2") + end end - it 'reports unknown if content does not identify a version' do - expect(repository).to receive(:run).and_return("foo") - expect(repository.fetch_supported_git_protocol).to eq('unknown') + describe '#fetch_supported_git_protocol' do + it "reports the detected version" do + expect(repository).to receive(:run).and_return("packet: git< version 2") + expect(repository.fetch_supported_git_protocol).to eq('2') + end + + it 'reports unknown if version is unknown' do + expect(repository).to receive(:run).and_return("packet: git< version -1") + expect(repository.fetch_supported_git_protocol).to eq('unknown') + end + + it 'reports unknown if content does not identify a version' do + expect(repository).to receive(:run).and_return("foo") + expect(repository.fetch_supported_git_protocol).to eq('unknown') + end end - end - def cd_empty_temp_directory - tmp_dir = 'tmp/git-repository-spec/' - FileUtils.rm_rf(tmp_dir) if ::File.exist?(tmp_dir) - FileUtils.mkdir_p tmp_dir - FileUtils.cd tmp_dir + describe '#use_default_credentials' do + it 'adds credentials to .netrc' do + expect(File.read(File.join(tmp_netrc_dir, '.netrc'))) + .to eq("machine foo login #{QA::Runtime::User.default_username} password #{QA::Runtime::User.default_password}\n") + end + end end - def set_bad_uri - repository.uri = 'http://foo/bar.git' + context 'with specific credentials' do + include_context 'git directory' + + context 'before setting credentials' do + it 'does not add credentials to .netrc' do + expect(repository).not_to receive(:save_netrc_content) + end + end + + describe '#password=' do + it 'raises an error if no username was given' do + expect { repository.password = 'foo' } + .to raise_error(QA::Git::Repository::InvalidCredentialsError, + "Please provide a username when setting a password") + end + + it 'adds credentials to .netrc' do + repository.username = 'user' + repository.password = 'foo' + + expect(File.read(File.join(tmp_netrc_dir, '.netrc'))) + .to eq("machine foo login user password foo\n") + end + end end end diff --git a/qa/spec/page/logging_spec.rb b/qa/spec/page/logging_spec.rb index 2eb826becea..f289ee3c2bb 100644 --- a/qa/spec/page/logging_spec.rb +++ b/qa/spec/page/logging_spec.rb @@ -1,14 +1,15 @@ # frozen_string_literal: true require 'capybara/dsl' +require 'logger' describe QA::Support::Page::Logging do include Support::StubENV - let(:page) { double().as_null_object } + let(:page) { double.as_null_object } before do - logger = Logger.new $stdout + logger = ::Logger.new $stdout logger.level = ::Logger::DEBUG QA::Runtime::Logger.logger = logger @@ -95,6 +96,13 @@ describe QA::Support::Page::Logging do .to output(/has_no_text\?\('foo'\) returned true/).to_stdout_from_any_process end + it 'logs finished_loading?' do + expect { subject.finished_loading? } + .to output(/waiting for loading to complete\.\.\./).to_stdout_from_any_process + expect { subject.finished_loading? } + .to output(/loading complete after .* seconds$/).to_stdout_from_any_process + end + it 'logs within_element' do expect { subject.within_element(:element) } .to output(/within element :element/).to_stdout_from_any_process 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/qa/spec/resource/user_spec.rb b/qa/spec/resource/user_spec.rb new file mode 100644 index 00000000000..d612dfc530e --- /dev/null +++ b/qa/spec/resource/user_spec.rb @@ -0,0 +1,118 @@ +# frozen_string_literal: true + +describe QA::Resource::User do + let(:api_resource) do + { + name: "GitLab QA", + username: "gitlab-qa", + web_url: "https://staging.gitlab.com/gitlab-qa", + public_email: "1614863-gitlab-qa@users.noreply.staging.gitlab.com" + } + end + + describe '#username' do + it 'generates a default username' do + expect(subject.username).to match(/qa-user-\w+/) + end + + it 'is possible to set the username' do + subject.username = 'johndoe' + + expect(subject.username).to eq('johndoe') + end + end + + describe '#password' do + it 'generates a default password' do + expect(subject.password).to eq('password') + end + + it 'is possible to set the password' do + subject.password = 'secret' + + expect(subject.password).to eq('secret') + end + end + + describe '#name' do + it 'defaults to the username' do + expect(subject.name).to eq(subject.username) + end + + it 'retrieves the name from the api_resource if present' do + subject.__send__(:api_resource=, api_resource) + + expect(subject.name).to eq(api_resource[:name]) + end + + it 'is possible to set the name' do + subject.name = 'John Doe' + + expect(subject.name).to eq('John Doe') + end + end + + describe '#email' do + it 'defaults to the <username>@example.com' do + expect(subject.email).to eq("#{subject.username}@example.com") + end + + it 'is possible to set the email' do + subject.email = 'johndoe@example.org' + + expect(subject.email).to eq('johndoe@example.org') + end + end + + describe '#public_email' do + it 'defaults to QA::Runtime::User.default_email' do + expect(subject.public_email).to eq(QA::Runtime::User.default_email) + end + + it 'retrieves the public_email from the api_resource if present' do + subject.__send__(:api_resource=, api_resource) + + expect(subject.public_email).to eq(api_resource[:public_email]) + end + + it 'defaults to QA::Runtime::User.default_email if the public_email from the api_resource is blank' do + subject.__send__(:api_resource=, api_resource.merge(public_email: '')) + + expect(subject.public_email).to eq(QA::Runtime::User.default_email) + end + end + + describe '#credentials_given?' do + it 'returns false when username and email have not been overridden' do + expect(subject).not_to be_credentials_given + end + + it 'returns false even after username and email have been called' do + # Call #username and #password to ensure this doesn't set their respective + # instance variable. + subject.username + subject.password + + expect(subject).not_to be_credentials_given + end + + it 'returns false if only the username has been overridden' do + subject.username = 'johndoe' + + expect(subject).not_to be_credentials_given + end + + it 'returns false if only the password has been overridden' do + subject.password = 'secret' + + expect(subject).not_to be_credentials_given + end + + it 'returns true if both the username and password have been overridden' do + subject.username = 'johndoe' + subject.password = 'secret' + + expect(subject).to be_credentials_given + end + end +end diff --git a/qa/spec/scenario/test/integration/oauth_spec.rb b/qa/spec/scenario/test/integration/oauth_spec.rb new file mode 100644 index 00000000000..c1c320be576 --- /dev/null +++ b/qa/spec/scenario/test/integration/oauth_spec.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +describe QA::Scenario::Test::Integration::OAuth do + context '#perform' do + it_behaves_like 'a QA scenario class' do + let(:tags) { [:oauth] } + end + end +end diff --git a/qa/spec/support/stub_env.rb b/qa/spec/support/stub_env.rb index 044804cd599..4788e0ab46c 100644 --- a/qa/spec/support/stub_env.rb +++ b/qa/spec/support/stub_env.rb @@ -19,7 +19,7 @@ module Support allow(ENV).to receive(:[]).with(key).and_return(value) allow(ENV).to receive(:key?).with(key).and_return(true) allow(ENV).to receive(:fetch).with(key).and_return(value) - allow(ENV).to receive(:fetch).with(key, anything()) do |_, default_val| + allow(ENV).to receive(:fetch).with(key, anything) do |_, default_val| value || default_val end end 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..6e0dee9e090 100755 --- a/scripts/review_apps/review-apps.sh +++ b/scripts/review_apps/review-apps.sh @@ -2,6 +2,7 @@ export TILLER_NAMESPACE="$KUBE_NAMESPACE" function echoerr() { printf "\033[0;31m%s\n\033[0m" "$*" >&2; } +function echoinfo() { printf "\033[0;33m%s\n\033[0m" "$*" >&2; } function check_kube_domain() { if [ -z ${REVIEW_APPS_DOMAIN+x} ]; then @@ -151,19 +152,19 @@ HELM_CMD=$(cat << EOF --set redis.resources.requests.cpu=100m \ --set minio.resources.requests.cpu=100m \ --set gitlab.migrations.image.repository="$gitlab_migrations_image_repository" \ - --set gitlab.migrations.image.tag="$CI_COMMIT_REF_NAME" \ + --set gitlab.migrations.image.tag="$CI_COMMIT_REF_SLUG" \ --set gitlab.sidekiq.image.repository="$gitlab_sidekiq_image_repository" \ - --set gitlab.sidekiq.image.tag="$CI_COMMIT_REF_NAME" \ + --set gitlab.sidekiq.image.tag="$CI_COMMIT_REF_SLUG" \ --set gitlab.unicorn.image.repository="$gitlab_unicorn_image_repository" \ - --set gitlab.unicorn.image.tag="$CI_COMMIT_REF_NAME" \ + --set gitlab.unicorn.image.tag="$CI_COMMIT_REF_SLUG" \ --set gitlab.task-runner.image.repository="$gitlab_task_runner_image_repository" \ - --set gitlab.task-runner.image.tag="$CI_COMMIT_REF_NAME" \ + --set gitlab.task-runner.image.tag="$CI_COMMIT_REF_SLUG" \ --set gitlab.gitaly.image.repository="registry.gitlab.com/gitlab-org/build/cng-mirror/gitaly" \ --set gitlab.gitaly.image.tag="v$GITALY_VERSION" \ --set gitlab.gitlab-shell.image.repository="registry.gitlab.com/gitlab-org/build/cng-mirror/gitlab-shell" \ --set gitlab.gitlab-shell.image.tag="v$GITLAB_SHELL_VERSION" \ --set gitlab.unicorn.workhorse.image="$gitlab_workhorse_image_repository" \ - --set gitlab.unicorn.workhorse.tag="$CI_COMMIT_REF_NAME" \ + --set gitlab.unicorn.workhorse.tag="$CI_COMMIT_REF_SLUG" \ --set nginx-ingress.controller.config.ssl-ciphers="ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-SHA256:AES128-SHA256:AES256-SHA:AES128-SHA:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4" \ --namespace="$KUBE_NAMESPACE" \ --version="$CI_PIPELINE_ID-$CI_JOB_ID" \ @@ -238,17 +239,17 @@ function get_pod() { local app_name="${1}" local status="${2-Running}" get_pod_cmd="kubectl get pods -n ${KUBE_NAMESPACE} --field-selector=status.phase=${status} -lapp=${app_name},release=${CI_ENVIRONMENT_SLUG} --no-headers -o=custom-columns=NAME:.metadata.name" - echoerr "Running '${get_pod_cmd}'" + echoinfo "Running '${get_pod_cmd}'" while true; do local pod_name="$(eval $get_pod_cmd)" [[ "${pod_name}" == "" ]] || break - echoerr "Waiting till '${app_name}' pod is ready"; + echoinfo "Waiting till '${app_name}' pod is ready"; sleep 5; done - echoerr "The pod name is '${pod_name}'." + echoinfo "The pod name is '${pod_name}'." echo "${pod_name}" } @@ -290,7 +291,7 @@ function get_job_id() { while true; do local url="https://gitlab.com/api/v4/projects/${CI_PROJECT_ID}/pipelines/${CI_PIPELINE_ID}/jobs?per_page=100&page=${page}${query_string}" - echoerr "GET ${url}" + echoinfo "GET ${url}" local job_id=$(curl --silent --show-error --header "PRIVATE-TOKEN: ${API_TOKEN}" "${url}" | jq "map(select(.name == \"${job_name}\")) | map(.id) | last") [[ "${job_id}" == "null" && "${page}" -lt "$max_page" ]] || break @@ -301,7 +302,7 @@ function get_job_id() { if [[ "${job_id}" == "" ]]; then echoerr "The '${job_name}' job ID couldn't be retrieved!" else - echoerr "The '${job_name}' job ID is ${job_id}" + echoinfo "The '${job_name}' job ID is ${job_id}" echo "${job_id}" fi } @@ -312,10 +313,10 @@ function play_job() { if [ -z "${job_id}" ]; then return; fi local url="https://gitlab.com/api/v4/projects/${CI_PROJECT_ID}/jobs/${job_id}/play" - echoerr "POST ${url}" + echoinfo "POST ${url}" local job_url=$(curl --silent --show-error --request POST --header "PRIVATE-TOKEN: ${API_TOKEN}" "${url}" | jq ".web_url") - echo "Manual job '${job_name}' started at: ${job_url}" + echoinfo "Manual job '${job_name}' started at: ${job_url}" } function wait_for_job_to_be_done() { @@ -324,10 +325,10 @@ function wait_for_job_to_be_done() { local job_id=$(get_job_id "${job_name}" "${query_string}"); if [ -z "${job_id}" ]; then return; fi - echoerr "Waiting for the '${job_name}' job to finish..." + echoinfo "Waiting for the '${job_name}' job to finish..." local url="https://gitlab.com/api/v4/projects/${CI_PROJECT_ID}/jobs/${job_id}" - echo "GET ${url}" + echoinfo "GET ${url}" # In case the job hasn't finished yet. Keep trying until the job times out. local interval=30 @@ -342,14 +343,13 @@ function wait_for_job_to_be_done() { done local elapsed_minutes=$((elapsed_seconds / 60)) - echoerr "Waited '${job_name}' for ${elapsed_minutes} minutes." + echoinfo "Waited '${job_name}' for ${elapsed_minutes} minutes." if [[ "${job_status}" == "failed" ]]; then - echo "The '${job_name}' failed." - exit 1 + echoerr "The '${job_name}' failed." elif [[ "${job_status}" == "manual" ]]; then - echo "The '${job_name}' is manual." + echoinfo "The '${job_name}' is manual." else - echo "The '${job_name}' passed." + echoinfo "The '${job_name}' passed." fi } diff --git a/scripts/trigger-build b/scripts/trigger-build index 4032ba853e6..9dbafffddfc 100755 --- a/scripts/trigger-build +++ b/scripts/trigger-build @@ -68,7 +68,7 @@ module Trigger def base_variables { - 'GITLAB_REF_SLUG' => ENV['CI_COMMIT_REF_SLUG'], + 'GITLAB_REF_SLUG' => ENV['CI_COMMIT_TAG'] ? ENV['CI_COMMIT_REF_NAME'] : ENV['CI_COMMIT_REF_SLUG'], 'TRIGGERED_USER' => ENV['TRIGGERED_USER'] || ENV['GITLAB_USER_NAME'], 'TRIGGER_SOURCE' => ENV['CI_JOB_URL'], 'TOP_UPSTREAM_SOURCE_PROJECT' => ENV['CI_PROJECT_PATH'], @@ -137,7 +137,11 @@ module Trigger edition = Trigger.ee? ? 'EE' : 'CE' { + # Back-compatibility until https://gitlab.com/gitlab-org/build/CNG/merge_requests/189 is merged "GITLAB_#{edition}_VERSION" => ENV['CI_COMMIT_REF_NAME'], + "GITLAB_VERSION" => ENV['CI_COMMIT_REF_NAME'], + "GITLAB_TAG" => ENV['CI_COMMIT_TAG'], + "GITLAB_ASSETS_TAG" => ENV['CI_COMMIT_REF_SLUG'], "#{edition}_PIPELINE" => 'true' } end diff --git a/spec/bin/changelog_spec.rb b/spec/bin/changelog_spec.rb index c59add88a82..b2c2fb810e8 100644 --- a/spec/bin/changelog_spec.rb +++ b/spec/bin/changelog_spec.rb @@ -80,7 +80,7 @@ describe 'bin/changelog' do end end - describe '.read_type' do + describe '.read_type' do let(:type) { '1' } it 'reads type from $stdin' do diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb index 43f561f7a25..c9e520317e8 100644 --- a/spec/controllers/application_controller_spec.rb +++ b/spec/controllers/application_controller_spec.rb @@ -423,7 +423,7 @@ describe ApplicationController do enforce_terms end - it 'redirects if the user did not accept the terms' do + it 'redirects if the user did not accept the terms' do get :index expect(response).to have_gitlab_http_status(302) @@ -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/boards/issues_controller_spec.rb b/spec/controllers/boards/issues_controller_spec.rb index 8657fc2ebc0..5eb05f01b8d 100644 --- a/spec/controllers/boards/issues_controller_spec.rb +++ b/spec/controllers/boards/issues_controller_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe Boards::IssuesController do - let(:project) { create(:project) } + let(:project) { create(:project, :private) } let(:board) { create(:board, project: project) } let(:user) { create(:user) } let(:guest) { create(:user) } @@ -127,14 +127,10 @@ describe Boards::IssuesController do end context 'with unauthorized user' do - before do - allow(Ability).to receive(:allowed?).and_call_original - allow(Ability).to receive(:allowed?).with(user, :read_project, project).and_return(true) - allow(Ability).to receive(:allowed?).with(user, :read_issue, project).and_return(false) - end + let(:unauth_user) { create(:user) } it 'returns a forbidden 403 response' do - list_issues user: user, board: board, list: list2 + list_issues user: unauth_user, board: board, list: list2 expect(response).to have_gitlab_http_status(403) end @@ -233,7 +229,7 @@ describe Boards::IssuesController do post :create, params: { board_id: board.to_param, list_id: list.to_param, - issue: { title: title, project_id: project.id } + issue: { title: title, project_id: project.id } }, format: :json end diff --git a/spec/controllers/boards/lists_controller_spec.rb b/spec/controllers/boards/lists_controller_spec.rb index 70033857168..e5b8aa2e678 100644 --- a/spec/controllers/boards/lists_controller_spec.rb +++ b/spec/controllers/boards/lists_controller_spec.rb @@ -31,13 +31,10 @@ describe Boards::ListsController do end context 'with unauthorized user' do - before do - allow(Ability).to receive(:allowed?).with(user, :read_project, project).and_return(true) - allow(Ability).to receive(:allowed?).with(user, :read_list, project).and_return(false) - end + let(:unauth_user) { create(:user) } it 'returns a forbidden 403 response' do - read_board_list user: user, board: board + read_board_list user: unauth_user, board: board expect(response).to have_gitlab_http_status(403) end diff --git a/spec/controllers/concerns/checks_collaboration_spec.rb b/spec/controllers/concerns/checks_collaboration_spec.rb index 1bd764290ae..d7f110e11f3 100644 --- a/spec/controllers/concerns/checks_collaboration_spec.rb +++ b/spec/controllers/concerns/checks_collaboration_spec.rb @@ -22,7 +22,7 @@ describe ChecksCollaboration do end end - it 'is true if the user can push to the project' do + it 'is true if the user can push to the project' do project.add_developer(user) expect(helper.can_collaborate_with_project?(project)).to be_truthy diff --git a/spec/controllers/concerns/issuable_collections_spec.rb b/spec/controllers/concerns/issuable_collections_spec.rb index 5a3a7a15f5a..307c5d60c57 100644 --- a/spec/controllers/concerns/issuable_collections_spec.rb +++ b/spec/controllers/concerns/issuable_collections_spec.rb @@ -17,10 +17,55 @@ describe IssuableCollections do controller = klass.new allow(controller).to receive(:params).and_return(ActionController::Parameters.new(params)) + allow(controller).to receive(:current_user).and_return(user) controller end + describe '#set_sort_order_from_user_preference' do + describe 'when sort param given' do + let(:params) { { sort: 'updated_desc' } } + + context 'when issuable_sorting_field is defined' do + before do + controller.class.define_method(:issuable_sorting_field) { :issues_sort} + end + + it 'sets user_preference with the right value' do + controller.send(:set_sort_order_from_user_preference) + + expect(user.user_preference.reload.issues_sort).to eq('updated_desc') + end + end + + context 'when no issuable_sorting_field is defined on the controller' do + it 'does not touch user_preference' do + allow(user).to receive(:user_preference) + + controller.send(:set_sort_order_from_user_preference) + + expect(user).not_to have_received(:user_preference) + end + end + end + + context 'when a user sorting preference exists' do + let(:params) { {} } + + before do + controller.class.define_method(:issuable_sorting_field) { :issues_sort } + end + + it 'returns the set preference' do + user.user_preference.update(issues_sort: 'updated_asc') + + sort_preference = controller.send(:set_sort_order_from_user_preference) + + expect(sort_preference).to eq('updated_asc') + end + end + end + describe '#set_set_order_from_cookie' do describe 'when sort param given' do let(:cookies) { {} } diff --git a/spec/controllers/dashboard/milestones_controller_spec.rb b/spec/controllers/dashboard/milestones_controller_spec.rb index c9ccd5f7c55..8b176e07bc8 100644 --- a/spec/controllers/dashboard/milestones_controller_spec.rb +++ b/spec/controllers/dashboard/milestones_controller_spec.rb @@ -56,6 +56,24 @@ describe Dashboard::MilestonesController do expect(json_response.map { |i| i["group_name"] }.compact).to match_array(group.name) end + it 'searches legacy project milestones by title when search_title is given' do + project_milestone = create(:milestone, title: 'Project milestone title', project: project) + + get :index, params: { search_title: 'Project mil' } + + expect(response.body).to include(project_milestone.title) + expect(response.body).not_to include(group_milestone.title) + end + + it 'searches group milestones by title when search_title is given' do + group_milestone = create(:milestone, title: 'Group milestone title', group: group) + + get :index, params: { search_title: 'Group mil' } + + expect(response.body).to include(group_milestone.title) + expect(response.body).not_to include(project_milestone.title) + end + it 'should contain group and project milestones to which the user belongs to' do get :index diff --git a/spec/controllers/groups/children_controller_spec.rb b/spec/controllers/groups/children_controller_spec.rb index 4d5bb1488ab..e1b97013408 100644 --- a/spec/controllers/groups/children_controller_spec.rb +++ b/spec/controllers/groups/children_controller_spec.rb @@ -110,7 +110,7 @@ describe Groups::ChildrenController do matched_project_1 = create(:project, :public, namespace: shared_subgroup, name: 'mobile-soc') l2_subgroup = create(:group, :public, parent: shared_subgroup, path: 'broadcom') - l3_subgroup = create(:group, :public, parent: l2_subgroup, path: 'wifi-group') + l3_subgroup = create(:group, :public, parent: l2_subgroup, path: 'wifi-group') matched_project_2 = create(:project, :public, namespace: l3_subgroup, name: 'mobile') get :index, params: { group_id: group.to_param, filter: 'mobile' }, format: :json @@ -289,7 +289,7 @@ describe Groups::ChildrenController do end context 'with subgroups and projects', :nested_groups do - let!(:first_page_subgroups) { create_list(:group, per_page, :public, parent: group) } + let!(:first_page_subgroups) { create_list(:group, per_page, :public, parent: group) } let!(:other_subgroup) { create(:group, :public, parent: group) } let!(:next_page_projects) { create_list(:project, per_page, :public, namespace: group) } @@ -306,7 +306,7 @@ describe Groups::ChildrenController do end context 'with a mixed first page' do - let!(:first_page_subgroups) { [create(:group, :public, parent: group)] } + let!(:first_page_subgroups) { [create(:group, :public, parent: group)] } let!(:first_page_projects) { create_list(:project, per_page, :public, namespace: group) } it 'correctly calculates the counts' do diff --git a/spec/controllers/groups/group_members_controller_spec.rb b/spec/controllers/groups/group_members_controller_spec.rb index ed38dadfd6b..3a801fabafc 100644 --- a/spec/controllers/groups/group_members_controller_spec.rb +++ b/spec/controllers/groups/group_members_controller_spec.rb @@ -126,7 +126,7 @@ describe Groups::GroupMembersController do it '[HTML] removes user from members' do delete :destroy, params: { group_id: group, id: member } - expect(response).to set_flash.to 'User was successfully removed from group.' + expect(response).to set_flash.to 'User was successfully removed from group and any subresources.' expect(response).to redirect_to(group_group_members_path(group)) expect(group.members).not_to include member end diff --git a/spec/controllers/groups/milestones_controller_spec.rb b/spec/controllers/groups/milestones_controller_spec.rb index 40d991a669c..043cf28514b 100644 --- a/spec/controllers/groups/milestones_controller_spec.rb +++ b/spec/controllers/groups/milestones_controller_spec.rb @@ -32,10 +32,35 @@ describe Groups::MilestonesController do end describe '#index' do - it 'shows group milestones page' do - get :index, params: { group_id: group.to_param } + describe 'as HTML' do + render_views - expect(response).to have_gitlab_http_status(200) + it 'shows group milestones page' do + milestone + + get :index, params: { group_id: group.to_param } + + expect(response).to have_gitlab_http_status(200) + expect(response.body).to include(milestone.title) + end + + it 'searches legacy milestones by title when search_title is given' do + project_milestone = create(:milestone, project: project, title: 'Project milestone title') + + get :index, params: { group_id: group.to_param, search_title: 'Project mil' } + + expect(response.body).to include(project_milestone.title) + expect(response.body).not_to include(milestone.title) + end + + it 'searches group milestones by title when search_title is given' do + group_milestone = create(:milestone, title: 'Group milestone title', group: group) + + get :index, params: { group_id: group.to_param, search_title: 'Group mil' } + + expect(response.body).to include(group_milestone.title) + expect(response.body).not_to include(milestone.title) + end end context 'as JSON' do diff --git a/spec/controllers/import/bitbucket_controller_spec.rb b/spec/controllers/import/bitbucket_controller_spec.rb index 51793f2c048..0bc09c86939 100644 --- a/spec/controllers/import/bitbucket_controller_spec.rb +++ b/spec/controllers/import/bitbucket_controller_spec.rb @@ -8,6 +8,7 @@ describe Import::BitbucketController do let(:secret) { "sekrettt" } let(:refresh_token) { SecureRandom.hex(15) } let(:access_params) { { token: token, expires_at: nil, expires_in: nil, refresh_token: nil } } + let(:code) { SecureRandom.hex(8) } def assign_session_tokens session[:bitbucket_token] = token @@ -32,10 +33,16 @@ describe Import::BitbucketController do expires_in: expires_in, refresh_token: refresh_token) allow_any_instance_of(OAuth2::Client) - .to receive(:get_token).and_return(access_token) + .to receive(:get_token) + .with(hash_including( + 'grant_type' => 'authorization_code', + 'code' => code, + redirect_uri: users_import_bitbucket_callback_url), + {}) + .and_return(access_token) stub_omniauth_provider('bitbucket') - get :callback + get :callback, params: { code: code } expect(session[:bitbucket_token]).to eq(token) expect(session[:bitbucket_refresh_token]).to eq(refresh_token) diff --git a/spec/controllers/import/bitbucket_server_controller_spec.rb b/spec/controllers/import/bitbucket_server_controller_spec.rb index bb282db5a41..a125e6ed16d 100644 --- a/spec/controllers/import/bitbucket_server_controller_spec.rb +++ b/spec/controllers/import/bitbucket_server_controller_spec.rb @@ -28,9 +28,11 @@ describe Import::BitbucketServerController do end describe 'POST create' do + let(:project_name) { "my-project_123" } + before do allow(controller).to receive(:bitbucket_client).and_return(client) - repo = double(name: 'my-project') + repo = double(name: project_name) allow(client).to receive(:repo).with(project_key, repo_slug).and_return(repo) assign_session_tokens end @@ -39,7 +41,7 @@ describe Import::BitbucketServerController do it 'returns the new project' do allow(Gitlab::BitbucketServerImport::ProjectCreator) - .to receive(:new).with(project_key, repo_slug, anything, 'my-project', user.namespace, user, anything) + .to receive(:new).with(project_key, repo_slug, anything, project_name, user.namespace, user, anything) .and_return(double(execute: project)) post :create, params: { project: project_key, repository: repo_slug }, format: :json @@ -47,6 +49,20 @@ describe Import::BitbucketServerController do expect(response).to have_gitlab_http_status(200) end + context 'with project key with tildes' do + let(:project_key) { '~someuser_123' } + + it 'successfully creates a project' do + allow(Gitlab::BitbucketServerImport::ProjectCreator) + .to receive(:new).with(project_key, repo_slug, anything, project_name, user.namespace, user, anything) + .and_return(double(execute: project)) + + post :create, params: { project: project_key, repository: repo_slug, format: :json } + + expect(response).to have_gitlab_http_status(200) + end + end + it 'returns an error when an invalid project key is used' do post :create, params: { project: 'some&project' } @@ -69,7 +85,7 @@ describe Import::BitbucketServerController do it 'returns an error when the project cannot be saved' do allow(Gitlab::BitbucketServerImport::ProjectCreator) - .to receive(:new).with(project_key, repo_slug, anything, 'my-project', user.namespace, user, anything) + .to receive(:new).with(project_key, repo_slug, anything, project_name, user.namespace, user, anything) .and_return(double(execute: build(:project))) post :create, params: { project: project_key, repository: repo_slug }, format: :json diff --git a/spec/controllers/import/github_controller_spec.rb b/spec/controllers/import/github_controller_spec.rb index 780e49f7b93..bca5f3f6589 100644 --- a/spec/controllers/import/github_controller_spec.rb +++ b/spec/controllers/import/github_controller_spec.rb @@ -12,9 +12,15 @@ describe Import::GithubController do it "redirects to GitHub for an access token if logged in with GitHub" do allow(controller).to receive(:logged_in_with_provider?).and_return(true) - expect(controller).to receive(:go_to_provider_for_permissions) + expect(controller).to receive(:go_to_provider_for_permissions).and_call_original + allow_any_instance_of(Gitlab::LegacyGithubImport::Client) + .to receive(:authorize_url) + .with(users_import_github_callback_url) + .and_call_original get :new + + expect(response).to have_http_status(302) end it "prompts for an access token if GitHub not configured" do diff --git a/spec/controllers/profiles/avatars_controller_spec.rb b/spec/controllers/profiles/avatars_controller_spec.rb index 909709e1103..1ee0bf44e92 100644 --- a/spec/controllers/profiles/avatars_controller_spec.rb +++ b/spec/controllers/profiles/avatars_controller_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe Profiles::AvatarsController do - let(:user) { create(:user, avatar: fixture_file_upload("spec/fixtures/dk.png")) } + let(:user) { create(:user, avatar: fixture_file_upload("spec/fixtures/dk.png")) } before do sign_in(user) diff --git a/spec/controllers/projects/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/branches_controller_spec.rb b/spec/controllers/projects/branches_controller_spec.rb index 02b3d5269a6..52a20fa8d07 100644 --- a/spec/controllers/projects/branches_controller_spec.rb +++ b/spec/controllers/projects/branches_controller_spec.rb @@ -331,7 +331,7 @@ describe Projects::BranchesController do let(:branch) { "feature" } it 'returns JSON response with message' do - expect(json_response).to eql("message" => 'Branch was removed') + expect(json_response).to eql("message" => 'Branch was deleted') end it { expect(response).to have_gitlab_http_status(200) } @@ -341,7 +341,7 @@ describe Projects::BranchesController do let(:branch) { "improve/awesome" } it 'returns JSON response with message' do - expect(json_response).to eql('message' => 'Branch was removed') + expect(json_response).to eql('message' => 'Branch was deleted') end it { expect(response).to have_gitlab_http_status(200) } @@ -351,7 +351,7 @@ describe Projects::BranchesController do let(:branch) { 'improve%2Fawesome' } it 'returns JSON response with message' do - expect(json_response).to eql('message' => 'Branch was removed') + expect(json_response).to eql('message' => 'Branch was deleted') end it { expect(response).to have_gitlab_http_status(200) } diff --git a/spec/controllers/projects/commit_controller_spec.rb b/spec/controllers/projects/commit_controller_spec.rb index 26eec90da06..19cac47325c 100644 --- a/spec/controllers/projects/commit_controller_spec.rb +++ b/spec/controllers/projects/commit_controller_spec.rb @@ -5,7 +5,7 @@ describe Projects::CommitController do set(:user) { create(:user) } let(:commit) { project.commit("master") } let(:master_pickable_sha) { '7d3b0f7cff5f37573aea97cebfd5692ea1689924' } - let(:master_pickable_commit) { project.commit(master_pickable_sha) } + let(:master_pickable_commit) { project.commit(master_pickable_sha) } before do sign_in(user) diff --git a/spec/controllers/projects/environments_controller_spec.rb b/spec/controllers/projects/environments_controller_spec.rb index 94fb85f217c..a4d494a820f 100644 --- a/spec/controllers/projects/environments_controller_spec.rb +++ b/spec/controllers/projects/environments_controller_spec.rb @@ -47,9 +47,43 @@ describe Projects::EnvironmentsController do let(:environments) { json_response['environments'] } + context 'with default parameters' do + before do + get :index, params: environment_params(format: :json) + end + + it 'responds with a flat payload describing available environments' do + expect(environments.count).to eq 3 + expect(environments.first['name']).to eq 'production' + expect(environments.second['name']).to eq 'staging/review-1' + expect(environments.third['name']).to eq 'staging/review-2' + expect(json_response['available_count']).to eq 3 + expect(json_response['stopped_count']).to eq 1 + end + + it 'sets the polling interval header' do + expect(response).to have_gitlab_http_status(:ok) + expect(response.headers['Poll-Interval']).to eq("3000") + end + end + + context 'when a folder-based nested structure is requested' do + before do + get :index, params: environment_params(format: :json, nested: true) + end + + it 'responds with a payload containing the latest environment for each folder' do + expect(environments.count).to eq 2 + expect(environments.first['name']).to eq 'production' + expect(environments.second['name']).to eq 'staging' + expect(environments.second['size']).to eq 2 + expect(environments.second['latest']['name']).to eq 'staging/review-2' + end + end + context 'when requesting available environments scope' do before do - get :index, params: environment_params(format: :json, scope: :available) + get :index, params: environment_params(format: :json, nested: true, scope: :available) end it 'responds with a payload describing available environments' do @@ -64,16 +98,11 @@ describe Projects::EnvironmentsController do expect(json_response['available_count']).to eq 3 expect(json_response['stopped_count']).to eq 1 end - - it 'sets the polling interval header' do - expect(response).to have_gitlab_http_status(:ok) - expect(response.headers['Poll-Interval']).to eq("3000") - end end context 'when requesting stopped environments scope' do before do - get :index, params: environment_params(format: :json, scope: :stopped) + get :index, params: environment_params(format: :json, nested: true, scope: :stopped) end it 'responds with a payload describing stopped environments' do 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 e0b6105bb94..4743ad04339 100644 --- a/spec/controllers/projects/issues_controller_spec.rb +++ b/spec/controllers/projects/issues_controller_spec.rb @@ -42,7 +42,9 @@ describe Projects::IssuesController do it_behaves_like "issuables list meta-data", :issue - it_behaves_like 'set sort order from user preference' + it_behaves_like 'set sort order from user preference' do + let(:sorting_param) { 'updated_asc' } + end it "returns index" do get :index, params: { namespace_id: project.namespace, project_id: project } @@ -66,7 +68,7 @@ describe Projects::IssuesController do end context 'with page param' do - let(:last_page) { project.issues.page().total_pages } + let(:last_page) { project.issues.page.total_pages } let!(:issue_list) { create_list(:issue, 2, project: project) } before do @@ -131,7 +133,7 @@ describe Projects::IssuesController do it 'redirects to signin if not logged in' do get :new, params: { namespace_id: project.namespace, project_id: project } - expect(flash[:notice]).to eq 'Please sign in to create the new issue.' + expect(flash[:alert]).to eq 'You need to sign in or sign up before continuing.' expect(response).to redirect_to(new_user_session_path) end @@ -1030,19 +1032,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) @@ -1131,6 +1120,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/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb index 4f4d3ca226f..ca5ff9b1e3b 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) } @@ -152,10 +153,12 @@ describe Projects::MergeRequestsController do it_behaves_like "issuables list meta-data", :merge_request - it_behaves_like 'set sort order from user preference' + it_behaves_like 'set sort order from user preference' do + let(:sorting_param) { 'updated_asc' } + end context 'when page param' do - let(:last_page) { project.merge_requests.page().total_pages } + let(:last_page) { project.merge_requests.page.total_pages } let!(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: project) } it 'redirects to last_page if page number is larger than number of pages' do @@ -252,8 +255,8 @@ describe Projects::MergeRequestsController do end context 'there is no source project' do - let(:project) { create(:project, :repository) } - let(:forked_project) { fork_project_with_submodules(project) } + let(:project) { create(:project, :repository) } + let(:forked_project) { fork_project_with_submodules(project) } let!(:merge_request) { create(:merge_request, source_project: forked_project, source_branch: 'add-submodule-version-bump', target_branch: 'master', target_project: project) } before do @@ -883,7 +886,7 @@ describe Projects::MergeRequestsController do end describe 'POST #rebase' do - let(:viewer) { user } + let(:viewer) { user } def post_rebase post :rebase, params: { namespace_id: project.namespace, project_id: project, id: merge_request } diff --git a/spec/controllers/projects/milestones_controller_spec.rb b/spec/controllers/projects/milestones_controller_spec.rb index 5892024e756..ac54b3c3952 100644 --- a/spec/controllers/projects/milestones_controller_spec.rb +++ b/spec/controllers/projects/milestones_controller_spec.rb @@ -42,10 +42,11 @@ describe Projects::MilestonesController do describe "#index" do context "as html" do - def render_index(project:, page:) + def render_index(project:, page:, search_title: '') get :index, params: { namespace_id: project.namespace.id, project_id: project.id, + search_title: search_title, page: page } end @@ -59,6 +60,15 @@ describe Projects::MilestonesController do expect(milestones.where(project_id: nil)).to be_empty end + it 'searches milestones by title when search_title is given' do + milestone1 = create(:milestone, title: 'Project milestone title', project: project) + + render_index project: project, page: 1, search_title: 'Project mile' + + milestones = assigns(:milestones) + expect(milestones).to eq([milestone1]) + end + it 'renders paginated milestones without missing or duplicates' do allow(Milestone).to receive(:default_per_page).and_return(2) create_list(:milestone, 5, 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/pipeline_schedules_controller_spec.rb b/spec/controllers/projects/pipeline_schedules_controller_spec.rb index 80506249ea9..fa732437fc1 100644 --- a/spec/controllers/projects/pipeline_schedules_controller_spec.rb +++ b/spec/controllers/projects/pipeline_schedules_controller_spec.rb @@ -3,9 +3,14 @@ require 'spec_helper' describe Projects::PipelineSchedulesController do include AccessMatchersForController + set(:user) { create(:user) } set(:project) { create(:project, :public, :repository) } set(:pipeline_schedule) { create(:ci_pipeline_schedule, project: project) } + before do + project.add_developer(user) + end + describe 'GET #index' do render_views @@ -14,6 +19,10 @@ describe Projects::PipelineSchedulesController do create(:ci_pipeline_schedule, :inactive, project: project) end + before do + sign_in(user) + end + it 'renders the index view' do visit_pipelines_schedules @@ -21,7 +30,7 @@ describe Projects::PipelineSchedulesController do expect(response).to render_template(:index) end - it 'avoids N + 1 queries' do + it 'avoids N + 1 queries', :request_store do control_count = ActiveRecord::QueryRecorder.new { visit_pipelines_schedules }.count create_list(:ci_pipeline_schedule, 2, project: project) diff --git a/spec/controllers/projects/pipelines_controller_spec.rb b/spec/controllers/projects/pipelines_controller_spec.rb index 0bb3ef76a3b..ece8532cb84 100644 --- a/spec/controllers/projects/pipelines_controller_spec.rb +++ b/spec/controllers/projects/pipelines_controller_spec.rb @@ -5,7 +5,7 @@ describe Projects::PipelinesController do set(:user) { create(:user) } let(:project) { create(:project, :public, :repository) } - let(:feature) { ProjectFeature::DISABLED } + let(:feature) { ProjectFeature::ENABLED } before do stub_not_protect_default_branch @@ -50,7 +50,7 @@ describe Projects::PipelinesController do end end - context 'when using legacy stages', :request_store do + context 'when using legacy stages', :request_store do before do stub_feature_flags(ci_pipeline_persisted_stages: false) end @@ -186,6 +186,27 @@ describe Projects::PipelinesController do end end + context 'when builds are disabled' do + let(:feature) { ProjectFeature::DISABLED } + + it 'users can not see internal pipelines' do + get_pipeline_json + + expect(response).to have_gitlab_http_status(:not_found) + end + + context 'when pipeline is external' do + let(:pipeline) { create(:ci_pipeline, source: :external, project: project) } + + it 'users can see the external pipeline' do + get_pipeline_json + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['id']).to be(pipeline.id) + end + end + end + def get_pipeline_json get :show, params: { namespace_id: project.namespace, project_id: project, id: pipeline }, format: :json end @@ -326,16 +347,14 @@ describe Projects::PipelinesController do format: :json end - context 'when builds are enabled' do - let(:feature) { ProjectFeature::ENABLED } - - it 'retries a pipeline without returning any content' do - expect(response).to have_gitlab_http_status(:no_content) - expect(build.reload).to be_retried - end + it 'retries a pipeline without returning any content' do + expect(response).to have_gitlab_http_status(:no_content) + expect(build.reload).to be_retried end context 'when builds are disabled' do + let(:feature) { ProjectFeature::DISABLED } + it 'fails to retry pipeline' do expect(response).to have_gitlab_http_status(:not_found) end @@ -355,16 +374,14 @@ describe Projects::PipelinesController do format: :json end - context 'when builds are enabled' do - let(:feature) { ProjectFeature::ENABLED } - - it 'cancels a pipeline without returning any content' do - expect(response).to have_gitlab_http_status(:no_content) - expect(pipeline.reload).to be_canceled - end + it 'cancels a pipeline without returning any content' do + expect(response).to have_gitlab_http_status(:no_content) + expect(pipeline.reload).to be_canceled end context 'when builds are disabled' do + let(:feature) { ProjectFeature::DISABLED } + it 'fails to retry pipeline' do expect(response).to have_gitlab_http_status(:not_found) end diff --git a/spec/controllers/projects/registry/tags_controller_spec.rb b/spec/controllers/projects/registry/tags_controller_spec.rb index ed0197afcfc..74ed89ba1c3 100644 --- a/spec/controllers/projects/registry/tags_controller_spec.rb +++ b/spec/controllers/projects/registry/tags_controller_spec.rb @@ -19,7 +19,7 @@ describe Projects::Registry::TagsController do end before do - stub_container_registry_tags(repository: /image/, tags: tags) + stub_container_registry_tags(repository: /image/, tags: tags, with_manifest: true) end context 'when user can control the registry' do diff --git a/spec/controllers/projects/serverless/functions_controller_spec.rb b/spec/controllers/projects/serverless/functions_controller_spec.rb index 87114d44bce..276cf340962 100644 --- a/spec/controllers/projects/serverless/functions_controller_spec.rb +++ b/spec/controllers/projects/serverless/functions_controller_spec.rb @@ -87,7 +87,7 @@ describe Projects::Serverless::FunctionsController do end it 'has data' do - get :index, params: params({ format: :json }) + get :index, params: params({ format: :json }) expect(response).to have_gitlab_http_status(200) 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/snippets_controller_spec.rb b/spec/controllers/projects/snippets_controller_spec.rb index 75c9839dd9b..8d9cb2c8ac0 100644 --- a/spec/controllers/projects/snippets_controller_spec.rb +++ b/spec/controllers/projects/snippets_controller_spec.rb @@ -12,7 +12,7 @@ describe Projects::SnippetsController do describe 'GET #index' do context 'when page param' do - let(:last_page) { project.snippets.page().total_pages } + let(:last_page) { project.snippets.page.total_pages } let!(:project_snippet) { create(:project_snippet, :public, project: project, author: user) } it 'redirects to last_page if page number is larger than number of pages' do diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb index f84f069f4db..a1662658ade 100644 --- a/spec/controllers/projects_controller_spec.rb +++ b/spec/controllers/projects_controller_spec.rb @@ -456,7 +456,7 @@ describe ProjectsController do end context "when the project is forked" do - let(:project) { create(:project, :repository) } + let(:project) { create(:project, :repository) } let(:forked_project) { fork_project(project, nil, repository: true) } let(:merge_request) do create(:merge_request, @@ -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/search_controller_spec.rb b/spec/controllers/search_controller_spec.rb index c9b53336fd1..02a0cfe0272 100644 --- a/spec/controllers/search_controller_spec.rb +++ b/spec/controllers/search_controller_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe SearchController do - let(:user) { create(:user) } + let(:user) { create(:user) } before do sign_in(user) diff --git a/spec/controllers/uploads_controller_spec.rb b/spec/controllers/uploads_controller_spec.rb index 19142aa1272..e52a5fe42f2 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' } @@ -321,7 +327,7 @@ describe UploadsController do end context "when viewing a group avatar" do - let!(:group) { create(:group, avatar: fixture_file_upload("spec/fixtures/dk.png", "image/png")) } + let!(:group) { create(:group, avatar: fixture_file_upload("spec/fixtures/dk.png", "image/png")) } context "when the group is public" do context "when not signed in" do @@ -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/controllers/users_controller_spec.rb b/spec/controllers/users_controller_spec.rb index 27edf226ca3..af61026098b 100644 --- a/spec/controllers/users_controller_spec.rb +++ b/spec/controllers/users_controller_spec.rb @@ -206,6 +206,38 @@ describe UsersController do end end + describe 'GET #contributed' do + let(:project) { create(:project, :public) } + let(:current_user) { create(:user) } + + before do + sign_in(current_user) + + project.add_developer(public_user) + project.add_developer(private_user) + end + + context 'with public profile' do + it 'renders contributed projects' do + create(:push_event, project: project, author: public_user) + + get :contributed, params: { username: public_user.username } + + expect(assigns[:contributed_projects]).not_to be_empty + end + end + + context 'with private profile' do + it 'does not render contributed projects' do + create(:push_event, project: project, author: private_user) + + get :contributed, params: { username: private_user.username } + + expect(assigns[:contributed_projects]).to be_empty + end + end + end + describe 'GET #snippets' do before do sign_in(user) diff --git a/spec/factories/ci/bridge.rb b/spec/factories/ci/bridge.rb index 5f83b80ad7b..b1d82b98411 100644 --- a/spec/factories/ci/bridge.rb +++ b/spec/factories/ci/bridge.rb @@ -10,8 +10,20 @@ FactoryBot.define do pipeline factory: :ci_pipeline + trait :variables do + yaml_variables [{ key: 'BRIDGE', value: 'cross', public: true }] + end + + transient { downstream nil } + after(:build) do |bridge, evaluator| bridge.project ||= bridge.pipeline.project + + if evaluator.downstream.present? + bridge.options = bridge.options.to_h.merge( + trigger: { project: evaluator.downstream.full_path } + ) + end end end end diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb index bb3c0d6537d..0b3e67b4987 100644 --- a/spec/factories/ci/builds.rb +++ b/spec/factories/ci/builds.rb @@ -78,7 +78,7 @@ FactoryBot.define do trait :scheduled do schedulable status 'scheduled' - scheduled_at { 1.minute.since } + scheduled_at { 1.minute.since } end trait :expired_scheduled do 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/container_repositories.rb b/spec/factories/container_repositories.rb index 62a89a12ef5..00fad7975c9 100644 --- a/spec/factories/container_repositories.rb +++ b/spec/factories/container_repositories.rb @@ -1,6 +1,6 @@ FactoryBot.define do factory :container_repository do - name 'test_container_image' + name 'test_image' project transient do diff --git a/spec/factories/error_tracking/project.rb b/spec/factories/error_tracking/project.rb new file mode 100644 index 00000000000..5e9219b241f --- /dev/null +++ b/spec/factories/error_tracking/project.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :error_tracking_project, class: Gitlab::ErrorTracking::Project do + id '1' + name 'Sentry Example' + slug 'sentry-example' + status 'active' + organization_name 'Sentry' + organization_id '1' + organization_slug 'sentry' + + skip_create + end +end diff --git a/spec/factories/group_members.rb b/spec/factories/group_members.rb index 12be63e5d92..077c6ddc5ae 100644 --- a/spec/factories/group_members.rb +++ b/spec/factories/group_members.rb @@ -8,7 +8,7 @@ FactoryBot.define do trait(:reporter) { access_level GroupMember::REPORTER } trait(:developer) { access_level GroupMember::DEVELOPER } trait(:maintainer) { access_level GroupMember::MAINTAINER } - trait(:owner) { access_level GroupMember::OWNER } + trait(:owner) { access_level GroupMember::OWNER } trait(:access_request) { requested_at { Time.now } } trait(:invited) do 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/admin/admin_abuse_reports_spec.rb b/spec/features/admin/admin_abuse_reports_spec.rb index d8fcdebfc6d..3ff1a66b0b2 100644 --- a/spec/features/admin/admin_abuse_reports_spec.rb +++ b/spec/features/admin/admin_abuse_reports_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe "Admin::AbuseReports", :js do +describe "Admin::AbuseReports", :js do let(:user) { create(:user) } context 'as an admin' do diff --git a/spec/features/admin/admin_projects_spec.rb b/spec/features/admin/admin_projects_spec.rb index d6ee256f5b5..2b6bfa40beb 100644 --- a/spec/features/admin/admin_projects_spec.rb +++ b/spec/features/admin/admin_projects_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe "Admin::Projects" do +describe "Admin::Projects" do include Select2Helper let(:user) { create :user } diff --git a/spec/features/atom/dashboard_issues_spec.rb b/spec/features/atom/dashboard_issues_spec.rb index 5fa1a26f1a6..dfa1c92ea49 100644 --- a/spec/features/atom/dashboard_issues_spec.rb +++ b/spec/features/atom/dashboard_issues_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe "Dashboard Issues Feed" do +describe "Dashboard Issues Feed" do describe "GET /issues" do let!(:user) { create(:user, email: 'private1@example.com', public_email: 'public1@example.com') } let!(:assignee) { create(:user, email: 'private2@example.com', public_email: 'public2@example.com') } diff --git a/spec/features/atom/dashboard_spec.rb b/spec/features/atom/dashboard_spec.rb index 86b3f88298f..947587220a9 100644 --- a/spec/features/atom/dashboard_spec.rb +++ b/spec/features/atom/dashboard_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe "Dashboard Feed" do +describe "Dashboard Feed" do describe "GET /" do let!(:user) { create(:user, name: "Jonh") } diff --git a/spec/features/atom/issues_spec.rb b/spec/features/atom/issues_spec.rb index ee3570a5b2b..714a9885caa 100644 --- a/spec/features/atom/issues_spec.rb +++ b/spec/features/atom/issues_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe 'Issues Feed' do +describe 'Issues Feed' do describe 'GET /issues' do let!(:user) { create(:user, email: 'private1@example.com', public_email: 'public1@example.com') } let!(:assignee) { create(:user, email: 'private2@example.com', public_email: 'public2@example.com') } diff --git a/spec/features/atom/users_spec.rb b/spec/features/atom/users_spec.rb index 8d7df346abb..7de8bea5049 100644 --- a/spec/features/atom/users_spec.rb +++ b/spec/features/atom/users_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe "User Feed" do +describe "User Feed" do describe "GET /" do let!(:user) { create(:user) } diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb index baa2b1d8af5..ea69ec0319b 100644 --- a/spec/features/boards/boards_spec.rb +++ b/spec/features/boards/boards_spec.rb @@ -68,7 +68,7 @@ describe 'Issue Boards', :js do let(:bug) { create(:label, project: project, name: 'Bug') } let!(:backlog) { create(:label, project: project, name: 'Backlog') } let!(:closed) { create(:label, project: project, name: 'Closed') } - let!(:accepting) { create(:label, project: project, name: 'Accepting Merge Requests') } + let!(:accepting) { create(:label, project: project, name: 'Accepting Merge Requests') } let!(:a_plus) { create(:label, project: project, name: 'A+') } let!(:list1) { create(:list, board: board, label: planning, position: 0) } @@ -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/boards/modal_filter_spec.rb b/spec/features/boards/modal_filter_spec.rb index d96707e55fd..e42d18b457e 100644 --- a/spec/features/boards/modal_filter_spec.rb +++ b/spec/features/boards/modal_filter_spec.rb @@ -112,7 +112,7 @@ describe 'Issue Boards add issue modal filtering', :js do page.within('.add-issues-modal') do wait_for_requests - expect(page).to have_selector('.js-visual-token', text: 'none') + expect(page).to have_selector('.js-visual-token', text: 'None') expect(page).to have_selector('.board-card', count: 1) end end @@ -147,7 +147,7 @@ describe 'Issue Boards add issue modal filtering', :js do page.within('.add-issues-modal') do wait_for_requests - expect(page).to have_selector('.js-visual-token', text: 'upcoming') + expect(page).to have_selector('.js-visual-token', text: 'Upcoming') expect(page).to have_selector('.board-card', count: 0) end end @@ -182,7 +182,7 @@ describe 'Issue Boards add issue modal filtering', :js do page.within('.add-issues-modal') do wait_for_requests - expect(page).to have_selector('.js-visual-token', text: 'none') + expect(page).to have_selector('.js-visual-token', text: 'None') expect(page).to have_selector('.board-card', count: 1) end end diff --git a/spec/features/container_registry_spec.rb b/spec/features/container_registry_spec.rb index 9986206f619..6f9901815e1 100644 --- a/spec/features/container_registry_spec.rb +++ b/spec/features/container_registry_spec.rb @@ -25,7 +25,7 @@ describe "Container Registry", :js do context 'when there are image repositories' do before do - stub_container_registry_tags(repository: %r{my/image}, tags: %w[latest]) + stub_container_registry_tags(repository: %r{my/image}, tags: %w[latest], with_manifest: true) project.container_repositories << container_repository end diff --git a/spec/features/cycle_analytics_spec.rb b/spec/features/cycle_analytics_spec.rb index 32c75cae0a1..f4b2b9033ab 100644 --- a/spec/features/cycle_analytics_spec.rb +++ b/spec/features/cycle_analytics_spec.rb @@ -11,7 +11,7 @@ describe 'Cycle Analytics', :js do context 'as an allowed user' do context 'when project is new' do - before do + before do project.add_maintainer(user) sign_in(user) diff --git a/spec/features/dashboard/datetime_on_tooltips_spec.rb b/spec/features/dashboard/datetime_on_tooltips_spec.rb index 0db8093411b..f44bd55ecf6 100644 --- a/spec/features/dashboard/datetime_on_tooltips_spec.rb +++ b/spec/features/dashboard/datetime_on_tooltips_spec.rb @@ -15,7 +15,7 @@ describe 'Tooltips on .timeago dates', :js do sign_in user visit user_activity_path(user) - wait_for_requests() + wait_for_requests page.find('.js-timeago').hover end @@ -32,7 +32,7 @@ describe 'Tooltips on .timeago dates', :js do sign_in user visit user_snippets_path(user) - wait_for_requests() + wait_for_requests page.find('.js-timeago.snippet-created-ago').hover end diff --git a/spec/features/dashboard/help_spec.rb b/spec/features/dashboard/help_spec.rb index fa12cecc984..467a503a62d 100644 --- a/spec/features/dashboard/help_spec.rb +++ b/spec/features/dashboard/help_spec.rb @@ -5,14 +5,6 @@ RSpec.describe 'Dashboard Help' do sign_in(create(:user)) end - context 'help dropdown' do - it 'shows the "What\'s new?" menu item' do - visit root_dashboard_path - - 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") diff --git a/spec/features/dashboard/projects_spec.rb b/spec/features/dashboard/projects_spec.rb index 975b7944741..6c4b04ab76b 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 @@ -144,6 +147,27 @@ describe 'Dashboard Projects' do expect(page).to have_link('Commit: passed') end end + + context 'guest user of project and project has private pipelines' do + let(:guest_user) { create(:user) } + + before do + project.update(public_builds: false) + project.add_guest(guest_user) + sign_in(guest_user) + end + + it 'shows that the last pipeline passed' do + visit dashboard_projects_path + + page.within('.controls') do + expect(page).not_to have_xpath("//a[@href='#{pipelines_project_commit_path(project, project.commit, ref: pipeline.ref)}']") + expect(page).not_to have_css('.ci-status-link') + expect(page).not_to have_css('.ci-status-icon-success') + expect(page).not_to have_link('Commit: passed') + end + end + end end context 'last push widget', :use_clean_rails_memory_store_caching do diff --git a/spec/features/dashboard/todos/todos_spec.rb b/spec/features/dashboard/todos/todos_spec.rb index 2284ee925a0..51f158d3045 100644 --- a/spec/features/dashboard/todos/todos_spec.rb +++ b/spec/features/dashboard/todos/todos_spec.rb @@ -112,7 +112,7 @@ describe 'Dashboard Todos' do end it 'shows issue assigned to yourself message' do - page.within('.js-todos-all') do + page.within('.js-todos-all') do expect(page).to have_content("You assigned issue #{issue.to_reference(full: true)} to yourself") end end @@ -125,7 +125,7 @@ describe 'Dashboard Todos' do end it 'shows you added a todo message' do - page.within('.js-todos-all') do + page.within('.js-todos-all') do expect(page).to have_content("You added a todo for issue #{issue.to_reference(full: true)}") expect(page).not_to have_content('to yourself') end @@ -139,7 +139,7 @@ describe 'Dashboard Todos' do end it 'shows you mentioned yourself message' do - page.within('.js-todos-all') do + page.within('.js-todos-all') do expect(page).to have_content("You mentioned yourself on issue #{issue.to_reference(full: true)}") expect(page).not_to have_content('to yourself') end @@ -153,7 +153,7 @@ describe 'Dashboard Todos' do end it 'shows you directly addressed yourself message' do - page.within('.js-todos-all') do + page.within('.js-todos-all') do expect(page).to have_content("You directly addressed yourself on issue #{issue.to_reference(full: true)}") expect(page).not_to have_content('to yourself') end @@ -169,7 +169,7 @@ describe 'Dashboard Todos' do end it 'shows you set yourself as an approver message' do - page.within('.js-todos-all') do + page.within('.js-todos-all') do expect(page).to have_content("You set yourself as an approver for merge request #{merge_request.to_reference(full: true)}") expect(page).not_to have_content('to yourself') end diff --git a/spec/features/groups/group_settings_spec.rb b/spec/features/groups/group_settings_spec.rb index 2cdbdcffbc3..378e4d5febc 100644 --- a/spec/features/groups/group_settings_spec.rb +++ b/spec/features/groups/group_settings_spec.rb @@ -18,14 +18,14 @@ describe 'Edit group settings' do update_path(new_group_path) visit new_group_full_path expect(current_path).to eq(new_group_full_path) - expect(find('h1.group-title')).to have_content(group.name) + expect(find('h1.home-panel-title')).to have_content(group.name) end it 'the old group path redirects to the new path' do update_path(new_group_path) visit old_group_full_path expect(current_path).to eq(new_group_full_path) - expect(find('h1.group-title')).to have_content(group.name) + expect(find('h1.home-panel-title')).to have_content(group.name) end context 'with a subgroup' do @@ -37,14 +37,14 @@ describe 'Edit group settings' do update_path(new_group_path) visit new_subgroup_full_path expect(current_path).to eq(new_subgroup_full_path) - expect(find('h1.group-title')).to have_content(subgroup.name) + expect(find('h1.home-panel-title')).to have_content(subgroup.name) end it 'the old subgroup path redirects to the new path' do update_path(new_group_path) visit old_subgroup_full_path expect(current_path).to eq(new_subgroup_full_path) - expect(find('h1.group-title')).to have_content(subgroup.name) + expect(find('h1.home-panel-title')).to have_content(subgroup.name) end end diff --git a/spec/features/groups_spec.rb b/spec/features/groups_spec.rb index d01fc04311a..c2f32c76422 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 @@ -201,7 +203,7 @@ describe 'Group' do visit path - expect(page).to have_css('.group-home-desc > p > strong') + expect(page).to have_css('.home-panel-description-markdown > p > strong') end it 'passes through html-pipeline' do @@ -209,7 +211,7 @@ describe 'Group' do visit path - expect(page).to have_css('.group-home-desc > p > gl-emoji') + expect(page).to have_css('.home-panel-description-markdown > p > gl-emoji') end it 'sanitizes unwanted tags' do @@ -217,7 +219,7 @@ describe 'Group' do visit path - expect(page).not_to have_css('.group-home-desc h1') + expect(page).not_to have_css('.home-panel-description-markdown h1') end it 'permits `rel` attribute on links' do @@ -225,7 +227,7 @@ describe 'Group' do visit path - expect(page).to have_css('.group-home-desc a[rel]') + expect(page).to have_css('.home-panel-description-markdown a[rel]') end end @@ -233,7 +235,7 @@ describe 'Group' do let!(:group) { create(:group) } let!(:nested_group) { create(:group, parent: group) } let!(:project) { create(:project, namespace: group) } - let!(:path) { group_path(group) } + let!(:path) { group_path(group) } it 'it renders projects and groups on the page' do visit path diff --git a/spec/features/ics/dashboard_issues_spec.rb b/spec/features/ics/dashboard_issues_spec.rb index ea714934ae7..debae0ea930 100644 --- a/spec/features/ics/dashboard_issues_spec.rb +++ b/spec/features/ics/dashboard_issues_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe 'Dashboard Issues Calendar Feed' do +describe 'Dashboard Issues Calendar Feed' do describe 'GET /issues' do let!(:user) { create(:user, email: 'private1@example.com', public_email: 'public1@example.com') } let!(:assignee) { create(:user, email: 'private2@example.com', public_email: 'public2@example.com') } diff --git a/spec/features/ics/group_issues_spec.rb b/spec/features/ics/group_issues_spec.rb index 24de5b4b7c6..4177c7f8704 100644 --- a/spec/features/ics/group_issues_spec.rb +++ b/spec/features/ics/group_issues_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe 'Group Issues Calendar Feed' do +describe 'Group Issues Calendar Feed' do describe 'GET /issues' do let!(:user) { create(:user, email: 'private1@example.com', public_email: 'public1@example.com') } let!(:assignee) { create(:user, email: 'private2@example.com', public_email: 'public2@example.com') } diff --git a/spec/features/ics/project_issues_spec.rb b/spec/features/ics/project_issues_spec.rb index 54143595e6b..0d9844be099 100644 --- a/spec/features/ics/project_issues_spec.rb +++ b/spec/features/ics/project_issues_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe 'Project Issues Calendar Feed' do +describe 'Project Issues Calendar Feed' do describe 'GET /issues' do let!(:user) { create(:user, email: 'private1@example.com', public_email: 'public1@example.com') } let!(:assignee) { create(:user, email: 'private2@example.com', public_email: 'public2@example.com') } 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/filtered_search/dropdown_assignee_spec.rb b/spec/features/issues/filtered_search/dropdown_assignee_spec.rb index e910fb54d23..e0b1e286dee 100644 --- a/spec/features/issues/filtered_search/dropdown_assignee_spec.rb +++ b/spec/features/issues/filtered_search/dropdown_assignee_spec.rb @@ -37,7 +37,7 @@ describe 'Dropdown assignee', :js do end it 'closes when the search bar is unfocused' do - find('body').click() + find('body').click expect(page).to have_css(js_dropdown_assignee, visible: false) end @@ -160,7 +160,7 @@ describe 'Dropdown assignee', :js do find('#js-dropdown-assignee .filter-dropdown-item', text: 'None').click expect(page).to have_css(js_dropdown_assignee, visible: false) - expect_tokens([assignee_token('none')]) + expect_tokens([assignee_token('None')]) expect_filtered_search_input_empty end @@ -168,7 +168,7 @@ describe 'Dropdown assignee', :js do find('#js-dropdown-assignee .filter-dropdown-item', text: 'Any').click expect(page).to have_css(js_dropdown_assignee, visible: false) - expect_tokens([assignee_token('any')]) + expect_tokens([assignee_token('Any')]) expect_filtered_search_input_empty end end diff --git a/spec/features/issues/filtered_search/dropdown_author_spec.rb b/spec/features/issues/filtered_search/dropdown_author_spec.rb index 50d819a6161..bedc61b9eed 100644 --- a/spec/features/issues/filtered_search/dropdown_author_spec.rb +++ b/spec/features/issues/filtered_search/dropdown_author_spec.rb @@ -45,7 +45,7 @@ describe 'Dropdown author', :js do end it 'closes when the search bar is unfocused' do - find('body').click() + find('body').click expect(page).to have_css(js_dropdown_author, visible: false) end diff --git a/spec/features/issues/filtered_search/dropdown_emoji_spec.rb b/spec/features/issues/filtered_search/dropdown_emoji_spec.rb index 97dd0afd002..f36d4e8f23f 100644 --- a/spec/features/issues/filtered_search/dropdown_emoji_spec.rb +++ b/spec/features/issues/filtered_search/dropdown_emoji_spec.rb @@ -64,7 +64,7 @@ describe 'Dropdown emoji', :js do end it 'closes when the search bar is unfocused' do - find('body').click() + find('body').click expect(page).to have_css(js_dropdown_emoji, visible: false) end @@ -125,7 +125,7 @@ describe 'Dropdown emoji', :js do find('#js-dropdown-my-reaction .filter-dropdown-item', text: 'None').click expect(page).to have_css(js_dropdown_emoji, visible: false) - expect_tokens([reaction_token('none', false)]) + expect_tokens([reaction_token('None', false)]) expect_filtered_search_input_empty end @@ -133,7 +133,7 @@ describe 'Dropdown emoji', :js do find('#js-dropdown-my-reaction .filter-dropdown-item', text: 'Any').click expect(page).to have_css(js_dropdown_emoji, visible: false) - expect_tokens([reaction_token('any', false)]) + expect_tokens([reaction_token('Any', false)]) expect_filtered_search_input_empty end diff --git a/spec/features/issues/filtered_search/dropdown_label_spec.rb b/spec/features/issues/filtered_search/dropdown_label_spec.rb index b25b1514d62..f502061dfce 100644 --- a/spec/features/issues/filtered_search/dropdown_label_spec.rb +++ b/spec/features/issues/filtered_search/dropdown_label_spec.rb @@ -238,7 +238,7 @@ describe 'Dropdown label', :js do find("#{js_dropdown_label} .filter-dropdown-item", text: 'None').click expect(page).not_to have_css(js_dropdown_label) - expect_tokens([label_token('none', false)]) + expect_tokens([label_token('None', false)]) expect_filtered_search_input_empty end @@ -246,7 +246,7 @@ describe 'Dropdown label', :js do find("#{js_dropdown_label} .filter-dropdown-item", text: 'Any').click expect(page).not_to have_css(js_dropdown_label) - expect_tokens([label_token('any', false)]) + expect_tokens([label_token('Any', false)]) expect_filtered_search_input_empty end end diff --git a/spec/features/issues/filtered_search/dropdown_milestone_spec.rb b/spec/features/issues/filtered_search/dropdown_milestone_spec.rb index ef5801e61e8..b330eafe1d1 100644 --- a/spec/features/issues/filtered_search/dropdown_milestone_spec.rb +++ b/spec/features/issues/filtered_search/dropdown_milestone_spec.rb @@ -44,7 +44,7 @@ describe 'Dropdown milestone', :js do end it 'closes when the search bar is unfocused' do - find('body').click() + find('body').click expect(page).to have_css(js_dropdown_milestone, visible: false) end @@ -192,7 +192,7 @@ describe 'Dropdown milestone', :js do click_static_milestone('None') expect(page).to have_css(js_dropdown_milestone, visible: false) - expect_tokens([milestone_token('none', false)]) + expect_tokens([milestone_token('None', false)]) expect_filtered_search_input_empty end @@ -200,7 +200,7 @@ describe 'Dropdown milestone', :js do click_static_milestone('Any') expect(page).to have_css(js_dropdown_milestone, visible: false) - expect_tokens([milestone_token('any', false)]) + expect_tokens([milestone_token('Any', false)]) expect_filtered_search_input_empty end @@ -208,7 +208,7 @@ describe 'Dropdown milestone', :js do click_static_milestone('Upcoming') expect(page).to have_css(js_dropdown_milestone, visible: false) - expect_tokens([milestone_token('upcoming', false)]) + expect_tokens([milestone_token('Upcoming', false)]) expect_filtered_search_input_empty end @@ -216,7 +216,7 @@ describe 'Dropdown milestone', :js do click_static_milestone('Started') expect(page).to have_css(js_dropdown_milestone, visible: false) - expect_tokens([milestone_token('started', false)]) + expect_tokens([milestone_token('Started', false)]) expect_filtered_search_input_empty end end diff --git a/spec/features/issues/filtered_search/filter_issues_spec.rb b/spec/features/issues/filtered_search/filter_issues_spec.rb index a29380a180e..fa8e5cb0ca9 100644 --- a/spec/features/issues/filtered_search/filter_issues_spec.rb +++ b/spec/features/issues/filtered_search/filter_issues_spec.rb @@ -108,7 +108,7 @@ describe 'Filter issues', :js do it 'filters issues by no assignee' do input_filtered_search('assignee:none') - expect_tokens([assignee_token('none')]) + expect_tokens([assignee_token('None')]) expect_issues_list_count(3) expect_filtered_search_input_empty end @@ -146,7 +146,7 @@ describe 'Filter issues', :js do it 'filters issues by no label' do input_filtered_search('label:none') - expect_tokens([label_token('none', false)]) + expect_tokens([label_token('None', false)]) expect_issues_list_count(4) expect_filtered_search_input_empty end @@ -287,7 +287,7 @@ describe 'Filter issues', :js do it 'filters issues by no milestone' do input_filtered_search("milestone:none") - expect_tokens([milestone_token('none', false)]) + expect_tokens([milestone_token('None', false)]) expect_issues_list_count(3) expect_filtered_search_input_empty end @@ -299,7 +299,7 @@ describe 'Filter issues', :js do input_filtered_search("milestone:upcoming") - expect_tokens([milestone_token('upcoming', false)]) + expect_tokens([milestone_token('Upcoming', false)]) expect_issues_list_count(1) expect_filtered_search_input_empty end @@ -307,7 +307,7 @@ describe 'Filter issues', :js do it 'filters issues by started milestones' do input_filtered_search("milestone:started") - expect_tokens([milestone_token('started', false)]) + expect_tokens([milestone_token('Started', false)]) expect_issues_list_count(5) expect_filtered_search_input_empty end diff --git a/spec/features/issues/filtered_search/visual_tokens_spec.rb b/spec/features/issues/filtered_search/visual_tokens_spec.rb index 1e1dd5691ab..a4c34ce85f0 100644 --- a/spec/features/issues/filtered_search/visual_tokens_spec.rb +++ b/spec/features/issues/filtered_search/visual_tokens_spec.rb @@ -122,7 +122,7 @@ describe 'Visual tokens', :js do end it 'changes value in visual token' do - expect(first('.tokens-container .filtered-search-token .value').text).to eq('none') + expect(first('.tokens-container .filtered-search-token .value').text).to eq('None') end it 'moves input to the right' do @@ -147,7 +147,7 @@ describe 'Visual tokens', :js do it 'selects static option from dropdown' do find("#js-dropdown-milestone").find('.filter-dropdown-item', text: 'Upcoming').click - expect(first('.tokens-container .filtered-search-token .value').text).to eq('upcoming') + expect(first('.tokens-container .filtered-search-token .value').text).to eq('Upcoming') expect(is_input_focused).to eq(true) end @@ -348,7 +348,7 @@ describe 'Visual tokens', :js do it 'tokenizes the search term to complete visual token' do expect_tokens([ author_token(user.name), - assignee_token('none') + assignee_token('None') ]) end end diff --git a/spec/features/issues/gfm_autocomplete_spec.rb b/spec/features/issues/gfm_autocomplete_spec.rb index 3b7a17ef355..c22ad0d20ef 100644 --- a/spec/features/issues/gfm_autocomplete_spec.rb +++ b/spec/features/issues/gfm_autocomplete_spec.rb @@ -279,7 +279,7 @@ describe 'GFM autocomplete', :js do end # This context has jsut one example in each contexts in order to improve spec performance. - context 'labels' do + context 'labels', :quarantine do let!(:backend) { create(:label, project: project, title: 'backend') } let!(:bug) { create(:label, project: project, title: 'bug') } let!(:feature_proposal) { create(:label, project: project, title: 'feature proposal') } diff --git a/spec/features/issues/resource_label_events_spec.rb b/spec/features/issues/resource_label_events_spec.rb index b0764db7751..3d380c183ec 100644 --- a/spec/features/issues/resource_label_events_spec.rb +++ b/spec/features/issues/resource_label_events_spec.rb @@ -6,7 +6,7 @@ describe 'List issue resource label events', :js do let(:user) { create(:user) } let(:project) { create(:project, :public) } let(:issue) { create(:issue, project: project, author: user) } - let!(:label) { create(:label, project: project, title: 'foo') } + let!(:label) { create(:label, project: project, title: 'foo') } let!(:user_status) { create(:user_status, user: user) } context 'when user displays the issue' do 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/labels_hierarchy_spec.rb b/spec/features/labels_hierarchy_spec.rb index 09904cb907f..7c31e67a7fa 100644 --- a/spec/features/labels_hierarchy_spec.rb +++ b/spec/features/labels_hierarchy_spec.rb @@ -179,7 +179,7 @@ describe 'Labels Hierarchy', :js, :nested_groups do end context 'on project board issue sidebar' do - let(:board) { create(:board, project: project_1) } + let(:board) { create(:board, project: project_1) } before do project_1.add_developer(user) @@ -195,7 +195,7 @@ describe 'Labels Hierarchy', :js, :nested_groups do end context 'on group board issue sidebar' do - let(:board) { create(:board, group: parent) } + let(:board) { create(:board, group: parent) } before do parent.add_developer(user) diff --git a/spec/features/markdown/copy_as_gfm_spec.rb b/spec/features/markdown/copy_as_gfm_spec.rb index 05228e27963..16754035076 100644 --- a/spec/features/markdown/copy_as_gfm_spec.rb +++ b/spec/features/markdown/copy_as_gfm_spec.rb @@ -19,9 +19,9 @@ describe 'Copy as GFM', :js do visit project_issue_path(@project, @feat.issue) end - # The filters referenced in lib/banzai/pipeline/gfm_pipeline.rb convert GitLab Flavored Markdown (GFM) to HTML. - # The handlers defined in app/assets/javascripts/behaviors/markdown/copy_as_gfm.js consequently convert that same HTML to GFM. - # To make sure these filters and handlers are properly aligned, this spec tests the GFM-to-HTML-to-GFM cycle + # The filters referenced in lib/banzai/pipeline/gfm_pipeline.rb transform GitLab Flavored Markdown (GFM) to HTML. + # The nodes and marks referenced in app/assets/javascripts/behaviors/markdown/editor_extensions.js consequently transform that same HTML to GFM. + # To make sure these filters and nodes/marks are properly aligned, this spec tests the GFM-to-HTML-to-GFM cycle # by verifying (`html_to_gfm(gfm_to_html(gfm)) == gfm`) for a number of examples of GFM for every filter, using the `verify` helper. # These are all in a single `it` for performance reasons. @@ -35,12 +35,15 @@ describe 'Copy as GFM', :js do verify( 'a real world example from the gitlab-ce README', - <<-GFM.strip_heredoc + <<~GFM # GitLab [![Build status](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/build.svg)](https://gitlab.com/gitlab-org/gitlab-ce/commits/master) + [![CE coverage report](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/coverage.svg?job=coverage)](https://gitlab-org.gitlab.io/gitlab-ce/coverage-ruby) + [![Code Climate](https://codeclimate.com/github/gitlabhq/gitlabhq.svg)](https://codeclimate.com/github/gitlabhq/gitlabhq) + [![Core Infrastructure Initiative Best Practices](https://bestpractices.coreinfrastructure.org/projects/42/badge)](https://bestpractices.coreinfrastructure.org/projects/42) ## Canonical source @@ -51,27 +54,31 @@ describe 'Copy as GFM', :js do To see how GitLab looks please see the [features page on our website](https://about.gitlab.com/features/). - - Manage Git repositories with fine grained access controls that keep your code secure + * Manage Git repositories with fine grained access controls that keep your code secure - - Perform code reviews and enhance collaboration with merge requests + * Perform code reviews and enhance collaboration with merge requests - - Complete continuous integration (CI) and CD pipelines to builds, test, and deploy your applications + * Complete continuous integration (CI) and CD pipelines to builds, test, and deploy your applications - - Each project can also have an issue tracker, issue board, and a wiki + * Each project can also have an issue tracker, issue board, and a wiki - - Used by more than 100,000 organizations, GitLab is the most popular solution to manage Git repositories on-premises + * Used by more than 100,000 organizations, GitLab is the most popular solution to manage Git repositories on-premises - - Completely free and open source (MIT Expat license) + * Completely free and open source (MIT Expat license) GFM ) aggregate_failures('an accidentally selected empty element') do gfm = '# Heading1' - html = <<-HTML.strip_heredoc + html = <<~HTML <h1>Heading1</h1> <h2></h2> + + <blockquote></blockquote> + + <pre class="code highlight"></pre> HTML output_gfm = html_to_gfm(html) @@ -81,7 +88,7 @@ describe 'Copy as GFM', :js do aggregate_failures('an accidentally selected other element') do gfm = 'Test comment with **Markdown!**' - html = <<-HTML.strip_heredoc + html = <<~HTML <li class="note"> <div class="md"> <p> @@ -107,10 +114,17 @@ describe 'Copy as GFM', :js do verify( 'TaskListFilter', - '- [ ] Unchecked task', - '- [x] Checked task', - '1. [ ] Unchecked numbered task', - '1. [x] Checked numbered task' + <<~GFM, + * [ ] Unchecked task + + * [x] Checked task + GFM + + <<~GFM + 1. [ ] Unchecked ordered task + + 1. [x] Checked ordered task + GFM ) verify( @@ -139,7 +153,16 @@ describe 'Copy as GFM', :js do verify( 'TableOfContentsFilter', - '[[_TOC_]]' + <<~GFM, + [[_TOC_]] + + # Heading 1 + + ## Heading 2 + GFM + + pipeline: :wiki, + project_wiki: @project.wiki ) verify( @@ -166,7 +189,7 @@ describe 'Copy as GFM', :js do '$`c = \pm\sqrt{a^2 + b^2}`$', # math block - <<-GFM.strip_heredoc + <<~GFM ```math c = \pm\sqrt{a^2 + b^2} ``` @@ -176,7 +199,7 @@ describe 'Copy as GFM', :js do aggregate_failures('MathFilter: math as transformed from HTML to KaTeX') do gfm = '$`c = \pm\sqrt{a^2 + b^2}`$' - html = <<-HTML.strip_heredoc + html = <<~HTML <span class="katex"> <span class="katex-mathml"> <math> @@ -287,7 +310,7 @@ describe 'Copy as GFM', :js do verify( 'MermaidFilter: mermaid as converted from GFM to HTML', - <<-GFM.strip_heredoc + <<~GFM ```mermaid graph TD; A-->B; @@ -296,14 +319,14 @@ describe 'Copy as GFM', :js do ) aggregate_failures('MermaidFilter: mermaid as transformed from HTML to SVG') do - gfm = <<-GFM.strip_heredoc + gfm = <<~GFM ```mermaid graph TD; A-->B; ``` GFM - html = <<-HTML.strip_heredoc + html = <<~HTML <svg id="mermaidChart1" xmlns="http://www.w3.org/2000/svg" height="100%" viewBox="0 0 87.234375 174" style="max-width:87.234375px;" class="mermaid"> <style> .mermaid { @@ -371,8 +394,7 @@ describe 'Copy as GFM', :js do </g> </g> <text class="source" display="none">graph TD; - A-->B; - </text> + A-->B;</text> </svg> HTML @@ -381,13 +403,82 @@ describe 'Copy as GFM', :js do end verify( + 'SuggestionFilter: suggestion as converted from GFM to HTML', + + <<~GFM + ```suggestion + New + And newer + ``` + GFM + ) + + aggregate_failures('SuggestionFilter: suggestion as transformed from HTML to Vue component') do + gfm = <<~GFM + ```suggestion + New + And newer + ``` + GFM + + html = <<~HTML + <div class="md-suggestion"> + <div class="md-suggestion-header border-bottom-0 mt-2 qa-suggestion-diff-header"> + <div class="qa-suggestion-diff-header font-weight-bold"> + Suggested change + <a href="/gitlab/help/user/discussions/index.md#suggest-changes" aria-label="Help" class="js-help-btn"> + <svg aria-hidden="true" class="s16 ic-question-o link-highlight"> + <use xlink:href="/gitlab/assets/icons.svg#question-o"></use> + </svg> + </a> + </div> + <!----> + <button type="button" class="btn qa-apply-btn">Apply suggestion</button> + </div> + <table class="mb-3 md-suggestion-diff js-syntax-highlight code white"> + <tbody> + <tr class="line_holder old"> + <td class="diff-line-num old_line qa-old-diff-line-number old">9</td> + <td class="diff-line-num new_line old"></td> + <td class="line_content old"><span>Old + </span></td> + </tr> + <tr class="line_holder new"> + <td class="diff-line-num old_line new"></td> + <td class="diff-line-num new_line qa-new-diff-line-number new">9</td> + <td class="line_content new"><span>New + </span></td> + </tr> + <tr class="line_holder new"> + <td class="diff-line-num old_line new"></td> + <td class="diff-line-num new_line qa-new-diff-line-number new">10</td> + <td class="line_content new"><span> And newer + </span></td> + </tr> + </tbody> + </table> + </div> + HTML + + output_gfm = html_to_gfm(html) + expect(output_gfm.strip).to eq(gfm.strip) + end + + verify( 'SanitizationFilter', - <<-GFM.strip_heredoc + <<~GFM <sub>sub</sub> <dl> <dt>dt</dt> + <dt>dt</dt> + <dd>dd</dd> + <dd>dd</dd> + + <dt>dt</dt> + <dt>dt</dt> + <dd>dd</dd> <dd>dd</dd> </dl> @@ -399,30 +490,26 @@ describe 'Copy as GFM', :js do <var>var</var> - <ruby>ruby</ruby> - - <rt>rt</rt> - - <rp>rp</rp> + <abbr title="HyperText "Markup" Language">HTML</abbr> - <abbr>abbr</abbr> + <details> + <summary>summary></summary> - <summary>summary</summary> - - <details>details</details> + details + </details> GFM ) verify( 'SanitizationFilter', - <<-GFM.strip_heredoc, + <<~GFM, ``` Plain text ``` GFM - <<-GFM.strip_heredoc, + <<~GFM, ```ruby def foo bar @@ -430,11 +517,9 @@ describe 'Copy as GFM', :js do ``` GFM - <<-GFM.strip_heredoc + <<~GFM Foo - This is an example of GFM - ```js Code goes here ``` @@ -452,9 +537,8 @@ describe 'Copy as GFM', :js do '> Quote', # multiline quote - <<-GFM.strip_heredoc, - > Multiline - > Quote + <<~GFM, + > Multiline Quote > > With multiple paragraphs GFM @@ -465,48 +549,58 @@ describe 'Copy as GFM', :js do '[Link](https://example.com)', - '- List item', + <<~GFM, + * List item + + * List item 2 + GFM # multiline list item - <<-GFM.strip_heredoc, - - Multiline - List item + <<~GFM, + * Multiline + + List item GFM # nested lists - <<-GFM.strip_heredoc, - - Nested + <<~GFM, + * Nested - - Lists + * Lists GFM # list with blockquote - <<-GFM.strip_heredoc, - - List + <<~GFM, + * List - > Blockquote + > Blockquote GFM - '1. Numbered list item', + <<~GFM, + 1. Ordered list item + + 1. Ordered list item 2 + GFM - # multiline numbered list item - <<-GFM.strip_heredoc, + # multiline ordered list item + <<~GFM, 1. Multiline - Numbered list item + + Ordered list item GFM - # nested numbered list - <<-GFM.strip_heredoc, + # nested ordered list + <<~GFM, 1. Nested - 1. Numbered lists + 1. Ordered lists GFM # list item followed by an HR - <<-GFM.strip_heredoc, - - list item + <<~GFM, + * list item - ----- + --- GFM '# Heading', @@ -518,14 +612,14 @@ describe 'Copy as GFM', :js do '**Bold**', - '_Italics_', + '*Italics*', '~~Strikethrough~~', - '-----', + '---', # table - <<-GFM.strip_heredoc, + <<~GFM, | Centered | Right | Left | |:--------:|------:|------| | Foo | Bar | **Baz** | @@ -533,9 +627,9 @@ describe 'Copy as GFM', :js do GFM # table with empty heading - <<-GFM.strip_heredoc, + <<~GFM, | | x | y | - |---|---|---| + |--|---|---| | a | 1 | 0 | | b | 0 | 1 | GFM @@ -545,9 +639,11 @@ describe 'Copy as GFM', :js do alias_method :gfm_to_html, :markdown def verify(label, *gfms) + markdown_options = gfms.extract_options! + aggregate_failures(label) do gfms.each do |gfm| - html = gfm_to_html(gfm).gsub(/\A
|
\z/, '') + html = gfm_to_html(gfm, markdown_options).gsub(/\A
|
\z/, '') output_gfm = html_to_gfm(html) expect(output_gfm.strip).to eq(gfm.strip) end @@ -594,7 +690,7 @@ describe 'Copy as GFM', :js do verify( '[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"], [id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_10"]', - <<-GFM.strip_heredoc, + <<~GFM, ```ruby raise RuntimeError, "System commands must be given as an array of strings" end @@ -627,7 +723,7 @@ describe 'Copy as GFM', :js do verify( '[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_8_8"], [id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_9_9"], [id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"], [id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_10"]', - <<-GFM.strip_heredoc, + <<~GFM, ```ruby unless cmd.is_a?(Array) raise "System commands must be given as an array of strings" @@ -645,7 +741,7 @@ describe 'Copy as GFM', :js do verify( '[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_8_8"], [id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_9_9"], [id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"], [id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_10"]', - <<-GFM.strip_heredoc, + <<~GFM, ```ruby unless cmd.is_a?(Array) raise RuntimeError, "System commands must be given as an array of strings" @@ -691,7 +787,7 @@ describe 'Copy as GFM', :js do verify( '.line[id="LC9"], .line[id="LC10"]', - <<-GFM.strip_heredoc, + <<~GFM, ```ruby raise RuntimeError, "System commands must be given as an array of strings" end @@ -733,7 +829,7 @@ describe 'Copy as GFM', :js do verify( '.line[id="LC27"], .line[id="LC28"]', - <<-GFM.strip_heredoc, + <<~GFM, ```json "bio": null, "skype": "", @@ -752,7 +848,7 @@ describe 'Copy as GFM', :js do end def html_for_selector(selector) - js = <<-JS.strip_heredoc + js = <<~JS (function(selector) { var els = document.querySelectorAll(selector); var htmls = [].slice.call(els).map(function(el) { return el.outerHTML; }); @@ -763,7 +859,7 @@ describe 'Copy as GFM', :js do end def html_to_gfm(html, transformer = 'transformGFMSelection', target: nil) - js = <<-JS.strip_heredoc + js = <<~JS (function(html) { var transformer = window.CopyAsGFM[#{transformer.inspect}]; diff --git a/spec/features/markdown/math_spec.rb b/spec/features/markdown/math_spec.rb index 6a23d6b78ab..16ad0d456be 100644 --- a/spec/features/markdown/math_spec.rb +++ b/spec/features/markdown/math_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe 'Math rendering', :js do + let!(:project) { create(:project, :public) } + it 'renders inline and display math correctly' do description = <<~MATH This math is inline $`a^2+b^2=c^2`$. @@ -11,12 +13,26 @@ describe 'Math rendering', :js do ``` MATH - project = create(:project, :public) issue = create(:issue, project: project, description: description) 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 + + it 'only renders non XSS links' do + description = <<~MATH + This link is valid $`\\href{javascript:alert('xss');}{xss}`$. + + This link is valid $`\\href{https://gitlab.com}{Gitlab}`$. + MATH + + issue = create(:issue, project: project, description: description) + + visit project_issue_path(project, issue) + + expect(page).to have_selector('.katex-error', text: "\href{javascript:alert('xss');}{xss}") + expect(page).to have_selector('.katex-html a', text: 'Gitlab') end end diff --git a/spec/features/merge_request/user_accepts_merge_request_spec.rb b/spec/features/merge_request/user_accepts_merge_request_spec.rb index 01aeed93947..00ac7c72a11 100644 --- a/spec/features/merge_request/user_accepts_merge_request_spec.rb +++ b/spec/features/merge_request/user_accepts_merge_request_spec.rb @@ -25,7 +25,7 @@ describe 'User accepts a merge request', :js do end it 'accepts a merge request' do - check('Remove source branch') + check('Delete source branch') click_button('Merge') expect(page).to have_content('The changes were merged into') @@ -60,7 +60,7 @@ describe 'User accepts a merge request', :js do end it 'accepts a merge request' do - check('Remove source branch') + check('Delete source branch') click_button('Merge') expect(page).to have_content('The changes were merged into') diff --git a/spec/features/merge_request/user_comments_on_diff_spec.rb b/spec/features/merge_request/user_comments_on_diff_spec.rb index 00cf368e8c9..eb4b2cf5bd0 100644 --- a/spec/features/merge_request/user_comments_on_diff_spec.rb +++ b/spec/features/merge_request/user_comments_on_diff_spec.rb @@ -91,6 +91,7 @@ describe 'User comments on a diff', :js do # Check the same comments in the side-by-side view. execute_script("window.scrollTo(0,0);") + find('.js-show-diff-settings').click click_button 'Side-by-side' wait_for_requests diff --git a/spec/features/merge_request/user_creates_mr_spec.rb b/spec/features/merge_request/user_creates_mr_spec.rb index 9d2a94a4a41..c169a68cd1c 100644 --- a/spec/features/merge_request/user_creates_mr_spec.rb +++ b/spec/features/merge_request/user_creates_mr_spec.rb @@ -30,7 +30,7 @@ describe 'Merge request > User creates MR' do end context 'source project', :js do - let(:user) { create(:user) } + let(:user) { create(:user) } let(:target_project) { create(:project, :public, :repository) } let(:source_project) { target_project } 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_merges_when_pipeline_succeeds_spec.rb b/spec/features/merge_request/user_merges_when_pipeline_succeeds_spec.rb index 29b3d2b629b..6e54aa6006b 100644 --- a/spec/features/merge_request/user_merges_when_pipeline_succeeds_spec.rb +++ b/spec/features/merge_request/user_merges_when_pipeline_succeeds_spec.rb @@ -33,7 +33,7 @@ describe 'Merge request > User merges when pipeline succeeds', :js do click_button "Merge when pipeline succeeds" expect(page).to have_content "Set by #{user.name} to be merged automatically when the pipeline succeeds" - expect(page).to have_content "The source branch will not be removed" + expect(page).to have_content "The source branch will not be deleted" expect(page).to have_selector ".js-cancel-auto-merge" visit project_merge_request_path(project, merge_request) # Needed to refresh the page expect(page).to have_content /enabled an automatic merge when the pipeline for \h{8} succeeds/i @@ -94,7 +94,7 @@ describe 'Merge request > User merges when pipeline succeeds', :js do click_link 'Merge when pipeline succeeds' expect(page).to have_content "Set by #{user.name} to be merged automatically when the pipeline succeeds" - expect(page).to have_content "The source branch will not be removed" + expect(page).to have_content "The source branch will not be deleted" expect(page).to have_link "Cancel automatic merge" end end @@ -127,10 +127,10 @@ describe 'Merge request > User merges when pipeline succeeds', :js do expect(page).to have_content "canceled the automatic merge" end - it 'allows to remove source branch' do - click_link "Remove source branch" + it 'allows to delete source branch' do + click_link "Delete source branch" - expect(page).to have_content "The source branch will be removed" + expect(page).to have_content "The source branch will be deleted" end context 'when pipeline succeeds' do diff --git a/spec/features/merge_request/user_resolves_diff_notes_and_discussions_resolve_spec.rb b/spec/features/merge_request/user_resolves_diff_notes_and_discussions_resolve_spec.rb index ba4806821f9..08fa4a98feb 100644 --- a/spec/features/merge_request/user_resolves_diff_notes_and_discussions_resolve_spec.rb +++ b/spec/features/merge_request/user_resolves_diff_notes_and_discussions_resolve_spec.rb @@ -126,6 +126,7 @@ describe 'Merge request > User resolves diff notes and discussions', :js do describe 'side-by-side view' do before do page.within('.merge-request-tabs') { click_link 'Changes' } + find('.js-show-diff-settings').click page.find('#parallel-diff-btn').click end diff --git a/spec/features/merge_request/user_sees_discussions_spec.rb b/spec/features/merge_request/user_sees_discussions_spec.rb index 4ab9a87ad4b..57be1d06708 100644 --- a/spec/features/merge_request/user_sees_discussions_spec.rb +++ b/spec/features/merge_request/user_sees_discussions_spec.rb @@ -88,5 +88,17 @@ describe 'Merge request > User sees discussions', :js do expect(page).to have_content "started a discussion on commit #{note.commit_id[0...7]}" end end + + context 'a commit non-diff discussion' do + let(:note) { create(:discussion_note_on_commit, project: project) } + + it 'displays correct header' do + page.within(find("#note_#{note.id}", match: :first)) do + refresh # Trigger a refresh of notes. + wait_for_requests + expect(page).to have_content "commented on commit #{note.commit_id[0...7]}" + end + end + end end end diff --git a/spec/features/merge_request/user_sees_merge_widget_spec.rb b/spec/features/merge_request/user_sees_merge_widget_spec.rb index d8ebd3c92af..afb978d7c45 100644 --- a/spec/features/merge_request/user_sees_merge_widget_spec.rb +++ b/spec/features/merge_request/user_sees_merge_widget_spec.rb @@ -316,7 +316,7 @@ describe 'Merge request > User sees merge widget', :js do it 'user cannot remove source branch' do expect(page).not_to have_field('remove-source-branch-input') - expect(page).to have_content('Removes source branch') + expect(page).to have_content('Deletes source branch') end end diff --git a/spec/features/merge_request/user_sees_mini_pipeline_graph_spec.rb b/spec/features/merge_request/user_sees_mini_pipeline_graph_spec.rb index 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/merge_request/user_toggles_whitespace_changes_spec.rb b/spec/features/merge_request/user_toggles_whitespace_changes_spec.rb index dd860382daa..0decdfe3a14 100644 --- a/spec/features/merge_request/user_toggles_whitespace_changes_spec.rb +++ b/spec/features/merge_request/user_toggles_whitespace_changes_spec.rb @@ -9,17 +9,23 @@ describe 'Merge request > User toggles whitespace changes', :js do project.add_maintainer(user) sign_in(user) visit diffs_project_merge_request_path(project, merge_request) + + find('.js-show-diff-settings').click end it 'has a button to toggle whitespace changes' do - expect(page).to have_content 'Hide whitespace changes' + expect(page).to have_content 'Show whitespace changes' end describe 'clicking "Hide whitespace changes" button' do it 'toggles the "Hide whitespace changes" button' do - click_link 'Hide whitespace changes' + find('#show-whitespace').click + + visit diffs_project_merge_request_path(project, merge_request) + + find('.js-show-diff-settings').click - expect(page).to have_content 'Show whitespace changes' + expect(find('#show-whitespace')).to be_checked end end end diff --git a/spec/features/merge_request/user_views_diffs_spec.rb b/spec/features/merge_request/user_views_diffs_spec.rb index 7f95a1282f9..0434db04113 100644 --- a/spec/features/merge_request/user_views_diffs_spec.rb +++ b/spec/features/merge_request/user_views_diffs_spec.rb @@ -23,6 +23,8 @@ describe 'User views diffs', :js do end it 'shows diffs' do + find('.js-show-diff-settings').click + expect(page).to have_css('.tab-content #diffs.active') expect(page).to have_css('#parallel-diff-btn', count: 1) expect(page).to have_css('#inline-diff-btn', count: 1) @@ -38,6 +40,8 @@ describe 'User views diffs', :js do context 'when in the side-by-side view' do before do + find('.js-show-diff-settings').click + click_button 'Side-by-side' wait_for_requests diff --git a/spec/features/merge_requests/user_mass_updates_spec.rb b/spec/features/merge_requests/user_mass_updates_spec.rb index cb6603d3f50..e535c7e5811 100644 --- a/spec/features/merge_requests/user_mass_updates_spec.rb +++ b/spec/features/merge_requests/user_mass_updates_spec.rb @@ -68,7 +68,7 @@ describe 'Merge requests > User mass updates', :js do end context 'milestone' do - let(:milestone) { create(:milestone, project: project) } + let(:milestone) { create(:milestone, project: project) } describe 'set milestone' do before do diff --git a/spec/features/merge_requests/user_squashes_merge_request_spec.rb b/spec/features/merge_requests/user_squashes_merge_request_spec.rb index ec1153b7f7f..47f9f10815c 100644 --- a/spec/features/merge_requests/user_squashes_merge_request_spec.rb +++ b/spec/features/merge_requests/user_squashes_merge_request_spec.rb @@ -38,7 +38,7 @@ describe 'User squashes a merge request', :js do def accept_mr expect(page).to have_button('Merge') - uncheck 'Remove source branch' + uncheck 'Delete source branch' click_on 'Merge' end diff --git a/spec/features/milestone_spec.rb b/spec/features/milestone_spec.rb index a0673b12738..6e349395017 100644 --- a/spec/features/milestone_spec.rb +++ b/spec/features/milestone_spec.rb @@ -3,7 +3,7 @@ require 'rails_helper' describe 'Milestone' do let(:group) { create(:group, :public) } let(:project) { create(:project, :public, namespace: group) } - let(:user) { create(:user) } + let(:user) { create(:user) } before do create(:group_member, group: group, user: user) 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/clusters/applications_spec.rb b/spec/features/projects/clusters/applications_spec.rb index fab9e035d53..2c8d014c36d 100644 --- a/spec/features/projects/clusters/applications_spec.rb +++ b/spec/features/projects/clusters/applications_spec.rb @@ -48,9 +48,9 @@ describe 'Clusters Applications', :js do it 'they see status transition' do page.within('.js-cluster-application-row-helm') do - # FE sends request and gets the response, then the buttons is "Install" + # FE sends request and gets the response, then the buttons is "Installing" expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to eq('true') - expect(page).to have_css('.js-cluster-application-install-button', exact_text: 'Install') + expect(page).to have_css('.js-cluster-application-install-button', exact_text: 'Installing') wait_until_helm_created! @@ -118,7 +118,7 @@ describe 'Clusters Applications', :js do page.within('.js-cluster-application-row-cert_manager') do expect(email_form_value).to eq(cluster.user.email) - expect(page).to have_css('.js-cluster-application-install-button', exact_text: 'Install') + expect(page).to have_css('.js-cluster-application-install-button', exact_text: 'Installing') page.find('.js-email').set("new_email@example.org") Clusters::Cluster.last.application_cert_manager.make_installing! @@ -153,9 +153,9 @@ describe 'Clusters Applications', :js do it 'they see status transition' do page.within('.js-cluster-application-row-ingress') do - # FE sends request and gets the response, then the buttons is "Install" + # FE sends request and gets the response, then the buttons is "Installing" expect(page).to have_css('.js-cluster-application-install-button[disabled]') - expect(page).to have_css('.js-cluster-application-install-button', exact_text: 'Install') + expect(page).to have_css('.js-cluster-application-install-button', exact_text: 'Installing') Clusters::Cluster.last.application_ingress.make_installing! diff --git a/spec/features/projects/commit/builds_spec.rb b/spec/features/projects/commit/builds_spec.rb index caf69796d52..d72476f36a9 100644 --- a/spec/features/projects/commit/builds_spec.rb +++ b/spec/features/projects/commit/builds_spec.rb @@ -20,7 +20,7 @@ describe 'project commit pipelines', :js do visit pipelines_project_commit_path(project, project.commit.sha) page.within('.table-holder') do - expect(page).to have_content project.ci_pipelines[0].id # pipeline ids + expect(page).to have_content project.ci_pipelines[0].id # pipeline ids end end end diff --git a/spec/features/projects/commit/cherry_pick_spec.rb b/spec/features/projects/commit/cherry_pick_spec.rb index a61b614dbc8..acfb582dba9 100644 --- a/spec/features/projects/commit/cherry_pick_spec.rb +++ b/spec/features/projects/commit/cherry_pick_spec.rb @@ -4,8 +4,8 @@ describe 'Cherry-pick Commits' do let(:user) { create(:user) } let(:group) { create(:group) } let(:project) { create(:project, :repository, namespace: group) } - let(:master_pickable_commit) { project.commit('7d3b0f7cff5f37573aea97cebfd5692ea1689924') } - let(:master_pickable_merge) { project.commit('e56497bb5f03a90a51293fc6d516788730953899') } + let(:master_pickable_commit) { project.commit('7d3b0f7cff5f37573aea97cebfd5692ea1689924') } + let(:master_pickable_merge) { project.commit('e56497bb5f03a90a51293fc6d516788730953899') } before do sign_in(user) diff --git a/spec/features/projects/deploy_keys_spec.rb b/spec/features/projects/deploy_keys_spec.rb index e12532e97fa..1fa9babaff5 100644 --- a/spec/features/projects/deploy_keys_spec.rb +++ b/spec/features/projects/deploy_keys_spec.rb @@ -20,7 +20,7 @@ describe 'Project deploy keys', :js do page.within(find('.deploy-keys')) do expect(page).to have_selector('.deploy-key', count: 1) - accept_confirm { find('.ic-remove').click() } + accept_confirm { find('.ic-remove').click } wait_for_requests diff --git a/spec/features/projects/files/undo_template_spec.rb b/spec/features/projects/files/undo_template_spec.rb index 5de0bc009fb..fa785ed10ef 100644 --- a/spec/features/projects/files/undo_template_spec.rb +++ b/spec/features/projects/files/undo_template_spec.rb @@ -50,7 +50,7 @@ end def check_content_reverted(template_content) find('.template-selectors-undo-menu .btn-info').click expect(page).not_to have_content(template_content) - expect(find('.template-type-selector .dropdown-toggle-text')).to have_content() + expect(find('.template-type-selector .dropdown-toggle-text')).to have_content end def select_file_template(template_selector_selector, template_name) diff --git a/spec/features/projects/jobs_spec.rb b/spec/features/projects/jobs_spec.rb index 8230396a4cc..24830b2bd3e 100644 --- a/spec/features/projects/jobs_spec.rb +++ b/spec/features/projects/jobs_spec.rb @@ -103,7 +103,7 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do end it 'shows commit`s data', :js do - requests = inspect_requests() do + requests = inspect_requests do visit project_job_path(project, job) end @@ -214,7 +214,7 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do end it 'downloads the zip file when user clicks the download button' do - requests = inspect_requests() do + requests = inspect_requests do click_link 'Download' end @@ -824,7 +824,7 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do before do job.run! visit project_job_path(project, job) - find('.js-cancel-job').click() + find('.js-cancel-job').click end it 'loads the page and shows all needed controls' do @@ -884,7 +884,7 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do end it do - requests = inspect_requests() do + requests = inspect_requests do visit download_project_job_artifacts_path(project, job2) 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..72ef460d315 100644 --- a/spec/features/projects/pipelines/pipeline_spec.rb +++ b/spec/features/projects/pipelines/pipeline_spec.rb @@ -286,6 +286,49 @@ describe 'Pipeline', :js do end end + context 'when a bridge job exists' do + include_context 'pipeline builds' + + let(:project) { create(:project, :repository) } + let(:downstream) { create(:project, :repository) } + + let(:pipeline) do + create(:ci_pipeline, project: project, + ref: 'master', + sha: project.commit.id, + user: user) + end + + let!(:bridge) do + create(:ci_bridge, pipeline: pipeline, + name: 'cross-build', + user: user, + downstream: downstream) + end + + describe 'GET /:project/pipelines/:id' do + before do + visit project_pipeline_path(project, pipeline) + end + + it 'shows the pipeline with a bridge job' do + expect(page).to have_selector('.pipeline-visualization') + expect(page).to have_content('cross-build') + end + end + + describe 'GET /:project/pipelines/:id/builds' do + before do + visit builds_project_pipeline_path(project, pipeline) + end + + it 'shows a bridge job on a list' do + expect(page).to have_content('cross-build') + expect(page).to have_content(bridge.id) + end + end + end + describe 'GET /:project/pipelines/:id/builds' do include_context 'pipeline builds' @@ -477,10 +520,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/pipelines/pipelines_spec.rb b/spec/features/projects/pipelines/pipelines_spec.rb index b75dee66592..ffa165c5440 100644 --- a/spec/features/projects/pipelines/pipelines_spec.rb +++ b/spec/features/projects/pipelines/pipelines_spec.rb @@ -465,7 +465,7 @@ describe 'Pipelines', :js do context 'with pagination' do before do allow(Ci::Pipeline).to receive(:default_per_page).and_return(1) - create(:ci_empty_pipeline, project: project) + create(:ci_empty_pipeline, project: project) end it 'should render pagination' do 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/settings/repository_settings_spec.rb b/spec/features/projects/settings/repository_settings_spec.rb index 1982136b89d..1259ad45791 100644 --- a/spec/features/projects/settings/repository_settings_spec.rb +++ b/spec/features/projects/settings/repository_settings_spec.rb @@ -54,7 +54,7 @@ describe 'Projects > Settings > Repository settings' do project.deploy_keys << private_deploy_key visit project_settings_repository_path(project) - find('.deploy-key', text: private_deploy_key.title).find('.ic-pencil').click() + find('.deploy-key', text: private_deploy_key.title).find('.ic-pencil').click fill_in 'deploy_key_title', with: 'updated_deploy_key' check 'deploy_key_deploy_keys_projects_attributes_0_can_push' @@ -71,14 +71,14 @@ describe 'Projects > Settings > Repository settings' do visit project_settings_repository_path(project) - find('.js-deployKeys-tab-available_project_keys').click() + find('.js-deployKeys-tab-available_project_keys').click - find('.deploy-key', text: private_deploy_key.title).find('.ic-pencil').click() + find('.deploy-key', text: private_deploy_key.title).find('.ic-pencil').click fill_in 'deploy_key_title', with: 'updated_deploy_key' click_button 'Save changes' - find('.js-deployKeys-tab-available_project_keys').click() + find('.js-deployKeys-tab-available_project_keys').click expect(page).to have_content('updated_deploy_key') end @@ -87,7 +87,7 @@ describe 'Projects > Settings > Repository settings' do project.deploy_keys << private_deploy_key visit project_settings_repository_path(project) - accept_confirm { find('.deploy-key', text: private_deploy_key.title).find('.ic-remove').click() } + accept_confirm { find('.deploy-key', text: private_deploy_key.title).find('.ic-remove').click } expect(page).not_to have_content(private_deploy_key.title) end diff --git a/spec/features/projects/settings/user_changes_default_branch_spec.rb b/spec/features/projects/settings/user_changes_default_branch_spec.rb index fcf05e04a5c..7dc18601f50 100644 --- a/spec/features/projects/settings/user_changes_default_branch_spec.rb +++ b/spec/features/projects/settings/user_changes_default_branch_spec.rb @@ -15,6 +15,9 @@ describe 'Projects > Settings > User changes default branch' do let(:project) { create(:project, :repository, namespace: user.namespace) } it 'allows to change the default branch', :js do + # Otherwise, running JS may overwrite our change to project_default_branch + wait_for_requests + select2('fix', from: '#project_default_branch') page.within '#default-branch-settings' do diff --git a/spec/features/projects/snippets/user_comments_on_snippet_spec.rb b/spec/features/projects/snippets/user_comments_on_snippet_spec.rb index d82e350e0f7..9c1ef78b0ca 100644 --- a/spec/features/projects/snippets/user_comments_on_snippet_spec.rb +++ b/spec/features/projects/snippets/user_comments_on_snippet_spec.rb @@ -31,7 +31,7 @@ describe 'Projects > Snippets > User comments on a snippet', :js do end it 'should have zen mode' do - find('.js-zen-enter').click() + find('.js-zen-enter').click expect(page).to have_selector('.fullscreen') end end diff --git a/spec/features/projects/tags/user_edits_tags_spec.rb b/spec/features/projects/tags/user_edits_tags_spec.rb new file mode 100644 index 00000000000..ebb2844d17f --- /dev/null +++ b/spec/features/projects/tags/user_edits_tags_spec.rb @@ -0,0 +1,82 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe 'Project > Tags', :js do + include DropzoneHelper + + let(:user) { create(:user) } + let(:role) { :developer } + let(:project) { create(:project, :repository) } + + before do + sign_in(user) + project.add_role(user, role) + end + + describe 'when opening project tags' do + before do + visit project_tags_path(project) + end + + context 'page with tags list' do + it 'shows tag name' do + page.within first('.tags > .content-list > li') do + expect(page.find('.row-main-content')).to have_content 'v1.1.0 Version 1.1.0' + end + end + + it 'shows tag edit button' do + page.within first('.tags > .content-list > li') do + edit_btn = page.find('.row-fixed-content.controls a.btn-edit') + + expect(edit_btn['href']).to have_content '/tags/v1.1.0/release/edit' + end + end + end + + context 'edit tag release notes' do + before do + find('.tags > .content-list > li:first-child .row-fixed-content.controls a.btn-edit').click + end + + it 'shows tag name header' do + page.within('.content') do + expect(page.find('.sub-header-block')).to have_content 'Release notes for tag v1.1.0' + end + end + + it 'shows release notes form' do + page.within('.content') do + expect(page).to have_selector('form.release-form') + end + end + + it 'toolbar buttons on release notes form are functional' do + page.within('.content form.release-form') do + note_textarea = page.find('.js-gfm-input') + + # Click on Bold button + page.find('.md-header-toolbar button.toolbar-btn:first-child').click + + expect(note_textarea.value).to eq('****') + end + end + + it 'release notes form shows "Attach a file" button', :js do + page.within('.content form.release-form') do + expect(page).to have_button('Attach a file') + expect(page).not_to have_selector('.uploading-progress-container', visible: true) + end + end + + it 'shows "Attaching a file" message on uploading 1 file', :js do + slow_requests do + dropzone_file([Rails.root.join('spec', 'fixtures', 'dk.png')], 0, false) + + expect(page).to have_selector('.attaching-file-message', visible: true, text: 'Attaching a file -') + end + end + end + end +end diff --git a/spec/features/projects_spec.rb b/spec/features/projects_spec.rb index eb70a3c41c1..f7efc3f325c 100644 --- a/spec/features/projects_spec.rb +++ b/spec/features/projects_spec.rb @@ -5,7 +5,7 @@ describe 'Project' do include MobileHelpers describe 'creating from template' do - let(:user) { create(:user) } + let(:user) { create(:user) } let(:template) { Gitlab::ProjectTemplate.find(:rails) } before do @@ -55,30 +55,30 @@ describe 'Project' do it 'parses Markdown' do project.update_attribute(:description, 'This is **my** project') visit path - expect(page).to have_css('.project-description > .project-description-markdown > p > strong') + expect(page).to have_css('.home-panel-description > .home-panel-description-markdown > p > strong') end it 'passes through html-pipeline' do project.update_attribute(:description, 'This project is the :poop:') visit path - expect(page).to have_css('.project-description > .project-description-markdown > p > gl-emoji') + expect(page).to have_css('.home-panel-description > .home-panel-description-markdown > p > gl-emoji') end it 'sanitizes unwanted tags' do project.update_attribute(:description, "```\ncode\n```") visit path - expect(page).not_to have_css('.project-description code') + expect(page).not_to have_css('.home-panel-description code') end it 'permits `rel` attribute on links' do project.update_attribute(:description, 'https://google.com/') visit path - expect(page).to have_css('.project-description a[rel]') + expect(page).to have_css('.home-panel-description a[rel]') end context 'read more', :js do let(:read_more_selector) { '.read-more-container' } - let(:read_more_trigger_selector) { '.project-home-desc .js-read-more-trigger' } + let(:read_more_trigger_selector) { '.home-panel-home-desc .js-read-more-trigger' } it 'does not display "read more" link on desktop breakpoint' do project.update_attribute(:description, 'This is **my** project') @@ -94,7 +94,7 @@ describe 'Project' do find(read_more_trigger_selector).click - expect(page).to have_css('.project-description .is-expanded') + expect(page).to have_css('.home-panel-description .is-expanded') end end end @@ -111,14 +111,14 @@ describe 'Project' do 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_css('.home-panel-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_css('.home-panel-topic-list') expect(page).to have_content('topic1, topic2, topic3 + 1 more') end end @@ -170,7 +170,7 @@ describe 'Project' do describe 'showing information about source of a project fork' do let(:user) { create(:user) } - let(:base_project) { create(:project, :public, :repository) } + let(:base_project) { create(:project, :public, :repository) } let(:forked_project) { fork_project(base_project, user, repository: true) } before do diff --git a/spec/features/protected_branches_spec.rb b/spec/features/protected_branches_spec.rb index 63c38a25f4b..0aff916ec83 100644 --- a/spec/features/protected_branches_spec.rb +++ b/spec/features/protected_branches_spec.rb @@ -97,7 +97,7 @@ describe 'Protected Branches', :js do set_protected_branch_name('some-branch') click_on "Protect" - within(".protected-branches-list") { expect(page).to have_content('branch was removed') } + within(".protected-branches-list") { expect(page).to have_content('branch was deleted') } end end diff --git a/spec/features/security/admin_access_spec.rb b/spec/features/security/admin_access_spec.rb index 3ca1303bda6..ff679034a36 100644 --- a/spec/features/security/admin_access_spec.rb +++ b/spec/features/security/admin_access_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe "Admin::Projects" do +describe "Admin::Projects" do include AccessMatchers describe "GET /admin/projects" do diff --git a/spec/features/security/dashboard_access_spec.rb b/spec/features/security/dashboard_access_spec.rb index 0c893e65d9c..07cddc92ac4 100644 --- a/spec/features/security/dashboard_access_spec.rb +++ b/spec/features/security/dashboard_access_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe "Dashboard access" do +describe "Dashboard access" do include AccessMatchers describe "GET /dashboard" do diff --git a/spec/features/security/profile_access_spec.rb b/spec/features/security/profile_access_spec.rb index 41eb7b26578..a198e65046f 100644 --- a/spec/features/security/profile_access_spec.rb +++ b/spec/features/security/profile_access_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe "Profile access" do +describe "Profile access" do include AccessMatchers describe "GET /profile/keys" do diff --git a/spec/features/security/project/internal_access_spec.rb b/spec/features/security/project/internal_access_spec.rb index 001e6c10eb2..e23000fa676 100644 --- a/spec/features/security/project/internal_access_spec.rb +++ b/spec/features/security/project/internal_access_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe "Internal Project Access" do +describe "Internal Project Access" do include AccessMatchers set(:project) { create(:project, :internal, :repository) } @@ -452,9 +452,9 @@ describe "Internal Project Access" do it { is_expected.to be_allowed_for(:owner).of(project) } it { is_expected.to be_allowed_for(:maintainer).of(project) } it { is_expected.to be_allowed_for(:developer).of(project) } - it { is_expected.to be_allowed_for(:reporter).of(project) } - it { is_expected.to be_allowed_for(:guest).of(project) } - it { is_expected.to be_allowed_for(:user) } + it { is_expected.to be_denied_for(:reporter).of(project) } + it { is_expected.to be_denied_for(:guest).of(project) } + it { is_expected.to be_denied_for(:user) } it { is_expected.to be_denied_for(:external) } it { is_expected.to be_denied_for(:visitor) } end diff --git a/spec/features/security/project/private_access_spec.rb b/spec/features/security/project/private_access_spec.rb index c6618355eea..f380bc122a7 100644 --- a/spec/features/security/project/private_access_spec.rb +++ b/spec/features/security/project/private_access_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe "Private Project Access" do +describe "Private Project Access" do include AccessMatchers set(:project) { create(:project, :private, :repository, public_builds: false) } @@ -485,7 +485,7 @@ describe "Private Project Access" do it { is_expected.to be_allowed_for(:owner).of(project) } it { is_expected.to be_allowed_for(:maintainer).of(project) } it { is_expected.to be_allowed_for(:developer).of(project) } - it { is_expected.to be_allowed_for(:reporter).of(project) } + it { is_expected.to be_denied_for(:reporter).of(project) } it { is_expected.to be_denied_for(:guest).of(project) } it { is_expected.to be_denied_for(:user) } it { is_expected.to be_denied_for(:external) } diff --git a/spec/features/security/project/public_access_spec.rb b/spec/features/security/project/public_access_spec.rb index 3717dc13f1e..57d56371719 100644 --- a/spec/features/security/project/public_access_spec.rb +++ b/spec/features/security/project/public_access_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe "Public Project Access" do +describe "Public Project Access" do include AccessMatchers set(:project) { create(:project, :public, :repository) } @@ -272,11 +272,11 @@ describe "Public Project Access" do it { is_expected.to be_allowed_for(:owner).of(project) } it { is_expected.to be_allowed_for(:maintainer).of(project) } it { is_expected.to be_allowed_for(:developer).of(project) } - it { is_expected.to be_allowed_for(:reporter).of(project) } - it { is_expected.to be_allowed_for(:guest).of(project) } - it { is_expected.to be_allowed_for(:user) } - it { is_expected.to be_allowed_for(:external) } - it { is_expected.to be_allowed_for(:visitor) } + it { is_expected.to be_denied_for(:reporter).of(project) } + it { is_expected.to be_denied_for(:guest).of(project) } + it { is_expected.to be_denied_for(:user) } + it { is_expected.to be_denied_for(:external) } + it { is_expected.to be_denied_for(:visitor) } end describe "GET /:project_path/environments" do diff --git a/spec/features/security/project/snippet/internal_access_spec.rb b/spec/features/security/project/snippet/internal_access_spec.rb index b87eb86b88b..0c58fdf2f12 100644 --- a/spec/features/security/project/snippet/internal_access_spec.rb +++ b/spec/features/security/project/snippet/internal_access_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe "Internal Project Snippets Access" do +describe "Internal Project Snippets Access" do include AccessMatchers let(:project) { create(:project, :internal) } diff --git a/spec/features/security/project/snippet/private_access_spec.rb b/spec/features/security/project/snippet/private_access_spec.rb index ead91d9a5fa..420f1938763 100644 --- a/spec/features/security/project/snippet/private_access_spec.rb +++ b/spec/features/security/project/snippet/private_access_spec.rb @@ -1,11 +1,11 @@ require 'spec_helper' -describe "Private Project Snippets Access" do +describe "Private Project Snippets Access" do include AccessMatchers let(:project) { create(:project, :private) } - let(:private_snippet) { create(:project_snippet, :private, project: project, author: project.owner) } + let(:private_snippet) { create(:project_snippet, :private, project: project, author: project.owner) } describe "GET /:project_path/snippets" do subject { project_snippets_path(project) } diff --git a/spec/features/security/project/snippet/public_access_spec.rb b/spec/features/security/project/snippet/public_access_spec.rb index 9bab3a474b8..6c75902c6e9 100644 --- a/spec/features/security/project/snippet/public_access_spec.rb +++ b/spec/features/security/project/snippet/public_access_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe "Public Project Snippets Access" do +describe "Public Project Snippets Access" do include AccessMatchers let(:project) { create(:project, :public) } diff --git a/spec/features/users/terms_spec.rb b/spec/features/users/terms_spec.rb index 5b2e7605c4d..84df1016594 100644 --- a/spec/features/users/terms_spec.rb +++ b/spec/features/users/terms_spec.rb @@ -76,7 +76,7 @@ describe 'Users > Terms' do project.add_developer(user) end - it 'redirects to terms and back to where the user was going' do + it 'redirects to terms and back to where the user was going' do visit project_path(project) enforce_terms diff --git a/spec/finders/contributed_projects_finder_spec.rb b/spec/finders/contributed_projects_finder_spec.rb index 81fb4e3561c..ee84fd067d4 100644 --- a/spec/finders/contributed_projects_finder_spec.rb +++ b/spec/finders/contributed_projects_finder_spec.rb @@ -31,4 +31,16 @@ describe ContributedProjectsFinder do it { is_expected.to match_array([private_project, internal_project, public_project]) } end + + context 'user with private profile' do + it 'does not return contributed projects' do + private_user = create(:user, private_profile: true) + public_project.add_maintainer(private_user) + create(:push_event, project: public_project, author: private_user) + + projects = described_class.new(private_user).execute(current_user) + + expect(projects).to be_empty + end + end end diff --git a/spec/finders/groups_finder_spec.rb b/spec/finders/groups_finder_spec.rb index 16c0d418d98..367ca43bdfe 100644 --- a/spec/finders/groups_finder_spec.rb +++ b/spec/finders/groups_finder_spec.rb @@ -8,22 +8,22 @@ describe GroupsFinder do using RSpec::Parameterized::TableSyntax where(:user_type, :params, :results) do - nil | { all_available: true } | %i(public_group user_public_group) - nil | { all_available: false } | %i(public_group user_public_group) + nil | { all_available: true } | %i(public_group user_public_group) + nil | { all_available: false } | %i(public_group user_public_group) nil | {} | %i(public_group user_public_group) - :regular | { all_available: true } | %i(public_group internal_group user_public_group user_internal_group - user_private_group) - :regular | { all_available: false } | %i(user_public_group user_internal_group user_private_group) + :regular | { all_available: true } | %i(public_group internal_group user_public_group user_internal_group + user_private_group) + :regular | { all_available: false } | %i(user_public_group user_internal_group user_private_group) :regular | {} | %i(public_group internal_group user_public_group user_internal_group user_private_group) - :external | { all_available: true } | %i(public_group user_public_group user_internal_group user_private_group) - :external | { all_available: false } | %i(user_public_group user_internal_group user_private_group) + :external | { all_available: true } | %i(public_group user_public_group user_internal_group user_private_group) + :external | { all_available: false } | %i(user_public_group user_internal_group user_private_group) :external | {} | %i(public_group user_public_group user_internal_group user_private_group) - :admin | { all_available: true } | %i(public_group internal_group private_group user_public_group - user_internal_group user_private_group) - :admin | { all_available: false } | %i(user_public_group user_internal_group user_private_group) + :admin | { all_available: true } | %i(public_group internal_group private_group user_public_group + user_internal_group user_private_group) + :admin | { all_available: false } | %i(user_public_group user_internal_group user_private_group) :admin | {} | %i(public_group internal_group private_group user_public_group user_internal_group user_private_group) end diff --git a/spec/finders/merge_request_target_project_finder_spec.rb b/spec/finders/merge_request_target_project_finder_spec.rb index f302cf80ce8..d26a75179de 100644 --- a/spec/finders/merge_request_target_project_finder_spec.rb +++ b/spec/finders/merge_request_target_project_finder_spec.rb @@ -7,7 +7,7 @@ describe MergeRequestTargetProjectFinder do subject(:finder) { described_class.new(current_user: user, source_project: forked_project) } shared_examples 'finding related projects' do - it 'finds sibling projects and base project' do + it 'finds sibling projects and base project' do other_fork expect(finder.execute).to contain_exactly(base_project, other_fork, forked_project) diff --git a/spec/finders/merge_requests_finder_spec.rb b/spec/finders/merge_requests_finder_spec.rb index ff4c6b8dd42..107da08a0a9 100644 --- a/spec/finders/merge_requests_finder_spec.rb +++ b/spec/finders/merge_requests_finder_spec.rb @@ -68,20 +68,34 @@ describe MergeRequestsFinder do expect(merge_requests.size).to eq(2) end - it 'filters by group' do - params = { group_id: group.id } + context 'filtering by group' do + it 'includes all merge requests when user has access' do + params = { group_id: group.id } - merge_requests = described_class.new(user, params).execute + merge_requests = described_class.new(user, params).execute - expect(merge_requests.size).to eq(3) - end + expect(merge_requests.size).to eq(3) + end - it 'filters by group including subgroups', :nested_groups do - params = { group_id: group.id, include_subgroups: true } + it 'excludes merge requests from projects the user does not have access to' do + private_project = create_project_without_n_plus_1(:private, group: group) + private_mr = create(:merge_request, :simple, author: user, source_project: private_project, target_project: private_project) + params = { group_id: group.id } - merge_requests = described_class.new(user, params).execute + private_project.add_guest(user) + merge_requests = described_class.new(user, params).execute - expect(merge_requests.size).to eq(6) + expect(merge_requests.size).to eq(3) + expect(merge_requests).not_to include(private_mr) + end + + it 'filters by group including subgroups', :nested_groups do + params = { group_id: group.id, include_subgroups: true } + + merge_requests = described_class.new(user, params).execute + + expect(merge_requests.size).to eq(6) + end end it 'filters by non_archived' do diff --git a/spec/finders/milestones_finder_spec.rb b/spec/finders/milestones_finder_spec.rb index 656d120311a..ecffbb9e197 100644 --- a/spec/finders/milestones_finder_spec.rb +++ b/spec/finders/milestones_finder_spec.rb @@ -69,6 +69,12 @@ describe MilestonesFinder do expect(result.to_a).to contain_exactly(milestone_1) end + + it 'filters by search_title' do + result = described_class.new(params.merge(search_title: 'one t')).execute + + expect(result.to_a).to contain_exactly(milestone_1) + end end describe '#find_by' do diff --git a/spec/finders/notes_finder_spec.rb b/spec/finders/notes_finder_spec.rb index b51f1955ac4..0a685152cf9 100644 --- a/spec/finders/notes_finder_spec.rb +++ b/spec/finders/notes_finder_spec.rb @@ -121,7 +121,7 @@ describe NotesFinder do let(:note1) { create :note_on_commit, project: project } let(:note2) { create :note_on_commit, project: project } let(:commit) { note1.noteable } - let(:params) { { target_id: commit.id, target_type: 'commit', last_fetched_at: 1.hour.ago.to_i } } + let(:params) { { target_id: commit.id, target_type: 'commit', last_fetched_at: 1.hour.ago.to_i } } before do note1 diff --git a/spec/finders/projects_finder_spec.rb b/spec/finders/projects_finder_spec.rb index 590e838f13e..ac866e49fcd 100644 --- a/spec/finders/projects_finder_spec.rb +++ b/spec/finders/projects_finder_spec.rb @@ -137,7 +137,7 @@ describe ProjectsFinder do end describe 'filter by trending' do - let!(:trending_project) { create(:trending_project, project: public_project) } + let!(:trending_project) { create(:trending_project, project: public_project) } let(:params) { { trending: true } } it { is_expected.to eq([public_project]) } diff --git a/spec/finders/snippets_finder_spec.rb b/spec/finders/snippets_finder_spec.rb index dfeeb3040c6..134fb5f2c04 100644 --- a/spec/finders/snippets_finder_spec.rb +++ b/spec/finders/snippets_finder_spec.rb @@ -107,7 +107,7 @@ describe SnippetsFinder do context 'filter by project' do let(:user) { create :user } let(:group) { create :group, :public } - let(:project1) { create(:project, :public, group: group) } + let(:project1) { create(:project, :public, group: group) } before do @snippet1 = create(:project_snippet, :private, project: project1) diff --git a/spec/fixtures/api/schemas/cluster_status.json b/spec/fixtures/api/schemas/cluster_status.json index 3d9e0628f63..138a6c5ed6b 100644 --- a/spec/fixtures/api/schemas/cluster_status.json +++ b/spec/fixtures/api/schemas/cluster_status.json @@ -30,6 +30,7 @@ ] } }, + "version": { "type": "string" }, "status_reason": { "type": ["string", "null"] }, "external_ip": { "type": ["string", "null"] }, "hostname": { "type": ["string", "null"] }, diff --git a/spec/fixtures/api/schemas/error_tracking/list_projects.json b/spec/fixtures/api/schemas/error_tracking/list_projects.json new file mode 100644 index 00000000000..2aaa525e38f --- /dev/null +++ b/spec/fixtures/api/schemas/error_tracking/list_projects.json @@ -0,0 +1,13 @@ +{ + "type": "object", + "required": [ + "projects" + ], + "properties": { + "projects": { + "type": "array", + "items": { "$ref": "project.json" } + } + }, + "additionalProperties": false +} diff --git a/spec/fixtures/api/schemas/error_tracking/project.json b/spec/fixtures/api/schemas/error_tracking/project.json new file mode 100644 index 00000000000..f6d611133c7 --- /dev/null +++ b/spec/fixtures/api/schemas/error_tracking/project.json @@ -0,0 +1,19 @@ +{ + "type": "object", + "required" : [ + "id", + "slug", + "organization_slug", + "name" + ], + "properties" : { + "id": { "type": "string"}, + "name": { "type": "string" }, + "slug": { "type": "string" }, + "status": { "type": "string" }, + "organization_name": { "type": "string" }, + "organization_slug": { "type": "string" }, + "organization_id": { "type": "string" } + }, + "additionalProperties": false +} diff --git a/spec/fixtures/api/schemas/registry/repository.json b/spec/fixtures/api/schemas/registry/repository.json index 4175642eb00..e0fd4620c43 100644 --- a/spec/fixtures/api/schemas/registry/repository.json +++ b/spec/fixtures/api/schemas/registry/repository.json @@ -2,20 +2,27 @@ "type": "object", "required" : [ "id", + "name", "path", "location", - "tags_path" + "created_at" ], "properties" : { "id": { "type": "integer" }, + "name": { + "type": "string" + }, "path": { "type": "string" }, "location": { "type": "string" }, + "created_at": { + "type": "date-time" + }, "tags_path": { "type": "string" }, diff --git a/spec/fixtures/api/schemas/registry/tag.json b/spec/fixtures/api/schemas/registry/tag.json index 3a2c88791e1..48f8402b65b 100644 --- a/spec/fixtures/api/schemas/registry/tag.json +++ b/spec/fixtures/api/schemas/registry/tag.json @@ -2,15 +2,22 @@ "type": "object", "required" : [ "name", + "path", "location" ], "properties" : { "name": { "type": "string" }, + "path": { + "type": "string" + }, "location": { "type": "string" }, + "digest": { + "type": "string" + }, "revision": { "type": "string" }, 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/fixtures/pages_non_writeable.zip b/spec/fixtures/pages_non_writeable.zip Binary files differnew file mode 100644 index 00000000000..69f175d8504 --- /dev/null +++ b/spec/fixtures/pages_non_writeable.zip diff --git a/spec/fixtures/safe_zip/invalid-symlink-does-not-exist.zip b/spec/fixtures/safe_zip/invalid-symlink-does-not-exist.zip Binary files differnew file mode 100644 index 00000000000..b9ae1548713 --- /dev/null +++ b/spec/fixtures/safe_zip/invalid-symlink-does-not-exist.zip diff --git a/spec/fixtures/safe_zip/invalid-symlinks-outside.zip b/spec/fixtures/safe_zip/invalid-symlinks-outside.zip Binary files differnew file mode 100644 index 00000000000..c184a1dafe2 --- /dev/null +++ b/spec/fixtures/safe_zip/invalid-symlinks-outside.zip diff --git a/spec/fixtures/safe_zip/valid-non-writeable.zip b/spec/fixtures/safe_zip/valid-non-writeable.zip Binary files differnew file mode 100644 index 00000000000..69f175d8504 --- /dev/null +++ b/spec/fixtures/safe_zip/valid-non-writeable.zip diff --git a/spec/fixtures/safe_zip/valid-simple.zip b/spec/fixtures/safe_zip/valid-simple.zip Binary files differnew file mode 100644 index 00000000000..a56b8b41dcc --- /dev/null +++ b/spec/fixtures/safe_zip/valid-simple.zip diff --git a/spec/fixtures/safe_zip/valid-symlinks-first.zip b/spec/fixtures/safe_zip/valid-symlinks-first.zip Binary files differnew file mode 100644 index 00000000000..f5952ef71c9 --- /dev/null +++ b/spec/fixtures/safe_zip/valid-symlinks-first.zip diff --git a/spec/fixtures/sentry/list_projects_sample_response.json b/spec/fixtures/sentry/list_projects_sample_response.json new file mode 100644 index 00000000000..fd79b0d0f30 --- /dev/null +++ b/spec/fixtures/sentry/list_projects_sample_response.json @@ -0,0 +1,81 @@ +[ + { + "status": "active", + "features": [ + "data-forwarding", + "rate-limits", + "releases" + ], + "color": "#5c3fbf", + "isInternal": false, + "isPublic": false, + "dateCreated": "2018-12-11T10:41:22.476Z", + "id": "2", + "slug": "sentry-example", + "name": "sentry-example", + "hasAccess": true, + "isBookmarked": false, + "platform": "node", + "firstEvent": "2018-12-12T15:07:18Z", + "avatar": { + "avatarUuid": null, + "avatarType": "letter_avatar" + }, + "isMember": true, + "organization": { + "status": { + "id": "active", + "name": "active" + }, + "require2FA": false, + "avatar": { + "avatarUuid": null, + "avatarType": "letter_avatar" + }, + "name": "Sentry", + "dateCreated": "2018-12-11T10:21:47.431Z", + "id": "1", + "isEarlyAdopter": false, + "slug": "sentry" + } + }, + { + "status": "active", + "features": [ + "data-forwarding", + "rate-limits" + ], + "color": "#bf873f", + "isInternal": true, + "isPublic": false, + "dateCreated": "2018-12-11T10:21:47.440Z", + "id": "1", + "slug": "internal", + "name": "Internal", + "hasAccess": true, + "isBookmarked": false, + "platform": null, + "firstEvent": "2018-12-11T10:54:35Z", + "avatar": { + "avatarUuid": null, + "avatarType": "letter_avatar" + }, + "isMember": true, + "organization": { + "status": { + "id": "active", + "name": "active" + }, + "require2FA": false, + "avatar": { + "avatarUuid": null, + "avatarType": "letter_avatar" + }, + "name": "Sentry", + "dateCreated": "2018-12-11T10:21:47.431Z", + "id": "1", + "isEarlyAdopter": false, + "slug": "sentry" + } + } +] diff --git a/spec/graphql/resolvers/project_pipelines_resolver_spec.rb b/spec/graphql/resolvers/project_pipelines_resolver_spec.rb index 407ca2f9d78..6862ae8a5ed 100644 --- a/spec/graphql/resolvers/project_pipelines_resolver_spec.rb +++ b/spec/graphql/resolvers/project_pipelines_resolver_spec.rb @@ -4,7 +4,7 @@ describe Resolvers::ProjectPipelinesResolver do include GraphqlHelpers set(:project) { create(:project) } - set(:pipeline) { create(:ci_pipeline, project: project) } + set(:pipeline) { create(:ci_pipeline, project: project) } set(:other_pipeline) { create(:ci_pipeline) } let(:current_user) { create(:user) } diff --git a/spec/graphql/types/permission_types/base_permission_type_spec.rb b/spec/graphql/types/permission_types/base_permission_type_spec.rb index a7e51797047..0ee8b883d51 100644 --- a/spec/graphql/types/permission_types/base_permission_type_spec.rb +++ b/spec/graphql/types/permission_types/base_permission_type_spec.rb @@ -8,7 +8,7 @@ describe Types::PermissionTypes::BasePermissionType do Class.new(described_class) do graphql_name 'TestClass' - permission_field :do_stuff, resolve: -> (_, _, _) { true } + permission_field :do_stuff, resolve: -> (_, _, _) { true } ability_field(:read_issue) abilities :admin_issue end diff --git a/spec/graphql/types/permission_types/project_spec.rb b/spec/graphql/types/permission_types/project_spec.rb index 927153adc5b..4288412eda3 100644 --- a/spec/graphql/types/permission_types/project_spec.rb +++ b/spec/graphql/types/permission_types/project_spec.rb @@ -5,7 +5,7 @@ describe Types::PermissionTypes::Project do expected_permissions = [ :change_namespace, :change_visibility_level, :rename_project, :remove_project, :archive_project, :remove_fork_project, :remove_pages, :read_project, :create_merge_request_in, - :read_wiki, :read_project_member, :create_issue, :upload_file, :read_cycle_analytics, + :read_wiki, :read_project_member, :create_issue, :upload_file, :read_cycle_analytics, :download_code, :download_wiki_code, :fork_project, :create_project_snippet, :read_commit_status, :request_access, :create_pipeline, :create_pipeline_schedule, :create_merge_request_from, :create_wiki, :push_code, :create_deployment, :push_to_delete_protected_branch, diff --git a/spec/graphql/types/project_type_spec.rb b/spec/graphql/types/project_type_spec.rb index 61d4c42665a..01d71abfac9 100644 --- a/spec/graphql/types/project_type_spec.rb +++ b/spec/graphql/types/project_type_spec.rb @@ -10,7 +10,7 @@ describe GitlabSchema.types['Project'] do it 'authorizes the merge request' do expect(described_class.fields['mergeRequest']) - .to require_graphql_authorizations(:read_merge_request) + .to require_graphql_authorizations(:read_merge_request) end end 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/helpers/emails_helper_spec.rb b/spec/helpers/emails_helper_spec.rb index 3820cf5cb9d..23d7e41803e 100644 --- a/spec/helpers/emails_helper_spec.rb +++ b/spec/helpers/emails_helper_spec.rb @@ -1,6 +1,20 @@ require 'spec_helper' describe EmailsHelper do + describe 'sanitize_name' do + context 'when name contains a valid URL string' do + it 'returns name with `.` replaced with `_` to prevent mail clients from auto-linking URLs' do + expect(sanitize_name('https://about.gitlab.com')).to eq('https://about_gitlab_com') + expect(sanitize_name('www.gitlab.com')).to eq('www_gitlab_com') + expect(sanitize_name('//about.gitlab.com/handbook/security/#best-practices')).to eq('//about_gitlab_com/handbook/security/#best-practices') + end + + it 'returns name as it is when it does not contain a URL' do + expect(sanitize_name('Foo Bar')).to eq('Foo Bar') + end + end + end + describe 'password_reset_token_valid_time' do def validate_time_string(time_limit, expected_string) Devise.reset_password_within = time_limit diff --git a/spec/helpers/import_helper_spec.rb b/spec/helpers/import_helper_spec.rb index cb0ea4e26ba..af4931e3370 100644 --- a/spec/helpers/import_helper_spec.rb +++ b/spec/helpers/import_helper_spec.rb @@ -2,6 +2,10 @@ require 'rails_helper' describe ImportHelper do describe '#sanitize_project_name' do + it 'removes leading tildes' do + expect(helper.sanitize_project_name('~~root')).to eq('root') + end + it 'removes whitespace' do expect(helper.sanitize_project_name('my test repo')).to eq('my-test-repo') end diff --git a/spec/helpers/issuables_helper_spec.rb b/spec/helpers/issuables_helper_spec.rb index 81231cca085..03e3a72a82f 100644 --- a/spec/helpers/issuables_helper_spec.rb +++ b/spec/helpers/issuables_helper_spec.rb @@ -5,8 +5,8 @@ describe IssuablesHelper do let(:label2) { build_stubbed(:label) } describe '#users_dropdown_label' do - let(:user) { build_stubbed(:user) } - let(:user2) { build_stubbed(:user) } + let(:user) { build_stubbed(:user) } + let(:user2) { build_stubbed(:user) } it 'returns unassigned' do expect(users_dropdown_label([])).to eq('Unassigned') @@ -22,7 +22,7 @@ describe IssuablesHelper do end describe '#group_dropdown_label' do - let(:group) { create(:group) } + let(:group) { create(:group) } let(:default) { 'default label' } it 'returns default group label when group_id is nil' do @@ -173,6 +173,7 @@ describe IssuablesHelper do before do allow(helper).to receive(:current_user).and_return(user) allow(helper).to receive(:can?).and_return(true) + stub_commonmark_sourcepos_disabled end it 'returns the correct json for an issue' do diff --git a/spec/helpers/issues_helper_spec.rb b/spec/helpers/issues_helper_spec.rb index 8bb2e234e9a..039143eb8d7 100644 --- a/spec/helpers/issues_helper_spec.rb +++ b/spec/helpers/issues_helper_spec.rb @@ -148,7 +148,7 @@ describe IssuesHelper do end describe "when passing a discussion" do - let(:diff_note) { create(:diff_note_on_merge_request) } + let(:diff_note) { create(:diff_note_on_merge_request) } let(:merge_request) { diff_note.noteable } let(:discussion) { diff_note.to_discussion } diff --git a/spec/helpers/members_helper_spec.rb b/spec/helpers/members_helper_spec.rb index 4590904c93d..908e8960f37 100644 --- a/spec/helpers/members_helper_spec.rb +++ b/spec/helpers/members_helper_spec.rb @@ -16,7 +16,7 @@ describe MembersHelper do it { expect(remove_member_message(project_member_invite)).to eq "Are you sure you want to revoke the invitation for #{project_member_invite.invite_email} to join the #{project.full_name} project?" } it { expect(remove_member_message(project_member_request)).to eq "Are you sure you want to deny #{requester.name}'s request to join the #{project.full_name} project?" } it { expect(remove_member_message(project_member_request, user: requester)).to eq "Are you sure you want to withdraw your access request for the #{project.full_name} project?" } - it { expect(remove_member_message(group_member)).to eq "Are you sure you want to remove #{group_member.user.name} from the #{group.name} group?" } + it { expect(remove_member_message(group_member)).to eq "Are you sure you want to remove #{group_member.user.name} from the #{group.name} group and any subresources?" } it { expect(remove_member_message(group_member_invite)).to eq "Are you sure you want to revoke the invitation for #{group_member_invite.invite_email} to join the #{group.name} group?" } it { expect(remove_member_message(group_member_request)).to eq "Are you sure you want to deny #{requester.name}'s request to join the #{group.name} group?" } it { expect(remove_member_message(group_member_request, user: requester)).to eq "Are you sure you want to withdraw your access request for the #{group.name} group?" } @@ -33,7 +33,7 @@ describe MembersHelper do it { expect(remove_member_title(project_member)).to eq 'Remove user from project' } it { expect(remove_member_title(project_member_request)).to eq 'Deny access request from project' } - it { expect(remove_member_title(group_member)).to eq 'Remove user from group' } + it { expect(remove_member_title(group_member)).to eq 'Remove user from group and any subresources' } it { expect(remove_member_title(group_member_request)).to eq 'Deny access request from group' } end diff --git a/spec/helpers/notes_helper_spec.rb b/spec/helpers/notes_helper_spec.rb index 21461e46cf4..0715f34dafe 100644 --- a/spec/helpers/notes_helper_spec.rb +++ b/spec/helpers/notes_helper_spec.rb @@ -185,8 +185,8 @@ describe NotesHelper do context 'for a non-diff discussion' do let(:discussion) { create(:discussion_note_on_commit, project: project).to_discussion } - it 'returns the commit path' do - expect(helper.discussion_path(discussion)).to eq(project_commit_path(project, commit)) + it 'returns the commit path with the note anchor' do + expect(helper.discussion_path(discussion)).to eq(project_commit_path(project, commit, anchor: "note_#{discussion.first_note.id}")) end end end diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb index 88b5d87f087..10f61731206 100644 --- a/spec/helpers/projects_helper_spec.rb +++ b/spec/helpers/projects_helper_spec.rb @@ -354,8 +354,40 @@ describe ProjectsHelper do allow(project).to receive(:builds_enabled?).and_return(false) end - it "do not include pipelines tab" do - is_expected.not_to include(:pipelines) + context 'when user has access to builds' do + it "does include pipelines tab" do + is_expected.to include(:pipelines) + end + end + + context 'when user does not have access to builds' do + before do + allow(helper).to receive(:can?) { false } + end + + it "does not include pipelines tab" do + is_expected.not_to include(:pipelines) + end + end + end + + context 'when project has external wiki' do + before do + allow(project).to receive(:has_external_wiki?).and_return(true) + end + + it 'includes external wiki tab' do + is_expected.to include(:external_wiki) + end + end + + context 'when project does not have external wiki' do + before do + allow(project).to receive(:has_external_wiki?).and_return(false) + end + + it 'does not include external wiki tab' do + is_expected.not_to include(:external_wiki) end end end diff --git a/spec/helpers/search_helper_spec.rb b/spec/helpers/search_helper_spec.rb index 4945749f524..9cff0291250 100644 --- a/spec/helpers/search_helper_spec.rb +++ b/spec/helpers/search_helper_spec.rb @@ -18,7 +18,7 @@ describe SearchHelper do end context "with a standard user" do - let(:user) { create(:user) } + let(:user) { create(:user) } before do allow(self).to receive(:current_user).and_return(user) diff --git a/spec/helpers/submodule_helper_spec.rb b/spec/helpers/submodule_helper_spec.rb index 8662cadc7a0..ea48c69e0ae 100644 --- a/spec/helpers/submodule_helper_spec.rb +++ b/spec/helpers/submodule_helper_spec.rb @@ -6,7 +6,7 @@ describe SubmoduleHelper do describe 'submodule links' do let(:submodule_item) { double(id: 'hash', path: 'rack') } let(:config) { Gitlab.config.gitlab } - let(:repo) { double() } + let(:repo) { double } before do self.instance_variable_set(:@repository, repo) diff --git a/spec/javascripts/behaviors/copy_as_gfm_spec.js b/spec/javascripts/behaviors/copy_as_gfm_spec.js index cf8c1b77861..6179a02ce16 100644 --- a/spec/javascripts/behaviors/copy_as_gfm_spec.js +++ b/spec/javascripts/behaviors/copy_as_gfm_spec.js @@ -87,7 +87,7 @@ describe('CopyAsGFM', () => { spyOn(window, 'getSelection').and.returnValue(selection); simulateCopy(); - const expectedGFM = '- List Item1\n- List Item2'; + const expectedGFM = '* List Item1\n\n* List Item2'; expect(clipboardData.setData).toHaveBeenCalledWith('text/x-gfm', expectedGFM); }); @@ -97,7 +97,7 @@ describe('CopyAsGFM', () => { spyOn(window, 'getSelection').and.returnValue(selection); simulateCopy(); - const expectedGFM = '1. List Item1\n1. List Item2'; + const expectedGFM = '1. List Item1\n\n1. List Item2'; expect(clipboardData.setData).toHaveBeenCalledWith('text/x-gfm', expectedGFM); }); diff --git a/spec/javascripts/behaviors/shortcuts/shortcuts_issuable_spec.js b/spec/javascripts/behaviors/shortcuts/shortcuts_issuable_spec.js index b709b937180..fe827bb1e18 100644 --- a/spec/javascripts/behaviors/shortcuts/shortcuts_issuable_spec.js +++ b/spec/javascripts/behaviors/shortcuts/shortcuts_issuable_spec.js @@ -186,7 +186,7 @@ describe('ShortcutsIssuable', function() { it('adds the quoted selection to the input', () => { ShortcutsIssuable.replyWithSelectedText(true); - expect($(FORM_SELECTOR).val()).toBe('> _Selected text._\n\n'); + expect($(FORM_SELECTOR).val()).toBe('> *Selected text.*\n\n'); }); it('triggers `focus`', () => { diff --git a/spec/javascripts/clusters/clusters_bundle_spec.js b/spec/javascripts/clusters/clusters_bundle_spec.js index 880b469284b..7928feeadfa 100644 --- a/spec/javascripts/clusters/clusters_bundle_spec.js +++ b/spec/javascripts/clusters/clusters_bundle_spec.js @@ -1,10 +1,5 @@ import Clusters from '~/clusters/clusters_bundle'; -import { - REQUEST_LOADING, - REQUEST_SUCCESS, - REQUEST_FAILURE, - APPLICATION_STATUS, -} from '~/clusters/constants'; +import { REQUEST_SUBMITTED, REQUEST_FAILURE, APPLICATION_STATUS } from '~/clusters/constants'; import getSetTimeoutPromise from 'spec/helpers/set_timeout_promise_helper'; describe('Clusters', () => { @@ -196,67 +191,43 @@ describe('Clusters', () => { }); describe('installApplication', () => { - it('tries to install helm', done => { + it('tries to install helm', () => { spyOn(cluster.service, 'installApplication').and.returnValue(Promise.resolve()); expect(cluster.store.state.applications.helm.requestStatus).toEqual(null); cluster.installApplication({ id: 'helm' }); - expect(cluster.store.state.applications.helm.requestStatus).toEqual(REQUEST_LOADING); + expect(cluster.store.state.applications.helm.requestStatus).toEqual(REQUEST_SUBMITTED); expect(cluster.store.state.applications.helm.requestReason).toEqual(null); expect(cluster.service.installApplication).toHaveBeenCalledWith('helm', undefined); - - getSetTimeoutPromise() - .then(() => { - expect(cluster.store.state.applications.helm.requestStatus).toEqual(REQUEST_SUCCESS); - expect(cluster.store.state.applications.helm.requestReason).toEqual(null); - }) - .then(done) - .catch(done.fail); }); - it('tries to install ingress', done => { + it('tries to install ingress', () => { spyOn(cluster.service, 'installApplication').and.returnValue(Promise.resolve()); expect(cluster.store.state.applications.ingress.requestStatus).toEqual(null); cluster.installApplication({ id: 'ingress' }); - expect(cluster.store.state.applications.ingress.requestStatus).toEqual(REQUEST_LOADING); + expect(cluster.store.state.applications.ingress.requestStatus).toEqual(REQUEST_SUBMITTED); expect(cluster.store.state.applications.ingress.requestReason).toEqual(null); expect(cluster.service.installApplication).toHaveBeenCalledWith('ingress', undefined); - - getSetTimeoutPromise() - .then(() => { - expect(cluster.store.state.applications.ingress.requestStatus).toEqual(REQUEST_SUCCESS); - expect(cluster.store.state.applications.ingress.requestReason).toEqual(null); - }) - .then(done) - .catch(done.fail); }); - it('tries to install runner', done => { + it('tries to install runner', () => { spyOn(cluster.service, 'installApplication').and.returnValue(Promise.resolve()); expect(cluster.store.state.applications.runner.requestStatus).toEqual(null); cluster.installApplication({ id: 'runner' }); - expect(cluster.store.state.applications.runner.requestStatus).toEqual(REQUEST_LOADING); + expect(cluster.store.state.applications.runner.requestStatus).toEqual(REQUEST_SUBMITTED); expect(cluster.store.state.applications.runner.requestReason).toEqual(null); expect(cluster.service.installApplication).toHaveBeenCalledWith('runner', undefined); - - getSetTimeoutPromise() - .then(() => { - expect(cluster.store.state.applications.runner.requestStatus).toEqual(REQUEST_SUCCESS); - expect(cluster.store.state.applications.runner.requestReason).toEqual(null); - }) - .then(done) - .catch(done.fail); }); - it('tries to install jupyter', done => { + it('tries to install jupyter', () => { spyOn(cluster.service, 'installApplication').and.returnValue(Promise.resolve()); expect(cluster.store.state.applications.jupyter.requestStatus).toEqual(null); @@ -265,19 +236,11 @@ describe('Clusters', () => { params: { hostname: cluster.store.state.applications.jupyter.hostname }, }); - expect(cluster.store.state.applications.jupyter.requestStatus).toEqual(REQUEST_LOADING); + expect(cluster.store.state.applications.jupyter.requestStatus).toEqual(REQUEST_SUBMITTED); expect(cluster.store.state.applications.jupyter.requestReason).toEqual(null); expect(cluster.service.installApplication).toHaveBeenCalledWith('jupyter', { hostname: cluster.store.state.applications.jupyter.hostname, }); - - getSetTimeoutPromise() - .then(() => { - expect(cluster.store.state.applications.jupyter.requestStatus).toEqual(REQUEST_SUCCESS); - expect(cluster.store.state.applications.jupyter.requestReason).toEqual(null); - }) - .then(done) - .catch(done.fail); }); it('sets error request status when the request fails', done => { @@ -289,7 +252,7 @@ describe('Clusters', () => { cluster.installApplication({ id: 'helm' }); - expect(cluster.store.state.applications.helm.requestStatus).toEqual(REQUEST_LOADING); + expect(cluster.store.state.applications.helm.requestStatus).toEqual(REQUEST_SUBMITTED); expect(cluster.store.state.applications.helm.requestReason).toEqual(null); expect(cluster.service.installApplication).toHaveBeenCalled(); diff --git a/spec/javascripts/clusters/components/application_row_spec.js b/spec/javascripts/clusters/components/application_row_spec.js index 45d56514930..d1f4a1cebb4 100644 --- a/spec/javascripts/clusters/components/application_row_spec.js +++ b/spec/javascripts/clusters/components/application_row_spec.js @@ -1,11 +1,6 @@ import Vue from 'vue'; import eventHub from '~/clusters/event_hub'; -import { - APPLICATION_STATUS, - REQUEST_LOADING, - REQUEST_SUCCESS, - REQUEST_FAILURE, -} from '~/clusters/constants'; +import { APPLICATION_STATUS, REQUEST_SUBMITTED, REQUEST_FAILURE } from '~/clusters/constants'; import applicationRow from '~/clusters/components/application_row.vue'; import mountComponent from 'spec/helpers/vue_mount_component_helper'; import { DEFAULT_APPLICATION_STATE } from '../services/mock_data'; @@ -57,6 +52,12 @@ describe('Application Row', () => { expect(vm.installButtonLabel).toBeUndefined(); }); + it('has install button', () => { + const installationBtn = vm.$el.querySelector('.js-cluster-application-install-button'); + + expect(installationBtn).not.toBe(null); + }); + it('has disabled "Install" when APPLICATION_STATUS.NOT_INSTALLABLE', () => { vm = mountComponent(ApplicationRow, { ...DEFAULT_APPLICATION_STATE, @@ -101,6 +102,18 @@ describe('Application Row', () => { expect(vm.installButtonDisabled).toEqual(true); }); + it('has loading "Installing" when REQUEST_SUBMITTED', () => { + vm = mountComponent(ApplicationRow, { + ...DEFAULT_APPLICATION_STATE, + status: APPLICATION_STATUS.INSTALLABLE, + requestStatus: REQUEST_SUBMITTED, + }); + + expect(vm.installButtonLabel).toEqual('Installing'); + expect(vm.installButtonLoading).toEqual(true); + expect(vm.installButtonDisabled).toEqual(true); + }); + it('has disabled "Installed" when APPLICATION_STATUS.INSTALLED', () => { vm = mountComponent(ApplicationRow, { ...DEFAULT_APPLICATION_STATE, @@ -134,30 +147,6 @@ describe('Application Row', () => { expect(vm.installButtonDisabled).toEqual(false); }); - it('has loading "Install" when REQUEST_LOADING', () => { - vm = mountComponent(ApplicationRow, { - ...DEFAULT_APPLICATION_STATE, - status: APPLICATION_STATUS.INSTALLABLE, - requestStatus: REQUEST_LOADING, - }); - - expect(vm.installButtonLabel).toEqual('Install'); - expect(vm.installButtonLoading).toEqual(true); - expect(vm.installButtonDisabled).toEqual(true); - }); - - it('has disabled "Install" when REQUEST_SUCCESS', () => { - vm = mountComponent(ApplicationRow, { - ...DEFAULT_APPLICATION_STATE, - status: APPLICATION_STATUS.INSTALLABLE, - requestStatus: REQUEST_SUCCESS, - }); - - expect(vm.installButtonLabel).toEqual('Install'); - expect(vm.installButtonLoading).toEqual(false); - expect(vm.installButtonDisabled).toEqual(true); - }); - it('has enabled "Install" when REQUEST_FAILURE (so you can try installing again)', () => { vm = mountComponent(ApplicationRow, { ...DEFAULT_APPLICATION_STATE, 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..2f0385454d7 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', ); }); @@ -51,15 +51,6 @@ describe('CompareVersions', () => { }); }); - it('should render whitespace toggle button with correct attributes', () => { - const whitespaceBtn = vm.$el.querySelector('.qa-toggle-whitespace'); - const href = vm.toggleWhitespacePath; - - expect(whitespaceBtn).not.toBeNull(); - expect(whitespaceBtn.getAttribute('href')).toEqual(href); - expect(whitespaceBtn.innerHTML).toContain('Hide whitespace changes'); - }); - it('should render view types buttons with correct values', () => { const inlineBtn = vm.$el.querySelector('#inline-diff-btn'); const parallelBtn = vm.$el.querySelector('#parallel-diff-btn'); @@ -100,27 +91,9 @@ describe('CompareVersions', () => { }); }); - describe('isWhitespaceVisible', () => { - const originalHref = window.location.href; - - afterEach(() => { - window.history.replaceState({}, null, originalHref); - }); - - it('should return "true" when no "w" flag is present in the URL (default)', () => { - expect(vm.isWhitespaceVisible()).toBe(true); - }); - - it('should return "false" when the flag is set to "1" in the URL', () => { - window.history.replaceState({}, null, '?w=1'); - - expect(vm.isWhitespaceVisible()).toBe(false); - }); - - it('should return "true" when the flag is set to "0" in the URL', () => { - window.history.replaceState({}, null, '?w=0'); - - expect(vm.isWhitespaceVisible()).toBe(true); + describe('baseVersionPath', () => { + it('should be set correctly from mergeRequestDiff', () => { + expect(vm.baseVersionPath).toEqual(vm.mergeRequestDiff.base_version_path); }); }); diff --git a/spec/javascripts/diffs/components/settings_dropdown_spec.js b/spec/javascripts/diffs/components/settings_dropdown_spec.js new file mode 100644 index 00000000000..5031846cff0 --- /dev/null +++ b/spec/javascripts/diffs/components/settings_dropdown_spec.js @@ -0,0 +1,167 @@ +import { mount, createLocalVue } from '@vue/test-utils'; +import Vuex from 'vuex'; +import diffModule from '~/diffs/store/modules'; +import SettingsDropdown from '~/diffs/components/settings_dropdown.vue'; +import { PARALLEL_DIFF_VIEW_TYPE, INLINE_DIFF_VIEW_TYPE } from '~/diffs/constants'; + +describe('Diff settiings dropdown component', () => { + let vm; + let actions; + + function createComponent(extendStore = () => {}) { + const localVue = createLocalVue(); + + localVue.use(Vuex); + + const store = new Vuex.Store({ + modules: { + diffs: { + namespaced: true, + actions, + state: diffModule().state, + getters: diffModule().getters, + }, + }, + }); + + extendStore(store); + + vm = mount(SettingsDropdown, { + localVue, + store, + }); + } + + beforeEach(() => { + actions = { + setInlineDiffViewType: jasmine.createSpy('setInlineDiffViewType'), + setParallelDiffViewType: jasmine.createSpy('setParallelDiffViewType'), + setRenderTreeList: jasmine.createSpy('setRenderTreeList'), + setShowWhitespace: jasmine.createSpy('setShowWhitespace'), + }; + }); + + afterEach(() => { + vm.destroy(); + }); + + describe('tree view buttons', () => { + it('list view button dispatches setRenderTreeList with false', () => { + createComponent(); + + vm.find('.js-list-view').trigger('click'); + + expect(actions.setRenderTreeList).toHaveBeenCalledWith(jasmine.anything(), false, undefined); + }); + + it('tree view button dispatches setRenderTreeList with true', () => { + createComponent(); + + vm.find('.js-tree-view').trigger('click'); + + expect(actions.setRenderTreeList).toHaveBeenCalledWith(jasmine.anything(), true, undefined); + }); + + it('sets list button as active when renderTreeList is false', () => { + createComponent(store => { + Object.assign(store.state.diffs, { + renderTreeList: false, + }); + }); + + expect(vm.find('.js-list-view').classes('active')).toBe(true); + expect(vm.find('.js-tree-view').classes('active')).toBe(false); + }); + + it('sets tree button as active when renderTreeList is true', () => { + createComponent(store => { + Object.assign(store.state.diffs, { + renderTreeList: true, + }); + }); + + expect(vm.find('.js-list-view').classes('active')).toBe(false); + expect(vm.find('.js-tree-view').classes('active')).toBe(true); + }); + }); + + describe('compare changes', () => { + it('sets inline button as active', () => { + createComponent(store => { + Object.assign(store.state.diffs, { + diffViewType: INLINE_DIFF_VIEW_TYPE, + }); + }); + + expect(vm.find('.js-inline-diff-button').classes('active')).toBe(true); + expect(vm.find('.js-parallel-diff-button').classes('active')).toBe(false); + }); + + it('sets parallel button as active', () => { + createComponent(store => { + Object.assign(store.state.diffs, { + diffViewType: PARALLEL_DIFF_VIEW_TYPE, + }); + }); + + expect(vm.find('.js-inline-diff-button').classes('active')).toBe(false); + expect(vm.find('.js-parallel-diff-button').classes('active')).toBe(true); + }); + + it('calls setInlineDiffViewType when clicking inline button', () => { + createComponent(); + + vm.find('.js-inline-diff-button').trigger('click'); + + expect(actions.setInlineDiffViewType).toHaveBeenCalled(); + }); + + it('calls setParallelDiffViewType when clicking parallel button', () => { + createComponent(); + + vm.find('.js-parallel-diff-button').trigger('click'); + + expect(actions.setParallelDiffViewType).toHaveBeenCalled(); + }); + }); + + describe('whitespace toggle', () => { + it('does not set as checked when showWhitespace is false', () => { + createComponent(store => { + Object.assign(store.state.diffs, { + showWhitespace: false, + }); + }); + + expect(vm.find('#show-whitespace').element.checked).toBe(false); + }); + + it('sets as checked when showWhitespace is true', () => { + createComponent(store => { + Object.assign(store.state.diffs, { + showWhitespace: true, + }); + }); + + expect(vm.find('#show-whitespace').element.checked).toBe(true); + }); + + it('calls setShowWhitespace on change', () => { + createComponent(); + + const checkbox = vm.find('#show-whitespace'); + + checkbox.element.checked = true; + checkbox.trigger('change'); + + expect(actions.setShowWhitespace).toHaveBeenCalledWith( + jasmine.anything(), + { + showWhitespace: true, + pushState: true, + }, + undefined, + ); + }); + }); +}); diff --git a/spec/javascripts/diffs/components/tree_list_spec.js b/spec/javascripts/diffs/components/tree_list_spec.js index 0a903bb7519..08b0b4f9e45 100644 --- a/spec/javascripts/diffs/components/tree_list_spec.js +++ b/spec/javascripts/diffs/components/tree_list_spec.js @@ -111,7 +111,7 @@ describe('Diffs tree list component', () => { }); it('renders as file list when renderTreeList is false', done => { - vm.renderTreeList = false; + vm.$store.state.diffs.renderTreeList = false; vm.$nextTick(() => { expect(vm.$el.querySelectorAll('.file-row').length).toBe(1); @@ -121,7 +121,7 @@ describe('Diffs tree list component', () => { }); it('renders file paths when renderTreeList is false', done => { - vm.renderTreeList = false; + vm.$store.state.diffs.renderTreeList = false; vm.$nextTick(() => { expect(vm.$el.querySelector('.file-row').textContent).toContain('index.js'); @@ -129,34 +129,6 @@ describe('Diffs tree list component', () => { done(); }); }); - - it('hides render buttons when input is focused', done => { - const focusEvent = new Event('focus'); - - vm.$el.querySelector('.form-control').dispatchEvent(focusEvent); - - vm.$nextTick(() => { - expect(vm.$el.querySelector('.tree-list-view-toggle').style.display).toBe('none'); - - done(); - }); - }); - - it('shows render buttons when input is blurred', done => { - const blurEvent = new Event('blur'); - vm.focusSearch = true; - - vm.$nextTick() - .then(() => { - vm.$el.querySelector('.form-control').dispatchEvent(blurEvent); - }) - .then(vm.$nextTick) - .then(() => { - expect(vm.$el.querySelector('.tree-list-view-toggle').style.display).not.toBe('none'); - }) - .then(done) - .catch(done.fail); - }); }); describe('clearSearch', () => { @@ -168,24 +140,4 @@ describe('Diffs tree list component', () => { expect(vm.search).toBe(''); }); }); - - describe('toggleRenderTreeList', () => { - it('updates renderTreeList', () => { - expect(vm.renderTreeList).toBe(true); - - vm.toggleRenderTreeList(false); - - expect(vm.renderTreeList).toBe(false); - }); - }); - - describe('toggleFocusSearch', () => { - it('updates focusSearch', () => { - expect(vm.focusSearch).toBe(false); - - vm.toggleFocusSearch(true); - - expect(vm.focusSearch).toBe(true); - }); - }); }); 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/actions_spec.js b/spec/javascripts/diffs/store/actions_spec.js index 033b5e86dbe..b53ae4cecfd 100644 --- a/spec/javascripts/diffs/store/actions_spec.js +++ b/spec/javascripts/diffs/store/actions_spec.js @@ -27,6 +27,8 @@ import actions, { scrollToFile, toggleShowTreeList, renderFileForDiscussionId, + setRenderTreeList, + setShowWhitespace, } from '~/diffs/store/actions'; import eventHub from '~/notes/event_hub'; import * as types from '~/diffs/store/mutation_types'; @@ -796,4 +798,55 @@ describe('DiffsStoreActions', () => { expect(scrollToElement).not.toHaveBeenCalled(); }); }); + + describe('setRenderTreeList', () => { + it('commits SET_RENDER_TREE_LIST', done => { + testAction( + setRenderTreeList, + true, + {}, + [{ type: types.SET_RENDER_TREE_LIST, payload: true }], + [], + done, + ); + }); + + it('sets localStorage', () => { + spyOn(localStorage, 'setItem').and.stub(); + + setRenderTreeList({ commit() {} }, true); + + expect(localStorage.setItem).toHaveBeenCalledWith('mr_diff_tree_list', true); + }); + }); + + describe('setShowWhitespace', () => { + it('commits SET_SHOW_WHITESPACE', done => { + testAction( + setShowWhitespace, + { showWhitespace: true }, + {}, + [{ type: types.SET_SHOW_WHITESPACE, payload: true }], + [], + done, + ); + }); + + it('sets localStorage', () => { + spyOn(localStorage, 'setItem').and.stub(); + + setShowWhitespace({ commit() {} }, { showWhitespace: true }); + + expect(localStorage.setItem).toHaveBeenCalledWith('mr_show_whitespace', true); + }); + + it('calls history pushState', () => { + spyOn(localStorage, 'setItem').and.stub(); + spyOn(window.history, 'pushState').and.stub(); + + setShowWhitespace({ commit() {} }, { showWhitespace: true, pushState: true }); + + expect(window.history.pushState).toHaveBeenCalled(); + }); + }); }); diff --git a/spec/javascripts/diffs/store/mutations_spec.js b/spec/javascripts/diffs/store/mutations_spec.js index d8733941181..a6f3f9b9dc3 100644 --- a/spec/javascripts/diffs/store/mutations_spec.js +++ b/spec/javascripts/diffs/store/mutations_spec.js @@ -628,4 +628,50 @@ 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']); + }); + }); + + describe('SET_RENDER_TREE_LIST', () => { + it('sets renderTreeList', () => { + const state = { + renderTreeList: true, + }; + + mutations[types.SET_RENDER_TREE_LIST](state, false); + + expect(state.renderTreeList).toBe(false); + }); + }); + + describe('SET_SHOW_WHITESPACE', () => { + it('sets showWhitespace', () => { + const state = { + showWhitespace: true, + }; + + mutations[types.SET_SHOW_WHITESPACE](state, false); + + expect(state.showWhitespace).toBe(false); + }); + }); }); diff --git a/spec/javascripts/diffs/store/utils_spec.js b/spec/javascripts/diffs/store/utils_spec.js index 036b320b314..c5e413a29d8 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,175 @@ 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: 'ee', + tree: [ + { + type: 'tree', + name: 'lib', + tree: [ + { + type: 'tree', + name: 'ee', + tree: [ + { + type: 'tree', + name: 'gitlab', + tree: [ + { + type: 'tree', + name: 'checks', + tree: [ + { + type: 'tree', + name: 'longtreenametomakepath', + tree: [ + { + type: 'blob', + name: 'diff_check.rb', + 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: 'ee/lib/…/…/…/longtreenametomakepath', + tree: [ + { + name: 'diff_check.rb', + tree: [], + type: 'blob', + }, + ], + }, + { + 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/ide/components/ide_status_bar_spec.js b/spec/javascripts/ide/components/ide_status_bar_spec.js index ab032b4cb98..bb8fb74c068 100644 --- a/spec/javascripts/ide/components/ide_status_bar_spec.js +++ b/spec/javascripts/ide/components/ide_status_bar_spec.js @@ -76,6 +76,9 @@ describe('ideStatusBar', () => { icon: 'status_success', }, }, + commit: { + author_gravatar_url: 'www', + }, }); vm.$nextTick() diff --git a/spec/javascripts/ide/lib/decorations/controller_spec.js b/spec/javascripts/ide/lib/decorations/controller_spec.js index a112361e0d1..4118774cca3 100644 --- a/spec/javascripts/ide/lib/decorations/controller_spec.js +++ b/spec/javascripts/ide/lib/decorations/controller_spec.js @@ -56,7 +56,7 @@ describe('Multi-file editor library decorations controller', () => { controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]); expect(controller.decorations.size).toBe(1); - expect(controller.decorations.keys().next().value).toBe('path--path'); + expect(controller.decorations.keys().next().value).toBe('gitlab:path--path'); }); it('calls decorate method', () => { @@ -90,7 +90,7 @@ describe('Multi-file editor library decorations controller', () => { controller.decorate(model); - expect(controller.editorDecorations.keys().next().value).toBe('path--path'); + expect(controller.editorDecorations.keys().next().value).toBe('gitlab:path--path'); }); }); diff --git a/spec/javascripts/lib/utils/common_utils_spec.js b/spec/javascripts/lib/utils/common_utils_spec.js index 0dc7e93539a..e3fd9604474 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', () => { @@ -513,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); @@ -536,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); @@ -582,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); @@ -660,51 +680,131 @@ describe('common_utils', () => { }); }); - describe('deep: true', () => { - it('converts object with child objects', () => { - const obj = { - snake_key: { - child_snake_key: 'value', - }, - }; + describe('with options', () => { + const objWithoutChildren = { + project_name: 'GitLab CE', + group_name: 'GitLab.org', + license_type: 'MIT', + }; - expect(commonUtils.convertObjectPropsToCamelCase(obj, { deep: true })).toEqual({ - snakeKey: { - childSnakeKey: 'value', - }, - }); - }); + const objWithChildren = { + project_name: 'GitLab CE', + group_name: 'GitLab.org', + license_type: 'MIT', + tech_stack: { + backend: 'Ruby', + frontend_framework: 'Vue', + database: 'PostgreSQL', + }, + }; - it('converts array with child objects', () => { - const arr = [ - { - child_snake_key: 'value', - }, - ]; - - expect(commonUtils.convertObjectPropsToCamelCase(arr, { deep: true })).toEqual([ - { - childSnakeKey: 'value', - }, - ]); - }); + describe('when options.deep is true', () => { + it('converts object with child objects', () => { + const obj = { + snake_key: { + child_snake_key: 'value', + }, + }; + + expect(commonUtils.convertObjectPropsToCamelCase(obj, { deep: true })).toEqual({ + snakeKey: { + childSnakeKey: 'value', + }, + }); + }); - it('converts array with child arrays', () => { - const arr = [ - [ + it('converts array with child objects', () => { + const arr = [ { child_snake_key: 'value', }, - ], - ]; + ]; - expect(commonUtils.convertObjectPropsToCamelCase(arr, { deep: true })).toEqual([ - [ + expect(commonUtils.convertObjectPropsToCamelCase(arr, { deep: true })).toEqual([ { childSnakeKey: 'value', }, - ], - ]); + ]); + }); + + it('converts array with child arrays', () => { + const arr = [ + [ + { + child_snake_key: 'value', + }, + ], + ]; + + expect(commonUtils.convertObjectPropsToCamelCase(arr, { deep: true })).toEqual([ + [ + { + childSnakeKey: 'value', + }, + ], + ]); + }); + }); + + describe('when options.dropKeys is provided', () => { + it('discards properties mentioned in `dropKeys` array', () => { + expect( + commonUtils.convertObjectPropsToCamelCase(objWithoutChildren, { + dropKeys: ['group_name'], + }), + ).toEqual({ + projectName: 'GitLab CE', + licenseType: 'MIT', + }); + }); + + it('discards properties mentioned in `dropKeys` array when `deep` is true', () => { + expect( + commonUtils.convertObjectPropsToCamelCase(objWithChildren, { + deep: true, + dropKeys: ['group_name', 'database'], + }), + ).toEqual({ + projectName: 'GitLab CE', + licenseType: 'MIT', + techStack: { + backend: 'Ruby', + frontendFramework: 'Vue', + }, + }); + }); + }); + + describe('when options.ignoreKeyNames is provided', () => { + it('leaves properties mentioned in `ignoreKeyNames` array intact', () => { + expect( + commonUtils.convertObjectPropsToCamelCase(objWithoutChildren, { + ignoreKeyNames: ['group_name'], + }), + ).toEqual({ + projectName: 'GitLab CE', + licenseType: 'MIT', + group_name: 'GitLab.org', + }); + }); + + it('leaves properties mentioned in `ignoreKeyNames` array intact when `deep` is true', () => { + expect( + commonUtils.convertObjectPropsToCamelCase(objWithChildren, { + deep: true, + ignoreKeyNames: ['group_name', 'frontend_framework'], + }), + ).toEqual({ + projectName: 'GitLab CE', + group_name: 'GitLab.org', + licenseType: 'MIT', + techStack: { + backend: 'Ruby', + frontend_framework: 'Vue', + database: 'PostgreSQL', + }, + }); + }); }); }); }); 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/monitoring/graph/axis_spec.js b/spec/javascripts/monitoring/graph/axis_spec.js deleted file mode 100644 index c7adba00637..00000000000 --- a/spec/javascripts/monitoring/graph/axis_spec.js +++ /dev/null @@ -1,65 +0,0 @@ -import Vue from 'vue'; -import GraphAxis from '~/monitoring/components/graph/axis.vue'; -import measurements from '~/monitoring/utils/measurements'; - -const createComponent = propsData => { - const Component = Vue.extend(GraphAxis); - - return new Component({ - propsData, - }).$mount(); -}; - -const defaultValuesComponent = { - graphWidth: 500, - graphHeight: 300, - graphHeightOffset: 120, - margin: measurements.large.margin, - measurements: measurements.large, - yAxisLabel: 'Values', - unitOfDisplay: 'MB', -}; - -function getTextFromNode(component, selector) { - return component.$el.querySelector(selector).firstChild.nodeValue.trim(); -} - -describe('Axis', () => { - describe('Computed props', () => { - it('textTransform', () => { - const component = createComponent(defaultValuesComponent); - - expect(component.textTransform).toContain('translate(15, 120) rotate(-90)'); - }); - - it('xPosition', () => { - const component = createComponent(defaultValuesComponent); - - expect(component.xPosition).toEqual(180); - }); - - it('yPosition', () => { - const component = createComponent(defaultValuesComponent); - - expect(component.yPosition).toEqual(240); - }); - - it('rectTransform', () => { - const component = createComponent(defaultValuesComponent); - - expect(component.rectTransform).toContain('translate(0, 120) rotate(-90)'); - }); - }); - - it('has 2 rect-axis-text rect svg elements', () => { - const component = createComponent(defaultValuesComponent); - - expect(component.$el.querySelectorAll('.rect-axis-text').length).toEqual(2); - }); - - it('contains text to signal the usage, title and time with multiple time series', () => { - const component = createComponent(defaultValuesComponent); - - expect(getTextFromNode(component, '.y-label-text')).toEqual('Values (MB)'); - }); -}); diff --git a/spec/javascripts/monitoring/graph/deployment_spec.js b/spec/javascripts/monitoring/graph/deployment_spec.js deleted file mode 100644 index 7d39c4345d2..00000000000 --- a/spec/javascripts/monitoring/graph/deployment_spec.js +++ /dev/null @@ -1,53 +0,0 @@ -import Vue from 'vue'; -import GraphDeployment from '~/monitoring/components/graph/deployment.vue'; -import { deploymentData } from '../mock_data'; - -const createComponent = propsData => { - const Component = Vue.extend(GraphDeployment); - - return new Component({ - propsData, - }).$mount(); -}; - -describe('MonitoringDeployment', () => { - describe('Methods', () => { - it('should contain a hidden gradient', () => { - const component = createComponent({ - showDeployInfo: true, - deploymentData, - graphHeight: 300, - graphWidth: 440, - graphHeightOffset: 120, - }); - - expect(component.$el.querySelector('#shadow-gradient')).not.toBeNull(); - }); - - it('transformDeploymentGroup translates an available deployment', () => { - const component = createComponent({ - showDeployInfo: false, - deploymentData, - graphHeight: 300, - graphWidth: 440, - graphHeightOffset: 120, - }); - - expect(component.transformDeploymentGroup({ xPos: 16 })).toContain('translate(11, 20)'); - }); - - describe('Computed props', () => { - it('calculatedHeight', () => { - const component = createComponent({ - showDeployInfo: true, - deploymentData, - graphHeight: 300, - graphWidth: 440, - graphHeightOffset: 120, - }); - - expect(component.calculatedHeight).toEqual(180); - }); - }); - }); -}); diff --git a/spec/javascripts/monitoring/graph/flag_spec.js b/spec/javascripts/monitoring/graph/flag_spec.js deleted file mode 100644 index 038bfffd44f..00000000000 --- a/spec/javascripts/monitoring/graph/flag_spec.js +++ /dev/null @@ -1,133 +0,0 @@ -import Vue from 'vue'; -import GraphFlag from '~/monitoring/components/graph/flag.vue'; -import { deploymentData } from '../mock_data'; - -const createComponent = propsData => { - const Component = Vue.extend(GraphFlag); - - return new Component({ - propsData, - }).$mount(); -}; - -const defaultValuesComponent = { - currentXCoordinate: 200, - currentYCoordinate: 100, - currentFlagPosition: 100, - currentData: { - time: new Date('2017-06-04T18:17:33.501Z'), - value: '1.49609375', - }, - graphHeight: 300, - graphHeightOffset: 120, - showFlagContent: true, - realPixelRatio: 1, - timeSeries: [ - { - values: [ - { - time: new Date('2017-06-04T18:17:33.501Z'), - value: '1.49609375', - }, - ], - }, - ], - unitOfDisplay: 'ms', - currentDataIndex: 0, - legendTitle: 'Average', - currentCoordinates: {}, -}; - -const deploymentFlagData = { - ...deploymentData[0], - ref: deploymentData[0].ref.name, - xPos: 10, - time: new Date(deploymentData[0].created_at), -}; - -describe('GraphFlag', () => { - let component; - - it('has a line at the currentXCoordinate', () => { - component = createComponent(defaultValuesComponent); - - expect(component.$el.style.left).toEqual(`${70 + component.currentXCoordinate}px`); - }); - - describe('Deployment flag', () => { - it('shows a deployment flag when deployment data provided', () => { - const deploymentFlagComponent = createComponent({ - ...defaultValuesComponent, - deploymentFlagData, - }); - - expect(deploymentFlagComponent.$el.querySelector('.popover-title')).toContainText('Deployed'); - }); - - it('contains the ref when a tag is available', () => { - const deploymentFlagComponent = createComponent({ - ...defaultValuesComponent, - deploymentFlagData: { - ...deploymentFlagData, - sha: 'f5bcd1d9dac6fa4137e2510b9ccd134ef2e84187', - tag: true, - ref: '1.0', - }, - }); - - expect(deploymentFlagComponent.$el.querySelector('.deploy-meta-content')).toContainText( - 'f5bcd1d9', - ); - - expect(deploymentFlagComponent.$el.querySelector('.deploy-meta-content')).toContainText( - '1.0', - ); - }); - - it('does not contain the ref when a tag is unavailable', () => { - const deploymentFlagComponent = createComponent({ - ...defaultValuesComponent, - deploymentFlagData: { - ...deploymentFlagData, - sha: 'f5bcd1d9dac6fa4137e2510b9ccd134ef2e84187', - tag: false, - ref: '1.0', - }, - }); - - expect(deploymentFlagComponent.$el.querySelector('.deploy-meta-content')).toContainText( - 'f5bcd1d9', - ); - - expect(deploymentFlagComponent.$el.querySelector('.deploy-meta-content')).not.toContainText( - '1.0', - ); - }); - }); - - describe('Computed props', () => { - beforeEach(() => { - component = createComponent(defaultValuesComponent); - }); - - it('formatTime', () => { - expect(component.formatTime).toMatch(/\d:17PM/); - }); - - it('formatDate', () => { - expect(component.formatDate).toEqual('04 Jun 2017, '); - }); - - it('cursorStyle', () => { - expect(component.cursorStyle).toEqual({ - top: '20px', - left: '270px', - height: '180px', - }); - }); - - it('flagOrientation', () => { - expect(component.flagOrientation).toEqual('left'); - }); - }); -}); diff --git a/spec/javascripts/monitoring/graph/legend_spec.js b/spec/javascripts/monitoring/graph/legend_spec.js deleted file mode 100644 index 9209e77dcf4..00000000000 --- a/spec/javascripts/monitoring/graph/legend_spec.js +++ /dev/null @@ -1,44 +0,0 @@ -import Vue from 'vue'; -import GraphLegend from '~/monitoring/components/graph/legend.vue'; -import createTimeSeries from '~/monitoring/utils/multiple_time_series'; -import mountComponent from 'spec/helpers/vue_mount_component_helper'; -import { singleRowMetricsMultipleSeries, convertDatesMultipleSeries } from '../mock_data'; - -const convertedMetrics = convertDatesMultipleSeries(singleRowMetricsMultipleSeries); - -const defaultValuesComponent = {}; - -const { timeSeries } = createTimeSeries(convertedMetrics[0].queries, 500, 300, 120); - -defaultValuesComponent.timeSeries = timeSeries; - -describe('Legend Component', () => { - let vm; - let Legend; - - beforeEach(() => { - Legend = Vue.extend(GraphLegend); - }); - - describe('View', () => { - beforeEach(() => { - vm = mountComponent(Legend, { - legendTitle: 'legend', - timeSeries, - currentDataIndex: 0, - unitOfDisplay: 'Req/Sec', - }); - }); - - it('should render the usage, title and time with multiple time series', () => { - const titles = vm.$el.querySelectorAll('.legend-metric-title'); - - expect(titles[0].textContent.indexOf('1xx')).not.toEqual(-1); - expect(titles[1].textContent.indexOf('2xx')).not.toEqual(-1); - }); - - it('should container the same number of rows in the table as time series', () => { - expect(vm.$el.querySelectorAll('.prometheus-table tr').length).toEqual(vm.timeSeries.length); - }); - }); -}); diff --git a/spec/javascripts/monitoring/graph/track_info_spec.js b/spec/javascripts/monitoring/graph/track_info_spec.js deleted file mode 100644 index ce93ae28842..00000000000 --- a/spec/javascripts/monitoring/graph/track_info_spec.js +++ /dev/null @@ -1,44 +0,0 @@ -import Vue from 'vue'; -import TrackInfo from '~/monitoring/components/graph/track_info.vue'; -import mountComponent from 'spec/helpers/vue_mount_component_helper'; -import createTimeSeries from '~/monitoring/utils/multiple_time_series'; -import { singleRowMetricsMultipleSeries, convertDatesMultipleSeries } from '../mock_data'; - -const convertedMetrics = convertDatesMultipleSeries(singleRowMetricsMultipleSeries); -const { timeSeries } = createTimeSeries(convertedMetrics[0].queries, 500, 300, 120); - -describe('TrackInfo component', () => { - let vm; - let Component; - - beforeEach(() => { - Component = Vue.extend(TrackInfo); - }); - - afterEach(() => { - vm.$destroy(); - }); - - describe('Computed props', () => { - beforeEach(() => { - vm = mountComponent(Component, { track: timeSeries[0] }); - }); - - it('summaryMetrics', () => { - expect(vm.summaryMetrics).toEqual('Avg: 0.000 · Max: 0.000'); - }); - }); - - describe('Rendered output', () => { - beforeEach(() => { - vm = mountComponent(Component, { track: timeSeries[0] }); - }); - - it('contains metric tag and the summary metrics', () => { - const metricTag = vm.$el.querySelector('strong'); - - expect(metricTag.textContent.trim()).toEqual(vm.track.metricTag); - expect(vm.$el.textContent).toContain('Avg: 0.000 · Max: 0.000'); - }); - }); -}); diff --git a/spec/javascripts/monitoring/graph/track_line_spec.js b/spec/javascripts/monitoring/graph/track_line_spec.js deleted file mode 100644 index 2a4f89ddf6e..00000000000 --- a/spec/javascripts/monitoring/graph/track_line_spec.js +++ /dev/null @@ -1,52 +0,0 @@ -import Vue from 'vue'; -import TrackLine from '~/monitoring/components/graph/track_line.vue'; -import mountComponent from 'spec/helpers/vue_mount_component_helper'; -import createTimeSeries from '~/monitoring/utils/multiple_time_series'; -import { singleRowMetricsMultipleSeries, convertDatesMultipleSeries } from '../mock_data'; - -const convertedMetrics = convertDatesMultipleSeries(singleRowMetricsMultipleSeries); -const { timeSeries } = createTimeSeries(convertedMetrics[0].queries, 500, 300, 120); - -describe('TrackLine component', () => { - let vm; - let Component; - - beforeEach(() => { - Component = Vue.extend(TrackLine); - }); - - afterEach(() => { - vm.$destroy(); - }); - - describe('Computed props', () => { - it('stylizedLine for dashed lineStyles', () => { - vm = mountComponent(Component, { track: { ...timeSeries[0], lineStyle: 'dashed' } }); - - expect(vm.stylizedLine).toEqual('6, 3'); - }); - - it('stylizedLine for dotted lineStyles', () => { - vm = mountComponent(Component, { track: { ...timeSeries[0], lineStyle: 'dotted' } }); - - expect(vm.stylizedLine).toEqual('3, 3'); - }); - }); - - describe('Rendered output', () => { - it('has an svg with a line', () => { - vm = mountComponent(Component, { track: { ...timeSeries[0] } }); - const svgEl = vm.$el.querySelector('svg'); - const lineEl = vm.$el.querySelector('svg line'); - - expect(svgEl.getAttribute('width')).toEqual('16'); - expect(svgEl.getAttribute('height')).toEqual('8'); - - expect(lineEl.getAttribute('stroke-width')).toEqual('4'); - expect(lineEl.getAttribute('x1')).toEqual('0'); - expect(lineEl.getAttribute('x2')).toEqual('16'); - expect(lineEl.getAttribute('y1')).toEqual('4'); - expect(lineEl.getAttribute('y2')).toEqual('4'); - }); - }); -}); diff --git a/spec/javascripts/monitoring/graph_path_spec.js b/spec/javascripts/monitoring/graph_path_spec.js deleted file mode 100644 index fd167b83d51..00000000000 --- a/spec/javascripts/monitoring/graph_path_spec.js +++ /dev/null @@ -1,56 +0,0 @@ -import Vue from 'vue'; -import GraphPath from '~/monitoring/components/graph/path.vue'; -import createTimeSeries from '~/monitoring/utils/multiple_time_series'; -import { singleRowMetricsMultipleSeries, convertDatesMultipleSeries } from './mock_data'; - -const createComponent = propsData => { - const Component = Vue.extend(GraphPath); - - return new Component({ - propsData, - }).$mount(); -}; - -const convertedMetrics = convertDatesMultipleSeries(singleRowMetricsMultipleSeries); - -const { timeSeries } = createTimeSeries(convertedMetrics[0].queries, 428, 272, 120); -const firstTimeSeries = timeSeries[0]; - -describe('Monitoring Paths', () => { - it('renders two paths to represent a line and the area underneath it', () => { - const component = createComponent({ - generatedLinePath: firstTimeSeries.linePath, - generatedAreaPath: firstTimeSeries.areaPath, - lineColor: firstTimeSeries.lineColor, - areaColor: firstTimeSeries.areaColor, - showDot: false, - }); - const metricArea = component.$el.querySelector('.metric-area'); - const metricLine = component.$el.querySelector('.metric-line'); - - expect(metricArea.getAttribute('fill')).toBe('#8fbce8'); - expect(metricArea.getAttribute('d')).toBe(firstTimeSeries.areaPath); - expect(metricLine.getAttribute('stroke')).toBe('#1f78d1'); - expect(metricLine.getAttribute('d')).toBe(firstTimeSeries.linePath); - }); - - describe('Computed properties', () => { - it('strokeDashArray', () => { - const component = createComponent({ - generatedLinePath: firstTimeSeries.linePath, - generatedAreaPath: firstTimeSeries.areaPath, - lineColor: firstTimeSeries.lineColor, - areaColor: firstTimeSeries.areaColor, - showDot: false, - }); - - component.lineStyle = 'dashed'; - - expect(component.strokeDashArray).toBe('3, 1'); - - component.lineStyle = 'dotted'; - - expect(component.strokeDashArray).toBe('1, 1'); - }); - }); -}); diff --git a/spec/javascripts/monitoring/graph_spec.js b/spec/javascripts/monitoring/graph_spec.js deleted file mode 100644 index 59d6d4f3a7f..00000000000 --- a/spec/javascripts/monitoring/graph_spec.js +++ /dev/null @@ -1,127 +0,0 @@ -import Vue from 'vue'; -import Graph from '~/monitoring/components/graph.vue'; -import MonitoringMixins from '~/monitoring/mixins/monitoring_mixins'; -import { - deploymentData, - convertDatesMultipleSeries, - singleRowMetricsMultipleSeries, - queryWithoutData, -} from './mock_data'; - -const tagsPath = 'http://test.host/frontend-fixtures/environments-project/tags'; -const projectPath = 'http://test.host/frontend-fixtures/environments-project'; -const createComponent = propsData => { - const Component = Vue.extend(Graph); - - return new Component({ - propsData, - }).$mount(); -}; - -const convertedMetrics = convertDatesMultipleSeries(singleRowMetricsMultipleSeries); - -describe('Graph', () => { - beforeEach(() => { - spyOn(MonitoringMixins.methods, 'formatDeployments').and.returnValue({}); - }); - - it('has a title', () => { - const component = createComponent({ - graphData: convertedMetrics[1], - updateAspectRatio: false, - deploymentData, - tagsPath, - projectPath, - }); - - expect(component.$el.querySelector('.prometheus-graph-title').innerText.trim()).toBe( - component.graphData.title, - ); - }); - - describe('Computed props', () => { - it('axisTransform translates an element Y position depending of its height', () => { - const component = createComponent({ - graphData: convertedMetrics[1], - updateAspectRatio: false, - deploymentData, - tagsPath, - projectPath, - }); - - const transformedHeight = `${component.graphHeight - 100}`; - - expect(component.axisTransform.indexOf(transformedHeight)).not.toEqual(-1); - }); - - it('outerViewBox gets a width and height property based on the DOM size of the element', () => { - const component = createComponent({ - graphData: convertedMetrics[1], - updateAspectRatio: false, - deploymentData, - tagsPath, - projectPath, - }); - - const viewBoxArray = component.outerViewBox.split(' '); - - expect(typeof component.outerViewBox).toEqual('string'); - expect(viewBoxArray[2]).toEqual(component.graphWidth.toString()); - expect(viewBoxArray[3]).toEqual((component.graphHeight - 50).toString()); - }); - }); - - it('has a title for the y-axis and the chart legend that comes from the backend', () => { - const component = createComponent({ - graphData: convertedMetrics[1], - updateAspectRatio: false, - deploymentData, - tagsPath, - projectPath, - }); - - expect(component.yAxisLabel).toEqual(component.graphData.y_label); - expect(component.legendTitle).toEqual(component.graphData.queries[0].label); - }); - - it('sets the currentData object based on the hovered data index', () => { - const component = createComponent({ - graphData: convertedMetrics[1], - updateAspectRatio: false, - deploymentData, - graphIdentifier: 0, - hoverData: { - hoveredDate: new Date('Sun Aug 27 2017 06:11:51 GMT-0500 (CDT)'), - currentDeployXPos: null, - }, - tagsPath, - projectPath, - }); - - // simulate moving mouse over data series - component.seriesUnderMouse = component.timeSeries; - - component.positionFlag(); - - expect(component.currentData).toBe(component.timeSeries[0].values[10]); - }); - - describe('Without data to display', () => { - it('shows a "no data to display" empty state on a graph', done => { - const component = createComponent({ - graphData: queryWithoutData, - deploymentData, - tagsPath, - projectPath, - }); - - Vue.nextTick(() => { - expect( - component.$el.querySelector('.js-no-data-to-display text').textContent.trim(), - ).toEqual('No data to display'); - - done(); - }); - }); - }); -}); diff --git a/spec/javascripts/monitoring/mock_data.js b/spec/javascripts/monitoring/mock_data.js index 18ad9843d22..b4e2cd75d47 100644 --- a/spec/javascripts/monitoring/mock_data.js +++ b/spec/javascripts/monitoring/mock_data.js @@ -6597,58 +6597,46 @@ export function convertDatesMultipleSeries(multipleSeries) { export const environmentData = [ { + id: 34, name: 'production', - size: 1, - latest: { - id: 34, - name: 'production', - state: 'available', - external_url: 'http://root-autodevops-deploy.my-fake-domain.com', - environment_type: null, - stop_action: false, - metrics_path: '/root/hello-prometheus/environments/34/metrics', - environment_path: '/root/hello-prometheus/environments/34', - stop_path: '/root/hello-prometheus/environments/34/stop', - terminal_path: '/root/hello-prometheus/environments/34/terminal', - folder_path: '/root/hello-prometheus/environments/folders/production', - created_at: '2018-06-29T16:53:38.301Z', - updated_at: '2018-06-29T16:57:09.825Z', - last_deployment: { - id: 127, - }, + state: 'available', + external_url: 'http://root-autodevops-deploy.my-fake-domain.com', + environment_type: null, + stop_action: false, + metrics_path: '/root/hello-prometheus/environments/34/metrics', + environment_path: '/root/hello-prometheus/environments/34', + stop_path: '/root/hello-prometheus/environments/34/stop', + terminal_path: '/root/hello-prometheus/environments/34/terminal', + folder_path: '/root/hello-prometheus/environments/folders/production', + created_at: '2018-06-29T16:53:38.301Z', + updated_at: '2018-06-29T16:57:09.825Z', + last_deployment: { + id: 127, }, }, { - name: 'review', - size: 1, - latest: { - id: 35, - name: 'review/noop-branch', - state: 'available', - external_url: 'http://root-autodevops-deploy-review-noop-branc-die93w.my-fake-domain.com', - environment_type: 'review', - stop_action: true, - metrics_path: '/root/hello-prometheus/environments/35/metrics', - environment_path: '/root/hello-prometheus/environments/35', - stop_path: '/root/hello-prometheus/environments/35/stop', - terminal_path: '/root/hello-prometheus/environments/35/terminal', - folder_path: '/root/hello-prometheus/environments/folders/review', - created_at: '2018-07-03T18:39:41.702Z', - updated_at: '2018-07-03T18:44:54.010Z', - last_deployment: { - id: 128, - }, + id: 35, + name: 'review/noop-branch', + state: 'available', + external_url: 'http://root-autodevops-deploy-review-noop-branc-die93w.my-fake-domain.com', + environment_type: 'review', + stop_action: true, + metrics_path: '/root/hello-prometheus/environments/35/metrics', + environment_path: '/root/hello-prometheus/environments/35', + stop_path: '/root/hello-prometheus/environments/35/stop', + terminal_path: '/root/hello-prometheus/environments/35/terminal', + folder_path: '/root/hello-prometheus/environments/folders/review', + created_at: '2018-07-03T18:39:41.702Z', + updated_at: '2018-07-03T18:44:54.010Z', + last_deployment: { + id: 128, }, }, { - name: 'no-deployment', - size: 1, - latest: { - id: 36, - name: 'no-deployment/noop-branch', - state: 'available', - created_at: '2018-07-04T18:39:41.702Z', - updated_at: '2018-07-04T18:44:54.010Z', - }, + id: 36, + name: 'no-deployment/noop-branch', + state: 'available', + created_at: '2018-07-04T18:39:41.702Z', + updated_at: '2018-07-04T18:44:54.010Z', }, ]; diff --git a/spec/javascripts/monitoring/utils/multiple_time_series_spec.js b/spec/javascripts/monitoring/utils/multiple_time_series_spec.js deleted file mode 100644 index 8937b7d9680..00000000000 --- a/spec/javascripts/monitoring/utils/multiple_time_series_spec.js +++ /dev/null @@ -1,22 +0,0 @@ -import createTimeSeries from '~/monitoring/utils/multiple_time_series'; -import { convertDatesMultipleSeries, singleRowMetricsMultipleSeries } from '../mock_data'; - -const convertedMetrics = convertDatesMultipleSeries(singleRowMetricsMultipleSeries); -const { timeSeries } = createTimeSeries(convertedMetrics[0].queries, 428, 272, 120); -const firstTimeSeries = timeSeries[0]; - -describe('Multiple time series', () => { - it('createTimeSeries returned array contains an object for each element', () => { - expect(typeof firstTimeSeries.linePath).toEqual('string'); - expect(typeof firstTimeSeries.areaPath).toEqual('string'); - expect(typeof firstTimeSeries.timeSeriesScaleX).toEqual('function'); - expect(typeof firstTimeSeries.areaColor).toEqual('string'); - expect(typeof firstTimeSeries.lineColor).toEqual('string'); - expect(firstTimeSeries.values instanceof Array).toEqual(true); - }); - - it('createTimeSeries returns an array', () => { - expect(timeSeries instanceof Array).toEqual(true); - expect(timeSeries.length).toEqual(2); - }); -}); diff --git a/spec/javascripts/notes/components/discussion_counter_spec.js b/spec/javascripts/notes/components/discussion_counter_spec.js index d09bc5037ef..fecc0d604b1 100644 --- a/spec/javascripts/notes/components/discussion_counter_spec.js +++ b/spec/javascripts/notes/components/discussion_counter_spec.js @@ -33,11 +33,13 @@ describe('DiscussionCounter component', () => { ...discussionMock, id: discussionMock.id, notes: [{ ...discussionMock.notes[0], resolvable: true, resolved: true }], + resolved: true, }, { ...discussionMock, id: discussionMock.id + 1, notes: [{ ...discussionMock.notes[0], resolvable: true, resolved: false }], + resolved: false, }, ]; const firstDiscussionId = discussionMock.id + 1; diff --git a/spec/javascripts/notes/components/discussion_jump_to_next_button_spec.js b/spec/javascripts/notes/components/discussion_jump_to_next_button_spec.js new file mode 100644 index 00000000000..c41b29fa788 --- /dev/null +++ b/spec/javascripts/notes/components/discussion_jump_to_next_button_spec.js @@ -0,0 +1,33 @@ +import jumpToNextDiscussionButton from '~/notes/components/discussion_jump_to_next_button.vue'; +import { shallowMount, createLocalVue } from '@vue/test-utils'; + +const localVue = createLocalVue(); + +describe('jumpToNextDiscussionButton', () => { + let wrapper; + + beforeEach(() => { + wrapper = shallowMount(jumpToNextDiscussionButton, { + localVue, + sync: false, + }); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('emits onClick event on button click', done => { + const button = wrapper.find({ ref: 'button' }); + + button.trigger('click'); + + localVue.nextTick(() => { + expect(wrapper.emitted()).toEqual({ + onClick: [[]], + }); + + done(); + }); + }); +}); diff --git a/spec/javascripts/notes/components/discussion_resolve_button_spec.js b/spec/javascripts/notes/components/discussion_resolve_button_spec.js new file mode 100644 index 00000000000..5024f40ec5d --- /dev/null +++ b/spec/javascripts/notes/components/discussion_resolve_button_spec.js @@ -0,0 +1,74 @@ +import resolveDiscussionButton from '~/notes/components/discussion_resolve_button.vue'; +import { createLocalVue, shallowMount } from '@vue/test-utils'; + +const buttonTitle = 'Resolve discussion'; + +describe('resolveDiscussionButton', () => { + let wrapper; + let localVue; + + const factory = options => { + localVue = createLocalVue(); + wrapper = shallowMount(resolveDiscussionButton, { + localVue, + ...options, + }); + }; + + beforeEach(() => { + factory({ + propsData: { + isResolving: false, + buttonTitle, + }, + }); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('should emit a onClick event on button click', () => { + const button = wrapper.find({ ref: 'button' }); + + button.trigger('click'); + + expect(wrapper.emitted()).toEqual({ + onClick: [[]], + }); + }); + + it('should contain the provided button title', () => { + const button = wrapper.find({ ref: 'button' }); + + expect(button.text()).toContain(buttonTitle); + }); + + it('should show a loading spinner while resolving', () => { + factory({ + propsData: { + isResolving: true, + buttonTitle, + }, + }); + + const button = wrapper.find({ ref: 'isResolvingIcon' }); + + expect(button.exists()).toEqual(true); + }); + + it('should only show a loading spinner while resolving', () => { + factory({ + propsData: { + isResolving: false, + buttonTitle, + }, + }); + + const button = wrapper.find({ ref: 'isResolvingIcon' }); + + localVue.nextTick(() => { + expect(button.exists()).toEqual(false); + }); + }); +}); diff --git a/spec/javascripts/notes/components/note_actions_spec.js b/spec/javascripts/notes/components/note_actions_spec.js index f6c854e6def..b102b7aecf7 100644 --- a/spec/javascripts/notes/components/note_actions_spec.js +++ b/spec/javascripts/notes/components/note_actions_spec.js @@ -1,20 +1,19 @@ import Vue from 'vue'; +import { shallowMount, createLocalVue } from '@vue/test-utils'; import createStore from '~/notes/stores'; import noteActions from '~/notes/components/note_actions.vue'; import { userDataMock } from '../mock_data'; -describe('issue_note_actions component', () => { - let vm; +describe('noteActions', () => { + let wrapper; let store; - let Component; beforeEach(() => { - Component = Vue.extend(noteActions); store = createStore(); }); afterEach(() => { - vm.$destroy(); + wrapper.destroy(); }); describe('user is logged in', () => { @@ -36,45 +35,57 @@ describe('issue_note_actions component', () => { store.dispatch('setUserData', userDataMock); - vm = new Component({ + const localVue = createLocalVue(); + wrapper = shallowMount(noteActions, { store, propsData: props, - }).$mount(); + localVue, + sync: false, + }); }); it('should render access level badge', () => { - expect(vm.$el.querySelector('.note-role').textContent.trim()).toEqual(props.accessLevel); + expect( + wrapper + .find('.note-role') + .text() + .trim(), + ).toEqual(props.accessLevel); }); it('should render emoji link', () => { - expect(vm.$el.querySelector('.js-add-award')).toBeDefined(); + expect(wrapper.find('.js-add-award').exists()).toBe(true); }); describe('actions dropdown', () => { it('should be possible to edit the comment', () => { - expect(vm.$el.querySelector('.js-note-edit')).toBeDefined(); + expect(wrapper.find('.js-note-edit').exists()).toBe(true); }); it('should be possible to report abuse to GitLab', () => { - expect(vm.$el.querySelector(`a[href="${props.reportAbusePath}"]`)).toBeDefined(); + expect(wrapper.find(`a[href="${props.reportAbusePath}"]`).exists()).toBe(true); }); it('should be possible to copy link to a note', () => { - expect(vm.$el.querySelector('.js-btn-copy-note-link')).not.toBeNull(); + expect(wrapper.find('.js-btn-copy-note-link').exists()).toBe(true); }); it('should not show copy link action when `noteUrl` prop is empty', done => { - vm.noteUrl = ''; + wrapper.setProps({ + ...props, + noteUrl: '', + }); + Vue.nextTick() .then(() => { - expect(vm.$el.querySelector('.js-btn-copy-note-link')).toBeNull(); + expect(wrapper.find('.js-btn-copy-note-link').exists()).toBe(false); }) .then(done) .catch(done.fail); }); it('should be possible to delete comment', () => { - expect(vm.$el.querySelector('.js-note-delete')).toBeDefined(); + expect(wrapper.find('.js-note-delete').exists()).toBe(true); }); }); }); @@ -96,18 +107,21 @@ describe('issue_note_actions component', () => { reportAbusePath: '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F7%23note_539&user_id=26', }; - vm = new Component({ + const localVue = createLocalVue(); + wrapper = shallowMount(noteActions, { store, propsData: props, - }).$mount(); + localVue, + sync: false, + }); }); it('should not render emoji link', () => { - expect(vm.$el.querySelector('.js-add-award')).toEqual(null); + expect(wrapper.find('.js-add-award').exists()).toBe(false); }); it('should not render actions dropdown', () => { - expect(vm.$el.querySelector('.more-actions')).toEqual(null); + expect(wrapper.find('.more-actions').exists()).toBe(false); }); }); }); diff --git a/spec/javascripts/notes/components/noteable_discussion_spec.js b/spec/javascripts/notes/components/noteable_discussion_spec.js index 3aff2dd0641..c4b7eb17393 100644 --- a/spec/javascripts/notes/components/noteable_discussion_spec.js +++ b/spec/javascripts/notes/components/noteable_discussion_spec.js @@ -1,4 +1,4 @@ -import Vue from 'vue'; +import { shallowMount, createLocalVue } from '@vue/test-utils'; import createStore from '~/notes/stores'; import noteableDiscussion from '~/notes/components/noteable_discussion.vue'; import '~/behaviors/markdown/render_gfm'; @@ -8,9 +8,8 @@ import mockDiffFile from '../../diffs/mock_data/diff_file'; const discussionWithTwoUnresolvedNotes = 'merge_requests/resolved_diff_discussion.json'; describe('noteable_discussion component', () => { - const Component = Vue.extend(noteableDiscussion); let store; - let vm; + let wrapper; preloadFixtures(discussionWithTwoUnresolvedNotes); @@ -20,54 +19,62 @@ describe('noteable_discussion component', () => { store.dispatch('setNoteableData', noteableDataMock); store.dispatch('setNotesData', notesDataMock); - vm = new Component({ + const localVue = createLocalVue(); + wrapper = shallowMount(noteableDiscussion, { store, propsData: { discussion: discussionMock }, - }).$mount(); + localVue, + sync: false, + }); }); afterEach(() => { - vm.$destroy(); + wrapper.destroy(); }); it('should render user avatar', () => { - expect(vm.$el.querySelector('.user-avatar-link')).not.toBeNull(); + expect(wrapper.find('.user-avatar-link').exists()).toBe(true); }); it('should not render discussion header for non diff discussions', () => { - expect(vm.$el.querySelector('.discussion-header')).toBeNull(); + expect(wrapper.find('.discussion-header').exists()).toBe(false); }); - it('should render discussion header', () => { + it('should render discussion header', done => { const discussion = { ...discussionMock }; discussion.diff_file = mockDiffFile; discussion.diff_discussion = true; - vm.$destroy(); - vm = new Component({ - store, - propsData: { discussion }, - }).$mount(); + wrapper.setProps({ discussion }); - expect(vm.$el.querySelector('.discussion-header')).not.toBeNull(); + wrapper.vm + .$nextTick() + .then(() => { + expect(wrapper.find('.discussion-header').exists()).toBe(true); + }) + .then(done) + .catch(done.fail); }); describe('actions', () => { it('should render reply button', () => { - expect(vm.$el.querySelector('.js-vue-discussion-reply').textContent.trim()).toEqual( - 'Reply...', - ); + expect( + wrapper + .find('.js-vue-discussion-reply') + .text() + .trim(), + ).toEqual('Reply...'); }); it('should toggle reply form', done => { - vm.$el.querySelector('.js-vue-discussion-reply').click(); + wrapper.find('.js-vue-discussion-reply').trigger('click'); - Vue.nextTick(() => { - expect(vm.isReplying).toEqual(true); + wrapper.vm.$nextTick(() => { + expect(wrapper.vm.isReplying).toEqual(true); // There is a watcher for `isReplying` which will init autosave in the next tick - Vue.nextTick(() => { - expect(vm.$refs.noteForm).not.toBeNull(); + wrapper.vm.$nextTick(() => { + expect(wrapper.vm.$refs.noteForm).not.toBeNull(); done(); }); }); @@ -75,8 +82,8 @@ describe('noteable_discussion component', () => { it('does not render jump to discussion button', () => { expect( - vm.$el.querySelector('*[data-original-title="Jump to next unresolved discussion"]'), - ).toBeNull(); + wrapper.find('*[data-original-title="Jump to next unresolved discussion"]').exists(), + ).toBe(false); }); }); @@ -87,12 +94,13 @@ describe('noteable_discussion component', () => { discussion2.resolved = false; discussion2.active = true; discussion2.id = 'next'; // prepare this for being identified as next one (to be jumped to) - vm.$store.dispatch('setInitialNotes', [discussionMock, discussion2]); + store.dispatch('setInitialNotes', [discussionMock, discussion2]); window.mrTabs.currentAction = 'show'; - Vue.nextTick() + wrapper.vm + .$nextTick() .then(() => { - spyOn(vm, 'expandDiscussion').and.stub(); + spyOn(wrapper.vm, 'expandDiscussion').and.stub(); const nextDiscussionId = discussion2.id; @@ -100,9 +108,11 @@ describe('noteable_discussion component', () => { <div class="discussion" data-discussion-id="${nextDiscussionId}"></div> `); - vm.jumpToNextDiscussion(); + wrapper.vm.jumpToNextDiscussion(); - expect(vm.expandDiscussion).toHaveBeenCalledWith({ discussionId: nextDiscussionId }); + expect(wrapper.vm.expandDiscussion).toHaveBeenCalledWith({ + discussionId: nextDiscussionId, + }); }) .then(done) .catch(done.fail); @@ -117,7 +127,7 @@ describe('noteable_discussion component', () => { notes: [{ body: 'hello world!' }], }; - const note = vm.componentData(data); + const note = wrapper.vm.componentData(data); expect(note).toEqual(data.notes[0]); }); @@ -127,7 +137,7 @@ describe('noteable_discussion component', () => { notes: [{ id: 12 }], }; - const note = vm.componentData(data); + const note = wrapper.vm.componentData(data); expect(note).toEqual(data); }); @@ -138,46 +148,48 @@ describe('noteable_discussion component', () => { const truncatedCommitId = commitId.substr(0, 8); let commitElement; - beforeEach(() => { - vm.$destroy(); - + beforeEach(done => { store.state.diffs = { projectPath: 'something', }; - vm = new Component({ - propsData: { - discussion: { - ...discussionMock, - for_commit: true, - commit_id: commitId, - diff_discussion: true, - diff_file: { - ...mockDiffFile, - }, + wrapper.setProps({ + discussion: { + ...discussionMock, + for_commit: true, + commit_id: commitId, + diff_discussion: true, + diff_file: { + ...mockDiffFile, }, - renderDiffFile: true, }, - store, - }).$mount(); + renderDiffFile: true, + }); - commitElement = vm.$el.querySelector('.commit-sha'); + wrapper.vm + .$nextTick() + .then(() => { + commitElement = wrapper.find('.commit-sha'); + }) + .then(done) + .catch(done.fail); }); describe('for commit discussions', () => { it('should display a monospace started a discussion on commit', () => { - expect(vm.$el).toContainText(`started a discussion on commit ${truncatedCommitId}`); - expect(commitElement).not.toBe(null); - expect(commitElement).toHaveText(truncatedCommitId); + expect(wrapper.text()).toContain(`started a discussion on commit ${truncatedCommitId}`); + expect(commitElement.exists()).toBe(true); + expect(commitElement.text()).toContain(truncatedCommitId); }); }); describe('for diff discussion with a commit id', () => { it('should display started discussion on commit header', done => { - vm.discussion.for_commit = false; + wrapper.vm.discussion.for_commit = false; + + wrapper.vm.$nextTick(() => { + expect(wrapper.text()).toContain(`started a discussion on commit ${truncatedCommitId}`); - vm.$nextTick(() => { - expect(vm.$el).toContainText(`started a discussion on commit ${truncatedCommitId}`); expect(commitElement).not.toBe(null); done(); @@ -185,11 +197,11 @@ describe('noteable_discussion component', () => { }); it('should display outdated change on commit header', done => { - vm.discussion.for_commit = false; - vm.discussion.active = false; + wrapper.vm.discussion.for_commit = false; + wrapper.vm.discussion.active = false; - vm.$nextTick(() => { - expect(vm.$el).toContainText( + wrapper.vm.$nextTick(() => { + expect(wrapper.text()).toContain( `started a discussion on an outdated change in commit ${truncatedCommitId}`, ); @@ -202,27 +214,27 @@ describe('noteable_discussion component', () => { describe('for diff discussions without a commit id', () => { it('should show started a discussion on the diff text', done => { - Object.assign(vm.discussion, { + Object.assign(wrapper.vm.discussion, { for_commit: false, commit_id: null, }); - vm.$nextTick(() => { - expect(vm.$el).toContainText('started a discussion on the diff'); + wrapper.vm.$nextTick(() => { + expect(wrapper.text()).toContain('started a discussion on the diff'); done(); }); }); it('should show discussion on older version text', done => { - Object.assign(vm.discussion, { + Object.assign(wrapper.vm.discussion, { for_commit: false, commit_id: null, active: false, }); - vm.$nextTick(() => { - expect(vm.$el).toContainText('started a discussion on an old version of the diff'); + wrapper.vm.$nextTick(() => { + expect(wrapper.text()).toContain('started a discussion on an old version of the diff'); done(); }); 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..b2b0a50911d 100644 --- a/spec/javascripts/test_bundle.js +++ b/spec/javascripts/test_bundle.js @@ -21,6 +21,16 @@ Vue.config.productionTip = false; let hasVueWarnings = false; Vue.config.warnHandler = (msg, vm, trace) => { + // The following workaround is necessary, so we are able to use setProps from Vue test utils + // see https://github.com/vuejs/vue-test-utils/issues/631#issuecomment-421108344 + const currentStack = new Error().stack; + const isInVueTestUtils = currentStack + .split('\n') + .some(line => line.startsWith(' at VueWrapper.setProps (')); + if (isInVueTestUtils) { + return; + } + hasVueWarnings = true; fail(`${msg}${trace}`); }; @@ -187,6 +197,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_merge_when_pipeline_succeeds_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_merge_when_pipeline_succeeds_spec.js index d46ad0acc9b..b9718a78fa4 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_merge_when_pipeline_succeeds_spec.js +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_merge_when_pipeline_succeeds_spec.js @@ -121,14 +121,14 @@ describe('MRWidgetMergeWhenPipelineSucceeds', () => { expect(vm.$el.innerText).toContain('to be merged automatically when the pipeline succeeds'); expect(vm.$el.innerText).toContain('The changes will be merged into'); expect(vm.$el.innerText).toContain(targetBranch); - expect(vm.$el.innerText).toContain('The source branch will not be removed'); + expect(vm.$el.innerText).toContain('The source branch will not be deleted'); expect(vm.$el.querySelector('.js-cancel-auto-merge').innerText).toContain( 'Cancel automatic merge', ); expect(vm.$el.querySelector('.js-cancel-auto-merge').getAttribute('disabled')).toBeFalsy(); expect(vm.$el.querySelector('.js-remove-source-branch').innerText).toContain( - 'Remove source branch', + 'Delete source branch', ); expect(vm.$el.querySelector('.js-remove-source-branch').getAttribute('disabled')).toBeFalsy(); @@ -143,19 +143,19 @@ describe('MRWidgetMergeWhenPipelineSucceeds', () => { }); }); - it('should show source branch will be removed text when it source branch set to remove', done => { + it('should show source branch will be deleted text when it source branch set to remove', done => { vm.mr.shouldRemoveSourceBranch = true; Vue.nextTick(() => { const normalizedText = vm.$el.innerText.replace(/\s+/g, ' '); - expect(normalizedText).toContain('The source branch will be removed'); - expect(normalizedText).not.toContain('The source branch will not be removed'); + expect(normalizedText).toContain('The source branch will be deleted'); + expect(normalizedText).not.toContain('The source branch will not be deleted'); done(); }); }); - it('should not show remove source branch button when user not able to remove source branch', done => { + it('should not show delete source branch button when user not able to delete source branch', done => { vm.mr.currentUserId = 4; Vue.nextTick(() => { @@ -164,7 +164,7 @@ describe('MRWidgetMergeWhenPipelineSucceeds', () => { }); }); - it('should disable remove source branch button when the action is in progress', done => { + it('should disable delete source branch button when the action is in progress', done => { vm.isRemovingSourceBranch = true; Vue.nextTick(() => { diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_merged_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_merged_spec.js index da5cb752c6f..1683da805b9 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_merged_spec.js +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_merged_spec.js @@ -128,7 +128,7 @@ describe('MRWidgetMerged', () => { new Promise(resolve => { resolve({ data: { - message: 'Branch was removed', + message: 'Branch was deleted', }, }); }), @@ -157,8 +157,8 @@ describe('MRWidgetMerged', () => { expect(vm.$el.textContent).toContain(targetBranch); }); - it('renders information about branch being removed', () => { - expect(vm.$el.textContent).toContain('The source branch has been removed'); + it('renders information about branch being deleted', () => { + expect(vm.$el.textContent).toContain('The source branch has been deleted'); }); it('shows revert and cherry-pick buttons', () => { @@ -189,24 +189,24 @@ describe('MRWidgetMerged', () => { expect(selectors.mergeCommitShaLink.href).toBe(vm.mr.mergeCommitPath); }); - it('should not show source branch removed text', done => { + it('should not show source branch deleted text', done => { vm.mr.sourceBranchRemoved = false; Vue.nextTick(() => { - expect(vm.$el.innerText).toContain('You can remove source branch now'); - expect(vm.$el.innerText).not.toContain('The source branch has been removed'); + expect(vm.$el.innerText).toContain('You can delete the source branch now'); + expect(vm.$el.innerText).not.toContain('The source branch has been deleted'); done(); }); }); - it('should show source branch removing text', done => { + it('should show source branch deleting text', done => { vm.mr.isRemovingSourceBranch = true; vm.mr.sourceBranchRemoved = false; Vue.nextTick(() => { - expect(vm.$el.innerText).toContain('The source branch is being removed'); - expect(vm.$el.innerText).not.toContain('You can remove source branch now'); - expect(vm.$el.innerText).not.toContain('The source branch has been removed'); + expect(vm.$el.innerText).toContain('The source branch is being deleted'); + expect(vm.$el.innerText).not.toContain('You can delete the source branch now'); + expect(vm.$el.innerText).not.toContain('The source branch has been deleted'); done(); }); }); diff --git a/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js b/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js index 99b80df766a..ff08a46b922 100644 --- a/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js +++ b/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js @@ -5,7 +5,7 @@ import notify from '~/lib/utils/notify'; import { stateKey } from '~/vue_merge_request_widget/stores/state_maps'; import mountComponent from 'spec/helpers/vue_mount_component_helper'; import mockData from './mock_data'; -import { faviconDataUrl, overlayDataUrl, faviconWithOverlayDataUrl } from '../lib/utils/mock_data'; +import { faviconDataUrl, overlayDataUrl } from '../lib/utils/mock_data'; const returnPromise = data => new Promise(resolve => { @@ -340,17 +340,27 @@ describe('mrWidgetOptions', () => { vm.mr.ciStatusFaviconPath = overlayDataUrl; vm.setFaviconHelper() .then(() => { - expect(faviconElement.getAttribute('href')).toEqual(faviconWithOverlayDataUrl); + /* + It would be better if we'd could mock commonUtils.setFaviconURL + with a spy and test that it was called. We are doing the following + tests as a proxy to show that the function has been called + */ + expect(faviconElement.getAttribute('href')).not.toEqual(null); + expect(faviconElement.getAttribute('href')).not.toEqual(overlayDataUrl); + expect(faviconElement.getAttribute('href')).not.toEqual(faviconDataUrl); done(); }) .catch(done.fail); }); - it('should not call setFavicon when there is no ciStatusFaviconPath', () => { + it('should not call setFavicon when there is no ciStatusFaviconPath', done => { vm.mr.ciStatusFaviconPath = null; - vm.setFaviconHelper(); - - expect(faviconElement.getAttribute('href')).toEqual(null); + vm.setFaviconHelper() + .then(() => { + expect(faviconElement.getAttribute('href')).toEqual(null); + done(); + }) + .catch(done.fail); }); }); @@ -453,7 +463,7 @@ describe('mrWidgetOptions', () => { vm.$nextTick(() => { const tooltip = vm.$el.querySelector('.fa-question-circle'); - expect(vm.$el.textContent).toContain('Removes source branch'); + expect(vm.$el.textContent).toContain('Deletes source branch'); expect(tooltip.getAttribute('data-original-title')).toBe( 'A user with write access to the source branch selected this option', ); @@ -468,8 +478,8 @@ describe('mrWidgetOptions', () => { vm.mr.state = 'merged'; vm.$nextTick(() => { - expect(vm.$el.textContent).toContain('The source branch has been removed'); - expect(vm.$el.textContent).not.toContain('Removes source branch'); + expect(vm.$el.textContent).toContain('The source branch has been deleted'); + expect(vm.$el.textContent).not.toContain('Deletes source branch'); done(); }); diff --git a/spec/javascripts/vue_shared/components/filtered_search_dropdown_spec.js b/spec/javascripts/vue_shared/components/filtered_search_dropdown_spec.js index b84b5ae67a8..3d251426b5a 100644 --- a/spec/javascripts/vue_shared/components/filtered_search_dropdown_spec.js +++ b/spec/javascripts/vue_shared/components/filtered_search_dropdown_spec.js @@ -88,4 +88,103 @@ describe('Filtered search dropdown', () => { }); }); }); + + describe('with create mode enabled', () => { + describe('when there are no matches', () => { + beforeEach(() => { + vm = mountComponent(Component, { + items: [ + { title: 'One' }, + { title: 'Two/three' }, + { title: 'Three four' }, + { title: 'Five' }, + ], + filterKey: 'title', + showCreateMode: true, + }); + + vm.$el.querySelector('.js-filtered-dropdown-input').value = 'eleven'; + vm.$el.querySelector('.js-filtered-dropdown-input').dispatchEvent(new Event('input')); + }); + + it('renders a create button', done => { + vm.$nextTick(() => { + expect(vm.$el.querySelector('.js-dropdown-create-button')).not.toBeNull(); + done(); + }); + }); + + it('renders computed button text', done => { + vm.$nextTick(() => { + expect(vm.$el.querySelector('.js-dropdown-create-button').textContent.trim()).toEqual( + 'Create eleven', + ); + done(); + }); + }); + + describe('on click create button', () => { + it('emits createItem event with the filter', done => { + spyOn(vm, '$emit'); + vm.$nextTick(() => { + vm.$el.querySelector('.js-dropdown-create-button').click(); + + expect(vm.$emit).toHaveBeenCalledWith('createItem', 'eleven'); + done(); + }); + }); + }); + }); + + describe('when there are matches', () => { + beforeEach(() => { + vm = mountComponent(Component, { + items: [ + { title: 'One' }, + { title: 'Two/three' }, + { title: 'Three four' }, + { title: 'Five' }, + ], + filterKey: 'title', + showCreateMode: true, + }); + + vm.$el.querySelector('.js-filtered-dropdown-input').value = 'one'; + vm.$el.querySelector('.js-filtered-dropdown-input').dispatchEvent(new Event('input')); + }); + + it('does not render a create button', done => { + vm.$nextTick(() => { + expect(vm.$el.querySelector('.js-dropdown-create-button')).toBeNull(); + done(); + }); + }); + }); + }); + + describe('with create mode disabled', () => { + describe('when there are no matches', () => { + beforeEach(() => { + vm = mountComponent(Component, { + items: [ + { title: 'One' }, + { title: 'Two/three' }, + { title: 'Three four' }, + { title: 'Five' }, + ], + filterKey: 'title', + }); + + vm.$el.querySelector('.js-filtered-dropdown-input').value = 'eleven'; + vm.$el.querySelector('.js-filtered-dropdown-input').dispatchEvent(new Event('input')); + }); + + it('does not render a create button', done => { + vm.$nextTick(() => { + expect(vm.$el.querySelector('.js-dropdown-create-button')).toBeNull(); + done(); + }); + }); + }); + }); }); 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/javascripts/vue_shared/components/user_popover/user_popover_spec.js b/spec/javascripts/vue_shared/components/user_popover/user_popover_spec.js index de3e0c149de..e8b41e8eeff 100644 --- a/spec/javascripts/vue_shared/components/user_popover/user_popover_spec.js +++ b/spec/javascripts/vue_shared/components/user_popover/user_popover_spec.js @@ -122,7 +122,7 @@ describe('User Popover Component', () => { describe('status data', () => { it('should show only message', () => { const testProps = Object.assign({}, DEFAULT_PROPS); - testProps.user.status = { message: 'Hello World' }; + testProps.user.status = { message_html: 'Hello World' }; vm = mountComponent(UserPopover, { ...DEFAULT_PROPS, @@ -134,12 +134,12 @@ describe('User Popover Component', () => { it('should show message and emoji', () => { const testProps = Object.assign({}, DEFAULT_PROPS); - testProps.user.status = { emoji: 'basketball_player', message: 'Hello World' }; + testProps.user.status = { emoji: 'basketball_player', message_html: 'Hello World' }; vm = mountComponent(UserPopover, { ...DEFAULT_PROPS, target: document.querySelector('.js-user-link'), - status: { emoji: 'basketball_player', message: 'Hello World' }, + status: { emoji: 'basketball_player', message_html: 'Hello World' }, }); expect(vm.$el.textContent).toContain('Hello World'); diff --git a/spec/lib/api/helpers/pagination_spec.rb b/spec/lib/api/helpers/pagination_spec.rb index 0a7682d906b..2890aa4ae38 100644 --- a/spec/lib/api/helpers/pagination_spec.rb +++ b/spec/lib/api/helpers/pagination_spec.rb @@ -237,26 +237,89 @@ describe API::Helpers::Pagination do .and_return({ page: 1, per_page: 2 }) end - it 'returns appropriate amount of resources' do - expect(subject.paginate(resource).count).to eq 2 + shared_examples 'response with pagination headers' do + it 'adds appropriate headers' do + expect_header('X-Total', '3') + expect_header('X-Total-Pages', '2') + expect_header('X-Per-Page', '2') + expect_header('X-Page', '1') + expect_header('X-Next-Page', '2') + expect_header('X-Prev-Page', '') + + expect_header('Link', anything) do |_key, val| + expect(val).to include('rel="first"') + expect(val).to include('rel="last"') + expect(val).to include('rel="next"') + expect(val).not_to include('rel="prev"') + end + + subject.paginate(resource) + end end - it 'adds appropriate headers' do - expect_header('X-Total', '3') - expect_header('X-Total-Pages', '2') - expect_header('X-Per-Page', '2') - expect_header('X-Page', '1') - expect_header('X-Next-Page', '2') - expect_header('X-Prev-Page', '') + shared_examples 'paginated response' do + it 'returns appropriate amount of resources' do + expect(subject.paginate(resource).count).to eq 2 + end - expect_header('Link', anything) do |_key, val| - expect(val).to include('rel="first"') - expect(val).to include('rel="last"') - expect(val).to include('rel="next"') - expect(val).not_to include('rel="prev"') + it 'executes only one SELECT COUNT query' do + expect { subject.paginate(resource) }.to make_queries_matching(/SELECT COUNT/, 1) end + end - subject.paginate(resource) + context 'when the api_kaminari_count_with_limit feature flag is unset' do + it_behaves_like 'paginated response' + it_behaves_like 'response with pagination headers' + end + + context 'when the api_kaminari_count_with_limit feature flag is disabled' do + before do + stub_feature_flags(api_kaminari_count_with_limit: false) + end + + it_behaves_like 'paginated response' + it_behaves_like 'response with pagination headers' + end + + context 'when the api_kaminari_count_with_limit feature flag is enabled' do + before do + stub_feature_flags(api_kaminari_count_with_limit: true) + end + + context 'when resources count is less than MAX_COUNT_LIMIT' do + before do + stub_const("::Kaminari::ActiveRecordRelationMethods::MAX_COUNT_LIMIT", 4) + end + + it_behaves_like 'paginated response' + it_behaves_like 'response with pagination headers' + end + + context 'when resources count is more than MAX_COUNT_LIMIT' do + before do + stub_const("::Kaminari::ActiveRecordRelationMethods::MAX_COUNT_LIMIT", 2) + end + + it_behaves_like 'paginated response' + + it 'does not return the X-Total and X-Total-Pages headers' do + expect_no_header('X-Total') + expect_no_header('X-Total-Pages') + expect_header('X-Per-Page', '2') + expect_header('X-Page', '1') + expect_header('X-Next-Page', '2') + expect_header('X-Prev-Page', '') + + expect_header('Link', anything) do |_key, val| + expect(val).to include('rel="first"') + expect(val).not_to include('rel="last"') + expect(val).to include('rel="next"') + expect(val).not_to include('rel="prev"') + end + + subject.paginate(resource) + end + end end end @@ -348,6 +411,10 @@ describe API::Helpers::Pagination do expect(subject).to receive(:header).with(*args, &block) end + def expect_no_header(*args, &block) + expect(subject).not_to receive(:header).with(*args) + end + def expect_message(method) expect(subject).to receive(method) .at_least(:once).and_return(value) diff --git a/spec/lib/banzai/color_parser_spec.rb b/spec/lib/banzai/color_parser_spec.rb index a1cb0c07b06..af2a8f215c1 100644 --- a/spec/lib/banzai/color_parser_spec.rb +++ b/spec/lib/banzai/color_parser_spec.rb @@ -57,7 +57,7 @@ describe Banzai::ColorParser do context 'HSL format' do [ - 'hsl(0,0%,0%)', 'hsl(0,100%,100%)', + 'hsl(0,0%,0%)', 'hsl(0,100%,100%)', 'hsl(540,0%,0%)', 'hsl(-720,0%,0%)', 'hsl(0deg,0%,0%)', 'hsl(0DEG,0%,0%)', 'hsl(0, 0%, 0%)', 'HSL(0,0%,0%)', diff --git a/spec/lib/banzai/filter/autolink_filter_spec.rb b/spec/lib/banzai/filter/autolink_filter_spec.rb index 7a457403b51..6217381c491 100644 --- a/spec/lib/banzai/filter/autolink_filter_spec.rb +++ b/spec/lib/banzai/filter/autolink_filter_spec.rb @@ -188,6 +188,22 @@ describe Banzai::Filter::AutolinkFilter do expect(doc.at_css('a')['class']).to eq 'custom' end + it 'escapes RTLO and other characters' do + # rendered text looks like "http://example.com/evilexe.mp3" + evil_link = "#{link}evil\u202E3pm.exe" + doc = filter("#{evil_link}") + + expect(doc.at_css('a')['href']).to eq "http://about.gitlab.com/evil%E2%80%AE3pm.exe" + end + + it 'encodes international domains' do + link = "http://one😄two.com" + expected = "http://one%F0%9F%98%84two.com" + doc = filter(link) + + expect(doc.at_css('a')['href']).to eq expected + end + described_class::IGNORE_PARENTS.each do |elem| it "ignores valid links contained inside '#{elem}' element" do exp = act = "<#{elem}>See #{link}</#{elem}>" diff --git a/spec/lib/banzai/filter/commit_range_reference_filter_spec.rb b/spec/lib/banzai/filter/commit_range_reference_filter_spec.rb index cbff2fdab14..4daf6be1bb7 100644 --- a/spec/lib/banzai/filter/commit_range_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/commit_range_reference_filter_spec.rb @@ -205,7 +205,7 @@ describe Banzai::Filter::CommitRangeReferenceFilter do context 'cross-project URL reference' do let(:namespace) { create(:namespace) } let(:project2) { create(:project, :public, :repository, namespace: namespace) } - let(:range) { CommitRange.new("#{commit1.id}...master", project) } + let(:range) { CommitRange.new("#{commit1.id}...master", project) } let(:reference) { urls.project_compare_url(project2, from: commit1.id, to: 'master') } before do diff --git a/spec/lib/banzai/filter/external_link_filter_spec.rb b/spec/lib/banzai/filter/external_link_filter_spec.rb index e6dae8d5382..2acbe05f082 100644 --- a/spec/lib/banzai/filter/external_link_filter_spec.rb +++ b/spec/lib/banzai/filter/external_link_filter_spec.rb @@ -62,6 +62,13 @@ describe Banzai::Filter::ExternalLinkFilter do expect(doc.to_html).to eq(expected) end + + it 'adds rel and target to improperly formatted autolinks' do + doc = filter %q(<p><a href="mailto://jblogs@example.com">mailto://jblogs@example.com</a></p>) + expected = %q(<p><a href="mailto://jblogs@example.com" rel="nofollow noreferrer noopener" target="_blank">mailto://jblogs@example.com</a></p>) + + expect(doc.to_html).to eq(expected) + end end context 'for links with a username' do @@ -112,4 +119,62 @@ describe Banzai::Filter::ExternalLinkFilter do it_behaves_like 'an external link with rel attribute' end + + context 'links with RTLO character' do + # In rendered text this looks like "http://example.com/evilexe.mp3" + let(:doc) { filter %Q(<a href="http://example.com/evil%E2%80%AE3pm.exe">http://example.com/evil\u202E3pm.exe</a>) } + + it_behaves_like 'an external link with rel attribute' + + it 'escapes RTLO in link text' do + expected = %q(http://example.com/evil%E2%80%AE3pm.exe</a>) + + expect(doc.to_html).to include(expected) + end + + it 'does not mangle the link text' do + doc = filter %Q(<a href="http://example.com">One<span>and</span>\u202Eexe.mp3</a>) + + expect(doc.to_html).to include('One<span>and</span>%E2%80%AEexe.mp3</a>') + end + end + + context 'for generated autolinks' do + context 'with an IDN character' do + let(:doc) { filter(%q(<a href="http://exa%F0%9F%98%84mple.com">http://exa😄mple.com</a>)) } + let(:doc_email) { filter(%q(<a href="http://exa%F0%9F%98%84mple.com">http://exa😄mple.com</a>), emailable_links: true) } + + it_behaves_like 'an external link with rel attribute' + + it 'does not change the link text' do + expect(doc.to_html).to include('http://exa😄mple.com</a>') + end + + it 'uses punycode for emails' do + expect(doc_email.to_html).to include('http://xn--example-6p25f.com/</a>') + end + end + end + + context 'for links that look malicious' do + context 'with an IDN character' do + let(:doc) { filter %q(<a href="http://exa%F0%9F%98%84mple.com">http://exa😄mple.com</a>) } + + it 'adds a toolip with punycode' do + expect(doc.to_html).to include('http://exa😄mple.com</a>') + expect(doc.to_html).to include('class="has-tooltip"') + expect(doc.to_html).to include('title="http://xn--example-6p25f.com/"') + end + end + + context 'with RTLO character' do + let(:doc) { filter %q(<a href="http://example.com/evil%E2%80%AE3pm.exe">Evil Test</a>) } + + it 'adds a toolip with punycode' do + expect(doc.to_html).to include('Evil Test</a>') + expect(doc.to_html).to include('class="has-tooltip"') + expect(doc.to_html).to include('title="http://example.com/evil%E2%80%AE3pm.exe"') + end + end + end end 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/issue_reference_filter_spec.rb b/spec/lib/banzai/filter/issue_reference_filter_spec.rb index 905fbb9434b..914c4e2d823 100644 --- a/spec/lib/banzai/filter/issue_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/issue_reference_filter_spec.rb @@ -445,7 +445,7 @@ describe Banzai::Filter::IssueReferenceFilter do end describe '.references_in' do - let(:merge_request) { create(:merge_request) } + let(:merge_request) { create(:merge_request) } it 'yields valid references' do expect do |b| diff --git a/spec/lib/banzai/filter/label_reference_filter_spec.rb b/spec/lib/banzai/filter/label_reference_filter_spec.rb index 9cfdb9e53a2..108d7b43a26 100644 --- a/spec/lib/banzai/filter/label_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/label_reference_filter_spec.rb @@ -237,7 +237,7 @@ describe Banzai::Filter::LabelReferenceFilter do end context 'References with html entities' do - let!(:label) { create(:label, name: '<html>', project: project) } + let!(:label) { create(:label, name: '<html>', project: project) } it 'links to a valid reference' do doc = reference_filter('See ~"<html>"') diff --git a/spec/lib/banzai/filter/markdown_filter_spec.rb b/spec/lib/banzai/filter/markdown_filter_spec.rb index cf49249756a..4c4e821deab 100644 --- a/spec/lib/banzai/filter/markdown_filter_spec.rb +++ b/spec/lib/banzai/filter/markdown_filter_spec.rb @@ -30,21 +30,21 @@ describe Banzai::Filter::MarkdownFilter do end it 'adds language to lang attribute when specified' do - result = filter("```html\nsome code\n```") + result = filter("```html\nsome code\n```", no_sourcepos: true) - expect(result).to start_with("<pre><code lang=\"html\">") + expect(result).to start_with('<pre><code lang="html">') end it 'does not add language to lang attribute when not specified' do - result = filter("```\nsome code\n```") + result = filter("```\nsome code\n```", no_sourcepos: true) - expect(result).to start_with("<pre><code>") + expect(result).to start_with('<pre><code>') end it 'works with utf8 chars in language' do - result = filter("```日\nsome code\n```") + result = filter("```日\nsome code\n```", no_sourcepos: true) - expect(result).to start_with("<pre><code lang=\"日\">") + expect(result).to start_with('<pre><code lang="日">') end end @@ -67,6 +67,38 @@ describe Banzai::Filter::MarkdownFilter do end end + describe 'source line position' do + context 'using CommonMark' do + before do + stub_const('Banzai::Filter::MarkdownFilter::DEFAULT_ENGINE', :common_mark) + end + + it 'defaults to add data-sourcepos' do + result = filter('test') + + expect(result).to eq '<p data-sourcepos="1:1-1:4">test</p>' + end + + it 'disables data-sourcepos' do + result = filter('test', no_sourcepos: true) + + expect(result).to eq '<p>test</p>' + end + end + + context 'using Redcarpet' do + before do + stub_const('Banzai::Filter::MarkdownFilter::DEFAULT_ENGINE', :redcarpet) + end + + it 'does not support data-sourcepos' do + result = filter('test') + + expect(result).to eq '<p>test</p>' + end + end + end + describe 'footnotes in tables' do it 'processes footnotes in table cells' do text = <<-MD.strip_heredoc @@ -77,7 +109,7 @@ describe Banzai::Filter::MarkdownFilter do [^1]: a footnote MD - result = filter(text) + result = filter(text, no_sourcepos: true) expect(result).to include('<td>foot <sup') expect(result).to include('<section class="footnotes">') diff --git a/spec/lib/banzai/filter/project_reference_filter_spec.rb b/spec/lib/banzai/filter/project_reference_filter_spec.rb index 48140305e26..69f9c1ae829 100644 --- a/spec/lib/banzai/filter/project_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/project_reference_filter_spec.rb @@ -13,7 +13,7 @@ describe Banzai::Filter::ProjectReferenceFilter do project.to_reference_with_postfix end - let(:project) { create(:project, :public) } + let(:project) { create(:project, :public) } subject { project } let(:subject_name) { "project" } let(:reference) { get_reference(project) } @@ -26,6 +26,12 @@ describe Banzai::Filter::ProjectReferenceFilter do expect(reference_filter(act).to_html).to eq(CGI.escapeHTML(exp)) end + it 'fails fast for long invalid string' do + expect do + Timeout.timeout(5.seconds) { reference_filter("A" * 50000).to_html } + end.not_to raise_error + end + it 'allows references with text after the > character' do doc = reference_filter("Hey #{reference}foo") expect(doc.css('a').first.attr('href')).to eq urls.project_url(subject) diff --git a/spec/lib/banzai/filter/relative_link_filter_spec.rb b/spec/lib/banzai/filter/relative_link_filter_spec.rb index 415ded05e6e..dad0a5535c0 100644 --- a/spec/lib/banzai/filter/relative_link_filter_spec.rb +++ b/spec/lib/banzai/filter/relative_link_filter_spec.rb @@ -353,5 +353,59 @@ describe Banzai::Filter::RelativeLinkFilter do expect(doc.at_css('a')['href']).to eq 'http://example.com' end end + + context 'to a personal snippet' do + let(:group) { nil } + let(:project) { nil } + let(:relative_path) { '/uploads/-/system/personal_snippet/6/674e4f07fbf0a7736c3439212896e51a/example.tar.gz' } + + context 'with an absolute URL' do + let(:absolute_path) { Gitlab.config.gitlab.url + relative_path } + let(:only_path) { false } + + it 'rewrites the link correctly' do + doc = filter(link(relative_path)) + + expect(doc.at_css('a')['href']).to eq(absolute_path) + end + end + + context 'with a relative URL root' do + let(:gitlab_root) { '/gitlab' } + let(:absolute_path) { Gitlab.config.gitlab.url + gitlab_root + relative_path } + + before do + stub_config_setting(relative_url_root: gitlab_root) + end + + context 'with an absolute URL' do + let(:only_path) { false } + + it 'rewrites the link correctly' do + doc = filter(link(relative_path)) + + expect(doc.at_css('a')['href']).to eq(absolute_path) + end + end + + it 'rewrites the link correctly' do + doc = filter(link(relative_path)) + + expect(doc.at_css('a')['href']).to eq(gitlab_root + relative_path) + end + end + + it 'rewrites the link correctly' do + doc = filter(link(relative_path)) + + expect(doc.at_css('a')['href']).to eq(relative_path) + end + + it 'does not modify absolute URL' do + doc = filter(link('http://example.com')) + + expect(doc.at_css('a')['href']).to eq 'http://example.com' + end + 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..f2a5d7b2c9f 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,55 @@ describe Banzai::Filter::SanitizationFilter do expect(act.to_html).to eq exp end + + it 'allows the `data-sourcepos` attribute globally' do + exp = %q{<p data-sourcepos="1:1-1:10">foo/bar.md</p>} + act = filter(exp) + + 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/description_pipeline_spec.rb b/spec/lib/banzai/pipeline/description_pipeline_spec.rb index 8cce1b96698..77cb1954ea3 100644 --- a/spec/lib/banzai/pipeline/description_pipeline_spec.rb +++ b/spec/lib/banzai/pipeline/description_pipeline_spec.rb @@ -13,6 +13,10 @@ describe Banzai::Pipeline::DescriptionPipeline do output end + before do + stub_commonmark_sourcepos_disabled + end + it 'uses a limited whitelist' do doc = parse('# Description') diff --git a/spec/lib/banzai/pipeline/email_pipeline_spec.rb b/spec/lib/banzai/pipeline/email_pipeline_spec.rb index 6a11ca2f9d5..b99161109eb 100644 --- a/spec/lib/banzai/pipeline/email_pipeline_spec.rb +++ b/spec/lib/banzai/pipeline/email_pipeline_spec.rb @@ -10,5 +10,19 @@ describe Banzai::Pipeline::EmailPipeline do expect(described_class.filters).not_to be_empty expect(described_class.filters).not_to include(Banzai::Filter::ImageLazyLoadFilter) end + + it 'shows punycode for autolinks' do + examples = %W[ + http://one😄two.com + http://\u0261itlab.com + ] + + examples.each do |markdown| + result = described_class.call(markdown, project: nil)[:output] + link = result.css('a').first + + expect(link.content).to include('http://xn--') + 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..3d3aa64d630 100644 --- a/spec/lib/banzai/pipeline/full_pipeline_spec.rb +++ b/spec/lib/banzai/pipeline/full_pipeline_spec.rb @@ -25,4 +25,76 @@ 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 + stub_commonmark_sourcepos_disabled + + expect(html.lines.map(&:strip).join("\n")).to eq filtered_footnote + end + end + + describe 'links are detected as malicious' do + it 'has tooltips for malicious links' do + examples = %W[ + http://example.com/evil\u202E3pm.exe + [evilexe.mp3](http://example.com/evil\u202E3pm.exe) + rdar://localhost.com/\u202E3pm.exe + http://one😄two.com + [Evil-Test](http://one😄two.com) + http://\u0261itlab.com + [Evil-GitLab-link](http://\u0261itlab.com) + ![Evil-GitLab-link](http://\u0261itlab.com.png) + ] + + examples.each do |markdown| + result = described_class.call(markdown, project: nil)[:output] + link = result.css('a').first + + expect(link[:class]).to include('has-tooltip') + end + end + + it 'has no tooltips for safe links' do + examples = %w[ + http://example.com + [Safe-Test](http://example.com) + https://commons.wikimedia.org/wiki/File:اسكرام_2_-_تمنراست.jpg + [Wikipedia-link](https://commons.wikimedia.org/wiki/File:اسكرام_2_-_تمنراست.jpg) + ] + + examples.each do |markdown| + result = described_class.call(markdown, project: nil)[:output] + link = result.css('a').first + + expect(link[:class]).to be_nil + 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/auth/o_auth/identity_linker_spec.rb b/spec/lib/gitlab/auth/o_auth/identity_linker_spec.rb index 528f1b4ec57..bf810d72f0e 100644 --- a/spec/lib/gitlab/auth/o_auth/identity_linker_spec.rb +++ b/spec/lib/gitlab/auth/o_auth/identity_linker_spec.rb @@ -23,7 +23,7 @@ describe Gitlab::Auth::OAuth::IdentityLinker do end context 'identity already linked to different user' do - let!(:identity) { create(:identity, provider: provider, extern_uid: uid) } + let!(:identity) { create(:identity, provider: provider, extern_uid: uid) } it "#changed? returns false" do subject.link diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb index 9ccd0b206cc..236808c0b69 100644 --- a/spec/lib/gitlab/auth_spec.rb +++ b/spec/lib/gitlab/auth_spec.rb @@ -380,7 +380,7 @@ describe Gitlab::Auth do password: password, password_confirmation: password) end - let(:username) { 'John' } # username isn't lowercase, test this + let(:username) { 'John' } # username isn't lowercase, test this let(:password) { 'my-secret' } it "finds user by valid login/password" do 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_import_state_spec.rb b/spec/lib/gitlab/background_migration/populate_import_state_spec.rb index f9952ee5163..fcb869022de 100644 --- a/spec/lib/gitlab/background_migration/populate_import_state_spec.rb +++ b/spec/lib/gitlab/background_migration/populate_import_state_spec.rb @@ -4,7 +4,7 @@ describe Gitlab::BackgroundMigration::PopulateImportState, :migration, schema: 2 let(:migration) { described_class.new } let(:namespaces) { table(:namespaces) } let(:projects) { table(:projects) } - let(:import_state) { table(:project_mirror_data) } + let(:import_state) { table(:project_mirror_data) } before do namespaces.create(id: 1, name: 'gitlab-org', path: 'gitlab-org') 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/background_migration/rollback_import_state_data_spec.rb b/spec/lib/gitlab/background_migration/rollback_import_state_data_spec.rb index 9f8c3bc220f..cef3b6e4568 100644 --- a/spec/lib/gitlab/background_migration/rollback_import_state_data_spec.rb +++ b/spec/lib/gitlab/background_migration/rollback_import_state_data_spec.rb @@ -4,7 +4,7 @@ describe Gitlab::BackgroundMigration::RollbackImportStateData, :migration, schem let(:migration) { described_class.new } let(:namespaces) { table(:namespaces) } let(:projects) { table(:projects) } - let(:import_state) { table(:project_mirror_data) } + let(:import_state) { table(:project_mirror_data) } before do namespaces.create(id: 1, name: 'gitlab-org', path: 'gitlab-org') diff --git a/spec/lib/gitlab/bitbucket_import/importer_spec.rb b/spec/lib/gitlab/bitbucket_import/importer_spec.rb index 7a681bc6610..0def685f177 100644 --- a/spec/lib/gitlab/bitbucket_import/importer_spec.rb +++ b/spec/lib/gitlab/bitbucket_import/importer_spec.rb @@ -15,7 +15,7 @@ describe Gitlab::BitbucketImport::Importer do "invalid", "duplicate", "wontfix", - "closed" # undocumented status + "closed" # undocumented status ] end diff --git a/spec/lib/gitlab/bitbucket_server_import/importer_spec.rb b/spec/lib/gitlab/bitbucket_server_import/importer_spec.rb index 70423823b89..1e90a2ef27f 100644 --- a/spec/lib/gitlab/bitbucket_server_import/importer_spec.rb +++ b/spec/lib/gitlab/bitbucket_server_import/importer_spec.rb @@ -21,12 +21,9 @@ describe Gitlab::BitbucketServerImport::Importer do end describe '#import_repository' do - before do + it 'adds a remote' do expect(subject).to receive(:import_pull_requests) expect(subject).to receive(:delete_temp_branches) - end - - it 'adds a remote' do expect(project.repository).to receive(:fetch_as_mirror) .with('http://bitbucket:test@my-bitbucket', refmap: [:heads, :tags, '+refs/pull-requests/*/to:refs/merge-requests/*/head'], @@ -34,6 +31,18 @@ describe Gitlab::BitbucketServerImport::Importer do subject.execute end + + it 'raises a Gitlab::Shell exception in the fetch' do + expect(project.repository).to receive(:fetch_as_mirror).and_raise(Gitlab::Shell::Error) + + expect { subject.execute }.to raise_error(Gitlab::Shell::Error) + end + + it 'raises an unhandled exception in the fetch' do + expect(project.repository).to receive(:fetch_as_mirror).and_raise(RuntimeError) + + expect { subject.execute }.to raise_error(RuntimeError) + end end describe '#import_pull_requests' do diff --git a/spec/lib/gitlab/cache/ci/project_pipeline_status_spec.rb b/spec/lib/gitlab/cache/ci/project_pipeline_status_spec.rb index be9b2588c90..483c5ea9cff 100644 --- a/spec/lib/gitlab/cache/ci/project_pipeline_status_spec.rb +++ b/spec/lib/gitlab/cache/ci/project_pipeline_status_spec.rb @@ -238,7 +238,7 @@ describe Gitlab::Cache::Ci::ProjectPipelineStatus, :clean_gitlab_redis_cache do end describe '#delete_from_cache' do - it 'deletes values from redis_cache' do + it 'deletes values from redis_cache' do pipeline_status.delete_from_cache key_exists = Gitlab::Redis::Cache.with { |redis| redis.exists(cache_key) } diff --git a/spec/lib/gitlab/ci/config_spec.rb b/spec/lib/gitlab/ci/config_spec.rb index cd6d2a2f343..18f255c1ab7 100644 --- a/spec/lib/gitlab/ci/config_spec.rb +++ b/spec/lib/gitlab/ci/config_spec.rb @@ -192,7 +192,7 @@ describe Gitlab::Ci::Config do end end - context "when gitlab_ci.yml has invalid 'include' defined" do + context "when gitlab_ci.yml has invalid 'include' defined" do let(:gitlab_ci_yml) do <<~HEREDOC include: invalid @@ -207,7 +207,7 @@ describe Gitlab::Ci::Config do end end - context "when gitlab_ci.yml has ambigious 'include' defined" do + context "when gitlab_ci.yml has ambigious 'include' defined" do let(:gitlab_ci_yml) do <<~HEREDOC include: diff --git a/spec/lib/gitlab/ci/pipeline/chain/populate_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/populate_spec.rb index 1b014ecfaa4..0302e4090cf 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) @@ -138,14 +163,14 @@ describe Gitlab::Ci::Pipeline::Chain::Populate do ->(pipeline) { pipeline.variables.create!(key: 'VAR', value: '123') } end - it 'raises exception' do + it 'wastes pipeline iid' do expect { step.perform! }.to raise_error(ActiveRecord::RecordNotSaved) - end - it 'wastes pipeline iid' do - expect { step.perform! }.to raise_error + last_iid = InternalId.ci_pipelines + .where(project_id: project.id) + .last.last_value - expect(InternalId.ci_pipelines.where(project_id: project.id).last.last_value).to be > 0 + expect(last_iid).to be > 0 end end end diff --git a/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb b/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb index a700cfd4546..fae8add6453 100644 --- a/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb @@ -5,8 +5,7 @@ describe Gitlab::Ci::Pipeline::Seed::Build do let(:pipeline) { create(:ci_empty_pipeline, project: project) } let(:attributes) do - { name: 'rspec', - ref: 'master' } + { name: 'rspec', ref: 'master' } end subject do @@ -21,10 +20,45 @@ describe Gitlab::Ci::Pipeline::Seed::Build do end end + describe '#bridge?' do + context 'when job is a bridge' do + let(:attributes) do + { name: 'rspec', ref: 'master', options: { trigger: 'my/project' } } + end + + it { is_expected.to be_bridge } + end + + context 'when trigger definition is empty' do + let(:attributes) do + { name: 'rspec', ref: 'master', options: { trigger: '' } } + end + + it { is_expected.not_to be_bridge } + end + + context 'when job is not a bridge' do + it { is_expected.not_to be_bridge } + end + end + describe '#to_resource' do - it 'returns a valid build resource' do - expect(subject.to_resource).to be_a(::Ci::Build) - expect(subject.to_resource).to be_valid + context 'when job is not a bridge' do + it 'returns a valid build resource' do + expect(subject.to_resource).to be_a(::Ci::Build) + expect(subject.to_resource).to be_valid + end + end + + context 'when job is a bridge' do + let(:attributes) do + { name: 'rspec', ref: 'master', options: { trigger: 'my/project' } } + end + + it 'returns a valid bridge resource' do + expect(subject.to_resource).to be_a(::Ci::Bridge) + expect(subject.to_resource).to be_valid + end end it 'memoizes a resource object' do diff --git a/spec/lib/gitlab/ci/pipeline/seed/stage_spec.rb b/spec/lib/gitlab/ci/pipeline/seed/stage_spec.rb index 82f741845db..493ca3cd7b5 100644 --- a/spec/lib/gitlab/ci/pipeline/seed/stage_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/seed/stage_spec.rb @@ -62,8 +62,18 @@ describe Gitlab::Ci::Pipeline::Seed::Stage do expect(subject.seeds.map(&:attributes)).to all(include(ref: 'master')) expect(subject.seeds.map(&:attributes)).to all(include(tag: false)) expect(subject.seeds.map(&:attributes)).to all(include(project: pipeline.project)) - expect(subject.seeds.map(&:attributes)) - .to all(include(trigger_request: pipeline.trigger_requests.first)) + end + + context 'when a legacy trigger exists' do + before do + create(:ci_trigger_request, pipeline: pipeline) + end + + it 'returns build seeds including legacy trigger' do + expect(pipeline.legacy_trigger).not_to be_nil + expect(subject.seeds.map(&:attributes)) + .to all(include(trigger_request: pipeline.legacy_trigger)) + end end context 'when a ref is protected' do diff --git a/spec/lib/gitlab/ci/status/external/common_spec.rb b/spec/lib/gitlab/ci/status/external/common_spec.rb index 40871f86568..0d02c371a92 100644 --- a/spec/lib/gitlab/ci/status/external/common_spec.rb +++ b/spec/lib/gitlab/ci/status/external/common_spec.rb @@ -11,7 +11,7 @@ describe Gitlab::Ci::Status::External::Common do end subject do - Gitlab::Ci::Status::Core + Gitlab::Ci::Status::Success .new(external_status, user) .extend(described_class) end @@ -20,6 +20,22 @@ describe Gitlab::Ci::Status::External::Common do it 'returns description' do expect(subject.label).to eq external_description end + + context 'when description is nil' do + let(:external_description) { nil } + + it 'uses core status label' do + expect(subject.label).to eq('passed') + end + end + + context 'when description is empty string' do + let(:external_description) { '' } + + it 'uses core status label' do + expect(subject.label).to eq('passed') + end + end end describe '#has_action?' do diff --git a/spec/lib/gitlab/ci/yaml_processor_spec.rb b/spec/lib/gitlab/ci/yaml_processor_spec.rb index b6c3431728c..91139d421f5 100644 --- a/spec/lib/gitlab/ci/yaml_processor_spec.rb +++ b/spec/lib/gitlab/ci/yaml_processor_spec.rb @@ -21,15 +21,12 @@ module Gitlab stage: "test", stage_idx: 1, name: "rspec", - coverage_regex: nil, - tag_list: [], options: { before_script: ["pwd"], script: ["rspec"] }, allow_failure: false, when: "on_success", - environment: nil, yaml_variables: [] }) end @@ -154,12 +151,9 @@ module Gitlab builds: [{ stage_idx: 1, stage: "test", - tag_list: [], name: "rspec", allow_failure: false, when: "on_success", - environment: nil, - coverage_regex: nil, yaml_variables: [], options: { script: ["rspec"] }, only: { refs: ["branches"] }, @@ -169,12 +163,9 @@ module Gitlab builds: [{ stage_idx: 2, stage: "deploy", - tag_list: [], name: "prod", allow_failure: false, when: "on_success", - environment: nil, - coverage_regex: nil, yaml_variables: [], options: { script: ["cap prod"] }, only: { refs: ["tags"] }, @@ -344,8 +335,6 @@ module Gitlab stage: "test", stage_idx: 1, name: "rspec", - coverage_regex: nil, - tag_list: [], options: { before_script: ["pwd"], script: ["rspec"], @@ -356,7 +345,6 @@ module Gitlab }, allow_failure: false, when: "on_success", - environment: nil, yaml_variables: [] }) end @@ -378,8 +366,6 @@ module Gitlab stage: "test", stage_idx: 1, name: "rspec", - coverage_regex: nil, - tag_list: [], options: { before_script: ["pwd"], script: ["rspec"], @@ -390,7 +376,6 @@ module Gitlab }, allow_failure: false, when: "on_success", - environment: nil, yaml_variables: [] }) end @@ -410,8 +395,6 @@ module Gitlab stage: "test", stage_idx: 1, name: "rspec", - coverage_regex: nil, - tag_list: [], options: { before_script: ["pwd"], script: ["rspec"], @@ -420,7 +403,6 @@ module Gitlab }, allow_failure: false, when: "on_success", - environment: nil, yaml_variables: [] }) end @@ -438,8 +420,6 @@ module Gitlab stage: "test", stage_idx: 1, name: "rspec", - coverage_regex: nil, - tag_list: [], options: { before_script: ["pwd"], script: ["rspec"], @@ -448,7 +428,6 @@ module Gitlab }, allow_failure: false, when: "on_success", - environment: nil, yaml_variables: [] }) end @@ -763,8 +742,6 @@ module Gitlab stage: "test", stage_idx: 1, name: "rspec", - coverage_regex: nil, - tag_list: [], options: { before_script: ["pwd"], script: ["rspec"], @@ -779,7 +756,6 @@ module Gitlab }, when: "on_success", allow_failure: false, - environment: nil, yaml_variables: [] }) end @@ -888,7 +864,7 @@ module Gitlab end context 'without matching job' do - let(:close_review) { nil } + let(:close_review) { nil } it 'raises error' do expect { builds }.to raise_error('review job: on_stop job close_review is not defined') @@ -976,14 +952,11 @@ module Gitlab stage: "test", stage_idx: 1, name: "normal_job", - coverage_regex: nil, - tag_list: [], options: { script: ["test"] }, when: "on_success", allow_failure: false, - environment: nil, yaml_variables: [] }) end @@ -1023,28 +996,22 @@ module Gitlab stage: "build", stage_idx: 0, name: "job1", - coverage_regex: nil, - tag_list: [], options: { script: ["execute-script-for-job"] }, when: "on_success", allow_failure: false, - environment: nil, yaml_variables: [] }) expect(subject.second).to eq({ stage: "build", stage_idx: 0, name: "job2", - coverage_regex: nil, - tag_list: [], options: { script: ["execute-script-for-job"] }, when: "on_success", allow_failure: false, - environment: nil, yaml_variables: [] }) end diff --git a/spec/lib/gitlab/data_builder/pipeline_spec.rb b/spec/lib/gitlab/data_builder/pipeline_spec.rb index 98f1696badb..9ef987a0826 100644 --- a/spec/lib/gitlab/data_builder/pipeline_spec.rb +++ b/spec/lib/gitlab/data_builder/pipeline_spec.rb @@ -37,7 +37,7 @@ describe Gitlab::DataBuilder::Pipeline do context 'pipeline without variables' do it 'has empty variables hash' do expect(attributes[:variables]).to be_a(Array) - expect(attributes[:variables]).to be_empty() + expect(attributes[:variables]).to be_empty end end diff --git a/spec/lib/gitlab/data_builder/push_spec.rb b/spec/lib/gitlab/data_builder/push_spec.rb index befdc18d1aa..0c4decc6518 100644 --- a/spec/lib/gitlab/data_builder/push_spec.rb +++ b/spec/lib/gitlab/data_builder/push_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' describe Gitlab::DataBuilder::Push do let(:project) { create(:project, :repository) } - let(:user) { create(:user) } + let(:user) { build(:user, public_email: 'public-email@example.com') } describe '.build_sample' do let(:data) { described_class.build_sample(project, user) } @@ -36,7 +36,7 @@ describe Gitlab::DataBuilder::Push do it { expect(data[:user_id]).to eq(user.id) } it { expect(data[:user_name]).to eq(user.name) } it { expect(data[:user_username]).to eq(user.username) } - it { expect(data[:user_email]).to eq(user.email) } + it { expect(data[:user_email]).to eq(user.public_email) } it { expect(data[:user_avatar]).to eq(user.avatar_url) } it { expect(data[:project_id]).to eq(project.id) } it { expect(data[:project]).to be_a(Hash) } diff --git a/spec/lib/gitlab/diff/position_tracer_spec.rb b/spec/lib/gitlab/diff/position_tracer_spec.rb index a2eed07ca55..866550753a8 100644 --- a/spec/lib/gitlab/diff/position_tracer_spec.rb +++ b/spec/lib/gitlab/diff/position_tracer_spec.rb @@ -1515,8 +1515,8 @@ describe Gitlab::Diff::PositionTracer do { new_path: file_name, new_line: 4, change: true }, { new_path: file_name, old_line: 3, change: true }, { old_path: file_name, new_path: file_name, old_line: 5, new_line: 5 }, - { new_path: file_name, old_line: 5, change: true }, - { new_path: file_name, new_line: 7 } + { new_path: file_name, old_line: 5, change: true }, + { new_path: file_name, new_line: 7 } ] expect_new_positions(old_position_attrs, new_position_attrs) @@ -1588,7 +1588,7 @@ describe Gitlab::Diff::PositionTracer do { new_path: file_name, new_line: 4, change: true }, { old_path: file_name, old_line: 3, change: true }, { old_path: file_name, new_path: file_name, old_line: 5, new_line: 5 }, - { old_path: file_name, old_line: 5, change: true }, + { old_path: file_name, old_line: 5, change: true }, { new_path: file_name, new_line: 7 } ] @@ -1638,13 +1638,13 @@ describe Gitlab::Diff::PositionTracer do new_position_attrs = [ { old_path: file_name, new_path: file_name, old_line: 1, new_line: 1 }, { old_path: file_name, old_line: 2 }, - { old_path: file_name, old_line: 2, change: true }, + { old_path: file_name, old_line: 2, change: true }, { old_path: file_name, new_path: file_name, old_line: 3, new_line: 2 }, - { old_path: file_name, old_line: 4, change: true }, + { old_path: file_name, old_line: 4, change: true }, { old_path: file_name, new_path: file_name, old_line: 5, new_line: 4 }, - { new_path: file_name, new_line: 5, change: true }, - { old_path: file_name, old_line: 6, change: true }, - { new_path: file_name, new_line: 6 } + { new_path: file_name, new_line: 5, change: true }, + { old_path: file_name, old_line: 6, change: true }, + { new_path: file_name, new_line: 6 } ] expect_new_positions(old_position_attrs, new_position_attrs) @@ -1880,7 +1880,7 @@ describe Gitlab::Diff::PositionTracer do new_position_attrs = [ { old_path: file_name, new_path: file_name, old_line: 1, new_line: 1 }, - { old_path: file_name, old_line: 2, change: true }, + { old_path: file_name, old_line: 2, change: true }, { new_path: file_name, new_line: 2 }, { old_path: file_name, new_path: file_name, old_line: 2, new_line: 3 }, { new_path: file_name, new_line: 4 }, diff --git a/spec/lib/gitlab/email/handler/create_note_handler_spec.rb b/spec/lib/gitlab/email/handler/create_note_handler_spec.rb index b1f48c15c21..e5420ea6bea 100644 --- a/spec/lib/gitlab/email/handler/create_note_handler_spec.rb +++ b/spec/lib/gitlab/email/handler/create_note_handler_spec.rb @@ -118,6 +118,43 @@ describe Gitlab::Email::Handler::CreateNoteHandler do end end + shared_examples "checks permissions on noteable" do + context "when user has access" do + before do + project.add_reporter(user) + end + + it "creates a comment" do + expect { receiver.execute }.to change { noteable.notes.count }.by(1) + end + end + + context "when user does not have access" do + it "raises UserNotAuthorizedError" do + expect { receiver.execute }.to raise_error(Gitlab::Email::UserNotAuthorizedError) + end + end + end + + context "when discussion is locked" do + before do + noteable.update_attribute(:discussion_locked, true) + end + + it_behaves_like "checks permissions on noteable" + end + + context "when issue is confidential" do + let(:issue) { create(:issue, project: project) } + let(:note) { create(:note, noteable: issue, project: project) } + + before do + issue.update_attribute(:confidential, true) + end + + it_behaves_like "checks permissions on noteable" + end + context "when everything is fine" do before do setup_attachment 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/compare_spec.rb b/spec/lib/gitlab/git/compare_spec.rb index 7cc6f52f8ee..771c71a16a9 100644 --- a/spec/lib/gitlab/git/compare_spec.rb +++ b/spec/lib/gitlab/git/compare_spec.rb @@ -67,7 +67,7 @@ describe Gitlab::Git::Compare, :seed_helper do end end - describe '#same' do + describe '#same' do subject do compare.same end diff --git a/spec/lib/gitlab/git/merge_base_spec.rb b/spec/lib/gitlab/git/merge_base_spec.rb index 8d16d451730..dbb4e3d0b3e 100644 --- a/spec/lib/gitlab/git/merge_base_spec.rb +++ b/spec/lib/gitlab/git/merge_base_spec.rb @@ -62,7 +62,7 @@ describe Gitlab::Git::MergeBase do end describe '#commit' do - context 'for existing refs with a merge base', :existing_refs do + context 'for existing refs with a merge base', :existing_refs do it 'finds the commit for the merge base' do expect(merge_base.commit).to be_a(Commit) 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/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb index 3e34dd592f2..634c370d211 100644 --- a/spec/lib/gitlab/git_access_spec.rb +++ b/spec/lib/gitlab/git_access_spec.rb @@ -776,10 +776,13 @@ describe Gitlab::GitAccess do it "has the correct permissions for #{role}s" do if role == :admin user.update_attribute(:admin, true) + project.add_guest(user) else project.add_role(user, role) end + protected_branch.save + aggregate_failures do matrix.each do |action, allowed| check = -> { push_changes(changes[action]) } @@ -861,25 +864,19 @@ describe Gitlab::GitAccess do [%w(feature exact), ['feat*', 'wildcard']].each do |protected_branch_name, protected_branch_type| context do - before do - create(:protected_branch, name: protected_branch_name, project: project) - end + let(:protected_branch) { create(:protected_branch, :maintainers_can_push, name: protected_branch_name, project: project) } run_permission_checks(permissions_matrix) end context "when developers are allowed to push into the #{protected_branch_type} protected branch" do - before do - create(:protected_branch, :developers_can_push, name: protected_branch_name, project: project) - end + let(:protected_branch) { create(:protected_branch, :developers_can_push, name: protected_branch_name, project: project) } run_permission_checks(permissions_matrix.deep_merge(developer: { push_protected_branch: true, push_all: true, merge_into_protected_branch: true })) end context "developers are allowed to merge into the #{protected_branch_type} protected branch" do - before do - create(:protected_branch, :developers_can_merge, name: protected_branch_name, project: project) - end + let(:protected_branch) { create(:protected_branch, :developers_can_merge, name: protected_branch_name, project: project) } context "when a merge request exists for the given source/target branch" do context "when the merge request is in progress" do @@ -906,17 +903,13 @@ describe Gitlab::GitAccess do end context "when developers are allowed to push and merge into the #{protected_branch_type} protected branch" do - before do - create(:protected_branch, :developers_can_merge, :developers_can_push, name: protected_branch_name, project: project) - end + let(:protected_branch) { create(:protected_branch, :developers_can_merge, :developers_can_push, name: protected_branch_name, project: project) } run_permission_checks(permissions_matrix.deep_merge(developer: { push_protected_branch: true, push_all: true, merge_into_protected_branch: true })) end context "when no one is allowed to push to the #{protected_branch_name} protected branch" do - before do - create(:protected_branch, :no_one_can_push, name: protected_branch_name, project: project) - end + let(:protected_branch) { build(:protected_branch, :no_one_can_push, name: protected_branch_name, project: project) } run_permission_checks(permissions_matrix.deep_merge(developer: { push_protected_branch: false, push_all: false, merge_into_protected_branch: false }, maintainer: { push_protected_branch: false, push_all: false, merge_into_protected_branch: false }, diff --git a/spec/lib/gitlab/gitaly_client_spec.rb b/spec/lib/gitlab/gitaly_client_spec.rb index e41a75c37a7..cf12baf1a93 100644 --- a/spec/lib/gitlab/gitaly_client_spec.rb +++ b/spec/lib/gitlab/gitaly_client_spec.rb @@ -119,6 +119,15 @@ describe Gitlab::GitalyClient do end end + describe '.connection_data' do + it 'returns connection data' do + address = 'tcp://localhost:9876' + stub_repos_storages address + + expect(described_class.connection_data('default')).to eq({ 'address' => address, 'token' => 'secret' }) + end + end + describe 'allow_n_plus_1_calls' do context 'when RequestStore is enabled', :request_store do it 'returns the result of the allow_n_plus_1_calls block' do diff --git a/spec/lib/gitlab/github_import/bulk_importing_spec.rb b/spec/lib/gitlab/github_import/bulk_importing_spec.rb index 861710f7e9b..91229d9c7d4 100644 --- a/spec/lib/gitlab/github_import/bulk_importing_spec.rb +++ b/spec/lib/gitlab/github_import/bulk_importing_spec.rb @@ -58,17 +58,5 @@ describe Gitlab::GithubImport::BulkImporting do importer.bulk_insert(model, rows, batch_size: 5) end - - it 'calls pre_hook for each slice if given' do - rows = [{ title: 'Foo' }] * 10 - model = double(:model, table_name: 'kittens') - pre_hook = double('pre_hook', call: nil) - allow(Gitlab::Database).to receive(:bulk_insert) - - expect(pre_hook).to receive(:call).with(rows[0..4]) - expect(pre_hook).to receive(:call).with(rows[5..9]) - - importer.bulk_insert(model, rows, batch_size: 5, pre_hook: pre_hook) - end end end diff --git a/spec/lib/gitlab/github_import/importer/issue_importer_spec.rb b/spec/lib/gitlab/github_import/importer/issue_importer_spec.rb index 65a2e1cb5cb..7901ae005d9 100644 --- a/spec/lib/gitlab/github_import/importer/issue_importer_spec.rb +++ b/spec/lib/gitlab/github_import/importer/issue_importer_spec.rb @@ -78,11 +78,6 @@ describe Gitlab::GithubImport::Importer::IssueImporter, :clean_gitlab_redis_cach .to receive(:id_for) .with(issue) .and_return(milestone.id) - - allow(importer.user_finder) - .to receive(:author_id_for) - .with(issue) - .and_return([user.id, true]) end context 'when the issue author could be found' do @@ -177,23 +172,6 @@ describe Gitlab::GithubImport::Importer::IssueImporter, :clean_gitlab_redis_cach expect(importer.create_issue).to be_a_kind_of(Numeric) end - - it 'triggers internal_id functionality to track greatest iids' do - allow(importer.user_finder) - .to receive(:author_id_for) - .with(issue) - .and_return([user.id, true]) - - issue = build_stubbed(:issue, project: project) - allow(importer) - .to receive(:insert_and_return_id) - .and_return(issue.id) - allow(project.issues).to receive(:find).with(issue.id).and_return(issue) - - expect(issue).to receive(:ensure_project_iid!) - - importer.create_issue - end end describe '#create_assignees' do diff --git a/spec/lib/gitlab/github_import/importer/lfs_object_importer_spec.rb b/spec/lib/gitlab/github_import/importer/lfs_object_importer_spec.rb index 4857f2afbe2..8fd328d9c1e 100644 --- a/spec/lib/gitlab/github_import/importer/lfs_object_importer_spec.rb +++ b/spec/lib/gitlab/github_import/importer/lfs_object_importer_spec.rb @@ -2,20 +2,26 @@ require 'spec_helper' describe Gitlab::GithubImport::Importer::LfsObjectImporter do let(:project) { create(:project) } - let(:download_link) { "http://www.gitlab.com/lfs_objects/oid" } - - let(:github_lfs_object) do - Gitlab::GithubImport::Representation::LfsObject.new( - oid: 'oid', download_link: download_link - ) + let(:lfs_attributes) do + { + oid: 'oid', + size: 1, + link: 'http://www.gitlab.com/lfs_objects/oid' + } end + let(:lfs_download_object) { LfsDownloadObject.new(lfs_attributes) } + let(:github_lfs_object) { Gitlab::GithubImport::Representation::LfsObject.new(lfs_attributes) } + let(:importer) { described_class.new(github_lfs_object, project, nil) } describe '#execute' do it 'calls the LfsDownloadService with the lfs object attributes' do - expect_any_instance_of(Projects::LfsPointers::LfsDownloadService) - .to receive(:execute).with('oid', download_link) + allow(importer).to receive(:lfs_download_object).and_return(lfs_download_object) + + service = double + expect(Projects::LfsPointers::LfsDownloadService).to receive(:new).with(project, lfs_download_object).and_return(service) + expect(service).to receive(:execute) importer.execute end diff --git a/spec/lib/gitlab/github_import/importer/lfs_objects_importer_spec.rb b/spec/lib/gitlab/github_import/importer/lfs_objects_importer_spec.rb index 5f5c6b803c0..50442552eee 100644 --- a/spec/lib/gitlab/github_import/importer/lfs_objects_importer_spec.rb +++ b/spec/lib/gitlab/github_import/importer/lfs_objects_importer_spec.rb @@ -5,7 +5,15 @@ describe Gitlab::GithubImport::Importer::LfsObjectsImporter do let(:client) { double(:client) } let(:download_link) { "http://www.gitlab.com/lfs_objects/oid" } - let(:github_lfs_object) { ['oid', download_link] } + let(:lfs_attributes) do + { + oid: 'oid', + size: 1, + link: 'http://www.gitlab.com/lfs_objects/oid' + } + end + + let(:lfs_download_object) { LfsDownloadObject.new(lfs_attributes) } describe '#parallel?' do it 'returns true when running in parallel mode' do @@ -48,7 +56,7 @@ describe Gitlab::GithubImport::Importer::LfsObjectsImporter do allow(importer) .to receive(:each_object_to_import) - .and_yield(['oid', download_link]) + .and_yield(lfs_download_object) expect(Gitlab::GithubImport::Importer::LfsObjectImporter) .to receive(:new) @@ -71,7 +79,7 @@ describe Gitlab::GithubImport::Importer::LfsObjectsImporter do allow(importer) .to receive(:each_object_to_import) - .and_yield(github_lfs_object) + .and_yield(lfs_download_object) expect(Gitlab::GithubImport::ImportLfsObjectWorker) .to receive(:perform_async) diff --git a/spec/lib/gitlab/github_import/importer/milestones_importer_spec.rb b/spec/lib/gitlab/github_import/importer/milestones_importer_spec.rb index db0be760c7b..b1cac3b6e46 100644 --- a/spec/lib/gitlab/github_import/importer/milestones_importer_spec.rb +++ b/spec/lib/gitlab/github_import/importer/milestones_importer_spec.rb @@ -29,25 +29,13 @@ describe Gitlab::GithubImport::Importer::MilestonesImporter, :clean_gitlab_redis expect(importer) .to receive(:bulk_insert) - .with(Milestone, [milestone_hash], any_args) + .with(Milestone, [milestone_hash]) expect(importer) .to receive(:build_milestones_cache) importer.execute end - - it 'tracks internal ids' do - milestone_hash = { iid: 1, title: '1.0', project_id: project.id } - allow(importer) - .to receive(:build_milestones) - .and_return([milestone_hash]) - - expect(InternalId).to receive(:track_greatest) - .with(nil, { project: project }, :milestones, 1, any_args) - - importer.execute - end end describe '#build_milestones' do diff --git a/spec/lib/gitlab/github_import/importer/pull_request_importer_spec.rb b/spec/lib/gitlab/github_import/importer/pull_request_importer_spec.rb index 25684ea9e2c..0f21b8843b6 100644 --- a/spec/lib/gitlab/github_import/importer/pull_request_importer_spec.rb +++ b/spec/lib/gitlab/github_import/importer/pull_request_importer_spec.rb @@ -111,16 +111,6 @@ describe Gitlab::GithubImport::Importer::PullRequestImporter, :clean_gitlab_redi expect(mr).to be_instance_of(MergeRequest) expect(exists).to eq(false) end - - it 'triggers internal_id functionality to track greatest iids' do - mr = build_stubbed(:merge_request, source_project: project, target_project: project) - allow(importer).to receive(:insert_and_return_id).and_return(mr.id) - allow(project.merge_requests).to receive(:find).with(mr.id).and_return(mr) - - expect(mr).to receive(:ensure_target_project_iid!) - - importer.create_merge_request - end end context 'when the author could not be found' do diff --git a/spec/lib/gitlab/graphql/connections/keyset_connection_spec.rb b/spec/lib/gitlab/graphql/connections/keyset_connection_spec.rb index 96615ae80de..9bcc1e78a78 100644 --- a/spec/lib/gitlab/graphql/connections/keyset_connection_spec.rb +++ b/spec/lib/gitlab/graphql/connections/keyset_connection_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' describe Gitlab::Graphql::Connections::KeysetConnection do let(:nodes) { Project.all.order(id: :asc) } - let(:arguments) { {} } + let(:arguments) { {} } subject(:connection) do described_class.new(nodes, arguments, max_page_size: 3) end diff --git a/spec/lib/gitlab/hashed_storage/migrator_spec.rb b/spec/lib/gitlab/hashed_storage/migrator_spec.rb index 01d43ed00a2..3942f168ceb 100644 --- a/spec/lib/gitlab/hashed_storage/migrator_spec.rb +++ b/spec/lib/gitlab/hashed_storage/migrator_spec.rb @@ -4,7 +4,7 @@ describe Gitlab::HashedStorage::Migrator do describe '#bulk_schedule' do it 'schedules job to StorageMigratorWorker' do Sidekiq::Testing.fake! do - expect { subject.bulk_schedule(1, 5) }.to change(StorageMigratorWorker.jobs, :size).by(1) + expect { subject.bulk_schedule(start: 1, finish: 5) }.to change(HashedStorage::MigratorWorker.jobs, :size).by(1) end end end @@ -15,13 +15,13 @@ describe Gitlab::HashedStorage::Migrator do it 'enqueue jobs to ProjectMigrateHashedStorageWorker' do Sidekiq::Testing.fake! do - expect { subject.bulk_migrate(ids.min, ids.max) }.to change(ProjectMigrateHashedStorageWorker.jobs, :size).by(2) + expect { subject.bulk_migrate(start: ids.min, finish: ids.max) }.to change(ProjectMigrateHashedStorageWorker.jobs, :size).by(2) end end it 'rescues and log exceptions' do allow_any_instance_of(Project).to receive(:migrate_to_hashed_storage!).and_raise(StandardError) - expect { subject.bulk_migrate(ids.min, ids.max) }.not_to raise_error + expect { subject.bulk_migrate(start: ids.min, finish: ids.max) }.not_to raise_error end it 'delegates each project in specified range to #migrate' do @@ -29,12 +29,12 @@ describe Gitlab::HashedStorage::Migrator do expect(subject).to receive(:migrate).with(project) end - subject.bulk_migrate(ids.min, ids.max) + subject.bulk_migrate(start: ids.min, finish: ids.max) end it 'has migrated projects set as writable' do perform_enqueued_jobs do - subject.bulk_migrate(ids.min, ids.max) + subject.bulk_migrate(start: ids.min, finish: ids.max) end projects.each do |project| @@ -46,7 +46,7 @@ describe Gitlab::HashedStorage::Migrator do describe '#migrate' do let(:project) { create(:project, :legacy_storage, :empty_repo) } - it 'enqueues job to ProjectMigrateHashedStorageWorker' do + it 'enqueues project migration job' do Sidekiq::Testing.fake! do expect { subject.migrate(project) }.to change(ProjectMigrateHashedStorageWorker.jobs, :size).by(1) end @@ -58,7 +58,7 @@ describe Gitlab::HashedStorage::Migrator do expect { subject.migrate(project) }.not_to raise_error end - it 'migrate project' do + it 'migrates project storage' do perform_enqueued_jobs do subject.migrate(project) end @@ -73,5 +73,19 @@ describe Gitlab::HashedStorage::Migrator do expect(project.reload.repository_read_only?).to be_falsey end + + context 'when project is already on hashed storage' do + let(:project) { create(:project, :empty_repo) } + + it 'doesnt enqueue any migration job' do + Sidekiq::Testing.fake! do + expect { subject.migrate(project) }.not_to change(ProjectMigrateHashedStorageWorker.jobs, :size) + end + end + + it 'returns false' do + expect(subject.migrate(project)).to be_falsey + end + end end end diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index 5afa9669b1a..6897ac8a3a8 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -114,6 +114,7 @@ ci_pipelines: - stages - statuses - builds +- processables - trigger_requests - variables - auto_canceled_by @@ -137,6 +138,7 @@ stages: - pipeline - statuses - builds +- bridges statuses: - project - pipeline diff --git a/spec/lib/gitlab/import_export/importer_spec.rb b/spec/lib/gitlab/import_export/importer_spec.rb index 11f98d782b1..898e4d07760 100644 --- a/spec/lib/gitlab/import_export/importer_spec.rb +++ b/spec/lib/gitlab/import_export/importer_spec.rb @@ -29,7 +29,7 @@ describe Gitlab::ImportExport::Importer do expect(shared.errors).to be_empty end - it 'extracts the archive' do + it 'extracts the archive' do expect(Gitlab::ImportExport::FileImporter).to receive(:import).and_call_original importer.execute diff --git a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb index 242c16c4bdc..6084dc96410 100644 --- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb @@ -12,7 +12,7 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do ] RSpec::Mocks.with_temporary_scope do - @project = create(:project, :builds_disabled, :issues_disabled, name: 'project', path: 'project') + @project = create(:project, :builds_enabled, :issues_disabled, name: 'project', path: 'project') @shared = @project.import_export_shared allow(@shared).to receive(:export_path).and_return('spec/lib/gitlab/import_export/') @@ -40,7 +40,7 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do project = Project.find_by_path('project') expect(project.project_feature.issues_access_level).to eq(ProjectFeature::DISABLED) - expect(project.project_feature.builds_access_level).to eq(ProjectFeature::DISABLED) + expect(project.project_feature.builds_access_level).to eq(ProjectFeature::ENABLED) expect(project.project_feature.snippets_access_level).to eq(ProjectFeature::ENABLED) expect(project.project_feature.wiki_access_level).to eq(ProjectFeature::ENABLED) expect(project.project_feature.merge_requests_access_level).to eq(ProjectFeature::ENABLED) @@ -273,6 +273,11 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do it 'has group milestone' do expect(project.group.milestones.size).to eq(results.fetch(:milestones, 0)) end + + it 'has the correct visibility level' do + # INTERNAL in the `project.json`, group's is PRIVATE + expect(project.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE) + end end context 'Light JSON' do @@ -347,7 +352,7 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do :issues_disabled, name: 'project', path: 'project', - group: create(:group)) + group: create(:group, visibility_level: Gitlab::VisibilityLevel::PRIVATE)) end before do @@ -434,4 +439,58 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do end end end + + describe '#restored_project' do + let(:project) { create(:project) } + let(:shared) { project.import_export_shared } + let(:tree_hash) { { 'visibility_level' => visibility } } + let(:restorer) { described_class.new(user: nil, shared: shared, project: project) } + + before do + restorer.instance_variable_set(:@tree_hash, tree_hash) + end + + context 'no group visibility' do + let(:visibility) { Gitlab::VisibilityLevel::PRIVATE } + + it 'uses the project visibility' do + expect(restorer.restored_project.visibility_level).to eq(visibility) + end + end + + context 'with group visibility' do + before do + group = create(:group, visibility_level: group_visibility) + + project.update(group: group) + end + + context 'private group visibility' do + let(:group_visibility) { Gitlab::VisibilityLevel::PRIVATE } + let(:visibility) { Gitlab::VisibilityLevel::PUBLIC } + + it 'uses the group visibility' do + expect(restorer.restored_project.visibility_level).to eq(group_visibility) + end + end + + context 'public group visibility' do + let(:group_visibility) { Gitlab::VisibilityLevel::PUBLIC } + let(:visibility) { Gitlab::VisibilityLevel::PRIVATE } + + it 'uses the project visibility' do + expect(restorer.restored_project.visibility_level).to eq(visibility) + end + end + + context 'internal group visibility' do + let(:group_visibility) { Gitlab::VisibilityLevel::INTERNAL } + let(:visibility) { Gitlab::VisibilityLevel::PUBLIC } + + it 'uses the group visibility' do + expect(restorer.restored_project.visibility_level).to eq(group_visibility) + end + end + end + end end diff --git a/spec/lib/gitlab/import_export/reader_spec.rb b/spec/lib/gitlab/import_export/reader_spec.rb index 1ef024d3078..f93ff074770 100644 --- a/spec/lib/gitlab/import_export/reader_spec.rb +++ b/spec/lib/gitlab/import_export/reader_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Gitlab::ImportExport::Reader do +describe Gitlab::ImportExport::Reader do let(:shared) { Gitlab::ImportExport::Shared.new(nil) } let(:test_config) { 'spec/support/import_export/import_export.yml' } let(:project_tree_hash) do diff --git a/spec/lib/gitlab/import_export/shared_spec.rb b/spec/lib/gitlab/import_export/shared_spec.rb new file mode 100644 index 00000000000..f2d750c6595 --- /dev/null +++ b/spec/lib/gitlab/import_export/shared_spec.rb @@ -0,0 +1,31 @@ +require 'spec_helper' +require 'fileutils' + +describe Gitlab::ImportExport::Shared do + let(:project) { build(:project) } + subject { project.import_export_shared } + + describe '#error' do + let(:error) { StandardError.new('Error importing into /my/folder Permission denied @ unlink_internal - /var/opt/gitlab/gitlab-rails/shared/a/b/c/uploads/file') } + + it 'filters any full paths' do + subject.error(error) + + expect(subject.errors).to eq(['Error importing into [FILTERED] Permission denied @ unlink_internal - [FILTERED]']) + end + + it 'calls the error logger with the full message' do + expect(subject).to receive(:log_error).with(hash_including(message: error.message)) + + subject.error(error) + end + + it 'calls the debug logger with a backtrace' do + error.set_backtrace('backtrace') + + expect(subject).to receive(:log_debug).with(hash_including(backtrace: 'backtrace')) + + subject.error(error) + end + end +end diff --git a/spec/lib/gitlab/import_export/version_checker_spec.rb b/spec/lib/gitlab/import_export/version_checker_spec.rb index 49d857d9483..76f8253ec9b 100644 --- a/spec/lib/gitlab/import_export/version_checker_spec.rb +++ b/spec/lib/gitlab/import_export/version_checker_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' include ImportExport::CommonUtil describe Gitlab::ImportExport::VersionChecker do - let(:shared) { Gitlab::ImportExport::Shared.new(nil) } + let!(:shared) { Gitlab::ImportExport::Shared.new(nil) } describe 'bundle a project Git repo' do let(:version) { Gitlab::ImportExport.version } diff --git a/spec/lib/gitlab/kubernetes/helm/pod_spec.rb b/spec/lib/gitlab/kubernetes/helm/pod_spec.rb index 2dd3a570a1d..27c802f34ec 100644 --- a/spec/lib/gitlab/kubernetes/helm/pod_spec.rb +++ b/spec/lib/gitlab/kubernetes/helm/pod_spec.rb @@ -2,8 +2,8 @@ require 'rails_helper' describe Gitlab::Kubernetes::Helm::Pod do describe '#generate' do - let(:app) { create(:clusters_applications_prometheus) } - let(:command) { app.install_command } + let(:app) { create(:clusters_applications_prometheus) } + let(:command) { app.install_command } let(:namespace) { Gitlab::Kubernetes::Helm::NAMESPACE } let(:service_account_name) { nil } @@ -30,7 +30,7 @@ describe Gitlab::Kubernetes::Helm::Pod do it 'should generate the appropriate specifications for the container' do container = subject.generate.spec.containers.first expect(container.name).to eq('helm') - expect(container.image).to eq('registry.gitlab.com/gitlab-org/cluster-integration/helm-install-image/releases/2.11.0-kube-1.11.0') + expect(container.image).to eq('registry.gitlab.com/gitlab-org/cluster-integration/helm-install-image/releases/2.12.2-kube-1.11.0') expect(container.env.count).to eq(3) expect(container.env.map(&:name)).to match_array([:HELM_VERSION, :TILLER_NAMESPACE, :COMMAND_SCRIPT]) expect(container.command).to match_array(["/bin/sh"]) diff --git a/spec/lib/gitlab/kubernetes/kube_client_spec.rb b/spec/lib/gitlab/kubernetes/kube_client_spec.rb index 8fc85301304..02364e92149 100644 --- a/spec/lib/gitlab/kubernetes/kube_client_spec.rb +++ b/spec/lib/gitlab/kubernetes/kube_client_spec.rb @@ -24,6 +24,32 @@ describe Gitlab::Kubernetes::KubeClient do end end + shared_examples 'redirection not allowed' do |method_name| + before do + redirect_url = 'https://not-under-our-control.example.com/api/v1/pods' + + stub_request(:get, %r{\A#{api_url}/}) + .to_return(status: 302, headers: { location: redirect_url }) + + stub_request(:get, redirect_url) + .to_return(status: 200, body: '{}') + end + + it 'does not follow redirects' do + method_call = -> do + case method_name + when /\A(get_|delete_)/ + client.public_send(method_name) + when /\A(create_|update_)/ + client.public_send(method_name, {}) + else + raise "Unknown method name #{method_name}" + end + end + expect { method_call.call }.to raise_error(Kubeclient::HttpError) + end + end + describe '#core_client' do subject { client.core_client } @@ -103,6 +129,8 @@ describe Gitlab::Kubernetes::KubeClient do :update_service_account ].each do |method| describe "##{method}" do + include_examples 'redirection not allowed', method + it 'delegates to the core client' do expect(client).to delegate_method(method).to(:core_client) end @@ -123,6 +151,8 @@ describe Gitlab::Kubernetes::KubeClient do :update_cluster_role_binding ].each do |method| describe "##{method}" do + include_examples 'redirection not allowed', method + it 'delegates to the rbac client' do expect(client).to delegate_method(method).to(:rbac_client) end @@ -139,6 +169,8 @@ describe Gitlab::Kubernetes::KubeClient do let(:extensions_client) { client.extensions_client } describe '#get_deployments' do + include_examples 'redirection not allowed', 'get_deployments' + it 'delegates to the extensions client' do expect(client).to delegate_method(:get_deployments).to(:extensions_client) end diff --git a/spec/lib/gitlab/loop_helpers_spec.rb b/spec/lib/gitlab/loop_helpers_spec.rb new file mode 100644 index 00000000000..e17a0342d64 --- /dev/null +++ b/spec/lib/gitlab/loop_helpers_spec.rb @@ -0,0 +1,45 @@ +require 'spec_helper' + +describe Gitlab::LoopHelpers do + let(:class_instance) { (Class.new { include ::Gitlab::LoopHelpers }).new } + + describe '#loop_until' do + subject do + class_instance.loop_until(**params) { true } + end + + context 'when limit is not given' do + let(:params) { { limit: nil } } + + it 'raises an error' do + expect { subject }.to raise_error(ArgumentError) + end + end + + context 'when timeout is specified' do + let(:params) { { timeout: 1.second } } + + it "returns false after it's expired" do + is_expected.to be_falsy + end + + it 'executes the block at least once' do + expect { |b| class_instance.loop_until(**params, &b) } + .to yield_control.at_least(1) + end + end + + context 'when iteration limit is specified' do + let(:params) { { limit: 1 } } + + it "returns false after it's expired" do + is_expected.to be_falsy + end + + it 'executes the block once' do + expect { |b| class_instance.loop_until(**params, &b) } + .to yield_control.once + end + 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 deleted file mode 100644 index 2c987df3767..00000000000 --- a/spec/lib/gitlab/release_blog_post_spec.rb +++ /dev/null @@ -1,97 +0,0 @@ -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..7bc4599e20f 100644 --- a/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb +++ b/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb @@ -16,7 +16,7 @@ describe Gitlab::SidekiqLogging::StructuredLogger do "correlation_id" => 'cid' } end - let(:logger) { double() } + let(:logger) { double } let(:start_payload) do job.merge( 'message' => 'TestWorker JID-da883554ee4fe414012f5f42: start', @@ -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/slash_commands/issue_new_spec.rb b/spec/lib/gitlab/slash_commands/issue_new_spec.rb index 724c76ade6e..59de11766d8 100644 --- a/spec/lib/gitlab/slash_commands/issue_new_spec.rb +++ b/spec/lib/gitlab/slash_commands/issue_new_spec.rb @@ -44,7 +44,7 @@ describe Gitlab::SlashCommands::IssueNew do end context 'issue cannot be created' do - let!(:issue) { create(:issue, project: project, title: 'bird is the word') } + let!(:issue) { create(:issue, project: project, title: 'bird is the word') } let(:regex_match) { described_class.match("issue create #{'a' * 512}}") } it 'displays the errors' do 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/rails/action_view_subscriber_spec.rb b/spec/lib/gitlab/tracing/rails/action_view_subscriber_spec.rb new file mode 100644 index 00000000000..c9d1a06b3e6 --- /dev/null +++ b/spec/lib/gitlab/tracing/rails/action_view_subscriber_spec.rb @@ -0,0 +1,147 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' +require 'rspec-parameterized' + +describe Gitlab::Tracing::Rails::ActionViewSubscriber do + using RSpec::Parameterized::TableSyntax + + shared_examples 'an actionview notification' do + it 'should notify the tracer when the hash contains null values' do + expect(subject).to receive(:postnotify_span).with(notification_name, start, finish, tags: expected_tags, exception: exception) + + subject.public_send(notify_method, start, finish, payload) + end + + it 'should notify the tracer when the payload is missing values' do + expect(subject).to receive(:postnotify_span).with(notification_name, start, finish, tags: expected_tags, exception: exception) + + subject.public_send(notify_method, start, finish, payload.compact) + end + + it 'should not throw exceptions when with the default tracer' do + expect { subject.public_send(notify_method, start, finish, payload) }.not_to raise_error + end + end + + describe '.instrument' do + it 'is unsubscribeable' do + unsubscribe = described_class.instrument + + expect(unsubscribe).not_to be_nil + expect { unsubscribe.call }.not_to raise_error + end + end + + describe '#notify_render_template' do + subject { described_class.new } + let(:start) { Time.now } + let(:finish) { Time.now } + let(:notification_name) { 'render_template' } + let(:notify_method) { :notify_render_template } + + where(:identifier, :layout, :exception) do + nil | nil | nil + "" | nil | nil + "show.haml" | nil | nil + nil | "" | nil + nil | "layout.haml" | nil + nil | nil | StandardError.new + end + + with_them do + let(:payload) do + { + exception: exception, + identifier: identifier, + layout: layout + } + end + + let(:expected_tags) do + { + 'component' => 'ActionView', + 'template.id' => identifier, + 'template.layout' => layout + } + end + + it_behaves_like 'an actionview notification' + end + end + + describe '#notify_render_collection' do + subject { described_class.new } + let(:start) { Time.now } + let(:finish) { Time.now } + let(:notification_name) { 'render_collection' } + let(:notify_method) { :notify_render_collection } + + where( + :identifier, :count, :expected_count, :cache_hits, :expected_cache_hits, :exception) do + nil | nil | 0 | nil | 0 | nil + "" | nil | 0 | nil | 0 | nil + "show.haml" | nil | 0 | nil | 0 | nil + nil | 0 | 0 | nil | 0 | nil + nil | 1 | 1 | nil | 0 | nil + nil | nil | 0 | 0 | 0 | nil + nil | nil | 0 | 1 | 1 | nil + nil | nil | 0 | nil | 0 | StandardError.new + end + + with_them do + let(:payload) do + { + exception: exception, + identifier: identifier, + count: count, + cache_hits: cache_hits + } + end + + let(:expected_tags) do + { + 'component' => 'ActionView', + 'template.id' => identifier, + 'template.count' => expected_count, + 'template.cache.hits' => expected_cache_hits + } + end + + it_behaves_like 'an actionview notification' + end + end + + describe '#notify_render_partial' do + subject { described_class.new } + let(:start) { Time.now } + let(:finish) { Time.now } + let(:notification_name) { 'render_partial' } + let(:notify_method) { :notify_render_partial } + + where(:identifier, :exception) do + nil | nil + "" | nil + "show.haml" | nil + nil | StandardError.new + end + + with_them do + let(:payload) do + { + exception: exception, + identifier: identifier + } + end + + let(:expected_tags) do + { + 'component' => 'ActionView', + 'template.id' => identifier + } + end + + it_behaves_like 'an actionview notification' + end + end +end diff --git a/spec/lib/gitlab/tracing/rails/active_record_subscriber_spec.rb b/spec/lib/gitlab/tracing/rails/active_record_subscriber_spec.rb new file mode 100644 index 00000000000..3d066843148 --- /dev/null +++ b/spec/lib/gitlab/tracing/rails/active_record_subscriber_spec.rb @@ -0,0 +1,73 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' +require 'rspec-parameterized' + +describe Gitlab::Tracing::Rails::ActiveRecordSubscriber do + using RSpec::Parameterized::TableSyntax + + describe '.instrument' do + it 'is unsubscribeable' do + unsubscribe = described_class.instrument + + expect(unsubscribe).not_to be_nil + expect { unsubscribe.call }.not_to raise_error + end + end + + describe '#notify' do + subject { described_class.new } + let(:start) { Time.now } + let(:finish) { Time.now } + + where(:name, :operation_name, :exception, :connection_id, :cached, :cached_response, :sql) do + nil | "active_record:sqlquery" | nil | nil | nil | false | nil + "" | "active_record:sqlquery" | nil | nil | nil | false | nil + "User Load" | "active_record:User Load" | nil | nil | nil | false | nil + "Repo Load" | "active_record:Repo Load" | StandardError.new | nil | nil | false | nil + nil | "active_record:sqlquery" | nil | 123 | nil | false | nil + nil | "active_record:sqlquery" | nil | nil | false | false | nil + nil | "active_record:sqlquery" | nil | nil | true | true | nil + nil | "active_record:sqlquery" | nil | nil | true | true | "SELECT * FROM users" + end + + with_them do + def payload + { + name: name, + exception: exception, + connection_id: connection_id, + cached: cached, + sql: sql + } + end + + def expected_tags + { + "component" => "ActiveRecord", + "span.kind" => "client", + "db.type" => "sql", + "db.connection_id" => connection_id, + "db.cached" => cached_response, + "db.statement" => sql + } + end + + it 'should notify the tracer when the hash contains null values' do + expect(subject).to receive(:postnotify_span).with(operation_name, start, finish, tags: expected_tags, exception: exception) + + subject.notify(start, finish, payload) + end + + it 'should notify the tracer when the payload is missing values' do + expect(subject).to receive(:postnotify_span).with(operation_name, start, finish, tags: expected_tags, exception: exception) + + subject.notify(start, finish, payload.compact) + end + + it 'should not throw exceptions when with the default tracer' do + expect { subject.notify(start, finish, payload) }.not_to raise_error + end + 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/tree_summary_spec.rb b/spec/lib/gitlab/tree_summary_spec.rb index 7ffcef2baef..e22f898dc4c 100644 --- a/spec/lib/gitlab/tree_summary_spec.rb +++ b/spec/lib/gitlab/tree_summary_spec.rb @@ -180,7 +180,7 @@ describe Gitlab::TreeSummary do with_them do before do - create_file('dummy', path: 'other') if num_entries.zero? + create_file('dummy', path: 'other') if num_entries.zero? 1.upto(num_entries) { |n| create_file(n, path: path) } end diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb index 2a09f581f68..4f5993ba226 100644 --- a/spec/lib/gitlab/usage_data_spec.rb +++ b/spec/lib/gitlab/usage_data_spec.rb @@ -26,6 +26,8 @@ describe Gitlab::UsageData do create(:clusters_applications_prometheus, :installed, cluster: gcp_cluster) create(:clusters_applications_runner, :installed, cluster: gcp_cluster) create(:clusters_applications_knative, :installed, cluster: gcp_cluster) + + ProjectFeature.first.update_attribute('repository_access_level', 0) end subject { described_class.data } @@ -112,6 +114,7 @@ describe Gitlab::UsageData do projects_slack_notifications_active projects_slack_slash_active projects_prometheus_active + projects_with_repositories_enabled pages_domains protected_branches releases @@ -134,6 +137,7 @@ describe Gitlab::UsageData do expect(count_data[:projects_jira_cloud_active]).to eq(1) expect(count_data[:projects_slack_notifications_active]).to eq(2) expect(count_data[:projects_slack_slash_active]).to eq(1) + expect(count_data[:projects_with_repositories_enabled]).to eq(2) expect(count_data[:clusters_enabled]).to eq(7) expect(count_data[:project_clusters_enabled]).to eq(6) diff --git a/spec/lib/gitlab_spec.rb b/spec/lib/gitlab_spec.rb index 6ac3d115bc6..5f7a0cca351 100644 --- a/spec/lib/gitlab_spec.rb +++ b/spec/lib/gitlab_spec.rb @@ -70,82 +70,6 @@ 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/lib/safe_zip/entry_spec.rb b/spec/lib/safe_zip/entry_spec.rb new file mode 100644 index 00000000000..115e28c5994 --- /dev/null +++ b/spec/lib/safe_zip/entry_spec.rb @@ -0,0 +1,196 @@ +require 'spec_helper' + +describe SafeZip::Entry do + let(:target_path) { Dir.mktmpdir('safe-zip') } + let(:directories) { %w(public folder/with/subfolder) } + let(:params) { SafeZip::ExtractParams.new(directories: directories, to: target_path) } + + let(:entry) { described_class.new(zip_archive, zip_entry, params) } + let(:entry_name) { 'public/folder/index.html' } + let(:entry_path_dir) { File.join(target_path, File.dirname(entry_name)) } + let(:entry_path) { File.join(target_path, entry_name) } + let(:zip_archive) { double } + + let(:zip_entry) do + double( + name: entry_name, + file?: false, + directory?: false, + symlink?: false) + end + + after do + FileUtils.remove_entry_secure(target_path) + end + + context '#path_dir' do + subject { entry.path_dir } + + it { is_expected.to eq(target_path + '/public/folder') } + end + + context '#exist?' do + subject { entry.exist? } + + context 'when entry does not exist' do + it { is_expected.not_to be_truthy } + end + + context 'when entry does exist' do + before do + create_entry + end + + it { is_expected.to be_truthy } + end + end + + describe '#extract' do + subject { entry.extract } + + context 'when entry does not match the filtered directories' do + using RSpec::Parameterized::TableSyntax + + where(:entry_name) do + [ + 'assets/folder/index.html', + 'public/../folder/index.html', + 'public/../../../../../index.html', + '../../../../../public/index.html', + '/etc/passwd' + ] + end + + with_them do + it 'does not extract file' do + is_expected.to be_falsey + end + end + end + + context 'when entry does exist' do + before do + create_entry + end + + it 'raises an exception' do + expect { subject }.to raise_error(SafeZip::Extract::AlreadyExistsError) + end + end + + context 'when entry type is unknown' do + it 'raises an exception' do + expect { subject }.to raise_error(SafeZip::Extract::UnsupportedEntryError) + end + end + + context 'when entry is valid' do + shared_examples 'secured symlinks' do + context 'when we try to extract entry into symlinked folder' do + before do + FileUtils.mkdir_p(File.join(target_path, "source")) + File.symlink("source", File.join(target_path, "public")) + end + + it 'raises an exception' do + expect { subject }.to raise_error(SafeZip::Extract::PermissionDeniedError) + end + end + end + + context 'and is file' do + before do + allow(zip_entry).to receive(:file?) { true } + end + + it 'does extract file' do + expect(zip_archive).to receive(:extract) + .with(zip_entry, entry_path) + .and_return(true) + + is_expected.to be_truthy + end + + it_behaves_like 'secured symlinks' + end + + context 'and is directory' do + let(:entry_name) { 'public/folder/assets' } + + before do + allow(zip_entry).to receive(:directory?) { true } + end + + it 'does create directory' do + is_expected.to be_truthy + + expect(File.exist?(entry_path)).to eq(true) + end + + it_behaves_like 'secured symlinks' + end + + context 'and is symlink' do + let(:entry_name) { 'public/folder/assets' } + + before do + allow(zip_entry).to receive(:symlink?) { true } + allow(zip_archive).to receive(:read).with(zip_entry) { entry_symlink } + end + + shared_examples 'a valid symlink' do + it 'does create symlink' do + is_expected.to be_truthy + + expect(File.exist?(entry_path)).to eq(true) + end + end + + context 'when source is within target' do + let(:entry_symlink) { '../images' } + + context 'but does not exist' do + it 'raises an exception' do + expect { subject }.to raise_error(SafeZip::Extract::SymlinkSourceDoesNotExistError) + end + end + + context 'and does exist' do + before do + FileUtils.mkdir_p(File.join(target_path, 'public', 'images')) + end + + it_behaves_like 'a valid symlink' + end + end + + context 'when source points outside of target' do + let(:entry_symlink) { '../../images' } + + before do + FileUtils.mkdir(File.join(target_path, 'images')) + end + + it 'raises an exception' do + expect { subject }.to raise_error(SafeZip::Extract::PermissionDeniedError) + end + end + + context 'when source points to /etc/passwd' do + let(:entry_symlink) { '/etc/passwd' } + + it 'raises an exception' do + expect { subject }.to raise_error(SafeZip::Extract::PermissionDeniedError) + end + end + end + end + end + + private + + def create_entry + FileUtils.mkdir_p(entry_path_dir) + FileUtils.touch(entry_path) + end +end diff --git a/spec/lib/safe_zip/extract_params_spec.rb b/spec/lib/safe_zip/extract_params_spec.rb new file mode 100644 index 00000000000..85e22cfa495 --- /dev/null +++ b/spec/lib/safe_zip/extract_params_spec.rb @@ -0,0 +1,54 @@ +require 'spec_helper' + +describe SafeZip::ExtractParams do + let(:target_path) { Dir.mktmpdir("safe-zip") } + let(:params) { described_class.new(directories: directories, to: target_path) } + let(:directories) { %w(public folder/with/subfolder) } + + after do + FileUtils.remove_entry_secure(target_path) + end + + describe '#extract_path' do + subject { params.extract_path } + + it { is_expected.to eq(target_path) } + end + + describe '#matching_target_directory' do + using RSpec::Parameterized::TableSyntax + + subject { params.matching_target_directory(target_path + path) } + + where(:path, :result) do + '/public/index.html' | '/public/' + '/non/existing/path' | nil + '/public' | nil + '/folder/with/index.html' | nil + end + + with_them do + it { is_expected.to eq(result ? target_path + result : nil) } + end + end + + describe '#target_directories' do + subject { params.target_directories } + + it 'starts with target_path' do + is_expected.to all(start_with(target_path + '/')) + end + + it 'ends with / for all paths' do + is_expected.to all(end_with('/')) + end + end + + describe '#directories_wildcard' do + subject { params.directories_wildcard } + + it 'adds * for all paths' do + is_expected.to all(end_with('/*')) + end + end +end diff --git a/spec/lib/safe_zip/extract_spec.rb b/spec/lib/safe_zip/extract_spec.rb new file mode 100644 index 00000000000..b75a8fede00 --- /dev/null +++ b/spec/lib/safe_zip/extract_spec.rb @@ -0,0 +1,80 @@ +require 'spec_helper' + +describe SafeZip::Extract do + let(:target_path) { Dir.mktmpdir('safe-zip') } + let(:directories) { %w(public) } + let(:object) { described_class.new(archive) } + let(:archive) { Rails.root.join('spec', 'fixtures', 'safe_zip', archive_name) } + + after do + FileUtils.remove_entry_secure(target_path) + end + + context '#extract' do + subject { object.extract(directories: directories, to: target_path) } + + shared_examples 'extracts archive' do |param| + before do + stub_feature_flags(safezip_use_rubyzip: param) + end + + it 'does extract archive' do + subject + + expect(File.exist?(File.join(target_path, 'public', 'index.html'))).to eq(true) + expect(File.exist?(File.join(target_path, 'source'))).to eq(false) + end + end + + shared_examples 'fails to extract archive' do |param| + before do + stub_feature_flags(safezip_use_rubyzip: param) + end + + it 'does not extract archive' do + expect { subject }.to raise_error(SafeZip::Extract::Error) + end + end + + %w(valid-simple.zip valid-symlinks-first.zip valid-non-writeable.zip).each do |name| + context "when using #{name} archive" do + let(:archive_name) { name } + + context 'for RubyZip' do + it_behaves_like 'extracts archive', true + end + + context 'for UnZip' do + it_behaves_like 'extracts archive', false + end + end + end + + %w(invalid-symlink-does-not-exist.zip invalid-symlinks-outside.zip).each do |name| + context "when using #{name} archive" do + let(:archive_name) { name } + + context 'for RubyZip' do + it_behaves_like 'fails to extract archive', true + end + + context 'for UnZip (UNSAFE)' do + it_behaves_like 'extracts archive', false + end + end + end + + context 'when no matching directories are found' do + let(:archive_name) { 'valid-simple.zip' } + let(:directories) { %w(non/existing) } + + context 'for RubyZip' do + it_behaves_like 'fails to extract archive', true + end + + context 'for UnZip' do + it_behaves_like 'fails to extract archive', false + end + end + end +end diff --git a/spec/lib/sentry/client_spec.rb b/spec/lib/sentry/client_spec.rb index b36be0fd9c1..6fbf60a6222 100644 --- a/spec/lib/sentry/client_spec.rb +++ b/spec/lib/sentry/client_spec.rb @@ -3,30 +3,76 @@ require 'spec_helper' describe Sentry::Client do - let(:issue_status) { 'unresolved' } - let(:limit) { 20 } let(:sentry_url) { 'https://sentrytest.gitlab.com/api/0/projects/sentry-org/sentry-project' } let(:token) { 'test-token' } - let(:sample_response) do + let(:issues_sample_response) do Gitlab::Utils.deep_indifferent_access( - JSON.parse(File.read(Rails.root.join('spec/fixtures/sentry/issues_sample_response.json'))) + JSON.parse(fixture_file('sentry/issues_sample_response.json')) + ) + end + + let(:projects_sample_response) do + Gitlab::Utils.deep_indifferent_access( + JSON.parse(fixture_file('sentry/list_projects_sample_response.json')) ) end subject(:client) { described_class.new(sentry_url, token) } - describe '#list_issues' do - subject { client.list_issues(issue_status: issue_status, limit: limit) } + # Requires sentry_api_url and subject to be defined + shared_examples 'no redirects' do + let(:redirect_to) { 'https://redirected.example.com' } + let(:other_url) { 'https://other.example.org' } + + let!(:redirected_req_stub) { stub_sentry_request(other_url) } + + let!(:redirect_req_stub) do + stub_sentry_request( + sentry_api_url, + status: 302, + headers: { location: redirect_to } + ) + end - before do - stub_sentry_request(sentry_url + '/issues/?limit=20&query=is:unresolved', body: sample_response) + it 'does not follow redirects' do + expect { subject }.to raise_exception(Sentry::Client::Error, 'Sentry response error: 302') + expect(redirect_req_stub).to have_been_requested + expect(redirected_req_stub).not_to have_been_requested end + end - it 'returns objects of type ErrorTracking::Error' do - expect(subject.length).to eq(1) - expect(subject[0]).to be_a(Gitlab::ErrorTracking::Error) + shared_examples 'has correct return type' do |klass| + it "returns objects of type #{klass}" do + expect(subject).to all( be_a(klass) ) end + end + + shared_examples 'has correct length' do |length| + it { expect(subject.length).to eq(length) } + end + + # Requires sentry_api_request and subject to be defined + shared_examples 'calls sentry api' do + it 'calls sentry api' do + subject + + expect(sentry_api_request).to have_been_requested + end + end + + describe '#list_issues' do + let(:issue_status) { 'unresolved' } + let(:limit) { 20 } + + let!(:sentry_api_request) { stub_sentry_request(sentry_url + '/issues/?limit=20&query=is:unresolved', body: issues_sample_response) } + + subject { client.list_issues(issue_status: issue_status, limit: limit) } + + it_behaves_like 'calls sentry api' + + it_behaves_like 'has correct return type', Gitlab::ErrorTracking::Error + it_behaves_like 'has correct length', 1 context 'error object created from sentry response' do using RSpec::Parameterized::TableSyntax @@ -50,7 +96,7 @@ describe Sentry::Client do end with_them do - it { expect(subject[0].public_send(error_object)).to eq(sample_response[0].dig(*sentry_response)) } + it { expect(subject[0].public_send(error_object)).to eq(issues_sample_response[0].dig(*sentry_response)) } end context 'external_url' do @@ -61,24 +107,9 @@ describe Sentry::Client do end context 'redirects' do - let(:redirect_to) { 'https://redirected.example.com' } - let(:other_url) { 'https://other.example.org' } - - let!(:redirected_req_stub) { stub_sentry_request(other_url) } - - let!(:redirect_req_stub) do - stub_sentry_request( - sentry_url + '/issues/?limit=20&query=is:unresolved', - status: 302, - headers: { location: redirect_to } - ) - end + let(:sentry_api_url) { sentry_url + '/issues/?limit=20&query=is:unresolved' } - it 'does not follow redirects' do - expect { subject }.to raise_exception(Sentry::Client::Error, 'Sentry response error: 302') - expect(redirect_req_stub).to have_been_requested - expect(redirected_req_stub).not_to have_been_requested - end + it_behaves_like 'no redirects' end # Sentry API returns 404 if there are extra slashes in the URL! @@ -99,7 +130,75 @@ describe Sentry::Client do anything ).and_call_original - client.list_issues(issue_status: issue_status, limit: limit) + subject + + expect(valid_req_stub).to have_been_requested + end + end + end + + describe '#list_projects' do + let(:sentry_list_projects_url) { 'https://sentrytest.gitlab.com/api/0/projects/' } + + let!(:sentry_api_request) { stub_sentry_request(sentry_list_projects_url, body: projects_sample_response) } + + subject { client.list_projects } + + it_behaves_like 'calls sentry api' + + it_behaves_like 'has correct return type', Gitlab::ErrorTracking::Project + it_behaves_like 'has correct length', 2 + + context 'keys missing in API response' do + it 'raises exception' do + projects_sample_response[0].delete(:slug) + + stub_sentry_request(sentry_list_projects_url, body: projects_sample_response) + + expect { subject }.to raise_error(Sentry::Client::SentryError, 'Sentry API response is missing keys. key not found: "slug"') + end + end + + context 'error object created from sentry response' do + using RSpec::Parameterized::TableSyntax + + where(:sentry_project_object, :sentry_response) do + :id | :id + :name | :name + :status | :status + :slug | :slug + :organization_name | [:organization, :name] + :organization_id | [:organization, :id] + :organization_slug | [:organization, :slug] + end + + with_them do + it { expect(subject[0].public_send(sentry_project_object)).to eq(projects_sample_response[0].dig(*sentry_response)) } + end + end + + context 'redirects' do + let(:sentry_api_url) { sentry_list_projects_url } + + it_behaves_like 'no redirects' + end + + # Sentry API returns 404 if there are extra slashes in the URL! + context 'extra slashes in URL' do + let(:sentry_url) { 'https://sentrytest.gitlab.com/api//0/projects//' } + let(:client) { described_class.new(sentry_url, token) } + + let!(:valid_req_stub) do + stub_sentry_request(sentry_list_projects_url) + end + + it 'removes extra slashes in api url' do + expect(Gitlab::HTTP).to receive(:get).with( + URI(sentry_list_projects_url), + anything + ).and_call_original + + subject expect(valid_req_stub).to have_been_requested end diff --git a/spec/mailers/emails/pages_domains_spec.rb b/spec/mailers/emails/pages_domains_spec.rb index fe428ea657d..c74fd66ad22 100644 --- a/spec/mailers/emails/pages_domains_spec.rb +++ b/spec/mailers/emails/pages_domains_spec.rb @@ -6,7 +6,7 @@ describe Emails::PagesDomains do include_context 'gitlab email notification' set(:project) { create(:project) } - set(:domain) { create(:pages_domain, project: project) } + set(:domain) { create(:pages_domain, project: project) } set(:user) { project.owner } shared_examples 'a pages domain email' do diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb index f2d99872401..4f578c48d5b 100644 --- a/spec/mailers/notify_spec.rb +++ b/spec/mailers/notify_spec.rb @@ -9,8 +9,10 @@ describe Notify do include_context 'gitlab email notification' + let(:current_user_sanitized) { 'www_example_com' } + set(:user) { create(:user) } - set(:current_user) { create(:user, email: "current@email.com") } + set(:current_user) { create(:user, email: "current@email.com", name: 'www.example.com') } set(:assignee) { create(:user, email: 'assignee@example.com', name: 'John Doe') } set(:merge_request) do @@ -182,7 +184,7 @@ describe Notify do aggregate_failures do is_expected.to have_referable_subject(issue, reply: true) is_expected.to have_body_text(status) - is_expected.to have_body_text(current_user.name) + is_expected.to have_body_text(current_user_sanitized) is_expected.to have_body_text(project_issue_path project, issue) end end @@ -361,7 +363,7 @@ describe Notify do aggregate_failures do is_expected.to have_referable_subject(merge_request, reply: true) is_expected.to have_body_text(status) - is_expected.to have_body_text(current_user.name) + is_expected.to have_body_text(current_user_sanitized) is_expected.to have_body_text(project_merge_request_path(project, merge_request)) end end @@ -887,7 +889,7 @@ describe Notify do allow(Note).to receive(:find).with(note.id).and_return(note) end - shared_examples 'an email for a note on a diff discussion' do |model| + shared_examples 'an email for a note on a diff discussion' do |model| let(:note) { create(model, author: note_author) } context 'when note is not on text' do 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/delete_inconsistent_internal_id_records_spec.rb b/spec/migrations/delete_inconsistent_internal_id_records_spec.rb index 51291cb362a..e2ce69a7bb1 100644 --- a/spec/migrations/delete_inconsistent_internal_id_records_spec.rb +++ b/spec/migrations/delete_inconsistent_internal_id_records_spec.rb @@ -102,7 +102,7 @@ describe DeleteInconsistentInternalIdRecords, :migration do context 'for milestones (by group)' do # milestones (by group) is a little different than most of the other models - let(:groups) { table(:namespaces) } + let(:groups) { table(:namespaces) } let(:group1) { groups.create(name: 'Group 1', type: 'Group', path: 'group_1') } let(:group2) { groups.create(name: 'Group 2', type: 'Group', path: 'group_2') } let(:group3) { groups.create(name: 'Group 2', type: 'Group', path: 'group_3') } diff --git a/spec/migrations/migrate_storage_migrator_sidekiq_queue_spec.rb b/spec/migrations/migrate_storage_migrator_sidekiq_queue_spec.rb new file mode 100644 index 00000000000..f8cf76cb339 --- /dev/null +++ b/spec/migrations/migrate_storage_migrator_sidekiq_queue_spec.rb @@ -0,0 +1,47 @@ +require 'spec_helper' +require Rails.root.join('db', 'post_migrate', '20190124200344_migrate_storage_migrator_sidekiq_queue.rb') + +describe MigrateStorageMigratorSidekiqQueue, :sidekiq, :redis do + include Gitlab::Database::MigrationHelpers + + context 'when there are jobs in the queues' do + it 'correctly migrates queue when migrating up' do + Sidekiq::Testing.disable! do + stubbed_worker(queue: :storage_migrator).perform_async(1, 5) + + described_class.new.up + + expect(sidekiq_queue_length('storage_migrator')).to eq 0 + expect(sidekiq_queue_length('hashed_storage:hashed_storage_migrator')).to eq 1 + end + end + + it 'correctly migrates queue when migrating down' do + Sidekiq::Testing.disable! do + stubbed_worker(queue: :'hashed_storage:hashed_storage_migrator').perform_async(1, 5) + + described_class.new.down + + expect(sidekiq_queue_length('storage_migrator')).to eq 1 + expect(sidekiq_queue_length('hashed_storage:hashed_storage_migrator')).to eq 0 + end + end + end + + context 'when there are no jobs in the queues' do + it 'does not raise error when migrating up' do + expect { described_class.new.up }.not_to raise_error + end + + it 'does not raise error when migrating down' do + expect { described_class.new.down }.not_to raise_error + end + end + + def stubbed_worker(queue:) + Class.new do + include Sidekiq::Worker + sidekiq_options queue: queue + end + end +end diff --git a/spec/migrations/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/migrations/update_project_import_visibility_level_spec.rb b/spec/migrations/update_project_import_visibility_level_spec.rb new file mode 100644 index 00000000000..9ea9b956f67 --- /dev/null +++ b/spec/migrations/update_project_import_visibility_level_spec.rb @@ -0,0 +1,86 @@ +# frozen_string_literal: true + +require 'spec_helper' +require Rails.root.join('db', 'post_migrate', '20181219130552_update_project_import_visibility_level.rb') + +describe UpdateProjectImportVisibilityLevel, :migration do + let(:namespaces) { table(:namespaces) } + let(:projects) { table(:projects) } + let(:project) { projects.find_by_name(name) } + + before do + stub_const("#{described_class}::BATCH_SIZE", 1) + end + + context 'private visibility level' do + let(:name) { 'private-public' } + + it 'updates the project visibility' do + create_namespace(name, Gitlab::VisibilityLevel::PRIVATE) + create_project(name, Gitlab::VisibilityLevel::PUBLIC) + + expect { migrate! }.to change { project.reload.visibility_level }.to(Gitlab::VisibilityLevel::PRIVATE) + end + end + + context 'internal visibility level' do + let(:name) { 'internal-public' } + + it 'updates the project visibility' do + create_namespace(name, Gitlab::VisibilityLevel::INTERNAL) + create_project(name, Gitlab::VisibilityLevel::PUBLIC) + + expect { migrate! }.to change { project.reload.visibility_level }.to(Gitlab::VisibilityLevel::INTERNAL) + end + end + + context 'public visibility level' do + let(:name) { 'public-public' } + + it 'does not update the project visibility' do + create_namespace(name, Gitlab::VisibilityLevel::PUBLIC) + create_project(name, Gitlab::VisibilityLevel::PUBLIC) + + expect { migrate! }.not_to change { project.reload.visibility_level } + end + end + + context 'private project visibility level' do + let(:name) { 'public-private' } + + it 'does not update the project visibility' do + create_namespace(name, Gitlab::VisibilityLevel::PUBLIC) + create_project(name, Gitlab::VisibilityLevel::PRIVATE) + + expect { migrate! }.not_to change { project.reload.visibility_level } + end + end + + context 'no namespace' do + let(:name) { 'no-namespace' } + + it 'does not update the project visibility' do + create_namespace(name, Gitlab::VisibilityLevel::PRIVATE, type: nil) + create_project(name, Gitlab::VisibilityLevel::PUBLIC) + + expect { migrate! }.not_to change { project.reload.visibility_level } + end + end + + def create_namespace(name, visibility, options = {}) + namespaces.create({ + name: name, + path: name, + type: 'Group', + visibility_level: visibility + }.merge(options)) + end + + def create_project(name, visibility) + projects.create!(namespace_id: namespaces.find_by_name(name).id, + name: name, + path: name, + import_type: 'gitlab_project', + visibility_level: visibility) + end +end diff --git a/spec/models/ability_spec.rb b/spec/models/ability_spec.rb index 199f49d0bf2..eee80e9bad7 100644 --- a/spec/models/ability_spec.rb +++ b/spec/models/ability_spec.rb @@ -298,7 +298,6 @@ describe Ability do context 'wiki named abilities' do it 'disables wiki abilities if the project has no wiki' do - expect(project).to receive(:has_external_wiki?).and_return(false) expect(subject).not_to be_allowed(:read_wiki) expect(subject).not_to be_allowed(:create_wiki) expect(subject).not_to be_allowed(:update_wiki) diff --git a/spec/models/appearance_spec.rb b/spec/models/appearance_spec.rb index ec2e7d672f0..cc76a2019ec 100644 --- a/spec/models/appearance_spec.rb +++ b/spec/models/appearance_spec.rb @@ -36,6 +36,13 @@ describe Appearance do expect(subject.send("#{logo_type}_path")).to be_nil end + it 'returns the path when the upload has been orphaned' do + appearance.send(logo_type).upload.destroy + appearance.reload + + expect(appearance.send("#{logo_type}_path")).to eq(expected_path) + end + it 'returns a local path using the system route' do expect(appearance.send("#{logo_type}_path")).to eq(expected_path) end diff --git a/spec/models/application_record_spec.rb b/spec/models/application_record_spec.rb new file mode 100644 index 00000000000..68aed387bfc --- /dev/null +++ b/spec/models/application_record_spec.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe ApplicationRecord do + describe '#id_in' do + let(:records) { create_list(:user, 3) } + + it 'returns records of the ids' do + expect(User.id_in(records.last(2).map(&:id))).to eq(records.last(2)) + end + end +end diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index 1afc2436bb5..8a1bbb26e57 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -2133,6 +2133,8 @@ describe Ci::Build do { key: 'CI_PROJECT_NAMESPACE', value: project.namespace.full_path, public: true }, { key: 'CI_PROJECT_URL', value: project.web_url, public: true }, { key: 'CI_PROJECT_VISIBILITY', value: 'private', public: true }, + { key: 'CI_PAGES_DOMAIN', value: Gitlab.config.pages.host, public: true }, + { key: 'CI_PAGES_URL', value: project.pages_url, public: true }, { key: 'CI_API_V4_URL', value: 'http://localhost/api/v4', public: true }, { key: 'CI_PIPELINE_IID', value: pipeline.iid.to_s, public: true }, { key: 'CI_CONFIG_PATH', value: pipeline.ci_yaml_file_path, public: true }, @@ -2480,7 +2482,7 @@ describe Ci::Build do context 'when container registry is enabled' do let(:container_registry_enabled) { true } let(:ci_registry) do - { key: 'CI_REGISTRY', value: 'registry.example.com', public: true } + { key: 'CI_REGISTRY', value: 'registry.example.com', public: true } end let(:ci_registry_image) do { key: 'CI_REGISTRY_IMAGE', value: project.container_registry_url, public: true } @@ -3028,6 +3030,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/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 17f33785fda..72a0df96a80 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -39,6 +39,29 @@ describe Ci::Pipeline, :mailer do end end + describe '.processables' do + before do + create(:ci_build, name: 'build', pipeline: pipeline) + create(:ci_bridge, name: 'bridge', pipeline: pipeline) + create(:commit_status, name: 'commit status', pipeline: pipeline) + create(:generic_commit_status, name: 'generic status', pipeline: pipeline) + end + + it 'has an association with processable CI/CD entities' do + pipeline.processables.pluck('name').yield_self do |processables| + expect(processables).to match_array %w[build bridge] + end + end + + it 'makes it possible to append a new processable' do + pipeline.processables << build(:ci_bridge) + + pipeline.save! + + expect(pipeline.processables.reload.count).to eq 3 + end + end + describe '.sort_by_merge_request_pipelines' do subject { described_class.sort_by_merge_request_pipelines } 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/clusters/kubernetes_namespace_spec.rb b/spec/models/clusters/kubernetes_namespace_spec.rb index 56c98d016c9..235e2ee4e69 100644 --- a/spec/models/clusters/kubernetes_namespace_spec.rb +++ b/spec/models/clusters/kubernetes_namespace_spec.rb @@ -28,7 +28,7 @@ RSpec.describe Clusters::KubernetesNamespace, type: :model do let(:cluster_project) { create(:cluster_project) } let(:kubernetes_namespace) { build(:cluster_kubernetes_namespace, namespace: 'my-namespace') } - subject { kubernetes_namespace } + subject { kubernetes_namespace } context 'when cluster is using the namespace' do before do diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb index a2d2d77746d..baad8352185 100644 --- a/spec/models/commit_spec.rb +++ b/spec/models/commit_spec.rb @@ -11,6 +11,7 @@ describe Commit do it { is_expected.to include_module(Participable) } it { is_expected.to include_module(Referable) } it { is_expected.to include_module(StaticModel) } + it { is_expected.to include_module(Presentable) } end describe '.lazy' do diff --git a/spec/models/concerns/cache_markdown_field_spec.rb b/spec/models/concerns/cache_markdown_field_spec.rb index f8d50e89d40..ef6af232999 100644 --- a/spec/models/concerns/cache_markdown_field_spec.rb +++ b/spec/models/concerns/cache_markdown_field_spec.rb @@ -67,13 +67,17 @@ describe CacheMarkdownField do end let(:markdown) { '`Foo`' } - let(:html) { '<p dir="auto"><code>Foo</code></p>' } + let(:html) { '<p dir="auto"><code>Foo</code></p>' } let(:updated_markdown) { '`Bar`' } - let(:updated_html) { '<p dir="auto"><code>Bar</code></p>' } + let(:updated_html) { '<p dir="auto"><code>Bar</code></p>' } let(:thing) { ThingWithMarkdownFields.new(foo: markdown, foo_html: html, cached_markdown_version: CacheMarkdownField::CACHE_COMMONMARK_VERSION) } + before do + stub_commonmark_sourcepos_disabled + end + describe '.attributes' do it 'excludes cache attributes' do expect(thing.attributes.keys.sort).to eq(%w[bar baz foo]) diff --git a/spec/models/concerns/cacheable_attributes_spec.rb b/spec/models/concerns/cacheable_attributes_spec.rb index 689e7d3058f..43a544cfe26 100644 --- a/spec/models/concerns/cacheable_attributes_spec.rb +++ b/spec/models/concerns/cacheable_attributes_spec.rb @@ -159,6 +159,10 @@ describe CacheableAttributes do describe 'edge cases' do describe 'caching behavior', :use_clean_rails_memory_store_caching do + before do + stub_commonmark_sourcepos_disabled + end + it 'retrieves upload fields properly' do ar_record = create(:appearance, :with_logo) ar_record.cache! diff --git a/spec/models/concerns/discussion_on_diff_spec.rb b/spec/models/concerns/discussion_on_diff_spec.rb index 4b16e6e3902..64bf04071e8 100644 --- a/spec/models/concerns/discussion_on_diff_spec.rb +++ b/spec/models/concerns/discussion_on_diff_spec.rb @@ -7,7 +7,7 @@ describe DiscussionOnDiff do let(:truncated_lines) { subject.truncated_diff_lines } context "when diff is greater than allowed number of truncated diff lines " do - it "returns fewer lines" do + it "returns fewer lines" do expect(subject.diff_lines.count).to be > DiffDiscussion::NUMBER_OF_TRUNCATED_DIFF_LINES expect(truncated_lines.count).to be <= DiffDiscussion::NUMBER_OF_TRUNCATED_DIFF_LINES diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb index a4bf3e2350a..5753c646106 100644 --- a/spec/models/concerns/issuable_spec.rb +++ b/spec/models/concerns/issuable_spec.rb @@ -566,7 +566,7 @@ describe Issuable do end let(:merged_mr) { create(:merge_request, :merged, author: contributor, target_project: project, source_project: project) } - let(:open_mr) { create(:merge_request, author: first_time_contributor, target_project: project, source_project: project) } + let(:open_mr) { create(:merge_request, author: first_time_contributor, target_project: project, source_project: project) } let(:merged_mr_other_project) { create(:merge_request, :merged, author: first_time_contributor, target_project: other_project, source_project: other_project) } context "for merge requests" do diff --git a/spec/models/concerns/redactable_spec.rb b/spec/models/concerns/redactable_spec.rb index 7d320edd492..7feeaa54069 100644 --- a/spec/models/concerns/redactable_spec.rb +++ b/spec/models/concerns/redactable_spec.rb @@ -1,6 +1,10 @@ require 'spec_helper' describe Redactable do + before do + stub_commonmark_sourcepos_disabled + end + shared_examples 'model with redactable field' do it 'redacts unsubscribe token' do model[field] = 'some text /sent_notifications/00000000000000000000000000000000/unsubscribe more text' diff --git a/spec/models/event_spec.rb b/spec/models/event_spec.rb index a64720f1876..ce4f8ee4705 100644 --- a/spec/models/event_spec.rb +++ b/spec/models/event_spec.rb @@ -399,10 +399,7 @@ describe Event do expect(event.visible_to_user?(nil)).to be_falsy expect(event.visible_to_user?(non_member)).to be_falsy expect(event.visible_to_user?(author)).to be_truthy - - # It is very unexpected that a private personal snippet is not visible - # to an instance administrator. This should be fixed in the future. - expect(event.visible_to_user?(admin)).to be_falsy + expect(event.visible_to_user?(admin)).to be_truthy end end end diff --git a/spec/models/external_issue_spec.rb b/spec/models/external_issue_spec.rb index c8748daf46b..83ba22caa03 100644 --- a/spec/models/external_issue_spec.rb +++ b/spec/models/external_issue_spec.rb @@ -30,7 +30,7 @@ describe ExternalIssue do end context 'if issue id is a number' do - let(:issue) { described_class.new('1234', project) } + let(:issue) { described_class.new('1234', project) } it 'returns the issue ID prefixed by #' do expect(issue.reference_link_text).to eq '#1234' end diff --git a/spec/models/global_milestone_spec.rb b/spec/models/global_milestone_spec.rb index 62699df5611..f93904065c7 100644 --- a/spec/models/global_milestone_spec.rb +++ b/spec/models/global_milestone_spec.rb @@ -91,6 +91,12 @@ describe GlobalMilestone do it 'sorts collection by due date' do expect(global_milestones.map(&:due_date)).to eq [milestone1_due_date, milestone1_due_date, milestone1_due_date, nil, nil, nil] end + + it 'filters milestones by search_title when params[:search_title] is present' do + global_milestones = described_class.build_collection(projects, { search_title: 'v1.2' }) + + expect(global_milestones.map(&:title)).to match_array(['Milestone v1.2', 'Milestone v1.2', 'Milestone v1.2']) + end end context 'when adding new milestones' 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/identity_spec.rb b/spec/models/identity_spec.rb index a5ce245c21d..e1a7a59dfd1 100644 --- a/spec/models/identity_spec.rb +++ b/spec/models/identity_spec.rb @@ -10,6 +10,40 @@ describe Identity do it { is_expected.to respond_to(:extern_uid) } end + describe 'validations' do + set(:user) { create(:user) } + + context 'with existing user and provider' do + before do + create(:identity, provider: 'ldapmain', user_id: user.id) + end + + it 'returns false for a duplicate entry' do + identity = user.identities.build(provider: 'ldapmain', user_id: user.id) + + expect(identity.validate).to be_falsey + end + + it 'returns true when a different provider is used' do + identity = user.identities.build(provider: 'gitlab', user_id: user.id) + + expect(identity.validate).to be_truthy + end + end + + context 'with newly-created user' do + before do + create(:identity, provider: 'ldapmain', user_id: nil) + end + + it 'successfully validates even with a nil user_id' do + identity = user.identities.build(provider: 'ldapmain') + + expect(identity.validate).to be_truthy + end + end + end + describe '#is_ldap?' do let(:ldap_identity) { create(:identity, provider: 'ldapmain') } let(:other_identity) { create(:identity, provider: 'twitter') } diff --git a/spec/models/internal_id_spec.rb b/spec/models/internal_id_spec.rb index 4696341c05f..d32f163f05b 100644 --- a/spec/models/internal_id_spec.rb +++ b/spec/models/internal_id_spec.rb @@ -13,6 +13,29 @@ describe InternalId do it { is_expected.to validate_presence_of(:usage) } end + describe '.flush_records!' do + subject { described_class.flush_records!(project: project) } + + let(:another_project) { create(:project) } + + before do + create_list(:issue, 2, project: project) + create_list(:issue, 2, project: another_project) + end + + it 'deletes all records for the given project' do + expect { subject }.to change { described_class.where(project: project).count }.from(1).to(0) + end + + it 'retains records for other projects' do + expect { subject }.not_to change { described_class.where(project: another_project).count } + end + + it 'does not allow an empty filter' do + expect { described_class.flush_records!({}) }.to raise_error(/filter cannot be empty/) + end + end + describe '.generate_next' do subject { described_class.generate_next(issue, scope, usage, init) } 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/label_note_spec.rb b/spec/models/label_note_spec.rb index f69874d94aa..dd2c702a7a9 100644 --- a/spec/models/label_note_spec.rb +++ b/spec/models/label_note_spec.rb @@ -3,8 +3,8 @@ require 'spec_helper' describe LabelNote do - set(:project) { create(:project, :repository) } - set(:user) { create(:user) } + set(:project) { create(:project, :repository) } + set(:user) { create(:user) } set(:label) { create(:label, project: project) } set(:label2) { create(:label, project: project) } let(:resource_parent) { project } diff --git a/spec/models/lfs_download_object_spec.rb b/spec/models/lfs_download_object_spec.rb new file mode 100644 index 00000000000..88838b127d2 --- /dev/null +++ b/spec/models/lfs_download_object_spec.rb @@ -0,0 +1,68 @@ +require 'rails_helper' + +describe LfsDownloadObject do + let(:oid) { 'cd293be6cea034bd45a0352775a219ef5dc7825ce55d1f7dae9762d80ce64411' } + let(:link) { 'http://www.example.com' } + let(:size) { 1 } + + subject { described_class.new(oid: oid, size: size, link: link) } + + describe 'validations' do + it { is_expected.to validate_numericality_of(:size).is_greater_than_or_equal_to(0) } + + context 'oid attribute' do + it 'must be 64 characters long' do + aggregate_failures do + expect(described_class.new(oid: 'a' * 63, size: size, link: link)).to be_invalid + expect(described_class.new(oid: 'a' * 65, size: size, link: link)).to be_invalid + expect(described_class.new(oid: 'a' * 64, size: size, link: link)).to be_valid + end + end + + it 'must contain only hexadecimal characters' do + aggregate_failures do + expect(subject).to be_valid + expect(described_class.new(oid: 'g' * 64, size: size, link: link)).to be_invalid + end + end + end + + context 'link attribute' do + it 'only http and https protocols are valid' do + aggregate_failures do + expect(described_class.new(oid: oid, size: size, link: 'http://www.example.com')).to be_valid + expect(described_class.new(oid: oid, size: size, link: 'https://www.example.com')).to be_valid + expect(described_class.new(oid: oid, size: size, link: 'ftp://www.example.com')).to be_invalid + expect(described_class.new(oid: oid, size: size, link: 'ssh://www.example.com')).to be_invalid + expect(described_class.new(oid: oid, size: size, link: 'git://www.example.com')).to be_invalid + end + end + + it 'cannot be empty' do + expect(described_class.new(oid: oid, size: size, link: '')).not_to be_valid + end + + context 'when localhost or local network addresses' do + subject { described_class.new(oid: oid, size: size, link: 'http://192.168.1.1') } + + before do + allow(ApplicationSetting) + .to receive(:current) + .and_return(ApplicationSetting.build_from_defaults(allow_local_requests_from_hooks_and_services: setting)) + end + + context 'are allowed' do + let(:setting) { true } + + it { expect(subject).to be_valid } + end + + context 'are not allowed' do + let(:setting) { false } + + it { expect(subject).to be_invalid } + end + end + end + end +end diff --git a/spec/models/lfs_file_lock_spec.rb b/spec/models/lfs_file_lock_spec.rb index e74f342d3eb..41ca1578b94 100644 --- a/spec/models/lfs_file_lock_spec.rb +++ b/spec/models/lfs_file_lock_spec.rb @@ -13,7 +13,7 @@ describe LfsFileLock do describe '#can_be_unlocked_by?' do let(:developer) { create(:user) } - let(:maintainer) { create(:user) } + let(:maintainer) { create(:user) } before do project = lfs_file_lock.project diff --git a/spec/models/members/project_member_spec.rb b/spec/models/members/project_member_spec.rb index 99d3ab41b97..36bfff2c339 100644 --- a/spec/models/members/project_member_spec.rb +++ b/spec/models/members/project_member_spec.rb @@ -39,7 +39,7 @@ describe ProjectMember do describe "#destroy" do let(:owner) { create(:project_member, access_level: ProjectMember::MAINTAINER) } let(:project) { owner.project } - let(:maintainer) { create(:project_member, project: project) } + let(:maintainer) { create(:project_member, project: project) } it "creates an expired event when left due to expiry" do expired = create(:project_member, project: project, expires_at: Time.now - 6.days) diff --git a/spec/models/merge_request_diff_commit_spec.rb b/spec/models/merge_request_diff_commit_spec.rb index 8c01a7ac18f..10487190a44 100644 --- a/spec/models/merge_request_diff_commit_spec.rb +++ b/spec/models/merge_request_diff_commit_spec.rb @@ -16,7 +16,7 @@ describe MergeRequestDiffCommit do end describe '.create_bulk' do - let(:sha_attribute) { Gitlab::Database::ShaAttribute.new } + let(:sha_attribute) { Gitlab::Database::ShaAttribute.new } let(:merge_request_diff_id) { merge_request.merge_request_diff.id } let(:commits) do [ diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index bfc9035cb56..b62f973ad1e 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -1032,7 +1032,7 @@ describe MergeRequest do end describe '#diverged_commits_count' do - let(:project) { create(:project, :repository) } + let(:project) { create(:project, :repository) } let(:forked_project) { fork_project(project, nil, repository: true) } context 'when the target branch does not exist anymore' do @@ -2298,9 +2298,9 @@ describe MergeRequest do end describe "#source_project_missing?" do - let(:project) { create(:project) } + let(:project) { create(:project) } let(:forked_project) { fork_project(project) } - let(:user) { create(:user) } + let(:user) { create(:user) } let(:unlink_project) { Projects::UnlinkForkService.new(forked_project, user) } context "when the fork exists" do @@ -2370,9 +2370,9 @@ describe MergeRequest do end describe "#closed_without_fork?" do - let(:project) { create(:project) } + let(:project) { create(:project) } let(:forked_project) { fork_project(project) } - let(:user) { create(:user) } + let(:user) { create(:user) } let(:unlink_project) { Projects::UnlinkForkService.new(forked_project, user) } context "when the merge request is closed" do @@ -2494,7 +2494,7 @@ describe MergeRequest do expect(merge_request.mergeable_with_quick_action?(user, last_diff_sha: mr_sha)).to be_falsey end - context 'closed MR' do + context 'closed MR' do before do merge_request.update_attribute(:state, :closed) end @@ -2504,7 +2504,7 @@ describe MergeRequest do end end - context 'MR with WIP' do + context 'MR with WIP' do before do merge_request.update_attribute(:title, 'WIP: some MR') end @@ -2514,19 +2514,19 @@ describe MergeRequest do end end - context 'sha differs from the MR diff_head_sha' do + context 'sha differs from the MR diff_head_sha' do it 'is not mergeable' do expect(merge_request.mergeable_with_quick_action?(developer, last_diff_sha: 'some other sha')).to be_falsey end end - context 'sha is not provided' do + context 'sha is not provided' do it 'is not mergeable' do expect(merge_request.mergeable_with_quick_action?(developer)).to be_falsey end end - context 'with pipeline ok' do + context 'with pipeline ok' do before do create_pipeline(:success) end @@ -2536,7 +2536,7 @@ describe MergeRequest do end end - context 'with failing pipeline' do + context 'with failing pipeline' do before do create_pipeline(:failed) end @@ -2546,7 +2546,7 @@ describe MergeRequest do end end - context 'with running pipeline' do + context 'with running pipeline' do before do create_pipeline(:running) end diff --git a/spec/models/milestone_spec.rb b/spec/models/milestone_spec.rb index 015db4d4e96..af7e3d3a6c9 100644 --- a/spec/models/milestone_spec.rb +++ b/spec/models/milestone_spec.rb @@ -240,6 +240,29 @@ describe Milestone do end end + describe '#search_title' do + let(:milestone) { create(:milestone, title: 'foo', description: 'bar') } + + it 'returns milestones with a matching title' do + expect(described_class.search_title(milestone.title)) .to eq([milestone]) + end + + it 'returns milestones with a partially matching title' do + expect(described_class.search_title(milestone.title[0..2])).to eq([milestone]) + end + + it 'returns milestones with a matching title regardless of the casing' do + expect(described_class.search_title(milestone.title.upcase)) + .to eq([milestone]) + end + + it 'searches only on the title and ignores milestones with a matching description' do + create(:milestone, title: 'bar', description: 'foo') + + expect(described_class.search_title(milestone.title)) .to eq([milestone]) + end + end + describe '#for_projects_and_groups' do let(:project) { create(:project) } let(:project_other) { create(:project) } @@ -286,8 +309,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_services/bamboo_service_spec.rb b/spec/models/project_services/bamboo_service_spec.rb index ee84fa95f0e..b880d90d28f 100644 --- a/spec/models/project_services/bamboo_service_spec.rb +++ b/spec/models/project_services/bamboo_service_spec.rb @@ -144,7 +144,7 @@ describe BambooService, :use_clean_rails_memory_store_caching do end end - describe '#calculate_reactive_cache' do + shared_examples 'reactive cache calculation' do context '#build_page' do subject { service.calculate_reactive_cache('123', 'unused')[:build_page] } @@ -155,7 +155,7 @@ describe BambooService, :use_clean_rails_memory_store_caching do end it 'returns a specific URL when response has no results' do - stub_request(body: bamboo_response(size: 0)) + stub_request(body: %q({"results":{"results":{"size":"0"}}})) is_expected.to eq('http://gitlab.com/bamboo/browse/foo') end @@ -224,6 +224,24 @@ describe BambooService, :use_clean_rails_memory_store_caching do end end + describe '#calculate_reactive_cache' do + context 'when Bamboo API returns single result' do + let(:bamboo_response_template) do + %q({"results":{"results":{"size":"1","result":{"buildState":"%{build_state}","planResultKey":{"key":"42"}}}}}) + end + + it_behaves_like 'reactive cache calculation' + end + + context 'when Bamboo API returns an array of results and we only consider the last one' do + let(:bamboo_response_template) do + %q({"results":{"results":{"size":"2","result":[{"buildState":"%{build_state}","planResultKey":{"key":"41"}},{"buildState":"%{build_state}","planResultKey":{"key":"42"}}]}}}) + end + + it_behaves_like 'reactive cache calculation' + end + end + def stub_update_and_build_request(status: 200, body: nil) bamboo_full_url = 'http://gitlab.com/bamboo/updateAndBuild.action?buildKey=foo&os_authType=basic' @@ -244,8 +262,8 @@ describe BambooService, :use_clean_rails_memory_store_caching do ).with(basic_auth: %w(mic password)) end - def bamboo_response(result_key: 42, build_state: 'success', size: 1) + def bamboo_response(build_state: 'success') # reference: https://docs.atlassian.com/atlassian-bamboo/REST/6.2.5/#d2e786 - %Q({"results":{"results":{"size":"#{size}","result":[{"buildState":"#{build_state}","planResultKey":{"key":"#{result_key}"}}]}}}) + bamboo_response_template % { build_state: build_state } end end diff --git a/spec/models/project_services/drone_ci_service_spec.rb b/spec/models/project_services/drone_ci_service_spec.rb index d8972aff407..26597d9b83c 100644 --- a/spec/models/project_services/drone_ci_service_spec.rb +++ b/spec/models/project_services/drone_ci_service_spec.rb @@ -117,7 +117,7 @@ describe DroneCiService, :use_clean_rails_memory_store_caching do describe "execute" do include_context :drone_ci_service - let(:user) { create(:user, username: 'username') } + let(:user) { create(:user, username: 'username') } let(:push_sample_data) do Gitlab::DataBuilder::Push.build_sample(project, user) end diff --git a/spec/models/project_services/external_wiki_service_spec.rb b/spec/models/project_services/external_wiki_service_spec.rb index 25e6ce7e804..62fd97b038b 100644 --- a/spec/models/project_services/external_wiki_service_spec.rb +++ b/spec/models/project_services/external_wiki_service_spec.rb @@ -1,7 +1,6 @@ require 'spec_helper' describe ExternalWikiService do - include ExternalWikiHelper describe "Associations" do it { is_expected.to belong_to :project } it { is_expected.to have_one :service_hook } @@ -25,24 +24,4 @@ describe ExternalWikiService do it { is_expected.not_to validate_presence_of(:external_wiki_url) } end end - - describe 'External wiki' do - let(:project) { create(:project) } - - context 'when it is active' do - before do - properties = { 'external_wiki_url' => 'https://gitlab.com' } - @service = project.create_external_wiki_service(active: true, properties: properties) - end - - after do - @service.destroy! - end - - it 'replaces the wiki url' do - wiki_path = get_project_wiki_path(project) - expect(wiki_path).to match('https://gitlab.com') - end - end - end end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 397b4d7c61f..ae137aa7b78 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 @@ -400,6 +405,30 @@ describe Project do end end + describe '#all_pipelines' do + let(:project) { create(:project) } + + before do + create(:ci_pipeline, project: project, ref: 'master', source: :web) + create(:ci_pipeline, project: project, ref: 'master', source: :external) + end + + it 'has all pipelines' do + expect(project.all_pipelines.size).to eq(2) + end + + context 'when builds are disabled' do + before do + project.project_feature.update_attribute(:builds_access_level, ProjectFeature::DISABLED) + end + + it 'should return .external pipelines' do + expect(project.all_pipelines).to all(have_attributes(source: 'external')) + expect(project.all_pipelines.size).to eq(1) + end + end + end + describe 'project token' do it 'sets an random token if none provided' do project = FactoryBot.create(:project, runners_token: '') @@ -3069,6 +3098,66 @@ describe Project do end end + describe '.with_feature_available_for_user' do + let!(:user) { create(:user) } + let!(:feature) { MergeRequest } + let!(:project) { create(:project, :public, :merge_requests_enabled) } + + subject { described_class.with_feature_available_for_user(feature, user) } + + context 'when user has access to project' do + subject { described_class.with_feature_available_for_user(feature, user) } + + before do + project.add_guest(user) + end + + context 'when public project' do + context 'when feature is public' do + it 'returns project' do + is_expected.to include(project) + end + end + + context 'when feature is private' do + let!(:project) { create(:project, :public, :merge_requests_private) } + + it 'returns project when user has access to the feature' do + project.add_maintainer(user) + + is_expected.to include(project) + end + + it 'does not return project when user does not have the minimum access level required' do + is_expected.not_to include(project) + end + end + end + + context 'when private project' do + let!(:project) { create(:project) } + + it 'returns project when user has access to the feature' do + project.add_maintainer(user) + + is_expected.to include(project) + end + + it 'does not return project when user does not have the minimum access level required' do + is_expected.not_to include(project) + end + end + end + + context 'when user does not have access to project' do + let!(:project) { create(:project) } + + it 'does not return project when user cant access project' do + is_expected.not_to include(project) + end + end + end + describe '#pages_available?' do let(:project) { create(:project, group: group) } @@ -3087,7 +3176,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 @@ -3219,7 +3308,7 @@ describe Project do end context 'legacy storage' do - let(:project) { create(:project, :repository, :legacy_storage) } + set(:project) { create(:project, :repository, :legacy_storage) } let(:gitlab_shell) { Gitlab::Shell.new } let(:project_storage) { project.send(:storage) } @@ -3274,13 +3363,14 @@ describe Project do end describe '#migrate_to_hashed_storage!' do + let(:project) { create(:project, :empty_repo, :legacy_storage) } + it 'returns true' do expect(project.migrate_to_hashed_storage!).to be_truthy end - it 'does not validate project visibility' do - expect(project).not_to receive(:visibility_level_allowed_as_fork) - expect(project).not_to receive(:visibility_level_allowed_by_group) + it 'does not run validation' do + expect(project).not_to receive(:valid?) project.migrate_to_hashed_storage! end @@ -3310,7 +3400,7 @@ describe Project do end context 'hashed storage' do - let(:project) { create(:project, :repository, skip_disk_validation: true) } + set(:project) { create(:project, :repository, skip_disk_validation: true) } let(:gitlab_shell) { Gitlab::Shell.new } let(:hash) { Digest::SHA2.hexdigest(project.id.to_s) } let(:hashed_prefix) { File.join('@hashed', hash[0..1], hash[2..3]) } @@ -3367,6 +3457,8 @@ describe Project do end describe '#migrate_to_hashed_storage!' do + let(:project) { create(:project, :repository, skip_disk_validation: true) } + it 'returns nil' do expect(project.migrate_to_hashed_storage!).to be_nil end @@ -3376,10 +3468,12 @@ describe Project do end context 'when partially migrated' do - it 'returns true' do + it 'enqueues a job' do project = create(:project, storage_version: 1, skip_disk_validation: true) - expect(project.migrate_to_hashed_storage!).to be_truthy + Sidekiq::Testing.fake! do + expect { project.migrate_to_hashed_storage! }.to change(ProjectMigrateHashedStorageWorker.jobs, :size).by(1) + end end end end @@ -3757,6 +3851,7 @@ describe Project do expect(import_state).to receive(:remove_jid) expect(project).to receive(:after_create_default_branch) expect(project).to receive(:refresh_markdown_cache!) + expect(InternalId).to receive(:flush_records!).with(project: project) project.after_import end @@ -3921,7 +4016,7 @@ describe Project do describe '#badges' do let(:project_group) { create(:group) } - let(:project) { create(:project, path: 'avatar', namespace: project_group) } + let(:project) { create(:project, path: 'avatar', namespace: project_group) } before do create_list(:project_badge, 2, project: project) @@ -4431,6 +4526,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/project_team_spec.rb b/spec/models/project_team_spec.rb index c4af17f4726..3537dead5d1 100644 --- a/spec/models/project_team_spec.rb +++ b/spec/models/project_team_spec.rb @@ -178,6 +178,21 @@ describe ProjectTeam do end end + describe '#members_in_project_and_ancestors' do + context 'group project' do + it 'filters out users who are not members of the project' do + group = create(:group) + project = create(:project, group: group) + group_member = create(:group_member, group: group) + old_user = create(:user) + + ProjectAuthorization.create!(project: project, user: old_user, access_level: Gitlab::Access::GUEST) + + expect(project.team.members_in_project_and_ancestors).to contain_exactly(group_member.user) + end + end + end + describe "#human_max_access" do it 'returns Maintainer role' do user = create(:user) 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/models/resource_label_event_spec.rb b/spec/models/resource_label_event_spec.rb index e7e3f7376e6..3797960ac3d 100644 --- a/spec/models/resource_label_event_spec.rb +++ b/spec/models/resource_label_event_spec.rb @@ -88,7 +88,7 @@ RSpec.describe ResourceLabelEvent, type: :model do end it 'returns false if label and reference are set' do - subject.attributes = { reference: 'whatever', cached_markdown_version: CacheMarkdownField::CACHE_COMMONMARK_VERSION } + subject.attributes = { reference: 'whatever', cached_markdown_version: CacheMarkdownField::CACHE_COMMONMARK_VERSION } expect(subject.outdated_markdown?).to be false end diff --git a/spec/models/sent_notification_spec.rb b/spec/models/sent_notification_spec.rb index 5ec04b99957..677613b7980 100644 --- a/spec/models/sent_notification_spec.rb +++ b/spec/models/sent_notification_spec.rb @@ -48,7 +48,7 @@ describe SentNotification do let(:note) { create(:diff_note_on_merge_request) } it 'creates a new SentNotification' do - expect { described_class.record_note(note, user.id) }.to change { described_class.count }.by(1) + expect { described_class.record_note(note, note.author.id) }.to change { described_class.count }.by(1) end end diff --git a/spec/models/user_preference_spec.rb b/spec/models/user_preference_spec.rb index 2898613545c..b2ef17a81d4 100644 --- a/spec/models/user_preference_spec.rb +++ b/spec/models/user_preference_spec.rb @@ -3,9 +3,10 @@ require 'spec_helper' describe UserPreference do + let(:user_preference) { create(:user_preference) } + describe '#set_notes_filter' do let(:issuable) { build_stubbed(:issue) } - let(:user_preference) { create(:user_preference) } shared_examples 'setting system notes' do it 'returns updated discussion filter' do @@ -50,4 +51,26 @@ describe UserPreference do end end end + + describe 'sort_by preferences' do + shared_examples_for 'a sort_by preference' do + it 'allows nil sort fields' do + user_preference.update(attribute => nil) + + expect(user_preference).to be_valid + end + end + + context 'merge_requests_sort attribute' do + let(:attribute) { :merge_requests_sort } + + it_behaves_like 'a sort_by preference' + end + + context 'issues_sort attribute' do + let(:attribute) { :issues_sort } + + it_behaves_like 'a sort_by preference' + end + end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 33842e74b92..78477ab0a5a 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -1997,6 +1997,33 @@ describe User do expect(subject).to include(accessible) expect(subject).not_to include(other) end + + context 'with min_access_level' do + let!(:user) { create(:user) } + let!(:project) { create(:project, :private, namespace: user.namespace) } + + before do + project.add_developer(user) + end + + subject { Project.where("EXISTS (?)", user.authorizations_for_projects(min_access_level: min_access_level)) } + + context 'when developer access' do + let(:min_access_level) { Gitlab::Access::DEVELOPER } + + it 'includes projects a user has access to' do + expect(subject).to include(project) + end + end + + context 'when owner access' do + let(:min_access_level) { Gitlab::Access::OWNER } + + it 'does not include projects with higher access level' do + expect(subject).not_to include(project) + end + end + end end describe '#authorized_projects', :delete do diff --git a/spec/policies/ci/pipeline_policy_spec.rb b/spec/policies/ci/pipeline_policy_spec.rb index 8022f61e67d..844d96017de 100644 --- a/spec/policies/ci/pipeline_policy_spec.rb +++ b/spec/policies/ci/pipeline_policy_spec.rb @@ -75,6 +75,14 @@ describe Ci::PipelinePolicy, :models do end end + context 'when user does not have access to internal CI' do + let(:project) { create(:project, :builds_disabled, :public) } + + it 'disallows the user from reading the pipeline' do + expect(policy).to be_disallowed :read_pipeline + end + end + describe 'destroy_pipeline' do let(:project) { create(:project, :public) } diff --git a/spec/policies/note_policy_spec.rb b/spec/policies/note_policy_spec.rb index 7e25c53e77c..0e848c74659 100644 --- a/spec/policies/note_policy_spec.rb +++ b/spec/policies/note_policy_spec.rb @@ -28,6 +28,7 @@ describe NotePolicy, mdoels: true do expect(policy).to be_disallowed(:admin_note) expect(policy).to be_disallowed(:resolve_note) expect(policy).to be_disallowed(:read_note) + expect(policy).to be_disallowed(:award_emoji) end end @@ -40,6 +41,7 @@ describe NotePolicy, mdoels: true do expect(policy).to be_allowed(:admin_note) expect(policy).to be_allowed(:resolve_note) expect(policy).to be_allowed(:read_note) + expect(policy).to be_allowed(:award_emoji) end end end diff --git a/spec/policies/personal_snippet_policy_spec.rb b/spec/policies/personal_snippet_policy_spec.rb index 3809692b373..a38e0dbd797 100644 --- a/spec/policies/personal_snippet_policy_spec.rb +++ b/spec/policies/personal_snippet_policy_spec.rb @@ -14,6 +14,13 @@ describe PersonalSnippetPolicy do ] end + let(:comment_permissions) do + [ + :comment_personal_snippet, + :create_note + ] + end + def permissions(user) described_class.new(user, snippet) end @@ -26,7 +33,7 @@ describe PersonalSnippetPolicy do it do is_expected.to be_allowed(:read_personal_snippet) - is_expected.to be_disallowed(:comment_personal_snippet) + is_expected.to be_disallowed(*comment_permissions) is_expected.to be_disallowed(:award_emoji) is_expected.to be_disallowed(*author_permissions) end @@ -37,7 +44,7 @@ describe PersonalSnippetPolicy do it do is_expected.to be_allowed(:read_personal_snippet) - is_expected.to be_allowed(:comment_personal_snippet) + is_expected.to be_allowed(*comment_permissions) is_expected.to be_allowed(:award_emoji) is_expected.to be_disallowed(*author_permissions) end @@ -48,7 +55,7 @@ describe PersonalSnippetPolicy do it do is_expected.to be_allowed(:read_personal_snippet) - is_expected.to be_allowed(:comment_personal_snippet) + is_expected.to be_allowed(*comment_permissions) is_expected.to be_allowed(:award_emoji) is_expected.to be_allowed(*author_permissions) end @@ -63,7 +70,7 @@ describe PersonalSnippetPolicy do it do is_expected.to be_disallowed(:read_personal_snippet) - is_expected.to be_disallowed(:comment_personal_snippet) + is_expected.to be_disallowed(*comment_permissions) is_expected.to be_disallowed(:award_emoji) is_expected.to be_disallowed(*author_permissions) end @@ -74,7 +81,7 @@ describe PersonalSnippetPolicy do it do is_expected.to be_allowed(:read_personal_snippet) - is_expected.to be_allowed(:comment_personal_snippet) + is_expected.to be_allowed(*comment_permissions) is_expected.to be_allowed(:award_emoji) is_expected.to be_disallowed(*author_permissions) end @@ -85,7 +92,7 @@ describe PersonalSnippetPolicy do it do is_expected.to be_disallowed(:read_personal_snippet) - is_expected.to be_disallowed(:comment_personal_snippet) + is_expected.to be_disallowed(*comment_permissions) is_expected.to be_disallowed(:award_emoji) is_expected.to be_disallowed(*author_permissions) end @@ -96,7 +103,7 @@ describe PersonalSnippetPolicy do it do is_expected.to be_allowed(:read_personal_snippet) - is_expected.to be_allowed(:comment_personal_snippet) + is_expected.to be_allowed(*comment_permissions) is_expected.to be_allowed(:award_emoji) is_expected.to be_allowed(*author_permissions) end @@ -111,7 +118,7 @@ describe PersonalSnippetPolicy do it do is_expected.to be_disallowed(:read_personal_snippet) - is_expected.to be_disallowed(:comment_personal_snippet) + is_expected.to be_disallowed(*comment_permissions) is_expected.to be_disallowed(:award_emoji) is_expected.to be_disallowed(*author_permissions) end @@ -122,6 +129,17 @@ describe PersonalSnippetPolicy do it do is_expected.to be_disallowed(:read_personal_snippet) + is_expected.to be_disallowed(*comment_permissions) + is_expected.to be_disallowed(:award_emoji) + is_expected.to be_disallowed(*author_permissions) + end + end + + context 'admin user' do + subject { permissions(admin_user) } + + it do + is_expected.to be_allowed(:read_personal_snippet) is_expected.to be_disallowed(:comment_personal_snippet) is_expected.to be_disallowed(:award_emoji) is_expected.to be_disallowed(*author_permissions) @@ -133,7 +151,7 @@ describe PersonalSnippetPolicy do it do is_expected.to be_disallowed(:read_personal_snippet) - is_expected.to be_disallowed(:comment_personal_snippet) + is_expected.to be_disallowed(*comment_permissions) is_expected.to be_disallowed(:award_emoji) is_expected.to be_disallowed(*author_permissions) end @@ -144,7 +162,7 @@ describe PersonalSnippetPolicy do it do is_expected.to be_allowed(:read_personal_snippet) - is_expected.to be_allowed(:comment_personal_snippet) + is_expected.to be_allowed(*comment_permissions) is_expected.to be_allowed(:award_emoji) is_expected.to be_allowed(*author_permissions) end diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb index 2a4030de998..93a468f585b 100644 --- a/spec/policies/project_policy_spec.rb +++ b/spec/policies/project_policy_spec.rb @@ -12,7 +12,7 @@ describe ProjectPolicy do let(:base_guest_permissions) do %i[ read_project read_board read_list read_wiki read_issue - read_project_for_iids read_issue_iid read_merge_request_iid read_label + read_project_for_iids read_issue_iid read_label read_milestone read_project_snippet read_project_member read_note create_project create_issue create_note upload_file create_merge_request_in award_emoji read_release @@ -69,7 +69,7 @@ describe ProjectPolicy do end # Used in EE specs - let(:additional_guest_permissions) { [] } + let(:additional_guest_permissions) { [] } let(:additional_reporter_permissions) { [] } let(:additional_maintainer_permissions) { [] } @@ -102,15 +102,27 @@ describe ProjectPolicy do expect(Ability).not_to be_allowed(user, :read_issue, project) end - context 'when the feature is disabled' do + context 'wiki feature' do + let(:permissions) { %i(read_wiki create_wiki update_wiki admin_wiki download_wiki_code) } + subject { described_class.new(owner, project) } - before do - project.project_feature.update_attribute(:wiki_access_level, ProjectFeature::DISABLED) - end + context 'when the feature is disabled' do + before do + project.project_feature.update_attribute(:wiki_access_level, ProjectFeature::DISABLED) + end - it 'does not include the wiki permissions' do - expect_disallowed :read_wiki, :create_wiki, :update_wiki, :admin_wiki, :download_wiki_code + it 'does not include the wiki permissions' do + expect_disallowed(*permissions) + end + + context 'when there is an external wiki' do + it 'does not include the wiki permissions' do + allow(project).to receive(:has_external_wiki?).and_return(true) + + expect_disallowed(*permissions) + end + end end end @@ -152,22 +164,52 @@ describe ProjectPolicy do end end + context 'for a guest in a private project' do + let(:project) { create(:project, :private) } + subject { described_class.new(guest, project) } + + it 'disallows the guest from reading the merge request and merge request iid' do + expect_disallowed(:read_merge_request) + expect_disallowed(:read_merge_request_iid) + end + end + context 'builds feature' do - subject { described_class.new(owner, project) } + context 'when builds are disabled' do + subject { described_class.new(owner, project) } - it 'disallows all permissions when the feature is disabled' do - project.project_feature.update(builds_access_level: ProjectFeature::DISABLED) + before do + project.project_feature.update(builds_access_level: ProjectFeature::DISABLED) + end - builds_permissions = [ - :create_pipeline, :update_pipeline, :admin_pipeline, :destroy_pipeline, - :create_build, :read_build, :update_build, :admin_build, :destroy_build, - :create_pipeline_schedule, :read_pipeline_schedule, :update_pipeline_schedule, :admin_pipeline_schedule, :destroy_pipeline_schedule, - :create_environment, :read_environment, :update_environment, :admin_environment, :destroy_environment, - :create_cluster, :read_cluster, :update_cluster, :admin_cluster, - :create_deployment, :read_deployment, :update_deployment, :admin_deployment, :destroy_deployment - ] + it 'disallows all permissions except pipeline when the feature is disabled' do + builds_permissions = [ + :create_build, :read_build, :update_build, :admin_build, :destroy_build, + :create_pipeline_schedule, :read_pipeline_schedule, :update_pipeline_schedule, :admin_pipeline_schedule, :destroy_pipeline_schedule, + :create_environment, :read_environment, :update_environment, :admin_environment, :destroy_environment, + :create_cluster, :read_cluster, :update_cluster, :admin_cluster, :destroy_cluster, + :create_deployment, :read_deployment, :update_deployment, :admin_deployment, :destroy_deployment + ] - expect_disallowed(*builds_permissions) + expect_disallowed(*builds_permissions) + end + end + + context 'when builds are disabled only for some users' do + subject { described_class.new(guest, project) } + + before do + project.project_feature.update(builds_access_level: ProjectFeature::PRIVATE) + end + + it 'disallows pipeline and commit_status permissions' do + builds_permissions = [ + :create_pipeline, :update_pipeline, :admin_pipeline, :destroy_pipeline, + :create_commit_status, :update_commit_status, :admin_commit_status, :destroy_commit_status + ] + + expect_disallowed(*builds_permissions) + end end end @@ -236,7 +278,7 @@ describe ProjectPolicy do let(:group) { create(:group, :public) } let(:project) { create(:project, :public, namespace: group) } let(:user_permissions) { [:create_merge_request_in, :create_project, :create_issue, :create_note, :upload_file, :award_emoji] } - let(:anonymous_permissions) { guest_permissions - user_permissions } + let(:anonymous_permissions) { guest_permissions - user_permissions } subject { described_class.new(nil, project) } diff --git a/spec/policies/project_snippet_policy_spec.rb b/spec/policies/project_snippet_policy_spec.rb index 4d32e06b553..d6329e84579 100644 --- a/spec/policies/project_snippet_policy_spec.rb +++ b/spec/policies/project_snippet_policy_spec.rb @@ -41,7 +41,7 @@ describe ProjectSnippetPolicy do subject { abilities(regular_user, :public) } it do - expect_allowed(:read_project_snippet) + expect_allowed(:read_project_snippet, :create_note) expect_disallowed(*author_permissions) end end @@ -50,7 +50,7 @@ describe ProjectSnippetPolicy do subject { abilities(external_user, :public) } it do - expect_allowed(:read_project_snippet) + expect_allowed(:read_project_snippet, :create_note) expect_disallowed(*author_permissions) end end @@ -70,7 +70,7 @@ describe ProjectSnippetPolicy do subject { abilities(regular_user, :internal) } it do - expect_allowed(:read_project_snippet) + expect_allowed(:read_project_snippet, :create_note) expect_disallowed(*author_permissions) end end @@ -79,7 +79,7 @@ describe ProjectSnippetPolicy do subject { abilities(external_user, :internal) } it do - expect_disallowed(:read_project_snippet) + expect_disallowed(:read_project_snippet, :create_note) expect_disallowed(*author_permissions) end end @@ -92,7 +92,7 @@ describe ProjectSnippetPolicy do end it do - expect_allowed(:read_project_snippet) + expect_allowed(:read_project_snippet, :create_note) expect_disallowed(*author_permissions) end end @@ -112,7 +112,7 @@ describe ProjectSnippetPolicy do subject { abilities(regular_user, :private) } it do - expect_disallowed(:read_project_snippet) + expect_disallowed(:read_project_snippet, :create_note) expect_disallowed(*author_permissions) end end @@ -123,7 +123,7 @@ describe ProjectSnippetPolicy do subject { described_class.new(regular_user, snippet) } it do - expect_allowed(:read_project_snippet) + expect_allowed(:read_project_snippet, :create_note) expect_allowed(*author_permissions) end end @@ -136,7 +136,7 @@ describe ProjectSnippetPolicy do end it do - expect_allowed(:read_project_snippet) + expect_allowed(:read_project_snippet, :create_note) expect_disallowed(*author_permissions) end end @@ -149,7 +149,7 @@ describe ProjectSnippetPolicy do end it do - expect_allowed(:read_project_snippet) + expect_allowed(:read_project_snippet, :create_note) expect_disallowed(*author_permissions) end end @@ -158,7 +158,7 @@ describe ProjectSnippetPolicy do subject { abilities(create(:admin), :private) } it do - expect_allowed(:read_project_snippet) + expect_allowed(:read_project_snippet, :create_note) expect_allowed(*author_permissions) end end diff --git a/spec/presenters/ci/trigger_presenter_spec.rb b/spec/presenters/ci/trigger_presenter_spec.rb new file mode 100644 index 00000000000..231b539c188 --- /dev/null +++ b/spec/presenters/ci/trigger_presenter_spec.rb @@ -0,0 +1,51 @@ +require 'spec_helper' + +describe Ci::TriggerPresenter do + set(:user) { create(:user) } + set(:project) { create(:project) } + + set(:trigger) do + create(:ci_trigger, token: '123456789abcd', project: project) + end + + subject do + described_class.new(trigger, current_user: user) + end + + before do + project.add_maintainer(user) + end + + context 'when user is not a trigger owner' do + describe '#token' do + it 'exposes only short token' do + expect(subject.token).not_to eq trigger.token + expect(subject.token).to eq '1234' + end + end + + describe '#has_token_exposed?' do + it 'does not have token exposed' do + expect(subject).not_to have_token_exposed + end + end + end + + context 'when user is a trigger owner and builds admin' do + before do + trigger.update(owner: user) + end + + describe '#token' do + it 'exposes full token' do + expect(subject.token).to eq trigger.token + end + end + + describe '#has_token_exposed?' do + it 'has token exposed' do + expect(subject).to have_token_exposed + end + end + end +end diff --git a/spec/presenters/commit_presenter_spec.rb b/spec/presenters/commit_presenter_spec.rb new file mode 100644 index 00000000000..4a0d3a28c32 --- /dev/null +++ b/spec/presenters/commit_presenter_spec.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe CommitPresenter do + let(:project) { create(:project, :repository) } + let(:commit) { project.commit } + let(:user) { create(:user) } + let(:presenter) { described_class.new(commit, current_user: user) } + + describe '#status_for' do + subject { presenter.status_for('ref') } + + context 'when user can read_commit_status' do + before do + allow(presenter).to receive(:can?).with(user, :read_commit_status, project).and_return(true) + end + + it 'returns commit status for ref' do + expect(commit).to receive(:status).with('ref').and_return('test') + + expect(subject).to eq('test') + end + end + + context 'when user can not read_commit_status' do + it 'is false' do + is_expected.to eq(false) + end + end + end + + describe '#any_pipelines?' do + subject { presenter.any_pipelines? } + + context 'when user can read pipeline' do + before do + allow(presenter).to receive(:can?).with(user, :read_pipeline, project).and_return(true) + end + + it 'returns if there are any pipelines for commit' do + expect(commit).to receive_message_chain(:pipelines, :any?).and_return(true) + + expect(subject).to eq(true) + end + end + + context 'when user can not read pipeline' do + it 'is false' do + is_expected.to eq(false) + end + end + end +end diff --git a/spec/presenters/projects/settings/deploy_keys_presenter_spec.rb b/spec/presenters/projects/settings/deploy_keys_presenter_spec.rb index 2cc0076d695..001545bb5df 100644 --- a/spec/presenters/projects/settings/deploy_keys_presenter_spec.rb +++ b/spec/presenters/projects/settings/deploy_keys_presenter_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe Projects::Settings::DeployKeysPresenter do let(:project) { create(:project) } let(:user) { create(:user) } - let(:deploy_key) { create(:deploy_key, public: true) } + let(:deploy_key) { create(:deploy_key, public: true) } let!(:deploy_keys_project) do create(:deploy_keys_project, project: project, deploy_key: deploy_key) diff --git a/spec/requests/api/avatar_spec.rb b/spec/requests/api/avatar_spec.rb index 17e66725dc9..9bc49bd5982 100644 --- a/spec/requests/api/avatar_spec.rb +++ b/spec/requests/api/avatar_spec.rb @@ -65,7 +65,7 @@ describe API::Avatar do expect(GravatarService).to receive(:new).and_return(gravatar_service) expect(gravatar_service).to( receive(:execute) - .with('private@example.com', nil, 2, { username: nil }) + .with('private@example.com', nil, 2, { username: nil }) .and_return('https://gravatar')) end diff --git a/spec/requests/api/award_emoji_spec.rb b/spec/requests/api/award_emoji_spec.rb index 22f6fcdc922..6c67d84b59b 100644 --- a/spec/requests/api/award_emoji_spec.rb +++ b/spec/requests/api/award_emoji_spec.rb @@ -7,7 +7,7 @@ describe API::AwardEmoji do set(:award_emoji) { create(:award_emoji, awardable: issue, user: user) } let!(:merge_request) { create(:merge_request, source_project: project, target_project: project) } let!(:downvote) { create(:award_emoji, :downvote, awardable: merge_request, user: user) } - set(:note) { create(:note, project: project, noteable: issue) } + set(:note) { create(:note, project: project, noteable: issue) } before do project.add_maintainer(user) @@ -144,7 +144,7 @@ describe API::AwardEmoji do end describe "POST /projects/:id/awardable/:awardable_id/award_emoji" do - let(:issue2) { create(:issue, project: project, author: user) } + let(:issue2) { create(:issue, project: project, author: user) } context "on an issue" do it "creates a new award emoji" do diff --git a/spec/requests/api/boards_spec.rb b/spec/requests/api/boards_spec.rb index ab4f42cad47..de79e8c4c5c 100644 --- a/spec/requests/api/boards_spec.rb +++ b/spec/requests/api/boards_spec.rb @@ -31,7 +31,7 @@ describe API::Boards do set(:board_label) { create(:label, project: board_parent) } set(:board) { create(:board, project: board_parent, lists: [dev_list, test_list]) } - it_behaves_like 'group and project boards', "/projects/:id/boards" + it_behaves_like 'group and project boards', "/projects/:id/boards" describe "POST /projects/:id/boards/lists" do let(:url) { "/projects/#{board_parent.id}/boards/#{board.id}/lists" } diff --git a/spec/requests/api/container_registry_spec.rb b/spec/requests/api/container_registry_spec.rb new file mode 100644 index 00000000000..ea035a8be4a --- /dev/null +++ b/spec/requests/api/container_registry_spec.rb @@ -0,0 +1,224 @@ +require 'spec_helper' + +describe API::ContainerRegistry do + set(:project) { create(:project, :private) } + set(:maintainer) { create(:user) } + set(:developer) { create(:user) } + set(:reporter) { create(:user) } + set(:guest) { create(:user) } + + let(:root_repository) { create(:container_repository, :root, project: project) } + let(:test_repository) { create(:container_repository, project: project) } + + let(:api_user) { maintainer } + + before do + project.add_maintainer(maintainer) + project.add_developer(developer) + project.add_reporter(reporter) + project.add_guest(guest) + + stub_feature_flags(container_registry_api: true) + stub_container_registry_config(enabled: true) + + root_repository + test_repository + end + + shared_examples 'being disallowed' do |param| + context "for #{param}" do + let(:api_user) { public_send(param) } + + it 'returns access denied' do + subject + + expect(response).to have_gitlab_http_status(:forbidden) + end + end + + context "for anonymous" do + let(:api_user) { nil } + + it 'returns not found' do + subject + + expect(response).to have_gitlab_http_status(:not_found) + end + end + end + + describe 'GET /projects/:id/registry/repositories' do + subject { get api("/projects/#{project.id}/registry/repositories", api_user) } + + it_behaves_like 'being disallowed', :guest + + context 'for reporter' do + let(:api_user) { reporter } + + it 'returns a list of repositories' do + subject + + expect(json_response.length).to eq(2) + expect(json_response.map { |repository| repository['id'] }).to contain_exactly( + root_repository.id, test_repository.id) + end + + it 'returns a matching schema' do + subject + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to match_response_schema('registry/repositories') + end + end + end + + describe 'DELETE /projects/:id/registry/repositories/:repository_id' do + subject { delete api("/projects/#{project.id}/registry/repositories/#{root_repository.id}", api_user) } + + it_behaves_like 'being disallowed', :developer + + context 'for maintainer' do + let(:api_user) { maintainer } + + it 'schedules removal of repository' do + expect(DeleteContainerRepositoryWorker).to receive(:perform_async) + .with(maintainer.id, root_repository.id) + + subject + + expect(response).to have_gitlab_http_status(:accepted) + end + end + end + + describe 'GET /projects/:id/registry/repositories/:repository_id/tags' do + subject { get api("/projects/#{project.id}/registry/repositories/#{root_repository.id}/tags", api_user) } + + it_behaves_like 'being disallowed', :guest + + context 'for reporter' do + let(:api_user) { reporter } + + before do + stub_container_registry_tags(repository: root_repository.path, tags: %w(rootA latest)) + end + + it 'returns a list of tags' do + subject + + expect(json_response.length).to eq(2) + expect(json_response.map { |repository| repository['name'] }).to eq %w(latest rootA) + end + + it 'returns a matching schema' do + subject + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to match_response_schema('registry/tags') + end + end + end + + describe 'DELETE /projects/:id/registry/repositories/:repository_id/tags' do + subject { delete api("/projects/#{project.id}/registry/repositories/#{root_repository.id}/tags", api_user), params: params } + + it_behaves_like 'being disallowed', :developer do + let(:params) do + { name_regex: 'v10.*' } + end + end + + context 'for maintainer' do + let(:api_user) { maintainer } + + context 'without required parameters' do + let(:params) { } + + it 'returns bad request' do + subject + + expect(response).to have_gitlab_http_status(:bad_request) + end + end + + context 'passes all declared parameters' do + let(:params) do + { name_regex: 'v10.*', + keep_n: 100, + older_than: '1 day', + other: 'some value' } + end + + let(:worker_params) do + { name_regex: 'v10.*', + keep_n: 100, + older_than: '1 day' } + end + + it 'schedules cleanup of tags repository' do + expect(CleanupContainerRepositoryWorker).to receive(:perform_async) + .with(maintainer.id, root_repository.id, worker_params) + + subject + + expect(response).to have_gitlab_http_status(:accepted) + end + end + end + end + + describe 'GET /projects/:id/registry/repositories/:repository_id/tags/:tag_name' do + subject { get api("/projects/#{project.id}/registry/repositories/#{root_repository.id}/tags/rootA", api_user) } + + it_behaves_like 'being disallowed', :guest + + context 'for reporter' do + let(:api_user) { reporter } + + before do + stub_container_registry_tags(repository: root_repository.path, tags: %w(rootA), with_manifest: true) + end + + it 'returns a details of tag' do + subject + + expect(json_response).to include( + 'name' => 'rootA', + 'digest' => 'sha256:4c8e63ca4cb663ce6c688cb06f1c372b088dac5b6d7ad7d49cd620d85cf72a15', + 'revision' => 'd7a513a663c1a6dcdba9ed832ca53c02ac2af0c333322cd6ca92936d1d9917ac', + 'total_size' => 2319870) + end + + it 'returns a matching schema' do + subject + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to match_response_schema('registry/tag') + end + end + end + + describe 'DELETE /projects/:id/registry/repositories/:repository_id/tags/:tag_name' do + subject { delete api("/projects/#{project.id}/registry/repositories/#{root_repository.id}/tags/rootA", api_user) } + + it_behaves_like 'being disallowed', :developer + + context 'for maintainer' do + let(:api_user) { maintainer } + + before do + stub_container_registry_tags(repository: root_repository.path, tags: %w(rootA), with_manifest: true) + end + + it 'properly removes tag' do + expect_any_instance_of(ContainerRegistry::Client) + .to receive(:delete_repository_tag).with(root_repository.path, + 'sha256:4c8e63ca4cb663ce6c688cb06f1c372b088dac5b6d7ad7d49cd620d85cf72a15') + + subject + + expect(response).to have_gitlab_http_status(:ok) + end + end + end +end diff --git a/spec/requests/api/discussions_spec.rb b/spec/requests/api/discussions_spec.rb index ef34192f888..35c448d187d 100644 --- a/spec/requests/api/discussions_spec.rb +++ b/spec/requests/api/discussions_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe API::Discussions do let(:user) { create(:user) } let!(:project) { create(:project, :public, :repository, namespace: user.namespace) } - let(:private_user) { create(:user) } + let(:private_user) { create(:user) } before do project.add_developer(user) 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/group_milestones_spec.rb b/spec/requests/api/group_milestones_spec.rb index 108721c6655..6980eb7f55d 100644 --- a/spec/requests/api/group_milestones_spec.rb +++ b/spec/requests/api/group_milestones_spec.rb @@ -8,7 +8,7 @@ describe API::GroupMilestones do let!(:closed_milestone) { create(:closed_milestone, group: group, title: 'version1', description: 'closed milestone') } let!(:milestone) { create(:milestone, group: group, title: 'version2', description: 'open milestone') } - it_behaves_like 'group and project milestones', "/groups/:id/milestones" do + it_behaves_like 'group and project milestones', "/groups/:id/milestones" do let(:route) { "/groups/#{group.id}/milestones" } end 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/internal_spec.rb b/spec/requests/api/internal_spec.rb index 0fe63e2e517..6a943b5237a 100644 --- a/spec/requests/api/internal_spec.rb +++ b/spec/requests/api/internal_spec.rb @@ -32,7 +32,7 @@ describe API::Internal do context 'broadcast message exists' do let!(:broadcast_message) { create(:broadcast_message, starts_at: 1.day.ago, ends_at: 1.day.from_now ) } - it 'returns one broadcast message' do + it 'returns one broadcast message' do get api('/internal/broadcast_message'), params: { secret_token: secret_token } expect(response).to have_gitlab_http_status(200) @@ -41,7 +41,7 @@ describe API::Internal do end context 'broadcast message does not exist' do - it 'returns nothing' do + it 'returns nothing' do get api('/internal/broadcast_message'), params: { secret_token: secret_token } expect(response).to have_gitlab_http_status(200) @@ -867,7 +867,7 @@ describe API::Internal do context 'broadcast message exists' do let!(:broadcast_message) { create(:broadcast_message, starts_at: 1.day.ago, ends_at: 1.day.from_now ) } - it 'returns one broadcast message' do + it 'returns one broadcast message' do post api("/internal/post_receive"), params: valid_params expect(response).to have_gitlab_http_status(200) @@ -876,7 +876,7 @@ describe API::Internal do end context 'broadcast message does not exist' do - it 'returns empty string' do + it 'returns empty string' do post api("/internal/post_receive"), params: valid_params expect(response).to have_gitlab_http_status(200) diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb index ba7930f6c9d..e0f1e303e96 100644 --- a/spec/requests/api/issues_spec.rb +++ b/spec/requests/api/issues_spec.rb @@ -993,7 +993,7 @@ describe API::Issues do end context 'user does not have permissions to create issue' do - let(:not_member) { create(:user) } + let(:not_member) { create(:user) } before do project.project_feature.update(issues_access_level: ProjectFeature::PRIVATE) diff --git a/spec/requests/api/markdown_spec.rb b/spec/requests/api/markdown_spec.rb index e82ef002d32..0cf5c5677b9 100644 --- a/spec/requests/api/markdown_spec.rb +++ b/spec/requests/api/markdown_spec.rb @@ -7,6 +7,8 @@ describe API::Markdown do let(:user) {} # No-op. It gets overwritten in the contexts below. before do + stub_commonmark_sourcepos_disabled + post api("/markdown", user), params: params end diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index 79c0a1953dc..4d42bc39ac3 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -10,7 +10,7 @@ describe API::MergeRequests do let!(:project) { create(:project, :public, :repository, creator: user, namespace: user.namespace, only_allow_merge_if_pipeline_succeeds: false) } let(:milestone) { create(:milestone, title: '1.0.0', project: project) } let(:pipeline) { create(:ci_empty_pipeline) } - let(:milestone1) { create(:milestone, title: '0.9', project: project) } + let(:milestone1) { create(:milestone, title: '0.9', project: project) } let!(:merge_request) { create(:merge_request, :simple, milestone: milestone1, author: user, assignee: user, source_project: project, target_project: project, title: "Test", created_at: base_time) } let!(:merge_request_closed) { create(:merge_request, state: "closed", milestone: milestone1, author: user, assignee: user, source_project: project, target_project: project, title: "Closed test", created_at: base_time + 1.second) } let!(:merge_request_merged) { create(:merge_request, state: "merged", author: user, assignee: user, source_project: project, target_project: project, title: "Merged test", created_at: base_time + 2.seconds, merge_commit_sha: '9999999999999999999999999999999999999999') } @@ -698,7 +698,7 @@ describe API::MergeRequests do let!(:user2) { create(:user) } let(:project) { create(:project, :public, :repository) } let!(:forked_project) { fork_project(project, user2, repository: true) } - let!(:unrelated_project) { create(:project, namespace: create(:user).namespace, creator_id: user2.id) } + let!(:unrelated_project) { create(:project, namespace: create(:user).namespace, creator_id: user2.id) } before do forked_project.add_reporter(user2) diff --git a/spec/requests/api/notes_spec.rb b/spec/requests/api/notes_spec.rb index 9bf753fe049..424f0a82e43 100644 --- a/spec/requests/api/notes_spec.rb +++ b/spec/requests/api/notes_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe API::Notes do let(:user) { create(:user) } let!(:project) { create(:project, :public, namespace: user.namespace) } - let(:private_user) { create(:user) } + let(:private_user) { create(:user) } before do project.add_reporter(user) @@ -46,7 +46,7 @@ describe API::Notes do create(:project, namespace: private_user.namespace) .tap { |p| p.add_maintainer(private_user) } end - let(:private_issue) { create(:issue, project: private_project) } + let(:private_issue) { create(:issue, project: private_project) } let(:ext_proj) { create(:project, :public) } let(:ext_issue) { create(:issue, project: ext_proj) } diff --git a/spec/requests/api/pages/private_access_spec.rb b/spec/requests/api/pages/private_access_spec.rb index d69c15b0477..c647537038e 100644 --- a/spec/requests/api/pages/private_access_spec.rb +++ b/spec/requests/api/pages/private_access_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe "Private Project Pages Access" do +describe "Private Project Pages Access" do using RSpec::Parameterized::TableSyntax include AccessMatchers diff --git a/spec/requests/api/pages/public_access_spec.rb b/spec/requests/api/pages/public_access_spec.rb index 882ca26ac51..16cc5697f30 100644 --- a/spec/requests/api/pages/public_access_spec.rb +++ b/spec/requests/api/pages/public_access_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe "Public Project Pages Access" do +describe "Public Project Pages Access" do using RSpec::Parameterized::TableSyntax include AccessMatchers diff --git a/spec/requests/api/project_milestones_spec.rb b/spec/requests/api/project_milestones_spec.rb index 0fa13dd71e2..49b5dfb0b33 100644 --- a/spec/requests/api/project_milestones_spec.rb +++ b/spec/requests/api/project_milestones_spec.rb @@ -10,7 +10,7 @@ describe API::ProjectMilestones do project.add_developer(user) end - it_behaves_like 'group and project milestones', "/projects/:id/milestones" do + it_behaves_like 'group and project milestones', "/projects/:id/milestones" do let(:route) { "/projects/#{project.id}/milestones" } end diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index 05b73a77f1a..70686158b7d 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -957,6 +957,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) @@ -976,6 +977,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 @@ -1154,6 +1156,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/resource_label_events_spec.rb b/spec/requests/api/resource_label_events_spec.rb index b7d4a5152cc..37b46eaeb86 100644 --- a/spec/requests/api/resource_label_events_spec.rb +++ b/spec/requests/api/resource_label_events_spec.rb @@ -5,7 +5,7 @@ require 'spec_helper' describe API::ResourceLabelEvents do set(:user) { create(:user) } set(:project) { create(:project, :public, :repository, namespace: user.namespace) } - set(:private_user) { create(:user) } + set(:private_user) { create(:user) } before do project.add_developer(user) diff --git a/spec/requests/api/runner_spec.rb b/spec/requests/api/runner_spec.rb index ec48bf60426..ed0108c846a 100644 --- a/spec/requests/api/runner_spec.rb +++ b/spec/requests/api/runner_spec.rb @@ -210,8 +210,8 @@ describe API::Runner, :clean_gitlab_redis_shared_state do it "sets the runner's ip_address" do post api('/runners'), - params: { token: registration_token }, - headers: { 'REMOTE_ADDR' => '123.111.123.111' } + params: { token: registration_token }, + headers: { 'X-Forwarded-For' => '123.111.123.111' } expect(response).to have_gitlab_http_status 201 expect(Ci::Runner.first.ip_address).to eq('123.111.123.111') @@ -520,7 +520,7 @@ describe API::Runner, :clean_gitlab_redis_shared_state do it "sets the runner's ip_address" do post api('/jobs/request'), params: { token: runner.token }, - headers: { 'User-Agent' => user_agent, 'REMOTE_ADDR' => '123.222.123.222' } + headers: { 'User-Agent' => user_agent, 'X-Forwarded-For' => '123.222.123.222' } expect(response).to have_gitlab_http_status 201 expect(runner.reload.ip_address).to eq('123.222.123.222') 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/triggers_spec.rb b/spec/requests/api/triggers_spec.rb index 15dc901d06e..f0f01e97f1d 100644 --- a/spec/requests/api/triggers_spec.rb +++ b/spec/requests/api/triggers_spec.rb @@ -1,8 +1,9 @@ require 'spec_helper' describe API::Triggers do - let(:user) { create(:user) } - let(:user2) { create(:user) } + set(:user) { create(:user) } + set(:user2) { create(:user) } + let!(:trigger_token) { 'secure_token' } let!(:trigger_token_2) { 'secure_token_2' } let!(:project) { create(:project, :repository, creator: user) } @@ -132,14 +133,17 @@ describe API::Triggers do end describe 'GET /projects/:id/triggers' do - context 'authenticated user with valid permissions' do - it 'returns list of triggers' do + context 'authenticated user who can access triggers' do + it 'returns a list of triggers with tokens exposed correctly' do get api("/projects/#{project.id}/triggers", user) expect(response).to have_gitlab_http_status(200) expect(response).to include_pagination_headers + expect(json_response).to be_a(Array) - expect(json_response[0]).to have_key('token') + expect(json_response.size).to eq 2 + expect(json_response.dig(0, 'token')).to eq trigger_token + expect(json_response.dig(1, 'token')).to eq trigger_token_2[0..3] end end diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index 1cf0f4f2cf7..b381431306d 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -1785,7 +1785,7 @@ describe API::Users do end describe 'POST /users/:id/unblock' do - let(:blocked_user) { create(:user, state: 'blocked') } + let(:blocked_user) { create(:user, state: 'blocked') } before do admin 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_http_spec.rb b/spec/requests/lfs_http_spec.rb index f1514e90eb2..1781759c54b 100644 --- a/spec/requests/lfs_http_spec.rb +++ b/spec/requests/lfs_http_spec.rb @@ -1086,6 +1086,12 @@ describe 'Git LFS API and storage' do end end + context 'and request to finalize the upload is not sent by gitlab-workhorse' do + it 'fails with a JWT decode error' do + expect { put_finalize(lfs_tmp_file, verified: false) }.to raise_error(JWT::DecodeError) + end + end + context 'and workhorse requests upload finalize for a new lfs object' do before do lfs_object.destroy @@ -1347,9 +1353,13 @@ describe 'Git LFS API and storage' do context 'when pushing the same lfs object to the second project' do before do + finalize_headers = headers + .merge('X-Gitlab-Lfs-Tmp' => lfs_tmp_file) + .merge(workhorse_internal_api_request_header) + put "#{second_project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}/#{sample_size}", params: {}, - headers: headers.merge('X-Gitlab-Lfs-Tmp' => lfs_tmp_file).compact + headers: finalize_headers end it 'responds with status 200' do @@ -1370,7 +1380,7 @@ describe 'Git LFS API and storage' do put "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}/#{sample_size}/authorize", params: {}, headers: authorize_headers end - def put_finalize(lfs_tmp = lfs_tmp_file, with_tempfile: false, args: {}) + def put_finalize(lfs_tmp = lfs_tmp_file, with_tempfile: false, verified: true, args: {}) upload_path = LfsObjectUploader.workhorse_local_upload_path file_path = upload_path + '/' + lfs_tmp if lfs_tmp @@ -1384,11 +1394,14 @@ describe 'Git LFS API and storage' do 'file.name' => File.basename(file_path) } - put_finalize_with_args(args.merge(extra_args).compact) + put_finalize_with_args(args.merge(extra_args).compact, verified: verified) end - def put_finalize_with_args(args) - put "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}/#{sample_size}", params: args, headers: headers + def put_finalize_with_args(args, verified:) + finalize_headers = headers + finalize_headers.merge!(workhorse_internal_api_request_header) if verified + + put "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}/#{sample_size}", params: args, headers: finalize_headers end def lfs_tmp_file diff --git a/spec/requests/lfs_locks_api_spec.rb b/spec/requests/lfs_locks_api_spec.rb index 28cb90e450e..5b7b3d2fdd6 100644 --- a/spec/requests/lfs_locks_api_spec.rb +++ b/spec/requests/lfs_locks_api_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe 'Git LFS File Locking API' do include WorkhorseHelpers - let(:project) { create(:project) } + let(:project) { create(:project) } let(:maintainer) { create(:user) } let(:developer) { create(:user) } let(:guest) { create(:user) } @@ -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 @@ -149,7 +160,7 @@ describe 'Git LFS File Locking API' do post(url, params: body.try(:to_json), headers: (headers || {}).merge('Content-Type' => LfsRequest::CONTENT_TYPE)) end - def do_get(url, params = nil, headers = nil) + def do_get(url, params = nil, headers = nil) get(url, params: (params || {}), headers: (headers || {}).merge('Content-Type' => LfsRequest::CONTENT_TYPE)) end diff --git a/spec/requests/projects/cycle_analytics_events_spec.rb b/spec/requests/projects/cycle_analytics_events_spec.rb index bcc3e3a2678..49412b628b3 100644 --- a/spec/requests/projects/cycle_analytics_events_spec.rb +++ b/spec/requests/projects/cycle_analytics_events_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe 'cycle analytics events' do let(:user) { create(:user) } let(:project) { create(:project, :repository, public_builds: false) } - let(:issue) { create(:issue, project: project, created_at: 2.days.ago) } + let(:issue) { create(:issue, project: project, created_at: 2.days.ago) } describe 'GET /:namespace/:project/cycle_analytics/events/issues' do before do diff --git a/spec/requests/request_profiler_spec.rb b/spec/requests/request_profiler_spec.rb index 284a51fcc32..75b22b1879b 100644 --- a/spec/requests/request_profiler_spec.rb +++ b/spec/requests/request_profiler_spec.rb @@ -18,7 +18,7 @@ describe 'Request Profiler' do path = "/#{project.full_path}" Timecop.freeze(time) do - get path, params: {}, headers: { 'X-Profile-Token' => Gitlab::RequestProfiler.profile_token } + get path, params: {}, headers: { 'X-Profile-Token' => Gitlab::RequestProfiler.profile_token } end profile_path = "#{Gitlab.config.shared.path}/tmp/requests_profiles/#{path.tr('/', '|')}_#{time.to_i}.html" 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/serializers/cluster_application_entity_spec.rb b/spec/serializers/cluster_application_entity_spec.rb index 852b6af9f7f..88d16a5b360 100644 --- a/spec/serializers/cluster_application_entity_spec.rb +++ b/spec/serializers/cluster_application_entity_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' describe ClusterApplicationEntity do describe '#as_json' do - let(:application) { build(:clusters_applications_helm) } + let(:application) { build(:clusters_applications_helm, version: '0.1.1') } subject { described_class.new(application).as_json } it 'has name' do @@ -13,6 +13,10 @@ describe ClusterApplicationEntity do expect(subject[:status]).to eq(:not_installable) end + it 'has version' do + expect(subject[:version]).to eq('0.1.1') + end + it 'has no status_reason' do expect(subject[:status_reason]).to be_nil end diff --git a/spec/serializers/container_repository_entity_spec.rb b/spec/serializers/container_repository_entity_spec.rb index c589cd18f77..15466bce514 100644 --- a/spec/serializers/container_repository_entity_spec.rb +++ b/spec/serializers/container_repository_entity_spec.rb @@ -19,7 +19,7 @@ describe ContainerRepositoryEntity do allow(request).to receive(:current_user).and_return(user) end - it 'exposes required informations' do + it 'exposes required informations' do expect(subject).to include(:id, :path, :location, :tags_path) end diff --git a/spec/serializers/container_tag_entity_spec.rb b/spec/serializers/container_tag_entity_spec.rb index 4beb50c70f8..ceb828a1cc5 100644 --- a/spec/serializers/container_tag_entity_spec.rb +++ b/spec/serializers/container_tag_entity_spec.rb @@ -16,7 +16,7 @@ describe ContainerTagEntity do before do stub_container_registry_config(enabled: true) - stub_container_registry_tags(repository: /image/, tags: %w[test]) + stub_container_registry_tags(repository: /image/, tags: %w[test], with_manifest: true) allow(request).to receive(:project).and_return(project) allow(request).to receive(:current_user).and_return(user) end diff --git a/spec/serializers/deployment_entity_spec.rb b/spec/serializers/deployment_entity_spec.rb index 8793a762f9d..cfa5414b40f 100644 --- a/spec/serializers/deployment_entity_spec.rb +++ b/spec/serializers/deployment_entity_spec.rb @@ -11,7 +11,7 @@ describe DeploymentEntity do allow(request).to receive(:current_user).and_return(user) end - it 'exposes internal deployment id' do + it 'exposes internal deployment id' do expect(subject).to include(:iid) end diff --git a/spec/serializers/group_child_entity_spec.rb b/spec/serializers/group_child_entity_spec.rb index dbc40bddc30..d02b4c554b1 100644 --- a/spec/serializers/group_child_entity_spec.rb +++ b/spec/serializers/group_child_entity_spec.rb @@ -10,6 +10,7 @@ describe GroupChildEntity do before do allow(request).to receive(:current_user).and_return(user) + stub_commonmark_sourcepos_disabled end shared_examples 'group child json' do diff --git a/spec/serializers/merge_request_widget_entity_spec.rb b/spec/serializers/merge_request_widget_entity_spec.rb index 561421d5ac8..376698a16df 100644 --- a/spec/serializers/merge_request_widget_entity_spec.rb +++ b/spec/serializers/merge_request_widget_entity_spec.rb @@ -31,23 +31,40 @@ describe MergeRequestWidgetEntity do describe 'pipeline' do let(:pipeline) { create(:ci_empty_pipeline, project: project, ref: resource.source_branch, sha: resource.source_branch_sha, head_pipeline_of: resource) } - context 'when is up to date' do - let(:req) { double('request', current_user: user, project: project) } + before do + allow_any_instance_of(MergeRequestPresenter).to receive(:can?).and_call_original + allow_any_instance_of(MergeRequestPresenter).to receive(:can?).with(user, :read_pipeline, anything).and_return(result) + end - it 'returns pipeline' do - pipeline_payload = PipelineDetailsEntity - .represent(pipeline, request: req) - .as_json + context 'when user has access to pipelines' do + let(:result) { true } + + context 'when is up to date' do + let(:req) { double('request', current_user: user, project: project) } + + it 'returns pipeline' do + pipeline_payload = PipelineDetailsEntity + .represent(pipeline, request: req) + .as_json + + expect(subject[:pipeline]).to eq(pipeline_payload) + end + end + + context 'when is not up to date' do + it 'returns nil' do + pipeline.update(sha: "not up to date") - expect(subject[:pipeline]).to eq(pipeline_payload) + expect(subject[:pipeline]).to eq(nil) + end end end - context 'when is not up to date' do - it 'returns nil' do - pipeline.update(sha: "not up to date") + context 'when user does not have access to pipelines' do + let(:result) { false } - expect(subject[:pipeline]).to be_nil + it 'does not have pipeline' do + expect(subject[:pipeline]).to eq(nil) end end end diff --git a/spec/services/application_settings/update_service_spec.rb b/spec/services/application_settings/update_service_spec.rb index daf5dfba6b1..a4a733eff77 100644 --- a/spec/services/application_settings/update_service_spec.rb +++ b/spec/services/application_settings/update_service_spec.rb @@ -17,7 +17,7 @@ describe ApplicationSettings::UpdateService do describe 'updating terms' do context 'when the passed terms are blank' do - let(:params) { { terms: '' } } + let(:params) { { terms: '' } } it 'does not create terms' do expect { subject.execute }.not_to change { ApplicationSetting::Term.count } @@ -25,7 +25,7 @@ describe ApplicationSettings::UpdateService do end context 'when passing terms' do - let(:params) { { terms: 'Be nice! ' } } + let(:params) { { terms: 'Be nice! ' } } it 'creates the terms' do expect { subject.execute }.to change { ApplicationSetting::Term.count }.by(1) diff --git a/spec/services/boards/issues/list_service_spec.rb b/spec/services/boards/issues/list_service_spec.rb index 010679b5360..aaad29536af 100644 --- a/spec/services/boards/issues/list_service_spec.rb +++ b/spec/services/boards/issues/list_service_spec.rb @@ -12,7 +12,7 @@ describe Boards::Issues::ListService do let(:bug) { create(:label, project: project, name: 'Bug') } let(:development) { create(:label, project: project, name: 'Development') } - let(:testing) { create(:label, project: project, name: 'Testing') } + let(:testing) { create(:label, project: project, name: 'Testing') } let(:p1) { create(:label, title: 'P1', project: project, priority: 1) } let(:p2) { create(:label, title: 'P2', project: project, priority: 2) } let(:p3) { create(:label, title: 'P3', project: project, priority: 3) } @@ -63,7 +63,7 @@ describe Boards::Issues::ListService do let(:bug) { create(:group_label, group: group, name: 'Bug') } let(:development) { create(:group_label, group: group, name: 'Development') } - let(:testing) { create(:group_label, group: group, name: 'Testing') } + let(:testing) { create(:group_label, group: group, name: 'Testing') } let(:p1) { create(:group_label, title: 'P1', group: group) } let(:p2) { create(:group_label, title: 'P2', group: group) } diff --git a/spec/services/boards/issues/move_service_spec.rb b/spec/services/boards/issues/move_service_spec.rb index dd0ad5f11bd..6020f0771e5 100644 --- a/spec/services/boards/issues/move_service_spec.rb +++ b/spec/services/boards/issues/move_service_spec.rb @@ -10,7 +10,7 @@ describe Boards::Issues::MoveService do let(:bug) { create(:label, project: project, name: 'Bug') } let(:development) { create(:label, project: project, name: 'Development') } - let(:testing) { create(:label, project: project, name: 'Testing') } + let(:testing) { create(:label, project: project, name: 'Testing') } let(:regression) { create(:label, project: project, name: 'Regression') } let!(:list1) { create(:list, board: board1, label: development, position: 0) } @@ -35,7 +35,7 @@ describe Boards::Issues::MoveService do let(:bug) { create(:group_label, group: group, name: 'Bug') } let(:development) { create(:group_label, group: group, name: 'Development') } - let(:testing) { create(:group_label, group: group, name: 'Testing') } + let(:testing) { create(:group_label, group: group, name: 'Testing') } let(:regression) { create(:group_label, group: group, name: 'Regression') } let!(:list1) { create(:list, board: board1, label: development, position: 0) } diff --git a/spec/services/ci/destroy_expired_job_artifacts_service_spec.rb b/spec/services/ci/destroy_expired_job_artifacts_service_spec.rb new file mode 100644 index 00000000000..80d82ba3ac9 --- /dev/null +++ b/spec/services/ci/destroy_expired_job_artifacts_service_spec.rb @@ -0,0 +1,104 @@ +require 'spec_helper' + +describe Ci::DestroyExpiredJobArtifactsService, :clean_gitlab_redis_shared_state do + include ExclusiveLeaseHelpers + + describe '.execute' do + subject { service.execute } + + let(:service) { described_class.new } + let!(:artifact) { create(:ci_job_artifact, expire_at: 1.day.ago) } + + it 'destroys expired job artifacts' do + expect { subject }.to change { Ci::JobArtifact.count }.by(-1) + end + + context 'when artifact is not expired' do + let!(:artifact) { create(:ci_job_artifact, expire_at: 1.day.since) } + + it 'does not destroy expired job artifacts' do + expect { subject }.not_to change { Ci::JobArtifact.count } + end + end + + context 'when artifact is permanent' do + let!(:artifact) { create(:ci_job_artifact, expire_at: nil) } + + it 'does not destroy expired job artifacts' do + expect { subject }.not_to change { Ci::JobArtifact.count } + end + end + + context 'when failed to destroy artifact' do + before do + stub_const('Ci::DestroyExpiredJobArtifactsService::LOOP_LIMIT', 10) + + allow_any_instance_of(Ci::JobArtifact) + .to receive(:destroy!) + .and_raise(ActiveRecord::RecordNotDestroyed) + end + + it 'raises an exception and stop destroying' do + expect { subject }.to raise_error(ActiveRecord::RecordNotDestroyed) + end + end + + context 'when exclusive lease has already been taken by the other instance' do + before do + stub_exclusive_lease_taken(described_class::EXCLUSIVE_LOCK_KEY, timeout: described_class::LOCK_TIMEOUT) + end + + it 'raises an error and does not start destroying' do + expect { subject }.to raise_error(Gitlab::ExclusiveLeaseHelpers::FailedToObtainLockError) + end + end + + context 'when timeout happens' do + before do + stub_const('Ci::DestroyExpiredJobArtifactsService::LOOP_TIMEOUT', 1.second) + allow_any_instance_of(described_class).to receive(:destroy_batch) { true } + end + + it 'returns false and does not continue destroying' do + is_expected.to be_falsy + end + end + + context 'when loop reached loop limit' do + before do + stub_const('Ci::DestroyExpiredJobArtifactsService::LOOP_LIMIT', 1) + stub_const('Ci::DestroyExpiredJobArtifactsService::BATCH_SIZE', 1) + end + + let!(:artifact) { create_list(:ci_job_artifact, 2, expire_at: 1.day.ago) } + + it 'raises an error and does not continue destroying' do + is_expected.to be_falsy + end + + it 'destroys one artifact' do + expect { subject }.to change { Ci::JobArtifact.count }.by(-1) + end + end + + context 'when there are no artifacts' do + let!(:artifact) { } + + it 'does not raise error' do + expect { subject }.not_to raise_error + end + end + + context 'when there are artifacts more than batch sizes' do + before do + stub_const('Ci::DestroyExpiredJobArtifactsService::BATCH_SIZE', 1) + end + + let!(:artifact) { create_list(:ci_job_artifact, 2, expire_at: 1.day.ago) } + + it 'destroys all expired artifacts' do + expect { subject }.to change { Ci::JobArtifact.count }.by(-2) + end + end + end +end diff --git a/spec/services/event_create_service_spec.rb b/spec/services/event_create_service_spec.rb index 68e310b0506..443665c9959 100644 --- a/spec/services/event_create_service_spec.rb +++ b/spec/services/event_create_service_spec.rb @@ -144,7 +144,7 @@ describe EventCreateService do it 'updates user last activity' do expect { service.push(project, user, push_data) } - .to change { user.last_activity_on }.to(Date.today) + .to change { user.last_activity_on }.to(Date.today) end it 'caches the last push event for the user' do diff --git a/spec/services/git_push_service_spec.rb b/spec/services/git_push_service_spec.rb index 45ef26aebbd..e8fce951155 100644 --- a/spec/services/git_push_service_spec.rb +++ b/spec/services/git_push_service_spec.rb @@ -15,7 +15,7 @@ describe GitPushService, services: true do end describe 'with remote mirrors' do - let(:project) { create(:project, :repository, :remote_mirror) } + let(:project) { create(:project, :repository, :remote_mirror) } subject do described_class.new(project, user, oldrev: oldrev, newrev: newrev, ref: ref) @@ -547,7 +547,7 @@ describe GitPushService, services: true do end context "closing an issue" do - let(:message) { "this is some work.\n\ncloses JIRA-1" } + let(:message) { "this is some work.\n\ncloses JIRA-1" } let(:comment_body) do { body: "Issue solved with [#{closing_commit.id}|http://#{Gitlab.config.gitlab.host}/#{project.full_path}/commit/#{closing_commit.id}]." diff --git a/spec/services/issues/create_service_spec.rb b/spec/services/issues/create_service_spec.rb index c61c1ddcb3d..715b1168bfb 100644 --- a/spec/services/issues/create_service_spec.rb +++ b/spec/services/issues/create_service_spec.rb @@ -172,7 +172,7 @@ describe Issues::CreateService do end it 'removes assignee when user id is 0' do - opts = { title: 'Title', description: 'Description', assignee_ids: [0] } + opts = { title: 'Title', description: 'Description', assignee_ids: [0] } issue = described_class.new(project, user, opts).execute diff --git a/spec/services/labels/find_or_create_service_spec.rb b/spec/services/labels/find_or_create_service_spec.rb index 97ba2742392..7af514a5bea 100644 --- a/spec/services/labels/find_or_create_service_spec.rb +++ b/spec/services/labels/find_or_create_service_spec.rb @@ -46,7 +46,7 @@ describe Labels::FindOrCreateService do end context 'when include_ancestor_groups is true' do - let(:group) { create(:group, :nested) } + let(:group) { create(:group, :nested) } let(:params) do { title: 'Audit', diff --git a/spec/services/labels/promote_service_spec.rb b/spec/services/labels/promote_service_spec.rb index aa9aba6bdff..c4c7f33e36a 100644 --- a/spec/services/labels/promote_service_spec.rb +++ b/spec/services/labels/promote_service_spec.rb @@ -5,9 +5,9 @@ describe Labels::PromoteService do let!(:user) { create(:user) } context 'project without group' do - let!(:project_1) { create(:project) } + let!(:project_1) { create(:project) } - let!(:project_label_1_1) { create(:label, project: project_1) } + let!(:project_label_1_1) { create(:label, project: project_1) } subject(:service) { described_class.new(project_1, user) } diff --git a/spec/services/lfs/unlock_file_service_spec.rb b/spec/services/lfs/unlock_file_service_spec.rb index 539417644db..fe42ca41633 100644 --- a/spec/services/lfs/unlock_file_service_spec.rb +++ b/spec/services/lfs/unlock_file_service_spec.rb @@ -62,7 +62,7 @@ describe Lfs::UnlockFileService do context 'when forced' do let(:developer) { create(:user) } - let(:maintainer) { create(:user) } + let(:maintainer) { create(:user) } before do project.add_developer(developer) diff --git a/spec/services/members/destroy_service_spec.rb b/spec/services/members/destroy_service_spec.rb index 5aa7165e135..d37ca13ebd2 100644 --- a/spec/services/members/destroy_service_spec.rb +++ b/spec/services/members/destroy_service_spec.rb @@ -69,14 +69,14 @@ describe Members::DestroyService do it 'calls Member#after_decline_request' do expect_any_instance_of(NotificationService).to receive(:decline_access_request).with(member) - described_class.new(current_user).execute(member) + described_class.new(current_user).execute(member, opts) end context 'when current user is the member' do it 'does not call Member#after_decline_request' do expect_any_instance_of(NotificationService).not_to receive(:decline_access_request).with(member) - described_class.new(member_user).execute(member) + described_class.new(member_user).execute(member, opts) end end end @@ -159,7 +159,7 @@ describe Members::DestroyService do end it_behaves_like 'a service destroying a member' do - let(:opts) { { skip_authorization: true } } + let(:opts) { { skip_authorization: true, skip_subresources: true } } let(:member) { group_project.requesters.find_by(user_id: member_user.id) } end @@ -168,12 +168,14 @@ describe Members::DestroyService do end it_behaves_like 'a service destroying a member' do - let(:opts) { { skip_authorization: true } } + let(:opts) { { skip_authorization: true, skip_subresources: true } } let(:member) { group.requesters.find_by(user_id: member_user.id) } end end context 'when current user can destroy the given access requester' do + let(:opts) { { skip_subresources: true } } + before do group_project.add_maintainer(current_user) group.add_owner(current_user) @@ -229,4 +231,54 @@ describe Members::DestroyService do end end end + + context 'subresources' do + let(:user) { create(:user) } + let(:member_user) { create(:user) } + let(:opts) { {} } + + let(:group) { create(:group, :public) } + let(:subgroup) { create(:group, parent: group) } + let(:subsubgroup) { create(:group, parent: subgroup) } + let(:subsubproject) { create(:project, group: subsubgroup) } + + let(:group_project) { create(:project, :public, group: group) } + let(:control_project) { create(:project, group: subsubgroup) } + + before do + create(:group_member, :developer, group: subsubgroup, user: member_user) + + subsubproject.add_developer(member_user) + control_project.add_maintainer(user) + group.add_owner(user) + + group_member = create(:group_member, :developer, group: group, user: member_user) + + described_class.new(user).execute(group_member, opts) + end + + it 'removes the project membership' do + expect(group_project.members.map(&:user)).not_to include(member_user) + end + + it 'removes the group membership' do + expect(group.members.map(&:user)).not_to include(member_user) + end + + it 'removes the subgroup membership', :postgresql do + expect(subgroup.members.map(&:user)).not_to include(member_user) + end + + it 'removes the subsubgroup membership', :postgresql do + expect(subsubgroup.members.map(&:user)).not_to include(member_user) + end + + it 'removes the subsubproject membership', :postgresql do + expect(subsubproject.members.map(&:user)).not_to include(member_user) + end + + it 'does not remove the user from the control project' do + expect(control_project.members.map(&:user)).to include(user) + end + end end diff --git a/spec/services/merge_requests/create_service_spec.rb b/spec/services/merge_requests/create_service_spec.rb index 308f99dc0da..4e64b0c9414 100644 --- a/spec/services/merge_requests/create_service_spec.rb +++ b/spec/services/merge_requests/create_service_spec.rb @@ -301,7 +301,7 @@ describe MergeRequests::CreateService do end it 'removes assignee_id when user id is 0' do - opts = { title: 'Title', description: 'Description', assignee_id: 0 } + opts = { title: 'Title', description: 'Description', assignee_id: 0 } merge_request = described_class.new(project, user, opts).execute diff --git a/spec/services/notes/build_service_spec.rb b/spec/services/notes/build_service_spec.rb index ff85c261cd4..9aaccb4bffe 100644 --- a/spec/services/notes/build_service_spec.rb +++ b/spec/services/notes/build_service_spec.rb @@ -45,6 +45,15 @@ describe Notes::BuildService do end end + context 'when user has no access to discussion' do + it 'sets an error' do + another_user = create(:user) + new_note = described_class.new(project, another_user, note: 'Test', in_reply_to_discussion_id: note.discussion_id).execute + + expect(new_note.errors[:base]).to include('Discussion to reply to cannot be found') + end + end + context 'personal snippet note' do def reply(note, user = nil) user ||= create(:user) diff --git a/spec/services/notes/create_service_spec.rb b/spec/services/notes/create_service_spec.rb index 80b015d4cd0..1b9ba42cfd6 100644 --- a/spec/services/notes/create_service_spec.rb +++ b/spec/services/notes/create_service_spec.rb @@ -127,6 +127,10 @@ describe Notes::CreateService do create(:diff_note_on_merge_request, noteable: merge_request, project: project_with_repo) end + before do + project_with_repo.add_maintainer(user) + end + context 'when eligible to have a note diff file' do let(:new_opts) do opts.merge(in_reply_to_discussion_id: nil, diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb index d20e712d365..6a5a6989607 100644 --- a/spec/services/notification_service_spec.rb +++ b/spec/services/notification_service_spec.rb @@ -1646,6 +1646,23 @@ describe NotificationService, :mailer do should_not_email(@u_guest_custom) should_not_email(@u_disabled) end + + context 'users not having access to the new location' do + it 'does not send email' do + old_user = create(:user) + ProjectAuthorization.create!(project: project, user: old_user, access_level: Gitlab::Access::GUEST) + + build_group(project) + reset_delivered_emails! + + notification.project_was_moved(project, "gitlab/gitlab") + + should_email(@g_watcher) + should_email(@g_global_watcher) + should_email(project.creator) + should_not_email(old_user) + end + end end context 'user with notifications disabled' do @@ -2232,8 +2249,8 @@ describe NotificationService, :mailer do # Users in the project's group but not part of project's team # with different notification settings - def build_group(project) - group = create_nested_group + def build_group(project, visibility: :public) + group = create_nested_group(visibility) project.update(namespace_id: group.id) # Group member: global=disabled, group=watch @@ -2249,10 +2266,10 @@ describe NotificationService, :mailer do # Creates a nested group only if supported # to avoid errors on MySQL - def create_nested_group + def create_nested_group(visibility) if Group.supports_nested_objects? - parent_group = create(:group, :public) - child_group = create(:group, :public, parent: parent_group) + parent_group = create(:group, visibility) + child_group = create(:group, visibility, parent: parent_group) # Parent group member: global=disabled, parent_group=watch, child_group=global @pg_watcher ||= create_user_with_notification(:watch, 'parent_group_watcher', parent_group) @@ -2272,7 +2289,7 @@ describe NotificationService, :mailer do child_group else - create(:group, :public) + create(:group, visibility) end end diff --git a/spec/services/projects/after_rename_service_spec.rb b/spec/services/projects/after_rename_service_spec.rb index 59c08b30f9f..b8055a285f2 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") + + service_execute - described_class.new(project).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 HashedStorage::MigrationService with correct options' do + stub_application_setting(hashed_storage_enabled: true) + + expect_next_instance_of(::Projects::HashedStorage::MigrationService) 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/container_repository/cleanup_tags_service_spec.rb b/spec/services/projects/container_repository/cleanup_tags_service_spec.rb new file mode 100644 index 00000000000..0659130bed2 --- /dev/null +++ b/spec/services/projects/container_repository/cleanup_tags_service_spec.rb @@ -0,0 +1,162 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Projects::ContainerRepository::CleanupTagsService do + set(:user) { create(:user) } + set(:project) { create(:project, :private) } + set(:repository) { create(:container_repository, :root, project: project) } + + let(:service) { described_class.new(project, user, params) } + + before do + project.add_maintainer(user) + + stub_feature_flags(container_registry_cleanup: true) + + stub_container_registry_config(enabled: true) + + stub_container_registry_tags( + repository: repository.path, + tags: %w(latest A Ba Bb C D E)) + + stub_tag_digest('latest', 'sha256:configA') + stub_tag_digest('A', 'sha256:configA') + stub_tag_digest('Ba', 'sha256:configB') + stub_tag_digest('Bb', 'sha256:configB') + stub_tag_digest('C', 'sha256:configC') + stub_tag_digest('D', 'sha256:configD') + stub_tag_digest('E', nil) + + stub_digest_config('sha256:configA', 1.hour.ago) + stub_digest_config('sha256:configB', 5.days.ago) + stub_digest_config('sha256:configC', 1.month.ago) + stub_digest_config('sha256:configD', nil) + end + + describe '#execute' do + subject { service.execute(repository) } + + context 'when no params are specified' do + let(:params) { {} } + + it 'does not remove anything' do + expect_any_instance_of(ContainerRegistry::Client).not_to receive(:delete_repository_tag) + + is_expected.to include(status: :success, deleted: []) + end + end + + context 'when regex matching everything is specified' do + let(:params) do + { 'name_regex' => '.*' } + end + + it 'does remove B* and C' do + # The :A cannot be removed as config is shared with :latest + # The :E cannot be removed as it does not have valid manifest + + expect_delete('sha256:configB').twice + expect_delete('sha256:configC') + expect_delete('sha256:configD') + + is_expected.to include(status: :success, deleted: %w(D Bb Ba C)) + end + end + + context 'when regex matching specific tags is used' do + let(:params) do + { 'name_regex' => 'C|D' } + end + + it 'does remove C and D' do + expect_delete('sha256:configC') + expect_delete('sha256:configD') + + is_expected.to include(status: :success, deleted: %w(D C)) + end + end + + context 'when removing a tagged image that is used by another tag' do + let(:params) do + { 'name_regex' => 'Ba' } + end + + it 'does not remove the tag' do + # Issue: https://gitlab.com/gitlab-org/gitlab-ce/issues/21405 + + is_expected.to include(status: :success, deleted: []) + end + end + + context 'when removing keeping only 3' do + let(:params) do + { 'name_regex' => '.*', + 'keep_n' => 3 } + end + + it 'does remove C as it is oldest' do + expect_delete('sha256:configC') + + is_expected.to include(status: :success, deleted: %w(C)) + end + end + + context 'when removing older than 1 day' do + let(:params) do + { 'name_regex' => '.*', + 'older_than' => '1 day' } + end + + it 'does remove B* and C as they are older than 1 day' do + expect_delete('sha256:configB').twice + expect_delete('sha256:configC') + + is_expected.to include(status: :success, deleted: %w(Bb Ba C)) + end + end + + context 'when combining all parameters' do + let(:params) do + { 'name_regex' => '.*', + 'keep_n' => 1, + 'older_than' => '1 day' } + end + + it 'does remove B* and C' do + expect_delete('sha256:configB').twice + expect_delete('sha256:configC') + + is_expected.to include(status: :success, deleted: %w(Bb Ba C)) + end + end + end + + private + + def stub_tag_digest(tag, digest) + allow_any_instance_of(ContainerRegistry::Client) + .to receive(:repository_tag_digest) + .with(repository.path, tag) { digest } + + allow_any_instance_of(ContainerRegistry::Client) + .to receive(:repository_manifest) + .with(repository.path, tag) do + { 'config' => { 'digest' => digest } } if digest + end + end + + def stub_digest_config(digest, created_at) + allow_any_instance_of(ContainerRegistry::Client) + .to receive(:blob) + .with(repository.path, digest, nil) do + { 'created' => created_at.to_datetime.rfc3339 }.to_json if created_at + end + end + + def expect_delete(digest) + expect_any_instance_of(ContainerRegistry::Client) + .to receive(:delete_repository_tag) + .with(repository.path, digest) + 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/hashed_storage/migrate_attachments_service_spec.rb b/spec/services/projects/hashed_storage/migrate_attachments_service_spec.rb index 28d8a95fe07..61dbb57ec08 100644 --- a/spec/services/projects/hashed_storage/migrate_attachments_service_spec.rb +++ b/spec/services/projects/hashed_storage/migrate_attachments_service_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe Projects::HashedStorage::MigrateAttachmentsService do subject(:service) { described_class.new(project, project.full_path, logger: nil) } - let(:project) { create(:project, :legacy_storage) } + let(:project) { create(:project, :repository, storage_version: 1, skip_disk_validation: true) } let(:legacy_storage) { Storage::LegacyProject.new(project) } let(:hashed_storage) { Storage::HashedProject.new(project) } @@ -28,6 +28,16 @@ describe Projects::HashedStorage::MigrateAttachmentsService do expect(File.file?(old_disk_path)).to be_falsey expect(File.file?(new_disk_path)).to be_truthy end + + it 'returns true' do + expect(service.execute).to be_truthy + end + + it 'sets skipped to false' do + service.execute + + expect(service.skipped?).to be_falsey + end end context 'when original folder does not exist anymore' do @@ -43,6 +53,16 @@ describe Projects::HashedStorage::MigrateAttachmentsService do expect(File.exist?(base_path(hashed_storage))).to be_falsey expect(File.file?(new_disk_path)).to be_falsey end + + it 'returns true' do + expect(service.execute).to be_truthy + end + + it 'sets skipped to true' do + service.execute + + expect(service.skipped?).to be_truthy + end end context 'when target folder already exists' do @@ -58,6 +78,18 @@ describe Projects::HashedStorage::MigrateAttachmentsService do end end + context '#old_disk_path' do + it 'returns old disk_path for project' do + expect(service.old_disk_path).to eq(project.full_path) + end + end + + context '#new_disk_path' do + it 'returns new disk_path for project' do + expect(service.new_disk_path).to eq(project.disk_path) + end + end + def base_path(storage) File.join(FileUploader.root, storage.disk_path) end diff --git a/spec/services/projects/hashed_storage/migrate_repository_service_spec.rb b/spec/services/projects/hashed_storage/migrate_repository_service_spec.rb index b720f37ffdb..0772dc4b85b 100644 --- a/spec/services/projects/hashed_storage/migrate_repository_service_spec.rb +++ b/spec/services/projects/hashed_storage/migrate_repository_service_spec.rb @@ -8,9 +8,12 @@ describe Projects::HashedStorage::MigrateRepositoryService do let(:legacy_storage) { Storage::LegacyProject.new(project) } let(:hashed_storage) { Storage::HashedProject.new(project) } - subject(:service) { described_class.new(project, project.full_path) } + subject(:service) { described_class.new(project, project.disk_path) } describe '#execute' do + let(:old_disk_path) { legacy_storage.disk_path } + let(:new_disk_path) { hashed_storage.disk_path } + before do allow(service).to receive(:gitlab_shell) { gitlab_shell } end @@ -33,8 +36,8 @@ describe Projects::HashedStorage::MigrateRepositoryService do it 'renames project and wiki repositories' do service.execute - expect(gitlab_shell.exists?(project.repository_storage, "#{hashed_storage.disk_path}.git")).to be_truthy - expect(gitlab_shell.exists?(project.repository_storage, "#{hashed_storage.disk_path}.wiki.git")).to be_truthy + expect(gitlab_shell.exists?(project.repository_storage, "#{new_disk_path}.git")).to be_truthy + expect(gitlab_shell.exists?(project.repository_storage, "#{new_disk_path}.wiki.git")).to be_truthy end it 'updates project to be hashed and not read-only' do @@ -45,8 +48,8 @@ describe Projects::HashedStorage::MigrateRepositoryService do end it 'move operation is called for both repositories' do - expect_move_repository(project.disk_path, hashed_storage.disk_path) - expect_move_repository("#{project.disk_path}.wiki", "#{hashed_storage.disk_path}.wiki") + expect_move_repository(old_disk_path, new_disk_path) + expect_move_repository("#{old_disk_path}.wiki", "#{new_disk_path}.wiki") service.execute end @@ -62,32 +65,27 @@ describe Projects::HashedStorage::MigrateRepositoryService do context 'when one move fails' do it 'rollsback repositories to original name' do - from_name = project.disk_path - to_name = hashed_storage.disk_path allow(service).to receive(:move_repository).and_call_original - allow(service).to receive(:move_repository).with(from_name, to_name).once { false } # will disable first move only + allow(service).to receive(:move_repository).with(old_disk_path, new_disk_path).once { false } # will disable first move only expect(service).to receive(:rollback_folder_move).and_call_original service.execute - expect(gitlab_shell.exists?(project.repository_storage, "#{hashed_storage.disk_path}.git")).to be_falsey - expect(gitlab_shell.exists?(project.repository_storage, "#{hashed_storage.disk_path}.wiki.git")).to be_falsey + expect(gitlab_shell.exists?(project.repository_storage, "#{new_disk_path}.git")).to be_falsey + expect(gitlab_shell.exists?(project.repository_storage, "#{new_disk_path}.wiki.git")).to be_falsey expect(project.repository_read_only?).to be_falsey end context 'when rollback fails' do - let(:from_name) { legacy_storage.disk_path } - let(:to_name) { hashed_storage.disk_path } - before do hashed_storage.ensure_storage_path_exists - gitlab_shell.mv_repository(project.repository_storage, from_name, to_name) + gitlab_shell.mv_repository(project.repository_storage, old_disk_path, new_disk_path) end - it 'does not try to move nil repository over hashed' do - expect(gitlab_shell).not_to receive(:mv_repository).with(project.repository_storage, from_name, to_name) - expect_move_repository("#{project.disk_path}.wiki", "#{hashed_storage.disk_path}.wiki") + it 'does not try to move nil repository over existing' do + expect(gitlab_shell).not_to receive(:mv_repository).with(project.repository_storage, old_disk_path, new_disk_path) + expect_move_repository("#{old_disk_path}.wiki", "#{new_disk_path}.wiki") service.execute end diff --git a/spec/services/projects/hashed_storage_migration_service_spec.rb b/spec/services/projects/hashed_storage/migration_service_spec.rb index 5368c3828dd..b4647586363 100644 --- a/spec/services/projects/hashed_storage_migration_service_spec.rb +++ b/spec/services/projects/hashed_storage/migration_service_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Projects::HashedStorageMigrationService do +describe Projects::HashedStorage::MigrationService do let(:project) { create(:project, :empty_repo, :wiki_repo, :legacy_storage) } let(:logger) { double } diff --git a/spec/services/projects/import_error_filter_spec.rb b/spec/services/projects/import_error_filter_spec.rb new file mode 100644 index 00000000000..312b658de89 --- /dev/null +++ b/spec/services/projects/import_error_filter_spec.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Projects::ImportErrorFilter do + it 'filters any full paths' do + message = 'Error importing into /my/folder Permission denied @ unlink_internal - /var/opt/gitlab/gitlab-rails/shared/a/b/c/uploads/file' + + expect(described_class.filter_message(message)).to eq('Error importing into [FILTERED] Permission denied @ unlink_internal - [FILTERED]') + end + + it 'filters any relative paths ignoring single slash ones' do + message = 'Error importing into my/project Permission denied @ unlink_internal - ../file/ and folder/../file' + + expect(described_class.filter_message(message)).to eq('Error importing into [FILTERED] Permission denied @ unlink_internal - [FILTERED] and [FILTERED]') + end +end diff --git a/spec/services/projects/import_service_spec.rb b/spec/services/projects/import_service_spec.rb index 06f865dc848..7faf0fc2868 100644 --- a/spec/services/projects/import_service_spec.rb +++ b/spec/services/projects/import_service_spec.rb @@ -136,12 +136,12 @@ describe Projects::ImportService do end it 'fails if repository import fails' do - expect_any_instance_of(Gitlab::Shell).to receive(:import_repository).and_raise(Gitlab::Shell::Error.new('Failed to import the repository')) + expect_any_instance_of(Gitlab::Shell).to receive(:import_repository).and_raise(Gitlab::Shell::Error.new('Failed to import the repository /a/b/c')) result = subject.execute expect(result[:status]).to eq :error - expect(result[:message]).to eq "Error importing repository #{project.safe_import_url} into #{project.full_path} - Failed to import the repository" + expect(result[:message]).to eq "Error importing repository #{project.safe_import_url} into #{project.full_path} - Failed to import the repository [FILTERED]" end context 'when repository import scheduled' do @@ -152,8 +152,11 @@ describe Projects::ImportService do it 'downloads lfs objects if lfs_enabled is enabled for project' do allow(project).to receive(:lfs_enabled?).and_return(true) + + service = double expect_any_instance_of(Projects::LfsPointers::LfsImportService).to receive(:execute).and_return(oid_download_links) - expect_any_instance_of(Projects::LfsPointers::LfsDownloadService).to receive(:execute).twice + expect(Projects::LfsPointers::LfsDownloadService).to receive(:new).and_return(service).twice + expect(service).to receive(:execute).twice subject.execute end @@ -211,8 +214,10 @@ describe Projects::ImportService do it 'does not have a custom repository importer downloads lfs objects' do allow(Gitlab::GithubImport::ParallelImporter).to receive(:imports_repository?).and_return(false) + service = double expect_any_instance_of(Projects::LfsPointers::LfsImportService).to receive(:execute).and_return(oid_download_links) - expect_any_instance_of(Projects::LfsPointers::LfsDownloadService).to receive(:execute) + expect(Projects::LfsPointers::LfsDownloadService).to receive(:new).and_return(service).twice + expect(service).to receive(:execute).twice subject.execute end diff --git a/spec/services/projects/lfs_pointers/lfs_download_link_list_service_spec.rb b/spec/services/projects/lfs_pointers/lfs_download_link_list_service_spec.rb index d7a2829d5f8..f222c52199f 100644 --- a/spec/services/projects/lfs_pointers/lfs_download_link_list_service_spec.rb +++ b/spec/services/projects/lfs_pointers/lfs_download_link_list_service_spec.rb @@ -37,8 +37,8 @@ describe Projects::LfsPointers::LfsDownloadLinkListService do describe '#execute' do it 'retrieves each download link of every non existent lfs object' do - subject.execute(new_oids).each do |oid, link| - expect(link).to eq "#{import_url}/gitlab-lfs/objects/#{oid}" + subject.execute(new_oids).each do |lfs_download_object| + expect(lfs_download_object.link).to eq "#{import_url}/gitlab-lfs/objects/#{lfs_download_object.oid}" end end @@ -50,8 +50,8 @@ describe Projects::LfsPointers::LfsDownloadLinkListService do it 'adds credentials to the download_link' do result = subject.execute(new_oids) - result.each do |oid, link| - expect(link.starts_with?('http://user:password@')).to be_truthy + result.each do |lfs_download_object| + expect(lfs_download_object.link.starts_with?('http://user:password@')).to be_truthy end end end @@ -60,8 +60,8 @@ describe Projects::LfsPointers::LfsDownloadLinkListService do it 'does not add any credentials' do result = subject.execute(new_oids) - result.each do |oid, link| - expect(link.starts_with?('http://user:password@')).to be_falsey + result.each do |lfs_download_object| + expect(lfs_download_object.link.starts_with?('http://user:password@')).to be_falsey end end end @@ -74,8 +74,8 @@ describe Projects::LfsPointers::LfsDownloadLinkListService do it 'downloads without any credentials' do result = subject.execute(new_oids) - result.each do |oid, link| - expect(link.starts_with?('http://user:password@')).to be_falsey + result.each do |lfs_download_object| + expect(lfs_download_object.link.starts_with?('http://user:password@')).to be_falsey end end end @@ -92,7 +92,7 @@ describe Projects::LfsPointers::LfsDownloadLinkListService do describe '#parse_response_links' do it 'does not add oid entry if href not found' do - expect(Rails.logger).to receive(:error).with("Link for Lfs Object with oid whatever not found or invalid.") + expect(subject).to receive(:log_error).with("Link for Lfs Object with oid whatever not found or invalid.") result = subject.send(:parse_response_links, invalid_object_response) diff --git a/spec/services/projects/lfs_pointers/lfs_download_service_spec.rb b/spec/services/projects/lfs_pointers/lfs_download_service_spec.rb index 95c9b6e63b8..876beb39801 100644 --- a/spec/services/projects/lfs_pointers/lfs_download_service_spec.rb +++ b/spec/services/projects/lfs_pointers/lfs_download_service_spec.rb @@ -2,68 +2,156 @@ require 'spec_helper' describe Projects::LfsPointers::LfsDownloadService do let(:project) { create(:project) } - let(:oid) { '9e548e25631dd9ce6b43afd6359ab76da2819d6a5b474e66118c7819e1d8b3e8' } - let(:download_link) { "http://gitlab.com/#{oid}" } let(:lfs_content) { SecureRandom.random_bytes(10) } + let(:oid) { Digest::SHA256.hexdigest(lfs_content) } + let(:download_link) { "http://gitlab.com/#{oid}" } + let(:size) { lfs_content.size } + let(:lfs_object) { LfsDownloadObject.new(oid: oid, size: size, link: download_link) } + let(:local_request_setting) { false } - subject { described_class.new(project) } + subject { described_class.new(project, lfs_object) } before do + ApplicationSetting.create_from_defaults + + stub_application_setting(allow_local_requests_from_hooks_and_services: local_request_setting) allow(project).to receive(:lfs_enabled?).and_return(true) - WebMock.stub_request(:get, download_link).to_return(body: lfs_content) + end + + shared_examples 'lfs temporal file is removed' do + it do + subject.execute - allow(Gitlab::CurrentSettings).to receive(:allow_local_requests_from_hooks_and_services?).and_return(false) + expect(File.exist?(subject.send(:tmp_filename))).to be false + end + end + + shared_examples 'no lfs object is created' do + it do + expect { subject.execute }.not_to change { LfsObject.count } + end + + it 'returns error result' do + expect(subject.execute[:status]).to eq :error + end + + it 'an error is logged' do + expect(subject).to receive(:log_error) + + subject.execute + end + + it_behaves_like 'lfs temporal file is removed' + end + + shared_examples 'lfs object is created' do + it do + expect(subject).to receive(:download_and_save_file!).and_call_original + + expect { subject.execute }.to change { LfsObject.count }.by(1) + end + + it 'returns success result' do + expect(subject.execute[:status]).to eq :success + end + + it_behaves_like 'lfs temporal file is removed' end describe '#execute' do context 'when file download succeeds' do - it 'a new lfs object is created' do - expect { subject.execute(oid, download_link) }.to change { LfsObject.count }.from(0).to(1) + before do + WebMock.stub_request(:get, download_link).to_return(body: lfs_content) end + it_behaves_like 'lfs object is created' + it 'has the same oid' do - subject.execute(oid, download_link) + subject.execute expect(LfsObject.first.oid).to eq oid end + it 'has the same size' do + subject.execute + + expect(LfsObject.first.size).to eq size + end + it 'stores the content' do - subject.execute(oid, download_link) + subject.execute expect(File.binread(LfsObject.first.file.file.file)).to eq lfs_content end end context 'when file download fails' do - it 'no lfs object is created' do - expect { subject.execute(oid, download_link) }.to change { LfsObject.count } + before do + allow(Gitlab::HTTP).to receive(:get).and_return(code: 500, 'success?' => false) + end + + it_behaves_like 'no lfs object is created' + + it 'raise StandardError exception' do + expect(subject).to receive(:download_and_save_file!).and_raise(StandardError) + + subject.execute + end + end + + context 'when downloaded lfs file has a different size' do + let(:size) { 1 } + + before do + WebMock.stub_request(:get, download_link).to_return(body: lfs_content) + end + + it_behaves_like 'no lfs object is created' + + it 'raise SizeError exception' do + expect(subject).to receive(:download_and_save_file!).and_raise(described_class::SizeError) + + subject.execute + end + end + + context 'when downloaded lfs file has a different oid' do + before do + WebMock.stub_request(:get, download_link).to_return(body: lfs_content) + allow_any_instance_of(Digest::SHA256).to receive(:hexdigest).and_return('foobar') + end + + it_behaves_like 'no lfs object is created' + + it 'raise OidError exception' do + expect(subject).to receive(:download_and_save_file!).and_raise(described_class::OidError) + + subject.execute end end context 'when credentials present' do let(:download_link_with_credentials) { "http://user:password@gitlab.com/#{oid}" } + let(:lfs_object) { LfsDownloadObject.new(oid: oid, size: size, link: download_link_with_credentials) } before do WebMock.stub_request(:get, download_link).with(headers: { 'Authorization' => 'Basic dXNlcjpwYXNzd29yZA==' }).to_return(body: lfs_content) end it 'the request adds authorization headers' do - subject.execute(oid, download_link_with_credentials) + subject end end context 'when localhost requests are allowed' do let(:download_link) { 'http://192.168.2.120' } + let(:local_request_setting) { true } before do - allow(Gitlab::CurrentSettings).to receive(:allow_local_requests_from_hooks_and_services?).and_return(true) + WebMock.stub_request(:get, download_link).to_return(body: lfs_content) end - it 'downloads the file' do - expect(subject).to receive(:download_and_save_file).and_call_original - - expect { subject.execute(oid, download_link) }.to change { LfsObject.count }.by(1) - end + it_behaves_like 'lfs object is created' end context 'when a bad URL is used' do @@ -71,7 +159,9 @@ describe Projects::LfsPointers::LfsDownloadService do with_them do it 'does not download the file' do - expect { subject.execute(oid, download_link) }.not_to change { LfsObject.count } + expect(subject).not_to receive(:download_lfs_file!) + + expect { subject.execute }.not_to change { LfsObject.count } end end end @@ -85,15 +175,11 @@ describe Projects::LfsPointers::LfsDownloadService do WebMock.stub_request(:get, download_link).to_return(status: 301, headers: { 'Location' => redirect_link }) end - it 'does not follow the redirection' do - expect(Rails.logger).to receive(:error).with(/LFS file with oid #{oid} couldn't be downloaded/) - - expect { subject.execute(oid, download_link) }.not_to change { LfsObject.count } - end + it_behaves_like 'no lfs object is created' end end - context 'that is valid' do + context 'that is not blocked' do let(:redirect_link) { "http://example.com/"} before do @@ -101,21 +187,35 @@ describe Projects::LfsPointers::LfsDownloadService do WebMock.stub_request(:get, redirect_link).to_return(body: lfs_content) end - it 'follows the redirection' do - expect { subject.execute(oid, download_link) }.to change { LfsObject.count }.from(0).to(1) - end + it_behaves_like 'lfs object is created' + end + end + + context 'when the lfs object attributes are invalid' do + let(:oid) { 'foobar' } + + before do + expect(lfs_object).to be_invalid + end + + it_behaves_like 'no lfs object is created' + + it 'does not download the file' do + expect(subject).not_to receive(:download_lfs_file!) + + subject.execute end end - context 'when an lfs object with the same oid already exists' do + context 'when an lfs object with the same oid already exists' do before do - create(:lfs_object, oid: 'oid') + create(:lfs_object, oid: oid) end it 'does not download the file' do - expect(subject).not_to receive(:download_and_save_file) + expect(subject).not_to receive(:download_lfs_file!) - subject.execute('oid', download_link) + subject.execute end end end 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/projects/update_pages_service_spec.rb b/spec/services/projects/update_pages_service_spec.rb index 36b619ba9be..8b70845befe 100644 --- a/spec/services/projects/update_pages_service_spec.rb +++ b/spec/services/projects/update_pages_service_spec.rb @@ -5,24 +5,27 @@ describe Projects::UpdatePagesService do set(:pipeline) { create(:ci_pipeline, project: project, sha: project.commit('HEAD').sha) } set(:build) { create(:ci_build, pipeline: pipeline, ref: 'HEAD') } let(:invalid_file) { fixture_file_upload('spec/fixtures/dk.png') } - let(:extension) { 'zip' } - let(:file) { fixture_file_upload("spec/fixtures/pages.#{extension}") } - let(:empty_file) { fixture_file_upload("spec/fixtures/pages_empty.#{extension}") } - let(:metadata) do - filename = "spec/fixtures/pages.#{extension}.meta" - fixture_file_upload(filename) if File.exist?(filename) - end + let(:file) { fixture_file_upload("spec/fixtures/pages.zip") } + let(:empty_file) { fixture_file_upload("spec/fixtures/pages_empty.zip") } + let(:metadata_filename) { "spec/fixtures/pages.zip.meta" } + let(:metadata) { fixture_file_upload(metadata_filename) if File.exist?(metadata_filename) } subject { described_class.new(project, build) } before do + stub_feature_flags(safezip_use_rubyzip: true) + project.remove_pages end - context 'legacy artifacts' do - let(:extension) { 'zip' } + context '::TMP_EXTRACT_PATH' do + subject { described_class::TMP_EXTRACT_PATH } + it { is_expected.not_to match(Gitlab::PathRegex.namespace_format_regex) } + end + + context 'legacy artifacts' do before do build.update(legacy_artifacts_file: file) build.update(legacy_artifacts_metadata: metadata) @@ -132,6 +135,20 @@ describe Projects::UpdatePagesService do end end + context 'when using pages with non-writeable public' do + let(:file) { fixture_file_upload("spec/fixtures/pages_non_writeable.zip") } + + context 'when using RubyZip' do + before do + stub_feature_flags(safezip_use_rubyzip: true) + end + + it 'succeeds to extract' do + expect(execute).to eq(:success) + end + end + end + context 'when timeout happens by DNS error' do before do allow_any_instance_of(described_class) diff --git a/spec/services/resource_events/change_labels_service_spec.rb b/spec/services/resource_events/change_labels_service_spec.rb index 4c9138fb1ef..070964eb1ec 100644 --- a/spec/services/resource_events/change_labels_service_spec.rb +++ b/spec/services/resource_events/change_labels_service_spec.rb @@ -10,7 +10,7 @@ describe ResourceEvents::ChangeLabelsService do describe '.change_labels' do subject { described_class.new(resource, author).execute(added_labels: added, removed_labels: removed) } - let(:labels) { create_list(:label, 2, project: project) } + let(:labels) { create_list(:label, 2, project: project) } def expect_label_event(event, label, action) expect(event.user).to eq(author) diff --git a/spec/services/resource_events/merge_into_notes_service_spec.rb b/spec/services/resource_events/merge_into_notes_service_spec.rb index c76f6e6f77e..72467091791 100644 --- a/spec/services/resource_events/merge_into_notes_service_spec.rb +++ b/spec/services/resource_events/merge_into_notes_service_spec.rb @@ -16,8 +16,8 @@ describe ResourceEvents::MergeIntoNotesService do create(:note_on_issue, opts.merge(params)) end - set(:project) { create(:project) } - set(:user) { create(:user) } + set(:project) { create(:project) } + set(:user) { create(:user) } set(:resource) { create(:issue, project: project) } set(:label) { create(:label, project: project) } set(:label2) { create(:label, project: project) } @@ -44,7 +44,7 @@ describe ResourceEvents::MergeIntoNotesService do create_event(created_at: time, user: user2) create_event(created_at: 1.day.ago, label: label2) - notes = described_class.new(resource, user).execute() + notes = described_class.new(resource, user).execute expected = [ "added #{label.to_reference} label and removed #{label2.to_reference} label", @@ -61,7 +61,7 @@ describe ResourceEvents::MergeIntoNotesService do event = create_event(created_at: 1.day.ago) notes = described_class.new(resource, user, - last_fetched_at: 2.days.ago.to_i).execute() + last_fetched_at: 2.days.ago.to_i).execute expect(notes.count).to eq 1 expect(notes.first.discussion_id).to eq event.discussion_id diff --git a/spec/services/suggestions/apply_service_spec.rb b/spec/services/suggestions/apply_service_spec.rb index e5ca1c155ed..8e77d582eb4 100644 --- a/spec/services/suggestions/apply_service_spec.rb +++ b/spec/services/suggestions/apply_service_spec.rb @@ -134,12 +134,11 @@ describe Suggestions::ApplyService do end end - context 'when diff ref from position is different from repo diff refs' do + context 'when HEAD from position is different from source branch HEAD on repo' 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 } + allow(suggestion.position).to receive(:head_sha) { 'old-sha' } + allow(suggestion.noteable).to receive(:source_branch_sha) { 'new-sha' } result = subject.execute(suggestion) diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb index 0fbfcb34e50..82544ab0413 100644 --- a/spec/services/system_note_service_spec.rb +++ b/spec/services/system_note_service_spec.rb @@ -819,7 +819,7 @@ describe SystemNoteService do end context 'for issues' do - let(:issue) { create(:issue, project: project) } + let(:issue) { create(:issue, project: project) } it "creates comment" do result = described_class.cross_reference(jira_issue, issue, author) @@ -1131,7 +1131,7 @@ describe SystemNoteService do end context 'across different projects' do - let(:other_project) { create(:project) } + let(:other_project) { create(:project) } let(:canonical_issue) { create(:issue, project: other_project) } it_behaves_like 'a system note' do @@ -1156,7 +1156,7 @@ describe SystemNoteService do end context 'across different projects' do - let(:other_project) { create(:project) } + let(:other_project) { create(:project) } let(:duplicate_issue) { create(:issue, project: other_project) } it_behaves_like 'a system note' do @@ -1168,7 +1168,7 @@ describe SystemNoteService do end describe '.discussion_lock' do - subject { described_class.discussion_lock(noteable, author) } + subject { described_class.discussion_lock(noteable, author) } context 'discussion unlocked' do it_behaves_like 'a system note' do diff --git a/spec/services/todo_service_spec.rb b/spec/services/todo_service_spec.rb index 253f2e44d10..8631f3f9a33 100644 --- a/spec/services/todo_service_spec.rb +++ b/spec/services/todo_service_spec.rb @@ -457,7 +457,7 @@ describe TodoService do end context 'on commit' do - let(:project) { create(:project, :repository) } + let(:project) { create(:project, :repository) } it 'creates a todo for each valid mentioned user when leaving a note on commit' do service.new_note(note_on_commit, john_doe) diff --git a/spec/services/todos/destroy/entity_leave_service_spec.rb b/spec/services/todos/destroy/entity_leave_service_spec.rb index 8cb91e7c1b9..4b238280848 100644 --- a/spec/services/todos/destroy/entity_leave_service_spec.rb +++ b/spec/services/todos/destroy/entity_leave_service_spec.rb @@ -173,10 +173,10 @@ describe Todos::Destroy::EntityLeaveService do let(:subproject) { create(:project, group: subgroup) } let(:subproject2) { create(:project, group: subgroup2) } - let!(:todo_subproject_user) { create(:todo, user: user, project: subproject) } - let!(:todo_subproject2_user) { create(:todo, user: user, project: subproject2) } - let!(:todo_subgroup_user) { create(:todo, user: user, group: subgroup) } - let!(:todo_subgroup2_user) { create(:todo, user: user, group: subgroup2) } + let!(:todo_subproject_user) { create(:todo, user: user, project: subproject) } + let!(:todo_subproject2_user) { create(:todo, user: user, project: subproject2) } + let!(:todo_subgroup_user) { create(:todo, user: user, group: subgroup) } + let!(:todo_subgroup2_user) { create(:todo, user: user, group: subgroup2) } let!(:todo_subproject_user2) { create(:todo, user: user2, project: subproject) } let!(:todo_subpgroup_user2) { create(:todo, user: user2, group: subgroup) } diff --git a/spec/services/todos/destroy/group_private_service_spec.rb b/spec/services/todos/destroy/group_private_service_spec.rb index 2f49b68f544..5cefbdd35ab 100644 --- a/spec/services/todos/destroy/group_private_service_spec.rb +++ b/spec/services/todos/destroy/group_private_service_spec.rb @@ -40,7 +40,7 @@ describe Todos::Destroy::GroupPrivateService do let(:parent_member) { create(:user) } let(:subgroup_member) { create(:user) } - let(:subgproject_member) { create(:user) } + let(:subgproject_member) { create(:user) } let!(:todo_parent_member) { create(:todo, user: parent_member, group: group) } let!(:todo_subgroup_member) { create(:todo, user: subgroup_member, group: group) } diff --git a/spec/services/users/migrate_to_ghost_user_service_spec.rb b/spec/services/users/migrate_to_ghost_user_service_spec.rb index ac3a8738cac..68b0f79c6d1 100644 --- a/spec/services/users/migrate_to_ghost_user_service_spec.rb +++ b/spec/services/users/migrate_to_ghost_user_service_spec.rb @@ -6,7 +6,7 @@ describe Users::MigrateToGhostUserService do let(:service) { described_class.new(user) } context "migrating a user's associated records to the ghost user" do - context 'issues' do + context 'issues' do context 'deleted user is present as both author and edited_user' do include_examples "migrating a deleted user's associated records to the ghost user", Issue, [:author, :last_edited_by] do let(:created_record) do diff --git a/spec/services/web_hook_service_spec.rb b/spec/services/web_hook_service_spec.rb index b1cc6d2eb83..5945a7dc0ad 100644 --- a/spec/services/web_hook_service_spec.rb +++ b/spec/services/web_hook_service_spec.rb @@ -61,7 +61,7 @@ describe WebHookService do end context 'when auth credentials are present' do - let(:url) {'https://example.org'} + let(:url) {'https://example.org'} let(:project_hook) { create(:project_hook, url: 'https://demo:demo@example.org/') } it 'uses the credentials' do @@ -76,7 +76,7 @@ describe WebHookService do end context 'when auth credentials are partial present' do - let(:url) {'https://example.org'} + let(:url) {'https://example.org'} let(:project_hook) { create(:project_hook, url: 'https://demo@example.org/') } it 'uses the credentials anyways' 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 ca977effcb6..97e7a019222 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,9 +127,9 @@ 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') + 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 diff --git a/spec/support/features/resolving_discussions_in_issues_shared_examples.rb b/spec/support/features/resolving_discussions_in_issues_shared_examples.rb index 4a946995f84..38e5fb155a4 100644 --- a/spec/support/features/resolving_discussions_in_issues_shared_examples.rb +++ b/spec/support/features/resolving_discussions_in_issues_shared_examples.rb @@ -5,7 +5,7 @@ shared_examples 'creating an issue for a discussion' do expect(title_field.value).to include(merge_request.title) end - it 'has a mention of the discussion in the description' do + it 'has a mention of the discussion in the description' do description_field = page.find_field('issue[description]') expect(description_field.value).to include(discussion.first_note.note) diff --git a/spec/support/helpers/graphql_helpers.rb b/spec/support/helpers/graphql_helpers.rb index dd32ea3985f..ea3a03879c5 100644 --- a/spec/support/helpers/graphql_helpers.rb +++ b/spec/support/helpers/graphql_helpers.rb @@ -149,7 +149,7 @@ module GraphqlHelpers # - List # - String! # - String - field_type = field_type.of_type while field_type.respond_to?(:of_type) + field_type = field_type.of_type while field_type.respond_to?(:of_type) field_type end diff --git a/spec/support/helpers/kubernetes_helpers.rb b/spec/support/helpers/kubernetes_helpers.rb index 6930b809048..9dc89b483b2 100644 --- a/spec/support/helpers/kubernetes_helpers.rb +++ b/spec/support/helpers/kubernetes_helpers.rb @@ -369,6 +369,6 @@ module KubernetesHelpers end def empty_deployment_rollout_status - ::Gitlab::Kubernetes::RolloutStatus.from_deployments() + ::Gitlab::Kubernetes::RolloutStatus.from_deployments end end diff --git a/spec/support/helpers/login_helpers.rb b/spec/support/helpers/login_helpers.rb index 87cfb6c04dc..3fee6872498 100644 --- a/spec/support/helpers/login_helpers.rb +++ b/spec/support/helpers/login_helpers.rb @@ -157,7 +157,7 @@ module LoginHelpers env['omniauth.error.strategy'] = strategy end - def stub_omniauth_saml_config(messages, context: Rails.application) + def stub_omniauth_saml_config(messages, context: Rails.application) set_devise_mapping(context: context) routes = Rails.application.routes routes.disable_clear_and_finalize = true diff --git a/spec/support/helpers/rake_helpers.rb b/spec/support/helpers/rake_helpers.rb index acd9cce6a67..7d8d7750bf3 100644 --- a/spec/support/helpers/rake_helpers.rb +++ b/spec/support/helpers/rake_helpers.rb @@ -14,7 +14,7 @@ module RakeHelpers end def silence_progress_bar - allow_any_instance_of(ProgressBar::Output).to receive(:stream).and_return(double().as_null_object) + allow_any_instance_of(ProgressBar::Output).to receive(:stream).and_return(double.as_null_object) end def main_object diff --git a/spec/support/helpers/select2_helper.rb b/spec/support/helpers/select2_helper.rb index 90618ba5b19..f4f0415985c 100644 --- a/spec/support/helpers/select2_helper.rb +++ b/spec/support/helpers/select2_helper.rb @@ -1,3 +1,5 @@ +require_relative 'wait_for_requests' + # Select2 ajax programmatic helper # It allows you to select value from select2 # @@ -11,9 +13,13 @@ # module Select2Helper + include WaitForRequests + def select2(value, options = {}) raise ArgumentError, 'options must be a Hash' unless options.is_a?(Hash) + wait_for_requests unless options[:async] + selector = options.fetch(:from) first(selector, visible: false) diff --git a/spec/support/helpers/stub_env.rb b/spec/support/helpers/stub_env.rb index 36b90fc68d6..1c2f474a015 100644 --- a/spec/support/helpers/stub_env.rb +++ b/spec/support/helpers/stub_env.rb @@ -18,7 +18,7 @@ module StubENV allow(ENV).to receive(:[]).with(key).and_return(value) allow(ENV).to receive(:key?).with(key).and_return(true) allow(ENV).to receive(:fetch).with(key).and_return(value) - allow(ENV).to receive(:fetch).with(key, anything()) do |_, default_val| + allow(ENV).to receive(:fetch).with(key, anything) do |_, default_val| value || default_val end end diff --git a/spec/support/helpers/stub_gitlab_calls.rb b/spec/support/helpers/stub_gitlab_calls.rb index 2933f2c78dc..4cb3b18df85 100644 --- a/spec/support/helpers/stub_gitlab_calls.rb +++ b/spec/support/helpers/stub_gitlab_calls.rb @@ -36,31 +36,47 @@ module StubGitlabCalls .to receive(:full_access_token).and_return('token') end - def stub_container_registry_tags(repository: :any, tags:) + def stub_container_registry_tags(repository: :any, tags: [], with_manifest: false) repository = any_args if repository == :any allow_any_instance_of(ContainerRegistry::Client) .to receive(:repository_tags).with(repository) .and_return({ 'tags' => tags }) - allow_any_instance_of(ContainerRegistry::Client) - .to receive(:repository_manifest).with(repository, anything) - .and_return(stub_container_registry_tag_manifest) + if with_manifest + tags.each do |tag| + allow_any_instance_of(ContainerRegistry::Client) + .to receive(:repository_tag_digest) + .with(repository, tag) + .and_return('sha256:4c8e63ca4cb663ce6c688cb06f1c3' \ + '72b088dac5b6d7ad7d49cd620d85cf72a15') + end - allow_any_instance_of(ContainerRegistry::Client) - .to receive(:blob).with(repository, anything, 'application/octet-stream') - .and_return(stub_container_registry_blob) + allow_any_instance_of(ContainerRegistry::Client) + .to receive(:repository_manifest).with(repository, anything) + .and_return(stub_container_registry_tag_manifest_content) + + allow_any_instance_of(ContainerRegistry::Client) + .to receive(:blob).with(repository, anything, 'application/octet-stream') + .and_return(stub_container_registry_blob_content) + end + end + + def stub_commonmark_sourcepos_disabled + allow_any_instance_of(Banzai::Filter::MarkdownEngines::CommonMark) + .to receive(:render_options) + .and_return(Banzai::Filter::MarkdownEngines::CommonMark::RENDER_OPTIONS) end private - def stub_container_registry_tag_manifest + def stub_container_registry_tag_manifest_content fixture_path = 'spec/fixtures/container_registry/tag_manifest.json' JSON.parse(File.read(Rails.root + fixture_path)) end - def stub_container_registry_blob + def stub_container_registry_blob_content fixture_path = 'spec/fixtures/container_registry/config_blob.json' File.read(Rails.root + fixture_path) diff --git a/spec/support/helpers/test_env.rb b/spec/support/helpers/test_env.rb index d352a7cdf1a..f485eb7b0eb 100644 --- a/spec/support/helpers/test_env.rb +++ b/spec/support/helpers/test_env.rb @@ -160,11 +160,12 @@ module TestEnv def setup_gitaly socket_path = Gitlab::GitalyClient.address('default').sub(/\Aunix:/, '') gitaly_dir = File.dirname(socket_path) + install_gitaly_args = [gitaly_dir, repos_path, gitaly_url].compact.join(',') component_timed_setup('Gitaly', install_dir: gitaly_dir, version: Gitlab::GitalyClient.expected_server_version, - task: "gitlab:gitaly:install[#{gitaly_dir},#{repos_path}]") do + task: "gitlab:gitaly:install[#{install_gitaly_args}]") do Gitlab::SetupHelper.create_gitaly_configuration(gitaly_dir, { 'default' => repos_path }, force: true) start_gitaly(gitaly_dir) @@ -215,6 +216,10 @@ module TestEnv # The process can already be gone if the test run was INTerrupted. end + def gitaly_url + ENV.fetch('GITALY_REPO_URL', nil) + end + def setup_factory_repo setup_repo(factory_repo_path, factory_repo_path_bare, factory_repo_name, BRANCH_SHA) 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/redis/redis_shared_examples.rb b/spec/support/redis/redis_shared_examples.rb index e650a176041..a8b00004fe7 100644 --- a/spec/support/redis/redis_shared_examples.rb +++ b/spec/support/redis/redis_shared_examples.rb @@ -1,7 +1,7 @@ RSpec.shared_examples "redis_shared_examples" do include StubENV - let(:test_redis_url) { "redis://redishost:#{redis_port}"} + let(:test_redis_url) { "redis://redishost:#{redis_port}"} before do stub_env(environment_config_file_name, Rails.root.join(config_file_name)) @@ -76,7 +76,7 @@ RSpec.shared_examples "redis_shared_examples" do context 'when yml file with env variable' do let(:config_file_name) { config_with_environment_variable_inside } - before do + before do stub_env(config_env_variable_url, test_redis_url) 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/controllers/set_sort_order_from_user_preference_shared_examples.rb b/spec/support/shared_examples/controllers/set_sort_order_from_user_preference_shared_examples.rb index d86838719d4..98ab04c5636 100644 --- a/spec/support/shared_examples/controllers/set_sort_order_from_user_preference_shared_examples.rb +++ b/spec/support/shared_examples/controllers/set_sort_order_from_user_preference_shared_examples.rb @@ -2,18 +2,12 @@ shared_examples 'set sort order from user preference' do describe '#set_sort_order_from_user_preference' do # There is no issuable_sorting_field defined in any CE controllers yet, # however any other field present in user_preferences table can be used for testing. - let(:sorting_field) { :issue_notes_filter } - let(:sorting_param) { 'any' } - - before do - allow(controller).to receive(:issuable_sorting_field).and_return(sorting_field) - end context 'when database is in read-only mode' do it 'it does not update user preference' do allow(Gitlab::Database).to receive(:read_only?).and_return(true) - expect_any_instance_of(UserPreference).not_to receive(:update_attribute).with(sorting_field, sorting_param) + expect_any_instance_of(UserPreference).not_to receive(:update).with({ controller.send(:issuable_sorting_field) => sorting_param }) get :index, params: { namespace_id: project.namespace, project_id: project, sort: sorting_param } end @@ -23,7 +17,7 @@ shared_examples 'set sort order from user preference' do it 'updates user preference' do allow(Gitlab::Database).to receive(:read_only?).and_return(false) - expect_any_instance_of(UserPreference).to receive(:update_attribute).with(sorting_field, sorting_param) + expect_any_instance_of(UserPreference).to receive(:update).with({ controller.send(:issuable_sorting_field) => sorting_param }) get :index, params: { namespace_id: project.namespace, project_id: project, sort: sorting_param } 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/features/editable_merge_request_shared_examples.rb b/spec/support/shared_examples/features/editable_merge_request_shared_examples.rb index a096627ee62..eef0327c9a6 100644 --- a/spec/support/shared_examples/features/editable_merge_request_shared_examples.rb +++ b/spec/support/shared_examples/features/editable_merge_request_shared_examples.rb @@ -129,12 +129,12 @@ RSpec.shared_examples 'an editable merge request' do expect(merge_request.merge_params['force_remove_source_branch']).to be_truthy visit edit_project_merge_request_path(target_project, merge_request) - uncheck 'Remove source branch when merge request is accepted' + uncheck 'Delete source branch when merge request is accepted' click_button 'Save changes' expect(page).to have_unchecked_field 'remove-source-branch-input' - expect(page).to have_content 'Remove source branch' + expect(page).to have_content 'Delete source branch' 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/support/shared_examples/malicious_regexp_shared_examples.rb b/spec/support/shared_examples/malicious_regexp_shared_examples.rb index 65026f1d7c0..db69b75c0c8 100644 --- a/spec/support/shared_examples/malicious_regexp_shared_examples.rb +++ b/spec/support/shared_examples/malicious_regexp_shared_examples.rb @@ -1,7 +1,7 @@ require 'timeout' shared_examples 'malicious regexp' do - let(:malicious_text) { 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!' } + let(:malicious_text) { 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!' } let(:malicious_regexp) { '(?i)^(([a-z])+.)+[A-Z]([a-z])+$' } it 'takes under a second' do diff --git a/spec/support/shared_examples/mentionable_shared_examples.rb b/spec/support/shared_examples/mentionable_shared_examples.rb index 1685decbe94..1226841f24c 100644 --- a/spec/support/shared_examples/mentionable_shared_examples.rb +++ b/spec/support/shared_examples/mentionable_shared_examples.rb @@ -8,8 +8,8 @@ shared_context 'mentionable context' do let(:project) { subject.project } let(:author) { subject.author } - let(:mentioned_issue) { create(:issue, project: project) } - let!(:mentioned_mr) { create(:merge_request, source_project: project) } + let(:mentioned_issue) { create(:issue, project: project) } + let!(:mentioned_mr) { create(:merge_request, source_project: project) } let(:mentioned_commit) { project.commit("HEAD~1") } let(:ext_proj) { create(:project, :public, :repository) } diff --git a/spec/support/shared_examples/url_validator_examples.rb b/spec/support/shared_examples/url_validator_examples.rb index b4757a70984..1f7e2f7ff79 100644 --- a/spec/support/shared_examples/url_validator_examples.rb +++ b/spec/support/shared_examples/url_validator_examples.rb @@ -1,5 +1,5 @@ RSpec.shared_examples 'url validator examples' do |protocols| - let(:validator) { described_class.new(attributes: [:link_url], **options) } + let(:validator) { described_class.new(attributes: [:link_url], **options) } let!(:badge) { build(:badge, link_url: 'http://www.example.com') } subject { validator.validate_each(badge, :link_url, badge.link_url) } diff --git a/spec/tasks/gitlab/storage_rake_spec.rb b/spec/tasks/gitlab/storage_rake_spec.rb index be902d7c679..6b50670c3c0 100644 --- a/spec/tasks/gitlab/storage_rake_spec.rb +++ b/spec/tasks/gitlab/storage_rake_spec.rb @@ -58,7 +58,7 @@ describe 'rake gitlab:storage:*' do context '0 legacy projects' do it 'does nothing' do - expect(StorageMigratorWorker).not_to receive(:perform_async) + expect(::HashedStorage::MigratorWorker).not_to receive(:perform_async) run_rake_task(task) end @@ -72,9 +72,9 @@ describe 'rake gitlab:storage:*' do stub_env('BATCH' => 1) end - it 'enqueues one StorageMigratorWorker per project' do + it 'enqueues one HashedStorage::MigratorWorker per project' do projects.each do |project| - expect(StorageMigratorWorker).to receive(:perform_async).with(project.id, project.id) + expect(::HashedStorage::MigratorWorker).to receive(:perform_async).with(project.id, project.id) end run_rake_task(task) @@ -86,10 +86,10 @@ describe 'rake gitlab:storage:*' do stub_env('BATCH' => 2) end - it 'enqueues one StorageMigratorWorker per 2 projects' do + it 'enqueues one HashedStorage::MigratorWorker per 2 projects' do projects.map(&:id).sort.each_slice(2) do |first, last| last ||= first - expect(StorageMigratorWorker).to receive(:perform_async).with(first, last) + expect(::HashedStorage::MigratorWorker).to receive(:perform_async).with(first, last) end run_rake_task(task) diff --git a/spec/uploaders/file_uploader_spec.rb b/spec/uploaders/file_uploader_spec.rb index db9e5eb2ad6..185c62491ce 100644 --- a/spec/uploaders/file_uploader_spec.rb +++ b/spec/uploaders/file_uploader_spec.rb @@ -4,7 +4,7 @@ describe FileUploader do let(:group) { create(:group, name: 'awesome') } let(:project) { create(:project, :legacy_storage, namespace: group, name: 'project') } let(:uploader) { described_class.new(project) } - let(:upload) { double(model: project, path: 'secret/foo.jpg') } + let(:upload) { double(model: project, path: 'secret/foo.jpg') } subject { uploader } diff --git a/spec/uploaders/import_export_uploader_spec.rb b/spec/uploaders/import_export_uploader_spec.rb index 51b173b682d..825c1cabc14 100644 --- a/spec/uploaders/import_export_uploader_spec.rb +++ b/spec/uploaders/import_export_uploader_spec.rb @@ -4,7 +4,7 @@ describe ImportExportUploader do let(:model) { build_stubbed(:import_export_upload) } let(:upload) { create(:upload, model: model) } - subject { described_class.new(model, :import_file) } + subject { described_class.new(model, :import_file) } context "object_store is REMOTE" do before do 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/layouts/nav/sidebar/_project.html.haml_spec.rb b/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb index 2852aa380b2..d9f05e5f94f 100644 --- a/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb +++ b/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb @@ -57,4 +57,58 @@ describe 'layouts/nav/sidebar/_project' do expect(rendered).to have_link('Releases', href: project_releases_path(project)) end end + + describe 'wiki entry tab' do + let(:can_read_wiki) { true } + + before do + allow(view).to receive(:can?).with(nil, :read_wiki, project).and_return(can_read_wiki) + end + + describe 'when wiki is enabled' do + it 'shows the wiki tab with the wiki internal link' do + render + + expect(rendered).to have_link('Wiki', href: project_wiki_path(project, :home)) + end + end + + describe 'when wiki is disabled' do + let(:can_read_wiki) { false } + + it 'does not show the wiki tab' do + render + + expect(rendered).not_to have_link('Wiki', href: project_wiki_path(project, :home)) + end + end + end + + describe 'external wiki entry tab' do + let(:properties) { { 'external_wiki_url' => 'https://gitlab.com' } } + let(:service_status) { true } + + before do + project.create_external_wiki_service(active: service_status, properties: properties) + project.reload + end + + context 'when it is active' do + it 'shows the external wiki tab with the external wiki service link' do + render + + expect(rendered).to have_link('External Wiki', href: properties['external_wiki_url']) + end + end + + context 'when it is disabled' do + let(:service_status) { false } + + it 'does not show the external wiki tab' do + render + + expect(rendered).not_to have_link('External Wiki', href: project_wiki_path(project, :home)) + end + end + end end diff --git a/spec/views/projects/_home_panel.html.haml_spec.rb b/spec/views/projects/_home_panel.html.haml_spec.rb index 006c93686d5..908ecb898e4 100644 --- a/spec/views/projects/_home_panel.html.haml_spec.rb +++ b/spec/views/projects/_home_panel.html.haml_spec.rb @@ -23,7 +23,7 @@ describe 'projects/_home_panel' do it 'makes it possible to set notification level' do render - expect(view).to render_template('projects/buttons/_notifications') + expect(view).to render_template('shared/notifications/_new_button') expect(rendered).to have_selector('.notification-dropdown') end end diff --git a/spec/views/projects/commit/_commit_box.html.haml_spec.rb b/spec/views/projects/commit/_commit_box.html.haml_spec.rb index 2fdd28a3be4..1086546c10d 100644 --- a/spec/views/projects/commit/_commit_box.html.haml_spec.rb +++ b/spec/views/projects/commit/_commit_box.html.haml_spec.rb @@ -9,6 +9,7 @@ describe 'projects/commit/_commit_box.html.haml' do assign(:commit, project.commit) allow(view).to receive(:current_user).and_return(user) allow(view).to receive(:can_collaborate_with_project?).and_return(false) + project.add_developer(user) end it 'shows the commit SHA' do @@ -48,7 +49,6 @@ describe 'projects/commit/_commit_box.html.haml' do context 'viewing a commit' do context 'as a developer' do before do - project.add_developer(user) allow(view).to receive(:can_collaborate_with_project?).and_return(true) end @@ -60,6 +60,10 @@ describe 'projects/commit/_commit_box.html.haml' do end context 'as a non-developer' do + before do + project.add_guest(user) + end + it 'does not have a link to create a new tag' do render diff --git a/spec/views/projects/issues/_related_branches.html.haml_spec.rb b/spec/views/projects/issues/_related_branches.html.haml_spec.rb index 8c845251765..5cff7694029 100644 --- a/spec/views/projects/issues/_related_branches.html.haml_spec.rb +++ b/spec/views/projects/issues/_related_branches.html.haml_spec.rb @@ -3,6 +3,7 @@ require 'spec_helper' describe 'projects/issues/_related_branches' do include Devise::Test::ControllerHelpers + let(:user) { create(:user) } let(:project) { create(:project, :repository) } let(:branch) { project.repository.find_branch('feature') } let!(:pipeline) { create(:ci_pipeline, project: project, sha: branch.dereferenced_target.id, ref: 'feature') } @@ -11,6 +12,9 @@ describe 'projects/issues/_related_branches' do assign(:project, project) assign(:related_branches, ['feature']) + project.add_developer(user) + allow(view).to receive(:current_user).and_return(user) + render end diff --git a/spec/views/projects/issues/show.html.haml_spec.rb b/spec/views/projects/issues/show.html.haml_spec.rb new file mode 100644 index 00000000000..ff88efd0e31 --- /dev/null +++ b/spec/views/projects/issues/show.html.haml_spec.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'projects/issues/show' do + let(:project) { create(:project, :repository) } + let(:issue) { create(:issue, project: project, author: user) } + let(:user) { create(:user) } + + before do + assign(:project, project) + assign(:issue, issue) + assign(:noteable, issue) + stub_template 'shared/issuable/_sidebar' => '' + stub_template 'projects/issues/_discussion' => '' + allow(view).to receive(:issuable_meta).and_return('') + end + + context 'when the issue is closed' do + before do + allow(issue).to receive(:closed?).and_return(true) + end + + it 'shows "Closed (moved)" if an issue has been moved' do + allow(issue).to receive(:moved?).and_return(true) + + render + + expect(rendered).to have_selector('.status-box-issue-closed:not(.hidden)', text: 'Closed (moved)') + end + + it 'shows "Closed" if an issue has not been moved' do + render + + expect(rendered).to have_selector('.status-box-issue-closed:not(.hidden)', text: 'Closed') + end + end + + context 'when the issue is open' do + before do + allow(issue).to receive(:closed?).and_return(false) + allow(issue).to receive(:disscussion_locked).and_return(false) + end + + it 'shows "Open" if an issue has been moved' do + render + + expect(rendered).to have_selector('.status-box-open:not(.hidden)', text: 'Open') + 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/cleanup_container_repository_worker_spec.rb b/spec/workers/cleanup_container_repository_worker_spec.rb new file mode 100644 index 00000000000..5bee7294010 --- /dev/null +++ b/spec/workers/cleanup_container_repository_worker_spec.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe CleanupContainerRepositoryWorker, :clean_gitlab_redis_shared_state do + let(:repository) { create(:container_repository) } + let(:project) { repository.project } + let(:user) { project.owner } + let(:params) { { key: 'value' } } + + subject { described_class.new } + + describe '#perform' do + let(:service) { instance_double(Projects::ContainerRepository::CleanupTagsService) } + + before do + allow(Projects::ContainerRepository::CleanupTagsService).to receive(:new) + .with(project, user, params).and_return(service) + end + + it 'executes the destroy service' do + expect(service).to receive(:execute) + + subject.perform(user.id, repository.id, params) + end + + it 'does not raise error when user could not be found' do + expect do + subject.perform(-1, repository.id, params) + end.not_to raise_error + end + + it 'does not raise error when repository could not be found' do + expect do + subject.perform(user.id, -1, params) + end.not_to raise_error + end + + context 'when executed twice in short period' do + it 'executes service only for the first time' do + expect(service).to receive(:execute).once + + 2.times { subject.perform(user.id, repository.id, params) } + end + end + end +end diff --git a/spec/workers/expire_build_artifacts_worker_spec.rb b/spec/workers/expire_build_artifacts_worker_spec.rb index b47b4a02a68..27995cf1611 100644 --- a/spec/workers/expire_build_artifacts_worker_spec.rb +++ b/spec/workers/expire_build_artifacts_worker_spec.rb @@ -11,6 +11,7 @@ describe ExpireBuildArtifactsWorker do describe '#perform' do before do + stub_feature_flags(ci_new_expire_job_artifacts_service: false) build end @@ -47,4 +48,17 @@ describe ExpireBuildArtifactsWorker do Sidekiq::Queues.jobs_by_worker['ExpireBuildInstanceArtifactsWorker'] end end + + describe '#perform with ci_new_expire_job_artifacts_service feature flag' do + before do + stub_feature_flags(ci_new_expire_job_artifacts_service: true) + end + + it 'executes a service' do + expect_any_instance_of(Ci::DestroyExpiredJobArtifactsService).to receive(:execute) + expect(ExpireBuildInstanceArtifactsWorker).not_to receive(:bulk_perform_async) + + worker.perform + end + end end diff --git a/spec/workers/storage_migrator_worker_spec.rb b/spec/workers/hashed_storage/migrator_worker_spec.rb index 808084c8f7c..a85f820a3eb 100644 --- a/spec/workers/storage_migrator_worker_spec.rb +++ b/spec/workers/hashed_storage/migrator_worker_spec.rb @@ -1,13 +1,13 @@ require 'spec_helper' -describe StorageMigratorWorker do +describe HashedStorage::MigratorWorker do subject(:worker) { described_class.new } let(:projects) { create_list(:project, 2, :legacy_storage, :empty_repo) } let(:ids) { projects.map(&:id) } describe '#perform' do it 'delegates to MigratorService' do - expect_any_instance_of(Gitlab::HashedStorage::Migrator).to receive(:bulk_migrate).with(5, 10) + expect_any_instance_of(Gitlab::HashedStorage::Migrator).to receive(:bulk_migrate).with(start: 5, finish: 10) worker.perform(5, 10) end diff --git a/spec/workers/project_migrate_hashed_storage_worker_spec.rb b/spec/workers/project_migrate_hashed_storage_worker_spec.rb index 3703320418b..333eb6a0569 100644 --- a/spec/workers/project_migrate_hashed_storage_worker_spec.rb +++ b/spec/workers/project_migrate_hashed_storage_worker_spec.rb @@ -4,12 +4,13 @@ describe ProjectMigrateHashedStorageWorker, :clean_gitlab_redis_shared_state do include ExclusiveLeaseHelpers describe '#perform' do - let(:project) { create(:project, :empty_repo) } + let(:project) { create(:project, :empty_repo, :legacy_storage) } let(:lease_key) { "project_migrate_hashed_storage_worker:#{project.id}" } - let(:lease_timeout) { ProjectMigrateHashedStorageWorker::LEASE_TIMEOUT } + let(:lease_timeout) { described_class::LEASE_TIMEOUT } + let(:migration_service) { ::Projects::HashedStorage::MigrationService } it 'skips when project no longer exists' do - expect(::Projects::HashedStorageMigrationService).not_to receive(:new) + expect(migration_service).not_to receive(:new) subject.perform(-1) end @@ -17,29 +18,29 @@ describe ProjectMigrateHashedStorageWorker, :clean_gitlab_redis_shared_state do it 'skips when project is pending delete' do pending_delete_project = create(:project, :empty_repo, pending_delete: true) - expect(::Projects::HashedStorageMigrationService).not_to receive(:new) + expect(migration_service).not_to receive(:new) subject.perform(pending_delete_project.id) end - it 'delegates removal to service class when have exclusive lease' do + it 'delegates migration to service class when we have exclusive lease' do stub_exclusive_lease(lease_key, 'uuid', timeout: lease_timeout) - migration_service = spy + service_spy = spy - allow(::Projects::HashedStorageMigrationService) + allow(migration_service) .to receive(:new).with(project, project.full_path, logger: subject.logger) - .and_return(migration_service) + .and_return(service_spy) subject.perform(project.id) - expect(migration_service).to have_received(:execute) + expect(service_spy).to have_received(:execute) end - it 'skips when dont have lease when dont have exclusive lease' do + it 'skips when it cant acquire the exclusive lease' do stub_exclusive_lease_taken(lease_key, timeout: lease_timeout) - expect(::Projects::HashedStorageMigrationService).not_to receive(:new) + expect(migration_service).not_to receive(:new) subject.perform(project.id) end diff --git a/spec/workers/prune_old_events_worker_spec.rb b/spec/workers/prune_old_events_worker_spec.rb index b999a6fd5b6..ea2b6ae229e 100644 --- a/spec/workers/prune_old_events_worker_spec.rb +++ b/spec/workers/prune_old_events_worker_spec.rb @@ -5,8 +5,8 @@ describe PruneOldEventsWorker do let(:user) { create(:user) } let!(:expired_event) { create(:event, :closed, author: user, created_at: 25.months.ago) } - let!(:not_expired_1_day_event) { create(:event, :closed, author: user, created_at: 1.day.ago) } - let!(:not_expired_13_month_event) { create(:event, :closed, author: user, created_at: 13.months.ago) } + let!(:not_expired_1_day_event) { create(:event, :closed, author: user, created_at: 1.day.ago) } + let!(:not_expired_13_month_event) { create(:event, :closed, author: user, created_at: 13.months.ago) } let!(:not_expired_2_years_event) { create(:event, :closed, author: user, created_at: 2.years.ago) } it 'prunes events older than 2 years' 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 cde83c8e8a7..5c9139fdbfa 100644 --- a/yarn.lock +++ b/yarn.lock @@ -64,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" @@ -238,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": @@ -254,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" @@ -270,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": @@ -467,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" @@ -544,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" @@ -576,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" @@ -644,15 +653,15 @@ eslint-plugin-promise "^4.0.1" eslint-plugin-vue "^5.0.0" -"@gitlab/svgs@^1.47.0": - version "1.47.0" - resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.47.0.tgz#c03dda41aefd3889cbfed95a391836106ae2ac4d" - integrity sha512-0Bx/HxqR8xpqqaLnZiFAHIh1jTAFQPFToVZ6Wi3QyhsAwmXRAbgw1SlkRMZ7w3e6l+G71Wnw+GnI4rx1gK8JLQ== +"@gitlab/svgs@^1.48.0": + version "1.48.0" + resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.48.0.tgz#7b2e20e357d85aa46e905e6ca51b0b4184ae2794" + integrity sha512-9lRsfqN0W3JxopiXnTzvDY31O465jMTGNKpiOCXy7uAMfwZA6UsRsc7Pp369uKnOLR0duXUGOxOv4NGsK6AeXw== -"@gitlab/ui@^1.20.0": - version "1.20.0" - resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-1.20.0.tgz#50bd4b092646a2c6337f0f462779af8e702dda05" - integrity sha512-EJgrqon/tYCUPoOgnNNAXbrDXOEAajJwKHr4aR2R6vkJI3kVZiq66RNIe5ftGIUoNqYCDnRIkpLyo7MqzJPgcw== +"@gitlab/ui@^1.22.1": + version "1.22.1" + resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-1.22.1.tgz#92ed77216c5702776049b9ac41eb717c1acd864e" + integrity sha512-pWbEaXOOcp8Xt2TjJtPas3lXwWVvizrBOf0M8yN0XAn2GgIRCVnRMpjNEN7/oNeBcEM9CrmPYApEM/hZO+maqQ== dependencies: babel-standalone "^6.26.0" bootstrap-vue "^2.0.0-rc.11" @@ -670,6 +679,11 @@ resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.7.0.tgz#9a06f4f137ee84d7df0460c1fdb1135ffa6c50fd" integrity sha512-ONhaKPIufzzrlNbqtWFFd+jlnemX6lJAgq9ZeiZtS7I1PIf/la7CW4m83rTXRnVnsMbW2k56pGYu7AUFJD9Pow== +"@types/anymatch@*": + version "1.3.0" + resolved "https://registry.yarnpkg.com/@types/anymatch/-/anymatch-1.3.0.tgz#d1d55958d1fccc5527d4aba29fc9c4b942f563ff" + integrity sha512-7WcbyctkE8GTzogDb0ulRAEw7v8oIS54ft9mQTU7PfM0hp5e+8kpa+HeQ7IQrFbKtJXBKcZ4bh+Em9dTw5L6AQ== + "@types/async@2.0.50": version "2.0.50" resolved "https://registry.yarnpkg.com/@types/async/-/async-2.0.50.tgz#117540e026d64e1846093abbd5adc7e27fda7bcb" @@ -724,6 +738,29 @@ resolved "https://registry.yarnpkg.com/@types/strip-json-comments/-/strip-json-comments-0.0.30.tgz#9aa30c04db212a9a0649d6ae6fd50accc40748a1" integrity sha512-7NQmHra/JILCd1QqpSzl8+mJRc8ZHz3uDm8YV1Ks9IhK0epEiTw8aIErbvH9PI+6XbqhyIQy3462nEsn7UVzjQ== +"@types/tapable@*": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@types/tapable/-/tapable-1.0.4.tgz#b4ffc7dc97b498c969b360a41eee247f82616370" + integrity sha512-78AdXtlhpCHT0K3EytMpn4JNxaf5tbqbLcbIRoQIHzpTIyjpxLQKRoxU55ujBXAtg3Nl2h/XWvfDa9dsMOd0pQ== + +"@types/uglify-js@*": + version "3.0.4" + resolved "https://registry.yarnpkg.com/@types/uglify-js/-/uglify-js-3.0.4.tgz#96beae23df6f561862a830b4288a49e86baac082" + integrity sha512-SudIN9TRJ+v8g5pTG8RRCqfqTMNqgWCKKd3vtynhGzkIIjxaicNAMuY5TRadJ6tzDu3Dotf3ngaMILtmOdmWEQ== + dependencies: + source-map "^0.6.1" + +"@types/webpack@^4.4.19": + version "4.4.23" + resolved "https://registry.yarnpkg.com/@types/webpack/-/webpack-4.4.23.tgz#059d6f4598cfd65ddee0e2db38317ef989696712" + integrity sha512-WswyG+2mRg0ul/ytPpCSWo+kOlVVPW/fKCBEVwqmPVC/2ffWEwhsCEQgnFbWDf8EWId2qGcpL623EjLfNTRk9A== + dependencies: + "@types/anymatch" "*" + "@types/node" "*" + "@types/tapable" "*" + "@types/uglify-js" "*" + source-map "^0.6.0" + "@types/zen-observable@^0.8.0": version "0.8.0" resolved "https://registry.yarnpkg.com/@types/zen-observable/-/zen-observable-0.8.0.tgz#8b63ab7f1aa5321248aad5ac890a485656dcea4d" @@ -927,12 +964,10 @@ accepts@~1.3.4, accepts@~1.3.5: mime-types "~2.1.18" negotiator "0.6.1" -acorn-dynamic-import@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/acorn-dynamic-import/-/acorn-dynamic-import-3.0.0.tgz#901ceee4c7faaef7e07ad2a47e890675da50a278" - integrity sha512-zVWV8Z8lislJoOKKqdNMOB+s6+XV5WERty8MnKBeFgwA+19XJjJHs2RP5dzM57FftIs+jQnRToLiWazKr6sSWg== - dependencies: - acorn "^5.0.0" +acorn-dynamic-import@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/acorn-dynamic-import/-/acorn-dynamic-import-4.0.0.tgz#482210140582a36b83c3e342e1cfebcaa9240948" + integrity sha512-d3OEjQV4ROpoflsnUA8HozoIR504TFxNivYEUi6uwz0IYhBkTDXGuWlNdMtybRt3nqVx/L6XqMt0FxkXuWKZhw== acorn-globals@^4.1.0: version "4.3.0" @@ -952,15 +987,15 @@ acorn-walk@^6.0.1: resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-6.1.1.tgz#d363b66f5fac5f018ff9c3a1e7b6f8e310cc3913" integrity sha512-OtUw6JUTgxA2QoqqmrmQ7F2NYqiBPi/L2jqHyFtllhOUvXYQXf0Z1CYUinIfyT4bTCGmrA7gX9FvHA81uzCoVw== -acorn@^5.0.0, acorn@^5.5.3, acorn@^5.6.2, acorn@^5.7.3: +acorn@^5.5.3, acorn@^5.7.3: version "5.7.3" resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.3.tgz#67aa231bf8812974b85235a96771eb6bd07ea279" integrity sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw== -acorn@^6.0.1, acorn@^6.0.2: - version "6.0.4" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.0.4.tgz#77377e7353b72ec5104550aa2d2097a2fd40b754" - integrity sha512-VY4i5EKSKkofY2I+6QLTbTTN/UvEQPCo6eiwzzSaSWfpaDhOmStMCMod6wmuPciNq+XS0faCglFu2lHZpdHUtg== +acorn@^6.0.1, acorn@^6.0.2, acorn@^6.0.5: + version "6.0.5" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.0.5.tgz#81730c0815f3f3b34d8efa95cb7430965f4d887a" + integrity sha512-i33Zgp3XWtmZBMNvCr4azvOFeWVw1Rk6p3hfi3LUDvIFraOMywb1kAtrbi+med14m4Xfpqm3zRZMT+c0FNE7kg== after@0.8.2: version "0.8.2" @@ -2097,7 +2132,7 @@ check-types@^7.3.0: resolved "https://registry.yarnpkg.com/check-types/-/check-types-7.3.0.tgz#468f571a4435c24248f5fd0cb0e8d87c3c341e7d" integrity sha1-Ro9XGkQ1wkJI9f0MsOjYfDw0Hn0= -chokidar@^2.0.0, chokidar@^2.0.2, chokidar@^2.0.3: +chokidar@^2.0.0, chokidar@^2.0.2, chokidar@^2.0.3, chokidar@^2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.0.4.tgz#356ff4e2b0e8e43e322d18a372460bbcf3accd26" integrity sha512-z9n7yt9rOvIJrMhvDtDictKrkFHeihkNl6uWMmZlmL6tJtX9Cs+87oK+teBx+JIgzvbX3yZHT3eF8vpbDxHJXQ== @@ -2179,6 +2214,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" @@ -2263,10 +2308,10 @@ color-name@1.1.3: resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= -colors@^1.1.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/colors/-/colors-1.1.2.tgz#168a4701756b6a7f51a12ce0c97bfa28c084ed63" - integrity sha1-FopHAXVran9RoSzgyXv6KMCE7WM= +colors@^1.1.0, 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" @@ -2282,7 +2327,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== @@ -3296,7 +3341,7 @@ duplexer3@^0.1.4: resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.4.tgz#ee01dd1cac0ed3cbc7fdbea37dc0a8f1ce002ce2" integrity sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI= -duplexer@^0.1.1, duplexer@~0.1.1: +duplexer@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.1.tgz#ace6ff808c1ce66b57d1ebf97977acb02334cfc1" integrity sha1-rOb/gIwc5mtX0ev5eXessCM0z8E= @@ -3796,19 +3841,6 @@ eve-raphael@0.5.0: resolved "https://registry.yarnpkg.com/eve-raphael/-/eve-raphael-0.5.0.tgz#17c754b792beef3fa6684d79cf5a47c63c4cda30" integrity sha1-F8dUt5K+7z+maE15z1pHxjxM2jA= -event-stream@~3.3.0: - version "3.3.4" - resolved "https://registry.yarnpkg.com/event-stream/-/event-stream-3.3.4.tgz#4ab4c9a0f5a54db9338b4c34d86bfce8f4b35571" - integrity sha1-SrTJoPWlTbkzi0w02Gv86PSzVXE= - dependencies: - duplexer "~0.1.1" - from "~0" - map-stream "~0.1.0" - pause-stream "0.0.11" - split "0.3" - stream-combiner "~0.0.4" - through "~2.3.1" - eventemitter3@1.x.x: version "1.2.0" resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-1.2.0.tgz#1c86991d816ad1e504750e73874224ecf3bec508" @@ -4070,6 +4102,13 @@ fastparse@^1.1.1: resolved "https://registry.yarnpkg.com/fastparse/-/fastparse-1.1.1.tgz#d1e2643b38a94d7583b479060e6c4affc94071f8" integrity sha1-0eJkOzipTXWDtHkGDmxK/8lAcfg= +fault@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/fault/-/fault-1.0.2.tgz#c3d0fec202f172a3a4d414042ad2bb5e2a3ffbaa" + integrity sha512-o2eo/X2syzzERAtN5LcGbiVQ0WwZSlN3qLtadwAz3X8Bu+XWD16dja/KMsjZLiQr+BLGPDnHGkc4yUJf1Xpkpw== + dependencies: + format "^0.2.2" + faye-websocket@^0.10.0: version "0.10.0" resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.10.0.tgz#4e492f8d04dfb6f89003507f6edbf2d501e7c6f4" @@ -4289,6 +4328,11 @@ form-data@~2.3.2: combined-stream "^1.0.6" mime-types "^2.1.12" +format@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/format/-/format-0.2.2.tgz#d6170107e9efdc4ed30c9dc39016df942b5cb58b" + integrity sha1-1hcBB+nv3E7TDJ3DkBbflCtctYs= + formdata-polyfill@^3.0.11: version "3.0.11" resolved "https://registry.yarnpkg.com/formdata-polyfill/-/formdata-polyfill-3.0.11.tgz#c82b4b4bea3356c0a6752219e54ce1edb2a7fb5b" @@ -4319,11 +4363,6 @@ from2@^2.1.0, from2@^2.1.1: inherits "^2.0.1" readable-stream "^2.0.0" -from@~0: - version "0.1.7" - resolved "https://registry.yarnpkg.com/from/-/from-0.1.7.tgz#83c60afc58b9c56997007ed1a768b3ab303a44fe" - integrity sha1-g8YK/Fi5xWmXAH7Rp2izqzA6RP4= - fs-access@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/fs-access/-/fs-access-1.0.1.tgz#d6a87f262271cefebec30c553407fb995da8777a" @@ -4792,7 +4831,7 @@ he@^1.1.0, he@^1.1.1: resolved "https://registry.yarnpkg.com/he/-/he-1.1.1.tgz#93410fd21b009735151f8868c2f271f3427e23fd" integrity sha1-k0EP0hsAlzUVH4howvJx80J+I/0= -highlight.js@^9.13.1: +highlight.js@^9.13.1, highlight.js@~9.13.0: version "9.13.1" resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-9.13.1.tgz#054586d53a6863311168488a0f58d6c505ce641e" integrity sha512-Sc28JNQNDzaH6PORtRLMvif9RSn1mYuOoX3omVjnb0+HbpPygU2ALBI0R/wsiqCb4/fcp07Gdo8g+fhtFrQl6A== @@ -6330,12 +6369,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" @@ -6443,6 +6482,13 @@ lightercollective@^0.1.0: resolved "https://registry.yarnpkg.com/lightercollective/-/lightercollective-0.1.0.tgz#70df102c530dcb8d0ccabfe6175a8d00d5f61300" integrity sha512-J9tg5uraYoQKaWbmrzDDexbG6hHnMcWS1qLYgJSWE+mpA3U5OCSeMUhb+K55otgZJ34oFdR0ECvdIb3xuO5JOQ== +linkify-it@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-2.1.0.tgz#c4caf38a6cd7ac2212ef3c7d2bde30a91561f9db" + integrity sha512-4REs8/062kV2DSHxNfq5183zrqXMl7WP0WzABH9IeJI+NLm429FgE1PDecltYfnOoFDFlZGh2T8PfZn0r+GTRg== + dependencies: + uc.micro "^1.0.1" + load-json-file@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0" @@ -6604,6 +6650,14 @@ lowercase-keys@1.0.0, lowercase-keys@^1.0.0: resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.0.tgz#4e3366b39e7f5457e35f1324bdf6f88d0bfc7306" integrity sha1-TjNms55/VFfjXxMkvfb4jQv8cwY= +lowlight@^1.11.0: + version "1.11.0" + resolved "https://registry.yarnpkg.com/lowlight/-/lowlight-1.11.0.tgz#1304d83005126d4e8b1dc0f07981e9b689ec2efc" + integrity sha512-xrGGN6XLL7MbTMdPD6NfWPwY43SNkjf/d0mecSx/CW36fUZTjRHEq0/Cdug3TWKtRXLWi7iMl1eP0olYxj/a4A== + dependencies: + fault "^1.0.2" + highlight.js "~9.13.0" + lru-cache@2.2.x: version "2.2.4" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-2.2.4.tgz#6c658619becf14031d0d0b594b16042ce4dc063d" @@ -6655,11 +6709,6 @@ map-cache@^0.2.2: resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf" integrity sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8= -map-stream@~0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/map-stream/-/map-stream-0.1.0.tgz#e56aa94c4c8055a16404a0674b78f215f7c8e194" - integrity sha1-5WqpTEyAVaFkBKBnS3jyFffI4ZQ= - map-visit@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/map-visit/-/map-visit-1.0.0.tgz#ecdca8f13144e660f1b5bd41f12f3479d98dfb8f" @@ -6667,16 +6716,22 @@ map-visit@^1.0.0: dependencies: object-visit "^1.0.0" +markdown-it@^8.4.2: + version "8.4.2" + resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-8.4.2.tgz#386f98998dc15a37722aa7722084f4020bdd9b54" + integrity sha512-GcRz3AWTqSUphY3vsUqQSFMbgR38a4Lh3GWlHRh/7MRwz8mcu9n2IO7HOh+bXHrR9kOPDl5RNCaEsrneb+xhHQ== + dependencies: + argparse "^1.0.7" + entities "~1.1.1" + linkify-it "^2.0.0" + mdurl "^1.0.1" + uc.micro "^1.0.5" + 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== -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== - math-random@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/math-random/-/math-random-1.0.1.tgz#8b3aac588b8a66e4975e3cdea67f7bb329601fac" @@ -6690,6 +6745,11 @@ md5.js@^1.3.4: hash-base "^3.0.0" inherits "^2.0.1" +mdurl@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e" + integrity sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4= + media-typer@0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" @@ -6923,15 +6983,17 @@ moment@2.x, moment@^2.21.0: resolved "https://registry.yarnpkg.com/moment/-/moment-2.22.2.tgz#3c257f9839fc0e93ff53149632239eb90783ff66" integrity sha1-PCV/mDn8DpP/UxSWMiOeuQeD/2Y= -monaco-editor-webpack-plugin@^1.5.4: - version "1.5.4" - resolved "https://registry.yarnpkg.com/monaco-editor-webpack-plugin/-/monaco-editor-webpack-plugin-1.5.4.tgz#6781a130e3e1379bb8f4cd190132f4af6dcd2c16" - integrity sha512-9YmWYQdZoAoZ1RLy/uvoDbCcb0EKy5O2qoMQn+UIVQxk+VTCXfJDgANczDIWko+UOzg0MY0P+sA8bl4XI14RJg== +monaco-editor-webpack-plugin@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/monaco-editor-webpack-plugin/-/monaco-editor-webpack-plugin-1.7.0.tgz#920cbeecca25f15d70d568a7e11b0ba4daf1ae83" + integrity sha512-oItymcnlL14Sjd7EF7q+CMhucfwR/2BxsqrXIBrWL6LQplFfAfV+grLEQRmVHeGSBZ/Gk9ptzfueXnWcoEcFuA== + dependencies: + "@types/webpack" "^4.4.19" -monaco-editor@^0.14.3: - version "0.14.3" - resolved "https://registry.yarnpkg.com/monaco-editor/-/monaco-editor-0.14.3.tgz#7cc4a4096a3821f52fea9b10489b527ef3034e22" - integrity sha512-RhaO4xXmWn/p0WrkEOXe4PoZj6xOcvDYjoAh0e1kGUrQnP1IOpc0m86Ceuaa2CLEMDINqKijBSmqhvBQnsPLHQ== +monaco-editor@^0.15.6: + version "0.15.6" + resolved "https://registry.yarnpkg.com/monaco-editor/-/monaco-editor-0.15.6.tgz#d63b3b06f86f803464f003b252627c3eb4a09483" + integrity sha512-JoU9V9k6KqT9R9Tiw1RTU8ohZ+Xnf9DMg6Ktqqw5hILumwmq7xqa/KLXw513uTUsWbhtnHoSJYYR++u3pkyxJg== mousetrap@^1.4.6: version "1.4.6" @@ -7118,21 +7180,21 @@ node-releases@^1.1.3: dependencies: semver "^5.3.0" -nodemon@^1.18.4: - version "1.18.4" - resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-1.18.4.tgz#873f65fdb53220eb166180cf106b1354ac5d714d" - integrity sha512-hyK6vl65IPnky/ee+D3IWvVGgJa/m3No2/Xc/3wanS6Ce1MWjCzH6NnhPJ/vZM+6JFym16jtHx51lmCMB9HDtg== +nodemon@^1.18.9: + version "1.18.9" + resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-1.18.9.tgz#90b467efd3b3c81b9453380aeb2a2cba535d0ead" + integrity sha512-oj/eEVTEI47pzYAjGkpcNw0xYwTl4XSTUQv2NPQI6PpN3b75PhpuYk3Vb3U80xHCyM2Jm+1j68ULHXl4OR3Afw== dependencies: - chokidar "^2.0.2" + chokidar "^2.0.4" debug "^3.1.0" ignore-by-default "^1.0.1" minimatch "^3.0.4" - pstree.remy "^1.1.0" + pstree.remy "^1.1.6" semver "^5.5.0" supports-color "^5.2.0" touch "^3.1.0" undefsafe "^2.0.2" - update-notifier "^2.3.0" + update-notifier "^2.5.0" nopt@3.x: version "3.0.6" @@ -7401,6 +7463,11 @@ optionator@^0.8.1, optionator@^0.8.2: type-check "~0.3.2" wordwrap "~1.0.0" +orderedmap@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/orderedmap/-/orderedmap-1.0.0.tgz#d90fc2ba1ed085190907d601dec6e6a53f8d41ba" + integrity sha1-2Q/Cuh7QhRkJB9YB3sbmpT+NQbo= + original@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/original/-/original-1.0.2.tgz#e442a61cffe1c5fd20a65f3261c26663b303f25f" @@ -7688,13 +7755,6 @@ path-type@^3.0.0: dependencies: pify "^3.0.0" -pause-stream@0.0.11: - version "0.0.11" - resolved "https://registry.yarnpkg.com/pause-stream/-/pause-stream-0.0.11.tgz#fe5a34b0cbce12b5aa6a2b403ee2e73b602f1445" - integrity sha1-/lo0sMvOErWqaitAPuLnO2AvFEU= - dependencies: - through "~2.3" - pbkdf2@^3.0.3: version "3.0.14" resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.0.14.tgz#a35e13c64799b06ce15320f459c230e68e73bade" @@ -7740,6 +7800,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" @@ -7771,6 +7838,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" @@ -7874,10 +7946,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" @@ -7932,6 +8004,122 @@ prompts@^0.1.9: kleur "^2.0.1" sisteransi "^0.1.1" +prosemirror-commands@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/prosemirror-commands/-/prosemirror-commands-1.0.7.tgz#e5a2ba821e29ea7065c88277fe2c3d7f6b0b9d37" + integrity sha512-IR8yMSdw7XlKuF68tydAak1J9P/lLD5ohsrL7pzoLsJAJAQU7mVPDXtGbQrrm0mesddFjcc1zNo/cJQN3lRYnA== + dependencies: + prosemirror-model "^1.0.0" + prosemirror-state "^1.0.0" + prosemirror-transform "^1.0.0" + +prosemirror-dropcursor@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/prosemirror-dropcursor/-/prosemirror-dropcursor-1.1.1.tgz#c60ed1ed6c58804a06a75db06a0d993b087b7622" + integrity sha512-GeUyMO/tOEf8MXrP7Xb7UIMrfK86OGh0fnyBrHfhav4VjY9cw65mNoqHy87CklE5711AhCP5Qzfp8RL/hVKusg== + dependencies: + prosemirror-state "^1.0.0" + prosemirror-transform "^1.1.0" + prosemirror-view "^1.1.0" + +prosemirror-gapcursor@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/prosemirror-gapcursor/-/prosemirror-gapcursor-1.0.3.tgz#acc6537fc5a35e9b38966f91a199a382dfc715c4" + integrity sha512-X+hJhr42PcHWiSWL+lI5f/UeOhXCxlBFb8M6O8aG1hssmaRrW7sS2/Fjg5jFV+pTdS1REFkmm1occh01FMdDIQ== + dependencies: + prosemirror-keymap "^1.0.0" + prosemirror-model "^1.0.0" + prosemirror-state "^1.0.0" + prosemirror-view "^1.0.0" + +prosemirror-history@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/prosemirror-history/-/prosemirror-history-1.0.3.tgz#5fb8591adfc272afaaf0b41bec64ee7d9522a118" + integrity sha512-IfFGbhafSx+R3aq7nLJGkXeu2iaUiP8mkU3aRu2uQcIIjU8Fq7RJfuvhIOJ2RNUoSyqF/ANkdTjnZ74F5eHs1Q== + dependencies: + prosemirror-state "^1.2.2" + prosemirror-transform "^1.0.0" + rope-sequence "^1.2.0" + +prosemirror-inputrules@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/prosemirror-inputrules/-/prosemirror-inputrules-1.0.1.tgz#f63305fd966379f218e82ca76a2a9b328b66dc7b" + integrity sha512-UHy22NmwxS5WIMQYkzraDttQAF8mpP82FfbJsmKFfx6jwkR/SZa+ZhbkLY0zKQ5fBdJN7euj36JG/B5iAlrpxA== + dependencies: + prosemirror-state "^1.0.0" + prosemirror-transform "^1.0.0" + +prosemirror-keymap@^1.0.0, prosemirror-keymap@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/prosemirror-keymap/-/prosemirror-keymap-1.0.1.tgz#03ef32b828e3a859dfb570eb84928bf2e5330bc2" + integrity sha512-e79ApE7PXXZMFtPz7WbjycjAFd1NPjgY1MkecVz98tqwlBSggXWXYQnWFk6x7UkmnBYRHHbXHkR/RXmu2wyBJg== + dependencies: + prosemirror-state "^1.0.0" + w3c-keyname "^1.1.8" + +prosemirror-markdown@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/prosemirror-markdown/-/prosemirror-markdown-1.3.0.tgz#a100d14c27da7d8fb70818230d786898eeadb7fa" + integrity sha512-76l3yLB/suy6sA7LpzRJvRRWkHtKwOTpgWVNwmlIAIIZJeMypWSPldT/gFyIG604eyXEPZitnx+j80Y2DpbnUQ== + dependencies: + markdown-it "^8.4.2" + prosemirror-model "^1.0.0" + +prosemirror-model@^1.0.0, prosemirror-model@^1.1.0, prosemirror-model@^1.6.4: + version "1.6.4" + resolved "https://registry.yarnpkg.com/prosemirror-model/-/prosemirror-model-1.6.4.tgz#2ac37a629448a7dbfd1635450e2fdd63c3450d7d" + integrity sha512-C2ALle8fZsAza+6stUF9Gv28jH9XtpNeczb33bowGlnb2cpNI4FZf1HHUyZjf6ou4cEvOlbt6fAYsT4NCKmlcQ== + dependencies: + orderedmap "^1.0.0" + +prosemirror-schema-list@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/prosemirror-schema-list/-/prosemirror-schema-list-1.0.1.tgz#f216e0cf4809b6074aa27912449ac89897f1ae94" + integrity sha512-AiLIX6qm6PEeDtMCKZLcSLi55WXo1ls7DnRK+4hSkoi0IIzNdxGsRlecCd3MzEu//DVz3nAEh+zEmslyW+uk8g== + dependencies: + prosemirror-model "^1.0.0" + prosemirror-transform "^1.0.0" + +prosemirror-state@^1.0.0, prosemirror-state@^1.2.1, prosemirror-state@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/prosemirror-state/-/prosemirror-state-1.2.2.tgz#8df26d95fd6fd327c0f9984a760e84d863204154" + integrity sha512-j8aC/kf9BJSCQau485I/9pj39XQoce+TqH5xzekT7WWFARTsRYFLJtiXBcCKakv1VSeev+sC3bJP0pLfz7Ft8g== + dependencies: + prosemirror-model "^1.0.0" + prosemirror-transform "^1.0.0" + +prosemirror-tables@^0.7.10, prosemirror-tables@^0.7.9: + version "0.7.10" + resolved "https://registry.yarnpkg.com/prosemirror-tables/-/prosemirror-tables-0.7.10.tgz#4b0f623422b4b8f84cdc9c559f8a87579846b3ba" + integrity sha512-VIu7UGS9keYEHs0Y6AEOTGbNE9QI2rL1OKng4vV6yoTshW/lYcb+s3hGXI12i+WLMjDVm7ujhfdWrpKpvFZOkQ== + dependencies: + prosemirror-keymap "^1.0.0" + prosemirror-model "^1.0.0" + prosemirror-state "^1.0.0" + prosemirror-transform "^1.0.0" + prosemirror-view "^1.0.0" + +prosemirror-transform@^1.0.0, prosemirror-transform@^1.1.0: + version "1.1.3" + resolved "https://registry.yarnpkg.com/prosemirror-transform/-/prosemirror-transform-1.1.3.tgz#28cfdf1f9ee514edc40466be7b7db39eed545fdf" + integrity sha512-1O6Di5lOL1mp4nuCnQNkHY7l2roIW5y8RH4ZG3hMYmkmDEWzTaFFnxxAAHsE5ipGLBSRcTlP7SsDhYBIdSuLpQ== + dependencies: + prosemirror-model "^1.0.0" + +prosemirror-utils@^0.7.5: + version "0.7.5" + resolved "https://registry.yarnpkg.com/prosemirror-utils/-/prosemirror-utils-0.7.5.tgz#11b477647b672ec8f10679ab298a5823dad6457a" + integrity sha512-F+63BUiBkUQb1S07c3rGHXjE4MDaZ5OjsNhmaO7eDdSh1lUNORTJJHrvlFEZKnLM7ChoDDXTIKhWNQwnCssQfA== + +prosemirror-view@^1.0.0, prosemirror-view@^1.1.0, prosemirror-view@^1.6.8: + version "1.6.8" + resolved "https://registry.yarnpkg.com/prosemirror-view/-/prosemirror-view-1.6.8.tgz#33fc1a6e2731633e5d6dc1af1967378f15810b74" + integrity sha512-YWX3rfji77xsU5EErt4ZoecVytYW9/4oHBYhV1MUHGMYIcppe+QZEBgRlyPMBUuu0lxdZX4m3sq7fCsDvv/MlQ== + dependencies: + prosemirror-model "^1.1.0" + prosemirror-state "^1.0.0" + prosemirror-transform "^1.1.0" + proto-list@~1.2.1: version "1.2.4" resolved "https://registry.yarnpkg.com/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849" @@ -7950,13 +8138,6 @@ prr@~1.0.1: resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476" integrity sha1-0/wRS6BplaRexok/SEzrHXj19HY= -ps-tree@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/ps-tree/-/ps-tree-1.1.0.tgz#b421b24140d6203f1ed3c76996b4427b08e8c014" - integrity sha1-tCGyQUDWID8e08dplrRCewjowBQ= - dependencies: - event-stream "~3.3.0" - pseudomap@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" @@ -7967,12 +8148,10 @@ psl@^1.1.24: resolved "https://registry.yarnpkg.com/psl/-/psl-1.1.29.tgz#60f580d360170bb722a797cc704411e6da850c67" integrity sha512-AeUmQ0oLN02flVHXWh9sSJF7mcdFq0ppid/JkErufc3hGIV/AMa8Fo9VgDo/cT2jFdOWoFvHp90qqBH54W+gjQ== -pstree.remy@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/pstree.remy/-/pstree.remy-1.1.0.tgz#f2af27265bd3e5b32bbfcc10e80bac55ba78688b" - integrity sha512-q5I5vLRMVtdWa8n/3UEzZX7Lfghzrg9eG2IKk2ENLSofKRCXVqMvMUHxCKgXNaqH/8ebhBxrqftHWnyTFweJ5Q== - dependencies: - ps-tree "^1.1.0" +pstree.remy@^1.1.6: + version "1.1.6" + resolved "https://registry.yarnpkg.com/pstree.remy/-/pstree.remy-1.1.6.tgz#73a55aad9e2d95814927131fbf4dc1b62d259f47" + integrity sha512-NdF35+QsqD7EgNEI5mkI/X+UwaxVEbQaz9f4IooEmMUv6ZPmlTQYGjBPJGgrlzNdjSvIy4MWMg6Q6vCgBO2K+w== public-encrypt@^4.0.0: version "4.0.0" @@ -8279,6 +8458,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" @@ -8525,6 +8713,11 @@ ripemd160@^2.0.0, ripemd160@^2.0.1: hash-base "^2.0.0" inherits "^2.0.1" +rope-sequence@^1.2.0: + version "1.2.2" + resolved "https://registry.yarnpkg.com/rope-sequence/-/rope-sequence-1.2.2.tgz#49c4e5c2f54a48e990b050926771e2871bcb31ce" + integrity sha1-ScTlwvVKSOmQsFCSZ3HihxvLMc4= + rsvp@^3.3.3: version "3.6.2" resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-3.6.2.tgz#2e96491599a96cde1b515d5674a8f7a91452926a" @@ -9065,13 +9258,6 @@ split-string@^3.0.1, split-string@^3.0.2: dependencies: extend-shallow "^3.0.0" -split@0.3: - version "0.3.3" - resolved "https://registry.yarnpkg.com/split/-/split-0.3.3.tgz#cd0eea5e63a211dfff7eb0f091c4133e2d0dd28f" - integrity sha1-zQ7qXmOiEd//frDwkcQTPi0N0o8= - dependencies: - through "2" - sprintf-js@~1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" @@ -9153,13 +9339,6 @@ stream-browserify@^2.0.1: inherits "~2.0.1" readable-stream "^2.0.2" -stream-combiner@~0.0.4: - version "0.0.4" - resolved "https://registry.yarnpkg.com/stream-combiner/-/stream-combiner-0.0.4.tgz#4d5e433c185261dde623ca3f44c586bcf5c4ad14" - integrity sha1-TV5DPBhSYd3mI8o/RMWGvPXErRQ= - dependencies: - duplexer "~0.1.1" - stream-each@^1.1.0: version "1.2.2" resolved "https://registry.yarnpkg.com/stream-each/-/stream-each-1.2.2.tgz#8e8c463f91da8991778765873fe4d960d8f616bd" @@ -9453,7 +9632,7 @@ through2@^2.0.0: readable-stream "^2.1.5" xtend "~4.0.1" -through@2, through@^2.3.6, through@~2.3, through@~2.3.1: +through@^2.3.6: version "2.3.8" resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= @@ -9487,6 +9666,57 @@ tiny-emitter@^2.0.0: resolved "https://registry.yarnpkg.com/tiny-emitter/-/tiny-emitter-2.0.2.tgz#82d27468aca5ade8e5fd1e6d22b57dd43ebdfb7c" integrity sha512-2NM0auVBGft5tee/OxP4PI3d8WItkDM+fPnaRAVo6xTDI2knbz9eC5ArWGqtGlYqiH3RU5yMpdyTTO7MguC4ow== +tiptap-commands@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/tiptap-commands/-/tiptap-commands-1.4.0.tgz#0cfb3ac138ee3099de56114cb119abd841fbcbe7" + integrity sha512-ytO8jFXgufK5DziamTaVojzUTolWvL4m2xNXaLkAVJYy9CWXruMK7avqeLoFYPI4GZlhleMn5i4gzYTbD7e2jA== + dependencies: + prosemirror-commands "^1.0.7" + prosemirror-inputrules "^1.0.1" + prosemirror-schema-list "^1.0.1" + prosemirror-state "^1.2.2" + tiptap-utils "^1.1.1" + +tiptap-extensions@^1.8.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/tiptap-extensions/-/tiptap-extensions-1.8.0.tgz#3067620a024f1a9e5fae4450790b143d7ebe4394" + integrity sha512-1JN9uk5QnA7DTID1j07gIBEqeOnRd6lwZ5rx/zqWXJLyreZu8VDPvP939tfP41GskO4oicGlhmsQ0aEnA5QYDw== + dependencies: + lowlight "^1.11.0" + prosemirror-history "^1.0.3" + prosemirror-state "^1.2.2" + prosemirror-tables "^0.7.10" + prosemirror-utils "^0.7.5" + prosemirror-view "^1.6.8" + tiptap "^1.8.0" + tiptap-commands "^1.4.0" + +tiptap-utils@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/tiptap-utils/-/tiptap-utils-1.1.1.tgz#e7aad3e84eb35f7abed704d15da0420029789d0d" + integrity sha512-yPIWwLFaL5a0GC7fcO7aoPlASnH3wOUQex0IlepNWbDCNycSL8shXhVx0HMN/tCnlp943zw1bwcYzpTW3wA4tw== + dependencies: + prosemirror-model "^1.6.4" + prosemirror-state "^1.2.2" + prosemirror-tables "^0.7.9" + prosemirror-utils "^0.7.5" + +tiptap@^1.8.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/tiptap/-/tiptap-1.8.0.tgz#c671188075ffa5ee4f86470f95818fd9ce6f1040" + integrity sha512-zIcVY8U1Wgj4bg3R4pX5a2BCpZUw/dTCh259VZ9g5MtClnzdLW2XpKCcwqfa9iUBEs6MCPSnB3t8jGRtGciHJg== + dependencies: + prosemirror-commands "^1.0.7" + prosemirror-dropcursor "^1.1.1" + prosemirror-gapcursor "^1.0.3" + prosemirror-inputrules "^1.0.1" + prosemirror-keymap "^1.0.1" + prosemirror-model "^1.6.4" + prosemirror-state "^1.2.1" + prosemirror-view "^1.6.8" + tiptap-commands "^1.4.0" + tiptap-utils "^1.1.1" + tmp@0.0.33, tmp@0.0.x, tmp@^0.0.33: version "0.0.33" resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" @@ -9638,6 +9868,11 @@ typescript@^2: resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.9.2.tgz#1cbf61d05d6b96269244eb6a3bce4bd914e0f00c" integrity sha512-Gr4p6nFNaoufRIY4NMdpQRNmgxVIGMs4Fcu/ujdYk3nAZqk7supzBE9idmvfZIlH/Cuj//dvi+019qEue9lV0w== +uc.micro@^1.0.1, uc.micro@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.5.tgz#0c65f15f815aa08b560a61ce8b4db7ffc3f45376" + integrity sha512-JoLI4g5zv5qNyT09f4YAvEZIIV1oOjqnewYg5D38dkQljIzpPT296dbIGvKro3digYI1bkb7W6EP1y4uDlmzLg== + uglify-js@^3.1.4: version "3.4.9" resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.4.9.tgz#af02f180c1207d76432e473ed24a28f4a782bae3" @@ -9762,15 +9997,16 @@ upath@^1.0.5: resolved "https://registry.yarnpkg.com/upath/-/upath-1.1.0.tgz#35256597e46a581db4793d0ce47fa9aebfc9fabd" integrity sha512-bzpH/oBhoS/QI/YtbkqCg6VEiPYjSZtrHQM6/QnJS6OL9pKUFLqb3aFh4Scvwm45+7iAgiMkLhSbaZxUqmrprw== -update-notifier@^2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/update-notifier/-/update-notifier-2.3.0.tgz#4e8827a6bb915140ab093559d7014e3ebb837451" - integrity sha1-TognpruRUUCrCTVZ1wFOPruDdFE= +update-notifier@^2.5.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/update-notifier/-/update-notifier-2.5.0.tgz#d0744593e13f161e406acb1d9408b72cad08aff6" + integrity sha512-gwMdhgJHGuj/+wHJJs9e6PcCszpxR1b236igrOkUofGhqJuG+amlIKwApH1IW1WWl7ovZxsX49lMBWLxSdm5Dw== dependencies: boxen "^1.2.1" chalk "^2.0.1" configstore "^3.0.0" import-lazy "^2.1.0" + is-ci "^1.0.10" is-installed-globally "^0.1.0" is-npm "^1.0.0" latest-version "^3.0.0" @@ -10041,6 +10277,11 @@ w3c-hr-time@^1.0.1: dependencies: browser-process-hrtime "^0.1.2" +w3c-keyname@^1.1.8: + version "1.1.8" + resolved "https://registry.yarnpkg.com/w3c-keyname/-/w3c-keyname-1.1.8.tgz#4e2219663760fd6535b7a1550f1552d71fc9372c" + integrity sha512-2HAdug8GTiu3b4NYhssdtY8PXRue3ICnh1IlxvZYl+hiINRq0GfNWei3XOPDg8L0PsxbmYjWVLuLj6BMRR/9vA== + walker@~1.0.5: version "1.0.7" resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.7.tgz#2f7f9b8fd10d677262b18a884e28d19618e028fb" @@ -10181,17 +10422,17 @@ webpack-stats-plugin@^0.2.1: resolved "https://registry.yarnpkg.com/webpack-stats-plugin/-/webpack-stats-plugin-0.2.1.tgz#1f5bac13fc25d62cbb5fd0ff646757dc802b8595" integrity sha512-OYMZLpZrK/qLA79NE4kC4DCt85h/5ipvWJcsefKe9MMw0qU4/ck/IJg+4OmWA+5EfrZZpHXDq92IptfYDWVfkw== -webpack@^4.28.1: - version "4.28.1" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.28.1.tgz#d0e2856e75d1224b170bf16c30b6ca9b75f0d958" - integrity sha512-qAS7BFyS5iuOZzGJxyDXmEI289h7tVNtJ5XMxf6Tz55J2riOyH42uaEsWF0F32TRaI+54SmI6qRgHM3GzsZ+sQ== +webpack@^4.29.0: + version "4.29.0" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.29.0.tgz#f2cfef83f7ae404ba889ff5d43efd285ca26e750" + integrity sha512-pxdGG0keDBtamE1mNvT5zyBdx+7wkh6mh7uzMOo/uRQ/fhsdj5FXkh/j5mapzs060forql1oXqXN9HJGju+y7w== dependencies: "@webassemblyjs/ast" "1.7.11" "@webassemblyjs/helper-module-context" "1.7.11" "@webassemblyjs/wasm-edit" "1.7.11" "@webassemblyjs/wasm-parser" "1.7.11" - acorn "^5.6.2" - acorn-dynamic-import "^3.0.0" + acorn "^6.0.5" + acorn-dynamic-import "^4.0.0" ajv "^6.1.0" ajv-keywords "^3.1.0" chrome-trace-event "^1.0.0" @@ -10442,6 +10683,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" @@ -10467,6 +10715,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" @@ -10503,10 +10769,10 @@ yargs@^12.0.4: y18n "^3.2.1 || ^4.0.0" yargs-parser "^11.1.1" -yarn-deduplicate@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/yarn-deduplicate/-/yarn-deduplicate-1.0.5.tgz#e56016f1c29e77e323f401ea838f5e8c7cdbfd42" - integrity sha512-4nds6N7dxuXcfUZAVaSUVSlI4TvwEdMaZg/DRBf/KM3iFezNBdkhcTYptcwKaecAYAfVxx3g0Ex21kssSr8YsA== +yarn-deduplicate@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/yarn-deduplicate/-/yarn-deduplicate-1.1.0.tgz#bdfdcc5a2473556c0232996424dfe039293f2f44" + integrity sha512-YTZzmzzUgDK7IllsKxgnTQ7zAGbTVnj3bnH3nxoqZ2dE0IY7NpaFpFYXR+BuBeDtxIgMhwJJvH1LTWm3k3fWpg== dependencies: "@yarnpkg/lockfile" "^1.1.0" commander "^2.10.0" |