diff options
1580 files changed, 24420 insertions, 13593 deletions
diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000000..b8e239e4049 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,72 @@ +# `build_from_dir` can't find Dockerfile when `.dockerignore` is "*" +# See https://github.com/swipely/docker-api/issues/484 +# Ignore all folders except qa/, config/initializers and the root of lib/ since +# the files we need to build the QA image are in these folders. +# Following are the files we need: +# - ./config/initializers/0_inject_enterprise_edition_module.rb +# - ./lib/gitlab.rb +# - ./qa/ +# - ./INSTALLATION_TYPE +# - ./VERSION + +/app/ +/bin/ +/builds/ +/changelogs/ +/config/environments/ +/config/helpers/ +/config/knative/ +/config/locales/ +/config/prometheus/ +/config/routes/ +/danger/ +/db/ +/doc/ +/docker/ +/ee/ +/fixtures/ +/templates/ +/lint/ +/lib/api/ +/lib/assets/ +/lib/backup/ +/lib/banzai/ +/lib/bitbucket/ +/lib/server/ +/lib/constraints/ +/lib/registry/ +/lib/policy/ +/lib/feature/ +/lib/flowdock/ +/lib/generators/ +/lib/gitaly/ +/lib/gitlab/ +/lib/api/ +/lib/token/ +/lib/mattermost/ +/lib/teams/ +/lib/storage/ +/lib/auth/ +/lib/peek/ +/lib/prometheus/ +/lib/quality/ +/lib/rouge/ +/lib/flaky/ +/lib/zip/ +/lib/sentry/ +/lib/serializers/ +/lib/support/ +/lib/check/ +/lib/tasks/ +/locale/ +/log/ +/modules/ +/plugins/ +/public/ +/rubocop/ +/scripts/ +/shared/ +/spec/ +/symbol/ +/tmp/ +/vendor/ diff --git a/.gitignore b/.gitignore index cb718a6939f..104c6930050 100644 --- a/.gitignore +++ b/.gitignore @@ -75,6 +75,8 @@ eslint-report.html /.rspec /plugins/* /.gitlab_pages_secret +/.gitlab_smime_key +/.gitlab_smime_cert package-lock.json /junit_*.xml /coverage-frontend/ diff --git a/.gitlab/ci/global.gitlab-ci.yml b/.gitlab/ci/global.gitlab-ci.yml index 78ef346d417..56fe9739d5f 100644 --- a/.gitlab/ci/global.gitlab-ci.yml +++ b/.gitlab/ci/global.gitlab-ci.yml @@ -76,3 +76,15 @@ - apk add --update openssl - wget $CI_PROJECT_URL/raw/$CI_COMMIT_SHA/scripts/$SCRIPT_NAME - chmod 755 $(basename $SCRIPT_NAME) + +.review-only: &review-only + only: + refs: + - branches@gitlab-org/gitlab-ce + - branches@gitlab-org/gitlab-ee + kubernetes: active + except: + refs: + - master + - /^\d+-\d+-auto-deploy-\d+$/ + - /(^docs[\/-].+|.+-docs$)/ diff --git a/.gitlab/ci/qa.gitlab-ci.yml b/.gitlab/ci/qa.gitlab-ci.yml index dcc681294d2..144e5392e55 100644 --- a/.gitlab/ci/qa.gitlab-ci.yml +++ b/.gitlab/ci/qa.gitlab-ci.yml @@ -15,13 +15,13 @@ - branches@gitlab-org/gitlab-ce - branches@gitlab-org/gitlab-ee -package-and-qa: - extends: .package-and-qa-base +package-and-qa-manual: + extends: + - .package-and-qa-base + - .no-docs-and-no-qa when: manual - except: - - /(^qa[\/-].*|.*-qa$)/ -package-and-qa-always: +package-and-qa: extends: .package-and-qa-base allow_failure: true only: diff --git a/.gitlab/ci/reports.gitlab-ci.yml b/.gitlab/ci/reports.gitlab-ci.yml index ca55bbd32a7..ccd7d6ec84d 100644 --- a/.gitlab/ci/reports.gitlab-ci.yml +++ b/.gitlab/ci/reports.gitlab-ci.yml @@ -2,6 +2,7 @@ include: - template: Code-Quality.gitlab-ci.yml - template: Security/SAST.gitlab-ci.yml - template: Security/Dependency-Scanning.gitlab-ci.yml + - template: Security/DAST.gitlab-ci.yml code_quality: extends: .dedicated-no-docs @@ -24,3 +25,14 @@ dependency_scanning: tags: [] before_script: [] cache: {} + +dast: + extends: + - .dedicated-runner + - .review-only + stage: qa + dependencies: + - review-deploy + before_script: + - export DAST_WEBSITE="$(cat review_app_url.txt)" + cache: {} diff --git a/.gitlab/ci/review.gitlab-ci.yml b/.gitlab/ci/review.gitlab-ci.yml index 4a9269ffd82..beb049c0b3b 100644 --- a/.gitlab/ci/review.gitlab-ci.yml +++ b/.gitlab/ci/review.gitlab-ci.yml @@ -1,15 +1,3 @@ -.review-only: &review-only - only: - refs: - - branches@gitlab-org/gitlab-ce - - branches@gitlab-org/gitlab-ee - kubernetes: active - except: - refs: - - master - - /^\d+-\d+-auto-deploy-\d+$/ - - /(^docs[\/-].+|.+-docs$)/ - .review-schedules-only: &review-schedules-only only: refs: @@ -24,8 +12,9 @@ - /(^docs[\/-].+|.+-docs$)/ .review-base: &review-base - extends: .dedicated-runner - <<: *review-only + extends: + - .dedicated-runner + - .review-only image: registry.gitlab.com/gitlab-org/gitlab-build-images:gitlab-charts-build-base cache: {} dependencies: [] @@ -50,7 +39,7 @@ build-qa-image: <<: *review-docker stage: test script: - - time docker build --cache-from ${LATEST_QA_IMAGE} --tag ${QA_IMAGE} ./qa/ + - time docker build --cache-from ${LATEST_QA_IMAGE} --tag ${QA_IMAGE} --file ./qa/Dockerfile ./ - echo "${CI_JOB_TOKEN}" | docker login --username gitlab-ci-token --password-stdin ${CI_REGISTRY} - time docker push ${QA_IMAGE} @@ -68,7 +57,7 @@ build-qa-image: - BUILD_TRIGGER_TOKEN=$REVIEW_APPS_BUILD_TRIGGER_TOKEN ./scripts/trigger-build cng review-build-cng: - <<: *review-only + extends: .review-only <<: *review-build-cng-base schedule:review-build-cng: @@ -117,8 +106,9 @@ schedule:review-deploy: <<: *review-schedules-only review-stop: - <<: *review-only - extends: .single-script-job-dedicated-runner + extends: + - .single-script-job-dedicated-runner + - .review-only image: registry.gitlab.com/gitlab-org/gitlab-build-images:gitlab-charts-build-base stage: review when: manual @@ -181,7 +171,9 @@ review-qa-all: - gitlab-qa Test::Instance::Any "${QA_IMAGE}" "${CI_ENVIRONMENT_URL}" -- --format RspecJunitFormatter --out tmp/rspec-${CI_JOB_ID}.xml --format html --out tmp/rspec.htm --color --format documentation parallel-spec-reports: - extends: .dedicated-runner + extends: + - .dedicated-runner + - .no-docs dependencies: - review-qa-all image: ruby:2.6-alpine diff --git a/.gitlab/issue_templates/Feature Flag Roll Out.md b/.gitlab/issue_templates/Feature Flag Roll Out.md index b7db5a33faf..0cac769bd55 100644 --- a/.gitlab/issue_templates/Feature Flag Roll Out.md +++ b/.gitlab/issue_templates/Feature Flag Roll Out.md @@ -39,5 +39,6 @@ If applicable, any groups/projects that are happy to have this feature turned on - [ ] Cross post chatops slack command to `#support_gitlab-com` and in your team channel - [ ] Announce on the issue that the flag has been enabled - [ ] Remove feature flag and add changelog entry +- [ ] After the flag removal is deployed, [clean up the feature flag](https://docs.gitlab.com/ee/development/feature_flags/controls.html#cleaning-up) by running chatops command in `#production` channel /label ~"feature flag" diff --git a/.gitlab/issue_templates/Feature proposal.md b/.gitlab/issue_templates/Feature proposal.md index 8a49715e0e8..2ba6b68a53e 100644 --- a/.gitlab/issue_templates/Feature proposal.md +++ b/.gitlab/issue_templates/Feature proposal.md @@ -26,7 +26,7 @@ Add all known Documentation Requirements here, per https://docs.gitlab.com/ee/de ### Testing -<!-- What risks does this change pose? How might it affect the quality of the product? What additional test coverage or changes to tests will be needed? Will it require cross-browser testing? See the test engineering process for further guidelines: https://about.gitlab.com/handbook/engineering/quality/guidelines/test-engineering/ --> +<!-- What risks does this change pose? How might it affect the quality of the product? What additional test coverage or changes to tests will be needed? Will it require cross-browser testing? See the test engineering process for further help: https://about.gitlab.com/handbook/engineering/quality/test-engineering/ --> ### What does success look like, and how can we measure that? diff --git a/.gitlab/issue_templates/Test plan.md b/.gitlab/issue_templates/Test plan.md index 3aedd5859d3..f194adebc87 100644 --- a/.gitlab/issue_templates/Test plan.md +++ b/.gitlab/issue_templates/Test plan.md @@ -89,7 +89,7 @@ New end-to-end and integration tests (Selenium and API) should be added to the Please note if automated tests already exist. -When adding new automated tests, please keep [testing levels](https://docs.gitlab.com/ce/development/testing_guide/testing_levels.html) +When adding new automated tests, please keep [testing levels](https://docs.gitlab.com/ee/development/testing_guide/testing_levels.html) in mind. --> diff --git a/.mdlrc.style b/.mdlrc.style index b315c99e3fc..85bc3aaa99b 100644 --- a/.mdlrc.style +++ b/.mdlrc.style @@ -18,7 +18,7 @@ rule "MD025" rule "MD028" rule "MD029", :style => :one rule "MD030" -rule "MD032" +# rule "MD032" rule "MD034" rule "MD037" rule "MD038" diff --git a/.rubocop.yml b/.rubocop.yml index 79e06439ac2..b75c63e1f58 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -8,7 +8,7 @@ require: - rubocop-rspec AllCops: - TargetRubyVersion: 2.5 + TargetRubyVersion: 2.6 TargetRailsVersion: 5.0 Exclude: - 'vendor/**/*' diff --git a/CHANGELOG.md b/CHANGELOG.md index 267a1caafec..8e343c4fd39 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,315 @@ documentation](doc/development/changelog.md) for instructions on adding your own entry. +## 12.2.3 + +### Security (22 changes) + +- Ensure only authorised users can create notes on Merge Requests and Issues. +- Gitaly: ignore git redirects. +- Add :login_recaptcha_protection_enabled setting to prevent bots from brute-force attacks. +- Speed up regexp in namespace format by failing fast after reaching maximum namespace depth. +- Limit the size of issuable description and comments. +- Send TODOs for comments on commits correctly. +- Restrict MergeRequests#test_reports to authenticated users with read-access on Builds. +- Added image proxy to mitigate potential stealing of IP addresses. +- Filter out old system notes for epics in notes api endpoint response. +- Avoid exposing unaccessible repo data upon GFM post processing. +- Fix HTML injection for label description. +- Make sure HTML text is always escaped when replacing label/milestone references. +- Prevent DNS rebind on JIRA service integration. +- Use admin_group authorization in Groups::RunnersController. +- Prevent disclosure of merge request ID via email. +- Show cross-referenced MR-id in issues' activities only to authorized users. +- Enforce max chars and max render time in markdown math. +- Check permissions before responding in MergeController#pipeline_status. +- Remove EXIF from users/personal snippet uploads. +- Fix project import restricted visibility bypass via API. +- Fix weak session management by clearing password reset tokens after login (username/email) are updated. +- Fix SSRF via DNS rebinding in Kubernetes Integration. + + +## 12.2.2 + +### Security (22 changes) + +- Ensure only authorised users can create notes on Merge Requests and Issues. +- Gitaly: ignore git redirects. +- Add :login_recaptcha_protection_enabled setting to prevent bots from brute-force attacks. +- Speed up regexp in namespace format by failing fast after reaching maximum namespace depth. +- Limit the size of issuable description and comments. +- Send TODOs for comments on commits correctly. +- Restrict MergeRequests#test_reports to authenticated users with read-access on Builds. +- Added image proxy to mitigate potential stealing of IP addresses. +- Filter out old system notes for epics in notes api endpoint response. +- Avoid exposing unaccessible repo data upon GFM post processing. +- Fix HTML injection for label description. +- Make sure HTML text is always escaped when replacing label/milestone references. +- Prevent DNS rebind on JIRA service integration. +- Use admin_group authorization in Groups::RunnersController. +- Prevent disclosure of merge request ID via email. +- Show cross-referenced MR-id in issues' activities only to authorized users. +- Enforce max chars and max render time in markdown math. +- Check permissions before responding in MergeController#pipeline_status. +- Remove EXIF from users/personal snippet uploads. +- Fix project import restricted visibility bypass via API. +- Fix weak session management by clearing password reset tokens after login (username/email) are updated. +- Fix SSRF via DNS rebinding in Kubernetes Integration. + + +## 12.2.1 + +### Fixed (3 changes) + +- Fix for embedded metrics undefined params. !31975 +- Fix "ERR value is not an integer or out of range" errors. !32126 +- Prevent duplicated trigger action button. + +### Performance (1 change) + +- Fix Gitaly N+1 calls with listing issues/MRs via API. !31938 + + +## 12.2.0 + +### Security (4 changes, 1 of them is from the community) + +- Update mini_magick to 4.9.5. !31505 (Takuya Noguchi) +- Upgrade Rugged to 0.28.3. !31794 +- Queries for Upload should be scoped by model. +- Restrict slash commands to users who can log in. + +### Removed (3 changes) + +- Remove Kubernetes service integration page. !31365 +- Remove line profiler from performance bar. +- Remove GC metrics from performance bar. + +### Fixed (74 changes, 4 of them are from the community) + +- Resolve Incorrect empty state message on Explore projects. !25578 +- Search issuables by iids. !28302 (Riccardo Padovani) +- Make it easier to find invited group members. !28436 +- fix: updates to include units for the y axis label. !30330 +- Align access permissions for wiki history to those of wiki pages. !30470 +- Add index for issues on relative position, project, and state for manual sorting. !30542 +- Fix suggestion on lines that are not part of an MR. !30606 +- Add empty chart component. !30682 +- Remove blank block from job sidebar. !30754 +- Remove duplicate buttons in diff discussion. !30757 +- Order projects in 'Move issue' dropdown by name. !30778 +- Fix bug in dashboard display of closed milestones. !30820 +- Fixes alignment issues with reports. !30839 +- Ensure visibility icons in group/project listings are grey. !30858 +- Fix admin labels page when there are invalid records. !30885 +- Extra logging for new live trace architecture. !30892 +- Fix pipeline emails not respecting group notification email setting. !30907 +- Handle trailing slashes when generating Jira issue URLs. !30911 +- Optimize relative re-positioning when moving issues. !30938 +- Better support clickable tasklists inside blockquotes. !30952 +- Add space to "merged by" widget. !30972 +- Remove duplicated mapping key in config/locales/en.yml. !30980 (Peter Dave Hello) +- Update Mermaid to v8.2.3. !30985 +- Use persistent Redis cluster for Workhorse pub/sub notifications. !30990 +- Remove :livesum from RubySampler metrics. !31047 +- Fix pid discovery for Unicorn processes in `PidProvider`. !31056 +- Respect group notification email when sending group access notifications. !31089 +- Default dependency job stage index to Infinity, and correctly report it as undefined in prior stages. !31116 +- Fix incorrect use of message interpolation. !31121 +- Moved labels out of fields on Search page. !31137 +- Ensure Warden triggers after_authentication callback. !31138 +- Fix admin area user access level radio button labels. !31154 +- Ignore Gitaly errors if cache flushing fails on project destruction. !31164 +- Prevent double slash in review apps path. !31212 +- Make pdf.js render CJK characters. !31220 +- Prevent discussion filter from persisting to `Show all activity` when opening links to notes. !31229 +- Improve layout of dropdowns in the metrics dashboard page. !31239 +- Remove pdf.js deprecation warnings. !31253 +- Fix GC::Profiler metrics fetching. !31331 +- Jupyter fixes. !31332 (Amit Rathi) +- Fix first-time contributor notes not rendering. !31340 +- Fix inline rendering of relative paths to SVGs from the current repository. !31352 +- Make `bin/web_puma` consider RAILS_ENV. !31378 +- Removed extrenal dashboard legend border. !31407 +- Fix visual review app storage keys. !31427 +- Fix flashing conflict warning when editing issues. !31469 +- Fix broken issue links and possible 500 error on cycle analytics page when project name and path are different. !31471 +- Prevent turning plain links into embedded when moving issues. !31489 +- Add a field for released_at to GH importer. !31496 +- Adjust size and align MR-widget loading icon. !31503 +- Fix an issue where clicking outside the MR/branch search box in WebIDE closed the dropdown. !31523 +- Don't attempt to contact registry if it is disabled. !31553 +- Fix IDE new files icon in tree. !31560 +- Fix missing author line (`Created by: <user>`) in MRs/issues/comments of imported Bitbucket Cloud project. !31579 +- Add missing report-uri to CSP config. !31593 +- Fixed display of some sections and externalized all text in the shortcuts modal overlay. !31594 +- Remove extra padding from disabled comment box. !31603 +- Allow CI to clone public projects when HTTP protocol is disabled. !31632 +- error message for general settings. !31636 (Mesut Güneş) +- Invalidate branches cache on PostReceive. !31653 +- Fix active metric files being wiped after the app starts. !31668 +- Fix :wiki_can_not_be_created_total counter. !31673 +- Fix job logs where style changes were broken down into separate lines. !31674 +- Properly save suggestions in project exports. !31690 +- Fix project avatar image in Slack pipeline notifications. !31788 +- Fix empty error flash message on profile:account page when updating username with username that has already been taken. !31809 +- Fix starrers counts after searching. !31823 +- Fix pipelines not always being created after a push. !31927 +- Fix 500 errors in commits api caused by empty ref_name parameter. +- Center loading icon in CI action component. +- Prevents showing 2 tooltips in pipelines table. +- Fix tag page layout. +- Prevent duplicated trigger action button. +- Hides loading spinner in pipelines actions after request has been fullfiled. + +### Changed (31 changes, 5 of them are from the community) + +- Update cluster page automatically when cluster is created. !27189 +- Add branch/tags/commits dropdown filter on the search page for searching codes. !28282 (minghuan lei) +- Add support for start_sha to commits API. !29598 +- Maintainers can create subgroups. !29718 (Fabio Papa) +- Extract Auto DevOps deploy functions into a base image. !30404 +- Add MR form to Visual Review (EE) runtime configuration. !30481 +- Adjust redis cache metrics. !30572 +- Add DS_PIP_DEPENDENCY_PATH option to configure Dependency Scanning for projects using pip. !30762 +- Bring scoped environment variables to core. !30779 +- Add Web IDE Usage Ping for Create SMAU. !30800 +- Update the container scanning CI template to use v12 of the clair scanner. !30809 +- Multiple pipeline support for Commit status. !30828 (Gaetan Semet) +- Add support for exporting repository type data for LFS objects. !30830 +- Avoid increasing redis counters when usage_ping is disabled. !30949 +- Added navbar searches usage ping counter. !30953 +- Convert githost.log to JSON format. !30967 +- Adjusted the clickable area of collapsed sidebar elements. !30974 (Michel Engelen) +- Mark push mirrors as failed after 1 hour. !30999 +- Allows masking @ and : characters. !31065 +- Remove incorrect fallback when determining which cluster to use when retrieving MR performance metrics. !31126 +- Retry push mirrors faster when running concurrently, improve error handling when push mirrors fail. !31247 +- Make issue boards importable. !31434 (Jason Colyer) +- Allow users to resend a confirmation link when the grace period has expired. !31476 +- Remove counts from default labels API responses. !31543 +- Upgrade to Gitaly v1.57.0. !31568 +- Rename githost.log -> git_json.log. !31634 +- Load search result counts asynchronously. !31663 +- feat: adds a download to csv functionality to the dropdown in prometheus metrics. !31679 +- Adjust copy for adding additional members. !31726 +- Upgrade to Gitaly v1.59.0. !31743 +- Filter title, description, and body parameters from logs. + +### Performance (17 changes, 1 of them is from the community) + +- Add partial index on identities table to speed up LDAP lookups. !26710 +- Improve MembersFinder query performance using UNION. !30451 (Jacopo Beschi @jacopo-beschi) +- Rake task to cleanup expired ActiveSession lookup keys. !30668 +- Update usage ping cron behavior. !30842 +- Make Bootsnap available via ENABLE_BOOTSNAP=1. !30963 +- Batch processing of commit refs in markdown processing. !31037 +- Use tablesample approximate counting by default. !31048 +- Create index on environments by state. !31231 +- Split MR widget into etag-cached and non-cached serializers. !31354 +- Speed up loading and filtering deploy keys and their projects. !31384 +- Only track Redis calls if Peek is enabled. !31438 +- Only expire tag cache once per push. !31641 +- Reduce Gitaly calls in PostReceive. !31741 +- Eliminate many Gitaly calls in discussions API. !31834 +- Optimize DB indexes for ES indexing of notes. !31846 +- Expire project caches once per push instead of once per ref. !31876 +- Look up upstream commits once before queuing ProcessCommitWorkers. + +### Added (51 changes, 11 of them are from the community) + +- Make starred projects and starrers of a project publicly visible. !24690 +- Make quick action commands applied banner more useful. !26672 (Jacopo Beschi @jacopo-beschi) +- Allow Helm to be uninstalled from the UI. !27359 +- Improve pipeline status Slack notifications. !27683 +- Add links to relevant configuration areas in admin area overview. !29306 +- Display project id on project admin page. !29734 (Zsolt Kovari) +- Display group id on group admin page. !29735 (Zsolt Kovari) +- Resolve Keyboard shortcut for jump to NEXT unresolved discussion. !30144 +- Personal access tokens are accepted using OAuth2 header format. !30277 +- Add Outbound requests whitelist for local networks. !30350 (Istvan Szalai) +- Allow multiple Auto DevOps projects to deploy to a single namespace within a k8s cluster. !30360 (James Keogh) +- Allow Knative to be uninstalled from the UI. !30458 +- Add admin-configurable "Support page URL" link to top Help dropdown menu. !30459 (Diego Louzán) +- Allow specifying variables when running manual jobs. !30485 +- Use predictable environment slugs. !30551 +- Return an ETag header for the archive endpoint. !30581 +- Add Rate Request Limiter to RawController#show endpoint. !30635 +- Add git blame to GitLab API. !30675 (Oleg Zubchenko) +- Use separate Kubernetes namespaces per environment. !30711 +- Support remove source branch on merge w/ push options. !30728 +- Deploy serverless apps with gitlabktl. !30740 +- Adjust group level analytics to accept multiple ids. !30744 +- Adds event enum column to DesignsVersions join table. !30745 +- Allow email notifications to be disabled for all members of a group or project. !30755 (Dustin Spicuzza) +- Export and download CSV from metrics charts. !30760 +- Add API endpoints to return container repositories and tags from the group level. !30817 +- Add support for deferred links in persistent user callouts. !30818 +- Add system notes for when a Zoom call was added/removed from an issue. !30857 (Jacopo Beschi @jacopo-beschi) +- Count wiki creation, update and delete events. !30864 +- Add new expansion options for merge request diffs. !30927 +- Count snippet creation, update and comment events. !30930 +- Update namespace label for GitLab-managed clusters. !30935 +- UI for disabling group/project email notifications. !30961 (Dustin Spicuzza) +- Support setting of merge request title and description using git push options. !31068 +- Add new table to store email domain per group. !31071 +- Redirect from a project wiki git route to the project wiki home. !31085 +- Link and embed metrics in GitLab Flavored Markdown. !31106 +- Moves snowplow tracking from ee to ce. !31160 (jejacks0n) +- Allow Cert-Manager to be uninstalled. !31166 +- Add new outbound network requests application setting for system hooks. !31177 +- Allow links to metrics dashboard at a specific time. !31283 +- Enable embedding of specific metrics charts in GFM. !31304 +- Support creating DAGs in CI config through the `needs` key. !31328 +- Generate shareable link for specific metric charts. !31339 +- Add support for Content-Security-Policy. !31402 +- Add BitBucketServer project import filtering. !31420 +- Embed specific metrics chart in issue. !31644 +- Track page views for cycle analytics show page. !31717 +- Add usage pings for source code pushes. !31734 +- Makes collapsible title clickable in job log. +- Adds highlight to the collapsible section. + +### Other (36 changes, 9 of them are from the community) + +- Rewrite `if:` argument in before_action and alike when `only:` is also used. !24412 (George Thomas @thegeorgeous) +- Create rake tasks for migrating legacy uploads out of deprecated paths. !29409 +- Remove the warning style from the U2F device message in user settings > account. !30119 (matejlatin) +- Set visibility level 'Private' for restricted 'Internal' imported projects when 'Internal' visibility setting is restricted in admin settings. !30522 +- Change BoardService in favor of boardsStore on board blank state of the component board. !30546 (eduarmreyes) +- Adds Sidekiq scheduling latency structured logging field. !30784 +- Adds chaos endpoints to Sidekiq. !30814 +- Added multi-select deletion of container registry images. !30837 +- When GitLab import fails during importer user mapping step, add an explicit error message mentioning importer. !30838 +- Add Rugged calls and duration to API and Rails logs. !30871 +- Fixed distorted avatars when resource not reachable. !30904 (Marc Schwede) +- Update GitLab Runner Helm Chart to 0.7.0. !30950 +- Use Rails 5.2 Redis caching store. !30966 +- Add Rugged calls to performance bar. !30983 +- add color selector to broadcast messages form. !30988 +- Harmonize selections in user settings. !31110 (Marc Schwede) +- Update rouge to v3.7.0. !31254 +- Update 'Ruby on Rails' project template. !31310 +- Fix mirroring help text. !31348 (jramsay) +- Enhance style of the shared runners limit. !31386 +- Enables storage statistics for root namespaces on database. !31392 +- Improve quick action error messages. !31451 +- Enable authenticated cookie encryption. !31463 +- Update karma to 4.2.0. !31495 (Takuya Noguchi) +- Add max_replication_slots to PG HA documentation. !31534 +- Create database tables for the new cycle analytics backend. !31621 +- Updated the detached pipeline badge tooltip text to offer a better explanation. !31626 +- Add Gitaly and Rugged call timing in Sidekiq logs. !31651 +- Fix the style-lint errors and warnings for `app/assets/stylesheets/pages/wiki.scss`. !31656 +- Update GraphicsMagick from 1.3.29 to 1.3.33 for CI tests. !31692 (Takuya Noguchi) +- Migrate remaining users with null private_profile. !31708 +- Bump Helm to 2.14.3 and kubectl to 1.11.10 for Kubernetes integration. !31716 +- Updated the personal access token api scope description to reflect the permissions it grants. !31759 +- Add finished_at to the internal API Deployment entity. !31808 +- Remove Security Dashboard feature flag. !31820 +- Update Packer.gitlab-ci.yml to use latest image. (Kelly Hair) + + ## 12.1.5 ### Security (2 changes) @@ -338,6 +647,34 @@ entry. - Removes EE differences for app/views/admin/users/show.html.haml. +## 12.0.7 + +### Security (22 changes) + +- Ensure only authorised users can create notes on Merge Requests and Issues. +- Add :login_recaptcha_protection_enabled setting to prevent bots from brute-force attacks. +- Queries for Upload should be scoped by model. +- Speed up regexp in namespace format by failing fast after reaching maximum namespace depth. +- Limit the size of issuable description and comments. +- Send TODOs for comments on commits correctly. +- Restrict MergeRequests#test_reports to authenticated users with read-access on Builds. +- Added image proxy to mitigate potential stealing of IP addresses. +- Filter out old system notes for epics in notes api endpoint response. +- Avoid exposing unaccessible repo data upon GFM post processing. +- Fix HTML injection for label description. +- Make sure HTML text is always escaped when replacing label/milestone references. +- Prevent DNS rebind on JIRA service integration. +- Use admin_group authorization in Groups::RunnersController. +- Prevent disclosure of merge request ID via email. +- Show cross-referenced MR-id in issues' activities only to authorized users. +- Enforce max chars and max render time in markdown math. +- Check permissions before responding in MergeController#pipeline_status. +- Remove EXIF from users/personal snippet uploads. +- Fix project import restricted visibility bypass via API. +- Fix weak session management by clearing password reset tokens after login (username/email) are updated. +- Fix SSRF via DNS rebinding in Kubernetes Integration. + + ## 12.0.6 - No changes. diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index f269cd1b8b6..91951fd8ad7 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -1.58.0
\ No newline at end of file +1.61.0 diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION index b13d146a7b0..ccfb75e5120 100644 --- a/GITLAB_SHELL_VERSION +++ b/GITLAB_SHELL_VERSION @@ -1 +1 @@ -9.3.0 +9.4.1 @@ -51,6 +51,7 @@ gem 'jwt', '~> 2.1.0' # Spam and anti-bot protection gem 'recaptcha', '~> 4.11', require: 'recaptcha/rails' gem 'akismet', '~> 2.0' +gem 'invisible_captcha', '~> 0.12.1' # Two-factor authentication gem 'devise-two-factor', '~> 3.0.0' @@ -274,7 +275,6 @@ gem 'font-awesome-rails', '~> 4.7' gem 'gemojione', '~> 3.3' gem 'gon', '~> 6.2' gem 'request_store', '~> 1.3' -gem 'virtus', '~> 1.0.1' gem 'base32', '~> 0.3.0' # Sentry integration @@ -283,7 +283,7 @@ gem 'sentry-raven', '~> 2.9' gem 'premailer-rails', '~> 1.9.7' # LabKit: Tracing and Correlation -gem 'gitlab-labkit', '~> 0.4.2' +gem 'gitlab-labkit', '~> 0.5' # I18n gem 'ruby_parser', '~> 3.8', require: false @@ -297,6 +297,9 @@ gem 'batch-loader', '~> 1.4.0' # Perf bar gem 'peek', '~> 1.0.1' +# Snowplow events tracking +gem 'snowplow-tracker', '~> 0.6.1' + # Memory benchmarks gem 'derailed_benchmarks', require: false @@ -371,8 +374,6 @@ group :development, :test do gem 'license_finder', '~> 5.4', require: false gem 'knapsack', '~> 1.17' - gem 'activerecord_sane_schema_dumper', '1.0' - gem 'stackprof', '~> 0.2.10', require: false gem 'simple_po_parser', '~> 1.1.2', require: false @@ -398,7 +399,7 @@ gem 'mail_room', '~> 0.9.1' gem 'email_reply_trimmer', '~> 0.1' gem 'html2text' -gem 'ruby-prof', '~> 0.17.0' +gem 'ruby-prof', '~> 1.0.0' gem 'rbtrace', '~> 0.4', require: false gem 'memory_profiler', '~> 0.9', require: false gem 'benchmark-memory', '~> 0.1', require: false @@ -437,6 +438,7 @@ gem 'toml-rb', '~> 1.0.0', require: false gem 'flipper', '~> 0.13.0' gem 'flipper-active_record', '~> 0.13.0' gem 'flipper-active_support_cache_store', '~> 0.13.0' +gem 'unleash', '~> 0.1.5' # Structured logging gem 'lograge', '~> 0.5' diff --git a/Gemfile.lock b/Gemfile.lock index 6aa96d54abb..a6a44cc6960 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -41,8 +41,6 @@ GEM activerecord-explain-analyze (0.1.0) activerecord (>= 4) pg - activerecord_sane_schema_dumper (1.0) - rails (>= 5, < 6) activestorage (5.2.3) actionpack (= 5.2.3) activerecord (= 5.2.3) @@ -152,6 +150,7 @@ GEM concurrent-ruby-ext (1.1.5) concurrent-ruby (= 1.1.5) connection_pool (2.2.2) + contracts (0.11.0) crack (0.4.3) safe_yaml (~> 1.0.0) crass (1.0.4) @@ -312,12 +311,13 @@ GEM gitaly (1.58.0) grpc (~> 1.0) github-markup (1.7.0) - gitlab-labkit (0.4.2) + gitlab-labkit (0.5.2) actionpack (~> 5) activesupport (~> 5) grpc (~> 1.19) jaeger-client (~> 0.10) opentracing (~> 0.4) + redis (> 3.0.0, < 5.0.0) gitlab-markup (1.7.0) gitlab-sidekiq-fetcher (0.5.1) sidekiq (~> 5) @@ -437,6 +437,8 @@ GEM influxdb (0.2.3) cause json + invisible_captcha (0.12.1) + rails (>= 3.2.0) ipaddress (0.8.3) jaeger-client (0.10.0) opentracing (~> 0.3) @@ -533,6 +535,7 @@ GEM multi_json (1.13.1) multi_xml (0.6.0) multipart-post (2.0.0) + murmurhash3 (0.1.6) mustermann (1.0.3) mustermann-grape (1.0.0) mustermann (~> 1.0.0) @@ -711,7 +714,7 @@ GEM rails-dom-testing (2.0.3) activesupport (>= 4.2.0) nokogiri (>= 1.6) - rails-html-sanitizer (1.0.4) + rails-html-sanitizer (1.2.0) loofah (~> 2.2, >= 2.2.2) rails-i18n (5.1.1) i18n (>= 0.7, < 2) @@ -834,7 +837,7 @@ GEM i18n ruby-fogbugz (0.2.1) crack (~> 0.4) - ruby-prof (0.17.0) + ruby-prof (1.0.0) ruby-progressbar (1.10.1) ruby-saml (1.7.2) nokogiri (>= 1.5.10) @@ -843,7 +846,7 @@ GEM rubyntlm (0.6.2) rubypants (0.2.0) rubyzip (1.2.2) - rugged (0.28.2) + rugged (0.28.3.1) safe_yaml (1.0.4) sanitize (4.6.6) crass (~> 1.0.2) @@ -901,6 +904,8 @@ GEM simplecov-html (~> 0.10.0) simplecov-html (0.10.2) slack-notifier (1.5.1) + snowplow-tracker (0.6.1) + contracts (~> 0.7, <= 0.11) spring (2.0.2) activesupport (>= 4.2) spring-commands-rspec (1.0.4) @@ -968,6 +973,8 @@ GEM get_process_mem (~> 0) unicorn (>= 4, < 6) uniform_notifier (1.10.0) + unleash (0.1.5) + murmurhash3 (~> 0.1.6) unparser (0.4.5) abstract_type (~> 0.0.7) adamantium (~> 0.2.0) @@ -1023,7 +1030,6 @@ DEPENDENCIES ace-rails-ap (~> 4.1.0) acme-client (~> 2.0.2) activerecord-explain-analyze (~> 0.1) - activerecord_sane_schema_dumper (= 1.0) acts-as-taggable-on (~> 6.0) addressable (~> 2.5.2) akismet (~> 2.0) @@ -1098,7 +1104,7 @@ DEPENDENCIES gettext_i18n_rails_js (~> 1.3) gitaly (~> 1.58.0) github-markup (~> 1.7.0) - gitlab-labkit (~> 0.4.2) + gitlab-labkit (~> 0.5) gitlab-markup (~> 1.7.0) gitlab-sidekiq-fetcher (= 0.5.1) gitlab-styles (~> 2.7) @@ -1126,6 +1132,7 @@ DEPENDENCIES httparty (~> 0.16.4) icalendar influxdb (~> 0.2) + invisible_captcha (~> 0.12.1) jira-ruby (~> 1.4) js_regex (~> 3.1) json-schema (~> 2.8.0) @@ -1211,7 +1218,7 @@ DEPENDENCIES rubocop-performance (~> 1.1.0) rubocop-rspec (~> 1.22.1) ruby-fogbugz (~> 0.2.1) - ruby-prof (~> 0.17.0) + ruby-prof (~> 1.0.0) ruby-progressbar ruby_parser (~> 3.8) rubyzip (~> 1.2.2) @@ -1229,6 +1236,7 @@ DEPENDENCIES simple_po_parser (~> 1.1.2) simplecov (~> 0.16.1) slack-notifier (~> 1.5.1) + snowplow-tracker (~> 0.6.1) spring (~> 2.0.0) spring-commands-rspec (~> 1.0.4) sprockets (~> 3.7.0) @@ -1246,9 +1254,9 @@ DEPENDENCIES unf (~> 0.1.4) unicorn (~> 5.4.1) unicorn-worker-killer (~> 0.4.4) + unleash (~> 0.1.5) validates_hostname (~> 1.0.6) version_sorter (~> 2.2.4) - virtus (~> 1.0.1) vmstat (~> 2.3.0) webmock (~> 3.5.1) webpack-rails (~> 0.9.10) @@ -1,4 +1,12 @@ -Copyright GitLab B.V. +Copyright (c) 2011-present GitLab B.V. + +Portions of this software are licensed as follows: + +* All content residing under the "doc/" directory of this repository is licensed under "Creative Commons: CC BY-SA 4.0 license". +* All content that resides under the "ee/" directory of this repository, if that directory exists, is licensed under the license defined in "ee/LICENSE". +* All client-side JavaScript (when served directly or after being compiled, arranged, augmented, or combined), is licensed under the "MIT Expat" license. +* All third party components incorporated into the GitLab Software are licensed under the original license provided by the owner of the applicable component. +* Content outside of the above mentioned directories or restrictions above is available under the "MIT Expat" license as defined below. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 73bfcc988cc..054e2d02461 100644 --- a/README.md +++ b/README.md @@ -20,10 +20,6 @@ To see how GitLab looks please see the [features page on our website](https://ab - 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) -## Hiring - -We're hiring developers, support people, and production engineers all the time, please see our [jobs page](https://about.gitlab.com/jobs/). - ## Editions There are two editions of GitLab: @@ -31,6 +27,15 @@ There are two editions of GitLab: - GitLab Community Edition (CE) is available freely under the MIT Expat license. - GitLab Enterprise Edition (EE) includes [extra features](https://about.gitlab.com/pricing/#compare-options) that are more useful for organizations with more than 100 users. To use EE and get official support please [become a subscriber](https://about.gitlab.com/pricing/). +## Licensing + +See the [LICENSE](LICENSE) file for licensing information as it pertains to +files in this repository. + +## Hiring + +We're hiring developers, support people, and production engineers all the time, please see our [jobs page](https://about.gitlab.com/jobs/). + ## Website On [about.gitlab.com](https://about.gitlab.com/) you can find more information about: @@ -58,14 +63,6 @@ There are various other options to install GitLab, please refer to the [installa GitLab is an open source project and we are very happy to accept community contributions. Please refer to [Contributing to GitLab page](https://about.gitlab.com/contributing/) for more details. -## Licensing - -GitLab Community Edition (CE) is available freely under the MIT Expat license. - -All third party components incorporated into the GitLab Software are licensed under the original license provided by the owner of the applicable component. - -All Documentation content that resides under the `doc/` directory of this repository is licensed under Creative Commons: CC BY-SA 4.0. - ## Install a development environment To work on GitLab itself, we recommend setting up your development environment with [the GitLab Development Kit](https://gitlab.com/gitlab-org/gitlab-development-kit). @@ -1 +1 @@ -12.2.0-pre +12.3.0-pre diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js index a649c521405..136ffdf8b9d 100644 --- a/app/assets/javascripts/api.js +++ b/app/assets/javascripts/api.js @@ -14,6 +14,7 @@ const Api = { projectPath: '/api/:version/projects/:id', forkedProjectsPath: '/api/:version/projects/:id/forks', projectLabelsPath: '/:namespace_path/:project_path/-/labels', + projectUsersPath: '/api/:version/projects/:id/users', projectMergeRequestsPath: '/api/:version/projects/:id/merge_requests', projectMergeRequestPath: '/api/:version/projects/:id/merge_requests/:mrid', projectMergeRequestChangesPath: '/api/:version/projects/:id/merge_requests/:mrid/changes', @@ -108,6 +109,20 @@ const Api = { }); }, + projectUsers(projectPath, query = '', options = {}) { + const url = Api.buildUrl(this.projectUsersPath).replace(':id', encodeURIComponent(projectPath)); + + return axios + .get(url, { + params: { + search: query, + per_page: 20, + ...options, + }, + }) + .then(({ data }) => data); + }, + // Return single project project(projectPath) { const url = Api.buildUrl(Api.projectPath).replace(':id', encodeURIComponent(projectPath)); diff --git a/app/assets/javascripts/behaviors/markdown/render_math.js b/app/assets/javascripts/behaviors/markdown/render_math.js index a68936d79e2..53867b3096b 100644 --- a/app/assets/javascripts/behaviors/markdown/render_math.js +++ b/app/assets/javascripts/behaviors/markdown/render_math.js @@ -1,6 +1,5 @@ -import $ from 'jquery'; -import { __ } from '~/locale'; import flash from '~/flash'; +import { s__, sprintf } from '~/locale'; // Renders math using KaTeX in any element with the // `js-render-math` class @@ -10,21 +9,131 @@ import flash from '~/flash'; // <code class="js-render-math"></div> // -// Loop over all math elements and render math -function renderWithKaTeX(elements, katex) { - elements.each(function katexElementsLoop() { - const mathNode = $('<span></span>'); - const $this = $(this); - - const display = $this.attr('data-math-style') === 'display'; - try { - katex.render($this.text(), mathNode.get(0), { displayMode: display, throwOnError: false }); - mathNode.insertAfter($this); - $this.remove(); - } catch (err) { - throw err; +const MAX_MATH_CHARS = 1000; +const MAX_RENDER_TIME_MS = 2000; + +// These messages might be used with inline errors in the future. Keep them around. For now, we will +// display a single error message using flash(). + +// const CHAR_LIMIT_EXCEEDED_MSG = sprintf( +// s__( +// 'math|The following math is too long. For performance reasons, math blocks are limited to %{maxChars} characters. Try splitting up this block, or include an image instead.', +// ), +// { maxChars: MAX_MATH_CHARS }, +// ); +// const RENDER_TIME_EXCEEDED_MSG = s__( +// "math|The math in this entry is taking too long to render. Any math below this point won't be shown. Consider splitting it among multiple entries.", +// ); + +const RENDER_FLASH_MSG = sprintf( + s__( + 'math|The math in this entry is taking too long to render and may not be displayed as expected. For performance reasons, math blocks are also limited to %{maxChars} characters. Consider splitting up large formulae, splitting math blocks among multiple entries, or using an image instead.', + ), + { maxChars: MAX_MATH_CHARS }, +); + +// Wait for the browser to reflow the layout. Reflowing SVG takes time. +// This has to wrap the inner function, otherwise IE/Edge throw "invalid calling object". +const waitForReflow = fn => { + window.requestAnimationFrame(fn); +}; + +/** + * Renders math blocks sequentially while protecting against DoS attacks. Math blocks have a maximum character limit of MAX_MATH_CHARS. If rendering math takes longer than MAX_RENDER_TIME_MS, all subsequent math blocks are skipped and an error message is shown. + */ +class SafeMathRenderer { + /* + How this works: + + The performance bottleneck in rendering math is in the browser trying to reflow the generated SVG. + During this time, the JS is blocked and the page becomes unresponsive. + We want to render math blocks one by one until a certain time is exceeded, after which we stop + rendering subsequent math blocks, to protect against DoS. However, browsers do reflowing in an + asynchronous task, so we can't time it synchronously. + + SafeMathRenderer essentially does the following: + 1. Replaces all math blocks with placeholders so that they're not mistakenly rendered twice. + 2. Places each placeholder element in a queue. + 3. Renders the element at the head of the queue and waits for reflow. + 4. After reflow, gets the elapsed time since step 3 and repeats step 3 until the queue is empty. + */ + queue = []; + totalMS = 0; + + constructor(elements, katex) { + this.elements = elements; + this.katex = katex; + + this.renderElement = this.renderElement.bind(this); + this.render = this.render.bind(this); + } + + renderElement() { + if (!this.queue.length) { + return; } - }); + + const el = this.queue.shift(); + const text = el.textContent; + + el.removeAttribute('style'); + + if (this.totalMS >= MAX_RENDER_TIME_MS || text.length > MAX_MATH_CHARS) { + if (!this.flashShown) { + flash(RENDER_FLASH_MSG); + this.flashShown = true; + } + + // Show unrendered math code + const codeElement = document.createElement('pre'); + codeElement.className = 'code'; + codeElement.textContent = el.textContent; + el.parentNode.replaceChild(codeElement, el); + + // Render the next math + this.renderElement(); + } else { + this.startTime = Date.now(); + + try { + el.innerHTML = this.katex.renderToString(text, { + displayMode: el.getAttribute('data-math-style') === 'display', + throwOnError: true, + maxSize: 20, + maxExpand: 20, + }); + } catch { + // Don't show a flash for now because it would override an existing flash message + el.textContent = s__('math|There was an error rendering this math block'); + // el.style.color = '#d00'; + el.className = 'katex-error'; + } + + // Give the browser time to reflow the svg + waitForReflow(() => { + const deltaTime = Date.now() - this.startTime; + this.totalMS += deltaTime; + + this.renderElement(); + }); + } + } + + render() { + // Replace math blocks with a placeholder so they aren't rendered twice + this.elements.forEach(el => { + const placeholder = document.createElement('span'); + placeholder.style.display = 'none'; + placeholder.setAttribute('data-math-style', el.getAttribute('data-math-style')); + placeholder.textContent = el.textContent; + el.parentNode.replaceChild(placeholder, el); + this.queue.push(placeholder); + }); + + // If we wait for the browser thread to settle down a bit, math rendering becomes 5-10x faster + // and less prone to timeouts. + setTimeout(this.renderElement, 400); + } } export default function renderMath($els) { @@ -34,7 +143,8 @@ export default function renderMath($els) { import(/* webpackChunkName: 'katex' */ 'katex/dist/katex.min.css'), ]) .then(([katex]) => { - renderWithKaTeX($els, katex); + const renderer = new SafeMathRenderer($els.get(), katex); + renderer.render(); }) - .catch(() => flash(__('An error occurred while rendering KaTeX'))); + .catch(() => {}); } diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts.js index eade1283513..7e3515b1f4b 100644 --- a/app/assets/javascripts/behaviors/shortcuts/shortcuts.js +++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts.js @@ -4,7 +4,7 @@ import Mousetrap from 'mousetrap'; import axios from '../../lib/utils/axios_utils'; import { refreshCurrentPage, visitUrl } from '../../lib/utils/url_utility'; import findAndFollowLink from '../../lib/utils/navigation_utility'; -import { parseBoolean } from '~/lib/utils/common_utils'; +import { parseBoolean, getCspNonceValue } from '~/lib/utils/common_utils'; const defaultStopCallback = Mousetrap.stopCallback; Mousetrap.stopCallback = (e, element, combo) => { @@ -94,7 +94,7 @@ export default class Shortcuts { responseType: 'text', }) .then(({ data }) => { - $.globalEval(data); + $.globalEval(data, { nonce: getCspNonceValue() }); if (location && location.length > 0) { const results = []; diff --git a/app/assets/javascripts/boards/components/board_card.vue b/app/assets/javascripts/boards/components/board_card.vue index 179148b6887..faf722f61af 100644 --- a/app/assets/javascripts/boards/components/board_card.vue +++ b/app/assets/javascripts/boards/components/board_card.vue @@ -83,6 +83,7 @@ export default { }" :index="index" :data-issue-id="issue.id" + data-qa-selector="board_card" class="board-card p-3 rounded" @mousedown="mouseDown" @mousemove="mouseMove" diff --git a/app/assets/javascripts/boards/components/board_list.vue b/app/assets/javascripts/boards/components/board_list.vue index 03a8a92575e..de41698ca04 100644 --- a/app/assets/javascripts/boards/components/board_list.vue +++ b/app/assets/javascripts/boards/components/board_list.vue @@ -227,6 +227,7 @@ export default { <div :class="{ 'd-none': !list.isExpanded, 'd-flex flex-column': list.isExpanded }" class="board-list-component position-relative h-100" + data-qa-selector="board_list_cards_area" > <div v-if="loading" class="board-list-loading text-center" :aria-label="__('Loading issues')"> <gl-loading-icon /> diff --git a/app/assets/javascripts/boards/components/board_sidebar.js b/app/assets/javascripts/boards/components/board_sidebar.js index 2ace0060c42..ba1fe9202fc 100644 --- a/app/assets/javascripts/boards/components/board_sidebar.js +++ b/app/assets/javascripts/boards/components/board_sidebar.js @@ -22,6 +22,8 @@ export default Vue.extend({ components: { AssigneeTitle, Assignees, + SidebarEpicsSelect: () => + import('ee_component/sidebar/components/sidebar_item_epics_select.vue'), RemoveBtn, Subscriptions, TimeTracker, diff --git a/app/assets/javascripts/boards/components/boards_selector.vue b/app/assets/javascripts/boards/components/boards_selector.vue index b05de4538f2..7296426549a 100644 --- a/app/assets/javascripts/boards/components/boards_selector.vue +++ b/app/assets/javascripts/boards/components/boards_selector.vue @@ -226,6 +226,7 @@ export default { <div class="boards-switcher js-boards-selector append-right-10"> <span class="boards-selector-wrapper js-boards-selector-wrapper"> <gl-dropdown + data-qa-selector="boards_dropdown" toggle-class="dropdown-menu-toggle js-dropdown-toggle" menu-class="flex-column dropdown-extended-height" :text="board.name" diff --git a/app/assets/javascripts/cycle_analytics/components/stage_card_list_item.vue b/app/assets/javascripts/cycle_analytics/components/stage_card_list_item.vue new file mode 100644 index 00000000000..d946594a069 --- /dev/null +++ b/app/assets/javascripts/cycle_analytics/components/stage_card_list_item.vue @@ -0,0 +1,41 @@ +<script> +import Icon from '~/vue_shared/components/icon.vue'; +import { GlButton } from '@gitlab/ui'; + +export default { + name: 'StageCardListItem', + components: { + Icon, + GlButton, + }, + props: { + isActive: { + type: Boolean, + required: true, + }, + canEdit: { + type: Boolean, + default: false, + required: false, + }, + }, +}; +</script> + +<template> + <div :class="{ active: isActive }" class="stage-nav-item d-flex pl-4 pr-4 m-0 mb-1 ml-2 rounded"> + <slot></slot> + <div v-if="canEdit" class="dropdown"> + <gl-button + :title="__('More actions')" + class="more-actions-toggle btn btn-transparent p-0" + data-toggle="dropdown" + > + <icon css-classes="icon" name="ellipsis_v" /> + </gl-button> + <ul class="more-actions-dropdown dropdown-menu dropdown-open-left"> + <slot name="dropdown-options"></slot> + </ul> + </div> + </div> +</template> diff --git a/app/assets/javascripts/cycle_analytics/components/stage_nav_item.vue b/app/assets/javascripts/cycle_analytics/components/stage_nav_item.vue new file mode 100644 index 00000000000..004d335f572 --- /dev/null +++ b/app/assets/javascripts/cycle_analytics/components/stage_nav_item.vue @@ -0,0 +1,88 @@ +<script> +import StageCardListItem from './stage_card_list_item.vue'; + +export default { + name: 'StageNavItem', + components: { + StageCardListItem, + }, + props: { + isDefaultStage: { + type: Boolean, + default: false, + required: false, + }, + isActive: { + type: Boolean, + default: false, + required: false, + }, + isUserAllowed: { + type: Boolean, + required: true, + }, + title: { + type: String, + required: true, + }, + value: { + type: String, + default: '', + required: false, + }, + canEdit: { + type: Boolean, + default: false, + required: false, + }, + }, + computed: { + hasValue() { + return this.value && this.value.length > 0; + }, + editable() { + return this.isUserAllowed && this.canEdit; + }, + }, +}; +</script> + +<template> + <li @click="$emit('select')"> + <stage-card-list-item :is-active="isActive" :can-edit="editable"> + <div class="stage-nav-item-cell stage-name p-0" :class="{ 'font-weight-bold': isActive }"> + {{ title }} + </div> + <div class="stage-nav-item-cell stage-median mr-4"> + <template v-if="isUserAllowed"> + <span v-if="hasValue">{{ value }}</span> + <span v-else class="stage-empty">{{ __('Not enough data') }}</span> + </template> + <template v-else> + <span class="not-available">{{ __('Not available') }}</span> + </template> + </div> + <template v-slot:dropdown-options> + <template v-if="isDefaultStage"> + <li> + <button type="button" class="btn-default btn-transparent"> + {{ __('Hide stage') }} + </button> + </li> + </template> + <template v-else> + <li> + <button type="button" class="btn-default btn-transparent"> + {{ __('Edit stage') }} + </button> + </li> + <li> + <button type="button" class="btn-danger danger"> + {{ __('Remove stage') }} + </button> + </li> + </template> + </template> + </stage-card-list-item> + </li> +</template> diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js index 671405602cc..b3ae47af750 100644 --- a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js +++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js @@ -12,6 +12,7 @@ import stageComponent from './components/stage_component.vue'; import stageReviewComponent from './components/stage_review_component.vue'; import stageStagingComponent from './components/stage_staging_component.vue'; import stageTestComponent from './components/stage_test_component.vue'; +import stageNavItem from './components/stage_nav_item.vue'; import CycleAnalyticsService from './cycle_analytics_service'; import CycleAnalyticsStore from './cycle_analytics_store'; @@ -41,6 +42,7 @@ export default () => { import('ee_component/analytics/shared/components/projects_dropdown_filter.vue'), DateRangeDropdown: () => import('ee_component/analytics/shared/components/date_range_dropdown.vue'), + 'stage-nav-item': stageNavItem, }, mixins: [filterMixins], data() { diff --git a/app/assets/javascripts/diffs/components/app.vue b/app/assets/javascripts/diffs/components/app.vue index 81da0754752..19b85710710 100644 --- a/app/assets/javascripts/diffs/components/app.vue +++ b/app/assets/javascripts/diffs/components/app.vue @@ -305,7 +305,7 @@ export default { <div v-show="showTreeList" :style="{ width: `${treeWidth}px` }" - class="diff-tree-list js-diff-tree-list" + class="diff-tree-list js-diff-tree-list mr-3" > <panel-resizer :size.sync="treeWidth" diff --git a/app/assets/javascripts/diffs/components/diff_expansion_cell.vue b/app/assets/javascripts/diffs/components/diff_expansion_cell.vue index 925385fa98a..839ab542377 100644 --- a/app/assets/javascripts/diffs/components/diff_expansion_cell.vue +++ b/app/assets/javascripts/diffs/components/diff_expansion_cell.vue @@ -212,19 +212,18 @@ export default { </script> <template> - <td :colspan="colspan"> + <td :colspan="colspan" class="text-center"> <div class="content js-line-expansion-content"> <a v-if="canExpandUp" v-tooltip - class="cursor-pointer js-unfold unfold-icon" + class="cursor-pointer js-unfold unfold-icon d-inline-block pt-2 pb-2" data-placement="top" data-container="body" :title="__('Expand up')" @click="handleExpandLines(EXPAND_UP)" > - <!-- TODO: remove style & replace with correct icon, waiting for MR https://gitlab.com/gitlab-org/gitlab-design/issues/499 --> - <icon :size="12" name="expand-left" aria-hidden="true" style="transform: rotate(270deg);" /> + <icon :size="12" name="expand-up" aria-hidden="true" /> </a> <a class="mx-2 cursor-pointer js-unfold-all" @click="handleExpandLines()"> <span>{{ s__('Diffs|Show all lines') }}</span> @@ -232,14 +231,13 @@ export default { <a v-if="canExpandDown" v-tooltip - class="cursor-pointer js-unfold-down has-tooltip unfold-icon" + class="cursor-pointer js-unfold-down has-tooltip unfold-icon d-inline-block pt-2 pb-2" data-placement="top" data-container="body" :title="__('Expand down')" @click="handleExpandLines(EXPAND_DOWN)" > - <!-- TODO: remove style & replace with correct icon, waiting for MR https://gitlab.com/gitlab-org/gitlab-design/issues/499 --> - <icon :size="12" name="expand-left" aria-hidden="true" style="transform: rotate(90deg);" /> + <icon :size="12" name="expand-down" aria-hidden="true" /> </a> </div> </td> diff --git a/app/assets/javascripts/diffs/components/diff_file_header.vue b/app/assets/javascripts/diffs/components/diff_file_header.vue index 32fbeaaa905..69ec6ab8600 100644 --- a/app/assets/javascripts/diffs/components/diff_file_header.vue +++ b/app/assets/javascripts/diffs/components/diff_file_header.vue @@ -130,7 +130,7 @@ export default { return `\`${this.diffFile.file_path}\``; }, isFileRenamed() { - return this.diffFile.viewer.name === diffViewerModes.renamed; + return this.diffFile.renamed_file; }, isModeChanged() { return this.diffFile.viewer.name === diffViewerModes.mode_changed; diff --git a/app/assets/javascripts/event_tracking/issue_sidebar.js b/app/assets/javascripts/event_tracking/issue_sidebar.js new file mode 100644 index 00000000000..6909f82c66f --- /dev/null +++ b/app/assets/javascripts/event_tracking/issue_sidebar.js @@ -0,0 +1,2 @@ +export const initSidebarTracking = () => {}; +export const trackEvent = () => {}; diff --git a/app/assets/javascripts/ide/components/commit_sidebar/actions.vue b/app/assets/javascripts/ide/components/commit_sidebar/actions.vue index 685d8a6b245..8b356ee6e97 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/actions.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/actions.vue @@ -41,10 +41,16 @@ export default { methods: { ...mapCommitActions(['updateCommitAction']), updateSelectedCommitAction() { - if (this.currentBranch && !this.currentBranch.can_push) { - this.updateCommitAction(consts.COMMIT_TO_NEW_BRANCH); - } else if (this.containsStagedChanges) { + if (!this.currentBranch) { + return; + } + + const { can_push: canPush = false, default: isDefault = false } = this.currentBranch; + + if (canPush && !isDefault) { this.updateCommitAction(consts.COMMIT_TO_CURRENT_BRANCH); + } else { + this.updateCommitAction(consts.COMMIT_TO_NEW_BRANCH); } }, }, diff --git a/app/assets/javascripts/ide/components/commit_sidebar/new_merge_request_option.vue b/app/assets/javascripts/ide/components/commit_sidebar/new_merge_request_option.vue index b2e7b15089c..daa44a42765 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/new_merge_request_option.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/new_merge_request_option.vue @@ -1,43 +1,36 @@ <script> -import { mapGetters, createNamespacedHelpers } from 'vuex'; +import { createNamespacedHelpers } from 'vuex'; const { mapState: mapCommitState, - mapGetters: mapCommitGetters, mapActions: mapCommitActions, + mapGetters: mapCommitGetters, } = createNamespacedHelpers('commit'); export default { computed: { ...mapCommitState(['shouldCreateMR']), - ...mapCommitGetters(['isCommittingToCurrentBranch', 'isCommittingToDefaultBranch']), - ...mapGetters(['hasMergeRequest', 'isOnDefaultBranch']), - currentBranchHasMr() { - return this.hasMergeRequest && this.isCommittingToCurrentBranch; - }, - showNewMrOption() { - return ( - this.isCommittingToDefaultBranch || !this.currentBranchHasMr || this.isCommittingToNewBranch - ); - }, - }, - mounted() { - this.setShouldCreateMR(); + ...mapCommitGetters(['shouldHideNewMrOption']), }, methods: { - ...mapCommitActions(['toggleShouldCreateMR', 'setShouldCreateMR']), + ...mapCommitActions(['toggleShouldCreateMR']), }, }; </script> <template> - <div v-if="showNewMrOption"> + <fieldset v-if="!shouldHideNewMrOption"> <hr class="my-2" /> - <label class="mb-0"> - <input :checked="shouldCreateMR" type="checkbox" @change="toggleShouldCreateMR" /> + <label class="mb-0 js-ide-commit-new-mr"> + <input + :checked="shouldCreateMR" + type="checkbox" + data-qa-selector="start_new_mr_checkbox" + @change="toggleShouldCreateMR" + /> <span class="prepend-left-10"> {{ __('Start a new merge request') }} </span> </label> - </div> + </fieldset> </template> diff --git a/app/assets/javascripts/ide/components/file_row_extra.vue b/app/assets/javascripts/ide/components/file_row_extra.vue index 80a6ab9598a..7254c50a568 100644 --- a/app/assets/javascripts/ide/components/file_row_extra.vue +++ b/app/assets/javascripts/ide/components/file_row_extra.vue @@ -87,7 +87,6 @@ export default { :file="file" :show-tooltip="true" :show-staged-icon="true" - :force-modified-icon="true" /> <new-dropdown :type="file.type" diff --git a/app/assets/javascripts/ide/stores/getters.js b/app/assets/javascripts/ide/stores/getters.js index 406903129db..85fd45358be 100644 --- a/app/assets/javascripts/ide/stores/getters.js +++ b/app/assets/javascripts/ide/stores/getters.js @@ -104,5 +104,8 @@ export const packageJson = state => state.entries[packageJsonPath]; export const isOnDefaultBranch = (_state, getters) => getters.currentProject && getters.currentProject.default_branch === getters.branchName; +export const canPushToBranch = (_state, getters) => + getters.currentBranch && getters.currentBranch.can_push; + // prevent babel-plugin-rewire from generating an invalid default during karma tests export default () => {}; diff --git a/app/assets/javascripts/ide/stores/modules/commit/actions.js b/app/assets/javascripts/ide/stores/modules/commit/actions.js index ac34491c1ad..23caf2d48ed 100644 --- a/app/assets/javascripts/ide/stores/modules/commit/actions.js +++ b/app/assets/javascripts/ide/stores/modules/commit/actions.js @@ -18,34 +18,15 @@ export const discardDraft = ({ commit }) => { commit(types.UPDATE_COMMIT_MESSAGE, ''); }; -export const updateCommitAction = ({ commit, dispatch }, commitAction) => { +export const updateCommitAction = ({ commit, getters }, commitAction) => { commit(types.UPDATE_COMMIT_ACTION, { commitAction, }); - dispatch('setShouldCreateMR'); + commit(types.TOGGLE_SHOULD_CREATE_MR, !getters.shouldHideNewMrOption); }; export const toggleShouldCreateMR = ({ commit }) => { commit(types.TOGGLE_SHOULD_CREATE_MR); - commit(types.INTERACT_WITH_NEW_MR); -}; - -export const setShouldCreateMR = ({ - commit, - getters, - rootGetters, - state: { interactedWithNewMR }, -}) => { - const committingToExistingMR = - getters.isCommittingToCurrentBranch && - rootGetters.hasMergeRequest && - !rootGetters.isOnDefaultBranch; - - if ((getters.isCommittingToDefaultBranch && !interactedWithNewMR) || committingToExistingMR) { - commit(types.TOGGLE_SHOULD_CREATE_MR, false); - } else if (!interactedWithNewMR) { - commit(types.TOGGLE_SHOULD_CREATE_MR, true); - } }; export const updateBranchName = ({ commit }, branchName) => { diff --git a/app/assets/javascripts/ide/stores/modules/commit/getters.js b/app/assets/javascripts/ide/stores/modules/commit/getters.js index 64779e9e4df..de289e27199 100644 --- a/app/assets/javascripts/ide/stores/modules/commit/getters.js +++ b/app/assets/javascripts/ide/stores/modules/commit/getters.js @@ -20,7 +20,7 @@ export const placeholderBranchName = (state, _, rootState) => )}`; export const branchName = (state, getters, rootState) => { - if (state.commitAction === consts.COMMIT_TO_NEW_BRANCH) { + if (getters.isCreatingNewBranch) { if (state.newBranchName === '') { return getters.placeholderBranchName; } @@ -48,11 +48,11 @@ export const preBuiltCommitMessage = (state, _, rootState) => { export const isCreatingNewBranch = state => state.commitAction === consts.COMMIT_TO_NEW_BRANCH; -export const isCommittingToCurrentBranch = state => - state.commitAction === consts.COMMIT_TO_CURRENT_BRANCH; - -export const isCommittingToDefaultBranch = (_state, getters, _rootState, rootGetters) => - getters.isCommittingToCurrentBranch && rootGetters.isOnDefaultBranch; +export const shouldHideNewMrOption = (_state, getters, _rootState, rootGetters) => + !getters.isCreatingNewBranch && + (rootGetters.hasMergeRequest || + (!rootGetters.hasMergeRequest && rootGetters.isOnDefaultBranch)) && + rootGetters.canPushToBranch; // prevent babel-plugin-rewire from generating an invalid default during karma tests export default () => {}; diff --git a/app/assets/javascripts/ide/stores/modules/commit/mutation_types.js b/app/assets/javascripts/ide/stores/modules/commit/mutation_types.js index b81918156b0..7ad8f3570b7 100644 --- a/app/assets/javascripts/ide/stores/modules/commit/mutation_types.js +++ b/app/assets/javascripts/ide/stores/modules/commit/mutation_types.js @@ -3,4 +3,3 @@ export const UPDATE_COMMIT_ACTION = 'UPDATE_COMMIT_ACTION'; export const UPDATE_NEW_BRANCH_NAME = 'UPDATE_NEW_BRANCH_NAME'; export const UPDATE_LOADING = 'UPDATE_LOADING'; export const TOGGLE_SHOULD_CREATE_MR = 'TOGGLE_SHOULD_CREATE_MR'; -export const INTERACT_WITH_NEW_MR = 'INTERACT_WITH_NEW_MR'; diff --git a/app/assets/javascripts/ide/stores/modules/commit/mutations.js b/app/assets/javascripts/ide/stores/modules/commit/mutations.js index 14957d283bb..73b618e250f 100644 --- a/app/assets/javascripts/ide/stores/modules/commit/mutations.js +++ b/app/assets/javascripts/ide/stores/modules/commit/mutations.js @@ -24,7 +24,4 @@ export default { shouldCreateMR: shouldCreateMR === undefined ? !state.shouldCreateMR : shouldCreateMR, }); }, - [types.INTERACT_WITH_NEW_MR](state) { - Object.assign(state, { interactedWithNewMR: true }); - }, }; diff --git a/app/assets/javascripts/ide/stores/modules/commit/state.js b/app/assets/javascripts/ide/stores/modules/commit/state.js index 53647a7e3e3..259577e48e0 100644 --- a/app/assets/javascripts/ide/stores/modules/commit/state.js +++ b/app/assets/javascripts/ide/stores/modules/commit/state.js @@ -3,6 +3,5 @@ export default () => ({ commitAction: '1', newBranchName: '', submitCommitLoading: false, - shouldCreateMR: false, - interactedWithNewMR: false, + shouldCreateMR: true, }); diff --git a/app/assets/javascripts/ide/stores/utils.js b/app/assets/javascripts/ide/stores/utils.js index 04e86afb268..52200ce7847 100644 --- a/app/assets/javascripts/ide/stores/utils.js +++ b/app/assets/javascripts/ide/stores/utils.js @@ -129,7 +129,7 @@ export const commitActionForFile = file => { export const getCommitFiles = stagedFiles => stagedFiles.reduce((acc, file) => { - if (file.moved) return acc; + if (file.moved || file.type === 'tree') return acc; return acc.concat({ ...file, diff --git a/app/assets/javascripts/issue_show/index.js b/app/assets/javascripts/issue_show/index.js index 529b6386221..5a9dd91817e 100644 --- a/app/assets/javascripts/issue_show/index.js +++ b/app/assets/javascripts/issue_show/index.js @@ -1,4 +1,5 @@ import Vue from 'vue'; +import { initSidebarTracking } from 'ee_else_ce/event_tracking/issue_sidebar'; import issuableApp from './components/app.vue'; import { parseIssuableData } from './utils/parse_data'; import '../vue_shared/vue_resource_interceptor'; @@ -9,6 +10,9 @@ export default function initIssueableApp() { components: { issuableApp, }, + mounted() { + initSidebarTracking(); + }, render(createElement) { return createElement('issuable-app', { props: parseIssuableData(), diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js index 31c4a920bbe..6e8f63a10a4 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js +++ b/app/assets/javascripts/lib/utils/common_utils.js @@ -732,6 +732,66 @@ export const NavigationType = { }; /** + * Method to perform case-insensitive search for a string + * within multiple properties and return object containing + * properties in case there are multiple matches or `null` + * if there's no match. + * + * Eg; Suppose we want to allow user to search using for a string + * within `iid`, `title`, `url` or `reference` props of a target object; + * + * const objectToSearch = { + * "iid": 1, + * "title": "Error omnis quos consequatur ullam a vitae sed omnis libero cupiditate. &3", + * "url": "/groups/gitlab-org/-/epics/1", + * "reference": "&1", + * }; + * + * Following is how we call searchBy and the return values it will yield; + * + * - `searchBy('omnis', objectToSearch);`: This will return `{ title: ... }` as our + * query was found within title prop we only return that. + * - `searchBy('1', objectToSearch);`: This will return `{ "iid": ..., "reference": ..., "url": ... }`. + * - `searchBy('https://gitlab.com/groups/gitlab-org/-/epics/1', objectToSearch);`: + * This will return `{ "url": ... }`. + * - `searchBy('foo', objectToSearch);`: This will return `null` as no property value + * matched with our query. + * + * You can learn more about behaviour of this method by referring to tests + * within `spec/javascripts/lib/utils/common_utils_spec.js`. + * + * @param {string} query String to search for + * @param {object} searchSpace Object containing properties to search in for `query` + */ +export const searchBy = (query = '', searchSpace = {}) => { + const targetKeys = searchSpace !== null ? Object.keys(searchSpace) : []; + + if (!query || !targetKeys.length) { + return null; + } + + const normalizedQuery = query.toLowerCase(); + const matches = targetKeys + .filter(item => { + const searchItem = `${searchSpace[item]}`.toLowerCase(); + + return ( + searchItem.indexOf(normalizedQuery) > -1 || + normalizedQuery.indexOf(searchItem) > -1 || + normalizedQuery === searchItem + ); + }) + .reduce((acc, prop) => { + const match = acc; + match[prop] = searchSpace[prop]; + + return acc; + }, {}); + + return Object.keys(matches).length ? matches : null; +}; + +/** * Checks if the given Label has a special syntax `::` in * it's title. * diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js index 1336b6a5461..7ead9d46fbb 100644 --- a/app/assets/javascripts/lib/utils/url_utility.js +++ b/app/assets/javascripts/lib/utils/url_utility.js @@ -1,5 +1,11 @@ import { join as joinPaths } from 'path'; +// Returns a decoded url parameter value +// - Treats '+' as '%20' +function decodeUrlParameter(val) { + return decodeURIComponent(val.replace(/\+/g, '%20')); +} + // Returns an array containing the value(s) of the // of the key passed as an argument export function getParameterValues(sParam, url = window.location) { @@ -30,7 +36,7 @@ export function mergeUrlParams(params, url) { .forEach(part => { if (part.length) { const kv = part.split('='); - merged[decodeURIComponent(kv[0])] = decodeURIComponent(kv.slice(1).join('=')); + merged[decodeUrlParameter(kv[0])] = decodeUrlParameter(kv.slice(1).join('=')); } }); } diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index 9e97f345717..39f2097c174 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -9,7 +9,11 @@ import './commons'; import './behaviors'; // lib/utils -import { handleLocationHash, addSelectOnFocusBehaviour } from './lib/utils/common_utils'; +import { + handleLocationHash, + addSelectOnFocusBehaviour, + getCspNonceValue, +} from './lib/utils/common_utils'; import { localTimeAgo } from './lib/utils/datetime_utility'; import { getLocationHash, visitUrl } from './lib/utils/url_utility'; @@ -39,6 +43,17 @@ import 'ee_else_ce/main_ee'; window.jQuery = jQuery; window.$ = jQuery; +// Add nonce to jQuery script handler +jQuery.ajaxSetup({ + converters: { + // eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings, func-names + 'text script': function(text) { + jQuery.globalEval(text, { nonce: getCspNonceValue() }); + return text; + }, + }, +}); + // inject test utilities if necessary if (process.env.NODE_ENV !== 'production' && gon && gon.test_env) { $.fx.off = true; @@ -107,6 +122,7 @@ function deferredInitialisation() { .then(() => { $('select.select2').select2({ width: 'resolve', + minimumResultsForSearch: 10, dropdownAutoWidth: true, }); diff --git a/app/assets/javascripts/members.js b/app/assets/javascripts/members.js index af2697444f2..d719fd8748d 100644 --- a/app/assets/javascripts/members.js +++ b/app/assets/javascripts/members.js @@ -17,6 +17,8 @@ export default class Members { } dropdownClicked(options) { + options.e.preventDefault(); + this.formSubmit(null, options.$el); } diff --git a/app/assets/javascripts/monitoring/components/charts/area.vue b/app/assets/javascripts/monitoring/components/charts/area.vue index 5b950f8c966..cac10474d06 100644 --- a/app/assets/javascripts/monitoring/components/charts/area.vue +++ b/app/assets/javascripts/monitoring/components/charts/area.vue @@ -1,7 +1,6 @@ <script> import { __ } from '~/locale'; -import { mapState } from 'vuex'; -import { GlLink, GlButton } from '@gitlab/ui'; +import { GlLink } from '@gitlab/ui'; import { GlAreaChart, GlChartSeriesLabel } from '@gitlab/ui/dist/charts'; import dateFormat from 'dateformat'; import { debounceByAnimationFrame, roundOffFloat } from '~/lib/utils/common_utils'; @@ -13,10 +12,12 @@ import { graphDataValidatorForValues } from '../../utils'; let debouncedResize; +// TODO: Remove this component in favor of the more general time_series.vue +// Please port all changes here to time_series.vue as well. + export default { components: { GlAreaChart, - GlButton, GlChartSeriesLabel, GlLink, Icon, @@ -47,6 +48,11 @@ export default { required: false, default: () => false, }, + singleEmbed: { + type: Boolean, + required: false, + default: false, + }, thresholds: { type: Array, required: false, @@ -69,7 +75,6 @@ export default { }; }, computed: { - ...mapState('monitoringDashboard', ['exportMetricsToCsvEnabled']), chartData() { // Transforms & supplements query data to render appropriate labels & styles // Input: [{ queryAttributes1 }, { queryAttributes2 }] @@ -121,7 +126,7 @@ export default { }, }, series: this.scatterSeries, - dataZoom: this.dataZoomConfig, + dataZoom: [this.dataZoomConfig], }; }, dataZoomConfig() { @@ -179,18 +184,6 @@ export default { yAxisLabel() { return `${this.graphData.y_label}`; }, - csvText() { - const chartData = this.chartData[0].data; - const header = `timestamp,${this.graphData.y_label}\r\n`; // eslint-disable-line @gitlab/i18n/no-non-i18n-strings - return chartData.reduce((csv, data) => { - const row = data.join(','); - return `${csv}${row}\r\n`; - }, header); - }, - downloadLink() { - const data = new Blob([this.csvText], { type: 'text/plain' }); - return window.URL.createObjectURL(data); - }, }, watch: { containerWidth: 'onResize', @@ -255,20 +248,13 @@ export default { </script> <template> - <div class="prometheus-graph col-12 col-lg-6" :class="[showBorder ? 'p-2' : 'p-0']"> + <div + class="prometheus-graph col-12" + :class="[showBorder ? 'p-2' : 'p-0', { 'col-lg-6': !singleEmbed }]" + > <div :class="{ 'prometheus-graph-embed w-100 p-3': showBorder }"> <div class="prometheus-graph-header"> <h5 ref="graphTitle" class="prometheus-graph-title">{{ graphData.title }}</h5> - <gl-button - v-if="exportMetricsToCsvEnabled" - :href="downloadLink" - :title="__('Download CSV')" - :aria-label="__('Download CSV')" - style="margin-left: 200px;" - download="chart_metrics.csv" - > - {{ __('Download CSV') }} - </gl-button> <div ref="graphWidgets" class="prometheus-graph-widgets"><slot></slot></div> </div> <gl-area-chart diff --git a/app/assets/javascripts/monitoring/components/charts/time_series.vue b/app/assets/javascripts/monitoring/components/charts/time_series.vue new file mode 100644 index 00000000000..02e7a7ba0a6 --- /dev/null +++ b/app/assets/javascripts/monitoring/components/charts/time_series.vue @@ -0,0 +1,342 @@ +<script> +import { __ } from '~/locale'; +import { mapState } from 'vuex'; +import { GlLink, GlButton } from '@gitlab/ui'; +import { GlAreaChart, GlLineChart, GlChartSeriesLabel } from '@gitlab/ui/dist/charts'; +import dateFormat from 'dateformat'; +import { debounceByAnimationFrame, roundOffFloat } from '~/lib/utils/common_utils'; +import { getSvgIconPathContent } from '~/lib/utils/icon_utils'; +import Icon from '~/vue_shared/components/icon.vue'; +import { chartHeight, graphTypes, lineTypes, symbolSizes, dateFormats } from '../../constants'; +import { makeDataSeries } from '~/helpers/monitor_helper'; +import { graphDataValidatorForValues } from '../../utils'; + +let debouncedResize; + +export default { + components: { + GlAreaChart, + GlLineChart, + GlButton, + GlChartSeriesLabel, + GlLink, + Icon, + }, + inheritAttrs: false, + props: { + graphData: { + type: Object, + required: true, + validator: graphDataValidatorForValues.bind(null, false), + }, + containerWidth: { + type: Number, + required: true, + }, + deploymentData: { + type: Array, + required: false, + default: () => [], + }, + projectPath: { + type: String, + required: false, + default: '', + }, + showBorder: { + type: Boolean, + required: false, + default: false, + }, + singleEmbed: { + type: Boolean, + required: false, + default: false, + }, + thresholds: { + type: Array, + required: false, + default: () => [], + }, + }, + data() { + return { + tooltip: { + title: '', + content: [], + commitUrl: '', + isDeployment: false, + sha: '', + }, + width: 0, + height: chartHeight, + svgs: {}, + primaryColor: null, + }; + }, + computed: { + ...mapState('monitoringDashboard', ['exportMetricsToCsvEnabled']), + chartData() { + // Transforms & supplements query data to render appropriate labels & styles + // Input: [{ queryAttributes1 }, { queryAttributes2 }] + // Output: [{ seriesAttributes1 }, { seriesAttributes2 }] + return this.graphData.queries.reduce((acc, query) => { + const { appearance } = query; + const lineType = + appearance && appearance.line && appearance.line.type + ? appearance.line.type + : lineTypes.default; + const lineWidth = + appearance && appearance.line && appearance.line.width + ? appearance.line.width + : undefined; + const areaStyle = { + opacity: + appearance && appearance.area && typeof appearance.area.opacity === 'number' + ? appearance.area.opacity + : undefined, + }; + + const series = makeDataSeries(query.result, { + name: this.formatLegendLabel(query), + lineStyle: { + type: lineType, + width: lineWidth, + }, + showSymbol: false, + areaStyle: this.graphData.type === 'area-chart' ? areaStyle : undefined, + }); + + return acc.concat(series); + }, []); + }, + chartOptions() { + return { + xAxis: { + name: __('Time'), + type: 'time', + axisLabel: { + formatter: date => dateFormat(date, dateFormats.timeOfDay), + }, + axisPointer: { + snap: true, + }, + }, + yAxis: { + name: this.yAxisLabel, + axisLabel: { + formatter: num => roundOffFloat(num, 3).toString(), + }, + }, + series: this.scatterSeries, + dataZoom: this.dataZoomConfig, + }; + }, + dataZoomConfig() { + const handleIcon = this.svgs['scroll-handle']; + + return handleIcon ? { handleIcon } : {}; + }, + earliestDatapoint() { + return this.chartData.reduce((acc, series) => { + const { data } = series; + const { length } = data; + if (!length) { + return acc; + } + + const [first] = data[0]; + const [last] = data[length - 1]; + const seriesEarliest = first < last ? first : last; + + return seriesEarliest < acc || acc === null ? seriesEarliest : acc; + }, null); + }, + glChartComponent() { + const chartTypes = { + 'area-chart': GlAreaChart, + 'line-chart': GlLineChart, + }; + return chartTypes[this.graphData.type] || GlAreaChart; + }, + isMultiSeries() { + return this.tooltip.content.length > 1; + }, + recentDeployments() { + return this.deploymentData.reduce((acc, deployment) => { + if (deployment.created_at >= this.earliestDatapoint) { + const { id, created_at, sha, ref, tag } = deployment; + acc.push({ + id, + createdAt: created_at, + sha, + commitUrl: `${this.projectPath}/commit/${sha}`, + tag, + tagUrl: tag ? `${this.tagsPath}/${ref.name}` : null, + ref: ref.name, + showDeploymentFlag: false, + }); + } + + return acc; + }, []); + }, + scatterSeries() { + return { + type: graphTypes.deploymentData, + data: this.recentDeployments.map(deployment => [deployment.createdAt, 0]), + symbol: this.svgs.rocket, + symbolSize: symbolSizes.default, + itemStyle: { + color: this.primaryColor, + }, + }; + }, + yAxisLabel() { + return `${this.graphData.y_label}`; + }, + csvText() { + const chartData = this.chartData[0].data; + const header = `timestamp,${this.graphData.y_label}\r\n`; // eslint-disable-line @gitlab/i18n/no-non-i18n-strings + return chartData.reduce((csv, data) => { + const row = data.join(','); + return `${csv}${row}\r\n`; + }, header); + }, + downloadLink() { + const data = new Blob([this.csvText], { type: 'text/plain' }); + return window.URL.createObjectURL(data); + }, + }, + watch: { + containerWidth: 'onResize', + }, + beforeDestroy() { + window.removeEventListener('resize', debouncedResize); + }, + created() { + debouncedResize = debounceByAnimationFrame(this.onResize); + window.addEventListener('resize', debouncedResize); + this.setSvg('rocket'); + this.setSvg('scroll-handle'); + }, + methods: { + formatLegendLabel(query) { + return `${query.label}`; + }, + formatTooltipText(params) { + this.tooltip.title = dateFormat(params.value, dateFormats.default); + this.tooltip.content = []; + params.seriesData.forEach(dataPoint => { + const [xVal, yVal] = dataPoint.value; + this.tooltip.isDeployment = dataPoint.componentSubType === graphTypes.deploymentData; + if (this.tooltip.isDeployment) { + const [deploy] = this.recentDeployments.filter( + deployment => deployment.createdAt === xVal, + ); + this.tooltip.sha = deploy.sha.substring(0, 8); + this.tooltip.commitUrl = deploy.commitUrl; + } else { + const { seriesName, color } = dataPoint; + const value = yVal.toFixed(3); + this.tooltip.content.push({ + name: seriesName, + value, + color, + }); + } + }); + }, + setSvg(name) { + getSvgIconPathContent(name) + .then(path => { + if (path) { + this.$set(this.svgs, name, `path://${path}`); + } + }) + .catch(e => { + // eslint-disable-next-line no-console, @gitlab/i18n/no-non-i18n-strings + console.error('SVG could not be rendered correctly: ', e); + }); + }, + onChartUpdated(chart) { + [this.primaryColor] = chart.getOption().color; + }, + onResize() { + if (!this.$refs.chart) return; + const { width } = this.$refs.chart.$el.getBoundingClientRect(); + this.width = width; + }, + }, +}; +</script> + +<template> + <div + class="prometheus-graph col-12" + :class="[showBorder ? 'p-2' : 'p-0', { 'col-lg-6': !singleEmbed }]" + > + <div :class="{ 'prometheus-graph-embed w-100 p-3': showBorder }"> + <div class="prometheus-graph-header"> + <h5 class="prometheus-graph-title js-graph-title">{{ graphData.title }}</h5> + <gl-button + v-if="exportMetricsToCsvEnabled" + :href="downloadLink" + :title="__('Download CSV')" + :aria-label="__('Download CSV')" + style="margin-left: 200px;" + download="chart_metrics.csv" + > + {{ __('Download CSV') }} + </gl-button> + <div class="prometheus-graph-widgets js-graph-widgets"> + <slot></slot> + </div> + </div> + + <component + :is="glChartComponent" + ref="chart" + v-bind="$attrs" + :data="chartData" + :option="chartOptions" + :format-tooltip-text="formatTooltipText" + :thresholds="thresholds" + :width="width" + :height="height" + @updated="onChartUpdated" + > + <template v-if="tooltip.isDeployment"> + <template slot="tooltipTitle"> + {{ __('Deployed') }} + </template> + <div slot="tooltipContent" class="d-flex align-items-center"> + <icon name="commit" class="mr-2" /> + <gl-link :href="tooltip.commitUrl">{{ tooltip.sha }}</gl-link> + </div> + </template> + <template v-else> + <template slot="tooltipTitle"> + <div class="text-nowrap"> + {{ tooltip.title }} + </div> + </template> + <template slot="tooltipContent"> + <div + v-for="(content, key) in tooltip.content" + :key="key" + class="d-flex justify-content-between" + > + <gl-chart-series-label :color="isMultiSeries ? content.color : ''"> + {{ content.name }} + </gl-chart-series-label> + <div class="prepend-left-32"> + {{ content.value }} + </div> + </div> + </template> + </template> + </component> + </div> + </div> +</template> diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue index 782e4310f3e..d330ceb836c 100644 --- a/app/assets/javascripts/monitoring/components/dashboard.vue +++ b/app/assets/javascripts/monitoring/components/dashboard.vue @@ -10,13 +10,13 @@ import { } from '@gitlab/ui'; import _ from 'underscore'; import { mapActions, mapState } from 'vuex'; -import { s__ } from '~/locale'; +import { __, s__ } from '~/locale'; import Icon from '~/vue_shared/components/icon.vue'; -import { getParameterValues } from '~/lib/utils/url_utility'; +import { getParameterValues, mergeUrlParams } from '~/lib/utils/url_utility'; import invalidUrl from '~/lib/utils/invalid_url'; -import MonitorAreaChart from './charts/area.vue'; +import PanelType from 'ee_else_ce/monitoring/components/panel_type.vue'; +import MonitorTimeSeriesChart from './charts/time_series.vue'; import MonitorSingleStatChart from './charts/single_stat.vue'; -import PanelType from './panel_type.vue'; import GraphGroup from './graph_group.vue'; import EmptyState from './empty_state.vue'; import { sidebarAnimationDuration, timeWindows } from '../constants'; @@ -26,7 +26,7 @@ let sidebarMutationObserver; export default { components: { - MonitorAreaChart, + MonitorTimeSeriesChart, MonitorSingleStatChart, PanelType, GraphGroup, @@ -141,6 +141,16 @@ export default { required: false, default: false, }, + alertsEndpoint: { + type: String, + required: false, + default: null, + }, + prometheusAlertsAvailable: { + type: Boolean, + required: false, + default: false, + }, }, data() { return { @@ -168,8 +178,11 @@ export default { 'multipleDashboardsEnabled', 'additionalPanelTypesEnabled', ]), + firstDashboard() { + return this.allDashboards[0] || {}; + }, selectedDashboardText() { - return this.currentDashboard || (this.allDashboards[0] && this.allDashboards[0].display_name); + return this.currentDashboard || this.firstDashboard.display_name; }, addingMetricsAvailable() { return IS_EE && this.canAddMetrics && !this.showEmptyState; @@ -235,6 +248,19 @@ export default { chart.metrics.some(metric => this.metricsWithData.includes(metric.metric_id)), ); }, + csvText(graphData) { + const chartData = graphData.queries[0].result[0].values; + const yLabel = graphData.y_label; + const header = `timestamp,${yLabel}\r\n`; // eslint-disable-line @gitlab/i18n/no-non-i18n-strings + return chartData.reduce((csv, data) => { + const row = data.join(','); + return `${csv}${row}\r\n`; + }, header); + }, + downloadCsv(graphData) { + const data = new Blob([this.csvText(graphData)], { type: 'text/plain' }); + return window.URL.createObjectURL(data); + }, // TODO: BEGIN, Duplicated code with panel_type until feature flag is removed // Issue number: https://gitlab.com/gitlab-org/gitlab-ce/issues/63845 getGraphAlerts(queries) { @@ -245,7 +271,15 @@ export default { getGraphAlertValues(queries) { return Object.values(this.getGraphAlerts(queries)); }, + showToast() { + this.$toast.show(__('Link copied to clipboard')); + }, // TODO: END + generateLink(group, title, yLabel) { + const dashboard = this.currentDashboard || this.firstDashboard.path; + const params = _.pick({ dashboard, group, title, y_label: yLabel }, value => value != null); + return mergeUrlParams(params, window.location.href); + }, hideAddMetricModal() { this.$refs.addMetricModal.hide(); }, @@ -422,13 +456,16 @@ export default { <panel-type v-for="(graphData, graphIndex) in groupData.metrics" :key="`panel-type-${graphIndex}`" + :clipboard-text="generateLink(groupData.group, graphData.title, graphData.y_label)" :graph-data="graphData" :dashboard-width="elWidth" + :alerts-endpoint="alertsEndpoint" + :prometheus-alerts-available="prometheusAlertsAvailable" :index="`${index}-${graphIndex}`" /> </template> <template v-else> - <monitor-area-chart + <monitor-time-series-chart v-for="(graphData, graphIndex) in chartsWithData(groupData.metrics)" :key="graphIndex" :graph-data="graphData" @@ -436,7 +473,7 @@ export default { :thresholds="getGraphAlertValues(graphData.queries)" :container-width="elWidth" :project-path="projectPath" - group-id="monitor-area-chart" + group-id="monitor-time-series-chart" > <div class="d-flex align-items-center"> <alert-widget @@ -448,7 +485,6 @@ export default { @setAlerts="setAlerts" /> <gl-dropdown - v-if="alertWidgetAvailable" v-gl-tooltip class="mx-2" toggle-class="btn btn-transparent border-0" @@ -459,6 +495,18 @@ export default { <template slot="button-content"> <icon name="ellipsis_v" class="text-secondary" /> </template> + <gl-dropdown-item :href="downloadCsv(graphData)" download="chart_metrics.csv"> + {{ __('Download CSV') }} + </gl-dropdown-item> + <gl-dropdown-item + class="js-chart-link" + :data-clipboard-text=" + generateLink(groupData.group, graphData.title, graphData.y_label) + " + @click="showToast" + > + {{ __('Generate link to chart') }} + </gl-dropdown-item> <gl-dropdown-item v-if="alertWidgetAvailable" v-gl-modal="`alert-modal-${index}-${graphIndex}`" @@ -467,7 +515,7 @@ export default { </gl-dropdown-item> </gl-dropdown> </div> - </monitor-area-chart> + </monitor-time-series-chart> </template> </graph-group> </div> diff --git a/app/assets/javascripts/monitoring/components/embed.vue b/app/assets/javascripts/monitoring/components/embed.vue index 9e85b0633fe..b516a82c170 100644 --- a/app/assets/javascripts/monitoring/components/embed.vue +++ b/app/assets/javascripts/monitoring/components/embed.vue @@ -2,7 +2,7 @@ import { mapActions, mapState } from 'vuex'; import { getParameterValues, removeParams } from '~/lib/utils/url_utility'; import GraphGroup from './graph_group.vue'; -import MonitorAreaChart from './charts/area.vue'; +import MonitorTimeSeriesChart from './charts/time_series.vue'; import { sidebarAnimationDuration } from '../constants'; import { getTimeDiff } from '../utils'; @@ -11,7 +11,7 @@ let sidebarMutationObserver; export default { components: { GraphGroup, - MonitorAreaChart, + MonitorTimeSeriesChart, }, props: { dashboardUrl: { @@ -36,12 +36,15 @@ export default { }, computed: { ...mapState('monitoringDashboard', ['groups', 'metricsWithData']), - groupData() { - const groupsWithData = this.groups.filter(group => this.chartsWithData(group.metrics).length); - if (groupsWithData.length) { - return groupsWithData[0]; - } - return null; + charts() { + const groupWithMetrics = this.groups.find(group => + group.metrics.find(chart => this.chartHasData(chart)), + ) || { metrics: [] }; + + return groupWithMetrics.metrics.filter(chart => this.chartHasData(chart)); + }, + isSingleChart() { + return this.charts.length === 1; }, }, mounted() { @@ -66,10 +69,8 @@ export default { 'setFeatureFlags', 'setShowErrorBanner', ]), - chartsWithData(charts) { - return charts.filter(chart => - chart.metrics.some(metric => this.metricsWithData.includes(metric.metric_id)), - ); + chartHasData(chart) { + return chart.metrics.some(metric => this.metricsWithData.includes(metric.metric_id)); }, onSidebarMutation() { setTimeout(() => { @@ -89,16 +90,17 @@ export default { }; </script> <template> - <div class="metrics-embed"> - <div v-if="groupData" class="row w-100 m-n2 pb-4"> - <monitor-area-chart - v-for="graphData in chartsWithData(groupData.metrics)" + <div class="metrics-embed" :class="{ 'd-inline-flex col-lg-6 p-0': isSingleChart }"> + <div v-if="charts.length" class="row w-100 m-n2 pb-4"> + <monitor-time-series-chart + v-for="graphData in charts" :key="graphData.title" :graph-data="graphData" :container-width="elWidth" group-id="monitor-area-chart" :project-path="null" :show-border="true" + :single-embed="isSingleChart" /> </div> </div> diff --git a/app/assets/javascripts/monitoring/components/panel_type.vue b/app/assets/javascripts/monitoring/components/panel_type.vue index 295c0851f12..73ff651d510 100644 --- a/app/assets/javascripts/monitoring/components/panel_type.vue +++ b/app/assets/javascripts/monitoring/components/panel_type.vue @@ -1,24 +1,38 @@ <script> import { mapState } from 'vuex'; import _ from 'underscore'; -import { GlDropdown, GlDropdownItem, GlModal, GlModalDirective } from '@gitlab/ui'; -import MonitorAreaChart from './charts/area.vue'; +import { __ } from '~/locale'; +import { + GlDropdown, + GlDropdownItem, + GlModal, + GlModalDirective, + GlTooltipDirective, +} from '@gitlab/ui'; +import Icon from '~/vue_shared/components/icon.vue'; +import MonitorTimeSeriesChart from './charts/time_series.vue'; import MonitorSingleStatChart from './charts/single_stat.vue'; import MonitorEmptyChart from './charts/empty_chart.vue'; export default { components: { - MonitorAreaChart, MonitorSingleStatChart, + MonitorTimeSeriesChart, MonitorEmptyChart, + Icon, GlDropdown, GlDropdownItem, GlModal, }, directives: { GlModal: GlModalDirective, + GlTooltip: GlTooltipDirective, }, props: { + clipboardText: { + type: String, + required: true, + }, graphData: { type: Object, required: true, @@ -41,6 +55,19 @@ export default { graphDataHasMetrics() { return this.graphData.queries[0].result.length > 0; }, + csvText() { + const chartData = this.graphData.queries[0].result[0].values; + const yLabel = this.graphData.y_label; + const header = `timestamp,${yLabel}\r\n`; // eslint-disable-line @gitlab/i18n/no-non-i18n-strings + return chartData.reduce((csv, data) => { + const row = data.join(','); + return `${csv}${row}\r\n`; + }, header); + }, + downloadCsv() { + const data = new Blob([this.csvText], { type: 'text/plain' }); + return window.URL.createObjectURL(data); + }, }, methods: { getGraphAlerts(queries) { @@ -54,6 +81,9 @@ export default { isPanelType(type) { return this.graphData.type && this.graphData.type === type; }, + showToast() { + this.$toast.show(__('Link copied to clipboard')); + }, }, }; </script> @@ -62,7 +92,7 @@ export default { v-if="isPanelType('single-stat') && graphDataHasMetrics" :graph-data="graphData" /> - <monitor-area-chart + <monitor-time-series-chart v-else-if="graphDataHasMetrics" :graph-data="graphData" :deployment-data="deploymentData" @@ -81,7 +111,6 @@ export default { @setAlerts="setAlerts" /> <gl-dropdown - v-if="alertWidgetAvailable" v-gl-tooltip class="mx-2" toggle-class="btn btn-transparent border-0" @@ -92,11 +121,21 @@ export default { <template slot="button-content"> <icon name="ellipsis_v" class="text-secondary" /> </template> + <gl-dropdown-item :href="downloadCsv" download="chart_metrics.csv"> + {{ __('Download CSV') }} + </gl-dropdown-item> + <gl-dropdown-item + class="js-chart-link" + :data-clipboard-text="clipboardText" + @click="showToast" + > + {{ __('Generate link to chart') }} + </gl-dropdown-item> <gl-dropdown-item v-if="alertWidgetAvailable" v-gl-modal="`alert-modal-${index}`"> {{ __('Alerts') }} </gl-dropdown-item> </gl-dropdown> </div> - </monitor-area-chart> + </monitor-time-series-chart> <monitor-empty-chart v-else :graph-title="graphData.title" /> </template> diff --git a/app/assets/javascripts/monitoring/constants.js b/app/assets/javascripts/monitoring/constants.js index d7d89522732..13aba3d9f44 100644 --- a/app/assets/javascripts/monitoring/constants.js +++ b/app/assets/javascripts/monitoring/constants.js @@ -8,6 +8,10 @@ export const graphTypes = { deploymentData: 'scatter', }; +export const symbolSizes = { + default: 14, +}; + export const lineTypes = { default: 'solid', }; @@ -21,6 +25,11 @@ export const timeWindows = { oneWeek: __('1 week'), }; +export const dateFormats = { + timeOfDay: 'h:MM TT', + default: 'dd mmm yyyy, h:MMTT', +}; + export const secondsIn = { thirtyMinutes: 60 * 30, threeHours: 60 * 60 * 3, diff --git a/app/assets/javascripts/monitoring/monitoring_bundle.js b/app/assets/javascripts/monitoring/monitoring_bundle.js index 366034becd0..51cef20455c 100644 --- a/app/assets/javascripts/monitoring/monitoring_bundle.js +++ b/app/assets/javascripts/monitoring/monitoring_bundle.js @@ -1,9 +1,12 @@ import Vue from 'vue'; +import { GlToast } from '@gitlab/ui'; import { parseBoolean } from '~/lib/utils/common_utils'; import { getParameterValues } from '~/lib/utils/url_utility'; import Dashboard from 'ee_else_ce/monitoring/components/dashboard.vue'; import store from './stores'; +Vue.use(GlToast); + export default (props = {}) => { const el = document.getElementById('prometheus-graphs'); @@ -13,7 +16,6 @@ export default (props = {}) => { prometheusEndpointEnabled: gon.features.environmentMetricsUsePrometheusEndpoint, multipleDashboardsEnabled: gon.features.environmentMetricsShowMultipleDashboards, additionalPanelTypesEnabled: gon.features.environmentMetricsAdditionalPanelTypes, - exportMetricsToCsvEnabled: gon.features.exportMetricsToCsvEnabled, }); } diff --git a/app/assets/javascripts/monitoring/stores/actions.js b/app/assets/javascripts/monitoring/stores/actions.js index a9c491c7c6c..0cbad179f17 100644 --- a/app/assets/javascripts/monitoring/stores/actions.js +++ b/app/assets/javascripts/monitoring/stores/actions.js @@ -37,17 +37,11 @@ export const setEndpoints = ({ commit }, endpoints) => { export const setFeatureFlags = ( { commit }, - { - prometheusEndpointEnabled, - multipleDashboardsEnabled, - additionalPanelTypesEnabled, - exportMetricsToCsvEnabled, - }, + { prometheusEndpointEnabled, multipleDashboardsEnabled, additionalPanelTypesEnabled }, ) => { commit(types.SET_DASHBOARD_ENABLED, prometheusEndpointEnabled); commit(types.SET_MULTIPLE_DASHBOARDS_ENABLED, multipleDashboardsEnabled); commit(types.SET_ADDITIONAL_PANEL_TYPES_ENABLED, additionalPanelTypesEnabled); - commit(types.SET_EXPORT_METRICS_TO_CSV_ENABLED, exportMetricsToCsvEnabled); }; export const setShowErrorBanner = ({ commit }, enabled) => { diff --git a/app/assets/javascripts/monitoring/stores/mutation_types.js b/app/assets/javascripts/monitoring/stores/mutation_types.js index 9ec8214b167..4b1aadbcf05 100644 --- a/app/assets/javascripts/monitoring/stores/mutation_types.js +++ b/app/assets/javascripts/monitoring/stores/mutation_types.js @@ -17,4 +17,3 @@ export const SET_ENDPOINTS = 'SET_ENDPOINTS'; export const SET_GETTING_STARTED_EMPTY_STATE = 'SET_GETTING_STARTED_EMPTY_STATE'; export const SET_NO_DATA_EMPTY_STATE = 'SET_NO_DATA_EMPTY_STATE'; export const SET_SHOW_ERROR_BANNER = 'SET_SHOW_ERROR_BANNER'; -export const SET_EXPORT_METRICS_TO_CSV_ENABLED = 'SET_EXPORT_METRICS_TO_CSV_ENABLED'; diff --git a/app/assets/javascripts/monitoring/stores/mutations.js b/app/assets/javascripts/monitoring/stores/mutations.js index a2dceb21fc0..b19520d6638 100644 --- a/app/assets/javascripts/monitoring/stores/mutations.js +++ b/app/assets/javascripts/monitoring/stores/mutations.js @@ -99,7 +99,4 @@ export default { [types.SET_SHOW_ERROR_BANNER](state, enabled) { state.showErrorBanner = enabled; }, - [types.SET_EXPORT_METRICS_TO_CSV_ENABLED](state, enabled) { - state.exportMetricsToCsvEnabled = enabled; - }, }; diff --git a/app/assets/javascripts/monitoring/stores/state.js b/app/assets/javascripts/monitoring/stores/state.js index a14a25e3a20..440bdc951e0 100644 --- a/app/assets/javascripts/monitoring/stores/state.js +++ b/app/assets/javascripts/monitoring/stores/state.js @@ -10,7 +10,6 @@ export default () => ({ useDashboardEndpoint: false, multipleDashboardsEnabled: false, additionalPanelTypesEnabled: false, - exportMetricsToCsvEnabled: false, emptyState: 'gettingStarted', showEmptyState: true, showErrorBanner: true, diff --git a/app/assets/javascripts/mr_notes/init_notes.js b/app/assets/javascripts/mr_notes/init_notes.js index 8caac68e0d4..622db360d1f 100644 --- a/app/assets/javascripts/mr_notes/init_notes.js +++ b/app/assets/javascripts/mr_notes/init_notes.js @@ -59,6 +59,10 @@ export default () => { render(createElement) { const isDiffView = this.activeTab === 'diffs'; + // NOTE: Even though `discussionKeyboardNavigator` is added to the `notes-app`, + // it adds a global key listener so it works on the diffs tab as well. + // If we create a single Vue app for all of the MR tabs, we should move this + // up the tree, to the root. return createElement(discussionKeyboardNavigator, { props: { isDiffView } }, [ createElement('notes-app', { props: { diff --git a/app/assets/javascripts/notes/components/discussion_keyboard_navigator.vue b/app/assets/javascripts/notes/components/discussion_keyboard_navigator.vue index 5fc2b6ba04c..7fbfe8eebb2 100644 --- a/app/assets/javascripts/notes/components/discussion_keyboard_navigator.vue +++ b/app/assets/javascripts/notes/components/discussion_keyboard_navigator.vue @@ -25,6 +25,10 @@ export default { Mousetrap.bind('n', () => this.jumpToNextDiscussion()); Mousetrap.bind('p', () => this.jumpToPreviousDiscussion()); }, + beforeDestroy() { + Mousetrap.unbind('n'); + Mousetrap.unbind('p'); + }, methods: { ...mapActions(['expandDiscussion']), jumpToNextDiscussion() { diff --git a/app/assets/javascripts/notes/stores/getters.js b/app/assets/javascripts/notes/stores/getters.js index 52410f18d4a..3d0ec8cd3a7 100644 --- a/app/assets/javascripts/notes/stores/getters.js +++ b/app/assets/javascripts/notes/stores/getters.js @@ -171,26 +171,33 @@ export const isLastUnresolvedDiscussion = (state, getters) => (discussionId, dif return lastDiscussionId === discussionId; }; -// Gets the ID of the discussion following the one provided, respecting order (diff or date) -// @param {Boolean} discussionId - id of the current discussion -// @param {Boolean} diffOrder - is ordered by diff? -export const nextUnresolvedDiscussionId = (state, getters) => (discussionId, diffOrder) => { - const idsOrdered = getters.unresolvedDiscussionsIdsOrdered(diffOrder); - const currentIndex = idsOrdered.indexOf(discussionId); - const slicedIds = idsOrdered.slice(currentIndex + 1, currentIndex + 2); +export const findUnresolvedDiscussionIdNeighbor = (state, getters) => ({ + discussionId, + diffOrder, + step, +}) => { + const ids = getters.unresolvedDiscussionsIdsOrdered(diffOrder); + const index = ids.indexOf(discussionId) + step; + + if (index < 0 && step < 0) { + return ids[ids.length - 1]; + } + + if (index === ids.length && step > 0) { + return ids[0]; + } - // Get the first ID if there is none after the currentIndex - return slicedIds.length ? idsOrdered.slice(currentIndex + 1, currentIndex + 2)[0] : idsOrdered[0]; + return ids[index]; }; -export const previousUnresolvedDiscussionId = (state, getters) => (discussionId, diffOrder) => { - const idsOrdered = getters.unresolvedDiscussionsIdsOrdered(diffOrder); - const currentIndex = idsOrdered.indexOf(discussionId); - const slicedIds = idsOrdered.slice(currentIndex - 1, currentIndex); +// Gets the ID of the discussion following the one provided, respecting order (diff or date) +// @param {Boolean} discussionId - id of the current discussion +// @param {Boolean} diffOrder - is ordered by diff? +export const nextUnresolvedDiscussionId = (state, getters) => (discussionId, diffOrder) => + getters.findUnresolvedDiscussionIdNeighbor({ discussionId, diffOrder, step: 1 }); - // Get the last ID if there is none after the currentIndex - return slicedIds.length ? slicedIds[0] : idsOrdered[idsOrdered.length - 1]; -}; +export const previousUnresolvedDiscussionId = (state, getters) => (discussionId, diffOrder) => + getters.findUnresolvedDiscussionIdNeighbor({ discussionId, diffOrder, step: -1 }); // @param {Boolean} diffOrder - is ordered by diff? export const firstUnresolvedDiscussionId = (state, getters) => diffOrder => { diff --git a/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue b/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue index 627d37bac68..a223a8f5b08 100644 --- a/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue +++ b/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue @@ -28,6 +28,11 @@ export default { type: Object, required: true, }, + canDisableEmails: { + type: Boolean, + required: false, + default: false, + }, canChangeVisibilityLevel: { type: Boolean, required: false, @@ -104,6 +109,7 @@ export default { lfsEnabled: true, requestAccessEnabled: true, highlightChangesClass: false, + emailsDisabled: false, }; return { ...defaults, ...this.currentSettings }; @@ -341,5 +347,14 @@ export default { /> </project-setting-row> </div> + <project-setting-row v-if="canDisableEmails" class="mb-3"> + <label class="js-emails-disabled"> + <input :value="emailsDisabled" type="hidden" name="project[emails_disabled]" /> + <input v-model="emailsDisabled" type="checkbox" /> {{ __('Disable email notifications') }} + </label> + <span class="form-text text-muted">{{ + __('This setting will override user notification preferences for all project members.') + }}</span> + </project-setting-row> </div> </template> diff --git a/app/assets/javascripts/pages/projects/wikis/wikis.js b/app/assets/javascripts/pages/projects/wikis/wikis.js index 9b58d42b47d..d41199f6374 100644 --- a/app/assets/javascripts/pages/projects/wikis/wikis.js +++ b/app/assets/javascripts/pages/projects/wikis/wikis.js @@ -1,6 +1,5 @@ import bp from '../../../breakpoints'; -import { parseQueryStringIntoObject } from '../../../lib/utils/common_utils'; -import { mergeUrlParams, redirectTo } from '../../../lib/utils/url_utility'; +import { s__, sprintf } from '~/locale'; export default class Wikis { constructor() { @@ -12,32 +11,37 @@ export default class Wikis { sidebarToggles[i].addEventListener('click', e => this.handleToggleSidebar(e)); } - this.newWikiForm = document.querySelector('form.new-wiki-page'); - if (this.newWikiForm) { - this.newWikiForm.addEventListener('submit', e => this.handleNewWikiSubmit(e)); + this.isNewWikiPage = Boolean(document.querySelector('.js-new-wiki-page')); + this.editTitleInput = document.querySelector('form.wiki-form #wiki_title'); + this.commitMessageInput = document.querySelector('form.wiki-form #wiki_message'); + this.commitMessageI18n = this.isNewWikiPage + ? s__('WikiPageCreate|Create %{pageTitle}') + : s__('WikiPageEdit|Update %{pageTitle}'); + + if (this.editTitleInput) { + // Initialize the commit message on load + if (this.editTitleInput.value) this.setWikiCommitMessage(this.editTitleInput.value); + + // Set the commit message as the page title is changed + this.editTitleInput.addEventListener('keyup', e => this.handleWikiTitleChange(e)); } window.addEventListener('resize', () => this.renderSidebar()); this.renderSidebar(); } - handleNewWikiSubmit(e) { - if (!this.newWikiForm) return; - - const slugInput = this.newWikiForm.querySelector('#new_wiki_path'); - - const slug = slugInput.value; + handleWikiTitleChange(e) { + this.setWikiCommitMessage(e.target.value); + } - if (slug.length > 0) { - const wikisPath = slugInput.getAttribute('data-wikis-path'); + setWikiCommitMessage(rawTitle) { + let title = rawTitle; - // If the wiki is empty, we need to merge the current URL params to keep the "create" view. - const params = parseQueryStringIntoObject(window.location.search.substr(1)); - const url = mergeUrlParams(params, `${wikisPath}/${slug}`); - redirectTo(url); + // Replace hyphens with spaces + if (title) title = title.replace(/-+/g, ' '); - e.preventDefault(); - } + const newCommitMessage = sprintf(this.commitMessageI18n, { pageTitle: title }); + this.commitMessageInput.value = newCommitMessage; } handleToggleSidebar(e) { diff --git a/app/assets/javascripts/pages/search/show/refresh_counts.js b/app/assets/javascripts/pages/search/show/refresh_counts.js new file mode 100644 index 00000000000..fa75ee6075d --- /dev/null +++ b/app/assets/javascripts/pages/search/show/refresh_counts.js @@ -0,0 +1,24 @@ +import axios from '~/lib/utils/axios_utils'; + +function showCount(el, count) { + el.textContent = count; + el.classList.remove('hidden'); +} + +function refreshCount(el) { + const { url } = el.dataset; + + return axios + .get(url) + .then(({ data }) => showCount(el, data.count)) + .catch(e => { + // eslint-disable-next-line no-console + console.error(`Failed to fetch search count from '${url}'.`, e); + }); +} + +export default function refreshCounts() { + const elements = Array.from(document.querySelectorAll('.js-search-count')); + + return Promise.all(elements.map(refreshCount)); +} diff --git a/app/assets/javascripts/pages/search/show/search.js b/app/assets/javascripts/pages/search/show/search.js index 81b6225cb18..86ec78e1df8 100644 --- a/app/assets/javascripts/pages/search/show/search.js +++ b/app/assets/javascripts/pages/search/show/search.js @@ -3,6 +3,7 @@ import Flash from '~/flash'; import Api from '~/api'; import { __ } from '~/locale'; import Project from '~/pages/projects/project'; +import refreshCounts from './refresh_counts'; export default class Search { constructor() { @@ -14,6 +15,7 @@ export default class Search { this.groupId = $groupDropdown.data('groupId'); this.eventListeners(); + refreshCounts(); $groupDropdown.glDropdown({ selectable: true, diff --git a/app/assets/javascripts/pipelines/components/pipeline_url.vue b/app/assets/javascripts/pipelines/components/pipeline_url.vue index e01080b04d6..a08f732dda7 100644 --- a/app/assets/javascripts/pipelines/components/pipeline_url.vue +++ b/app/assets/javascripts/pipelines/components/pipeline_url.vue @@ -67,7 +67,7 @@ export default { <span v-if="pipeline.flags.latest" v-gl-tooltip - :title="__('Latest pipeline for this branch')" + :title="__('Latest pipeline for the most recent commit on this branch')" class="js-pipeline-url-latest badge badge-success" > {{ __('latest') }} @@ -104,7 +104,7 @@ export default { v-gl-tooltip :title=" __( - 'The code of a detached pipeline is tested against the source branch instead of merged results', + 'Pipelines for merge requests are configured. A detached pipeline runs in the context of the merge request, and not against the merged result. Learn more on the documentation for Pipelines for Merged Results.', ) " class="js-pipeline-url-detached badge badge-info" diff --git a/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue b/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue index 03281aa1317..12ee1ce2f0c 100644 --- a/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue +++ b/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue @@ -38,7 +38,9 @@ export default { }, computed: { statusTitle() { - return sprintf(s__('Commits|Commit: %{commitText}'), { commitText: this.ciStatus.text }); + return sprintf(s__('PipelineStatusTooltip|Pipeline: %{ciStatus}'), { + ciStatus: this.ciStatus.text, + }); }, }, mounted() { diff --git a/app/assets/javascripts/registry/components/app.vue b/app/assets/javascripts/registry/components/app.vue index efbf0a4e3cf..346dc470a59 100644 --- a/app/assets/javascripts/registry/components/app.vue +++ b/app/assets/javascripts/registry/components/app.vue @@ -1,10 +1,9 @@ <script> import { mapGetters, mapActions } from 'vuex'; -import { GlLoadingIcon } from '@gitlab/ui'; +import { GlLoadingIcon, GlEmptyState } from '@gitlab/ui'; import store from '../stores'; import clipboardButton from '../../vue_shared/components/clipboard_button.vue'; import CollapsibleContainer from './collapsible_container.vue'; -import SvgMessage from './svg_message.vue'; import { s__, sprintf } from '../../locale'; export default { @@ -12,8 +11,8 @@ export default { components: { clipboardButton, CollapsibleContainer, + GlEmptyState, GlLoadingIcon, - SvgMessage, }, props: { endpoint: { @@ -93,7 +92,9 @@ export default { this.setMainEndpoint(this.endpoint); }, mounted() { - this.fetchRepos(); + if (!this.characterError) { + this.fetchRepos(); + } }, methods: { ...mapActions(['setMainEndpoint', 'fetchRepos']), @@ -102,61 +103,63 @@ export default { </script> <template> <div> - <svg-message v-if="characterError" id="invalid-characters" :svg-path="containersErrorImage"> - <h4> - {{ s__('ContainerRegistry|Docker connection error') }} - </h4> - <p v-html="dockerConnectionErrorText"></p> - </svg-message> + <gl-empty-state + v-if="characterError" + :title="s__('ContainerRegistry|Docker connection error')" + :svg-path="containersErrorImage" + > + <template #description> + <p v-html="dockerConnectionErrorText"></p> + </template> + </gl-empty-state> - <gl-loading-icon v-else-if="isLoading && !characterError" size="md" class="prepend-top-16" /> + <gl-loading-icon v-else-if="isLoading" size="md" class="prepend-top-16" /> - <div v-else-if="!isLoading && !characterError && repos.length"> + <div v-else-if="!isLoading && repos.length"> <h4>{{ s__('ContainerRegistry|Container Registry') }}</h4> <p v-html="introText"></p> <collapsible-container v-for="item in repos" :key="item.id" :repo="item" /> </div> - <svg-message - v-else-if="!isLoading && !characterError && !repos.length" - id="no-container-images" + <gl-empty-state + v-else + :title="s__('ContainerRegistry|There are no container images stored for this project')" :svg-path="noContainersImage" + class="container-message" > - <h4> - {{ s__('ContainerRegistry|There are no container images stored for this project') }} - </h4> - <p v-html="noContainerImagesText"></p> - - <h5>{{ s__('ContainerRegistry|Quick Start') }}</h5> - <p> - {{ - s__( - 'ContainerRegistry|You can add an image to this registry with the following commands:', - ) - }} - </p> + <template #description> + <p class="js-no-container-images-text" v-html="noContainerImagesText"></p> + <h5>{{ s__('ContainerRegistry|Quick Start') }}</h5> + <p> + {{ + s__( + 'ContainerRegistry|You can add an image to this registry with the following commands:', + ) + }} + </p> - <div class="input-group append-bottom-10"> - <input :value="dockerBuildCommand" type="text" class="form-control monospace" readonly /> - <span class="input-group-append"> - <clipboard-button - :text="dockerBuildCommand" - :title="s__('ContainerRegistry|Copy build command to clipboard')" - class="input-group-text" - /> - </span> - </div> + <div class="input-group append-bottom-10"> + <input :value="dockerBuildCommand" type="text" class="form-control monospace" readonly /> + <span class="input-group-append"> + <clipboard-button + :text="dockerBuildCommand" + :title="s__('ContainerRegistry|Copy build command to clipboard')" + class="input-group-text" + /> + </span> + </div> - <div class="input-group"> - <input :value="dockerPushCommand" type="text" class="form-control monospace" readonly /> - <span class="input-group-append"> - <clipboard-button - :text="dockerPushCommand" - :title="s__('ContainerRegistry|Copy push command to clipboard')" - class="input-group-text" - /> - </span> - </div> - </svg-message> + <div class="input-group"> + <input :value="dockerPushCommand" type="text" class="form-control monospace" readonly /> + <span class="input-group-append"> + <clipboard-button + :text="dockerPushCommand" + :title="s__('ContainerRegistry|Copy push command to clipboard')" + class="input-group-text" + /> + </span> + </div> + </template> + </gl-empty-state> </div> </template> diff --git a/app/assets/javascripts/registry/components/collapsible_container.vue b/app/assets/javascripts/registry/components/collapsible_container.vue index e157036871b..bfb2305c48c 100644 --- a/app/assets/javascripts/registry/components/collapsible_container.vue +++ b/app/assets/javascripts/registry/components/collapsible_container.vue @@ -84,7 +84,7 @@ export default { v-gl-modal="modalId" :title="s__('ContainerRegistry|Remove repository')" :aria-label="s__('ContainerRegistry|Remove repository')" - class="js-remove-repo" + class="js-remove-repo btn-inverted" variant="danger" > <icon name="remove" /> diff --git a/app/assets/javascripts/registry/components/svg_message.vue b/app/assets/javascripts/registry/components/svg_message.vue deleted file mode 100644 index 617093e054e..00000000000 --- a/app/assets/javascripts/registry/components/svg_message.vue +++ /dev/null @@ -1,26 +0,0 @@ -<script> -export default { - name: 'RegistrySvgMessage', - props: { - id: { - type: String, - required: true, - }, - svgPath: { - type: String, - required: true, - }, - }, -}; -</script> - -<template> - <div :id="id" class="empty-state container-message"> - <div class="svg-content"> - <img :src="svgPath" class="flex-align-self-center" /> - </div> - <div class="text-content"> - <slot></slot> - </div> - </div> -</template> diff --git a/app/assets/javascripts/registry/components/table_registry.vue b/app/assets/javascripts/registry/components/table_registry.vue index a498a553908..e9067bc2b56 100644 --- a/app/assets/javascripts/registry/components/table_registry.vue +++ b/app/assets/javascripts/registry/components/table_registry.vue @@ -1,7 +1,13 @@ <script> import { mapActions } from 'vuex'; -import { GlButton, GlTooltipDirective, GlModal, GlModalDirective } from '@gitlab/ui'; -import { n__ } from '../../locale'; +import { + GlButton, + GlFormCheckbox, + GlTooltipDirective, + GlModal, + GlModalDirective, +} from '@gitlab/ui'; +import { n__, s__, sprintf } from '../../locale'; import createFlash from '../../flash'; import ClipboardButton from '../../vue_shared/components/clipboard_button.vue'; import TablePagination from '../../vue_shared/components/pagination/table_pagination.vue'; @@ -14,6 +20,7 @@ export default { components: { ClipboardButton, TablePagination, + GlFormCheckbox, GlButton, Icon, GlModal, @@ -31,33 +38,98 @@ export default { }, data() { return { - itemToBeDeleted: null, + itemsToBeDeleted: [], modalId: `confirm-image-deletion-modal-${this.repo.id}`, + selectAllChecked: false, + modalDescription: '', }; }, computed: { + bulkDeletePath() { + return this.repo.tagsPath ? this.repo.tagsPath.replace('?format=json', '/bulk_destroy') : ''; + }, shouldRenderPagination() { return this.repo.pagination.total > this.repo.pagination.perPage; }, + modalTitle() { + return n__( + 'ContainerRegistry|Remove image', + 'ContainerRegistry|Remove images', + this.itemsToBeDeleted.length === 0 ? 1 : this.itemsToBeDeleted.length, + ); + }, + }, + mounted() { + this.$refs.deleteModal.$refs.modal.$on('hide', this.removeModalEvents); }, methods: { - ...mapActions(['fetchList', 'deleteItem']), + ...mapActions(['fetchList', 'deleteItem', 'multiDeleteItems']), + setModalDescription(itemIndex = -1) { + if (itemIndex === -1) { + this.modalDescription = sprintf( + s__(`ContainerRegistry|You are about to delete <b>%{count}</b> images. This will + delete the images and all tags pointing to them.`), + { count: this.itemsToBeDeleted.length }, + ); + } else { + const { tag } = this.repo.list[itemIndex]; + + this.modalDescription = sprintf( + s__(`ContainerRegistry|You are about to delete the image <b>%{title}</b>. This will + delete the image and all tags pointing to this image.`), + { title: `${this.repo.name}:${tag}` }, + ); + } + }, layers(item) { return item.layers ? n__('%d layer', '%d layers', item.layers) : ''; }, formatSize(size) { return numberToHumanSize(size); }, - setItemToBeDeleted(item) { - this.itemToBeDeleted = item; + removeModalEvents() { + this.$refs.deleteModal.$refs.modal.$off('ok'); + }, + deleteSingleItem(index) { + this.setModalDescription(index); + + this.$refs.deleteModal.$refs.modal.$once('ok', () => { + this.removeModalEvents(); + this.handleSingleDelete(this.repo.list[index]); + }); + }, + deleteMultipleItems() { + if (this.itemsToBeDeleted.length === 1) { + this.setModalDescription(this.itemsToBeDeleted[0]); + } else if (this.itemsToBeDeleted.length > 1) { + this.setModalDescription(); + } + + this.$refs.deleteModal.$refs.modal.$once('ok', () => { + this.removeModalEvents(); + this.handleMultipleDelete(); + }); }, - handleDeleteRegistry() { - const { itemToBeDeleted } = this; - this.itemToBeDeleted = null; - this.deleteItem(itemToBeDeleted) + handleSingleDelete(itemToDelete) { + this.deleteItem(itemToDelete) .then(() => this.fetchList({ repo: this.repo })) .catch(() => this.showError(errorMessagesTypes.DELETE_REGISTRY)); }, + handleMultipleDelete() { + const { itemsToBeDeleted } = this; + this.itemsToBeDeleted = []; + + if (this.bulkDeletePath) { + this.multiDeleteItems({ + path: this.bulkDeletePath, + items: itemsToBeDeleted.map(x => this.repo.list[x].tag), + }) + .then(() => this.fetchList({ repo: this.repo })) + .catch(() => this.showError(errorMessagesTypes.DELETE_REGISTRY)); + } else { + this.showError(errorMessagesTypes.DELETE_REGISTRY); + } + }, onPageChange(pageNumber) { this.fetchList({ repo: this.repo, page: pageNumber }).catch(() => this.showError(errorMessagesTypes.FETCH_REGISTRY), @@ -66,6 +138,35 @@ export default { showError(message) { createFlash(errorMessages[message]); }, + onSelectAllChange() { + if (this.selectAllChecked) { + this.deselectAll(); + } else { + this.selectAll(); + } + }, + selectAll() { + this.itemsToBeDeleted = this.repo.list.map((x, index) => index); + this.selectAllChecked = true; + }, + deselectAll() { + this.itemsToBeDeleted = []; + this.selectAllChecked = false; + }, + updateItemsToBeDeleted(index) { + const delIndex = this.itemsToBeDeleted.findIndex(x => x === index); + + if (delIndex > -1) { + this.itemsToBeDeleted.splice(delIndex, 1); + this.selectAllChecked = false; + } else { + this.itemsToBeDeleted.push(index); + + if (this.itemsToBeDeleted.length === this.repo.list.length) { + this.selectAllChecked = true; + } + } + }, }, }; </script> @@ -74,15 +175,44 @@ export default { <table class="table tags"> <thead> <tr> + <th> + <gl-form-checkbox + v-if="repo.canDelete" + class="js-select-all-checkbox" + :checked="selectAllChecked" + @change="onSelectAllChange" + /> + </th> <th>{{ s__('ContainerRegistry|Tag') }}</th> <th>{{ s__('ContainerRegistry|Tag ID') }}</th> <th>{{ s__('ContainerRegistry|Size') }}</th> <th>{{ s__('ContainerRegistry|Last Updated') }}</th> - <th></th> + <th> + <gl-button + v-if="repo.canDelete" + v-gl-tooltip + v-gl-modal="modalId" + :disabled="!itemsToBeDeleted || itemsToBeDeleted.length === 0" + class="js-delete-registry float-right" + variant="danger" + :title="s__('ContainerRegistry|Remove selected images')" + :aria-label="s__('ContainerRegistry|Remove selected images')" + @click="deleteMultipleItems()" + ><icon name="remove" + /></gl-button> + </th> </tr> </thead> <tbody> - <tr v-for="item in repo.list" :key="item.tag"> + <tr v-for="(item, index) in repo.list" :key="item.tag" class="registry-image-row"> + <td class="check"> + <gl-form-checkbox + v-if="item.canDelete" + class="js-select-checkbox" + :checked="itemsToBeDeleted && itemsToBeDeleted.includes(index)" + @change="updateItemsToBeDeleted(index)" + /> + </td> <td class="monospace"> {{ item.tag }} <clipboard-button @@ -111,16 +241,15 @@ export default { </span> </td> - <td class="content"> + <td class="content action-buttons"> <gl-button v-if="item.canDelete" - v-gl-tooltip v-gl-modal="modalId" :title="s__('ContainerRegistry|Remove image')" :aria-label="s__('ContainerRegistry|Remove image')" variant="danger" - class="js-delete-registry d-none d-sm-block float-right" - @click="setItemToBeDeleted(item)" + class="js-delete-registry-row float-right btn-inverted btn-border-color btn-icon" + @click="deleteSingleItem(index)" > <icon name="remove" /> </gl-button> @@ -135,19 +264,10 @@ export default { :page-info="repo.pagination" /> - <gl-modal :modal-id="modalId" ok-variant="danger" @ok="handleDeleteRegistry"> - <template v-slot:modal-title>{{ s__('ContainerRegistry|Remove image') }}</template> - <template v-slot:modal-ok>{{ s__('ContainerRegistry|Remove image and tags') }}</template> - <p - v-html=" - sprintf( - s__( - 'ContainerRegistry|You are about to delete the image <b>%{title}</b>. This will delete the image and all tags pointing to this image.', - ), - { title: repo.name }, - ) - " - ></p> + <gl-modal ref="deleteModal" :modal-id="modalId" ok-variant="danger"> + <template v-slot:modal-title>{{ modalTitle }}</template> + <template v-slot:modal-ok>{{ s__('ContainerRegistry|Remove image(s) and tags') }}</template> + <p v-html="modalDescription"></p> </gl-modal> </div> </template> diff --git a/app/assets/javascripts/registry/stores/actions.js b/app/assets/javascripts/registry/stores/actions.js index 0f5e9cc73a0..a2e0130e79e 100644 --- a/app/assets/javascripts/registry/stores/actions.js +++ b/app/assets/javascripts/registry/stores/actions.js @@ -36,6 +36,8 @@ export const fetchList = ({ commit }, { repo, page }) => { }; export const deleteItem = (_, item) => axios.delete(item.destroyPath); +export const multiDeleteItems = (_, { path, items }) => + axios.delete(path, { params: { ids: items } }); export const setMainEndpoint = ({ commit }, data) => commit(types.SET_MAIN_ENDPOINT, data); export const toggleLoading = ({ commit }) => commit(types.TOGGLE_MAIN_LOADING); diff --git a/app/assets/javascripts/sidebar/components/assignees/assignee_avatar.vue b/app/assets/javascripts/sidebar/components/assignees/assignee_avatar.vue new file mode 100644 index 00000000000..71a1fc31315 --- /dev/null +++ b/app/assets/javascripts/sidebar/components/assignees/assignee_avatar.vue @@ -0,0 +1,48 @@ +<script> +import { __, sprintf } from '~/locale'; + +export default { + props: { + user: { + type: Object, + required: true, + }, + imgSize: { + type: Number, + required: true, + }, + issuableType: { + type: String, + required: false, + default: 'issue', + }, + }, + computed: { + assigneeAlt() { + return sprintf(__("%{userName}'s avatar"), { userName: this.user.name }); + }, + avatarUrl() { + return this.user.avatar || this.user.avatar_url || gon.default_avatar_url; + }, + isMergeRequest() { + return this.issuableType === 'merge_request'; + }, + hasMergeIcon() { + return this.isMergeRequest && !this.user.can_merge; + }, + }, +}; +</script> + +<template> + <span class="position-relative"> + <img + :alt="assigneeAlt" + :src="avatarUrl" + :width="imgSize" + :class="`s${imgSize}`" + class="avatar avatar-inline m-0" + /> + <i v-if="hasMergeIcon" aria-hidden="true" class="fa fa-exclamation-triangle merge-icon"></i> + </span> +</template> diff --git a/app/assets/javascripts/sidebar/components/assignees/assignee_avatar_link.vue b/app/assets/javascripts/sidebar/components/assignees/assignee_avatar_link.vue new file mode 100644 index 00000000000..6633a63d046 --- /dev/null +++ b/app/assets/javascripts/sidebar/components/assignees/assignee_avatar_link.vue @@ -0,0 +1,83 @@ +<script> +import { __, sprintf } from '~/locale'; +import { GlTooltipDirective, GlLink } from '@gitlab/ui'; +import { joinPaths } from '~/lib/utils/url_utility'; +import AssigneeAvatar from './assignee_avatar.vue'; + +export default { + components: { + AssigneeAvatar, + GlLink, + }, + directives: { + GlTooltip: GlTooltipDirective, + }, + props: { + user: { + type: Object, + required: true, + }, + rootPath: { + type: String, + required: true, + }, + tooltipPlacement: { + type: String, + default: 'bottom', + required: false, + }, + tooltipHasName: { + type: Boolean, + default: true, + required: false, + }, + issuableType: { + type: String, + default: 'issue', + required: false, + }, + }, + computed: { + cannotMerge() { + return this.issuableType === 'merge_request' && !this.user.can_merge; + }, + tooltipTitle() { + if (this.cannotMerge && this.tooltipHasName) { + return sprintf(__('%{userName} (cannot merge)'), { userName: this.user.name }); + } else if (this.cannotMerge) { + return __('Cannot merge'); + } else if (this.tooltipHasName) { + return this.user.name; + } + + return ''; + }, + tooltipOption() { + return { + container: 'body', + placement: this.tooltipPlacement, + boundary: 'viewport', + }; + }, + assigneeUrl() { + return joinPaths(`${this.rootPath}`, `${this.user.username}`); + }, + }, +}; +</script> + +<template> + <!-- must be `d-inline-block` or parent flex-basis causes width issues --> + <gl-link + v-gl-tooltip="tooltipOption" + :href="assigneeUrl" + :title="tooltipTitle" + class="d-inline-block" + > + <!-- use d-flex so that slot can be appropriately styled --> + <span class="d-flex"> + <assignee-avatar :user="user" :img-size="32" :issuable-type="issuableType" /> + <slot :user="user"></slot> + </span> + </gl-link> +</template> diff --git a/app/assets/javascripts/sidebar/components/assignees/assignee_title.vue b/app/assets/javascripts/sidebar/components/assignees/assignee_title.vue index fa6b6bfaef1..63b93a80ead 100644 --- a/app/assets/javascripts/sidebar/components/assignees/assignee_title.vue +++ b/app/assets/javascripts/sidebar/components/assignees/assignee_title.vue @@ -1,5 +1,6 @@ <script> import { n__ } from '~/locale'; +import { trackEvent } from 'ee_else_ce/event_tracking/issue_sidebar'; export default { name: 'AssigneeTitle', @@ -29,13 +30,23 @@ export default { return n__('Assignee', `%d Assignees`, assignees); }, }, + methods: { + trackEdit() { + trackEvent('click_edit_button', 'assignee'); + }, + }, }; </script> <template> <div class="title hide-collapsed"> {{ assigneeTitle }} <i v-if="loading" aria-hidden="true" class="fa fa-spinner fa-spin block-loading"></i> - <a v-if="editable" class="js-sidebar-dropdown-toggle edit-link float-right" href="#"> + <a + v-if="editable" + class="js-sidebar-dropdown-toggle edit-link float-right" + href="#" + @click.prevent="trackEdit" + > {{ __('Edit') }} </a> <a diff --git a/app/assets/javascripts/sidebar/components/assignees/assignees.vue b/app/assets/javascripts/sidebar/components/assignees/assignees.vue index 631e2e28d4d..d9739e8d197 100644 --- a/app/assets/javascripts/sidebar/components/assignees/assignees.vue +++ b/app/assets/javascripts/sidebar/components/assignees/assignees.vue @@ -1,13 +1,14 @@ <script> -import { __, sprintf } from '~/locale'; -import tooltip from '~/vue_shared/directives/tooltip'; +import CollapsedAssigneeList from '../assignees/collapsed_assignee_list.vue'; +import UncollapsedAssigneeList from '../assignees/uncollapsed_assignee_list.vue'; export default { // name: 'Assignees' is a false positive: https://gitlab.com/gitlab-org/frontend/eslint-plugin-i18n/issues/26#possible-false-positives // eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings name: 'Assignees', - directives: { - tooltip, + components: { + CollapsedAssigneeList, + UncollapsedAssigneeList, }, props: { rootPath: { @@ -24,171 +25,34 @@ export default { }, issuableType: { type: String, - require: true, + required: false, default: 'issue', }, }, - data() { - return { - defaultRenderCount: 5, - defaultMaxCounter: 99, - showLess: true, - }; - }, computed: { - firstUser() { - return this.users[0]; - }, - hasMoreThanTwoAssignees() { - return this.users.length > 2; - }, - hasMoreThanOneAssignee() { - return this.users.length > 1; - }, - hasAssignees() { - return this.users.length > 0; - }, hasNoUsers() { return !this.users.length; }, - hasOneUser() { - return this.users.length === 1; - }, - renderShowMoreSection() { - return this.users.length > this.defaultRenderCount; - }, - numberOfHiddenAssignees() { - return this.users.length - this.defaultRenderCount; - }, - isHiddenAssignees() { - return this.numberOfHiddenAssignees > 0; - }, - hiddenAssigneesLabel() { - const { numberOfHiddenAssignees } = this; - return sprintf(__('+ %{numberOfHiddenAssignees} more'), { numberOfHiddenAssignees }); - }, - collapsedTooltipTitle() { - const maxRender = Math.min(this.defaultRenderCount, this.users.length); - const renderUsers = this.users.slice(0, maxRender); - const names = renderUsers.map(u => u.name); - - if (this.users.length > maxRender) { - names.push(`+ ${this.users.length - maxRender} more`); - } - - if (!this.users.length) { - const emptyTooltipLabel = __('Assignee(s)'); - names.push(emptyTooltipLabel); - } - - return names.join(', '); - }, - sidebarAvatarCounter() { - let counter = `+${this.users.length - 1}`; - - if (this.users.length > this.defaultMaxCounter) { - counter = `${this.defaultMaxCounter}+`; - } + sortedAssigness() { + const canMergeUsers = this.users.filter(user => user.can_merge); + const canNotMergeUsers = this.users.filter(user => !user.can_merge); - return counter; - }, - mergeNotAllowedTooltipMessage() { - const assigneesCount = this.users.length; - - if (this.issuableType !== 'merge_request' || assigneesCount === 0) { - return null; - } - - const cannotMergeCount = this.users.filter(u => u.can_merge === false).length; - const canMergeCount = assigneesCount - cannotMergeCount; - - if (canMergeCount === assigneesCount) { - // Everyone can merge - return null; - } else if (cannotMergeCount === assigneesCount && assigneesCount > 1) { - return __('No one can merge'); - } else if (assigneesCount === 1) { - return __('Cannot merge'); - } - - return sprintf(__('%{canMergeCount}/%{assigneesCount} can merge'), { - canMergeCount, - assigneesCount, - }); + return [...canMergeUsers, ...canNotMergeUsers]; }, }, methods: { assignSelf() { this.$emit('assign-self'); }, - toggleShowLess() { - this.showLess = !this.showLess; - }, - renderAssignee(index) { - return !this.showLess || (index < this.defaultRenderCount && this.showLess); - }, - avatarUrl(user) { - return user.avatar || user.avatar_url || gon.default_avatar_url; - }, - assigneeUrl(user) { - return `${this.rootPath}${user.username}`; - }, - assigneeAlt(user) { - return sprintf(__("%{userName}'s avatar"), { userName: user.name }); - }, - assigneeUsername(user) { - return `@${user.username}`; - }, - shouldRenderCollapsedAssignee(index) { - const firstTwo = this.users.length <= 2 && index <= 2; - - return index === 0 || firstTwo; - }, }, }; </script> <template> <div> - <div - v-tooltip - :class="{ 'multiple-users': hasMoreThanOneAssignee }" - :title="collapsedTooltipTitle" - class="sidebar-collapsed-icon sidebar-collapsed-user" - data-container="body" - data-placement="left" - data-boundary="viewport" - > - <i v-if="hasNoUsers" :aria-label="__('None')" class="fa fa-user"> </i> - <button - v-for="(user, index) in users" - v-if="shouldRenderCollapsedAssignee(index)" - :key="user.id" - type="button" - class="btn-link" - > - <img - :alt="assigneeAlt(user)" - :src="avatarUrl(user)" - width="24" - class="avatar avatar-inline s24" - /> - <span class="author"> {{ user.name }} </span> - </button> - <button v-if="hasMoreThanTwoAssignees" class="btn-link" type="button"> - <span class="avatar-counter sidebar-avatar-counter"> {{ sidebarAvatarCounter }} </span> - </button> - </div> + <collapsed-assignee-list :users="sortedAssigness" :issuable-type="issuableType" /> + <div class="value hide-collapsed"> - <span - v-if="mergeNotAllowedTooltipMessage" - v-tooltip - :title="mergeNotAllowedTooltipMessage" - data-placement="left" - class="float-right cannot-be-merged" - > - <i aria-hidden="true" data-hidden="true" class="fa fa-exclamation-triangle"></i> - </span> <template v-if="hasNoUsers"> <span class="assign-yourself no-value qa-assign-yourself"> {{ __('None') }} @@ -200,51 +64,13 @@ export default { </template> </span> </template> - <template v-else-if="hasOneUser"> - <a :href="assigneeUrl(firstUser)" class="author-link bold"> - <img - :alt="assigneeAlt(firstUser)" - :src="avatarUrl(firstUser)" - width="32" - class="avatar avatar-inline s32" - /> - <span class="author"> {{ firstUser.name }} </span> - <span class="username"> {{ assigneeUsername(firstUser) }} </span> - </a> - </template> - <template v-else> - <div class="user-list"> - <div - v-for="(user, index) in users" - v-if="renderAssignee(index)" - :key="user.id" - class="user-item" - > - <a - :href="assigneeUrl(user)" - :data-title="user.name" - class="user-link has-tooltip" - data-container="body" - data-placement="bottom" - > - <img - :alt="assigneeAlt(user)" - :src="avatarUrl(user)" - width="32" - class="avatar avatar-inline s32" - /> - </a> - </div> - </div> - <div v-if="renderShowMoreSection" class="user-list-more"> - <button type="button" class="btn-link" @click="toggleShowLess"> - <template v-if="showLess"> - {{ hiddenAssigneesLabel }} - </template> - <template v-else>{{ __('- show less') }}</template> - </button> - </div> - </template> + + <uncollapsed-assignee-list + v-else + :users="sortedAssigness" + :root-path="rootPath" + :issuable-type="issuableType" + /> </div> </div> </template> diff --git a/app/assets/javascripts/sidebar/components/assignees/collapsed_assignee.vue b/app/assets/javascripts/sidebar/components/assignees/collapsed_assignee.vue new file mode 100644 index 00000000000..2f654409561 --- /dev/null +++ b/app/assets/javascripts/sidebar/components/assignees/collapsed_assignee.vue @@ -0,0 +1,27 @@ +<script> +import AssigneeAvatar from './assignee_avatar.vue'; + +export default { + components: { + AssigneeAvatar, + }, + props: { + user: { + type: Object, + required: true, + }, + issuableType: { + type: String, + required: false, + default: 'issue', + }, + }, +}; +</script> + +<template> + <button type="button" class="btn-link"> + <assignee-avatar :user="user" :img-size="24" :issuable-type="issuableType" /> + <span class="author"> {{ user.name }} </span> + </button> +</template> diff --git a/app/assets/javascripts/sidebar/components/assignees/collapsed_assignee_list.vue b/app/assets/javascripts/sidebar/components/assignees/collapsed_assignee_list.vue new file mode 100644 index 00000000000..5b4a43399ca --- /dev/null +++ b/app/assets/javascripts/sidebar/components/assignees/collapsed_assignee_list.vue @@ -0,0 +1,121 @@ +<script> +import { __, sprintf } from '~/locale'; +import { GlTooltipDirective } from '@gitlab/ui'; +import CollapsedAssignee from './collapsed_assignee.vue'; + +const DEFAULT_MAX_COUNTER = 99; +const DEFAULT_RENDER_COUNT = 5; + +export default { + directives: { + GlTooltip: GlTooltipDirective, + }, + components: { + CollapsedAssignee, + }, + props: { + users: { + type: Array, + required: true, + }, + issuableType: { + type: String, + required: false, + default: 'issue', + }, + }, + computed: { + isMergeRequest() { + return this.issuableType === 'merge_request'; + }, + hasNoUsers() { + return !this.users.length; + }, + hasMoreThanOneAssignee() { + return this.users.length > 1; + }, + hasMoreThanTwoAssignees() { + return this.users.length > 2; + }, + allAssigneesCanMerge() { + return this.users.every(user => user.can_merge); + }, + sidebarAvatarCounter() { + if (this.users.length > DEFAULT_MAX_COUNTER) { + return `${DEFAULT_MAX_COUNTER}+`; + } + + return `+${this.users.length - 1}`; + }, + collapsedUsers() { + const collapsedLength = this.hasMoreThanTwoAssignees ? 1 : this.users.length; + + return this.users.slice(0, collapsedLength); + }, + tooltipTitleMergeStatus() { + if (!this.isMergeRequest) { + return ''; + } + + const mergeLength = this.users.filter(u => u.can_merge).length; + + if (mergeLength === this.users.length) { + return ''; + } else if (mergeLength > 0) { + return sprintf(__('%{mergeLength}/%{usersLength} can merge'), { + mergeLength, + usersLength: this.users.length, + }); + } + + return this.users.length === 1 ? __('cannot merge') : __('no one can merge'); + }, + tooltipTitle() { + const maxRender = Math.min(DEFAULT_RENDER_COUNT, this.users.length); + const renderUsers = this.users.slice(0, maxRender); + const names = renderUsers.map(u => u.name); + + if (!this.users.length) { + return __('Assignee(s)'); + } + + if (this.users.length > names.length) { + names.push(sprintf(__('+ %{amount} more'), { amount: this.users.length - names.length })); + } + + const text = names.join(', '); + + return this.tooltipTitleMergeStatus ? `${text} (${this.tooltipTitleMergeStatus})` : text; + }, + + tooltipOptions() { + return { container: 'body', placement: 'left', boundary: 'viewport' }; + }, + }, +}; +</script> + +<template> + <div + v-gl-tooltip="tooltipOptions" + :class="{ 'multiple-users': hasMoreThanOneAssignee }" + :title="tooltipTitle" + class="sidebar-collapsed-icon sidebar-collapsed-user" + > + <i v-if="hasNoUsers" :aria-label="__('None')" class="fa fa-user"> </i> + <collapsed-assignee + v-for="user in collapsedUsers" + :key="user.id" + :user="user" + :issuable-type="issuableType" + /> + <button v-if="hasMoreThanTwoAssignees" class="btn-link" type="button"> + <span class="avatar-counter sidebar-avatar-counter"> {{ sidebarAvatarCounter }} </span> + <i + v-if="isMergeRequest && !allAssigneesCanMerge" + aria-hidden="true" + class="fa fa-exclamation-triangle merge-icon" + ></i> + </button> + </div> +</template> diff --git a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue index be1e4811856..c6cc04a139f 100644 --- a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue +++ b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue @@ -29,7 +29,7 @@ export default { }, issuableType: { type: String, - require: true, + required: false, default: 'issue', }, }, diff --git a/app/assets/javascripts/sidebar/components/assignees/uncollapsed_assignee_list.vue b/app/assets/javascripts/sidebar/components/assignees/uncollapsed_assignee_list.vue new file mode 100644 index 00000000000..3a4623121f4 --- /dev/null +++ b/app/assets/javascripts/sidebar/components/assignees/uncollapsed_assignee_list.vue @@ -0,0 +1,96 @@ +<script> +import { __, sprintf } from '~/locale'; +import AssigneeAvatarLink from './assignee_avatar_link.vue'; + +const DEFAULT_RENDER_COUNT = 5; + +export default { + components: { + AssigneeAvatarLink, + }, + props: { + users: { + type: Array, + required: true, + }, + rootPath: { + type: String, + required: true, + }, + issuableType: { + type: String, + required: false, + default: 'issue', + }, + }, + data() { + return { + showLess: true, + }; + }, + computed: { + firstUser() { + return this.users[0]; + }, + hasOneUser() { + return this.users.length === 1; + }, + hiddenAssigneesLabel() { + const { numberOfHiddenAssignees } = this; + return sprintf(__('+ %{numberOfHiddenAssignees} more'), { numberOfHiddenAssignees }); + }, + renderShowMoreSection() { + return this.users.length > DEFAULT_RENDER_COUNT; + }, + numberOfHiddenAssignees() { + return this.users.length - DEFAULT_RENDER_COUNT; + }, + uncollapsedUsers() { + const uncollapsedLength = this.showLess + ? Math.min(this.users.length, DEFAULT_RENDER_COUNT) + : this.users.length; + return this.showLess ? this.users.slice(0, uncollapsedLength) : this.users; + }, + username() { + return `@${this.firstUser.username}`; + }, + }, + methods: { + toggleShowLess() { + this.showLess = !this.showLess; + }, + }, +}; +</script> + +<template> + <assignee-avatar-link + v-if="hasOneUser" + v-slot="{ user }" + tooltip-placement="left" + :tooltip-has-name="false" + :user="firstUser" + :root-path="rootPath" + :issuable-type="issuableType" + > + <div class="ml-2"> + <span class="author"> {{ user.name }} </span> + <span class="username"> {{ username }} </span> + </div> + </assignee-avatar-link> + <div v-else> + <div class="user-list"> + <div v-for="user in uncollapsedUsers" :key="user.id" class="user-item"> + <assignee-avatar-link :user="user" :root-path="rootPath" :issuable-type="issuableType" /> + </div> + </div> + <div v-if="renderShowMoreSection" class="user-list-more"> + <button type="button" class="btn-link" @click="toggleShowLess"> + <template v-if="showLess"> + {{ hiddenAssigneesLabel }} + </template> + <template v-else>{{ __('- show less') }}</template> + </button> + </div> + </div> +</template> diff --git a/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue b/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue index 597b723a9d9..1c75b6148e8 100644 --- a/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue +++ b/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue @@ -5,6 +5,7 @@ import tooltip from '~/vue_shared/directives/tooltip'; import Icon from '~/vue_shared/components/icon.vue'; import eventHub from '~/sidebar/event_hub'; import editForm from './edit_form.vue'; +import { trackEvent } from 'ee_else_ce/event_tracking/issue_sidebar'; export default { components: { @@ -51,6 +52,11 @@ export default { toggleForm() { this.edit = !this.edit; }, + onEditClick() { + this.toggleForm(); + + trackEvent('click_edit_button', 'confidentiality'); + }, updateConfidentialAttribute(confidential) { this.service .update('issue', { confidential }) @@ -82,7 +88,7 @@ export default { v-if="isEditable" class="float-right confidential-edit" href="#" - @click.prevent="toggleForm" + @click.prevent="onEditClick" > {{ __('Edit') }} </a> 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 c5cfa92f3c8..ec2a7b93a98 100644 --- a/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue +++ b/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue @@ -6,6 +6,7 @@ import issuableMixin from '~/vue_shared/mixins/issuable'; import Icon from '~/vue_shared/components/icon.vue'; import eventHub from '~/sidebar/event_hub'; import editForm from './edit_form.vue'; +import { trackEvent } from 'ee_else_ce/event_tracking/issue_sidebar'; export default { components: { @@ -65,7 +66,11 @@ export default { toggleForm() { this.mediator.store.isLockDialogOpen = !this.mediator.store.isLockDialogOpen; }, + onEditClick() { + this.toggleForm(); + trackEvent('click_edit_button', 'lock_issue'); + }, updateLockedAttribute(locked) { this.mediator.service .update(this.issuableType, { @@ -109,7 +114,7 @@ export default { v-if="isEditable" class="float-right lock-edit" type="button" - @click.prevent="toggleForm" + @click.prevent="onEditClick" > {{ __('Edit') }} </button> diff --git a/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue b/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue index 0d1faceef11..1f5f19d1931 100644 --- a/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue +++ b/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue @@ -4,6 +4,7 @@ import icon from '~/vue_shared/components/icon.vue'; import toggleButton from '~/vue_shared/components/toggle_button.vue'; import tooltip from '~/vue_shared/directives/tooltip'; import eventHub from '../../event_hub'; +import { trackEvent } from 'ee_else_ce/event_tracking/issue_sidebar'; const ICON_ON = 'notifications'; const ICON_OFF = 'notifications-off'; @@ -63,6 +64,8 @@ export default { // Component event emission. this.$emit('toggleSubscription', this.id); + + trackEvent('toggle_button', 'notifications', this.subscribed ? 0 : 1); }, onClickCollapsedIcon() { this.$emit('toggleSidebar'); diff --git a/app/assets/javascripts/test_utils/simulate_drag.js b/app/assets/javascripts/test_utils/simulate_drag.js index be9ebc81c6b..c9bf234fcce 100644 --- a/app/assets/javascripts/test_utils/simulate_drag.js +++ b/app/assets/javascripts/test_utils/simulate_drag.js @@ -153,7 +153,11 @@ export default function simulateDrag(options) { if (progress >= 1) { if (options.ondragend) options.ondragend(); - simulateEvent(toEl, 'mouseup'); + + if (options.performDrop) { + simulateEvent(toEl, 'mouseup'); + } + clearInterval(dragInterval); window.SIMULATE_DRAG_ACTIVE = 0; } diff --git a/app/assets/javascripts/tracking.js b/app/assets/javascripts/tracking.js index 2d0b099cf0b..a852f937eec 100644 --- a/app/assets/javascripts/tracking.js +++ b/app/assets/javascripts/tracking.js @@ -15,8 +15,14 @@ const extractData = (el, opts = {}) => { }; export default class Tracking { + static trackable() { + return !['1', 'yes'].includes( + window.doNotTrack || navigator.doNotTrack || navigator.msDoNotTrack, + ); + } + static enabled() { - return typeof window.snowplow === 'function'; + return typeof window.snowplow === 'function' && this.trackable(); } static event(category = document.body.dataset.page, event = 'generic', data = {}) { diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js index 33cedf78331..12c939aa70f 100644 --- a/app/assets/javascripts/users_select.js +++ b/app/assets/javascripts/users_select.js @@ -62,6 +62,8 @@ function UsersSelect(currentUser, els, options = {}) { options.showCurrentUser = $dropdown.data('currentUser'); options.todoFilter = $dropdown.data('todoFilter'); options.todoStateFilter = $dropdown.data('todoStateFilter'); + options.iid = $dropdown.data('iid'); + options.issuableType = $dropdown.data('issuableType'); showNullUser = $dropdown.data('nullUser'); defaultNullUser = $dropdown.data('nullUserDefault'); showMenuAbove = $dropdown.data('showMenuAbove'); @@ -239,7 +241,7 @@ function UsersSelect(currentUser, els, options = {}) { '<% if( avatar ) { %> <a class="author-link" href="/<%- username %>"> <img width="24" class="avatar avatar-inline s24" alt="" src="<%- avatar %>"> </a> <% } else { %> <i class="fa fa-user"></i> <% } %>', ); assigneeTemplate = _.template( - `<% if (username) { %> <a class="author-link bold" href="/<%- username %>"> <% if( avatar ) { %> <img width="32" class="avatar avatar-inline s32" alt="" src="<%- avatar %>"> <% } %> <span class="author"><%- name %></span> <span class="username"> @<%- username %> </span> </a> <% } else { %> <span class="no-value assign-yourself"> + `<% if (username) { %> <a class="author-link bold" href="/<%- username %>"> <% if( avatar ) { %> <img width="32" class="avatar avatar-inline s32" alt="" src="<%- avatar %>"> <% } %> <span class="author"><%- name %></span> <span class="username"> @<%- username %> </span> </a> <% } else { %> <span class="no-value assign-yourself"> ${sprintf(s__('UsersSelect|No assignee - %{openingTag} assign yourself %{closingTag}'), { openingTag: '<a href="#" class="js-assign-yourself">', closingTag: '</a>', @@ -423,6 +425,8 @@ function UsersSelect(currentUser, els, options = {}) { const { $el, e, isMarking } = options; const user = options.selectedObj; + $el.tooltip('dispose'); + if ($dropdown.hasClass('js-multiselect')) { const isActive = $el.hasClass('is-active'); const previouslySelected = $dropdown @@ -570,20 +574,11 @@ function UsersSelect(currentUser, els, options = {}) { user.name, )}</a></li>`; } else { - img = "<img src='" + avatar + "' class='avatar avatar-inline' width='32' />"; + // 0 margin, because it's now handled by a wrapper + img = "<img src='" + avatar + "' class='avatar avatar-inline m-0' width='32' />"; } - return ` - <li data-user-id=${user.id}> - <a href='#' class='dropdown-menu-user-link ${selected === true ? 'is-active' : ''}'> - ${img} - <strong class='dropdown-menu-user-full-name'> - ${_.escape(user.name)} - </strong> - ${username ? `<span class='dropdown-menu-user-username'>${username}</span>` : ''} - </a> - </li> - `; + return _this.renderRow(options.issuableType, user, selected, username, img); }, }); }; @@ -764,6 +759,11 @@ UsersSelect.prototype.users = function(query, options, callback) { author_id: options.authorId || null, skip_users: options.skipUsers || null, }; + + if (options.issuableType === 'merge_request') { + params.merge_request_iid = options.iid || null; + } + return axios.get(url, { params }).then(({ data }) => { callback(data); }); @@ -776,4 +776,44 @@ UsersSelect.prototype.buildUrl = function(url) { return url; }; +UsersSelect.prototype.renderRow = function(issuableType, user, selected, username, img) { + const tooltip = issuableType === 'merge_request' && !user.can_merge ? __('Cannot merge') : ''; + const tooltipClass = tooltip ? `has-tooltip` : ''; + const selectedClass = selected === true ? 'is-active' : ''; + const linkClasses = `${selectedClass} ${tooltipClass}`; + const tooltipAttributes = tooltip + ? `data-container="body" data-placement="left" data-title="${tooltip}"` + : ''; + + return ` + <li data-user-id=${user.id}> + <a href="#" class="dropdown-menu-user-link d-flex align-items-center ${linkClasses}" ${tooltipAttributes}> + ${this.renderRowAvatar(issuableType, user, img)} + <span class="d-flex flex-column overflow-hidden"> + <strong class="dropdown-menu-user-full-name"> + ${_.escape(user.name)} + </strong> + ${username ? `<span class="dropdown-menu-user-username">${username}</span>` : ''} + </span> + </a> + </li> + `; +}; + +UsersSelect.prototype.renderRowAvatar = function(issuableType, user, img) { + if (user.beforeDivider) { + return img; + } + + const mergeIcon = + issuableType === 'merge_request' && !user.can_merge + ? '<i class="fa fa-exclamation-triangle merge-icon"></i>' + : ''; + + return `<span class="position-relative mr-2"> + ${img} + ${mergeIcon} + </span>`; +}; + export default UsersSelect; diff --git a/app/assets/javascripts/visual_review_toolbar/components/comment.js b/app/assets/javascripts/visual_review_toolbar/components/comment.js deleted file mode 100644 index a03dc14b319..00000000000 --- a/app/assets/javascripts/visual_review_toolbar/components/comment.js +++ /dev/null @@ -1,39 +0,0 @@ -import { nextView } from '../store'; -import { localStorage, COMMENT_BOX, LOGOUT, STORAGE_MR_ID, STORAGE_TOKEN } from '../shared'; -import { clearNote } from './note'; -import { buttonClearStyles } from './utils'; -import { addForm } from './wrapper'; -import { changeSelectedMr, selectedMrNote } from './comment_mr_note'; -import postComment from './comment_post'; -import { saveComment, getSavedComment } from './comment_storage'; - -const comment = state => { - const savedComment = getSavedComment(); - - return ` - <div> - <textarea id="${COMMENT_BOX}" name="${COMMENT_BOX}" rows="3" placeholder="Enter your feedback or idea" class="gitlab-input" aria-required="true">${savedComment}</textarea> - ${selectedMrNote(state)} - <p class="gitlab-metadata-note">Additional metadata will be included: browser, OS, current page, user agent, and viewport dimensions.</p> - </div> - <div class="gitlab-button-wrapper"> - <button class="gitlab-button gitlab-button-success" style="${buttonClearStyles}" type="button" id="gitlab-comment-button"> Send feedback </button> - <button class="gitlab-button gitlab-button-secondary" style="${buttonClearStyles}" type="button" id="${LOGOUT}"> Log out </button> - </div> - `; -}; - -// This function is here becaause it is called only from the comment view -// If we reach a design where we can logout from multiple views, promote this -// to it's own package -const logoutUser = state => { - localStorage.removeItem(STORAGE_TOKEN); - localStorage.removeItem(STORAGE_MR_ID); - state.token = ''; - state.mergeRequestId = ''; - - clearNote(); - addForm(nextView(state, COMMENT_BOX)); -}; - -export { changeSelectedMr, comment, logoutUser, postComment, saveComment }; diff --git a/app/assets/javascripts/visual_review_toolbar/components/comment_mr_note.js b/app/assets/javascripts/visual_review_toolbar/components/comment_mr_note.js deleted file mode 100644 index da67763261c..00000000000 --- a/app/assets/javascripts/visual_review_toolbar/components/comment_mr_note.js +++ /dev/null @@ -1,31 +0,0 @@ -import { nextView } from '../store'; -import { localStorage, CHANGE_MR_ID_BUTTON, COMMENT_BOX, STORAGE_MR_ID } from '../shared'; -import { clearNote } from './note'; -import { buttonClearStyles } from './utils'; -import { addForm } from './wrapper'; - -const selectedMrNote = state => { - const { mrUrl, projectPath, mergeRequestId } = state; - - const mrLink = `${mrUrl}/${projectPath}/merge_requests/${mergeRequestId}`; - - return ` - <p class="gitlab-metadata-note"> - This posts to merge request <a class="gitlab-link" href="${mrLink}">!${mergeRequestId}</a>. - <button style="${buttonClearStyles}" type="button" id="${CHANGE_MR_ID_BUTTON}" class="gitlab-link gitlab-link-button">Change</button> - </p> - `; -}; - -const clearMrId = state => { - localStorage.removeItem(STORAGE_MR_ID); - state.mergeRequestId = ''; -}; - -const changeSelectedMr = state => { - clearMrId(state); - clearNote(); - addForm(nextView(state, COMMENT_BOX)); -}; - -export { changeSelectedMr, selectedMrNote }; diff --git a/app/assets/javascripts/visual_review_toolbar/components/comment_post.js b/app/assets/javascripts/visual_review_toolbar/components/comment_post.js deleted file mode 100644 index ee5f2b62425..00000000000 --- a/app/assets/javascripts/visual_review_toolbar/components/comment_post.js +++ /dev/null @@ -1,145 +0,0 @@ -import { BLACK, COMMENT_BOX, MUTED } from '../shared'; -import { clearSavedComment } from './comment_storage'; -import { clearNote, postError } from './note'; -import { selectCommentBox, selectCommentButton, selectNote, selectNoteContainer } from './utils'; - -const resetCommentButton = () => { - const commentButton = selectCommentButton(); - - /* eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings */ - commentButton.innerText = 'Send feedback'; - commentButton.classList.replace('gitlab-button-secondary', 'gitlab-button-success'); - commentButton.style.opacity = 1; -}; - -const resetCommentBox = () => { - const commentBox = selectCommentBox(); - commentBox.style.pointerEvents = 'auto'; - commentBox.style.color = BLACK; -}; - -const resetCommentText = () => { - const commentBox = selectCommentBox(); - commentBox.value = ''; - clearSavedComment(); -}; - -const resetComment = () => { - resetCommentButton(); - resetCommentBox(); - resetCommentText(); -}; - -const confirmAndClear = feedbackInfo => { - const commentButton = selectCommentButton(); - const currentNote = selectNote(); - const noteContainer = selectNoteContainer(); - - /* eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings */ - commentButton.innerText = 'Feedback sent'; - noteContainer.style.visibility = 'visible'; - currentNote.insertAdjacentHTML('beforeend', feedbackInfo); - - setTimeout(resetComment, 1000); - setTimeout(clearNote, 6000); -}; - -const setInProgressState = () => { - const commentButton = selectCommentButton(); - const commentBox = selectCommentBox(); - - /* eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings */ - commentButton.innerText = 'Sending feedback'; - commentButton.classList.replace('gitlab-button-success', 'gitlab-button-secondary'); - commentButton.style.opacity = 0.5; - commentBox.style.color = MUTED; - commentBox.style.pointerEvents = 'none'; -}; - -const commentErrors = error => { - switch (error.status) { - case 401: - /* eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings */ - return 'Unauthorized. You may have entered an incorrect authentication token.'; - case 404: - /* eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings */ - return 'Not found. You may have entered an incorrect merge request ID.'; - default: - /* eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings */ - return `Your comment could not be sent. Please try again. Error: ${error.message}`; - } -}; - -const postComment = ({ - platform, - browser, - userAgent, - innerWidth, - innerHeight, - projectId, - projectPath, - mergeRequestId, - mrUrl, - token, -}) => { - // Clear any old errors - clearNote(COMMENT_BOX); - - setInProgressState(); - - const commentText = selectCommentBox().value.trim(); - // Get the href at the last moment to support SPAs - const { href } = window.location; - - if (!commentText) { - /* eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings */ - postError('Your comment appears to be empty.', COMMENT_BOX); - resetCommentBox(); - resetCommentButton(); - return; - } - - const detailText = ` - \n -<details> - <summary>Metadata</summary> - Posted from ${href} | ${platform} | ${browser} | ${innerWidth} x ${innerHeight}. - <br /><br /> - <em>User agent: ${userAgent}</em> -</details> - `; - - const url = ` - ${mrUrl}/api/v4/projects/${projectId}/merge_requests/${mergeRequestId}/discussions`; - - const body = `${commentText} ${detailText}`; - - fetch(url, { - method: 'POST', - headers: { - 'PRIVATE-TOKEN': token, - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ body }), - }) - .then(response => { - if (response.ok) { - return response.json(); - } - - throw response; - }) - .then(data => { - const commentId = data.notes[0].id; - const feedbackLink = `${mrUrl}/${projectPath}/merge_requests/${mergeRequestId}#note_${commentId}`; - const feedbackInfo = `Feedback sent. View at <a class="gitlab-link" href="${feedbackLink}">${projectPath} !${mergeRequestId} (comment ${commentId})</a>`; - confirmAndClear(feedbackInfo); - }) - .catch(err => { - postError(commentErrors(err), COMMENT_BOX); - resetCommentBox(); - resetCommentButton(); - }); -}; - -export default postComment; diff --git a/app/assets/javascripts/visual_review_toolbar/components/comment_storage.js b/app/assets/javascripts/visual_review_toolbar/components/comment_storage.js deleted file mode 100644 index 49c9400437e..00000000000 --- a/app/assets/javascripts/visual_review_toolbar/components/comment_storage.js +++ /dev/null @@ -1,20 +0,0 @@ -import { selectCommentBox } from './utils'; -import { sessionStorage, STORAGE_COMMENT } from '../shared'; - -const getSavedComment = () => sessionStorage.getItem(STORAGE_COMMENT) || ''; - -const saveComment = () => { - const currentComment = selectCommentBox(); - - // This may be added to any view via top-level beforeunload listener - // so let's skip if it does not apply - if (currentComment && currentComment.value) { - sessionStorage.setItem(STORAGE_COMMENT, currentComment.value); - } -}; - -const clearSavedComment = () => { - sessionStorage.removeItem(STORAGE_COMMENT); -}; - -export { getSavedComment, saveComment, clearSavedComment }; diff --git a/app/assets/javascripts/visual_review_toolbar/components/form_elements.js b/app/assets/javascripts/visual_review_toolbar/components/form_elements.js deleted file mode 100644 index 608488a6fea..00000000000 --- a/app/assets/javascripts/visual_review_toolbar/components/form_elements.js +++ /dev/null @@ -1,17 +0,0 @@ -import { REMEMBER_ITEM } from '../shared'; -import { buttonClearStyles } from './utils'; - -/* eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings */ -const rememberBox = (rememberText = 'Remember me') => ` - <div class="gitlab-checkbox-wrapper"> - <input type="checkbox" id="${REMEMBER_ITEM}" name="${REMEMBER_ITEM}" value="remember"> - <label for="${REMEMBER_ITEM}" class="gitlab-checkbox-label">${rememberText}</label> - </div> -`; - -const submitButton = buttonId => ` - <div class="gitlab-button-wrapper"> - <button class="gitlab-button-wide gitlab-button gitlab-button-success" style="${buttonClearStyles}" type="button" id="${buttonId}"> Submit </button> - </div> -`; -export { rememberBox, submitButton }; diff --git a/app/assets/javascripts/visual_review_toolbar/components/index.js b/app/assets/javascripts/visual_review_toolbar/components/index.js deleted file mode 100644 index e88b3637ad8..00000000000 --- a/app/assets/javascripts/visual_review_toolbar/components/index.js +++ /dev/null @@ -1,23 +0,0 @@ -import { changeSelectedMr, comment, logoutUser, postComment, saveComment } from './comment'; -import { authorizeUser, login } from './login'; -import { addMr, mrForm } from './mr_id'; -import { note } from './note'; -import { selectContainer, selectForm } from './utils'; -import { buttonAndForm, toggleForm } from './wrapper'; - -export { - addMr, - authorizeUser, - buttonAndForm, - changeSelectedMr, - comment, - login, - logoutUser, - mrForm, - note, - postComment, - saveComment, - selectContainer, - selectForm, - toggleForm, -}; diff --git a/app/assets/javascripts/visual_review_toolbar/components/login.js b/app/assets/javascripts/visual_review_toolbar/components/login.js deleted file mode 100644 index 20ab01bc690..00000000000 --- a/app/assets/javascripts/visual_review_toolbar/components/login.js +++ /dev/null @@ -1,47 +0,0 @@ -import { nextView } from '../store'; -import { localStorage, LOGIN, TOKEN_BOX, STORAGE_TOKEN } from '../shared'; -import { clearNote, postError } from './note'; -import { rememberBox, submitButton } from './form_elements'; -import { selectRemember, selectToken } from './utils'; -import { addForm } from './wrapper'; - -const labelText = ` - Enter your <a class="gitlab-link" href="https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html">personal access token</a> -`; - -const login = ` - <div> - <label for="${TOKEN_BOX}" class="gitlab-label">${labelText}</label> - <input class="gitlab-input" type="password" id="${TOKEN_BOX}" name="${TOKEN_BOX}" autocomplete="current-password" aria-required="true"> - </div> - ${rememberBox()} - ${submitButton(LOGIN)} -`; - -const storeToken = (token, state) => { - const rememberMe = selectRemember().checked; - - if (rememberMe) { - localStorage.setItem(STORAGE_TOKEN, token); - } - - state.token = token; -}; - -const authorizeUser = state => { - // Clear any old errors - clearNote(TOKEN_BOX); - - const token = selectToken().value; - - if (!token) { - /* eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings */ - postError('Please enter your token.', TOKEN_BOX); - return; - } - - storeToken(token, state); - addForm(nextView(state, LOGIN)); -}; - -export { authorizeUser, login, storeToken }; diff --git a/app/assets/javascripts/visual_review_toolbar/components/mr_id.js b/app/assets/javascripts/visual_review_toolbar/components/mr_id.js deleted file mode 100644 index 695b3af8aa0..00000000000 --- a/app/assets/javascripts/visual_review_toolbar/components/mr_id.js +++ /dev/null @@ -1,63 +0,0 @@ -import { nextView } from '../store'; -import { MR_ID, MR_ID_BUTTON, STORAGE_MR_ID, localStorage } from '../shared'; -import { clearNote, postError } from './note'; -import { rememberBox, submitButton } from './form_elements'; -import { selectForm, selectMrBox, selectRemember } from './utils'; -import { addForm } from './wrapper'; - -/* eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings */ -const mrLabel = `Enter your merge request ID`; -/* eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings */ -const mrRememberText = `Remember this number`; - -const mrForm = ` - <div> - <label for="${MR_ID}" class="gitlab-label">${mrLabel}</label> - <input class="gitlab-input" type="text" pattern="[1-9][0-9]*" id="${MR_ID}" name="${MR_ID}" placeholder="e.g., 321" aria-required="true"> - </div> - ${rememberBox(mrRememberText)} - ${submitButton(MR_ID_BUTTON)} -`; - -const storeMR = (id, state) => { - const rememberMe = selectRemember().checked; - - if (rememberMe) { - localStorage.setItem(STORAGE_MR_ID, id); - } - - state.mergeRequestId = id; -}; - -const getFormError = (mrNumber, form) => { - if (!mrNumber) { - /* eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings */ - return 'Please enter your merge request ID number.'; - } - - if (!form.checkValidity()) { - /* eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings */ - return 'Please remove any non-number values from the field.'; - } - - return null; -}; - -const addMr = state => { - // Clear any old errors - clearNote(MR_ID); - - const mrNumber = selectMrBox().value; - const form = selectForm(); - const formError = getFormError(mrNumber, form); - - if (formError) { - postError(formError, MR_ID); - return; - } - - storeMR(mrNumber, state); - addForm(nextView(state, MR_ID)); -}; - -export { addMr, mrForm, storeMR }; diff --git a/app/assets/javascripts/visual_review_toolbar/components/note.js b/app/assets/javascripts/visual_review_toolbar/components/note.js deleted file mode 100644 index 9cddcb710f2..00000000000 --- a/app/assets/javascripts/visual_review_toolbar/components/note.js +++ /dev/null @@ -1,35 +0,0 @@ -import { NOTE, NOTE_CONTAINER, RED } from '../shared'; -import { selectById, selectNote, selectNoteContainer } from './utils'; - -const note = ` - <div id="${NOTE_CONTAINER}" style="visibility: hidden;"> - <p id="${NOTE}" class="gitlab-message"></p> - </div> -`; - -const clearNote = inputId => { - const currentNote = selectNote(); - const noteContainer = selectNoteContainer(); - - currentNote.innerText = ''; - currentNote.style.color = ''; - noteContainer.style.visibility = 'hidden'; - - if (inputId) { - const field = document.getElementById(inputId); - field.style.borderColor = ''; - } -}; - -const postError = (message, inputId) => { - const currentNote = selectNote(); - const noteContainer = selectNoteContainer(); - const field = selectById(inputId); - field.style.borderColor = RED; - currentNote.style.color = RED; - currentNote.innerText = message; - noteContainer.style.visibility = 'visible'; - setTimeout(clearNote.bind(null, inputId), 5000); -}; - -export { clearNote, note, postError }; diff --git a/app/assets/javascripts/visual_review_toolbar/components/utils.js b/app/assets/javascripts/visual_review_toolbar/components/utils.js deleted file mode 100644 index 4ec9bd4a32a..00000000000 --- a/app/assets/javascripts/visual_review_toolbar/components/utils.js +++ /dev/null @@ -1,51 +0,0 @@ -/* global document */ - -import { - COLLAPSE_BUTTON, - COMMENT_BOX, - COMMENT_BUTTON, - FORM, - FORM_CONTAINER, - MR_ID, - NOTE, - NOTE_CONTAINER, - REMEMBER_ITEM, - REVIEW_CONTAINER, - TOKEN_BOX, -} from '../shared'; - -// this style must be applied inline in a handful of components -/* eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings */ -const buttonClearStyles = ` - -webkit-appearance: none; -`; - -// selector functions to abstract out a little -const selectById = id => document.getElementById(id); -const selectCollapseButton = () => document.getElementById(COLLAPSE_BUTTON); -const selectCommentBox = () => document.getElementById(COMMENT_BOX); -const selectCommentButton = () => document.getElementById(COMMENT_BUTTON); -const selectContainer = () => document.getElementById(REVIEW_CONTAINER); -const selectForm = () => document.getElementById(FORM); -const selectFormContainer = () => document.getElementById(FORM_CONTAINER); -const selectMrBox = () => document.getElementById(MR_ID); -const selectNote = () => document.getElementById(NOTE); -const selectNoteContainer = () => document.getElementById(NOTE_CONTAINER); -const selectRemember = () => document.getElementById(REMEMBER_ITEM); -const selectToken = () => document.getElementById(TOKEN_BOX); - -export { - buttonClearStyles, - selectById, - selectCollapseButton, - selectContainer, - selectCommentBox, - selectCommentButton, - selectForm, - selectFormContainer, - selectMrBox, - selectNote, - selectNoteContainer, - selectRemember, - selectToken, -}; diff --git a/app/assets/javascripts/visual_review_toolbar/components/wrapper.js b/app/assets/javascripts/visual_review_toolbar/components/wrapper.js deleted file mode 100644 index fdf8ad7c41f..00000000000 --- a/app/assets/javascripts/visual_review_toolbar/components/wrapper.js +++ /dev/null @@ -1,79 +0,0 @@ -import { CLEAR, FORM, FORM_CONTAINER, WHITE } from '../shared'; -import { - selectCollapseButton, - selectForm, - selectFormContainer, - selectNoteContainer, -} from './utils'; -import { collapseButton, commentIcon, compressIcon } from './wrapper_icons'; - -const form = content => ` - <form id="${FORM}" novalidate> - ${content} - </form> -`; - -const buttonAndForm = content => ` - <div id="${FORM_CONTAINER}" class="gitlab-form-open"> - ${collapseButton} - ${form(content)} - </div> -`; - -const addForm = nextForm => { - const formWrapper = selectForm(); - formWrapper.innerHTML = nextForm; -}; - -function toggleForm() { - const toggleButton = selectCollapseButton(); - const currentForm = selectForm(); - const formContainer = selectFormContainer(); - const noteContainer = selectNoteContainer(); - const OPEN = 'open'; - const CLOSED = 'closed'; - - /* - You may wonder why we spread the arrays before we reverse them. - In the immortal words of MDN, - Careful: reverse is destructive. It also changes the original array - */ - - const openButtonClasses = ['gitlab-collapse-closed', 'gitlab-collapse-open']; - const closedButtonClasses = [...openButtonClasses].reverse(); - const openContainerClasses = ['gitlab-wrapper-closed', 'gitlab-wrapper-open']; - const closedContainerClasses = [...openContainerClasses].reverse(); - - const stateVals = { - [OPEN]: { - buttonClasses: openButtonClasses, - containerClasses: openContainerClasses, - icon: compressIcon, - display: 'flex', - backgroundColor: WHITE, - }, - [CLOSED]: { - buttonClasses: closedButtonClasses, - containerClasses: closedContainerClasses, - icon: commentIcon, - display: 'none', - backgroundColor: CLEAR, - }, - }; - - const nextState = toggleButton.classList.contains('gitlab-collapse-open') ? CLOSED : OPEN; - const currentVals = stateVals[nextState]; - - formContainer.classList.replace(...currentVals.containerClasses); - formContainer.style.backgroundColor = currentVals.backgroundColor; - formContainer.classList.toggle('gitlab-form-open'); - currentForm.style.display = currentVals.display; - toggleButton.classList.replace(...currentVals.buttonClasses); - toggleButton.innerHTML = currentVals.icon; - - if (noteContainer && noteContainer.innerText.length > 0) { - noteContainer.style.display = currentVals.display; - } -} - -export { addForm, buttonAndForm, toggleForm }; diff --git a/app/assets/javascripts/visual_review_toolbar/components/wrapper_icons.js b/app/assets/javascripts/visual_review_toolbar/components/wrapper_icons.js deleted file mode 100644 index b686fd4f5c2..00000000000 --- a/app/assets/javascripts/visual_review_toolbar/components/wrapper_icons.js +++ /dev/null @@ -1,15 +0,0 @@ -import { buttonClearStyles } from './utils'; - -const commentIcon = ` - <svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><title>icn/comment</title><path d="M4 11.132l1.446-.964A1 1 0 0 1 6 10h5a1 1 0 0 0 1-1V5a1 1 0 0 0-1-1H5a1 1 0 0 0-1 1v6.132zM6.303 12l-2.748 1.832A1 1 0 0 1 2 13V5a3 3 0 0 1 3-3h6a3 3 0 0 1 3 3v4a3 3 0 0 1-3 3H6.303z" id="gitlab-comment-icon"/></svg> -`; - -const compressIcon = ` - <svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><title>icn/compress</title><path d="M5.27 12.182l-1.562 1.561a1 1 0 0 1-1.414 0h-.001a1 1 0 0 1 0-1.415l1.56-1.56L2.44 9.353a.5.5 0 0 1 .353-.854H7.09a.5.5 0 0 1 .5.5v4.294a.5.5 0 0 1-.853.353l-1.467-1.465zm6.911-6.914l1.464 1.464a.5.5 0 0 1-.353.854H8.999a.5.5 0 0 1-.5-.5V2.793a.5.5 0 0 1 .854-.354l1.414 1.415 1.56-1.561a1 1 0 1 1 1.415 1.414l-1.561 1.56z" id="gitlab-compress-icon"/></svg> -`; - -const collapseButton = ` - <button id='gitlab-collapse' style='${buttonClearStyles}' class='gitlab-button gitlab-button-secondary gitlab-collapse gitlab-collapse-open'>${compressIcon}</button> -`; - -export { commentIcon, compressIcon, collapseButton }; diff --git a/app/assets/javascripts/visual_review_toolbar/index.js b/app/assets/javascripts/visual_review_toolbar/index.js deleted file mode 100644 index 67b3fadd772..00000000000 --- a/app/assets/javascripts/visual_review_toolbar/index.js +++ /dev/null @@ -1,51 +0,0 @@ -import './styles/toolbar.css'; - -import { buttonAndForm, note, selectForm, selectContainer } from './components'; -import { REVIEW_CONTAINER } from './shared'; -import { eventLookup, getInitialView, initializeGlobalListeners, initializeState } from './store'; - -/* - - Welcome to the visual review toolbar files. A few useful notes: - - - These files build a static script that is served from our webpack - assets folder. (https://gitlab.com/assets/webpack/visual_review_toolbar.js) - - - To compile this file, run `yarn webpack-vrt`. - - - Vue is not used in these files because we do not want to ask users to - install another library at this time. It's all pure vanilla javascript. - -*/ - -window.addEventListener('load', () => { - initializeState(window, document); - - const mainContent = buttonAndForm(getInitialView()); - const container = document.createElement('div'); - container.setAttribute('id', REVIEW_CONTAINER); - container.insertAdjacentHTML('beforeend', note); - container.insertAdjacentHTML('beforeend', mainContent); - - document.body.insertBefore(container, document.body.firstChild); - - selectContainer().addEventListener('click', event => { - eventLookup(event.target.id)(); - }); - - selectForm().addEventListener('submit', event => { - // this is important to prevent the form from adding data - // as URL params and inadvertently revealing secrets - event.preventDefault(); - - const id = - event.target.querySelector('.gitlab-button-wrapper') && - event.target.querySelector('.gitlab-button-wrapper').getElementsByTagName('button')[0] && - event.target.querySelector('.gitlab-button-wrapper').getElementsByTagName('button')[0].id; - - // even if this is called with false, it's ok; it will get the default no-op - eventLookup(id)(); - }); - - initializeGlobalListeners(); -}); diff --git a/app/assets/javascripts/visual_review_toolbar/shared/constants.js b/app/assets/javascripts/visual_review_toolbar/shared/constants.js deleted file mode 100644 index 0d5b666ab0a..00000000000 --- a/app/assets/javascripts/visual_review_toolbar/shared/constants.js +++ /dev/null @@ -1,56 +0,0 @@ -// component selectors -const CHANGE_MR_ID_BUTTON = 'gitlab-change-mr'; -const COLLAPSE_BUTTON = 'gitlab-collapse'; -const COMMENT_BOX = 'gitlab-comment'; -const COMMENT_BUTTON = 'gitlab-comment-button'; -const FORM = 'gitlab-form'; -const FORM_CONTAINER = 'gitlab-form-wrapper'; -const LOGIN = 'gitlab-login-button'; -const LOGOUT = 'gitlab-logout-button'; -const MR_ID = 'gitlab-submit-mr'; -const MR_ID_BUTTON = 'gitlab-submit-mr-button'; -const NOTE = 'gitlab-validation-note'; -const NOTE_CONTAINER = 'gitlab-note-wrapper'; -const REMEMBER_ITEM = 'gitlab-remember-item'; -const REVIEW_CONTAINER = 'gitlab-review-container'; -const TOKEN_BOX = 'gitlab-token'; - -// Storage keys -const STORAGE_PREFIX = '--gitlab'; // Using `--` to make the prefix more unique -const STORAGE_MR_ID = `${STORAGE_PREFIX}-merge-request-id`; -const STORAGE_TOKEN = `${STORAGE_PREFIX}-token`; -const STORAGE_COMMENT = `${STORAGE_PREFIX}-comment`; - -// colors — these are applied programmatically -// rest of styles belong in ./styles -const BLACK = 'rgba(46, 46, 46, 1)'; -const CLEAR = 'rgba(255, 255, 255, 0)'; -const MUTED = 'rgba(223, 223, 223, 0.5)'; -const RED = 'rgba(219, 59, 33, 1)'; -const WHITE = 'rgba(250, 250, 250, 1)'; - -export { - CHANGE_MR_ID_BUTTON, - COLLAPSE_BUTTON, - COMMENT_BOX, - COMMENT_BUTTON, - FORM, - FORM_CONTAINER, - LOGIN, - LOGOUT, - MR_ID, - MR_ID_BUTTON, - NOTE, - NOTE_CONTAINER, - REMEMBER_ITEM, - REVIEW_CONTAINER, - TOKEN_BOX, - STORAGE_MR_ID, - STORAGE_TOKEN, - STORAGE_COMMENT, - BLACK, - CLEAR, - MUTED, - RED, - WHITE, -}; diff --git a/app/assets/javascripts/visual_review_toolbar/shared/index.js b/app/assets/javascripts/visual_review_toolbar/shared/index.js deleted file mode 100644 index d8ccb170592..00000000000 --- a/app/assets/javascripts/visual_review_toolbar/shared/index.js +++ /dev/null @@ -1,55 +0,0 @@ -import { - CHANGE_MR_ID_BUTTON, - COLLAPSE_BUTTON, - COMMENT_BOX, - COMMENT_BUTTON, - FORM, - FORM_CONTAINER, - LOGIN, - LOGOUT, - MR_ID, - MR_ID_BUTTON, - NOTE, - NOTE_CONTAINER, - REMEMBER_ITEM, - REVIEW_CONTAINER, - TOKEN_BOX, - STORAGE_MR_ID, - STORAGE_TOKEN, - STORAGE_COMMENT, - BLACK, - CLEAR, - MUTED, - RED, - WHITE, -} from './constants'; - -import { localStorage, sessionStorage } from './storage_utils'; - -export { - localStorage, - sessionStorage, - CHANGE_MR_ID_BUTTON, - COLLAPSE_BUTTON, - COMMENT_BOX, - COMMENT_BUTTON, - FORM, - FORM_CONTAINER, - LOGIN, - LOGOUT, - MR_ID, - MR_ID_BUTTON, - NOTE, - NOTE_CONTAINER, - REMEMBER_ITEM, - REVIEW_CONTAINER, - TOKEN_BOX, - STORAGE_MR_ID, - STORAGE_TOKEN, - STORAGE_COMMENT, - BLACK, - CLEAR, - MUTED, - RED, - WHITE, -}; diff --git a/app/assets/javascripts/visual_review_toolbar/shared/storage_utils.js b/app/assets/javascripts/visual_review_toolbar/shared/storage_utils.js deleted file mode 100644 index 00456d3536e..00000000000 --- a/app/assets/javascripts/visual_review_toolbar/shared/storage_utils.js +++ /dev/null @@ -1,42 +0,0 @@ -import { setUsingGracefulStorageFlag } from '../store/state'; - -const TEST_KEY = 'gitlab-storage-test'; - -const createStorageStub = () => { - const items = {}; - - return { - getItem(key) { - return items[key]; - }, - setItem(key, value) { - items[key] = value; - }, - removeItem(key) { - delete items[key]; - }, - }; -}; - -const hasStorageSupport = storage => { - // Support test taken from https://stackoverflow.com/a/11214467/1708147 - try { - storage.setItem(TEST_KEY, TEST_KEY); - storage.removeItem(TEST_KEY); - setUsingGracefulStorageFlag(true); - - return true; - } catch (err) { - setUsingGracefulStorageFlag(false); - return false; - } -}; - -const useGracefulStorage = storage => - // If a browser does not support local storage, let's return a graceful implementation. - hasStorageSupport(storage) ? storage : createStorageStub(); - -const localStorage = useGracefulStorage(window.localStorage); -const sessionStorage = useGracefulStorage(window.sessionStorage); - -export { localStorage, sessionStorage }; diff --git a/app/assets/javascripts/visual_review_toolbar/store/events.js b/app/assets/javascripts/visual_review_toolbar/store/events.js deleted file mode 100644 index c9095c77ef1..00000000000 --- a/app/assets/javascripts/visual_review_toolbar/store/events.js +++ /dev/null @@ -1,73 +0,0 @@ -import { - addMr, - authorizeUser, - changeSelectedMr, - logoutUser, - postComment, - saveComment, - toggleForm, -} from '../components'; - -import { - CHANGE_MR_ID_BUTTON, - COLLAPSE_BUTTON, - COMMENT_BUTTON, - LOGIN, - LOGOUT, - MR_ID_BUTTON, -} from '../shared'; - -import { state } from './state'; -import debounce from './utils'; - -const noop = () => {}; - -// State needs to be bound here to be acted on -// because these are called by click events and -// as such are called with only the `event` object -const eventLookup = id => { - switch (id) { - case CHANGE_MR_ID_BUTTON: - return () => { - saveComment(); - changeSelectedMr(state); - }; - case COLLAPSE_BUTTON: - return toggleForm; - case COMMENT_BUTTON: - return postComment.bind(null, state); - case LOGIN: - return authorizeUser.bind(null, state); - case LOGOUT: - return () => { - saveComment(); - logoutUser(state); - }; - case MR_ID_BUTTON: - return addMr.bind(null, state); - default: - return noop; - } -}; - -const updateWindowSize = wind => { - state.innerWidth = wind.innerWidth; - state.innerHeight = wind.innerHeight; -}; - -const initializeGlobalListeners = () => { - window.addEventListener('resize', debounce(updateWindowSize.bind(null, window), 200)); - window.addEventListener('beforeunload', event => { - if (state.usingGracefulStorage) { - // if there is no browser storage support, reloading will lose the comment; this way, the user will be warned - // we assign the return value because it is required by Chrome see: https://developer.mozilla.org/en-US/docs/Web/API/WindowEventHandlers/onbeforeunload#Example, - event.preventDefault(); - /* eslint-disable-next-line no-param-reassign */ - event.returnValue = ''; - } - - saveComment(); - }); -}; - -export { eventLookup, initializeGlobalListeners }; diff --git a/app/assets/javascripts/visual_review_toolbar/store/index.js b/app/assets/javascripts/visual_review_toolbar/store/index.js deleted file mode 100644 index 07c8dd6f1d2..00000000000 --- a/app/assets/javascripts/visual_review_toolbar/store/index.js +++ /dev/null @@ -1,11 +0,0 @@ -import { eventLookup, initializeGlobalListeners } from './events'; -import { nextView, getInitialView, initializeState, setUsingGracefulStorageFlag } from './state'; - -export { - eventLookup, - getInitialView, - initializeGlobalListeners, - initializeState, - nextView, - setUsingGracefulStorageFlag, -}; diff --git a/app/assets/javascripts/visual_review_toolbar/store/state.js b/app/assets/javascripts/visual_review_toolbar/store/state.js deleted file mode 100644 index b7853bb0723..00000000000 --- a/app/assets/javascripts/visual_review_toolbar/store/state.js +++ /dev/null @@ -1,95 +0,0 @@ -import { comment, login, mrForm } from '../components'; -import { localStorage, COMMENT_BOX, LOGIN, MR_ID, STORAGE_MR_ID, STORAGE_TOKEN } from '../shared'; - -const state = { - browser: '', - usingGracefulStorage: '', - innerWidth: '', - innerHeight: '', - mergeRequestId: '', - mrUrl: '', - platform: '', - projectId: '', - userAgent: '', - token: '', -}; - -// adapted from https://developer.mozilla.org/en-US/docs/Web/API/Window/navigator#Example_2_Browser_detect_and_return_an_index -const getBrowserId = sUsrAg => { - /* eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings */ - const aKeys = ['MSIE', 'Edge', 'Firefox', 'Safari', 'Chrome', 'Opera']; - let nIdx = aKeys.length - 1; - - for (nIdx; nIdx > -1 && sUsrAg.indexOf(aKeys[nIdx]) === -1; nIdx -= 1); - return aKeys[nIdx]; -}; - -const nextView = (appState, form = 'none') => { - const formsList = { - [COMMENT_BOX]: currentState => (currentState.token ? mrForm : login), - [LOGIN]: currentState => (currentState.mergeRequestId ? comment(currentState) : mrForm), - [MR_ID]: currentState => (currentState.token ? comment(currentState) : login), - none: currentState => { - if (!currentState.token) { - return login; - } - - if (currentState.token && !currentState.mergeRequestId) { - return mrForm; - } - - return comment(currentState); - }, - }; - - return formsList[form](appState); -}; - -const initializeState = (wind, doc) => { - const { - innerWidth, - innerHeight, - navigator: { platform, userAgent }, - } = wind; - - const browser = getBrowserId(userAgent); - - const scriptEl = doc.getElementById('review-app-toolbar-script'); - const { projectId, mergeRequestId, mrUrl, projectPath } = scriptEl.dataset; - - // This mutates our default state object above. It's weird but it makes the linter happy. - Object.assign(state, { - browser, - innerWidth, - innerHeight, - mergeRequestId, - mrUrl, - platform, - projectId, - projectPath, - userAgent, - }); - - return state; -}; - -const getInitialView = () => { - const token = localStorage.getItem(STORAGE_TOKEN); - const mrId = localStorage.getItem(STORAGE_MR_ID); - - if (token) { - state.token = token; - } - - if (mrId) { - state.mergeRequestId = mrId; - } - - return nextView(state); -}; - -const setUsingGracefulStorageFlag = flag => { - state.usingGracefulStorage = !flag; -}; - -export { initializeState, getInitialView, nextView, setUsingGracefulStorageFlag, state }; diff --git a/app/assets/javascripts/visual_review_toolbar/store/utils.js b/app/assets/javascripts/visual_review_toolbar/store/utils.js deleted file mode 100644 index 5cf145351b3..00000000000 --- a/app/assets/javascripts/visual_review_toolbar/store/utils.js +++ /dev/null @@ -1,15 +0,0 @@ -const debounce = (fn, time) => { - let current; - - const debounced = () => { - if (current) { - clearTimeout(current); - } - - current = setTimeout(fn, time); - }; - - return debounced; -}; - -export default debounce; diff --git a/app/assets/javascripts/visual_review_toolbar/styles/toolbar.css b/app/assets/javascripts/visual_review_toolbar/styles/toolbar.css deleted file mode 100644 index d1a8d66ef40..00000000000 --- a/app/assets/javascripts/visual_review_toolbar/styles/toolbar.css +++ /dev/null @@ -1,188 +0,0 @@ -/* - As a standalone script, the toolbar has its own css - */ - -#gitlab-collapse > * { - pointer-events: none; -} - -#gitlab-comment { - background-color: #fafafa; -} - -#gitlab-form { - display: flex; - flex-direction: column; - width: 100%; - margin-bottom: 0; -} - -#gitlab-note-wrapper { - display: flex; - flex-direction: column; - background-color: #fafafa; - border-radius: 4px; - margin-bottom: .5rem; - padding: 1rem; -} - -#gitlab-form-wrapper { - overflow: auto; - display: flex; - flex-direction: row-reverse; - border-radius: 4px; -} - -#gitlab-review-container { - max-width: 22rem; - max-height: 22rem; - overflow: auto; - display: flex; - flex-direction: column; - position: fixed; - bottom: 1rem; - right: 1rem; - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Noto Sans', Ubuntu, Cantarell, - 'Helvetica Neue', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', - 'Noto Color Emoji'; - font-size: .8rem; - font-weight: 400; - color: #2e2e2e; - z-index: 9999; /* toolbar should always be on top */ -} - -.gitlab-wrapper-open { - max-width: 22rem; - max-height: 22rem; -} - -.gitlab-wrapper-closed { - max-width: 3.4rem; - max-height: 3.4rem; -} - -.gitlab-button { - cursor: pointer; - transition: background-color 100ms linear, border-color 100ms linear, color 100ms linear, box-shadow 100ms linear; -} - -.gitlab-button-secondary { - background: none #fafafa; - margin: 0 .5rem; - border: 1px solid #e3e3e3; -} - -.gitlab-button-secondary:hover { - background-color: #f0f0f0; - border-color: #e3e3e3; - color: #2e2e2e; -} - -.gitlab-button-secondary:active { - color: #2e2e2e; - background-color: #e1e1e1; - border-color: #dadada; -} - -.gitlab-button-success:hover { - color: #fff; - background-color: #137e3f; - border-color: #127339; -} - -.gitlab-button-success:active { - background-color: #168f48; - border-color: #12753a; - color: #fff; -} - -.gitlab-button-success { - background-color: #1aaa55; - border: 1px solid #168f48; - color: #fff; -} - -.gitlab-button-wide { - width: 100%; -} - -.gitlab-button-wrapper { - margin-top: 0.5rem; - display: flex; - align-items: baseline; - /* - this makes sure the hit enter to submit picks the correct button - on the comment view - */ - flex-direction: row-reverse; -} - -.gitlab-collapse { - width: 2.4rem; - height: 2.2rem; - margin-left: 1rem; - padding: .5rem; -} - -.gitlab-collapse-closed { - align-self: center; -} - -.gitlab-checkbox-label { - padding: 0 .2rem; -} - -.gitlab-checkbox-wrapper { - display: flex; - align-items: baseline; -} - -.gitlab-form-open { - padding: 1rem; - background-color: #fafafa; -} - -.gitlab-label { - font-weight: 600; - display: inline-block; - width: 100%; -} - -.gitlab-link { - color: #1b69b6; - text-decoration: none; - background-color: transparent; - background-image: none; -} - -.gitlab-link:hover { - text-decoration: underline; -} - -.gitlab-link-button { - border: none; - cursor: pointer; - padding: 0 .15rem; -} - -.gitlab-message { - padding: .25rem 0; - margin: 0; - line-height: 1.2rem; -} - -.gitlab-metadata-note { - font-size: .7rem; - line-height: 1rem; - color: #666; - margin-bottom: .5rem; -} - -.gitlab-input { - width: 100%; - border: 1px solid #dfdfdf; - border-radius: 4px; - padding: .1rem .2rem; - min-height: 2rem; - max-width: 17rem; -} diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_merge_help.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_merge_help.vue index a347269c916..53bf9d5ab6f 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_merge_help.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_merge_help.vue @@ -23,7 +23,7 @@ export default { }; </script> <template> - <section class="mr-widget-help"> + <section class="mr-widget-help font-italic"> <template v-if="missingBranch"> {{ missingBranchInfo }} </template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue index 76b96c8c1c0..8fdf61a6b8d 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue @@ -18,8 +18,8 @@ export default { Deployment, MrWidgetContainer, MrWidgetPipeline, - MergeTrainInfo: () => - import('ee_component/vue_merge_request_widget/components/merge_train_info.vue'), + MergeTrainPositionIndicator: () => + import('ee_component/vue_merge_request_widget/components/merge_train_position_indicator.vue'), }, props: { mr: { @@ -62,7 +62,7 @@ export default { showVisualReviewAppLink() { return this.mr.visualReviewAppAvailable; }, - showMergeTrainInfo() { + showMergeTrainPositionIndicator() { return _.isNumber(this.mr.mergeTrainIndex); }, }, @@ -90,8 +90,8 @@ export default { :visual-review-app-meta="visualReviewAppMeta" /> </div> - <merge-train-info - v-if="showMergeTrainInfo" + <merge-train-position-indicator + v-if="showMergeTrainPositionIndicator" class="mr-widget-extension" :merge-train-index="mr.mergeTrainIndex" /> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue index c7b064b8506..339e154affc 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue @@ -50,6 +50,7 @@ export default { startTag: '<span class="label-branch">', endTag: '</span>', }, + false, ); }, }, 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 d4514767912..e294e1de976 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 @@ -94,9 +94,6 @@ export default { return __('Merge'); }, - shouldShowMergeOptionsDropdown() { - return this.isAutoMergeAvailable && !this.mr.onlyAllowMergeIfPipelineSucceeds; - }, isRemoveSourceBranchButtonDisabled() { return this.isMergeButtonDisabled; }, @@ -246,7 +243,7 @@ export default { {{ mergeButtonText }} </button> <button - v-if="isAutoMergeAvailable" + v-if="shouldShowMergeImmediatelyDropdown" :disabled="isMergeButtonDisabled" type="button" class="btn btn-sm btn-info dropdown-toggle js-merge-moment" @@ -256,7 +253,7 @@ export default { <i class="fa fa-chevron-down qa-merge-moment-dropdown" aria-hidden="true"></i> </button> <ul - v-if="shouldShowMergeOptionsDropdown" + v-if="shouldShowMergeImmediatelyDropdown" class="dropdown-menu dropdown-menu-right" role="menu" > diff --git a/app/assets/javascripts/vue_merge_request_widget/mixins/ready_to_merge.js b/app/assets/javascripts/vue_merge_request_widget/mixins/ready_to_merge.js index 116d537c463..eef49e20159 100644 --- a/app/assets/javascripts/vue_merge_request_widget/mixins/ready_to_merge.js +++ b/app/assets/javascripts/vue_merge_request_widget/mixins/ready_to_merge.js @@ -15,5 +15,8 @@ export default { // MWPS is currently the only auto merge strategy available in CE return __('Merge when pipeline succeeds'); }, + shouldShowMergeImmediatelyDropdown() { + return this.mr.isPipelineActive && !this.mr.onlyAllowMergeIfPipelineSucceeds; + }, }, }; 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 3eab8e6fc0b..7843409f4a7 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 @@ -31,6 +31,9 @@ export default class MergeRequestStore { this.targetBranchSha = data.target_branch_sha; this.sourceBranch = data.source_branch; this.sourceBranchProtected = data.source_branch_protected; + this.conflictsDocsPath = data.conflicts_docs_path; + this.mergeRequestPipelinesHelpPath = data.merge_request_pipelines_docs_path; + this.mergeTrainWhenPipelineSucceedsDocsPath = data.merge_train_when_pipeline_succeeds_docs_path; this.mergeStatus = data.merge_status; this.commitMessage = data.default_merge_commit_message; this.shortMergeCommitSha = data.short_merge_commit_sha; @@ -84,9 +87,6 @@ export default class MergeRequestStore { this.allowCollaboration = data.allow_collaboration; this.sourceProjectId = data.source_project_id; this.targetProjectId = data.target_project_id; - this.mergePipelinesEnabled = Boolean(data.merge_pipelines_enabled); - this.mergeTrainsCount = data.merge_trains_count || 0; - this.mergeTrainIndex = data.merge_train_index; // CI related this.hasCI = data.has_ci; diff --git a/app/assets/javascripts/vue_shared/components/changed_file_icon.vue b/app/assets/javascripts/vue_shared/components/changed_file_icon.vue index cdf2d1020ba..beb2ac09992 100644 --- a/app/assets/javascripts/vue_shared/components/changed_file_icon.vue +++ b/app/assets/javascripts/vue_shared/components/changed_file_icon.vue @@ -26,11 +26,6 @@ export default { required: false, default: false, }, - forceModifiedIcon: { - type: Boolean, - required: false, - default: false, - }, size: { type: Number, required: false, @@ -48,8 +43,6 @@ export default { // eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings const suffix = !this.file.changed && this.file.staged && !this.showStagedIcon ? '-solid' : ''; - if (this.forceModifiedIcon) return `file-modified${suffix}`; - return `${getCommitIconMap(this.file).icon}${suffix}`; }, changedIconClass() { @@ -88,7 +81,7 @@ export default { v-gl-tooltip.right :title="tooltipTitle" :class="{ 'ml-auto': isCentered }" - class="file-changed-icon" + class="file-changed-icon d-inline-block" > <icon v-if="showIcon" :name="changedIcon" :size="size" :css-classes="changedIconClass" /> </span> diff --git a/app/assets/javascripts/vue_shared/components/file_row.vue b/app/assets/javascripts/vue_shared/components/file_row.vue index fe5289ff371..f49e69c473b 100644 --- a/app/assets/javascripts/vue_shared/components/file_row.vue +++ b/app/assets/javascripts/vue_shared/components/file_row.vue @@ -146,6 +146,7 @@ export default { <span ref="textOutput" :style="levelIndentation" class="file-row-name str-truncated"> <file-icon v-if="!showChangedIcon || file.type === 'tree'" + class="file-row-icon" :file-name="file.name" :loading="file.loading" :folder="isTree" @@ -223,13 +224,8 @@ export default { white-space: nowrap; } -.file-row-name svg { +.file-row-name .file-row-icon { margin-right: 2px; vertical-align: middle; } - -.file-row-name .loading-container { - display: inline-block; - margin-right: 4px; -} </style> diff --git a/app/assets/javascripts/vue_shared/components/recaptcha_modal.vue b/app/assets/javascripts/vue_shared/components/recaptcha_modal.vue index df19906309c..f0aae20477b 100644 --- a/app/assets/javascripts/vue_shared/components/recaptcha_modal.vue +++ b/app/assets/javascripts/vue_shared/components/recaptcha_modal.vue @@ -30,9 +30,16 @@ export default { }, mounted() { + if (window.recaptchaDialogCallback) { + throw new Error('recaptchaDialogCallback is already defined!'); + } window.recaptchaDialogCallback = this.submit.bind(this); }, + beforeDestroy() { + window.recaptchaDialogCallback = null; + }, + methods: { appendRecaptchaScript() { this.removeRecaptchaScript(); diff --git a/app/assets/javascripts/vue_shared/directives/autofocusonshow.js b/app/assets/javascripts/vue_shared/directives/autofocusonshow.js new file mode 100644 index 00000000000..4659ec20ceb --- /dev/null +++ b/app/assets/javascripts/vue_shared/directives/autofocusonshow.js @@ -0,0 +1,39 @@ +/** + * Input/Textarea Autofocus Directive for Vue + */ +export default { + /** + * Set focus when element is rendered, but + * is not visible, using IntersectionObserver + * + * @param {Element} el Target element + */ + inserted(el) { + if ('IntersectionObserver' in window) { + // Element visibility is dynamic, so we attach observer + el.visibilityObserver = new IntersectionObserver(entries => { + entries.forEach(entry => { + // Combining `intersectionRatio > 0` and + // element's `offsetParent` presence will + // deteremine if element is truely visible + if (entry.intersectionRatio > 0 && entry.target.offsetParent) { + entry.target.focus(); + } + }); + }); + + // Bind the observer. + el.visibilityObserver.observe(el, { root: document.documentElement }); + } + }, + /** + * Detach observer on unbind hook. + * + * @param {Element} el Target element + */ + unbind(el) { + if (el.visibilityObserver) { + el.visibilityObserver.disconnect(); + } + }, +}; diff --git a/app/assets/stylesheets/errors.scss b/app/assets/stylesheets/errors.scss index d287215096e..89029a58d1e 100644 --- a/app/assets/stylesheets/errors.scss +++ b/app/assets/stylesheets/errors.scss @@ -96,7 +96,7 @@ a { } .error-nav { - padding: 0; + padding: $gl-padding 0 0; text-align: center; li { diff --git a/app/assets/stylesheets/framework/badges.scss b/app/assets/stylesheets/framework/badges.scss index c6060161dec..c036267a7c8 100644 --- a/app/assets/stylesheets/framework/badges.scss +++ b/app/assets/stylesheets/framework/badges.scss @@ -1,6 +1,6 @@ .badge.badge-pill { font-weight: $gl-font-weight-normal; background-color: $badge-bg; - color: $gl-text-color-secondary; + color: $gray-800; vertical-align: baseline; } diff --git a/app/assets/stylesheets/framework/callout.scss b/app/assets/stylesheets/framework/callout.scss index 643b20c56bc..c5bb2a1256a 100644 --- a/app/assets/stylesheets/framework/callout.scss +++ b/app/assets/stylesheets/framework/callout.scss @@ -9,7 +9,9 @@ .bs-callout { margin: $gl-padding 0; padding: $gl-padding; - border-left: 3px solid $border-color; + border-color: $border-color; + border-style: solid; + border-width: 0 0 0 3px; color: $text-color; background: $gray-light; @@ -48,6 +50,10 @@ background-color: $blue-100; border-color: $blue-200; color: $blue-700; + + h4 { + color: $blue-700; + } } .bs-callout-success { diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index f384a49e0ae..e9218dcec67 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -438,6 +438,7 @@ img.emoji { .w-3rem { width: 3rem; } .h-12em { height: 12em; } +.h-32-px { height: 32px;} .mw-460 { max-width: 460px; } .mw-6em { max-width: 6em; } diff --git a/app/assets/stylesheets/framework/flash.scss b/app/assets/stylesheets/framework/flash.scss index e3dd127366d..96f6d02a68f 100644 --- a/app/assets/stylesheets/framework/flash.scss +++ b/app/assets/stylesheets/framework/flash.scss @@ -43,6 +43,7 @@ @extend .alert; background-color: $orange-100; color: $orange-900; + cursor: default; margin: 0; } diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index 1bc597bd4ae..ca737c53318 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -131,7 +131,6 @@ > li:not(.d-none) a { @include media-breakpoint-down(xs) { margin-left: 0; - min-width: 100%; } } } @@ -233,7 +232,6 @@ .impersonation-btn, .impersonation-btn:hover { background-color: $white-light; - margin-left: 0; border-top-left-radius: 0; border-bottom-left-radius: 0; diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 92190f8979e..9871771542d 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -469,6 +469,7 @@ $link-active-background: rgba(0, 0, 0, 0.04); $link-hover-background: rgba(0, 0, 0, 0.06); $inactive-badge-background: rgba(0, 0, 0, 0.08); $sidebar-toggle-height: 60px; +$sidebar-toggle-width: 40px; $sidebar-milestone-toggle-bottom-margin: 10px; /* diff --git a/app/assets/stylesheets/highlight/common.scss b/app/assets/stylesheets/highlight/common.scss index ac3214a07d9..bdeac7e97c0 100644 --- a/app/assets/stylesheets/highlight/common.scss +++ b/app/assets/stylesheets/highlight/common.scss @@ -16,3 +16,16 @@ color: $dark-diff-match-bg; background: $dark-diff-match-color; } + +@mixin diff-expansion($background, $border, $link) { + background-color: $background; + + td { + border-top: 1px solid $border; + border-bottom: 1px solid $border; + } + + a { + color: $link; + } +} diff --git a/app/assets/stylesheets/highlight/themes/dark.scss b/app/assets/stylesheets/highlight/themes/dark.scss index 16893dd047e..cbce0ba3f1e 100644 --- a/app/assets/stylesheets/highlight/themes/dark.scss +++ b/app/assets/stylesheets/highlight/themes/dark.scss @@ -111,6 +111,10 @@ $dark-il: #de935f; color: $dark-line-color; } + .line_expansion { + @include diff-expansion($dark-main-bg, $dark-border, $dark-na); + } + // Diff line .line_holder { &.match .line_content, diff --git a/app/assets/stylesheets/highlight/themes/monokai.scss b/app/assets/stylesheets/highlight/themes/monokai.scss index 37fe61b925c..1b61ffa37e3 100644 --- a/app/assets/stylesheets/highlight/themes/monokai.scss +++ b/app/assets/stylesheets/highlight/themes/monokai.scss @@ -111,6 +111,10 @@ $monokai-gi: #a6e22e; color: $monokai-text-color; } + .line_expansion { + @include diff-expansion($monokai-bg, $monokai-border, $monokai-k); + } + // Diff line .line_holder { &.match .line_content, diff --git a/app/assets/stylesheets/highlight/themes/none.scss b/app/assets/stylesheets/highlight/themes/none.scss index b4217aac37a..a7ede266fb5 100644 --- a/app/assets/stylesheets/highlight/themes/none.scss +++ b/app/assets/stylesheets/highlight/themes/none.scss @@ -34,8 +34,11 @@ color: $gl-text-color; } -// Diff line + .line_expansion { + @include diff-expansion($gray-light, $white-normal, $gl-text-color); + } + // Diff line $none-over-bg: #ded7fc; $none-expanded-border: #e0e0e0; $none-expanded-bg: #e0e0e0; diff --git a/app/assets/stylesheets/highlight/themes/solarized-dark.scss b/app/assets/stylesheets/highlight/themes/solarized-dark.scss index a4e9eda22c9..6569f3abc8b 100644 --- a/app/assets/stylesheets/highlight/themes/solarized-dark.scss +++ b/app/assets/stylesheets/highlight/themes/solarized-dark.scss @@ -115,6 +115,10 @@ $solarized-dark-il: #2aa198; color: $solarized-dark-pre-color; } + .line_expansion { + @include diff-expansion($solarized-dark-line-bg, $solarized-dark-border, $solarized-dark-kd); + } + // Diff line .line_holder { &.match .line_content, diff --git a/app/assets/stylesheets/highlight/themes/solarized-light.scss b/app/assets/stylesheets/highlight/themes/solarized-light.scss index b604d1ccb6c..4e74a9ea50a 100644 --- a/app/assets/stylesheets/highlight/themes/solarized-light.scss +++ b/app/assets/stylesheets/highlight/themes/solarized-light.scss @@ -122,6 +122,10 @@ $solarized-light-il: #2aa198; color: $solarized-light-pre-color; } + .line_expansion { + @include diff-expansion($solarized-light-line-bg, $solarized-light-border, $solarized-light-kd); + } + // Diff line .line_holder { &.match .line_content, diff --git a/app/assets/stylesheets/highlight/white_base.scss b/app/assets/stylesheets/highlight/white_base.scss index b3974df8639..973f94c63aa 100644 --- a/app/assets/stylesheets/highlight/white_base.scss +++ b/app/assets/stylesheets/highlight/white_base.scss @@ -101,24 +101,8 @@ pre.code, color: $white-code-color; } -// Expansion line .line_expansion { - background-color: $gray-light; - - td { - border-top: 1px solid $border-color; - border-bottom: 1px solid $border-color; - text-align: center; - } - - a { - color: $blue-600; - } - - .unfold-icon { - display: inline-block; - padding: 8px 0; - } + @include diff-expansion($gray-light, $border-color, $blue-600); } // Diff line diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss index 343cca96851..e77a2d1e333 100644 --- a/app/assets/stylesheets/pages/boards.scss +++ b/app/assets/stylesheets/pages/boards.scss @@ -86,6 +86,9 @@ } .board { + // the next line cannot be replaced with .d-inline-block because it breaks display: none of SortableJS + // see https://gitlab.com/gitlab-org/gitlab-ce/issues/64828 + display: inline-block; width: calc(85vw - 15px); @include media-breakpoint-up(sm) { diff --git a/app/assets/stylesheets/pages/container_registry.scss b/app/assets/stylesheets/pages/container_registry.scss index a21fa29f34a..b88bd78cf3d 100644 --- a/app/assets/stylesheets/pages/container_registry.scss +++ b/app/assets/stylesheets/pages/container_registry.scss @@ -3,10 +3,6 @@ */ .container-message { - pre { - white-space: pre-line; - } - span .btn { margin: 0; } @@ -31,4 +27,21 @@ .table.tags { margin-bottom: 0; + + .registry-image-row { + .check { + padding-right: $gl-padding; + width: 5%; + } + + .action-buttons { + opacity: 0; + } + + &:hover { + .action-buttons { + opacity: 1; + } + } + } } diff --git a/app/assets/stylesheets/pages/cycle_analytics.scss b/app/assets/stylesheets/pages/cycle_analytics.scss index 2b932d164a5..d80155a416d 100644 --- a/app/assets/stylesheets/pages/cycle_analytics.scss +++ b/app/assets/stylesheets/pages/cycle_analytics.scss @@ -51,27 +51,19 @@ } .stage-header { - width: 26%; - padding-left: $gl-padding; + width: 18.5%; } .median-header { - width: 14%; + width: 21.5%; } .event-header { width: 45%; - padding-left: $gl-padding; } .total-time-header { width: 15%; - text-align: right; - padding-right: $gl-padding; - } - - .stage-name { - font-weight: $gl-font-weight-bold; } } @@ -153,23 +145,13 @@ } .stage-nav-item { - display: flex; line-height: 65px; - border-top: 1px solid transparent; - border-bottom: 1px solid transparent; - border-right: 1px solid $border-color; - background-color: $gray-light; + border: 1px solid $border-color; &.active { - background-color: transparent; - border-right-color: transparent; - border-top-color: $border-color; - border-bottom-color: $border-color; - box-shadow: inset 2px 0 0 0 $blue-500; - - .stage-name { - font-weight: $gl-font-weight-bold; - } + background: $blue-50; + border-color: $blue-300; + box-shadow: inset 4px 0 0 0 $blue-500; } &:hover:not(.active) { @@ -178,24 +160,12 @@ cursor: pointer; } - &:first-child { - border-top: 0; - } - - &:last-child { - border-bottom: 0; - } - - .stage-nav-item-cell { - &.stage-median { - margin-left: auto; - margin-right: $gl-padding; - min-width: calc(35% - #{$gl-padding}); - } + .stage-nav-item-cell.stage-name { + width: 44.5%; } - .stage-name { - padding-left: 16px; + .stage-nav-item-cell.stage-median { + min-width: 43%; } .stage-empty, diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index ffb27e54f34..77a2fd6b876 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -1032,7 +1032,6 @@ table.code { $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; z-index: 202; .with-performance-bar & { @@ -1043,7 +1042,7 @@ table.code { .drag-handle { bottom: 16px; - transform: translateX(-6px); + transform: translateX(10px); } } diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 66b4f3bad2b..0e844b0e4a5 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -126,6 +126,16 @@ } } +.assignee { + .merge-icon { + color: $orange-500; + position: absolute; + bottom: 0; + right: 0; + text-shadow: -1px -1px 0 $white-light, 1px -1px 0 $white-light, -1px 1px 0 $white-light, 1px 1px 0 $white-light; + } +} + .right-sidebar { position: fixed; top: $header-height; @@ -202,7 +212,6 @@ &.assignee { .author-link { display: block; - padding-left: 42px; position: relative; &:hover { @@ -210,12 +219,6 @@ text-decoration: underline; } } - - .avatar { - left: 0; - position: absolute; - top: 0; - } } } } @@ -354,13 +357,6 @@ margin-top: 0; } - .assignee .avatar { - float: left; - margin-right: 10px; - margin-bottom: 0; - margin-left: 0; - } - .assignee .user-list .avatar { margin: 0; } @@ -521,7 +517,12 @@ display: none; } + .merge-icon { + font-size: 10px; + } + .multiple-users { + position: relative; height: 24px; margin-bottom: 17px; margin-top: 4px; diff --git a/app/assets/stylesheets/pages/members.scss b/app/assets/stylesheets/pages/members.scss index 45408c9ab3c..ae92a2fbd7b 100644 --- a/app/assets/stylesheets/pages/members.scss +++ b/app/assets/stylesheets/pages/members.scss @@ -58,11 +58,6 @@ } } -.member-access-text { - margin-left: auto; - line-height: 43px; -} - .member-search-btn { position: absolute; right: 4px; diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index 3c1e384d6ed..c8d155706a9 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -397,7 +397,6 @@ .mr-widget-help { padding: 10px 16px 10px ($gl-padding-8 * 7); - font-style: italic; } .ci-coverage { @@ -906,7 +905,7 @@ } .deploy-heading, -.merge-train-info { +.merge-train-position-indicator { @include media-breakpoint-up(md) { padding: $gl-padding-8 $gl-padding; } diff --git a/app/assets/stylesheets/pages/reports.scss b/app/assets/stylesheets/pages/reports.scss index 85e9f303dde..0fbf7033aa5 100644 --- a/app/assets/stylesheets/pages/reports.scss +++ b/app/assets/stylesheets/pages/reports.scss @@ -48,11 +48,6 @@ padding: $gl-padding-top $gl-padding; border-top: 1px solid $border-color; } - - .report-block-list-icon .loading-container { - position: relative; - left: -2px; - } } .report-block-container { diff --git a/app/assets/stylesheets/pages/todos.scss b/app/assets/stylesheets/pages/todos.scss index 586365eb1ce..7b64c67ae34 100644 --- a/app/assets/stylesheets/pages/todos.scss +++ b/app/assets/stylesheets/pages/todos.scss @@ -4,7 +4,7 @@ */ .todos-list > .todo { - // workaround because we cannot use border-colapse + // workaround because we cannot use border-collapse border-top: 1px solid transparent; display: flex; flex-direction: row; diff --git a/app/assets/stylesheets/pages/wiki.scss b/app/assets/stylesheets/pages/wiki.scss index 60400f10ca5..0b65b915abf 100644 --- a/app/assets/stylesheets/pages/wiki.scss +++ b/app/assets/stylesheets/pages/wiki.scss @@ -1,19 +1,3 @@ -.new-wiki-page { - .new-wiki-page-slug-tip { - display: inline-block; - max-width: 100%; - margin-top: 5px; - } -} - -.wiki-form { - .edit-wiki-page-slug-tip { - display: inline-block; - max-width: 100%; - margin-top: 5px; - } -} - .title .edit-wiki-header { width: 780px; margin-left: auto; @@ -22,7 +6,6 @@ } .wiki-page-header { - @extend .top-area; position: relative; .wiki-breadcrumb { @@ -49,13 +32,11 @@ color: $gl-text-color-secondary; } - .git-access-header { - padding: $gl-padding 0 $gl-padding-top; - } - .git-clone-holder { - width: 100%; - padding-bottom: 40px; + .input-group-prepend, + .input-group-append { + background-color: transparent; + } } button.sidebar-toggle { @@ -65,19 +46,8 @@ display: block; } - @include media-breakpoint-up(sm) { - &.has-sidebar-toggle { - padding-right: 40px; - } - - .git-clone-holder { - width: 480px; - padding-bottom: $gl-padding; - } - - .nav-controls { - width: auto; - } + &.has-sidebar-toggle .git-access-header { + padding-right: $sidebar-toggle-width; } @include media-breakpoint-up(md) { @@ -122,10 +92,6 @@ padding: 0 $gl-padding; } - .block { - width: 100%; - } - a { color: $layout-link-gray; diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 1d55a073f3b..af6644b8fcc 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -12,6 +12,7 @@ class ApplicationController < ActionController::Base include EnforcesTwoFactorAuthentication include WithPerformanceBar include SessionlessAuthentication + include ConfirmEmailWarning before_action :authenticate_user! before_action :enforce_terms!, if: :should_enforce_terms? @@ -116,7 +117,7 @@ class ApplicationController < ActionController::Base def render(*args) super.tap do # Set a header for custom error pages to prevent them from being intercepted by gitlab-workhorse - if response.content_type == 'text/html' && (400..599).cover?(response.status) + if (400..599).cover?(response.status) && workhorse_excluded_content_types.include?(response.content_type) response.headers['X-GitLab-Custom-Error'] = '1' end end @@ -124,6 +125,10 @@ class ApplicationController < ActionController::Base protected + def workhorse_excluded_content_types + @workhorse_excluded_content_types ||= %w(text/html application/json) + end + def append_info_to_payload(payload) super diff --git a/app/controllers/autocomplete_controller.rb b/app/controllers/autocomplete_controller.rb index f111c7ca8cc..30a567c3bef 100644 --- a/app/controllers/autocomplete_controller.rb +++ b/app/controllers/autocomplete_controller.rb @@ -36,7 +36,7 @@ class AutocompleteController < ApplicationController end def award_emojis - render json: AwardedEmojiFinder.new(current_user).execute + render json: AwardEmojis::CollectUserEmojiService.new(current_user).execute end def merge_request_target_branches diff --git a/app/controllers/concerns/confirm_email_warning.rb b/app/controllers/concerns/confirm_email_warning.rb new file mode 100644 index 00000000000..5a4b5897a4f --- /dev/null +++ b/app/controllers/concerns/confirm_email_warning.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module ConfirmEmailWarning + extend ActiveSupport::Concern + + included do + before_action :set_confirm_warning, if: -> { Feature.enabled?(:soft_email_confirmation) } + end + + protected + + def set_confirm_warning + return unless current_user + return if current_user.confirmed? + return if peek_request? || json_request? || !request.get? + + email = current_user.unconfirmed_email || current_user.email + + flash.now[:warning] = _("Please check your email (%{email}) to verify that you own this address. Didn't receive it? %{resend_link}. Wrong email address? %{update_link}.").html_safe % { + email: email, + resend_link: view_context.link_to(_('Resend it'), user_confirmation_path(user: { email: email }), method: :post), + update_link: view_context.link_to(_('Update it'), profile_path) + } + end +end diff --git a/app/controllers/concerns/invisible_captcha.rb b/app/controllers/concerns/invisible_captcha.rb new file mode 100644 index 00000000000..45c0a5c58ef --- /dev/null +++ b/app/controllers/concerns/invisible_captcha.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +module InvisibleCaptcha + extend ActiveSupport::Concern + + included do + invisible_captcha only: :create, on_spam: :on_honeypot_spam_callback, on_timestamp_spam: :on_timestamp_spam_callback + end + + def on_honeypot_spam_callback + return unless Feature.enabled?(:invisible_captcha) + + invisible_captcha_honeypot_counter.increment + log_request('Invisible_Captcha_Honeypot_Request') + + head(200) + end + + def on_timestamp_spam_callback + return unless Feature.enabled?(:invisible_captcha) + + invisible_captcha_timestamp_counter.increment + log_request('Invisible_Captcha_Timestamp_Request') + + redirect_to new_user_session_path, alert: InvisibleCaptcha.timestamp_error_message + end + + def invisible_captcha_honeypot_counter + @invisible_captcha_honeypot_counter ||= + Gitlab::Metrics.counter(:bot_blocked_by_invisible_captcha_honeypot, + 'Counter of blocked sign up attempts with filled honeypot') + end + + def invisible_captcha_timestamp_counter + @invisible_captcha_timestamp_counter ||= + Gitlab::Metrics.counter(:bot_blocked_by_invisible_captcha_timestamp, + 'Counter of blocked sign up attempts with invalid timestamp') + end + + def log_request(message) + request_information = { + message: message, + env: :invisible_captcha_signup_bot_detected, + remote_ip: request.ip, + request_method: request.request_method, + path: request.fullpath + } + + Gitlab::AuthLogger.error(request_information) + end +end diff --git a/app/controllers/concerns/issuable_collections.rb b/app/controllers/concerns/issuable_collections.rb index 3489ea78b77..8ea77b994de 100644 --- a/app/controllers/concerns/issuable_collections.rb +++ b/app/controllers/concerns/issuable_collections.rb @@ -2,8 +2,8 @@ module IssuableCollections extend ActiveSupport::Concern - include CookiesHelper include SortingHelper + include SortingPreference include Gitlab::IssuableMetadata include Gitlab::Utils::StrongMemoize @@ -127,47 +127,8 @@ module IssuableCollections 'opened' end - def set_sort_order - set_sort_order_from_user_preference || set_sort_order_from_cookie || default_sort_order - end - - def set_sort_order_from_user_preference - return unless current_user - return unless issuable_sorting_field - - user_preference = current_user.user_preference - - sort_param = params[:sort] - sort_param ||= user_preference[issuable_sorting_field] - - return sort_param if Gitlab::Database.read_only? - - if user_preference[issuable_sorting_field] != sort_param - user_preference.update(issuable_sorting_field => sort_param) - end - - sort_param - end - - # Implement issuable_sorting_field method on controllers - # to choose which column to store the sorting parameter. - def issuable_sorting_field - nil - end - - def set_sort_order_from_cookie - sort_param = params[:sort] if params[:sort].present? - # fallback to legacy cookie value for backward compatibility - sort_param ||= cookies['issuable_sort'] - sort_param ||= cookies[remember_sorting_key] - - sort_value = update_cookie_value(sort_param) - set_secure_cookie(remember_sorting_key, sort_value) - sort_value - end - - def remember_sorting_key - @remember_sorting_key ||= "#{collection_type.downcase}_sort" + def legacy_sort_cookie_name + 'issuable_sort' end def default_sort_order @@ -178,17 +139,6 @@ module IssuableCollections end end - # Update old values to the actual ones. - def update_cookie_value(value) - case value - when 'id_asc' then sort_value_oldest_created - when 'id_desc' then sort_value_recently_created - when 'downvotes_asc' then sort_value_popularity - when 'downvotes_desc' then sort_value_popularity - else value - end - end - def finder @finder ||= issuable_finder_for(finder_type) end diff --git a/app/controllers/concerns/issuable_collections_action.rb b/app/controllers/concerns/issuable_collections_action.rb index 4ad287c4a13..0a6f684a9fc 100644 --- a/app/controllers/concerns/issuable_collections_action.rb +++ b/app/controllers/concerns/issuable_collections_action.rb @@ -32,7 +32,7 @@ module IssuableCollectionsAction private - def issuable_sorting_field + def sorting_field case action_name when 'issues' Issue::SORTING_PREFERENCE_FIELD diff --git a/app/controllers/concerns/sorting_preference.rb b/app/controllers/concerns/sorting_preference.rb new file mode 100644 index 00000000000..a51b68147d5 --- /dev/null +++ b/app/controllers/concerns/sorting_preference.rb @@ -0,0 +1,85 @@ +# frozen_string_literal: true + +module SortingPreference + include SortingHelper + include CookiesHelper + + def set_sort_order + set_sort_order_from_user_preference || set_sort_order_from_cookie || params[:sort] || default_sort_order + end + + # Implement sorting_field method on controllers + # to choose which column to store the sorting parameter. + def sorting_field + nil + end + + # Implement default_sort_order method on controllers + # to choose which default sort should be applied if + # sort param is not provided. + def default_sort_order + nil + end + + # Implement legacy_sort_cookie_name method on controllers + # to set sort from cookie for backwards compatibility. + def legacy_sort_cookie_name + nil + end + + private + + def set_sort_order_from_user_preference + return unless current_user + return unless sorting_field + + user_preference = current_user.user_preference + + sort_param = params[:sort] + sort_param ||= user_preference[sorting_field] + + return sort_param if Gitlab::Database.read_only? + + if user_preference[sorting_field] != sort_param + user_preference.update(sorting_field => sort_param) + end + + sort_param + end + + def set_sort_order_from_cookie + return unless legacy_sort_cookie_name + + sort_param = params[:sort] if params[:sort].present? + # fallback to legacy cookie value for backward compatibility + sort_param ||= cookies[legacy_sort_cookie_name] + sort_param ||= cookies[remember_sorting_key] + + sort_value = update_cookie_value(sort_param) + set_secure_cookie(remember_sorting_key, sort_value) + sort_value + end + + # Convert sorting_field to legacy cookie name for backwards compatibility + # :merge_requests_sort => 'mergerequest_sort' + # :issues_sort => 'issue_sort' + def remember_sorting_key + @remember_sorting_key ||= sorting_field + .to_s + .split('_')[0..-2] + .map(&:singularize) + .join('') + .concat('_sort') + end + + # Update old values to the actual ones. + def update_cookie_value(value) + case value + when 'id_asc' then sort_value_oldest_created + when 'id_desc' then sort_value_recently_created + when 'downvotes_asc' then sort_value_popularity + when 'downvotes_desc' then sort_value_popularity + else value + end + end +end diff --git a/app/controllers/concerns/toggle_award_emoji.rb b/app/controllers/concerns/toggle_award_emoji.rb index 97b343f8b1a..24d178781d6 100644 --- a/app/controllers/concerns/toggle_award_emoji.rb +++ b/app/controllers/concerns/toggle_award_emoji.rb @@ -7,12 +7,9 @@ module ToggleAwardEmoji authenticate_user! name = params.require(:name) - if awardable.user_can_award?(current_user) - awardable.toggle_award_emoji(name, current_user) - - todoable = to_todoable(awardable) - TodoService.new.new_award_emoji(todoable, current_user) if todoable + service = AwardEmojis::ToggleService.new(awardable, name, current_user).execute + if service[:status] == :success render json: { ok: true } else render json: { ok: false } @@ -21,18 +18,6 @@ module ToggleAwardEmoji private - def to_todoable(awardable) - case awardable - when Note - # we don't create todos for personal snippet comments for now - awardable.for_personal_snippet? ? nil : awardable.noteable - when MergeRequest, Issue - awardable - when Snippet - nil - end - end - def awardable raise NotImplementedError end diff --git a/app/controllers/confirmations_controller.rb b/app/controllers/confirmations_controller.rb index 2ae500a2fdf..b192189ba3c 100644 --- a/app/controllers/confirmations_controller.rb +++ b/app/controllers/confirmations_controller.rb @@ -11,7 +11,7 @@ class ConfirmationsController < Devise::ConfirmationsController protected def after_resending_confirmation_instructions_path_for(resource) - users_almost_there_path + Feature.enabled?(:soft_email_confirmation) ? stored_location_for(resource) || dashboard_projects_path : users_almost_there_path end def after_confirmation_path_for(resource_name, resource) diff --git a/app/controllers/dashboard/projects_controller.rb b/app/controllers/dashboard/projects_controller.rb index daeb8fda417..71f18694613 100644 --- a/app/controllers/dashboard/projects_controller.rb +++ b/app/controllers/dashboard/projects_controller.rb @@ -4,10 +4,12 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController include ParamsBackwardCompatibility include RendersMemberAccess include OnboardingExperimentHelper + include SortingHelper + include SortingPreference prepend_before_action(only: [:index]) { authenticate_sessionless_user!(:rss) } before_action :set_non_archived_param - before_action :default_sorting + before_action :set_sorting before_action :projects, only: [:index] skip_cross_project_access_check :index, :starred @@ -59,11 +61,6 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController end end - def default_sorting - params[:sort] ||= 'latest_activity_desc' - @sort = params[:sort] - end - # rubocop: disable CodeReuse/ActiveRecord def load_projects(finder_params) @total_user_projects_count = ProjectsFinder.new(params: { non_public: true }, current_user: current_user).execute @@ -88,4 +85,17 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController Events::RenderService.new(current_user).execute(@events, atom_request: request.format.atom?) end + + def set_sorting + params[:sort] = set_sort_order + @sort = params[:sort] + end + + def default_sort_order + sort_value_latest_activity + end + + def sorting_field + Project::SORTING_PREFERENCE_FIELD + end end diff --git a/app/controllers/explore/projects_controller.rb b/app/controllers/explore/projects_controller.rb index ef86d5f981a..271f2b4b57d 100644 --- a/app/controllers/explore/projects_controller.rb +++ b/app/controllers/explore/projects_controller.rb @@ -3,12 +3,13 @@ class Explore::ProjectsController < Explore::ApplicationController include ParamsBackwardCompatibility include RendersMemberAccess + include SortingHelper + include SortingPreference before_action :set_non_archived_param + before_action :set_sorting def index - params[:sort] ||= 'latest_activity_desc' - @sort = params[:sort] @projects = load_projects respond_to do |format| @@ -23,7 +24,6 @@ class Explore::ProjectsController < Explore::ApplicationController def trending params[:trending] = true - @sort = params[:sort] @projects = load_projects respond_to do |format| @@ -67,4 +67,17 @@ class Explore::ProjectsController < Explore::ApplicationController prepare_projects_for_rendering(projects) end # rubocop: enable CodeReuse/ActiveRecord + + def set_sorting + params[:sort] = set_sort_order + @sort = params[:sort] + end + + def default_sort_order + sort_value_latest_activity + end + + def sorting_field + Project::SORTING_PREFERENCE_FIELD + end end diff --git a/app/controllers/groups/runners_controller.rb b/app/controllers/groups/runners_controller.rb index f8e32451b02..af2b2cbd1fd 100644 --- a/app/controllers/groups/runners_controller.rb +++ b/app/controllers/groups/runners_controller.rb @@ -3,7 +3,7 @@ class Groups::RunnersController < Groups::ApplicationController # Proper policies should be implemented per # https://gitlab.com/gitlab-org/gitlab-ce/issues/45894 - before_action :authorize_admin_pipeline! + before_action :authorize_admin_group! before_action :runner, only: [:edit, :update, :destroy, :pause, :resume, :show] @@ -50,10 +50,6 @@ class Groups::RunnersController < Groups::ApplicationController @runner ||= @group.runners.find(params[:id]) end - def authorize_admin_pipeline! - return render_404 unless can?(current_user, :admin_pipeline, group) - end - def runner_params params.require(:runner).permit(Ci::Runner::FORM_EDITABLE) end diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index 5472ef05d7c..886d1f99d69 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -176,6 +176,7 @@ class GroupsController < Groups::ApplicationController [ :avatar, :description, + :emails_disabled, :lfs_enabled, :name, :path, diff --git a/app/controllers/jwt_controller.rb b/app/controllers/jwt_controller.rb index 5ecf4f114cf..da39d64c93d 100644 --- a/app/controllers/jwt_controller.rb +++ b/app/controllers/jwt_controller.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true class JwtController < ApplicationController + skip_around_action :set_session_storage skip_before_action :authenticate_user! skip_before_action :verify_authenticity_token before_action :authenticate_project_or_user diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb index b04ffe80db4..4125f44d00a 100644 --- a/app/controllers/projects/blob_controller.rb +++ b/app/controllers/projects/blob_controller.rb @@ -92,7 +92,7 @@ class Projects::BlobController < Projects::ApplicationController def diff apply_diff_view_cookie! - @form = Blobs::UnfoldPresenter.new(blob, params.to_unsafe_h) + @form = Blobs::UnfoldPresenter.new(blob, diff_params) # keep only json rendering when # https://gitlab.com/gitlab-org/gitlab-ce/issues/44988 is done @@ -239,4 +239,8 @@ class Projects::BlobController < Projects::ApplicationController def tree_path @path.rpartition('/').first end + + def diff_params + params.permit(:full, :since, :to, :bottom, :unfold, :offset, :indent) + end end diff --git a/app/controllers/projects/cycle_analytics_controller.rb b/app/controllers/projects/cycle_analytics_controller.rb index 2d46a71bf99..3b0abecf2c9 100644 --- a/app/controllers/projects/cycle_analytics_controller.rb +++ b/app/controllers/projects/cycle_analytics_controller.rb @@ -14,8 +14,14 @@ class Projects::CycleAnalyticsController < Projects::ApplicationController @cycle_analytics_no_data = @cycle_analytics.no_stats? respond_to do |format| - format.html - format.json { render json: cycle_analytics_json } + format.html do + Gitlab::UsageDataCounters::CycleAnalyticsCounter.count(:views) + + render :show + end + format.json do + render json: cycle_analytics_json + end end end diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb index df9e55fda2a..5a1f93dc609 100644 --- a/app/controllers/projects/environments_controller.rb +++ b/app/controllers/projects/environments_controller.rb @@ -15,7 +15,6 @@ class Projects::EnvironmentsController < Projects::ApplicationController push_frontend_feature_flag(:environment_metrics_show_multiple_dashboards) push_frontend_feature_flag(:environment_metrics_additional_panel_types) push_frontend_feature_flag(:prometheus_computed_alerts) - push_frontend_feature_flag(:export_metrics_to_csv_enabled) end def index diff --git a/app/controllers/projects/git_http_client_controller.rb b/app/controllers/projects/git_http_client_controller.rb index 956093b972b..abf8407a51c 100644 --- a/app/controllers/projects/git_http_client_controller.rb +++ b/app/controllers/projects/git_http_client_controller.rb @@ -49,7 +49,8 @@ class Projects::GitHttpClientController < Projects::ApplicationController send_final_spnego_response return # Allow access end - elsif project && download_request? && Guest.can?(:download_code, project) + elsif project && download_request? && http_allowed? && Guest.can?(:download_code, project) + @authentication_result = Gitlab::Auth::Result.new(nil, project, :none, [:download_code]) return # Allow access @@ -113,4 +114,8 @@ class Projects::GitHttpClientController < Projects::ApplicationController def ci? authentication_result.ci?(project) end + + def http_allowed? + Gitlab::ProtocolAccess.allowed?('http') + end end diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index bc9166b9df3..b7fd286bfe0 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -190,7 +190,7 @@ class Projects::IssuesController < Projects::ApplicationController protected - def issuable_sorting_field + def sorting_field Issue::SORTING_PREFERENCE_FIELD end diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index ee755d68cf1..75d4a24b559 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -12,6 +12,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo skip_before_action :merge_request, only: [:index, :bulk_update] before_action :whitelist_query_limiting, only: [:assign_related_issues, :update] before_action :authorize_update_issuable!, only: [:close, :edit, :update, :remove_wip, :sort] + before_action :authorize_test_reports!, only: [:test_reports] before_action :set_issuables_index, only: [:index] before_action :authenticate_user!, only: [:assign_related_issues] before_action :check_user_can_push_to_source_branch!, only: [:rebase] @@ -219,7 +220,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo alias_method :issuable, :merge_request alias_method :awardable, :merge_request - def issuable_sorting_field + def sorting_field MergeRequest::SORTING_PREFERENCE_FIELD end @@ -343,4 +344,9 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo render json: { status_reason: 'Unknown error' }, status: :internal_server_error end end + + def authorize_test_reports! + # MergeRequest#actual_head_pipeline is the pipeline accessed in MergeRequest#compare_reports. + return render_404 unless can?(current_user, :read_build, merge_request.actual_head_pipeline) + end end diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb index db3b7c8b177..499d4918899 100644 --- a/app/controllers/projects/pipelines_controller.rb +++ b/app/controllers/projects/pipelines_controller.rb @@ -3,6 +3,7 @@ class Projects::PipelinesController < Projects::ApplicationController before_action :whitelist_query_limiting, only: [:create, :retry] before_action :pipeline, except: [:index, :new, :create, :charts] + before_action :set_pipeline_path, only: [:show] before_action :authorize_read_pipeline! before_action :authorize_read_build!, only: [:index] before_action :authorize_create_pipeline!, only: [:new, :create] @@ -174,14 +175,36 @@ class Projects::PipelinesController < Projects::ApplicationController # rubocop: disable CodeReuse/ActiveRecord def pipeline - @pipeline ||= project - .all_pipelines - .includes(user: :status) - .find_by!(id: params[:id]) - .present(current_user: current_user) + @pipeline ||= if params[:id].blank? && params[:latest] + latest_pipeline + else + project + .all_pipelines + .includes(builds: :tags, user: :status) + .find_by!(id: params[:id]) + .present(current_user: current_user) + end end # rubocop: enable CodeReuse/ActiveRecord + def set_pipeline_path + @pipeline_path ||= if params[:id].blank? && params[:latest] + latest_project_pipelines_path(@project, params['ref']) + else + project_pipeline_path(@project, @pipeline) + end + end + + def latest_pipeline + ref = params['ref'].presence || @project.default_branch + sha = @project.commit(ref)&.sha + + @project.ci_pipelines + .newest_first(ref: ref, sha: sha) + .first + &.present(current_user: current_user) + end + def whitelist_query_limiting # Also see https://gitlab.com/gitlab-org/gitlab-ce/issues/42343 Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42339') diff --git a/app/controllers/projects/raw_controller.rb b/app/controllers/projects/raw_controller.rb index 3254229d9cb..c94fdd9483d 100644 --- a/app/controllers/projects/raw_controller.rb +++ b/app/controllers/projects/raw_controller.rb @@ -26,7 +26,7 @@ class Projects::RawController < Projects::ApplicationController limiter.log_request(request, :raw_blob_request_limit, current_user) flash[:alert] = _('You cannot access the raw file. Please wait a minute.') - redirect_to project_blob_path(@project, File.join(@ref, @path)) + redirect_to project_blob_path(@project, File.join(@ref, @path)), status: :too_many_requests end def raw_blob_request_limit diff --git a/app/controllers/projects/registry/tags_controller.rb b/app/controllers/projects/registry/tags_controller.rb index bf1d8d8b5fc..54e2faa2dd7 100644 --- a/app/controllers/projects/registry/tags_controller.rb +++ b/app/controllers/projects/registry/tags_controller.rb @@ -5,6 +5,8 @@ module Projects class TagsController < ::Projects::Registry::ApplicationController before_action :authorize_destroy_container_image!, only: [:destroy] + LIMIT = 15 + def index respond_to do |format| format.json do @@ -28,10 +30,40 @@ module Projects end end + def bulk_destroy + unless params[:ids].present? + head :bad_request + return + end + + tag_names = params[:ids] || [] + if tag_names.size > LIMIT + head :bad_request + return + end + + @tags = tag_names.map { |tag_name| image.tag(tag_name) } + unless @tags.all? { |tag| tag.valid_name? } + head :bad_request + return + end + + success_count = 0 + @tags.each do |tag| + if tag.delete + success_count += 1 + end + end + + respond_to do |format| + format.json { head(success_count == @tags.size ? :no_content : :bad_request) } + end + end + private def tags - Kaminari::PaginatableArray.new(image.tags, limit: 15) + Kaminari::PaginatableArray.new(image.tags, limit: LIMIT) end def image diff --git a/app/controllers/projects/starrers_controller.rb b/app/controllers/projects/starrers_controller.rb index c8facea1d70..4efe956e973 100644 --- a/app/controllers/projects/starrers_controller.rb +++ b/app/controllers/projects/starrers_controller.rb @@ -5,25 +5,11 @@ class Projects::StarrersController < Projects::ApplicationController def index @starrers = UsersStarProjectsFinder.new(@project, params, current_user: @current_user).execute - - # Normally the number of public starrers is equal to the number of visible - # starrers. We need to fix the counts in two cases: when the current user - # is an admin (and can see everything) and when the current user has a - # private profile and has starred the project (and can see itself). - @public_count = - if @current_user&.admin? - @starrers.with_public_profile.count - elsif @current_user&.private_profile && has_starred_project?(@starrers) - @starrers.size - 1 - else - @starrers.size - end - - @total_count = @project.starrers.size - @private_count = @total_count - @public_count - @sort = params[:sort].presence || sort_value_name - @starrers = @starrers.sort_by_attribute(@sort).page(params[:page]) + @starrers = @starrers.preload_users.sort_by_attribute(@sort).page(params[:page]) + @public_count = @project.starrers.with_public_profile.size + @total_count = @project.starrers.size + @private_count = @total_count - @public_count end private diff --git a/app/controllers/projects/wikis_controller.rb b/app/controllers/projects/wikis_controller.rb index d1914c35bd3..b187fdb2723 100644 --- a/app/controllers/projects/wikis_controller.rb +++ b/app/controllers/projects/wikis_controller.rb @@ -16,6 +16,10 @@ class Projects::WikisController < Projects::ApplicationController redirect_to(project_wiki_path(@project, @page)) end + def new + redirect_to project_wiki_path(@project, SecureRandom.uuid, random_title: true) + end + def pages @wiki_pages = Kaminari.paginate_array( @project_wiki.list_pages(sort: params[:sort], direction: params[:direction]) @@ -24,17 +28,25 @@ class Projects::WikisController < Projects::ApplicationController @wiki_entries = WikiPage.group_by_directory(@wiki_pages) end + # `#show` handles a number of scenarios: + # + # - If `id` matches a WikiPage, then show the wiki page. + # - If `id` is a file in the wiki repository, then send the file. + # - If we know the user wants to create a new page with the given `id`, + # then display a create form. + # - Otherwise show the empty wiki page and invite the user to create a page. def show - view_param = @project_wiki.empty? ? params[:view] : 'create' - if @page set_encoding_error unless valid_encoding? render 'show' elsif file_blob send_blob(@project_wiki.repository, file_blob) - elsif can?(current_user, :create_wiki, @project) && view_param == 'create' - @page = build_page(title: params[:id]) + elsif show_create_form? + # Assign a title to the WikiPage unless `id` is a randomly generated slug from #new + title = params[:id] unless params[:random_title].present? + + @page = build_page(title: title) render 'edit' else @@ -110,6 +122,15 @@ class Projects::WikisController < Projects::ApplicationController private + def show_create_form? + can?(current_user, :create_wiki, @project) && + @page.nil? && + # Always show the create form when the wiki has had at least one page created. + # Otherwise, we only show the form when the user has navigated from + # the 'empty wiki' page + (@project_wiki.exists? || params[:view] == 'create') + end + def load_project_wiki @project_wiki = load_wiki @@ -135,7 +156,7 @@ class Projects::WikisController < Projects::ApplicationController params.require(:wiki).permit(:title, :content, :format, :message, :last_commit_sha) end - def build_page(args) + def build_page(args = {}) WikiPage.new(@project_wiki).tap do |page| page.update_attributes(args) # rubocop:disable Rails/ActiveRecordAliases end diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index d4ff72c2314..5f335de4d6b 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -29,6 +29,7 @@ class ProjectsController < Projects::ApplicationController # Authorize before_action :authorize_admin_project!, only: [:edit, :update, :housekeeping, :download_export, :export, :remove_export, :generate_new_export] + before_action :authorize_archive_project!, only: [:archive, :unarchive] before_action :event_filter, only: [:show, :activity] layout :determine_layout @@ -164,8 +165,6 @@ class ProjectsController < Projects::ApplicationController end def archive - return access_denied! unless can?(current_user, :archive_project, @project) - ::Projects::UpdateService.new(@project, current_user, archived: true).execute respond_to do |format| @@ -174,8 +173,6 @@ class ProjectsController < Projects::ApplicationController end def unarchive - return access_denied! unless can?(current_user, :archive_project, @project) - ::Projects::UpdateService.new(@project, current_user, archived: false).execute respond_to do |format| @@ -361,6 +358,7 @@ class ProjectsController < Projects::ApplicationController :container_registry_enabled, :default_branch, :description, + :emails_disabled, :external_authorization_classification_label, :import_url, :issues_tracker, diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb index 638934694e0..e773ec09924 100644 --- a/app/controllers/registrations_controller.rb +++ b/app/controllers/registrations_controller.rb @@ -4,6 +4,7 @@ class RegistrationsController < Devise::RegistrationsController include Recaptcha::Verify include AcceptsPendingInvitations include RecaptchaExperimentHelper + include InvisibleCaptcha prepend_before_action :check_captcha, only: :create before_action :whitelist_query_limiting, only: [:destroy] @@ -68,12 +69,12 @@ class RegistrationsController < Devise::RegistrationsController def after_sign_up_path_for(user) Gitlab::AppLogger.info(user_created_message(confirmed: user.confirmed?)) - user.confirmed? ? stored_location_for(user) || dashboard_projects_path : users_almost_there_path + confirmed_or_unconfirmed_access_allowed(user) ? stored_location_or_dashboard(user) : users_almost_there_path end def after_inactive_sign_up_path_for(resource) Gitlab::AppLogger.info(user_created_message) - users_almost_there_path + Feature.enabled?(:soft_email_confirmation) ? dashboard_projects_path : users_almost_there_path end private @@ -134,4 +135,12 @@ class RegistrationsController < Devise::RegistrationsController def terms_accepted? Gitlab::Utils.to_boolean(params[:terms_opt_in]) end + + def confirmed_or_unconfirmed_access_allowed(user) + user.confirmed? || Feature.enabled?(:soft_email_confirmation) + end + + def stored_location_or_dashboard(user) + stored_location_for(user) || dashboard_projects_path + end end diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb index 13741548687..e30935be4b6 100644 --- a/app/controllers/search_controller.rb +++ b/app/controllers/search_controller.rb @@ -36,6 +36,15 @@ class SearchController < ApplicationController check_single_commit_result end + def count + params.require([:search, :scope]) + + scope = search_service.scope + count = search_service.search_results.formatted_count(scope) + + render json: { count: count } + end + # rubocop: disable CodeReuse/ActiveRecord def autocomplete term = params[:term] diff --git a/app/finders/award_emojis_finder.rb b/app/finders/award_emojis_finder.rb new file mode 100644 index 00000000000..7320e035409 --- /dev/null +++ b/app/finders/award_emojis_finder.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +class AwardEmojisFinder + attr_reader :awardable, :params + + def initialize(awardable, params = {}) + @awardable = awardable + @params = params + + validate_params + end + + def execute + awards = awardable.award_emoji + awards = by_name(awards) + awards = by_awarded_by(awards) + awards + end + + private + + def by_name(awards) + return awards unless params[:name] + + awards.named(params[:name]) + end + + def by_awarded_by(awards) + return awards unless params[:awarded_by] + + awards.awarded_by(params[:awarded_by]) + end + + def validate_params + return unless params.present? + + validate_name_param + validate_awarded_by_param + end + + def validate_name_param + return unless params[:name] + + raise ArgumentError, 'Invalid name param' unless params[:name].in?(Gitlab::Emoji.emojis_names) + end + + def validate_awarded_by_param + return unless params[:awarded_by] + + # awarded_by can be a `User`, or an ID + unless params[:awarded_by].is_a?(User) || params[:awarded_by].to_s.match(/\A\d+\Z/) + raise ArgumentError, 'Invalid awarded_by param' + end + end +end diff --git a/app/finders/awarded_emoji_finder.rb b/app/finders/awarded_emoji_finder.rb deleted file mode 100644 index f0cc17f3b26..00000000000 --- a/app/finders/awarded_emoji_finder.rb +++ /dev/null @@ -1,21 +0,0 @@ -# frozen_string_literal: true - -# Class for retrieving information about emoji awarded _by_ a particular user. -class AwardedEmojiFinder - attr_reader :current_user - - # current_user - The User to generate the data for. - def initialize(current_user = nil) - @current_user = current_user - end - - def execute - return [] unless current_user - - # We want the resulting data set to be an Array containing the emoji names - # in descending order, based on how often they were awarded. - AwardEmoji - .award_counts_for_user(current_user) - .map { |name, _| { name: name } } - end -end diff --git a/app/finders/remote_mirror_finder.rb b/app/finders/remote_mirror_finder.rb deleted file mode 100644 index 420db0077aa..00000000000 --- a/app/finders/remote_mirror_finder.rb +++ /dev/null @@ -1,15 +0,0 @@ -# frozen_string_literal: true - -class RemoteMirrorFinder - attr_accessor :params - - def initialize(params) - @params = params - end - - # rubocop: disable CodeReuse/ActiveRecord - def execute - RemoteMirror.find_by(id: params[:id]) - end - # rubocop: enable CodeReuse/ActiveRecord -end diff --git a/app/graphql/mutations/award_emojis/add.rb b/app/graphql/mutations/award_emojis/add.rb index 8e050dd6d29..85f3eb065bb 100644 --- a/app/graphql/mutations/award_emojis/add.rb +++ b/app/graphql/mutations/award_emojis/add.rb @@ -10,14 +10,11 @@ module Mutations check_object_is_awardable!(awardable) - # TODO this will be handled by AwardEmoji::AddService - # See https://gitlab.com/gitlab-org/gitlab-ce/issues/63372 and - # https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/29782 - award = awardable.create_award_emoji(args[:name], current_user) + service = ::AwardEmojis::AddService.new(awardable, args[:name], current_user).execute { - award_emoji: (award if award.persisted?), - errors: errors_on_object(award) + award_emoji: (service[:award] if service[:status] == :success), + errors: service[:errors] || [] } end end diff --git a/app/graphql/mutations/award_emojis/remove.rb b/app/graphql/mutations/award_emojis/remove.rb index 3ba85e445b8..f8a3d0ce390 100644 --- a/app/graphql/mutations/award_emojis/remove.rb +++ b/app/graphql/mutations/award_emojis/remove.rb @@ -10,22 +10,11 @@ module Mutations check_object_is_awardable!(awardable) - # TODO this check can be removed once AwardEmoji services are available. - # See https://gitlab.com/gitlab-org/gitlab-ce/issues/63372 and - # https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/29782 - unless awardable.awarded_emoji?(args[:name], current_user) - raise Gitlab::Graphql::Errors::ResourceNotAvailable, - 'You have not awarded emoji of type name to the awardable' - end - - # TODO this will be handled by AwardEmoji::DestroyService - # See https://gitlab.com/gitlab-org/gitlab-ce/issues/63372 and - # https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/29782 - awardable.remove_award_emoji(args[:name], current_user) + service = ::AwardEmojis::DestroyService.new(awardable, args[:name], current_user).execute { # Mutation response is always a `nil` award_emoji - errors: [] + errors: service[:errors] || [] } end end diff --git a/app/graphql/mutations/award_emojis/toggle.rb b/app/graphql/mutations/award_emojis/toggle.rb index c03902e8035..d822048f3a6 100644 --- a/app/graphql/mutations/award_emojis/toggle.rb +++ b/app/graphql/mutations/award_emojis/toggle.rb @@ -15,23 +15,15 @@ module Mutations check_object_is_awardable!(awardable) - # TODO this will be handled by AwardEmoji::ToggleService - # See https://gitlab.com/gitlab-org/gitlab-ce/issues/63372 and - # https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/29782 - award = awardable.toggle_award_emoji(args[:name], current_user) - - # Destroy returns a collection :( - award = award.first if award.is_a?(Array) - - errors = errors_on_object(award) + service = ::AwardEmojis::ToggleService.new(awardable, args[:name], current_user).execute toggled_on = awardable.awarded_emoji?(args[:name], current_user) { # For consistency with the AwardEmojis::Remove mutation, only return # the AwardEmoji if it was created and not destroyed - award_emoji: (award if toggled_on), - errors: errors, + award_emoji: (service[:award] if toggled_on), + errors: service[:errors] || [], toggled_on: toggled_on } end diff --git a/app/graphql/types/namespace_type.rb b/app/graphql/types/namespace_type.rb index f105e9e6e28..35a97b5ace0 100644 --- a/app/graphql/types/namespace_type.rb +++ b/app/graphql/types/namespace_type.rb @@ -19,6 +19,11 @@ module Types field :lfs_enabled, GraphQL::BOOLEAN_TYPE, null: true, method: :lfs_enabled? field :request_access_enabled, GraphQL::BOOLEAN_TYPE, null: true + field :root_storage_statistics, Types::RootStorageStatisticsType, + null: true, + description: 'The aggregated storage statistics. Only available for root namespaces', + resolve: -> (obj, _args, _ctx) { Gitlab::Graphql::Loaders::BatchRootStorageStatisticsLoader.new(obj.id).find } + field :projects, Types::ProjectType.connection_type, null: false, diff --git a/app/graphql/types/root_storage_statistics_type.rb b/app/graphql/types/root_storage_statistics_type.rb new file mode 100644 index 00000000000..a7498ee0a2e --- /dev/null +++ b/app/graphql/types/root_storage_statistics_type.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module Types + class RootStorageStatisticsType < BaseObject + graphql_name 'RootStorageStatistics' + + authorize :read_statistics + + field :storage_size, GraphQL::INT_TYPE, null: false, description: 'The total storage in bytes' + field :repository_size, GraphQL::INT_TYPE, null: false, description: 'The git repository size in bytes' + field :lfs_objects_size, GraphQL::INT_TYPE, null: false, description: 'The LFS objects size in bytes' + field :build_artifacts_size, GraphQL::INT_TYPE, null: false, description: 'The CI artifacts size in bytes' + field :packages_size, GraphQL::INT_TYPE, null: false, description: 'The packages size in bytes' + field :wiki_size, GraphQL::INT_TYPE, null: false, description: 'The wiki size in bytes' + end +end diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb index acbcf0ded17..0ab19f1d2d2 100644 --- a/app/helpers/application_settings_helper.rb +++ b/app/helpers/application_settings_helper.rb @@ -270,7 +270,11 @@ module ApplicationSettingsHelper :diff_max_patch_bytes, :commit_email_hostname, :protected_ci_variables, - :local_markdown_version + :local_markdown_version, + :snowplow_collector_hostname, + :snowplow_cookie_domain, + :snowplow_enabled, + :snowplow_site_id ] end diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb index f2b5b82b013..144df676304 100644 --- a/app/helpers/ci_status_helper.rb +++ b/app/helpers/ci_status_helper.rb @@ -105,14 +105,13 @@ module CiStatusHelper path = pipelines_project_commit_path(project, commit, ref: ref) render_status_with_link( - 'commit', commit.status(ref), path, tooltip_placement: tooltip_placement, icon_size: 24) end - def render_status_with_link(type, status, path = nil, tooltip_placement: 'left', cssclass: '', container: 'body', icon_size: 16) + def render_status_with_link(status, path = nil, type: _('pipeline'), tooltip_placement: 'left', cssclass: '', container: 'body', icon_size: 16) klass = "ci-status-link ci-status-icon-#{status.dasherize} d-inline-flex #{cssclass}" title = "#{type.titleize}: #{ci_label_for_status(status)}" data = { toggle: 'tooltip', placement: tooltip_placement, container: container } diff --git a/app/helpers/emails_helper.rb b/app/helpers/emails_helper.rb index 36122d3a22a..23596769738 100644 --- a/app/helpers/emails_helper.rb +++ b/app/helpers/emails_helper.rb @@ -90,6 +90,8 @@ module EmailsHelper when MergeRequest merge_request = MergeRequest.find(closed_via[:id]).present + return "" unless Ability.allowed?(@recipient, :read_merge_request, merge_request) + case format when :html merge_request_link = link_to(merge_request.to_reference, merge_request.web_url) @@ -102,6 +104,8 @@ module EmailsHelper # Technically speaking this should be Commit but per # https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/15610#note_163812339 # we can't deserialize Commit without custom serializer for ActiveJob + return "" unless Ability.allowed?(@recipient, :download_code, @project) + _("via %{closed_via}") % { closed_via: closed_via } else "" diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb index 160f9ac4793..bd26bd01313 100644 --- a/app/helpers/groups_helper.rb +++ b/app/helpers/groups_helper.rb @@ -31,6 +31,11 @@ module GroupsHelper can?(current_user, :change_share_with_group_lock, group) end + def can_disable_group_emails?(group) + Feature.enabled?(:emails_disabled, group, default_enabled: true) && + can?(current_user, :set_emails_disabled, group) && !group.parent&.emails_disabled? + end + def group_issues_count(state:) IssuesFinder .new(current_user, group_id: @group.id, state: state, non_archived: true, include_subgroups: true) diff --git a/app/helpers/import_helper.rb b/app/helpers/import_helper.rb index 3d494c3de6a..9122ad5b35a 100644 --- a/app/helpers/import_helper.rb +++ b/app/helpers/import_helper.rb @@ -45,17 +45,14 @@ module ImportHelper end def import_github_authorize_message - _('To import GitHub repositories, you first need to authorize GitLab to access the list of your GitHub repositories:') + _('To connect GitHub repositories, you first need to authorize GitLab to access the list of your GitHub repositories.') end def import_github_personal_access_token_message - personal_access_token_link = link_to _('Personal Access Token'), 'https://github.com/settings/tokens' + link_url = 'https://github.com/settings/tokens' + link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: link_url } - if github_import_configured? - _('Alternatively, you can use a %{personal_access_token_link}. When you create your Personal Access Token, you will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to import.').html_safe % { personal_access_token_link: personal_access_token_link } - else - _('To import GitHub repositories, you can use a %{personal_access_token_link}. When you create your Personal Access Token, you will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to import.').html_safe % { personal_access_token_link: personal_access_token_link } - end + _('Create and provide your GitHub %{link_start}Personal Access Token%{link_end}. You will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to import.').html_safe % { link_start: link_start, link_end: '</a>'.html_safe } end def import_configure_github_admin_message diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index e2e007eee50..b88b25eb845 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -405,7 +405,11 @@ module IssuablesHelper placement: is_collapsed ? 'left' : nil, container: is_collapsed ? 'body' : nil, boundary: 'viewport', - is_collapsed: is_collapsed + is_collapsed: is_collapsed, + track_label: "right_sidebar", + track_property: "update_todo", + track_event: "click_button", + track_value: "" } end diff --git a/app/helpers/notifications_helper.rb b/app/helpers/notifications_helper.rb index 11b9cf22142..8855e0cdd70 100644 --- a/app/helpers/notifications_helper.rb +++ b/app/helpers/notifications_helper.rb @@ -5,7 +5,7 @@ module NotificationsHelper def notification_icon_class(level) case level.to_sym - when :disabled + when :disabled, :owner_disabled 'microphone-slash' when :participating 'volume-up' @@ -18,6 +18,16 @@ module NotificationsHelper end end + def notification_icon_level(notification_setting, emails_disabled = false) + if emails_disabled + 'owner_disabled' + elsif notification_setting.global? + current_user.global_notification_setting.level + else + notification_setting.level + end + end + def notification_icon(level, text = nil) icon("#{notification_icon_class(level)} fw", text: text) end @@ -53,6 +63,8 @@ module NotificationsHelper _('Use your global notification setting') when :custom _('You will only receive notifications for the events you choose') + when :owner_disabled + _('Notifications have been disabled by the project or group owner') end end @@ -94,9 +106,9 @@ module NotificationsHelper end end - def notification_setting_icon(notification_setting) + def notification_setting_icon(notification_setting = nil) sprite_icon( - notification_setting.disabled? ? "notifications-off" : "notifications", + !notification_setting.present? || notification_setting.disabled? ? "notifications-off" : "notifications", css_class: "icon notifications-icon js-notifications-icon" ) end diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 71c9c121e48..33bf2d57fae 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -155,6 +155,12 @@ module ProjectsHelper end end + def can_disable_emails?(project, current_user) + return false if project.group&.emails_disabled? + + can?(current_user, :set_emails_disabled, project) && Feature.enabled?(:emails_disabled, project, default_enabled: true) + end + def last_push_event current_user&.recent_push(@project) end @@ -541,13 +547,15 @@ module ProjectsHelper snippetsAccessLevel: feature.snippets_access_level, pagesAccessLevel: feature.pages_access_level, containerRegistryEnabled: !!project.container_registry_enabled, - lfsEnabled: !!project.lfs_enabled + lfsEnabled: !!project.lfs_enabled, + emailsDisabled: project.emails_disabled? } end def project_permissions_panel_data(project) { currentSettings: project_permissions_settings(project), + canDisableEmails: can_disable_emails?(project, current_user), canChangeVisibilityLevel: can_change_visibility_level?(project, current_user), allowedVisibilityOptions: project_allowed_visibility_levels(project), visibilityHelpPath: help_page_path('public_access/public_access'), diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb index f5c4686a3bf..91c83380b62 100644 --- a/app/helpers/search_helper.rb +++ b/app/helpers/search_helper.rb @@ -145,17 +145,27 @@ module SearchHelper Sanitize.clean(str) end - def search_filter_path(options = {}) - exist_opts = { - search: params[:search], - project_id: params[:project_id], - group_id: params[:group_id], - scope: params[:scope], - repository_ref: params[:repository_ref] - } + def search_filter_link(scope, label, data: {}, search: {}) + search_params = params + .merge(search) + .merge({ scope: scope }) + .permit(:search, :scope, :project_id, :group_id, :repository_ref, :snippets) + + if @scope == scope + li_class = 'active' + count = @search_results.formatted_count(scope) + else + badge_class = 'js-search-count hidden' + badge_data = { url: search_count_path(search_params) } + end - options = exist_opts.merge(options) - search_path(options) + content_tag :li, class: li_class, data: data do + link_to search_path(search_params) do + concat label + concat ' ' + concat content_tag(:span, count, class: ['badge badge-pill', badge_class], data: badge_data) + end + end end def search_filter_input_options(type) @@ -212,10 +222,6 @@ module SearchHelper sanitize(html, tags: %w(a p ol ul li pre code)) end - def limited_count(count, limit = 1000) - count > limit ? "#{limit}+" : count - end - def search_tabs?(tab) return false if Feature.disabled?(:users_search, default_enabled: true) diff --git a/app/helpers/sessions_helper.rb b/app/helpers/sessions_helper.rb new file mode 100644 index 00000000000..af98a611b8b --- /dev/null +++ b/app/helpers/sessions_helper.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +module SessionsHelper + def unconfirmed_email? + flash[:alert] == t(:unconfirmed, scope: [:devise, :failure]) + end +end diff --git a/app/helpers/todos_helper.rb b/app/helpers/todos_helper.rb index 645160077f5..38142bc68cb 100644 --- a/app/helpers/todos_helper.rb +++ b/app/helpers/todos_helper.rb @@ -26,7 +26,7 @@ module TodosHelper end def todo_target_link(todo) - text = raw("#{todo.target_type.titleize.downcase} ") + + text = raw(todo_target_type_name(todo) + ' ') + if todo.for_commit? content_tag(:span, todo.target_reference, class: 'commit-sha') else @@ -36,23 +36,34 @@ module TodosHelper link_to text, todo_target_path(todo), class: 'has-tooltip', title: todo.target.title end + def todo_target_type_name(todo) + todo.target_type.titleize.downcase + end + def todo_target_path(todo) return unless todo.target.present? - anchor = dom_id(todo.note) if todo.note.present? + path_options = todo_target_path_options(todo) if todo.for_commit? - project_commit_path(todo.project, - todo.target, anchor: anchor) + project_commit_path(todo.project, todo.target, path_options) else path = [todo.parent, todo.target] path.unshift(:pipelines) if todo.build_failed? - polymorphic_path(path, anchor: anchor) + polymorphic_path(path, path_options) end end + def todo_target_path_options(todo) + { anchor: todo_target_path_anchor(todo) } + end + + def todo_target_path_anchor(todo) + dom_id(todo.note) if todo.note.present? + end + def todo_target_state_pill(todo) return unless show_todo_state?(todo) diff --git a/app/helpers/tracking_helper.rb b/app/helpers/tracking_helper.rb index 51ea79d1ddd..221d9692661 100644 --- a/app/helpers/tracking_helper.rb +++ b/app/helpers/tracking_helper.rb @@ -2,6 +2,21 @@ module TrackingHelper def tracking_attrs(label, event, property) - {} # CE has no tracking features + return {} unless tracking_enabled? + + { + data: { + track_label: label, + track_event: event, + track_property: property + } + } + end + + private + + def tracking_enabled? + Rails.env.production? && + ::Gitlab::CurrentSettings.snowplow_enabled? end end diff --git a/app/mailers/emails/issues.rb b/app/mailers/emails/issues.rb index f3a3203f7ad..0bba2a8bf24 100644 --- a/app/mailers/emails/issues.rb +++ b/app/mailers/emails/issues.rb @@ -34,6 +34,8 @@ module Emails setup_issue_mail(issue_id, recipient_id, closed_via: closed_via) @updated_by = User.find(updated_by_user_id) + @recipient = User.find(recipient_id) + mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id, reason)) end diff --git a/app/mailers/emails/remote_mirrors.rb b/app/mailers/emails/remote_mirrors.rb index 2d8137843ec..f3938a052b0 100644 --- a/app/mailers/emails/remote_mirrors.rb +++ b/app/mailers/emails/remote_mirrors.rb @@ -3,7 +3,7 @@ module Emails module RemoteMirrors def remote_mirror_update_failed_email(remote_mirror_id, recipient_id) - @remote_mirror = RemoteMirrorFinder.new(id: remote_mirror_id).execute + @remote_mirror = RemoteMirror.find_by_id(remote_mirror_id) @project = @remote_mirror.project mail(to: recipient(recipient_id, @project.group), subject: subject('Remote mirror update failed')) diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb index 5d292094a05..3683f2ea9a9 100644 --- a/app/mailers/notify.rb +++ b/app/mailers/notify.rb @@ -125,9 +125,8 @@ class Notify < BaseMailer def mail_thread(model, headers = {}) add_project_headers add_unsubscription_headers_and_links + add_model_headers(model) - headers["X-GitLab-#{model.class.name}-ID"] = model.id - headers["X-GitLab-#{model.class.name}-IID"] = model.iid if model.respond_to?(:iid) headers['X-GitLab-Reply-Key'] = reply_key @reason = headers['X-GitLab-NotificationReason'] @@ -196,6 +195,18 @@ class Notify < BaseMailer @reply_key ||= SentNotification.reply_key end + # This method applies threading headers to the email to identify + # the instance we are discussing. + # + # All model instances must have `#id`, and may implement `#iid`. + def add_model_headers(object) + # Use replacement so we don't strip the module. + prefix = "X-GitLab-#{object.class.name.gsub(/::/, '-')}" + + headers["#{prefix}-ID"] = object.id + headers["#{prefix}-IID"] = object.iid if object.respond_to?(:iid) + end + def add_project_headers return unless @project diff --git a/app/models/analytics/cycle_analytics.rb b/app/models/analytics/cycle_analytics.rb new file mode 100644 index 00000000000..626fc91cc41 --- /dev/null +++ b/app/models/analytics/cycle_analytics.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module Analytics + module CycleAnalytics + def self.table_name_prefix + 'analytics_cycle_analytics_' + end + end +end diff --git a/app/models/analytics/cycle_analytics/project_stage.rb b/app/models/analytics/cycle_analytics/project_stage.rb new file mode 100644 index 00000000000..a312bd24e78 --- /dev/null +++ b/app/models/analytics/cycle_analytics/project_stage.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module Analytics + module CycleAnalytics + class ProjectStage < ApplicationRecord + include Analytics::CycleAnalytics::Stage + + validates :project, presence: true + belongs_to :project + + alias_attribute :parent, :project + end + end +end diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index cb6346421ec..2a99c6e5c59 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -99,6 +99,11 @@ class ApplicationSetting < ApplicationRecord presence: true, if: :plantuml_enabled + validates :snowplow_collector_hostname, + presence: true, + hostname: true, + if: :snowplow_enabled + validates :max_attachment_size, presence: true, numericality: { only_integer: true, greater_than: 0 } diff --git a/app/models/application_setting_implementation.rb b/app/models/application_setting_implementation.rb index b7a4d7aa803..55ac1e129cf 100644 --- a/app/models/application_setting_implementation.rb +++ b/app/models/application_setting_implementation.rb @@ -97,6 +97,10 @@ module ApplicationSettingImplementation usage_stats_set_by_user_id: nil, diff_max_patch_bytes: Gitlab::Git::Diff::DEFAULT_MAX_PATCH_BYTES, commit_email_hostname: default_commit_email_hostname, + snowplow_collector_hostname: nil, + snowplow_cookie_domain: nil, + snowplow_enabled: false, + snowplow_site_id: nil, protected_ci_variables: false, local_markdown_version: 0, outbound_local_requests_whitelist: [], diff --git a/app/models/award_emoji.rb b/app/models/award_emoji.rb index e26162f6151..0ab302a0f3e 100644 --- a/app/models/award_emoji.rb +++ b/app/models/award_emoji.rb @@ -16,8 +16,10 @@ class AwardEmoji < ApplicationRecord participant :user - scope :downvotes, -> { where(name: DOWNVOTE_NAME) } - scope :upvotes, -> { where(name: UPVOTE_NAME) } + scope :downvotes, -> { named(DOWNVOTE_NAME) } + scope :upvotes, -> { named(UPVOTE_NAME) } + scope :named, -> (names) { where(name: names) } + scope :awarded_by, -> (users) { where(user: users) } after_save :expire_etag_cache after_destroy :expire_etag_cache diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index ac88d9714ac..7930bef5cf2 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -121,6 +121,8 @@ module Ci scope :scheduled_actions, ->() { where(when: :delayed, status: COMPLETED_STATUSES + %i[scheduled]) } scope :ref_protected, -> { where(protected: true) } scope :with_live_trace, -> { where('EXISTS (?)', Ci::BuildTraceChunk.where('ci_builds.id = ci_build_trace_chunks.build_id').select(1)) } + scope :with_stale_live_trace, -> { with_live_trace.finished_before(12.hours.ago) } + scope :finished_before, -> (date) { finished.where('finished_at < ?', date) } scope :matches_tag_ids, -> (tag_ids) do matcher = ::ActsAsTaggableOn::Tagging @@ -384,7 +386,7 @@ module Ci return unless has_environment? strong_memoize(:expanded_environment_name) do - ExpandVariables.expand(environment, simple_variables) + ExpandVariables.expand(environment, -> { simple_variables }) end end @@ -716,7 +718,7 @@ module Ci depended_jobs = depends_on_builds # find all jobs that are needed - if Feature.enabled?(:ci_dag_support, project) && needs.exists? + if Feature.enabled?(:ci_dag_support, project, default_enabled: true) && needs.exists? depended_jobs = depended_jobs.where(name: needs.select(:name)) end diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 3b28eb246db..64e372878e6 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -203,6 +203,7 @@ module Ci scope :for_sha, -> (sha) { where(sha: sha) } scope :for_source_sha, -> (source_sha) { where(source_sha: source_sha) } scope :for_sha_or_source_sha, -> (sha) { for_sha(sha).or(for_source_sha(sha)) } + scope :created_after, -> (time) { where('ci_pipelines.created_at > ?', time) } scope :triggered_by_merge_request, -> (merge_request) do where(source: :merge_request_event, merge_request: merge_request) @@ -328,6 +329,10 @@ module Ci config_sources.values_at(:repository_source, :auto_devops_source, :unknown_source) end + def self.bridgeable_statuses + ::Ci::Pipeline::AVAILABLE_STATUSES - %w[created preparing pending] + end + def stages_count statuses.select(:stage).distinct.count end diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb index 43ff874ac23..1c1c7a5ae7a 100644 --- a/app/models/ci/runner.rb +++ b/app/models/ci/runner.rb @@ -23,13 +23,17 @@ module Ci project_type: 3 } - RUNNER_QUEUE_EXPIRY_TIME = 60.minutes ONLINE_CONTACT_TIMEOUT = 1.hour - UPDATE_DB_RUNNER_INFO_EVERY = 40.minutes + RUNNER_QUEUE_EXPIRY_TIME = 1.hour + + # This needs to be less than `ONLINE_CONTACT_TIMEOUT` + UPDATE_CONTACT_COLUMN_EVERY = (40.minutes..55.minutes).freeze + AVAILABLE_TYPES_LEGACY = %w[specific shared].freeze AVAILABLE_TYPES = runner_types.keys.freeze AVAILABLE_STATUSES = %w[active paused online offline].freeze AVAILABLE_SCOPES = (AVAILABLE_TYPES_LEGACY + AVAILABLE_TYPES + AVAILABLE_STATUSES).freeze + FORM_EDITABLE = %i[description tag_list active run_untagged locked access_level maximum_timeout_human_readable].freeze ignore_column :is_shared @@ -46,7 +50,7 @@ module Ci scope :active, -> { where(active: true) } scope :paused, -> { where(active: false) } - scope :online, -> { where('contacted_at > ?', contact_time_deadline) } + scope :online, -> { where('contacted_at > ?', online_contact_time_deadline) } # The following query using negation is cheaper than using `contacted_at <= ?` # because there are less runners online than have been created. The # resulting query is quickly finding online ones and then uses the regular @@ -56,6 +60,8 @@ module Ci scope :offline, -> { where.not(id: online) } scope :ordered, -> { order(id: :desc) } + scope :with_recent_runner_queue, -> { where('contacted_at > ?', recent_queue_deadline) } + # BACKWARD COMPATIBILITY: There are needed to maintain compatibility with `AVAILABLE_SCOPES` used by `lib/api/runners.rb` scope :deprecated_shared, -> { instance_type } scope :deprecated_specific, -> { project_type.or(group_type) } @@ -137,10 +143,18 @@ module Ci fuzzy_search(query, [:token, :description]) end - def self.contact_time_deadline + def self.online_contact_time_deadline ONLINE_CONTACT_TIMEOUT.ago end + def self.recent_queue_deadline + # we add queue expiry + online + # - contacted_at can be updated at any time within this interval + # we have always accurate `contacted_at` but it is stored in Redis + # and not persisted in database + (ONLINE_CONTACT_TIMEOUT + RUNNER_QUEUE_EXPIRY_TIME).ago + end + def self.order_by(order) if order == 'contacted_asc' order_contacted_at_asc @@ -174,7 +188,7 @@ module Ci end def online? - contacted_at && contacted_at > self.class.contact_time_deadline + contacted_at && contacted_at > self.class.online_contact_time_deadline end def status @@ -275,9 +289,7 @@ module Ci def persist_cached_data? # Use a random threshold to prevent beating DB updates. - # It generates a distribution between [40m, 80m]. - - contacted_at_max_age = UPDATE_DB_RUNNER_INFO_EVERY + Random.rand(UPDATE_DB_RUNNER_INFO_EVERY) + contacted_at_max_age = Random.rand(UPDATE_CONTACT_COLUMN_EVERY) real_contacted_at = read_attribute(:contacted_at) real_contacted_at.nil? || diff --git a/app/models/clusters/applications/cert_manager.rb b/app/models/clusters/applications/cert_manager.rb index 2fc1b67dfd2..6bd7473c8ff 100644 --- a/app/models/clusters/applications/cert_manager.rb +++ b/app/models/clusters/applications/cert_manager.rb @@ -64,11 +64,15 @@ module Clusters end def delete_private_key - "kubectl delete secret -n #{Gitlab::Kubernetes::Helm::NAMESPACE} #{private_key_name} --ignore-not-found" if private_key_name.present? + return unless private_key_name.present? + + args = %W(secret -n #{Gitlab::Kubernetes::Helm::NAMESPACE} #{private_key_name} --ignore-not-found) + + Gitlab::Kubernetes::KubectlCmd.delete(*args) end def delete_crd(definition) - "kubectl delete crd #{definition} --ignore-not-found" + Gitlab::Kubernetes::KubectlCmd.delete("crd", definition, "--ignore-not-found") end def cluster_issuer_file diff --git a/app/models/clusters/applications/helm.rb b/app/models/clusters/applications/helm.rb index 3a175fec148..455cf200fbc 100644 --- a/app/models/clusters/applications/helm.rb +++ b/app/models/clusters/applications/helm.rb @@ -41,7 +41,7 @@ module Clusters extra_apps = Clusters::Applications::Helm.where('EXISTS (?)', klass.select(1).where(cluster_id: cluster_id)) - applications = applications.present? ? applications.or(extra_apps) : extra_apps + applications = applications ? applications.or(extra_apps) : extra_apps end !applications.exists? diff --git a/app/models/clusters/applications/knative.rb b/app/models/clusters/applications/knative.rb index 5eae23659ae..244fe738396 100644 --- a/app/models/clusters/applications/knative.rb +++ b/app/models/clusters/applications/knative.rb @@ -89,7 +89,7 @@ module Clusters def delete_knative_services cluster.kubernetes_namespaces.map do |kubernetes_namespace| - "kubectl delete ksvc --all -n #{kubernetes_namespace.namespace}" + Gitlab::Kubernetes::KubectlCmd.delete("ksvc", "--all", "-n", kubernetes_namespace.namespace) end end @@ -99,14 +99,14 @@ module Clusters def delete_knative_namespaces [ - "kubectl delete --ignore-not-found ns knative-serving", - "kubectl delete --ignore-not-found ns knative-build" + Gitlab::Kubernetes::KubectlCmd.delete("--ignore-not-found", "ns", "knative-serving"), + Gitlab::Kubernetes::KubectlCmd.delete("--ignore-not-found", "ns", "knative-build") ] end def delete_knative_and_istio_crds api_resources.map do |crd| - "kubectl delete --ignore-not-found crd #{crd}" + Gitlab::Kubernetes::KubectlCmd.delete("--ignore-not-found", "crd", "#{crd}") end end @@ -119,13 +119,13 @@ module Clusters def install_knative_metrics return [] unless cluster.application_prometheus_available? - ["kubectl apply -f #{METRICS_CONFIG}"] + [Gitlab::Kubernetes::KubectlCmd.apply_file(METRICS_CONFIG)] end def delete_knative_istio_metrics return [] unless cluster.application_prometheus_available? - ["kubectl delete --ignore-not-found -f #{METRICS_CONFIG}"] + [Gitlab::Kubernetes::KubectlCmd.delete("--ignore-not-found", "-f", METRICS_CONFIG)] end def verify_cluster? diff --git a/app/models/clusters/applications/prometheus.rb b/app/models/clusters/applications/prometheus.rb index 08e52f32bb3..f31a6b8b50e 100644 --- a/app/models/clusters/applications/prometheus.rb +++ b/app/models/clusters/applications/prometheus.rb @@ -106,13 +106,13 @@ module Clusters def install_knative_metrics return [] unless cluster.application_knative_available? - ["kubectl apply -f #{Clusters::Applications::Knative::METRICS_CONFIG}"] + [Gitlab::Kubernetes::KubectlCmd.apply_file(Clusters::Applications::Knative::METRICS_CONFIG)] end def delete_knative_istio_metrics return [] unless cluster.application_knative_available? - ["kubectl delete -f #{Clusters::Applications::Knative::METRICS_CONFIG}"] + [Gitlab::Kubernetes::KubectlCmd.delete("-f", Clusters::Applications::Knative::METRICS_CONFIG)] end end end diff --git a/app/models/clusters/clusters_hierarchy.rb b/app/models/clusters/clusters_hierarchy.rb index dab034b7234..5556fc8d3f0 100644 --- a/app/models/clusters/clusters_hierarchy.rb +++ b/app/models/clusters/clusters_hierarchy.rb @@ -46,7 +46,7 @@ module Clusters def group_clusters_base_query group_parent_id_alias = alias_as_column(groups[:parent_id], 'group_parent_id') - join_sources = ::Group.left_joins(:clusters).join_sources + join_sources = ::Group.left_joins(:clusters).arel.join_sources model .unscoped @@ -59,7 +59,7 @@ module Clusters def project_clusters_base_query projects = ::Project.arel_table project_parent_id_alias = alias_as_column(projects[:namespace_id], 'group_parent_id') - join_sources = ::Project.left_joins(:clusters).join_sources + join_sources = ::Project.left_joins(:clusters).arel.join_sources model .unscoped diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index a88cac6b8e6..4be4d95b4a1 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -40,8 +40,11 @@ class CommitStatus < ApplicationRecord scope :ordered, -> { order(:name) } scope :latest_ordered, -> { latest.ordered.includes(project: :namespace) } scope :retried_ordered, -> { retried.ordered.includes(project: :namespace) } + scope :before_stage, -> (index) { where('stage_idx < ?', index) } + scope :for_stage, -> (index) { where(stage_idx: index) } scope :after_stage, -> (index) { where('stage_idx > ?', index) } scope :processables, -> { where(type: %w[Ci::Build Ci::Bridge]) } + scope :for_ids, -> (ids) { where(id: ids) } scope :with_needs, -> (names = nil) do needs = Ci::BuildNeed.scoped_build.select(1) @@ -49,8 +52,10 @@ class CommitStatus < ApplicationRecord where('EXISTS (?)', needs).preload(:needs) end - scope :without_needs, -> do - where('NOT EXISTS (?)', Ci::BuildNeed.scoped_build.select(1)) + scope :without_needs, -> (names = nil) do + needs = Ci::BuildNeed.scoped_build.select(1) + needs = needs.where(name: names) if names + where('NOT EXISTS (?)', needs) end # We use `CommitStatusEnums.failure_reasons` here so that EE can more easily @@ -149,6 +154,18 @@ class CommitStatus < ApplicationRecord end end + def self.names + select(:name) + end + + def self.status_for_prior_stages(index) + before_stage(index).latest.status || 'success' + end + + def self.status_for_names(names) + where(name: names).latest.status || 'success' + end + def locking_enabled? will_save_change_to_status? end diff --git a/app/models/concerns/analytics/cycle_analytics/stage.rb b/app/models/concerns/analytics/cycle_analytics/stage.rb new file mode 100644 index 00000000000..0c603c2d5e6 --- /dev/null +++ b/app/models/concerns/analytics/cycle_analytics/stage.rb @@ -0,0 +1,68 @@ +# frozen_string_literal: true + +module Analytics + module CycleAnalytics + module Stage + extend ActiveSupport::Concern + + included do + validates :name, presence: true + validates :start_event_identifier, presence: true + validates :end_event_identifier, presence: true + validate :validate_stage_event_pairs + + enum start_event_identifier: Gitlab::Analytics::CycleAnalytics::StageEvents.to_enum, _prefix: :start_event_identifier + enum end_event_identifier: Gitlab::Analytics::CycleAnalytics::StageEvents.to_enum, _prefix: :end_event_identifier + + alias_attribute :custom_stage?, :custom + end + + def parent=(_) + raise NotImplementedError + end + + def parent + raise NotImplementedError + end + + def start_event + Gitlab::Analytics::CycleAnalytics::StageEvents[start_event_identifier].new(params_for_start_event) + end + + def end_event + Gitlab::Analytics::CycleAnalytics::StageEvents[end_event_identifier].new(params_for_end_event) + end + + def params_for_start_event + {} + end + + def params_for_end_event + {} + end + + def default_stage? + !custom + end + + # The model that is going to be queried, Issue or MergeRequest + def subject_model + start_event.object_type + end + + private + + def validate_stage_event_pairs + return if start_event_identifier.nil? || end_event_identifier.nil? + + unless pairing_rules.fetch(start_event.class, []).include?(end_event.class) + errors.add(:end_event, :not_allowed_for_the_given_start_event) + end + end + + def pairing_rules + Gitlab::Analytics::CycleAnalytics::StageEvents.pairing_rules + end + end + end +end diff --git a/app/models/concerns/awardable.rb b/app/models/concerns/awardable.rb index 14bc56f0eee..f229b42ade6 100644 --- a/app/models/concerns/awardable.rb +++ b/app/models/concerns/awardable.rb @@ -106,30 +106,6 @@ module Awardable end def awarded_emoji?(emoji_name, current_user) - award_emoji.where(name: emoji_name, user: current_user).exists? - end - - def create_award_emoji(name, current_user) - return unless emoji_awardable? - - award_emoji.create(name: normalize_name(name), user: current_user) - end - - def remove_award_emoji(name, current_user) - award_emoji.where(name: name, user: current_user).destroy_all # rubocop: disable DestroyAll - end - - def toggle_award_emoji(emoji_name, current_user) - if awarded_emoji?(emoji_name, current_user) - remove_award_emoji(emoji_name, current_user) - else - create_award_emoji(emoji_name, current_user) - end - end - - private - - def normalize_name(name) - Gitlab::Emoji.normalize_emoji_name(name) + award_emoji.named(emoji_name).awarded_by(current_user).exists? end end diff --git a/app/models/concerns/has_status.rb b/app/models/concerns/has_status.rb index 27a5c3d5286..71ebb586c13 100644 --- a/app/models/concerns/has_status.rb +++ b/app/models/concerns/has_status.rb @@ -106,10 +106,15 @@ module HasStatus scope :running_or_pending, -> { with_status(:running, :pending) } scope :finished, -> { with_status(:success, :failed, :canceled) } scope :failed_or_canceled, -> { with_status(:failed, :canceled) } + scope :incomplete, -> { without_statuses(completed_statuses) } scope :cancelable, -> do where(status: [:running, :preparing, :pending, :created, :scheduled]) end + + scope :without_statuses, -> (names) do + with_status(all_state_names - names.to_a) + end end def started? diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index e60b6497cb7..db46d7afbb9 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -186,16 +186,15 @@ module Issuable def sort_by_attribute(method, excluded_labels: []) sorted = case method.to_s - when 'downvotes_desc' then order_downvotes_desc - when 'label_priority' then order_labels_priority(excluded_labels: excluded_labels) - when 'label_priority_desc' then order_labels_priority('DESC', excluded_labels: excluded_labels) - when 'milestone', 'milestone_due_asc' then order_milestone_due_asc - when 'milestone_due_desc' then order_milestone_due_desc - when 'popularity', 'popularity_desc' then order_upvotes_desc - when 'popularity_asc' then order_upvotes_asc - when 'priority', 'priority_asc' then order_due_date_and_labels_priority(excluded_labels: excluded_labels) - when 'priority_desc' then order_due_date_and_labels_priority('DESC', excluded_labels: excluded_labels) - when 'upvotes_desc' then order_upvotes_desc + when 'downvotes_desc' then order_downvotes_desc + when 'label_priority', 'label_priority_asc' then order_labels_priority(excluded_labels: excluded_labels) + when 'label_priority_desc' then order_labels_priority('DESC', excluded_labels: excluded_labels) + when 'milestone', 'milestone_due_asc' then order_milestone_due_asc + when 'milestone_due_desc' then order_milestone_due_desc + when 'popularity_asc' then order_upvotes_asc + when 'popularity', 'popularity_desc', 'upvotes_desc' then order_upvotes_desc + when 'priority', 'priority_asc' then order_due_date_and_labels_priority(excluded_labels: excluded_labels) + when 'priority_desc' then order_due_date_and_labels_priority('DESC', excluded_labels: excluded_labels) else order_by(method) end diff --git a/app/models/concerns/sortable.rb b/app/models/concerns/sortable.rb index df1a9e3fe6e..c4af1b1fab2 100644 --- a/app/models/concerns/sortable.rb +++ b/app/models/concerns/sortable.rb @@ -27,14 +27,18 @@ module Sortable def simple_sorts { 'created_asc' => -> { order_created_asc }, + 'created_at_asc' => -> { order_created_asc }, 'created_date' => -> { order_created_desc }, 'created_desc' => -> { order_created_desc }, + 'created_at_desc' => -> { order_created_desc }, 'id_asc' => -> { order_id_asc }, 'id_desc' => -> { order_id_desc }, 'name_asc' => -> { order_name_asc }, 'name_desc' => -> { order_name_desc }, 'updated_asc' => -> { order_updated_asc }, - 'updated_desc' => -> { order_updated_desc } + 'updated_at_asc' => -> { order_updated_asc }, + 'updated_desc' => -> { order_updated_desc }, + 'updated_at_desc' => -> { order_updated_desc } } end diff --git a/app/models/deployment.rb b/app/models/deployment.rb index 68586e7a1fd..bff5d348ca0 100644 --- a/app/models/deployment.rb +++ b/app/models/deployment.rb @@ -162,6 +162,14 @@ class Deployment < ApplicationRecord deployed_at&.to_time&.in_time_zone&.to_s(:medium) end + def deployed_by + # We use deployable's user if available because Ci::PlayBuildService + # does not update the deployment's user, just the one for the deployable. + # TODO: use deployment's user once https://gitlab.com/gitlab-org/gitlab-ce/issues/66442 + # is completed. + deployable&.user || user + end + private def ref_path diff --git a/app/models/issue.rb b/app/models/issue.rb index c5a18f0af0f..caea8eadd18 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -128,11 +128,10 @@ class Issue < ApplicationRecord def self.sort_by_attribute(method, excluded_labels: []) case method.to_s - when 'closest_future_date' then order_closest_future_date - when 'due_date' then order_due_date_asc - when 'due_date_asc' then order_due_date_asc - when 'due_date_desc' then order_due_date_desc - when 'relative_position' then order_relative_position_asc.with_order_id_desc + when 'closest_future_date', 'closest_future_date_asc' then order_closest_future_date + when 'due_date', 'due_date_asc' then order_due_date_asc + when 'due_date_desc' then order_due_date_desc + when 'relative_position', 'relative_position_asc' then order_relative_position_asc.with_order_id_desc else super end diff --git a/app/models/label.rb b/app/models/label.rb index 25de26b8384..d9455b36242 100644 --- a/app/models/label.rb +++ b/app/models/label.rb @@ -138,6 +138,8 @@ class Label < ApplicationRecord end def self.on_project_board?(project_id, label_id) + return false if label_id.blank? + on_project_boards(project_id).where(id: label_id).exists? end diff --git a/app/models/members/group_member.rb b/app/models/members/group_member.rb index f6b19317c50..3d6f397e599 100644 --- a/app/models/members/group_member.rb +++ b/app/models/members/group_member.rb @@ -15,8 +15,8 @@ class GroupMember < Member default_scope { where(source_type: SOURCE_TYPE) } scope :of_groups, ->(groups) { where(source_id: groups.select(:id)) } - scope :count_users_by_group_id, -> { joins(:user).group(:source_id).count } + scope :of_ldap_type, -> { where(ldap: true) } after_create :update_two_factor_requirement, unless: :invite? after_destroy :update_two_factor_requirement, unless: :invite? diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 4306dd9266f..bfd636fa62a 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -220,18 +220,7 @@ class MergeRequest < ApplicationRecord end def rebase_in_progress? - (rebase_jid.present? && Gitlab::SidekiqStatus.running?(rebase_jid)) || - gitaly_rebase_in_progress? - end - - # TODO: remove the Gitaly lookup after v12.1, when rebase_jid will be reliable - def gitaly_rebase_in_progress? - strong_memoize(:gitaly_rebase_in_progress) do - # The source project can be deleted - next false unless source_project - - source_project.repository.rebase_in_progress?(id) - end + rebase_jid.present? && Gitlab::SidekiqStatus.running?(rebase_jid) end # Use this method whenever you need to make sure the head_pipeline is synced with the diff --git a/app/models/namespace.rb b/app/models/namespace.rb index 058350b16ce..9f9c4288667 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -172,6 +172,13 @@ class Namespace < ApplicationRecord end end + # any ancestor can disable emails for all descendants + def emails_disabled? + strong_memoize(:emails_disabled) do + Feature.enabled?(:emails_disabled, self, default_enabled: true) && self_and_ancestors.where(emails_disabled: true).exists? + end + end + def lfs_enabled? # User namespace will always default to the global setting Gitlab.config.lfs.enabled diff --git a/app/models/namespace/root_storage_statistics.rb b/app/models/namespace/root_storage_statistics.rb index 56c430013ee..ae9b2f14343 100644 --- a/app/models/namespace/root_storage_statistics.rb +++ b/app/models/namespace/root_storage_statistics.rb @@ -8,6 +8,8 @@ class Namespace::RootStorageStatistics < ApplicationRecord belongs_to :namespace has_one :route, through: :namespace + scope :for_namespace_ids, ->(namespace_ids) { where(namespace_id: namespace_ids) } + delegate :all_projects, to: :namespace def recalculate! diff --git a/app/models/notification_recipient.rb b/app/models/notification_recipient.rb index a7f73c0f29c..8e44e3d8e17 100644 --- a/app/models/notification_recipient.rb +++ b/app/models/notification_recipient.rb @@ -4,6 +4,7 @@ class NotificationRecipient include Gitlab::Utils::StrongMemoize attr_reader :user, :type, :reason + def initialize(user, type, **opts) unless NotificationSetting.levels.key?(type) || type == :subscription raise ArgumentError, "invalid type: #{type.inspect}" @@ -30,6 +31,7 @@ class NotificationRecipient def notifiable? return false unless has_access? + return false if emails_disabled? return false if own_activity? # even users with :disabled notifications receive manual subscriptions @@ -109,6 +111,12 @@ class NotificationRecipient private + # They are disabled if the project or group has disallowed it. + # No need to check the group if there is already a project + def emails_disabled? + @project ? @project.emails_disabled? : @group&.emails_disabled? + end + def read_ability return if @skip_read_ability return @read_ability if instance_variable_defined?(:@read_ability) diff --git a/app/models/project.rb b/app/models/project.rb index a6e43efa1f3..10679fb1f85 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -55,6 +55,8 @@ class Project < ApplicationRecord VALID_MIRROR_PORTS = [22, 80, 443].freeze VALID_MIRROR_PROTOCOLS = %w(http https ssh git).freeze + SORTING_PREFERENCE_FIELD = :projects_sort + cache_markdown_field :description, pipeline: :description delegate :feature_available?, :builds_enabled?, :wiki_enabled?, @@ -283,6 +285,7 @@ class Project < ApplicationRecord has_one :ci_cd_settings, class_name: 'ProjectCiCdSetting', inverse_of: :project, autosave: true, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_many :remote_mirrors, inverse_of: :project + has_many :cycle_analytics_stages, class_name: 'Analytics::CycleAnalytics::ProjectStage' accepts_nested_attributes_for :variables, allow_destroy: true accepts_nested_attributes_for :project_feature, update_only: true @@ -631,6 +634,13 @@ class Project < ApplicationRecord alias_method :ancestors, :ancestors_upto + def emails_disabled? + strong_memoize(:emails_disabled) do + # disabling in the namespace overrides the project setting + Feature.enabled?(:emails_disabled, self, default_enabled: true) && (super || namespace.emails_disabled?) + end + end + def lfs_enabled? return namespace.lfs_enabled? if self[:lfs_enabled].nil? @@ -1230,6 +1240,14 @@ class Project < ApplicationRecord end end + def has_active_hooks?(hooks_scope = :push_hooks) + hooks.hooks_for(hooks_scope).any? || SystemHook.hooks_for(hooks_scope).any? + end + + def has_active_services?(hooks_scope = :push_hooks) + services.public_send(hooks_scope).any? # rubocop:disable GitlabSecurity/PublicSend + end + def valid_repo? repository.exists? rescue diff --git a/app/models/project_services/chat_message/pipeline_message.rb b/app/models/project_services/chat_message/pipeline_message.rb index 4edf263433f..a3793d9937b 100644 --- a/app/models/project_services/chat_message/pipeline_message.rb +++ b/app/models/project_services/chat_message/pipeline_message.rb @@ -68,7 +68,7 @@ module ChatMessage title_link: pipeline_url, fields: attachments_fields, footer: project.name, - footer_icon: project.avatar_url, + footer_icon: project.avatar_url(only_path: false), ts: finished_at }] end diff --git a/app/models/project_services/emails_on_push_service.rb b/app/models/project_services/emails_on_push_service.rb index 45de64a9990..8ca40138a8f 100644 --- a/app/models/project_services/emails_on_push_service.rb +++ b/app/models/project_services/emails_on_push_service.rb @@ -24,6 +24,7 @@ class EmailsOnPushService < Service def execute(push_data) return unless supported_events.include?(push_data[:object_kind]) + return if project.emails_disabled? EmailsOnPushWorker.perform_async( project_id, diff --git a/app/models/project_services/slash_commands_service.rb b/app/models/project_services/slash_commands_service.rb index 5f5cff97808..cb16ad75d14 100644 --- a/app/models/project_services/slash_commands_service.rb +++ b/app/models/project_services/slash_commands_service.rb @@ -35,6 +35,8 @@ class SlashCommandsService < Service chat_user = find_chat_user(params) if chat_user&.user + return Gitlab::SlashCommands::Presenters::Access.new.access_denied unless chat_user.user.can?(:use_slash_commands) + Gitlab::SlashCommands::Command.new(project, chat_user, params).execute else url = authorize_chat_name_url(params) diff --git a/app/models/project_wiki.rb b/app/models/project_wiki.rb index c91add6439f..4a19e05bf76 100644 --- a/app/models/project_wiki.rb +++ b/app/models/project_wiki.rb @@ -85,6 +85,10 @@ class ProjectWiki list_pages(limit: 1).empty? end + def exists? + !empty? + end + # Lists wiki pages of the repository. # # limit - max number of pages returned by the method. diff --git a/app/models/remote_mirror.rb b/app/models/remote_mirror.rb index 6b5605f9999..c9ee0653d86 100644 --- a/app/models/remote_mirror.rb +++ b/app/models/remote_mirror.rb @@ -4,6 +4,8 @@ class RemoteMirror < ApplicationRecord include AfterCommitQueue include MirrorAuthentication + MAX_FIRST_RUNTIME = 3.hours + MAX_INCREMENTAL_RUNTIME = 1.hour PROTECTED_BACKOFF_DELAY = 1.minute UNPROTECTED_BACKOFF_DELAY = 5.minutes @@ -31,11 +33,18 @@ class RemoteMirror < ApplicationRecord scope :enabled, -> { where(enabled: true) } scope :started, -> { with_update_status(:started) } - scope :stuck, -> { started.where('last_update_at < ? OR (last_update_at IS NULL AND updated_at < ?)', 1.hour.ago, 3.hours.ago) } + + scope :stuck, -> do + started + .where('(last_update_started_at < ? AND last_update_at IS NOT NULL)', + MAX_INCREMENTAL_RUNTIME.ago) + .or(where('(last_update_started_at < ? AND last_update_at IS NULL)', + MAX_FIRST_RUNTIME.ago)) + end state_machine :update_status, initial: :none do event :update_start do - transition [:none, :finished, :failed] => :started + transition any => :started end event :update_finish do @@ -46,9 +55,14 @@ class RemoteMirror < ApplicationRecord transition started: :failed end + event :update_retry do + transition started: :to_retry + end + state :started state :finished state :failed + state :to_retry after_transition any => :started do |remote_mirror, _| Gitlab::Metrics.add_event(:remote_mirrors_running) @@ -138,16 +152,27 @@ class RemoteMirror < ApplicationRecord end def updated_since?(timestamp) - last_update_started_at && last_update_started_at > timestamp && !update_failed? + return false if failed? + + last_update_started_at && last_update_started_at > timestamp end def mark_for_delete_if_blank_url mark_for_destruction if url.blank? end - def mark_as_failed(error_message) - update_column(:last_error, Gitlab::UrlSanitizer.sanitize(error_message)) - update_fail + def update_error_message(error_message) + self.last_error = Gitlab::UrlSanitizer.sanitize(error_message) + end + + def mark_for_retry!(error_message) + update_error_message(error_message) + update_retry! + end + + def mark_as_failed!(error_message) + update_error_message(error_message) + update_fail! end def url=(value) @@ -190,6 +215,18 @@ class RemoteMirror < ApplicationRecord update_column(:error_notification_sent, true) end + def backoff_delay + if self.only_protected_branches + PROTECTED_BACKOFF_DELAY + else + UNPROTECTED_BACKOFF_DELAY + end + end + + def max_runtime + last_update_at.present? ? MAX_INCREMENTAL_RUNTIME : MAX_FIRST_RUNTIME + end + private def store_credentials @@ -219,14 +256,6 @@ class RemoteMirror < ApplicationRecord self.last_update_started_at >= Time.now - backoff_delay end - def backoff_delay - if self.only_protected_branches - PROTECTED_BACKOFF_DELAY - else - UNPROTECTED_BACKOFF_DELAY - end - end - def reset_fields update_columns( last_error: nil, diff --git a/app/models/repository.rb b/app/models/repository.rb index a89f573e3d6..b957b9b0bdd 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -239,13 +239,13 @@ class Repository def branch_exists?(branch_name) return false unless raw_repository - branch_names.include?(branch_name) + branch_names_include?(branch_name) end def tag_exists?(tag_name) return false unless raw_repository - tag_names.include?(tag_name) + tag_names_include?(tag_name) end def ref_exists?(ref) @@ -389,11 +389,15 @@ class Repository expire_statistics_caches end - # Runs code after a repository has been created. - def after_create + def expire_status_cache expire_exists_cache expire_root_ref_cache expire_emptiness_caches + end + + # Runs code after a repository has been created. + def after_create + expire_status_cache repository_event(:create_repository) end @@ -418,25 +422,29 @@ class Repository end # Runs code before pushing (= creating or removing) a tag. + # + # Note that this doesn't expire the tags. You may need to call + # expire_caches_for_tags or expire_tags_cache. def before_push_tag + repository_event(:push_tag) + end + + def expire_caches_for_tags expire_statistics_caches expire_emptiness_caches expire_tags_cache - - repository_event(:push_tag) end # Runs code before removing a tag. def before_remove_tag - expire_tags_cache - expire_statistics_caches + expire_caches_for_tags repository_event(:remove_tag) end # Runs code after removing a tag. def after_remove_tag - expire_tags_cache + expire_caches_for_tags end # Runs code after the HEAD of a repository is changed. @@ -460,8 +468,8 @@ class Repository end # Runs code after a new branch has been created. - def after_create_branch - expire_branches_cache + def after_create_branch(expire_cache: true) + expire_branches_cache if expire_cache repository_event(:push_branch) end @@ -474,8 +482,8 @@ class Repository end # Runs code after an existing branch has been removed. - def after_remove_branch - expire_branches_cache + def after_remove_branch(expire_cache: true) + expire_branches_cache if expire_cache end def method_missing(msg, *args, &block) @@ -557,10 +565,10 @@ class Repository end delegate :branch_names, to: :raw_repository - cache_method :branch_names, fallback: [] + cache_method_as_redis_set :branch_names, fallback: [] delegate :tag_names, to: :raw_repository - cache_method :tag_names, fallback: [] + cache_method_as_redis_set :tag_names, fallback: [] delegate :branch_count, :tag_count, :has_visible_content?, to: :raw_repository cache_method :branch_count, fallback: 0 @@ -1122,6 +1130,10 @@ class Repository @cache ||= Gitlab::RepositoryCache.new(self) end + def redis_set_cache + @redis_set_cache ||= Gitlab::RepositorySetCache.new(self) + end + def request_store_cache @request_store_cache ||= Gitlab::RepositoryCache.new(self, backend: Gitlab::SafeRequestStore) end diff --git a/app/models/system_note_metadata.rb b/app/models/system_note_metadata.rb index 9a2640db9ca..a19755d286a 100644 --- a/app/models/system_note_metadata.rb +++ b/app/models/system_note_metadata.rb @@ -9,7 +9,7 @@ class SystemNoteMetadata < ApplicationRecord TYPES_WITH_CROSS_REFERENCES = %w[ commit cross_reference close duplicate - moved + moved merge ].freeze ICON_TYPES = %w[ diff --git a/app/models/user.rb b/app/models/user.rb index ac83c8e3256..909f5f3873d 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -438,18 +438,20 @@ class User < ApplicationRecord order = <<~SQL CASE - WHEN users.name = %{query} THEN 0 - WHEN users.username = %{query} THEN 1 - WHEN users.email = %{query} THEN 2 + WHEN users.name = :query THEN 0 + WHEN users.username = :query THEN 1 + WHEN users.email = :query THEN 2 ELSE 3 END SQL + sanitized_order_sql = Arel.sql(sanitize_sql_array([order, query: query])) + where( fuzzy_arel_match(:name, query, lower_exact_match: true) .or(fuzzy_arel_match(:username, query, lower_exact_match: true)) .or(arel_table[:email].eq(query)) - ).reorder(order % { query: ApplicationRecord.connection.quote(query) }, :name) + ).reorder(sanitized_order_sql, :name) end # Limits the result set to users _not_ in the given query/list of IDs. @@ -641,6 +643,13 @@ class User < ApplicationRecord end end + # will_save_change_to_attribute? is used by Devise to check if it is necessary + # to clear any existing reset_password_tokens before updating an authentication_key + # and login in our case is a virtual attribute to allow login by username or email. + def will_save_change_to_login? + will_save_change_to_username? || will_save_change_to_email? + end + def unique_email if !emails.exists?(email: email) && Email.exists?(email: email) errors.add(:email, _('has already been taken')) @@ -1505,6 +1514,13 @@ class User < ApplicationRecord super end + # override from Devise::Confirmable + def confirmation_period_valid? + return false if Feature.disabled?(:soft_email_confirmation) + + super + end + private def default_private_profile_to_false diff --git a/app/models/users_star_project.rb b/app/models/users_star_project.rb index 3c7a805cc5c..c633e2d8b3d 100644 --- a/app/models/users_star_project.rb +++ b/app/models/users_star_project.rb @@ -17,6 +17,7 @@ class UsersStarProject < ApplicationRecord scope :by_project, -> (project) { where(project_id: project.id) } scope :with_visible_profile, -> (user) { joins(:user).merge(User.with_visible_profile(user)) } scope :with_public_profile, -> { joins(:user).merge(User.with_public_profile) } + scope :preload_users, -> { preload(:user) } class << self def sort_by_attribute(method) diff --git a/app/policies/global_policy.rb b/app/policies/global_policy.rb index 134de1c9ace..311aab0dcd4 100644 --- a/app/policies/global_policy.rb +++ b/app/policies/global_policy.rb @@ -33,6 +33,7 @@ class GlobalPolicy < BasePolicy enable :access_git enable :receive_notifications enable :use_quick_actions + enable :use_slash_commands end rule { blocked | internal }.policy do @@ -40,6 +41,7 @@ class GlobalPolicy < BasePolicy prevent :access_api prevent :access_git prevent :receive_notifications + prevent :use_slash_commands end rule { required_terms_not_accepted }.policy do @@ -57,6 +59,7 @@ class GlobalPolicy < BasePolicy rule { access_locked }.policy do prevent :log_in + prevent :use_slash_commands end rule { ~(anonymous & restricted_public_level) }.policy do diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb index 52c944491bf..5d2b74b17a2 100644 --- a/app/policies/group_policy.rb +++ b/app/policies/group_policy.rb @@ -92,6 +92,7 @@ class GroupPolicy < BasePolicy enable :change_visibility_level enable :set_note_created_at + enable :set_emails_disabled end rule { can?(:read_nested_project_resources) }.policy do @@ -123,6 +124,8 @@ class GroupPolicy < BasePolicy rule { developer & developer_maintainer_access }.enable :create_projects rule { create_projects_disabled }.prevent :create_projects + rule { owner | admin }.enable :read_statistics + def access_level return GroupMember::NO_ACCESS if @user.nil? diff --git a/app/policies/namespace/root_storage_statistics_policy.rb b/app/policies/namespace/root_storage_statistics_policy.rb new file mode 100644 index 00000000000..63fcaf20dfe --- /dev/null +++ b/app/policies/namespace/root_storage_statistics_policy.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class Namespace::RootStorageStatisticsPolicy < BasePolicy + delegate { @subject.namespace } +end diff --git a/app/policies/namespace_policy.rb b/app/policies/namespace_policy.rb index 2babcb0a2d9..937666c7e54 100644 --- a/app/policies/namespace_policy.rb +++ b/app/policies/namespace_policy.rb @@ -11,6 +11,7 @@ class NamespacePolicy < BasePolicy enable :create_projects enable :admin_namespace enable :read_namespace + enable :read_statistics end rule { personal_project & ~can_create_personal_project }.prevent :create_projects diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb index e79bac6bee3..b8dee1b0789 100644 --- a/app/policies/project_policy.rb +++ b/app/policies/project_policy.rb @@ -162,6 +162,7 @@ class ProjectPolicy < BasePolicy enable :set_issue_created_at enable :set_issue_updated_at enable :set_note_created_at + enable :set_emails_disabled end rule { can?(:guest_access) }.policy do diff --git a/app/presenters/blobs/unfold_presenter.rb b/app/presenters/blobs/unfold_presenter.rb index 4c6dd6895cf..a256dd05a4d 100644 --- a/app/presenters/blobs/unfold_presenter.rb +++ b/app/presenters/blobs/unfold_presenter.rb @@ -1,30 +1,34 @@ # frozen_string_literal: true -require 'gt_one_coercion' - module Blobs class UnfoldPresenter < BlobPresenter - include Virtus.model + include ActiveModel::Attributes + include ActiveModel::AttributeAssignment include Gitlab::Utils::StrongMemoize - attribute :full, Boolean, default: false - attribute :since, GtOneCoercion - attribute :to, Integer - attribute :bottom, Boolean - attribute :unfold, Boolean, default: true - attribute :offset, Integer - attribute :indent, Integer, default: 0 + attribute :full, :boolean, default: false + attribute :since, :integer, default: 1 + attribute :to, :integer, default: 1 + attribute :bottom, :boolean, default: false + attribute :unfold, :boolean, default: true + attribute :offset, :integer, default: 0 + attribute :indent, :integer, default: 0 + + alias_method :full?, :full + alias_method :bottom?, :bottom + alias_method :unfold?, :unfold def initialize(blob, params) + super(blob) + self.attributes = params + # Load all blob data first as we need to ensure they're all loaded first # so we can accurately show the rest of the diff when unfolding. load_all_blob_data - @subject = blob @all_lines = blob.data.lines - super(params) - self.attributes = prepare_attributes + handle_full_or_end! end # Returns an array of Gitlab::Diff::Line with match line added @@ -56,21 +60,18 @@ module Blobs private - def prepare_attributes - return attributes unless full? || to == -1 + def handle_full_or_end! + return unless full? || to == -1 - full_opts = { - since: 1, + self.since = 1 if full? + + self.attributes = { to: all_lines_size, bottom: false, unfold: false, offset: 0, indent: 0 } - - return full_opts if full? - - full_opts.merge(attributes.slice(:since)) end def all_lines_size diff --git a/app/serializers/deployment_entity.rb b/app/serializers/deployment_entity.rb index 943c707218d..8b967459173 100644 --- a/app/serializers/deployment_entity.rb +++ b/app/serializers/deployment_entity.rb @@ -18,9 +18,10 @@ class DeploymentEntity < Grape::Entity end expose :created_at + expose :deployed_at expose :tag expose :last? - expose :user, using: UserEntity + expose :deployed_by, as: :user, using: UserEntity expose :deployable do |deployment, opts| deployment.deployable.yield_self do |deployable| diff --git a/app/serializers/deployment_serializer.rb b/app/serializers/deployment_serializer.rb index 04db6b88489..b48037dd53f 100644 --- a/app/serializers/deployment_serializer.rb +++ b/app/serializers/deployment_serializer.rb @@ -4,7 +4,7 @@ class DeploymentSerializer < BaseSerializer entity DeploymentEntity def represent_concise(resource, opts = {}) - opts[:only] = [:iid, :id, :sha, :created_at, :tag, :last?, :id, ref: [:name]] + opts[:only] = [:iid, :id, :sha, :created_at, :deployed_at, :tag, :last?, :id, ref: [:name]] represent(resource, opts) end end diff --git a/app/serializers/issuable_sidebar_basic_entity.rb b/app/serializers/issuable_sidebar_basic_entity.rb index 61de3c93337..058c707ef9d 100644 --- a/app/serializers/issuable_sidebar_basic_entity.rb +++ b/app/serializers/issuable_sidebar_basic_entity.rb @@ -4,6 +4,7 @@ class IssuableSidebarBasicEntity < Grape::Entity include RequestAwareEntity expose :id + expose :iid expose :type do |issuable| issuable.to_ability_name end @@ -98,6 +99,10 @@ class IssuableSidebarBasicEntity < Grape::Entity autocomplete_projects_path(project_id: issuable.project.id) end + expose :project_emails_disabled do |issuable| + issuable.project.emails_disabled? + end + private def current_user diff --git a/app/serializers/merge_request_serializer.rb b/app/serializers/merge_request_serializer.rb index 8ad1df5dfe0..bd2e682a122 100644 --- a/app/serializers/merge_request_serializer.rb +++ b/app/serializers/merge_request_serializer.rb @@ -8,7 +8,7 @@ class MergeRequestSerializer < BaseSerializer entity ||= case opts[:serializer] when 'sidebar' - IssuableSidebarBasicEntity + MergeRequestSidebarBasicEntity when 'sidebar_extras' MergeRequestSidebarExtrasEntity when 'basic' diff --git a/app/serializers/merge_request_sidebar_basic_entity.rb b/app/serializers/merge_request_sidebar_basic_entity.rb new file mode 100644 index 00000000000..3c911bbe4c8 --- /dev/null +++ b/app/serializers/merge_request_sidebar_basic_entity.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class MergeRequestSidebarBasicEntity < IssuableSidebarBasicEntity + expose :current_user, if: lambda { |_issuable| current_user } do + expose :can_merge do |merge_request| + merge_request.can_be_merged_by?(current_user) + end + end +end diff --git a/app/serializers/merge_request_widget_entity.rb b/app/serializers/merge_request_widget_entity.rb index 554b307d4f8..c8088608cb0 100644 --- a/app/serializers/merge_request_widget_entity.rb +++ b/app/serializers/merge_request_widget_entity.rb @@ -84,8 +84,10 @@ class MergeRequestWidgetEntity < Grape::Entity private + delegate :current_user, to: :request + def presenter(merge_request) @presenters ||= {} - @presenters[merge_request] ||= MergeRequestPresenter.new(merge_request, current_user: request.current_user) # rubocop: disable CodeReuse/Presenter + @presenters[merge_request] ||= MergeRequestPresenter.new(merge_request, current_user: current_user) # rubocop: disable CodeReuse/Presenter end end diff --git a/app/services/award_emojis/add_service.rb b/app/services/award_emojis/add_service.rb new file mode 100644 index 00000000000..eac15dabbf0 --- /dev/null +++ b/app/services/award_emojis/add_service.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +module AwardEmojis + class AddService < AwardEmojis::BaseService + include Gitlab::Utils::StrongMemoize + + def execute + unless awardable.user_can_award?(current_user) + return error('User cannot award emoji to awardable', status: :forbidden) + end + + unless awardable.emoji_awardable? + return error('Awardable cannot be awarded emoji', status: :unprocessable_entity) + end + + award = awardable.award_emoji.create(name: name, user: current_user) + + if award.persisted? + TodoService.new.new_award_emoji(todoable, current_user) if todoable + success(award: award) + else + error(award.errors.full_messages, award: award) + end + end + + private + + def todoable + strong_memoize(:todoable) do + case awardable + when Note + # We don't create todos for personal snippet comments for now + awardable.noteable unless awardable.for_personal_snippet? + when MergeRequest, Issue + awardable + when Snippet + nil + end + end + end + end +end diff --git a/app/services/award_emojis/base_service.rb b/app/services/award_emojis/base_service.rb new file mode 100644 index 00000000000..a677d03a221 --- /dev/null +++ b/app/services/award_emojis/base_service.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +module AwardEmojis + class BaseService < ::BaseService + attr_accessor :awardable, :name + + def initialize(awardable, name, current_user) + @awardable = awardable + @name = normalize_name(name) + + super(awardable.project, current_user) + end + + private + + def normalize_name(name) + Gitlab::Emoji.normalize_emoji_name(name) + end + + # Provide more error state data than what BaseService allows. + # - An array of errors + # - The `AwardEmoji` if present + def error(errors, award: nil, status: nil) + errors = Array.wrap(errors) + + super(errors.to_sentence.presence, status).merge({ + award: award, + errors: errors + }) + end + end +end diff --git a/app/services/award_emojis/collect_user_emoji_service.rb b/app/services/award_emojis/collect_user_emoji_service.rb new file mode 100644 index 00000000000..6cab23f3edf --- /dev/null +++ b/app/services/award_emojis/collect_user_emoji_service.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +# Class for retrieving information about emoji awarded _by_ a particular user. +module AwardEmojis + class CollectUserEmojiService + attr_reader :current_user + + # current_user - The User to generate the data for. + def initialize(current_user = nil) + @current_user = current_user + end + + def execute + return [] unless current_user + + # We want the resulting data set to be an Array containing the emoji names + # in descending order, based on how often they were awarded. + AwardEmoji + .award_counts_for_user(current_user) + .map { |name, _| { name: name } } + end + end +end diff --git a/app/services/award_emojis/destroy_service.rb b/app/services/award_emojis/destroy_service.rb new file mode 100644 index 00000000000..3789a8403bc --- /dev/null +++ b/app/services/award_emojis/destroy_service.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module AwardEmojis + class DestroyService < AwardEmojis::BaseService + def execute + unless awardable.user_can_award?(current_user) + return error('User cannot destroy emoji on the awardable', status: :forbidden) + end + + awards = AwardEmojisFinder.new(awardable, name: name, awarded_by: current_user).execute + + if awards.empty? + return error("User has not awarded emoji of type #{name} on the awardable", status: :forbidden) + end + + award = awards.destroy_all.first # rubocop: disable DestroyAll + + success(award: award) + end + end +end diff --git a/app/services/award_emojis/toggle_service.rb b/app/services/award_emojis/toggle_service.rb new file mode 100644 index 00000000000..812dd1c2889 --- /dev/null +++ b/app/services/award_emojis/toggle_service.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module AwardEmojis + class ToggleService < AwardEmojis::BaseService + def execute + if awardable.awarded_emoji?(name, current_user) + DestroyService.new(awardable, name, current_user).execute + else + AddService.new(awardable, name, current_user).execute + end + end + end +end diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb index cdcc4b15bea..29317f1176e 100644 --- a/app/services/ci/create_pipeline_service.rb +++ b/app/services/ci/create_pipeline_service.rb @@ -15,7 +15,8 @@ module Ci Gitlab::Ci::Pipeline::Chain::Limit::Size, Gitlab::Ci::Pipeline::Chain::Populate, Gitlab::Ci::Pipeline::Chain::Create, - Gitlab::Ci::Pipeline::Chain::Limit::Activity].freeze + Gitlab::Ci::Pipeline::Chain::Limit::Activity, + Gitlab::Ci::Pipeline::Chain::Limit::JobActivity].freeze def execute(source, ignore_skip_ci: false, save_on_errors: true, trigger_request: nil, schedule: nil, merge_request: nil, **options, &block) @pipeline = Ci::Pipeline.new diff --git a/app/services/ci/process_pipeline_service.rb b/app/services/ci/process_pipeline_service.rb index 99d4ff9ecd1..3b145a65d79 100644 --- a/app/services/ci/process_pipeline_service.rb +++ b/app/services/ci/process_pipeline_service.rb @@ -40,16 +40,21 @@ module Ci def process_builds_with_needs(trigger_build_ids) return false unless trigger_build_ids.present? - return false unless Feature.enabled?(:ci_dag_support, project) + return false unless Feature.enabled?(:ci_dag_support, project, default_enabled: true) - # rubocop: disable CodeReuse/ActiveRecord - trigger_build_names = pipeline.statuses - .where(id: trigger_build_ids) - .select(:name) - # rubocop: enable CodeReuse/ActiveRecord + # we find processables that are dependent: + # 1. because of current dependency, + trigger_build_names = pipeline.processables.latest + .for_ids(trigger_build_ids).names + # 2. does not have builds that not yet complete + incomplete_build_names = pipeline.processables.latest + .incomplete.names + + # Each found processable is guaranteed here to have completed status created_processables .with_needs(trigger_build_names) + .without_needs(incomplete_build_names) .find_each .map(&method(:process_build_with_needs)) .any? @@ -70,17 +75,13 @@ module Ci end end - # rubocop: disable CodeReuse/ActiveRecord def status_for_prior_stages(index) - pipeline.builds.where('stage_idx < ?', index).latest.status || 'success' + pipeline.processables.status_for_prior_stages(index) end - # rubocop: enable CodeReuse/ActiveRecord - # rubocop: disable CodeReuse/ActiveRecord def status_for_build_needs(needs) - pipeline.builds.where(name: needs).latest.status || 'success' + pipeline.processables.status_for_names(needs) end - # rubocop: enable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord def stage_indexes_of_created_processables_without_needs @@ -89,15 +90,13 @@ module Ci end # rubocop: enable CodeReuse/ActiveRecord - # rubocop: disable CodeReuse/ActiveRecord def created_processables_in_stage_without_needs(index) created_processables_without_needs - .where(stage_idx: index) + .for_stage(index) end - # rubocop: enable CodeReuse/ActiveRecord def created_processables_without_needs - if Feature.enabled?(:ci_dag_support, project) + if Feature.enabled?(:ci_dag_support, project, default_enabled: true) pipeline.processables.created.without_needs else pipeline.processables.created diff --git a/app/services/ci/update_build_queue_service.rb b/app/services/ci/update_build_queue_service.rb index 9c589d910eb..31c7178c9e7 100644 --- a/app/services/ci/update_build_queue_service.rb +++ b/app/services/ci/update_build_queue_service.rb @@ -9,6 +9,10 @@ module Ci private def tick_for(build, runners) + if Feature.enabled?(:ci_update_queues_for_online_runners, build.project, default_enabled: true) + runners = runners.with_recent_runner_queue + end + runners.each do |runner| runner.pick_build!(build) end diff --git a/app/services/git/base_hooks_service.rb b/app/services/git/base_hooks_service.rb index d30df34e54b..47c308c8280 100644 --- a/app/services/git/base_hooks_service.rb +++ b/app/services/git/base_hooks_service.rb @@ -8,8 +8,6 @@ module Git PROCESS_COMMIT_LIMIT = 100 def execute - project.repository.after_create if project.empty_repo? - create_events create_pipelines execute_project_hooks @@ -19,7 +17,7 @@ module Git update_remote_mirrors - push_data + success end private @@ -33,7 +31,7 @@ module Git end def limited_commits - commits.last(PROCESS_COMMIT_LIMIT) + @limited_commits ||= commits.last(PROCESS_COMMIT_LIMIT) end def commits_count @@ -48,43 +46,69 @@ module Git [] end + # Push events in the activity feed only show information for the + # last commit. def create_events - EventCreateService.new.push(project, current_user, push_data) + EventCreateService.new.push(project, current_user, event_push_data) end def create_pipelines return unless params.fetch(:create_pipelines, true) Ci::CreatePipelineService - .new(project, current_user, push_data) + .new(project, current_user, pipeline_params) .execute(:push, pipeline_options) end def execute_project_hooks - project.execute_hooks(push_data, hook_name) - project.execute_services(push_data, hook_name) + # Creating push_data invokes one CommitDelta RPC per commit. Only + # build this data if we actually need it. + project.execute_hooks(push_data, hook_name) if project.has_active_hooks?(hook_name) + project.execute_services(push_data, hook_name) if project.has_active_services?(hook_name) end def enqueue_invalidate_cache - ProjectCacheWorker.perform_async( - project.id, - invalidated_file_types, - [:commit_count, :repository_size] - ) + file_types = invalidated_file_types + + return unless file_types.present? + + ProjectCacheWorker.perform_async(project.id, file_types, [], false) end - def push_data - @push_data ||= Gitlab::DataBuilder::Push.build( - project: project, - user: current_user, + def pipeline_params + { + before: params[:oldrev], + after: params[:newrev], + ref: params[:ref], + push_options: params[:push_options] || {}, + checkout_sha: Gitlab::DataBuilder::Push.checkout_sha( + project.repository, params[:newrev], params[:ref]) + } + end + + def push_data_params(commits:, with_changed_files: true) + { oldrev: params[:oldrev], newrev: params[:newrev], ref: params[:ref], - commits: limited_commits, + project: project, + user: current_user, + commits: commits, message: event_message, commits_count: commits_count, - push_options: params[:push_options] || {} - ) + with_changed_files: with_changed_files + } + end + + def event_push_data + # We only need the last commit for the event push, and we don't + # need the full deltas either. + @event_push_data ||= Gitlab::DataBuilder::Push.build( + push_data_params(commits: commits.last, with_changed_files: false)) + end + + def push_data + @push_data ||= Gitlab::DataBuilder::Push.build(push_data_params(commits: limited_commits)) # Dependent code may modify the push data, so return a duplicate each time @push_data.dup diff --git a/app/services/git/branch_hooks_service.rb b/app/services/git/branch_hooks_service.rb index c41f445c3c4..d2b037a680c 100644 --- a/app/services/git/branch_hooks_service.rb +++ b/app/services/git/branch_hooks_service.rb @@ -63,7 +63,7 @@ module Git end def branch_create_hooks - project.repository.after_create_branch + project.repository.after_create_branch(expire_cache: false) project.after_create_default_branch if default_branch? end @@ -78,13 +78,21 @@ module Git end def branch_remove_hooks - project.repository.after_remove_branch + project.repository.after_remove_branch(expire_cache: false) end # Schedules processing of commit messages def enqueue_process_commit_messages - limited_commits.each do |commit| - next unless commit.matches_cross_reference_regex? + referencing_commits = limited_commits.select(&:matches_cross_reference_regex?) + + upstream_commit_ids = upstream_commit_ids(referencing_commits) + + referencing_commits.each do |commit| + # Avoid reprocessing commits that already exist upstream if the project + # is a fork. This will prevent duplicated/superfluous system notes on + # mentionables referenced by a commit that is pushed to the upstream, + # that is then also pushed to forks when these get synced by users. + next if upstream_commit_ids.include?(commit.id) ProcessCommitWorker.perform_async( project.id, @@ -142,5 +150,18 @@ module Git def branch_name strong_memoize(:branch_name) { Gitlab::Git.ref_name(params[:ref]) } end + + def upstream_commit_ids(commits) + set = Set.new + + upstream_project = project.fork_source + if upstream_project + upstream_project + .commits_by(oids: commits.map(&:id)) + .each { |commit| set << commit.id } + end + + set + end end end diff --git a/app/services/groups/update_service.rb b/app/services/groups/update_service.rb index 73e1e00dc33..116756bacfe 100644 --- a/app/services/groups/update_service.rb +++ b/app/services/groups/update_service.rb @@ -46,6 +46,11 @@ module Groups params.delete(:parent_id) end + # overridden in EE + def remove_unallowed_params + params.delete(:emails_disabled) unless can?(current_user, :set_emails_disabled, group) + end + def valid_share_with_group_lock_change? return true unless changing_share_with_group_lock? return true if can?(current_user, :change_share_with_group_lock, group) diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb index 77c2224ee3b..2ab6e88599f 100644 --- a/app/services/issuable_base_service.rb +++ b/app/services/issuable_base_service.rb @@ -344,10 +344,7 @@ class IssuableBaseService < BaseService def toggle_award(issuable) award = params.delete(:emoji_award) - if award - todo_service.new_award_emoji(issuable, current_user) - issuable.toggle_award_emoji(award, current_user) - end + AwardEmojis::ToggleService.new(issuable, award, current_user).execute if award end def associations_before_update(issuable) diff --git a/app/services/merge_requests/base_service.rb b/app/services/merge_requests/base_service.rb index 067510a8a0a..c6aae4c28f2 100644 --- a/app/services/merge_requests/base_service.rb +++ b/app/services/merge_requests/base_service.rb @@ -17,11 +17,9 @@ module MergeRequests end def execute_hooks(merge_request, action = 'open', old_rev: nil, old_associations: {}) - if merge_request.project - merge_data = hook_data(merge_request, action, old_rev: old_rev, old_associations: old_associations) - merge_request.project.execute_hooks(merge_data, :merge_request_hooks) - merge_request.project.execute_services(merge_data, :merge_request_hooks) - end + merge_data = hook_data(merge_request, action, old_rev: old_rev, old_associations: old_associations) + merge_request.project.execute_hooks(merge_data, :merge_request_hooks) + merge_request.project.execute_services(merge_data, :merge_request_hooks) end def cleanup_environments(merge_request) diff --git a/app/services/merge_requests/rebase_service.rb b/app/services/merge_requests/rebase_service.rb index 8d3b9b05819..27c16ba1777 100644 --- a/app/services/merge_requests/rebase_service.rb +++ b/app/services/merge_requests/rebase_service.rb @@ -15,7 +15,8 @@ module MergeRequests end def rebase - if merge_request.gitaly_rebase_in_progress? + # Ensure Gitaly isn't already running a rebase + if source_project.repository.rebase_in_progress?(merge_request.id) log_error('Rebase task canceled: Another rebase is already in progress', save_message_on_model: true) return false end diff --git a/app/services/notes/create_service.rb b/app/services/notes/create_service.rb index a09272f0d83..248e81080cc 100644 --- a/app/services/notes/create_service.rb +++ b/app/services/notes/create_service.rb @@ -53,7 +53,7 @@ module Notes # We must add the error after we call #save because errors are reset # when #save is called if only_commands - note.errors.add(:commands_only, message.presence || _('Commands did not apply')) + note.errors.add(:commands_only, message.presence || _('Failed to apply commands.')) end end diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index 21fab22e0d4..83710ffce2f 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -321,6 +321,9 @@ class NotificationService end def decline_project_invite(project_member) + # Must always send, regardless of project/namespace configuration since it's a + # response to the user's action. + mailer.member_invite_declined_email( project_member.real_source_type, project_member.project.id, @@ -351,8 +354,8 @@ class NotificationService end def decline_group_invite(group_member) - # always send this one, since it's a response to the user's own - # action + # Must always send, regardless of project/namespace configuration since it's a + # response to the user's action. mailer.member_invite_declined_email( group_member.real_source_type, @@ -410,6 +413,10 @@ class NotificationService end def pipeline_finished(pipeline, recipients = nil) + # Must always check project configuration since recipients could be a list of emails + # from the PipelinesEmailService integration. + return if pipeline.project.emails_disabled? + email_template = "pipeline_#{pipeline.status}_email" return unless mailer.respond_to?(email_template) @@ -428,6 +435,8 @@ class NotificationService end def autodevops_disabled(pipeline, recipients) + return if pipeline.project.emails_disabled? + recipients.each do |recipient| mailer.autodevops_disabled_email(pipeline, recipient).deliver_later end @@ -472,10 +481,14 @@ class NotificationService end def repository_cleanup_success(project, user) + return if project.emails_disabled? + mailer.send(:repository_cleanup_success_email, project, user).deliver_later end def repository_cleanup_failure(project, user, error) + return if project.emails_disabled? + mailer.send(:repository_cleanup_failure_email, project, user, error).deliver_later end diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb index 89dc4375c63..942a45286b2 100644 --- a/app/services/projects/create_service.rb +++ b/app/services/projects/create_service.rb @@ -5,9 +5,11 @@ module Projects include ValidatesClassificationLabel def initialize(user, params) - @current_user, @params = user, params.dup - @skip_wiki = @params.delete(:skip_wiki) + @current_user, @params = user, params.dup + @skip_wiki = @params.delete(:skip_wiki) @initialize_with_readme = Gitlab::Utils.to_boolean(@params.delete(:initialize_with_readme)) + @import_data = @params.delete(:import_data) + @relations_block = @params.delete(:relations_block) end def execute @@ -15,14 +17,11 @@ module Projects return ::Projects::CreateFromTemplateService.new(current_user, params).execute end - import_data = params.delete(:import_data) - relations_block = params.delete(:relations_block) - @project = Project.new(params) # Make sure that the user is allowed to use the specified visibility level - unless Gitlab::VisibilityLevel.allowed_for?(current_user, @project.visibility_level) - deny_visibility_level(@project) + if project_visibility.restricted? + deny_visibility_level(@project, project_visibility.visibility_level) return @project end @@ -44,7 +43,7 @@ module Projects @project.namespace_id = current_user.namespace_id end - relations_block&.call(@project) + @relations_block&.call(@project) yield(@project) if block_given? validate_classification_label(@project, :external_authorization_classification_label) @@ -54,7 +53,7 @@ module Projects @project.creator = current_user - save_project_and_import_data(import_data) + save_project_and_import_data after_create_actions if @project.persisted? @@ -129,9 +128,9 @@ module Projects !@project.feature_available?(:wiki, current_user) || @skip_wiki end - def save_project_and_import_data(import_data) + def save_project_and_import_data Project.transaction do - @project.create_or_update_import_data(data: import_data[:data], credentials: import_data[:credentials]) if import_data + @project.create_or_update_import_data(data: @import_data[:data], credentials: @import_data[:credentials]) if @import_data if @project.save unless @project.gitlab_project_import? @@ -192,5 +191,11 @@ module Projects fail(error: @project.errors.full_messages.join(', ')) end end + + def project_visibility + @project_visibility ||= Gitlab::VisibilityLevelChecker + .new(current_user, @project, project_params: { import_data: @import_data }) + .level_restricted? + end end end diff --git a/app/services/projects/update_remote_mirror_service.rb b/app/services/projects/update_remote_mirror_service.rb index 1244a0f72a7..13a467a3ef9 100644 --- a/app/services/projects/update_remote_mirror_service.rb +++ b/app/services/projects/update_remote_mirror_service.rb @@ -2,31 +2,52 @@ module Projects class UpdateRemoteMirrorService < BaseService - attr_reader :errors + MAX_TRIES = 3 - def execute(remote_mirror) + def execute(remote_mirror, tries) return success unless remote_mirror.enabled? - errors = [] + update_mirror(remote_mirror) - begin - remote_mirror.ensure_remote! - repository.fetch_remote(remote_mirror.remote_name, ssh_auth: remote_mirror, no_tags: true) + success + rescue Gitlab::Git::CommandError => e + # This happens if one of the gitaly calls above fail, for example when + # branches have diverged, or the pre-receive hook fails. + retry_or_fail(remote_mirror, e.message, tries) - opts = {} - if remote_mirror.only_protected_branches? - opts[:only_branches_matching] = project.protected_branches.select(:name).map(&:name) - end + error(e.message) + rescue => e + remote_mirror.mark_as_failed!(e.message) + raise e + end + + private + + def update_mirror(remote_mirror) + remote_mirror.update_start! + + remote_mirror.ensure_remote! + repository.fetch_remote(remote_mirror.remote_name, ssh_auth: remote_mirror, no_tags: true) - remote_mirror.update_repository(opts) - rescue => e - errors << e.message.strip + opts = {} + if remote_mirror.only_protected_branches? + opts[:only_branches_matching] = project.protected_branches.select(:name).map(&:name) end - if errors.present? - error(errors.join("\n\n")) + remote_mirror.update_repository(opts) + + remote_mirror.update_finish! + end + + def retry_or_fail(mirror, message, tries) + if tries < MAX_TRIES + mirror.mark_for_retry!(message) else - success + # It's not likely we'll be able to recover from this ourselves, so we'll + # notify the users of the problem, and don't trigger any sidekiq retries + # Instead, we'll wait for the next change to try the push again, or until + # a user manually retries. + mirror.mark_as_failed!(message) end end end diff --git a/app/services/projects/update_service.rb b/app/services/projects/update_service.rb index caab946174d..8acbdc7e02b 100644 --- a/app/services/projects/update_service.rb +++ b/app/services/projects/update_service.rb @@ -9,6 +9,7 @@ module Projects # rubocop: disable CodeReuse/ActiveRecord def execute + remove_unallowed_params validate! ensure_wiki_exists if enabling_wiki? @@ -54,6 +55,10 @@ module Projects end end + def remove_unallowed_params + params.delete(:emails_disabled) unless can?(current_user, :set_emails_disabled, project) + end + def after_update todos_features_changes = %w( issues_access_level diff --git a/app/services/self_monitoring/project/create_service.rb b/app/services/self_monitoring/project/create_service.rb deleted file mode 100644 index c925c6a1610..00000000000 --- a/app/services/self_monitoring/project/create_service.rb +++ /dev/null @@ -1,219 +0,0 @@ -# frozen_string_literal: true - -module SelfMonitoring - module Project - class CreateService < ::BaseService - include Stepable - include Gitlab::Utils::StrongMemoize - - VISIBILITY_LEVEL = Gitlab::VisibilityLevel::INTERNAL - PROJECT_NAME = 'GitLab Instance Administration' - PROJECT_DESCRIPTION = <<~HEREDOC - This project is automatically generated and will be used to help monitor this GitLab instance. - HEREDOC - - GROUP_NAME = 'GitLab Instance Administrators' - GROUP_PATH = 'gitlab-instance-administrators' - - steps :validate_admins, - :create_group, - :create_project, - :save_project_id, - :add_group_members, - :add_to_whitelist, - :add_prometheus_manual_configuration - - def initialize - super(nil) - end - - def execute - execute_steps - end - - private - - def validate_admins - unless instance_admins.any? - log_error('No active admin user found') - return error('No active admin user found') - end - - success - end - - def create_group - if project_created? - log_info(_('Instance administrators group already exists')) - @group = application_settings.instance_administration_project.owner - return success(group: @group) - end - - admin_user = group_owner - @group = ::Groups::CreateService.new(admin_user, create_group_params).execute - - if @group.persisted? - success(group: @group) - else - error('Could not create group') - end - end - - def create_project - if project_created? - log_info(_('Instance administration project already exists')) - @project = application_settings.instance_administration_project - return success(project: project) - end - - admin_user = group_owner - @project = ::Projects::CreateService.new(admin_user, create_project_params).execute - - if project.persisted? - success(project: project) - else - log_error(_("Could not create instance administration project. Errors: %{errors}") % { errors: project.errors.full_messages }) - error(_('Could not create project')) - end - end - - def save_project_id - return success if project_created? - - result = ApplicationSettings::UpdateService.new( - application_settings, - group_owner, - { instance_administration_project_id: @project.id } - ).execute - - if result - success - else - log_error(_("Could not save instance administration project ID, errors: %{errors}") % { errors: application_settings.errors.full_messages }) - error(_('Could not save project ID')) - end - end - - def add_group_members - members = @group.add_users(group_maintainers, Gitlab::Access::MAINTAINER) - errors = members.flat_map { |member| member.errors.full_messages } - - if errors.any? - log_error("Could not add admins as members to self-monitoring project. Errors: #{errors}") - error('Could not add admins as members') - else - success - end - end - - def add_to_whitelist - return success unless prometheus_enabled? - return success unless prometheus_listen_address.present? - - uri = parse_url(internal_prometheus_listen_address_uri) - return error(_('Prometheus listen_address is not a valid URI')) unless uri - - result = ApplicationSettings::UpdateService.new( - application_settings, - group_owner, - add_to_outbound_local_requests_whitelist: [uri.normalized_host] - ).execute - - if result - success - else - log_error(_("Could not add prometheus URL to whitelist, errors: %{errors}") % { errors: application_settings.errors.full_messages }) - error(_('Could not add prometheus URL to whitelist')) - end - end - - def add_prometheus_manual_configuration - return success unless prometheus_enabled? - return success unless prometheus_listen_address.present? - - service = project.find_or_initialize_service('prometheus') - - unless service.update(prometheus_service_attributes) - log_error("Could not save prometheus manual configuration for self-monitoring project. Errors: #{service.errors.full_messages}") - return error('Could not save prometheus manual configuration') - end - - success - end - - def application_settings - strong_memoize(:application_settings) do - Gitlab::CurrentSettings.expire_current_application_settings - Gitlab::CurrentSettings.current_application_settings - end - end - - def project_created? - application_settings.instance_administration_project.present? - end - - def parse_url(uri_string) - Addressable::URI.parse(uri_string) - rescue Addressable::URI::InvalidURIError, TypeError - end - - def prometheus_enabled? - Gitlab.config.prometheus.enable - rescue Settingslogic::MissingSetting - false - end - - def prometheus_listen_address - Gitlab.config.prometheus.listen_address - rescue Settingslogic::MissingSetting - end - - def instance_admins - @instance_admins ||= User.admins.active - end - - def group_owner - instance_admins.first - end - - def group_maintainers - # Exclude the first so that the group_owner is not added again as a member. - instance_admins - [group_owner] - end - - def create_group_params - { - name: GROUP_NAME, - path: "#{GROUP_PATH}-#{SecureRandom.hex(4)}", - visibility_level: VISIBILITY_LEVEL - } - end - - def create_project_params - { - initialize_with_readme: true, - visibility_level: VISIBILITY_LEVEL, - name: PROJECT_NAME, - description: PROJECT_DESCRIPTION, - namespace_id: @group.id - } - end - - def internal_prometheus_listen_address_uri - if prometheus_listen_address.starts_with?('http') - prometheus_listen_address - else - 'http://' + prometheus_listen_address - end - end - - def prometheus_service_attributes - { - api_url: internal_prometheus_listen_address_uri, - manual_configuration: true, - active: true - } - end - end - end -end diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb index e30debbbe75..ee7223d6349 100644 --- a/app/services/system_note_service.rb +++ b/app/services/system_note_service.rb @@ -598,11 +598,11 @@ module SystemNoteService end def zoom_link_added(issue, project, author) - create_note(NoteSummary.new(issue, project, author, _('a Zoom call was added to this issue'), action: 'pinned_embed')) + create_note(NoteSummary.new(issue, project, author, _('added a Zoom call to this issue'), action: 'pinned_embed')) end def zoom_link_removed(issue, project, author) - create_note(NoteSummary.new(issue, project, author, _('a Zoom call was removed from this issue'), action: 'pinned_embed')) + create_note(NoteSummary.new(issue, project, author, _('removed a Zoom call from this issue'), action: 'pinned_embed')) end private diff --git a/app/services/todo_service.rb b/app/services/todo_service.rb index 0ea230a44a1..b1256df35d6 100644 --- a/app/services/todo_service.rb +++ b/app/services/todo_service.rb @@ -314,11 +314,9 @@ class TodoService end def reject_users_without_access(users, parent, target) - if target.is_a?(Note) && target.for_issuable? - target = target.noteable - end + target = target.noteable if target.is_a?(Note) - if target.is_a?(Issuable) + if target.respond_to?(:to_ability_name) select_users(users, :"read_#{target.to_ability_name}", target) else select_users(users, :read_project, parent) diff --git a/app/services/update_deployment_service.rb b/app/services/update_deployment_service.rb index 49a7d0178f4..dcafebae52d 100644 --- a/app/services/update_deployment_service.rb +++ b/app/services/update_deployment_service.rb @@ -42,7 +42,7 @@ class UpdateDeploymentService return unless environment_url @expanded_environment_url = - ExpandVariables.expand(environment_url, variables) + ExpandVariables.expand(environment_url, -> { variables }) end def environment_url diff --git a/app/views/admin/application_settings/_snowplow.html.haml b/app/views/admin/application_settings/_snowplow.html.haml new file mode 100644 index 00000000000..b60b5d55a1b --- /dev/null +++ b/app/views/admin/application_settings/_snowplow.html.haml @@ -0,0 +1,30 @@ +- expanded = true if !@application_setting.valid? && @application_setting.errors.any? { |k| k.to_s.start_with?('snowplow_') } +%section.settings.as-snowplow.no-animate#js-snowplow-settings{ class: ('expanded' if expanded) } + .settings-header + %h4 + = _('Snowplow') + %button.btn.btn-default.js-settings-toggle{ type: 'button' } + = expanded ? _('Collapse') : _('Expand') + %p + = _('Configure the %{link} integration.').html_safe % { link: link_to('Snowplow', 'https://snowplowanalytics.com/', target: '_blank') } + .settings-content + + = form_for @application_setting, url: integrations_admin_application_settings_path, html: { class: 'fieldset-form' } do |f| + = form_errors(@application_setting) + + %fieldset + .form-group + .form-check + = f.check_box :snowplow_enabled, class: 'form-check-input' + = f.label :snowplow_enabled, _('Enable snowplow tracking'), class: 'form-check-label' + .form-group + = f.label :snowplow_collector_hostname, _('Collector hostname'), class: 'label-light' + = f.text_field :snowplow_collector_hostname, class: 'form-control', placeholder: 'snowplow.example.com' + .form-group + = f.label :snowplow_site_id, _('Site ID'), class: 'label-light' + = f.text_field :snowplow_site_id, class: 'form-control' + .form-group + = f.label :snowplow_cookie_domain, _('Cookie domain'), class: 'label-light' + = f.text_field :snowplow_cookie_domain, class: 'form-control' + + = f.submit _('Save changes'), class: 'btn btn-success' diff --git a/app/views/admin/applications/_delete_form.html.haml b/app/views/admin/applications/_delete_form.html.haml index 82781f6716d..86f09bf1cb0 100644 --- a/app/views/admin/applications/_delete_form.html.haml +++ b/app/views/admin/applications/_delete_form.html.haml @@ -1,4 +1,4 @@ - submit_btn_css ||= 'btn btn-link btn-remove btn-sm' = form_tag admin_application_path(application) do %input{ :name => "_method", :type => "hidden", :value => "delete" }/ - = submit_tag 'Destroy', onclick: "return confirm('Are you sure?')", class: submit_btn_css + = submit_tag 'Destroy', class: submit_btn_css, data: { confirm: _('Are you sure?') } diff --git a/app/views/ci/status/_icon.html.haml b/app/views/ci/status/_icon.html.haml index 1249b98221f..fdaacb732c7 100644 --- a/app/views/ci/status/_icon.html.haml +++ b/app/views/ci/status/_icon.html.haml @@ -1,13 +1,10 @@ - status = local_assigns.fetch(:status) - size = local_assigns.fetch(:size, 16) -- type = local_assigns.fetch(:type, 'pipeline') - tooltip_placement = local_assigns.fetch(:tooltip_placement, "left") - path = local_assigns.fetch(:path, status.has_details? ? status.details_path : nil) - option_css_classes = local_assigns.fetch(:option_css_classes, '') - css_classes = "ci-status-link ci-status-icon ci-status-icon-#{status.group} has-tooltip #{option_css_classes}" - title = s_("PipelineStatusTooltip|Pipeline: %{ci_status}") % {ci_status: status.label} -- if type == 'commit' - - title = s_("PipelineStatusTooltip|Commit: %{ci_status}") % {ci_status: status.label} - if path = link_to path, class: css_classes, title: title, data: { placement: tooltip_placement } do diff --git a/app/views/devise/sessions/_new_base.html.haml b/app/views/devise/sessions/_new_base.html.haml index 2f10f08c839..0b1d3d1ddb3 100644 --- a/app/views/devise/sessions/_new_base.html.haml +++ b/app/views/devise/sessions/_new_base.html.haml @@ -1,20 +1,23 @@ = form_for(resource, as: resource_name, url: session_path(resource_name), html: { class: 'new_user gl-show-field-errors', 'aria-live' => 'assertive'}) do |f| .form-group - = f.label "Username or email", for: "user_login", class: 'label-bold' - = f.text_field :login, class: "form-control top", autofocus: "autofocus", autocapitalize: "off", autocorrect: "off", required: true, title: "This field is required.", data: { qa_selector: 'login_field' } + = f.label _('Username or email'), for: 'user_login', class: 'label-bold' + = f.text_field :login, class: 'form-control top', autofocus: 'autofocus', autocapitalize: 'off', autocorrect: 'off', required: true, title: _('This field is required.'), data: { qa_selector: 'login_field' } .form-group = f.label :password, class: 'label-bold' - = f.password_field :password, class: "form-control bottom", required: true, title: "This field is required.", data: { qa_selector: 'password_field' } + = f.password_field :password, class: 'form-control bottom', required: true, title: _('This field is required.'), data: { qa_selector: 'password_field' } - if devise_mapping.rememberable? .remember-me - %label{ for: "user_remember_me" } + %label{ for: 'user_remember_me' } = f.check_box :remember_me, class: 'remember-me-checkbox' %span Remember me - .float-right.forgot-password - = link_to "Forgot your password?", new_password_path(:user) + .float-right + - if unconfirmed_email? + = link_to _('Resend confirmation email'), new_user_confirmation_path + - else + = link_to _('Forgot your password?'), new_password_path(:user) %div - if captcha_enabled? = recaptcha_tags .submit-container.move-submit-down - = f.submit "Sign in", class: "btn btn-success", data: { qa_selector: 'sign_in_button' } + = f.submit _('Sign in'), class: 'btn btn-success', data: { qa_selector: 'sign_in_button' } diff --git a/app/views/devise/shared/_signup_box.html.haml b/app/views/devise/shared/_signup_box.html.haml index 074edf645ba..2cd77af6877 100644 --- a/app/views/devise/shared/_signup_box.html.haml +++ b/app/views/devise/shared/_signup_box.html.haml @@ -5,6 +5,8 @@ = form_for(resource, as: "new_#{resource_name}", url: registration_path(resource_name), html: { class: "new_new_user gl-show-field-errors", "aria-live" => "assertive" }) do |f| .devise-errors = render "devise/shared/error_messages", resource: resource + - if Feature.enabled?(:invisible_captcha) + = invisible_captcha .name.form-group = f.label :name, _('Full name'), class: 'label-bold' = f.text_field :name, class: "form-control top js-block-emoji js-validate-length", :data => { :max_length => max_name_length, :max_length_message => s_("SignUp|Name is too long (maximum is %{max_length} characters).") % { max_length: max_name_length }, :qa_selector => 'new_user_name_field' }, required: true, title: _("This field is required.") diff --git a/app/views/doorkeeper/authorized_applications/_delete_form.html.haml b/app/views/doorkeeper/authorized_applications/_delete_form.html.haml index 69cc510e9c1..9bc5e2ee42f 100644 --- a/app/views/doorkeeper/authorized_applications/_delete_form.html.haml +++ b/app/views/doorkeeper/authorized_applications/_delete_form.html.haml @@ -5,4 +5,4 @@ = form_tag path do %input{ :name => "_method", :type => "hidden", :value => "delete" }/ - = submit_tag _('Revoke'), onclick: "return confirm('#{_('Are you sure?')}')", class: 'btn btn-remove btn-sm' + = submit_tag _('Revoke'), class: 'btn btn-remove btn-sm', data: { confirm: _('Are you sure?') } diff --git a/app/views/errors/access_denied.html.haml b/app/views/errors/access_denied.html.haml index 46931b5932d..1ed7b56db1d 100644 --- a/app/views/errors/access_denied.html.haml +++ b/app/views/errors/access_denied.html.haml @@ -10,7 +10,7 @@ = message %p = s_('403|Please contact your GitLab administrator to get permission.') - .action-container.js-go-back{ style: 'display: none' } - %a{ href: 'javascript:history.back()', class: 'btn btn-success' } + .action-container.js-go-back{ hidden: true } + %button{ type: 'button', class: 'btn btn-success' } = s_('Go Back') = render "errors/footer" diff --git a/app/views/groups/_home_panel.html.haml b/app/views/groups/_home_panel.html.haml index 4daf3683eaf..e50d2b8e994 100644 --- a/app/views/groups/_home_panel.html.haml +++ b/app/views/groups/_home_panel.html.haml @@ -1,4 +1,5 @@ - can_create_subgroups = can?(current_user, :create_subgroup, @group) +- emails_disabled = @group.emails_disabled? .group-home-panel .row.mb-3 @@ -21,7 +22,7 @@ .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' + = render 'shared/notifications/new_button', notification_setting: @notification_setting, btn_class: 'btn', emails_disabled: emails_disabled - if can? current_user, :create_projects, @group - new_project_label = _("New project") - new_subgroup_label = _("New subgroup") diff --git a/app/views/groups/settings/_permissions.html.haml b/app/views/groups/settings/_permissions.html.haml index d3375e00bad..94a938021f9 100644 --- a/app/views/groups/settings/_permissions.html.haml +++ b/app/views/groups/settings/_permissions.html.haml @@ -11,13 +11,20 @@ .form-check = f.check_box :share_with_group_lock, disabled: !can_change_share_with_group_lock?(@group), class: 'form-check-input' = f.label :share_with_group_lock, class: 'form-check-label' do - %span + %span.d-block - group_link = link_to @group.name, group_path(@group) = s_('GroupSettings|Prevent sharing a project within %{group} with other groups').html_safe % { group: group_link } - %br %span.descr.text-muted= share_with_group_lock_help_text(@group) + .form-group.append-bottom-default + .form-check + = f.check_box :emails_disabled, checked: @group.emails_disabled?, disabled: !can_disable_group_emails?(@group), class: 'form-check-input' + = f.label :emails_disabled, class: 'form-check-label' do + %span.d-block= s_('GroupSettings|Disable email notifications') + %span.text-muted= s_('GroupSettings|This setting will override user notification preferences for all members of the group, subgroups, and projects.') + = render_if_exists 'groups/settings/ip_restriction', f: f, group: @group + = render_if_exists 'groups/settings/allowed_email_domain', f: f, group: @group = render 'groups/settings/lfs', f: f = render 'groups/settings/project_creation_level', f: f, group: @group = render 'groups/settings/subgroup_creation_level', f: f, group: @group diff --git a/app/views/import/github/new.html.haml b/app/views/import/github/new.html.haml index 72e5934574a..518c44cc687 100644 --- a/app/views/import/github/new.html.haml +++ b/app/views/import/github/new.html.haml @@ -1,30 +1,32 @@ -- title = has_ci_cd_only_params? ? _('Connect repositories from GitHub') : _('GitHub import') +- title = _('Authenticate with GitHub') - page_title title - breadcrumb_title title - header_title _("Projects"), root_path -%h3.page-title - = icon 'github', text: _('Import repositories from GitHub') +%h2.page-title + = title -- if github_import_configured? - %p - = import_github_authorize_message +%p + = import_github_authorize_message - = link_to _('List your GitHub repositories'), status_import_github_path(ci_cd_only: params[:ci_cd_only]), class: 'btn btn-success' +- if github_import_configured? && !has_ci_cd_only_params? + = link_to icon('github', text: title), status_import_github_path, class: 'btn btn-success' %hr -%p - = import_github_personal_access_token_message +- unless github_import_configured? || has_ci_cd_only_params? + .bs-callout.bs-callout-info + = import_configure_github_admin_message -= form_tag personal_access_token_import_github_path, method: :post, class: 'form-inline' do += form_tag personal_access_token_import_github_path, method: :post do .form-group - = text_field_tag :personal_access_token, '', class: 'form-control append-right-8', placeholder: _('Personal Access Token'), size: 40 - = submit_tag _('List your GitHub repositories'), class: 'btn btn-success' + %label.label-bold= _('Personal Access Token') + = text_field_tag :personal_access_token, '', class: 'form-control', placeholder: _('e.g. %{token}') % { token: '8d3f016698e...' } + %span.form-text.text-muted + = import_github_personal_access_token_message = render_if_exists 'import/github/ci_cd_only' -- unless github_import_configured? - %hr - %p - = import_configure_github_admin_message + .form-actions.d-flex.justify-content-end + = link_to _('Cancel'), new_project_path, class: 'btn' + = submit_tag _('Authenticate'), class: 'btn btn-success ml-2' diff --git a/app/views/layouts/_search.html.haml b/app/views/layouts/_search.html.haml index bc900992cb0..e6a235e39da 100644 --- a/app/views/layouts/_search.html.haml +++ b/app/views/layouts/_search.html.haml @@ -47,5 +47,6 @@ = hidden_field_tag :snippets, true = hidden_field_tag :repository_ref, @ref = hidden_field_tag :nav_source, 'navbar' + -# workaround for non-JS feature specs, for JS you need to use find('#search').send_keys(:enter) = button_tag 'Go' if ENV['RAILS_ENV'] == 'test' .search-autocomplete-opts.hide{ :'data-autocomplete-path' => search_autocomplete_path, :'data-autocomplete-project-id' => @project.try(:id), :'data-autocomplete-project-ref' => @ref } diff --git a/app/views/layouts/_snowplow.html.haml b/app/views/layouts/_snowplow.html.haml new file mode 100644 index 00000000000..5f5c5e984c5 --- /dev/null +++ b/app/views/layouts/_snowplow.html.haml @@ -0,0 +1,29 @@ +- return unless Gitlab::CurrentSettings.snowplow_enabled? + += javascript_tag nonce: true do + :plain + ;(function(p,l,o,w,i,n,g){if(!p[i]){p.GlobalSnowplowNamespace=p.GlobalSnowplowNamespace||[]; + p.GlobalSnowplowNamespace.push(i);p[i]=function(){(p[i].q=p[i].q||[]).push(arguments) + };p[i].q=p[i].q||[];n=l.createElement(o);g=l.getElementsByTagName(o)[0];n.async=1; + n.src=w;g.parentNode.insertBefore(n,g)}}(window,document,"script","#{asset_url('snowplow/sp.js')}","snowplow")); + + window.snowplow('newTracker', '#{Gitlab::SnowplowTracker::NAMESPACE}', '#{Gitlab::CurrentSettings.snowplow_collector_hostname}', { + appId: '#{Gitlab::CurrentSettings.snowplow_site_id}', + cookieDomain: '#{Gitlab::CurrentSettings.snowplow_cookie_domain}', + userFingerprint: false, + respectDoNotTrack: true, + forceSecureTracker: true, + post: true, + contexts: { webPage: true }, + stateStorageStrategy: "localStorage" + }); + + window.snowplow('enableActivityTracking', 30, 30); + window.snowplow('trackPageView'); + +- return unless Feature.enabled?(:additional_snowplow_tracking, @group) + += javascript_tag nonce: true do + :plain + window.snowplow('enableFormTracking'); + window.snowplow('enableLinkClickTracking'); diff --git a/app/views/layouts/errors.html.haml b/app/views/layouts/errors.html.haml index 74484005b48..dc924a0e25d 100644 --- a/app/views/layouts/errors.html.haml +++ b/app/views/layouts/errors.html.haml @@ -14,6 +14,10 @@ var goBackElement = document.querySelector('.js-go-back'); if (goBackElement && history.length > 1) { - goBackElement.style.display = 'block'; + goBackElement.removeAttribute('hidden'); + + goBackElement.querySelector('button').addEventListener('click', function() { + history.back(); + }); } }()); diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml index 89f99472270..14c7b2428b2 100644 --- a/app/views/layouts/header/_default.html.haml +++ b/app/views/layouts/header/_default.html.haml @@ -4,6 +4,7 @@ - search_path_url = search_path(group_id: group.id) - else - search_path_url = search_path +- has_impersonation_link = header_link?(:admin_impersonation) %header.navbar.navbar-gitlab.navbar-expand-sm.js-navbar{ data: { qa_selector: 'navbar' } } %a.sr-only.gl-accessibility{ href: "#content-body", tabindex: "1" } Skip to content @@ -64,14 +65,14 @@ .dropdown-menu.dropdown-menu-right = render 'layouts/header/help_dropdown' - if header_link?(:user_dropdown) - %li.nav-item.header-user.dropdown{ data: { track_label: "profile_dropdown", track_event: "click_dropdown", track_value: "", qa_selector: 'user_menu' } } + %li.nav-item.header-user.dropdown{ data: { track_label: "profile_dropdown", track_event: "click_dropdown", track_value: "", qa_selector: 'user_menu' }, class: ('mr-0' if has_impersonation_link) } = link_to current_user, class: user_dropdown_class, data: { toggle: "dropdown" } do = image_tag avatar_icon_for_user(current_user, 23), width: 23, height: 23, class: "header-user-avatar qa-user-avatar" = sprite_icon('angle-down', css_class: 'caret-down') .dropdown-menu.dropdown-menu-right = render 'layouts/header/current_user_dropdown' - - if header_link?(:admin_impersonation) - %li.nav-item.impersonation + - if has_impersonation_link + %li.nav-item.impersonation.ml-0 = link_to admin_impersonation_path, class: 'nav-link impersonation-btn', method: :delete, title: _('Stop impersonation'), aria: { label: _('Stop impersonation') }, data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } do = icon('user-secret') - if header_link?(:sign_in) diff --git a/app/views/profiles/notifications/_group_settings.html.haml b/app/views/profiles/notifications/_group_settings.html.haml index cf17ee44145..1776d260e19 100644 --- a/app/views/profiles/notifications/_group_settings.html.haml +++ b/app/views/profiles/notifications/_group_settings.html.haml @@ -1,16 +1,15 @@ +- emails_disabled = group.emails_disabled? + .gl-responsive-table-row.notification-list-item .table-section.section-40 %span.notification.fa.fa-holder.append-right-5 - - if setting.global? - = notification_icon(current_user.global_notification_setting.level) - - else - = notification_icon(setting.level) + = notification_icon(notification_icon_level(setting, emails_disabled)) %span.str-truncated = link_to group.name, group_path(group) .table-section.section-30.text-right - = render 'shared/notifications/button', notification_setting: setting + = render 'shared/notifications/button', notification_setting: setting, emails_disabled: emails_disabled .table-section.section-30 = form_for @user.notification_settings.find { |ns| ns.source == group }, url: profile_notifications_group_path(group), method: :put, html: { class: 'update-notifications' } do |f| diff --git a/app/views/profiles/notifications/_project_settings.html.haml b/app/views/profiles/notifications/_project_settings.html.haml index 823fec3fab4..63a77b335b6 100644 --- a/app/views/profiles/notifications/_project_settings.html.haml +++ b/app/views/profiles/notifications/_project_settings.html.haml @@ -1,12 +1,11 @@ +- emails_disabled = project.emails_disabled? + %li.notification-list-item %span.notification.fa.fa-holder.append-right-5 - - if setting.global? - = notification_icon(current_user.global_notification_setting.level) - - else - = notification_icon(setting.level) + = notification_icon(notification_icon_level(setting, emails_disabled)) %span.str-truncated = link_to_project(project) .float-right - = render 'shared/notifications/button', notification_setting: setting + = render 'shared/notifications/button', notification_setting: setting, emails_disabled: emails_disabled diff --git a/app/views/profiles/personal_access_tokens/index.html.haml b/app/views/profiles/personal_access_tokens/index.html.haml index 65ef9690062..08a39fc4f58 100644 --- a/app/views/profiles/personal_access_tokens/index.html.haml +++ b/app/views/profiles/personal_access_tokens/index.html.haml @@ -31,7 +31,7 @@ = s_('AccessTokens|It cannot be used to access any other data.') .col-lg-8.feed-token-reset = label_tag :feed_token, s_('AccessTokens|Feed token'), class: "label-bold" - = text_field_tag :feed_token, current_user.feed_token, class: 'form-control', readonly: true, onclick: 'this.select()' + = text_field_tag :feed_token, current_user.feed_token, class: 'form-control js-select-on-focus', readonly: true %p.form-text.text-muted - reset_link = link_to s_('AccessTokens|reset it'), [:reset, :feed_token, :profile], method: :put, data: { confirm: s_('AccessTokens|Are you sure? Any RSS or calendar URLs currently in use will stop working.') } - reset_message = s_('AccessTokens|Keep this token secret. Anyone who gets ahold of it can read activity and issue RSS feeds or your calendar feed as if they were you. You should %{link_reset_it} if that ever happens.') % { link_reset_it: reset_link } @@ -49,7 +49,7 @@ = s_('AccessTokens|It cannot be used to access any other data.') .col-lg-8.incoming-email-token-reset = label_tag :incoming_email_token, s_('AccessTokens|Incoming email token'), class: "label-bold" - = text_field_tag :incoming_email_token, current_user.incoming_email_token, class: 'form-control', readonly: true, onclick: 'this.select()' + = text_field_tag :incoming_email_token, current_user.incoming_email_token, class: 'form-control js-select-on-focus', readonly: true %p.form-text.text-muted - reset_link = link_to s_('AccessTokens|reset it'), [:reset, :incoming_email_token, :profile], method: :put, data: { confirm: s_('AccessTokens|Are you sure? Any issue email addresses currently in use will stop working.') } - reset_message = s_('AccessTokens|Keep this token secret. Anyone who gets ahold of it can create issues as if they were you. You should %{link_reset_it} if that ever happens.') % { link_reset_it: reset_link } diff --git a/app/views/profiles/preferences/show.html.haml b/app/views/profiles/preferences/show.html.haml index d16e2dddbe0..d99063e344f 100644 --- a/app/views/profiles/preferences/show.html.haml +++ b/app/views/profiles/preferences/show.html.haml @@ -45,20 +45,20 @@ .form-group = f.label :layout, class: 'label-bold' do = s_('Preferences|Layout width') - = f.select :layout, layout_choices, {}, class: 'form-control' + = f.select :layout, layout_choices, {}, class: 'select2' .form-text.text-muted = s_('Preferences|Choose between fixed (max. 1280px) and fluid (100%%) application layout.') .form-group = f.label :dashboard, class: 'label-bold' do = s_('Preferences|Default dashboard') - = f.select :dashboard, dashboard_choices, {}, class: 'form-control' + = f.select :dashboard, dashboard_choices, {}, class: 'select2' = render_if_exists 'profiles/preferences/group_overview_selector', f: f # EE-specific .form-group = f.label :project_view, class: 'label-bold' do = s_('Preferences|Project overview content') - = f.select :project_view, project_view_choices, {}, class: 'form-control' + = f.select :project_view, project_view_choices, {}, class: 'select2' .form-text.text-muted = s_('Preferences|Choose what content you want to see on a project’s overview page.') @@ -82,7 +82,7 @@ .form-group = f.label :first_day_of_week, class: 'label-bold' do = _('First day of the week') - = f.select :first_day_of_week, first_day_of_week_choices_with_default, {}, class: 'form-control' + = f.select :first_day_of_week, first_day_of_week_choices_with_default, {}, class: 'select2' - if Feature.enabled?(:user_time_settings) .col-sm-12 %hr diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml index 824fe3c791d..4783b10cf6d 100644 --- a/app/views/projects/_home_panel.html.haml +++ b/app/views/projects/_home_panel.html.haml @@ -1,6 +1,8 @@ - empty_repo = @project.empty_repo? - show_auto_devops_callout = show_auto_devops_callout?(@project) - max_project_topic_length = 15 +- emails_disabled = @project.emails_disabled? + .project-home-panel{ class: [("empty-project" if empty_repo), ("js-keep-hidden-on-navigation" if vue_file_list_enabled?)] } .row.append-bottom-8 .home-panel-title-row.col-md-12.col-lg-6.d-flex @@ -41,7 +43,7 @@ .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 'shared/notifications/new_button', notification_setting: @notification_setting, btn_class: 'btn-xs' + = render 'shared/notifications/new_button', notification_setting: @notification_setting, btn_class: 'btn-xs', emails_disabled: emails_disabled .count-buttons.d-inline-flex = render 'projects/buttons/star' diff --git a/app/views/projects/commit/_ajax_signature.html.haml b/app/views/projects/commit/_ajax_signature.html.haml index ae9aef5a9b0..e1bf0940f59 100644 --- a/app/views/projects/commit/_ajax_signature.html.haml +++ b/app/views/projects/commit/_ajax_signature.html.haml @@ -1,2 +1,2 @@ - if commit.has_signature? - %a{ href: 'javascript:void(0)', tabindex: 0, class: commit_signature_badge_classes('js-loading-gpg-badge'), data: { toggle: 'tooltip', placement: 'top', title: _('GPG signature (loading...)'), 'commit-sha' => commit.sha } } + %button{ tabindex: 0, class: commit_signature_badge_classes('js-loading-gpg-badge'), data: { toggle: 'tooltip', placement: 'top', title: _('GPG signature (loading...)'), 'commit-sha' => commit.sha } } diff --git a/app/views/projects/commit/_signature_badge.html.haml b/app/views/projects/commit/_signature_badge.html.haml index 1331fa179fc..cbd998c60ef 100644 --- a/app/views/projects/commit/_signature_badge.html.haml +++ b/app/views/projects/commit/_signature_badge.html.haml @@ -24,5 +24,5 @@ = link_to(_('Learn more about signing commits'), help_page_path('user/project/repository/gpg_signed_commits/index.md'), class: 'gpg-popover-help-link') -%a{ href: 'javascript:void(0)', tabindex: 0, class: css_classes, data: { toggle: 'popover', html: 'true', placement: 'top', title: title, content: content } } +%button{ tabindex: 0, class: css_classes, data: { toggle: 'popover', html: 'true', placement: 'top', title: title, content: content } } = label diff --git a/app/views/projects/cycle_analytics/show.html.haml b/app/views/projects/cycle_analytics/show.html.haml index 59f0afd59e6..2b594c125f4 100644 --- a/app/views/projects/cycle_analytics/show.html.haml +++ b/app/views/projects/cycle_analytics/show.html.haml @@ -34,40 +34,29 @@ {{ n__('Last %d day', 'Last %d days', 90) }} .stage-panel-container .card.stage-panel - .card-header + .card-header.border-bottom-0 %nav.col-headers %ul - %li.stage-header - %span.stage-name + %li.stage-header.pl-5 + %span.stage-name.font-weight-bold {{ s__('ProjectLifecycle|Stage') }} %i.has-tooltip.fa.fa-question-circle{ "data-placement" => "top", title: _("The phase of the development lifecycle."), "aria-hidden" => "true" } %li.median-header - %span.stage-name + %span.stage-name.font-weight-bold {{ __('Median') }} %i.has-tooltip.fa.fa-question-circle{ "data-placement" => "top", title: _("The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6."), "aria-hidden" => "true" } - %li.event-header - %span.stage-name + %li.event-header.pl-3 + %span.stage-name.font-weight-bold {{ currentStage ? __(currentStage.legend) : __('Related Issues') }} %i.has-tooltip.fa.fa-question-circle{ "data-placement" => "top", title: _("The collection of events added to the data gathered for that stage."), "aria-hidden" => "true" } - %li.total-time-header - %span.stage-name + %li.total-time-header.pr-5.text-right + %span.stage-name.font-weight-bold {{ __('Total Time') }} %i.has-tooltip.fa.fa-question-circle{ "data-placement" => "top", title: _("The time taken by each data entry gathered by that stage."), "aria-hidden" => "true" } .stage-panel-body %nav.stage-nav %ul - %li.stage-nav-item{ ':class' => '{ active: stage.active }', '@click' => 'selectStage(stage)', "v-for" => "stage in state.stages" } - .stage-nav-item-cell.stage-name - {{ stage.title }} - .stage-nav-item-cell.stage-median - %template{ "v-if" => "stage.isUserAllowed" } - %span{ "v-if" => "stage.value" } - {{ stage.value }} - %span.stage-empty{ "v-else" => true } - {{ __('Not enough data') }} - %template{ "v-else" => true } - %span.not-available - {{ __('Not available') }} + %stage-nav-item{ "v-for" => "stage in state.stages", ":key" => '`ca-stage-title-${stage.title}`', '@select' => 'selectStage(stage)', ":title" => "stage.title", ":is-user-allowed" => "stage.isUserAllowed", ":value" => "stage.value", ":is-active" => "stage.active" } .section.stage-events %template{ "v-if" => "isLoadingStage" } = icon("spinner spin") diff --git a/app/views/projects/deployments/_deployment.html.haml b/app/views/projects/deployments/_deployment.html.haml index a11e23b6daa..752be02443c 100644 --- a/app/views/projects/deployments/_deployment.html.haml +++ b/app/views/projects/deployments/_deployment.html.haml @@ -15,10 +15,10 @@ .flex-truncate-child = link_to [@project.namespace.becomes(Namespace), @project, deployment.deployable], class: 'build-link' do #{deployment.deployable.name} (##{deployment.deployable.id}) - - if deployment.user + - if deployment.deployed_by %div by - = user_avatar(user: deployment.user, size: 20, css_class: "mr-0 float-none") + = user_avatar(user: deployment.deployed_by, size: 20, css_class: "mr-0 float-none") .table-section.section-15{ role: 'gridcell' } .table-mobile-header{ role: 'rowheader' }= _("Created") diff --git a/app/views/projects/mirrors/_mirror_repos.html.haml b/app/views/projects/mirrors/_mirror_repos.html.haml index 280ec6d715b..eb100e5cf47 100644 --- a/app/views/projects/mirrors/_mirror_repos.html.haml +++ b/app/views/projects/mirrors/_mirror_repos.html.haml @@ -43,7 +43,8 @@ = _('Mirrored repositories') = render_if_exists 'projects/mirrors/mirrored_repositories_count' %th= _('Direction') - %th= _('Last update') + %th= _('Last update attempt') + %th= _('Last successful update') %th %th %tbody.js-mirrors-table-body @@ -53,6 +54,8 @@ %tr.qa-mirrored-repository-row.rspec-mirrored-repository-row{ class: ('bg-secondary' if mirror.disabled?) } %td.qa-mirror-repository-url= mirror.safe_url %td= _('Push') + %td + = mirror.last_update_started_at.present? ? time_ago_with_tooltip(mirror.last_update_started_at) : _('Never') %td.qa-mirror-last-update-at= mirror.last_update_at.present? ? time_ago_with_tooltip(mirror.last_update_at) : _('Never') %td - if mirror.disabled? diff --git a/app/views/projects/pages_domains/_certificate.html.haml b/app/views/projects/pages_domains/_certificate.html.haml new file mode 100644 index 00000000000..42631fca5e8 --- /dev/null +++ b/app/views/projects/pages_domains/_certificate.html.haml @@ -0,0 +1,18 @@ +- if @domain.auto_ssl_enabled? + - if @domain.enabled? + - if @domain.certificate_text + %pre + = @domain.certificate_text + - else + .bs-callout.bs-callout-info + = _("GitLab is obtaining a Let's Encrypt SSL certificate for this domain. This process can take some time. Please try again later.") + - else + .bs-callout.bs-callout-warning + = _("A Let's Encrypt SSL certificate can not be obtained until your domain is verified.") +- else + - if @domain.certificate_text + %pre + = @domain.certificate_text + - else + .light + = _("missing") diff --git a/app/views/projects/pages_domains/show.html.haml b/app/views/projects/pages_domains/show.html.haml index e9019175219..d0b54946f7e 100644 --- a/app/views/projects/pages_domains/show.html.haml +++ b/app/views/projects/pages_domains/show.html.haml @@ -60,9 +60,4 @@ %td = _("Certificate") %td - - if @domain.certificate_text - %pre - = @domain.certificate_text - - else - .light - = _("missing") + = render 'certificate' diff --git a/app/views/projects/pipelines/_info.html.haml b/app/views/projects/pipelines/_info.html.haml index 2b56ada8b73..53bb3c7487d 100644 --- a/app/views/projects/pipelines/_info.html.haml +++ b/app/views/projects/pipelines/_info.html.haml @@ -21,7 +21,7 @@ .icon-container = sprite_icon('flag') - if @pipeline.latest? - %span.js-pipeline-url-latest.badge.badge-success.has-tooltip{ title: _("Latest pipeline for this branch") } + %span.js-pipeline-url-latest.badge.badge-success.has-tooltip{ title: _("Latest pipeline for the most recent commit on this branch") } latest - if @pipeline.has_yaml_errors? %span.js-pipeline-url-yaml.badge.badge-danger.has-tooltip{ title: @pipeline.yaml_errors } @@ -43,7 +43,7 @@ } } Auto DevOps - if @pipeline.detached_merge_request_pipeline? - %span.js-pipeline-url-mergerequest.badge.badge-info.has-tooltip{ title: "The code of a detached pipeline is tested against the source branch instead of merged results" } + %span.js-pipeline-url-mergerequest.badge.badge-info.has-tooltip{ title: _('Pipelines for merge requests are configured. A detached pipeline runs in the context of the merge request, and not against the merged result. Learn more on the documentation for Pipelines for Merged Results.') } detached - if @pipeline.stuck? %span.js-pipeline-url-stuck.badge.badge-warning diff --git a/app/views/projects/pipelines/_with_tabs.html.haml b/app/views/projects/pipelines/_with_tabs.html.haml index c04f076a3ab..56995ffbcee 100644 --- a/app/views/projects/pipelines/_with_tabs.html.haml +++ b/app/views/projects/pipelines/_with_tabs.html.haml @@ -1,7 +1,7 @@ .tabs-holder %ul.pipelines-tabs.nav-links.no-top.no-bottom.mobile-separator.nav.nav-tabs %li.js-pipeline-tab-link - = link_to project_pipeline_path(@project, @pipeline), data: { target: '#js-tab-pipeline', action: 'pipelines', toggle: 'tab' }, class: 'pipeline-tab' do + = link_to @pipeline_path, data: { target: '#js-tab-pipeline', action: 'pipelines', toggle: 'tab' }, class: 'pipeline-tab' do = _('Pipeline') %li.js-builds-tab-link = link_to builds_project_pipeline_path(@project, @pipeline), data: { target: '#js-tab-builds', action: 'builds', toggle: 'tab' }, class: 'builds-tab' do diff --git a/app/views/projects/project_members/_new_project_member.html.haml b/app/views/projects/project_members/_new_project_member.html.haml index efabb7f7b19..149b0d6cddd 100644 --- a/app/views/projects/project_members/_new_project_member.html.haml +++ b/app/views/projects/project_members/_new_project_member.html.haml @@ -2,7 +2,7 @@ .col-sm-12 = form_for @project_member, as: :project_member, url: project_project_members_path(@project), html: { class: 'users-project-form' } do |f| .form-group - = label_tag :user_ids, _("Select members to invite"), class: "label-bold" + = label_tag :user_ids, _("GitLab member or Email address"), class: "label-bold" = users_select_tag(:user_ids, multiple: true, class: "input-clamp qa-member-select-input", scope: :all, email_user: true, placeholder: "Search for members to update or invite") .form-group = label_tag :access_level, _("Choose a role permission"), class: "label-bold" diff --git a/app/views/projects/serverless/functions/index.html.haml b/app/views/projects/serverless/functions/index.html.haml index 9c69aedfbfc..bac6c76684b 100644 --- a/app/views/projects/serverless/functions/index.html.haml +++ b/app/views/projects/serverless/functions/index.html.haml @@ -14,5 +14,5 @@ .js-serverless-functions-notice .flash-container - .top-area.adjust + .top-area.adjust.d-flex.justify-content-center .serverless-functions-table#js-serverless-functions diff --git a/app/views/projects/update.js.haml b/app/views/projects/update.js.haml index 70f1bf8ef46..c5eecc900b2 100644 --- a/app/views/projects/update.js.haml +++ b/app/views/projects/update.js.haml @@ -4,7 +4,7 @@ location.reload(); - else :plain - $(".project-edit-errors").html("#{escape_javascript(render('errors'))}"); + $(".flash-container").html("#{escape_javascript(render('errors'))}"); $('.save-project-loader').hide(); $('.project-edit-container').show(); $('.edit-project .js-btn-success-general-project-settings').enable(); diff --git a/app/views/projects/wikis/_form.html.haml b/app/views/projects/wikis/_form.html.haml index 66a614b0197..a153f527ee0 100644 --- a/app/views/projects/wikis/_form.html.haml +++ b/app/views/projects/wikis/_form.html.haml @@ -1,8 +1,8 @@ -- commit_message = @page.persisted? ? s_("WikiPageEdit|Update %{page_title}") : s_("WikiPageCreate|Create %{page_title}") -- commit_message = commit_message % { page_title: @page.title } +- form_classes = 'wiki-form common-note-form prepend-top-default js-quick-submit' +- form_classes += ' js-new-wiki-page' unless @page.persisted? = form_for [@project.namespace.becomes(Namespace), @project, @page], method: @page.persisted? ? :put : :post, - html: { class: 'wiki-form common-note-form prepend-top-default js-quick-submit' }, + html: { class: form_classes }, data: { uploads_path: uploads_path } do |f| = form_errors(@page) @@ -12,12 +12,14 @@ .form-group.row .col-sm-12= f.label :title, class: 'control-label-full-width' .col-sm-12 - = 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') + = f.text_field :title, class: 'form-control qa-wiki-title-textbox', value: @page.title, required: true, autofocus: !@page.persisted?, placeholder: _('Wiki|Page title') + %span.d-inline-block.mw-100.prepend-top-5 + = icon('lightbulb-o') + - if @page.persisted? = s_("WikiEditPageTip|Tip: You can move this page by adding the path to the beginning of the title.") = link_to icon('question-circle'), help_page_path('user/project/wiki/index', anchor: 'moving-a-wiki-page'), target: '_blank' + - else + = s_("WikiNewPageTip|Tip: You can specify the full path for the new file. We will automatically create any missing directories.") .form-group.row .col-sm-12= f.label :format, class: 'control-label-full-width' .col-sm-12 @@ -43,7 +45,7 @@ .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 qa-wiki-message-textbox', rows: 18, value: commit_message + .col-sm-12= f.text_field :message, class: 'form-control qa-wiki-message-textbox', rows: 18, value: nil .form-actions - if @page && @page.persisted? diff --git a/app/views/projects/wikis/_main_links.html.haml b/app/views/projects/wikis/_main_links.html.haml index 643b51e01d1..2e1e176c42a 100644 --- a/app/views/projects/wikis/_main_links.html.haml +++ b/app/views/projects/wikis/_main_links.html.haml @@ -1,9 +1,9 @@ - if (@page && @page.persisted?) - if can?(current_user, :create_wiki, @project) - = link_to '#modal-new-wiki', class: "add-new-wiki btn btn-success", "data-toggle" => "modal" do + = link_to project_wikis_new_path(@project), class: "add-new-wiki btn btn-success", role: "button" do = s_("Wiki|New page") - = link_to project_wiki_history_path(@project, @page), class: "btn" do + = link_to project_wiki_history_path(@project, @page), class: "btn", role: "button" do = s_("Wiki|Page history") - if can?(current_user, :create_wiki, @project) && @page.latest? && @valid_encoding - = link_to project_wiki_edit_path(@project, @page), class: "btn js-wiki-edit" do + = link_to project_wiki_edit_path(@project, @page), class: "btn js-wiki-edit", role: "button" do = _("Edit") diff --git a/app/views/projects/wikis/_new.html.haml b/app/views/projects/wikis/_new.html.haml deleted file mode 100644 index dc12e368b35..00000000000 --- a/app/views/projects/wikis/_new.html.haml +++ /dev/null @@ -1,18 +0,0 @@ -#modal-new-wiki.modal - .modal-dialog - .modal-content - .modal-header - %h3.page-title= s_("WikiNewPageTitle|New Wiki Page") - %button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') } - %span{ "aria-hidden": true } × - .modal-body - %form.new-wiki-page - .form-group - = label_tag :new_wiki_path do - %span= s_("WikiPage|Page slug") - = text_field_tag :new_wiki_path, nil, placeholder: s_("WikiNewPagePlaceholder|how-to-setup"), class: 'form-control', required: true, :'data-wikis-path' => project_wikis_path(@project), autofocus: true - %span.new-wiki-page-slug-tip - = icon('lightbulb-o') - = s_("WikiNewPageTip|Tip: You can specify the full path for the new file. We will automatically create any missing directories.") - .form-actions - = button_tag s_("Wiki|Create page"), class: "build-new-wiki btn btn-success" diff --git a/app/views/projects/wikis/_sidebar.html.haml b/app/views/projects/wikis/_sidebar.html.haml index 28353927135..83d145444d8 100644 --- a/app/views/projects/wikis/_sidebar.html.haml +++ b/app/views/projects/wikis/_sidebar.html.haml @@ -1,6 +1,6 @@ %aside.right-sidebar.right-sidebar-expanded.wiki-sidebar.js-wiki-sidebar.js-right-sidebar{ data: { "offset-top" => "50", "spy" => "affix" } } .sidebar-container - .block.wiki-sidebar-header.append-bottom-default + .block.wiki-sidebar-header.append-bottom-default.w-100 %a.gutter-toggle.float-right.d-block.d-sm-block.d-md-none.js-sidebar-wiki-toggle{ href: "#" } = icon('angle-double-right') @@ -10,14 +10,12 @@ %span= _("Clone repository") .blocks-container - .block.block-first + .block.block-first.w-100 - if @sidebar_page = render_wiki_content(@sidebar_page) - else %ul.wiki-pages = render @sidebar_wiki_entries, context: 'sidebar' - .block + .block.w-100 = link_to project_wikis_pages_path(@project), class: 'btn btn-block' do = s_("Wiki|More Pages") - -= render 'projects/wikis/new' diff --git a/app/views/projects/wikis/edit.html.haml b/app/views/projects/wikis/edit.html.haml index 1277ea6c743..9ccf5acfefc 100644 --- a/app/views/projects/wikis/edit.html.haml +++ b/app/views/projects/wikis/edit.html.haml @@ -5,7 +5,7 @@ = wiki_page_errors(@error) -.wiki-page-header.has-sidebar-toggle +.wiki-page-header.top-area.has-sidebar-toggle.flex-column.flex-lg-row %button.btn.btn-default.sidebar-toggle.js-sidebar-wiki-toggle{ role: "button", type: "button" } = icon('angle-double-left') @@ -13,16 +13,13 @@ %h2.wiki-page-title - if @page.persisted? = link_to @page.human_title, project_wiki_path(@project, @page) - - else - = @page.human_title - %span.light - · - - if @page.persisted? + %span.light + · = s_("Wiki|Edit Page") - - else - = s_("Wiki|Create Page") + - else + = s_("Wiki|Create New Page") - .nav-controls + .nav-controls.pb-md-3.pb-lg-0 - if @page.persisted? = link_to project_wiki_history_path(@project, @page), class: "btn" do = s_("Wiki|Page history") diff --git a/app/views/projects/wikis/git_access.html.haml b/app/views/projects/wikis/git_access.html.haml index 8c2cbd495a0..6972eda9bb7 100644 --- a/app/views/projects/wikis/git_access.html.haml +++ b/app/views/projects/wikis/git_access.html.haml @@ -1,15 +1,17 @@ - @content_class = "limit-container-width" unless fluid_layout - page_title s_("WikiClone|Git Access"), _("Wiki") -.wiki-page-header.has-sidebar-toggle +.wiki-page-header.top-area.has-sidebar-toggle.py-3.flex-column.flex-lg-row %button.btn.btn-default.d-block.d-sm-block.d-md-none.float-right.sidebar-toggle.js-sidebar-wiki-toggle{ role: "button", type: "button" } = icon('angle-double-left') - .git-access-header - = _("Clone repository") - %strong= @project_wiki.full_path + .git-access-header.w-100.d-flex.flex-column.justify-content-center + %span + = _("Clone repository") + %strong= @project_wiki.full_path - = render "shared/clone_panel", project: @project_wiki + .pt-3.pt-lg-0.w-100 + = render "shared/clone_panel", project: @project_wiki .wiki-git-access %h3= s_("WikiClone|Install Gollum") diff --git a/app/views/projects/wikis/history.html.haml b/app/views/projects/wikis/history.html.haml index c5fbeeafa54..d3a55c53649 100644 --- a/app/views/projects/wikis/history.html.haml +++ b/app/views/projects/wikis/history.html.haml @@ -1,6 +1,6 @@ - page_title _("History"), @page.human_title, _("Wiki") -.wiki-page-header.has-sidebar-toggle +.wiki-page-header.top-area.has-sidebar-toggle.flex-column.flex-lg-row %button.btn.btn-default.sidebar-toggle.js-sidebar-wiki-toggle{ role: "button", type: "button" } = icon('angle-double-left') diff --git a/app/views/projects/wikis/pages.html.haml b/app/views/projects/wikis/pages.html.haml index 2191e5ab287..275dc5dbd23 100644 --- a/app/views/projects/wikis/pages.html.haml +++ b/app/views/projects/wikis/pages.html.haml @@ -5,13 +5,13 @@ - sort_title = wiki_sort_title(params[:sort]) %div{ class: container_class } - .wiki-page-header + .wiki-page-header.top-area.flex-column.flex-lg-row .nav-text.flex-fill %h2.wiki-page-title = s_("Wiki|Wiki Pages") - .nav-controls + .nav-controls.pb-md-3.pb-lg-0 = link_to project_wikis_git_access_path(@project), class: 'btn' do = icon('cloud-download') = _("Clone repository") diff --git a/app/views/projects/wikis/show.html.haml b/app/views/projects/wikis/show.html.haml index 95cd3356ec8..c6197fe576e 100644 --- a/app/views/projects/wikis/show.html.haml +++ b/app/views/projects/wikis/show.html.haml @@ -4,7 +4,7 @@ - page_title @page.human_title, _("Wiki") - add_to_breadcrumbs _("Wiki"), project_wiki_path(@project, :home) -.wiki-page-header.has-sidebar-toggle +.wiki-page-header.top-area.has-sidebar-toggle.flex-column.flex-lg-row %button.btn.btn-default.sidebar-toggle.js-sidebar-wiki-toggle{ role: "button", type: "button" } = icon('angle-double-left') @@ -15,7 +15,7 @@ = (_("Last edited by %{name}") % { name: "<strong>#{@page.last_version.author_name}</strong>" }).html_safe #{time_ago_with_tooltip(@page.last_version.authored_date)} - .nav-controls + .nav-controls.pb-md-3.pb-lg-0 = render 'main_links' - if @page.historical? diff --git a/app/views/search/_category.html.haml b/app/views/search/_category.html.haml index 18613ff4c16..eae2a491ceb 100644 --- a/app/views/search/_category.html.haml +++ b/app/views/search/_category.html.haml @@ -1,10 +1,6 @@ - users = capture_haml do - if search_tabs?(:members) - %li{ class: active_when(@scope == 'users') } - = link_to search_filter_path(scope: 'users') do - Users - %span.badge.badge-pill - = limited_count(@search_results.limited_users_count) + = search_filter_link 'users', _("Users") .scrolling-tabs-container.inner-page-scroll-tabs.is-smaller .fade-left= icon('angle-left') @@ -12,80 +8,28 @@ %ul.nav-links.search-filter.scrolling-tabs.nav.nav-tabs - if @project - if project_search_tabs?(:blobs) - %li{ class: active_when(@scope == 'blobs'), data: { qa_selector: 'code_tab' } } - = link_to search_filter_path(scope: 'blobs') do - = _("Code") - %span.badge.badge-pill - = @search_results.blobs_count + = search_filter_link 'blobs', _("Code"), data: { qa_selector: 'code_tab' } - if project_search_tabs?(:issues) - %li{ class: active_when(@scope == 'issues') } - = link_to search_filter_path(scope: 'issues') do - = _("Issues") - %span.badge.badge-pill - = limited_count(@search_results.limited_issues_count) + = search_filter_link 'issues', _("Issues") - if project_search_tabs?(:merge_requests) - %li{ class: active_when(@scope == 'merge_requests') } - = link_to search_filter_path(scope: 'merge_requests') do - = _("Merge requests") - %span.badge.badge-pill - = limited_count(@search_results.limited_merge_requests_count) + = search_filter_link 'merge_requests', _("Merge requests") - if project_search_tabs?(:milestones) - %li{ class: active_when(@scope == 'milestones') } - = link_to search_filter_path(scope: 'milestones') do - = _("Milestones") - %span.badge.badge-pill - = limited_count(@search_results.limited_milestones_count) + = search_filter_link 'milestones', _("Milestones") - if project_search_tabs?(:notes) - %li{ class: active_when(@scope == 'notes') } - = link_to search_filter_path(scope: 'notes') do - = _("Comments") - %span.badge.badge-pill - = limited_count(@search_results.limited_notes_count) + = search_filter_link 'notes', _("Comments") - if project_search_tabs?(:wiki) - %li{ class: active_when(@scope == 'wiki_blobs') } - = link_to search_filter_path(scope: 'wiki_blobs') do - = _("Wiki") - %span.badge.badge-pill - = @search_results.wiki_blobs_count + = search_filter_link 'wiki_blobs', _("Wiki") - if project_search_tabs?(:commits) - %li{ class: active_when(@scope == 'commits') } - = link_to search_filter_path(scope: 'commits') do - = _("Commits") - %span.badge.badge-pill - = @search_results.commits_count + = search_filter_link 'commits', _("Commits") = users - 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") - %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") - %span.badge.badge-pill - = @search_results.snippet_titles_count + = search_filter_link 'snippet_blobs', _("Snippet Contents"), search: { snippets: true, group_id: nil, project_id: nil } + = search_filter_link 'snippet_titles', _("Titles and Filenames"), search: { snippets: true, group_id: nil, project_id: nil } - else - %li{ class: active_when(@scope == 'projects') } - = link_to search_filter_path(scope: 'projects') do - = _("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") - %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") - %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") - %span.badge.badge-pill - = limited_count(@search_results.limited_milestones_count) + = search_filter_link 'projects', _("Projects") + = search_filter_link 'issues', _("Issues") + = search_filter_link 'merge_requests', _("Merge requests") + = search_filter_link 'milestones', _("Milestones") = render_if_exists 'search/category_elasticsearch' = users diff --git a/app/views/shared/boards/_show.html.haml b/app/views/shared/boards/_show.html.haml index 1be230eedb9..4f0be117035 100644 --- a/app/views/shared/boards/_show.html.haml +++ b/app/views/shared/boards/_show.html.haml @@ -16,7 +16,7 @@ #board-app.boards-app.position-relative{ "v-cloak" => "true", data: board_data, ":class" => "{ 'is-compact': detailIssueVisible }" } = render 'shared/issuable/search_bar', type: :boards, board: board - .boards-list.w-100.py-3.px-2.text-nowrap + .boards-list.w-100.py-3.px-2.text-nowrap{ data: { qa_selector: "boards_list" } } .boards-app-loading.w-100.text-center{ "v-if" => "loading" } = icon("spinner spin 2x") %board{ "v-cloak" => "true", diff --git a/app/views/shared/boards/components/_board.html.haml b/app/views/shared/boards/components/_board.html.haml index 6c0613605eb..ffa24d1c041 100644 --- a/app/views/shared/boards/components/_board.html.haml +++ b/app/views/shared/boards/components/_board.html.haml @@ -1,7 +1,7 @@ -.board.d-inline-block.h-100.px-2.align-top.ws-normal{ ":class" => '{ "is-draggable": !list.preset, "is-expandable": list.isExpandable, "is-collapsed": !list.isExpanded, "board-type-assignee": list.type === "assignee" }', - ":data-id" => "list.id" } +.board.h-100.px-2.align-top.ws-normal{ ":class" => '{ "is-draggable": !list.preset, "is-expandable": list.isExpandable, "is-collapsed": !list.isExpanded, "board-type-assignee": list.type === "assignee" }', + ":data-id" => "list.id", data: { qa_selector: "board_list" } } .board-inner.d-flex.flex-column.position-relative.h-100.rounded - %header.board-header{ ":class" => '{ "has-border": list.label && list.label.color, "position-relative": list.isExpanded, "position-absolute position-top-0 position-left-0 w-100 h-100": !list.isExpanded }', ":style" => "{ borderTopColor: (list.label && list.label.color ? list.label.color : null) }" } + %header.board-header{ ":class" => '{ "has-border": list.label && list.label.color, "position-relative": list.isExpanded, "position-absolute position-top-0 position-left-0 w-100 h-100": !list.isExpanded }', ":style" => "{ borderTopColor: (list.label && list.label.color ? list.label.color : null) }", data: { qa_selector: "board_list_header" } } %h3.board-title.m-0.d-flex.js-board-handle{ ":class" => '{ "user-can-drag": (!disabled && !list.preset), "border-bottom-0": !list.isExpanded }' } .board-title-caret.no-drag{ "v-if": "list.isExpandable", diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index b4f8377c008..837707707a9 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -38,7 +38,7 @@ = _('Milestone') = icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true') - if can_edit_issuable - = link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link float-right' + = link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link float-right', data: { track_label: "right_sidebar", track_property: "milestone", track_event: "click_edit_button", track_value: "" } .value.hide-collapsed - if milestone.present? = link_to milestone[:title], milestone[:web_url], class: "bold has-tooltip", title: sidebar_milestone_remaining_days(milestone), data: { container: "body", html: 'true', boundary: 'viewport' } @@ -66,7 +66,7 @@ = _('Due date') = icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true') - if can_edit_issuable - = link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link float-right' + = link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link float-right', data: { track_label: "right_sidebar", track_property: "due_date", track_event: "click_edit_button", track_value: "" } .value.hide-collapsed %span.value-content - if issuable_sidebar[:due_date] @@ -102,7 +102,7 @@ = _('Labels') = icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true') - if can_edit_issuable - = link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link qa-edit-link-labels float-right' + = link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link qa-edit-link-labels float-right', data: { track_label: "right_sidebar", track_property: "labels", track_event: "click_edit_button", track_value: "" } .value.issuable-show-labels.dont-hide.hide-collapsed.qa-labels-block{ class: ("has-labels" if selected_labels.any?) } - if selected_labels.any? - selected_labels.each do |label_hash| @@ -137,7 +137,13 @@ .js-sidebar-participants-entry-point - if signed_in - .js-sidebar-subscriptions-entry-point + - if issuable_sidebar[:project_emails_disabled] + .block.js-emails-disabled + .sidebar-collapsed-icon.has-tooltip{ title: notification_description(:owner_disabled), data: { placement: "left", container: "body", boundary: 'viewport' } } + = notification_setting_icon + .hide-collapsed= notification_description(:owner_disabled) + - else + .js-sidebar-subscriptions-entry-point - project_ref = issuable_sidebar[:reference] .block.project-reference @@ -156,7 +162,7 @@ = custom_icon('icon_arrow_right') .dropdown.sidebar-move-issue-dropdown.hide-collapsed %button.btn.btn-default.btn-block.js-sidebar-dropdown-toggle.js-move-issue{ type: 'button', - data: { toggle: 'dropdown', display: 'static' } } + data: { toggle: 'dropdown', display: 'static', track_label: "right_sidebar", track_property: "move_issue", track_event: "click_button", track_value: "" } } = _('Move issue') .dropdown-menu.dropdown-menu-selectable.dropdown-extended-height = dropdown_title(_('Move issue')) diff --git a/app/views/shared/issuable/_sidebar_assignees.html.haml b/app/views/shared/issuable/_sidebar_assignees.html.haml index ab01094ed6e..1dc538826dc 100644 --- a/app/views/shared/issuable/_sidebar_assignees.html.haml +++ b/app/views/shared/issuable/_sidebar_assignees.html.haml @@ -20,6 +20,8 @@ placeholder: _('Search users'), data: { first_user: issuable_sidebar.dig(:current_user, :username), current_user: true, + iid: issuable_sidebar[:iid], + issuable_type: issuable_type, project_id: issuable_sidebar[:project_id], author_id: issuable_sidebar[:author_id], field_name: "#{issuable_type}[assignee_ids][]", diff --git a/app/views/shared/members/_group.html.haml b/app/views/shared/members/_group.html.haml index e83ca5eaab8..42a823e3a8d 100644 --- a/app/views/shared/members/_group.html.haml +++ b/app/views/shared/members/_group.html.haml @@ -32,7 +32,7 @@ %ul - Gitlab::Access.options.each do |role, role_id| %li - = link_to role, "javascript:void(0)", + = link_to role, '#', class: ("is-active" if group_link.group_access == role_id), data: { id: role_id, el_id: dom_id } .clearable-input.member-form-control.d-sm-inline-block diff --git a/app/views/shared/members/_member.html.haml b/app/views/shared/members/_member.html.haml index 331283f7eec..6762f211a80 100644 --- a/app/views/shared/members/_member.html.haml +++ b/app/views/shared/members/_member.html.haml @@ -82,7 +82,7 @@ %ul - member.valid_level_roles.each do |role, role_id| %li - = link_to role, "javascript:void(0)", + = link_to role, '#', class: ("is-active" if member.access_level == role_id), data: { id: role_id, el_id: dom_id(member) } = render_if_exists 'shared/members/ee/revert_ldap_group_sync_option', diff --git a/app/views/shared/notifications/_button.html.haml b/app/views/shared/notifications/_button.html.haml index 749aa258af6..b4266937a4e 100644 --- a/app/views/shared/notifications/_button.html.haml +++ b/app/views/shared/notifications/_button.html.haml @@ -1,6 +1,15 @@ -- btn_class = local_assigns.fetch(:btn_class, nil) +- btn_class = local_assigns.fetch(:btn_class, '') +- emails_disabled = local_assigns.fetch(:emails_disabled, false) - if notification_setting + - if emails_disabled + - button_title = notification_description(:owner_disabled) + - aria_label = button_title + - btn_class << " disabled" + - else + - button_title = _("Notification setting") + - aria_label = _("Notification setting - %{notification_title}") % { notification_title: notification_title(notification_setting.level) } + .js-notification-dropdown.notification-dropdown.mr-md-2.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) @@ -8,14 +17,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.text-left#notifications-button{ type: "button", title: _("Notification setting"), 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.text-left#notifications-button{ type: "button", title: button_title, class: "#{btn_class}", "aria-label" => aria_label, data: { container: "body", toggle: "modal", target: "#" + notifications_menu_identifier("modal", notification_setting), display: 'static' } } = icon("bell", class: "js-notification-loading") = notification_title(notification_setting.level) %button.btn.dropdown-toggle{ data: { toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting), flip: "false" } } = icon('caret-down') .sr-only Toggle dropdown - else - %button.dropdown-new.btn.btn-default.has-tooltip.notifications-btn#notifications-button{ type: "button", title: _("Notification setting"), class: "#{btn_class}", "aria-label" => _("Notification setting - %{notification_title}") % { notification_title: 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: button_title, class: "#{btn_class}", "aria-label" => aria_label, data: { container: "body", toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting), flip: "false" } } .float-left = icon("bell", class: "js-notification-loading") = notification_title(notification_setting.level) diff --git a/app/views/shared/notifications/_new_button.html.haml b/app/views/shared/notifications/_new_button.html.haml index 052e6da5bae..3c8cc023848 100644 --- a/app/views/shared/notifications/_new_button.html.haml +++ b/app/views/shared/notifications/_new_button.html.haml @@ -1,6 +1,13 @@ -- btn_class = local_assigns.fetch(:btn_class, nil) +- btn_class = local_assigns.fetch(:btn_class, '') +- emails_disabled = local_assigns.fetch(:emails_disabled, false) - if notification_setting + - if emails_disabled + - button_title = notification_description(:owner_disabled) + - btn_class << " disabled" + - else + - button_title = _("Notification setting - %{notification_title}") % { notification_title: notification_title(notification_setting.level) } + .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) @@ -9,14 +16,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", placement: 'top', 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: button_title, class: "#{btn_class}", "aria-label" => button_title, data: { container: "body", placement: 'top', toggle: "modal", target: "#" + notifications_menu_identifier("modal", notification_setting), display: 'static' } } = notification_setting_icon(notification_setting) %span.js-notification-loading.fa.hidden %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_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: "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: button_title, class: "#{btn_class}", "aria-label" => button_title, data: { container: "body", placement: 'top', toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting), flip: "false" } } = notification_setting_icon(notification_setting) %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 b7474d891dc..573ed36d7f4 100644 --- a/app/views/shared/projects/_project.html.haml +++ b/app/views/shared/projects/_project.html.haml @@ -89,7 +89,7 @@ - if pipeline_status && can?(current_user, :read_cross_project) && project.pipeline_status.has_status? && can?(current_user, :read_build, project) - pipeline_path = pipelines_project_commit_path(project.pipeline_status.project, project.pipeline_status.sha, ref: project.pipeline_status.ref) %span.icon-wrapper.pipeline-status - = render 'ci/status/icon', status: project.commit.last_pipeline.detailed_status(current_user), type: 'commit', tooltip_placement: 'top', path: pipeline_path + = render 'ci/status/icon', status: project.commit.last_pipeline.detailed_status(current_user), tooltip_placement: 'top', path: pipeline_path .updated-note %span = _('Updated') diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml index 400becdd023..991a177018e 100644 --- a/app/workers/all_queues.yml +++ b/app/workers/all_queues.yml @@ -88,7 +88,6 @@ - pipeline_processing:ci_build_prepare - pipeline_processing:build_queue - pipeline_processing:build_success -- pipeline_processing:build_process - pipeline_processing:pipeline_process - pipeline_processing:pipeline_success - pipeline_processing:pipeline_update diff --git a/app/workers/build_process_worker.rb b/app/workers/build_process_worker.rb deleted file mode 100644 index 9cd9519df1f..00000000000 --- a/app/workers/build_process_worker.rb +++ /dev/null @@ -1,16 +0,0 @@ -# frozen_string_literal: true - -class BuildProcessWorker - include ApplicationWorker - include PipelineQueue - - queue_namespace :pipeline_processing - - # rubocop: disable CodeReuse/ActiveRecord - def perform(build_id) - CommitStatus.find_by(id: build_id).try do |build| - build.pipeline.process!([build_id]) - end - end - # rubocop: enable CodeReuse/ActiveRecord -end diff --git a/app/workers/ci/archive_traces_cron_worker.rb b/app/workers/ci/archive_traces_cron_worker.rb index 75e68d0233a..ef2da729705 100644 --- a/app/workers/ci/archive_traces_cron_worker.rb +++ b/app/workers/ci/archive_traces_cron_worker.rb @@ -10,7 +10,7 @@ module Ci # Archive stale live traces which still resides in redis or database # This could happen when ArchiveTraceWorker sidekiq jobs were lost by receiving SIGKILL # More details in https://gitlab.com/gitlab-org/gitlab-ce/issues/36791 - Ci::Build.finished.with_live_trace.find_each(batch_size: 100) do |build| + Ci::Build.with_stale_live_trace.find_each(batch_size: 100) do |build| Ci::ArchiveTraceService.new.execute(build, worker_name: self.class.name) end end diff --git a/app/workers/git_garbage_collect_worker.rb b/app/workers/git_garbage_collect_worker.rb index 489d6215774..5499e12e49b 100644 --- a/app/workers/git_garbage_collect_worker.rb +++ b/app/workers/git_garbage_collect_worker.rb @@ -24,7 +24,7 @@ class GitGarbageCollectWorker task = task.to_sym - ::Projects::GitDeduplicationService.new(project).execute + ::Projects::GitDeduplicationService.new(project).execute if task == :gc gitaly_call(task, project.repository.raw_repository) diff --git a/app/workers/post_receive.rb b/app/workers/post_receive.rb index dba7837bd12..61d34981458 100644 --- a/app/workers/post_receive.rb +++ b/app/workers/post_receive.rb @@ -42,6 +42,9 @@ class PostReceive user = identify_user(post_received) return false unless user + # We only need to expire certain caches once per push + expire_caches(post_received) + post_received.enum_for(:changes_refs).with_index do |(oldrev, newrev, ref), index| service_klass = if Gitlab::Git.tag_ref?(ref) @@ -69,9 +72,34 @@ class PostReceive after_project_changes_hooks(post_received, user, refs.to_a, changes) end + # Expire the project, branch, and tag cache once per push. Schedule an + # update for the repository size and commit count if necessary. + def expire_caches(post_received) + project = post_received.project + + project.repository.expire_status_cache if project.empty_repo? + project.repository.expire_branches_cache if post_received.includes_branches? + project.repository.expire_caches_for_tags if post_received.includes_tags? + + enqueue_repository_cache_update(post_received) + end + + def enqueue_repository_cache_update(post_received) + stats_to_invalidate = [:repository_size] + stats_to_invalidate << :commit_count if post_received.includes_default_branch? + + ProjectCacheWorker.perform_async( + post_received.project.id, + [], + stats_to_invalidate, + true + ) + end + def after_project_changes_hooks(post_received, user, refs, changes) hook_data = Gitlab::DataBuilder::Repository.update(post_received.project, user, changes, refs) SystemHooksService.new.execute_hooks(hook_data, :repository_update_hooks) + Gitlab::UsageDataCounters::SourceCodeCounter.count(:pushes) end def process_wiki_changes(post_received) diff --git a/app/workers/process_commit_worker.rb b/app/workers/process_commit_worker.rb index 3efb5343a96..f6ebe4ab006 100644 --- a/app/workers/process_commit_worker.rb +++ b/app/workers/process_commit_worker.rb @@ -2,7 +2,8 @@ # Worker for processing individual commit messages pushed to a repository. # -# Jobs for this worker are scheduled for every commit that is being pushed. As a +# Jobs for this worker are scheduled for every commit that contains mentionable +# references in its message and does not exist in the upstream project. As a # result of this the workload of this worker should be kept to a bare minimum. # Consider using an extra worker if you need to add any extra (and potentially # slow) processing of commits. @@ -19,7 +20,6 @@ class ProcessCommitWorker project = Project.find_by(id: project_id) return unless project - return if commit_exists_in_upstream?(project, commit_hash) user = User.find_by(id: user_id) @@ -77,17 +77,4 @@ class ProcessCommitWorker Commit.from_hash(hash, project) end - - private - - # Avoid reprocessing commits that already exist in the upstream - # when project is forked. This will also prevent duplicated system notes. - def commit_exists_in_upstream?(project, commit_hash) - upstream_project = project.fork_source - - return false unless upstream_project - - commit_id = commit_hash.with_indifferent_access[:id] - upstream_project.commit(commit_id).present? - end end diff --git a/app/workers/project_cache_worker.rb b/app/workers/project_cache_worker.rb index 4e8ea903139..5ac860c93e0 100644 --- a/app/workers/project_cache_worker.rb +++ b/app/workers/project_cache_worker.rb @@ -12,13 +12,15 @@ class ProjectCacheWorker # CHANGELOG. # statistics - An Array containing columns from ProjectStatistics to # refresh, if empty all columns will be refreshed + # refresh_statistics - A boolean that determines whether project statistics should + # be updated. # rubocop: disable CodeReuse/ActiveRecord - def perform(project_id, files = [], statistics = []) + def perform(project_id, files = [], statistics = [], refresh_statistics = true) project = Project.find_by(id: project_id) return unless project - update_statistics(project, statistics) + update_statistics(project, statistics) if refresh_statistics return unless project.repository.exists? diff --git a/app/workers/remote_mirror_notification_worker.rb b/app/workers/remote_mirror_notification_worker.rb index 5bafe8e2046..368abfeda99 100644 --- a/app/workers/remote_mirror_notification_worker.rb +++ b/app/workers/remote_mirror_notification_worker.rb @@ -4,7 +4,7 @@ class RemoteMirrorNotificationWorker include ApplicationWorker def perform(remote_mirror_id) - remote_mirror = RemoteMirrorFinder.new(id: remote_mirror_id).execute + remote_mirror = RemoteMirror.find_by_id(remote_mirror_id) # We check again if there's an error because a newer run since this job was # fired could've completed successfully. diff --git a/app/workers/repository_update_remote_mirror_worker.rb b/app/workers/repository_update_remote_mirror_worker.rb index 03a7ff2cd7a..d13c7641eb3 100644 --- a/app/workers/repository_update_remote_mirror_worker.rb +++ b/app/workers/repository_update_remote_mirror_worker.rb @@ -1,50 +1,53 @@ # frozen_string_literal: true class RepositoryUpdateRemoteMirrorWorker - UpdateAlreadyInProgressError = Class.new(StandardError) UpdateError = Class.new(StandardError) include ApplicationWorker + include Gitlab::ExclusiveLeaseHelpers sidekiq_options retry: 3, dead: false - sidekiq_retry_in { |count| 30 * count } + LOCK_WAIT_TIME = 30.seconds + MAX_TRIES = 3 - sidekiq_retries_exhausted do |msg, _| - Sidekiq.logger.warn "Failed #{msg['class']} with #{msg['args']}: #{msg['error_message']}" - end - - def perform(remote_mirror_id, scheduled_time) - remote_mirror = RemoteMirrorFinder.new(id: remote_mirror_id).execute + def perform(remote_mirror_id, scheduled_time, tries = 0) + remote_mirror = RemoteMirror.find_by_id(remote_mirror_id) + return unless remote_mirror return if remote_mirror.updated_since?(scheduled_time) - raise UpdateAlreadyInProgressError if remote_mirror.update_in_progress? + # If the update is already running, wait for it to finish before running again + # This will wait for a total of 90 seconds in 3 steps + in_lock(remote_mirror_update_lock(remote_mirror.id), + retries: 3, + ttl: remote_mirror.max_runtime, + sleep_sec: LOCK_WAIT_TIME) do + update_mirror(remote_mirror, scheduled_time, tries) + end + rescue Gitlab::ExclusiveLeaseHelpers::FailedToObtainLockError + # If an update runs longer than 1.5 minutes, we'll reschedule it + # with a backoff. The next run will check if the previous update would + # include the changes that triggered this update and become a no-op. + self.class.perform_in(remote_mirror.backoff_delay, remote_mirror.id, scheduled_time, tries) + end - remote_mirror.update_start + private - project = remote_mirror.project + def update_mirror(mirror, scheduled_time, tries) + project = mirror.project current_user = project.creator - result = Projects::UpdateRemoteMirrorService.new(project, current_user).execute(remote_mirror) - raise UpdateError, result[:message] if result[:status] == :error - - remote_mirror.update_finish - rescue UpdateAlreadyInProgressError - raise - rescue UpdateError => ex - fail_remote_mirror(remote_mirror, ex.message) - raise - rescue => ex - return unless remote_mirror + result = Projects::UpdateRemoteMirrorService.new(project, current_user).execute(mirror, tries) - fail_remote_mirror(remote_mirror, ex.message) - raise UpdateError, "#{ex.class}: #{ex.message}" + if result[:status] == :error && mirror.to_retry? + schedule_retry(mirror, scheduled_time, tries) + end end - private - - def fail_remote_mirror(remote_mirror, message) - remote_mirror.mark_as_failed(message) + def remote_mirror_update_lock(mirror_id) + [self.class.name, mirror_id].join(':') + end - Rails.logger.error(message) # rubocop:disable Gitlab/RailsLogger + def schedule_retry(mirror, scheduled_time, tries) + self.class.perform_in(mirror.backoff_delay, mirror.id, scheduled_time, tries + 1) end end diff --git a/bin/changelog b/bin/changelog index 328d9495b96..ec068d06507 100755 --- a/bin/changelog +++ b/bin/changelog @@ -23,7 +23,7 @@ module ChangelogHelpers Abort = Class.new(StandardError) Done = Class.new(StandardError) - MAX_FILENAME_LENGTH = 140 # ecryptfs has a limit of 140 characters + MAX_FILENAME_LENGTH = 99 # GNU tar has a 99 character limit def capture_stdout(cmd) output = IO.popen(cmd, &:read) diff --git a/changelogs/unreleased/11090-export-design-management-lfs-data.yml b/changelogs/unreleased/11090-export-design-management-lfs-data.yml deleted file mode 100644 index 36b773124d7..00000000000 --- a/changelogs/unreleased/11090-export-design-management-lfs-data.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Add support for exporting repository type data for LFS objects -merge_request: 30830 -author: -type: changed diff --git a/changelogs/unreleased/17276-breakage-in-displaying-svg-in-the-same-repository.yml b/changelogs/unreleased/17276-breakage-in-displaying-svg-in-the-same-repository.yml deleted file mode 100644 index 93936d441e7..00000000000 --- a/changelogs/unreleased/17276-breakage-in-displaying-svg-in-the-same-repository.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix inline rendering of relative paths to SVGs from the current repository -merge_request: 31352 -author: -type: fixed diff --git a/changelogs/unreleased/19186-redirect-wiki-git-route-to-wiki.yml b/changelogs/unreleased/19186-redirect-wiki-git-route-to-wiki.yml deleted file mode 100644 index 705621d06f7..00000000000 --- a/changelogs/unreleased/19186-redirect-wiki-git-route-to-wiki.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Redirect from a project wiki git route to the project wiki home -merge_request: 31085 -author: -type: added diff --git a/changelogs/unreleased/20137-starrers.yml b/changelogs/unreleased/20137-starrers.yml deleted file mode 100644 index d597b06f224..00000000000 --- a/changelogs/unreleased/20137-starrers.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Make starred projects and starrers of a project publicly visible -merge_request: 24690 -author: -type: added diff --git a/changelogs/unreleased/21671-multiple-pipeline-status-api.yml b/changelogs/unreleased/21671-multiple-pipeline-status-api.yml deleted file mode 100644 index b7b0f5fa0c7..00000000000 --- a/changelogs/unreleased/21671-multiple-pipeline-status-api.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Multiple pipeline support for Commit status -merge_request: 30828 -author: Gaetan Semet -type: changed diff --git a/changelogs/unreleased/26866-api-endpoint-to-list-the-docker-images-tags-of-a-group.yml b/changelogs/unreleased/26866-api-endpoint-to-list-the-docker-images-tags-of-a-group.yml deleted file mode 100644 index adbd7971a14..00000000000 --- a/changelogs/unreleased/26866-api-endpoint-to-list-the-docker-images-tags-of-a-group.yml +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: Add API endpoints to return container repositories and tags from the group - level -merge_request: 30817 -author: -type: added diff --git a/changelogs/unreleased/30974-issue-search-by-number.yml b/changelogs/unreleased/30974-issue-search-by-number.yml deleted file mode 100644 index da50ee32c83..00000000000 --- a/changelogs/unreleased/30974-issue-search-by-number.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: "Search issuables by iids" -merge_request: 28302 -author: Riccardo Padovani -type: fixed diff --git a/changelogs/unreleased/31434-make-issue-boards-importable.yml b/changelogs/unreleased/31434-make-issue-boards-importable.yml deleted file mode 100644 index fd270a236dc..00000000000 --- a/changelogs/unreleased/31434-make-issue-boards-importable.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Make issue boards importable -merge_request: 31434 -author: Jason Colyer -type: changed diff --git a/changelogs/unreleased/32032-html-code-shown-in-merge-request.yml b/changelogs/unreleased/32032-html-code-shown-in-merge-request.yml new file mode 100644 index 00000000000..ffd58067784 --- /dev/null +++ b/changelogs/unreleased/32032-html-code-shown-in-merge-request.yml @@ -0,0 +1,5 @@ +--- +title: Fix HTML rendering for fast-forward rebases in merge request widget +merge_request: 32032 +author: +type: fixed diff --git a/changelogs/unreleased/32495-improve-slack-notification-on-pipeline-status.yml b/changelogs/unreleased/32495-improve-slack-notification-on-pipeline-status.yml deleted file mode 100644 index b7b39303c2e..00000000000 --- a/changelogs/unreleased/32495-improve-slack-notification-on-pipeline-status.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Improve pipeline status Slack notifications -merge_request: 27683 -author: -type: added diff --git a/changelogs/unreleased/39217-remove-kubernetes-service-integration.yml b/changelogs/unreleased/39217-remove-kubernetes-service-integration.yml deleted file mode 100644 index e13e3e86a37..00000000000 --- a/changelogs/unreleased/39217-remove-kubernetes-service-integration.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Remove Kubernetes service integration page -merge_request: 31365 -author: -type: removed diff --git a/changelogs/unreleased/43080-speed-up-deploy-keys.yml b/changelogs/unreleased/43080-speed-up-deploy-keys.yml deleted file mode 100644 index 73c9a9e5f82..00000000000 --- a/changelogs/unreleased/43080-speed-up-deploy-keys.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Speed up loading and filtering deploy keys and their projects -merge_request: 31384 -author: -type: performance diff --git a/changelogs/unreleased/44036-someone-edited-the-issue-at-the-same-time-is-regularly-seen-despite-that-being-a-filthy-lie.yml b/changelogs/unreleased/44036-someone-edited-the-issue-at-the-same-time-is-regularly-seen-despite-that-being-a-filthy-lie.yml deleted file mode 100644 index 674d53286e6..00000000000 --- a/changelogs/unreleased/44036-someone-edited-the-issue-at-the-same-time-is-regularly-seen-despite-that-being-a-filthy-lie.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix flashing conflict warning when editing issues -merge_request: 31469 -author: -type: fixed diff --git a/changelogs/unreleased/46299-wiki-page-creation.yml b/changelogs/unreleased/46299-wiki-page-creation.yml new file mode 100644 index 00000000000..2e8f2accf45 --- /dev/null +++ b/changelogs/unreleased/46299-wiki-page-creation.yml @@ -0,0 +1,5 @@ +--- +title: Remove wiki page slug dialog step when creating wiki page +merge_request: 31362 +author: +type: changed diff --git a/changelogs/unreleased/47814-search-view-labels.yml b/changelogs/unreleased/47814-search-view-labels.yml deleted file mode 100644 index b4f10150d13..00000000000 --- a/changelogs/unreleased/47814-search-view-labels.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Moved labels out of fields on Search page -merge_request: 31137 -author: -type: fixed diff --git a/changelogs/unreleased/48717-rate-limit-raw-controller-show.yml b/changelogs/unreleased/48717-rate-limit-raw-controller-show.yml deleted file mode 100644 index 38ee95a7553..00000000000 --- a/changelogs/unreleased/48717-rate-limit-raw-controller-show.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Add Rate Request Limiter to RawController#show endpoint -merge_request: 30635 -author: -type: added diff --git a/changelogs/unreleased/49392-exempt-jwt-auth-for-user-gitlab-ci-token-from-rate-limiting.yml b/changelogs/unreleased/49392-exempt-jwt-auth-for-user-gitlab-ci-token-from-rate-limiting.yml new file mode 100644 index 00000000000..3ce96e64736 --- /dev/null +++ b/changelogs/unreleased/49392-exempt-jwt-auth-for-user-gitlab-ci-token-from-rate-limiting.yml @@ -0,0 +1,5 @@ +--- +title: Exempt user gitlab-ci-token from rate limiting +merge_request: 31909 +author: +type: fixed diff --git a/changelogs/unreleased/50130-cluster-cluster-details-update-automatically-after-cluster-is-created.yml b/changelogs/unreleased/50130-cluster-cluster-details-update-automatically-after-cluster-is-created.yml deleted file mode 100644 index dc718572cfb..00000000000 --- a/changelogs/unreleased/50130-cluster-cluster-details-update-automatically-after-cluster-is-created.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Update cluster page automatically when cluster is created -merge_request: 27189 -author: -type: changed diff --git a/changelogs/unreleased/51470-webide-default-commit.yml b/changelogs/unreleased/51470-webide-default-commit.yml new file mode 100644 index 00000000000..d64111e7ec3 --- /dev/null +++ b/changelogs/unreleased/51470-webide-default-commit.yml @@ -0,0 +1,5 @@ +--- +title: Updated WebIDE default commit options +merge_request: 31449 +author: +type: changed diff --git a/changelogs/unreleased/52494-separate-namespace-per-project-environment-slug.yml b/changelogs/unreleased/52494-separate-namespace-per-project-environment-slug.yml deleted file mode 100644 index 645c92127a3..00000000000 --- a/changelogs/unreleased/52494-separate-namespace-per-project-environment-slug.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Use separate Kubernetes namespaces per environment -merge_request: 30711 -author: -type: added diff --git a/changelogs/unreleased/55360-redundant-index-in-the-releases-table_v2.yml b/changelogs/unreleased/55360-redundant-index-in-the-releases-table_v2.yml new file mode 100644 index 00000000000..91a1fb5e6c9 --- /dev/null +++ b/changelogs/unreleased/55360-redundant-index-in-the-releases-table_v2.yml @@ -0,0 +1,5 @@ +--- +title: Removed redundant index on releases table +merge_request: 31487 +author: +type: removed diff --git a/changelogs/unreleased/55564-remove-if-in-before-after-action.yml b/changelogs/unreleased/55564-remove-if-in-before-after-action.yml deleted file mode 100644 index a787faa8a9c..00000000000 --- a/changelogs/unreleased/55564-remove-if-in-before-after-action.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Rewrite `if:` argument in before_action and alike when `only:` is also used -merge_request: 24412 -author: George Thomas @thegeorgeous -type: other diff --git a/changelogs/unreleased/55999-misleading-pipeline-tooltip-messages-and-misleading-ci-status-icons.yml b/changelogs/unreleased/55999-misleading-pipeline-tooltip-messages-and-misleading-ci-status-icons.yml new file mode 100644 index 00000000000..a937614be38 --- /dev/null +++ b/changelogs/unreleased/55999-misleading-pipeline-tooltip-messages-and-misleading-ci-status-icons.yml @@ -0,0 +1,5 @@ +--- +title: Remove "Commit" from pipeline status tooltips +merge_request: 31861 +author: +type: fixed diff --git a/changelogs/unreleased/56100-make-quick-action-commands-applied-banner-more-useful.yml b/changelogs/unreleased/56100-make-quick-action-commands-applied-banner-more-useful.yml deleted file mode 100644 index a2fa07c6ed2..00000000000 --- a/changelogs/unreleased/56100-make-quick-action-commands-applied-banner-more-useful.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Make quick action commands applied banner more useful -merge_request: 26672 -author: Jacopo Beschi @jacopo-beschi -type: added diff --git a/changelogs/unreleased/56130-deployed_at.yml b/changelogs/unreleased/56130-deployed_at.yml new file mode 100644 index 00000000000..c53658de752 --- /dev/null +++ b/changelogs/unreleased/56130-deployed_at.yml @@ -0,0 +1,5 @@ +--- +title: Replace finished_at with deployed_at for the internal API Deployment entity +merge_request: 32000 +author: +type: other diff --git a/changelogs/unreleased/56883-migration.yml b/changelogs/unreleased/56883-migration.yml new file mode 100644 index 00000000000..d6eb49e62e1 --- /dev/null +++ b/changelogs/unreleased/56883-migration.yml @@ -0,0 +1,5 @@ +--- +title: Create a project for self-monitoring the GitLab instance +merge_request: 31389 +author: +type: added diff --git a/changelogs/unreleased/57402-upate-issues-list-sort-options.yml b/changelogs/unreleased/57402-upate-issues-list-sort-options.yml new file mode 100644 index 00000000000..900e71e3204 --- /dev/null +++ b/changelogs/unreleased/57402-upate-issues-list-sort-options.yml @@ -0,0 +1,5 @@ +--- +title: Updates issues REST API to allow extended sort options +merge_request: 31849 +author: +type: changed diff --git a/changelogs/unreleased/57953-fix-unfolded-diff-suggestions.yml b/changelogs/unreleased/57953-fix-unfolded-diff-suggestions.yml deleted file mode 100644 index f634c0cd98a..00000000000 --- a/changelogs/unreleased/57953-fix-unfolded-diff-suggestions.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix suggestion on lines that are not part of an MR -merge_request: 30606 -author: -type: fixed diff --git a/changelogs/unreleased/58035-expand-mr-diff.yml b/changelogs/unreleased/58035-expand-mr-diff.yml deleted file mode 100644 index 7163cce29f2..00000000000 --- a/changelogs/unreleased/58035-expand-mr-diff.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Add new expansion options for merge request diffs -merge_request: 30927 -author: -type: added diff --git a/changelogs/unreleased/58256-incorrect-empty-state-message-displayed-on-explore-projects-tab.yml b/changelogs/unreleased/58256-incorrect-empty-state-message-displayed-on-explore-projects-tab.yml deleted file mode 100644 index f719338b9cb..00000000000 --- a/changelogs/unreleased/58256-incorrect-empty-state-message-displayed-on-explore-projects-tab.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Resolve Incorrect empty state message on Explore projects -merge_request: 25578 -author: -type: fixed diff --git a/changelogs/unreleased/59053-no-oauth-for-cicd-github-fe.yml b/changelogs/unreleased/59053-no-oauth-for-cicd-github-fe.yml new file mode 100644 index 00000000000..221e8408dad --- /dev/null +++ b/changelogs/unreleased/59053-no-oauth-for-cicd-github-fe.yml @@ -0,0 +1,5 @@ +--- +title: Remove oauth form from GitHub CI/CD only import authentication +merge_request: 31488 +author: +type: changed diff --git a/changelogs/unreleased/59325-units-are-not-shown-on-the-performance-dashboard-2.yml b/changelogs/unreleased/59325-units-are-not-shown-on-the-performance-dashboard-2.yml deleted file mode 100644 index 38cfa0f273e..00000000000 --- a/changelogs/unreleased/59325-units-are-not-shown-on-the-performance-dashboard-2.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: 'fix: updates to include units for the y axis label' -merge_request: 30330 -author: -type: fixed diff --git a/changelogs/unreleased/59521-job-sidebar-has-a-blank-block.yml b/changelogs/unreleased/59521-job-sidebar-has-a-blank-block.yml deleted file mode 100644 index 4c93a108f2b..00000000000 --- a/changelogs/unreleased/59521-job-sidebar-has-a-blank-block.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Remove blank block from job sidebar -merge_request: 30754 -author: -type: fixed diff --git a/changelogs/unreleased/59590-keyboard-shortcut-for-jump-to-next-unresolved-discussion.yml b/changelogs/unreleased/59590-keyboard-shortcut-for-jump-to-next-unresolved-discussion.yml deleted file mode 100644 index 02e81c7fc87..00000000000 --- a/changelogs/unreleased/59590-keyboard-shortcut-for-jump-to-next-unresolved-discussion.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Resolve Keyboard shortcut for jump to NEXT unresolved discussion -merge_request: 30144 -author: -type: added diff --git a/changelogs/unreleased/59712-resolve-the-search-problem-issue.yml b/changelogs/unreleased/59712-resolve-the-search-problem-issue.yml deleted file mode 100644 index 964962cb817..00000000000 --- a/changelogs/unreleased/59712-resolve-the-search-problem-issue.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Add branch/tags/commits dropdown filter on the search page for searching codes -merge_request: 28282 -author: minghuan lei -type: changed diff --git a/changelogs/unreleased/59786-show-renamed-file-in-mr.yml b/changelogs/unreleased/59786-show-renamed-file-in-mr.yml new file mode 100644 index 00000000000..e8c52b592d2 --- /dev/null +++ b/changelogs/unreleased/59786-show-renamed-file-in-mr.yml @@ -0,0 +1,5 @@ +--- +title: Fix to show renamed file in mr +merge_request: 31888 +author: +type: changed diff --git a/changelogs/unreleased/60449-reduce-gitaly-calls-when-rendering-commits-in-md.yml b/changelogs/unreleased/60449-reduce-gitaly-calls-when-rendering-commits-in-md.yml deleted file mode 100644 index ef11e8743f6..00000000000 --- a/changelogs/unreleased/60449-reduce-gitaly-calls-when-rendering-commits-in-md.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Batch processing of commit refs in markdown processing -merge_request: 31037 -author: -type: performance diff --git a/changelogs/unreleased/60516-uninstall-tiller.yml b/changelogs/unreleased/60516-uninstall-tiller.yml deleted file mode 100644 index db25e7b3338..00000000000 --- a/changelogs/unreleased/60516-uninstall-tiller.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Allow Helm to be uninstalled from the UI -merge_request: 27359 -author: -type: added diff --git a/changelogs/unreleased/60664-kubernetes-applications-uninstall-cert-manager.yml b/changelogs/unreleased/60664-kubernetes-applications-uninstall-cert-manager.yml deleted file mode 100644 index efc3ec241e2..00000000000 --- a/changelogs/unreleased/60664-kubernetes-applications-uninstall-cert-manager.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Allow Cert-Manager to be uninstalled -merge_request: 31166 -author: -type: added diff --git a/changelogs/unreleased/60668-kubernetes-applications-uninstall-knative.yml b/changelogs/unreleased/60668-kubernetes-applications-uninstall-knative.yml deleted file mode 100644 index c33dc0f50cd..00000000000 --- a/changelogs/unreleased/60668-kubernetes-applications-uninstall-knative.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Allow Knative to be uninstalled from the UI -merge_request: 30458 -author: -type: added diff --git a/changelogs/unreleased/60948-display-groupid-on-group-admin-page.yml b/changelogs/unreleased/60948-display-groupid-on-group-admin-page.yml deleted file mode 100644 index 17763b4c69e..00000000000 --- a/changelogs/unreleased/60948-display-groupid-on-group-admin-page.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Display group id on group admin page -merge_request: 29735 -author: Zsolt Kovari -type: added diff --git a/changelogs/unreleased/60949-display-projectid-on-project-admin-page.yml b/changelogs/unreleased/60949-display-projectid-on-project-admin-page.yml deleted file mode 100644 index 3ff83ede2fa..00000000000 --- a/changelogs/unreleased/60949-display-projectid-on-project-admin-page.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Display project id on project admin page -merge_request: 29734 -author: Zsolt Kovari -type: added diff --git a/changelogs/unreleased/61207-adjusted-hoverable-area-in-sidebar.yml b/changelogs/unreleased/61207-adjusted-hoverable-area-in-sidebar.yml deleted file mode 100644 index 99fc817d703..00000000000 --- a/changelogs/unreleased/61207-adjusted-hoverable-area-in-sidebar.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: "Adjusted the clickable area of collapsed sidebar elements" -merge_request: 30974 -author: Michel Engelen -type: changed diff --git a/changelogs/unreleased/61332-web-ide-mr-branch-dropdown-closes-unexpectedly.yml b/changelogs/unreleased/61332-web-ide-mr-branch-dropdown-closes-unexpectedly.yml deleted file mode 100644 index 1f5e507d48d..00000000000 --- a/changelogs/unreleased/61332-web-ide-mr-branch-dropdown-closes-unexpectedly.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix an issue where clicking outside the MR/branch search box in WebIDE closed the dropdown. -merge_request: 31523 -author: -type: fixed diff --git a/changelogs/unreleased/61445-prevent-persisting-auto-switch-discussion-filter.yml b/changelogs/unreleased/61445-prevent-persisting-auto-switch-discussion-filter.yml deleted file mode 100644 index 58e29212462..00000000000 --- a/changelogs/unreleased/61445-prevent-persisting-auto-switch-discussion-filter.yml +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: Prevent discussion filter from persisting to `Show all activity` when opening - links to notes -merge_request: 31229 -author: -type: fixed diff --git a/changelogs/unreleased/61776-fixing-the-U2F-warning-message-text-colour.yml b/changelogs/unreleased/61776-fixing-the-U2F-warning-message-text-colour.yml deleted file mode 100644 index 988eb77db12..00000000000 --- a/changelogs/unreleased/61776-fixing-the-U2F-warning-message-text-colour.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Remove the warning style from the U2F device message in user settings > account -merge_request: 30119 -author: matejlatin -type: other diff --git a/changelogs/unreleased/61787-the-colour-selector-for-broadcast-messages-should-provide-a-few-default-options-with-descriptive-labels-like.yml b/changelogs/unreleased/61787-the-colour-selector-for-broadcast-messages-should-provide-a-few-default-options-with-descriptive-labels-like.yml deleted file mode 100644 index ec6e9c5aff8..00000000000 --- a/changelogs/unreleased/61787-the-colour-selector-for-broadcast-messages-should-provide-a-few-default-options-with-descriptive-labels-like.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: add color selector to broadcast messages form -merge_request: 30988 -author: -type: other diff --git a/changelogs/unreleased/62137-add-tooltip-to-improve-clarity-of-detached-label-state-in-the-merge-request-pipeline.yml b/changelogs/unreleased/62137-add-tooltip-to-improve-clarity-of-detached-label-state-in-the-merge-request-pipeline.yml deleted file mode 100644 index ccc3195e6ae..00000000000 --- a/changelogs/unreleased/62137-add-tooltip-to-improve-clarity-of-detached-label-state-in-the-merge-request-pipeline.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Updated the detached pipeline badge tooltip text to offer a better explanation -merge_request: 31626 -author: -type: other diff --git a/changelogs/unreleased/62322-add-optional-id-to-label-api-put-delete-pd.yml b/changelogs/unreleased/62322-add-optional-id-to-label-api-put-delete-pd.yml new file mode 100644 index 00000000000..7e1eeddef32 --- /dev/null +++ b/changelogs/unreleased/62322-add-optional-id-to-label-api-put-delete-pd.yml @@ -0,0 +1,5 @@ +--- +title: Add optional label_id parameter to label API for PUT and DELETE +merge_request: 31804 +author: +type: changed diff --git a/changelogs/unreleased/62373-badge-counter-very-low-contrast-between-foreground-and-background-c.yml b/changelogs/unreleased/62373-badge-counter-very-low-contrast-between-foreground-and-background-c.yml new file mode 100644 index 00000000000..12b19da1868 --- /dev/null +++ b/changelogs/unreleased/62373-badge-counter-very-low-contrast-between-foreground-and-background-c.yml @@ -0,0 +1,6 @@ +--- +title: 'Resolve Badge counter: Very low contrast between foreground and background + colors' +merge_request: 31922 +author: +type: other diff --git a/changelogs/unreleased/62609-test-summary-loading-spinner-is-too-large.yml b/changelogs/unreleased/62609-test-summary-loading-spinner-is-too-large.yml deleted file mode 100644 index f8e4a26dad8..00000000000 --- a/changelogs/unreleased/62609-test-summary-loading-spinner-is-too-large.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Adjust size and align MR-widget loading icon -merge_request: 31503 -author: -type: fixed diff --git a/changelogs/unreleased/62973-specify-time-frame-in-shareable-link-for-embedding-metrics.yml b/changelogs/unreleased/62973-specify-time-frame-in-shareable-link-for-embedding-metrics.yml deleted file mode 100644 index aaf0ddfa48d..00000000000 --- a/changelogs/unreleased/62973-specify-time-frame-in-shareable-link-for-embedding-metrics.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Allow links to metrics dashboard at a specific time -merge_request: 31283 -author: -type: added diff --git a/changelogs/unreleased/63181-collapsible-line.yml b/changelogs/unreleased/63181-collapsible-line.yml deleted file mode 100644 index c13d4eeab6c..00000000000 --- a/changelogs/unreleased/63181-collapsible-line.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Makes collapsible title clickable in job log -merge_request: -author: -type: added diff --git a/changelogs/unreleased/63438-oauth2-support-with-gitlab-personal-access-token.yml b/changelogs/unreleased/63438-oauth2-support-with-gitlab-personal-access-token.yml deleted file mode 100644 index 815010e15ae..00000000000 --- a/changelogs/unreleased/63438-oauth2-support-with-gitlab-personal-access-token.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Personal access tokens are accepted using OAuth2 header format -merge_request: 30277 -author: -type: added diff --git a/changelogs/unreleased/63485-fix-pipeline-emails-to-use-group-setting.yml b/changelogs/unreleased/63485-fix-pipeline-emails-to-use-group-setting.yml deleted file mode 100644 index c3ee3108216..00000000000 --- a/changelogs/unreleased/63485-fix-pipeline-emails-to-use-group-setting.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix pipeline emails not respecting group notification email setting -merge_request: 30907 -author: -type: fixed diff --git a/changelogs/unreleased/63547-add-system-notes-for-when-a-zoom-call-was-added-removed-from-an-issue.yml b/changelogs/unreleased/63547-add-system-notes-for-when-a-zoom-call-was-added-removed-from-an-issue.yml deleted file mode 100644 index 387c01dc135..00000000000 --- a/changelogs/unreleased/63547-add-system-notes-for-when-a-zoom-call-was-added-removed-from-an-issue.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Add system notes for when a Zoom call was added/removed from an issue -merge_request: 30857 -author: Jacopo Beschi @jacopo-beschi -type: added diff --git a/changelogs/unreleased/63568-access-email-notifications-custom-email.yml b/changelogs/unreleased/63568-access-email-notifications-custom-email.yml deleted file mode 100644 index ece6442d7cf..00000000000 --- a/changelogs/unreleased/63568-access-email-notifications-custom-email.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Respect group notification email when sending group access notifications -merge_request: 31089 -author: -type: fixed diff --git a/changelogs/unreleased/63571-fix-gc-profiler-data-being-wiped.yml b/changelogs/unreleased/63571-fix-gc-profiler-data-being-wiped.yml deleted file mode 100644 index 7943d9573f3..00000000000 --- a/changelogs/unreleased/63571-fix-gc-profiler-data-being-wiped.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix GC::Profiler metrics fetching -merge_request: 31331 -author: -type: fixed diff --git a/changelogs/unreleased/63671-remove-extra-padding-from-the-disabled-comment-area.yml b/changelogs/unreleased/63671-remove-extra-padding-from-the-disabled-comment-area.yml deleted file mode 100644 index ab116674ced..00000000000 --- a/changelogs/unreleased/63671-remove-extra-padding-from-the-disabled-comment-area.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Remove extra padding from disabled comment box -merge_request: 31603 -author: -type: fixed diff --git a/changelogs/unreleased/63730-fix-500-status-labels-pd.yml b/changelogs/unreleased/63730-fix-500-status-labels-pd.yml deleted file mode 100644 index a1e2ae0e5df..00000000000 --- a/changelogs/unreleased/63730-fix-500-status-labels-pd.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix admin labels page when there are invalid records -merge_request: 30885 -author: -type: fixed diff --git a/changelogs/unreleased/63833-fix-jira-issues-url.yml b/changelogs/unreleased/63833-fix-jira-issues-url.yml deleted file mode 100644 index 24d6bca3842..00000000000 --- a/changelogs/unreleased/63833-fix-jira-issues-url.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Handle trailing slashes when generating Jira issue URLs -merge_request: 30911 -author: -type: fixed diff --git a/changelogs/unreleased/63888-snippets-usage-ping-for-create-smau.yml b/changelogs/unreleased/63888-snippets-usage-ping-for-create-smau.yml deleted file mode 100644 index 1a5a552b120..00000000000 --- a/changelogs/unreleased/63888-snippets-usage-ping-for-create-smau.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Count snippet creation, update and comment events -merge_request: 30930 -author: -type: added diff --git a/changelogs/unreleased/63905-discussion-expand-collapse-button-is-only-clickable-on-one-side.yml b/changelogs/unreleased/63905-discussion-expand-collapse-button-is-only-clickable-on-one-side.yml new file mode 100644 index 00000000000..61cd69e88bf --- /dev/null +++ b/changelogs/unreleased/63905-discussion-expand-collapse-button-is-only-clickable-on-one-side.yml @@ -0,0 +1,5 @@ +--- +title: All of discussion expand/collapse button is clickable +merge_request: 31730 +author: +type: fixed diff --git a/changelogs/unreleased/64081-override-helm-release-name.yml b/changelogs/unreleased/64081-override-helm-release-name.yml deleted file mode 100644 index 2bf39b17c03..00000000000 --- a/changelogs/unreleased/64081-override-helm-release-name.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Allow multiple Auto DevOps projects to deploy to a single namespace within a k8s cluster -merge_request: 30360 -author: James Keogh -type: added diff --git a/changelogs/unreleased/64092-removes-update-statistics-namespace-feature-flag.yml b/changelogs/unreleased/64092-removes-update-statistics-namespace-feature-flag.yml deleted file mode 100644 index 272c830a914..00000000000 --- a/changelogs/unreleased/64092-removes-update-statistics-namespace-feature-flag.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Enables storage statistics for root namespaces on database -merge_request: 31392 -author: -type: other diff --git a/changelogs/unreleased/64160-fix-duplicate-buttons.yml b/changelogs/unreleased/64160-fix-duplicate-buttons.yml deleted file mode 100644 index 12416a611ed..00000000000 --- a/changelogs/unreleased/64160-fix-duplicate-buttons.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Remove duplicate buttons in diff discussion -merge_request: 30757 -author: -type: fixed diff --git a/changelogs/unreleased/64180-membersfinder-contains-slow-database-query-with-or-conditions.yml b/changelogs/unreleased/64180-membersfinder-contains-slow-database-query-with-or-conditions.yml deleted file mode 100644 index f86c63a15b6..00000000000 --- a/changelogs/unreleased/64180-membersfinder-contains-slow-database-query-with-or-conditions.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Improve MembersFinder query performance using UNION -merge_request: 30451 -author: Jacopo Beschi @jacopo-beschi -type: performance diff --git a/changelogs/unreleased/64190-add-mr-form.yml b/changelogs/unreleased/64190-add-mr-form.yml deleted file mode 100644 index 08340d01fd8..00000000000 --- a/changelogs/unreleased/64190-add-mr-form.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Add MR form to Visual Review (EE) runtime configuration -merge_request: 30481 -author: -type: changed diff --git a/changelogs/unreleased/64251-branch-name-set-cache.yml b/changelogs/unreleased/64251-branch-name-set-cache.yml new file mode 100644 index 00000000000..6ce4bdf5e43 --- /dev/null +++ b/changelogs/unreleased/64251-branch-name-set-cache.yml @@ -0,0 +1,5 @@ +--- +title: Cache branch and tag names as Redis sets +merge_request: 30476 +author: +type: performance diff --git a/changelogs/unreleased/64257-active_session_lookup_key_cleanup.yml b/changelogs/unreleased/64257-active_session_lookup_key_cleanup.yml deleted file mode 100644 index df3cd98830e..00000000000 --- a/changelogs/unreleased/64257-active_session_lookup_key_cleanup.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Rake task to cleanup expired ActiveSession lookup keys -merge_request: 30668 -author: -type: performance diff --git a/changelogs/unreleased/64257-warden_set_user_fix.yml b/changelogs/unreleased/64257-warden_set_user_fix.yml deleted file mode 100644 index 7b6818876fb..00000000000 --- a/changelogs/unreleased/64257-warden_set_user_fix.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Ensure Warden triggers after_authentication callback -merge_request: 31138 -author: -type: fixed diff --git a/changelogs/unreleased/64265-center-loading-icon.yml b/changelogs/unreleased/64265-center-loading-icon.yml deleted file mode 100644 index cd4253b63c6..00000000000 --- a/changelogs/unreleased/64265-center-loading-icon.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Center loading icon in CI action component -merge_request: -author: -type: fixed diff --git a/changelogs/unreleased/64269-pipeline-api-fails-with-401.yml b/changelogs/unreleased/64269-pipeline-api-fails-with-401.yml new file mode 100644 index 00000000000..582339901ae --- /dev/null +++ b/changelogs/unreleased/64269-pipeline-api-fails-with-401.yml @@ -0,0 +1,5 @@ +--- +title: Read pipelines from public projects through API without an access token +merge_request: 31816 +author: +type: fixed diff --git a/changelogs/unreleased/64295-predictable-environment-slugs.yml b/changelogs/unreleased/64295-predictable-environment-slugs.yml deleted file mode 100644 index de581b2b8e1..00000000000 --- a/changelogs/unreleased/64295-predictable-environment-slugs.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Use predictable environment slugs -merge_request: 30551 -author: -type: added diff --git a/changelogs/unreleased/64341-user-callout-deferred-link-support.yml b/changelogs/unreleased/64341-user-callout-deferred-link-support.yml deleted file mode 100644 index 05230ddc124..00000000000 --- a/changelogs/unreleased/64341-user-callout-deferred-link-support.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Add support for deferred links in persistent user callouts. -merge_request: 30818 -author: -type: added diff --git a/changelogs/unreleased/64383-pattern-matching-with-variables-causes-gitlabs-ci-lint-to-throw-500.yml b/changelogs/unreleased/64383-pattern-matching-with-variables-causes-gitlabs-ci-lint-to-throw-500.yml new file mode 100644 index 00000000000..ba4bd614170 --- /dev/null +++ b/changelogs/unreleased/64383-pattern-matching-with-variables-causes-gitlabs-ci-lint-to-throw-500.yml @@ -0,0 +1,5 @@ +--- +title: Fix 500 errors caused by pattern matching with variables in CI Lint +merge_request: 31719 +author: +type: fixed diff --git a/changelogs/unreleased/64385-charts-scroll-handle-icon-has-disappeared.yml b/changelogs/unreleased/64385-charts-scroll-handle-icon-has-disappeared.yml new file mode 100644 index 00000000000..93ed2ff4619 --- /dev/null +++ b/changelogs/unreleased/64385-charts-scroll-handle-icon-has-disappeared.yml @@ -0,0 +1,5 @@ +--- +title: fix charts scroll handle icon to use gitlab svg +merge_request: 31825 +author: +type: fixed diff --git a/changelogs/unreleased/64608-double-tooltips.yml b/changelogs/unreleased/64608-double-tooltips.yml deleted file mode 100644 index f6cb1944d26..00000000000 --- a/changelogs/unreleased/64608-double-tooltips.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Prevents showing 2 tooltips in pipelines table -merge_request: -author: -type: fixed diff --git a/changelogs/unreleased/64630-add-warning-to-pages-domains-that-obtaining-deploying-ssl-certifica.yml b/changelogs/unreleased/64630-add-warning-to-pages-domains-that-obtaining-deploying-ssl-certifica.yml new file mode 100644 index 00000000000..bd2c9a3e2dc --- /dev/null +++ b/changelogs/unreleased/64630-add-warning-to-pages-domains-that-obtaining-deploying-ssl-certifica.yml @@ -0,0 +1,6 @@ +--- +title: Add warning to pages domains that obtaining/deploying SSL certificates through + Let's Encrypt can take some time +merge_request: 31765 +author: +type: other diff --git a/changelogs/unreleased/64675-Dashboard-URL-legend-border.yml b/changelogs/unreleased/64675-Dashboard-URL-legend-border.yml deleted file mode 100644 index f35261fcd6c..00000000000 --- a/changelogs/unreleased/64675-Dashboard-URL-legend-border.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Removed extrenal dashboard legend border -merge_request: 31407 -author: -type: fixed diff --git a/changelogs/unreleased/64677-delete-directory-webide.yml b/changelogs/unreleased/64677-delete-directory-webide.yml new file mode 100644 index 00000000000..27d596b6b19 --- /dev/null +++ b/changelogs/unreleased/64677-delete-directory-webide.yml @@ -0,0 +1,5 @@ +--- +title: Fixed removing directories in Web IDE +merge_request: 31727 +author: +type: fixed diff --git a/changelogs/unreleased/64697-markdown-issues-checkbox-inside-blockquote-status-won-t-be-saved.yml b/changelogs/unreleased/64697-markdown-issues-checkbox-inside-blockquote-status-won-t-be-saved.yml deleted file mode 100644 index 00664d64050..00000000000 --- a/changelogs/unreleased/64697-markdown-issues-checkbox-inside-blockquote-status-won-t-be-saved.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Better support clickable tasklists inside blockquotes -merge_request: 30952 -author: -type: fixed diff --git a/changelogs/unreleased/64700-fix-the-color-of-the-visibility-icon-on-project-lists.yml b/changelogs/unreleased/64700-fix-the-color-of-the-visibility-icon-on-project-lists.yml deleted file mode 100644 index 0d2fbaf01ed..00000000000 --- a/changelogs/unreleased/64700-fix-the-color-of-the-visibility-icon-on-project-lists.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Ensure visibility icons in group/project listings are grey -merge_request: 30858 -author: -type: fixed diff --git a/changelogs/unreleased/64730-metrics-dashboard-menu-is-cramped-with-new-features-enabled.yml b/changelogs/unreleased/64730-metrics-dashboard-menu-is-cramped-with-new-features-enabled.yml deleted file mode 100644 index c564b98bb41..00000000000 --- a/changelogs/unreleased/64730-metrics-dashboard-menu-is-cramped-with-new-features-enabled.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Improve layout of dropdowns in the metrics dashboard page -merge_request: 31239 -author: -type: fixed diff --git a/changelogs/unreleased/64746-Commit-authors-avatar-sretched-in-commit-view-if-no-image-is-loaded.yml b/changelogs/unreleased/64746-Commit-authors-avatar-sretched-in-commit-view-if-no-image-is-loaded.yml deleted file mode 100644 index fb0f4cedc62..00000000000 --- a/changelogs/unreleased/64746-Commit-authors-avatar-sretched-in-commit-view-if-no-image-is-loaded.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fixed distorted avatars when resource not reachable -merge_request: 30904 -author: Marc Schwede -type: other diff --git a/changelogs/unreleased/64763-fix-tags-page-layout.yml b/changelogs/unreleased/64763-fix-tags-page-layout.yml deleted file mode 100644 index db6b1f31506..00000000000 --- a/changelogs/unreleased/64763-fix-tags-page-layout.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix tag page layout -merge_request: -author: -type: fixed diff --git a/changelogs/unreleased/64764-fix-serverless-layout.yml b/changelogs/unreleased/64764-fix-serverless-layout.yml new file mode 100644 index 00000000000..9717062df93 --- /dev/null +++ b/changelogs/unreleased/64764-fix-serverless-layout.yml @@ -0,0 +1,5 @@ +--- +title: Fix serverless entry page layout +merge_request: 32029 +author: +type: fixed diff --git a/changelogs/unreleased/64831-add-padding-to-merged-by-widget.yml b/changelogs/unreleased/64831-add-padding-to-merged-by-widget.yml deleted file mode 100644 index 2a45eec78ef..00000000000 --- a/changelogs/unreleased/64831-add-padding-to-merged-by-widget.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Add space to "merged by" widget -merge_request: 30972 -author: -type: fixed diff --git a/changelogs/unreleased/64972-fix-unicorn-workers-metric.yml b/changelogs/unreleased/64972-fix-unicorn-workers-metric.yml deleted file mode 100644 index f80111f0bd9..00000000000 --- a/changelogs/unreleased/64972-fix-unicorn-workers-metric.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix pid discovery for Unicorn processes in `PidProvider` -merge_request: 31056 -author: -type: fixed diff --git a/changelogs/unreleased/64974-remove-livesum-from-ruby-sampler-metrics.yml b/changelogs/unreleased/64974-remove-livesum-from-ruby-sampler-metrics.yml deleted file mode 100644 index 4fa3b7783c5..00000000000 --- a/changelogs/unreleased/64974-remove-livesum-from-ruby-sampler-metrics.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Remove :livesum from RubySampler metrics -merge_request: 31047 -author: -type: fixed diff --git a/changelogs/unreleased/65088-incorrect-message-interpolation-on-project-listing.yml b/changelogs/unreleased/65088-incorrect-message-interpolation-on-project-listing.yml deleted file mode 100644 index dd74b8443bc..00000000000 --- a/changelogs/unreleased/65088-incorrect-message-interpolation-on-project-listing.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix incorrect use of message interpolation -merge_request: 31121 -author: -type: fixed diff --git a/changelogs/unreleased/65263-manual-action.yml b/changelogs/unreleased/65263-manual-action.yml deleted file mode 100644 index 47b2a2ed329..00000000000 --- a/changelogs/unreleased/65263-manual-action.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Hides loading spinner in pipelines actions after request has been fullfiled -merge_request: -author: -type: fixed diff --git a/changelogs/unreleased/65278-fix-puma-master-counter-wipe.yml b/changelogs/unreleased/65278-fix-puma-master-counter-wipe.yml deleted file mode 100644 index fb9d6fa251d..00000000000 --- a/changelogs/unreleased/65278-fix-puma-master-counter-wipe.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix active metric files being wiped after the app starts -merge_request: 31668 -author: -type: fixed diff --git a/changelogs/unreleased/65412-add-support-for-line-charts.yml b/changelogs/unreleased/65412-add-support-for-line-charts.yml new file mode 100644 index 00000000000..cb9043596b7 --- /dev/null +++ b/changelogs/unreleased/65412-add-support-for-line-charts.yml @@ -0,0 +1,5 @@ +--- +title: Create component to display area and line charts in monitor dashboards +merge_request: 31639 +author: +type: added diff --git a/changelogs/unreleased/65427-improve-system-notes-for-zoom-links.yml b/changelogs/unreleased/65427-improve-system-notes-for-zoom-links.yml new file mode 100644 index 00000000000..d081620c710 --- /dev/null +++ b/changelogs/unreleased/65427-improve-system-notes-for-zoom-links.yml @@ -0,0 +1,5 @@ +--- +title: Improve system notes for Zoom links +merge_request: 31410 +author: Jacopo Beschi @jacopo-beschi +type: changed diff --git a/changelogs/unreleased/65530-add-externalization-and-fix-regression-in-shortcuts-helper-modal.yml b/changelogs/unreleased/65530-add-externalization-and-fix-regression-in-shortcuts-helper-modal.yml deleted file mode 100644 index fc29a514c42..00000000000 --- a/changelogs/unreleased/65530-add-externalization-and-fix-regression-in-shortcuts-helper-modal.yml +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: Fixed display of some sections and externalized all text in the shortcuts modal - overlay -merge_request: 31594 -author: -type: fixed diff --git a/changelogs/unreleased/65660-update-karma-to-4-2-0.yml b/changelogs/unreleased/65660-update-karma-to-4-2-0.yml deleted file mode 100644 index c0cb40ce169..00000000000 --- a/changelogs/unreleased/65660-update-karma-to-4-2-0.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Update karma to 4.2.0 -merge_request: 31495 -author: Takuya Noguchi -type: other diff --git a/changelogs/unreleased/65671-update-mini_magick-to-4-9-5.yml b/changelogs/unreleased/65671-update-mini_magick-to-4-9-5.yml deleted file mode 100644 index a6f8576ae0b..00000000000 --- a/changelogs/unreleased/65671-update-mini_magick-to-4-9-5.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Update mini_magick to 4.9.5 -merge_request: 31505 -author: Takuya Noguchi -type: security diff --git a/changelogs/unreleased/65700-document-max-replication-slots-pg-ha.yml b/changelogs/unreleased/65700-document-max-replication-slots-pg-ha.yml deleted file mode 100644 index bacc3f5fc11..00000000000 --- a/changelogs/unreleased/65700-document-max-replication-slots-pg-ha.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Add max_replication_slots to PG HA documentation -merge_request: 31534 -author: -type: other diff --git a/changelogs/unreleased/65705-two-buttons.yml b/changelogs/unreleased/65705-two-buttons.yml deleted file mode 100644 index b92e28f9d68..00000000000 --- a/changelogs/unreleased/65705-two-buttons.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Prevent duplicated trigger action button -merge_request: -author: -type: fixed diff --git a/changelogs/unreleased/65790-highlight.yml b/changelogs/unreleased/65790-highlight.yml deleted file mode 100644 index 2531a3730ed..00000000000 --- a/changelogs/unreleased/65790-highlight.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Adds highlight to the collapsible section -merge_request: -author: -type: added diff --git a/changelogs/unreleased/66022-git-clone-url-box-on-wiki-git-access-page-is-broken.yml b/changelogs/unreleased/66022-git-clone-url-box-on-wiki-git-access-page-is-broken.yml new file mode 100644 index 00000000000..931e947f8ed --- /dev/null +++ b/changelogs/unreleased/66022-git-clone-url-box-on-wiki-git-access-page-is-broken.yml @@ -0,0 +1,5 @@ +--- +title: Fix broken git clone box on wiki git access page +merge_request: 31898 +author: +type: fixed diff --git a/changelogs/unreleased/66037-deployment-user.yml b/changelogs/unreleased/66037-deployment-user.yml new file mode 100644 index 00000000000..8a61b8145af --- /dev/null +++ b/changelogs/unreleased/66037-deployment-user.yml @@ -0,0 +1,5 @@ +--- +title: Return correct user for manual deployments +merge_request: 32004 +author: +type: fixed diff --git a/changelogs/unreleased/66061-update-tooltip-of-detached-label-state.yml b/changelogs/unreleased/66061-update-tooltip-of-detached-label-state.yml new file mode 100644 index 00000000000..826d5548999 --- /dev/null +++ b/changelogs/unreleased/66061-update-tooltip-of-detached-label-state.yml @@ -0,0 +1,5 @@ +--- +title: Updates tooltip of 'detached' label/state +merge_request: +author: +type: other diff --git a/changelogs/unreleased/66066-dark-theme-style-for-expansion-on-mr-diffs.yml b/changelogs/unreleased/66066-dark-theme-style-for-expansion-on-mr-diffs.yml new file mode 100644 index 00000000000..13607ae938a --- /dev/null +++ b/changelogs/unreleased/66066-dark-theme-style-for-expansion-on-mr-diffs.yml @@ -0,0 +1,5 @@ +--- +title: Add syntax highlighting for line expansion +merge_request: 31821 +author: +type: fixed diff --git a/changelogs/unreleased/66073-use-time-series-chart-instead-of-area-chart-in-panel_types.yml b/changelogs/unreleased/66073-use-time-series-chart-instead-of-area-chart-in-panel_types.yml new file mode 100644 index 00000000000..8d7af96a7d8 --- /dev/null +++ b/changelogs/unreleased/66073-use-time-series-chart-instead-of-area-chart-in-panel_types.yml @@ -0,0 +1,5 @@ +--- +title: Enable line charts in dashbaord panels and embedded charts +merge_request: 31920 +author: +type: added diff --git a/changelogs/unreleased/66161-replace-expand-icons.yml b/changelogs/unreleased/66161-replace-expand-icons.yml new file mode 100644 index 00000000000..e28e01108a8 --- /dev/null +++ b/changelogs/unreleased/66161-replace-expand-icons.yml @@ -0,0 +1,5 @@ +--- +title: Replaced expand diff icons +merge_request: 31907 +author: +type: changed diff --git a/changelogs/unreleased/66402-use-visual-review-tools-npm-package.yml b/changelogs/unreleased/66402-use-visual-review-tools-npm-package.yml new file mode 100644 index 00000000000..46773e12002 --- /dev/null +++ b/changelogs/unreleased/66402-use-visual-review-tools-npm-package.yml @@ -0,0 +1,5 @@ +--- +title: Move visual review toolbar code to NPM +merge_request: 32159 +author: +type: fixed diff --git a/changelogs/unreleased/FixLocaleEN.yml b/changelogs/unreleased/FixLocaleEN.yml deleted file mode 100644 index 49738a6d127..00000000000 --- a/changelogs/unreleased/FixLocaleEN.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Remove duplicated mapping key in config/locales/en.yml -merge_request: 30980 -author: Peter Dave Hello -type: fixed diff --git a/changelogs/unreleased/GL-12412.yml b/changelogs/unreleased/GL-12412.yml deleted file mode 100644 index 304bd63d150..00000000000 --- a/changelogs/unreleased/GL-12412.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Add DS_PIP_DEPENDENCY_PATH option to configure Dependency Scanning for projects using pip. -merge_request: 30762 -author: -type: changed diff --git a/changelogs/unreleased/GL-12757.yml b/changelogs/unreleased/GL-12757.yml deleted file mode 100644 index e58ecf9259f..00000000000 --- a/changelogs/unreleased/GL-12757.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Update the container scanning CI template to use v12 of the clair scanner. -merge_request: 30809 -author: -type: changed diff --git a/changelogs/unreleased/ab-add-index-on-environments.yml b/changelogs/unreleased/ab-add-index-on-environments.yml deleted file mode 100644 index 6c7641912f4..00000000000 --- a/changelogs/unreleased/ab-add-index-on-environments.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Create index on environments by state -merge_request: 31231 -author: -type: performance diff --git a/changelogs/unreleased/ab-count-strategies.yml b/changelogs/unreleased/ab-count-strategies.yml deleted file mode 100644 index bd95ff45d6f..00000000000 --- a/changelogs/unreleased/ab-count-strategies.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Use tablesample approximate counting by default. -merge_request: 31048 -author: -type: performance diff --git a/changelogs/unreleased/ac-graphql-root-namespace-stats.yml b/changelogs/unreleased/ac-graphql-root-namespace-stats.yml new file mode 100644 index 00000000000..9784605ab2e --- /dev/null +++ b/changelogs/unreleased/ac-graphql-root-namespace-stats.yml @@ -0,0 +1,5 @@ +--- +title: Expose namespace storage statistics with GraphQL +merge_request: 32012 +author: +type: added diff --git a/changelogs/unreleased/add-caching-to-archive-endpoint.yml b/changelogs/unreleased/add-caching-to-archive-endpoint.yml deleted file mode 100644 index 770ec16e804..00000000000 --- a/changelogs/unreleased/add-caching-to-archive-endpoint.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Return an ETag header for the archive endpoint -merge_request: 30581 -author: -type: added diff --git a/changelogs/unreleased/add-git-blame-api.yml b/changelogs/unreleased/add-git-blame-api.yml deleted file mode 100644 index cdb77041433..00000000000 --- a/changelogs/unreleased/add-git-blame-api.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Add git blame to GitLab API -merge_request: 30675 -author: Oleg Zubchenko -type: added diff --git a/changelogs/unreleased/add-outbound-requests-whitelist-for-local-networks.yml b/changelogs/unreleased/add-outbound-requests-whitelist-for-local-networks.yml deleted file mode 100644 index 9b50175f536..00000000000 --- a/changelogs/unreleased/add-outbound-requests-whitelist-for-local-networks.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Add Outbound requests whitelist for local networks -merge_request: 30350 -author: Istvan Szalai -type: added diff --git a/changelogs/unreleased/add-release-to-github-importer.yml b/changelogs/unreleased/add-release-to-github-importer.yml deleted file mode 100644 index d11e7c725f7..00000000000 --- a/changelogs/unreleased/add-release-to-github-importer.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Add a field for released_at to GH importer -merge_request: 31496 -author: -type: fixed diff --git a/changelogs/unreleased/add-support-for-start-sha-to-commits-api.yml b/changelogs/unreleased/add-support-for-start-sha-to-commits-api.yml deleted file mode 100644 index f810c2c5ada..00000000000 --- a/changelogs/unreleased/add-support-for-start-sha-to-commits-api.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Add support for start_sha to commits API -merge_request: 29598 -author: -type: changed diff --git a/changelogs/unreleased/add_links_to_latest_pipelines.yml b/changelogs/unreleased/add_links_to_latest_pipelines.yml new file mode 100644 index 00000000000..333cd6c92b4 --- /dev/null +++ b/changelogs/unreleased/add_links_to_latest_pipelines.yml @@ -0,0 +1,5 @@ +--- +title: 'Add links for latest pipelines' +merge_request: 20865 +author: Alex Ives +type: added diff --git a/changelogs/unreleased/adjust-group-level-analytics-to-accept-multiple-ids.yml b/changelogs/unreleased/adjust-group-level-analytics-to-accept-multiple-ids.yml deleted file mode 100644 index 5e138e1059c..00000000000 --- a/changelogs/unreleased/adjust-group-level-analytics-to-accept-multiple-ids.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Adjust group level analytics to accept multiple ids -merge_request: 30744 -author: -type: added diff --git a/changelogs/unreleased/alipniagov-fix-wiki_can_not_be_created_total-counter.yml b/changelogs/unreleased/alipniagov-fix-wiki_can_not_be_created_total-counter.yml deleted file mode 100644 index 58f969ed742..00000000000 --- a/changelogs/unreleased/alipniagov-fix-wiki_can_not_be_created_total-counter.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix :wiki_can_not_be_created_total counter -merge_request: 31673 -author: -type: fixed diff --git a/changelogs/unreleased/allow-all-users-to-see-history.yml b/changelogs/unreleased/allow-all-users-to-see-history.yml deleted file mode 100644 index 7423fa079cc..00000000000 --- a/changelogs/unreleased/allow-all-users-to-see-history.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Align access permissions for wiki history to those of wiki pages -merge_request: 30470 -type: fixed diff --git a/changelogs/unreleased/an-fix-sidekiq-histogram-buckets.yml b/changelogs/unreleased/an-fix-sidekiq-histogram-buckets.yml new file mode 100644 index 00000000000..696b8f3987e --- /dev/null +++ b/changelogs/unreleased/an-fix-sidekiq-histogram-buckets.yml @@ -0,0 +1,5 @@ +--- +title: Allow latency measurements of sidekiq jobs taking > 2.5s +merge_request: 32001 +author: +type: fixed diff --git a/changelogs/unreleased/an-sidekiq-chaos.yml b/changelogs/unreleased/an-sidekiq-chaos.yml deleted file mode 100644 index cede35c95cc..00000000000 --- a/changelogs/unreleased/an-sidekiq-chaos.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Adds chaos endpoints to Sidekiq -merge_request: 30814 -author: -type: other diff --git a/changelogs/unreleased/an-sidekiq-scheduling_latency.yml b/changelogs/unreleased/an-sidekiq-scheduling_latency.yml deleted file mode 100644 index 2d6f462745e..00000000000 --- a/changelogs/unreleased/an-sidekiq-scheduling_latency.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Adds Sidekiq scheduling latency structured logging field -merge_request: 30784 -author: -type: other diff --git a/changelogs/unreleased/avoid-race-condition-of-archive-trace-cron-worker.yml b/changelogs/unreleased/avoid-race-condition-of-archive-trace-cron-worker.yml new file mode 100644 index 00000000000..b3f84ec32d2 --- /dev/null +++ b/changelogs/unreleased/avoid-race-condition-of-archive-trace-cron-worker.yml @@ -0,0 +1,5 @@ +--- +title: Avoid conflicts between ArchiveTracesCronWorker and ArchiveTraceWorker +merge_request: 31376 +author: +type: fixed diff --git a/changelogs/unreleased/bjk-64064_cache_metrics.yml b/changelogs/unreleased/bjk-64064_cache_metrics.yml deleted file mode 100644 index c9baff7cb7b..00000000000 --- a/changelogs/unreleased/bjk-64064_cache_metrics.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Adjust redis cache metrics -merge_request: 30572 -author: -type: changed diff --git a/changelogs/unreleased/bjk-usage_ping.yml b/changelogs/unreleased/bjk-usage_ping.yml deleted file mode 100644 index dee6c1ad291..00000000000 --- a/changelogs/unreleased/bjk-usage_ping.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Update usage ping cron behavior -merge_request: 30842 -author: -type: performance diff --git a/changelogs/unreleased/bring-scoped-environment-variables-to-core.yml b/changelogs/unreleased/bring-scoped-environment-variables-to-core.yml deleted file mode 100644 index 48dfe662206..00000000000 --- a/changelogs/unreleased/bring-scoped-environment-variables-to-core.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Bring scoped environment variables to core -merge_request: 30779 -author: -type: changed diff --git a/changelogs/unreleased/bvl-mark-remote-mirrors-as-failed-sooner.yml b/changelogs/unreleased/bvl-mark-remote-mirrors-as-failed-sooner.yml deleted file mode 100644 index 1db0a4952b2..00000000000 --- a/changelogs/unreleased/bvl-mark-remote-mirrors-as-failed-sooner.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Mark push mirrors as failed after 1 hour -merge_request: 30999 -author: -type: changed diff --git a/changelogs/unreleased/bvl-mr-commit-note-counter.yml b/changelogs/unreleased/bvl-mr-commit-note-counter.yml new file mode 100644 index 00000000000..0e9becbd31e --- /dev/null +++ b/changelogs/unreleased/bvl-mr-commit-note-counter.yml @@ -0,0 +1,5 @@ +--- +title: Count comments on commits and merge requests +merge_request: 31912 +author: +type: other diff --git a/changelogs/unreleased/bw-add-index-for-relative-position.yml b/changelogs/unreleased/bw-add-index-for-relative-position.yml deleted file mode 100644 index 80ca20992e6..00000000000 --- a/changelogs/unreleased/bw-add-index-for-relative-position.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Add index for issues on relative position, project, and state for manual sorting -merge_request: 30542 -author: -type: fixed diff --git a/changelogs/unreleased/ce-22058-improve-ux-multi-assignees-in-mr.yml b/changelogs/unreleased/ce-22058-improve-ux-multi-assignees-in-mr.yml new file mode 100644 index 00000000000..e7453a2b9bd --- /dev/null +++ b/changelogs/unreleased/ce-22058-improve-ux-multi-assignees-in-mr.yml @@ -0,0 +1,5 @@ +--- +title: Update assignee (cannot merge) style +merge_request: 31545 +author: +type: changed diff --git a/changelogs/unreleased/ce-xanf-add-links-to-admin-area.yml b/changelogs/unreleased/ce-xanf-add-links-to-admin-area.yml deleted file mode 100644 index 9eb692c948b..00000000000 --- a/changelogs/unreleased/ce-xanf-add-links-to-admin-area.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Add links to relevant configuration areas in admin area overview -merge_request: 29306 -author: -type: added diff --git a/changelogs/unreleased/ci-config-on-policy.yml b/changelogs/unreleased/ci-config-on-policy.yml new file mode 100644 index 00000000000..d9804f0323a --- /dev/null +++ b/changelogs/unreleased/ci-config-on-policy.yml @@ -0,0 +1,5 @@ +--- +title: Introduced Build::Rules configuration for Ci::Build +merge_request: 29011 +author: +type: added diff --git a/changelogs/unreleased/clean-obsolete-css.yml b/changelogs/unreleased/clean-obsolete-css.yml new file mode 100644 index 00000000000..d9716d16762 --- /dev/null +++ b/changelogs/unreleased/clean-obsolete-css.yml @@ -0,0 +1,5 @@ +--- +title: Fix MR reports section loading icon alignment +merge_request: 31897 +author: +type: fixed diff --git a/changelogs/unreleased/dblessing-fix-admin-user-radio-labels.yml b/changelogs/unreleased/dblessing-fix-admin-user-radio-labels.yml deleted file mode 100644 index 4f119d46a1f..00000000000 --- a/changelogs/unreleased/dblessing-fix-admin-user-radio-labels.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix admin area user access level radio button labels -merge_request: 31154 -author: -type: fixed diff --git a/changelogs/unreleased/delete-designs-v2.yml b/changelogs/unreleased/delete-designs-v2.yml deleted file mode 100644 index a678e4f93b9..00000000000 --- a/changelogs/unreleased/delete-designs-v2.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Adds event enum column to DesignsVersions join table -merge_request: 30745 -type: added diff --git a/changelogs/unreleased/double-slash-64592.yml b/changelogs/unreleased/double-slash-64592.yml deleted file mode 100644 index e3b5b197ac5..00000000000 --- a/changelogs/unreleased/double-slash-64592.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Prevent double slash in review apps path -merge_request: 31212 -author: -type: fixed diff --git a/changelogs/unreleased/ee-2502-refactor-ee-app-assets-javascripts-approvals-components-approvers_select-vue-to-remove-approverusers.yml b/changelogs/unreleased/ee-2502-refactor-ee-app-assets-javascripts-approvals-components-approvers_select-vue-to-remove-approverusers.yml new file mode 100644 index 00000000000..dfa0f0cb593 --- /dev/null +++ b/changelogs/unreleased/ee-2502-refactor-ee-app-assets-javascripts-approvals-components-approvers_select-vue-to-remove-approverusers.yml @@ -0,0 +1,5 @@ +--- +title: 'Add new API method in Api.js: projectUsers' +merge_request: 31801 +author: +type: other diff --git a/changelogs/unreleased/extract_auto_deploy_into_base_image.yml b/changelogs/unreleased/extract_auto_deploy_into_base_image.yml deleted file mode 100644 index ff0d1f3bd71..00000000000 --- a/changelogs/unreleased/extract_auto_deploy_into_base_image.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Extract Auto DevOps deploy functions into a base image -merge_request: 30404 -author: -type: changed diff --git a/changelogs/unreleased/fe-delete-old-boardservice.yml b/changelogs/unreleased/fe-delete-old-boardservice.yml deleted file mode 100644 index bb06bfe80d5..00000000000 --- a/changelogs/unreleased/fe-delete-old-boardservice.yml +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: Change BoardService in favor of boardsStore on board blank state of the component - board -merge_request: 30546 -author: eduarmreyes -type: other diff --git a/changelogs/unreleased/fe-fix-issuable-sidebar-icon-of-notification-disabled.yml b/changelogs/unreleased/fe-fix-issuable-sidebar-icon-of-notification-disabled.yml new file mode 100644 index 00000000000..736e12ff694 --- /dev/null +++ b/changelogs/unreleased/fe-fix-issuable-sidebar-icon-of-notification-disabled.yml @@ -0,0 +1,5 @@ +--- +title: Fix issuable sidebar icon on notification disabled +merge_request: 32134 +author: +type: fixed diff --git a/changelogs/unreleased/fe-fix-merge-url-params-with-plus.yml b/changelogs/unreleased/fe-fix-merge-url-params-with-plus.yml new file mode 100644 index 00000000000..7ca8ae0a8f6 --- /dev/null +++ b/changelogs/unreleased/fe-fix-merge-url-params-with-plus.yml @@ -0,0 +1,5 @@ +--- +title: Fix search preserving space when change branch +merge_request: 31973 +author: minghuan lei +type: fixed diff --git a/changelogs/unreleased/feat-add-support-page-link-in-help-menu.yml b/changelogs/unreleased/feat-add-support-page-link-in-help-menu.yml deleted file mode 100644 index 2cddd52212e..00000000000 --- a/changelogs/unreleased/feat-add-support-page-link-in-help-menu.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Add admin-configurable "Support page URL" link to top Help dropdown menu -merge_request: 30459 -author: Diego Louzán -type: added diff --git a/changelogs/unreleased/feat-smime-signed-notification-emails.yml b/changelogs/unreleased/feat-smime-signed-notification-emails.yml new file mode 100644 index 00000000000..9672d0d964c --- /dev/null +++ b/changelogs/unreleased/feat-smime-signed-notification-emails.yml @@ -0,0 +1,5 @@ +--- +title: Notification emails can be signed with SMIME +merge_request: 30644 +author: Diego Louzán +type: added diff --git a/changelogs/unreleased/feature-gb-serverless-app-deployment-template.yml b/changelogs/unreleased/feature-gb-serverless-app-deployment-template.yml deleted file mode 100644 index bd9001bd671..00000000000 --- a/changelogs/unreleased/feature-gb-serverless-app-deployment-template.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Deploy serverless apps with gitlabktl -merge_request: 30740 -author: -type: added diff --git a/changelogs/unreleased/filter-title-description-and-body-from-logs.yml b/changelogs/unreleased/filter-title-description-and-body-from-logs.yml deleted file mode 100644 index 8b592790629..00000000000 --- a/changelogs/unreleased/filter-title-description-and-body-from-logs.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Filter title, description, and body parameters from logs -merge_request: -author: -type: changed diff --git a/changelogs/unreleased/fix-alignment-on-security-reports.yml b/changelogs/unreleased/fix-alignment-on-security-reports.yml deleted file mode 100644 index 5339b6d764d..00000000000 --- a/changelogs/unreleased/fix-alignment-on-security-reports.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fixes alignment issues with reports -merge_request: 30839 -author: -type: fixed diff --git a/changelogs/unreleased/fix-bin-web-puma-script-to-consider-rails-env.yml b/changelogs/unreleased/fix-bin-web-puma-script-to-consider-rails-env.yml deleted file mode 100644 index a0564369b02..00000000000 --- a/changelogs/unreleased/fix-bin-web-puma-script-to-consider-rails-env.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Make `bin/web_puma` consider RAILS_ENV -merge_request: 31378 -author: -type: fixed diff --git a/changelogs/unreleased/fix-file-row-styling.yml b/changelogs/unreleased/fix-file-row-styling.yml new file mode 100644 index 00000000000..a4427fdd48f --- /dev/null +++ b/changelogs/unreleased/fix-file-row-styling.yml @@ -0,0 +1,5 @@ +--- +title: Fix loading icon causing text to jump in file row of Web IDE +merge_request: 31884 +author: +type: fixed diff --git a/changelogs/unreleased/fix-job-log-formatting.yml b/changelogs/unreleased/fix-job-log-formatting.yml deleted file mode 100644 index 0dd545aaecc..00000000000 --- a/changelogs/unreleased/fix-job-log-formatting.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix job logs where style changes were broken down into separate lines -merge_request: 31674 -author: -type: fixed diff --git a/changelogs/unreleased/fix-name-vs-path-problem-for-cycle-analytics.yml b/changelogs/unreleased/fix-name-vs-path-problem-for-cycle-analytics.yml deleted file mode 100644 index 7d171c2cf5b..00000000000 --- a/changelogs/unreleased/fix-name-vs-path-problem-for-cycle-analytics.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix broken issue links and possible 500 error on cycle analytics page when project name and path are different -merge_request: 31471 -author: -type: fixed diff --git a/changelogs/unreleased/fj-avoid-incresaing-usage-ping-when-not-enabled.yml b/changelogs/unreleased/fj-avoid-incresaing-usage-ping-when-not-enabled.yml deleted file mode 100644 index f1077f2d56d..00000000000 --- a/changelogs/unreleased/fj-avoid-incresaing-usage-ping-when-not-enabled.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Avoid increasing redis counters when usage_ping is disabled -merge_request: 30949 -author: -type: changed diff --git a/changelogs/unreleased/fj-count-web-ide-merge-requests.yml b/changelogs/unreleased/fj-count-web-ide-merge-requests.yml deleted file mode 100644 index adcd0af37e8..00000000000 --- a/changelogs/unreleased/fj-count-web-ide-merge-requests.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Add Web IDE Usage Ping for Create SMAU -merge_request: 30800 -author: -type: changed diff --git a/changelogs/unreleased/fj-navbar-searches-usage-ping-counter.yml b/changelogs/unreleased/fj-navbar-searches-usage-ping-counter.yml deleted file mode 100644 index ab7c1697fd6..00000000000 --- a/changelogs/unreleased/fj-navbar-searches-usage-ping-counter.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Added navbar searches usage ping counter -merge_request: 30953 -author: -type: changed diff --git a/changelogs/unreleased/georgekoltsov-18720-persistent-dashboard-sort.yml b/changelogs/unreleased/georgekoltsov-18720-persistent-dashboard-sort.yml new file mode 100644 index 00000000000..7eed8550acd --- /dev/null +++ b/changelogs/unreleased/georgekoltsov-18720-persistent-dashboard-sort.yml @@ -0,0 +1,5 @@ +--- +title: Add persistance to last choice of projects sorting on projects dashboard page +merge_request: 31669 +author: +type: added diff --git a/changelogs/unreleased/georgekoltsov-51260-add-filtering-to-bitbucket-server-import.yml b/changelogs/unreleased/georgekoltsov-51260-add-filtering-to-bitbucket-server-import.yml deleted file mode 100644 index c455b4cf642..00000000000 --- a/changelogs/unreleased/georgekoltsov-51260-add-filtering-to-bitbucket-server-import.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Add BitBucketServer project import filtering -merge_request: 31420 -author: -type: added diff --git a/changelogs/unreleased/georgekoltsov-55474-outbound-setting-system-hooks.yml b/changelogs/unreleased/georgekoltsov-55474-outbound-setting-system-hooks.yml deleted file mode 100644 index fb1acb1e9f5..00000000000 --- a/changelogs/unreleased/georgekoltsov-55474-outbound-setting-system-hooks.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Add new outbound network requests application setting for system hooks -merge_request: 31177 -author: -type: added diff --git a/changelogs/unreleased/georgekoltsov-63408-user-mapping.yml b/changelogs/unreleased/georgekoltsov-63408-user-mapping.yml deleted file mode 100644 index 451aac9c2e3..00000000000 --- a/changelogs/unreleased/georgekoltsov-63408-user-mapping.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: 'Fix missing author line (`Created by: <user>`) in MRs/issues/comments of imported Bitbucket Cloud project' -merge_request: 31579 -author: -type: fixed diff --git a/changelogs/unreleased/georgekoltsov-64311-set-visibility-private-if-internal-restricted.yml b/changelogs/unreleased/georgekoltsov-64311-set-visibility-private-if-internal-restricted.yml deleted file mode 100644 index 18af16e5216..00000000000 --- a/changelogs/unreleased/georgekoltsov-64311-set-visibility-private-if-internal-restricted.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Set visibility level 'Private' for restricted 'Internal' imported projects when 'Internal' visibility setting is restricted in admin settings -merge_request: 30522 -author: -type: other diff --git a/changelogs/unreleased/georgekoltsov-64377-add-better-log-msg-to-members-mapper.yml b/changelogs/unreleased/georgekoltsov-64377-add-better-log-msg-to-members-mapper.yml deleted file mode 100644 index 9557e633f76..00000000000 --- a/changelogs/unreleased/georgekoltsov-64377-add-better-log-msg-to-members-mapper.yml +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: When GitLab import fails during importer user mapping step, add an explicit - error message mentioning importer -merge_request: 30838 -author: -type: other diff --git a/changelogs/unreleased/gitaly-version-v1.57.0.yml b/changelogs/unreleased/gitaly-version-v1.57.0.yml deleted file mode 100644 index 65596199e16..00000000000 --- a/changelogs/unreleased/gitaly-version-v1.57.0.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Upgrade to Gitaly v1.57.0 -merge_request: 31568 -author: -type: changed diff --git a/changelogs/unreleased/group-milestones-dashboard-blunceford.yml b/changelogs/unreleased/group-milestones-dashboard-blunceford.yml deleted file mode 100644 index a6ded27cb8c..00000000000 --- a/changelogs/unreleased/group-milestones-dashboard-blunceford.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix bug in dashboard display of closed milestones -merge_request: 30820 -author: -type: fixed diff --git a/changelogs/unreleased/id-mr-widget-etag-caching.yml b/changelogs/unreleased/id-mr-widget-etag-caching.yml deleted file mode 100644 index 69bf89991d4..00000000000 --- a/changelogs/unreleased/id-mr-widget-etag-caching.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Split MR widget into etag-cached and non-cached serializers -merge_request: 31354 -author: -type: performance diff --git a/changelogs/unreleased/implement-dag.yml b/changelogs/unreleased/implement-dag.yml deleted file mode 100644 index 72f3f9a510c..00000000000 --- a/changelogs/unreleased/implement-dag.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: "Support creating DAGs in CI config through the `needs` key" -merge_request: 31328 -author: -type: added diff --git a/changelogs/unreleased/issue_58494.yml b/changelogs/unreleased/issue_58494.yml deleted file mode 100644 index c74768fc020..00000000000 --- a/changelogs/unreleased/issue_58494.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Prevent turning plain links into embedded when moving issues -merge_request: 31489 -author: -type: fixed diff --git a/changelogs/unreleased/je-separate-namespace-fe.yml b/changelogs/unreleased/je-separate-namespace-fe.yml deleted file mode 100644 index fb6be071eb6..00000000000 --- a/changelogs/unreleased/je-separate-namespace-fe.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Update namespace label for GitLab-managed clusters -merge_request: 30935 -author: -type: added diff --git a/changelogs/unreleased/jivanvl-add-chart-empty-state.yml b/changelogs/unreleased/jivanvl-add-chart-empty-state.yml deleted file mode 100644 index 7b81ee82582..00000000000 --- a/changelogs/unreleased/jivanvl-add-chart-empty-state.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Add empty chart component -merge_request: 30682 -author: -type: fixed diff --git a/changelogs/unreleased/jprovazn-fix-positioning.yml b/changelogs/unreleased/jprovazn-fix-positioning.yml deleted file mode 100644 index 5d703008bba..00000000000 --- a/changelogs/unreleased/jprovazn-fix-positioning.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Optimize relative re-positioning when moving issues. -merge_request: 30938 -author: -type: fixed diff --git a/changelogs/unreleased/jprovazn-project-search.yml b/changelogs/unreleased/jprovazn-project-search.yml deleted file mode 100644 index f76d4858924..00000000000 --- a/changelogs/unreleased/jprovazn-project-search.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Order projects in 'Move issue' dropdown by name. -merge_request: 30778 -author: -type: fixed diff --git a/changelogs/unreleased/jramsay-fix-mirroring-help-text-typo.yml b/changelogs/unreleased/jramsay-fix-mirroring-help-text-typo.yml deleted file mode 100644 index 4c049dffc58..00000000000 --- a/changelogs/unreleased/jramsay-fix-mirroring-help-text-typo.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix mirroring help text -merge_request: 31348 -author: jramsay -type: other diff --git a/changelogs/unreleased/jupyter-fixes-v1.yml b/changelogs/unreleased/jupyter-fixes-v1.yml deleted file mode 100644 index 7a34f273c90..00000000000 --- a/changelogs/unreleased/jupyter-fixes-v1.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Jupyter fixes -merge_request: 31332 -author: Amit Rathi -type: fixed diff --git a/changelogs/unreleased/khair1-master-patch-79459.yml b/changelogs/unreleased/khair1-master-patch-79459.yml deleted file mode 100644 index 22b0877336d..00000000000 --- a/changelogs/unreleased/khair1-master-patch-79459.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Update Packer.gitlab-ci.yml to use latest image -merge_request: -author: Kelly Hair -type: other diff --git a/changelogs/unreleased/label-descr-push-opts.yml b/changelogs/unreleased/label-descr-push-opts.yml deleted file mode 100644 index 9b01cfdaed2..00000000000 --- a/changelogs/unreleased/label-descr-push-opts.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Support setting of merge request title and description using git push options -merge_request: 31068 -author: -type: added diff --git a/changelogs/unreleased/labkit-cache-tracing.yml b/changelogs/unreleased/labkit-cache-tracing.yml new file mode 100644 index 00000000000..7e58e1b88dd --- /dev/null +++ b/changelogs/unreleased/labkit-cache-tracing.yml @@ -0,0 +1,5 @@ +--- +title: Add Redis interceptor tracing +merge_request: 30238 +author: +type: other diff --git a/changelogs/unreleased/latest-pipeline.yml b/changelogs/unreleased/latest-pipeline.yml new file mode 100644 index 00000000000..1177bb57121 --- /dev/null +++ b/changelogs/unreleased/latest-pipeline.yml @@ -0,0 +1,5 @@ +--- +title: Updated latest pipeline tag tooltip to be more descriptive +merge_request: 31624 +author: +type: changed diff --git a/changelogs/unreleased/lm-download-csv-of-charts-from-metrics-dashboard.yml b/changelogs/unreleased/lm-download-csv-of-charts-from-metrics-dashboard.yml deleted file mode 100644 index 59f12fca1f1..00000000000 --- a/changelogs/unreleased/lm-download-csv-of-charts-from-metrics-dashboard.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Export and download CSV from metrics charts -merge_request: 30760 -author: -type: added diff --git a/changelogs/unreleased/maintainers-can-create-subgroup.yml b/changelogs/unreleased/maintainers-can-create-subgroup.yml deleted file mode 100644 index 180f0f7247f..00000000000 --- a/changelogs/unreleased/maintainers-can-create-subgroup.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Maintainers can create subgroups -merge_request: 29718 -author: Fabio Papa -type: changed diff --git a/changelogs/unreleased/mc-feature-add-at-colon-variable-masking.yml b/changelogs/unreleased/mc-feature-add-at-colon-variable-masking.yml deleted file mode 100644 index 9b8eff8e043..00000000000 --- a/changelogs/unreleased/mc-feature-add-at-colon-variable-masking.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: 'Allows masking @ and : characters.' -merge_request: 31065 -author: -type: changed diff --git a/changelogs/unreleased/mc-feature-manual-job-variables.yml b/changelogs/unreleased/mc-feature-manual-job-variables.yml deleted file mode 100644 index a71cabfe303..00000000000 --- a/changelogs/unreleased/mc-feature-manual-job-variables.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Allow specifying variables when running manual jobs -merge_request: 30485 -author: -type: added diff --git a/changelogs/unreleased/optimise-build-queue-service.yml b/changelogs/unreleased/optimise-build-queue-service.yml new file mode 100644 index 00000000000..8ffaa3bfbdc --- /dev/null +++ b/changelogs/unreleased/optimise-build-queue-service.yml @@ -0,0 +1,5 @@ +--- +title: Optimise UpdateBuildQueueService +merge_request: 32095 +author: +type: performance diff --git a/changelogs/unreleased/pb-update-gitlab-shell-9-4-0.yml b/changelogs/unreleased/pb-update-gitlab-shell-9-4-0.yml new file mode 100644 index 00000000000..81db7bb4900 --- /dev/null +++ b/changelogs/unreleased/pb-update-gitlab-shell-9-4-0.yml @@ -0,0 +1,5 @@ +--- +title: Update to GitLab Shell v9.4.0 +merge_request: 32009 +author: +type: other diff --git a/changelogs/unreleased/rails-template-update.yml b/changelogs/unreleased/rails-template-update.yml deleted file mode 100644 index 53eb57c84ff..00000000000 --- a/changelogs/unreleased/rails-template-update.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Update 'Ruby on Rails' project template -merge_request: 31310 -author: -type: other diff --git a/changelogs/unreleased/remove-line-profile-from-performance-bar.yml b/changelogs/unreleased/remove-line-profile-from-performance-bar.yml deleted file mode 100644 index c1c7450fbbd..00000000000 --- a/changelogs/unreleased/remove-line-profile-from-performance-bar.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Remove line profiler from performance bar -merge_request: -author: -type: removed diff --git a/changelogs/unreleased/remove-margin-from-user-header.yml b/changelogs/unreleased/remove-margin-from-user-header.yml new file mode 100644 index 00000000000..31200d13c93 --- /dev/null +++ b/changelogs/unreleased/remove-margin-from-user-header.yml @@ -0,0 +1,5 @@ +--- +title: Remove margin from user header +merge_request: 30878 +author: lucyfox +type: fixed diff --git a/changelogs/unreleased/remove-peek-gc.yml b/changelogs/unreleased/remove-peek-gc.yml deleted file mode 100644 index 9412cd7c9a6..00000000000 --- a/changelogs/unreleased/remove-peek-gc.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Remove GC metrics from performance bar -merge_request: -author: -type: removed diff --git a/changelogs/unreleased/remove_deployment_metrics_deployment_platform_fallback.yml b/changelogs/unreleased/remove_deployment_metrics_deployment_platform_fallback.yml deleted file mode 100644 index d32cbd1d8e0..00000000000 --- a/changelogs/unreleased/remove_deployment_metrics_deployment_platform_fallback.yml +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: Remove incorrect fallback when determining which cluster to use when retrieving - MR performance metrics -merge_request: 31126 -author: -type: changed diff --git a/changelogs/unreleased/report-missing-job-dependency.yml b/changelogs/unreleased/report-missing-job-dependency.yml deleted file mode 100644 index 660cdfc856e..00000000000 --- a/changelogs/unreleased/report-missing-job-dependency.yml +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: Default dependency job stage index to Infinity, and correctly report it as - undefined in prior stages -merge_request: 31116 -author: -type: fixed diff --git a/changelogs/unreleased/rm-src-branch.yml b/changelogs/unreleased/rm-src-branch.yml deleted file mode 100644 index 03b91d0c7db..00000000000 --- a/changelogs/unreleased/rm-src-branch.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Support remove source branch on merge w/ push options -merge_request: 30728 -author: -type: added diff --git a/changelogs/unreleased/safe-archiving-for-traces.yml b/changelogs/unreleased/safe-archiving-for-traces.yml deleted file mode 100644 index 2b9070bacfe..00000000000 --- a/changelogs/unreleased/safe-archiving-for-traces.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Extra logging for new live trace architecture -merge_request: 30892 -author: -type: fixed diff --git a/changelogs/unreleased/security-60551-fix-upload-scope.yml b/changelogs/unreleased/security-60551-fix-upload-scope.yml deleted file mode 100644 index 7d7096833a7..00000000000 --- a/changelogs/unreleased/security-60551-fix-upload-scope.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Queries for Upload should be scoped by model -merge_request: -author: -type: security diff --git a/changelogs/unreleased/security-64711-fix-commit-todos.yml b/changelogs/unreleased/security-64711-fix-commit-todos.yml new file mode 100644 index 00000000000..ce4b3cdeeaf --- /dev/null +++ b/changelogs/unreleased/security-64711-fix-commit-todos.yml @@ -0,0 +1,5 @@ +--- +title: Send TODOs for comments on commits correctly +merge_request: +author: +type: security diff --git a/changelogs/unreleased/security-ci-metrics-permissions.yml b/changelogs/unreleased/security-ci-metrics-permissions.yml new file mode 100644 index 00000000000..51c6493442a --- /dev/null +++ b/changelogs/unreleased/security-ci-metrics-permissions.yml @@ -0,0 +1,6 @@ +--- +title: Restrict MergeRequests#test_reports to authenticated users with read-access + on Builds +merge_request: +author: +type: security diff --git a/changelogs/unreleased/security-exposed-default-branch.yml b/changelogs/unreleased/security-exposed-default-branch.yml new file mode 100644 index 00000000000..bf32617ee8a --- /dev/null +++ b/changelogs/unreleased/security-exposed-default-branch.yml @@ -0,0 +1,5 @@ +--- +title: Avoid exposing unaccessible repo data upon GFM post processing +merge_request: +author: +type: security diff --git a/changelogs/unreleased/security-fix-markdown-xss.yml b/changelogs/unreleased/security-fix-markdown-xss.yml new file mode 100644 index 00000000000..7ef19f13fd5 --- /dev/null +++ b/changelogs/unreleased/security-fix-markdown-xss.yml @@ -0,0 +1,5 @@ +--- +title: Make sure HTML text is always escaped when replacing label/milestone references. +merge_request: +author: +type: security diff --git a/changelogs/unreleased/security-gitaly-1-61-0.yml b/changelogs/unreleased/security-gitaly-1-61-0.yml new file mode 100644 index 00000000000..cbcd5f545a0 --- /dev/null +++ b/changelogs/unreleased/security-gitaly-1-61-0.yml @@ -0,0 +1,5 @@ +--- +title: "Gitaly: ignore git redirects" +merge_request: +author: +type: security diff --git a/changelogs/unreleased/security-group-runners-permissions.yml b/changelogs/unreleased/security-group-runners-permissions.yml new file mode 100644 index 00000000000..6c74be30b6d --- /dev/null +++ b/changelogs/unreleased/security-group-runners-permissions.yml @@ -0,0 +1,5 @@ +--- +title: Use admin_group authorization in Groups::RunnersController +merge_request: +author: +type: security diff --git a/changelogs/unreleased/security-hide_merge_request_ids_on_emails.yml b/changelogs/unreleased/security-hide_merge_request_ids_on_emails.yml new file mode 100644 index 00000000000..cd8c9590a70 --- /dev/null +++ b/changelogs/unreleased/security-hide_merge_request_ids_on_emails.yml @@ -0,0 +1,5 @@ +--- +title: Prevent disclosure of merge request ID via email +merge_request: +author: +type: security diff --git a/changelogs/unreleased/security-id-filter-timeline-activities-for-guests.yml b/changelogs/unreleased/security-id-filter-timeline-activities-for-guests.yml new file mode 100644 index 00000000000..0fa5f89e2c0 --- /dev/null +++ b/changelogs/unreleased/security-id-filter-timeline-activities-for-guests.yml @@ -0,0 +1,5 @@ +--- +title: Show cross-referenced MR-id in issues' activities only to authorized users +merge_request: +author: +type: security diff --git a/changelogs/unreleased/security-katex-dos-master.yml b/changelogs/unreleased/security-katex-dos-master.yml new file mode 100644 index 00000000000..df803a5eafd --- /dev/null +++ b/changelogs/unreleased/security-katex-dos-master.yml @@ -0,0 +1,5 @@ +--- +title: Enforce max chars and max render time in markdown math +merge_request: +author: +type: security diff --git a/changelogs/unreleased/security-project-import-bypass.yml b/changelogs/unreleased/security-project-import-bypass.yml new file mode 100644 index 00000000000..fc7b823509c --- /dev/null +++ b/changelogs/unreleased/security-project-import-bypass.yml @@ -0,0 +1,5 @@ +--- +title: Fix project import restricted visibility bypass via API +merge_request: +author: +type: security diff --git a/changelogs/unreleased/security-sarcila-fix-weak-session-management.yml b/changelogs/unreleased/security-sarcila-fix-weak-session-management.yml new file mode 100644 index 00000000000..a37a3099519 --- /dev/null +++ b/changelogs/unreleased/security-sarcila-fix-weak-session-management.yml @@ -0,0 +1,6 @@ +--- +title: Fix weak session management by clearing password reset tokens after login (username/email) + are updated +merge_request: +author: +type: security diff --git a/changelogs/unreleased/security-ssrf-kubernetes-dns.yml b/changelogs/unreleased/security-ssrf-kubernetes-dns.yml new file mode 100644 index 00000000000..4d6335e4b08 --- /dev/null +++ b/changelogs/unreleased/security-ssrf-kubernetes-dns.yml @@ -0,0 +1,5 @@ +--- +title: Fix SSRF via DNS rebinding in Kubernetes Integration +merge_request: +author: +type: security diff --git a/changelogs/unreleased/sh-add-cmaps-for-pdfjs.yml b/changelogs/unreleased/sh-add-cmaps-for-pdfjs.yml deleted file mode 100644 index f4686484e33..00000000000 --- a/changelogs/unreleased/sh-add-cmaps-for-pdfjs.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Make pdf.js render CJK characters -merge_request: 31220 -author: -type: fixed diff --git a/changelogs/unreleased/sh-add-gitaly-and-rugged-data-sidekiq.yml b/changelogs/unreleased/sh-add-gitaly-and-rugged-data-sidekiq.yml deleted file mode 100644 index d2143e83045..00000000000 --- a/changelogs/unreleased/sh-add-gitaly-and-rugged-data-sidekiq.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Add Gitaly and Rugged call timing in Sidekiq logs -merge_request: 31651 -author: -type: other diff --git a/changelogs/unreleased/sh-add-index-extern-uid.yml b/changelogs/unreleased/sh-add-index-extern-uid.yml deleted file mode 100644 index 531770237a8..00000000000 --- a/changelogs/unreleased/sh-add-index-extern-uid.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Add partial index on identities table to speed up LDAP lookups -merge_request: 26710 -author: -type: performance diff --git a/changelogs/unreleased/sh-add-missing-csp-report-uri.yml b/changelogs/unreleased/sh-add-missing-csp-report-uri.yml deleted file mode 100644 index 656eb8e9c37..00000000000 --- a/changelogs/unreleased/sh-add-missing-csp-report-uri.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Add missing report-uri to CSP config -merge_request: 31593 -author: -type: fixed diff --git a/changelogs/unreleased/sh-add-rugged-logs.yml b/changelogs/unreleased/sh-add-rugged-logs.yml deleted file mode 100644 index 1f464dd92ff..00000000000 --- a/changelogs/unreleased/sh-add-rugged-logs.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Add Rugged calls and duration to API and Rails logs -merge_request: 30871 -author: -type: other diff --git a/changelogs/unreleased/sh-add-rugged-to-peek.yml b/changelogs/unreleased/sh-add-rugged-to-peek.yml deleted file mode 100644 index 8a030f3daf2..00000000000 --- a/changelogs/unreleased/sh-add-rugged-to-peek.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Add Rugged calls to performance bar -merge_request: 30983 -author: -type: other diff --git a/changelogs/unreleased/sh-break-out-invited-group-members.yml b/changelogs/unreleased/sh-break-out-invited-group-members.yml deleted file mode 100644 index 091f1d48843..00000000000 --- a/changelogs/unreleased/sh-break-out-invited-group-members.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Make it easier to find invited group members -merge_request: 28436 -author: -type: fixed diff --git a/changelogs/unreleased/sh-disable-redis-peek.yml b/changelogs/unreleased/sh-disable-redis-peek.yml deleted file mode 100644 index de86c0031c7..00000000000 --- a/changelogs/unreleased/sh-disable-redis-peek.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Only track Redis calls if Peek is enabled -merge_request: 31438 -author: -type: performance diff --git a/changelogs/unreleased/sh-disable-registry-delete.yml b/changelogs/unreleased/sh-disable-registry-delete.yml deleted file mode 100644 index 180b983e07c..00000000000 --- a/changelogs/unreleased/sh-disable-registry-delete.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Don't attempt to contact registry if it is disabled -merge_request: 31553 -author: -type: fixed diff --git a/changelogs/unreleased/sh-eliminate-gitaly-nplus-one-notes.yml b/changelogs/unreleased/sh-eliminate-gitaly-nplus-one-notes.yml new file mode 100644 index 00000000000..00523f53dd7 --- /dev/null +++ b/changelogs/unreleased/sh-eliminate-gitaly-nplus-one-notes.yml @@ -0,0 +1,5 @@ +--- +title: Eliminate Gitaly N+1 queries with notes API +merge_request: 32089 +author: +type: performance diff --git a/changelogs/unreleased/sh-enable-bootsnap.yml b/changelogs/unreleased/sh-enable-bootsnap.yml deleted file mode 100644 index 674a900ee01..00000000000 --- a/changelogs/unreleased/sh-enable-bootsnap.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Make Bootsnap available via ENABLE_BOOTSNAP=1 -merge_request: 30963 -author: -type: performance diff --git a/changelogs/unreleased/sh-fix-special-role-error-500.yml b/changelogs/unreleased/sh-fix-special-role-error-500.yml deleted file mode 100644 index 9aed0710da3..00000000000 --- a/changelogs/unreleased/sh-fix-special-role-error-500.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix first-time contributor notes not rendering -merge_request: 31340 -author: -type: fixed diff --git a/changelogs/unreleased/sh-ignore-git-errors-delete-project.yml b/changelogs/unreleased/sh-ignore-git-errors-delete-project.yml deleted file mode 100644 index f5e2147f00e..00000000000 --- a/changelogs/unreleased/sh-ignore-git-errors-delete-project.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Ignore Gitaly errors if cache flushing fails on project destruction -merge_request: 31164 -author: -type: fixed diff --git a/changelogs/unreleased/sh-make-githost-json.yml b/changelogs/unreleased/sh-make-githost-json.yml deleted file mode 100644 index f4113a693cc..00000000000 --- a/changelogs/unreleased/sh-make-githost-json.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Convert githost.log to JSON format -merge_request: 30967 -author: -type: changed diff --git a/changelogs/unreleased/sh-remove-pdfjs-deprecations.yml b/changelogs/unreleased/sh-remove-pdfjs-deprecations.yml deleted file mode 100644 index a00ceedf3f4..00000000000 --- a/changelogs/unreleased/sh-remove-pdfjs-deprecations.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Remove pdf.js deprecation warnings -merge_request: 31253 -author: -type: fixed diff --git a/changelogs/unreleased/sh-rename-githost-to-gitjson.yml b/changelogs/unreleased/sh-rename-githost-to-gitjson.yml deleted file mode 100644 index 24fcd1e9781..00000000000 --- a/changelogs/unreleased/sh-rename-githost-to-gitjson.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Rename githost.log -> git_json.log -merge_request: 31634 -author: -type: changed diff --git a/changelogs/unreleased/sh-support-csp-nonce.yml b/changelogs/unreleased/sh-support-csp-nonce.yml deleted file mode 100644 index 3e6ac1e4a32..00000000000 --- a/changelogs/unreleased/sh-support-csp-nonce.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Add support for Content-Security-Policy -merge_request: 31402 -author: -type: added diff --git a/changelogs/unreleased/sh-update-mermaid.yml b/changelogs/unreleased/sh-update-mermaid.yml deleted file mode 100644 index 58d94ec6235..00000000000 --- a/changelogs/unreleased/sh-update-mermaid.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Update Mermaid to v8.2.3 -merge_request: 30985 -author: -type: fixed diff --git a/changelogs/unreleased/sh-update-rouge-3-7-0.yml b/changelogs/unreleased/sh-update-rouge-3-7-0.yml deleted file mode 100644 index 6828f48863c..00000000000 --- a/changelogs/unreleased/sh-update-rouge-3-7-0.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Update rouge to v3.7.0 -merge_request: 31254 -author: -type: other diff --git a/changelogs/unreleased/sh-use-redis-caching-store.yml b/changelogs/unreleased/sh-use-redis-caching-store.yml deleted file mode 100644 index e61bdb490bc..00000000000 --- a/changelogs/unreleased/sh-use-redis-caching-store.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Use Rails 5.2 Redis caching store -merge_request: 30966 -author: -type: other diff --git a/changelogs/unreleased/sh-use-shared-state-cluster-pubsub.yml b/changelogs/unreleased/sh-use-shared-state-cluster-pubsub.yml deleted file mode 100644 index 5e72f23d7ad..00000000000 --- a/changelogs/unreleased/sh-use-shared-state-cluster-pubsub.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Use persistent Redis cluster for Workhorse pub/sub notifications -merge_request: 30990 -author: -type: fixed diff --git a/changelogs/unreleased/snowplow-ee-to-ce.yml b/changelogs/unreleased/snowplow-ee-to-ce.yml deleted file mode 100644 index 85c959f0510..00000000000 --- a/changelogs/unreleased/snowplow-ee-to-ce.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Moves snowplow tracking from ee to ce -merge_request: 31160 -author: jejacks0n -type: added diff --git a/changelogs/unreleased/speed-up-labels-api.yml b/changelogs/unreleased/speed-up-labels-api.yml deleted file mode 100644 index d5ac4313414..00000000000 --- a/changelogs/unreleased/speed-up-labels-api.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Remove counts from default labels API responses -merge_request: 31543 -author: -type: changed diff --git a/changelogs/unreleased/tr-remove-embed-metrics-flag.yml b/changelogs/unreleased/tr-remove-embed-metrics-flag.yml deleted file mode 100644 index a327a6868d3..00000000000 --- a/changelogs/unreleased/tr-remove-embed-metrics-flag.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Link and embed metrics in GitLab Flavored Markdown -merge_request: 31106 -author: -type: added diff --git a/changelogs/unreleased/uncomment_commit_signatures_feature_flag.yml b/changelogs/unreleased/uncomment_commit_signatures_feature_flag.yml new file mode 100644 index 00000000000..8f6c89e7dd3 --- /dev/null +++ b/changelogs/unreleased/uncomment_commit_signatures_feature_flag.yml @@ -0,0 +1,5 @@ +--- +title: Upgrade to Gitaly 1.60.0 +merge_request: 31981 +author: +type: changed diff --git a/changelogs/unreleased/update-babel-to-7-5-5.yml b/changelogs/unreleased/update-babel-to-7-5-5.yml new file mode 100644 index 00000000000..c498e2adfe8 --- /dev/null +++ b/changelogs/unreleased/update-babel-to-7-5-5.yml @@ -0,0 +1,5 @@ +--- +title: Upgrade babel to 7.5.5 +merge_request: 31819 +author: Takuya Noguchi +type: other diff --git a/changelogs/unreleased/update-gitlab-runner-helm-chart-to-0-7-0.yml b/changelogs/unreleased/update-gitlab-runner-helm-chart-to-0-7-0.yml deleted file mode 100644 index ab1e7d77520..00000000000 --- a/changelogs/unreleased/update-gitlab-runner-helm-chart-to-0-7-0.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Update GitLab Runner Helm Chart to 0.7.0 -merge_request: 30950 -author: -type: other diff --git a/changelogs/unreleased/update-graphicsmagick-to-1-3-33.yml b/changelogs/unreleased/update-graphicsmagick-to-1-3-33.yml deleted file mode 100644 index 898fdef830a..00000000000 --- a/changelogs/unreleased/update-graphicsmagick-to-1-3-33.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Update GraphicsMagick from 1.3.29 to 1.3.33 for CI tests -merge_request: 31692 -author: Takuya Noguchi -type: other diff --git a/changelogs/unreleased/update-pipelines-minutes-expiry-banner-to-an-alert-component-type.yml b/changelogs/unreleased/update-pipelines-minutes-expiry-banner-to-an-alert-component-type.yml deleted file mode 100644 index 8c1a033dd29..00000000000 --- a/changelogs/unreleased/update-pipelines-minutes-expiry-banner-to-an-alert-component-type.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Enhance style of the shared runners limit -merge_request: 31386 -author: -type: other diff --git a/changelogs/unreleased/visual-review-tools-constant-storage-keys.yml b/changelogs/unreleased/visual-review-tools-constant-storage-keys.yml deleted file mode 100644 index 4c9b048aaa3..00000000000 --- a/changelogs/unreleased/visual-review-tools-constant-storage-keys.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix visual review app storage keys -merge_request: 31427 -author: -type: fixed diff --git a/changelogs/unreleased/wiki-usage-pings.yml b/changelogs/unreleased/wiki-usage-pings.yml deleted file mode 100644 index c3d084228c3..00000000000 --- a/changelogs/unreleased/wiki-usage-pings.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Count wiki creation, update and delete events -merge_request: 30864 -author: -type: added diff --git a/changelogs/unreleased/winh-deduplicate-board-headers.yml b/changelogs/unreleased/winh-deduplicate-board-headers.yml new file mode 100644 index 00000000000..009ce59b6bc --- /dev/null +++ b/changelogs/unreleased/winh-deduplicate-board-headers.yml @@ -0,0 +1,5 @@ +--- +title: Hide duplicate board list while dragging +merge_request: 32099 +author: +type: fixed diff --git a/config.ru b/config.ru index f6a7dca0542..750c84f7642 100644 --- a/config.ru +++ b/config.ru @@ -17,24 +17,16 @@ end require ::File.expand_path('../config/environment', __FILE__) -# The following is necessary to ensure stale Prometheus metrics don't accumulate over time. -# It needs to be done as early as here to ensure metrics files aren't deleted. -# After we hit our app in `warmup`, first metrics and corresponding files already being created, -# for example in `lib/gitlab/metrics/requests_rack_middleware.rb`. -def cleanup_prometheus_multiproc_dir - if dir = ::Prometheus::Client.configuration.multiprocess_files_dir - old_metrics = Dir[File.join(dir, '*.db')] - - FileUtils.rm_rf(old_metrics) - end -end - def master_process? Prometheus::PidProvider.worker_id.in? %w(unicorn_master puma_master) end warmup do |app| - cleanup_prometheus_multiproc_dir if master_process? + # The following is necessary to ensure stale Prometheus metrics don't accumulate over time. + # It needs to be done as early as here to ensure metrics files aren't deleted. + # After we hit our app in `warmup`, first metrics and corresponding files already being created, + # for example in `lib/gitlab/metrics/requests_rack_middleware.rb`. + Prometheus::CleanupMultiprocDirService.new.execute if master_process? client = Rack::MockRequest.new(app) client.get('/') diff --git a/config/application.rb b/config/application.rb index 47c5ab71285..294ed470298 100644 --- a/config/application.rb +++ b/config/application.rb @@ -41,11 +41,6 @@ module Gitlab #{config.root}/app/models/hooks #{config.root}/app/models/members #{config.root}/app/models/project_services - #{config.root}/app/workers/concerns - #{config.root}/app/policies/concerns - #{config.root}/app/services/concerns - #{config.root}/app/serializers/concerns - #{config.root}/app/finders/concerns #{config.root}/app/graphql/resolvers/concerns #{config.root}/app/graphql/mutations/concerns]) @@ -246,10 +241,7 @@ module Gitlab end # Use caching across all environments - # Full list of options: - # https://api.rubyonrails.org/classes/ActiveSupport/Cache/RedisCacheStore.html#method-c-new caching_config_hash = Gitlab::Redis::Cache.params - caching_config_hash[:compress] = false caching_config_hash[:namespace] = Gitlab::Redis::Cache::CACHE_NAMESPACE caching_config_hash[:expires_in] = 2.weeks # Cache should not grow forever if Sidekiq.server? # threaded context @@ -257,7 +249,7 @@ module Gitlab caching_config_hash[:pool_timeout] = 1 end - config.cache_store = :redis_cache_store, caching_config_hash + config.cache_store = :redis_store, caching_config_hash config.active_job.queue_adapter = :sidekiq @@ -293,10 +285,5 @@ module Gitlab Gitlab::Routing.add_helpers(project_url_helpers) Gitlab::Routing.add_helpers(MilestonesRoutingHelper) end - - # This makes generated cookies to be compatible with Rails 5.1 and older - # We can remove this when we're confident that there are no issues with the Rails 5.2 upgrade - # and we won't need to rollback to older versions - config.action_dispatch.use_authenticated_cookie_encryption = false end end diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index 226f2ec3722..973c2747838 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -50,24 +50,24 @@ production: &base # Content Security Policy # See https://guides.rubyonrails.org/security.html#content-security-policy content_security_policy: - enabled: false + enabled: true report_only: false directives: base_uri: child_src: - connect_src: "'self' http://localhost:3808 ws://localhost:3808 wss://localhost:3000" + connect_src: "'self' http://localhost:* ws://localhost:* wss://localhost:*" default_src: "'self'" font_src: form_action: frame_ancestors: "'self'" frame_src: "'self' https://www.google.com/recaptcha/ https://www.recaptcha.net/ https://content.googleapis.com https://content-compute.googleapis.com https://content-cloudbilling.googleapis.com https://content-cloudresourcemanager.googleapis.com" - img_src: "* data: blob" + img_src: "* data: blob:" manifest_src: media_src: - object_src: "'self' http://localhost:3808 'unsafe-inline' 'unsafe-eval' https://www.google.com/recaptcha/ https://www.recaptcha.net/ https://www.gstatic.com/recaptcha/ https://apis.google.com" - script_src: + object_src: "'none'" + script_src: "'self' 'unsafe-eval' http://localhost:* https://www.google.com/recaptcha/ https://www.recaptcha.net/ https://www.gstatic.com/recaptcha/ https://apis.google.com" style_src: "'self' 'unsafe-inline'" - worker_src: "http://localhost:3000 blob:" + worker_src: "'self' blob:" report_uri: # Trusted Proxies @@ -95,6 +95,15 @@ production: &base email_display_name: GitLab email_reply_to: noreply@example.com email_subject_suffix: '' + email_smime: + # Uncomment and set to true if you need to enable email S/MIME signing (default: false) + # enabled: false + # S/MIME private key file in PEM format, unencrypted + # Default is '.gitlab_smime_key' relative to Rails.root (i.e. root of the GitLab app). + # key_file: /home/git/gitlab/.gitlab_smime_key + # S/MIME public certificate key in PEM format, will be attached to signed messages + # Default is '.gitlab_smime_cert' relative to Rails.root (i.e. root of the GitLab app). + # cert_file: /home/git/gitlab/.gitlab_smime_cert # Email server smtp settings are in config/initializers/smtp_settings.rb.sample @@ -1090,6 +1099,27 @@ test: host: localhost port: 80 + content_security_policy: + enabled: true + report_only: false + directives: + base_uri: + child_src: + connect_src: + default_src: "'self'" + font_src: + form_action: + frame_ancestors: "'self'" + frame_src: "'self' https://www.google.com/recaptcha/ https://www.recaptcha.net/ https://content.googleapis.com https://content-compute.googleapis.com https://content-cloudbilling.googleapis.com https://content-cloudresourcemanager.googleapis.com" + img_src: "* data: blob:" + manifest_src: + media_src: + object_src: "'none'" + script_src: "'self' 'unsafe-eval' http://localhost:* https://www.google.com/recaptcha/ https://www.recaptcha.net/ https://www.gstatic.com/recaptcha/ https://apis.google.com" + style_src: "'self' 'unsafe-inline'" + worker_src: "'self' blob:" + report_uri: + # When you run tests we clone and set up gitlab-shell # In order to set it up correctly you need to specify # your system username you use to run GitLab diff --git a/config/initializers/0_inject_enterprise_edition_module.rb b/config/initializers/0_inject_enterprise_edition_module.rb index 4b21732e179..39595e23abe 100644 --- a/config/initializers/0_inject_enterprise_edition_module.rb +++ b/config/initializers/0_inject_enterprise_edition_module.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require 'active_support/inflector' + module InjectEnterpriseEditionModule def prepend_if_ee(constant) prepend(constant.constantize) if Gitlab.ee? diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 828732126b6..fdc6b0a05ab 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -1,5 +1,6 @@ require_relative '../settings' require_relative '../object_store_settings' +require_relative '../smime_signature_settings' # Default settings Settings['ldap'] ||= Settingslogic.new({}) @@ -171,6 +172,7 @@ Settings.gitlab['email_from'] ||= ENV['GITLAB_EMAIL_FROM'] || "gitlab@#{Settings Settings.gitlab['email_display_name'] ||= ENV['GITLAB_EMAIL_DISPLAY_NAME'] || 'GitLab' Settings.gitlab['email_reply_to'] ||= ENV['GITLAB_EMAIL_REPLY_TO'] || "noreply@#{Settings.gitlab.host}" Settings.gitlab['email_subject_suffix'] ||= ENV['GITLAB_EMAIL_SUBJECT_SUFFIX'] || "" +Settings.gitlab['email_smime'] = SmimeSignatureSettings.parse(Settings.gitlab['email_smime']) Settings.gitlab['base_url'] ||= Settings.__send__(:build_base_gitlab_url) Settings.gitlab['url'] ||= Settings.__send__(:build_gitlab_url) Settings.gitlab['user'] ||= 'git' diff --git a/config/initializers/7_prometheus_metrics.rb b/config/initializers/7_prometheus_metrics.rb index 70e5dcd042e..143b34b5368 100644 --- a/config/initializers/7_prometheus_metrics.rb +++ b/config/initializers/7_prometheus_metrics.rb @@ -32,6 +32,9 @@ end Sidekiq.configure_server do |config| config.on(:startup) do + # webserver metrics are cleaned up in config.ru: `warmup` block + Prometheus::CleanupMultiprocDirService.new.execute + Gitlab::Metrics::SidekiqMetricsExporter.instance.start end end diff --git a/config/initializers/8_devise.rb b/config/initializers/8_devise.rb index 3dd12c7e64d..8ef9ff6b7fc 100644 --- a/config/initializers/8_devise.rb +++ b/config/initializers/8_devise.rb @@ -81,7 +81,7 @@ Devise.setup do |config| # You can use this to let your user access some features of your application # without confirming the account, but blocking it after a certain period # (ie 2 days). - # config.allow_unconfirmed_access_for = 2.days + config.allow_unconfirmed_access_for = 30.days # Defines which key will be used when confirming an account # config.confirmation_keys = [ :email ] diff --git a/config/initializers/action_mailer_hooks.rb b/config/initializers/action_mailer_hooks.rb index f1b3c1f8ae8..02ca6ef13bf 100644 --- a/config/initializers/action_mailer_hooks.rb +++ b/config/initializers/action_mailer_hooks.rb @@ -10,3 +10,8 @@ ActionMailer::Base.register_interceptors( ) ActionMailer::Base.register_observer(::Gitlab::Email::Hook::DeliveryMetricsObserver) + +if Gitlab.config.gitlab.email_enabled && Gitlab.config.gitlab.email_smime.enabled + ActionMailer::Base.register_interceptor(::Gitlab::Email::Hook::SmimeSignatureInterceptor) + Gitlab::AppLogger.debug "S/MIME signing of outgoing emails enabled" +end diff --git a/config/initializers/elastic_client_setup.rb b/config/initializers/elastic_client_setup.rb index 2ecb7956007..a1abb29838b 100644 --- a/config/initializers/elastic_client_setup.rb +++ b/config/initializers/elastic_client_setup.rb @@ -5,46 +5,42 @@ require 'gitlab/current_settings' Gitlab.ee do + require 'elasticsearch/model' + + ### Modified from elasticsearch-model/lib/elasticsearch/model.rb + + [ + Elasticsearch::Model::Client::ClassMethods, + Elasticsearch::Model::Naming::ClassMethods, + Elasticsearch::Model::Indexing::ClassMethods, + Elasticsearch::Model::Searching::ClassMethods + ].each do |mod| + Elasticsearch::Model::Proxy::ClassMethodsProxy.include mod + end + + [ + Elasticsearch::Model::Client::InstanceMethods, + Elasticsearch::Model::Naming::InstanceMethods, + Elasticsearch::Model::Indexing::InstanceMethods, + Elasticsearch::Model::Serializing::InstanceMethods + ].each do |mod| + Elasticsearch::Model::Proxy::InstanceMethodsProxy.include mod + end + + Elasticsearch::Model::Proxy::InstanceMethodsProxy.class_eval <<-CODE, __FILE__, __LINE__ + 1 + def as_indexed_json(options={}) + target.respond_to?(:as_indexed_json) ? target.__send__(:as_indexed_json, options) : super + end + CODE + + ### Monkey patches + Elasticsearch::Model::Response::Records.prepend GemExtensions::Elasticsearch::Model::Response::Records Elasticsearch::Model::Adapter::Multiple::Records.prepend GemExtensions::Elasticsearch::Model::Adapter::Multiple::Records Elasticsearch::Model::Indexing::InstanceMethods.prepend GemExtensions::Elasticsearch::Model::Indexing::InstanceMethods - - module Elasticsearch - module Model - module Client - # This mutex is only used to synchronize *creation* of a new client, so - # all including classes can share the same client instance - CLIENT_MUTEX = Mutex.new - - cattr_accessor :cached_client - cattr_accessor :cached_config - - module ClassMethods - # Override the default ::Elasticsearch::Model::Client implementation to - # return a client configured from application settings. All including - # classes will use the same instance, which is refreshed automatically - # if the settings change. - # - # _client is present to match the arity of the overridden method, where - # it is also not used. - # - # @return [Elasticsearch::Transport::Client] - def client(_client = nil) - store = ::Elasticsearch::Model::Client - - store::CLIENT_MUTEX.synchronize do - config = Gitlab::CurrentSettings.elasticsearch_config - - if store.cached_client.nil? || config != store.cached_config - store.cached_client = ::Gitlab::Elastic::Client.build(config) - store.cached_config = config - end - end - - store.cached_client - end - end - end - end - end + Elasticsearch::Model::Adapter::ActiveRecord::Importing.prepend GemExtensions::Elasticsearch::Model::Adapter::ActiveRecord::Importing + Elasticsearch::Model::Client::InstanceMethods.prepend GemExtensions::Elasticsearch::Model::Client + Elasticsearch::Model::Client::ClassMethods.prepend GemExtensions::Elasticsearch::Model::Client + Elasticsearch::Model::ClassMethods.prepend GemExtensions::Elasticsearch::Model::Client + Elasticsearch::Model.singleton_class.prepend GemExtensions::Elasticsearch::Model::Client end diff --git a/config/initializers/invisible_captcha.rb b/config/initializers/invisible_captcha.rb new file mode 100644 index 00000000000..5177c730596 --- /dev/null +++ b/config/initializers/invisible_captcha.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +InvisibleCaptcha.setup do |config| + config.honeypots = %w(firstname lastname) + config.timestamp_enabled = true + config.timestamp_threshold = 4 +end diff --git a/config/initializers/rack_attack_logging.rb b/config/initializers/rack_attack_logging.rb index 7eb34bd69e5..b43fff24bb0 100644 --- a/config/initializers/rack_attack_logging.rb +++ b/config/initializers/rack_attack_logging.rb @@ -7,9 +7,9 @@ ActiveSupport::Notifications.subscribe('rack.attack') do |name, start, finish, r rack_attack_info = { message: 'Rack_Attack', env: req.env['rack.attack.match_type'], - ip: req.ip, + remote_ip: req.ip, request_method: req.request_method, - fullpath: req.fullpath + path: req.fullpath } if %w(throttle_authenticated_api throttle_authenticated_web).include? req.env['rack.attack.matched'] diff --git a/config/initializers/rest-client-hostname_override.rb b/config/initializers/rest-client-hostname_override.rb new file mode 100644 index 00000000000..80b123ebe61 --- /dev/null +++ b/config/initializers/rest-client-hostname_override.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +module RestClient + class Request + attr_accessor :hostname_override + + module UrlBlocker + def transmit(uri, req, payload, &block) + begin + ip, hostname_override = Gitlab::UrlBlocker.validate!(uri, allow_local_network: allow_settings_local_requests?, + allow_localhost: allow_settings_local_requests?, + dns_rebind_protection: dns_rebind_protection?) + + self.hostname_override = hostname_override + rescue Gitlab::UrlBlocker::BlockedUrlError => e + raise ArgumentError, "URL '#{uri}' is blocked: #{e.message}" + end + + # Gitlab::UrlBlocker returns a Addressable::URI which we need to coerce + # to URI so that rest-client can use it to determine if it's a + # URI::HTTPS or not. It uses it to set `net.use_ssl` to true or not: + # + # https://github.com/rest-client/rest-client/blob/f450a0f086f1cd1049abbef2a2c66166a1a9ba71/lib/restclient/request.rb#L656 + ip_as_uri = URI.parse(ip) + super(ip_as_uri, req, payload, &block) + end + + def net_http_object(hostname, port) + super.tap do |http| + http.hostname_override = hostname_override if hostname_override + end + end + + private + + def dns_rebind_protection? + return false if Gitlab.http_proxy_env? + + Gitlab::CurrentSettings.dns_rebinding_protection_enabled? + end + + def allow_settings_local_requests? + Gitlab::CurrentSettings.allow_local_requests_from_web_hooks_and_services? + end + end + + prepend UrlBlocker + end +end diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb index 7217f098fd9..9f3e104bc2b 100644 --- a/config/initializers/sidekiq.rb +++ b/config/initializers/sidekiq.rb @@ -28,11 +28,13 @@ if Rails.env.development? end enable_json_logs = Gitlab.config.sidekiq.log_format == 'json' +enable_sidekiq_monitor = ENV.fetch("SIDEKIQ_MONITOR_WORKER", 0).to_i.nonzero? Sidekiq.configure_server do |config| config.redis = queues_config_hash config.server_middleware do |chain| + chain.add Gitlab::SidekiqMiddleware::Monitor if enable_sidekiq_monitor chain.add Gitlab::SidekiqMiddleware::Metrics if Settings.monitoring.sidekiq_exporter chain.add Gitlab::SidekiqMiddleware::ArgumentsLogger if ENV['SIDEKIQ_LOG_ARGUMENTS'] && !enable_json_logs chain.add Gitlab::SidekiqMiddleware::MemoryKiller if ENV['SIDEKIQ_MEMORY_KILLER_MAX_RSS'] @@ -57,6 +59,8 @@ Sidekiq.configure_server do |config| # Clear any connections that might have been obtained before starting # Sidekiq (e.g. in an initializer). ActiveRecord::Base.clear_all_connections! + + Gitlab::SidekiqMonitor.instance.start if enable_sidekiq_monitor end if enable_reliable_fetch? diff --git a/config/initializers/tracing.rb b/config/initializers/tracing.rb index 3c8779f238f..5b55a06692e 100644 --- a/config/initializers/tracing.rb +++ b/config/initializers/tracing.rb @@ -21,9 +21,13 @@ if Labkit::Tracing.enabled? end end + # Instrument Redis + Labkit::Tracing::Redis.instrument + # Instrument Rails Labkit::Tracing::Rails::ActiveRecordSubscriber.instrument Labkit::Tracing::Rails::ActionViewSubscriber.instrument + Labkit::Tracing::Rails::ActiveSupportSubscriber.instrument # In multi-processed clustered architectures (puma, unicorn) don't # start tracing until the worker processes are spawned. This works diff --git a/config/initializers/zz_metrics.rb b/config/initializers/zz_metrics.rb index b005fdf159b..af4aec7b355 100644 --- a/config/initializers/zz_metrics.rb +++ b/config/initializers/zz_metrics.rb @@ -100,11 +100,7 @@ def instrument_classes(instrumentation) instrumentation.instrument_instance_methods(Gitlab::Elastic::SnippetSearchResults) instrumentation.instrument_methods(Gitlab::Elastic::Helper) - instrumentation.instrument_instance_methods(Elastic::ApplicationSearch) - instrumentation.instrument_instance_methods(Elastic::IssuesSearch) - instrumentation.instrument_instance_methods(Elastic::MergeRequestsSearch) - instrumentation.instrument_instance_methods(Elastic::MilestonesSearch) - instrumentation.instrument_instance_methods(Elastic::NotesSearch) + instrumentation.instrument_instance_methods(Elastic::ApplicationVersionedSearch) instrumentation.instrument_instance_methods(Elastic::ProjectsSearch) instrumentation.instrument_instance_methods(Elastic::RepositoriesSearch) instrumentation.instrument_instance_methods(Elastic::SnippetsSearch) diff --git a/config/locales/doorkeeper.en.yml b/config/locales/doorkeeper.en.yml index a8234263275..258d8a99986 100644 --- a/config/locales/doorkeeper.en.yml +++ b/config/locales/doorkeeper.en.yml @@ -69,7 +69,7 @@ en: email: Allows read-only access to the user's primary email address using OpenID Connect scope_desc: api: - Grants complete read/write access to the API, including all groups and projects. + Grants complete read/write access to the API, including all groups and projects, the container registry, and the package registry. read_user: Grants read-only access to the authenticated user's profile through the /user API endpoint, which includes username, public email, and full name. Also grants access to read-only API endpoints under /users. read_repository: diff --git a/config/locales/invisible_captcha.en.yml b/config/locales/invisible_captcha.en.yml new file mode 100644 index 00000000000..5978549c0c3 --- /dev/null +++ b/config/locales/invisible_captcha.en.yml @@ -0,0 +1,4 @@ +en: + invisible_captcha: + sentence_for_humans: If you are human, please ignore this field. + timestamp_error_message: That was a bit too quick! Please resubmit. diff --git a/config/prometheus/common_metrics.yml b/config/prometheus/common_metrics.yml index 32475ef8380..08504d6f7d5 100644 --- a/config/prometheus/common_metrics.yml +++ b/config/prometheus/common_metrics.yml @@ -166,7 +166,7 @@ panel_groups: label: Total (cores) unit: "cores" - title: "Memory Usage (Pod average)" - type: "area-chart" + type: "line-chart" y_label: "Memory Used per Pod (MB)" weight: 2 metrics: @@ -175,7 +175,7 @@ panel_groups: label: Pod average (MB) unit: MB - title: "Canary: Memory Usage (Pod Average)" - type: "area-chart" + type: "line-chart" y_label: "Memory Used per Pod (MB)" weight: 2 metrics: @@ -185,7 +185,7 @@ panel_groups: unit: MB track: canary - title: "Core Usage (Pod Average)" - type: "area-chart" + type: "line-chart" y_label: "Cores per Pod" weight: 1 metrics: @@ -194,7 +194,7 @@ panel_groups: label: Pod average (cores) unit: "cores" - title: "Canary: Core Usage (Pod Average)" - type: "area-chart" + type: "line-chart" y_label: "Cores per Pod" weight: 1 metrics: diff --git a/config/routes.rb b/config/routes.rb index 459f2b22bf0..d633228a495 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -58,6 +58,7 @@ Rails.application.routes.draw do # Search get 'search' => 'search#show' get 'search/autocomplete' => 'search#autocomplete', as: :search_autocomplete + get 'search/count' => 'search#count', as: :search_count # JSON Web Token get 'jwt/auth' => 'jwt#auth' @@ -108,6 +109,7 @@ Rails.application.routes.draw do Gitlab.ee do draw :smartcard draw :jira_connect + draw :username end Gitlab.ee do diff --git a/config/routes/project.rb b/config/routes/project.rb index b9258a35f0c..29e462f904d 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -359,6 +359,9 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do collection do resource :pipelines_settings, path: 'settings', only: [:show, :update] get :charts + scope '(*ref)', constraints: { ref: Gitlab::PathRegex.git_reference_regex } do + get :latest, action: :show, defaults: { latest: true } + end end member do @@ -477,7 +480,11 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do # in JSON format, or a request for tag named `latest.json`. scope format: false do resources :tags, only: [:index, :destroy], - constraints: { id: Gitlab::Regex.container_registry_tag_regex } + constraints: { id: Gitlab::Regex.container_registry_tag_regex } do + collection do + delete :bulk_destroy + end + end end end end @@ -505,7 +512,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do get :discussions, format: :json Gitlab.ee do - get 'designs(/*vueroute)', to: 'issues#show', format: false + get 'designs(/*vueroute)', to: 'issues#show', as: :designs, format: false end end diff --git a/config/routes/user.rb b/config/routes/user.rb index 3f768d5d384..d4616c8080d 100644 --- a/config/routes/user.rb +++ b/config/routes/user.rb @@ -43,13 +43,6 @@ scope '-/users', module: :users do end end -scope '-/users', module: :users do - resources :terms, only: [:index] do - post :accept, on: :member - post :decline, on: :member - end -end - scope(constraints: { username: Gitlab::PathRegex.root_namespace_route_regex }) do scope(path: 'users/:username', as: :user, diff --git a/config/routes/wiki.rb b/config/routes/wiki.rb index 2ca52e55fca..d439c99270e 100644 --- a/config/routes/wiki.rb +++ b/config/routes/wiki.rb @@ -2,6 +2,7 @@ scope(controller: :wikis) do scope(path: 'wikis', as: :wikis) do get :git_access get :pages + get :new get '/', to: redirect('%{namespace_id}/%{project_id}/wikis/home') post '/', to: 'wikis#create' end diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml index c7586aa1e38..ea165508d29 100644 --- a/config/sidekiq_queues.yml +++ b/config/sidekiq_queues.yml @@ -113,3 +113,4 @@ - [elastic_namespace_indexer, 1] - [export_csv, 1] - [incident_management, 2] + - [jira_connect, 1] diff --git a/config/smime_signature_settings.rb b/config/smime_signature_settings.rb new file mode 100644 index 00000000000..3d19db84c19 --- /dev/null +++ b/config/smime_signature_settings.rb @@ -0,0 +1,11 @@ +# Set default values for email_smime settings +class SmimeSignatureSettings + def self.parse(email_smime) + email_smime ||= Settingslogic.new({}) + email_smime['enabled'] = false unless email_smime['enabled'] + email_smime['key_file'] ||= Rails.root.join('.gitlab_smime_key') + email_smime['cert_file'] ||= Rails.root.join('.gitlab_smime_cert') + + email_smime + end +end diff --git a/config/webpack.config.js b/config/webpack.config.js index 442b4b4c21e..969a84e85dd 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -298,6 +298,13 @@ module.exports = { from: path.join(ROOT_PATH, 'node_modules/pdfjs-dist/cmaps/'), to: path.join(ROOT_PATH, 'public/assets/webpack/cmaps/'), }, + { + from: path.join( + ROOT_PATH, + 'node_modules/@gitlab/visual-review-tools/dist/visual_review_toolbar.js', + ), + to: path.join(ROOT_PATH, 'public/assets/webpack'), + }, ]), // compression can require a lot of compute time and is disabled in CI diff --git a/config/webpack.config.review_toolbar.js b/config/webpack.config.review_toolbar.js deleted file mode 100644 index baaba7ed387..00000000000 --- a/config/webpack.config.review_toolbar.js +++ /dev/null @@ -1,58 +0,0 @@ -const path = require('path'); -const CompressionPlugin = require('compression-webpack-plugin'); - -const ROOT_PATH = path.resolve(__dirname, '..'); -const CACHE_PATH = process.env.WEBPACK_CACHE_PATH || path.join(ROOT_PATH, 'tmp/cache'); -const NO_SOURCEMAPS = process.env.NO_SOURCEMAPS; -const IS_PRODUCTION = process.env.NODE_ENV === 'production'; - -const devtool = IS_PRODUCTION ? 'source-map' : 'cheap-module-eval-source-map'; - -const alias = { - vendor: path.join(ROOT_PATH, 'vendor/assets/javascripts'), - spec: path.join(ROOT_PATH, 'spec/javascripts'), -}; - -module.exports = { - mode: IS_PRODUCTION ? 'production' : 'development', - - context: path.join(ROOT_PATH, 'app/assets/javascripts'), - - name: 'visual_review_toolbar', - - entry: './visual_review_toolbar', - - output: { - path: path.join(ROOT_PATH, 'public/assets/webpack'), - filename: 'visual_review_toolbar.js', - library: 'VisualReviewToolbar', - libraryTarget: 'var', - }, - - resolve: { - alias, - }, - - module: { - rules: [ - { - test: /\.js$/, - loader: 'babel-loader', - options: { - cacheDirectory: path.join(CACHE_PATH, 'babel-loader'), - }, - }, - { - test: /\.css$/, - use: ['style-loader', 'css-loader'], - }, - ], - }, - - plugins: [ - // compression can require a lot of compute time and is disabled in CI - new CompressionPlugin(), - ].filter(Boolean), - - devtool: NO_SOURCEMAPS ? false : devtool, -}; diff --git a/danger/changelog/Dangerfile b/danger/changelog/Dangerfile index 63b2f6f5c5c..2d1ca64a9e8 100644 --- a/danger/changelog/Dangerfile +++ b/danger/changelog/Dangerfile @@ -1,19 +1,18 @@ +# frozen_string_literal: true # rubocop:disable Style/SignalException require 'yaml' -NO_CHANGELOG_LABELS = %w[backstage ci-build Documentation meta QA test].freeze -SEE_DOC = "See [the documentation](https://docs.gitlab.com/ce/development/changelog.html).".freeze -CREATE_CHANGELOG_MESSAGE = <<~MSG.freeze +NO_CHANGELOG_LABELS = %w[backstage ci-build meta].freeze +SEE_DOC = "See [the documentation](https://docs.gitlab.com/ce/development/changelog.html)." +CREATE_CHANGELOG_MESSAGE = <<~MSG You can create one with: ``` bin/changelog -m %<mr_iid>s "%<mr_title>s" ``` -If your merge request doesn't warrant a CHANGELOG entry, -consider adding any of the %<labels>s labels. -#{SEE_DOC} +Note: Merge requests with %<labels>s do not trigger this check. MSG def ee_changelog?(changelog_path) @@ -60,7 +59,7 @@ if changelog_needed if changelog_found check_changelog(changelog_found) else - warn "**[CHANGELOG missing](https://docs.gitlab.com/ce/development/changelog.html).**\n\n" + + message "**[CHANGELOG missing](https://docs.gitlab.com/ce/development/changelog.html)**: If this merge request [doesn't need a CHANGELOG entry](https://docs.gitlab.com/ee/development/changelog.html#what-warrants-a-changelog-entry), feel free to ignore this message.\n\n" + format(CREATE_CHANGELOG_MESSAGE, mr_iid: gitlab.mr_json["iid"], mr_title: mr_title, labels: presented_no_changelog_labels) end end diff --git a/danger/only_documentation/Dangerfile b/danger/only_documentation/Dangerfile index ff65f8713d2..dad12c0d29c 100644 --- a/danger/only_documentation/Dangerfile +++ b/danger/only_documentation/Dangerfile @@ -1,7 +1,7 @@ # rubocop:disable Style/SignalException # frozen_string_literal: true -has_only_docs_changes = helper.all_changed_files.all? { |file| file.start_with?('doc/', '.gitlab/ci/docs.gitlab-ci.yml', '.mdlrc') } +has_only_docs_changes = helper.all_changed_files.all? { |file| file.start_with?('doc/', '.gitlab/ci/docs.gitlab-ci.yml', '.mdlrc') || file.end_with?('.md') } is_docs_only_branch = gitlab.branch_for_head =~ /(^docs[\/-].*|.*-docs$)/ if is_docs_only_branch && !has_only_docs_changes diff --git a/db/fixtures/development/14_pipelines.rb b/db/fixtures/development/14_pipelines.rb index 05bda7d3672..5c8b681fa92 100644 --- a/db/fixtures/development/14_pipelines.rb +++ b/db/fixtures/development/14_pipelines.rb @@ -1,7 +1,7 @@ require './spec/support/sidekiq' class Gitlab::Seeder::Pipelines - STAGES = %w[build test security deploy notify] + STAGES = %w[build test deploy notify] BUILDS = [ # build stage { name: 'build:linux', stage: 'build', status: :success, @@ -31,16 +31,6 @@ class Gitlab::Seeder::Pipelines { name: 'spinach:osx', stage: 'test', status: :failed, allow_failure: true, queued_at: 8.hour.ago, started_at: 8.hour.ago, finished_at: 7.hour.ago }, - # security stage - { name: 'dast', stage: 'security', status: :success, - queued_at: 8.hour.ago, started_at: 8.hour.ago, finished_at: 7.hour.ago }, - { name: 'sast', stage: 'security', status: :success, - queued_at: 8.hour.ago, started_at: 8.hour.ago, finished_at: 7.hour.ago }, - { name: 'dependency_scanning', stage: 'security', status: :success, - queued_at: 8.hour.ago, started_at: 8.hour.ago, finished_at: 7.hour.ago }, - { name: 'container_scanning', stage: 'security', status: :success, - queued_at: 8.hour.ago, started_at: 8.hour.ago, finished_at: 7.hour.ago }, - # deploy stage { name: 'staging', stage: 'deploy', environment: 'staging', status_event: :success, options: { environment: { action: 'start', on_stop: 'stop staging' } }, @@ -127,11 +117,6 @@ class Gitlab::Seeder::Pipelines setup_artifacts(build) setup_test_reports(build) - if build.ref == build.project.default_branch - setup_security_reports_file(build) - else - setup_security_reports_legacy_archive(build) - end setup_build_log(build) build.project.environments. @@ -167,55 +152,6 @@ class Gitlab::Seeder::Pipelines end end - def setup_security_reports_file(build) - return unless build.stage == "security" - - # we have two sources: master and feature-branch - branch_name = build.ref == build.project.default_branch ? - 'master' : 'feature-branch' - - artifacts_cache_file(security_reports_path(branch_name, build.name)) do |file| - build.job_artifacts.build( - project: build.project, - file_type: build.name, - file_format: :raw, - file: file) - end - end - - def setup_security_reports_legacy_archive(build) - return unless build.stage == "security" - - # we have two sources: master and feature-branch - branch_name = build.ref == build.project.default_branch ? - 'master' : 'feature-branch' - - artifacts_cache_file(security_reports_archive_path(branch_name)) do |file| - build.job_artifacts.build( - project: build.project, - file_type: :archive, - file_format: :zip, - file: file) - end - - # assign dummy metadata - artifacts_cache_file(artifacts_metadata_path) do |file| - build.job_artifacts.build( - project: build.project, - file_type: :metadata, - file_format: :gzip, - file: file) - end - - build.options = { - artifacts: { - paths: [ - Ci::JobArtifact::DEFAULT_FILE_NAMES.fetch(build.name.to_sym) - ] - } - } - end - def setup_build_log(build) if %w(running success failed).include?(build.status) build.trace.set(FFaker::Lorem.paragraphs(6).join("\n\n")) @@ -267,15 +203,6 @@ class Gitlab::Seeder::Pipelines Rails.root + 'spec/fixtures/junit/junit.xml.gz' end - def security_reports_archive_path(branch) - Rails.root.join('spec', 'fixtures', 'security-reports', branch + '.zip') - end - - def security_reports_path(branch, name) - file_name = Ci::JobArtifact::DEFAULT_FILE_NAMES.fetch(name.to_sym) - Rails.root.join('spec', 'fixtures', 'security-reports', branch, file_name) - end - def artifacts_cache_file(file_path) file = Tempfile.new("artifacts") file.close diff --git a/db/fixtures/development/15_award_emoji.rb b/db/fixtures/development/15_award_emoji.rb index 137a036edaf..a9dcc048586 100644 --- a/db/fixtures/development/15_award_emoji.rb +++ b/db/fixtures/development/15_award_emoji.rb @@ -1,35 +1,22 @@ require './spec/support/sidekiq' Gitlab::Seeder.quiet do - emoji = Gitlab::Emoji.emojis.keys + EMOJI = Gitlab::Emoji.emojis.keys - Issue.order(Gitlab::Database.random).limit(Issue.count / 2).each do |issue| - project = issue.project + def seed_award_emoji(klass) + klass.order(Gitlab::Database.random).limit(klass.count / 2).each do |awardable| + awardable.project.authorized_users.where('project_authorizations.access_level > ?', Gitlab::Access::GUEST).sample(2).each do |user| + AwardEmojis::AddService.new(awardable, EMOJI.sample, user).execute - project.team.users.sample(2).each do |user| - issue.create_award_emoji(emoji.sample, user) + awardable.notes.user.sample(2).each do |note| + AwardEmojis::AddService.new(note, EMOJI.sample, user).execute + end - issue.notes.sample(2).each do |note| - next if note.system? - note.create_award_emoji(emoji.sample, user) + print '.' end - - print '.' end end - MergeRequest.order(Gitlab::Database.random).limit(MergeRequest.count / 2).each do |mr| - project = mr.project - - project.team.users.sample(2).each do |user| - mr.create_award_emoji(emoji.sample, user) - - mr.notes.sample(2).each do |note| - next if note.system? - note.create_award_emoji(emoji.sample, user) - end - - print '.' - end - end + seed_award_emoji(Issue) + seed_award_emoji(MergeRequest) end diff --git a/db/migrate/20171230123729_init_schema.rb b/db/migrate/20171230123729_init_schema.rb index fa90b37954f..a474ea2f925 100644 --- a/db/migrate/20171230123729_init_schema.rb +++ b/db/migrate/20171230123729_init_schema.rb @@ -5,6 +5,7 @@ # rubocop:disable Metrics/AbcSize # rubocop:disable Migration/AddConcurrentForeignKey # rubocop:disable Style/WordArray +# rubocop:disable Migration/AddLimitToStringColumns class InitSchema < ActiveRecord::Migration[4.2] DOWNTIME = false @@ -1852,3 +1853,4 @@ class InitSchema < ActiveRecord::Migration[4.2] raise ActiveRecord::IrreversibleMigration, "The initial migration is not revertable" end end +# rubocop:enable Migration/AddLimitToStringColumns diff --git a/db/migrate/20180101160629_create_prometheus_metrics.rb b/db/migrate/20180101160629_create_prometheus_metrics.rb index e3b1ed710d6..a098b999a0a 100644 --- a/db/migrate/20180101160629_create_prometheus_metrics.rb +++ b/db/migrate/20180101160629_create_prometheus_metrics.rb @@ -4,6 +4,7 @@ class CreatePrometheusMetrics < ActiveRecord::Migration[4.2] DOWNTIME = false def change + # rubocop:disable Migration/AddLimitToStringColumns create_table :prometheus_metrics do |t| t.references :project, index: true, foreign_key: { on_delete: :cascade }, null: false t.string :title, null: false @@ -14,5 +15,6 @@ class CreatePrometheusMetrics < ActiveRecord::Migration[4.2] t.integer :group, null: false, index: true t.timestamps_with_timezone null: false end + # rubocop:enable Migration/AddLimitToStringColumns end end diff --git a/db/migrate/20180122162010_add_auto_devops_domain_to_application_settings.rb b/db/migrate/20180122162010_add_auto_devops_domain_to_application_settings.rb index c76dc5b3a68..eb446ad0d72 100644 --- a/db/migrate/20180122162010_add_auto_devops_domain_to_application_settings.rb +++ b/db/migrate/20180122162010_add_auto_devops_domain_to_application_settings.rb @@ -8,6 +8,6 @@ class AddAutoDevopsDomainToApplicationSettings < ActiveRecord::Migration[4.2] DOWNTIME = false def change - add_column :application_settings, :auto_devops_domain, :string + add_column :application_settings, :auto_devops_domain, :string # rubocop:disable Migration/AddLimitToStringColumns end end diff --git a/db/migrate/20180129193323_add_uploads_builder_context.rb b/db/migrate/20180129193323_add_uploads_builder_context.rb index c7227bf0f1e..710fa7b3ba8 100644 --- a/db/migrate/20180129193323_add_uploads_builder_context.rb +++ b/db/migrate/20180129193323_add_uploads_builder_context.rb @@ -8,7 +8,9 @@ class AddUploadsBuilderContext < ActiveRecord::Migration[4.2] DOWNTIME = false def change + # rubocop:disable Migration/AddLimitToStringColumns add_column :uploads, :mount_point, :string add_column :uploads, :secret, :string + # rubocop:enable Migration/AddLimitToStringColumns end end diff --git a/db/migrate/20180212030105_add_external_ip_to_clusters_applications_ingress.rb b/db/migrate/20180212030105_add_external_ip_to_clusters_applications_ingress.rb index e2a9a68b1ad..78aa2014601 100644 --- a/db/migrate/20180212030105_add_external_ip_to_clusters_applications_ingress.rb +++ b/db/migrate/20180212030105_add_external_ip_to_clusters_applications_ingress.rb @@ -4,6 +4,6 @@ class AddExternalIpToClustersApplicationsIngress < ActiveRecord::Migration[4.2] DOWNTIME = false def change - add_column :clusters_applications_ingress, :external_ip, :string + add_column :clusters_applications_ingress, :external_ip, :string # rubocop:disable Migration/AddLimitToStringColumns end end diff --git a/db/migrate/20180214093516_create_badges.rb b/db/migrate/20180214093516_create_badges.rb index 66e017b115a..fe27d465571 100644 --- a/db/migrate/20180214093516_create_badges.rb +++ b/db/migrate/20180214093516_create_badges.rb @@ -2,6 +2,7 @@ class CreateBadges < ActiveRecord::Migration[4.2] DOWNTIME = false def change + # rubocop:disable Migration/AddLimitToStringColumns create_table :badges do |t| t.string :link_url, null: false t.string :image_url, null: false @@ -11,6 +12,7 @@ class CreateBadges < ActiveRecord::Migration[4.2] t.timestamps_with_timezone null: false end + # rubocop:enable Migration/AddLimitToStringColumns # rubocop:disable Migration/AddConcurrentForeignKey add_foreign_key :badges, :namespaces, column: :group_id, on_delete: :cascade diff --git a/db/migrate/20180214155405_create_clusters_applications_runners.rb b/db/migrate/20180214155405_create_clusters_applications_runners.rb index ce594c91890..f611fefbb0d 100644 --- a/db/migrate/20180214155405_create_clusters_applications_runners.rb +++ b/db/migrate/20180214155405_create_clusters_applications_runners.rb @@ -13,7 +13,7 @@ class CreateClustersApplicationsRunners < ActiveRecord::Migration[4.2] t.index :cluster_id, unique: true t.integer :status, null: false t.timestamps_with_timezone null: false - t.string :version, null: false + t.string :version, null: false # rubocop:disable Migration/AddLimitToStringColumns t.text :status_reason end diff --git a/db/migrate/20180216120000_add_pages_domain_verification.rb b/db/migrate/20180216120000_add_pages_domain_verification.rb index f709f5a5809..b2f19f2e1a9 100644 --- a/db/migrate/20180216120000_add_pages_domain_verification.rb +++ b/db/migrate/20180216120000_add_pages_domain_verification.rb @@ -3,6 +3,6 @@ class AddPagesDomainVerification < ActiveRecord::Migration[4.2] def change add_column :pages_domains, :verified_at, :datetime_with_timezone - add_column :pages_domains, :verification_code, :string + add_column :pages_domains, :verification_code, :string # rubocop:disable Migration/AddLimitToStringColumns end end diff --git a/db/migrate/20180222043024_add_ip_address_to_runner.rb b/db/migrate/20180222043024_add_ip_address_to_runner.rb index b52366c0be1..08fb0bd900c 100644 --- a/db/migrate/20180222043024_add_ip_address_to_runner.rb +++ b/db/migrate/20180222043024_add_ip_address_to_runner.rb @@ -4,6 +4,6 @@ class AddIpAddressToRunner < ActiveRecord::Migration[4.2] DOWNTIME = false def change - add_column :ci_runners, :ip_address, :string + add_column :ci_runners, :ip_address, :string # rubocop:disable Migration/AddLimitToStringColumns end end diff --git a/db/migrate/20180308125206_add_user_internal_regex_to_application_setting.rb b/db/migrate/20180308125206_add_user_internal_regex_to_application_setting.rb index 5e4bf96f86f..9bdd44baf58 100644 --- a/db/migrate/20180308125206_add_user_internal_regex_to_application_setting.rb +++ b/db/migrate/20180308125206_add_user_internal_regex_to_application_setting.rb @@ -4,7 +4,7 @@ class AddUserInternalRegexToApplicationSetting < ActiveRecord::Migration[4.2] DOWNTIME = false def up - add_column :application_settings, :user_default_internal_regex, :string, null: true + add_column :application_settings, :user_default_internal_regex, :string, null: true # rubocop:disable Migration/AddLimitToStringColumns end def down diff --git a/db/migrate/20180315160435_add_external_auth_mutual_tls_fields_to_project_settings.rb b/db/migrate/20180315160435_add_external_auth_mutual_tls_fields_to_project_settings.rb index ee3d1078f5e..c379d207ff0 100644 --- a/db/migrate/20180315160435_add_external_auth_mutual_tls_fields_to_project_settings.rb +++ b/db/migrate/20180315160435_add_external_auth_mutual_tls_fields_to_project_settings.rb @@ -2,6 +2,7 @@ class AddExternalAuthMutualTlsFieldsToProjectSettings < ActiveRecord::Migration[ DOWNTIME = false def change + # rubocop:disable Migration/AddLimitToStringColumns add_column :application_settings, :external_auth_client_cert, :text add_column :application_settings, @@ -12,5 +13,6 @@ class AddExternalAuthMutualTlsFieldsToProjectSettings < ActiveRecord::Migration[ :encrypted_external_auth_client_key_pass, :string add_column :application_settings, :encrypted_external_auth_client_key_pass_iv, :string + # rubocop:enable Migration/AddLimitToStringColumns end end diff --git a/db/migrate/20180319190020_create_deploy_tokens.rb b/db/migrate/20180319190020_create_deploy_tokens.rb index a4d797679c5..f444521b3ae 100644 --- a/db/migrate/20180319190020_create_deploy_tokens.rb +++ b/db/migrate/20180319190020_create_deploy_tokens.rb @@ -2,6 +2,7 @@ class CreateDeployTokens < ActiveRecord::Migration[4.2] DOWNTIME = false def change + # rubocop:disable Migration/AddLimitToStringColumns create_table :deploy_tokens do |t| t.boolean :revoked, default: false t.boolean :read_repository, null: false, default: false @@ -15,5 +16,6 @@ class CreateDeployTokens < ActiveRecord::Migration[4.2] t.index [:token, :expires_at, :id], where: "(revoked IS FALSE)" end + # rubocop:enable Migration/AddLimitToStringColumns end end diff --git a/db/migrate/20180502122856_create_project_mirror_data.rb b/db/migrate/20180502122856_create_project_mirror_data.rb index 9781815a97b..04367e1c98b 100644 --- a/db/migrate/20180502122856_create_project_mirror_data.rb +++ b/db/migrate/20180502122856_create_project_mirror_data.rb @@ -3,6 +3,7 @@ class CreateProjectMirrorData < ActiveRecord::Migration[4.2] DOWNTIME = false + # rubocop:disable Migration/AddLimitToStringColumns def up if table_exists?(:project_mirror_data) add_column :project_mirror_data, :status, :string unless column_exists?(:project_mirror_data, :status) @@ -17,6 +18,7 @@ class CreateProjectMirrorData < ActiveRecord::Migration[4.2] end end end + # rubocop:enable Migration/AddLimitToStringColumns def down remove_column :project_mirror_data, :status diff --git a/db/migrate/20180503131624_create_remote_mirrors.rb b/db/migrate/20180503131624_create_remote_mirrors.rb index 288ae365f0f..a079c1b3328 100644 --- a/db/migrate/20180503131624_create_remote_mirrors.rb +++ b/db/migrate/20180503131624_create_remote_mirrors.rb @@ -5,6 +5,7 @@ class CreateRemoteMirrors < ActiveRecord::Migration[4.2] disable_ddl_transaction! + # rubocop:disable Migration/AddLimitToStringColumns def up return if table_exists?(:remote_mirrors) @@ -27,6 +28,7 @@ class CreateRemoteMirrors < ActiveRecord::Migration[4.2] t.timestamps null: false end end + # rubocop:enable Migration/AddLimitToStringColumns def down # ee/db/migrate/20160321161032_create_remote_mirrors_ee.rb will remove the table diff --git a/db/migrate/20180503175053_ensure_missing_columns_to_project_mirror_data.rb b/db/migrate/20180503175053_ensure_missing_columns_to_project_mirror_data.rb index 3775b3a08c9..f00493ed515 100644 --- a/db/migrate/20180503175053_ensure_missing_columns_to_project_mirror_data.rb +++ b/db/migrate/20180503175053_ensure_missing_columns_to_project_mirror_data.rb @@ -4,8 +4,8 @@ class EnsureMissingColumnsToProjectMirrorData < ActiveRecord::Migration[4.2] DOWNTIME = false def up - add_column :project_mirror_data, :status, :string unless column_exists?(:project_mirror_data, :status) - add_column :project_mirror_data, :jid, :string unless column_exists?(:project_mirror_data, :jid) + add_column :project_mirror_data, :status, :string unless column_exists?(:project_mirror_data, :status) # rubocop:disable Migration/AddLimitToStringColumns + add_column :project_mirror_data, :jid, :string unless column_exists?(:project_mirror_data, :jid) # rubocop:disable Migration/AddLimitToStringColumns add_column :project_mirror_data, :last_error, :text unless column_exists?(:project_mirror_data, :last_error) end diff --git a/db/migrate/20180511131058_create_clusters_applications_jupyter.rb b/db/migrate/20180511131058_create_clusters_applications_jupyter.rb index 749aeeb4792..4633d930e2d 100644 --- a/db/migrate/20180511131058_create_clusters_applications_jupyter.rb +++ b/db/migrate/20180511131058_create_clusters_applications_jupyter.rb @@ -7,17 +7,19 @@ class CreateClustersApplicationsJupyter < ActiveRecord::Migration[4.2] DOWNTIME = false def change + # rubocop:disable Migration/AddLimitToStringColumns create_table :clusters_applications_jupyter do |t| t.references :cluster, null: false, unique: true, foreign_key: { on_delete: :cascade } t.references :oauth_application, foreign_key: { on_delete: :nullify } t.integer :status, null: false - t.string :version, null: false - t.string :hostname + t.string :version, null: false # rubocop:disable Migration/AddLimitToStringColumns + t.string :hostname # rubocop:disable Migration/AddLimitToStringColumns t.timestamps_with_timezone null: false t.text :status_reason end + # rubocop:enable Migration/AddLimitToStringColumns end end diff --git a/db/migrate/20180515121227_create_notes_diff_files.rb b/db/migrate/20180515121227_create_notes_diff_files.rb index e50324d8599..5f6dba11ff9 100644 --- a/db/migrate/20180515121227_create_notes_diff_files.rb +++ b/db/migrate/20180515121227_create_notes_diff_files.rb @@ -4,6 +4,7 @@ class CreateNotesDiffFiles < ActiveRecord::Migration[4.2] disable_ddl_transaction! def change + # rubocop:disable Migration/AddLimitToStringColumns create_table :note_diff_files do |t| t.references :diff_note, references: :notes, null: false, index: { unique: true } t.text :diff, null: false @@ -18,5 +19,6 @@ class CreateNotesDiffFiles < ActiveRecord::Migration[4.2] # rubocop:disable Migration/AddConcurrentForeignKey add_foreign_key :note_diff_files, :notes, column: :diff_note_id, on_delete: :cascade + # rubocop:enable Migration/AddLimitToStringColumns end end diff --git a/db/migrate/20180529093006_ensure_remote_mirror_columns.rb b/db/migrate/20180529093006_ensure_remote_mirror_columns.rb index 207e1f089fb..a0a1150f022 100644 --- a/db/migrate/20180529093006_ensure_remote_mirror_columns.rb +++ b/db/migrate/20180529093006_ensure_remote_mirror_columns.rb @@ -8,7 +8,7 @@ class EnsureRemoteMirrorColumns < ActiveRecord::Migration[4.2] def up # rubocop:disable Migration/Datetime add_column :remote_mirrors, :last_update_started_at, :datetime unless column_exists?(:remote_mirrors, :last_update_started_at) - add_column :remote_mirrors, :remote_name, :string unless column_exists?(:remote_mirrors, :remote_name) + add_column :remote_mirrors, :remote_name, :string unless column_exists?(:remote_mirrors, :remote_name) # rubocop:disable Migration/AddLimitToStringColumns unless column_exists?(:remote_mirrors, :only_protected_branches) add_column_with_default(:remote_mirrors, diff --git a/db/migrate/20180531185349_add_repository_languages.rb b/db/migrate/20180531185349_add_repository_languages.rb index 26a01c3bb26..d517c21c26c 100644 --- a/db/migrate/20180531185349_add_repository_languages.rb +++ b/db/migrate/20180531185349_add_repository_languages.rb @@ -4,6 +4,7 @@ class AddRepositoryLanguages < ActiveRecord::Migration[4.2] DOWNTIME = false def up + # rubocop:disable Migration/AddLimitToStringColumns create_table(:programming_languages) do |t| t.string :name, null: false t.string :color, null: false @@ -19,6 +20,7 @@ class AddRepositoryLanguages < ActiveRecord::Migration[4.2] add_index :programming_languages, :name, unique: true add_index :repository_languages, [:project_id, :programming_language_id], unique: true, name: "index_repository_languages_on_project_and_languages_id" + # rubocop:enable Migration/AddLimitToStringColumns end def down diff --git a/db/migrate/20180613081317_create_ci_builds_runner_session.rb b/db/migrate/20180613081317_create_ci_builds_runner_session.rb index eb41f76b105..68af38834d2 100644 --- a/db/migrate/20180613081317_create_ci_builds_runner_session.rb +++ b/db/migrate/20180613081317_create_ci_builds_runner_session.rb @@ -8,6 +8,7 @@ class CreateCiBuildsRunnerSession < ActiveRecord::Migration[4.2] DOWNTIME = false def change + # rubocop:disable Migration/AddLimitToStringColumns create_table :ci_builds_runner_session, id: :bigserial do |t| t.integer :build_id, null: false t.string :url, null: false @@ -17,5 +18,6 @@ class CreateCiBuildsRunnerSession < ActiveRecord::Migration[4.2] t.foreign_key :ci_builds, column: :build_id, on_delete: :cascade t.index :build_id, unique: true end + # rubocop:enable Migration/AddLimitToStringColumns end end diff --git a/db/migrate/20180713092803_create_user_statuses.rb b/db/migrate/20180713092803_create_user_statuses.rb index 43b96805c1e..3abab4e45a9 100644 --- a/db/migrate/20180713092803_create_user_statuses.rb +++ b/db/migrate/20180713092803_create_user_statuses.rb @@ -6,6 +6,7 @@ class CreateUserStatuses < ActiveRecord::Migration[4.2] DOWNTIME = false def change + # rubocop:disable Migration/AddLimitToStringColumns create_table :user_statuses, id: false, primary_key: :user_id do |t| t.references :user, foreign_key: { on_delete: :cascade }, @@ -16,5 +17,6 @@ class CreateUserStatuses < ActiveRecord::Migration[4.2] t.string :message, limit: 100 t.string :message_html end + # rubocop:enable Migration/AddLimitToStringColumns end end diff --git a/db/migrate/20180814153625_add_commit_email_to_users.rb b/db/migrate/20180814153625_add_commit_email_to_users.rb index 4d9217ea504..98bafc14a03 100644 --- a/db/migrate/20180814153625_add_commit_email_to_users.rb +++ b/db/migrate/20180814153625_add_commit_email_to_users.rb @@ -28,6 +28,6 @@ class AddCommitEmailToUsers < ActiveRecord::Migration[4.2] # disable_ddl_transaction! def change - add_column :users, :commit_email, :string + add_column :users, :commit_email, :string # rubocop:disable Migration/AddLimitToStringColumns end end diff --git a/db/migrate/20180831164908_add_identifier_to_prometheus_metric.rb b/db/migrate/20180831164908_add_identifier_to_prometheus_metric.rb index 7aa5950249c..8f30363c310 100644 --- a/db/migrate/20180831164908_add_identifier_to_prometheus_metric.rb +++ b/db/migrate/20180831164908_add_identifier_to_prometheus_metric.rb @@ -6,6 +6,6 @@ class AddIdentifierToPrometheusMetric < ActiveRecord::Migration[4.2] DOWNTIME = false def change - add_column :prometheus_metrics, :identifier, :string + add_column :prometheus_metrics, :identifier, :string # rubocop:disable Migration/AddLimitToStringColumns end end diff --git a/db/migrate/20180910115836_add_attr_encrypted_columns_to_web_hook.rb b/db/migrate/20180910115836_add_attr_encrypted_columns_to_web_hook.rb index ca8dbdba2bb..9757f7fdc79 100644 --- a/db/migrate/20180910115836_add_attr_encrypted_columns_to_web_hook.rb +++ b/db/migrate/20180910115836_add_attr_encrypted_columns_to_web_hook.rb @@ -6,10 +6,12 @@ class AddAttrEncryptedColumnsToWebHook < ActiveRecord::Migration[4.2] DOWNTIME = false def change + # rubocop:disable Migration/AddLimitToStringColumns add_column :web_hooks, :encrypted_token, :string add_column :web_hooks, :encrypted_token_iv, :string add_column :web_hooks, :encrypted_url, :string add_column :web_hooks, :encrypted_url_iv, :string + # rubocop:enable Migration/AddLimitToStringColumns end end diff --git a/db/migrate/20180910153412_add_token_digest_to_personal_access_tokens.rb b/db/migrate/20180910153412_add_token_digest_to_personal_access_tokens.rb index 142e454832f..52923f52499 100644 --- a/db/migrate/20180910153412_add_token_digest_to_personal_access_tokens.rb +++ b/db/migrate/20180910153412_add_token_digest_to_personal_access_tokens.rb @@ -8,7 +8,7 @@ class AddTokenDigestToPersonalAccessTokens < ActiveRecord::Migration[4.2] def up change_column :personal_access_tokens, :token, :string, null: true - add_column :personal_access_tokens, :token_digest, :string + add_column :personal_access_tokens, :token_digest, :string # rubocop:disable Migration/AddLimitToStringColumns end def down diff --git a/db/migrate/20180912111628_add_knative_application.rb b/db/migrate/20180912111628_add_knative_application.rb index 86d9100d2e7..7c55de02d1c 100644 --- a/db/migrate/20180912111628_add_knative_application.rb +++ b/db/migrate/20180912111628_add_knative_application.rb @@ -6,6 +6,7 @@ class AddKnativeApplication < ActiveRecord::Migration[4.2] DOWNTIME = false def change + # rubocop:disable Migration/AddLimitToStringColumns create_table "clusters_applications_knative" do |t| t.references :cluster, null: false, unique: true, foreign_key: { on_delete: :cascade } @@ -16,5 +17,6 @@ class AddKnativeApplication < ActiveRecord::Migration[4.2] t.string "hostname" t.text "status_reason" end + # rubocop:enable Migration/AddLimitToStringColumns end end diff --git a/db/migrate/20181009190428_create_clusters_kubernetes_namespaces.rb b/db/migrate/20181009190428_create_clusters_kubernetes_namespaces.rb index 62ad6c63d0a..b6ffb2866aa 100644 --- a/db/migrate/20181009190428_create_clusters_kubernetes_namespaces.rb +++ b/db/migrate/20181009190428_create_clusters_kubernetes_namespaces.rb @@ -5,6 +5,7 @@ class CreateClustersKubernetesNamespaces < ActiveRecord::Migration[4.2] INDEX_NAME = 'kubernetes_namespaces_cluster_and_namespace' def change + # rubocop:disable Migration/AddLimitToStringColumns create_table :clusters_kubernetes_namespaces, id: :bigserial do |t| t.references :cluster, null: false, index: true, foreign_key: { on_delete: :cascade } t.references :project, index: true, foreign_key: { on_delete: :nullify } @@ -20,5 +21,6 @@ class CreateClustersKubernetesNamespaces < ActiveRecord::Migration[4.2] t.index [:cluster_id, :namespace], name: INDEX_NAME, unique: true end + # rubocop:enable Migration/AddLimitToStringColumns end end diff --git a/db/migrate/20181019032400_add_shards_table.rb b/db/migrate/20181019032400_add_shards_table.rb index e31af97cc94..82287e5c3b5 100644 --- a/db/migrate/20181019032400_add_shards_table.rb +++ b/db/migrate/20181019032400_add_shards_table.rb @@ -5,7 +5,7 @@ class AddShardsTable < ActiveRecord::Migration[4.2] def change create_table :shards do |t| - t.string :name, null: false, index: { unique: true } + t.string :name, null: false, index: { unique: true } # rubocop:disable Migration/AddLimitToStringColumns end end end diff --git a/db/migrate/20181019032408_add_repositories_table.rb b/db/migrate/20181019032408_add_repositories_table.rb index 2153c1c9fc6..dd510b44084 100644 --- a/db/migrate/20181019032408_add_repositories_table.rb +++ b/db/migrate/20181019032408_add_repositories_table.rb @@ -6,7 +6,7 @@ class AddRepositoriesTable < ActiveRecord::Migration[4.2] def change create_table :repositories, id: :bigserial do |t| t.references :shard, null: false, index: true, foreign_key: { on_delete: :restrict } - t.string :disk_path, null: false, index: { unique: true } + t.string :disk_path, null: false, index: { unique: true } # rubocop:disable Migration/AddLimitToStringColumns end add_column :projects, :pool_repository_id, :bigint diff --git a/db/migrate/20181025115728_add_private_commit_email_hostname_to_application_settings.rb b/db/migrate/20181025115728_add_private_commit_email_hostname_to_application_settings.rb index 052a344f182..c0e4897b8d7 100644 --- a/db/migrate/20181025115728_add_private_commit_email_hostname_to_application_settings.rb +++ b/db/migrate/20181025115728_add_private_commit_email_hostname_to_application_settings.rb @@ -6,6 +6,6 @@ class AddPrivateCommitEmailHostnameToApplicationSettings < ActiveRecord::Migrati DOWNTIME = false def change - add_column(:application_settings, :commit_email_hostname, :string, null: true) + add_column(:application_settings, :commit_email_hostname, :string, null: true) # rubocop:disable Migration/AddLimitToStringColumns end end diff --git a/db/migrate/20181031190559_drop_gcp_clusters_table.rb b/db/migrate/20181031190559_drop_gcp_clusters_table.rb index 597fe49f4c8..850fa93c75a 100644 --- a/db/migrate/20181031190559_drop_gcp_clusters_table.rb +++ b/db/migrate/20181031190559_drop_gcp_clusters_table.rb @@ -10,6 +10,7 @@ class DropGcpClustersTable < ActiveRecord::Migration[4.2] end def down + # rubocop:disable Migration/AddLimitToStringColumns create_table :gcp_clusters do |t| # Order columns by best align scheme t.references :project, null: false, index: { unique: true }, foreign_key: { on_delete: :cascade } @@ -49,5 +50,6 @@ class DropGcpClustersTable < ActiveRecord::Migration[4.2] t.text :encrypted_gcp_token t.string :encrypted_gcp_token_iv end + # rubocop:enable Migration/AddLimitToStringColumns end end diff --git a/db/migrate/20181101191341_create_clusters_applications_cert_manager.rb b/db/migrate/20181101191341_create_clusters_applications_cert_manager.rb index 0b6155356d9..3bc20046311 100644 --- a/db/migrate/20181101191341_create_clusters_applications_cert_manager.rb +++ b/db/migrate/20181101191341_create_clusters_applications_cert_manager.rb @@ -6,6 +6,7 @@ class CreateClustersApplicationsCertManager < ActiveRecord::Migration[4.2] DOWNTIME = false def change + # rubocop:disable Migration/AddLimitToStringColumns create_table :clusters_applications_cert_managers do |t| t.references :cluster, null: false, index: false, foreign_key: { on_delete: :cascade } t.integer :status, null: false @@ -15,5 +16,6 @@ class CreateClustersApplicationsCertManager < ActiveRecord::Migration[4.2] t.text :status_reason t.index :cluster_id, unique: true end + # rubocop:enable Migration/AddLimitToStringColumns end end diff --git a/db/migrate/20181115140140_add_encrypted_runners_token_to_settings.rb b/db/migrate/20181115140140_add_encrypted_runners_token_to_settings.rb index 5b2bb4f6b08..124eedf7933 100644 --- a/db/migrate/20181115140140_add_encrypted_runners_token_to_settings.rb +++ b/db/migrate/20181115140140_add_encrypted_runners_token_to_settings.rb @@ -6,6 +6,6 @@ class AddEncryptedRunnersTokenToSettings < ActiveRecord::Migration[4.2] DOWNTIME = false def change - add_column :application_settings, :runners_registration_token_encrypted, :string + add_column :application_settings, :runners_registration_token_encrypted, :string # rubocop:disable Migration/AddLimitToStringColumns end end diff --git a/db/migrate/20181116050532_knative_external_ip.rb b/db/migrate/20181116050532_knative_external_ip.rb index 5645b040a23..4634b411108 100644 --- a/db/migrate/20181116050532_knative_external_ip.rb +++ b/db/migrate/20181116050532_knative_external_ip.rb @@ -9,6 +9,6 @@ class KnativeExternalIp < ActiveRecord::Migration[4.2] DOWNTIME = false def change - add_column :clusters_applications_knative, :external_ip, :string + add_column :clusters_applications_knative, :external_ip, :string # rubocop:disable Migration/AddLimitToStringColumns end end diff --git a/db/migrate/20181116141415_add_encrypted_runners_token_to_namespaces.rb b/db/migrate/20181116141415_add_encrypted_runners_token_to_namespaces.rb index dcf565cd6c0..0a8ed912891 100644 --- a/db/migrate/20181116141415_add_encrypted_runners_token_to_namespaces.rb +++ b/db/migrate/20181116141415_add_encrypted_runners_token_to_namespaces.rb @@ -6,6 +6,6 @@ class AddEncryptedRunnersTokenToNamespaces < ActiveRecord::Migration[4.2] DOWNTIME = false def change - add_column :namespaces, :runners_token_encrypted, :string + add_column :namespaces, :runners_token_encrypted, :string # rubocop:disable Migration/AddLimitToStringColumns end end diff --git a/db/migrate/20181116141504_add_encrypted_runners_token_to_projects.rb b/db/migrate/20181116141504_add_encrypted_runners_token_to_projects.rb index 13cd80e5c8b..3083dff49b8 100644 --- a/db/migrate/20181116141504_add_encrypted_runners_token_to_projects.rb +++ b/db/migrate/20181116141504_add_encrypted_runners_token_to_projects.rb @@ -6,6 +6,6 @@ class AddEncryptedRunnersTokenToProjects < ActiveRecord::Migration[4.2] DOWNTIME = false def change - add_column :projects, :runners_token_encrypted, :string + add_column :projects, :runners_token_encrypted, :string # rubocop:disable Migration/AddLimitToStringColumns end end diff --git a/db/migrate/20181120151656_add_token_encrypted_to_ci_runners.rb b/db/migrate/20181120151656_add_token_encrypted_to_ci_runners.rb index 8b990451adc..2270246dfb4 100644 --- a/db/migrate/20181120151656_add_token_encrypted_to_ci_runners.rb +++ b/db/migrate/20181120151656_add_token_encrypted_to_ci_runners.rb @@ -6,6 +6,6 @@ class AddTokenEncryptedToCiRunners < ActiveRecord::Migration[4.2] DOWNTIME = false def change - add_column :ci_runners, :token_encrypted, :string + add_column :ci_runners, :token_encrypted, :string # rubocop:disable Migration/AddLimitToStringColumns end end diff --git a/db/migrate/20181122160027_create_project_repositories.rb b/db/migrate/20181122160027_create_project_repositories.rb index e42cef9b1c6..3f123daa150 100644 --- a/db/migrate/20181122160027_create_project_repositories.rb +++ b/db/migrate/20181122160027_create_project_repositories.rb @@ -11,7 +11,7 @@ class CreateProjectRepositories < ActiveRecord::Migration[5.0] def change create_table :project_repositories, id: :bigserial do |t| t.references :shard, null: false, index: true, foreign_key: { on_delete: :restrict } - t.string :disk_path, null: false, index: { unique: true } + t.string :disk_path, null: false, index: { unique: true } # rubocop:disable Migration/AddLimitToStringColumns t.references :project, null: false, index: { unique: true }, foreign_key: { on_delete: :cascade } end end diff --git a/db/migrate/20181123144235_create_suggestions.rb b/db/migrate/20181123144235_create_suggestions.rb index 1723f6de7eb..78888517db5 100644 --- a/db/migrate/20181123144235_create_suggestions.rb +++ b/db/migrate/20181123144235_create_suggestions.rb @@ -8,7 +8,7 @@ class CreateSuggestions < ActiveRecord::Migration[5.0] t.references :note, foreign_key: { on_delete: :cascade }, null: false t.integer :relative_order, null: false, limit: 2 t.boolean :applied, null: false, default: false - t.string :commit_id + t.string :commit_id # rubocop:disable Migration/AddLimitToStringColumns t.text :from_content, null: false t.text :to_content, null: false diff --git a/db/migrate/20181128123704_add_state_to_pool_repository.rb b/db/migrate/20181128123704_add_state_to_pool_repository.rb index 714232ede56..04bbcf24f62 100644 --- a/db/migrate/20181128123704_add_state_to_pool_repository.rb +++ b/db/migrate/20181128123704_add_state_to_pool_repository.rb @@ -9,7 +9,7 @@ class AddStateToPoolRepository < ActiveRecord::Migration[5.0] # the transactions don't have to be disabled # rubocop: disable Migration/AddConcurrentForeignKey, Migration/AddIndex def change - add_column(:pool_repositories, :state, :string, null: true) + add_column(:pool_repositories, :state, :string, null: true) # rubocop:disable Migration/AddLimitToStringColumns add_column :pool_repositories, :source_project_id, :integer add_index :pool_repositories, :source_project_id, unique: true diff --git a/db/migrate/20181129104854_add_token_encrypted_to_ci_builds.rb b/db/migrate/20181129104854_add_token_encrypted_to_ci_builds.rb index 11b98203793..62a7421eae0 100644 --- a/db/migrate/20181129104854_add_token_encrypted_to_ci_builds.rb +++ b/db/migrate/20181129104854_add_token_encrypted_to_ci_builds.rb @@ -6,6 +6,6 @@ class AddTokenEncryptedToCiBuilds < ActiveRecord::Migration[5.0] DOWNTIME = false def change - add_column :ci_builds, :token_encrypted, :string + add_column :ci_builds, :token_encrypted, :string # rubocop:disable Migration/AddLimitToStringColumns end end diff --git a/db/migrate/20181203002526_add_project_bfg_object_map_column.rb b/db/migrate/20181203002526_add_project_bfg_object_map_column.rb index 8b42cd6f941..5e6d416895c 100644 --- a/db/migrate/20181203002526_add_project_bfg_object_map_column.rb +++ b/db/migrate/20181203002526_add_project_bfg_object_map_column.rb @@ -4,6 +4,6 @@ class AddProjectBfgObjectMapColumn < ActiveRecord::Migration[5.0] DOWNTIME = false def change - add_column :projects, :bfg_object_map, :string + add_column :projects, :bfg_object_map, :string # rubocop:disable Migration/AddLimitToStringColumns end end diff --git a/db/migrate/20181211092510_add_name_author_id_and_sha_to_releases.rb b/db/migrate/20181211092510_add_name_author_id_and_sha_to_releases.rb index 60815e0c31a..3ab808ba667 100644 --- a/db/migrate/20181211092510_add_name_author_id_and_sha_to_releases.rb +++ b/db/migrate/20181211092510_add_name_author_id_and_sha_to_releases.rb @@ -7,7 +7,7 @@ class AddNameAuthorIdAndShaToReleases < ActiveRecord::Migration[5.0] def change add_column :releases, :author_id, :integer - add_column :releases, :name, :string - add_column :releases, :sha, :string + add_column :releases, :name, :string # rubocop:disable Migration/AddLimitToStringColumns + add_column :releases, :sha, :string # rubocop:disable Migration/AddLimitToStringColumns end end diff --git a/db/migrate/20181212171634_create_error_tracking_settings.rb b/db/migrate/20181212171634_create_error_tracking_settings.rb index 18c38bd2c47..950b9a88005 100644 --- a/db/migrate/20181212171634_create_error_tracking_settings.rb +++ b/db/migrate/20181212171634_create_error_tracking_settings.rb @@ -6,6 +6,7 @@ class CreateErrorTrackingSettings < ActiveRecord::Migration[5.0] DOWNTIME = false def change + # rubocop:disable Migration/AddLimitToStringColumns create_table :project_error_tracking_settings, id: :int, primary_key: :project_id, default: nil do |t| t.boolean :enabled, null: false, default: true t.string :api_url, null: false @@ -13,5 +14,6 @@ class CreateErrorTrackingSettings < ActiveRecord::Migration[5.0] t.string :encrypted_token_iv t.foreign_key :projects, column: :project_id, on_delete: :cascade end + # rubocop:enable Migration/AddLimitToStringColumns end end diff --git a/db/migrate/20181228175414_create_releases_link_table.rb b/db/migrate/20181228175414_create_releases_link_table.rb index 03558202137..168c4722cc1 100644 --- a/db/migrate/20181228175414_create_releases_link_table.rb +++ b/db/migrate/20181228175414_create_releases_link_table.rb @@ -6,6 +6,7 @@ class CreateReleasesLinkTable < ActiveRecord::Migration[5.0] DOWNTIME = false def change + # rubocop:disable Migration/AddLimitToStringColumns create_table :release_links, id: :bigserial do |t| t.references :release, null: false, index: false, foreign_key: { on_delete: :cascade } t.string :url, null: false @@ -15,5 +16,6 @@ class CreateReleasesLinkTable < ActiveRecord::Migration[5.0] t.index [:release_id, :url], unique: true t.index [:release_id, :name], unique: true end + # rubocop:enable Migration/AddLimitToStringColumns end end diff --git a/db/migrate/20190109153125_add_merge_request_external_diffs.rb b/db/migrate/20190109153125_add_merge_request_external_diffs.rb index c67903c7f67..a680856a3d8 100644 --- a/db/migrate/20190109153125_add_merge_request_external_diffs.rb +++ b/db/migrate/20190109153125_add_merge_request_external_diffs.rb @@ -11,7 +11,7 @@ class AddMergeRequestExternalDiffs < ActiveRecord::Migration[5.0] def change # Allow the merge request diff to store details about an external file - add_column :merge_request_diffs, :external_diff, :string + add_column :merge_request_diffs, :external_diff, :string # rubocop:disable Migration/AddLimitToStringColumns add_column :merge_request_diffs, :external_diff_store, :integer add_column :merge_request_diffs, :stored_externally, :boolean diff --git a/db/migrate/20190114172110_add_domain_to_cluster.rb b/db/migrate/20190114172110_add_domain_to_cluster.rb index 58d7664b8c0..d8f10af9cad 100644 --- a/db/migrate/20190114172110_add_domain_to_cluster.rb +++ b/db/migrate/20190114172110_add_domain_to_cluster.rb @@ -4,6 +4,6 @@ class AddDomainToCluster < ActiveRecord::Migration[5.0] DOWNTIME = false def change - add_column :clusters, :domain, :string + add_column :clusters, :domain, :string # rubocop:disable Migration/AddLimitToStringColumns end end diff --git a/db/migrate/20190115092821_add_columns_project_error_tracking_settings.rb b/db/migrate/20190115092821_add_columns_project_error_tracking_settings.rb index 190b6f958fd..afed929cce4 100644 --- a/db/migrate/20190115092821_add_columns_project_error_tracking_settings.rb +++ b/db/migrate/20190115092821_add_columns_project_error_tracking_settings.rb @@ -6,8 +6,8 @@ class AddColumnsProjectErrorTrackingSettings < ActiveRecord::Migration[5.0] DOWNTIME = false def change - add_column :project_error_tracking_settings, :project_name, :string - add_column :project_error_tracking_settings, :organization_name, :string + add_column :project_error_tracking_settings, :project_name, :string # rubocop:disable Migration/AddLimitToStringColumns + add_column :project_error_tracking_settings, :organization_name, :string # rubocop:disable Migration/AddLimitToStringColumns change_column_default :project_error_tracking_settings, :enabled, from: true, to: false diff --git a/db/migrate/20190116234221_add_sorting_fields_to_user_preference.rb b/db/migrate/20190116234221_add_sorting_fields_to_user_preference.rb index 7bf581fe9b0..39aab600546 100644 --- a/db/migrate/20190116234221_add_sorting_fields_to_user_preference.rb +++ b/db/migrate/20190116234221_add_sorting_fields_to_user_preference.rb @@ -10,8 +10,8 @@ class AddSortingFieldsToUserPreference < ActiveRecord::Migration[5.0] DOWNTIME = false def up - add_column :user_preferences, :issues_sort, :string - add_column :user_preferences, :merge_requests_sort, :string + add_column :user_preferences, :issues_sort, :string # rubocop:disable Migration/AddLimitToStringColumns + add_column :user_preferences, :merge_requests_sort, :string # rubocop:disable Migration/AddLimitToStringColumns end def down diff --git a/db/migrate/20190301182457_add_external_hostname_to_ingress_and_knative.rb b/db/migrate/20190301182457_add_external_hostname_to_ingress_and_knative.rb index 2c3a54b12a9..37ba1090cf0 100644 --- a/db/migrate/20190301182457_add_external_hostname_to_ingress_and_knative.rb +++ b/db/migrate/20190301182457_add_external_hostname_to_ingress_and_knative.rb @@ -4,7 +4,7 @@ class AddExternalHostnameToIngressAndKnative < ActiveRecord::Migration[5.0] DOWNTIME = false def change - add_column :clusters_applications_ingress, :external_hostname, :string - add_column :clusters_applications_knative, :external_hostname, :string + add_column :clusters_applications_ingress, :external_hostname, :string # rubocop:disable Migration/AddLimitToStringColumns + add_column :clusters_applications_knative, :external_hostname, :string # rubocop:disable Migration/AddLimitToStringColumns end end diff --git a/db/migrate/20190320174702_add_lets_encrypt_notification_email_to_application_settings.rb b/db/migrate/20190320174702_add_lets_encrypt_notification_email_to_application_settings.rb index e9cf2af84a5..aeabf4e3cb4 100644 --- a/db/migrate/20190320174702_add_lets_encrypt_notification_email_to_application_settings.rb +++ b/db/migrate/20190320174702_add_lets_encrypt_notification_email_to_application_settings.rb @@ -10,6 +10,6 @@ class AddLetsEncryptNotificationEmailToApplicationSettings < ActiveRecord::Migra DOWNTIME = false def change - add_column :application_settings, :lets_encrypt_notification_email, :string + add_column :application_settings, :lets_encrypt_notification_email, :string # rubocop:disable Migration/AddLimitToStringColumns end end diff --git a/db/migrate/20190325105715_add_fields_to_user_preferences.rb b/db/migrate/20190325105715_add_fields_to_user_preferences.rb index 9ea3b4f9cd8..4da5c496147 100644 --- a/db/migrate/20190325105715_add_fields_to_user_preferences.rb +++ b/db/migrate/20190325105715_add_fields_to_user_preferences.rb @@ -11,7 +11,7 @@ class AddFieldsToUserPreferences < ActiveRecord::Migration[5.0] DOWNTIME = false def up - add_column(:user_preferences, :timezone, :string) + add_column(:user_preferences, :timezone, :string) # rubocop:disable Migration/AddLimitToStringColumns add_column(:user_preferences, :time_display_relative, :boolean) add_column(:user_preferences, :time_format_in_24h, :boolean) end diff --git a/db/migrate/20190327163904_add_notification_email_to_notification_settings.rb b/db/migrate/20190327163904_add_notification_email_to_notification_settings.rb index 2f3069032a1..d912f922510 100644 --- a/db/migrate/20190327163904_add_notification_email_to_notification_settings.rb +++ b/db/migrate/20190327163904_add_notification_email_to_notification_settings.rb @@ -6,6 +6,6 @@ class AddNotificationEmailToNotificationSettings < ActiveRecord::Migration[5.0] DOWNTIME = false def change - add_column :notification_settings, :notification_email, :string + add_column :notification_settings, :notification_email, :string # rubocop:disable Migration/AddLimitToStringColumns end end diff --git a/db/migrate/20190402150158_backport_enterprise_schema.rb b/db/migrate/20190402150158_backport_enterprise_schema.rb index 8762cc53ed7..3f13b68c2f3 100644 --- a/db/migrate/20190402150158_backport_enterprise_schema.rb +++ b/db/migrate/20190402150158_backport_enterprise_schema.rb @@ -2,6 +2,7 @@ # rubocop: disable Metrics/AbcSize # rubocop: disable Migration/Datetime +# rubocop: disable Migration/AddLimitToStringColumns class BackportEnterpriseSchema < ActiveRecord::Migration[5.0] include Gitlab::Database::MigrationHelpers @@ -2190,3 +2191,4 @@ class BackportEnterpriseSchema < ActiveRecord::Migration[5.0] end # rubocop: enable Metrics/AbcSize # rubocop: enable Migration/Datetime +# rubocop: enable Migration/AddLimitToStringColumns diff --git a/db/migrate/20190409224933_add_name_to_geo_nodes.rb b/db/migrate/20190409224933_add_name_to_geo_nodes.rb index 2dff81b429c..65c01683995 100644 --- a/db/migrate/20190409224933_add_name_to_geo_nodes.rb +++ b/db/migrate/20190409224933_add_name_to_geo_nodes.rb @@ -10,7 +10,7 @@ class AddNameToGeoNodes < ActiveRecord::Migration[5.0] DOWNTIME = false def up - add_column :geo_nodes, :name, :string + add_column :geo_nodes, :name, :string # rubocop:disable Migration/AddLimitToStringColumns # url is also unique, and its type and size is identical to the name column, # so this is safe. diff --git a/db/migrate/20190422082247_create_project_metrics_settings.rb b/db/migrate/20190422082247_create_project_metrics_settings.rb index 3e21dd0a934..a0a2ed64820 100644 --- a/db/migrate/20190422082247_create_project_metrics_settings.rb +++ b/db/migrate/20190422082247_create_project_metrics_settings.rb @@ -7,7 +7,7 @@ class CreateProjectMetricsSettings < ActiveRecord::Migration[5.0] def change create_table :project_metrics_settings, id: :int, primary_key: :project_id, default: nil do |t| - t.string :external_dashboard_url, null: false + t.string :external_dashboard_url, null: false # rubocop:disable Migration/AddLimitToStringColumns t.foreign_key :projects, column: :project_id, on_delete: :cascade end end diff --git a/db/migrate/20190429082448_create_pages_domain_acme_orders.rb b/db/migrate/20190429082448_create_pages_domain_acme_orders.rb index af811e83518..ca1796d054c 100644 --- a/db/migrate/20190429082448_create_pages_domain_acme_orders.rb +++ b/db/migrate/20190429082448_create_pages_domain_acme_orders.rb @@ -10,6 +10,7 @@ class CreatePagesDomainAcmeOrders < ActiveRecord::Migration[5.1] DOWNTIME = false def change + # rubocop:disable Migration/AddLimitToStringColumns create_table :pages_domain_acme_orders do |t| t.references :pages_domain, null: false, index: true, foreign_key: { on_delete: :cascade }, type: :integer @@ -24,5 +25,6 @@ class CreatePagesDomainAcmeOrders < ActiveRecord::Migration[5.1] t.text :encrypted_private_key, null: false t.text :encrypted_private_key_iv, null: false end + # rubocop:enable Migration/AddLimitToStringColumns end end diff --git a/db/migrate/20190430131225_create_issue_tracker_data.rb b/db/migrate/20190430131225_create_issue_tracker_data.rb index 7859bea9c22..d2134ad82c7 100644 --- a/db/migrate/20190430131225_create_issue_tracker_data.rb +++ b/db/migrate/20190430131225_create_issue_tracker_data.rb @@ -9,6 +9,7 @@ class CreateIssueTrackerData < ActiveRecord::Migration[5.1] DOWNTIME = false def change + # rubocop:disable Migration/AddLimitToStringColumns create_table :issue_tracker_data do |t| t.references :service, foreign_key: { on_delete: :cascade }, type: :integer, index: true, null: false t.timestamps_with_timezone @@ -19,5 +20,6 @@ class CreateIssueTrackerData < ActiveRecord::Migration[5.1] t.string :encrypted_new_issue_url t.string :encrypted_new_issue_url_iv end + # rubocop:enable Migration/AddLimitToStringColumns end end diff --git a/db/migrate/20190430142025_create_jira_tracker_data.rb b/db/migrate/20190430142025_create_jira_tracker_data.rb index d328ad63854..5e53e5a701a 100644 --- a/db/migrate/20190430142025_create_jira_tracker_data.rb +++ b/db/migrate/20190430142025_create_jira_tracker_data.rb @@ -9,6 +9,7 @@ class CreateJiraTrackerData < ActiveRecord::Migration[5.1] DOWNTIME = false def change + # rubocop:disable Migration/AddLimitToStringColumns create_table :jira_tracker_data do |t| t.references :service, foreign_key: { on_delete: :cascade }, type: :integer, index: true, null: false t.timestamps_with_timezone @@ -22,5 +23,6 @@ class CreateJiraTrackerData < ActiveRecord::Migration[5.1] t.string :encrypted_password_iv t.string :jira_issue_transition_id end + # rubocop:enable Migration/AddLimitToStringColumns end end diff --git a/db/migrate/20190514105711_create_ip_restriction.rb b/db/migrate/20190514105711_create_ip_restriction.rb index dfafbe32ad1..69f8c1b8c4e 100644 --- a/db/migrate/20190514105711_create_ip_restriction.rb +++ b/db/migrate/20190514105711_create_ip_restriction.rb @@ -12,7 +12,7 @@ class CreateIpRestriction < ActiveRecord::Migration[5.1] type: :integer, null: false, index: true - t.string :range, null: false + t.string :range, null: false # rubocop:disable Migration/AddLimitToStringColumns end add_foreign_key(:ip_restrictions, :namespaces, column: :group_id, on_delete: :cascade) # rubocop: disable Migration/AddConcurrentForeignKey diff --git a/db/migrate/20190527011309_add_required_template_name_to_application_settings.rb b/db/migrate/20190527011309_add_required_template_name_to_application_settings.rb index 6cbac0ed507..5c47e6f33c2 100644 --- a/db/migrate/20190527011309_add_required_template_name_to_application_settings.rb +++ b/db/migrate/20190527011309_add_required_template_name_to_application_settings.rb @@ -10,6 +10,6 @@ class AddRequiredTemplateNameToApplicationSettings < ActiveRecord::Migration[5.1 DOWNTIME = false def change - add_column :application_settings, :required_instance_ci_template, :string, null: true + add_column :application_settings, :required_instance_ci_template, :string, null: true # rubocop:disable Migration/AddLimitToStringColumns end end diff --git a/db/migrate/20190606054742_add_token_encrypted_to_operations_feature_flags_clients.rb b/db/migrate/20190606054742_add_token_encrypted_to_operations_feature_flags_clients.rb index 024b5bd2ba5..2db4dc85750 100644 --- a/db/migrate/20190606054742_add_token_encrypted_to_operations_feature_flags_clients.rb +++ b/db/migrate/20190606054742_add_token_encrypted_to_operations_feature_flags_clients.rb @@ -6,6 +6,6 @@ class AddTokenEncryptedToOperationsFeatureFlagsClients < ActiveRecord::Migration DOWNTIME = false def change - add_column :operations_feature_flags_clients, :token_encrypted, :string + add_column :operations_feature_flags_clients, :token_encrypted, :string # rubocop:disable Migration/AddLimitToStringColumns end end diff --git a/db/migrate/20190613044655_add_username_to_deploy_tokens.rb b/db/migrate/20190613044655_add_username_to_deploy_tokens.rb index 793553afe35..a0acb02013b 100644 --- a/db/migrate/20190613044655_add_username_to_deploy_tokens.rb +++ b/db/migrate/20190613044655_add_username_to_deploy_tokens.rb @@ -4,6 +4,6 @@ class AddUsernameToDeployTokens < ActiveRecord::Migration[5.1] DOWNTIME = false def change - add_column :deploy_tokens, :username, :string + add_column :deploy_tokens, :username, :string # rubocop:disable Migration/AddLimitToStringColumns end end diff --git a/db/migrate/20190613073003_create_project_aliases.rb b/db/migrate/20190613073003_create_project_aliases.rb index 5a2c2ba0cf2..896d3ca5813 100644 --- a/db/migrate/20190613073003_create_project_aliases.rb +++ b/db/migrate/20190613073003_create_project_aliases.rb @@ -8,7 +8,7 @@ class CreateProjectAliases < ActiveRecord::Migration[5.1] def change create_table :project_aliases do |t| t.references :project, null: false, index: true, foreign_key: { on_delete: :cascade }, type: :integer - t.string :name, null: false, index: { unique: true } + t.string :name, null: false, index: { unique: true } # rubocop:disable Migration/AddLimitToStringColumns t.timestamps_with_timezone null: false end diff --git a/db/migrate/20190621151636_add_merge_request_rebase_jid.rb b/db/migrate/20190621151636_add_merge_request_rebase_jid.rb index 1fed5690ead..6c1081732e8 100644 --- a/db/migrate/20190621151636_add_merge_request_rebase_jid.rb +++ b/db/migrate/20190621151636_add_merge_request_rebase_jid.rb @@ -4,6 +4,6 @@ class AddMergeRequestRebaseJid < ActiveRecord::Migration[5.1] DOWNTIME = false def change - add_column :merge_requests, :rebase_jid, :string + add_column :merge_requests, :rebase_jid, :string # rubocop:disable Migration/AddLimitToStringColumns end end diff --git a/db/migrate/20190624123615_add_grafana_url_to_settings.rb b/db/migrate/20190624123615_add_grafana_url_to_settings.rb index 61efe64a7a1..835ec4e9094 100644 --- a/db/migrate/20190624123615_add_grafana_url_to_settings.rb +++ b/db/migrate/20190624123615_add_grafana_url_to_settings.rb @@ -8,8 +8,10 @@ class AddGrafanaUrlToSettings < ActiveRecord::Migration[5.1] DOWNTIME = false def up + # rubocop:disable Migration/AddLimitToStringColumns add_column_with_default(:application_settings, :grafana_url, :string, default: '/-/grafana', allow_null: false) + # rubocop:enable Migration/AddLimitToStringColumns end def down diff --git a/db/migrate/20190711124721_create_job_variables.rb b/db/migrate/20190711124721_create_job_variables.rb index a860522f39e..4ff4b031d8f 100644 --- a/db/migrate/20190711124721_create_job_variables.rb +++ b/db/migrate/20190711124721_create_job_variables.rb @@ -10,6 +10,7 @@ class CreateJobVariables < ActiveRecord::Migration[5.1] DOWNTIME = false def change + # rubocop:disable Migration/AddLimitToStringColumns create_table :ci_job_variables do |t| t.string :key, null: false t.text :encrypted_value @@ -17,6 +18,7 @@ class CreateJobVariables < ActiveRecord::Migration[5.1] t.references :job, null: false, index: true, foreign_key: { to_table: :ci_builds, on_delete: :cascade } t.integer :variable_type, null: false, limit: 2, default: 1 end + # rubocop:enable Migration/AddLimitToStringColumns add_index :ci_job_variables, [:key, :job_id], unique: true end diff --git a/db/migrate/20190715173819_add_object_storage_flag_to_geo_node.rb b/db/migrate/20190715173819_add_object_storage_flag_to_geo_node.rb new file mode 100644 index 00000000000..2d3243f3357 --- /dev/null +++ b/db/migrate/20190715173819_add_object_storage_flag_to_geo_node.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 AddObjectStorageFlagToGeoNode < ActiveRecord::Migration[5.2] + include Gitlab::Database::MigrationHelpers + + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + + disable_ddl_transaction! + + def up + add_column_with_default :geo_nodes, :sync_object_storage, :boolean, default: false + end + + def down + remove_column :geo_nodes, :sync_object_storage + end +end diff --git a/db/migrate/20190715215532_add_project_emails_disabled.rb b/db/migrate/20190715215532_add_project_emails_disabled.rb new file mode 100644 index 00000000000..536ea34c0fb --- /dev/null +++ b/db/migrate/20190715215532_add_project_emails_disabled.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class AddProjectEmailsDisabled < ActiveRecord::Migration[5.2] + DOWNTIME = false + + def change + add_column :projects, :emails_disabled, :boolean + end +end diff --git a/db/migrate/20190715215549_add_group_emails_disabled.rb b/db/migrate/20190715215549_add_group_emails_disabled.rb new file mode 100644 index 00000000000..d3fd4d2d923 --- /dev/null +++ b/db/migrate/20190715215549_add_group_emails_disabled.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class AddGroupEmailsDisabled < ActiveRecord::Migration[5.2] + DOWNTIME = false + + def change + add_column :namespaces, :emails_disabled, :boolean + end +end diff --git a/db/migrate/20190716144222_create_analytics_cycle_analytics_project_stages.rb b/db/migrate/20190716144222_create_analytics_cycle_analytics_project_stages.rb new file mode 100644 index 00000000000..5c005377b00 --- /dev/null +++ b/db/migrate/20190716144222_create_analytics_cycle_analytics_project_stages.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +class CreateAnalyticsCycleAnalyticsProjectStages < ActiveRecord::Migration[5.2] + DOWNTIME = false + + INDEX_PREFIX = 'index_analytics_ca_project_stages_' + + def change + create_table :analytics_cycle_analytics_project_stages do |t| + t.timestamps_with_timezone + t.integer :relative_position + t.integer :start_event_identifier, null: false + t.integer :end_event_identifier, null: false + t.references(:project, { + null: false, + foreign_key: { to_table: :projects, on_delete: :cascade }, + index: { name: INDEX_PREFIX + 'on_project_id' } + }) + t.references(:start_event_label, { + foreign_key: { to_table: :labels, on_delete: :cascade }, + index: { name: INDEX_PREFIX + 'on_start_event_label_id' } + }) + t.references(:end_event_label, { + foreign_key: { to_table: :labels, on_delete: :cascade }, + index: { name: INDEX_PREFIX + 'on_end_event_label_id' } + }) + t.boolean :hidden, default: false, null: false + t.boolean :custom, default: true, null: false + t.string :name, null: false, limit: 255 + end + + add_index :analytics_cycle_analytics_project_stages, [:project_id, :name], unique: true, name: INDEX_PREFIX + 'on_project_id_and_name' + add_index :analytics_cycle_analytics_project_stages, [:relative_position], name: INDEX_PREFIX + 'on_relative_position' + end +end diff --git a/db/migrate/20190723153247_create_allowed_email_domains_for_groups.rb b/db/migrate/20190723153247_create_allowed_email_domains_for_groups.rb new file mode 100644 index 00000000000..c6c5b56ed8b --- /dev/null +++ b/db/migrate/20190723153247_create_allowed_email_domains_for_groups.rb @@ -0,0 +1,22 @@ +# 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 CreateAllowedEmailDomainsForGroups < ActiveRecord::Migration[5.2] + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + + def change + create_table :allowed_email_domains do |t| + t.timestamps_with_timezone null: false + t.references :group, references: :namespace, + column: :group_id, + type: :integer, + null: false, + index: true + t.foreign_key :namespaces, column: :group_id, on_delete: :cascade + t.string :domain, null: false, limit: 255 + end + end +end diff --git a/db/migrate/20190726101050_rename_allow_local_requests_from_hooks_and_services_application_setting.rb b/db/migrate/20190726101050_rename_allow_local_requests_from_hooks_and_services_application_setting.rb index ac65e8d745c..cce8942128c 100644 --- a/db/migrate/20190726101050_rename_allow_local_requests_from_hooks_and_services_application_setting.rb +++ b/db/migrate/20190726101050_rename_allow_local_requests_from_hooks_and_services_application_setting.rb @@ -12,6 +12,6 @@ class RenameAllowLocalRequestsFromHooksAndServicesApplicationSetting < ActiveRec end def down - cleanup_concurrent_column_rename :application_settings, :allow_local_requests_from_web_hooks_and_services, :allow_local_requests_from_hooks_and_services + undo_rename_column_concurrently :application_settings, :allow_local_requests_from_hooks_and_services, :allow_local_requests_from_web_hooks_and_services end end diff --git a/db/migrate/20190729062536_create_analytics_cycle_analytics_group_stages.rb b/db/migrate/20190729062536_create_analytics_cycle_analytics_group_stages.rb new file mode 100644 index 00000000000..5b327dc5332 --- /dev/null +++ b/db/migrate/20190729062536_create_analytics_cycle_analytics_group_stages.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +class CreateAnalyticsCycleAnalyticsGroupStages < ActiveRecord::Migration[5.2] + DOWNTIME = false + + INDEX_PREFIX = 'index_analytics_ca_group_stages_' + + def change + create_table :analytics_cycle_analytics_group_stages do |t| + t.timestamps_with_timezone + t.integer :relative_position + t.integer :start_event_identifier, null: false + t.integer :end_event_identifier, null: false + t.references(:group, { + null: false, + foreign_key: { to_table: :namespaces, on_delete: :cascade }, + index: { name: INDEX_PREFIX + 'on_group_id' } + }) + t.references(:start_event_label, { + foreign_key: { to_table: :labels, on_delete: :cascade }, + index: { name: INDEX_PREFIX + 'on_start_event_label_id' } + }) + t.references(:end_event_label, { + foreign_key: { to_table: :labels, on_delete: :cascade }, + index: { name: INDEX_PREFIX + 'on_end_event_label_id' } + }) + t.boolean :hidden, default: false, null: false + t.boolean :custom, default: true, null: false + t.string :name, null: false, limit: 255 + end + + add_index :analytics_cycle_analytics_group_stages, [:group_id, :name], unique: true, name: INDEX_PREFIX + 'on_group_id_and_name' + add_index :analytics_cycle_analytics_group_stages, [:relative_position], name: INDEX_PREFIX + 'on_relative_position' + end +end diff --git a/db/migrate/20190805140353_remove_rendundant_index_from_releases.rb b/db/migrate/20190805140353_remove_rendundant_index_from_releases.rb new file mode 100644 index 00000000000..fc4bc1a423b --- /dev/null +++ b/db/migrate/20190805140353_remove_rendundant_index_from_releases.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 RemoveRendundantIndexFromReleases < ActiveRecord::Migration[5.2] + include Gitlab::Database::MigrationHelpers + + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + + disable_ddl_transaction! + + def up + remove_concurrent_index :releases, :project_id + end + + def down + add_concurrent_index :releases, :project_id + end +end diff --git a/db/migrate/20190808152507_add_projects_sorting_field_to_user_preferences.rb b/db/migrate/20190808152507_add_projects_sorting_field_to_user_preferences.rb new file mode 100644 index 00000000000..941fead655e --- /dev/null +++ b/db/migrate/20190808152507_add_projects_sorting_field_to_user_preferences.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class AddProjectsSortingFieldToUserPreferences < ActiveRecord::Migration[5.2] + DOWNTIME = false + + def up + add_column :user_preferences, :projects_sort, :string, limit: 64 + end + + def down + remove_column :user_preferences, :projects_sort + end +end diff --git a/db/migrate/20190814205640_import_common_metrics_line_charts.rb b/db/migrate/20190814205640_import_common_metrics_line_charts.rb new file mode 100644 index 00000000000..1c28d686a42 --- /dev/null +++ b/db/migrate/20190814205640_import_common_metrics_line_charts.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class ImportCommonMetricsLineCharts < ActiveRecord::Migration[5.2] + DOWNTIME = false + + def up + ::Gitlab::DatabaseImporters::CommonMetrics::Importer.new.execute + end + + def down + # no-op + end +end diff --git a/db/migrate/20190815093936_add_index_notes_on_project_id_and_id_and_system_false.rb b/db/migrate/20190815093936_add_index_notes_on_project_id_and_id_and_system_false.rb new file mode 100644 index 00000000000..cbbece35901 --- /dev/null +++ b/db/migrate/20190815093936_add_index_notes_on_project_id_and_id_and_system_false.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +class AddIndexNotesOnProjectIdAndIdAndSystemFalse < ActiveRecord::Migration[5.2] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + add_concurrent_index(*index_arguments) + end + + def down + remove_concurrent_index(*index_arguments) + end + + private + + def index_arguments + [ + :notes, + [:project_id, :id], + { + name: 'index_notes_on_project_id_and_id_and_system_false', + where: 'NOT system' + } + ] + end +end diff --git a/db/migrate/20190815093949_remove_index_notes_on_noteable_type.rb b/db/migrate/20190815093949_remove_index_notes_on_noteable_type.rb new file mode 100644 index 00000000000..158c88e6258 --- /dev/null +++ b/db/migrate/20190815093949_remove_index_notes_on_noteable_type.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +class RemoveIndexNotesOnNoteableType < ActiveRecord::Migration[5.2] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + remove_concurrent_index(*index_arguments) + end + + def down + add_concurrent_index(*index_arguments) + end + + private + + def index_arguments + [ + :notes, + [:noteable_type], + { + name: 'index_notes_on_noteable_type' + } + ] + end +end diff --git a/db/migrate/20190816151221_add_active_jobs_limit_to_plans.rb b/db/migrate/20190816151221_add_active_jobs_limit_to_plans.rb new file mode 100644 index 00000000000..951ff41f1a8 --- /dev/null +++ b/db/migrate/20190816151221_add_active_jobs_limit_to_plans.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class AddActiveJobsLimitToPlans < ActiveRecord::Migration[5.2] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + add_column_with_default :plans, :active_jobs_limit, :integer, default: 0 + end + + def down + remove_column :plans, :active_jobs_limit + end +end diff --git a/db/post_migrate/20181013005024_remove_koding_from_application_settings.rb b/db/post_migrate/20181013005024_remove_koding_from_application_settings.rb index 550ad94f4ab..b6e5473e896 100644 --- a/db/post_migrate/20181013005024_remove_koding_from_application_settings.rb +++ b/db/post_migrate/20181013005024_remove_koding_from_application_settings.rb @@ -12,6 +12,6 @@ class RemoveKodingFromApplicationSettings < ActiveRecord::Migration[4.2] def down add_column :application_settings, :koding_enabled, :boolean # rubocop:disable Migration/SaferBooleanColumn - add_column :application_settings, :koding_url, :string + add_column :application_settings, :koding_url, :string # rubocop:disable Migration/AddLimitToStringColumns end end diff --git a/db/post_migrate/20190404231137_remove_alternate_url_from_geo_nodes.rb b/db/post_migrate/20190404231137_remove_alternate_url_from_geo_nodes.rb index 785ceb2fb28..8e7ef0ec54f 100644 --- a/db/post_migrate/20190404231137_remove_alternate_url_from_geo_nodes.rb +++ b/db/post_migrate/20190404231137_remove_alternate_url_from_geo_nodes.rb @@ -16,6 +16,6 @@ class RemoveAlternateUrlFromGeoNodes < ActiveRecord::Migration[5.0] end def down - add_column :geo_nodes, :alternate_url, :string + add_column :geo_nodes, :alternate_url, :string # rubocop:disable Migration/AddLimitToStringColumns end end diff --git a/db/post_migrate/20190625184066_remove_sentry_from_application_settings.rb b/db/post_migrate/20190625184066_remove_sentry_from_application_settings.rb index 427df343193..9d71bfafffb 100644 --- a/db/post_migrate/20190625184066_remove_sentry_from_application_settings.rb +++ b/db/post_migrate/20190625184066_remove_sentry_from_application_settings.rb @@ -32,7 +32,7 @@ class RemoveSentryFromApplicationSettings < ActiveRecord::Migration[5.0] end SENTRY_DSN_COLUMNS.each do |column| - add_column(:application_settings, column, :string) unless column_exists?(:application_settings, column) + add_column(:application_settings, column, :string) unless column_exists?(:application_settings, column) # rubocop:disable Migration/AddLimitToStringColumns end end end diff --git a/db/post_migrate/20190801072937_add_gitlab_instance_administration_project.rb b/db/post_migrate/20190801072937_add_gitlab_instance_administration_project.rb new file mode 100644 index 00000000000..8b2cf7b3d76 --- /dev/null +++ b/db/post_migrate/20190801072937_add_gitlab_instance_administration_project.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +class AddGitlabInstanceAdministrationProject < ActiveRecord::Migration[5.2] + DOWNTIME = false + + def up + Gitlab::DatabaseImporters::SelfMonitoring::Project::CreateService.new.execute! + end + + def down + ApplicationSetting.current_without_cache + &.instance_administration_project + &.owner + &.destroy! + end +end diff --git a/db/post_migrate/20190801114109_cleanup_allow_local_requests_from_hooks_and_services_application_setting_rename.rb b/db/post_migrate/20190801114109_cleanup_allow_local_requests_from_hooks_and_services_application_setting_rename.rb index 127e44254ac..cb86f843f9c 100644 --- a/db/post_migrate/20190801114109_cleanup_allow_local_requests_from_hooks_and_services_application_setting_rename.rb +++ b/db/post_migrate/20190801114109_cleanup_allow_local_requests_from_hooks_and_services_application_setting_rename.rb @@ -12,6 +12,6 @@ class CleanupAllowLocalRequestsFromHooksAndServicesApplicationSettingRename < Ac end def down - rename_column_concurrently :application_settings, :allow_local_requests_from_web_hooks_and_services, :allow_local_requests_from_hooks_and_services + undo_cleanup_concurrent_column_rename :application_settings, :allow_local_requests_from_hooks_and_services, :allow_local_requests_from_web_hooks_and_services end end diff --git a/db/post_migrate/20190809072552_set_self_monitoring_project_alerting_token.rb b/db/post_migrate/20190809072552_set_self_monitoring_project_alerting_token.rb new file mode 100644 index 00000000000..0c4faebc548 --- /dev/null +++ b/db/post_migrate/20190809072552_set_self_monitoring_project_alerting_token.rb @@ -0,0 +1,73 @@ +# frozen_string_literal: true + +class SetSelfMonitoringProjectAlertingToken < ActiveRecord::Migration[5.2] + DOWNTIME = false + + module Migratable + module Alerting + class ProjectAlertingSetting < ApplicationRecord + self.table_name = 'project_alerting_settings' + + belongs_to :project + + validates :token, presence: true + + attr_encrypted :token, + mode: :per_attribute_iv, + key: Settings.attr_encrypted_db_key_base_truncated, + algorithm: 'aes-256-gcm' + + before_validation :ensure_token + + private + + def ensure_token + self.token ||= generate_token + end + + def generate_token + SecureRandom.hex + end + end + end + + class Project < ApplicationRecord + has_one :alerting_setting, inverse_of: :project, class_name: 'Alerting::ProjectAlertingSetting' + end + + class ApplicationSetting < ApplicationRecord + self.table_name = 'application_settings' + + belongs_to :instance_administration_project, class_name: 'Project' + + def self.current_without_cache + last + end + end + end + + def setup_alertmanager_token(project) + return unless License.feature_available?(:prometheus_alerts) + + project.create_alerting_setting! + end + + def up + Gitlab.ee do + project = Migratable::ApplicationSetting.current_without_cache&.instance_administration_project + + if project + setup_alertmanager_token(project) + end + end + end + + def down + Gitlab.ee do + Migratable::ApplicationSetting.current_without_cache + &.instance_administration_project + &.alerting_setting + &.destroy! + end + end +end diff --git a/db/post_migrate/20190812070645_migrate_private_profile_nulls.rb b/db/post_migrate/20190812070645_migrate_private_profile_nulls.rb new file mode 100644 index 00000000000..063c1e16c27 --- /dev/null +++ b/db/post_migrate/20190812070645_migrate_private_profile_nulls.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +class MigratePrivateProfileNulls < ActiveRecord::Migration[5.2] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + DELAY = 5.minutes.to_i + BATCH_SIZE = 1_000 + + disable_ddl_transaction! + + class User < ActiveRecord::Base + self.table_name = 'users' + + include ::EachBatch + end + + def up + # Migration will take about 7 hours + User.where(private_profile: nil).each_batch(of: BATCH_SIZE) do |batch, index| + range = batch.pluck(Arel.sql("MIN(id)"), Arel.sql("MAX(id)")).first + delay = index * DELAY + + BackgroundMigrationWorker.perform_in(delay.seconds, 'MigrateNullPrivateProfileToFalse', [*range]) + end + end + + def down + # noop + end +end diff --git a/db/schema.rb b/db/schema.rb index 591758af0e4..d1cba5e0954 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: 2019_08_06_071559) do +ActiveRecord::Schema.define(version: 2019_08_16_151221) do # These are extensions that must be enabled in order to support this database enable_extension "pg_trgm" @@ -26,6 +26,52 @@ ActiveRecord::Schema.define(version: 2019_08_06_071559) do t.integer "cached_markdown_version" end + create_table "allowed_email_domains", force: :cascade do |t| + t.datetime_with_timezone "created_at", null: false + t.datetime_with_timezone "updated_at", null: false + t.integer "group_id", null: false + t.string "domain", limit: 255, null: false + t.index ["group_id"], name: "index_allowed_email_domains_on_group_id" + end + + create_table "analytics_cycle_analytics_group_stages", force: :cascade do |t| + t.datetime_with_timezone "created_at", null: false + t.datetime_with_timezone "updated_at", null: false + t.integer "relative_position" + t.integer "start_event_identifier", null: false + t.integer "end_event_identifier", null: false + t.bigint "group_id", null: false + t.bigint "start_event_label_id" + t.bigint "end_event_label_id" + t.boolean "hidden", default: false, null: false + t.boolean "custom", default: true, null: false + t.string "name", limit: 255, null: false + t.index ["end_event_label_id"], name: "index_analytics_ca_group_stages_on_end_event_label_id" + t.index ["group_id", "name"], name: "index_analytics_ca_group_stages_on_group_id_and_name", unique: true + t.index ["group_id"], name: "index_analytics_ca_group_stages_on_group_id" + t.index ["relative_position"], name: "index_analytics_ca_group_stages_on_relative_position" + t.index ["start_event_label_id"], name: "index_analytics_ca_group_stages_on_start_event_label_id" + end + + create_table "analytics_cycle_analytics_project_stages", force: :cascade do |t| + t.datetime_with_timezone "created_at", null: false + t.datetime_with_timezone "updated_at", null: false + t.integer "relative_position" + t.integer "start_event_identifier", null: false + t.integer "end_event_identifier", null: false + t.bigint "project_id", null: false + t.bigint "start_event_label_id" + t.bigint "end_event_label_id" + t.boolean "hidden", default: false, null: false + t.boolean "custom", default: true, null: false + t.string "name", limit: 255, null: false + t.index ["end_event_label_id"], name: "index_analytics_ca_project_stages_on_end_event_label_id" + t.index ["project_id", "name"], name: "index_analytics_ca_project_stages_on_project_id_and_name", unique: true + t.index ["project_id"], name: "index_analytics_ca_project_stages_on_project_id" + t.index ["relative_position"], name: "index_analytics_ca_project_stages_on_relative_position" + t.index ["start_event_label_id"], name: "index_analytics_ca_project_stages_on_start_event_label_id" + end + create_table "appearances", id: :serial, force: :cascade do |t| t.string "title", null: false t.text "description", null: false @@ -1456,6 +1502,7 @@ ActiveRecord::Schema.define(version: 2019_08_06_071559) do t.integer "container_repositories_max_capacity", default: 10, null: false t.datetime_with_timezone "created_at" t.datetime_with_timezone "updated_at" + t.boolean "sync_object_storage", default: false, null: false t.index ["access_key"], name: "index_geo_nodes_on_access_key" t.index ["name"], name: "index_geo_nodes_on_name", unique: true t.index ["primary"], name: "index_geo_nodes_on_primary" @@ -2174,6 +2221,7 @@ ActiveRecord::Schema.define(version: 2019_08_06_071559) do t.boolean "membership_lock", default: false t.integer "last_ci_minutes_usage_notification_level" t.integer "subgroup_creation_level", default: 1 + t.boolean "emails_disabled" t.index ["created_at"], name: "index_namespaces_on_created_at" t.index ["custom_project_templates_group_id", "type"], name: "index_namespaces_on_custom_project_templates_group_id_and_type", where: "(custom_project_templates_group_id IS NOT NULL)" t.index ["file_template_project_id"], name: "index_namespaces_on_file_template_project_id" @@ -2239,7 +2287,7 @@ ActiveRecord::Schema.define(version: 2019_08_06_071559) do t.index ["line_code"], name: "index_notes_on_line_code" t.index ["note"], name: "index_notes_on_note_trigram", opclass: :gin_trgm_ops, using: :gin t.index ["noteable_id", "noteable_type"], name: "index_notes_on_noteable_id_and_noteable_type" - t.index ["noteable_type"], name: "index_notes_on_noteable_type" + t.index ["project_id", "id"], name: "index_notes_on_project_id_and_id_and_system_false", where: "(NOT system)" t.index ["project_id", "noteable_type"], name: "index_notes_on_project_id_and_noteable_type" t.index ["review_id"], name: "index_notes_on_review_id" end @@ -2452,6 +2500,7 @@ ActiveRecord::Schema.define(version: 2019_08_06_071559) do t.string "title" t.integer "active_pipelines_limit" t.integer "pipeline_size_limit" + t.integer "active_jobs_limit", default: 0 t.index ["name"], name: "index_plans_on_name" end @@ -2744,6 +2793,7 @@ ActiveRecord::Schema.define(version: 2019_08_06_071559) do t.boolean "reset_approvals_on_push", default: true t.boolean "service_desk_enabled", default: true t.integer "approvals_before_merge", default: 0, null: false + t.boolean "emails_disabled" t.index ["archived", "pending_delete", "merge_requests_require_code_owner_approval"], name: "projects_requiring_code_owner_approval", where: "((pending_delete = false) AND (archived = false) AND (merge_requests_require_code_owner_approval = true))" t.index ["created_at"], name: "index_projects_on_created_at" t.index ["creator_id"], name: "index_projects_on_creator_id" @@ -2966,7 +3016,6 @@ ActiveRecord::Schema.define(version: 2019_08_06_071559) do t.datetime_with_timezone "released_at", null: false t.index ["author_id"], name: "index_releases_on_author_id" t.index ["project_id", "tag"], name: "index_releases_on_project_id_and_tag" - t.index ["project_id"], name: "index_releases_on_project_id" end create_table "remote_mirrors", id: :serial, force: :cascade do |t| @@ -3362,6 +3411,7 @@ ActiveRecord::Schema.define(version: 2019_08_06_071559) do t.string "epics_sort" t.integer "roadmap_epics_state" t.string "roadmaps_sort" + t.string "projects_sort", limit: 64 t.index ["user_id"], name: "index_user_preferences_on_user_id", unique: true end @@ -3629,6 +3679,13 @@ ActiveRecord::Schema.define(version: 2019_08_06_071559) do t.index ["type"], name: "index_web_hooks_on_type" end + add_foreign_key "allowed_email_domains", "namespaces", column: "group_id", on_delete: :cascade + add_foreign_key "analytics_cycle_analytics_group_stages", "labels", column: "end_event_label_id", on_delete: :cascade + add_foreign_key "analytics_cycle_analytics_group_stages", "labels", column: "start_event_label_id", on_delete: :cascade + add_foreign_key "analytics_cycle_analytics_group_stages", "namespaces", column: "group_id", on_delete: :cascade + add_foreign_key "analytics_cycle_analytics_project_stages", "labels", column: "end_event_label_id", on_delete: :cascade + add_foreign_key "analytics_cycle_analytics_project_stages", "labels", column: "start_event_label_id", on_delete: :cascade + add_foreign_key "analytics_cycle_analytics_project_stages", "projects", on_delete: :cascade add_foreign_key "application_settings", "namespaces", column: "custom_project_templates_group_id", on_delete: :nullify add_foreign_key "application_settings", "projects", column: "file_template_project_id", name: "fk_ec757bd087", on_delete: :nullify add_foreign_key "application_settings", "projects", column: "instance_administration_project_id", on_delete: :nullify diff --git a/doc/README.md b/doc/README.md index c60e4eb177d..f12c06199c2 100644 --- a/doc/README.md +++ b/doc/README.md @@ -354,10 +354,11 @@ The following documentation relates to the DevOps **Secure** stage: | Secure Topics | Description | |:------------------------------------------------------------------------------------------------------|:-----------------------------------------------------------------------| | [Container Scanning](user/application_security/container_scanning/index.md) **(ULTIMATE)** | Use Clair to scan docker images for known vulnerabilities. | +| [Dependency List](user/application_security/dependency_list/index.md) **(ULTIMATE)** | View your project's dependencies and their known vulnerabilities. | | [Dependency Scanning](user/application_security/dependency_scanning/index.md) **(ULTIMATE)** | Analyze your dependencies for known vulnerabilities. | | [Dynamic Application Security Testing (DAST)](user/application_security/dast/index.md) **(ULTIMATE)** | Analyze running web applications for known vulnerabilities. | | [Group Security Dashboard](user/application_security/security_dashboard/index.md) **(ULTIMATE)** | View vulnerabilities in all the projects in a group and its subgroups. | -| [License Management](user/application_security/license_management/index.md) **(ULTIMATE)** | Search your project's dependencies for their licenses. | +| [License Compliance](user/application_security/license_management/index.md) **(ULTIMATE)** | Search your project's dependencies for their licenses. | | [Project Security Dashboard](user/application_security/security_dashboard/index.md) **(ULTIMATE)** | View the latest security reports for your project. | | [Static Application Security Testing (SAST)](user/application_security/sast/index.md) **(ULTIMATE)** | Analyze source code for known vulnerabilities. | diff --git a/doc/administration/audit_events.md b/doc/administration/audit_events.md index 02de2caf558..8075a40cae7 100644 --- a/doc/administration/audit_events.md +++ b/doc/administration/audit_events.md @@ -75,6 +75,8 @@ From there, you can see the following actions: - User was removed from project - Project export was downloaded - Project repository was downloaded +- Project was archived +- Project was unarchived ### Instance events **(PREMIUM ONLY)** diff --git a/doc/administration/container_registry.md b/doc/administration/container_registry.md index e418938451a..f5d58db0133 100644 --- a/doc/administration/container_registry.md +++ b/doc/administration/container_registry.md @@ -137,6 +137,15 @@ otherwise you will run into conflicts. 1. Save the file and [reconfigure GitLab][] for the changes to take effect. +1. Validate using: + + ```sh + openssl s_client -showcerts -servername gitlab.example.com -connect gitlab.example.com:443 > cacert.pem + ``` + +NOTE: **Note:** +If your certificate provider provides the CA Bundle certificates, append them to the TLS certificate file. + **Installations from source** 1. Open `/home/git/gitlab/config/gitlab.yml`, find the `registry` entry and @@ -163,9 +172,13 @@ docker login gitlab.example.com:4567 If the Registry is configured to use its own domain, you will need a TLS certificate for that specific domain (e.g., `registry.example.com`) or maybe -a wildcard certificate if hosted under a subdomain of your existing GitLab +a wildcard certificate if hosted under a subdomain of your existing GitLab domain (e.g., `registry.gitlab.example.com`). +NOTE: **Note:** +As well as manually generated SSL certificates (explained here), certificates automatically +generated by Let's Encrypt are also [supported in Omnibus installs](https://docs.gitlab.com/omnibus/settings/ssl.html#host-services). + Let's assume that you want the container Registry to be accessible at `https://registry.gitlab.example.com`. @@ -458,7 +471,7 @@ You can use GitLab as an auth endpoint and use a non-bundled Container Registry. ``` 1. A certificate keypair is required for GitLab and the Container Registry to - communicate securely. By default omnibus-gitlab will generate one keypair, + communicate securely. By default Omnibus GitLab will generate one keypair, which is saved to `/var/opt/gitlab/gitlab-rails/etc/gitlab-registry.key`. When using a non-bundled Container Registry, you will need to supply a custom certificate key. To do that, add the following to @@ -474,7 +487,7 @@ You can use GitLab as an auth endpoint and use a non-bundled Container Registry. **Note:** The file specified at `registry_key_path` gets populated with the content specified by `internal_key`, each time reconfigure is executed. If - no file is specified, omnibus-gitlab will default it to + no file is specified, Omnibus GitLab will default it to `/var/opt/gitlab/gitlab-rails/etc/gitlab-registry.key` and will populate it. @@ -669,6 +682,39 @@ To get around this, you can [change the group path](../user/group/index.md#chang branch name. Another option is to create a [push rule](../push_rules/push_rules.html) to prevent this at the instance level. +### Image push errors + +When getting errors or "retrying" loops in an attempt to push an image but `docker login` works fine, +there is likely an issue with the headers forwarded to the registry by NGINX. The default recommended +NGINX configurations should handle this, but it might occur in custom setups where the SSL is +offloaded to a third party reverse proxy. + +This problem was discussed in a [docker project issue][docker-image-push-issue] and a simple solution +would be to enable relative urls in the registry. + +**For Omnibus installations** + +1. Edit `/etc/gitlab/gitlab.rb`: + + ```ruby + registry['env'] = { + "REGISTRY_HTTP_RELATIVEURLS" => true + } + ``` + +1. Save the file and [reconfigure GitLab][] for the changes to take effect. + +**For installations from source** + +1. Edit the YML configuration file you created when you [deployed the registry][registry-deploy]. Add the following snippet: + + ```yaml + http: + relativeurls: true + ``` + +1. Restart the registry for the changes to take affect. + [ce-18239]: https://gitlab.com/gitlab-org/gitlab-ce/issues/18239 [docker-insecure-self-signed]: https://docs.docker.com/registry/insecure/#use-self-signed-certificates [reconfigure gitlab]: restart_gitlab.md#omnibus-gitlab-reconfigure @@ -687,3 +733,4 @@ this at the instance level. [new-domain]: #configure-container-registry-under-its-own-domain [notifications-config]: https://docs.docker.com/registry/notifications/ [registry-notifications-config]: https://docs.docker.com/registry/configuration/#notifications +[docker-image-push-issue]: https://github.com/docker/distribution/issues/970 diff --git a/doc/administration/custom_hooks.md b/doc/administration/custom_hooks.md index 32462a95a1a..7238d08ab09 100644 --- a/doc/administration/custom_hooks.md +++ b/doc/administration/custom_hooks.md @@ -12,17 +12,17 @@ NOTE: **Note:** Custom Git hooks won't be replicated to secondary nodes if you use [GitLab Geo](geo/replication/index.md) Git natively supports hooks that are executed on different actions. -Examples of server-side git hooks include pre-receive, post-receive, and update. +Examples of server-side Git hooks include pre-receive, post-receive, and update. See [Git SCM Server-Side Hooks][hooks] for more information about each hook type. -As of gitlab-shell version 2.2.0 (which requires GitLab 7.5+), GitLab -administrators can add custom git hooks to any GitLab project. +As of GitLab Shell version 2.2.0 (which requires GitLab 7.5+), GitLab +administrators can add custom Git hooks to any GitLab project. ## Create a custom Git hook for a repository Server-side Git hooks are typically placed in the repository's `hooks` -subdirectory. In GitLab, hook directories are symlinked to the gitlab-shell -`hooks` directory for ease of maintenance between gitlab-shell upgrades. +subdirectory. In GitLab, hook directories are symlinked to the GitLab Shell +`hooks` directory for ease of maintenance between GitLab Shell upgrades. Custom hooks are implemented differently, but the behavior is exactly the same once the hook is created. Follow the steps below to set up a custom hook for a repository: @@ -36,7 +36,7 @@ repository: 1. Inside the new `custom_hooks` directory, create a file with a name matching the hook type. For a pre-receive hook the file name should be `pre-receive` with no extension. -1. Make the hook file executable and make sure it's owned by git. +1. Make the hook file executable and make sure it's owned by Git. 1. Write the code to make the Git hook function as expected. Hooks can be in any language. Ensure the 'shebang' at the top properly reflects the language type. For example, if the script is in Ruby the shebang will probably be @@ -49,17 +49,17 @@ as appropriate. To create a Git hook that applies to all of your repositories in your instance, set a global Git hook. Since all the repositories' `hooks` -directories are symlinked to gitlab-shell's `hooks` directory, adding any hook -to the gitlab-shell `hooks` directory will also apply it to all repositories. Follow +directories are symlinked to GitLab Shell's `hooks` directory, adding any hook +to the GitLab Shell `hooks` directory will also apply it to all repositories. Follow the steps below to properly set up a custom hook for all repositories: 1. On the GitLab server, navigate to the configured custom hook directory. The - default is in the gitlab-shell directory. The gitlab-shell `hook` directory + default is in the GitLab Shell directory. The GitLab Shell `hook` directory for an installation from source the path is usually `/home/git/gitlab-shell/hooks`. For Omnibus installs the path is usually `/opt/gitlab/embedded/service/gitlab-shell/hooks`. To look in a different directory for the global custom hooks, - set `custom_hooks_dir` in the gitlab-shell config. For + set `custom_hooks_dir` in the GitLab Shell config. For Omnibus installations, this can be set in `gitlab.rb`; and in source installations, this can be set in `gitlab-shell/config.yml`. 1. Create a new directory in this location. Depending on your hook, it will be diff --git a/doc/administration/dependency_proxy.md b/doc/administration/dependency_proxy.md index d2c52b67e67..5153666705f 100644 --- a/doc/administration/dependency_proxy.md +++ b/doc/administration/dependency_proxy.md @@ -48,7 +48,7 @@ local location or even use object storage. The dependency proxy files for Omnibus GitLab installations are stored under `/var/opt/gitlab/gitlab-rails/shared/dependency_proxy/` and for source -installations under `shared/dependency_proxy/` (relative to the git home directory). +installations under `shared/dependency_proxy/` (relative to the Git home directory). To change the local storage path: **Omnibus GitLab installations** diff --git a/doc/administration/geo/disaster_recovery/index.md b/doc/administration/geo/disaster_recovery/index.md index d44e141b66b..ba95843b0b0 100644 --- a/doc/administration/geo/disaster_recovery/index.md +++ b/doc/administration/geo/disaster_recovery/index.md @@ -1,4 +1,4 @@ -# Disaster Recovery **(PREMIUM ONLY)** +# Disaster Recovery (Geo) **(PREMIUM ONLY)** Geo replicates your database, your Git repositories, and few other assets. We will support and replicate more data in the future, that will enable you to diff --git a/doc/administration/geo/replication/index.md b/doc/administration/geo/replication/index.md index 5aedb665935..dbd466b906d 100644 --- a/doc/administration/geo/replication/index.md +++ b/doc/administration/geo/replication/index.md @@ -1,4 +1,4 @@ -# Geo Replication **(PREMIUM ONLY)** +# Replication (Geo) **(PREMIUM ONLY)** > - Introduced in GitLab Enterprise Edition 8.9. > - Using Geo in combination with @@ -6,7 +6,7 @@ > is considered **Generally Available** (GA) in > [GitLab Premium](https://about.gitlab.com/pricing/) 10.4. -Geo is the solution for widely distributed development teams. +Replication with Geo is the solution for widely distributed development teams. ## Overview diff --git a/doc/administration/git_protocol.md b/doc/administration/git_protocol.md index 1780e1babe9..9d653d4e09e 100644 --- a/doc/administration/git_protocol.md +++ b/doc/administration/git_protocol.md @@ -53,7 +53,7 @@ sudo service ssh restart ## Instructions In order to use the new protocol, clients need to either pass the configuration -`-c protocol.version=2` to the git command, or set it globally: +`-c protocol.version=2` to the Git command, or set it globally: ```sh git config --global protocol.version 2 diff --git a/doc/administration/gitaly/index.md b/doc/administration/gitaly/index.md index 150494c47e5..e0a06e71316 100644 --- a/doc/administration/gitaly/index.md +++ b/doc/administration/gitaly/index.md @@ -64,6 +64,7 @@ The following list depicts what the network architecture of Gitaly is: topology. - A `(Gitaly address, Gitaly token)` corresponds to a Gitaly server. - A Gitaly server hosts one or more storages. +- A GitLab server can use one or more Gitaly servers. - Gitaly addresses must be specified in such a way that they resolve correctly for ALL Gitaly clients. - Gitaly clients are: Unicorn, Sidekiq, gitlab-workhorse, @@ -77,14 +78,16 @@ The following list depicts what the network architecture of Gitaly is: - Authentication is done through a static token which is shared among the Gitaly and GitLab Rails nodes. -Below we describe how to configure a Gitaly server at address -`gitaly.internal:8075` with secret token `abc123secret`. We assume -your GitLab installation has two repository storages, `default` and -`storage1`. +Below we describe how to configure two Gitaly servers one at +`gitaly1.internal` and the other at `gitaly2.internal` +with secret token `abc123secret`. We assume +your GitLab installation has three repository storages: `default`, +`storage1` and `storage2`. ### 1. Installation -First install Gitaly using either Omnibus GitLab or install it from source: +First install Gitaly on each Gitaly server using either +Omnibus GitLab or install it from source: - For Omnibus GitLab: [Download/install](https://about.gitlab.com/install/) the Omnibus GitLab package you want using **steps 1 and 2** from the GitLab downloads page but @@ -119,7 +122,7 @@ Configure a token on the instance that runs the GitLab Rails application. ### 3. Gitaly server configuration -Next, on the Gitaly server, you need to configure storage paths, enable +Next, on the Gitaly servers, you need to configure storage paths, enable the network listener and configure the token. NOTE: **Note:** if you want to reduce the risk of downtime when you enable @@ -175,15 +178,29 @@ Check the directory layout on your Gitaly server to be sure. gitaly['listen_addr'] = "0.0.0.0:8075" gitaly['auth_token'] = 'abc123secret' + # To use TLS for Gitaly you need to add + gitaly['tls_listen_addr'] = "0.0.0.0:9999" + gitaly['certificate_path'] = "path/to/cert.pem" + gitaly['key_path'] = "path/to/key.pem" + ``` + +1. Append the following to `/etc/gitlab/gitlab.rb` for each respective server: + + For `gitaly1.internal`: + + ``` gitaly['storage'] = [ { 'name' => 'default' }, { 'name' => 'storage1' }, ] + ``` + + For `gitaly2.internal`: - # To use TLS for Gitaly you need to add - gitaly['tls_listen_addr'] = "0.0.0.0:9999" - gitaly['certificate_path'] = "path/to/cert.pem" - gitaly['key_path'] = "path/to/key.pem" + ``` + gitaly['storage'] = [ + { 'name' => 'storage2' }, + ] ``` NOTE: **Note:** @@ -206,13 +223,26 @@ Check the directory layout on your Gitaly server to be sure. [auth] token = 'abc123secret' + ``` +1. Append the following to `/home/git/gitaly/config.toml` for each respective server: + + For `gitaly1.internal`: + + ```toml [[storage]] name = 'default' [[storage]] name = 'storage1' ``` + + For `gitaly2.internal`: + + ```toml + [[storage]] + name = 'storage2' + ``` NOTE: **Note:** In some cases, you'll have to set `path` for each `[[storage]]` in the @@ -231,9 +261,13 @@ then all Gitaly requests will fail. Additionally, you need to [disable Rugged if previously manually enabled](../high_availability/nfs.md#improving-nfs-performance-with-gitlab). -We assume that your Gitaly server can be reached at -`gitaly.internal:8075` from your GitLab server, and that Gitaly can read and -write to `/mnt/gitlab/default` and `/mnt/gitlab/storage1` respectively. +We assume that your `gitaly1.internal` Gitaly server can be reached at +`gitaly1.internal:8075` from your GitLab server, and that Gitaly server +can read and write to `/mnt/gitlab/default` and `/mnt/gitlab/storage1`. + +We assume also that your `gitaly2.internal` Gitaly server can be reached at +`gitaly2.internal:8075` from your GitLab server, and that Gitaly server +can read and write to `/mnt/gitlab/storage2`. **For Omnibus GitLab** @@ -241,8 +275,9 @@ write to `/mnt/gitlab/default` and `/mnt/gitlab/storage1` respectively. ```ruby git_data_dirs({ - 'default' => { 'gitaly_address' => 'tcp://gitaly.internal:8075' }, - 'storage1' => { 'gitaly_address' => 'tcp://gitaly.internal:8075' }, + 'default' => { 'gitaly_address' => 'tcp://gitaly1.internal:8075' }, + 'storage1' => { 'gitaly_address' => 'tcp://gitaly1.internal:8075' }, + 'storage2' => { 'gitaly_address' => 'tcp://gitaly2.internal:8075' }, }) gitlab_rails['gitaly_token'] = 'abc123secret' @@ -268,9 +303,11 @@ write to `/mnt/gitlab/default` and `/mnt/gitlab/storage1` respectively. repositories: storages: default: - gitaly_address: tcp://gitaly.internal:8075 + gitaly_address: tcp://gitaly1.internal:8075 storage1: - gitaly_address: tcp://gitaly.internal:8075 + gitaly_address: tcp://gitaly1.internal:8075 + storage2: + gitaly_address: tcp://gitaly2.internal:8075 gitaly: token: 'abc123secret' @@ -350,8 +387,9 @@ To configure Gitaly with TLS: ```ruby git_data_dirs({ - 'default' => { 'gitaly_address' => 'tls://gitaly.internal:9999' }, - 'storage1' => { 'gitaly_address' => 'tls://gitaly.internal:9999' }, + 'default' => { 'gitaly_address' => 'tls://gitaly1.internal:9999' }, + 'storage1' => { 'gitaly_address' => 'tls://gitaly1.internal:9999' }, + 'storage2' => { 'gitaly_address' => 'tls://gitaly2.internal:9999' }, }) gitlab_rails['gitaly_token'] = 'abc123secret' @@ -377,9 +415,11 @@ To configure Gitaly with TLS: repositories: storages: default: - gitaly_address: tls://gitaly.internal:9999 + gitaly_address: tls://gitaly1.internal:9999 storage1: - gitaly_address: tls://gitaly.internal:9999 + gitaly_address: tls://gitaly1.internal:9999 + storage2: + gitaly_address: tls://gitaly2.internal:9999 gitaly: token: 'abc123secret' @@ -615,3 +655,33 @@ To fix this problem, confirm that your [`gitlab-secrets.json` file](#3-gitaly-se on the Gitaly node matches the one on all other nodes. If it doesn't match, update the secrets file on the Gitaly node to match the others, then [reconfigure the node](../restart_gitlab.md#omnibus-gitlab-reconfigure). + +### Command line tools cannot connect to Gitaly + +If you are having trouble connecting to a Gitaly node with command line (CLI) tools, and certain actions result in a `14: Connect Failed` error message, it means that gRPC cannot reach your Gitaly node. + +Verify that you can reach Gitaly via TCP: + +```bash +sudo gitlab-rake gitlab:tcp_check[GITALY_SERVER_IP,GITALY_LISTEN_PORT] +``` + +If the TCP connection fails, check your network settings and your firewall rules. If the TCP connection succeeds, your networking and firewall rules are correct. + +If you use proxy servers in your command line environment, such as Bash, these can interfere with your gRPC traffic. + +If you use Bash or a compatible command line environment, run the following commands to determine whether you have proxy servers configured: + +```bash +echo $http_proxy +echo $https_proxy +``` + +If either of these variables have a value, your Gitaly CLI connections may be getting routed through a proxy which cannot connect to Gitaly. + +To remove the proxy setting, run the following commands (depending on which variables had values): + +```bash +unset http_proxy +unset https_proxy +```
\ No newline at end of file diff --git a/doc/administration/high_availability/load_balancer.md b/doc/administration/high_availability/load_balancer.md index 9e9f604317a..f11d27487d1 100644 --- a/doc/administration/high_availability/load_balancer.md +++ b/doc/administration/high_availability/load_balancer.md @@ -60,11 +60,21 @@ for details on managing SSL certificates and configuring Nginx. ### Basic ports -| LB Port | Backend Port | Protocol | -| ------- | ------------ | --------------- | -| 80 | 80 | HTTP [^1] | -| 443 | 443 | TCP or HTTPS [^1] [^2] | -| 22 | 22 | TCP | +| LB Port | Backend Port | Protocol | +| ------- | ------------ | ------------------------ | +| 80 | 80 | HTTP (*1*) | +| 443 | 443 | TCP or HTTPS (*1*) (*2*) | +| 22 | 22 | TCP | + +- (*1*): [Web terminal](../../ci/environments.md#web-terminals) support requires + your load balancer to correctly handle WebSocket connections. When using + HTTP or HTTPS proxying, this means your load balancer must be configured + to pass through the `Connection` and `Upgrade` hop-by-hop headers. See the + [web terminal](../integration/terminal.md) integration guide for + more details. +- (*2*): When using HTTPS protocol for port 443, you will need to add an SSL + certificate to the load balancers. If you wish to terminate SSL at the + GitLab application server instead, use TCP protocol. ### GitLab Pages Ports @@ -72,12 +82,19 @@ If you're using GitLab Pages with custom domain support you will need some additional port configurations. GitLab Pages requires a separate virtual IP address. Configure DNS to point the `pages_external_url` from `/etc/gitlab/gitlab.rb` at the new virtual IP address. See the -[GitLab Pages documentation][gitlab-pages] for more information. +[GitLab Pages documentation](../pages/index.md) for more information. -| LB Port | Backend Port | Protocol | -| ------- | ------------ | -------- | -| 80 | Varies [^3] | HTTP | -| 443 | Varies [^3] | TCP [^4] | +| LB Port | Backend Port | Protocol | +| ------- | ------------- | --------- | +| 80 | Varies (*1*) | HTTP | +| 443 | Varies (*1*) | TCP (*2*) | + +- (*1*): The backend port for GitLab Pages depends on the + `gitlab_pages['external_http']` and `gitlab_pages['external_https']` + setting. See [GitLab Pages documentation](../pages/index.md) for more details. +- (*2*): Port 443 for GitLab Pages should always use the TCP protocol. Users can + configure custom domains with custom SSL, which would not be possible + if SSL was terminated at the load balancer. ### Alternate SSH Port @@ -86,7 +103,7 @@ it may be helpful to configure an alternate SSH hostname that allows users to use SSH on port 443. An alternate SSH hostname will require a new virtual IP address compared to the other GitLab HTTP configuration above. -Configure DNS for an alternate SSH hostname such as altssh.gitlab.example.com. +Configure DNS for an alternate SSH hostname such as `altssh.gitlab.example.com`. | LB Port | Backend Port | Protocol | | ------- | ------------ | -------- | @@ -101,24 +118,6 @@ Read more on high-availability configuration: 1. [Configure NFS](nfs.md) 1. [Configure the GitLab application servers](gitlab.md) -[^1]: [Web terminal](../../ci/environments.md#web-terminals) support requires - your load balancer to correctly handle WebSocket connections. When using - HTTP or HTTPS proxying, this means your load balancer must be configured - to pass through the `Connection` and `Upgrade` hop-by-hop headers. See the - [web terminal](../integration/terminal.md) integration guide for - more details. -[^2]: When using HTTPS protocol for port 443, you will need to add an SSL - certificate to the load balancers. If you wish to terminate SSL at the - GitLab application server instead, use TCP protocol. -[^3]: The backend port for GitLab Pages depends on the - `gitlab_pages['external_http']` and `gitlab_pages['external_https']` - setting. See [GitLab Pages documentation][gitlab-pages] for more details. -[^4]: Port 443 for GitLab Pages should always use the TCP protocol. Users can - configure custom domains with custom SSL, which would not be possible - if SSL was terminated at the load balancer. - -[gitlab-pages]: ../pages/index.md - <!-- ## Troubleshooting Include any troubleshooting steps that you can foresee. If you know beforehand what issues diff --git a/doc/administration/incoming_email.md b/doc/administration/incoming_email.md index 73a39a6dd35..29915cb3a99 100644 --- a/doc/administration/incoming_email.md +++ b/doc/administration/incoming_email.md @@ -151,7 +151,7 @@ Reply by email should now be working. #### Postfix -Example configuration for Postfix mail server. Assumes mailbox incoming@gitlab.example.com. +Example configuration for Postfix mail server. Assumes mailbox `incoming@gitlab.example.com`. Example for Omnibus installs: @@ -218,7 +218,7 @@ incoming_email: #### Gmail -Example configuration for Gmail/G Suite. Assumes mailbox gitlab-incoming@gmail.com. +Example configuration for Gmail/G Suite. Assumes mailbox `gitlab-incoming@gmail.com`. Example for Omnibus installs: diff --git a/doc/administration/index.md b/doc/administration/index.md index fb07b41eb8b..650cb10a64a 100644 --- a/doc/administration/index.md +++ b/doc/administration/index.md @@ -64,6 +64,7 @@ Learn how to install, configure, update, and maintain your GitLab instance. - [External Classification Policy Authorization](../user/admin_area/settings/external_authorization.md) **(PREMIUM ONLY)** - [Upload a license](../user/admin_area/license.md): Upload a license to unlock features that are in paid tiers of GitLab. **(STARTER ONLY)** - [Admin Area](../user/admin_area/index.md): for self-managed instance-wide configuration and maintenance. +- [S/MIME Signing](smime_signing_email.md): how to sign all outgoing notification emails with S/MIME #### Customizing GitLab's appearance @@ -190,3 +191,8 @@ Learn how to install, configure, update, and maintain your GitLab instance. - [Troubleshooting ElasticSearch](troubleshooting/elasticsearch.md): Tips to troubleshoot ElasticSearch. - [Kubernetes troubleshooting](troubleshooting/kubernetes_cheat_sheet.md): Commands and tips useful for troubleshooting Kubernetes-related issues. +- Useful links from the Support Team: + - [GitLab Developer Docs](https://docs.gitlab.com/ee/development/README.html). + - [Repairing and recovering broken Git repositories](https://git.seveas.net/repairing-and-recovering-broken-git-repositories.html). + - [Testing with OpenSSL](https://www.feistyduck.com/library/openssl-cookbook/online/ch-testing-with-openssl.html). + - [Strace zine](https://wizardzines.com/zines/strace/). diff --git a/doc/administration/instance_review.md b/doc/administration/instance_review.md index ab6a4646a71..825435deff9 100644 --- a/doc/administration/instance_review.md +++ b/doc/administration/instance_review.md @@ -14,4 +14,3 @@ Additionally you will be contacted by our team for further review which should h [6995]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/6995 [ee]: https://about.gitlab.com/pricing/ - diff --git a/doc/administration/integration/plantuml.md b/doc/administration/integration/plantuml.md index c2ac063ce37..16a193550a1 100644 --- a/doc/administration/integration/plantuml.md +++ b/doc/administration/integration/plantuml.md @@ -72,12 +72,12 @@ our AsciiDoc snippets, wikis and repos using delimited blocks: - **Markdown** - ````markdown + ~~~markdown ```plantuml Bob -> Alice : hello Alice -> Bob : Go Away ``` - ```` + ~~~ - **AsciiDoc** diff --git a/doc/administration/issue_closing_pattern.md b/doc/administration/issue_closing_pattern.md index e1bbabb2878..66ab3ef8d81 100644 --- a/doc/administration/issue_closing_pattern.md +++ b/doc/administration/issue_closing_pattern.md @@ -13,7 +13,7 @@ in the project's default branch. In order to change the pattern you need to have access to the server that GitLab is installed on. -The default pattern can be located in [gitlab.yml.example] under the +The default pattern can be located in [`gitlab.yml.example`] under the "Automatic issue closing" section. > **Tip:** diff --git a/doc/administration/job_artifacts.md b/doc/administration/job_artifacts.md index 2b624f8de77..350cd5b7992 100644 --- a/doc/administration/job_artifacts.md +++ b/doc/administration/job_artifacts.md @@ -94,7 +94,7 @@ If you're enabling S3 in [GitLab HA](high_availability/README.md), you will need ### Object Storage Settings -For source installations the following settings are nested under `artifacts:` and then `object_store:`. On omnibus installs they are prefixed by `artifacts_object_store_`. +For source installations the following settings are nested under `artifacts:` and then `object_store:`. On Omnibus GitLab installs they are prefixed by `artifacts_object_store_`. | Setting | Description | Default | |---------|-------------|---------| @@ -115,7 +115,7 @@ The connection settings match those provided by [Fog](https://github.com/fog), a | `aws_access_key_id` | AWS credentials, or compatible | | | `aws_secret_access_key` | AWS credentials, or compatible | | | `aws_signature_version` | AWS signature version to use. 2 or 4 are valid options. Digital Ocean Spaces and other providers may need 2. | 4 | -| `enable_signature_v4_streaming` | Set to true to enable HTTP chunked transfers with AWS v4 signatures (https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-streaming.html). Oracle Cloud S3 needs this to be false | true +| `enable_signature_v4_streaming` | Set to true to enable HTTP chunked transfers with [AWS v4 signatures](https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-streaming.html). Oracle Cloud S3 needs this to be false | true | | `region` | AWS region | us-east-1 | | `host` | S3 compatible host for when not using AWS, e.g. `localhost` or `storage.example.com` | s3.amazonaws.com | | `endpoint` | Can be used when configuring an S3 compatible service such as [Minio](https://www.minio.io), by entering a URL such as `http://127.0.0.1:9000` | (optional) | diff --git a/doc/administration/logs.md b/doc/administration/logs.md index 47abbc512e0..9030c941cad 100644 --- a/doc/administration/logs.md +++ b/doc/administration/logs.md @@ -88,7 +88,7 @@ Introduced in GitLab 10.0, this file lives in It helps you see requests made directly to the API. For example: ```json -{"time":"2018-10-29T12:49:42.123Z","severity":"INFO","duration":709.08,"db":14.59,"view":694.49,"status":200,"method":"GET","path":"/api/v4/projects","params":[{"key":"action","value":"git-upload-pack"},{"key":"changes","value":"_any"},{"key":"key_id","value":"secret"},{"key":"secret_token","value":"[FILTERED]"}],"host":"localhost","ip":"::1","ua":"Ruby","route":"/api/:version/projects","user_id":1,"username":"root","queue_duration":100.31,"gitaly_calls":30,"gitaly_duration":5.36} +{"time":"2018-10-29T12:49:42.123Z","severity":"INFO","duration":709.08,"db":14.59,"view":694.49,"status":200,"method":"GET","path":"/api/v4/projects","params":[{"key":"action","value":"git-upload-pack"},{"key":"changes","value":"_any"},{"key":"key_id","value":"secret"},{"key":"secret_token","value":"[FILTERED]"}],"host":"localhost","remote_ip":"::1","ua":"Ruby","route":"/api/:version/projects","user_id":1,"username":"root","queue_duration":100.31,"gitaly_calls":30,"gitaly_duration":5.36} ``` This entry above shows an access to an internal endpoint to check whether an @@ -284,13 +284,16 @@ Introduced in GitLab 11.3. This file lives in `/var/log/gitlab/gitlab-rails/impo Omnibus GitLab packages or in `/home/git/gitlab/log/importer.log` for installations from source. -## `auth.log` +## `auth.log` Introduced in GitLab 12.0. This file lives in `/var/log/gitlab/gitlab-rails/auth.log` for Omnibus GitLab packages or in `/home/git/gitlab/log/auth.log` for installations from source. -It logs information whenever [Rack Attack] registers an abusive request. +This log records: + +- Information whenever [Rack Attack] registers an abusive request. +- Requests over the [Rate Limit] on raw endpoints. NOTE: **Note:** From [%12.1](https://gitlab.com/gitlab-org/gitlab-ce/issues/62756), user id and username are available on this log. @@ -309,6 +312,12 @@ GraphQL queries are recorded in that file. For example: {"query_string":"query IntrospectionQuery{__schema {queryType { name },mutationType { name }}}...(etc)","variables":{"a":1,"b":2},"complexity":181,"depth":1,"duration":7} ``` +## `migrations.log` + +Introduced in GitLab 12.3. This file lives in `/var/log/gitlab/gitlab-rails/migrations.log` for +Omnibus GitLab packages or in `/home/git/gitlab/log/migrations.log` for +installations from source. + ## Reconfigure Logs Reconfigure log files live in `/var/log/gitlab/reconfigure` for Omnibus GitLab @@ -328,3 +337,4 @@ installations from source. [repocheck]: repository_checks.md [Rack Attack]: ../security/rack_attack.md +[Rate Limit]: ../user/admin_area/settings/rate_limits_on_raw_endpoints.md diff --git a/doc/administration/merge_request_diffs.md b/doc/administration/merge_request_diffs.md index 0b065082ded..d52d865cec5 100644 --- a/doc/administration/merge_request_diffs.md +++ b/doc/administration/merge_request_diffs.md @@ -90,7 +90,7 @@ The connection settings match those provided by [Fog](https://github.com/fog), a | `aws_access_key_id` | AWS credentials, or compatible | | | `aws_secret_access_key` | AWS credentials, or compatible | | | `aws_signature_version` | AWS signature version to use. 2 or 4 are valid options. Digital Ocean Spaces and other providers may need 2. | 4 | -| `enable_signature_v4_streaming` | Set to true to enable HTTP chunked transfers with AWS v4 signatures (https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-streaming.html). Oracle Cloud S3 needs this to be false | true +| `enable_signature_v4_streaming` | Set to true to enable HTTP chunked transfers with [AWS v4 signatures](https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-streaming.html). Oracle Cloud S3 needs this to be false | true | | `region` | AWS region | us-east-1 | | `host` | S3 compatible host for when not using AWS, e.g. `localhost` or `storage.example.com` | s3.amazonaws.com | | `endpoint` | Can be used when configuring an S3 compatible service such as [Minio](https://www.minio.io), by entering a URL such as `http://127.0.0.1:9000` | (optional) | diff --git a/doc/administration/monitoring/gitlab_instance_administration_project/index.md b/doc/administration/monitoring/gitlab_instance_administration_project/index.md new file mode 100644 index 00000000000..d445b68721d --- /dev/null +++ b/doc/administration/monitoring/gitlab_instance_administration_project/index.md @@ -0,0 +1,36 @@ +# GitLab instance administration project + +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/56883) in GitLab 12.2. + +GitLab has been adding the ability for administrators to see insights into the health of +their GitLab instance. In order to surface this experience in a native way, similar to how +you would interact with an application deployed via GitLab, a base project called +"GitLab Instance Administration" with +[internal visibility](../../../public_access/public_access.md#internal-projects) will be +added under a group called "GitLab Instance Administrators" specifically created for +visualizing and configuring the monitoring of your GitLab instance. + +All administrators at the time of creation of the project and group will be added +as maintainers of the group and project, and as an admin, you'll be able to add new +members to the group in order to give them maintainer access to the project. + +This project will be used for self-monitoring your GitLab instance. + +## Connection to Prometheus + +The project will be automatically configured to connect to the +[internal Prometheus](../prometheus/index.md) instance if the Prometheus +instance is present (should be the case if GitLab was installed via Omnibus +and you haven't disabled it). + +If that's not the case or if you have an external Prometheus instance or an HA setup, +you should +[configure it manually](../../../user/project/integrations/prometheus.md#manual-configuration-of-prometheus). + +## Taking action on Prometheus alerts **(ULTIMATE)** + +You can [add a webhook](../../../user/project/integrations/prometheus.md#external-prometheus-instances) +to the Prometheus config in order for GitLab to receive notifications of any alerts. + +Once the webhook is setup, you can +[take action on incoming alerts](../../../user/project/integrations/prometheus.md#taking-action-on-incidents-ultimate). diff --git a/doc/administration/monitoring/index.md b/doc/administration/monitoring/index.md index fa0459b24ff..2b3daec42bd 100644 --- a/doc/administration/monitoring/index.md +++ b/doc/administration/monitoring/index.md @@ -2,6 +2,9 @@ Explore our features to monitor your GitLab instance: +- [GitLab self-monitoring](gitlab_instance_administration_project/index.md): The + GitLab instance administration project helps to monitor the GitLab instance and + take action on alerts. - [Performance monitoring](performance/index.md): GitLab Performance Monitoring makes it possible to measure a wide variety of statistics of your instance. - [Prometheus](prometheus/index.md): Prometheus is a powerful time-series monitoring service, providing a flexible platform for monitoring GitLab and other software products. - [GitHub imports](github_imports.md): Monitor the health and progress of GitLab's GitHub importer with various Prometheus metrics. diff --git a/doc/administration/monitoring/performance/grafana_configuration.md b/doc/administration/monitoring/performance/grafana_configuration.md index 6778c339922..95be0d5fd88 100644 --- a/doc/administration/monitoring/performance/grafana_configuration.md +++ b/doc/administration/monitoring/performance/grafana_configuration.md @@ -118,6 +118,36 @@ If you have set up Grafana, you can enable a link to access it easily from the s 1. Click **Save changes**. 1. The new link will be available in the admin area under **Monitoring > Metrics Dashboard**. +## Security Update + +Users running GitLab version 12.0 or later should immediately upgrade to one of the following security releases due to a known vulnerability with the embedded Grafana dashboard: + +- 12.0.6 +- 12.1.6 + +After upgrading, the Grafana dashboard will be disabled and the location of your existing Grafana data will be changed from `/var/opt/gitlab/grafana/data/` to `/var/opt/gitlab/grafana/data.bak.#{Date.today}/`. + +To prevent the data from being relocated, you can run the following command prior to upgrading: + +```sh +echo "0" > /var/opt/gitlab/grafana/CVE_reset_status +``` + +To reinstate your old data, move it back into its original location: + +``` +sudo mv /var/opt/gitlab/grafana/data.bak.xxxx/ /var/opt/gitlab/grafana/data/ +``` + +However, you should **not** reinstate your old data _except_ under one of the following conditions: + +1. If you are certain that you changed your default admin password when you enabled Grafana +1. If you run GitLab in a private network, accessed only by trusted users, and your Grafana login page has not been exposed to the internet + +If you require access to your old Grafana data but do not meet one of these criteria, you may consider reinstating it temporarily, [exporting the dashboards](https://grafana.com/docs/reference/export_import/#exporting-a-dashboard) you need, then refreshing the data and [re-importing your dashboards](https://grafana.com/docs/reference/export_import/#importing-a-dashboard). Note that this poses a temporary vulnerability while your old Grafana data is in use, and the decision to do so should be weighed carefully with your need to access existing data and dashboards. + +For more information and further mitigation details, please refer to our [blog post on the security release](https://about.gitlab.com/2019/08/12/critical-security-release-gitlab-12-dot-1-dot-6-released/). + --- Read more on: diff --git a/doc/administration/monitoring/performance/request_profiling.md b/doc/administration/monitoring/performance/request_profiling.md index 9f671e0db11..c32edb60f9d 100644 --- a/doc/administration/monitoring/performance/request_profiling.md +++ b/doc/administration/monitoring/performance/request_profiling.md @@ -5,7 +5,7 @@ 1. Grab the profiling token from **Monitoring > Requests Profiles** admin page (highlighted in a blue in the image below). ![Profile token](img/request_profiling_token.png) -1. Pass the header `X-Profile-Token: <token>` and `X-Profile-Mode: <mode>`(where <mode> can be `execution` or `memory`) to the request you want to profile. You can use: +1. Pass the header `X-Profile-Token: <token>` and `X-Profile-Mode: <mode>`(where `<mode>` can be `execution` or `memory`) to the request you want to profile. You can use: - Browser extensions. For example, [ModHeader](https://chrome.google.com/webstore/detail/modheader/idgpnmonknjnojddfkpgkljpfnnfcklj) Chrome extension. - `curl`. For example, `curl --header 'X-Profile-Token: <token>' --header 'X-Profile-Mode: <mode>' https://gitlab.example.com/group/project`. 1. Once request is finished (which will take a little longer than usual), you can diff --git a/doc/administration/monitoring/prometheus/gitlab_metrics.md b/doc/administration/monitoring/prometheus/gitlab_metrics.md index 054fa547704..0605fb76e2f 100644 --- a/doc/administration/monitoring/prometheus/gitlab_metrics.md +++ b/doc/administration/monitoring/prometheus/gitlab_metrics.md @@ -19,38 +19,106 @@ it, the client IP needs to be [included in a whitelist][whitelist]. For Omnibus and Chart installations, these metrics are automatically enabled and collected as of [GitLab 9.4](https://gitlab.com/gitlab-org/omnibus-gitlab/merge_requests/1702). For source installations or earlier versions, these metrics will need to be enabled manually and collected by a Prometheus server. -## Unicorn Metrics available +## Metrics available The following metrics are available: -| Metric | Type | Since | Description | -|:--------------------------------- |:--------- |:----- |:----------- | -| db_ping_timeout | Gauge | 9.4 | Whether or not the last database ping timed out | -| db_ping_success | Gauge | 9.4 | Whether or not the last database ping succeeded | -| db_ping_latency_seconds | Gauge | 9.4 | Round trip time of the database ping | -| filesystem_access_latency_seconds | Gauge | 9.4 | Latency in accessing a specific filesystem | -| filesystem_accessible | Gauge | 9.4 | Whether or not a specific filesystem is accessible | -| filesystem_write_latency_seconds | Gauge | 9.4 | Write latency of a specific filesystem | -| filesystem_writable | Gauge | 9.4 | Whether or not the filesystem is writable | -| filesystem_read_latency_seconds | Gauge | 9.4 | Read latency of a specific filesystem | -| filesystem_readable | Gauge | 9.4 | Whether or not the filesystem is readable | -| gitlab_cache_misses_total | Counter | 10.2 | Cache read miss | -| gitlab_cache_operation_duration_seconds | Histogram | 10.2 | Cache access time | -| gitlab_cache_operations_total | Counter | 12.2 | Cache operations by controller/action | -| http_requests_total | Counter | 9.4 | Rack request count | -| http_request_duration_seconds | Histogram | 9.4 | HTTP response time from rack middleware | -| pipelines_created_total | Counter | 9.4 | Counter of pipelines created | -| rack_uncaught_errors_total | Counter | 9.4 | Rack connections handling uncaught errors count | -| redis_ping_timeout | Gauge | 9.4 | Whether or not the last redis ping timed out | -| redis_ping_success | Gauge | 9.4 | Whether or not the last redis ping succeeded | -| redis_ping_latency_seconds | Gauge | 9.4 | Round trip time of the redis ping | -| user_session_logins_total | Counter | 9.4 | Counter of how many users have logged in | -| upload_file_does_not_exist | Counter | 10.7 in EE, 11.5 in CE | Number of times an upload record could not find its file | -| failed_login_captcha_total | Gauge | 11.0 | Counter of failed CAPTCHA attempts during login | -| successful_login_captcha_total | Gauge | 11.0 | Counter of successful CAPTCHA attempts during login | -| unicorn_active_connections | Gauge | 11.0 | The number of active Unicorn connections (workers) | -| unicorn_queued_connections | Gauge | 11.0 | The number of queued Unicorn connections | -| unicorn_workers | Gauge | 12.0 | The number of Unicorn workers | +| Metric | Type | Since | Description | Labels | +|:-------------------------------------------------------------|:----------|-----------------------:|:----------------------------------------------------------------------------------------------------|:----------------------------------------------------| +| gitlab_banzai_cached_render_real_duration_seconds | Histogram | 9.4 | Duration of rendering markdown into HTML when cached output exists | controller, action | +| gitlab_banzai_cacheless_render_real_duration_seconds | Histogram | 9.4 | Duration of rendering markdown into HTML when cached outupt does not exist | controller, action | +| gitlab_cache_misses_total | Counter | 10.2 | Cache read miss | controller, action | +| gitlab_cache_operation_duration_seconds | Histogram | 10.2 | Cache access time | | +| gitlab_cache_operations_total | Counter | 12.2 | Cache operations by controller/action | controller, action, operation | +| gitlab_database_transaction_seconds | Histogram | 12.1 | Time spent in database transactions, in seconds | | +| gitlab_method_call_duration_seconds | Histogram | 10.2 | Method calls real duration | controller, action, module, method | +| gitlab_rails_queue_duration_seconds | Histogram | 9.4 | Measures latency between gitlab-workhorse forwarding a request to Rails | | +| gitlab_sql_duration_seconds | Histogram | 10.2 | SQL execution time, excluding SCHEMA operations and BEGIN / COMMIT | | +| gitlab_transaction_allocated_memory_bytes | Histogram | 10.2 | Allocated memory for all transactions (gitlab_transaction_* metrics) | | +| gitlab_transaction_cache_<key>_count_total | Counter | 10.2 | Counter for total Rails cache calls (per key) | | +| gitlab_transaction_cache_<key>_duration_total | Counter | 10.2 | Counter for total time (seconds) spent in Rails cache calls (per key) | | +| gitlab_transaction_cache_count_total | Counter | 10.2 | Counter for total Rails cache calls (aggregate) | | +| gitlab_transaction_cache_duration_total | Counter | 10.2 | Counter for total time (seconds) spent in Rails cache calls (aggregate) | | +| gitlab_transaction_cache_read_hit_count_total | Counter | 10.2 | Counter for cache hits for Rails cache calls | controller, action | +| gitlab_transaction_cache_read_miss_count_total | Counter | 10.2 | Counter for cache misses for Rails cache calls | controller, action | +| gitlab_transaction_duration_seconds | Histogram | 10.2 | Duration for all transactions (gitlab_transaction_* metrics) | controller, action | +| gitlab_transaction_event_build_found_total | Counter | 9.4 | Counter for build found for api /jobs/request | | +| gitlab_transaction_event_build_invalid_total | Counter | 9.4 | Counter for build invalid due to concurrency conflict for api /jobs/request | | +| gitlab_transaction_event_build_not_found_cached_total | Counter | 9.4 | Counter for cached response of build not found for api /jobs/request | | +| gitlab_transaction_event_build_not_found_total | Counter | 9.4 | Counter for build not found for api /jobs/request | | +| gitlab_transaction_event_change_default_branch_total | Counter | 9.4 | Counter when default branch is changed for any repository | | +| gitlab_transaction_event_create_repository_total | Counter | 9.4 | Counter when any repository is created | | +| gitlab_transaction_event_etag_caching_cache_hit_total | Counter | 9.4 | Counter for etag cache hit. | endpoint | +| gitlab_transaction_event_etag_caching_header_missing_total | Counter | 9.4 | Counter for etag cache miss - header missing | endpoint | +| gitlab_transaction_event_etag_caching_key_not_found_total | Counter | 9.4 | Counter for etag cache miss - key not found | endpoint | +| gitlab_transaction_event_etag_caching_middleware_used_total | Counter | 9.4 | Counter for etag middleware accessed | endpoint | +| gitlab_transaction_event_etag_caching_resource_changed_total | Counter | 9.4 | Counter for etag cache miss - resource changed | endpoint | +| gitlab_transaction_event_fork_repository_total | Counter | 9.4 | Counter for repository forks (RepositoryForkWorker). Only incremented when source repository exists | | +| gitlab_transaction_event_import_repository_total | Counter | 9.4 | Counter for repository imports (RepositoryImportWorker) | | +| gitlab_transaction_event_push_branch_total | Counter | 9.4 | Counter for all branch pushes | | +| gitlab_transaction_event_push_commit_total | Counter | 9.4 | Counter for commits | branch | +| gitlab_transaction_event_push_tag_total | Counter | 9.4 | Counter for tag pushes | | +| gitlab_transaction_event_rails_exception_total | Counter | 9.4 | Counter for number of rails exceptions | | +| gitlab_transaction_event_receive_email_total | Counter | 9.4 | Counter for recieved emails | handler | +| gitlab_transaction_event_remote_mirrors_failed_total | Counter | 10.8 | Counter for failed remote mirrors | | +| gitlab_transaction_event_remote_mirrors_finished_total | Counter | 10.8 | Counter for finished remote mirrors | | +| gitlab_transaction_event_remote_mirrors_running_total | Counter | 10.8 | Counter for running remote mirrors | | +| gitlab_transaction_event_remove_branch_total | Counter | 9.4 | Counter when a branch is removed for any repository | | +| gitlab_transaction_event_remove_repository_total | Counter | 9.4 | Counter when a repository is removed | | +| gitlab_transaction_event_remove_tag_total | Counter | 9.4 | Counter when a tag is remove for any repository | | +| gitlab_transaction_event_sidekiq_exception_total | Counter | 9.4 | Counter of sidekiq exceptions | | +| gitlab_transaction_event_stuck_import_jobs_total | Counter | 9.4 | Count of stuck import jobs | projects_without_jid_count, projects_with_jid_count | +| gitlab_transaction_event_update_build_total | Counter | 9.4 | Counter for update build for api /jobs/request/:id | | +| gitlab_transaction_new_redis_connections_total | Counter | 9.4 | Counter for new redis connections | | +| gitlab_transaction_queue_duration_total | Counter | 9.4 | Duration jobs were enqueued before processing | | +| gitlab_transaction_rails_queue_duration_total | Counter | 9.4 | Measures latency between gitlab-workhorse forwarding a request to Rails | controller, action | +| gitlab_transaction_view_duration_total | Counter | 9.4 | Duration for views | controller, action, view | +| gitlab_view_rendering_duration_seconds | Histogram | 10.2 | Duration for views (histogram) | controller, action, view | +| http_requests_total | Counter | 9.4 | Rack request count | method | +| http_request_duration_seconds | Histogram | 9.4 | HTTP response time from rack middleware | method, status | +| pipelines_created_total | Counter | 9.4 | Counter of pipelines created | | +| rack_uncaught_errors_total | Counter | 9.4 | Rack connections handling uncaught errors count | | +| user_session_logins_total | Counter | 9.4 | Counter of how many users have logged in | | +| upload_file_does_not_exist | Counter | 10.7 in EE, 11.5 in CE | Number of times an upload record could not find its file | | +| failed_login_captcha_total | Gauge | 11.0 | Counter of failed CAPTCHA attempts during login | | +| successful_login_captcha_total | Gauge | 11.0 | Counter of successful CAPTCHA attempts during login | | + +## Metrics controlled by a feature flag + +The following metrics can be controlled by feature flags: + +| Metric | Feature Flag | +|:-------------------------------------------------------------|:-----------------------------------------------------------------| +| gitlab_method_call_duration_seconds | prometheus_metrics_method_instrumentation | +| gitlab_transaction_allocated_memory_bytes | prometheus_metrics_transaction_allocated_memory | +| gitlab_transaction_event_build_found_total | prometheus_transaction_event_build_found_total | +| gitlab_transaction_event_build_invalid_total | prometheus_transaction_event_build_invalid_total | +| gitlab_transaction_event_build_not_found_cached_total | prometheus_transaction_event_build_not_found_cached_total | +| gitlab_transaction_event_build_not_found_total | prometheus_transaction_event_build_not_found_total | +| gitlab_transaction_event_change_default_branch_total | prometheus_transaction_event_change_default_branch_total | +| gitlab_transaction_event_create_repository_total | prometheus_transaction_event_create_repository_total | +| gitlab_transaction_event_etag_caching_cache_hit_total | prometheus_transaction_event_etag_caching_cache_hit_total | +| gitlab_transaction_event_etag_caching_header_missing_total | prometheus_transaction_event_etag_caching_header_missing_total | +| gitlab_transaction_event_etag_caching_key_not_found_total | prometheus_transaction_event_etag_caching_key_not_found_total | +| gitlab_transaction_event_etag_caching_middleware_used_total | prometheus_transaction_event_etag_caching_middleware_used_total | +| gitlab_transaction_event_etag_caching_resource_changed_total | prometheus_transaction_event_etag_caching_resource_changed_total | +| gitlab_transaction_event_fork_repository_total | prometheus_transaction_event_fork_repository_total | +| gitlab_transaction_event_import_repository_total | prometheus_transaction_event_import_repository_total | +| gitlab_transaction_event_push_branch_total | prometheus_transaction_event_push_branch_total | +| gitlab_transaction_event_push_commit_total | prometheus_transaction_event_push_commit_total | +| gitlab_transaction_event_push_tag_total | prometheus_transaction_event_push_tag_total | +| gitlab_transaction_event_rails_exception_total | prometheus_transaction_event_rails_exception_total | +| gitlab_transaction_event_receive_email_total | prometheus_transaction_event_receive_email_total | +| gitlab_transaction_event_remote_mirrors_failed_total | prometheus_transaction_event_remote_mirrors_failed_total | +| gitlab_transaction_event_remote_mirrors_finished_total | prometheus_transaction_event_remote_mirrors_finished_total | +| gitlab_transaction_event_remote_mirrors_running_total | prometheus_transaction_event_remote_mirrors_running_total | +| gitlab_transaction_event_remove_branch_total | prometheus_transaction_event_remove_branch_total | +| gitlab_transaction_event_remove_repository_total | prometheus_transaction_event_remove_repository_total | +| gitlab_transaction_event_remove_tag_total | prometheus_transaction_event_remove_tag_total | +| gitlab_transaction_event_sidekiq_exception_total | prometheus_transaction_event_sidekiq_exception_total | +| gitlab_transaction_event_stuck_import_jobs_total | prometheus_transaction_event_stuck_import_jobs_total | +| gitlab_transaction_event_update_build_total | prometheus_transaction_event_update_build_total | +| gitlab_view_rendering_duration_seconds | prometheus_metrics_view_instrumentation | ## Sidekiq Metrics available for Geo **(PREMIUM)** @@ -99,17 +167,27 @@ Some basic Ruby runtime metrics are available: | Metric | Type | Since | Description | |:-------------------------------------- |:--------- |:----- |:----------- | -| ruby_gc_duration_seconds_total | Counter | 11.1 | Time spent by Ruby in GC | +| ruby_gc_duration_seconds | Counter | 11.1 | Time spent by Ruby in GC | | ruby_gc_stat_... | Gauge | 11.1 | Various metrics from [GC.stat] | | ruby_file_descriptors | Gauge | 11.1 | File descriptors per process | | ruby_memory_bytes | Gauge | 11.1 | Memory usage by process | -| ruby_sampler_duration_seconds_total | Counter | 11.1 | Time spent collecting stats | +| ruby_sampler_duration_seconds | Counter | 11.1 | Time spent collecting stats | | ruby_process_cpu_seconds_total | Gauge | 12.0 | Total amount of CPU time per process | | ruby_process_max_fds | Gauge | 12.0 | Maximum number of open file descriptors per process | | ruby_process_resident_memory_bytes | Gauge | 12.0 | Memory usage by process, measured in bytes | | ruby_process_start_time_seconds | Gauge | 12.0 | UNIX timestamp of process start time | -[GC.stat]: https://ruby-doc.org/core-2.3.0/GC.html#method-c-stat +[GC.stat]: https://ruby-doc.org/core-2.6.3/GC.html#method-c-stat + +## Unicorn Metrics + +Unicorn specific metrics, when Unicorn is used. + +| Metric | Type | Since | Description | +|:---------------------------|:------|:------|:---------------------------------------------------| +| unicorn_active_connections | Gauge | 11.0 | The number of active Unicorn connections (workers) | +| unicorn_queued_connections | Gauge | 11.0 | The number of queued Unicorn connections | +| unicorn_workers | Gauge | 12.0 | The number of Unicorn workers | ## Puma Metrics **(EXPERIMENTAL)** @@ -120,14 +198,12 @@ When Puma is used instead of Unicorn, following metrics are available: | puma_workers | Gauge | 12.0 | Total number of workers | | puma_running_workers | Gauge | 12.0 | Number of booted workers | | puma_stale_workers | Gauge | 12.0 | Number of old workers | -| puma_phase | Gauge | 12.0 | Phase number (increased during phased restarts) | | puma_running | Gauge | 12.0 | Number of running threads | | puma_queued_connections | Gauge | 12.0 | Number of connections in that worker's "todo" set waiting for a worker thread | | puma_active_connections | Gauge | 12.0 | Number of threads processing a request | | puma_pool_capacity | Gauge | 12.0 | Number of requests the worker is capable of taking right now | | puma_max_threads | Gauge | 12.0 | Maximum number of worker threads | | puma_idle_threads | Gauge | 12.0 | Number of spawned threads which are not processing a request | -| rack_state_total | Gauge | 12.0 | Number of requests in a given rack state | | puma_killer_terminations_total | Gauge | 12.0 | Number of workers terminated by PumaWorkerKiller | ## Metrics shared directory diff --git a/doc/administration/operations/unicorn.md b/doc/administration/operations/unicorn.md index ae67d7f08d6..8178cb243f3 100644 --- a/doc/administration/operations/unicorn.md +++ b/doc/administration/operations/unicorn.md @@ -69,7 +69,7 @@ unicorn['worker_memory_limit_min'] = "400 * 1 << 20" unicorn['worker_memory_limit_max'] = "650 * 1 << 20" ``` -Otherwise, you can set the `GITLAB_UNICORN_MEMORY_MIN` and `GITLAB_UNICORN_MEMORY_MIN` +Otherwise, you can set the `GITLAB_UNICORN_MEMORY_MIN` and `GITLAB_UNICORN_MEMORY_MAX` [environment variables](../environment_variables.md). This is what a Unicorn worker memory restart looks like in unicorn_stderr.log. diff --git a/doc/administration/packages.md b/doc/administration/packages.md index c0f8777a8c0..1628b6d6f91 100644 --- a/doc/administration/packages.md +++ b/doc/administration/packages.md @@ -55,7 +55,7 @@ local location or even use object storage. The packages for Omnibus GitLab installations are stored under `/var/opt/gitlab/gitlab-rails/shared/packages/` and for source -installations under `shared/packages/` (relative to the git homedir). +installations under `shared/packages/` (relative to the Git homedir). To change the local storage path: **Omnibus GitLab installations** diff --git a/doc/administration/plugins.md b/doc/administration/plugins.md index 4cf3c607dae..92a4d56ca63 100644 --- a/doc/administration/plugins.md +++ b/doc/administration/plugins.md @@ -39,7 +39,7 @@ Follow the steps below to set up a custom hook: 1. Inside the `plugins` directory, create a file with a name of your choice, without spaces or special characters. -1. Make the hook file executable and make sure it's owned by the git user. +1. Make the hook file executable and make sure it's owned by the Git user. 1. Write the code to make the plugin function as expected. That can be in any language, and ensure the 'shebang' at the top properly reflects the language type. For example, if the script is in Ruby the shebang will @@ -112,4 +112,4 @@ Validating plugins from /plugins directory [system hooks]: ../system_hooks/system_hooks.md [webhooks]: ../user/project/integrations/webhooks.md -[highly available]: ./high_availability/README.md
\ No newline at end of file +[highly available]: ./high_availability/README.md diff --git a/doc/administration/raketasks/uploads/migrate.md b/doc/administration/raketasks/uploads/migrate.md index fd8ea8d3162..86e8b763f51 100644 --- a/doc/administration/raketasks/uploads/migrate.md +++ b/doc/administration/raketasks/uploads/migrate.md @@ -103,3 +103,13 @@ sudo -u git -H bundle exec rake "gitlab:uploads:migrate[NamespaceFileUploader, S sudo -u git -H bundle exec rake "gitlab:uploads:migrate[FileUploader, MergeRequest]" ``` + +## Migrate legacy uploads out of deprecated paths + +> Introduced in GitLab 12.3. + +To migrate all uploads created by legacy uploaders, run: + +```shell +bundle exec rake gitlab:uploads:legacy:migrate +``` diff --git a/doc/administration/reply_by_email_postfix_setup.md b/doc/administration/reply_by_email_postfix_setup.md index 406f7e8a034..56cd23b2eb8 100644 --- a/doc/administration/reply_by_email_postfix_setup.md +++ b/doc/administration/reply_by_email_postfix_setup.md @@ -18,7 +18,7 @@ The instructions make the assumption that you will be using the email address `i sudo apt-get install postfix ``` - When asked about the environment, select 'Internet Site'. When asked to confirm the hostname, make sure it matches gitlab.example.com`. + When asked about the environment, select 'Internet Site'. When asked to confirm the hostname, make sure it matches `gitlab.example.com`. 1. Install the `mailutils` package. @@ -331,7 +331,7 @@ Courier, which we will install later to add IMAP authentication, requires mailbo a logout ``` -## Done! +## Done If all the tests were successful, Postfix is all set up and ready to receive email! Continue with the [incoming email] guide to configure GitLab. diff --git a/doc/administration/repository_checks.md b/doc/administration/repository_checks.md index 05a7cb006a3..ab911c1cf0e 100644 --- a/doc/administration/repository_checks.md +++ b/doc/administration/repository_checks.md @@ -3,7 +3,7 @@ > [Introduced][ce-3232] in GitLab 8.7. It is OFF by default because it still causes too many false alarms. -Git has a built-in mechanism, [git fsck][git-fsck], to verify the +Git has a built-in mechanism, [`git fsck`][git-fsck], to verify the integrity of all data committed to a repository. GitLab administrators can trigger such a check for a project via the project page under the admin panel. The checks run asynchronously so it may take a few minutes diff --git a/doc/administration/repository_storage_types.md b/doc/administration/repository_storage_types.md index 1669f8b128c..142bcc65561 100644 --- a/doc/administration/repository_storage_types.md +++ b/doc/administration/repository_storage_types.md @@ -118,7 +118,7 @@ If you do have any existing integration, you may want to do a small rollout firs to validate. You can do so by specifying a range with the operation. This is an example of how to limit the rollout to Project IDs 50 to 100, running in -an Omnibus Gitlab installation: +an Omnibus GitLab installation: ```bash sudo gitlab-rake gitlab:storage:migrate_to_hashed ID_FROM=50 ID_TO=100 @@ -139,7 +139,7 @@ To schedule a complete rollback, see the [rake task documentation for storage rollback](raketasks/storage.md#rollback-from-hashed-storage-to-legacy-storage) for instructions. The rollback task also supports specifying a range of Project IDs. Here is an example -of limiting the rollout to Project IDs 50 to 100, in an Omnibus Gitlab installation: +of limiting the rollout to Project IDs 50 to 100, in an Omnibus GitLab installation: ```bash sudo gitlab-rake gitlab:storage:rollback_to_legacy ID_FROM=50 ID_TO=100 @@ -185,7 +185,7 @@ CI Artifacts are S3 compatible since **9.4** (GitLab Premium), and available in ##### LFS Objects -LFS Objects implements a similar storage pattern using 2 chars, 2 level folders, following git own implementation: +LFS Objects implements a similar storage pattern using 2 chars, 2 level folders, following Git own implementation: ```ruby "shared/lfs-objects/#{oid[0..1}/#{oid[2..3]}/#{oid[4..-1]}" diff --git a/doc/administration/smime_signing_email.md b/doc/administration/smime_signing_email.md new file mode 100644 index 00000000000..9f719088f25 --- /dev/null +++ b/doc/administration/smime_signing_email.md @@ -0,0 +1,49 @@ +# Signing outgoing email with S/MIME + +Notification emails sent by Gitlab can be signed with S/MIME for improved +security. + +> **Note:** +Please be aware that S/MIME certificates and TLS/SSL certificates are not the +same and are used for different purposes: TLS creates a secure channel, whereas +S/MIME signs and/or encrypts the message itself + +## Enable S/MIME signing + +This setting must be explicitly enabled and a single pair of key and certificate +files must be provided in `gitlab.rb` or `gitlab.yml` if you are using Omnibus +GitLab or installed GitLab from source respectively: + +```yaml +email_smime: + enabled: true + key_file: /etc/pki/smime/private/gitlab.key + cert_file: /etc/pki/smime/certs/gitlab.crt +``` + +- Both files must be provided PEM-encoded. +- The key file must be unencrypted so that Gitlab can read it without user + intervention. + +NOTE: **Note:** Be mindful of the access levels for your private keys and visibility to +third parties. + +### How to convert S/MIME PKCS#12 / PFX format to PEM encoding + +Typically S/MIME certificates are handled in binary PKCS#12 format (`.pfx` or `.p12` +extensions), which contain the following in a single encrypted file: + +- Server certificate +- Intermediate certificates (if any) +- Private key + +In order to export the required files in PEM encoding from the PKCS#12 file, +the `openssl` command can be used: + +```bash +#-- Extract private key in PEM encoding (no password, unencrypted) +$ openssl pkcs12 -in gitlab.p12 -nocerts -nodes -out gitlab.key + +#-- Extract certificates in PEM encoding (full certs chain including CA) +$ openssl pkcs12 -in gitlab.p12 -nokeys -out gitlab.crt +``` diff --git a/doc/administration/troubleshooting/kubernetes_cheat_sheet.md b/doc/administration/troubleshooting/kubernetes_cheat_sheet.md index 95cdb1508fa..260af333e8e 100644 --- a/doc/administration/troubleshooting/kubernetes_cheat_sheet.md +++ b/doc/administration/troubleshooting/kubernetes_cheat_sheet.md @@ -29,7 +29,7 @@ and they will assist you with any issues you are having. ```bash # for minikube: minikube dashboard —url - # for non-local installations if access via kubectl is configured: + # for non-local installations if access via Kubectl is configured: kubectl proxy ``` @@ -49,7 +49,7 @@ and they will assist you with any issues you are having. - What to do with pods in `CrashLoopBackoff` status: - Check logs via Kubernetes dashboard. - - Check logs via `kubectl`: + - Check logs via Kubectl: ```bash kubectl logs <unicorn pod> -c dependencies @@ -72,7 +72,7 @@ and they will assist you with any issues you are having. This is the principle of Kubernetes, read [Twelve-factor app](https://12factor.net/) for details. -## Gitlab-specific kubernetes information +## GitLab-specific kubernetes information - Minimal config that can be used to test a Kubernetes helm chart can be found [here](https://gitlab.com/charts/gitlab/issues/620). @@ -83,12 +83,22 @@ and they will assist you with any issues you are having. kubectl logs gitlab-unicorn-7656fdd6bf-jqzfs -c unicorn ``` -- It is not possible to get all the logs via `kubectl` at once, like with `gitlab-ctl tail`, - but a number of third-party tools can be used to do it: +- Tail and follow all pods that share a label (in this case, `unicorn`): - - [Kubetail](https://github.com/johanhaleby/kubetail) - - [kail: kubernetes tail](https://github.com/boz/kail) - - [stern](https://github.com/wercker/stern) + ```bash + # all containers in the unicorn pods + kubectl logs -f -l app=unicorn --all-containers=true --max-log-requests=50 + + # only the unicorn containers in all unicorn pods + kubectl logs -f -l app=unicorn -c unicorn --max-log-requests=50 + ``` + +- One can stream logs from all containers at once, similar to the Omnibus + command `gitlab-ctl tail`: + + ```bash + kubectl logs -f -l release=gitlab --all-containers=true --max-log-requests=100 + ``` - Check all events in the `gitlab` namespace (the namespace name can be different if you specified a different one when deploying the helm chart): @@ -131,7 +141,7 @@ and they will assist you with any issues you are having. - Check the output of `kubectl get events -w --all-namespaces`. - Check the logs of pods within `gitlab-managed-apps` namespace. - On the side of GitLab check sidekiq log and kubernetes log. When GitLab is installed - via helm chart, kubernetes.log can be found inside the sidekiq pod. + via Helm Chart, `kubernetes.log` can be found inside the sidekiq pod. - How to get your initial admin password <https://docs.gitlab.com/charts/installation/deployment.html#initial-login>: @@ -142,19 +152,19 @@ and they will assist you with any issues you are having. kubectl get secret <secret-name> -ojsonpath={.data.password} | base64 --decode ; echo ``` -- How to connect to a GitLab postgres database: +- How to connect to a GitLab Postgres database: ```bash kubectl exec -it <task-runner-pod-name> -- /srv/gitlab/bin/rails dbconsole -p ``` -- How to get info about helm installation status: +- How to get info about Helm installation status: ```bash helm status name-of-installation ``` -- How to update GitLab installed using helm chart: +- How to update GitLab installed using Helm Chart: ```bash helm repo upgrade @@ -179,25 +189,25 @@ and they will assist you with any issues you are having. helm upgrade <release name> <chart path> -f gitlab.yaml ``` -## Installation of minimal GitLab config via minukube on macOS +## Installation of minimal GitLab config via Minukube on macOS This section is based on [Developing for Kubernetes with Minikube](https://gitlab.com/charts/gitlab/blob/master/doc/minikube/index.md) and [Helm](https://gitlab.com/charts/gitlab/blob/master/doc/helm/index.md). Refer to those documents for details. -- Install kubectl via Homebrew: +- Install Kubectl via Homebrew: ```bash brew install kubernetes-cli ``` -- Install minikube via Homebrew: +- Install Minikube via Homebrew: ```bash brew cask install minikube ``` -- Start minikube and configure it. If minikube cannot start, try running `minikube delete && minikube start` +- Start Minikube and configure it. If Minikube cannot start, try running `minikube delete && minikube start` and repeat the steps: ```bash @@ -206,7 +216,7 @@ to those documents for details. minikube addons enable kube-dns ``` -- Install helm via Homebrew and initialize it: +- Install Helm via Homebrew and initialize it: ```bash brew install kubernetes-helm @@ -219,7 +229,7 @@ to those documents for details. - Find the IP address in the output of `minikube ip` and update the yaml file with this IP address. -- Install the GitLab helm chart: +- Install the GitLab Helm Chart: ```bash helm repo add gitlab https://charts.gitlab.io diff --git a/doc/administration/troubleshooting/sidekiq.md b/doc/administration/troubleshooting/sidekiq.md index 7067958ecb4..9b016c64e29 100644 --- a/doc/administration/troubleshooting/sidekiq.md +++ b/doc/administration/troubleshooting/sidekiq.md @@ -169,3 +169,121 @@ The PostgreSQL wiki has details on the query you can run to see blocking queries. The query is different based on PostgreSQL version. See [Lock Monitoring](https://wiki.postgresql.org/wiki/Lock_Monitoring) for the query details. + +## Managing Sidekiq queues + +It is possible to use [Sidekiq API](https://github.com/mperham/sidekiq/wiki/API) +to perform a number of troubleshoting on Sidekiq. + +These are the administrative commands and it should only be used if currently +admin interface is not suitable due to scale of installation. + +All this commands should be run using `gitlab-rails console`. + +### View the queue size + +```ruby +Sidekiq::Queue.new("pipeline_processing:build_queue").size +``` + +### Enumerate all enqueued jobs + +```ruby +queue = Sidekiq::Queue.new("chaos:chaos_sleep") +queue.each do |job| + # job.klass # => 'MyWorker' + # job.args # => [1, 2, 3] + # job.jid # => jid + # job.queue # => chaos:chaos_sleep + # job["retry"] # => 3 + # job.item # => { + # "class"=>"Chaos::SleepWorker", + # "args"=>[1000], + # "retry"=>3, + # "queue"=>"chaos:chaos_sleep", + # "backtrace"=>true, + # "queue_namespace"=>"chaos", + # "jid"=>"39bc482b823cceaf07213523", + # "created_at"=>1566317076.266069, + # "correlation_id"=>"c323b832-a857-4858-b695-672de6f0e1af", + # "enqueued_at"=>1566317076.26761}, + # } + + # job.delete if job.jid == 'abcdef1234567890' +end +``` + +### Enumerate currently running jobs + +```ruby +workers = Sidekiq::Workers.new +workers.each do |process_id, thread_id, work| + # process_id is a unique identifier per Sidekiq process + # thread_id is a unique identifier per thread + # work is a Hash which looks like: + # {"queue"=>"chaos:chaos_sleep", + # "payload"=> + # { "class"=>"Chaos::SleepWorker", + # "args"=>[1000], + # "retry"=>3, + # "queue"=>"chaos:chaos_sleep", + # "backtrace"=>true, + # "queue_namespace"=>"chaos", + # "jid"=>"b2a31e3eac7b1a99ff235869", + # "created_at"=>1566316974.9215662, + # "correlation_id"=>"e484fb26-7576-45f9-bf21-b99389e1c53c", + # "enqueued_at"=>1566316974.9229589}, + # "run_at"=>1566316974}], +end +``` + +### Remove sidekiq jobs for given parameters (destructive) + +```ruby +# for jobs like this: +# RepositoryImportWorker.new.perform_async(100) +id_list = [100] + +queue = Sidekiq::Queue.new('repository_import') +queue.each do |job| + job.delete if id_list.include?(job.args[0]) +end +``` + +### Remove specific job ID (destructive) + +```ruby +queue = Sidekiq::Queue.new('repository_import') +queue.each do |job| + job.delete if job.jid == 'my-job-id' +end +``` + +## Canceling running jobs (destructive) + +> Introduced in GitLab 12.3. + +This is highly risky operation and use it as last resort. +Doing that might result in data corruption, as the job +is interrupted mid-execution and it is not guaranteed +that proper rollback of transactions is implemented. + +```ruby +Gitlab::SidekiqMonitor.cancel_job('job-id') +``` + +> This requires the Sidekiq to be run with `SIDEKIQ_MONITOR_WORKER=1` +> environment variable. + +To perform of the interrupt we use `Thread.raise` which +has number of drawbacks, as mentioned in [Why Ruby’s Timeout is dangerous (and Thread.raise is terrifying)](https://jvns.ca/blog/2015/11/27/why-rubys-timeout-is-dangerous-and-thread-dot-raise-is-terrifying/): + +> This is where the implications get interesting, and terrifying. This means that an exception can get raised: +> +> * during a network request (ok, as long as the surrounding code is prepared to catch Timeout::Error) +> * during the cleanup for the network request +> * during a rescue block +> * while creating an object to save to the database afterwards +> * in any of your code, regardless of whether it could have possibly raised an exception before +> +> Nobody writes code to defend against an exception being raised on literally any line. That’s not even possible. So Thread.raise is basically like a sneak attack on your code that could result in almost anything. It would probably be okay if it were pure-functional code that did not modify any state. But this is Ruby, so that’s unlikely :) diff --git a/doc/administration/uploads.md b/doc/administration/uploads.md index 99f1c386183..a4bed72b965 100644 --- a/doc/administration/uploads.md +++ b/doc/administration/uploads.md @@ -57,7 +57,7 @@ This configuration relies on valid AWS credentials to be configured already. ## Object Storage Settings -For source installations the following settings are nested under `uploads:` and then `object_store:`. On omnibus installs they are prefixed by `uploads_object_store_`. +For source installations the following settings are nested under `uploads:` and then `object_store:`. On Omnibus GitLab installs they are prefixed by `uploads_object_store_`. | Setting | Description | Default | |---------|-------------|---------| @@ -78,7 +78,7 @@ The connection settings match those provided by [Fog](https://github.com/fog), a | `aws_access_key_id` | AWS credentials, or compatible | | | `aws_secret_access_key` | AWS credentials, or compatible | | | `aws_signature_version` | AWS signature version to use. 2 or 4 are valid options. Digital Ocean Spaces and other providers may need 2. | 4 | -| `enable_signature_v4_streaming` | Set to true to enable HTTP chunked transfers with AWS v4 signatures (https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-streaming.html). Oracle Cloud S3 needs this to be false | true +| `enable_signature_v4_streaming` | Set to true to enable HTTP chunked transfers with [AWS v4 signatures](https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-streaming.html). Oracle Cloud S3 needs this to be false | true | | `region` | AWS region | us-east-1 | | `host` | S3 compatible host for when not using AWS, e.g. `localhost` or `storage.example.com` | s3.amazonaws.com | | `endpoint` | Can be used when configuring an S3 compatible service such as [Minio](https://www.minio.io), by entering a URL such as `http://127.0.0.1:9000` | (optional) | @@ -220,7 +220,7 @@ _The uploads are stored by default in openstack_temp_url_key: OPENSTACK_TEMP_URL_KEY openstack_auth_url: 'https://auth.cloud.ovh.net/v2.0/' openstack_region: DE1 - openstack_tenant: 'TENANT_ID' + openstack_tenant: 'TENANT_ID' ``` 1. Save the file and [reconfigure GitLab][] for the changes to take effect. diff --git a/doc/api/README.md b/doc/api/README.md index b7ee710b87a..33394d46a2d 100644 --- a/doc/api/README.md +++ b/doc/api/README.md @@ -77,11 +77,12 @@ authentication is not provided. For those cases where it is not required, this will be mentioned in the documentation for each individual endpoint. For example, the [`/projects/:id` endpoint](projects.md). -There are three ways to authenticate with the GitLab API: +There are four ways to authenticate with the GitLab API: 1. [OAuth2 tokens](#oauth2-tokens) 1. [Personal access tokens](#personal-access-tokens) 1. [Session cookie](#session-cookie) +1. [GitLab CI job token](#gitlab-ci-job-token-premium) **(PREMIUM)** For admins who want to authenticate with the API as a specific user, or who want to build applications or scripts that do so, two options are available: @@ -151,6 +152,14 @@ The primary user of this authentication method is the web frontend of GitLab its which can use the API as the authenticated user to get a list of their projects, for example, without needing to explicitly pass an access token. +### GitLab CI job token **(PREMIUM)** + +With a few API endpoints you can use a [GitLab CI job token](../user/project/new_ci_build_permissions_model.md#job-token) +to authenticate with the API: + +- [Get job artifacts](jobs.md#get-job-artifacts) +- [Pipeline triggers](pipeline_triggers.md) + ### Impersonation tokens > [Introduced][ce-9099] in GitLab 9.0. Needs admin permissions. diff --git a/doc/api/api_resources.md b/doc/api/api_resources.md index b32f11464ef..9af5430f1c8 100644 --- a/doc/api/api_resources.md +++ b/doc/api/api_resources.md @@ -67,7 +67,7 @@ The following API resources are available in the project context: | [Search](search.md) | `/projects/:id/search` (also available for groups and standalone) | | [Services](services.md) | `/projects/:id/services` | | [Tags](tags.md) | `/projects/:id/repository/tags` | -| [Vulnerabilities](vulnerabilities.md) **(ULTIMATE)** | `/projects/:id/vulnerabilities` (also available for groups) | +| [Vulnerabilities](vulnerabilities.md) **(ULTIMATE)** | `/projects/:id/vulnerabilities` | [Wikis](wikis.md) | `/projects/:id/wikis` | ## Group resources diff --git a/doc/api/boards.md b/doc/api/boards.md index 08ec1d832df..b848d7788cd 100644 --- a/doc/api/boards.md +++ b/doc/api/boards.md @@ -466,7 +466,7 @@ Example response: ## Delete a board list -Only for admins and project owners. Soft deletes the board list in question. +Only for admins and project owners. Deletes the board list in question. ``` DELETE /projects/:id/boards/:board_id/lists/:list_id diff --git a/doc/api/dependencies.md b/doc/api/dependencies.md index 2496b038c7f..5296d4e316f 100644 --- a/doc/api/dependencies.md +++ b/doc/api/dependencies.md @@ -5,20 +5,21 @@ This API is in an alpha stage and considered unstable. The response payload may be subject to change or breakage across GitLab releases. -Every call to this endpoint requires authentication. To perform this call, user should be authorized to read -[Project Security Dashboard](../user/application_security/security_dashboard/index.md#project-security-dashboard). +Every call to this endpoint requires authentication. To perform this call, user should be authorized to read repository. +To see vulnerabilities in response, user should be authorized to read +[Project Security Dashboard](../user/application_security/security_dashboard/index.md#project-security-dashboard). ## List project dependencies -Get a list of project dependencies. This API partially mirroring -[Dependency List](../user/application_security/dependency_scanning/index.md#dependency-list) feature. +Get a list of project dependencies. This API partially mirroring +[Dependency List](../user/application_security/dependency_list/index.md) feature. This list can be generated only for [languages and package managers](../user/application_security/dependency_scanning/index.md#supported-languages-and-package-managers) -supported by Gemnasium. +supported by Gemnasium. ``` GET /projects/:id/dependencies -GET /projects/:id/vulnerabilities?package_manager=maven -GET /projects/:id/vulnerabilities?package_manager=yarn,bundler +GET /projects/:id/dependencies?package_manager=maven +GET /projects/:id/dependencies?package_manager=yarn,bundler ``` | Attribute | Type | Required | Description | @@ -38,13 +39,18 @@ Example response: "name": "rails", "version": "5.0.1", "package_manager": "bundler", - "dependency_file_path": "Gemfile.lock" + "dependency_file_path": "Gemfile.lock", + "vulnerabilities": [{ + "name": "DDoS", + "severity": "unknown" + }] }, { "name": "hanami", "version": "1.3.1", "package_manager": "bundler", - "dependency_file_path": "Gemfile.lock" + "dependency_file_path": "Gemfile.lock", + "vulnerabilities": [] } ] ``` diff --git a/doc/api/deploy_keys.md b/doc/api/deploy_keys.md index 94351e1a300..df8cf06fc4a 100644 --- a/doc/api/deploy_keys.md +++ b/doc/api/deploy_keys.md @@ -203,6 +203,7 @@ Example response: "created_at" : "2015-08-29T12:44:31.550Z" } ``` + ## Adding deploy keys to multiple projects If you want to easily add the same deploy key to multiple projects in the same diff --git a/doc/api/discussions.md b/doc/api/discussions.md index b4a2d0b15f6..12dbba78291 100644 --- a/doc/api/discussions.md +++ b/doc/api/discussions.md @@ -160,7 +160,7 @@ curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab Adds a new note to the thread. This can also [create a thread from a single comment](../user/discussions/#start-a-thread-by-replying-to-a-standard-comment). **WARNING** -Notes can be added to other items than comments (system notes, etc.) making them threads. +Notes can be added to other items than comments (system notes, etc.) making them threads. ``` POST /projects/:id/issues/:issue_iid/discussions/:discussion_id/notes diff --git a/doc/api/epics.md b/doc/api/epics.md index 3036b3c2364..87ae0c48199 100644 --- a/doc/api/epics.md +++ b/doc/api/epics.md @@ -65,6 +65,8 @@ Example response: "title": "Accusamus iste et ullam ratione voluptatem omnis debitis dolor est.", "description": "Molestias dolorem eos vitae expedita impedit necessitatibus quo voluptatum.", "state": "opened", + "web_edit_url": "http://localhost:3001/groups/test/-/epics/4", + "reference": "&4", "author": { "id": 10, "name": "Lu Mayer", @@ -118,6 +120,8 @@ Example response: "title": "Ea cupiditate dolores ut vero consequatur quasi veniam voluptatem et non.", "description": "Molestias dolorem eos vitae expedita impedit necessitatibus quo voluptatum.", "state": "opened", + "web_edit_url": "http://localhost:3001/groups/test/-/epics/5", + "reference": "&5", "author":{ "id": 7, "name": "Pamella Huel", @@ -182,6 +186,8 @@ Example response: "title": "Epic", "description": "Epic description", "state": "opened", + "web_edit_url": "http://localhost:3001/groups/test/-/epics/6", + "reference": "&6", "author": { "name" : "Alexandra Bashirian", "avatar_url" : null, @@ -247,6 +253,8 @@ Example response: "title": "New Title", "description": "Epic description", "state": "opened", + "web_edit_url": "http://localhost:3001/groups/test/-/epics/6", + "reference": "&6", "author": { "name" : "Alexandra Bashirian", "avatar_url" : null, diff --git a/doc/api/features.md b/doc/api/features.md index 6ecd4ec14b9..e8d0c7c942b 100644 --- a/doc/api/features.md +++ b/doc/api/features.md @@ -60,8 +60,8 @@ 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 | -| `group` | string | no | A GitLab group's path, for example 'gitlab-org' | -| `project` | string | no | A projects path, for example 'gitlab-org/gitlab-ce' | +| `group` | string | no | A GitLab group's path, for example `gitlab-org` | +| `project` | string | no | A projects path, for example `gitlab-org/gitlab-ce` | Note that you can enable or disable a feature for a `feature_group`, a `user`, a `group`, and a `project` in a single API call. diff --git a/doc/api/geo_nodes.md b/doc/api/geo_nodes.md index e3af5d60533..5eba7f038ed 100644 --- a/doc/api/geo_nodes.md +++ b/doc/api/geo_nodes.md @@ -10,7 +10,7 @@ GET /geo_nodes ``` ```bash -curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/geo_nodes +curl --header "PRIVATE-TOKEN: <your_access_token>" https://primary.example.com/api/v4/geo_nodes ``` Example response: @@ -27,8 +27,15 @@ Example response: "current": true, "files_max_capacity": 10, "repos_max_capacity": 25, + "container_repositories_max_capacity": 10, "verification_max_capacity": 100, - "clone_protocol": "http" + "clone_protocol": "http", + "web_edit_url": "https://primary.example.com/admin/geo/nodes/1/edit", + "_links": { + "self": "https://primary.example.com/api/v4/geo_nodes/1", + "status":"https://primary.example.com/api/v4/geo_nodes/1/status", + "repair":"https://primary.example.com/api/v4/geo_nodes/1/repair" + } }, { "id": 2, @@ -40,8 +47,17 @@ Example response: "current": false, "files_max_capacity": 10, "repos_max_capacity": 25, + "container_repositories_max_capacity": 10, "verification_max_capacity": 100, - "clone_protocol": "http" + "sync_object_storage": true, + "clone_protocol": "http", + "web_edit_url": "https://primary.example.com/admin/geo/nodes/2/edit", + "web_geo_projects_url": "https://secondary.example.com/admin/geo/projects", + "_links": { + "self":"https://primary.example.com/api/v4/geo_nodes/2", + "status":"https://primary.example.com/api/v4/geo_nodes/2/status", + "repair":"https://primary.example.com/api/v4/geo_nodes/2/repair" + } } ] ``` @@ -53,7 +69,7 @@ GET /geo_nodes/:id ``` ```bash -curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/geo_nodes/1 +curl --header "PRIVATE-TOKEN: <your_access_token>" https://primary.example.com/api/v4/geo_nodes/1 ``` Example response: @@ -69,8 +85,15 @@ Example response: "current": true, "files_max_capacity": 10, "repos_max_capacity": 25, + "container_repositories_max_capacity": 10, "verification_max_capacity": 100, - "clone_protocol": "http" + "clone_protocol": "http", + "web_edit_url": "https://primary.example.com/admin/geo/nodes/1/edit", + "_links": { + "self": "https://primary.example.com/api/v4/geo_nodes/1", + "status":"https://primary.example.com/api/v4/geo_nodes/1/status", + "repair":"https://primary.example.com/api/v4/geo_nodes/1/repair" + } } ``` @@ -84,16 +107,18 @@ _This can only be run against a primary Geo node._ PUT /geo_nodes/:id ``` -| Attribute | Type | Required | Description | -|----------------------|---------|-----------|---------------------------------------------------------------------------| -| `id` | integer | yes | The ID of the Geo node. | -| `enabled` | boolean | no | Flag indicating if the Geo node is enabled. | -| `name` | string | yes | The unique identifier for the Geo node. Must match `geo_node_name` if it is set in gitlab.rb, otherwise it must match `external_url`. | -| `url` | string | yes | The user-facing URL of the Geo node. | -| `internal_url` | string | no | The URL defined on the primary node that secondary nodes should use to contact it. Returns `url` if not set.| -| `files_max_capacity` | integer | no | Control the maximum concurrency of LFS/attachment backfill for this secondary node. | -| `repos_max_capacity` | integer | no | Control the maximum concurrency of repository backfill for this secondary node. | -| `verification_max_capacity` | integer | no | Control the maximum concurrency of verification for this node. | +| Attribute | Type | Required | Description | +|-----------------------------|---------|-----------|---------------------------------------------------------------------------| +| `id` | integer | yes | The ID of the Geo node. | +| `enabled` | boolean | no | Flag indicating if the Geo node is enabled. | +| `name` | string | yes | The unique identifier for the Geo node. Must match `geo_node_name` if it is set in `gitlab.rb`, otherwise it must match `external_url`. | +| `url` | string | yes | The user-facing URL of the Geo node. | +| `internal_url` | string | no | The URL defined on the primary node that secondary nodes should use to contact it. Returns `url` if not set.| +| `files_max_capacity` | integer | no | Control the maximum concurrency of LFS/attachment backfill for this secondary node. | +| `repos_max_capacity` | integer | no | Control the maximum concurrency of repository backfill for this secondary node. | +| `verification_max_capacity` | integer | no | Control the maximum concurrency of verification for this node. | +| `container_repositories_max_capacity` | integer | no | Control the maximum concurrency of container repository sync for this node. | +| `sync_object_storage` | boolean | no | Flag indicating if the secondary Geo node will replicate blobs in Object Storage. | Example response: @@ -108,8 +133,17 @@ Example response: "current": true, "files_max_capacity": 10, "repos_max_capacity": 25, + "container_repositories_max_capacity": 10, "verification_max_capacity": 100, - "clone_protocol": "http" + "sync_object_storage": true, + "clone_protocol": "http", + "web_edit_url": "https://primary.example.com/admin/geo/nodes/2/edit", + "web_geo_projects_url": "https://secondary.example.com/admin/geo/projects", + "_links": { + "self":"https://primary.example.com/api/v4/geo_nodes/2", + "status":"https://primary.example.com/api/v4/geo_nodes/2/status", + "repair":"https://primary.example.com/api/v4/geo_nodes/2/repair" + } } ``` @@ -151,8 +185,15 @@ Example response: "current": true, "files_max_capacity": 10, "repos_max_capacity": 25, + "container_repositories_max_capacity": 10, "verification_max_capacity": 100, - "clone_protocol": "http" + "clone_protocol": "http", + "web_edit_url": "https://primary.example.com/admin/geo/nodes/1/edit", + "_links": { + "self": "https://primary.example.com/api/v4/geo_nodes/1", + "status":"https://primary.example.com/api/v4/geo_nodes/1/status", + "repair":"https://primary.example.com/api/v4/geo_nodes/1/repair" + } } ``` @@ -163,7 +204,7 @@ GET /geo_nodes/status ``` ```bash -curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/geo_nodes/status +curl --header "PRIVATE-TOKEN: <your_access_token>" https://primary.example.com/api/v4/geo_nodes/status ``` Example response: @@ -314,7 +355,7 @@ GET /geo_nodes/:id/status ``` ```bash -curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/geo_nodes/2/status +curl --header "PRIVATE-TOKEN: <your_access_token>" https://primary.example.com/api/v4/geo_nodes/2/status ``` Example response: @@ -388,7 +429,7 @@ GET /geo_nodes/current/failures This endpoint uses [Pagination](README.md#pagination). ```bash -curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/geo_nodes/current/failures +curl --header "PRIVATE-TOKEN: <your_access_token>" https://primary.example.com/api/v4/geo_nodes/current/failures ``` Example response: diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 2d3bec4ff67..d99a4c37d72 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -109,6 +109,7 @@ The API can be explored interactively using the [GraphiQL IDE](../index.md#graph | `visibility` | String | | | `lfsEnabled` | Boolean | | | `requestAccessEnabled` | Boolean | | +| `rootStorageStatistics` | RootStorageStatistics | The aggregated storage statistics. Only available if the namespace has no parent | | `userPermissions` | GroupPermissions! | Permissions for the current user on the resource | | `webUrl` | String! | | | `avatarUrl` | String | | @@ -453,6 +454,17 @@ The API can be explored interactively using the [GraphiQL IDE](../index.md#graph | `exists` | Boolean! | | | `tree` | Tree | | +### RootStorageStatistics + +| Name | Type | Description | +| --- | ---- | ---------- | +| `storageSize` | Int! | The total storage in Bytes | +| `repositorySize` | Int! | The git repository size in Bytes | +| `lfsObjectsSize` | Int! | The LFS objects size in Bytes | +| `buildArtifactsSize` | Int! | The CI artifacts size in Bytes | +| `packagesSize` | Int! | The packages size in Bytes | +| `wikiSize` | Int! | The wiki size in Bytes | + ### Submodule | Name | Type | Description | diff --git a/doc/api/group_boards.md b/doc/api/group_boards.md index 4d10f83720b..99b522a7ae9 100644 --- a/doc/api/group_boards.md +++ b/doc/api/group_boards.md @@ -536,7 +536,7 @@ Example response: ## Delete a group issue board list -Only for admins and group owners. Soft deletes the board list in question. +Only for admins and group owners. Deletes the board list in question. ``` DELETE /groups/:id/boards/:board_id/lists/:list_id diff --git a/doc/api/issues.md b/doc/api/issues.md index 96a547551f1..8313dd2c3bd 100644 --- a/doc/api/issues.md +++ b/doc/api/issues.md @@ -49,7 +49,7 @@ GET /issues?confidential=true | `my_reaction_emoji` | string | no | Return issues reacted by the authenticated user by the given `emoji`. `None` returns issues not given a reaction. `Any` returns issues given at least one reaction. _([Introduced][ce-14016] in GitLab 10.0)_ | | `weight` **(STARTER)** | integer | no | Return issues with the specified `weight`. `None` returns issues with no weight assigned. `Any` returns issues with a weight assigned. | | `iids[]` | integer array | no | Return only the issues having the given `iid` | -| `order_by` | string | no | Return issues ordered by `created_at` or `updated_at` fields. Default is `created_at` | +| `order_by` | string | no | Return issues ordered by `created_at`, `updated_at`, `priority`, `due_date`, `relative_position`, `label_priority`, `milestone_due`, `popularity`, `weight` fields. Default is `created_at` | | `sort` | string | no | Return issues sorted in `asc` or `desc` order. Default is `desc` | | `search` | string | no | Search issues against their `title` and `description` | | `in` | string | no | Modify the scope of the `search` attribute. `title`, `description`, or a string joining them with comma. Default is `title,description` | @@ -136,7 +136,6 @@ Example response: "award_emoji":"http://example.com/api/v4/projects/1/issues/76/award_emoji", "project":"http://example.com/api/v4/projects/1" }, - "subscribed": false, "task_completion_status":{ "count":0, "completed_count":0 @@ -199,7 +198,7 @@ GET /groups/:id/issues?confidential=true | `assignee_username` | string array | no | Return issues assigned to the given `username`. Similar to `assignee_id` and mutually exclusive with `assignee_id`. In CE version `assignee_username` array should only contain a single value or an invalid param error will be returned otherwise. | | `my_reaction_emoji` | string | no | Return issues reacted by the authenticated user by the given `emoji`. `None` returns issues not given a reaction. `Any` returns issues given at least one reaction. _([Introduced][ce-14016] in GitLab 10.0)_ | | `weight` **(STARTER)** | integer | no | Return issues with the specified `weight`. `None` returns issues with no weight assigned. `Any` returns issues with a weight assigned. | -| `order_by` | string | no | Return issues ordered by `created_at` or `updated_at` fields. Default is `created_at` | +| `order_by` | string | no | Return issues ordered by `created_at`, `updated_at`, `priority`, `due_date`, `relative_position`, `label_priority`, `milestone_due`, `popularity`, `weight` fields. Default is `created_at` | | `sort` | string | no | Return issues sorted in `asc` or `desc` order. Default is `desc` | | `search` | string | no | Search group issues against their `title` and `description` | | `created_after` | datetime | no | Return issues created on or after the given time | @@ -348,7 +347,7 @@ GET /projects/:id/issues?confidential=true | `assignee_username` | string array | no | Return issues assigned to the given `username`. Similar to `assignee_id` and mutually exclusive with `assignee_id`. In CE version `assignee_username` array should only contain a single value or an invalid param error will be returned otherwise. | | `my_reaction_emoji` | string | no | Return issues reacted by the authenticated user by the given `emoji`. `None` returns issues not given a reaction. `Any` returns issues given at least one reaction. _([Introduced][ce-14016] in GitLab 10.0)_ | | `weight` **(STARTER)** | integer | no | Return issues with the specified `weight`. `None` returns issues with no weight assigned. `Any` returns issues with a weight assigned. | -| `order_by` | string | no | Return issues ordered by `created_at` or `updated_at` fields. Default is `created_at` | +| `order_by` | string | no | Return issues ordered by `created_at`, `updated_at`, `priority`, `due_date`, `relative_position`, `label_priority`, `milestone_due`, `popularity`, `weight` fields. Default is `created_at` | | `sort` | string | no | Return issues sorted in `asc` or `desc` order. Default is `desc` | | `search` | string | no | Search project issues against their `title` and `description` | | `created_after` | datetime | no | Return issues created on or after the given time | @@ -441,7 +440,6 @@ Example response: "award_emoji":"http://example.com/api/v4/projects/4/issues/41/award_emoji", "project":"http://example.com/api/v4/projects/4" }, - "subscribed": false, "task_completion_status":{ "count":0, "completed_count":0 @@ -790,7 +788,7 @@ the `weight` parameter: ## Delete an issue -Only for admins and project owners. Soft deletes the issue in question. +Only for admins and project owners. Deletes the issue in question. ``` DELETE /projects/:id/issues/:issue_iid diff --git a/doc/api/labels.md b/doc/api/labels.md index 5db0edcf14d..fde1d861cf6 100644 --- a/doc/api/labels.md +++ b/doc/api/labels.md @@ -137,8 +137,9 @@ DELETE /projects/:id/labels | 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 | -| `name` | string | yes | The name of the label | +| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | +| `label_id` | integer | yes (or `name`) | The id of the existing label | +| `name` | string | yes (or `label_id`) | The name of the existing label | ```bash curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/labels?name=bug" @@ -156,7 +157,8 @@ PUT /projects/:id/labels | 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 | -| `name` | string | yes | The name of the existing label | +| `label_id` | integer | yes (or `name`) | The id of the existing label | +| `name` | string | yes (or `label_id`) | The name of the existing label | | `new_name` | string | yes if `color` is not provided | The new name of the label | | `color` | string | yes if `new_name` is not provided | The color of the label given in 6-digit hex notation with leading '#' sign (e.g. #FFAABB) or one of the [CSS color names](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value#Color_keywords) | | `description` | string | no | The new description of the label | diff --git a/doc/api/lint.md b/doc/api/lint.md index 79f5e629c7f..dacd3f4c493 100644 --- a/doc/api/lint.md +++ b/doc/api/lint.md @@ -1,4 +1,4 @@ -# Validate the .gitlab-ci.yml (API) +# Validate the `.gitlab-ci.yml` (API) > [Introduced][ce-5953] in GitLab 8.12. @@ -10,7 +10,7 @@ POST /ci/lint | Attribute | Type | Required | Description | | ---------- | ------- | -------- | -------- | -| `content` | string | yes | the .gitlab-ci.yaml content| +| `content` | string | yes | the `.gitlab-ci.yaml` content| ```bash curl --header "Content-Type: application/json" https://gitlab.example.com/api/v4/ci/lint --data '{"content": "{ \"image\": \"ruby:2.6\", \"services\": [\"postgres\"], \"before_script\": [\"bundle install\", \"bundle exec rake db:create\"], \"variables\": {\"DB_NAME\": \"postgres\"}, \"types\": [\"test\", \"deploy\", \"notify\"], \"rspec\": { \"script\": \"rake spec\", \"tags\": [\"ruby\", \"postgres\"], \"only\": [\"branches\"]}}"}' diff --git a/doc/api/merge_request_approvals.md b/doc/api/merge_request_approvals.md index cc95689a65f..bf83bfd7e6a 100644 --- a/doc/api/merge_request_approvals.md +++ b/doc/api/merge_request_approvals.md @@ -23,36 +23,6 @@ GET /projects/:id/approvals ```json { - "approvers": [ - { - "user": { - "id": 5, - "name": "John Doe6", - "username": "user5", - "state":"active","avatar_url":"https://www.gravatar.com/avatar/4aea8cf834ed91844a2da4ff7ae6b491?s=80\u0026d=identicon","web_url":"http://localhost/user5" - } - } - ], - "approver_groups": [ - { - "group": { - "id": 1, - "name": "group1", - "path": "group1", - "description": "", - "visibility": "public", - "lfs_enabled": false, - "avatar_url": null, - "web_url": "http://localhost/groups/group1", - "request_access_enabled": false, - "full_name": "group1", - "full_path": "group1", - "parent_id": null, - "ldap_cn": null, - "ldap_access": null - } - } - ], "approvals_before_merge": 2, "reset_approvals_on_push": true, "disable_overriding_approvers_per_merge_request": false @@ -75,7 +45,7 @@ POST /projects/:id/approvals | Attribute | Type | Required | Description | | ------------------------------------------------ | ------- | -------- | --------------------------------------------------------------------------------------------------- | | `id` | integer | yes | The ID of a project | -| `approvals_before_merge` | integer | no | How many approvals are required before an MR can be merged | +| `approvals_before_merge` | integer | no | How many approvals are required before an MR can be merged. Deprecated in 12.0 in favor of Approval Rules API. | | `reset_approvals_on_push` | boolean | no | Reset approvals on a new push | | `disable_overriding_approvers_per_merge_request` | boolean | no | Allow/Disallow overriding approvers per MR | | `merge_requests_author_approval` | boolean | no | Allow/Disallow authors from self approving merge requests; `true` means authors cannot self approve | @@ -83,20 +53,68 @@ POST /projects/:id/approvals ```json { - "approvers": [ - { - "user": { + "approvals_before_merge": 2, + "reset_approvals_on_push": true, + "disable_overriding_approvers_per_merge_request": false, + "merge_requests_author_approval": false, + "merge_requests_disable_committers_approval": false +} +``` + +### Get project-level rules + +>**Note:** This API endpoint is only available on 12.3 Starter and above. + +You can request information about a project's approval rules using the following endpoint: + +``` +GET /projects/:id/approval_rules +``` + +**Parameters:** + +| Attribute | Type | Required | Description | +|----------------------|---------|----------|-----------------------------------------------------------| +| `id` | integer | yes | The ID of a project | + +```json +[ + { + "id": 1, + "name": "security", + "rule_type": "regular", + "eligible_approvers": [ + { "id": 5, - "name": "John Doe6", - "username": "user5", - "state":"active","avatar_url":"https://www.gravatar.com/avatar/4aea8cf834ed91844a2da4ff7ae6b491?s=80\u0026d=identicon","web_url":"http://localhost/user5" + "name": "John Doe", + "username": "jdoe", + "state": "active", + "avatar_url": "https://www.gravatar.com/avatar/0?s=80&d=identicon", + "web_url": "http://localhost/jdoe" + }, + { + "id": 50, + "name": "Group Member 1", + "username": "group_member_1", + "state": "active", + "avatar_url": "https://www.gravatar.com/avatar/0?s=80&d=identicon", + "web_url": "http://localhost/group_member_1" } - } - ], - "approver_groups": [ - { - "group": { - "id": 1, + ], + "approvals_required": 3, + "users": [ + { + "id": 5, + "name": "John Doe", + "username": "jdoe", + "state": "active", + "avatar_url": "https://www.gravatar.com/avatar/0?s=80&d=identicon", + "web_url": "http://localhost/jdoe" + } + ], + "groups": [ + { + "id": 5, "name": "group1", "path": "group1", "description": "", @@ -111,18 +129,187 @@ POST /projects/:id/approvals "ldap_cn": null, "ldap_access": null } + ], + "contains_hidden_groups": false + } +] +``` + +### Create project-level rule + +>**Note:** This API endpoint is only available on 12.3 Starter and above. + +You can create project approval rules using the following endpoint: + +``` +POST /projects/:id/approval_rules +``` + +**Parameters:** + +| Attribute | Type | Required | Description | +|----------------------|---------|----------|-----------------------------------------------------------| +| `id` | integer | yes | The ID of a project | +| `name` | string | yes | The name of the approval rule | +| `approvals_required` | integer | yes | The number of required approvals for this rule | +| `users` | integer | no | The ids of users as approvers | +| `groups` | integer | no | The ids of groups as approvers | + +```json +{ + "id": 1, + "name": "security", + "rule_type": "regular", + "eligible_approvers": [ + { + "id": 2, + "name": "John Doe", + "username": "jdoe", + "state": "active", + "avatar_url": "https://www.gravatar.com/avatar/0?s=80&d=identicon", + "web_url": "http://localhost/jdoe" + }, + { + "id": 50, + "name": "Group Member 1", + "username": "group_member_1", + "state": "active", + "avatar_url": "https://www.gravatar.com/avatar/0?s=80&d=identicon", + "web_url": "http://localhost/group_member_1" } ], - "approvals_before_merge": 2, - "reset_approvals_on_push": true, - "disable_overriding_approvers_per_merge_request": false, - "merge_requests_author_approval": false, - "merge_requests_disable_committers_approval": false + "approvals_required": 1, + "users": [ + { + "id": 2, + "name": "John Doe", + "username": "jdoe", + "state": "active", + "avatar_url": "https://www.gravatar.com/avatar/0?s=80&d=identicon", + "web_url": "http://localhost/jdoe" + } + ], + "groups": [ + { + "id": 5, + "name": "group1", + "path": "group1", + "description": "", + "visibility": "public", + "lfs_enabled": false, + "avatar_url": null, + "web_url": "http://localhost/groups/group1", + "request_access_enabled": false, + "full_name": "group1", + "full_path": "group1", + "parent_id": null, + "ldap_cn": null, + "ldap_access": null + } + ], + "contains_hidden_groups": false +} +``` + +### Update project-level rule + +>**Note:** This API endpoint is only available on 12.3 Starter and above. + +You can update project approval rules using the following endpoint: + +``` +PUT /projects/:id/approval_rules/:approval_rule_id +``` + +**Important:** Approvers and groups not in the `users`/`groups` param will be **removed** + +**Parameters:** + +| Attribute | Type | Required | Description | +|----------------------|---------|----------|-----------------------------------------------------------| +| `id` | integer | yes | The ID of a project | +| `approval_rule_id` | integer | yes | The ID of a approval rule | +| `name` | string | yes | The name of the approval rule | +| `approvals_required` | integer | yes | The number of required approvals for this rule | +| `users` | integer | no | The ids of users as approvers | +| `groups` | integer | no | The ids of groups as approvers | + +```json +{ + "id": 1, + "name": "security", + "rule_type": "regular", + "eligible_approvers": [ + { + "id": 2, + "name": "John Doe", + "username": "jdoe", + "state": "active", + "avatar_url": "https://www.gravatar.com/avatar/0?s=80&d=identicon", + "web_url": "http://localhost/jdoe" + }, + { + "id": 50, + "name": "Group Member 1", + "username": "group_member_1", + "state": "active", + "avatar_url": "https://www.gravatar.com/avatar/0?s=80&d=identicon", + "web_url": "http://localhost/group_member_1" + } + ], + "approvals_required": 1, + "users": [ + { + "id": 2, + "name": "John Doe", + "username": "jdoe", + "state": "active", + "avatar_url": "https://www.gravatar.com/avatar/0?s=80&d=identicon", + "web_url": "http://localhost/jdoe" + } + ], + "groups": [ + { + "id": 5, + "name": "group1", + "path": "group1", + "description": "", + "visibility": "public", + "lfs_enabled": false, + "avatar_url": null, + "web_url": "http://localhost/groups/group1", + "request_access_enabled": false, + "full_name": "group1", + "full_path": "group1", + "parent_id": null, + "ldap_cn": null, + "ldap_access": null + } + ], + "contains_hidden_groups": false } ``` +### Delete project-level rule + +>**Note:** This API endpoint is only available on 12.3 Starter and above. + +You can delete project approval rules using the following endpoint: + +``` +DELETE /projects/:id/approval_rules/:approval_rule_id +``` + +**Parameters:** + +| Attribute | Type | Required | Description | +|----------------------|---------|----------|-----------------------------------------------------------| +| `id` | integer | yes | The ID of a project | +| `approval_rule_id` | integer | yes | The ID of a approval rule + ### Change allowed approvers +>**Note:** This API endpoint has been deprecated. Please use Approval Rule API instead. >**Note:** This API endpoint is only available on 10.6 Starter and above. If you are allowed to, you can change approvers and approver groups using @@ -227,8 +414,6 @@ GET /projects/:id/merge_requests/:merge_request_iid/approvals } } ], - "approvers": [], - "approver_groups": [] } ``` @@ -249,7 +434,7 @@ POST /projects/:id/merge_requests/:merge_request_iid/approvals |----------------------|---------|----------|--------------------------------------------| | `id` | integer | yes | The ID of a project | | `merge_request_iid` | integer | yes | The IID of MR | -| `approvals_required` | integer | yes | Approvals required before MR can be merged | +| `approvals_required` | integer | yes | Approvals required before MR can be merged. Deprecated in 12.0 in favor of Approval Rules API. | ```json { @@ -264,14 +449,13 @@ POST /projects/:id/merge_requests/:merge_request_iid/approvals "merge_status": "cannot_be_merged", "approvals_required": 2, "approvals_left": 2, - "approved_by": [], - "approvers": [], - "approver_groups": [] + "approved_by": [] } ``` ### Change allowed approvers for Merge Request +>**Note:** This API endpoint has been deprecated. Please use Approval Rule API instead. >**Note:** This API endpoint is only available on 10.6 Starter and above. If you are allowed to, you can change approvers and approver groups using @@ -401,8 +585,6 @@ does not match, the response code will be `409`. } } ], - "approvers": [], - "approver_groups": [] } ``` diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md index 1ade46efb1c..49ed4968b0d 100644 --- a/doc/api/merge_requests.md +++ b/doc/api/merge_requests.md @@ -1127,7 +1127,7 @@ the `approvals_before_merge` parameter: ## Delete a merge request -Only for admins and project owners. Soft deletes the merge request in question. +Only for admins and project owners. Deletes the merge request in question. ``` DELETE /projects/:id/merge_requests/:merge_request_iid diff --git a/doc/api/projects.md b/doc/api/projects.md index 70df44ec0fd..9f392418153 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -855,6 +855,7 @@ GET /projects/:id/users | Attribute | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `search` | string | no | Search for specific users | +| `skip_users` | array[int] | no | Filter out users with the specified IDs | ```json [ @@ -2036,13 +2037,13 @@ Read more in the [Project Badges](project_badges.md) documentation. The non-default [issue and merge request description templates](../user/project/description_templates.md) are managed inside the project's repository. So you can manage them via the API through the [Repositories API](repositories.md) and the [Repository Files API](repository_files.md). -## Download snapshot of a git repository +## Download snapshot of a Git repository > Introduced in GitLab 10.7 This endpoint may only be accessed by an administrative user. -Download a snapshot of the project (or wiki, if requested) git repository. This +Download a snapshot of the project (or wiki, if requested) Git repository. This snapshot is always in uncompressed [tar](https://en.wikipedia.org/wiki/Tar_(computing)) format. diff --git a/doc/api/protected_branches.md b/doc/api/protected_branches.md index 9309306ba05..ffb4a70168b 100644 --- a/doc/api/protected_branches.md +++ b/doc/api/protected_branches.md @@ -185,6 +185,7 @@ Example response: { "access_level": 30, "access_level_description": "Developers + Maintainers" + } ], "unprotect_access_levels": [ { @@ -217,6 +218,7 @@ Example response: "user_id": null, "group_id": null, "access_level_description": "Developers + Maintainers" + } ], "unprotect_access_levels": [ { @@ -232,7 +234,7 @@ Example response: ### Example with user / group level access **(STARTER)** Elements in the `allowed_to_push` / `allowed_to_merge` / `allowed_to_unprotect` array should take the -form `{user_id: integer}`, `{group_id: integer}` or `{access_level: integer}`. Each user must have access to the project and each group must [have this project shared](../user/project/members/share_project_with_groups.md). These access levels allow [more granular control over protected branch access](../user/project/protected_branches.md#restricting-push-and-merge-access-to-certain-users-starter) and were [added to the API in ][ee-3516] in GitLab 10.3 EE. +form `{user_id: integer}`, `{group_id: integer}` or `{access_level: integer}`. Each user must have access to the project and each group must [have this project shared](../user/project/members/share_project_with_groups.md). These access levels allow [more granular control over protected branch access](../user/project/protected_branches.md#restricting-push-and-merge-access-to-certain-users-starter) and were [added to the API in](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/3516) in GitLab 10.3 EE. ```bash curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" 'https://gitlab.example.com/api/v4/projects/5/protected_branches?name=*-stable&allowed_to_push%5B%5D%5Buser_id%5D=1' @@ -242,29 +244,29 @@ Example response: ```json { - "name":"*-stable", + "name": "*-stable", "push_access_levels": [ { - "access_level":null, - "user_id":1, - "group_id":null, - "access_level_description":"Administrator" + "access_level": null, + "user_id": 1, + "group_id": null, + "access_level_description": "Administrator" } ], "merge_access_levels": [ { - "access_level":40, - "user_id":null, - "group_id":null, - "access_level_description":"Maintainers" + "access_level": 40, + "user_id": null, + "group_id": null, + "access_level_description": "Maintainers" } ], "unprotect_access_levels": [ { - "access_level":40, - "user_id":null, - "group_id":null, - "access_level_description":"Maintainers" + "access_level": 40, + "user_id": null, + "group_id": null, + "access_level_description": "Maintainers" } ] } @@ -286,5 +288,3 @@ curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" 'https://git | --------- | ---- | -------- | ----------- | | `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | | `name` | string | yes | The name of the branch | - -[ee-3516]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/3516 "ProtectedBranches API handles per user/group granularity" diff --git a/doc/api/releases/links.md b/doc/api/releases/links.md index 9c91264ed65..b1b75abe32f 100644 --- a/doc/api/releases/links.md +++ b/doc/api/releases/links.md @@ -21,7 +21,7 @@ GET /projects/:id/releases/:tag_name/assets/links Example request: ```sh -curl --header "PRIVATE-TOKEN: gDybLx3yrUK_HLp3qPjS" "http://localhost:3000/api/v4/projects/24/releases/v0.1/assets/links" +curl --header "PRIVATE-TOKEN: gDybLx3yrUK_HLp3qPjS" "https://gitlab.example.com/api/v4/projects/24/releases/v0.1/assets/links" ``` Example response: @@ -60,7 +60,7 @@ GET /projects/:id/releases/:tag_name/assets/links/:link_id Example request: ```sh -curl --header "PRIVATE-TOKEN: gDybLx3yrUK_HLp3qPjS" "http://localhost:3000/api/v4/projects/24/releases/v0.1/assets/links/1" +curl --header "PRIVATE-TOKEN: gDybLx3yrUK_HLp3qPjS" "https://gitlab.example.com/api/v4/projects/24/releases/v0.1/assets/links/1" ``` Example response: @@ -96,7 +96,7 @@ curl --request POST \ --header "PRIVATE-TOKEN: gDybLx3yrUK_HLp3qPjS" \ --data name="awesome-v0.2.dmg" \ --data url="http://192.168.10.15:3000" \ - "http://localhost:3000/api/v4/projects/24/releases/v0.1/assets/links" + "https://gitlab.example.com/api/v4/projects/24/releases/v0.1/assets/links" ``` Example response: @@ -132,7 +132,7 @@ You have to specify at least one of `name` or `url` Example request: ```sh -curl --request PUT --data name="new name" --header "PRIVATE-TOKEN: gDybLx3yrUK_HLp3qPjS" "http://localhost:3000/api/v4/projects/24/releases/v0.1/assets/links/1" +curl --request PUT --data name="new name" --header "PRIVATE-TOKEN: gDybLx3yrUK_HLp3qPjS" "https://gitlab.example.com/api/v4/projects/24/releases/v0.1/assets/links/1" ``` Example response: @@ -163,7 +163,7 @@ DELETE /projects/:id/releases/:tag_name/assets/links/:link_id Example request: ```sh -curl --request DELETE --header "PRIVATE-TOKEN: gDybLx3yrUK_HLp3qPjS" "http://localhost:3000/api/v4/projects/24/releases/v0.1/assets/links/1" +curl --request DELETE --header "PRIVATE-TOKEN: gDybLx3yrUK_HLp3qPjS" "https://gitlab.example.com/api/v4/projects/24/releases/v0.1/assets/links/1" ``` Example response: diff --git a/doc/api/repository_files.md b/doc/api/repository_files.md index b292c9dd7de..513dc996c91 100644 --- a/doc/api/repository_files.md +++ b/doc/api/repository_files.md @@ -246,7 +246,7 @@ error message. Possible causes for a failed commit include: user tried to make an empty commit; - the branch was updated by a Git push while the file edit was in progress. -Currently gitlab-shell has a boolean return code, preventing GitLab from specifying the error. +Currently GitLab Shell has a boolean return code, preventing GitLab from specifying the error. ## Delete existing file in repository diff --git a/doc/api/settings.md b/doc/api/settings.md index 83125aff264..710b63c9a2f 100644 --- a/doc/api/settings.md +++ b/doc/api/settings.md @@ -232,7 +232,7 @@ are listed in the descriptions of the relevant settings. | `file_template_project_id` | integer | no | **(PREMIUM)** The ID of a project to load custom file templates from | | `first_day_of_week` | integer | no | Start day of the week for calendar views and date pickers. Valid values are `0` (default) for Sunday, `1` for Monday, and `6` for Saturday. | | `geo_status_timeout` | integer | no | **(PREMIUM)** The amount of seconds after which a request to get a secondary node status will time out. | -| `gitaly_timeout_default` | integer | no | Default Gitaly timeout, in seconds. This timeout is not enforced for git fetch/push operations or Sidekiq jobs. Set to `0` to disable timeouts. | +| `gitaly_timeout_default` | integer | no | Default Gitaly timeout, in seconds. This timeout is not enforced for Git fetch/push operations or Sidekiq jobs. Set to `0` to disable timeouts. | | `gitaly_timeout_fast` | integer | no | Gitaly fast operation timeout, in seconds. Some Gitaly operations are expected to be fast. If they exceed this threshold, there may be a problem with a storage shard and 'failing fast' can help maintain the stability of the GitLab instance. Set to `0` to disable timeouts. | | `gitaly_timeout_medium` | integer | no | Medium Gitaly timeout, in seconds. This should be a value between the Fast and the Default timeout. Set to `0` to disable timeouts. | | `gravatar_enabled` | boolean | no | Enable Gravatar. | @@ -244,7 +244,7 @@ are listed in the descriptions of the relevant settings. | `hide_third_party_offers` | boolean | no | Do not display offers from third parties within GitLab. | | `home_page_url` | string | no | Redirect to this URL when not logged in. | | `housekeeping_bitmaps_enabled` | boolean | required by: `housekeeping_enabled` | Enable Git pack file bitmap creation. | -| `housekeeping_enabled` | boolean | no | (**If enabled, requires:** `housekeeping_bitmaps_enabled`, `housekeeping_full_repack_period`, `housekeeping_gc_period`, and `housekeeping_incremental_repack_period`) Enable or disable git housekeeping. | +| `housekeeping_enabled` | boolean | no | (**If enabled, requires:** `housekeeping_bitmaps_enabled`, `housekeeping_full_repack_period`, `housekeeping_gc_period`, and `housekeeping_incremental_repack_period`) Enable or disable Git housekeeping. | | `housekeeping_full_repack_period` | integer | required by: `housekeeping_enabled` | Number of Git pushes after which an incremental `git repack` is run. | | `housekeeping_gc_period` | integer | required by: `housekeeping_enabled` | Number of Git pushes after which `git gc` is run. | | `housekeeping_incremental_repack_period` | integer | required by: `housekeeping_enabled` | Number of Git pushes after which an incremental `git repack` is run. | @@ -321,4 +321,8 @@ are listed in the descriptions of the relevant settings. | `user_show_add_ssh_key_message` | boolean | no | When set to `false` disable the "You won't be able to pull or push project code via SSH" warning shown to users with no uploaded SSH key. | | `version_check_enabled` | boolean | no | Let GitLab inform you when an update is available. | | `local_markdown_version` | integer | no | Increase this value when any cached markdown should be invalidated. | +| `snowplow_enabled` | boolean | no | Enable snowplow tracking. | +| `snowplow_collector_hostname` | string | required by: `snowplow_enabled` | The Snowplow collector hostname. (e.g. `snowplow.trx.gitlab.net`) | +| `snowplow_site_id` | string | no | The Snowplow site name / application id. (e.g. `gitlab`) | +| `snowplow_cookie_domain` | string | no | The Snowplow cookie domain. (e.g. `.gitlab.com`) | | `geo_node_allowed_ips` | string | yes | **(PREMIUM)** Comma-separated list of IPs and CIDRs of allowed secondary nodes. For example, `1.1.1.1, 2.2.2.0/24`. | diff --git a/doc/api/tags.md b/doc/api/tags.md index af86ba961f4..1d874fea1f8 100644 --- a/doc/api/tags.md +++ b/doc/api/tags.md @@ -112,7 +112,7 @@ Parameters: - `tag_name` (required) - The name of a tag - `ref` (required) - Create tag using commit SHA, another tag name, or branch name. - `message` (optional) - Creates annotated tag. -- `release_description` (optional) - Add release notes to the git tag and store it in the GitLab database. +- `release_description` (optional) - Add release notes to the Git tag and store it in the GitLab database. ```json { @@ -166,7 +166,7 @@ Parameters: ## Create a new release -Add release notes to the existing git tag. If there +Add release notes to the existing Git tag. If there already exists a release for the given tag, status code `409` is returned. ``` diff --git a/doc/ci/README.md b/doc/ci/README.md index f3006528d01..4be13204227 100644 --- a/doc/ci/README.md +++ b/doc/ci/README.md @@ -16,7 +16,7 @@ through the [continuous methodologies](introduction/index.md#introduction-to-cic NOTE: **Out-of-the-box management systems can decrease hours spent on maintaining toolchains by 10% or more.** Watch our ["Mastering continuous software development"](https://about.gitlab.com/webcast/mastering-ci-cd/) -webcast to learn about continuous methods and how GitLab’s built-in CI can help you simplify and scale software development. +webcast to learn about continuous methods and how GitLab’s built-in CI can help you simplify and scale software development. ## Overview @@ -67,11 +67,11 @@ to your needs: For a broader overview, see the [CI/CD getting started](quick_start/README.md) guide. Once you're familiar with how GitLab CI/CD works, see the -[`. gitlab-ci.yml` full reference](yaml/README.md) +[`.gitlab-ci.yml` full reference](yaml/README.md) for all the attributes you can set and use. NOTE: **Note:** -GitLab CI/CD and [shared runners](runners/README.md#shared-specific-and-group-runners) are enabled in GitLab.com and available for all users, limited only to the [user's pipelines quota](../user/admin_area/settings/continuous_integration.md#extra-shared-runners-pipeline-minutes-quota-free-only). +GitLab CI/CD and [shared runners](runners/README.md#shared-specific-and-group-runners) are enabled in GitLab.com and available for all users, limited only to the [user's pipelines quota](../user/gitlab_com/index.md#shared-runners). ## Configuration @@ -131,7 +131,7 @@ Its feature set is listed on the table below according to DevOps stages. | **Secure** || | [Container Scanning](../user/application_security/container_scanning/index.md) **(ULTIMATE)** | Check your Docker containers for known vulnerabilities.| | [Dependency Scanning](../user/application_security/dependency_scanning/index.md) **(ULTIMATE)** | Analyze your dependencies for known vulnerabilities. | -| [License Management](../user/application_security/license_management/index.md) **(ULTIMATE)** | Search your project dependencies for their licenses. | +| [License Compliance](../user/application_security/license_management/index.md) **(ULTIMATE)** | Search your project dependencies for their licenses. | | [Security Test reports](../user/project/merge_requests/index.md#security-reports-ultimate) **(ULTIMATE)** | Check for app vulnerabilities. | ## Examples @@ -174,7 +174,7 @@ been necessary. These are: #### 12.0 -- [Use refspec to clone/fetch git +- [Use refspec to clone/fetch Git repository](https://gitlab.com/gitlab-org/gitlab-runner/issues/4069). - [Old cache configuration](https://gitlab.com/gitlab-org/gitlab-runner/issues/4070). diff --git a/doc/ci/directed_acyclic_graph/index.md b/doc/ci/directed_acyclic_graph/index.md new file mode 100644 index 00000000000..1c38c08b7cb --- /dev/null +++ b/doc/ci/directed_acyclic_graph/index.md @@ -0,0 +1,76 @@ +--- +type: reference +--- + +# Directed Acyclic Graph + +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/47063) in GitLab 12.2 (enabled by `ci_dag_support` feature flag). + +A [directed acyclic graph](https://www.techopedia.com/definition/5739/directed-acyclic-graph-dag) can be +used in the context of a CI/CD pipeline to build relationships between jobs such that +execution is performed in the quickest possible manner, regardless how stages may +be set up. + +For example, you may have a specific tool or separate website that is built +as part of your main project. Using a DAG, you can specify the relationship between +these jobs and GitLab will then execute the jobs as soon as possible instead of waiting +for each stage to complete. + +Unlike other DAG solutions for CI/CD, GitLab does not require you to choose one or the +other. You can implement a hybrid combination of DAG and traditional +stage-based operation within a single pipeline. Configuration is kept very simple, +requiring a single keyword to enable the feature for any job. + +Consider a monorepo as follows: + +``` +./service_a +./service_b +./service_c +./service_d +``` + +It has a pipeline that looks like the following: + +| build | test | deploy | +| ----- | ---- | ------ | +| build_a | test_a | deploy_a | +| build_b | test_b | deploy_b | +| build_c | test_c | deploy_c | +| build_d | test_d | deploy_d | + +Using a DAG, you can relate the `_a` jobs to each other separately from the `_b` jobs, +and even if service `a` takes a very long time to build, service `b` will not +wait for it and will finish as quickly as it can. In this very same pipeline, `_c` and +`_d` can be left alone and will run together in staged sequence just like any normal +GitLab pipeline. + +## Use cases + +A DAG can help solve several different kinds of relationships between jobs within +a CI/CD pipeline. Most typically this would cover when jobs need to fan in or out, +and/or merge back together (diamond dependencies). This can happen when you're +handling multi-platform builds or complex webs of dependencies as in something like +an operating system build or a complex deployment graph of independently deployable +but related microservices. + +Additionally, a DAG can help with general speediness of pipelines and helping +to deliver fast feedback. By creating dependency relationships that don't unnecessarily +block each other, your pipelines will run as quickly as possible regardless of +pipeline stages, ensuring output (including errors) is available to developers +as quickly as possible. + +## Usage + +Relationships are defined between jobs using the [`needs:` keyword](../yaml/README.md#needs). + +Note that `needs:` also works with the [parallel](../yaml/README.md#parallel) keyword, +giving you powerful options for parallelization within your pipeline. + +## Limitations + +A directed acyclic graph is a complicated feature, and as of the initial MVC there +are certain use cases that you may need to work around. For more information: + +- [`needs` requirements and limitations](../yaml/README.md#requirements-and-limitations). +- Related epic [gitlab-org#1716](https://gitlab.com/groups/gitlab-org/-/epics/1716). diff --git a/doc/ci/docker/using_docker_images.md b/doc/ci/docker/using_docker_images.md index d5056568dff..489791141ed 100644 --- a/doc/ci/docker/using_docker_images.md +++ b/doc/ci/docker/using_docker_images.md @@ -495,14 +495,6 @@ that runner. ## Define an image from a private Container Registry -> **Notes:** -> -> - This feature requires GitLab Runner **1.8** or higher -> - For GitLab Runner versions **>= 0.6, <1.8** there was a partial -> support for using private registries, which required manual configuration -> of credentials on runner's host. We recommend to upgrade your Runner to -> at least version **1.8** if you want to use private registries. - To access private container registries, the GitLab Runner process can use: - [Statically defined credentials](#using-statically-defined-credentials). That is, a username and password for a specific registry. @@ -525,6 +517,17 @@ it's provided as an environment variable. This is because GitLab Runnner uses ** `config.toml` configuration and doesn't interpolate **ANY** environment variables at runtime. +### Requirements and limitations + +- This feature requires GitLab Runner **1.8** or higher. +- For GitLab Runner versions **>= 0.6, <1.8** there was a partial + support for using private registries, which required manual configuration + of credentials on runner's host. We recommend to upgrade your Runner to + at least version **1.8** if you want to use private registries. +- Not available for [Kubernetes executor](https://docs.gitlab.com/runner/executors/kubernetes.html), + follow <https://gitlab.com/gitlab-org/gitlab-runner/issues/2673> for + details. + ### Using statically-defined credentials There are two approaches that you can take in order to access a diff --git a/doc/ci/environments.md b/doc/ci/environments.md index 9e1a62bae71..f6c47a99712 100644 --- a/doc/ci/environments.md +++ b/doc/ci/environments.md @@ -619,6 +619,10 @@ versions of the app, all without leaving GitLab. Add a [button to the Monitoring dashboard](../user/project/operations/linking_to_an_external_dashboard.md) linking directly to your existing external dashboards. +#### Embedding metrics in GitLab Flavored Markdown + +Metric charts can be embedded within GitLab Flavored Markdown. See [Embedding Metrics within GitLab Flavored Markdown](../user/project/integrations/prometheus.md#embedding-metric-charts-within-gitlab-flavored-markdown) for more details. + ### Web terminals > Web terminals were added in GitLab 8.15 and are only available to project Maintainers and Owners. diff --git a/doc/ci/merge_request_pipelines/pipelines_for_merged_results/index.md b/doc/ci/merge_request_pipelines/pipelines_for_merged_results/index.md index ad07c662965..126e12e460f 100644 --- a/doc/ci/merge_request_pipelines/pipelines_for_merged_results/index.md +++ b/doc/ci/merge_request_pipelines/pipelines_for_merged_results/index.md @@ -35,10 +35,9 @@ get out of WIP status or resolve merge conflicts as soon as possible. ## Requirements and limitations -Pipelines for merged results require: +Pipelines for merged results require a [GitLab Runner][runner] 11.9 or newer. -- [GitLab Runner](https://gitlab.com/gitlab-org/gitlab-runner) 11.9 or newer. -- [Gitaly](https://gitlab.com/gitlab-org/gitaly) 1.21.0 or newer. +[runner]: https://gitlab.com/gitlab-org/gitlab-runner In addition, pipelines for merged results have the following limitations: diff --git a/doc/ci/multi_project_pipelines.md b/doc/ci/multi_project_pipelines.md index ced4344a0b0..61f260cb70d 100644 --- a/doc/ci/multi_project_pipelines.md +++ b/doc/ci/multi_project_pipelines.md @@ -176,6 +176,21 @@ Upstream pipelines take precedence over downstream ones. If there are two variables with the same name defined in both upstream and downstream projects, the ones defined in the upstream project will take precedence. +### Mirroring status from upstream pipeline + +You can mirror the pipeline status from an upstream pipeline to a bridge job by +using the `needs:pipeline` keyword. The latest pipeline status from master is +replicated to the bridge job. + +Example: + +```yaml +upstream_bridge: + stage: test + needs: + pipeline: other/project +``` + ### Limitations Because bridge jobs are a little different to regular jobs, it is not @@ -190,5 +205,5 @@ Some features are not implemented yet. For example, support for environments. - `stage` - `allow_failure` - `only` and `except` -- `when` +- `when` (only with `on_success`, `on_failure`, and `always` values) - `extends` diff --git a/doc/ci/pipelines.md b/doc/ci/pipelines.md index ed8d0e3bc35..eaa6efc526d 100644 --- a/doc/ci/pipelines.md +++ b/doc/ci/pipelines.md @@ -377,6 +377,10 @@ This functionality is only available: - For users with at least Developer access. - If the the stage contains [manual actions](#manual-actions-from-pipeline-graphs). +## Most Recent Pipeline + +There's a link to the latest pipeline for the last commit of a given branch at `/project/pipelines/[branch]/latest`. Also, `/project/pipelines/latest` will redirect you to the latest pipeline for the last commit on the project's default branch. + ## Security on protected branches A strict security model is enforced when pipelines are executed on diff --git a/doc/ci/quick_start/img/build_log.png b/doc/ci/quick_start/img/build_log.png Binary files differindex 2bf0992c50e..16698629edc 100644 --- a/doc/ci/quick_start/img/build_log.png +++ b/doc/ci/quick_start/img/build_log.png diff --git a/doc/ci/quick_start/img/builds_status.png b/doc/ci/quick_start/img/builds_status.png Binary files differindex 58978e23978..b4aeeb988d2 100644 --- a/doc/ci/quick_start/img/builds_status.png +++ b/doc/ci/quick_start/img/builds_status.png diff --git a/doc/ci/quick_start/img/pipelines_status.png b/doc/ci/quick_start/img/pipelines_status.png Binary files differindex 06d1559f5d2..39a77a26b25 100644 --- a/doc/ci/quick_start/img/pipelines_status.png +++ b/doc/ci/quick_start/img/pipelines_status.png diff --git a/doc/ci/quick_start/img/runners_activated.png b/doc/ci/quick_start/img/runners_activated.png Binary files differindex cd83c1a7e4c..ac09e1d0137 100644 --- a/doc/ci/quick_start/img/runners_activated.png +++ b/doc/ci/quick_start/img/runners_activated.png diff --git a/doc/ci/runners/README.md b/doc/ci/runners/README.md index c3678fc948e..269bd5c3428 100644 --- a/doc/ci/runners/README.md +++ b/doc/ci/runners/README.md @@ -88,7 +88,7 @@ visit the project you want to make the Runner work for in GitLab: ## Registering a group Runner -Creating a group Runner requires Maintainer permissions for the group. To create a +Creating a group Runner requires Owner permissions for the group. To create a group Runner visit the group you want to make the Runner work for in GitLab: 1. Go to **Settings > CI/CD** to obtain the token @@ -124,9 +124,9 @@ To lock/unlock a Runner: ## Assigning a Runner to another project -If you are Maintainer on a project where a specific Runner is assigned to, and the +If you are an Owner on a project where a specific Runner is assigned to, and the Runner is not [locked only to that project](#locking-a-specific-runner-from-being-enabled-for-other-projects), -you can enable the Runner also on any other project where you have Maintainer permissions. +you can enable the Runner also on any other project where you have Owner permissions. To enable/disable a Runner in your project: @@ -250,7 +250,7 @@ When you [register a Runner][register], its default behavior is to **only pick** [tagged jobs](../yaml/README.md#tags). NOTE: **Note:** -Maintainer [permissions](../../user/permissions.md) are required to change the +Owner [permissions](../../user/permissions.md) are required to change the Runner settings. To make a Runner pick untagged jobs: @@ -319,21 +319,21 @@ How this feature will work: 1. You set the _maximum job timeout_ for a Runner to 24 hours 1. You set the _CI/CD Timeout_ for a project to **2 hours** 1. You start a job -1. The job, if running longer, will be timeouted after **2 hours** +1. The job, if running longer, will be timed out after **2 hours** **Example 2 - Runner timeout not configured** 1. You remove the _maximum job timeout_ configuration from a Runner 1. You set the _CI/CD Timeout_ for a project to **2 hours** 1. You start a job -1. The job, if running longer, will be timeouted after **2 hours** +1. The job, if running longer, will be timed out after **2 hours** **Example 3 - Runner timeout smaller than project timeout** 1. You set the _maximum job timeout_ for a Runner to **30 minutes** 1. You set the _CI/CD Timeout_ for a project to 2 hours 1. You start a job -1. The job, if running longer, will be timeouted after **30 minutes** +1. The job, if running longer, will be timed out after **30 minutes** ### Be careful with sensitive information diff --git a/doc/ci/ssh_keys/README.md b/doc/ci/ssh_keys/README.md index d9f022a7125..b6aebd3bd78 100644 --- a/doc/ci/ssh_keys/README.md +++ b/doc/ci/ssh_keys/README.md @@ -76,7 +76,7 @@ to access it. This is where an SSH key pair comes in handy. ## without extra base64 encoding. ## https://gitlab.com/gitlab-examples/ssh-private-key/issues/1#note_48526556 ## - - echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add - > /dev/null + - echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add - ## ## Create the SSH directory and give it the right permissions diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md index d741482b662..5a15b907da0 100644 --- a/doc/ci/variables/README.md +++ b/doc/ci/variables/README.md @@ -94,7 +94,10 @@ This means that the value of the variable will be hidden in job logs, though it must match certain requirements to do so: - The value must be in a single line. -- The value must only consist of characters from the Base64 alphabet ([RFC4648](https://tools.ietf.org/html/rfc4648)) with the addition of `@` and `:`. +- The value must only consist of characters from the Base64 alphabet (RFC4648). + + [In GitLab 12.2](https://gitlab.com/gitlab-org/gitlab-ce/issues/63043) + and newer, `@` and `:` are also valid values. - The value must be at least 8 characters long. - The value must not use variables. @@ -509,7 +512,7 @@ Below you can find supported syntax reference: 1. Checking for an empty variable Examples: - + - `$VARIABLE == ""` - `$VARIABLE != ""` (introduced in GitLab 11.11) diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index a6051e87366..89a61b2a9e3 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -518,10 +518,24 @@ Four keys are available: - `changes` - `kubernetes` -If you use multiple keys under `only` or `except`, they act as an AND. The logic is: +If you use multiple keys under `only` or `except`, the keys will be evaluated as a +single conjoined expression. That is: + +- `only:` means "include this job if all of the conditions match". +- `except:` means "exclude this job if any of the conditions match". + +With `only`, individual keys are logically joined by an AND: > (any of refs) AND (any of variables) AND (any of changes) AND (if kubernetes is active) +`except` is implemented as a negation of this complete expression: + +> NOT((any of refs) AND (any of variables) AND (any of changes) AND (if kubernetes is active)) + +This, more intuitively, means the keys join by an OR. A functionally equivalent expression: + +> (any of refs) OR (any of variables) OR (any of changes) OR (if kubernetes is active) + #### `only:refs`/`except:refs` > `refs` policy introduced in GitLab 10.0. @@ -1568,7 +1582,7 @@ dashboards. The `license_management` report collects [Licenses](../../user/project/merge_requests/license_management.md) as artifacts. -The collected License Management report will be uploaded to GitLab as an artifact and will +The collected License Compliance report will be uploaded to GitLab as an artifact and will be automatically shown in merge requests, pipeline view and provide data for security dashboards. @@ -1665,6 +1679,84 @@ You can ask your administrator to [flip this switch](../../administration/job_artifacts.md#validation-for-dependencies) and bring back the old behavior. +### `needs` + +> Introduced in GitLab 12.2. + +The `needs:` keyword enables executing jobs out-of-order, allowing you to implement +a [directed acyclic graph](../directed_acyclic_graph/index.md) in your `.gitlab-ci.yml`. + +This lets you run some jobs without waiting for other ones, disregarding stage ordering +so you can have multiple stages running concurrently. + +Let's consider the following example: + +```yaml +linux:build: + stage: build + +mac:build: + stage: build + +linux:rspec: + stage: test + needs: [linux:build] + +linux:rubocop: + stage: test + needs: [linux:build] + +mac:rspec: + stage: test + needs: [mac:build] + +mac:rubocop: + stage: test + needs: [mac:build] + +production: + stage: deploy +``` + +This example creates three paths of execution: + +- Linux path: the `linux:rspec` and `linux:rubocop` jobs will be run as soon + as the `linux:build` job finishes without waiting for `mac:build` to finish. + +- macOS path: the `mac:rspec` and `mac:rubocop` jobs will be run as soon + as the `mac:build` job finishes, without waiting for `linux:build` to finish. + +- The `production` job will be executed as soon as all previous jobs + finish; in this case: `linux:build`, `linux:rspec`, `linux:rubocop`, + `mac:build`, `mac:rspec`, `mac:rubocop`. + +#### Requirements and limitations + +1. If `needs:` is set to point to a job that is not instantiated + because of `only/except` rules or otherwise does not exist, the + job will fail. +1. Note that on day one of the launch, we are temporarily limiting the + maximum number of jobs that a single job can need in the `needs:` array. Track + our [infrastructure issue](https://gitlab.com/gitlab-com/gl-infra/infrastructure/issues/7541) + for details on the current limit. +1. If you use `dependencies:` with `needs:`, it's important that you + do not mark a job as having a dependency on something that won't + have been run at the time it needs it. It's better to use both + keywords in this case so that GitLab handles the ordering appropriately. +1. It is impossible for now to have `needs: []` (empty needs), + the job always needs to depend on something, unless this is the job + in the first stage (see [gitlab-ce#65504](https://gitlab.com/gitlab-org/gitlab-ce/issues/65504)). +1. If `needs:` refers to a job that is marked as `parallel:`. + the current job will depend on all parallel jobs created. +1. `needs:` is similar to `dependencies:` in that it needs to use jobs from + prior stages, meaning it is impossible to create circular + dependencies or depend on jobs in the current stage (see [gitlab-ce#65505](https://gitlab.com/gitlab-org/gitlab-ce/issues/65505)). +1. Related to the above, stages must be explicitly defined for all jobs + that have the keyword `needs:` or are referred to by one. +1. For self-managed users, the feature must be turned on using the `ci_dag_support` + feature flag. The `ci_dag_limit_needs` option, if set, will limit the number of + jobs that a single job can need to `50`. If unset, the limit is `5`. + ### `coverage` > [Introduced][ce-7447] in GitLab 8.17. diff --git a/doc/customization/issue_and_merge_request_template.md b/doc/customization/issue_and_merge_request_template.md index adaa120a37e..ebf711e105b 100644 --- a/doc/customization/issue_and_merge_request_template.md +++ b/doc/customization/issue_and_merge_request_template.md @@ -1,5 +1,5 @@ --- -redirect_to: '../user/project/description_templates.md#setting-a-default-template-for-issues-and-merge-requests--starter' +redirect_to: '../user/project/description_templates.md#setting-a-default-template-for-merge-requests-and-issues--starter' --- -This document was moved to [description_templates](../user/project/description_templates.md#setting-a-default-template-for-issues-and-merge-requests--starter). +This document was moved to [description_templates](../user/project/description_templates.md#setting-a-default-template-for-merge-requests-and-issues--starter). diff --git a/doc/customization/libravatar.md b/doc/customization/libravatar.md index 1c3bf877fa1..6d96528f760 100644 --- a/doc/customization/libravatar.md +++ b/doc/customization/libravatar.md @@ -14,7 +14,7 @@ server. ## Configuration -In the [gitlab.yml gravatar section](https://gitlab.com/gitlab-org/gitlab-ce/blob/672bd3902d86b78d730cea809fce312ec49d39d7/config/gitlab.yml.example#L122), set +In the [`gitlab.yml` gravatar section](https://gitlab.com/gitlab-org/gitlab-ce/blob/672bd3902d86b78d730cea809fce312ec49d39d7/config/gitlab.yml.example#L122), set the configuration options as follows: ### For HTTP @@ -46,7 +46,7 @@ For example, you host a service on `http://libravatar.example.com` and the `http://libravatar.example.com/avatar/%{hash}?s=%{size}&d=identicon` -### Omnibus-gitlab example +### Omnibus GitLab example In `/etc/gitlab/gitlab.rb`: diff --git a/doc/customization/system_header_and_footer_messages.md b/doc/customization/system_header_and_footer_messages.md index 15830be4e8a..bd2de3e201c 100644 --- a/doc/customization/system_header_and_footer_messages.md +++ b/doc/customization/system_header_and_footer_messages.md @@ -8,7 +8,7 @@ Navigate to the **Admin** area and go to the **Appearance** page. Under **System header and footer** insert your header message and/or footer message. Both background and font color of the header and footer are customizable. -You can also apply the header and footer messages to gitlab emails, +You can also apply the header and footer messages to GitLab emails, by checking the **Enable header and footer in emails** checkbox. Note that color settings will only be applied within the app interface and not to emails diff --git a/doc/development/README.md b/doc/development/README.md index 6281bb809ff..3912a828dec 100644 --- a/doc/development/README.md +++ b/doc/development/README.md @@ -5,7 +5,7 @@ description: 'Learn how to contribute to GitLab.' # Contributor and Development Docs -## Get started! +## Get started - Set up GitLab's development environment with [GitLab Development Kit (GDK)](https://gitlab.com/gitlab-org/gitlab-development-kit/blob/master/doc/howto/README.md) - [GitLab contributing guide](contributing/index.md) @@ -65,6 +65,7 @@ description: 'Learn how to contribute to GitLab.' - [Repository mirroring](repository_mirroring.md) - [Git LFS](lfs.md) - [Developing against interacting components or features](interacting_components.md) +- [File uploads](uploads.md) ## Performance guides @@ -116,6 +117,7 @@ description: 'Learn how to contribute to GitLab.' ## Case studies - [Database case study: Filtering by label](filtering_by_label.md) +- [Database case study: Namespaces storage statistics](namespaces_storage_statistics.md) ## Integration guides diff --git a/doc/development/api_styleguide.md b/doc/development/api_styleguide.md index 0866d3baeeb..61576236c96 100644 --- a/doc/development/api_styleguide.md +++ b/doc/development/api_styleguide.md @@ -51,7 +51,7 @@ allowed. – <https://github.com/ruby-grape/grape#declared> -### Exclude params from parent namespaces! +### Exclude params from parent namespaces > By default `declared(params)`includes parameters that were defined in all parent namespaces. @@ -64,7 +64,7 @@ In most cases you will want to exclude params from the parent namespaces: declared(params, include_parent_namespaces: false) ``` -### When to use `declared(params)`? +### When to use `declared(params)` You should always use `declared(params)` when you pass the params hash as arguments to a method call. diff --git a/doc/development/architecture.md b/doc/development/architecture.md index 87405bc2fec..5cb2ddf6e52 100644 --- a/doc/development/architecture.md +++ b/doc/development/architecture.md @@ -20,7 +20,7 @@ A typical install of GitLab will be on GNU/Linux. It uses Nginx or Apache as a w We also support deploying GitLab on Kubernetes using our [gitlab Helm chart](https://docs.gitlab.com/charts/). -The GitLab web app uses MySQL or PostgreSQL for persistent database information (e.g. users, permissions, issues, other meta data). GitLab stores the bare git repositories it serves in `/home/git/repositories` by default. It also keeps default branch and hook information with the bare repository. +The GitLab web app uses PostgreSQL for persistent database information (e.g. users, permissions, issues, other meta data). GitLab stores the bare git repositories it serves in `/home/git/repositories` by default. It also keeps default branch and hook information with the bare repository. When serving repositories over HTTP/HTTPS GitLab utilizes the GitLab API to resolve authorization and access as well as serving git objects. @@ -511,7 +511,15 @@ To summarize here's the [directory structure of the `git` user home directory](. ps aux | grep '^git' ``` -GitLab has several components to operate. As a system user (i.e. any user that is not the `git` user) it requires a persistent database (MySQL/PostreSQL) and redis database. It also uses Apache httpd or Nginx to proxypass Unicorn. As the `git` user it starts Sidekiq and Unicorn (a simple ruby HTTP server running on port `8080` by default). Under the GitLab user there are normally 4 processes: `unicorn_rails master` (1 process), `unicorn_rails worker` (2 processes), `sidekiq` (1 process). +GitLab has several components to operate. It requires a persistent database +(PostgreSQL) and redis database, and uses Apache httpd or Nginx to proxypass +Unicorn. All these components should run as different system users to GitLab +(e.g., `postgres`, `redis` and `www-data`, instead of `git`). + +As the `git` user it starts Sidekiq and Unicorn (a simple ruby HTTP server +running on port `8080` by default). Under the GitLab user there are normally 4 +processes: `unicorn_rails master` (1 process), `unicorn_rails worker` +(2 processes), `sidekiq` (1 process). ### Repository access @@ -554,12 +562,9 @@ $ /etc/init.d/nginx Usage: nginx {start|stop|restart|reload|force-reload|status|configtest} ``` -Persistent database (one of the following) +Persistent database ``` -/etc/init.d/mysqld -Usage: /etc/init.d/mysqld {start|stop|status|restart|condrestart|try-restart|reload|force-reload} - $ /etc/init.d/postgresql Usage: /etc/init.d/postgresql {start|stop|restart|reload|force-reload|status} [version ..] ``` @@ -597,11 +602,6 @@ PostgreSQL - `/var/log/postgresql/*` -MySQL - -- `/var/log/mysql/*` -- `/var/log/mysql.*` - ### GitLab specific config files GitLab has configuration files located in `/home/git/gitlab/config/*`. Commonly referenced config files include: diff --git a/doc/development/automatic_ce_ee_merge.md b/doc/development/automatic_ce_ee_merge.md index 423b35a9e3a..158606aa6a2 100644 --- a/doc/development/automatic_ce_ee_merge.md +++ b/doc/development/automatic_ce_ee_merge.md @@ -171,6 +171,19 @@ Now, every time you create an MR for CE and EE: job failed, you are required to submit the EE MR so that you can fix the conflicts in EE before merging your changes into CE. +## How we run the Automatic CE->EE merge at GitLab + +At GitLab, we use the [Merge Train](https://gitlab.com/gitlab-org/merge-train) +project to keep our [gitlab-ee](https://gitlab.com/gitlab-org/gitlab-ee) +repository updated with commits from +[gitlab-ce](https://gitlab.com/gitlab-org/gitlab-ce). + +We have a mirror of the [Merge Train](https://gitlab.com/gitlab-org/merge-train) +project [configured](https://ops.gitlab.net/gitlab-org/merge-train) to run an +automatic CE->EE merge job every twenty minutes as a scheduled CI job. The +[configured](https://ops.gitlab.net/gitlab-org/merge-train) Merge Train project +is only accessible to authorized GitLab staff. + ## FAQ ### How does automatic merging work? @@ -187,7 +200,7 @@ code. ### Why merge automatically? As we work towards continuous deployments and a single repository for both CE -and EE, we need to first make sure that all CE changes make their way into CE as +and EE, we need to first make sure that all CE changes make their way into EE as fast as possible. Past experiences and data have shown that periodic CE to EE merge requests do not scale, and often take a very long time to complete. For example, [in this diff --git a/doc/development/contributing/issue_workflow.md b/doc/development/contributing/issue_workflow.md index a38794c49af..b2e3ef7bf63 100644 --- a/doc/development/contributing/issue_workflow.md +++ b/doc/development/contributing/issue_workflow.md @@ -60,37 +60,18 @@ Subject labels are always all-lowercase. ## Team labels -**Important**: Most of the team labels will be soon deprecated in favor of [Group labels](#group-labels). +**Important**: Most of the historical team labels (e.g. Manage, Plan etc.) are +now deprecated in favor of [Group labels](#group-labels) and [Stage labels](#stage-labels). Team labels specify what team is responsible for this issue. Assigning a team label makes sure issues get the attention of the appropriate people. -The team labels planned for deprecation are: - -- ~Configure -- ~Create -- ~Defend -- ~Distribution -- ~Ecosystem -- ~Geo -- ~Gitaly -- ~Growth -- ~Manage -- ~Memory -- ~Monitor -- ~Plan -- ~Release -- ~Secure -- ~Verify - -The following team labels are **true** teams per our [organization structure](https://about.gitlab.com/company/team/structure/#organizational-structure) which will remain post deprecation. +The current team labels are: - ~Delivery - ~Documentation - -The descriptions on the [labels page](https://gitlab.com/gitlab-org/gitlab-ce/-/labels) explain what falls under the -responsibility of each team. +- ~Quality Team labels are always capitalized so that they show up as the first label for any issue. @@ -120,13 +101,13 @@ and thus are mutually exclusive. You can find the groups listed in the [Product Stages, Groups, and Categories][product-categories] page. -We use the term group to map down product requirements from our product stages. +We use the term group to map down product requirements from our product stages. As a team needs some way to collect the work their members are planning to be assigned to, we use the `~group::` labels to do so. Normally there is a 1:1 relationship between Stage labels and Group labels. In the spirit of "Everyone can contribute", any issue can be picked up by any group, depending on current priorities. For example, an issue labeled ~"devops::create" may be picked up by the ~"group::access" group. -We also use stage and group labels to help quantify our [throughput](https://about.gitlab.com/handbook/engineering/management/throughput). +We also use stage and group labels to help quantify our [throughput](https://about.gitlab.com/handbook/engineering/management/throughput). Please read [Stage and Group labels in Throughtput](https://about.gitlab.com/handbook/engineering/management/throughput/#stage-and-group-labels-in-throughput) for more information on how the labels are used in this context. [structure-groups]: https://about.gitlab.com/company/team/structure/#groups diff --git a/doc/development/contributing/merge_request_workflow.md b/doc/development/contributing/merge_request_workflow.md index 17328762c5b..4e9c5c81379 100644 --- a/doc/development/contributing/merge_request_workflow.md +++ b/doc/development/contributing/merge_request_workflow.md @@ -143,6 +143,37 @@ If the guidelines are not met, the MR will not pass the [Danger checks](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/danger/commit_messages/Dangerfile). For more information see [How to Write a Git Commit Message](https://chris.beams.io/posts/git-commit/). +Example commit message template that can be used on your machine that embodies the above (guide for [how to apply template](https://codeinthehole.com/tips/a-useful-template-for-commit-messages/)): + +```text +# (If applied, this commit will...) <subject> (Max 50 char) +# |<---- Using a Maximum Of 50 Characters ---->| + + +# Explain why this change is being made +# |<---- Try To Limit Each Line to a Maximum Of 72 Characters ---->| + +# Provide links or keys to any relevant tickets, articles or other resources +# Use issues and merge requests' full URLs instead of short references, +# as they are displayed as plain text outside of GitLab + +# --- COMMIT END --- +# -------------------- +# Remember to +# Capitalize the subject line +# Use the imperative mood in the subject line +# Do not end the subject line with a period +# Subject must contain at least 3 words +# Separate subject from body with a blank line +# Commits that change 30 or more lines across at least 3 files must +# describe these changes in the commit body +# Do not use Emojis +# Use the body to explain what and why vs. how +# Can use multiple lines with "-" for bullet points in body +# For more information: https://chris.beams.io/posts/git-commit/ +# -------------------- +``` + ## Contribution acceptance criteria To make sure that your merge request can be approved, please ensure that it meets diff --git a/doc/development/distributed_tracing.md b/doc/development/distributed_tracing.md index bfce7488a8d..4776c8348d4 100644 --- a/doc/development/distributed_tracing.md +++ b/doc/development/distributed_tracing.md @@ -179,4 +179,3 @@ By default, the Jaeger search UI is available at <http://localhost:16686/search> TIP: **Tip:** Don't forget that you will need to generate traces by using the application before they appear in the Jaeger UI. - diff --git a/doc/development/documentation/feature-change-workflow.md b/doc/development/documentation/feature-change-workflow.md index ac93ada5a4b..00c76fe0f1b 100644 --- a/doc/development/documentation/feature-change-workflow.md +++ b/doc/development/documentation/feature-change-workflow.md @@ -69,7 +69,7 @@ To follow a consistent workflow every month, documentation changes involve the Product Managers, the developer who shipped the feature, and the technical writer for the DevOps stage. Each role is described below. -The Documentation items in the GitLab CE/EE [Feature Proposal issue template](https://gitlab.com/gitlab-org/gitlab-ce/raw/template-improvements-for-documentation/.gitlab/issue_templates/Feature%20proposal.md) +The Documentation items in the GitLab CE/EE [Feature Proposal issue template](https://gitlab.com/gitlab-org/gitlab-ce/raw/master/.gitlab/issue_templates/Feature%20proposal.md) and default merge request template will assist you with following this process. ### Product Manager role diff --git a/doc/development/documentation/styleguide.md b/doc/development/documentation/styleguide.md index e84d65f424e..c1e3eb9680b 100644 --- a/doc/development/documentation/styleguide.md +++ b/doc/development/documentation/styleguide.md @@ -41,7 +41,7 @@ Include any media types/sources if the content is relevant to readers. You can f ### No special types -In the software industry, it is a best practice to organize documentatioin in different types. For example, [Divio recommends](https://www.divio.com/blog/documentation/): +In the software industry, it is a best practice to organize documentation in different types. For example, [Divio recommends](https://www.divio.com/blog/documentation/): 1. Tutorials 1. How-to guides @@ -283,24 +283,44 @@ Check specific punctuation rules for [list items](#list-items) below. ## List items -- Always start list items with a capital letter. +- Always start list items with a capital letter, unless they are parameters or commands + that are in backticks, or similar. - Always leave a blank line before and after a list. -- Begin a line with spaces (not tabs) to denote a subitem. -- To nest subitems, indent them with two spaces. -- To nest code blocks, indent them with four spaces. -- Only use ordered lists when their items describe a sequence of steps to follow. +- Begin a line with spaces (not tabs) to denote a [nested subitem](#nesting-inside-a-list-item). +- Only use ordered lists when their items describe a sequence of steps to follow: + + Do: + + These are the steps to do something: + + 1. First, do step 1 + 1. Then, do step 2 + 1. Finally, do step 3 + + Don't: + + This is a list of different features: + + 1. Feature 1 + 1. Feature 2 + 1. Feature 3 **Markup:** - Use dashes (`-`) for unordered lists instead of asterisks (`*`). -- Use the number one (`1`) for each item in an ordered list. - When rendered, the list items will appear with sequential numbering. +- Prefix `1.` to each item in an ordered list. + When rendered, the list items will appear with sequential numbering automatically. **Punctuation:** -- Do not add commas (`,`) or semicolons (`;`) to the end of a list item. -- Only add periods to the end of a list item if the item consists of a complete sentence. The [definition of full sentence](https://www2.le.ac.uk/offices/ld/resources/writing/grammar/grammar-guides/sentence) is: _"a complete sentence always contains a verb, expresses a complete idea, and makes sense standing alone"_. -- Be consistent throughout the list: if the majority of the items do not end in a period, do not end any of the items in a period, even if they consist of a complete sentence. The opposite is also valid: if the majority of the items end with a period, end all with a period. +- Do not add commas (`,`) or semicolons (`;`) to the end of list items. +- Only add periods to the end of a list item if the item consists of a complete sentence. + The [definition of full sentence](https://www2.le.ac.uk/offices/ld/resources/writing/grammar/grammar-guides/sentence) + is: _"a complete sentence always contains a verb, expresses a complete idea, and makes sense standing alone"_. +- Be consistent throughout the list: if the majority of the items do not end in a period, + do not end any of the items in a period, even if they consist of a complete sentence. + The opposite is also valid: if the majority of the items end with a period, end + all with a period. - Separate list items from explanatory text with a colon (`:`). For example: ```md @@ -330,12 +350,86 @@ Do: - Let's say this is also a complete sentence. - Not a complete sentence. -Don't: +Don't (third item should have a `.` to match the first and second items): - Let's say this is a complete sentence. - Let's say this is also a complete sentence. - Not a complete sentence +### Nesting inside a list item + +It is possible to nest items under a list item, so that they render with the same indentation +as the list item. This can be done with: + +- [Code blocks](#code-blocks) +- [Blockquotes](#blockquotes) +- [Alert boxes](#alert-boxes) +- [Images](#images) + +Items nested in lists should always align with the first character of the list item. +In unordered lists (using `-`), this means two spaces for each level of indentation: + +~~~md +- Unordered list item 1 + + A line nested using 2 spaces to align with the `U` above. + +- Unordered list item 2 + + > A quote block that will nest + > inside list item 2. + +- Unordered list item 3 + + ```text + a codeblock that will next inside list item 3 + ``` + +- Unordered list item 4 + + ![an image that will nest inside list item 4](image.png) +~~~ + +For ordered lists, use three spaces for each level of indentation: + +~~~md +1. Ordered list item 1 + + A line nested using 3 spaces to align with the `O` above. + +1. Ordered list item 2 + + > A quote block that will nest + > inside list item 2. + +1. Ordered list item 3 + + ```text + a codeblock that will next inside list item 3 + ``` + +1. Ordered list item 4 + + ![an image that will nest inside list item 4](image.png) +~~~ + +You can nest full lists inside other lists using the same rules as above. If you wish +to mix types, that is also possible, as long as you don't mix items at the same level: + +``` +1. Ordered list item one. +1. Ordered list item two. + - Nested unordered list item one. + - Nested unordered list item two. +1. Ordered list item three. + +- Unordered list item one. +- Unordered list item two. + 1. Nested ordered list item one. + 1. Nested ordered list item two. +- Unordered list item three. +``` + ## Quotes Valid for markdown content only, not for frontmatter entries: @@ -561,15 +655,16 @@ nicely on different mobile devices. ## Code blocks -- Always wrap code added to a sentence in inline code blocks (``` ` ```). +- Always wrap code added to a sentence in inline code blocks (`` ` ``). E.g., `.gitlab-ci.yml`, `git add .`, `CODEOWNERS`, `only: master`. File names, commands, entries, and anything that refers to code should be added to code blocks. To make things easier for the user, always add a full code block for things that can be useful to copy and paste, as they can easily do it with the button on code blocks. +- Add a blank line above and below code blocks. - For regular code blocks, always use a highlighting class corresponding to the language for better readability. Examples: - ````md + ~~~md ```ruby Ruby code ``` @@ -579,16 +674,17 @@ nicely on different mobile devices. ``` ```md - Markdown code + [Markdown code example](example.md) ``` ```text - Code for which no specific highlighting class is available. + Code or text for which no specific highlighting class is available. ``` - ```` + ~~~ -- To display raw markdown instead of rendered markdown, use four backticks on their own lines around the - markdown to display. See [example](https://gitlab.com/gitlab-org/gitlab-ce/blob/8c1991b9bb7e3b8d606481fdea316d633cfa5eb7/doc/development/documentation/styleguide.md#L275-287). +- To display raw markdown instead of rendered markdown, you can use triple backticks + with `md`, like the `Markdown code` example above, unless you want to include triple + backticks in the code block as well. In that case, use triple tildes (`~~~`) instead. - For a complete reference on code blocks, check the [Kramdown guide](https://about.gitlab.com/handbook/product/technical-writing/markdown-guide/#code-blocks). ## Alert boxes @@ -783,10 +879,10 @@ Other text includes deprecation notices and version-specific how-to information. When a feature is available in EE-only tiers, add the corresponding tier according to the feature availability: +- For GitLab Core and GitLab.com Free: `**(CORE)**`. - For GitLab Starter and GitLab.com Bronze: `**(STARTER)**`. - For GitLab Premium and GitLab.com Silver: `**(PREMIUM)**`. - For GitLab Ultimate and GitLab.com Gold: `**(ULTIMATE)**`. -- For GitLab Core and GitLab.com Free: `**(CORE)**`. To exclude GitLab.com tiers (when the feature is not available in GitLab.com), add the keyword "only": @@ -798,6 +894,7 @@ keyword "only": For GitLab.com only tiers (when the feature is not available for self-hosted instances): +- For GitLab Free and higher tiers: `**(FREE ONLY)**`. - For GitLab Bronze and higher tiers: `**(BRONZE ONLY)**`. - For GitLab Silver and higher tiers: `**(SILVER ONLY)**`. - For GitLab Gold: `**(GOLD ONLY)**`. @@ -929,7 +1026,7 @@ on this document. Further explanation is given below. The following can be used as a template to get started: -````md +~~~md ## Descriptive title One or two sentence description of what endpoint does. @@ -957,7 +1054,7 @@ Example response: } ] ``` -```` +~~~ ### Fake tokens @@ -985,7 +1082,7 @@ You can use the following fake tokens as examples. ### Method description Use the following table headers to describe the methods. Attributes should -always be in code blocks using backticks (``` ` ```). +always be in code blocks using backticks (`` ` ``). ```md | Attribute | Type | Required | Description | diff --git a/doc/development/ee_features.md b/doc/development/ee_features.md index 2217dedccd3..a732a94b7c4 100644 --- a/doc/development/ee_features.md +++ b/doc/development/ee_features.md @@ -945,7 +945,7 @@ export default { - Since we [can't async load a mixin](https://github.com/vuejs/vue-loader/issues/418#issuecomment-254032223) we will use the [`ee_else_ce`](../development/ee_features.md#javascript-code-in-assetsjavascripts) alias we already have for webpack. - This means all the EE specific props, computed properties, methods, etc that are EE only should be in a mixin in the `ee/` folder and we need to create a CE counterpart of the mixin -##### Example: +##### Example ```javascript import mixin from 'ee_else_ce/path/mixin'; @@ -976,7 +976,7 @@ For regular JS files, the approach is similar. 1. An EE file should be created with the EE only code, and it should extend the CE counterpart. 1. For code inside functions that can't be extended, the code should be moved into a new file and we should use `ee_else_ce` helper: -#### Example: +#### Example ```javascript import eeCode from 'ee_else_ce/ee_code'; diff --git a/doc/development/elasticsearch.md b/doc/development/elasticsearch.md index 0965db29557..090e5235619 100644 --- a/doc/development/elasticsearch.md +++ b/doc/development/elasticsearch.md @@ -148,6 +148,59 @@ Uses an [Edge NGram token filter](https://www.elastic.co/guide/en/elasticsearch/ - Searches can have their own analyzers. Remember to check when editing analyzers - `Character` filters (as opposed to token filters) always replace the original character, so they're not a good choice as they can hinder exact searches +## Zero downtime reindexing with multiple indices + +Currently GitLab can only handle a single version of setting. Any setting/schema changes would require reindexing everything from scratch. Since reindexing can take a long time, this can cause search functionality downtime. + +To avoid downtime, GitLab is working to support multiple indices that +can function at the same time. Whenever the schema changes, the admin +will be able to create a new index and reindex to it, while searches +continue to go to the older, stable index. Any data updates will be +forwarded to both indices. Once the new index is ready, an admin can +mark it active, which will direct all searches to it, and remove the old +index. + +This is also helpful for migrating to new servers, e.g. moving to/from AWS. + +Currently we are on the process of migrating to this new design. Everything is hardwired to work with one single version for now. + +### Architecture + +The traditional setup, provided by `elasticsearch-rails`, is to communicate through its internal proxy classes. Developers would write model-specific logic in a module for the model to include in (e.g. `SnippetsSearch`). The `__elasticsearch__` methods would return a proxy object, e.g.: + +- `Issue.__elasticsearch__` returns an instance of `Elasticsearch::Model::Proxy::ClassMethodsProxy` +- `Issue.first.__elasticsearch__` returns an instance of `Elasticsearch::Model::Proxy::InstanceMethodsProxy`. + +These proxy objects would talk to Elasticsearch server directly (see top half of the diagram). + +![Elasticsearch Architecture](img/elasticsearch_architecture.svg) + +In the planned new design, each model would have a pair of corresponding subclassed proxy objects, in which model-specific logic is located. For example, `Snippet` would have `SnippetClassProxy` and `SnippetInstanceProxy` (being subclass of `Elasticsearch::Model::Proxy::ClassMethodsProxy` and `Elasticsearch::Model::Proxy::InstanceMethodsProxy`, respectively). + +`__elasticsearch__` would represent another layer of proxy object, keeping track of multiple actual proxy objects. It would forward method calls to the appropriate index. For example: + +- `model.__elasticsearch__.search` would be forwarded to the one stable index, since it is a read operation. +- `model.__elasticsearch__.update_document` would be forwarded to all indices, to keep all indices up-to-date. + +The global configurations per version are now in the `Elastic::(Version)::Config` class. You can change mappings there. + +### Creating new version of schema + +NOTE: **Note:** this is not applicable yet as multiple indices functionality is not fully implemented. + +Folders like `ee/lib/elastic/v12p1` contain snapshots of search logic from different versions. To keep a continuous git history, the latest version lives under `ee/lib/elastic/latest`, but its classes are aliased under an actual version (e.g. `ee/lib/elastic/v12p3`). When referencing these classes, never use the `Latest` namespace directly, but use the actual version (e.g. `V12p3`). + +The version name basically follows GitLab's release version. If setting is changed in 12.3, we will create a new namespace called `V12p3` (p stands for "point"). Raise an issue if there is a need to name a version differently. + +If the current version is `v12p1`, and we need to create a new version for `v12p3`, the steps are as follows: + +1. Copy the entire folder of `v12p1` as `v12p3` +1. Change the namespace for files under `v12p3` folder from `V12p1` to `V12p3` (which are still aliased to `Latest`) +1. Delete `v12p1` folder +1. Copy the entire folder of `latest` as `v12p1` +1. Change the namespace for files under `v12p1` folder from `Latest` to `V12p1` +1. Make changes to files under the `latest` folder as needed + ## Troubleshooting ### Getting `flood stage disk watermark [95%] exceeded` diff --git a/doc/development/emails.md b/doc/development/emails.md index e6af075a282..edec0f86989 100644 --- a/doc/development/emails.md +++ b/doc/development/emails.md @@ -5,6 +5,10 @@ To view rendered emails "sent" in your development instance, visit [`/rails/letter_opener`](http://localhost:3000/rails/letter_opener). +Please note that [S/MIME signed](../administration/smime_signing_email.md) emails +[cannot be currently previewed](https://github.com/fgrehm/letter_opener_web/issues/96) with +`letter_opener`. + ## Mailer previews Rails provides a way to preview our mailer templates in HTML and plaintext using diff --git a/doc/development/fe_guide/axios.md b/doc/development/fe_guide/axios.md index 09b4a3c3d96..6e7cf523f36 100644 --- a/doc/development/fe_guide/axios.md +++ b/doc/development/fe_guide/axios.md @@ -38,7 +38,7 @@ Advantages over [`spyOn()`]: - no need to create response objects - does not allow call through (which we want to avoid) -- simple API to test error cases +- simple API to test error cases - provides `replyOnce()` to allow for different responses We have also decided against using [axios interceptors] because they are not suitable for mocking. diff --git a/doc/development/fe_guide/event_tracking.md b/doc/development/fe_guide/event_tracking.md index 715d91c6db6..1b417d4c8c2 100644 --- a/doc/development/fe_guide/event_tracking.md +++ b/doc/development/fe_guide/event_tracking.md @@ -1,6 +1,8 @@ -# Event Tracking +# Event tracking -We use a tracking interface that wraps up [Snowplow](https://github.com/snowplow/snowplow) for tracking custom events. Snowplow implements page tracking, but also exposes custom event tracking. +GitLab provides `Tracking`, an interface that wraps +[Snowplow](https://github.com/snowplow/snowplow) for tracking custom events. +It uses Snowplow's custom event tracking functions. The tracking interface can be imported in JS files as follows: diff --git a/doc/development/fe_guide/index.md b/doc/development/fe_guide/index.md index 6bf6113dd81..deaef8e768b 100644 --- a/doc/development/fe_guide/index.md +++ b/doc/development/fe_guide/index.md @@ -17,6 +17,8 @@ Working with our frontend assets requires Node (v8.10.0 or greater) and Yarn For our currently-supported browsers, see our [requirements][requirements]. +Use [BrowserStack](https://www.browserstack.com/) to test with our supported browsers. Login to BrowserStack with the credentials saved in GitLab's [shared 1Password account](https://about.gitlab.com/handbook/security/#1password-for-teams). + ## Initiatives Current high-level frontend goals are listed on [Frontend Epics](https://gitlab.com/groups/gitlab-org/-/epics?label_name%5B%5D=frontend). diff --git a/doc/development/fe_guide/style_guide_js.md b/doc/development/fe_guide/style_guide_js.md index d3fa350b847..125b11afcd0 100644 --- a/doc/development/fe_guide/style_guide_js.md +++ b/doc/development/fe_guide/style_guide_js.md @@ -21,31 +21,31 @@ See [our current .eslintrc](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/ 1. **Never Ever EVER** disable eslint globally for a file ```javascript - // bad - /* eslint-disable */ + // bad + /* eslint-disable */ - // better - /* eslint-disable some-rule, some-other-rule */ + // better + /* eslint-disable some-rule, some-other-rule */ - // best - // nothing :) + // best + // nothing :) ``` 1. If you do need to disable a rule for a single violation, try to do it as locally as possible ```javascript - // bad - /* eslint-disable no-new */ + // bad + /* eslint-disable no-new */ - import Foo from 'foo'; + import Foo from 'foo'; - new Foo(); + new Foo(); - // better - import Foo from 'foo'; + // better + import Foo from 'foo'; - // eslint-disable-next-line no-new - new Foo(); + // eslint-disable-next-line no-new + new Foo(); ``` 1. There are few rules that we need to disable due to technical debt. Which are: @@ -56,16 +56,16 @@ See [our current .eslintrc](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/ followed by any global declarations, then a blank newline prior to any imports or code. ```javascript - // bad - /* global Foo */ - /* eslint-disable no-new */ - import Bar from './bar'; + // bad + /* global Foo */ + /* eslint-disable no-new */ + import Bar from './bar'; - // good - /* eslint-disable no-new */ - /* global Foo */ + // good + /* eslint-disable no-new */ + /* global Foo */ - import Bar from './bar'; + import Bar from './bar'; ``` 1. **Never** disable the `no-undef` rule. Declare globals with `/* global Foo */` instead. @@ -73,23 +73,23 @@ See [our current .eslintrc](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/ 1. When declaring multiple globals, always use one `/* global [name] */` line per variable. ```javascript - // bad - /* globals Flash, Cookies, jQuery */ + // bad + /* globals Flash, Cookies, jQuery */ - // good - /* global Flash */ - /* global Cookies */ - /* global jQuery */ + // good + /* global Flash */ + /* global Cookies */ + /* global jQuery */ ``` 1. Use up to 3 parameters for a function or class. If you need more accept an Object instead. ```javascript - // bad - fn(p1, p2, p3, p4) {} + // bad + fn(p1, p2, p3, p4) {} - // good - fn(options) {} + // good + fn(options) {} ``` #### Modules, Imports, and Exports @@ -97,32 +97,32 @@ See [our current .eslintrc](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/ 1. Use ES module syntax to import modules ```javascript - // bad - const SomeClass = require('some_class'); + // bad + const SomeClass = require('some_class'); - // good - import SomeClass from 'some_class'; + // good + import SomeClass from 'some_class'; - // bad - module.exports = SomeClass; + // bad + module.exports = SomeClass; - // good - export default SomeClass; + // good + export default SomeClass; ``` Import statements are following usual naming guidelines, for example object literals use camel case: ```javascript - // some_object file - export default { - key: 'value', - }; + // some_object file + export default { + key: 'value', + }; - // bad - import ObjectLiteral from 'some_object'; + // bad + import ObjectLiteral from 'some_object'; - // good - import objectLiteral from 'some_object'; + // good + import objectLiteral from 'some_object'; ``` 1. Relative paths: when importing a module in the same directory, a child @@ -171,22 +171,22 @@ See [our current .eslintrc](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/ 1. Avoid adding to the global namespace. ```javascript - // bad - window.MyClass = class { /* ... */ }; + // bad + window.MyClass = class { /* ... */ }; - // good - export default class MyClass { /* ... */ } + // good + export default class MyClass { /* ... */ } ``` 1. Side effects are forbidden in any script which contains export ```javascript - // bad - export default class MyClass { /* ... */ } + // bad + export default class MyClass { /* ... */ } - document.addEventListener("DOMContentLoaded", function(event) { - new MyClass(); - } + document.addEventListener("DOMContentLoaded", function(event) { + new MyClass(); + } ``` #### Data Mutation and Pure functions @@ -257,17 +257,17 @@ See [our current .eslintrc](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/ `.reduce` or `.filter` ```javascript - const users = [ { name: 'Foo' }, { name: 'Bar' } ]; + const users = [ { name: 'Foo' }, { name: 'Bar' } ]; - // bad - users.forEach((user, index) => { - user.id = index; - }); + // bad + users.forEach((user, index) => { + user.id = index; + }); - // good - const usersWithId = users.map((user, index) => { - return Object.assign({}, user, { id: index }); - }); + // good + const usersWithId = users.map((user, index) => { + return Object.assign({}, user, { id: index }); + }); ``` #### Parse Strings into Numbers @@ -275,14 +275,14 @@ See [our current .eslintrc](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/ 1. `parseInt()` is preferable over `Number()` or `+` ```javascript - // bad - +'10' // 10 + // bad + +'10' // 10 - // good - Number('10') // 10 + // good + Number('10') // 10 - // better - parseInt('10', 10); + // better + parseInt('10', 10); ``` #### CSS classes used for JavaScript @@ -290,15 +290,15 @@ See [our current .eslintrc](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/ 1. If the class is being used in Javascript it needs to be prepend with `js-` ```html - // bad - <button class="add-user"> - Add User - </button> + // bad + <button class="add-user"> + Add User + </button> - // good - <button class="js-add-user"> - Add User - </button> + // good + <button class="js-add-user"> + Add User + </button> ``` ### Vue.js @@ -315,41 +315,41 @@ Please check this [rules][eslint-plugin-vue-rules] for more documentation. 1. Use a function in the bundle file to instantiate the Vue component: ```javascript - // bad - class { - init() { - new Component({}) - } + // bad + class { + init() { + new Component({}) } + } - // good - document.addEventListener('DOMContentLoaded', () => new Vue({ - el: '#element', - components: { - componentName - }, - render: createElement => createElement('component-name'), - })); + // good + document.addEventListener('DOMContentLoaded', () => new Vue({ + el: '#element', + components: { + componentName + }, + render: createElement => createElement('component-name'), + })); ``` 1. Do not use a singleton for the service or the store ```javascript - // bad - class Store { - constructor() { - if (!this.prototype.singleton) { - // do something - } + // bad + class Store { + constructor() { + if (!this.prototype.singleton) { + // do something } } + } - // good - class Store { - constructor() { - // do something - } + // good + class Store { + constructor() { + // do something } + } ``` 1. Use `.vue` for Vue templates. Do not use `%template` in HAML. @@ -360,36 +360,36 @@ Please check this [rules][eslint-plugin-vue-rules] for more documentation. 1. **Reference Naming**: Use PascalCase for their instances: ```javascript - // bad - import cardBoard from 'cardBoard.vue' + // bad + import cardBoard from 'cardBoard.vue' - components: { - cardBoard, - }; + components: { + cardBoard, + }; - // good - import CardBoard from 'cardBoard.vue' + // good + import CardBoard from 'cardBoard.vue' - components: { - CardBoard, - }; + components: { + CardBoard, + }; ``` 1. **Props Naming:** Avoid using DOM component prop names. 1. **Props Naming:** Use kebab-case instead of camelCase to provide props in templates. ```javascript - // bad - <component class="btn"> + // bad + <component class="btn"> - // good - <component css-class="btn"> + // good + <component css-class="btn"> - // bad - <component myProp="prop" /> + // bad + <component myProp="prop" /> - // good - <component my-prop="prop" /> + // good + <component my-prop="prop" /> ``` [#34371]: https://gitlab.com/gitlab-org/gitlab-ce/issues/34371 @@ -401,37 +401,37 @@ Please check this [rules][eslint-plugin-vue-rules] for more documentation. 1. With more than one attribute, all attributes should be on a new line: ```javascript - // bad - <component v-if="bar" - param="baz" /> + // bad + <component v-if="bar" + param="baz" /> - <button class="btn">Click me</button> + <button class="btn">Click me</button> - // good - <component - v-if="bar" - param="baz" - /> + // good + <component + v-if="bar" + param="baz" + /> - <button class="btn"> - Click me - </button> + <button class="btn"> + Click me + </button> ``` 1. The tag can be inline if there is only one attribute: ```javascript - // good - <component bar="bar" /> + // good + <component bar="bar" /> - // good - <component - bar="bar" - /> + // good + <component + bar="bar" + /> - // bad - <component - bar="bar" /> + // bad + <component + bar="bar" /> ``` #### Quotes @@ -439,15 +439,15 @@ Please check this [rules][eslint-plugin-vue-rules] for more documentation. 1. Always use double quotes `"` inside templates and single quotes `'` for all other JS. ```javascript - // bad - template: ` - <button :class='style'>Button</button> - ` + // bad + template: ` + <button :class='style'>Button</button> + ` - // good - template: ` - <button :class="style">Button</button> - ` + // good + template: ` + <button :class="style">Button</button> + ` ``` #### Props @@ -455,37 +455,37 @@ Please check this [rules][eslint-plugin-vue-rules] for more documentation. 1. Props should be declared as an object ```javascript - // bad - props: ['foo'] - - // good - props: { - foo: { - type: String, - required: false, - default: 'bar' - } + // bad + props: ['foo'] + + // good + props: { + foo: { + type: String, + required: false, + default: 'bar' } + } ``` 1. Required key should always be provided when declaring a prop ```javascript - // bad - props: { - foo: { - type: String, - } + // bad + props: { + foo: { + type: String, } + } - // good - props: { - foo: { - type: String, - required: false, - default: 'bar' - } + // good + props: { + foo: { + type: String, + required: false, + default: 'bar' } + } ``` 1. Default key should be provided if the prop is not required. @@ -493,30 +493,30 @@ Please check this [rules][eslint-plugin-vue-rules] for more documentation. On those a default key should not be provided. ```javascript - // good - props: { - foo: { - type: String, - required: false, - } + // good + props: { + foo: { + type: String, + required: false, } + } - // good - props: { - foo: { - type: String, - required: false, - default: 'bar' - } + // good + props: { + foo: { + type: String, + required: false, + default: 'bar' } + } - // good - props: { - foo: { - type: String, - required: true - } + // good + props: { + foo: { + type: String, + required: true } + } ``` #### Data @@ -524,17 +524,17 @@ Please check this [rules][eslint-plugin-vue-rules] for more documentation. 1. `data` method should always be a function ```javascript - // bad - data: { - foo: 'foo' - } + // bad + data: { + foo: 'foo' + } - // good - data() { - return { - foo: 'foo' - }; - } + // good + data() { + return { + foo: 'foo' + }; + } ``` #### Directives @@ -542,31 +542,31 @@ Please check this [rules][eslint-plugin-vue-rules] for more documentation. 1. Shorthand `@` is preferable over `v-on` ```javascript - // bad - <component v-on:click="eventHandler"/> + // bad + <component v-on:click="eventHandler"/> - // good - <component @click="eventHandler"/> + // good + <component @click="eventHandler"/> ``` 1. Shorthand `:` is preferable over `v-bind` ```javascript - // bad - <component v-bind:class="btn"/> + // bad + <component v-bind:class="btn"/> - // good - <component :class="btn"/> + // good + <component :class="btn"/> ``` 1. Shorthand `#` is preferable over `v-slot` ```javascript - // bad - <template v-slot:header></template> + // bad + <template v-slot:header></template> - // good - <template #header></template> + // good + <template #header></template> ``` #### Closing tags @@ -574,11 +574,11 @@ Please check this [rules][eslint-plugin-vue-rules] for more documentation. 1. Prefer self closing component tags ```javascript - // bad - <component></component> + // bad + <component></component> - // good - <component /> + // good + <component /> ``` #### Ordering @@ -610,48 +610,48 @@ When using `v-for` you need to provide a *unique* `:key` attribute for each item 1. If the elements of the array being iterated have an unique `id` it is advised to use it: ```html - <div - v-for="item in items" - :key="item.id" - > - <!-- content --> - </div> + <div + v-for="item in items" + :key="item.id" + > + <!-- content --> + </div> ``` 1. When the elements being iterated don't have a unique id, you can use the array index as the `:key` attribute ```html - <div - v-for="(item, index) in items" - :key="index" - > - <!-- content --> - </div> + <div + v-for="(item, index) in items" + :key="index" + > + <!-- content --> + </div> ``` 1. When using `v-for` with `template` and there is more than one child element, the `:key` values must be unique. It's advised to use `kebab-case` namespaces. ```html - <template v-for="(item, index) in items"> - <span :key="`span-${index}`"></span> - <button :key="`button-${index}`"></button> - </template> + <template v-for="(item, index) in items"> + <span :key="`span-${index}`"></span> + <button :key="`button-${index}`"></button> + </template> ``` 1. When dealing with nested `v-for` use the same guidelines as above. ```html - <div - v-for="item in items" - :key="item.id" + <div + v-for="item in items" + :key="item.id" + > + <span + v-for="element in array" + :key="element.id" > - <span - v-for="element in array" - :key="element.id" - > - <!-- content --> - </span> - </div> + <!-- content --> + </span> + </div> ``` Useful links: @@ -664,19 +664,19 @@ Useful links: 1. Tooltips: Do not rely on `has-tooltip` class name for Vue components ```javascript - // bad - <span - class="has-tooltip" - title="Some tooltip text"> - Text - </span> + // bad + <span + class="has-tooltip" + title="Some tooltip text"> + Text + </span> - // good - <span - v-tooltip - title="Some tooltip text"> - Text - </span> + // good + <span + v-tooltip + title="Some tooltip text"> + Text + </span> ``` 1. Tooltips: When using a tooltip, include the tooltip directive, `./app/assets/javascripts/vue_shared/directives/tooltip.js` @@ -684,13 +684,13 @@ Useful links: 1. Don't change `data-original-title`. ```javascript - // bad - <span data-original-title="tooltip text">Foo</span> + // bad + <span data-original-title="tooltip text">Foo</span> - // good - <span title="tooltip text">Foo</span> + // good + <span title="tooltip text">Foo</span> - $('span').tooltip('_fixTitle'); + $('span').tooltip('_fixTitle'); ``` ### The Javascript/Vue Accord diff --git a/doc/development/fe_guide/vuex.md b/doc/development/fe_guide/vuex.md index 9eeaee4482f..557d3132d71 100644 --- a/doc/development/fe_guide/vuex.md +++ b/doc/development/fe_guide/vuex.md @@ -313,7 +313,7 @@ export default { 1. Do not call a mutation directly. Always use an action to commit a mutation. Doing so will keep consistency throughout the application. From Vuex docs: - > why don't we just call store.commit('action') directly? Well, remember that mutations must be synchronous? Actions aren't. We can perform asynchronous operations inside an action. + > Why don't we just call store.commit('action') directly? Well, remember that mutations must be synchronous? Actions aren't. We can perform asynchronous operations inside an action. ```javascript // component.vue diff --git a/doc/development/file_storage.md b/doc/development/file_storage.md index 475d1c1611e..44af2b020a4 100644 --- a/doc/development/file_storage.md +++ b/doc/development/file_storage.md @@ -2,6 +2,8 @@ We use the [CarrierWave] gem to handle file upload, store and retrieval. +File uploads should be accelerated by workhorse, for details please refer to [uploads development documentation](uploads.md). + There are many places where file uploading is used, according to contexts: - System diff --git a/doc/development/go_guide/index.md b/doc/development/go_guide/index.md index 9f0ac8cc753..83444093f9c 100644 --- a/doc/development/go_guide/index.md +++ b/doc/development/go_guide/index.md @@ -107,6 +107,32 @@ Modules](https://github.com/golang/go/wiki/Modules). It provides a way to define and lock dependencies for reproducible builds. It should be used whenever possible. +When Go Modules are in use, there should not be a `vendor/` directory. Instead, +Go will automatically download dependencies when they are needed to build the +project. This is in line with how dependencies are handled with Bundler in Ruby +projects, and makes merge requests easier to review. + +In some cases, such as building a Go project for it to act as a dependency of a +CI run for another project, removing the `vendor/` directory means the code must +be downloaded repeatedly, which can lead to intermittent problems due to rate +limiting or network failures. In these circumstances, you should cache the +downloaded code between runs with a `.gitlab-ci.yml` snippet like this: + +```yaml +.go-cache: + variables: + GOPATH: $CI_PROJECT_DIR/.go + before_script: + - mkdir -p .go + cache: + paths: + - .go/pkg/mod/ + +test: + extends: .go-cache + # ... +``` + There was a [bug on modules checksums](https://github.com/golang/go/issues/29278) in Go < v1.11.4, so make sure to use at least this version to avoid `checksum mismatch` errors. diff --git a/doc/development/hash_indexes.md b/doc/development/hash_indexes.md index e6c1b3590b1..417ea18e22f 100644 --- a/doc/development/hash_indexes.md +++ b/doc/development/hash_indexes.md @@ -1,6 +1,6 @@ # Hash Indexes -Both PostgreSQL and MySQL support hash indexes besides the regular btree +PostgreSQL supports hash indexes besides the regular btree indexes. Hash indexes however are to be avoided at all costs. While they may _sometimes_ provide better performance the cost of rehashing can be very high. More importantly: at least until PostgreSQL 10.0 hash indexes are not diff --git a/doc/development/img/elasticsearch_architecture.svg b/doc/development/img/elasticsearch_architecture.svg new file mode 100644 index 00000000000..2f38f9b04ee --- /dev/null +++ b/doc/development/img/elasticsearch_architecture.svg @@ -0,0 +1 @@ +<svg version="1.2" width="210mm" height="297mm" viewBox="0 0 21000 29700" preserveAspectRatio="xMidYMid" fill-rule="evenodd" stroke-width="28.222" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><defs class="ClipPathGroup"><clipPath id="a" clipPathUnits="userSpaceOnUse"><path d="M0 0h21000v29700H0z"/></clipPath></defs><g class="SlideGroup"><g class="Slide" clip-path="url(#a)"><g class="Page"><g class="com.sun.star.drawing.CustomShape"><path class="BoundingBox" fill="none" d="M1975 5575h3051v1651H1975z"/><path fill="#FFF" d="M3500 7200H2000V5600h3000v1600H3500z"/><path fill="none" stroke="#3465A4" stroke-width="50" d="M3500 7200H2000V5600h3000v1600H3500z"/><text class="TextShape"><tspan class="TextParagraph" font-family="Arial, sans-serif" font-size="423" font-weight="400"><tspan class="TextPosition" x="2778" y="6311"><tspan>Snippet</tspan></tspan><tspan class="TextPosition" x="2099" y="6785"><tspan>(ActiveRecord)</tspan></tspan></tspan></text></g><g class="com.sun.star.drawing.CustomShape"><path class="BoundingBox" fill="none" d="M1475 3975h4051v3551H1475z"/><path fill="none" stroke="#3465A4" stroke-width="50" d="M3500 7500H1500V4000h4000v3500H3500z"/><text class="TextShape"><tspan class="TextParagraph" font-family="Arial, sans-serif" font-size="423" font-weight="400"><tspan class="TextPosition" x="1788" y="5048"><tspan>ApplicationSearch</tspan></tspan></tspan></text></g><g class="com.sun.star.drawing.ConnectorShape"><path class="BoundingBox" fill="none" d="M5975 4675h8051v701H5975z"/><path fill="none" stroke="#3465A4" stroke-width="50" d="M6000 5350h4000v-650h4000"/></g><g class="com.sun.star.drawing.ConnectorShape"><path class="BoundingBox" fill="none" d="M5975 5325h8051v1101H5975z"/><path fill="none" stroke="#3465A4" stroke-width="50" d="M6000 5350h4000v1050h4000"/></g><g class="com.sun.star.drawing.CustomShape"><path class="BoundingBox" fill="none" d="M1075 2875h4951v4951H1075z"/><path fill="none" stroke="#F33" stroke-width="50" d="M3550 7800H1100V2900h4900v4900H3550z"/><text class="TextShape"><tspan class="TextParagraph" font-family="Arial, sans-serif" font-size="423" font-weight="700"><tspan class="TextPosition" x="1946" y="3514"><tspan fill="#C9211E">SnippetsSearch</tspan></tspan></tspan></text></g><g class="com.sun.star.drawing.CustomShape"><path class="BoundingBox" fill="none" d="M1975 12175h3051v1651H1975z"/><path fill="#FFF" d="M3500 13800H2000v-1600h3000v1600H3500z"/><path fill="none" stroke="#3465A4" stroke-width="50" d="M3500 13800H2000v-1600h3000v1600H3500z"/><text class="TextShape"><tspan class="TextParagraph" font-family="Arial, sans-serif" font-size="423" font-weight="400"><tspan class="TextPosition" x="2778" y="12911"><tspan>Snippet</tspan></tspan><tspan class="TextPosition" x="2099" y="13385"><tspan>(ActiveRecord)</tspan></tspan></tspan></text></g><g class="com.sun.star.drawing.CustomShape"><path class="BoundingBox" fill="none" d="M1075 10775h4951v3251H1075z"/><path fill="none" stroke="#3465A4" stroke-width="50" d="M3550 14000H1100v-3200h4900v3200H3550z"/><text class="TextShape"><tspan class="TextParagraph" font-family="Arial, sans-serif" font-size="423" font-weight="400"><tspan class="TextPosition" x="2511" y="11461"><tspan>Application</tspan></tspan><tspan class="TextPosition" x="1933" y="11935"><tspan>VersionedSearch</tspan></tspan></tspan></text></g><g class="com.sun.star.drawing.ConnectorShape"><path class="BoundingBox" fill="none" d="M3525 13975h4501v7451H3525z"/><path fill="none" stroke="#3465A4" stroke-width="50" d="M3550 14000v7400h4450"/></g><g class="com.sun.star.drawing.CustomShape"><path class="BoundingBox" fill="none" d="M14008 14075h4985v851h-4985z"/><path fill="none" stroke="#999" stroke-width="50" d="M16500 14900h-2467v-800h4934v800h-2467z"/><text class="TextShape"><tspan class="TextParagraph" font-family="Arial, sans-serif" font-size="423" font-weight="400"><tspan class="TextPosition" x="14720" y="14648"><tspan fill="gray">ClassMethodProxy</tspan></tspan></tspan></text></g><g class="com.sun.star.drawing.CustomShape"><path class="BoundingBox" fill="none" d="M13375 13075h6251v2151h-6251z"/><path fill="none" stroke="#F33" stroke-width="50" d="M16500 15200h-3100v-2100h6200v2100h-3100z"/><text class="TextShape"><tspan class="TextParagraph" font-family="Arial, sans-serif" font-size="423" font-weight="700"><tspan class="TextPosition" x="13799" y="13731"><tspan fill="#C9211E">V12p1::SnippetClassProxy</tspan></tspan></tspan></text></g><g class="com.sun.star.drawing.CustomShape"><path class="BoundingBox" fill="none" d="M7975 14575h3051v1851H7975z"/><path fill="none" stroke="#3465A4" stroke-width="50" d="M9500 16400H8000v-1800h3000v1800H9500z"/><text class="TextShape"><tspan class="TextParagraph" font-family="Arial, sans-serif" font-size="423" font-weight="400"><tspan class="TextPosition" x="8277" y="15411"><tspan>MultiVersion-</tspan></tspan><tspan class="TextPosition" x="8429" y="15885"><tspan>ClassProxy</tspan></tspan></tspan></text></g><g class="com.sun.star.drawing.CustomShape"><path class="BoundingBox" fill="none" d="M14008 16875h4985v851h-4985z"/><path fill="none" stroke="#999" stroke-width="50" d="M16500 17700h-2467v-800h4934v800h-2467z"/><text class="TextShape"><tspan class="TextParagraph" font-family="Arial, sans-serif" font-size="423" font-weight="400"><tspan class="TextPosition" x="14720" y="17448"><tspan fill="gray">ClassMethodProxy</tspan></tspan></tspan></text></g><g class="com.sun.star.drawing.CustomShape"><path class="BoundingBox" fill="none" d="M13375 15875h6251v2151h-6251z"/><path fill="none" stroke="#F33" stroke-width="50" d="M16500 18000h-3100v-2100h6200v2100h-3100z"/><text class="TextShape"><tspan class="TextParagraph" font-family="Arial, sans-serif" font-size="423" font-weight="700"><tspan class="TextPosition" x="13799" y="16531"><tspan fill="#C9211E">V12p2::SnippetClassProxy</tspan></tspan></tspan></text></g><g class="com.sun.star.drawing.ConnectorShape"><path class="BoundingBox" fill="none" d="M10975 14125h2451v1401h-2451z"/><path fill="none" stroke="#3465A4" stroke-width="50" d="M11000 15500h1463v-1350h937"/></g><g class="com.sun.star.drawing.ConnectorShape"><path class="BoundingBox" fill="none" d="M10975 15475h2451v1501h-2451z"/><path fill="none" stroke="#3465A4" stroke-width="50" d="M11000 15500h1463v1450h937"/></g><g class="com.sun.star.drawing.ConnectorShape"><path class="BoundingBox" fill="none" d="M3525 13975h4501v1551H3525z"/><path fill="none" stroke="#3465A4" stroke-width="50" d="M3550 14000v1500h4450"/></g><g class="com.sun.star.drawing.CustomShape"><path class="BoundingBox" fill="none" d="M14008 19975h4985v851h-4985z"/><path fill="none" stroke="#999" stroke-width="50" d="M16500 20800h-2467v-800h4934v800h-2467z"/><text class="TextShape"><tspan class="TextParagraph" font-family="Arial, sans-serif" font-size="423" font-weight="400"><tspan class="TextPosition" x="14445" y="20548"><tspan fill="gray">InstanceMethodProxy</tspan></tspan></tspan></text></g><g class="com.sun.star.drawing.CustomShape"><path class="BoundingBox" fill="none" d="M13375 18975h6251v2151h-6251z"/><path fill="none" stroke="#F33" stroke-width="50" d="M16500 21100h-3100v-2100h6200v2100h-3100z"/><text class="TextShape"><tspan class="TextParagraph" font-family="Arial, sans-serif" font-size="423" font-weight="700"><tspan class="TextPosition" x="13505" y="19631"><tspan fill="#C9211E">V12p1::SnippetInstanceProxy</tspan></tspan></tspan></text></g><g class="com.sun.star.drawing.CustomShape"><path class="BoundingBox" fill="none" d="M7975 20275h3051v2251H7975z"/><path fill="none" stroke="#3465A4" stroke-width="50" d="M9500 22500H8000v-2200h3000v2200H9500z"/><text class="TextShape"><tspan class="TextParagraph" font-family="Arial, sans-serif" font-size="423" font-weight="400"><tspan class="TextPosition" x="8277" y="21311"><tspan>MultiVersion-</tspan></tspan><tspan class="TextPosition" x="8154" y="21785"><tspan>InstanceProxy</tspan></tspan></tspan></text></g><g class="com.sun.star.drawing.CustomShape"><path class="BoundingBox" fill="none" d="M14008 22775h4985v851h-4985z"/><path fill="none" stroke="#999" stroke-width="50" d="M16500 23600h-2467v-800h4934v800h-2467z"/><text class="TextShape"><tspan class="TextParagraph" font-family="Arial, sans-serif" font-size="423" font-weight="400"><tspan class="TextPosition" x="14445" y="23348"><tspan fill="gray">InstanceMethodProxy</tspan></tspan></tspan></text></g><g class="com.sun.star.drawing.CustomShape"><path class="BoundingBox" fill="none" d="M13375 21775h6251v2151h-6251z"/><path fill="none" stroke="#F33" stroke-width="50" d="M16500 23900h-3100v-2100h6200v2100h-3100z"/><text class="TextShape"><tspan class="TextParagraph" font-family="Arial, sans-serif" font-size="423" font-weight="700"><tspan class="TextPosition" x="13505" y="22431"><tspan fill="#C9211E">V12p2::SnippetInstanceProxy</tspan></tspan></tspan></text></g><g class="com.sun.star.drawing.ConnectorShape"><path class="BoundingBox" fill="none" d="M10975 20025h2451v1401h-2451z"/><path fill="none" stroke="#3465A4" stroke-width="50" d="M11000 21400h1463v-1350h937"/></g><g class="com.sun.star.drawing.ConnectorShape"><path class="BoundingBox" fill="none" d="M10975 21375h2451v1501h-2451z"/><path fill="none" stroke="#3465A4" stroke-width="50" d="M11000 21400h1463v1450h937"/></g><g class="com.sun.star.drawing.TextShape"><path class="BoundingBox" fill="none" d="M900 1600h10697v879H900z"/><text class="TextShape"><tspan class="TextParagraph" font-family="Arial, sans-serif" font-size="564" font-weight="400"><tspan class="TextPosition" x="1150" y="2233"><tspan>Standard elasticsearch-rails setup</tspan></tspan></tspan></text></g><g class="com.sun.star.drawing.TextShape"><path class="BoundingBox" fill="none" d="M900 9300h7683v879H900z"/><text class="TextShape"><tspan class="TextParagraph" font-family="Arial, sans-serif" font-size="564" font-weight="400"><tspan class="TextPosition" x="1150" y="9933"><tspan>GitLab multi-indices setup</tspan></tspan></tspan></text></g><g class="com.sun.star.drawing.TextShape"><path class="BoundingBox" fill="none" d="M3400 21300h4821v1197H3400z"/><text class="TextShape"><tspan class="TextParagraph" font-size="388" font-weight="400"><tspan class="TextPosition" x="4250" y="21840"><tspan fill="gray">(instance method)</tspan></tspan><tspan class="TextPosition" x="3651" y="22264"><tspan font-family="Courier" font-size="423">__elasticsearch__</tspan></tspan></tspan></text></g><g class="com.sun.star.drawing.TextShape"><path class="BoundingBox" fill="none" d="M3380 15400h4821v1197H3380z"/><text class="TextShape"><tspan class="TextParagraph" font-size="388" font-weight="400"><tspan class="TextPosition" x="4512" y="15940"><tspan fill="gray">(class method)</tspan></tspan><tspan class="TextPosition" x="3631" y="16364"><tspan font-family="Courier" font-size="423">__elasticsearch__</tspan></tspan></tspan></text></g><g class="com.sun.star.drawing.TextShape"><path class="BoundingBox" fill="none" d="M9000 3500h4821v1197H9000z"/><text class="TextShape"><tspan class="TextParagraph" font-size="388" font-weight="400"><tspan class="TextPosition" x="10132" y="4040"><tspan fill="gray">(class method)</tspan></tspan><tspan class="TextPosition" x="9251" y="4464"><tspan font-family="Courier" font-size="423">__elasticsearch__</tspan></tspan></tspan></text></g><g class="com.sun.star.drawing.TextShape"><path class="BoundingBox" fill="none" d="M9000 6400h4821v1197H9000z"/><text class="TextShape"><tspan class="TextParagraph" font-size="388" font-weight="400"><tspan class="TextPosition" x="9850" y="6940"><tspan fill="gray">(instance method)</tspan></tspan><tspan class="TextPosition" x="9251" y="7364"><tspan font-family="Courier" font-size="423">__elasticsearch__</tspan></tspan></tspan></text></g><g class="com.sun.star.drawing.CustomShape"><path class="BoundingBox" fill="none" d="M1975 25175h2051v851H1975z"/><path fill="none" stroke="#999" stroke-width="50" d="M3000 26000H2000v-800h2000v800H3000z"/><text class="TextShape"><tspan class="TextParagraph" font-family="Arial, sans-serif" font-size="423" font-weight="400"><tspan class="TextPosition" x="2634" y="25748"><tspan fill="gray">Foo</tspan></tspan></tspan></text></g><g class="com.sun.star.drawing.TextShape"><path class="BoundingBox" fill="none" d="M4400 25200h7101v726H4400z"/><text class="TextShape"><tspan class="TextParagraph" font-family="Arial, sans-serif" font-size="423" font-weight="400"><tspan class="TextPosition" x="4650" y="25710"><tspan>elasticsearch-rails’ internal class</tspan></tspan></tspan></text></g><g class="com.sun.star.drawing.TextShape"><path class="BoundingBox" fill="none" d="M4400 26400h8601v1200H4400z"/><text class="TextShape"><tspan class="TextParagraph" font-family="Arial, sans-serif" font-size="423" font-weight="400"><tspan class="TextPosition" x="4650" y="26910"><tspan>where model-specific logic is</tspan></tspan></tspan></text></g><g class="com.sun.star.drawing.CustomShape"><path class="BoundingBox" fill="none" d="M1975 26275h2051v851H1975z"/><path fill="none" stroke="#F33" stroke-width="50" d="M3000 27100H2000v-800h2000v800H3000z"/><text class="TextShape"><tspan class="TextParagraph" font-family="Arial, sans-serif" font-size="423" font-weight="700"><tspan class="TextPosition" x="2613" y="26848"><tspan fill="#C9211E">Foo</tspan></tspan></tspan></text></g><g class="com.sun.star.drawing.TextShape"><path class="BoundingBox" fill="none" d="M4900 17289h5901v2312H4900z"/><text class="TextShape"><tspan class="TextParagraph" font-family="Arial, sans-serif" font-size="370" font-weight="400"><tspan class="TextPosition" x="7236" y="17748"><tspan fill="gray">Write operations like </tspan></tspan><tspan class="TextPosition" x="5323" y="18159"><tspan fill="gray">indexing/updating are forwarded </tspan></tspan><tspan class="TextPosition" x="8024" y="18570"><tspan fill="gray">to all instances.</tspan></tspan></tspan><tspan class="TextParagraph" font-family="Arial, sans-serif" font-size="370" font-weight="400"><tspan class="TextPosition" x="5501" y="18981"><tspan fill="gray">Read operations are forwarded </tspan></tspan><tspan class="TextPosition" x="7126" y="19392"><tspan fill="gray">to specified instance.</tspan></tspan></tspan></text></g><g class="com.sun.star.drawing.ConnectorShape"><path class="BoundingBox" fill="none" d="M10785 15769h1422v2691h-1422z"/><path fill="none" stroke="#999" stroke-width="30" d="M10800 18444c1429 0 934-1618 1119-2337"/><path fill="#999" d="M12206 15769l-460 293 267 217 193-510z"/></g><g class="com.sun.star.drawing.ConnectorShape"><path class="BoundingBox" fill="none" d="M10785 18429h1528v2862h-1528z"/><path fill="none" stroke="#999" stroke-width="30" d="M10800 18444c1509 0 970 1782 1200 2526"/><path fill="#999" d="M12312 21290l-227-496-252 235 479 261z"/></g><g class="com.sun.star.drawing.TextShape"><path class="BoundingBox" fill="none" d="M1800 24000h7101v807H1800z"/><text class="TextShape"><tspan class="TextParagraph" font-family="Arial, sans-serif" font-size="494" font-weight="700"><tspan class="TextPosition" x="2050" y="24574"><tspan>Legend</tspan></tspan></tspan></text></g><g class="com.sun.star.drawing.CustomShape"><path class="BoundingBox" fill="none" d="M13975 4275h5085v851h-5085z"/><path fill="none" stroke="#999" stroke-width="50" d="M16517 5100h-2517v-800h5034v800h-2517z"/><text class="TextShape"><tspan class="TextParagraph" font-family="Arial, sans-serif" font-size="423" font-weight="400"><tspan class="TextPosition" x="14737" y="4848"><tspan fill="gray">ClassMethodProxy</tspan></tspan></tspan></text></g><g class="com.sun.star.drawing.CustomShape"><path class="BoundingBox" fill="none" d="M13975 5975h5085v851h-5085z"/><path fill="none" stroke="#999" stroke-width="50" d="M16517 6800h-2517v-800h5034v800h-2517z"/><text class="TextShape"><tspan class="TextParagraph" font-family="Arial, sans-serif" font-size="423" font-weight="400"><tspan class="TextPosition" x="14462" y="6548"><tspan fill="gray">InstanceMethodProxy</tspan></tspan></tspan></text></g></g></g></g></svg>
\ No newline at end of file diff --git a/doc/development/instrumentation.md b/doc/development/instrumentation.md index 5f95cf3707c..777d372ec60 100644 --- a/doc/development/instrumentation.md +++ b/doc/development/instrumentation.md @@ -1,6 +1,6 @@ # Instrumenting Ruby Code -GitLab Performance Monitoring allows instrumenting of both methods and custom +[GitLab Performance Monitoring](../administration/monitoring/performance/index.md) allows instrumenting of both methods and custom blocks of Ruby code. Method instrumentation is the primary form of instrumentation with block-based instrumentation only being used when we want to drill down to specific regions of code within a method. diff --git a/doc/development/interacting_components.md b/doc/development/interacting_components.md index 74d52d808e2..5e6dc8d460a 100644 --- a/doc/development/interacting_components.md +++ b/doc/development/interacting_components.md @@ -9,8 +9,8 @@ when making _backend_ changes that might involve multiple features or [component ## Uploads -GitLab supports uploads to [object storage]. That means every feature and -change that affects uploads should also be tested against [object storage], +GitLab supports uploads to [object storage]. That means every feature and +change that affects uploads should also be tested against [object storage], which is _not_ enabled by default in [GDK](https://gitlab.com/gitlab-org/gitlab-development-kit). When working on a related feature, make sure to enable and test it diff --git a/doc/development/lfs.md b/doc/development/lfs.md index 8c3408eb6e2..cb4c2d8967b 100644 --- a/doc/development/lfs.md +++ b/doc/development/lfs.md @@ -8,4 +8,4 @@ In April 2019, Francisco Javier López hosted a [Deep Dive] on GitLab's [Git LFS [Git LFS]: ../workflow/lfs/manage_large_binaries_with_git_lfs.html [recording on YouTube]: https://www.youtube.com/watch?v=Yyxwcksr0Qc [Google Slides]: https://docs.google.com/presentation/d/1E-aw6-z0rYd0346YhIWE7E9A65zISL9iIMAOq2zaw9E/edit -[PDF]: https://gitlab.com/gitlab-org/create-stage/uploads/07a89257a140db067bdfb484aecd35e1/Git_LFS_Deep_Dive__Create_.pdf
\ No newline at end of file +[PDF]: https://gitlab.com/gitlab-org/create-stage/uploads/07a89257a140db067bdfb484aecd35e1/Git_LFS_Deep_Dive__Create_.pdf diff --git a/doc/development/namespaces_storage_statistics.md b/doc/development/namespaces_storage_statistics.md new file mode 100644 index 00000000000..b3285de4705 --- /dev/null +++ b/doc/development/namespaces_storage_statistics.md @@ -0,0 +1,178 @@ +# Database case study: Namespaces storage statistics + +## Introduction + +On [Storage and limits management for groups](https://gitlab.com/groups/gitlab-org/-/epics/886), +we want to facilitate a method for easily viewing the amount of +storage consumed by a group, and allow easy management. + +## Proposal + +1. Create a new ActiveRecord model to hold the namespaces' statistics in an aggregated form (only for root namespaces). +1. Refresh the statistics in this model every time a project belonging to this namespace is changed. + +## Problem + +In GitLab, we update the project storage statistics through a +[callback](https://gitlab.com/gitlab-org/gitlab-ce/blob/v12.2.0.pre/app/models/project.rb#L90) +every time the project is saved. + +The summary of those statistics per namespace is then retrieved +by [`Namespaces#with_statistics`](https://gitlab.com/gitlab-org/gitlab-ce/blob/v12.2.0.pre/app/models/namespace.rb#L70) scope. Analyzing this query we noticed that: + +* It takes up to `1.2` seconds for namespaces with over `15k` projects. +* It can't be analyzed with [ChatOps](chatops_on_gitlabcom.md), as it times out. + +Additionally, the pattern that is currently used to update the project statistics +(the callback) doesn't scale adequately. It is currently one of the largest +[database queries transactions on production](https://gitlab.com/gitlab-org/gitlab-ce/issues/62488) +that takes the most time overall. We can't add one more query to it as +it will increase the transaction's length. + +Because of all of the above, we can't apply the same pattern to store +and update the namespaces statistics, as the `namespaces` table is one +of the largest tables on GitLab.com. Therefore we needed to find a performant and +alternative method. + +## Attempts + +### Attempt A: PostgreSQL materialized view + +Model can be updated through a refresh strategy based on a project routes SQL and a [materialized view](https://www.postgresql.org/docs/9.6/rules-materializedviews.html): + +```sql +SELECT split_part("rs".path, '/', 1) as root_path, + COALESCE(SUM(ps.storage_size), 0) AS storage_size, + COALESCE(SUM(ps.repository_size), 0) AS repository_size, + COALESCE(SUM(ps.wiki_size), 0) AS wiki_size, + COALESCE(SUM(ps.lfs_objects_size), 0) AS lfs_objects_size, + COALESCE(SUM(ps.build_artifacts_size), 0) AS build_artifacts_size, + COALESCE(SUM(ps.packages_size), 0) AS packages_size +FROM "projects" + INNER JOIN routes rs ON rs.source_id = projects.id AND rs.source_type = 'Project' + INNER JOIN project_statistics ps ON ps.project_id = projects.id +GROUP BY root_path +``` + +We could then execute the query with: + +```sql +REFRESH MATERIALIZED VIEW root_namespace_storage_statistics; +``` + +While this implied a single query update (and probably a fast one), it has some downsides: + +* Materialized views syntax varies from PostgreSQL and MySQL. While this feature was worked on, MySQL was still supported by GitLab. +* Rails does not have native support for materialized views. We'd need to use a specialized gem to take care of the management of the database views, which implies additional work. + +### Attempt B: An update through a CTE + +Similar to Attempt A: Model update done through a refresh strategy with a [Common Table Expression](https://www.postgresql.org/docs/9.1/queries-with.html) + +```sql +WITH refresh AS ( + SELECT split_part("rs".path, '/', 1) as root_path, + COALESCE(SUM(ps.storage_size), 0) AS storage_size, + COALESCE(SUM(ps.repository_size), 0) AS repository_size, + COALESCE(SUM(ps.wiki_size), 0) AS wiki_size, + COALESCE(SUM(ps.lfs_objects_size), 0) AS lfs_objects_size, + COALESCE(SUM(ps.build_artifacts_size), 0) AS build_artifacts_size, + COALESCE(SUM(ps.packages_size), 0) AS packages_size + FROM "projects" + INNER JOIN routes rs ON rs.source_id = projects.id AND rs.source_type = 'Project' + INNER JOIN project_statistics ps ON ps.project_id = projects.id + GROUP BY root_path) +UPDATE namespace_storage_statistics +SET storage_size = refresh.storage_size, + repository_size = refresh.repository_size, + wiki_size = refresh.wiki_size, + lfs_objects_size = refresh.lfs_objects_size, + build_artifacts_size = refresh.build_artifacts_size, + packages_size = refresh.packages_size +FROM refresh + INNER JOIN routes rs ON rs.path = refresh.root_path AND rs.source_type = 'Namespace' +WHERE namespace_storage_statistics.namespace_id = rs.source_id +``` + +Same benefits and downsides as attempt A. + +### Attempt C: Get rid of the model and store the statistics on Redis + +We could get rid of the model that stores the statistics in aggregated form and instead use a Redis Set. +This would be the [boring solution](https://about.gitlab.com/handbook/values/#boring-solutions) and the fastest one +to implement, as GitLab already includes Redis as part of its [Architecture](architecture.md#redis). + +The downside of this approach is that Redis does not provide the same persistence/consistency guarantees as PostgreSQL, +and this is information we can't afford to lose in a Redis failure. + +### Attempt D: Tag the root namespace and its child namespaces + +Directly relate the root namespace to its child namespaces, so +whenever a namespace is created without a parent, this one is tagged +with the root namespace ID: + +| id | root_id | parent_id +|:---|:--------|:---------- +| 1 | 1 | NULL +| 2 | 1 | 1 +| 3 | 1 | 2 + +To aggregate the statistics inside a namespace we'd execute something like: + +```sql +SELECT COUNT(...) +FROM projects +WHERE namespace_id IN ( + SELECT id + FROM namespaces + WHERE root_id = X +) +``` + +Even though this approach would make aggregating much easier, it has some major downsides: + +* We'd have to migrate **all namespaces** by adding and filling a new column. Because of the size of the table, dealing with time/cost will not be great. The background migration will take approximately `153h`, see <https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/29772>. +* Background migration has to be shipped one release before, delaying the functionality by another milestone. + +### Attempt E (final): Update the namespace storage statistics in async way + +This approach consists of keep using the incremental statistics updates we currently already have, +but we refresh them through Sidekiq jobs and in different transactions: + +1. Create a second table (`namespace_aggregation_schedules`) with two columns `id` and `namespace_id`. +1. Whenever the statistics of a project changes, insert a row into `namespace_aggregation_schedules` + - We don't insert a new row if there's already one related to the root namespace. + - Keeping in mind the length of the transaction that involves updating `project_statistics`(<https://gitlab.com/gitlab-org/gitlab-ce/issues/62488>), the insertion should be done in a different transaction and through a Sidekiq Job. +1. After inserting the row, we schedule another worker to be executed async at two different moments: + - One enqueued for immediate execution and another one scheduled in `1.5h` hours. + - We only schedule the jobs, if we can obtain a `1.5h` lease on Redis on a key based on the root namespace ID. + - If we can't obtain the lease, it indicates there's another aggregation already in progress, or scheduled in no more than `1.5h`. +1. This worker will: + - Update the root namespace storage statistics by querying all the namespaces through a service. + - Delete the related `namespace_aggregation_schedules` after the update. +1. Another Sidekiq job is also included to traverse any remaining rows on the `namespace_aggregation_schedules` table and schedule jobs for every pending row. + - This job is scheduled with cron to run every night (UTC). + +This implementation has the following benefits: + +* All the updates are done async, so we're not increasing the length of the transactions for `project_statistics`. +* We're doing the update in a single SQL query. +* It is compatible with PostgreSQL and MySQL. +* No background migration required. + +The only downside of this approach is that namespaces' statistics are updated up to `1.5` hours after the change is done, +which means there's a time window in which the statistics are inaccurate. Because we're still not +[enforcing storage limits](https://gitlab.com/gitlab-org/gitlab-ce/issues/30421), this is not a major problem. + +## Conclusion + +Updating the storage statistics asynchronously, was the less problematic and +performant approach of aggregating the root namespaces. + +All the details regarding this use case can be found on: + +* <https://gitlab.com/gitlab-org/gitlab-ce/issues/62214> +* Merge Request with the implementation: <https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/28996> + +Performance of the namespace storage statistics were measured in staging and production (GitLab.com). All results were posted +on <https://gitlab.com/gitlab-org/gitlab-ce/issues/64092>: No problem has been reported so far. diff --git a/doc/development/new_fe_guide/development/testing.md b/doc/development/new_fe_guide/development/testing.md index f7ea496d935..e0d413b748b 100644 --- a/doc/development/new_fe_guide/development/testing.md +++ b/doc/development/new_fe_guide/development/testing.md @@ -1,361 +1,6 @@ -# Overview of Frontend Testing +--- +redirect_to: '../../testing_guide/frontend_testing.md' +--- -Tests relevant for frontend development can be found at the following places: +This document was moved to [another location](../../testing_guide/frontend_testing.md). -- `spec/javascripts/` which are run by Karma (command: `yarn karma`) and contain - - [frontend unit tests](#frontend-unit-tests) - - [frontend component tests](#frontend-component-tests) - - [frontend integration tests](#frontend-integration-tests) -- `spec/frontend/` which are run by Jest (command: `yarn jest`) and contain - - [frontend unit tests](#frontend-unit-tests) - - [frontend component tests](#frontend-component-tests) - - [frontend integration tests](#frontend-integration-tests) -- `spec/features/` which are run by RSpec and contain - - [feature tests](#feature-tests) - -All tests in `spec/javascripts/` will eventually be migrated to `spec/frontend/` (see also [#52483](https://gitlab.com/gitlab-org/gitlab-ce/issues/52483)). - -In addition there were feature tests in `features/` run by Spinach in the past. -These have been removed from our codebase in May 2018 ([#23036](https://gitlab.com/gitlab-org/gitlab-ce/issues/23036)). - -See also: - -- [Old testing guide](../../testing_guide/frontend_testing.html). -- [Notes on testing Vue components](../../fe_guide/vue.html#testing-vue-components). - -## Frontend unit tests - -Unit tests are on the lowest abstraction level and typically test functionality that is not directly perceivable by a user. - -### When to use unit tests - -<details> - <summary>exported functions and classes</summary> - Anything that is exported can be reused at various places in a way you have no control over. - Therefore it is necessary to document the expected behavior of the public interface with tests. -</details> - -<details> - <summary>Vuex actions</summary> - Any Vuex action needs to work in a consistent way independent of the component it is triggered from. -</details> - -<details> - <summary>Vuex mutations</summary> - For complex Vuex mutations it helps to identify the source of a problem by separating the tests from other parts of the Vuex store. -</details> - -### When *not* to use unit tests - -<details> - <summary>non-exported functions or classes</summary> - Anything that is not exported from a module can be considered private or an implementation detail and doesn't need to be tested. -</details> - -<details> - <summary>constants</summary> - Testing the value of a constant would mean to copy it. - This results in extra effort without additional confidence that the value is correct. -</details> - -<details> - <summary>Vue components</summary> - Computed properties, methods, and lifecycle hooks can be considered an implementation detail of components and don't need to be tested. - They are implicitly covered by component tests. - The <a href="https://vue-test-utils.vuejs.org/guides/#getting-started">official Vue guidelines</a> suggest the same. -</details> - -### What to mock in unit tests - -<details> - <summary>state of the class under test</summary> - Modifying the state of the class under test directly rather than using methods of the class avoids side-effects in test setup. -</details> - -<details> - <summary>other exported classes</summary> - Every class needs to be tested in isolation to prevent test scenarios from growing exponentially. -</details> - -<details> - <summary>single DOM elements if passed as parameters</summary> - For tests that only operate on single DOM elements rather than a whole page, creating these elements is cheaper than loading a whole HTML fixture. -</details> - -<details> - <summary>all server requests</summary> - When running frontend unit tests, the backend may not be reachable. - Therefore all outgoing requests need to be mocked. -</details> - -<details> - <summary>asynchronous background operations</summary> - Background operations cannot be stopped or waited on, so they will continue running in the following tests and cause side effects. -</details> - -### What *not* to mock in unit tests - -<details> - <summary>non-exported functions or classes</summary> - Everything that is not exported can be considered private to the module and will be implicitly tested via the exported classes / functions. -</details> - -<details> - <summary>methods of the class under test</summary> - By mocking methods of the class under test, the mocks will be tested and not the real methods. -</details> - -<details> - <summary>utility functions (pure functions, or those that only modify parameters)</summary> - If a function has no side effects because it has no state, it is safe to not mock it in tests. -</details> - -<details> - <summary>full HTML pages</summary> - Loading the HTML of a full page slows down tests, so it should be avoided in unit tests. -</details> - -## Frontend component tests - -Component tests cover the state of a single component that is perceivable by a user depending on external signals such as user input, events fired from other components, or application state. - -### When to use component tests - -- Vue components - -### When *not* to use component tests - -<details> - <summary>Vue applications</summary> - Vue applications may contain many components. - Testing them on a component level requires too much effort. - Therefore they are tested on frontend integration level. -</details> - -<details> - <summary>HAML templates</summary> - HAML templates contain only Markup and no frontend-side logic. - Therefore they are not complete components. -</details> - -### What to mock in component tests - -<details> - <summary>DOM</summary> - Operating on the real DOM is significantly slower than on the virtual DOM. -</details> - -<details> - <summary>properties and state of the component under test</summary> - Similarly to testing classes, modifying the properties directly (rather than relying on methods of the component) avoids side-effects. -</details> - -<details> - <summary>Vuex store</summary> - To avoid side effects and keep component tests simple, Vuex stores are replaced with mocks. -</details> - -<details> - <summary>all server requests</summary> - Similar to unit tests, when running component tests, the backend may not be reachable. - Therefore all outgoing requests need to be mocked. -</details> - -<details> - <summary>asynchronous background operations</summary> - Similar to unit tests, background operations cannot be stopped or waited on, so they will continue running in the following tests and cause side effects. -</details> - -<details> - <summary>child components</summary> - Every component is tested individually, so child components are mocked. - See also <a href="https://vue-test-utils.vuejs.org/api/#shallowmount">shallowMount()</a> -</details> - -### What *not* to mock in component tests - -<details> - <summary>methods or computed properties of the component under test</summary> - By mocking part of the component under test, the mocks will be tested and not the real component. -</details> - -<details> - <summary>functions and classes independent from Vue</summary> - All plain JavaScript code is already covered by unit tests and needs not to be mocked in component tests. -</details> - -## Frontend integration tests - -Integration tests cover the interaction between all components on a single page. -Their abstraction level is comparable to how a user would interact with the UI. - -### When to use integration tests - -<details> - <summary>page bundles (<code>index.js</code> files in <code>app/assets/javascripts/pages/</code>)</summary> - Testing the page bundles ensures the corresponding frontend components integrate well. -</details> - -<details> - <summary>Vue applications outside of page bundles</summary> - Testing Vue applications as a whole ensures the corresponding frontend components integrate well. -</details> - -### What to mock in integration tests - -<details> - <summary>HAML views (use fixtures instead)</summary> - Rendering HAML views requires a Rails environment including a running database which we cannot rely on in frontend tests. -</details> - -<details> - <summary>all server requests</summary> - Similar to unit and component tests, when running component tests, the backend may not be reachable. - Therefore all outgoing requests need to be mocked. -</details> - -<details> - <summary>asynchronous background operations that are not perceivable on the page</summary> - Background operations that affect the page need to be tested on this level. - All other background operations cannot be stopped or waited on, so they will continue running in the following tests and cause side effects. -</details> - -### What *not* to mock in integration tests - -<details> - <summary>DOM</summary> - Testing on the real DOM ensures our components work in the environment they are meant for. - Part of this will be delegated to <a href="https://gitlab.com/gitlab-org/quality/team-tasks/issues/45">cross-browser testing</a>. -</details> - -<details> - <summary>properties or state of components</summary> - On this level, all tests can only perform actions a user would do. - For example to change the state of a component, a click event would be fired. -</details> - -<details> - <summary>Vuex stores</summary> - When testing the frontend code of a page as a whole, the interaction between Vue components and Vuex stores is covered as well. -</details> - -## Feature tests - -In contrast to [frontend integration tests](#frontend-integration-tests), feature tests make requests against the real backend instead of using fixtures. -This also implies that database queries are executed which makes this category significantly slower. - -See also the [RSpec testing guidelines](../../testing_guide/best_practices.md#rspec). - -### When to use feature tests - -- use cases that require a backend and cannot be tested using fixtures -- behavior that is not part of a page bundle but defined globally - -### Relevant notes - -A `:js` flag is added to the test to make sure the full environment is loaded. - -``` -scenario 'successfully', :js do - sign_in(create(:admin)) -end -``` - -The steps of each test are written using capybara methods ([documentation](https://www.rubydoc.info/gems/capybara)). - -Bear in mind <abbr title="XMLHttpRequest">XHR</abbr> calls might require you to use `wait_for_requests` in between steps, like so: - -```rspec -find('.form-control').native.send_keys(:enter) - -wait_for_requests - -expect(page).not_to have_selector('.card') -``` - -## Test helpers - -### Vuex Helper: `testAction` - -We have a helper available to make testing actions easier, as per [official documentation](https://vuex.vuejs.org/guide/testing.html): - -``` -testAction( - actions.actionName, // action - { }, // params to be passed to action - state, // state - [ - { type: types.MUTATION}, - { type: types.MUTATION_1, payload: {}}, - ], // mutations committed - [ - { type: 'actionName', payload: {}}, - { type: 'actionName1', payload: {}}, - ] // actions dispatched - done, -); -``` - -Check an example in [spec/javascripts/ide/stores/actions_spec.jsspec/javascripts/ide/stores/actions_spec.js](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/spec/javascripts/ide/stores/actions_spec.js). - -### Vue Helper: `mountComponent` - -To make mounting a Vue component easier and more readable, we have a few helpers available in `spec/helpers/vue_mount_component_helper`. - -- `createComponentWithStore` -- `mountComponentWithStore` - -Examples of usage: - -``` -beforeEach(() => { - vm = createComponentWithStore(Component, store); - - vm.$store.state.currentBranchId = 'master'; - - vm.$mount(); -}, -``` - -``` -beforeEach(() => { - vm = mountComponentWithStore(Component, { - el: '#dummy-element', - store, - props: { badge }, - }); -}, -``` - -Don't forget to clean up: - -``` -afterEach(() => { - vm.$destroy(); -}); -``` - -## Testing with older browsers - -Some regressions only affect a specific browser version. We can install and test in particular browsers with either Firefox or Browserstack using the following steps: - -### Browserstack - -[Browserstack](https://www.browserstack.com/) allows you to test more than 1200 mobile devices and browsers. -You can use it directly through the [live app](https://www.browserstack.com/live) or you can install the [chrome extension](https://chrome.google.com/webstore/detail/browserstack/nkihdmlheodkdfojglpcjjmioefjahjb) for easy access. -You can find the credentials on 1Password, under `frontendteam@gitlab.com`. - -### Firefox - -#### macOS - -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. -1. Select your preferred language, you will find the dmg package inside, download it. -1. Drag and drop the application to any other folder but the `Applications` folder. -1. Rename the application to something like `Firefox_Old`. -1. Move the application to the `Applications` folder. -1. Open up a terminal and run `/Applications/Firefox_Old.app/Contents/MacOS/firefox-bin -profilemanager` to create a new profile specific to that Firefox version. -1. Once the profile has been created, quit the app, and run it again like normal. You now have a working older Firefox version. diff --git a/doc/development/new_fe_guide/modules/dirty_submit.md b/doc/development/new_fe_guide/modules/dirty_submit.md index 6c03958b463..217743ea395 100644 --- a/doc/development/new_fe_guide/modules/dirty_submit.md +++ b/doc/development/new_fe_guide/modules/dirty_submit.md @@ -1,7 +1,6 @@ # Dirty Submit -> [Introduced][ce-21115] in GitLab 11.3. -> [dirty_submit][dirty-submit] +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/21115) in GitLab 11.3. ## Summary @@ -9,6 +8,9 @@ Prevent submitting forms with no changes. Currently handles `input`, `textarea` and `select` elements. +Also, see [the code](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/app/assets/javascripts/dirty_submit/) +within the GitLab project. + ## Usage ```js @@ -18,6 +20,3 @@ new DirtySubmitForm(document.querySelector('form')); // or new DirtySubmitForm(document.querySelectorAll('form')); ``` - -[ce-21115]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/21115 -[dirty-submit]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/app/assets/javascripts/dirty_submit/
\ No newline at end of file diff --git a/doc/development/new_fe_guide/style/javascript.md b/doc/development/new_fe_guide/style/javascript.md index 802ebd12d92..b742d567f41 100644 --- a/doc/development/new_fe_guide/style/javascript.md +++ b/doc/development/new_fe_guide/style/javascript.md @@ -192,4 +192,4 @@ rules only if you are invoking/instantiating existing code modules. - [class-method-use-this](http://eslint.org/docs/rules/class-methods-use-this) > Note: Disable these rules on a per line basis. This makes it easier to refactor - in the future. E.g. use `eslint-disable-next-line` or `eslint-disable-line`. +> in the future. E.g. use `eslint-disable-next-line` or `eslint-disable-line`. diff --git a/doc/development/query_recorder.md b/doc/development/query_recorder.md index a6b60149ea4..3787e2ef187 100644 --- a/doc/development/query_recorder.md +++ b/doc/development/query_recorder.md @@ -36,6 +36,13 @@ it "avoids N+1 database queries" do end ``` +## Use request specs instead of controller specs + +Use a [request spec](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/spec/requests) when writing a N+1 test on the controller level. + +Controller specs should not be used to write N+1 tests as the controller is only initialized once per example. +This could lead to false successes where subsequent "requests" could have queries reduced (e.g. because of memoization). + ## Finding the source of the query It may be useful to identify the source of the queries by looking at the call backtrace. diff --git a/doc/development/rake_tasks.md b/doc/development/rake_tasks.md index c97e179910b..e9d6cfe00b2 100644 --- a/doc/development/rake_tasks.md +++ b/doc/development/rake_tasks.md @@ -9,7 +9,7 @@ bundle exec rake setup ``` The `setup` task is an alias for `gitlab:setup`. -This tasks calls `db:reset` to create the database, calls `add_limits_mysql` that adds limits to the database schema in case of a MySQL database and finally it calls `db:seed_fu` to seed the database. +This tasks calls `db:reset` to create the database, and calls `db:seed_fu` to seed the database. Note: `db:setup` calls `db:seed` but this does nothing. ### Seeding issues for all or a given project diff --git a/doc/development/repository_mirroring.md b/doc/development/repository_mirroring.md index f8c33ff2b85..dc51bf80e92 100644 --- a/doc/development/repository_mirroring.md +++ b/doc/development/repository_mirroring.md @@ -8,4 +8,4 @@ In December 2018, Tiago Botelho hosted a [Deep Dive] on GitLab's [Pull Repositor [Pull Repository Mirroring functionality]: ../workflow/repository_mirroring.md#pulling-from-a-remote-repository-starter [recording on YouTube]: https://www.youtube.com/watch?v=sSZq0fpdY-Y [Google Slides]: https://docs.google.com/presentation/d/17BTT6M6RyNRckV4wTt-dr07nIfBvD325_xVBoLtSoPM/edit?usp=sharing -[PDF]: https://gitlab.com/gitlab-org/create-stage/uploads/8693404888a941fd851f8a8ecdec9675/Gitlab_Create_-_Pull_Mirroring_Deep_Dive.pdf
\ No newline at end of file +[PDF]: https://gitlab.com/gitlab-org/create-stage/uploads/8693404888a941fd851f8a8ecdec9675/Gitlab_Create_-_Pull_Mirroring_Deep_Dive.pdf diff --git a/doc/development/sha1_as_binary.md b/doc/development/sha1_as_binary.md index 3151cc29bbc..6c4252ec634 100644 --- a/doc/development/sha1_as_binary.md +++ b/doc/development/sha1_as_binary.md @@ -2,7 +2,7 @@ Storing SHA1 hashes as strings is not very space efficient. A SHA1 as a string requires at least 40 bytes, an additional byte to store the encoding, and -perhaps more space depending on the internals of PostgreSQL and MySQL. +perhaps more space depending on the internals of PostgreSQL. On the other hand, if one were to store a SHA1 as binary one would only need 20 bytes for the actual SHA1, and 1 or 4 bytes of additional space (again depending diff --git a/doc/development/shell_scripting_guide/index.md b/doc/development/shell_scripting_guide/index.md index ae7f2154682..0809f8b1a0a 100644 --- a/doc/development/shell_scripting_guide/index.md +++ b/doc/development/shell_scripting_guide/index.md @@ -24,7 +24,8 @@ Having said all of the above, we recommend staying away from shell scripts as much as possible. A language like Ruby or Python (if required for consistency with codebases that we leverage) is almost always a better choice. The high-level interpreted languages have more readable syntax, offer much more -mature capabilities for unit-testing, linting, and error reporting. +mature capabilities for unit-testing, linting, and error reporting. + Use shell scripts only if there's a strong restriction on project's dependencies size or any other requirements that are more important in a particular case. @@ -48,12 +49,12 @@ that is: This section describes the tools that should be made a mandatory part of a project's CI pipeline if it contains shell scripts. These tools -automate shell code formatting, checking for errors or vulnerabilities, etc. +automate shell code formatting, checking for errors or vulnerabilities, etc. ### Linting We're using the [ShellCheck](https://www.shellcheck.net/) utility in its default configuration to lint our -shell scripts. +shell scripts. All projects with shell scripts should use this GitLab CI/CD job: @@ -98,7 +99,7 @@ NOTE: **Note:** This is a work in progress. It is an [ongoing effort](https://gitlab.com/gitlab-org/gitlab-ce/issues/64016) to evaluate different tools for the -automated testing of shell scripts (like [BATS](https://github.com/sstephenson/bats)). +automated testing of shell scripts (like [BATS](https://github.com/sstephenson/bats)). ## Code Review diff --git a/doc/development/sql.md b/doc/development/sql.md index a256fd46c09..2584dcfb4ca 100644 --- a/doc/development/sql.md +++ b/doc/development/sql.md @@ -15,14 +15,11 @@ FROM issues WHERE title LIKE 'WIP:%'; ``` -On PostgreSQL the `LIKE` statement is case-sensitive. On MySQL this depends on -the case-sensitivity of the collation, which is usually case-insensitive. To -perform a case-insensitive `LIKE` on PostgreSQL you have to use `ILIKE` instead. -This statement in turn isn't supported on MySQL. +On PostgreSQL the `LIKE` statement is case-sensitive. To perform a case-insensitive +`LIKE` you have to use `ILIKE` instead. -To work around this problem you should write `LIKE` queries using Arel instead -of raw SQL fragments as Arel automatically uses `ILIKE` on PostgreSQL and `LIKE` -on MySQL. This means that instead of this: +To handle this automatically you should use `LIKE` queries using Arel instead +of raw SQL fragments, as Arel automatically uses `ILIKE` on PostgreSQL. ```ruby Issue.where('title LIKE ?', 'WIP:%') @@ -45,7 +42,7 @@ table = Issue.arel_table Issue.where(table[:title].matches('WIP:%').or(table[:foo].matches('WIP:%'))) ``` -For PostgreSQL this produces: +On PostgreSQL, this produces: ```sql SELECT * @@ -53,18 +50,10 @@ FROM issues WHERE (title ILIKE 'WIP:%' OR foo ILIKE 'WIP:%') ``` -In turn for MySQL this produces: - -```sql -SELECT * -FROM issues -WHERE (title LIKE 'WIP:%' OR foo LIKE 'WIP:%') -``` - ## LIKE & Indexes -Neither PostgreSQL nor MySQL use any indexes when using `LIKE` / `ILIKE` with a -wildcard at the start. For example, this will not use any indexes: +PostgreSQL won't use any indexes when using `LIKE` / `ILIKE` with a wildcard at +the start. For example, this will not use any indexes: ```sql SELECT * @@ -75,9 +64,8 @@ WHERE title ILIKE '%WIP:%'; Because the value for `ILIKE` starts with a wildcard the database is not able to use an index as it doesn't know where to start scanning the indexes. -MySQL provides no known solution to this problem. Luckily PostgreSQL _does_ -provide a solution: trigram GIN indexes. These indexes can be created as -follows: +Luckily, PostgreSQL _does_ provide a solution: trigram GIN indexes. These +indexes can be created as follows: ```sql CREATE INDEX [CONCURRENTLY] index_name_here diff --git a/doc/development/testing_guide/best_practices.md b/doc/development/testing_guide/best_practices.md index 448d9fd01c4..f30a83a4c71 100644 --- a/doc/development/testing_guide/best_practices.md +++ b/doc/development/testing_guide/best_practices.md @@ -15,16 +15,6 @@ manifest themselves within our code. When designing our tests, take time to revi our test design. We can find some helpful heuristics documented in the Handbook in the [Test Design](https://about.gitlab.com/handbook/engineering/quality/guidelines/test-engineering/test-design/) section. -## Run tests against MySQL - -By default, tests are only run against PostgreSQL, but you can run them on -demand against MySQL by following one of the following conventions: - -| Convention | Valid example | -|:----------------------|:-----------------------------| -| Include `mysql` in your branch name | `enhance-mysql-support` | -| Include `[run mysql]` in your commit message | `Fix MySQL support<br><br>[run mysql]` | - ## Test speed GitLab has a massive test suite that, without [parallelization], can take hours @@ -70,6 +60,7 @@ bundle exec rspec spec/[path]/[to]/[spec].rb - On `before` and `after` hooks, prefer it scoped to `:context` over `:all` - When using `evaluate_script("$('.js-foo').testSomething()")` (or `execute_script`) which acts on a given element, use a Capyabara matcher beforehand (e.g. `find('.js-foo')`) to ensure the element actually exists. +- Use `focus: true` to isolate parts of the specs you want to run. [four-phase-test]: https://robots.thoughtbot.com/four-phase-test @@ -454,6 +445,19 @@ complexity of RSpec expectations.They should be placed under a certain type of specs only (e.g. features, requests etc.) but shouldn't be if they apply to multiple type of specs. +#### `be_like_time` + +Time returned from a database can differ in precision from time objects +in Ruby, so we need flexible tolerances when comparing in specs. We can +use `be_like_time` to compare that times are within one second of each +other. + +Example: + +```ruby +expect(metrics.merged_at).to be_like_time(time) +``` + #### `have_gitlab_http_status` Prefer `have_gitlab_http_status` over `have_http_status` because the former diff --git a/doc/development/testing_guide/ci.md b/doc/development/testing_guide/ci.md index 87d48726268..d9f66a827de 100644 --- a/doc/development/testing_guide/ci.md +++ b/doc/development/testing_guide/ci.md @@ -39,7 +39,6 @@ slowest test files and try to improve them. ## CI setup -- On CE and EE, the test suite runs both PostgreSQL and MySQL. - Rails logging to `log/test.log` is disabled by default in CI [for performance reasons][logging]. To override this setting, provide the `RAILS_ENABLE_TEST_LOG` environment variable. diff --git a/doc/development/testing_guide/end_to_end/index.md b/doc/development/testing_guide/end_to_end/index.md index d6b944a3e74..3ae3ce183d9 100644 --- a/doc/development/testing_guide/end_to_end/index.md +++ b/doc/development/testing_guide/end_to_end/index.md @@ -45,11 +45,11 @@ Results are reported in the `#qa-staging` Slack channel. ### Testing code in merge requests -#### Using the `package-and-qa` job +#### Using the `package-and-qa-manual` job It is possible to run end-to-end tests for a merge request, eventually being run in a pipeline in the [`gitlab-qa`](https://gitlab.com/gitlab-org/gitlab-qa/) project, -by triggering the `package-and-qa` manual action in the `test` stage (not +by triggering the `package-and-qa-manual` manual action in the `test` stage (not available for forks). **This runs end-to-end tests against a custom Omnibus package built from your @@ -71,7 +71,7 @@ graph LR B2[`Trigger-qa` stage<br>`Trigger:qa-test` job] -.->|2. Triggers a gitlab-qa pipeline and wait for it to be done| A3 subgraph "gitlab-ce/ee pipeline" - A1[`test` stage<br>`package-and-qa` job] + A1[`test` stage<br>`package-and-qa-manual` job] end subgraph "omnibus-gitlab pipeline" @@ -79,7 +79,7 @@ subgraph "omnibus-gitlab pipeline" end subgraph "gitlab-qa pipeline" - A3>QA jobs run] -.->|3. Reports back the pipeline result to the `package-and-qa` job<br>and post the result on the original commit tested| A1 + A3>QA jobs run] -.->|3. Reports back the pipeline result to the `package-and-qa-manual` job<br>and post the result on the original commit tested| A1 end ``` diff --git a/doc/development/testing_guide/end_to_end/page_objects.md b/doc/development/testing_guide/end_to_end/page_objects.md index 47e58a425fd..850ea6b60ac 100644 --- a/doc/development/testing_guide/end_to_end/page_objects.md +++ b/doc/development/testing_guide/end_to_end/page_objects.md @@ -40,7 +40,7 @@ the time it would take to build packages and test everything. That is why when someone changes `t.text_field :login` to `t.text_field :username` in the _new session_ view we won't know about this change until our GitLab QA nightly pipeline fails, or until someone triggers -`package-and-qa` action in their merge request. +`package-and-qa-manual` action in their merge request. Obviously such a change would break all tests. We call this problem a _fragile tests problem_. diff --git a/doc/development/testing_guide/flaky_tests.md b/doc/development/testing_guide/flaky_tests.md index 931cbc51cae..eb0bf6fc563 100644 --- a/doc/development/testing_guide/flaky_tests.md +++ b/doc/development/testing_guide/flaky_tests.md @@ -35,8 +35,8 @@ Once a test is in quarantine, there are 3 choices: 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) +- `rspec-pg-quarantine` (CE & EE) +- `rspec-pg-quarantine-ee` (EE only) ## Automatic retries and flaky tests detection diff --git a/doc/development/testing_guide/frontend_testing.md b/doc/development/testing_guide/frontend_testing.md index 2985278cc92..7dc89a3fcdb 100644 --- a/doc/development/testing_guide/frontend_testing.md +++ b/doc/development/testing_guide/frontend_testing.md @@ -232,7 +232,7 @@ module. GitLab has a custom `spyOnDependency` method which utilizes [babel-plugin-rewire](https://github.com/speedskater/babel-plugin-rewire) to achieve this. It can be used like so: -```js +```javascript // my_module.js import { visitUrl } from '~/lib/utils/url_utility'; @@ -241,7 +241,7 @@ export default function doSomething() { } ``` -```js +```javascript // my_module_spec.js import doSomething from '~/my_module'; @@ -593,6 +593,517 @@ end [capybara]: https://github.com/teamcapybara/capybara [jasmine]: https://jasmine.github.io/ +## Overview of Frontend Testing Levels + +Tests relevant for frontend development can be found at the following places: + +- `spec/javascripts/` which are run by Karma (command: `yarn karma`) and contain + - [frontend unit tests](#frontend-unit-tests) + - [frontend component tests](#frontend-component-tests) + - [frontend integration tests](#frontend-integration-tests) +- `spec/frontend/` which are run by Jest (command: `yarn jest`) and contain + - [frontend unit tests](#frontend-unit-tests) + - [frontend component tests](#frontend-component-tests) + - [frontend integration tests](#frontend-integration-tests) +- `spec/features/` which are run by RSpec and contain + - [feature tests](#feature-tests) + +All tests in `spec/javascripts/` will eventually be migrated to `spec/frontend/` (see also [#52483](https://gitlab.com/gitlab-org/gitlab-ce/issues/52483)). + +In addition, there used to be feature tests in `features/`, run by Spinach. +These were removed from the codebase in May 2018 ([#23036](https://gitlab.com/gitlab-org/gitlab-ce/issues/23036)). + +See also [Notes on testing Vue components](../fe_guide/vue.html#testing-vue-components). + +### Frontend unit tests + +Unit tests are on the lowest abstraction level and typically test functionality that is not directly perceivable by a user. + +```mermaid +graph RL + plain[Plain JavaScript]; + Vue[Vue Components]; + feature-flags[Feature Flags]; + license-checks[License Checks]; + + plain---Vuex; + plain---GraphQL; + Vue---plain; + Vue---Vuex; + Vue---GraphQL; + browser---plain; + browser---Vue; + plain---backend; + Vuex---backend; + GraphQL---backend; + Vue---backend; + backend---database; + backend---feature-flags; + backend---license-checks; + + class plain tested; + class Vuex tested; + + classDef node color:#909090,fill:#f0f0f0,stroke-width:2px,stroke:#909090 + classDef label stroke-width:0; + classDef tested color:#000000,fill:#a0c0ff,stroke:#6666ff,stroke-width:2px,stroke-dasharray: 5, 5; + + subgraph " " + tested; + mocked; + class tested tested; + end +``` + +#### When to use unit tests + +<details> + <summary>exported functions and classes</summary> + Anything that is exported can be reused at various places in a way you have no control over. + Therefore it is necessary to document the expected behavior of the public interface with tests. +</details> + +<details> + <summary>Vuex actions</summary> + Any Vuex action needs to work in a consistent way independent of the component it is triggered from. +</details> + +<details> + <summary>Vuex mutations</summary> + For complex Vuex mutations it helps to identify the source of a problem by separating the tests from other parts of the Vuex store. +</details> + +#### When *not* to use unit tests + +<details> + <summary>non-exported functions or classes</summary> + Anything that is not exported from a module can be considered private or an implementation detail and doesn't need to be tested. +</details> + +<details> + <summary>constants</summary> + Testing the value of a constant would mean to copy it. + This results in extra effort without additional confidence that the value is correct. +</details> + +<details> + <summary>Vue components</summary> + Computed properties, methods, and lifecycle hooks can be considered an implementation detail of components and don't need to be tested. + They are implicitly covered by component tests. + The <a href="https://vue-test-utils.vuejs.org/guides/#getting-started">official Vue guidelines</a> suggest the same. +</details> + +#### What to mock in unit tests + +<details> + <summary>state of the class under test</summary> + Modifying the state of the class under test directly rather than using methods of the class avoids side-effects in test setup. +</details> + +<details> + <summary>other exported classes</summary> + Every class needs to be tested in isolation to prevent test scenarios from growing exponentially. +</details> + +<details> + <summary>single DOM elements if passed as parameters</summary> + For tests that only operate on single DOM elements rather than a whole page, creating these elements is cheaper than loading a whole HTML fixture. +</details> + +<details> + <summary>all server requests</summary> + When running frontend unit tests, the backend may not be reachable. + Therefore all outgoing requests need to be mocked. +</details> + +<details> + <summary>asynchronous background operations</summary> + Background operations cannot be stopped or waited on, so they will continue running in the following tests and cause side effects. +</details> + +#### What *not* to mock in unit tests + +<details> + <summary>non-exported functions or classes</summary> + Everything that is not exported can be considered private to the module and will be implicitly tested via the exported classes / functions. +</details> + +<details> + <summary>methods of the class under test</summary> + By mocking methods of the class under test, the mocks will be tested and not the real methods. +</details> + +<details> + <summary>utility functions (pure functions, or those that only modify parameters)</summary> + If a function has no side effects because it has no state, it is safe to not mock it in tests. +</details> + +<details> + <summary>full HTML pages</summary> + Loading the HTML of a full page slows down tests, so it should be avoided in unit tests. +</details> + +### Frontend component tests + +Component tests cover the state of a single component that is perceivable by a user depending on external signals such as user input, events fired from other components, or application state. + +```mermaid +graph RL + plain[Plain JavaScript]; + Vue[Vue Components]; + feature-flags[Feature Flags]; + license-checks[License Checks]; + + plain---Vuex; + plain---GraphQL; + Vue---plain; + Vue---Vuex; + Vue---GraphQL; + browser---plain; + browser---Vue; + plain---backend; + Vuex---backend; + GraphQL---backend; + Vue---backend; + backend---database; + backend---feature-flags; + backend---license-checks; + + class Vue tested; + + classDef node color:#909090,fill:#f0f0f0,stroke-width:2px,stroke:#909090 + classDef label stroke-width:0; + classDef tested color:#000000,fill:#a0c0ff,stroke:#6666ff,stroke-width:2px,stroke-dasharray: 5, 5; + + subgraph " " + tested; + mocked; + class tested tested; + end +``` + +#### When to use component tests + +- Vue components + +#### When *not* to use component tests + +<details> + <summary>Vue applications</summary> + Vue applications may contain many components. + Testing them on a component level requires too much effort. + Therefore they are tested on frontend integration level. +</details> + +<details> + <summary>HAML templates</summary> + HAML templates contain only Markup and no frontend-side logic. + Therefore they are not complete components. +</details> + +#### What to mock in component tests + +<details> + <summary>DOM</summary> + Operating on the real DOM is significantly slower than on the virtual DOM. +</details> + +<details> + <summary>properties and state of the component under test</summary> + Similarly to testing classes, modifying the properties directly (rather than relying on methods of the component) avoids side-effects. +</details> + +<details> + <summary>Vuex store</summary> + To avoid side effects and keep component tests simple, Vuex stores are replaced with mocks. +</details> + +<details> + <summary>all server requests</summary> + Similar to unit tests, when running component tests, the backend may not be reachable. + Therefore all outgoing requests need to be mocked. +</details> + +<details> + <summary>asynchronous background operations</summary> + Similar to unit tests, background operations cannot be stopped or waited on, so they will continue running in the following tests and cause side effects. +</details> + +<details> + <summary>child components</summary> + Every component is tested individually, so child components are mocked. + See also <a href="https://vue-test-utils.vuejs.org/api/#shallowmount">shallowMount()</a> +</details> + +#### What *not* to mock in component tests + +<details> + <summary>methods or computed properties of the component under test</summary> + By mocking part of the component under test, the mocks will be tested and not the real component. +</details> + +<details> + <summary>functions and classes independent from Vue</summary> + All plain JavaScript code is already covered by unit tests and needs not to be mocked in component tests. +</details> + +### Frontend integration tests + +Integration tests cover the interaction between all components on a single page. +Their abstraction level is comparable to how a user would interact with the UI. + +```mermaid +graph RL + plain[Plain JavaScript]; + Vue[Vue Components]; + feature-flags[Feature Flags]; + license-checks[License Checks]; + + plain---Vuex; + plain---GraphQL; + Vue---plain; + Vue---Vuex; + Vue---GraphQL; + browser---plain; + browser---Vue; + plain---backend; + Vuex---backend; + GraphQL---backend; + Vue---backend; + backend---database; + backend---feature-flags; + backend---license-checks; + + class plain tested; + class Vue tested; + class Vuex tested; + class GraphQL tested; + class browser tested; + linkStyle 0,1,2,3,4,5,6 stroke:#6666ff,stroke-width:2px,stroke-dasharray: 5, 5; + + classDef node color:#909090,fill:#f0f0f0,stroke-width:2px,stroke:#909090 + classDef label stroke-width:0; + classDef tested color:#000000,fill:#a0c0ff,stroke:#6666ff,stroke-width:2px,stroke-dasharray: 5, 5; + + subgraph " " + tested; + mocked; + class tested tested; + end +``` + +#### When to use integration tests + +<details> + <summary>page bundles (<code>index.js</code> files in <code>app/assets/javascripts/pages/</code>)</summary> + Testing the page bundles ensures the corresponding frontend components integrate well. +</details> + +<details> + <summary>Vue applications outside of page bundles</summary> + Testing Vue applications as a whole ensures the corresponding frontend components integrate well. +</details> + +#### What to mock in integration tests + +<details> + <summary>HAML views (use fixtures instead)</summary> + Rendering HAML views requires a Rails environment including a running database which we cannot rely on in frontend tests. +</details> + +<details> + <summary>all server requests</summary> + Similar to unit and component tests, when running component tests, the backend may not be reachable. + Therefore all outgoing requests need to be mocked. +</details> + +<details> + <summary>asynchronous background operations that are not perceivable on the page</summary> + Background operations that affect the page need to be tested on this level. + All other background operations cannot be stopped or waited on, so they will continue running in the following tests and cause side effects. +</details> + +#### What *not* to mock in integration tests + +<details> + <summary>DOM</summary> + Testing on the real DOM ensures our components work in the environment they are meant for. + Part of this will be delegated to <a href="https://gitlab.com/gitlab-org/quality/team-tasks/issues/45">cross-browser testing</a>. +</details> + +<details> + <summary>properties or state of components</summary> + On this level, all tests can only perform actions a user would do. + For example to change the state of a component, a click event would be fired. +</details> + +<details> + <summary>Vuex stores</summary> + When testing the frontend code of a page as a whole, the interaction between Vue components and Vuex stores is covered as well. +</details> + +### Feature tests + +In contrast to [frontend integration tests](#frontend-integration-tests), feature tests make requests against the real backend instead of using fixtures. +This also implies that database queries are executed which makes this category significantly slower. + +See also the [RSpec testing guidelines](../testing_guide/best_practices.md#rspec). + +```mermaid +graph RL + plain[Plain JavaScript]; + Vue[Vue Components]; + feature-flags[Feature Flags]; + license-checks[License Checks]; + + plain---Vuex; + plain---GraphQL; + Vue---plain; + Vue---Vuex; + Vue---GraphQL; + browser---plain; + browser---Vue; + plain---backend; + Vuex---backend; + GraphQL---backend; + Vue---backend; + backend---database; + backend---feature-flags; + backend---license-checks; + + class backend tested; + class plain tested; + class Vue tested; + class Vuex tested; + class GraphQL tested; + class browser tested; + linkStyle 0,1,2,3,4,5,6,7,8,9,10 stroke:#6666ff,stroke-width:2px,stroke-dasharray: 5, 5; + + classDef node color:#909090,fill:#f0f0f0,stroke-width:2px,stroke:#909090 + classDef label stroke-width:0; + classDef tested color:#000000,fill:#a0c0ff,stroke:#6666ff,stroke-width:2px,stroke-dasharray: 5, 5; + + subgraph " " + tested; + mocked; + class tested tested; + end +``` + +#### When to use feature tests + +- Use cases that require a backend and cannot be tested using fixtures. +- Behavior that is not part of a page bundle but defined globally. + +#### Relevant notes + +A `:js` flag is added to the test to make sure the full environment is loaded. + +```ruby +scenario 'successfully', :js do + sign_in(create(:admin)) +end +``` + +The steps of each test are written using capybara methods ([documentation](https://www.rubydoc.info/gems/capybara)). + +Bear in mind <abbr title="XMLHttpRequest">XHR</abbr> calls might require you to use `wait_for_requests` in between steps, like so: + +```ruby +find('.form-control').native.send_keys(:enter) + +wait_for_requests + +expect(page).not_to have_selector('.card') +``` + +## Test helpers + +### Vuex Helper: `testAction` + +We have a helper available to make testing actions easier, as per [official documentation](https://vuex.vuejs.org/guide/testing.html): + +```javascript +testAction( + actions.actionName, // action + { }, // params to be passed to action + state, // state + [ + { type: types.MUTATION}, + { type: types.MUTATION_1, payload: {}}, + ], // mutations committed + [ + { type: 'actionName', payload: {}}, + { type: 'actionName1', payload: {}}, + ] // actions dispatched + done, +); +``` + +Check an example in [spec/javascripts/ide/stores/actions_spec.jsspec/javascripts/ide/stores/actions_spec.js](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/spec/javascripts/ide/stores/actions_spec.js). + +### Vue Helper: `mountComponent` + +To make mounting a Vue component easier and more readable, we have a few helpers available in `spec/helpers/vue_mount_component_helper`: + +- `createComponentWithStore` +- `mountComponentWithStore` + +Examples of usage: + +```javascript +beforeEach(() => { + vm = createComponentWithStore(Component, store); + + vm.$store.state.currentBranchId = 'master'; + + vm.$mount(); +}); +``` + +```javascript +beforeEach(() => { + vm = mountComponentWithStore(Component, { + el: '#dummy-element', + store, + props: { badge }, + }); +}); +``` + +Don't forget to clean up: + +```javascript +afterEach(() => { + vm.$destroy(); +}); +``` + +## Testing with older browsers + +Some regressions only affect a specific browser version. We can install and test in particular browsers with either Firefox or Browserstack using the following steps: + +### Browserstack + +[Browserstack](https://www.browserstack.com/) allows you to test more than 1200 mobile devices and browsers. +You can use it directly through the [live app](https://www.browserstack.com/live) or you can install the [chrome extension](https://chrome.google.com/webstore/detail/browserstack/nkihdmlheodkdfojglpcjjmioefjahjb) for easy access. +You can find the credentials on 1Password, under `frontendteam@gitlab.com`. + +### Firefox + +#### macOS + +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. +1. Select your preferred language, you will find the dmg package inside, download it. +1. Drag and drop the application to any other folder but the `Applications` folder. +1. Rename the application to something like `Firefox_Old`. +1. Move the application to the `Applications` folder. +1. Open up a terminal and run `/Applications/Firefox_Old.app/Contents/MacOS/firefox-bin -profilemanager` to create a new profile specific to that Firefox version. +1. Once the profile has been created, quit the app, and run it again like normal. You now have a working older Firefox version. + --- [Return to Testing documentation](index.md) diff --git a/doc/development/testing_guide/review_apps.md b/doc/development/testing_guide/review_apps.md index 7843fc4c874..11449712a04 100644 --- a/doc/development/testing_guide/review_apps.md +++ b/doc/development/testing_guide/review_apps.md @@ -255,8 +255,8 @@ that a machine will hit the "too many mount points" problem in the future. thousands of unused Docker images.** > We have to start somewhere and improve later. Also, we're using the - CNG-mirror project to store these Docker images so that we can just wipe out - the registry at some point, and use a new fresh, empty one. + > CNG-mirror project to store these Docker images so that we can just wipe out + > the registry at some point, and use a new fresh, empty one. **How do we secure this from abuse? Apps are open to the world so we need to find a way to limit it to only us.** diff --git a/doc/development/testing_guide/testing_levels.md b/doc/development/testing_guide/testing_levels.md index e1ce4d3b7d1..0090c84cbf0 100644 --- a/doc/development/testing_guide/testing_levels.md +++ b/doc/development/testing_guide/testing_levels.md @@ -63,10 +63,9 @@ They're useful to test permissions, redirections, what view is rendered etc. | Code path | Tests path | Testing engine | Notes | | --------- | ---------- | -------------- | ----- | -| `app/controllers/` | `spec/controllers/` | RSpec | | +| `app/controllers/` | `spec/controllers/` | RSpec | For N+1 tests, use [request specs](../query_recorder.md#use-request-specs-instead-of-controller-specs) | | `app/mailers/` | `spec/mailers/` | RSpec | | | `lib/api/` | `spec/requests/api/` | RSpec | | -| `lib/ci/api/` | `spec/requests/ci/api/` | RSpec | | | `app/assets/javascripts/` | `spec/javascripts/`, `spec/frontend/` | Karma & Jest | More details in the [Frontend Testing guide](frontend_testing.md) section. | ### About controller tests diff --git a/doc/development/understanding_explain_plans.md b/doc/development/understanding_explain_plans.md index 11aafd7b639..7c926c83a36 100644 --- a/doc/development/understanding_explain_plans.md +++ b/doc/development/understanding_explain_plans.md @@ -199,7 +199,7 @@ more common ones here. A full list of all the available nodes and their descriptions can be found in the [PostgreSQL source file -"plannodes.h"](https://github.com/postgres/postgres/blob/master/src/include/nodes/plannodes.h) +"plannodes.h"](https://gitlab.com/postgres/postgres/blob/master/src/include/nodes/plannodes.h) ### Seq Scan @@ -224,7 +224,7 @@ used when we would read too much data from an index scan, but too little to perform a sequential scan. A bitmap scan uses what is known as a [bitmap index](https://en.wikipedia.org/wiki/Bitmap_index) to perform its work. -The [source code of PostgreSQL](https://github.com/postgres/postgres/blob/1c2cb2744bf3d8ad751cd5cf3b347f10f48492b3/src/include/nodes/plannodes.h#L446-L457) +The [source code of PostgreSQL](https://gitlab.com/postgres/postgres/blob/REL_11_STABLE/src/include/nodes/plannodes.h#L441) states the following on bitmap scans: > Bitmap Index Scan delivers a bitmap of potential tuple locations; it does not diff --git a/doc/development/uploads.md b/doc/development/uploads.md new file mode 100644 index 00000000000..234539bb673 --- /dev/null +++ b/doc/development/uploads.md @@ -0,0 +1,271 @@ +# Uploads development documentation + +[GitLab Workhorse](https://gitlab.com/gitlab-org/gitlab-workhorse) has special rules for handling uploads. +To prevent occupying a ruby process on I/O operations, we process the upload in workhorse, where is cheaper. +This process can also directly upload to object storage. + +## The problem description + +The following graph explains machine boundaries in a scalable GitLab installation. Without any workhorse optimization in place, we can expect incoming requests to follow the numbers on the arrows. + +```mermaid +graph TB + subgraph "load balancers" + LB(HA Proxy) + end + + subgraph "Shared storage" + nfs(NFS) + end + + subgraph "redis cluster" + r(persisted redis) + end + LB-- 1 -->workhorse + + subgraph "web or API fleet" + workhorse-- 2 -->rails + end + rails-- "3 (write files)" -->nfs + rails-- "4 (schedule a job)" -->r + + subgraph sidekiq + s(sidekiq) + end + s-- "5 (fetch a job)" -->r + s-- "6 (read files)" -->nfs +``` + +We have three challenges here: performance, availability, and scalability. + +### Performance + +Rails process are expensive in terms of both CPU and memory. Ruby [global interpreter lock](https://en.wikipedia.org/wiki/Global_interpreter_lock) adds to cost too because the ruby process will spend time on I/O operations on step 3 causing incoming requests to pile up. + +In order to improve this, [workhorse disk acceleration](#workhorse-disk-acceleration) was implemented. With this, Rails no longer deals with writing uploaded files to disk. + +```mermaid +graph TB + subgraph "load balancers" + LB(HA Proxy) + end + + subgraph "Shared storage" + nfs(NFS) + end + + subgraph "redis cluster" + r(persisted redis) + end + LB-- 1 -->workhorse + + subgraph "web or API fleet" + workhorse-- "3 (without files)" -->rails + end + workhorse -- "2 (write files)" -->nfs + rails-- "4 (schedule a job)" -->r + + subgraph sidekiq + s(sidekiq) + end + s-- "5 (fetch a job)" -->r + s-- "6 (read files)" -->nfs +``` + +### Availability + +There's also an availability problem in this setup, NFS is a [single point of failure](https://en.wikipedia.org/wiki/Single_point_of_failure). + +To address this problem an HA object storage can be used and it's supported by [workhorse object storage acceleration](#workhorse-object-storage-acceleration) + +### Scalability + +Scaling NFS is outside of our support scope, and NFS is not a part of cloud native installations. + +All features that require sidekiq and do not use object storage acceleration won't work without NFS. In Kubernetes, machine boundaries translate to PODs, and in this case the uploaded file will be written into the POD private disk. Since sidekiq POD cannot reach into other pods, the operation will fail to read it. + +## How to select the proper level of acceleration? + +Selecting the proper acceleration is a tradeoff between speed of development and operational costs. + +We can identify three major use-cases for an upload: + +1. **storage:** if we are uploading for storing a file (i.e. artifacts, packages, discussion attachments). In this case [object storage acceleration](#workhorse-object-storage-acceleration) is the proper level as it's the less resource-intensive operation. Additional information can be found on [File Storage in GitLab](file_storage.md). +1. **in-controller/synchronous processing:** if we allow processing **small files** synchronously, using [disk acceleration](#workhorse-disk-acceleration) may speed up development. +1. **sidekiq/asynchronous processing:** Async processing must implement [object storage acceleration](#workhorse-object-storage-acceleration), the reason being that it's the only way to support Cloud Native deployments without a shared NFS. + +For more details about currently broken feature see [epic &1802](https://gitlab.com/groups/gitlab-org/-/epics/1802). + +### Handling repository uploads + +Some features involves git repository uploads without using a regular git client. +Some examples are uploading a repository file from the web interface and [design management](../user/project/issues/design_management.md). + +Those uploads requires the rails controller to act as a git client in lieu of the user. +Those operation falls into _in-controller/synchronous processing_ category, but we have no warranties on the file size. + +In case of a LFS upload, the file pointer is committed synchronously, but file upload to object storage is performed asynchronously with sidekiq. + +## Upload encodings + +By upload encoding we mean how the file is included within the incoming request. + +We have three kinds of file encoding in our uploads: + +1. <i class="fa fa-check-circle"></i> **multipart**: `multipart/form-data` is the most common, a file is encoded as a part of a multipart encoded request. +1. <i class="fa fa-check-circle"></i> **body**: some APIs uploads files as the whole request body. +1. <i class="fa fa-times-circle"></i> **JSON**: some JSON API uploads files as base64 encoded strings. This requires [gitlab-workhorse#226](https://gitlab.com/gitlab-org/gitlab-workhorse/issues/226) to be implemented. + +## Uploading technologies + +By uploading technologies we mean how all the involved services interact with each other. + +GitLab supports 3 kinds of uploading technologies, here follows a brief description with a sequence diagram for each one. Diagrams are not meant to be exhaustive. + +### Regular rails upload + +This is the default kind of upload, and it's most expensive in terms of resources. + +In this case, workhorse is unaware of files being uploaded and acts as a regular proxy. + +When a multipart request reaches the rails application, `Rack::Multipart` leaves behind tempfiles in `/tmp` and uses valuable Ruby process time to copy files around. + +```mermaid +sequenceDiagram + participant c as Client + participant w as Workhorse + participant r as Rails + + activate c + c ->>+w: POST /some/url/upload + w->>+r: POST /some/url/upload + + r->>r: save the incoming file on /tmp + r->>r: read the file for processing + + r-->>-c: request result + deactivate c + deactivate w +``` + +### Workhorse disk acceleration + +This kind of upload avoids wasting resources caused by handling upload writes to `/tmp` in rails. + +This optimization is not active by default on REST API requests. + +When enabled, Workhorse looks for files in multipart MIME requests, uploading +any it finds to a temporary file on shared storage. The MIME data in the request +is replaced with the path to the corresponding file before it is forwarded to +Rails. + +To prevent abuse of this feature, Workhorse signs the modified request with a +special header, stating which entries it modified. Rails will ignore any +unsigned path entries. + +```mermaid +sequenceDiagram + participant c as Client + participant w as Workhorse + participant r as Rails + participant s as NFS + + activate c + c ->>+w: POST /some/url/upload + + w->>+s: save the incoming file on a temporary location + s-->>-w: + + w->>+r: POST /some/url/upload + Note over w,r: file was replaced with its location<br>and other metadata + + opt requires async processing + r->>+redis: schedule a job + redis-->>-r: + end + + r-->>-c: request result + deactivate c + w->>-w: cleanup + + opt requires async processing + activate sidekiq + sidekiq->>+redis: fetch a job + redis-->>-sidekiq: job + + sidekiq->>+s: read file + s-->>-sidekiq: file + + sidekiq->>sidekiq: process file + + deactivate sidekiq + end +``` + +### Workhorse object storage acceleration + +This is the more advanced acceleration technique we have in place. + +Workhorse asks rails for temporary pre-signed object storage URLs and directly uploads to object storage. + +In this setup an extra rails route needs to be implemented in order to handle authorization, +you can see an example of this in [`Projects::LfsStorageController`](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/app/controllers/projects/lfs_storage_controller.rb) +and [its routes](https://gitlab.com/gitlab-org/gitlab-ce/blob/v12.2.0/config/routes/git_http.rb#L31-32). + +**note:** this will fallback to _Workhorse disk acceleration_ when object storage is not enabled in the gitlab instance. The answer to the `/authorize` call will only contain a file system path. + +```mermaid +sequenceDiagram + participant c as Client + participant w as Workhorse + participant r as Rails + participant os as Object Storage + + activate c + c ->>+w: POST /some/url/upload + + w ->>+r: POST /some/url/upload/authorize + Note over w,r: this request has an empty body + r-->>-w: presigned OS URL + + w->>+os: PUT file + Note over w,os: file is stored on a temporary location. Rails select the destination + os-->>-w: + + w->>+r: POST /some/url/upload + Note over w,r: file was replaced with its location<br>and other metadata + + r->>+os: move object to final destination + os-->>-r: + + opt requires async processing + r->>+redis: schedule a job + redis-->>-r: + end + + r-->>-c: request result + deactivate c + w->>-w: cleanup + + opt requires async processing + activate sidekiq + sidekiq->>+redis: fetch a job + redis-->>-sidekiq: job + + sidekiq->>+os: get object + os-->>-sidekiq: file + + sidekiq->>sidekiq: process file + + deactivate sidekiq + end +``` + +## What does the `direct_upload` setting mean? + +[Object storage setting](../administration/uploads.md#object-storage-settings) allows instance administators to enable `direct_upload`, this in an option that only affects the behavior of [workhorse object storage acceleration](#workhorse-object-storage-acceleration). + +This option affect the response to the `/authorize` call. When not enabled, the API response will not contain presigned URLs and workhorse will write the file the shared disk, on the path is provided by rails, acting like object storage was disabled. + +Once the request reachs rails, it will schedule an object storage upload as a sidekiq job. + diff --git a/doc/development/verifying_database_capabilities.md b/doc/development/verifying_database_capabilities.md index ccec6f7d719..6b4995aebe2 100644 --- a/doc/development/verifying_database_capabilities.md +++ b/doc/development/verifying_database_capabilities.md @@ -1,15 +1,15 @@ # Verifying Database Capabilities -Sometimes certain bits of code may only work on a certain database and/or +Sometimes certain bits of code may only work on a certain database version. While we try to avoid such code as much as possible sometimes it is necessary to add database (version) specific behaviour. To facilitate this we have the following methods that you can use: -- `Gitlab::Database.postgresql?`: returns `true` if PostgreSQL is being used -- `Gitlab::Database.mysql?`: returns `true` if MySQL is being used +- `Gitlab::Database.postgresql?`: returns `true` if PostgreSQL is being used. + You can normally just assume this is the case. - `Gitlab::Database.version`: returns the PostgreSQL version number as a string - in the format `X.Y.Z`. This method does not work for MySQL + in the format `X.Y.Z`. This allows you to write code such as: diff --git a/doc/development/what_requires_downtime.md b/doc/development/what_requires_downtime.md index f0da1cc2ddc..944bf5900c5 100644 --- a/doc/development/what_requires_downtime.md +++ b/doc/development/what_requires_downtime.md @@ -7,9 +7,8 @@ downtime. ## Adding Columns -On PostgreSQL you can safely add a new column to an existing table as long as it -does **not** have a default value. For example, this query would not require -downtime: +You can safely add a new column to an existing table as long as it does **not** +have a default value. For example, this query would not require downtime: ```sql ALTER TABLE projects ADD COLUMN random_value int; @@ -27,11 +26,6 @@ This requires updating every single row in the `projects` table so that indexes in a table. This in turn acquires enough locks on the table for it to effectively block any other queries. -As of MySQL 5.6 adding a column to a table is still quite an expensive -operation, even when using `ALGORITHM=INPLACE` and `LOCK=NONE`. This means -downtime _may_ be required when modifying large tables as otherwise the -operation could potentially take hours to complete. - Adding a column with a default value _can_ be done without requiring downtime when using the migration helper method `Gitlab::Database::MigrationHelpers#add_column_with_default`. This method works @@ -311,8 +305,7 @@ migrations](background_migrations.md#cleaning-up). ## Adding Indexes Adding indexes is an expensive process that blocks INSERT and UPDATE queries for -the duration. When using PostgreSQL one can work around this by using the -`CONCURRENTLY` option: +the duration. You can work around this by using the `CONCURRENTLY` option: ```sql CREATE INDEX CONCURRENTLY index_name ON projects (column_name); @@ -336,17 +329,9 @@ end Note that `add_concurrent_index` can not be reversed automatically, thus you need to manually define `up` and `down`. -When running this on PostgreSQL the `CONCURRENTLY` option mentioned above is -used. On MySQL this method produces a regular `CREATE INDEX` query. - -MySQL doesn't really have a workaround for this. Supposedly it _can_ create -indexes without the need for downtime but only for variable width columns. The -details on this are a bit sketchy. Since it's better to be safe than sorry one -should assume that adding indexes requires downtime on MySQL. - ## Dropping Indexes -Dropping an index does not require downtime on both PostgreSQL and MySQL. +Dropping an index does not require downtime. ## Adding Tables @@ -370,7 +355,7 @@ transaction this means this approach would require downtime. GitLab allows you to work around this by using `Gitlab::Database::MigrationHelpers#add_concurrent_foreign_key`. This method -ensures that when PostgreSQL is used no downtime is needed. +ensures that no downtime is needed. ## Removing Foreign Keys diff --git a/doc/gitlab-basics/start-using-git.md b/doc/gitlab-basics/start-using-git.md index 3e3f96fb31f..5de173abbff 100644 --- a/doc/gitlab-basics/start-using-git.md +++ b/doc/gitlab-basics/start-using-git.md @@ -63,8 +63,8 @@ git config --global user.email You'll need to do this only once, since you are using the `--global` option. It tells Git to always use this information for anything you do on that system. If you want -to override this with a different username or email address for specific projects, -you can run the command without the `--global` option when you’re in that project. +to override this with a different username or email address for specific projects or repositories, +you can run the command without the `--global` option when you’re in that project, and that will default to `--local`. You can read more on how Git manages configurations in the [Git Config](https://git-scm.com/book/en/v2/Customizing-Git-Git-Configuration) documentation. ## Check your information @@ -102,8 +102,7 @@ files to your local computer, automatically preserving the Git connection with t 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 only once. +it via HTTPS, you'll have to enter your credentials every time you pull and push. You can read more about credential storage in the [Git Credentials documentation](https://git-scm.com/book/en/v2/Git-Tools-Credential-Storage). With SSH, you enter your credentials only once. 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 @@ -152,13 +151,15 @@ to get the main branch code, or the branch name of the branch you are currently in. ```bash -git pull REMOTE <name-of-branch> +git pull <REMOTE> <name-of-branch> ``` -When you first clone a repository, REMOTE is typically `origin`. This is where the +When you clone a repository, `REMOTE` is typically `origin`. This is where the repository was cloned from, and it indicates the SSH or HTTPS URL of the repository on the remote server. `<name-of-branch>` is usually `master`, but it may be any existing -branch. +branch. You can create additional named remotes and branches as necessary. + +You can learn more on how Git manages remote repositories in the [Git Remote documentation](https://git-scm.com/book/en/v2/Git-Basics-Working-with-Remotes). ### View your remote repositories @@ -168,6 +169,8 @@ To view your remote repositories, type: git remote -v ``` +The `-v` flag stands for verbose. + ### Add a remote repository To add a link to a remote repository: @@ -186,7 +189,7 @@ following (spaces won't be recognized in the branch name, so you will need to us hyphen or underscore): ```bash -git checkout -b <name-of-branch>> +git checkout -b <name-of-branch> ``` ### Work on an existing branch @@ -238,7 +241,7 @@ git commit -m "COMMENT TO DESCRIBE THE INTENTION OF THE COMMIT" ``` NOTE: **Note:** -The `.` character typically means _all_ in Git. +The `.` character means _all file changes in the current directory and all subdirectories_. ### Send changes to GitLab.com diff --git a/doc/install/azure/index.md b/doc/install/azure/index.md index 543a222bd25..c5939dc6856 100644 --- a/doc/install/azure/index.md +++ b/doc/install/azure/index.md @@ -407,7 +407,7 @@ on any cloud service you choose. ## Where to next? -Check out our other [Technical Articles](../../articles/index.md) or browse the [GitLab Documentation][GitLab-Docs](../../README.md) to learn more about GitLab. +Check out our other [Technical Articles](../../articles/index.md) or browse the [GitLab Documentation](../../README.md) to learn more about GitLab. ### Useful links diff --git a/doc/install/installation.md b/doc/install/installation.md index 72a3514e2d5..6d64b8a4936 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -172,7 +172,7 @@ sudo make install # Download and compile from source cd /tmp curl --remote-name --location --progress https://www.kernel.org/pub/software/scm/git/git-2.22.0.tar.gz -echo 'a4b7e4365bee43caa12a38d646d2c93743d755d1cea5eab448ffb40906c9da0b' git-2.22.0.tar.gz' | shasum -a256 -c - && tar -xzf git-2.22.0.tar.gz +echo 'a4b7e4365bee43caa12a38d646d2c93743d755d1cea5eab448ffb40906c9da0b git-2.22.0.tar.gz' | shasum -a256 -c - && tar -xzf git-2.22.0.tar.gz cd git-2.22.0/ ./configure --with-libpcre make prefix=/usr/local all @@ -202,8 +202,8 @@ Then select 'Internet Site' and press enter to confirm the hostname. The Ruby interpreter is required to run GitLab. -**Note:** The current supported Ruby (MRI) version is 2.5.x. GitLab 11.6 - dropped support for Ruby 2.4.x. +**Note:** The current supported Ruby (MRI) version is 2.6.x. GitLab 12.2 + dropped support for Ruby 2.5.x. The use of Ruby version managers such as [RVM], [rbenv] or [chruby] with GitLab in production, frequently leads to hard to diagnose problems. For example, @@ -789,7 +789,7 @@ nginx: configuration file /etc/nginx/nginx.conf test failed` sudo service nginx restart ``` -## Done! +## Post-install ### Double-check Application Status diff --git a/doc/install/requirements.md b/doc/install/requirements.md index 83a9e7fe294..234e5acb394 100644 --- a/doc/install/requirements.md +++ b/doc/install/requirements.md @@ -40,7 +40,7 @@ Please consider using a virtual machine to run GitLab. ## Ruby versions -GitLab requires Ruby (MRI) 2.5. Support for Ruby versions below 2.5 (2.3, 2.4) will stop with GitLab 11.6. +GitLab requires Ruby (MRI) 2.6. Support for Ruby versions below 2.6 (2.4, 2.5) will stop with GitLab 12.2. You will have to use the standard MRI implementation of Ruby. We love [JRuby](https://www.jruby.org/) and [Rubinius](https://rubinius.com) but GitLab diff --git a/doc/integration/elasticsearch.md b/doc/integration/elasticsearch.md index 1c80fc543af..eee05eaef02 100644 --- a/doc/integration/elasticsearch.md +++ b/doc/integration/elasticsearch.md @@ -333,6 +333,10 @@ curl --request PUT localhost:9200/gitlab-production/_settings --data '{ Enable Elasticsearch search in **Admin > Settings > Integrations**. That's it. Enjoy it! +### Index limit + +Currently for repository and snippet files, GitLab would only index up to 1 MB of content, in order to avoid indexing timeout. + ## GitLab Elasticsearch Rake Tasks There are several rake tasks available to you via the command line: diff --git a/doc/integration/jenkins.md b/doc/integration/jenkins.md index 6755640d39c..d865f977799 100644 --- a/doc/integration/jenkins.md +++ b/doc/integration/jenkins.md @@ -35,7 +35,7 @@ and [Migrating from Jenkins to GitLab](https://www.youtube.com/watch?v=RlEVGOpYF For a real use case, read the blog post [Continuous integration: From Jenkins to GitLab using Docker](https://about.gitlab.com/2017/07/27/docker-my-precious/). -NOTE: **Moving from a traditional CI plug-in to a single application for the entire software development lifecycle can decrease hours spent on maintaining toolchains by 10% or more.** +NOTE: **Moving from a traditional CI plug-in to a single application for the entire software development lifecycle can decrease hours spent on maintaining toolchains by 10% or more.** Visit the ['GitLab vs. Jenkins' comparison page](https://about.gitlab.com/devops-tools/jenkins-vs-gitlab.html) to learn how our built-in CI compares to Jenkins. ## Requirements diff --git a/doc/integration/oauth2_generic.md b/doc/integration/oauth2_generic.md index f4119b1d1ce..ad59592c45f 100644 --- a/doc/integration/oauth2_generic.md +++ b/doc/integration/oauth2_generic.md @@ -12,7 +12,7 @@ This strategy is designed to allow configuration of the simple OmniAuth SSO proc 1. Strategy parses user information from the response, using a **configurable** format 1. GitLab finds or creates the returned user and logs them in -## Limitations of this Strategy: +## Limitations of this Strategy - It can only be used for Single Sign on, and will not provide any other access granted by any OAuth provider (importing projects or users, etc) diff --git a/doc/migrate_ci_to_ce/README.md b/doc/migrate_ci_to_ce/README.md index 9947c19a667..2b8379141c3 100644 --- a/doc/migrate_ci_to_ce/README.md +++ b/doc/migrate_ci_to_ce/README.md @@ -227,7 +227,7 @@ sudo mv /path/to/12345_gitlab_ci_backup.tar /var/opt/gitlab/backups/ sudo mv /path/to/12345_gitlab_ci_backup.tar /home/git/gitlab/tmp/backups/ ``` -### 5. Import the CI data into GitLab. +### 5. Import the CI data into GitLab This step will delete any existing CI data on your GitLab server. There should be no CI data yet because you turned CI on the GitLab server off earlier. diff --git a/doc/policy/maintenance.md b/doc/policy/maintenance.md index 018c273c51a..f7ba7c16a9e 100644 --- a/doc/policy/maintenance.md +++ b/doc/policy/maintenance.md @@ -86,6 +86,7 @@ Please see the table below for some examples: | 9.4.5 | 8.13.4 | `8.13.4` -> `8.17.7` -> `9.4.5` | `8.17.7` is the last version in version `8` | | 10.1.4 | 8.13.4 | `8.13.4 -> 8.17.7 -> 9.5.10 -> 10.1.4` | `8.17.7` is the last version in version `8`, `9.5.10` is the last version in version `9` | | 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` | +| 12.0.2 | 11.3.4 | `11.3.4` -> `11.11.x` -> `12.0.2` | `11.11.x` is the last version in version `11` More information about the release procedures can be found in our [release documentation](https://gitlab.com/gitlab-org/release/docs). You may also want to read our diff --git a/doc/public_access/public_access.md b/doc/public_access/public_access.md index 0142e5075cc..dc6ee9b2503 100644 --- a/doc/public_access/public_access.md +++ b/doc/public_access/public_access.md @@ -6,7 +6,7 @@ type: reference GitLab allows [Owners](../user/permissions.md) to set a projects' visibility as **public**, **internal** or **private**. These visibility levels affect who can see the project in the -public access directory (`/public` under your GitLab instance), like at [https://gitlab.com/public](). +public access directory (`/public` under your GitLab instance), like at <https://gitlab.com/public> ## Visibility of projects diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md index 51f04df27c7..b4b7d711d5a 100644 --- a/doc/raketasks/backup_restore.md +++ b/doc/raketasks/backup_restore.md @@ -978,4 +978,3 @@ If this happens, check the following: 1. Confirm there is sufficent diskspace for the gzip operation. 1. If NFS is being used, check if the mount option `timeo` is set. The default is `600`, and changing this to smaller values have resulted in this error. - diff --git a/doc/raketasks/cleanup.md b/doc/raketasks/cleanup.md index 832078d23cb..f84d29cca9a 100644 --- a/doc/raketasks/cleanup.md +++ b/doc/raketasks/cleanup.md @@ -1,6 +1,10 @@ # Cleanup -## Remove garbage from filesystem. Important! Data loss! +## Remove garbage from filesystem + +DANGER: **Danger:** +The commands below will remove data permanently from your GitLab instance. Only use +these commands if you are 100% certain that it is safe to delete this data. Remove namespaces(dirs) from all repository storage paths if they don't exist in GitLab database. diff --git a/doc/raketasks/import.md b/doc/raketasks/import.md index 8f65fab366e..c76180a242e 100644 --- a/doc/raketasks/import.md +++ b/doc/raketasks/import.md @@ -11,7 +11,7 @@ ## How to use -### Create a new folder to import your Git repositories from. +### Create a new folder to import your Git repositories from The new folder needs to have git user ownership and read/write/execute access for git user and its group: @@ -19,7 +19,7 @@ The new folder needs to have git user ownership and read/write/execute access fo sudo -u git mkdir -p /var/opt/gitlab/git-data/repository-import-<date>/new_group ``` -### Copy your bare repositories inside this newly created folder: +### Copy your bare repositories inside this newly created folder - Any .git repositories found on any of the subfolders will be imported as projects - Groups will be created as needed, these could be nested folders. Example: @@ -38,7 +38,7 @@ sudo chown -R git:git /var/opt/gitlab/git-data/repository-import-<date> If you are using an installation from source, replace `/var/opt/gitlab/` with `/home/git`. -### Run the command below depending on your type of installation: +### Run the command below depending on your type of installation #### Omnibus Installation diff --git a/doc/raketasks/web_hooks.md b/doc/raketasks/web_hooks.md index cc1166a04cc..d9a042ec8fe 100644 --- a/doc/raketasks/web_hooks.md +++ b/doc/raketasks/web_hooks.md @@ -1,6 +1,6 @@ # Webhooks administration **(CORE ONLY)** -## Add a webhook for **ALL** projects: +## Add a webhook for **ALL** projects ```sh # omnibus-gitlab @@ -9,7 +9,7 @@ sudo gitlab-rake gitlab:web_hook:add URL="http://example.com/hook" bundle exec rake gitlab:web_hook:add URL="http://example.com/hook" RAILS_ENV=production ``` -## Add a webhook for projects in a given **NAMESPACE**: +## Add a webhook for projects in a given **NAMESPACE** ```sh # omnibus-gitlab @@ -18,7 +18,7 @@ sudo gitlab-rake gitlab:web_hook:add URL="http://example.com/hook" NAMESPACE=acm bundle exec rake gitlab:web_hook:add URL="http://example.com/hook" NAMESPACE=acme RAILS_ENV=production ``` -## Remove a webhook from **ALL** projects using: +## Remove a webhook from **ALL** projects using ```sh # omnibus-gitlab @@ -27,7 +27,7 @@ sudo gitlab-rake gitlab:web_hook:rm URL="http://example.com/hook" bundle exec rake gitlab:web_hook:rm URL="http://example.com/hook" RAILS_ENV=production ``` -## Remove a webhook from projects in a given **NAMESPACE**: +## Remove a webhook from projects in a given **NAMESPACE** ```sh # omnibus-gitlab @@ -36,7 +36,7 @@ sudo gitlab-rake gitlab:web_hook:rm URL="http://example.com/hook" NAMESPACE=acme bundle exec rake gitlab:web_hook:rm URL="http://example.com/hook" NAMESPACE=acme RAILS_ENV=production ``` -## List **ALL** webhooks: +## List **ALL** webhooks ```sh # omnibus-gitlab @@ -45,7 +45,7 @@ sudo gitlab-rake gitlab:web_hook:list bundle exec rake gitlab:web_hook:list RAILS_ENV=production ``` -## List the webhooks from projects in a given **NAMESPACE**: +## List the webhooks from projects in a given **NAMESPACE** ```sh # omnibus-gitlab diff --git a/doc/security/rate_limits.md b/doc/security/rate_limits.md index 0e5bdcd9c79..80088da77a0 100644 --- a/doc/security/rate_limits.md +++ b/doc/security/rate_limits.md @@ -22,8 +22,8 @@ similarly mitigated by a rate limit. ## Admin Area settings -See -[User and IP rate limits](../user/admin_area/settings/user_and_ip_rate_limits.md). +- [User and IP rate limits](../user/admin_area/settings/user_and_ip_rate_limits.md). +- [Rate limits on raw endpoints](../user/admin_area/settings/rate_limits_on_raw_endpoints.md) ## Rack Attack initializer diff --git a/doc/user/admin_area/settings/img/additional_minutes.png b/doc/subscriptions/img/additional_minutes.png Binary files differindex b159b98c9ce..b159b98c9ce 100644 --- a/doc/user/admin_area/settings/img/additional_minutes.png +++ b/doc/subscriptions/img/additional_minutes.png diff --git a/doc/user/admin_area/settings/img/buy_btn.png b/doc/subscriptions/img/buy_btn.png Binary files differindex 4fd05c0fba7..4fd05c0fba7 100644 --- a/doc/user/admin_area/settings/img/buy_btn.png +++ b/doc/subscriptions/img/buy_btn.png diff --git a/doc/user/admin_area/settings/img/buy_minutes_card.png b/doc/subscriptions/img/buy_minutes_card.png Binary files differindex cab098300cd..cab098300cd 100644 --- a/doc/user/admin_area/settings/img/buy_minutes_card.png +++ b/doc/subscriptions/img/buy_minutes_card.png diff --git a/doc/subscriptions/index.md b/doc/subscriptions/index.md index 9317ba0c71e..13c406727ab 100644 --- a/doc/subscriptions/index.md +++ b/doc/subscriptions/index.md @@ -221,6 +221,52 @@ The following table describes details of your subscription for groups: | Subscription start date | Date your subscription started. If this is for a Free plan, is the date you transitioned off your group's paid plan. | | Subscription end date | Date your current subscription will end. Does not apply to Free plans. | +#### Extra Shared Runners pipeline minutes + +If you're using GitLab.com, you can purchase additional CI minutes so your +pipelines will not be blocked after you have used all your CI minutes from your +main quota. Additional minutes: + +- Are only used once the shared quota included in your subscription runs out. +- Roll over month to month. + +In order to purchase additional minutes, you should follow these steps: + +1. Go to **Group > Settings > Pipelines quota**. Once you are on that page, click on **Buy additional minutes**. + + ![Buy additional minutes](img/buy_btn.png) + +1. Locate the subscription card that is linked to your group on GitLab.com, + click on **Buy more CI minutes**, and complete the details about the transaction. + + ![Buy additional minutes](img/buy_minutes_card.png) + +1. Once we have processed your payment, the extra CI minutes + will be synced to your Group and you can visualize it from the + **Group > Settings > Pipelines quota** page: + + ![Additional minutes](img/additional_minutes.png) + +Be aware that: + +1. If you have purchased extra CI minutes before the purchase of a paid plan, + we will calculate a pro-rated charge for your paid plan. That means you may + be charged for less than one year since your subscription was previously + created with the extra CI minutes. +1. Once the extra CI minutes has been assigned to a Group they cannot be transferred + to a different Group. +1. If you have some minutes used over your default quota, these minutes will + be deducted from your Additional Minutes quota immediately after your purchase of additional + minutes. + +##### What happens when my CI minutes run out + +When the CI minutes run out, an email is sent automatically to notify the owner(s) +of the group/namespace, including a link to [purchase more minutes](https://customers.gitlab.com/plans). + +If you are not the owner of the group, you will need to contact them to let them know they need to +[purchase more minutes](https://customers.gitlab.com/plans). + ## Subscription changes and your data When your subscription or trial expires, GitLab does not delete your data. diff --git a/doc/topics/autodevops/index.md b/doc/topics/autodevops/index.md index 95220d6364c..26d5221acc5 100644 --- a/doc/topics/autodevops/index.md +++ b/doc/topics/autodevops/index.md @@ -65,7 +65,7 @@ project in a simple and automatic way: 1. [Auto Code Quality](#auto-code-quality-starter) **(STARTER)** 1. [Auto SAST (Static Application Security Testing)](#auto-sast-ultimate) **(ULTIMATE)** 1. [Auto Dependency Scanning](#auto-dependency-scanning-ultimate) **(ULTIMATE)** -1. [Auto License Management](#auto-license-management-ultimate) **(ULTIMATE)** +1. [Auto License Compliance](#auto-license-compliance-ultimate) **(ULTIMATE)** 1. [Auto Container Scanning](#auto-container-scanning-ultimate) **(ULTIMATE)** 1. [Auto Review Apps](#auto-review-apps) 1. [Auto DAST (Dynamic Application Security Testing)](#auto-dast-ultimate) **(ULTIMATE)** @@ -401,13 +401,13 @@ check out. Any security warnings are also shown in the merge request widget. Read more about [Dependency Scanning](../../user/application_security/dependency_scanning/index.md). -### Auto License Management **(ULTIMATE)** +### Auto License Compliance **(ULTIMATE)** > Introduced in [GitLab Ultimate][ee] 11.0. -License Management uses the -[License Management Docker image](https://gitlab.com/gitlab-org/security-products/license-management) -to search the project dependencies for their license. The Auto License Management stage +License Compliance uses the +[License Compliance Docker image](https://gitlab.com/gitlab-org/security-products/license-management) +to search the project dependencies for their license. The Auto License Compliance stage will be skipped on licenses other than Ultimate. Once the @@ -415,7 +415,7 @@ report is created, it's uploaded as an artifact which you can later download and check out. Any licenses are also shown in the merge request widget. Read more how -[License Management works](../../user/application_security/license_management/index.md). +[License Compliance works](../../user/application_security/license_management/index.md). ### Auto Container Scanning **(ULTIMATE)** @@ -584,6 +584,55 @@ Unless you have a `Dockerfile` in your repo, your image is built with Herokuish, and you must prefix commands run in these images with `/bin/herokuish procfile exec` to replicate the environment where your application will run. +#### Workers + +Some web applications need to run extra deployments for "worker processes". For +example, it is common in a Rails application to have a separate worker process +to run background tasks like sending emails. + +The [default Helm chart](https://gitlab.com/gitlab-org/charts/auto-deploy-app) +used in Auto Deploy [has support for running worker +processes](https://gitlab.com/gitlab-org/charts/auto-deploy-app/merge_requests/9). + +In order to run a worker, you'll need to ensure that it is able to respond to +the standard health checks, which expect a successful HTTP response on port +`5000`. For [Sidekiq](https://github.com/mperham/sidekiq), you could make use of +the [`sidekiq_alive` gem](https://rubygems.org/gems/sidekiq_alive) to do this. + +In order to work with Sidekiq, you'll also need to ensure your deployments have +access to a Redis instance. Auto DevOps won't deploy this for you so you'll +need to: + +- Maintain your own Redis instance. +- Set a CI variable `K8S_SECRET_REDIS_URL`, which the URL of this instance to + ensure it's passed into your deployments. + +Once you have configured your worker to respond to health checks, you will +need to configure a CI variable `HELM_UPGRADE_EXTRA_ARGS` with the value +`--values helm-values.yaml`. + +Then you can, for example, run a Sidekiq worker for your Rails application +by adding a file named `helm-values.yaml` to your repository with the following +content: + +```yml +workers: + sidekiq: + replicaCount: 1 + command: + - /bin/herokuish + - procfile + - exec + - sidekiq + preStopCommand: + - /bin/herokuish + - procfile + - exec + - sidekiqctl + - quiet + terminationGracePeriodSeconds: 60 +``` + ### Auto Monitoring See the [requirements](#requirements) for Auto Monitoring to enable this stage. @@ -816,7 +865,7 @@ To configure your application variables: 1. Create a CI Variable, ensuring the key is prefixed with `K8S_SECRET_`. For example, you can create a variable with key -`K8S_SECRET_RAILS_MASTER_KEY`. + `K8S_SECRET_RAILS_MASTER_KEY`. 1. Run an Auto Devops pipeline either by manually creating a new pipeline or by pushing a code change to GitLab. @@ -1017,10 +1066,10 @@ Everything behaves the same way, except: - It's enabled by setting the `INCREMENTAL_ROLLOUT_MODE` variable to `timed`. - Instead of the standard `production` job, the following jobs with a 5 minute delay between each are created: - 1. `timed rollout 10%` - 1. `timed rollout 25%` - 1. `timed rollout 50%` - 1. `timed rollout 100%` + 1. `timed rollout 10%` + 1. `timed rollout 25%` + 1. `timed rollout 50%` + 1. `timed rollout 100%` ## Currently supported languages diff --git a/doc/topics/autodevops/quick_start_guide.md b/doc/topics/autodevops/quick_start_guide.md index 7ab59b80374..35a5aff6a60 100644 --- a/doc/topics/autodevops/quick_start_guide.md +++ b/doc/topics/autodevops/quick_start_guide.md @@ -167,7 +167,7 @@ In the **test** stage, GitLab runs various checks on the application: - The `sast` job runs static analysis on the current code to check for potential security issues and is allowed to fail([Auto SAST](index.md#auto-sast-ultimate)) **(ULTIMATE)** - The `license_management` job searches the application's dependencies to determine each of their - licenses and is allowed to fail ([Auto License Management](index.md#auto-license-management-ultimate)) **(ULTIMATE)** + licenses and is allowed to fail ([Auto License Compliance](index.md#auto-license-compliance-ultimate)) **(ULTIMATE)** NOTE: **Note:** As you might have noticed, all jobs except `test` are allowed to fail in the diff --git a/doc/university/README.md b/doc/university/README.md index b17f40b91a5..25f77906e68 100644 --- a/doc/university/README.md +++ b/doc/university/README.md @@ -57,7 +57,7 @@ The GitLab University curriculum is composed of GitLab videos, screencasts, pres 1. [Migrating from SVN](../user/project/import/svn.md) 1. [Migrating from Fogbugz](../user/project/import/fogbugz.md) -### 1.6. GitLab Inc. +### 1.6. The GitLab team 1. [About GitLab](https://about.gitlab.com/about/) 1. [GitLab Direction](https://about.gitlab.com/direction/) diff --git a/doc/university/training/end-user/README.md b/doc/university/training/end-user/README.md index 423ba1cfbd7..1218465c87a 100644 --- a/doc/university/training/end-user/README.md +++ b/doc/university/training/end-user/README.md @@ -9,12 +9,8 @@ which can be found at [End User Slides](https://gitlab-org.gitlab.io/end-user-tr through it's [RevealJS](https://gitlab.com/gitlab-org/end-user-training-slides) project. ---- - ## Git Intro ---- - ### What is a Version Control System (VCS) - Records changes to a file @@ -22,8 +18,6 @@ project. - Disaster Recovery - Types of VCS: Local, Centralized and Distributed ---- - ### Short Story of Git - 1991-2002: The Linux kernel was being maintained by sharing archived files @@ -31,8 +25,6 @@ project. - 2002: The Linux kernel project began using a DVCS called BitKeeper - 2005: BitKeeper revoked the free-of-charge status and Git was created ---- - ### What is Git - Distributed Version Control System @@ -42,8 +34,6 @@ project. - Disaster recovery friendly - Open Source ---- - ### Getting Help - Use the tools at your disposal when you get stuck. @@ -51,14 +41,10 @@ project. - Use Google (i.e. StackOverflow, Google groups) - Read documentation at <https://git-scm.com> ---- - ## Git Setup Workshop Time! ---- - ### Setup - Windows: Install 'Git for Windows' @@ -69,8 +55,6 @@ Workshop Time! - Debian: `sudo apt-get install git-all` - Red Hat `sudo yum install git-all` ---- - ### Configure - One-time configuration of the Git client: @@ -91,16 +75,12 @@ git config --global --list - You might want or be required to use an SSH key. - Instructions: [SSH](http://doc.gitlab.com/ce/ssh/README.html) ---- - ### Workspace - Choose a directory on you machine easy to access - Create a workspace or development directory - This is where we'll be working and adding content ---- - ```bash mkdir ~/development cd ~/development @@ -111,12 +91,8 @@ mkdir ~/workspace cd ~/workspace ``` ---- - ## Git Basics ---- - ### Git Workflow - Untracked files @@ -128,8 +104,6 @@ cd ~/workspace - Upstream - Hosted repository on a shared server ---- - ### GitLab - GitLab is an application to code, test and deploy. @@ -137,8 +111,6 @@ cd ~/workspace issue tracking, Merge Requests, and other features. - The hosted version of GitLab is gitlab.com ---- - ### New Project - Sign in into your gitlab.com account @@ -146,8 +118,6 @@ cd ~/workspace - 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 ---- - ### Git and GitLab basics 1. Edit `edit_this_file.rb` in `training-examples` @@ -158,8 +128,6 @@ cd ~/workspace 1. Push the commit to the remote 1. View the git log ---- - ```shell # Edit `edit_this_file.rb` git status @@ -170,8 +138,6 @@ git push origin master git log ``` ---- - ### Feature Branching 1. Create a new feature branch called `squash_some_bugs` @@ -179,8 +145,6 @@ git log 1. Commit 1. Push ---- - ```shell git checkout -b squash_some_bugs # Edit `bugs.rb` @@ -190,14 +154,8 @@ git commit -m 'Fix some buggy code' git push origin squash_some_bugs ``` ---- - ## Merge Request ---- - -### Merge requests - - When you want feedback create a merge request - Target is the ‘default’ branch (usually master) - Assign or mention the person you would like to review @@ -206,8 +164,6 @@ git push origin squash_some_bugs - Anyone can comment, not just the assignee - Push corrections to the same branch ---- - ### Merge request example - Create your first merge request @@ -216,8 +172,6 @@ git push origin squash_some_bugs - Push a new commit to the same branch - Review the changes again and notice the update ---- - ### Feedback and Collaboration - Merge requests are a time for feedback and collaboration @@ -230,24 +184,17 @@ git push origin squash_some_bugs --- -- Review the Thoughtbot code-review guide for suggestions to follow when reviewing merge requests:[Thoughtbot](https://github.com/thoughtbot/guides/tree/master/code-review) +- Review the Thoughtbot code-review guide for suggestions to follow when reviewing merge requests: + [Thoughtbot](https://github.com/thoughtbot/guides/tree/master/code-review) - See GitLab merge requests for examples: [Merge Requests](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests) ---- - ## Merge Conflicts ---- - -### Merge Conflicts - - Happen often - Learning to fix conflicts is hard - Practice makes perfect - Force push after fixing conflicts. Be careful! ---- - ### Example Plan 1. Checkout a new branch and edit conflicts.rb. Add 'Line4' and 'Line5'. @@ -261,8 +208,6 @@ git push origin squash_some_bugs 1. Force push the changes 1. Finally continue with the Merge Request ---- - ### Example 1/2 ```sh @@ -282,8 +227,6 @@ git commit -am "add line6 and line7" git push origin master ``` ---- - ### Example 2/2 Create a merge request on the GitLab web UI. You'll see a conflict warning. @@ -305,8 +248,6 @@ git rebase --continue git push origin conflicts_branch -f ``` ---- - ### Notes - When to use `git merge` and when to use `git rebase` @@ -314,12 +255,8 @@ git push origin conflicts_branch -f - Merge when bringing changes from feature to master - Reference: <https://www.atlassian.com/git/tutorials/merging-vs-rebasing/> ---- - ## Revert and Unstage ---- - ### Unstage To remove files from stage use reset HEAD. Where HEAD is the last commit of the current branch: @@ -347,8 +284,6 @@ If we want to remove a file from the repository but keep it on disk, say we forg git rm <filename> --cache ``` ---- - ### Undo Commits Undo last commit putting everything back into the staging area: @@ -377,8 +312,6 @@ git reset --hard HEAD^^ Don't reset after pushing ---- - ### Reset Workflow 1. Edit file again 'edit_this_file.rb' @@ -392,8 +325,6 @@ Don't reset after pushing 1. Pull for updates 1. Push changes ---- - ```sh # Change file edit_this_file.rb git status @@ -407,8 +338,6 @@ git pull origin master git push origin master ``` ---- - ### git revert vs git reset Reset removes the commit while revert removes the changes but leaves the commit @@ -425,16 +354,10 @@ git revert <rev commit hash> # reverted commit is back (new commit created again) ``` ---- - ## Questions ---- - ## Instructor Notes ---- - ### Version Control - Local VCS was used with a filesystem or a simple db. diff --git a/doc/university/training/topics/bisect.md b/doc/university/training/topics/bisect.md index 4178afa2086..24dc670d9d5 100644 --- a/doc/university/training/topics/bisect.md +++ b/doc/university/training/topics/bisect.md @@ -4,13 +4,11 @@ comments: false # Bisect -## Bisect - - Find a commit that introduced a bug - Works through a process of elimination - Specify a known good and bad revision to begin -## Bisect +## Bisect sample workflow 1. Start the bisect process 1. Enter the bad revision (usually latest commit) diff --git a/doc/university/training/topics/cherry_picking.md b/doc/university/training/topics/cherry_picking.md index fa0cb5fe6a4..f5bcdfcbf12 100644 --- a/doc/university/training/topics/cherry_picking.md +++ b/doc/university/training/topics/cherry_picking.md @@ -4,13 +4,11 @@ comments: false # Cherry Pick -## Cherry Pick - - Given an existing commit on one branch, apply the change to another branch - Useful for backporting bug fixes to previous release branches - Make the commit on the master branch and pick in to stable -## Cherry Pick +## Cherry Pick sample workflow 1. Check out a new 'stable' branch from 'master' 1. Change back to 'master' @@ -19,8 +17,6 @@ comments: false 1. Check out the 'stable' branch 1. Cherry pick the commit using the SHA obtained earlier -## Commands - ```bash git checkout master git checkout -b stable diff --git a/doc/university/training/topics/feature_branching.md b/doc/university/training/topics/feature_branching.md index d2efe634533..f530389d4da 100644 --- a/doc/university/training/topics/feature_branching.md +++ b/doc/university/training/topics/feature_branching.md @@ -11,15 +11,13 @@ comments: false - Push branches to the server frequently - Hint: This is a cheap backup for your work-in-progress code -## Feature branching +## Feature branching sample workflow 1. Create a new feature branch called 'squash_some_bugs' 1. Edit '`bugs.rb`' and remove all the bugs. 1. Commit 1. Push -## Commands - ```sh git checkout -b squash_some_bugs # Edit `bugs.rb` diff --git a/doc/university/training/topics/getting_started.md b/doc/university/training/topics/getting_started.md index e8ff7916590..3fadb58e804 100644 --- a/doc/university/training/topics/getting_started.md +++ b/doc/university/training/topics/getting_started.md @@ -35,8 +35,6 @@ comments: false 1. Create a '`Workspace`' directory in your home directory. 1. Clone the '`training-examples`' project. -## Commands - ```sh mkdir ~/workspace cd ~/workspace @@ -69,8 +67,6 @@ Modified files that have been marked to go in the next commit. 1. Push the commit to the remote 1. View the git log -## Commands - ```sh # Edit `edit_this_file.rb` git status diff --git a/doc/university/training/topics/git_add.md b/doc/university/training/topics/git_add.md index 7152fc2030b..0c9a50bb5e1 100644 --- a/doc/university/training/topics/git_add.md +++ b/doc/university/training/topics/git_add.md @@ -4,8 +4,6 @@ comments: false # Git Add -## Git Add - Adds content to the index or staging area. - Adds a list of file: @@ -20,8 +18,6 @@ Adds content to the index or staging area. git add -A ``` -## Git add continued - - Add all text files in current dir: ```bash diff --git a/doc/university/training/topics/merge_conflicts.md b/doc/university/training/topics/merge_conflicts.md index dd235fe3a81..97bb038f405 100644 --- a/doc/university/training/topics/merge_conflicts.md +++ b/doc/university/training/topics/merge_conflicts.md @@ -9,7 +9,7 @@ comments: false - Practice makes perfect - Force push after fixing conflicts. Be careful! -## Merge conflicts +## Merge conflicts sample workflow 1. Checkout a new branch and edit `conflicts.rb`. Add 'Line4' and 'Line5'. 1. Commit and push. @@ -22,8 +22,6 @@ comments: false 1. Force push the changes. 1. Finally continue with the Merge Request. -## Commands - ```sh git checkout -b conflicts_branch diff --git a/doc/university/training/topics/merge_requests.md b/doc/university/training/topics/merge_requests.md index b5bbe7b2e1e..656871ae5b2 100644 --- a/doc/university/training/topics/merge_requests.md +++ b/doc/university/training/topics/merge_requests.md @@ -30,8 +30,6 @@ comments: false - Be as receptive as possible - Feedback is about the best code, not the person. You are not your code -## Feedback and Collaboration - Review the Thoughtbot code-review guide for suggestions to follow when reviewing merge requests: [https://github.com/thoughtbot/guides/tree/master/code-review](https://github.com/thoughtbot/guides/tree/master/code-review) diff --git a/doc/university/training/topics/stash.md b/doc/university/training/topics/stash.md index 21abad88cfa..d3e63db0c6a 100644 --- a/doc/university/training/topics/stash.md +++ b/doc/university/training/topics/stash.md @@ -25,7 +25,7 @@ and we need to change to a different branch. git stash apply stash@{3} ``` -- Every time we save a stash it gets stacked so by using list we can see all our +- Every time we save a stash it gets stacked so by using `list` we can see all our stashes. ```sh @@ -54,7 +54,7 @@ and we need to change to a different branch. - If we meet conflicts we need to either reset or commit our changes. - Conflicts through `pop` will not drop a stash afterwards. -## Git Stash +## Git Stash sample workflow 1. Modify a file 1. Stage file @@ -64,8 +64,6 @@ and we need to change to a different branch. 1. Apply with pop 1. View list to confirm changes -## Commands - ```sh # Modify edit_this_file.rb file git add . diff --git a/doc/university/training/topics/tags.md b/doc/university/training/topics/tags.md index cdbb8a2da7c..631b93cc384 100644 --- a/doc/university/training/topics/tags.md +++ b/doc/university/training/topics/tags.md @@ -11,18 +11,12 @@ type: reference - Many projects combine an annotated release tag with a stable branch - Consider setting deployment/release tags automatically -# Tags +## Tags sample workflow - Create a lightweight tag - Create an annotated tag - Push the tags to the remote repository -**Additional resources** - -<https://git-scm.com/book/en/Git-Basics-Tagging> - -# Commands - ```sh git checkout master @@ -36,6 +30,10 @@ git tag git push origin --tags ``` +**Additional resources** + +<https://git-scm.com/book/en/Git-Basics-Tagging> + <!-- ## Troubleshooting Include any troubleshooting steps that you can foresee. If you know beforehand what issues diff --git a/doc/university/training/topics/unstage.md b/doc/university/training/topics/unstage.md index fa1f63f9ec4..d7482bf2bd5 100644 --- a/doc/university/training/topics/unstage.md +++ b/doc/university/training/topics/unstage.md @@ -4,8 +4,6 @@ comments: false # Unstage -## Unstage - - To remove files from stage use reset HEAD where HEAD is the last commit of the current branch. This will unstage the file but maintain the modifications. ```bash diff --git a/doc/update/README.md b/doc/update/README.md index 974982da5d0..42c43110a19 100644 --- a/doc/update/README.md +++ b/doc/update/README.md @@ -135,6 +135,30 @@ If you need to downgrade your Enterprise Edition installation back to Community Edition, you can follow [this guide][ee-ce] to make the process as smooth as possible. +## Version specific upgrading instructions + +### 12.2.0 + +In 12.2.0, we enabled Rails' authenticated cookie encryption. Old sessions are +automatically upgraded. + +However, session cookie downgrades are not supported. So after upgrading to 12.2.0, +any downgrades would result to all sessions being invalidated and users are logged out. + +### 12.0.0 + +In 12.0.0 we made various database related changes. These changes require that +users first upgrade to the latest 11.11 patch release. Once upgraded to 11.11.x, +users can upgrade to 12.x. Failure to do so may result in database migrations +not being applied, which could lead to application errors. + +Example 1: you are currently using GitLab 11.11.3, which is the latest patch +release for 11.11.x. You can upgrade as usual to 12.0.0, 12.1.0, etc. + +Example 2: you are currently using a version of GitLab 10.x. To upgrade, first +upgrade to 11.11.3. Once upgraded to 11.11.3 you can safely upgrade to 12.0.0 +or future versions. + ## Miscellaneous - [MySQL to PostgreSQL](mysql_to_postgresql.md) guides you through migrating diff --git a/doc/update/patch_versions.md b/doc/update/patch_versions.md index 0506d992d4b..6a41c6aec32 100644 --- a/doc/update/patch_versions.md +++ b/doc/update/patch_versions.md @@ -41,7 +41,7 @@ sudo -u git -H git checkout -- Gemfile.lock db/schema.rb locale sudo -u git -H git checkout LATEST_TAG -b LATEST_TAG ``` -### 3. Install libs, migrations, etc. +### 3. Install libs, migrations, etc ```bash cd /home/git/gitlab diff --git a/doc/update/upgrading_from_ce_to_ee.md b/doc/update/upgrading_from_ce_to_ee.md index bea5bcd9dd7..49e2fb2568e 100644 --- a/doc/update/upgrading_from_ce_to_ee.md +++ b/doc/update/upgrading_from_ce_to_ee.md @@ -54,7 +54,7 @@ sudo -u git -H git remote add -f ee https://gitlab.com/gitlab-org/gitlab-ee.git sudo -u git -H git checkout EE_BRANCH ``` -### 3. Install libs, migrations, etc. +### 3. Install libs, migrations, etc ```sh cd /home/git/gitlab diff --git a/doc/update/upgrading_from_source.md b/doc/update/upgrading_from_source.md index d3b0a3c2829..f6b64dcf694 100644 --- a/doc/update/upgrading_from_source.md +++ b/doc/update/upgrading_from_source.md @@ -47,8 +47,8 @@ sudo service gitlab stop ### 3. Update Ruby -NOTE: Beginning in GitLab 11.6, we only support Ruby 2.5 or higher, and dropped -support for Ruby 2.4. Be sure to upgrade if necessary. +NOTE: Beginning in GitLab 12.2, we only support Ruby 2.6 or higher, and dropped +support for Ruby 2.5. Be sure to upgrade if necessary. You can check which version you are running with `ruby -v`. @@ -313,7 +313,7 @@ For Ubuntu 16.04.1 LTS: sudo systemctl daemon-reload ``` -### 13. Install libs, migrations, etc. +### 13. Install libs, migrations, etc ```bash cd /home/git/gitlab @@ -378,20 +378,6 @@ Example: Additional instructions here. --> -### 12.0.0 - -In 12.0.0 we made various database related changes. These changes require that -users first upgrade to the latest 11.11 patch release. Once upgraded to 11.11.x, -users can upgrade to 12.x. Failure to do so may result in database migrations -not being applied, which could lead to application errors. - -Example 1: you are currently using GitLab 11.11.3, which is the latest patch -release for 11.11.x. You can upgrade as usual to 12.0.0, 12.1.0, etc. - -Example 2: you are currently using a version of GitLab 10.x. To upgrade, first -upgrade to 11.11.3. Once upgraded to 11.11.3 you can safely upgrade to 12.0.0 -or future versions. - ## Things went south? Revert to previous version ### 1. Revert the code to the previous version diff --git a/doc/user/admin_area/abuse_reports.md b/doc/user/admin_area/abuse_reports.md index 0c5d2f81e25..cd8dc7bcbf6 100644 --- a/doc/user/admin_area/abuse_reports.md +++ b/doc/user/admin_area/abuse_reports.md @@ -2,7 +2,7 @@ type: reference, howto --- -# Abuse reports +# Abuse reports **(CORE ONLY)** View and resolve abuse reports from GitLab users. diff --git a/doc/user/admin_area/broadcast_messages.md b/doc/user/admin_area/broadcast_messages.md index 01b6558bdbe..b0491499f88 100644 --- a/doc/user/admin_area/broadcast_messages.md +++ b/doc/user/admin_area/broadcast_messages.md @@ -2,7 +2,7 @@ type: reference, howto --- -# Broadcast Messages +# Broadcast Messages **(CORE ONLY)** GitLab can display messages to all users of a GitLab instance in a banner that appears in the UI. diff --git a/doc/user/admin_area/diff_limits.md b/doc/user/admin_area/diff_limits.md index 4063c40a751..9fe4b50a991 100644 --- a/doc/user/admin_area/diff_limits.md +++ b/doc/user/admin_area/diff_limits.md @@ -2,7 +2,7 @@ type: reference --- -# Diff limits administration +# Diff limits administration **(CORE ONLY)** You can set a maximum size for display of diff files (patches). diff --git a/doc/user/admin_area/monitoring/health_check.md b/doc/user/admin_area/monitoring/health_check.md index 35e7b6fb541..a3f4bc774ce 100644 --- a/doc/user/admin_area/monitoring/health_check.md +++ b/doc/user/admin_area/monitoring/health_check.md @@ -2,7 +2,7 @@ type: concepts, howto --- -# Health Check +# Health Check **(CORE ONLY)** > - Liveness and readiness probes were [introduced][ce-10416] in GitLab 9.1. > - The `health_check` endpoint was [introduced][ce-3888] in GitLab 8.8 and was @@ -21,28 +21,71 @@ traffic until the system is ready or restart the container as needed. To access monitoring resources, the requesting client IP needs to be included in a whitelist. For details, see [how to add IPs to a whitelist for the monitoring endpoints](../../../administration/monitoring/ip_whitelist.md). -## Using the endpoints +## Using the endpoints locally With default whitelist settings, the probes can be accessed from localhost using the following URLs: -- `http://localhost/-/health` -- `http://localhost/-/readiness` -- `http://localhost/-/liveness` +```text +GET http://localhost/-/health +``` +```text +GET http://localhost/-/readiness +``` +```text +GET http://localhost/-/liveness +```text +GET http://localhost/-/health +``` + +```text +GET http://localhost/-/readiness +``` -The first endpoint, `health`, only checks whether the application server is running. It does not verify the database or other services are running. A successful response will return a 200 status code with the following message: +```text +GET http://localhost/-/liveness +``` + +## Health + +Checks whether the application server is running. It does not verify the database or other services are running. + +```text +GET /-/health +``` + +Example request: + +```sh +curl 'https://gitlab.example.com/-/health' +``` + +Example response: ```text GitLab OK ``` -The readiness and liveness probes will provide a report of system health in JSON format. +## Readiness + +The readiness probe checks whether the Gitlab instance is ready to use. It checks the dependent services (Database, Redis, Gitaly etc.) and gives a status for each. + +```text +GET /-/readiness +``` + +Example request: + +```sh +curl 'https://gitlab.example.com/-/readiness' +``` -`readiness` probe example output: +Example response: ```json { "db_check":{ - "status":"ok" + "status":"failed", + "message": "unexpected Db check result: 0" }, "redis_check":{ "status":"ok" @@ -65,7 +108,23 @@ The readiness and liveness probes will provide a report of system health in JSON } ``` -`liveness` probe example output: +## Liveness + +The liveness probe checks whether the application server is alive. Unlike the [`health`](#health) check, this check hits the database. + +```text +GET /-/liveness +``` + +Example request: + +```sh +curl 'https://gitlab.example.com/-/liveness' +``` + +Example response: + +On success, the endpoint will return a valid successful HTTP status code, and a response like below. ```json { @@ -90,10 +149,7 @@ The readiness and liveness probes will provide a report of system health in JSON } ``` -## Status - -On failure, the endpoint will return a `500` HTTP status code. On success, the endpoint -will return a valid successful HTTP status code, and a `success` message. +On failure, the endpoint will return a `500` HTTP status code. ## Access token (Deprecated) diff --git a/doc/user/admin_area/settings/account_and_limit_settings.md b/doc/user/admin_area/settings/account_and_limit_settings.md index 7fbb4d84cfc..5e385b7216d 100644 --- a/doc/user/admin_area/settings/account_and_limit_settings.md +++ b/doc/user/admin_area/settings/account_and_limit_settings.md @@ -2,7 +2,18 @@ type: reference --- -# Account and limit settings +# Account and limit settings **(CORE ONLY)** + +## Max attachment size + +You can change the maximum file size for attachments in comments and replies in GitLab. +Navigate to **Admin Area (wrench icon) > Settings > General**, then expand **Account and Limit**. +From here, you can increase or decrease by changing the value in `Maximum attachment size (MB)`. + +NOTE: **Note:** +If you choose a size larger than what is currently configured for the web server, +you will likely get errors. See the [troubleshooting section](#troubleshooting) for more +details. ## Repository size limit **(STARTER)** @@ -51,14 +62,18 @@ For details on manually purging files, see [reducing the repository size using G NOTE: **Note:** GitLab.com repository size [is set by GitLab](../../gitlab_com/index.md#repository-size-limit). -<!-- ## Troubleshooting +## Troubleshooting + +### 413 Request Entity Too Large + +If you are attaching a file to a comment or reply in GitLab and receive the `413 Request Entity Too Large` +error, it is likely caused by having a [max attachment size](#max-attachment-size) +larger than what the web server is configured to allow. -Include any troubleshooting steps that you can foresee. If you know beforehand what issues -one might have when setting this up, or when something is changed, or on upgrading, it's -important to describe those, too. Think of things that may go wrong and include them here. -This is important to minimize requests for support, and to avoid doc comments with -questions that you know someone might ask. +If you wanted to increase the max attachment size to 200m in a GitLab +[Omnibus](https://docs.gitlab.com/omnibus/) install, for example, you might need to +add the line below to `/etc/gitlab/gitlab.rb` before increasing the max attachment size: -Each scenario can be a third-level heading, e.g. `### Getting error message X`. -If you have none to add when creating a doc, leave this section in place -but commented out to help encourage others to add to it in the future. --> +``` +nginx['client_max_body_size'] = "200m" +``` diff --git a/doc/user/admin_area/settings/continuous_integration.md b/doc/user/admin_area/settings/continuous_integration.md index 43640f1b16a..bd76b052422 100644 --- a/doc/user/admin_area/settings/continuous_integration.md +++ b/doc/user/admin_area/settings/continuous_integration.md @@ -94,52 +94,6 @@ a group in the **Usage Quotas** page available to the group page settings list. ![Group pipelines quota](img/group_pipelines_quota.png) -## Extra Shared Runners pipeline minutes quota **(FREE ONLY)** - -If you're using GitLab.com, you can purchase additional CI minutes so your -pipelines will not be blocked after you have used all your CI minutes from your -main quota. Additional minutes: - -- Are only used once the shared quota included in your subscription runs out. -- Roll over month to month. - -In order to purchase additional minutes, you should follow these steps: - -1. Go to **Group > Settings > Pipelines quota**. Once you are on that page, click on **Buy additional minutes**. - - ![Buy additional minutes](img/buy_btn.png) - -1. Locate the subscription card that is linked to your group on GitLab.com, - click on **Buy more CI minutes**, and complete the details about the transaction. - - ![Buy additional minutes](img/buy_minutes_card.png) - -1. Once we have processed your payment, the extra CI minutes - will be synced to your Group and you can visualize it from the - **Group > Settings > Pipelines quota** page: - - ![Additional minutes](img/additional_minutes.png) - -Be aware that: - -1. If you have purchased extra CI minutes before the purchase of a paid plan, - we will calculate a pro-rated charge for your paid plan. That means you may - be charged for less than one year since your subscription was previously - created with the extra CI minutes. -1. Once the extra CI minutes has been assigned to a Group they cannot be transferred - to a different Group. -1. If you have some minutes used over your default quota, these minutes will - be deducted from your Additional Minutes quota immediately after your purchase of additional - minutes. - -## What happens when my CI minutes quota run out - -When the CI minutes quota run out, an email is sent automatically to notifies the owner(s) of the group/namespace which -includes a link to [purchase more minutes](https://customers.gitlab.com/plans). - -If you are not the owner of the group, you will need to contact them to let them know they need to -[purchase more minutes](https://customers.gitlab.com/plans). - ## Archive jobs **(CORE ONLY)** Archiving jobs is useful for reducing the CI/CD footprint on the system by diff --git a/doc/user/admin_area/settings/email.md b/doc/user/admin_area/settings/email.md index 490163c1816..ddf989d0181 100644 --- a/doc/user/admin_area/settings/email.md +++ b/doc/user/admin_area/settings/email.md @@ -2,7 +2,7 @@ type: reference --- -# Email +# Email **(CORE ONLY)** You can customize some of the content in emails sent from your GitLab instance. diff --git a/doc/user/admin_area/settings/img/rate_limits_on_raw_endpoints.png b/doc/user/admin_area/settings/img/rate_limits_on_raw_endpoints.png Binary files differnew file mode 100644 index 00000000000..c32eb93c8a8 --- /dev/null +++ b/doc/user/admin_area/settings/img/rate_limits_on_raw_endpoints.png diff --git a/doc/user/admin_area/settings/rate_limits_on_raw_endpoints.md b/doc/user/admin_area/settings/rate_limits_on_raw_endpoints.md new file mode 100644 index 00000000000..8e53a6995fb --- /dev/null +++ b/doc/user/admin_area/settings/rate_limits_on_raw_endpoints.md @@ -0,0 +1,22 @@ +--- +type: reference +--- + +# Rate limits on raw endpoints **(CORE ONLY)** + +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/30829) in GitLab 12.2. + +This setting allows you to rate limit the requests to raw endpoints, defaults to `300` requests per minute. +It can be modified in **Admin Area > Network > Performance Optimization**. + +For example, requests over `300` per minute to `https://gitlab.com/gitlab-org/gitlab-ce/raw/master/app/controllers/application_controller.rb` will be blocked. Access to the raw file will be released after 1 minute. + +![Rate limits on raw endpoints](img/rate_limits_on_raw_endpoints.png) + +This limit is: + +- Applied independently per project, per commit and per file path. +- Not applied per IP address. +- Active by default. To disable, set the option to `0`. + +Requests over the rate limit are logged into `auth.log`. diff --git a/doc/user/admin_area/settings/sign_up_restrictions.md b/doc/user/admin_area/settings/sign_up_restrictions.md index 1b1bcbcd6e8..aea717e806d 100644 --- a/doc/user/admin_area/settings/sign_up_restrictions.md +++ b/doc/user/admin_area/settings/sign_up_restrictions.md @@ -2,7 +2,7 @@ type: reference --- -# Sign-up restrictions +# Sign-up restrictions **(CORE ONLY)** You can use sign-up restrictions to require user email confirmation, as well as to blacklist or whitelist email addresses belonging to specific domains. diff --git a/doc/user/admin_area/settings/terms.md b/doc/user/admin_area/settings/terms.md index a1bce5a6c69..baf219ac9c7 100644 --- a/doc/user/admin_area/settings/terms.md +++ b/doc/user/admin_area/settings/terms.md @@ -2,7 +2,7 @@ type: reference --- -# Enforce accepting Terms of Service +# Enforce accepting Terms of Service **(CORE ONLY)** > [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/18570) > in [GitLab Core](https://about.gitlab.com/pricing/) 10.8 diff --git a/doc/user/admin_area/settings/third_party_offers.md b/doc/user/admin_area/settings/third_party_offers.md index d3c9cf7d8ff..ca26147b287 100644 --- a/doc/user/admin_area/settings/third_party_offers.md +++ b/doc/user/admin_area/settings/third_party_offers.md @@ -2,7 +2,7 @@ type: reference --- -# Third party offers +# Third party offers **(CORE ONLY)** > [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/20379) > in [GitLab Core](https://about.gitlab.com/pricing/) 11.1 diff --git a/doc/user/admin_area/settings/usage_statistics.md b/doc/user/admin_area/settings/usage_statistics.md index 516600c9d99..efac7e699f3 100644 --- a/doc/user/admin_area/settings/usage_statistics.md +++ b/doc/user/admin_area/settings/usage_statistics.md @@ -2,7 +2,7 @@ type: reference --- -# Usage statistics +# Usage statistics **(CORE ONLY)** GitLab Inc. will periodically collect information about your instance in order to perform various actions. diff --git a/doc/user/admin_area/settings/user_and_ip_rate_limits.md b/doc/user/admin_area/settings/user_and_ip_rate_limits.md index e3a495750f2..b9d93bf3671 100644 --- a/doc/user/admin_area/settings/user_and_ip_rate_limits.md +++ b/doc/user/admin_area/settings/user_and_ip_rate_limits.md @@ -2,7 +2,7 @@ type: reference --- -# User and IP rate limits +# User and IP rate limits **(CORE ONLY)** Rate limiting is a common technique used to improve the security and durability of a web application. For more details, see diff --git a/doc/user/admin_area/settings/visibility_and_access_controls.md b/doc/user/admin_area/settings/visibility_and_access_controls.md index bf59f49b993..1e2f5705728 100644 --- a/doc/user/admin_area/settings/visibility_and_access_controls.md +++ b/doc/user/admin_area/settings/visibility_and_access_controls.md @@ -2,7 +2,7 @@ type: reference --- -# Visibility and access controls +# Visibility and access controls **(CORE ONLY)** GitLab allows administrators to: diff --git a/doc/user/analytics/cycle_analytics.md b/doc/user/analytics/cycle_analytics.md new file mode 100644 index 00000000000..b7389c8689d --- /dev/null +++ b/doc/user/analytics/cycle_analytics.md @@ -0,0 +1,182 @@ +# Cycle Analytics + +> - Introduced prior to GitLab 12.2 at the project level. +> - [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/12077) in [GitLab Premium](https://about.gitlab.com/pricing/) 12.2 at the group level (enabled by feature flag `analytics`). + +Cycle Analytics measures the time spent to go from an [idea to production] - also known +as cycle time - for each of your projects. Cycle Analytics displays the median time for an idea to +reach production, along with the time typically spent in each DevOps stage along the way. + +Cycle Analytics is useful in order to quickly determine the velocity of a given +project. It points to bottlenecks in the development process, enabling management +to uncover, triage, and identify the root cause of slowdowns in the software development life cycle. + +Cycle Analytics is tightly coupled with the [GitLab flow] and +calculates a separate median for each stage. + +## Overview + +Cycle Analytics is available: + +- From GitLab 12.2, at the group level in the analytics workspace at + **Analytics > Cycle Analytics**. **(PREMIUM)** + + In the future, multiple groups will be selectable which will effectively make this an + instance-level feature. + + NOTE: **Note:** + Requires the [analytics workspace](index.md) to be enabled. + +- At the project level via **Project > Cycle Analytics**. + +There are seven stages that are tracked as part of the Cycle Analytics calculations. + +- **Issue** (Tracker) + - Time to schedule an issue (by milestone or by adding it to an issue board) +- **Plan** (Board) + - Time to first commit +- **Code** (IDE) + - Time to create a merge request +- **Test** (CI) + - Time it takes GitLab CI/CD to test your code +- **Review** (Merge Request/MR) + - Time spent on code review +- **Staging** (Continuous Deployment) + - Time between merging and deploying to production +- **Production** (Total) + - Total lifecycle time; i.e. the velocity of the project or team + +## How the data is measured + +Cycle Analytics records cycle time and data based on the project issues with the +exception of the staging and production stages, where only data deployed to +production are measured. + +Specifically, if your CI is not set up and you have not defined a `production` +or `production/*` [environment], then you will not have any data for those stages. + +Each stage of Cycle Analytics is further described in the table below. + +| **Stage** | **Description** | +| --------- | --------------- | +| Issue | Measures the median time between creating an issue and taking action to solve it, by either labeling it or adding it to a milestone, whatever comes first. The label will be tracked only if it already has an [Issue Board list](../project/issue_board.md#creating-a-new-list) created for it. | +| Plan | Measures the median time between the action you took for the previous stage, and pushing the first commit to the branch. The very first commit of the branch is the one that triggers the separation between **Plan** and **Code**, and at least one of the commits in the branch needs to contain the related issue number (e.g., `#42`). If none of the commits in the branch mention the related issue number, it is not considered to the measurement time of the stage. | +| Code | Measures the median time between pushing a first commit (previous stage) and creating a merge request (MR) related to that commit. The key to keep the process tracked is to include the [issue closing pattern](../project/issues/managing_issues.md#closing-issues-automatically) to the description of the merge request (for example, `Closes #xxx`, where `xxx` is the number of the issue related to this merge request). If the issue closing pattern is not present in the merge request description, the MR is not considered to the measurement time of the stage. | +| Test | Measures the median time to run the entire pipeline for that project. It's related to the time GitLab CI takes to run every job for the commits pushed to that merge request defined in the previous stage. It is basically the start->finish time for all pipelines. | +| Review | Measures the median time taken to review the merge request that has closing issue pattern, between its creation and until it's merged. | +| Staging | Measures the median time between merging the merge request with closing issue pattern until the very first deployment to production. It's tracked by the [environment] set to `production` or matching `production/*` (case-sensitive, `Production` won't work) in your GitLab CI configuration. If there isn't a production environment, this is not tracked. | +| Production| The sum of all time (medians) taken to run the entire process, from issue creation to deploying the code to production. | + +--- + +How this works, behind the scenes: + +1. Issues and merge requests are grouped together in pairs, such that for each + `<issue, merge request>` pair, the merge request has the [issue closing pattern](../project/issues/managing_issues.md#closing-issues-automatically) + for the corresponding issue. All other issues and merge requests are **not** + considered. +1. Then the `<issue, merge request>` pairs are filtered out by last XX days (specified + by the UI - default is 90 days). So it prohibits these pairs from being considered. +1. For the remaining `<issue, merge request>` pairs, we check the information that + we need for the stages, like issue creation date, merge request merge time, + etc. + +To sum up, anything that doesn't follow [GitLab flow] will not be tracked and the +Cycle Analytics dashboard will not present any data for: + +- merge requests that do not close an issue. +- issues not labeled with a label present in the Issue Board or for issues not assigned a milestone. +- staging and production stages, if the project has no `production` or `production/*` + environment. + +## Example workflow + +Below is a simple fictional workflow of a single cycle that happens in a +single day passing through all seven stages. Note that if a stage does not have +a start and a stop mark, it is not measured and hence not calculated in the median +time. It is assumed that milestones are created and CI for testing and setting +environments is configured. + +1. Issue is created at 09:00 (start of **Issue** stage). +1. Issue is added to a milestone at 11:00 (stop of **Issue** stage / start of + **Plan** stage). +1. Start working on the issue, create a branch locally and make one commit at + 12:00. +1. Make a second commit to the branch which mentions the issue number at 12.30 + (stop of **Plan** stage / start of **Code** stage). +1. Push branch and create a merge request that contains the [issue closing pattern](../project/issues/managing_issues.md#closing-issues-automatically) + in its description at 14:00 (stop of **Code** stage / start of **Test** and + **Review** stages). +1. The CI starts running your scripts defined in [`.gitlab-ci.yml`][yml] and + takes 5min (stop of **Test** stage). +1. Review merge request, ensure that everything is OK and merge the merge + request at 19:00. (stop of **Review** stage / start of **Staging** stage). +1. Now that the merge request is merged, a deployment to the `production` + environment starts and finishes at 19:30 (stop of **Staging** stage). +1. The cycle completes and the sum of the median times of the previous stages + is recorded to the **Production** stage. That is the time between creating an + issue and deploying its relevant merge request to production. + +From the above example you can conclude the time it took each stage to complete +as long as their total time: + +- **Issue**: 2h (11:00 - 09:00) +- **Plan**: 1h (12:00 - 11:00) +- **Code**: 2h (14:00 - 12:00) +- **Test**: 5min +- **Review**: 5h (19:00 - 14:00) +- **Staging**: 30min (19:30 - 19:00) +- **Production**: Since this stage measures the sum of median time off all + previous stages, we cannot calculate it if we don't know the status of the + stages before. In case this is the very first cycle that is run in the project, + then the **Production** time is 10h 30min (19:30 - 09:00) + +A few notes: + +- In the above example we demonstrated that it doesn't matter if your first + commit doesn't mention the issue number, you can do this later in any commit + of the branch you are working on. +- You can see that the **Test** stage is not calculated to the overall time of + the cycle since it is included in the **Review** process (every MR should be + tested). +- The example above was just **one cycle** of the seven stages. Add multiple + cycles, calculate their median time and the result is what the dashboard of + Cycle Analytics is showing. + +## Permissions + +The current permissions on the Project Cycle Analytics dashboard are: + +- Public projects - anyone can access +- Internal projects - any authenticated user can access +- Private projects - any member Guest and above can access + +You can [read more about permissions][permissions] in general. + +NOTE: **Note:** +As of GitLab 12.2, the project-level page is deprecated. You should access +project-level Cycle Analytics from **Analytics > Cycle Analytics** in the top +navigation bar. We will ensure that the same project-level functionality is available +to CE users in the new analytics space. + +For Cycle Analytics functionality introduced in GitLab 12.2 and later: + +- Users must have Reporter access or above. +- Features are available only on + [Premium or Silver tiers](https://about.gitlab.com/pricing/) and above. + +## More resources + +Learn more about Cycle Analytics in the following resources: + +- [Cycle Analytics feature page](https://about.gitlab.com/features/cycle-analytics/) +- [Cycle Analytics feature preview](https://about.gitlab.com/2016/09/16/feature-preview-introducing-cycle-analytics/) +- [Cycle Analytics feature highlight](https://about.gitlab.com/2016/09/21/cycle-analytics-feature-highlight/) + +[ce-5986]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5986 +[ce-20975]: https://gitlab.com/gitlab-org/gitlab-ce/issues/20975 +[environment]: ../../ci/yaml/README.md#environment +[GitLab flow]: ../../workflow/gitlab_flow.md +[idea to production]: https://about.gitlab.com/2016/08/05/continuous-integration-delivery-and-deployment-with-gitlab/#from-idea-to-production-with-gitlab +[permissions]: ../permissions.md +[yml]: ../../ci/yaml/README.md diff --git a/doc/user/analytics/index.md b/doc/user/analytics/index.md new file mode 100644 index 00000000000..ec719c0b4a1 --- /dev/null +++ b/doc/user/analytics/index.md @@ -0,0 +1,22 @@ +# Analytics workspace + +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/12077) in GitLab 12.2 (enabled using `analytics` feature flag). + +The Analytics workspace will make it possible to aggregate analytics across +GitLab, so that users can view information across multiple projects and groups +in one place. + +To access the centralized analytics workspace: + +1. Ensure it's enabled. Requires a GitLab administrator to enable it with the `analytics` feature + flag. +1. Once enabled, click on **Analytics** from the top navigation bar. + +## Available analytics + +From the centralized analytics workspace, the following analytics are available: + +- [Cycle Analytics](cycle_analytics.md). + +NOTE: **Note:** +Project-level Cycle Analytics are still available at a project's **Project > Cycle Analytics**. diff --git a/doc/user/application_security/dependency_list/img/dependency_list_v12_2.png b/doc/user/application_security/dependency_list/img/dependency_list_v12_2.png Binary files differnew file mode 100644 index 00000000000..af9cee08d71 --- /dev/null +++ b/doc/user/application_security/dependency_list/img/dependency_list_v12_2.png diff --git a/doc/user/application_security/dependency_list/index.md b/doc/user/application_security/dependency_list/index.md new file mode 100644 index 00000000000..38c38bbd8a9 --- /dev/null +++ b/doc/user/application_security/dependency_list/index.md @@ -0,0 +1,49 @@ +# Dependency List **(ULTIMATE)** + +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/10075) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 12.0. + +The Dependency list allows you to see your project's dependencies, and key +details about them, including their known vulnerabilities. To see it, +navigate to **Security & Compliance > Dependency List** in your project's +sidebar. + +## Requirements + +1. The [Dependency Scanning](../dependency_scanning/index.md) CI job must be + configured for your project. +1. Your project uses at least one of the + [languages and package managers](../dependency_scanning/index.md#supported-languages-and-package-managers) + supported by Gemnasium. + +## Viewing dependencies + +![Dependency List](img/dependency_list_v12_2.png) + +Dependencies are displayed with the following information: + +| Field | Description | +| --------- | ----------- | +| Status | Displays whether or not the dependency has any known vulnerabilities | +| Component | The dependency's name | +| Version | The exact locked version of the dependency your project uses | +| Packager | The packager used to install the depedency | +| Location | A link to the packager-specific lockfile in your project that declared the dependency | + +Dependencies shown are initially sorted by their names. They can also be sorted +by the packager they were installed by, or by the severity of their known +vulnerabilities. + +There is a second list under the `Vulnerable components` tab displaying only +those dependencies with known vulnerabilities. If there are none, this tab is +disabled. + +### Vulnerabilities + +If a dependency has known vulnerabilities, they can be viewed by clicking on the +`Status` cell of that dependency. The severity and description of each +vulnerability will then be displayed below it. + +## Downloading the Dependency List + +Your project's full list of dependencies and their details can be downloaded in +`JSON` format by clicking on the download button. diff --git a/doc/user/application_security/dependency_scanning/index.md b/doc/user/application_security/dependency_scanning/index.md index 10b4d9d4c7c..3148ec63c79 100644 --- a/doc/user/application_security/dependency_scanning/index.md +++ b/doc/user/application_security/dependency_scanning/index.md @@ -327,16 +327,11 @@ Once a vulnerability is found, you can interact with it. Read more on how to For more information about the vulnerabilities database update, check the [maintenance table](../index.md#maintenance-and-update-of-the-vulnerabilities-database). -## Dependency List +## Dependency List **(ULTIMATE)** -> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/10075) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 12.0. - -An additional benefit of Dependency Scanning is the ability to get a list of your -project's dependencies with their versions. This list can be generated only for -[languages and package managers](#supported-languages-and-package-managers) -supported by Gemnasium. - -To see the generated dependency list, navigate to your project's **Security & Compliance > Dependency List**. +An additional benefit of Dependency Scanning is the ability to view your +project's dependencies and their known vulnerabilities. Read more about +the [Dependency List](../dependency_list/index.md). ## Versioning and release process diff --git a/doc/user/application_security/index.md b/doc/user/application_security/index.md index 4dcb416c110..fcd683ca2db 100644 --- a/doc/user/application_security/index.md +++ b/doc/user/application_security/index.md @@ -25,9 +25,10 @@ GitLab can scan and report any vulnerabilities found in your project. | Secure scanning tool | Description | |:-----------------------------------------------------------------------------|:-----------------------------------------------------------------------| | [Container Scanning](container_scanning/index.md) **(ULTIMATE)** | Scan Docker containers for known vulnerabilities. | +| [Dependency List](dependency_list/index.md) **(ULTIMATE)** | View your project's dependencies and their known vulnerabilities. | | [Dependency Scanning](dependency_scanning/index.md) **(ULTIMATE)** | Analyze your dependencies for known vulnerabilities. | | [Dynamic Application Security Testing (DAST)](dast/index.md) **(ULTIMATE)** | Analyze running web applications for known vulnerabilities. | -| [License Management](license_management/index.md) **(ULTIMATE)** | Search your project's dependencies for their licenses. | +| [License Compliance](license_management/index.md) **(ULTIMATE)** | Search your project's dependencies for their licenses. | | [Security Dashboard](security_dashboard/index.md) **(ULTIMATE)** | View vulnerabilities in all your projects and groups. | | [Static Application Security Testing (SAST)](sast/index.md) **(ULTIMATE)** | Analyze source code for known vulnerabilities. | diff --git a/doc/user/application_security/license_management/index.md b/doc/user/application_security/license_management/index.md index c324848c703..912f2f0e209 100644 --- a/doc/user/application_security/license_management/index.md +++ b/doc/user/application_security/license_management/index.md @@ -2,7 +2,7 @@ type: reference, howto --- -# License Management **(ULTIMATE)** +# License Compliance **(ULTIMATE)** > [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/5483) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 11.0. @@ -10,18 +10,18 @@ in [GitLab Ultimate](https://about.gitlab.com/pricing/) 11.0. ## Overview If you are using [GitLab CI/CD](../../../ci/README.md), you can search your project dependencies for their licenses -using License Management. +using License Compliance. -You can take advantage of License Management by either [including the job](#configuration) +You can take advantage of License Compliance by either [including the job](#configuration) in your existing `.gitlab-ci.yml` file or by implicitly using -[Auto License Management](../../../topics/autodevops/index.md#auto-license-management-ultimate) +[Auto License Compliance](../../../topics/autodevops/index.md#auto-license-compliance-ultimate) that is provided by [Auto DevOps](../../../topics/autodevops/index.md). -GitLab checks the License Management report, compares the licenses between the +GitLab checks the License Compliance report, compares the licenses between the source and target branches, and shows the information right on the merge request. Blacklisted licenses will be clearly visible with an `x` red icon next to them as well as new licenses which need a decision from you. In addition, you can -[manually approve or blacklist](#project-policies-for-license-management) +[manually approve or blacklist](#project-policies-for-license-compliance) licenses in your project's settings. NOTE: **Note:** @@ -31,7 +31,7 @@ will be displayed in the merge request area. That is the case when you add the Consecutive merge requests will have something to compare to and the license management report will be shown properly. -![License Management Widget](img/license_management.png) +![License Compliance Widget](img/license_management.png) If you are a project or group Maintainer, you can click on a license to be given the choice to approve it or blacklist it. @@ -66,12 +66,12 @@ The following languages and package managers are supported. ## Requirements -To run a License Management scanning job, you need GitLab Runner with the +To run a License Compliance scanning job, you need GitLab Runner with the [`docker` executor](https://docs.gitlab.com/runner/executors/docker.html). ## Configuration -For GitLab 11.9 and later, to enable License Management, you must +For GitLab 11.9 and later, to enable License Compliance, you must [include](../../../ci/yaml/README.md#includetemplate) the [`License-Management.gitlab-ci.yml` template](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/lib/gitlab/ci/templates/Security/License-Management.gitlab-ci.yml) that's provided as a part of your GitLab installation. @@ -89,14 +89,14 @@ The included template will create a `license_management` job in your CI/CD pipel and scan your dependencies to find their licenses. The results will be saved as a -[License Management report artifact](../../../ci/yaml/README.md#artifactsreportslicense_management-ultimate) +[License Compliance report artifact](../../../ci/yaml/README.md#artifactsreportslicense_management-ultimate) that you can later download and analyze. Due to implementation limitations, we -always take the latest License Management artifact available. Behind the scenes, the -[GitLab License Management Docker image](https://gitlab.com/gitlab-org/security-products/license-management) +always take the latest License Compliance artifact available. Behind the scenes, the +[GitLab License Compliance Docker image](https://gitlab.com/gitlab-org/security-products/license-management) is used to detect the languages/frameworks and in turn analyzes the licenses. -The License Management settings can be changed through environment variables by using the -[`variables`](../../../ci/yaml/README.md#variables) parameter in `.gitlab-ci.yml`. These variables are documented in the [License Management documentation](https://gitlab.com/gitlab-org/security-products/license-management#settings). +The License Compliance settings can be changed through environment variables by using the +[`variables`](../../../ci/yaml/README.md#variables) parameter in `.gitlab-ci.yml`. These variables are documented in the [License Compliance documentation](https://gitlab.com/gitlab-org/security-products/license-management#settings). ### Installing custom dependencies @@ -143,7 +143,7 @@ license_management: ### Configuring Maven projects -The License Management tool provides a `MAVEN_CLI_OPTS` environment variable which can hold +The License Compliance tool provides a `MAVEN_CLI_OPTS` environment variable which can hold the command line arguments to pass to the `mvn install` command which is executed under the hood. Feel free to use it for the customization of Maven execution. For example: @@ -169,7 +169,7 @@ If you still need to run tests during `mvn install`, add `-DskipTests=false` to > [Introduced](https://gitlab.com/gitlab-org/security-products/license-management/merge_requests/36) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 12.0. -License Management uses Python 2.7 and pip 10.0 by default. +License Compliance uses Python 2.7 and pip 10.0 by default. If your project requires Python 3, you can switch to Python 3.5 and pip 19.1 by setting the `LM_PYTHON_VERSION` environment variable to `3`. @@ -182,7 +182,7 @@ license_management: LM_PYTHON_VERSION: 3 ``` -## Project policies for License Management +## Project policies for License Compliance > [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/5940) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 11.4. @@ -196,10 +196,10 @@ To approve or blacklist a license: 1. Either use the **Manage licenses** button in the merge request widget, or navigate to the project's **Settings > CI/CD** and expand the - **License Management** section. + **License Compliance** section. 1. Click the **Add a license** button. - ![License Management Add License](img/license_management_add_license.png) + ![License Compliance Add License](img/license_management_add_license.png) 1. In the **License name** dropdown, either: - Select one of the available licenses. You can search for licenses in the field @@ -211,17 +211,17 @@ To approve or blacklist a license: To modify an existing license: -1. In the **License Management** list, click the **Approved/Declined** dropdown to change it to the desired status. +1. In the **License Compliance** list, click the **Approved/Declined** dropdown to change it to the desired status. - ![License Management Settings](img/license_management_settings.png) + ![License Compliance Settings](img/license_management_settings.png) Searching for Licenses: 1. Use the **Search** box to search for a specific license. - ![License Management Search](img/license_management_search.png) + ![License Compliance Search](img/license_management_search.png) -## License Management report under pipelines +## License Compliance report under pipelines > [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/5491) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 11.2. @@ -230,7 +230,7 @@ From your project's left sidebar, navigate to **CI/CD > Pipelines** and click on pipeline ID that has a `license_management` job to see the Licenses tab with the listed licenses (if any). -![License Management Pipeline Tab](img/license_management_pipeline_tab.png) +![License Compliance Pipeline Tab](img/license_management_pipeline_tab.png) <!-- ## Troubleshooting diff --git a/doc/user/asciidoc.md b/doc/user/asciidoc.md index df86b2a1cbe..862316b57da 100644 --- a/doc/user/asciidoc.md +++ b/doc/user/asciidoc.md @@ -277,11 +277,11 @@ source - a listing that is embellished with (colorized) syntax highlighting ---- ``` -````asciidoc +~~~asciidoc \```language fenced code - a shorthand syntax for the source block \``` -```` +~~~ ```asciidoc [,attribution,citetitle] diff --git a/doc/user/discussions/img/make_suggestion.png b/doc/user/discussions/img/make_suggestion.png Binary files differindex 20acc1417da..a24e29770aa 100644 --- a/doc/user/discussions/img/make_suggestion.png +++ b/doc/user/discussions/img/make_suggestion.png diff --git a/doc/user/discussions/img/suggestion.png b/doc/user/discussions/img/suggestion.png Binary files differindex 68a67e6ae5e..f7962305a15 100644 --- a/doc/user/discussions/img/suggestion.png +++ b/doc/user/discussions/img/suggestion.png diff --git a/doc/user/gitlab_com/index.md b/doc/user/gitlab_com/index.md index 928950126da..af37cc896ad 100644 --- a/doc/user/gitlab_com/index.md +++ b/doc/user/gitlab_com/index.md @@ -43,13 +43,15 @@ Host gitlab.com Below are the settings for [GitLab Pages]. -| Setting | GitLab.com | Default | -| ----------------------- | ---------------- | ------------- | -| Domain name | `gitlab.io` | - | -| IP address | `35.185.44.232` | - | -| Custom domains support | yes | no | -| TLS certificates support| yes | no | +| Setting | GitLab.com | Default | +| --------------------------- | ---------------- | ------------- | +| Domain name | `gitlab.io` | - | +| IP address | `35.185.44.232` | - | +| Custom domains support | yes | no | +| TLS certificates support | yes | no | +| Maximum size (uncompressed) | 1G | 100M | +NOTE: **Note:** The maximum size of your Pages site is regulated by the artifacts maximum size which is part of [GitLab CI/CD](#gitlab-cicd). @@ -59,7 +61,7 @@ Below are the current settings regarding [GitLab CI/CD](../../ci/README.md). | Setting | GitLab.com | Default | | ----------- | ----------------- | ------------- | -| Artifacts maximum size | 1G | 100M | +| Artifacts maximum size (uncompressed) | 1G | 100M | | Artifacts [expiry time](../../ci/yaml/README.md#artifactsexpire_in) | kept forever | deleted after 30 days unless otherwise specified | ## Repository size limit @@ -81,13 +83,12 @@ IP based firewall can be configured by looking up all ## Shared Runners -Shared Runners on GitLab.com run in [autoscale mode] and powered by -Google Cloud Platform. Autoscaling means reduced -waiting times to spin up CI/CD jobs, and isolated VMs for each project, -thus maximizing security. -They're free to use for public open source projects and limited to 2000 CI -minutes per month per group for private projects. Read about all -[GitLab.com plans](https://about.gitlab.com/pricing/). +Shared Runners on GitLab.com run in [autoscale mode] and powered by Google Cloud Platform. +Autoscaling means reduced waiting times to spin up CI/CD jobs, and isolated VMs for each project, +thus maximizing security. They're free to use for public open source projects and limited +to 2000 CI minutes per month per group for private projects. More minutes +[can be purchased](../../subscriptions/index.md#extra-shared-runners-pipeline-minutes), if +needed. Read about all [GitLab.com plans](https://about.gitlab.com/pricing/). All your CI/CD jobs run on [n1-standard-1 instances](https://cloud.google.com/compute/docs/machine-types) with 3.75GB of RAM, CoreOS and the latest Docker Engine installed. Instances provide 1 vCPU and 25GB of HDD disk space. The default @@ -113,57 +114,6 @@ Below are the shared Runners settings. The full contents of our `config.toml` are: -**DigitalOcean** - -```toml -concurrent = X -check_interval = 1 -metrics_server = "X" -sentry_dsn = "X" - -[[runners]] - name = "docker-auto-scale" - request_concurrency = X - url = "https://gitlab.com/" - token = "SHARED_RUNNER_TOKEN" - executor = "docker+machine" - environment = [ - "DOCKER_DRIVER=overlay2" - ] - limit = X - [runners.docker] - image = "ruby:2.5" - privileged = true - [runners.machine] - IdleCount = 20 - IdleTime = 1800 - OffPeakPeriods = ["* * * * * sat,sun *"] - OffPeakTimezone = "UTC" - OffPeakIdleCount = 5 - OffPeakIdleTime = 1800 - MaxBuilds = 1 - MachineName = "srm-%s" - MachineDriver = "digitalocean" - MachineOptions = [ - "digitalocean-image=X", - "digitalocean-ssh-user=core", - "digitalocean-region=nyc1", - "digitalocean-size=s-2vcpu-2gb", - "digitalocean-private-networking", - "digitalocean-tags=shared_runners,gitlab_com", - "engine-registry-mirror=http://INTERNAL_IP_OF_OUR_REGISTRY_MIRROR", - "digitalocean-access-token=DIGITAL_OCEAN_ACCESS_TOKEN", - ] - [runners.cache] - Type = "s3" - BucketName = "runner" - Insecure = true - Shared = true - ServerAddress = "INTERNAL_IP_OF_OUR_CACHE_SERVER" - AccessKey = "ACCESS_KEY" - SecretKey = "ACCESS_SECRET_KEY" -``` - **Google Cloud Platform** ```toml @@ -179,20 +129,25 @@ sentry_dsn = "X" token = "SHARED_RUNNER_TOKEN" executor = "docker+machine" environment = [ - "DOCKER_DRIVER=overlay2" + "DOCKER_DRIVER=overlay2", + "DOCKER_TLS_CERTDIR=" ] limit = X [runners.docker] image = "ruby:2.5" privileged = true + volumes = [ + "/certs/client", + "/dummy-sys-class-dmi-id:/sys/class/dmi/id:ro" # Make kaniko builds work on GCP. + ] [runners.machine] - IdleCount = 20 - IdleTime = 1800 + IdleCount = 50 + IdleTime = 3600 OffPeakPeriods = ["* * * * * sat,sun *"] OffPeakTimezone = "UTC" - OffPeakIdleCount = 5 - OffPeakIdleTime = 1800 - MaxBuilds = 1 + OffPeakIdleCount = 15 + OffPeakIdleTime = 3600 + MaxBuilds = 1 # For security reasons we delete the VM after job has finished so it's not reused. MachineName = "srm-%s" MachineDriver = "google" MachineOptions = [ @@ -203,17 +158,18 @@ sentry_dsn = "X" "google-tags=gitlab-com,srm", "google-use-internal-ip", "google-zone=us-east1-d", + "engine-opt=mtu=1460", # Set MTU for container interface, for more information check https://gitlab.com/gitlab-org/gitlab-runner/issues/3214#note_82892928 "google-machine-image=PROJECT/global/images/IMAGE", - "engine-registry-mirror=http://INTERNAL_IP_OF_OUR_REGISTRY_MIRROR" + "engine-opt=ipv6", # This will create IPv6 interfaces in the containers. + "engine-opt=fixed-cidr-v6=fc00::/7", + "google-operation-backoff-initial-interval=2" # Custom flag from forked docker-machine, for more information check https://github.com/docker/machine/pull/4600 ] [runners.cache] - Type = "s3" - BucketName = "runner" - Insecure = true + Type = "gcs" Shared = true - ServerAddress = "INTERNAL_IP_OF_OUR_CACHE_SERVER" - AccessKey = "ACCESS_KEY" - SecretKey = "ACCESS_SECRET_KEY" + [runners.cache.gcs] + CredentialsFile = "/path/to/file" + BucketName = "bucket-name" ``` ## Sidekiq @@ -223,7 +179,7 @@ and the following environment variables: | Setting | GitLab.com | Default | |-------- |----------- |-------- | -| `SIDEKIQ_MEMORY_KILLER_MAX_RSS` | `1000000` | `1000000` | +| `SIDEKIQ_MEMORY_KILLER_MAX_RSS` | `1000000` | `2000000` | | `SIDEKIQ_MEMORY_KILLER_SHUTDOWN_SIGNAL` | `SIGKILL` | - | | `SIDEKIQ_LOG_ARGUMENTS` | `1` | - | diff --git a/doc/user/group/index.md b/doc/user/group/index.md index 43fd0bfd45a..864f1a397d5 100644 --- a/doc/user/group/index.md +++ b/doc/user/group/index.md @@ -342,7 +342,7 @@ underlying projects, issues, etc, by IP address. This can help ensure that particular content doesn't leave the premises, while not blocking off access to the entire instance. -Add whitelisted IP subnet using CIDR notation to the group settings and anyone +Add one or more whitelisted IP subnets using CIDR notation in comma separated format to the group settings and anyone coming from a different IP address won't be able to access the restricted content. @@ -350,6 +350,38 @@ Restriction currently applies to UI, API access is not restricted. To avoid accidental lock-out, admins and group owners are are able to access the group regardless of the IP restriction. +#### Allowed domain restriction **(PREMIUM ONLY)** + +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/7297) in +[GitLab Premium](https://about.gitlab.com/pricing/) 12.2. + +You can restrict access to groups and their underlying projects by +allowing only users with email addresses in particular domains to be added to the group. + +Add email domains you want to whitelist and users with emails from different +domains won't be allowed to be added to this group. + +Some domains cannot be restricted. These are the most popular public email domains, such as: + +- `gmail.com` +- `yahoo.com` +- `hotmail.com` +- `aol.com` +- `msn.com` +- `hotmail.co.uk` +- `hotmail.fr` +- `live.com` +- `outlook.com` +- `icloud.com` + +To enable this feature: + +1. Navigate to the group's **Settings > General** page. +1. Expand the **Permissions, LFS, 2FA** section, and enter domain name into **Restrict membership by email** field. +1. Click **Save changes**. + +This will enable the domain-checking for all new users added to the group from this moment on. + #### Group file templates **(PREMIUM)** Group file templates allow you to share a set of templates for common file @@ -379,6 +411,17 @@ To enable this feature, navigate to the group settings page, expand the Define project templates at a group level by setting a group as the template source. [Learn more about group-level project templates](custom_project_templates.md). +#### Disabling email notifications + +You can disable all email notifications related to the group, which also includes +it's subgroups and projects. + +To enable this feature: + +1. Navigate to the group's **Settings > General** page. +1. Expand the **Permissions, LFS, 2FA** section, and select **Disable email notifications**. +1. Click **Save changes**. + ### Advanced settings - **Projects**: View all projects within that group, add members to each project, diff --git a/doc/user/group/saml_sso/index.md b/doc/user/group/saml_sso/index.md index e3f657af564..bd50367681e 100644 --- a/doc/user/group/saml_sso/index.md +++ b/doc/user/group/saml_sso/index.md @@ -39,6 +39,25 @@ However, users will not be prompted to log via SSO on each visit. GitLab will ch We intend to add a similar SSO requirement for [Git and API activity](https://gitlab.com/gitlab-org/gitlab-ee/issues/9152) in the future. +#### Group-managed accounts + +[Introduced in GitLab 12.1](https://gitlab.com/groups/gitlab-org/-/epics/709). + +When SSO is being enforced, groups can enable an additional level of protection by enforcing the creation of dedicated user accounts to access the group. + +Without group-managed accounts, users can link their SAML identity with any existing user on the instance. With group-managed accounts enabled, users are required to create a new, dedicated user linked to the group. The notification email address associated with the user is locked to the email address received from the configured identity provider. + +When this option is enabled: + +- All existing and new users in the group will be required to log in via the SSO URL associated with the group. +- On successfully authenticating, GitLab will prompt the user to create a new, dedicated account using the email address received from the configured identity provider. +- After the group managed account has been created, group activity will require the use of this user account. + +Since use of the group managed account requires the use of SSO, users of group managed accounts will lose access to these accounts when they are no longer able to authenticate with the connected identity provider. In the case of an offboarded employee who has been removed from your identity provider: + +- The user will be unable to access the group (their credentials will no longer work on the identity provider when prompted to SSO). +- Contributions in the group (e.g. issues, merge requests) will remain intact. + ### NameID GitLab.com uses the SAML NameID to identify users. The NameID element: diff --git a/doc/user/group/saml_sso/scim_setup.md b/doc/user/group/saml_sso/scim_setup.md index f8bef8b8a6a..5d136ad62da 100644 --- a/doc/user/group/saml_sso/scim_setup.md +++ b/doc/user/group/saml_sso/scim_setup.md @@ -59,15 +59,14 @@ Once [Single sign-on](index.md) has been configured, we can: ### Azure -First, double check the [Single sign-on](index.md) configuration for your group and ensure that **Name identifier value** (NameID) points to `user.objectid` or another unique identifier. This will match the `extern_uid` used on GitLab. +The SAML application that was created during [Single sign-on](index.md) setup now needs to be set up for SCIM. -![Name identifier value mapping](img/scim_name_identifier_mapping.png) +1. Check the configuration for your GitLab SAML app and ensure that **Name identifier value** (NameID) points to `user.objectid` or another unique identifier. This will match the `extern_uid` used on GitLab. -#### Set up admin credentials + ![Name identifier value mapping](img/scim_name_identifier_mapping.png) -Next, configure your GitLab application in Azure by following the -[Provisioning users and groups to applications that support SCIM](https://docs.microsoft.com/en-us/azure/active-directory/manage-apps/use-scim-to-provision-users-and-groups#provisioning-users-and-groups-to-applications-that-support-scim) -section in Azure's SCIM setup documentation. +1. Set up automatic provisioning and administrative credentials by following the + [Provisioning users and groups to applications that support SCIM](https://docs.microsoft.com/en-us/azure/active-directory/manage-apps/use-scim-to-provision-users-and-groups#provisioning-users-and-groups-to-applications-that-support-scim) section in Azure's SCIM setup documentation. During this configuration, note the following: @@ -97,6 +96,7 @@ You can then test the connection by clicking on **Test Connection**. If the conn NOTE: **Note:** If you used a unique identifier **other than** `objectId`, be sure to map it instead to both `id` and `externalId`. 1. Below the mapping list click on **Show advanced options > Edit attribute list for AppName**. + 1. Leave the `id` as the primary and only required field. NOTE: **Note:** @@ -129,8 +129,7 @@ When testing the connection, you may encounter an error: **You appear to have en When checking the Audit Logs for the Provisioning, you can sometimes see the error `Namespace can't be blank, Name can't be blank, and User can't be blank.` -This is likely caused because not all required fields (such as first name and -last name) are present for all users being mapped. +This is likely caused because not all required fields (such as first name and last name) are present for all users being mapped. As a workaround, try an alternate mapping: diff --git a/doc/user/markdown.md b/doc/user/markdown.md index edb1e904f2b..17bbed2945d 100644 --- a/doc/user/markdown.md +++ b/doc/user/markdown.md @@ -57,12 +57,6 @@ render incorrectly: - milk ``` -1. Chocolate - - dark - - milk - ---- - Simply add a space to each nested item to align the `-` with the first character of the top list item (`C` in this case): @@ -265,8 +259,7 @@ this font installed by default. ### Front matter -> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/23331) - in GitLab 11.6. +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/23331) in GitLab 11.6. Front matter is metadata included at the beginning of a markdown document, preceding its content. This data can be used by static site generators such as [Jekyll](https://jekyllrb.com/docs/front-matter/), @@ -533,6 +526,10 @@ This snippet links to `<wiki_root>/miscellaneous.md`: [Link to Related Page](/miscellaneous.md) ``` +### Embedding metrics in GitLab Flavored Markdown + +Metric charts can be embedded within GitLab Flavored Markdown. See [Embedding Metrics within GitLab flavored Markdown](../user/project/integrations/prometheus.md#embedding-metric-charts-within-gitlab-flavored-markdown) for more details. + ## Standard markdown and extensions in GitLab All standard markdown formatting should work as expected within GitLab. Some standard @@ -862,18 +859,6 @@ or underscores ___ ``` -Three or more hyphens, - ---- - -asterisks, - -*** - -or underscores - -___ - ### Images Examples: diff --git a/doc/user/permissions.md b/doc/user/permissions.md index d92435ef724..80d1bf992ec 100644 --- a/doc/user/permissions.md +++ b/doc/user/permissions.md @@ -45,8 +45,9 @@ The following table depicts the various user permission levels in a project. | Leave comments | ✓ (*1*) | ✓ | ✓ | ✓ | ✓ | | View Insights charts **(ULTIMATE)** | ✓ | ✓ | ✓ | ✓ | ✓ | | View approved/blacklisted licenses **(ULTIMATE)** | ✓ | ✓ | ✓ | ✓ | ✓ | -| View license management reports **(ULTIMATE)** | ✓ (*1*) | ✓ | ✓ | ✓ | ✓ | +| View License Compliance reports **(ULTIMATE)** | ✓ (*1*) | ✓ | ✓ | ✓ | ✓ | | View Security reports **(ULTIMATE)** | ✓ (*1*) | ✓ | ✓ | ✓ | ✓ | +| View Dependency list **(ULTIMATE)** | ✓ (*1*) | ✓ | ✓ | ✓ | ✓ | | View [Design Management](project/issues/design_management.md) pages **(PREMIUM)** | ✓ (*1*) | ✓ | ✓ | ✓ | ✓ | | View project code | ✓ (*1*) | ✓ | ✓ | ✓ | ✓ | | Pull project code | ✓ (*1*) | ✓ | ✓ | ✓ | ✓ | @@ -93,7 +94,7 @@ The following table depicts the various user permission levels in a project. | Remove a container registry image | | | ✓ | ✓ | ✓ | | Create/edit/delete project milestones | | | ✓ | ✓ | ✓ | | Use security dashboard **(ULTIMATE)** | | | ✓ | ✓ | ✓ | -| View dependency list **(ULTIMATE)** | | | ✓ | ✓ | ✓ | +| View vulnerabilities in Dependency list **(ULTIMATE)** | | | ✓ | ✓ | ✓ | | Create issue from vulnerability **(ULTIMATE)** | | | ✓ | ✓ | ✓ | | Dismiss vulnerability **(ULTIMATE)** | | | ✓ | ✓ | ✓ | | Apply code change suggestions | | | ✓ | ✓ | ✓ | @@ -125,6 +126,7 @@ The following table depicts the various user permission levels in a project. | Transfer project to another namespace | | | | | ✓ | | Remove project | | | | | ✓ | | Delete issues | | | | | ✓ | +| Disable notification emails | | | | | ✓ | | Force push to protected branches (*4*) | | | | | | | Remove protected branches (*4*) | | | | | | @@ -160,7 +162,7 @@ to learn more. ### Cycle Analytics permissions Find the current permissions on the Cycle Analytics dashboard on -the [documentation on Cycle Analytics permissions](project/cycle_analytics.md#permissions). +the [documentation on Cycle Analytics permissions](analytics/cycle_analytics.md#permissions). ### Issue Board permissions @@ -219,6 +221,7 @@ group. | Remove group | | | | | ✓ | | Delete group epic **(ULTIMATE)** | | | | | ✓ | | View group Audit Events | | | | | ✓ | +| Disable notification emails | | | | | ✓ | - (1): Groups can be set to [allow either Owners or Owners and Maintainers to create subgroups](group/subgroups/index.md#creating-a-subgroup) @@ -232,6 +235,19 @@ nested groups if you have membership in one of its parents. To learn more, read through the documentation on [subgroups memberships](group/subgroups/index.md#membership). +## Guest User + +When a user is given `Guest` permissions on a project and/or group, and holds no +higher permission level on any other project or group on the instance, the user +is considered a guest user by GitLab and will not consume a license seat. +There is no other specific "guest" designation for newly created users. + +If the user is assigned a higher role on any projects or groups, the user will +take a license seat. If a user creates a project, the user becomes a `Maintainer` +on the project, resulting in the use of a license seat. To prevent a guest user +from creating projects, you can edit the user profile to mark the user as +[External](#external-users-permissions). + ## External users permissions In cases where it is desired that a user has access only to some internal or diff --git a/doc/user/profile/account/two_factor_authentication.md b/doc/user/profile/account/two_factor_authentication.md index e2b1a20a605..f7ba921aa7d 100644 --- a/doc/user/profile/account/two_factor_authentication.md +++ b/doc/user/profile/account/two_factor_authentication.md @@ -48,6 +48,7 @@ To enable 2FA: - [andOTP](https://github.com/andOTP/andOTP): feature rich open source app for Android which supports PGP encrypted backups. - [FreeOTP](https://freeotp.github.io/): open source app for Android. - [Google Authenticator](https://support.google.com/accounts/answer/1066447?hl=en): proprietary app for iOS and Android. + - [SailOTP](https://openrepos.net/content/seiichiro0185/sailotp): open source app for SailFish OS. 1. In the application, add a new entry in one of two ways: - Scan the code presented in GitLab with your device's camera to add the entry automatically. diff --git a/doc/user/profile/personal_access_tokens.md b/doc/user/profile/personal_access_tokens.md index 248188a6457..d556daa3460 100644 --- a/doc/user/profile/personal_access_tokens.md +++ b/doc/user/profile/personal_access_tokens.md @@ -44,7 +44,7 @@ the following table. | Scope | Introduced in | Description | | ------------------ | ------------- | ----------- | | `read_user` | [GitLab 8.15](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5951) | Allows access to the read-only endpoints under `/users`. Essentially, any of the `GET` requests in the [Users API][users] are allowed. | -| `api` | [GitLab 8.15](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5951) | Grants complete access to the API and Container Registry (read/write). | +| `api` | [GitLab 8.15](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5951) | Grants complete read/write access to the API, including all groups and projects, the container registry, and the package registry. | | `read_registry` | [GitLab 9.3](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/11845) | Allows to read (pull) [container registry] images if a project is private and authorization is required. | | `sudo` | [GitLab 10.2](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/14838) | Allows performing API actions as any user in the system (if the authenticated user is an admin). | | `read_repository` | [GitLab 10.7](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/17894) | Allows read-only access (pull) to the repository through git clone. | diff --git a/doc/user/profile/preferences.md b/doc/user/profile/preferences.md index 5bd4b263a58..82a6d2b3703 100644 --- a/doc/user/profile/preferences.md +++ b/doc/user/profile/preferences.md @@ -87,6 +87,16 @@ You have 8 options here that you can use for your default dashboard view: - Assigned Merge Requests - Operations Dashboard **(PREMIUM)** +### Group overview content + +The **Group overview content** dropdown allows you to choose what information is +displayed on a group’s home page. + +You can choose between 2 options: + +- Details (default) +- [Security dashboard](../application_security/security_dashboard/index.md) **(ULTIMATE)** + ### Project overview content The project overview content setting allows you to choose what content you want to diff --git a/doc/user/project/clusters/index.md b/doc/user/project/clusters/index.md index ffaa07cb3a4..cf3a3fef79f 100644 --- a/doc/user/project/clusters/index.md +++ b/doc/user/project/clusters/index.md @@ -173,6 +173,9 @@ Starting from [GitLab 12.1](https://gitlab.com/gitlab-org/gitlab-ce/issues/55902 ### Add existing Kubernetes cluster +NOTE: **Note:** +Kubernetes integration is not supported for arm64 clusters. See the issue [Helm Tiller fails to install on arm64 cluster](https://gitlab.com/gitlab-org/gitlab-ce/issues/64044) for details. + To add an existing Kubernetes cluster to your project: 1. Navigate to your project's **Operations > Kubernetes** page. diff --git a/doc/user/project/cycle_analytics.md b/doc/user/project/cycle_analytics.md index 6707b88c317..87577c9ec88 100644 --- a/doc/user/project/cycle_analytics.md +++ b/doc/user/project/cycle_analytics.md @@ -1,161 +1,5 @@ -# Cycle Analytics - -Cycle Analytics measures the time spent to go from an [idea to production] - also known -as cycle time - for each of your projects. Cycle Analytics displays the median time for an idea to -reach production, along with the time typically spent in each DevOps stage along the way. - -Cycle Analytics is useful in order to quickly determine the velocity of a given -project. It points to bottlenecks in the development process, enabling management -to uncover, triage, and root-cause slowdowns in the software development life cycle. - -Cycle Analytics is tightly coupled with the [GitLab flow] and -calculates a separate median for each stage. - -## Overview - -You can find the Cycle Analytics page under your project's **Project ➔ Cycle -Analytics** tab. - -![Cycle Analytics landing page](img/cycle_analytics_landing_page.png) - -There are seven stages that are tracked as part of the Cycle Analytics calculations. - -- **Issue** (Tracker) - - Time to schedule an issue (by milestone or by adding it to an issue board) -- **Plan** (Board) - - Time to first commit -- **Code** (IDE) - - Time to create a merge request -- **Test** (CI) - - Time it takes GitLab CI/CD to test your code -- **Review** (Merge Request/MR) - - Time spent on code review -- **Staging** (Continuous Deployment) - - Time between merging and deploying to production -- **Production** (Total) - - Total lifecycle time; i.e. the velocity of the project or team - -## How the data is measured - -Cycle Analytics records cycle time and data based on the project issues with the -exception of the staging and production stages, where only data deployed to -production are measured. - -Specifically, if your CI is not set up and you have not defined a `production` -or `production/*` [environment], then you will not have any data for those stages. - -Below you can see in more detail what the various stages of Cycle Analytics mean. - -| **Stage** | **Description** | -| --------- | --------------- | -| Issue | Measures the median time between creating an issue and taking action to solve it, by either labeling it or adding it to a milestone, whatever comes first. The label will be tracked only if it already has an [Issue Board list][board] created for it. | -| Plan | Measures the median time between the action you took for the previous stage, and pushing the first commit to the branch. The very first commit of the branch is the one that triggers the separation between **Plan** and **Code**, and at least one of the commits in the branch needs to contain the related issue number (e.g., `#42`). If none of the commits in the branch mention the related issue number, it is not considered to the measurement time of the stage. | -| Code | Measures the median time between pushing a first commit (previous stage) and creating a merge request (MR) related to that commit. The key to keep the process tracked is to include the [issue closing pattern] to the description of the merge request (for example, `Closes #xxx`, where `xxx` is the number of the issue related to this merge request). If the issue closing pattern is not present in the merge request description, the MR is not considered to the measurement time of the stage. | -| Test | Measures the median time to run the entire pipeline for that project. It's related to the time GitLab CI takes to run every job for the commits pushed to that merge request defined in the previous stage. It is basically the start->finish time for all pipelines. | -| Review | Measures the median time taken to review the merge request that has closing issue pattern, between its creation and until it's merged. | -| Staging | Measures the median time between merging the merge request with closing issue pattern until the very first deployment to production. It's tracked by the [environment] set to `production` or matching `production/*` (case-sensitive, `Production` won't work) in your GitLab CI configuration. If there isn't a production environment, this is not tracked. | -| Production| The sum of all time (medians) taken to run the entire process, from issue creation to deploying the code to production. | - +--- +redirect_to: '../analytics/cycle_analytics.md' --- -Here's a little explanation of how this works behind the scenes: - -1. Issues and merge requests are grouped together in pairs, such that for each - `<issue, merge request>` pair, the merge request has the [issue closing pattern] - for the corresponding issue. All other issues and merge requests are **not** - considered. -1. Then the `<issue, merge request>` pairs are filtered out by last XX days (specified - by the UI - default is 90 days). So it prohibits these pairs from being considered. -1. For the remaining `<issue, merge request>` pairs, we check the information that - we need for the stages, like issue creation date, merge request merge time, - etc. - -To sum up, anything that doesn't follow the [GitLab flow] won't be tracked at all. -So, the Cycle Analytics dashboard won't present any data: - -- For merge requests that do not close an issue. -- For issues not labeled with a label present in the Issue Board or for issues not assigned a milestone. -- For staging and production stages, if the project has no `production` or `production/*` - environment. - -## Example workflow - -Below is a simple fictional workflow of a single cycle that happens in a -single day passing through all seven stages. Note that if a stage does not have -a start and a stop mark, it is not measured and hence not calculated in the median -time. It is assumed that milestones are created and CI for testing and setting -environments is configured. - -1. Issue is created at 09:00 (start of **Issue** stage). -1. Issue is added to a milestone at 11:00 (stop of **Issue** stage / start of - **Plan** stage). -1. Start working on the issue, create a branch locally and make one commit at - 12:00. -1. Make a second commit to the branch which mentions the issue number at 12.30 - (stop of **Plan** stage / start of **Code** stage). -1. Push branch and create a merge request that contains the [issue closing pattern] - in its description at 14:00 (stop of **Code** stage / start of **Test** and - **Review** stages). -1. The CI starts running your scripts defined in [`.gitlab-ci.yml`][yml] and - takes 5min (stop of **Test** stage). -1. Review merge request, ensure that everything is OK and merge the merge - request at 19:00. (stop of **Review** stage / start of **Staging** stage). -1. Now that the merge request is merged, a deployment to the `production` - environment starts and finishes at 19:30 (stop of **Staging** stage). -1. The cycle completes and the sum of the median times of the previous stages - is recorded to the **Production** stage. That is the time between creating an - issue and deploying its relevant merge request to production. - -From the above example you can conclude the time it took each stage to complete -as long as their total time: - -- **Issue**: 2h (11:00 - 09:00) -- **Plan**: 1h (12:00 - 11:00) -- **Code**: 2h (14:00 - 12:00) -- **Test**: 5min -- **Review**: 5h (19:00 - 14:00) -- **Staging**: 30min (19:30 - 19:00) -- **Production**: Since this stage measures the sum of median time off all - previous stages, we cannot calculate it if we don't know the status of the - stages before. In case this is the very first cycle that is run in the project, - then the **Production** time is 10h 30min (19:30 - 09:00) - -A few notes: - -- In the above example we demonstrated that it doesn't matter if your first - commit doesn't mention the issue number, you can do this later in any commit - of the branch you are working on. -- You can see that the **Test** stage is not calculated to the overall time of - the cycle since it is included in the **Review** process (every MR should be - tested). -- The example above was just **one cycle** of the seven stages. Add multiple - cycles, calculate their median time and the result is what the dashboard of - Cycle Analytics is showing. - -## Permissions - -The current permissions on the Cycle Analytics dashboard are: - -- Public projects - anyone can access -- Internal projects - any authenticated user can access -- Private projects - any member Guest and above can access - -You can [read more about permissions][permissions] in general. - -## More resources - -Learn more about Cycle Analytics in the following resources: - -- [Cycle Analytics feature page](https://about.gitlab.com/features/cycle-analytics/) -- [Cycle Analytics feature preview](https://about.gitlab.com/2016/09/16/feature-preview-introducing-cycle-analytics/) -- [Cycle Analytics feature highlight](https://about.gitlab.com/2016/09/21/cycle-analytics-feature-highlight/) - -[board]: issue_board.md#creating-a-new-list -[ce-5986]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5986 -[ce-20975]: https://gitlab.com/gitlab-org/gitlab-ce/issues/20975 -[environment]: ../../ci/yaml/README.md#environment -[GitLab flow]: ../../workflow/gitlab_flow.md -[idea to production]: https://about.gitlab.com/2016/08/05/continuous-integration-delivery-and-deployment-with-gitlab/#from-idea-to-production-with-gitlab -[issue closing pattern]: issues/managing_issues.md#closing-issues-automatically -[permissions]: ../permissions.md -[yml]: ../../ci/yaml/README.md +This document was moved to [another location](../analytics/cycle_analytics.md) diff --git a/doc/user/project/description_templates.md b/doc/user/project/description_templates.md index 196874fdc86..f53dc056010 100644 --- a/doc/user/project/description_templates.md +++ b/doc/user/project/description_templates.md @@ -55,7 +55,7 @@ changes you made after picking the template and return it to its initial status. ![Description templates](img/description_templates.png) -## Setting a default template for issues and merge requests **(STARTER)** +## Setting a default template for merge requests and issues **(STARTER)** > **Notes:** > @@ -66,20 +66,20 @@ changes you made after picking the template and return it to its initial status. > - Templates for merge requests were [introduced][ee-7478ece] in GitLab EE 6.9. The visibility of issues and/or merge requests should be set to either "Everyone -with access" or "Only Project Members" in your project's **Settings** otherwise the +with access" or "Only Project Members" in your project's **Settings / Visibility, project features, permissions** section, otherwise the template text areas won't show. This is the default behavior so in most cases you should be fine. -Go to your project's **Settings** and fill in the "Default description template -for issues" and "Default description template for merge requests" text areas -for issues and merge requests respectively. Since GitLab issues and merge -request support [Markdown](../markdown.md), you can use special markup like +Go to your project's **Settings** and under the **Merge requests** header, click *Expand* and fill in the "Default description template +for merge requests" text area. Under the **Default issue template**, click *Expand* and fill in "Default description template for issues" text area. Since GitLab merge request and issues + support [Markdown](../markdown.md), you can use special markup like headings, lists, etc. -![Default description templates](img/description_templates_default_settings.png) +![Default merge request description templates](img/description_templates_merge_request_settings.png) +![Default issue description templates](img/description_templates_issue_settings.png) After you add the description, hit **Save changes** for the settings to take -effect. Now, every time a new issue or merge request is created, it will be +effect. Now, every time a new merge request or issue is created, it will be pre-filled with the text you entered in the template(s). ## Description template example diff --git a/doc/user/project/img/cycle_analytics_landing_page.png b/doc/user/project/img/cycle_analytics_landing_page.png Binary files differdeleted file mode 100644 index c0c07e84a82..00000000000 --- a/doc/user/project/img/cycle_analytics_landing_page.png +++ /dev/null diff --git a/doc/user/project/img/description_templates_default_settings.png b/doc/user/project/img/description_templates_default_settings.png Binary files differdeleted file mode 100644 index ab314e83d06..00000000000 --- a/doc/user/project/img/description_templates_default_settings.png +++ /dev/null diff --git a/doc/user/project/img/description_templates_issue_settings.png b/doc/user/project/img/description_templates_issue_settings.png Binary files differnew file mode 100644 index 00000000000..53328108835 --- /dev/null +++ b/doc/user/project/img/description_templates_issue_settings.png diff --git a/doc/user/project/img/description_templates_merge_request_settings.png b/doc/user/project/img/description_templates_merge_request_settings.png Binary files differnew file mode 100644 index 00000000000..eda264f7f37 --- /dev/null +++ b/doc/user/project/img/description_templates_merge_request_settings.png diff --git a/doc/user/project/import/gitlab_com.md b/doc/user/project/import/gitlab_com.md index f48a158e2d3..2f87f257754 100644 --- a/doc/user/project/import/gitlab_com.md +++ b/doc/user/project/import/gitlab_com.md @@ -1,21 +1,21 @@ # Project importing from GitLab.com to your private GitLab instance -You can import your existing GitLab.com projects to your GitLab instance. But keep in mind that it is possible only if -GitLab.com integration is enabled on your GitLab instance. +You can import your existing GitLab.com projects to your GitLab instance, but keep in +mind that it is possible only if GitLab.com integration is enabled on your GitLab instance. [Read more about GitLab.com integration for self-managed GitLab instances](../../../integration/gitlab.md). To get to the importer page you need to go to "New project" page. >**Note:** -If you are interested in importing Wiki and Merge Request data to your new -instance, you'll need to follow the instructions for [project export](../settings/import_export.md) +If you are interested in importing Wiki and Merge Request data to your new instance, +you'll need to follow the instructions for [exporting a project](../settings/import_export.md#exporting-a-project-and-its-data) -![New project page](img/gitlab_new_project_page.png) +![New project page](img/gitlab_new_project_page_v12_2.png) -Click on the "Import projects from GitLab.com" link and you will be redirected to GitLab.com +Go to the **Import Projects** tab, then click on **GitLab.com**, and you will be redirected to GitLab.com for permission to access your projects. After accepting, you'll be automatically redirected to the importer. ![Importer page](img/gitlab_importer.png) -To import a project, you can simple click "Import". The importer will import your repository and issues. +To import a project, click "Import". The importer will import your repository and issues. Once the importer is done, a new GitLab project will be created with your imported data. diff --git a/doc/user/project/import/img/gitlab_new_project_page.png b/doc/user/project/import/img/gitlab_new_project_page.png Binary files differdeleted file mode 100644 index c673724f436..00000000000 --- a/doc/user/project/import/img/gitlab_new_project_page.png +++ /dev/null diff --git a/doc/user/project/import/img/gitlab_new_project_page_v12_2.png b/doc/user/project/import/img/gitlab_new_project_page_v12_2.png Binary files differnew file mode 100644 index 00000000000..e79c27f32c0 --- /dev/null +++ b/doc/user/project/import/img/gitlab_new_project_page_v12_2.png diff --git a/doc/user/project/index.md b/doc/user/project/index.md index 45e96437517..932e7fd10b2 100644 --- a/doc/user/project/index.md +++ b/doc/user/project/index.md @@ -98,7 +98,8 @@ When you create a project in GitLab, you'll have access to a large number of - [Maven packages](packages/maven_repository.md): your private Maven repository in GitLab. **(PREMIUM)** - [NPM packages](packages/npm_registry.md): your private NPM package registry in GitLab. **(PREMIUM)** - [Code owners](code_owners.md): specify code owners for certain files **(STARTER)** -- [License Management](../application_security/license_management/index.md): approve and blacklist licenses for projects. **(ULTIMATE)** +- [License Compliance](../application_security/license_management/index.md): approve and blacklist licenses for projects. **(ULTIMATE)** +- [Dependency List](../application_security/dependency_list/index.md): view project dependencies. **(ULTIMATE)** ### Project integrations diff --git a/doc/user/project/integrations/img/download_as_csv.png b/doc/user/project/integrations/img/download_as_csv.png Binary files differnew file mode 100644 index 00000000000..0ed5ab8db89 --- /dev/null +++ b/doc/user/project/integrations/img/download_as_csv.png diff --git a/doc/user/project/integrations/img/embed_metrics.png b/doc/user/project/integrations/img/embed_metrics.png Binary files differnew file mode 100644 index 00000000000..6f9660c9aec --- /dev/null +++ b/doc/user/project/integrations/img/embed_metrics.png diff --git a/doc/user/project/integrations/img/generate_link_to_chart.png b/doc/user/project/integrations/img/generate_link_to_chart.png Binary files differnew file mode 100644 index 00000000000..03e018969b1 --- /dev/null +++ b/doc/user/project/integrations/img/generate_link_to_chart.png diff --git a/doc/user/project/integrations/img/jira_service_page.png b/doc/user/project/integrations/img/jira_service_page.png Binary files differdeleted file mode 100644 index 76fd5f4641c..00000000000 --- a/doc/user/project/integrations/img/jira_service_page.png +++ /dev/null diff --git a/doc/user/project/integrations/img/jira_service_page_v12_2.png b/doc/user/project/integrations/img/jira_service_page_v12_2.png Binary files differnew file mode 100644 index 00000000000..ba7dad9b438 --- /dev/null +++ b/doc/user/project/integrations/img/jira_service_page_v12_2.png diff --git a/doc/user/project/integrations/jira.md b/doc/user/project/integrations/jira.md index ca990ee6c32..61f6f6c9412 100644 --- a/doc/user/project/integrations/jira.md +++ b/doc/user/project/integrations/jira.md @@ -93,7 +93,7 @@ even if the status you are changing to is the same. After saving the configuration, your GitLab project will be able to interact with all Jira projects in your Jira instance and you'll see the Jira link on the GitLab project pages that takes you to the appropriate Jira project. -![Jira service page](img/jira_service_page.png) +![Jira service page](img/jira_service_page_v12_2.png) ## Jira issues diff --git a/doc/user/project/integrations/mattermost.md b/doc/user/project/integrations/mattermost.md index ea58a08e127..6e0f39956d3 100644 --- a/doc/user/project/integrations/mattermost.md +++ b/doc/user/project/integrations/mattermost.md @@ -14,7 +14,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 **Mattermost System Console > Integrations > Integration Management**, or on **Mattermost System Console > Integrations > Custom Integrations** in Mattermost versions 5.11 and earlier. Display name override is not enabled by default, you need to ask your admin to enable it on that same section. diff --git a/doc/user/project/integrations/prometheus.md b/doc/user/project/integrations/prometheus.md index 44439b59e77..9fc0ade809e 100644 --- a/doc/user/project/integrations/prometheus.md +++ b/doc/user/project/integrations/prometheus.md @@ -269,6 +269,12 @@ Note the following properties: ![single stat panel type](img/prometheus_dashboard_single_stat_panel_type.png) +### Downloading data as CSV + +Data from Prometheus charts on the metrics dashboard can be downloaded as CSV. + +![Downloading as CSV](img/download_as_csv.png) + ### Setting up alerts for Prometheus metrics **(ULTIMATE)** #### Managed Prometheus instances @@ -354,6 +360,31 @@ Prometheus server. ![Merge Request with Performance Impact](img/merge_request_performance.png) +## Embedding metric charts within GitLab Flavored Markdown + +> [Introduced][ce-29691] in GitLab 12.2. +> Requires [Kubernetes](prometheus_library/kubernetes.md) metrics. + +It is possible to display metrics charts within [GitLab Flavored Markdown](../../markdown.md#gitlab-flavored-markdown-gfm). + +To display a metric chart, include a link of the form `https://<root_url>/<project>/environments/<environment_id>/metrics`. + +A single chart may also be embedded. You can generate a link to the chart via the dropdown located on the right side of the chart: + +![Generate Link To Chart](img/generate_link_to_chart.png) + +The following requirements must be met for the metric to unfurl: + +- The `<environment_id>` must correspond to a real environment. +- Prometheus must be monitoring the environment. +- The GitLab instance must be configured to receive data from the environment. +- The user must be allowed access to the monitoring dashboard for the environment ([Reporter or higher](../../permissions.md)). +- The dashboard must have data within the last 8 hours. + + If all of the above are true, then the metric will unfurl as seen below: + +![Embedded Metrics](img/embed_metrics.png) + ## Troubleshooting If the "No data found" screen continues to appear, it could be due to: @@ -376,4 +407,5 @@ If the "No data found" screen continues to appear, it could be due to: [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 +[ce-29691]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/29691 [promgldocs]: ../../../administration/monitoring/prometheus/index.md diff --git a/doc/user/project/issues/design_management.md b/doc/user/project/issues/design_management.md index bffbcb544e3..1324a90e00b 100644 --- a/doc/user/project/issues/design_management.md +++ b/doc/user/project/issues/design_management.md @@ -35,9 +35,19 @@ to be enabled: ## Limitations -- Files uploaded must have a file extension of either `png`, `jpg`, `jpeg`, `gif`, `bmp`, `tiff` or `ico`. The [`svg` extension is not yet supported](https://gitlab.com/gitlab-org/gitlab-ee/issues/12771). +- Files uploaded must have a file extension of either `png`, `jpg`, `jpeg`, `gif`, `bmp`, `tiff` or `ico`. + The [`svg` extension is not yet supported](https://gitlab.com/gitlab-org/gitlab-ee/issues/12771). +- Design uploads are limited to 10 files at a time. - [Designs cannot yet be deleted](https://gitlab.com/gitlab-org/gitlab-ee/issues/11089). -- Design Management is [not yet supported in the project export](https://gitlab.com/gitlab-org/gitlab-ee/issues/11090). +- Design Management is + [not yet supported in the project export](https://gitlab.com/gitlab-org/gitlab-ee/issues/11090). +- Design Management data + [isn't deleted when a project is destroyed](https://gitlab.com/gitlab-org/gitlab-ee/issues/13429) yet. +- Design Management data [won't be moved](https://gitlab.com/gitlab-org/gitlab-ee/issues/13426) + when an issue is moved, nor [deleted](https://gitlab.com/gitlab-org/gitlab-ee/issues/13427) + when an issue is deleted. +- Design Management + [isn't supported by Geo](https://gitlab.com/groups/gitlab-org/-/epics/1633) yet. ## The Design Management page diff --git a/doc/user/project/issues/issue_data_and_actions.md b/doc/user/project/issues/issue_data_and_actions.md index 7b031f83cb1..d7d168710ef 100644 --- a/doc/user/project/issues/issue_data_and_actions.md +++ b/doc/user/project/issues/issue_data_and_actions.md @@ -50,7 +50,12 @@ The button to do this has a different label depending on whether the issue is al #### 3. Assignee -An issue can be assigned to yourself, another person, or [many people](#31-multiple-assignees-STARTER). +An issue can be assigned to: + +- Yourself. +- Another person. +- [Many people](#31-multiple-assignees-STARTER). **(STARTER)** + The assignee(s) can be changed as often as needed. The idea is that the assignees are responsible for that issue until it's reassigned to someone else to take it from there. When assigned to someone, it will appear in their assigned issues list. diff --git a/doc/user/project/members/img/access_requests_management.png b/doc/user/project/members/img/access_requests_management.png Binary files differindex 8996d9564d7..9a1c9621e41 100644 --- a/doc/user/project/members/img/access_requests_management.png +++ b/doc/user/project/members/img/access_requests_management.png diff --git a/doc/user/project/members/img/request_access_button.png b/doc/user/project/members/img/request_access_button.png Binary files differindex e8b490b91b8..e693f9a9ac2 100644 --- a/doc/user/project/members/img/request_access_button.png +++ b/doc/user/project/members/img/request_access_button.png diff --git a/doc/user/project/members/img/withdraw_access_request_button.png b/doc/user/project/members/img/withdraw_access_request_button.png Binary files differindex 6a3172dfcdb..e5a8fe0b356 100644 --- a/doc/user/project/members/img/withdraw_access_request_button.png +++ b/doc/user/project/members/img/withdraw_access_request_button.png diff --git a/doc/user/project/merge_requests/fast_forward_merge.md b/doc/user/project/merge_requests/fast_forward_merge.md index 4f3c68090e4..2d59e535ce1 100644 --- a/doc/user/project/merge_requests/fast_forward_merge.md +++ b/doc/user/project/merge_requests/fast_forward_merge.md @@ -15,7 +15,7 @@ to accept merge requests without creating merge commits. When the fast-forward merge ([`--ff-only`](https://git-scm.com/docs/git-merge#git-merge---ff-only)) setting is enabled, no merge commits will be created and all merges are fast-forwarded, -which means that merging is only allowed if the branch could be fast-forwarded. +which means that merging is only allowed if the branch can be fast-forwarded. When a fast-forward merge is not possible, the user is given the option to rebase. @@ -28,9 +28,15 @@ When a fast-forward merge is not possible, the user is given the option to rebas Now, when you visit the merge request page, you will be able to accept it **only if a fast-forward merge is possible**. +![Fast forward merge request](img/ff_merge_mr.png) + +If a fast-forward merge is not possible but a conflict free rebase is possible, +a rebase button will be offered. + ![Fast forward merge request](img/ff_merge_rebase.png) -If the target branch is ahead of the source branch, you need to rebase the +If the target branch is ahead of the source branch and a conflict free rebase is +not possible, you need to rebase the source branch locally before you will be able to do a fast-forward merge. ![Fast forward merge rebase locally](img/ff_merge_rebase_locally.png) diff --git a/doc/user/project/merge_requests/img/approvals_premium_project_edit.png b/doc/user/project/merge_requests/img/approvals_premium_project_edit.png Binary files differdeleted file mode 100644 index 6a09412533f..00000000000 --- a/doc/user/project/merge_requests/img/approvals_premium_project_edit.png +++ /dev/null diff --git a/doc/user/project/merge_requests/img/approvals_premium_project_edit_v12_3.png b/doc/user/project/merge_requests/img/approvals_premium_project_edit_v12_3.png Binary files differnew file mode 100644 index 00000000000..32b9a3b9ce4 --- /dev/null +++ b/doc/user/project/merge_requests/img/approvals_premium_project_edit_v12_3.png diff --git a/doc/user/project/merge_requests/img/cross-project-dependencies-edit-inaccessible.png b/doc/user/project/merge_requests/img/cross_project_dependencies_edit_inaccessible_v12_2.png Binary files differindex 2dc02634fd8..2dc02634fd8 100644 --- a/doc/user/project/merge_requests/img/cross-project-dependencies-edit-inaccessible.png +++ b/doc/user/project/merge_requests/img/cross_project_dependencies_edit_inaccessible_v12_2.png diff --git a/doc/user/project/merge_requests/img/cross-project-dependencies-edit.png b/doc/user/project/merge_requests/img/cross_project_dependencies_edit_v12_2.png Binary files differindex 362e7e0ead2..362e7e0ead2 100644 --- a/doc/user/project/merge_requests/img/cross-project-dependencies-edit.png +++ b/doc/user/project/merge_requests/img/cross_project_dependencies_edit_v12_2.png diff --git a/doc/user/project/merge_requests/img/cross-project-dependencies-view.png b/doc/user/project/merge_requests/img/cross_project_dependencies_view_v12_2.png Binary files differindex e00231c839b..e00231c839b 100644 --- a/doc/user/project/merge_requests/img/cross-project-dependencies-view.png +++ b/doc/user/project/merge_requests/img/cross_project_dependencies_view_v12_2.png diff --git a/doc/user/project/merge_requests/img/ff_merge_mr.png b/doc/user/project/merge_requests/img/ff_merge_mr.png Binary files differnew file mode 100644 index 00000000000..241cc990343 --- /dev/null +++ b/doc/user/project/merge_requests/img/ff_merge_mr.png diff --git a/doc/user/project/merge_requests/img/incrementally_expand_merge_request_diffs_v12_2.png b/doc/user/project/merge_requests/img/incrementally_expand_merge_request_diffs_v12_2.png Binary files differnew file mode 100644 index 00000000000..ee94dbdea5c --- /dev/null +++ b/doc/user/project/merge_requests/img/incrementally_expand_merge_request_diffs_v12_2.png diff --git a/doc/user/project/merge_requests/index.md b/doc/user/project/merge_requests/index.md index 7ff30d1b813..04db54872d3 100644 --- a/doc/user/project/merge_requests/index.md +++ b/doc/user/project/merge_requests/index.md @@ -41,7 +41,7 @@ With **[GitLab Enterprise Edition][ee]**, you can also: - View the deployment process across projects with [Multi-Project Pipelines](../../../ci/multi_project_pipelines.md) **(PREMIUM)** - Request [approvals](merge_request_approvals.md) from your managers **(STARTER)** - Analyze the impact of your changes with [Code Quality reports](code_quality.md) **(STARTER)** -- Manage the licenses of your dependencies with [License Management](../../application_security/license_management/index.md) **(ULTIMATE)** +- Manage the licenses of your dependencies with [License Compliance](../../application_security/license_management/index.md) **(ULTIMATE)** - Analyze your source code for vulnerabilities with [Static Application Security Testing](../../application_security/sast/index.md) **(ULTIMATE)** - Analyze your running web applications for vulnerabilities with [Dynamic Application Security Testing](../../application_security/dast/index.md) **(ULTIMATE)** - Analyze your dependencies for vulnerabilities with [Dependency Scanning](../../application_security/dependency_scanning/index.md) **(ULTIMATE)** @@ -57,7 +57,7 @@ A. Consider you are a software developer working in a team: 1. You gather feedback from your team 1. You work on the implementation optimizing code with [Code Quality reports](code_quality.md) **(STARTER)** 1. You verify your changes with [JUnit test reports](../../../ci/junit_test_reports.md) in GitLab CI/CD -1. You avoid using dependencies whose license is not compatible with your project with [License Management reports](license_management.md) **(ULTIMATE)** +1. You avoid using dependencies whose license is not compatible with your project with [License Compliance reports](license_management.md) **(ULTIMATE)** 1. You request the [approval](#merge-request-approvals-starter) from your manager 1. Your manager pushes a commit with their final review, [approves the merge request](merge_request_approvals.md), and set it to [merge when pipeline succeeds](#merge-when-pipeline-succeeds) (Merge Request Approvals are available in GitLab Starter) 1. Your changes get deployed to production with [manual actions](../../../ci/yaml/README.md#whenmanual) for GitLab CI/CD @@ -287,6 +287,8 @@ as pushing changes: - Set the target of the merge request to a particular branch. - Set the merge request to merge when its pipeline succeeds. - Set the merge request to remove the source branch when it's merged. +- Set the title of the merge request to a particular title. +- Set the description of the merge request to a particular description. ### Create a new merge request using git push options @@ -332,6 +334,8 @@ git push -o merge_request.create -o merge_request.merge_when_pipeline_succeeds ### Set removing the source branch using git push options +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/64320) in GitLab 12.2. + To set an existing merge request to remove the source branch when the merge request is merged, the `merge_request.remove_source_branch` push option can be used: @@ -345,6 +349,8 @@ You can also use this push option in addition to the ### Set merge request title using git push options +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/64320) in GitLab 12.2. + To set the title of an existing merge request, use the `merge_request.title` push option: @@ -357,6 +363,8 @@ You can also use this push option in addition to the ### Set merge request description using git push options +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/64320) in GitLab 12.2. + To set the description of an existing merge request, use the `merge_request.description` push option: @@ -480,15 +488,6 @@ without having to check the entire job log. [Read more about JUnit test reports](../../../ci/junit_test_reports.md). -## Live preview with Review Apps - -If you configured [Review Apps](https://about.gitlab.com/features/review-apps/) for your project, -you can preview the changes submitted to a feature-branch through a merge request -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) - ## Merge request diff file navigation When reviewing changes in the **Changes** tab the diff can be navigated using @@ -498,6 +497,15 @@ list. ![Merge request diff file navigation](img/merge_request_diff_file_navigation.png) +### Incrementally expand merge request diffs + +By default, the diff shows only the parts of a file which are changed. +To view more unchanged lines above or below a change click on the +**Expand up** or **Expand down** icons. You can also click on **Show all lines** +to expand the entire file. + +![Incrementally expand merge request diffs](img/incrementally_expand_merge_request_diffs_v12_2.png) + ## Ignore whitespace changes in Merge Request diff view If you click the **Hide whitespace changes** button, you can see the diff diff --git a/doc/user/project/merge_requests/merge_request_approvals.md b/doc/user/project/merge_requests/merge_request_approvals.md index 509576c8d6f..a0432414bcf 100644 --- a/doc/user/project/merge_requests/merge_request_approvals.md +++ b/doc/user/project/merge_requests/merge_request_approvals.md @@ -83,7 +83,7 @@ request approval rules: 1. Give the approval rule a name that describes the set of approvers selected. 1. Click **Add approvers** to submit the new rule. - ![Approvals premium project edit](img/approvals_premium_project_edit.png) + ![Approvals premium project edit](img/approvals_premium_project_edit_v12_3.png) ## Multiple approval rules **(PREMIUM)** @@ -229,12 +229,6 @@ The default approval settings can now be overridden when creating a in the **No. approvals required** box. 1. Click **Update approvers**. -There are however some restrictions: - -- The amount of required approvals, if changed, must be greater than the default - set at the project level. This ensures that you're not forced to adjust settings - when someone is unavailable for approval, yet the process is still enforced. - NOTE: **Note:** If you are contributing to a forked project, things are a little different. Read what happens when the @@ -242,14 +236,9 @@ Read what happens when the ## Overriding merge request approvals default settings **(PREMIUM)** -In GitLab Premium, when the approval rules are [set at the project level](#editing-approvals-premium), and -**Can override approvers and approvals required per merge request** is checked, there are a few more -restrictions (compared to [GitLab Starter](#overriding-the-merge-request-approvals-default-settings)): - -- Approval rules can be added to an MR with no restriction. -- For project sourced approval rules, editing and removing approvers is not allowed. -- The approvals required of all approval rules is configurable, but if a rule is backed by a project rule, then it is restricted - to the minimum approvals required set in the project's corresponding rule. +In GitLab Premium, when the approval rules are [set at the project level](#editing-approvals-premium), +and **Can override approvers and approvals required per merge request** is checked, +approval rules can be added to an MR with no restriction. ## Resetting approvals on push diff --git a/doc/user/project/merge_requests/merge_request_dependencies.md b/doc/user/project/merge_requests/merge_request_dependencies.md index e046b3466c4..b30e24b2386 100644 --- a/doc/user/project/merge_requests/merge_request_dependencies.md +++ b/doc/user/project/merge_requests/merge_request_dependencies.md @@ -2,9 +2,9 @@ type: reference, concepts --- -# Cross-project merge request dependencies **(PREMIUM)** +# Cross-project Merge Request dependencies **(PREMIUM)** -> Introduced in GitLab Premium 12.2 +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/9688) in [GitLab Premium](https://about.gitlab.com/pricing/) 12.2. Cross-project merge request dependencies allows a required order of merging between merge requests in different projects to be expressed. If a @@ -24,11 +24,11 @@ merge requests in the same project cannot depend on each other. ## Use cases - Ensure changes to a library are merged before changes to a project that - imports the library + imports the library. - Prevent a documentation-only merge request from being merged before the merge request - implementing the feature to be documented + implementing the feature to be documented. - Require an merge request updating a permissions matrix to be merged before merging an - merge request from someone who hasn't yet been granted permissions + merge request from someone who hasn't yet been granted permissions. It is common for a single logical change to span several merge requests, spread out across multiple projects, and the order in which they are merged can be @@ -60,33 +60,33 @@ new merge request in `awesome-project` (or by editing it, if it already exists). The dependency needs to be configured on the **dependent** merge request. There is a "Cross-project dependencies" section in the form: -![Cross-project dependencies form control](img/cross-project-dependencies-edit.png) +![Cross-project dependencies form control](img/cross_project_dependencies_edit_v12_2.png) Anyone who can edit a merge request can change the list of dependencies. New dependencies can be added by reference, or by URL. To remove a dependency, -press the "X" by its reference. +press the **X** by its reference. As dependencies are specified across projects, it's possible that someone else has added a dependency for a merge request in a project you don't have access to. These are shown as a simple count: -![Cross-project dependencies form control with inaccessible merge requests](img/cross-project-dependencies-edit-inaccessible.png) +![Cross-project dependencies form control with inaccessible merge requests](img/cross_project_dependencies_edit_inaccessible_v12_2.png) -If necessary, you can remove all the dependencies like this by pressing the "X", -just as you would for a single, visible dependency. +If necessary, you can remove all the dependencies like this by pressing the +**X**, just as you would for a single, visible dependency. -Once you're finished, press the "Save changes" button to submit the request, or -"Cancel" to return without making any changes. +Once you're finished, press the **Save changes** button to submit the request, +or **Cancel** to return without making any changes. The list of configured dependencies, and the status of each one, is shown in the merge request widget: -![Cross-project dependencies in merge request widget](img/cross-project-dependencies-view.png) +![Cross-project dependencies in merge request widget](img/cross_project_dependencies_view_v12_2.png) -Until all dependencies have, themselves, been merged, the "Merge" +Until all dependencies have, themselves, been merged, the **Merge** button will be disabled for the dependent merge request. In -particular, note that **closed** merge request still prevent their +particular, note that **closed merge requests** still prevent their dependents from being merged - it is impossible to automatically determine whether the dependency expressed by a closed merge request has been satisfied in some other way or not. diff --git a/doc/user/project/new_ci_build_permissions_model.md b/doc/user/project/new_ci_build_permissions_model.md index 03ae24242e3..8606d92f20c 100644 --- a/doc/user/project/new_ci_build_permissions_model.md +++ b/doc/user/project/new_ci_build_permissions_model.md @@ -28,13 +28,13 @@ The reasons to do it like that are: With the new behavior, any job that is triggered by the user, is also marked with their read permissions. When a user does a `git push` or changes files through the web UI, a new pipeline will be usually created. This pipeline will be marked -as created be the pusher (local push or via the UI) and any job created in this +as created by the pusher (local push or via the UI) and any job created in this pipeline will have the read permissions of the pusher but not write permissions. This allows us to make it really easy to evaluate the access for all projects that have [Git submodules][gitsub] or are using container images that the pusher -would have access too. **The permission is granted only for time that job is -running. The access is revoked after the job is finished.** +would have access too. **The permission is granted only for the time that the job +is running. The access is revoked after the job is finished.** ## Types of users @@ -74,7 +74,7 @@ We try to make sure that this token doesn't leak by: 1. Securing all API endpoints to not expose the job token. 1. Masking the job token from job logs. -1. Allowing to use the job token **only** when job is running. +1. Granting permissions to the job token **only** when the job is running. However, this brings a question about the Runners security. To make sure that this token doesn't leak, you should also make sure that you configure @@ -86,12 +86,6 @@ your Runners in the most possible secure way, by avoiding the following: By using an insecure GitLab Runner configuration, you allow the rogue developers to steal the tokens of other jobs. -## Pipeline triggers - -Since 9.0 [pipeline triggers][triggers] do support the new permission model. -The new triggers do impersonate their associated user including their access -to projects and their project permissions. - ## Before GitLab 8.12 In versions before GitLab 8.12, all CI jobs would use the CI Runner's token @@ -203,7 +197,7 @@ Container Registries for private projects. > **Notes:** > > - GitLab Runner versions prior to 1.8 don't incorporate the introduced changes -> for permissions. This makes the `image:` directive to not work with private +> for permissions. This makes the `image:` directive not work with private > projects automatically and it needs to be configured manually on Runner's host > with a predefined account (for example administrator's personal account with > access token created explicitly for this purpose). This issue is resolved with @@ -227,11 +221,22 @@ test: - docker run $CI_REGISTRY/group/other-project:latest ``` +### Pipeline triggers + +Since 9.0 [pipeline triggers][triggers] do support the new permission model. +The new triggers do impersonate their associated user including their access +to projects and their project permissions. + +### API + +GitLab API cannot be used via `CI_JOB_TOKEN` but there is a [proposal](https://gitlab.com/gitlab-org/gitlab-ce/issues/29566) +to support it. + [job permissions]: ../permissions.md#job-permissions [comment]: https://gitlab.com/gitlab-org/gitlab-ce/issues/22484#note_16648302 [gitsub]: ../../ci/git_submodules.md [https]: ../admin_area/settings/visibility_and_access_controls.md#enabled-git-access-protocols -[triggers]: ../../ci/triggers/README.md +[triggers]: ../../ci/triggers/README.md#ci-job-token [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-environment-variables diff --git a/doc/user/project/operations/feature_flags.md b/doc/user/project/operations/feature_flags.md index 19ccde6e16a..75b0623e6b0 100644 --- a/doc/user/project/operations/feature_flags.md +++ b/doc/user/project/operations/feature_flags.md @@ -112,6 +112,19 @@ If this strategy is selected, then the Unleash client **must** be given a user i **Percent rollout (logged in users)** is implemented using the Unleash [gradualRolloutUserId](https://unleash.github.io/docs/activation_strategy#gradualrolloutuserid) activation strategy. +## Target Users + +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/8240) in GitLab 12.2. + +A feature flag may be enabled for a list of target users. + +![Feature flag target users](img/target_users_v12_2.png) + +CAUTION: **Caution:** +The Unleash client **must** be given a user id for the feature to be enabled for target users. See the [Ruby example](#ruby-application-example) below. + +**Target users** is implemented using the Unleash [userWithId](https://unleash.github.io/docs/activation_strategy#userwithid) activation strategy. + ## Integrating with your application In order to use Feature Flags, you need to first @@ -207,7 +220,7 @@ func main() { Here's an example of how to integrate the feature flags in a Ruby application. -The Unleash client is given a user id for use with a **Percent rollout (logged in users)** rollout strategy. +The Unleash client is given a user id for use with a **Percent rollout (logged in users)** rollout strategy or a list of **Target Users**. ```ruby #!/usr/bin/env ruby diff --git a/doc/user/project/operations/img/target_users_v12_2.png b/doc/user/project/operations/img/target_users_v12_2.png Binary files differnew file mode 100644 index 00000000000..c88d2b7be97 --- /dev/null +++ b/doc/user/project/operations/img/target_users_v12_2.png diff --git a/doc/user/project/pipelines/schedules.md b/doc/user/project/pipelines/schedules.md index 4e25d8545e9..f3e9c950efd 100644 --- a/doc/user/project/pipelines/schedules.md +++ b/doc/user/project/pipelines/schedules.md @@ -6,7 +6,9 @@ type: reference, howto > - Introduced in GitLab 9.1 as [Trigger Schedule](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10533). > - [Renamed to Pipeline Schedule](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10853) in GitLab 9.2. -> - Cron notation is parsed by [Fugit](https://github.com/floraison/fugit). + +NOTE: **Note:** +Cron notation is parsed by [Fugit](https://github.com/floraison/fugit). Pipelines are normally run based on certain conditions being met. For example, when a branch is pushed to repository. diff --git a/doc/user/project/quick_actions.md b/doc/user/project/quick_actions.md index 6758adf2b43..647250bd02a 100644 --- a/doc/user/project/quick_actions.md +++ b/doc/user/project/quick_actions.md @@ -40,18 +40,20 @@ discussions, and descriptions: | `/label ~label1 ~label2` | Add label(s). Label names can also start without ~ but mixed syntax is not supported. | ✓ | ✓ | | `/unlabel ~label1 ~label2` | Remove all or specific label(s)| ✓ | ✓ | | `/relabel ~label1 ~label2` | Replace existing label(s) with those specified | ✓ | ✓ | -| `/copy_metadata <#issue | !merge_request>` | Copy labels and milestone from other issue or merge request in the project | ✓ | ✓ | +| `/copy_metadata <#issue>` | Copy labels and milestone from another issue in the project | ✓ | ✓ | +| `/copy_metadata <!merge_request>` | Copy labels and milestone from another merge request in the project | ✓ | ✓ | | `/estimate <1w 3d 2h 14m>` | Set time estimate | ✓ | ✓ | | `/remove_estimate` | Remove time estimate | ✓ | ✓ | -| `/spend <time(1h 30m | -1h 5m)> <date(YYYY-MM-DD)>` | Add or subtract spent time; optionally, specify the date that time was spent on | ✓ | ✓ | +| `/spend <time(1h 30m)> <date(YYYY-MM-DD)>` | Add spent time; optionally, specify the date that time was spent on | ✓ | ✓ | +| `/spend <time(-1h 5m)> <date(YYYY-MM-DD)>` | Subtract spent time; optionally, specify the date that time was spent on | ✓ | ✓ | | `/remove_time_spent` | Remove time spent | ✓ | ✓ | | `/lock` | Lock the thread | ✓ | ✓ | | `/unlock` | Unlock the thread | ✓ | ✓ | -| `/due <in 2 days | this Friday | December 31st>`| Set due date | ✓ | | +| `/due <date>` | Set due date. Examples of valid `<date>` include `in 2 days`, `this Friday` and `December 31st`. | ✓ | | | `/remove_due_date` | Remove due date | ✓ | | -| `/weight <0 | 1 | 2 | ...>` | Set weight **(STARTER)** | ✓ | | +| `/weight <value>` | Set weight. Valid options for `<value>` include `0`, `1`, `2`, etc. **(STARTER)** | ✓ | | | `/clear_weight` | Clears weight **(STARTER)** | ✓ | | -| `/epic <&epic | group&epic | Epic URL>` | Add to epic **(ULTIMATE)** | ✓ | | +| `/epic <epic>` | Add to epic `<epic>`. The `<epic>` value should be in the format of `&epic`, `group&epic` or `epic-URL`. **(ULTIMATE)** | ✓ | | | `/remove_epic` | Removes from epic **(ULTIMATE)** | ✓ | | | `/promote` | Promote issue to epic **(ULTIMATE)** | ✓ | | | `/confidential` | Make confidential | ✓ | | @@ -110,9 +112,9 @@ The following quick actions are applicable for epics threads and description: | `/label ~label1 ~label2` | Add label(s) | | `/unlabel ~label1 ~label2` | Remove all or specific label(s) | | `/relabel ~label1 ~label2` | Replace existing label(s) with those specified | -| `/child_epic <&epic | group&epic | Epic URL>` | Adds child epic to epic ([introduced in GitLab 12.0](https://gitlab.com/gitlab-org/gitlab-ee/issues/7330)) | -| `/remove_child_epic <&epic | group&epic | Epic URL>` | Removes child epic from epic ([introduced in GitLab 12.0](https://gitlab.com/gitlab-org/gitlab-ee/issues/7330)) | -| `/parent_epic <&epic | group&epic | Epic URL>` | Sets parent epic to epic ([introduced in GitLab 12.1](https://gitlab.com/gitlab-org/gitlab-ee/issues/10556)) | +| `/child_epic <epic>` | Adds child epic to `<epic>`. The `<epic>` value should be in the format of `&epic`, `group&epic` or `epic-URL`. ([Introduced in GitLab 12.0](https://gitlab.com/gitlab-org/gitlab-ee/issues/7330)) **(ULTIMATE)**| +| `/remove_child_epic <epic>` | Removes child epic from `<epic>`. The `<epic>` value should be in the format of `&epic`, `group&epic` or `epic-URL`. ([Introduced in GitLab 12.0](https://gitlab.com/gitlab-org/gitlab-ee/issues/7330)) **(ULTIMATE)** | +| `/parent_epic <epic>` | Sets parent epic to `<epic>`. The `<epic>` value should be in the format of `&epic`, `group&epic` or `epic-URL`. ([introduced in GitLab 12.1](https://gitlab.com/gitlab-org/gitlab-ee/issues/10556)) **(ULTIMATE)** | | `/remove_parent_epic` | Removes parent epic from epic ([introduced in GitLab 12.1](https://gitlab.com/gitlab-org/gitlab-ee/issues/10556)) | <!-- ## Troubleshooting diff --git a/doc/user/project/repository/img/repository_languages.png b/doc/user/project/repository/img/repository_languages.png Binary files differdeleted file mode 100644 index 5977ad7faae..00000000000 --- a/doc/user/project/repository/img/repository_languages.png +++ /dev/null diff --git a/doc/user/project/repository/img/repository_languages_v12_2.gif b/doc/user/project/repository/img/repository_languages_v12_2.gif Binary files differnew file mode 100644 index 00000000000..d0a0e57c919 --- /dev/null +++ b/doc/user/project/repository/img/repository_languages_v12_2.gif diff --git a/doc/user/project/repository/index.md b/doc/user/project/repository/index.md index 84d63d29929..bd966185c94 100644 --- a/doc/user/project/repository/index.md +++ b/doc/user/project/repository/index.md @@ -182,7 +182,7 @@ were used and display this on the projects pages. If this information is missing be added after updating the default branch on the project. This process can take up to 5 minutes. -![Repository Languages bar](img/repository_languages.png) +![Repository Languages bar](img/repository_languages_v12_2.gif) Not all files are detected, among others; documentation, vendored code, and most markup languages are excluded. This behaviour can be diff --git a/doc/user/project/settings/index.md b/doc/user/project/settings/index.md index 17ec9ecb5d1..4e3db95b6d6 100644 --- a/doc/user/project/settings/index.md +++ b/doc/user/project/settings/index.md @@ -32,6 +32,12 @@ links will be missing from the sidebar UI. You can still access them with direct links if you can access Merge Requests. This is deliberate, if you can see Issues or Merge Requests, both of which use Labels and Milestones, then you shouldn't be denied access to Labels and Milestones pages. +#### Disabling email notifications + +You can disable all email notifications related to the project by selecting the +**Disable email notifications** checkbox. Only the project owner is allowed to change +this setting. + ### Issue settings Add an [issue description template](../description_templates.md#description-templates) to your project, so that every new issue will start with a custom template. diff --git a/doc/user/project/wiki/index.md b/doc/user/project/wiki/index.md index e3c6cd6d6ff..d1bab47de25 100644 --- a/doc/user/project/wiki/index.md +++ b/doc/user/project/wiki/index.md @@ -28,11 +28,12 @@ NOTE: **Note:** Requires Developer [permissions](../../permissions.md). Create a new page by clicking the **New page** button that can be found -in all wiki pages. You will be asked to fill in the page name from which GitLab -will create the path to the page. You can specify a full path for the new file -and any missing directories will be created automatically. +in all wiki pages. -![New page modal](img/wiki_create_new_page_modal.png) +You will be asked to fill in a title for your new wiki page. Wiki titles +also determine the path to the wiki page. You can specify a full path +(using "`/`" for subdirectories) for the new title and any missing +directories will be created automatically. Once you enter the page name, it's time to fill in its content. GitLab wikis support Markdown, RDoc and AsciiDoc. For Markdown based pages, all the diff --git a/doc/workflow/git_annex.md b/doc/workflow/git_annex.md index 9543859f86f..cdc38a5b7eb 100644 --- a/doc/workflow/git_annex.md +++ b/doc/workflow/git_annex.md @@ -200,7 +200,7 @@ searching for your distribution. Although there is no general guide for `git-annex` errors, there are a few tips on how to go around the warnings. -### git-annex-shell: Not a git-annex or gcrypt repository. +### git-annex-shell: Not a git-annex or gcrypt repository This warning can appear on the initial `git annex sync --content` and is caused by differences in `git-annex-shell`. You can read more about it diff --git a/doc/workflow/lfs/lfs_administration.md b/doc/workflow/lfs/lfs_administration.md index 3160b0c4deb..7e880b3009d 100644 --- a/doc/workflow/lfs/lfs_administration.md +++ b/doc/workflow/lfs/lfs_administration.md @@ -46,8 +46,7 @@ In `config/gitlab.yml`: ## Storing LFS objects in remote object storage -> [Introduced][ee-2760] in [GitLab Premium][eep] 10.0. Brought to GitLab Core -in 10.7. +> [Introduced][ee-2760] in [GitLab Premium][eep] 10.0. Brought to GitLab Core in 10.7. It is possible to store LFS objects in remote object storage which allows you to offload local hard disk R/W operations, and free up disk space significantly. @@ -91,7 +90,7 @@ Here is a configuration example with S3. | `aws_access_key_id` | AWS credentials, or compatible | `ABC123DEF456` | | `aws_secret_access_key` | AWS credentials, or compatible | `ABC123DEF456ABC123DEF456ABC123DEF456` | | `aws_signature_version` | AWS signature version to use. 2 or 4 are valid options. Digital Ocean Spaces and other providers may need 2. | 4 | -| `enable_signature_v4_streaming` | Set to true to enable HTTP chunked transfers with AWS v4 signatures (https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-streaming.html). Oracle Cloud S3 needs this to be false | true +| `enable_signature_v4_streaming` | Set to true to enable HTTP chunked transfers with [AWS v4 signatures](https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-streaming.html). Oracle Cloud S3 needs this to be false | true | | `region` | AWS region | us-east-1 | | `host` | S3 compatible host for when not using AWS, e.g. `localhost` or `storage.example.com` | s3.amazonaws.com | | `endpoint` | Can be used when configuring an S3 compatible service such as [Minio](https://www.minio.io), by entering a URL such as `http://127.0.0.1:9000` | (optional) | @@ -107,7 +106,9 @@ Here is a configuration example with GCS. | `google_client_email` | The email address of the service account | `foo@gcp-project-12345.iam.gserviceaccount.com` | | `google_json_key_location` | The json key path | `/path/to/gcp-project-12345-abcde.json` | -_NOTE: The service account must have permission to access the bucket. [See more](https://cloud.google.com/storage/docs/authentication)_ +NOTE: **Note:** +The service account must have permission to access the bucket. +[See more](https://cloud.google.com/storage/docs/authentication) Here is a configuration example with Rackspace Cloud Files. @@ -119,7 +120,13 @@ Here is a configuration example with Rackspace Cloud Files. | `rackspace_region` | The Rackspace storage region to use, a three letter code from the [list of service access endpoints](https://developer.rackspace.com/docs/cloud-files/v1/general-api-info/service-access/) | `iad` | | `rackspace_temp_url_key` | The private key you have set in the Rackspace API for temporary URLs. Read more [here](https://developer.rackspace.com/docs/cloud-files/v1/use-cases/public-access-to-your-cloud-files-account/#tempurl) | `ABC123DEF456ABC123DEF456ABC123DE` | -_NOTES: Regardless of whether the container has public access enabled or disabled, Fog will use the TempURL method to grant access to LFS objects. If you see errors in logs referencing instantiating storage with a temp-url-key, ensure that you have set they key properly on the Rackspace API and in gitlab.rb. You can verify the value of the key Rackspace has set by sending a GET request with token header to the service access endpoint URL and comparing the output of the returned headers._ +NOTE: **Note:** +Regardless of whether the container has public access enabled or disabled, Fog will +use the TempURL method to grant access to LFS objects. If you see errors in logs referencing +instantiating storage with a temp-url-key, ensure that you have set they key properly +on the Rackspace API and in gitlab.rb. You can verify the value of the key Rackspace +has set by sending a GET request with token header to the service access endpoint URL +and comparing the output of the returned headers. ### Manual uploading to an object storage @@ -167,13 +174,13 @@ On Omnibus installations, the settings are prefixed by `lfs_object_store_`: 1. Save the file and [reconfigure GitLab]s for the changes to take effect. 1. Migrate any existing local LFS objects to the object storage: - ```bash - gitlab-rake gitlab:lfs:migrate - ``` + ```bash + gitlab-rake gitlab:lfs:migrate + ``` - This will migrate existing LFS objects to object storage. New LFS objects - will be forwarded to object storage unless - `gitlab_rails['lfs_object_store_background_upload']` is set to false. + This will migrate existing LFS objects to object storage. New LFS objects + will be forwarded to object storage unless + `gitlab_rails['lfs_object_store_background_upload']` is set to false. ### S3 for installations from source @@ -203,13 +210,13 @@ For source installations the settings are nested under `lfs:` and then 1. Save the file and [restart GitLab][] for the changes to take effect. 1. Migrate any existing local LFS objects to the object storage: - ```bash - sudo -u git -H bundle exec rake gitlab:lfs:migrate RAILS_ENV=production - ``` + ```bash + sudo -u git -H bundle exec rake gitlab:lfs:migrate RAILS_ENV=production + ``` - This will migrate existing LFS objects to object storage. New LFS objects - will be forwarded to object storage unless `background_upload` is set to - false. + This will migrate existing LFS objects to object storage. New LFS objects + will be forwarded to object storage unless `background_upload` is set to + false. ## Storage statistics diff --git a/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md b/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md index 264372a512d..f5593927cc2 100644 --- a/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md +++ b/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md @@ -35,9 +35,10 @@ Documentation for GitLab instance administrators is under [LFS administration do - Git LFS always assumes HTTPS so if you have GitLab server on HTTP you will have to add the URL to Git config manually (see [troubleshooting](#troubleshooting)) ->**Note**: With 8.12 GitLab added LFS support to SSH. The Git LFS communication - still goes over HTTP, but now the SSH client passes the correct credentials - to the Git LFS client, so no action is required by the user. +NOTE: **Note:** +With 8.12 GitLab added LFS support to SSH. The Git LFS communication +still goes over HTTP, but now the SSH client passes the correct credentials +to the Git LFS client, so no action is required by the user. ## Using Git LFS @@ -61,12 +62,13 @@ git commit -am "Added Debian iso" # commit the file meta data git push origin master # sync the git repo and large file to the GitLab server ``` -> **Note**: Make sure that `.gitattributes` is tracked by git. Otherwise Git -> LFS will not be working properly for people cloning the project. -> -> ```bash -> git add .gitattributes -> ``` +NOTE: **Note:** +**Make sure** that `.gitattributes` is tracked by Git. Otherwise Git +LFS will not be working properly for people cloning the project. + +```bash +git add .gitattributes +``` Cloning the repository works the same as before. Git automatically detects the LFS-tracked files and clones them via HTTP. If you performed the git clone @@ -216,9 +218,10 @@ git config --add lfs.url "http://gitlab.example.com/group/project.git/info/lfs" ### Credentials are always required when pushing an object ->**Note**: With 8.12 GitLab added LFS support to SSH. The Git LFS communication - still goes over HTTP, but now the SSH client passes the correct credentials - to the Git LFS client, so no action is required by the user. +NOTE: **Note:** +With 8.12 GitLab added LFS support to SSH. The Git LFS communication +still goes over HTTP, but now the SSH client passes the correct credentials +to the Git LFS client, so no action is required by the user. Given that Git LFS uses HTTP Basic Authentication to authenticate the user pushing the LFS object on every push for every object, user HTTPS credentials are required. diff --git a/doc/workflow/lfs/migrate_from_git_annex_to_git_lfs.md b/doc/workflow/lfs/migrate_from_git_annex_to_git_lfs.md index 75673d23799..5ceeb3253f1 100644 --- a/doc/workflow/lfs/migrate_from_git_annex_to_git_lfs.md +++ b/doc/workflow/lfs/migrate_from_git_annex_to_git_lfs.md @@ -76,7 +76,7 @@ Here you'll find a guide on Since Annex files are stored as objects with symlinks and cannot be directly modified, we need to first remove those symlinks. ->**Note:** +NOTE: **Note:** Make sure the you read about the [`direct` mode][annex-direct] as it contains useful information that may fit in your use case. Note that `annex direct` is deprecated in Git Annex version 6, so you may need to upgrade your repository diff --git a/doc/workflow/notifications.md b/doc/workflow/notifications.md index d82f7c6fdc7..5abae929355 100644 --- a/doc/workflow/notifications.md +++ b/doc/workflow/notifications.md @@ -51,6 +51,10 @@ Organization like this is suitable for users that belong to different groups but same need for being notified for every group they are member of. These settings can be configured on group page under the name of the group. It will be the dropdown with the bell icon. They can also be configured on the user profile notifications dropdown. +The group owner can disable email notifications for a group, which also includes +it's subgroups and projects. If this is the case, you will not receive any corresponding notifications, +and the notification button will be disabled with an explanatory tooltip. + ### Project Settings ![notification settings](img/notification_project_settings.png) @@ -60,6 +64,10 @@ other setting. This is suitable for users that have different needs for notifications per project basis. These settings can be configured on project page under the name of the project. It will be the dropdown with the bell icon. They can also be configured on the user profile notifications dropdown. +The project owner (or it's group owner) can disable email notifications for the project. +If this is the case, you will not receive any corresponding notifications, and the notification +button will be disabled with an explanatory tooltip. + ## Notification events Below is the table of events users can be notified of: diff --git a/doc/workflow/repository_mirroring.md b/doc/workflow/repository_mirroring.md index 0b8e7d851b0..753518d0424 100644 --- a/doc/workflow/repository_mirroring.md +++ b/doc/workflow/repository_mirroring.md @@ -37,8 +37,7 @@ The following are some possible use cases for repository mirroring: ## Pushing to a remote repository **(CORE)** -> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/249) in GitLab Enterprise -> Edition 8.7. [Moved to GitLab Core](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/18715) in 10.8. +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/249) in GitLab Enterprise Edition 8.7. [Moved to GitLab Core](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/18715) in 10.8. For an existing project, you can set up push mirroring as follows: @@ -67,8 +66,7 @@ section. ### Push only protected branches **(CORE)** -> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/3350) in -> [GitLab Starter](https://about.gitlab.com/pricing/) 10.3. [Moved to GitLab Core](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/18715) in 10.8. +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/3350) in [GitLab Starter](https://about.gitlab.com/pricing/) 10.3. [Moved to GitLab Core](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/18715) in 10.8. You can choose to only push your protected branches from GitLab to your remote repository. @@ -98,8 +96,7 @@ The repository will push soon. To force a push, click the appropriate button. ## Pulling from a remote repository **(STARTER)** -> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/51) in GitLab Enterprise Edition 8.2. -> [Added Git LFS support](https://gitlab.com/gitlab-org/gitlab-ee/issues/10871) in [GitLab Starter](https://about.gitlab.com/pricing/) 11.11. +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/51) in GitLab Enterprise Edition 8.2. [Added Git LFS support](https://gitlab.com/gitlab-org/gitlab-ee/issues/10871) in [GitLab Starter](https://about.gitlab.com/pricing/) 11.11. NOTE: **Note:** This feature [is available for free](https://gitlab.com/gitlab-org/gitlab-ee/issues/10361) to GitLab.com users until September 22nd, 2019. @@ -157,8 +154,7 @@ Repository mirrors are updated as Sidekiq becomes available to process them. If ### SSH authentication -> [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 +> [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: @@ -245,8 +241,7 @@ key to keep the mirror running. ### Overwrite diverged branches **(STARTER)** -> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/4559) in -> [GitLab Starter](https://about.gitlab.com/pricing/) 10.6. +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/4559) in [GitLab Starter](https://about.gitlab.com/pricing/) 10.6. You can choose to always update your local branches with remote versions, even if they have diverged from the remote. @@ -258,8 +253,7 @@ To use this option, check the **Overwrite diverged branches** box when creating ### Only mirror protected branches **(STARTER)** -> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/3326) in -> [GitLab Starter](https://about.gitlab.com/pricing/) 10.3. +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/3326) in [GitLab Starter](https://about.gitlab.com/pricing/) 10.3. You can choose to pull mirror only the protected branches from your remote repository to GitLab. Non-protected branches are not mirrored and can diverge. @@ -268,8 +262,7 @@ To use this option, check the **Only mirror protected branches** box when creati ### Hard failure **(STARTER)** -> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/3117) in -> [GitLab Starter](https://about.gitlab.com/pricing/) 10.2. +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/3117) in [GitLab Starter](https://about.gitlab.com/pricing/) 10.2. Once the mirroring process is unsuccessfully retried 14 times in a row, it will get marked as hard failed. This will become visible in either the: @@ -282,8 +275,7 @@ project mirroring again by [Forcing an update](#forcing-an-update-core). ### Trigger update using API **(STARTER)** -> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/3453) in -[GitLab Starter](https://about.gitlab.com/pricing/) 10.3. +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/3453) in [GitLab Starter](https://about.gitlab.com/pricing/) 10.3. Pull mirroring uses polling to detect new branches and commits added upstream, often minutes afterwards. If you notify GitLab by [API](../api/projects.md#start-the-pull-mirroring-process-for-a-project-starter), @@ -325,9 +317,10 @@ protected branches. ### Preventing conflicts using a `pre-receive` hook -> **Warning:** The solution proposed will negatively impact the performance of -> Git push operations because they will be proxied to the upstream Git -> repository. +CAUTION: **Warning:** +The solution proposed will negatively impact the performance of +Git push operations because they will be proxied to the upstream Git +repository. A server-side `pre-receive` hook can be used to prevent the race condition described above by only accepting the push after first pushing the commit to diff --git a/doc/workflow/todos.md b/doc/workflow/todos.md index 0432c406f31..211d242e2d3 100644 --- a/doc/workflow/todos.md +++ b/doc/workflow/todos.md @@ -35,8 +35,8 @@ A To Do displays on your To-Do List when: - You are `@mentioned` in a comment on a commit - A job in the CI pipeline running for your merge request failed, but this job is not allowed to fail -- An open merge request becomes unmergeable due to conflict, and you are either: - - The author +- An open merge request becomes unmergeable due to conflict, and you are either: + - The author - Have set it to automatically merge once the pipeline succeeds To-do triggers are not affected by [GitLab Notification Email settings](notifications.md). diff --git a/lib/api/api.rb b/lib/api/api.rb index e500a93b31e..219ed45eff6 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -18,7 +18,7 @@ module API formatter: Gitlab::GrapeLogging::Formatters::LogrageWithTimestamp.new, include: [ GrapeLogging::Loggers::FilterParameters.new(LOG_FILTERS), - GrapeLogging::Loggers::ClientEnv.new, + Gitlab::GrapeLogging::Loggers::ClientEnvLogger.new, Gitlab::GrapeLogging::Loggers::RouteLogger.new, Gitlab::GrapeLogging::Loggers::UserLogger.new, Gitlab::GrapeLogging::Loggers::QueueDurationLogger.new, diff --git a/lib/api/award_emoji.rb b/lib/api/award_emoji.rb index a1851ba3627..89b7e5c5e4b 100644 --- a/lib/api/award_emoji.rb +++ b/lib/api/award_emoji.rb @@ -69,12 +69,12 @@ module API post endpoint do not_found!('Award Emoji') unless can_read_awardable? && can_award_awardable? - award = awardable.create_award_emoji(params[:name], current_user) + service = AwardEmojis::AddService.new(awardable, params[:name], current_user).execute - if award.persisted? - present award, with: Entities::AwardEmoji + if service[:status] == :success + present service[:award], with: Entities::AwardEmoji else - not_found!("Award Emoji #{award.errors.messages}") + not_found!("Award Emoji #{service[:message]}") end end diff --git a/lib/api/commits.rb b/lib/api/commits.rb index e4f4e79cd46..a2f3e87ebd2 100644 --- a/lib/api/commits.rb +++ b/lib/api/commits.rb @@ -43,7 +43,7 @@ module API path = params[:path] before = params[:until] after = params[:since] - ref = params[:ref_name] || user_project.try(:default_branch) || 'master' unless params[:all] + ref = params[:ref_name].presence || user_project.try(:default_branch) || 'master' unless params[:all] offset = (params[:page] - 1) * params[:per_page] all = params[:all] with_stats = params[:with_stats] diff --git a/lib/api/discussions.rb b/lib/api/discussions.rb index cc62ce22a1b..6c1acc3963f 100644 --- a/lib/api/discussions.rb +++ b/lib/api/discussions.rb @@ -4,6 +4,7 @@ module API class Discussions < Grape::API include PaginationParams helpers ::API::Helpers::NotesHelpers + helpers ::RendersNotes before { authenticate! } @@ -23,21 +24,15 @@ module API requires :noteable_id, types: [Integer, String], desc: 'The ID of the noteable' use :pagination end - # rubocop: disable CodeReuse/ActiveRecord + get ":id/#{noteables_path}/:noteable_id/discussions" do noteable = find_noteable(parent_type, params[:id], noteable_type, params[:noteable_id]) - notes = noteable.notes - .inc_relations_for_view - .includes(:noteable) - .fresh - - notes = notes.reject { |n| n.cross_reference_not_visible_for?(current_user) } + notes = readable_discussion_notes(noteable) discussions = Kaminari.paginate_array(Discussion.build_collection(notes, noteable)) present paginate(discussions), with: Entities::Discussion end - # rubocop: enable CodeReuse/ActiveRecord desc "Get a single #{noteable_type.to_s.downcase} discussion" do success Entities::Discussion @@ -226,13 +221,24 @@ module API helpers do # rubocop: disable CodeReuse/ActiveRecord - def readable_discussion_notes(noteable, discussion_id) + def readable_discussion_notes(noteable, discussion_id = nil) notes = noteable.notes - .where(discussion_id: discussion_id) + notes = notes.where(discussion_id: discussion_id) if discussion_id + notes = notes .inc_relations_for_view .includes(:noteable) .fresh + # Without RendersActions#prepare_notes_for_rendering, + # Note#cross_reference_not_visible_for? will attempt to render + # Markdown references mentioned in the note to see whether they + # should be redacted. For notes that reference a commit, this + # would also incur a Gitaly call to verify the commit exists. + # + # With prepare_notes_for_rendering, we can avoid Gitaly calls + # because notes are redacted if they point to projects that + # cannot be accessed by the user. + notes = prepare_notes_for_rendering(notes) notes.reject { |n| n.cross_reference_not_visible_for?(current_user) } end # rubocop: enable CodeReuse/ActiveRecord diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 09253ab6b0e..5e66b4e76a5 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -645,7 +645,10 @@ module API end end - expose :subscribed do |issue, options| + # Calculating the value of subscribed field triggers Markdown + # processing. We can't do that for multiple issues / merge + # requests in a single API request. + expose :subscribed, if: -> (_, options) { options.fetch(:include_subscribed, true) } do |issue, options| issue.subscribed?(options[:current_user], options[:project] || issue.project) end end diff --git a/lib/api/helpers/issues_helpers.rb b/lib/api/helpers/issues_helpers.rb index 5b7199fddb0..a8480bb9339 100644 --- a/lib/api/helpers/issues_helpers.rb +++ b/lib/api/helpers/issues_helpers.rb @@ -27,6 +27,10 @@ module API ] end + def self.sort_options + %w[created_at updated_at priority due_date relative_position label_priority milestone_due popularity] + end + def issue_finder(args = {}) args = declared_params.merge(args) @@ -34,15 +38,14 @@ module API args[:milestone_title] ||= args.delete(:milestone) args[:label_name] ||= args.delete(:labels) args[:scope] = args[:scope].underscore if args[:scope] + args[:sort] = "#{args[:order_by]}_#{args[:sort]}" IssuesFinder.new(current_user, args) end def find_issues(args = {}) finder = issue_finder(args) - issues = finder.execute.with_api_entity_associations - - issues.reorder(order_options_with_tie_breaker) # rubocop: disable CodeReuse/ActiveRecord + finder.execute.with_api_entity_associations end def issues_statistics(args = {}) diff --git a/lib/api/helpers/label_helpers.rb b/lib/api/helpers/label_helpers.rb index 896b0aba52b..ec5b688dd1c 100644 --- a/lib/api/helpers/label_helpers.rb +++ b/lib/api/helpers/label_helpers.rb @@ -11,9 +11,9 @@ module API optional :description, type: String, desc: 'The description of label to be created' end - def find_label(parent, id, include_ancestor_groups: true) + def find_label(parent, id_or_title, include_ancestor_groups: true) labels = available_labels_for(parent, include_ancestor_groups: include_ancestor_groups) - label = labels.find_by_id(id) || labels.find_by_title(id) + label = labels.find_by_id(id_or_title) || labels.find_by_title(id_or_title) label || not_found!('Label') end @@ -35,12 +35,7 @@ module API priority = params.delete(:priority) label_params = declared_params(include_missing: false) - label = - if parent.is_a?(Project) - ::Labels::CreateService.new(label_params).execute(project: parent) - else - ::Labels::CreateService.new(label_params).execute(group: parent) - end + label = ::Labels::CreateService.new(label_params).execute(create_service_params(parent)) if label.persisted? if parent.is_a?(Project) @@ -56,10 +51,13 @@ module API def update_label(parent, entity) authorize! :admin_label, parent - label = find_label(parent, params[:name], include_ancestor_groups: false) + label = find_label(parent, params_id_or_title, include_ancestor_groups: false) update_priority = params.key?(:priority) priority = params.delete(:priority) + # params is used to update the label so we need to remove this field here + params.delete(:label_id) + label = ::Labels::UpdateService.new(declared_params(include_missing: false)).execute(label) render_validation_error!(label) unless label.valid? @@ -77,10 +75,24 @@ module API def delete_label(parent) authorize! :admin_label, parent - label = find_label(parent, params[:name], include_ancestor_groups: false) + label = find_label(parent, params_id_or_title, include_ancestor_groups: false) destroy_conditionally!(label) end + + def params_id_or_title + @params_id_or_title ||= params[:label_id] || params[:name] + end + + def create_service_params(parent) + if parent.is_a?(Project) + { project: parent } + elsif parent.is_a?(Group) + { group: parent } + else + raise TypeError, 'Parent type is not supported' + end + end end end end diff --git a/lib/api/helpers/notes_helpers.rb b/lib/api/helpers/notes_helpers.rb index 6bf9057fad7..b2bf6bf7417 100644 --- a/lib/api/helpers/notes_helpers.rb +++ b/lib/api/helpers/notes_helpers.rb @@ -3,6 +3,8 @@ module API module Helpers module NotesHelpers + include ::RendersNotes + def self.noteable_types # This is a method instead of a constant, allowing EE to more easily # extend it. diff --git a/lib/api/issues.rb b/lib/api/issues.rb index d687acf3423..e16eeef202c 100644 --- a/lib/api/issues.rb +++ b/lib/api/issues.rb @@ -44,7 +44,7 @@ module API optional :with_labels_details, type: Boolean, desc: 'Return more label data than just lable title', default: false optional :state, type: String, values: %w[opened closed all], default: 'all', desc: 'Return opened, closed, or all issues' - optional :order_by, type: String, values: %w[created_at updated_at], default: 'created_at', + optional :order_by, type: String, values: Helpers::IssuesHelpers.sort_options, default: 'created_at', desc: 'Return issues ordered by `created_at` or `updated_at` fields.' optional :sort, type: String, values: %w[asc desc], default: 'desc', desc: 'Return issues sorted in `asc` or `desc` order.' @@ -96,7 +96,8 @@ module API with: Entities::Issue, with_labels_details: declared_params[:with_labels_details], current_user: current_user, - issuable_metadata: issuable_meta_data(issues, 'Issue', current_user) + issuable_metadata: issuable_meta_data(issues, 'Issue', current_user), + include_subscribed: false } present issues, options @@ -122,7 +123,8 @@ module API with: Entities::Issue, with_labels_details: declared_params[:with_labels_details], current_user: current_user, - issuable_metadata: issuable_meta_data(issues, 'Issue', current_user) + issuable_metadata: issuable_meta_data(issues, 'Issue', current_user), + include_subscribed: false } present issues, options diff --git a/lib/api/labels.rb b/lib/api/labels.rb index c183198d3c6..83d645ca07a 100644 --- a/lib/api/labels.rb +++ b/lib/api/labels.rb @@ -38,11 +38,13 @@ module API success Entities::ProjectLabel end params do - requires :name, type: String, desc: 'The name of the label to be updated' + optional :label_id, type: Integer, desc: 'The id of the label to be updated' + optional :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' optional :priority, type: Integer, desc: 'The priority of the label', allow_blank: true + exactly_one_of :label_id, :name at_least_one_of :new_name, :color, :description, :priority end put ':id/labels' do @@ -53,7 +55,9 @@ module API success Entities::ProjectLabel end params do - requires :name, type: String, desc: 'The name of the label to be deleted' + optional :label_id, type: Integer, desc: 'The id of the label to be deleted' + optional :name, type: String, desc: 'The name of the label to be deleted' + exactly_one_of :label_id, :name end delete ':id/labels' do delete_label(user_project) diff --git a/lib/api/notes.rb b/lib/api/notes.rb index 9381f045144..84563d66ee8 100644 --- a/lib/api/notes.rb +++ b/lib/api/notes.rb @@ -36,12 +36,13 @@ module API # page can have less elements than :per_page even if # there's more than one page. raw_notes = noteable.notes.with_metadata.reorder(order_options_with_tie_breaker) - notes = - # paginate() only works with a relation. This could lead to a - # mismatch between the pagination headers info and the actual notes - # array returned, but this is really a edge-case. - paginate(raw_notes) - .reject { |n| n.cross_reference_not_visible_for?(current_user) } + + # paginate() only works with a relation. This could lead to a + # mismatch between the pagination headers info and the actual notes + # array returned, but this is really a edge-case. + notes = paginate(raw_notes) + notes = prepare_notes_for_rendering(notes) + notes = notes.reject { |n| n.cross_reference_not_visible_for?(current_user) } present notes, with: Entities::Note end # rubocop: enable CodeReuse/ActiveRecord diff --git a/lib/api/pipelines.rb b/lib/api/pipelines.rb index 667bf1ec801..9e888368e7b 100644 --- a/lib/api/pipelines.rb +++ b/lib/api/pipelines.rb @@ -4,7 +4,7 @@ module API class Pipelines < Grape::API include PaginationParams - before { authenticate! } + before { authenticate_non_get! } params do requires :id, type: String, desc: 'The project ID' @@ -32,6 +32,7 @@ module API end get ':id/pipelines' do authorize! :read_pipeline, user_project + authorize! :read_build, user_project pipelines = PipelinesFinder.new(user_project, current_user, params).execute present paginate(pipelines), with: Entities::PipelineBasic diff --git a/lib/api/projects.rb b/lib/api/projects.rb index 996205d4b7b..3073c14b341 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -489,11 +489,13 @@ module API end params do optional :search, type: String, desc: 'Return list of users matching the search criteria' + optional :skip_users, type: Array[Integer], desc: 'Filter out users with the specified IDs' use :pagination end get ':id/users' do users = DeclarativePolicy.subject_scope { user_project.team.users } users = users.search(params[:search]) if params[:search].present? + users = users.where_not_in(params[:skip_users]) if params[:skip_users].present? present paginate(users), with: Entities::UserBasic end diff --git a/lib/api/settings.rb b/lib/api/settings.rb index 196ef1fcdfa..c36ee5af63f 100644 --- a/lib/api/settings.rb +++ b/lib/api/settings.rb @@ -125,6 +125,12 @@ module API optional :instance_statistics_visibility_private, type: Boolean, desc: 'When set to `true` Instance statistics will only be available to admins' optional :local_markdown_version, type: Integer, desc: "Local markdown version, increase this value when any cached markdown should be invalidated" optional :allow_local_requests_from_hooks_and_services, type: Boolean, desc: 'Deprecated: Use :allow_local_requests_from_web_hooks_and_services instead. Allow requests to the local network from hooks and services.' # support legacy names, can be removed in v5 + optional :snowplow_enabled, type: Grape::API::Boolean, desc: 'Enable Snowplow tracking' + given snowplow_enabled: ->(val) { val } do + requires :snowplow_collector_hostname, type: String, desc: 'The Snowplow collector hostname' + optional :snowplow_cookie_domain, type: String, desc: 'The Snowplow cookie domain' + optional :snowplow_site_id, type: String, desc: 'The Snowplow site name / application ic' + end ApplicationSetting::SUPPORTED_KEY_TYPES.each do |type| optional :"#{type}_key_restriction", diff --git a/lib/api/todos.rb b/lib/api/todos.rb index 7260ecfb5ee..404675bfaec 100644 --- a/lib/api/todos.rb +++ b/lib/api/todos.rb @@ -13,6 +13,13 @@ module API 'issues' => ->(iid) { find_project_issue(iid) } }.freeze + helpers do + # EE::API::Todos would override this method + def find_todos + TodosFinder.new(current_user, params).execute + end + end + params do requires :id, type: String, desc: 'The ID of a project' end @@ -41,10 +48,6 @@ module API resource :todos do helpers do - def find_todos - TodosFinder.new(current_user, params).execute - end - def issuable_and_awardable?(type) obj_type = Object.const_get(type) @@ -107,3 +110,5 @@ module API end end end + +API::Todos.prepend_if_ee('EE::API::Todos') diff --git a/lib/banzai/filter/abstract_reference_filter.rb b/lib/banzai/filter/abstract_reference_filter.rb index 52af28ce8ec..a0439089879 100644 --- a/lib/banzai/filter/abstract_reference_filter.rb +++ b/lib/banzai/filter/abstract_reference_filter.rb @@ -7,6 +7,14 @@ module Banzai class AbstractReferenceFilter < ReferenceFilter include CrossProjectReference + # REFERENCE_PLACEHOLDER is used for re-escaping HTML text except found + # reference (which we replace with placeholder during re-scaping). The + # random number helps ensure it's pretty close to unique. Since it's a + # transitory value (it never gets saved) we can initialize once, and it + # doesn't matter if it changes on a restart. + REFERENCE_PLACEHOLDER = "_reference_#{SecureRandom.hex(16)}_" + REFERENCE_PLACEHOLDER_PATTERN = %r{#{REFERENCE_PLACEHOLDER}(\d+)}.freeze + def self.object_class # Implement in child class # Example: MergeRequest @@ -389,6 +397,14 @@ module Banzai def escape_html_entities(text) CGI.escapeHTML(text.to_s) end + + def escape_with_placeholders(text, placeholder_data) + escaped = escape_html_entities(text) + + escaped.gsub(REFERENCE_PLACEHOLDER_PATTERN) do |match| + placeholder_data[$1.to_i] + end + end end end end diff --git a/lib/banzai/filter/inline_metrics_filter.rb b/lib/banzai/filter/inline_metrics_filter.rb index 0120cc37d6f..c5a328c21b2 100644 --- a/lib/banzai/filter/inline_metrics_filter.rb +++ b/lib/banzai/filter/inline_metrics_filter.rb @@ -15,17 +15,6 @@ module Banzai ) end - # Endpoint FE should hit to collect the appropriate - # chart information - def metrics_dashboard_url(params) - Gitlab::Metrics::Dashboard::Url.build_dashboard_url( - params['namespace'], - params['project'], - params['environment'], - embedded: true - ) - end - # Search params for selecting metrics links. A few # simple checks is enough to boost performance without # the cost of doing a full regex match. @@ -38,6 +27,28 @@ module Banzai def link_pattern Gitlab::Metrics::Dashboard::Url.regex end + + private + + # Endpoint FE should hit to collect the appropriate + # chart information + def metrics_dashboard_url(params) + Gitlab::Metrics::Dashboard::Url.build_dashboard_url( + params['namespace'], + params['project'], + params['environment'], + embedded: true, + **query_params(params['url']) + ) + end + + # Parses query params out from full url string into hash. + # + # Ex) 'https://<root>/<project>/<environment>/metrics?title=Title&group=Group' + # --> { title: 'Title', group: 'Group' } + def query_params(url) + Gitlab::Metrics::Dashboard::Url.parse_query(url) + end end end end diff --git a/lib/banzai/filter/label_reference_filter.rb b/lib/banzai/filter/label_reference_filter.rb index 4892668fc22..a0789b7ca06 100644 --- a/lib/banzai/filter/label_reference_filter.rb +++ b/lib/banzai/filter/label_reference_filter.rb @@ -14,24 +14,24 @@ module Banzai find_labels(parent_object).find(id) end - def self.references_in(text, pattern = Label.reference_pattern) - unescape_html_entities(text).gsub(pattern) do |match| - yield match, $~[:label_id].to_i, $~[:label_name], $~[:project], $~[:namespace], $~ - end - end - def references_in(text, pattern = Label.reference_pattern) - unescape_html_entities(text).gsub(pattern) do |match| + labels = {} + unescaped_html = unescape_html_entities(text).gsub(pattern) do |match| namespace, project = $~[:namespace], $~[:project] project_path = full_project_path(namespace, project) label = find_label(project_path, $~[:label_id], $~[:label_name]) if label - yield match, label.id, project, namespace, $~ + labels[label.id] = yield match, label.id, project, namespace, $~ + "#{REFERENCE_PLACEHOLDER}#{label.id}" else - escape_html_entities(match) + match end end + + return text if labels.empty? + + escape_with_placeholders(unescaped_html, labels) end def find_label(parent_ref, label_id, label_name) diff --git a/lib/banzai/filter/milestone_reference_filter.rb b/lib/banzai/filter/milestone_reference_filter.rb index 08969753d75..4c47ee4dba1 100644 --- a/lib/banzai/filter/milestone_reference_filter.rb +++ b/lib/banzai/filter/milestone_reference_filter.rb @@ -51,15 +51,21 @@ module Banzai # default implementation. return super(text, pattern) if pattern != Milestone.reference_pattern - unescape_html_entities(text).gsub(pattern) do |match| + milestones = {} + unescaped_html = unescape_html_entities(text).gsub(pattern) do |match| milestone = find_milestone($~[:project], $~[:namespace], $~[:milestone_iid], $~[:milestone_name]) if milestone - yield match, milestone.id, $~[:project], $~[:namespace], $~ + milestones[milestone.id] = yield match, milestone.id, $~[:project], $~[:namespace], $~ + "#{REFERENCE_PLACEHOLDER}#{milestone.id}" else - escape_html_entities(match) + match end end + + return text if milestones.empty? + + escape_with_placeholders(unescaped_html, milestones) end def find_milestone(project_ref, namespace_ref, milestone_id, milestone_name) diff --git a/lib/banzai/filter/relative_link_filter.rb b/lib/banzai/filter/relative_link_filter.rb index 86f18679496..846a7d46aad 100644 --- a/lib/banzai/filter/relative_link_filter.rb +++ b/lib/banzai/filter/relative_link_filter.rb @@ -9,6 +9,7 @@ module Banzai # Context options: # :commit # :group + # :current_user # :project # :project_wiki # :ref @@ -18,6 +19,7 @@ module Banzai def call return doc if context[:system_note] + return doc unless visible_to_user? @uri_types = {} clear_memoization(:linkable_files) @@ -166,6 +168,16 @@ module Banzai Gitlab.config.gitlab.relative_url_root.presence || '/' end + def visible_to_user? + if project + Ability.allowed?(current_user, :download_code, project) + elsif group + Ability.allowed?(current_user, :read_group, group) + else # Objects detached from projects or groups, e.g. Personal Snippets. + true + end + end + def ref context[:ref] || project.default_branch end @@ -178,6 +190,10 @@ module Banzai context[:project] end + def current_user + context[:current_user] + end + def repository @repository ||= project&.repository end diff --git a/lib/container_registry/tag.rb b/lib/container_registry/tag.rb index ef41dc560c9..ebea84fa1ca 100644 --- a/lib/container_registry/tag.rb +++ b/lib/container_registry/tag.rb @@ -6,6 +6,9 @@ module ContainerRegistry attr_reader :repository, :name + # https://github.com/docker/distribution/commit/3150937b9f2b1b5b096b2634d0e7c44d4a0f89fb + TAG_NAME_REGEX = /^[\w][\w.-]{0,127}$/.freeze + delegate :registry, :client, to: :repository delegate :revision, :short_revision, to: :config_blob, allow_nil: true @@ -13,6 +16,10 @@ module ContainerRegistry @repository, @name = repository, name end + def valid_name? + !name.match(TAG_NAME_REGEX).nil? + end + def valid? manifest.present? end diff --git a/lib/expand_variables.rb b/lib/expand_variables.rb index c83cec9dc4a..45af30f46dc 100644 --- a/lib/expand_variables.rb +++ b/lib/expand_variables.rb @@ -3,6 +3,20 @@ module ExpandVariables class << self def expand(value, variables) + variables_hash = nil + + value.gsub(/\$([a-zA-Z_][a-zA-Z0-9_]*)|\${\g<1>}|%\g<1>%/) do + variables_hash ||= transform_variables(variables) + variables_hash[$1 || $2] + end + end + + private + + def transform_variables(variables) + # Lazily initialise variables + variables = variables.call if variables.is_a?(Proc) + # Convert hash array to variables if variables.is_a?(Array) variables = variables.reduce({}) do |hash, variable| @@ -11,9 +25,7 @@ module ExpandVariables end end - value.gsub(/\$([a-zA-Z_][a-zA-Z0-9_]*)|\${\g<1>}|%\g<1>%/) do - variables[$1 || $2] - end + variables end end end diff --git a/lib/feature/gitaly.rb b/lib/feature/gitaly.rb index edfd2fb17f3..656becbffd3 100644 --- a/lib/feature/gitaly.rb +++ b/lib/feature/gitaly.rb @@ -6,8 +6,11 @@ class Feature class Gitaly # Server feature flags should use '_' to separate words. SERVER_FEATURE_FLAGS = - [ - 'get_commit_signatures'.freeze + %w[ + get_commit_signatures + cache_invalidator + inforef_uploadpack_cache + get_all_lfs_pointers_go ].freeze DEFAULT_ON_FLAGS = Set.new([]).freeze diff --git a/lib/gitlab/action_rate_limiter.rb b/lib/gitlab/action_rate_limiter.rb index fdb06d00548..0e8707af631 100644 --- a/lib/gitlab/action_rate_limiter.rb +++ b/lib/gitlab/action_rate_limiter.rb @@ -49,9 +49,9 @@ module Gitlab request_information = { message: 'Action_Rate_Limiter_Request', env: type, - ip: request.ip, + remote_ip: request.ip, request_method: request.request_method, - fullpath: request.fullpath + path: request.fullpath } if current_user diff --git a/lib/gitlab/analytics/cycle_analytics/default_stages.rb b/lib/gitlab/analytics/cycle_analytics/default_stages.rb new file mode 100644 index 00000000000..286c393005f --- /dev/null +++ b/lib/gitlab/analytics/cycle_analytics/default_stages.rb @@ -0,0 +1,98 @@ +# frozen_string_literal: true + +# This module represents the default Cycle Analytics stages that are currently provided by CE +# Each method returns a hash that can be used to build a new stage object. +# +# Example: +# +# params = Gitlab::Analytics::CycleAnalytics::DefaultStages.params_for_issue_stage +# Analytics::CycleAnalytics::ProjectStage.new(params) +module Gitlab + module Analytics + module CycleAnalytics + module DefaultStages + def self.all + [ + params_for_issue_stage, + params_for_plan_stage, + params_for_code_stage, + params_for_test_stage, + params_for_review_stage, + params_for_staging_stage, + params_for_production_stage + ] + end + + def self.params_for_issue_stage + { + name: 'issue', + custom: false, # this stage won't be customizable, we provide it as it is + relative_position: 1, # when opening the CycleAnalytics page in CE, this stage will be the first item + start_event_identifier: :issue_created, # IssueCreated class is used as start event + end_event_identifier: :issue_stage_end # IssueStageEnd class is used as end event + } + end + + def self.params_for_plan_stage + { + name: 'plan', + custom: false, + relative_position: 2, + start_event_identifier: :plan_stage_start, + end_event_identifier: :issue_first_mentioned_in_commit + } + end + + def self.params_for_code_stage + { + name: 'code', + custom: false, + relative_position: 3, + start_event_identifier: :code_stage_start, + end_event_identifier: :merge_request_created + } + end + + def self.params_for_test_stage + { + name: 'test', + custom: false, + relative_position: 4, + start_event_identifier: :merge_request_last_build_started, + end_event_identifier: :merge_request_last_build_finished + } + end + + def self.params_for_review_stage + { + name: 'review', + custom: false, + relative_position: 5, + start_event_identifier: :merge_request_created, + end_event_identifier: :merge_request_merged + } + end + + def self.params_for_staging_stage + { + name: 'staging', + custom: false, + relative_position: 6, + start_event_identifier: :merge_request_merged, + end_event_identifier: :merge_request_first_deployed_to_production + } + end + + def self.params_for_production_stage + { + name: 'production', + custom: false, + relative_position: 7, + start_event_identifier: :merge_request_merged, + end_event_identifier: :merge_request_first_deployed_to_production + } + end + end + end + end +end diff --git a/lib/gitlab/analytics/cycle_analytics/stage_events.rb b/lib/gitlab/analytics/cycle_analytics/stage_events.rb new file mode 100644 index 00000000000..d21f344f483 --- /dev/null +++ b/lib/gitlab/analytics/cycle_analytics/stage_events.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: true + +module Gitlab + module Analytics + module CycleAnalytics + module StageEvents + # Convention: + # Issue: < 100 + # MergeRequest: >= 100 && < 1000 + # Custom events for default stages: >= 1000 (legacy) + ENUM_MAPPING = { + StageEvents::IssueCreated => 1, + StageEvents::IssueFirstMentionedInCommit => 2, + StageEvents::MergeRequestCreated => 100, + StageEvents::MergeRequestFirstDeployedToProduction => 101, + StageEvents::MergeRequestLastBuildFinished => 102, + StageEvents::MergeRequestLastBuildStarted => 103, + StageEvents::MergeRequestMerged => 104, + StageEvents::CodeStageStart => 1_000, + StageEvents::IssueStageEnd => 1_001, + StageEvents::PlanStageStart => 1_002 + }.freeze + + EVENTS = ENUM_MAPPING.keys.freeze + + # Defines which start_event and end_event pairs are allowed + PAIRING_RULES = { + StageEvents::PlanStageStart => [ + StageEvents::IssueFirstMentionedInCommit + ], + StageEvents::CodeStageStart => [ + StageEvents::MergeRequestCreated + ], + StageEvents::IssueCreated => [ + StageEvents::IssueStageEnd + ], + StageEvents::MergeRequestCreated => [ + StageEvents::MergeRequestMerged + ], + StageEvents::MergeRequestLastBuildStarted => [ + StageEvents::MergeRequestLastBuildFinished + ], + StageEvents::MergeRequestMerged => [ + StageEvents::MergeRequestFirstDeployedToProduction + ] + }.freeze + + def [](identifier) + events.find { |e| e.identifier.to_s.eql?(identifier.to_s) } || raise(KeyError) + end + + # hash for defining ActiveRecord enum: identifier => number + def to_enum + ENUM_MAPPING.each_with_object({}) { |(k, v), hash| hash[k.identifier] = v } + end + + # will be overridden in EE with custom events + def pairing_rules + PAIRING_RULES + end + + # will be overridden in EE with custom events + def events + EVENTS + end + + module_function :[], :to_enum, :pairing_rules, :events + end + end + end +end diff --git a/lib/gitlab/analytics/cycle_analytics/stage_events/code_stage_start.rb b/lib/gitlab/analytics/cycle_analytics/stage_events/code_stage_start.rb new file mode 100644 index 00000000000..ff9c8a79225 --- /dev/null +++ b/lib/gitlab/analytics/cycle_analytics/stage_events/code_stage_start.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Gitlab + module Analytics + module CycleAnalytics + module StageEvents + class CodeStageStart < SimpleStageEvent + def self.name + s_("CycleAnalyticsEvent|Issue first mentioned in a commit") + end + + def self.identifier + :code_stage_start + end + + def object_type + MergeRequest + end + end + end + end + end +end diff --git a/lib/gitlab/analytics/cycle_analytics/stage_events/issue_created.rb b/lib/gitlab/analytics/cycle_analytics/stage_events/issue_created.rb new file mode 100644 index 00000000000..a601c9797f8 --- /dev/null +++ b/lib/gitlab/analytics/cycle_analytics/stage_events/issue_created.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Gitlab + module Analytics + module CycleAnalytics + module StageEvents + class IssueCreated < SimpleStageEvent + def self.name + s_("CycleAnalyticsEvent|Issue created") + end + + def self.identifier + :issue_created + end + + def object_type + Issue + end + end + end + end + end +end diff --git a/lib/gitlab/analytics/cycle_analytics/stage_events/issue_first_mentioned_in_commit.rb b/lib/gitlab/analytics/cycle_analytics/stage_events/issue_first_mentioned_in_commit.rb new file mode 100644 index 00000000000..7424043ef7b --- /dev/null +++ b/lib/gitlab/analytics/cycle_analytics/stage_events/issue_first_mentioned_in_commit.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Gitlab + module Analytics + module CycleAnalytics + module StageEvents + class IssueFirstMentionedInCommit < SimpleStageEvent + def self.name + s_("CycleAnalyticsEvent|Issue first mentioned in a commit") + end + + def self.identifier + :issue_first_mentioned_in_commit + end + + def object_type + Issue + end + end + end + end + end +end diff --git a/lib/gitlab/analytics/cycle_analytics/stage_events/issue_stage_end.rb b/lib/gitlab/analytics/cycle_analytics/stage_events/issue_stage_end.rb new file mode 100644 index 00000000000..ceb229c552f --- /dev/null +++ b/lib/gitlab/analytics/cycle_analytics/stage_events/issue_stage_end.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Gitlab + module Analytics + module CycleAnalytics + module StageEvents + class IssueStageEnd < SimpleStageEvent + def self.name + PlanStageStart.name + end + + def self.identifier + :issue_stage_end + end + + def object_type + Issue + end + end + end + end + end +end diff --git a/lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_created.rb b/lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_created.rb new file mode 100644 index 00000000000..8be00831b4f --- /dev/null +++ b/lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_created.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Gitlab + module Analytics + module CycleAnalytics + module StageEvents + class MergeRequestCreated < SimpleStageEvent + def self.name + s_("CycleAnalyticsEvent|Merge request created") + end + + def self.identifier + :merge_request_created + end + + def object_type + MergeRequest + end + end + end + end + end +end diff --git a/lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_first_deployed_to_production.rb b/lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_first_deployed_to_production.rb new file mode 100644 index 00000000000..6d7a2c023ff --- /dev/null +++ b/lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_first_deployed_to_production.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Gitlab + module Analytics + module CycleAnalytics + module StageEvents + class MergeRequestFirstDeployedToProduction < SimpleStageEvent + def self.name + s_("CycleAnalyticsEvent|Merge request first deployed to production") + end + + def self.identifier + :merge_request_first_deployed_to_production + end + + def object_type + MergeRequest + end + end + end + end + end +end diff --git a/lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_last_build_finished.rb b/lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_last_build_finished.rb new file mode 100644 index 00000000000..12d82fe2c62 --- /dev/null +++ b/lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_last_build_finished.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Gitlab + module Analytics + module CycleAnalytics + module StageEvents + class MergeRequestLastBuildFinished < SimpleStageEvent + def self.name + s_("CycleAnalyticsEvent|Merge request last build finish time") + end + + def self.identifier + :merge_request_last_build_finished + end + + def object_type + MergeRequest + end + end + end + end + end +end diff --git a/lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_last_build_started.rb b/lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_last_build_started.rb new file mode 100644 index 00000000000..9e749b0fdfa --- /dev/null +++ b/lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_last_build_started.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Gitlab + module Analytics + module CycleAnalytics + module StageEvents + class MergeRequestLastBuildStarted < SimpleStageEvent + def self.name + s_("CycleAnalyticsEvent|Merge request last build start time") + end + + def self.identifier + :merge_request_last_build_started + end + + def object_type + MergeRequest + end + end + end + end + end +end diff --git a/lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_merged.rb b/lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_merged.rb new file mode 100644 index 00000000000..bbfb5d12992 --- /dev/null +++ b/lib/gitlab/analytics/cycle_analytics/stage_events/merge_request_merged.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Gitlab + module Analytics + module CycleAnalytics + module StageEvents + class MergeRequestMerged < SimpleStageEvent + def self.name + s_("CycleAnalyticsEvent|Merge request merged") + end + + def self.identifier + :merge_request_merged + end + + def object_type + MergeRequest + end + end + end + end + end +end diff --git a/lib/gitlab/analytics/cycle_analytics/stage_events/plan_stage_start.rb b/lib/gitlab/analytics/cycle_analytics/stage_events/plan_stage_start.rb new file mode 100644 index 00000000000..803317d8b55 --- /dev/null +++ b/lib/gitlab/analytics/cycle_analytics/stage_events/plan_stage_start.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Gitlab + module Analytics + module CycleAnalytics + module StageEvents + class PlanStageStart < SimpleStageEvent + def self.name + s_("CycleAnalyticsEvent|Issue first associated with a milestone or issue first added to a board") + end + + def self.identifier + :plan_stage_start + end + + def object_type + Issue + end + end + end + end + end +end diff --git a/lib/gitlab/analytics/cycle_analytics/stage_events/simple_stage_event.rb b/lib/gitlab/analytics/cycle_analytics/stage_events/simple_stage_event.rb new file mode 100644 index 00000000000..253c489d822 --- /dev/null +++ b/lib/gitlab/analytics/cycle_analytics/stage_events/simple_stage_event.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module Gitlab + module Analytics + module CycleAnalytics + module StageEvents + # Represents a simple event that usually refers to one database column and does not require additional user input + class SimpleStageEvent < StageEvent + end + end + end + end +end diff --git a/lib/gitlab/analytics/cycle_analytics/stage_events/stage_event.rb b/lib/gitlab/analytics/cycle_analytics/stage_events/stage_event.rb new file mode 100644 index 00000000000..a55eee048c2 --- /dev/null +++ b/lib/gitlab/analytics/cycle_analytics/stage_events/stage_event.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +module Gitlab + module Analytics + module CycleAnalytics + module StageEvents + # Base class for expressing an event that can be used for a stage. + class StageEvent + def initialize(params) + @params = params + end + + def self.name + raise NotImplementedError + end + + def self.identifier + raise NotImplementedError + end + + def object_type + raise NotImplementedError + end + end + end + end + end +end diff --git a/lib/gitlab/auth.rb b/lib/gitlab/auth.rb index 82e0c7ceeaa..e17a096ef19 100644 --- a/lib/gitlab/auth.rb +++ b/lib/gitlab/auth.rb @@ -46,7 +46,7 @@ module Gitlab user_with_password_for_git(login, password) || Gitlab::Auth::Result.new - rate_limit!(ip, success: result.success?, login: login) + rate_limit!(ip, success: result.success?, login: login) unless skip_rate_limit?(login: login) Gitlab::Auth::UniqueIpsLimiter.limit_user!(result.actor) return result if result.success? || authenticate_using_internal_or_ldap_password? @@ -119,6 +119,10 @@ module Gitlab private + def skip_rate_limit?(login:) + ::Ci::Build::CI_REGISTRY_USER == login + end + def authenticate_using_internal_or_ldap_password? Gitlab::CurrentSettings.password_authentication_enabled_for_git? || Gitlab::Auth::LDAP::Config.enabled? end diff --git a/lib/gitlab/background_migration/legacy_upload_mover.rb b/lib/gitlab/background_migration/legacy_upload_mover.rb new file mode 100644 index 00000000000..051c1176edb --- /dev/null +++ b/lib/gitlab/background_migration/legacy_upload_mover.rb @@ -0,0 +1,140 @@ +# frozen_string_literal: true + +module Gitlab + module BackgroundMigration + # This class takes a legacy upload and migrates it to the correct location + class LegacyUploadMover + include Gitlab::Utils::StrongMemoize + + attr_reader :upload, :project, :note + attr_accessor :logger + + def initialize(upload) + @upload = upload + @note = Note.find_by(id: upload.model_id) + @project = note&.project + @logger = Gitlab::BackgroundMigration::Logger.build + end + + def execute + return unless upload + + if !project + # if we don't have models associated with the upload we can not move it + warn('Deleting upload due to model not found.') + + destroy_legacy_upload + elsif note.is_a?(LegacyDiffNote) + return unless move_legacy_diff_file + + migrate_upload + elsif !legacy_file_exists? + warn('Deleting upload due to file not found.') + destroy_legacy_upload + else + migrate_upload + end + end + + private + + def migrate_upload + return unless copy_upload_to_project + + add_upload_link_to_note_text + destroy_legacy_file + destroy_legacy_upload + end + + # we should proceed and log whenever one upload copy fails, no matter the reasons + # rubocop: disable Lint/RescueException + def copy_upload_to_project + @uploader = FileUploader.copy_to(legacy_file_uploader, project) + + logger.info( + message: 'MigrateLegacyUploads: File copied successfully', + old_path: legacy_file_uploader.file.path, new_path: @uploader.file.path + ) + true + rescue Exception => e + warn( + 'File could not be copied to project uploads', + file_path: legacy_file_uploader.file.path, error: e.message + ) + false + end + # rubocop: enable Lint/RescueException + + def destroy_legacy_upload + if note + note.remove_attachment = true + note.save + end + + if upload.destroy + logger.info(message: 'MigrateLegacyUploads: Upload was destroyed.', upload: upload.inspect) + else + warn('MigrateLegacyUploads: Upload destroy failed.') + end + end + + def destroy_legacy_file + legacy_file_uploader.file.delete + end + + def add_upload_link_to_note_text + new_text = "#{note.note} \n #{@uploader.markdown_link}" + # Bypass validations because old data may have invalid + # noteable values. If we fail hard here, we may kill the + # entire background migration, which affects a range of notes. + note.update_attribute(:note, new_text) + end + + def legacy_file_uploader + strong_memoize(:legacy_file_uploader) do + uploader = upload.build_uploader + uploader.retrieve_from_store!(File.basename(upload.path)) + uploader + end + end + + def legacy_file_exists? + legacy_file_uploader.file.exists? + end + + # we should proceed and log whenever one upload copy fails, no matter the reasons + # rubocop: disable Lint/RescueException + def move_legacy_diff_file + old_path = upload.absolute_path + old_path_sub = '-/system/note/attachment' + + if !File.exist?(old_path) || !old_path.include?(old_path_sub) + log_legacy_diff_note_problem(old_path) + return false + end + + new_path = upload.absolute_path.sub(old_path_sub, '-/system/legacy_diff_note/attachment') + new_dir = File.dirname(new_path) + FileUtils.mkdir_p(new_dir) + + FileUtils.mv(old_path, new_path) + rescue Exception => e + log_legacy_diff_note_problem(old_path, new_path, e) + false + end + + def warn(message, params = {}) + logger.warn( + params.merge(message: "MigrateLegacyUploads: #{message}", upload: upload.inspect) + ) + end + + def log_legacy_diff_note_problem(old_path, new_path = nil, error = nil) + warn('LegacyDiffNote upload could not be moved to a new path', + old_path: old_path, new_path: new_path, error: error&.message + ) + end + # rubocop: enable Lint/RescueException + end + end +end diff --git a/lib/gitlab/background_migration/legacy_uploads_migrator.rb b/lib/gitlab/background_migration/legacy_uploads_migrator.rb new file mode 100644 index 00000000000..a9d38a27e0c --- /dev/null +++ b/lib/gitlab/background_migration/legacy_uploads_migrator.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Gitlab + module BackgroundMigration + # This migration takes all legacy uploads (that were uploaded using AttachmentUploader) + # and migrate them to the new (FileUploader) location (=under projects). + # + # We have dependencies (uploaders) in this migration because extracting code would add a lot of complexity + # and possible errors could appear as the logic in the uploaders is not trivial. + # + # This migration will be removed in 13.0 in order to get rid of a migration that depends on + # the application code. + class LegacyUploadsMigrator + include Database::MigrationHelpers + + def perform(start_id, end_id) + Upload.where(id: start_id..end_id, uploader: 'AttachmentUploader').find_each do |upload| + LegacyUploadMover.new(upload).execute + end + end + end + end +end diff --git a/lib/gitlab/background_migration/logger.rb b/lib/gitlab/background_migration/logger.rb new file mode 100644 index 00000000000..4ea89771eff --- /dev/null +++ b/lib/gitlab/background_migration/logger.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +module Gitlab + module BackgroundMigration + # Logger that can be used for migrations logging + class Logger < ::Gitlab::JsonLogger + def self.file_name_noext + 'migrations' + end + end + end +end diff --git a/lib/gitlab/ci/build/policy/variables.rb b/lib/gitlab/ci/build/policy/variables.rb index 0698136166a..e9c8864123f 100644 --- a/lib/gitlab/ci/build/policy/variables.rb +++ b/lib/gitlab/ci/build/policy/variables.rb @@ -10,7 +10,7 @@ module Gitlab end def satisfied_by?(pipeline, seed) - variables = seed.to_resource.scoped_variables_hash + variables = seed.scoped_variables_hash statements = @expressions.map do |statement| ::Gitlab::Ci::Pipeline::Expression::Statement diff --git a/lib/gitlab/ci/build/rules.rb b/lib/gitlab/ci/build/rules.rb new file mode 100644 index 00000000000..89623a809c9 --- /dev/null +++ b/lib/gitlab/ci/build/rules.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + module Build + class Rules + include ::Gitlab::Utils::StrongMemoize + + Result = Struct.new(:when, :start_in) + + def initialize(rule_hashes, default_when = 'on_success') + @rule_list = Rule.fabricate_list(rule_hashes) + @default_when = default_when + end + + def evaluate(pipeline, build) + if @rule_list.nil? + Result.new(@default_when) + elsif matched_rule = match_rule(pipeline, build) + Result.new( + matched_rule.attributes[:when] || @default_when, + matched_rule.attributes[:start_in] + ) + else + Result.new('never') + end + end + + private + + def match_rule(pipeline, build) + @rule_list.find { |rule| rule.matches?(pipeline, build) } + end + end + end + end +end diff --git a/lib/gitlab/ci/build/rules/rule.rb b/lib/gitlab/ci/build/rules/rule.rb new file mode 100644 index 00000000000..8d52158c8d2 --- /dev/null +++ b/lib/gitlab/ci/build/rules/rule.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + module Build + class Rules::Rule + attr_accessor :attributes + + def self.fabricate_list(list) + list.map(&method(:new)) if list + end + + def initialize(spec) + @clauses = [] + @attributes = {} + + spec.each do |type, value| + if clause = Clause.fabricate(type, value) + @clauses << clause + else + @attributes.merge!(type => value) + end + end + end + + def matches?(pipeline, build) + @clauses.all? { |clause| clause.satisfied_by?(pipeline, build) } + end + end + end + end +end diff --git a/lib/gitlab/ci/build/rules/rule/clause.rb b/lib/gitlab/ci/build/rules/rule/clause.rb new file mode 100644 index 00000000000..ff0baf3348c --- /dev/null +++ b/lib/gitlab/ci/build/rules/rule/clause.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + module Build + class Rules::Rule::Clause + ## + # Abstract class that defines an interface of a single + # job rule specification. + # + # Used for job's inclusion rules configuration. + # + UnknownClauseError = Class.new(StandardError) + + def self.fabricate(type, value) + type = type.to_s.camelize + + self.const_get(type).new(value) if self.const_defined?(type) + end + + def initialize(spec) + @spec = spec + end + + def satisfied_by?(pipeline, seed = nil) + raise NotImplementedError + end + end + end + end +end diff --git a/lib/gitlab/ci/build/rules/rule/clause/changes.rb b/lib/gitlab/ci/build/rules/rule/clause/changes.rb new file mode 100644 index 00000000000..81d2ee6c24c --- /dev/null +++ b/lib/gitlab/ci/build/rules/rule/clause/changes.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + module Build + class Rules::Rule::Clause::Changes < Rules::Rule::Clause + def initialize(globs) + @globs = Array(globs) + end + + def satisfied_by?(pipeline, seed) + return true if pipeline.modified_paths.nil? + + pipeline.modified_paths.any? do |path| + @globs.any? do |glob| + File.fnmatch?(glob, path, File::FNM_PATHNAME | File::FNM_DOTMATCH | File::FNM_EXTGLOB) + end + end + end + end + end + end +end diff --git a/lib/gitlab/ci/build/rules/rule/clause/if.rb b/lib/gitlab/ci/build/rules/rule/clause/if.rb new file mode 100644 index 00000000000..18c3b450f95 --- /dev/null +++ b/lib/gitlab/ci/build/rules/rule/clause/if.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + module Build + class Rules::Rule::Clause::If < Rules::Rule::Clause + def initialize(expression) + @expression = expression + end + + def satisfied_by?(pipeline, seed) + variables = seed.scoped_variables_hash + + ::Gitlab::Ci::Pipeline::Expression::Statement.new(@expression, variables).truthful? + end + end + end + end +end diff --git a/lib/gitlab/ci/config/entry/job.rb b/lib/gitlab/ci/config/entry/job.rb index 2fd76bc3690..6e11c582750 100644 --- a/lib/gitlab/ci/config/entry/job.rb +++ b/lib/gitlab/ci/config/entry/job.rb @@ -11,17 +11,28 @@ module Gitlab include ::Gitlab::Config::Entry::Configurable include ::Gitlab::Config::Entry::Attributable - ALLOWED_KEYS = %i[tags script only except type image services + ALLOWED_WHEN = %w[on_success on_failure always manual delayed].freeze + ALLOWED_KEYS = %i[tags script only except rules type image services allow_failure type stage when start_in artifacts cache dependencies needs before_script after_script variables environment coverage retry parallel extends].freeze + REQUIRED_BY_NEEDS = %i[stage].freeze + validations do + validates :config, type: Hash validates :config, allowed_keys: ALLOWED_KEYS + validates :config, required_keys: REQUIRED_BY_NEEDS, if: :has_needs? validates :config, presence: true validates :script, presence: true validates :name, presence: true validates :name, type: Symbol + validates :config, + disallowed_keys: { + in: %i[only except when start_in], + message: 'key may not be used with `rules`' + }, + if: :has_rules? with_options allow_nil: true do validates :tags, array_of_strings: true @@ -29,17 +40,19 @@ module Gitlab validates :parallel, numericality: { only_integer: true, greater_than_or_equal_to: 2, less_than_or_equal_to: 50 } - validates :when, - inclusion: { in: %w[on_success on_failure always manual delayed], - message: 'should be on_success, on_failure, ' \ - 'always, manual or delayed' } + validates :when, inclusion: { + in: ALLOWED_WHEN, + message: "should be one of: #{ALLOWED_WHEN.join(', ')}" + } + validates :dependencies, array_of_strings: true validates :needs, array_of_strings: true validates :extends, array_of_strings_or_string: true + validates :rules, array_of_hashes: true end validates :start_in, duration: { limit: '1 day' }, if: :delayed? - validates :start_in, absence: true, unless: :delayed? + validates :start_in, absence: true, if: -> { has_rules? || !delayed? } validate do next unless dependencies.present? @@ -88,6 +101,9 @@ module Gitlab entry :except, Entry::Policy, description: 'Refs policy this job will be executed for.' + entry :rules, Entry::Rules, + description: 'List of evaluable Rules to determine job inclusion.' + entry :variables, Entry::Variables, description: 'Environment variables available for this job.' @@ -109,7 +125,7 @@ module Gitlab :parallel, :needs attributes :script, :tags, :allow_failure, :when, :dependencies, - :needs, :retry, :parallel, :extends, :start_in + :needs, :retry, :parallel, :extends, :start_in, :rules def self.matching?(name, config) !name.to_s.start_with?('.') && @@ -148,6 +164,10 @@ module Gitlab self.when == 'delayed' end + def has_rules? + @config.try(:key?, :rules) + end + def ignored? allow_failure.nil? ? manual_action? : allow_failure end diff --git a/lib/gitlab/ci/config/entry/rules.rb b/lib/gitlab/ci/config/entry/rules.rb new file mode 100644 index 00000000000..65cad0880f5 --- /dev/null +++ b/lib/gitlab/ci/config/entry/rules.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + class Config + module Entry + class Rules < ::Gitlab::Config::Entry::Node + include ::Gitlab::Config::Entry::Validatable + + validations do + validates :config, presence: true + validates :config, type: Array + end + + def compose!(deps = nil) + super(deps) do + @config.each_with_index do |rule, index| + @entries[index] = ::Gitlab::Config::Entry::Factory.new(Entry::Rules::Rule) + .value(rule) + .with(key: "rule", parent: self, description: "rule definition.") # rubocop:disable CodeReuse/ActiveRecord + .create! + end + + @entries.each_value do |entry| + entry.compose!(deps) + end + end + end + end + end + end + end +end diff --git a/lib/gitlab/ci/config/entry/rules/rule.rb b/lib/gitlab/ci/config/entry/rules/rule.rb new file mode 100644 index 00000000000..1f2a34ec90e --- /dev/null +++ b/lib/gitlab/ci/config/entry/rules/rule.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + class Config + module Entry + class Rules::Rule < ::Gitlab::Config::Entry::Node + include ::Gitlab::Config::Entry::Validatable + include ::Gitlab::Config::Entry::Attributable + + CLAUSES = %i[if changes].freeze + ALLOWED_KEYS = %i[if changes when start_in].freeze + ALLOWED_WHEN = %w[on_success on_failure always never manual delayed].freeze + + attributes :if, :changes, :when, :start_in + + validations do + validates :config, presence: true + validates :config, type: { with: Hash } + validates :config, allowed_keys: ALLOWED_KEYS + validates :config, disallowed_keys: %i[start_in], unless: :specifies_delay? + validates :start_in, presence: true, if: :specifies_delay? + validates :start_in, duration: { limit: '1 day' }, if: :specifies_delay? + + with_options allow_nil: true do + validates :if, expression: true + validates :changes, array_of_strings: true + validates :when, allowed_values: { in: ALLOWED_WHEN } + end + end + + def specifies_delay? + self.when == 'delayed' + end + + def default + end + end + end + end + end +end diff --git a/lib/gitlab/ci/pipeline/chain/limit/job_activity.rb b/lib/gitlab/ci/pipeline/chain/limit/job_activity.rb new file mode 100644 index 00000000000..31c218bf954 --- /dev/null +++ b/lib/gitlab/ci/pipeline/chain/limit/job_activity.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + module Pipeline + module Chain + module Limit + class JobActivity < Chain::Base + def perform! + # to be overridden in EE + end + + def break? + false # to be overridden in EE + end + end + end + end + end + end +end diff --git a/lib/gitlab/ci/pipeline/chain/populate.rb b/lib/gitlab/ci/pipeline/chain/populate.rb index 0405292a25b..65029f5ce7f 100644 --- a/lib/gitlab/ci/pipeline/chain/populate.rb +++ b/lib/gitlab/ci/pipeline/chain/populate.rb @@ -23,12 +23,17 @@ module Gitlab @command.seeds_block&.call(pipeline) ## - # Populate pipeline with all stages, and stages with builds. + # Gather all runtime build/stage errors # - pipeline.stage_seeds.each do |stage| - pipeline.stages << stage.to_resource + if seeds_errors = pipeline.stage_seeds.flat_map(&:errors).compact.presence + return error(seeds_errors.join("\n")) end + ## + # Populate pipeline with all stages, and stages with builds. + # + pipeline.stages = pipeline.stage_seeds.map(&:to_resource) + if pipeline.stages.none? return error('No stages / jobs for this pipeline.') end diff --git a/lib/gitlab/ci/pipeline/expression/lexeme/matches.rb b/lib/gitlab/ci/pipeline/expression/lexeme/matches.rb index 942e4e55323..f7b0720d4a9 100644 --- a/lib/gitlab/ci/pipeline/expression/lexeme/matches.rb +++ b/lib/gitlab/ci/pipeline/expression/lexeme/matches.rb @@ -11,8 +11,9 @@ module Gitlab def evaluate(variables = {}) text = @left.evaluate(variables) regexp = @right.evaluate(variables) + return false unless regexp - regexp.scan(text.to_s).any? + regexp.scan(text.to_s).present? end def self.build(_value, behind, ahead) diff --git a/lib/gitlab/ci/pipeline/expression/lexeme/not_matches.rb b/lib/gitlab/ci/pipeline/expression/lexeme/not_matches.rb index 831c27fa0ea..02479ed28a4 100644 --- a/lib/gitlab/ci/pipeline/expression/lexeme/not_matches.rb +++ b/lib/gitlab/ci/pipeline/expression/lexeme/not_matches.rb @@ -11,8 +11,9 @@ module Gitlab def evaluate(variables = {}) text = @left.evaluate(variables) regexp = @right.evaluate(variables) + return true unless regexp - regexp.scan(text.to_s).none? + regexp.scan(text.to_s).empty? end def self.build(_value, behind, ahead) diff --git a/lib/gitlab/ci/pipeline/seed/base.rb b/lib/gitlab/ci/pipeline/seed/base.rb index 1fd3a61017f..e9e22569ae0 100644 --- a/lib/gitlab/ci/pipeline/seed/base.rb +++ b/lib/gitlab/ci/pipeline/seed/base.rb @@ -13,6 +13,10 @@ module Gitlab raise NotImplementedError end + def errors + raise NotImplementedError + end + def to_resource raise NotImplementedError end diff --git a/lib/gitlab/ci/pipeline/seed/build.rb b/lib/gitlab/ci/pipeline/seed/build.rb index ab0d4c38ab6..1066331062b 100644 --- a/lib/gitlab/ci/pipeline/seed/build.rb +++ b/lib/gitlab/ci/pipeline/seed/build.rb @@ -7,17 +7,28 @@ module Gitlab class Build < Seed::Base include Gitlab::Utils::StrongMemoize - delegate :dig, to: :@attributes + delegate :dig, to: :@seed_attributes + + # When the `ci_dag_limit_needs` is enabled it uses the lower limit + LOW_NEEDS_LIMIT = 5 + HARD_NEEDS_LIMIT = 50 def initialize(pipeline, attributes, previous_stages) @pipeline = pipeline - @attributes = attributes + @seed_attributes = attributes @previous_stages = previous_stages + @needs_attributes = dig(:needs_attributes) + + @using_rules = attributes.key?(:rules) + @using_only = attributes.key?(:only) + @using_except = attributes.key?(:except) @only = Gitlab::Ci::Build::Policy .fabricate(attributes.delete(:only)) @except = Gitlab::Ci::Build::Policy .fabricate(attributes.delete(:except)) + @rules = Gitlab::Ci::Build::Rules + .new(attributes.delete(:rules)) end def name @@ -26,28 +37,61 @@ module Gitlab def included? strong_memoize(:inclusion) do - all_of_only? && - none_of_except? && - all_of_needs? + if @using_rules + included_by_rules? + elsif @using_only || @using_except + all_of_only? && none_of_except? + else + true + end + end + end + + def errors + return unless included? + + strong_memoize(:errors) do + needs_errors end end def attributes - @attributes.merge( - pipeline: @pipeline, - project: @pipeline.project, - user: @pipeline.user, - ref: @pipeline.ref, - tag: @pipeline.tag, - trigger_request: @pipeline.legacy_trigger, - protected: @pipeline.protected_ref? - ) + @seed_attributes + .deep_merge(pipeline_attributes) + .deep_merge(rules_attributes) end def bridge? - @attributes.to_h.dig(:options, :trigger).present? + attributes_hash = @seed_attributes.to_h + attributes_hash.dig(:options, :trigger).present? || + (attributes_hash.dig(:options, :bridge_needs).instance_of?(Hash) && + attributes_hash.dig(:options, :bridge_needs, :pipeline).present?) + end + + def to_resource + strong_memoize(:resource) do + if bridge? + ::Ci::Bridge.new(attributes) + else + ::Ci::Build.new(attributes) + end + end + end + + def scoped_variables_hash + strong_memoize(:scoped_variables_hash) do + # This is a temporary piece of technical debt to allow us access + # to the CI variables to evaluate rules before we persist a Build + # with the result. We should refactor away the extra Build.new, + # but be able to get CI Variables directly from the Seed::Build. + ::Ci::Build.new( + @seed_attributes.merge(pipeline_attributes) + ).scoped_variables_hash + end end + private + def all_of_only? @only.all? { |spec| spec.satisfied_by?(@pipeline, self) } end @@ -56,24 +100,52 @@ module Gitlab @except.none? { |spec| spec.satisfied_by?(@pipeline, self) } end - def all_of_needs? - return true unless Feature.enabled?(:ci_dag_support, @pipeline.project) - return true if dig(:needs_attributes).nil? + def needs_errors + return if @needs_attributes.nil? - dig(:needs_attributes).all? do |need| - @previous_stages.any? do |stage| + if @needs_attributes.size > max_needs_allowed + return [ + "#{name}: one job can only need #{max_needs_allowed} others, but you have listed #{@needs_attributes.size}. " \ + "See needs keyword documentation for more details" + ] + end + + @needs_attributes.flat_map do |need| + result = @previous_stages.any? do |stage| stage.seeds_names.include?(need[:name]) end + + "#{name}: needs '#{need[:name]}'" unless result + end.compact + end + + def max_needs_allowed + if Feature.enabled?(:ci_dag_limit_needs, @project, default_enabled: true) + LOW_NEEDS_LIMIT + else + HARD_NEEDS_LIMIT end end - def to_resource - strong_memoize(:resource) do - if bridge? - ::Ci::Bridge.new(attributes) - else - ::Ci::Build.new(attributes) - end + def pipeline_attributes + { + pipeline: @pipeline, + project: @pipeline.project, + user: @pipeline.user, + ref: @pipeline.ref, + tag: @pipeline.tag, + trigger_request: @pipeline.legacy_trigger, + protected: @pipeline.protected_ref? + } + end + + def included_by_rules? + rules_attributes[:when] != 'never' + end + + def rules_attributes + strong_memoize(:rules_attributes) do + @using_rules ? @rules.evaluate(@pipeline, self).to_h.compact : {} end end end diff --git a/lib/gitlab/ci/pipeline/seed/stage.rb b/lib/gitlab/ci/pipeline/seed/stage.rb index 7c737027445..b600df2f656 100644 --- a/lib/gitlab/ci/pipeline/seed/stage.rb +++ b/lib/gitlab/ci/pipeline/seed/stage.rb @@ -33,6 +33,12 @@ module Gitlab end end + def errors + strong_memoize(:errors) do + seeds.flat_map(&:errors).compact + end + end + def seeds_names strong_memoize(:seeds_names) do seeds.map(&:name).to_set diff --git a/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml b/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml index 4190de73e1f..90278122361 100644 --- a/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml @@ -46,11 +46,14 @@ sast: SAST_DOCKER_CLIENT_NEGOTIATION_TIMEOUT \ SAST_PULL_ANALYZER_IMAGE_TIMEOUT \ SAST_RUN_ANALYZER_TIMEOUT \ + SAST_JAVA_VERSION \ ANT_HOME \ ANT_PATH \ GRADLE_PATH \ JAVA_OPTS \ JAVA_PATH \ + JAVA_8_VERSION \ + JAVA_11_VERSION \ MAVEN_CLI_OPTS \ MAVEN_PATH \ MAVEN_REPO_PATH \ diff --git a/lib/gitlab/ci/yaml_processor.rb b/lib/gitlab/ci/yaml_processor.rb index 998130e5bd0..2e1eab270ff 100644 --- a/lib/gitlab/ci/yaml_processor.rb +++ b/lib/gitlab/ci/yaml_processor.rb @@ -55,7 +55,8 @@ module Gitlab parallel: job[:parallel], instance: job[:instance], start_in: job[:start_in], - trigger: job[:trigger] + trigger: job[:trigger], + bridge_needs: job[:needs] }.compact }.compact end diff --git a/lib/gitlab/config/entry/attributable.rb b/lib/gitlab/config/entry/attributable.rb index 560fe63df0e..87bd257f69a 100644 --- a/lib/gitlab/config/entry/attributable.rb +++ b/lib/gitlab/config/entry/attributable.rb @@ -18,6 +18,10 @@ module Gitlab config[attribute] end + + define_method("has_#{attribute}?") do + config.is_a?(Hash) && config.key?(attribute) + end end end end diff --git a/lib/gitlab/config/entry/validators.rb b/lib/gitlab/config/entry/validators.rb index 6796fcce75f..374f929878e 100644 --- a/lib/gitlab/config/entry/validators.rb +++ b/lib/gitlab/config/entry/validators.rb @@ -20,7 +20,20 @@ module Gitlab present_keys = value.try(:keys).to_a & options[:in] if present_keys.any? - record.errors.add(attribute, "contains disallowed keys: " + + message = options[:message] || "contains disallowed keys" + message += ": #{present_keys.join(', ')}" + + record.errors.add(attribute, message) + end + end + end + + class RequiredKeysValidator < ActiveModel::EachValidator + def validate_each(record, attribute, value) + present_keys = options[:in] - value.try(:keys).to_a + + if present_keys.any? + record.errors.add(attribute, "missing required keys: " + present_keys.join(', ')) end end @@ -54,6 +67,16 @@ module Gitlab end end + class ArrayOfHashesValidator < ActiveModel::EachValidator + include LegacyValidationHelpers + + def validate_each(record, attribute, value) + unless value.is_a?(Array) && value.map { |hsh| hsh.is_a?(Hash) }.all? + record.errors.add(attribute, 'should be an array of hashes') + end + end + end + class ArrayOrStringValidator < ActiveModel::EachValidator def validate_each(record, attribute, value) unless value.is_a?(Array) || value.is_a?(String) @@ -220,6 +243,14 @@ module Gitlab end end + class ExpressionValidator < ActiveModel::EachValidator + def validate_each(record, attribute, value) + unless value.is_a?(String) && ::Gitlab::Ci::Pipeline::Expression::Statement.new(value).valid? + record.errors.add(attribute, 'Invalid expression syntax') + end + end + end + class PortNamePresentAndUniqueValidator < ActiveModel::EachValidator def validate_each(record, attribute, value) return unless value.is_a?(Array) diff --git a/lib/gitlab/daemon.rb b/lib/gitlab/daemon.rb index 6d5fc4219fb..2f4ae010e74 100644 --- a/lib/gitlab/daemon.rb +++ b/lib/gitlab/daemon.rb @@ -46,7 +46,10 @@ module Gitlab if thread thread.wakeup if thread.alive? - thread.join unless Thread.current == thread + begin + thread.join unless Thread.current == thread + rescue Exception # rubocop:disable Lint/RescueException + end @thread = nil end end diff --git a/lib/gitlab/danger/helper.rb b/lib/gitlab/danger/helper.rb index c0a12318990..332ca8bf9b8 100644 --- a/lib/gitlab/danger/helper.rb +++ b/lib/gitlab/danger/helper.rb @@ -113,7 +113,7 @@ module Gitlab yarn\.lock )\z}x => :frontend, - %r{\A(ee/)?db/} => :database, + %r{\A(ee/)?db/(?!fixtures)[^/]+} => :database, %r{\A(ee/)?lib/gitlab/(database|background_migration|sql|github_import)(/|\.rb)} => :database, %r{\A(app/models/project_authorization|app/services/users/refresh_authorized_projects_service)(/|\.rb)} => :database, %r{\Arubocop/cop/migration(/|\.rb)} => :database, diff --git a/lib/gitlab/data_builder/push.rb b/lib/gitlab/data_builder/push.rb index 40bda3410e1..75d9a2d55b9 100644 --- a/lib/gitlab/data_builder/push.rb +++ b/lib/gitlab/data_builder/push.rb @@ -60,7 +60,8 @@ module Gitlab # rubocop:disable Metrics/ParameterLists def build( project:, user:, ref:, oldrev: nil, newrev: nil, - commits: [], commits_count: nil, message: nil, push_options: {}) + commits: [], commits_count: nil, message: nil, push_options: {}, + with_changed_files: true) commits = Array(commits) @@ -75,7 +76,7 @@ module Gitlab # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/38259 commit_attrs = Gitlab::GitalyClient.allow_n_plus_1_calls do commits_limited.map do |commit| - commit.hook_attrs(with_changed_files: true) + commit.hook_attrs(with_changed_files: with_changed_files) end end @@ -128,8 +129,6 @@ module Gitlab SAMPLE_DATA end - private - def checkout_sha(repository, newrev, ref) # Checkout sha is nil when we remove branch or tag return if Gitlab::Git.blank_ref?(newrev) diff --git a/lib/gitlab/database/migration_helpers.rb b/lib/gitlab/database/migration_helpers.rb index 9bba4f6ce1e..57a413f8e04 100644 --- a/lib/gitlab/database/migration_helpers.rb +++ b/lib/gitlab/database/migration_helpers.rb @@ -470,7 +470,7 @@ module Gitlab # We set the default value _after_ adding the column so we don't end up # updating any existing data with the default value. This isn't # necessary since we copy over old values further down. - change_column_default(table, new, old_col.default) if old_col.default + change_column_default(table, new, old_col.default) unless old_col.default.nil? install_rename_triggers(table, old, new) @@ -482,6 +482,16 @@ module Gitlab copy_foreign_keys(table, old, new) end + def undo_rename_column_concurrently(table, old, new) + trigger_name = rename_trigger_name(table, old, new) + + check_trigger_permissions!(table) + + remove_rename_triggers_for_postgresql(table, trigger_name) + + remove_column(table, new) + end + # Installs triggers in a table that keep a new column in sync with an old # one. # @@ -547,6 +557,35 @@ module Gitlab remove_column(table, old) end + def undo_cleanup_concurrent_column_rename(table, old, new, type: nil) + if transaction_open? + raise 'undo_cleanup_concurrent_column_rename can not be run inside a transaction' + end + + check_trigger_permissions!(table) + + new_column = column_for(table, new) + + add_column(table, old, type || new_column.type, + limit: new_column.limit, + precision: new_column.precision, + scale: new_column.scale) + + # We set the default value _after_ adding the column so we don't end up + # updating any existing data with the default value. This isn't + # necessary since we copy over old values further down. + change_column_default(table, old, new_column.default) unless new_column.default.nil? + + install_rename_triggers(table, old, new) + + update_column_in_batches(table, old, Arel::Table.new(table)[new]) + + change_column_null(table, old, false) unless new_column.null + + copy_indexes(table, new, old) + copy_foreign_keys(table, new, old) + end + # Changes the column type of a table using a background migration. # # Because this method uses a background migration it's more suitable for @@ -747,6 +786,11 @@ module Gitlab EOF execute <<-EOF.strip_heredoc + DROP TRIGGER IF EXISTS #{trigger} + ON #{table} + EOF + + execute <<-EOF.strip_heredoc CREATE TRIGGER #{trigger} BEFORE INSERT OR UPDATE ON #{table} diff --git a/lib/gitlab/database_importers/self_monitoring/project/create_service.rb b/lib/gitlab/database_importers/self_monitoring/project/create_service.rb new file mode 100644 index 00000000000..164854e1e1a --- /dev/null +++ b/lib/gitlab/database_importers/self_monitoring/project/create_service.rb @@ -0,0 +1,251 @@ +# frozen_string_literal: true + +module Gitlab + module DatabaseImporters + module SelfMonitoring + module Project + include Stepable + + class CreateService < ::BaseService + include Stepable + + STEPS_ALLOWED_TO_FAIL = [ + :validate_application_settings, :validate_project_created, :validate_admins + ].freeze + + VISIBILITY_LEVEL = Gitlab::VisibilityLevel::INTERNAL + PROJECT_NAME = 'GitLab Instance Administration' + + steps :validate_application_settings, + :validate_project_created, + :validate_admins, + :create_group, + :create_project, + :save_project_id, + :add_group_members, + :add_to_whitelist, + :add_prometheus_manual_configuration + + def initialize + super(nil) + end + + def execute! + result = execute_steps + + if result[:status] == :success + result + elsif STEPS_ALLOWED_TO_FAIL.include?(result[:failed_step]) + success + else + raise StandardError, result[:message] + end + end + + private + + def validate_application_settings + return success if application_settings + + log_error(_('No application_settings found')) + error(_('No application_settings found')) + end + + def validate_project_created + return success unless project_created? + + log_error(_('Project already created')) + error(_('Project already created')) + end + + def validate_admins + unless instance_admins.any? + log_error(_('No active admin user found')) + return error(_('No active admin user found')) + end + + success + end + + def create_group + if project_created? + log_info(_('Instance administrators group already exists')) + @group = application_settings.instance_administration_project.owner + return success(group: @group) + end + + @group = ::Groups::CreateService.new(group_owner, create_group_params).execute + + if @group.persisted? + success(group: @group) + else + error(_('Could not create group')) + end + end + + def create_project + if project_created? + log_info(_('Instance administration project already exists')) + @project = application_settings.instance_administration_project + return success(project: project) + end + + @project = ::Projects::CreateService.new(group_owner, create_project_params).execute + + if project.persisted? + success(project: project) + else + log_error(_("Could not create instance administration project. Errors: %{errors}") % { errors: project.errors.full_messages }) + error(_('Could not create project')) + end + end + + def save_project_id + return success if project_created? + + result = application_settings.update(instance_administration_project_id: @project.id) + + if result + success + else + log_error(_("Could not save instance administration project ID, errors: %{errors}") % { errors: application_settings.errors.full_messages }) + error(_('Could not save project ID')) + end + end + + def add_group_members + members = @group.add_users(members_to_add, Gitlab::Access::MAINTAINER) + errors = members.flat_map { |member| member.errors.full_messages } + + if errors.any? + log_error(_('Could not add admins as members to self-monitoring project. Errors: %{errors}') % { errors: errors }) + error(_('Could not add admins as members')) + else + success + end + end + + def add_to_whitelist + return success unless prometheus_enabled? + return success unless prometheus_listen_address.present? + + uri = parse_url(internal_prometheus_listen_address_uri) + return error(_('Prometheus listen_address is not a valid URI')) unless uri + + application_settings.add_to_outbound_local_requests_whitelist([uri.normalized_host]) + result = application_settings.save + + if result + # Expire the Gitlab::CurrentSettings cache after updating the whitelist. + # This happens automatically in an after_commit hook, but in migrations, + # the after_commit hook only runs at the end of the migration. + Gitlab::CurrentSettings.expire_current_application_settings + success + else + log_error(_("Could not add prometheus URL to whitelist, errors: %{errors}") % { errors: application_settings.errors.full_messages }) + error(_('Could not add prometheus URL to whitelist')) + end + end + + def add_prometheus_manual_configuration + return success unless prometheus_enabled? + return success unless prometheus_listen_address.present? + + service = project.find_or_initialize_service('prometheus') + + unless service.update(prometheus_service_attributes) + log_error(_('Could not save prometheus manual configuration for self-monitoring project. Errors: %{errors}') % { errors: service.errors.full_messages }) + return error(_('Could not save prometheus manual configuration')) + end + + success + end + + def application_settings + @application_settings ||= ApplicationSetting.current_without_cache + end + + def project_created? + application_settings.instance_administration_project.present? + end + + def parse_url(uri_string) + Addressable::URI.parse(uri_string) + rescue Addressable::URI::InvalidURIError, TypeError + end + + def prometheus_enabled? + Gitlab.config.prometheus.enable + rescue Settingslogic::MissingSetting + log_error(_('prometheus.enable is not present in gitlab.yml')) + + false + end + + def prometheus_listen_address + Gitlab.config.prometheus.listen_address + rescue Settingslogic::MissingSetting + log_error(_('prometheus.listen_address is not present in gitlab.yml')) + + nil + end + + def instance_admins + @instance_admins ||= User.admins.active + end + + def group_owner + instance_admins.first + end + + def members_to_add + # Exclude admins who are already members of group because + # `@group.add_users(users)` returns an error if the users parameter contains + # users who are already members of the group. + instance_admins - @group.members.collect(&:user) + end + + def create_group_params + { + name: 'GitLab Instance Administrators', + path: "gitlab-instance-administrators-#{SecureRandom.hex(4)}", + visibility_level: VISIBILITY_LEVEL + } + end + + def docs_path + Rails.application.routes.url_helpers.help_page_path( + 'administration/monitoring/gitlab_instance_administration_project/index' + ) + end + + def create_project_params + { + initialize_with_readme: true, + visibility_level: VISIBILITY_LEVEL, + name: PROJECT_NAME, + description: "This project is automatically generated and will be used to help monitor this GitLab instance. [More information](#{docs_path})", + namespace_id: @group.id + } + end + + def internal_prometheus_listen_address_uri + if prometheus_listen_address.starts_with?('http') + prometheus_listen_address + else + 'http://' + prometheus_listen_address + end + end + + def prometheus_service_attributes + { + api_url: internal_prometheus_listen_address_uri, + manual_configuration: true, + active: true + } + end + end + end + end + end +end diff --git a/lib/gitlab/email/hook/smime_signature_interceptor.rb b/lib/gitlab/email/hook/smime_signature_interceptor.rb new file mode 100644 index 00000000000..e48041d9218 --- /dev/null +++ b/lib/gitlab/email/hook/smime_signature_interceptor.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +module Gitlab + module Email + module Hook + class SmimeSignatureInterceptor + # Sign emails with SMIME if enabled + class << self + def delivering_email(message) + signed_message = Gitlab::Email::Smime::Signer.sign( + cert: certificate.cert, + key: certificate.key, + data: message.encoded) + signed_email = Mail.new(signed_message) + + overwrite_body(message, signed_email) + overwrite_headers(message, signed_email) + end + + private + + def certificate + @certificate ||= Gitlab::Email::Smime::Certificate.from_files(key_path, cert_path) + end + + def key_path + Gitlab.config.gitlab.email_smime.key_file + end + + def cert_path + Gitlab.config.gitlab.email_smime.cert_file + end + + def overwrite_body(message, signed_email) + # since this is a multipart email, assignment to nil is important, + # otherwise Message#body will add a new mail part + message.body = nil + message.body = signed_email.body.encoded + end + + def overwrite_headers(message, signed_email) + message.content_disposition = signed_email.content_disposition + message.content_transfer_encoding = signed_email.content_transfer_encoding + message.content_type = signed_email.content_type + end + end + end + end + end +end diff --git a/lib/gitlab/email/smime/certificate.rb b/lib/gitlab/email/smime/certificate.rb new file mode 100644 index 00000000000..b331c4ca19c --- /dev/null +++ b/lib/gitlab/email/smime/certificate.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +module Gitlab + module Email + module Smime + class Certificate + include OpenSSL + + attr_reader :key, :cert + + def key_string + @key.to_s + end + + def cert_string + @cert.to_pem + end + + def self.from_strings(key_string, cert_string) + key = PKey::RSA.new(key_string) + cert = X509::Certificate.new(cert_string) + new(key, cert) + end + + def self.from_files(key_path, cert_path) + from_strings(File.read(key_path), File.read(cert_path)) + end + + def initialize(key, cert) + @key = key + @cert = cert + end + end + end + end +end diff --git a/lib/gitlab/email/smime/signer.rb b/lib/gitlab/email/smime/signer.rb new file mode 100644 index 00000000000..2fa83014003 --- /dev/null +++ b/lib/gitlab/email/smime/signer.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +require 'openssl' + +module Gitlab + module Email + module Smime + # Tooling for signing and verifying data with SMIME + class Signer + include OpenSSL + + def self.sign(cert:, key:, data:) + signed_data = PKCS7.sign(cert, key, data, nil, PKCS7::DETACHED) + PKCS7.write_smime(signed_data) + end + + # return nil if data cannot be verified, otherwise the signed content data + def self.verify_signature(cert:, ca_cert: nil, signed_data:) + store = X509::Store.new + store.set_default_paths + store.add_cert(ca_cert) if ca_cert + + signed_smime = PKCS7.read_smime(signed_data) + signed_smime if signed_smime.verify([cert], store) + end + end + end + end +end diff --git a/lib/gitlab/git_post_receive.rb b/lib/gitlab/git_post_receive.rb index d98b85fecc4..2a8bcd015a8 100644 --- a/lib/gitlab/git_post_receive.rb +++ b/lib/gitlab/git_post_receive.rb @@ -27,6 +27,29 @@ module Gitlab end end + def includes_branches? + enum_for(:changes_refs).any? do |_oldrev, _newrev, ref| + Gitlab::Git.branch_ref?(ref) + end + end + + def includes_tags? + enum_for(:changes_refs).any? do |_oldrev, _newrev, ref| + Gitlab::Git.tag_ref?(ref) + end + end + + def includes_default_branch? + # If the branch doesn't have a default branch yet, we presume the + # first branch pushed will be the default. + return true unless project.default_branch.present? + + enum_for(:changes_refs).any? do |_oldrev, _newrev, ref| + Gitlab::Git.branch_ref?(ref) && + Gitlab::Git.branch_name(ref) == project.default_branch + end + end + private def deserialize_changes(changes) diff --git a/lib/gitlab/gitaly_client.rb b/lib/gitlab/gitaly_client.rb index e6cbfb00f60..201db9fec26 100644 --- a/lib/gitlab/gitaly_client.rb +++ b/lib/gitlab/gitaly_client.rb @@ -406,7 +406,8 @@ module Gitlab def self.filesystem_id(storage) response = Gitlab::GitalyClient::ServerService.new(storage).info storage_status = response.storage_statuses.find { |status| status.storage_name == storage } - storage_status.filesystem_id + + storage_status&.filesystem_id end def self.filesystem_id_from_disk(storage) diff --git a/lib/gitlab/grape_logging/loggers/client_env_logger.rb b/lib/gitlab/grape_logging/loggers/client_env_logger.rb new file mode 100644 index 00000000000..3acc6f6a2ef --- /dev/null +++ b/lib/gitlab/grape_logging/loggers/client_env_logger.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +# This is a fork of +# https://github.com/aserafin/grape_logging/blob/master/lib/grape_logging/loggers/client_env.rb +# to use remote_ip instead of ip. +module Gitlab + module GrapeLogging + module Loggers + class ClientEnvLogger < ::GrapeLogging::Loggers::Base + def parameters(request, _) + { remote_ip: request.env["HTTP_X_FORWARDED_FOR"] || request.env["REMOTE_ADDR"], ua: request.env["HTTP_USER_AGENT"] } + end + end + end + end +end diff --git a/lib/gitlab/graphql/loaders/batch_root_storage_statistics_loader.rb b/lib/gitlab/graphql/loaders/batch_root_storage_statistics_loader.rb new file mode 100644 index 00000000000..a0312366d66 --- /dev/null +++ b/lib/gitlab/graphql/loaders/batch_root_storage_statistics_loader.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Gitlab + module Graphql + module Loaders + class BatchRootStorageStatisticsLoader + attr_reader :namespace_id + + def initialize(namespace_id) + @namespace_id = namespace_id + end + + def find + BatchLoader.for(namespace_id).batch do |namespace_ids, loader| + Namespace::RootStorageStatistics.for_namespace_ids(namespace_ids).each do |statistics| + loader.call(statistics.namespace_id, statistics) + end + end + end + end + end + end +end diff --git a/lib/gitlab/import_export/import_export.yml b/lib/gitlab/import_export/import_export.yml index f3888857bb6..bd0f3e70749 100644 --- a/lib/gitlab/import_export/import_export.yml +++ b/lib/gitlab/import_export/import_export.yml @@ -37,11 +37,11 @@ project_tree: - :user - merge_requests: - :metrics - - :suggestions - notes: - :author - events: - :push_event_payload + - :suggestions - merge_request_diff: - :merge_request_diff_commits - :merge_request_diff_files @@ -137,6 +137,7 @@ excluded_attributes: - :packages_enabled - :mirror_last_update_at - :mirror_last_successful_update_at + - :emails_disabled namespaces: - :runners_token - :runners_token_encrypted diff --git a/lib/gitlab/kubernetes/helm.rb b/lib/gitlab/kubernetes/helm.rb index 42c4745ff98..6e4286589c1 100644 --- a/lib/gitlab/kubernetes/helm.rb +++ b/lib/gitlab/kubernetes/helm.rb @@ -3,8 +3,8 @@ module Gitlab module Kubernetes module Helm - HELM_VERSION = '2.12.3'.freeze - KUBECTL_VERSION = '1.11.7'.freeze + HELM_VERSION = '2.14.3'.freeze + KUBECTL_VERSION = '1.11.10'.freeze NAMESPACE = 'gitlab-managed-apps'.freeze SERVICE_ACCOUNT = 'tiller'.freeze CLUSTER_ROLE_BINDING = 'tiller-admin'.freeze diff --git a/lib/gitlab/kubernetes/helm/reset_command.rb b/lib/gitlab/kubernetes/helm/reset_command.rb index 37e1d8573ab..a35ffa34c58 100644 --- a/lib/gitlab/kubernetes/helm/reset_command.rb +++ b/lib/gitlab/kubernetes/helm/reset_command.rb @@ -38,9 +38,9 @@ module Gitlab # Tracking this method to be removed here: # https://gitlab.com/gitlab-org/gitlab-ce/issues/52791#note_199374155 def delete_tiller_replicaset - command = %w[kubectl delete replicaset -n gitlab-managed-apps -l name=tiller] + delete_args = %w[replicaset -n gitlab-managed-apps -l name=tiller] - command.shelljoin + Gitlab::Kubernetes::KubectlCmd.delete(*delete_args) end def reset_helm_command diff --git a/lib/gitlab/kubernetes/kubectl_cmd.rb b/lib/gitlab/kubernetes/kubectl_cmd.rb new file mode 100644 index 00000000000..981eb5681dc --- /dev/null +++ b/lib/gitlab/kubernetes/kubectl_cmd.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module Gitlab + module Kubernetes + module KubectlCmd + class << self + def delete(*args) + %w(kubectl delete).concat(args).shelljoin + end + + def apply_file(filename, *args) + raise ArgumentError, "filename is not present" unless filename.present? + + %w(kubectl apply -f).concat([filename], args).shelljoin + end + end + end + end +end diff --git a/lib/gitlab/markdown_cache.rb b/lib/gitlab/markdown_cache.rb index 0354c710a3f..03a2f62cbd9 100644 --- a/lib/gitlab/markdown_cache.rb +++ b/lib/gitlab/markdown_cache.rb @@ -3,8 +3,8 @@ module Gitlab module MarkdownCache # Increment this number every time the renderer changes its output + CACHE_COMMONMARK_VERSION = 17 CACHE_COMMONMARK_VERSION_START = 10 - CACHE_COMMONMARK_VERSION = 16 BaseError = Class.new(StandardError) UnsupportedClassError = Class.new(BaseError) diff --git a/lib/gitlab/metrics/dashboard/url.rb b/lib/gitlab/metrics/dashboard/url.rb index b197e7ca86b..94f8b2e02b1 100644 --- a/lib/gitlab/metrics/dashboard/url.rb +++ b/lib/gitlab/metrics/dashboard/url.rb @@ -21,14 +21,26 @@ module Gitlab \/(?<environment>\d+) \/metrics (?<query> - \?[a-z0-9_=-]+ - (&[a-z0-9_=-]+)* + \?[a-zA-Z0-9%.()+_=-]+ + (&[a-zA-Z0-9%.()+_=-]+)* )? (?<anchor>\#[a-z0-9_-]+)? ) }x end + # Parses query params out from full url string into hash. + # + # Ex) 'https://<root>/<project>/<environment>/metrics?title=Title&group=Group' + # --> { title: 'Title', group: 'Group' } + def parse_query(url) + query_string = URI.parse(url).query.to_s + + CGI.parse(query_string) + .transform_values { |value| value.first } + .symbolize_keys + end + # Builds a metrics dashboard url based on the passed in arguments def build_dashboard_url(*args) Gitlab::Routing.url_helpers.metrics_dashboard_namespace_project_environment_url(*args) diff --git a/lib/gitlab/metrics/samplers/puma_sampler.rb b/lib/gitlab/metrics/samplers/puma_sampler.rb index 4e835f37c04..8a24d4f3663 100644 --- a/lib/gitlab/metrics/samplers/puma_sampler.rb +++ b/lib/gitlab/metrics/samplers/puma_sampler.rb @@ -15,7 +15,6 @@ module Gitlab puma_workers: ::Gitlab::Metrics.gauge(:puma_workers, 'Total number of workers'), puma_running_workers: ::Gitlab::Metrics.gauge(:puma_running_workers, 'Number of active workers'), puma_stale_workers: ::Gitlab::Metrics.gauge(:puma_stale_workers, 'Number of stale workers'), - puma_phase: ::Gitlab::Metrics.gauge(:puma_phase, 'Phase number (increased during phased restarts)'), puma_running: ::Gitlab::Metrics.gauge(:puma_running, 'Number of running threads'), puma_queued_connections: ::Gitlab::Metrics.gauge(:puma_queued_connections, 'Number of connections in that worker\'s "todo" set waiting for a worker thread'), puma_active_connections: ::Gitlab::Metrics.gauge(:puma_active_connections, 'Number of threads processing a request'), @@ -54,7 +53,6 @@ module Gitlab last_status = worker['last_status'] labels = { worker: "worker_#{worker['index']}" } - metrics[:puma_phase].set(labels, worker['phase']) set_worker_metrics(last_status, labels) if last_status.present? end end @@ -76,7 +74,6 @@ module Gitlab metrics[:puma_workers].set(labels, stats['workers']) metrics[:puma_running_workers].set(labels, stats['booted_workers']) metrics[:puma_stale_workers].set(labels, stats['old_workers']) - metrics[:puma_phase].set(labels, stats['phase']) end def set_worker_metrics(stats, labels = {}) diff --git a/lib/gitlab/profiler.rb b/lib/gitlab/profiler.rb index ec7671f9a8b..425c30d67fe 100644 --- a/lib/gitlab/profiler.rb +++ b/lib/gitlab/profiler.rb @@ -97,7 +97,7 @@ module Gitlab attr_reader :load_times_by_model, :private_token def debug(message, *) - message.gsub!(private_token, FILTERED_STRING) if private_token + message = message.gsub(private_token, FILTERED_STRING) if private_token _, type, time = *message.match(/(\w+) Load \(([0-9.]+)ms\)/) diff --git a/lib/gitlab/project_search_results.rb b/lib/gitlab/project_search_results.rb index 5e77d31760d..2669adb8455 100644 --- a/lib/gitlab/project_search_results.rb +++ b/lib/gitlab/project_search_results.rb @@ -29,6 +29,21 @@ module Gitlab end end + def formatted_count(scope) + case scope + when 'blobs' + blobs_count.to_s + when 'notes' + formatted_limited_count(limited_notes_count) + when 'wiki_blobs' + wiki_blobs_count.to_s + when 'commits' + commits_count.to_s + else + super + end + end + def users super.where(id: @project.team.members) # rubocop:disable CodeReuse/ActiveRecord end diff --git a/lib/gitlab/project_template.rb b/lib/gitlab/project_template.rb index dbf469a44c1..fa1d1203842 100644 --- a/lib/gitlab/project_template.rb +++ b/lib/gitlab/project_template.rb @@ -24,6 +24,14 @@ module Gitlab "#{preview}.git" end + def project_path + URI.parse(preview).path.sub(%r{\A/}, '') + end + + def uri_encoded_project_path + ERB::Util.url_encode(project_path) + end + def ==(other) name == other.name && title == other.title end diff --git a/lib/gitlab/quick_actions/issuable_actions.rb b/lib/gitlab/quick_actions/issuable_actions.rb index b975a967d03..e5d99ebee35 100644 --- a/lib/gitlab/quick_actions/issuable_actions.rb +++ b/lib/gitlab/quick_actions/issuable_actions.rb @@ -145,9 +145,9 @@ module Gitlab run_label_command(labels: find_labels(labels_param), command: :relabel, updates_key: :label_ids) end - desc _('Add a todo') - explanation _('Adds a todo.') - execution_message _('Added a todo.') + desc _('Add a To Do') + explanation _('Adds a To Do.') + execution_message _('Added a To Do.') types Issuable condition do quick_action_target.persisted? && @@ -157,9 +157,9 @@ module Gitlab @updates[:todo_event] = 'add' end - desc _('Mark to do as done') - explanation _('Marks to do as done.') - execution_message _('Marked to do as done.') + desc _('Mark To Do as done') + explanation _('Marks To Do as done.') + execution_message _('Marked To Do as done.') types Issuable condition do quick_action_target.persisted? && diff --git a/lib/gitlab/quick_actions/issue_actions.rb b/lib/gitlab/quick_actions/issue_actions.rb index 0868bd85600..da28fbf5be0 100644 --- a/lib/gitlab/quick_actions/issue_actions.rb +++ b/lib/gitlab/quick_actions/issue_actions.rb @@ -57,19 +57,18 @@ module Gitlab labels = find_labels(target_list_name) label_ids = labels.map(&:id) - if label_ids.size == 1 + if label_ids.size > 1 + message = _('Failed to move this issue because only a single label can be provided.') + elsif !Label.on_project_board?(quick_action_target.project_id, label_ids.first) + message = _('Failed to move this issue because label was not found.') + else label_id = label_ids.first - # Ensure this label corresponds to a list on the board - next unless Label.on_project_board?(quick_action_target.project_id, label_id) - @updates[:remove_label_ids] = quick_action_target.labels.on_project_boards(quick_action_target.project_id).where.not(id: label_id).pluck(:id) # rubocop: disable CodeReuse/ActiveRecord @updates[:add_label_ids] = [label_id] message = _("Moved issue to %{label} column in the board.") % { label: labels_to_reference(labels).first } - else - message = _('Move this issue failed because you need to specify only one label.') end @execution_message[:board_move] = message @@ -93,7 +92,7 @@ module Gitlab message = _("Marked this issue as a duplicate of %{duplicate_param}.") % { duplicate_param: duplicate_param } else - message = _('Mark as duplicate failed because referenced issue was not found') + message = _('Failed to mark this issue as a duplicate because referenced issue was not found.') end @execution_message[:duplicate] = message @@ -117,18 +116,18 @@ module Gitlab message = _("Moved this issue to %{path_to_project}.") % { path_to_project: target_project_path } else - message = _("Move this issue failed because target project doesn't exists") + message = _("Failed to move this issue because target project doesn't exist.") end @execution_message[:move] = message end - desc _('Make issue confidential.') + desc _('Make issue confidential') explanation do - _('Makes this issue confidential') + _('Makes this issue confidential.') end execution_message do - _('Made this issue confidential') + _('Made this issue confidential.') end types Issue condition do @@ -138,19 +137,19 @@ module Gitlab @updates[:confidential] = true end - desc _('Create a merge request.') + desc _('Create a merge request') explanation do |branch_name = nil| if branch_name - _("Creates branch '%{branch_name}' and a merge request to resolve this issue") % { branch_name: branch_name } + _("Creates branch '%{branch_name}' and a merge request to resolve this issue.") % { branch_name: branch_name } else - _('Creates a branch and a merge request to resolve this issue') + _('Creates a branch and a merge request to resolve this issue.') end end execution_message do |branch_name = nil| if branch_name - _("Created branch '%{branch_name}' and a merge request to resolve this issue") % { branch_name: branch_name } + _("Created branch '%{branch_name}' and a merge request to resolve this issue.") % { branch_name: branch_name } else - _('Created a branch and a merge request to resolve this issue') + _('Created a branch and a merge request to resolve this issue.') end end params "<branch name>" diff --git a/lib/gitlab/quick_actions/issue_and_merge_request_actions.rb b/lib/gitlab/quick_actions/issue_and_merge_request_actions.rb index 41ffd51cde8..533c74ba9b4 100644 --- a/lib/gitlab/quick_actions/issue_and_merge_request_actions.rb +++ b/lib/gitlab/quick_actions/issue_and_merge_request_actions.rb @@ -24,7 +24,7 @@ module Gitlab end command :assign do |users| if users.empty? - @execution_message[:assign] = _("Assign command failed because no user was found") + @execution_message[:assign] = _("Failed to assign a user because no user was found.") next end @@ -211,8 +211,8 @@ module Gitlab end desc _("Lock the discussion") - explanation _("Locks the discussion") - execution_message _("Locked the discussion") + explanation _("Locks the discussion.") + execution_message _("Locked the discussion.") types Issue, MergeRequest condition do quick_action_target.persisted? && @@ -224,8 +224,8 @@ module Gitlab end desc _("Unlock the discussion") - explanation _("Unlocks the discussion") - execution_message _("Unlocked the discussion") + explanation _("Unlocks the discussion.") + execution_message _("Unlocked the discussion.") types Issue, MergeRequest condition do quick_action_target.persisted? && diff --git a/lib/gitlab/quick_actions/substitution_definition.rb b/lib/gitlab/quick_actions/substitution_definition.rb index 2f78ea05cf0..0fda056a4fe 100644 --- a/lib/gitlab/quick_actions/substitution_definition.rb +++ b/lib/gitlab/quick_actions/substitution_definition.rb @@ -17,8 +17,9 @@ module Gitlab return unless content all_names.each do |a_name| - content.gsub!(%r{/#{a_name} ?(.*)$}i, execute_block(action_block, context, '\1')) + content = content.gsub(%r{/#{a_name} ?(.*)$}i, execute_block(action_block, context, '\1')) end + content end end diff --git a/lib/gitlab/repository_cache_adapter.rb b/lib/gitlab/repository_cache_adapter.rb index e40c366ed02..75503ee1789 100644 --- a/lib/gitlab/repository_cache_adapter.rb +++ b/lib/gitlab/repository_cache_adapter.rb @@ -23,6 +23,37 @@ module Gitlab end end + # Caches and strongly memoizes the method as a Redis Set. + # + # This only works for methods that do not take any arguments. The method + # should return an Array of Strings to be cached. + # + # In addition to overriding the named method, a "name_include?" method is + # defined. This uses the "SISMEMBER" query to efficiently check membership + # without needing to load the entire set into memory. + # + # name - The name of the method to be cached. + # fallback - A value to fall back to if the repository does not exist, or + # in case of a Git error. Defaults to nil. + def cache_method_as_redis_set(name, fallback: nil) + uncached_name = alias_uncached_method(name) + + define_method(name) do + cache_method_output_as_redis_set(name, fallback: fallback) do + __send__(uncached_name) # rubocop:disable GitlabSecurity/PublicSend + end + end + + define_method("#{name}_include?") do |value| + # If the cache isn't populated, we can't rely on it + return redis_set_cache.include?(name, value) if redis_set_cache.exist?(name) + + # Since we have to pull all branch names to populate the cache, use + # the data we already have to answer the query just this once + __send__(name).include?(value) # rubocop:disable GitlabSecurity/PublicSend + end + end + # Caches truthy values from the method. All values are strongly memoized, # and cached in RequestStore. # @@ -84,6 +115,11 @@ module Gitlab raise NotImplementedError end + # RepositorySetCache to be used. Should be overridden by the including class + def redis_set_cache + raise NotImplementedError + end + # List of cached methods. Should be overridden by the including class def cached_methods raise NotImplementedError @@ -100,6 +136,18 @@ module Gitlab end end + # Caches and strongly memoizes the supplied block as a Redis Set. The result + # will be provided as a sorted array. + # + # name - The name of the method to be cached. + # fallback - A value to fall back to if the repository does not exist, or + # in case of a Git error. Defaults to nil. + def cache_method_output_as_redis_set(name, fallback: nil, &block) + memoize_method_output(name, fallback: fallback) do + redis_set_cache.fetch(name, &block).sort + end + end + # Caches truthy values from the supplied block. All values are strongly # memoized, and cached in RequestStore. # @@ -154,6 +202,7 @@ module Gitlab clear_memoization(memoizable_name(name)) end + expire_redis_set_method_caches(methods) expire_request_store_method_caches(methods) end @@ -169,6 +218,10 @@ module Gitlab end end + def expire_redis_set_method_caches(methods) + methods.each { |name| redis_set_cache.expire(name) } + end + # All cached repository methods depend on the existence of a Git repository, # so if the repository doesn't exist, we already know not to call it. def fallback_early?(method_name) diff --git a/lib/gitlab/repository_set_cache.rb b/lib/gitlab/repository_set_cache.rb new file mode 100644 index 00000000000..fb634328a95 --- /dev/null +++ b/lib/gitlab/repository_set_cache.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +# Interface to the Redis-backed cache store for keys that use a Redis set +module Gitlab + class RepositorySetCache + attr_reader :repository, :namespace, :expires_in + + def initialize(repository, extra_namespace: nil, expires_in: 2.weeks) + @repository = repository + @namespace = "#{repository.full_path}:#{repository.project.id}" + @namespace = "#{@namespace}:#{extra_namespace}" if extra_namespace + @expires_in = expires_in + end + + def cache_key(type) + [type, namespace, 'set'].join(':') + end + + def expire(key) + with { |redis| redis.del(cache_key(key)) } + end + + def exist?(key) + with { |redis| redis.exists(cache_key(key)) } + end + + def read(key) + with { |redis| redis.smembers(cache_key(key)) } + end + + def write(key, value) + full_key = cache_key(key) + + with do |redis| + redis.multi do + redis.del(full_key) + + # Splitting into groups of 1000 prevents us from creating a too-long + # Redis command + value.in_groups_of(1000, false) { |subset| redis.sadd(full_key, subset) } + + redis.expire(full_key, expires_in) + end + end + + value + end + + def fetch(key, &block) + if exist?(key) + read(key) + else + write(key, yield) + end + end + + def include?(key, value) + with { |redis| redis.sismember(cache_key(key), value) } + end + + private + + def with(&blk) + Gitlab::Redis::Cache.with(&blk) # rubocop:disable CodeReuse/ActiveRecord + end + end +end diff --git a/lib/gitlab/search_results.rb b/lib/gitlab/search_results.rb index 7c1e6b1baff..ce4c1611687 100644 --- a/lib/gitlab/search_results.rb +++ b/lib/gitlab/search_results.rb @@ -43,6 +43,29 @@ module Gitlab without_count ? collection.without_count : collection end + def formatted_count(scope) + case scope + when 'projects' + formatted_limited_count(limited_projects_count) + when 'issues' + formatted_limited_count(limited_issues_count) + when 'merge_requests' + formatted_limited_count(limited_merge_requests_count) + when 'milestones' + formatted_limited_count(limited_milestones_count) + when 'users' + formatted_limited_count(limited_users_count) + end + end + + def formatted_limited_count(count) + if count >= COUNT_LIMIT + "#{COUNT_LIMIT - 1}+" + else + count.to_s + end + end + def limited_projects_count @limited_projects_count ||= limited_count(projects) end diff --git a/lib/gitlab/sentry.rb b/lib/gitlab/sentry.rb index 764db14d720..005cb3112b8 100644 --- a/lib/gitlab/sentry.rb +++ b/lib/gitlab/sentry.rb @@ -39,9 +39,14 @@ module Gitlab # development and test. If you need development and test to behave # just the same as production you can use this instead of # track_exception. + # + # If the exception implements the method `sentry_extra_data` and that method + # returns a Hash, then the return value of that method will be merged into + # `extra`. Exceptions can use this mechanism to provide structured data + # to sentry in addition to their message and back-trace. def self.track_acceptable_exception(exception, issue_url: nil, extra: {}) if enabled? - extra[:issue_url] = issue_url if issue_url + extra = build_extra_data(exception, issue_url, extra) context # Make sure we've set everything we know in the context Raven.capture_exception(exception, tags: default_tags, extra: extra) @@ -58,5 +63,15 @@ module Gitlab locale: I18n.locale } end + + def self.build_extra_data(exception, issue_url, extra) + exception.try(:sentry_extra_data)&.tap do |data| + extra.merge!(data) if data.is_a?(Hash) + end + + extra.merge({ issue_url: issue_url }.compact) + end + + private_class_method :build_extra_data end end diff --git a/lib/gitlab/sidekiq_logging/structured_logger.rb b/lib/gitlab/sidekiq_logging/structured_logger.rb index 60782306ade..48b1524f9c7 100644 --- a/lib/gitlab/sidekiq_logging/structured_logger.rb +++ b/lib/gitlab/sidekiq_logging/structured_logger.rb @@ -8,16 +8,16 @@ module Gitlab MAXIMUM_JOB_ARGUMENTS_LENGTH = 10.kilobytes def call(job, queue) - started_at = current_time + started_time = get_time base_payload = parse_job(job) - Sidekiq.logger.info log_job_start(started_at, base_payload) + Sidekiq.logger.info log_job_start(base_payload) yield - Sidekiq.logger.info log_job_done(job, started_at, base_payload) + Sidekiq.logger.info log_job_done(job, started_time, base_payload) rescue => job_exception - Sidekiq.logger.warn log_job_done(job, started_at, base_payload, job_exception) + Sidekiq.logger.warn log_job_done(job, started_time, base_payload, job_exception) raise end @@ -32,7 +32,7 @@ module Gitlab output_payload.merge!(job.slice(*::Gitlab::InstrumentationHelper::KEYS)) end - def log_job_start(started_at, payload) + def log_job_start(payload) payload['message'] = "#{base_message(payload)}: start" payload['job_status'] = 'start' @@ -45,11 +45,12 @@ module Gitlab payload end - def log_job_done(job, started_at, payload, job_exception = nil) + def log_job_done(job, started_time, payload, job_exception = nil) payload = payload.dup add_instrumentation_keys!(job, payload) - payload['duration'] = elapsed(started_at) - payload['completed_at'] = Time.now.utc + + elapsed_time = elapsed(started_time) + add_time_keys!(elapsed_time, payload) message = base_message(payload) @@ -69,6 +70,14 @@ module Gitlab payload end + def add_time_keys!(time, payload) + payload['duration'] = time[:duration].round(3) + payload['system_s'] = time[:stime].round(3) + payload['user_s'] = time[:utime].round(3) + payload['child_s'] = time[:ctime].round(3) if time[:ctime] > 0 + payload['completed_at'] = Time.now.utc + end + def parse_job(job) job = job.dup @@ -93,8 +102,25 @@ module Gitlab (Time.now.utc - start).to_f.round(3) end - def elapsed(start) - (current_time - start).round(3) + def elapsed(t0) + t1 = get_time + { + duration: t1[:now] - t0[:now], + stime: t1[:times][:stime] - t0[:times][:stime], + utime: t1[:times][:utime] - t0[:times][:utime], + ctime: ctime(t1[:times]) - ctime(t0[:times]) + } + end + + def get_time + { + now: current_time, + times: Process.times + } + end + + def ctime(times) + times[:cstime] + times[:cutime] end def current_time diff --git a/lib/gitlab/sidekiq_middleware/metrics.rb b/lib/gitlab/sidekiq_middleware/metrics.rb index b06ffa9c121..3dc9521ee8b 100644 --- a/lib/gitlab/sidekiq_middleware/metrics.rb +++ b/lib/gitlab/sidekiq_middleware/metrics.rb @@ -3,6 +3,10 @@ module Gitlab module SidekiqMiddleware class Metrics + # SIDEKIQ_LATENCY_BUCKETS are latency histogram buckets better suited to Sidekiq + # timeframes than the DEFAULT_BUCKET definition. Defined in seconds. + SIDEKIQ_LATENCY_BUCKETS = [0.1, 0.25, 0.5, 1, 2.5, 5, 10, 60, 300, 600].freeze + def initialize @metrics = init_metrics end @@ -31,7 +35,7 @@ module Gitlab def init_metrics { - sidekiq_jobs_completion_seconds: ::Gitlab::Metrics.histogram(:sidekiq_jobs_completion_seconds, 'Seconds to complete sidekiq job'), + sidekiq_jobs_completion_seconds: ::Gitlab::Metrics.histogram(:sidekiq_jobs_completion_seconds, 'Seconds to complete sidekiq job', buckets: SIDEKIQ_LATENCY_BUCKETS), sidekiq_jobs_failed_total: ::Gitlab::Metrics.counter(:sidekiq_jobs_failed_total, 'Sidekiq jobs failed'), sidekiq_jobs_retried_total: ::Gitlab::Metrics.counter(:sidekiq_jobs_retried_total, 'Sidekiq jobs retried'), sidekiq_running_jobs: ::Gitlab::Metrics.gauge(:sidekiq_running_jobs, 'Number of Sidekiq jobs running', {}, :livesum) diff --git a/lib/gitlab/sidekiq_middleware/monitor.rb b/lib/gitlab/sidekiq_middleware/monitor.rb new file mode 100644 index 00000000000..53a6132edac --- /dev/null +++ b/lib/gitlab/sidekiq_middleware/monitor.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module Gitlab + module SidekiqMiddleware + class Monitor + def call(worker, job, queue) + Gitlab::SidekiqMonitor.instance.within_job(job['jid'], queue) do + yield + end + rescue Gitlab::SidekiqMonitor::CancelledError + # push job to DeadSet + payload = ::Sidekiq.dump_json(job) + ::Sidekiq::DeadSet.new.kill(payload, notify_failure: false) + + # ignore retries + raise ::Sidekiq::JobRetry::Skip + end + end + end +end diff --git a/lib/gitlab/sidekiq_monitor.rb b/lib/gitlab/sidekiq_monitor.rb new file mode 100644 index 00000000000..9842f1f53f7 --- /dev/null +++ b/lib/gitlab/sidekiq_monitor.rb @@ -0,0 +1,182 @@ +# frozen_string_literal: true + +module Gitlab + class SidekiqMonitor < Daemon + include ::Gitlab::Utils::StrongMemoize + + NOTIFICATION_CHANNEL = 'sidekiq:cancel:notifications'.freeze + CANCEL_DEADLINE = 24.hours.seconds + RECONNECT_TIME = 3.seconds + + # We use exception derived from `Exception` + # to consider this as an very low-level exception + # that should not be caught by application + CancelledError = Class.new(Exception) # rubocop:disable Lint/InheritException + + attr_reader :jobs_thread + attr_reader :jobs_mutex + + def initialize + super + + @jobs_thread = {} + @jobs_mutex = Mutex.new + end + + def within_job(jid, queue) + jobs_mutex.synchronize do + jobs_thread[jid] = Thread.current + end + + if cancelled?(jid) + Sidekiq.logger.warn( + class: self.class.to_s, + action: 'run', + queue: queue, + jid: jid, + canceled: true + ) + raise CancelledError + end + + yield + ensure + jobs_mutex.synchronize do + jobs_thread.delete(jid) + end + end + + def self.cancel_job(jid) + payload = { + action: 'cancel', + jid: jid + }.to_json + + ::Gitlab::Redis::SharedState.with do |redis| + redis.setex(cancel_job_key(jid), CANCEL_DEADLINE, 1) + redis.publish(NOTIFICATION_CHANNEL, payload) + end + end + + private + + def start_working + Sidekiq.logger.info( + class: self.class.to_s, + action: 'start', + message: 'Starting Monitor Daemon' + ) + + while enabled? + process_messages + sleep(RECONNECT_TIME) + end + + ensure + Sidekiq.logger.warn( + class: self.class.to_s, + action: 'stop', + message: 'Stopping Monitor Daemon' + ) + end + + def stop_working + thread.raise(Interrupt) if thread.alive? + end + + def process_messages + ::Gitlab::Redis::SharedState.with do |redis| + redis.subscribe(NOTIFICATION_CHANNEL) do |on| + on.message do |channel, message| + process_message(message) + end + end + end + rescue Exception => e # rubocop:disable Lint/RescueException + Sidekiq.logger.warn( + class: self.class.to_s, + action: 'exception', + message: e.message + ) + + # we re-raise system exceptions + raise e unless e.is_a?(StandardError) + end + + def process_message(message) + Sidekiq.logger.info( + class: self.class.to_s, + channel: NOTIFICATION_CHANNEL, + message: 'Received payload on channel', + payload: message + ) + + message = safe_parse(message) + return unless message + + case message['action'] + when 'cancel' + process_job_cancel(message['jid']) + else + # unknown message + end + end + + def safe_parse(message) + JSON.parse(message) + rescue JSON::ParserError + end + + def process_job_cancel(jid) + return unless jid + + # try to find thread without lock + return unless find_thread_unsafe(jid) + + Thread.new do + # try to find a thread, but with guaranteed + # that handle for thread corresponds to actually + # running job + find_thread_with_lock(jid) do |thread| + Sidekiq.logger.warn( + class: self.class.to_s, + action: 'cancel', + message: 'Canceling thread with CancelledError', + jid: jid, + thread_id: thread.object_id + ) + + thread&.raise(CancelledError) + end + end + end + + # This method needs to be thread-safe + # This is why it passes thread in block, + # to ensure that we do process this thread + def find_thread_unsafe(jid) + jobs_thread[jid] + end + + def find_thread_with_lock(jid) + # don't try to lock if we cannot find the thread + return unless find_thread_unsafe(jid) + + jobs_mutex.synchronize do + find_thread_unsafe(jid).tap do |thread| + yield(thread) if thread + end + end + end + + def cancelled?(jid) + ::Gitlab::Redis::SharedState.with do |redis| + redis.exists(self.class.cancel_job_key(jid)) + end + end + + def self.cancel_job_key(jid) + "sidekiq:cancel:#{jid}" + end + end +end diff --git a/lib/gitlab/snippet_search_results.rb b/lib/gitlab/snippet_search_results.rb index e360b552f89..ac3b219e0c7 100644 --- a/lib/gitlab/snippet_search_results.rb +++ b/lib/gitlab/snippet_search_results.rb @@ -22,6 +22,17 @@ module Gitlab end end + def formatted_count(scope) + case scope + when 'snippet_titles' + snippet_titles_count.to_s + when 'snippet_blobs' + snippet_blobs_count.to_s + else + super + end + end + def snippet_titles_count @snippet_titles_count ||= snippet_titles.count end diff --git a/lib/gitlab/snowplow_tracker.rb b/lib/gitlab/snowplow_tracker.rb new file mode 100644 index 00000000000..9f12513e09e --- /dev/null +++ b/lib/gitlab/snowplow_tracker.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +require 'snowplow-tracker' + +module Gitlab + module SnowplowTracker + NAMESPACE = 'cf' + + class << self + def track_event(category, action, label: nil, property: nil, value: nil, context: nil) + tracker&.track_struct_event(category, action, label, property, value, context, Time.now.to_i) + end + + private + + def tracker + return unless enabled? + + @tracker ||= ::SnowplowTracker::Tracker.new(emitter, subject, NAMESPACE, Gitlab::CurrentSettings.snowplow_site_id) + end + + def subject + ::SnowplowTracker::Subject.new + end + + def emitter + ::SnowplowTracker::Emitter.new(Gitlab::CurrentSettings.snowplow_collector_hostname) + end + + def enabled? + Gitlab::CurrentSettings.snowplow_enabled? + end + end + end +end diff --git a/lib/gitlab/usage_data.rb b/lib/gitlab/usage_data.rb index 7e3a695e52a..353298e67b3 100644 --- a/lib/gitlab/usage_data.rb +++ b/lib/gitlab/usage_data.rb @@ -100,9 +100,7 @@ module Gitlab .merge(services_usage) .merge(approximate_counts) }.tap do |data| - if Feature.enabled?(:group_overview_security_dashboard) - data[:counts][:user_preferences] = user_preferences_usage - end + data[:counts][:user_preferences] = user_preferences_usage end end # rubocop: enable CodeReuse/ActiveRecord @@ -142,7 +140,9 @@ module Gitlab Gitlab::UsageDataCounters::WebIdeCounter, Gitlab::UsageDataCounters::NoteCounter, Gitlab::UsageDataCounters::SnippetCounter, - Gitlab::UsageDataCounters::SearchCounter + Gitlab::UsageDataCounters::SearchCounter, + Gitlab::UsageDataCounters::CycleAnalyticsCounter, + Gitlab::UsageDataCounters::SourceCodeCounter ] end @@ -188,8 +188,8 @@ module Gitlab {} # augmented in EE end - def count(relation, fallback: -1) - relation.count + def count(relation, count_by: nil, fallback: -1) + count_by ? relation.count(count_by) : relation.count rescue ActiveRecord::StatementInvalid fallback end diff --git a/lib/gitlab/usage_data_counters/cycle_analytics_counter.rb b/lib/gitlab/usage_data_counters/cycle_analytics_counter.rb new file mode 100644 index 00000000000..1ff4296ef65 --- /dev/null +++ b/lib/gitlab/usage_data_counters/cycle_analytics_counter.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +module Gitlab::UsageDataCounters + class CycleAnalyticsCounter < BaseCounter + KNOWN_EVENTS = %w[views].freeze + PREFIX = 'cycle_analytics' + end +end diff --git a/lib/gitlab/usage_data_counters/note_counter.rb b/lib/gitlab/usage_data_counters/note_counter.rb index e93a0bcfa27..672450ec82b 100644 --- a/lib/gitlab/usage_data_counters/note_counter.rb +++ b/lib/gitlab/usage_data_counters/note_counter.rb @@ -4,7 +4,7 @@ module Gitlab::UsageDataCounters class NoteCounter < BaseCounter KNOWN_EVENTS = %w[create].freeze PREFIX = 'note' - COUNTABLE_TYPES = %w[Snippet].freeze + COUNTABLE_TYPES = %w[Snippet Commit MergeRequest].freeze class << self def redis_key(event, noteable_type) @@ -24,9 +24,9 @@ module Gitlab::UsageDataCounters end def totals - { - snippet_comment: read(:create, 'Snippet') - } + COUNTABLE_TYPES.map do |countable_type| + [:"#{countable_type.underscore}_comment", read(:create, countable_type)] + end.to_h end private diff --git a/lib/gitlab/usage_data_counters/source_code_counter.rb b/lib/gitlab/usage_data_counters/source_code_counter.rb new file mode 100644 index 00000000000..8a1771a7bd1 --- /dev/null +++ b/lib/gitlab/usage_data_counters/source_code_counter.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +module Gitlab::UsageDataCounters + class SourceCodeCounter < BaseCounter + KNOWN_EVENTS = %w[pushes].freeze + PREFIX = 'source_code' + end +end diff --git a/lib/gitlab/visibility_level_checker.rb b/lib/gitlab/visibility_level_checker.rb new file mode 100644 index 00000000000..f15f1486a4e --- /dev/null +++ b/lib/gitlab/visibility_level_checker.rb @@ -0,0 +1,88 @@ +# frozen_string_literal: true + +# Gitlab::VisibilityLevelChecker verifies that: +# - Current @project.visibility_level is not restricted +# - Override visibility param is not restricted +# - @see https://docs.gitlab.com/ce/api/project_import_export.html#import-a-file +# +# @param current_user [User] Current user object to verify visibility level against +# @param project [Project] Current project that is being created/imported +# @param project_params [Hash] Supplementary project params (e.g. import +# params containing visibility override) +# +# @example +# user = User.find(2) +# project = Project.last +# project_params = {:import_data=>{:data=>{:override_params=>{"visibility"=>"public"}}}} +# level_checker = Gitlab::VisibilityLevelChecker.new(user, project, project_params: project_params) +# +# project_visibility = level_checker.level_restricted? +# => #<Gitlab::VisibilityEvaluationResult:0x00007fbe16ee33c0 @restricted=true, @visibility_level=20> +# +# if project_visibility.restricted? +# deny_visibility_level(project, project_visibility.visibility_level) +# end +# +# @return [VisibilityEvaluationResult] Visibility evaluation result. Responds to: +# #restricted - boolean indicating if level is restricted +# #visibility_level - integer of restricted visibility level +# +module Gitlab + class VisibilityLevelChecker + def initialize(current_user, project, project_params: {}) + @current_user = current_user + @project = project + @project_params = project_params + end + + def level_restricted? + return VisibilityEvaluationResult.new(true, override_visibility_level_value) if override_visibility_restricted? + return VisibilityEvaluationResult.new(true, project.visibility_level) if project_visibility_restricted? + + VisibilityEvaluationResult.new(false, nil) + end + + private + + attr_reader :current_user, :project, :project_params + + def override_visibility_restricted? + return unless import_data + return unless override_visibility_level + return if Gitlab::VisibilityLevel.allowed_for?(current_user, override_visibility_level_value) + + true + end + + def project_visibility_restricted? + return if Gitlab::VisibilityLevel.allowed_for?(current_user, project.visibility_level) + + true + end + + def import_data + @import_data ||= project_params[:import_data] + end + + def override_visibility_level + @override_visibility_level ||= import_data.deep_symbolize_keys.dig(:data, :override_params, :visibility) + end + + def override_visibility_level_value + @override_visibility_level_value ||= Gitlab::VisibilityLevel.level_value(override_visibility_level) + end + end + + class VisibilityEvaluationResult + attr_reader :visibility_level + + def initialize(restricted, visibility_level) + @restricted = restricted + @visibility_level = visibility_level + end + + def restricted? + @restricted + end + end +end diff --git a/lib/gt_one_coercion.rb b/lib/gt_one_coercion.rb deleted file mode 100644 index 99be51bc8c6..00000000000 --- a/lib/gt_one_coercion.rb +++ /dev/null @@ -1,7 +0,0 @@ -# frozen_string_literal: true - -class GtOneCoercion < Virtus::Attribute - def coerce(value) - [1, value.to_i].max - end -end diff --git a/lib/prometheus/cleanup_multiproc_dir_service.rb b/lib/prometheus/cleanup_multiproc_dir_service.rb new file mode 100644 index 00000000000..6418b4de166 --- /dev/null +++ b/lib/prometheus/cleanup_multiproc_dir_service.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Prometheus + class CleanupMultiprocDirService + include Gitlab::Utils::StrongMemoize + + def execute + FileUtils.rm_rf(old_metrics) if old_metrics + end + + private + + def old_metrics + strong_memoize(:old_metrics) do + Dir[File.join(multiprocess_files_dir, '*.db')] if multiprocess_files_dir + end + end + + def multiprocess_files_dir + ::Prometheus::Client.configuration.multiprocess_files_dir + end + end +end diff --git a/lib/tasks/gitlab/assets.rake b/lib/tasks/gitlab/assets.rake index a07ae3a418a..7a42e4e92a0 100644 --- a/lib/tasks/gitlab/assets.rake +++ b/lib/tasks/gitlab/assets.rake @@ -10,15 +10,9 @@ namespace :gitlab do rake:assets:precompile webpack:compile gitlab:assets:fix_urls - gitlab:assets:compile_vrt ].each(&Gitlab::TaskHelpers.method(:invoke_and_time_task)) end - desc 'GitLab | Assets | Compile visual review toolbar' - task :compile_vrt do - system 'yarn', 'webpack-vrt' - end - desc 'GitLab | Assets | Clean up old compiled frontend assets' task clean: ['rake:assets:clean'] diff --git a/lib/tasks/gitlab/update_templates.rake b/lib/tasks/gitlab/update_templates.rake index 8267c235a7f..fdcd34320b1 100644 --- a/lib/tasks/gitlab/update_templates.rake +++ b/lib/tasks/gitlab/update_templates.rake @@ -40,7 +40,6 @@ namespace :gitlab do templates.each do |template| params = { - import_url: template.clone_url, namespace_id: tmp_namespace.id, path: template.name, skip_wiki: true @@ -53,22 +52,46 @@ namespace :gitlab do raise "Failed to create project: #{project.errors.messages}" end - loop do - if project.import_finished? - puts "Import finished for #{template.name}" - break + uri_encoded_project_path = template.uri_encoded_project_path + + # extract a concrete commit for signing off what we actually downloaded + # this way we do the right thing even if the repository gets updated in the meantime + get_commits_response = Gitlab::HTTP.get("https://gitlab.com/api/v4/projects/#{uri_encoded_project_path}/repository/commits", + query: { page: 1, per_page: 1 } + ) + raise "Failed to retrieve latest commit for template '#{template.name}'" unless get_commits_response.success? + + commit_sha = get_commits_response.parsed_response.dig(0, 'id') + + project_archive_uri = "https://gitlab.com/api/v4/projects/#{uri_encoded_project_path}/repository/archive.tar.gz?sha=#{commit_sha}" + commit_message = <<~MSG + Initialized from '#{template.title}' project template + + Template repository: #{template.preview} + Commit SHA: #{commit_sha} + MSG + + Dir.mktmpdir do |tmpdir| + Dir.chdir(tmpdir) do + Gitlab::TaskHelpers.run_command!(['wget', project_archive_uri, '-O', 'archive.tar.gz']) + Gitlab::TaskHelpers.run_command!(['tar', 'xf', 'archive.tar.gz']) + extracted_project_basename = Dir['*/'].first + Dir.chdir(extracted_project_basename) do + Gitlab::TaskHelpers.run_command!(%w(git init)) + Gitlab::TaskHelpers.run_command!(%w(git add .)) + Gitlab::TaskHelpers.run_command!(['git', 'commit', '--author', 'GitLab <root@localhost>', '--message', commit_message]) + + # Hacky workaround to push to the project in a way that works with both GDK and the test environment + Gitlab::GitalyClient::StorageSettings.allow_disk_access do + Gitlab::TaskHelpers.run_command!(['git', 'remote', 'add', 'origin', "file://#{project.repository.raw.path}"]) + end + Gitlab::TaskHelpers.run_command!(['git', 'push', '-u', 'origin', 'master']) + end end - - if project.import_failed? - raise "Failed to import from #{project_params[:import_url]}" - end - - puts "Waiting for the import to finish" - - sleep(5) - project.reset end + project.reset + Projects::ImportExport::ExportService.new(project, admin).execute downloader.call(project.export_file, template.archive_path) diff --git a/lib/tasks/gitlab/uploads/legacy.rake b/lib/tasks/gitlab/uploads/legacy.rake new file mode 100644 index 00000000000..18fb8afe455 --- /dev/null +++ b/lib/tasks/gitlab/uploads/legacy.rake @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +namespace :gitlab do + namespace :uploads do + namespace :legacy do + desc "GitLab | Uploads | Migrate all legacy attachments" + task migrate: :environment do + class Upload < ApplicationRecord + self.table_name = 'uploads' + + include ::EachBatch + end + + migration = 'LegacyUploadsMigrator'.freeze + batch_size = 5000 + delay_interval = 5.minutes.to_i + + Upload.where(uploader: 'AttachmentUploader').each_batch(of: batch_size) do |relation, index| + start_id, end_id = relation.pluck('MIN(id), MAX(id)').first + delay = index * delay_interval + + BackgroundMigrationWorker.perform_in(delay, migration, [start_id, end_id]) + end + end + end + end +end diff --git a/lib/tasks/services.rake b/lib/tasks/services.rake index 56b81106c5f..4ec4fdd281f 100644 --- a/lib/tasks/services.rake +++ b/lib/tasks/services.rake @@ -86,7 +86,7 @@ namespace :services do doc_start = Time.now doc_path = File.join(Rails.root, 'doc', 'api', 'services.md') - result = ERB.new(services_template, 0, '>') + result = ERB.new(services_template, trim_mode: '>') .result(OpenStruct.new(services: services).instance_eval { binding }) File.open(doc_path, 'w') do |f| diff --git a/locale/gitlab.pot b/locale/gitlab.pot index bcbe515288c..840c27d1699 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -128,9 +128,6 @@ msgstr[1] "" msgid "%{actionText} & %{openOrClose} %{noteable}" msgstr "" -msgid "%{canMergeCount}/%{assigneesCount} can merge" -msgstr "" - msgid "%{commit_author_link} authored %{commit_timeago}" msgstr "" @@ -202,6 +199,9 @@ msgstr "" msgid "%{lock_path} is locked by GitLab User %{lock_user_id}" msgstr "" +msgid "%{mergeLength}/%{usersLength} can merge" +msgstr "" + msgid "%{mrText}, this issue will be closed automatically." msgstr "" @@ -279,6 +279,9 @@ msgstr "" msgid "%{usage_ping_link_start}Learn more%{usage_ping_link_end} about what information is shared with GitLab Inc." msgstr "" +msgid "%{userName} (cannot merge)" +msgstr "" + msgid "%{userName}'s avatar" msgstr "" @@ -306,6 +309,9 @@ msgstr "" msgid "(external source)" msgstr "" +msgid "+ %{amount} more" +msgstr "" + msgid "+ %{count} more" msgstr "" @@ -469,6 +475,9 @@ msgstr "" msgid "A Jekyll site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features." msgstr "" +msgid "A Let's Encrypt SSL certificate can not be obtained until your domain is verified." +msgstr "" + msgid "A Let's Encrypt account will be configured for this GitLab installation using your email address. You will receive emails to warn of expiring certificates." msgstr "" @@ -675,9 +684,6 @@ msgstr "" msgid "Add a task list" msgstr "" -msgid "Add a todo" -msgstr "" - msgid "Add an SSH key" msgstr "" @@ -753,7 +759,7 @@ msgstr "" msgid "Added %{label_references} %{label_text}." msgstr "" -msgid "Added a todo." +msgid "Added a To Do." msgstr "" msgid "Added at" @@ -768,7 +774,7 @@ msgstr "" msgid "Adds %{labels} %{label_text}." msgstr "" -msgid "Adds a todo." +msgid "Adds a To Do." msgstr "" msgid "Admin Area" @@ -990,9 +996,6 @@ msgstr "" msgid "Alternate support URL for help page and help dropdown" msgstr "" -msgid "Alternatively, you can use a %{personal_access_token_link}. When you create your Personal Access Token, you will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to import." -msgstr "" - msgid "Amount of time (in hours) that users are allowed to skip forced configuration of two-factor authentication" msgstr "" @@ -1098,9 +1101,6 @@ msgstr "" msgid "An error occurred while parsing recent searches" msgstr "" -msgid "An error occurred while rendering KaTeX" -msgstr "" - msgid "An error occurred while rendering preview broadcast message" msgstr "" @@ -1365,9 +1365,6 @@ msgstr "" msgid "Assign" msgstr "" -msgid "Assign command failed because no user was found" -msgstr "" - msgid "Assign custom color like #FF0000" msgstr "" @@ -1432,6 +1429,12 @@ msgstr "" msgid "August" msgstr "" +msgid "Authenticate" +msgstr "" + +msgid "Authenticate with GitHub" +msgstr "" + msgid "Authentication Log" msgstr "" @@ -2959,6 +2962,9 @@ msgstr "" msgid "Collapse sidebar" msgstr "" +msgid "Collector hostname" +msgstr "" + msgid "ComboSearch is not defined" msgstr "" @@ -2968,9 +2974,6 @@ msgstr "" msgid "Commands applied" msgstr "" -msgid "Commands did not apply" -msgstr "" - msgid "Comment" msgstr "" @@ -3129,6 +3132,9 @@ msgstr "" msgid "Configure storage path settings." msgstr "" +msgid "Configure the %{link} integration." +msgstr "" + msgid "Configure the way a user creates a new account." msgstr "" @@ -3181,14 +3187,19 @@ msgid "ContainerRegistry|Quick Start" msgstr "" msgid "ContainerRegistry|Remove image" -msgstr "" +msgid_plural "ContainerRegistry|Remove images" +msgstr[0] "" +msgstr[1] "" -msgid "ContainerRegistry|Remove image and tags" +msgid "ContainerRegistry|Remove image(s) and tags" msgstr "" msgid "ContainerRegistry|Remove repository" msgstr "" +msgid "ContainerRegistry|Remove selected images" +msgstr "" + msgid "ContainerRegistry|Size" msgstr "" @@ -3210,6 +3221,9 @@ msgstr "" msgid "ContainerRegistry|With the Docker Container Registry integrated into GitLab, every project can have its own space to store its Docker images. %{docLinkStart}More Information%{docLinkEnd}" msgstr "" +msgid "ContainerRegistry|You are about to delete <b>%{count}</b> images. This will delete the images and all tags pointing to them." +msgstr "" + msgid "ContainerRegistry|You are about to delete the image <b>%{title}</b>. This will delete the image and all tags pointing to this image." msgstr "" @@ -3270,6 +3284,9 @@ msgstr "" msgid "ConvDev Index" msgstr "" +msgid "Cookie domain" +msgstr "" + msgid "Copied" msgstr "" @@ -3333,6 +3350,12 @@ msgstr "" msgid "Copy token to clipboard" msgstr "" +msgid "Could not add admins as members" +msgstr "" + +msgid "Could not add admins as members to self-monitoring project. Errors: %{errors}" +msgstr "" + msgid "Could not add prometheus URL to whitelist" msgstr "" @@ -3351,6 +3374,9 @@ msgstr "" msgid "Could not create Wiki Repository at this time. Please try again later." msgstr "" +msgid "Could not create group" +msgstr "" + msgid "Could not create instance administration project. Errors: %{errors}" msgstr "" @@ -3378,6 +3404,12 @@ msgstr "" msgid "Could not save project ID" msgstr "" +msgid "Could not save prometheus manual configuration" +msgstr "" + +msgid "Could not save prometheus manual configuration for self-monitoring project. Errors: %{errors}" +msgstr "" + msgid "Coverage" msgstr "" @@ -3396,7 +3428,7 @@ msgstr "" msgid "Create a GitLab account first, and then connect it to your %{label} account." msgstr "" -msgid "Create a merge request." +msgid "Create a merge request" msgstr "" msgid "Create a new branch" @@ -3414,6 +3446,9 @@ msgstr "" msgid "Create a personal access token on your account to pull or push via %{protocol}." msgstr "" +msgid "Create and provide your GitHub %{link_start}Personal Access Token%{link_end}. You will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to import." +msgstr "" + msgid "Create board" msgstr "" @@ -3504,10 +3539,10 @@ msgstr "" msgid "Created At" msgstr "" -msgid "Created a branch and a merge request to resolve this issue" +msgid "Created a branch and a merge request to resolve this issue." msgstr "" -msgid "Created branch '%{branch_name}' and a merge request to resolve this issue" +msgid "Created branch '%{branch_name}' and a merge request to resolve this issue." msgstr "" msgid "Created by me" @@ -3519,10 +3554,10 @@ msgstr "" msgid "Created on:" msgstr "" -msgid "Creates a branch and a merge request to resolve this issue" +msgid "Creates a branch and a merge request to resolve this issue." msgstr "" -msgid "Creates branch '%{branch_name}' and a merge request to resolve this issue" +msgid "Creates branch '%{branch_name}' and a merge request to resolve this issue." msgstr "" msgid "Creating graphs uses the data from the Prometheus server. If this takes a long time, ensure that data is available." @@ -3579,6 +3614,30 @@ msgstr "" msgid "Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project." msgstr "" +msgid "CycleAnalyticsEvent|Issue created" +msgstr "" + +msgid "CycleAnalyticsEvent|Issue first associated with a milestone or issue first added to a board" +msgstr "" + +msgid "CycleAnalyticsEvent|Issue first mentioned in a commit" +msgstr "" + +msgid "CycleAnalyticsEvent|Merge request created" +msgstr "" + +msgid "CycleAnalyticsEvent|Merge request first deployed to production" +msgstr "" + +msgid "CycleAnalyticsEvent|Merge request last build finish time" +msgstr "" + +msgid "CycleAnalyticsEvent|Merge request last build start time" +msgstr "" + +msgid "CycleAnalyticsEvent|Merge request merged" +msgstr "" + msgid "CycleAnalyticsStage|Code" msgstr "" @@ -3935,6 +3994,9 @@ msgstr "" msgid "Disable" msgstr "" +msgid "Disable email notifications" +msgstr "" + msgid "Disable for this project" msgstr "" @@ -4133,6 +4195,9 @@ msgstr "" msgid "Edit public deploy key" msgstr "" +msgid "Edit stage" +msgstr "" + msgid "Edit wiki page" msgstr "" @@ -4262,6 +4327,9 @@ msgstr "" msgid "Enable shared Runners" msgstr "" +msgid "Enable snowplow tracking" +msgstr "" + msgid "Enable two-factor authentication" msgstr "" @@ -4799,6 +4867,12 @@ msgstr "" msgid "Failed create wiki" msgstr "" +msgid "Failed to apply commands." +msgstr "" + +msgid "Failed to assign a user because no user was found." +msgstr "" + msgid "Failed to change the owner" msgstr "" @@ -4841,6 +4915,18 @@ msgstr "" msgid "Failed to load related branches" msgstr "" +msgid "Failed to mark this issue as a duplicate because referenced issue was not found." +msgstr "" + +msgid "Failed to move this issue because label was not found." +msgstr "" + +msgid "Failed to move this issue because only a single label can be provided." +msgstr "" + +msgid "Failed to move this issue because target project doesn't exist." +msgstr "" + msgid "Failed to promote label due to internal error. Please contact administrators." msgstr "" @@ -5056,6 +5142,9 @@ msgstr "" msgid "For public projects, anyone can view pipelines and access job details (output logs and artifacts)" msgstr "" +msgid "Forgot your password?" +msgstr "" + msgid "Fork" msgstr "" @@ -5149,6 +5238,9 @@ msgstr "" msgid "Generate a default set of labels" msgstr "" +msgid "Generate link to chart" +msgstr "" + msgid "Generate new export" msgstr "" @@ -5206,6 +5298,12 @@ msgstr "" msgid "GitLab User" msgstr "" +msgid "GitLab is obtaining a Let's Encrypt SSL certificate for this domain. This process can take some time. Please try again later." +msgstr "" + +msgid "GitLab member or Email address" +msgstr "" + msgid "GitLab project export" msgstr "" @@ -5443,6 +5541,9 @@ msgstr "" msgid "GroupSettings|Default to Auto DevOps pipeline for all projects within this group" msgstr "" +msgid "GroupSettings|Disable email notifications" +msgstr "" + msgid "GroupSettings|Learn more about badges." msgstr "" @@ -5470,6 +5571,9 @@ msgstr "" msgid "GroupSettings|This setting will be applied to all subgroups unless overridden by a group owner. Groups that already have access to the project will continue to have access unless removed manually." msgstr "" +msgid "GroupSettings|This setting will override user notification preferences for all members of the group, subgroups, and projects." +msgstr "" + msgid "GroupSettings|cannot be disabled when the parent group \"Share with group lock\" is enabled, except by the owner of the parent group" msgstr "" @@ -5611,6 +5715,9 @@ msgstr "" msgid "Hide shared projects" msgstr "" +msgid "Hide stage" +msgstr "" + msgid "Hide value" msgid_plural "Hide values" msgstr[0] "" @@ -6410,9 +6517,15 @@ msgstr "" msgid "Last seen" msgstr "" +msgid "Last successful update" +msgstr "" + msgid "Last update" msgstr "" +msgid "Last update attempt" +msgstr "" + msgid "Last updated" msgstr "" @@ -6431,7 +6544,7 @@ msgstr "" msgid "Latest changes" msgstr "" -msgid "Latest pipeline for this branch" +msgid "Latest pipeline for the most recent commit on this branch" msgstr "" msgid "Lead" @@ -6493,6 +6606,9 @@ msgid_plural "Limited to showing %d events at most" msgstr[0] "" msgstr[1] "" +msgid "Link copied to clipboard" +msgstr "" + msgid "Linked emails (%{email_count})" msgstr "" @@ -6514,9 +6630,6 @@ msgstr "" msgid "List your Bitbucket Server repositories" msgstr "" -msgid "List your GitHub repositories" -msgstr "" - msgid "Live preview" msgstr "" @@ -6562,13 +6675,13 @@ msgstr "" msgid "Locked by %{fileLockUserName}" msgstr "" -msgid "Locked the discussion" +msgid "Locked the discussion." msgstr "" msgid "Locked to current projects" msgstr "" -msgid "Locks the discussion" +msgid "Locks the discussion." msgstr "" msgid "Logo was successfully removed." @@ -6586,13 +6699,13 @@ msgstr "" msgid "MRDiff|Show full file" msgstr "" -msgid "Made this issue confidential" +msgid "Made this issue confidential." msgstr "" msgid "Make and review changes in the browser with the Web IDE" msgstr "" -msgid "Make issue confidential." +msgid "Make issue confidential" msgstr "" msgid "Make sure you save it - you won't be able to access it again." @@ -6601,7 +6714,7 @@ msgstr "" msgid "Make sure you're logged into the account that owns the projects you'd like to import." msgstr "" -msgid "Makes this issue confidential" +msgid "Makes this issue confidential." msgstr "" msgid "Manage" @@ -6667,10 +6780,10 @@ msgstr "" msgid "March" msgstr "" -msgid "Mark as done" +msgid "Mark To Do as done" msgstr "" -msgid "Mark as duplicate failed because referenced issue was not found" +msgid "Mark as done" msgstr "" msgid "Mark as resolved" @@ -6682,9 +6795,6 @@ msgstr "" msgid "Mark this issue as a duplicate of another issue" msgstr "" -msgid "Mark to do as done" -msgstr "" - msgid "Markdown" msgstr "" @@ -6697,13 +6807,16 @@ msgstr "" msgid "Markdown is supported" msgstr "" +msgid "Marked To Do as done." +msgstr "" + msgid "Marked this %{noun} as Work In Progress." msgstr "" msgid "Marked this issue as a duplicate of %{duplicate_param}." msgstr "" -msgid "Marked to do as done." +msgid "Marks To Do as done." msgstr "" msgid "Marks this %{noun} as Work In Progress." @@ -6712,9 +6825,6 @@ msgstr "" msgid "Marks this issue as a duplicate of %{duplicate_reference}." msgstr "" -msgid "Marks to do as done." -msgstr "" - msgid "Max access level" msgstr "" @@ -7081,12 +7191,6 @@ msgstr "" msgid "Move selection up" msgstr "" -msgid "Move this issue failed because target project doesn't exists" -msgstr "" - -msgid "Move this issue failed because you need to specify only one label." -msgstr "" - msgid "Move this issue to another project." msgstr "" @@ -7308,9 +7412,15 @@ msgstr "" msgid "No Tag" msgstr "" +msgid "No active admin user found" +msgstr "" + msgid "No activities found" msgstr "" +msgid "No application_settings found" +msgstr "" + msgid "No available namespaces to fork the project." msgstr "" @@ -7380,9 +7490,6 @@ msgstr "" msgid "No milestones to show" msgstr "" -msgid "No one can merge" -msgstr "" - msgid "No other labels with such name or description" msgstr "" @@ -7554,6 +7661,9 @@ msgstr "" msgid "Notifications" msgstr "" +msgid "Notifications have been disabled by the project or group owner" +msgstr "" + msgid "Notifications off" msgstr "" @@ -7934,7 +8044,7 @@ msgstr "" msgid "PipelineSheduleIntervalPattern|Custom" msgstr "" -msgid "PipelineStatusTooltip|Commit: %{ci_status}" +msgid "PipelineStatusTooltip|Pipeline: %{ciStatus}" msgstr "" msgid "PipelineStatusTooltip|Pipeline: %{ci_status}" @@ -7958,6 +8068,9 @@ msgstr "" msgid "Pipelines for last year" msgstr "" +msgid "Pipelines for merge requests are configured. A detached pipeline runs in the context of the merge request, and not against the merged result. Learn more on the documentation for Pipelines for Merged Results." +msgstr "" + msgid "Pipelines settings for '%{project_name}' were successfully updated." msgstr "" @@ -8114,6 +8227,9 @@ msgstr "" msgid "Please add a list to your board first" msgstr "" +msgid "Please check your email (%{email}) to verify that you own this address. Didn't receive it? %{resend_link}. Wrong email address? %{update_link}." +msgstr "" + msgid "Please choose a group URL with no special characters." msgstr "" @@ -8648,6 +8764,9 @@ msgstr "" msgid "Project access must be granted explicitly to each user." msgstr "" +msgid "Project already created" +msgstr "" + msgid "Project and wiki repositories" msgstr "" @@ -9304,6 +9423,9 @@ msgstr "" msgid "Remove spent time" msgstr "" +msgid "Remove stage" +msgstr "" + msgid "Remove time estimate" msgstr "" @@ -9496,6 +9618,9 @@ msgstr "" msgid "Resend invite" msgstr "" +msgid "Resend it" +msgstr "" + msgid "Reset health check access token" msgstr "" @@ -9906,9 +10031,6 @@ msgstr "" msgid "Select branch/tag" msgstr "" -msgid "Select members to invite" -msgstr "" - msgid "Select merge moment" msgstr "" @@ -10280,6 +10402,9 @@ msgstr "" msgid "Similar issues" msgstr "" +msgid "Site ID" +msgstr "" + msgid "Size and domain settings for static websites" msgstr "" @@ -10310,6 +10435,9 @@ msgstr "" msgid "SnippetsEmptyState|They can be either public or private." msgstr "" +msgid "Snowplow" +msgstr "" + msgid "Some email servers do not support overriding the email sender name. Enable this option to include the name of the author of the issue, merge request or comment in the email body instead." msgstr "" @@ -11086,9 +11214,6 @@ msgstr "" msgid "The character highlighter helps you keep the subject line to %{titleLength} characters and wrap the body at %{bodyLength} so they are readable in git." msgstr "" -msgid "The code of a detached pipeline is tested against the source branch instead of merged results" -msgstr "" - msgid "The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request." msgstr "" @@ -11614,6 +11739,9 @@ msgstr "" msgid "This setting can be overridden in each project." msgstr "" +msgid "This setting will override user notification preferences for all project members." +msgstr "" + msgid "This setting will update the hostname that is used to generate private commit emails. %{learn_more}" msgstr "" @@ -11861,6 +11989,9 @@ msgstr "" msgid "To add the entry manually, provide the following details to the application on your phone." msgstr "" +msgid "To connect GitHub repositories, you first need to authorize GitLab to access the list of your GitHub repositories." +msgstr "" + msgid "To define internal users, first enable new users set to external" msgstr "" @@ -11879,12 +12010,6 @@ msgstr "" msgid "To help improve GitLab, we would like to periodically collect usage information. This can be changed at any time in %{settings_link_start}Settings%{link_end}. %{info_link_start}More Information%{link_end}" msgstr "" -msgid "To import GitHub repositories, you can use a %{personal_access_token_link}. When you create your Personal Access Token, you will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to import." -msgstr "" - -msgid "To import GitHub repositories, you first need to authorize GitLab to access the list of your GitHub repositories:" -msgstr "" - msgid "To import an SVN repository, check out %{svn_link}." msgstr "" @@ -12188,10 +12313,10 @@ msgstr "" msgid "Unlocked" msgstr "" -msgid "Unlocked the discussion" +msgid "Unlocked the discussion." msgstr "" -msgid "Unlocks the discussion" +msgid "Unlocks the discussion." msgstr "" msgid "Unmarked this %{noun} as Work In Progress." @@ -12269,6 +12394,9 @@ msgstr "" msgid "Update failed" msgstr "" +msgid "Update it" +msgstr "" + msgid "Update now" msgstr "" @@ -12503,6 +12631,9 @@ msgstr "" msgid "Username is available." msgstr "" +msgid "Username or email" +msgstr "" + msgid "Users" msgstr "" @@ -12805,15 +12936,9 @@ msgstr "" msgid "WikiMarkdownTip|To link to a (new) page, simply type %{link_example}" msgstr "" -msgid "WikiNewPagePlaceholder|how-to-setup" -msgstr "" - msgid "WikiNewPageTip|Tip: You can specify the full path for the new file. We will automatically create any missing directories." msgstr "" -msgid "WikiNewPageTitle|New Wiki Page" -msgstr "" - msgid "WikiPageConfirmDelete|Are you sure you want to delete this page?" msgstr "" @@ -12829,19 +12954,16 @@ msgstr "" msgid "WikiPageConflictMessage|the page" msgstr "" -msgid "WikiPageCreate|Create %{page_title}" +msgid "WikiPageCreate|Create %{pageTitle}" msgstr "" -msgid "WikiPageEdit|Update %{page_title}" -msgstr "" - -msgid "WikiPage|Page slug" +msgid "WikiPageEdit|Update %{pageTitle}" msgstr "" msgid "WikiPage|Write your content or drag files here…" msgstr "" -msgid "Wiki|Create Page" +msgid "Wiki|Create New Page" msgstr "" msgid "Wiki|Create page" @@ -12862,6 +12984,9 @@ msgstr "" msgid "Wiki|Page history" msgstr "" +msgid "Wiki|Page title" +msgstr "" + msgid "Wiki|Page version" msgstr "" @@ -13318,18 +13443,15 @@ msgstr "" msgid "Your request for access has been queued for review." msgstr "" -msgid "a Zoom call was added to this issue" -msgstr "" - -msgid "a Zoom call was removed from this issue" -msgstr "" - msgid "a deleted user" msgstr "" msgid "added %{created_at_timeago}" msgstr "" +msgid "added a Zoom call to this issue" +msgstr "" + msgid "ago" msgstr "" @@ -13369,6 +13491,9 @@ msgstr "" msgid "cannot include leading slash or directory traversal." msgstr "" +msgid "cannot merge" +msgstr "" + msgid "comment" msgstr "" @@ -13416,6 +13541,9 @@ msgstr "" msgid "done" msgstr "" +msgid "e.g. %{token}" +msgstr "" + msgid "element is not a hierarchy" msgstr "" @@ -13545,6 +13673,12 @@ msgstr "" msgid "manual" msgstr "" +msgid "math|The math in this entry is taking too long to render and may not be displayed as expected. For performance reasons, math blocks are also limited to %{maxChars} characters. Consider splitting up large formulae, splitting math blocks among multiple entries, or using an image instead." +msgstr "" + +msgid "math|There was an error rendering this math block" +msgstr "" + msgid "merge request" msgid_plural "merge requests" msgstr[0] "" @@ -13796,6 +13930,9 @@ msgstr "" msgid "no contributions" msgstr "" +msgid "no one can merge" +msgstr "" + msgid "none" msgstr "" @@ -13833,6 +13970,9 @@ msgstr "" msgid "pending comment" msgstr "" +msgid "pipeline" +msgstr "" + msgid "private" msgstr "" @@ -13848,6 +13988,12 @@ msgstr "" msgid "project avatar" msgstr "" +msgid "prometheus.enable is not present in gitlab.yml" +msgstr "" + +msgid "prometheus.listen_address is not present in gitlab.yml" +msgstr "" + msgid "quick actions" msgstr "" @@ -13866,6 +14012,9 @@ msgstr "" msgid "remove due date" msgstr "" +msgid "removed a Zoom call from this issue" +msgstr "" + msgid "rendered diff" msgstr "" diff --git a/package.json b/package.json index 803aebcb5fd..e076f9e6664 100644 --- a/package.json +++ b/package.json @@ -26,20 +26,20 @@ "stylelint-create-utility-map": "node scripts/frontend/stylelint/stylelint-utility-map.js", "test": "node scripts/frontend/test", "webpack": "NODE_OPTIONS=\"--max-old-space-size=3584\" webpack --config config/webpack.config.js", - "webpack-prod": "NODE_OPTIONS=\"--max-old-space-size=3584\" NODE_ENV=production webpack --config config/webpack.config.js", - "webpack-vrt": "NODE_OPTIONS=\"--max-old-space-size=3584\" NODE_ENV=production webpack --config config/webpack.config.review_toolbar.js" + "webpack-prod": "NODE_OPTIONS=\"--max-old-space-size=3584\" NODE_ENV=production webpack --config config/webpack.config.js" }, "dependencies": { - "@babel/core": "^7.4.4", - "@babel/plugin-proposal-class-properties": "^7.4.4", + "@babel/core": "^7.5.5", + "@babel/plugin-proposal-class-properties": "^7.5.5", "@babel/plugin-proposal-json-strings": "^7.2.0", "@babel/plugin-proposal-private-methods": "^7.4.4", "@babel/plugin-syntax-dynamic-import": "^7.2.0", "@babel/plugin-syntax-import-meta": "^7.2.0", - "@babel/preset-env": "^7.4.4", + "@babel/preset-env": "^7.5.5", "@gitlab/csslab": "^1.9.0", - "@gitlab/svgs": "^1.67.0", - "@gitlab/ui": "5.15.0", + "@gitlab/svgs": "^1.70.0", + "@gitlab/ui": "5.18.0", + "@gitlab/visual-review-tools": "^1.0.0", "apollo-cache-inmemory": "^1.5.1", "apollo-client": "^2.5.1", "apollo-link": "^1.2.11", @@ -145,7 +145,7 @@ "xterm": "^3.5.0" }, "devDependencies": { - "@babel/plugin-transform-modules-commonjs": "^7.2.0", + "@babel/plugin-transform-modules-commonjs": "^7.5.0", "@gitlab/eslint-config": "^1.6.0", "@gitlab/eslint-plugin-i18n": "^1.1.0", "@gitlab/eslint-plugin-vue-i18n": "^1.2.0", diff --git a/qa/Dockerfile b/qa/Dockerfile index 74be373b8e8..3309f5b6ce3 100644 --- a/qa/Dockerfile +++ b/qa/Dockerfile @@ -47,9 +47,13 @@ RUN export CLOUD_SDK_REPO="cloud-sdk-$(lsb_release -c -s)" && \ curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add - && \ apt-get update -y && apt-get install google-cloud-sdk kubectl -y -WORKDIR /home/qa -COPY ./Gemfile* ./ -RUN bundle install -COPY ./ ./ +WORKDIR /home/gitlab/qa +COPY ./qa/Gemfile* /home/gitlab/qa/ +COPY ./config/initializers/0_inject_enterprise_edition_module.rb /home/gitlab/config/initializers/ +COPY ./lib/gitlab.rb /home/gitlab/lib/ +COPY ./INSTALLATION_TYPE /home/gitlab/ +COPY ./VERSION /home/gitlab/ +RUN cd /home/gitlab/qa/ && bundle install +COPY ./qa /home/gitlab/qa ENTRYPOINT ["bin/test"] diff --git a/qa/Gemfile b/qa/Gemfile index 6abc0d622ad..f04ecb13879 100644 --- a/qa/Gemfile +++ b/qa/Gemfile @@ -1,6 +1,7 @@ source 'https://rubygems.org' gem 'gitlab-qa' +gem 'activesupport', '5.2.3' # This should stay in sync with the root's Gemfile gem 'pry-byebug', '~> 3.5.1', platform: :mri gem 'capybara', '~> 2.16.1' gem 'capybara-screenshot', '~> 1.0.18' diff --git a/qa/Gemfile.lock b/qa/Gemfile.lock index bf051a115b5..d582d77c5cd 100644 --- a/qa/Gemfile.lock +++ b/qa/Gemfile.lock @@ -1,9 +1,9 @@ GEM remote: https://rubygems.org/ specs: - activesupport (5.1.4) + activesupport (5.2.3) concurrent-ruby (~> 1.0, >= 1.0.2) - i18n (~> 0.7) + i18n (>= 0.7, < 2) minitest (~> 5.1) tzinfo (~> 1.1) addressable (2.5.2) @@ -28,7 +28,7 @@ GEM childprocess (0.9.0) ffi (~> 1.0, >= 1.0.11) coderay (1.1.2) - concurrent-ruby (1.0.5) + concurrent-ruby (1.1.5) diff-lcs (1.3) domain_name (0.5.20170404) unf (>= 0.0.5, < 1.0.0) @@ -38,7 +38,7 @@ GEM gitlab-qa (4.0.0) http-cookie (1.0.3) domain_name (~> 0.5) - i18n (0.9.1) + i18n (1.6.0) concurrent-ruby (~> 1.0) knapsack (1.17.1) rake @@ -50,7 +50,7 @@ GEM mime-types-data (3.2016.0521) mini_mime (1.0.0) mini_portile2 (2.4.0) - minitest (5.11.1) + minitest (5.11.3) netrc (0.11.0) nokogiri (1.10.4) mini_portile2 (~> 2.4.0) @@ -94,7 +94,7 @@ GEM childprocess (~> 0.5) rubyzip (~> 1.2, >= 1.2.2) thread_safe (0.3.6) - tzinfo (1.2.4) + tzinfo (1.2.5) thread_safe (~> 0.1) unf (0.1.4) unf_ext @@ -106,6 +106,7 @@ PLATFORMS ruby DEPENDENCIES + activesupport (= 5.2.3) airborne (~> 0.2.13) capybara (~> 2.16.1) capybara-screenshot (~> 1.0.18) diff --git a/qa/README.md b/qa/README.md index bab19665dac..16e5e730a6b 100644 --- a/qa/README.md +++ b/qa/README.md @@ -30,7 +30,7 @@ and corresponding views / partials / selectors in CE / EE. Whenever `qa:selectors` job fails in your merge request, you are supposed to fix [page objects](../doc/development/testing_guide/end_to_end/page_objects.md). You should also trigger end-to-end tests -using `package-and-qa` manual action, to test if everything works fine. +using `package-and-qa-manual` manual action, to test if everything works fine. ## How can I use it? @@ -39,6 +39,11 @@ have an instance available you can follow the instructions below to use the [GitLab Development Kit (GDK)][GDK]. This is the recommended option if you would like to contribute to the tests. +Note: GitLab QA uses [Selenium WebDriver](https://www.seleniumhq.org/) via +[Cabybara](http://teamcapybara.github.io/capybara/), and by default it targets Chrome as +the browser to use. You will need to have Chrome (or Chromium) and +[chromedriver](https://chromedriver.chromium.org/) installed / in your `$PATH`. + ### Run the end-to-end tests in a local development environment Follow the GDK instructions to [prepare](https://gitlab.com/gitlab-org/gitlab-development-kit/blob/master/doc/prepare.md) @@ -123,10 +128,11 @@ To set multiple cookies, separate them with the `;` character, for example: `QA_ Once you have made changes to the CE/EE repositories, you may want to build a Docker image to test locally instead of waiting for the `gitlab-ce-qa` or -`gitlab-ee-qa` nightly builds. To do that, you can run from this directory: +`gitlab-ee-qa` nightly builds. To do that, you can run **from the top `gitlab` +directory** (one level up from this directory): ```sh -docker build -t gitlab/gitlab-ce-qa:nightly . +docker build -t gitlab/gitlab-ce-qa:nightly --file ./qa/Dockerfile ./ ``` [GDK]: https://gitlab.com/gitlab-org/gitlab-development-kit/ @@ -4,6 +4,9 @@ $: << File.expand_path(File.dirname(__FILE__)) Encoding.default_external = 'UTF-8' +require_relative '../lib/gitlab' +require_relative '../config/initializers/0_inject_enterprise_edition_module' + module QA ## # GitLab QA runtime classes, mostly singletons. diff --git a/qa/qa/page/main/login.rb b/qa/qa/page/main/login.rb index c2b0482d789..94245bbfcba 100644 --- a/qa/qa/page/main/login.rb +++ b/qa/qa/page/main/login.rb @@ -52,13 +52,11 @@ module QA raise NotImplementedError if Runtime::User.ldap_user? && user&.credentials_given? if Runtime::User.ldap_user? - sign_in_using_ldap_credentials + sign_in_using_ldap_credentials(user || Runtime::User) else sign_in_using_gitlab_credentials(user || Runtime::User) end end - - Page::Main::Menu.perform(&:has_personal_area?) end def sign_in_using_admin_credentials @@ -76,6 +74,25 @@ module QA Page::Main::Menu.perform(&:has_personal_area?) end + def sign_in_using_ldap_credentials(user) + # Log out if already logged in + Page::Main::Menu.perform do |menu| + menu.sign_out if menu.has_personal_area?(wait: 0) + end + + using_wait_time 0 do + set_initial_password_if_present + + switch_to_ldap_tab + + fill_element :username_field, user.ldap_username + fill_element :password_field, user.ldap_password + click_element :sign_in_button + end + + Page::Main::Menu.perform(&:has_personal_area?) + end + def self.path '/users/sign_in' end @@ -133,14 +150,6 @@ module QA private - def sign_in_using_ldap_credentials - switch_to_ldap_tab - - fill_element :username_field, Runtime::User.ldap_username - fill_element :password_field, Runtime::User.ldap_password - click_element :sign_in_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/project/import/github.rb b/qa/qa/page/project/import/github.rb index 5973a5a958e..cc0c4e1e835 100644 --- a/qa/qa/page/project/import/github.rb +++ b/qa/qa/page/project/import/github.rb @@ -9,7 +9,7 @@ module QA view 'app/views/import/github/new.html.haml' do element :personal_access_token_field, 'text_field_tag :personal_access_token' # rubocop:disable QA/ElementWithPattern - element :list_repos_button, "submit_tag _('List your GitHub repositories')" # rubocop:disable QA/ElementWithPattern + element :authenticate_button, "submit_tag _('Authenticate')" # rubocop:disable QA/ElementWithPattern end view 'app/assets/javascripts/import_projects/components/provider_repo_table_row.vue' do diff --git a/qa/qa/page/project/issue/new.rb b/qa/qa/page/project/issue/new.rb index 0d138417176..65c02801d67 100644 --- a/qa/qa/page/project/issue/new.rb +++ b/qa/qa/page/project/issue/new.rb @@ -6,7 +6,7 @@ module QA module Issue class New < Page::Base view 'app/views/shared/issuable/_form.html.haml' do - element :submit_issue_button, 'form.submit "Submit' # rubocop:disable QA/ElementWithPattern + element :issuable_create_button end view 'app/views/shared/issuable/form/_title.html.haml' do @@ -26,7 +26,7 @@ module QA end def create_new_issue - click_on 'Submit issue' + click_element :issuable_create_button, Page::Project::Issue::Show end end end diff --git a/qa/qa/page/project/issue/show.rb b/qa/qa/page/project/issue/show.rb index 507dccb52d0..45dad9bc0ae 100644 --- a/qa/qa/page/project/issue/show.rb +++ b/qa/qa/page/project/issue/show.rb @@ -14,7 +14,7 @@ module QA end view 'app/assets/javascripts/notes/components/discussion_filter.vue' do - element :discussion_filter + element :discussion_filter, required: true element :filter_options end diff --git a/qa/qa/page/project/menu.rb b/qa/qa/page/project/menu.rb index 3fe048f752a..838d59b59cb 100644 --- a/qa/qa/page/project/menu.rb +++ b/qa/qa/page/project/menu.rb @@ -5,7 +5,7 @@ module QA module Project class Menu < Page::Base include SubMenus::Common - + include SubMenus::Project include SubMenus::CiCd include SubMenus::Issues include SubMenus::Operations diff --git a/qa/qa/page/project/web_ide/edit.rb b/qa/qa/page/project/web_ide/edit.rb index b5a36862389..37bca97fec7 100644 --- a/qa/qa/page/project/web_ide/edit.rb +++ b/qa/qa/page/project/web_ide/edit.rb @@ -39,6 +39,10 @@ module QA element :commit_button end + view 'app/assets/javascripts/ide/components/commit_sidebar/new_merge_request_option.vue' do + element :start_new_mr_checkbox + end + def has_file?(file_name) within_element(:file_list) do page.has_content? file_name @@ -100,6 +104,7 @@ module QA # animation is still in process even when the buttons have the # expected visibility. commit_success_msg_shown = retry_until do + uncheck_element :start_new_mr_checkbox click_element :commit_button wait(reload: false) do diff --git a/qa/qa/resource/issue.rb b/qa/qa/resource/issue.rb index 51b2af8b4ef..16ab59352f3 100644 --- a/qa/qa/resource/issue.rb +++ b/qa/qa/resource/issue.rb @@ -3,7 +3,7 @@ module QA module Resource class Issue < Base - attr_writer :description + attr_writer :description, :milestone attribute :project do Project.fabricate! do |resource| @@ -44,7 +44,9 @@ module QA { labels: labels, title: title - } + }.tap do |hash| + hash[:milestone_id] = @milestone.id if @milestone + end end end end diff --git a/qa/qa/resource/label.rb b/qa/qa/resource/label.rb index 3750725c440..b5e88d8aefc 100644 --- a/qa/qa/resource/label.rb +++ b/qa/qa/resource/label.rb @@ -7,6 +7,7 @@ module QA class Label < Base attr_accessor :description, :color + attribute :id attribute :title attribute :project do diff --git a/qa/qa/resource/merge_request.rb b/qa/qa/resource/merge_request.rb index 7969de726e4..45ab2396a04 100644 --- a/qa/qa/resource/merge_request.rb +++ b/qa/qa/resource/merge_request.rb @@ -5,7 +5,8 @@ require 'securerandom' module QA module Resource class MergeRequest < Base - attr_accessor :title, + attr_accessor :id, + :title, :description, :source_branch, :target_branch, @@ -74,6 +75,28 @@ module QA page.create_merge_request end end + + def fabricate_via_api! + populate(:target, :source) + super + end + + def api_get_path + "/projects/#{project.id}/merge_requests/#{id}" + end + + def api_post_path + "/projects/#{project.id}/merge_requests" + end + + def api_post_body + { + description: @description, + source_branch: @source_branch, + target_branch: @target_branch, + title: @title + } + end end end end diff --git a/qa/qa/runtime/api/request.rb b/qa/qa/runtime/api/request.rb index 310c1dfeeb4..724b499d32f 100644 --- a/qa/qa/runtime/api/request.rb +++ b/qa/qa/runtime/api/request.rb @@ -12,6 +12,10 @@ module QA @session_address = Runtime::Address.new(api_client.address, request_path) end + def mask_url + @session_address.address.sub(/private_token=.*/, "private_token=[****]") + end + def url @session_address.address end diff --git a/qa/qa/runtime/fixtures.rb b/qa/qa/runtime/fixtures.rb index 72004d5b00a..02cecffd4df 100644 --- a/qa/qa/runtime/fixtures.rb +++ b/qa/qa/runtime/fixtures.rb @@ -3,10 +3,19 @@ module QA module Runtime module Fixtures + include Support::Api + + TemplateNotFoundError = Class.new(RuntimeError) + def fetch_template_from_api(api_path, key) request = Runtime::API::Request.new(api_client, "/templates/#{api_path}/#{key}") - get request.url - json_body[:content] + response = get(request.url) + + unless response.code == HTTP_STATUS_OK + raise TemplateNotFoundError, "Template at #{request.mask_url} could not be found (#{response.code}): `#{response}`." + end + + parse_body(response)[:content] end private diff --git a/qa/qa/scenario/test/sanity/selectors.rb b/qa/qa/scenario/test/sanity/selectors.rb index b4d70fc191a..632a0f5f2a9 100644 --- a/qa/qa/scenario/test/sanity/selectors.rb +++ b/qa/qa/scenario/test/sanity/selectors.rb @@ -7,11 +7,13 @@ module QA class Selectors < Scenario::Template include Scenario::Bootable - PAGES = [QA::Page].freeze + def pages + @pages ||= [QA::Page] + end def perform(*) - validators = PAGES.map do |pages| - Page::Validator.new(pages) + validators = pages.map do |page| + Page::Validator.new(page) end validators.flat_map(&:errors).tap do |errors| diff --git a/qa/qa/service/omnibus.rb b/qa/qa/service/omnibus.rb index b54fd5628f2..c5cddff56cd 100644 --- a/qa/qa/service/omnibus.rb +++ b/qa/qa/service/omnibus.rb @@ -11,11 +11,12 @@ module QA end def gitlab_ctl(command, input: nil) - if input.nil? - shell "docker exec #{@name} gitlab-ctl #{command}" - else - shell "docker exec #{@name} bash -c '#{input} | gitlab-ctl #{command}'" - end + docker_exec("gitlab-ctl #{command}", input: input) + end + + def docker_exec(command, input: nil) + command = "#{input} | #{command}" if input + shell "docker exec #{@name} bash -c '#{command}'" end end end diff --git a/qa/qa/specs/features/browser_ui/1_manage/project/add_project_member_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/project/add_project_member_spec.rb index f51c16f472c..1c1f552e224 100644 --- a/qa/qa/specs/features/browser_ui/1_manage/project/add_project_member_spec.rb +++ b/qa/qa/specs/features/browser_ui/1_manage/project/add_project_member_spec.rb @@ -19,7 +19,7 @@ module QA page.add_member(user.username) end - expect(page).to have_content(/#{user.name} (. )?@#{user.username} Given access/) + expect(page).to have_content(/@#{user.username}(\n| )?Given access/) end end end diff --git a/qa/qa/specs/features/browser_ui/2_plan/issue/filter_issue_comments_spec.rb b/qa/qa/specs/features/browser_ui/2_plan/issue/filter_issue_comments_spec.rb index 317e31feea8..cdb85902758 100644 --- a/qa/qa/specs/features/browser_ui/2_plan/issue/filter_issue_comments_spec.rb +++ b/qa/qa/specs/features/browser_ui/2_plan/issue/filter_issue_comments_spec.rb @@ -1,7 +1,8 @@ # frozen_string_literal: true module QA - context 'Plan' do + # Failure issue https://gitlab.com/gitlab-org/quality/staging/issues/68 + context 'Plan', :quarantine do describe 'filter issue comments activities' do let(:issue_title) { 'issue title' } diff --git a/qa/qa/specs/features/browser_ui/3_create/merge_request/view_merge_request_diff_patch_spec.rb b/qa/qa/specs/features/browser_ui/3_create/merge_request/view_merge_request_diff_patch_spec.rb index 9e48ee7ca2a..891cef6c420 100644 --- a/qa/qa/specs/features/browser_ui/3_create/merge_request/view_merge_request_diff_patch_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/merge_request/view_merge_request_diff_patch_spec.rb @@ -7,13 +7,18 @@ module QA Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.perform(&:sign_in_using_credentials) - @merge_request = Resource::MergeRequest.fabricate! do |merge_request| + project = Resource::Project.fabricate_via_api! do |project| + project.name = 'project' + end + + @merge_request = Resource::MergeRequest.fabricate_via_api! do |merge_request| + merge_request.project = project merge_request.title = 'This is a merge request' - merge_request.description = 'For downloading patches and diffs' + merge_request.description = '... for downloading patches and diffs' end end - it 'user views merge request email patches' do + it 'views the merge request email patches' do @merge_request.visit! Page::MergeRequest::Show.perform(&:view_email_patches) @@ -22,7 +27,7 @@ module QA expect(page).to have_content('diff --git a/added_file.txt b/added_file.txt') end - it 'user views merge request plain diff' do + it 'views the merge request plain diff' do @merge_request.visit! Page::MergeRequest::Show.perform(&:view_plain_diff) diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/add_file_template_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/add_file_template_spec.rb index 567c6a83ddf..458072b1507 100644 --- a/qa/qa/specs/features/browser_ui/3_create/repository/add_file_template_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/repository/add_file_template_spec.rb @@ -1,7 +1,8 @@ # frozen_string_literal: true module QA - context 'Create' do + # Failure issue: https://gitlab.com/gitlab-org/quality/nightly/issues/127 + context 'Create', :quarantine do describe 'File templates' do include Runtime::Fixtures diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/user_views_commit_diff_patch_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/user_views_commit_diff_patch_spec.rb index f915d412bf3..21785ca3ed3 100644 --- a/qa/qa/specs/features/browser_ui/3_create/repository/user_views_commit_diff_patch_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/repository/user_views_commit_diff_patch_spec.rb @@ -49,7 +49,7 @@ module QA Page::Project::Commit::Show.perform(&:select_email_patches) - expect(page).to have_content("From: #{@user.name} <#{@user.public_email}>") + expect(page).to have_content(/From: "?#{Regexp.escape(@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/web_ide/add_file_template_spec.rb b/qa/qa/specs/features/browser_ui/3_create/web_ide/add_file_template_spec.rb index c09c65a57a5..9ff7919f199 100644 --- a/qa/qa/specs/features/browser_ui/3_create/web_ide/add_file_template_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/web_ide/add_file_template_spec.rb @@ -1,7 +1,8 @@ # frozen_string_literal: true module QA - context 'Create' do + context 'Create', :quarantine do + # Failure issue: https://gitlab.com/gitlab-org/quality/nightly/issues/127 describe 'Web IDE file templates' do include Runtime::Fixtures diff --git a/qa/qa/specs/features/browser_ui/6_release/deploy_key/clone_using_deploy_key_spec.rb b/qa/qa/specs/features/browser_ui/6_release/deploy_key/clone_using_deploy_key_spec.rb index 2fe4e4d9d1f..f6411d8c5ad 100644 --- a/qa/qa/specs/features/browser_ui/6_release/deploy_key/clone_using_deploy_key_spec.rb +++ b/qa/qa/specs/features/browser_ui/6_release/deploy_key/clone_using_deploy_key_spec.rb @@ -3,36 +3,29 @@ require 'digest/sha1' module QA - # Failure issue: https://gitlab.com/gitlab-org/quality/nightly/issues/70 - context 'Release', :docker, :quarantine do + context 'Release', :docker do describe 'Git clone using a deploy key' do - def login + before do Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.perform(&:sign_in_using_credentials) - end - - before(:all) do - login @runner_name = "qa-runner-#{Time.now.to_i}" - @project = Resource::Project.fabricate! do |resource| + @project = Resource::Project.fabricate_via_api! do |resource| resource.name = 'deploy-key-clone-project' end @repository_location = @project.repository_ssh_location - Resource::Runner.fabricate! do |resource| + Resource::Runner.fabricate_via_browser_ui! do |resource| resource.project = @project resource.name = @runner_name resource.tags = %w[qa docker] resource.image = 'gitlab/gitlab-runner:ubuntu' end - - Page::Main::Menu.perform(&:sign_out) end - after(:all) do + after do Service::Runner.new(@runner_name).remove! end @@ -46,9 +39,7 @@ module QA it "user sets up a deploy key with #{key_class}(#{bits}) to clone code using pipelines" do key = key_class.new(*bits) - login - - Resource::DeployKey.fabricate! do |resource| + Resource::DeployKey.fabricate_via_browser_ui! do |resource| resource.project = @project resource.title = "deploy key #{key.name}(#{key.bits})" resource.key = key.public_key @@ -56,7 +47,7 @@ module QA deploy_key_name = "DEPLOY_KEY_#{key.name}_#{key.bits}" - Resource::CiVariable.fabricate! do |resource| + Resource::CiVariable.fabricate_via_browser_ui! do |resource| resource.project = @project resource.key = deploy_key_name resource.value = key.private_key diff --git a/qa/qa/vendor/saml_idp/page/login.rb b/qa/qa/vendor/saml_idp/page/login.rb index 9c1f9904a7a..1b8c926532a 100644 --- a/qa/qa/vendor/saml_idp/page/login.rb +++ b/qa/qa/vendor/saml_idp/page/login.rb @@ -12,6 +12,14 @@ module QA fill_in 'password', with: 'user1pass' click_on 'Login' end + + def login_if_required + login if login_required? + end + + def login_required? + page.has_text?('Enter your username and password') + end end end end diff --git a/qa/spec/resource/repository/push_spec.rb b/qa/spec/resource/repository/push_spec.rb index bf3ebce0cfe..2f9e4958ae1 100644 --- a/qa/spec/resource/repository/push_spec.rb +++ b/qa/spec/resource/repository/push_spec.rb @@ -19,7 +19,11 @@ describe QA::Resource::Repository::Push do expect { subject.files = [] }.to raise_error(ArgumentError) end - it 'does not raise if files is an array' do + it 'raises an error if files is not an array of hashes with :name and :content keys' do + expect { subject.files = [{ foo: 'foo' }] }.to raise_error(ArgumentError) + end + + it 'does not raise if files is an array of hashes with :name and :content keys' do expect { subject.files = files }.not_to raise_error end end diff --git a/qa/spec/runtime/env_spec.rb b/qa/spec/runtime/env_spec.rb index caf96a213e1..340831aa06d 100644 --- a/qa/spec/runtime/env_spec.rb +++ b/qa/spec/runtime/env_spec.rb @@ -192,6 +192,30 @@ describe QA::Runtime::Env do end end + describe '.knapsack?' do + it 'returns true if KNAPSACK_GENERATE_REPORT is defined' do + stub_env('KNAPSACK_GENERATE_REPORT', 'true') + + expect(described_class.knapsack?).to be_truthy + end + + it 'returns true if KNAPSACK_REPORT_PATH is defined' do + stub_env('KNAPSACK_REPORT_PATH', '/a/path') + + expect(described_class.knapsack?).to be_truthy + end + + it 'returns true if KNAPSACK_TEST_FILE_PATTERN is defined' do + stub_env('KNAPSACK_TEST_FILE_PATTERN', '/a/**/pattern') + + expect(described_class.knapsack?).to be_truthy + end + + it 'returns false if neither KNAPSACK_GENERATE_REPORT nor KNAPSACK_REPORT_PATH nor KNAPSACK_TEST_FILE_PATTERN are defined' do + expect(described_class.knapsack?).to be_falsey + end + end + describe '.require_github_access_token!' do it 'raises ArgumentError if GITHUB_ACCESS_TOKEN is not defined' do stub_env('GITHUB_ACCESS_TOKEN', nil) diff --git a/rubocop/cop/migration/add_limit_to_string_columns.rb b/rubocop/cop/migration/add_limit_to_string_columns.rb new file mode 100644 index 00000000000..30affcbb089 --- /dev/null +++ b/rubocop/cop/migration/add_limit_to_string_columns.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +require_relative '../../migration_helpers' + +module RuboCop + module Cop + module Migration + # Cop that enforces length constraints to string columns + class AddLimitToStringColumns < RuboCop::Cop::Cop + include MigrationHelpers + + ADD_COLUMNS_METHODS = %i(add_column add_column_with_default).freeze + + MSG = 'String columns should have a limit constraint. 255 is suggested'.freeze + + def on_def(node) + return unless in_migration?(node) + + node.each_descendant(:send) do |send_node| + next unless string_operation?(send_node) + + add_offense(send_node, location: :selector) unless limit_on_string_column?(send_node) + end + end + + private + + def string_operation?(node) + modifier = node.children[0] + migration_method = node.children[1] + + if migration_method == :string + modifier.type == :lvar + elsif ADD_COLUMNS_METHODS.include?(migration_method) + modifier.nil? && string_column?(node.children[4]) + end + end + + def string_column?(column_type) + column_type.type == :sym && column_type.value == :string + end + + def limit_on_string_column?(node) + migration_method = node.children[1] + + if migration_method == :string + limit_present?(node.children) + elsif ADD_COLUMNS_METHODS.include?(migration_method) + limit_present?(node) + end + end + + def limit_present?(statement) + !(statement.to_s =~ /:limit/).nil? + end + end + end + end +end diff --git a/rubocop/rubocop.rb b/rubocop/rubocop.rb index 58a7ead6f13..d1328c4eb38 100644 --- a/rubocop/rubocop.rb +++ b/rubocop/rubocop.rb @@ -17,6 +17,7 @@ require_relative 'cop/migration/add_column' require_relative 'cop/migration/add_concurrent_foreign_key' require_relative 'cop/migration/add_concurrent_index' require_relative 'cop/migration/add_index' +require_relative 'cop/migration/add_limit_to_string_columns' require_relative 'cop/migration/add_reference' require_relative 'cop/migration/add_timestamps' require_relative 'cop/migration/datetime' diff --git a/spec/bin/changelog_spec.rb b/spec/bin/changelog_spec.rb index 84bbe0525e5..c3a5abf2b7a 100644 --- a/spec/bin/changelog_spec.rb +++ b/spec/bin/changelog_spec.rb @@ -15,7 +15,7 @@ describe 'bin/changelog' do allow(entry).to receive(:branch_name).and_return('long-branch-' * 100) file_path = entry.send(:file_path) - expect(file_path.length).to eq(140) + expect(file_path.length).to eq(99) end end diff --git a/spec/config/smime_signature_settings_spec.rb b/spec/config/smime_signature_settings_spec.rb new file mode 100644 index 00000000000..4f0c227d866 --- /dev/null +++ b/spec/config/smime_signature_settings_spec.rb @@ -0,0 +1,56 @@ +require 'fast_spec_helper' + +describe SmimeSignatureSettings do + describe '.parse' do + let(:default_smime_key) { Rails.root.join('.gitlab_smime_key') } + let(:default_smime_cert) { Rails.root.join('.gitlab_smime_cert') } + + it 'sets correct default values to disabled' do + parsed_settings = described_class.parse(nil) + + expect(parsed_settings['enabled']).to be(false) + expect(parsed_settings['key_file']).to eq(default_smime_key) + expect(parsed_settings['cert_file']).to eq(default_smime_cert) + end + + context 'when providing custom values' do + it 'sets correct default values to disabled' do + custom_settings = Settingslogic.new({}) + + parsed_settings = described_class.parse(custom_settings) + + expect(parsed_settings['enabled']).to be(false) + expect(parsed_settings['key_file']).to eq(default_smime_key) + expect(parsed_settings['cert_file']).to eq(default_smime_cert) + end + + it 'enables smime with default key and cert' do + custom_settings = Settingslogic.new({ + 'enabled' => true + }) + + parsed_settings = described_class.parse(custom_settings) + + expect(parsed_settings['enabled']).to be(true) + expect(parsed_settings['key_file']).to eq(default_smime_key) + expect(parsed_settings['cert_file']).to eq(default_smime_cert) + end + + it 'enables smime with custom key and cert' do + custom_key = '/custom/key' + custom_cert = '/custom/cert' + custom_settings = Settingslogic.new({ + 'enabled' => true, + 'key_file' => custom_key, + 'cert_file' => custom_cert + }) + + parsed_settings = described_class.parse(custom_settings) + + expect(parsed_settings['enabled']).to be(true) + expect(parsed_settings['key_file']).to eq(custom_key) + expect(parsed_settings['cert_file']).to eq(custom_cert) + end + end + end +end diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb index 84bbbac39b0..0b3833e6515 100644 --- a/spec/controllers/application_controller_spec.rb +++ b/spec/controllers/application_controller_spec.rb @@ -641,24 +641,32 @@ describe ApplicationController do end end - it 'does not set a custom header' do + it 'sets a custom header' do get :index, format: :json - expect(response.headers['X-GitLab-Custom-Error']).to be_nil + expect(response.headers['X-GitLab-Custom-Error']).to eq '1' end - end - context 'given a json response for an html request' do - controller do - def index - render json: {}, status: :unprocessable_entity + context 'for html request' do + it 'sets a custom header' do + get :index + + expect(response.headers['X-GitLab-Custom-Error']).to eq '1' end end - it 'does not set a custom header' do - get :index + context 'for 200 response' do + controller do + def index + render json: {}, status: :ok + end + end - expect(response.headers['X-GitLab-Custom-Error']).to be_nil + it 'does not set a custom header' do + get :index, format: :json + + expect(response.headers['X-GitLab-Custom-Error']).to be_nil + end end end end diff --git a/spec/controllers/concerns/confirm_email_warning_spec.rb b/spec/controllers/concerns/confirm_email_warning_spec.rb new file mode 100644 index 00000000000..0c598a360af --- /dev/null +++ b/spec/controllers/concerns/confirm_email_warning_spec.rb @@ -0,0 +1,98 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe ConfirmEmailWarning do + before do + stub_feature_flags(soft_email_confirmation: true) + allow(User).to receive(:allow_unconfirmed_access_for).and_return 2.days + end + + controller(ApplicationController) do + # `described_class` is not available in this context + include ConfirmEmailWarning # rubocop:disable RSpec/DescribedClass + + def index + head :ok + end + end + + RSpec::Matchers.define :set_confirm_warning_for do |email| + match do |response| + expect(response).to set_flash.now[:warning].to include("Please check your email (#{email}) to verify that you own this address.") + end + end + + describe 'confirm email flash warning' do + context 'when not signed in' do + let(:user) { create(:user, confirmed_at: nil) } + + before do + get :index + end + + it { is_expected.not_to set_confirm_warning_for(user.email) } + end + + context 'when signed in' do + before do + sign_in(user) + end + + context 'with a confirmed user' do + let(:user) { create(:user) } + + before do + get :index + end + + it { is_expected.not_to set_confirm_warning_for(user.email) } + end + + context 'with an unconfirmed user' do + let(:user) { create(:user, confirmed_at: nil) } + + context 'when executing a peek request' do + before do + request.path = '/-/peek' + get :index + end + + it { is_expected.not_to set_confirm_warning_for(user.email) } + end + + context 'when executing a json request' do + before do + get :index, format: :json + end + + it { is_expected.not_to set_confirm_warning_for(user.email) } + end + + context 'when executing a post request' do + before do + post :index + end + + it { is_expected.not_to set_confirm_warning_for(user.email) } + end + + context 'when executing a get request' do + before do + get :index + end + + context 'with an unconfirmed email address present' do + let(:user) { create(:user, confirmed_at: nil, unconfirmed_email: 'unconfirmed@gitlab.com') } + + it { is_expected.to set_confirm_warning_for(user.unconfirmed_email) } + end + + context 'without an unconfirmed email address present' do + it { is_expected.to set_confirm_warning_for(user.email) } + end + end + end + end + end +end diff --git a/spec/controllers/concerns/issuable_collections_spec.rb b/spec/controllers/concerns/issuable_collections_spec.rb index f210537aad5..7bdf5c49425 100644 --- a/spec/controllers/concerns/issuable_collections_spec.rb +++ b/spec/controllers/concerns/issuable_collections_spec.rb @@ -24,78 +24,6 @@ describe IssuableCollections do 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) { {} } - let(:params) { { sort: 'downvotes_asc' } } - - it 'sets the cookie with the right values and flags' do - allow(controller).to receive(:cookies).and_return(cookies) - - controller.send(:set_sort_order_from_cookie) - - expect(cookies['issue_sort']).to eq({ value: 'popularity', secure: false, httponly: false }) - end - end - - describe 'when cookie exists' do - let(:cookies) { { 'issue_sort' => 'id_asc' } } - let(:params) { {} } - - it 'sets the cookie with the right values and flags' do - allow(controller).to receive(:cookies).and_return(cookies) - - controller.send(:set_sort_order_from_cookie) - - expect(cookies['issue_sort']).to eq({ value: 'created_asc', secure: false, httponly: false }) - end - end - end - describe '#page_count_for_relation' do let(:params) { { state: 'opened' } } diff --git a/spec/controllers/concerns/sorting_preference_spec.rb b/spec/controllers/concerns/sorting_preference_spec.rb new file mode 100644 index 00000000000..a36124c6776 --- /dev/null +++ b/spec/controllers/concerns/sorting_preference_spec.rb @@ -0,0 +1,93 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe SortingPreference do + let(:user) { create(:user) } + + let(:controller_class) do + Class.new do + def self.helper_method(name); end + + include SortingPreference + include SortingHelper + end + end + + let(:controller) { controller_class.new } + + before do + allow(controller).to receive(:params).and_return(ActionController::Parameters.new(params)) + allow(controller).to receive(:current_user).and_return(user) + allow(controller).to receive(:legacy_sort_cookie_name).and_return('issuable_sort') + allow(controller).to receive(:sorting_field).and_return(:issues_sort) + end + + describe '#set_sort_order_from_user_preference' do + subject { controller.send(:set_sort_order_from_user_preference) } + + context 'when sort param given' do + let(:params) { { sort: 'updated_desc' } } + + context 'when sorting_field is defined' do + it 'sets user_preference with the right value' do + is_expected.to eq('updated_desc') + end + end + + context 'when no sorting_field is defined on the controller' do + before do + allow(controller).to receive(:sorting_field).and_return(nil) + end + + it 'does not touch user_preference' do + expect(user).not_to receive(:user_preference) + + subject + end + end + end + + context 'when a user sorting preference exists' do + let(:params) { {} } + + before do + user.user_preference.update!(issues_sort: 'updated_asc') + end + + it 'returns the set preference' do + is_expected.to eq('updated_asc') + end + end + end + + describe '#set_set_order_from_cookie' do + subject { controller.send(:set_sort_order_from_cookie) } + + before do + allow(controller).to receive(:cookies).and_return(cookies) + end + + context 'when sort param given' do + let(:cookies) { {} } + let(:params) { { sort: 'downvotes_asc' } } + + it 'sets the cookie with the right values and flags' do + subject + + expect(cookies['issue_sort']).to eq(value: 'popularity', secure: false, httponly: false) + end + end + + context 'when cookie exists' do + let(:cookies) { { 'issue_sort' => 'id_asc' } } + let(:params) { {} } + + it 'sets the cookie with the right values and flags' do + subject + + expect(cookies['issue_sort']).to eq(value: 'created_asc', secure: false, httponly: false) + end + end + end +end diff --git a/spec/controllers/dashboard/projects_controller_spec.rb b/spec/controllers/dashboard/projects_controller_spec.rb index 6591901a9dc..8b95c9f2496 100644 --- a/spec/controllers/dashboard/projects_controller_spec.rb +++ b/spec/controllers/dashboard/projects_controller_spec.rb @@ -40,6 +40,14 @@ describe Dashboard::ProjectsController do expect(assigns(:projects)).to eq([project, project2]) end + + context 'project sorting' do + let(:project) { create(:project) } + + it_behaves_like 'set sort order from user preference' do + let(:sorting_param) { 'created_asc' } + end + end end end diff --git a/spec/controllers/explore/projects_controller_spec.rb b/spec/controllers/explore/projects_controller_spec.rb index 463586ee422..6752d2b8ebd 100644 --- a/spec/controllers/explore/projects_controller_spec.rb +++ b/spec/controllers/explore/projects_controller_spec.rb @@ -3,56 +3,91 @@ require 'spec_helper' describe Explore::ProjectsController do - describe 'GET #index.json' do - render_views + shared_examples 'explore projects' do + describe 'GET #index.json' do + render_views - before do - get :index, format: :json + before do + get :index, format: :json + end + + it { is_expected.to respond_with(:success) } end - it { is_expected.to respond_with(:success) } - end + describe 'GET #trending.json' do + render_views - describe 'GET #trending.json' do - render_views + before do + get :trending, format: :json + end - before do - get :trending, format: :json + it { is_expected.to respond_with(:success) } + end + + describe 'GET #starred.json' do + render_views + + before do + get :starred, format: :json + end + + it { is_expected.to respond_with(:success) } end - it { is_expected.to respond_with(:success) } + describe 'GET #trending' do + context 'sorting by update date' do + let(:project1) { create(:project, :public, updated_at: 3.days.ago) } + let(:project2) { create(:project, :public, updated_at: 1.day.ago) } + + before do + create(:trending_project, project: project1) + create(:trending_project, project: project2) + end + + it 'sorts by last updated' do + get :trending, params: { sort: 'updated_desc' } + + expect(assigns(:projects)).to eq [project2, project1] + end + + it 'sorts by oldest updated' do + get :trending, params: { sort: 'updated_asc' } + + expect(assigns(:projects)).to eq [project1, project2] + end + end + end end - describe 'GET #starred.json' do - render_views + context 'when user is signed in' do + let(:user) { create(:user) } before do - get :starred, format: :json + sign_in(user) end - it { is_expected.to respond_with(:success) } - end + include_examples 'explore projects' - describe 'GET #trending' do - context 'sorting by update date' do - let(:project1) { create(:project, :public, updated_at: 3.days.ago) } - let(:project2) { create(:project, :public, updated_at: 1.day.ago) } + context 'user preference sorting' do + let(:project) { create(:project) } - before do - create(:trending_project, project: project1) - create(:trending_project, project: project2) + it_behaves_like 'set sort order from user preference' do + let(:sorting_param) { 'created_asc' } end + end + end - it 'sorts by last updated' do - get :trending, params: { sort: 'updated_desc' } + context 'when user is not signed in' do + include_examples 'explore projects' - expect(assigns(:projects)).to eq [project2, project1] - end + context 'user preference sorting' do + let(:project) { create(:project) } + let(:sorting_param) { 'created_asc' } - it 'sorts by oldest updated' do - get :trending, params: { sort: 'updated_asc' } + it 'does not set sort order from user preference' do + expect_any_instance_of(UserPreference).not_to receive(:update) - expect(assigns(:projects)).to eq [project1, project2] + get :index, params: { sort: sorting_param } end end end diff --git a/spec/controllers/groups/group_members_controller_spec.rb b/spec/controllers/groups/group_members_controller_spec.rb index 908c564e761..0c3dd971582 100644 --- a/spec/controllers/groups/group_members_controller_spec.rb +++ b/spec/controllers/groups/group_members_controller_spec.rb @@ -172,7 +172,7 @@ describe Groups::GroupMembersController do it '[JS] removes user from members' do delete :destroy, params: { group_id: group, id: member }, xhr: true - expect(response).to be_success + expect(response).to be_successful expect(group.members).not_to include member end end diff --git a/spec/controllers/groups/milestones_controller_spec.rb b/spec/controllers/groups/milestones_controller_spec.rb index bf164aeed38..41927907fd1 100644 --- a/spec/controllers/groups/milestones_controller_spec.rb +++ b/spec/controllers/groups/milestones_controller_spec.rb @@ -186,7 +186,7 @@ describe Groups::MilestonesController do it "removes milestone" do delete :destroy, params: { group_id: group.to_param, id: milestone.iid }, format: :js - expect(response).to be_success + expect(response).to be_successful expect { Milestone.find(milestone.id) }.to raise_exception(ActiveRecord::RecordNotFound) end end diff --git a/spec/controllers/groups/runners_controller_spec.rb b/spec/controllers/groups/runners_controller_spec.rb index 91f9e2c7832..14b0cf959b3 100644 --- a/spec/controllers/groups/runners_controller_spec.rb +++ b/spec/controllers/groups/runners_controller_spec.rb @@ -3,73 +3,202 @@ require 'spec_helper' describe Groups::RunnersController do - let(:user) { create(:user) } - let(:group) { create(:group) } + let(:user) { create(:user) } + let(:group) { create(:group) } let(:runner) { create(:ci_runner, :group, groups: [group]) } - - let(:params) do - { - group_id: group, - id: runner - } - end + let(:params) { { group_id: group, id: runner } } before do sign_in(user) - group.add_maintainer(user) + end + + describe '#show' do + context 'when user is owner' do + before do + group.add_owner(user) + end + + it 'renders show with 200 status code' do + get :show, params: { group_id: group, id: runner } + + expect(response).to have_gitlab_http_status(200) + expect(response).to render_template(:show) + end + end + + context 'when user is not owner' do + before do + group.add_maintainer(user) + end + + it 'renders a 404' do + get :show, params: { group_id: group, id: runner } + + expect(response).to have_gitlab_http_status(404) + end + end + end + + describe '#edit' do + context 'when user is owner' do + before do + group.add_owner(user) + end + + it 'renders show with 200 status code' do + get :edit, params: { group_id: group, id: runner } + + expect(response).to have_gitlab_http_status(200) + expect(response).to render_template(:edit) + end + end + + context 'when user is not owner' do + before do + group.add_maintainer(user) + end + + it 'renders a 404' do + get :edit, params: { group_id: group, id: runner } + + expect(response).to have_gitlab_http_status(404) + end + end end describe '#update' do - it 'updates the runner and ticks the queue' do - new_desc = runner.description.swapcase + context 'when user is an owner' do + before do + group.add_owner(user) + end - expect do - post :update, params: params.merge(runner: { description: new_desc } ) - end.to change { runner.ensure_runner_queue_value } + it 'updates the runner, ticks the queue, and redirects' do + new_desc = runner.description.swapcase - runner.reload + expect do + post :update, params: params.merge(runner: { description: new_desc } ) + end.to change { runner.ensure_runner_queue_value } - expect(response).to have_gitlab_http_status(302) - expect(runner.description).to eq(new_desc) + expect(response).to have_gitlab_http_status(302) + expect(runner.reload.description).to eq(new_desc) + end + end + + context 'when user is not an owner' do + before do + group.add_maintainer(user) + end + + it 'rejects the update and responds 404' do + old_desc = runner.description + + expect do + post :update, params: params.merge(runner: { description: old_desc.swapcase } ) + end.not_to change { runner.ensure_runner_queue_value } + + expect(response).to have_gitlab_http_status(404) + expect(runner.reload.description).to eq(old_desc) + end end end describe '#destroy' do - it 'destroys the runner' do - delete :destroy, params: params + context 'when user is an owner' do + before do + group.add_owner(user) + end + + it 'destroys the runner and redirects' do + delete :destroy, params: params + + expect(response).to have_gitlab_http_status(302) + expect(Ci::Runner.find_by(id: runner.id)).to be_nil + end + end + + context 'when user is not an owner' do + before do + group.add_maintainer(user) + end + + it 'responds 404 and does not destroy the runner' do + delete :destroy, params: params - expect(response).to have_gitlab_http_status(302) - expect(Ci::Runner.find_by(id: runner.id)).to be_nil + expect(response).to have_gitlab_http_status(404) + expect(Ci::Runner.find_by(id: runner.id)).to be_present + end end end describe '#resume' do - it 'marks the runner as active and ticks the queue' do - runner.update(active: false) + context 'when user is an owner' do + before do + group.add_owner(user) + end - expect do - post :resume, params: params - end.to change { runner.ensure_runner_queue_value } + it 'marks the runner as active, ticks the queue, and redirects' do + runner.update(active: false) - runner.reload + expect do + post :resume, params: params + end.to change { runner.ensure_runner_queue_value } - expect(response).to have_gitlab_http_status(302) - expect(runner.active).to eq(true) + expect(response).to have_gitlab_http_status(302) + expect(runner.reload.active).to eq(true) + end + end + + context 'when user is not an owner' do + before do + group.add_maintainer(user) + end + + it 'responds 404 and does not activate the runner' do + runner.update(active: false) + + expect do + post :resume, params: params + end.not_to change { runner.ensure_runner_queue_value } + + expect(response).to have_gitlab_http_status(404) + expect(runner.reload.active).to eq(false) + end end end describe '#pause' do - it 'marks the runner as inactive and ticks the queue' do - runner.update(active: true) + context 'when user is an owner' do + before do + group.add_owner(user) + end + + it 'marks the runner as inactive, ticks the queue, and redirects' do + runner.update(active: true) + + expect do + post :pause, params: params + end.to change { runner.ensure_runner_queue_value } + + expect(response).to have_gitlab_http_status(302) + expect(runner.reload.active).to eq(false) + end + end + + context 'when user is not an owner' do + before do + group.add_maintainer(user) + end - expect do - post :pause, params: params - end.to change { runner.ensure_runner_queue_value } + it 'responds 404 and does not update the runner or queue' do + runner.update(active: true) - runner.reload + expect do + post :pause, params: params + end.not_to change { runner.ensure_runner_queue_value } - expect(response).to have_gitlab_http_status(302) - expect(runner.active).to eq(false) + expect(response).to have_gitlab_http_status(404) + expect(runner.reload.active).to eq(true) + end end end end diff --git a/spec/controllers/health_check_controller_spec.rb b/spec/controllers/health_check_controller_spec.rb index 92f005faf4a..b48b7dc86e0 100644 --- a/spec/controllers/health_check_controller_spec.rb +++ b/spec/controllers/health_check_controller_spec.rb @@ -33,14 +33,14 @@ describe HealthCheckController do get :index - expect(response).to be_success + expect(response).to be_successful expect(response.content_type).to eq 'text/plain' end it 'supports passing the token in query params' do get :index, params: { token: token } - expect(response).to be_success + expect(response).to be_successful expect(response.content_type).to eq 'text/plain' end end @@ -54,14 +54,14 @@ describe HealthCheckController do it 'supports successful plaintext response' do get :index - expect(response).to be_success + expect(response).to be_successful expect(response.content_type).to eq 'text/plain' end it 'supports successful json response' do get :index, format: :json - expect(response).to be_success + expect(response).to be_successful expect(response.content_type).to eq 'application/json' expect(json_response['healthy']).to be true end @@ -69,7 +69,7 @@ describe HealthCheckController do it 'supports successful xml response' do get :index, format: :xml - expect(response).to be_success + expect(response).to be_successful expect(response.content_type).to eq 'application/xml' expect(xml_response['healthy']).to be true end @@ -77,7 +77,7 @@ describe HealthCheckController do it 'supports successful responses for specific checks' do get :index, params: { checks: 'email' }, format: :json - expect(response).to be_success + expect(response).to be_successful expect(response.content_type).to eq 'application/json' expect(json_response['healthy']).to be true end diff --git a/spec/controllers/help_controller_spec.rb b/spec/controllers/help_controller_spec.rb index 43c910da7a5..03b6e85b653 100644 --- a/spec/controllers/help_controller_spec.rb +++ b/spec/controllers/help_controller_spec.rb @@ -114,7 +114,7 @@ describe HelpController do path: 'user/project/img/labels_default_v12_1' }, format: :png - expect(response).to be_success + expect(response).to be_successful expect(response.content_type).to eq 'image/png' expect(response.headers['Content-Disposition']).to match(/^inline;/) end diff --git a/spec/controllers/profiles/keys_controller_spec.rb b/spec/controllers/profiles/keys_controller_spec.rb index 753eb432c5e..3bed117deb0 100644 --- a/spec/controllers/profiles/keys_controller_spec.rb +++ b/spec/controllers/profiles/keys_controller_spec.rb @@ -10,7 +10,7 @@ describe Profiles::KeysController do it "does not generally work" do get :get_keys, params: { username: 'not-existent' } - expect(response).not_to be_success + expect(response).not_to be_successful end end @@ -18,7 +18,7 @@ describe Profiles::KeysController do it "does generally work" do get :get_keys, params: { username: user.username } - expect(response).to be_success + expect(response).to be_successful end it "renders all keys separated with a new line" do @@ -41,7 +41,7 @@ describe Profiles::KeysController do it "does generally work" do get :get_keys, params: { username: user.username } - expect(response).to be_success + expect(response).to be_successful end it "renders all non deploy keys separated with a new line" do diff --git a/spec/controllers/projects/ci/lints_controller_spec.rb b/spec/controllers/projects/ci/lints_controller_spec.rb index 96e82b7086c..14128fb5b0e 100644 --- a/spec/controllers/projects/ci/lints_controller_spec.rb +++ b/spec/controllers/projects/ci/lints_controller_spec.rb @@ -20,9 +20,7 @@ describe Projects::Ci::LintsController do get :show, params: { namespace_id: project.namespace, project_id: project } end - it 'is success' do - expect(response).to be_success - end + it { expect(response).to be_successful } it 'renders show page' do expect(response).to render_template :show @@ -78,9 +76,7 @@ describe Projects::Ci::LintsController do post :create, params: { namespace_id: project.namespace, project_id: project, content: content } end - it 'is success' do - expect(response).to be_success - end + it { expect(response).to be_successful } it 'render show page' do expect(response).to render_template :show diff --git a/spec/controllers/projects/commit_controller_spec.rb b/spec/controllers/projects/commit_controller_spec.rb index 58a1d96d010..afd5cb15e0f 100644 --- a/spec/controllers/projects/commit_controller_spec.rb +++ b/spec/controllers/projects/commit_controller_spec.rb @@ -45,14 +45,14 @@ describe Projects::CommitController do it 'handles binary files' do go(id: TestEnv::BRANCH_SHA['binary-encoding'], format: 'html') - expect(response).to be_success + expect(response).to be_successful end shared_examples "export as" do |format| it "does generally work" do go(id: commit.id, format: format) - expect(response).to be_success + expect(response).to be_successful end it "generates it" do @@ -110,7 +110,7 @@ describe Projects::CommitController do id: commit.id }) - expect(response).to be_success + expect(response).to be_successful end end @@ -177,7 +177,7 @@ describe Projects::CommitController do id: commit.id }) - expect(response).not_to be_success + expect(response).not_to be_successful expect(response).to have_gitlab_http_status(404) end end @@ -234,7 +234,7 @@ describe Projects::CommitController do id: master_pickable_commit.id }) - expect(response).not_to be_success + expect(response).not_to be_successful expect(response).to have_gitlab_http_status(404) end end diff --git a/spec/controllers/projects/commits_controller_spec.rb b/spec/controllers/projects/commits_controller_spec.rb index 9db1ac2a46c..9c4d6fdcb2a 100644 --- a/spec/controllers/projects/commits_controller_spec.rb +++ b/spec/controllers/projects/commits_controller_spec.rb @@ -79,7 +79,7 @@ describe Projects::CommitsController do end it "renders as atom" do - expect(response).to be_success + expect(response).to be_successful expect(response.content_type).to eq('application/atom+xml') end @@ -104,7 +104,7 @@ describe Projects::CommitsController do end it "renders as HTML" do - expect(response).to be_success + expect(response).to be_successful expect(response.content_type).to eq('text/html') end end diff --git a/spec/controllers/projects/compare_controller_spec.rb b/spec/controllers/projects/compare_controller_spec.rb index 48a92a772dc..9afc46c4be9 100644 --- a/spec/controllers/projects/compare_controller_spec.rb +++ b/spec/controllers/projects/compare_controller_spec.rb @@ -19,7 +19,7 @@ describe Projects::CompareController do end it 'returns successfully' do - expect(response).to be_success + expect(response).to be_successful end end @@ -49,7 +49,7 @@ describe Projects::CompareController do it 'shows some diffs with ignore whitespace change option' do show_request - expect(response).to be_success + expect(response).to be_successful diff_file = assigns(:diffs).diff_files.first expect(diff_file).not_to be_nil expect(assigns(:commits).length).to be >= 1 @@ -67,7 +67,7 @@ describe Projects::CompareController do it 'sets the diffs and commits ivars' do show_request - expect(response).to be_success + expect(response).to be_successful expect(assigns(:diffs).diff_files.first).not_to be_nil expect(assigns(:commits).length).to be >= 1 end @@ -81,7 +81,7 @@ describe Projects::CompareController do it 'sets empty diff and commit ivars' do show_request - expect(response).to be_success + expect(response).to be_successful expect(assigns(:diffs)).to eq([]) expect(assigns(:commits)).to eq([]) end @@ -94,7 +94,7 @@ describe Projects::CompareController do it 'sets empty diff and commit ivars' do show_request - expect(response).to be_success + expect(response).to be_successful expect(assigns(:diffs)).to eq([]) expect(assigns(:commits)).to eq([]) end diff --git a/spec/controllers/projects/cycle_analytics/events_controller_spec.rb b/spec/controllers/projects/cycle_analytics/events_controller_spec.rb index 8fc3ae0aa32..b828c678d0c 100644 --- a/spec/controllers/projects/cycle_analytics/events_controller_spec.rb +++ b/spec/controllers/projects/cycle_analytics/events_controller_spec.rb @@ -16,7 +16,7 @@ describe Projects::CycleAnalytics::EventsController do it 'is empty' do get_issue - expect(response).to be_success + expect(response).to be_successful expect(JSON.parse(response.body)['events']).to be_empty end end @@ -32,7 +32,7 @@ describe Projects::CycleAnalytics::EventsController do it 'is not empty' do get_issue - expect(response).to be_success + expect(response).to be_successful end it 'contains event detais' do @@ -49,7 +49,7 @@ describe Projects::CycleAnalytics::EventsController do it 'is empty' do get_issue(additional_params: { cycle_analytics: { start_date: 7 } }) - expect(response).to be_success + expect(response).to be_successful expect(JSON.parse(response.body)['events']).to be_empty end diff --git a/spec/controllers/projects/cycle_analytics_controller_spec.rb b/spec/controllers/projects/cycle_analytics_controller_spec.rb index 2dc97e18113..65eee7b8ead 100644 --- a/spec/controllers/projects/cycle_analytics_controller_spec.rb +++ b/spec/controllers/projects/cycle_analytics_controller_spec.rb @@ -11,6 +11,20 @@ describe Projects::CycleAnalyticsController do project.add_maintainer(user) end + context "counting page views for 'show'" do + it 'increases the counter' do + expect(Gitlab::UsageDataCounters::CycleAnalyticsCounter).to receive(:count).with(:views) + + get(:show, + params: { + namespace_id: project.namespace, + project_id: project + }) + + expect(response).to be_successful + end + end + describe 'cycle analytics not set up flag' do context 'with no data' do it 'is true' do @@ -20,7 +34,7 @@ describe Projects::CycleAnalyticsController do project_id: project }) - expect(response).to be_success + expect(response).to be_successful expect(assigns(:cycle_analytics_no_data)).to eq(true) end end @@ -41,7 +55,7 @@ describe Projects::CycleAnalyticsController do project_id: project }) - expect(response).to be_success + expect(response).to be_successful expect(assigns(:cycle_analytics_no_data)).to eq(false) end end diff --git a/spec/controllers/projects/git_http_controller_spec.rb b/spec/controllers/projects/git_http_controller_spec.rb index bf099e8deeb..88fa2236e33 100644 --- a/spec/controllers/projects/git_http_controller_spec.rb +++ b/spec/controllers/projects/git_http_controller_spec.rb @@ -12,4 +12,15 @@ describe Projects::GitHttpController do expect(response.status).to eq(403) end end + + describe 'GET #info_refs' do + it 'returns 401 for unauthenticated requests to public repositories when http protocol is disabled' do + stub_application_setting(enabled_git_access_protocol: 'ssh') + project = create(:project, :public, :repository) + + get :info_refs, params: { service: 'git-upload-pack', namespace_id: project.namespace.to_param, project_id: project.path + '.git' } + + expect(response.status).to eq(401) + end + end end diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb index fab47aa4701..187c7864ad7 100644 --- a/spec/controllers/projects/issues_controller_spec.rb +++ b/spec/controllers/projects/issues_controller_spec.rb @@ -1104,18 +1104,39 @@ describe Projects::IssuesController do project.add_developer(user) end + subject do + post(:toggle_award_emoji, params: { + namespace_id: project.namespace, + project_id: project, + id: issue.iid, + name: emoji_name + }) + end + let(:emoji_name) { 'thumbsup' } + it "toggles the award emoji" do expect do - post(:toggle_award_emoji, params: { - namespace_id: project.namespace, - project_id: project, - id: issue.iid, - name: "thumbsup" - }) + subject end.to change { issue.award_emoji.count }.by(1) expect(response).to have_gitlab_http_status(200) end + + it "removes the already awarded emoji" do + create(:award_emoji, awardable: issue, name: emoji_name, user: user) + + expect { subject }.to change { AwardEmoji.count }.by(-1) + + expect(response).to have_gitlab_http_status(200) + end + + it 'marks Todos on the Issue as done' do + todo = create(:todo, target: issue, project: project, user: user) + + subject + + expect(todo.reload).to be_done + end end describe 'POST create_merge_request' do diff --git a/spec/controllers/projects/merge_requests/creations_controller_spec.rb b/spec/controllers/projects/merge_requests/creations_controller_spec.rb index 3816e1c7a31..ce977f26ec6 100644 --- a/spec/controllers/projects/merge_requests/creations_controller_spec.rb +++ b/spec/controllers/projects/merge_requests/creations_controller_spec.rb @@ -28,7 +28,7 @@ describe Projects::MergeRequests::CreationsController do it 'renders new merge request widget template' do get :new, params: get_diff_params - expect(response).to be_success + expect(response).to be_successful end end @@ -56,7 +56,7 @@ describe Projects::MergeRequests::CreationsController do it 'limits total commits' do get :new, params: large_diff_params - expect(response).to be_success + expect(response).to be_successful total = assigns(:total_commit_count) expect(assigns(:commits)).to be_an Array @@ -70,7 +70,7 @@ describe Projects::MergeRequests::CreationsController do it 'shows total commits' do get :new, params: large_diff_params - expect(response).to be_success + expect(response).to be_successful total = assigns(:total_commit_count) expect(assigns(:commits)).to be_an CommitCollection @@ -89,7 +89,7 @@ describe Projects::MergeRequests::CreationsController do get :diffs, params: get_diff_params.merge(format: 'json') - expect(response).to be_success + expect(response).to be_successful expect(assigns[:diffs]).to be_nil end end diff --git a/spec/controllers/projects/merge_requests/diffs_controller_spec.rb b/spec/controllers/projects/merge_requests/diffs_controller_spec.rb index d940d226176..ac3e9901123 100644 --- a/spec/controllers/projects/merge_requests/diffs_controller_spec.rb +++ b/spec/controllers/projects/merge_requests/diffs_controller_spec.rb @@ -66,7 +66,7 @@ describe Projects::MergeRequests::DiffsController do end it 'renders' do - expect(response).to be_success + expect(response).to be_successful expect(response.body).to have_content('Subproject commit') end end diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb index 8a5fdbc98f4..4b9b6913997 100644 --- a/spec/controllers/projects/merge_requests_controller_spec.rb +++ b/spec/controllers/projects/merge_requests_controller_spec.rb @@ -57,7 +57,7 @@ describe Projects::MergeRequestsController do go(format: :html) - expect(response).to be_success + expect(response).to be_successful end end @@ -66,7 +66,7 @@ describe Projects::MergeRequestsController do go(format: :html) - expect(response).to be_success + expect(response).to be_successful end context "that is invalid" do @@ -75,7 +75,7 @@ describe Projects::MergeRequestsController do it "renders merge request page" do go(format: :html) - expect(response).to be_success + expect(response).to be_successful end end end @@ -124,7 +124,7 @@ describe Projects::MergeRequestsController do it "renders merge request page" do go(format: :json) - expect(response).to be_success + expect(response).to be_successful end end end @@ -719,19 +719,63 @@ describe Projects::MergeRequestsController do end describe 'GET test_reports' do + let(:merge_request) do + create(:merge_request, + :with_diffs, + :with_merge_request_pipeline, + target_project: project, + source_project: project + ) + end + subject do - get :test_reports, - params: { - namespace_id: project.namespace.to_param, - project_id: project, - id: merge_request.iid - }, - format: :json + get :test_reports, params: { + namespace_id: project.namespace.to_param, + project_id: project, + id: merge_request.iid + }, + format: :json end before do allow_any_instance_of(MergeRequest) - .to receive(:compare_test_reports).and_return(comparison_status) + .to receive(:compare_test_reports) + .and_return(comparison_status) + + allow_any_instance_of(MergeRequest) + .to receive(:actual_head_pipeline) + .and_return(merge_request.all_pipelines.take) + end + + describe 'permissions on a public project with private CI/CD' do + let(:project) { create :project, :repository, :public, :builds_private } + let(:comparison_status) { { status: :parsed, data: { summary: 1 } } } + + context 'while signed out' do + before do + sign_out(user) + end + + it 'responds with a 404' do + subject + + expect(response).to have_gitlab_http_status(404) + expect(response.body).to be_blank + end + end + + context 'while signed in as an unrelated user' do + before do + sign_in(create(:user)) + end + + it 'responds with a 404' do + subject + + expect(response).to have_gitlab_http_status(404) + expect(response.body).to be_blank + end + end end context 'when comparison is being processed' do diff --git a/spec/controllers/projects/milestones_controller_spec.rb b/spec/controllers/projects/milestones_controller_spec.rb index 9b2025b836c..cbf9d437909 100644 --- a/spec/controllers/projects/milestones_controller_spec.rb +++ b/spec/controllers/projects/milestones_controller_spec.rb @@ -139,7 +139,7 @@ describe Projects::MilestonesController do expect(issue.milestone_id).to eq(milestone.id) delete :destroy, params: { namespace_id: project.namespace.id, project_id: project.id, id: milestone.iid }, format: :js - expect(response).to be_success + expect(response).to be_successful expect(Event.recent.first.action).to eq(Event::DESTROYED) diff --git a/spec/controllers/projects/notes_controller_spec.rb b/spec/controllers/projects/notes_controller_spec.rb index 9ab565dc2e8..4500c412521 100644 --- a/spec/controllers/projects/notes_controller_spec.rb +++ b/spec/controllers/projects/notes_controller_spec.rb @@ -543,23 +543,32 @@ describe Projects::NotesController do project.add_developer(user) end + subject { post(:toggle_award_emoji, params: request_params.merge(name: emoji_name)) } + let(:emoji_name) { 'thumbsup' } + it "toggles the award emoji" do expect do - post(:toggle_award_emoji, params: request_params.merge(name: "thumbsup")) + subject end.to change { note.award_emoji.count }.by(1) expect(response).to have_gitlab_http_status(200) end it "removes the already awarded emoji" do - post(:toggle_award_emoji, params: request_params.merge(name: "thumbsup")) + create(:award_emoji, awardable: note, name: emoji_name, user: user) - expect do - post(:toggle_award_emoji, params: request_params.merge(name: "thumbsup")) - end.to change { AwardEmoji.count }.by(-1) + expect { subject }.to change { AwardEmoji.count }.by(-1) expect(response).to have_gitlab_http_status(200) end + + it 'marks Todos on the Noteable as done' do + todo = create(:todo, target: note.noteable, project: project, user: user) + + subject + + expect(todo.reload).to be_done + end end describe "resolving and unresolving" do diff --git a/spec/controllers/projects/pipelines_controller_spec.rb b/spec/controllers/projects/pipelines_controller_spec.rb index 9a50ea79f5e..212d8b15252 100644 --- a/spec/controllers/projects/pipelines_controller_spec.rb +++ b/spec/controllers/projects/pipelines_controller_spec.rb @@ -177,18 +177,22 @@ describe Projects::PipelinesController do end it 'does not perform N + 1 queries' do + # Set up all required variables + get_pipeline_json + control_count = ActiveRecord::QueryRecorder.new { get_pipeline_json }.count - create_build('test', 1, 'rspec 1') - create_build('test', 1, 'spinach 0') - create_build('test', 1, 'spinach 1') - create_build('test', 1, 'audit') - create_build('post deploy', 3, 'pages 1') - create_build('post deploy', 3, 'pages 2') + first_build = pipeline.builds.first + first_build.tag_list << [:hello, :world] + create(:deployment, deployable: first_build) + + second_build = pipeline.builds.second + second_build.tag_list << [:docker, :ruby] + create(:deployment, deployable: second_build) new_count = ActiveRecord::QueryRecorder.new { get_pipeline_json }.count - expect(new_count).to be_within(12).of(control_count) + expect(new_count).to be_within(1).of(control_count) end end @@ -393,4 +397,69 @@ describe Projects::PipelinesController do end end end + + describe 'GET latest' do + let(:branch_main) { project.repository.branches[0] } + let(:branch_secondary) { project.repository.branches[1] } + + let!(:pipeline_master) do + create(:ci_pipeline, + ref: branch_main.name, + sha: branch_main.target, + project: project) + end + + let!(:pipeline_secondary) do + create(:ci_pipeline, + ref: branch_secondary.name, + sha: branch_secondary.target, + project: project) + end + + before do + project.change_head(branch_main.name) + project.reload_default_branch + end + + context 'no ref provided' do + it 'shows latest pipeline for the default project branch' do + get :show, params: { namespace_id: project.namespace, project_id: project, latest: true, ref: nil } + + expect(response).to have_gitlab_http_status(200) + expect(assigns(:pipeline)).to have_attributes(id: pipeline_master.id) + end + end + + context 'ref provided' do + before do + create(:ci_pipeline, ref: 'master', project: project) + end + + it 'shows the latest pipeline for the provided ref' do + get :show, params: { namespace_id: project.namespace, project_id: project, latest: true, ref: branch_secondary.name } + + expect(response).to have_gitlab_http_status(200) + expect(assigns(:pipeline)).to have_attributes(id: pipeline_secondary.id) + end + + context 'newer pipeline exists for older sha' do + before do + create(:ci_pipeline, ref: branch_secondary.name, sha: project.commit(branch_secondary.name).parent, project: project) + end + + it 'shows the provided ref with the last sha/pipeline combo' do + get :show, params: { namespace_id: project.namespace, project_id: project, latest: true, ref: branch_secondary.name } + + expect(response).to have_gitlab_http_status(200) + expect(assigns(:pipeline)).to have_attributes(id: pipeline_secondary.id) + end + end + end + + it 'renders a 404 if no pipeline is found for the ref' do + get :show, params: { namespace_id: project.namespace, project_id: project, ref: 'no-branch' } + + expect(response).to have_gitlab_http_status(404) + end + end end diff --git a/spec/controllers/projects/project_members_controller_spec.rb b/spec/controllers/projects/project_members_controller_spec.rb index 4141e41c7a7..5130e26c928 100644 --- a/spec/controllers/projects/project_members_controller_spec.rb +++ b/spec/controllers/projects/project_members_controller_spec.rb @@ -158,7 +158,7 @@ describe Projects::ProjectMembersController do id: member }, xhr: true - expect(response).to be_success + expect(response).to be_successful expect(project.members).not_to include member end end diff --git a/spec/controllers/projects/raw_controller_spec.rb b/spec/controllers/projects/raw_controller_spec.rb index 8ee3168273f..8b43d1264b2 100644 --- a/spec/controllers/projects/raw_controller_spec.rb +++ b/spec/controllers/projects/raw_controller_spec.rb @@ -60,16 +60,16 @@ describe Projects::RawController do execute_raw_requests(requests: 6, project: project, file_path: file_path) expect(flash[:alert]).to eq('You cannot access the raw file. Please wait a minute.') - expect(response).to redirect_to(project_blob_path(project, file_path)) + expect(response).to have_gitlab_http_status(429) end it 'logs the event on auth.log' do attributes = { message: 'Action_Rate_Limiter_Request', env: :raw_blob_request_limit, - ip: '0.0.0.0', + remote_ip: '0.0.0.0', request_method: 'GET', - fullpath: "/#{project.full_path}/raw/#{file_path}" + path: "/#{project.full_path}/raw/#{file_path}" } expect(Gitlab::AuthLogger).to receive(:error).with(attributes).once @@ -92,7 +92,7 @@ describe Projects::RawController do execute_raw_requests(requests: 3, project: project, file_path: modified_path) expect(flash[:alert]).to eq('You cannot access the raw file. Please wait a minute.') - expect(response).to redirect_to(project_blob_path(project, modified_path)) + expect(response).to have_gitlab_http_status(429) end end @@ -120,7 +120,7 @@ describe Projects::RawController do execute_raw_requests(requests: 6, project: project, file_path: file_path) expect(flash[:alert]).to eq('You cannot access the raw file. Please wait a minute.') - expect(response).to redirect_to(project_blob_path(project, file_path)) + expect(response).to have_gitlab_http_status(429) # Accessing upcase version of readme file_path = "#{commit_sha}/README.md" diff --git a/spec/controllers/projects/refs_controller_spec.rb b/spec/controllers/projects/refs_controller_spec.rb index 6db98f2428b..646c7a7db7c 100644 --- a/spec/controllers/projects/refs_controller_spec.rb +++ b/spec/controllers/projects/refs_controller_spec.rb @@ -49,7 +49,7 @@ describe Projects::RefsController do expect(::Gitlab::GitalyClient).to receive(:allow_ref_name_caching).and_call_original xhr_get(:js) - expect(response).to be_success + expect(response).to be_successful end it 'renders JSON' do @@ -57,7 +57,7 @@ describe Projects::RefsController do xhr_get(:json) - expect(response).to be_success + expect(response).to be_successful expect(json_response).to be_kind_of(Array) end end diff --git a/spec/controllers/projects/registry/tags_controller_spec.rb b/spec/controllers/projects/registry/tags_controller_spec.rb index ff35139ae2e..c6e063d8229 100644 --- a/spec/controllers/projects/registry/tags_controller_spec.rb +++ b/spec/controllers/projects/registry/tags_controller_spec.rb @@ -113,4 +113,37 @@ describe Projects::Registry::TagsController do format: :json end end + + describe 'POST bulk_destroy' do + context 'when user has access to registry' do + before do + project.add_developer(user) + end + + context 'when there is matching tag present' do + before do + stub_container_registry_tags(repository: repository.path, tags: %w[rc1 test.]) + end + + it 'makes it possible to delete tags in bulk' do + allow_any_instance_of(ContainerRegistry::Tag).to receive(:delete) { |*args| ContainerRegistry::Tag.delete(*args) } + expect(ContainerRegistry::Tag).to receive(:delete).exactly(2).times + + bulk_destroy_tags(['rc1', 'test.']) + end + end + end + + private + + def bulk_destroy_tags(names) + post :bulk_destroy, params: { + namespace_id: project.namespace, + project_id: project, + repository_id: repository, + ids: names + }, + format: :json + end + end end diff --git a/spec/controllers/projects/services_controller_spec.rb b/spec/controllers/projects/services_controller_spec.rb index 68eabce8513..563b61962cf 100644 --- a/spec/controllers/projects/services_controller_spec.rb +++ b/spec/controllers/projects/services_controller_spec.rb @@ -159,7 +159,7 @@ describe Projects::ServicesController do context 'with approved services' do it 'renders edit page' do - expect(response).to be_success + expect(response).to be_successful end end end diff --git a/spec/controllers/projects/starrers_controller_spec.rb b/spec/controllers/projects/starrers_controller_spec.rb index 59d258e99ce..5774ff7c576 100644 --- a/spec/controllers/projects/starrers_controller_spec.rb +++ b/spec/controllers/projects/starrers_controller_spec.rb @@ -3,23 +3,47 @@ require 'spec_helper' describe Projects::StarrersController do - let(:user) { create(:user) } - let(:private_user) { create(:user, private_profile: true) } + let(:user_1) { create(:user, name: 'John') } + let(:user_2) { create(:user, name: 'Michael') } + let(:private_user) { create(:user, name: 'Michael Douglas', private_profile: true) } let(:admin) { create(:user, admin: true) } - let(:project) { create(:project, :public, :repository) } + let(:project) { create(:project, :public) } before do - user.toggle_star(project) + user_1.toggle_star(project) + user_2.toggle_star(project) private_user.toggle_star(project) end describe 'GET index' do - def get_starrers - get :index, - params: { - namespace_id: project.namespace, - project_id: project - } + def get_starrers(search: nil) + get :index, params: { namespace_id: project.namespace, project_id: project, search: search } + end + + def user_ids + assigns[:starrers].map { |s| s['user_id'] } + end + + shared_examples 'starrers counts' do + it 'starrers counts are correct' do + expect(assigns[:total_count]).to eq(3) + expect(assigns[:public_count]).to eq(2) + expect(assigns[:private_count]).to eq(1) + end + end + + context 'N+1 queries' do + render_views + + it 'avoids N+1s loading users', :request_store do + get_starrers + + control_count = ActiveRecord::QueryRecorder.new { get_starrers }.count + + create_list(:user, 5).each { |user| user.toggle_star(project) } + + expect { get_starrers }.not_to exceed_query_limit(control_count) + end end context 'when project is public' do @@ -28,55 +52,118 @@ describe Projects::StarrersController do end context 'when no user is logged in' do + context 'with no searching' do + before do + get_starrers + end + + it 'only users with public profiles are visible' do + expect(user_ids).to contain_exactly(user_1.id, user_2.id) + end + + include_examples 'starrers counts' + end + + context 'when searching by user' do + before do + get_starrers(search: 'Michael') + end + + it 'only users with public profiles are visible' do + expect(user_ids).to contain_exactly(user_2.id) + end + + include_examples 'starrers counts' + end + end + + context 'when public user is logged in' do before do - get_starrers + sign_in(user_1) end - it 'only public starrers are visible' do - user_ids = assigns[:starrers].map { |s| s['user_id'] } - expect(user_ids).to include(user.id) - expect(user_ids).not_to include(private_user.id) + context 'with no searching' do + before do + get_starrers + end + + it 'their star is also visible' do + expect(user_ids).to contain_exactly(user_1.id, user_2.id) + end + + include_examples 'starrers counts' end - it 'public/private starrers counts are correct' do - expect(assigns[:public_count]).to eq(1) - expect(assigns[:private_count]).to eq(1) + context 'when searching by user' do + before do + get_starrers(search: 'Michael') + end + + it 'only users with public profiles are visible' do + expect(user_ids).to contain_exactly(user_2.id) + end + + include_examples 'starrers counts' end end context 'when private user is logged in' do before do sign_in(private_user) - - get_starrers end - it 'their star is also visible' do - user_ids = assigns[:starrers].map { |s| s['user_id'] } - expect(user_ids).to include(user.id, private_user.id) + context 'with no searching' do + before do + get_starrers + end + + it 'their star is also visible' do + expect(user_ids).to contain_exactly(user_1.id, user_2.id, private_user.id) + end + + include_examples 'starrers counts' end - it 'public/private starrers counts are correct' do - expect(assigns[:public_count]).to eq(1) - expect(assigns[:private_count]).to eq(1) + context 'when searching by user' do + before do + get_starrers(search: 'Michael') + end + + it 'only users with public profiles are visible' do + expect(user_ids).to contain_exactly(user_2.id, private_user.id) + end + + include_examples 'starrers counts' end end context 'when admin is logged in' do before do sign_in(admin) - - get_starrers end - it 'all stars are visible' do - user_ids = assigns[:starrers].map { |s| s['user_id'] } - expect(user_ids).to include(user.id, private_user.id) + context 'with no searching' do + before do + get_starrers + end + + it 'all users are visible' do + expect(user_ids).to include(user_1.id, user_2.id, private_user.id) + end + + include_examples 'starrers counts' end - it 'public/private starrers counts are correct' do - expect(assigns[:public_count]).to eq(1) - expect(assigns[:private_count]).to eq(1) + context 'when searching by user' do + before do + get_starrers(search: 'Michael') + end + + it 'public and private starrers are visible' do + expect(user_ids).to contain_exactly(user_2.id, private_user.id) + end + + include_examples 'starrers counts' end end end @@ -95,15 +182,14 @@ describe Projects::StarrersController do context 'when user is logged in' do before do sign_in(project.creator) - end - - it 'only public starrers are visible' do get_starrers + end - user_ids = assigns[:starrers].map { |s| s['user_id'] } - expect(user_ids).to include(user.id) - expect(user_ids).not_to include(private_user.id) + it 'only users with public profiles are visible' do + expect(user_ids).to contain_exactly(user_1.id, user_2.id) end + + include_examples 'starrers counts' end end end diff --git a/spec/controllers/projects/wikis_controller_spec.rb b/spec/controllers/projects/wikis_controller_spec.rb index fbca1d5740f..6fea6bca4f2 100644 --- a/spec/controllers/projects/wikis_controller_spec.rb +++ b/spec/controllers/projects/wikis_controller_spec.rb @@ -3,11 +3,11 @@ require 'spec_helper' describe Projects::WikisController do - let(:project) { create(:project, :public, :repository) } - let(:user) { project.owner } + set(:project) { create(:project, :public, :repository) } + set(:user) { project.owner } let(:project_wiki) { ProjectWiki.new(project, user) } let(:wiki) { project_wiki.wiki } - let(:wiki_title) { 'page-title-test' } + let(:wiki_title) { 'page title test' } before do create_page(wiki_title, 'hello world') @@ -19,6 +19,21 @@ describe Projects::WikisController do destroy_page(wiki_title) end + describe 'GET #new' do + subject { get :new, params: { namespace_id: project.namespace, project_id: project } } + + it 'redirects to #show and appends a `random_title` param' do + subject + + expect(response).to have_http_status(302) + expect(Rails.application.routes.recognize_path(response.redirect_url)).to include( + controller: 'projects/wikis', + action: 'show' + ) + expect(response.redirect_url).to match(/\?random_title=true\Z/) + end + end + describe 'GET #pages' do subject { get :pages, params: { namespace_id: project.namespace, project_id: project, id: wiki_title } } @@ -75,40 +90,62 @@ describe Projects::WikisController do describe 'GET #show' do render_views - subject { get :show, params: { namespace_id: project.namespace, project_id: project, id: wiki_title } } + let(:random_title) { nil } - it 'limits the retrieved pages for the sidebar' do - expect(controller).to receive(:load_wiki).and_return(project_wiki) + subject { get :show, params: { namespace_id: project.namespace, project_id: project, id: id, random_title: random_title } } - # empty? call - expect(project_wiki).to receive(:list_pages).with(limit: 1).and_call_original - # Sidebar entries - expect(project_wiki).to receive(:list_pages).with(limit: 15).and_call_original + context 'when page exists' do + let(:id) { wiki_title } - subject + it 'limits the retrieved pages for the sidebar' do + expect(controller).to receive(:load_wiki).and_return(project_wiki) + expect(project_wiki).to receive(:list_pages).with(limit: 15).and_call_original + + subject + + expect(response).to have_http_status(:ok) + expect(assigns(:page).title).to eq(wiki_title) + end + + context 'when page content encoding is invalid' do + it 'sets flash error' do + allow(controller).to receive(:valid_encoding?).and_return(false) - expect(response).to have_http_status(:ok) - expect(response.body).to include(wiki_title) + subject + + expect(response).to have_http_status(:ok) + expect(flash[:notice]).to eq('The content of this page is not encoded in UTF-8. Edits can only be made via the Git repository.') + end + end end - context 'when page content encoding is invalid' do - it 'sets flash error' do - allow(controller).to receive(:valid_encoding?).and_return(false) + context 'when the page does not exist' do + let(:id) { 'does not exist' } + before do subject + end - expect(response).to have_http_status(:ok) - expect(flash[:notice]).to eq 'The content of this page is not encoded in UTF-8. Edits can only be made via the Git repository.' + it 'builds a new wiki page with the id as the title' do + expect(assigns(:page).title).to eq(id) + end + + context 'when a random_title param is present' do + let(:random_title) { true } + + it 'builds a new wiki page with no title' do + expect(assigns(:page).title).to be_empty + end end end context 'when page is a file' do include WikiHelpers - let(:path) { upload_file_to_wiki(project, user, file_name) } + let(:id) { upload_file_to_wiki(project, user, file_name) } before do - get :show, params: { namespace_id: project.namespace, project_id: project, id: path } + subject end context 'when file is an image' do diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb index 083a1c1383a..c732caa6160 100644 --- a/spec/controllers/projects_controller_spec.rb +++ b/spec/controllers/projects_controller_spec.rb @@ -318,6 +318,102 @@ describe ProjectsController do end end + describe 'POST #archive' do + let(:group) { create(:group) } + let(:project) { create(:project, group: group) } + + before do + sign_in(user) + end + + context 'for a user with the ability to archive a project' do + before do + group.add_owner(user) + + post :archive, params: { + namespace_id: project.namespace.path, + id: project.path + } + end + + it 'archives the project' do + expect(project.reload.archived?).to be_truthy + end + + it 'redirects to projects path' do + expect(response).to have_gitlab_http_status(302) + expect(response).to redirect_to(project_path(project)) + end + end + + context 'for a user that does not have the ability to archive a project' do + before do + project.add_maintainer(user) + + post :archive, params: { + namespace_id: project.namespace.path, + id: project.path + } + end + + it 'does not archive the project' do + expect(project.reload.archived?).to be_falsey + end + + it 'returns 404' do + expect(response).to have_gitlab_http_status(404) + end + end + end + + describe 'POST #unarchive' do + let(:group) { create(:group) } + let(:project) { create(:project, :archived, group: group) } + + before do + sign_in(user) + end + + context 'for a user with the ability to unarchive a project' do + before do + group.add_owner(user) + + post :unarchive, params: { + namespace_id: project.namespace.path, + id: project.path + } + end + + it 'unarchives the project' do + expect(project.reload.archived?).to be_falsey + end + + it 'redirects to projects path' do + expect(response).to have_gitlab_http_status(302) + expect(response).to redirect_to(project_path(project)) + end + end + + context 'for a user that does not have the ability to unarchive a project' do + before do + project.add_maintainer(user) + + post :unarchive, params: { + namespace_id: project.namespace.path, + id: project.path + } + end + + it 'does not unarchive the project' do + expect(project.reload.archived?).to be_truthy + end + + it 'returns 404' do + expect(response).to have_gitlab_http_status(404) + end + end + end + describe '#housekeeping' do let(:group) { create(:group) } let(:project) { create(:project, group: group) } diff --git a/spec/controllers/registrations_controller_spec.rb b/spec/controllers/registrations_controller_spec.rb index faf3c990cb2..35487682462 100644 --- a/spec/controllers/registrations_controller_spec.rb +++ b/spec/controllers/registrations_controller_spec.rb @@ -5,6 +5,10 @@ require 'spec_helper' describe RegistrationsController do include TermsHelper + before do + stub_feature_flags(invisible_captcha: false) + end + describe '#create' do let(:base_user_params) { { name: 'new_user', username: 'new_username', email: 'new@user.com', password: 'Any_password' } } let(:user_params) { { user: base_user_params } } @@ -26,13 +30,36 @@ describe RegistrationsController do end context 'when send_user_confirmation_email is true' do - it 'does not authenticate user and sends confirmation email' do + before do stub_application_setting(send_user_confirmation_email: true) + end + + context 'when soft email confirmation is not enabled' do + before do + stub_feature_flags(soft_email_confirmation: false) + allow(User).to receive(:allow_unconfirmed_access_for).and_return 0 + end - post(:create, params: user_params) + it 'does not authenticate the user and sends a confirmation email' do + post(:create, params: user_params) - expect(ActionMailer::Base.deliveries.last.to.first).to eq(user_params[:user][:email]) - expect(subject.current_user).to be_nil + expect(ActionMailer::Base.deliveries.last.to.first).to eq(user_params[:user][:email]) + expect(subject.current_user).to be_nil + end + end + + context 'when soft email confirmation is enabled' do + before do + stub_feature_flags(soft_email_confirmation: true) + allow(User).to receive(:allow_unconfirmed_access_for).and_return 2.days + end + + it 'authenticates the user and sends a confirmation email' do + post(:create, params: user_params) + + expect(ActionMailer::Base.deliveries.last.to.first).to eq(user_params[:user][:email]) + expect(response).to redirect_to(dashboard_projects_path) + end end end @@ -88,6 +115,88 @@ describe RegistrationsController do end end + context 'when invisible captcha is enabled' do + before do + stub_feature_flags(invisible_captcha: true) + InvisibleCaptcha.timestamp_threshold = treshold + end + + let(:treshold) { 4 } + let(:session_params) { { invisible_captcha_timestamp: form_rendered_time.iso8601 } } + let(:form_rendered_time) { Time.current } + let(:submit_time) { form_rendered_time + treshold } + let(:auth_log_attributes) do + { + message: auth_log_message, + env: :invisible_captcha_signup_bot_detected, + remote_ip: '0.0.0.0', + request_method: 'POST', + path: '/users' + } + end + + describe 'the honeypot has not been filled and the signup form has not been submitted too quickly' do + it 'creates an account' do + travel_to(submit_time) do + expect { post(:create, params: user_params, session: session_params) }.to change(User, :count).by(1) + end + end + end + + describe 'honeypot spam detection' do + let(:user_params) { super().merge(firstname: 'Roy', lastname: 'Batty') } + let(:auth_log_message) { 'Invisible_Captcha_Honeypot_Request' } + + it 'logs the request, refuses to create an account and renders an empty body' do + travel_to(submit_time) do + expect(Gitlab::Metrics).to receive(:counter) + .with(:bot_blocked_by_invisible_captcha_honeypot, 'Counter of blocked sign up attempts with filled honeypot') + .and_call_original + expect(Gitlab::AuthLogger).to receive(:error).with(auth_log_attributes).once + expect { post(:create, params: user_params, session: session_params) }.not_to change(User, :count) + expect(response).to have_gitlab_http_status(200) + expect(response.body).to be_empty + end + end + end + + describe 'timestamp spam detection' do + let(:auth_log_message) { 'Invisible_Captcha_Timestamp_Request' } + + context 'the sign up form has been submitted without the invisible_captcha_timestamp parameter' do + let(:session_params) { nil } + + it 'logs the request, refuses to create an account and displays a flash alert' do + travel_to(submit_time) do + expect(Gitlab::Metrics).to receive(:counter) + .with(:bot_blocked_by_invisible_captcha_timestamp, 'Counter of blocked sign up attempts with invalid timestamp') + .and_call_original + expect(Gitlab::AuthLogger).to receive(:error).with(auth_log_attributes).once + expect { post(:create, params: user_params, session: session_params) }.not_to change(User, :count) + expect(response).to redirect_to(new_user_session_path) + expect(flash[:alert]).to include 'That was a bit too quick! Please resubmit.' + end + end + end + + context 'the sign up form has been submitted too quickly' do + let(:submit_time) { form_rendered_time } + + it 'logs the request, refuses to create an account and displays a flash alert' do + travel_to(submit_time) do + expect(Gitlab::Metrics).to receive(:counter) + .with(:bot_blocked_by_invisible_captcha_timestamp, 'Counter of blocked sign up attempts with invalid timestamp') + .and_call_original + expect(Gitlab::AuthLogger).to receive(:error).with(auth_log_attributes).once + expect { post(:create, params: user_params, session: session_params) }.not_to change(User, :count) + expect(response).to redirect_to(new_user_session_path) + expect(flash[:alert]).to include 'That was a bit too quick! Please resubmit.' + end + end + end + end + end + context 'when terms are enforced' do before do enforce_terms diff --git a/spec/controllers/search_controller_spec.rb b/spec/controllers/search_controller_spec.rb index 5a5c0a1f6ac..3e0d53a6573 100644 --- a/spec/controllers/search_controller_spec.rb +++ b/spec/controllers/search_controller_spec.rb @@ -11,151 +11,173 @@ describe SearchController do sign_in(user) end - context 'uses the right partials depending on scope' do - using RSpec::Parameterized::TableSyntax - render_views - - set(:project) { create(:project, :public, :repository, :wiki_repo) } - + shared_examples_for 'when the user cannot read cross project' do |action, params| before do - expect(::Gitlab::GitalyClient).to receive(:allow_ref_name_caching).and_call_original + allow(Ability).to receive(:allowed?).and_call_original + allow(Ability).to receive(:allowed?) + .with(user, :read_cross_project, :global) { false } end - subject { get(:show, params: { project_id: project.id, scope: scope, search: 'merge' }) } + it 'blocks access without a project_id' do + get action, params: params - where(:partial, :scope) do - '_blob' | :blobs - '_wiki_blob' | :wiki_blobs - '_commit' | :commits + expect(response).to have_gitlab_http_status(403) end - with_them do - it do - project_wiki = create(:project_wiki, project: project, user: user) - create(:wiki_page, wiki: project_wiki, attrs: { title: 'merge', content: 'merge' }) + it 'allows access with a project_id' do + get action, params: params.merge(project_id: create(:project, :public).id) - expect(subject).to render_template("search/results/#{partial}") - end + expect(response).to have_gitlab_http_status(200) end end - context 'global search' do - render_views - - it 'omits pipeline status from load' do - project = create(:project, :public) - expect(Gitlab::Cache::Ci::ProjectPipelineStatus).not_to receive(:load_in_batch_for_projects) - - get :show, params: { scope: 'projects', search: project.name } + shared_examples_for 'with external authorization service enabled' do |action, params| + let(:project) { create(:project, namespace: user.namespace) } + let(:note) { create(:note_on_issue, project: project) } - expect(assigns[:search_objects].first).to eq project + before do + enable_external_authorization_service_check end - end - - it 'finds issue comments' do - project = create(:project, :public) - note = create(:note_on_issue, project: project) - get :show, params: { project_id: project.id, scope: 'notes', search: note.note } + it 'renders a 403 when no project is given' do + get action, params: params - expect(assigns[:search_objects].first).to eq note - end - - context 'when the user cannot read cross project' do - before do - allow(Ability).to receive(:allowed?).and_call_original - allow(Ability).to receive(:allowed?) - .with(user, :read_cross_project, :global) { false } + expect(response).to have_gitlab_http_status(403) end - it 'still allows accessing the search page' do - get :show + it 'renders a 200 when a project was set' do + get action, params: params.merge(project_id: project.id) expect(response).to have_gitlab_http_status(200) end + end - it 'still blocks searches without a project_id' do - get :show, params: { search: 'hello' } + describe 'GET #show' do + it_behaves_like 'when the user cannot read cross project', :show, { search: 'hello' } do + it 'still allows accessing the search page' do + get :show - expect(response).to have_gitlab_http_status(403) + expect(response).to have_gitlab_http_status(200) + end end - it 'allows searches with a project_id' do - get :show, params: { search: 'hello', project_id: create(:project, :public).id } + it_behaves_like 'with external authorization service enabled', :show, { search: 'hello' } - expect(response).to have_gitlab_http_status(200) - end - end + context 'uses the right partials depending on scope' do + using RSpec::Parameterized::TableSyntax + render_views + + set(:project) { create(:project, :public, :repository, :wiki_repo) } - context 'on restricted projects' do - context 'when signed out' do before do - sign_out(user) + expect(::Gitlab::GitalyClient).to receive(:allow_ref_name_caching).and_call_original end - it "doesn't expose comments on issues" do - project = create(:project, :public, :issues_private) - note = create(:note_on_issue, project: project) + subject { get(:show, params: { project_id: project.id, scope: scope, search: 'merge' }) } - get :show, params: { project_id: project.id, scope: 'notes', search: note.note } + where(:partial, :scope) do + '_blob' | :blobs + '_wiki_blob' | :wiki_blobs + '_commit' | :commits + end - expect(assigns[:search_objects].count).to eq(0) + with_them do + it do + project_wiki = create(:project_wiki, project: project, user: user) + create(:wiki_page, wiki: project_wiki, attrs: { title: 'merge', content: 'merge' }) + + expect(subject).to render_template("search/results/#{partial}") + end end end - it "doesn't expose comments on merge_requests" do - project = create(:project, :public, :merge_requests_private) - note = create(:note_on_merge_request, project: project) + context 'global search' do + render_views - get :show, params: { project_id: project.id, scope: 'notes', search: note.note } + it 'omits pipeline status from load' do + project = create(:project, :public) + expect(Gitlab::Cache::Ci::ProjectPipelineStatus).not_to receive(:load_in_batch_for_projects) + + get :show, params: { scope: 'projects', search: project.name } - expect(assigns[:search_objects].count).to eq(0) + expect(assigns[:search_objects].first).to eq project + end end - it "doesn't expose comments on snippets" do - project = create(:project, :public, :snippets_private) - note = create(:note_on_project_snippet, project: project) + it 'finds issue comments' do + project = create(:project, :public) + note = create(:note_on_issue, project: project) get :show, params: { project_id: project.id, scope: 'notes', search: note.note } - expect(assigns[:search_objects].count).to eq(0) + expect(assigns[:search_objects].first).to eq note end - end - context 'with external authorization service enabled' do - let(:project) { create(:project, namespace: user.namespace) } - let(:note) { create(:note_on_issue, project: project) } + context 'on restricted projects' do + context 'when signed out' do + before do + sign_out(user) + end - before do - enable_external_authorization_service_check - end + it "doesn't expose comments on issues" do + project = create(:project, :public, :issues_private) + note = create(:note_on_issue, project: project) - describe 'GET #show' do - it 'renders a 403 when no project is given' do - get :show, params: { scope: 'notes', search: note.note } + get :show, params: { project_id: project.id, scope: 'notes', search: note.note } - expect(response).to have_gitlab_http_status(403) + expect(assigns[:search_objects].count).to eq(0) + end end - it 'renders a 200 when a project was set' do + it "doesn't expose comments on merge_requests" do + project = create(:project, :public, :merge_requests_private) + note = create(:note_on_merge_request, project: project) + get :show, params: { project_id: project.id, scope: 'notes', search: note.note } - expect(response).to have_gitlab_http_status(200) + expect(assigns[:search_objects].count).to eq(0) end - end - describe 'GET #autocomplete' do - it 'renders a 403 when no project is given' do - get :autocomplete, params: { term: 'hello' } + it "doesn't expose comments on snippets" do + project = create(:project, :public, :snippets_private) + note = create(:note_on_project_snippet, project: project) - expect(response).to have_gitlab_http_status(403) + get :show, params: { project_id: project.id, scope: 'notes', search: note.note } + + expect(assigns[:search_objects].count).to eq(0) end + end + end - it 'renders a 200 when a project was set' do - get :autocomplete, params: { project_id: project.id, term: 'hello' } + describe 'GET #count' do + it_behaves_like 'when the user cannot read cross project', :count, { search: 'hello', scope: 'projects' } + it_behaves_like 'with external authorization service enabled', :count, { search: 'hello', scope: 'projects' } - expect(response).to have_gitlab_http_status(200) - end + it 'returns the result count for the given term and scope' do + create(:project, :public, name: 'hello world') + create(:project, :public, name: 'foo bar') + + get :count, params: { search: 'hello', scope: 'projects' } + + expect(response).to have_gitlab_http_status(200) + expect(json_response).to eq({ 'count' => '1' }) + end + + it 'raises an error if search term is missing' do + expect do + get :count, params: { scope: 'projects' } + end.to raise_error(ActionController::ParameterMissing) end + + it 'raises an error if search scope is missing' do + expect do + get :count, params: { search: 'hello' } + end.to raise_error(ActionController::ParameterMissing) + end + end + + describe 'GET #autocomplete' do + it_behaves_like 'when the user cannot read cross project', :autocomplete, { term: 'hello' } + it_behaves_like 'with external authorization service enabled', :autocomplete, { term: 'hello' } end end diff --git a/spec/controllers/snippets/notes_controller_spec.rb b/spec/controllers/snippets/notes_controller_spec.rb index 652533ac49f..fd4b95ce226 100644 --- a/spec/controllers/snippets/notes_controller_spec.rb +++ b/spec/controllers/snippets/notes_controller_spec.rb @@ -288,11 +288,13 @@ describe Snippets::NotesController do describe 'POST toggle_award_emoji' do let(:note) { create(:note_on_personal_snippet, noteable: public_snippet) } + let(:emoji_name) { 'thumbsup'} + before do sign_in(user) end - subject { post(:toggle_award_emoji, params: { snippet_id: public_snippet, id: note.id, name: "thumbsup" }) } + subject { post(:toggle_award_emoji, params: { snippet_id: public_snippet, id: note.id, name: emoji_name }) } it "toggles the award emoji" do expect { subject }.to change { note.award_emoji.count }.by(1) @@ -301,7 +303,7 @@ describe Snippets::NotesController do end it "removes the already awarded emoji when it exists" do - note.toggle_award_emoji('thumbsup', user) # create award emoji before + create(:award_emoji, awardable: note, name: emoji_name, user: user) expect { subject }.to change { AwardEmoji.count }.by(-1) diff --git a/spec/controllers/users_controller_spec.rb b/spec/controllers/users_controller_spec.rb index 8b8d4c57000..5566df0c216 100644 --- a/spec/controllers/users_controller_spec.rb +++ b/spec/controllers/users_controller_spec.rb @@ -19,7 +19,7 @@ describe UsersController do it 'renders the show template' do get :show, params: { username: user.username } - expect(response).to be_success + expect(response).to be_successful expect(response).to render_template('show') end end @@ -362,7 +362,7 @@ describe UsersController do it 'responds with success' do get :show, params: { username: user.username } - expect(response).to be_success + expect(response).to be_successful end end @@ -418,7 +418,7 @@ describe UsersController do it 'responds with success' do get :projects, params: { username: user.username } - expect(response).to be_success + expect(response).to be_successful end end diff --git a/spec/factories/ci/bridge.rb b/spec/factories/ci/bridge.rb index 6491b9dca19..b1b714277e4 100644 --- a/spec/factories/ci/bridge.rb +++ b/spec/factories/ci/bridge.rb @@ -8,7 +8,7 @@ FactoryBot.define do ref 'master' tag false created_at 'Di 29. Okt 09:50:00 CET 2013' - status :success + status :created pipeline factory: :ci_pipeline @@ -17,6 +17,7 @@ FactoryBot.define do end transient { downstream nil } + transient { upstream nil } after(:build) do |bridge, evaluator| bridge.project ||= bridge.pipeline.project @@ -26,6 +27,12 @@ FactoryBot.define do trigger: { project: evaluator.downstream.full_path } ) end + + if evaluator.upstream.present? + bridge.options = bridge.options.to_h.merge( + bridge_needs: { pipeline: evaluator.upstream.full_path } + ) + end end end end diff --git a/spec/factories/ci/job_artifacts.rb b/spec/factories/ci/job_artifacts.rb index 2d68a8e9fe3..6f553cadfa3 100644 --- a/spec/factories/ci/job_artifacts.rb +++ b/spec/factories/ci/job_artifacts.rb @@ -8,6 +8,10 @@ FactoryBot.define do file_type :archive file_format :zip + trait :expired do + expire_at { Date.yesterday } + end + trait :remote_store do file_store JobArtifactUploader::Store::REMOTE end diff --git a/spec/factories/group_members.rb b/spec/factories/group_members.rb index 8dab6c71b06..4c875935d82 100644 --- a/spec/factories/group_members.rb +++ b/spec/factories/group_members.rb @@ -20,5 +20,9 @@ FactoryBot.define do "email#{n}@email.com" end end + + trait(:ldap) do + ldap true + end end end diff --git a/spec/factories/services.rb b/spec/factories/services.rb index f3e662ad4f5..b2d6ada91fa 100644 --- a/spec/factories/services.rb +++ b/spec/factories/services.rb @@ -16,6 +16,19 @@ FactoryBot.define do ) end + factory :emails_on_push_service do + project + type 'EmailsOnPushService' + active true + push_events true + tag_push_events true + properties( + recipients: 'test@example.com', + disable_diffs: true, + send_from_committer_email: true + ) + end + factory :mock_deployment_service do project type 'MockDeploymentService' diff --git a/spec/factories/uploads.rb b/spec/factories/uploads.rb index 2ede92c8af0..3f6326114c9 100644 --- a/spec/factories/uploads.rb +++ b/spec/factories/uploads.rb @@ -56,10 +56,7 @@ FactoryBot.define do end trait :attachment_upload do - transient do - mount_point :attachment - end - + mount_point :attachment model { build(:note) } uploader "AttachmentUploader" end diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb index 4e7b25115d7..902ecdcd3e8 100644 --- a/spec/features/boards/boards_spec.rb +++ b/spec/features/boards/boards_spec.rb @@ -236,6 +236,15 @@ describe 'Issue Boards', :js do expect(find('.board:nth-child(2)')).to have_content(planning.title) end + it 'dragging does not duplicate list' do + selector = '.board:not(.is-ghost) .board-header' + expect(page).to have_selector(selector, text: development.title, count: 1) + + drag(list_from_index: 2, list_to_index: 1, selector: '.board-header', perform_drop: false) + + expect(page).to have_selector(selector, text: development.title, count: 1) + end + it 'issue moves between lists' do drag(list_from_index: 1, from_index: 1, list_to_index: 2) @@ -576,7 +585,7 @@ describe 'Issue Boards', :js do end end - def drag(selector: '.board-list', list_from_index: 0, from_index: 0, to_index: 0, list_to_index: 0) + def drag(selector: '.board-list', list_from_index: 0, from_index: 0, to_index: 0, list_to_index: 0, perform_drop: true) # ensure there is enough horizontal space for four boards resize_window(2000, 800) @@ -585,7 +594,8 @@ describe 'Issue Boards', :js do list_from_index: list_from_index, from_index: from_index, to_index: to_index, - list_to_index: list_to_index) + list_to_index: list_to_index, + perform_drop: perform_drop) end def wait_for_board_cards(board_number, expected_cards) diff --git a/spec/features/boards/multiple_boards_spec.rb b/spec/features/boards/multiple_boards_spec.rb new file mode 100644 index 00000000000..9a2b7a80498 --- /dev/null +++ b/spec/features/boards/multiple_boards_spec.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe 'Multiple Issue Boards', :js do + set(:user) { create(:user) } + set(:project) { create(:project, :public) } + set(:planning) { create(:label, project: project, name: 'Planning') } + set(:board) { create(:board, name: 'board1', project: project) } + set(:board2) { create(:board, name: 'board2', project: project) } + let(:parent) { project } + let(:boards_path) { project_boards_path(project) } + + it_behaves_like 'multiple issue boards' +end diff --git a/spec/features/container_registry_spec.rb b/spec/features/container_registry_spec.rb index 89dece97a35..aefdc4d6d4f 100644 --- a/spec/features/container_registry_spec.rb +++ b/spec/features/container_registry_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe "Container Registry", :js do +describe 'Container Registry', :js do let(:user) { create(:user) } let(:project) { create(:project) } @@ -40,8 +40,7 @@ describe "Container Registry", :js do it 'user removes entire container repository' do visit_container_registry - expect_any_instance_of(ContainerRepository) - .to receive(:delete_tags!).and_return(true) + expect_any_instance_of(ContainerRepository).to receive(:delete_tags!).and_return(true) click_on(class: 'js-remove-repo') expect(find('.modal .modal-title')).to have_content 'Remove repository' @@ -54,10 +53,9 @@ describe "Container Registry", :js do find('.js-toggle-repo').click wait_for_requests - expect_any_instance_of(ContainerRegistry::Tag) - .to receive(:delete).and_return(true) + expect_any_instance_of(ContainerRegistry::Tag).to receive(:delete).and_return(true) - click_on(class: 'js-delete-registry') + click_on(class: 'js-delete-registry-row', visible: false) expect(find('.modal .modal-title')).to have_content 'Remove image' find('.modal .modal-footer .btn-danger').click end diff --git a/spec/features/dashboard/projects_spec.rb b/spec/features/dashboard/projects_spec.rb index e2100c8562b..e4728f37217 100644 --- a/spec/features/dashboard/projects_spec.rb +++ b/spec/features/dashboard/projects_spec.rb @@ -169,7 +169,7 @@ describe 'Dashboard Projects' do expect(page).to have_xpath("//a[@href='#{pipelines_project_commit_path(project, project.commit, ref: pipeline.ref)}']") expect(page).to have_css('.ci-status-link') expect(page).to have_css('.ci-status-icon-success') - expect(page).to have_link('Commit: passed') + expect(page).to have_link('Pipeline: passed') end end @@ -189,7 +189,7 @@ describe 'Dashboard Projects' 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') + expect(page).not_to have_link('Pipeline: passed') end end end diff --git a/spec/features/groups/show_spec.rb b/spec/features/groups/show_spec.rb index 942a9889488..bcaed2a5f18 100644 --- a/spec/features/groups/show_spec.rb +++ b/spec/features/groups/show_spec.rb @@ -161,4 +161,27 @@ describe 'Group show page' do expect(find('.group-row:nth-child(3) .namespace-title > a')).to have_content(project3.title) end end + + context 'notification button', :js do + let(:maintainer) { create(:user) } + let!(:project) { create(:project, namespace: group) } + + before do + group.add_maintainer(maintainer) + sign_in(maintainer) + end + + it 'is enabled by default' do + visit path + + expect(page).to have_selector('.notifications-btn:not(.disabled)', visible: true) + end + + it 'is disabled if emails are disabled' do + group.update_attribute(:emails_disabled, true) + visit path + + expect(page).to have_selector('.notifications-btn.disabled', visible: true) + end + end end diff --git a/spec/features/invites_spec.rb b/spec/features/invites_spec.rb index 855cf22642e..1e054a7b358 100644 --- a/spec/features/invites_spec.rb +++ b/spec/features/invites_spec.rb @@ -10,17 +10,17 @@ describe 'Invites' do let(:group_invite) { group.group_members.invite.last } before do + stub_feature_flags(invisible_captcha: false) project.add_maintainer(owner) group.add_user(owner, Gitlab::Access::OWNER) group.add_developer('user@example.com', owner) group_invite.generate_invite_token! end - def confirm_email_and_sign_in(new_user) + def confirm_email(new_user) new_user_token = User.find_by_email(new_user.email).confirmation_token visit user_confirmation_path(confirmation_token: new_user_token) - fill_in_sign_in_form(new_user) end def fill_in_sign_up_form(new_user) @@ -154,17 +154,41 @@ describe 'Invites' do context 'email confirmation enabled' do let(:send_email_confirmation) { true } - it 'signs up and redirects to root page with all the project/groups invitation automatically accepted' do - fill_in_sign_up_form(new_user) - confirm_email_and_sign_in(new_user) + context 'when soft email confirmation is not enabled' do + before do + # stub_feature_flags(soft_email_confirmation: false) + allow(User).to receive(:allow_unconfirmed_access_for).and_return 0 + end - expect(current_path).to eq(root_path) - expect(page).to have_content(project.full_name) - visit group_path(group) - expect(page).to have_content(group.full_name) + it 'signs up and redirects to root page with all the project/groups invitation automatically accepted' do + fill_in_sign_up_form(new_user) + confirm_email(new_user) + fill_in_sign_in_form(new_user) + + expect(current_path).to eq(root_path) + expect(page).to have_content(project.full_name) + visit group_path(group) + expect(page).to have_content(group.full_name) + end end - it "doesn't accept invitations until the user confirm his email" do + context 'when soft email confirmation is enabled' do + before do + allow(User).to receive(:allow_unconfirmed_access_for).and_return 2.days + end + + it 'signs up and redirects to root page with all the project/groups invitation automatically accepted' do + fill_in_sign_up_form(new_user) + confirm_email(new_user) + + expect(current_path).to eq(root_path) + expect(page).to have_content(project.full_name) + visit group_path(group) + expect(page).to have_content(group.full_name) + end + end + + it "doesn't accept invitations until the user confirms his email" do fill_in_sign_up_form(new_user) sign_in(owner) @@ -175,11 +199,32 @@ describe 'Invites' do context 'the user sign-up using a different email address' do let(:invite_email) { build_stubbed(:user).email } - it 'signs up and redirects to the invitation page' do - fill_in_sign_up_form(new_user) - confirm_email_and_sign_in(new_user) + context 'when soft email confirmation is not enabled' do + before do + stub_feature_flags(soft_email_confirmation: false) + allow(User).to receive(:allow_unconfirmed_access_for).and_return 0 + end - expect(current_path).to eq(invite_path(group_invite.raw_invite_token)) + it 'signs up and redirects to the invitation page' do + fill_in_sign_up_form(new_user) + confirm_email(new_user) + fill_in_sign_in_form(new_user) + + expect(current_path).to eq(invite_path(group_invite.raw_invite_token)) + end + end + + context 'when soft email confirmation is enabled' do + before do + stub_feature_flags(soft_email_confirmation: true) + allow(User).to receive(:allow_unconfirmed_access_for).and_return 2.days + end + + it 'signs up and redirects to the invitation page' do + fill_in_sign_up_form(new_user) + + expect(current_path).to eq(invite_path(group_invite.raw_invite_token)) + end end end end diff --git a/spec/features/issues/user_toggles_subscription_spec.rb b/spec/features/issues/user_toggles_subscription_spec.rb index 7a721adc8dd..165d41950da 100644 --- a/spec/features/issues/user_toggles_subscription_spec.rb +++ b/spec/features/issues/user_toggles_subscription_spec.rb @@ -27,4 +27,14 @@ describe "User toggles subscription", :js do # Check we're unsubscribed. expect(subscription_button).to have_css("button:not(.is-checked)") end + + context 'when project emails are disabled' do + let(:project) { create(:project_empty_repo, :public, emails_disabled: true) } + + it 'is disabled' do + expect(page).to have_content('Notifications have been disabled by the project or group owner') + expect(page).to have_selector('.js-emails-disabled', visible: true) + expect(page).not_to have_selector('.js-issuable-subscribe-button') + end + end end diff --git a/spec/features/markdown/math_spec.rb b/spec/features/markdown/math_spec.rb index 68d99b4241a..76eef66c517 100644 --- a/spec/features/markdown/math_spec.rb +++ b/spec/features/markdown/math_spec.rb @@ -34,7 +34,9 @@ describe 'Math rendering', :js do 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') + page.within '.description > .md' do + expect(page).to have_selector('.katex-error') + expect(page).to have_selector('.katex-html a', text: 'Gitlab') + end end end diff --git a/spec/features/markdown/metrics_spec.rb b/spec/features/markdown/metrics_spec.rb index aa53ac50c78..4de67cfcdbe 100644 --- a/spec/features/markdown/metrics_spec.rb +++ b/spec/features/markdown/metrics_spec.rb @@ -26,13 +26,31 @@ describe 'Metrics rendering', :js, :use_clean_rails_memory_store_caching do restore_host end - context 'with deployments and related deployable present' do - it 'shows embedded metrics' do + it 'shows embedded metrics' do + visit project_issue_path(project, issue) + + expect(page).to have_css('div.prometheus-graph') + expect(page).to have_text('Memory Usage (Total)') + expect(page).to have_text('Core Usage (Total)') + end + + context 'when dashboard params are in included the url' do + let(:metrics_url) { metrics_project_environment_url(project, environment, **chart_params) } + + let(:chart_params) do + { + group: 'System metrics (Kubernetes)', + title: 'Memory Usage (Pod average)', + y_label: 'Memory Used per Pod (MB)' + } + end + + it 'shows embedded metrics for the specifiec chart' do visit project_issue_path(project, issue) expect(page).to have_css('div.prometheus-graph') - expect(page).to have_text('Memory Usage (Total)') - expect(page).to have_text('Core Usage (Total)') + expect(page).to have_text(chart_params[:title]) + expect(page).to have_text(chart_params[:y_label]) end end diff --git a/spec/features/profiles/user_edit_profile_spec.rb b/spec/features/profiles/user_edit_profile_spec.rb index 1ab7742b36e..0905ab0aef8 100644 --- a/spec/features/profiles/user_edit_profile_spec.rb +++ b/spec/features/profiles/user_edit_profile_spec.rb @@ -49,6 +49,23 @@ describe 'User edit profile' do end end + describe 'when I change my email' do + before do + user.send_reset_password_instructions + end + + it 'clears the reset password token' do + expect(user.reset_password_token?).to be true + + fill_in 'user_email', with: 'new-email@example.com' + submit_settings + + user.reload + expect(user.confirmation_token).not_to be_nil + expect(user.reset_password_token?).to be false + end + end + context 'user avatar' do before do attach_file(:user_avatar, Rails.root.join('spec', 'fixtures', 'banana_sample.gif')) diff --git a/spec/features/profiles/user_visits_notifications_tab_spec.rb b/spec/features/profiles/user_visits_notifications_tab_spec.rb index 1472cc882a7..d788c0574e2 100644 --- a/spec/features/profiles/user_visits_notifications_tab_spec.rb +++ b/spec/features/profiles/user_visits_notifications_tab_spec.rb @@ -20,4 +20,12 @@ describe 'User visits the notifications tab', :js do expect(page).to have_selector('#notifications-button', text: 'On mention') end + + context 'when project emails are disabled' do + let(:project) { create(:project, emails_disabled: true) } + + it 'notification button is disabled' do + expect(page).to have_selector('.notifications-btn.disabled', visible: true) + end + end end diff --git a/spec/features/profiles/user_visits_profile_preferences_page_spec.rb b/spec/features/profiles/user_visits_profile_preferences_page_spec.rb index 5e52c82a234..4dbdea02e27 100644 --- a/spec/features/profiles/user_visits_profile_preferences_page_spec.rb +++ b/spec/features/profiles/user_visits_profile_preferences_page_spec.rb @@ -38,7 +38,7 @@ describe 'User visits the profile preferences page' do describe 'User changes their default dashboard', :js do it 'creates a flash message' do - select 'Starred Projects', from: 'user_dashboard' + select2('stars', from: '#user_dashboard') click_button 'Save' wait_for_requests @@ -47,7 +47,7 @@ describe 'User visits the profile preferences page' do end it 'updates their preference' do - select 'Starred Projects', from: 'user_dashboard' + select2('stars', from: '#user_dashboard') click_button 'Save' wait_for_requests diff --git a/spec/features/projects/files/user_browses_a_tree_with_a_folder_containing_only_a_folder_spec.rb b/spec/features/projects/files/user_browses_a_tree_with_a_folder_containing_only_a_folder_spec.rb index c19e46da913..6bd569e5ee2 100644 --- a/spec/features/projects/files/user_browses_a_tree_with_a_folder_containing_only_a_folder_spec.rb +++ b/spec/features/projects/files/user_browses_a_tree_with_a_folder_containing_only_a_folder_spec.rb @@ -3,18 +3,15 @@ require 'spec_helper' # This is a regression test for https://gitlab.com/gitlab-org/gitlab-ce/issues/37569 -# Quarantine: https://gitlab.com/gitlab-org/gitlab-ce/issues/65329 -describe 'Projects > Files > User browses a tree with a folder containing only a folder', :quarantine do +describe 'Projects > Files > User browses a tree with a folder containing only a folder', :js do let(:project) { create(:project, :empty_repo) } let(:user) { project.owner } before do - # We need to disable the tree.flat_path provided by Gitaly to reproduce the issue - allow(Gitlab::GitalyClient).to receive(:feature_enabled?).and_return(false) - project.repository.create_dir(user, 'foo/bar', branch_name: 'master', message: 'Add the foo/bar folder') sign_in(user) visit(project_tree_path(project, project.repository.root_ref)) + wait_for_requests end it 'shows the nested folder on a single row' do diff --git a/spec/features/projects/new_project_spec.rb b/spec/features/projects/new_project_spec.rb index 010a5de6930..22a0d268243 100644 --- a/spec/features/projects/new_project_spec.rb +++ b/spec/features/projects/new_project_spec.rb @@ -280,7 +280,7 @@ describe 'New project' do end it 'shows import instructions' do - expect(page).to have_content('Import repositories from GitHub') + expect(page).to have_content('Authenticate with GitHub') expect(current_path).to eq new_import_github_path end end diff --git a/spec/features/projects/pages_lets_encrypt_spec.rb b/spec/features/projects/pages_lets_encrypt_spec.rb index a5f8702302c..8b5964b2eee 100644 --- a/spec/features/projects/pages_lets_encrypt_spec.rb +++ b/spec/features/projects/pages_lets_encrypt_spec.rb @@ -75,12 +75,10 @@ describe "Pages with Let's Encrypt", :https_pages_enabled do end shared_examples 'user sees private keys only for user provided certificate' do - before do - visit edit_project_pages_domain_path(project, domain) - end - shared_examples 'user do not see private key' do it 'user do not see private key' do + visit edit_project_pages_domain_path(project, domain) + expect(find_field('Key (PEM)', visible: :all, disabled: :all).value).to be_blank end end @@ -101,6 +99,8 @@ describe "Pages with Let's Encrypt", :https_pages_enabled do let(:domain) { create(:pages_domain, project: project) } it 'user sees private key' do + visit edit_project_pages_domain_path(project, domain) + expect(find_field('Key (PEM)').value).not_to be_blank end end diff --git a/spec/features/projects/raw/user_interacts_with_raw_endpoint_spec.rb b/spec/features/projects/raw/user_interacts_with_raw_endpoint_spec.rb new file mode 100644 index 00000000000..6d587053b4f --- /dev/null +++ b/spec/features/projects/raw/user_interacts_with_raw_endpoint_spec.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'Projects > Raw > User interacts with raw endpoint' do + include RepoHelpers + + let(:user) { create(:user) } + let(:project) { create(:project, :repository, :public) } + let(:file_path) { 'master/README.md' } + + before do + stub_application_setting(raw_blob_request_limit: 3) + project.add_developer(user) + create_file_in_repo(project, 'master', 'master', 'README.md', 'readme content') + + sign_in(user) + end + + context 'when user access a raw file' do + it 'renders the page successfully' do + visit project_raw_url(project, file_path) + + expect(source).to eq('') # Body is filled in by gitlab-workhorse + end + end + + context 'when user goes over the rate requests limit' do + it 'returns too many requests' do + 4.times do + visit project_raw_url(project, file_path) + end + + expect(source).to have_content('You are being redirected') + click_link('redirected') + expect(page).to have_content('You cannot access the raw file. Please wait a minute.') + end + end +end diff --git a/spec/features/projects/settings/visibility_settings_spec.rb b/spec/features/projects/settings/visibility_settings_spec.rb index 46fd676954d..0e757e647a0 100644 --- a/spec/features/projects/settings/visibility_settings_spec.rb +++ b/spec/features/projects/settings/visibility_settings_spec.rb @@ -59,6 +59,12 @@ describe 'Projects > Settings > Visibility settings', :js do end end end + + context 'disable email notifications' do + it 'is visible' do + expect(page).to have_selector('.js-emails-disabled', visible: true) + end + end end context 'as maintainer' do @@ -76,5 +82,11 @@ describe 'Projects > Settings > Visibility settings', :js do expect(visibility_select_container).to have_selector 'select[name="project[visibility_level]"]:disabled' expect(visibility_select_container).to have_content 'The project can be accessed by anyone, regardless of authentication.' end + + context 'disable email notifications' do + it 'is not available' do + expect(page).not_to have_selector('.js-emails-disabled', visible: true) + end + end end end diff --git a/spec/features/projects/show/user_manages_notifications_spec.rb b/spec/features/projects/show/user_manages_notifications_spec.rb index 5e9c98428cf..851a09cf28a 100644 --- a/spec/features/projects/show/user_manages_notifications_spec.rb +++ b/spec/features/projects/show/user_manages_notifications_spec.rb @@ -65,4 +65,12 @@ describe 'Projects > Show > User manages notifications', :js do end end end + + context 'when project emails are disabled' do + let(:project) { create(:project, :public, :repository, emails_disabled: true) } + + it 'is disabled' do + expect(page).to have_selector('.notifications-btn.disabled', visible: true) + end + end end diff --git a/spec/features/projects/show/user_sees_last_commit_ci_status_spec.rb b/spec/features/projects/show/user_sees_last_commit_ci_status_spec.rb index a1cad261875..fdc238d55cf 100644 --- a/spec/features/projects/show/user_sees_last_commit_ci_status_spec.rb +++ b/spec/features/projects/show/user_sees_last_commit_ci_status_spec.rb @@ -18,7 +18,7 @@ describe 'Projects > Show > User sees last commit CI status' do page.within '.blob-commit-info' do expect(page).to have_content(project.commit.sha[0..6]) - expect(page).to have_link('Commit: skipped') + expect(page).to have_link('Pipeline: skipped') end end end diff --git a/spec/features/projects/tree/create_directory_spec.rb b/spec/features/projects/tree/create_directory_spec.rb index 7ac5da86702..99285011405 100644 --- a/spec/features/projects/tree/create_directory_spec.rb +++ b/spec/features/projects/tree/create_directory_spec.rb @@ -32,10 +32,12 @@ describe 'Multi-file editor new directory', :js do click_button('Create directory') end + expect(page).to have_content('folder name') + first('.ide-tree-actions button').click - page.within('.modal-dialog') do - find('.form-control').set('file name') + page.within('.modal') do + find('.form-control').set('folder name/file name') click_button('Create file') end @@ -44,13 +46,18 @@ describe 'Multi-file editor new directory', :js do find('.js-ide-commit-mode').click - find('.multi-file-commit-list-item').hover click_button 'Stage' fill_in('commit-message', with: 'commit message ide') + find(:css, ".js-ide-commit-new-mr input").set(false) + + wait_for_requests + page.within '.multi-file-commit-form' do click_button('Commit') + + wait_for_requests end find('.js-ide-edit-mode').click diff --git a/spec/features/projects/tree/create_file_spec.rb b/spec/features/projects/tree/create_file_spec.rb index 00eefe9db42..780575a5975 100644 --- a/spec/features/projects/tree/create_file_spec.rb +++ b/spec/features/projects/tree/create_file_spec.rb @@ -36,15 +36,20 @@ describe 'Multi-file editor new file', :js do find('.js-ide-commit-mode').click - find('.multi-file-commit-list-item').hover click_button 'Stage' fill_in('commit-message', with: 'commit message ide') + find(:css, ".js-ide-commit-new-mr input").set(false) + page.within '.multi-file-commit-form' do click_button('Commit') + + wait_for_requests end + find('.js-ide-edit-mode').click + expect(page).to have_content('file name') end end diff --git a/spec/features/projects/wiki/markdown_preview_spec.rb b/spec/features/projects/wiki/markdown_preview_spec.rb index 7b511c4d3d5..5c6b04a7141 100644 --- a/spec/features/projects/wiki/markdown_preview_spec.rb +++ b/spec/features/projects/wiki/markdown_preview_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe 'Projects > Wiki > User previews markdown changes', :js do - let(:user) { create(:user) } + set(:user) { create(:user) } let(:project) { create(:project, :wiki_repo, namespace: user.namespace) } let(:wiki_page) { create(:wiki_page, wiki: project.wiki, attrs: { title: 'home', content: '[some link](other-page)' }) } let(:wiki_content) do @@ -20,23 +20,12 @@ describe 'Projects > Wiki > User previews markdown changes', :js do project.add_maintainer(user) sign_in(user) - - visit project_wiki_path(project, wiki_page) end context "while creating a new wiki page" do context "when there are no spaces or hyphens in the page name" do it "rewrites relative links as expected" do - find('.add-new-wiki').click - page.within '#modal-new-wiki' do - fill_in :new_wiki_path, with: 'a/b/c/d' - click_button 'Create page' - end - - page.within '.wiki-form' do - fill_in :wiki_content, with: wiki_content - click_on "Preview" - end + create_wiki_page('a/b/c/d', content: wiki_content) expect(page).to have_content("regular link") @@ -50,16 +39,7 @@ describe 'Projects > Wiki > User previews markdown changes', :js do context "when there are spaces in the page name" do it "rewrites relative links as expected" do - click_link 'New page' - page.within '#modal-new-wiki' do - fill_in :new_wiki_path, with: 'a page/b page/c page/d page' - click_button 'Create page' - end - - page.within '.wiki-form' do - fill_in :wiki_content, with: wiki_content - click_on "Preview" - end + create_wiki_page('a page/b page/c page/d page', content: wiki_content) expect(page).to have_content("regular link") @@ -73,16 +53,7 @@ describe 'Projects > Wiki > User previews markdown changes', :js do context "when there are hyphens in the page name" do it "rewrites relative links as expected" do - click_link 'New page' - page.within '#modal-new-wiki' do - fill_in :new_wiki_path, with: 'a-page/b-page/c-page/d-page' - click_button 'Create page' - end - - page.within '.wiki-form' do - fill_in :wiki_content, with: wiki_content - click_on "Preview" - end + create_wiki_page('a-page/b-page/c-page/d-page', content: wiki_content) expect(page).to have_content("regular link") @@ -96,23 +67,9 @@ describe 'Projects > Wiki > User previews markdown changes', :js do end context "while editing a wiki page" do - def create_wiki_page(path) - find('.add-new-wiki').click - - page.within '#modal-new-wiki' do - fill_in :new_wiki_path, with: path - click_button 'Create page' - end - - page.within '.wiki-form' do - fill_in :wiki_content, with: 'content' - click_on "Create page" - end - end - context "when there are no spaces or hyphens in the page name" do it "rewrites relative links as expected" do - create_wiki_page 'a/b/c/d' + create_wiki_page('a/b/c/d') click_link 'Edit' fill_in :wiki_content, with: wiki_content @@ -130,7 +87,7 @@ describe 'Projects > Wiki > User previews markdown changes', :js do context "when there are spaces in the page name" do it "rewrites relative links as expected" do - create_wiki_page 'a page/b page/c page/d page' + create_wiki_page('a page/b page/c page/d page') click_link 'Edit' fill_in :wiki_content, with: wiki_content @@ -148,7 +105,7 @@ describe 'Projects > Wiki > User previews markdown changes', :js do context "when there are hyphens in the page name" do it "rewrites relative links as expected" do - create_wiki_page 'a-page/b-page/c-page/d-page' + create_wiki_page('a-page/b-page/c-page/d-page') click_link 'Edit' fill_in :wiki_content, with: wiki_content @@ -166,7 +123,7 @@ describe 'Projects > Wiki > User previews markdown changes', :js do context 'when rendering the preview' do it 'renders content with CommonMark' do - create_wiki_page 'a-page/b-page/c-page/common-mark' + create_wiki_page('a-page/b-page/c-page/common-mark') click_link 'Edit' fill_in :wiki_content, with: "1. one\n - sublist\n" @@ -180,25 +137,31 @@ describe 'Projects > Wiki > User previews markdown changes', :js do end it "does not linkify double brackets inside code blocks as expected" do - click_link 'New page' - page.within '#modal-new-wiki' do - fill_in :new_wiki_path, with: 'linkify_test' - click_button 'Create page' - end + wiki_content = <<-HEREDOC + `[[do_not_linkify]]` + ``` + [[also_do_not_linkify]] + ``` + HEREDOC - page.within '.wiki-form' do - fill_in :wiki_content, with: <<-HEREDOC - `[[do_not_linkify]]` - ``` - [[also_do_not_linkify]] - ``` - HEREDOC - click_on "Preview" - end + create_wiki_page('linkify_test', wiki_content) expect(page).to have_content("do_not_linkify") expect(page.html).to include('[[do_not_linkify]]') expect(page.html).to include('[[also_do_not_linkify]]') end + + private + + def create_wiki_page(path, content = 'content') + visit project_wiki_path(project, wiki_page) + + click_link 'New page' + + fill_in :wiki_title, with: path + fill_in :wiki_content, with: content + + click_button 'Create page' + end end diff --git a/spec/features/projects/wiki/user_creates_wiki_page_spec.rb b/spec/features/projects/wiki/user_creates_wiki_page_spec.rb index cc6dbaa6eb8..56d0518015d 100644 --- a/spec/features/projects/wiki/user_creates_wiki_page_spec.rb +++ b/spec/features/projects/wiki/user_creates_wiki_page_spec.rb @@ -42,10 +42,10 @@ describe "User creates wiki page" do click_link("link test") - expect(page).to have_content("Create Page") + expect(page).to have_content("Create New Page") end - it "shows non-escaped link in the pages list", :js, :quarantine do + it "shows non-escaped link in the pages list", :quarantine do fill_in(:wiki_title, with: "one/two/three-test") page.within(".wiki-form") do @@ -58,7 +58,9 @@ describe "User creates wiki page" do expect(page).to have_xpath("//a[@href='/#{project.full_path}/wikis/one/two/three-test']") end - it "has `Create home` as a commit message" do + it "has `Create home` as a commit message", :js do + wait_for_requests + expect(page).to have_field("wiki[message]", with: "Create home") end @@ -81,7 +83,7 @@ describe "User creates wiki page" do expect(current_path).to eq(project_wiki_path(project, "test")) page.within(:css, ".nav-text") do - expect(page).to have_content("test").and have_content("Create Page") + expect(page).to have_content("Create New Page") end click_link("Home") @@ -93,7 +95,7 @@ describe "User creates wiki page" do expect(current_path).to eq(project_wiki_path(project, "api")) page.within(:css, ".nav-text") do - expect(page).to have_content("Create").and have_content("api") + expect(page).to have_content("Create") end click_link("Home") @@ -105,7 +107,7 @@ describe "User creates wiki page" do expect(current_path).to eq(project_wiki_path(project, "raketasks")) page.within(:css, ".nav-text") do - expect(page).to have_content("Create").and have_content("rake") + expect(page).to have_content("Create") end end @@ -150,6 +152,8 @@ describe "User creates wiki page" do let(:project) { create(:project, :wiki_repo, namespace: create(:group, :public)) } it "has `Create home` as a commit message" do + wait_for_requests + expect(page).to have_field("wiki[message]", with: "Create home") end @@ -181,20 +185,15 @@ describe "User creates wiki page" do it "creates a page with a single word" do click_link("New page") - page.within("#modal-new-wiki") do - fill_in(:new_wiki_path, with: "foo") - - click_button("Create page") + page.within(".wiki-form") do + fill_in(:wiki_title, with: "foo") + fill_in(:wiki_content, with: "My awesome wiki!") end # Commit message field should have correct value. expect(page).to have_field("wiki[message]", with: "Create foo") - page.within(".wiki-form") do - fill_in(:wiki_content, with: "My awesome wiki!") - - click_button("Create page") - end + click_button("Create page") expect(page).to have_content("foo") .and have_content("Last edited by #{user.name}") @@ -204,20 +203,15 @@ describe "User creates wiki page" do it "creates a page with spaces in the name" do click_link("New page") - page.within("#modal-new-wiki") do - fill_in(:new_wiki_path, with: "Spaces in the name") - - click_button("Create page") + page.within(".wiki-form") do + fill_in(:wiki_title, with: "Spaces in the name") + fill_in(:wiki_content, with: "My awesome wiki!") end # Commit message field should have correct value. expect(page).to have_field("wiki[message]", with: "Create Spaces in the name") - page.within(".wiki-form") do - fill_in(:wiki_content, with: "My awesome wiki!") - - click_button("Create page") - end + click_button("Create page") expect(page).to have_content("Spaces in the name") .and have_content("Last edited by #{user.name}") @@ -227,10 +221,9 @@ describe "User creates wiki page" do it "creates a page with hyphens in the name" do click_link("New page") - page.within("#modal-new-wiki") do - fill_in(:new_wiki_path, with: "hyphens-in-the-name") - - click_button("Create page") + page.within(".wiki-form") do + fill_in(:wiki_title, with: "hyphens-in-the-name") + fill_in(:wiki_content, with: "My awesome wiki!") end # Commit message field should have correct value. @@ -251,12 +244,6 @@ describe "User creates wiki page" do it "shows the emoji autocompletion dropdown" do click_link("New page") - page.within("#modal-new-wiki") do - fill_in(:new_wiki_path, with: "test-autocomplete") - - click_button("Create page") - end - page.within(".wiki-form") do find("#wiki_content").native.send_keys("") @@ -274,20 +261,15 @@ describe "User creates wiki page" do it "creates a page" do click_link("New page") - page.within("#modal-new-wiki") do - fill_in(:new_wiki_path, with: "foo") - - click_button("Create page") + page.within(".wiki-form") do + fill_in(:wiki_title, with: "foo") + fill_in(:wiki_content, with: "My awesome wiki!") end # Commit message field should have correct value. expect(page).to have_field("wiki[message]", with: "Create foo") - page.within(".wiki-form") do - fill_in(:wiki_content, with: "My awesome wiki!") - - click_button("Create page") - end + click_button("Create page") expect(page).to have_content("foo") .and have_content("Last edited by #{user.name}") diff --git a/spec/features/projects/wiki/user_updates_wiki_page_spec.rb b/spec/features/projects/wiki/user_updates_wiki_page_spec.rb index 2aab8fda62d..3f3711f9eb8 100644 --- a/spec/features/projects/wiki/user_updates_wiki_page_spec.rb +++ b/spec/features/projects/wiki/user_updates_wiki_page_spec.rb @@ -70,7 +70,7 @@ describe 'User updates wiki page' do context 'in a user namespace' do let(:project) { create(:project, :wiki_repo) } - it 'updates a page' do + it 'updates a page', :js do # Commit message field should have correct value. expect(page).to have_field('wiki[message]', with: 'Update home') @@ -82,6 +82,18 @@ describe 'User updates wiki page' do expect(page).to have_content('My awesome wiki!') end + it 'updates the commit message as the title is changed', :js do + fill_in(:wiki_title, with: 'Wiki title') + + expect(page).to have_field('wiki[message]', with: 'Update Wiki title') + end + + it 'does not allow XSS', :js do + fill_in(:wiki_title, with: '<script>') + + expect(page).to have_field('wiki[message]', with: 'Update <script>') + end + it 'shows a validation error message' do fill_in(:wiki_content, with: '') click_button('Save changes') @@ -129,7 +141,7 @@ describe 'User updates wiki page' do context 'in a group namespace' do let(:project) { create(:project, :wiki_repo, namespace: create(:group, :public)) } - it 'updates a page' do + it 'updates a page', :js do # Commit message field should have correct value. expect(page).to have_field('wiki[message]', with: 'Update home') diff --git a/spec/features/projects/wiki/user_views_wiki_page_spec.rb b/spec/features/projects/wiki/user_views_wiki_page_spec.rb index 05742b63c43..77e725e7f11 100644 --- a/spec/features/projects/wiki/user_views_wiki_page_spec.rb +++ b/spec/features/projects/wiki/user_views_wiki_page_spec.rb @@ -101,8 +101,7 @@ describe 'User views a wiki page' do click_on('image') expect(current_path).to match("wikis/#{path}") - expect(page).to have_content('New Wiki Page') - expect(page).to have_content('Create page') + expect(page).to have_content('Create New Page') end end @@ -156,6 +155,6 @@ describe 'User views a wiki page' do find('.shortcuts-wiki').click click_link "Create your first page" - expect(page).to have_content('Home · Create Page') + expect(page).to have_content('Create New Page') end end diff --git a/spec/features/search/user_searches_for_code_spec.rb b/spec/features/search/user_searches_for_code_spec.rb index 5a60991c1bf..4ca79ccaea8 100644 --- a/spec/features/search/user_searches_for_code_spec.rb +++ b/spec/features/search/user_searches_for_code_spec.rb @@ -39,17 +39,16 @@ describe 'User searches for code' do context 'when on a project page', :js do before do visit(search_path) - end - - include_examples 'top right search form' - - it 'finds code' do find('.js-search-project-dropdown').click page.within('.project-filter') do click_link(project.full_name) end + end + include_examples 'top right search form' + + it 'finds code' do fill_in('dashboard_search', with: 'rspec') find('.btn-search').click @@ -57,6 +56,27 @@ describe 'User searches for code' do expect(find(:css, '.search-results')).to have_content('Update capybara, rspec-rails, poltergeist to recent versions') end end + + it 'search mutiple words with refs switching' do + expected_result = 'Use `snake_case` for naming files' + search = 'for naming files' + + fill_in('dashboard_search', with: search) + find('.btn-search').click + + page.within('.results') do + expect(find('.search-results')).to have_content(expected_result) + end + + find('.js-project-refs-dropdown').click + find('.dropdown-page-one .dropdown-content').click_link('v1.0.0') + + page.within('.results') do + expect(find(:css, '.search-results')).to have_content(expected_result) + end + + expect(find_field('dashboard_search').value).to eq(search) + end end context 'search code within refs', :js do diff --git a/spec/features/search/user_searches_for_users_spec.rb b/spec/features/search/user_searches_for_users_spec.rb index 2517a843c62..e10c1afc0b8 100644 --- a/spec/features/search/user_searches_for_users_spec.rb +++ b/spec/features/search/user_searches_for_users_spec.rb @@ -4,7 +4,7 @@ require 'spec_helper' describe 'User searches for users' do context 'when on the dashboard' do - it 'finds the user' do + it 'finds the user', :js do create(:user, username: 'gob_bluth', name: 'Gob Bluth') sign_in(create(:user)) @@ -12,7 +12,7 @@ describe 'User searches for users' do visit dashboard_projects_path fill_in 'search', with: 'gob' - click_button 'Go' + find('#search').send_keys(:enter) expect(page).to have_content('Users 1') diff --git a/spec/features/search/user_uses_header_search_field_spec.rb b/spec/features/search/user_uses_header_search_field_spec.rb index c781048d06d..5006631cc14 100644 --- a/spec/features/search/user_uses_header_search_field_spec.rb +++ b/spec/features/search/user_uses_header_search_field_spec.rb @@ -96,6 +96,23 @@ describe 'User uses header search field', :js do let(:url) { root_path } let(:scope_name) { 'All GitLab' } end + + context 'when searching through the search field' do + before do + create(:issue, project: project, title: 'project issue') + + fill_in('search', with: 'project') + find('#search').send_keys(:enter) + end + + it 'displays result counts for all categories' do + expect(page).to have_content('Projects 1') + expect(page).to have_content('Issues 1') + expect(page).to have_content('Merge requests 0') + expect(page).to have_content('Milestones 0') + expect(page).to have_content('Users 0') + end + end end context 'when user is in a project scope' do diff --git a/spec/features/signed_commits_spec.rb b/spec/features/signed_commits_spec.rb index e2b3444272e..70e6978a7b6 100644 --- a/spec/features/signed_commits_spec.rb +++ b/spec/features/signed_commits_spec.rb @@ -15,8 +15,8 @@ describe 'GPG signed commits' do visit project_commit_path(project, ref) - expect(page).to have_link 'Unverified' - expect(page).not_to have_link 'Verified' + expect(page).to have_button 'Unverified' + expect(page).not_to have_button 'Verified' # user changes his email which makes the gpg key verified perform_enqueued_jobs do @@ -26,8 +26,8 @@ describe 'GPG signed commits' do visit project_commit_path(project, ref) - expect(page).not_to have_link 'Unverified' - expect(page).to have_link 'Verified' + expect(page).not_to have_button 'Unverified' + expect(page).to have_button 'Verified' end it 'changes from unverified to verified when the user adds the missing gpg key' do @@ -36,8 +36,8 @@ describe 'GPG signed commits' do visit project_commit_path(project, ref) - expect(page).to have_link 'Unverified' - expect(page).not_to have_link 'Verified' + expect(page).to have_button 'Unverified' + expect(page).not_to have_button 'Verified' # user adds the gpg key which makes the signature valid perform_enqueued_jobs do @@ -46,8 +46,8 @@ describe 'GPG signed commits' do visit project_commit_path(project, ref) - expect(page).not_to have_link 'Unverified' - expect(page).to have_link 'Verified' + expect(page).not_to have_button 'Unverified' + expect(page).to have_button 'Verified' end context 'shows popover badges', :js do @@ -136,7 +136,7 @@ describe 'GPG signed commits' do visit project_commit_path(project, GpgHelpers::SIGNED_AND_AUTHORED_SHA) # wait for the signature to get generated - expect(page).to have_link 'Verified' + expect(page).to have_button 'Verified' user_1.destroy! diff --git a/spec/features/users/login_spec.rb b/spec/features/users/login_spec.rb index dac8c8e7a29..8e4db2ca840 100644 --- a/spec/features/users/login_spec.rb +++ b/spec/features/users/login_spec.rb @@ -95,6 +95,42 @@ describe 'Login' do end end + describe 'with an unconfirmed email address' do + let!(:user) { create(:user, confirmed_at: nil) } + let(:grace_period) { 2.days } + + before do + stub_application_setting(send_user_confirmation_email: true) + allow(User).to receive(:allow_unconfirmed_access_for).and_return grace_period + end + + context 'within the grace period' do + it 'allows to login' do + expect(authentication_metrics).to increment(:user_authenticated_counter) + + gitlab_sign_in(user) + + expect(page).not_to have_content('You have to confirm your email address before continuing.') + expect(page).not_to have_link('Resend confirmation email', href: new_user_confirmation_path) + end + end + + context 'when the confirmation grace period is expired' do + it 'prevents the user from logging in and renders a resend confirmation email link' do + travel_to((grace_period + 1.day).from_now) do + expect(authentication_metrics) + .to increment(:user_unauthenticated_counter) + .and increment(:user_session_destroyed_counter).twice + + gitlab_sign_in(user) + + expect(page).to have_content('You have to confirm your email address before continuing.') + expect(page).to have_link('Resend confirmation email', href: new_user_confirmation_path) + end + end + end + end + describe 'with the ghost user' do it 'disallows login' do expect(authentication_metrics) @@ -745,4 +781,39 @@ describe 'Login' do end end end + + context 'when sending confirmation email and not yet confirmed' do + let!(:user) { create(:user, confirmed_at: nil) } + let(:grace_period) { 2.days } + + before do + stub_application_setting(send_user_confirmation_email: true) + stub_feature_flags(soft_email_confirmation: true) + allow(User).to receive(:allow_unconfirmed_access_for).and_return grace_period + end + + it 'allows login and shows a flash warning to confirm the email address' do + expect(authentication_metrics).to increment(:user_authenticated_counter) + + gitlab_sign_in(user) + + expect(current_path).to eq root_path + expect(page).to have_content("Please check your email (#{user.email}) to verify that you own this address.") + end + + context "when not having confirmed within Devise's allow_unconfirmed_access_for time" do + it 'does not allow login and shows a flash alert to confirm the email address' do + travel_to((grace_period + 1.day).from_now) do + expect(authentication_metrics) + .to increment(:user_unauthenticated_counter) + .and increment(:user_session_destroyed_counter).twice + + gitlab_sign_in(user) + + expect(current_path).to eq new_user_session_path + expect(page).to have_content('You have to confirm your email address before continuing.') + end + end + end + end end diff --git a/spec/features/users/signup_spec.rb b/spec/features/users/signup_spec.rb index f5897bffaf0..fb927a9ca3b 100644 --- a/spec/features/users/signup_spec.rb +++ b/spec/features/users/signup_spec.rb @@ -5,6 +5,10 @@ require 'spec_helper' describe 'Signup' do include TermsHelper + before do + stub_feature_flags(invisible_captcha: false) + end + let(:new_user) { build_stubbed(:user) } describe 'username validation', :js do @@ -162,24 +166,51 @@ describe 'Signup' do end context 'with no errors' do - context "when sending confirmation email" do + context 'when sending confirmation email' do before do stub_application_setting(send_user_confirmation_email: true) end - it 'creates the user account and sends a confirmation email' do - visit root_path + context 'when soft email confirmation is not enabled' do + before do + stub_feature_flags(soft_email_confirmation: false) + end - fill_in 'new_user_name', with: new_user.name - fill_in 'new_user_username', with: new_user.username - fill_in 'new_user_email', with: new_user.email - fill_in 'new_user_email_confirmation', with: new_user.email - fill_in 'new_user_password', with: new_user.password + it 'creates the user account and sends a confirmation email' do + visit root_path + + fill_in 'new_user_name', with: new_user.name + fill_in 'new_user_username', with: new_user.username + fill_in 'new_user_email', with: new_user.email + fill_in 'new_user_email_confirmation', with: new_user.email + fill_in 'new_user_password', with: new_user.password + + expect { click_button 'Register' }.to change { User.count }.by(1) + + expect(current_path).to eq users_almost_there_path + expect(page).to have_content('Please check your email to confirm your account') + end + end + + context 'when soft email confirmation is enabled' do + before do + stub_feature_flags(soft_email_confirmation: true) + end + + it 'creates the user account and sends a confirmation email' do + visit root_path + + fill_in 'new_user_name', with: new_user.name + fill_in 'new_user_username', with: new_user.username + fill_in 'new_user_email', with: new_user.email + fill_in 'new_user_email_confirmation', with: new_user.email + fill_in 'new_user_password', with: new_user.password - expect { click_button 'Register' }.to change { User.count }.by(1) + expect { click_button 'Register' }.to change { User.count }.by(1) - expect(current_path).to eq users_almost_there_path - expect(page).to have_content("Please check your email to confirm your account") + expect(current_path).to eq dashboard_projects_path + expect(page).to have_content("Please check your email (#{new_user.email}) to verify that you own this address.") + end end end diff --git a/spec/finders/award_emojis_finder_spec.rb b/spec/finders/award_emojis_finder_spec.rb new file mode 100644 index 00000000000..ccac475daad --- /dev/null +++ b/spec/finders/award_emojis_finder_spec.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe AwardEmojisFinder do + set(:issue_1) { create(:issue) } + set(:issue_1_thumbsup) { create(:award_emoji, name: 'thumbsup', awardable: issue_1) } + set(:issue_1_thumbsdown) { create(:award_emoji, name: 'thumbsdown', awardable: issue_1) } + # Create a matching set of emoji for a second issue. + # These should never appear in our finder results + set(:issue_2) { create(:issue) } + set(:issue_2_thumbsup) { create(:award_emoji, name: 'thumbsup', awardable: issue_2) } + set(:issue_2_thumbsdown) { create(:award_emoji, name: 'thumbsdown', awardable: issue_2) } + + describe 'param validation' do + it 'raises an error if `name` is invalid' do + expect { described_class.new(issue_1, { name: 'invalid' }).execute }.to raise_error( + ArgumentError, + 'Invalid name param' + ) + end + + it 'raises an error if `awarded_by` is invalid' do + expectation = [ArgumentError, 'Invalid awarded_by param'] + + expect { described_class.new(issue_1, { awarded_by: issue_2 }).execute }.to raise_error(*expectation) + expect { described_class.new(issue_1, { awarded_by: 'not-an-id' }).execute }.to raise_error(*expectation) + expect { described_class.new(issue_1, { awarded_by: 1.123 }).execute }.to raise_error(*expectation) + end + end + + describe '#execute' do + it 'scopes to the awardable' do + expect(described_class.new(issue_1).execute).to contain_exactly( + issue_1_thumbsup, issue_1_thumbsdown + ) + end + + it 'filters by emoji name' do + expect(described_class.new(issue_1, { name: 'thumbsup' }).execute).to contain_exactly(issue_1_thumbsup) + expect(described_class.new(issue_1, { name: '8ball' }).execute).to be_empty + end + + it 'filters by user' do + expect(described_class.new(issue_1, { awarded_by: issue_1_thumbsup.user }).execute).to contain_exactly(issue_1_thumbsup) + expect(described_class.new(issue_1, { awarded_by: issue_2_thumbsup.user }).execute).to be_empty + end + end +end diff --git a/spec/fixtures/api/schemas/deployment.json b/spec/fixtures/api/schemas/deployment.json index 0828f113495..fe725b97c21 100644 --- a/spec/fixtures/api/schemas/deployment.json +++ b/spec/fixtures/api/schemas/deployment.json @@ -3,6 +3,7 @@ "required": [ "sha", "created_at", + "deployed_at", "iid", "tag", "last?", @@ -11,6 +12,7 @@ ], "properties": { "created_at": { "type": "string" }, + "deployed_at": { "type": ["string", "null"] }, "id": { "type": "integer" }, "iid": { "type": "integer" }, "last?": { "type": "boolean" }, diff --git a/spec/fixtures/api/schemas/entities/merge_request_sidebar.json b/spec/fixtures/api/schemas/entities/merge_request_sidebar.json index 214b67a9a0f..9945de8a856 100644 --- a/spec/fixtures/api/schemas/entities/merge_request_sidebar.json +++ b/spec/fixtures/api/schemas/entities/merge_request_sidebar.json @@ -2,6 +2,7 @@ "type": "object", "properties" : { "id": { "type": "integer" }, + "iid": { "type": "integer" }, "type": { "type": "string" }, "author_id": { "type": "integer" }, "project_id": { "type": "integer" }, diff --git a/spec/fixtures/security-reports/dependency_list/gl-dependency-scanning-report.json b/spec/fixtures/security-reports/dependency_list/gl-dependency-scanning-report.json deleted file mode 100644 index 8fb66f6652b..00000000000 --- a/spec/fixtures/security-reports/dependency_list/gl-dependency-scanning-report.json +++ /dev/null @@ -1,422 +0,0 @@ -{ - "version": "2.1", - "vulnerabilities": [ - { - "category": "dependency_scanning", - "name": "Vulnerabilities in libxml2", - "message": "Vulnerabilities in libxml2 in nokogiri", - "description": " The version of libxml2 packaged with Nokogiri contains several vulnerabilities.\r\n Nokogiri has mitigated these issues by upgrading to libxml 2.9.5.\r\n\r\n It was discovered that a type confusion error existed in libxml2. An\r\n attacker could use this to specially construct XML data that\r\n could cause a denial of service or possibly execute arbitrary\r\n code. (CVE-2017-0663)\r\n\r\n It was discovered that libxml2 did not properly validate parsed entity\r\n references. An attacker could use this to specially construct XML\r\n data that could expose sensitive information. (CVE-2017-7375)\r\n\r\n It was discovered that a buffer overflow existed in libxml2 when\r\n handling HTTP redirects. An attacker could use this to specially\r\n construct XML data that could cause a denial of service or possibly\r\n execute arbitrary code. (CVE-2017-7376)\r\n\r\n Marcel Böhme and Van-Thuan Pham discovered a buffer overflow in\r\n libxml2 when handling elements. An attacker could use this to specially\r\n construct XML data that could cause a denial of service or possibly\r\n execute arbitrary code. (CVE-2017-9047)\r\n\r\n Marcel Böhme and Van-Thuan Pham discovered a buffer overread\r\n in libxml2 when handling elements. An attacker could use this\r\n to specially construct XML data that could cause a denial of\r\n service. (CVE-2017-9048)\r\n\r\n Marcel Böhme and Van-Thuan Pham discovered multiple buffer overreads\r\n in libxml2 when handling parameter-entity references. An attacker\r\n could use these to specially construct XML data that could cause a\r\n denial of service. (CVE-2017-9049, CVE-2017-9050)", - "cve": "rails/Gemfile.lock:nokogiri:gemnasium:06565b64-486d-4326-b906-890d9915804d", - "severity": "High", - "solution": "Upgrade to latest version.", - "scanner": { - "id": "gemnasium", - "name": "Gemnasium" - }, - "location": { - "file": "rails/Gemfile.lock", - "dependency": { - "package": { - "name": "nokogiri" - }, - "version": "1.8.0" - } - }, - "identifiers": [ - { - "type": "gemnasium", - "name": "Gemnasium-06565b64-486d-4326-b906-890d9915804d", - "value": "06565b64-486d-4326-b906-890d9915804d", - "url": "https://deps.sec.gitlab.com/packages/gem/nokogiri/versions/1.8.0/advisories" - }, - { - "type": "usn", - "name": "USN-3424-1", - "value": "USN-3424-1", - "url": "https://usn.ubuntu.com/3424-1/" - } - ], - "links": [ - { - "url": "https://github.com/sparklemotion/nokogiri/issues/1673" - } - ] - }, - { - "category": "dependency_scanning", - "name": "Infinite recursion in parameter entities", - "message": "Infinite recursion in parameter entities in nokogiri", - "description": "libxml2 incorrectly handles certain parameter entities. An attacker can leverage this with specially constructed XML data to cause libxml2 to consume resources, leading to a denial of service.", - "cve": "rails/Gemfile.lock:nokogiri:gemnasium:6a0d56f6-2441-492a-9b14-edb95ac31919", - "severity": "High", - "solution": "Upgrade to latest version.", - "scanner": { - "id": "gemnasium", - "name": "Gemnasium" - }, - "location": { - "file": "rails/Gemfile.lock", - "dependency": { - "package": { - "name": "nokogiri" - }, - "version": "1.8.0" - } - }, - "identifiers": [ - { - "type": "gemnasium", - "name": "Gemnasium-6a0d56f6-2441-492a-9b14-edb95ac31919", - "value": "6a0d56f6-2441-492a-9b14-edb95ac31919", - "url": "https://deps.sec.gitlab.com/packages/gem/nokogiri/versions/1.8.0/advisories" - }, - { - "type": "cve", - "name": "CVE-2017-16932", - "value": "CVE-2017-16932", - "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-16932" - } - ], - "links": [ - { - "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-16932" - }, - { - "url": "https://github.com/sparklemotion/nokogiri/issues/1714" - }, - { - "url": "https://people.canonical.com/~ubuntu-security/cve/2017/CVE-2017-16932.html" - }, - { - "url": "https://usn.ubuntu.com/usn/usn-3504-1/" - } - ] - }, - { - "category": "dependency_scanning", - "name": "Denial of Service", - "message": "Denial of Service in nokogiri", - "description": "libxml2 incorrectly handles certain files. An attacker can use this issue with specially constructed XML data to cause libxml2 to consume resources, leading to a denial of service.\r\n\r\n", - "cve": "rails/Gemfile.lock:nokogiri:gemnasium:78658378-bd8f-4d79-81c8-07c419302426", - "severity": "Unknown", - "solution": "Upgrade to latest version.", - "scanner": { - "id": "gemnasium", - "name": "Gemnasium" - }, - "location": { - "file": "rails/Gemfile.lock", - "dependency": { - "package": { - "name": "nokogiri" - }, - "version": "1.8.0" - } - }, - "identifiers": [ - { - "type": "gemnasium", - "name": "Gemnasium-78658378-bd8f-4d79-81c8-07c419302426", - "value": "78658378-bd8f-4d79-81c8-07c419302426", - "url": "https://deps.sec.gitlab.com/packages/gem/nokogiri/versions/1.8.0/advisories" - }, - { - "type": "cve", - "name": "CVE-2017-15412", - "value": "CVE-2017-15412", - "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-15412" - } - ], - "links": [ - { - "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-15412" - }, - { - "url": "https://github.com/sparklemotion/nokogiri/issues/1714" - }, - { - "url": "https://people.canonical.com/~ubuntu-security/cve/2017/CVE-2017-15412.html" - } - ] - }, - { - "category": "dependency_scanning", - "name": "Bypass of a protection mechanism in libxslt", - "message": "Bypass of a protection mechanism in libxslt in nokogiri", - "description": "libxslt through 1.1.33 allows bypass of a protection mechanism because callers of xsltCheckRead and xsltCheckWrite permit access even upon receiving a -1 error code. xsltCheckRead can return -1 for a crafted URL that is not actually invalid and is subsequently loaded. Vendored version of libxslt has been patched to remediate this vulnerability. Note that this patch is not yet (as of 2019-04-22) in an upstream release of libxslt.", - "cve": "rails/Gemfile.lock:nokogiri:gemnasium:1a2e2e6e-67ba-4142-bfa1-3391f5416e4c", - "severity": "Unknown", - "solution": "Upgrade to latest version if using vendored version of libxslt OR update the system library libxslt to a fixed version", - "scanner": { - "id": "gemnasium", - "name": "Gemnasium" - }, - "location": { - "file": "rails/Gemfile.lock", - "dependency": { - "package": { - "name": "nokogiri" - }, - "version": "1.8.0" - } - }, - "identifiers": [ - { - "type": "gemnasium", - "name": "Gemnasium-1a2e2e6e-67ba-4142-bfa1-3391f5416e4c", - "value": "1a2e2e6e-67ba-4142-bfa1-3391f5416e4c", - "url": "https://deps.sec.gitlab.com/packages/gem/nokogiri/versions/1.8.0/advisories" - }, - { - "type": "cve", - "name": "CVE-2019-11068", - "value": "CVE-2019-11068", - "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-11068" - } - ], - "links": [ - { - "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-11068" - }, - { - "url": "https://github.com/sparklemotion/nokogiri/issues/1892" - }, - { - "url": "https://people.canonical.com/~ubuntu-security/cve/CVE-2019-11068" - }, - { - "url": "https://security-tracker.debian.org/tracker/CVE-2019-11068" - } - ] - }, - { - "category": "dependency_scanning", - "name": "Regular Expression Denial of Service", - "message": "Regular Expression Denial of Service in debug", - "description": "The debug module is vulnerable to regular expression denial of service when untrusted user input is passed into the `o` formatter. It takes around 50k characters to block for 2 seconds making this a low severity issue.", - "cve": "yarn/yarn.lock:debug:gemnasium:37283ed4-0380-40d7-ada7-2d994afcc62a", - "severity": "Unknown", - "solution": "Upgrade to latest versions.", - "scanner": { - "id": "gemnasium", - "name": "Gemnasium" - }, - "location": { - "file": "yarn/yarn.lock", - "dependency": { - "package": { - "name": "debug" - }, - "version": "1.0.5" - } - }, - "identifiers": [ - { - "type": "gemnasium", - "name": "Gemnasium-37283ed4-0380-40d7-ada7-2d994afcc62a", - "value": "37283ed4-0380-40d7-ada7-2d994afcc62a", - "url": "https://deps.sec.gitlab.com/packages/npm/debug/versions/1.0.5/advisories" - } - ], - "links": [ - { - "url": "https://github.com/visionmedia/debug/issues/501" - }, - { - "url": "https://github.com/visionmedia/debug/pull/504" - }, - { - "url": "https://nodesecurity.io/advisories/534" - } - ] - }, - { - "category": "dependency_scanning", - "name": "Authentication bypass via incorrect DOM traversal and canonicalization", - "message": "Authentication bypass via incorrect DOM traversal and canonicalization in saml2-js", - "description": "Some XML DOM traversal and canonicalization APIs may be inconsistent in handling of comments within XML nodes. Incorrect use of these APIs by some SAML libraries results in incorrect parsing of the inner text of XML nodes such that any inner text after the comment is lost prior to cryptographically signing the SAML message. Text after the comment therefore has no impact on the signature on the SAML message.\r\n\r\nA remote attacker can modify SAML content for a SAML service provider without invalidating the cryptographic signature, which may allow attackers to bypass primary authentication for the affected SAML service provider.", - "cve": "yarn/yarn.lock:saml2-js:gemnasium:9952e574-7b5b-46fa-a270-aeb694198a98", - "severity": "Unknown", - "solution": "Upgrade to fixed version.\r\n", - "scanner": { - "id": "gemnasium", - "name": "Gemnasium" - }, - "location": { - "file": "yarn/yarn.lock", - "dependency": { - "package": { - "name": "saml2-js" - }, - "version": "1.5.0" - } - }, - "identifiers": [ - { - "type": "gemnasium", - "name": "Gemnasium-9952e574-7b5b-46fa-a270-aeb694198a98", - "value": "9952e574-7b5b-46fa-a270-aeb694198a98", - "url": "https://deps.sec.gitlab.com/packages/npm/saml2-js/versions/1.5.0/advisories" - }, - { - "type": "cve", - "name": "CVE-2017-11429", - "value": "CVE-2017-11429", - "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-11429" - } - ], - "links": [ - { - "url": "https://github.com/Clever/saml2/commit/3546cb61fd541f219abda364c5b919633609ef3d#diff-af730f9f738de1c9ad87596df3f6de84R279" - }, - { - "url": "https://github.com/Clever/saml2/issues/127" - }, - { - "url": "https://www.kb.cert.org/vuls/id/475445" - } - ] - } - ], - "remediations": [], - "dependency_files": [ - { - "path": "rails/Gemfile.lock", - "package_manager": "bundler", - "dependencies": [ - { - "package": { - "name": "mini_portile2" - }, - "version": "2.2.0" - }, - { - "package": { - "name": "nokogiri" - }, - "version": "1.8.0" - } - ] - }, - { - "path": "yarn/yarn.lock", - "package_manager": "yarn", - "dependencies": [ - { - "package": { - "name": "async" - }, - "version": "0.2.10" - }, - { - "package": { - "name": "async" - }, - "version": "1.5.2" - }, - { - "package": { - "name": "debug" - }, - "version": "1.0.5" - }, - { - "package": { - "name": "ejs" - }, - "version": "0.8.8" - }, - { - "package": { - "name": "ms" - }, - "version": "2.0.0" - }, - { - "package": { - "name": "node-forge" - }, - "version": "0.2.24" - }, - { - "package": { - "name": "saml2-js" - }, - "version": "1.5.0" - }, - { - "package": { - "name": "sax" - }, - "version": "1.2.4" - }, - { - "package": { - "name": "underscore" - }, - "version": "1.9.1" - }, - { - "package": { - "name": "underscore" - }, - "version": "1.6.0" - }, - { - "package": { - "name": "xml-crypto" - }, - "version": "0.8.5" - }, - { - "package": { - "name": "xml-encryption" - }, - "version": "0.7.4" - }, - { - "package": { - "name": "xml2js" - }, - "version": "0.4.19" - }, - { - "package": { - "name": "xmlbuilder" - }, - "version": "2.1.0" - }, - { - "package": { - "name": "xmlbuilder" - }, - "version": "9.0.7" - }, - { - "package": { - "name": "xmldom" - }, - "version": "0.1.19" - }, - { - "package": { - "name": "xmldom" - }, - "version": "0.1.27" - }, - { - "package": { - "name": "xpath.js" - }, - "version": "1.1.0" - }, - { - "package": { - "name": "xpath" - }, - "version": "0.0.5" - } - ] - } - ] -} diff --git a/spec/fixtures/security-reports/deprecated/gl-dependency-scanning-report.json b/spec/fixtures/security-reports/deprecated/gl-dependency-scanning-report.json deleted file mode 100644 index ce66f562175..00000000000 --- a/spec/fixtures/security-reports/deprecated/gl-dependency-scanning-report.json +++ /dev/null @@ -1,178 +0,0 @@ -[ - { - "category": "dependency_scanning", - "name": "io.netty/netty - CVE-2014-3488", - "message": "DoS by CPU exhaustion when using malicious SSL packets", - "cve": "app/pom.xml:io.netty/netty@3.9.1.Final:CVE-2014-3488", - "severity": "Unknown", - "solution": "Upgrade to the latest version", - "scanner": { - "id": "gemnasium", - "name": "Gemnasium" - }, - "location": { - "file": "app/pom.xml", - "dependency": { - "package": { - "name": "io.netty/netty" - }, - "version": "3.9.1.Final" - } - }, - "identifiers": [ - { - "type": "gemnasium", - "name": "Gemnasium-d1bf36d9-9f07-46cd-9cfc-8675338ada8f", - "value": "d1bf36d9-9f07-46cd-9cfc-8675338ada8f", - "url": "https://deps.sec.gitlab.com/packages/maven/io.netty/netty/versions/3.9.1.Final/advisories" - }, - { - "type": "cve", - "name": "CVE-2014-3488", - "value": "CVE-2014-3488", - "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2014-3488" - } - ], - "links": [ - { - "url": "https://bugzilla.redhat.com/CVE-2014-3488" - }, - { - "url": "http://netty.io/news/2014/06/11/3.html" - }, - { - "url": "https://github.com/netty/netty/issues/2562" - } - ], - "priority": "Unknown", - "file": "app/pom.xml", - "url": "https://bugzilla.redhat.com/CVE-2014-3488", - "tool": "gemnasium" - }, - { - "category": "dependency_scanning", - "name": "Django - CVE-2017-12794", - "message": "Possible XSS in traceback section of technical 500 debug page", - "cve": "app/requirements.txt:Django@1.11.3:CVE-2017-12794", - "severity": "Unknown", - "solution": "Upgrade to latest version or apply patch.", - "scanner": { - "id": "gemnasium", - "name": "Gemnasium" - }, - "location": { - "file": "app/requirements.txt", - "dependency": { - "package": { - "name": "Django" - }, - "version": "1.11.3" - } - }, - "identifiers": [ - { - "type": "gemnasium", - "name": "Gemnasium-6162a015-8635-4a15-8d7c-dc9321db366f", - "value": "6162a015-8635-4a15-8d7c-dc9321db366f", - "url": "https://deps.sec.gitlab.com/packages/pypi/Django/versions/1.11.3/advisories" - }, - { - "type": "cve", - "name": "CVE-2017-12794", - "value": "CVE-2017-12794", - "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-12794" - } - ], - "links": [ - { - "url": "https://www.djangoproject.com/weblog/2017/sep/05/security-releases/" - } - ], - "priority": "Unknown", - "file": "app/requirements.txt", - "url": "https://www.djangoproject.com/weblog/2017/sep/05/security-releases/", - "tool": "gemnasium" - }, - { - "category": "dependency_scanning", - "name": "nokogiri - USN-3424-1", - "message": "Vulnerabilities in libxml2", - "cve": "rails/Gemfile.lock:nokogiri@1.8.0:USN-3424-1", - "severity": "Unknown", - "solution": "Upgrade to latest version.", - "scanner": { - "id": "gemnasium", - "name": "Gemnasium" - }, - "location": { - "file": "rails/Gemfile.lock", - "dependency": { - "package": { - "name": "nokogiri" - }, - "version": "1.8.0" - } - }, - "identifiers": [ - { - "type": "gemnasium", - "name": "Gemnasium-06565b64-486d-4326-b906-890d9915804d", - "value": "06565b64-486d-4326-b906-890d9915804d", - "url": "https://deps.sec.gitlab.com/packages/gem/nokogiri/versions/1.8.0/advisories" - }, - { - "type": "usn", - "name": "USN-3424-1", - "value": "USN-3424-1", - "url": "https://usn.ubuntu.com/3424-1/" - } - ], - "links": [ - { - "url": "https://github.com/sparklemotion/nokogiri/issues/1673" - } - ], - "priority": "Unknown", - "file": "rails/Gemfile.lock", - "url": "https://github.com/sparklemotion/nokogiri/issues/1673", - "tool": "gemnasium" - }, - { - "category": "dependency_scanning", - "name": "ffi - CVE-2018-1000201", - "message": "ruby-ffi DDL loading issue on Windows OS", - "cve": "ffi:1.9.18:CVE-2018-1000201", - "severity": "High", - "solution": "upgrade to \u003e= 1.9.24", - "scanner": { - "id": "bundler_audit", - "name": "bundler-audit" - }, - "location": { - "file": "sast-sample-rails/Gemfile.lock", - "dependency": { - "package": { - "name": "ffi" - }, - "version": "1.9.18" - } - }, - "identifiers": [ - { - "type": "cve", - "name": "CVE-2018-1000201", - "value": "CVE-2018-1000201", - "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2018-1000201" - } - ], - "links": [ - { - "url": "https://github.com/ffi/ffi/releases/tag/1.9.24" - } - ], - "priority": "High", - "file": "sast-sample-rails/Gemfile.lock", - "url": "https://github.com/ffi/ffi/releases/tag/1.9.24", - "tool": "bundler_audit" - } -] diff --git a/spec/fixtures/security-reports/deprecated/gl-sast-report.json b/spec/fixtures/security-reports/deprecated/gl-sast-report.json deleted file mode 100644 index 2f7e47281e2..00000000000 --- a/spec/fixtures/security-reports/deprecated/gl-sast-report.json +++ /dev/null @@ -1,964 +0,0 @@ -[ - { - "category": "sast", - "message": "Probable insecure usage of temp file/directory.", - "cve": "python/hardcoded/hardcoded-tmp.py:52865813c884a507be1f152d654245af34aba8a391626d01f1ab6d3f52ec8779:B108", - "severity": "Medium", - "confidence": "Medium", - "scanner": { - "id": "bandit", - "name": "Bandit" - }, - "location": { - "file": "python/hardcoded/hardcoded-tmp.py", - "start_line": 1, - "end_line": 1 - }, - "identifiers": [ - { - "type": "bandit_test_id", - "name": "Bandit Test ID B108", - "value": "B108", - "url": "https://docs.openstack.org/bandit/latest/plugins/b108_hardcoded_tmp_directory.html" - } - ], - "priority": "Medium", - "file": "python/hardcoded/hardcoded-tmp.py", - "line": 1, - "url": "https://docs.openstack.org/bandit/latest/plugins/b108_hardcoded_tmp_directory.html", - "tool": "bandit" - }, - { - "category": "sast", - "name": "Predictable pseudorandom number generator", - "message": "Predictable pseudorandom number generator", - "cve": "groovy/src/main/java/com/gitlab/security_products/tests/App.groovy:47:PREDICTABLE_RANDOM", - "severity": "Medium", - "confidence": "Medium", - "scanner": { - "id": "find_sec_bugs", - "name": "Find Security Bugs" - }, - "location": { - "file": "groovy/src/main/java/com/gitlab/security_products/tests/App.groovy", - "start_line": 47, - "end_line": 47, - "class": "com.gitlab.security_products.tests.App", - "method": "generateSecretToken2" - }, - "identifiers": [ - { - "type": "find_sec_bugs_type", - "name": "Find Security Bugs-PREDICTABLE_RANDOM", - "value": "PREDICTABLE_RANDOM", - "url": "https://find-sec-bugs.github.io/bugs.htm#PREDICTABLE_RANDOM" - } - ], - "priority": "Medium", - "file": "groovy/src/main/java/com/gitlab/security_products/tests/App.groovy", - "line": 47, - "url": "https://find-sec-bugs.github.io/bugs.htm#PREDICTABLE_RANDOM", - "tool": "find_sec_bugs" - }, - { - "category": "sast", - "name": "Predictable pseudorandom number generator", - "message": "Predictable pseudorandom number generator", - "cve": "groovy/src/main/java/com/gitlab/security_products/tests/App.groovy:41:PREDICTABLE_RANDOM", - "severity": "Medium", - "confidence": "Medium", - "scanner": { - "id": "find_sec_bugs", - "name": "Find Security Bugs" - }, - "location": { - "file": "groovy/src/main/java/com/gitlab/security_products/tests/App.groovy", - "start_line": 41, - "end_line": 41, - "class": "com.gitlab.security_products.tests.App", - "method": "generateSecretToken1" - }, - "identifiers": [ - { - "type": "find_sec_bugs_type", - "name": "Find Security Bugs-PREDICTABLE_RANDOM", - "value": "PREDICTABLE_RANDOM", - "url": "https://find-sec-bugs.github.io/bugs.htm#PREDICTABLE_RANDOM" - } - ], - "priority": "Medium", - "file": "groovy/src/main/java/com/gitlab/security_products/tests/App.groovy", - "line": 41, - "url": "https://find-sec-bugs.github.io/bugs.htm#PREDICTABLE_RANDOM", - "tool": "find_sec_bugs" - }, - { - "category": "sast", - "message": "Use of insecure MD2, MD4, or MD5 hash function.", - "cve": "python/imports/imports-aliases.py:cb203b465dffb0cb3a8e8bd8910b84b93b0a5995a938e4b903dbb0cd6ffa1254:B303", - "severity": "Medium", - "confidence": "High", - "scanner": { - "id": "bandit", - "name": "Bandit" - }, - "location": { - "file": "python/imports/imports-aliases.py", - "start_line": 11, - "end_line": 11 - }, - "identifiers": [ - { - "type": "bandit_test_id", - "name": "Bandit Test ID B303", - "value": "B303" - } - ], - "priority": "Medium", - "file": "python/imports/imports-aliases.py", - "line": 11, - "tool": "bandit" - }, - { - "category": "sast", - "message": "Use of insecure MD2, MD4, or MD5 hash function.", - "cve": "python/imports/imports-aliases.py:a7173c43ae66bd07466632d819d450e0071e02dbf782763640d1092981f9631b:B303", - "severity": "Medium", - "confidence": "High", - "scanner": { - "id": "bandit", - "name": "Bandit" - }, - "location": { - "file": "python/imports/imports-aliases.py", - "start_line": 12, - "end_line": 12 - }, - "identifiers": [ - { - "type": "bandit_test_id", - "name": "Bandit Test ID B303", - "value": "B303" - } - ], - "priority": "Medium", - "file": "python/imports/imports-aliases.py", - "line": 12, - "tool": "bandit" - }, - { - "category": "sast", - "message": "Use of insecure MD2, MD4, or MD5 hash function.", - "cve": "python/imports/imports-aliases.py:017017b77deb0b8369b6065947833eeea752a92ec8a700db590fece3e934cf0d:B303", - "severity": "Medium", - "confidence": "High", - "scanner": { - "id": "bandit", - "name": "Bandit" - }, - "location": { - "file": "python/imports/imports-aliases.py", - "start_line": 13, - "end_line": 13 - }, - "identifiers": [ - { - "type": "bandit_test_id", - "name": "Bandit Test ID B303", - "value": "B303" - } - ], - "priority": "Medium", - "file": "python/imports/imports-aliases.py", - "line": 13, - "tool": "bandit" - }, - { - "category": "sast", - "message": "Use of insecure MD2, MD4, or MD5 hash function.", - "cve": "python/imports/imports-aliases.py:45fc8c53aea7b84f06bc4e590cc667678d6073c4c8a1d471177ca2146fb22db2:B303", - "severity": "Medium", - "confidence": "High", - "scanner": { - "id": "bandit", - "name": "Bandit" - }, - "location": { - "file": "python/imports/imports-aliases.py", - "start_line": 14, - "end_line": 14 - }, - "identifiers": [ - { - "type": "bandit_test_id", - "name": "Bandit Test ID B303", - "value": "B303" - } - ], - "priority": "Medium", - "file": "python/imports/imports-aliases.py", - "line": 14, - "tool": "bandit" - }, - { - "category": "sast", - "message": "Pickle library appears to be in use, possible security issue.", - "cve": "python/imports/imports-aliases.py:5f200d47291e7bbd8352db23019b85453ca048dd98ea0c291260fa7d009963a4:B301", - "severity": "Medium", - "confidence": "High", - "scanner": { - "id": "bandit", - "name": "Bandit" - }, - "location": { - "file": "python/imports/imports-aliases.py", - "start_line": 15, - "end_line": 15 - }, - "identifiers": [ - { - "type": "bandit_test_id", - "name": "Bandit Test ID B301", - "value": "B301" - } - ], - "priority": "Medium", - "file": "python/imports/imports-aliases.py", - "line": 15, - "tool": "bandit" - }, - { - "category": "sast", - "name": "ECB mode is insecure", - "message": "ECB mode is insecure", - "cve": "groovy/src/main/java/com/gitlab/security_products/tests/App.groovy:29:ECB_MODE", - "severity": "Medium", - "confidence": "High", - "scanner": { - "id": "find_sec_bugs", - "name": "Find Security Bugs" - }, - "location": { - "file": "groovy/src/main/java/com/gitlab/security_products/tests/App.groovy", - "start_line": 29, - "end_line": 29, - "class": "com.gitlab.security_products.tests.App", - "method": "insecureCypher" - }, - "identifiers": [ - { - "type": "find_sec_bugs_type", - "name": "Find Security Bugs-ECB_MODE", - "value": "ECB_MODE", - "url": "https://find-sec-bugs.github.io/bugs.htm#ECB_MODE" - } - ], - "priority": "Medium", - "file": "groovy/src/main/java/com/gitlab/security_products/tests/App.groovy", - "line": 29, - "url": "https://find-sec-bugs.github.io/bugs.htm#ECB_MODE", - "tool": "find_sec_bugs" - }, - { - "category": "sast", - "name": "Cipher with no integrity", - "message": "Cipher with no integrity", - "cve": "groovy/src/main/java/com/gitlab/security_products/tests/App.groovy:29:CIPHER_INTEGRITY", - "severity": "Medium", - "confidence": "High", - "scanner": { - "id": "find_sec_bugs", - "name": "Find Security Bugs" - }, - "location": { - "file": "groovy/src/main/java/com/gitlab/security_products/tests/App.groovy", - "start_line": 29, - "end_line": 29, - "class": "com.gitlab.security_products.tests.App", - "method": "insecureCypher" - }, - "identifiers": [ - { - "type": "find_sec_bugs_type", - "name": "Find Security Bugs-CIPHER_INTEGRITY", - "value": "CIPHER_INTEGRITY", - "url": "https://find-sec-bugs.github.io/bugs.htm#CIPHER_INTEGRITY" - } - ], - "priority": "Medium", - "file": "groovy/src/main/java/com/gitlab/security_products/tests/App.groovy", - "line": 29, - "url": "https://find-sec-bugs.github.io/bugs.htm#CIPHER_INTEGRITY", - "tool": "find_sec_bugs" - }, - { - "category": "sast", - "message": "Probable insecure usage of temp file/directory.", - "cve": "python/hardcoded/hardcoded-tmp.py:63dd4d626855555b816985d82c4614a790462a0a3ada89dc58eb97f9c50f3077:B108", - "severity": "Medium", - "confidence": "Medium", - "scanner": { - "id": "bandit", - "name": "Bandit" - }, - "location": { - "file": "python/hardcoded/hardcoded-tmp.py", - "start_line": 14, - "end_line": 14 - }, - "identifiers": [ - { - "type": "bandit_test_id", - "name": "Bandit Test ID B108", - "value": "B108", - "url": "https://docs.openstack.org/bandit/latest/plugins/b108_hardcoded_tmp_directory.html" - } - ], - "priority": "Medium", - "file": "python/hardcoded/hardcoded-tmp.py", - "line": 14, - "url": "https://docs.openstack.org/bandit/latest/plugins/b108_hardcoded_tmp_directory.html", - "tool": "bandit" - }, - { - "category": "sast", - "message": "Probable insecure usage of temp file/directory.", - "cve": "python/hardcoded/hardcoded-tmp.py:4ad6d4c40a8c263fc265f3384724014e0a4f8dd6200af83e51ff120420038031:B108", - "severity": "Medium", - "confidence": "Medium", - "scanner": { - "id": "bandit", - "name": "Bandit" - }, - "location": { - "file": "python/hardcoded/hardcoded-tmp.py", - "start_line": 10, - "end_line": 10 - }, - "identifiers": [ - { - "type": "bandit_test_id", - "name": "Bandit Test ID B108", - "value": "B108", - "url": "https://docs.openstack.org/bandit/latest/plugins/b108_hardcoded_tmp_directory.html" - } - ], - "priority": "Medium", - "file": "python/hardcoded/hardcoded-tmp.py", - "line": 10, - "url": "https://docs.openstack.org/bandit/latest/plugins/b108_hardcoded_tmp_directory.html", - "tool": "bandit" - }, - { - "category": "sast", - "message": "Consider possible security implications associated with Popen module.", - "cve": "python/imports/imports-aliases.py:2c3e1fa1e54c3c6646e8bcfaee2518153c6799b77587ff8d9a7b0631f6d34785:B404", - "severity": "Low", - "confidence": "High", - "scanner": { - "id": "bandit", - "name": "Bandit" - }, - "location": { - "file": "python/imports/imports-aliases.py", - "start_line": 1, - "end_line": 1 - }, - "identifiers": [ - { - "type": "bandit_test_id", - "name": "Bandit Test ID B404", - "value": "B404" - } - ], - "priority": "Low", - "file": "python/imports/imports-aliases.py", - "line": 1, - "tool": "bandit" - }, - { - "category": "sast", - "message": "Consider possible security implications associated with pickle module.", - "cve": "python/imports/imports.py:af58d07f6ad519ef5287fcae65bf1a6999448a1a3a8bc1ac2a11daa80d0b96bf:B403", - "severity": "Low", - "confidence": "High", - "scanner": { - "id": "bandit", - "name": "Bandit" - }, - "location": { - "file": "python/imports/imports.py", - "start_line": 2, - "end_line": 2 - }, - "identifiers": [ - { - "type": "bandit_test_id", - "name": "Bandit Test ID B403", - "value": "B403" - } - ], - "priority": "Low", - "file": "python/imports/imports.py", - "line": 2, - "tool": "bandit" - }, - { - "category": "sast", - "message": "Consider possible security implications associated with subprocess module.", - "cve": "python/imports/imports.py:8de9bc98029d212db530785a5f6780cfa663548746ff228ab8fa96c5bb82f089:B404", - "severity": "Low", - "confidence": "High", - "scanner": { - "id": "bandit", - "name": "Bandit" - }, - "location": { - "file": "python/imports/imports.py", - "start_line": 4, - "end_line": 4 - }, - "identifiers": [ - { - "type": "bandit_test_id", - "name": "Bandit Test ID B404", - "value": "B404" - } - ], - "priority": "Low", - "file": "python/imports/imports.py", - "line": 4, - "tool": "bandit" - }, - { - "category": "sast", - "message": "Possible hardcoded password: 'blerg'", - "cve": "python/hardcoded/hardcoded-passwords.py:97c30f1d76d2a88913e3ce9ae74087874d740f87de8af697a9c455f01119f633:B106", - "severity": "Low", - "confidence": "Medium", - "scanner": { - "id": "bandit", - "name": "Bandit" - }, - "location": { - "file": "python/hardcoded/hardcoded-passwords.py", - "start_line": 22, - "end_line": 22 - }, - "identifiers": [ - { - "type": "bandit_test_id", - "name": "Bandit Test ID B106", - "value": "B106", - "url": "https://docs.openstack.org/bandit/latest/plugins/b106_hardcoded_password_funcarg.html" - } - ], - "priority": "Low", - "file": "python/hardcoded/hardcoded-passwords.py", - "line": 22, - "url": "https://docs.openstack.org/bandit/latest/plugins/b106_hardcoded_password_funcarg.html", - "tool": "bandit" - }, - { - "category": "sast", - "message": "Possible hardcoded password: 'root'", - "cve": "python/hardcoded/hardcoded-passwords.py:7431c73a0bc16d94ece2a2e75ef38f302574d42c37ac0c3c38ad0b3bf8a59f10:B105", - "severity": "Low", - "confidence": "Medium", - "scanner": { - "id": "bandit", - "name": "Bandit" - }, - "location": { - "file": "python/hardcoded/hardcoded-passwords.py", - "start_line": 5, - "end_line": 5 - }, - "identifiers": [ - { - "type": "bandit_test_id", - "name": "Bandit Test ID B105", - "value": "B105", - "url": "https://docs.openstack.org/bandit/latest/plugins/b105_hardcoded_password_string.html" - } - ], - "priority": "Low", - "file": "python/hardcoded/hardcoded-passwords.py", - "line": 5, - "url": "https://docs.openstack.org/bandit/latest/plugins/b105_hardcoded_password_string.html", - "tool": "bandit" - }, - { - "category": "sast", - "message": "Possible hardcoded password: ''", - "cve": "python/hardcoded/hardcoded-passwords.py:d2d1857c27caedd49c57bfbcdc23afcc92bd66a22701fcdc632869aab4ca73ee:B105", - "severity": "Low", - "confidence": "Medium", - "scanner": { - "id": "bandit", - "name": "Bandit" - }, - "location": { - "file": "python/hardcoded/hardcoded-passwords.py", - "start_line": 9, - "end_line": 9 - }, - "identifiers": [ - { - "type": "bandit_test_id", - "name": "Bandit Test ID B105", - "value": "B105", - "url": "https://docs.openstack.org/bandit/latest/plugins/b105_hardcoded_password_string.html" - } - ], - "priority": "Low", - "file": "python/hardcoded/hardcoded-passwords.py", - "line": 9, - "url": "https://docs.openstack.org/bandit/latest/plugins/b105_hardcoded_password_string.html", - "tool": "bandit" - }, - { - "category": "sast", - "message": "Possible hardcoded password: 'ajklawejrkl42348swfgkg'", - "cve": "python/hardcoded/hardcoded-passwords.py:fb3866215a61393a5c9c32a3b60e2058171a23219c353f722cbd3567acab21d2:B105", - "severity": "Low", - "confidence": "Medium", - "scanner": { - "id": "bandit", - "name": "Bandit" - }, - "location": { - "file": "python/hardcoded/hardcoded-passwords.py", - "start_line": 13, - "end_line": 13 - }, - "identifiers": [ - { - "type": "bandit_test_id", - "name": "Bandit Test ID B105", - "value": "B105", - "url": "https://docs.openstack.org/bandit/latest/plugins/b105_hardcoded_password_string.html" - } - ], - "priority": "Low", - "file": "python/hardcoded/hardcoded-passwords.py", - "line": 13, - "url": "https://docs.openstack.org/bandit/latest/plugins/b105_hardcoded_password_string.html", - "tool": "bandit" - }, - { - "category": "sast", - "message": "Possible hardcoded password: 'blerg'", - "cve": "python/hardcoded/hardcoded-passwords.py:63c62a8b7e1e5224439bd26b28030585ac48741e28ca64561a6071080c560a5f:B105", - "severity": "Low", - "confidence": "Medium", - "scanner": { - "id": "bandit", - "name": "Bandit" - }, - "location": { - "file": "python/hardcoded/hardcoded-passwords.py", - "start_line": 23, - "end_line": 23 - }, - "identifiers": [ - { - "type": "bandit_test_id", - "name": "Bandit Test ID B105", - "value": "B105", - "url": "https://docs.openstack.org/bandit/latest/plugins/b105_hardcoded_password_string.html" - } - ], - "priority": "Low", - "file": "python/hardcoded/hardcoded-passwords.py", - "line": 23, - "url": "https://docs.openstack.org/bandit/latest/plugins/b105_hardcoded_password_string.html", - "tool": "bandit" - }, - { - "category": "sast", - "message": "Possible hardcoded password: 'blerg'", - "cve": "python/hardcoded/hardcoded-passwords.py:4311b06d08df8fa58229b341c531da8e1a31ec4520597bdff920cd5c098d86f9:B105", - "severity": "Low", - "confidence": "Medium", - "scanner": { - "id": "bandit", - "name": "Bandit" - }, - "location": { - "file": "python/hardcoded/hardcoded-passwords.py", - "start_line": 24, - "end_line": 24 - }, - "identifiers": [ - { - "type": "bandit_test_id", - "name": "Bandit Test ID B105", - "value": "B105", - "url": "https://docs.openstack.org/bandit/latest/plugins/b105_hardcoded_password_string.html" - } - ], - "priority": "Low", - "file": "python/hardcoded/hardcoded-passwords.py", - "line": 24, - "url": "https://docs.openstack.org/bandit/latest/plugins/b105_hardcoded_password_string.html", - "tool": "bandit" - }, - { - "category": "sast", - "message": "Consider possible security implications associated with subprocess module.", - "cve": "python/imports/imports-function.py:5858400c2f39047787702de44d03361ef8d954c9d14bd54ee1c2bef9e6a7df93:B404", - "severity": "Low", - "confidence": "High", - "scanner": { - "id": "bandit", - "name": "Bandit" - }, - "location": { - "file": "python/imports/imports-function.py", - "start_line": 4, - "end_line": 4 - }, - "identifiers": [ - { - "type": "bandit_test_id", - "name": "Bandit Test ID B404", - "value": "B404" - } - ], - "priority": "Low", - "file": "python/imports/imports-function.py", - "line": 4, - "tool": "bandit" - }, - { - "category": "sast", - "message": "Consider possible security implications associated with pickle module.", - "cve": "python/imports/imports-function.py:dbda3cf4190279d30e0aad7dd137eca11272b0b225e8af4e8bf39682da67d956:B403", - "severity": "Low", - "confidence": "High", - "scanner": { - "id": "bandit", - "name": "Bandit" - }, - "location": { - "file": "python/imports/imports-function.py", - "start_line": 2, - "end_line": 2 - }, - "identifiers": [ - { - "type": "bandit_test_id", - "name": "Bandit Test ID B403", - "value": "B403" - } - ], - "priority": "Low", - "file": "python/imports/imports-function.py", - "line": 2, - "tool": "bandit" - }, - { - "category": "sast", - "message": "Consider possible security implications associated with Popen module.", - "cve": "python/imports/imports-from.py:eb8a0db9cd1a8c1ab39a77e6025021b1261cc2a0b026b2f4a11fca4e0636d8dd:B404", - "severity": "Low", - "confidence": "High", - "scanner": { - "id": "bandit", - "name": "Bandit" - }, - "location": { - "file": "python/imports/imports-from.py", - "start_line": 7, - "end_line": 7 - }, - "identifiers": [ - { - "type": "bandit_test_id", - "name": "Bandit Test ID B404", - "value": "B404" - } - ], - "priority": "Low", - "file": "python/imports/imports-from.py", - "line": 7, - "tool": "bandit" - }, - { - "category": "sast", - "message": "subprocess call with shell=True seems safe, but may be changed in the future, consider rewriting without shell", - "cve": "python/imports/imports-aliases.py:f99f9721e27537fbcb6699a4cf39c6740d6234d2c6f06cfc2d9ea977313c483d:B602", - "severity": "Low", - "confidence": "High", - "scanner": { - "id": "bandit", - "name": "Bandit" - }, - "location": { - "file": "python/imports/imports-aliases.py", - "start_line": 9, - "end_line": 9 - }, - "identifiers": [ - { - "type": "bandit_test_id", - "name": "Bandit Test ID B602", - "value": "B602", - "url": "https://docs.openstack.org/bandit/latest/plugins/b602_subprocess_popen_with_shell_equals_true.html" - } - ], - "priority": "Low", - "file": "python/imports/imports-aliases.py", - "line": 9, - "url": "https://docs.openstack.org/bandit/latest/plugins/b602_subprocess_popen_with_shell_equals_true.html", - "tool": "bandit" - }, - { - "category": "sast", - "message": "Consider possible security implications associated with subprocess module.", - "cve": "python/imports/imports-from.py:332a12ab1146698f614a905ce6a6a5401497a12281aef200e80522711c69dcf4:B404", - "severity": "Low", - "confidence": "High", - "scanner": { - "id": "bandit", - "name": "Bandit" - }, - "location": { - "file": "python/imports/imports-from.py", - "start_line": 6, - "end_line": 6 - }, - "identifiers": [ - { - "type": "bandit_test_id", - "name": "Bandit Test ID B404", - "value": "B404" - } - ], - "priority": "Low", - "file": "python/imports/imports-from.py", - "line": 6, - "tool": "bandit" - }, - { - "category": "sast", - "message": "Consider possible security implications associated with Popen module.", - "cve": "python/imports/imports-from.py:0a48de4a3d5348853a03666cb574697e3982998355e7a095a798bd02a5947276:B404", - "severity": "Low", - "confidence": "High", - "scanner": { - "id": "bandit", - "name": "Bandit" - }, - "location": { - "file": "python/imports/imports-from.py", - "start_line": 1, - "end_line": 2 - }, - "identifiers": [ - { - "type": "bandit_test_id", - "name": "Bandit Test ID B404", - "value": "B404" - } - ], - "priority": "Low", - "file": "python/imports/imports-from.py", - "line": 1, - "tool": "bandit" - }, - { - "category": "sast", - "message": "Consider possible security implications associated with pickle module.", - "cve": "python/imports/imports-aliases.py:51b71661dff994bde3529639a727a678c8f5c4c96f00d300913f6d5be1bbdf26:B403", - "severity": "Low", - "confidence": "High", - "scanner": { - "id": "bandit", - "name": "Bandit" - }, - "location": { - "file": "python/imports/imports-aliases.py", - "start_line": 7, - "end_line": 8 - }, - "identifiers": [ - { - "type": "bandit_test_id", - "name": "Bandit Test ID B403", - "value": "B403" - } - ], - "priority": "Low", - "file": "python/imports/imports-aliases.py", - "line": 7, - "tool": "bandit" - }, - { - "category": "sast", - "message": "Consider possible security implications associated with loads module.", - "cve": "python/imports/imports-aliases.py:6ff02aeb3149c01ab68484d794a94f58d5d3e3bb0d58557ef4153644ea68ea54:B403", - "severity": "Low", - "confidence": "High", - "scanner": { - "id": "bandit", - "name": "Bandit" - }, - "location": { - "file": "python/imports/imports-aliases.py", - "start_line": 6, - "end_line": 6 - }, - "identifiers": [ - { - "type": "bandit_test_id", - "name": "Bandit Test ID B403", - "value": "B403" - } - ], - "priority": "Low", - "file": "python/imports/imports-aliases.py", - "line": 6, - "tool": "bandit" - }, - { - "category": "sast", - "message": "Statically-sized arrays can be improperly restricted, leading to potential overflows or other issues (CWE-119!/CWE-120)", - "cve": "c/subdir/utils.c:b466873101951fe96e1332f6728eb7010acbbd5dfc3b65d7d53571d091a06d9e:CWE-119!/CWE-120", - "confidence": "Low", - "solution": "Perform bounds checking, use functions that limit length, or ensure that the size is larger than the maximum possible length", - "scanner": { - "id": "flawfinder", - "name": "Flawfinder" - }, - "location": { - "file": "c/subdir/utils.c", - "start_line": 4 - }, - "identifiers": [ - { - "type": "flawfinder_func_name", - "name": "Flawfinder - char", - "value": "char" - }, - { - "type": "cwe", - "name": "CWE-119", - "value": "119", - "url": "https://cwe.mitre.org/data/definitions/119.html" - }, - { - "type": "cwe", - "name": "CWE-120", - "value": "120", - "url": "https://cwe.mitre.org/data/definitions/120.html" - } - ], - "file": "c/subdir/utils.c", - "line": 4, - "url": "https://cwe.mitre.org/data/definitions/119.html", - "tool": "flawfinder" - }, - { - "category": "sast", - "message": "Check when opening files - can an attacker redirect it (via symlinks), force the opening of special file type (e.g., device files), move things around to create a race condition, control its ancestors, or change its contents? (CWE-362)", - "cve": "c/subdir/utils.c:bab681140fcc8fc3085b6bba74081b44ea145c1c98b5e70cf19ace2417d30770:CWE-362", - "confidence": "Low", - "scanner": { - "id": "flawfinder", - "name": "Flawfinder" - }, - "location": { - "file": "c/subdir/utils.c", - "start_line": 8 - }, - "identifiers": [ - { - "type": "flawfinder_func_name", - "name": "Flawfinder - fopen", - "value": "fopen" - }, - { - "type": "cwe", - "name": "CWE-362", - "value": "362", - "url": "https://cwe.mitre.org/data/definitions/362.html" - } - ], - "file": "c/subdir/utils.c", - "line": 8, - "url": "https://cwe.mitre.org/data/definitions/362.html", - "tool": "flawfinder" - }, - { - "category": "sast", - "message": "Statically-sized arrays can be improperly restricted, leading to potential overflows or other issues (CWE-119!/CWE-120)", - "cve": "cplusplus/src/hello.cpp:c8c6dd0afdae6814194cf0930b719f757ab7b379cf8f261e7f4f9f2f323a818a:CWE-119!/CWE-120", - "confidence": "Low", - "solution": "Perform bounds checking, use functions that limit length, or ensure that the size is larger than the maximum possible length", - "scanner": { - "id": "flawfinder", - "name": "Flawfinder" - }, - "location": { - "file": "cplusplus/src/hello.cpp", - "start_line": 6 - }, - "identifiers": [ - { - "type": "flawfinder_func_name", - "name": "Flawfinder - char", - "value": "char" - }, - { - "type": "cwe", - "name": "CWE-119", - "value": "119", - "url": "https://cwe.mitre.org/data/definitions/119.html" - }, - { - "type": "cwe", - "name": "CWE-120", - "value": "120", - "url": "https://cwe.mitre.org/data/definitions/120.html" - } - ], - "file": "cplusplus/src/hello.cpp", - "line": 6, - "url": "https://cwe.mitre.org/data/definitions/119.html", - "tool": "flawfinder" - }, - { - "category": "sast", - "message": "Does not check for buffer overflows when copying to destination [MS-banned] (CWE-120)", - "cve": "cplusplus/src/hello.cpp:331c04062c4fe0c7c486f66f59e82ad146ab33cdd76ae757ca41f392d568cbd0:CWE-120", - "confidence": "Low", - "solution": "Consider using snprintf, strcpy_s, or strlcpy (warning: strncpy easily misused)", - "scanner": { - "id": "flawfinder", - "name": "Flawfinder" - }, - "location": { - "file": "cplusplus/src/hello.cpp", - "start_line": 7 - }, - "identifiers": [ - { - "type": "flawfinder_func_name", - "name": "Flawfinder - strcpy", - "value": "strcpy" - }, - { - "type": "cwe", - "name": "CWE-120", - "value": "120", - "url": "https://cwe.mitre.org/data/definitions/120.html" - } - ], - "file": "cplusplus/src/hello.cpp", - "line": 7, - "url": "https://cwe.mitre.org/data/definitions/120.html", - "tool": "flawfinder" - } -] diff --git a/spec/fixtures/security-reports/feature-branch.zip b/spec/fixtures/security-reports/feature-branch.zip Binary files differdeleted file mode 100644 index dd49f4e9e1d..00000000000 --- a/spec/fixtures/security-reports/feature-branch.zip +++ /dev/null diff --git a/spec/fixtures/security-reports/feature-branch/gl-container-scanning-report.json b/spec/fixtures/security-reports/feature-branch/gl-container-scanning-report.json deleted file mode 100644 index 6f89d20d4bf..00000000000 --- a/spec/fixtures/security-reports/feature-branch/gl-container-scanning-report.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "image": "registry.gitlab.com/bikebilly/auto-devops-10-6/feature-branch:e7315ba964febb11bac8f5cd6ec433db8a3a1583", - "unapproved": ["CVE-2017-15650"], - "vulnerabilities": [ - { - "featurename": "musl", - "featureversion": "1.1.14-r15", - "vulnerability": "CVE-2017-15650", - "namespace": "alpine:v3.4", - "description": "", - "link": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-15650", - "severity": "Medium", - "fixedby": "1.1.14-r16" - } - ] -} diff --git a/spec/fixtures/security-reports/feature-branch/gl-dast-report.json b/spec/fixtures/security-reports/feature-branch/gl-dast-report.json deleted file mode 100644 index 3a308bf047e..00000000000 --- a/spec/fixtures/security-reports/feature-branch/gl-dast-report.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "site": { - "alerts": [ - { - "sourceid": "3", - "wascid": "15", - "cweid": "16", - "reference": "<p>http://msdn.microsoft.com/en-us/library/ie/gg622941%28v=vs.85%29.aspx</p><p>https://www.owasp.org/index.php/List_of_useful_HTTP_headers</p>", - "otherinfo": "<p>This issue still applies to error type pages (401, 403, 500, etc) as those pages are often still affected by injection issues, in which case there is still concern for browsers sniffing pages away from their actual content type.</p><p>At \"High\" threshold this scanner will not alert on client or server error responses.</p>", - "solution": "<p>Ensure that the application/web server sets the Content-Type header appropriately, and that it sets the X-Content-Type-Options header to 'nosniff' for all web pages.</p><p>If possible, ensure that the end user uses a standards-compliant and modern web browser that does not perform MIME-sniffing at all, or that can be directed by the web application/web server to not perform MIME-sniffing.</p>", - "count": "2", - "pluginid": "10021", - "alert": "X-Content-Type-Options Header Missing", - "name": "X-Content-Type-Options Header Missing", - "riskcode": "1", - "confidence": "2", - "riskdesc": "Low (Medium)", - "desc": "<p>The Anti-MIME-Sniffing header X-Content-Type-Options was not set to 'nosniff'. This allows older versions of Internet Explorer and Chrome to perform MIME-sniffing on the response body, potentially causing the response body to be interpreted and displayed as a content type other than the declared content type. Current (early 2014) and legacy versions of Firefox will use the declared content type (if one is set), rather than performing MIME-sniffing.</p>", - "instances": [ - { - "param": "X-Content-Type-Options", - "method": "GET", - "uri": "http://bikebilly-spring-auto-devops-review-feature-br-3y2gpb.35.192.176.43.xip.io" - }, - { - "param": "X-Content-Type-Options", - "method": "GET", - "uri": "http://bikebilly-spring-auto-devops-review-feature-br-3y2gpb.35.192.176.43.xip.io/" - } - ] - } - ], - "@ssl": "false", - "@port": "80", - "@host": "bikebilly-spring-auto-devops-review-feature-br-3y2gpb.35.192.176.43.xip.io", - "@name": "http://bikebilly-spring-auto-devops-review-feature-br-3y2gpb.35.192.176.43.xip.io" - }, - "@generated": "Fri, 13 Apr 2018 09:22:01", - "@version": "2.7.0" -} diff --git a/spec/fixtures/security-reports/feature-branch/gl-dependency-scanning-report.json b/spec/fixtures/security-reports/feature-branch/gl-dependency-scanning-report.json deleted file mode 100644 index 8555be6618c..00000000000 --- a/spec/fixtures/security-reports/feature-branch/gl-dependency-scanning-report.json +++ /dev/null @@ -1,181 +0,0 @@ -{ - "version": "1.3", - "vulnerabilities": [ - { - "category": "dependency_scanning", - "name": "io.netty/netty - CVE-2014-3488", - "message": "DoS by CPU exhaustion when using malicious SSL packets", - "cve": "app/pom.xml:io.netty/netty@3.9.1.Final:CVE-2014-3488", - "severity": "Unknown", - "solution": "Upgrade to the latest version", - "scanner": { - "id": "gemnasium", - "name": "Gemnasium" - }, - "location": { - "file": "app/pom.xml", - "dependency": { - "package": { - "name": "io.netty/netty" - }, - "version": "3.9.1.Final" - } - }, - "identifiers": [ - { - "type": "gemnasium", - "name": "Gemnasium-d1bf36d9-9f07-46cd-9cfc-8675338ada8f", - "value": "d1bf36d9-9f07-46cd-9cfc-8675338ada8f", - "url": "https://deps.sec.gitlab.com/packages/maven/io.netty/netty/versions/3.9.1.Final/advisories" - }, - { - "type": "cve", - "name": "CVE-2014-3488", - "value": "CVE-2014-3488", - "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2014-3488" - } - ], - "links": [ - { - "url": "https://bugzilla.redhat.com/CVE-2014-3488" - }, - { - "url": "http://netty.io/news/2014/06/11/3.html" - }, - { - "url": "https://github.com/netty/netty/issues/2562" - } - ], - "priority": "Unknown", - "file": "app/pom.xml", - "url": "https://bugzilla.redhat.com/CVE-2014-3488", - "tool": "gemnasium" - }, - { - "category": "dependency_scanning", - "name": "Django - CVE-2017-12794", - "message": "Possible XSS in traceback section of technical 500 debug page", - "cve": "app/requirements.txt:Django@1.11.3:CVE-2017-12794", - "severity": "Unknown", - "solution": "Upgrade to latest version or apply patch.", - "scanner": { - "id": "gemnasium", - "name": "Gemnasium" - }, - "location": { - "file": "app/requirements.txt", - "dependency": { - "package": { - "name": "Django" - }, - "version": "1.11.3" - } - }, - "identifiers": [ - { - "type": "gemnasium", - "name": "Gemnasium-6162a015-8635-4a15-8d7c-dc9321db366f", - "value": "6162a015-8635-4a15-8d7c-dc9321db366f", - "url": "https://deps.sec.gitlab.com/packages/pypi/Django/versions/1.11.3/advisories" - }, - { - "type": "cve", - "name": "CVE-2017-12794", - "value": "CVE-2017-12794", - "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-12794" - } - ], - "links": [ - { - "url": "https://www.djangoproject.com/weblog/2017/sep/05/security-releases/" - } - ], - "priority": "Unknown", - "file": "app/requirements.txt", - "url": "https://www.djangoproject.com/weblog/2017/sep/05/security-releases/", - "tool": "gemnasium" - }, - { - "category": "dependency_scanning", - "name": "nokogiri - USN-3424-1", - "message": "Vulnerabilities in libxml2", - "cve": "rails/Gemfile.lock:nokogiri@1.8.0:USN-3424-1", - "severity": "Unknown", - "solution": "Upgrade to latest version.", - "scanner": { - "id": "gemnasium", - "name": "Gemnasium" - }, - "location": { - "file": "rails/Gemfile.lock", - "dependency": { - "package": { - "name": "nokogiri" - }, - "version": "1.8.0" - } - }, - "identifiers": [ - { - "type": "gemnasium", - "name": "Gemnasium-06565b64-486d-4326-b906-890d9915804d", - "value": "06565b64-486d-4326-b906-890d9915804d", - "url": "https://deps.sec.gitlab.com/packages/gem/nokogiri/versions/1.8.0/advisories" - }, - { - "type": "usn", - "name": "USN-3424-1", - "value": "USN-3424-1", - "url": "https://usn.ubuntu.com/3424-1/" - } - ], - "links": [ - { - "url": "https://github.com/sparklemotion/nokogiri/issues/1673" - } - ], - "priority": "Unknown", - "file": "rails/Gemfile.lock", - "url": "https://github.com/sparklemotion/nokogiri/issues/1673", - "tool": "gemnasium" - }, - { - "category": "dependency_scanning", - "name": "ffi - CVE-2018-1000201", - "message": "ruby-ffi DDL loading issue on Windows OS", - "cve": "ffi:1.9.18:CVE-2018-1000201", - "severity": "High", - "solution": "upgrade to \u003e= 1.9.24", - "scanner": { - "id": "bundler_audit", - "name": "bundler-audit" - }, - "location": { - "file": "sast-sample-rails/Gemfile.lock", - "dependency": { - "package": { - "name": "ffi" - }, - "version": "1.9.18" - } - }, - "identifiers": [ - { - "type": "cve", - "name": "CVE-2018-1000201", - "value": "CVE-2018-1000201", - "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2018-1000201" - } - ], - "links": [ - { - "url": "https://github.com/ffi/ffi/releases/tag/1.9.24" - } - ], - "priority": "High", - "file": "sast-sample-rails/Gemfile.lock", - "url": "https://github.com/ffi/ffi/releases/tag/1.9.24", - "tool": "bundler_audit" - } - ] -} diff --git a/spec/fixtures/security-reports/feature-branch/gl-license-management-report.json b/spec/fixtures/security-reports/feature-branch/gl-license-management-report.json deleted file mode 100644 index 5fd81fd69bd..00000000000 --- a/spec/fixtures/security-reports/feature-branch/gl-license-management-report.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "licenses": [ - { - "count": 1, - "name": "WTFPL" - }, - { - "count": 1, - "name": "MIT" - } - ], - "dependencies": [ - { - "license": { - "name": "MIT", - "url": "http://opensource.org/licenses/mit-license" - }, - "dependency": { - "name": "actioncable", - "url": "http://rubyonrails.org", - "description": "WebSocket framework for Rails.", - "pathes": [ - "." - ] - } - }, - { - "license": { - "name": "WTFPL", - "url": "http://www.wtfpl.net/" - }, - "dependency": { - "name": "wtfpl_init", - "url": "https://rubygems.org/gems/wtfpl_init", - "description": "Download WTFPL license file and rename to LICENSE.md or something", - "pathes": [ - "." - ] - } - } - ] -} diff --git a/spec/fixtures/security-reports/feature-branch/gl-sast-report.json b/spec/fixtures/security-reports/feature-branch/gl-sast-report.json deleted file mode 100644 index 4bef3d22f70..00000000000 --- a/spec/fixtures/security-reports/feature-branch/gl-sast-report.json +++ /dev/null @@ -1,947 +0,0 @@ -{ - "version": "1.2", - "vulnerabilities": [ - { - "category": "sast", - "message": "Probable insecure usage of temp file/directory.", - "cve": "python/hardcoded/hardcoded-tmp.py:52865813c884a507be1f152d654245af34aba8a391626d01f1ab6d3f52ec8779:B108", - "severity": "Medium", - "confidence": "Medium", - "scanner": { - "id": "bandit", - "name": "Bandit" - }, - "location": { - "file": "python/hardcoded/hardcoded-tmp.py", - "start_line": 1, - "end_line": 1 - }, - "identifiers": [ - { - "type": "bandit_test_id", - "name": "Bandit Test ID B108", - "value": "B108", - "url": "https://docs.openstack.org/bandit/latest/plugins/b108_hardcoded_tmp_directory.html" - } - ], - "priority": "Medium", - "file": "python/hardcoded/hardcoded-tmp.py", - "line": 1, - "url": "https://docs.openstack.org/bandit/latest/plugins/b108_hardcoded_tmp_directory.html", - "tool": "bandit" - }, - { - "category": "sast", - "name": "Predictable pseudorandom number generator", - "message": "Predictable pseudorandom number generator", - "cve": "groovy/src/main/java/com/gitlab/security_products/tests/App.groovy:47:PREDICTABLE_RANDOM", - "severity": "Medium", - "confidence": "Medium", - "scanner": { - "id": "find_sec_bugs", - "name": "Find Security Bugs" - }, - "location": { - "file": "groovy/src/main/java/com/gitlab/security_products/tests/App.groovy", - "start_line": 47, - "end_line": 47, - "class": "com.gitlab.security_products.tests.App", - "method": "generateSecretToken2" - }, - "identifiers": [ - { - "type": "find_sec_bugs_type", - "name": "Find Security Bugs-PREDICTABLE_RANDOM", - "value": "PREDICTABLE_RANDOM", - "url": "https://find-sec-bugs.github.io/bugs.htm#PREDICTABLE_RANDOM" - } - ], - "priority": "Medium", - "file": "groovy/src/main/java/com/gitlab/security_products/tests/App.groovy", - "line": 47, - "url": "https://find-sec-bugs.github.io/bugs.htm#PREDICTABLE_RANDOM", - "tool": "find_sec_bugs" - }, - { - "category": "sast", - "name": "Predictable pseudorandom number generator", - "message": "Predictable pseudorandom number generator", - "cve": "groovy/src/main/java/com/gitlab/security_products/tests/App.groovy:41:PREDICTABLE_RANDOM", - "severity": "Medium", - "confidence": "Medium", - "scanner": { - "id": "find_sec_bugs", - "name": "Find Security Bugs" - }, - "location": { - "file": "groovy/src/main/java/com/gitlab/security_products/tests/App.groovy", - "start_line": 41, - "end_line": 41, - "class": "com.gitlab.security_products.tests.App", - "method": "generateSecretToken1" - }, - "identifiers": [ - { - "type": "find_sec_bugs_type", - "name": "Find Security Bugs-PREDICTABLE_RANDOM", - "value": "PREDICTABLE_RANDOM", - "url": "https://find-sec-bugs.github.io/bugs.htm#PREDICTABLE_RANDOM" - } - ], - "priority": "Medium", - "file": "groovy/src/main/java/com/gitlab/security_products/tests/App.groovy", - "line": 41, - "url": "https://find-sec-bugs.github.io/bugs.htm#PREDICTABLE_RANDOM", - "tool": "find_sec_bugs" - }, - { - "category": "sast", - "message": "Use of insecure MD2, MD4, or MD5 hash function.", - "cve": "python/imports/imports-aliases.py:cb203b465dffb0cb3a8e8bd8910b84b93b0a5995a938e4b903dbb0cd6ffa1254:B303", - "severity": "Medium", - "confidence": "High", - "scanner": { - "id": "bandit", - "name": "Bandit" - }, - "location": { - "file": "python/imports/imports-aliases.py", - "start_line": 11, - "end_line": 11 - }, - "identifiers": [ - { - "type": "bandit_test_id", - "name": "Bandit Test ID B303", - "value": "B303" - } - ], - "priority": "Medium", - "file": "python/imports/imports-aliases.py", - "line": 11, - "tool": "bandit" - }, - { - "category": "sast", - "message": "Use of insecure MD2, MD4, or MD5 hash function.", - "cve": "python/imports/imports-aliases.py:a7173c43ae66bd07466632d819d450e0071e02dbf782763640d1092981f9631b:B303", - "severity": "Medium", - "confidence": "High", - "scanner": { - "id": "bandit", - "name": "Bandit" - }, - "location": { - "file": "python/imports/imports-aliases.py", - "start_line": 12, - "end_line": 12 - }, - "identifiers": [ - { - "type": "bandit_test_id", - "name": "Bandit Test ID B303", - "value": "B303" - } - ], - "priority": "Medium", - "file": "python/imports/imports-aliases.py", - "line": 12, - "tool": "bandit" - }, - { - "category": "sast", - "message": "Use of insecure MD2, MD4, or MD5 hash function.", - "cve": "python/imports/imports-aliases.py:017017b77deb0b8369b6065947833eeea752a92ec8a700db590fece3e934cf0d:B303", - "severity": "Medium", - "confidence": "High", - "scanner": { - "id": "bandit", - "name": "Bandit" - }, - "location": { - "file": "python/imports/imports-aliases.py", - "start_line": 13, - "end_line": 13 - }, - "identifiers": [ - { - "type": "bandit_test_id", - "name": "Bandit Test ID B303", - "value": "B303" - } - ], - "priority": "Medium", - "file": "python/imports/imports-aliases.py", - "line": 13, - "tool": "bandit" - }, - { - "category": "sast", - "message": "Use of insecure MD2, MD4, or MD5 hash function.", - "cve": "python/imports/imports-aliases.py:45fc8c53aea7b84f06bc4e590cc667678d6073c4c8a1d471177ca2146fb22db2:B303", - "severity": "Medium", - "confidence": "High", - "scanner": { - "id": "bandit", - "name": "Bandit" - }, - "location": { - "file": "python/imports/imports-aliases.py", - "start_line": 14, - "end_line": 14 - }, - "identifiers": [ - { - "type": "bandit_test_id", - "name": "Bandit Test ID B303", - "value": "B303" - } - ], - "priority": "Medium", - "file": "python/imports/imports-aliases.py", - "line": 14, - "tool": "bandit" - }, - { - "category": "sast", - "message": "Pickle library appears to be in use, possible security issue.", - "cve": "python/imports/imports-aliases.py:5f200d47291e7bbd8352db23019b85453ca048dd98ea0c291260fa7d009963a4:B301", - "severity": "Medium", - "confidence": "High", - "scanner": { - "id": "bandit", - "name": "Bandit" - }, - "location": { - "file": "python/imports/imports-aliases.py", - "start_line": 15, - "end_line": 15 - }, - "identifiers": [ - { - "type": "bandit_test_id", - "name": "Bandit Test ID B301", - "value": "B301" - } - ], - "priority": "Medium", - "file": "python/imports/imports-aliases.py", - "line": 15, - "tool": "bandit" - }, - { - "category": "sast", - "name": "ECB mode is insecure", - "message": "ECB mode is insecure", - "cve": "groovy/src/main/java/com/gitlab/security_products/tests/App.groovy:29:ECB_MODE", - "severity": "Medium", - "confidence": "High", - "scanner": { - "id": "find_sec_bugs", - "name": "Find Security Bugs" - }, - "location": { - "file": "groovy/src/main/java/com/gitlab/security_products/tests/App.groovy", - "start_line": 29, - "end_line": 29, - "class": "com.gitlab.security_products.tests.App", - "method": "insecureCypher" - }, - "identifiers": [ - { - "type": "find_sec_bugs_type", - "name": "Find Security Bugs-ECB_MODE", - "value": "ECB_MODE", - "url": "https://find-sec-bugs.github.io/bugs.htm#ECB_MODE" - } - ], - "priority": "Medium", - "file": "groovy/src/main/java/com/gitlab/security_products/tests/App.groovy", - "line": 29, - "url": "https://find-sec-bugs.github.io/bugs.htm#ECB_MODE", - "tool": "find_sec_bugs" - }, - { - "category": "sast", - "name": "Cipher with no integrity", - "message": "Cipher with no integrity", - "cve": "groovy/src/main/java/com/gitlab/security_products/tests/App.groovy:29:CIPHER_INTEGRITY", - "severity": "Medium", - "confidence": "High", - "scanner": { - "id": "find_sec_bugs", - "name": "Find Security Bugs" - }, - "location": { - "file": "groovy/src/main/java/com/gitlab/security_products/tests/App.groovy", - "start_line": 29, - "end_line": 29, - "class": "com.gitlab.security_products.tests.App", - "method": "insecureCypher" - }, - "identifiers": [ - { - "type": "find_sec_bugs_type", - "name": "Find Security Bugs-CIPHER_INTEGRITY", - "value": "CIPHER_INTEGRITY", - "url": "https://find-sec-bugs.github.io/bugs.htm#CIPHER_INTEGRITY" - } - ], - "priority": "Medium", - "file": "groovy/src/main/java/com/gitlab/security_products/tests/App.groovy", - "line": 29, - "url": "https://find-sec-bugs.github.io/bugs.htm#CIPHER_INTEGRITY", - "tool": "find_sec_bugs" - }, - { - "category": "sast", - "message": "Probable insecure usage of temp file/directory.", - "cve": "python/hardcoded/hardcoded-tmp.py:63dd4d626855555b816985d82c4614a790462a0a3ada89dc58eb97f9c50f3077:B108", - "severity": "Medium", - "confidence": "Medium", - "scanner": { - "id": "bandit", - "name": "Bandit" - }, - "location": { - "file": "python/hardcoded/hardcoded-tmp.py", - "start_line": 14, - "end_line": 14 - }, - "identifiers": [ - { - "type": "bandit_test_id", - "name": "Bandit Test ID B108", - "value": "B108", - "url": "https://docs.openstack.org/bandit/latest/plugins/b108_hardcoded_tmp_directory.html" - } - ], - "priority": "Medium", - "file": "python/hardcoded/hardcoded-tmp.py", - "line": 14, - "url": "https://docs.openstack.org/bandit/latest/plugins/b108_hardcoded_tmp_directory.html", - "tool": "bandit" - }, - { - "category": "sast", - "message": "Probable insecure usage of temp file/directory.", - "cve": "python/hardcoded/hardcoded-tmp.py:4ad6d4c40a8c263fc265f3384724014e0a4f8dd6200af83e51ff120420038031:B108", - "severity": "Medium", - "confidence": "Medium", - "scanner": { - "id": "bandit", - "name": "Bandit" - }, - "location": { - "file": "python/hardcoded/hardcoded-tmp.py", - "start_line": 10, - "end_line": 10 - }, - "identifiers": [ - { - "type": "bandit_test_id", - "name": "Bandit Test ID B108", - "value": "B108", - "url": "https://docs.openstack.org/bandit/latest/plugins/b108_hardcoded_tmp_directory.html" - } - ], - "priority": "Medium", - "file": "python/hardcoded/hardcoded-tmp.py", - "line": 10, - "url": "https://docs.openstack.org/bandit/latest/plugins/b108_hardcoded_tmp_directory.html", - "tool": "bandit" - }, - { - "category": "sast", - "message": "Consider possible security implications associated with Popen module.", - "cve": "python/imports/imports-aliases.py:2c3e1fa1e54c3c6646e8bcfaee2518153c6799b77587ff8d9a7b0631f6d34785:B404", - "severity": "Low", - "confidence": "High", - "scanner": { - "id": "bandit", - "name": "Bandit" - }, - "location": { - "file": "python/imports/imports-aliases.py", - "start_line": 1, - "end_line": 1 - }, - "identifiers": [ - { - "type": "bandit_test_id", - "name": "Bandit Test ID B404", - "value": "B404" - } - ], - "priority": "Low", - "file": "python/imports/imports-aliases.py", - "line": 1, - "tool": "bandit" - }, - { - "category": "sast", - "message": "Consider possible security implications associated with pickle module.", - "cve": "python/imports/imports.py:af58d07f6ad519ef5287fcae65bf1a6999448a1a3a8bc1ac2a11daa80d0b96bf:B403", - "severity": "Low", - "confidence": "High", - "scanner": { - "id": "bandit", - "name": "Bandit" - }, - "location": { - "file": "python/imports/imports.py", - "start_line": 2, - "end_line": 2 - }, - "identifiers": [ - { - "type": "bandit_test_id", - "name": "Bandit Test ID B403", - "value": "B403" - } - ], - "priority": "Low", - "file": "python/imports/imports.py", - "line": 2, - "tool": "bandit" - }, - { - "category": "sast", - "message": "Consider possible security implications associated with subprocess module.", - "cve": "python/imports/imports.py:8de9bc98029d212db530785a5f6780cfa663548746ff228ab8fa96c5bb82f089:B404", - "severity": "Low", - "confidence": "High", - "scanner": { - "id": "bandit", - "name": "Bandit" - }, - "location": { - "file": "python/imports/imports.py", - "start_line": 4, - "end_line": 4 - }, - "identifiers": [ - { - "type": "bandit_test_id", - "name": "Bandit Test ID B404", - "value": "B404" - } - ], - "priority": "Low", - "file": "python/imports/imports.py", - "line": 4, - "tool": "bandit" - }, - { - "category": "sast", - "message": "Possible hardcoded password: 'blerg'", - "cve": "python/hardcoded/hardcoded-passwords.py:97c30f1d76d2a88913e3ce9ae74087874d740f87de8af697a9c455f01119f633:B106", - "severity": "Low", - "confidence": "Medium", - "scanner": { - "id": "bandit", - "name": "Bandit" - }, - "location": { - "file": "python/hardcoded/hardcoded-passwords.py", - "start_line": 22, - "end_line": 22 - }, - "identifiers": [ - { - "type": "bandit_test_id", - "name": "Bandit Test ID B106", - "value": "B106", - "url": "https://docs.openstack.org/bandit/latest/plugins/b106_hardcoded_password_funcarg.html" - } - ], - "priority": "Low", - "file": "python/hardcoded/hardcoded-passwords.py", - "line": 22, - "url": "https://docs.openstack.org/bandit/latest/plugins/b106_hardcoded_password_funcarg.html", - "tool": "bandit" - }, - { - "category": "sast", - "message": "Possible hardcoded password: 'root'", - "cve": "python/hardcoded/hardcoded-passwords.py:7431c73a0bc16d94ece2a2e75ef38f302574d42c37ac0c3c38ad0b3bf8a59f10:B105", - "severity": "Low", - "confidence": "Medium", - "scanner": { - "id": "bandit", - "name": "Bandit" - }, - "location": { - "file": "python/hardcoded/hardcoded-passwords.py", - "start_line": 5, - "end_line": 5 - }, - "identifiers": [ - { - "type": "bandit_test_id", - "name": "Bandit Test ID B105", - "value": "B105", - "url": "https://docs.openstack.org/bandit/latest/plugins/b105_hardcoded_password_string.html" - } - ], - "priority": "Low", - "file": "python/hardcoded/hardcoded-passwords.py", - "line": 5, - "url": "https://docs.openstack.org/bandit/latest/plugins/b105_hardcoded_password_string.html", - "tool": "bandit" - }, - { - "category": "sast", - "message": "Possible hardcoded password: ''", - "cve": "python/hardcoded/hardcoded-passwords.py:d2d1857c27caedd49c57bfbcdc23afcc92bd66a22701fcdc632869aab4ca73ee:B105", - "severity": "Low", - "confidence": "Medium", - "scanner": { - "id": "bandit", - "name": "Bandit" - }, - "location": { - "file": "python/hardcoded/hardcoded-passwords.py", - "start_line": 9, - "end_line": 9 - }, - "identifiers": [ - { - "type": "bandit_test_id", - "name": "Bandit Test ID B105", - "value": "B105", - "url": "https://docs.openstack.org/bandit/latest/plugins/b105_hardcoded_password_string.html" - } - ], - "priority": "Low", - "file": "python/hardcoded/hardcoded-passwords.py", - "line": 9, - "url": "https://docs.openstack.org/bandit/latest/plugins/b105_hardcoded_password_string.html", - "tool": "bandit" - }, - { - "category": "sast", - "message": "Possible hardcoded password: 'ajklawejrkl42348swfgkg'", - "cve": "python/hardcoded/hardcoded-passwords.py:fb3866215a61393a5c9c32a3b60e2058171a23219c353f722cbd3567acab21d2:B105", - "severity": "Low", - "confidence": "Medium", - "scanner": { - "id": "bandit", - "name": "Bandit" - }, - "location": { - "file": "python/hardcoded/hardcoded-passwords.py", - "start_line": 13, - "end_line": 13 - }, - "identifiers": [ - { - "type": "bandit_test_id", - "name": "Bandit Test ID B105", - "value": "B105", - "url": "https://docs.openstack.org/bandit/latest/plugins/b105_hardcoded_password_string.html" - } - ], - "priority": "Low", - "file": "python/hardcoded/hardcoded-passwords.py", - "line": 13, - "url": "https://docs.openstack.org/bandit/latest/plugins/b105_hardcoded_password_string.html", - "tool": "bandit" - }, - { - "category": "sast", - "message": "Possible hardcoded password: 'blerg'", - "cve": "python/hardcoded/hardcoded-passwords.py:63c62a8b7e1e5224439bd26b28030585ac48741e28ca64561a6071080c560a5f:B105", - "severity": "Low", - "confidence": "Medium", - "scanner": { - "id": "bandit", - "name": "Bandit" - }, - "location": { - "file": "python/hardcoded/hardcoded-passwords.py", - "start_line": 23, - "end_line": 23 - }, - "identifiers": [ - { - "type": "bandit_test_id", - "name": "Bandit Test ID B105", - "value": "B105", - "url": "https://docs.openstack.org/bandit/latest/plugins/b105_hardcoded_password_string.html" - } - ], - "priority": "Low", - "file": "python/hardcoded/hardcoded-passwords.py", - "line": 23, - "url": "https://docs.openstack.org/bandit/latest/plugins/b105_hardcoded_password_string.html", - "tool": "bandit" - }, - { - "category": "sast", - "message": "Possible hardcoded password: 'blerg'", - "cve": "python/hardcoded/hardcoded-passwords.py:4311b06d08df8fa58229b341c531da8e1a31ec4520597bdff920cd5c098d86f9:B105", - "severity": "Low", - "confidence": "Medium", - "scanner": { - "id": "bandit", - "name": "Bandit" - }, - "location": { - "file": "python/hardcoded/hardcoded-passwords.py", - "start_line": 24, - "end_line": 24 - }, - "identifiers": [ - { - "type": "bandit_test_id", - "name": "Bandit Test ID B105", - "value": "B105", - "url": "https://docs.openstack.org/bandit/latest/plugins/b105_hardcoded_password_string.html" - } - ], - "priority": "Low", - "file": "python/hardcoded/hardcoded-passwords.py", - "line": 24, - "url": "https://docs.openstack.org/bandit/latest/plugins/b105_hardcoded_password_string.html", - "tool": "bandit" - }, - { - "category": "sast", - "message": "Consider possible security implications associated with subprocess module.", - "cve": "python/imports/imports-function.py:5858400c2f39047787702de44d03361ef8d954c9d14bd54ee1c2bef9e6a7df93:B404", - "severity": "Low", - "confidence": "High", - "scanner": { - "id": "bandit", - "name": "Bandit" - }, - "location": { - "file": "python/imports/imports-function.py", - "start_line": 4, - "end_line": 4 - }, - "identifiers": [ - { - "type": "bandit_test_id", - "name": "Bandit Test ID B404", - "value": "B404" - } - ], - "priority": "Low", - "file": "python/imports/imports-function.py", - "line": 4, - "tool": "bandit" - }, - { - "category": "sast", - "message": "Consider possible security implications associated with pickle module.", - "cve": "python/imports/imports-function.py:dbda3cf4190279d30e0aad7dd137eca11272b0b225e8af4e8bf39682da67d956:B403", - "severity": "Low", - "confidence": "High", - "scanner": { - "id": "bandit", - "name": "Bandit" - }, - "location": { - "file": "python/imports/imports-function.py", - "start_line": 2, - "end_line": 2 - }, - "identifiers": [ - { - "type": "bandit_test_id", - "name": "Bandit Test ID B403", - "value": "B403" - } - ], - "priority": "Low", - "file": "python/imports/imports-function.py", - "line": 2, - "tool": "bandit" - }, - { - "category": "sast", - "message": "Consider possible security implications associated with Popen module.", - "cve": "python/imports/imports-from.py:eb8a0db9cd1a8c1ab39a77e6025021b1261cc2a0b026b2f4a11fca4e0636d8dd:B404", - "severity": "Low", - "confidence": "High", - "scanner": { - "id": "bandit", - "name": "Bandit" - }, - "location": { - "file": "python/imports/imports-from.py", - "start_line": 7, - "end_line": 7 - }, - "identifiers": [ - { - "type": "bandit_test_id", - "name": "Bandit Test ID B404", - "value": "B404" - } - ], - "priority": "Low", - "file": "python/imports/imports-from.py", - "line": 7, - "tool": "bandit" - }, - { - "category": "sast", - "message": "subprocess call with shell=True seems safe, but may be changed in the future, consider rewriting without shell", - "cve": "python/imports/imports-aliases.py:f99f9721e27537fbcb6699a4cf39c6740d6234d2c6f06cfc2d9ea977313c483d:B602", - "severity": "Low", - "confidence": "High", - "scanner": { - "id": "bandit", - "name": "Bandit" - }, - "location": { - "file": "python/imports/imports-aliases.py", - "start_line": 9, - "end_line": 9 - }, - "identifiers": [ - { - "type": "bandit_test_id", - "name": "Bandit Test ID B602", - "value": "B602", - "url": "https://docs.openstack.org/bandit/latest/plugins/b602_subprocess_popen_with_shell_equals_true.html" - } - ], - "priority": "Low", - "file": "python/imports/imports-aliases.py", - "line": 9, - "url": "https://docs.openstack.org/bandit/latest/plugins/b602_subprocess_popen_with_shell_equals_true.html", - "tool": "bandit" - }, - { - "category": "sast", - "message": "Consider possible security implications associated with subprocess module.", - "cve": "python/imports/imports-from.py:332a12ab1146698f614a905ce6a6a5401497a12281aef200e80522711c69dcf4:B404", - "severity": "Low", - "confidence": "High", - "scanner": { - "id": "bandit", - "name": "Bandit" - }, - "location": { - "file": "python/imports/imports-from.py", - "start_line": 6, - "end_line": 6 - }, - "identifiers": [ - { - "type": "bandit_test_id", - "name": "Bandit Test ID B404", - "value": "B404" - } - ], - "priority": "Low", - "file": "python/imports/imports-from.py", - "line": 6, - "tool": "bandit" - }, - { - "category": "sast", - "message": "Consider possible security implications associated with Popen module.", - "cve": "python/imports/imports-from.py:0a48de4a3d5348853a03666cb574697e3982998355e7a095a798bd02a5947276:B404", - "severity": "Low", - "confidence": "High", - "scanner": { - "id": "bandit", - "name": "Bandit" - }, - "location": { - "file": "python/imports/imports-from.py", - "start_line": 1, - "end_line": 2 - }, - "identifiers": [ - { - "type": "bandit_test_id", - "name": "Bandit Test ID B404", - "value": "B404" - } - ], - "priority": "Low", - "file": "python/imports/imports-from.py", - "line": 1, - "tool": "bandit" - }, - { - "category": "sast", - "message": "Consider possible security implications associated with pickle module.", - "cve": "python/imports/imports-aliases.py:51b71661dff994bde3529639a727a678c8f5c4c96f00d300913f6d5be1bbdf26:B403", - "severity": "Low", - "confidence": "High", - "scanner": { - "id": "bandit", - "name": "Bandit" - }, - "location": { - "file": "python/imports/imports-aliases.py", - "start_line": 7, - "end_line": 8 - }, - "identifiers": [ - { - "type": "bandit_test_id", - "name": "Bandit Test ID B403", - "value": "B403" - } - ], - "priority": "Low", - "file": "python/imports/imports-aliases.py", - "line": 7, - "tool": "bandit" - }, - { - "category": "sast", - "message": "Consider possible security implications associated with loads module.", - "cve": "python/imports/imports-aliases.py:6ff02aeb3149c01ab68484d794a94f58d5d3e3bb0d58557ef4153644ea68ea54:B403", - "severity": "Low", - "confidence": "High", - "scanner": { - "id": "bandit", - "name": "Bandit" - }, - "location": { - "file": "python/imports/imports-aliases.py", - "start_line": 6, - "end_line": 6 - }, - "identifiers": [ - { - "type": "bandit_test_id", - "name": "Bandit Test ID B403", - "value": "B403" - } - ], - "priority": "Low", - "file": "python/imports/imports-aliases.py", - "line": 6, - "tool": "bandit" - }, - { - "category": "sast", - "message": "Statically-sized arrays can be improperly restricted, leading to potential overflows or other issues (CWE-119!/CWE-120)", - "cve": "c/subdir/utils.c:b466873101951fe96e1332f6728eb7010acbbd5dfc3b65d7d53571d091a06d9e:CWE-119!/CWE-120", - "confidence": "Low", - "solution": "Perform bounds checking, use functions that limit length, or ensure that the size is larger than the maximum possible length", - "scanner": { - "id": "flawfinder", - "name": "Flawfinder" - }, - "location": { - "file": "c/subdir/utils.c", - "start_line": 4 - }, - "identifiers": [ - { - "type": "cwe", - "name": "CWE-119", - "value": "119", - "url": "https://cwe.mitre.org/data/definitions/119.html" - }, - { - "type": "cwe", - "name": "CWE-120", - "value": "120", - "url": "https://cwe.mitre.org/data/definitions/120.html" - } - ], - "file": "c/subdir/utils.c", - "line": 4, - "url": "https://cwe.mitre.org/data/definitions/119.html", - "tool": "flawfinder" - }, - { - "category": "sast", - "message": "Check when opening files - can an attacker redirect it (via symlinks), force the opening of special file type (e.g., device files), move things around to create a race condition, control its ancestors, or change its contents? (CWE-362)", - "cve": "c/subdir/utils.c:bab681140fcc8fc3085b6bba74081b44ea145c1c98b5e70cf19ace2417d30770:CWE-362", - "confidence": "Low", - "scanner": { - "id": "flawfinder", - "name": "Flawfinder" - }, - "location": { - "file": "c/subdir/utils.c", - "start_line": 8 - }, - "identifiers": [ - { - "type": "cwe", - "name": "CWE-362", - "value": "362", - "url": "https://cwe.mitre.org/data/definitions/362.html" - } - ], - "file": "c/subdir/utils.c", - "line": 8, - "url": "https://cwe.mitre.org/data/definitions/362.html", - "tool": "flawfinder" - }, - { - "category": "sast", - "message": "Statically-sized arrays can be improperly restricted, leading to potential overflows or other issues (CWE-119!/CWE-120)", - "cve": "cplusplus/src/hello.cpp:c8c6dd0afdae6814194cf0930b719f757ab7b379cf8f261e7f4f9f2f323a818a:CWE-119!/CWE-120", - "confidence": "Low", - "solution": "Perform bounds checking, use functions that limit length, or ensure that the size is larger than the maximum possible length", - "scanner": { - "id": "flawfinder", - "name": "Flawfinder" - }, - "location": { - "file": "cplusplus/src/hello.cpp", - "start_line": 6 - }, - "identifiers": [ - { - "type": "cwe", - "name": "CWE-119", - "value": "119", - "url": "https://cwe.mitre.org/data/definitions/119.html" - }, - { - "type": "cwe", - "name": "CWE-120", - "value": "120", - "url": "https://cwe.mitre.org/data/definitions/120.html" - } - ], - "file": "cplusplus/src/hello.cpp", - "line": 6, - "url": "https://cwe.mitre.org/data/definitions/119.html", - "tool": "flawfinder" - }, - { - "category": "sast", - "message": "Does not check for buffer overflows when copying to destination [MS-banned] (CWE-120)", - "cve": "cplusplus/src/hello.cpp:331c04062c4fe0c7c486f66f59e82ad146ab33cdd76ae757ca41f392d568cbd0:CWE-120", - "confidence": "Low", - "solution": "Consider using snprintf, strcpy_s, or strlcpy (warning: strncpy easily misused)", - "scanner": { - "id": "flawfinder", - "name": "Flawfinder" - }, - "location": { - "file": "cplusplus/src/hello.cpp", - "start_line": 7 - }, - "identifiers": [ - { - "type": "cwe", - "name": "CWE-120", - "value": "120", - "url": "https://cwe.mitre.org/data/definitions/120.html" - } - ], - "file": "cplusplus/src/hello.cpp", - "line": 7, - "url": "https://cwe.mitre.org/data/definitions/120.html", - "tool": "flawfinder" - } - ] -} diff --git a/spec/fixtures/security-reports/master.zip b/spec/fixtures/security-reports/master.zip Binary files differdeleted file mode 100644 index 2261b5a1674..00000000000 --- a/spec/fixtures/security-reports/master.zip +++ /dev/null diff --git a/spec/fixtures/security-reports/master/gl-container-scanning-report.json b/spec/fixtures/security-reports/master/gl-container-scanning-report.json deleted file mode 100644 index 03dfc647162..00000000000 --- a/spec/fixtures/security-reports/master/gl-container-scanning-report.json +++ /dev/null @@ -1,105 +0,0 @@ -{ - "image": "registry.gitlab.com/groulot/container-scanning-test/master:5f21de6956aee99ddb68ae49498662d9872f50ff", - "unapproved": [ - "CVE-2017-18269", - "CVE-2017-16997", - "CVE-2018-1000001", - "CVE-2016-10228", - "CVE-2018-18520", - "CVE-2010-4052", - "CVE-2018-16869", - "CVE-2018-18311" - ], - "vulnerabilities": [ - { - "featurename": "glibc", - "featureversion": "2.24-11+deb9u3", - "vulnerability": "CVE-2017-18269", - "namespace": "debian:9", - "description": "SSE2-optimized memmove implementation problem.", - "link": "https://security-tracker.debian.org/tracker/CVE-2017-18269", - "severity": "Defcon1", - "fixedby": "2.24-11+deb9u4" - }, - { - "featurename": "glibc", - "featureversion": "2.24-11+deb9u3", - "vulnerability": "CVE-2017-16997", - "namespace": "debian:9", - "description": "elf/dl-load.c in the GNU C Library (aka glibc or libc6) 2.19 through 2.26 mishandles RPATH and RUNPATH containing $ORIGIN for a privileged (setuid or AT_SECURE) program, which allows local users to gain privileges via a Trojan horse library in the current working directory, related to the fillin_rpath and decompose_rpath functions. This is associated with misinterpretion of an empty RPATH/RUNPATH token as the \"./\" directory. NOTE: this configuration of RPATH/RUNPATH for a privileged program is apparently very uncommon; most likely, no such program is shipped with any common Linux distribution.", - "link": "https://security-tracker.debian.org/tracker/CVE-2017-16997", - "severity": "Critical", - "fixedby": "" - }, - { - "featurename": "glibc", - "featureversion": "2.24-11+deb9u3", - "vulnerability": "CVE-2018-1000001", - "namespace": "debian:9", - "description": "In glibc 2.26 and earlier there is confusion in the usage of getcwd() by realpath() which can be used to write before the destination buffer leading to a buffer underflow and potential code execution.", - "link": "https://security-tracker.debian.org/tracker/CVE-2018-1000001", - "severity": "High", - "fixedby": "" - }, - { - "featurename": "glibc", - "featureversion": "2.24-11+deb9u3", - "vulnerability": "CVE-2016-10228", - "namespace": "debian:9", - "description": "The iconv program in the GNU C Library (aka glibc or libc6) 2.25 and earlier, when invoked with the -c option, enters an infinite loop when processing invalid multi-byte input sequences, leading to a denial of service.", - "link": "https://security-tracker.debian.org/tracker/CVE-2016-10228", - "severity": "Medium", - "fixedby": "" - }, - { - "featurename": "elfutils", - "featureversion": "0.168-1", - "vulnerability": "CVE-2018-18520", - "namespace": "debian:9", - "description": "An Invalid Memory Address Dereference exists in the function elf_end in libelf in elfutils through v0.174. Although eu-size is intended to support ar files inside ar files, handle_ar in size.c closes the outer ar file before handling all inner entries. The vulnerability allows attackers to cause a denial of service (application crash) with a crafted ELF file.", - "link": "https://security-tracker.debian.org/tracker/CVE-2018-18520", - "severity": "Low", - "fixedby": "" - }, - { - "featurename": "glibc", - "featureversion": "2.24-11+deb9u3", - "vulnerability": "CVE-2010-4052", - "namespace": "debian:9", - "description": "Stack consumption vulnerability in the regcomp implementation in the GNU C Library (aka glibc or libc6) through 2.11.3, and 2.12.x through 2.12.2, allows context-dependent attackers to cause a denial of service (resource exhaustion) via a regular expression containing adjacent repetition operators, as demonstrated by a {10,}{10,}{10,}{10,} sequence in the proftpd.gnu.c exploit for ProFTPD.", - "link": "https://security-tracker.debian.org/tracker/CVE-2010-4052", - "severity": "Negligible", - "fixedby": "" - }, - { - "featurename": "nettle", - "featureversion": "3.3-1", - "vulnerability": "CVE-2018-16869", - "namespace": "debian:9", - "description": "A Bleichenbacher type side-channel based padding oracle attack was found in the way nettle handles endian conversion of RSA decrypted PKCS#1 v1.5 data. An attacker who is able to run a process on the same physical core as the victim process, could use this flaw extract plaintext or in some cases downgrade any TLS connections to a vulnerable server.", - "link": "https://security-tracker.debian.org/tracker/CVE-2018-16869", - "severity": "Unknown", - "fixedby": "" - }, - { - "featurename": "perl", - "featureversion": "5.24.1-3+deb9u4", - "vulnerability": "CVE-2018-18311", - "namespace": "debian:9", - "description": "Perl before 5.26.3 and 5.28.x before 5.28.1 has a buffer overflow via a crafted regular expression that triggers invalid write operations.", - "link": "https://security-tracker.debian.org/tracker/CVE-2018-18311", - "severity": "Unknown", - "fixedby": "5.24.1-3+deb9u5" - }, - { - "featurename": "foo", - "featureversion": "1.3", - "vulnerability": "CVE-2018-666", - "namespace": "debian:9", - "description": "Foo has a vulnerability nobody cares about and whitelist.", - "link": "https://security-tracker.debian.org/tracker/CVE-2018-666", - "severity": "Unknown", - "fixedby": "1.4" - } - ] -} diff --git a/spec/fixtures/security-reports/master/gl-dast-report.json b/spec/fixtures/security-reports/master/gl-dast-report.json deleted file mode 100644 index df459d9419d..00000000000 --- a/spec/fixtures/security-reports/master/gl-dast-report.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "site": [ - { - "alerts": [ - { - "sourceid": "3", - "wascid": "15", - "cweid": "16", - "reference": "<p>http://msdn.microsoft.com/en-us/library/ie/gg622941%28v=vs.85%29.aspx</p><p>https://www.owasp.org/index.php/List_of_useful_HTTP_headers</p>", - "otherinfo": "<p>This issue still applies to error type pages (401, 403, 500, etc) as those pages are often still affected by injection issues, in which case there is still concern for browsers sniffing pages away from their actual content type.</p><p>At \"High\" threshold this scanner will not alert on client or server error responses.</p>", - "solution": "<p>Ensure that the application/web server sets the Content-Type header appropriately, and that it sets the X-Content-Type-Options header to 'nosniff' for all web pages.</p><p>If possible, ensure that the end user uses a standards-compliant and modern web browser that does not perform MIME-sniffing at all, or that can be directed by the web application/web server to not perform MIME-sniffing.</p>", - "count": "2", - "pluginid": "10021", - "alert": "X-Content-Type-Options Header Missing", - "name": "X-Content-Type-Options Header Missing", - "riskcode": "1", - "confidence": "2", - "riskdesc": "Low (Medium)", - "desc": "<p>The Anti-MIME-Sniffing header X-Content-Type-Options was not set to 'nosniff'. This allows older versions of Internet Explorer and Chrome to perform MIME-sniffing on the response body, potentially causing the response body to be interpreted and displayed as a content type other than the declared content type. Current (early 2014) and legacy versions of Firefox will use the declared content type (if one is set), rather than performing MIME-sniffing.</p>", - "instances": [ - { - "param": "X-Content-Type-Options", - "method": "GET", - "uri": "http://bikebilly-spring-auto-devops-review-feature-br-3y2gpb.35.192.176.43.xip.io" - }, - { - "param": "X-Content-Type-Options", - "method": "GET", - "uri": "http://bikebilly-spring-auto-devops-review-feature-br-3y2gpb.35.192.176.43.xip.io/" - } - ] - } - ], - "@ssl": "false", - "@port": "80", - "@host": "bikebilly-spring-auto-devops-review-feature-br-3y2gpb.35.192.176.43.xip.io", - "@name": "http://bikebilly-spring-auto-devops-review-feature-br-3y2gpb.35.192.176.43.xip.io" - } - ], - "@generated": "Fri, 13 Apr 2018 09:22:01", - "@version": "2.7.0" -} diff --git a/spec/fixtures/security-reports/master/gl-dependency-scanning-report.json b/spec/fixtures/security-reports/master/gl-dependency-scanning-report.json deleted file mode 100644 index 8555be6618c..00000000000 --- a/spec/fixtures/security-reports/master/gl-dependency-scanning-report.json +++ /dev/null @@ -1,181 +0,0 @@ -{ - "version": "1.3", - "vulnerabilities": [ - { - "category": "dependency_scanning", - "name": "io.netty/netty - CVE-2014-3488", - "message": "DoS by CPU exhaustion when using malicious SSL packets", - "cve": "app/pom.xml:io.netty/netty@3.9.1.Final:CVE-2014-3488", - "severity": "Unknown", - "solution": "Upgrade to the latest version", - "scanner": { - "id": "gemnasium", - "name": "Gemnasium" - }, - "location": { - "file": "app/pom.xml", - "dependency": { - "package": { - "name": "io.netty/netty" - }, - "version": "3.9.1.Final" - } - }, - "identifiers": [ - { - "type": "gemnasium", - "name": "Gemnasium-d1bf36d9-9f07-46cd-9cfc-8675338ada8f", - "value": "d1bf36d9-9f07-46cd-9cfc-8675338ada8f", - "url": "https://deps.sec.gitlab.com/packages/maven/io.netty/netty/versions/3.9.1.Final/advisories" - }, - { - "type": "cve", - "name": "CVE-2014-3488", - "value": "CVE-2014-3488", - "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2014-3488" - } - ], - "links": [ - { - "url": "https://bugzilla.redhat.com/CVE-2014-3488" - }, - { - "url": "http://netty.io/news/2014/06/11/3.html" - }, - { - "url": "https://github.com/netty/netty/issues/2562" - } - ], - "priority": "Unknown", - "file": "app/pom.xml", - "url": "https://bugzilla.redhat.com/CVE-2014-3488", - "tool": "gemnasium" - }, - { - "category": "dependency_scanning", - "name": "Django - CVE-2017-12794", - "message": "Possible XSS in traceback section of technical 500 debug page", - "cve": "app/requirements.txt:Django@1.11.3:CVE-2017-12794", - "severity": "Unknown", - "solution": "Upgrade to latest version or apply patch.", - "scanner": { - "id": "gemnasium", - "name": "Gemnasium" - }, - "location": { - "file": "app/requirements.txt", - "dependency": { - "package": { - "name": "Django" - }, - "version": "1.11.3" - } - }, - "identifiers": [ - { - "type": "gemnasium", - "name": "Gemnasium-6162a015-8635-4a15-8d7c-dc9321db366f", - "value": "6162a015-8635-4a15-8d7c-dc9321db366f", - "url": "https://deps.sec.gitlab.com/packages/pypi/Django/versions/1.11.3/advisories" - }, - { - "type": "cve", - "name": "CVE-2017-12794", - "value": "CVE-2017-12794", - "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-12794" - } - ], - "links": [ - { - "url": "https://www.djangoproject.com/weblog/2017/sep/05/security-releases/" - } - ], - "priority": "Unknown", - "file": "app/requirements.txt", - "url": "https://www.djangoproject.com/weblog/2017/sep/05/security-releases/", - "tool": "gemnasium" - }, - { - "category": "dependency_scanning", - "name": "nokogiri - USN-3424-1", - "message": "Vulnerabilities in libxml2", - "cve": "rails/Gemfile.lock:nokogiri@1.8.0:USN-3424-1", - "severity": "Unknown", - "solution": "Upgrade to latest version.", - "scanner": { - "id": "gemnasium", - "name": "Gemnasium" - }, - "location": { - "file": "rails/Gemfile.lock", - "dependency": { - "package": { - "name": "nokogiri" - }, - "version": "1.8.0" - } - }, - "identifiers": [ - { - "type": "gemnasium", - "name": "Gemnasium-06565b64-486d-4326-b906-890d9915804d", - "value": "06565b64-486d-4326-b906-890d9915804d", - "url": "https://deps.sec.gitlab.com/packages/gem/nokogiri/versions/1.8.0/advisories" - }, - { - "type": "usn", - "name": "USN-3424-1", - "value": "USN-3424-1", - "url": "https://usn.ubuntu.com/3424-1/" - } - ], - "links": [ - { - "url": "https://github.com/sparklemotion/nokogiri/issues/1673" - } - ], - "priority": "Unknown", - "file": "rails/Gemfile.lock", - "url": "https://github.com/sparklemotion/nokogiri/issues/1673", - "tool": "gemnasium" - }, - { - "category": "dependency_scanning", - "name": "ffi - CVE-2018-1000201", - "message": "ruby-ffi DDL loading issue on Windows OS", - "cve": "ffi:1.9.18:CVE-2018-1000201", - "severity": "High", - "solution": "upgrade to \u003e= 1.9.24", - "scanner": { - "id": "bundler_audit", - "name": "bundler-audit" - }, - "location": { - "file": "sast-sample-rails/Gemfile.lock", - "dependency": { - "package": { - "name": "ffi" - }, - "version": "1.9.18" - } - }, - "identifiers": [ - { - "type": "cve", - "name": "CVE-2018-1000201", - "value": "CVE-2018-1000201", - "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2018-1000201" - } - ], - "links": [ - { - "url": "https://github.com/ffi/ffi/releases/tag/1.9.24" - } - ], - "priority": "High", - "file": "sast-sample-rails/Gemfile.lock", - "url": "https://github.com/ffi/ffi/releases/tag/1.9.24", - "tool": "bundler_audit" - } - ] -} diff --git a/spec/fixtures/security-reports/master/gl-license-management-report.json b/spec/fixtures/security-reports/master/gl-license-management-report.json deleted file mode 100644 index e0de6f58fdf..00000000000 --- a/spec/fixtures/security-reports/master/gl-license-management-report.json +++ /dev/null @@ -1,817 +0,0 @@ -{ - "licenses": [ - { - "count": 52, - "name": "MIT" - }, - { - "count": 3, - "name": "New BSD" - }, - { - "count": 1, - "name": "Apache 2.0" - }, - { - "count": 1, - "name": "unknown" - } - ], - "dependencies": [ - { - "license": { - "name": "MIT", - "url": "http://opensource.org/licenses/mit-license" - }, - "dependency": { - "name": "actioncable", - "url": "http://rubyonrails.org", - "description": "WebSocket framework for Rails.", - "pathes": [ - "." - ] - } - }, - { - "license": { - "name": "MIT", - "url": "http://opensource.org/licenses/mit-license" - }, - "dependency": { - "name": "actionmailer", - "url": "http://rubyonrails.org", - "description": "Email composition, delivery, and receiving framework (part of Rails).", - "pathes": [ - "." - ] - } - }, - { - "license": { - "name": "MIT", - "url": "http://opensource.org/licenses/mit-license" - }, - "dependency": { - "name": "actionpack", - "url": "http://rubyonrails.org", - "description": "Web-flow and rendering framework putting the VC in MVC (part of Rails).", - "pathes": [ - "." - ] - } - }, - { - "license": { - "name": "MIT", - "url": "http://opensource.org/licenses/mit-license" - }, - "dependency": { - "name": "actionview", - "url": "http://rubyonrails.org", - "description": "Rendering framework putting the V in MVC (part of Rails).", - "pathes": [ - "." - ] - } - }, - { - "license": { - "name": "MIT", - "url": "http://opensource.org/licenses/mit-license" - }, - "dependency": { - "name": "activejob", - "url": "http://rubyonrails.org", - "description": "Job framework with pluggable queues.", - "pathes": [ - "." - ] - } - }, - { - "license": { - "name": "MIT", - "url": "http://opensource.org/licenses/mit-license" - }, - "dependency": { - "name": "activemodel", - "url": "http://rubyonrails.org", - "description": "A toolkit for building modeling frameworks (part of Rails).", - "pathes": [ - "." - ] - } - }, - { - "license": { - "name": "MIT", - "url": "http://opensource.org/licenses/mit-license" - }, - "dependency": { - "name": "activerecord", - "url": "http://rubyonrails.org", - "description": "Object-relational mapper framework (part of Rails).", - "pathes": [ - "." - ] - } - }, - { - "license": { - "name": "MIT", - "url": "http://opensource.org/licenses/mit-license" - }, - "dependency": { - "name": "activesupport", - "url": "http://rubyonrails.org", - "description": "A toolkit of support libraries and Ruby core extensions extracted from the Rails framework.", - "pathes": [ - "." - ] - } - }, - { - "license": { - "name": "MIT", - "url": "http://opensource.org/licenses/mit-license" - }, - "dependency": { - "name": "arel", - "url": "https://github.com/rails/arel", - "description": "Arel Really Exasperates Logicians Arel is a SQL AST manager for Ruby", - "pathes": [ - "." - ] - } - }, - { - "license": { - "name": "MIT", - "url": "http://opensource.org/licenses/mit-license" - }, - "dependency": { - "name": "builder", - "url": "http://onestepback.org", - "description": "Builders for MarkUp.", - "pathes": [ - "." - ] - } - }, - { - "license": { - "name": "MIT", - "url": "http://opensource.org/licenses/mit-license" - }, - "dependency": { - "name": "bundler", - "url": "http://bundler.io", - "description": "The best way to manage your application's dependencies", - "pathes": [ - "." - ] - } - }, - { - "license": { - "name": "MIT", - "url": "http://opensource.org/licenses/mit-license" - }, - "dependency": { - "name": "coffee-rails", - "url": "https://github.com/rails/coffee-rails", - "description": "CoffeeScript adapter for the Rails asset pipeline.", - "pathes": [ - "." - ] - } - }, - { - "license": { - "name": "MIT", - "url": "http://opensource.org/licenses/mit-license" - }, - "dependency": { - "name": "coffee-script", - "url": "http://github.com/josh/ruby-coffee-script", - "description": "Ruby CoffeeScript Compiler", - "pathes": [ - "." - ] - } - }, - { - "license": { - "name": "MIT", - "url": "http://opensource.org/licenses/mit-license" - }, - "dependency": { - "name": "coffee-script-source", - "url": "http://coffeescript.org", - "description": "The CoffeeScript Compiler", - "pathes": [ - "." - ] - } - }, - { - "license": { - "name": "MIT", - "url": "http://opensource.org/licenses/mit-license" - }, - "dependency": { - "name": "concurrent-ruby", - "url": "http://www.concurrent-ruby.com", - "description": "Modern concurrency tools for Ruby. Inspired by Erlang, Clojure, Scala, Haskell, F#, C#, Java, and classic concurrency patterns.", - "pathes": [ - "." - ] - } - }, - { - "license": { - "name": "MIT", - "url": "http://opensource.org/licenses/mit-license" - }, - "dependency": { - "name": "crass", - "url": "https://github.com/rgrove/crass/", - "description": "CSS parser based on the CSS Syntax Level 3 spec.", - "pathes": [ - "." - ] - } - }, - { - "license": { - "name": "MIT", - "url": "http://opensource.org/licenses/mit-license" - }, - "dependency": { - "name": "erubis", - "url": "http://www.kuwata-lab.com/erubis/", - "description": "a fast and extensible eRuby implementation which supports multi-language", - "pathes": [ - "." - ] - } - }, - { - "license": { - "name": "MIT", - "url": "http://opensource.org/licenses/mit-license" - }, - "dependency": { - "name": "execjs", - "url": "https://github.com/rails/execjs", - "description": "Run JavaScript code from Ruby", - "pathes": [ - "." - ] - } - }, - { - "license": { - "name": "New BSD", - "url": "http://opensource.org/licenses/BSD-3-Clause" - }, - "dependency": { - "name": "ffi", - "url": "http://wiki.github.com/ffi/ffi", - "description": "Ruby FFI", - "pathes": [ - "." - ] - } - }, - { - "license": { - "name": "MIT", - "url": "http://opensource.org/licenses/mit-license" - }, - "dependency": { - "name": "globalid", - "url": "http://www.rubyonrails.org", - "description": "Refer to any model with a URI: gid://app/class/id", - "pathes": [ - "." - ] - } - }, - { - "license": { - "name": "MIT", - "url": "http://opensource.org/licenses/mit-license" - }, - "dependency": { - "name": "i18n", - "url": "http://github.com/svenfuchs/i18n", - "description": "New wave Internationalization support for Ruby", - "pathes": [ - "." - ] - } - }, - { - "license": { - "name": "MIT", - "url": "http://opensource.org/licenses/mit-license" - }, - "dependency": { - "name": "jbuilder", - "url": "https://github.com/rails/jbuilder", - "description": "Create JSON structures via a Builder-style DSL", - "pathes": [ - "." - ] - } - }, - { - "license": { - "name": "MIT", - "url": "http://opensource.org/licenses/mit-license" - }, - "dependency": { - "name": "loofah", - "description": "", - "pathes": [ - "." - ] - } - }, - { - "license": { - "name": "MIT", - "url": "http://opensource.org/licenses/mit-license" - }, - "dependency": { - "name": "mail", - "url": "https://github.com/mikel/mail", - "description": "Mail provides a nice Ruby DSL for making, sending and reading emails.", - "pathes": [ - "." - ] - } - }, - { - "license": { - "name": "MIT", - "url": "http://opensource.org/licenses/mit-license" - }, - "dependency": { - "name": "method_source", - "url": "http://banisterfiend.wordpress.com", - "description": "retrieve the sourcecode for a method", - "pathes": [ - "." - ] - } - }, - { - "license": { - "name": "MIT", - "url": "http://opensource.org/licenses/mit-license" - }, - "dependency": { - "name": "mini_mime", - "url": "https://github.com/discourse/mini_mime", - "description": "A lightweight mime type lookup toy", - "pathes": [ - "." - ] - } - }, - { - "license": { - "name": "MIT", - "url": "http://opensource.org/licenses/mit-license" - }, - "dependency": { - "name": "mini_portile2", - "url": "http://github.com/flavorjones/mini_portile", - "description": "Simplistic port-like solution for developers", - "pathes": [ - "." - ] - } - }, - { - "license": { - "name": "MIT", - "url": "http://opensource.org/licenses/mit-license" - }, - "dependency": { - "name": "minitest", - "url": "https://github.com/seattlerb/minitest", - "description": "minitest provides a complete suite of testing facilities supporting TDD, BDD, mocking, and benchmarking", - "pathes": [ - "." - ] - } - }, - { - "license": { - "name": "MIT", - "url": "http://opensource.org/licenses/mit-license" - }, - "dependency": { - "name": "multi_json", - "url": "http://github.com/intridea/multi_json", - "description": "A common interface to multiple JSON libraries.", - "pathes": [ - "." - ] - } - }, - { - "license": { - "name": "MIT", - "url": "http://opensource.org/licenses/mit-license" - }, - "dependency": { - "name": "nio4r", - "url": "https://github.com/celluloid/nio4r", - "description": "NIO provides a high performance selector API for monitoring IO objects", - "pathes": [ - "." - ] - } - }, - { - "license": { - "name": "MIT", - "url": "http://opensource.org/licenses/mit-license" - }, - "dependency": { - "name": "nokogiri", - "url": "http://nokogiri.org", - "description": "Nokogiri (鋸) is an HTML, XML, SAX, and Reader parser", - "pathes": [ - "." - ] - } - }, - { - "license": { - "name": "New BSD", - "url": "http://opensource.org/licenses/BSD-3-Clause" - }, - "dependency": { - "name": "puma", - "url": "http://puma.io", - "description": "Puma is a simple, fast, threaded, and highly concurrent HTTP 1.1 server for Ruby/Rack applications", - "pathes": [ - "." - ] - } - }, - { - "license": { - "name": "MIT", - "url": "http://opensource.org/licenses/mit-license" - }, - "dependency": { - "name": "rack", - "url": "https://rack.github.io/", - "description": "a modular Ruby webserver interface", - "pathes": [ - "." - ] - } - }, - { - "license": { - "name": "MIT", - "url": "http://opensource.org/licenses/mit-license" - }, - "dependency": { - "name": "rack-test", - "url": "http://github.com/brynary/rack-test", - "description": "Simple testing API built on Rack", - "pathes": [ - "." - ] - } - }, - { - "license": { - "name": "MIT", - "url": "http://opensource.org/licenses/mit-license" - }, - "dependency": { - "name": "rails", - "url": "http://rubyonrails.org", - "description": "Full-stack web application framework.", - "pathes": [ - "." - ] - } - }, - { - "license": { - "name": "MIT", - "url": "http://opensource.org/licenses/mit-license" - }, - "dependency": { - "name": "rails-dom-testing", - "url": "https://github.com/rails/rails-dom-testing", - "description": "Dom and Selector assertions for Rails applications", - "pathes": [ - "." - ] - } - }, - { - "license": { - "name": "MIT", - "url": "http://opensource.org/licenses/mit-license" - }, - "dependency": { - "name": "rails-html-sanitizer", - "url": "https://github.com/rails/rails-html-sanitizer", - "description": "This gem is responsible to sanitize HTML fragments in Rails applications.", - "pathes": [ - "." - ] - } - }, - { - "license": { - "name": "MIT", - "url": "http://opensource.org/licenses/mit-license" - }, - "dependency": { - "name": "railties", - "url": "http://rubyonrails.org", - "description": "Tools for creating, working with, and running Rails applications.", - "pathes": [ - "." - ] - } - }, - { - "license": { - "name": "MIT", - "url": "http://opensource.org/licenses/mit-license" - }, - "dependency": { - "name": "rake", - "url": "https://github.com/ruby/rake", - "description": "Rake is a Make-like program implemented in Ruby", - "pathes": [ - "." - ] - } - }, - { - "license": { - "name": "MIT", - "url": "http://opensource.org/licenses/mit-license" - }, - "dependency": { - "name": "rb-fsevent", - "url": "http://rubygems.org/gems/rb-fsevent", - "description": "Very simple & usable FSEvents API", - "pathes": [ - "." - ] - } - }, - { - "license": { - "name": "MIT", - "url": "http://opensource.org/licenses/mit-license" - }, - "dependency": { - "name": "rb-inotify", - "url": "https://github.com/guard/rb-inotify", - "description": "A Ruby wrapper for Linux inotify, using FFI", - "pathes": [ - "." - ] - } - }, - { - "license": { - "name": "unknown" - }, - "dependency": { - "name": "ruby-bundler-rails", - "description": "", - "pathes": [ - "." - ] - } - }, - { - "license": { - "name": "MIT", - "url": "http://opensource.org/licenses/mit-license" - }, - "dependency": { - "name": "sass", - "url": "http://sass-lang.com/", - "description": "A powerful but elegant CSS compiler that makes CSS fun again.", - "pathes": [ - "." - ] - } - }, - { - "license": { - "name": "MIT", - "url": "http://opensource.org/licenses/mit-license" - }, - "dependency": { - "name": "sass-listen", - "url": "https://github.com/sass/listen", - "description": "Fork of guard/listen", - "pathes": [ - "." - ] - } - }, - { - "license": { - "name": "MIT", - "url": "http://opensource.org/licenses/mit-license" - }, - "dependency": { - "name": "sass-rails", - "url": "https://github.com/rails/sass-rails", - "description": "Sass adapter for the Rails asset pipeline.", - "pathes": [ - "." - ] - } - }, - { - "license": { - "name": "MIT", - "url": "http://opensource.org/licenses/mit-license" - }, - "dependency": { - "name": "sprockets", - "url": "https://github.com/rails/sprockets", - "description": "Rack-based asset packaging system", - "pathes": [ - "." - ] - } - }, - { - "license": { - "name": "MIT", - "url": "http://opensource.org/licenses/mit-license" - }, - "dependency": { - "name": "sprockets-rails", - "url": "https://github.com/rails/sprockets-rails", - "description": "Sprockets Rails integration", - "pathes": [ - "." - ] - } - }, - { - "license": { - "name": "New BSD", - "url": "http://opensource.org/licenses/BSD-3-Clause" - }, - "dependency": { - "name": "sqlite3", - "url": "https://github.com/sparklemotion/sqlite3-ruby", - "description": "This module allows Ruby programs to interface with the SQLite3 database engine (http://www.sqlite.org)", - "pathes": [ - "." - ] - } - }, - { - "license": { - "name": "MIT", - "url": "http://opensource.org/licenses/mit-license" - }, - "dependency": { - "name": "thor", - "url": "http://whatisthor.com/", - "description": "Thor is a toolkit for building powerful command-line interfaces.", - "pathes": [ - "." - ] - } - }, - { - "license": { - "name": "Apache 2.0", - "url": "http://www.apache.org/licenses/LICENSE-2.0.txt" - }, - "dependency": { - "name": "thread_safe", - "url": "https://github.com/ruby-concurrency/thread_safe", - "description": "Thread-safe collections and utilities for Ruby", - "pathes": [ - "." - ] - } - }, - { - "license": { - "name": "MIT", - "url": "http://opensource.org/licenses/mit-license" - }, - "dependency": { - "name": "tilt", - "url": "http://github.com/rtomayko/tilt/", - "description": "Generic interface to multiple Ruby template engines", - "pathes": [ - "." - ] - } - }, - { - "license": { - "name": "MIT", - "url": "http://opensource.org/licenses/mit-license" - }, - "dependency": { - "name": "turbolinks", - "url": "https://github.com/turbolinks/turbolinks", - "description": "Turbolinks makes navigating your web application faster", - "pathes": [ - "." - ] - } - }, - { - "license": { - "name": "MIT", - "url": "http://opensource.org/licenses/mit-license" - }, - "dependency": { - "name": "turbolinks-source", - "url": "https://github.com/turbolinks/turbolinks-source-gem", - "description": "Turbolinks JavaScript assets", - "pathes": [ - "." - ] - } - }, - { - "license": { - "name": "MIT", - "url": "http://opensource.org/licenses/mit-license" - }, - "dependency": { - "name": "tzinfo", - "url": "http://tzinfo.github.io", - "description": "Daylight savings aware timezone library", - "pathes": [ - "." - ] - } - }, - { - "license": { - "name": "MIT", - "url": "http://opensource.org/licenses/mit-license" - }, - "dependency": { - "name": "uglifier", - "url": "http://github.com/lautis/uglifier", - "description": "Ruby wrapper for UglifyJS JavaScript compressor", - "pathes": [ - "." - ] - } - }, - { - "license": { - "name": "MIT", - "url": "http://opensource.org/licenses/mit-license" - }, - "dependency": { - "name": "websocket-driver", - "url": "http://github.com/faye/websocket-driver-ruby", - "description": "WebSocket protocol handler with pluggable I/O", - "pathes": [ - "." - ] - } - }, - { - "license": { - "name": "MIT", - "url": "http://opensource.org/licenses/mit-license" - }, - "dependency": { - "name": "websocket-extensions", - "url": "https://github.com/faye/websocket-extensions-ruby", - "description": "Generic extension manager for WebSocket connections", - "pathes": [ - "." - ] - } - } - ] -} diff --git a/spec/fixtures/security-reports/master/gl-sast-report.json b/spec/fixtures/security-reports/master/gl-sast-report.json deleted file mode 100644 index 345e1e9f83a..00000000000 --- a/spec/fixtures/security-reports/master/gl-sast-report.json +++ /dev/null @@ -1,967 +0,0 @@ -{ - "version": "1.2", - "vulnerabilities": [ - { - "category": "sast", - "message": "Probable insecure usage of temp file/directory.", - "cve": "python/hardcoded/hardcoded-tmp.py:52865813c884a507be1f152d654245af34aba8a391626d01f1ab6d3f52ec8779:B108", - "severity": "Medium", - "confidence": "Medium", - "scanner": { - "id": "bandit", - "name": "Bandit" - }, - "location": { - "file": "python/hardcoded/hardcoded-tmp.py", - "start_line": 1, - "end_line": 1 - }, - "identifiers": [ - { - "type": "bandit_test_id", - "name": "Bandit Test ID B108", - "value": "B108", - "url": "https://docs.openstack.org/bandit/latest/plugins/b108_hardcoded_tmp_directory.html" - } - ], - "priority": "Medium", - "file": "python/hardcoded/hardcoded-tmp.py", - "line": 1, - "url": "https://docs.openstack.org/bandit/latest/plugins/b108_hardcoded_tmp_directory.html", - "tool": "bandit" - }, - { - "category": "sast", - "name": "Predictable pseudorandom number generator", - "message": "Predictable pseudorandom number generator", - "cve": "groovy/src/main/java/com/gitlab/security_products/tests/App.groovy:47:PREDICTABLE_RANDOM", - "severity": "Medium", - "confidence": "Medium", - "scanner": { - "id": "find_sec_bugs", - "name": "Find Security Bugs" - }, - "location": { - "file": "groovy/src/main/java/com/gitlab/security_products/tests/App.groovy", - "start_line": 47, - "end_line": 47, - "class": "com.gitlab.security_products.tests.App", - "method": "generateSecretToken2" - }, - "identifiers": [ - { - "type": "find_sec_bugs_type", - "name": "Find Security Bugs-PREDICTABLE_RANDOM", - "value": "PREDICTABLE_RANDOM", - "url": "https://find-sec-bugs.github.io/bugs.htm#PREDICTABLE_RANDOM" - } - ], - "priority": "Medium", - "file": "groovy/src/main/java/com/gitlab/security_products/tests/App.groovy", - "line": 47, - "url": "https://find-sec-bugs.github.io/bugs.htm#PREDICTABLE_RANDOM", - "tool": "find_sec_bugs" - }, - { - "category": "sast", - "name": "Predictable pseudorandom number generator", - "message": "Predictable pseudorandom number generator", - "cve": "groovy/src/main/java/com/gitlab/security_products/tests/App.groovy:41:PREDICTABLE_RANDOM", - "severity": "Medium", - "confidence": "Medium", - "scanner": { - "id": "find_sec_bugs", - "name": "Find Security Bugs" - }, - "location": { - "file": "groovy/src/main/java/com/gitlab/security_products/tests/App.groovy", - "start_line": 41, - "end_line": 41, - "class": "com.gitlab.security_products.tests.App", - "method": "generateSecretToken1" - }, - "identifiers": [ - { - "type": "find_sec_bugs_type", - "name": "Find Security Bugs-PREDICTABLE_RANDOM", - "value": "PREDICTABLE_RANDOM", - "url": "https://find-sec-bugs.github.io/bugs.htm#PREDICTABLE_RANDOM" - } - ], - "priority": "Medium", - "file": "groovy/src/main/java/com/gitlab/security_products/tests/App.groovy", - "line": 41, - "url": "https://find-sec-bugs.github.io/bugs.htm#PREDICTABLE_RANDOM", - "tool": "find_sec_bugs" - }, - { - "category": "sast", - "message": "Use of insecure MD2, MD4, or MD5 hash function.", - "cve": "python/imports/imports-aliases.py:cb203b465dffb0cb3a8e8bd8910b84b93b0a5995a938e4b903dbb0cd6ffa1254:B303", - "severity": "Medium", - "confidence": "High", - "scanner": { - "id": "bandit", - "name": "Bandit" - }, - "location": { - "file": "python/imports/imports-aliases.py", - "start_line": 11, - "end_line": 11 - }, - "identifiers": [ - { - "type": "bandit_test_id", - "name": "Bandit Test ID B303", - "value": "B303" - } - ], - "priority": "Medium", - "file": "python/imports/imports-aliases.py", - "line": 11, - "tool": "bandit" - }, - { - "category": "sast", - "message": "Use of insecure MD2, MD4, or MD5 hash function.", - "cve": "python/imports/imports-aliases.py:a7173c43ae66bd07466632d819d450e0071e02dbf782763640d1092981f9631b:B303", - "severity": "Medium", - "confidence": "High", - "scanner": { - "id": "bandit", - "name": "Bandit" - }, - "location": { - "file": "python/imports/imports-aliases.py", - "start_line": 12, - "end_line": 12 - }, - "identifiers": [ - { - "type": "bandit_test_id", - "name": "Bandit Test ID B303", - "value": "B303" - } - ], - "priority": "Medium", - "file": "python/imports/imports-aliases.py", - "line": 12, - "tool": "bandit" - }, - { - "category": "sast", - "message": "Use of insecure MD2, MD4, or MD5 hash function.", - "cve": "python/imports/imports-aliases.py:017017b77deb0b8369b6065947833eeea752a92ec8a700db590fece3e934cf0d:B303", - "severity": "Medium", - "confidence": "High", - "scanner": { - "id": "bandit", - "name": "Bandit" - }, - "location": { - "file": "python/imports/imports-aliases.py", - "start_line": 13, - "end_line": 13 - }, - "identifiers": [ - { - "type": "bandit_test_id", - "name": "Bandit Test ID B303", - "value": "B303" - } - ], - "priority": "Medium", - "file": "python/imports/imports-aliases.py", - "line": 13, - "tool": "bandit" - }, - { - "category": "sast", - "message": "Use of insecure MD2, MD4, or MD5 hash function.", - "cve": "python/imports/imports-aliases.py:45fc8c53aea7b84f06bc4e590cc667678d6073c4c8a1d471177ca2146fb22db2:B303", - "severity": "Medium", - "confidence": "High", - "scanner": { - "id": "bandit", - "name": "Bandit" - }, - "location": { - "file": "python/imports/imports-aliases.py", - "start_line": 14, - "end_line": 14 - }, - "identifiers": [ - { - "type": "bandit_test_id", - "name": "Bandit Test ID B303", - "value": "B303" - } - ], - "priority": "Medium", - "file": "python/imports/imports-aliases.py", - "line": 14, - "tool": "bandit" - }, - { - "category": "sast", - "message": "Pickle library appears to be in use, possible security issue.", - "cve": "python/imports/imports-aliases.py:5f200d47291e7bbd8352db23019b85453ca048dd98ea0c291260fa7d009963a4:B301", - "severity": "Medium", - "confidence": "High", - "scanner": { - "id": "bandit", - "name": "Bandit" - }, - "location": { - "file": "python/imports/imports-aliases.py", - "start_line": 15, - "end_line": 15 - }, - "identifiers": [ - { - "type": "bandit_test_id", - "name": "Bandit Test ID B301", - "value": "B301" - } - ], - "priority": "Medium", - "file": "python/imports/imports-aliases.py", - "line": 15, - "tool": "bandit" - }, - { - "category": "sast", - "name": "ECB mode is insecure", - "message": "ECB mode is insecure", - "cve": "groovy/src/main/java/com/gitlab/security_products/tests/App.groovy:29:ECB_MODE", - "severity": "Medium", - "confidence": "High", - "scanner": { - "id": "find_sec_bugs", - "name": "Find Security Bugs" - }, - "location": { - "file": "groovy/src/main/java/com/gitlab/security_products/tests/App.groovy", - "start_line": 29, - "end_line": 29, - "class": "com.gitlab.security_products.tests.App", - "method": "insecureCypher" - }, - "identifiers": [ - { - "type": "find_sec_bugs_type", - "name": "Find Security Bugs-ECB_MODE", - "value": "ECB_MODE", - "url": "https://find-sec-bugs.github.io/bugs.htm#ECB_MODE" - } - ], - "priority": "Medium", - "file": "groovy/src/main/java/com/gitlab/security_products/tests/App.groovy", - "line": 29, - "url": "https://find-sec-bugs.github.io/bugs.htm#ECB_MODE", - "tool": "find_sec_bugs" - }, - { - "category": "sast", - "name": "Cipher with no integrity", - "message": "Cipher with no integrity", - "cve": "groovy/src/main/java/com/gitlab/security_products/tests/App.groovy:29:CIPHER_INTEGRITY", - "severity": "Medium", - "confidence": "High", - "scanner": { - "id": "find_sec_bugs", - "name": "Find Security Bugs" - }, - "location": { - "file": "groovy/src/main/java/com/gitlab/security_products/tests/App.groovy", - "start_line": 29, - "end_line": 29, - "class": "com.gitlab.security_products.tests.App", - "method": "insecureCypher" - }, - "identifiers": [ - { - "type": "find_sec_bugs_type", - "name": "Find Security Bugs-CIPHER_INTEGRITY", - "value": "CIPHER_INTEGRITY", - "url": "https://find-sec-bugs.github.io/bugs.htm#CIPHER_INTEGRITY" - } - ], - "priority": "Medium", - "file": "groovy/src/main/java/com/gitlab/security_products/tests/App.groovy", - "line": 29, - "url": "https://find-sec-bugs.github.io/bugs.htm#CIPHER_INTEGRITY", - "tool": "find_sec_bugs" - }, - { - "category": "sast", - "message": "Probable insecure usage of temp file/directory.", - "cve": "python/hardcoded/hardcoded-tmp.py:63dd4d626855555b816985d82c4614a790462a0a3ada89dc58eb97f9c50f3077:B108", - "severity": "Medium", - "confidence": "Medium", - "scanner": { - "id": "bandit", - "name": "Bandit" - }, - "location": { - "file": "python/hardcoded/hardcoded-tmp.py", - "start_line": 14, - "end_line": 14 - }, - "identifiers": [ - { - "type": "bandit_test_id", - "name": "Bandit Test ID B108", - "value": "B108", - "url": "https://docs.openstack.org/bandit/latest/plugins/b108_hardcoded_tmp_directory.html" - } - ], - "priority": "Medium", - "file": "python/hardcoded/hardcoded-tmp.py", - "line": 14, - "url": "https://docs.openstack.org/bandit/latest/plugins/b108_hardcoded_tmp_directory.html", - "tool": "bandit" - }, - { - "category": "sast", - "message": "Probable insecure usage of temp file/directory.", - "cve": "python/hardcoded/hardcoded-tmp.py:4ad6d4c40a8c263fc265f3384724014e0a4f8dd6200af83e51ff120420038031:B108", - "severity": "Medium", - "confidence": "Medium", - "scanner": { - "id": "bandit", - "name": "Bandit" - }, - "location": { - "file": "python/hardcoded/hardcoded-tmp.py", - "start_line": 10, - "end_line": 10 - }, - "identifiers": [ - { - "type": "bandit_test_id", - "name": "Bandit Test ID B108", - "value": "B108", - "url": "https://docs.openstack.org/bandit/latest/plugins/b108_hardcoded_tmp_directory.html" - } - ], - "priority": "Medium", - "file": "python/hardcoded/hardcoded-tmp.py", - "line": 10, - "url": "https://docs.openstack.org/bandit/latest/plugins/b108_hardcoded_tmp_directory.html", - "tool": "bandit" - }, - { - "category": "sast", - "message": "Consider possible security implications associated with Popen module.", - "cve": "python/imports/imports-aliases.py:2c3e1fa1e54c3c6646e8bcfaee2518153c6799b77587ff8d9a7b0631f6d34785:B404", - "severity": "Low", - "confidence": "High", - "scanner": { - "id": "bandit", - "name": "Bandit" - }, - "location": { - "file": "python/imports/imports-aliases.py", - "start_line": 1, - "end_line": 1 - }, - "identifiers": [ - { - "type": "bandit_test_id", - "name": "Bandit Test ID B404", - "value": "B404" - } - ], - "priority": "Low", - "file": "python/imports/imports-aliases.py", - "line": 1, - "tool": "bandit" - }, - { - "category": "sast", - "message": "Consider possible security implications associated with pickle module.", - "cve": "python/imports/imports.py:af58d07f6ad519ef5287fcae65bf1a6999448a1a3a8bc1ac2a11daa80d0b96bf:B403", - "severity": "Low", - "confidence": "High", - "scanner": { - "id": "bandit", - "name": "Bandit" - }, - "location": { - "file": "python/imports/imports.py", - "start_line": 2, - "end_line": 2 - }, - "identifiers": [ - { - "type": "bandit_test_id", - "name": "Bandit Test ID B403", - "value": "B403" - } - ], - "priority": "Low", - "file": "python/imports/imports.py", - "line": 2, - "tool": "bandit" - }, - { - "category": "sast", - "message": "Consider possible security implications associated with subprocess module.", - "cve": "python/imports/imports.py:8de9bc98029d212db530785a5f6780cfa663548746ff228ab8fa96c5bb82f089:B404", - "severity": "Low", - "confidence": "High", - "scanner": { - "id": "bandit", - "name": "Bandit" - }, - "location": { - "file": "python/imports/imports.py", - "start_line": 4, - "end_line": 4 - }, - "identifiers": [ - { - "type": "bandit_test_id", - "name": "Bandit Test ID B404", - "value": "B404" - } - ], - "priority": "Low", - "file": "python/imports/imports.py", - "line": 4, - "tool": "bandit" - }, - { - "category": "sast", - "message": "Possible hardcoded password: 'blerg'", - "cve": "python/hardcoded/hardcoded-passwords.py:97c30f1d76d2a88913e3ce9ae74087874d740f87de8af697a9c455f01119f633:B106", - "severity": "Low", - "confidence": "Medium", - "scanner": { - "id": "bandit", - "name": "Bandit" - }, - "location": { - "file": "python/hardcoded/hardcoded-passwords.py", - "start_line": 22, - "end_line": 22 - }, - "identifiers": [ - { - "type": "bandit_test_id", - "name": "Bandit Test ID B106", - "value": "B106", - "url": "https://docs.openstack.org/bandit/latest/plugins/b106_hardcoded_password_funcarg.html" - } - ], - "priority": "Low", - "file": "python/hardcoded/hardcoded-passwords.py", - "line": 22, - "url": "https://docs.openstack.org/bandit/latest/plugins/b106_hardcoded_password_funcarg.html", - "tool": "bandit" - }, - { - "category": "sast", - "message": "Possible hardcoded password: 'root'", - "cve": "python/hardcoded/hardcoded-passwords.py:7431c73a0bc16d94ece2a2e75ef38f302574d42c37ac0c3c38ad0b3bf8a59f10:B105", - "severity": "Low", - "confidence": "Medium", - "scanner": { - "id": "bandit", - "name": "Bandit" - }, - "location": { - "file": "python/hardcoded/hardcoded-passwords.py", - "start_line": 5, - "end_line": 5 - }, - "identifiers": [ - { - "type": "bandit_test_id", - "name": "Bandit Test ID B105", - "value": "B105", - "url": "https://docs.openstack.org/bandit/latest/plugins/b105_hardcoded_password_string.html" - } - ], - "priority": "Low", - "file": "python/hardcoded/hardcoded-passwords.py", - "line": 5, - "url": "https://docs.openstack.org/bandit/latest/plugins/b105_hardcoded_password_string.html", - "tool": "bandit" - }, - { - "category": "sast", - "message": "Possible hardcoded password: ''", - "cve": "python/hardcoded/hardcoded-passwords.py:d2d1857c27caedd49c57bfbcdc23afcc92bd66a22701fcdc632869aab4ca73ee:B105", - "severity": "Low", - "confidence": "Medium", - "scanner": { - "id": "bandit", - "name": "Bandit" - }, - "location": { - "file": "python/hardcoded/hardcoded-passwords.py", - "start_line": 9, - "end_line": 9 - }, - "identifiers": [ - { - "type": "bandit_test_id", - "name": "Bandit Test ID B105", - "value": "B105", - "url": "https://docs.openstack.org/bandit/latest/plugins/b105_hardcoded_password_string.html" - } - ], - "priority": "Low", - "file": "python/hardcoded/hardcoded-passwords.py", - "line": 9, - "url": "https://docs.openstack.org/bandit/latest/plugins/b105_hardcoded_password_string.html", - "tool": "bandit" - }, - { - "category": "sast", - "message": "Possible hardcoded password: 'ajklawejrkl42348swfgkg'", - "cve": "python/hardcoded/hardcoded-passwords.py:fb3866215a61393a5c9c32a3b60e2058171a23219c353f722cbd3567acab21d2:B105", - "severity": "Low", - "confidence": "Medium", - "scanner": { - "id": "bandit", - "name": "Bandit" - }, - "location": { - "file": "python/hardcoded/hardcoded-passwords.py", - "start_line": 13, - "end_line": 13 - }, - "identifiers": [ - { - "type": "bandit_test_id", - "name": "Bandit Test ID B105", - "value": "B105", - "url": "https://docs.openstack.org/bandit/latest/plugins/b105_hardcoded_password_string.html" - } - ], - "priority": "Low", - "file": "python/hardcoded/hardcoded-passwords.py", - "line": 13, - "url": "https://docs.openstack.org/bandit/latest/plugins/b105_hardcoded_password_string.html", - "tool": "bandit" - }, - { - "category": "sast", - "message": "Possible hardcoded password: 'blerg'", - "cve": "python/hardcoded/hardcoded-passwords.py:63c62a8b7e1e5224439bd26b28030585ac48741e28ca64561a6071080c560a5f:B105", - "severity": "Low", - "confidence": "Medium", - "scanner": { - "id": "bandit", - "name": "Bandit" - }, - "location": { - "file": "python/hardcoded/hardcoded-passwords.py", - "start_line": 23, - "end_line": 23 - }, - "identifiers": [ - { - "type": "bandit_test_id", - "name": "Bandit Test ID B105", - "value": "B105", - "url": "https://docs.openstack.org/bandit/latest/plugins/b105_hardcoded_password_string.html" - } - ], - "priority": "Low", - "file": "python/hardcoded/hardcoded-passwords.py", - "line": 23, - "url": "https://docs.openstack.org/bandit/latest/plugins/b105_hardcoded_password_string.html", - "tool": "bandit" - }, - { - "category": "sast", - "message": "Possible hardcoded password: 'blerg'", - "cve": "python/hardcoded/hardcoded-passwords.py:4311b06d08df8fa58229b341c531da8e1a31ec4520597bdff920cd5c098d86f9:B105", - "severity": "Low", - "confidence": "Medium", - "scanner": { - "id": "bandit", - "name": "Bandit" - }, - "location": { - "file": "python/hardcoded/hardcoded-passwords.py", - "start_line": 24, - "end_line": 24 - }, - "identifiers": [ - { - "type": "bandit_test_id", - "name": "Bandit Test ID B105", - "value": "B105", - "url": "https://docs.openstack.org/bandit/latest/plugins/b105_hardcoded_password_string.html" - } - ], - "priority": "Low", - "file": "python/hardcoded/hardcoded-passwords.py", - "line": 24, - "url": "https://docs.openstack.org/bandit/latest/plugins/b105_hardcoded_password_string.html", - "tool": "bandit" - }, - { - "category": "sast", - "message": "Consider possible security implications associated with subprocess module.", - "cve": "python/imports/imports-function.py:5858400c2f39047787702de44d03361ef8d954c9d14bd54ee1c2bef9e6a7df93:B404", - "severity": "Low", - "confidence": "High", - "scanner": { - "id": "bandit", - "name": "Bandit" - }, - "location": { - "file": "python/imports/imports-function.py", - "start_line": 4, - "end_line": 4 - }, - "identifiers": [ - { - "type": "bandit_test_id", - "name": "Bandit Test ID B404", - "value": "B404" - } - ], - "priority": "Low", - "file": "python/imports/imports-function.py", - "line": 4, - "tool": "bandit" - }, - { - "category": "sast", - "message": "Consider possible security implications associated with pickle module.", - "cve": "python/imports/imports-function.py:dbda3cf4190279d30e0aad7dd137eca11272b0b225e8af4e8bf39682da67d956:B403", - "severity": "Low", - "confidence": "High", - "scanner": { - "id": "bandit", - "name": "Bandit" - }, - "location": { - "file": "python/imports/imports-function.py", - "start_line": 2, - "end_line": 2 - }, - "identifiers": [ - { - "type": "bandit_test_id", - "name": "Bandit Test ID B403", - "value": "B403" - } - ], - "priority": "Low", - "file": "python/imports/imports-function.py", - "line": 2, - "tool": "bandit" - }, - { - "category": "sast", - "message": "Consider possible security implications associated with Popen module.", - "cve": "python/imports/imports-from.py:eb8a0db9cd1a8c1ab39a77e6025021b1261cc2a0b026b2f4a11fca4e0636d8dd:B404", - "severity": "Low", - "confidence": "High", - "scanner": { - "id": "bandit", - "name": "Bandit" - }, - "location": { - "file": "python/imports/imports-from.py", - "start_line": 7, - "end_line": 7 - }, - "identifiers": [ - { - "type": "bandit_test_id", - "name": "Bandit Test ID B404", - "value": "B404" - } - ], - "priority": "Low", - "file": "python/imports/imports-from.py", - "line": 7, - "tool": "bandit" - }, - { - "category": "sast", - "message": "subprocess call with shell=True seems safe, but may be changed in the future, consider rewriting without shell", - "cve": "python/imports/imports-aliases.py:f99f9721e27537fbcb6699a4cf39c6740d6234d2c6f06cfc2d9ea977313c483d:B602", - "severity": "Low", - "confidence": "High", - "scanner": { - "id": "bandit", - "name": "Bandit" - }, - "location": { - "file": "python/imports/imports-aliases.py", - "start_line": 9, - "end_line": 9 - }, - "identifiers": [ - { - "type": "bandit_test_id", - "name": "Bandit Test ID B602", - "value": "B602", - "url": "https://docs.openstack.org/bandit/latest/plugins/b602_subprocess_popen_with_shell_equals_true.html" - } - ], - "priority": "Low", - "file": "python/imports/imports-aliases.py", - "line": 9, - "url": "https://docs.openstack.org/bandit/latest/plugins/b602_subprocess_popen_with_shell_equals_true.html", - "tool": "bandit" - }, - { - "category": "sast", - "message": "Consider possible security implications associated with subprocess module.", - "cve": "python/imports/imports-from.py:332a12ab1146698f614a905ce6a6a5401497a12281aef200e80522711c69dcf4:B404", - "severity": "Low", - "confidence": "High", - "scanner": { - "id": "bandit", - "name": "Bandit" - }, - "location": { - "file": "python/imports/imports-from.py", - "start_line": 6, - "end_line": 6 - }, - "identifiers": [ - { - "type": "bandit_test_id", - "name": "Bandit Test ID B404", - "value": "B404" - } - ], - "priority": "Low", - "file": "python/imports/imports-from.py", - "line": 6, - "tool": "bandit" - }, - { - "category": "sast", - "message": "Consider possible security implications associated with Popen module.", - "cve": "python/imports/imports-from.py:0a48de4a3d5348853a03666cb574697e3982998355e7a095a798bd02a5947276:B404", - "severity": "Low", - "confidence": "High", - "scanner": { - "id": "bandit", - "name": "Bandit" - }, - "location": { - "file": "python/imports/imports-from.py", - "start_line": 1, - "end_line": 2 - }, - "identifiers": [ - { - "type": "bandit_test_id", - "name": "Bandit Test ID B404", - "value": "B404" - } - ], - "priority": "Low", - "file": "python/imports/imports-from.py", - "line": 1, - "tool": "bandit" - }, - { - "category": "sast", - "message": "Consider possible security implications associated with pickle module.", - "cve": "python/imports/imports-aliases.py:51b71661dff994bde3529639a727a678c8f5c4c96f00d300913f6d5be1bbdf26:B403", - "severity": "Low", - "confidence": "High", - "scanner": { - "id": "bandit", - "name": "Bandit" - }, - "location": { - "file": "python/imports/imports-aliases.py", - "start_line": 7, - "end_line": 8 - }, - "identifiers": [ - { - "type": "bandit_test_id", - "name": "Bandit Test ID B403", - "value": "B403" - } - ], - "priority": "Low", - "file": "python/imports/imports-aliases.py", - "line": 7, - "tool": "bandit" - }, - { - "category": "sast", - "message": "Consider possible security implications associated with loads module.", - "cve": "python/imports/imports-aliases.py:6ff02aeb3149c01ab68484d794a94f58d5d3e3bb0d58557ef4153644ea68ea54:B403", - "severity": "Low", - "confidence": "High", - "scanner": { - "id": "bandit", - "name": "Bandit" - }, - "location": { - "file": "python/imports/imports-aliases.py", - "start_line": 6, - "end_line": 6 - }, - "identifiers": [ - { - "type": "bandit_test_id", - "name": "Bandit Test ID B403", - "value": "B403" - } - ], - "priority": "Low", - "file": "python/imports/imports-aliases.py", - "line": 6, - "tool": "bandit" - }, - { - "category": "sast", - "message": "Statically-sized arrays can be improperly restricted, leading to potential overflows or other issues (CWE-119!/CWE-120)", - "cve": "c/subdir/utils.c:b466873101951fe96e1332f6728eb7010acbbd5dfc3b65d7d53571d091a06d9e:CWE-119!/CWE-120", - "confidence": "Low", - "solution": "Perform bounds checking, use functions that limit length, or ensure that the size is larger than the maximum possible length", - "scanner": { - "id": "flawfinder", - "name": "Flawfinder" - }, - "location": { - "file": "c/subdir/utils.c", - "start_line": 4 - }, - "identifiers": [ - { - "type": "flawfinder_func_name", - "name": "Flawfinder - char", - "value": "char" - }, - { - "type": "cwe", - "name": "CWE-119", - "value": "119", - "url": "https://cwe.mitre.org/data/definitions/119.html" - }, - { - "type": "cwe", - "name": "CWE-120", - "value": "120", - "url": "https://cwe.mitre.org/data/definitions/120.html" - } - ], - "file": "c/subdir/utils.c", - "line": 4, - "url": "https://cwe.mitre.org/data/definitions/119.html", - "tool": "flawfinder" - }, - { - "category": "sast", - "message": "Check when opening files - can an attacker redirect it (via symlinks), force the opening of special file type (e.g., device files), move things around to create a race condition, control its ancestors, or change its contents? (CWE-362)", - "cve": "c/subdir/utils.c:bab681140fcc8fc3085b6bba74081b44ea145c1c98b5e70cf19ace2417d30770:CWE-362", - "confidence": "Low", - "scanner": { - "id": "flawfinder", - "name": "Flawfinder" - }, - "location": { - "file": "c/subdir/utils.c", - "start_line": 8 - }, - "identifiers": [ - { - "type": "flawfinder_func_name", - "name": "Flawfinder - fopen", - "value": "fopen" - }, - { - "type": "cwe", - "name": "CWE-362", - "value": "362", - "url": "https://cwe.mitre.org/data/definitions/362.html" - } - ], - "file": "c/subdir/utils.c", - "line": 8, - "url": "https://cwe.mitre.org/data/definitions/362.html", - "tool": "flawfinder" - }, - { - "category": "sast", - "message": "Statically-sized arrays can be improperly restricted, leading to potential overflows or other issues (CWE-119!/CWE-120)", - "cve": "cplusplus/src/hello.cpp:c8c6dd0afdae6814194cf0930b719f757ab7b379cf8f261e7f4f9f2f323a818a:CWE-119!/CWE-120", - "confidence": "Low", - "solution": "Perform bounds checking, use functions that limit length, or ensure that the size is larger than the maximum possible length", - "scanner": { - "id": "flawfinder", - "name": "Flawfinder" - }, - "location": { - "file": "cplusplus/src/hello.cpp", - "start_line": 6 - }, - "identifiers": [ - { - "type": "flawfinder_func_name", - "name": "Flawfinder - char", - "value": "char" - }, - { - "type": "cwe", - "name": "CWE-119", - "value": "119", - "url": "https://cwe.mitre.org/data/definitions/119.html" - }, - { - "type": "cwe", - "name": "CWE-120", - "value": "120", - "url": "https://cwe.mitre.org/data/definitions/120.html" - } - ], - "file": "cplusplus/src/hello.cpp", - "line": 6, - "url": "https://cwe.mitre.org/data/definitions/119.html", - "tool": "flawfinder" - }, - { - "category": "sast", - "message": "Does not check for buffer overflows when copying to destination [MS-banned] (CWE-120)", - "cve": "cplusplus/src/hello.cpp:331c04062c4fe0c7c486f66f59e82ad146ab33cdd76ae757ca41f392d568cbd0:CWE-120", - "confidence": "Low", - "solution": "Consider using snprintf, strcpy_s, or strlcpy (warning: strncpy easily misused)", - "scanner": { - "id": "flawfinder", - "name": "Flawfinder" - }, - "location": { - "file": "cplusplus/src/hello.cpp", - "start_line": 7 - }, - "identifiers": [ - { - "type": "flawfinder_func_name", - "name": "Flawfinder - strcpy", - "value": "strcpy" - }, - { - "type": "cwe", - "name": "CWE-120", - "value": "120", - "url": "https://cwe.mitre.org/data/definitions/120.html" - } - ], - "file": "cplusplus/src/hello.cpp", - "line": 7, - "url": "https://cwe.mitre.org/data/definitions/120.html", - "tool": "flawfinder" - } - ] -} diff --git a/spec/fixtures/security-reports/remediations/gl-dependency-scanning-report.json b/spec/fixtures/security-reports/remediations/gl-dependency-scanning-report.json deleted file mode 100644 index c96e831b027..00000000000 --- a/spec/fixtures/security-reports/remediations/gl-dependency-scanning-report.json +++ /dev/null @@ -1,104 +0,0 @@ -{ - "version": "2.0", - "vulnerabilities": [ - { - "category": "dependency_scanning", - "name": "Regular Expression Denial of Service", - "message": "Regular Expression Denial of Service in debug", - "description": "The debug module is vulnerable to regular expression denial of service when untrusted user input is passed into the `o` formatter. It takes around 50k characters to block for 2 seconds making this a low severity issue.", - "cve": "yarn.lock:debug:gemnasium:37283ed4-0380-40d7-ada7-2d994afcc62a", - "severity": "Unknown", - "solution": "Upgrade to latest versions.", - "scanner": { - "id": "gemnasium", - "name": "Gemnasium" - }, - "location": { - "file": "yarn.lock", - "dependency": { - "package": { - "name": "debug" - }, - "version": "1.0.5" - } - }, - "identifiers": [ - { - "type": "gemnasium", - "name": "Gemnasium-37283ed4-0380-40d7-ada7-2d994afcc62a", - "value": "37283ed4-0380-40d7-ada7-2d994afcc62a", - "url": "https://deps.sec.gitlab.com/packages/npm/debug/versions/1.0.5/advisories" - } - ], - "links": [ - { - "url": "https://nodesecurity.io/advisories/534" - }, - { - "url": "https://github.com/visionmedia/debug/issues/501" - }, - { - "url": "https://github.com/visionmedia/debug/pull/504" - } - ] - }, - { - "category": "dependency_scanning", - "name": "Authentication bypass via incorrect DOM traversal and canonicalization", - "message": "Authentication bypass via incorrect DOM traversal and canonicalization in saml2-js", - "description": "Some XML DOM traversal and canonicalization APIs may be inconsistent in handling of comments within XML nodes. Incorrect use of these APIs by some SAML libraries results in incorrect parsing of the inner text of XML nodes such that any inner text after the comment is lost prior to cryptographically signing the SAML message. Text after the comment therefore has no impact on the signature on the SAML message.\r\n\r\nA remote attacker can modify SAML content for a SAML service provider without invalidating the cryptographic signature, which may allow attackers to bypass primary authentication for the affected SAML service provider.", - "cve": "yarn.lock:saml2-js:gemnasium:9952e574-7b5b-46fa-a270-aeb694198a98", - "severity": "Unknown", - "solution": "Upgrade to fixed version.\r\n", - "scanner": { - "id": "gemnasium", - "name": "Gemnasium" - }, - "location": { - "file": "yarn.lock", - "dependency": { - "package": { - "name": "saml2-js" - }, - "version": "1.5.0" - } - }, - "identifiers": [ - { - "type": "gemnasium", - "name": "Gemnasium-9952e574-7b5b-46fa-a270-aeb694198a98", - "value": "9952e574-7b5b-46fa-a270-aeb694198a98", - "url": "https://deps.sec.gitlab.com/packages/npm/saml2-js/versions/1.5.0/advisories" - }, - { - "type": "cve", - "name": "CVE-2017-11429", - "value": "CVE-2017-11429", - "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-11429" - } - ], - "links": [ - { - "url": "https://github.com/Clever/saml2/commit/3546cb61fd541f219abda364c5b919633609ef3d#diff-af730f9f738de1c9ad87596df3f6de84R279" - }, - { - "url": "https://github.com/Clever/saml2/issues/127" - }, - { - "url": "https://www.kb.cert.org/vuls/id/475445" - } - ] - } - ], - "remediations": [ - { - "fixes": [ - { - "cve": "yarn.lock:saml2-js:gemnasium:9952e574-7b5b-46fa-a270-aeb694198a98" - } - ], - "summary": "Upgrade saml2-js", - "diff": "ZGlmZiAtLWdpdCBhL3lhcm4ubG9jayBiL3lhcm4ubG9jawppbmRleCAwZWNjOTJmLi43ZmE0NTU0IDEwMDY0NAotLS0gYS95YXJuLmxvY2sKKysrIGIveWFybi5sb2NrCkBAIC0yLDEwMyArMiwxMjQgQEAKICMgeWFybiBsb2NrZmlsZSB2MQogCiAKLWFzeW5jQH4wLjIuNzoKLSAgdmVyc2lvbiAiMC4yLjEwIgotICByZXNvbHZlZCAiaHR0cDovL3JlZ2lzdHJ5Lm5wbWpzLm9yZy9hc3luYy8tL2FzeW5jLTAuMi4xMC50Z3ojYjZiYmUwYjA2NzRiOWQ3MTk3MDhjYTM4ZGU4YzIzN2NiNTI2YzNkMSIKLQotYXN5bmNAfjEuNS4yOgotICB2ZXJzaW9uICIxLjUuMiIKLSAgcmVzb2x2ZWQgImh0dHA6Ly9yZWdpc3RyeS5ucG1qcy5vcmcvYXN5bmMvLS9hc3luYy0xLjUuMi50Z3ojZWM2YTYxYWU1NjQ4MGMwYzNjYjI0MWM5NTYxOGUyMDg5MmY5NjcyYSIKK2FzeW5jQF4yLjEuNSwgYXN5bmNAXjIuNS4wOgorICB2ZXJzaW9uICIyLjYuMSIKKyAgcmVzb2x2ZWQgImh0dHBzOi8vcmVnaXN0cnkueWFybnBrZy5jb20vYXN5bmMvLS9hc3luYy0yLjYuMS50Z3ojYjI0NWEyM2NhNzE5MzAwNDRlYzUzZmE0NmFhMDBhM2U4N2M2YTYxMCIKKyAgaW50ZWdyaXR5IHNoYTUxMi1mTkVpTDIrQVp0NkFsQXcvMjlDcjBVRGU0c1JBSENwRUhoNTRXTXorQmI3UWZOY0Z3NGgzbG9vZnlKcExlUXM0WXg3eXVxdS8yZExnTTVoS09zNkhsUT09CisgIGRlcGVuZGVuY2llczoKKyAgICBsb2Rhc2ggIl40LjE3LjEwIgogCi1kZWJ1Z0BeMS4wLjQ6Ci0gIHZlcnNpb24gIjEuMC41IgotICByZXNvbHZlZCAiaHR0cHM6Ly9yZWdpc3RyeS55YXJucGtnLmNvbS9kZWJ1Zy8tL2RlYnVnLTEuMC41LnRneiNmNzI0MTIxNzQzMGY5OWRlYzRjMmI0NzNlYWI5MjIyOGU4NzRjMmFjIgorZGVidWdAXjIuNi4wOgorICB2ZXJzaW9uICIyLjYuOSIKKyAgcmVzb2x2ZWQgImh0dHBzOi8vcmVnaXN0cnkueWFybnBrZy5jb20vZGVidWcvLS9kZWJ1Zy0yLjYuOS50Z3ojNWQxMjg1MTVkZjEzNGZmMzI3ZTkwYTRjOTNmNGUwNzdhNTM2MzQxZiIKKyAgaW50ZWdyaXR5IHNoYTUxMi1iQzdFbHJkSmFKblBiQVArMUVvdFl2cVpzYjNlY2w1d2k2QmZpNkJKVFVjTm93cDZjdnNwZzBqWHpuUlRLRGptL0U3QWRnRkJWZUFQVk1OY0tHc0hNQT09CiAgIGRlcGVuZGVuY2llczoKICAgICBtcyAiMi4wLjAiCiAKLWVqc0B+MC44LjM6Ci0gIHZlcnNpb24gIjAuOC44IgotICByZXNvbHZlZCAiaHR0cHM6Ly9yZWdpc3RyeS55YXJucGtnLmNvbS9lanMvLS9lanMtMC44LjgudGd6I2ZmZGM1NmRjYzM1ZDAyOTI2ZGQ1MGFkMTM0MzliYmM1NDA2MWQ1OTgiCitlanNAXjIuNS42OgorICB2ZXJzaW9uICIyLjYuMSIKKyAgcmVzb2x2ZWQgImh0dHBzOi8vcmVnaXN0cnkueWFybnBrZy5jb20vZWpzLy0vZWpzLTIuNi4xLnRneiM0OThlYzBkNDk1NjU1YWJjNmYyM2NkNjE4NjhkOTI2NDY0MDcxYWEwIgorICBpbnRlZ3JpdHkgc2hhNTEyLTB4eTRBL3R3ZnJSQ25raGZrOEVyRGk1RHFkQXNBcWVHeGh0NHhrQ1Vyc3ZoaGJRTnM3RSs0alYwQ043K05LSVkwYUhFNzIrWHZxdEJJWHpEMzFaYlhRPT0KKworbG9kYXNoLW5vZGVAfjIuNC4xOgorICB2ZXJzaW9uICIyLjQuMSIKKyAgcmVzb2x2ZWQgImh0dHBzOi8vcmVnaXN0cnkueWFybnBrZy5jb20vbG9kYXNoLW5vZGUvLS9sb2Rhc2gtbm9kZS0yLjQuMS50Z3ojZWE4MmY3YjEwMGM3MzNkMWE0MmFmNzY4MDFlNTA2MTA1ZTJhODBlYyIKKyAgaW50ZWdyaXR5IHNoYTEtNm9MM3NRREhNOUdrS3Zkb0FlVUdFRjRxZ093PQorCitsb2Rhc2hAXjQuMTcuMTA6CisgIHZlcnNpb24gIjQuMTcuMTEiCisgIHJlc29sdmVkICJodHRwczovL3JlZ2lzdHJ5Lnlhcm5wa2cuY29tL2xvZGFzaC8tL2xvZGFzaC00LjE3LjExLnRneiNiMzllYTYyMjllZjYwN2VjZDg5ZTJjOGRmMTI1MzY4OTFjYWM5YjhkIgorICBpbnRlZ3JpdHkgc2hhNTEyLWNRS2g4aWdvNVFVaFo3bGczOERZV0F4TXZqU0FLRzBBOHdHU1ZpbVAwN1NJVUVLMlVPK2FyU1JLYlJaV3RlbE10TjVWMEhrd2g1cnlPdG8vU3NoWUlnPT0KIAogbXNAMi4wLjA6CiAgIHZlcnNpb24gIjIuMC4wIgogICByZXNvbHZlZCAiaHR0cHM6Ly9yZWdpc3RyeS55YXJucGtnLmNvbS9tcy8tL21zLTIuMC4wLnRneiM1NjA4YWVhZGZjMDBiZTZjMjkwMWRmNWY5ODYxNzg4ZGUwZDU5N2M4IgorICBpbnRlZ3JpdHkgc2hhMS1WZ2l1cmZ3QXZtd3BBZDlmbUdGNGplRFZsOGc9CiAKLW5vZGUtZm9yZ2VAMC4yLjI0OgotICB2ZXJzaW9uICIwLjIuMjQiCi0gIHJlc29sdmVkICJodHRwczovL3JlZ2lzdHJ5Lnlhcm5wa2cuY29tL25vZGUtZm9yZ2UvLS9ub2RlLWZvcmdlLTAuMi4yNC50Z3ojZmE2Zjg0NmY0MmZhOTNmNjNhMGEzMGM5ZmJmZjdiNGUxMzBlMDg1OCIKK25vZGUtZm9yZ2VAXjAuNy4wOgorICB2ZXJzaW9uICIwLjcuNiIKKyAgcmVzb2x2ZWQgImh0dHBzOi8vcmVnaXN0cnkueWFybnBrZy5jb20vbm9kZS1mb3JnZS8tL25vZGUtZm9yZ2UtMC43LjYudGd6I2ZkZjNiNDE4YWVlMWY5NGYwZWY2NDJjZDYzNDg2Yzc3Y2E5NzI0YWMiCisgIGludGVncml0eSBzaGE1MTItc29sMzBMVXB6MWpRRkJqT0t3Ymp4aWppRTNiNnBqZDc0WXdmRDBmSk9LUGpGK2ZPTktiMllnOHJZZ1M2K2JLNlZEbCsvd2ZyNElZcEM3akR6TFVJZnc9PQogCiBzYW1sMi1qc0BeMS41LjA6Ci0gIHZlcnNpb24gIjEuNS4wIgotICByZXNvbHZlZCAiaHR0cHM6Ly9yZWdpc3RyeS55YXJucGtnLmNvbS9zYW1sMi1qcy8tL3NhbWwyLWpzLTEuNS4wLnRneiNjMGQyMjY4YTE3OWU3MzI5ZDI5ZWIyNWFhODJkZjU1MDM3NzRiMGQ5IgorICB2ZXJzaW9uICIxLjEyLjQiCisgIHJlc29sdmVkICJodHRwczovL3JlZ2lzdHJ5Lnlhcm5wa2cuY29tL3NhbWwyLWpzLy0vc2FtbDItanMtMS4xMi40LnRneiNjMjg4ZjIwYmRhNmQyYjkxMDczYjE2Yzk0ZWE3MmYyMjM0OWFjM2IzIgorICBpbnRlZ3JpdHkgc2hhMS13b2p5QzlwdEs1RUhPeGJKVHFjdklqU2F3N009CiAgIGRlcGVuZGVuY2llczoKLSAgICBhc3luYyAifjEuNS4yIgotICAgIGRlYnVnICJeMS4wLjQiCi0gICAgdW5kZXJzY29yZSAifjEuNi4wIgotICAgIHhtbC1jcnlwdG8gIl4wLjguMSIKLSAgICB4bWwtZW5jcnlwdGlvbiAifjAuNy40IgotICAgIHhtbDJqcyAifjAuNC4xIgotICAgIHhtbGJ1aWxkZXIgIn4yLjEuMCIKLSAgICB4bWxkb20gIn4wLjEuMTkiCisgICAgYXN5bmMgIl4yLjUuMCIKKyAgICBkZWJ1ZyAiXjIuNi4wIgorICAgIHVuZGVyc2NvcmUgIl4xLjguMCIKKyAgICB4bWwtY3J5cHRvICJeMC4xMC4wIgorICAgIHhtbC1lbmNyeXB0aW9uICJeMC4xMS4wIgorICAgIHhtbDJqcyAiXjAuNC4wIgorICAgIHhtbGJ1aWxkZXIgIn4yLjIuMCIKKyAgICB4bWxkb20gIl4wLjEuMCIKIAogc2F4QD49MC42LjA6CiAgIHZlcnNpb24gIjEuMi40IgogICByZXNvbHZlZCAiaHR0cHM6Ly9yZWdpc3RyeS55YXJucGtnLmNvbS9zYXgvLS9zYXgtMS4yLjQudGd6IzI4MTYyMzRlMjM3OGJkZGM0ZTUzNTRmYWI1Y2FhODk1ZGY3MTAwZDkiCisgIGludGVncml0eSBzaGE1MTItTnFWRHY5VHBBTlVqRm0wTjh1TTVHeEwzNlVnS2k5L2F0WncreDdZRm5ROGNrd0ZHS3JsNHhYNHlXdHJleTNVSm01blAxa1VibllnTG9wcVdOU1JoV3c9PQogCi11bmRlcnNjb3JlQD49MS41Lng6Cit1bmRlcnNjb3JlQF4xLjguMDoKICAgdmVyc2lvbiAiMS45LjEiCiAgIHJlc29sdmVkICJodHRwczovL3JlZ2lzdHJ5Lnlhcm5wa2cuY29tL3VuZGVyc2NvcmUvLS91bmRlcnNjb3JlLTEuOS4xLnRneiMwNmRjZTM0YTBlNjhhN2JhYmMyOWIzNjViOGU3NGI4OTI1MjAzOTYxIgorICBpbnRlZ3JpdHkgc2hhNTEyLTUvNGV0bkNrZDljOGd3Z293aTUvb20vbVlPNWFqQ2FPZ2R6ai9vVyswZVFWOVd4S0JEWnc1K3ljbUttZWFUWGpJblMvVzBCenBHTG8yeFIyYUJ3WmRnPT0KIAotdW5kZXJzY29yZUB+MS42LjA6Ci0gIHZlcnNpb24gIjEuNi4wIgotICByZXNvbHZlZCAiaHR0cHM6Ly9yZWdpc3RyeS55YXJucGtnLmNvbS91bmRlcnNjb3JlLy0vdW5kZXJzY29yZS0xLjYuMC50Z3ojOGIzOGIxMGNhY2RlZjYzMzM3YjhiMjRlNGZmODZkNDVhZWE1MjlhOCIKLQoteG1sLWNyeXB0b0BeMC44LjE6Ci0gIHZlcnNpb24gIjAuOC41IgotICByZXNvbHZlZCAiaHR0cDovL3JlZ2lzdHJ5Lm5wbWpzLm9yZy94bWwtY3J5cHRvLy0veG1sLWNyeXB0by0wLjguNS50Z3ojMmJiY2ZiM2ViMzNmM2E4MmEyMThiODIyYmY2NzJiNmIxYzIwZTUzOCIKK3htbC1jcnlwdG9AXjAuMTAuMDoKKyAgdmVyc2lvbiAiMC4xMC4xIgorICByZXNvbHZlZCAiaHR0cHM6Ly9yZWdpc3RyeS55YXJucGtnLmNvbS94bWwtY3J5cHRvLy0veG1sLWNyeXB0by0wLjEwLjEudGd6I2Y4MzJmNzRjY2Y1NmYyNGFmY2FlMTE2M2ExZmNhYjQ0ZDk2Nzc0YTgiCisgIGludGVncml0eSBzaGExLStETDNUTTlXOGtyOHJoRmpvZnlyUk5sbmRLZz0KICAgZGVwZW5kZW5jaWVzOgogICAgIHhtbGRvbSAiPTAuMS4xOSIKICAgICB4cGF0aC5qcyAiPj0wLjAuMyIKIAoteG1sLWVuY3J5cHRpb25AfjAuNy40OgotICB2ZXJzaW9uICIwLjcuNCIKLSAgcmVzb2x2ZWQgImh0dHBzOi8vcmVnaXN0cnkueWFybnBrZy5jb20veG1sLWVuY3J5cHRpb24vLS94bWwtZW5jcnlwdGlvbi0wLjcuNC50Z3ojNDI3OTFlYzY0ZDU1NmQyNDU1ZGNiOWRhMGE1NDEyMzY2NWFjNjVjNyIKK3htbC1lbmNyeXB0aW9uQF4wLjExLjA6CisgIHZlcnNpb24gIjAuMTEuMiIKKyAgcmVzb2x2ZWQgImh0dHBzOi8vcmVnaXN0cnkueWFybnBrZy5jb20veG1sLWVuY3J5cHRpb24vLS94bWwtZW5jcnlwdGlvbi0wLjExLjIudGd6I2MyMTdmNTUwOTU0N2UzNGI1MDBiODI5ZjJjMGJjYTg1Y2NhNzNhMjEiCisgIGludGVncml0eSBzaGE1MTItalZ2RVM3aTVvdmRPN04rTmpnbmNBMzI2eFlLamhxZUFubnZJZ1JuWTdST0xDZkZxRURMd1AwU3hwLzMwU0hHMEFYUVYxMDQ4VDV5aW5PRnl2d0dGemc9PQogICBkZXBlbmRlbmNpZXM6Ci0gICAgYXN5bmMgIn4wLjIuNyIKLSAgICBlanMgIn4wLjguMyIKLSAgICBub2RlLWZvcmdlICIwLjIuMjQiCisgICAgYXN5bmMgIl4yLjEuNSIKKyAgICBlanMgIl4yLjUuNiIKKyAgICBub2RlLWZvcmdlICJeMC43LjAiCiAgICAgeG1sZG9tICJ+MC4xLjE1IgotICAgIHhwYXRoICIwLjAuNSIKKyAgICB4cGF0aCAiMC4wLjI3IgogCi14bWwyanNAfjAuNC4xOgoreG1sMmpzQF4wLjQuMDoKICAgdmVyc2lvbiAiMC40LjE5IgogICByZXNvbHZlZCAiaHR0cHM6Ly9yZWdpc3RyeS55YXJucGtnLmNvbS94bWwyanMvLS94bWwyanMtMC40LjE5LnRneiM2ODZjMjBmMjEzMjA5ZTk0YWJmMGQxYmNmMWVmYWEyOTFjNzgyN2E3IgorICBpbnRlZ3JpdHkgc2hhNTEyLWVzWm5KWkpPaUpSOXdXS015dXZTRTF5NkRxNUxDdUphbnFoeHNsSDJieE02ZHVhaE5aK0hNcENMaEJRR1prYlg2eFJmOHgxWTJlSmxndDJxM3FvNDlRPT0KICAgZGVwZW5kZW5jaWVzOgogICAgIHNheCAiPj0wLjYuMCIKICAgICB4bWxidWlsZGVyICJ+OS4wLjEiCiAKLXhtbGJ1aWxkZXJAfjIuMS4wOgotICB2ZXJzaW9uICIyLjEuMCIKLSAgcmVzb2x2ZWQgImh0dHA6Ly9yZWdpc3RyeS5ucG1qcy5vcmcveG1sYnVpbGRlci8tL3htbGJ1aWxkZXItMi4xLjAudGd6IzZkZGFlMzE2ODNiNmRmMTIxMDBiMjlmYzhhMGQ0ZjQ2MzQ5YWJiZWQiCit4bWxidWlsZGVyQH4yLjIuMDoKKyAgdmVyc2lvbiAiMi4yLjEiCisgIHJlc29sdmVkICJodHRwczovL3JlZ2lzdHJ5Lnlhcm5wa2cuY29tL3htbGJ1aWxkZXIvLS94bWxidWlsZGVyLTIuMi4xLnRneiM5MzI2NDMwZjEzMGQ4NzQzNWQ0YzQwODY2NDNhYTI5MjZlMTA1YTMyIgorICBpbnRlZ3JpdHkgc2hhMS1reVpERHhNTmgwTmRURUNHWkRxaWttNFFXakk9CiAgIGRlcGVuZGVuY2llczoKLSAgICB1bmRlcnNjb3JlICI+PTEuNS54IgorICAgIGxvZGFzaC1ub2RlICJ+Mi40LjEiCiAKIHhtbGJ1aWxkZXJAfjkuMC4xOgogICB2ZXJzaW9uICI5LjAuNyIKLSAgcmVzb2x2ZWQgImh0dHA6Ly9yZWdpc3RyeS5ucG1qcy5vcmcveG1sYnVpbGRlci8tL3htbGJ1aWxkZXItOS4wLjcudGd6IzEzMmVlNjNkMmVjNTU2NWM1NTdlMjBmNGMyMmRmOWFjYTY4NmIxMGQiCisgIHJlc29sdmVkICJodHRwczovL3JlZ2lzdHJ5Lnlhcm5wa2cuY29tL3htbGJ1aWxkZXIvLS94bWxidWlsZGVyLTkuMC43LnRneiMxMzJlZTYzZDJlYzU1NjVjNTU3ZTIwZjRjMjJkZjlhY2E2ODZiMTBkIgorICBpbnRlZ3JpdHkgc2hhMS1FeTdtUFM3RlZseFZmaUQwd2kzNXJLYUdzUTA9CiAKIHhtbGRvbUA9MC4xLjE5OgogICB2ZXJzaW9uICIwLjEuMTkiCiAgIHJlc29sdmVkICJodHRwczovL3JlZ2lzdHJ5Lnlhcm5wa2cuY29tL3htbGRvbS8tL3htbGRvbS0wLjEuMTkudGd6IzYzMWZjMDc3NzZlZmQ4NDExOGJmMjUxNzFiMzdlZDRkMDc1YTBhYmMiCisgIGludGVncml0eSBzaGExLVl4L0FkM2J2MkVFWXZ5VVhHemZ0VFFkYUNydz0KIAoteG1sZG9tQH4wLjEuMTUsIHhtbGRvbUB+MC4xLjE5OgoreG1sZG9tQF4wLjEuMCwgeG1sZG9tQH4wLjEuMTU6CiAgIHZlcnNpb24gIjAuMS4yNyIKICAgcmVzb2x2ZWQgImh0dHBzOi8vcmVnaXN0cnkueWFybnBrZy5jb20veG1sZG9tLy0veG1sZG9tLTAuMS4yNy50Z3ojZDUwMWY5N2IzYmRiNDAzYWY4ZWY5ZWNjMjA1NzMxODdhYWRhYzBlOSIKKyAgaW50ZWdyaXR5IHNoYTEtMVFINWV6dmJRRHI0NzU3TUlGY3hoNnJhd09rPQogCiB4cGF0aC5qc0A+PTAuMC4zOgogICB2ZXJzaW9uICIxLjEuMCIKICAgcmVzb2x2ZWQgImh0dHBzOi8vcmVnaXN0cnkueWFybnBrZy5jb20veHBhdGguanMvLS94cGF0aC5qcy0xLjEuMC50Z3ojMzgxNmE0NGVkNGJiMzUyMDkxMDgzZDAwMmEzODNkZDUxMDRhNWZmMSIKKyAgaW50ZWdyaXR5IHNoYTUxMi1qZytxa2ZTNEs4RTc5NjVzcWFVbDhtUm5nWGlLYjNXWkdmT05nRTE4cHIwM0ZVUWl1U1Y2RytFajR0UzU1QitySVFTRkVJdzNwaGRWQVE0cFBxTldmUT09CiAKLXhwYXRoQDAuMC41OgotICB2ZXJzaW9uICIwLjAuNSIKLSAgcmVzb2x2ZWQgImh0dHBzOi8vcmVnaXN0cnkueWFybnBrZy5jb20veHBhdGgvLS94cGF0aC0wLjAuNS50Z3ojNDU0MDM2ZjZlZjBmM2RmNWFmNWQ0YmE0YTExOWZiNzU2NzRiM2U2YyIKK3hwYXRoQDAuMC4yNzoKKyAgdmVyc2lvbiAiMC4wLjI3IgorICByZXNvbHZlZCAiaHR0cHM6Ly9yZWdpc3RyeS55YXJucGtnLmNvbS94cGF0aC8tL3hwYXRoLTAuMC4yNy50Z3ojZGQzNDIxZmJkY2M1NjQ2YWMzMmM0ODUzMWI0ZDdlOWQwYzJjZmE5MiIKKyAgaW50ZWdyaXR5IHNoYTUxMi1mZzAzV1J4dGtDVjZvaENsZVBOQUVDWXNtcEtLVHY1TDh5L1gzRG4xaFFyZWMzUE94MmpIWi8wUDJxUTZIdnNyVTFCbWVxWGNvZjNOR0d1ZUc2THh3UT09Cg==" - } - ] -} diff --git a/spec/fixtures/security-reports/remediations/remediation.patch b/spec/fixtures/security-reports/remediations/remediation.patch deleted file mode 100644 index bbfb6874627..00000000000 --- a/spec/fixtures/security-reports/remediations/remediation.patch +++ /dev/null @@ -1,180 +0,0 @@ -diff --git a/yarn.lock b/yarn.lock -index 0ecc92f..7fa4554 100644 ---- a/yarn.lock -+++ b/yarn.lock -@@ -2,103 +2,124 @@ - # yarn lockfile v1 - - --async@~0.2.7: -- version "0.2.10" -- resolved "http://registry.npmjs.org/async/-/async-0.2.10.tgz#b6bbe0b0674b9d719708ca38de8c237cb526c3d1" -- --async@~1.5.2: -- version "1.5.2" -- resolved "http://registry.npmjs.org/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a" -+async@^2.1.5, async@^2.5.0: -+ version "2.6.1" -+ resolved "https://registry.yarnpkg.com/async/-/async-2.6.1.tgz#b245a23ca71930044ec53fa46aa00a3e87c6a610" -+ integrity sha512-fNEiL2+AZt6AlAw/29Cr0UDe4sRAHCpEHh54WMz+Bb7QfNcFw4h3loofyJpLeQs4Yx7yuqu/2dLgM5hKOs6HlQ== -+ dependencies: -+ lodash "^4.17.10" - --debug@^1.0.4: -- version "1.0.5" -- resolved "https://registry.yarnpkg.com/debug/-/debug-1.0.5.tgz#f7241217430f99dec4c2b473eab92228e874c2ac" -+debug@^2.6.0: -+ version "2.6.9" -+ resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" -+ integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== - dependencies: - ms "2.0.0" - --ejs@~0.8.3: -- version "0.8.8" -- resolved "https://registry.yarnpkg.com/ejs/-/ejs-0.8.8.tgz#ffdc56dcc35d02926dd50ad13439bbc54061d598" -+ejs@^2.5.6: -+ version "2.6.1" -+ resolved "https://registry.yarnpkg.com/ejs/-/ejs-2.6.1.tgz#498ec0d495655abc6f23cd61868d926464071aa0" -+ integrity sha512-0xy4A/twfrRCnkhfk8ErDi5DqdAsAqeGxht4xkCUrsvhhbQNs7E+4jV0CN7+NKIY0aHE72+XvqtBIXzD31ZbXQ== -+ -+lodash-node@~2.4.1: -+ version "2.4.1" -+ resolved "https://registry.yarnpkg.com/lodash-node/-/lodash-node-2.4.1.tgz#ea82f7b100c733d1a42af76801e506105e2a80ec" -+ integrity sha1-6oL3sQDHM9GkKvdoAeUGEF4qgOw= -+ -+lodash@^4.17.10: -+ version "4.17.11" -+ resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d" -+ integrity sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg== - - ms@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" -+ integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= - --node-forge@0.2.24: -- version "0.2.24" -- resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.2.24.tgz#fa6f846f42fa93f63a0a30c9fbff7b4e130e0858" -+node-forge@^0.7.0: -+ version "0.7.6" -+ resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.7.6.tgz#fdf3b418aee1f94f0ef642cd63486c77ca9724ac" -+ integrity sha512-sol30LUpz1jQFBjOKwbjxijiE3b6pjd74YwfD0fJOKPjF+fONKb2Yg8rYgS6+bK6VDl+/wfr4IYpC7jDzLUIfw== - - saml2-js@^1.5.0: -- version "1.5.0" -- resolved "https://registry.yarnpkg.com/saml2-js/-/saml2-js-1.5.0.tgz#c0d2268a179e7329d29eb25aa82df5503774b0d9" -+ version "1.12.4" -+ resolved "https://registry.yarnpkg.com/saml2-js/-/saml2-js-1.12.4.tgz#c288f20bda6d2b91073b16c94ea72f22349ac3b3" -+ integrity sha1-wojyC9ptK5EHOxbJTqcvIjSaw7M= - dependencies: -- async "~1.5.2" -- debug "^1.0.4" -- underscore "~1.6.0" -- xml-crypto "^0.8.1" -- xml-encryption "~0.7.4" -- xml2js "~0.4.1" -- xmlbuilder "~2.1.0" -- xmldom "~0.1.19" -+ async "^2.5.0" -+ debug "^2.6.0" -+ underscore "^1.8.0" -+ xml-crypto "^0.10.0" -+ xml-encryption "^0.11.0" -+ xml2js "^0.4.0" -+ xmlbuilder "~2.2.0" -+ xmldom "^0.1.0" - - sax@>=0.6.0: - version "1.2.4" - resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" -+ integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== - --underscore@>=1.5.x: -+underscore@^1.8.0: - version "1.9.1" - resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.9.1.tgz#06dce34a0e68a7babc29b365b8e74b8925203961" -+ integrity sha512-5/4etnCkd9c8gwgowi5/om/mYO5ajCaOgdzj/oW+0eQV9WxKBDZw5+ycmKmeaTXjInS/W0BzpGLo2xR2aBwZdg== - --underscore@~1.6.0: -- version "1.6.0" -- resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.6.0.tgz#8b38b10cacdef63337b8b24e4ff86d45aea529a8" -- --xml-crypto@^0.8.1: -- version "0.8.5" -- resolved "http://registry.npmjs.org/xml-crypto/-/xml-crypto-0.8.5.tgz#2bbcfb3eb33f3a82a218b822bf672b6b1c20e538" -+xml-crypto@^0.10.0: -+ version "0.10.1" -+ resolved "https://registry.yarnpkg.com/xml-crypto/-/xml-crypto-0.10.1.tgz#f832f74ccf56f24afcae1163a1fcab44d96774a8" -+ integrity sha1-+DL3TM9W8kr8rhFjofyrRNlndKg= - dependencies: - xmldom "=0.1.19" - xpath.js ">=0.0.3" - --xml-encryption@~0.7.4: -- version "0.7.4" -- resolved "https://registry.yarnpkg.com/xml-encryption/-/xml-encryption-0.7.4.tgz#42791ec64d556d2455dcb9da0a54123665ac65c7" -+xml-encryption@^0.11.0: -+ version "0.11.2" -+ resolved "https://registry.yarnpkg.com/xml-encryption/-/xml-encryption-0.11.2.tgz#c217f5509547e34b500b829f2c0bca85cca73a21" -+ integrity sha512-jVvES7i5ovdO7N+NjgncA326xYKjhqeAnnvIgRnY7ROLCfFqEDLwP0Sxp/30SHG0AXQV1048T5yinOFyvwGFzg== - dependencies: -- async "~0.2.7" -- ejs "~0.8.3" -- node-forge "0.2.24" -+ async "^2.1.5" -+ ejs "^2.5.6" -+ node-forge "^0.7.0" - xmldom "~0.1.15" -- xpath "0.0.5" -+ xpath "0.0.27" - --xml2js@~0.4.1: -+xml2js@^0.4.0: - version "0.4.19" - resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.19.tgz#686c20f213209e94abf0d1bcf1efaa291c7827a7" -+ integrity sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q== - dependencies: - sax ">=0.6.0" - xmlbuilder "~9.0.1" - --xmlbuilder@~2.1.0: -- version "2.1.0" -- resolved "http://registry.npmjs.org/xmlbuilder/-/xmlbuilder-2.1.0.tgz#6ddae31683b6df12100b29fc8a0d4f46349abbed" -+xmlbuilder@~2.2.0: -+ version "2.2.1" -+ resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-2.2.1.tgz#9326430f130d87435d4c4086643aa2926e105a32" -+ integrity sha1-kyZDDxMNh0NdTECGZDqikm4QWjI= - dependencies: -- underscore ">=1.5.x" -+ lodash-node "~2.4.1" - - xmlbuilder@~9.0.1: - version "9.0.7" -- resolved "http://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz#132ee63d2ec5565c557e20f4c22df9aca686b10d" -+ resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-9.0.7.tgz#132ee63d2ec5565c557e20f4c22df9aca686b10d" -+ integrity sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0= - - xmldom@=0.1.19: - version "0.1.19" - resolved "https://registry.yarnpkg.com/xmldom/-/xmldom-0.1.19.tgz#631fc07776efd84118bf25171b37ed4d075a0abc" -+ integrity sha1-Yx/Ad3bv2EEYvyUXGzftTQdaCrw= - --xmldom@~0.1.15, xmldom@~0.1.19: -+xmldom@^0.1.0, xmldom@~0.1.15: - version "0.1.27" - resolved "https://registry.yarnpkg.com/xmldom/-/xmldom-0.1.27.tgz#d501f97b3bdb403af8ef9ecc20573187aadac0e9" -+ integrity sha1-1QH5ezvbQDr4757MIFcxh6rawOk= - - xpath.js@>=0.0.3: - version "1.1.0" - resolved "https://registry.yarnpkg.com/xpath.js/-/xpath.js-1.1.0.tgz#3816a44ed4bb352091083d002a383dd5104a5ff1" -+ integrity sha512-jg+qkfS4K8E7965sqaUl8mRngXiKb3WZGfONgE18pr03FUQiuSV6G+Ej4tS55B+rIQSFEIw3phdVAQ4pPqNWfQ== - --xpath@0.0.5: -- version "0.0.5" -- resolved "https://registry.yarnpkg.com/xpath/-/xpath-0.0.5.tgz#454036f6ef0f3df5af5d4ba4a119fb75674b3e6c" -+xpath@0.0.27: -+ version "0.0.27" -+ resolved "https://registry.yarnpkg.com/xpath/-/xpath-0.0.27.tgz#dd3421fbdcc5646ac32c48531b4d7e9d0c2cfa92" -+ integrity sha512-fg03WRxtkCV6ohClePNAECYsmpKKTv5L8y/X3Dn1hQrec3POx2jHZ/0P2qQ6HvsrU1BmeqXcof3NGGueG6LxwQ== diff --git a/spec/fixtures/security-reports/remediations/yarn.lock b/spec/fixtures/security-reports/remediations/yarn.lock deleted file mode 100644 index 0ecc92fb711..00000000000 --- a/spec/fixtures/security-reports/remediations/yarn.lock +++ /dev/null @@ -1,104 +0,0 @@ -# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# yarn lockfile v1 - - -async@~0.2.7: - version "0.2.10" - resolved "http://registry.npmjs.org/async/-/async-0.2.10.tgz#b6bbe0b0674b9d719708ca38de8c237cb526c3d1" - -async@~1.5.2: - version "1.5.2" - resolved "http://registry.npmjs.org/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a" - -debug@^1.0.4: - version "1.0.5" - resolved "https://registry.yarnpkg.com/debug/-/debug-1.0.5.tgz#f7241217430f99dec4c2b473eab92228e874c2ac" - dependencies: - ms "2.0.0" - -ejs@~0.8.3: - version "0.8.8" - resolved "https://registry.yarnpkg.com/ejs/-/ejs-0.8.8.tgz#ffdc56dcc35d02926dd50ad13439bbc54061d598" - -ms@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" - -node-forge@0.2.24: - version "0.2.24" - resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.2.24.tgz#fa6f846f42fa93f63a0a30c9fbff7b4e130e0858" - -saml2-js@^1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/saml2-js/-/saml2-js-1.5.0.tgz#c0d2268a179e7329d29eb25aa82df5503774b0d9" - dependencies: - async "~1.5.2" - debug "^1.0.4" - underscore "~1.6.0" - xml-crypto "^0.8.1" - xml-encryption "~0.7.4" - xml2js "~0.4.1" - xmlbuilder "~2.1.0" - xmldom "~0.1.19" - -sax@>=0.6.0: - version "1.2.4" - resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" - -underscore@>=1.5.x: - version "1.9.1" - resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.9.1.tgz#06dce34a0e68a7babc29b365b8e74b8925203961" - -underscore@~1.6.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.6.0.tgz#8b38b10cacdef63337b8b24e4ff86d45aea529a8" - -xml-crypto@^0.8.1: - version "0.8.5" - resolved "http://registry.npmjs.org/xml-crypto/-/xml-crypto-0.8.5.tgz#2bbcfb3eb33f3a82a218b822bf672b6b1c20e538" - dependencies: - xmldom "=0.1.19" - xpath.js ">=0.0.3" - -xml-encryption@~0.7.4: - version "0.7.4" - resolved "https://registry.yarnpkg.com/xml-encryption/-/xml-encryption-0.7.4.tgz#42791ec64d556d2455dcb9da0a54123665ac65c7" - dependencies: - async "~0.2.7" - ejs "~0.8.3" - node-forge "0.2.24" - xmldom "~0.1.15" - xpath "0.0.5" - -xml2js@~0.4.1: - version "0.4.19" - resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.19.tgz#686c20f213209e94abf0d1bcf1efaa291c7827a7" - dependencies: - sax ">=0.6.0" - xmlbuilder "~9.0.1" - -xmlbuilder@~2.1.0: - version "2.1.0" - resolved "http://registry.npmjs.org/xmlbuilder/-/xmlbuilder-2.1.0.tgz#6ddae31683b6df12100b29fc8a0d4f46349abbed" - dependencies: - underscore ">=1.5.x" - -xmlbuilder@~9.0.1: - version "9.0.7" - resolved "http://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz#132ee63d2ec5565c557e20f4c22df9aca686b10d" - -xmldom@=0.1.19: - version "0.1.19" - resolved "https://registry.yarnpkg.com/xmldom/-/xmldom-0.1.19.tgz#631fc07776efd84118bf25171b37ed4d075a0abc" - -xmldom@~0.1.15, xmldom@~0.1.19: - version "0.1.27" - resolved "https://registry.yarnpkg.com/xmldom/-/xmldom-0.1.27.tgz#d501f97b3bdb403af8ef9ecc20573187aadac0e9" - -xpath.js@>=0.0.3: - version "1.1.0" - resolved "https://registry.yarnpkg.com/xpath.js/-/xpath.js-1.1.0.tgz#3816a44ed4bb352091083d002a383dd5104a5ff1" - -xpath@0.0.5: - version "0.0.5" - resolved "https://registry.yarnpkg.com/xpath/-/xpath-0.0.5.tgz#454036f6ef0f3df5af5d4ba4a119fb75674b3e6c" diff --git a/spec/frontend/api_spec.js b/spec/frontend/api_spec.js index 7004373be0e..62ba0d36982 100644 --- a/spec/frontend/api_spec.js +++ b/spec/frontend/api_spec.js @@ -151,6 +151,28 @@ describe('Api', () => { }); }); + describe('projectUsers', () => { + it('fetches all users of a particular project', done => { + const query = 'dummy query'; + const options = { unused: 'option' }; + const projectPath = 'gitlab-org%2Fgitlab-ce'; + const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/${projectPath}/users`; + mock.onGet(expectedUrl).reply(200, [ + { + name: 'test', + }, + ]); + + Api.projectUsers('gitlab-org/gitlab-ce', query, options) + .then(response => { + expect(response.length).toBe(1); + expect(response[0].name).toBe('test'); + }) + .then(done) + .catch(done.fail); + }); + }); + describe('projectMergeRequests', () => { const projectPath = 'abc'; const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/${projectPath}/merge_requests`; diff --git a/spec/frontend/autosave_spec.js b/spec/frontend/autosave_spec.js index 4d9c8f96d62..33d402388c9 100644 --- a/spec/frontend/autosave_spec.js +++ b/spec/frontend/autosave_spec.js @@ -63,12 +63,15 @@ describe('Autosave', () => { expect(field.trigger).toHaveBeenCalled(); }); - it('triggers native event', done => { - autosave.field.get(0).addEventListener('change', () => { - done(); - }); + it('triggers native event', () => { + const fieldElement = autosave.field.get(0); + const eventHandler = jest.fn(); + fieldElement.addEventListener('change', eventHandler); Autosave.prototype.restore.call(autosave); + + expect(eventHandler).toHaveBeenCalledTimes(1); + fieldElement.removeEventListener('change', eventHandler); }); }); diff --git a/spec/frontend/cycle_analytics/stage_nav_item_spec.js b/spec/frontend/cycle_analytics/stage_nav_item_spec.js new file mode 100644 index 00000000000..ff079082ca7 --- /dev/null +++ b/spec/frontend/cycle_analytics/stage_nav_item_spec.js @@ -0,0 +1,177 @@ +import { mount, shallowMount } from '@vue/test-utils'; +import StageNavItem from '~/cycle_analytics/components/stage_nav_item.vue'; + +describe('StageNavItem', () => { + let wrapper = null; + const title = 'Cool stage'; + const value = '1 day'; + + function createComponent(props, shallow = true) { + const func = shallow ? shallowMount : mount; + return func(StageNavItem, { + propsData: { + canEdit: false, + isActive: false, + isUserAllowed: false, + isDefaultStage: true, + title, + value, + ...props, + }, + }); + } + + function hasStageName() { + const stageName = wrapper.find('.stage-name'); + expect(stageName.exists()).toBe(true); + expect(stageName.text()).toEqual(title); + } + + it('renders stage name', () => { + wrapper = createComponent({ isUserAllowed: true }); + hasStageName(); + wrapper.destroy(); + }); + + describe('User has access', () => { + describe('with a value', () => { + beforeEach(() => { + wrapper = createComponent({ isUserAllowed: true }); + }); + + afterEach(() => { + wrapper.destroy(); + }); + it('renders the value for median value', () => { + expect(wrapper.find('.stage-empty').exists()).toBe(false); + expect(wrapper.find('.not-available').exists()).toBe(false); + expect(wrapper.find('.stage-median').text()).toEqual(value); + }); + }); + + describe('without a value', () => { + beforeEach(() => { + wrapper = createComponent({ isUserAllowed: true, value: null }); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('has the stage-empty class', () => { + expect(wrapper.find('.stage-empty').exists()).toBe(true); + }); + + it('renders Not enough data for the median value', () => { + expect(wrapper.find('.stage-median').text()).toEqual('Not enough data'); + }); + }); + }); + + describe('is active', () => { + beforeEach(() => { + wrapper = createComponent({ isActive: true }, false); + }); + + afterEach(() => { + wrapper.destroy(); + }); + it('has the active class', () => { + expect(wrapper.find('.stage-nav-item').classes('active')).toBe(true); + }); + }); + + describe('is not active', () => { + beforeEach(() => { + wrapper = createComponent(); + }); + + afterEach(() => { + wrapper.destroy(); + }); + it('emits the `select` event when clicked', () => { + expect(wrapper.emitted().select).toBeUndefined(); + wrapper.trigger('click'); + expect(wrapper.emitted().select.length).toBe(1); + }); + }); + + describe('User does not have access', () => { + beforeEach(() => { + wrapper = createComponent({ isUserAllowed: false }, false); + }); + + afterEach(() => { + wrapper.destroy(); + }); + it('renders stage name', () => { + hasStageName(); + }); + + it('has class not-available', () => { + expect(wrapper.find('.stage-empty').exists()).toBe(false); + expect(wrapper.find('.not-available').exists()).toBe(true); + }); + + it('renders Not available for the median value', () => { + expect(wrapper.find('.stage-median').text()).toBe('Not available'); + }); + it('does not render options menu', () => { + expect(wrapper.find('.more-actions-toggle').exists()).toBe(false); + }); + }); + + describe('User can edit stages', () => { + beforeEach(() => { + wrapper = createComponent({ canEdit: true, isUserAllowed: true }, false); + }); + + afterEach(() => { + wrapper.destroy(); + }); + it('renders stage name', () => { + hasStageName(); + }); + + it('renders options menu', () => { + expect(wrapper.find('.more-actions-toggle').exists()).toBe(true); + }); + + describe('Default stages', () => { + beforeEach(() => { + wrapper = createComponent( + { canEdit: true, isUserAllowed: true, isDefaultStage: true }, + false, + ); + }); + it('can hide the stage', () => { + expect(wrapper.text()).toContain('Hide stage'); + }); + it('can not edit the stage', () => { + expect(wrapper.text()).not.toContain('Edit stage'); + }); + it('can not remove the stage', () => { + expect(wrapper.text()).not.toContain('Remove stage'); + }); + }); + + describe('Custom stages', () => { + beforeEach(() => { + wrapper = createComponent( + { canEdit: true, isUserAllowed: true, isDefaultStage: false }, + false, + ); + }); + it('can edit the stage', () => { + expect(wrapper.text()).toContain('Edit stage'); + }); + it('can remove the stage', () => { + expect(wrapper.text()).toContain('Remove stage'); + }); + + it('can not hide the stage', () => { + expect(wrapper.text()).not.toContain('Hide stage'); + }); + }); + }); +}); diff --git a/spec/javascripts/environments/confirm_rollback_modal_spec.js b/spec/frontend/environments/confirm_rollback_modal_spec.js index 05715bce38f..a1a22274e8f 100644 --- a/spec/javascripts/environments/confirm_rollback_modal_spec.js +++ b/spec/frontend/environments/confirm_rollback_modal_spec.js @@ -61,7 +61,7 @@ describe('Confirm Rollback Modal Component', () => { environment, }, }); - const eventHubSpy = spyOn(eventHub, '$emit'); + const eventHubSpy = jest.spyOn(eventHub, '$emit'); const modal = component.find(GlModal); modal.vm.$emit('ok'); diff --git a/spec/javascripts/environments/environment_rollback_spec.js b/spec/frontend/environments/environment_rollback_spec.js index 8c47f6a12c0..fb62a096c3d 100644 --- a/spec/javascripts/environments/environment_rollback_spec.js +++ b/spec/frontend/environments/environment_rollback_spec.js @@ -1,46 +1,38 @@ -import Vue from 'vue'; -import { shallowMount } from '@vue/test-utils'; +import { shallowMount, mount } from '@vue/test-utils'; import { GlButton } from '@gitlab/ui'; import eventHub from '~/environments/event_hub'; -import rollbackComp from '~/environments/components/environment_rollback.vue'; +import RollbackComponent from '~/environments/components/environment_rollback.vue'; describe('Rollback Component', () => { const retryUrl = 'https://gitlab.com/retry'; - let RollbackComponent; - - beforeEach(() => { - RollbackComponent = Vue.extend(rollbackComp); - }); it('Should render Re-deploy label when isLastDeployment is true', () => { - const component = new RollbackComponent({ - el: document.querySelector('.test-dom-element'), + const wrapper = mount(RollbackComponent, { propsData: { retryUrl, isLastDeployment: true, environment: {}, }, - }).$mount(); + }); - expect(component.$el).toHaveSpriteIcon('repeat'); + expect(wrapper.element).toHaveSpriteIcon('repeat'); }); it('Should render Rollback label when isLastDeployment is false', () => { - const component = new RollbackComponent({ - el: document.querySelector('.test-dom-element'), + const wrapper = mount(RollbackComponent, { propsData: { retryUrl, isLastDeployment: false, environment: {}, }, - }).$mount(); + }); - expect(component.$el).toHaveSpriteIcon('redo'); + expect(wrapper.element).toHaveSpriteIcon('redo'); }); it('should emit a "rollback" event on button click', () => { - const eventHubSpy = spyOn(eventHub, '$emit'); - const component = shallowMount(RollbackComponent, { + const eventHubSpy = jest.spyOn(eventHub, '$emit'); + const wrapper = shallowMount(RollbackComponent, { propsData: { retryUrl, environment: { @@ -48,7 +40,7 @@ describe('Rollback Component', () => { }, }, }); - const button = component.find(GlButton); + const button = wrapper.find(GlButton); button.vm.$emit('click'); diff --git a/spec/frontend/ide/stores/modules/commit/mutations_spec.js b/spec/frontend/ide/stores/modules/commit/mutations_spec.js index 246500a2f34..45ac1a86ab3 100644 --- a/spec/frontend/ide/stores/modules/commit/mutations_spec.js +++ b/spec/frontend/ide/stores/modules/commit/mutations_spec.js @@ -62,12 +62,4 @@ describe('IDE commit module mutations', () => { expect(state.shouldCreateMR).toBe(false); }); }); - - describe('INTERACT_WITH_NEW_MR', () => { - it('sets interactedWithNewMR to true', () => { - mutations.INTERACT_WITH_NEW_MR(state); - - expect(state.interactedWithNewMR).toBe(true); - }); - }); }); diff --git a/spec/frontend/lib/utils/url_utility_spec.js b/spec/frontend/lib/utils/url_utility_spec.js index a986bc49f28..b0bdd924921 100644 --- a/spec/frontend/lib/utils/url_utility_spec.js +++ b/spec/frontend/lib/utils/url_utility_spec.js @@ -94,6 +94,12 @@ describe('URL utility', () => { it('adds and updates encoded params', () => { expect(urlUtils.mergeUrlParams({ a: '&', q: '?' }, '?a=%23#frag')).toBe('?a=%26&q=%3F#frag'); }); + + it('treats "+" as "%20"', () => { + expect(urlUtils.mergeUrlParams({ ref: 'bogus' }, '?a=lorem+ipsum&ref=charlie')).toBe( + '?a=lorem%20ipsum&ref=bogus', + ); + }); }); describe('removeParams', () => { diff --git a/spec/frontend/matchers.js b/spec/frontend/matchers.js new file mode 100644 index 00000000000..35c362d0bf5 --- /dev/null +++ b/spec/frontend/matchers.js @@ -0,0 +1,38 @@ +export default { + toHaveSpriteIcon: (element, iconName) => { + if (!iconName) { + throw new Error('toHaveSpriteIcon is missing iconName argument!'); + } + + if (!(element instanceof HTMLElement)) { + throw new Error(`${element} is not a DOM element!`); + } + + const iconReferences = [].slice.apply(element.querySelectorAll('svg use')); + const matchingIcon = iconReferences.find(reference => + reference.getAttribute('xlink:href').endsWith(`#${iconName}`), + ); + + const pass = Boolean(matchingIcon); + + let message; + if (pass) { + message = `${element.outerHTML} contains the sprite icon "${iconName}"!`; + } else { + message = `${element.outerHTML} does not contain the sprite icon "${iconName}"!`; + + const existingIcons = iconReferences.map(reference => { + const iconUrl = reference.getAttribute('xlink:href'); + return `"${iconUrl.replace(/^.+#/, '')}"`; + }); + if (existingIcons.length > 0) { + message += ` (only found ${existingIcons.join(',')})`; + } + } + + return { + pass, + message: () => message, + }; + }, +}; diff --git a/spec/frontend/mocks/mocks_helper_spec.js b/spec/frontend/mocks/mocks_helper_spec.js index 34be110a7e3..b8bb02c2f43 100644 --- a/spec/frontend/mocks/mocks_helper_spec.js +++ b/spec/frontend/mocks/mocks_helper_spec.js @@ -46,7 +46,9 @@ describe('mocks_helper.js', () => { readdir.sync.mockReturnValue([]); setupManualMocks(); - readdir.mock.calls.forEach(call => { + const readdirSpy = readdir.sync; + expect(readdirSpy).toHaveBeenCalled(); + readdirSpy.mock.calls.forEach(call => { expect(call[1].deep).toBeLessThan(100); }); }); diff --git a/spec/frontend/monitoring/embed/embed_spec.js b/spec/frontend/monitoring/embed/embed_spec.js index 3b18a0f77c7..1ce14e2418a 100644 --- a/spec/frontend/monitoring/embed/embed_spec.js +++ b/spec/frontend/monitoring/embed/embed_spec.js @@ -1,7 +1,7 @@ import { createLocalVue, shallowMount } from '@vue/test-utils'; import Vuex from 'vuex'; import Embed from '~/monitoring/components/embed.vue'; -import MonitorAreaChart from '~/monitoring/components/charts/area.vue'; +import MonitorTimeSeriesChart from '~/monitoring/components/charts/time_series.vue'; import { TEST_HOST } from 'helpers/test_constants'; import { groups, initialState, metricsData, metricsWithData } from './mock_data'; @@ -55,7 +55,7 @@ describe('Embed', () => { it('shows an empty state when no metrics are present', () => { expect(wrapper.find('.metrics-embed').exists()).toBe(true); - expect(wrapper.find(MonitorAreaChart).exists()).toBe(false); + expect(wrapper.find(MonitorTimeSeriesChart).exists()).toBe(false); }); }); @@ -71,8 +71,8 @@ describe('Embed', () => { it('shows a chart when metrics are present', () => { wrapper.setProps({}); expect(wrapper.find('.metrics-embed').exists()).toBe(true); - expect(wrapper.find(MonitorAreaChart).exists()).toBe(true); - expect(wrapper.findAll(MonitorAreaChart).length).toBe(2); + expect(wrapper.find(MonitorTimeSeriesChart).exists()).toBe(true); + expect(wrapper.findAll(MonitorTimeSeriesChart).length).toBe(2); }); }); }); diff --git a/spec/frontend/notes/components/discussion_keyboard_navigator_spec.js b/spec/frontend/notes/components/discussion_keyboard_navigator_spec.js index 6d50713999d..8881bedf3cc 100644 --- a/spec/frontend/notes/components/discussion_keyboard_navigator_spec.js +++ b/spec/frontend/notes/components/discussion_keyboard_navigator_spec.js @@ -74,4 +74,31 @@ describe('notes/components/discussion_keyboard_navigator', () => { expect(wrapper.vm.currentDiscussionId).toEqual(expectedPrevId); }); }); + + describe('on destroy', () => { + beforeEach(() => { + jest.spyOn(Mousetrap, 'unbind'); + + createComponent(); + + wrapper.destroy(); + }); + + it('unbinds keys', () => { + expect(Mousetrap.unbind).toHaveBeenCalledWith('n'); + expect(Mousetrap.unbind).toHaveBeenCalledWith('p'); + }); + + it('does not call jumpToNextDiscussion when pressing `n`', () => { + Mousetrap.trigger('n'); + + expect(wrapper.vm.jumpToDiscussion).not.toHaveBeenCalled(); + }); + + it('does not call jumpToNextDiscussion when pressing `p`', () => { + Mousetrap.trigger('p'); + + expect(wrapper.vm.jumpToDiscussion).not.toHaveBeenCalled(); + }); + }); }); diff --git a/spec/frontend/pages/search/show/__snapshots__/refresh_counts_spec.js.snap b/spec/frontend/pages/search/show/__snapshots__/refresh_counts_spec.js.snap new file mode 100644 index 00000000000..ce456d6c899 --- /dev/null +++ b/spec/frontend/pages/search/show/__snapshots__/refresh_counts_spec.js.snap @@ -0,0 +1,7 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`pages/search/show/refresh_counts fetches and displays search counts 1`] = ` +"<div class=\\"badge\\">22</div> +<div class=\\"badge js-search-count\\" data-url=\\"http://test.host/search/count?search=lorem+ipsum&project_id=3&scope=issues\\">4</div> +<div class=\\"badge js-search-count\\" data-url=\\"http://test.host/search/count?search=lorem+ipsum&project_id=3&scope=merge_requests\\">5</div>" +`; diff --git a/spec/frontend/pages/search/show/refresh_counts_spec.js b/spec/frontend/pages/search/show/refresh_counts_spec.js new file mode 100644 index 00000000000..ead268b3971 --- /dev/null +++ b/spec/frontend/pages/search/show/refresh_counts_spec.js @@ -0,0 +1,35 @@ +import MockAdapter from 'axios-mock-adapter'; +import { TEST_HOST } from 'helpers/test_constants'; +import axios from '~/lib/utils/axios_utils'; +import refreshCounts from '~/pages/search/show/refresh_counts'; + +const URL = `${TEST_HOST}/search/count?search=lorem+ipsum&project_id=3`; +const urlWithScope = scope => `${URL}&scope=${scope}`; +const counts = [{ scope: 'issues', count: 4 }, { scope: 'merge_requests', count: 5 }]; +const fixture = `<div class="badge">22</div> +<div class="badge js-search-count hidden" data-url="${urlWithScope('issues')}"></div> +<div class="badge js-search-count hidden" data-url="${urlWithScope('merge_requests')}"></div>`; + +describe('pages/search/show/refresh_counts', () => { + let mock; + + beforeEach(() => { + mock = new MockAdapter(axios); + setFixtures(fixture); + }); + + afterEach(() => { + mock.restore(); + }); + + it('fetches and displays search counts', () => { + counts.forEach(({ scope, count }) => { + mock.onGet(urlWithScope(scope)).reply(200, { count }); + }); + + // assert before act behavior + return refreshCounts().then(() => { + expect(document.body.innerHTML).toMatchSnapshot(); + }); + }); +}); diff --git a/spec/frontend/sidebar/components/assignees/assignee_avatar_link_spec.js b/spec/frontend/sidebar/components/assignees/assignee_avatar_link_spec.js new file mode 100644 index 00000000000..452d4cd07cc --- /dev/null +++ b/spec/frontend/sidebar/components/assignees/assignee_avatar_link_spec.js @@ -0,0 +1,85 @@ +import { shallowMount } from '@vue/test-utils'; +import { joinPaths } from '~/lib/utils/url_utility'; +import { TEST_HOST } from 'helpers/test_constants'; +import AssigneeAvatarLink from '~/sidebar/components/assignees/assignee_avatar_link.vue'; +import AssigneeAvatar from '~/sidebar/components/assignees/assignee_avatar.vue'; +import userDataMock from '../../user_data_mock'; + +const TOOLTIP_PLACEMENT = 'bottom'; +const { name: USER_NAME, username: USER_USERNAME } = userDataMock(); +const TEST_ISSUABLE_TYPE = 'merge_request'; + +describe('AssigneeAvatarLink component', () => { + let wrapper; + + function createComponent(props = {}) { + const propsData = { + user: userDataMock(), + showLess: true, + rootPath: TEST_HOST, + tooltipPlacement: TOOLTIP_PLACEMENT, + singleUser: false, + issuableType: TEST_ISSUABLE_TYPE, + ...props, + }; + + wrapper = shallowMount(AssigneeAvatarLink, { + propsData, + sync: false, + }); + } + + afterEach(() => { + wrapper.destroy(); + }); + + const findTooltipText = () => wrapper.attributes('data-original-title'); + + it('has the root url present in the assigneeUrl method', () => { + createComponent(); + const assigneeUrl = joinPaths(TEST_HOST, USER_USERNAME); + + expect(wrapper.attributes().href).toEqual(assigneeUrl); + }); + + it('renders assignee avatar', () => { + createComponent(); + + expect(wrapper.find(AssigneeAvatar).props()).toEqual( + expect.objectContaining({ + issuableType: TEST_ISSUABLE_TYPE, + user: userDataMock(), + }), + ); + }); + + describe.each` + issuableType | tooltipHasName | canMerge | expected + ${'merge_request'} | ${true} | ${true} | ${USER_NAME} + ${'merge_request'} | ${true} | ${false} | ${`${USER_NAME} (cannot merge)`} + ${'merge_request'} | ${false} | ${true} | ${''} + ${'merge_request'} | ${false} | ${false} | ${'Cannot merge'} + ${'issue'} | ${true} | ${true} | ${USER_NAME} + ${'issue'} | ${true} | ${false} | ${USER_NAME} + ${'issue'} | ${false} | ${true} | ${''} + ${'issue'} | ${false} | ${false} | ${''} + `( + 'with $issuableType and tooltipHasName=$tooltipHasName and canMerge=$canMerge', + ({ issuableType, tooltipHasName, canMerge, expected }) => { + beforeEach(() => { + createComponent({ + issuableType, + tooltipHasName, + user: { + ...userDataMock(), + can_merge: canMerge, + }, + }); + }); + + it('sets tooltip', () => { + expect(findTooltipText()).toBe(expected); + }); + }, + ); +}); diff --git a/spec/frontend/sidebar/components/assignees/assignee_avatar_spec.js b/spec/frontend/sidebar/components/assignees/assignee_avatar_spec.js new file mode 100644 index 00000000000..d60ae17733b --- /dev/null +++ b/spec/frontend/sidebar/components/assignees/assignee_avatar_spec.js @@ -0,0 +1,78 @@ +import { shallowMount } from '@vue/test-utils'; +import AssigneeAvatar from '~/sidebar/components/assignees/assignee_avatar.vue'; +import { TEST_HOST } from 'helpers/test_constants'; +import userDataMock from '../../user_data_mock'; + +const TEST_AVATAR = `${TEST_HOST}/avatar.png`; +const TEST_DEFAULT_AVATAR_URL = `${TEST_HOST}/default/avatar/url.png`; + +describe('AssigneeAvatar', () => { + let origGon; + let wrapper; + + function createComponent(props = {}) { + const propsData = { + user: userDataMock(), + imgSize: 24, + issuableType: 'merge_request', + ...props, + }; + + wrapper = shallowMount(AssigneeAvatar, { + propsData, + sync: false, + }); + } + + beforeEach(() => { + origGon = window.gon; + window.gon = { default_avatar_url: TEST_DEFAULT_AVATAR_URL }; + }); + + afterEach(() => { + window.gon = origGon; + wrapper.destroy(); + }); + + const findImg = () => wrapper.find('img'); + + it('does not show warning icon if assignee can merge', () => { + createComponent(); + + expect(wrapper.find('.merge-icon').exists()).toBe(false); + }); + + it('shows warning icon if assignee cannot merge', () => { + createComponent({ + user: { + can_merge: false, + }, + }); + + expect(wrapper.find('.merge-icon').exists()).toBe(true); + }); + + it('does not show warning icon for issuableType = "issue"', () => { + createComponent({ + issuableType: 'issue', + }); + + expect(wrapper.find('.merge-icon').exists()).toBe(false); + }); + + it.each` + avatar | avatar_url | expected | desc + ${TEST_AVATAR} | ${null} | ${TEST_AVATAR} | ${'with avatar'} + ${null} | ${TEST_AVATAR} | ${TEST_AVATAR} | ${'with avatar_url'} + ${null} | ${null} | ${TEST_DEFAULT_AVATAR_URL} | ${'with no avatar'} + `('$desc', ({ avatar, avatar_url, expected }) => { + createComponent({ + user: { + avatar, + avatar_url, + }, + }); + + expect(findImg().attributes('src')).toEqual(expected); + }); +}); diff --git a/spec/frontend/sidebar/components/assignees/collapsed_assignee_list_spec.js b/spec/frontend/sidebar/components/assignees/collapsed_assignee_list_spec.js new file mode 100644 index 00000000000..ff0c8d181b5 --- /dev/null +++ b/spec/frontend/sidebar/components/assignees/collapsed_assignee_list_spec.js @@ -0,0 +1,189 @@ +import { shallowMount } from '@vue/test-utils'; +import CollapsedAssigneeList from '~/sidebar/components/assignees/collapsed_assignee_list.vue'; +import CollapsedAssignee from '~/sidebar/components/assignees/collapsed_assignee.vue'; +import UsersMockHelper from 'helpers/user_mock_data_helper'; + +const DEFAULT_MAX_COUNTER = 99; + +describe('CollapsedAssigneeList component', () => { + let wrapper; + + function createComponent(props = {}) { + const propsData = { + users: [], + issuableType: 'merge_request', + ...props, + }; + + wrapper = shallowMount(CollapsedAssigneeList, { + propsData, + sync: false, + }); + } + + const findNoUsersIcon = () => wrapper.find('i[aria-label=None]'); + const findAvatarCounter = () => wrapper.find('.avatar-counter'); + const findAssignees = () => wrapper.findAll(CollapsedAssignee); + const getTooltipTitle = () => wrapper.attributes('data-original-title'); + + afterEach(() => { + wrapper.destroy(); + }); + + describe('No assignees/users', () => { + beforeEach(() => { + createComponent({ + users: [], + }); + }); + + it('has no users', () => { + expect(findNoUsersIcon().exists()).toBe(true); + }); + }); + + describe('One assignee/user', () => { + let users; + + beforeEach(() => { + users = UsersMockHelper.createNumberRandomUsers(1); + }); + + it('should not show no users icon', () => { + createComponent({ users }); + + expect(findNoUsersIcon().exists()).toBe(false); + }); + + it('has correct "cannot merge" tooltip when user cannot merge', () => { + users[0].can_merge = false; + + createComponent({ users }); + + expect(getTooltipTitle()).toContain('cannot merge'); + }); + + it('does not have "merge" word in tooltip if user can merge', () => { + users[0].can_merge = true; + + createComponent({ users }); + + expect(getTooltipTitle()).not.toContain('merge'); + }); + }); + + describe('More than one assignees/users', () => { + let users; + + beforeEach(() => { + users = UsersMockHelper.createNumberRandomUsers(2); + + createComponent({ users }); + }); + + it('has multiple-users class', () => { + expect(wrapper.classes('multiple-users')).toBe(true); + }); + + it('does not display an avatar count', () => { + expect(findAvatarCounter().exists()).toBe(false); + }); + + it('returns just two collapsed users', () => { + expect(findAssignees().length).toBe(2); + }); + }); + + describe('More than two assignees/users', () => { + let users; + let userNames; + + beforeEach(() => { + users = UsersMockHelper.createNumberRandomUsers(3); + userNames = users.map(x => x.name).join(', '); + }); + + describe('default', () => { + beforeEach(() => { + createComponent({ users }); + }); + + it('does display an avatar count', () => { + expect(findAvatarCounter().exists()).toBe(true); + expect(findAvatarCounter().text()).toEqual('+2'); + }); + + it('returns one collapsed users', () => { + expect(findAssignees().length).toBe(1); + }); + }); + + it('has corrent "no one can merge" tooltip when no one can merge', () => { + users[0].can_merge = false; + users[1].can_merge = false; + users[2].can_merge = false; + + createComponent({ + users, + }); + + expect(getTooltipTitle()).toEqual(`${userNames} (no one can merge)`); + }); + + it('has correct "cannot merge" tooltip when one user can merge', () => { + users[0].can_merge = true; + users[1].can_merge = false; + users[2].can_merge = false; + + createComponent({ + users, + }); + + expect(getTooltipTitle()).toEqual(`${userNames} (1/3 can merge)`); + }); + + it('has correct "cannot merge" tooltip when more than one user can merge', () => { + users[0].can_merge = false; + users[1].can_merge = true; + users[2].can_merge = true; + + createComponent({ + users, + }); + + expect(getTooltipTitle()).toEqual(`${userNames} (2/3 can merge)`); + }); + + it('does not have "merge" in tooltip if everyone can merge', () => { + users[0].can_merge = true; + users[1].can_merge = true; + users[2].can_merge = true; + + createComponent({ + users, + }); + + expect(getTooltipTitle()).toEqual(userNames); + }); + + it('displays the correct avatar count', () => { + users = UsersMockHelper.createNumberRandomUsers(5); + + createComponent({ + users, + }); + + expect(findAvatarCounter().text()).toEqual(`+${users.length - 1}`); + }); + + it('displays the correct avatar count via a computed property if more than default max counter', () => { + users = UsersMockHelper.createNumberRandomUsers(100); + + createComponent({ + users, + }); + + expect(findAvatarCounter().text()).toEqual(`${DEFAULT_MAX_COUNTER}+`); + }); + }); +}); diff --git a/spec/frontend/sidebar/components/assignees/collapsed_assignee_spec.js b/spec/frontend/sidebar/components/assignees/collapsed_assignee_spec.js new file mode 100644 index 00000000000..f9ca7bc1ecb --- /dev/null +++ b/spec/frontend/sidebar/components/assignees/collapsed_assignee_spec.js @@ -0,0 +1,49 @@ +import { shallowMount } from '@vue/test-utils'; +import CollapsedAssignee from '~/sidebar/components/assignees/collapsed_assignee.vue'; +import AssigneeAvatar from '~/sidebar/components/assignees/assignee_avatar.vue'; +import userDataMock from '../../user_data_mock'; + +const TEST_USER = userDataMock(); +const TEST_ISSUABLE_TYPE = 'merge_request'; + +describe('CollapsedAssignee assignee component', () => { + let wrapper; + + function createComponent(props = {}) { + const propsData = { + user: userDataMock(), + issuableType: TEST_ISSUABLE_TYPE, + ...props, + }; + + wrapper = shallowMount(CollapsedAssignee, { + propsData, + sync: false, + }); + } + + afterEach(() => { + wrapper.destroy(); + }); + + it('has author name', () => { + createComponent(); + + expect( + wrapper + .find('.author') + .text() + .trim(), + ).toEqual(TEST_USER.name); + }); + + it('has assignee avatar', () => { + createComponent(); + + expect(wrapper.find(AssigneeAvatar).props()).toEqual({ + imgSize: 24, + user: TEST_USER, + issuableType: TEST_ISSUABLE_TYPE, + }); + }); +}); diff --git a/spec/frontend/sidebar/components/assignees/uncollapsed_assignee_list_spec.js b/spec/frontend/sidebar/components/assignees/uncollapsed_assignee_list_spec.js new file mode 100644 index 00000000000..6398351834c --- /dev/null +++ b/spec/frontend/sidebar/components/assignees/uncollapsed_assignee_list_spec.js @@ -0,0 +1,103 @@ +import { mount } from '@vue/test-utils'; +import UncollapsedAssigneeList from '~/sidebar/components/assignees/uncollapsed_assignee_list.vue'; +import AssigneeAvatarLink from '~/sidebar/components/assignees/assignee_avatar_link.vue'; +import { TEST_HOST } from 'helpers/test_constants'; +import userDataMock from '../../user_data_mock'; +import UsersMockHelper from '../../../helpers/user_mock_data_helper'; + +const DEFAULT_RENDER_COUNT = 5; + +describe('UncollapsedAssigneeList component', () => { + let wrapper; + + function createComponent(props = {}) { + const propsData = { + users: [], + rootPath: TEST_HOST, + ...props, + }; + + wrapper = mount(UncollapsedAssigneeList, { + sync: false, + propsData, + }); + } + + afterEach(() => { + wrapper.destroy(); + }); + + const findMoreButton = () => wrapper.find('.user-list-more button'); + + describe('One assignee/user', () => { + let user; + + beforeEach(() => { + user = userDataMock(); + + createComponent({ + users: [user], + }); + }); + + it('only has one user', () => { + expect(wrapper.findAll(AssigneeAvatarLink).length).toBe(1); + }); + + it('calls the AssigneeAvatarLink with the proper props', () => { + expect(wrapper.find(AssigneeAvatarLink).exists()).toBe(true); + expect(wrapper.find(AssigneeAvatarLink).props().tooltipPlacement).toEqual('left'); + }); + + it('Shows one user with avatar, username and author name', () => { + expect(wrapper.text()).toContain(user.name); + expect(wrapper.text()).toContain(`@${user.username}`); + }); + }); + + describe('n+ more label', () => { + describe('when users count is rendered users', () => { + beforeEach(() => { + createComponent({ + users: UsersMockHelper.createNumberRandomUsers(DEFAULT_RENDER_COUNT), + }); + }); + + it('does not show more label', () => { + expect(findMoreButton().exists()).toBe(false); + }); + }); + + describe('when more than rendered users', () => { + beforeEach(() => { + createComponent({ + users: UsersMockHelper.createNumberRandomUsers(DEFAULT_RENDER_COUNT + 1), + }); + }); + + it('shows "+1 more" label', () => { + expect(findMoreButton().text()).toBe('+ 1 more'); + }); + + it('shows truncated users', () => { + expect(wrapper.findAll(AssigneeAvatarLink).length).toBe(DEFAULT_RENDER_COUNT); + }); + + describe('when more button is clicked', () => { + beforeEach(() => { + findMoreButton().trigger('click'); + + return wrapper.vm.$nextTick(); + }); + + it('shows "show less" label', () => { + expect(findMoreButton().text()).toBe('- show less'); + }); + + it('shows all users', () => { + expect(wrapper.findAll(AssigneeAvatarLink).length).toBe(DEFAULT_RENDER_COUNT + 1); + }); + }); + }); + }); +}); diff --git a/spec/frontend/sidebar/user_data_mock.js b/spec/frontend/sidebar/user_data_mock.js new file mode 100644 index 00000000000..8ad70bb3499 --- /dev/null +++ b/spec/frontend/sidebar/user_data_mock.js @@ -0,0 +1,9 @@ +export default () => ({ + avatar_url: 'mock_path', + id: 1, + name: 'Root', + state: 'active', + username: 'root', + web_url: '', + can_merge: true, +}); diff --git a/spec/frontend/test_setup.js b/spec/frontend/test_setup.js index 8b6f7802b15..d52aeb1fe6b 100644 --- a/spec/frontend/test_setup.js +++ b/spec/frontend/test_setup.js @@ -6,6 +6,7 @@ import { config as testUtilsConfig } from '@vue/test-utils'; import { initializeTestTimeout } from './helpers/timeout'; import { loadHTMLFixture, setHTMLFixture } from './helpers/fixtures'; import { setupManualMocks } from './mocks/mocks_helper'; +import customMatchers from './matchers'; // Expose jQuery so specs using jQuery plugins can be imported nicely. // Here is an issue to explore better alternatives: @@ -67,6 +68,8 @@ Object.entries(jqueryMatchers).forEach(([matcherName, matcherFactory]) => { }); }); +expect.extend(customMatchers); + // Tech debt issue TBD testUtilsConfig.logModifiedComponents = false; @@ -90,3 +93,9 @@ Object.assign(global, { clearTimeout(id); }, }); + +// make sure that each test actually tests something +// see https://jestjs.io/docs/en/expect#expecthasassertions +beforeEach(() => { + expect.hasAssertions(); +}); diff --git a/spec/frontend/tracking_spec.js b/spec/frontend/tracking_spec.js index cd0bf50f8e9..0e862c683d3 100644 --- a/spec/frontend/tracking_spec.js +++ b/spec/frontend/tracking_spec.js @@ -15,6 +15,12 @@ describe('Tracking', () => { snowplowSpy = jest.spyOn(window, 'snowplow'); }); + afterEach(() => { + window.doNotTrack = undefined; + navigator.doNotTrack = undefined; + navigator.msDoNotTrack = undefined; + }); + it('tracks to snowplow (our current tracking system)', () => { Tracking.event('_category_', '_eventName_', { label: '_label_' }); @@ -31,6 +37,27 @@ describe('Tracking', () => { expect(snowplowSpy).not.toHaveBeenCalled(); }); + + it('skips tracking if the user does not want to be tracked (general spec)', () => { + window.doNotTrack = '1'; + Tracking.event('_category_', '_eventName_'); + + expect(snowplowSpy).not.toHaveBeenCalled(); + }); + + it('skips tracking if the user does not want to be tracked (firefox legacy)', () => { + navigator.doNotTrack = 'yes'; + Tracking.event('_category_', '_eventName_'); + + expect(snowplowSpy).not.toHaveBeenCalled(); + }); + + it('skips tracking if the user does not want to be tracked (IE legacy)', () => { + navigator.msDoNotTrack = '1'; + Tracking.event('_category_', '_eventName_'); + + expect(snowplowSpy).not.toHaveBeenCalled(); + }); }); describe('tracking interface events', () => { diff --git a/spec/frontend/vue_shared/components/changed_file_icon_spec.js b/spec/frontend/vue_shared/components/changed_file_icon_spec.js new file mode 100644 index 00000000000..806602877ef --- /dev/null +++ b/spec/frontend/vue_shared/components/changed_file_icon_spec.js @@ -0,0 +1,123 @@ +import { shallowMount } from '@vue/test-utils'; +import ChangedFileIcon from '~/vue_shared/components/changed_file_icon.vue'; +import Icon from '~/vue_shared/components/icon.vue'; + +const changedFile = () => ({ changed: true }); +const stagedFile = () => ({ changed: false, staged: true }); +const changedAndStagedFile = () => ({ changed: true, staged: true }); +const newFile = () => ({ changed: true, tempFile: true }); +const unchangedFile = () => ({ changed: false, tempFile: false, staged: false, deleted: false }); + +describe('Changed file icon', () => { + let wrapper; + + const factory = (props = {}) => { + wrapper = shallowMount(ChangedFileIcon, { + propsData: { + file: changedFile(), + showTooltip: true, + ...props, + }, + sync: false, + }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + const findIcon = () => wrapper.find(Icon); + const findIconName = () => findIcon().props('name'); + const findIconClasses = () => + findIcon() + .props('cssClasses') + .split(' '); + const findTooltipText = () => wrapper.attributes('data-original-title'); + + it('with isCentered true, adds center class', () => { + factory({ + isCentered: true, + }); + + expect(wrapper.classes('ml-auto')).toBe(true); + }); + + it('with isCentered false, does not center', () => { + factory({ + isCentered: false, + }); + + expect(wrapper.classes('ml-auto')).toBe(false); + }); + + it('with showTooltip false, does not show tooltip', () => { + factory({ + showTooltip: false, + }); + + expect(findTooltipText()).toBeFalsy(); + }); + + describe.each` + file | iconName | tooltipText | desc + ${changedFile()} | ${'file-modified'} | ${'Unstaged modification'} | ${'with file changed'} + ${stagedFile()} | ${'file-modified-solid'} | ${'Staged modification'} | ${'with file staged'} + ${changedAndStagedFile()} | ${'file-modified'} | ${'Unstaged and staged modification'} | ${'with file changed and staged'} + ${newFile()} | ${'file-addition'} | ${'Unstaged addition'} | ${'with file new'} + `('$desc', ({ file, iconName, tooltipText }) => { + beforeEach(() => { + factory({ file }); + }); + + it('renders icon', () => { + expect(findIconName()).toBe(iconName); + expect(findIconClasses()).toContain(iconName); + }); + + it('renders tooltip text', () => { + expect(findTooltipText()).toBe(tooltipText); + }); + }); + + describe('with file unchanged', () => { + beforeEach(() => { + factory({ + file: unchangedFile(), + }); + }); + + it('does not show icon', () => { + expect(findIcon().exists()).toBe(false); + }); + + it('does not have tooltip text', () => { + expect(findTooltipText()).toBe(''); + }); + }); + + it('with size set, sets icon size', () => { + const size = 8; + + factory({ + file: changedFile(), + size, + }); + + expect(findIcon().props('size')).toBe(size); + }); + + // NOTE: It looks like 'showStagedIcon' behavior is backwards to what the name suggests + // https://gitlab.com/gitlab-org/gitlab-ce/issues/66071 + it.each` + showStagedIcon | iconName | desc + ${false} | ${'file-modified-solid'} | ${'with showStagedIcon false, renders staged icon'} + ${true} | ${'file-modified'} | ${'with showStagedIcon true, renders regular icon'} + `('$desc', ({ showStagedIcon, iconName }) => { + factory({ + file: stagedFile(), + showStagedIcon, + }); + + expect(findIconName()).toEqual(iconName); + }); +}); diff --git a/spec/frontend/wikis_spec.js b/spec/frontend/wikis_spec.js new file mode 100644 index 00000000000..b2475488d97 --- /dev/null +++ b/spec/frontend/wikis_spec.js @@ -0,0 +1,74 @@ +import Wikis from '~/pages/projects/wikis/wikis'; +import { setHTMLFixture } from './helpers/fixtures'; + +describe('Wikis', () => { + describe('setting the commit message when the title changes', () => { + const editFormHtmlFixture = args => `<form class="wiki-form ${ + args.newPage ? 'js-new-wiki-page' : '' + }"> + <input type="text" id="wiki_title" value="My title" /> + <input type="text" id="wiki_message" /> + </form>`; + + let wikis; + let titleInput; + let messageInput; + + describe('when the wiki page is being created', () => { + const formHtmlFixture = editFormHtmlFixture({ newPage: true }); + + beforeEach(() => { + setHTMLFixture(formHtmlFixture); + + titleInput = document.getElementById('wiki_title'); + messageInput = document.getElementById('wiki_message'); + wikis = new Wikis(); + }); + + it('binds an event listener to the title input', () => { + wikis.handleWikiTitleChange = jest.fn(); + + titleInput.dispatchEvent(new Event('keyup')); + + expect(wikis.handleWikiTitleChange).toHaveBeenCalled(); + }); + + it('sets the commit message when title changes', () => { + titleInput.value = 'My title'; + messageInput.value = ''; + + titleInput.dispatchEvent(new Event('keyup')); + + expect(messageInput.value).toEqual('Create My title'); + }); + + it('replaces hyphens with spaces', () => { + titleInput.value = 'my-hyphenated-title'; + titleInput.dispatchEvent(new Event('keyup')); + + expect(messageInput.value).toEqual('Create my hyphenated title'); + }); + }); + + describe('when the wiki page is being updated', () => { + const formHtmlFixture = editFormHtmlFixture({ newPage: false }); + + beforeEach(() => { + setHTMLFixture(formHtmlFixture); + + titleInput = document.getElementById('wiki_title'); + messageInput = document.getElementById('wiki_message'); + wikis = new Wikis(); + }); + + it('sets the commit message when title changes, prefixing with "Update"', () => { + titleInput.value = 'My title'; + messageInput.value = ''; + + titleInput.dispatchEvent(new Event('keyup')); + + expect(messageInput.value).toEqual('Update My title'); + }); + }); + }); +}); diff --git a/spec/graphql/types/namespace_type_spec.rb b/spec/graphql/types/namespace_type_spec.rb index e1153832cc9..f476dd7286f 100644 --- a/spec/graphql/types/namespace_type_spec.rb +++ b/spec/graphql/types/namespace_type_spec.rb @@ -8,7 +8,7 @@ describe GitlabSchema.types['Namespace'] do it 'has the expected fields' do expected_fields = %w[ id name path full_name full_path description description_html visibility - lfs_enabled request_access_enabled projects + lfs_enabled request_access_enabled projects root_storage_statistics ] is_expected.to have_graphql_fields(*expected_fields) diff --git a/spec/graphql/types/root_storage_statistics_type_spec.rb b/spec/graphql/types/root_storage_statistics_type_spec.rb new file mode 100644 index 00000000000..8c69c13aa73 --- /dev/null +++ b/spec/graphql/types/root_storage_statistics_type_spec.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe GitlabSchema.types['RootStorageStatistics'] do + it { expect(described_class.graphql_name).to eq('RootStorageStatistics') } + + it 'has all the required fields' do + is_expected.to have_graphql_fields(:storage_size, :repository_size, :lfs_objects_size, + :build_artifacts_size, :packages_size, :wiki_size) + end + + it { is_expected.to require_graphql_authorizations(:read_statistics) } +end diff --git a/spec/helpers/ci_status_helper_spec.rb b/spec/helpers/ci_status_helper_spec.rb index bc2422aba90..4f665dc0514 100644 --- a/spec/helpers/ci_status_helper_spec.rb +++ b/spec/helpers/ci_status_helper_spec.rb @@ -53,4 +53,80 @@ describe CiStatusHelper do expect(helper.pipeline_status_cache_key(pipeline_status)).to eq("pipeline-status/123abc-success") end end + + describe "#render_status_with_link" do + subject { helper.render_status_with_link("success") } + + it "renders a passed status icon" do + is_expected.to include("<span class=\"ci-status-link ci-status-icon-success d-inline-flex") + end + + it "has 'Pipeline' as the status type in the title" do + is_expected.to include("title=\"Pipeline: passed\"") + end + + it "has the success status icon" do + is_expected.to include("ci-status-icon-success") + end + + context "when pipeline has commit path" do + subject { helper.render_status_with_link("success", "/commit-path") } + + it "links to commit" do + is_expected.to include("href=\"/commit-path\"") + end + + it "does not contain a span element" do + is_expected.not_to include("<span") + end + + it "has 'Pipeline' as the status type in the title" do + is_expected.to include("title=\"Pipeline: passed\"") + end + + it "has the correct status icon" do + is_expected.to include("ci-status-icon-success") + end + end + + context "when different type than pipeline is provided" do + subject { helper.render_status_with_link("success", type: "commit") } + + it "has the provided type in the title" do + is_expected.to include("title=\"Commit: passed\"") + end + end + + context "when tooltip_placement is provided" do + subject { helper.render_status_with_link("success", tooltip_placement: "right") } + + it "has the provided tooltip placement" do + is_expected.to include("data-placement=\"right\"") + end + end + + context "when additional CSS classes are provided" do + subject { helper.render_status_with_link("success", cssclass: "extra-class") } + + it "has appended extra class to icon classes" do + is_expected.to include("class=\"ci-status-link ci-status-icon-success d-inline-flex extra-class\"") + end + end + + context "when container is provided" do + subject { helper.render_status_with_link("success", container: "my-container") } + + it "has the provided container in data" do + is_expected.to include("data-container=\"my-container\"") + end + end + + context "when icon_size is provided" do + subject { helper.render_status_with_link("success", icon_size: 24) } + + it "has the svg class to change size" do + is_expected.to include("<svg class=\"s24\">") + end + end + end end diff --git a/spec/helpers/emails_helper_spec.rb b/spec/helpers/emails_helper_spec.rb index d25f0c6de4a..a14ae2cde4b 100644 --- a/spec/helpers/emails_helper_spec.rb +++ b/spec/helpers/emails_helper_spec.rb @@ -6,30 +6,62 @@ describe EmailsHelper do let(:merge_request) { create(:merge_request) } let(:merge_request_presenter) { merge_request.present } - context "and format is text" do - it "returns plain text" do - expect(closure_reason_text(merge_request, format: :text)).to eq("via merge request #{merge_request.to_reference} (#{merge_request_presenter.web_url})") + context 'when user can read merge request' do + let(:user) { create(:user) } + + before do + merge_request.project.add_developer(user) + self.instance_variable_set(:@recipient, user) + self.instance_variable_set(:@project, merge_request.project) + end + + context "and format is text" do + it "returns plain text" do + expect(helper.closure_reason_text(merge_request, format: :text)).to eq("via merge request #{merge_request.to_reference} (#{merge_request_presenter.web_url})") + end end - end - context "and format is HTML" do - it "returns HTML" do - expect(closure_reason_text(merge_request, format: :html)).to eq("via merge request #{link_to(merge_request.to_reference, merge_request_presenter.web_url)}") + context "and format is HTML" do + it "returns HTML" do + expect(helper.closure_reason_text(merge_request, format: :html)).to eq("via merge request #{link_to(merge_request.to_reference, merge_request_presenter.web_url)}") + end + end + + context "and format is unknown" do + it "returns plain text" do + expect(helper.closure_reason_text(merge_request, format: :text)).to eq("via merge request #{merge_request.to_reference} (#{merge_request_presenter.web_url})") + end end end - context "and format is unknown" do - it "returns plain text" do - expect(closure_reason_text(merge_request, format: :text)).to eq("via merge request #{merge_request.to_reference} (#{merge_request_presenter.web_url})") + context 'when user cannot read merge request' do + it "does not have link to merge request" do + expect(helper.closure_reason_text(merge_request)).to be_empty end end end context 'when given a String' do + let(:user) { create(:user) } + let(:project) { create(:project) } let(:closed_via) { "5a0eb6fd7e0f133044378c662fcbbc0d0c16dbfa" } - it "returns plain text" do - expect(closure_reason_text(closed_via)).to eq("via #{closed_via}") + context 'when user can read commits' do + before do + project.add_developer(user) + self.instance_variable_set(:@recipient, user) + self.instance_variable_set(:@project, project) + end + + it "returns plain text" do + expect(closure_reason_text(closed_via)).to eq("via #{closed_via}") + end + end + + context 'when user cannot read commits' do + it "returns plain text" do + expect(closure_reason_text(closed_via)).to be_empty + end end end diff --git a/spec/helpers/groups_helper_spec.rb b/spec/helpers/groups_helper_spec.rb index 037b16c90ed..98719697cea 100644 --- a/spec/helpers/groups_helper_spec.rb +++ b/spec/helpers/groups_helper_spec.rb @@ -262,4 +262,44 @@ describe GroupsHelper do expect(parent_group_options(group2)).to eq([{ id: group.id, text: group.human_name }].to_json) end end + + describe '#can_disable_group_emails?' do + let(:current_user) { create(:user) } + let(:group) { create(:group, name: 'group') } + let(:subgroup) { create(:group, name: 'subgroup', parent: group) } + + before do + allow(helper).to receive(:current_user) { current_user } + end + + it 'returns true for the group owner' do + allow(helper).to receive(:can?).with(current_user, :set_emails_disabled, group) { true } + + expect(helper.can_disable_group_emails?(group)).to be_truthy + end + + it 'returns false for anyone else' do + allow(helper).to receive(:can?).with(current_user, :set_emails_disabled, group) { false } + + expect(helper.can_disable_group_emails?(group)).to be_falsey + end + + context 'when subgroups' do + before do + allow(helper).to receive(:can?).with(current_user, :set_emails_disabled, subgroup) { true } + end + + it 'returns false if parent group is disabling emails' do + allow(group).to receive(:emails_disabled?).and_return(true) + + expect(helper.can_disable_group_emails?(subgroup)).to be_falsey + end + + it 'returns true if parent group is not disabling emails' do + allow(group).to receive(:emails_disabled?).and_return(false) + + expect(helper.can_disable_group_emails?(subgroup)).to be_truthy + end + end + end end diff --git a/spec/helpers/markup_helper_spec.rb b/spec/helpers/markup_helper_spec.rb index f6e1720e113..1757ec8fa4d 100644 --- a/spec/helpers/markup_helper_spec.rb +++ b/spec/helpers/markup_helper_spec.rb @@ -65,6 +65,9 @@ describe MarkupHelper do describe 'inside a group' do before do + # Ensure the generated reference links aren't redacted + group.add_maintainer(user) + helper.instance_variable_set(:@group, group) helper.instance_variable_set(:@project, nil) end @@ -78,6 +81,9 @@ describe MarkupHelper do let(:project_in_group) { create(:project, group: group) } before do + # Ensure the generated reference links aren't redacted + project_in_group.add_maintainer(user) + helper.instance_variable_set(:@group, group) helper.instance_variable_set(:@project, project_in_group) end diff --git a/spec/helpers/notifications_helper_spec.rb b/spec/helpers/notifications_helper_spec.rb index 9ecaabc04ed..5717b15d656 100644 --- a/spec/helpers/notifications_helper_spec.rb +++ b/spec/helpers/notifications_helper_spec.rb @@ -3,6 +3,7 @@ require 'spec_helper' describe NotificationsHelper do describe 'notification_icon' do it { expect(notification_icon(:disabled)).to match('class="fa fa-microphone-slash fa-fw"') } + it { expect(notification_icon(:owner_disabled)).to match('class="fa fa-microphone-slash fa-fw"') } it { expect(notification_icon(:participating)).to match('class="fa fa-volume-up fa-fw"') } it { expect(notification_icon(:mention)).to match('class="fa fa-at fa-fw"') } it { expect(notification_icon(:global)).to match('class="fa fa-globe fa-fw"') } @@ -19,4 +20,14 @@ describe NotificationsHelper do it { expect(notification_event_name(:success_pipeline)).to match('Successful pipeline') } it { expect(notification_event_name(:failed_pipeline)).to match('Failed pipeline') } end + + describe '#notification_icon_level' do + let(:user) { create(:user) } + let(:global_setting) { user.global_notification_setting } + let(:notification_setting) { create(:notification_setting, level: :watch) } + + it { expect(notification_icon_level(notification_setting, true)).to eq 'owner_disabled' } + it { expect(notification_icon_level(notification_setting)).to eq 'watch' } + it { expect(notification_icon_level(global_setting)).to eq 'participating' } + end end diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb index 3716879c458..a70bfc2adc7 100644 --- a/spec/helpers/projects_helper_spec.rb +++ b/spec/helpers/projects_helper_spec.rb @@ -107,6 +107,30 @@ describe ProjectsHelper do end end + describe '#can_disable_emails?' do + let(:project) { create(:project) } + let(:user) { create(:project_member, :maintainer, user: create(:user), project: project).user } + + it 'returns true for the project owner' do + allow(helper).to receive(:can?).with(project.owner, :set_emails_disabled, project) { true } + + expect(helper.can_disable_emails?(project, project.owner)).to be_truthy + end + + it 'returns false for anyone else' do + allow(helper).to receive(:can?).with(user, :set_emails_disabled, project) { false } + + expect(helper.can_disable_emails?(project, user)).to be_falsey + end + + it 'returns false if group emails disabled' do + project = create(:project, group: create(:group)) + allow(project.group).to receive(:emails_disabled?).and_return(true) + + expect(helper.can_disable_emails?(project, project.owner)).to be_falsey + end + end + describe "readme_cache_key" do let(:project) { create(:project, :repository) } @@ -477,6 +501,7 @@ describe ProjectsHelper do it 'returns the command to push to create project over SSH' do allow(Gitlab::CurrentSettings.current_application_settings).to receive(:enabled_git_access_protocol) { 'ssh' } + allow(Gitlab.config.gitlab_shell).to receive(:ssh_path_prefix).and_return('git@localhost:') expect(helper.push_to_create_project_command(user)).to eq('git push --set-upstream git@localhost:john/$(git rev-parse --show-toplevel | xargs basename).git $(git rev-parse --abbrev-ref HEAD)') end diff --git a/spec/helpers/search_helper_spec.rb b/spec/helpers/search_helper_spec.rb index c69493b579f..2ab72679ee7 100644 --- a/spec/helpers/search_helper_spec.rb +++ b/spec/helpers/search_helper_spec.rb @@ -177,4 +177,48 @@ describe SearchHelper do end end end + + describe 'search_filter_link' do + it 'renders a search filter link for the current scope' do + @scope = 'projects' + @search_results = double + + expect(@search_results).to receive(:formatted_count).with('projects').and_return('23') + + link = search_filter_link('projects', 'Projects') + + expect(link).to have_css('li.active') + expect(link).to have_link('Projects', href: search_path(scope: 'projects')) + expect(link).to have_css('span.badge.badge-pill:not(.js-search-count):not(.hidden):not([data-url])', text: '23') + end + + it 'renders a search filter link for another scope' do + link = search_filter_link('projects', 'Projects') + count_path = search_count_path(scope: 'projects') + + expect(link).to have_css('li:not([class="active"])') + expect(link).to have_link('Projects', href: search_path(scope: 'projects')) + expect(link).to have_css("span.badge.badge-pill.js-search-count.hidden[data-url='#{count_path}']", text: '') + end + + it 'merges in the current search params and given params' do + expect(self).to receive(:params).and_return( + ActionController::Parameters.new( + search: 'hello', + scope: 'ignored', + other_param: 'ignored' + ) + ) + + link = search_filter_link('projects', 'Projects', search: { project_id: 23 }) + + expect(link).to have_link('Projects', href: search_path(scope: 'projects', search: 'hello', project_id: 23)) + end + + it 'assigns given data attributes on the list container' do + link = search_filter_link('projects', 'Projects', data: { foo: 'bar' }) + + expect(link).to have_css('li[data-foo="bar"]') + end + end end diff --git a/spec/helpers/sessions_helper_spec.rb b/spec/helpers/sessions_helper_spec.rb new file mode 100644 index 00000000000..647771ace92 --- /dev/null +++ b/spec/helpers/sessions_helper_spec.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe SessionsHelper do + describe '#unconfirmed_email?' do + it 'returns true when the flash alert contains a devise failure unconfirmed message' do + flash[:alert] = t(:unconfirmed, scope: [:devise, :failure]) + expect(helper.unconfirmed_email?).to be_truthy + end + + it 'returns false when the flash alert does not contain a devise failure unconfirmed message' do + flash[:alert] = 'something else' + expect(helper.unconfirmed_email?).to be_falsey + end + end +end diff --git a/spec/helpers/tracking_helper_spec.rb b/spec/helpers/tracking_helper_spec.rb index 71505e8ea69..b0c98be4130 100644 --- a/spec/helpers/tracking_helper_spec.rb +++ b/spec/helpers/tracking_helper_spec.rb @@ -4,8 +4,32 @@ require 'spec_helper' describe TrackingHelper do describe '#tracking_attrs' do - it 'returns an empty hash' do - expect(helper.tracking_attrs('a', 'b', 'c')).to eq({}) + using RSpec::Parameterized::TableSyntax + + let(:input) { %w(a b c) } + let(:results) do + { + no_data: {}, + with_data: { data: { track_label: 'a', track_event: 'b', track_property: 'c' } } + } + end + + where(:snowplow_enabled, :environment, :result) do + true | 'production' | :with_data + false | 'production' | :no_data + true | 'development' | :no_data + false | 'development' | :no_data + true | 'test' | :no_data + false | 'test' | :no_data + end + + with_them do + it 'returns a hash' do + stub_application_setting(snowplow_enabled: snowplow_enabled) + allow(Rails).to receive(:env).and_return(environment.inquiry) + + expect(helper.tracking_attrs(*input)).to eq(results[result]) + end end end end diff --git a/spec/initializers/action_mailer_hooks_spec.rb b/spec/initializers/action_mailer_hooks_spec.rb new file mode 100644 index 00000000000..3826ed9b00a --- /dev/null +++ b/spec/initializers/action_mailer_hooks_spec.rb @@ -0,0 +1,46 @@ +require 'spec_helper' + +describe 'ActionMailer hooks' do + describe 'smime signature interceptor' do + before do + class_spy(ActionMailer::Base).as_stubbed_const + end + + it 'is disabled by default' do + load Rails.root.join('config/initializers/action_mailer_hooks.rb') + + expect(ActionMailer::Base).not_to( + have_received(:register_interceptor).with(Gitlab::Email::Hook::SmimeSignatureInterceptor)) + end + + describe 'interceptor testbed' do + where(:email_enabled, :email_smime_enabled, :smime_interceptor_enabled) do + [ + [false, false, false], + [false, true, false], + [true, false, false], + [true, true, true] + ] + end + + with_them do + before do + stub_config_setting(email_enabled: email_enabled) + stub_config_setting(email_smime: { enabled: email_smime_enabled }) + end + + it 'is enabled depending on settings' do + load Rails.root.join('config/initializers/action_mailer_hooks.rb') + + if smime_interceptor_enabled + expect(ActionMailer::Base).to( + have_received(:register_interceptor).with(Gitlab::Email::Hook::SmimeSignatureInterceptor)) + else + expect(ActionMailer::Base).not_to( + have_received(:register_interceptor).with(Gitlab::Email::Hook::SmimeSignatureInterceptor)) + end + end + end + end + end +end diff --git a/spec/initializers/rest-client-hostname_override_spec.rb b/spec/initializers/rest-client-hostname_override_spec.rb new file mode 100644 index 00000000000..3707e001d41 --- /dev/null +++ b/spec/initializers/rest-client-hostname_override_spec.rb @@ -0,0 +1,147 @@ +require 'spec_helper' + +describe 'rest-client dns rebinding protection' do + include StubRequests + + context 'when local requests are not allowed' do + it 'allows an external request with http' do + request_stub = stub_full_request('http://example.com', ip_address: '93.184.216.34') + + RestClient.get('http://example.com/') + + expect(request_stub).to have_been_requested + end + + it 'allows an external request with https' do + request_stub = stub_full_request('https://example.com', ip_address: '93.184.216.34') + + RestClient.get('https://example.com/') + + expect(request_stub).to have_been_requested + end + + it 'raises error when it is a request that resolves to a local address' do + stub_full_request('https://example.com', ip_address: '172.16.0.0') + + expect { RestClient.get('https://example.com') } + .to raise_error(ArgumentError, + "URL 'https://example.com' is blocked: Requests to the local network are not allowed") + end + + it 'raises error when it is a request that resolves to a localhost address' do + stub_full_request('https://example.com', ip_address: '127.0.0.1') + + expect { RestClient.get('https://example.com') } + .to raise_error(ArgumentError, + "URL 'https://example.com' is blocked: Requests to localhost are not allowed") + end + + it 'raises error when it is a request to local address' do + expect { RestClient.get('http://172.16.0.0') } + .to raise_error(ArgumentError, + "URL 'http://172.16.0.0' is blocked: Requests to the local network are not allowed") + end + + it 'raises error when it is a request to localhost address' do + expect { RestClient.get('http://127.0.0.1') } + .to raise_error(ArgumentError, + "URL 'http://127.0.0.1' is blocked: Requests to localhost are not allowed") + end + end + + context 'when port different from URL scheme is used' do + it 'allows the request' do + request_stub = stub_full_request('https://example.com:8080', ip_address: '93.184.216.34') + + RestClient.get('https://example.com:8080/') + + expect(request_stub).to have_been_requested + end + + it 'raises error when it is a request to local address' do + expect { RestClient.get('https://172.16.0.0:8080') } + .to raise_error(ArgumentError, + "URL 'https://172.16.0.0:8080' is blocked: Requests to the local network are not allowed") + end + + it 'raises error when it is a request to localhost address' do + expect { RestClient.get('https://127.0.0.1:8080') } + .to raise_error(ArgumentError, + "URL 'https://127.0.0.1:8080' is blocked: Requests to localhost are not allowed") + end + end + + context 'when DNS rebinding protection is disabled' do + before do + stub_application_setting(dns_rebinding_protection_enabled: false) + end + + it 'allows the request' do + request_stub = stub_request(:get, 'https://example.com') + + RestClient.get('https://example.com/') + + expect(request_stub).to have_been_requested + end + end + + context 'when http(s) proxy environment variable is set' do + before do + stub_env('https_proxy' => 'https://my.proxy') + end + + it 'allows the request' do + request_stub = stub_request(:get, 'https://example.com') + + RestClient.get('https://example.com/') + + expect(request_stub).to have_been_requested + end + end + + context 'when local requests are allowed' do + before do + stub_application_setting(allow_local_requests_from_web_hooks_and_services: true) + end + + it 'allows an external request' do + request_stub = stub_full_request('https://example.com', ip_address: '93.184.216.34') + + RestClient.get('https://example.com/') + + expect(request_stub).to have_been_requested + end + + it 'allows an external request that resolves to a local address' do + request_stub = stub_full_request('https://example.com', ip_address: '172.16.0.0') + + RestClient.get('https://example.com/') + + expect(request_stub).to have_been_requested + end + + it 'allows an external request that resolves to a localhost address' do + request_stub = stub_full_request('https://example.com', ip_address: '127.0.0.1') + + RestClient.get('https://example.com/') + + expect(request_stub).to have_been_requested + end + + it 'allows a local address request' do + request_stub = stub_request(:get, 'http://172.16.0.0') + + RestClient.get('http://172.16.0.0') + + expect(request_stub).to have_been_requested + end + + it 'allows a localhost address request' do + request_stub = stub_request(:get, 'http://127.0.0.1') + + RestClient.get('http://127.0.0.1') + + expect(request_stub).to have_been_requested + end + end +end diff --git a/spec/javascripts/diffs/components/diff_file_header_spec.js b/spec/javascripts/diffs/components/diff_file_header_spec.js index d4280d3ec2c..356e7a8f1fe 100644 --- a/spec/javascripts/diffs/components/diff_file_header_spec.js +++ b/spec/javascripts/diffs/components/diff_file_header_spec.js @@ -372,7 +372,7 @@ describe('diff_file_header', () => { }); it('displays old and new path if the file was renamed', () => { - props.diffFile.viewer.name = diffViewerModes.renamed; + props.diffFile.renamed_file = true; vm = mountComponentWithStore(Component, { props, store }); diff --git a/spec/javascripts/ide/components/commit_sidebar/actions_spec.js b/spec/javascripts/ide/components/commit_sidebar/actions_spec.js index b903abe63fc..a3db3ee1b18 100644 --- a/spec/javascripts/ide/components/commit_sidebar/actions_spec.js +++ b/spec/javascripts/ide/components/commit_sidebar/actions_spec.js @@ -1,30 +1,28 @@ import Vue from 'vue'; -import store from '~/ide/stores'; -import consts from '~/ide/stores/modules/commit/constants'; +import { createStore } from '~/ide/stores'; import commitActions from '~/ide/components/commit_sidebar/actions.vue'; +import consts from '~/ide/stores/modules/commit/constants'; import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; -import { resetStore } from 'spec/ide/helpers'; -import { projectData } from 'spec/ide/mock_data'; +import { projectData, branches } from 'spec/ide/mock_data'; + +const ACTION_UPDATE_COMMIT_ACTION = 'commit/updateCommitAction'; describe('IDE commit sidebar actions', () => { + let store; let vm; - const createComponent = ({ - hasMR = false, - commitAction = consts.COMMIT_TO_NEW_BRANCH, - mergeRequestsEnabled = true, - currentBranchId = 'master', - shouldCreateMR = false, - } = {}) => { + + const createComponent = ({ hasMR = false, currentBranchId = 'master' } = {}) => { const Component = Vue.extend(commitActions); vm = createComponentWithStore(Component, store); vm.$store.state.currentBranchId = currentBranchId; vm.$store.state.currentProjectId = 'abcproject'; - vm.$store.state.commit.commitAction = commitAction; - Vue.set(vm.$store.state.projects, 'abcproject', { ...projectData }); - vm.$store.state.projects.abcproject.merge_requests_enabled = mergeRequestsEnabled; - vm.$store.state.commit.shouldCreateMR = shouldCreateMR; + + const proj = { ...projectData }; + proj.branches[currentBranchId] = branches.find(branch => branch.name === currentBranchId); + + Vue.set(vm.$store.state.projects, 'abcproject', proj); if (hasMR) { vm.$store.state.currentMergeRequestId = '1'; @@ -33,13 +31,19 @@ describe('IDE commit sidebar actions', () => { ] = { foo: 'bar' }; } - return vm.$mount(); + vm.$mount(); + + return vm; }; + beforeEach(() => { + store = createStore(); + spyOn(store, 'dispatch'); + }); + afterEach(() => { vm.$destroy(); - - resetStore(vm.$store); + vm = null; }); it('renders 2 groups', () => { @@ -73,4 +77,152 @@ describe('IDE commit sidebar actions', () => { expect(vm.commitToCurrentBranchText).not.toContain(injectedSrc); }); }); + + describe('updateSelectedCommitAction', () => { + it('does not return anything if currentBranch does not exist', () => { + createComponent({ currentBranchId: null }); + + expect(vm.$store.dispatch).not.toHaveBeenCalled(); + }); + + it('calls again after staged changes', done => { + createComponent({ currentBranchId: null }); + + vm.$store.state.currentBranchId = 'master'; + vm.$store.state.changedFiles.push({}); + vm.$store.state.stagedFiles.push({}); + + vm.$nextTick() + .then(() => { + expect(vm.$store.dispatch).toHaveBeenCalledWith( + ACTION_UPDATE_COMMIT_ACTION, + jasmine.anything(), + ); + }) + .then(done) + .catch(done.fail); + }); + + describe('default branch', () => { + it('dispatches correct action for default branch', () => { + createComponent({ + currentBranchId: 'master', + }); + + expect(vm.$store.dispatch).toHaveBeenCalledTimes(1); + expect(vm.$store.dispatch).toHaveBeenCalledWith( + ACTION_UPDATE_COMMIT_ACTION, + consts.COMMIT_TO_NEW_BRANCH, + ); + }); + }); + + describe('protected branch', () => { + describe('with write access', () => { + it('dispatches correct action when MR exists', () => { + createComponent({ + hasMR: true, + currentBranchId: 'protected/access', + }); + + expect(vm.$store.dispatch).toHaveBeenCalledWith( + ACTION_UPDATE_COMMIT_ACTION, + consts.COMMIT_TO_CURRENT_BRANCH, + ); + }); + + it('dispatches correct action when MR does not exists', () => { + createComponent({ + hasMR: false, + currentBranchId: 'protected/access', + }); + + expect(vm.$store.dispatch).toHaveBeenCalledWith( + ACTION_UPDATE_COMMIT_ACTION, + consts.COMMIT_TO_CURRENT_BRANCH, + ); + }); + }); + + describe('without write access', () => { + it('dispatches correct action when MR exists', () => { + createComponent({ + hasMR: true, + currentBranchId: 'protected/no-access', + }); + + expect(vm.$store.dispatch).toHaveBeenCalledWith( + ACTION_UPDATE_COMMIT_ACTION, + consts.COMMIT_TO_NEW_BRANCH, + ); + }); + + it('dispatches correct action when MR does not exists', () => { + createComponent({ + hasMR: false, + currentBranchId: 'protected/no-access', + }); + + expect(vm.$store.dispatch).toHaveBeenCalledWith( + ACTION_UPDATE_COMMIT_ACTION, + consts.COMMIT_TO_NEW_BRANCH, + ); + }); + }); + }); + + describe('regular branch', () => { + describe('with write access', () => { + it('dispatches correct action when MR exists', () => { + createComponent({ + hasMR: true, + currentBranchId: 'regular', + }); + + expect(vm.$store.dispatch).toHaveBeenCalledWith( + ACTION_UPDATE_COMMIT_ACTION, + consts.COMMIT_TO_CURRENT_BRANCH, + ); + }); + + it('dispatches correct action when MR does not exists', () => { + createComponent({ + hasMR: false, + currentBranchId: 'regular', + }); + + expect(vm.$store.dispatch).toHaveBeenCalledWith( + ACTION_UPDATE_COMMIT_ACTION, + consts.COMMIT_TO_CURRENT_BRANCH, + ); + }); + }); + + describe('without write access', () => { + it('dispatches correct action when MR exists', () => { + createComponent({ + hasMR: true, + currentBranchId: 'regular/no-access', + }); + + expect(vm.$store.dispatch).toHaveBeenCalledWith( + ACTION_UPDATE_COMMIT_ACTION, + consts.COMMIT_TO_NEW_BRANCH, + ); + }); + + it('dispatches correct action when MR does not exists', () => { + createComponent({ + hasMR: false, + currentBranchId: 'regular/no-access', + }); + + expect(vm.$store.dispatch).toHaveBeenCalledWith( + ACTION_UPDATE_COMMIT_ACTION, + consts.COMMIT_TO_NEW_BRANCH, + ); + }); + }); + }); + }); }); diff --git a/spec/javascripts/ide/components/commit_sidebar/new_merge_request_option_spec.js b/spec/javascripts/ide/components/commit_sidebar/new_merge_request_option_spec.js index 7017bfcd6a6..5f2db695241 100644 --- a/spec/javascripts/ide/components/commit_sidebar/new_merge_request_option_spec.js +++ b/spec/javascripts/ide/components/commit_sidebar/new_merge_request_option_spec.js @@ -1,33 +1,36 @@ import Vue from 'vue'; import store from '~/ide/stores'; -import consts from '~/ide/stores/modules/commit/constants'; import NewMergeRequestOption from '~/ide/components/commit_sidebar/new_merge_request_option.vue'; import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; -import { projectData } from 'spec/ide/mock_data'; +import { projectData, branches } from 'spec/ide/mock_data'; import { resetStore } from 'spec/ide/helpers'; +import consts from '../../../../../app/assets/javascripts/ide/stores/modules/commit/constants'; describe('create new MR checkbox', () => { let vm; - const createComponent = ({ - hasMR = false, - commitAction = consts.COMMIT_TO_NEW_BRANCH, - currentBranchId = 'master', - } = {}) => { + const setMR = () => { + vm.$store.state.currentMergeRequestId = '1'; + vm.$store.state.projects[store.state.currentProjectId].mergeRequests[ + store.state.currentMergeRequestId + ] = { foo: 'bar' }; + }; + + const createComponent = ({ currentBranchId = 'master', createNewBranch = false } = {}) => { const Component = Vue.extend(NewMergeRequestOption); vm = createComponentWithStore(Component, store); + vm.$store.state.commit.commitAction = createNewBranch + ? consts.COMMIT_TO_NEW_BRANCH + : consts.COMMIT_TO_CURRENT_BRANCH; + vm.$store.state.currentBranchId = currentBranchId; vm.$store.state.currentProjectId = 'abcproject'; - vm.$store.state.commit.commitAction = commitAction; - Vue.set(vm.$store.state.projects, 'abcproject', { ...projectData }); - if (hasMR) { - vm.$store.state.currentMergeRequestId = '1'; - vm.$store.state.projects[store.state.currentProjectId].mergeRequests[ - store.state.currentMergeRequestId - ] = { foo: 'bar' }; - } + const proj = JSON.parse(JSON.stringify(projectData)); + proj.branches[currentBranchId] = branches.find(branch => branch.name === currentBranchId); + + Vue.set(vm.$store.state.projects, 'abcproject', proj); return vm.$mount(); }; @@ -38,30 +41,131 @@ describe('create new MR checkbox', () => { resetStore(vm.$store); }); - it('is hidden when an MR already exists and committing to current branch', () => { - createComponent({ - hasMR: true, - commitAction: consts.COMMIT_TO_CURRENT_BRANCH, - currentBranchId: 'feature', + describe('for default branch', () => { + describe('is rendered when pushing to a new branch', () => { + beforeEach(() => { + createComponent({ + currentBranchId: 'master', + createNewBranch: true, + }); + }); + + it('has NO new MR', () => { + expect(vm.$el.textContent).not.toBe(''); + }); + + it('has new MR', done => { + setMR(); + + vm.$nextTick() + .then(() => { + expect(vm.$el.textContent).not.toBe(''); + }) + .then(done) + .catch(done.fail); + }); }); - expect(vm.$el.textContent).toBe(''); + describe('is NOT rendered when pushing to the same branch', () => { + beforeEach(() => { + createComponent({ + currentBranchId: 'master', + createNewBranch: false, + }); + }); + + it('has NO new MR', () => { + expect(vm.$el.textContent).toBe(''); + }); + + it('has new MR', done => { + setMR(); + + vm.$nextTick() + .then(() => { + expect(vm.$el.textContent).toBe(''); + }) + .then(done) + .catch(done.fail); + }); + }); }); - it('does not hide checkbox if MR does not exist', () => { - createComponent({ hasMR: false }); + describe('for protected branch', () => { + describe('when user does not have the write access', () => { + beforeEach(() => { + createComponent({ + currentBranchId: 'protected/no-access', + }); + }); + + it('is rendered if MR does not exists', () => { + expect(vm.$el.textContent).not.toBe(''); + }); + + it('is rendered if MR exists', done => { + setMR(); - expect(vm.$el.querySelector('input[type="checkbox"]').hidden).toBe(false); + vm.$nextTick() + .then(() => { + expect(vm.$el.textContent).not.toBe(''); + }) + .then(done) + .catch(done.fail); + }); + }); + + describe('when user has the write access', () => { + beforeEach(() => { + createComponent({ + currentBranchId: 'protected/access', + }); + }); + + it('is rendered if MR does not exist', () => { + expect(vm.$el.textContent).not.toBe(''); + }); + + it('is hidden if MR exists', done => { + setMR(); + + vm.$nextTick() + .then(() => { + expect(vm.$el.textContent).toBe(''); + }) + .then(done) + .catch(done.fail); + }); + }); }); - it('does not hide checkbox when creating a new branch', () => { - createComponent({ commitAction: consts.COMMIT_TO_NEW_BRANCH }); + describe('for regular branch', () => { + beforeEach(() => { + createComponent({ + currentBranchId: 'regular', + }); + }); - expect(vm.$el.querySelector('input[type="checkbox"]').hidden).toBe(false); + it('is rendered if no MR exists', () => { + expect(vm.$el.textContent).not.toBe(''); + }); + + it('is hidden if MR exists', done => { + setMR(); + + vm.$nextTick() + .then(() => { + expect(vm.$el.textContent).toBe(''); + }) + .then(done) + .catch(done.fail); + }); }); it('dispatches toggleShouldCreateMR when clicking checkbox', () => { - createComponent(); + createComponent({ + currentBranchId: 'regular', + }); const el = vm.$el.querySelector('input[type="checkbox"]'); spyOn(vm.$store, 'dispatch'); el.dispatchEvent(new Event('change')); diff --git a/spec/javascripts/ide/mock_data.js b/spec/javascripts/ide/mock_data.js index 570a396c5e3..c02c7e5d45e 100644 --- a/spec/javascripts/ide/mock_data.js +++ b/spec/javascripts/ide/mock_data.js @@ -176,23 +176,51 @@ export const branches = [ committed_date: '2018-08-01T00:20:05Z', }, can_push: true, + protected: true, + default: true, }, { id: 2, - name: 'feature/lorem-ipsum', + name: 'protected/no-access', commit: { message: 'Update some stuff', committed_date: '2018-08-02T00:00:05Z', }, - can_push: true, + can_push: false, + protected: true, + default: false, }, { id: 3, - name: 'feature/dolar-amit', + name: 'protected/access', + commit: { + message: 'Update some stuff', + committed_date: '2018-08-02T00:00:05Z', + }, + can_push: true, + protected: true, + default: false, + }, + { + id: 4, + name: 'regular', commit: { message: 'Update some more stuff', committed_date: '2018-06-30T00:20:05Z', }, can_push: true, + protected: false, + default: false, + }, + { + id: 5, + name: 'regular/no-access', + commit: { + message: 'Update some more stuff', + committed_date: '2018-06-30T00:20:05Z', + }, + can_push: false, + protected: false, + default: false, }, ]; diff --git a/spec/javascripts/ide/stores/getters_spec.js b/spec/javascripts/ide/stores/getters_spec.js index 735bbd47f55..73a8d993a13 100644 --- a/spec/javascripts/ide/stores/getters_spec.js +++ b/spec/javascripts/ide/stores/getters_spec.js @@ -221,4 +221,36 @@ describe('IDE store getters', () => { }); }); }); + + describe('canPushToBranch', () => { + it('returns false when no currentBranch exists', () => { + const localGetters = { + currentProject: undefined, + }; + + expect(getters.canPushToBranch({}, localGetters)).toBeFalsy(); + }); + + it('returns true when can_push to currentBranch', () => { + const localGetters = { + currentProject: { + default_branch: 'master', + }, + currentBranch: { can_push: true }, + }; + + expect(getters.canPushToBranch({}, localGetters)).toBeTruthy(); + }); + + it('returns false when !can_push to currentBranch', () => { + const localGetters = { + currentProject: { + default_branch: 'master', + }, + currentBranch: { can_push: false }, + }; + + expect(getters.canPushToBranch({}, localGetters)).toBeFalsy(); + }); + }); }); diff --git a/spec/javascripts/ide/stores/modules/commit/actions_spec.js b/spec/javascripts/ide/stores/modules/commit/actions_spec.js index 14d861f21d2..091b454c0d2 100644 --- a/spec/javascripts/ide/stores/modules/commit/actions_spec.js +++ b/spec/javascripts/ide/stores/modules/commit/actions_spec.js @@ -57,6 +57,44 @@ describe('IDE commit module actions', () => { .then(done) .catch(done.fail); }); + + it('sets shouldCreateMR to true if "Create new MR" option is visible', done => { + store.state.shouldHideNewMrOption = false; + + testAction( + actions.updateCommitAction, + {}, + store.state, + [ + { + type: mutationTypes.UPDATE_COMMIT_ACTION, + payload: { commitAction: jasmine.anything() }, + }, + { type: mutationTypes.TOGGLE_SHOULD_CREATE_MR, payload: true }, + ], + [], + done, + ); + }); + + it('sets shouldCreateMR to false if "Create new MR" option is hidden', done => { + store.state.shouldHideNewMrOption = true; + + testAction( + actions.updateCommitAction, + {}, + store.state, + [ + { + type: mutationTypes.UPDATE_COMMIT_ACTION, + payload: { commitAction: jasmine.anything() }, + }, + { type: mutationTypes.TOGGLE_SHOULD_CREATE_MR, payload: false }, + ], + [], + done, + ); + }); }); describe('updateBranchName', () => { @@ -541,147 +579,10 @@ describe('IDE commit module actions', () => { actions.toggleShouldCreateMR, {}, store.state, - [ - { type: mutationTypes.TOGGLE_SHOULD_CREATE_MR }, - { type: mutationTypes.INTERACT_WITH_NEW_MR }, - ], + [{ type: mutationTypes.TOGGLE_SHOULD_CREATE_MR }], [], done, ); }); }); - - describe('setShouldCreateMR', () => { - beforeEach(() => { - store.state.projects = { - project: { - default_branch: 'master', - branches: { - master: { - name: 'master', - }, - feature: { - name: 'feature', - }, - }, - }, - }; - - store.state.currentProjectId = 'project'; - }); - - it('sets to false when the current branch already has an MR', done => { - store.state.commit.currentMergeRequestId = 1; - store.state.commit.commitAction = consts.COMMIT_TO_CURRENT_BRANCH; - store.state.currentMergeRequestId = '1'; - store.state.currentBranchId = 'feature'; - spyOn(store, 'commit').and.callThrough(); - - store - .dispatch('commit/setShouldCreateMR') - .then(() => { - expect(store.commit.calls.allArgs()[0]).toEqual( - jasmine.arrayContaining([`commit/${mutationTypes.TOGGLE_SHOULD_CREATE_MR}`, false]), - ); - done(); - }) - .catch(done.fail); - }); - - it('changes to false when current branch is the default branch and user has not interacted', done => { - store.state.commit.interactedWithNewMR = false; - store.state.currentBranchId = 'master'; - store.state.commit.commitAction = consts.COMMIT_TO_CURRENT_BRANCH; - spyOn(store, 'commit').and.callThrough(); - - store - .dispatch('commit/setShouldCreateMR') - .then(() => { - expect(store.commit.calls.allArgs()[0]).toEqual( - jasmine.arrayContaining([`commit/${mutationTypes.TOGGLE_SHOULD_CREATE_MR}`, false]), - ); - done(); - }) - .catch(done.fail); - }); - - it('changes to true when "create new branch" is selected and user has not interacted', done => { - store.state.commit.commitAction = consts.COMMIT_TO_NEW_BRANCH; - store.state.commit.interactedWithNewMR = false; - spyOn(store, 'commit').and.callThrough(); - - store - .dispatch('commit/setShouldCreateMR') - .then(() => { - expect(store.commit.calls.allArgs()[0]).toEqual( - jasmine.arrayContaining([`commit/${mutationTypes.TOGGLE_SHOULD_CREATE_MR}`, true]), - ); - done(); - }) - .catch(done.fail); - }); - - it('does not change anything if user has interacted and comitting to new branch', done => { - store.state.commit.commitAction = consts.COMMIT_TO_NEW_BRANCH; - store.state.commit.interactedWithNewMR = true; - spyOn(store, 'commit').and.callThrough(); - - store - .dispatch('commit/setShouldCreateMR') - .then(() => { - expect(store.commit).not.toHaveBeenCalled(); - done(); - }) - .catch(done.fail); - }); - - it('does not change anything if user has interacted and comitting to branch without MR', done => { - store.state.commit.commitAction = consts.COMMIT_TO_CURRENT_BRANCH; - store.state.commit.currentMergeRequestId = null; - store.state.commit.interactedWithNewMR = true; - spyOn(store, 'commit').and.callThrough(); - - store - .dispatch('commit/setShouldCreateMR') - .then(() => { - expect(store.commit).not.toHaveBeenCalled(); - done(); - }) - .catch(done.fail); - }); - - it('still changes to false if hiding the checkbox', done => { - store.state.currentBranchId = 'feature'; - store.state.commit.commitAction = consts.COMMIT_TO_CURRENT_BRANCH; - store.state.currentMergeRequestId = '1'; - store.state.commit.interactedWithNewMR = true; - spyOn(store, 'commit').and.callThrough(); - - store - .dispatch('commit/setShouldCreateMR') - .then(() => { - expect(store.commit.calls.allArgs()[0]).toEqual( - jasmine.arrayContaining([`commit/${mutationTypes.TOGGLE_SHOULD_CREATE_MR}`, false]), - ); - done(); - }) - .catch(done.fail); - }); - - it('does not change to false when on master and user has interacted even if MR exists', done => { - store.state.currentBranchId = 'master'; - store.state.commit.commitAction = consts.COMMIT_TO_CURRENT_BRANCH; - store.state.currentMergeRequestId = '1'; - store.state.commit.interactedWithNewMR = true; - spyOn(store, 'commit').and.callThrough(); - - store - .dispatch('commit/setShouldCreateMR') - .then(() => { - expect(store.commit).not.toHaveBeenCalled(); - done(); - }) - .catch(done.fail); - }); - }); }); diff --git a/spec/javascripts/ide/stores/modules/commit/getters_spec.js b/spec/javascripts/ide/stores/modules/commit/getters_spec.js index 6e71a790deb..07445c22917 100644 --- a/spec/javascripts/ide/stores/modules/commit/getters_spec.js +++ b/spec/javascripts/ide/stores/modules/commit/getters_spec.js @@ -1,6 +1,6 @@ import commitState from '~/ide/stores/modules/commit/state'; -import consts from '~/ide/stores/modules/commit/constants'; import * as getters from '~/ide/stores/modules/commit/getters'; +import consts from '~/ide/stores/modules/commit/constants'; describe('IDE commit module getters', () => { let state; @@ -55,15 +55,15 @@ describe('IDE commit module getters', () => { }); }); - it('defualts to currentBranchId', () => { - expect(getters.branchName(state, null, rootState)).toBe('master'); + it('defaults to currentBranchId when not committing to a new branch', () => { + localGetters.isCreatingNewBranch = false; + + expect(getters.branchName(state, localGetters, rootState)).toBe('master'); }); - describe('COMMIT_TO_NEW_BRANCH', () => { + describe('commit to a new branch', () => { beforeEach(() => { - Object.assign(state, { - commitAction: consts.COMMIT_TO_NEW_BRANCH, - }); + localGetters.isCreatingNewBranch = true; }); it('uses newBranchName when not empty', () => { @@ -144,4 +144,152 @@ describe('IDE commit module getters', () => { }); }); }); + + describe('isCreatingNewBranch', () => { + it('returns false if NOT creating a new branch', () => { + state.commitAction = consts.COMMIT_TO_CURRENT_BRANCH; + + expect(getters.isCreatingNewBranch(state)).toBeFalsy(); + }); + + it('returns true if creating a new branch', () => { + state.commitAction = consts.COMMIT_TO_NEW_BRANCH; + + expect(getters.isCreatingNewBranch(state)).toBeTruthy(); + }); + }); + + describe('shouldHideNewMrOption', () => { + let localGetters = {}; + let rootGetters = {}; + + beforeEach(() => { + localGetters = { + isCreatingNewBranch: null, + }; + rootGetters = { + isOnDefaultBranch: null, + hasMergeRequest: null, + canPushToBranch: null, + }; + }); + + describe('NO existing MR for the branch', () => { + beforeEach(() => { + rootGetters.hasMergeRequest = false; + }); + + it('should never hide "New MR" option', () => { + expect(getters.shouldHideNewMrOption(state, localGetters, null, rootGetters)).toBeFalsy(); + }); + }); + + describe('existing MR for the branch', () => { + beforeEach(() => { + rootGetters.hasMergeRequest = true; + }); + + it('should NOT hide "New MR" option if user can NOT push to the current branch', () => { + rootGetters.canPushToBranch = false; + + expect(getters.shouldHideNewMrOption(state, localGetters, null, rootGetters)).toBeFalsy(); + }); + + it('should hide "New MR" option if user can push to the current branch', () => { + rootGetters.canPushToBranch = true; + + expect(getters.shouldHideNewMrOption(state, localGetters, null, rootGetters)).toBeTruthy(); + }); + }); + + describe('user can NOT push the branch', () => { + beforeEach(() => { + rootGetters.canPushToBranch = false; + }); + + it('should never hide "New MR" option', () => { + expect(getters.shouldHideNewMrOption(state, localGetters, null, rootGetters)).toBeFalsy(); + }); + }); + + describe('user can push to the branch', () => { + beforeEach(() => { + rootGetters.canPushToBranch = true; + }); + + it('should NOT hide "New MR" option if there is NO existing MR for the current branch', () => { + rootGetters.hasMergeRequest = false; + + expect(getters.shouldHideNewMrOption(state, localGetters, null, rootGetters)).toBeFalsy(); + }); + + it('should hide "New MR" option if there is existing MR for the current branch', () => { + rootGetters.hasMergeRequest = true; + + expect(getters.shouldHideNewMrOption(state, localGetters, null, rootGetters)).toBeTruthy(); + }); + }); + + describe('default branch', () => { + beforeEach(() => { + rootGetters.isOnDefaultBranch = true; + }); + + describe('committing to the same branch', () => { + beforeEach(() => { + localGetters.isCreatingNewBranch = false; + rootGetters.canPushToBranch = true; + }); + + it('should hide "New MR" when there is an existing MR', () => { + rootGetters.hasMergeRequest = true; + + expect( + getters.shouldHideNewMrOption(state, localGetters, null, rootGetters), + ).toBeTruthy(); + }); + + it('should hide "New MR" when there is no existing MR', () => { + rootGetters.hasMergeRequest = false; + + expect( + getters.shouldHideNewMrOption(state, localGetters, null, rootGetters), + ).toBeTruthy(); + }); + }); + + describe('creating a new branch', () => { + beforeEach(() => { + localGetters.isCreatingNewBranch = true; + }); + + it('should NOT hide "New MR" option no matter existence of an MR or write access', () => { + rootGetters.hasMergeRequest = false; + rootGetters.canPushToBranch = true; + + expect(getters.shouldHideNewMrOption(state, localGetters, null, rootGetters)).toBeFalsy(); + + rootGetters.hasMergeRequest = true; + rootGetters.canPushToBranch = true; + + expect(getters.shouldHideNewMrOption(state, localGetters, null, rootGetters)).toBeFalsy(); + + rootGetters.hasMergeRequest = false; + rootGetters.canPushToBranch = false; + + expect(getters.shouldHideNewMrOption(state, localGetters, null, rootGetters)).toBeFalsy(); + }); + }); + }); + + it('should never hide "New MR" option when creating a new branch', () => { + localGetters.isCreatingNewBranch = true; + + rootGetters.isOnDefaultBranch = false; + rootGetters.hasMergeRequest = true; + rootGetters.canPushToBranch = true; + + expect(getters.shouldHideNewMrOption(state, localGetters, null, rootGetters)).toBeFalsy(); + }); + }); }); diff --git a/spec/javascripts/ide/stores/utils_spec.js b/spec/javascripts/ide/stores/utils_spec.js index bceb3a8db91..0fc9519a6bf 100644 --- a/spec/javascripts/ide/stores/utils_spec.js +++ b/spec/javascripts/ide/stores/utils_spec.js @@ -261,6 +261,41 @@ describe('Multi-file store utils', () => { }, ]); }); + + it('filters out folders from the list', () => { + const files = [ + { + path: 'a', + type: 'blob', + deleted: true, + }, + { + path: 'c', + type: 'tree', + deleted: true, + }, + { + path: 'c/d', + type: 'blob', + deleted: true, + }, + ]; + + const flattendFiles = utils.getCommitFiles(files); + + expect(flattendFiles).toEqual([ + { + path: 'a', + type: 'blob', + deleted: true, + }, + { + path: 'c/d', + type: 'blob', + deleted: true, + }, + ]); + }); }); describe('mergeTrees', () => { diff --git a/spec/javascripts/issue_show/components/description_spec.js b/spec/javascripts/issue_show/components/description_spec.js index 7e00fbf2745..e10426a9858 100644 --- a/spec/javascripts/issue_show/components/description_spec.js +++ b/spec/javascripts/issue_show/components/description_spec.js @@ -1,5 +1,6 @@ import $ from 'jquery'; import Vue from 'vue'; +import '~/behaviors/markdown/render_gfm'; import Description from '~/issue_show/components/description.vue'; import mountComponent from 'spec/helpers/vue_mount_component_helper'; @@ -91,6 +92,7 @@ describe('Description component', () => { let TaskList; beforeEach(() => { + vm.$destroy(); vm = mountComponent( DescriptionComponent, Object.assign({}, props, { diff --git a/spec/javascripts/lib/utils/common_utils_spec.js b/spec/javascripts/lib/utils/common_utils_spec.js index 296ee85089f..85949f2ae86 100644 --- a/spec/javascripts/lib/utils/common_utils_spec.js +++ b/spec/javascripts/lib/utils/common_utils_spec.js @@ -895,6 +895,45 @@ describe('common_utils', () => { }); }); + describe('searchBy', () => { + const searchSpace = { + iid: 1, + reference: '&1', + title: 'Error omnis quos consequatur ullam a vitae sed omnis libero cupiditate.', + url: '/groups/gitlab-org/-/epics/1', + }; + + it('returns null when `query` or `searchSpace` params are empty/undefined', () => { + expect(commonUtils.searchBy('omnis', null)).toBeNull(); + expect(commonUtils.searchBy('', searchSpace)).toBeNull(); + expect(commonUtils.searchBy()).toBeNull(); + }); + + it('returns object with matching props based on `query` & `searchSpace` params', () => { + // String `omnis` is found only in `title` prop so return just that + expect(commonUtils.searchBy('omnis', searchSpace)).toEqual( + jasmine.objectContaining({ + title: searchSpace.title, + }), + ); + + // String `1` is found in both `iid` and `reference` props so return both + expect(commonUtils.searchBy('1', searchSpace)).toEqual( + jasmine.objectContaining({ + iid: searchSpace.iid, + reference: searchSpace.reference, + }), + ); + + // String `/epics/1` is found in `url` prop so return just that + expect(commonUtils.searchBy('/epics/1', searchSpace)).toEqual( + jasmine.objectContaining({ + url: searchSpace.url, + }), + ); + }); + }); + describe('isScopedLabel', () => { it('returns true when `::` is present in title', () => { expect(commonUtils.isScopedLabel({ title: 'foo::bar' })).toBe(true); diff --git a/spec/javascripts/monitoring/charts/area_spec.js b/spec/javascripts/monitoring/charts/area_spec.js index 4541119dd2e..1e49a955815 100644 --- a/spec/javascripts/monitoring/charts/area_spec.js +++ b/spec/javascripts/monitoring/charts/area_spec.js @@ -24,7 +24,6 @@ describe('Area component', () => { store.commit(`monitoringDashboard/${types.RECEIVE_METRICS_DATA_SUCCESS}`, MonitoringMock.data); store.commit(`monitoringDashboard/${types.RECEIVE_DEPLOYMENTS_DATA_SUCCESS}`, deploymentData); - store.dispatch('monitoringDashboard/setFeatureFlags', { exportMetricsToCsvEnabled: true }); [mockGraphData] = store.state.monitoringDashboard.groups[0].metrics; areaChart = shallowMount(Area, { @@ -109,16 +108,6 @@ describe('Area component', () => { }); }); - describe('when exportMetricsToCsvEnabled is disabled', () => { - beforeEach(() => { - store.dispatch('monitoringDashboard/setFeatureFlags', { exportMetricsToCsvEnabled: false }); - }); - - it('does not render the Download CSV button', () => { - expect(areaChart.contains('glbutton-stub')).toBe(false); - }); - }); - describe('methods', () => { describe('formatTooltipText', () => { const mockDate = deploymentData[0].created_at; @@ -236,6 +225,14 @@ describe('Area component', () => { }); describe('chartOptions', () => { + describe('dataZoom', () => { + it('contains an svg object within an array to properly render icon', () => { + const dataZoomObject = [{}]; + + expect(areaChart.vm.chartOptions.dataZoom).toEqual(dataZoomObject); + }); + }); + describe('yAxis formatter', () => { let format; @@ -264,23 +261,5 @@ describe('Area component', () => { expect(areaChart.vm.yAxisLabel).toBe('CPU'); }); }); - - describe('csvText', () => { - it('converts data from json to csv', () => { - const header = `timestamp,${mockGraphData.y_label}`; - const data = mockGraphData.queries[0].result[0].values; - const firstRow = `${data[0][0]},${data[0][1]}`; - - expect(areaChart.vm.csvText).toMatch(`^${header}\r\n${firstRow}`); - }); - }); - - describe('downloadLink', () => { - it('produces a link to download metrics as csv', () => { - const link = areaChart.vm.downloadLink; - - expect(link).toContain('blob:'); - }); - }); }); }); diff --git a/spec/javascripts/monitoring/charts/time_series_spec.js b/spec/javascripts/monitoring/charts/time_series_spec.js new file mode 100644 index 00000000000..d145a64e8d0 --- /dev/null +++ b/spec/javascripts/monitoring/charts/time_series_spec.js @@ -0,0 +1,335 @@ +import { shallowMount } from '@vue/test-utils'; +import { createStore } from '~/monitoring/stores'; +import { GlLink } from '@gitlab/ui'; +import { GlAreaChart, GlLineChart, GlChartSeriesLabel } from '@gitlab/ui/dist/charts'; +import { shallowWrapperContainsSlotText } from 'spec/helpers/vue_test_utils_helper'; +import TimeSeries from '~/monitoring/components/charts/time_series.vue'; +import * as types from '~/monitoring/stores/mutation_types'; +import { TEST_HOST } from 'spec/test_constants'; +import MonitoringMock, { deploymentData, mockProjectPath } from '../mock_data'; + +describe('Time series component', () => { + const mockSha = 'mockSha'; + const mockWidgets = 'mockWidgets'; + const mockSvgPathContent = 'mockSvgPathContent'; + const projectPath = `${TEST_HOST}${mockProjectPath}`; + const commitUrl = `${projectPath}/commit/${mockSha}`; + let mockGraphData; + let makeTimeSeriesChart; + let spriteSpy; + let store; + + beforeEach(() => { + store = createStore(); + store.commit(`monitoringDashboard/${types.RECEIVE_METRICS_DATA_SUCCESS}`, MonitoringMock.data); + store.commit(`monitoringDashboard/${types.RECEIVE_DEPLOYMENTS_DATA_SUCCESS}`, deploymentData); + store.dispatch('monitoringDashboard/setFeatureFlags', { exportMetricsToCsvEnabled: true }); + [mockGraphData] = store.state.monitoringDashboard.groups[0].metrics; + + makeTimeSeriesChart = (graphData, type) => + shallowMount(TimeSeries, { + propsData: { + graphData: { ...graphData, type }, + containerWidth: 0, + deploymentData: store.state.monitoringDashboard.deploymentData, + projectPath, + }, + slots: { + default: mockWidgets, + }, + sync: false, + store, + }); + + spriteSpy = spyOnDependency(TimeSeries, 'getSvgIconPathContent').and.callFake( + () => new Promise(resolve => resolve(mockSvgPathContent)), + ); + }); + + describe('general functions', () => { + let timeSeriesChart; + + beforeEach(() => { + timeSeriesChart = makeTimeSeriesChart(mockGraphData, 'area-chart'); + }); + + it('renders chart title', () => { + expect(timeSeriesChart.find('.js-graph-title').text()).toBe(mockGraphData.title); + }); + + it('contains graph widgets from slot', () => { + expect(timeSeriesChart.find('.js-graph-widgets').text()).toBe(mockWidgets); + }); + + describe('when exportMetricsToCsvEnabled is disabled', () => { + beforeEach(() => { + store.dispatch('monitoringDashboard/setFeatureFlags', { exportMetricsToCsvEnabled: false }); + }); + + it('does not render the Download CSV button', done => { + timeSeriesChart.vm.$nextTick(() => { + expect(timeSeriesChart.contains('glbutton-stub')).toBe(false); + done(); + }); + }); + }); + + describe('methods', () => { + describe('formatTooltipText', () => { + const mockDate = deploymentData[0].created_at; + const mockCommitUrl = deploymentData[0].commitUrl; + const generateSeriesData = type => ({ + seriesData: [ + { + seriesName: timeSeriesChart.vm.chartData[0].name, + componentSubType: type, + value: [mockDate, 5.55555], + seriesIndex: 0, + }, + ], + value: mockDate, + }); + + describe('when series is of line type', () => { + beforeEach(done => { + timeSeriesChart.vm.formatTooltipText(generateSeriesData('line')); + timeSeriesChart.vm.$nextTick(done); + }); + + it('formats tooltip title', () => { + expect(timeSeriesChart.vm.tooltip.title).toBe('31 May 2017, 9:23PM'); + }); + + it('formats tooltip content', () => { + const name = 'Core Usage'; + const value = '5.556'; + const seriesLabel = timeSeriesChart.find(GlChartSeriesLabel); + + expect(seriesLabel.vm.color).toBe(''); + expect(shallowWrapperContainsSlotText(seriesLabel, 'default', name)).toBe(true); + expect(timeSeriesChart.vm.tooltip.content).toEqual([{ name, value, color: undefined }]); + expect( + shallowWrapperContainsSlotText( + timeSeriesChart.find(GlAreaChart), + 'tooltipContent', + value, + ), + ).toBe(true); + }); + }); + + describe('when series is of scatter type', () => { + beforeEach(() => { + timeSeriesChart.vm.formatTooltipText(generateSeriesData('scatter')); + }); + + it('formats tooltip title', () => { + expect(timeSeriesChart.vm.tooltip.title).toBe('31 May 2017, 9:23PM'); + }); + + it('formats tooltip sha', () => { + expect(timeSeriesChart.vm.tooltip.sha).toBe('f5bcd1d9'); + }); + + it('formats tooltip commit url', () => { + expect(timeSeriesChart.vm.tooltip.commitUrl).toBe(mockCommitUrl); + }); + }); + }); + + describe('setSvg', () => { + const mockSvgName = 'mockSvgName'; + + beforeEach(done => { + timeSeriesChart.vm.setSvg(mockSvgName); + timeSeriesChart.vm.$nextTick(done); + }); + + it('gets svg path content', () => { + expect(spriteSpy).toHaveBeenCalledWith(mockSvgName); + }); + + it('sets svg path content', () => { + timeSeriesChart.vm.$nextTick(() => { + expect(timeSeriesChart.vm.svgs[mockSvgName]).toBe(`path://${mockSvgPathContent}`); + }); + }); + }); + + describe('onResize', () => { + const mockWidth = 233; + + beforeEach(() => { + spyOn(Element.prototype, 'getBoundingClientRect').and.callFake(() => ({ + width: mockWidth, + })); + timeSeriesChart.vm.onResize(); + }); + + it('sets area chart width', () => { + expect(timeSeriesChart.vm.width).toBe(mockWidth); + }); + }); + }); + + describe('computed', () => { + describe('chartData', () => { + let chartData; + const seriesData = () => chartData[0]; + + beforeEach(() => { + ({ chartData } = timeSeriesChart.vm); + }); + + it('utilizes all data points', () => { + const { values } = mockGraphData.queries[0].result[0]; + + expect(chartData.length).toBe(1); + expect(seriesData().data.length).toBe(values.length); + }); + + it('creates valid data', () => { + const { data } = seriesData(); + + expect( + data.filter( + ([time, value]) => new Date(time).getTime() > 0 && typeof value === 'number', + ).length, + ).toBe(data.length); + }); + + it('formats line width correctly', () => { + expect(chartData[0].lineStyle.width).toBe(2); + }); + }); + + describe('chartOptions', () => { + describe('yAxis formatter', () => { + let format; + + beforeEach(() => { + format = timeSeriesChart.vm.chartOptions.yAxis.axisLabel.formatter; + }); + + it('rounds to 3 decimal places', () => { + expect(format(0.88888)).toBe('0.889'); + }); + }); + }); + + describe('scatterSeries', () => { + it('utilizes deployment data', () => { + expect(timeSeriesChart.vm.scatterSeries.data).toEqual([ + ['2017-05-31T21:23:37.881Z', 0], + ['2017-05-30T20:08:04.629Z', 0], + ['2017-05-30T17:42:38.409Z', 0], + ]); + + expect(timeSeriesChart.vm.scatterSeries.symbolSize).toBe(14); + }); + }); + + describe('yAxisLabel', () => { + it('constructs a label for the chart y-axis', () => { + expect(timeSeriesChart.vm.yAxisLabel).toBe('CPU'); + }); + }); + + describe('csvText', () => { + it('converts data from json to csv', () => { + const header = `timestamp,${mockGraphData.y_label}`; + const data = mockGraphData.queries[0].result[0].values; + const firstRow = `${data[0][0]},${data[0][1]}`; + + expect(timeSeriesChart.vm.csvText).toMatch(`^${header}\r\n${firstRow}`); + }); + }); + + describe('downloadLink', () => { + it('produces a link to download metrics as csv', () => { + const link = timeSeriesChart.vm.downloadLink; + + expect(link).toContain('blob:'); + }); + }); + }); + + afterEach(() => { + timeSeriesChart.destroy(); + }); + }); + + describe('wrapped components', () => { + const glChartComponents = [ + { + chartType: 'area-chart', + component: GlAreaChart, + }, + { + chartType: 'line-chart', + component: GlLineChart, + }, + ]; + + glChartComponents.forEach(dynamicComponent => { + describe(`GitLab UI: ${dynamicComponent.chartType}`, () => { + let timeSeriesAreaChart; + let glChart; + + beforeEach(done => { + timeSeriesAreaChart = makeTimeSeriesChart(mockGraphData, dynamicComponent.chartType); + glChart = timeSeriesAreaChart.find(dynamicComponent.component); + timeSeriesAreaChart.vm.$nextTick(done); + }); + + it('is a Vue instance', () => { + expect(glChart.exists()).toBe(true); + expect(glChart.isVueInstance()).toBe(true); + }); + + it('receives data properties needed for proper chart render', () => { + const props = glChart.props(); + + expect(props.data).toBe(timeSeriesAreaChart.vm.chartData); + expect(props.option).toBe(timeSeriesAreaChart.vm.chartOptions); + expect(props.formatTooltipText).toBe(timeSeriesAreaChart.vm.formatTooltipText); + expect(props.thresholds).toBe(timeSeriesAreaChart.vm.thresholds); + }); + + it('recieves a tooltip title', done => { + const mockTitle = 'mockTitle'; + timeSeriesAreaChart.vm.tooltip.title = mockTitle; + + timeSeriesAreaChart.vm.$nextTick(() => { + expect(shallowWrapperContainsSlotText(glChart, 'tooltipTitle', mockTitle)).toBe(true); + done(); + }); + }); + + describe('when tooltip is showing deployment data', () => { + beforeEach(done => { + timeSeriesAreaChart.vm.tooltip.isDeployment = true; + timeSeriesAreaChart.vm.$nextTick(done); + }); + + it('uses deployment title', () => { + expect(shallowWrapperContainsSlotText(glChart, 'tooltipTitle', 'Deployed')).toBe(true); + }); + + it('renders clickable commit sha in tooltip content', done => { + timeSeriesAreaChart.vm.tooltip.sha = mockSha; + timeSeriesAreaChart.vm.tooltip.commitUrl = commitUrl; + + timeSeriesAreaChart.vm.$nextTick(() => { + const commitLink = timeSeriesAreaChart.find(GlLink); + + expect(shallowWrapperContainsSlotText(commitLink, 'default', mockSha)).toBe(true); + expect(commitLink.attributes('href')).toEqual(commitUrl); + done(); + }); + }); + }); + }); + }); + }); +}); diff --git a/spec/javascripts/monitoring/dashboard_spec.js b/spec/javascripts/monitoring/components/dashboard_spec.js index 36f650d5933..f3ec7520c6f 100644 --- a/spec/javascripts/monitoring/dashboard_spec.js +++ b/spec/javascripts/monitoring/components/dashboard_spec.js @@ -1,18 +1,21 @@ import Vue from 'vue'; +import { shallowMount, createLocalVue } from '@vue/test-utils'; +import { GlToast } from '@gitlab/ui'; import MockAdapter from 'axios-mock-adapter'; import Dashboard from '~/monitoring/components/dashboard.vue'; import { timeWindows, timeWindowsKeyNames } from '~/monitoring/constants'; import * as types from '~/monitoring/stores/mutation_types'; import { createStore } from '~/monitoring/stores'; import axios from '~/lib/utils/axios_utils'; -import { +import MonitoringMock, { metricsGroupsAPIResponse, mockApiEndpoint, environmentData, singleGroupResponse, dashboardGitResponse, -} from './mock_data'; +} from '../mock_data'; +const localVue = createLocalVue(); const propsData = { hasMetrics: false, documentationPath: '/path/to/docs', @@ -40,6 +43,7 @@ describe('Dashboard', () => { let mock; let store; let component; + let mockGraphData; beforeEach(() => { setFixtures(` @@ -58,7 +62,9 @@ describe('Dashboard', () => { }); afterEach(() => { - component.$destroy(); + if (component) { + component.$destroy(); + } mock.restore(); }); @@ -372,6 +378,71 @@ describe('Dashboard', () => { }); }); + describe('link to chart', () => { + let wrapper; + const currentDashboard = 'TEST_DASHBOARD'; + localVue.use(GlToast); + const link = () => wrapper.find('.js-chart-link'); + const clipboardText = () => link().element.dataset.clipboardText; + + beforeEach(done => { + mock.onGet(mockApiEndpoint).reply(200, metricsGroupsAPIResponse); + + wrapper = shallowMount(DashboardComponent, { + localVue, + sync: false, + attachToDocument: true, + propsData: { ...propsData, hasMetrics: true, currentDashboard }, + store, + }); + + setTimeout(done); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('adds a copy button to the dropdown', () => { + expect(link().text()).toContain('Generate link to chart'); + }); + + it('contains a link to the dashboard', () => { + expect(clipboardText()).toContain(`dashboard=${currentDashboard}`); + expect(clipboardText()).toContain(`group=`); + expect(clipboardText()).toContain(`title=`); + expect(clipboardText()).toContain(`y_label=`); + }); + + it('undefined parameter is stripped', done => { + wrapper.setProps({ currentDashboard: undefined }); + + wrapper.vm.$nextTick(() => { + expect(clipboardText()).not.toContain(`dashboard=`); + expect(clipboardText()).toContain(`y_label=`); + done(); + }); + }); + + it('null parameter is stripped', done => { + wrapper.setProps({ currentDashboard: null }); + + wrapper.vm.$nextTick(() => { + expect(clipboardText()).not.toContain(`dashboard=`); + expect(clipboardText()).toContain(`y_label=`); + done(); + }); + }); + + it('creates a toast when clicked', () => { + spyOn(wrapper.vm.$toast, 'show').and.stub(); + + link().vm.$emit('click'); + + expect(wrapper.vm.$toast.show).toHaveBeenCalled(); + }); + }); + describe('when the window resizes', () => { beforeEach(() => { mock.onGet(mockApiEndpoint).reply(200, metricsGroupsAPIResponse); @@ -482,4 +553,36 @@ describe('Dashboard', () => { }); }); }); + + describe('when downloading metrics data as CSV', () => { + beforeEach(() => { + component = new DashboardComponent({ + propsData: { + ...propsData, + }, + store, + }); + store.commit( + `monitoringDashboard/${types.RECEIVE_METRICS_DATA_SUCCESS}`, + MonitoringMock.data, + ); + [mockGraphData] = component.$store.state.monitoringDashboard.groups[0].metrics; + }); + + describe('csvText', () => { + it('converts metrics data from json to csv', () => { + const header = `timestamp,${mockGraphData.y_label}`; + const data = mockGraphData.queries[0].result[0].values; + const firstRow = `${data[0][0]},${data[0][1]}`; + + expect(component.csvText(mockGraphData)).toMatch(`^${header}\r\n${firstRow}`); + }); + }); + + describe('downloadCsv', () => { + it('produces a link with a Blob', () => { + expect(component.downloadCsv(mockGraphData)).toContain(`blob:`); + }); + }); + }); }); diff --git a/spec/javascripts/monitoring/mock_data.js b/spec/javascripts/monitoring/mock_data.js index 85e660d3925..17e7314e214 100644 --- a/spec/javascripts/monitoring/mock_data.js +++ b/spec/javascripts/monitoring/mock_data.js @@ -1,5 +1,7 @@ export const mockApiEndpoint = `${gl.TEST_HOST}/monitoring/mock`; +export const mockProjectPath = '/frontend-fixtures/environments-project'; + export const metricsGroupsAPIResponse = { success: true, data: [ @@ -902,7 +904,7 @@ export const metricsDashboardResponse = { }, { title: 'Memory Usage (Pod average)', - type: 'area-chart', + type: 'line-chart', y_label: 'Memory Used per Pod', weight: 2, metrics: [ diff --git a/spec/javascripts/monitoring/panel_type_spec.js b/spec/javascripts/monitoring/panel_type_spec.js index 8ce24041e97..a2366e74d43 100644 --- a/spec/javascripts/monitoring/panel_type_spec.js +++ b/spec/javascripts/monitoring/panel_type_spec.js @@ -1,20 +1,25 @@ import { shallowMount } from '@vue/test-utils'; import PanelType from '~/monitoring/components/panel_type.vue'; import EmptyChart from '~/monitoring/components/charts/empty_chart.vue'; +import TimeSeriesChart from '~/monitoring/components/charts/time_series.vue'; import { graphDataPrometheusQueryRange } from './mock_data'; +import { createStore } from '~/monitoring/stores'; describe('Panel Type component', () => { + let store; let panelType; const dashboardWidth = 100; describe('When no graphData is available', () => { let glEmptyChart; - const graphDataNoResult = graphDataPrometheusQueryRange; + // Deep clone object before modifying + const graphDataNoResult = JSON.parse(JSON.stringify(graphDataPrometheusQueryRange)); graphDataNoResult.queries[0].result = []; beforeEach(() => { panelType = shallowMount(PanelType, { propsData: { + clipboardText: 'dashboard_link', dashboardWidth, graphData: graphDataNoResult, }, @@ -41,4 +46,34 @@ describe('Panel Type component', () => { }); }); }); + + describe('when Graph data is available', () => { + const exampleText = 'example_text'; + + beforeEach(() => { + store = createStore(); + panelType = shallowMount(PanelType, { + propsData: { + clipboardText: exampleText, + dashboardWidth, + graphData: graphDataPrometheusQueryRange, + }, + store, + }); + }); + + describe('Time Series Chart panel type', () => { + it('is rendered', () => { + expect(panelType.find(TimeSeriesChart).isVueInstance()).toBe(true); + expect(panelType.find(TimeSeriesChart).exists()).toBe(true); + }); + + it('sets clipboard text on the dropdown', () => { + const link = () => panelType.find('.js-chart-link'); + const clipboardText = () => link().element.dataset.clipboardText; + + expect(clipboardText()).toBe(exampleText); + }); + }); + }); }); diff --git a/spec/javascripts/notes/stores/getters_spec.js b/spec/javascripts/notes/stores/getters_spec.js index 71dcba114a9..d69f469c7c7 100644 --- a/spec/javascripts/notes/stores/getters_spec.js +++ b/spec/javascripts/notes/stores/getters_spec.js @@ -14,6 +14,13 @@ import { const discussionWithTwoUnresolvedNotes = 'merge_requests/resolved_diff_discussion.json'; +// Helper function to ensure that we're using the same schema across tests. +const createDiscussionNeighborParams = (discussionId, diffOrder, step) => ({ + discussionId, + diffOrder, + step, +}); + describe('Getters Notes Store', () => { let state; @@ -25,7 +32,6 @@ describe('Getters Notes Store', () => { targetNoteHash: 'hash', lastFetchedAt: 'timestamp', isNotesFetched: false, - notesData: notesDataMock, userData: userDataMock, noteableData: noteableDataMock, @@ -244,62 +250,104 @@ describe('Getters Notes Store', () => { }); }); - describe('nextUnresolvedDiscussionId', () => { - const localGetters = { - unresolvedDiscussionsIdsOrdered: () => ['123', '456', '789'], - }; + describe('findUnresolvedDiscussionIdNeighbor', () => { + let localGetters; + beforeEach(() => { + localGetters = { + unresolvedDiscussionsIdsOrdered: () => ['123', '456', '789'], + }; + }); - it('should return the ID of the discussion after the ID provided', () => { - expect(getters.nextUnresolvedDiscussionId(state, localGetters)('123')).toBe('456'); - expect(getters.nextUnresolvedDiscussionId(state, localGetters)('456')).toBe('789'); - expect(getters.nextUnresolvedDiscussionId(state, localGetters)('789')).toBe('123'); + [ + { step: 1, id: '123', expected: '456' }, + { step: 1, id: '456', expected: '789' }, + { step: 1, id: '789', expected: '123' }, + { step: -1, id: '123', expected: '789' }, + { step: -1, id: '456', expected: '123' }, + { step: -1, id: '789', expected: '456' }, + ].forEach(({ step, id, expected }) => { + it(`with step ${step} and id ${id}, returns next value`, () => { + const params = createDiscussionNeighborParams(id, true, step); + + expect(getters.findUnresolvedDiscussionIdNeighbor(state, localGetters)(params)).toBe( + expected, + ); + }); }); - }); - describe('previousUnresolvedDiscussionId', () => { - describe('with unresolved discussions', () => { - const localGetters = { - unresolvedDiscussionsIdsOrdered: () => ['123', '456', '789'], - }; + describe('with 1 unresolved discussion', () => { + beforeEach(() => { + localGetters = { + unresolvedDiscussionsIdsOrdered: () => ['123'], + }; + }); + + [{ step: 1, id: '123', expected: '123' }, { step: -1, id: '123', expected: '123' }].forEach( + ({ step, id, expected }) => { + it(`with step ${step} and match, returns only value`, () => { + const params = createDiscussionNeighborParams(id, true, step); - it('with bogus returns falsey', () => { - expect(getters.previousUnresolvedDiscussionId(state, localGetters)('bogus')).toBe('456'); + expect(getters.findUnresolvedDiscussionIdNeighbor(state, localGetters)(params)).toBe( + expected, + ); + }); + }, + ); + + it('with no match, returns only value', () => { + const params = createDiscussionNeighborParams('bogus', true, 1); + + expect(getters.findUnresolvedDiscussionIdNeighbor(state, localGetters)(params)).toBe('123'); }); + }); - [ - { id: '123', expected: '789' }, - { id: '456', expected: '123' }, - { id: '789', expected: '456' }, - ].forEach(({ id, expected }) => { - it(`with ${id}, returns previous value`, () => { - expect(getters.previousUnresolvedDiscussionId(state, localGetters)(id)).toBe(expected); + describe('with 0 unresolved discussions', () => { + beforeEach(() => { + localGetters = { + unresolvedDiscussionsIdsOrdered: () => [], + }; + }); + + [{ step: 1 }, { step: -1 }].forEach(({ step }) => { + it(`with step ${step}, returns undefined`, () => { + const params = createDiscussionNeighborParams('bogus', true, step); + + expect( + getters.findUnresolvedDiscussionIdNeighbor(state, localGetters)(params), + ).toBeUndefined(); }); }); }); + }); - describe('with 1 unresolved discussion', () => { - const localGetters = { - unresolvedDiscussionsIdsOrdered: () => ['123'], - }; + describe('findUnresolvedDiscussionIdNeighbor aliases', () => { + let neighbor; + let findUnresolvedDiscussionIdNeighbor; + let localGetters; - it('with bogus returns id', () => { - expect(getters.previousUnresolvedDiscussionId(state, localGetters)('bogus')).toBe('123'); - }); + beforeEach(() => { + neighbor = {}; + findUnresolvedDiscussionIdNeighbor = jasmine.createSpy().and.returnValue(neighbor); + localGetters = { findUnresolvedDiscussionIdNeighbor }; + }); - it('with match, returns value', () => { - expect(getters.previousUnresolvedDiscussionId(state, localGetters)('123')).toEqual('123'); + describe('nextUnresolvedDiscussionId', () => { + it('should return result of find neighbor', () => { + const expectedParams = createDiscussionNeighborParams('123', true, 1); + const result = getters.nextUnresolvedDiscussionId(state, localGetters)('123', true); + + expect(findUnresolvedDiscussionIdNeighbor).toHaveBeenCalledWith(expectedParams); + expect(result).toBe(neighbor); }); }); - describe('with 0 unresolved discussions', () => { - const localGetters = { - unresolvedDiscussionsIdsOrdered: () => [], - }; + describe('previosuUnresolvedDiscussionId', () => { + it('should return result of find neighbor', () => { + const expectedParams = createDiscussionNeighborParams('123', true, -1); + const result = getters.previousUnresolvedDiscussionId(state, localGetters)('123', true); - it('returns undefined', () => { - expect( - getters.previousUnresolvedDiscussionId(state, localGetters)('bogus'), - ).toBeUndefined(); + expect(findUnresolvedDiscussionIdNeighbor).toHaveBeenCalledWith(expectedParams); + expect(result).toBe(neighbor); }); }); }); diff --git a/spec/javascripts/registry/components/app_spec.js b/spec/javascripts/registry/components/app_spec.js index e7675669f7a..5ea3f85a247 100644 --- a/spec/javascripts/registry/components/app_spec.js +++ b/spec/javascripts/registry/components/app_spec.js @@ -84,12 +84,7 @@ describe('Registry List', () => { it('should render empty message', done => { setTimeout(() => { - expect( - vm.$el - .querySelector('p') - .textContent.trim() - .replace(/[\r\n]+/g, ' '), - ).toEqual( + expect(vm.$el.querySelector('.js-no-container-images-text').textContent).toEqual( 'With the Container Registry, every project can have its own space to store its Docker images. More Information', ); done(); @@ -124,7 +119,9 @@ describe('Registry List', () => { it('should render invalid characters error message', done => { setTimeout(() => { - expect(vm.$el.querySelector('.container-message')).not.toBe(null); + expect(vm.$el.querySelector('p')).not.toContain( + 'We are having trouble connecting to Docker, which could be due to an issue with your project name or path. More information', + ); done(); }); }); diff --git a/spec/javascripts/registry/components/table_registry_spec.js b/spec/javascripts/registry/components/table_registry_spec.js index 31ac970378e..9c7439206ef 100644 --- a/spec/javascripts/registry/components/table_registry_spec.js +++ b/spec/javascripts/registry/components/table_registry_spec.js @@ -1,61 +1,159 @@ import Vue from 'vue'; import tableRegistry from '~/registry/components/table_registry.vue'; import store from '~/registry/stores'; +import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; import { repoPropsData } from '../mock_data'; -const [firstImage] = repoPropsData.list; +const [firstImage, secondImage] = repoPropsData.list; describe('table registry', () => { let vm; - let Component; + const Component = Vue.extend(tableRegistry); + const bulkDeletePath = 'path'; const findDeleteBtn = () => vm.$el.querySelector('.js-delete-registry'); + const findDeleteBtnRow = () => vm.$el.querySelector('.js-delete-registry-row'); + const findSelectAllCheckbox = () => vm.$el.querySelector('.js-select-all-checkbox > input'); + const findAllRowCheckboxes = () => + Array.from(vm.$el.querySelectorAll('.js-select-checkbox input')); + const confirmationModal = (child = '') => document.querySelector(`#${vm.modalId} ${child}`); - beforeEach(() => { - Component = Vue.extend(tableRegistry); - vm = new Component({ + const createComponent = () => { + vm = mountComponentWithStore(Component, { store, - propsData: { + props: { repo: repoPropsData, }, - }).$mount(); + }); + }; + + const selectAllCheckboxes = () => vm.selectAll(); + const deselectAllCheckboxes = () => vm.deselectAll(); + + beforeEach(() => { + createComponent(); }); afterEach(() => { vm.$destroy(); }); - it('should render a table with the registry list', () => { - expect(vm.$el.querySelectorAll('table tbody tr').length).toEqual(repoPropsData.list.length); + describe('rendering', () => { + it('should render a table with the registry list', () => { + expect(vm.$el.querySelectorAll('table tbody tr').length).toEqual(repoPropsData.list.length); + }); + + it('should render registry tag', () => { + const textRendered = vm.$el + .querySelector('.table tbody tr') + .textContent.trim() + // replace additional whitespace characters (e.g. new lines) with a single empty space + .replace(/\s\s+/g, ' '); + + expect(textRendered).toContain(repoPropsData.list[0].tag); + expect(textRendered).toContain(repoPropsData.list[0].shortRevision); + expect(textRendered).toContain(repoPropsData.list[0].layers); + expect(textRendered).toContain(repoPropsData.list[0].size); + }); }); - it('should render registry tag', () => { - const textRendered = vm.$el - .querySelector('.table tbody tr') - .textContent.trim() - .replace(/\s\s+/g, ' '); + describe('multi select', () => { + it('should support multiselect and selecting a row should enable delete button', done => { + findSelectAllCheckbox().click(); + selectAllCheckboxes(); + + expect(findSelectAllCheckbox().checked).toBe(true); + + Vue.nextTick(() => { + expect(findDeleteBtn().disabled).toBe(false); + done(); + }); + }); + + it('selecting all checkbox should select all rows and enable delete button', done => { + selectAllCheckboxes(); + + Vue.nextTick(() => { + const checkedValues = findAllRowCheckboxes().filter(x => x.checked); + + expect(checkedValues.length).toBe(repoPropsData.list.length); + done(); + }); + }); + + it('deselecting select all checkbox should deselect all rows and disable delete button', done => { + selectAllCheckboxes(); + deselectAllCheckboxes(); + + Vue.nextTick(() => { + const checkedValues = findAllRowCheckboxes().filter(x => x.checked); + + expect(checkedValues.length).toBe(0); + done(); + }); + }); + + it('should delete multiple items when multiple items are selected', done => { + selectAllCheckboxes(); + + Vue.nextTick(() => { + expect(vm.itemsToBeDeleted).toEqual([0, 1]); + expect(findDeleteBtn().disabled).toBe(false); + + findDeleteBtn().click(); + spyOn(vm, 'multiDeleteItems').and.returnValue(Promise.resolve()); + + Vue.nextTick(() => { + const modal = confirmationModal(); + confirmationModal('.btn-danger').click(); + + expect(modal).toExist(); - expect(textRendered).toContain(repoPropsData.list[0].tag); - expect(textRendered).toContain(repoPropsData.list[0].shortRevision); - expect(textRendered).toContain(repoPropsData.list[0].layers); - expect(textRendered).toContain(repoPropsData.list[0].size); + Vue.nextTick(() => { + expect(vm.itemsToBeDeleted).toEqual([]); + expect(vm.multiDeleteItems).toHaveBeenCalledWith({ + path: bulkDeletePath, + items: [firstImage.tag, secondImage.tag], + }); + done(); + }); + }); + }); + }); }); describe('delete registry', () => { - it('should be possible to delete a registry', () => { - expect(findDeleteBtn()).toBeDefined(); + beforeEach(() => { + vm.itemsToBeDeleted = [0]; }); - it('should call deleteItem and reset itemToBeDeleted when confirming deletion', done => { - findDeleteBtn().click(); - spyOn(vm, 'deleteItem').and.returnValue(Promise.resolve()); + it('should be possible to delete a registry', done => { + Vue.nextTick(() => { + expect(vm.itemsToBeDeleted).toEqual([0]); + expect(findDeleteBtn()).toBeDefined(); + expect(findDeleteBtn().disabled).toBe(false); + expect(findDeleteBtnRow()).toBeDefined(); + done(); + }); + }); + it('should call deleteItems and reset itemsToBeDeleted when confirming deletion', done => { Vue.nextTick(() => { - document.querySelector(`#${vm.modalId} .btn-danger`).click(); + expect(vm.itemsToBeDeleted).toEqual([0]); + expect(findDeleteBtn().disabled).toBe(false); + findDeleteBtn().click(); + spyOn(vm, 'multiDeleteItems').and.returnValue(Promise.resolve()); - expect(vm.deleteItem).toHaveBeenCalledWith(firstImage); - expect(vm.itemToBeDeleted).toBeNull(); - done(); + Vue.nextTick(() => { + confirmationModal('.btn-danger').click(); + + expect(vm.itemsToBeDeleted).toEqual([]); + expect(vm.multiDeleteItems).toHaveBeenCalledWith({ + path: bulkDeletePath, + items: [firstImage.tag], + }); + done(); + }); }); }); }); @@ -65,4 +163,27 @@ describe('table registry', () => { expect(vm.$el.querySelector('.gl-pagination')).toBeDefined(); }); }); + + describe('modal content', () => { + it('should show the singular title and image name when deleting a single image', done => { + findDeleteBtnRow().click(); + + Vue.nextTick(() => { + expect(vm.modalTitle).toBe('Remove image'); + expect(vm.modalDescription).toContain(firstImage.tag); + done(); + }); + }); + + it('should show the plural title and image count when deleting more than one image', done => { + selectAllCheckboxes(); + vm.setModalDescription(); + + Vue.nextTick(() => { + expect(vm.modalTitle).toBe('Remove images'); + expect(vm.modalDescription).toContain('<b>2</b> images'); + done(); + }); + }); + }); }); diff --git a/spec/javascripts/registry/mock_data.js b/spec/javascripts/registry/mock_data.js index 22db203e77f..130ab298e89 100644 --- a/spec/javascripts/registry/mock_data.js +++ b/spec/javascripts/registry/mock_data.js @@ -108,6 +108,17 @@ export const repoPropsData = { destroyPath: 'path', canDelete: true, }, + { + tag: 'test-image', + revision: 'b969de599faea2b3d9b6605a8b0897261c571acaa36db1bdc7349b5775b4e0b4', + shortRevision: 'b969de599', + size: 19, + layers: 10, + location: 'location-2', + createdAt: 1505828744434, + destroyPath: 'path-2', + canDelete: true, + }, ], location: 'location', name: 'foo', diff --git a/spec/javascripts/sidebar/assignee_title_spec.js b/spec/javascripts/sidebar/assignee_title_spec.js index 509edba2036..7fff7c075d9 100644 --- a/spec/javascripts/sidebar/assignee_title_spec.js +++ b/spec/javascripts/sidebar/assignee_title_spec.js @@ -4,8 +4,10 @@ import AssigneeTitle from '~/sidebar/components/assignees/assignee_title.vue'; describe('AssigneeTitle component', () => { let component; let AssigneeTitleComponent; + let statsSpy; beforeEach(() => { + statsSpy = spyOnDependency(AssigneeTitle, 'trackEvent'); AssigneeTitleComponent = Vue.extend(AssigneeTitle); }); @@ -102,4 +104,16 @@ describe('AssigneeTitle component', () => { expect(component.$el.querySelector('.edit-link')).not.toBeNull(); }); + + it('calls trackEvent when edit is clicked', () => { + component = new AssigneeTitleComponent({ + propsData: { + numberOfAssignees: 0, + editable: true, + }, + }).$mount(); + component.$el.querySelector('.js-sidebar-dropdown-toggle').click(); + + expect(statsSpy).toHaveBeenCalled(); + }); }); diff --git a/spec/javascripts/sidebar/assignees_spec.js b/spec/javascripts/sidebar/assignees_spec.js index 4ae2141d5f0..a1df5389a38 100644 --- a/spec/javascripts/sidebar/assignees_spec.js +++ b/spec/javascripts/sidebar/assignees_spec.js @@ -94,115 +94,9 @@ describe('Assignee component', () => { expect(assignee.querySelector('.author').innerText.trim()).toEqual(UsersMock.user.name); }); - - it('Shows one user with avatar, username and author name', () => { - component = new AssigneeComponent({ - propsData: { - rootPath: 'http://localhost:3000/', - users: [UsersMock.user], - editable: true, - }, - }).$mount(); - - expect(component.$el.querySelector('.author-link')).not.toBeNull(); - // The image - expect(component.$el.querySelector('.author-link img').getAttribute('src')).toEqual( - UsersMock.user.avatar, - ); - // Author name - expect(component.$el.querySelector('.author-link .author').innerText.trim()).toEqual( - UsersMock.user.name, - ); - // Username - expect(component.$el.querySelector('.author-link .username').innerText.trim()).toEqual( - `@${UsersMock.user.username}`, - ); - }); - - it('has the root url present in the assigneeUrl method', () => { - component = new AssigneeComponent({ - propsData: { - rootPath: 'http://localhost:3000/', - users: [UsersMock.user], - editable: true, - }, - }).$mount(); - - expect(component.assigneeUrl(UsersMock.user).indexOf('http://localhost:3000/')).not.toEqual( - -1, - ); - }); - - it('has correct "cannot merge" tooltip when user cannot merge', () => { - const user = Object.assign({}, UsersMock.user, { can_merge: false }); - - component = new AssigneeComponent({ - propsData: { - rootPath: 'http://localhost:3000/', - users: [user], - editable: true, - issuableType: 'merge_request', - }, - }).$mount(); - - expect(component.mergeNotAllowedTooltipMessage).toEqual('Cannot merge'); - }); }); describe('Two or more assignees/users', () => { - it('has correct "cannot merge" tooltip when one user can merge', () => { - const users = UsersMockHelper.createNumberRandomUsers(3); - users[0].can_merge = true; - users[1].can_merge = false; - users[2].can_merge = false; - - component = new AssigneeComponent({ - propsData: { - rootPath: 'http://localhost:3000/', - users, - editable: true, - issuableType: 'merge_request', - }, - }).$mount(); - - expect(component.mergeNotAllowedTooltipMessage).toEqual('1/3 can merge'); - }); - - it('has correct "cannot merge" tooltip when no user can merge', () => { - const users = UsersMockHelper.createNumberRandomUsers(2); - users[0].can_merge = false; - users[1].can_merge = false; - - component = new AssigneeComponent({ - propsData: { - rootPath: 'http://localhost:3000/', - users, - editable: true, - issuableType: 'merge_request', - }, - }).$mount(); - - expect(component.mergeNotAllowedTooltipMessage).toEqual('No one can merge'); - }); - - it('has correct "cannot merge" tooltip when more than one user can merge', () => { - const users = UsersMockHelper.createNumberRandomUsers(3); - users[0].can_merge = false; - users[1].can_merge = true; - users[2].can_merge = true; - - component = new AssigneeComponent({ - propsData: { - rootPath: 'http://localhost:3000/', - users, - editable: true, - issuableType: 'merge_request', - }, - }).$mount(); - - expect(component.mergeNotAllowedTooltipMessage).toEqual('2/3 can merge'); - }); - it('has no "cannot merge" tooltip when every user can merge', () => { const users = UsersMockHelper.createNumberRandomUsers(2); users[0].can_merge = true; @@ -217,7 +111,7 @@ describe('Assignee component', () => { }, }).$mount(); - expect(component.mergeNotAllowedTooltipMessage).toEqual(null); + expect(component.collapsedTooltipTitle).not.toContain('cannot merge'); }); it('displays two assignee icons when collapsed', () => { @@ -295,8 +189,12 @@ describe('Assignee component', () => { expect(component.$el.querySelector('.user-list-more')).toBe(null); }); - it('sets tooltip container to body', () => { - const users = UsersMockHelper.createNumberRandomUsers(2); + it('shows sorted assignee where "can merge" users are sorted first', () => { + const users = UsersMockHelper.createNumberRandomUsers(3); + users[0].can_merge = false; + users[1].can_merge = false; + users[2].can_merge = true; + component = new AssigneeComponent({ propsData: { rootPath: 'http://localhost:3000', @@ -305,98 +203,46 @@ describe('Assignee component', () => { }, }).$mount(); - expect(component.$el.querySelector('.user-link').getAttribute('data-container')).toBe('body'); + expect(component.sortedAssigness[0].can_merge).toBe(true); }); - it('Shows the "show-less" assignees label', done => { - const users = UsersMockHelper.createNumberRandomUsers(6); + it('passes the sorted assignees to the uncollapsed-assignee-list', () => { + const users = UsersMockHelper.createNumberRandomUsers(3); + users[0].can_merge = false; + users[1].can_merge = false; + users[2].can_merge = true; + component = new AssigneeComponent({ propsData: { rootPath: 'http://localhost:3000', users, - editable: true, + editable: false, }, }).$mount(); - expect(component.$el.querySelectorAll('.user-item').length).toEqual( - component.defaultRenderCount, - ); - - expect(component.$el.querySelector('.user-list-more')).not.toBe(null); - const usersLabelExpectation = users.length - component.defaultRenderCount; + const userItems = component.$el.querySelectorAll('.user-list .user-item a'); - expect(component.$el.querySelector('.user-list-more .btn-link').innerText.trim()).not.toBe( - `+${usersLabelExpectation} more`, - ); - component.toggleShowLess(); - Vue.nextTick(() => { - expect(component.$el.querySelector('.user-list-more .btn-link').innerText.trim()).toBe( - '- show less', - ); - done(); - }); + expect(userItems.length).toBe(3); + expect(userItems[0].dataset.originalTitle).toBe(users[2].name); }); - it('Shows the "show-less" when "n+ more " label is clicked', done => { - const users = UsersMockHelper.createNumberRandomUsers(6); - component = new AssigneeComponent({ - propsData: { - rootPath: 'http://localhost:3000', - users, - editable: true, - }, - }).$mount(); - - component.$el.querySelector('.user-list-more .btn-link').click(); - Vue.nextTick(() => { - expect(component.$el.querySelector('.user-list-more .btn-link').innerText.trim()).toBe( - '- show less', - ); - done(); - }); - }); + it('passes the sorted assignees to the collapsed-assignee-list', () => { + const users = UsersMockHelper.createNumberRandomUsers(3); + users[0].can_merge = false; + users[1].can_merge = false; + users[2].can_merge = true; - it('gets the count of avatar via a computed property ', () => { - const users = UsersMockHelper.createNumberRandomUsers(6); component = new AssigneeComponent({ propsData: { rootPath: 'http://localhost:3000', users, - editable: true, + editable: false, }, }).$mount(); - expect(component.sidebarAvatarCounter).toEqual(`+${users.length - 1}`); - }); + const collapsedButton = component.$el.querySelector('.sidebar-collapsed-user button'); - describe('n+ more label', () => { - beforeEach(() => { - const users = UsersMockHelper.createNumberRandomUsers(6); - component = new AssigneeComponent({ - propsData: { - rootPath: 'http://localhost:3000', - users, - editable: true, - }, - }).$mount(); - }); - - it('shows "+1 more" label', () => { - expect(component.$el.querySelector('.user-list-more .btn-link').innerText.trim()).toBe( - '+ 1 more', - ); - }); - - it('shows "show less" label', done => { - component.toggleShowLess(); - - Vue.nextTick(() => { - expect(component.$el.querySelector('.user-list-more .btn-link').innerText.trim()).toBe( - '- show less', - ); - done(); - }); - }); + expect(collapsedButton.innerText.trim()).toBe(users[2].name); }); }); }); diff --git a/spec/javascripts/sidebar/confidential_issue_sidebar_spec.js b/spec/javascripts/sidebar/confidential_issue_sidebar_spec.js index 486a7241e33..ea9e5677bc5 100644 --- a/spec/javascripts/sidebar/confidential_issue_sidebar_spec.js +++ b/spec/javascripts/sidebar/confidential_issue_sidebar_spec.js @@ -4,8 +4,10 @@ import confidentialIssueSidebar from '~/sidebar/components/confidential/confiden describe('Confidential Issue Sidebar Block', () => { let vm1; let vm2; + let statsSpy; beforeEach(() => { + statsSpy = spyOnDependency(confidentialIssueSidebar, 'trackEvent'); const Component = Vue.extend(confidentialIssueSidebar); const service = { update: () => Promise.resolve(true), @@ -67,4 +69,10 @@ describe('Confidential Issue Sidebar Block', () => { done(); }); }); + + it('calls trackEvent when "Edit" is clicked', () => { + vm1.$el.querySelector('.confidential-edit').click(); + + expect(statsSpy).toHaveBeenCalled(); + }); }); diff --git a/spec/javascripts/sidebar/lock/lock_issue_sidebar_spec.js b/spec/javascripts/sidebar/lock/lock_issue_sidebar_spec.js index ca882032bdf..2d930428230 100644 --- a/spec/javascripts/sidebar/lock/lock_issue_sidebar_spec.js +++ b/spec/javascripts/sidebar/lock/lock_issue_sidebar_spec.js @@ -4,8 +4,10 @@ import lockIssueSidebar from '~/sidebar/components/lock/lock_issue_sidebar.vue'; describe('LockIssueSidebar', () => { let vm1; let vm2; + let statsSpy; beforeEach(() => { + statsSpy = spyOnDependency(lockIssueSidebar, 'trackEvent'); const Component = Vue.extend(lockIssueSidebar); const mediator = { @@ -59,6 +61,12 @@ describe('LockIssueSidebar', () => { }); }); + it('calls trackEvent when "Edit" is clicked', () => { + vm1.$el.querySelector('.lock-edit').click(); + + expect(statsSpy).toHaveBeenCalled(); + }); + it('displays the edit form when opened from collapsed state', done => { expect(vm1.isLockDialogOpen).toBe(false); diff --git a/spec/javascripts/sidebar/subscriptions_spec.js b/spec/javascripts/sidebar/subscriptions_spec.js index 32728e58b06..2efa13f3fe8 100644 --- a/spec/javascripts/sidebar/subscriptions_spec.js +++ b/spec/javascripts/sidebar/subscriptions_spec.js @@ -6,8 +6,10 @@ import mountComponent from 'spec/helpers/vue_mount_component_helper'; describe('Subscriptions', function() { let vm; let Subscriptions; + let statsSpy; beforeEach(() => { + statsSpy = spyOnDependency(subscriptions, 'trackEvent'); Subscriptions = Vue.extend(subscriptions); }); @@ -58,6 +60,13 @@ describe('Subscriptions', function() { expect(vm.$emit).toHaveBeenCalledWith('toggleSubscription', jasmine.any(Object)); }); + it('calls trackEvent when toggled', () => { + vm = mountComponent(Subscriptions, { subscribed: true }); + vm.toggleSubscription(); + + expect(statsSpy).toHaveBeenCalled(); + }); + it('onClickCollapsedIcon method emits `toggleSidebar` event on component', () => { vm = mountComponent(Subscriptions, { subscribed: true }); spyOn(vm, '$emit'); diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_rebase_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_rebase_spec.js index 212519743aa..7216ad00cc1 100644 --- a/spec/javascripts/vue_mr_widget/components/mr_widget_rebase_spec.js +++ b/spec/javascripts/vue_mr_widget/components/mr_widget_rebase_spec.js @@ -83,6 +83,24 @@ describe('Merge request widget rebase component', () => { expect(text).toContain('foo'); expect(text.replace(/\s\s+/g, ' ')).toContain('to allow this merge request to be merged.'); }); + + it('should render the correct target branch name', () => { + const targetBranch = 'fake-branch-to-test-with'; + vm = mountComponent(Component, { + mr: { + rebaseInProgress: false, + canPushToSourceBranch: false, + targetBranch, + }, + service: {}, + }); + + const elem = vm.$el.querySelector('.rebase-state-find-class-convention span'); + + expect(elem.innerHTML).toContain( + `Fast-forward merge is not possible. Rebase the source branch onto <span class="label-branch">${targetBranch}</span> to allow this merge request to be merged.`, + ); + }); }); describe('methods', () => { diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js index ba3ba01944d..53e1f077610 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js @@ -236,24 +236,26 @@ describe('ReadyToMerge', () => { }); }); - describe('shouldShowMergeOptionsDropdown', () => { - it('should return false when no auto merge strategies are available', () => { - Vue.set(vm.mr, 'availableAutoMergeStrategies', []); + describe('shouldShowMergeImmediatelyDropdown', () => { + it('should return false if no pipeline is active', () => { + Vue.set(vm.mr, 'isPipelineActive', false); + Vue.set(vm.mr, 'onlyAllowMergeIfPipelineSucceeds', false); - expect(vm.shouldShowMergeOptionsDropdown).toBe(false); + expect(vm.shouldShowMergeImmediatelyDropdown).toBe(false); }); - it('should return true when at least one auto merge strategy is available', () => { - Vue.set(vm.mr, 'availableAutoMergeStrategies', [ATMTWPS_MERGE_STRATEGY]); + it('should return false if "Pipelines must succeed" is enabled for the current project', () => { + Vue.set(vm.mr, 'isPipelineActive', true); + Vue.set(vm.mr, 'onlyAllowMergeIfPipelineSucceeds', true); - expect(vm.shouldShowMergeOptionsDropdown).toBe(true); + expect(vm.shouldShowMergeImmediatelyDropdown).toBe(false); }); - it('should return false when pipeline active but only merge when pipeline succeeds set in project options', () => { - Vue.set(vm.mr, 'availableAutoMergeStrategies', [ATMTWPS_MERGE_STRATEGY]); - Vue.set(vm.mr, 'onlyAllowMergeIfPipelineSucceeds', true); + it('should return true if the MR\'s pipeline is active and "Pipelines must succeed" is not enabled for the current project', () => { + Vue.set(vm.mr, 'isPipelineActive', true); + Vue.set(vm.mr, 'onlyAllowMergeIfPipelineSucceeds', false); - expect(vm.shouldShowMergeOptionsDropdown).toBe(false); + expect(vm.shouldShowMergeImmediatelyDropdown).toBe(true); }); }); diff --git a/spec/javascripts/vue_mr_widget/mock_data.js b/spec/javascripts/vue_mr_widget/mock_data.js index 253413ae43e..a55d5537df7 100644 --- a/spec/javascripts/vue_mr_widget/mock_data.js +++ b/spec/javascripts/vue_mr_widget/mock_data.js @@ -233,6 +233,8 @@ export default { 'http://localhost:3000/root/acets-app/commit/53027d060246c8f47e4a9310fb332aa52f221775', troubleshooting_docs_path: 'help', merge_request_pipelines_docs_path: '/help/ci/merge_request_pipelines/index.md', + merge_train_when_pipeline_succeeds_docs_path: + '/help/ci/merge_request_pipelines/pipelines_for_merged_results/merge_trains/#startadd-to-merge-train-when-pipeline-succeeds', squash: true, visual_review_app_available: true, merge_trains_enabled: true, diff --git a/spec/javascripts/vue_mr_widget/stores/mr_widget_store_spec.js b/spec/javascripts/vue_mr_widget/stores/mr_widget_store_spec.js index e27a506f426..e2cd0f084fd 100644 --- a/spec/javascripts/vue_mr_widget/stores/mr_widget_store_spec.js +++ b/spec/javascripts/vue_mr_widget/stores/mr_widget_store_spec.js @@ -82,47 +82,5 @@ describe('MergeRequestStore', () => { expect(store.isNothingToMergeState).toEqual(false); }); }); - - describe('mergePipelinesEnabled', () => { - it('should set mergePipelinesEnabled = true when merge_pipelines_enabled is true', () => { - store.setData({ ...mockData, merge_pipelines_enabled: true }); - - expect(store.mergePipelinesEnabled).toBe(true); - }); - - it('should set mergePipelinesEnabled = false when merge_pipelines_enabled is not provided', () => { - store.setData({ ...mockData, merge_pipelines_enabled: undefined }); - - expect(store.mergePipelinesEnabled).toBe(false); - }); - }); - - describe('mergeTrainsCount', () => { - it('should set mergeTrainsCount when merge_trains_count is provided', () => { - store.setData({ ...mockData, merge_trains_count: 3 }); - - expect(store.mergeTrainsCount).toBe(3); - }); - - it('should set mergeTrainsCount = 0 when merge_trains_count is not provided', () => { - store.setData({ ...mockData, merge_trains_count: undefined }); - - expect(store.mergeTrainsCount).toBe(0); - }); - }); - - describe('mergeTrainIndex', () => { - it('should set mergeTrainIndex when merge_train_index is provided', () => { - store.setData({ ...mockData, merge_train_index: 3 }); - - expect(store.mergeTrainIndex).toBe(3); - }); - - it('should not set mergeTrainIndex when merge_train_index is not provided', () => { - store.setData({ ...mockData, merge_train_index: undefined }); - - expect(store.mergeTrainIndex).toBeUndefined(); - }); - }); }); }); diff --git a/spec/javascripts/vue_shared/components/changed_file_icon_spec.js b/spec/javascripts/vue_shared/components/changed_file_icon_spec.js deleted file mode 100644 index 634ba8403d5..00000000000 --- a/spec/javascripts/vue_shared/components/changed_file_icon_spec.js +++ /dev/null @@ -1,63 +0,0 @@ -import Vue from 'vue'; -import changedFileIcon from '~/vue_shared/components/changed_file_icon.vue'; -import createComponent from 'spec/helpers/vue_mount_component_helper'; - -describe('Changed file icon', () => { - let vm; - - function factory(props = {}) { - const component = Vue.extend(changedFileIcon); - - vm = createComponent(component, { - ...props, - file: { - tempFile: false, - changed: true, - }, - }); - } - - afterEach(() => { - vm.$destroy(); - }); - - it('centers icon', () => { - factory({ - isCentered: true, - }); - - expect(vm.$el.classList).toContain('ml-auto'); - }); - - describe('changedIcon', () => { - it('equals file-modified when not a temp file and has changes', () => { - factory(); - - expect(vm.changedIcon).toBe('file-modified'); - }); - - it('equals file-addition when a temp file', () => { - factory(); - - vm.file.tempFile = true; - - expect(vm.changedIcon).toBe('file-addition'); - }); - }); - - describe('changedIconClass', () => { - it('includes file-modified when not a temp file', () => { - factory(); - - expect(vm.changedIconClass).toContain('file-modified'); - }); - - it('includes file-addition when a temp file', () => { - factory(); - - vm.file.tempFile = true; - - expect(vm.changedIconClass).toContain('file-addition'); - }); - }); -}); diff --git a/spec/javascripts/vue_shared/directives/autofocusonshow_spec.js b/spec/javascripts/vue_shared/directives/autofocusonshow_spec.js new file mode 100644 index 00000000000..f1ca5f61496 --- /dev/null +++ b/spec/javascripts/vue_shared/directives/autofocusonshow_spec.js @@ -0,0 +1,38 @@ +import autofocusonshow from '~/vue_shared/directives/autofocusonshow'; + +/** + * We're testing this directive's hooks as pure functions + * since behaviour of this directive is highly-dependent + * on underlying DOM methods. + */ +describe('AutofocusOnShow directive', () => { + describe('with input invisible on component render', () => { + let el; + + beforeAll(() => { + setFixtures('<div id="container" style="display: none;"><input id="inputel"/></div>'); + el = document.querySelector('#inputel'); + }); + + it('should bind IntersectionObserver on input element', () => { + spyOn(el, 'focus'); + + autofocusonshow.inserted(el); + + expect(el.visibilityObserver).toBeDefined(); + expect(el.focus).not.toHaveBeenCalled(); + }); + + it('should stop IntersectionObserver on input element on unbind hook', () => { + el.visibilityObserver = { + disconnect: () => {}, + }; + spyOn(el.visibilityObserver, 'disconnect'); + + autofocusonshow.unbind(el); + + expect(el.visibilityObserver).toBeDefined(); + expect(el.visibilityObserver.disconnect).toHaveBeenCalled(); + }); + }); +}); diff --git a/spec/lib/api/helpers/label_helpers_spec.rb b/spec/lib/api/helpers/label_helpers_spec.rb new file mode 100644 index 00000000000..138e9a22d70 --- /dev/null +++ b/spec/lib/api/helpers/label_helpers_spec.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe API::Helpers::LabelHelpers do + describe 'create_service_params' do + let(:label_helper) do + Class.new do + include API::Helpers::LabelHelpers + end.new + end + + context 'when a project is given' do + it 'returns the expected params' do + project = create(:project) + expect(label_helper.create_service_params(project)).to eq({ project: project }) + end + end + + context 'when a group is given' do + it 'returns the expected params' do + group = create(:group) + expect(label_helper.create_service_params(group)).to eq({ group: group }) + end + end + + context 'when something else is given' do + it 'raises a type error' do + expect { label_helper.create_service_params(Class.new) }.to raise_error(TypeError) + end + end + end +end diff --git a/spec/lib/banzai/filter/inline_metrics_filter_spec.rb b/spec/lib/banzai/filter/inline_metrics_filter_spec.rb index 542a9ced6d7..66bbcbf7292 100644 --- a/spec/lib/banzai/filter/inline_metrics_filter_spec.rb +++ b/spec/lib/banzai/filter/inline_metrics_filter_spec.rb @@ -12,7 +12,7 @@ describe Banzai::Filter::InlineMetricsFilter do let(:url) { 'https://foo.com' } it 'leaves regular non-metrics links unchanged' do - expect(doc.to_s).to eq input + expect(doc.to_s).to eq(input) end end @@ -21,7 +21,7 @@ describe Banzai::Filter::InlineMetricsFilter do let(:url) { urls.metrics_namespace_project_environment_url(*params) } it 'leaves the original link unchanged' do - expect(doc.at_css('a').to_s).to eq input + expect(doc.at_css('a').to_s).to eq(input) end it 'appends a metrics charts placeholder with dashboard url after metrics links' do @@ -29,7 +29,7 @@ describe Banzai::Filter::InlineMetricsFilter do expect(node).to be_present dashboard_url = urls.metrics_dashboard_namespace_project_environment_url(*params, embedded: true) - expect(node.attribute('data-dashboard-url').to_s).to eq dashboard_url + expect(node.attribute('data-dashboard-url').to_s).to eq(dashboard_url) end context 'when the metrics dashboard link is part of a paragraph' do @@ -37,9 +37,34 @@ describe Banzai::Filter::InlineMetricsFilter do let(:input) { %(<p>#{paragraph}</p>) } it 'appends the charts placeholder after the enclosing paragraph' do - expect(doc.at_css('p').to_s).to include paragraph + expect(doc.at_css('p').to_s).to include(paragraph) expect(doc.at_css('.js-render-metrics')).to be_present end end + + context 'with dashboard params specified' do + let(:params) do + [ + 'foo', + 'bar', + 12, + { + embedded: true, + dashboard: 'config/prometheus/common_metrics.yml', + group: 'System metrics (Kubernetes)', + title: 'Core Usage (Pod Average)', + y_label: 'Cores per Pod' + } + ] + end + + it 'appends a metrics charts placeholder with dashboard url after metrics links' do + node = doc.at_css('.js-render-metrics') + expect(node).to be_present + + dashboard_url = urls.metrics_dashboard_namespace_project_environment_url(*params) + expect(node.attribute('data-dashboard-url').to_s).to eq(dashboard_url) + end + end end end diff --git a/spec/lib/banzai/filter/label_reference_filter_spec.rb b/spec/lib/banzai/filter/label_reference_filter_spec.rb index 213a5459118..35e99d2586e 100644 --- a/spec/lib/banzai/filter/label_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/label_reference_filter_spec.rb @@ -10,6 +10,11 @@ describe Banzai::Filter::LabelReferenceFilter do let(:label) { create(:label, project: project) } let(:reference) { label.to_reference } + it_behaves_like 'HTML text with references' do + let(:resource) { label } + let(:resource_text) { resource.title } + end + it 'requires project context' do expect { described_class.call('') }.to raise_error(ArgumentError, /:project/) end diff --git a/spec/lib/banzai/filter/milestone_reference_filter_spec.rb b/spec/lib/banzai/filter/milestone_reference_filter_spec.rb index 3f021adc756..ab0c2c383c5 100644 --- a/spec/lib/banzai/filter/milestone_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/milestone_reference_filter_spec.rb @@ -329,6 +329,10 @@ describe Banzai::Filter::MilestoneReferenceFilter do it_behaves_like 'cross-project / same-namespace complete reference' it_behaves_like 'cross project shorthand reference' it_behaves_like 'references with HTML entities' + it_behaves_like 'HTML text with references' do + let(:resource) { milestone } + let(:resource_text) { "#{resource.class.reference_prefix}#{resource.title}" } + end end shared_context 'group milestones' do @@ -340,6 +344,10 @@ describe Banzai::Filter::MilestoneReferenceFilter do it_behaves_like 'String-based multi-word references in quotes' it_behaves_like 'referencing a milestone in a link href' it_behaves_like 'references with HTML entities' + it_behaves_like 'HTML text with references' do + let(:resource) { milestone } + let(:resource_text) { "#{resource.class.reference_prefix}#{resource.title}" } + end it 'does not support references by IID' do doc = reference_filter("See #{Milestone.reference_prefix}#{milestone.iid}") diff --git a/spec/lib/banzai/filter/relative_link_filter_spec.rb b/spec/lib/banzai/filter/relative_link_filter_spec.rb index ecb83b6cb66..789530fbc56 100644 --- a/spec/lib/banzai/filter/relative_link_filter_spec.rb +++ b/spec/lib/banzai/filter/relative_link_filter_spec.rb @@ -7,6 +7,7 @@ describe Banzai::Filter::RelativeLinkFilter do contexts.reverse_merge!({ commit: commit, project: project, + current_user: user, group: group, project_wiki: project_wiki, ref: ref, @@ -33,7 +34,8 @@ describe Banzai::Filter::RelativeLinkFilter do %(<div>#{element}</div>) end - let(:project) { create(:project, :repository) } + let(:project) { create(:project, :repository, :public) } + let(:user) { create(:user) } let(:group) { nil } let(:project_path) { project.full_path } let(:ref) { 'markdown' } @@ -75,6 +77,11 @@ describe Banzai::Filter::RelativeLinkFilter do include_examples :preserve_unchanged end + context 'without project repository access' do + let(:project) { create(:project, :repository, repository_access_level: ProjectFeature::PRIVATE) } + include_examples :preserve_unchanged + end + it 'does not raise an exception on invalid URIs' do act = link("://foo") expect { filter(act) }.not_to raise_error @@ -282,6 +289,37 @@ describe Banzai::Filter::RelativeLinkFilter do let(:relative_path) { "/#{project.full_path}#{upload_path}" } context 'to a project upload' do + context 'without project repository access' do + let(:project) { create(:project, :repository, repository_access_level: ProjectFeature::PRIVATE) } + + it 'does not rebuild relative URL for a link' do + doc = filter(link(upload_path)) + expect(doc.at_css('a')['href']).to eq(upload_path) + + doc = filter(nested(link(upload_path))) + expect(doc.at_css('a')['href']).to eq(upload_path) + end + + it 'does not rebuild relative URL for an image' do + doc = filter(image(upload_path)) + expect(doc.at_css('img')['src']).to eq(upload_path) + + doc = filter(nested(image(upload_path))) + expect(doc.at_css('img')['src']).to eq(upload_path) + end + + context 'with an absolute URL' do + let(:absolute_path) { Gitlab.config.gitlab.url + relative_path } + let(:only_path) { false } + + it 'does not rewrite the link' do + doc = filter(link(upload_path)) + + expect(doc.at_css('a')['href']).to eq(upload_path) + end + end + end + context 'with an absolute URL' do let(:absolute_path) { Gitlab.config.gitlab.url + relative_path } let(:only_path) { false } @@ -331,11 +369,41 @@ describe Banzai::Filter::RelativeLinkFilter do end context 'to a group upload' do - let(:upload_link) { link('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg') } + let(:upload_path) { '/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg' } + let(:upload_link) { link(upload_path) } let(:group) { create(:group) } let(:project) { nil } let(:relative_path) { "/groups/#{group.full_path}/-/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg" } + context 'without group read access' do + let(:group) { create(:group, :private) } + + it 'does not rewrite the link' do + doc = filter(upload_link) + + expect(doc.at_css('a')['href']).to eq(upload_path) + end + + it 'does not rewrite the link for subgroup' do + group.update!(parent: create(:group)) + + doc = filter(upload_link) + + expect(doc.at_css('a')['href']).to eq(upload_path) + end + + context 'with an absolute URL' do + let(:absolute_path) { Gitlab.config.gitlab.url + relative_path } + let(:only_path) { false } + + it 'does not rewrite the link' do + doc = filter(upload_link) + + expect(doc.at_css('a')['href']).to eq(upload_path) + end + end + end + context 'with an absolute URL' do let(:absolute_path) { Gitlab.config.gitlab.url + relative_path } let(:only_path) { false } diff --git a/spec/lib/expand_variables_spec.rb b/spec/lib/expand_variables_spec.rb index 099d7b6b67c..394efa85701 100644 --- a/spec/lib/expand_variables_spec.rb +++ b/spec/lib/expand_variables_spec.rb @@ -4,62 +4,131 @@ require 'spec_helper' describe ExpandVariables do describe '#expand' do - subject { described_class.expand(value, variables) } + context 'table tests' do + using RSpec::Parameterized::TableSyntax - tests = [ - { value: 'key', - result: 'key', - variables: [] }, - { value: 'key$variable', - result: 'key', - variables: [] }, - { value: 'key$variable', - result: 'keyvalue', - variables: [ - { key: 'variable', value: 'value' } - ] }, - { value: 'key${variable}', - result: 'keyvalue', - variables: [ - { key: 'variable', value: 'value' } - ] }, - { value: 'key$variable$variable2', - result: 'keyvalueresult', - variables: [ - { key: 'variable', value: 'value' }, - { key: 'variable2', value: 'result' } - ] }, - { value: 'key${variable}${variable2}', - result: 'keyvalueresult', - variables: [ - { key: 'variable', value: 'value' }, - { key: 'variable2', value: 'result' } - ] }, - { value: 'key$variable2$variable', - result: 'keyresultvalue', - variables: [ - { key: 'variable', value: 'value' }, - { key: 'variable2', value: 'result' } - ] }, - { value: 'key${variable2}${variable}', - result: 'keyresultvalue', - variables: [ - { key: 'variable', value: 'value' }, - { key: 'variable2', value: 'result' } - ] }, - { value: 'review/$CI_COMMIT_REF_NAME', - result: 'review/feature/add-review-apps', - variables: [ - { key: 'CI_COMMIT_REF_NAME', value: 'feature/add-review-apps' } - ] } - ] + where do + { + "no expansion": { + value: 'key', + result: 'key', + variables: [] + }, + "missing variable": { + value: 'key$variable', + result: 'key', + variables: [] + }, + "simple expansion": { + value: 'key$variable', + result: 'keyvalue', + variables: [ + { key: 'variable', value: 'value' } + ] + }, + "simple with hash of variables": { + value: 'key$variable', + result: 'keyvalue', + variables: { + 'variable' => 'value' + } + }, + "complex expansion": { + value: 'key${variable}', + result: 'keyvalue', + variables: [ + { key: 'variable', value: 'value' } + ] + }, + "simple expansions": { + value: 'key$variable$variable2', + result: 'keyvalueresult', + variables: [ + { key: 'variable', value: 'value' }, + { key: 'variable2', value: 'result' } + ] + }, + "complex expansions": { + value: 'key${variable}${variable2}', + result: 'keyvalueresult', + variables: [ + { key: 'variable', value: 'value' }, + { key: 'variable2', value: 'result' } + ] + }, + "complex expansions with missing variable": { + value: 'key${variable}${variable2}', + result: 'keyvalue', + variables: [ + { key: 'variable', value: 'value' } + ] + }, + "out-of-order expansion": { + value: 'key$variable2$variable', + result: 'keyresultvalue', + variables: [ + { key: 'variable', value: 'value' }, + { key: 'variable2', value: 'result' } + ] + }, + "out-of-order complex expansion": { + value: 'key${variable2}${variable}', + result: 'keyresultvalue', + variables: [ + { key: 'variable', value: 'value' }, + { key: 'variable2', value: 'result' } + ] + }, + "review-apps expansion": { + value: 'review/$CI_COMMIT_REF_NAME', + result: 'review/feature/add-review-apps', + variables: [ + { key: 'CI_COMMIT_REF_NAME', value: 'feature/add-review-apps' } + ] + }, + "do not lazily access variables when no expansion": { + value: 'key', + result: 'key', + variables: -> { raise NotImplementedError } + }, + "lazily access variables": { + value: 'key$variable', + result: 'keyvalue', + variables: -> { [{ key: 'variable', value: 'value' }] } + } + } + end + + with_them do + subject { ExpandVariables.expand(value, variables) } # rubocop:disable RSpec/DescribedClass + + it { is_expected.to eq(result) } + end + end + + context 'lazily inits variables' do + let(:variables) { -> { [{ key: 'variable', value: 'result' }] } } + + subject { described_class.expand(value, variables) } + + context 'when expanding variable' do + let(:value) { 'key$variable$variable2' } + + it 'calls block at most once' do + expect(variables).to receive(:call).once.and_call_original + + is_expected.to eq('keyresult') + end + end + + context 'when no expansion is needed' do + let(:value) { 'key' } - tests.each do |test| - context "#{test[:value]} resolves to #{test[:result]}" do - let(:value) { test[:value] } - let(:variables) { test[:variables] } + it 'does not call block' do + expect(variables).not_to receive(:call) - it { is_expected.to eq(test[:result]) } + is_expected.to eq('key') + end end end end diff --git a/spec/lib/gitlab/action_rate_limiter_spec.rb b/spec/lib/gitlab/action_rate_limiter_spec.rb index 8dbad32dfb4..8b510a475d2 100644 --- a/spec/lib/gitlab/action_rate_limiter_spec.rb +++ b/spec/lib/gitlab/action_rate_limiter_spec.rb @@ -74,9 +74,9 @@ describe Gitlab::ActionRateLimiter, :clean_gitlab_redis_cache do { message: 'Action_Rate_Limiter_Request', env: type, - ip: '127.0.0.1', + remote_ip: '127.0.0.1', request_method: 'GET', - fullpath: fullpath + path: fullpath } end diff --git a/spec/lib/gitlab/analytics/cycle_analytics/stage_events/stage_event_spec.rb b/spec/lib/gitlab/analytics/cycle_analytics/stage_events/stage_event_spec.rb new file mode 100644 index 00000000000..29f4be76a65 --- /dev/null +++ b/spec/lib/gitlab/analytics/cycle_analytics/stage_events/stage_event_spec.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Analytics::CycleAnalytics::StageEvents::StageEvent do + it { expect(described_class).to respond_to(:name) } + it { expect(described_class).to respond_to(:identifier) } + + it { expect(described_class.new({})).to respond_to(:object_type) } +end diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb index edff38f05ec..098c33f9cb1 100644 --- a/spec/lib/gitlab/auth_spec.rb +++ b/spec/lib/gitlab/auth_spec.rb @@ -86,7 +86,7 @@ describe Gitlab::Auth do let(:project) { build.project } before do - expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: 'gitlab-ci-token') + expect(gl_auth).not_to receive(:rate_limit!).with('ip', success: true, login: 'gitlab-ci-token') end it 'recognises user-less build' do @@ -106,7 +106,7 @@ describe Gitlab::Auth do let(:project) { build.project } before do - expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: 'gitlab-ci-token') + expect(gl_auth).not_to receive(:rate_limit!).with('ip', success: false, login: 'gitlab-ci-token') end it 'denies authentication' do diff --git a/spec/lib/gitlab/background_migration/legacy_upload_mover_spec.rb b/spec/lib/gitlab/background_migration/legacy_upload_mover_spec.rb new file mode 100644 index 00000000000..7d67dc0251d --- /dev/null +++ b/spec/lib/gitlab/background_migration/legacy_upload_mover_spec.rb @@ -0,0 +1,296 @@ +# frozen_string_literal: true +require 'spec_helper' + +# rubocop: disable RSpec/FactoriesInMigrationSpecs +describe Gitlab::BackgroundMigration::LegacyUploadMover do + let(:test_dir) { FileUploader.options['storage_path'] } + let(:filename) { 'image.png' } + + let!(:namespace) { create(:namespace) } + let!(:legacy_project) { create(:project, :legacy_storage, namespace: namespace) } + let!(:hashed_project) { create(:project, namespace: namespace) } + # default project + let(:project) { legacy_project } + + let!(:issue) { create(:issue, project: project) } + let!(:note) { create(:note, note: 'some note', project: project, noteable: issue) } + + let(:legacy_upload) { create_upload(note, filename) } + + def create_remote_upload(model, filename) + create(:upload, :attachment_upload, + path: "note/attachment/#{model.id}/#{filename}", secret: nil, + store: ObjectStorage::Store::REMOTE, model: model) + end + + def create_upload(model, filename, with_file = true) + params = { + path: "uploads/-/system/note/attachment/#{model.id}/#{filename}", + model: model, + store: ObjectStorage::Store::LOCAL + } + + if with_file + upload = create(:upload, :with_file, :attachment_upload, params) + model.update(attachment: upload.build_uploader) + model.attachment.upload + else + create(:upload, :attachment_upload, params) + end + end + + def new_upload + Upload.find_by(model_id: project.id, model_type: 'Project') + end + + def expect_error_log + expect_next_instance_of(Gitlab::BackgroundMigration::Logger) do |logger| + expect(logger).to receive(:warn) + end + end + + shared_examples 'legacy upload deletion' do + it 'removes the upload record' do + described_class.new(legacy_upload).execute + + expect { legacy_upload.reload }.to raise_error(ActiveRecord::RecordNotFound) + end + end + + shared_examples 'move error' do + it 'does not remove the upload file' do + expect_error_log + + described_class.new(legacy_upload).execute + + expect(legacy_upload.reload).to eq(legacy_upload) + end + end + + shared_examples 'migrates the file correctly' do + before do + described_class.new(legacy_upload).execute + end + + it 'creates a new uplaod record correctly' do + expect(new_upload.secret).not_to be_nil + expect(new_upload.path).to end_with("#{new_upload.secret}/image.png") + expect(new_upload.model_id).to eq(project.id) + expect(new_upload.model_type).to eq('Project') + expect(new_upload.uploader).to eq('FileUploader') + end + + it 'updates the legacy upload note so that it references the file in the markdown' do + expected_path = File.join('/uploads', new_upload.secret, 'image.png') + expected_markdown = "some note \n ![image](#{expected_path})" + expect(note.reload.note).to eq(expected_markdown) + end + + it 'removes the attachment from the note model' do + expect(note.reload.attachment.file).to be_nil + end + end + + context 'when no model found for the upload' do + before do + legacy_upload.model = nil + expect_error_log + end + + it_behaves_like 'legacy upload deletion' + end + + context 'when the upload move fails' do + before do + expect(FileUploader).to receive(:copy_to).and_raise('failed') + end + + it_behaves_like 'move error' + end + + context 'when the upload is in local storage' do + shared_examples 'legacy local file' do + it 'removes the file correctly' do + expect(File.exist?(legacy_upload.absolute_path)).to be_truthy + + described_class.new(legacy_upload).execute + + expect(File.exist?(legacy_upload.absolute_path)).to be_falsey + end + + it 'moves legacy uploads to the correct location' do + described_class.new(legacy_upload).execute + + expected_path = File.join(test_dir, 'uploads', project.disk_path, new_upload.secret, filename) + expect(File.exist?(expected_path)).to be_truthy + end + end + + context 'when the upload file does not exist on the filesystem' do + let(:legacy_upload) { create_upload(note, filename, false) } + + before do + expect_error_log + end + + it_behaves_like 'legacy upload deletion' + end + + context 'when an upload belongs to a legacy_diff_note' do + let!(:merge_request) { create(:merge_request, source_project: project) } + let!(:note) do + create(:legacy_diff_note_on_merge_request, + note: 'some note', project: project, noteable: merge_request) + end + let(:legacy_upload) do + create(:upload, :with_file, :attachment_upload, + path: "uploads/-/system/note/attachment/#{note.id}/#{filename}", model: note) + end + + context 'when the file does not exist for the upload' do + let(:legacy_upload) do + create(:upload, :attachment_upload, + path: "uploads/-/system/note/attachment/#{note.id}/#{filename}", model: note) + end + + it_behaves_like 'move error' + end + + context 'when the file does not exist on expected path' do + let(:legacy_upload) do + create(:upload, :attachment_upload, :with_file, + path: "uploads/-/system/note/attachment/some_part/#{note.id}/#{filename}", model: note) + end + + it_behaves_like 'move error' + end + + context 'when the file path does not include system/note/attachment' do + let(:legacy_upload) do + create(:upload, :attachment_upload, :with_file, + path: "uploads/-/system#{note.id}/#{filename}", model: note) + end + + it_behaves_like 'move error' + end + + context 'when the file move raises an error' do + before do + allow(FileUtils).to receive(:mv).and_raise(Errno::EACCES) + end + + it_behaves_like 'move error' + end + + context 'when the file can be handled correctly' do + it_behaves_like 'migrates the file correctly' + it_behaves_like 'legacy local file' + it_behaves_like 'legacy upload deletion' + end + end + + context 'when object storage is disabled for FileUploader' do + context 'when the file belongs to a legacy project' do + let(:project) { legacy_project } + + it_behaves_like 'migrates the file correctly' + it_behaves_like 'legacy local file' + it_behaves_like 'legacy upload deletion' + end + + context 'when the file belongs to a hashed project' do + let(:project) { hashed_project } + + it_behaves_like 'migrates the file correctly' + it_behaves_like 'legacy local file' + it_behaves_like 'legacy upload deletion' + end + end + + context 'when object storage is enabled for FileUploader' do + # The process of migrating to object storage is a manual one, + # so it would go against expectations to automatically migrate these files + # to object storage during this migration. + # After this migration, these files should be able to successfully migrate to object storage. + + before do + stub_uploads_object_storage(FileUploader) + end + + context 'when the file belongs to a legacy project' do + let(:project) { legacy_project } + + it_behaves_like 'migrates the file correctly' + it_behaves_like 'legacy local file' + it_behaves_like 'legacy upload deletion' + end + + context 'when the file belongs to a hashed project' do + let(:project) { hashed_project } + + it_behaves_like 'migrates the file correctly' + it_behaves_like 'legacy local file' + it_behaves_like 'legacy upload deletion' + end + end + end + + context 'when legacy uploads are stored in object storage' do + let(:legacy_upload) { create_remote_upload(note, filename) } + let(:remote_file) do + { key: "#{legacy_upload.path}" } + end + let(:connection) { ::Fog::Storage.new(FileUploader.object_store_credentials) } + let(:bucket) { connection.directories.create(key: 'uploads') } + + before do + stub_uploads_object_storage(FileUploader) + end + + shared_examples 'legacy remote file' do + it 'removes the file correctly' do + # expect(bucket.files.get(remote_file[:key])).to be_nil + + described_class.new(legacy_upload).execute + + expect(bucket.files.get(remote_file[:key])).to be_nil + end + + it 'moves legacy uploads to the correct remote location' do + described_class.new(legacy_upload).execute + + connection = ::Fog::Storage.new(FileUploader.object_store_credentials) + expect(connection.get_object('uploads', new_upload.path)[:status]).to eq(200) + end + end + + context 'when the upload file does not exist on the filesystem' do + it_behaves_like 'legacy upload deletion' + end + + context 'when the file belongs to a legacy project' do + before do + bucket.files.create(remote_file) + end + + let(:project) { legacy_project } + + it_behaves_like 'migrates the file correctly' + it_behaves_like 'legacy remote file' + it_behaves_like 'legacy upload deletion' + end + + context 'when the file belongs to a hashed project' do + before do + bucket.files.create(remote_file) + end + + let(:project) { hashed_project } + + it_behaves_like 'migrates the file correctly' + it_behaves_like 'legacy remote file' + it_behaves_like 'legacy upload deletion' + end + end +end +# rubocop: enable RSpec/FactoriesInMigrationSpecs diff --git a/spec/lib/gitlab/background_migration/legacy_uploads_migrator_spec.rb b/spec/lib/gitlab/background_migration/legacy_uploads_migrator_spec.rb new file mode 100644 index 00000000000..ed8cbfeb11f --- /dev/null +++ b/spec/lib/gitlab/background_migration/legacy_uploads_migrator_spec.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true +require 'spec_helper' + +# rubocop: disable RSpec/FactoriesInMigrationSpecs +describe Gitlab::BackgroundMigration::LegacyUploadsMigrator do + let(:test_dir) { FileUploader.options['storage_path'] } + + let!(:hashed_project) { create(:project) } + let!(:legacy_project) { create(:project, :legacy_storage) } + let!(:issue) { create(:issue, project: hashed_project) } + let!(:issue_legacy) { create(:issue, project: legacy_project) } + + let!(:note1) { create(:note, project: hashed_project, noteable: issue) } + let!(:note2) { create(:note, project: hashed_project, noteable: issue) } + let!(:note_legacy) { create(:note, project: legacy_project, noteable: issue_legacy) } + + def create_upload(model, with_file = true) + filename = 'image.png' + params = { + path: "uploads/-/system/note/attachment/#{model.id}/#{filename}", + model: model, + store: ObjectStorage::Store::LOCAL + } + + if with_file + upload = create(:upload, :with_file, :attachment_upload, params) + model.update(attachment: upload.build_uploader) + model.attachment.upload + else + create(:upload, :attachment_upload, params) + end + end + + let!(:legacy_upload) { create_upload(note1) } + let!(:legacy_upload_no_file) { create_upload(note2, false) } + let!(:legacy_upload_legacy_project) { create_upload(note_legacy) } + + let(:start_id) { 1 } + let(:end_id) { 10000 } + + subject { described_class.new.perform(start_id, end_id) } + + it 'removes all legacy files' do + expect(File.exist?(legacy_upload.absolute_path)).to be_truthy + expect(File.exist?(legacy_upload_no_file.absolute_path)).to be_falsey + expect(File.exist?(legacy_upload_legacy_project.absolute_path)).to be_truthy + + subject + + expect(File.exist?(legacy_upload.absolute_path)).to be_falsey + expect(File.exist?(legacy_upload_no_file.absolute_path)).to be_falsey + expect(File.exist?(legacy_upload_legacy_project.absolute_path)).to be_falsey + end + + it 'removes all AttachmentUploader records' do + expect { subject }.to change { Upload.where(uploader: 'AttachmentUploader').count }.from(3).to(0) + end + + it 'creates new uploads for successfully migrated records' do + expect { subject }.to change { Upload.where(uploader: 'FileUploader').count }.from(0).to(2) + end +end +# rubocop: enable RSpec/FactoriesInMigrationSpecs diff --git a/spec/lib/gitlab/ci/build/policy/variables_spec.rb b/spec/lib/gitlab/ci/build/policy/variables_spec.rb index f712f47a558..7140c14facb 100644 --- a/spec/lib/gitlab/ci/build/policy/variables_spec.rb +++ b/spec/lib/gitlab/ci/build/policy/variables_spec.rb @@ -13,7 +13,12 @@ describe Gitlab::Ci::Build::Policy::Variables do build(:ci_build, pipeline: pipeline, project: project, ref: 'master') end - let(:seed) { double('build seed', to_resource: ci_build) } + let(:seed) do + double('build seed', + to_resource: ci_build, + scoped_variables_hash: ci_build.scoped_variables_hash + ) + end before do pipeline.variables.build(key: 'CI_PROJECT_NAME', value: '') @@ -83,7 +88,12 @@ describe Gitlab::Ci::Build::Policy::Variables do build(:ci_bridge, pipeline: pipeline, project: project, ref: 'master') end - let(:seed) { double('bridge seed', to_resource: bridge) } + let(:seed) do + double('bridge seed', + to_resource: bridge, + scoped_variables_hash: ci_build.scoped_variables_hash + ) + end it 'is satisfied by a matching expression for a bridge job' do policy = described_class.new(['$MY_VARIABLE']) diff --git a/spec/lib/gitlab/ci/build/rules/rule_spec.rb b/spec/lib/gitlab/ci/build/rules/rule_spec.rb new file mode 100644 index 00000000000..99852bd4228 --- /dev/null +++ b/spec/lib/gitlab/ci/build/rules/rule_spec.rb @@ -0,0 +1,50 @@ +require 'spec_helper' + +describe Gitlab::Ci::Build::Rules::Rule do + let(:seed) do + double('build seed', + to_resource: ci_build, + scoped_variables_hash: ci_build.scoped_variables_hash + ) + end + + let(:pipeline) { create(:ci_pipeline) } + let(:ci_build) { build(:ci_build, pipeline: pipeline) } + let(:rule) { described_class.new(rule_hash) } + + describe '#matches?' do + subject { rule.matches?(pipeline, seed) } + + context 'with one matching clause' do + let(:rule_hash) do + { if: '$VAR == null', when: 'always' } + end + + it { is_expected.to eq(true) } + end + + context 'with two matching clauses' do + let(:rule_hash) do + { if: '$VAR == null', changes: '**/*', when: 'always' } + end + + it { is_expected.to eq(true) } + end + + context 'with a matching and non-matching clause' do + let(:rule_hash) do + { if: '$VAR != null', changes: '$VAR == null', when: 'always' } + end + + it { is_expected.to eq(false) } + end + + context 'with two non-matching clauses' do + let(:rule_hash) do + { if: '$VAR != null', changes: 'README', when: 'always' } + end + + it { is_expected.to eq(false) } + end + end +end diff --git a/spec/lib/gitlab/ci/build/rules_spec.rb b/spec/lib/gitlab/ci/build/rules_spec.rb new file mode 100644 index 00000000000..d7793ebc806 --- /dev/null +++ b/spec/lib/gitlab/ci/build/rules_spec.rb @@ -0,0 +1,168 @@ +require 'spec_helper' + +describe Gitlab::Ci::Build::Rules do + let(:pipeline) { create(:ci_pipeline) } + let(:ci_build) { build(:ci_build, pipeline: pipeline) } + + let(:seed) do + double('build seed', + to_resource: ci_build, + scoped_variables_hash: ci_build.scoped_variables_hash + ) + end + + let(:rules) { described_class.new(rule_list) } + + describe '.new' do + let(:rules_ivar) { rules.instance_variable_get :@rule_list } + let(:default_when) { rules.instance_variable_get :@default_when } + + context 'with no rules' do + let(:rule_list) { [] } + + it 'sets @rule_list to an empty array' do + expect(rules_ivar).to eq([]) + end + + it 'sets @default_when to "on_success"' do + expect(default_when).to eq('on_success') + end + end + + context 'with one rule' do + let(:rule_list) { [{ if: '$VAR == null', when: 'always' }] } + + it 'sets @rule_list to an array of a single rule' do + expect(rules_ivar).to be_an(Array) + end + + it 'sets @default_when to "on_success"' do + expect(default_when).to eq('on_success') + end + end + + context 'with multiple rules' do + let(:rule_list) do + [ + { if: '$VAR == null', when: 'always' }, + { if: '$VAR == null', when: 'always' } + ] + end + + it 'sets @rule_list to an array of a single rule' do + expect(rules_ivar).to be_an(Array) + end + + it 'sets @default_when to "on_success"' do + expect(default_when).to eq('on_success') + end + end + + context 'with a specified default when:' do + let(:rule_list) { [{ if: '$VAR == null', when: 'always' }] } + let(:rules) { described_class.new(rule_list, 'manual') } + + it 'sets @rule_list to an array of a single rule' do + expect(rules_ivar).to be_an(Array) + end + + it 'sets @default_when to "manual"' do + expect(default_when).to eq('manual') + end + end + end + + describe '#evaluate' do + subject { rules.evaluate(pipeline, seed) } + + context 'with nil rules' do + let(:rule_list) { nil } + + it { is_expected.to eq(described_class::Result.new('on_success')) } + + context 'and when:manual set as the default' do + let(:rules) { described_class.new(rule_list, 'manual') } + + it { is_expected.to eq(described_class::Result.new('manual')) } + end + end + + context 'with no rules' do + let(:rule_list) { [] } + + it { is_expected.to eq(described_class::Result.new('never')) } + + context 'and when:manual set as the default' do + let(:rules) { described_class.new(rule_list, 'manual') } + + it { is_expected.to eq(described_class::Result.new('never')) } + end + end + + context 'with one rule without any clauses' do + let(:rule_list) { [{ when: 'manual' }] } + + it { is_expected.to eq(described_class::Result.new('manual')) } + end + + context 'with one matching rule' do + let(:rule_list) { [{ if: '$VAR == null', when: 'always' }] } + + it { is_expected.to eq(described_class::Result.new('always')) } + end + + context 'with two matching rules' do + let(:rule_list) do + [ + { if: '$VAR == null', when: 'delayed', start_in: '1 day' }, + { if: '$VAR == null', when: 'always' } + ] + end + + it 'returns the value of the first matched rule in the list' do + expect(subject).to eq(described_class::Result.new('delayed', '1 day')) + end + end + + context 'with a non-matching and matching rule' do + let(:rule_list) do + [ + { if: '$VAR =! null', when: 'delayed', start_in: '1 day' }, + { if: '$VAR == null', when: 'always' } + ] + end + + it { is_expected.to eq(described_class::Result.new('always')) } + end + + context 'with a matching and non-matching rule' do + let(:rule_list) do + [ + { if: '$VAR == null', when: 'delayed', start_in: '1 day' }, + { if: '$VAR != null', when: 'always' } + ] + end + + it { is_expected.to eq(described_class::Result.new('delayed', '1 day')) } + end + + context 'with non-matching rules' do + let(:rule_list) do + [ + { if: '$VAR != null', when: 'delayed', start_in: '1 day' }, + { if: '$VAR != null', when: 'always' } + ] + end + + it { is_expected.to eq(described_class::Result.new('never')) } + + context 'and when:manual set as the default' do + let(:rules) { described_class.new(rule_list, 'manual') } + + it 'does not return the default when:' do + expect(subject).to eq(described_class::Result.new('never')) + end + end + end + end +end diff --git a/spec/lib/gitlab/ci/config/entry/job_spec.rb b/spec/lib/gitlab/ci/config/entry/job_spec.rb index 800ef122203..1853efde350 100644 --- a/spec/lib/gitlab/ci/config/entry/job_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/job_spec.rb @@ -11,7 +11,7 @@ describe Gitlab::Ci::Config::Entry::Job do let(:result) do %i[before_script script stage type after_script cache - image services only except variables artifacts + image services only except rules variables artifacts environment coverage retry] end @@ -89,14 +89,23 @@ describe Gitlab::Ci::Config::Entry::Job do context 'when has needs' do let(:config) do - { script: 'echo', needs: ['another-job'] } + { + stage: 'test', + script: 'echo', + needs: ['another-job'] + } end it { expect(entry).to be_valid } context 'when has dependencies' do let(:config) do - { script: 'echo', dependencies: ['another-job'], needs: ['another-job'] } + { + stage: 'test', + script: 'echo', + dependencies: ['another-job'], + needs: ['another-job'] + } end it { expect(entry).to be_valid } @@ -192,6 +201,21 @@ describe Gitlab::Ci::Config::Entry::Job do expect(entry.errors).to include 'job parallel must be an integer' end end + + context 'when it uses both "when:" and "rules:"' do + let(:config) do + { + script: 'echo', + when: 'on_failure', + rules: [{ if: '$VARIABLE', when: 'on_success' }] + } + end + + it 'returns an error about when: being combined with rules' do + expect(entry).not_to be_valid + expect(entry.errors).to include 'job config key may not be used with `rules`: when' + end + end end context 'when delayed job' do @@ -231,6 +255,100 @@ describe Gitlab::Ci::Config::Entry::Job do end end + context 'when only: is used with rules:' do + let(:config) { { only: ['merge_requests'], rules: [{ if: '$THIS' }] } } + + it 'returns error about mixing only: with rules:' do + expect(entry).not_to be_valid + expect(entry.errors).to include /may not be used with `rules`/ + end + + context 'and only: is blank' do + let(:config) { { only: nil, rules: [{ if: '$THIS' }] } } + + it 'returns error about mixing only: with rules:' do + expect(entry).not_to be_valid + expect(entry.errors).to include /may not be used with `rules`/ + end + end + + context 'and rules: is blank' do + let(:config) { { only: ['merge_requests'], rules: nil } } + + it 'returns error about mixing only: with rules:' do + expect(entry).not_to be_valid + expect(entry.errors).to include /may not be used with `rules`/ + end + end + end + + context 'when except: is used with rules:' do + let(:config) { { except: { refs: %w[master] }, rules: [{ if: '$THIS' }] } } + + it 'returns error about mixing except: with rules:' do + expect(entry).not_to be_valid + expect(entry.errors).to include /may not be used with `rules`/ + end + + context 'and except: is blank' do + let(:config) { { except: nil, rules: [{ if: '$THIS' }] } } + + it 'returns error about mixing except: with rules:' do + expect(entry).not_to be_valid + expect(entry.errors).to include /may not be used with `rules`/ + end + end + + context 'and rules: is blank' do + let(:config) { { except: { refs: %w[master] }, rules: nil } } + + it 'returns error about mixing except: with rules:' do + expect(entry).not_to be_valid + expect(entry.errors).to include /may not be used with `rules`/ + end + end + end + + context 'when only: and except: are both used with rules:' do + let(:config) do + { + only: %w[merge_requests], + except: { refs: %w[master] }, + rules: [{ if: '$THIS' }] + } + end + + it 'returns errors about mixing both only: and except: with rules:' do + expect(entry).not_to be_valid + expect(entry.errors).to include /may not be used with `rules`/ + expect(entry.errors).to include /may not be used with `rules`/ + end + + context 'when only: and except: as both blank' do + let(:config) do + { only: nil, except: nil, rules: [{ if: '$THIS' }] } + end + + it 'returns errors about mixing both only: and except: with rules:' do + expect(entry).not_to be_valid + expect(entry.errors).to include /may not be used with `rules`/ + expect(entry.errors).to include /may not be used with `rules`/ + end + end + + context 'when rules: is blank' do + let(:config) do + { only: %w[merge_requests], except: { refs: %w[master] }, rules: nil } + end + + it 'returns errors about mixing both only: and except: with rules:' do + expect(entry).not_to be_valid + expect(entry.errors).to include /may not be used with `rules`/ + expect(entry.errors).to include /may not be used with `rules`/ + end + end + end + context 'when start_in specified without delayed specification' do let(:config) { { start_in: '1 day' } } @@ -256,7 +374,11 @@ describe Gitlab::Ci::Config::Entry::Job do context 'when has needs' do context 'that are not a array of strings' do let(:config) do - { script: 'echo', needs: 'build-job' } + { + stage: 'test', + script: 'echo', + needs: 'build-job' + } end it 'returns error about invalid type' do @@ -267,7 +389,12 @@ describe Gitlab::Ci::Config::Entry::Job do context 'when have dependencies that are not subset of needs' do let(:config) do - { script: 'echo', dependencies: ['another-job'], needs: ['build-job'] } + { + stage: 'test', + script: 'echo', + dependencies: ['another-job'], + needs: ['build-job'] + } end it 'returns error about invalid data' do @@ -275,6 +402,20 @@ describe Gitlab::Ci::Config::Entry::Job do expect(entry.errors).to include 'job dependencies the another-job should be part of needs' end end + + context 'when stage: is missing' do + let(:config) do + { + script: 'echo', + needs: ['build-job'] + } + end + + it 'returns error about invalid data' do + expect(entry).not_to be_valid + expect(entry.errors).to include 'job config missing required keys: stage' + end + end end end end diff --git a/spec/lib/gitlab/ci/config/entry/policy_spec.rb b/spec/lib/gitlab/ci/config/entry/policy_spec.rb index 266a27c1e47..a606eb303e7 100644 --- a/spec/lib/gitlab/ci/config/entry/policy_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/policy_spec.rb @@ -51,8 +51,6 @@ describe Gitlab::Ci::Config::Entry::Policy do let(:config) { ['/^(?!master).+/'] } - subject { described_class.new([regexp]) } - context 'when allow_unsafe_ruby_regexp is disabled' do before do stub_feature_flags(allow_unsafe_ruby_regexp: false) @@ -113,8 +111,6 @@ describe Gitlab::Ci::Config::Entry::Policy do let(:config) { { refs: ['/^(?!master).+/'] } } - subject { described_class.new([regexp]) } - context 'when allow_unsafe_ruby_regexp is disabled' do before do stub_feature_flags(allow_unsafe_ruby_regexp: false) @@ -204,6 +200,14 @@ describe Gitlab::Ci::Config::Entry::Policy do end context 'when changes policy is invalid' do + let(:config) { { changes: 'some/*' } } + + it 'returns errors' do + expect(entry.errors).to include /changes should be an array of strings/ + end + end + + context 'when changes policy is invalid' do let(:config) { { changes: [1, 2] } } it 'returns errors' do diff --git a/spec/lib/gitlab/ci/config/entry/rules/rule_spec.rb b/spec/lib/gitlab/ci/config/entry/rules/rule_spec.rb new file mode 100644 index 00000000000..c25344ec1a4 --- /dev/null +++ b/spec/lib/gitlab/ci/config/entry/rules/rule_spec.rb @@ -0,0 +1,208 @@ +require 'fast_spec_helper' +require 'chronic_duration' +require 'support/helpers/stub_feature_flags' +require_dependency 'active_model' + +describe Gitlab::Ci::Config::Entry::Rules::Rule do + let(:entry) { described_class.new(config) } + + describe '.new' do + subject { entry } + + context 'with a when: value but no clauses' do + let(:config) { { when: 'manual' } } + + it { is_expected.to be_valid } + end + + context 'when specifying an if: clause' do + let(:config) { { if: '$THIS || $THAT', when: 'manual' } } + + it { is_expected.to be_valid } + + describe '#when' do + subject { entry.when } + + it { is_expected.to eq('manual') } + end + end + + context 'using a list of multiple expressions' do + let(:config) { { if: ['$MY_VAR == "this"', '$YOUR_VAR == "that"'] } } + + it { is_expected.not_to be_valid } + + it 'reports an error about invalid format' do + expect(subject.errors).to include(/invalid expression syntax/) + end + end + + context 'when specifying an invalid if: clause expression' do + let(:config) { { if: ['$MY_VAR =='] } } + + it { is_expected.not_to be_valid } + + it 'reports an error about invalid statement' do + expect(subject.errors).to include(/invalid expression syntax/) + end + end + + context 'when specifying an if: clause expression with an invalid token' do + let(:config) { { if: ['$MY_VAR == 123'] } } + + it { is_expected.not_to be_valid } + + it 'reports an error about invalid statement' do + expect(subject.errors).to include(/invalid expression syntax/) + end + end + + context 'when using invalid regex in an if: clause' do + let(:config) { { if: ['$MY_VAR =~ /some ( thing/'] } } + + it 'reports an error about invalid expression' do + expect(subject.errors).to include(/invalid expression syntax/) + end + end + + context 'when using an if: clause with lookahead regex character "?"' do + let(:config) { { if: '$CI_COMMIT_REF =~ /^(?!master).+/' } } + + context 'when allow_unsafe_ruby_regexp is disabled' do + it { is_expected.not_to be_valid } + + it 'reports an error about invalid expression syntax' do + expect(subject.errors).to include(/invalid expression syntax/) + end + end + end + + context 'when using a changes: clause' do + let(:config) { { changes: %w[app/ lib/ spec/ other/* paths/**/*.rb] } } + + it { is_expected.to be_valid } + end + + context 'when using a string as an invalid changes: clause' do + let(:config) { { changes: 'a regular string' } } + + it { is_expected.not_to be_valid } + + it 'reports an error about invalid policy' do + expect(subject.errors).to include(/should be an array of strings/) + end + end + + context 'when using a list as an invalid changes: clause' do + let(:config) { { changes: [1, 2] } } + + it { is_expected.not_to be_valid } + + it 'returns errors' do + expect(subject.errors).to include(/changes should be an array of strings/) + end + end + + context 'specifying a delayed job' do + let(:config) { { if: '$THIS || $THAT', when: 'delayed', start_in: '15 minutes' } } + + it { is_expected.to be_valid } + + it 'sets attributes for the job delay' do + expect(entry.when).to eq('delayed') + expect(entry.start_in).to eq('15 minutes') + end + + context 'without a when: key' do + let(:config) { { if: '$THIS || $THAT', start_in: '15 minutes' } } + + it { is_expected.not_to be_valid } + + it 'returns an error about the disallowed key' do + expect(entry.errors).to include(/disallowed keys: start_in/) + end + end + + context 'without a start_in: key' do + let(:config) { { if: '$THIS || $THAT', when: 'delayed' } } + + it { is_expected.not_to be_valid } + + it 'returns an error about tstart_in being blank' do + expect(entry.errors).to include(/start in can't be blank/) + end + end + end + + context 'when specifying unknown policy' do + let(:config) { { invalid: :something } } + + it { is_expected.not_to be_valid } + + it 'returns error about invalid key' do + expect(entry.errors).to include(/unknown keys: invalid/) + end + end + + context 'when clause is empty' do + let(:config) { {} } + + it { is_expected.not_to be_valid } + + it 'is not a valid configuration' do + expect(entry.errors).to include(/can't be blank/) + end + end + + context 'when policy strategy does not match' do + let(:config) { 'string strategy' } + + it { is_expected.not_to be_valid } + + it 'returns information about errors' do + expect(entry.errors) + .to include(/should be a hash/) + end + end + end + + describe '#value' do + subject { entry.value } + + context 'when specifying an if: clause' do + let(:config) { { if: '$THIS || $THAT', when: 'manual' } } + + it 'stores the expression as "if"' do + expect(subject).to eq(if: '$THIS || $THAT', when: 'manual') + end + end + + context 'when using a changes: clause' do + let(:config) { { changes: %w[app/ lib/ spec/ other/* paths/**/*.rb] } } + + it { is_expected.to eq(config) } + end + + context 'when default value has been provided' do + let(:config) { { changes: %w[app/**/*.rb] } } + + before do + entry.default = { changes: %w[**/*] } + end + + it 'does not set a default value' do + expect(entry.default).to eq(nil) + end + + it 'does not add to provided configuration' do + expect(entry.value).to eq(config) + end + end + end + + describe '.default' do + it 'does not have default value' do + expect(described_class.default).to be_nil + end + end +end diff --git a/spec/lib/gitlab/ci/config/entry/rules_spec.rb b/spec/lib/gitlab/ci/config/entry/rules_spec.rb new file mode 100644 index 00000000000..291e7373daf --- /dev/null +++ b/spec/lib/gitlab/ci/config/entry/rules_spec.rb @@ -0,0 +1,135 @@ +require 'fast_spec_helper' +require 'support/helpers/stub_feature_flags' +require_dependency 'active_model' + +describe Gitlab::Ci::Config::Entry::Rules do + let(:entry) { described_class.new(config) } + + describe '.new' do + subject { entry } + + context 'with a list of rule rule' do + let(:config) do + [{ if: '$THIS == "that"', when: 'never' }] + end + + it { is_expected.to be_a(described_class) } + it { is_expected.to be_valid } + + context 'after #compose!' do + before do + subject.compose! + end + + it { is_expected.to be_valid } + end + end + + context 'with a list of two rules' do + let(:config) do + [ + { if: '$THIS == "that"', when: 'always' }, + { if: '$SKIP', when: 'never' } + ] + end + + it { is_expected.to be_a(described_class) } + it { is_expected.to be_valid } + + context 'after #compose!' do + before do + subject.compose! + end + + it { is_expected.to be_valid } + end + end + + context 'with a single rule object' do + let(:config) do + { if: '$SKIP', when: 'never' } + end + + it { is_expected.not_to be_valid } + end + + context 'with an invalid boolean when:' do + let(:config) do + [{ if: '$THIS == "that"', when: false }] + end + + it { is_expected.to be_a(described_class) } + it { is_expected.to be_valid } + + context 'after #compose!' do + before do + subject.compose! + end + + it { is_expected.not_to be_valid } + + it 'returns an error about invalid when:' do + expect(subject.errors).to include(/when unknown value: false/) + end + end + end + + context 'with an invalid string when:' do + let(:config) do + [{ if: '$THIS == "that"', when: 'explode' }] + end + + it { is_expected.to be_a(described_class) } + it { is_expected.to be_valid } + + context 'after #compose!' do + before do + subject.compose! + end + + it { is_expected.not_to be_valid } + + it 'returns an error about invalid when:' do + expect(subject.errors).to include(/when unknown value: explode/) + end + end + end + end + + describe '#value' do + subject { entry.value } + + context 'with a list of rule rule' do + let(:config) do + [{ if: '$THIS == "that"', when: 'never' }] + end + + it { is_expected.to eq(config) } + end + + context 'with a list of two rules' do + let(:config) do + [ + { if: '$THIS == "that"', when: 'always' }, + { if: '$SKIP', when: 'never' } + ] + end + + it { is_expected.to eq(config) } + end + + context 'with a single rule object' do + let(:config) do + { if: '$SKIP', when: 'never' } + end + + it { is_expected.to eq(config) } + end + end + + describe '.default' do + it 'does not have default policy' do + expect(described_class.default).to be_nil + end + end +end diff --git a/spec/lib/gitlab/ci/pipeline/expression/lexeme/matches_spec.rb b/spec/lib/gitlab/ci/pipeline/expression/lexeme/matches_spec.rb index 4e4f1bf6ad3..a527783ffac 100644 --- a/spec/lib/gitlab/ci/pipeline/expression/lexeme/matches_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/expression/lexeme/matches_spec.rb @@ -69,6 +69,34 @@ describe Gitlab::Ci::Pipeline::Expression::Lexeme::Matches do it { is_expected.to eq(false) } end + context 'when right is nil' do + let(:left_value) { 'my-awesome-string' } + let(:right_value) { nil } + + it { is_expected.to eq(false) } + end + + context 'when left and right are nil' do + let(:left_value) { nil } + let(:right_value) { nil } + + it { is_expected.to eq(false) } + end + + context 'when left is an empty string' do + let(:left_value) { '' } + let(:right_value) { Gitlab::UntrustedRegexp.new('pattern') } + + it { is_expected.to eq(false) } + end + + context 'when left and right are empty strings' do + let(:left_value) { '' } + let(:right_value) { Gitlab::UntrustedRegexp.new('') } + + it { is_expected.to eq(true) } + end + context 'when left is a multiline string and matches right' do let(:left_value) do <<~TEXT diff --git a/spec/lib/gitlab/ci/pipeline/expression/lexeme/not_matches_spec.rb b/spec/lib/gitlab/ci/pipeline/expression/lexeme/not_matches_spec.rb index 6b81008ffb1..fb4238ecaf3 100644 --- a/spec/lib/gitlab/ci/pipeline/expression/lexeme/not_matches_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/expression/lexeme/not_matches_spec.rb @@ -69,6 +69,34 @@ describe Gitlab::Ci::Pipeline::Expression::Lexeme::NotMatches do it { is_expected.to eq(true) } end + context 'when right is nil' do + let(:left_value) { 'my-awesome-string' } + let(:right_value) { nil } + + it { is_expected.to eq(true) } + end + + context 'when left and right are nil' do + let(:left_value) { nil } + let(:right_value) { nil } + + it { is_expected.to eq(true) } + end + + context 'when left is an empty string' do + let(:left_value) { '' } + let(:right_value) { Gitlab::UntrustedRegexp.new('pattern') } + + it { is_expected.to eq(true) } + end + + context 'when left and right are empty strings' do + let(:left_value) { '' } + let(:right_value) { Gitlab::UntrustedRegexp.new('') } + + it { is_expected.to eq(false) } + end + context 'when left is a multiline string and matches right' do let(:left_value) do <<~TEXT diff --git a/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb b/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb index 762025f9bd9..89431b80be3 100644 --- a/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb @@ -15,25 +15,95 @@ describe Gitlab::Ci::Pipeline::Seed::Build do it { is_expected.to be_a(Hash) } it { is_expected.to include(:name, :project, :ref) } + + context 'with job:when' do + let(:attributes) { { name: 'rspec', ref: 'master', when: 'on_failure' } } + + it { is_expected.to include(when: 'on_failure') } + end + + context 'with job:when:delayed' do + let(:attributes) { { name: 'rspec', ref: 'master', when: 'delayed', start_in: '3 hours' } } + + it { is_expected.to include(when: 'delayed', start_in: '3 hours') } + end + + context 'with job:rules:[when:]' do + context 'is matched' do + let(:attributes) { { name: 'rspec', ref: 'master', rules: [{ if: '$VAR == null', when: 'always' }] } } + + it { is_expected.to include(when: 'always') } + end + + context 'is not matched' do + let(:attributes) { { name: 'rspec', ref: 'master', rules: [{ if: '$VAR != null', when: 'always' }] } } + + it { is_expected.to include(when: 'never') } + end + end + + context 'with job:rules:[when:delayed]' do + context 'is matched' do + let(:attributes) { { name: 'rspec', ref: 'master', rules: [{ if: '$VAR == null', when: 'delayed', start_in: '3 hours' }] } } + + it { is_expected.to include(when: 'delayed', start_in: '3 hours') } + end + + context 'is not matched' do + let(:attributes) { { name: 'rspec', ref: 'master', rules: [{ if: '$VAR != null', when: 'delayed', start_in: '3 hours' }] } } + + it { is_expected.to include(when: 'never') } + end + end + + context 'with job:rules but no explicit when:' do + context 'is matched' do + let(:attributes) { { name: 'rspec', ref: 'master', rules: [{ if: '$VAR == null' }] } } + + it { is_expected.to include(when: 'on_success') } + end + + context 'is not matched' do + let(:attributes) { { name: 'rspec', ref: 'master', rules: [{ if: '$VAR != null' }] } } + + it { is_expected.to include(when: 'never') } + end + end end describe '#bridge?' do subject { seed_build.bridge? } - context 'when job is a bridge' do + context 'when job is a downstream bridge' do let(:attributes) do { name: 'rspec', ref: 'master', options: { trigger: 'my/project' } } end it { is_expected.to be_truthy } + + context 'when trigger definition is empty' do + let(:attributes) do + { name: 'rspec', ref: 'master', options: { trigger: '' } } + end + + it { is_expected.to be_falsey } + end end - context 'when trigger definition is empty' do + context 'when job is an upstream bridge' do let(:attributes) do - { name: 'rspec', ref: 'master', options: { trigger: '' } } + { name: 'rspec', ref: 'master', options: { bridge_needs: { pipeline: 'my/project' } } } end - it { is_expected.to be_falsey } + it { is_expected.to be_truthy } + + context 'when upstream definition is empty' do + let(:attributes) do + { name: 'rspec', ref: 'master', options: { bridge_needs: { pipeline: '' } } } + end + + it { is_expected.to be_falsey } + end end context 'when job is not a bridge' do @@ -350,9 +420,25 @@ describe Gitlab::Ci::Pipeline::Seed::Build do it { is_expected.not_to be_included } end + + context 'when using both only and except policies' do + let(:attributes) do + { + name: 'rspec', + only: { + refs: ["branches@#{pipeline.project_full_path}"] + }, + except: { + refs: ["branches@#{pipeline.project_full_path}"] + } + } + end + + it { is_expected.not_to be_included } + end end - context 'when repository path does not matches' do + context 'when repository path does not match' do context 'when using only' do let(:attributes) do { name: 'rspec', only: { refs: %w[branches@fork] } } @@ -381,22 +467,242 @@ describe Gitlab::Ci::Pipeline::Seed::Build do it { is_expected.not_to be_included } end end + + context 'using rules:' do + using RSpec::Parameterized + + let(:attributes) { { name: 'rspec', rules: rule_set } } + + context 'with a matching if: rule' do + context 'with an explicit `when: never`' do + where(:rule_set) do + [ + [[{ if: '$VARIABLE == null', when: 'never' }]], + [[{ if: '$VARIABLE == null', when: 'never' }, { if: '$VARIABLE == null', when: 'always' }]], + [[{ if: '$VARIABLE != "the wrong value"', when: 'never' }, { if: '$VARIABLE == null', when: 'always' }]] + ] + end + + with_them do + it { is_expected.not_to be_included } + + it 'correctly populates when:' do + expect(seed_build.attributes).to include(when: 'never') + end + end + end + + context 'with an explicit `when: always`' do + where(:rule_set) do + [ + [[{ if: '$VARIABLE == null', when: 'always' }]], + [[{ if: '$VARIABLE == null', when: 'always' }, { if: '$VARIABLE == null', when: 'never' }]], + [[{ if: '$VARIABLE != "the wrong value"', when: 'always' }, { if: '$VARIABLE == null', when: 'never' }]] + ] + end + + with_them do + it { is_expected.to be_included } + + it 'correctly populates when:' do + expect(seed_build.attributes).to include(when: 'always') + end + end + end + + context 'with an explicit `when: on_failure`' do + where(:rule_set) do + [ + [[{ if: '$CI_JOB_NAME == "rspec" && $VAR == null', when: 'on_failure' }]], + [[{ if: '$VARIABLE != null', when: 'delayed', start_in: '1 day' }, { if: '$CI_JOB_NAME == "rspec"', when: 'on_failure' }]], + [[{ if: '$VARIABLE == "the wrong value"', when: 'delayed', start_in: '1 day' }, { if: '$CI_BUILD_NAME == "rspec"', when: 'on_failure' }]] + ] + end + + with_them do + it { is_expected.to be_included } + + it 'correctly populates when:' do + expect(seed_build.attributes).to include(when: 'on_failure') + end + end + end + + context 'with an explicit `when: delayed`' do + where(:rule_set) do + [ + [[{ if: '$VARIABLE == null', when: 'delayed', start_in: '1 day' }]], + [[{ if: '$VARIABLE == null', when: 'delayed', start_in: '1 day' }, { if: '$VARIABLE == null', when: 'never' }]], + [[{ if: '$VARIABLE != "the wrong value"', when: 'delayed', start_in: '1 day' }, { if: '$VARIABLE == null', when: 'never' }]] + ] + end + + with_them do + it { is_expected.to be_included } + + it 'correctly populates when:' do + expect(seed_build.attributes).to include(when: 'delayed', start_in: '1 day') + end + end + end + + context 'without an explicit when: value' do + where(:rule_set) do + [ + [[{ if: '$VARIABLE == null' }]], + [[{ if: '$VARIABLE == null' }, { if: '$VARIABLE == null' }]], + [[{ if: '$VARIABLE != "the wrong value"' }, { if: '$VARIABLE == null' }]] + ] + end + + with_them do + it { is_expected.to be_included } + + it 'correctly populates when:' do + expect(seed_build.attributes).to include(when: 'on_success') + end + end + end + end + + context 'with a matching changes: rule' do + let(:pipeline) do + create(:ci_pipeline, project: project).tap do |pipeline| + stub_pipeline_modified_paths(pipeline, %w[app/models/ci/pipeline.rb spec/models/ci/pipeline_spec.rb .gitlab-ci.yml]) + end + end + + context 'with an explicit `when: never`' do + where(:rule_set) do + [ + [[{ changes: %w[*/**/*.rb], when: 'never' }, { changes: %w[*/**/*.rb], when: 'always' }]], + [[{ changes: %w[app/models/ci/pipeline.rb], when: 'never' }, { changes: %w[app/models/ci/pipeline.rb], when: 'always' }]], + [[{ changes: %w[spec/**/*.rb], when: 'never' }, { changes: %w[spec/**/*.rb], when: 'always' }]], + [[{ changes: %w[*.yml], when: 'never' }, { changes: %w[*.yml], when: 'always' }]], + [[{ changes: %w[.*.yml], when: 'never' }, { changes: %w[.*.yml], when: 'always' }]], + [[{ changes: %w[**/*], when: 'never' }, { changes: %w[**/*], when: 'always' }]], + [[{ changes: %w[*/**/*.rb *.yml], when: 'never' }, { changes: %w[*/**/*.rb *.yml], when: 'always' }]], + [[{ changes: %w[.*.yml **/*], when: 'never' }, { changes: %w[.*.yml **/*], when: 'always' }]] + ] + end + + with_them do + it { is_expected.not_to be_included } + + it 'correctly populates when:' do + expect(seed_build.attributes).to include(when: 'never') + end + end + end + + context 'with an explicit `when: always`' do + where(:rule_set) do + [ + [[{ changes: %w[*/**/*.rb], when: 'always' }, { changes: %w[*/**/*.rb], when: 'never' }]], + [[{ changes: %w[app/models/ci/pipeline.rb], when: 'always' }, { changes: %w[app/models/ci/pipeline.rb], when: 'never' }]], + [[{ changes: %w[spec/**/*.rb], when: 'always' }, { changes: %w[spec/**/*.rb], when: 'never' }]], + [[{ changes: %w[*.yml], when: 'always' }, { changes: %w[*.yml], when: 'never' }]], + [[{ changes: %w[.*.yml], when: 'always' }, { changes: %w[.*.yml], when: 'never' }]], + [[{ changes: %w[**/*], when: 'always' }, { changes: %w[**/*], when: 'never' }]], + [[{ changes: %w[*/**/*.rb *.yml], when: 'always' }, { changes: %w[*/**/*.rb *.yml], when: 'never' }]], + [[{ changes: %w[.*.yml **/*], when: 'always' }, { changes: %w[.*.yml **/*], when: 'never' }]] + ] + end + + with_them do + it { is_expected.to be_included } + + it 'correctly populates when:' do + expect(seed_build.attributes).to include(when: 'always') + end + end + end + + context 'without an explicit when: value' do + where(:rule_set) do + [ + [[{ changes: %w[*/**/*.rb] }]], + [[{ changes: %w[app/models/ci/pipeline.rb] }]], + [[{ changes: %w[spec/**/*.rb] }]], + [[{ changes: %w[*.yml] }]], + [[{ changes: %w[.*.yml] }]], + [[{ changes: %w[**/*] }]], + [[{ changes: %w[*/**/*.rb *.yml] }]], + [[{ changes: %w[.*.yml **/*] }]] + ] + end + + with_them do + it { is_expected.to be_included } + + it 'correctly populates when:' do + expect(seed_build.attributes).to include(when: 'on_success') + end + end + end + end + + context 'with no matching rule' do + where(:rule_set) do + [ + [[{ if: '$VARIABLE != null', when: 'never' }]], + [[{ if: '$VARIABLE != null', when: 'never' }, { if: '$VARIABLE != null', when: 'always' }]], + [[{ if: '$VARIABLE == "the wrong value"', when: 'never' }, { if: '$VARIABLE != null', when: 'always' }]], + [[{ if: '$VARIABLE != null', when: 'always' }]], + [[{ if: '$VARIABLE != null', when: 'always' }, { if: '$VARIABLE != null', when: 'never' }]], + [[{ if: '$VARIABLE == "the wrong value"', when: 'always' }, { if: '$VARIABLE != null', when: 'never' }]], + [[{ if: '$VARIABLE != null' }]], + [[{ if: '$VARIABLE != null' }, { if: '$VARIABLE != null' }]], + [[{ if: '$VARIABLE == "the wrong value"' }, { if: '$VARIABLE != null' }]] + ] + end + + with_them do + it { is_expected.not_to be_included } + + it 'correctly populates when:' do + expect(seed_build.attributes).to include(when: 'never') + end + end + end + + context 'with no rules' do + let(:rule_set) { [] } + + it { is_expected.not_to be_included } + + it 'correctly populates when:' do + expect(seed_build.attributes).to include(when: 'never') + end + end + end end describe 'applying needs: dependency' do subject { seed_build } + let(:needs_count) { 1 } + + let(:needs_attributes) do + Array.new(needs_count, name: 'build') + end + let(:attributes) do { name: 'rspec', - needs_attributes: [{ - name: 'build' - }] + needs_attributes: needs_attributes } end context 'when build job is not present in prior stages' do - it { is_expected.not_to be_included } + it "is included" do + is_expected.to be_included + end + + it "returns an error" do + expect(subject.errors).to contain_exactly( + "rspec: needs 'build'") + end end context 'when build job is part of prior stages' do @@ -414,7 +720,45 @@ describe Gitlab::Ci::Pipeline::Seed::Build do let(:previous_stages) { [stage_seed] } - it { is_expected.to be_included } + it "is included" do + is_expected.to be_included + end + + it "does not have errors" do + expect(subject.errors).to be_empty + end + end + + context 'when lower limit of needs is reached' do + before do + stub_feature_flags(ci_dag_limit_needs: true) + end + + let(:needs_count) { described_class::LOW_NEEDS_LIMIT + 1 } + + it "returns an error" do + expect(subject.errors).to contain_exactly( + "rspec: one job can only need 5 others, but you have listed 6. See needs keyword documentation for more details") + end + end + + context 'when upper limit of needs is reached' do + before do + stub_feature_flags(ci_dag_limit_needs: false) + end + + let(:needs_count) { described_class::HARD_NEEDS_LIMIT + 1 } + + it "returns an error" do + expect(subject.errors).to contain_exactly( + "rspec: one job can only need 50 others, but you have listed 51. See needs keyword documentation for more details") + end end end + + describe '#scoped_variables_hash' do + subject { seed_build.scoped_variables_hash } + + it { is_expected.to eq(seed_build.to_resource.scoped_variables_hash) } + end end diff --git a/spec/lib/gitlab/ci/pipeline/seed/stage_spec.rb b/spec/lib/gitlab/ci/pipeline/seed/stage_spec.rb index 6fba9f37d91..a13335f63d5 100644 --- a/spec/lib/gitlab/ci/pipeline/seed/stage_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/seed/stage_spec.rb @@ -121,6 +121,16 @@ describe Gitlab::Ci::Pipeline::Seed::Stage do end end + describe '#seeds_errors' do + it 'returns all errors from seeds' do + expect(subject.seeds.first) + .to receive(:errors) { ["build error"] } + + expect(subject.errors).to contain_exactly( + "build error") + end + end + describe '#to_resource' do it 'builds a valid stage object with all builds' do subject.to_resource.save! diff --git a/spec/lib/gitlab/ci/yaml_processor_spec.rb b/spec/lib/gitlab/ci/yaml_processor_spec.rb index 4ffa1fc9fd8..91c559dcd9b 100644 --- a/spec/lib/gitlab/ci/yaml_processor_spec.rb +++ b/spec/lib/gitlab/ci/yaml_processor_spec.rb @@ -125,9 +125,11 @@ module Gitlab describe 'delayed job entry' do context 'when delayed is defined' do let(:config) do - YAML.dump(rspec: { script: 'rollout 10%', - when: 'delayed', - start_in: '1 day' }) + YAML.dump(rspec: { + script: 'rollout 10%', + when: 'delayed', + start_in: '1 day' + }) end it 'has the attributes' do @@ -726,12 +728,12 @@ module Gitlab end end - describe "When" do - %w(on_success on_failure always).each do |when_state| - it "returns #{when_state} when defined" do + describe 'when:' do + (Gitlab::Ci::Config::Entry::Job::ALLOWED_WHEN - %w[delayed]).each do |when_state| + it "#{when_state} creates one build and sets when:" do config = YAML.dump({ - rspec: { script: "rspec", when: when_state } - }) + rspec: { script: 'rspec', when: when_state } + }) config_processor = Gitlab::Ci::YamlProcessor.new(config) builds = config_processor.stage_builds_attributes("test") @@ -740,6 +742,35 @@ module Gitlab expect(builds.first[:when]).to eq(when_state) end end + + context 'delayed' do + context 'with start_in' do + it 'creates one build and sets when:' do + config = YAML.dump({ + rspec: { script: 'rspec', when: 'delayed', start_in: '1 hour' } + }) + + config_processor = Gitlab::Ci::YamlProcessor.new(config) + builds = config_processor.stage_builds_attributes("test") + + expect(builds.size).to eq(1) + expect(builds.first[:when]).to eq('delayed') + expect(builds.first[:options][:start_in]).to eq('1 hour') + end + end + + context 'without start_in' do + it 'raises an error' do + config = YAML.dump({ + rspec: { script: 'rspec', when: 'delayed' } + }) + + expect do + Gitlab::Ci::YamlProcessor.new(config) + end.to raise_error(YamlProcessor::ValidationError, /start in should be a duration/) + end + end + end end describe 'Parallel' do @@ -1132,7 +1163,7 @@ module Gitlab it { expect { subject }.not_to raise_error } end - context 'needs to builds' do + context 'needs two builds' do let(:needs) { %w(build1 build2) } it "does create jobs with valid specification" do @@ -1153,7 +1184,10 @@ module Gitlab stage_idx: 1, name: "test1", options: { - script: ["test"] + script: ["test"], + # This does not make sense, there is a follow-up: + # https://gitlab.com/gitlab-org/gitlab-ce/issues/65569 + bridge_needs: %w[build1 build2] }, needs_attributes: [ { name: "build1" }, @@ -1166,7 +1200,7 @@ module Gitlab end end - context 'needs to builds defined as symbols' do + context 'needs two builds defined as symbols' do let(:needs) { [:build1, :build2] } it { expect { subject }.not_to raise_error } @@ -1192,6 +1226,67 @@ module Gitlab end end + describe 'rules' do + subject { Gitlab::Ci::YamlProcessor.new(YAML.dump(config)) } + + let(:config) do + { + var_default: { stage: 'build', script: 'test', rules: [{ if: '$VAR == null' }] }, + var_when: { stage: 'build', script: 'test', rules: [{ if: '$VAR == null', when: 'always' }] }, + var_and_changes: { stage: 'build', script: 'test', rules: [{ if: '$VAR == null', changes: %w[README], when: 'always' }] }, + changes_not_var: { stage: 'test', script: 'test', rules: [{ if: '$VAR != null', changes: %w[README] }] }, + var_not_changes: { stage: 'test', script: 'test', rules: [{ if: '$VAR == null', changes: %w[other/file.rb], when: 'always' }] }, + nothing: { stage: 'test', script: 'test', rules: [{ when: 'manual' }] }, + var_never: { stage: 'deploy', script: 'test', rules: [{ if: '$VAR == null', when: 'never' }] }, + var_delayed: { stage: 'deploy', script: 'test', rules: [{ if: '$VAR == null', when: 'delayed', start_in: '3 hours' }] }, + two_rules: { stage: 'deploy', script: 'test', rules: [{ if: '$VAR == null', when: 'on_success' }, { changes: %w[README], when: 'manual' }] } + } + end + + it 'raises no exceptions' do + expect { subject }.not_to raise_error + end + + it 'returns all jobs regardless of their inclusion' do + expect(subject.builds.count).to eq(config.keys.count) + end + + context 'used with job-level when' do + let(:config) do + { + var_default: { + stage: 'build', + script: 'test', + when: 'always', + rules: [{ if: '$VAR == null' }] + } + } + end + + it 'raises a ValidationError' do + expect { subject }.to raise_error(YamlProcessor::ValidationError, /may not be used with `rules`: when/) + end + end + + context 'used with job-level when:delayed' do + let(:config) do + { + var_default: { + stage: 'build', + script: 'test', + when: 'delayed', + start_in: '10 minutes', + rules: [{ if: '$VAR == null' }] + } + } + end + + it 'raises a ValidationError' do + expect { subject }.to raise_error(YamlProcessor::ValidationError, /may not be used with `rules`: when, start_in/) + end + end + end + describe "Hidden jobs" do let(:config_processor) { Gitlab::Ci::YamlProcessor.new(config) } subject { config_processor.stage_builds_attributes("test") } @@ -1510,7 +1605,7 @@ module Gitlab config = YAML.dump({ rspec: { script: "test", when: 1 } }) expect do Gitlab::Ci::YamlProcessor.new(config) - end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec when should be on_success, on_failure, always, manual or delayed") + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec when should be one of: #{Gitlab::Ci::Config::Entry::Job::ALLOWED_WHEN.join(', ')}") end it "returns errors if job artifacts:name is not an a string" do diff --git a/spec/lib/gitlab/config/entry/attributable_spec.rb b/spec/lib/gitlab/config/entry/attributable_spec.rb index 82614bb8238..6b548d5c4a8 100644 --- a/spec/lib/gitlab/config/entry/attributable_spec.rb +++ b/spec/lib/gitlab/config/entry/attributable_spec.rb @@ -25,7 +25,9 @@ describe Gitlab::Config::Entry::Attributable do end it 'returns the value of config' do + expect(instance).to have_name expect(instance.name).to eq 'some name' + expect(instance).to have_test expect(instance.test).to eq 'some test' end @@ -42,6 +44,7 @@ describe Gitlab::Config::Entry::Attributable do end it 'returns nil' do + expect(instance).not_to have_test expect(instance.test).to be_nil end end diff --git a/spec/lib/gitlab/daemon_spec.rb b/spec/lib/gitlab/daemon_spec.rb index d3e73314b87..0372b770844 100644 --- a/spec/lib/gitlab/daemon_spec.rb +++ b/spec/lib/gitlab/daemon_spec.rb @@ -34,12 +34,12 @@ describe Gitlab::Daemon do end end - describe 'when Daemon is enabled' do + context 'when Daemon is enabled' do before do allow(subject).to receive(:enabled?).and_return(true) end - describe 'when Daemon is stopped' do + context 'when Daemon is stopped' do describe '#start' do it 'starts the Daemon' do expect { subject.start.join }.to change { subject.thread? }.from(false).to(true) @@ -57,14 +57,14 @@ describe Gitlab::Daemon do end end - describe 'when Daemon is running' do + context 'when Daemon is running' do before do - subject.start.join + subject.start end describe '#start' do it "doesn't start running Daemon" do - expect { subject.start.join }.not_to change { subject.thread? } + expect { subject.start.join }.not_to change { subject.thread } expect(subject).to have_received(:start_working).once end @@ -76,11 +76,29 @@ describe Gitlab::Daemon do expect(subject).to have_received(:stop_working) end + + context 'when stop_working raises exception' do + before do + allow(subject).to receive(:start_working) do + sleep(1000) + end + end + + it 'shutdowns Daemon' do + expect(subject).to receive(:stop_working) do + subject.thread.raise(Interrupt) + end + + expect(subject.thread).to be_alive + expect { subject.stop }.not_to raise_error + expect(subject.thread).to be_nil + end + end end end end - describe 'when Daemon is disabled' do + context 'when Daemon is disabled' do before do allow(subject).to receive(:enabled?).and_return(false) end diff --git a/spec/lib/gitlab/danger/helper_spec.rb b/spec/lib/gitlab/danger/helper_spec.rb index f11f68ab3c2..2990594c538 100644 --- a/spec/lib/gitlab/danger/helper_spec.rb +++ b/spec/lib/gitlab/danger/helper_spec.rb @@ -101,13 +101,13 @@ describe Gitlab::Danger::Helper do describe '#changes_by_category' do it 'categorizes changed files' do - expect(fake_git).to receive(:added_files) { %w[foo foo.md foo.rb foo.js db/foo lib/gitlab/database/foo.rb qa/foo ee/changelogs/foo.yml] } + expect(fake_git).to receive(:added_files) { %w[foo foo.md foo.rb foo.js db/migrate/foo lib/gitlab/database/foo.rb qa/foo ee/changelogs/foo.yml] } allow(fake_git).to receive(:modified_files) { [] } allow(fake_git).to receive(:renamed_files) { [] } expect(helper.changes_by_category).to eq( backend: %w[foo.rb], - database: %w[db/foo lib/gitlab/database/foo.rb], + database: %w[db/migrate/foo lib/gitlab/database/foo.rb], frontend: %w[foo.js], none: %w[ee/changelogs/foo.yml foo.md], qa: %w[qa/foo], @@ -173,8 +173,13 @@ describe Gitlab::Danger::Helper do 'ee/FOO_VERSION' | :unknown - 'db/foo' | :database - 'ee/db/foo' | :database + 'db/schema.rb' | :database + 'db/migrate/foo' | :database + 'db/post_migrate/foo' | :database + 'ee/db/migrate/foo' | :database + 'ee/db/post_migrate/foo' | :database + 'ee/db/geo/migrate/foo' | :database + 'ee/db/geo/post_migrate/foo' | :database 'app/models/project_authorization.rb' | :database 'app/services/users/refresh_authorized_projects_service.rb' | :database 'lib/gitlab/background_migration.rb' | :database @@ -188,6 +193,9 @@ describe Gitlab::Danger::Helper do 'lib/gitlab/sql/foo' | :database 'rubocop/cop/migration/foo' | :database + 'db/fixtures/foo.rb' | :backend + 'ee/db/fixtures/foo.rb' | :backend + 'qa/foo' | :qa 'ee/qa/foo' | :qa diff --git a/spec/lib/gitlab/data_builder/push_spec.rb b/spec/lib/gitlab/data_builder/push_spec.rb index cc31f88d365..e8a9f0b06a8 100644 --- a/spec/lib/gitlab/data_builder/push_spec.rb +++ b/spec/lib/gitlab/data_builder/push_spec.rb @@ -3,9 +3,43 @@ require 'spec_helper' describe Gitlab::DataBuilder::Push do + include RepoHelpers + let(:project) { create(:project, :repository) } let(:user) { build(:user, public_email: 'public-email@example.com') } + describe '.build' do + let(:sample) { RepoHelpers.sample_compare } + let(:commits) { project.repository.commits_between(sample.commits.first, sample.commits.last) } + let(:subject) do + described_class.build(project: project, + user: user, + ref: sample.target_branch, + commits: commits, + commits_count: commits.length, + message: 'test message', + with_changed_files: with_changed_files) + end + + context 'with changed files' do + let(:with_changed_files) { true } + + it 'returns commit hook data' do + expect(subject[:project]).to eq(project.hook_attrs) + expect(subject[:commits].first.keys).to include(*%i(added removed modified)) + end + end + + context 'without changed files' do + let(:with_changed_files) { false } + + it 'returns commit hook data without include deltas' do + expect(subject[:project]).to eq(project.hook_attrs) + expect(subject[:commits].first.keys).not_to include(*%i(added removed modified)) + end + end + end + describe '.build_sample' do let(:data) { described_class.build_sample(project, user) } diff --git a/spec/lib/gitlab/database/migration_helpers_spec.rb b/spec/lib/gitlab/database/migration_helpers_spec.rb index 2731fc8573f..cff4eb398bf 100644 --- a/spec/lib/gitlab/database/migration_helpers_spec.rb +++ b/spec/lib/gitlab/database/migration_helpers_spec.rb @@ -576,6 +576,38 @@ describe Gitlab::Database::MigrationHelpers do model.rename_column_concurrently(:users, :old, :new) end + + context 'when default is false' do + let(:old_column) do + double(:column, + type: :boolean, + limit: nil, + default: false, + null: false, + precision: nil, + scale: nil) + end + + it 'copies the default to the new column' do + expect(model).to receive(:change_column_default) + .with(:users, :new, old_column.default) + + model.rename_column_concurrently(:users, :old, :new) + end + end + end + end + + describe '#undo_rename_column_concurrently' do + it 'reverses the operations of rename_column_concurrently' do + expect(model).to receive(:check_trigger_permissions!).with(:users) + + expect(model).to receive(:remove_rename_triggers_for_postgresql) + .with(:users, /trigger_.{12}/) + + expect(model).to receive(:remove_column).with(:users, :new) + + model.undo_rename_column_concurrently(:users, :old, :new) end end @@ -592,6 +624,80 @@ describe Gitlab::Database::MigrationHelpers do end end + describe '#undo_cleanup_concurrent_column_rename' do + context 'in a transaction' do + it 'raises RuntimeError' do + allow(model).to receive(:transaction_open?).and_return(true) + + expect { model.undo_cleanup_concurrent_column_rename(:users, :old, :new) } + .to raise_error(RuntimeError) + end + end + + context 'outside a transaction' do + let(:new_column) do + double(:column, + type: :integer, + limit: 8, + default: 0, + null: false, + precision: 5, + scale: 1) + end + + let(:trigger_name) { model.rename_trigger_name(:users, :old, :new) } + + before do + allow(model).to receive(:transaction_open?).and_return(false) + allow(model).to receive(:column_for).and_return(new_column) + end + + it 'reverses the operations of cleanup_concurrent_column_rename' do + expect(model).to receive(:check_trigger_permissions!).with(:users) + + expect(model).to receive(:install_rename_triggers_for_postgresql) + .with(trigger_name, '"users"', '"old"', '"new"') + + expect(model).to receive(:add_column) + .with(:users, :old, :integer, + limit: new_column.limit, + precision: new_column.precision, + scale: new_column.scale) + + expect(model).to receive(:change_column_default) + .with(:users, :old, new_column.default) + + expect(model).to receive(:update_column_in_batches) + + expect(model).to receive(:change_column_null).with(:users, :old, false) + + expect(model).to receive(:copy_indexes).with(:users, :new, :old) + expect(model).to receive(:copy_foreign_keys).with(:users, :new, :old) + + model.undo_cleanup_concurrent_column_rename(:users, :old, :new) + end + + context 'when default is false' do + let(:new_column) do + double(:column, + type: :boolean, + limit: nil, + default: false, + null: false, + precision: nil, + scale: nil) + end + + it 'copies the default to the old column' do + expect(model).to receive(:change_column_default) + .with(:users, :old, new_column.default) + + model.undo_cleanup_concurrent_column_rename(:users, :old, :new) + end + end + end + end + describe '#change_column_type_concurrently' do it 'changes the column type' do expect(model).to receive(:rename_column_concurrently) @@ -619,10 +725,18 @@ describe Gitlab::Database::MigrationHelpers do .with(/CREATE OR REPLACE FUNCTION foo()/m) expect(model).to receive(:execute) + .with(/DROP TRIGGER IF EXISTS foo/m) + + expect(model).to receive(:execute) .with(/CREATE TRIGGER foo/m) model.install_rename_triggers_for_postgresql('foo', :users, :old, :new) end + + it 'does not fail if trigger already exists' do + model.install_rename_triggers_for_postgresql('foo', :users, :old, :new) + model.install_rename_triggers_for_postgresql('foo', :users, :old, :new) + end end describe '#remove_rename_triggers_for_postgresql' do diff --git a/spec/services/self_monitoring/project/create_service_spec.rb b/spec/lib/gitlab/database_importers/self_monitoring/project/create_service_spec.rb index def20448bd9..9bd0d800086 100644 --- a/spec/services/self_monitoring/project/create_service_spec.rb +++ b/spec/lib/gitlab/database_importers/self_monitoring/project/create_service_spec.rb @@ -2,29 +2,48 @@ require 'spec_helper' -describe SelfMonitoring::Project::CreateService do +describe Gitlab::DatabaseImporters::SelfMonitoring::Project::CreateService do describe '#execute' do - let(:result) { subject.execute } + let(:result) { subject.execute! } let(:prometheus_settings) do - OpenStruct.new( + { enable: true, listen_address: 'localhost:9090' - ) + } end before do - allow(Gitlab.config).to receive(:prometheus).and_return(prometheus_settings) + stub_config(prometheus: prometheus_settings) + end + + context 'without application_settings' do + it 'does not fail' do + expect(subject).to receive(:log_error).and_call_original + expect(result).to eq( + status: :success + ) + + expect(Project.count).to eq(0) + expect(Group.count).to eq(0) + end end context 'without admin users' do - it 'returns error' do + let(:application_setting) { Gitlab::CurrentSettings.current_application_settings } + + before do + allow(ApplicationSetting).to receive(:current_without_cache) { application_setting } + end + + it 'does not fail' do expect(subject).to receive(:log_error).and_call_original expect(result).to eq( - status: :error, - message: 'No active admin user found', - failed_step: :validate_admins + status: :success ) + + expect(Project.count).to eq(0) + expect(Group.count).to eq(0) end end @@ -36,6 +55,7 @@ describe SelfMonitoring::Project::CreateService do let!(:user) { create(:user, :admin) } before do + allow(ApplicationSetting).to receive(:current_without_cache) { application_setting } application_setting.allow_local_requests_from_web_hooks_and_services = true end @@ -56,8 +76,8 @@ describe SelfMonitoring::Project::CreateService do it 'creates group' do expect(result[:status]).to eq(:success) expect(group).to be_persisted - expect(group.name).to eq(described_class::GROUP_NAME) - expect(group.path).to start_with(described_class::GROUP_PATH) + expect(group.name).to eq('GitLab Instance Administrators') + expect(group.path).to start_with('gitlab-instance-administrators') expect(group.path.split('-').last.length).to eq(8) expect(group.visibility_level).to eq(described_class::VISIBILITY_LEVEL) end @@ -77,9 +97,16 @@ describe SelfMonitoring::Project::CreateService do end it 'creates project with correct name and description' do + path = 'administration/monitoring/gitlab_instance_administration_project/index' + docs_path = Rails.application.routes.url_helpers.help_page_path(path) + expect(result[:status]).to eq(:success) expect(project.name).to eq(described_class::PROJECT_NAME) - expect(project.description).to eq(described_class::PROJECT_DESCRIPTION) + expect(project.description).to eq( + 'This project is automatically generated and will be used to help monitor this GitLab instance. ' \ + "[More information](#{docs_path})" + ) + expect(File).to exist("doc/#{path}.md") end it 'adds all admins as maintainers' do @@ -105,19 +132,30 @@ describe SelfMonitoring::Project::CreateService do it 'returns error when saving project ID fails' do allow(application_setting).to receive(:update) { false } - expect(result[:status]).to eq(:error) - expect(result[:failed_step]).to eq(:save_project_id) - expect(result[:message]).to eq('Could not save project ID') + expect { result }.to raise_error(StandardError, 'Could not save project ID') end - it 'does not fail when a project already exists' do - expect(result[:status]).to eq(:success) + context 'when project already exists' do + let(:existing_group) { create(:group) } + let(:existing_project) { create(:project, namespace: existing_group) } - second_result = subject.execute + before do + admin1 = create(:user, :admin) + admin2 = create(:user, :admin) + + existing_group.add_owner(user) + existing_group.add_users([admin1, admin2], Gitlab::Access::MAINTAINER) + + application_setting.instance_administration_project_id = existing_project.id + end + + it 'does not fail' do + expect(subject).to receive(:log_error).and_call_original + expect(result[:status]).to eq(:success) - expect(second_result[:status]).to eq(:success) - expect(second_result[:project]).to eq(project) - expect(second_result[:group]).to eq(group) + expect(Project.count).to eq(1) + expect(Group.count).to eq(1) + end end context 'when local requests from hooks and services are not allowed' do @@ -138,8 +176,11 @@ describe SelfMonitoring::Project::CreateService do end context 'with non default prometheus address' do - before do - prometheus_settings.listen_address = 'https://localhost:9090' + let(:prometheus_settings) do + { + enable: true, + listen_address: 'https://localhost:9090' + } end it_behaves_like 'has prometheus service', 'https://localhost:9090' @@ -157,8 +198,11 @@ describe SelfMonitoring::Project::CreateService do end context 'when prometheus setting is disabled in gitlab.yml' do - before do - prometheus_settings.enable = false + let(:prometheus_settings) do + { + enable: false, + listen_address: 'http://localhost:9090' + } end it 'does not configure prometheus' do @@ -168,9 +212,7 @@ describe SelfMonitoring::Project::CreateService do end context 'when prometheus listen address is blank in gitlab.yml' do - before do - prometheus_settings.listen_address = '' - end + let(:prometheus_settings) { { enable: true, listen_address: '' } } it 'does not configure prometheus' do expect(result).to include(status: :success) @@ -192,11 +234,7 @@ describe SelfMonitoring::Project::CreateService do it 'returns error' do expect(subject).to receive(:log_error).and_call_original - expect(result).to eq({ - status: :error, - message: 'Could not create project', - failed_step: :create_project - }) + expect { result }.to raise_error(StandardError, 'Could not create project') end end @@ -207,26 +245,21 @@ describe SelfMonitoring::Project::CreateService do it 'returns error' do expect(subject).to receive(:log_error).and_call_original - expect(result).to eq({ - status: :error, - message: 'Could not add admins as members', - failed_step: :add_group_members - }) + expect { result }.to raise_error(StandardError, 'Could not add admins as members') end end context 'when prometheus manual configuration cannot be saved' do - before do - prometheus_settings.listen_address = 'httpinvalid://localhost:9090' + let(:prometheus_settings) do + { + enable: true, + listen_address: 'httpinvalid://localhost:9090' + } end it 'returns error' do expect(subject).to receive(:log_error).and_call_original - expect(result).to eq( - status: :error, - message: 'Could not save prometheus manual configuration', - failed_step: :add_prometheus_manual_configuration - ) + expect { result }.to raise_error(StandardError, 'Could not save prometheus manual configuration') end end end diff --git a/spec/lib/gitlab/email/hook/disable_email_interceptor_spec.rb b/spec/lib/gitlab/email/hook/disable_email_interceptor_spec.rb index 0c58cf088cc..c8ed12523d0 100644 --- a/spec/lib/gitlab/email/hook/disable_email_interceptor_spec.rb +++ b/spec/lib/gitlab/email/hook/disable_email_interceptor_spec.rb @@ -13,9 +13,6 @@ describe Gitlab::Email::Hook::DisableEmailInterceptor do end after do - # Removing interceptor from the list because unregister_interceptor is - # implemented in later version of mail gem - # See: https://github.com/mikel/mail/pull/705 Mail.unregister_interceptor(described_class) end diff --git a/spec/lib/gitlab/email/hook/smime_signature_interceptor_spec.rb b/spec/lib/gitlab/email/hook/smime_signature_interceptor_spec.rb new file mode 100644 index 00000000000..35aa663b0a5 --- /dev/null +++ b/spec/lib/gitlab/email/hook/smime_signature_interceptor_spec.rb @@ -0,0 +1,52 @@ +require 'spec_helper' + +describe Gitlab::Email::Hook::SmimeSignatureInterceptor do + include SmimeHelper + + # cert generation is an expensive operation and they are used read-only, + # so we share them as instance variables in all tests + before :context do + @root_ca = generate_root + @cert = generate_cert(root_ca: @root_ca) + end + + let(:root_certificate) do + Gitlab::Email::Smime::Certificate.new(@root_ca[:key], @root_ca[:cert]) + end + + let(:certificate) do + Gitlab::Email::Smime::Certificate.new(@cert[:key], @cert[:cert]) + end + + let(:mail) do + ActionMailer::Base.mail(to: 'test@example.com', from: 'info@example.com', body: 'signed hello') + end + + before do + allow(Gitlab::Email::Smime::Certificate).to receive_messages(from_files: certificate) + + Mail.register_interceptor(described_class) + mail.deliver_now + end + + after do + Mail.unregister_interceptor(described_class) + end + + it 'signs the email appropriately with SMIME' do + expect(mail.header['To'].value).to eq('test@example.com') + expect(mail.header['From'].value).to eq('info@example.com') + expect(mail.header['Content-Type'].value).to match('multipart/signed').and match('protocol="application/x-pkcs7-signature"') + + # verify signature and obtain pkcs7 encoded content + p7enc = Gitlab::Email::Smime::Signer.verify_signature( + cert: certificate.cert, + ca_cert: root_certificate.cert, + signed_data: mail.encoded) + + # envelope in a Mail object and obtain the body + decoded_mail = Mail.new(p7enc.data) + + expect(decoded_mail.body.encoded).to eq('signed hello') + end +end diff --git a/spec/lib/gitlab/email/smime/certificate_spec.rb b/spec/lib/gitlab/email/smime/certificate_spec.rb new file mode 100644 index 00000000000..90b27602413 --- /dev/null +++ b/spec/lib/gitlab/email/smime/certificate_spec.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Email::Smime::Certificate do + include SmimeHelper + + # cert generation is an expensive operation and they are used read-only, + # so we share them as instance variables in all tests + before :context do + @root_ca = generate_root + @cert = generate_cert(root_ca: @root_ca) + end + + describe 'testing environment setup' do + describe 'generate_root' do + subject { @root_ca } + + it 'generates a root CA that expires a long way in the future' do + expect(subject[:cert].not_after).to be > 999.years.from_now + end + end + + describe 'generate_cert' do + subject { @cert } + + it 'generates a cert properly signed by the root CA' do + expect(subject[:cert].issuer).to eq(@root_ca[:cert].subject) + end + + it 'generates a cert that expires soon' do + expect(subject[:cert].not_after).to be < 60.minutes.from_now + end + + it 'generates a cert intended for email signing' do + expect(subject[:cert].extensions).to include(an_object_having_attributes(oid: 'extendedKeyUsage', value: match('E-mail Protection'))) + end + + context 'passing in INFINITE_EXPIRY' do + subject { generate_cert(root_ca: @root_ca, expires_in: SmimeHelper::INFINITE_EXPIRY) } + + it 'generates a cert that expires a long way in the future' do + expect(subject[:cert].not_after).to be > 999.years.from_now + end + end + end + end + + describe '.from_strings' do + it 'parses correctly a certificate and key' do + parsed_cert = described_class.from_strings(@cert[:key].to_s, @cert[:cert].to_pem) + + common_cert_tests(parsed_cert, @cert, @root_ca) + end + end + + describe '.from_files' do + it 'parses correctly a certificate and key' do + allow(File).to receive(:read).with('a_key').and_return(@cert[:key].to_s) + allow(File).to receive(:read).with('a_cert').and_return(@cert[:cert].to_pem) + + parsed_cert = described_class.from_files('a_key', 'a_cert') + + common_cert_tests(parsed_cert, @cert, @root_ca) + end + end + + def common_cert_tests(parsed_cert, cert, root_ca) + expect(parsed_cert.cert).to be_a(OpenSSL::X509::Certificate) + expect(parsed_cert.cert.subject).to eq(cert[:cert].subject) + expect(parsed_cert.cert.issuer).to eq(root_ca[:cert].subject) + expect(parsed_cert.cert.not_before).to eq(cert[:cert].not_before) + expect(parsed_cert.cert.not_after).to eq(cert[:cert].not_after) + expect(parsed_cert.cert.extensions).to include(an_object_having_attributes(oid: 'extendedKeyUsage', value: match('E-mail Protection'))) + expect(parsed_cert.key).to be_a(OpenSSL::PKey::RSA) + end +end diff --git a/spec/lib/gitlab/email/smime/signer_spec.rb b/spec/lib/gitlab/email/smime/signer_spec.rb new file mode 100644 index 00000000000..56048b7148c --- /dev/null +++ b/spec/lib/gitlab/email/smime/signer_spec.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Email::Smime::Signer do + include SmimeHelper + + it 'signs data appropriately with SMIME' do + root_certificate = generate_root + certificate = generate_cert(root_ca: root_certificate) + + signed_content = described_class.sign( + cert: certificate[:cert], + key: certificate[:key], + data: 'signed content') + expect(signed_content).not_to be_nil + + p7enc = described_class.verify_signature( + cert: certificate[:cert], + ca_cert: root_certificate[:cert], + signed_data: signed_content) + + expect(p7enc).not_to be_nil + expect(p7enc.data).to eq('signed content') + end +end diff --git a/spec/lib/gitlab/git_post_receive_spec.rb b/spec/lib/gitlab/git_post_receive_spec.rb new file mode 100644 index 00000000000..f0df3794e29 --- /dev/null +++ b/spec/lib/gitlab/git_post_receive_spec.rb @@ -0,0 +1,138 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe ::Gitlab::GitPostReceive do + set(:project) { create(:project, :repository) } + + subject { described_class.new(project, "project-#{project.id}", changes.dup, {}) } + + describe '#includes_branches?' do + context 'with no branches' do + let(:changes) do + <<~EOF + 654321 210987 refs/nobranches/tag1 + 654322 210986 refs/tags/test1 + 654323 210985 refs/merge-requests/mr1 + EOF + end + + it 'returns false' do + expect(subject.includes_branches?).to be_falsey + end + end + + context 'with branches' do + let(:changes) do + <<~EOF + 654322 210986 refs/heads/test1 + 654321 210987 refs/tags/tag1 + 654323 210985 refs/merge-requests/mr1 + EOF + end + + it 'returns true' do + expect(subject.includes_branches?).to be_truthy + end + end + + context 'with malformed changes' do + let(:changes) do + <<~EOF + ref/heads/1 a + somebranch refs/heads/2 + EOF + end + + it 'returns false' do + expect(subject.includes_branches?).to be_falsey + end + end + end + + describe '#includes_tags?' do + context 'with no tags' do + let(:changes) do + <<~EOF + 654321 210987 refs/notags/tag1 + 654322 210986 refs/heads/test1 + 654323 210985 refs/merge-requests/mr1 + EOF + end + + it 'returns false' do + expect(subject.includes_tags?).to be_falsey + end + end + + context 'with tags' do + let(:changes) do + <<~EOF + 654322 210986 refs/heads/test1 + 654321 210987 refs/tags/tag1 + 654323 210985 refs/merge-requests/mr1 + EOF + end + + it 'returns true' do + expect(subject.includes_tags?).to be_truthy + end + end + + context 'with malformed changes' do + let(:changes) do + <<~EOF + ref/tags/1 a + sometag refs/tags/2 + EOF + end + + it 'returns false' do + expect(subject.includes_tags?).to be_falsey + end + end + end + + describe '#includes_default_branch?' do + context 'with no default branch' do + let(:changes) do + <<~EOF + 654321 210987 refs/heads/test1 + 654322 210986 refs/tags/#{project.default_branch} + 654323 210985 refs/heads/test3 + EOF + end + + it 'returns false' do + expect(subject.includes_default_branch?).to be_falsey + end + end + + context 'with a project with no default branch' do + let(:changes) do + <<~EOF + 654321 210987 refs/heads/test1 + EOF + end + + it 'returns true' do + expect(project).to receive(:default_branch).and_return(nil) + expect(subject.includes_default_branch?).to be_truthy + end + end + + context 'with default branch' do + let(:changes) do + <<~EOF + 654322 210986 refs/heads/test1 + 654321 210987 refs/tags/test2 + 654323 210985 refs/heads/#{project.default_branch} + EOF + end + + it 'returns true' do + expect(subject.includes_default_branch?).to be_truthy + end + end + end +end diff --git a/spec/lib/gitlab/gitaly_client_spec.rb b/spec/lib/gitlab/gitaly_client_spec.rb index e9fb6c0125c..99d563e03ec 100644 --- a/spec/lib/gitlab/gitaly_client_spec.rb +++ b/spec/lib/gitlab/gitaly_client_spec.rb @@ -27,6 +27,16 @@ describe Gitlab::GitalyClient do end end + describe '.filesystem_id' do + it 'returns an empty string when the storage is not found in the response' do + response = double("response") + allow(response).to receive(:storage_statuses).and_return([]) + allow_any_instance_of(Gitlab::GitalyClient::ServerService).to receive(:info).and_return(response) + + expect(described_class.filesystem_id('default')).to eq(nil) + end + end + describe '.stub_class' do it 'returns the gRPC health check stub' do expect(described_class.stub_class(:health_check)).to eq(::Grpc::Health::V1::Health::Stub) diff --git a/spec/lib/gitlab/graphql/loaders/batch_root_storage_statistics_loader_spec.rb b/spec/lib/gitlab/graphql/loaders/batch_root_storage_statistics_loader_spec.rb new file mode 100644 index 00000000000..38931f7ab5e --- /dev/null +++ b/spec/lib/gitlab/graphql/loaders/batch_root_storage_statistics_loader_spec.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Graphql::Loaders::BatchRootStorageStatisticsLoader do + describe '#find' do + it 'only queries once for project statistics' do + stats = create_list(:namespace_root_storage_statistics, 2) + namespace1 = stats.first.namespace + namespace2 = stats.last.namespace + + expect do + described_class.new(namespace1.id).find + described_class.new(namespace2.id).find + end.not_to exceed_query_limit(1) + end + end +end diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index fddb5066d6f..3c6b17c10ec 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -242,6 +242,7 @@ project: - cluster_project - cluster_ingresses - creator +- cycle_analytics_stages - group - namespace - boards diff --git a/spec/lib/gitlab/import_export/project.json b/spec/lib/gitlab/import_export/project.json index 6d70b147666..a211675bbf2 100644 --- a/spec/lib/gitlab/import_export/project.json +++ b/spec/lib/gitlab/import_export/project.json @@ -2450,7 +2450,21 @@ "author": { "name": "Ottis Schuster II" }, - "events": [] + "events": [], + "suggestions": [ + { + "id": 1, + "note_id": 674, + "relative_order": 0, + "applied": false, + "commit_id": null, + "from_content": "Original line\n", + "to_content": "New line\n", + "lines_above": 0, + "lines_below": 0, + "outdated": false + } + ] }, { "id": 675, 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 baec24590b4..d6e1fbaa979 100644 --- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb @@ -125,6 +125,13 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do expect(MergeRequest.find_by(title: 'MR1').resource_label_events).not_to be_empty end + it 'restores suggestion' do + note = Note.find_by("note LIKE 'Saepe asperiores exercitationem non dignissimos laborum reiciendis et ipsum%'") + + expect(note.suggestions.count).to eq(1) + expect(note.suggestions.first.from_content).to eq("Original line\n") + end + context 'event at forth level of the tree' do let(:event) { Event.where(action: 6).first } diff --git a/spec/lib/gitlab/kubernetes/helm/pod_spec.rb b/spec/lib/gitlab/kubernetes/helm/pod_spec.rb index 06c8d127951..fce2aded786 100644 --- a/spec/lib/gitlab/kubernetes/helm/pod_spec.rb +++ b/spec/lib/gitlab/kubernetes/helm/pod_spec.rb @@ -30,7 +30,7 @@ describe Gitlab::Kubernetes::Helm::Pod do it 'generates 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.12.3-kube-1.11.7') + expect(container.image).to eq('registry.gitlab.com/gitlab-org/cluster-integration/helm-install-image/releases/2.14.3-kube-1.11.10') 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 f49d4e23e39..e5d688aa391 100644 --- a/spec/lib/gitlab/kubernetes/kube_client_spec.rb +++ b/spec/lib/gitlab/kubernetes/kube_client_spec.rb @@ -3,6 +3,7 @@ require 'spec_helper' describe Gitlab::Kubernetes::KubeClient do + include StubRequests include KubernetesHelpers let(:api_url) { 'https://kubernetes.example.com/prefix' } @@ -14,6 +15,17 @@ describe Gitlab::Kubernetes::KubeClient do stub_kubeclient_discover(api_url) end + def method_call(client, method_name) + 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 + shared_examples 'a Kubeclient' do it 'is a Kubeclient::Client' do is_expected.to be_an_instance_of Kubeclient::Client @@ -25,28 +37,30 @@ describe Gitlab::Kubernetes::KubeClient do end shared_examples 'redirection not allowed' do |method_name| - before do - redirect_url = 'https://not-under-our-control.example.com/api/v1/pods' + context 'api_url is redirected' do + 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, %r{\A#{api_url}/}) + .to_return(status: 302, headers: { location: redirect_url }) - stub_request(:get, redirect_url) - .to_return(status: 200, body: '{}') - end + 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 + it 'does not follow redirects' do + expect { method_call(client, method_name) }.to raise_error(Kubeclient::HttpError) end - expect { method_call.call }.to raise_error(Kubeclient::HttpError) + end + end + + shared_examples 'dns rebinding not allowed' do |method_name| + it 'does not allow DNS rebinding' do + stub_dns(api_url, ip_address: '8.8.8.8') + client + + stub_dns(api_url, ip_address: '192.168.2.120') + expect { method_call(client, method_name) }.to raise_error(ArgumentError, /is blocked/) end end @@ -160,6 +174,7 @@ describe Gitlab::Kubernetes::KubeClient do ].each do |method| describe "##{method}" do include_examples 'redirection not allowed', method + include_examples 'dns rebinding not allowed', method it 'delegates to the core client' do expect(client).to delegate_method(method).to(:core_client) @@ -185,6 +200,7 @@ describe Gitlab::Kubernetes::KubeClient do ].each do |method| describe "##{method}" do include_examples 'redirection not allowed', method + include_examples 'dns rebinding not allowed', method it 'delegates to the rbac client' do expect(client).to delegate_method(method).to(:rbac_client) @@ -203,6 +219,7 @@ describe Gitlab::Kubernetes::KubeClient do describe '#get_deployments' do include_examples 'redirection not allowed', 'get_deployments' + include_examples 'dns rebinding not allowed', 'get_deployments' it 'delegates to the extensions client' do expect(client).to delegate_method(:get_deployments).to(:extensions_client) diff --git a/spec/lib/gitlab/kubernetes/kubectl_cmd_spec.rb b/spec/lib/gitlab/kubernetes/kubectl_cmd_spec.rb new file mode 100644 index 00000000000..f24ab5579df --- /dev/null +++ b/spec/lib/gitlab/kubernetes/kubectl_cmd_spec.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' + +describe Gitlab::Kubernetes::KubectlCmd do + describe '.delete' do + it 'constructs string properly' do + args = %w(resource_type type --flag-1 --flag-2) + + expected_command = 'kubectl delete resource_type type --flag-1 --flag-2' + + expect(described_class.delete(*args)).to eq expected_command + end + end + + describe '.apply_file' do + context 'without optional args' do + it 'requires filename to be present' do + expect { described_class.apply_file(nil) }.to raise_error(ArgumentError, "filename is not present") + expect { described_class.apply_file(" ") }.to raise_error(ArgumentError, "filename is not present") + end + + it 'constructs string properly' do + expected_command = 'kubectl apply -f filename' + + expect(described_class.apply_file('filename')).to eq expected_command + end + end + + context 'with optional args' do + it 'constructs command properly with many args' do + args = %w(arg-1 --flag-0-1 arg-2 --flag-0-2) + + expected_command = 'kubectl apply -f filename arg-1 --flag-0-1 arg-2 --flag-0-2' + + expect(described_class.apply_file('filename', *args)).to eq expected_command + end + + it 'constructs command properly with single arg' do + args = "arg-1" + + expected_command = 'kubectl apply -f filename arg-1' + + expect(described_class.apply_file('filename', args)).to eq(expected_command) + end + end + end +end diff --git a/spec/lib/gitlab/legacy_github_import/release_formatter_spec.rb b/spec/lib/gitlab/legacy_github_import/release_formatter_spec.rb index 534cf219520..2cf4b367c0b 100644 --- a/spec/lib/gitlab/legacy_github_import/release_formatter_spec.rb +++ b/spec/lib/gitlab/legacy_github_import/release_formatter_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::LegacyGithubImport::ReleaseFormatter do diff --git a/spec/lib/gitlab/legacy_github_import/user_formatter_spec.rb b/spec/lib/gitlab/legacy_github_import/user_formatter_spec.rb index 3cd096eb0ad..919847fe061 100644 --- a/spec/lib/gitlab/legacy_github_import/user_formatter_spec.rb +++ b/spec/lib/gitlab/legacy_github_import/user_formatter_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::LegacyGithubImport::UserFormatter do diff --git a/spec/lib/gitlab/legacy_github_import/wiki_formatter_spec.rb b/spec/lib/gitlab/legacy_github_import/wiki_formatter_spec.rb index 7519707293c..639fb9d80eb 100644 --- a/spec/lib/gitlab/legacy_github_import/wiki_formatter_spec.rb +++ b/spec/lib/gitlab/legacy_github_import/wiki_formatter_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::LegacyGithubImport::WikiFormatter do diff --git a/spec/lib/gitlab/loop_helpers_spec.rb b/spec/lib/gitlab/loop_helpers_spec.rb index e17a0342d64..7e59b41d5b9 100644 --- a/spec/lib/gitlab/loop_helpers_spec.rb +++ b/spec/lib/gitlab/loop_helpers_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::LoopHelpers do diff --git a/spec/lib/gitlab/manifest_import/manifest_spec.rb b/spec/lib/gitlab/manifest_import/manifest_spec.rb index ded93e23c08..c1135f710ea 100644 --- a/spec/lib/gitlab/manifest_import/manifest_spec.rb +++ b/spec/lib/gitlab/manifest_import/manifest_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::ManifestImport::Manifest do diff --git a/spec/lib/gitlab/manifest_import/project_creator_spec.rb b/spec/lib/gitlab/manifest_import/project_creator_spec.rb index a7487972f51..a8cfcfb41d3 100644 --- a/spec/lib/gitlab/manifest_import/project_creator_spec.rb +++ b/spec/lib/gitlab/manifest_import/project_creator_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::ManifestImport::ProjectCreator do diff --git a/spec/lib/gitlab/markup_helper_spec.rb b/spec/lib/gitlab/markup_helper_spec.rb index 09e518ff989..b93538cae5a 100644 --- a/spec/lib/gitlab/markup_helper_spec.rb +++ b/spec/lib/gitlab/markup_helper_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::MarkupHelper do diff --git a/spec/lib/gitlab/metrics/background_transaction_spec.rb b/spec/lib/gitlab/metrics/background_transaction_spec.rb index 17445fe6de5..d87d2c839ad 100644 --- a/spec/lib/gitlab/metrics/background_transaction_spec.rb +++ b/spec/lib/gitlab/metrics/background_transaction_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::Metrics::BackgroundTransaction do diff --git a/spec/lib/gitlab/metrics/dashboard/url_spec.rb b/spec/lib/gitlab/metrics/dashboard/url_spec.rb index 34bc6359414..e0dc6d98efc 100644 --- a/spec/lib/gitlab/metrics/dashboard/url_spec.rb +++ b/spec/lib/gitlab/metrics/dashboard/url_spec.rb @@ -9,14 +9,22 @@ describe Gitlab::Metrics::Dashboard::Url do end it 'matches a metrics dashboard link with named params' do - url = Gitlab::Routing.url_helpers.metrics_namespace_project_environment_url('foo', 'bar', 1, start: 123345456, anchor: 'title') + url = Gitlab::Routing.url_helpers.metrics_namespace_project_environment_url( + 'foo', + 'bar', + 1, + start: '2019-08-02T05:43:09.000Z', + dashboard: 'config/prometheus/common_metrics.yml', + group: 'awesome group', + anchor: 'title' + ) expected_params = { 'url' => url, 'namespace' => 'foo', 'project' => 'bar', 'environment' => '1', - 'query' => '?start=123345456', + 'query' => '?dashboard=config%2Fprometheus%2Fcommon_metrics.yml&group=awesome+group&start=2019-08-02T05%3A43%3A09.000Z', 'anchor' => '#title' } diff --git a/spec/lib/gitlab/metrics/delta_spec.rb b/spec/lib/gitlab/metrics/delta_spec.rb index 718387cdee1..9bb011dc8fc 100644 --- a/spec/lib/gitlab/metrics/delta_spec.rb +++ b/spec/lib/gitlab/metrics/delta_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::Metrics::Delta do diff --git a/spec/lib/gitlab/metrics/instrumentation_spec.rb b/spec/lib/gitlab/metrics/instrumentation_spec.rb index 977bc250049..0e2f274f157 100644 --- a/spec/lib/gitlab/metrics/instrumentation_spec.rb +++ b/spec/lib/gitlab/metrics/instrumentation_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::Metrics::Instrumentation do diff --git a/spec/lib/gitlab/metrics/method_call_spec.rb b/spec/lib/gitlab/metrics/method_call_spec.rb index d9379cfe674..3b5e04e2df5 100644 --- a/spec/lib/gitlab/metrics/method_call_spec.rb +++ b/spec/lib/gitlab/metrics/method_call_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::Metrics::MethodCall do diff --git a/spec/lib/gitlab/metrics/methods_spec.rb b/spec/lib/gitlab/metrics/methods_spec.rb index 9d41ed2442b..bca94deb1d8 100644 --- a/spec/lib/gitlab/metrics/methods_spec.rb +++ b/spec/lib/gitlab/metrics/methods_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::Metrics::Methods do diff --git a/spec/lib/gitlab/metrics/metric_spec.rb b/spec/lib/gitlab/metrics/metric_spec.rb index d240b8a01fd..611b59231ba 100644 --- a/spec/lib/gitlab/metrics/metric_spec.rb +++ b/spec/lib/gitlab/metrics/metric_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::Metrics::Metric do diff --git a/spec/lib/gitlab/metrics/prometheus_spec.rb b/spec/lib/gitlab/metrics/prometheus_spec.rb index 3d4dd5fdf01..b37624982e2 100644 --- a/spec/lib/gitlab/metrics/prometheus_spec.rb +++ b/spec/lib/gitlab/metrics/prometheus_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::Metrics::Prometheus, :prometheus do diff --git a/spec/lib/gitlab/metrics/rack_middleware_spec.rb b/spec/lib/gitlab/metrics/rack_middleware_spec.rb index b84387204ee..1c1681cc5ab 100644 --- a/spec/lib/gitlab/metrics/rack_middleware_spec.rb +++ b/spec/lib/gitlab/metrics/rack_middleware_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::Metrics::RackMiddleware do diff --git a/spec/lib/gitlab/metrics/requests_rack_middleware_spec.rb b/spec/lib/gitlab/metrics/requests_rack_middleware_spec.rb index ebe66948a91..c29db3a93ec 100644 --- a/spec/lib/gitlab/metrics/requests_rack_middleware_spec.rb +++ b/spec/lib/gitlab/metrics/requests_rack_middleware_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::Metrics::RequestsRackMiddleware do diff --git a/spec/lib/gitlab/metrics/samplers/influx_sampler_spec.rb b/spec/lib/gitlab/metrics/samplers/influx_sampler_spec.rb index 2923048f742..2d4b27a6ac1 100644 --- a/spec/lib/gitlab/metrics/samplers/influx_sampler_spec.rb +++ b/spec/lib/gitlab/metrics/samplers/influx_sampler_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::Metrics::Samplers::InfluxSampler do diff --git a/spec/lib/gitlab/metrics/samplers/puma_sampler_spec.rb b/spec/lib/gitlab/metrics/samplers/puma_sampler_spec.rb index f4a6e1fc7d9..b8add3c1324 100644 --- a/spec/lib/gitlab/metrics/samplers/puma_sampler_spec.rb +++ b/spec/lib/gitlab/metrics/samplers/puma_sampler_spec.rb @@ -46,8 +46,6 @@ describe Gitlab::Metrics::Samplers::PumaSampler do expect(subject.metrics[:puma_workers]).to receive(:set).with(labels, 2) expect(subject.metrics[:puma_running_workers]).to receive(:set).with(labels, 2) expect(subject.metrics[:puma_stale_workers]).to receive(:set).with(labels, 0) - expect(subject.metrics[:puma_phase]).to receive(:set).once.with(labels, 2) - expect(subject.metrics[:puma_phase]).to receive(:set).once.with({ worker: 'worker_0' }, 1) subject.sample end diff --git a/spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb b/spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb index 5005a5d9ebc..8c4071a7ed1 100644 --- a/spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb +++ b/spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::Metrics::Samplers::RubySampler do diff --git a/spec/lib/gitlab/metrics/samplers/unicorn_sampler_spec.rb b/spec/lib/gitlab/metrics/samplers/unicorn_sampler_spec.rb index 4b697b2ba0f..cdfd95e3885 100644 --- a/spec/lib/gitlab/metrics/samplers/unicorn_sampler_spec.rb +++ b/spec/lib/gitlab/metrics/samplers/unicorn_sampler_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::Metrics::Samplers::UnicornSampler do diff --git a/spec/lib/gitlab/metrics/sidekiq_metrics_exporter_spec.rb b/spec/lib/gitlab/metrics/sidekiq_metrics_exporter_spec.rb index 61eb059a731..9eea3eb79dc 100644 --- a/spec/lib/gitlab/metrics/sidekiq_metrics_exporter_spec.rb +++ b/spec/lib/gitlab/metrics/sidekiq_metrics_exporter_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::Metrics::SidekiqMetricsExporter do diff --git a/spec/lib/gitlab/metrics/sidekiq_middleware_spec.rb b/spec/lib/gitlab/metrics/sidekiq_middleware_spec.rb index ae1d8b47fe9..bb95d5ab2ad 100644 --- a/spec/lib/gitlab/metrics/sidekiq_middleware_spec.rb +++ b/spec/lib/gitlab/metrics/sidekiq_middleware_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::Metrics::SidekiqMiddleware do diff --git a/spec/lib/gitlab/metrics/subscribers/action_view_spec.rb b/spec/lib/gitlab/metrics/subscribers/action_view_spec.rb index 9f3af1acef7..25c0e7b695a 100644 --- a/spec/lib/gitlab/metrics/subscribers/action_view_spec.rb +++ b/spec/lib/gitlab/metrics/subscribers/action_view_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::Metrics::Subscribers::ActionView do diff --git a/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb b/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb index ee6d6fc961f..1624cea8bda 100644 --- a/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb +++ b/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::Metrics::Subscribers::ActiveRecord do diff --git a/spec/lib/gitlab/metrics/subscribers/rails_cache_spec.rb b/spec/lib/gitlab/metrics/subscribers/rails_cache_spec.rb index e04056b3450..ab0d89b2683 100644 --- a/spec/lib/gitlab/metrics/subscribers/rails_cache_spec.rb +++ b/spec/lib/gitlab/metrics/subscribers/rails_cache_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::Metrics::Subscribers::RailsCache do diff --git a/spec/lib/gitlab/metrics/system_spec.rb b/spec/lib/gitlab/metrics/system_spec.rb index 3b434a02f63..6d2764a06f2 100644 --- a/spec/lib/gitlab/metrics/system_spec.rb +++ b/spec/lib/gitlab/metrics/system_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::Metrics::System do diff --git a/spec/lib/gitlab/metrics/web_transaction_spec.rb b/spec/lib/gitlab/metrics/web_transaction_spec.rb index 0b3b23e930f..2b35f07cc0d 100644 --- a/spec/lib/gitlab/metrics/web_transaction_spec.rb +++ b/spec/lib/gitlab/metrics/web_transaction_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::Metrics::WebTransaction do diff --git a/spec/lib/gitlab/metrics_spec.rb b/spec/lib/gitlab/metrics_spec.rb index 03c185ddc07..f0ba12c1cd0 100644 --- a/spec/lib/gitlab/metrics_spec.rb +++ b/spec/lib/gitlab/metrics_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::Metrics do diff --git a/spec/lib/gitlab/middleware/basic_health_check_spec.rb b/spec/lib/gitlab/middleware/basic_health_check_spec.rb index 86bdc479b66..07fda691ac8 100644 --- a/spec/lib/gitlab/middleware/basic_health_check_spec.rb +++ b/spec/lib/gitlab/middleware/basic_health_check_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::Middleware::BasicHealthCheck do diff --git a/spec/lib/gitlab/middleware/multipart_spec.rb b/spec/lib/gitlab/middleware/multipart_spec.rb index 3f6ada6832a..33797817578 100644 --- a/spec/lib/gitlab/middleware/multipart_spec.rb +++ b/spec/lib/gitlab/middleware/multipart_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' require 'tempfile' diff --git a/spec/lib/gitlab/middleware/rails_queue_duration_spec.rb b/spec/lib/gitlab/middleware/rails_queue_duration_spec.rb index 14f2c3cb86f..31359abdce3 100644 --- a/spec/lib/gitlab/middleware/rails_queue_duration_spec.rb +++ b/spec/lib/gitlab/middleware/rails_queue_duration_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::Middleware::RailsQueueDuration do diff --git a/spec/lib/gitlab/middleware/read_only_spec.rb b/spec/lib/gitlab/middleware/read_only_spec.rb index 24d49a049b6..d2c8f4ab0bd 100644 --- a/spec/lib/gitlab/middleware/read_only_spec.rb +++ b/spec/lib/gitlab/middleware/read_only_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::Middleware::ReadOnly do diff --git a/spec/lib/gitlab/middleware/release_env_spec.rb b/spec/lib/gitlab/middleware/release_env_spec.rb index 5e3aa877409..3ca40f4ebd0 100644 --- a/spec/lib/gitlab/middleware/release_env_spec.rb +++ b/spec/lib/gitlab/middleware/release_env_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::Middleware::ReleaseEnv do diff --git a/spec/lib/gitlab/multi_collection_paginator_spec.rb b/spec/lib/gitlab/multi_collection_paginator_spec.rb index 28cd704b05a..f2049884b83 100644 --- a/spec/lib/gitlab/multi_collection_paginator_spec.rb +++ b/spec/lib/gitlab/multi_collection_paginator_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::MultiCollectionPaginator do diff --git a/spec/lib/gitlab/object_hierarchy_spec.rb b/spec/lib/gitlab/object_hierarchy_spec.rb index bfd456cdd7e..b16eccbcb2c 100644 --- a/spec/lib/gitlab/object_hierarchy_spec.rb +++ b/spec/lib/gitlab/object_hierarchy_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::ObjectHierarchy do diff --git a/spec/lib/gitlab/octokit/middleware_spec.rb b/spec/lib/gitlab/octokit/middleware_spec.rb index 43f6d13f7ba..8aa6d17ac9e 100644 --- a/spec/lib/gitlab/octokit/middleware_spec.rb +++ b/spec/lib/gitlab/octokit/middleware_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::Octokit::Middleware do diff --git a/spec/lib/gitlab/omniauth_initializer_spec.rb b/spec/lib/gitlab/omniauth_initializer_spec.rb index ef5c93e5c6b..99684bb2ab2 100644 --- a/spec/lib/gitlab/omniauth_initializer_spec.rb +++ b/spec/lib/gitlab/omniauth_initializer_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::OmniauthInitializer do diff --git a/spec/lib/gitlab/optimistic_locking_spec.rb b/spec/lib/gitlab/optimistic_locking_spec.rb index 6fdf61ee0a7..9dfcb775dfa 100644 --- a/spec/lib/gitlab/optimistic_locking_spec.rb +++ b/spec/lib/gitlab/optimistic_locking_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::OptimisticLocking do diff --git a/spec/lib/gitlab/other_markup_spec.rb b/spec/lib/gitlab/other_markup_spec.rb index e26f39e193e..b5cf5b0999d 100644 --- a/spec/lib/gitlab/other_markup_spec.rb +++ b/spec/lib/gitlab/other_markup_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::OtherMarkup do diff --git a/spec/lib/gitlab/otp_key_rotator_spec.rb b/spec/lib/gitlab/otp_key_rotator_spec.rb index 6e6e9ce29ac..f5a567d5ea0 100644 --- a/spec/lib/gitlab/otp_key_rotator_spec.rb +++ b/spec/lib/gitlab/otp_key_rotator_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::OtpKeyRotator do diff --git a/spec/lib/gitlab/pages_client_spec.rb b/spec/lib/gitlab/pages_client_spec.rb index da6d26f4aee..84381843221 100644 --- a/spec/lib/gitlab/pages_client_spec.rb +++ b/spec/lib/gitlab/pages_client_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::PagesClient do diff --git a/spec/lib/gitlab/path_regex_spec.rb b/spec/lib/gitlab/path_regex_spec.rb index 84b2e2dc823..7dcdad7ff92 100644 --- a/spec/lib/gitlab/path_regex_spec.rb +++ b/spec/lib/gitlab/path_regex_spec.rb @@ -1,4 +1,6 @@ # coding: utf-8 +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::PathRegex do diff --git a/spec/lib/gitlab/performance_bar_spec.rb b/spec/lib/gitlab/performance_bar_spec.rb index 71c109db1f1..8d8ac2aebbe 100644 --- a/spec/lib/gitlab/performance_bar_spec.rb +++ b/spec/lib/gitlab/performance_bar_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::PerformanceBar do diff --git a/spec/lib/gitlab/phabricator_import/importer_spec.rb b/spec/lib/gitlab/phabricator_import/importer_spec.rb index bf14010a187..99a6e4dad6b 100644 --- a/spec/lib/gitlab/phabricator_import/importer_spec.rb +++ b/spec/lib/gitlab/phabricator_import/importer_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::PhabricatorImport::Importer do diff --git a/spec/lib/gitlab/phabricator_import/user_finder_spec.rb b/spec/lib/gitlab/phabricator_import/user_finder_spec.rb index 096321cda5f..918ff28c8f5 100644 --- a/spec/lib/gitlab/phabricator_import/user_finder_spec.rb +++ b/spec/lib/gitlab/phabricator_import/user_finder_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::PhabricatorImport::UserFinder, :clean_gitlab_redis_cache do diff --git a/spec/lib/gitlab/phabricator_import/worker_state_spec.rb b/spec/lib/gitlab/phabricator_import/worker_state_spec.rb index a44947445c9..b6f2524a9d0 100644 --- a/spec/lib/gitlab/phabricator_import/worker_state_spec.rb +++ b/spec/lib/gitlab/phabricator_import/worker_state_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::PhabricatorImport::WorkerState, :clean_gitlab_redis_shared_state do diff --git a/spec/lib/gitlab/plugin_spec.rb b/spec/lib/gitlab/plugin_spec.rb index 33dd4f79130..a8ddd774f3f 100644 --- a/spec/lib/gitlab/plugin_spec.rb +++ b/spec/lib/gitlab/plugin_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::Plugin do diff --git a/spec/lib/gitlab/polling_interval_spec.rb b/spec/lib/gitlab/polling_interval_spec.rb index eb8e618156b..979164269bd 100644 --- a/spec/lib/gitlab/polling_interval_spec.rb +++ b/spec/lib/gitlab/polling_interval_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::PollingInterval do diff --git a/spec/lib/gitlab/popen/runner_spec.rb b/spec/lib/gitlab/popen/runner_spec.rb index 2e2cb4ca28f..de19106eaee 100644 --- a/spec/lib/gitlab/popen/runner_spec.rb +++ b/spec/lib/gitlab/popen/runner_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::Popen::Runner do diff --git a/spec/lib/gitlab/popen_spec.rb b/spec/lib/gitlab/popen_spec.rb index c1b84e9f077..29afd9df74e 100644 --- a/spec/lib/gitlab/popen_spec.rb +++ b/spec/lib/gitlab/popen_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::Popen do diff --git a/spec/lib/gitlab/profiler_spec.rb b/spec/lib/gitlab/profiler_spec.rb index 5af52db7a1f..a19392f4bcb 100644 --- a/spec/lib/gitlab/profiler_spec.rb +++ b/spec/lib/gitlab/profiler_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::Profiler do diff --git a/spec/lib/gitlab/project_authorizations_spec.rb b/spec/lib/gitlab/project_authorizations_spec.rb index 75e2d5e1319..82ccb42f8a6 100644 --- a/spec/lib/gitlab/project_authorizations_spec.rb +++ b/spec/lib/gitlab/project_authorizations_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::ProjectAuthorizations do diff --git a/spec/lib/gitlab/project_search_results_spec.rb b/spec/lib/gitlab/project_search_results_spec.rb index 4a41d5cf51e..0dbfcf96124 100644 --- a/spec/lib/gitlab/project_search_results_spec.rb +++ b/spec/lib/gitlab/project_search_results_spec.rb @@ -1,4 +1,6 @@ # coding: utf-8 +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::ProjectSearchResults do @@ -22,6 +24,28 @@ describe Gitlab::ProjectSearchResults do it { expect(results.query).to eq('hello world') } end + describe '#formatted_count' do + using RSpec::Parameterized::TableSyntax + + let(:results) { described_class.new(user, project, query) } + + where(:scope, :count_method, :expected) do + 'blobs' | :blobs_count | '1234' + 'notes' | :limited_notes_count | '1000+' + 'wiki_blobs' | :wiki_blobs_count | '1234' + 'commits' | :commits_count | '1234' + 'projects' | :limited_projects_count | '1000+' + 'unknown' | nil | nil + end + + with_them do + it 'returns the expected formatted count' do + expect(results).to receive(count_method).and_return(1234) if count_method + expect(results.formatted_count(scope)).to eq(expected) + end + end + end + shared_examples 'general blob search' do |entity_type, blob_kind| let(:query) { 'files' } subject(:results) { described_class.new(user, project, query).objects(blob_type) } diff --git a/spec/lib/gitlab/project_template_spec.rb b/spec/lib/gitlab/project_template_spec.rb index 8b82ea7faa5..83acd979a80 100644 --- a/spec/lib/gitlab/project_template_spec.rb +++ b/spec/lib/gitlab/project_template_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::ProjectTemplate do @@ -28,6 +30,18 @@ describe Gitlab::ProjectTemplate do end end + describe '#project_path' do + subject { described_class.new('name', 'title', 'description', 'https://gitlab.com/some/project/path').project_path } + + it { is_expected.to eq 'some/project/path' } + end + + describe '#uri_encoded_project_path' do + subject { described_class.new('name', 'title', 'description', 'https://gitlab.com/some/project/path').uri_encoded_project_path } + + it { is_expected.to eq 'some%2Fproject%2Fpath' } + end + describe '.find' do subject { described_class.find(query) } diff --git a/spec/lib/gitlab/project_transfer_spec.rb b/spec/lib/gitlab/project_transfer_spec.rb index 0b9b1f537b5..d54817ea02b 100644 --- a/spec/lib/gitlab/project_transfer_spec.rb +++ b/spec/lib/gitlab/project_transfer_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::ProjectTransfer do diff --git a/spec/lib/gitlab/prometheus/additional_metrics_parser_spec.rb b/spec/lib/gitlab/prometheus/additional_metrics_parser_spec.rb index 1a108003bc2..3f97a69b5eb 100644 --- a/spec/lib/gitlab/prometheus/additional_metrics_parser_spec.rb +++ b/spec/lib/gitlab/prometheus/additional_metrics_parser_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::Prometheus::AdditionalMetricsParser do diff --git a/spec/lib/gitlab/prometheus/queries/additional_metrics_deployment_query_spec.rb b/spec/lib/gitlab/prometheus/queries/additional_metrics_deployment_query_spec.rb index c7169717fc1..4bdc57c8c04 100644 --- a/spec/lib/gitlab/prometheus/queries/additional_metrics_deployment_query_spec.rb +++ b/spec/lib/gitlab/prometheus/queries/additional_metrics_deployment_query_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::Prometheus::Queries::AdditionalMetricsDeploymentQuery do diff --git a/spec/lib/gitlab/prometheus/queries/additional_metrics_environment_query_spec.rb b/spec/lib/gitlab/prometheus/queries/additional_metrics_environment_query_spec.rb index a6589f0c0a3..35dbdd55cfa 100644 --- a/spec/lib/gitlab/prometheus/queries/additional_metrics_environment_query_spec.rb +++ b/spec/lib/gitlab/prometheus/queries/additional_metrics_environment_query_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::Prometheus::Queries::AdditionalMetricsEnvironmentQuery do diff --git a/spec/lib/gitlab/prometheus/queries/deployment_query_spec.rb b/spec/lib/gitlab/prometheus/queries/deployment_query_spec.rb index ffe3ad85baa..0ad2de218fe 100644 --- a/spec/lib/gitlab/prometheus/queries/deployment_query_spec.rb +++ b/spec/lib/gitlab/prometheus/queries/deployment_query_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::Prometheus::Queries::DeploymentQuery do diff --git a/spec/lib/gitlab/prometheus/queries/matched_metric_query_spec.rb b/spec/lib/gitlab/prometheus/queries/matched_metric_query_spec.rb index 936447b8474..35034d814bf 100644 --- a/spec/lib/gitlab/prometheus/queries/matched_metric_query_spec.rb +++ b/spec/lib/gitlab/prometheus/queries/matched_metric_query_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::Prometheus::Queries::MatchedMetricQuery do diff --git a/spec/lib/gitlab/prometheus_client_spec.rb b/spec/lib/gitlab/prometheus_client_spec.rb index 0a4e8dbced5..86a1c14ed3f 100644 --- a/spec/lib/gitlab/prometheus_client_spec.rb +++ b/spec/lib/gitlab/prometheus_client_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::PrometheusClient do diff --git a/spec/lib/gitlab/query_limiting/active_support_subscriber_spec.rb b/spec/lib/gitlab/query_limiting/active_support_subscriber_spec.rb index f8faeffb935..2db6d2fb60f 100644 --- a/spec/lib/gitlab/query_limiting/active_support_subscriber_spec.rb +++ b/spec/lib/gitlab/query_limiting/active_support_subscriber_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::QueryLimiting::ActiveSupportSubscriber do diff --git a/spec/lib/gitlab/query_limiting/middleware_spec.rb b/spec/lib/gitlab/query_limiting/middleware_spec.rb index a04bcdecb4b..fb1c30118c2 100644 --- a/spec/lib/gitlab/query_limiting/middleware_spec.rb +++ b/spec/lib/gitlab/query_limiting/middleware_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::QueryLimiting::Middleware do diff --git a/spec/lib/gitlab/query_limiting/transaction_spec.rb b/spec/lib/gitlab/query_limiting/transaction_spec.rb index b72b8574174..39d5a575efc 100644 --- a/spec/lib/gitlab/query_limiting/transaction_spec.rb +++ b/spec/lib/gitlab/query_limiting/transaction_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::QueryLimiting::Transaction do diff --git a/spec/lib/gitlab/query_limiting_spec.rb b/spec/lib/gitlab/query_limiting_spec.rb index 42877b1e2dd..f0d0340cd6e 100644 --- a/spec/lib/gitlab/query_limiting_spec.rb +++ b/spec/lib/gitlab/query_limiting_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::QueryLimiting do diff --git a/spec/lib/gitlab/quick_actions/command_definition_spec.rb b/spec/lib/gitlab/quick_actions/command_definition_spec.rb index 21f2c87a755..45b710adf07 100644 --- a/spec/lib/gitlab/quick_actions/command_definition_spec.rb +++ b/spec/lib/gitlab/quick_actions/command_definition_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::QuickActions::CommandDefinition do diff --git a/spec/lib/gitlab/quick_actions/dsl_spec.rb b/spec/lib/gitlab/quick_actions/dsl_spec.rb index 78b9b3804c3..c98c36622f5 100644 --- a/spec/lib/gitlab/quick_actions/dsl_spec.rb +++ b/spec/lib/gitlab/quick_actions/dsl_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::QuickActions::Dsl do diff --git a/spec/lib/gitlab/quick_actions/extractor_spec.rb b/spec/lib/gitlab/quick_actions/extractor_spec.rb index 873bb359d6e..f1acb5b7049 100644 --- a/spec/lib/gitlab/quick_actions/extractor_spec.rb +++ b/spec/lib/gitlab/quick_actions/extractor_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::QuickActions::Extractor do diff --git a/spec/lib/gitlab/quick_actions/spend_time_and_date_separator_spec.rb b/spec/lib/gitlab/quick_actions/spend_time_and_date_separator_spec.rb index 8b58f0b3725..fd149cd1114 100644 --- a/spec/lib/gitlab/quick_actions/spend_time_and_date_separator_spec.rb +++ b/spec/lib/gitlab/quick_actions/spend_time_and_date_separator_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::QuickActions::SpendTimeAndDateSeparator do diff --git a/spec/lib/gitlab/quick_actions/substitution_definition_spec.rb b/spec/lib/gitlab/quick_actions/substitution_definition_spec.rb index 1bb8bc51c96..e4f25bc35a9 100644 --- a/spec/lib/gitlab/quick_actions/substitution_definition_spec.rb +++ b/spec/lib/gitlab/quick_actions/substitution_definition_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::QuickActions::SubstitutionDefinition do diff --git a/spec/lib/gitlab/redis/cache_spec.rb b/spec/lib/gitlab/redis/cache_spec.rb index 5a4f17cfcf6..0718998f981 100644 --- a/spec/lib/gitlab/redis/cache_spec.rb +++ b/spec/lib/gitlab/redis/cache_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::Redis::Cache do diff --git a/spec/lib/gitlab/redis/queues_spec.rb b/spec/lib/gitlab/redis/queues_spec.rb index 01ca25635a9..93207b6f469 100644 --- a/spec/lib/gitlab/redis/queues_spec.rb +++ b/spec/lib/gitlab/redis/queues_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::Redis::Queues do diff --git a/spec/lib/gitlab/redis/shared_state_spec.rb b/spec/lib/gitlab/redis/shared_state_spec.rb index 24b73745dc5..aa61fd99eb5 100644 --- a/spec/lib/gitlab/redis/shared_state_spec.rb +++ b/spec/lib/gitlab/redis/shared_state_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::Redis::SharedState do diff --git a/spec/lib/gitlab/redis/wrapper_spec.rb b/spec/lib/gitlab/redis/wrapper_spec.rb index 0c22a0d62cc..e4cc42130db 100644 --- a/spec/lib/gitlab/redis/wrapper_spec.rb +++ b/spec/lib/gitlab/redis/wrapper_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::Redis::Wrapper do diff --git a/spec/lib/gitlab/reference_counter_spec.rb b/spec/lib/gitlab/reference_counter_spec.rb index b2344d1870a..f9361d08faf 100644 --- a/spec/lib/gitlab/reference_counter_spec.rb +++ b/spec/lib/gitlab/reference_counter_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::ReferenceCounter do diff --git a/spec/lib/gitlab/regex_spec.rb b/spec/lib/gitlab/regex_spec.rb index ba295386a55..e19210d8fbf 100644 --- a/spec/lib/gitlab/regex_spec.rb +++ b/spec/lib/gitlab/regex_spec.rb @@ -1,4 +1,6 @@ # coding: utf-8 +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::Regex do diff --git a/spec/lib/gitlab/repo_path_spec.rb b/spec/lib/gitlab/repo_path_spec.rb index 8fbda929064..cffd7cc89e7 100644 --- a/spec/lib/gitlab/repo_path_spec.rb +++ b/spec/lib/gitlab/repo_path_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe ::Gitlab::RepoPath do diff --git a/spec/lib/gitlab/repository_cache_adapter_spec.rb b/spec/lib/gitlab/repository_cache_adapter_spec.rb index 0295138fc3a..fd1338b55a6 100644 --- a/spec/lib/gitlab/repository_cache_adapter_spec.rb +++ b/spec/lib/gitlab/repository_cache_adapter_spec.rb @@ -1,9 +1,12 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::RepositoryCacheAdapter do let(:project) { create(:project, :repository) } let(:repository) { project.repository } let(:cache) { repository.send(:cache) } + let(:redis_set_cache) { repository.send(:redis_set_cache) } describe '#cache_method_output', :use_clean_rails_memory_store_caching do let(:fallback) { 10 } @@ -206,9 +209,11 @@ describe Gitlab::RepositoryCacheAdapter do describe '#expire_method_caches' do it 'expires the caches of the given methods' do expect(cache).to receive(:expire).with(:rendered_readme) - expect(cache).to receive(:expire).with(:gitignore) + expect(cache).to receive(:expire).with(:branch_names) + expect(redis_set_cache).to receive(:expire).with(:rendered_readme) + expect(redis_set_cache).to receive(:expire).with(:branch_names) - repository.expire_method_caches(%i(rendered_readme gitignore)) + repository.expire_method_caches(%i(rendered_readme branch_names)) end it 'does not expire caches for non-existent methods' do diff --git a/spec/lib/gitlab/repository_cache_spec.rb b/spec/lib/gitlab/repository_cache_spec.rb index 741ee12633f..6a684595eb8 100644 --- a/spec/lib/gitlab/repository_cache_spec.rb +++ b/spec/lib/gitlab/repository_cache_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::RepositoryCache do diff --git a/spec/lib/gitlab/repository_set_cache_spec.rb b/spec/lib/gitlab/repository_set_cache_spec.rb new file mode 100644 index 00000000000..87e51f801e5 --- /dev/null +++ b/spec/lib/gitlab/repository_set_cache_spec.rb @@ -0,0 +1,75 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::RepositorySetCache, :clean_gitlab_redis_cache do + let(:project) { create(:project) } + let(:repository) { project.repository } + let(:namespace) { "#{repository.full_path}:#{project.id}" } + let(:cache) { described_class.new(repository) } + + describe '#cache_key' do + subject { cache.cache_key(:foo) } + + it 'includes the namespace' do + is_expected.to eq("foo:#{namespace}:set") + end + + context 'with a given namespace' do + let(:extra_namespace) { 'my:data' } + let(:cache) { described_class.new(repository, extra_namespace: extra_namespace) } + + it 'includes the full namespace' do + is_expected.to eq("foo:#{namespace}:#{extra_namespace}:set") + end + end + end + + describe '#expire' do + it 'expires the given key from the cache' do + cache.write(:foo, ['value']) + + expect(cache.read(:foo)).to contain_exactly('value') + expect(cache.expire(:foo)).to eq(1) + expect(cache.read(:foo)).to be_empty + end + end + + describe '#exist?' do + it 'checks whether the key exists' do + expect(cache.exist?(:foo)).to be(false) + + cache.write(:foo, ['value']) + + expect(cache.exist?(:foo)).to be(true) + end + end + + describe '#fetch' do + let(:blk) { -> { ['block value'] } } + + subject { cache.fetch(:foo, &blk) } + + it 'fetches the key from the cache when filled' do + cache.write(:foo, ['value']) + + is_expected.to contain_exactly('value') + end + + it 'writes the value of the provided block when empty' do + cache.expire(:foo) + + is_expected.to contain_exactly('block value') + expect(cache.read(:foo)).to contain_exactly('block value') + end + end + + describe '#include?' do + it 'checks inclusion in the Redis set' do + cache.write(:foo, ['value']) + + expect(cache.include?(:foo, 'value')).to be(true) + expect(cache.include?(:foo, 'bar')).to be(false) + end + end +end diff --git a/spec/lib/gitlab/request_context_spec.rb b/spec/lib/gitlab/request_context_spec.rb index 23e45aff1c5..a744f48da1f 100644 --- a/spec/lib/gitlab/request_context_spec.rb +++ b/spec/lib/gitlab/request_context_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::RequestContext do diff --git a/spec/lib/gitlab/request_forgery_protection_spec.rb b/spec/lib/gitlab/request_forgery_protection_spec.rb index 305de613866..b7a3dc16eff 100644 --- a/spec/lib/gitlab/request_forgery_protection_spec.rb +++ b/spec/lib/gitlab/request_forgery_protection_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::RequestForgeryProtection, :allow_forgery_protection do diff --git a/spec/lib/gitlab/request_profiler/profile_spec.rb b/spec/lib/gitlab/request_profiler/profile_spec.rb index b37ee558e1a..a75f3c66156 100644 --- a/spec/lib/gitlab/request_profiler/profile_spec.rb +++ b/spec/lib/gitlab/request_profiler/profile_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'fast_spec_helper' describe Gitlab::RequestProfiler::Profile do diff --git a/spec/lib/gitlab/request_profiler_spec.rb b/spec/lib/gitlab/request_profiler_spec.rb index 498c045b6cd..f157189a72d 100644 --- a/spec/lib/gitlab/request_profiler_spec.rb +++ b/spec/lib/gitlab/request_profiler_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::RequestProfiler do diff --git a/spec/lib/gitlab/route_map_spec.rb b/spec/lib/gitlab/route_map_spec.rb index a39c774429e..d5e70b91fb4 100644 --- a/spec/lib/gitlab/route_map_spec.rb +++ b/spec/lib/gitlab/route_map_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::RouteMap do diff --git a/spec/lib/gitlab/routing_spec.rb b/spec/lib/gitlab/routing_spec.rb index 01d5acfc15b..965564cb83b 100644 --- a/spec/lib/gitlab/routing_spec.rb +++ b/spec/lib/gitlab/routing_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::Routing do diff --git a/spec/lib/gitlab/sanitizers/exif_spec.rb b/spec/lib/gitlab/sanitizers/exif_spec.rb index bd5f330c7a1..22c5f27dc6d 100644 --- a/spec/lib/gitlab/sanitizers/exif_spec.rb +++ b/spec/lib/gitlab/sanitizers/exif_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::Sanitizers::Exif do diff --git a/spec/lib/gitlab/sanitizers/svg_spec.rb b/spec/lib/gitlab/sanitizers/svg_spec.rb index df46a874528..a8c7495376d 100644 --- a/spec/lib/gitlab/sanitizers/svg_spec.rb +++ b/spec/lib/gitlab/sanitizers/svg_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::Sanitizers::SVG do diff --git a/spec/lib/gitlab/search/found_blob_spec.rb b/spec/lib/gitlab/search/found_blob_spec.rb index da263bc7523..3496fb29836 100644 --- a/spec/lib/gitlab/search/found_blob_spec.rb +++ b/spec/lib/gitlab/search/found_blob_spec.rb @@ -1,4 +1,5 @@ # coding: utf-8 +# frozen_string_literal: true require 'spec_helper' @@ -108,7 +109,7 @@ describe Gitlab::Search::FoundBlob do end context 'with ISO-8859-1' do - let(:search_result) { "master:encoding/iso8859.txt\x001\x00\xC4\xFC\nmaster:encoding/iso8859.txt\x002\x00\nmaster:encoding/iso8859.txt\x003\x00foo\n".force_encoding(Encoding::ASCII_8BIT) } + let(:search_result) { (+"master:encoding/iso8859.txt\x001\x00\xC4\xFC\nmaster:encoding/iso8859.txt\x002\x00\nmaster:encoding/iso8859.txt\x003\x00foo\n").force_encoding(Encoding::ASCII_8BIT) } it 'returns results as UTF-8' do expect(subject.filename).to eq('encoding/iso8859.txt') diff --git a/spec/lib/gitlab/search/query_spec.rb b/spec/lib/gitlab/search/query_spec.rb index 2d00428fffa..112e9a59f04 100644 --- a/spec/lib/gitlab/search/query_spec.rb +++ b/spec/lib/gitlab/search/query_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::Search::Query do diff --git a/spec/lib/gitlab/search_results_spec.rb b/spec/lib/gitlab/search_results_spec.rb index 3d27156b356..5621c686b8a 100644 --- a/spec/lib/gitlab/search_results_spec.rb +++ b/spec/lib/gitlab/search_results_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::SearchResults do @@ -29,6 +31,43 @@ describe Gitlab::SearchResults do end end + describe '#formatted_count' do + using RSpec::Parameterized::TableSyntax + + where(:scope, :count_method, :expected) do + 'projects' | :limited_projects_count | '1000+' + 'issues' | :limited_issues_count | '1000+' + 'merge_requests' | :limited_merge_requests_count | '1000+' + 'milestones' | :limited_milestones_count | '1000+' + 'users' | :limited_users_count | '1000+' + 'unknown' | nil | nil + end + + with_them do + it 'returns the expected formatted count' do + expect(results).to receive(count_method).and_return(1234) if count_method + expect(results.formatted_count(scope)).to eq(expected) + end + end + end + + describe '#formatted_limited_count' do + using RSpec::Parameterized::TableSyntax + + where(:count, :expected) do + 23 | '23' + 1000 | '1000' + 1001 | '1000+' + 1234 | '1000+' + end + + with_them do + it 'returns the expected formatted limited count' do + expect(results.formatted_limited_count(count)).to eq(expected) + end + end + end + context "when count_limit is lower than total amount" do before do allow(results).to receive(:count_limit).and_return(1) diff --git a/spec/lib/gitlab/sentry_spec.rb b/spec/lib/gitlab/sentry_spec.rb index af8b059b984..9c4f3b8f42e 100644 --- a/spec/lib/gitlab/sentry_spec.rb +++ b/spec/lib/gitlab/sentry_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::Sentry do @@ -65,6 +67,7 @@ describe Gitlab::Sentry do context '.track_acceptable_exception' do let(:exception) { RuntimeError.new('boom') } + let(:issue_url) { 'http://gitlab.com/gitlab-org/gitlab-ce/issues/1' } before do allow(described_class).to receive(:enabled?).and_return(true) @@ -74,7 +77,7 @@ describe Gitlab::Sentry do it 'calls Raven.capture_exception' do expected_extras = { some_other_info: 'info', - issue_url: 'http://gitlab.com/gitlab-org/gitlab-ce/issues/1' + issue_url: issue_url } expected_tags = { @@ -88,9 +91,33 @@ describe Gitlab::Sentry do described_class.track_acceptable_exception( exception, - issue_url: 'http://gitlab.com/gitlab-org/gitlab-ce/issues/1', + issue_url: issue_url, extra: { some_other_info: 'info' } ) end + + context 'the exception implements :sentry_extra_data' do + let(:extra_info) { { event: 'explosion', size: :massive } } + let(:exception) { double(message: 'bang!', sentry_extra_data: extra_info) } + + it 'includes the extra data from the exception in the tracking information' do + expect(Raven).to receive(:capture_exception) + .with(exception, a_hash_including(extra: a_hash_including(extra_info))) + + described_class.track_acceptable_exception(exception) + end + end + + context 'the exception implements :sentry_extra_data, which returns nil' do + let(:exception) { double(message: 'bang!', sentry_extra_data: nil) } + + it 'just includes the other extra info' do + extra_info = { issue_url: issue_url } + expect(Raven).to receive(:capture_exception) + .with(exception, a_hash_including(extra: a_hash_including(extra_info))) + + described_class.track_acceptable_exception(exception, extra_info) + end + end end end diff --git a/spec/lib/gitlab/serializer/ci/variables_spec.rb b/spec/lib/gitlab/serializer/ci/variables_spec.rb index 1d1fd5b0763..900508420c9 100644 --- a/spec/lib/gitlab/serializer/ci/variables_spec.rb +++ b/spec/lib/gitlab/serializer/ci/variables_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'fast_spec_helper' describe Gitlab::Serializer::Ci::Variables do diff --git a/spec/lib/gitlab/serializer/pagination_spec.rb b/spec/lib/gitlab/serializer/pagination_spec.rb index c54be78f050..1e7f441f258 100644 --- a/spec/lib/gitlab/serializer/pagination_spec.rb +++ b/spec/lib/gitlab/serializer/pagination_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::Serializer::Pagination do diff --git a/spec/lib/gitlab/shard_health_cache_spec.rb b/spec/lib/gitlab/shard_health_cache_spec.rb index e1a69261939..f747849b5e9 100644 --- a/spec/lib/gitlab/shard_health_cache_spec.rb +++ b/spec/lib/gitlab/shard_health_cache_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::ShardHealthCache, :clean_gitlab_redis_cache do diff --git a/spec/lib/gitlab/shell_spec.rb b/spec/lib/gitlab/shell_spec.rb index bce2e754176..0ba16b93ee7 100644 --- a/spec/lib/gitlab/shell_spec.rb +++ b/spec/lib/gitlab/shell_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' require 'stringio' diff --git a/spec/lib/gitlab/sherlock/collection_spec.rb b/spec/lib/gitlab/sherlock/collection_spec.rb index 873ed14f804..bdc89c3d3cf 100644 --- a/spec/lib/gitlab/sherlock/collection_spec.rb +++ b/spec/lib/gitlab/sherlock/collection_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::Sherlock::Collection do diff --git a/spec/lib/gitlab/sherlock/file_sample_spec.rb b/spec/lib/gitlab/sherlock/file_sample_spec.rb index 394421504e0..b09ba5c62dc 100644 --- a/spec/lib/gitlab/sherlock/file_sample_spec.rb +++ b/spec/lib/gitlab/sherlock/file_sample_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::Sherlock::FileSample do diff --git a/spec/lib/gitlab/sherlock/line_profiler_spec.rb b/spec/lib/gitlab/sherlock/line_profiler_spec.rb index f2f8040fa0b..c1997606839 100644 --- a/spec/lib/gitlab/sherlock/line_profiler_spec.rb +++ b/spec/lib/gitlab/sherlock/line_profiler_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::Sherlock::LineProfiler do diff --git a/spec/lib/gitlab/sherlock/line_sample_spec.rb b/spec/lib/gitlab/sherlock/line_sample_spec.rb index 5f02f6a3213..b68e8cc0266 100644 --- a/spec/lib/gitlab/sherlock/line_sample_spec.rb +++ b/spec/lib/gitlab/sherlock/line_sample_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::Sherlock::LineSample do diff --git a/spec/lib/gitlab/sherlock/location_spec.rb b/spec/lib/gitlab/sherlock/location_spec.rb index b295a624b35..7b40c84c2d1 100644 --- a/spec/lib/gitlab/sherlock/location_spec.rb +++ b/spec/lib/gitlab/sherlock/location_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::Sherlock::Location do diff --git a/spec/lib/gitlab/sherlock/middleware_spec.rb b/spec/lib/gitlab/sherlock/middleware_spec.rb index 2016023df06..8d6e362f622 100644 --- a/spec/lib/gitlab/sherlock/middleware_spec.rb +++ b/spec/lib/gitlab/sherlock/middleware_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::Sherlock::Middleware do diff --git a/spec/lib/gitlab/sherlock/query_spec.rb b/spec/lib/gitlab/sherlock/query_spec.rb index 426071c7f92..13c7e6f8f8b 100644 --- a/spec/lib/gitlab/sherlock/query_spec.rb +++ b/spec/lib/gitlab/sherlock/query_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::Sherlock::Query do diff --git a/spec/lib/gitlab/sherlock/transaction_spec.rb b/spec/lib/gitlab/sherlock/transaction_spec.rb index 4a14dfbec56..2245c3ee8e2 100644 --- a/spec/lib/gitlab/sherlock/transaction_spec.rb +++ b/spec/lib/gitlab/sherlock/transaction_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::Sherlock::Transaction do diff --git a/spec/lib/gitlab/sidekiq_config_spec.rb b/spec/lib/gitlab/sidekiq_config_spec.rb index 0c66d764851..1e8ccb447b1 100644 --- a/spec/lib/gitlab/sidekiq_config_spec.rb +++ b/spec/lib/gitlab/sidekiq_config_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rails_helper' describe Gitlab::SidekiqConfig do diff --git a/spec/lib/gitlab/sidekiq_logging/json_formatter_spec.rb b/spec/lib/gitlab/sidekiq_logging/json_formatter_spec.rb index fed9aeba30c..a2cb38ec5b1 100644 --- a/spec/lib/gitlab/sidekiq_logging/json_formatter_spec.rb +++ b/spec/lib/gitlab/sidekiq_logging/json_formatter_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::SidekiqLogging::JSONFormatter do diff --git a/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb b/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb index 5621d3d17d1..1b89c094a6b 100644 --- a/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb +++ b/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::SidekiqLogging::StructuredLogger do @@ -36,7 +38,9 @@ describe Gitlab::SidekiqLogging::StructuredLogger do 'message' => 'TestWorker JID-da883554ee4fe414012f5f42: done: 0.0 sec', 'job_status' => 'done', 'duration' => 0.0, - "completed_at" => timestamp.iso8601(3) + "completed_at" => timestamp.iso8601(3), + "system_s" => 0.0, + "user_s" => 0.0 ) end let(:exception_payload) do @@ -52,6 +56,13 @@ describe Gitlab::SidekiqLogging::StructuredLogger do allow(Sidekiq).to receive(:logger).and_return(logger) allow(subject).to receive(:current_time).and_return(timestamp.to_f) + + allow(Process).to receive(:times).and_return( + stime: 0.0, + utime: 0.0, + cutime: 0.0, + cstime: 0.0 + ) end subject { described_class.new } @@ -177,5 +188,31 @@ describe Gitlab::SidekiqLogging::StructuredLogger do end end end + + def ctime(times) + times[:cstime] + times[:cutime] + end + + context 'with ctime value greater than 0' do + let(:times_start) { { stime: 0.04999, utime: 0.0483, cstime: 0.0188, cutime: 0.0188 } } + let(:times_end) { { stime: 0.0699, utime: 0.0699, cstime: 0.0399, cutime: 0.0399 } } + + before do + end_payload['system_s'] = 0.02 + end_payload['user_s'] = 0.022 + end_payload['child_s'] = 0.042 + + allow(Process).to receive(:times).and_return(times_start, times_end) + end + + it 'logs with ctime data and other cpu data' do + Timecop.freeze(timestamp) do + expect(logger).to receive(:info).with(start_payload.except('args')).ordered + expect(logger).to receive(:info).with(end_payload.except('args')).ordered + + subject.call(job, 'test_queue') { } + end + end + end end end diff --git a/spec/lib/gitlab/sidekiq_middleware/memory_killer_spec.rb b/spec/lib/gitlab/sidekiq_middleware/memory_killer_spec.rb index 1de9a644610..bf3bc8e1add 100644 --- a/spec/lib/gitlab/sidekiq_middleware/memory_killer_spec.rb +++ b/spec/lib/gitlab/sidekiq_middleware/memory_killer_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::SidekiqMiddleware::MemoryKiller do diff --git a/spec/lib/gitlab/sidekiq_middleware/metrics_spec.rb b/spec/lib/gitlab/sidekiq_middleware/metrics_spec.rb index c6df1c6a0d8..e430599bd94 100644 --- a/spec/lib/gitlab/sidekiq_middleware/metrics_spec.rb +++ b/spec/lib/gitlab/sidekiq_middleware/metrics_spec.rb @@ -13,7 +13,7 @@ describe Gitlab::SidekiqMiddleware::Metrics do let(:running_jobs_metric) { double('running jobs metric') } before do - allow(Gitlab::Metrics).to receive(:histogram).with(:sidekiq_jobs_completion_seconds, anything).and_return(completion_seconds_metric) + allow(Gitlab::Metrics).to receive(:histogram).with(:sidekiq_jobs_completion_seconds, anything, anything).and_return(completion_seconds_metric) allow(Gitlab::Metrics).to receive(:counter).with(:sidekiq_jobs_failed_total, anything).and_return(failed_total_metric) allow(Gitlab::Metrics).to receive(:counter).with(:sidekiq_jobs_retried_total, anything).and_return(retried_total_metric) allow(Gitlab::Metrics).to receive(:gauge).with(:sidekiq_running_jobs, anything, {}, :livesum).and_return(running_jobs_metric) diff --git a/spec/lib/gitlab/sidekiq_middleware/monitor_spec.rb b/spec/lib/gitlab/sidekiq_middleware/monitor_spec.rb new file mode 100644 index 00000000000..7319cdc2399 --- /dev/null +++ b/spec/lib/gitlab/sidekiq_middleware/monitor_spec.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::SidekiqMiddleware::Monitor do + let(:monitor) { described_class.new } + + describe '#call' do + let(:worker) { double } + let(:job) { { 'jid' => 'job-id' } } + let(:queue) { 'my-queue' } + + it 'calls SidekiqMonitor' do + expect(Gitlab::SidekiqMonitor.instance).to receive(:within_job) + .with('job-id', 'my-queue') + .and_call_original + + expect { |blk| monitor.call(worker, job, queue, &blk) }.to yield_control + end + + it 'passthroughs the return value' do + result = monitor.call(worker, job, queue) do + 'value' + end + + expect(result).to eq('value') + end + + context 'when cancel happens' do + subject do + monitor.call(worker, job, queue) do + raise Gitlab::SidekiqMonitor::CancelledError + end + end + + it 'skips the job' do + expect { subject }.to raise_error(Sidekiq::JobRetry::Skip) + end + + it 'puts job in DeadSet' do + ::Sidekiq::DeadSet.new.clear + + expect do + subject rescue Sidekiq::JobRetry::Skip + end.to change { ::Sidekiq::DeadSet.new.size }.by(1) + end + end + end +end diff --git a/spec/lib/gitlab/sidekiq_monitor_spec.rb b/spec/lib/gitlab/sidekiq_monitor_spec.rb new file mode 100644 index 00000000000..bbd7bf90217 --- /dev/null +++ b/spec/lib/gitlab/sidekiq_monitor_spec.rb @@ -0,0 +1,261 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::SidekiqMonitor do + let(:monitor) { described_class.new } + + describe '#within_job' do + it 'tracks thread' do + blk = proc do + expect(monitor.jobs_thread['jid']).not_to be_nil + + "OK" + end + + expect(monitor.within_job('jid', 'queue', &blk)).to eq("OK") + end + + context 'when job is canceled' do + let(:jid) { SecureRandom.hex } + + before do + described_class.cancel_job(jid) + end + + it 'does not execute a block' do + expect do |blk| + monitor.within_job(jid, 'queue', &blk) + rescue described_class::CancelledError + end.not_to yield_control + end + + it 'raises exception' do + expect { monitor.within_job(jid, 'queue') }.to raise_error( + described_class::CancelledError) + end + end + end + + describe '#start_working' do + subject { monitor.send(:start_working) } + + before do + # we want to run at most once cycle + # we toggle `enabled?` flag after the first call + stub_const('Gitlab::SidekiqMonitor::RECONNECT_TIME', 0) + allow(monitor).to receive(:enabled?).and_return(true, false) + + allow(Sidekiq.logger).to receive(:info) + allow(Sidekiq.logger).to receive(:warn) + end + + context 'when structured logging is used' do + it 'logs start message' do + expect(Sidekiq.logger).to receive(:info) + .with( + class: described_class.to_s, + action: 'start', + message: 'Starting Monitor Daemon') + + expect(::Gitlab::Redis::SharedState).to receive(:with) + + subject + end + + it 'logs stop message' do + expect(Sidekiq.logger).to receive(:warn) + .with( + class: described_class.to_s, + action: 'stop', + message: 'Stopping Monitor Daemon') + + expect(::Gitlab::Redis::SharedState).to receive(:with) + + subject + end + + it 'logs StandardError message' do + expect(Sidekiq.logger).to receive(:warn) + .with( + class: described_class.to_s, + action: 'exception', + message: 'My Exception') + + expect(::Gitlab::Redis::SharedState).to receive(:with) + .and_raise(StandardError, 'My Exception') + + expect { subject }.not_to raise_error + end + + it 'logs and raises Exception message' do + expect(Sidekiq.logger).to receive(:warn) + .with( + class: described_class.to_s, + action: 'exception', + message: 'My Exception') + + expect(::Gitlab::Redis::SharedState).to receive(:with) + .and_raise(Exception, 'My Exception') + + expect { subject }.to raise_error(Exception, 'My Exception') + end + end + + context 'when StandardError is raised' do + it 'does retry connection' do + expect(::Gitlab::Redis::SharedState).to receive(:with) + .and_raise(StandardError, 'My Exception') + + expect(::Gitlab::Redis::SharedState).to receive(:with) + + # we expect to run `process_messages` twice + expect(monitor).to receive(:enabled?).and_return(true, true, false) + + subject + end + end + + context 'when message is published' do + let(:subscribed) { double } + + before do + expect_any_instance_of(::Redis).to receive(:subscribe) + .and_yield(subscribed) + + expect(subscribed).to receive(:message) + .and_yield( + described_class::NOTIFICATION_CHANNEL, + payload + ) + + expect(Sidekiq.logger).to receive(:info) + .with( + class: described_class.to_s, + action: 'start', + message: 'Starting Monitor Daemon') + + expect(Sidekiq.logger).to receive(:info) + .with( + class: described_class.to_s, + channel: described_class::NOTIFICATION_CHANNEL, + message: 'Received payload on channel', + payload: payload + ) + end + + context 'and message is valid' do + let(:payload) { '{"action":"cancel","jid":"my-jid"}' } + + it 'processes cancel' do + expect(monitor).to receive(:process_job_cancel).with('my-jid') + + subject + end + end + + context 'and message is not valid json' do + let(:payload) { '{"action"}' } + + it 'skips processing' do + expect(monitor).not_to receive(:process_job_cancel) + + subject + end + end + end + end + + describe '#stop' do + let!(:monitor_thread) { monitor.start } + + it 'does stop the thread' do + expect(monitor_thread).to be_alive + + expect { monitor.stop }.not_to raise_error + + expect(monitor_thread).not_to be_alive + expect { monitor_thread.value }.to raise_error(Interrupt) + end + end + + describe '#process_job_cancel' do + subject { monitor.send(:process_job_cancel, jid) } + + context 'when jid is missing' do + let(:jid) { nil } + + it 'does not run thread' do + expect(subject).to be_nil + end + end + + context 'when jid is provided' do + let(:jid) { 'my-jid' } + + context 'when jid is not found' do + it 'does not log cancellation message' do + expect(Sidekiq.logger).not_to receive(:warn) + expect(subject).to be_nil + end + end + + context 'when jid is found' do + let(:thread) { Thread.new { sleep 1000 } } + + before do + monitor.jobs_thread[jid] = thread + end + + after do + thread.kill + rescue + end + + it 'does log cancellation message' do + expect(Sidekiq.logger).to receive(:warn) + .with( + class: described_class.to_s, + action: 'cancel', + message: 'Canceling thread with CancelledError', + jid: 'my-jid', + thread_id: thread.object_id) + + expect(subject).to be_a(Thread) + + subject.join + end + + it 'does cancel the thread' do + expect(subject).to be_a(Thread) + + subject.join + + # we wait for the thread to be cancelled + # by `process_job_cancel` + expect { thread.join(5) }.to raise_error(described_class::CancelledError) + end + end + end + end + + describe '.cancel_job' do + subject { described_class.cancel_job('my-jid') } + + it 'sets a redis key' do + expect_any_instance_of(::Redis).to receive(:setex) + .with('sidekiq:cancel:my-jid', anything, 1) + + subject + end + + it 'notifies all workers' do + payload = '{"action":"cancel","jid":"my-jid"}' + + expect_any_instance_of(::Redis).to receive(:publish) + .with('sidekiq:cancel:notifications', payload) + + subject + end + end +end diff --git a/spec/lib/gitlab/sidekiq_signals_spec.rb b/spec/lib/gitlab/sidekiq_signals_spec.rb index 77ecd1840d2..10f1bad32cd 100644 --- a/spec/lib/gitlab/sidekiq_signals_spec.rb +++ b/spec/lib/gitlab/sidekiq_signals_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::SidekiqSignals do diff --git a/spec/lib/gitlab/sidekiq_status/client_middleware_spec.rb b/spec/lib/gitlab/sidekiq_status/client_middleware_spec.rb index 37d9e1d3e6b..1ca8cea66fc 100644 --- a/spec/lib/gitlab/sidekiq_status/client_middleware_spec.rb +++ b/spec/lib/gitlab/sidekiq_status/client_middleware_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::SidekiqStatus::ClientMiddleware do diff --git a/spec/lib/gitlab/sidekiq_status/server_middleware_spec.rb b/spec/lib/gitlab/sidekiq_status/server_middleware_spec.rb index 04e09d3dec8..40bcb49d1d3 100644 --- a/spec/lib/gitlab/sidekiq_status/server_middleware_spec.rb +++ b/spec/lib/gitlab/sidekiq_status/server_middleware_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::SidekiqStatus::ServerMiddleware do diff --git a/spec/lib/gitlab/sidekiq_status_spec.rb b/spec/lib/gitlab/sidekiq_status_spec.rb index 884f27b212c..7b5c75b2f3b 100644 --- a/spec/lib/gitlab/sidekiq_status_spec.rb +++ b/spec/lib/gitlab/sidekiq_status_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::SidekiqStatus do diff --git a/spec/lib/gitlab/sidekiq_versioning/manager_spec.rb b/spec/lib/gitlab/sidekiq_versioning/manager_spec.rb index 7debf70a16f..2aa7d1fd6d8 100644 --- a/spec/lib/gitlab/sidekiq_versioning/manager_spec.rb +++ b/spec/lib/gitlab/sidekiq_versioning/manager_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::SidekiqVersioning::Manager do diff --git a/spec/lib/gitlab/sidekiq_versioning_spec.rb b/spec/lib/gitlab/sidekiq_versioning_spec.rb index fa6d42e730d..dade5961775 100644 --- a/spec/lib/gitlab/sidekiq_versioning_spec.rb +++ b/spec/lib/gitlab/sidekiq_versioning_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::SidekiqVersioning, :sidekiq, :redis do diff --git a/spec/lib/gitlab/slash_commands/command_spec.rb b/spec/lib/gitlab/slash_commands/command_spec.rb index eceacac58af..c4ea8cbf2b1 100644 --- a/spec/lib/gitlab/slash_commands/command_spec.rb +++ b/spec/lib/gitlab/slash_commands/command_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::SlashCommands::Command do diff --git a/spec/lib/gitlab/slash_commands/deploy_spec.rb b/spec/lib/gitlab/slash_commands/deploy_spec.rb index 25f3e8a0409..93a724d8e12 100644 --- a/spec/lib/gitlab/slash_commands/deploy_spec.rb +++ b/spec/lib/gitlab/slash_commands/deploy_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::SlashCommands::Deploy do diff --git a/spec/lib/gitlab/slash_commands/issue_move_spec.rb b/spec/lib/gitlab/slash_commands/issue_move_spec.rb index 9a990e1fad7..962ac3668bc 100644 --- a/spec/lib/gitlab/slash_commands/issue_move_spec.rb +++ b/spec/lib/gitlab/slash_commands/issue_move_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::SlashCommands::IssueMove, service: true do diff --git a/spec/lib/gitlab/slash_commands/issue_new_spec.rb b/spec/lib/gitlab/slash_commands/issue_new_spec.rb index 59de11766d8..90f0518a63e 100644 --- a/spec/lib/gitlab/slash_commands/issue_new_spec.rb +++ b/spec/lib/gitlab/slash_commands/issue_new_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::SlashCommands::IssueNew do diff --git a/spec/lib/gitlab/slash_commands/issue_search_spec.rb b/spec/lib/gitlab/slash_commands/issue_search_spec.rb index 47787307990..b766a9a1361 100644 --- a/spec/lib/gitlab/slash_commands/issue_search_spec.rb +++ b/spec/lib/gitlab/slash_commands/issue_search_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::SlashCommands::IssueSearch do diff --git a/spec/lib/gitlab/slash_commands/issue_show_spec.rb b/spec/lib/gitlab/slash_commands/issue_show_spec.rb index 5c4ba2736ba..e53f79dcd86 100644 --- a/spec/lib/gitlab/slash_commands/issue_show_spec.rb +++ b/spec/lib/gitlab/slash_commands/issue_show_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::SlashCommands::IssueShow do diff --git a/spec/lib/gitlab/slash_commands/presenters/access_spec.rb b/spec/lib/gitlab/slash_commands/presenters/access_spec.rb index ef3d217f7be..286fec892e6 100644 --- a/spec/lib/gitlab/slash_commands/presenters/access_spec.rb +++ b/spec/lib/gitlab/slash_commands/presenters/access_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::SlashCommands::Presenters::Access do diff --git a/spec/lib/gitlab/slash_commands/presenters/deploy_spec.rb b/spec/lib/gitlab/slash_commands/presenters/deploy_spec.rb index d16d122c64e..9c2e9ab982f 100644 --- a/spec/lib/gitlab/slash_commands/presenters/deploy_spec.rb +++ b/spec/lib/gitlab/slash_commands/presenters/deploy_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::SlashCommands::Presenters::Deploy do diff --git a/spec/lib/gitlab/slash_commands/presenters/issue_move_spec.rb b/spec/lib/gitlab/slash_commands/presenters/issue_move_spec.rb index 58c341a284e..56b64d32192 100644 --- a/spec/lib/gitlab/slash_commands/presenters/issue_move_spec.rb +++ b/spec/lib/gitlab/slash_commands/presenters/issue_move_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::SlashCommands::Presenters::IssueMove do diff --git a/spec/lib/gitlab/slash_commands/presenters/issue_new_spec.rb b/spec/lib/gitlab/slash_commands/presenters/issue_new_spec.rb index 76e4bad88fd..f926783fbea 100644 --- a/spec/lib/gitlab/slash_commands/presenters/issue_new_spec.rb +++ b/spec/lib/gitlab/slash_commands/presenters/issue_new_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::SlashCommands::Presenters::IssueNew do diff --git a/spec/lib/gitlab/slash_commands/presenters/issue_search_spec.rb b/spec/lib/gitlab/slash_commands/presenters/issue_search_spec.rb index 5a7ec0685fe..e1c011133c4 100644 --- a/spec/lib/gitlab/slash_commands/presenters/issue_search_spec.rb +++ b/spec/lib/gitlab/slash_commands/presenters/issue_search_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::SlashCommands::Presenters::IssueSearch do diff --git a/spec/lib/gitlab/slash_commands/presenters/issue_show_spec.rb b/spec/lib/gitlab/slash_commands/presenters/issue_show_spec.rb index 8f607d7a9c9..56d6bf1c788 100644 --- a/spec/lib/gitlab/slash_commands/presenters/issue_show_spec.rb +++ b/spec/lib/gitlab/slash_commands/presenters/issue_show_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::SlashCommands::Presenters::IssueShow do diff --git a/spec/lib/gitlab/snippet_search_results_spec.rb b/spec/lib/gitlab/snippet_search_results_spec.rb index b661a894c0c..89d290aaa81 100644 --- a/spec/lib/gitlab/snippet_search_results_spec.rb +++ b/spec/lib/gitlab/snippet_search_results_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::SnippetSearchResults do @@ -16,4 +18,22 @@ describe Gitlab::SnippetSearchResults do expect(results.snippet_blobs_count).to eq(1) end end + + describe '#formatted_count' do + using RSpec::Parameterized::TableSyntax + + where(:scope, :count_method, :expected) do + 'snippet_titles' | :snippet_titles_count | '1234' + 'snippet_blobs' | :snippet_blobs_count | '1234' + 'projects' | :limited_projects_count | '1000+' + 'unknown' | nil | nil + end + + with_them do + it 'returns the expected formatted count' do + expect(results).to receive(count_method).and_return(1234) if count_method + expect(results.formatted_count(scope)).to eq(expected) + end + end + end end diff --git a/spec/lib/gitlab/snowplow_tracker_spec.rb b/spec/lib/gitlab/snowplow_tracker_spec.rb new file mode 100644 index 00000000000..073a33e5973 --- /dev/null +++ b/spec/lib/gitlab/snowplow_tracker_spec.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true +require 'spec_helper' + +describe Gitlab::SnowplowTracker do + let(:timestamp) { Time.utc(2017, 3, 22) } + + around do |example| + Timecop.freeze(timestamp) { example.run } + end + + subject { described_class.track_event('epics', 'action', property: 'what', value: 'doit') } + + context '.track_event' do + context 'when Snowplow tracker is disabled' do + it 'does not track the event' do + expect(SnowplowTracker::Tracker).not_to receive(:new) + + subject + end + end + + context 'when Snowplow tracker is enabled' do + before do + stub_application_setting(snowplow_enabled: true) + stub_application_setting(snowplow_site_id: 'awesome gitlab') + stub_application_setting(snowplow_collector_hostname: 'url.com') + end + + it 'tracks the event' do + tracker = double + + expect(::SnowplowTracker::Tracker).to receive(:new) + .with( + an_instance_of(::SnowplowTracker::Emitter), + an_instance_of(::SnowplowTracker::Subject), + 'cf', 'awesome gitlab' + ).and_return(tracker) + expect(tracker).to receive(:track_struct_event) + .with('epics', 'action', nil, 'what', 'doit', nil, timestamp.to_i) + + subject + end + end + end +end diff --git a/spec/lib/gitlab/sql/cte_spec.rb b/spec/lib/gitlab/sql/cte_spec.rb index 5d2164491b5..e6194924f5a 100644 --- a/spec/lib/gitlab/sql/cte_spec.rb +++ b/spec/lib/gitlab/sql/cte_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::SQL::CTE do diff --git a/spec/lib/gitlab/sql/glob_spec.rb b/spec/lib/gitlab/sql/glob_spec.rb index 3147b52dcc5..83eed309ecc 100644 --- a/spec/lib/gitlab/sql/glob_spec.rb +++ b/spec/lib/gitlab/sql/glob_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::SQL::Glob do diff --git a/spec/lib/gitlab/sql/pattern_spec.rb b/spec/lib/gitlab/sql/pattern_spec.rb index 98838712eae..31944d51b3c 100644 --- a/spec/lib/gitlab/sql/pattern_spec.rb +++ b/spec/lib/gitlab/sql/pattern_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::SQL::Pattern do diff --git a/spec/lib/gitlab/sql/recursive_cte_spec.rb b/spec/lib/gitlab/sql/recursive_cte_spec.rb index 407a4d8a247..20e36c224b0 100644 --- a/spec/lib/gitlab/sql/recursive_cte_spec.rb +++ b/spec/lib/gitlab/sql/recursive_cte_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::SQL::RecursiveCTE do diff --git a/spec/lib/gitlab/sql/union_spec.rb b/spec/lib/gitlab/sql/union_spec.rb index fe6422c32b6..f8f6da19fa5 100644 --- a/spec/lib/gitlab/sql/union_spec.rb +++ b/spec/lib/gitlab/sql/union_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::SQL::Union do diff --git a/spec/lib/gitlab/ssh_public_key_spec.rb b/spec/lib/gitlab/ssh_public_key_spec.rb index a6ea07e8b6d..f8becb0c796 100644 --- a/spec/lib/gitlab/ssh_public_key_spec.rb +++ b/spec/lib/gitlab/ssh_public_key_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::SSHPublicKey, lib: true do diff --git a/spec/lib/gitlab/string_placeholder_replacer_spec.rb b/spec/lib/gitlab/string_placeholder_replacer_spec.rb index 7a03ea4154c..0295bf1265f 100644 --- a/spec/lib/gitlab/string_placeholder_replacer_spec.rb +++ b/spec/lib/gitlab/string_placeholder_replacer_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::StringPlaceholderReplacer do diff --git a/spec/lib/gitlab/string_range_marker_spec.rb b/spec/lib/gitlab/string_range_marker_spec.rb index 6bc02459dbd..7ed43db3d10 100644 --- a/spec/lib/gitlab/string_range_marker_spec.rb +++ b/spec/lib/gitlab/string_range_marker_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::StringRangeMarker do diff --git a/spec/lib/gitlab/string_regex_marker_spec.rb b/spec/lib/gitlab/string_regex_marker_spec.rb index 37b1298b962..2b19edbe7f9 100644 --- a/spec/lib/gitlab/string_regex_marker_spec.rb +++ b/spec/lib/gitlab/string_regex_marker_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::StringRegexMarker do diff --git a/spec/lib/gitlab/tcp_checker_spec.rb b/spec/lib/gitlab/tcp_checker_spec.rb index 4acf0334496..49f04f269ae 100644 --- a/spec/lib/gitlab/tcp_checker_spec.rb +++ b/spec/lib/gitlab/tcp_checker_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::TcpChecker do diff --git a/spec/lib/gitlab/template/finders/global_template_finder_spec.rb b/spec/lib/gitlab/template/finders/global_template_finder_spec.rb index c7f58fbd2a5..082ffa855b7 100644 --- a/spec/lib/gitlab/template/finders/global_template_finder_spec.rb +++ b/spec/lib/gitlab/template/finders/global_template_finder_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::Template::Finders::GlobalTemplateFinder do diff --git a/spec/lib/gitlab/template/finders/repo_template_finders_spec.rb b/spec/lib/gitlab/template/finders/repo_template_finders_spec.rb index e329d55d837..c8f2a37c5d6 100644 --- a/spec/lib/gitlab/template/finders/repo_template_finders_spec.rb +++ b/spec/lib/gitlab/template/finders/repo_template_finders_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::Template::Finders::RepoTemplateFinder do diff --git a/spec/lib/gitlab/template/gitignore_template_spec.rb b/spec/lib/gitlab/template/gitignore_template_spec.rb index 97797f42aaa..e8f632889ad 100644 --- a/spec/lib/gitlab/template/gitignore_template_spec.rb +++ b/spec/lib/gitlab/template/gitignore_template_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::Template::GitignoreTemplate do diff --git a/spec/lib/gitlab/template/gitlab_ci_yml_template_spec.rb b/spec/lib/gitlab/template/gitlab_ci_yml_template_spec.rb index 5f0a7e925ca..52e100768a7 100644 --- a/spec/lib/gitlab/template/gitlab_ci_yml_template_spec.rb +++ b/spec/lib/gitlab/template/gitlab_ci_yml_template_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::Template::GitlabCiYmlTemplate do diff --git a/spec/lib/gitlab/template/issue_template_spec.rb b/spec/lib/gitlab/template/issue_template_spec.rb index 7098499f996..54e46d3a9ec 100644 --- a/spec/lib/gitlab/template/issue_template_spec.rb +++ b/spec/lib/gitlab/template/issue_template_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::Template::IssueTemplate do diff --git a/spec/lib/gitlab/template/merge_request_template_spec.rb b/spec/lib/gitlab/template/merge_request_template_spec.rb index bd7ff64aa8a..bbc184d4dfc 100644 --- a/spec/lib/gitlab/template/merge_request_template_spec.rb +++ b/spec/lib/gitlab/template/merge_request_template_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::Template::MergeRequestTemplate do diff --git a/spec/lib/gitlab/themes_spec.rb b/spec/lib/gitlab/themes_spec.rb index a8213988f70..e0278eb9c7f 100644 --- a/spec/lib/gitlab/themes_spec.rb +++ b/spec/lib/gitlab/themes_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::Themes, lib: true do diff --git a/spec/lib/gitlab/tree_summary_spec.rb b/spec/lib/gitlab/tree_summary_spec.rb index e22f898dc4c..e15463ed0eb 100644 --- a/spec/lib/gitlab/tree_summary_spec.rb +++ b/spec/lib/gitlab/tree_summary_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::TreeSummary do diff --git a/spec/lib/gitlab/untrusted_regexp/ruby_syntax_spec.rb b/spec/lib/gitlab/untrusted_regexp/ruby_syntax_spec.rb index f1882e03581..68402e64012 100644 --- a/spec/lib/gitlab/untrusted_regexp/ruby_syntax_spec.rb +++ b/spec/lib/gitlab/untrusted_regexp/ruby_syntax_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'fast_spec_helper' require 'support/shared_examples/malicious_regexp_shared_examples' require 'support/helpers/stub_feature_flags' diff --git a/spec/lib/gitlab/untrusted_regexp_spec.rb b/spec/lib/gitlab/untrusted_regexp_spec.rb index 9d483f13a5e..4cc21e94a83 100644 --- a/spec/lib/gitlab/untrusted_regexp_spec.rb +++ b/spec/lib/gitlab/untrusted_regexp_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'fast_spec_helper' require 'support/shared_examples/malicious_regexp_shared_examples' diff --git a/spec/lib/gitlab/uploads_transfer_spec.rb b/spec/lib/gitlab/uploads_transfer_spec.rb index 4275e7b015b..16560fc8f12 100644 --- a/spec/lib/gitlab/uploads_transfer_spec.rb +++ b/spec/lib/gitlab/uploads_transfer_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::UploadsTransfer do diff --git a/spec/lib/gitlab/url_blocker_spec.rb b/spec/lib/gitlab/url_blocker_spec.rb index 45d9022abeb..df8a1f82f81 100644 --- a/spec/lib/gitlab/url_blocker_spec.rb +++ b/spec/lib/gitlab/url_blocker_spec.rb @@ -1,4 +1,6 @@ # coding: utf-8 +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::UrlBlocker do diff --git a/spec/lib/gitlab/url_builder_spec.rb b/spec/lib/gitlab/url_builder_spec.rb index bbcb92608d8..08d3c638f9e 100644 --- a/spec/lib/gitlab/url_builder_spec.rb +++ b/spec/lib/gitlab/url_builder_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::UrlBuilder do diff --git a/spec/lib/gitlab/url_sanitizer_spec.rb b/spec/lib/gitlab/url_sanitizer_spec.rb index 7242255d535..b39609c594b 100644 --- a/spec/lib/gitlab/url_sanitizer_spec.rb +++ b/spec/lib/gitlab/url_sanitizer_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::UrlSanitizer do diff --git a/spec/lib/gitlab/usage_data_counters/cycle_analytics_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/cycle_analytics_counter_spec.rb new file mode 100644 index 00000000000..71be37692e2 --- /dev/null +++ b/spec/lib/gitlab/usage_data_counters/cycle_analytics_counter_spec.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::UsageDataCounters::CycleAnalyticsCounter do + it_behaves_like 'a redis usage counter', 'CycleAnalytics', :views + + it_behaves_like 'a redis usage counter with totals', :cycle_analytics, views: 3 +end diff --git a/spec/lib/gitlab/usage_data_counters/note_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/note_counter_spec.rb index 1669a22879f..b385d1b07c7 100644 --- a/spec/lib/gitlab/usage_data_counters/note_counter_spec.rb +++ b/spec/lib/gitlab/usage_data_counters/note_counter_spec.rb @@ -26,16 +26,22 @@ describe Gitlab::UsageDataCounters::NoteCounter, :clean_gitlab_redis_shared_stat end it_behaves_like 'a note usage counter', :create, 'Snippet' + it_behaves_like 'a note usage counter', :create, 'MergeRequest' + it_behaves_like 'a note usage counter', :create, 'Commit' describe '.totals' do let(:combinations) do [ - [:create, 'Snippet', 3] + [:create, 'Snippet', 3], + [:create, 'MergeRequest', 4], + [:create, 'Commit', 5] ] end let(:expected_totals) do - { snippet_comment: 3 } + { snippet_comment: 3, + merge_request_comment: 4, + commit_comment: 5 } end before do @@ -57,14 +63,18 @@ describe Gitlab::UsageDataCounters::NoteCounter, :clean_gitlab_redis_shared_stat let(:unknown_event_error) { Gitlab::UsageDataCounters::BaseCounter::UnknownEvent } where(:event, :noteable_type, :expected_count, :should_raise) do - :create | 'Snippet' | 1 | false - :wibble | 'Snippet' | 0 | true - :create | 'Issue' | 0 | false - :wibble | 'Issue' | 0 | false + :create | 'Snippet' | 1 | false + :wibble | 'Snippet' | 0 | true + :create | 'MergeRequest' | 1 | false + :wibble | 'MergeRequest' | 0 | true + :create | 'Commit' | 1 | false + :wibble | 'Commit' | 0 | true + :create | 'Issue' | 0 | false + :wibble | 'Issue' | 0 | false end with_them do - it "handles event" do + it 'handles event' do if should_raise expect { described_class.count(event, noteable_type) }.to raise_error(unknown_event_error) else diff --git a/spec/lib/gitlab/usage_data_counters/source_code_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/source_code_counter_spec.rb new file mode 100644 index 00000000000..47077345e0c --- /dev/null +++ b/spec/lib/gitlab/usage_data_counters/source_code_counter_spec.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::UsageDataCounters::SourceCodeCounter do + it_behaves_like 'a redis usage counter', 'Source Code', :pushes + + it_behaves_like 'a redis usage counter with totals', :source_code, pushes: 5 +end diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb index bf36273251b..dda119e09b1 100644 --- a/spec/lib/gitlab/usage_data_spec.rb +++ b/spec/lib/gitlab/usage_data_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::UsageData do @@ -34,7 +36,7 @@ describe Gitlab::UsageData do subject { described_class.data } - it "gathers usage data" do + it 'gathers usage data' do expect(subject.keys).to include(*%i( active_user_count counts @@ -59,23 +61,28 @@ describe Gitlab::UsageData do avg_cycle_analytics influxdb_metrics_enabled prometheus_metrics_enabled + cycle_analytics_views )) expect(subject).to include( snippet_create: a_kind_of(Integer), snippet_update: a_kind_of(Integer), snippet_comment: a_kind_of(Integer), + merge_request_comment: a_kind_of(Integer), + commit_comment: a_kind_of(Integer), wiki_pages_create: a_kind_of(Integer), wiki_pages_update: a_kind_of(Integer), wiki_pages_delete: a_kind_of(Integer), web_ide_views: a_kind_of(Integer), web_ide_commits: a_kind_of(Integer), web_ide_merge_requests: a_kind_of(Integer), - navbar_searches: a_kind_of(Integer) + navbar_searches: a_kind_of(Integer), + cycle_analytics_views: a_kind_of(Integer), + source_code_pushes: a_kind_of(Integer) ) end - it "gathers usage counts" do + it 'gathers usage counts' do expected_keys = %i( assignee_lists boards @@ -151,11 +158,6 @@ describe Gitlab::UsageData do expect(expected_keys - count_data.keys).to be_empty end - it 'does not gather user preferences usage data when the feature is disabled' do - stub_feature_flags(group_overview_security_dashboard: false) - expect(subject[:counts].keys).not_to include(:user_preferences) - end - it 'gathers projects data correctly' do count_data = subject[:counts] @@ -250,7 +252,7 @@ describe Gitlab::UsageData do describe '#license_usage_data' do subject { described_class.license_usage_data } - it "gathers license data" do + it 'gathers license data' do expect(subject[:uuid]).to eq(Gitlab::CurrentSettings.uuid) expect(subject[:version]).to eq(Gitlab::VERSION) expect(subject[:installation_type]).to eq('gitlab-development-kit') @@ -268,6 +270,12 @@ describe Gitlab::UsageData do expect(described_class.count(relation)).to eq(1) end + it 'returns the count for count_by when provided' do + allow(relation).to receive(:count).with(:creator_id).and_return(2) + + expect(described_class.count(relation, count_by: :creator_id)).to eq(2) + end + it 'returns the fallback value when counting fails' do allow(relation).to receive(:count).and_raise(ActiveRecord::StatementInvalid.new('')) diff --git a/spec/lib/gitlab/user_access_spec.rb b/spec/lib/gitlab/user_access_spec.rb index 9da06bb40f4..c25bd14fcba 100644 --- a/spec/lib/gitlab/user_access_spec.rb +++ b/spec/lib/gitlab/user_access_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::UserAccess do diff --git a/spec/lib/gitlab/utils/deep_size_spec.rb b/spec/lib/gitlab/utils/deep_size_spec.rb index 1e619a15980..47dfc04f46f 100644 --- a/spec/lib/gitlab/utils/deep_size_spec.rb +++ b/spec/lib/gitlab/utils/deep_size_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::Utils::DeepSize do diff --git a/spec/lib/gitlab/utils/merge_hash_spec.rb b/spec/lib/gitlab/utils/merge_hash_spec.rb index 4fa7bb31301..72620e549a9 100644 --- a/spec/lib/gitlab/utils/merge_hash_spec.rb +++ b/spec/lib/gitlab/utils/merge_hash_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::Utils::MergeHash do describe '.crush' do diff --git a/spec/lib/gitlab/utils/override_spec.rb b/spec/lib/gitlab/utils/override_spec.rb index 9e7c97f8095..5855c4374a9 100644 --- a/spec/lib/gitlab/utils/override_spec.rb +++ b/spec/lib/gitlab/utils/override_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'fast_spec_helper' describe Gitlab::Utils::Override do diff --git a/spec/lib/gitlab/utils/sanitize_node_link_spec.rb b/spec/lib/gitlab/utils/sanitize_node_link_spec.rb index 064c2707d06..80b0935a7ed 100644 --- a/spec/lib/gitlab/utils/sanitize_node_link_spec.rb +++ b/spec/lib/gitlab/utils/sanitize_node_link_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::Utils::SanitizeNodeLink do diff --git a/spec/lib/gitlab/utils/strong_memoize_spec.rb b/spec/lib/gitlab/utils/strong_memoize_spec.rb index 473f8100771..26baaf873a8 100644 --- a/spec/lib/gitlab/utils/strong_memoize_spec.rb +++ b/spec/lib/gitlab/utils/strong_memoize_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::Utils::StrongMemoize do diff --git a/spec/lib/gitlab/utils_spec.rb b/spec/lib/gitlab/utils_spec.rb index 0c20b3aa4c8..890918d4a7c 100644 --- a/spec/lib/gitlab/utils_spec.rb +++ b/spec/lib/gitlab/utils_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::Utils do diff --git a/spec/lib/gitlab/verify/job_artifacts_spec.rb b/spec/lib/gitlab/verify/job_artifacts_spec.rb index 6e916a56564..b50ec1528d4 100644 --- a/spec/lib/gitlab/verify/job_artifacts_spec.rb +++ b/spec/lib/gitlab/verify/job_artifacts_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::Verify::JobArtifacts do diff --git a/spec/lib/gitlab/verify/lfs_objects_spec.rb b/spec/lib/gitlab/verify/lfs_objects_spec.rb index 2feaedd6f14..c27c9b6efa1 100644 --- a/spec/lib/gitlab/verify/lfs_objects_spec.rb +++ b/spec/lib/gitlab/verify/lfs_objects_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::Verify::LfsObjects do diff --git a/spec/lib/gitlab/verify/uploads_spec.rb b/spec/lib/gitlab/verify/uploads_spec.rb index 38c30fab1ba..a3d3f5d46f3 100644 --- a/spec/lib/gitlab/verify/uploads_spec.rb +++ b/spec/lib/gitlab/verify/uploads_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::Verify::Uploads do diff --git a/spec/lib/gitlab/version_info_spec.rb b/spec/lib/gitlab/version_info_spec.rb index 30035c79e58..8c14b187410 100644 --- a/spec/lib/gitlab/version_info_spec.rb +++ b/spec/lib/gitlab/version_info_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe 'Gitlab::VersionInfo' do diff --git a/spec/lib/gitlab/view/presenter/base_spec.rb b/spec/lib/gitlab/view/presenter/base_spec.rb index 02c2fd47197..e196ab23482 100644 --- a/spec/lib/gitlab/view/presenter/base_spec.rb +++ b/spec/lib/gitlab/view/presenter/base_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::View::Presenter::Base do diff --git a/spec/lib/gitlab/view/presenter/delegated_spec.rb b/spec/lib/gitlab/view/presenter/delegated_spec.rb index 940a2ce6ebd..0a21cd1358e 100644 --- a/spec/lib/gitlab/view/presenter/delegated_spec.rb +++ b/spec/lib/gitlab/view/presenter/delegated_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::View::Presenter::Delegated do diff --git a/spec/lib/gitlab/view/presenter/factory_spec.rb b/spec/lib/gitlab/view/presenter/factory_spec.rb index 6120bafb2e3..515a1b0a8e4 100644 --- a/spec/lib/gitlab/view/presenter/factory_spec.rb +++ b/spec/lib/gitlab/view/presenter/factory_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::View::Presenter::Factory do diff --git a/spec/lib/gitlab/view/presenter/simple_spec.rb b/spec/lib/gitlab/view/presenter/simple_spec.rb index 1795ed2405b..70e2b170a36 100644 --- a/spec/lib/gitlab/view/presenter/simple_spec.rb +++ b/spec/lib/gitlab/view/presenter/simple_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::View::Presenter::Simple do diff --git a/spec/lib/gitlab/visibility_level_checker_spec.rb b/spec/lib/gitlab/visibility_level_checker_spec.rb new file mode 100644 index 00000000000..325ac3c6f31 --- /dev/null +++ b/spec/lib/gitlab/visibility_level_checker_spec.rb @@ -0,0 +1,82 @@ +require 'spec_helper' + +describe Gitlab::VisibilityLevelChecker do + let(:user) { create(:user) } + let(:project) { create(:project) } + let(:visibility_level_checker) { } + let(:override_params) { {} } + + subject { described_class.new(user, project, project_params: override_params) } + + describe '#level_restricted?' do + context 'when visibility level is allowed' do + it 'returns false with nil for visibility level' do + result = subject.level_restricted? + + expect(result.restricted?).to eq(false) + expect(result.visibility_level).to be_nil + end + end + + context 'when visibility level is restricted' do + before do + stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC]) + end + + it 'returns true and visibility name' do + project.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC) + result = subject.level_restricted? + + expect(result.restricted?).to eq(true) + expect(result.visibility_level).to eq(Gitlab::VisibilityLevel::PUBLIC) + end + + context 'overridden visibility' do + let(:override_params) do + { + import_data: { + data: { + override_params: { + visibility: override_visibility + } + } + } + } + end + + context 'when restricted' do + let(:override_visibility) { 'public' } + + it 'returns true and visibility name' do + result = subject.level_restricted? + + expect(result.restricted?).to eq(true) + expect(result.visibility_level).to eq(Gitlab::VisibilityLevel::PUBLIC) + end + end + + context 'when misspelled' do + let(:override_visibility) { 'publik' } + + it 'returns false with nil for visibility level' do + result = subject.level_restricted? + + expect(result.restricted?).to eq(false) + expect(result.visibility_level).to be_nil + end + end + + context 'when import_data is missing' do + let(:override_params) { {} } + + it 'returns false with nil for visibility level' do + result = subject.level_restricted? + + expect(result.restricted?).to eq(false) + expect(result.visibility_level).to be_nil + end + end + end + end + end +end diff --git a/spec/lib/gitlab/visibility_level_spec.rb b/spec/lib/gitlab/visibility_level_spec.rb index 0a170a157fe..75dc7d8e6d1 100644 --- a/spec/lib/gitlab/visibility_level_spec.rb +++ b/spec/lib/gitlab/visibility_level_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::VisibilityLevel do diff --git a/spec/lib/gitlab/wiki_file_finder_spec.rb b/spec/lib/gitlab/wiki_file_finder_spec.rb index 025d1203dc5..fdd95d5e6e6 100644 --- a/spec/lib/gitlab/wiki_file_finder_spec.rb +++ b/spec/lib/gitlab/wiki_file_finder_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::WikiFileFinder do diff --git a/spec/lib/gitlab/workhorse_spec.rb b/spec/lib/gitlab/workhorse_spec.rb index 451e18ed91b..5c5ff46112f 100644 --- a/spec/lib/gitlab/workhorse_spec.rb +++ b/spec/lib/gitlab/workhorse_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::Workhorse do diff --git a/spec/lib/gitlab_spec.rb b/spec/lib/gitlab_spec.rb index c293f58c9cb..cbc5649fc09 100644 --- a/spec/lib/gitlab_spec.rb +++ b/spec/lib/gitlab_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'fast_spec_helper' require_dependency 'gitlab' diff --git a/spec/lib/google_api/auth_spec.rb b/spec/lib/google_api/auth_spec.rb index 87a3f43274f..a25004ac385 100644 --- a/spec/lib/google_api/auth_spec.rb +++ b/spec/lib/google_api/auth_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe GoogleApi::Auth do diff --git a/spec/lib/google_api/cloud_platform/client_spec.rb b/spec/lib/google_api/cloud_platform/client_spec.rb index 1fefc947636..c24998d32f8 100644 --- a/spec/lib/google_api/cloud_platform/client_spec.rb +++ b/spec/lib/google_api/cloud_platform/client_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe GoogleApi::CloudPlatform::Client do diff --git a/spec/lib/json_web_token/rsa_token_spec.rb b/spec/lib/json_web_token/rsa_token_spec.rb index a3c54651e80..a127c787e28 100644 --- a/spec/lib/json_web_token/rsa_token_spec.rb +++ b/spec/lib/json_web_token/rsa_token_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + describe JSONWebToken::RSAToken do let(:rsa_key) do OpenSSL::PKey::RSA.new <<-eos.strip_heredoc diff --git a/spec/lib/json_web_token/token_spec.rb b/spec/lib/json_web_token/token_spec.rb index d7e7560d962..916d11ce0ed 100644 --- a/spec/lib/json_web_token/token_spec.rb +++ b/spec/lib/json_web_token/token_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + describe JSONWebToken::Token do let(:token) { described_class.new } diff --git a/spec/lib/mattermost/client_spec.rb b/spec/lib/mattermost/client_spec.rb index dc11a414717..5fe35eb5f93 100644 --- a/spec/lib/mattermost/client_spec.rb +++ b/spec/lib/mattermost/client_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Mattermost::Client do diff --git a/spec/lib/mattermost/command_spec.rb b/spec/lib/mattermost/command_spec.rb index 7c194749dfb..f8c451a1522 100644 --- a/spec/lib/mattermost/command_spec.rb +++ b/spec/lib/mattermost/command_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Mattermost::Command do diff --git a/spec/lib/mattermost/session_spec.rb b/spec/lib/mattermost/session_spec.rb index 346455067a7..ea12bd76c8d 100644 --- a/spec/lib/mattermost/session_spec.rb +++ b/spec/lib/mattermost/session_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Mattermost::Session, type: :request do diff --git a/spec/lib/mattermost/team_spec.rb b/spec/lib/mattermost/team_spec.rb index 030aa5d06a8..2823dab67c9 100644 --- a/spec/lib/mattermost/team_spec.rb +++ b/spec/lib/mattermost/team_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Mattermost::Team do diff --git a/spec/lib/microsoft_teams/activity_spec.rb b/spec/lib/microsoft_teams/activity_spec.rb index 7890ae2e7b0..3fad2437f3e 100644 --- a/spec/lib/microsoft_teams/activity_spec.rb +++ b/spec/lib/microsoft_teams/activity_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe MicrosoftTeams::Activity do diff --git a/spec/lib/microsoft_teams/notifier_spec.rb b/spec/lib/microsoft_teams/notifier_spec.rb index 2aaa7c24ad8..64ab8d85807 100644 --- a/spec/lib/microsoft_teams/notifier_spec.rb +++ b/spec/lib/microsoft_teams/notifier_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe MicrosoftTeams::Notifier do diff --git a/spec/lib/milestone_array_spec.rb b/spec/lib/milestone_array_spec.rb index df91677b925..375cb87dde6 100644 --- a/spec/lib/milestone_array_spec.rb +++ b/spec/lib/milestone_array_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe MilestoneArray do diff --git a/spec/lib/object_storage/direct_upload_spec.rb b/spec/lib/object_storage/direct_upload_spec.rb index 8ccbd90ddb8..fae0c636bdc 100644 --- a/spec/lib/object_storage/direct_upload_spec.rb +++ b/spec/lib/object_storage/direct_upload_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe ObjectStorage::DirectUpload do diff --git a/spec/lib/omni_auth/strategies/jwt_spec.rb b/spec/lib/omni_auth/strategies/jwt_spec.rb index c1eaf0bb0bf..bdf3ea6be98 100644 --- a/spec/lib/omni_auth/strategies/jwt_spec.rb +++ b/spec/lib/omni_auth/strategies/jwt_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe OmniAuth::Strategies::Jwt do diff --git a/spec/lib/prometheus/cleanup_multiproc_dir_service_spec.rb b/spec/lib/prometheus/cleanup_multiproc_dir_service_spec.rb new file mode 100644 index 00000000000..c7302a1a656 --- /dev/null +++ b/spec/lib/prometheus/cleanup_multiproc_dir_service_spec.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Prometheus::CleanupMultiprocDirService do + describe '.call' do + subject { described_class.new.execute } + + let(:metrics_multiproc_dir) { Dir.mktmpdir } + let(:metrics_file_path) { File.join(metrics_multiproc_dir, 'counter_puma_master-0.db') } + + before do + FileUtils.touch(metrics_file_path) + end + + after do + FileUtils.rm_r(metrics_multiproc_dir) + end + + context 'when `multiprocess_files_dir` is defined' do + before do + expect(Prometheus::Client.configuration) + .to receive(:multiprocess_files_dir) + .and_return(metrics_multiproc_dir) + .at_least(:once) + end + + it 'removes old metrics' do + expect { subject } + .to change { File.exist?(metrics_file_path) } + .from(true) + .to(false) + end + end + + context 'when `multiprocess_files_dir` is not defined' do + before do + expect(Prometheus::Client.configuration) + .to receive(:multiprocess_files_dir) + .and_return(nil) + .at_least(:once) + end + + it 'does not remove any files' do + expect { subject } + .not_to change { File.exist?(metrics_file_path) } + .from(true) + end + end + end +end diff --git a/spec/lib/rspec_flaky/config_spec.rb b/spec/lib/rspec_flaky/config_spec.rb index 4a71b1feebd..13b2219267b 100644 --- a/spec/lib/rspec_flaky/config_spec.rb +++ b/spec/lib/rspec_flaky/config_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe RspecFlaky::Config, :aggregate_failures do diff --git a/spec/lib/rspec_flaky/example_spec.rb b/spec/lib/rspec_flaky/example_spec.rb index 5b4fd5ddf3e..4679dd818db 100644 --- a/spec/lib/rspec_flaky/example_spec.rb +++ b/spec/lib/rspec_flaky/example_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe RspecFlaky::Example do diff --git a/spec/lib/rspec_flaky/flaky_example_spec.rb b/spec/lib/rspec_flaky/flaky_example_spec.rb index d19c34bebb3..092bbc781a5 100644 --- a/spec/lib/rspec_flaky/flaky_example_spec.rb +++ b/spec/lib/rspec_flaky/flaky_example_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe RspecFlaky::FlakyExample, :aggregate_failures do diff --git a/spec/lib/rspec_flaky/flaky_examples_collection_spec.rb b/spec/lib/rspec_flaky/flaky_examples_collection_spec.rb index 6731a27ed17..2e224cda61b 100644 --- a/spec/lib/rspec_flaky/flaky_examples_collection_spec.rb +++ b/spec/lib/rspec_flaky/flaky_examples_collection_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe RspecFlaky::FlakyExamplesCollection, :aggregate_failures do diff --git a/spec/lib/rspec_flaky/listener_spec.rb b/spec/lib/rspec_flaky/listener_spec.rb index ef085445081..44b8d99b74f 100644 --- a/spec/lib/rspec_flaky/listener_spec.rb +++ b/spec/lib/rspec_flaky/listener_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe RspecFlaky::Listener, :aggregate_failures do diff --git a/spec/lib/rspec_flaky/report_spec.rb b/spec/lib/rspec_flaky/report_spec.rb index 7d57d99f7e5..6a98a7a4e6b 100644 --- a/spec/lib/rspec_flaky/report_spec.rb +++ b/spec/lib/rspec_flaky/report_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe RspecFlaky::Report, :aggregate_failures do diff --git a/spec/lib/safe_zip/entry_spec.rb b/spec/lib/safe_zip/entry_spec.rb index 115e28c5994..0974f732188 100644 --- a/spec/lib/safe_zip/entry_spec.rb +++ b/spec/lib/safe_zip/entry_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe SafeZip::Entry do diff --git a/spec/lib/safe_zip/extract_params_spec.rb b/spec/lib/safe_zip/extract_params_spec.rb index 85e22cfa495..f66d3de89ee 100644 --- a/spec/lib/safe_zip/extract_params_spec.rb +++ b/spec/lib/safe_zip/extract_params_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe SafeZip::ExtractParams do diff --git a/spec/lib/safe_zip/extract_spec.rb b/spec/lib/safe_zip/extract_spec.rb index b75a8fede00..3b8c64c1c9f 100644 --- a/spec/lib/safe_zip/extract_spec.rb +++ b/spec/lib/safe_zip/extract_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe SafeZip::Extract do diff --git a/spec/lib/serializers/json_spec.rb b/spec/lib/serializers/json_spec.rb index 847a01d186c..a8d82d70e89 100644 --- a/spec/lib/serializers/json_spec.rb +++ b/spec/lib/serializers/json_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'fast_spec_helper' describe Serializers::JSON do diff --git a/spec/lib/system_check/app/git_user_default_ssh_config_check_spec.rb b/spec/lib/system_check/app/git_user_default_ssh_config_check_spec.rb index a0fb86345f3..f132f608ab6 100644 --- a/spec/lib/system_check/app/git_user_default_ssh_config_check_spec.rb +++ b/spec/lib/system_check/app/git_user_default_ssh_config_check_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe SystemCheck::App::GitUserDefaultSSHConfigCheck do diff --git a/spec/lib/system_check/base_check_spec.rb b/spec/lib/system_check/base_check_spec.rb index faf8c99e772..ccb7b483bdc 100644 --- a/spec/lib/system_check/base_check_spec.rb +++ b/spec/lib/system_check/base_check_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe SystemCheck::BaseCheck do diff --git a/spec/lib/system_check/orphans/namespace_check_spec.rb b/spec/lib/system_check/orphans/namespace_check_spec.rb index 2a61ff3ad65..f7491e40438 100644 --- a/spec/lib/system_check/orphans/namespace_check_spec.rb +++ b/spec/lib/system_check/orphans/namespace_check_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' require 'rake_helper' diff --git a/spec/lib/system_check/orphans/repository_check_spec.rb b/spec/lib/system_check/orphans/repository_check_spec.rb index b0c2267d177..a5e06f30e75 100644 --- a/spec/lib/system_check/orphans/repository_check_spec.rb +++ b/spec/lib/system_check/orphans/repository_check_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' require 'rake_helper' diff --git a/spec/lib/system_check/simple_executor_spec.rb b/spec/lib/system_check/simple_executor_spec.rb index e71e9da369d..94094343ec6 100644 --- a/spec/lib/system_check/simple_executor_spec.rb +++ b/spec/lib/system_check/simple_executor_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' require 'rake_helper' diff --git a/spec/lib/system_check_spec.rb b/spec/lib/system_check_spec.rb index 4d9e17fa6ec..f3ed6ca31c9 100644 --- a/spec/lib/system_check_spec.rb +++ b/spec/lib/system_check_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' require 'rake_helper' diff --git a/spec/lib/uploaded_file_spec.rb b/spec/lib/uploaded_file_spec.rb index a2f5c2e7121..2cb4727bd4b 100644 --- a/spec/lib/uploaded_file_spec.rb +++ b/spec/lib/uploaded_file_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe UploadedFile do diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb index dcc4b70a382..6cba7df114c 100644 --- a/spec/mailers/notify_spec.rb +++ b/spec/mailers/notify_spec.rb @@ -543,6 +543,73 @@ describe Notify do end end + describe '#mail_thread' do + set(:mail_thread_note) { create(:note) } + + let(:headers) do + { + from: 'someone@test.com', + to: 'someone-else@test.com', + subject: 'something', + template_name: '_note_email' # re-use this for testing + } + end + + let(:mailer) do + mailer = described_class.new + mailer.instance_variable_set(:@note, mail_thread_note) + mailer + end + + context 'the model has no namespace' do + class TopLevelThing + include Referable + include Noteable + + def to_reference(*_args) + 'tlt-ref' + end + + def id + 'tlt-id' + end + end + + subject do + mailer.send(:mail_thread, TopLevelThing.new, headers) + end + + it 'has X-GitLab-Namespaced-Thing-ID header' do + expect(subject.header['X-GitLab-TopLevelThing-ID'].value).to eq('tlt-id') + end + end + + context 'the model has a namespace' do + module Namespaced + class Thing + include Referable + include Noteable + + def to_reference(*_args) + 'some-reference' + end + + def id + 'some-id' + end + end + end + + subject do + mailer.send(:mail_thread, Namespaced::Thing.new, headers) + end + + it 'has X-GitLab-Namespaced-Thing-ID header' do + expect(subject.header['X-GitLab-Namespaced-Thing-ID'].value).to eq('some-id') + end + end + end + context 'for issue notes' do let(:host) { Gitlab.config.gitlab.host } diff --git a/spec/migrations/add_gitlab_instance_administration_project_spec.rb b/spec/migrations/add_gitlab_instance_administration_project_spec.rb new file mode 100644 index 00000000000..08e20a4e8ff --- /dev/null +++ b/spec/migrations/add_gitlab_instance_administration_project_spec.rb @@ -0,0 +1,252 @@ +# frozen_string_literal: true + +require 'spec_helper' +require Rails.root.join('db', 'post_migrate', '20190801072937_add_gitlab_instance_administration_project.rb') + +describe AddGitlabInstanceAdministrationProject, :migration do + let(:application_settings) { table(:application_settings) } + let(:users) { table(:users) } + let(:projects) { table(:projects) } + let(:namespaces) { table(:namespaces) } + let(:members) { table(:members) } + + let(:service_class) do + Gitlab::DatabaseImporters::SelfMonitoring::Project::CreateService + end + + let(:prometheus_settings) do + { + enable: true, + listen_address: 'localhost:9090' + } + end + + before do + stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false') + + stub_config(prometheus: prometheus_settings) + end + + describe 'down' do + let!(:application_setting) { application_settings.create! } + let!(:user) { users.create!(admin: true, email: 'admin1@example.com', projects_limit: 10, state: :active) } + + it 'deletes group and project' do + migrate! + + expect(Project.count).to eq(1) + expect(Group.count).to eq(1) + + schema_migrate_down! + + expect(Project.count).to eq(0) + expect(Group.count).to eq(0) + end + end + + describe 'up' do + context 'without application_settings' do + it 'does not fail' do + migrate! + + expect(Project.count).to eq(0) + end + end + + context 'without admin users' do + let!(:application_setting) { application_settings.create! } + + it 'does not fail' do + migrate! + + expect(Project.count).to eq(0) + end + end + + context 'with admin users' do + let(:project) { Project.last } + let(:group) { Group.last } + let!(:application_setting) { application_settings.create! } + let!(:user) { users.create!(admin: true, email: 'admin1@example.com', projects_limit: 10, state: :active) } + + before do + stub_application_setting(allow_local_requests_from_web_hooks_and_services: true) + end + + shared_examples 'has prometheus service' do |listen_address| + it do + migrate! + + prometheus = project.prometheus_service + expect(prometheus).to be_persisted + expect(prometheus).not_to eq(nil) + expect(prometheus.api_url).to eq(listen_address) + expect(prometheus.active).to eq(true) + expect(prometheus.manual_configuration).to eq(true) + end + end + + it_behaves_like 'has prometheus service', 'http://localhost:9090' + + it 'creates GitLab Instance Administrator group' do + migrate! + + expect(group).to be_persisted + expect(group.name).to eq('GitLab Instance Administrators') + expect(group.path).to start_with('gitlab-instance-administrators') + expect(group.path.split('-').last.length).to eq(8) + expect(group.visibility_level).to eq(service_class::VISIBILITY_LEVEL) + end + + it 'creates project with internal visibility' do + migrate! + + expect(project.visibility_level).to eq(Gitlab::VisibilityLevel::INTERNAL) + expect(project).to be_persisted + end + + it 'creates project with correct name and description' do + migrate! + + path = 'administration/monitoring/gitlab_instance_administration_project/index' + docs_path = Rails.application.routes.url_helpers.help_page_path(path) + + expect(project.name).to eq(service_class::PROJECT_NAME) + expect(project.description).to eq( + 'This project is automatically generated and will be used to help monitor this GitLab instance. ' \ + "[More information](#{docs_path})" + ) + expect(File).to exist("doc/#{path}.md") + end + + it 'adds all admins as maintainers' do + admin1 = users.create!(admin: true, email: 'admin2@example.com', projects_limit: 10, state: :active) + admin2 = users.create!(admin: true, email: 'admin3@example.com', projects_limit: 10, state: :active) + users.create!(email: 'nonadmin1@example.com', projects_limit: 10, state: :active) + + migrate! + + expect(project.owner).to eq(group) + expect(group.members.collect(&:user).collect(&:id)).to contain_exactly(user.id, admin1.id, admin2.id) + expect(group.members.collect(&:access_level)).to contain_exactly( + Gitlab::Access::OWNER, + Gitlab::Access::MAINTAINER, + Gitlab::Access::MAINTAINER + ) + end + + it 'saves the project id' do + migrate! + + application_setting.reload + expect(application_setting.instance_administration_project_id).to eq(project.id) + end + + it 'does not fail when a project already exists' do + group = namespaces.create!( + path: 'gitlab-instance-administrators', + name: 'GitLab Instance Administrators', + type: 'Group' + ) + project = projects.create!( + namespace_id: group.id, + name: 'GitLab Instance Administration' + ) + + admin1 = users.create!(admin: true, email: 'admin4@example.com', projects_limit: 10, state: :active) + admin2 = users.create!(admin: true, email: 'admin5@example.com', projects_limit: 10, state: :active) + + members.create!( + user_id: admin1.id, + source_id: group.id, + source_type: 'Namespace', + type: 'GroupMember', + access_level: GroupMember::MAINTAINER, + notification_level: NotificationSetting.levels[:global] + ) + members.create!( + user_id: admin2.id, + source_id: group.id, + source_type: 'Namespace', + type: 'GroupMember', + access_level: GroupMember::MAINTAINER, + notification_level: NotificationSetting.levels[:global] + ) + + stub_application_setting(instance_administration_project: project) + + migrate! + + expect(Project.last.id).to eq(project.id) + expect(Group.last.id).to eq(group.id) + end + + context 'when local requests from hooks and services are not allowed' do + before do + stub_application_setting(allow_local_requests_from_web_hooks_and_services: false) + end + + it_behaves_like 'has prometheus service', 'http://localhost:9090' + + it 'does not overwrite the existing whitelist' do + application_setting.update!(outbound_local_requests_whitelist: ['example.com']) + + migrate! + + application_setting.reload + expect(application_setting.outbound_local_requests_whitelist).to contain_exactly( + 'example.com', 'localhost' + ) + end + end + + context 'with non default prometheus address' do + let(:prometheus_settings) do + { + enable: true, + listen_address: 'https://localhost:9090' + } + end + + it_behaves_like 'has prometheus service', 'https://localhost:9090' + end + + context 'when prometheus setting is not present in gitlab.yml' do + before do + allow(Gitlab.config).to receive(:prometheus).and_raise(Settingslogic::MissingSetting) + end + + it 'does not fail' do + migrate! + + expect(project.prometheus_service).to be_nil + end + end + + context 'when prometheus setting is disabled in gitlab.yml' do + let(:prometheus_settings) do + { + enable: false, + listen_address: 'localhost:9090' + } + end + + it 'does not configure prometheus' do + migrate! + + expect(project.prometheus_service).to be_nil + end + end + + context 'when prometheus listen address is blank in gitlab.yml' do + let(:prometheus_settings) { { enable: true, listen_address: '' } } + + it 'does not configure prometheus' do + migrate! + + expect(project.prometheus_service).to be_nil + end + end + end + end +end diff --git a/spec/models/analytics/cycle_analytics/project_stage_spec.rb b/spec/models/analytics/cycle_analytics/project_stage_spec.rb new file mode 100644 index 00000000000..83d6ff754c5 --- /dev/null +++ b/spec/models/analytics/cycle_analytics/project_stage_spec.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Analytics::CycleAnalytics::ProjectStage do + describe 'associations' do + it { is_expected.to belong_to(:project) } + end + + it 'default stages must be valid' do + project = create(:project) + + Gitlab::Analytics::CycleAnalytics::DefaultStages.all.each do |params| + stage = described_class.new(params.merge(project: project)) + expect(stage).to be_valid + end + end + + it_behaves_like "cycle analytics stage" do + let(:parent) { create(:project) } + let(:parent_name) { :project } + end +end diff --git a/spec/models/award_emoji_spec.rb b/spec/models/award_emoji_spec.rb index 8452ac69734..b15b26b1630 100644 --- a/spec/models/award_emoji_spec.rb +++ b/spec/models/award_emoji_spec.rb @@ -44,6 +44,29 @@ describe AwardEmoji do end end + describe 'scopes' do + set(:thumbsup) { create(:award_emoji, name: 'thumbsup') } + set(:thumbsdown) { create(:award_emoji, name: 'thumbsdown') } + + describe '.upvotes' do + it { expect(described_class.upvotes).to contain_exactly(thumbsup) } + end + + describe '.downvotes' do + it { expect(described_class.downvotes).to contain_exactly(thumbsdown) } + end + + describe '.named' do + it { expect(described_class.named('thumbsup')).to contain_exactly(thumbsup) } + it { expect(described_class.named(%w[thumbsup thumbsdown])).to contain_exactly(thumbsup, thumbsdown) } + end + + describe '.awarded_by' do + it { expect(described_class.awarded_by(thumbsup.user)).to contain_exactly(thumbsup) } + it { expect(described_class.awarded_by([thumbsup.user, thumbsdown.user])).to contain_exactly(thumbsup, thumbsdown) } + end + end + describe 'expiring ETag cache' do context 'on a note' do let(:note) { create(:note_on_issue) } diff --git a/spec/models/ci/bridge_spec.rb b/spec/models/ci/bridge_spec.rb index eb32198265b..a871f9b3fe6 100644 --- a/spec/models/ci/bridge_spec.rb +++ b/spec/models/ci/bridge_spec.rb @@ -23,7 +23,7 @@ describe Ci::Bridge do let(:status) { bridge.detailed_status(user) } it 'returns detailed status object' do - expect(status).to be_a Gitlab::Ci::Status::Success + expect(status).to be_a Gitlab::Ci::Status::Created end end diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index 4aac4b640f4..bc853d45085 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -149,6 +149,56 @@ describe Ci::Build do end end + describe '.with_stale_live_trace' do + subject { described_class.with_stale_live_trace } + + context 'when build has a stale live trace' do + let!(:build) { create(:ci_build, :success, :trace_live, finished_at: 1.day.ago) } + + it 'selects the build' do + is_expected.to eq([build]) + end + end + + context 'when build does not have a stale live trace' do + let!(:build) { create(:ci_build, :success, :trace_live, finished_at: 1.hour.ago) } + + it 'does not select the build' do + is_expected.to be_empty + end + end + end + + describe '.finished_before' do + subject { described_class.finished_before(date) } + + let(:date) { 1.hour.ago } + + context 'when build has finished one day ago' do + let!(:build) { create(:ci_build, :success, finished_at: 1.day.ago) } + + it 'selects the build' do + is_expected.to eq([build]) + end + end + + context 'when build has finished 30 minutes ago' do + let!(:build) { create(:ci_build, :success, finished_at: 30.minutes.ago) } + + it 'returns an empty array' do + is_expected.to be_empty + end + end + + context 'when build is still running' do + let!(:build) { create(:ci_build, :running) } + + it 'returns an empty array' do + is_expected.to be_empty + end + end + end + describe '.with_reports' do subject { described_class.with_reports(Ci::JobArtifact.test_reports) } diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 1fb83fbb088..78be4a8131a 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -1929,6 +1929,13 @@ describe Ci::Pipeline, :mailer do it { is_expected.to be_an(Array) } end + describe '.bridgeable_statuses' do + subject { described_class.bridgeable_statuses } + + it { is_expected.to be_an(Array) } + it { is_expected.not_to include('created', 'preparing', 'pending') } + end + describe '#status' do let(:build) do create(:ci_build, :created, pipeline: pipeline, name: 'test') diff --git a/spec/models/ci/runner_spec.rb b/spec/models/ci/runner_spec.rb index 78b151631c1..70ff3cf5dc4 100644 --- a/spec/models/ci/runner_spec.rb +++ b/spec/models/ci/runner_spec.rb @@ -80,6 +80,13 @@ describe Ci::Runner do end end + describe 'constraints' do + it '.UPDATE_CONTACT_COLUMN_EVERY' do + expect(described_class::UPDATE_CONTACT_COLUMN_EVERY.max) + .to be <= described_class::ONLINE_CONTACT_TIMEOUT + end + end + describe '#access_level' do context 'when creating new runner and access_level is nil' do let(:runner) do diff --git a/spec/models/clusters/applications/helm_spec.rb b/spec/models/clusters/applications/helm_spec.rb index d4f8b552088..00b5c72a3d3 100644 --- a/spec/models/clusters/applications/helm_spec.rb +++ b/spec/models/clusters/applications/helm_spec.rb @@ -23,7 +23,7 @@ describe Clusters::Applications::Helm do Clusters::Cluster::APPLICATIONS.keys.each do |application_name| next if application_name == 'helm' - it do + it "is false when #{application_name} is installed" do cluster_application = create("clusters_applications_#{application_name}".to_sym) helm = cluster_application.cluster.application_helm @@ -31,6 +31,14 @@ describe Clusters::Applications::Helm do expect(helm.allowed_to_uninstall?).to be_falsy end end + + it 'executes a single query only' do + cluster_application = create(:clusters_applications_ingress) + helm = cluster_application.cluster.application_helm + + query_count = ActiveRecord::QueryRecorder.new { helm.allowed_to_uninstall? }.count + expect(query_count).to eq(1) + end end context "without other existing applications" do diff --git a/spec/models/concerns/awardable_spec.rb b/spec/models/concerns/awardable_spec.rb index 9e7106281ee..76da42cf243 100644 --- a/spec/models/concerns/awardable_spec.rb +++ b/spec/models/concerns/awardable_spec.rb @@ -82,16 +82,6 @@ describe Awardable do end end - describe "#toggle_award_emoji" do - it "adds an emoji if it isn't awarded yet" do - expect { issue.toggle_award_emoji("thumbsup", award_emoji.user) }.to change { AwardEmoji.count }.by(1) - end - - it "toggles already awarded emoji" do - expect { issue.toggle_award_emoji("thumbsdown", award_emoji.user) }.to change { AwardEmoji.count }.by(-1) - end - end - describe 'querying award_emoji on an Awardable' do let(:issue) { create(:issue) } diff --git a/spec/models/deployment_spec.rb b/spec/models/deployment_spec.rb index d4e631f109b..51ed8e9421b 100644 --- a/spec/models/deployment_spec.rb +++ b/spec/models/deployment_spec.rb @@ -322,4 +322,30 @@ describe Deployment do end end end + + describe '#deployed_by' do + it 'returns the deployment user if there is no deployable' do + deployment_user = create(:user) + deployment = create(:deployment, deployable: nil, user: deployment_user) + + expect(deployment.deployed_by).to eq(deployment_user) + end + + it 'returns the deployment user if the deployable have no user' do + deployment_user = create(:user) + build = create(:ci_build, user: nil) + deployment = create(:deployment, deployable: build, user: deployment_user) + + expect(deployment.deployed_by).to eq(deployment_user) + end + + it 'returns the deployable user if there is one' do + build_user = create(:user) + deployment_user = create(:user) + build = create(:ci_build, user: build_user) + deployment = create(:deployment, deployable: build, user: deployment_user) + + expect(deployment.deployed_by).to eq(build_user) + end + end end diff --git a/spec/models/members/group_member_spec.rb b/spec/models/members/group_member_spec.rb index ebb0bfca369..ad7dfac87af 100644 --- a/spec/models/members/group_member_spec.rb +++ b/spec/models/members/group_member_spec.rb @@ -3,19 +3,29 @@ require 'spec_helper' describe GroupMember do - describe '.count_users_by_group_id' do - it 'counts users by group ID' do - user_1 = create(:user) - user_2 = create(:user) - group_1 = create(:group) - group_2 = create(:group) - - group_1.add_owner(user_1) - group_1.add_owner(user_2) - group_2.add_owner(user_1) - - expect(described_class.count_users_by_group_id).to eq(group_1.id => 2, - group_2.id => 1) + context 'scopes' do + describe '.count_users_by_group_id' do + it 'counts users by group ID' do + user_1 = create(:user) + user_2 = create(:user) + group_1 = create(:group) + group_2 = create(:group) + + group_1.add_owner(user_1) + group_1.add_owner(user_2) + group_2.add_owner(user_1) + + expect(described_class.count_users_by_group_id).to eq(group_1.id => 2, + group_2.id => 1) + end + end + + describe '.of_ldap_type' do + it 'returns ldap type users' do + group_member = create(:group_member, :ldap) + + expect(described_class.of_ldap_type).to eq([group_member]) + end end end diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 53424204db7..d344a6d0f0d 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -3015,9 +3015,6 @@ describe MergeRequest do subject { merge_request.rebase_in_progress? } it do - # Stub out the legacy gitaly implementation - allow(merge_request).to receive(:gitaly_rebase_in_progress?) { false } - allow(Gitlab::SidekiqStatus).to receive(:running?).with(rebase_jid) { jid_valid } merge_request.rebase_jid = rebase_jid @@ -3027,42 +3024,6 @@ describe MergeRequest do end end - describe '#gitaly_rebase_in_progress?' do - let(:repo_path) do - Gitlab::GitalyClient::StorageSettings.allow_disk_access do - subject.source_project.repository.path - end - end - let(:rebase_path) { File.join(repo_path, "gitlab-worktree", "rebase-#{subject.id}") } - - before do - system(*%W(#{Gitlab.config.git.bin_path} -C #{repo_path} worktree add --detach #{rebase_path} master)) - end - - it 'returns true when there is a current rebase directory' do - expect(subject.rebase_in_progress?).to be_truthy - end - - it 'returns false when there is no rebase directory' do - FileUtils.rm_rf(rebase_path) - - expect(subject.rebase_in_progress?).to be_falsey - end - - it 'returns false when the rebase directory has expired' do - time = 20.minutes.ago.to_time - File.utime(time, time, rebase_path) - - expect(subject.rebase_in_progress?).to be_falsey - end - - it 'returns false when the source project has been removed' do - allow(subject).to receive(:source_project).and_return(nil) - - expect(subject.rebase_in_progress?).to be_falsey - end - end - describe '#allow_collaboration' do let(:merge_request) do build(:merge_request, source_branch: 'fixes', allow_collaboration: true) diff --git a/spec/models/namespace/root_storage_statistics_spec.rb b/spec/models/namespace/root_storage_statistics_spec.rb index 5341278db7c..9e12831a704 100644 --- a/spec/models/namespace/root_storage_statistics_spec.rb +++ b/spec/models/namespace/root_storage_statistics_spec.rb @@ -8,6 +8,19 @@ RSpec.describe Namespace::RootStorageStatistics, type: :model do it { is_expected.to delegate_method(:all_projects).to(:namespace) } + context 'scopes' do + describe '.for_namespace_ids' do + it 'returns only requested namespaces' do + stats = create_list(:namespace_root_storage_statistics, 3) + namespace_ids = stats[0..1].map { |s| s.namespace_id } + + requested_stats = described_class.for_namespace_ids(namespace_ids).pluck(:namespace_id) + + expect(requested_stats).to eq(namespace_ids) + end + end + end + describe '#recalculate!' do let(:namespace) { create(:group) } let(:root_storage_statistics) { create(:namespace_root_storage_statistics, namespace: namespace) } diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb index 2b9c3c43af9..972f26ac745 100644 --- a/spec/models/namespace_spec.rb +++ b/spec/models/namespace_spec.rb @@ -853,4 +853,64 @@ describe Namespace do it { is_expected.to be_falsy } end end + + describe '#emails_disabled?' do + context 'when not a subgroup' do + it 'returns false' do + group = create(:group, emails_disabled: false) + + expect(group.emails_disabled?).to be_falsey + end + + it 'returns true' do + group = create(:group, emails_disabled: true) + + expect(group.emails_disabled?).to be_truthy + end + end + + context 'when a subgroup' do + let(:grandparent) { create(:group) } + let(:parent) { create(:group, parent: grandparent) } + let(:group) { create(:group, parent: parent) } + + it 'returns false' do + expect(group.emails_disabled?).to be_falsey + end + + context 'when ancestor emails are disabled' do + it 'returns true' do + grandparent.update_attribute(:emails_disabled, true) + + expect(group.emails_disabled?).to be_truthy + end + end + end + + context 'when :emails_disabled feature flag is off' do + before do + stub_feature_flags(emails_disabled: false) + end + + context 'when not a subgroup' do + it 'returns false' do + group = create(:group, emails_disabled: true) + + expect(group.emails_disabled?).to be_falsey + end + end + + context 'when a subgroup and ancestor emails are disabled' do + let(:grandparent) { create(:group) } + let(:parent) { create(:group, parent: grandparent) } + let(:group) { create(:group, parent: parent) } + + it 'returns false' do + grandparent.update_attribute(:emails_disabled, true) + + expect(group.emails_disabled?).to be_falsey + end + end + end + end end diff --git a/spec/models/notification_recipient_spec.rb b/spec/models/notification_recipient_spec.rb index 4122736c148..2ba53818e54 100644 --- a/spec/models/notification_recipient_spec.rb +++ b/spec/models/notification_recipient_spec.rb @@ -9,6 +9,38 @@ describe NotificationRecipient do subject(:recipient) { described_class.new(user, :watch, target: target, project: project) } + describe '#notifiable?' do + let(:recipient) { described_class.new(user, :mention, target: target, project: project) } + + context 'when emails are disabled' do + it 'returns false if group disabled' do + expect(project.namespace).to receive(:emails_disabled?).and_return(true) + expect(recipient).to receive(:emails_disabled?).and_call_original + expect(recipient.notifiable?).to eq false + end + + it 'returns false if project disabled' do + expect(project).to receive(:emails_disabled?).and_return(true) + expect(recipient).to receive(:emails_disabled?).and_call_original + expect(recipient.notifiable?).to eq false + end + end + + context 'when emails are enabled' do + it 'returns true if group enabled' do + expect(project.namespace).to receive(:emails_disabled?).and_return(false) + expect(recipient).to receive(:emails_disabled?).and_call_original + expect(recipient.notifiable?).to eq true + end + + it 'returns true if project enabled' do + expect(project).to receive(:emails_disabled?).and_return(false) + expect(recipient).to receive(:emails_disabled?).and_call_original + expect(recipient.notifiable?).to eq true + end + end + end + describe '#has_access?' do before do allow(user).to receive(:can?).and_call_original diff --git a/spec/models/project_services/chat_message/pipeline_message_spec.rb b/spec/models/project_services/chat_message/pipeline_message_spec.rb index 619ab96af94..cf7c7bf7e61 100644 --- a/spec/models/project_services/chat_message/pipeline_message_spec.rb +++ b/spec/models/project_services/chat_message/pipeline_message_spec.rb @@ -42,9 +42,9 @@ describe ChatMessage::PipelineMessage do before do test_commit = double("A test commit", committer: args[:user], title: "A test commit message") - test_project = double("A test project", - commit_by: test_commit, name: args[:project][:name], - web_url: args[:project][:web_url], avatar_url: args[:project][:avatar_url]) + test_project = double("A test project", commit_by: test_commit, name: args[:project][:name], web_url: args[:project][:web_url]) + allow(test_project).to receive(:avatar_url).with(no_args).and_return("/avatar") + allow(test_project).to receive(:avatar_url).with(only_path: false).and_return(args[:project][:avatar_url]) allow(Project).to receive(:find) { test_project } test_pipeline = double("A test pipeline", has_yaml_errors?: has_yaml_errors, diff --git a/spec/models/project_services/discord_service_spec.rb b/spec/models/project_services/discord_service_spec.rb index be82f223478..96ac532dcd1 100644 --- a/spec/models/project_services/discord_service_spec.rb +++ b/spec/models/project_services/discord_service_spec.rb @@ -8,4 +8,37 @@ describe DiscordService do let(:client_arguments) { { url: webhook_url } } let(:content_key) { :content } end + + describe '#execute' do + include StubRequests + + let(:user) { create(:user) } + let(:project) { create(:project, :repository) } + let(:webhook_url) { "https://example.gitlab.com/" } + + let(:sample_data) do + Gitlab::DataBuilder::Push.build_sample(project, user) + end + + before do + allow(subject).to receive_messages( + project: project, + project_id: project.id, + service_hook: true, + webhook: webhook_url + ) + + WebMock.stub_request(:post, webhook_url) + end + + context 'DNS rebind to local address' do + before do + stub_dns(webhook_url, ip_address: '192.168.2.120') + end + + it 'does not allow DNS rebinding' do + expect { subject.execute(sample_data) }.to raise_error(ArgumentError, /is blocked/) + end + end + end end diff --git a/spec/models/project_services/emails_on_push_service_spec.rb b/spec/models/project_services/emails_on_push_service_spec.rb index 0a58eb367e3..ffe241aa880 100644 --- a/spec/models/project_services/emails_on_push_service_spec.rb +++ b/spec/models/project_services/emails_on_push_service_spec.rb @@ -20,4 +20,24 @@ describe EmailsOnPushService do it { is_expected.not_to validate_presence_of(:recipients) } end end + + context 'project emails' do + let(:push_data) { { object_kind: 'push' } } + let(:project) { create(:project, :repository) } + let(:service) { create(:emails_on_push_service, project: project) } + + it 'does not send emails when disabled' do + expect(project).to receive(:emails_disabled?).and_return(true) + expect(EmailsOnPushWorker).not_to receive(:perform_async) + + service.execute(push_data) + end + + it 'does send emails when enabled' do + expect(project).to receive(:emails_disabled?).and_return(false) + expect(EmailsOnPushWorker).to receive(:perform_async) + + service.execute(push_data) + end + end end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 29a589eba20..ff9e94afc12 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -98,6 +98,7 @@ describe Project do it { is_expected.to have_many(:lfs_file_locks) } it { is_expected.to have_many(:project_deploy_tokens) } it { is_expected.to have_many(:deploy_tokens).through(:project_deploy_tokens) } + it { is_expected.to have_many(:cycle_analytics_stages) } it 'has an inverse relationship with merge requests' do expect(described_class.reflect_on_association(:merge_requests).has_inverse?).to eq(:target_project) @@ -2252,6 +2253,21 @@ describe Project do end end + describe '#mark_stuck_remote_mirrors_as_failed!' do + it 'fails stuck remote mirrors' do + project = create(:project, :repository, :remote_mirror) + + project.remote_mirrors.first.update( + update_status: :started, + last_update_started_at: 2.days.ago + ) + + expect do + project.mark_stuck_remote_mirrors_as_failed! + end.to change { project.remote_mirrors.stuck.count }.from(1).to(0) + end + end + describe '#ancestors_upto' do let(:parent) { create(:group) } let(:child) { create(:group, parent: parent) } @@ -2300,6 +2316,57 @@ describe Project do end end + describe '#emails_disabled?' do + let(:project) { create(:project, emails_disabled: false) } + + context 'emails disabled in group' do + it 'returns true' do + allow(project.namespace).to receive(:emails_disabled?) { true } + + expect(project.emails_disabled?).to be_truthy + end + end + + context 'emails enabled in group' do + before do + allow(project.namespace).to receive(:emails_disabled?) { false } + end + + it 'returns false' do + expect(project.emails_disabled?).to be_falsey + end + + it 'returns true' do + project.update_attribute(:emails_disabled, true) + + expect(project.emails_disabled?).to be_truthy + end + end + + context 'when :emails_disabled feature flag is off' do + before do + stub_feature_flags(emails_disabled: false) + end + + context 'emails disabled in group' do + it 'returns false' do + allow(project.namespace).to receive(:emails_disabled?) { true } + + expect(project.emails_disabled?).to be_falsey + end + end + + context 'emails enabled in group' do + it 'returns false' do + allow(project.namespace).to receive(:emails_disabled?) { false } + project.update_attribute(:emails_disabled, true) + + expect(project.emails_disabled?).to be_falsey + end + end + end + end + describe '#lfs_enabled?' do let(:project) { create(:project) } @@ -4297,6 +4364,39 @@ describe Project do end end + describe '#has_active_hooks?' do + set(:project) { create(:project) } + + it { expect(project.has_active_hooks?).to be_falsey } + + it 'returns true when a matching push hook exists' do + create(:project_hook, push_events: true, project: project) + + expect(project.has_active_hooks?(:merge_request_events)).to be_falsey + expect(project.has_active_hooks?).to be_truthy + end + + it 'returns true when a matching system hook exists' do + create(:system_hook, push_events: true) + + expect(project.has_active_hooks?(:merge_request_events)).to be_falsey + expect(project.has_active_hooks?).to be_truthy + end + end + + describe '#has_active_services?' do + set(:project) { create(:project) } + + it { expect(project.has_active_services?).to be_falsey } + + it 'returns true when a matching service exists' do + create(:custom_issue_tracker_service, push_events: true, merge_requests_events: false, project: project) + + expect(project.has_active_services?(:merge_request_hooks)).to be_falsey + expect(project.has_active_services?).to be_truthy + end + end + describe '#badges' do let(:project_group) { create(:group) } let(:project) { create(:project, path: 'avatar', namespace: project_group) } diff --git a/spec/models/remote_mirror_spec.rb b/spec/models/remote_mirror_spec.rb index 687b0935c55..7edeb56efe2 100644 --- a/spec/models/remote_mirror_spec.rb +++ b/spec/models/remote_mirror_spec.rb @@ -153,14 +153,14 @@ describe RemoteMirror, :mailer do end end - describe '#mark_as_failed' do + describe '#mark_as_failed!' do let(:remote_mirror) { create(:remote_mirror) } let(:error_message) { 'http://user:pass@test.com/root/repoC.git/' } let(:sanitized_error_message) { 'http://*****:*****@test.com/root/repoC.git/' } subject do remote_mirror.update_start - remote_mirror.mark_as_failed(error_message) + remote_mirror.mark_as_failed!(error_message) end it 'sets the update_status to failed' do @@ -204,8 +204,8 @@ describe RemoteMirror, :mailer do it 'includes mirrors that were started over an hour ago' do mirror = create_mirror(url: 'http://cantbeblank', update_status: 'started', - last_update_at: 3.hours.ago, - updated_at: 2.hours.ago) + last_update_started_at: 3.hours.ago, + last_update_at: 2.hours.ago) expect(described_class.stuck.last).to eq(mirror) end @@ -214,7 +214,7 @@ describe RemoteMirror, :mailer do mirror = create_mirror(url: 'http://cantbeblank', update_status: 'started', last_update_at: nil, - updated_at: 4.hours.ago) + last_update_started_at: 4.hours.ago) expect(described_class.stuck.last).to eq(mirror) end diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 12dff440ce2..79395fcc994 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -1223,36 +1223,66 @@ describe Repository do end describe '#branch_exists?' do - it 'uses branch_names' do - allow(repository).to receive(:branch_names).and_return(['foobar']) + let(:branch) { repository.root_ref } - expect(repository.branch_exists?('foobar')).to eq(true) - expect(repository.branch_exists?('master')).to eq(false) + subject { repository.branch_exists?(branch) } + + it 'delegates to branch_names when the cache is empty' do + repository.expire_branches_cache + + expect(repository).to receive(:branch_names).and_call_original + is_expected.to eq(true) + end + + it 'uses redis set caching when the cache is filled' do + repository.branch_names # ensure the branch name cache is filled + + expect(repository) + .to receive(:branch_names_include?) + .with(branch) + .and_call_original + + is_expected.to eq(true) end end describe '#tag_exists?' do - it 'uses tag_names' do - allow(repository).to receive(:tag_names).and_return(['foobar']) + let(:tag) { repository.tags.first.name } + + subject { repository.tag_exists?(tag) } + + it 'delegates to tag_names when the cache is empty' do + repository.expire_tags_cache + + expect(repository).to receive(:tag_names).and_call_original + is_expected.to eq(true) + end + + it 'uses redis set caching when the cache is filled' do + repository.tag_names # ensure the tag name cache is filled + + expect(repository) + .to receive(:tag_names_include?) + .with(tag) + .and_call_original - expect(repository.tag_exists?('foobar')).to eq(true) - expect(repository.tag_exists?('master')).to eq(false) + is_expected.to eq(true) end end - describe '#branch_names', :use_clean_rails_memory_store_caching do + describe '#branch_names', :clean_gitlab_redis_cache do let(:fake_branch_names) { ['foobar'] } it 'gets cached across Repository instances' do allow(repository.raw_repository).to receive(:branch_names).once.and_return(fake_branch_names) - expect(repository.branch_names).to eq(fake_branch_names) + expect(repository.branch_names).to match_array(fake_branch_names) fresh_repository = Project.find(project.id).repository expect(fresh_repository.object_id).not_to eq(repository.object_id) expect(fresh_repository.raw_repository).not_to receive(:branch_names) - expect(fresh_repository.branch_names).to eq(fake_branch_names) + expect(fresh_repository.branch_names).to match_array(fake_branch_names) end end @@ -1744,12 +1774,23 @@ describe Repository do end end - describe '#before_push_tag' do + describe '#expires_caches_for_tags' do it 'flushes the cache' do expect(repository).to receive(:expire_statistics_caches) expect(repository).to receive(:expire_emptiness_caches) expect(repository).to receive(:expire_tags_cache) + repository.expire_caches_for_tags + end + end + + describe '#before_push_tag' do + it 'logs an event' do + expect(repository).not_to receive(:expire_statistics_caches) + expect(repository).not_to receive(:expire_emptiness_caches) + expect(repository).not_to receive(:expire_tags_cache) + expect(repository).to receive(:repository_event).with(:push_tag) + repository.before_push_tag end end @@ -1781,6 +1822,12 @@ describe Repository do repository.after_create_branch end + + it 'does not expire the branch caches when specified' do + expect(repository).not_to receive(:expire_branches_cache) + + repository.after_create_branch(expire_cache: false) + end end describe '#after_remove_branch' do @@ -1789,25 +1836,45 @@ describe Repository do repository.after_remove_branch end + + it 'does not expire the branch caches when specified' do + expect(repository).not_to receive(:expire_branches_cache) + + repository.after_remove_branch(expire_cache: false) + end end describe '#after_create' do + it 'calls expire_status_cache' do + expect(repository).to receive(:expire_status_cache) + + repository.after_create + end + + it 'logs an event' do + expect(repository).to receive(:repository_event).with(:create_repository) + + repository.after_create + end + end + + describe '#expire_status_cache' do it 'flushes the exists cache' do expect(repository).to receive(:expire_exists_cache) - repository.after_create + repository.expire_status_cache end it 'flushes the root ref cache' do expect(repository).to receive(:expire_root_ref_cache) - repository.after_create + repository.expire_status_cache end it 'flushes the emptiness caches' do expect(repository).to receive(:expire_emptiness_caches) - repository.after_create + repository.expire_status_cache end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 46b86e8393d..1a641c868d9 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -3045,6 +3045,47 @@ describe User do end end + describe '#will_save_change_to_login?' do + let(:user) { create(:user, username: 'old-username', email: 'old-email@example.org') } + let(:new_username) { 'new-name' } + let(:new_email) { 'new-email@example.org' } + + subject { user.will_save_change_to_login? } + + context 'when the username is changed' do + before do + user.username = new_username + end + + it { is_expected.to be true } + end + + context 'when the email is changed' do + before do + user.email = new_email + end + + it { is_expected.to be true } + end + + context 'when both email and username are changed' do + before do + user.username = new_username + user.email = new_email + end + + it { is_expected.to be true } + end + + context 'when email and username aren\'t changed' do + before do + user.name = 'new_name' + end + + it { is_expected.to be_falsy } + end + end + describe '#sync_attribute?' do let(:user) { described_class.new } diff --git a/spec/policies/clusters/instance_policy_spec.rb b/spec/policies/clusters/instance_policy_spec.rb index 7b61819e079..2373fef8aa6 100644 --- a/spec/policies/clusters/instance_policy_spec.rb +++ b/spec/policies/clusters/instance_policy_spec.rb @@ -9,6 +9,8 @@ describe Clusters::InstancePolicy do describe 'rules' do context 'when user' do it { expect(policy).to be_disallowed :read_cluster } + it { expect(policy).to be_disallowed :add_cluster } + it { expect(policy).to be_disallowed :create_cluster } it { expect(policy).to be_disallowed :update_cluster } it { expect(policy).to be_disallowed :admin_cluster } end @@ -17,6 +19,8 @@ describe Clusters::InstancePolicy do let(:user) { create(:admin) } it { expect(policy).to be_allowed :read_cluster } + it { expect(policy).to be_allowed :add_cluster } + it { expect(policy).to be_allowed :create_cluster } it { expect(policy).to be_allowed :update_cluster } it { expect(policy).to be_allowed :admin_cluster } end diff --git a/spec/policies/global_policy_spec.rb b/spec/policies/global_policy_spec.rb index 12be3927e18..df6cc526eb0 100644 --- a/spec/policies/global_policy_spec.rb +++ b/spec/policies/global_policy_spec.rb @@ -226,4 +226,32 @@ describe GlobalPolicy do it { is_expected.not_to be_allowed(:read_instance_statistics) } end end + + describe 'slash commands' do + context 'regular user' do + it { is_expected.to be_allowed(:use_slash_commands) } + end + + context 'when internal' do + let(:current_user) { User.ghost } + + it { is_expected.not_to be_allowed(:use_slash_commands) } + end + + context 'when blocked' do + before do + current_user.block + end + + it { is_expected.not_to be_allowed(:use_slash_commands) } + end + + context 'when access locked' do + before do + current_user.lock_access! + end + + it { is_expected.not_to be_allowed(:use_slash_commands) } + end + end end diff --git a/spec/policies/namespace/root_storage_statistics_policy_spec.rb b/spec/policies/namespace/root_storage_statistics_policy_spec.rb new file mode 100644 index 00000000000..8d53050fffb --- /dev/null +++ b/spec/policies/namespace/root_storage_statistics_policy_spec.rb @@ -0,0 +1,80 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Namespace::RootStorageStatisticsPolicy do + using RSpec::Parameterized::TableSyntax + + describe '#rules' do + let(:statistics) { create(:namespace_root_storage_statistics, namespace: namespace) } + let(:user) { create(:user) } + + subject { Ability.allowed?(user, :read_statistics, statistics) } + + shared_examples 'deny anonymous users' do + context 'when the users is anonymous' do + let(:user) { nil } + + it { is_expected.to be_falsey } + end + end + + context 'when the namespace is a personal namespace' do + let(:owner) { create(:user) } + let(:namespace) { owner.namespace } + + include_examples 'deny anonymous users' + + context 'when the user is not the owner' do + it { is_expected.to be_falsey } + end + + context 'when the user is the owner' do + let(:user) { owner } + + it { is_expected.to be_truthy } + end + end + + context 'when the namespace is a group' do + let(:user) { create(:user) } + let(:external) { create(:user, :external) } + + shared_examples 'allows only owners' do |group_type| + let(:group) { create(:group, visibility_level: Gitlab::VisibilityLevel.level_value(group_type.to_s)) } + let(:namespace) { group } + + include_examples 'deny anonymous users' + + where(:user_type, :outcome) do + [ + [:non_member, false], + [:guest, false], + [:reporter, false], + [:developer, false], + [:maintainer, false], + [:owner, true] + ] + end + + with_them do + before do + group.add_user(user, user_type) unless user_type == :non_member + end + + it { is_expected.to eq(outcome) } + + context 'when the user is external' do + let(:user) { external } + + it { is_expected.to eq(outcome) } + end + end + end + + include_examples 'allows only owners', :public + include_examples 'allows only owners', :private + include_examples 'allows only owners', :internal + end + end +end diff --git a/spec/policies/namespace_policy_spec.rb b/spec/policies/namespace_policy_spec.rb index 99fa8b1fe44..216aaae70ee 100644 --- a/spec/policies/namespace_policy_spec.rb +++ b/spec/policies/namespace_policy_spec.rb @@ -6,7 +6,7 @@ describe NamespacePolicy do let(:admin) { create(:admin) } let(:namespace) { create(:namespace, owner: owner) } - let(:owner_permissions) { [:create_projects, :admin_namespace, :read_namespace] } + let(:owner_permissions) { [:create_projects, :admin_namespace, :read_namespace, :read_statistics] } subject { described_class.new(current_user, namespace) } diff --git a/spec/presenters/blobs/unfold_presenter_spec.rb b/spec/presenters/blobs/unfold_presenter_spec.rb index ab3f8080257..83004809536 100644 --- a/spec/presenters/blobs/unfold_presenter_spec.rb +++ b/spec/presenters/blobs/unfold_presenter_spec.rb @@ -10,16 +10,31 @@ describe Blobs::UnfoldPresenter do let(:subject) { described_class.new(blob, params) } describe '#initialize' do + let(:result) { subject } + + context 'with empty params' do + let(:params) { {} } + + it 'sets default attributes' do + expect(result.full?).to eq(false) + expect(result.since).to eq(1) + expect(result.to).to eq(1) + expect(result.bottom).to eq(false) + expect(result.unfold).to eq(true) + expect(result.offset).to eq(0) + expect(result.indent).to eq(0) + end + end + context 'when full is false' do let(:params) { { full: false, since: 2, to: 3, bottom: false, offset: 1, indent: 1 } } it 'sets attributes' do - result = subject - expect(result.full?).to eq(false) expect(result.since).to eq(2) expect(result.to).to eq(3) expect(result.bottom).to eq(false) + expect(result.unfold).to eq(true) expect(result.offset).to eq(1) expect(result.indent).to eq(1) end @@ -29,12 +44,11 @@ describe Blobs::UnfoldPresenter do let(:params) { { full: true, since: 2, to: 3, bottom: false, offset: 1, indent: 1 } } it 'sets other attributes' do - result = subject - expect(result.full?).to eq(true) expect(result.since).to eq(1) expect(result.to).to eq(blob.lines.size) expect(result.bottom).to eq(false) + expect(result.unfold).to eq(false) expect(result.offset).to eq(0) expect(result.indent).to eq(0) end @@ -44,12 +58,11 @@ describe Blobs::UnfoldPresenter do let(:params) { { full: false, since: 2, to: -1, bottom: true, offset: 1, indent: 1 } } it 'sets other attributes' do - result = subject - expect(result.full?).to eq(false) expect(result.since).to eq(2) expect(result.to).to eq(blob.lines.size) expect(result.bottom).to eq(false) + expect(result.unfold).to eq(false) expect(result.offset).to eq(0) expect(result.indent).to eq(0) end diff --git a/spec/requests/api/award_emoji_spec.rb b/spec/requests/api/award_emoji_spec.rb index 6c67d84b59b..342fcfa1041 100644 --- a/spec/requests/api/award_emoji_spec.rb +++ b/spec/requests/api/award_emoji_spec.rb @@ -155,6 +155,14 @@ describe API::AwardEmoji do expect(json_response['user']['username']).to eq(user.username) end + it 'marks Todos on the Issue as done' do + todo = create(:todo, target: issue, project: project, user: user) + + post api("/projects/#{project.id}/issues/#{issue.iid}/award_emoji", user), params: { name: '8ball' } + + expect(todo.reload).to be_done + end + it "returns a 400 bad request error if the name is not given" do post api("/projects/#{project.id}/issues/#{issue.iid}/award_emoji", user) @@ -209,6 +217,14 @@ describe API::AwardEmoji do expect(json_response['user']['username']).to eq(user.username) end + it 'marks Todos on the Noteable as done' do + todo = create(:todo, target: note2.noteable, project: project, user: user) + + post api("/projects/#{project.id}/issues/#{issue.iid}/notes/#{note.id}/award_emoji", user), params: { name: 'rocket' } + + expect(todo.reload).to be_done + end + it "normalizes +1 as thumbsup award" do post api("/projects/#{project.id}/issues/#{issue.iid}/notes/#{note.id}/award_emoji", user), params: { name: '+1' } diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb index e8e17228523..5e6ff40e8cf 100644 --- a/spec/requests/api/commits_spec.rb +++ b/spec/requests/api/commits_spec.rb @@ -126,6 +126,12 @@ describe API::Commits do end end + context "with empty ref_name parameter" do + let(:route) { "/projects/#{project_id}/repository/commits?ref_name=" } + + it_behaves_like 'project commits' + end + context "path optional parameter" do it "returns project commits matching provided path parameter" do path = 'files/ruby/popen.rb' diff --git a/spec/requests/api/discussions_spec.rb b/spec/requests/api/discussions_spec.rb index ca1ffe3c524..0420201efe3 100644 --- a/spec/requests/api/discussions_spec.rb +++ b/spec/requests/api/discussions_spec.rb @@ -9,6 +9,13 @@ describe API::Discussions do project.add_developer(user) end + context 'when discussions have cross-reference system notes' do + let(:url) { "/projects/#{project.id}/merge_requests/#{merge_request.iid}/discussions" } + let(:notes_in_response) { json_response.first['notes'] } + + it_behaves_like 'with cross-reference system notes' + end + context 'when noteable is an Issue' do let!(:issue) { create(:issue, project: project, author: user) } let!(:issue_note) { create(:discussion_note_on_issue, noteable: issue, project: project, author: user) } diff --git a/spec/requests/api/graphql/mutations/award_emojis/add_spec.rb b/spec/requests/api/graphql/mutations/award_emojis/add_spec.rb index 3982125a38a..5b910d5bfe0 100644 --- a/spec/requests/api/graphql/mutations/award_emojis/add_spec.rb +++ b/spec/requests/api/graphql/mutations/award_emojis/add_spec.rb @@ -5,9 +5,9 @@ require 'spec_helper' describe 'Adding an AwardEmoji' do include GraphqlHelpers - let(:current_user) { create(:user) } - let(:awardable) { create(:note) } - let(:project) { awardable.project } + set(:current_user) { create(:user) } + set(:project) { create(:project) } + set(:awardable) { create(:note, project: project) } let(:emoji_name) { 'thumbsup' } let(:mutation) do variables = { @@ -43,7 +43,7 @@ describe 'Adding an AwardEmoji' do end context 'when the given awardable is not an Awardable' do - let(:awardable) { create(:label) } + let(:awardable) { create(:label, project: project) } it_behaves_like 'a mutation that does not create an AwardEmoji' @@ -52,7 +52,7 @@ describe 'Adding an AwardEmoji' do end context 'when the given awardable is an Awardable but still cannot be awarded an emoji' do - let(:awardable) { create(:system_note) } + let(:awardable) { create(:system_note, project: project) } it_behaves_like 'a mutation that does not create an AwardEmoji' @@ -73,6 +73,13 @@ describe 'Adding an AwardEmoji' do expect(mutation_response['awardEmoji']['name']).to eq(emoji_name) end + describe 'marking Todos as done' do + let(:user) { current_user} + subject { post_graphql_mutation(mutation, current_user: user) } + + include_examples 'creating award emojis marks Todos as done' + end + context 'when there were active record validation errors' do before do expect_next_instance_of(AwardEmoji) do |award| diff --git a/spec/requests/api/graphql/mutations/award_emojis/toggle_spec.rb b/spec/requests/api/graphql/mutations/award_emojis/toggle_spec.rb index 31145730f10..ae628d3e56c 100644 --- a/spec/requests/api/graphql/mutations/award_emojis/toggle_spec.rb +++ b/spec/requests/api/graphql/mutations/award_emojis/toggle_spec.rb @@ -5,9 +5,9 @@ require 'spec_helper' describe 'Toggling an AwardEmoji' do include GraphqlHelpers - let(:current_user) { create(:user) } - let(:awardable) { create(:note) } - let(:project) { awardable.project } + set(:current_user) { create(:user) } + set(:project) { create(:project) } + set(:awardable) { create(:note, project: project) } let(:emoji_name) { 'thumbsup' } let(:mutation) do variables = { @@ -40,7 +40,7 @@ describe 'Toggling an AwardEmoji' do end context 'when the given awardable is not an Awardable' do - let(:awardable) { create(:label) } + let(:awardable) { create(:label, project: project) } it_behaves_like 'a mutation that does not create or destroy an AwardEmoji' @@ -49,7 +49,7 @@ describe 'Toggling an AwardEmoji' do end context 'when the given awardable is an Awardable but still cannot be awarded an emoji' do - let(:awardable) { create(:system_note) } + let(:awardable) { create(:system_note, project: project) } it_behaves_like 'a mutation that does not create or destroy an AwardEmoji' @@ -81,6 +81,13 @@ describe 'Toggling an AwardEmoji' do expect(mutation_response['toggledOn']).to eq(true) end + describe 'marking Todos as done' do + let(:user) { current_user} + subject { post_graphql_mutation(mutation, current_user: user) } + + include_examples 'creating award emojis marks Todos as done' + end + context 'when there were active record validation errors' do before do expect_next_instance_of(AwardEmoji) do |award| diff --git a/spec/requests/api/graphql/namespace/root_storage_statistics_spec.rb b/spec/requests/api/graphql/namespace/root_storage_statistics_spec.rb new file mode 100644 index 00000000000..ac76d991bd4 --- /dev/null +++ b/spec/requests/api/graphql/namespace/root_storage_statistics_spec.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'rendering namespace statistics' do + include GraphqlHelpers + + let(:namespace) { user.namespace } + let!(:statistics) { create(:namespace_root_storage_statistics, namespace: namespace, packages_size: 5.megabytes) } + let(:user) { create(:user) } + + let(:query) do + graphql_query_for('namespace', + { 'fullPath' => namespace.full_path }, + "rootStorageStatistics { #{all_graphql_fields_for('RootStorageStatistics')} }") + end + + shared_examples 'a working namespace with storage statistics query' do + it_behaves_like 'a working graphql query' do + before do + post_graphql(query, current_user: user) + end + end + + it 'includes the packages size if the user can read the statistics' do + post_graphql(query, current_user: user) + + expect(graphql_data['namespace']['rootStorageStatistics']).not_to be_blank + expect(graphql_data['namespace']['rootStorageStatistics']['packagesSize']).to eq(5.megabytes) + end + end + + it_behaves_like 'a working namespace with storage statistics query' + + context 'when the namespace is a group' do + let(:group) { create(:group) } + let(:namespace) { group } + + before do + group.add_owner(user) + end + + it_behaves_like 'a working namespace with storage statistics query' + + context 'when the namespace is public' do + let(:group) { create(:group, :public)} + + it 'hides statistics for unauthenticated requests' do + post_graphql(query, current_user: nil) + + expect(graphql_data['namespace']).to be_blank + end + end + end +end diff --git a/spec/requests/api/graphql/project/project_statistics_spec.rb b/spec/requests/api/graphql/project/project_statistics_spec.rb index 14a3f37b779..ddee8537454 100644 --- a/spec/requests/api/graphql/project/project_statistics_spec.rb +++ b/spec/requests/api/graphql/project/project_statistics_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe 'rendering namespace statistics' do +describe 'rendering project statistics' do include GraphqlHelpers let(:project) { create(:project) } diff --git a/spec/requests/api/issues/get_group_issues_spec.rb b/spec/requests/api/issues/get_group_issues_spec.rb index 5916bb11516..c487471e4a1 100644 --- a/spec/requests/api/issues/get_group_issues_spec.rb +++ b/spec/requests/api/issues/get_group_issues_spec.rb @@ -342,6 +342,14 @@ describe API::Issues do group_project.add_reporter(user) end + it 'exposes known attributes' do + get api(base_url, admin) + + expect(response).to have_gitlab_http_status(200) + expect(json_response.last.keys).to include(*%w(id iid project_id title description)) + expect(json_response.last).not_to have_key('subscribed') + end + it 'returns all group issues (including opened and closed)' do get api(base_url, admin) diff --git a/spec/requests/api/issues/get_project_issues_spec.rb b/spec/requests/api/issues/get_project_issues_spec.rb index f7ca6fd1e0a..521d6b88734 100644 --- a/spec/requests/api/issues/get_project_issues_spec.rb +++ b/spec/requests/api/issues/get_project_issues_spec.rb @@ -575,6 +575,7 @@ describe API::Issues do expect(json_response['assignee']).to be_a Hash expect(json_response['author']).to be_a Hash expect(json_response['confidential']).to be_falsy + expect(json_response['subscribed']).to be_truthy end it 'exposes the closed_at attribute' do diff --git a/spec/requests/api/issues/issues_spec.rb b/spec/requests/api/issues/issues_spec.rb index d195f54be11..f19c2dcc6fe 100644 --- a/spec/requests/api/issues/issues_spec.rb +++ b/spec/requests/api/issues/issues_spec.rb @@ -216,6 +216,10 @@ describe API::Issues do expect_paginated_array_response([issue.id, closed_issue.id]) expect(json_response.first['title']).to eq(issue.title) expect(json_response.last).to have_key('web_url') + # Calculating the value of subscribed field triggers Markdown + # processing. We can't do that for multiple issues / merge + # requests in a single API request. + expect(json_response.last).not_to have_key('subscribed') end it 'returns an array of closed issues' do @@ -603,6 +607,22 @@ describe API::Issues do expect_paginated_array_response([closed_issue.id, issue.id]) end + context 'with issues list sort options' do + it 'accepts only predefined order by params' do + API::Helpers::IssuesHelpers.sort_options.each do |sort_opt| + get api('/issues', user), params: { order_by: sort_opt, sort: 'asc' } + expect(response).to have_gitlab_http_status(200) + end + end + + it 'fails to sort with non predefined options' do + %w(milestone title abracadabra).each do |sort_opt| + get api('/issues', user), params: { order_by: sort_opt, sort: 'asc' } + expect(response).to have_gitlab_http_status(400) + end + end + end + it 'matches V4 response schema' do get api('/issues', user) diff --git a/spec/requests/api/labels_spec.rb b/spec/requests/api/labels_spec.rb index ad0974f55a3..f6640fe41d0 100644 --- a/spec/requests/api/labels_spec.rb +++ b/spec/requests/api/labels_spec.rb @@ -6,6 +6,180 @@ describe API::Labels do let!(:label1) { create(:label, title: 'label1', project: project) } let!(:priority_label) { create(:label, title: 'bug', project: project, priority: 3) } + shared_examples 'label update API' do + it 'returns 200 if name is changed' do + request_params = { + new_name: 'New Label' + }.merge(spec_params) + + put api("/projects/#{project.id}/labels", user), + params: request_params + + expect(response).to have_gitlab_http_status(200) + expect(json_response['name']).to eq('New Label') + expect(json_response['color']).to eq(label1.color) + end + + it 'returns 200 if colors is changed' do + request_params = { + color: '#FFFFFF' + }.merge(spec_params) + + put api("/projects/#{project.id}/labels", user), + params: request_params + + expect(response).to have_gitlab_http_status(200) + expect(json_response['name']).to eq(label1.name) + expect(json_response['color']).to eq('#FFFFFF') + end + + it 'returns 200 if a priority is added' do + request_params = { + priority: 3 + }.merge(spec_params) + + put api("/projects/#{project.id}/labels", user), + params: request_params + + expect(response.status).to eq(200) + expect(json_response['name']).to eq(label1.name) + expect(json_response['priority']).to eq(3) + end + + it 'returns 400 if no new parameters given' do + put api("/projects/#{project.id}/labels", user), params: spec_params + + expect(response).to have_gitlab_http_status(400) + expect(json_response['error']).to eq('new_name, color, description, priority are missing, '\ + 'at least one parameter must be provided') + end + + it 'returns 400 when color code is too short' do + request_params = { + color: '#FF' + }.merge(spec_params) + + put api("/projects/#{project.id}/labels", user), + params: request_params + + expect(response).to have_gitlab_http_status(400) + expect(json_response['message']['color']).to eq(['must be a valid color code']) + end + + it 'returns 400 for too long color code' do + request_params = { + color: '#FFAAFFFF' + }.merge(spec_params) + + put api("/projects/#{project.id}/labels", user), + params: request_params + + expect(response).to have_gitlab_http_status(400) + expect(json_response['message']['color']).to eq(['must be a valid color code']) + end + + it 'returns 400 for invalid priority' do + request_params = { + priority: 'foo' + }.merge(spec_params) + + put api("/projects/#{project.id}/labels", user), + params: request_params + + expect(response).to have_gitlab_http_status(400) + end + + it 'returns 200 if name and colors and description are changed' do + request_params = { + new_name: 'New Label', + color: '#FFFFFF', + description: 'test' + }.merge(spec_params) + + put api("/projects/#{project.id}/labels", user), + params: request_params + + expect(response).to have_gitlab_http_status(200) + expect(json_response['name']).to eq('New Label') + expect(json_response['color']).to eq('#FFFFFF') + expect(json_response['description']).to eq('test') + end + + it 'returns 400 for invalid name' do + request_params = { + new_name: ',', + color: '#FFFFFF' + }.merge(spec_params) + + put api("/projects/#{project.id}/labels", user), + params: request_params + + expect(response).to have_gitlab_http_status(400) + expect(json_response['message']['title']).to eq(['is invalid']) + end + + it 'returns 200 if description is changed' do + request_params = { + description: 'test' + }.merge(spec_params) + + put api("/projects/#{project.id}/labels", user), + params: request_params + + expect(response).to have_gitlab_http_status(200) + expect(json_response['id']).to eq(expected_response_label_id) + expect(json_response['description']).to eq('test') + end + + it 'returns 200 if priority is changed' do + request_params = { + priority: 10 + }.merge(spec_params) + + put api("/projects/#{project.id}/labels", user), + params: request_params + + expect(response.status).to eq(200) + expect(json_response['id']).to eq(expected_response_label_id) + expect(json_response['priority']).to eq(10) + end + + it 'returns 200 if a priority is removed' do + label = find_by_spec_params(spec_params) + expect(label).not_to be_nil + + label.priorities.create(project: label.project, priority: 1) + label.save! + + request_params = { + priority: nil + }.merge(spec_params) + + put api("/projects/#{project.id}/labels", user), + params: request_params + + expect(response.status).to eq(200) + expect(json_response['id']).to eq(expected_response_label_id) + expect(json_response['priority']).to be_nil + end + + def find_by_spec_params(params) + if params.key?(:label_id) + Label.find(params[:label_id]) + else + Label.find_by(name: params[:name]) + end + end + end + + shared_examples 'label delete API' do + it 'returns 204 for existing label' do + delete api("/projects/#{project.id}/labels", user), params: spec_params + + expect(response).to have_gitlab_http_status(204) + end + end + before do project.add_maintainer(user) end @@ -208,174 +382,92 @@ describe API::Labels do end describe 'DELETE /projects/:id/labels' do - it 'returns 204 for existing label' do - delete api("/projects/#{project.id}/labels", user), params: { name: 'label1' } + it_behaves_like 'label delete API' do + let(:spec_params) { { name: 'label1' } } + end - expect(response).to have_gitlab_http_status(204) + it_behaves_like 'label delete API' do + let(:spec_params) { { label_id: label1.id } } end it 'returns 404 for non existing label' do delete api("/projects/#{project.id}/labels", user), params: { name: 'label2' } + expect(response).to have_gitlab_http_status(404) expect(json_response['message']).to eq('404 Label Not Found') end it 'returns 400 for wrong parameters' do delete api("/projects/#{project.id}/labels", user) - expect(response).to have_gitlab_http_status(400) - end - - it_behaves_like '412 response' do - let(:request) { api("/projects/#{project.id}/labels", user) } - let(:params) { { name: 'label1' } } - end - end - describe 'PUT /projects/:id/labels' do - it 'returns 200 if name and colors and description are changed' do - put api("/projects/#{project.id}/labels", user), - params: { - name: 'label1', - new_name: 'New Label', - color: '#FFFFFF', - description: 'test' - } - expect(response).to have_gitlab_http_status(200) - expect(json_response['name']).to eq('New Label') - expect(json_response['color']).to eq('#FFFFFF') - expect(json_response['description']).to eq('test') + expect(response).to have_gitlab_http_status(400) end - it 'returns 200 if name is changed' do - put api("/projects/#{project.id}/labels", user), + it 'fails if label_id and name are given in params' do + delete api("/projects/#{project.id}/labels", user), params: { - name: 'label1', - new_name: 'New Label' + label_id: label1.id, + name: priority_label.name } - expect(response).to have_gitlab_http_status(200) - expect(json_response['name']).to eq('New Label') - expect(json_response['color']).to eq(label1.color) - end - it 'returns 200 if colors is changed' do - put api("/projects/#{project.id}/labels", user), - params: { - name: 'label1', - color: '#FFFFFF' - } - expect(response).to have_gitlab_http_status(200) - expect(json_response['name']).to eq(label1.name) - expect(json_response['color']).to eq('#FFFFFF') + expect(response).to have_gitlab_http_status(400) end - it 'returns 200 if description is changed' do - put api("/projects/#{project.id}/labels", user), - params: { - name: 'bug', - description: 'test' - } - - expect(response).to have_gitlab_http_status(200) - expect(json_response['name']).to eq(priority_label.name) - expect(json_response['description']).to eq('test') - expect(json_response['priority']).to eq(3) + it_behaves_like '412 response' do + let(:request) { api("/projects/#{project.id}/labels", user) } + let(:params) { { name: 'label1' } } end + end - it 'returns 200 if priority is changed' do - put api("/projects/#{project.id}/labels", user), - params: { - name: 'bug', - priority: 10 - } - - expect(response.status).to eq(200) - expect(json_response['name']).to eq(priority_label.name) - expect(json_response['priority']).to eq(10) + describe 'PUT /projects/:id/labels' do + context 'when using name' do + it_behaves_like 'label update API' do + let(:spec_params) { { name: 'label1' } } + let(:expected_response_label_id) { label1.id } + end end - it 'returns 200 if a priority is added' do - put api("/projects/#{project.id}/labels", user), - params: { - name: 'label1', - priority: 3 - } - - expect(response.status).to eq(200) - expect(json_response['name']).to eq(label1.name) - expect(json_response['priority']).to eq(3) + context 'when using label_id' do + it_behaves_like 'label update API' do + let(:spec_params) { { label_id: label1.id } } + let(:expected_response_label_id) { label1.id } + end end - it 'returns 200 if the priority is removed' do + it 'returns 404 if label does not exist' do put api("/projects/#{project.id}/labels", user), params: { - name: priority_label.name, - priority: nil + name: 'label2', + new_name: 'label3' } - expect(response.status).to eq(200) - expect(json_response['name']).to eq(priority_label.name) - expect(json_response['priority']).to be_nil + expect(response).to have_gitlab_http_status(404) end - it 'returns 404 if label does not exist' do + it 'returns 404 if label by id does not exist' do put api("/projects/#{project.id}/labels", user), params: { - name: 'label2', + label_id: 0, new_name: 'label3' } + expect(response).to have_gitlab_http_status(404) end - it 'returns 400 if no label name given' do + it 'returns 400 if no label name and id is given' do put api("/projects/#{project.id}/labels", user), params: { new_name: 'label2' } - expect(response).to have_gitlab_http_status(400) - expect(json_response['error']).to eq('name is missing') - end - - it 'returns 400 if no new parameters given' do - put api("/projects/#{project.id}/labels", user), params: { name: 'label1' } - expect(response).to have_gitlab_http_status(400) - expect(json_response['error']).to eq('new_name, color, description, priority are missing, '\ - 'at least one parameter must be provided') - end - it 'returns 400 for invalid name' do - put api("/projects/#{project.id}/labels", user), - params: { - name: 'label1', - new_name: ',', - color: '#FFFFFF' - } expect(response).to have_gitlab_http_status(400) - expect(json_response['message']['title']).to eq(['is invalid']) + expect(json_response['error']).to eq('label_id, name are missing, exactly one parameter must be provided') end - it 'returns 400 when color code is too short' do + it 'fails if label_id and name are given in params' do put api("/projects/#{project.id}/labels", user), params: { - name: 'label1', - color: '#FF' + label_id: label1.id, + name: priority_label.name, + new_name: 'New Label' } - expect(response).to have_gitlab_http_status(400) - expect(json_response['message']['color']).to eq(['must be a valid color code']) - end - - it 'returns 400 for too long color code' do - put api("/projects/#{project.id}/labels", user), - params: { - name: 'label1', - color: '#FFAAFFFF' - } - expect(response).to have_gitlab_http_status(400) - expect(json_response['message']['color']).to eq(['must be a valid color code']) - end - - it 'returns 400 for invalid priority' do - put api("/projects/#{project.id}/labels", user), - params: { - name: 'label1', - priority: 'foo' - } expect(response).to have_gitlab_http_status(400) end diff --git a/spec/requests/api/notes_spec.rb b/spec/requests/api/notes_spec.rb index 424f0a82e43..6c1e30791d2 100644 --- a/spec/requests/api/notes_spec.rb +++ b/spec/requests/api/notes_spec.rb @@ -9,6 +9,13 @@ describe API::Notes do project.add_reporter(user) end + context 'when there are cross-reference system notes' do + let(:url) { "/projects/#{project.id}/merge_requests/#{merge_request.iid}/notes" } + let(:notes_in_response) { json_response } + + it_behaves_like 'with cross-reference system notes' + end + context "when noteable is an Issue" do let!(:issue) { create(:issue, project: project, author: user) } let!(:issue_note) { create(:note, noteable: issue, project: project, author: user) } diff --git a/spec/requests/api/pipelines_spec.rb b/spec/requests/api/pipelines_spec.rb index 35b3dd219f7..174b3214d13 100644 --- a/spec/requests/api/pipelines_spec.rb +++ b/spec/requests/api/pipelines_spec.rb @@ -17,6 +17,8 @@ describe API::Pipelines do end describe 'GET /projects/:id/pipelines ' do + it_behaves_like 'pipelines visibility table' + context 'authorized user' do it 'returns project pipelines' do get api("/projects/#{project.id}/pipelines", user) @@ -401,6 +403,15 @@ describe API::Pipelines do end describe 'GET /projects/:id/pipelines/:pipeline_id' do + it_behaves_like 'pipelines visibility table' do + let(:pipelines_api_path) do + "/projects/#{project.id}/pipelines/#{pipeline.id}" + end + + let(:api_response) { response_status == 200 ? response : json_response } + let(:response_200) { match_response_schema('public_api/v4/pipeline/detail') } + end + context 'authorized user' do it 'exposes known attributes' do get api("/projects/#{project.id}/pipelines/#{pipeline.id}", user) diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index 1d7ca85cdd2..5465fe0c366 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -1494,6 +1494,17 @@ describe API::Projects do expect(response).to have_gitlab_http_status(404) end + + it 'filters out users listed in skip_users' do + other_user = create(:user) + project.team.add_developer(other_user) + + get api("/projects/#{project.id}/users?skip_users=#{user.id}", user) + + expect(response).to have_gitlab_http_status(200) + expect(json_response.size).to eq(1) + expect(json_response[0]['id']).to eq(other_user.id) + end end end diff --git a/spec/requests/api/settings_spec.rb b/spec/requests/api/settings_spec.rb index 184c00a356a..590107d5161 100644 --- a/spec/requests/api/settings_spec.rb +++ b/spec/requests/api/settings_spec.rb @@ -144,6 +144,7 @@ describe API::Settings, 'Settings' do external_auth_client_key_pass: "5iveL!fe" } end + let(:attribute_names) { settings.keys.map(&:to_s) } it 'includes the attributes in the API' do @@ -165,6 +166,56 @@ describe API::Settings, 'Settings' do end end + context "snowplow tracking settings" do + let(:settings) do + { + snowplow_collector_hostname: "snowplow.example.com", + snowplow_cookie_domain: ".example.com", + snowplow_enabled: true, + snowplow_site_id: "site_id" + } + end + + let(:attribute_names) { settings.keys.map(&:to_s) } + + it "includes the attributes in the API" do + get api("/application/settings", admin) + + expect(response).to have_gitlab_http_status(200) + attribute_names.each do |attribute| + expect(json_response.keys).to include(attribute) + end + end + + it "allows updating the settings" do + put api("/application/settings", admin), params: settings + + expect(response).to have_gitlab_http_status(200) + settings.each do |attribute, value| + expect(ApplicationSetting.current.public_send(attribute)).to eq(value) + end + end + + context "missing snowplow_collector_hostname value when snowplow_enabled is true" do + it "returns a blank parameter error message" do + put api("/application/settings", admin), params: { snowplow_enabled: true } + + expect(response).to have_gitlab_http_status(400) + expect(json_response["error"]).to eq("snowplow_collector_hostname is missing") + end + + it "handles validation errors" do + put api("/application/settings", admin), params: settings.merge({ + snowplow_collector_hostname: nil + }) + + expect(response).to have_gitlab_http_status(400) + message = json_response["message"] + expect(message["snowplow_collector_hostname"]).to include("can't be blank") + end + end + end + context "missing plantuml_url value when plantuml_enabled is true" do it "returns a blank parameter error message" do put api("/application/settings", admin), params: { plantuml_enabled: true } diff --git a/spec/requests/jwt_controller_spec.rb b/spec/requests/jwt_controller_spec.rb index bba473f1c20..8b2c698fee1 100644 --- a/spec/requests/jwt_controller_spec.rb +++ b/spec/requests/jwt_controller_spec.rb @@ -108,6 +108,14 @@ describe JwtController do end end end + + it 'does not cause session based checks to be activated' do + expect(Gitlab::Session).not_to receive(:with_session) + + get '/jwt/auth', params: parameters, headers: headers + + expect(response).to have_gitlab_http_status(200) + end end context 'using invalid login' do diff --git a/spec/requests/rack_attack_global_spec.rb b/spec/requests/rack_attack_global_spec.rb index d832963292c..478f09a7881 100644 --- a/spec/requests/rack_attack_global_spec.rb +++ b/spec/requests/rack_attack_global_spec.rb @@ -112,9 +112,9 @@ describe 'Rack Attack global throttles' do arguments = { message: 'Rack_Attack', env: :throttle, - ip: '127.0.0.1', + remote_ip: '127.0.0.1', request_method: 'GET', - fullpath: get_args.first, + path: get_args.first, user_id: user.id, username: user.username } @@ -213,9 +213,9 @@ describe 'Rack Attack global throttles' do arguments = { message: 'Rack_Attack', env: :throttle, - ip: '127.0.0.1', + remote_ip: '127.0.0.1', request_method: 'GET', - fullpath: '/users/sign_in' + path: '/users/sign_in' } expect(Gitlab::AuthLogger).to receive(:error).with(arguments) @@ -377,9 +377,9 @@ describe 'Rack Attack global throttles' do arguments = { message: 'Rack_Attack', env: :throttle, - ip: '127.0.0.1', + remote_ip: '127.0.0.1', request_method: 'GET', - fullpath: '/dashboard/snippets', + path: '/dashboard/snippets', user_id: user.id, username: user.username } diff --git a/spec/rubocop/cop/migration/add_limit_to_string_columns_spec.rb b/spec/rubocop/cop/migration/add_limit_to_string_columns_spec.rb new file mode 100644 index 00000000000..97a3ae8f2bc --- /dev/null +++ b/spec/rubocop/cop/migration/add_limit_to_string_columns_spec.rb @@ -0,0 +1,268 @@ +# frozen_string_literal: true + +require 'spec_helper' +require 'rubocop' +require 'rubocop/rspec/support' + +require_relative '../../../../rubocop/cop/migration/add_limit_to_string_columns' + +describe RuboCop::Cop::Migration::AddLimitToStringColumns do + include CopHelper + + subject(:cop) { described_class.new } + + context 'in migration' do + before do + allow(cop).to receive(:in_migration?).and_return(true) + + inspect_source(migration) + end + + context 'when creating a table' do + context 'with string columns and limit' do + let(:migration) do + %q( + class CreateUsers < ActiveRecord::Migration[5.2] + DOWNTIME = false + + def change + create_table :users do |t| + t.string :username, null: false, limit: 255 + t.timestamps_with_timezone null: true + end + end + end + ) + end + + it 'register no offense' do + expect(cop.offenses.size).to eq(0) + end + + context 'with limit in a different position' do + let(:migration) do + %q( + class CreateUsers < ActiveRecord::Migration[5.2] + DOWNTIME = false + + def change + create_table :users do |t| + t.string :username, limit: 255, null: false + t.timestamps_with_timezone null: true + end + end + end + ) + end + + it 'registers an offense' do + expect(cop.offenses.size).to eq(0) + end + end + end + + context 'with string columns and no limit' do + let(:migration) do + %q( + class CreateUsers < ActiveRecord::Migration[5.2] + DOWNTIME = false + + def change + create_table :users do |t| + t.string :username, null: false + t.timestamps_with_timezone null: true + end + end + end + ) + end + + it 'registers an offense' do + expect(cop.offenses.size).to eq(1) + expect(cop.offenses.first.message) + .to eq('String columns should have a limit constraint. 255 is suggested') + end + end + + context 'with no string columns' do + let(:migration) do + %q( + class CreateMilestoneReleases < ActiveRecord::Migration[5.2] + DOWNTIME = false + + def change + create_table :milestone_releases do |t| + t.integer :milestone_id + t.integer :release_id + end + end + end + ) + end + + it 'register no offense' do + expect(cop.offenses.size).to eq(0) + end + end + end + + context 'when adding columns' do + context 'with string columns with limit' do + let(:migration) do + %q( + class AddEmailToUsers < ActiveRecord::Migration[5.2] + DOWNTIME = false + + def change + add_column :users, :email, :string, limit: 255 + end + end + ) + end + + it 'registers no offense' do + expect(cop.offenses.size).to eq(0) + end + + context 'with limit in a different position' do + let(:migration) do + %q( + class AddEmailToUsers < ActiveRecord::Migration[5.2] + DOWNTIME = false + + def change + add_column :users, :email, :string, limit: 255, default: 'example@email.com' + end + end + ) + end + + it 'registers no offense' do + expect(cop.offenses.size).to eq(0) + end + end + end + + context 'with string columns with no limit' do + let(:migration) do + %q( + class AddEmailToUsers < ActiveRecord::Migration[5.2] + DOWNTIME = false + + def change + add_column :users, :email, :string + end + end + ) + end + + it 'registers offense' do + expect(cop.offenses.size).to eq(1) + expect(cop.offenses.first.message) + .to eq('String columns should have a limit constraint. 255 is suggested') + end + end + + context 'with no string columns' do + let(:migration) do + %q( + class AddEmailToUsers < ActiveRecord::Migration[5.2] + DOWNTIME = false + + def change + add_column :users, :active, :boolean, default: false + end + end + ) + end + + it 'registers no offense' do + expect(cop.offenses.size).to eq(0) + end + end + end + + context 'with add_column_with_default' do + context 'with a limit' do + let(:migration) do + %q( + class AddRuleTypeToApprovalMergeRequestRules < ActiveRecord::Migration[5.2] + DOWNTIME = false + + def change + add_column_with_default(:approval_merge_request_rules, :rule_type, :string, limit: 2, default: 1) + end + end + ) + end + + it 'registers no offense' do + expect(cop.offenses.size).to eq(0) + end + end + + context 'without a limit' do + let(:migration) do + %q( + class AddRuleTypeToApprovalMergeRequestRules < ActiveRecord::Migration[5.2] + DOWNTIME = false + + def change + add_column_with_default(:approval_merge_request_rules, :rule_type, :string, default: 1) + end + end + ) + end + + it 'registers an offense' do + expect(cop.offenses.size).to eq(1) + end + end + end + + context 'with methods' do + let(:migration) do + %q( + class AddEmailToUsers < ActiveRecord::Migration[5.2] + DOWNTIME = false + + def change + add_column_if_table_not_exists :users, :first_name, :string, limit: 255 + search_namespace(user_name) + end + + def add_column_if_not_exists(table, name, *args) + add_column(table, name, *args) unless column_exists?(table, name) + end + + def search_namespace(username) + Uniquify.new.string(username) do |str| + query = "SELECT id FROM namespaces WHERE parent_id IS NULL AND path='#{str}' LIMIT 1" + connection.exec_query(query) + end + end + end + ) + end + + it 'registers no offense' do + expect(cop.offenses.size).to eq(0) + end + end + end + + context 'outside of migrations' do + let(:active_record_model) do + %q( + class User < ApplicationRecord + end + ) + end + + it 'registers no offense' do + inspect_source(active_record_model) + + expect(cop.offenses.size).to eq(0) + end + end +end diff --git a/spec/serializers/deployment_entity_spec.rb b/spec/serializers/deployment_entity_spec.rb index 76ad2aee5c5..1b19eac9a97 100644 --- a/spec/serializers/deployment_entity_spec.rb +++ b/spec/serializers/deployment_entity_spec.rb @@ -32,6 +32,10 @@ describe DeploymentEntity do expect(subject).to include(:created_at) end + it 'exposes deployed_at' do + expect(subject).to include(:deployed_at) + end + context 'when the pipeline has another manual action' do let(:other_build) { create(:ci_build, :manual, name: 'another deploy', pipeline: pipeline) } let!(:other_deployment) { create(:deployment, deployable: other_build) } diff --git a/spec/serializers/merge_request_sidebar_basic_entity_spec.rb b/spec/serializers/merge_request_sidebar_basic_entity_spec.rb new file mode 100644 index 00000000000..b364b1a3306 --- /dev/null +++ b/spec/serializers/merge_request_sidebar_basic_entity_spec.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe MergeRequestSidebarBasicEntity do + let(:project) { create :project, :repository } + let(:merge_request) { create(:merge_request, source_project: project, target_project: project) } + let(:user) { create(:user) } + + let(:request) { double('request', current_user: user, project: project) } + + let(:entity) { described_class.new(merge_request, request: request).as_json } + + describe '#current_user' do + it 'contains attributes related to the current user' do + expect(entity[:current_user].keys).to contain_exactly( + :id, :name, :username, :state, :avatar_url, :web_url, :todo, + :can_edit, :can_move, :can_admin_label, :can_merge + ) + end + end +end diff --git a/spec/services/award_emojis/add_service_spec.rb b/spec/services/award_emojis/add_service_spec.rb new file mode 100644 index 00000000000..037db39ba80 --- /dev/null +++ b/spec/services/award_emojis/add_service_spec.rb @@ -0,0 +1,103 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe AwardEmojis::AddService do + set(:user) { create(:user) } + set(:project) { create(:project) } + set(:awardable) { create(:note, project: project) } + let(:name) { 'thumbsup' } + subject(:service) { described_class.new(awardable, name, user) } + + describe '#execute' do + context 'when user is not authorized' do + it 'does not add an emoji' do + expect { service.execute }.not_to change { AwardEmoji.count } + end + + it 'returns an error state' do + result = service.execute + + expect(result[:status]).to eq(:error) + expect(result[:http_status]).to eq(:forbidden) + end + end + + context 'when user is authorized' do + before do + project.add_developer(user) + end + + it 'creates an award emoji' do + expect { service.execute }.to change { AwardEmoji.count }.by(1) + end + + it 'returns the award emoji' do + result = service.execute + + expect(result[:award]).to be_kind_of(AwardEmoji) + end + + it 'return a success status' do + result = service.execute + + expect(result[:status]).to eq(:success) + end + + it 'sets the correct properties on the award emoji' do + award = service.execute[:award] + + expect(award.name).to eq(name) + expect(award.user).to eq(user) + end + + describe 'marking Todos as done' do + subject { service.execute } + + include_examples 'creating award emojis marks Todos as done' + end + + context 'when the awardable cannot have emoji awarded to it' do + before do + expect(awardable).to receive(:emoji_awardable?).and_return(false) + end + + it 'does not add an emoji' do + expect { service.execute }.not_to change { AwardEmoji.count } + end + + it 'returns an error status' do + result = service.execute + + expect(result[:status]).to eq(:error) + expect(result[:http_status]).to eq(:unprocessable_entity) + end + end + + context 'when the awardable is invalid' do + before do + expect_next_instance_of(AwardEmoji) do |award| + expect(award).to receive(:valid?).and_return(false) + expect(award).to receive_message_chain(:errors, :full_messages).and_return(['Error 1', 'Error 2']) + end + end + + it 'does not add an emoji' do + expect { service.execute }.not_to change { AwardEmoji.count } + end + + it 'returns an error status' do + result = service.execute + + expect(result[:status]).to eq(:error) + end + + it 'returns an error message' do + result = service.execute + + expect(result[:message]).to eq('Error 1 and Error 2') + end + end + end + end +end diff --git a/spec/finders/awarded_emoji_finder_spec.rb b/spec/services/award_emojis/collect_user_emoji_service_spec.rb index d4479df7418..a0dea31b403 100644 --- a/spec/finders/awarded_emoji_finder_spec.rb +++ b/spec/services/award_emojis/collect_user_emoji_service_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe AwardedEmojiFinder do +describe AwardEmojis::CollectUserEmojiService do describe '#execute' do it 'returns an Array containing the awarded emoji names' do user = create(:user) diff --git a/spec/services/award_emojis/destroy_service_spec.rb b/spec/services/award_emojis/destroy_service_spec.rb new file mode 100644 index 00000000000..c4a7d5ec20e --- /dev/null +++ b/spec/services/award_emojis/destroy_service_spec.rb @@ -0,0 +1,89 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe AwardEmojis::DestroyService do + set(:user) { create(:user) } + set(:awardable) { create(:note) } + set(:project) { awardable.project } + let(:name) { 'thumbsup' } + let!(:award_from_other_user) do + create(:award_emoji, name: name, awardable: awardable, user: create(:user)) + end + subject(:service) { described_class.new(awardable, name, user) } + + describe '#execute' do + shared_examples_for 'a service that does not authorize the user' do |error:| + it 'does not remove the emoji' do + expect { service.execute }.not_to change { AwardEmoji.count } + end + + it 'returns an error state' do + result = service.execute + + expect(result[:status]).to eq(:error) + expect(result[:http_status]).to eq(:forbidden) + end + + it 'returns a nil award' do + result = service.execute + + expect(result).to have_key(:award) + expect(result[:award]).to be_nil + end + + it 'returns the error' do + result = service.execute + + expect(result[:message]).to eq(error) + expect(result[:errors]).to eq([error]) + end + end + + context 'when user is not authorized' do + it_behaves_like 'a service that does not authorize the user', + error: 'User cannot destroy emoji on the awardable' + end + + context 'when the user is authorized' do + before do + project.add_developer(user) + end + + context 'when user has not awarded an emoji to the awardable' do + let!(:award_from_user) { create(:award_emoji, name: name, user: user) } + + it_behaves_like 'a service that does not authorize the user', + error: 'User has not awarded emoji of type thumbsup on the awardable' + end + + context 'when user has awarded an emoji to the awardable' do + let!(:award_from_user) { create(:award_emoji, name: name, awardable: awardable, user: user) } + + it 'removes the emoji' do + expect { service.execute }.to change { AwardEmoji.count }.by(-1) + end + + it 'returns a success status' do + result = service.execute + + expect(result[:status]).to eq(:success) + end + + it 'returns no errors' do + result = service.execute + + expect(result).not_to have_key(:error) + expect(result).not_to have_key(:errors) + end + + it 'returns the destroyed award' do + result = service.execute + + expect(result[:award]).to eq(award_from_user) + expect(result[:award]).to be_destroyed + end + end + end + end +end diff --git a/spec/services/award_emojis/toggle_service_spec.rb b/spec/services/award_emojis/toggle_service_spec.rb new file mode 100644 index 00000000000..972a1d5fc06 --- /dev/null +++ b/spec/services/award_emojis/toggle_service_spec.rb @@ -0,0 +1,72 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe AwardEmojis::ToggleService do + set(:user) { create(:user) } + set(:project) { create(:project, :public) } + set(:awardable) { create(:note, project: project) } + let(:name) { 'thumbsup' } + subject(:service) { described_class.new(awardable, name, user) } + + describe '#execute' do + context 'when user has awarded an emoji' do + let!(:award_from_other_user) { create(:award_emoji, name: name, awardable: awardable, user: create(:user)) } + let!(:award) { create(:award_emoji, name: name, awardable: awardable, user: user) } + + it 'calls AwardEmojis::DestroyService' do + expect(AwardEmojis::AddService).not_to receive(:new) + + expect_next_instance_of(AwardEmojis::DestroyService) do |service| + expect(service).to receive(:execute) + end + + service.execute + end + + it 'destroys an AwardEmoji' do + expect { service.execute }.to change { AwardEmoji.count }.by(-1) + end + + it 'returns the result of DestroyService#execute' do + mock_result = double(foo: true) + + expect_next_instance_of(AwardEmojis::DestroyService) do |service| + expect(service).to receive(:execute).and_return(mock_result) + end + + result = service.execute + + expect(result).to eq(mock_result) + end + end + + context 'when user has not awarded an emoji' do + it 'calls AwardEmojis::AddService' do + expect_next_instance_of(AwardEmojis::AddService) do |service| + expect(service).to receive(:execute) + end + + expect(AwardEmojis::DestroyService).not_to receive(:new) + + service.execute + end + + it 'creates an AwardEmoji' do + expect { service.execute }.to change { AwardEmoji.count }.by(1) + end + + it 'returns the result of AddService#execute' do + mock_result = double(foo: true) + + expect_next_instance_of(AwardEmojis::AddService) do |service| + expect(service).to receive(:execute).and_return(mock_result) + end + + result = service.execute + + expect(result).to eq(mock_result) + end + end + end +end diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb index 7e2f311a065..deb68899309 100644 --- a/spec/services/ci/create_pipeline_service_spec.rb +++ b/spec/services/ci/create_pipeline_service_spec.rb @@ -1113,7 +1113,7 @@ describe Ci::CreatePipelineService do test_a: { stage: "test", script: "ls", - only: %w[master feature tags], + only: %w[master feature], needs: %w[build_a] }, deploy: { @@ -1143,6 +1143,7 @@ describe Ci::CreatePipelineService do it 'does not create a pipeline as test_a depends on build_a' do expect(pipeline).not_to be_persisted expect(pipeline.builds).to be_empty + expect(pipeline.errors[:base]).to contain_exactly("test_a: needs 'build_a'") end end diff --git a/spec/services/ci/update_build_queue_service_spec.rb b/spec/services/ci/update_build_queue_service_spec.rb index 4b869385128..522dd1ba1c2 100644 --- a/spec/services/ci/update_build_queue_service_spec.rb +++ b/spec/services/ci/update_build_queue_service_spec.rb @@ -7,84 +7,108 @@ describe Ci::UpdateBuildQueueService do let(:build) { create(:ci_build, pipeline: pipeline) } let(:pipeline) { create(:ci_pipeline, project: project) } - context 'when updating specific runners' do - let(:runner) { create(:ci_runner, :project, projects: [project]) } - - context 'when there is a runner that can pick build' do - it 'ticks runner queue value' do - expect { subject.execute(build) }.to change { runner.ensure_runner_queue_value } - end + shared_examples 'refreshes runner' do + it 'ticks runner queue value' do + expect { subject.execute(build) }.to change { runner.ensure_runner_queue_value } end + end - context 'when there is no runner that can pick build' do - let(:another_project) { create(:project) } - let(:runner) { create(:ci_runner, :project, projects: [another_project]) } - - it 'does not tick runner queue value' do - expect { subject.execute(build) }.not_to change { runner.ensure_runner_queue_value } - end + shared_examples 'does not refresh runner' do + it 'ticks runner queue value' do + expect { subject.execute(build) }.not_to change { runner.ensure_runner_queue_value } end end - context 'when updating shared runners' do - let(:runner) { create(:ci_runner, :instance) } - - context 'when there is no runner that can pick build' do - it 'ticks runner queue value' do - expect { subject.execute(build) }.to change { runner.ensure_runner_queue_value } + shared_examples 'matching build' do + context 'when there is a online runner that can pick build' do + before do + runner.update!(contacted_at: 30.minutes.ago) end + + it_behaves_like 'refreshes runner' end + end + shared_examples 'mismatching tags' do context 'when there is no runner that can pick build due to tag mismatch' do before do build.tag_list = [:docker] end - it 'does not tick runner queue value' do - expect { subject.execute(build) }.not_to change { runner.ensure_runner_queue_value } - end + it_behaves_like 'does not refresh runner' end + end - context 'when there is no runner that can pick build due to being disabled on project' do + shared_examples 'recent runner queue' do + context 'when there is runner with expired cache' do before do - build.project.shared_runners_enabled = false + runner.update!(contacted_at: Ci::Runner.recent_queue_deadline) end - it 'does not tick runner queue value' do - expect { subject.execute(build) }.not_to change { runner.ensure_runner_queue_value } + context 'when ci_update_queues_for_online_runners is enabled' do + before do + stub_feature_flags(ci_update_queues_for_online_runners: true) + end + + it_behaves_like 'does not refresh runner' + end + + context 'when ci_update_queues_for_online_runners is disabled' do + before do + stub_feature_flags(ci_update_queues_for_online_runners: false) + end + + it_behaves_like 'refreshes runner' end end end - context 'when updating group runners' do - let(:group) { create(:group) } - let(:project) { create(:project, group: group) } - let(:runner) { create(:ci_runner, :group, groups: [group]) } + context 'when updating specific runners' do + let(:runner) { create(:ci_runner, :project, projects: [project]) } - context 'when there is a runner that can pick build' do - it 'ticks runner queue value' do - expect { subject.execute(build) }.to change { runner.ensure_runner_queue_value } - end + it_behaves_like 'matching build' + it_behaves_like 'mismatching tags' + it_behaves_like 'recent runner queue' + + context 'when the runner is assigned to another project' do + let(:another_project) { create(:project) } + let(:runner) { create(:ci_runner, :project, projects: [another_project]) } + + it_behaves_like 'does not refresh runner' end + end - context 'when there is no runner that can pick build due to tag mismatch' do + context 'when updating shared runners' do + let(:runner) { create(:ci_runner, :instance) } + + it_behaves_like 'matching build' + it_behaves_like 'mismatching tags' + it_behaves_like 'recent runner queue' + + context 'when there is no runner that can pick build due to being disabled on project' do before do - build.tag_list = [:docker] + build.project.shared_runners_enabled = false end - it 'does not tick runner queue value' do - expect { subject.execute(build) }.not_to change { runner.ensure_runner_queue_value } - end + it_behaves_like 'does not refresh runner' end + end + + context 'when updating group runners' do + let(:group) { create(:group) } + let(:project) { create(:project, group: group) } + let(:runner) { create(:ci_runner, :group, groups: [group]) } + + it_behaves_like 'matching build' + it_behaves_like 'mismatching tags' + it_behaves_like 'recent runner queue' context 'when there is no runner that can pick build due to being disabled on project' do before do build.project.group_runners_enabled = false end - it 'does not tick runner queue value' do - expect { subject.execute(build) }.not_to change { runner.ensure_runner_queue_value } - end + it_behaves_like 'does not refresh runner' end end end diff --git a/spec/services/git/base_hooks_service_spec.rb b/spec/services/git/base_hooks_service_spec.rb index 4a2ec769116..874df9a68cd 100644 --- a/spec/services/git/base_hooks_service_spec.rb +++ b/spec/services/git/base_hooks_service_spec.rb @@ -14,6 +14,78 @@ describe Git::BaseHooksService do let(:newrev) { "8a2a6eb295bb170b34c24c76c49ed0e9b2eaf34b" } # gitlab-test: git rev-parse refs/tags/v1.1.0 let(:ref) { 'refs/tags/v1.1.0' } + describe '#execute_project_hooks' do + class TestService < described_class + def hook_name + :push_hooks + end + + def commits + [] + end + end + + let(:project) { create(:project, :repository) } + + subject { TestService.new(project, user, oldrev: oldrev, newrev: newrev, ref: ref) } + + context '#execute_hooks' do + before do + expect(project).to receive(:has_active_hooks?).and_return(active) + end + + context 'active hooks' do + let(:active) { true } + + it 'executes the hooks' do + expect(subject).to receive(:push_data).at_least(:once).and_call_original + expect(project).to receive(:execute_hooks) + + subject.execute + end + end + + context 'inactive hooks' do + let(:active) { false } + + it 'does not execute the hooks' do + expect(subject).not_to receive(:push_data) + expect(project).not_to receive(:execute_hooks) + + subject.execute + end + end + end + + context '#execute_services' do + before do + expect(project).to receive(:has_active_services?).and_return(active) + end + + context 'active services' do + let(:active) { true } + + it 'executes the services' do + expect(subject).to receive(:push_data).at_least(:once).and_call_original + expect(project).to receive(:execute_services) + + subject.execute + end + end + + context 'inactive services' do + let(:active) { false } + + it 'does not execute the services' do + expect(subject).not_to receive(:push_data) + expect(project).not_to receive(:execute_services) + + subject.execute + end + end + end + end + describe 'with remote mirrors' do class TestService < described_class def commits diff --git a/spec/services/git/branch_hooks_service_spec.rb b/spec/services/git/branch_hooks_service_spec.rb index 23be400059e..2bf7dc32436 100644 --- a/spec/services/git/branch_hooks_service_spec.rb +++ b/spec/services/git/branch_hooks_service_spec.rb @@ -4,6 +4,7 @@ require 'spec_helper' describe Git::BranchHooksService do include RepoHelpers + include ProjectForksHelper let(:project) { create(:project, :repository) } let(:user) { project.creator } @@ -25,7 +26,7 @@ describe Git::BranchHooksService do end describe "Git Push Data" do - subject(:push_data) { service.execute } + subject(:push_data) { service.send(:push_data) } it 'has expected push data attributes' do is_expected.to match a_hash_including( @@ -109,6 +110,7 @@ describe Git::BranchHooksService do expect(event.push_event_payload).to be_an_instance_of(PushEventPayload) expect(event.push_event_payload.commit_from).to eq(oldrev) expect(event.push_event_payload.commit_to).to eq(newrev) + expect(event.push_event_payload.commit_title).to eq('Change some files') expect(event.push_event_payload.ref).to eq('master') expect(event.push_event_payload.commit_count).to eq(1) end @@ -124,6 +126,7 @@ describe Git::BranchHooksService do expect(event.push_event_payload).to be_an_instance_of(PushEventPayload) expect(event.push_event_payload.commit_from).to be_nil expect(event.push_event_payload.commit_to).to eq(newrev) + expect(event.push_event_payload.commit_title).to eq('Initial commit') expect(event.push_event_payload.ref).to eq('master') expect(event.push_event_payload.commit_count).to be > 1 end @@ -156,9 +159,13 @@ describe Git::BranchHooksService do let(:blank_sha) { Gitlab::Git::BLANK_SHA } def clears_cache(extended: []) - expect(ProjectCacheWorker) - .to receive(:perform_async) - .with(project.id, extended, %i[commit_count repository_size]) + expect(service).to receive(:invalidated_file_types).and_return(extended) + + if extended.present? + expect(ProjectCacheWorker) + .to receive(:perform_async) + .with(project.id, extended, [], false) + end service.execute end @@ -266,10 +273,10 @@ describe Git::BranchHooksService do end describe 'Processing commit messages' do - # Create 4 commits, 2 of which have references. Limiting to 2 commits, we - # expect to see one commit message processor enqueued. - let(:commit_ids) do - Array.new(4) do |i| + # Create 6 commits, 3 of which have references. Limiting to 4 commits, we + # expect to see two commit message processors enqueued. + let!(:commit_ids) do + Array.new(6) do |i| message = "Issue #{'#' if i.even?}#{i}" project.repository.update_file( user, 'README.md', '', message: message, branch_name: branch @@ -277,18 +284,18 @@ describe Git::BranchHooksService do end end - let(:oldrev) { commit_ids.first } + let(:oldrev) { project.commit(commit_ids.first).parent_id } let(:newrev) { commit_ids.last } before do - stub_const("::Git::BaseHooksService::PROCESS_COMMIT_LIMIT", 2) + stub_const("::Git::BaseHooksService::PROCESS_COMMIT_LIMIT", 4) end context 'creating the default branch' do let(:oldrev) { Gitlab::Git::BLANK_SHA } it 'processes a limited number of commit messages' do - expect(ProcessCommitWorker).to receive(:perform_async).once + expect(ProcessCommitWorker).to receive(:perform_async).twice service.execute end @@ -296,7 +303,7 @@ describe Git::BranchHooksService do context 'updating the default branch' do it 'processes a limited number of commit messages' do - expect(ProcessCommitWorker).to receive(:perform_async).once + expect(ProcessCommitWorker).to receive(:perform_async).twice service.execute end @@ -317,7 +324,7 @@ describe Git::BranchHooksService do let(:oldrev) { Gitlab::Git::BLANK_SHA } it 'processes a limited number of commit messages' do - expect(ProcessCommitWorker).to receive(:perform_async).once + expect(ProcessCommitWorker).to receive(:perform_async).twice service.execute end @@ -327,7 +334,7 @@ describe Git::BranchHooksService do let(:branch) { 'fix' } it 'processes a limited number of commit messages' do - expect(ProcessCommitWorker).to receive(:perform_async).once + expect(ProcessCommitWorker).to receive(:perform_async).twice service.execute end @@ -343,6 +350,55 @@ describe Git::BranchHooksService do service.execute end end + + context 'when the project is forked' do + let(:upstream_project) { project } + let(:forked_project) { fork_project(upstream_project, user, repository: true) } + + let!(:forked_service) do + described_class.new(forked_project, user, oldrev: oldrev, newrev: newrev, ref: ref) + end + + context 'when commits already exists in the upstream project' do + it 'does not process commit messages' do + expect(ProcessCommitWorker).not_to receive(:perform_async) + + forked_service.execute + end + end + + context 'when a commit does not exist in the upstream repo' do + # On top of the existing 6 commits, 3 of which have references, + # create 2 more, 1 of which has a reference. Limiting to 4 commits, we + # expect to see one commit message processor enqueued. + let!(:forked_commit_ids) do + Array.new(2) do |i| + message = "Issue #{'#' if i.even?}#{i}" + forked_project.repository.update_file( + user, 'README.md', '', message: message, branch_name: branch + ) + end + end + + let(:newrev) { forked_commit_ids.last } + + it 'processes the commit message' do + expect(ProcessCommitWorker).to receive(:perform_async).once + + forked_service.execute + end + end + + context 'when the upstream project no longer exists' do + it 'processes the commit messages' do + upstream_project.destroy! + + expect(ProcessCommitWorker).to receive(:perform_async).twice + + forked_service.execute + end + end + end end describe 'New branch detection' do diff --git a/spec/services/git/branch_push_service_spec.rb b/spec/services/git/branch_push_service_spec.rb index 6e39fa6b3c0..d9e607cd251 100644 --- a/spec/services/git/branch_push_service_spec.rb +++ b/spec/services/git/branch_push_service_spec.rb @@ -76,9 +76,28 @@ describe Git::BranchPushService, services: true do stub_ci_pipeline_to_return_yaml_file end + it 'creates a pipeline with the right parameters' do + expect(Ci::CreatePipelineService) + .to receive(:new) + .with(project, + user, + { + before: oldrev, + after: newrev, + ref: ref, + checkout_sha: SeedRepo::Commit::ID, + push_options: {} + }).and_call_original + + subject + end + it "creates a new pipeline" do expect { subject }.to change { Ci::Pipeline.count } - expect(Ci::Pipeline.last).to be_push + + pipeline = Ci::Pipeline.last + expect(pipeline).to be_push + expect(Gitlab::Git::BRANCH_REF_PREFIX + pipeline.ref).to eq(ref) end end @@ -123,6 +142,10 @@ describe Git::BranchPushService, services: true do describe "Webhooks" do context "execute webhooks" do + before do + create(:project_hook, push_events: true, project: project) + end + it "when pushing a branch for the first time" do expect(project).to receive(:execute_hooks) expect(project.default_branch).to eq("master") diff --git a/spec/services/git/tag_hooks_service_spec.rb b/spec/services/git/tag_hooks_service_spec.rb index f5938a5c708..e362577d289 100644 --- a/spec/services/git/tag_hooks_service_spec.rb +++ b/spec/services/git/tag_hooks_service_spec.rb @@ -26,7 +26,8 @@ describe Git::TagHooksService, :service do describe 'System hooks' do it 'Executes system hooks' do - push_data = service.execute + push_data = service.send(:push_data) + expect(project).to receive(:has_active_hooks?).and_return(true) expect_next_instance_of(SystemHooksService) do |system_hooks_service| expect(system_hooks_service) @@ -40,6 +41,7 @@ describe Git::TagHooksService, :service do describe "Webhooks" do it "executes hooks on the project" do + expect(project).to receive(:has_active_hooks?).and_return(true) expect(project).to receive(:execute_hooks) service.execute @@ -61,7 +63,7 @@ describe Git::TagHooksService, :service do describe 'Push data' do shared_examples_for 'tag push data expectations' do - subject(:push_data) { service.execute } + subject(:push_data) { service.send(:push_data) } it 'has expected push data attributes' do is_expected.to match a_hash_including( object_kind: 'tag_push', diff --git a/spec/services/git/tag_push_service_spec.rb b/spec/services/git/tag_push_service_spec.rb index 418952b52da..7e008637182 100644 --- a/spec/services/git/tag_push_service_spec.rb +++ b/spec/services/git/tag_push_service_spec.rb @@ -26,8 +26,8 @@ describe Git::TagPushService do subject end - it 'flushes the tags cache' do - expect(project.repository).to receive(:expire_tags_cache) + it 'does not flush the tags cache' do + expect(project.repository).not_to receive(:expire_tags_cache) subject end diff --git a/spec/services/groups/update_service_spec.rb b/spec/services/groups/update_service_spec.rb index 5d4576139f7..12e9c2b2f3a 100644 --- a/spec/services/groups/update_service_spec.rb +++ b/spec/services/groups/update_service_spec.rb @@ -86,6 +86,7 @@ describe Groups::UpdateService do context "unauthorized visibility_level validation" do let!(:service) { described_class.new(internal_group, user, visibility_level: 99) } + before do internal_group.add_user(user, Gitlab::Access::MAINTAINER) end @@ -96,6 +97,20 @@ describe Groups::UpdateService do end end + context 'when updating #emails_disabled' do + let(:service) { described_class.new(internal_group, user, emails_disabled: true) } + + it 'updates the attribute' do + internal_group.add_user(user, Gitlab::Access::OWNER) + + expect { service.execute }.to change { internal_group.emails_disabled }.to(true) + end + + it 'does not update when not group owner' do + expect { service.execute }.not_to change { internal_group.emails_disabled } + end + end + context 'rename group' do let!(:service) { described_class.new(internal_group, user, path: SecureRandom.hex) } diff --git a/spec/services/issues/close_service_spec.rb b/spec/services/issues/close_service_spec.rb index 6874a8a0929..642a49d57d5 100644 --- a/spec/services/issues/close_service_spec.rb +++ b/spec/services/issues/close_service_spec.rb @@ -60,35 +60,63 @@ describe Issues::CloseService do describe '#close_issue' do context "closed by a merge request" do - before do + it 'mentions closure via a merge request' do perform_enqueued_jobs do described_class.new(project, user).close_issue(issue, closed_via: closing_merge_request) end - end - it 'mentions closure via a merge request' do email = ActionMailer::Base.deliveries.last expect(email.to.first).to eq(user2.email) expect(email.subject).to include(issue.title) expect(email.body.parts.map(&:body)).to all(include(closing_merge_request.to_reference)) end + + context 'when user cannot read merge request' do + it 'does not mention merge request' do + project.project_feature.update_attribute(:repository_access_level, ProjectFeature::DISABLED) + perform_enqueued_jobs do + described_class.new(project, user).close_issue(issue, closed_via: closing_merge_request) + end + + email = ActionMailer::Base.deliveries.last + body_text = email.body.parts.map(&:body).join(" ") + + expect(email.to.first).to eq(user2.email) + expect(email.subject).to include(issue.title) + expect(body_text).not_to include(closing_merge_request.to_reference) + end + end end context "closed by a commit" do - before do + it 'mentions closure via a commit' do perform_enqueued_jobs do described_class.new(project, user).close_issue(issue, closed_via: closing_commit) end - end - it 'mentions closure via a commit' do email = ActionMailer::Base.deliveries.last expect(email.to.first).to eq(user2.email) expect(email.subject).to include(issue.title) expect(email.body.parts.map(&:body)).to all(include(closing_commit.id)) end + + context 'when user cannot read the commit' do + it 'does not mention the commit id' do + project.project_feature.update_attribute(:repository_access_level, ProjectFeature::DISABLED) + perform_enqueued_jobs do + described_class.new(project, user).close_issue(issue, closed_via: closing_commit) + end + + email = ActionMailer::Base.deliveries.last + body_text = email.body.parts.map(&:body).join(" ") + + expect(email.to.first).to eq(user2.email) + expect(email.subject).to include(issue.title) + expect(body_text).not_to include(closing_commit.id) + end + end end context "valid params" do diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb index d9f35afee06..fd9a63b79cc 100644 --- a/spec/services/issues/update_service_spec.rb +++ b/spec/services/issues/update_service_spec.rb @@ -229,10 +229,10 @@ describe Issues::UpdateService, :mailer do it 'creates zoom_link_added system note when a zoom link is added to the description' do update_issue(description: 'Changed description https://zoom.us/j/5873603787') - note = find_note('a Zoom call was added') + note = find_note('added a Zoom call') expect(note).not_to be_nil - expect(note.note).to eq('a Zoom call was added to this issue') + expect(note.note).to eq('added a Zoom call to this issue') end context 'when issue turns confidential' do diff --git a/spec/services/merge_requests/rebase_service_spec.rb b/spec/services/merge_requests/rebase_service_spec.rb index ee9caaf2f47..7b8c94c86fe 100644 --- a/spec/services/merge_requests/rebase_service_spec.rb +++ b/spec/services/merge_requests/rebase_service_spec.rb @@ -25,7 +25,7 @@ describe MergeRequests::RebaseService do describe '#execute' do context 'when another rebase is already in progress' do before do - allow(merge_request).to receive(:gitaly_rebase_in_progress?).and_return(true) + allow(repository).to receive(:rebase_in_progress?).with(merge_request.id).and_return(true) end it 'saves the error message' do diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb index 1dcade1de0d..d925aa2b6c3 100644 --- a/spec/services/notification_service_spec.rb +++ b/spec/services/notification_service_spec.rb @@ -240,45 +240,50 @@ describe NotificationService, :mailer do end describe '#new_note' do - it do - add_users(project) - add_user_subscriptions(issue) - reset_delivered_emails! + context do + before do + add_users(project) + add_user_subscriptions(issue) + reset_delivered_emails! + end - expect(SentNotification).to receive(:record).with(issue, any_args).exactly(10).times + it do + expect(SentNotification).to receive(:record).with(issue, any_args).exactly(10).times - notification.new_note(note) + notification.new_note(note) - should_email(@u_watcher) - should_email(note.noteable.author) - should_email(note.noteable.assignees.first) - should_email(@u_custom_global) - should_email(@u_mentioned) - should_email(@subscriber) - should_email(@watcher_and_subscriber) - should_email(@subscribed_participant) - should_email(@u_custom_off) - should_email(@unsubscribed_mentioned) - should_not_email(@u_guest_custom) - should_not_email(@u_guest_watcher) - should_not_email(note.author) - should_not_email(@u_participating) - should_not_email(@u_disabled) - should_not_email(@unsubscriber) - should_not_email(@u_outsider_mentioned) - should_not_email(@u_lazy_participant) - end + should_email(@u_watcher) + should_email(note.noteable.author) + should_email(note.noteable.assignees.first) + should_email(@u_custom_global) + should_email(@u_mentioned) + should_email(@subscriber) + should_email(@watcher_and_subscriber) + should_email(@subscribed_participant) + should_email(@u_custom_off) + should_email(@unsubscribed_mentioned) + should_not_email(@u_guest_custom) + should_not_email(@u_guest_watcher) + should_not_email(note.author) + should_not_email(@u_participating) + should_not_email(@u_disabled) + should_not_email(@unsubscriber) + should_not_email(@u_outsider_mentioned) + should_not_email(@u_lazy_participant) + end - it "emails the note author if they've opted into notifications about their activity" do - add_users(project) - add_user_subscriptions(issue) - reset_delivered_emails! + it "emails the note author if they've opted into notifications about their activity" do + note.author.notified_of_own_activity = true - note.author.notified_of_own_activity = true + notification.new_note(note) - notification.new_note(note) + should_email(note.author) + end - should_email(note.author) + it_behaves_like 'project emails are disabled' do + let(:notification_target) { note } + let(:notification_trigger) { notification.new_note(note) } + end end it 'filters out "mentioned in" notes' do @@ -337,6 +342,11 @@ describe NotificationService, :mailer do it_behaves_like 'new note notifications' + it_behaves_like 'project emails are disabled' do + let(:notification_target) { note } + let(:notification_trigger) { notification.new_note(note) } + end + context 'which is a subgroup' do let!(:parent) { create(:group) } let!(:group) { create(:group, parent: parent) } @@ -472,6 +482,11 @@ describe NotificationService, :mailer do expect(Notify).not_to receive(:note_issue_email) notification.new_note(mentioned_note) end + + it_behaves_like 'project emails are disabled' do + let(:notification_target) { note } + let(:notification_trigger) { notification.new_note(note) } + end end end @@ -619,6 +634,11 @@ describe NotificationService, :mailer do notification.new_note(note) should_not_email(@u_committer) end + + it_behaves_like 'project emails are disabled' do + let(:notification_target) { note } + let(:notification_trigger) { notification.new_note(note) } + end end end @@ -645,6 +665,11 @@ describe NotificationService, :mailer do .to contain_exactly(*merge_request.assignees.pluck(:id), merge_request.author.id, @u_watcher.id) expect(SentNotification.last.in_reply_to_discussion_id).to eq(note.discussion_id) end + + it_behaves_like 'project emails are disabled' do + let(:notification_target) { note } + let(:notification_trigger) { notification.new_note(note) } + end end end end @@ -819,6 +844,11 @@ describe NotificationService, :mailer do should_email(user_4) end + it_behaves_like 'project emails are disabled' do + let(:notification_target) { issue } + let(:notification_trigger) { notification.new_issue(issue, @u_disabled) } + end + context 'confidential issues' do let(:author) { create(:user) } let(:assignee) { create(:user) } @@ -861,6 +891,11 @@ describe NotificationService, :mailer do let(:mentionable) { issue } include_examples 'notifications for new mentions' + + it_behaves_like 'project emails are disabled' do + let(:notification_target) { issue } + let(:notification_trigger) { send_notifications(@u_watcher, @u_participant_mentioned, @u_custom_global, @u_mentioned) } + end end describe '#reassigned_issue' do @@ -969,6 +1004,11 @@ describe NotificationService, :mailer do let(:issuable) { issue } let(:notification_trigger) { notification.reassigned_issue(issue, @u_disabled, [assignee]) } end + + it_behaves_like 'project emails are disabled' do + let(:notification_target) { issue } + let(:notification_trigger) { notification.reassigned_issue(issue, @u_disabled, [assignee]) } + end end describe '#relabeled_issue' do @@ -1028,6 +1068,11 @@ describe NotificationService, :mailer do should_email(subscriber_to_both) end + it_behaves_like 'project emails are disabled' do + let(:notification_target) { issue } + let(:notification_trigger) { notification.relabeled_issue(issue, [group_label_2, label_2], @u_disabled) } + end + context 'confidential issues' do let(:author) { create(:user) } let(:assignee) { create(:user) } @@ -1065,12 +1110,19 @@ describe NotificationService, :mailer do end describe '#removed_milestone_issue' do - it_behaves_like 'altered milestone notification on issue' do + context do let(:milestone) { create(:milestone, project: project, issues: [issue]) } let!(:subscriber_to_new_milestone) { create(:user) { |u| issue.toggle_subscription(u, project) } } - before do - notification.removed_milestone_issue(issue, issue.author) + it_behaves_like 'altered milestone notification on issue' do + before do + notification.removed_milestone_issue(issue, issue.author) + end + end + + it_behaves_like 'project emails are disabled' do + let(:notification_target) { issue } + let(:notification_trigger) { notification.removed_milestone_issue(issue, issue.author) } end end @@ -1110,12 +1162,19 @@ describe NotificationService, :mailer do end describe '#changed_milestone_issue' do - it_behaves_like 'altered milestone notification on issue' do + context do let(:new_milestone) { create(:milestone, project: project, issues: [issue]) } let!(:subscriber_to_new_milestone) { create(:user) { |u| issue.toggle_subscription(u, project) } } - before do - notification.changed_milestone_issue(issue, new_milestone, issue.author) + it_behaves_like 'altered milestone notification on issue' do + before do + notification.changed_milestone_issue(issue, new_milestone, issue.author) + end + end + + it_behaves_like 'project emails are disabled' do + let(:notification_target) { issue } + let(:notification_trigger) { notification.changed_milestone_issue(issue, new_milestone, issue.author) } end end @@ -1183,6 +1242,11 @@ describe NotificationService, :mailer do let(:issuable) { issue } let(:notification_trigger) { notification.close_issue(issue, @u_disabled) } end + + it_behaves_like 'project emails are disabled' do + let(:notification_target) { issue } + let(:notification_trigger) { notification.close_issue(issue, @u_disabled) } + end end describe '#reopen_issue' do @@ -1214,6 +1278,11 @@ describe NotificationService, :mailer do let(:issuable) { issue } let(:notification_trigger) { notification.reopen_issue(issue, @u_disabled) } end + + it_behaves_like 'project emails are disabled' do + let(:notification_target) { issue } + let(:notification_trigger) { notification.reopen_issue(issue, @u_disabled) } + end end describe '#issue_moved' do @@ -1240,6 +1309,11 @@ describe NotificationService, :mailer do let(:issuable) { issue } let(:notification_trigger) { notification.issue_moved(issue, new_issue, @u_disabled) } end + + it_behaves_like 'project emails are disabled' do + let(:notification_target) { issue } + let(:notification_trigger) { notification.issue_moved(issue, new_issue, @u_disabled) } + end end describe '#issue_due' do @@ -1280,6 +1354,11 @@ describe NotificationService, :mailer do let(:issuable) { issue } let(:notification_trigger) { notification.issue_due(issue) } end + + it_behaves_like 'project emails are disabled' do + let(:notification_target) { issue } + let(:notification_trigger) { notification.issue_due(issue) } + end end end @@ -1374,6 +1453,11 @@ describe NotificationService, :mailer do should_email(user_4) end + it_behaves_like 'project emails are disabled' do + let(:notification_target) { merge_request } + let(:notification_trigger) { notification.new_merge_request(merge_request, @u_disabled) } + end + context 'participating' do it_should_behave_like 'participating by assignee notification' do let(:participant) { create(:user, username: 'user-participant')} @@ -1406,6 +1490,11 @@ describe NotificationService, :mailer do let(:mentionable) { merge_request } include_examples 'notifications for new mentions' + + it_behaves_like 'project emails are disabled' do + let(:notification_target) { merge_request } + let(:notification_trigger) { send_notifications(@u_watcher, @u_participant_mentioned, @u_custom_global, @u_mentioned) } + end end describe '#reassigned_merge_request' do @@ -1449,6 +1538,11 @@ describe NotificationService, :mailer do let(:issuable) { merge_request } let(:notification_trigger) { notification.reassigned_merge_request(merge_request, current_user, [assignee]) } end + + it_behaves_like 'project emails are disabled' do + let(:notification_target) { merge_request } + let(:notification_trigger) { notification.reassigned_merge_request(merge_request, current_user, [assignee]) } + end end describe '#push_to_merge_request' do @@ -1479,6 +1573,11 @@ describe NotificationService, :mailer do let(:issuable) { merge_request } let(:notification_trigger) { notification.push_to_merge_request(merge_request, @u_disabled) } end + + it_behaves_like 'project emails are disabled' do + let(:notification_target) { merge_request } + let(:notification_trigger) { notification.push_to_merge_request(merge_request, @u_disabled) } + end end describe '#relabel_merge_request' do @@ -1512,28 +1611,43 @@ describe NotificationService, :mailer do should_not_email(@u_participating) should_not_email(@u_lazy_participant) end + + it_behaves_like 'project emails are disabled' do + let(:notification_target) { merge_request } + let(:notification_trigger) { notification.relabeled_merge_request(merge_request, [group_label_2, label_2], @u_disabled) } + end end describe '#removed_milestone_merge_request' do - it_behaves_like 'altered milestone notification on merge request' do - let(:milestone) { create(:milestone, project: project, merge_requests: [merge_request]) } - let!(:subscriber_to_new_milestone) { create(:user) { |u| merge_request.toggle_subscription(u, project) } } + let(:milestone) { create(:milestone, project: project, merge_requests: [merge_request]) } + let!(:subscriber_to_new_milestone) { create(:user) { |u| merge_request.toggle_subscription(u, project) } } + it_behaves_like 'altered milestone notification on merge request' do before do notification.removed_milestone_merge_request(merge_request, merge_request.author) end end + + it_behaves_like 'project emails are disabled' do + let(:notification_target) { merge_request } + let(:notification_trigger) { notification.removed_milestone_merge_request(merge_request, merge_request.author) } + end end describe '#changed_milestone_merge_request' do - it_behaves_like 'altered milestone notification on merge request' do - let(:new_milestone) { create(:milestone, project: project, merge_requests: [merge_request]) } - let!(:subscriber_to_new_milestone) { create(:user) { |u| merge_request.toggle_subscription(u, project) } } + let(:new_milestone) { create(:milestone, project: project, merge_requests: [merge_request]) } + let!(:subscriber_to_new_milestone) { create(:user) { |u| merge_request.toggle_subscription(u, project) } } + it_behaves_like 'altered milestone notification on merge request' do before do notification.changed_milestone_merge_request(merge_request, new_milestone, merge_request.author) end end + + it_behaves_like 'project emails are disabled' do + let(:notification_target) { merge_request } + let(:notification_trigger) { notification.changed_milestone_merge_request(merge_request, new_milestone, merge_request.author) } + end end describe '#merge_request_unmergeable' do @@ -1544,6 +1658,11 @@ describe NotificationService, :mailer do expect(email_recipients.size).to eq(1) end + it_behaves_like 'project emails are disabled' do + let(:notification_target) { merge_request } + let(:notification_trigger) { notification.merge_request_unmergeable(merge_request) } + end + describe 'when merge_when_pipeline_succeeds is true' do before do merge_request.update( @@ -1590,6 +1709,11 @@ describe NotificationService, :mailer do let(:issuable) { merge_request } let(:notification_trigger) { notification.close_mr(merge_request, @u_disabled) } end + + it_behaves_like 'project emails are disabled' do + let(:notification_target) { merge_request } + let(:notification_trigger) { notification.close_mr(merge_request, @u_disabled) } + end end describe '#merged_merge_request' do @@ -1642,6 +1766,11 @@ describe NotificationService, :mailer do let(:issuable) { merge_request } let(:notification_trigger) { notification.merge_mr(merge_request, @u_disabled) } end + + it_behaves_like 'project emails are disabled' do + let(:notification_target) { merge_request } + let(:notification_trigger) { notification.merge_mr(merge_request, @u_disabled) } + end end describe '#reopen_merge_request' do @@ -1672,6 +1801,11 @@ describe NotificationService, :mailer do let(:issuable) { merge_request } let(:notification_trigger) { notification.reopen_mr(merge_request, @u_disabled) } end + + it_behaves_like 'project emails are disabled' do + let(:notification_target) { merge_request } + let(:notification_trigger) { notification.reopen_mr(merge_request, @u_disabled) } + end end describe "#resolve_all_discussions" do @@ -1695,6 +1829,11 @@ describe NotificationService, :mailer do let(:issuable) { merge_request } let(:notification_trigger) { notification.resolve_all_discussions(merge_request, @u_disabled) } end + + it_behaves_like 'project emails are disabled' do + let(:notification_target) { merge_request } + let(:notification_trigger) { notification.resolve_all_discussions(merge_request, @u_disabled) } + end end end @@ -1719,6 +1858,11 @@ describe NotificationService, :mailer do should_not_email(@u_disabled) end + it_behaves_like 'project emails are disabled' do + let(:notification_target) { project } + let(:notification_trigger) { notification.project_was_moved(project, "gitlab/gitlab") } + end + context 'users not having access to the new location' do it 'does not send email' do old_user = create(:user) @@ -1762,6 +1906,11 @@ describe NotificationService, :mailer do should_only_email(@u_participating) end + + it_behaves_like 'project emails are disabled' do + let(:notification_target) { project } + let(:notification_trigger) { notification.project_exported(project, @u_participating) } + end end describe '#project_not_exported' do @@ -1770,6 +1919,11 @@ describe NotificationService, :mailer do should_only_email(@u_participating) end + + it_behaves_like 'project emails are disabled' do + let(:notification_target) { project } + let(:notification_trigger) { notification.project_not_exported(project, @u_participating, ['error']) } + end end end end @@ -1800,6 +1954,11 @@ describe NotificationService, :mailer do should_email(maintainer) should_not_email(developer) end + + it_behaves_like 'group emails are disabled' do + let(:notification_target) { group } + let(:notification_trigger) { group.request_access(added_user) } + end end describe '#decline_group_invite' do @@ -1839,6 +1998,11 @@ describe NotificationService, :mailer do should_not_email_anyone end end + + it_behaves_like 'group emails are disabled' do + let(:notification_target) { group } + let(:notification_trigger) { group.add_guest(added_user) } + end end end @@ -1859,6 +2023,11 @@ describe NotificationService, :mailer do should_only_email(project.owner) end + + it_behaves_like 'project emails are disabled' do + let(:notification_target) { project } + let(:notification_trigger) { project.request_access(added_user) } + end end context 'for a project in a group' do @@ -1878,7 +2047,7 @@ describe NotificationService, :mailer do end end - describe '#decline_group_invite' do + describe '#decline_project_invite' do let(:member) { create(:user) } before do @@ -1900,6 +2069,11 @@ describe NotificationService, :mailer do should_only_email(added_user) end + it_behaves_like 'project emails are disabled' do + let(:notification_target) { project } + let(:notification_trigger) { create_member! } + end + context 'when notifications are disabled' do before do create_global_setting_for(added_user, :disabled) @@ -2071,6 +2245,11 @@ describe NotificationService, :mailer do should_only_email(u_custom_notification_enabled, kind: :bcc) end + it_behaves_like 'project emails are disabled' do + let(:notification_target) { pipeline } + let(:notification_trigger) { notification.pipeline_finished(pipeline) } + end + context 'when the creator has group notification email set' do let(:group_notification_email) { 'user+group@example.com' } @@ -2100,6 +2279,11 @@ describe NotificationService, :mailer do should_only_email(u_member, kind: :bcc) end + it_behaves_like 'project emails are disabled' do + let(:notification_target) { pipeline } + let(:notification_trigger) { notification.pipeline_finished(pipeline) } + end + context 'when the creator has group notification email set' do let(:group_notification_email) { 'user+group@example.com' } @@ -2215,6 +2399,11 @@ describe NotificationService, :mailer do should_only_email(u_maintainer1, u_maintainer2, u_owner) end + it_behaves_like 'project emails are disabled' do + let(:notification_target) { domain } + let(:notification_trigger) { notify! } + end + it 'emails nobody if the project is missing' do domain.project = nil @@ -2224,30 +2413,6 @@ describe NotificationService, :mailer do end end end - - describe '#pages_domain_verification_failed' do - it 'emails current watching maintainers' do - notification.pages_domain_verification_failed(domain) - - should_only_email(u_maintainer1, u_maintainer2, u_owner) - end - end - - describe '#pages_domain_enabled' do - it 'emails current watching maintainers' do - notification.pages_domain_enabled(domain) - - should_only_email(u_maintainer1, u_maintainer2, u_owner) - end - end - - describe '#pages_domain_disabled' do - it 'emails current watching maintainers' do - notification.pages_domain_disabled(domain) - - should_only_email(u_maintainer1, u_maintainer2, u_owner) - end - end end context 'Auto DevOps notifications' do @@ -2266,6 +2431,11 @@ describe NotificationService, :mailer do should_email(owner, times: 1) # Once for the disable pipeline. should_email(pipeline_user, times: 2) # Once for the new permission, once for the disable. end + + it_behaves_like 'project emails are disabled' do + let(:notification_target) { project } + let(:notification_trigger) { notification.autodevops_disabled(pipeline, [owner.email, pipeline_user.email]) } + end end end @@ -2279,6 +2449,11 @@ describe NotificationService, :mailer do should_email(user) end + + it_behaves_like 'project emails are disabled' do + let(:notification_target) { project } + let(:notification_trigger) { notification.repository_cleanup_success(project, user) } + end end describe '#repository_cleanup_failure' do @@ -2287,6 +2462,11 @@ describe NotificationService, :mailer do should_email(user) end + + it_behaves_like 'project emails are disabled' do + let(:notification_target) { project } + let(:notification_trigger) { notification.repository_cleanup_failure(project, user, 'Some error') } + end end end @@ -2320,6 +2500,11 @@ describe NotificationService, :mailer do should_only_email(u_maintainer1, u_maintainer2, u_owner) end + + it_behaves_like 'project emails are disabled' do + let(:notification_target) { project } + let(:notification_trigger) { notification.remote_mirror_update_failed(remote_mirror) } + end end end diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb index b0b74407812..8178b7d2ba2 100644 --- a/spec/services/projects/create_service_spec.rb +++ b/spec/services/projects/create_service_spec.rb @@ -78,6 +78,7 @@ describe Projects::CreateService, '#execute' do expect(project).to be_valid expect(project.owner).to eq(group) expect(project.namespace).to eq(group) + expect(project.team.owners).to include(user) expect(user.authorized_projects).to include(project) end end @@ -182,27 +183,65 @@ describe Projects::CreateService, '#execute' do context 'restricted visibility level' do before do stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC]) + end - opts.merge!( - visibility_level: Gitlab::VisibilityLevel::PUBLIC - ) + shared_examples 'restricted visibility' do + it 'does not allow a restricted visibility level for non-admins' do + project = create_project(user, opts) + + expect(project).to respond_to(:errors) + expect(project.errors.messages).to have_key(:visibility_level) + expect(project.errors.messages[:visibility_level].first).to( + match('restricted by your GitLab administrator') + ) + end + + it 'allows a restricted visibility level for admins' do + admin = create(:admin) + project = create_project(admin, opts) + + expect(project.errors.any?).to be(false) + expect(project.saved?).to be(true) + end end - it 'does not allow a restricted visibility level for non-admins' do - project = create_project(user, opts) - expect(project).to respond_to(:errors) - expect(project.errors.messages).to have_key(:visibility_level) - expect(project.errors.messages[:visibility_level].first).to( - match('restricted by your GitLab administrator') - ) + context 'when visibility is project based' do + before do + opts.merge!( + visibility_level: Gitlab::VisibilityLevel::PUBLIC + ) + end + + include_examples 'restricted visibility' end - it 'allows a restricted visibility level for admins' do - admin = create(:admin) - project = create_project(admin, opts) + context 'when visibility is overridden' do + let(:visibility) { 'public' } - expect(project.errors.any?).to be(false) - expect(project.saved?).to be(true) + before do + opts.merge!( + import_data: { + data: { + override_params: { + visibility: visibility + } + } + } + ) + end + + include_examples 'restricted visibility' + + context 'when visibility is misspelled' do + let(:visibility) { 'publik' } + + it 'does not restrict project creation' do + project = create_project(user, opts) + + expect(project.errors.any?).to be(false) + expect(project.saved?).to be(true) + end + end end end diff --git a/spec/services/projects/update_remote_mirror_service_spec.rb b/spec/services/projects/update_remote_mirror_service_spec.rb index be2811ab1e7..4396ccab584 100644 --- a/spec/services/projects/update_remote_mirror_service_spec.rb +++ b/spec/services/projects/update_remote_mirror_service_spec.rb @@ -10,49 +10,91 @@ describe Projects::UpdateRemoteMirrorService do subject(:service) { described_class.new(project, project.creator) } - describe "#execute" do + describe '#execute' do + subject(:execute!) { service.execute(remote_mirror, 0) } + before do project.repository.add_branch(project.owner, 'existing-branch', 'master') allow(remote_mirror).to receive(:update_repository).and_return(true) end - it "ensures the remote exists" do + it 'ensures the remote exists' do stub_fetch_remote(project, remote_name: remote_name, ssh_auth: remote_mirror) expect(remote_mirror).to receive(:ensure_remote!) - service.execute(remote_mirror) + execute! end - it "fetches the remote repository" do + it 'fetches the remote repository' do expect(project.repository) .to receive(:fetch_remote) - .with(remote_mirror.remote_name, no_tags: true, ssh_auth: remote_mirror) + .with(remote_mirror.remote_name, no_tags: true, ssh_auth: remote_mirror) - service.execute(remote_mirror) + execute! end - it "returns success when updated succeeds" do + it 'marks the mirror as started when beginning' do + expect(remote_mirror).to receive(:update_start!).and_call_original + + execute! + end + + it 'marks the mirror as successfully finished' do stub_fetch_remote(project, remote_name: remote_name, ssh_auth: remote_mirror) - result = service.execute(remote_mirror) + result = execute! expect(result[:status]).to eq(:success) + expect(remote_mirror).to be_finished + end + + it 'marks the mirror as failed and raises the error when an unexpected error occurs' do + allow(project.repository).to receive(:fetch_remote).and_raise('Badly broken') + + expect { execute! }.to raise_error /Badly broken/ + + expect(remote_mirror).to be_failed + expect(remote_mirror.last_error).to include('Badly broken') + end + + context 'when the update fails because of a `Gitlab::Git::CommandError`' do + before do + allow(project.repository).to receive(:fetch_remote).and_raise(Gitlab::Git::CommandError.new('fetch failed')) + end + + it 'wraps `Gitlab::Git::CommandError`s in a service error' do + expect(execute!).to eq(status: :error, message: 'fetch failed') + end + + it 'marks the mirror as to be retried' do + execute! + + expect(remote_mirror).to be_to_retry + expect(remote_mirror.last_error).to include('fetch failed') + end + + it "marks the mirror as failed after #{described_class::MAX_TRIES} tries" do + service.execute(remote_mirror, described_class::MAX_TRIES) + + expect(remote_mirror).to be_failed + expect(remote_mirror.last_error).to include('fetch failed') + end end context 'when syncing all branches' do - it "push all the branches the first time" do + it 'push all the branches the first time' do stub_fetch_remote(project, remote_name: remote_name, ssh_auth: remote_mirror) expect(remote_mirror).to receive(:update_repository).with({}) - service.execute(remote_mirror) + execute! end end context 'when only syncing protected branches' do - it "sync updated protected branches" do + it 'sync updated protected branches' do stub_fetch_remote(project, remote_name: remote_name, ssh_auth: remote_mirror) protected_branch = create_protected_branch(project) remote_mirror.only_protected_branches = true @@ -61,7 +103,7 @@ describe Projects::UpdateRemoteMirrorService do .to receive(:update_repository) .with(only_branches_matching: [protected_branch.name]) - service.execute(remote_mirror) + execute! end def create_protected_branch(project) diff --git a/spec/services/projects/update_service_spec.rb b/spec/services/projects/update_service_spec.rb index 82010dd283c..31bd0f0f836 100644 --- a/spec/services/projects/update_service_spec.rb +++ b/spec/services/projects/update_service_spec.rb @@ -369,9 +369,28 @@ describe Projects::UpdateService do end end + context 'when updating #emails_disabled' do + it 'updates the attribute for the project owner' do + expect { update_project(project, user, emails_disabled: true) } + .to change { project.emails_disabled } + .to(true) + end + + it 'does not update when not project owner' do + maintainer = create(:user) + project.add_user(maintainer, :maintainer) + + expect { update_project(project, maintainer, emails_disabled: true) } + .not_to change { project.emails_disabled } + end + end + context 'with external authorization enabled' do before do enable_external_authorization_service_check + + allow(::Gitlab::ExternalAuthorization) + .to receive(:access_allowed?).with(user, 'default_label', project.full_path).and_call_original end it 'does not save the project with an error if the service denies access' do @@ -402,8 +421,7 @@ describe Projects::UpdateService do end it 'does not check the label when it does not change' do - expect(::Gitlab::ExternalAuthorization) - .not_to receive(:access_allowed?) + expect(::Gitlab::ExternalAuthorization).to receive(:access_allowed?).once update_project(project, user, { name: 'New name' }) end diff --git a/spec/services/quick_actions/interpret_service_spec.rb b/spec/services/quick_actions/interpret_service_spec.rb index bf5f211b11c..c9714964fc9 100644 --- a/spec/services/quick_actions/interpret_service_spec.rb +++ b/spec/services/quick_actions/interpret_service_spec.rb @@ -226,7 +226,7 @@ describe QuickActions::InterpretService do it 'returns the todo message' do _, _, message = service.execute(content, issuable) - expect(message).to eq('Added a todo.') + expect(message).to eq('Added a To Do.') end end @@ -242,7 +242,7 @@ describe QuickActions::InterpretService do TodoService.new.mark_todo(issuable, developer) _, _, message = service.execute(content, issuable) - expect(message).to eq('Marked to do as done.') + expect(message).to eq('Marked To Do as done.') end end @@ -453,7 +453,7 @@ describe QuickActions::InterpretService do it 'returns the lock discussion message' do _, _, message = service.execute(content, issuable) - expect(message).to eq('Locked the discussion') + expect(message).to eq('Locked the discussion.') end end @@ -470,7 +470,7 @@ describe QuickActions::InterpretService do it 'returns the unlock discussion message' do _, _, message = service.execute(content, issuable) - expect(message).to eq('Unlocked the discussion') + expect(message).to eq('Unlocked the discussion.') end end @@ -570,7 +570,7 @@ describe QuickActions::InterpretService do it 'returns move issue failure message when the referenced issue is not found' do _, _, message = service.execute('/move invalid', issue) - expect(message).to eq("Move this issue failed because target project doesn't exists") + expect(message).to eq("Failed to move this issue because target project doesn't exist.") end end @@ -584,7 +584,7 @@ describe QuickActions::InterpretService do it 'returns the confidential message' do _, _, message = service.execute(content, issuable) - expect(message).to eq('Made this issue confidential') + expect(message).to eq('Made this issue confidential.') end end @@ -783,7 +783,7 @@ describe QuickActions::InterpretService do end end - it_behaves_like 'empty command', "Assign command failed because no user was found" do + it_behaves_like 'empty command', "Failed to assign a user because no user was found." do let(:content) { '/assign @abcd1234' } let(:issuable) { issue } end @@ -1217,12 +1217,12 @@ describe QuickActions::InterpretService do let(:issuable) { issue } end - it_behaves_like 'empty command', 'Mark as duplicate failed because referenced issue was not found' do + it_behaves_like 'empty command', 'Failed to mark this issue as a duplicate because referenced issue was not found.' do let(:content) { "/duplicate imaginary#1234" } let(:issuable) { issue } end - it_behaves_like 'empty command', 'Mark as duplicate failed because referenced issue was not found' do + it_behaves_like 'empty command', 'Failed to mark this issue as a duplicate because referenced issue was not found.' do let(:other_project) { create(:project, :private) } let(:issue_duplicate) { create(:issue, project: other_project) } @@ -1287,7 +1287,7 @@ describe QuickActions::InterpretService do let(:issuable) { issue } end - it_behaves_like 'empty command', 'Mark as duplicate failed because referenced issue was not found' do + it_behaves_like 'empty command', 'Failed to mark this issue as a duplicate because referenced issue was not found.' do let(:content) { '/duplicate #{issue.to_reference}' } let(:issuable) { issue } end @@ -1463,19 +1463,19 @@ describe QuickActions::InterpretService do context 'if the given label does not exist' do let(:issuable) { issue } let(:content) { '/board_move ~"Fake Label"' } - it_behaves_like 'empty command', 'Move this issue failed because you need to specify only one label.' + it_behaves_like 'empty command', 'Failed to move this issue because label was not found.' end context 'if multiple labels are given' do let(:issuable) { issue } let(:content) { %{/board_move ~"#{inreview.title}" ~"#{todo.title}"} } - it_behaves_like 'empty command', 'Move this issue failed because you need to specify only one label.' + it_behaves_like 'empty command', 'Failed to move this issue because only a single label can be provided.' end context 'if the given label is not a list on the board' do let(:issuable) { issue } let(:content) { %{/board_move ~"#{bug.title}"} } - it_behaves_like 'empty command' + it_behaves_like 'empty command', 'Failed to move this issue because label was not found.' end context 'if issuable is not an Issue' do @@ -1552,7 +1552,7 @@ describe QuickActions::InterpretService do it 'returns the create_merge_request message' do _, _, message = service.execute(content, issuable) - expect(message).to eq("Created branch '#{branch_name}' and a merge request to resolve this issue") + expect(message).to eq("Created branch '#{branch_name}' and a merge request to resolve this issue.") end end end @@ -1814,13 +1814,13 @@ describe QuickActions::InterpretService do it 'uses the default branch name' do _, explanations = service.explain(content, issue) - expect(explanations).to eq(['Creates a branch and a merge request to resolve this issue']) + expect(explanations).to eq(['Creates a branch and a merge request to resolve this issue.']) end it 'returns the execution message using the default branch name' do _, _, message = service.execute(content, issue) - expect(message).to eq('Created a branch and a merge request to resolve this issue') + expect(message).to eq('Created a branch and a merge request to resolve this issue.') end end @@ -1830,13 +1830,13 @@ describe QuickActions::InterpretService do it 'uses the given branch name' do _, explanations = service.explain(content, issue) - expect(explanations).to eq(["Creates branch 'foo' and a merge request to resolve this issue"]) + expect(explanations).to eq(["Creates branch 'foo' and a merge request to resolve this issue."]) end it 'returns the execution message using the given branch name' do _, _, message = service.execute(content, issue) - expect(message).to eq("Created branch 'foo' and a merge request to resolve this issue") + expect(message).to eq("Created branch 'foo' and a merge request to resolve this issue.") end end end diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb index 486d0ca0c56..f46f9633c1c 100644 --- a/spec/services/system_note_service_spec.rb +++ b/spec/services/system_note_service_spec.rb @@ -521,7 +521,7 @@ describe SystemNoteService do end it 'sets the zoom link added note text' do - expect(subject.note).to eq('a Zoom call was added to this issue') + expect(subject.note).to eq('added a Zoom call to this issue') end end @@ -533,7 +533,7 @@ describe SystemNoteService do end it 'sets the zoom link removed note text' do - expect(subject.note).to eq('a Zoom call was removed from this issue') + expect(subject.note).to eq('removed a Zoom call from this issue') end end diff --git a/spec/services/todo_service_spec.rb b/spec/services/todo_service_spec.rb index 9ee23f3eb48..bdf2f59704c 100644 --- a/spec/services/todo_service_spec.rb +++ b/spec/services/todo_service_spec.rb @@ -436,25 +436,114 @@ describe TodoService do should_create_todo(user: john_doe, target: confidential_issue, author: john_doe, action: Todo::DIRECTLY_ADDRESSED, note: addressed_note_on_confidential_issue) end - context 'on commit' do - 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) - - should_create_todo(user: member, target_id: nil, target_type: 'Commit', commit_id: note_on_commit.commit_id, author: john_doe, action: Todo::MENTIONED, note: note_on_commit) - should_create_todo(user: author, target_id: nil, target_type: 'Commit', commit_id: note_on_commit.commit_id, author: john_doe, action: Todo::MENTIONED, note: note_on_commit) - should_create_todo(user: john_doe, target_id: nil, target_type: 'Commit', commit_id: note_on_commit.commit_id, author: john_doe, action: Todo::MENTIONED, note: note_on_commit) - should_not_create_todo(user: non_member, target_id: nil, target_type: 'Commit', commit_id: note_on_commit.commit_id, author: john_doe, action: Todo::MENTIONED, note: note_on_commit) + context 'commits' do + let(:base_commit_todo_attrs) { { target_id: nil, target_type: 'Commit', author: john_doe } } + + context 'leaving a note on a commit in a public project' do + let(:project) { create(:project, :repository, :public) } + it 'creates a todo for each valid mentioned user' do + expected_todo = base_commit_todo_attrs.merge( + action: Todo::MENTIONED, + note: note_on_commit, + commit_id: note_on_commit.commit_id + ) + + service.new_note(note_on_commit, john_doe) + + should_create_todo(expected_todo.merge(user: member)) + should_create_todo(expected_todo.merge(user: author)) + should_create_todo(expected_todo.merge(user: john_doe)) + should_create_todo(expected_todo.merge(user: guest)) + should_create_todo(expected_todo.merge(user: non_member)) + end + + it 'creates a directly addressed todo for each valid mentioned user' do + expected_todo = base_commit_todo_attrs.merge( + action: Todo::DIRECTLY_ADDRESSED, + note: addressed_note_on_commit, + commit_id: addressed_note_on_commit.commit_id + ) + + service.new_note(addressed_note_on_commit, john_doe) + + should_create_todo(expected_todo.merge(user: member)) + should_create_todo(expected_todo.merge(user: author)) + should_create_todo(expected_todo.merge(user: john_doe)) + should_create_todo(expected_todo.merge(user: guest)) + should_create_todo(expected_todo.merge(user: non_member)) + end end - it 'creates a directly addressed todo for each valid mentioned user when leaving a note on commit' do - service.new_note(addressed_note_on_commit, john_doe) + context 'leaving a note on a commit in a public project with private code' do + let(:project) { create(:project, :repository, :public, :repository_private) } + + it 'creates a todo for each valid mentioned user' do + expected_todo = base_commit_todo_attrs.merge( + action: Todo::MENTIONED, + note: note_on_commit, + commit_id: note_on_commit.commit_id + ) + + service.new_note(note_on_commit, john_doe) + + should_create_todo(expected_todo.merge(user: member)) + should_create_todo(expected_todo.merge(user: author)) + should_create_todo(expected_todo.merge(user: john_doe)) + should_create_todo(expected_todo.merge(user: guest)) + should_not_create_todo(expected_todo.merge(user: non_member)) + end + + it 'creates a directly addressed todo for each valid mentioned user' do + expected_todo = base_commit_todo_attrs.merge( + action: Todo::DIRECTLY_ADDRESSED, + note: addressed_note_on_commit, + commit_id: addressed_note_on_commit.commit_id + ) + + service.new_note(addressed_note_on_commit, john_doe) + + should_create_todo(expected_todo.merge(user: member)) + should_create_todo(expected_todo.merge(user: author)) + should_create_todo(expected_todo.merge(user: john_doe)) + should_create_todo(expected_todo.merge(user: guest)) + should_not_create_todo(expected_todo.merge(user: non_member)) + end + end - should_create_todo(user: member, target_id: nil, target_type: 'Commit', commit_id: addressed_note_on_commit.commit_id, author: john_doe, action: Todo::DIRECTLY_ADDRESSED, note: addressed_note_on_commit) - should_create_todo(user: author, target_id: nil, target_type: 'Commit', commit_id: addressed_note_on_commit.commit_id, author: john_doe, action: Todo::DIRECTLY_ADDRESSED, note: addressed_note_on_commit) - should_create_todo(user: john_doe, target_id: nil, target_type: 'Commit', commit_id: addressed_note_on_commit.commit_id, author: john_doe, action: Todo::DIRECTLY_ADDRESSED, note: addressed_note_on_commit) - should_not_create_todo(user: non_member, target_id: nil, target_type: 'Commit', commit_id: addressed_note_on_commit.commit_id, author: john_doe, action: Todo::DIRECTLY_ADDRESSED, note: addressed_note_on_commit) + context 'leaving a note on a commit in a private project' do + let(:project) { create(:project, :repository, :private) } + + it 'creates a todo for each valid mentioned user' do + expected_todo = base_commit_todo_attrs.merge( + action: Todo::MENTIONED, + note: note_on_commit, + commit_id: note_on_commit.commit_id + ) + + service.new_note(note_on_commit, john_doe) + + should_create_todo(expected_todo.merge(user: member)) + should_create_todo(expected_todo.merge(user: author)) + should_create_todo(expected_todo.merge(user: john_doe)) + should_not_create_todo(expected_todo.merge(user: guest)) + should_not_create_todo(expected_todo.merge(user: non_member)) + end + + it 'creates a directly addressed todo for each valid mentioned user' do + expected_todo = base_commit_todo_attrs.merge( + action: Todo::DIRECTLY_ADDRESSED, + note: addressed_note_on_commit, + commit_id: addressed_note_on_commit.commit_id + ) + + service.new_note(addressed_note_on_commit, john_doe) + + should_create_todo(expected_todo.merge(user: member)) + should_create_todo(expected_todo.merge(user: author)) + should_create_todo(expected_todo.merge(user: john_doe)) + should_not_create_todo(expected_todo.merge(user: guest)) + should_not_create_todo(expected_todo.merge(user: non_member)) + end end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index bcc133790d1..bd504f1553b 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -48,6 +48,9 @@ Dir[Rails.root.join("spec/support/**/*.rb")].each { |f| require f } quality_level = Quality::TestLevel.new RSpec.configure do |config| + config.filter_run focus: true + config.run_all_when_everything_filtered = true + config.use_transactional_fixtures = true config.use_instantiated_fixtures = false config.fixture_path = Rails.root diff --git a/spec/support/capybara.rb b/spec/support/capybara.rb index 8accc5c1df5..4c688094352 100644 --- a/spec/support/capybara.rb +++ b/spec/support/capybara.rb @@ -47,6 +47,9 @@ Capybara.register_driver :chrome do |app| # Explicitly set user-data-dir to prevent crashes. See https://gitlab.com/gitlab-org/gitlab-ce/issues/58882#note_179811508 options.add_argument("user-data-dir=/tmp/chrome") if ENV['CI'] || ENV['CI_SERVER'] + # Chrome 75 defaults to W3C mode which doesn't allow console log access + options.add_option(:w3c, false) + Capybara::Selenium::Driver.new( app, browser: :chrome, diff --git a/spec/support/helpers/drag_to_helper.rb b/spec/support/helpers/drag_to_helper.rb index 6099f87323f..2e9932f2e8a 100644 --- a/spec/support/helpers/drag_to_helper.rb +++ b/spec/support/helpers/drag_to_helper.rb @@ -1,8 +1,23 @@ # frozen_string_literal: true module DragTo - def drag_to(list_from_index: 0, from_index: 0, to_index: 0, list_to_index: 0, selector: '', scrollable: 'body', duration: 1000) - evaluate_script("simulateDrag({scrollable: $('#{scrollable}').get(0), duration: #{duration}, from: {el: $('#{selector}').eq(#{list_from_index}).get(0), index: #{from_index}}, to: {el: $('#{selector}').eq(#{list_to_index}).get(0), index: #{to_index}}});") + def drag_to(list_from_index: 0, from_index: 0, to_index: 0, list_to_index: 0, selector: '', scrollable: 'body', duration: 1000, perform_drop: true) + js = <<~JS + simulateDrag({ + scrollable: document.querySelector('#{scrollable}'), + duration: #{duration}, + from: { + el: document.querySelectorAll('#{selector}')[#{list_from_index}], + index: #{from_index} + }, + to: { + el: document.querySelectorAll('#{selector}')[#{list_to_index}], + index: #{to_index} + }, + performDrop: #{perform_drop} + }); + JS + evaluate_script(js) Timeout.timeout(Capybara.default_max_wait_time) do loop while drag_active? diff --git a/spec/support/helpers/email_helpers.rb b/spec/support/helpers/email_helpers.rb index 83ba654fab3..024340310a1 100644 --- a/spec/support/helpers/email_helpers.rb +++ b/spec/support/helpers/email_helpers.rb @@ -31,6 +31,10 @@ module EmailHelpers expect(ActionMailer::Base.deliveries).to be_empty end + def should_email_anyone + expect(ActionMailer::Base.deliveries).not_to be_empty + end + def email_recipients(kind: :to) ActionMailer::Base.deliveries.flat_map(&kind) end diff --git a/spec/support/helpers/smime_helper.rb b/spec/support/helpers/smime_helper.rb new file mode 100644 index 00000000000..656b3e196ba --- /dev/null +++ b/spec/support/helpers/smime_helper.rb @@ -0,0 +1,55 @@ +module SmimeHelper + include OpenSSL + + INFINITE_EXPIRY = 1000.years + SHORT_EXPIRY = 30.minutes + + def generate_root + issue(signed_by: nil, expires_in: INFINITE_EXPIRY, certificate_authority: true) + end + + def generate_cert(root_ca:, expires_in: SHORT_EXPIRY) + issue(signed_by: root_ca, expires_in: expires_in, certificate_authority: false) + end + + # returns a hash { key:, cert: } containing a generated key, cert pair + def issue(email_address: 'test@example.com', signed_by:, expires_in:, certificate_authority:) + key = OpenSSL::PKey::RSA.new(4096) + public_key = key.public_key + + subject = if certificate_authority + X509::Name.parse("/CN=EU") + else + X509::Name.parse("/CN=#{email_address}") + end + + cert = X509::Certificate.new + cert.subject = subject + + cert.issuer = signed_by&.fetch(:cert, nil)&.subject || subject + + cert.not_before = Time.now + cert.not_after = expires_in.from_now + cert.public_key = public_key + cert.serial = 0x0 + cert.version = 2 + + extension_factory = X509::ExtensionFactory.new + if certificate_authority + extension_factory.subject_certificate = cert + extension_factory.issuer_certificate = cert + cert.add_extension(extension_factory.create_extension('subjectKeyIdentifier', 'hash')) + cert.add_extension(extension_factory.create_extension('basicConstraints', 'CA:TRUE', true)) + cert.add_extension(extension_factory.create_extension('keyUsage', 'cRLSign,keyCertSign', true)) + else + cert.add_extension(extension_factory.create_extension('subjectAltName', "email:#{email_address}", false)) + cert.add_extension(extension_factory.create_extension('basicConstraints', 'CA:FALSE', true)) + cert.add_extension(extension_factory.create_extension('keyUsage', 'digitalSignature,keyEncipherment', true)) + cert.add_extension(extension_factory.create_extension('extendedKeyUsage', 'clientAuth,emailProtection', false)) + end + + cert.sign(signed_by&.fetch(:key, nil) || key, Digest::SHA256.new) + + { key: key, cert: cert } + end +end diff --git a/spec/support/helpers/stub_configuration.rb b/spec/support/helpers/stub_configuration.rb index c8b2bf040e6..dec7898d8d2 100644 --- a/spec/support/helpers/stub_configuration.rb +++ b/spec/support/helpers/stub_configuration.rb @@ -30,6 +30,10 @@ module StubConfiguration allow(Gitlab.config.gitlab).to receive_messages(to_settings(messages)) end + def stub_config(messages) + allow(Gitlab.config).to receive_messages(to_settings(messages)) + end + def stub_default_url_options(host: "localhost", protocol: "http") url_options = { host: host, protocol: protocol } allow(Rails.application.routes).to receive(:default_url_options).and_return(url_options) diff --git a/spec/support/helpers/stub_gitlab_calls.rb b/spec/support/helpers/stub_gitlab_calls.rb index badea94352a..7d10cffe920 100644 --- a/spec/support/helpers/stub_gitlab_calls.rb +++ b/spec/support/helpers/stub_gitlab_calls.rb @@ -22,6 +22,10 @@ module StubGitlabCalls allow_any_instance_of(Ci::Pipeline).to receive(:ci_yaml_file) { ci_yaml } end + def stub_pipeline_modified_paths(pipeline, modified_paths) + allow(pipeline).to receive(:modified_paths).and_return(modified_paths) + end + def stub_repository_ci_yaml_file(sha:, path: '.gitlab-ci.yml') allow_any_instance_of(Repository) .to receive(:gitlab_ci_yml_for).with(sha, path) diff --git a/spec/support/shared_contexts/policies/group_policy_shared_context.rb b/spec/support/shared_contexts/policies/group_policy_shared_context.rb index fd24c443288..b89723b1e1a 100644 --- a/spec/support/shared_contexts/policies/group_policy_shared_context.rb +++ b/spec/support/shared_contexts/policies/group_policy_shared_context.rb @@ -31,7 +31,8 @@ RSpec.shared_context 'GroupPolicy context' do :admin_group_member, :change_visibility_level, :set_note_created_at, - :create_subgroup + :create_subgroup, + :read_statistics ].compact end diff --git a/spec/support/shared_examples/award_emoji_todo_shared_examples.rb b/spec/support/shared_examples/award_emoji_todo_shared_examples.rb new file mode 100644 index 00000000000..88ad37d232f --- /dev/null +++ b/spec/support/shared_examples/award_emoji_todo_shared_examples.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +# Shared examples to that test code that creates AwardEmoji also mark Todos +# as done. +# +# The examples expect these to be defined in the calling spec: +# - `subject` the callable code that executes the creation of an AwardEmoji +# - `user` +# - `project` +RSpec.shared_examples 'creating award emojis marks Todos as done' do + using RSpec::Parameterized::TableSyntax + + before do + project.add_developer(user) + end + + where(:type, :expectation) do + :issue | true + :merge_request | true + :project_snippet | false + end + + with_them do + let(:project) { awardable.project } + let(:awardable) { create(type) } + let!(:todo) { create(:todo, target: awardable, project: project, user: user) } + + it do + subject + + expect(todo.reload.done?).to eq(expectation) + end + end + + # Notes have more complicated rules than other Todoables + describe 'for notes' do + let!(:todo) { create(:todo, target: awardable.noteable, project: project, user: user) } + + context 'regular Notes' do + let(:awardable) { create(:note, project: project) } + + it 'marks the Todo as done' do + subject + + expect(todo.reload.done?).to eq(true) + end + end + + context 'PersonalSnippet Notes' do + let(:awardable) { create(:note, noteable: create(:personal_snippet, author: user)) } + + it 'does not mark the Todo as done' do + subject + + expect(todo.reload.done?).to eq(false) + end + end + end +end diff --git a/spec/support/shared_examples/boards/multiple_issue_boards_shared_examples.rb b/spec/support/shared_examples/boards/multiple_issue_boards_shared_examples.rb new file mode 100644 index 00000000000..76d82649c5f --- /dev/null +++ b/spec/support/shared_examples/boards/multiple_issue_boards_shared_examples.rb @@ -0,0 +1,144 @@ +# frozen_string_literal: true + +shared_examples_for 'multiple issue boards' do + dropdown_selector = '.js-boards-selector .dropdown-menu' + + context 'authorized user' do + before do + parent.add_maintainer(user) + + login_as(user) + + visit boards_path + wait_for_requests + end + + it 'shows current board name' do + page.within('.boards-switcher') do + expect(page).to have_content(board.name) + end + end + + it 'shows a list of boards' do + click_button board.name + + page.within(dropdown_selector) do + expect(page).to have_content(board.name) + expect(page).to have_content(board2.name) + end + end + + it 'switches current board' do + click_button board.name + + page.within(dropdown_selector) do + click_link board2.name + end + + wait_for_requests + + page.within('.boards-switcher') do + expect(page).to have_content(board2.name) + end + end + + it 'creates new board without detailed configuration' do + click_button board.name + + page.within(dropdown_selector) do + click_button 'Create new board' + end + + fill_in 'board-new-name', with: 'This is a new board' + click_button 'Create board' + wait_for_requests + + expect(page).to have_button('This is a new board') + end + + it 'deletes board' do + click_button board.name + + wait_for_requests + + page.within(dropdown_selector) do + click_button 'Delete board' + end + + expect(page).to have_content('Are you sure you want to delete this board?') + click_button 'Delete' + + click_button board2.name + page.within(dropdown_selector) do + expect(page).not_to have_content(board.name) + expect(page).to have_content(board2.name) + end + end + + it 'adds a list to the none default board' do + click_button board.name + + page.within(dropdown_selector) do + click_link board2.name + end + + wait_for_requests + + page.within('.boards-switcher') do + expect(page).to have_content(board2.name) + end + + click_button 'Add list' + + wait_for_requests + + page.within '.dropdown-menu-issues-board-new' do + click_link planning.title + end + + wait_for_requests + + expect(page).to have_selector('.board', count: 3) + + click_button board2.name + + page.within(dropdown_selector) do + click_link board.name + end + + wait_for_requests + + expect(page).to have_selector('.board', count: 2) + end + + it 'maintains sidebar state over board switch' do + assert_boards_nav_active + + find('.boards-switcher').click + wait_for_requests + click_link board2.name + + assert_boards_nav_active + end + end + + context 'unauthorized user' do + before do + visit boards_path + wait_for_requests + end + + it 'does not show action links' do + click_button board.name + + page.within(dropdown_selector) do + expect(page).not_to have_content('Create new board') + expect(page).not_to have_content('Delete board') + end + end + end + + def assert_boards_nav_active + expect(find('.nav-sidebar .active .active')).to have_selector('a', text: 'Boards') + end +end diff --git a/spec/support/shared_examples/chat_slash_commands_shared_examples.rb b/spec/support/shared_examples/chat_slash_commands_shared_examples.rb index 82975027e5b..dcc92dda950 100644 --- a/spec/support/shared_examples/chat_slash_commands_shared_examples.rb +++ b/spec/support/shared_examples/chat_slash_commands_shared_examples.rb @@ -93,6 +93,19 @@ RSpec.shared_examples 'chat slash commands service' do subject.trigger(params) end + + context 'when user is blocked' do + before do + chat_name.user.block + end + + it 'blocks command execution' do + expect_any_instance_of(Gitlab::SlashCommands::Command).not_to receive(:execute) + + result = subject.trigger(params) + expect(result).to include(text: /^Whoops! This action is not allowed/) + end + end end end end diff --git a/spec/support/shared_examples/controllers/external_authorization_service_shared_examples.rb b/spec/support/shared_examples/controllers/external_authorization_service_shared_examples.rb index 2faa0cf8c1c..d8a1ae83f61 100644 --- a/spec/support/shared_examples/controllers/external_authorization_service_shared_examples.rb +++ b/spec/support/shared_examples/controllers/external_authorization_service_shared_examples.rb @@ -8,7 +8,7 @@ shared_examples 'disabled when using an external authorization service' do it 'works when the feature is not enabled' do subject - expect(response).to be_success + expect(response).to be_successful end it 'renders a 404 with a message when the feature is enabled' do 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 1cd14ea2251..d89eded6e69 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,14 +2,14 @@ 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, + # There is no sorting_field defined in any CE controllers yet, # however any other field present in user_preferences table can be used for testing. context 'when database is in read-only mode' do 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).with({ controller.send(:issuable_sorting_field) => sorting_param }) + expect_any_instance_of(UserPreference).not_to receive(:update).with({ controller.send(:sorting_field) => sorting_param }) get :index, params: { namespace_id: project.namespace, project_id: project, sort: sorting_param } end @@ -19,7 +19,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).with({ controller.send(:issuable_sorting_field) => sorting_param }) + expect_any_instance_of(UserPreference).to receive(:update).with({ controller.send(: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/cycle_analytics_stage_examples.rb b/spec/support/shared_examples/cycle_analytics_stage_examples.rb new file mode 100644 index 00000000000..151f5325e84 --- /dev/null +++ b/spec/support/shared_examples/cycle_analytics_stage_examples.rb @@ -0,0 +1,74 @@ +# frozen_string_literal: true + +shared_examples_for 'cycle analytics stage' do + let(:valid_params) do + { + name: 'My Stage', + parent: parent, + start_event_identifier: :merge_request_created, + end_event_identifier: :merge_request_merged + } + end + + describe 'validation' do + it 'is valid' do + expect(described_class.new(valid_params)).to be_valid + end + + it 'validates presence of parent' do + stage = described_class.new(valid_params.except(:parent)) + + expect(stage).not_to be_valid + expect(stage.errors.details[parent_name]).to eq([{ error: :blank }]) + end + + it 'validates presence of start_event_identifier' do + stage = described_class.new(valid_params.except(:start_event_identifier)) + + expect(stage).not_to be_valid + expect(stage.errors.details[:start_event_identifier]).to eq([{ error: :blank }]) + end + + it 'validates presence of end_event_identifier' do + stage = described_class.new(valid_params.except(:end_event_identifier)) + + expect(stage).not_to be_valid + expect(stage.errors.details[:end_event_identifier]).to eq([{ error: :blank }]) + end + + it 'is invalid when end_event is not allowed for the given start_event' do + invalid_params = valid_params.merge( + start_event_identifier: :merge_request_merged, + end_event_identifier: :merge_request_created + ) + stage = described_class.new(invalid_params) + + expect(stage).not_to be_valid + expect(stage.errors.details[:end_event]).to eq([{ error: :not_allowed_for_the_given_start_event }]) + end + end + + describe '#subject_model' do + it 'infers the model from the start event' do + stage = described_class.new(valid_params) + + expect(stage.subject_model).to eq(MergeRequest) + end + end + + describe '#start_event' do + it 'builds start_event object based on start_event_identifier' do + stage = described_class.new(start_event_identifier: 'merge_request_created') + + expect(stage.start_event).to be_a_kind_of(Gitlab::Analytics::CycleAnalytics::StageEvents::MergeRequestCreated) + end + end + + describe '#end_event' do + it 'builds end_event object based on end_event_identifier' do + stage = described_class.new(end_event_identifier: 'merge_request_merged') + + expect(stage.end_event).to be_a_kind_of(Gitlab::Analytics::CycleAnalytics::StageEvents::MergeRequestMerged) + end + end +end diff --git a/spec/support/shared_examples/lib/banzai/filters/reference_filter_shared_examples.rb b/spec/support/shared_examples/lib/banzai/filters/reference_filter_shared_examples.rb new file mode 100644 index 00000000000..b1ecd4fd007 --- /dev/null +++ b/spec/support/shared_examples/lib/banzai/filters/reference_filter_shared_examples.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'HTML text with references' do + let(:markdown_prepend) { "<img src=\"\" onerror=alert(`bug`)>" } + + it 'preserves escaped HTML text and adds valid references' do + reference = resource.to_reference(format: :name) + + doc = reference_filter("#{markdown_prepend}#{reference}") + + expect(doc.to_html).to start_with(markdown_prepend) + expect(doc.text).to eq %(<img src="" onerror=alert(`bug`)>#{resource_text}) + end + + it 'preserves escaped HTML text if there are no valid references' do + reference = "#{resource.class.reference_prefix}invalid" + text = "#{markdown_prepend}#{reference}" + + doc = reference_filter(text) + + expect(doc.to_html).to eq text + end +end diff --git a/spec/support/shared_examples/policies/clusterable_shared_examples.rb b/spec/support/shared_examples/policies/clusterable_shared_examples.rb index 4f9873d53e4..0b427c23256 100644 --- a/spec/support/shared_examples/policies/clusterable_shared_examples.rb +++ b/spec/support/shared_examples/policies/clusterable_shared_examples.rb @@ -13,7 +13,11 @@ shared_examples 'clusterable policies' do clusterable.add_developer(current_user) end + it { expect_disallowed(:read_cluster) } it { expect_disallowed(:add_cluster) } + it { expect_disallowed(:create_cluster) } + it { expect_disallowed(:update_cluster) } + it { expect_disallowed(:admin_cluster) } end context 'with a maintainer' do @@ -22,7 +26,11 @@ shared_examples 'clusterable policies' do end context 'with no clusters' do + it { expect_allowed(:read_cluster) } it { expect_allowed(:add_cluster) } + it { expect_allowed(:create_cluster) } + it { expect_allowed(:update_cluster) } + it { expect_allowed(:admin_cluster) } end end end diff --git a/spec/support/shared_examples/quick_actions/issue/move_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issue/move_quick_action_shared_examples.rb index 85682b4919d..a37b2392d52 100644 --- a/spec/support/shared_examples/quick_actions/issue/move_quick_action_shared_examples.rb +++ b/spec/support/shared_examples/quick_actions/issue/move_quick_action_shared_examples.rb @@ -40,7 +40,7 @@ shared_examples 'move quick action' do wait_for_requests - expect(page).to have_content "Move this issue failed because target project doesn't exists" + expect(page).to have_content "Failed to move this issue because target project doesn't exist." expect(issue.reload).to be_open end end diff --git a/spec/support/shared_examples/requests/api/discussions.rb b/spec/support/shared_examples/requests/api/discussions.rb index fc72287f265..a36bc2dc9b5 100644 --- a/spec/support/shared_examples/requests/api/discussions.rb +++ b/spec/support/shared_examples/requests/api/discussions.rb @@ -1,5 +1,59 @@ # frozen_string_literal: true +shared_examples 'with cross-reference system notes' do + let(:merge_request) { create(:merge_request) } + let(:project) { merge_request.project } + let(:new_merge_request) { create(:merge_request) } + let(:commit) { new_merge_request.project.commit } + let!(:note) { create(:system_note, noteable: merge_request, project: project, note: cross_reference) } + let!(:note_metadata) { create(:system_note_metadata, note: note, action: 'cross_reference') } + let(:cross_reference) { "test commit #{commit.to_reference(project)}" } + let(:pat) { create(:personal_access_token, user: user) } + + before do + project.add_developer(user) + new_merge_request.project.add_developer(user) + + hidden_merge_request = create(:merge_request) + new_cross_reference = "test commit #{hidden_merge_request.project.commit}" + new_note = create(:system_note, noteable: merge_request, project: project, note: new_cross_reference) + create(:system_note_metadata, note: new_note, action: 'cross_reference') + end + + it 'returns only the note that the user should see' do + get api(url, user, personal_access_token: pat) + + expect(response).to have_gitlab_http_status(200) + expect(json_response.count).to eq(1) + expect(notes_in_response.count).to eq(1) + + parsed_note = notes_in_response.first + expect(parsed_note['id']).to eq(note.id) + expect(parsed_note['body']).to eq(cross_reference) + expect(parsed_note['system']).to be true + end + + it 'avoids Git calls and N+1 SQL queries', :request_store do + expect_any_instance_of(Repository).not_to receive(:find_commit).with(commit.id) + + control = ActiveRecord::QueryRecorder.new do + get api(url, user, personal_access_token: pat) + end + + expect(response).to have_gitlab_http_status(200) + + RequestStore.clear! + + new_note = create(:system_note, noteable: merge_request, project: project, note: cross_reference) + create(:system_note_metadata, note: new_note, action: 'cross_reference') + + RequestStore.clear! + + expect { get api(url, user, personal_access_token: pat) }.not_to exceed_query_limit(control) + expect(response).to have_gitlab_http_status(200) + end +end + shared_examples 'discussions API' do |parent_type, noteable_type, id_name, can_reply_to_individual_notes: false| describe "GET /#{parent_type}/:id/#{noteable_type}/:noteable_id/discussions" do it "returns an array of discussions" do diff --git a/spec/support/shared_examples/requests/api/pipelines/visibility_table_examples.rb b/spec/support/shared_examples/requests/api/pipelines/visibility_table_examples.rb new file mode 100644 index 00000000000..dfd07176b1c --- /dev/null +++ b/spec/support/shared_examples/requests/api/pipelines/visibility_table_examples.rb @@ -0,0 +1,235 @@ +# frozen_string_literal: true + +shared_examples 'pipelines visibility table' do + using RSpec::Parameterized::TableSyntax + + let(:ci_user) { create(:user) } + let(:api_user) { user_role && ci_user } + + let(:pipelines_api_path) do + "/projects/#{project.id}/pipelines" + end + + let(:response_200) do + a_collection_containing_exactly( + a_hash_including('sha', 'ref', 'status', 'web_url', 'id' => pipeline.id) + ) + end + + let(:response_40x) do + a_hash_including('message') + end + + let(:expected_response) do + if response_status == 200 + response_200 + else + response_40x + end + end + + let(:api_response) { json_response } + + let(:visibility_levels) do + { + private: Gitlab::VisibilityLevel::PRIVATE, + internal: Gitlab::VisibilityLevel::INTERNAL, + public: Gitlab::VisibilityLevel::PUBLIC + } + end + + let(:builds_access_levels) do + { + enabled: ProjectFeature::ENABLED, + private: ProjectFeature::PRIVATE + } + end + + let(:project_attributes) do + { + visibility_level: visibility_levels[visibility_level], + public_builds: public_builds + } + end + + let(:project_feature_attributes) do + { + builds_access_level: builds_access_levels[builds_access_level] + } + end + + where(:visibility_level, :builds_access_level, :public_builds, :is_admin, :user_role, :response_status) do + :private | :enabled | true | true | :non_member | 200 + :private | :enabled | true | true | :guest | 200 + :private | :enabled | true | true | :reporter | 200 + :private | :enabled | true | true | :developer | 200 + :private | :enabled | true | true | :maintainer | 200 + + :private | :enabled | true | false | nil | 404 + :private | :enabled | true | false | :non_member | 404 + :private | :enabled | true | false | :guest | 200 + :private | :enabled | true | false | :reporter | 200 + :private | :enabled | true | false | :developer | 200 + :private | :enabled | true | false | :maintainer | 200 + + :private | :enabled | false | true | :non_member | 200 + :private | :enabled | false | true | :guest | 200 + :private | :enabled | false | true | :reporter | 200 + :private | :enabled | false | true | :developer | 200 + :private | :enabled | false | true | :maintainer | 200 + + :private | :enabled | false | false | nil | 404 + :private | :enabled | false | false | :non_member | 404 + :private | :enabled | false | false | :guest | 403 + :private | :enabled | false | false | :reporter | 200 + :private | :enabled | false | false | :developer | 200 + :private | :enabled | false | false | :maintainer | 200 + + :private | :private | true | true | :non_member | 200 + :private | :private | true | true | :guest | 200 + :private | :private | true | true | :reporter | 200 + :private | :private | true | true | :developer | 200 + :private | :private | true | true | :maintainer | 200 + + :private | :private | true | false | nil | 404 + :private | :private | true | false | :non_member | 404 + :private | :private | true | false | :guest | 200 + :private | :private | true | false | :reporter | 200 + :private | :private | true | false | :developer | 200 + :private | :private | true | false | :maintainer | 200 + + :private | :private | false | true | :non_member | 200 + :private | :private | false | true | :guest | 200 + :private | :private | false | true | :reporter | 200 + :private | :private | false | true | :developer | 200 + :private | :private | false | true | :maintainer | 200 + + :private | :private | false | false | nil | 404 + :private | :private | false | false | :non_member | 404 + :private | :private | false | false | :guest | 403 + :private | :private | false | false | :reporter | 200 + :private | :private | false | false | :developer | 200 + :private | :private | false | false | :maintainer | 200 + + :internal | :enabled | true | true | :non_member | 200 + :internal | :enabled | true | true | :guest | 200 + :internal | :enabled | true | true | :reporter | 200 + :internal | :enabled | true | true | :developer | 200 + :internal | :enabled | true | true | :maintainer | 200 + + :internal | :enabled | true | false | nil | 404 + :internal | :enabled | true | false | :non_member | 200 + :internal | :enabled | true | false | :guest | 200 + :internal | :enabled | true | false | :reporter | 200 + :internal | :enabled | true | false | :developer | 200 + :internal | :enabled | true | false | :maintainer | 200 + + :internal | :enabled | false | true | :non_member | 200 + :internal | :enabled | false | true | :guest | 200 + :internal | :enabled | false | true | :reporter | 200 + :internal | :enabled | false | true | :developer | 200 + :internal | :enabled | false | true | :maintainer | 200 + + :internal | :enabled | false | false | nil | 404 + :internal | :enabled | false | false | :non_member | 403 + :internal | :enabled | false | false | :guest | 403 + :internal | :enabled | false | false | :reporter | 200 + :internal | :enabled | false | false | :developer | 200 + :internal | :enabled | false | false | :maintainer | 200 + + :internal | :private | true | true | :non_member | 200 + :internal | :private | true | true | :guest | 200 + :internal | :private | true | true | :reporter | 200 + :internal | :private | true | true | :developer | 200 + :internal | :private | true | true | :maintainer | 200 + + :internal | :private | true | false | nil | 404 + :internal | :private | true | false | :non_member | 403 + :internal | :private | true | false | :guest | 200 + :internal | :private | true | false | :reporter | 200 + :internal | :private | true | false | :developer | 200 + :internal | :private | true | false | :maintainer | 200 + + :internal | :private | false | true | :non_member | 200 + :internal | :private | false | true | :guest | 200 + :internal | :private | false | true | :reporter | 200 + :internal | :private | false | true | :developer | 200 + :internal | :private | false | true | :maintainer | 200 + + :internal | :private | false | false | nil | 404 + :internal | :private | false | false | :non_member | 403 + :internal | :private | false | false | :guest | 403 + :internal | :private | false | false | :reporter | 200 + :internal | :private | false | false | :developer | 200 + :internal | :private | false | false | :maintainer | 200 + + :public | :enabled | true | true | :non_member | 200 + :public | :enabled | true | true | :guest | 200 + :public | :enabled | true | true | :reporter | 200 + :public | :enabled | true | true | :developer | 200 + :public | :enabled | true | true | :maintainer | 200 + + :public | :enabled | true | false | nil | 200 + :public | :enabled | true | false | :non_member | 200 + :public | :enabled | true | false | :guest | 200 + :public | :enabled | true | false | :reporter | 200 + :public | :enabled | true | false | :developer | 200 + :public | :enabled | true | false | :maintainer | 200 + + :public | :enabled | false | true | :non_member | 200 + :public | :enabled | false | true | :guest | 200 + :public | :enabled | false | true | :reporter | 200 + :public | :enabled | false | true | :developer | 200 + :public | :enabled | false | true | :maintainer | 200 + + :public | :enabled | false | false | nil | 403 + :public | :enabled | false | false | :non_member | 403 + :public | :enabled | false | false | :guest | 403 + :public | :enabled | false | false | :reporter | 200 + :public | :enabled | false | false | :developer | 200 + :public | :enabled | false | false | :maintainer | 200 + + :public | :private | true | true | :non_member | 200 + :public | :private | true | true | :guest | 200 + :public | :private | true | true | :reporter | 200 + :public | :private | true | true | :developer | 200 + :public | :private | true | true | :maintainer | 200 + + :public | :private | true | false | nil | 403 + :public | :private | true | false | :non_member | 403 + :public | :private | true | false | :guest | 200 + :public | :private | true | false | :reporter | 200 + :public | :private | true | false | :developer | 200 + :public | :private | true | false | :maintainer | 200 + + :public | :private | false | true | :non_member | 200 + :public | :private | false | true | :guest | 200 + :public | :private | false | true | :reporter | 200 + :public | :private | false | true | :developer | 200 + :public | :private | false | true | :maintainer | 200 + + :public | :private | false | false | nil | 403 + :public | :private | false | false | :non_member | 403 + :public | :private | false | false | :guest | 403 + :public | :private | false | false | :reporter | 200 + :public | :private | false | false | :developer | 200 + :public | :private | false | false | :maintainer | 200 + end + + with_them do + before do + ci_user.update!(admin: is_admin) if user_role + + project.update!(project_attributes) + project.project_feature.update!(project_feature_attributes) + project.add_role(ci_user, user_role) if user_role && user_role != :non_member + + get api(pipelines_api_path, api_user) + end + + it do + expect(response).to have_gitlab_http_status(response_status) + expect(api_response).to match(expected_response) + end + end +end diff --git a/spec/support/shared_examples/services/notification_service_shared_examples.rb b/spec/support/shared_examples/services/notification_service_shared_examples.rb new file mode 100644 index 00000000000..dd338ea47c7 --- /dev/null +++ b/spec/support/shared_examples/services/notification_service_shared_examples.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +# Note that we actually update the attribute on the target_project/group, rather than +# using `allow`. This is because there are some specs where, based on how the notification +# is done, using an `allow` doesn't change the correct object. +shared_examples 'project emails are disabled' do + let(:target_project) { notification_target.is_a?(Project) ? notification_target : notification_target.project } + + before do + reset_delivered_emails! + target_project.clear_memoization(:emails_disabled) + end + + it 'sends no emails with project emails disabled' do + target_project.update_attribute(:emails_disabled, true) + + notification_trigger + + should_not_email_anyone + end + + it 'sends emails to someone' do + target_project.update_attribute(:emails_disabled, false) + + notification_trigger + + should_email_anyone + end +end + +shared_examples 'group emails are disabled' do + let(:target_group) { notification_target.is_a?(Group) ? notification_target : notification_target.project.group } + + before do + reset_delivered_emails! + target_group.clear_memoization(:emails_disabled) + end + + it 'sends no emails with group emails disabled' do + target_group.update_attribute(:emails_disabled, true) + + notification_trigger + + should_not_email_anyone + end + + it 'sends emails to someone' do + target_group.update_attribute(:emails_disabled, false) + + notification_trigger + + should_email_anyone + end +end diff --git a/spec/tasks/gitlab/update_templates_rake_spec.rb b/spec/tasks/gitlab/update_templates_rake_spec.rb index 7b17549b8c7..14b4ad5e3d8 100644 --- a/spec/tasks/gitlab/update_templates_rake_spec.rb +++ b/spec/tasks/gitlab/update_templates_rake_spec.rb @@ -8,9 +8,18 @@ describe 'gitlab:update_project_templates rake task' do before do Rake.application.rake_require 'tasks/gitlab/update_templates' create(:admin) + allow(Gitlab::ProjectTemplate) .to receive(:archive_directory) .and_return(Pathname.new(tmpdir)) + + # Gitlab::HTTP resolves the domain to an IP prior to WebMock taking effect, hence the wildcard + stub_request(:get, %r{^https://.*/api/v4/projects/gitlab-org%2Fproject-templates%2Frails/repository/commits\?page=1&per_page=1}) + .to_return( + status: 200, + body: [{ id: '67812735b83cb42710f22dc98d73d42c8bf4d907' }].to_json, + headers: { 'Content-Type' => 'application/json' } + ) end after do diff --git a/spec/views/layouts/_head.html.haml_spec.rb b/spec/views/layouts/_head.html.haml_spec.rb index cbb4199954a..70cdc08b4b6 100644 --- a/spec/views/layouts/_head.html.haml_spec.rb +++ b/spec/views/layouts/_head.html.haml_spec.rb @@ -70,6 +70,23 @@ describe 'layouts/_head' do expect(rendered).to match('<link rel="stylesheet" media="all" href="/stylesheets/highlight/themes/solarised-light.css" />') end + context 'when an asset_host is set and snowplow url is set' do + let(:asset_host) { 'http://test.host' } + + before do + allow(ActionController::Base).to receive(:asset_host).and_return(asset_host) + allow(Gitlab::CurrentSettings).to receive(:snowplow_enabled?).and_return(true) + allow(Gitlab::CurrentSettings).to receive(:snowplow_collector_hostname).and_return('www.snow.plow') + end + + it 'add a snowplow script tag with asset host' do + render + expect(rendered).to match('http://test.host/assets/snowplow/') + expect(rendered).to match('window.snowplow') + expect(rendered).to match('www.snow.plow') + end + end + def stub_helper_with_safe_string(method) allow_any_instance_of(PageLayoutHelper).to receive(method) .and_return(%q{foo" http-equiv="refresh}.html_safe) diff --git a/spec/views/projects/pages_domains/show.html.haml_spec.rb b/spec/views/projects/pages_domains/show.html.haml_spec.rb new file mode 100644 index 00000000000..da27a04bfe9 --- /dev/null +++ b/spec/views/projects/pages_domains/show.html.haml_spec.rb @@ -0,0 +1,66 @@ +require 'spec_helper' + +describe 'projects/pages_domains/show' do + let(:project) { create(:project, :repository) } + + before do + assign(:project, project) + assign(:domain, domain) + end + + context 'when auto_ssl is enabled' do + context 'when domain is disabled' do + let(:domain) { create(:pages_domain, :disabled, project: project, auto_ssl_enabled: true) } + + it 'shows verification warning' do + render + + expect(rendered).to have_content("A Let's Encrypt SSL certificate can not be obtained until your domain is verified.") + end + end + + context 'when certificate is absent' do + let(:domain) { create(:pages_domain, :without_key, :without_certificate, project: project, auto_ssl_enabled: true) } + + it 'shows alert about time of obtaining certificate' do + render + + expect(rendered).to have_content("GitLab is obtaining a Let's Encrypt SSL certificate for this domain. This process can take some time. Please try again later.") + end + end + + context 'when certificate is present' do + let(:domain) { create(:pages_domain, :letsencrypt, project: project) } + + it 'shows certificate info' do + render + + # test just a random part of cert represenations(X509v3 Subject Key Identifier:) + expect(rendered).to have_content("C6:5F:56:4B:10:69:AC:1D:33:D2:26:C9:B3:7A:D7:12:4D:3E:F7:90") + end + end + end + + context 'when auto_ssl is disabled' do + context 'when certificate is present' do + let(:domain) { create(:pages_domain, project: project) } + + it 'shows certificate info' do + render + + # test just a random part of cert represenations(X509v3 Subject Key Identifier:) + expect(rendered).to have_content("C6:5F:56:4B:10:69:AC:1D:33:D2:26:C9:B3:7A:D7:12:4D:3E:F7:90") + end + end + + context 'when certificate is absent' do + let(:domain) { create(:pages_domain, :without_certificate, :without_key, project: project) } + + it 'shows missing certificate' do + render + + expect(rendered).to have_content("missing") + end + end + end +end diff --git a/spec/workers/build_process_worker_spec.rb b/spec/workers/build_process_worker_spec.rb deleted file mode 100644 index d9a02ece142..00000000000 --- a/spec/workers/build_process_worker_spec.rb +++ /dev/null @@ -1,26 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -describe BuildProcessWorker do - describe '#perform' do - context 'when build exists' do - let(:pipeline) { create(:ci_pipeline) } - let(:build) { create(:ci_build, pipeline: pipeline) } - - it 'processes build' do - expect_any_instance_of(Ci::Pipeline).to receive(:process!) - .with([build.id]) - - described_class.new.perform(build.id) - end - end - - context 'when build does not exist' do - it 'does not raise exception' do - expect { described_class.new.perform(123) } - .not_to raise_error - end - end - end -end diff --git a/spec/workers/ci/archive_traces_cron_worker_spec.rb b/spec/workers/ci/archive_traces_cron_worker_spec.rb index 28381fdc3be..01232e2a58b 100644 --- a/spec/workers/ci/archive_traces_cron_worker_spec.rb +++ b/spec/workers/ci/archive_traces_cron_worker_spec.rb @@ -5,6 +5,8 @@ require 'spec_helper' describe Ci::ArchiveTracesCronWorker do subject { described_class.new.perform } + let(:finished_at) { 1.day.ago } + before do stub_feature_flags(ci_enable_live_trace: true) end @@ -28,7 +30,7 @@ describe Ci::ArchiveTracesCronWorker do end context 'when a job succeeded' do - let!(:build) { create(:ci_build, :success, :trace_live) } + let!(:build) { create(:ci_build, :success, :trace_live, finished_at: finished_at) } it_behaves_like 'archives trace' @@ -39,9 +41,15 @@ describe Ci::ArchiveTracesCronWorker do subject end + context 'when the job finished recently' do + let(:finished_at) { 1.hour.ago } + + it_behaves_like 'does not archive trace' + end + context 'when a trace had already been archived' do let!(:build) { create(:ci_build, :success, :trace_live, :trace_artifact) } - let!(:build2) { create(:ci_build, :success, :trace_live) } + let!(:build2) { create(:ci_build, :success, :trace_live, finished_at: finished_at) } it 'continues to archive live traces' do subject @@ -52,7 +60,7 @@ describe Ci::ArchiveTracesCronWorker do end context 'when an unexpected exception happened during archiving' do - let!(:build) { create(:ci_build, :success, :trace_live) } + let!(:build) { create(:ci_build, :success, :trace_live, finished_at: finished_at) } before do allow(Gitlab::Sentry).to receive(:track_exception) @@ -71,7 +79,7 @@ describe Ci::ArchiveTracesCronWorker do end context 'when a job was cancelled' do - let!(:build) { create(:ci_build, :canceled, :trace_live) } + let!(:build) { create(:ci_build, :canceled, :trace_live, finished_at: finished_at) } it_behaves_like 'archives trace' end diff --git a/spec/workers/post_receive_spec.rb b/spec/workers/post_receive_spec.rb index 39f1beb4efa..c8a0c22b0e8 100644 --- a/spec/workers/post_receive_spec.rb +++ b/spec/workers/post_receive_spec.rb @@ -14,6 +14,10 @@ describe PostReceive do create(:project, :repository, auto_cancel_pending_pipelines: 'disabled') end + def perform(changes: base64_changes) + described_class.new.perform(gl_repository, key_id, changes) + end + context "as a sidekiq worker" do it "responds to #perform" do expect(described_class.new).to respond_to(:perform) @@ -28,18 +32,41 @@ describe PostReceive do it "returns false and logs an error" do expect(Gitlab::GitLogger).to receive(:error).with("POST-RECEIVE: #{error_message}") - expect(described_class.new.perform(gl_repository, key_id, base64_changes)).to be(false) + expect(perform).to be(false) end end describe "#process_project_changes" do + context 'with an empty project' do + let(:empty_project) { create(:project, :empty_repo) } + let(:changes) { "123456 789012 refs/heads/tést1\n" } + + before do + allow_any_instance_of(Gitlab::GitPostReceive).to receive(:identify).and_return(empty_project.owner) + allow(Gitlab::GlRepository).to receive(:parse).and_return([empty_project, Gitlab::GlRepository::PROJECT]) + end + + it 'expire the status cache' do + expect(empty_project.repository).to receive(:expire_status_cache) + + perform + end + + it 'schedules a cache update for commit count and size' do + expect(ProjectCacheWorker).to receive(:perform_async) + .with(empty_project.id, [], [:repository_size, :commit_count], true) + + perform + end + end + context 'empty changes' do it "does not call any PushService but runs after project hooks" do expect(Git::BranchPushService).not_to receive(:new) expect(Git::TagPushService).not_to receive(:new) expect_next_instance_of(SystemHooksService) { |service| expect(service).to receive(:execute_hooks) } - described_class.new.perform(gl_repository, key_id, "") + perform(changes: "") end end @@ -50,40 +77,118 @@ describe PostReceive do expect(Git::BranchPushService).not_to receive(:new) expect(Git::TagPushService).not_to receive(:new) - expect(described_class.new.perform(gl_repository, key_id, base64_changes)).to be false + expect(perform).to be false end end context 'with changes' do before do allow_any_instance_of(Gitlab::GitPostReceive).to receive(:identify).and_return(project.owner) + allow(Gitlab::GlRepository).to receive(:parse).and_return([project, Gitlab::GlRepository::PROJECT]) end context "branches" do - let(:changes) { "123456 789012 refs/heads/tést" } + let(:changes) do + <<~EOF + 123456 789012 refs/heads/tést1 + 123456 789012 refs/heads/tést2 + EOF + end - it "calls Git::BranchPushService" do - expect_next_instance_of(Git::BranchPushService) do |service| + it 'expires the branches cache' do + expect(project.repository).to receive(:expire_branches_cache).once + + perform + end + + it 'expires the status cache' do + expect(project).to receive(:empty_repo?).and_return(true) + expect(project.repository).to receive(:expire_status_cache) + + perform + end + + it 'calls Git::BranchPushService' do + expect_any_instance_of(Git::BranchPushService) do |service| expect(service).to receive(:execute).and_return(true) end expect(Git::TagPushService).not_to receive(:new) - described_class.new.perform(gl_repository, key_id, base64_changes) + perform + end + + it 'schedules a cache update for repository size only' do + expect(ProjectCacheWorker).to receive(:perform_async) + .with(project.id, [], [:repository_size], true) + + perform + end + + context 'with a default branch' do + let(:changes) do + <<~EOF + 123456 789012 refs/heads/tést1 + 123456 789012 refs/heads/tést2 + 678912 123455 refs/heads/#{project.default_branch} + EOF + end + + it 'schedules a cache update for commit count and size' do + expect(ProjectCacheWorker).to receive(:perform_async) + .with(project.id, [], [:repository_size, :commit_count], true) + + perform + end end end context "tags" do - let(:changes) { "123456 789012 refs/tags/tag" } + let(:changes) do + <<~EOF + 654321 210987 refs/tags/tag1 + 654322 210986 refs/tags/tag2 + 654323 210985 refs/tags/tag3 + 654324 210984 refs/tags/tag4 + 654325 210983 refs/tags/tag5 + EOF + end + + before do + expect(Gitlab::GlRepository).to receive(:parse).and_return([project, Gitlab::GlRepository::PROJECT]) + end + + it 'does not expire branches cache' do + expect(project.repository).not_to receive(:expire_branches_cache) + + perform + end + + it "only invalidates tags once" do + expect(project.repository).to receive(:repository_event).exactly(5).times.with(:push_tag).and_call_original + expect(project.repository).to receive(:expire_caches_for_tags).once.and_call_original + expect(project.repository).to receive(:expire_tags_cache).once.and_call_original + + perform + end it "calls Git::TagPushService" do - expect(Git::BranchPushService).not_to receive(:execute) + expect(Git::BranchPushService).not_to receive(:new) - expect_next_instance_of(Git::TagPushService) do |service| + expect_any_instance_of(Git::TagPushService) do |service| expect(service).to receive(:execute).and_return(true) end - described_class.new.perform(gl_repository, key_id, base64_changes) + expect(Git::BranchPushService).not_to receive(:new) + + perform + end + + it 'schedules a single ProjectCacheWorker update' do + expect(ProjectCacheWorker).to receive(:perform_async) + .with(project.id, [], [:repository_size], true) + + perform end end @@ -94,7 +199,7 @@ describe PostReceive do expect(Git::BranchPushService).not_to receive(:new) expect(Git::TagPushService).not_to receive(:new) - described_class.new.perform(gl_repository, key_id, base64_changes) + perform end end @@ -111,7 +216,7 @@ describe PostReceive do let(:changes_count) { changes.lines.count } - subject { described_class.new.perform(gl_repository, key_id, base64_changes) } + subject { perform } context "with valid .gitlab-ci.yml" do before do @@ -180,7 +285,13 @@ describe PostReceive do it 'calls SystemHooksService' do expect_any_instance_of(SystemHooksService).to receive(:execute_hooks).with(fake_hook_data, :repository_update_hooks).and_return(true) - described_class.new.perform(gl_repository, key_id, base64_changes) + perform + end + + it 'increments the usage data counter of pushes event' do + counter = Gitlab::UsageDataCounters::SourceCodeCounter + + expect { perform }.to change { counter.read(:pushes) }.by(1) end end end @@ -197,7 +308,7 @@ describe PostReceive do # a second to ensure we see changes. Timecop.freeze(1.second.from_now) do expect do - described_class.new.perform(gl_repository, key_id, base64_changes) + perform project.reload end.to change(project, :last_activity_at) .and change(project, :last_repository_updated_at) @@ -208,7 +319,8 @@ describe PostReceive do context "webhook" do it "fetches the correct project" do expect(Project).to receive(:find_by).with(id: project.id.to_s) - described_class.new.perform(gl_repository, key_id, base64_changes) + + perform end it "does not run if the author is not in the project" do @@ -218,16 +330,18 @@ describe PostReceive do expect(project).not_to receive(:execute_hooks) - expect(described_class.new.perform(gl_repository, key_id, base64_changes)).to be_falsey + expect(perform).to be_falsey end it "asks the project to trigger all hooks" do + create(:project_hook, push_events: true, tag_push_events: true, project: project) + create(:custom_issue_tracker_service, push_events: true, merge_requests_events: false, project: project) allow(Project).to receive(:find_by).and_return(project) expect(project).to receive(:execute_hooks).twice expect(project).to receive(:execute_services).twice - described_class.new.perform(gl_repository, key_id, base64_changes) + perform end it "enqueues a UpdateMergeRequestsWorker job" do @@ -235,7 +349,7 @@ describe PostReceive do expect(UpdateMergeRequestsWorker).to receive(:perform_async).with(project.id, project.owner.id, any_args) - described_class.new.perform(gl_repository, key_id, base64_changes) + perform end end end diff --git a/spec/workers/process_commit_worker_spec.rb b/spec/workers/process_commit_worker_spec.rb index 47bac63511e..eb1d3c364ac 100644 --- a/spec/workers/process_commit_worker_spec.rb +++ b/spec/workers/process_commit_worker_spec.rb @@ -3,8 +3,6 @@ require 'spec_helper' describe ProcessCommitWorker do - include ProjectForksHelper - let(:worker) { described_class.new } let(:user) { create(:user) } let(:project) { create(:project, :public, :repository) } @@ -35,44 +33,6 @@ describe ProcessCommitWorker do worker.perform(project.id, user.id, commit.to_hash) end - - context 'when the project is forked' do - context 'when commit already exists in the upstream project' do - it 'does not process the commit message' do - forked = fork_project(project, user, repository: true) - - expect(worker).not_to receive(:process_commit_message) - - worker.perform(forked.id, user.id, forked.commit.to_hash) - end - end - - context 'when the commit does not exist in the upstream project' do - it 'processes the commit message' do - empty_project = create(:project, :public) - forked = fork_project(empty_project, user, repository: true) - - TestEnv.copy_repo(forked, - bare_repo: TestEnv.factory_repo_path_bare, - refs: TestEnv::BRANCH_SHA) - - expect(worker).to receive(:process_commit_message) - - worker.perform(forked.id, user.id, forked.commit.to_hash) - end - end - - context 'when the upstream project no longer exists' do - it 'processes the commit message' do - forked = fork_project(project, user, repository: true) - project.destroy! - - expect(worker).to receive(:process_commit_message) - - worker.perform(forked.id, user.id, forked.commit.to_hash) - end - end - end end describe '#process_commit_message' do diff --git a/spec/workers/project_cache_worker_spec.rb b/spec/workers/project_cache_worker_spec.rb index edc55920b8e..7f3c4881b89 100644 --- a/spec/workers/project_cache_worker_spec.rb +++ b/spec/workers/project_cache_worker_spec.rb @@ -49,6 +49,16 @@ describe ProjectCacheWorker do worker.perform(project.id, %w(readme)) end + context 'with statistics disabled' do + let(:statistics) { [] } + + it 'does not update the project statistics' do + expect(worker).not_to receive(:update_statistics) + + worker.perform(project.id, [], [], false) + end + end + context 'with statistics' do let(:statistics) { %w(repository_size) } diff --git a/spec/workers/repository_update_remote_mirror_worker_spec.rb b/spec/workers/repository_update_remote_mirror_worker_spec.rb index 4de51ecb3e9..66d517332ba 100644 --- a/spec/workers/repository_update_remote_mirror_worker_spec.rb +++ b/spec/workers/repository_update_remote_mirror_worker_spec.rb @@ -2,99 +2,70 @@ require 'rails_helper' -describe RepositoryUpdateRemoteMirrorWorker do +describe RepositoryUpdateRemoteMirrorWorker, :clean_gitlab_redis_shared_state do subject { described_class.new } - let(:remote_mirror) { create(:project, :repository, :remote_mirror).remote_mirrors.first } + let(:remote_mirror) { create(:remote_mirror) } let(:scheduled_time) { Time.now - 5.minutes } around do |example| Timecop.freeze(Time.now) { example.run } end - describe '#perform' do - context 'with status none' do - before do - remote_mirror.update(update_status: 'none') - end - - it 'sets status as finished when update remote mirror service executes successfully' do - expect_any_instance_of(Projects::UpdateRemoteMirrorService).to receive(:execute).with(remote_mirror).and_return(status: :success) - - 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!' - - expect_next_instance_of(Projects::UpdateRemoteMirrorService) do |service| - expect(service).to receive(:execute).with(remote_mirror).and_return(status: :error, message: error_message) - end + def expect_mirror_service_to_return(mirror, result, tries = 0) + expect_next_instance_of(Projects::UpdateRemoteMirrorService) do |service| + expect(service).to receive(:execute).with(mirror, tries).and_return(result) + end + end - # Mock the finder so that it returns an object we can set expectations on - expect_next_instance_of(RemoteMirrorFinder) do |finder| - expect(finder).to receive(:execute).and_return(remote_mirror) - end - expect(remote_mirror).to receive(:mark_as_failed).with(error_message) + describe '#perform' do + it 'calls out to the service to perform the update' do + expect_mirror_service_to_return(remote_mirror, status: :success) - expect do - subject.perform(remote_mirror.id, Time.now) - end.to raise_error(RepositoryUpdateRemoteMirrorWorker::UpdateError, error_message) - end + subject.perform(remote_mirror.id, scheduled_time) + end - it 'does nothing if last_update_started_at is higher than the time the job was scheduled in' do - remote_mirror.update(last_update_started_at: Time.now) + it 'does not do anything if the mirror was already updated' do + remote_mirror.update(last_update_started_at: Time.now, update_status: :finished) - expect_any_instance_of(RemoteMirror).to receive(:updated_since?).with(scheduled_time).and_return(true) - expect_any_instance_of(Projects::UpdateRemoteMirrorService).not_to receive(:execute).with(remote_mirror) + expect(Projects::UpdateRemoteMirrorService).not_to receive(:new) - expect(subject.perform(remote_mirror.id, scheduled_time)).to be_nil - end + subject.perform(remote_mirror.id, scheduled_time) end - context 'with unexpected error' do - it 'marks mirror as failed' do - allow_any_instance_of(Projects::UpdateRemoteMirrorService).to receive(:execute).with(remote_mirror).and_raise(RuntimeError) + it 'schedules a retry when the mirror is marked for retrying' do + remote_mirror = create(:remote_mirror, update_status: :to_retry) + expect_mirror_service_to_return(remote_mirror, status: :error, message: 'Retry!') - expect do - subject.perform(remote_mirror.id, Time.now) - end.to raise_error(RepositoryUpdateRemoteMirrorWorker::UpdateError) - expect(remote_mirror.reload.update_status).to eq('failed') - end - end + expect(described_class) + .to receive(:perform_in) + .with(remote_mirror.backoff_delay, remote_mirror.id, scheduled_time, 1) - context 'with another worker already running' do - before do - remote_mirror.update(update_status: 'started') - end - - it 'raises RemoteMirrorUpdateAlreadyInProgressError' do - expect do - subject.perform(remote_mirror.id, Time.now) - end.to raise_error(RepositoryUpdateRemoteMirrorWorker::UpdateAlreadyInProgressError) - end + subject.perform(remote_mirror.id, scheduled_time) end - context 'with status failed' do - before do - remote_mirror.update(update_status: 'failed') + it 'clears the lease if there was an unexpected exception' do + expect_next_instance_of(Projects::UpdateRemoteMirrorService) do |service| + expect(service).to receive(:execute).with(remote_mirror, 1).and_raise('Unexpected!') end + expect { subject.perform(remote_mirror.id, Time.now, 1) }.to raise_error('Unexpected!') - it 'sets status as finished if last_update_started_at is higher than the time the job was scheduled in' do - remote_mirror.update(last_update_started_at: Time.now) + lease = Gitlab::ExclusiveLease.new("#{described_class.name}:#{remote_mirror.id}", timeout: 1.second) - expect_any_instance_of(RemoteMirror).to receive(:updated_since?).with(scheduled_time).and_return(false) - expect_any_instance_of(Projects::UpdateRemoteMirrorService).to receive(:execute).with(remote_mirror).and_return(status: :success) + expect(lease.try_obtain).not_to be_nil + end - expect { subject.perform(remote_mirror.id, scheduled_time) }.to change { remote_mirror.reload.update_status }.to('finished') - end + it 'retries 3 times for the worker to finish before rescheduling' do + expect(subject).to receive(:in_lock) + .with("#{described_class.name}:#{remote_mirror.id}", + retries: 3, + ttl: remote_mirror.max_runtime, + sleep_sec: described_class::LOCK_WAIT_TIME) + .and_raise(Gitlab::ExclusiveLeaseHelpers::FailedToObtainLockError) + expect(described_class).to receive(:perform_in) + .with(remote_mirror.backoff_delay, remote_mirror.id, scheduled_time, 0) + + subject.perform(remote_mirror.id, scheduled_time) end end end diff --git a/yarn.lock b/yarn.lock index ed1f06523c0..ef66861488c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9,7 +9,7 @@ dependencies: "@babel/highlight" "^7.0.0" -"@babel/core@>=7.2.2", "@babel/core@^7.1.0", "@babel/core@^7.1.2", "@babel/core@^7.4.4": +"@babel/core@>=7.2.2", "@babel/core@^7.1.0", "@babel/core@^7.1.2", "@babel/core@^7.5.5": version "7.5.5" resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.5.5.tgz#17b2686ef0d6bc58f963dddd68ab669755582c30" integrity sha512-i4qoSr2KTtce0DmkuuQBV4AuQgGPUcPXMr9L5MyYAtk06z068lQ10a4O009fe5OB/DfNV+h+qqT7ddNV8UnRjg== @@ -139,16 +139,16 @@ "@babel/types" "^7.0.0" "@babel/helper-module-transforms@^7.1.0", "@babel/helper-module-transforms@^7.4.4": - version "7.4.4" - resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.4.4.tgz#96115ea42a2f139e619e98ed46df6019b94414b8" - integrity sha512-3Z1yp8TVQf+B4ynN7WoHPKS8EkdTbgAEy0nU0rs/1Kw4pDgmvYH3rz3aI11KgxKCba2cn7N+tqzV1mY2HMN96w== + version "7.5.5" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.5.5.tgz#f84ff8a09038dcbca1fd4355661a500937165b4a" + integrity sha512-jBeCvETKuJqeiaCdyaheF40aXnnU1+wkSiUs/IQg3tB85up1LyL8x77ClY8qJpuRJUcXQo+ZtdNESmZl4j56Pw== dependencies: "@babel/helper-module-imports" "^7.0.0" "@babel/helper-simple-access" "^7.1.0" "@babel/helper-split-export-declaration" "^7.4.4" "@babel/template" "^7.4.4" - "@babel/types" "^7.4.4" - lodash "^4.17.11" + "@babel/types" "^7.5.5" + lodash "^4.17.13" "@babel/helper-optimise-call-expression@^7.0.0": version "7.0.0" @@ -163,11 +163,11 @@ integrity sha512-CYAOUCARwExnEixLdB6sDm2dIJ/YgEAKDM1MOeMeZu9Ld/bDgVo8aiWrXwcY7OBh+1Ea2uUcVRcxKk0GJvW7QA== "@babel/helper-regex@^7.0.0", "@babel/helper-regex@^7.4.4": - version "7.4.4" - resolved "https://registry.yarnpkg.com/@babel/helper-regex/-/helper-regex-7.4.4.tgz#a47e02bc91fb259d2e6727c2a30013e3ac13c4a2" - integrity sha512-Y5nuB/kESmR3tKjU8Nkn1wMGEx1tjJX076HBMeL3XLQCu6vA/YRzuTW0bbb+qRnXvQGn+d6Rx953yffl8vEy7Q== + version "7.5.5" + resolved "https://registry.yarnpkg.com/@babel/helper-regex/-/helper-regex-7.5.5.tgz#0aa6824f7100a2e0e89c1527c23936c152cab351" + integrity sha512-CkCYQLkfkiugbRDO8eZn6lRuR8kzZoGXCg3149iTk5se7g6qykSpy3+hELSwquhu+TgHn8nkLiBwHvNX8Hofcw== dependencies: - lodash "^4.17.11" + lodash "^4.17.13" "@babel/helper-remap-async-to-generator@^7.1.0": version "7.1.0" @@ -225,9 +225,9 @@ "@babel/types" "^7.5.5" "@babel/highlight@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.0.0.tgz#f710c38c8d458e6dd9a201afb637fcb781ce99e4" - integrity sha512-UFMC4ZeFC48Tpvj7C8UgLvtkaUuovQX+5xNWrsIoMG8o2z+XFKjKaN9iVmS84dPwVN00W4wPmqvYoZF3EGAsfw== + version "7.5.0" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.5.0.tgz#56d11312bd9248fa619591d02472be6e8cb32540" + integrity sha512-7dV4eu9gBxoM0dAnj/BCFDW9LFU0zvTrkq0ugM7pnHEgguOEeOz1so2ZghEdzviYzQEED0r4EAgpsBChKy1TRQ== dependencies: chalk "^2.0.0" esutils "^2.0.2" @@ -252,7 +252,7 @@ "@babel/helper-remap-async-to-generator" "^7.1.0" "@babel/plugin-syntax-async-generators" "^7.2.0" -"@babel/plugin-proposal-class-properties@^7.1.0", "@babel/plugin-proposal-class-properties@^7.4.4": +"@babel/plugin-proposal-class-properties@^7.1.0", "@babel/plugin-proposal-class-properties@^7.5.5": version "7.5.5" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.5.5.tgz#a974cfae1e37c3110e71f3c6a2e48b8e71958cd4" integrity sha512-AF79FsnWFxjlaosgdi421vmYG6/jg79bVD0dpD44QdgobzHKuLZ6S3vl8la9qIeSwGi8i1fS0O1mfuDAAdo1/A== @@ -833,7 +833,7 @@ "@babel/helper-regex" "^7.4.4" regexpu-core "^4.5.4" -"@babel/preset-env@^7.1.0", "@babel/preset-env@^7.4.4": +"@babel/preset-env@^7.1.0", "@babel/preset-env@^7.5.5": version "7.5.5" resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.5.5.tgz#bc470b53acaa48df4b8db24a570d6da1fef53c9a" integrity sha512-GMZQka/+INwsMz1A5UEql8tG015h5j/qjptpKY2gJ7giy8ohzU710YciJB5rcKsWGWHiW3RUnHib0E5/m3Tp3A== @@ -914,9 +914,9 @@ integrity sha512-FBMd0IiARPtH5aaOFUVki6evHiJQiY0pFy7fizyRF7dtwc+el3nwpzvhb9qBNzceG1OIJModG1xpE0DDFjPXwA== "@babel/standalone@^7.0.0": - version "7.3.4" - resolved "https://registry.yarnpkg.com/@babel/standalone/-/standalone-7.3.4.tgz#b622c1e522acef91b2a14f22bdcdd4f935a1a474" - integrity sha512-4L9c5i4WlGqbrjOVX0Yp8TIR5cEiw1/tPYYZENW/iuO2uI6viY38U7zALidzNfGdZIwNc+A/AWqMEWKeScWkBg== + version "7.5.5" + resolved "https://registry.yarnpkg.com/@babel/standalone/-/standalone-7.5.5.tgz#9d3143f6078ff408db694a4254bd6f03c5c33962" + integrity sha512-YIp5taErC4uvp4d5urJtWMui3cpvZt83x57l4oVJNvFtDzumf3pMgRmoTSpGuEzh1yzo7jHhg3mbQmMhmKPbjA== "@babel/template@^7.1.0", "@babel/template@^7.4.0", "@babel/template@^7.4.4": version "7.4.4" @@ -998,15 +998,15 @@ dependencies: vue-eslint-parser "^6.0.4" -"@gitlab/svgs@^1.67.0": - version "1.67.0" - resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.67.0.tgz#c7b94eca13b99fd3aaa737fb6dcc0abc41d3c579" - integrity sha512-hJOmWEs6RkjzyKkb1vc9wwKGZIBIP0coHkxu/KgOoxhBVudpGk4CH7xJ6UuB2TKpb0SEh5CC1CzRZfBYaFhsaA== +"@gitlab/svgs@^1.70.0": + version "1.70.0" + resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.70.0.tgz#bdae478148c15d955fc06e69fd5d5ecae8298943" + integrity sha512-0uV9fgTwe17Fyy0hTcrsGX2jJuCrz3uRIe8yffuqc6pbQrSfYJyN66mfCCB45wq8lKTgOB5q0qcUyJx3RQEcKg== -"@gitlab/ui@5.15.0": - version "5.15.0" - resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-5.15.0.tgz#1ce92cfed77dcd63a90d751043b42b19e64431c9" - integrity sha512-YrYgERcmTxC+oP4GaY7onqvYgvTsyGCiiegQbZbXdNRLGGAmvfxWPzQRz8Ci9yIYkLvi0X2AV7BT8RTVOPQgXg== +"@gitlab/ui@5.18.0": + version "5.18.0" + resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-5.18.0.tgz#f7f93ad41673f5ae081cd2246dda5456d3c956a2" + integrity sha512-umB7JCKVDZOajed0N563JBYSpiyK+VDL2eCdkinW+twWhMct8onoW4CLTpkgBi6Z9Y1Bq47aqnFtGf+1IKNIGw== dependencies: "@babel/standalone" "^7.0.0" "@gitlab/vue-toasted" "^1.2.1" @@ -1021,6 +1021,11 @@ vue "^2.6.10" vue-loader "^15.4.2" +"@gitlab/visual-review-tools@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@gitlab/visual-review-tools/-/visual-review-tools-1.0.0.tgz#6012e0a19797c1f5dad34ccf4dacdaf38e400a73" + integrity sha512-xMvz9IwrXisQ1MH+Tj6lfbQcQSiQy88nTPuQV6OTLBGuV+vIQeVwXeIkQeTKuSpd0GqZvigPdRqxyQCa3blpIg== + "@gitlab/vue-toasted@^1.2.1": version "1.2.1" resolved "https://registry.yarnpkg.com/@gitlab/vue-toasted/-/vue-toasted-1.2.1.tgz#f407b5aa710863e5b7f021f4a1f66160331ab263" |