diff --git a/.babelrc.js b/.babelrc.js index bfcc7d9663..1b05a67354 100644 --- a/.babelrc.js +++ b/.babelrc.js @@ -18,6 +18,7 @@ const plugins = [ '@babel/plugin-syntax-import-meta', '@babel/plugin-proposal-class-properties', '@babel/plugin-proposal-json-strings', + '@babel/plugin-proposal-private-methods', ]; // add code coverage tooling if necessary diff --git a/.gitignore b/.gitignore index 65f61e1fad..0696dd217a 100644 --- a/.gitignore +++ b/.gitignore @@ -80,3 +80,4 @@ eslint-report.html package-lock.json /junit_*.xml /coverage-frontend/ +jsdoc/ diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 45de5ce61c..4e8453726a 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,4 +1,4 @@ -image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.5.3-golang-1.9-git-2.18-chrome-69.0-node-10.x-yarn-1.12-postgresql-9.6-graphicsmagick-1.3.29" +image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.5.3-golang-1.11-git-2.18-chrome-71.0-node-10.x-yarn-1.12-postgresql-9.6-graphicsmagick-1.3.29" .dedicated-runner: &dedicated-runner retry: 1 @@ -120,9 +120,8 @@ stages: variables: &single-script-job-variables GIT_STRATEGY: none before_script: - # We need to download the script rather than clone the repo since the - # package-and-qa job will not be able to run when the branch gets - # deleted (when merging the MR). + # We don't clone the repo by using GIT_STRATEGY: none and only download the + # single script we need here so it's much faster than cloning. - export SCRIPT_NAME="${SCRIPT_NAME:-$CI_JOB_NAME}" - apk add --update openssl - wget $CI_PROJECT_URL/raw/$CI_COMMIT_SHA/scripts/$SCRIPT_NAME @@ -228,20 +227,21 @@ stages: # Trigger a package build in omnibus-gitlab repository # package-and-qa: - <<: *single-script-job + image: ruby:2.5-alpine + stage: test + before_script: [] + dependencies: [] + cache: {} variables: - <<: *single-script-job-variables + GIT_DEPTH: "1" API_TOKEN: "${GITLAB_BOT_MULTI_PROJECT_PIPELINE_POLLING_TOKEN}" - SCRIPT_NAME: trigger-build retry: 0 script: - - gem install gitlab --no-document - apk add --update openssl curl jq - - wget $CI_PROJECT_URL/raw/$CI_COMMIT_SHA/scripts/review_apps/review-apps.sh - - chmod 755 review-apps.sh - - source ./review-apps.sh + - gem install gitlab --no-document + - source ./scripts/review_apps/review-apps.sh - wait_for_job_to_be_done "gitlab:assets:compile" - - ./$SCRIPT_NAME omnibus + - ./scripts/trigger-build omnibus when: manual only: - //@gitlab-org/gitlab-ce @@ -386,20 +386,27 @@ flaky-examples-check: - scripts/merge-reports ${NEW_FLAKY_SPECS_REPORT} rspec_flaky/new_*_*.json - scripts/detect-new-flaky-examples $NEW_FLAKY_SPECS_REPORT +.assets-compile-cache: &assets-compile-cache + cache: + key: "assets-compile:vendor_ruby:.yarn-cache:tmp_cache_assets_sprockets:v4" + paths: + - vendor/ruby/ + - .yarn-cache/ + # We have disabled caching of sprockets for now, as it fails to pick up changes in SCSS: + # https://gitlab.com/gitlab-org/gitlab-ce/issues/57431 + # - tmp/cache/assets/sprockets + compile-assets: <<: *dedicated-runner <<: *except-docs <<: *use-pg stage: prepare - cache: - <<: *default-cache script: - node --version - - date - yarn install --frozen-lockfile --cache-folder .yarn-cache - - date - free -m - bundle exec rake gitlab:assets:compile + - scripts/clean-old-cached-assets variables: # we override the max_old_space_size to prevent OOM errors NODE_OPTIONS: --max_old_space_size=3584 @@ -408,6 +415,7 @@ compile-assets: paths: - node_modules - public/assets + <<: *assets-compile-cache setup-test-env: <<: *dedicated-runner @@ -427,15 +435,7 @@ setup-test-env: - vendor/gitaly-ruby # GitLab Review apps -.review-base: &review-base - <<: *dedicated-no-docs-no-db-pull-cache-job - image: registry.gitlab.com/gitlab-org/gitlab-build-images:gitlab-charts-build-base - stage: test - cache: {} - dependencies: [] - environment: &review-environment - name: review/${CI_COMMIT_REF_NAME} - url: https://gitlab-${CI_ENVIRONMENT_SLUG}.${REVIEW_APPS_DOMAIN} +.review-only: &review-only only: refs: - branches@gitlab-org/gitlab-ce @@ -445,6 +445,17 @@ setup-test-env: refs: - master - /(^docs[\/-].*|.*-docs$)/ + +.review-base: &review-base + <<: *dedicated-no-docs-no-db-pull-cache-job + <<: *review-only + image: registry.gitlab.com/gitlab-org/gitlab-build-images:gitlab-charts-build-base + stage: test + cache: {} + dependencies: [] + environment: &review-environment + name: review/${CI_COMMIT_REF_NAME} + url: https://gitlab-${CI_ENVIRONMENT_SLUG}.${REVIEW_APPS_DOMAIN} before_script: [] .review-docker: &review-docker @@ -499,6 +510,22 @@ rspec-mysql: <<: *rspec-metadata-mysql parallel: 50 +.rspec-quarantine: &rspec-quarantine + script: + - export CACHE_CLASSES=true + - scripts/gitaly-test-spawn + - bin/rspec --color --format documentation --tag quarantine spec/ + +rspec-pg-quarantine: + <<: *rspec-metadata-pg + <<: *rspec-quarantine + allow_failure: true + +rspec-mysql-quarantine: + <<: *rspec-metadata-mysql + <<: *rspec-quarantine + allow_failure: true + static-analysis: <<: *dedicated-no-docs-no-db-pull-cache-job dependencies: @@ -527,7 +554,7 @@ docs lint: script: - scripts/lint-doc.sh - scripts/lint-changelog-yaml - - mv doc/ /tmp/gitlab-docs/content/ + - mv doc/ /tmp/gitlab-docs/content/$DOCS_GITLAB_REPO_SUFFIX - cd /tmp/gitlab-docs # Build HTML from Markdown - bundle exec nanoc @@ -608,8 +635,9 @@ gitlab:setup-mysql: # Frontend-related jobs gitlab:assets:compile: <<: *dedicated-no-docs-pull-cache-job - image: dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.5.3-git-2.18-chrome-69.0-node-8.x-yarn-1.12-graphicsmagick-1.3.29-docker-18.06.1 - dependencies: [] + image: dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.5.3-git-2.18-chrome-71.0-node-8.x-yarn-1.12-graphicsmagick-1.3.29-docker-18.06.1 + dependencies: + - setup-test-env services: - docker:stable-dind variables: @@ -623,18 +651,19 @@ gitlab:assets:compile: DOCKER_DRIVER: overlay2 DOCKER_HOST: tcp://docker:2375 script: - - date + - node --version - yarn install --frozen-lockfile --production --cache-folder .yarn-cache - - date - free -m - bundle exec rake gitlab:assets:compile - - scripts/build_assets_image + - time scripts/build_assets_image + - scripts/clean-old-cached-assets artifacts: name: webpack-report expire_in: 31d paths: - webpack-report/ - public/assets/ + <<: *assets-compile-cache only: - //@gitlab-org/gitlab-ce - //@gitlab-org/gitlab-ee @@ -788,6 +817,7 @@ qa:selectors: - bundle exec bin/qa Test::Sanity::Selectors .qa-frontend-node: &qa-frontend-node + <<: *dedicated-no-docs-no-db-pull-cache-job stage: test variables: NODE_OPTIONS: --max_old_space_size=3584 @@ -802,11 +832,6 @@ qa:selectors: - yarn install --frozen-lockfile --cache-folder .yarn-cache - date - yarn run webpack-prod - <<: *except-docs - -qa-frontend-node:6: - <<: *qa-frontend-node - image: node:6-alpine qa-frontend-node:8: <<: *qa-frontend-node @@ -854,6 +879,21 @@ lint:javascript:report: paths: - eslint-report.html +jsdoc: + <<: *dedicated-no-docs-pull-cache-job + stage: post-test + dependencies: + - compile-assets + before_script: [] + script: + - date + - yarn run jsdoc || true # ignore exit code + artifacts: + name: jsdoc + expire_in: 31d + paths: + - jsdoc/ + pages: <<: *dedicated-no-docs-no-db-pull-cache-job before_script: [] @@ -863,6 +903,7 @@ pages: - karma - gitlab:assets:compile - lint:javascript:report + - jsdoc script: - mv public/ .public/ - mkdir public/ @@ -872,6 +913,7 @@ pages: - mv webpack-report/ public/webpack-report/ || true - cp .public/assets/application-*.css public/application.css || true - cp .public/assets/application-*.css.gz public/application.css.gz || true + - mv jsdoc/ public/jsdoc/ || true artifacts: paths: - public @@ -918,6 +960,23 @@ no_ee_check: - //@gitlab-org/gitlab-ce # GitLab Review apps +review-build-cng: + <<: *review-only + image: ruby:2.5-alpine + stage: test + before_script: [] + dependencies: [] + cache: {} + variables: + GIT_DEPTH: "1" + API_TOKEN: "${GITLAB_BOT_MULTI_PROJECT_PIPELINE_POLLING_TOKEN}" + script: + - apk add --update openssl curl jq + - gem install gitlab --no-document + - source ./scripts/review_apps/review-apps.sh + - wait_for_job_to_be_done "gitlab:assets:compile" + - BUILD_TRIGGER_TOKEN=$REVIEW_APPS_BUILD_TRIGGER_TOKEN ./scripts/trigger-build cng + review-deploy: <<: *review-base retry: 2 @@ -932,15 +991,14 @@ review-deploy: <<: *review-environment on_stop: review-stop before_script: - - apk update && apk add jq - - gem install gitlab --no-document - script: - export GITLAB_SHELL_VERSION=$( + +- [ ] Mention the proposal in the next backend weekly call and the #backend channel to encourage contribution +- [ ] Proceed with the proposal once 50% of the maintainers have weighed in, and 80% of the votes are :+1: +- [ ] Once approved, mention it again in the next backend weekly call and the #backend channel + + +/label ~"development guidelines" +/label ~"Style decision" +/label ~Documentation + +/cc @gitlab-org/maintainers/rails-backend diff --git a/.gitlab/issue_templates/Feature proposal.md b/.gitlab/issue_templates/Feature proposal.md index 639a236631..1bb8d33ff6 100644 --- a/.gitlab/issue_templates/Feature proposal.md +++ b/.gitlab/issue_templates/Feature proposal.md @@ -4,7 +4,30 @@ ### Target audience - + ### Further details @@ -12,12 +35,12 @@ ### Proposal - + ### What does success look like, and how can we measure that? - + ### Links / references -/label ~"feature proposal" +/label ~feature diff --git a/.gitlab/issue_templates/Security Release.md b/.gitlab/issue_templates/Security Release.md new file mode 100644 index 0000000000..ae469d3b12 --- /dev/null +++ b/.gitlab/issue_templates/Security Release.md @@ -0,0 +1,69 @@ + + +## Releases tasks + +- https://gitlab.com/gitlab-org/release/docs/blob/master/general/security/release-manager.md +- https://gitlab.com/gitlab-org/release/docs/blob/master/general/security/developer.md +- https://gitlab.com/gitlab-org/release/docs/blob/master/general/security/security-engineer.md + +## Version issues: + +* 11.4.X: {release task link} +* 11.3.X: {release task link} +* 11.2.X: {release task link} + +## Security Issues: + +### CE + +* {https://gitlab.com/gitlab-org/gitlab-ce/issues link} + +### EE + +* {https://gitlab.com/gitlab-org/gitlab-ee/issues link} + +## Security Issues in dev.gitlab.org: + +### CE + +- {https://dev.gitlab.org/gitlab/gitlabhq/issues link} + +| Version | MR | +|---------|----| +| 11.4 | {https://dev.gitlab.org/gitlab/gitlabhq/merge_requests/ link} | +| 11.3 | {https://dev.gitlab.org/gitlab/gitlabhq/merge_requests/ link} | +| 11.2 | {https://dev.gitlab.org/gitlab/gitlabhq/merge_requests/ link} | +| master | {https://dev.gitlab.org/gitlab/gitlabhq/merge_requests/ link} | + + + +### EE + +* {https://dev.gitlab.org/gitlab/gitlabhq/issues/ link} + + +| Version | MR | +|---------|----| +| 11.4| {https://dev.gitlab.org/gitlab/gitlab-ee/merge_requests/ link} | +| 11.3 | {https://dev.gitlab.org/gitlab/gitlab-ee/merge_requests/ link} | +| 11.2 | {https://dev.gitlab.org/gitlab/gitlab-ee/merge_requests/ link} | +| master | {https://dev.gitlab.org/gitlab/gitlab-ee/merge_requests/ link} | + + +## QA +{QA issue link} + +## Blog post + +Dev: {https://dev.gitlab.org/gitlab/www-gitlab-com/merge_requests/ link}
+gitlab.com: {https://gitlab.com/gitlab-com/www-gitlab-com/merge_requests/ link} + +## Email notification +{https://gitlab.com/gitlab-com/marketing/general/issues/ link} + +/label ~security +/confidential diff --git a/.gitlab/issue_templates/Security developer workflow.md b/.gitlab/issue_templates/Security developer workflow.md index 08651195d9..aaa1614539 100644 --- a/.gitlab/issue_templates/Security developer workflow.md +++ b/.gitlab/issue_templates/Security developer workflow.md @@ -3,30 +3,26 @@ Create this issue under https://dev.gitlab.org/gitlab/gitlabhq -Set the title to: `[Security] Description of the original issue` +Set the title to: `Description of the original issue` --> -### Prior to the security release +### Prior to starting the security release work - [ ] Read the [security process for developers] if you are not familiar with it. - [ ] Link to the original issue adding it to the [links section](#links) - [ ] Run `scripts/security-harness` in the CE, EE, and/or Omnibus to prevent pushing to any remote besides `dev.gitlab.org` -- [ ] Create an MR targetting `org` `master`, prefixing your branch with `security-` -- [ ] Label your MR with the ~security label, prefix the title with `WIP: [master]` -- [ ] Add a link to the MR to the [links section](#links) -- [ ] Add a link to an EE MR if required -- [ ] Make sure the MR remains in-progress and gets approved after the review cycle, **but never merged**. -- [ ] Add a link to this issue on the original security issue. +- [ ] Create a new branch prefixing it with `security-` +- [ ] Create a MR targeting `dev.gitlab.org` `master` +- [ ] Add a link to this issue in the original security issue on `gitlab.com`. #### Backports -- [ ] Once the MR is ready to be merged, create MRs targetting the last 3 releases +- [ ] Once the MR is ready to be merged, create MRs targetting the last 3 releases, plus the current RC if between the 7th and 22nd of the month. - [ ] At this point, it might be easy to squash the commits from the MR into one - You can use the script `bin/secpick` instead of the following steps, to help you cherry-picking. See the [secpick documentation] - - [ ] Create the branch `security-X-Y` from `X-Y-stable` if it doesn't exist (and make sure it's up to date with stable) - - [ ] Create each MR targetting the security branch `security-X-Y` - - [ ] Add the ~security label and prefix with the version `WIP: [X.Y]` the title of the MR -- [ ] Add the ~"Merge into Security" label to all of the MRs. + - [ ] Create each MR targetting the stable branch `X-Y-stable`, using the "Security Release" merge request template. + - Every merge request will have its own set of TODOs, so make sure to + complete those. - [ ] Make sure all MRs have a link in the [links section](#links) [secpick documentation]: https://gitlab.com/gitlab-org/release/docs/blob/master/general/security/developer.md#secpick-script diff --git a/.gitlab/merge_request_templates/Security Release.md b/.gitlab/merge_request_templates/Security Release.md new file mode 100644 index 0000000000..246f2dae00 --- /dev/null +++ b/.gitlab/merge_request_templates/Security Release.md @@ -0,0 +1,31 @@ + +## Related issues + + + +## Developer checklist + +- [ ] Link to the developer security workflow issue on `dev.gitlab.org` +- [ ] MR targets `master`, or `X-Y-stable` for backports +- [ ] Milestone is set for the version this MR applies to +- [ ] Title of this MR is the same as for all backports +- [ ] A [CHANGELOG entry](https://docs.gitlab.com/ee/development/changelog.html) is added without a `merge_request` value, with `type` set to `security` +- [ ] Add a link to this MR in the `links` section of related issue +- [ ] Add a link to an EE MR if required +- [ ] Assign to a reviewer + +## Reviewer checklist + +- [ ] Correct milestone is applied and the title is matching across all backports +- [ ] Assigned to `@gitlab-release-tools-bot` with passing CI pipelines + +/label ~security diff --git a/.rubocop.yml b/.rubocop.yml index e8e550fdbd..bcff67ded8 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -143,6 +143,7 @@ Naming/FileName: - XMPP - XSRF - XSS + - GRPC # GitLab ################################################################### diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 847a0f74aa..77ad4753c8 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -15,12 +15,6 @@ Capybara/CurrentPathExpectation: Layout/EmptyLinesAroundArguments: Enabled: false -# Offense count: 253 -# Cop supports --auto-correct. -# Configuration parameters: AllowForAlignment, ForceEqualSignAlignment. -Layout/ExtraSpacing: - Enabled: false - # Offense count: 83 # Cop supports --auto-correct. # Configuration parameters: EnforcedStyle, IndentationWidth. @@ -86,11 +80,6 @@ Lint/InterpolationCheck: Lint/MissingCopEnableDirective: Enabled: false -# Offense count: 1 -Lint/ReturnInVoidContext: - Exclude: - - 'app/models/project.rb' - # Offense count: 9 Lint/UriEscapeUnescape: Exclude: @@ -443,11 +432,6 @@ Style/LineEndConcatenation: - 'spec/lib/gitlab/gfm/reference_rewriter_spec.rb' - 'spec/lib/gitlab/incoming_email_spec.rb' -# Offense count: 39 -# Cop supports --auto-correct. -Style/MethodCallWithoutArgsParentheses: - Enabled: false - # Offense count: 18 Style/MethodMissing: Enabled: false @@ -686,17 +670,6 @@ Style/TrailingUnderscoreVariable: - 'spec/lib/gitlab/etag_caching/middleware_spec.rb' - 'spec/services/quick_actions/interpret_service_spec.rb' -# Offense count: 5 -# Cop supports --auto-correct. -# Configuration parameters: ExactNameMatch, AllowPredicates, AllowDSLWriters, IgnoreClassMethods, Whitelist. -# Whitelist: to_ary, to_a, to_c, to_enum, to_h, to_hash, to_i, to_int, to_io, to_open, to_path, to_proc, to_r, to_regexp, to_str, to_s, to_sym -Style/TrivialAccessors: - Exclude: - - 'app/models/external_issue.rb' - - 'app/serializers/base_serializer.rb' - - 'lib/gitlab/auth/ldap/person.rb' - - 'lib/system_check/base_check.rb' - # Offense count: 4 # Cop supports --auto-correct. Style/UnlessElse: diff --git a/CHANGELOG.md b/CHANGELOG.md index 8890a82932..feda5e0835 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,253 @@ documentation](doc/development/changelog.md) for instructions on adding your own entry. +## 11.8.0 (2019-02-22) + +### Security (7 changes, 1 of them is from the community) + +- Sanitize user full name to clean up any URL to prevent mail clients from auto-linking URLs. !2793 +- Update Helm to 2.12.2 to address Helm client vulnerability. !24418 (Takuya Noguchi) +- Use sanitized user status message for user popover. +- Validate bundle files before unpacking them. +- Alias GitHub and BitBucket OAuth2 callback URLs. +- Fixed XSS content in KaTex links. +- Disallows unauthorized users from accessing the pipelines section. + +### Removed (2 changes, 1 of them is from the community) + +- Removed deprecated Redcarpet markdown engine. +- Remove Cancel all jobs button in general jobs list view. (Jordi Llull) + +### Fixed (84 changes, 20 of them are from the community) + +- Fix ambiguous brackets in task lists. !18514 (Jared Deckard ) +- Fix lost line number when navigating to a specific line in a protected file before authenticating. !19165 (Scott Escue) +- Fix suboptimal handling of checkbox and radio input events causing group general settings submit button to stay disabled after changing its visibility. !23022 +- Fix upcoming milestones filter not including group milestones. !23098 (Heinrich Lee Yu) +- Update runner admin page to make description field larger. !23593 (Sascha Reynolds) +- Fix Bitbucket Server import not allowing personal projects. !23601 +- Fix bug causing repository mirror settings UI to break. !23712 +- Fix foreground color for labels to ensure consistency of label appearance. !23873 (Nathan Friend) +- Resolve In Merge Request diff screen, master is not a hyperlink. !23874 +- Show the correct error page when access is denied. !23932 +- Increase reliability and performance of toggling task items. !23938 +- Modify file restore to rectify tar issue. !24000 +- Fix default visibility_level for new projects. !24120 (Fabian Schneider @fabsrc) +- Footnotes now render properly in markdown. !24168 +- Emoji and cancel button are taller than input in set user status modal. !24173 (Dhiraj Bodicherla) +- Adjusts duplicated line when commenting on unfolded diff lines (in the bottom). !24201 +- Adjust height of "Add list" dropdown in issue boards. !24227 +- Improves restriction of multiple Kubernetes clusters through API. !24251 +- Fix files/blob api endpoints content disposition. !24267 +- Cleanup stale +deleted repo paths on project removal (adjusts project removal bug). !24269 +- Handle regular job dependencies next to parallelized job dependencies. !24273 +- Proper align Projects dropdown on issue boards page. !24277 (Johann Hubert Sonntagbauer) +- Resolve When merging an MR, the squash checkbox isnt always supported. !24296 +- Fix Bitbucket Server importer error handling. !24343 +- Fix syntax highlighting for suggested changes preview. !24358 +- API: Support dots in wiki slugs. !24383 (Robert Schilling) +- Show CI artifact file size with 3 significant digits on 'browse job artifacts' page. !24387 +- API: Support username with dots. !24395 (Robert Schilling) +- API: Fix default_branch_protection admin setting. !24398 (Robert Schilling) +- Remove unwanted margin above suggested changes. !24419 +- Prevent checking protected_ref? for ambiguous refs. !24437 +- Update metrics environment dropdown to show complete option set. !24441 +- Fix empty labels of CI builds for gitlab-pages on pipeline page. !24451 +- Do not run spam checks on confidential issues. !24453 +- Upgrade KaTeX to version 0.10.0. !24478 (Andrew Harmon) +- Avoid overwriting default jaeger values with nil. !24482 +- Display SAML failure messages instead of expecting CSRF token. !24509 +- Adjust vertical alignment for project visibility icons. !24511 (Martin Hobert) +- Load initUserInternalRegexPlaceholder only when required. !24522 +- Hashed Storage: `AfterRenameService` was receiving the wrong `old_path` under some circunstances. !24526 +- Resolve Runners IPv6 address overlaps other values. !24531 +- Fix 404s with snippet uploads in object storage. !24550 +- Fixed oversized custom project notification selector dropdown. !24557 +- Allow users with full private access to read private personal snippets. !24560 +- Resolve Pipeline stages job action button icon is not aligned. !24577 +- Fix cluster page non-interactive on form validation error. !24583 +- Fix 404s for snippet uploads when relative URL root used. !24588 +- Fix markdown table border. !24601 +- Fix CSS grid on a new Project/Group Milestone. !24614 (Takuya Noguchi) +- Prevent unload when Recaptcha is open. !24625 +- Clean up unicorn sampler metric labels. !24626 (bjk-gitlab) +- Support bamboo api polymorphism. !24680 (Alex Lossent) +- Ensure Cert Manager works with Auto DevOps URLs greater than 64 bytes. !24683 +- Fix failed LDAP logins when nil user_id present. !24749 +- fix display comment avatars issue in IE 11. !24777 (Gokhan Apaydin) +- Fix template labels not being created on new projects. !24803 +- Fix cluster installation processing spinner. !24814 +- Append prioritized label before pagination. !24815 +- Resolve UI bug adding group members with lower permissions. !24820 +- Make `ActionController::Parameters` serializable for sidekiq jobs. !24864 +- Fix Jira Service password validation on project integration services. !24896 (Daniel Juarez) +- Fix potential Addressable::URI::InvalidURIError. !24908 +- Update Workhorse to v8.2.0. !24909 +- Encode Content-Disposition filenames. !24919 +- Avoid race conditions when creating GpgSignature. !24939 +- Create the source branch for a GitHub import. !25064 +- Fix suggested changes syntax highlighting. !25116 +- Fix counts in milestones dashboard. !25230 +- Fixes incorrect TLD validation errors for Kubernetes cluster domain. !25262 +- Fix 403 errors when adding an assignee list in project boards. !25263 +- Prevent Auto DevOps from trying to deploy without a domain name. !25308 +- Fix uninitialized constant with GitLab Pages. +- Increase line height of project summaries. (gfyoung) +- Remove extra space between MR tab bar and sticky file headers. +- Correct spacing for comparison page. +- Update CI YAML param table with include. +- Return bottom border on MR Tabs. +- Fixes z-index and margins of archived alert in job page. +- Fixes archived sticky top bar without perfomance bar. +- Fixed rebase button not showing in merge request widget. +- Fixed double tooltips on note awards buttons. +- Allow suggestions to be copied and pasted as GFM. +- Fix bug that caused Suggestion Markdown toolbar button to insert snippet with leading +/-/. +- Moved primary button for labels to follow the design patterns used on rest of the site. (Martin Hobert) + +### Changed (37 changes, 11 of them are from the community) + +- Change spawning of tooltips to be top by default. !21223 +- Standardize filter value capitlization in filter bar in both issues and boards pages. !23846 (obahareth) +- Refresh group overview to match project overview. !23866 +- Build number does not need to be tweaked anymore for the TeamCity integration to work properly. !23898 +- Added empty project illustration and updated text to user profile overview. !23973 (Fernando Arias) +- Modified Knative list view to provide more details. !24072 (Chris Baumbauer) +- Move cancel & new issue button on job page. !24074 +- Make issuable empty states actionable. !24077 +- Fix code search when text is larger than max gRPC message size. !24111 +- Update string structure for available group runners. !24187 (George Tsiolis) +- Remove multilingual translation from the word "in" in the job details sidebar. !24192 (Nathan Friend) +- Fix duplicate project disk path in BackfillLegacyProjectRepositories. !24213 +- Ensured links to a comment or system note anchor resolves to the right note if a user has a discussion filter. !24228 +- Remove expansion hover animation from pipeline status icon buttons. !24268 (Nathan Friend) +- Redesigned related merge requests in issue page. !24270 +- Return the maximum group access level in the projects API. !24403 +- Update project topics styling to use badges design. !24415 +- Display "commented" only for commit discussions on merge requests. !24427 +- Upgrade js-regex gem to version 3.1. !24433 (rroger) +- Prevent Sidekiq arguments over 10 KB in size from being logged to JSON. !24493 +- Added Avatar in the settings sidebar. !24515 (Yoginth) +- Refresh empty states for profile page tabs. !24549 +- remove red/green colors from diff view of no-color syntax theme. !24582 (khm) +- Get remote IP address of runner. !24624 +- Update last_activity_on for Users on some main GET endpoints. !24642 +- Update metrics dashboard graph design. !24653 +- Update to GitLab SVG icon from Font Awesome in profile for location and work. !24671 (Yoginth) +- Add template for Android with Fastlane. !24722 +- Display timestamps to messages printed by gitlab:backup:restore rake tasks. (Will Chandler) +- Show MR statistics in diff comparisons. +- Make possible to toggle file tree while scrolling through diffs. +- Use delete instead of remove when referring to `git branch -D`. +- Add folder header to files in merge request tree list. +- Added fuzzy file finder to merge requests. +- Collapse directory structure in merge request file tree. +- Adds skeleton loading to releases page. +- Support multiple outputs in jupyter notebooks. + +### Performance (8 changes, 1 of them is from the community) + +- Remove unused button classes `btn-create` and `comment-btn`. !23232 (George Tsiolis) +- [API] Omit `X-Total` and `X-Total-Pages` headers when items count is more than 10,000. !23931 +- Improve efficiency of GitHub importer by reducing amount of locks needed. !24102 +- Improve milestone queries using subqueries instead of separate queries for ids. !24325 +- Efficiently remove expired artifacts in `ExpireBuildArtifactsWorker`. !24450 +- Eliminate N+1 queries in /api/groups/:id. !24513 +- Use deployment relation to get an environment name. !24890 +- Do not reload daemon if configuration file of pages does not change. + +### Added (35 changes, 18 of them are from the community) + +- Add badge count to projects. !18425 (George Tsiolis) +- API: Add support for group labels. !21368 (Robert Schilling) +- Add setting for first day of the week. !22755 (Fabian Schneider @fabsrc) +- Pages for subgroups. !23505 +- Add support for customer provided encryption keys for Amazon S3 remote backups. !23797 (Pepijn Van Eeckhoudt) +- Add Knative detailed view. !23863 (Chris Baumbauer) +- Add group full path to project's shared_with_groups. !24052 (Mathieu Parent) +- Added feature to specify a custom Auto DevOps chart repository. !24162 (walkafwalka) +- Add flat-square badge style. !24172 (Fabian Schneider @fabsrc) +- Display last activity and created at datetimes for users. !24181 +- Allow setting of feature gates per project. !24184 +- Save issues/merge request sorting options to backend. !24198 +- Added support for custom hosts/domains to Auto DevOps. !24248 (walkafwalka) +- Adds milestone search. !24265 (Jacopo Beschi @jacopo-beschi) +- Allow merge request diffs to be placed into an object store. !24276 +- Add Container Registry API with cleanup function. !24303 +- GitLab now supports the profile and email scopes from OpenID Connect. !24335 (Goten Xiao) +- Add 'in' filter that modifies scope of 'search' filter to issues and merge requests API. !24350 (Hiroyuki Sato) +- Add `with_programming_language` filter for projects to API. !24377 (Dylan MacKenzie) +- API: Support searching for tags. !24385 (Robert Schilling) +- Document graphicsmagick installation for source installation. !24404 (Alexis Reigel) +- Redirect GET projects/:id to project page. !24467 +- Indicate on Issue Status if an Issue was Moved. !24470 +- Redeploy Auto DevOps deployment on variable updates. !24498 (walkafwalka) +- Don't create new merge request pipeline without commits. !24503 (Hiroyuki Sato) +- Add GitLab Pages predefined CI variables 'CI_PAGES_DOMAIN' and 'CI_PAGES_URL'. !24504 (Adrian Moisey) +- Moves domain setting from Auto DevOps to Cluster's page. !24580 +- API allows setting the squash commit message when squashing a merge request. !24784 +- Added ability to upgrade cluster applications. !24789 +- Add argument iids for issues in GraphQL. !24802 +- Add repositories count to usage ping data. !24823 +- Add support for extensionless pages URLs. !24876 +- Add templates for most popular Pages templates. !24906 +- Introduce Internal API for searching environment names. !24923 +- Allow admins to invalidate markdown texts by setting local markdown version. + +### Other (50 changes, 18 of them are from the community) + +- Externalize strings from `/app/views/projects/project_members`. !23227 (Tao Wang) +- Add CSS & JS global flags to represent browser and platform. !24017 +- Fix deprecation: Passing an argument to force an association to reload is now deprecated. !24136 (Jasper Maes) +- Cleanup legacy artifact background migration. !24144 +- Bump kubectl in Auto DevOps to 1.11.6. !24176 +- Conditionally initialize the global opentracing tracer. !24186 +- Remove horizontal whitespace on user profile overview on small breakpoints. !24189 +- Bump nginx-ingress chart to 1.1.2. !24203 +- Use monospace font for registry table tag id and tag name. !24205 +- Rename project tags to project topics. !24219 +- Add uniqueness validation to url column in Releases::Link model. !24223 +- Update sidekiq-cron to 1.0.4 and use fugit to replace rufus-scheduler to parse cron syntax. !24235 +- Adds inter-service OpenTracing propagation. !24239 +- Fixes Auto DevOps title on CI/CD admin settings. !24249 +- Upgrade kubeclient to 4.2.2 and swap out monkey-patch to disallow redirects. !24284 +- i18n: externalize strings from 'app/views/search'. !24297 (Tao Wang) +- Fix several ActionController::Parameters deprecations. !24332 (Jasper Maes) +- Remove all `$theme-gray-{weight}` variables in favor of `$gray-{weight}`. !24333 (George Tsiolis) +- Update gitlab-styles to 2.5.1. !24336 (Jasper Maes) +- Modifies environment scope UI on cluster page. !24376 +- Extract process_name from GitLab::Sentry. !24422 +- Upgrade Gitaly to 1.13.0. !24429 +- Actually set raise_on_unfiltered_parameters to true. !24443 (Jasper Maes) +- Refactored NoteableDiscussion by extracting ResolveDiscussionButton. !24505 (Martin Hobert) +- Extracted JumpToNextDiscussionButton to its own component. !24506 (Martin Hobert) +- Extracted ReplyPlaceholder to its own component. !24507 (Martin Hobert) +- Block emojis and symbol characters from users full names. !24523 +- Update GitLab Runner Helm Chart to 0.1.45. !24564 +- Updated docs for fields in pushing mirror from GitLab to GitHub. !24566 (Joseph Yu) +- Upgrade gitlab-workhorse to 8.1.0. !24571 +- Externalize strings from `/app/views/sent_notifications`. !24576 (George Tsiolis) +- Adds tracing support for ActiveRecord notifications. !24604 +- Externalize strings from `/app/views/projects/ci`. !24617 (George Tsiolis) +- Move permission check of manual actions of deployments. !24660 +- Externalize strings from `/app/views/clusters`. !24666 (George Tsiolis) +- Update UI for admin appearance settings. !24685 +- Externalize strings from `/app/views/projects/pages_domains`. !24723 (George Tsiolis) +- Externalize strings from `/app/views/projects/milestones`. !24726 (George Tsiolis) +- Add OpenTracing instrumentation for Action View Render events. !24728 +- Expose version for each application in cluster_status JSON endpoint. !24791 +- Externalize strings from `/app/views/instance_statistics`. !24809 (George Tsiolis) +- Update cluster application version on updated and installed status. !24810 +- Project list UI improvements. !24855 +- Externalize strings from `/app/views/email_rejection_mailer`. !24869 (George Tsiolis) +- Update Gitaly to v1.17.0. !24873 +- Update Workhorse to v8.3.0. !24959 +- Upgrade gitaly to 1.18.0. !24981 +- Update Workhorse to v8.3.1. +- Upgraded Codesandbox smooshpack package. +- Creates mixin to reduce code duplication between CE and EE in graph component. + + ## 11.7.5 (2019-02-06) ### Fixed (8 changes) @@ -16,19 +263,15 @@ entry. - Changed external wiki query method to prevent attribute caching. !24907 -## 11.7.4 (2019-02-04) - -### Security (1 change) - -- Use sanitized user status message for user popover. - - -## 11.7.3 (2019-01-30) - -- No changes. - ## 11.7.2 (2019-01-29) +### Fixed (1 change) + +- Fix uninitialized constant with GitLab Pages. + + +## 11.7.1 (2019-01-28) + ### Security (24 changes) - Make potentially malicious links more visible in the UI and scrub RTLO chars from links. !2770 @@ -56,14 +299,6 @@ entry. - Notify only users who can access the project on project move. - Alias GitHub and BitBucket OAuth2 callback URLs. -### Fixed (1 change) - -- Fix uninitialized constant with GitLab Pages. - - -## 11.7.1 (2019-01-28) - -- Unreleased due to quality assurance failure. ## 11.7.0 (2019-01-22) @@ -251,6 +486,28 @@ entry. - Update url placeholder for the sentry configuration page. !24338 +## 11.6.8 (2019-01-30) + +- No changes. + +## 11.6.5 (2019-01-17) + +### Fixed (5 changes) + +- Add syntax highlighting to suggestion diff. !24156 +- Fix broken templated "Too many changes to show" text. !24282 +- Fix requests profiler in admin page not rendering HTML properly. !24291 +- Fix no avatar not showing in user selection box. !24346 +- Fixed diff suggestions removing dashes. + + +## 11.6.4 (2019-01-15) + +### Security (1 change) + +- Validate bundle files before unpacking them. + + ## 11.6.3 (2019-01-04) ### Fixed (1 change) @@ -573,6 +830,33 @@ entry. - Enable Rubocop on lib/gitlab. (gfyoung) +## 11.5.8 (2019-01-28) + +### Security (21 changes) + +- Make potentially malicious links more visible in the UI and scrub RTLO chars from links. !2770 +- Don't process MR refs for guests in the notes. !2771 +- Fixed XSS content in KaTex links. +- Verify that LFS upload requests are genuine. +- Extract GitLab Pages using RubyZip. +- Prevent awarding emojis to notes whose parent is not visible to user. +- Prevent unauthorized replies when discussion is locked or confidential. +- Disable git v2 protocol temporarily. +- Fix showing ci status for guest users when public pipline are not set. +- Fix contributed projects info still visible when user enable private profile. +- Disallows unauthorized users from accessing the pipelines section. +- Add more LFS validations to prevent forgery. +- Use common error for unauthenticated users when creating issues. +- Fix slow regex in project reference pattern. +- Fix private user email being visible in push (and tag push) webhooks. +- Fix wiki access rights when external wiki is enabled. +- Fix path disclosure on project import error. +- Restrict project import visibility based on its group. +- Expose CI/CD trigger token only to the trigger owner. +- Notify only users who can access the project on project move. +- Alias GitHub and BitBucket OAuth2 callback URLs. + + ## 11.5.5 (2018-12-20) ### Security (1 change) diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index 6b89d58f86..3989355915 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -1.12.2 +1.20.0 diff --git a/GITLAB_PAGES_VERSION b/GITLAB_PAGES_VERSION index 3a3cd8cc8b..bc80560fad 100644 --- a/GITLAB_PAGES_VERSION +++ b/GITLAB_PAGES_VERSION @@ -1 +1 @@ -1.3.1 +1.5.0 diff --git a/GITLAB_WORKHORSE_VERSION b/GITLAB_WORKHORSE_VERSION index 8b22a322d0..56b6be4ebb 100644 --- a/GITLAB_WORKHORSE_VERSION +++ b/GITLAB_WORKHORSE_VERSION @@ -1 +1 @@ -8.0.2 +8.3.1 diff --git a/Gemfile b/Gemfile index f59e61208a..a3b01c275c 100644 --- a/Gemfile +++ b/Gemfile @@ -16,7 +16,7 @@ gem 'gitlab-default_value_for', '~> 3.1.1', require: 'default_value_for' # Supported DBs gem 'mysql2', '~> 0.4.10', group: :mysql -gem 'pg', '~> 0.18.2', group: :postgres +gem 'pg', '~> 1.1', group: :postgres gem 'rugged', '~> 0.27' gem 'grape-path-helpers', '~> 1.0' @@ -113,10 +113,9 @@ gem 'seed-fu', '~> 2.3.7' # Markdown and HTML processing gem 'html-pipeline', '~> 2.8' -gem 'deckar01-task_list', '2.0.0' +gem 'deckar01-task_list', '2.2.0' gem 'gitlab-markup', '~> 1.6.5' gem 'github-markup', '~> 1.7.0', require: 'github/markup' -gem 'redcarpet', '~> 3.4' gem 'commonmarker', '~> 0.17' gem 'RedCloth', '~> 4.3.2' gem 'rdoc', '~> 6.0' @@ -126,9 +125,9 @@ gem 'wikicloth', '0.8.1' gem 'asciidoctor', '~> 1.5.8' gem 'asciidoctor-plantuml', '0.0.8' gem 'rouge', '~> 3.1' -gem 'truncato', '~> 0.7.9' +gem 'truncato', '~> 0.7.11' gem 'bootstrap_form', '~> 2.7.0' -gem 'nokogiri', '~> 1.8.5' +gem 'nokogiri', '~> 1.10.1' gem 'escape_utils', '~> 1.1' # Calendar rendering @@ -161,12 +160,12 @@ gem 'acts-as-taggable-on', '~> 5.0' # Background jobs gem 'sidekiq', '~> 5.2.1' -gem 'sidekiq-cron', '~> 0.6.0' +gem 'sidekiq-cron', '~> 1.0' gem 'redis-namespace', '~> 1.6.0' gem 'gitlab-sidekiq-fetcher', '~> 0.4.0', require: 'sidekiq-reliable-fetch' # Cron Parser -gem 'rufus-scheduler', '~> 3.4' +gem 'fugit', '~> 1.1' # HTTP requests gem 'httparty', '~> 0.13.3' @@ -188,7 +187,7 @@ gem 're2', '~> 1.1.1' gem 'version_sorter', '~> 2.1.0' # Export Ruby Regex to Javascript -gem 'js_regex', '~> 2.2.1' +gem 'js_regex', '~> 3.1' # User agent parsing gem 'device_detector' @@ -225,7 +224,7 @@ gem 'asana', '~> 0.8.1' gem 'ruby-fogbugz', '~> 0.2.1' # Kubernetes integration -gem 'kubeclient', '~> 4.0.0' +gem 'kubeclient', '~> 4.2.2' # Sanitize user input gem 'sanitize', '~> 4.6' @@ -305,6 +304,12 @@ group :metrics do gem 'raindrops', '~> 0.18' end +group :tracing do + # OpenTracing + gem 'opentracing', '~> 0.4.3' + gem 'jaeger-client', '~> 0.10.0' +end + group :development do gem 'foreman', '~> 0.84.0' gem 'brakeman', '~> 4.2', require: false @@ -417,7 +422,7 @@ group :ed25519 do end # Gitaly GRPC client -gem 'gitaly-proto', '~> 1.5.0', require: 'gitaly' +gem 'gitaly-proto', '~> 1.10.0', require: 'gitaly' gem 'grpc', '~> 1.15.0' gem 'google-protobuf', '~> 3.6' diff --git a/Gemfile.lock b/Gemfile.lock index 77b4360cf4..0b2bd2c96b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -113,6 +113,7 @@ GEM activesupport (>= 4.0.0) mime-types (>= 1.16) cause (0.1) + character_set (1.1.2) charlock_holmes (0.7.6) childprocess (0.9.0) ffi (~> 1.0, >= 1.0.11) @@ -143,7 +144,7 @@ GEM database_cleaner (1.7.0) debug_inspector (0.0.3) debugger-ruby_core_source (1.3.8) - deckar01-task_list (2.0.0) + deckar01-task_list (2.2.0) html-pipeline declarative (0.0.10) declarative-option (0.1.0) @@ -185,7 +186,7 @@ GEM erubi (1.7.1) erubis (2.7.0) escape_utils (1.2.1) - et-orbi (1.0.3) + et-orbi (1.1.7) tzinfo eventmachine (1.2.7) excon (0.62.0) @@ -206,7 +207,7 @@ GEM fast_blank (1.0.0) fast_gettext (1.6.0) ffaker (2.10.0) - ffi (1.9.25) + ffi (1.10.0) flipper (0.13.0) flipper-active_record (0.13.0) activerecord (>= 3.2, < 6) @@ -258,6 +259,9 @@ GEM foreman (0.84.0) thor (~> 0.19.1) formatador (0.2.5) + fugit (1.1.7) + et-orbi (~> 1.1, >= 1.1.7) + raabro (~> 1.1) fuubar (2.2.0) rspec-core (~> 3.0) ruby-progressbar (~> 1.4) @@ -274,7 +278,7 @@ GEM gettext_i18n_rails (>= 0.7.1) po_to_json (>= 1.0.0) rails (>= 3.2.0) - gitaly-proto (1.5.0) + gitaly-proto (1.10.0) grpc (~> 1.0) github-markup (1.7.0) gitlab-default_value_for (3.1.1) @@ -282,7 +286,7 @@ GEM gitlab-markup (1.6.5) gitlab-sidekiq-fetcher (0.4.0) sidekiq (~> 5) - gitlab-styles (2.4.1) + gitlab-styles (2.5.1) rubocop (~> 0.54.0) rubocop-gitlab-security (~> 0.1.0) rubocop-rspec (~> 1.19) @@ -389,13 +393,18 @@ GEM cause json ipaddress (0.8.3) + jaeger-client (0.10.0) + opentracing (~> 0.3) + thrift jira-ruby (1.4.1) activesupport multipart-post oauth (~> 0.5, >= 0.5.0) jquery-atwho-rails (1.3.2) - js_regex (2.2.1) - regexp_parser (>= 0.4.11, <= 0.5.0) + js_regex (3.1.1) + character_set (~> 1.1) + regexp_parser (~> 1.1) + regexp_property_values (~> 0.3) json (1.8.6) json-jwt (1.9.4) activesupport @@ -419,7 +428,7 @@ GEM kgio (2.10.0) knapsack (1.17.0) rake - kubeclient (4.0.0) + kubeclient (4.2.2) http (~> 3.0) recursive-open-struct (~> 1.0, >= 1.0.4) rest-client (~> 2.0) @@ -462,9 +471,9 @@ GEM mimemagic (0.3.2) mini_magick (4.8.0) mini_mime (1.0.1) - mini_portile2 (2.3.0) + mini_portile2 (2.4.0) minitest (5.11.3) - msgpack (1.2.4) + msgpack (1.2.6) multi_json (1.13.1) multi_xml (0.6.0) multipart-post (2.0.0) @@ -477,8 +486,8 @@ GEM net-ssh (5.0.1) netrc (0.11.0) nio4r (2.3.1) - nokogiri (1.8.5) - mini_portile2 (~> 2.3.0) + nokogiri (1.10.1) + mini_portile2 (~> 2.4.0) nokogumbo (1.5.0) nokogiri numerizer (0.1.1) @@ -544,6 +553,8 @@ GEM activesupport nokogiri (>= 1.4.4) omniauth (~> 1.0) + opentracing (0.4.3) + optimist (3.0.0) org-ruby (0.9.12) rubypants (~> 0.2) orm_adapter (0.5.0) @@ -575,7 +586,7 @@ GEM atomic (>= 1.0.0) peek redis - pg (0.18.4) + pg (1.1.3) po_to_json (1.0.1) json (>= 1.6.0) powerpack (0.1.1) @@ -606,6 +617,7 @@ GEM get_process_mem (~> 0.2) puma (>= 2.7, < 4) pyu-ruby-sasl (0.0.3.3) + raabro (1.1.6) rack (2.0.6) rack-accept (0.4.5) rack (>= 0.4) @@ -618,7 +630,7 @@ GEM httpclient (>= 2.4) multi_json (>= 1.3.6) rack (>= 1.1) - rack-protection (2.0.4) + rack-protection (2.0.5) rack rack-proxy (0.6.0) rack @@ -664,16 +676,15 @@ GEM ffi (>= 0.5.0, < 2) rblineprof (0.3.6) debugger-ruby_core_source (~> 1.3) - rbtrace (0.4.10) + rbtrace (0.4.11) ffi (>= 1.0.6) msgpack (>= 0.4.3) - trollop (>= 1.16.2) + optimist (>= 3.0.0) rdoc (6.0.4) re2 (1.1.1) recaptcha (3.0.0) json recursive-open-struct (1.1.0) - redcarpet (3.4.0) redis (3.3.5) redis-actionpack (5.0.2) actionpack (>= 4.0, < 6) @@ -693,7 +704,8 @@ GEM redis-store (>= 1.2, < 2) redis-store (1.6.0) redis (>= 2.2, < 5) - regexp_parser (0.5.0) + regexp_parser (1.3.0) + regexp_property_values (0.3.4) representable (3.0.4) declarative (< 0.1.0) declarative-option (< 0.2.0) @@ -775,8 +787,6 @@ GEM rubyntlm (0.6.2) rubypants (0.2.0) rubyzip (1.2.2) - rufus-scheduler (3.4.0) - et-orbi (~> 1.0) rugged (0.27.5) safe_yaml (1.0.4) sanitize (4.6.6) @@ -816,12 +826,13 @@ GEM rack shoulda-matchers (3.1.2) activesupport (>= 4.0.0) - sidekiq (5.2.3) + sidekiq (5.2.5) connection_pool (~> 2.2, >= 2.2.2) + rack (>= 1.5.0) rack-protection (>= 1.5.0) redis (>= 3.3.5, < 5) - sidekiq-cron (0.6.0) - rufus-scheduler (>= 3.3.0) + sidekiq-cron (1.0.4) + fugit (~> 1.1) sidekiq (>= 4.2.1) signet (0.11.0) addressable (~> 2.3) @@ -868,6 +879,7 @@ GEM rack (>= 1, < 3) thor (0.19.4) thread_safe (0.3.6) + thrift (0.11.0.0) tilt (2.0.8) timecop (0.8.1) timfel-krb5-auth (0.8.3) @@ -875,10 +887,9 @@ GEM parslet (~> 1.8.0) toml-rb (1.0.0) citrus (~> 3.0, > 3.0) - trollop (2.1.3) - truncato (0.7.10) + truncato (0.7.11) htmlentities (~> 4.3.1) - nokogiri (~> 1.8.0, >= 1.7.0) + nokogiri (>= 1.7.0, <= 2.0) tzinfo (1.2.5) thread_safe (~> 0.1) u2f (0.2.1) @@ -974,7 +985,7 @@ DEPENDENCIES connection_pool (~> 2.0) creole (~> 0.5.0) database_cleaner (~> 1.7.0) - deckar01-task_list (= 2.0.0) + deckar01-task_list (= 2.2.0) device_detector devise (~> 4.4) devise-two-factor (~> 3.0.0) @@ -1003,12 +1014,13 @@ DEPENDENCIES fog-rackspace (~> 0.1.1) font-awesome-rails (~> 4.7) foreman (~> 0.84.0) + fugit (~> 1.1) fuubar (~> 2.2.0) gemojione (~> 3.3) gettext (~> 3.2.2) gettext_i18n_rails (~> 1.8.0) gettext_i18n_rails_js (~> 1.3) - gitaly-proto (~> 1.5.0) + gitaly-proto (~> 1.10.0) github-markup (~> 1.7.0) gitlab-default_value_for (~> 3.1.1) gitlab-markup (~> 1.6.5) @@ -1037,14 +1049,15 @@ DEPENDENCIES httparty (~> 0.13.3) icalendar influxdb (~> 0.2) + jaeger-client (~> 0.10.0) jira-ruby (~> 1.4) jquery-atwho-rails (~> 1.3.2) - js_regex (~> 2.2.1) + js_regex (~> 3.1) json-schema (~> 2.8.0) jwt (~> 2.1.0) kaminari (~> 1.0) knapsack (~> 1.17) - kubeclient (~> 4.0.0) + kubeclient (~> 4.2.2) letter_opener_web (~> 1.3.0) license_finder (~> 5.4) licensee (~> 8.9) @@ -1059,7 +1072,7 @@ DEPENDENCIES nakayoshi_fork (~> 0.0.4) net-ldap net-ssh (~> 5.0) - nokogiri (~> 1.8.5) + nokogiri (~> 1.10.1) oauth2 (~> 1.4) octokit (~> 4.9) omniauth (~> 1.8) @@ -1077,6 +1090,7 @@ DEPENDENCIES omniauth-shibboleth (~> 1.3.0) omniauth-twitter (~> 1.4) omniauth_crowd (~> 2.2.0) + opentracing (~> 0.4.3) org-ruby (~> 0.9.12) peek (~> 1.0.1) peek-gc (~> 0.0.2) @@ -1084,7 +1098,7 @@ DEPENDENCIES peek-pg (~> 1.3.0) peek-rblineprof (~> 0.2.0) peek-redis (~> 1.2.0) - pg (~> 0.18.2) + pg (~> 1.1) premailer-rails (~> 1.9.7) prometheus-client-mmap (~> 0.9.4) pry-byebug (~> 3.5.1) @@ -1107,7 +1121,6 @@ DEPENDENCIES rdoc (~> 6.0) re2 (~> 1.1.1) recaptcha (~> 3.0) - redcarpet (~> 3.4) redis (~> 3.2) redis-namespace (~> 1.6.0) redis-rails (~> 5.0.2) @@ -1128,7 +1141,6 @@ DEPENDENCIES ruby-progressbar ruby_parser (~> 3.8) rubyzip (~> 1.2.2) - rufus-scheduler (~> 3.4) rugged (~> 0.27) sanitize (~> 4.6) sass (~> 3.5) @@ -1142,7 +1154,7 @@ DEPENDENCIES sham_rack (~> 1.3.6) shoulda-matchers (~> 3.1.2) sidekiq (~> 5.2.1) - sidekiq-cron (~> 0.6.0) + sidekiq-cron (~> 1.0) simple_po_parser (~> 1.1.2) simplecov (~> 0.14.0) slack-notifier (~> 1.5.1) @@ -1157,7 +1169,7 @@ DEPENDENCIES thin (~> 1.7.0) timecop (~> 0.8.0) toml-rb (~> 1.0.0) - truncato (~> 0.7.9) + truncato (~> 0.7.11) u2f (~> 0.2.1) uglifier (~> 2.7.2) unf (~> 0.1.4) diff --git a/PROCESS.md b/PROCESS.md index f2eed5544f..7fdac09880 100644 --- a/PROCESS.md +++ b/PROCESS.md @@ -56,7 +56,7 @@ Below we describe the contributing process to GitLab for two reasons: Several people from the [GitLab team][team] are helping community members to get their contributions accepted by meeting our [Definition of done][done]. -What you can expect from them is described at https://about.gitlab.com/roles/merge-request-coach/. +What you can expect from them is described at https://about.gitlab.com/job-families/expert/merge-request-coach/. ### Milestones on community contribution issues diff --git a/VERSION b/VERSION index 64e4bdff1c..897063bb32 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -11.7.5 +11.8.0 diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js index d1396b6c4b..85eb08cc97 100644 --- a/app/assets/javascripts/api.js +++ b/app/assets/javascripts/api.js @@ -5,6 +5,7 @@ import axios from './lib/utils/axios_utils'; const Api = { groupsPath: '/api/:version/groups.json', groupPath: '/api/:version/groups/:id', + groupMembersPath: '/api/:version/groups/:id/members', subgroupsPath: '/api/:version/groups/:id/subgroups', namespacesPath: '/api/:version/namespaces.json', groupProjectsPath: '/api/:version/groups/:id/projects.json', @@ -40,6 +41,12 @@ const Api = { }); }, + groupMembers(id) { + const url = Api.buildUrl(this.groupMembersPath).replace(':id', encodeURIComponent(id)); + + return axios.get(url); + }, + // Return groups list. Filtered by query groups(query, options, callback = $.noop) { const url = Api.buildUrl(Api.groupsPath); diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js index cace8bb9db..73ce3e760a 100644 --- a/app/assets/javascripts/awards_handler.js +++ b/app/assets/javascripts/awards_handler.js @@ -437,7 +437,7 @@ export class AwardsHandler { createAwardButtonForVotesBlock(votesBlock, emojiName) { const buttonHtml = ` - diff --git a/app/assets/javascripts/badges/components/badge_list_row.vue b/app/assets/javascripts/badges/components/badge_list_row.vue index 9051be1e10..cad5611c8c 100644 --- a/app/assets/javascripts/badges/components/badge_list_row.vue +++ b/app/assets/javascripts/badges/components/badge_list_row.vue @@ -55,7 +55,7 @@ export default { :disabled="badge.isDeleting" class="btn btn-default append-right-8" type="button" - @click="editBadge(badge);" + @click="editBadge(badge)" > @@ -65,7 +65,7 @@ export default { type="button" data-toggle="modal" data-target="#delete-badge-modal" - @click="updateBadgeInModal(badge);" + @click="updateBadgeInModal(badge)" > diff --git a/app/assets/javascripts/behaviors/gl_emoji.js b/app/assets/javascripts/behaviors/gl_emoji.js index 56293d5f96..d1d7565818 100644 --- a/app/assets/javascripts/behaviors/gl_emoji.js +++ b/app/assets/javascripts/behaviors/gl_emoji.js @@ -1,11 +1,10 @@ -import installCustomElements from 'document-register-element'; +import 'document-register-element'; import isEmojiUnicodeSupported from '../emoji/support'; -installCustomElements(window); +class GlEmoji extends HTMLElement { + constructor() { + super(); -export default function installGlEmojiElement() { - const GlEmojiElementProto = Object.create(HTMLElement.prototype); - GlEmojiElementProto.createdCallback = function createdCallback() { const emojiUnicode = this.textContent.trim(); const { name, unicodeVersion, fallbackSrc, fallbackSpriteClass } = this.dataset; @@ -43,9 +42,11 @@ export default function installGlEmojiElement() { }); } } - }; - - document.registerElement('gl-emoji', { - prototype: GlEmojiElementProto, - }); + } +} + +export default function installGlEmojiElement() { + if (!customElements.get('gl-emoji')) { + customElements.define('gl-emoji', GlEmoji); + } } diff --git a/app/assets/javascripts/behaviors/markdown/copy_as_gfm.js b/app/assets/javascripts/behaviors/markdown/copy_as_gfm.js index fe02096d90..9482a9f166 100644 --- a/app/assets/javascripts/behaviors/markdown/copy_as_gfm.js +++ b/app/assets/javascripts/behaviors/markdown/copy_as_gfm.js @@ -1,320 +1,5 @@ -/* eslint-disable object-shorthand, no-unused-vars, no-use-before-define, no-restricted-syntax, guard-for-in, no-continue */ - import $ from 'jquery'; -import _ from 'underscore'; -import { insertText, getSelectedFragment, nodeMatchesSelector } from '~/lib/utils/common_utils'; -import { placeholderImage } from '~/lazy_loader'; - -const gfmRules = { - // The filters referenced in lib/banzai/pipeline/gfm_pipeline.rb convert - // GitLab Flavored Markdown (GFM) to HTML. - // These handlers consequently convert that same HTML to GFM to be copied to the clipboard. - // Every filter in lib/banzai/pipeline/gfm_pipeline.rb that generates HTML - // from GFM should have a handler here, in reverse order. - // The GFM-to-HTML-to-GFM cycle is tested in spec/features/copy_as_gfm_spec.rb. - InlineDiffFilter: { - 'span.idiff.addition'(el, text) { - return `{+${text}+}`; - }, - 'span.idiff.deletion'(el, text) { - return `{-${text}-}`; - }, - }, - TaskListFilter: { - 'input[type=checkbox].task-list-item-checkbox'(el) { - return `[${el.checked ? 'x' : ' '}]`; - }, - }, - ReferenceFilter: { - '.tooltip'(el) { - return ''; - }, - 'a.gfm:not([data-link=true])'(el, text) { - return el.dataset.original || text; - }, - }, - AutolinkFilter: { - a(el, text) { - // Fallback on the regular MarkdownFilter's `a` handler. - if (text !== el.getAttribute('href')) return false; - - return text; - }, - }, - TableOfContentsFilter: { - 'ul.section-nav'(el) { - return '[[_TOC_]]'; - }, - }, - EmojiFilter: { - 'img.emoji'(el) { - return el.getAttribute('alt'); - }, - 'gl-emoji'(el) { - return `:${el.getAttribute('data-name')}:`; - }, - }, - ImageLinkFilter: { - 'a.no-attachment-icon'(el, text) { - return text; - }, - }, - ImageLazyLoadFilter: { - img(el, text) { - return `![${el.getAttribute('alt')}](${el.getAttribute('src')})`; - }, - }, - VideoLinkFilter: { - '.video-container'(el) { - const videoEl = el.querySelector('video'); - if (!videoEl) return false; - - return CopyAsGFM.nodeToGFM(videoEl); - }, - video(el) { - return `![${el.dataset.title}](${el.getAttribute('src')})`; - }, - }, - MermaidFilter: { - 'svg.mermaid'(el, text) { - const sourceEl = el.querySelector('text.source'); - if (!sourceEl) return false; - - return `\`\`\`mermaid\n${CopyAsGFM.nodeToGFM(sourceEl)}\n\`\`\``; - }, - 'svg.mermaid style, svg.mermaid g'(el, text) { - // We don't want to include the content of these elements in the copied text. - return ''; - }, - }, - MathFilter: { - 'pre.code.math[data-math-style=display]'(el, text) { - return `\`\`\`math\n${text.trim()}\n\`\`\``; - }, - 'code.code.math[data-math-style=inline]'(el, text) { - return `$\`${text}\`$`; - }, - 'span.katex-display span.katex-mathml'(el) { - const mathAnnotation = el.querySelector('annotation[encoding="application/x-tex"]'); - if (!mathAnnotation) return false; - - return `\`\`\`math\n${CopyAsGFM.nodeToGFM(mathAnnotation)}\n\`\`\``; - }, - 'span.katex-mathml'(el) { - const mathAnnotation = el.querySelector('annotation[encoding="application/x-tex"]'); - if (!mathAnnotation) return false; - - return `$\`${CopyAsGFM.nodeToGFM(mathAnnotation)}\`$`; - }, - 'span.katex-html'(el) { - // We don't want to include the content of this element in the copied text. - return ''; - }, - 'annotation[encoding="application/x-tex"]'(el, text) { - return text.trim(); - }, - }, - SanitizationFilter: { - 'a[name]:not([href]):empty'(el) { - return el.outerHTML; - }, - dl(el, text) { - let lines = text - .replace(/\n\n/g, '\n') - .trim() - .split('\n'); - // Add two spaces to the front of subsequent list items lines, - // or leave the line entirely blank. - lines = lines.map(l => { - const line = l.trim(); - if (line.length === 0) return ''; - - return ` ${line}`; - }); - - return `
\n${lines.join('\n')}\n
\n`; - }, - 'dt, dd, summary, details'(el, text) { - const tag = el.nodeName.toLowerCase(); - return `<${tag}>${text}\n`; - }, - 'sup, sub, kbd, q, samp, var, ruby, rt, rp, abbr'(el, text) { - const tag = el.nodeName.toLowerCase(); - return `<${tag}>${text}`; - }, - }, - SyntaxHighlightFilter: { - 'pre.code.highlight'(el, t) { - const text = t.trimRight(); - - let lang = el.getAttribute('lang'); - if (!lang || lang === 'plaintext') { - lang = ''; - } - - // Prefixes lines with 4 spaces if the code contains triple backticks - if (lang === '' && text.match(/^```/gm)) { - return text - .split('\n') - .map(l => { - const line = l.trim(); - if (line.length === 0) return ''; - - return ` ${line}`; - }) - .join('\n'); - } - - return `\`\`\`${lang}\n${text}\n\`\`\``; - }, - 'pre > code'(el, text) { - // Don't wrap code blocks in `` - return text; - }, - }, - MarkdownFilter: { - br(el) { - // Two spaces at the end of a line are turned into a BR - return ' '; - }, - code(el, text) { - let backtickCount = 1; - const backtickMatch = text.match(/`+/); - if (backtickMatch) { - backtickCount = backtickMatch[0].length + 1; - } - - const backticks = Array(backtickCount + 1).join('`'); - const spaceOrNoSpace = backtickCount > 1 ? ' ' : ''; - - return backticks + spaceOrNoSpace + text.trim() + spaceOrNoSpace + backticks; - }, - blockquote(el, text) { - return text - .trim() - .split('\n') - .map(s => `> ${s}`.trim()) - .join('\n'); - }, - img(el) { - const imageSrc = el.src; - const imageUrl = imageSrc && imageSrc !== placeholderImage ? imageSrc : el.dataset.src || ''; - return `![${el.getAttribute('alt')}](${imageUrl})`; - }, - 'a.anchor'(el, text) { - // Don't render a Markdown link for the anchor link inside a heading - return text; - }, - a(el, text) { - return `[${text}](${el.getAttribute('href')})`; - }, - li(el, text) { - const lines = text.trim().split('\n'); - const firstLine = `- ${lines.shift()}`; - // Add four spaces to the front of subsequent list items lines, - // or leave the line entirely blank. - const nextLines = lines.map(s => { - if (s.trim().length === 0) return ''; - - return ` ${s}`; - }); - - return `${firstLine}\n${nextLines.join('\n')}`; - }, - ul(el, text) { - return text; - }, - ol(el, text) { - // LIs get a `- ` prefix by default, which we replace by `1. ` for ordered lists. - return text.replace(/^- /gm, '1. '); - }, - h1(el, text) { - return `# ${text.trim()}\n`; - }, - h2(el, text) { - return `## ${text.trim()}\n`; - }, - h3(el, text) { - return `### ${text.trim()}\n`; - }, - h4(el, text) { - return `#### ${text.trim()}\n`; - }, - h5(el, text) { - return `##### ${text.trim()}\n`; - }, - h6(el, text) { - return `###### ${text.trim()}\n`; - }, - strong(el, text) { - return `**${text}**`; - }, - em(el, text) { - return `_${text}_`; - }, - del(el, text) { - return `~~${text}~~`; - }, - hr(el) { - // extra leading \n is to ensure that there is a blank line between - // a list followed by an hr, otherwise this breaks old redcarpet rendering - return '\n-----\n'; - }, - p(el, text) { - return `${text.trim()}\n`; - }, - table(el) { - const theadEl = el.querySelector('thead'); - const tbodyEl = el.querySelector('tbody'); - if (!theadEl || !tbodyEl) return false; - - const theadText = CopyAsGFM.nodeToGFM(theadEl); - const tbodyText = CopyAsGFM.nodeToGFM(tbodyEl); - - return [theadText, tbodyText].join('\n'); - }, - thead(el, text) { - const cells = _.map(el.querySelectorAll('th'), cell => { - let chars = CopyAsGFM.nodeToGFM(cell).length + 2; - - let before = ''; - let after = ''; - const alignment = cell.align || cell.style.textAlign; - - switch (alignment) { - case 'center': - before = ':'; - after = ':'; - chars -= 2; - break; - case 'right': - after = ':'; - chars -= 1; - break; - default: - break; - } - - chars = Math.max(chars, 3); - - const middle = Array(chars + 1).join('-'); - - return before + middle + after; - }); - - const separatorRow = `|${cells.join('|')}|`; - - return [text, separatorRow].join('\n'); - }, - tr(el) { - const cellEls = el.querySelectorAll('td, th'); - if (cellEls.length === 0) return false; - - const cells = _.map(cellEls, cell => CopyAsGFM.nodeToGFM(cell)); - return `| ${cells.join(' | ')} |`; - }, - }, -}; +import { getSelectedFragment } from '~/lib/utils/common_utils'; export class CopyAsGFM { constructor() { @@ -347,8 +32,24 @@ export class CopyAsGFM { e.preventDefault(); e.stopPropagation(); + const div = document.createElement('div'); + div.appendChild(el.cloneNode(true)); + const html = div.innerHTML; + clipboardData.setData('text/plain', el.textContent); - clipboardData.setData('text/x-gfm', this.nodeToGFM(el)); + clipboardData.setData('text/html', html); + // We are also setting this as fallback to transform the selection to gfm on paste + clipboardData.setData('text/x-gfm-html', html); + + CopyAsGFM.nodeToGFM(el) + .then(res => { + clipboardData.setData('text/x-gfm', res); + }) + .catch(() => { + // Not showing the error as Firefox might doesn't allow + // it or other browsers who have a time limit on the execution + // of the copy event + }); } static pasteGFM(e) { @@ -357,11 +58,28 @@ export class CopyAsGFM { const text = clipboardData.getData('text/plain'); const gfm = clipboardData.getData('text/x-gfm'); - if (!gfm) return; + const gfmHtml = clipboardData.getData('text/x-gfm-html'); + if (!gfm && !gfmHtml) return; e.preventDefault(); - window.gl.utils.insertText(e.target, (textBefore, textAfter) => { + // We have the original selection already converted to gfm + if (gfm) { + CopyAsGFM.insertPastedText(e.target, text, gfm); + } else { + // Due to the async copy call we are not able to produce gfm so we transform the cached HTML + const div = document.createElement('div'); + div.innerHTML = gfmHtml; + CopyAsGFM.nodeToGFM(div) + .then(transformedGfm => { + CopyAsGFM.insertPastedText(e.target, text, transformedGfm); + }) + .catch(() => {}); + } + } + + static insertPastedText(target, text, gfm) { + window.gl.utils.insertText(target, textBefore => { // If the text before the cursor contains an odd number of backticks, // we are either inside an inline code span that starts with 1 backtick // or a code block that starts with 3 backticks. @@ -443,75 +161,22 @@ export class CopyAsGFM { return codeElement; } - static nodeToGFM(node, respectWhitespaceParam = false) { - if (node.nodeType === Node.COMMENT_NODE) { - return ''; - } + static nodeToGFM(node) { + return Promise.all([ + import(/* webpackChunkName: 'gfm_copy_extra' */ 'prosemirror-model'), + import(/* webpackChunkName: 'gfm_copy_extra' */ './schema'), + import(/* webpackChunkName: 'gfm_copy_extra' */ './serializer'), + ]) + .then(([prosemirrorModel, schema, markdownSerializer]) => { + const { DOMParser } = prosemirrorModel; + const wrapEl = document.createElement('div'); + wrapEl.appendChild(node.cloneNode(true)); + const doc = DOMParser.fromSchema(schema.default).parse(wrapEl); - if (node.nodeType === Node.TEXT_NODE) { - return node.textContent; - } - - const respectWhitespace = - respectWhitespaceParam || (node.nodeName === 'PRE' || node.nodeName === 'CODE'); - - const text = this.innerGFM(node, respectWhitespace); - - if (node.nodeType === Node.DOCUMENT_FRAGMENT_NODE) { - return text; - } - - for (const filter in gfmRules) { - const rules = gfmRules[filter]; - - for (const selector in rules) { - const func = rules[selector]; - - if (!nodeMatchesSelector(node, selector)) continue; - - let result; - if (func.length === 2) { - // if `func` takes 2 arguments, it depends on text. - // if there is no text, we don't need to generate GFM for this node. - if (text.length === 0) continue; - - result = func(node, text); - } else { - result = func(node); - } - - if (result === false) continue; - - return result; - } - } - - return text; - } - - static innerGFM(parentNode, respectWhitespace = false) { - const nodes = parentNode.childNodes; - - const clonedParentNode = parentNode.cloneNode(true); - const clonedNodes = Array.prototype.slice.call(clonedParentNode.childNodes, 0); - - for (let i = 0; i < nodes.length; i += 1) { - const node = nodes[i]; - const clonedNode = clonedNodes[i]; - - const text = this.nodeToGFM(node, respectWhitespace); - - // `clonedNode.replaceWith(text)` is not yet widely supported - clonedNode.parentNode.replaceChild(document.createTextNode(text), clonedNode); - } - - let nodeText = clonedParentNode.innerText || clonedParentNode.textContent; - - if (!respectWhitespace) { - nodeText = nodeText.trim(); - } - - return nodeText; + const res = markdownSerializer.default.serialize(doc); + return res; + }) + .catch(() => {}); } } diff --git a/app/assets/javascripts/behaviors/markdown/editor_extensions.js b/app/assets/javascripts/behaviors/markdown/editor_extensions.js new file mode 100644 index 0000000000..47e5fc65c4 --- /dev/null +++ b/app/assets/javascripts/behaviors/markdown/editor_extensions.js @@ -0,0 +1,106 @@ +import Doc from './nodes/doc'; +import Paragraph from './nodes/paragraph'; +import Text from './nodes/text'; + +import Blockquote from './nodes/blockquote'; +import CodeBlock from './nodes/code_block'; +import HardBreak from './nodes/hard_break'; +import Heading from './nodes/heading'; +import HorizontalRule from './nodes/horizontal_rule'; +import Image from './nodes/image'; + +import Table from './nodes/table'; +import TableHead from './nodes/table_head'; +import TableBody from './nodes/table_body'; +import TableHeaderRow from './nodes/table_header_row'; +import TableRow from './nodes/table_row'; +import TableCell from './nodes/table_cell'; + +import Emoji from './nodes/emoji'; +import Reference from './nodes/reference'; + +import TableOfContents from './nodes/table_of_contents'; +import Video from './nodes/video'; + +import BulletList from './nodes/bullet_list'; +import OrderedList from './nodes/ordered_list'; +import ListItem from './nodes/list_item'; + +import DescriptionList from './nodes/description_list'; +import DescriptionTerm from './nodes/description_term'; +import DescriptionDetails from './nodes/description_details'; + +import TaskList from './nodes/task_list'; +import OrderedTaskList from './nodes/ordered_task_list'; +import TaskListItem from './nodes/task_list_item'; + +import Summary from './nodes/summary'; +import Details from './nodes/details'; + +import Bold from './marks/bold'; +import Italic from './marks/italic'; +import Strike from './marks/strike'; +import InlineDiff from './marks/inline_diff'; + +import Link from './marks/link'; +import Code from './marks/code'; +import MathMark from './marks/math'; +import InlineHTML from './marks/inline_html'; + +// The filters referenced in lib/banzai/pipeline/gfm_pipeline.rb transform +// GitLab Flavored Markdown (GFM) to HTML. +// The nodes and marks referenced here transform that same HTML to GFM to be copied to the clipboard. +// Every filter in lib/banzai/pipeline/gfm_pipeline.rb that generates HTML +// from GFM should have a node or mark here. +// The GFM-to-HTML-to-GFM cycle is tested in spec/features/copy_as_gfm_spec.rb. + +export default [ + new Doc(), + new Paragraph(), + new Text(), + + new Blockquote(), + new CodeBlock(), + new HardBreak(), + new Heading({ maxLevel: 6 }), + new HorizontalRule(), + new Image(), + + new Table(), + new TableHead(), + new TableBody(), + new TableHeaderRow(), + new TableRow(), + new TableCell(), + + new Emoji(), + new Reference(), + + new TableOfContents(), + new Video(), + + new BulletList(), + new OrderedList(), + new ListItem(), + + new DescriptionList(), + new DescriptionTerm(), + new DescriptionDetails(), + + new TaskList(), + new OrderedTaskList(), + new TaskListItem(), + + new Summary(), + new Details(), + + new Bold(), + new Italic(), + new Strike(), + new InlineDiff(), + + new Link(), + new Code(), + new MathMark(), + new InlineHTML(), +]; diff --git a/app/assets/javascripts/behaviors/markdown/marks/bold.js b/app/assets/javascripts/behaviors/markdown/marks/bold.js new file mode 100644 index 0000000000..b537954c1c --- /dev/null +++ b/app/assets/javascripts/behaviors/markdown/marks/bold.js @@ -0,0 +1,11 @@ +/* eslint-disable class-methods-use-this */ + +import { Bold as BaseBold } from 'tiptap-extensions'; +import { defaultMarkdownSerializer } from 'prosemirror-markdown'; + +// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter +export default class Bold extends BaseBold { + get toMarkdown() { + return defaultMarkdownSerializer.marks.strong; + } +} diff --git a/app/assets/javascripts/behaviors/markdown/marks/code.js b/app/assets/javascripts/behaviors/markdown/marks/code.js new file mode 100644 index 0000000000..a760ee80dd --- /dev/null +++ b/app/assets/javascripts/behaviors/markdown/marks/code.js @@ -0,0 +1,11 @@ +/* eslint-disable class-methods-use-this */ + +import { Code as BaseCode } from 'tiptap-extensions'; +import { defaultMarkdownSerializer } from 'prosemirror-markdown'; + +// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter +export default class Code extends BaseCode { + get toMarkdown() { + return defaultMarkdownSerializer.marks.code; + } +} diff --git a/app/assets/javascripts/behaviors/markdown/marks/inline_diff.js b/app/assets/javascripts/behaviors/markdown/marks/inline_diff.js new file mode 100644 index 0000000000..ce425e80cd --- /dev/null +++ b/app/assets/javascripts/behaviors/markdown/marks/inline_diff.js @@ -0,0 +1,41 @@ +/* eslint-disable class-methods-use-this */ + +import { Mark } from 'tiptap'; + +// Transforms generated HTML back to GFM for Banzai::Filter::InlineDiffFilter +export default class InlineDiff extends Mark { + get name() { + return 'inline_diff'; + } + + get schema() { + return { + attrs: { + addition: { + default: true, + }, + }, + parseDOM: [ + { tag: 'span.idiff.addition', attrs: { addition: true } }, + { tag: 'span.idiff.deletion', attrs: { addition: false } }, + ], + toDOM: node => [ + 'span', + { class: `idiff left right ${node.attrs.addition ? 'addition' : 'deletion'}` }, + 0, + ], + }; + } + + get toMarkdown() { + return { + mixable: true, + open(state, mark) { + return mark.attrs.addition ? '{+' : '{-'; + }, + close(state, mark) { + return mark.attrs.addition ? '+}' : '-}'; + }, + }; + } +} diff --git a/app/assets/javascripts/behaviors/markdown/marks/inline_html.js b/app/assets/javascripts/behaviors/markdown/marks/inline_html.js new file mode 100644 index 0000000000..ebed8698e2 --- /dev/null +++ b/app/assets/javascripts/behaviors/markdown/marks/inline_html.js @@ -0,0 +1,46 @@ +/* eslint-disable class-methods-use-this */ + +import { Mark } from 'tiptap'; +import _ from 'underscore'; + +// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter +export default class InlineHTML extends Mark { + get name() { + return 'inline_html'; + } + + get schema() { + return { + excludes: '', + attrs: { + tag: {}, + title: { default: null }, + }, + parseDOM: [ + { + tag: 'sup, sub, kbd, q, samp, var', + getAttrs: el => ({ tag: el.nodeName.toLowerCase() }), + }, + { + tag: 'abbr', + getAttrs: el => ({ tag: 'abbr', title: el.getAttribute('title') }), + }, + ], + toDOM: node => [node.attrs.tag, { title: node.attrs.title }, 0], + }; + } + + get toMarkdown() { + return { + mixable: true, + open(state, mark) { + return `<${mark.attrs.tag}${ + mark.attrs.title ? ` title="${state.esc(_.escape(mark.attrs.title))}"` : '' + }>`; + }, + close(state, mark) { + return ``; + }, + }; + } +} diff --git a/app/assets/javascripts/behaviors/markdown/marks/italic.js b/app/assets/javascripts/behaviors/markdown/marks/italic.js new file mode 100644 index 0000000000..44b35c9773 --- /dev/null +++ b/app/assets/javascripts/behaviors/markdown/marks/italic.js @@ -0,0 +1,11 @@ +/* eslint-disable class-methods-use-this */ + +import { Italic as BaseItalic } from 'tiptap-extensions'; +import { defaultMarkdownSerializer } from 'prosemirror-markdown'; + +// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter +export default class Italic extends BaseItalic { + get toMarkdown() { + return defaultMarkdownSerializer.marks.em; + } +} diff --git a/app/assets/javascripts/behaviors/markdown/marks/link.js b/app/assets/javascripts/behaviors/markdown/marks/link.js new file mode 100644 index 0000000000..5c23d6a5ce --- /dev/null +++ b/app/assets/javascripts/behaviors/markdown/marks/link.js @@ -0,0 +1,21 @@ +/* eslint-disable class-methods-use-this */ + +import { Link as BaseLink } from 'tiptap-extensions'; +import { defaultMarkdownSerializer } from 'prosemirror-markdown'; + +// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter +export default class Link extends BaseLink { + get toMarkdown() { + return { + mixable: true, + open(state, mark, parent, index) { + const open = defaultMarkdownSerializer.marks.link.open(state, mark, parent, index); + return open === '<' ? '' : open; + }, + close(state, mark, parent, index) { + const close = defaultMarkdownSerializer.marks.link.close(state, mark, parent, index); + return close === '>' ? '' : close; + }, + }; + } +} diff --git a/app/assets/javascripts/behaviors/markdown/marks/math.js b/app/assets/javascripts/behaviors/markdown/marks/math.js new file mode 100644 index 0000000000..e582fb18f1 --- /dev/null +++ b/app/assets/javascripts/behaviors/markdown/marks/math.js @@ -0,0 +1,41 @@ +/* eslint-disable class-methods-use-this */ + +import { Mark } from 'tiptap'; +import { defaultMarkdownSerializer } from 'prosemirror-markdown'; + +// Transforms generated HTML back to GFM for Banzai::Filter::MathFilter +export default class MathMark extends Mark { + get name() { + return 'math'; + } + + get schema() { + return { + parseDOM: [ + // Matches HTML generated by Banzai::Filter::MathFilter + { + tag: 'code.code.math[data-math-style=inline]', + priority: 51, + }, + // Matches HTML after being transformed by app/assets/javascripts/behaviors/markdown/render_math.js + { + tag: 'span.katex', + contentElement: 'annotation[encoding="application/x-tex"]', + }, + ], + toDOM: () => ['code', { class: 'code math', 'data-math-style': 'inline' }, 0], + }; + } + + get toMarkdown() { + return { + escape: false, + open(state, mark, parent, index) { + return `$${defaultMarkdownSerializer.marks.code.open(state, mark, parent, index)}`; + }, + close(state, mark, parent, index) { + return `${defaultMarkdownSerializer.marks.code.close(state, mark, parent, index)}$`; + }, + }; + } +} diff --git a/app/assets/javascripts/behaviors/markdown/marks/strike.js b/app/assets/javascripts/behaviors/markdown/marks/strike.js new file mode 100644 index 0000000000..c2951a40a4 --- /dev/null +++ b/app/assets/javascripts/behaviors/markdown/marks/strike.js @@ -0,0 +1,15 @@ +/* eslint-disable class-methods-use-this */ + +import { Strike as BaseStrike } from 'tiptap-extensions'; + +// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter +export default class Strike extends BaseStrike { + get toMarkdown() { + return { + open: '~~', + close: '~~', + mixable: true, + expelEnclosingWhitespace: true, + }; + } +} diff --git a/app/assets/javascripts/behaviors/markdown/nodes/blockquote.js b/app/assets/javascripts/behaviors/markdown/nodes/blockquote.js new file mode 100644 index 0000000000..b0bc8f7964 --- /dev/null +++ b/app/assets/javascripts/behaviors/markdown/nodes/blockquote.js @@ -0,0 +1,13 @@ +/* eslint-disable class-methods-use-this */ + +import { Blockquote as BaseBlockquote } from 'tiptap-extensions'; +import { defaultMarkdownSerializer } from 'prosemirror-markdown'; + +// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter +export default class Blockquote extends BaseBlockquote { + toMarkdown(state, node) { + if (!node.childCount) return; + + defaultMarkdownSerializer.nodes.blockquote(state, node); + } +} diff --git a/app/assets/javascripts/behaviors/markdown/nodes/bullet_list.js b/app/assets/javascripts/behaviors/markdown/nodes/bullet_list.js new file mode 100644 index 0000000000..3b0792e1af --- /dev/null +++ b/app/assets/javascripts/behaviors/markdown/nodes/bullet_list.js @@ -0,0 +1,11 @@ +/* eslint-disable class-methods-use-this */ + +import { BulletList as BaseBulletList } from 'tiptap-extensions'; +import { defaultMarkdownSerializer } from 'prosemirror-markdown'; + +// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter +export default class BulletList extends BaseBulletList { + toMarkdown(state, node) { + defaultMarkdownSerializer.nodes.bullet_list(state, node); + } +} diff --git a/app/assets/javascripts/behaviors/markdown/nodes/code_block.js b/app/assets/javascripts/behaviors/markdown/nodes/code_block.js new file mode 100644 index 0000000000..1e0c05eff0 --- /dev/null +++ b/app/assets/javascripts/behaviors/markdown/nodes/code_block.js @@ -0,0 +1,99 @@ +/* eslint-disable class-methods-use-this */ + +import { CodeBlock as BaseCodeBlock } from 'tiptap-extensions'; + +const PLAINTEXT_LANG = 'plaintext'; + +// Transforms generated HTML back to GFM for: +// - Banzai::Filter::SyntaxHighlightFilter +// - Banzai::Filter::MathFilter +// - Banzai::Filter::MermaidFilter +// - Banzai::Filter::SuggestionFilter +export default class CodeBlock extends BaseCodeBlock { + get schema() { + return { + content: 'text*', + marks: '', + group: 'block', + code: true, + defining: true, + attrs: { + lang: { default: PLAINTEXT_LANG }, + }, + parseDOM: [ + // Matches HTML generated by Banzai::Filter::SyntaxHighlightFilter, Banzai::Filter::MathFilter, Banzai::Filter::MermaidFilter, or Banzai::Filter::SuggestionFilter + { + tag: 'pre.code.highlight', + preserveWhitespace: 'full', + getAttrs: el => { + const lang = el.getAttribute('lang'); + if (!lang || lang === '') return {}; + + return { lang }; + }, + }, + // Matches HTML generated by Banzai::Filter::MathFilter, + // after being transformed by app/assets/javascripts/behaviors/markdown/render_math.js + { + tag: 'span.katex-display', + preserveWhitespace: 'full', + contentElement: 'annotation[encoding="application/x-tex"]', + attrs: { lang: 'math' }, + }, + // Matches HTML generated by Banzai::Filter::MermaidFilter, + // after being transformed by app/assets/javascripts/behaviors/markdown/render_mermaid.js + { + tag: 'svg.mermaid', + preserveWhitespace: 'full', + contentElement: 'text.source', + attrs: { lang: 'mermaid' }, + }, + // Matches HTML generated by Banzai::Filter::SuggestionFilter, + // after being transformed by app/assets/javascripts/vue_shared/components/markdown/suggestions.vue + { + tag: '.md-suggestion', + skip: true, + }, + { + tag: '.md-suggestion-header', + ignore: true, + }, + { + tag: '.md-suggestion-diff', + preserveWhitespace: 'full', + getContent: (el, schema) => + [...el.querySelectorAll('.line_content.new span')].map(span => + schema.text(span.innerText), + ), + attrs: { lang: 'suggestion' }, + }, + ], + toDOM: node => ['pre', { class: 'code highlight', lang: node.attrs.lang }, ['code', 0]], + }; + } + + toMarkdown(state, node) { + if (!node.childCount) return; + + const { + textContent: text, + attrs: { lang }, + } = node; + + // Prefixes lines with 4 spaces if the code contains a line that starts with triple backticks + if (lang === PLAINTEXT_LANG && text.match(/^```/gm)) { + state.wrapBlock(' ', null, node, () => state.text(text, false)); + return; + } + + state.write('```'); + if (lang !== PLAINTEXT_LANG) state.write(lang); + + state.ensureNewLine(); + state.text(text, false); + state.ensureNewLine(); + + state.write('```'); + state.closeBlock(node); + } +} diff --git a/app/assets/javascripts/behaviors/markdown/nodes/description_details.js b/app/assets/javascripts/behaviors/markdown/nodes/description_details.js new file mode 100644 index 0000000000..a4451d8ce8 --- /dev/null +++ b/app/assets/javascripts/behaviors/markdown/nodes/description_details.js @@ -0,0 +1,28 @@ +/* eslint-disable class-methods-use-this */ + +import { Node } from 'tiptap'; + +// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter +export default class DescriptionDetails extends Node { + get name() { + return 'description_details'; + } + + get schema() { + return { + content: 'text*', + marks: '', + defining: true, + parseDOM: [{ tag: 'dd' }], + toDOM: () => ['dd', 0], + }; + } + + toMarkdown(state, node) { + state.flushClose(1); + state.write('
'); + state.text(node.textContent, false); + state.write('
'); + state.closeBlock(node); + } +} diff --git a/app/assets/javascripts/behaviors/markdown/nodes/description_list.js b/app/assets/javascripts/behaviors/markdown/nodes/description_list.js new file mode 100644 index 0000000000..6aa1aca29d --- /dev/null +++ b/app/assets/javascripts/behaviors/markdown/nodes/description_list.js @@ -0,0 +1,28 @@ +/* eslint-disable class-methods-use-this */ + +import { Node } from 'tiptap'; + +// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter +export default class DescriptionList extends Node { + get name() { + return 'description_list'; + } + + get schema() { + return { + content: '(description_term+ description_details+)+', + group: 'block', + parseDOM: [{ tag: 'dl' }], + toDOM: () => ['dl', 0], + }; + } + + toMarkdown(state, node) { + state.write('
\n'); + state.wrapBlock(' ', null, node, () => state.renderContent(node)); + state.flushClose(1); + state.ensureNewLine(); + state.write('
'); + state.closeBlock(node); + } +} diff --git a/app/assets/javascripts/behaviors/markdown/nodes/description_term.js b/app/assets/javascripts/behaviors/markdown/nodes/description_term.js new file mode 100644 index 0000000000..89057ec644 --- /dev/null +++ b/app/assets/javascripts/behaviors/markdown/nodes/description_term.js @@ -0,0 +1,28 @@ +/* eslint-disable class-methods-use-this */ + +import { Node } from 'tiptap'; + +// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter +export default class DescriptionTerm extends Node { + get name() { + return 'description_term'; + } + + get schema() { + return { + content: 'text*', + marks: '', + defining: true, + parseDOM: [{ tag: 'dt' }], + toDOM: () => ['dt', 0], + }; + } + + toMarkdown(state, node) { + state.flushClose(state.closed && state.closed.type === node.type ? 1 : 2); + state.write('
'); + state.text(node.textContent, false); + state.write('
'); + state.closeBlock(node); + } +} diff --git a/app/assets/javascripts/behaviors/markdown/nodes/details.js b/app/assets/javascripts/behaviors/markdown/nodes/details.js new file mode 100644 index 0000000000..1c40dbb816 --- /dev/null +++ b/app/assets/javascripts/behaviors/markdown/nodes/details.js @@ -0,0 +1,28 @@ +/* eslint-disable class-methods-use-this */ + +import { Node } from 'tiptap'; + +// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter +export default class Details extends Node { + get name() { + return 'details'; + } + + get schema() { + return { + content: 'summary block*', + group: 'block', + parseDOM: [{ tag: 'details' }], + toDOM: () => ['details', { open: true, onclick: 'return false', tabindex: '-1' }, 0], + }; + } + + toMarkdown(state, node) { + state.write('
\n'); + state.renderContent(node); + state.flushClose(1); + state.ensureNewLine(); + state.write('
'); + state.closeBlock(node); + } +} diff --git a/app/assets/javascripts/behaviors/markdown/nodes/doc.js b/app/assets/javascripts/behaviors/markdown/nodes/doc.js new file mode 100644 index 0000000000..88b16fd85d --- /dev/null +++ b/app/assets/javascripts/behaviors/markdown/nodes/doc.js @@ -0,0 +1,15 @@ +/* eslint-disable class-methods-use-this */ + +import { Node } from 'tiptap'; + +export default class Doc extends Node { + get name() { + return 'doc'; + } + + get schema() { + return { + content: 'block+', + }; + } +} diff --git a/app/assets/javascripts/behaviors/markdown/nodes/emoji.js b/app/assets/javascripts/behaviors/markdown/nodes/emoji.js new file mode 100644 index 0000000000..a7cc3e828f --- /dev/null +++ b/app/assets/javascripts/behaviors/markdown/nodes/emoji.js @@ -0,0 +1,41 @@ +/* eslint-disable class-methods-use-this */ + +import { Node } from 'tiptap'; + +// Transforms generated HTML back to GFM for Banzai::Filter::EmojiFilter +export default class Emoji extends Node { + get name() { + return 'emoji'; + } + + get schema() { + return { + inline: true, + group: 'inline', + attrs: { + name: {}, + title: {}, + moji: {}, + }, + parseDOM: [ + { + tag: 'gl-emoji', + getAttrs: el => ({ + name: el.dataset.name, + title: el.getAttribute('title'), + moji: el.textContent, + }), + }, + ], + toDOM: node => [ + 'gl-emoji', + { 'data-name': node.attrs.name, title: node.attrs.title }, + node.attrs.moji, + ], + }; + } + + toMarkdown(state, node) { + state.write(`:${node.attrs.name}:`); + } +} diff --git a/app/assets/javascripts/behaviors/markdown/nodes/hard_break.js b/app/assets/javascripts/behaviors/markdown/nodes/hard_break.js new file mode 100644 index 0000000000..59e5d8ab3e --- /dev/null +++ b/app/assets/javascripts/behaviors/markdown/nodes/hard_break.js @@ -0,0 +1,10 @@ +/* eslint-disable class-methods-use-this */ + +import { HardBreak as BaseHardBreak } from 'tiptap-extensions'; + +// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter +export default class HardBreak extends BaseHardBreak { + toMarkdown(state) { + if (!state.atBlank()) state.write(' \n'); + } +} diff --git a/app/assets/javascripts/behaviors/markdown/nodes/heading.js b/app/assets/javascripts/behaviors/markdown/nodes/heading.js new file mode 100644 index 0000000000..fec8608cf5 --- /dev/null +++ b/app/assets/javascripts/behaviors/markdown/nodes/heading.js @@ -0,0 +1,13 @@ +/* eslint-disable class-methods-use-this */ + +import { Heading as BaseHeading } from 'tiptap-extensions'; +import { defaultMarkdownSerializer } from 'prosemirror-markdown'; + +// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter +export default class Heading extends BaseHeading { + toMarkdown(state, node) { + if (!node.childCount) return; + + defaultMarkdownSerializer.nodes.heading(state, node); + } +} diff --git a/app/assets/javascripts/behaviors/markdown/nodes/horizontal_rule.js b/app/assets/javascripts/behaviors/markdown/nodes/horizontal_rule.js new file mode 100644 index 0000000000..695c7160bd --- /dev/null +++ b/app/assets/javascripts/behaviors/markdown/nodes/horizontal_rule.js @@ -0,0 +1,11 @@ +/* eslint-disable class-methods-use-this */ + +import { HorizontalRule as BaseHorizontalRule } from 'tiptap-extensions'; +import { defaultMarkdownSerializer } from 'prosemirror-markdown'; + +// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter +export default class HorizontalRule extends BaseHorizontalRule { + toMarkdown(state, node) { + defaultMarkdownSerializer.nodes.horizontal_rule(state, node); + } +} diff --git a/app/assets/javascripts/behaviors/markdown/nodes/image.js b/app/assets/javascripts/behaviors/markdown/nodes/image.js new file mode 100644 index 0000000000..c225a5ed87 --- /dev/null +++ b/app/assets/javascripts/behaviors/markdown/nodes/image.js @@ -0,0 +1,52 @@ +/* eslint-disable class-methods-use-this */ + +import { Image as BaseImage } from 'tiptap-extensions'; +import { placeholderImage } from '~/lazy_loader'; +import { defaultMarkdownSerializer } from 'prosemirror-markdown'; + +export default class Image extends BaseImage { + get schema() { + return { + attrs: { + src: {}, + alt: { + default: null, + }, + title: { + default: null, + }, + }, + group: 'inline', + inline: true, + draggable: true, + parseDOM: [ + // Matches HTML generated by Banzai::Filter::ImageLinkFilter + { + tag: 'a.no-attachment-icon', + priority: 51, + skip: true, + }, + // Matches HTML generated by Banzai::Filter::ImageLazyLoadFilter + { + tag: 'img[src]', + getAttrs: el => { + const imageSrc = el.src; + const imageUrl = + imageSrc && imageSrc !== placeholderImage ? imageSrc : el.dataset.src || ''; + + return { + src: imageUrl, + title: el.getAttribute('title'), + alt: el.getAttribute('alt'), + }; + }, + }, + ], + toDOM: node => ['img', node.attrs], + }; + } + + toMarkdown(state, node) { + defaultMarkdownSerializer.nodes.image(state, node); + } +} diff --git a/app/assets/javascripts/behaviors/markdown/nodes/list_item.js b/app/assets/javascripts/behaviors/markdown/nodes/list_item.js new file mode 100644 index 0000000000..4237637ed9 --- /dev/null +++ b/app/assets/javascripts/behaviors/markdown/nodes/list_item.js @@ -0,0 +1,11 @@ +/* eslint-disable class-methods-use-this */ + +import { ListItem as BaseListItem } from 'tiptap-extensions'; +import { defaultMarkdownSerializer } from 'prosemirror-markdown'; + +// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter +export default class ListItem extends BaseListItem { + toMarkdown(state, node) { + defaultMarkdownSerializer.nodes.list_item(state, node); + } +} diff --git a/app/assets/javascripts/behaviors/markdown/nodes/ordered_list.js b/app/assets/javascripts/behaviors/markdown/nodes/ordered_list.js new file mode 100644 index 0000000000..4c1542d14e --- /dev/null +++ b/app/assets/javascripts/behaviors/markdown/nodes/ordered_list.js @@ -0,0 +1,10 @@ +/* eslint-disable class-methods-use-this */ + +import { OrderedList as BaseOrderedList } from 'tiptap-extensions'; + +// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter +export default class OrderedList extends BaseOrderedList { + toMarkdown(state, node) { + state.renderList(node, ' ', () => '1. '); + } +} diff --git a/app/assets/javascripts/behaviors/markdown/nodes/ordered_task_list.js b/app/assets/javascripts/behaviors/markdown/nodes/ordered_task_list.js new file mode 100644 index 0000000000..25c4976a1b --- /dev/null +++ b/app/assets/javascripts/behaviors/markdown/nodes/ordered_task_list.js @@ -0,0 +1,28 @@ +/* eslint-disable class-methods-use-this */ + +import { Node } from 'tiptap'; + +// Transforms generated HTML back to GFM for Banzai::Filter::TaskListFilter +export default class OrderedTaskList extends Node { + get name() { + return 'ordered_task_list'; + } + + get schema() { + return { + group: 'block', + content: '(task_list_item|list_item)+', + parseDOM: [ + { + priority: 51, + tag: 'ol.task-list', + }, + ], + toDOM: () => ['ol', { class: 'task-list' }, 0], + }; + } + + toMarkdown(state, node) { + state.renderList(node, ' ', () => '1. '); + } +} diff --git a/app/assets/javascripts/behaviors/markdown/nodes/paragraph.js b/app/assets/javascripts/behaviors/markdown/nodes/paragraph.js new file mode 100644 index 0000000000..dec3207b1b --- /dev/null +++ b/app/assets/javascripts/behaviors/markdown/nodes/paragraph.js @@ -0,0 +1,24 @@ +/* eslint-disable class-methods-use-this */ + +import { Node } from 'tiptap'; +import { defaultMarkdownSerializer } from 'prosemirror-markdown'; + +// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter +export default class Paragraph extends Node { + get name() { + return 'paragraph'; + } + + get schema() { + return { + content: 'inline*', + group: 'block', + parseDOM: [{ tag: 'p' }], + toDOM: () => ['p', 0], + }; + } + + toMarkdown(state, node) { + defaultMarkdownSerializer.nodes.paragraph(state, node); + } +} diff --git a/app/assets/javascripts/behaviors/markdown/nodes/reference.js b/app/assets/javascripts/behaviors/markdown/nodes/reference.js new file mode 100644 index 0000000000..5d6bbeca83 --- /dev/null +++ b/app/assets/javascripts/behaviors/markdown/nodes/reference.js @@ -0,0 +1,52 @@ +/* eslint-disable class-methods-use-this */ + +import { Node } from 'tiptap'; + +// Transforms generated HTML back to GFM for Banzai::Filter::ReferenceFilter and subclasses +export default class Reference extends Node { + get name() { + return 'reference'; + } + + get schema() { + return { + inline: true, + group: 'inline', + atom: true, + attrs: { + className: {}, + referenceType: {}, + originalText: { default: null }, + href: {}, + text: {}, + }, + parseDOM: [ + { + tag: 'a.gfm:not([data-link=true])', + priority: 51, + getAttrs: el => ({ + className: el.className, + referenceType: el.dataset.referenceType, + originalText: el.dataset.original, + href: el.getAttribute('href'), + text: el.textContent, + }), + }, + ], + toDOM: node => [ + 'a', + { + class: node.attrs.className, + href: node.attrs.href, + 'data-reference-type': node.attrs.referenceType, + 'data-original': node.attrs.originalText, + }, + node.attrs.text, + ], + }; + } + + toMarkdown(state, node) { + state.write(node.attrs.originalText || node.attrs.text); + } +} diff --git a/app/assets/javascripts/behaviors/markdown/nodes/summary.js b/app/assets/javascripts/behaviors/markdown/nodes/summary.js new file mode 100644 index 0000000000..2e36e316d7 --- /dev/null +++ b/app/assets/javascripts/behaviors/markdown/nodes/summary.js @@ -0,0 +1,27 @@ +/* eslint-disable class-methods-use-this */ + +import { Node } from 'tiptap'; + +// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter +export default class Summary extends Node { + get name() { + return 'summary'; + } + + get schema() { + return { + content: 'text*', + marks: '', + defining: true, + parseDOM: [{ tag: 'summary' }], + toDOM: () => ['summary', 0], + }; + } + + toMarkdown(state, node) { + state.write(''); + state.text(node.textContent, false); + state.write(''); + state.closeBlock(node); + } +} diff --git a/app/assets/javascripts/behaviors/markdown/nodes/table.js b/app/assets/javascripts/behaviors/markdown/nodes/table.js new file mode 100644 index 0000000000..a7fcb9227c --- /dev/null +++ b/app/assets/javascripts/behaviors/markdown/nodes/table.js @@ -0,0 +1,25 @@ +/* eslint-disable class-methods-use-this */ + +import { Node } from 'tiptap'; + +// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter +export default class Table extends Node { + get name() { + return 'table'; + } + + get schema() { + return { + content: 'table_head table_body', + group: 'block', + isolating: true, + parseDOM: [{ tag: 'table' }], + toDOM: () => ['table', 0], + }; + } + + toMarkdown(state, node) { + state.renderContent(node); + state.closeBlock(node); + } +} diff --git a/app/assets/javascripts/behaviors/markdown/nodes/table_body.js b/app/assets/javascripts/behaviors/markdown/nodes/table_body.js new file mode 100644 index 0000000000..403556dc0c --- /dev/null +++ b/app/assets/javascripts/behaviors/markdown/nodes/table_body.js @@ -0,0 +1,24 @@ +/* eslint-disable class-methods-use-this */ + +import { Node } from 'tiptap'; + +// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter +export default class TableBody extends Node { + get name() { + return 'table_body'; + } + + get schema() { + return { + content: 'table_row+', + parseDOM: [{ tag: 'tbody' }], + toDOM: () => ['tbody', 0], + }; + } + + toMarkdown(state, node) { + state.flushClose(1); + state.renderContent(node); + state.closeBlock(node); + } +} diff --git a/app/assets/javascripts/behaviors/markdown/nodes/table_cell.js b/app/assets/javascripts/behaviors/markdown/nodes/table_cell.js new file mode 100644 index 0000000000..c63bfe10e3 --- /dev/null +++ b/app/assets/javascripts/behaviors/markdown/nodes/table_cell.js @@ -0,0 +1,35 @@ +/* eslint-disable class-methods-use-this */ + +import { Node } from 'tiptap'; + +// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter +export default class TableCell extends Node { + get name() { + return 'table_cell'; + } + + get schema() { + return { + attrs: { + header: { default: false }, + align: { default: null }, + }, + content: 'inline*', + isolating: true, + parseDOM: [ + { + tag: 'td, th', + getAttrs: el => ({ + header: el.tagName === 'TH', + align: el.getAttribute('align') || el.style.textAlign, + }), + }, + ], + toDOM: node => [node.attrs.header ? 'th' : 'td', { align: node.attrs.align }, 0], + }; + } + + toMarkdown(state, node) { + state.renderInline(node); + } +} diff --git a/app/assets/javascripts/behaviors/markdown/nodes/table_head.js b/app/assets/javascripts/behaviors/markdown/nodes/table_head.js new file mode 100644 index 0000000000..4cb94bf088 --- /dev/null +++ b/app/assets/javascripts/behaviors/markdown/nodes/table_head.js @@ -0,0 +1,24 @@ +/* eslint-disable class-methods-use-this */ + +import { Node } from 'tiptap'; + +// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter +export default class TableHead extends Node { + get name() { + return 'table_head'; + } + + get schema() { + return { + content: 'table_header_row', + parseDOM: [{ tag: 'thead' }], + toDOM: () => ['thead', 0], + }; + } + + toMarkdown(state, node) { + state.flushClose(1); + state.renderContent(node); + state.closeBlock(node); + } +} diff --git a/app/assets/javascripts/behaviors/markdown/nodes/table_header_row.js b/app/assets/javascripts/behaviors/markdown/nodes/table_header_row.js new file mode 100644 index 0000000000..e7eee63640 --- /dev/null +++ b/app/assets/javascripts/behaviors/markdown/nodes/table_header_row.js @@ -0,0 +1,43 @@ +/* eslint-disable class-methods-use-this */ + +import TableRow from './table_row'; + +const CENTER_ALIGN = 'center'; + +// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter +export default class TableHeaderRow extends TableRow { + get name() { + return 'table_header_row'; + } + + get schema() { + return { + content: 'table_cell+', + parseDOM: [ + { + tag: 'thead tr', + priority: 51, + }, + ], + toDOM: () => ['tr', 0], + }; + } + + toMarkdown(state, node) { + const cellWidths = super.toMarkdown(state, node); + + state.flushClose(1); + + state.write('|'); + node.forEach((cell, _, i) => { + if (i) state.write('|'); + + state.write(cell.attrs.align === CENTER_ALIGN ? ':' : '-'); + state.write(state.repeat('-', cellWidths[i])); + state.write(cell.attrs.align === CENTER_ALIGN || cell.attrs.align === 'right' ? ':' : '-'); + }); + state.write('|'); + + state.closeBlock(node); + } +} diff --git a/app/assets/javascripts/behaviors/markdown/nodes/table_of_contents.js b/app/assets/javascripts/behaviors/markdown/nodes/table_of_contents.js new file mode 100644 index 0000000000..20c7fa8a9a --- /dev/null +++ b/app/assets/javascripts/behaviors/markdown/nodes/table_of_contents.js @@ -0,0 +1,33 @@ +/* eslint-disable class-methods-use-this */ + +import { Node } from 'tiptap'; + +// Transforms generated HTML back to GFM for Banzai::Filter::TableOfContentsFilter +export default class TableOfContents extends Node { + get name() { + return 'table_of_contents'; + } + + get schema() { + return { + group: 'block', + atom: true, + parseDOM: [ + { + tag: 'ul.section-nav', + priority: 51, + }, + { + tag: 'p.table-of-contents', + priority: 51, + }, + ], + toDOM: () => ['p', { class: 'table-of-contents' }, 'Table of Contents'], + }; + } + + toMarkdown(state, node) { + state.write('[[_TOC_]]'); + state.closeBlock(node); + } +} diff --git a/app/assets/javascripts/behaviors/markdown/nodes/table_row.js b/app/assets/javascripts/behaviors/markdown/nodes/table_row.js new file mode 100644 index 0000000000..5852502773 --- /dev/null +++ b/app/assets/javascripts/behaviors/markdown/nodes/table_row.js @@ -0,0 +1,38 @@ +/* eslint-disable class-methods-use-this */ + +import { Node } from 'tiptap'; + +// Transforms generated HTML back to GFM for Banzai::Filter::MarkdownFilter +export default class TableRow extends Node { + get name() { + return 'table_row'; + } + + get schema() { + return { + content: 'table_cell+', + parseDOM: [{ tag: 'tr' }], + toDOM: () => ['tr', 0], + }; + } + + toMarkdown(state, node) { + const cellWidths = []; + + state.flushClose(1); + + state.write('| '); + node.forEach((cell, _, i) => { + if (i) state.write(' | '); + + const { length } = state.out; + state.render(cell, node, i); + cellWidths.push(state.out.length - length); + }); + state.write(' |'); + + state.closeBlock(node); + + return cellWidths; + } +} diff --git a/app/assets/javascripts/behaviors/markdown/nodes/task_list.js b/app/assets/javascripts/behaviors/markdown/nodes/task_list.js new file mode 100644 index 0000000000..ab33bc2150 --- /dev/null +++ b/app/assets/javascripts/behaviors/markdown/nodes/task_list.js @@ -0,0 +1,28 @@ +/* eslint-disable class-methods-use-this */ + +import { Node } from 'tiptap'; + +// Transforms generated HTML back to GFM for Banzai::Filter::TaskListFilter +export default class TaskList extends Node { + get name() { + return 'task_list'; + } + + get schema() { + return { + group: 'block', + content: '(task_list_item|list_item)+', + parseDOM: [ + { + priority: 51, + tag: 'ul.task-list', + }, + ], + toDOM: () => ['ul', { class: 'task-list' }, 0], + }; + } + + toMarkdown(state, node) { + state.renderList(node, ' ', () => '* '); + } +} diff --git a/app/assets/javascripts/behaviors/markdown/nodes/task_list_item.js b/app/assets/javascripts/behaviors/markdown/nodes/task_list_item.js new file mode 100644 index 0000000000..d0ee7333d5 --- /dev/null +++ b/app/assets/javascripts/behaviors/markdown/nodes/task_list_item.js @@ -0,0 +1,49 @@ +/* eslint-disable class-methods-use-this */ + +import { Node } from 'tiptap'; + +// Transforms generated HTML back to GFM for Banzai::Filter::TaskListFilter +export default class TaskListItem extends Node { + get name() { + return 'task_list_item'; + } + + get schema() { + return { + attrs: { + done: { + default: false, + }, + }, + defining: true, + draggable: false, + content: 'paragraph block*', + parseDOM: [ + { + priority: 51, + tag: 'li.task-list-item', + getAttrs: el => { + const checkbox = el.querySelector('input[type=checkbox].task-list-item-checkbox'); + return { done: checkbox && checkbox.checked }; + }, + }, + ], + toDOM(node) { + return [ + 'li', + { class: 'task-list-item' }, + [ + 'input', + { type: 'checkbox', class: 'task-list-item-checkbox', checked: node.attrs.done }, + ], + ['div', { class: 'todo-content' }, 0], + ]; + }, + }; + } + + toMarkdown(state, node) { + state.write(`[${node.attrs.done ? 'x' : ' '}] `); + state.renderContent(node); + } +} diff --git a/app/assets/javascripts/behaviors/markdown/nodes/text.js b/app/assets/javascripts/behaviors/markdown/nodes/text.js new file mode 100644 index 0000000000..84838c1499 --- /dev/null +++ b/app/assets/javascripts/behaviors/markdown/nodes/text.js @@ -0,0 +1,20 @@ +/* eslint-disable class-methods-use-this */ + +import { Node } from 'tiptap'; +import { defaultMarkdownSerializer } from 'prosemirror-markdown'; + +export default class Text extends Node { + get name() { + return 'text'; + } + + get schema() { + return { + group: 'inline', + }; + } + + toMarkdown(state, node) { + defaultMarkdownSerializer.nodes.text(state, node); + } +} diff --git a/app/assets/javascripts/behaviors/markdown/nodes/video.js b/app/assets/javascripts/behaviors/markdown/nodes/video.js new file mode 100644 index 0000000000..516f983397 --- /dev/null +++ b/app/assets/javascripts/behaviors/markdown/nodes/video.js @@ -0,0 +1,54 @@ +/* eslint-disable class-methods-use-this */ + +import { Node } from 'tiptap'; +import { defaultMarkdownSerializer } from 'prosemirror-markdown'; + +// Transforms generated HTML back to GFM for Banzai::Filter::VideoLinkFilter +export default class Video extends Node { + get name() { + return 'video'; + } + + get schema() { + return { + attrs: { + src: {}, + alt: { + default: null, + }, + }, + group: 'block', + draggable: true, + parseDOM: [ + { + tag: '.video-container', + skip: true, + }, + { + tag: '.video-container p', + priority: 51, + ignore: true, + }, + { + tag: 'video[src]', + getAttrs: el => ({ src: el.getAttribute('src'), alt: el.dataset.title }), + }, + ], + toDOM: node => [ + 'video', + { + src: node.attrs.src, + width: '400', + controls: true, + 'data-setup': '{}', + 'data-title': node.attrs.alt, + }, + ], + }; + } + + toMarkdown(state, node) { + defaultMarkdownSerializer.nodes.image(state, node); + state.closeBlock(node); + } +} diff --git a/app/assets/javascripts/behaviors/markdown/schema.js b/app/assets/javascripts/behaviors/markdown/schema.js new file mode 100644 index 0000000000..163182ab77 --- /dev/null +++ b/app/assets/javascripts/behaviors/markdown/schema.js @@ -0,0 +1,24 @@ +import { Schema } from 'prosemirror-model'; +import editorExtensions from './editor_extensions'; + +const nodes = editorExtensions + .filter(extension => extension.type === 'node') + .reduce( + (ns, { name, schema }) => ({ + ...ns, + [name]: schema, + }), + {}, + ); + +const marks = editorExtensions + .filter(extension => extension.type === 'mark') + .reduce( + (ms, { name, schema }) => ({ + ...ms, + [name]: schema, + }), + {}, + ); + +export default new Schema({ nodes, marks }); diff --git a/app/assets/javascripts/behaviors/markdown/serializer.js b/app/assets/javascripts/behaviors/markdown/serializer.js new file mode 100644 index 0000000000..70dbd8bd20 --- /dev/null +++ b/app/assets/javascripts/behaviors/markdown/serializer.js @@ -0,0 +1,24 @@ +import { MarkdownSerializer } from 'prosemirror-markdown'; +import editorExtensions from './editor_extensions'; + +const nodes = editorExtensions + .filter(extension => extension.type === 'node') + .reduce( + (ns, { name, toMarkdown }) => ({ + ...ns, + [name]: toMarkdown, + }), + {}, + ); + +const marks = editorExtensions + .filter(extension => extension.type === 'mark') + .reduce( + (ms, { name, toMarkdown }) => ({ + ...ms, + [name]: toMarkdown, + }), + {}, + ); + +export default new MarkdownSerializer(nodes, marks); diff --git a/app/assets/javascripts/behaviors/preview_markdown.js b/app/assets/javascripts/behaviors/preview_markdown.js index 35f1bb6b08..7adccbb062 100644 --- a/app/assets/javascripts/behaviors/preview_markdown.js +++ b/app/assets/javascripts/behaviors/preview_markdown.js @@ -28,16 +28,13 @@ MarkdownPreview.prototype.ajaxCache = {}; MarkdownPreview.prototype.showPreview = function($form) { var mdText; - var markdownVersion; - var url; var preview = $form.find('.js-md-preview'); + var url = preview.data('url'); if (preview.hasClass('md-preview-loading')) { return; } mdText = $form.find('textarea.markdown-area').val(); - markdownVersion = $form.attr('data-markdown-version'); - url = this.versionedPreviewPath(preview.data('url'), markdownVersion); if (mdText.trim().length === 0) { preview.text(this.emptyMessage); @@ -67,16 +64,6 @@ MarkdownPreview.prototype.showPreview = function($form) { } }; -MarkdownPreview.prototype.versionedPreviewPath = function(markdownPreviewPath, markdownVersion) { - if (typeof markdownVersion === 'undefined') { - return markdownPreviewPath; - } - - return `${markdownPreviewPath}${ - markdownPreviewPath.indexOf('?') === -1 ? '?' : '&' - }markdown_version=${markdownVersion}`; -}; - MarkdownPreview.prototype.fetchMarkdownPreview = function(text, url, success) { if (!url) { return; diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js index 2918e1486a..680f203140 100644 --- a/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js +++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js @@ -1,6 +1,5 @@ import $ from 'jquery'; import Mousetrap from 'mousetrap'; -import _ from 'underscore'; import Sidebar from '../../right_sidebar'; import Shortcuts from './shortcuts'; import { CopyAsGFM } from '../markdown/copy_as_gfm'; @@ -63,28 +62,32 @@ export default class ShortcutsIssuable extends Shortcuts { } const el = CopyAsGFM.transformGFMSelection(documentFragment.cloneNode(true)); - const selected = CopyAsGFM.nodeToGFM(el); + const blockquoteEl = document.createElement('blockquote'); + blockquoteEl.appendChild(el); + CopyAsGFM.nodeToGFM(blockquoteEl) + .then(text => { + if (text.trim() === '') { + return false; + } - if (selected.trim() === '') { - return false; - } + // If replyField already has some content, add a newline before our quote + const separator = ($replyField.val().trim() !== '' && '\n\n') || ''; + $replyField + .val((a, current) => `${current}${separator}${text}\n\n`) + .trigger('input') + .trigger('change'); - const quote = _.map(selected.split('\n'), val => `${`> ${val}`.trim()}\n`); + // Trigger autosize + const event = document.createEvent('Event'); + event.initEvent('autosize:update', true, false); + $replyField.get(0).dispatchEvent(event); - // If replyField already has some content, add a newline before our quote - const separator = ($replyField.val().trim() !== '' && '\n\n') || ''; - $replyField - .val((a, current) => `${current}${separator}${quote.join('')}\n`) - .trigger('input') - .trigger('change'); + // Focus the input field + $replyField.focus(); - // Trigger autosize - const event = document.createEvent('Event'); - event.initEvent('autosize:update', true, false); - $replyField.get(0).dispatchEvent(event); - - // Focus the input field - $replyField.focus(); + return false; + }) + .catch(() => {}); return false; } diff --git a/app/assets/javascripts/boards/components/board_card.vue b/app/assets/javascripts/boards/components/board_card.vue index 30fbdb9e97..f569322ab7 100644 --- a/app/assets/javascripts/boards/components/board_card.vue +++ b/app/assets/javascripts/boards/components/board_card.vue @@ -86,7 +86,7 @@ export default { class="board-card" @mousedown="mouseDown" @mousemove="mouseMove" - @mouseup="showIssue($event);" + @mouseup="showIssue($event)" >
-
+
An error occurred. Please try again.
diff --git a/app/assets/javascripts/boards/components/issue_card_inner.vue b/app/assets/javascripts/boards/components/issue_card_inner.vue index 0f581c3d37..90ab3a7634 100644 --- a/app/assets/javascripts/boards/components/issue_card_inner.vue +++ b/app/assets/javascripts/boards/components/issue_card_inner.vue @@ -184,7 +184,7 @@ export default { :title="label.description" class="badge color-label append-right-4 prepend-top-4" type="button" - @click="filterByLabel(label);" + @click="filterByLabel(label)" > {{ label.title }} diff --git a/app/assets/javascripts/boards/components/modal/empty_state.vue b/app/assets/javascripts/boards/components/modal/empty_state.vue index defd857b92..2a0008467c 100644 --- a/app/assets/javascripts/boards/components/modal/empty_state.vue +++ b/app/assets/javascripts/boards/components/modal/empty_state.vue @@ -58,7 +58,7 @@ export default { v-if="activeTab === 'selected'" class="btn btn-default" type="button" - @click="changeTab('all');" + @click="changeTab('all')" > Open issues diff --git a/app/assets/javascripts/boards/components/modal/footer.vue b/app/assets/javascripts/boards/components/modal/footer.vue index b1bc7d8708..d4afd9d59d 100644 --- a/app/assets/javascripts/boards/components/modal/footer.vue +++ b/app/assets/javascripts/boards/components/modal/footer.vue @@ -71,7 +71,7 @@ export default { to list
- diff --git a/app/assets/javascripts/boards/components/modal/header.vue b/app/assets/javascripts/boards/components/modal/header.vue index d0e285a149..1f0961e02d 100644 --- a/app/assets/javascripts/boards/components/modal/header.vue +++ b/app/assets/javascripts/boards/components/modal/header.vue @@ -58,7 +58,7 @@ export default { class="close" data-dismiss="modal" aria-label="Close" - @click="toggleModal(false);" + @click="toggleModal(false)" > diff --git a/app/assets/javascripts/boards/components/modal/list.vue b/app/assets/javascripts/boards/components/modal/list.vue index 878bb002c6..e9ed2de859 100644 --- a/app/assets/javascripts/boards/components/modal/list.vue +++ b/app/assets/javascripts/boards/components/modal/list.vue @@ -130,7 +130,7 @@ export default {
{{ list.title }} diff --git a/app/assets/javascripts/boards/components/modal/tabs.vue b/app/assets/javascripts/boards/components/modal/tabs.vue index 7b800a6ab9..2d2920e312 100644 --- a/app/assets/javascripts/boards/components/modal/tabs.vue +++ b/app/assets/javascripts/boards/components/modal/tabs.vue @@ -21,12 +21,12 @@ export default {