diff --git a/.codeclimate.yml b/.codeclimate.yml index 216ecf43beb..8699a903f2a 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -10,12 +10,6 @@ engines: - javascript exclude_paths: - "lib/api/v3/*" - eslint: - enabled: true - channel: "eslint-4" - rubocop: - enabled: true - channel: "gitlab-rubocop-0-52-1" ratings: paths: - Gemfile.lock diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 70f41e4dc98..4659722854e 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,4 +1,4 @@ -image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.6-golang-1.9-git-2.16-chrome-63.0-node-8.x-yarn-1.2-postgresql-9.6" +image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.7-golang-1.9-git-2.17-chrome-63.0-node-8.x-yarn-1.2-postgresql-9.6" .dedicated-runner: &dedicated-runner retry: 1 @@ -6,7 +6,7 @@ image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.6-golang-1.9-git - gitlab-org .default-cache: &default-cache - key: "ruby-2.3.6-with-yarn" + key: "ruby-2.3.7-with-yarn" paths: - vendor/ruby - .yarn-cache/ @@ -78,6 +78,19 @@ stages: - mysql:latest - redis:alpine +.rails5-variables: &rails5-variables + script: + - export RAILS5=${RAILS5} + - export BUNDLE_GEMFILE=${BUNDLE_GEMFILE} + +.rails5: &rails5 + allow_failure: true + only: + - /rails5/ + variables: + BUNDLE_GEMFILE: "Gemfile.rails5" + RAILS5: "true" + # Skip all jobs except the ones that begin with 'docs/'. # Used for commits including ONLY documentation changes. # https://docs.gitlab.com/ce/development/writing_documentation.html#testing @@ -118,6 +131,7 @@ stages: <<: *dedicated-runner <<: *except-docs-and-qa <<: *pull-cache + <<: *rails5-variables stage: test script: - JOB_NAME=( $CI_JOB_NAME ) @@ -148,14 +162,23 @@ stages: <<: *rspec-metadata <<: *use-pg +.rspec-metadata-pg-rails5: &rspec-metadata-pg-rails5 + <<: *rspec-metadata-pg + <<: *rails5 + .rspec-metadata-mysql: &rspec-metadata-mysql <<: *rspec-metadata <<: *use-mysql +.rspec-metadata-mysql-rails5: &rspec-metadata-mysql-rails5 + <<: *rspec-metadata-mysql + <<: *rails5 + .spinach-metadata: &spinach-metadata <<: *dedicated-runner <<: *except-docs-and-qa <<: *pull-cache + <<: *rails5-variables stage: test script: - JOB_NAME=( $CI_JOB_NAME ) @@ -179,10 +202,18 @@ stages: <<: *spinach-metadata <<: *use-pg +.spinach-metadata-pg-rails5: &spinach-metadata-pg-rails5 + <<: *spinach-metadata-pg + <<: *rails5 + .spinach-metadata-mysql: &spinach-metadata-mysql <<: *spinach-metadata <<: *use-mysql +.spinach-metadata-mysql-rails5: &spinach-metadata-mysql-rails5 + <<: *spinach-metadata-mysql + <<: *rails5 + .only-canonical-masters: &only-canonical-masters only: - master@gitlab-org/gitlab-ce @@ -264,8 +295,18 @@ package-and-qa: stage: build cache: {} when: manual + variables: + GIT_STRATEGY: none + retry: 0 + 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). + - apk add --update openssl + - wget https://gitlab.com/$CI_PROJECT_PATH/raw/$CI_COMMIT_SHA/scripts/trigger-build-omnibus + - chmod 755 trigger-build-omnibus script: - - scripts/trigger-build-omnibus + - ./trigger-build-omnibus only: - //@gitlab-org/gitlab-ce - //@gitlab-org/gitlab-ee @@ -458,6 +499,70 @@ spinach-pg 1 2: *spinach-metadata-pg spinach-mysql 0 2: *spinach-metadata-mysql spinach-mysql 1 2: *spinach-metadata-mysql +rspec-pg-rails5 0 28: *rspec-metadata-pg-rails5 +rspec-pg-rails5 1 28: *rspec-metadata-pg-rails5 +rspec-pg-rails5 2 28: *rspec-metadata-pg-rails5 +rspec-pg-rails5 3 28: *rspec-metadata-pg-rails5 +rspec-pg-rails5 4 28: *rspec-metadata-pg-rails5 +rspec-pg-rails5 5 28: *rspec-metadata-pg-rails5 +rspec-pg-rails5 6 28: *rspec-metadata-pg-rails5 +rspec-pg-rails5 7 28: *rspec-metadata-pg-rails5 +rspec-pg-rails5 8 28: *rspec-metadata-pg-rails5 +rspec-pg-rails5 9 28: *rspec-metadata-pg-rails5 +rspec-pg-rails5 10 28: *rspec-metadata-pg-rails5 +rspec-pg-rails5 11 28: *rspec-metadata-pg-rails5 +rspec-pg-rails5 12 28: *rspec-metadata-pg-rails5 +rspec-pg-rails5 13 28: *rspec-metadata-pg-rails5 +rspec-pg-rails5 14 28: *rspec-metadata-pg-rails5 +rspec-pg-rails5 15 28: *rspec-metadata-pg-rails5 +rspec-pg-rails5 16 28: *rspec-metadata-pg-rails5 +rspec-pg-rails5 17 28: *rspec-metadata-pg-rails5 +rspec-pg-rails5 18 28: *rspec-metadata-pg-rails5 +rspec-pg-rails5 19 28: *rspec-metadata-pg-rails5 +rspec-pg-rails5 20 28: *rspec-metadata-pg-rails5 +rspec-pg-rails5 21 28: *rspec-metadata-pg-rails5 +rspec-pg-rails5 22 28: *rspec-metadata-pg-rails5 +rspec-pg-rails5 23 28: *rspec-metadata-pg-rails5 +rspec-pg-rails5 24 28: *rspec-metadata-pg-rails5 +rspec-pg-rails5 25 28: *rspec-metadata-pg-rails5 +rspec-pg-rails5 26 28: *rspec-metadata-pg-rails5 +rspec-pg-rails5 27 28: *rspec-metadata-pg-rails5 + +rspec-mysql-rails5 0 28: *rspec-metadata-mysql-rails5 +rspec-mysql-rails5 1 28: *rspec-metadata-mysql-rails5 +rspec-mysql-rails5 2 28: *rspec-metadata-mysql-rails5 +rspec-mysql-rails5 3 28: *rspec-metadata-mysql-rails5 +rspec-mysql-rails5 4 28: *rspec-metadata-mysql-rails5 +rspec-mysql-rails5 5 28: *rspec-metadata-mysql-rails5 +rspec-mysql-rails5 6 28: *rspec-metadata-mysql-rails5 +rspec-mysql-rails5 7 28: *rspec-metadata-mysql-rails5 +rspec-mysql-rails5 8 28: *rspec-metadata-mysql-rails5 +rspec-mysql-rails5 9 28: *rspec-metadata-mysql-rails5 +rspec-mysql-rails5 10 28: *rspec-metadata-mysql-rails5 +rspec-mysql-rails5 11 28: *rspec-metadata-mysql-rails5 +rspec-mysql-rails5 12 28: *rspec-metadata-mysql-rails5 +rspec-mysql-rails5 13 28: *rspec-metadata-mysql-rails5 +rspec-mysql-rails5 14 28: *rspec-metadata-mysql-rails5 +rspec-mysql-rails5 15 28: *rspec-metadata-mysql-rails5 +rspec-mysql-rails5 16 28: *rspec-metadata-mysql-rails5 +rspec-mysql-rails5 17 28: *rspec-metadata-mysql-rails5 +rspec-mysql-rails5 18 28: *rspec-metadata-mysql-rails5 +rspec-mysql-rails5 19 28: *rspec-metadata-mysql-rails5 +rspec-mysql-rails5 20 28: *rspec-metadata-mysql-rails5 +rspec-mysql-rails5 21 28: *rspec-metadata-mysql-rails5 +rspec-mysql-rails5 22 28: *rspec-metadata-mysql-rails5 +rspec-mysql-rails5 23 28: *rspec-metadata-mysql-rails5 +rspec-mysql-rails5 24 28: *rspec-metadata-mysql-rails5 +rspec-mysql-rails5 25 28: *rspec-metadata-mysql-rails5 +rspec-mysql-rails5 26 28: *rspec-metadata-mysql-rails5 +rspec-mysql-rails5 27 28: *rspec-metadata-mysql-rails5 + +spinach-pg-rails5 0 2: *spinach-metadata-pg-rails5 +spinach-pg-rails5 1 2: *spinach-metadata-pg-rails5 + +spinach-mysql-rails5 0 2: *spinach-metadata-mysql-rails5 +spinach-mysql-rails5 1 2: *spinach-metadata-mysql-rails5 + static-analysis: <<: *dedicated-no-docs-no-db-pull-cache-job dependencies: @@ -466,7 +571,7 @@ static-analysis: script: - scripts/static-analysis cache: - key: "ruby-2.3.6-with-yarn-and-rubocop" + key: "ruby-2.3.7-with-yarn-and-rubocop" paths: - vendor/ruby - .yarn-cache/ @@ -608,21 +713,23 @@ karma: codequality: <<: *dedicated-no-docs-no-db-pull-cache-job - image: docker:latest + image: docker:stable + allow_failure: true + # gitlab-org runners set `privileged: false` but we need to have it set to true + # since we're using Docker in Docker + tags: [] before_script: [] services: - docker:dind variables: SETUP_DB: "false" DOCKER_DRIVER: overlay2 - CODECLIMATE_FORMAT: json cache: {} dependencies: [] script: - - apk update && apk add jq - - ./scripts/codequality analyze -f json > raw_codeclimate.json || true - # The following line keeps only the fields used in the MR widget, reducing the JSON artifact size - - jq -c 'map({check_name,description,fingerprint,location})' raw_codeclimate.json > codeclimate.json + # Extract "MAJOR.MINOR" from CI_SERVER_VERSION and generate "MAJOR-MINOR-stable" for Security Products + - export SP_VERSION=$(echo "$CI_SERVER_VERSION" | sed 's/^\([0-9]*\)\.\([0-9]*\).*/\1-\2-stable/') + - docker run --env SOURCE_CODE="$PWD" --volume "$PWD":/code --volume /var/run/docker.sock:/var/run/docker.sock "registry.gitlab.com/gitlab-org/security-products/codequality:$SP_VERSION" /code artifacts: paths: [codeclimate.json] expire_in: 1 week @@ -655,7 +762,13 @@ qa:selectors: - bundle exec bin/qa Test::Sanity::Selectors coverage: - <<: *dedicated-no-docs-no-db-pull-cache-job + # Don't include dedicated-no-docs-no-db-pull-cache-job here since we need to + # download artifacts from all the rspec jobs instead of from setup-test-env only + <<: *dedicated-runner + <<: *except-docs-and-qa + <<: *pull-cache + variables: + SETUP_DB: "false" stage: post-test script: - bundle exec scripts/merge-simplecov diff --git a/.gitlab/merge_request_templates/Database Changes.md b/.gitlab/merge_request_templates/Database Changes.md index 8302b3b30c7..68bc0fd1c7f 100644 --- a/.gitlab/merge_request_templates/Database Changes.md +++ b/.gitlab/merge_request_templates/Database Changes.md @@ -33,13 +33,16 @@ When removing columns, tables, indexes or other structures: ## General Checklist -- [ ] [Changelog entry](https://docs.gitlab.com/ce/development/changelog.html) added, if necessary -- [ ] [Documentation created/updated](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/development/doc_styleguide.md) +- [ ] [Changelog entry](https://docs.gitlab.com/ee/development/changelog.html) added, if necessary +- [ ] [Documentation created/updated](https://docs.gitlab.com/ee/development/doc_styleguide.html) - [ ] API support added - [ ] Tests added for this feature/bug - Review - [ ] Has been reviewed by Backend - [ ] Has been reviewed by Database -- [ ] Conform by the [merge request performance guides](http://docs.gitlab.com/ce/development/merge_request_performance_guidelines.html) -- [ ] Conform by the [style guides](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#style-guides) +- [ ] Conform by the [merge request performance guides](https://docs.gitlab.com/ee/development/merge_request_performance_guidelines.html) +- [ ] Conform by the [style guides](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/CONTRIBUTING.md#style-guides) - [ ] [Squashed related commits together](https://git-scm.com/book/en/Git-Tools-Rewriting-History#Squashing-Commits) +- [ ] Internationalization required/considered +- [ ] If paid feature, have we considered GitLab.com plan and how it works for groups and is there a design for promoting it to users who aren't on the correct plan +- [ ] End-to-end tests pass (`package-qa` manual pipeline job) diff --git a/.ruby-version b/.ruby-version index e75da3e63d6..00355e29d11 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -2.3.6 +2.3.7 diff --git a/.scss-lint.yml b/.scss-lint.yml index dcd4cac780a..180d377d6f8 100644 --- a/.scss-lint.yml +++ b/.scss-lint.yml @@ -59,6 +59,8 @@ linters: # Reports when you define the same property twice in a single rule set. DuplicateProperty: enabled: true + ignore_consecutive: + - cursor # Separate rule, function, and mixin declarations with empty lines. EmptyLineBetweenBlocks: diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a90a7fcdc2..d56c86523f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,32 @@ documentation](doc/development/changelog.md) for instructions on adding your own entry. +## 10.6.4 (2018-04-09) + +### Fixed (8 changes, 1 of them is from the community) + +- Correct copy text for the promote milestone and label modals. !17726 +- Avoid validation errors when running the Pages domain verification service. !17992 +- Fix autolinking URLs containing ampersands. !18045 +- Fix exceptions raised when migrating pipeline stages in the background. !18076 +- Work around Prometheus Helm chart name changes to fix integration. !18206 (joshlambert) +- Don't show Jump to Discussion button on Issues. +- Fix listing commit branch/tags that contain special characters. +- Fix 404 in group boards when moving issue between lists. + +### Performance (1 change) + +- Free open file descriptors and libgit2 buffers in UpdatePagesService. + + +## 10.6.3 (2018-04-03) + +### Security (2 changes) + +- Fix XSS on diff view stored on filenames. +- Adds confidential notes channel for Slack/Mattermost. + + ## 10.6.2 (2018-03-29) ### Fixed (2 changes, 1 of them is from the community) @@ -191,7 +217,6 @@ entry. - Enable privileged mode for GitLab Runner. !17528 - Expose GITLAB_FEATURES as CI/CD variable (fixes #40994). - Upgrade GitLab Workhorse to 4.0.0. -- Allow CI/CD Jobs being grouped on version strings. - Add discussions API for Issues and Snippets. - Add one group board to Libre. - Add support for filtering by source and target branch to merge requests API. @@ -218,6 +243,14 @@ entry. - Use host URL to build JIRA remote link icon. +## 10.5.7 (2018-04-03) + +### Security (2 changes) + +- Fix XSS on diff view stored on filenames. +- Adds confidential notes channel for Slack/Mattermost. + + ## 10.5.6 (2018-03-16) ### Security (2 changes) @@ -485,6 +518,14 @@ entry. - Adds empty state illustration for pending job. +## 10.4.7 (2018-04-03) + +### Security (2 changes) + +- Fix XSS on diff view stored on filenames. +- Adds confidential notes channel for Slack/Mattermost. + + ## 10.4.6 (2018-03-16) ### Security (2 changes) diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index 36545ad338e..5f8cbfdb7d7 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -0.92.0 +0.95.0 diff --git a/GITLAB_PAGES_VERSION b/GITLAB_PAGES_VERSION index 39e898a4f95..a3df0a6959e 100644 --- a/GITLAB_PAGES_VERSION +++ b/GITLAB_PAGES_VERSION @@ -1 +1 @@ -0.7.1 +0.8.0 diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION index 21c8c7b46b8..a8a18875682 100644 --- a/GITLAB_SHELL_VERSION +++ b/GITLAB_SHELL_VERSION @@ -1 +1 @@ -7.1.1 +7.1.2 diff --git a/Gemfile b/Gemfile index a1e43700260..647138cd90f 100644 --- a/Gemfile +++ b/Gemfile @@ -6,7 +6,6 @@ end gem_versions = {} gem_versions['activerecord_sane_schema_dumper'] = rails5? ? '1.0' : '0.2' gem_versions['default_value_for'] = rails5? ? '~> 3.0.5' : '~> 3.0.0' -gem_versions['html-pipeline'] = rails5? ? '~> 2.6.0' : '~> 1.11.0' gem_versions['rails'] = rails5? ? '5.0.6' : '4.2.10' gem_versions['rails-i18n'] = rails5? ? '~> 5.1' : '~> 4.0.9' # --- The end of special code for migrating to Rails 5.0 --- @@ -136,7 +135,7 @@ gem 'unf', '~> 0.1.4' gem 'seed-fu', '~> 2.3.7' # Markdown and HTML processing -gem 'html-pipeline', gem_versions['html-pipeline'] +gem 'html-pipeline', '~> 2.7.1' gem 'deckar01-task_list', '2.0.0' gem 'gitlab-markup', '~> 1.6.2' gem 'redcarpet', '~> 3.4' @@ -310,7 +309,7 @@ end group :development do gem 'foreman', '~> 0.84.0' - gem 'brakeman', '~> 3.6.0', require: false + gem 'brakeman', '~> 4.2', require: false gem 'letter_opener_web', '~> 1.3.0' gem 'rblineprof', '~> 0.3.6', platform: :mri, require: false @@ -385,7 +384,8 @@ group :test do gem 'email_spec', '~> 1.6.0' gem 'json-schema', '~> 2.8.0' gem 'webmock', '~> 2.3.2' - gem 'test_after_commit', '~> 1.1' + gem 'rails-controller-testing' if rails5? # Rails5 only gem. + gem 'test_after_commit', '~> 1.1' unless rails5? # Remove this gem when migrated to rails 5.0. It's been integrated to rails 5.0. gem 'sham_rack', '~> 1.3.6' gem 'concurrent-ruby', '~> 1.0.5' gem 'test-prof', '~> 0.2.5' @@ -422,7 +422,7 @@ group :ed25519 do end # Gitaly GRPC client -gem 'gitaly-proto', '~> 0.91.0', require: 'gitaly' +gem 'gitaly-proto', '~> 0.94.0', require: 'gitaly' gem 'grpc', '~> 1.10.0' # Locked until https://github.com/google/protobuf/issues/4210 is closed @@ -441,3 +441,5 @@ gem 'grape_logging', '~> 1.7' # Asset synchronization gem 'asset_sync', '~> 2.2.0' + +gem 'goldiloader', '~> 2.0' diff --git a/Gemfile.lock b/Gemfile.lock index bb510f6b539..76e1a17155f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -95,7 +95,7 @@ GEM autoprefixer-rails (>= 5.2.1) sass (>= 3.3.4) bootstrap_form (2.7.0) - brakeman (3.6.1) + brakeman (4.2.1) browser (2.2.0) builder (3.2.3) bullet (5.5.1) @@ -120,7 +120,7 @@ GEM activesupport (>= 4.0.0) mime-types (>= 1.16) cause (0.1) - charlock_holmes (0.7.5) + charlock_holmes (0.7.6) childprocess (0.7.0) ffi (~> 1.0, >= 1.0.11) chronic (0.10.2) @@ -290,7 +290,7 @@ GEM po_to_json (>= 1.0.0) rails (>= 3.2.0) gherkin-ruby (0.3.2) - gitaly-proto (0.91.0) + gitaly-proto (0.94.0) google-protobuf (~> 3.1) grpc (~> 1.0) github-linguist (5.3.3) @@ -320,6 +320,9 @@ GEM rubyntlm (~> 0.5) globalid (0.4.1) activesupport (>= 4.2.0) + goldiloader (2.0.1) + activerecord (>= 4.2, < 5.2) + activesupport (>= 4.2, < 5.2) gollum-grit_adapter (1.0.1) gitlab-grit (~> 2.7, >= 2.7.1) gollum-lib (4.2.7) @@ -399,9 +402,9 @@ GEM hipchat (1.5.2) httparty mimemagic - html-pipeline (1.11.0) + html-pipeline (2.7.1) activesupport (>= 2) - nokogiri (~> 1.4) + nokogiri (>= 1.4) html2text (0.2.0) nokogiri (~> 1.6) htmlentities (4.3.4) @@ -587,7 +590,7 @@ GEM orm_adapter (0.5.0) os (0.9.6) parallel (1.12.1) - parser (2.5.0.3) + parser (2.5.0.5) ast (~> 2.4.0) parslet (1.5.0) blankslate (~> 2.0) @@ -1012,7 +1015,7 @@ DEPENDENCIES binding_of_caller (~> 0.7.2) bootstrap-sass (~> 3.3.0) bootstrap_form (~> 2.7.0) - brakeman (~> 3.6.0) + brakeman (~> 4.2) browser (~> 2.2) bullet (~> 5.5.0) bundler-audit (~> 0.5.0) @@ -1061,12 +1064,13 @@ DEPENDENCIES gettext (~> 3.2.2) gettext_i18n_rails (~> 1.8.0) gettext_i18n_rails_js (~> 1.3) - gitaly-proto (~> 0.91.0) + gitaly-proto (~> 0.94.0) github-linguist (~> 5.3.3) gitlab-flowdock-git-hook (~> 1.0.1) gitlab-markup (~> 1.6.2) gitlab-styles (~> 2.3) gitlab_omniauth-ldap (~> 2.0.4) + goldiloader (~> 2.0) gollum-lib (~> 4.2) gollum-rugged_adapter (~> 0.4.4) gon (~> 6.1.0) @@ -1083,7 +1087,7 @@ DEPENDENCIES hashie-forbidden_attributes health_check (~> 2.6.0) hipchat (~> 1.5.0) - html-pipeline (~> 1.11.0) + html-pipeline (~> 2.7.1) html2text httparty (~> 0.13.3) influxdb (~> 0.2) diff --git a/Gemfile.rails5.lock b/Gemfile.rails5.lock index 86a22dbe550..03fe5f2ed26 100644 --- a/Gemfile.rails5.lock +++ b/Gemfile.rails5.lock @@ -60,7 +60,7 @@ GEM faraday_middleware-multi_json (~> 0.0) oauth2 (~> 1.0) asciidoctor (1.5.6.1) - asciidoctor-plantuml (0.0.7) + asciidoctor-plantuml (0.0.8) asciidoctor (~> 1.5) asset_sync (2.2.0) activemodel (>= 4.1.0) @@ -97,7 +97,7 @@ GEM autoprefixer-rails (>= 5.2.1) sass (>= 3.3.4) bootstrap_form (2.7.0) - brakeman (3.6.2) + brakeman (4.2.1) browser (2.5.3) builder (3.2.3) bullet (5.5.1) @@ -144,6 +144,7 @@ GEM connection_pool (2.2.1) crack (0.4.3) safe_yaml (~> 1.0.0) + crass (1.0.3) creole (0.5.0) css_parser (1.6.0) addressable @@ -244,10 +245,11 @@ GEM builder excon (~> 0.58) formatador (~> 0.2) - fog-google (0.6.0) + fog-google (1.3.3) fog-core fog-json fog-xml + google-api-client (~> 0.19.1) fog-json (1.0.2) fog-core (~> 1.0) multi_json (~> 1.10) @@ -289,7 +291,7 @@ GEM po_to_json (>= 1.0.0) rails (>= 3.2.0) gherkin-ruby (0.3.2) - gitaly-proto (0.88.0) + gitaly-proto (0.94.0) google-protobuf (~> 3.1) grpc (~> 1.0) github-linguist (5.3.3) @@ -398,7 +400,7 @@ GEM hipchat (1.5.4) httparty mimemagic - html-pipeline (2.6.0) + html-pipeline (2.7.1) activesupport (>= 2) nokogiri (>= 1.4) html2text (0.2.1) @@ -484,7 +486,8 @@ GEM activesupport (>= 4) railties (>= 4) request_store (~> 1.0) - loofah (2.0.3) + loofah (2.2.2) + crass (~> 1.0.2) nokogiri (>= 1.5.9) mail (2.7.0) mini_mime (>= 0.1.1) @@ -527,8 +530,8 @@ GEM omniauth (1.8.1) hashie (>= 3.4.6, < 3.6.0) rack (>= 1.6.2, < 3) - omniauth-auth0 (1.4.2) - omniauth-oauth2 (~> 1.1) + omniauth-auth0 (2.0.0) + omniauth-oauth2 (~> 1.4) omniauth-authentiq (0.3.1) omniauth-oauth2 (~> 1.3, >= 1.3.1) omniauth-azure-oauth2 (0.0.9) @@ -551,6 +554,9 @@ GEM jwt (>= 1.5) omniauth (>= 1.1.1) omniauth-oauth2 (>= 1.5) + omniauth-jwt (0.0.2) + jwt + omniauth (~> 1.1) omniauth-kerberos (0.3.0) omniauth-multipassword timfel-krb5-auth (~> 0.8) @@ -569,9 +575,9 @@ GEM ruby-saml (~> 1.7) omniauth-shibboleth (1.2.1) omniauth (>= 1.0.0) - omniauth-twitter (1.2.1) - json (~> 1.3) + omniauth-twitter (1.4.0) omniauth-oauth (~> 1.1) + rack omniauth_crowd (2.2.3) activesupport nokogiri (>= 1.4.4) @@ -581,7 +587,7 @@ GEM orm_adapter (0.5.0) os (0.9.6) parallel (1.12.1) - parser (2.5.0.4) + parser (2.5.0.5) ast (~> 2.4.0) parslet (1.5.0) blankslate (~> 2.0) @@ -672,6 +678,10 @@ GEM bundler (>= 1.3.0) railties (= 5.0.6) sprockets-rails (>= 2.0.0) + rails-controller-testing (1.0.2) + actionpack (~> 5.x, >= 5.0.1) + actionview (~> 5.x, >= 5.0.1) + activesupport (~> 5.x) rails-deprecated_sanitizer (1.0.3) activesupport (>= 4.2.0.alpha) rails-dom-testing (2.0.3) @@ -808,7 +818,7 @@ GEM rubyzip (1.2.1) rufus-scheduler (3.4.2) et-orbi (~> 1.0) - rugged (0.26.0) + rugged (0.27.0) safe_yaml (1.0.4) sanitize (2.1.0) nokogiri (>= 1.4.4) @@ -907,8 +917,6 @@ GEM sysexits (1.2.0) temple (0.7.7) test-prof (0.2.5) - test_after_commit (1.1.0) - activerecord (>= 3.2) text (1.3.1) thin (1.7.2) daemons (~> 1.0, >= 1.0.9) @@ -995,8 +1003,8 @@ DEPENDENCIES akismet (~> 2.0) allocations (~> 1.0) asana (~> 0.6.0) - asciidoctor (~> 1.5.2) - asciidoctor-plantuml (= 0.0.7) + asciidoctor (~> 1.5.6) + asciidoctor-plantuml (= 0.0.8) asset_sync (~> 2.2.0) attr_encrypted (~> 3.0.0) awesome_print (~> 1.2.0) @@ -1009,7 +1017,7 @@ DEPENDENCIES binding_of_caller (~> 0.7.2) bootstrap-sass (~> 3.3.0) bootstrap_form (~> 2.7.0) - brakeman (~> 3.6.0) + brakeman (~> 4.2) browser (~> 2.2) bullet (~> 5.5.0) bundler-audit (~> 0.5.0) @@ -1044,9 +1052,9 @@ DEPENDENCIES flipper-active_record (~> 0.13.0) flipper-active_support_cache_store (~> 0.13.0) fog-aliyun (~> 0.2.0) - fog-aws (~> 2.0) + fog-aws (~> 2.0.1) fog-core (~> 1.44) - fog-google (~> 0.5) + fog-google (~> 1.3.3) fog-local (~> 0.3) fog-openstack (~> 0.1) fog-rackspace (~> 0.1.1) @@ -1058,7 +1066,7 @@ DEPENDENCIES gettext (~> 3.2.2) gettext_i18n_rails (~> 1.8.0) gettext_i18n_rails_js (~> 1.3) - gitaly-proto (~> 0.88.0) + gitaly-proto (~> 0.94.0) github-linguist (~> 5.3.3) gitlab-flowdock-git-hook (~> 1.0.1) gitlab-markup (~> 1.6.2) @@ -1080,7 +1088,7 @@ DEPENDENCIES hashie-forbidden_attributes health_check (~> 2.6.0) hipchat (~> 1.5.0) - html-pipeline (~> 2.6.0) + html-pipeline (~> 2.7.1) html2text httparty (~> 0.13.3) influxdb (~> 0.2) @@ -1095,7 +1103,7 @@ DEPENDENCIES license_finder (~> 3.1) licensee (~> 8.9) lograge (~> 0.5) - loofah (~> 2.0.3) + loofah (~> 2.2) mail_room (~> 0.9.1) method_source (~> 0.8) minitest (~> 5.7.0) @@ -1107,19 +1115,20 @@ DEPENDENCIES oauth2 (~> 1.4) octokit (~> 4.8) omniauth (~> 1.8) - omniauth-auth0 (~> 1.4.1) + omniauth-auth0 (~> 2.0.0) omniauth-authentiq (~> 0.3.1) omniauth-azure-oauth2 (~> 0.0.9) omniauth-cas3 (~> 1.1.4) omniauth-facebook (~> 4.0.0) omniauth-github (~> 1.1.1) omniauth-gitlab (~> 1.0.2) - omniauth-google-oauth2 (~> 0.5.2) + omniauth-google-oauth2 (~> 0.5.3) + omniauth-jwt (~> 0.0.2) omniauth-kerberos (~> 0.3.0) omniauth-oauth2-generic (~> 0.2.2) omniauth-saml (~> 1.10) omniauth-shibboleth (~> 1.2.0) - omniauth-twitter (~> 1.2.0) + omniauth-twitter (~> 1.4) omniauth_crowd (~> 2.2.0) org-ruby (~> 0.9.12) peek (~> 1.0.1) @@ -1140,6 +1149,7 @@ DEPENDENCIES rack-oauth2 (~> 1.2.1) rack-proxy (~> 0.6.0) rails (= 5.0.6) + rails-controller-testing rails-deprecated_sanitizer (~> 1.0.3) rails-i18n (~> 5.1) rainbow (~> 2.2) @@ -1169,7 +1179,7 @@ DEPENDENCIES ruby-prof (~> 0.17.0) ruby_parser (~> 3.8) rufus-scheduler (~> 3.4) - rugged (~> 0.26.0) + rugged (~> 0.27) sanitize (~> 2.0) sass-rails (~> 5.0.6) scss_lint (~> 0.56.0) @@ -1197,7 +1207,6 @@ DEPENDENCIES state_machines-activerecord (~> 0.5.1) sys-filesystem (~> 1.1.6) test-prof (~> 0.2.5) - test_after_commit (~> 1.1) thin (~> 1.7.0) timecop (~> 0.8.0) toml-rb (~> 1.0.0) diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js index cbcefb2c18f..8ad3d18b302 100644 --- a/app/assets/javascripts/api.js +++ b/app/assets/javascripts/api.js @@ -10,6 +10,9 @@ const Api = { projectsPath: '/api/:version/projects.json', projectPath: '/api/:version/projects/:id', projectLabelsPath: '/:namespace_path/:project_path/labels', + mergeRequestPath: '/api/:version/projects/:id/merge_requests/:mrid', + mergeRequestChangesPath: '/api/:version/projects/:id/merge_requests/:mrid/changes', + mergeRequestVersionsPath: '/api/:version/projects/:id/merge_requests/:mrid/versions', groupLabelsPath: '/groups/:namespace_path/-/labels', licensePath: '/api/:version/templates/licenses/:key', gitignorePath: '/api/:version/templates/gitignores/:key', @@ -22,25 +25,27 @@ const Api = { createBranchPath: '/api/:version/projects/:id/repository/branches', group(groupId, callback) { - const url = Api.buildUrl(Api.groupPath) - .replace(':id', groupId); - return axios.get(url) - .then(({ data }) => { - callback(data); + const url = Api.buildUrl(Api.groupPath).replace(':id', groupId); + return axios.get(url).then(({ data }) => { + callback(data); - return data; - }); + return data; + }); }, // Return groups list. Filtered by query groups(query, options, callback = $.noop) { const url = Api.buildUrl(Api.groupsPath); - return axios.get(url, { - params: Object.assign({ - search: query, - per_page: 20, - }, options), - }) + return axios + .get(url, { + params: Object.assign( + { + search: query, + per_page: 20, + }, + options, + ), + }) .then(({ data }) => { callback(data); @@ -51,12 +56,13 @@ const Api = { // Return namespaces list. Filtered by query namespaces(query, callback) { const url = Api.buildUrl(Api.namespacesPath); - return axios.get(url, { - params: { - search: query, - per_page: 20, - }, - }) + return axios + .get(url, { + params: { + search: query, + per_page: 20, + }, + }) .then(({ data }) => callback(data)); }, @@ -73,9 +79,10 @@ const Api = { defaults.membership = true; } - return axios.get(url, { - params: Object.assign(defaults, options), - }) + return axios + .get(url, { + params: Object.assign(defaults, options), + }) .then(({ data }) => { callback(data); @@ -85,8 +92,32 @@ const Api = { // Return single project project(projectPath) { - const url = Api.buildUrl(Api.projectPath) - .replace(':id', encodeURIComponent(projectPath)); + const url = Api.buildUrl(Api.projectPath).replace(':id', encodeURIComponent(projectPath)); + + return axios.get(url); + }, + + // Return Merge Request for project + mergeRequest(projectPath, mergeRequestId) { + const url = Api.buildUrl(Api.mergeRequestPath) + .replace(':id', encodeURIComponent(projectPath)) + .replace(':mrid', mergeRequestId); + + return axios.get(url); + }, + + mergeRequestChanges(projectPath, mergeRequestId) { + const url = Api.buildUrl(Api.mergeRequestChangesPath) + .replace(':id', encodeURIComponent(projectPath)) + .replace(':mrid', mergeRequestId); + + return axios.get(url); + }, + + mergeRequestVersions(projectPath, mergeRequestId) { + const url = Api.buildUrl(Api.mergeRequestVersionsPath) + .replace(':id', encodeURIComponent(projectPath)) + .replace(':mrid', mergeRequestId); return axios.get(url); }, @@ -102,30 +133,30 @@ const Api = { url = Api.buildUrl(Api.groupLabelsPath).replace(':namespace_path', namespacePath); } - return axios.post(url, { - label: data, - }) + return axios + .post(url, { + label: data, + }) .then(res => callback(res.data)) .catch(e => callback(e.response.data)); }, // Return group projects list. Filtered by query groupProjects(groupId, query, callback) { - const url = Api.buildUrl(Api.groupProjectsPath) - .replace(':id', groupId); - return axios.get(url, { - params: { - search: query, - per_page: 20, - }, - }) + const url = Api.buildUrl(Api.groupProjectsPath).replace(':id', groupId); + return axios + .get(url, { + params: { + search: query, + per_page: 20, + }, + }) .then(({ data }) => callback(data)); }, commitMultiple(id, data) { // see https://docs.gitlab.com/ce/api/commits.html#create-a-commit-with-multiple-files-and-actions - const url = Api.buildUrl(Api.commitPath) - .replace(':id', encodeURIComponent(id)); + const url = Api.buildUrl(Api.commitPath).replace(':id', encodeURIComponent(id)); return axios.post(url, JSON.stringify(data), { headers: { 'Content-Type': 'application/json; charset=utf-8', @@ -136,39 +167,34 @@ const Api = { branchSingle(id, branch) { const url = Api.buildUrl(Api.branchSinglePath) .replace(':id', encodeURIComponent(id)) - .replace(':branch', branch); + .replace(':branch', encodeURIComponent(branch)); return axios.get(url); }, // Return text for a specific license licenseText(key, data, callback) { - const url = Api.buildUrl(Api.licensePath) - .replace(':key', key); - return axios.get(url, { - params: data, - }) + const url = Api.buildUrl(Api.licensePath).replace(':key', key); + return axios + .get(url, { + params: data, + }) .then(res => callback(res.data)); }, gitignoreText(key, callback) { - const url = Api.buildUrl(Api.gitignorePath) - .replace(':key', key); - return axios.get(url) - .then(({ data }) => callback(data)); + const url = Api.buildUrl(Api.gitignorePath).replace(':key', key); + return axios.get(url).then(({ data }) => callback(data)); }, gitlabCiYml(key, callback) { - const url = Api.buildUrl(Api.gitlabCiYmlPath) - .replace(':key', key); - return axios.get(url) - .then(({ data }) => callback(data)); + const url = Api.buildUrl(Api.gitlabCiYmlPath).replace(':key', key); + return axios.get(url).then(({ data }) => callback(data)); }, dockerfileYml(key, callback) { const url = Api.buildUrl(Api.dockerfilePath).replace(':key', key); - return axios.get(url) - .then(({ data }) => callback(data)); + return axios.get(url).then(({ data }) => callback(data)); }, issueTemplate(namespacePath, projectPath, key, type, callback) { @@ -177,7 +203,8 @@ const Api = { .replace(':type', type) .replace(':project_path', projectPath) .replace(':namespace_path', namespacePath); - return axios.get(url) + return axios + .get(url) .then(({ data }) => callback(null, data)) .catch(callback); }, @@ -185,10 +212,13 @@ const Api = { users(query, options) { const url = Api.buildUrl(this.usersPath); return axios.get(url, { - params: Object.assign({ - search: query, - per_page: 20, - }, options), + params: Object.assign( + { + search: query, + per_page: 20, + }, + options, + ), }); }, diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js index 6da33a26e58..976d32abe9b 100644 --- a/app/assets/javascripts/awards_handler.js +++ b/app/assets/javascripts/awards_handler.js @@ -4,7 +4,8 @@ import $ from 'jquery'; import _ from 'underscore'; import Cookies from 'js-cookie'; import { __ } from './locale'; -import { isInIssuePage, isInMRPage, hasVueMRDiscussionsCookie, updateTooltipTitle } from './lib/utils/common_utils'; +import { updateTooltipTitle } from './lib/utils/common_utils'; +import { isInVueNoteablePage } from './lib/utils/dom_utils'; import flash from './flash'; import axios from './lib/utils/axios_utils'; @@ -243,7 +244,7 @@ class AwardsHandler { addAward(votesBlock, awardUrl, emoji, checkMutuality, callback) { const isMainAwardsBlock = votesBlock.closest('.js-noteable-awards').length; - if (this.isInVueNoteablePage() && !isMainAwardsBlock) { + if (isInVueNoteablePage() && !isMainAwardsBlock) { const id = votesBlock.attr('id').replace('note_', ''); this.hideMenuElement($('.emoji-menu')); @@ -295,16 +296,8 @@ class AwardsHandler { } } - isVueMRDiscussions() { - return isInMRPage() && hasVueMRDiscussionsCookie() && !$('#diffs').is(':visible'); - } - - isInVueNoteablePage() { - return isInIssuePage() || this.isVueMRDiscussions(); - } - getVotesBlock() { - if (this.isInVueNoteablePage()) { + if (isInVueNoteablePage()) { const $el = $('.js-add-award.is-active').closest('.note.timeline-entry'); if ($el.length) { diff --git a/app/assets/javascripts/badges/components/badge.vue b/app/assets/javascripts/badges/components/badge.vue new file mode 100644 index 00000000000..6e6cb31e3ac --- /dev/null +++ b/app/assets/javascripts/badges/components/badge.vue @@ -0,0 +1,121 @@ + + + diff --git a/app/assets/javascripts/badges/components/badge_form.vue b/app/assets/javascripts/badges/components/badge_form.vue new file mode 100644 index 00000000000..ae942b2c1a7 --- /dev/null +++ b/app/assets/javascripts/badges/components/badge_form.vue @@ -0,0 +1,219 @@ + + + diff --git a/app/assets/javascripts/badges/components/badge_list.vue b/app/assets/javascripts/badges/components/badge_list.vue new file mode 100644 index 00000000000..ca7197e1e0f --- /dev/null +++ b/app/assets/javascripts/badges/components/badge_list.vue @@ -0,0 +1,57 @@ + + + diff --git a/app/assets/javascripts/badges/components/badge_list_row.vue b/app/assets/javascripts/badges/components/badge_list_row.vue new file mode 100644 index 00000000000..af062bdf8c6 --- /dev/null +++ b/app/assets/javascripts/badges/components/badge_list_row.vue @@ -0,0 +1,89 @@ + + + diff --git a/app/assets/javascripts/badges/components/badge_settings.vue b/app/assets/javascripts/badges/components/badge_settings.vue new file mode 100644 index 00000000000..83f78394238 --- /dev/null +++ b/app/assets/javascripts/badges/components/badge_settings.vue @@ -0,0 +1,70 @@ + + + diff --git a/app/assets/javascripts/badges/constants.js b/app/assets/javascripts/badges/constants.js new file mode 100644 index 00000000000..8fbe3db5ef1 --- /dev/null +++ b/app/assets/javascripts/badges/constants.js @@ -0,0 +1,2 @@ +export const GROUP_BADGE = 'group'; +export const PROJECT_BADGE = 'project'; diff --git a/app/assets/javascripts/badges/empty_badge.js b/app/assets/javascripts/badges/empty_badge.js new file mode 100644 index 00000000000..49a9b5e1be8 --- /dev/null +++ b/app/assets/javascripts/badges/empty_badge.js @@ -0,0 +1,7 @@ +export default () => ({ + imageUrl: '', + isDeleting: false, + linkUrl: '', + renderedImageUrl: '', + renderedLinkUrl: '', +}); diff --git a/app/assets/javascripts/badges/store/actions.js b/app/assets/javascripts/badges/store/actions.js new file mode 100644 index 00000000000..5542278b3e0 --- /dev/null +++ b/app/assets/javascripts/badges/store/actions.js @@ -0,0 +1,167 @@ +import axios from '~/lib/utils/axios_utils'; +import types from './mutation_types'; + +export const transformBackendBadge = badge => ({ + id: badge.id, + imageUrl: badge.image_url, + kind: badge.kind, + linkUrl: badge.link_url, + renderedImageUrl: badge.rendered_image_url, + renderedLinkUrl: badge.rendered_link_url, + isDeleting: false, +}); + +export default { + requestNewBadge({ commit }) { + commit(types.REQUEST_NEW_BADGE); + }, + receiveNewBadge({ commit }, newBadge) { + commit(types.RECEIVE_NEW_BADGE, newBadge); + }, + receiveNewBadgeError({ commit }) { + commit(types.RECEIVE_NEW_BADGE_ERROR); + }, + addBadge({ dispatch, state }) { + const newBadge = state.badgeInAddForm; + const endpoint = state.apiEndpointUrl; + dispatch('requestNewBadge'); + return axios + .post(endpoint, { + image_url: newBadge.imageUrl, + link_url: newBadge.linkUrl, + }) + .catch(error => { + dispatch('receiveNewBadgeError'); + throw error; + }) + .then(res => { + dispatch('receiveNewBadge', transformBackendBadge(res.data)); + }); + }, + requestDeleteBadge({ commit }, badgeId) { + commit(types.REQUEST_DELETE_BADGE, badgeId); + }, + receiveDeleteBadge({ commit }, badgeId) { + commit(types.RECEIVE_DELETE_BADGE, badgeId); + }, + receiveDeleteBadgeError({ commit }, badgeId) { + commit(types.RECEIVE_DELETE_BADGE_ERROR, badgeId); + }, + deleteBadge({ dispatch, state }, badge) { + const badgeId = badge.id; + dispatch('requestDeleteBadge', badgeId); + const endpoint = `${state.apiEndpointUrl}/${badgeId}`; + return axios + .delete(endpoint) + .catch(error => { + dispatch('receiveDeleteBadgeError', badgeId); + throw error; + }) + .then(() => { + dispatch('receiveDeleteBadge', badgeId); + }); + }, + + editBadge({ commit }, badge) { + commit(types.START_EDITING, badge); + }, + + requestLoadBadges({ commit }, data) { + commit(types.REQUEST_LOAD_BADGES, data); + }, + receiveLoadBadges({ commit }, badges) { + commit(types.RECEIVE_LOAD_BADGES, badges); + }, + receiveLoadBadgesError({ commit }) { + commit(types.RECEIVE_LOAD_BADGES_ERROR); + }, + + loadBadges({ dispatch, state }, data) { + dispatch('requestLoadBadges', data); + const endpoint = state.apiEndpointUrl; + return axios + .get(endpoint) + .catch(error => { + dispatch('receiveLoadBadgesError'); + throw error; + }) + .then(res => { + dispatch('receiveLoadBadges', res.data.map(transformBackendBadge)); + }); + }, + + requestRenderedBadge({ commit }) { + commit(types.REQUEST_RENDERED_BADGE); + }, + receiveRenderedBadge({ commit }, renderedBadge) { + commit(types.RECEIVE_RENDERED_BADGE, renderedBadge); + }, + receiveRenderedBadgeError({ commit }) { + commit(types.RECEIVE_RENDERED_BADGE_ERROR); + }, + + renderBadge({ dispatch, state }) { + const badge = state.isEditing ? state.badgeInEditForm : state.badgeInAddForm; + const { linkUrl, imageUrl } = badge; + if (!linkUrl || linkUrl.trim() === '' || !imageUrl || imageUrl.trim() === '') { + return Promise.resolve(badge); + } + + dispatch('requestRenderedBadge'); + + const parameters = [ + `link_url=${encodeURIComponent(linkUrl)}`, + `image_url=${encodeURIComponent(imageUrl)}`, + ].join('&'); + const renderEndpoint = `${state.apiEndpointUrl}/render?${parameters}`; + return axios + .get(renderEndpoint) + .catch(error => { + dispatch('receiveRenderedBadgeError'); + throw error; + }) + .then(res => { + dispatch('receiveRenderedBadge', transformBackendBadge(res.data)); + }); + }, + + requestUpdatedBadge({ commit }) { + commit(types.REQUEST_UPDATED_BADGE); + }, + receiveUpdatedBadge({ commit }, updatedBadge) { + commit(types.RECEIVE_UPDATED_BADGE, updatedBadge); + }, + receiveUpdatedBadgeError({ commit }) { + commit(types.RECEIVE_UPDATED_BADGE_ERROR); + }, + + saveBadge({ dispatch, state }) { + const badge = state.badgeInEditForm; + const endpoint = `${state.apiEndpointUrl}/${badge.id}`; + dispatch('requestUpdatedBadge'); + return axios + .put(endpoint, { + image_url: badge.imageUrl, + link_url: badge.linkUrl, + }) + .catch(error => { + dispatch('receiveUpdatedBadgeError'); + throw error; + }) + .then(res => { + dispatch('receiveUpdatedBadge', transformBackendBadge(res.data)); + }); + }, + + stopEditing({ commit }) { + commit(types.STOP_EDITING); + }, + + updateBadgeInForm({ commit }, badge) { + commit(types.UPDATE_BADGE_IN_FORM, badge); + }, + + updateBadgeInModal({ commit }, badge) { + commit(types.UPDATE_BADGE_IN_MODAL, badge); + }, +}; diff --git a/app/assets/javascripts/badges/store/index.js b/app/assets/javascripts/badges/store/index.js new file mode 100644 index 00000000000..7a5df403a0e --- /dev/null +++ b/app/assets/javascripts/badges/store/index.js @@ -0,0 +1,13 @@ +import Vue from 'vue'; +import Vuex from 'vuex'; +import createState from './state'; +import actions from './actions'; +import mutations from './mutations'; + +Vue.use(Vuex); + +export default new Vuex.Store({ + state: createState(), + actions, + mutations, +}); diff --git a/app/assets/javascripts/badges/store/mutation_types.js b/app/assets/javascripts/badges/store/mutation_types.js new file mode 100644 index 00000000000..d73f91b6005 --- /dev/null +++ b/app/assets/javascripts/badges/store/mutation_types.js @@ -0,0 +1,21 @@ +export default { + RECEIVE_DELETE_BADGE: 'RECEIVE_DELETE_BADGE', + RECEIVE_DELETE_BADGE_ERROR: 'RECEIVE_DELETE_BADGE_ERROR', + RECEIVE_LOAD_BADGES: 'RECEIVE_LOAD_BADGES', + RECEIVE_LOAD_BADGES_ERROR: 'RECEIVE_LOAD_BADGES_ERROR', + RECEIVE_NEW_BADGE: 'RECEIVE_NEW_BADGE', + RECEIVE_NEW_BADGE_ERROR: 'RECEIVE_NEW_BADGE_ERROR', + RECEIVE_RENDERED_BADGE: 'RECEIVE_RENDERED_BADGE', + RECEIVE_RENDERED_BADGE_ERROR: 'RECEIVE_RENDERED_BADGE_ERROR', + RECEIVE_UPDATED_BADGE: 'RECEIVE_UPDATED_BADGE', + RECEIVE_UPDATED_BADGE_ERROR: 'RECEIVE_UPDATED_BADGE_ERROR', + REQUEST_DELETE_BADGE: 'REQUEST_DELETE_BADGE', + REQUEST_LOAD_BADGES: 'REQUEST_LOAD_BADGES', + REQUEST_NEW_BADGE: 'REQUEST_NEW_BADGE', + REQUEST_RENDERED_BADGE: 'REQUEST_RENDERED_BADGE', + REQUEST_UPDATED_BADGE: 'REQUEST_UPDATED_BADGE', + START_EDITING: 'START_EDITING', + STOP_EDITING: 'STOP_EDITING', + UPDATE_BADGE_IN_FORM: 'UPDATE_BADGE_IN_FORM', + UPDATE_BADGE_IN_MODAL: 'UPDATE_BADGE_IN_MODAL', +}; diff --git a/app/assets/javascripts/badges/store/mutations.js b/app/assets/javascripts/badges/store/mutations.js new file mode 100644 index 00000000000..bd84e68c00f --- /dev/null +++ b/app/assets/javascripts/badges/store/mutations.js @@ -0,0 +1,158 @@ +import types from './mutation_types'; +import { PROJECT_BADGE } from '../constants'; + +const reorderBadges = badges => + badges.sort((a, b) => { + if (a.kind !== b.kind) { + return a.kind === PROJECT_BADGE ? 1 : -1; + } + + return a.id - b.id; + }); + +export default { + [types.RECEIVE_NEW_BADGE](state, newBadge) { + Object.assign(state, { + badgeInAddForm: null, + badges: reorderBadges(state.badges.concat(newBadge)), + isSaving: false, + renderedBadge: null, + }); + }, + [types.RECEIVE_NEW_BADGE_ERROR](state) { + Object.assign(state, { + isSaving: false, + }); + }, + [types.REQUEST_NEW_BADGE](state) { + Object.assign(state, { + isSaving: true, + }); + }, + + [types.RECEIVE_UPDATED_BADGE](state, updatedBadge) { + const badges = state.badges.map(badge => { + if (badge.id === updatedBadge.id) { + return updatedBadge; + } + return badge; + }); + Object.assign(state, { + badgeInEditForm: null, + badges, + isEditing: false, + isSaving: false, + renderedBadge: null, + }); + }, + [types.RECEIVE_UPDATED_BADGE_ERROR](state) { + Object.assign(state, { + isSaving: false, + }); + }, + [types.REQUEST_UPDATED_BADGE](state) { + Object.assign(state, { + isSaving: true, + }); + }, + + [types.RECEIVE_LOAD_BADGES](state, badges) { + Object.assign(state, { + badges: reorderBadges(badges), + isLoading: false, + }); + }, + [types.RECEIVE_LOAD_BADGES_ERROR](state) { + Object.assign(state, { + isLoading: false, + }); + }, + [types.REQUEST_LOAD_BADGES](state, data) { + Object.assign(state, { + kind: data.kind, // project or group + apiEndpointUrl: data.apiEndpointUrl, + docsUrl: data.docsUrl, + isLoading: true, + }); + }, + + [types.RECEIVE_DELETE_BADGE](state, badgeId) { + const badges = state.badges.filter(badge => badge.id !== badgeId); + Object.assign(state, { + badges, + }); + }, + [types.RECEIVE_DELETE_BADGE_ERROR](state, badgeId) { + const badges = state.badges.map(badge => { + if (badge.id === badgeId) { + return { + ...badge, + isDeleting: false, + }; + } + + return badge; + }); + Object.assign(state, { + badges, + }); + }, + [types.REQUEST_DELETE_BADGE](state, badgeId) { + const badges = state.badges.map(badge => { + if (badge.id === badgeId) { + return { + ...badge, + isDeleting: true, + }; + } + + return badge; + }); + Object.assign(state, { + badges, + }); + }, + + [types.RECEIVE_RENDERED_BADGE](state, renderedBadge) { + Object.assign(state, { isRendering: false, renderedBadge }); + }, + [types.RECEIVE_RENDERED_BADGE_ERROR](state) { + Object.assign(state, { isRendering: false }); + }, + [types.REQUEST_RENDERED_BADGE](state) { + Object.assign(state, { isRendering: true }); + }, + + [types.START_EDITING](state, badge) { + Object.assign(state, { + badgeInEditForm: { ...badge }, + isEditing: true, + renderedBadge: { ...badge }, + }); + }, + [types.STOP_EDITING](state) { + Object.assign(state, { + badgeInEditForm: null, + isEditing: false, + renderedBadge: null, + }); + }, + + [types.UPDATE_BADGE_IN_FORM](state, badge) { + if (state.isEditing) { + Object.assign(state, { + badgeInEditForm: badge, + }); + } else { + Object.assign(state, { + badgeInAddForm: badge, + }); + } + }, + + [types.UPDATE_BADGE_IN_MODAL](state, badge) { + Object.assign(state, { + badgeInModal: badge, + }); + }, +}; diff --git a/app/assets/javascripts/badges/store/state.js b/app/assets/javascripts/badges/store/state.js new file mode 100644 index 00000000000..43413aeb5bb --- /dev/null +++ b/app/assets/javascripts/badges/store/state.js @@ -0,0 +1,13 @@ +export default () => ({ + apiEndpointUrl: null, + badgeInAddForm: null, + badgeInEditForm: null, + badgeInModal: null, + badges: [], + docsUrl: null, + renderedBadge: null, + isEditing: false, + isLoading: false, + isRendering: false, + isSaving: false, +}); diff --git a/app/assets/javascripts/blob/file_template_mediator.js b/app/assets/javascripts/blob/file_template_mediator.js index 030ca1907e5..ff1cbcad145 100644 --- a/app/assets/javascripts/blob/file_template_mediator.js +++ b/app/assets/javascripts/blob/file_template_mediator.js @@ -94,7 +94,7 @@ export default class FileTemplateMediator { const hash = urlPieces[1]; if (hash === 'preview') { this.hideTemplateSelectorMenu(); - } else if (hash === 'editor') { + } else if (hash === 'editor' && !this.typeSelector.isHidden()) { this.showTemplateSelectorMenu(); } }); diff --git a/app/assets/javascripts/blob/file_template_selector.js b/app/assets/javascripts/blob/file_template_selector.js index e52cf249f3a..02228434a29 100644 --- a/app/assets/javascripts/blob/file_template_selector.js +++ b/app/assets/javascripts/blob/file_template_selector.js @@ -32,6 +32,10 @@ export default class FileTemplateSelector { } } + isHidden() { + return this.$wrapper.hasClass('hidden'); + } + getToggleText() { return this.$dropdownToggleText.text(); } diff --git a/app/assets/javascripts/boards/components/board.js b/app/assets/javascripts/boards/components/board.js index 3cffd91716a..bea818010a4 100644 --- a/app/assets/javascripts/boards/components/board.js +++ b/app/assets/javascripts/boards/components/board.js @@ -5,7 +5,7 @@ import Sortable from 'vendor/Sortable'; import Vue from 'vue'; import AccessorUtilities from '../../lib/utils/accessor'; import boardList from './board_list.vue'; -import boardBlankState from './board_blank_state'; +import BoardBlankState from './board_blank_state.vue'; import './board_delete'; const Store = gl.issueBoards.BoardsStore; @@ -18,7 +18,7 @@ gl.issueBoards.Board = Vue.extend({ components: { boardList, 'board-delete': gl.issueBoards.BoardDelete, - boardBlankState, + BoardBlankState, }, props: { list: Object, diff --git a/app/assets/javascripts/boards/components/board_blank_state.js b/app/assets/javascripts/boards/components/board_blank_state.vue similarity index 61% rename from app/assets/javascripts/boards/components/board_blank_state.js rename to app/assets/javascripts/boards/components/board_blank_state.vue index 72db626d3c7..2049eeb9c30 100644 --- a/app/assets/javascripts/boards/components/board_blank_state.js +++ b/app/assets/javascripts/boards/components/board_blank_state.vue @@ -1,42 +1,11 @@ + + + diff --git a/app/assets/javascripts/boards/components/board_sidebar.js b/app/assets/javascripts/boards/components/board_sidebar.js index a44969272a1..c4ee4f6c855 100644 --- a/app/assets/javascripts/boards/components/board_sidebar.js +++ b/app/assets/javascripts/boards/components/board_sidebar.js @@ -60,10 +60,6 @@ gl.issueBoards.BoardSidebar = Vue.extend({ this.issue = this.detail.issue; this.list = this.detail.list; - - this.$nextTick(() => { - this.endpoint = this.$refs.assigneeDropdown.dataset.issueUpdate; - }); }, deep: true }, @@ -91,7 +87,7 @@ gl.issueBoards.BoardSidebar = Vue.extend({ saveAssignees () { this.loadingAssignees = true; - gl.issueBoards.BoardsStore.detail.issue.update(this.endpoint) + gl.issueBoards.BoardsStore.detail.issue.update() .then(() => { this.loadingAssignees = false; }) diff --git a/app/assets/javascripts/boards/components/issue_card_inner.js b/app/assets/javascripts/boards/components/issue_card_inner.js index 8aee5b23c76..84fe9b1288a 100644 --- a/app/assets/javascripts/boards/components/issue_card_inner.js +++ b/app/assets/javascripts/boards/components/issue_card_inner.js @@ -68,15 +68,6 @@ gl.issueBoards.IssueCardInner = Vue.extend({ return this.issue.assignees.length > this.numberOverLimit; }, - cardUrl() { - let baseUrl = this.issueLinkBase; - - if (this.groupId && this.issue.project) { - baseUrl = this.issueLinkBase.replace(':project_path', this.issue.project.path); - } - - return `${baseUrl}/${this.issue.iid}`; - }, issueId() { if (this.issue.iid) { return `#${this.issue.iid}`; @@ -153,13 +144,13 @@ gl.issueBoards.IssueCardInner = Vue.extend({ /> {{ issue.title }} - {{ issueId }} + {{ issue.referencePath }}
diff --git a/app/assets/javascripts/boards/components/modal/empty_state.js b/app/assets/javascripts/boards/components/modal/empty_state.js index e571b11a83d..9e37f95cdd6 100644 --- a/app/assets/javascripts/boards/components/modal/empty_state.js +++ b/app/assets/javascripts/boards/components/modal/empty_state.js @@ -1,9 +1,9 @@ import Vue from 'vue'; - -const ModalStore = gl.issueBoards.ModalStore; +import ModalStore from '../../stores/modal_store'; +import modalMixin from '../../mixins/modal_mixins'; gl.issueBoards.ModalEmptyState = Vue.extend({ - mixins: [gl.issueBoards.ModalMixins], + mixins: [modalMixin], data() { return ModalStore.store; }, diff --git a/app/assets/javascripts/boards/components/modal/footer.js b/app/assets/javascripts/boards/components/modal/footer.js index 03cd7ef65cb..9735e0ddacc 100644 --- a/app/assets/javascripts/boards/components/modal/footer.js +++ b/app/assets/javascripts/boards/components/modal/footer.js @@ -3,11 +3,11 @@ import Flash from '../../../flash'; import { __ } from '../../../locale'; import './lists_dropdown'; import { pluralize } from '../../../lib/utils/text_utility'; - -const ModalStore = gl.issueBoards.ModalStore; +import ModalStore from '../../stores/modal_store'; +import modalMixin from '../../mixins/modal_mixins'; gl.issueBoards.ModalFooter = Vue.extend({ - mixins: [gl.issueBoards.ModalMixins], + mixins: [modalMixin], data() { return { modal: ModalStore.store, diff --git a/app/assets/javascripts/boards/components/modal/header.js b/app/assets/javascripts/boards/components/modal/header.js index 31f59d295bf..67c29ebca72 100644 --- a/app/assets/javascripts/boards/components/modal/header.js +++ b/app/assets/javascripts/boards/components/modal/header.js @@ -1,11 +1,11 @@ import Vue from 'vue'; import modalFilters from './filters'; import './tabs'; - -const ModalStore = gl.issueBoards.ModalStore; +import ModalStore from '../../stores/modal_store'; +import modalMixin from '../../mixins/modal_mixins'; gl.issueBoards.ModalHeader = Vue.extend({ - mixins: [gl.issueBoards.ModalMixins], + mixins: [modalMixin], props: { projectId: { type: Number, diff --git a/app/assets/javascripts/boards/components/modal/index.js b/app/assets/javascripts/boards/components/modal/index.js index d825ff38587..3083b3e4405 100644 --- a/app/assets/javascripts/boards/components/modal/index.js +++ b/app/assets/javascripts/boards/components/modal/index.js @@ -7,8 +7,7 @@ import './header'; import './list'; import './footer'; import './empty_state'; - -const ModalStore = gl.issueBoards.ModalStore; +import ModalStore from '../../stores/modal_store'; gl.issueBoards.IssuesModal = Vue.extend({ props: { diff --git a/app/assets/javascripts/boards/components/modal/list.js b/app/assets/javascripts/boards/components/modal/list.js index 7c62134b3a3..6b04a6c7a6c 100644 --- a/app/assets/javascripts/boards/components/modal/list.js +++ b/app/assets/javascripts/boards/components/modal/list.js @@ -2,8 +2,7 @@ import Vue from 'vue'; import bp from '../../../breakpoints'; - -const ModalStore = gl.issueBoards.ModalStore; +import ModalStore from '../../stores/modal_store'; gl.issueBoards.ModalList = Vue.extend({ props: { diff --git a/app/assets/javascripts/boards/components/modal/lists_dropdown.js b/app/assets/javascripts/boards/components/modal/lists_dropdown.js index 4684ea76647..e644de2d4fc 100644 --- a/app/assets/javascripts/boards/components/modal/lists_dropdown.js +++ b/app/assets/javascripts/boards/components/modal/lists_dropdown.js @@ -1,6 +1,5 @@ import Vue from 'vue'; - -const ModalStore = gl.issueBoards.ModalStore; +import ModalStore from '../../stores/modal_store'; gl.issueBoards.ModalFooterListsDropdown = Vue.extend({ data() { diff --git a/app/assets/javascripts/boards/components/modal/tabs.js b/app/assets/javascripts/boards/components/modal/tabs.js index 3e5d08e3d75..b6465a88e5e 100644 --- a/app/assets/javascripts/boards/components/modal/tabs.js +++ b/app/assets/javascripts/boards/components/modal/tabs.js @@ -1,9 +1,9 @@ import Vue from 'vue'; - -const ModalStore = gl.issueBoards.ModalStore; +import ModalStore from '../../stores/modal_store'; +import modalMixin from '../../mixins/modal_mixins'; gl.issueBoards.ModalTabs = Vue.extend({ - mixins: [gl.issueBoards.ModalMixins], + mixins: [modalMixin], data() { return ModalStore.store; }, diff --git a/app/assets/javascripts/boards/components/sidebar/remove_issue.js b/app/assets/javascripts/boards/components/sidebar/remove_issue.js index 09c683ff621..0a0820ec5fd 100644 --- a/app/assets/javascripts/boards/components/sidebar/remove_issue.js +++ b/app/assets/javascripts/boards/components/sidebar/remove_issue.js @@ -17,14 +17,10 @@ gl.issueBoards.RemoveIssueBtn = Vue.extend({ type: Object, required: true, }, - issueUpdate: { - type: String, - required: true, - }, }, computed: { updateUrl() { - return this.issueUpdate.replace(':project_path', this.issue.project.path); + return this.issue.path; }, }, methods: { diff --git a/app/assets/javascripts/boards/filtered_search_boards.js b/app/assets/javascripts/boards/filtered_search_boards.js index fb40b9f5565..70367c4f711 100644 --- a/app/assets/javascripts/boards/filtered_search_boards.js +++ b/app/assets/javascripts/boards/filtered_search_boards.js @@ -6,6 +6,7 @@ export default class FilteredSearchBoards extends FilteredSearchManager { constructor(store, updateUrl = false, cantEdit = []) { super({ page: 'boards', + isGroupDecendent: true, stateFiltersSelector: '.issues-state-filters', }); diff --git a/app/assets/javascripts/boards/index.js b/app/assets/javascripts/boards/index.js index 8b1c14c04ff..a6f8681cfac 100644 --- a/app/assets/javascripts/boards/index.js +++ b/app/assets/javascripts/boards/index.js @@ -17,9 +17,9 @@ import './models/milestone'; import './models/project'; import './models/assignee'; import './stores/boards_store'; -import './stores/modal_store'; +import ModalStore from './stores/modal_store'; import BoardService from './services/board_service'; -import './mixins/modal_mixins'; +import modalMixin from './mixins/modal_mixins'; import './mixins/sortable_default_options'; import './filters/due_date_filters'; import './components/board'; @@ -31,7 +31,6 @@ import '~/vue_shared/vue_resource_interceptor'; // eslint-disable-line import/fi export default () => { const $boardApp = document.getElementById('board-app'); const Store = gl.issueBoards.BoardsStore; - const ModalStore = gl.issueBoards.ModalStore; window.gl = window.gl || {}; @@ -176,7 +175,7 @@ export default () => { gl.IssueBoardsModalAddBtn = new Vue({ el: document.getElementById('js-add-issues-btn'), - mixins: [gl.issueBoards.ModalMixins], + mixins: [modalMixin], data() { return { modal: ModalStore.store, diff --git a/app/assets/javascripts/boards/mixins/modal_mixins.js b/app/assets/javascripts/boards/mixins/modal_mixins.js index 2b0a1aaa89f..6c97e1629bf 100644 --- a/app/assets/javascripts/boards/mixins/modal_mixins.js +++ b/app/assets/javascripts/boards/mixins/modal_mixins.js @@ -1,6 +1,6 @@ -const ModalStore = gl.issueBoards.ModalStore; +import ModalStore from '../stores/modal_store'; -gl.issueBoards.ModalMixins = { +export default { methods: { toggleModal(toggle) { ModalStore.store.showAddIssuesModal = toggle; diff --git a/app/assets/javascripts/boards/models/issue.js b/app/assets/javascripts/boards/models/issue.js index 4c5079efc8b..b381d48d625 100644 --- a/app/assets/javascripts/boards/models/issue.js +++ b/app/assets/javascripts/boards/models/issue.js @@ -23,6 +23,8 @@ class ListIssue { }; this.isLoading = {}; this.sidebarInfoEndpoint = obj.issue_sidebar_endpoint; + this.referencePath = obj.reference_path; + this.path = obj.real_path; this.toggleSubscriptionEndpoint = obj.toggle_subscription_endpoint; this.milestone_id = obj.milestone_id; this.project_id = obj.project_id; @@ -98,7 +100,7 @@ class ListIssue { this.isLoading[key] = value; } - update (url) { + update () { const data = { issue: { milestone_id: this.milestone ? this.milestone.id : null, @@ -113,7 +115,7 @@ class ListIssue { } const projectPath = this.project ? this.project.path : ''; - return Vue.http.patch(url.replace(':project_path', projectPath), data); + return Vue.http.patch(`${this.path}.json`, data); } } diff --git a/app/assets/javascripts/boards/services/board_service.js b/app/assets/javascripts/boards/services/board_service.js index d78d4701974..7c90597f77c 100644 --- a/app/assets/javascripts/boards/services/board_service.js +++ b/app/assets/javascripts/boards/services/board_service.js @@ -19,7 +19,7 @@ export default class BoardService { } static generateIssuePath(boardId, id) { - return `${gon.relative_url_root}/-/boards/${boardId ? `/${boardId}` : ''}/issues${id ? `/${id}` : ''}`; + return `${gon.relative_url_root}/-/boards/${boardId ? `${boardId}` : ''}/issues${id ? `/${id}` : ''}`; } all() { diff --git a/app/assets/javascripts/boards/stores/modal_store.js b/app/assets/javascripts/boards/stores/modal_store.js index 4fdc925c825..a4220cd840d 100644 --- a/app/assets/javascripts/boards/stores/modal_store.js +++ b/app/assets/javascripts/boards/stores/modal_store.js @@ -1,6 +1,3 @@ -window.gl = window.gl || {}; -window.gl.issueBoards = window.gl.issueBoards || {}; - class ModalStore { constructor() { this.store = { @@ -95,4 +92,4 @@ class ModalStore { } } -gl.issueBoards.ModalStore = new ModalStore(); +export default new ModalStore(); diff --git a/app/assets/javascripts/commit/pipelines/pipelines_table.vue b/app/assets/javascripts/commit/pipelines/pipelines_table.vue index 466a5b5d635..24d63b99a29 100644 --- a/app/assets/javascripts/commit/pipelines/pipelines_table.vue +++ b/app/assets/javascripts/commit/pipelines/pipelines_table.vue @@ -55,22 +55,20 @@ }, methods: { successCallback(resp) { - return resp.json().then((response) => { - // depending of the endpoint the response can either bring a `pipelines` key or not. - const pipelines = response.pipelines || response; - this.setCommonData(pipelines); + // depending of the endpoint the response can either bring a `pipelines` key or not. + const pipelines = resp.data.pipelines || resp.data; + this.setCommonData(pipelines); - const updatePipelinesEvent = new CustomEvent('update-pipelines-count', { - detail: { - pipelines: response, - }, - }); - - // notifiy to update the count in tabs - if (this.$el.parentElement) { - this.$el.parentElement.dispatchEvent(updatePipelinesEvent); - } + const updatePipelinesEvent = new CustomEvent('update-pipelines-count', { + detail: { + pipelines: resp.data, + }, }); + + // notifiy to update the count in tabs + if (this.$el.parentElement) { + this.$el.parentElement.dispatchEvent(updatePipelinesEvent); + } }, }, }; diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js index e6390f0855b..d7e1de18d09 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js @@ -26,8 +26,8 @@ export default class FilteredSearchDropdownManager { this.filteredSearchInput = this.container.querySelector('.filtered-search'); this.page = page; this.groupsOnly = isGroup; - this.groupAncestor = isGroupAncestor; - this.isGroupDecendent = isGroupDecendent; + this.includeAncestorGroups = isGroupAncestor; + this.includeDescendantGroups = isGroupDecendent; this.setupMapping(); @@ -108,7 +108,19 @@ export default class FilteredSearchDropdownManager { } getLabelsEndpoint() { - const endpoint = `${this.baseEndpoint}/labels.json`; + let endpoint = `${this.baseEndpoint}/labels.json?`; + + if (this.groupsOnly) { + endpoint = `${endpoint}only_group_labels=true&`; + } + + if (this.includeAncestorGroups) { + endpoint = `${endpoint}include_ancestor_groups=true&`; + } + + if (this.includeDescendantGroups) { + endpoint = `${endpoint}include_descendant_groups=true`; + } return endpoint; } diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js index 71b7e80335b..cf5ba1e1771 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js @@ -21,7 +21,7 @@ export default class FilteredSearchManager { constructor({ page, isGroup = false, - isGroupAncestor = false, + isGroupAncestor = true, isGroupDecendent = false, filteredSearchTokenKeys = FilteredSearchTokenKeys, stateFiltersSelector = '.issues-state-filters', @@ -86,6 +86,7 @@ export default class FilteredSearchManager { page: this.page, isGroup: this.isGroup, isGroupAncestor: this.isGroupAncestor, + isGroupDecendent: this.isGroupDecendent, filteredSearchTokenKeys: this.filteredSearchTokenKeys, }); diff --git a/app/assets/javascripts/ide/components/changed_file_icon.vue b/app/assets/javascripts/ide/components/changed_file_icon.vue index 0c54c992e51..037e3efb4ce 100644 --- a/app/assets/javascripts/ide/components/changed_file_icon.vue +++ b/app/assets/javascripts/ide/components/changed_file_icon.vue @@ -1,25 +1,25 @@