diff options
1027 files changed, 10392 insertions, 5736 deletions
diff --git a/.eslintrc b/.eslintrc index aba8112c5a9..73cd7ecf66d 100644 --- a/.eslintrc +++ b/.eslintrc @@ -27,6 +27,7 @@ }, "rules": { "filenames/match-regex": [2, "^[a-z0-9_]+$"], + "import/no-commonjs": "error", "no-multiple-empty-lines": ["error", { "max": 1 }], "promise/catch-or-return": "error" } diff --git a/.gitignore b/.gitignore index f3decfd7dfe..89da29fd790 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,7 @@ eslint-report.html .sass-cache/ /.secret /.vagrant +/.yarn-cache /.byebug_history /Vagrantfile /backups/* diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 7fbfda4a5a8..638553d7bf7 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,9 +1,10 @@ image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.3-golang-1.8-git-2.7-phantomjs-2.1-node-7.1-postgresql-9.6" cache: - key: "ruby-233" + key: "ruby-233-with-yarn" paths: - vendor/ruby + - .yarn-cache/ variables: MYSQL_ALLOW_EMPTY_PASSWORD: "1" @@ -51,7 +52,7 @@ stages: .use-pg: &use-pg services: - - postgres:latest + - postgres:9.2 - redis:alpine .use-mysql: &use-mysql @@ -62,6 +63,7 @@ stages: .only-master-and-ee-or-mysql: &only-master-and-ee-or-mysql only: - /mysql/ + - /-stable$/ - master@gitlab-org/gitlab-ce - master@gitlab/gitlabhq - tags@gitlab-org/gitlab-ce @@ -74,7 +76,7 @@ stages: # https://docs.gitlab.com/ce/development/writing_documentation.html#testing .except-docs: &except-docs except: - - /^docs\/.*/ + - /(^docs[\/-].*|.*-docs$)/ .rspec-knapsack: &rspec-knapsack stage: test @@ -83,7 +85,7 @@ stages: - JOB_NAME=( $CI_JOB_NAME ) - export CI_NODE_INDEX=${JOB_NAME[-2]} - export CI_NODE_TOTAL=${JOB_NAME[-1]} - - export KNAPSACK_REPORT_PATH=knapsack/${CI_PROJECT_NAME}/${JOB_NAME[0]}_${JOB_NAME[1]}_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json + - export KNAPSACK_REPORT_PATH=knapsack/${CI_PROJECT_NAME}/${JOB_NAME[0]}_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json - export KNAPSACK_GENERATE_REPORT=true - export CACHE_CLASSES=true - cp ${KNAPSACK_RSPEC_SUITE_REPORT_PATH} ${KNAPSACK_REPORT_PATH} @@ -114,7 +116,7 @@ stages: - JOB_NAME=( $CI_JOB_NAME ) - export CI_NODE_INDEX=${JOB_NAME[-2]} - export CI_NODE_TOTAL=${JOB_NAME[-1]} - - export KNAPSACK_REPORT_PATH=knapsack/${CI_PROJECT_NAME}/${JOB_NAME[0]}_${JOB_NAME[1]}_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json + - export KNAPSACK_REPORT_PATH=knapsack/${CI_PROJECT_NAME}/${JOB_NAME[0]}_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json - export KNAPSACK_GENERATE_REPORT=true - export CACHE_CLASSES=true - cp ${KNAPSACK_SPINACH_SUITE_REPORT_PATH} ${KNAPSACK_REPORT_PATH} @@ -138,6 +140,13 @@ stages: <<: *only-master-and-ee-or-mysql <<: *except-docs +.only-canonical-masters: &only-canonical-masters + only: + - master@gitlab-org/gitlab-ce + - master@gitlab-org/gitlab-ee + - master@gitlab/gitlabhq + - master@gitlab/gitlab-ee + # Trigger a package build on omnibus-gitlab repository build-package: @@ -167,17 +176,13 @@ knapsack: update-knapsack: <<: *knapsack-state <<: *dedicated-runner + <<: *only-canonical-masters stage: post-test script: - - scripts/merge-reports ${KNAPSACK_RSPEC_SUITE_REPORT_PATH} knapsack/${CI_PROJECT_NAME}/rspec_pg_node_*.json - - scripts/merge-reports ${KNAPSACK_SPINACH_SUITE_REPORT_PATH} knapsack/${CI_PROJECT_NAME}/spinach_pg_node_*.json + - scripts/merge-reports ${KNAPSACK_RSPEC_SUITE_REPORT_PATH} knapsack/${CI_PROJECT_NAME}/rspec-pg_node_*.json + - scripts/merge-reports ${KNAPSACK_SPINACH_SUITE_REPORT_PATH} knapsack/${CI_PROJECT_NAME}/spinach-pg_node_*.json - '[[ -z ${KNAPSACK_S3_BUCKET} ]] || scripts/sync-reports put $KNAPSACK_S3_BUCKET $KNAPSACK_RSPEC_SUITE_REPORT_PATH $KNAPSACK_SPINACH_SUITE_REPORT_PATH' - rm -f knapsack/${CI_PROJECT_NAME}/*_node_*.json - only: - - master@gitlab-org/gitlab-ce - - master@gitlab-org/gitlab-ee - - master@gitlab/gitlabhq - - master@gitlab/gitlab-ee setup-test-env: <<: *use-pg @@ -186,7 +191,7 @@ setup-test-env: stage: prepare script: - node --version - - yarn install --pure-lockfile + - yarn install --pure-lockfile --cache-folder .yarn-cache - bundle exec rake gitlab:assets:compile - bundle exec ruby -Ispec -e 'require "spec_helper" ; TestEnv.init' artifacts: @@ -196,76 +201,75 @@ setup-test-env: - public/assets - tmp/tests -rspec pg 0 20: *rspec-knapsack-pg -rspec pg 1 20: *rspec-knapsack-pg -rspec pg 2 20: *rspec-knapsack-pg -rspec pg 3 20: *rspec-knapsack-pg -rspec pg 4 20: *rspec-knapsack-pg -rspec pg 5 20: *rspec-knapsack-pg -rspec pg 6 20: *rspec-knapsack-pg -rspec pg 7 20: *rspec-knapsack-pg -rspec pg 8 20: *rspec-knapsack-pg -rspec pg 9 20: *rspec-knapsack-pg -rspec pg 10 20: *rspec-knapsack-pg -rspec pg 11 20: *rspec-knapsack-pg -rspec pg 12 20: *rspec-knapsack-pg -rspec pg 13 20: *rspec-knapsack-pg -rspec pg 14 20: *rspec-knapsack-pg -rspec pg 15 20: *rspec-knapsack-pg -rspec pg 16 20: *rspec-knapsack-pg -rspec pg 17 20: *rspec-knapsack-pg -rspec pg 18 20: *rspec-knapsack-pg -rspec pg 19 20: *rspec-knapsack-pg - -rspec mysql 0 20: *rspec-knapsack-mysql -rspec mysql 1 20: *rspec-knapsack-mysql -rspec mysql 2 20: *rspec-knapsack-mysql -rspec mysql 3 20: *rspec-knapsack-mysql -rspec mysql 4 20: *rspec-knapsack-mysql -rspec mysql 5 20: *rspec-knapsack-mysql -rspec mysql 6 20: *rspec-knapsack-mysql -rspec mysql 7 20: *rspec-knapsack-mysql -rspec mysql 8 20: *rspec-knapsack-mysql -rspec mysql 9 20: *rspec-knapsack-mysql -rspec mysql 10 20: *rspec-knapsack-mysql -rspec mysql 11 20: *rspec-knapsack-mysql -rspec mysql 12 20: *rspec-knapsack-mysql -rspec mysql 13 20: *rspec-knapsack-mysql -rspec mysql 14 20: *rspec-knapsack-mysql -rspec mysql 15 20: *rspec-knapsack-mysql -rspec mysql 16 20: *rspec-knapsack-mysql -rspec mysql 17 20: *rspec-knapsack-mysql -rspec mysql 18 20: *rspec-knapsack-mysql -rspec mysql 19 20: *rspec-knapsack-mysql - -spinach pg 0 10: *spinach-knapsack-pg -spinach pg 1 10: *spinach-knapsack-pg -spinach pg 2 10: *spinach-knapsack-pg -spinach pg 3 10: *spinach-knapsack-pg -spinach pg 4 10: *spinach-knapsack-pg -spinach pg 5 10: *spinach-knapsack-pg -spinach pg 6 10: *spinach-knapsack-pg -spinach pg 7 10: *spinach-knapsack-pg -spinach pg 8 10: *spinach-knapsack-pg -spinach pg 9 10: *spinach-knapsack-pg - -spinach mysql 0 10: *spinach-knapsack-mysql -spinach mysql 1 10: *spinach-knapsack-mysql -spinach mysql 2 10: *spinach-knapsack-mysql -spinach mysql 3 10: *spinach-knapsack-mysql -spinach mysql 4 10: *spinach-knapsack-mysql -spinach mysql 5 10: *spinach-knapsack-mysql -spinach mysql 6 10: *spinach-knapsack-mysql -spinach mysql 7 10: *spinach-knapsack-mysql -spinach mysql 8 10: *spinach-knapsack-mysql -spinach mysql 9 10: *spinach-knapsack-mysql - -# Other generic tests +rspec-pg 0 20: *rspec-knapsack-pg +rspec-pg 1 20: *rspec-knapsack-pg +rspec-pg 2 20: *rspec-knapsack-pg +rspec-pg 3 20: *rspec-knapsack-pg +rspec-pg 4 20: *rspec-knapsack-pg +rspec-pg 5 20: *rspec-knapsack-pg +rspec-pg 6 20: *rspec-knapsack-pg +rspec-pg 7 20: *rspec-knapsack-pg +rspec-pg 8 20: *rspec-knapsack-pg +rspec-pg 9 20: *rspec-knapsack-pg +rspec-pg 10 20: *rspec-knapsack-pg +rspec-pg 11 20: *rspec-knapsack-pg +rspec-pg 12 20: *rspec-knapsack-pg +rspec-pg 13 20: *rspec-knapsack-pg +rspec-pg 14 20: *rspec-knapsack-pg +rspec-pg 15 20: *rspec-knapsack-pg +rspec-pg 16 20: *rspec-knapsack-pg +rspec-pg 17 20: *rspec-knapsack-pg +rspec-pg 18 20: *rspec-knapsack-pg +rspec-pg 19 20: *rspec-knapsack-pg + +rspec-mysql 0 20: *rspec-knapsack-mysql +rspec-mysql 1 20: *rspec-knapsack-mysql +rspec-mysql 2 20: *rspec-knapsack-mysql +rspec-mysql 3 20: *rspec-knapsack-mysql +rspec-mysql 4 20: *rspec-knapsack-mysql +rspec-mysql 5 20: *rspec-knapsack-mysql +rspec-mysql 6 20: *rspec-knapsack-mysql +rspec-mysql 7 20: *rspec-knapsack-mysql +rspec-mysql 8 20: *rspec-knapsack-mysql +rspec-mysql 9 20: *rspec-knapsack-mysql +rspec-mysql 10 20: *rspec-knapsack-mysql +rspec-mysql 11 20: *rspec-knapsack-mysql +rspec-mysql 12 20: *rspec-knapsack-mysql +rspec-mysql 13 20: *rspec-knapsack-mysql +rspec-mysql 14 20: *rspec-knapsack-mysql +rspec-mysql 15 20: *rspec-knapsack-mysql +rspec-mysql 16 20: *rspec-knapsack-mysql +rspec-mysql 17 20: *rspec-knapsack-mysql +rspec-mysql 18 20: *rspec-knapsack-mysql +rspec-mysql 19 20: *rspec-knapsack-mysql + +spinach-pg 0 10: *spinach-knapsack-pg +spinach-pg 1 10: *spinach-knapsack-pg +spinach-pg 2 10: *spinach-knapsack-pg +spinach-pg 3 10: *spinach-knapsack-pg +spinach-pg 4 10: *spinach-knapsack-pg +spinach-pg 5 10: *spinach-knapsack-pg +spinach-pg 6 10: *spinach-knapsack-pg +spinach-pg 7 10: *spinach-knapsack-pg +spinach-pg 8 10: *spinach-knapsack-pg +spinach-pg 9 10: *spinach-knapsack-pg + +spinach-mysql 0 10: *spinach-knapsack-mysql +spinach-mysql 1 10: *spinach-knapsack-mysql +spinach-mysql 2 10: *spinach-knapsack-mysql +spinach-mysql 3 10: *spinach-knapsack-mysql +spinach-mysql 4 10: *spinach-knapsack-mysql +spinach-mysql 5 10: *spinach-knapsack-mysql +spinach-mysql 6 10: *spinach-knapsack-mysql +spinach-mysql 7 10: *spinach-knapsack-mysql +spinach-mysql 8 10: *spinach-knapsack-mysql +spinach-mysql 9 10: *spinach-knapsack-mysql + +# Static analysis jobs .ruby-static-analysis: &ruby-static-analysis variables: SIMPLECOV: "false" SETUP_DB: "false" - USE_BUNDLE_INSTALL: "true" .rake-exec: &rake-exec <<: *ruby-static-analysis @@ -308,7 +312,7 @@ downtime_check: - master - tags - /^[\d-]+-stable(-ee)?$/ - - /^docs\/*/ + - /(^docs[\/-].*|.*-docs$)/ ee_compat_check: <<: *rake-exec @@ -330,6 +334,7 @@ ee_compat_check: paths: - ee_compat_check/patches/*.patch +# DB migration, rollback, and seed jobs .db-migrate-reset: &db-migrate-reset stage: test <<: *dedicated-runner @@ -337,14 +342,38 @@ ee_compat_check: script: - bundle exec rake db:migrate:reset -rake pg db:migrate:reset: +db:migrate:reset-pg: <<: *db-migrate-reset <<: *use-pg -rake mysql db:migrate:reset: +db:migrate:reset-mysql: <<: *db-migrate-reset <<: *use-mysql +.migration-paths: &migration-paths + stage: test + <<: *dedicated-runner + variables: + SETUP_DB: "false" + <<: *only-canonical-masters + script: + - git fetch origin v8.14.10 + - git checkout -f FETCH_HEAD + - bundle install $BUNDLE_INSTALL_FLAGS + - bundle exec rake db:drop db:create db:schema:load db:seed_fu + - git checkout $CI_COMMIT_SHA + - bundle install $BUNDLE_INSTALL_FLAGS + - . scripts/prepare_build.sh + - bundle exec rake db:migrate + +migration:path-pg: + <<: *migration-paths + <<: *use-pg + +migration:path-mysql: + <<: *migration-paths + <<: *use-mysql + .db-rollback: &db-rollback stage: test <<: *dedicated-runner @@ -353,11 +382,11 @@ rake mysql db:migrate:reset: - bundle exec rake db:rollback STEP=120 - bundle exec rake db:migrate -rake pg db:rollback: +db:rollback-pg: <<: *db-rollback <<: *use-pg -rake mysql db:rollback: +db:rollback-mysql: <<: *db-rollback <<: *use-mysql @@ -379,15 +408,16 @@ rake mysql db:rollback: paths: - log/development.log -rake pg db:seed_fu: +db:seed_fu-pg: <<: *db-seed_fu <<: *use-pg -rake mysql db:seed_fu: +db:seed_fu-mysql: <<: *db-seed_fu <<: *use-mysql -rake gitlab:assets:compile: +# Frontend-related jobs +gitlab:assets:compile: stage: test <<: *dedicated-runner <<: *except-docs @@ -400,14 +430,15 @@ rake gitlab:assets:compile: SKIP_STORAGE_VALIDATION: "true" WEBPACK_REPORT: "true" script: - - bundle exec rake yarn:install gitlab:assets:compile + - yarn install --pure-lockfile --production --cache-folder .yarn-cache + - bundle exec rake gitlab:assets:compile artifacts: name: webpack-report expire_in: 31d paths: - webpack-report/ -rake karma: +karma: stage: test <<: *use-pg <<: *dedicated-runner @@ -423,34 +454,6 @@ rake karma: paths: - coverage-javascript/ -.migration-paths: &migration-paths - stage: test - <<: *dedicated-runner - variables: - SETUP_DB: "false" - only: - - master@gitlab-org/gitlab-ce - - master@gitlab-org/gitlab-ee - - master@gitlab/gitlabhq - - master@gitlab/gitlab-ee - script: - - git fetch origin v8.14.10 - - git checkout -f FETCH_HEAD - - bundle install $BUNDLE_INSTALL_FLAGS - - bundle exec rake db:drop db:create db:schema:load db:seed_fu - - git checkout $CI_COMMIT_SHA - - bundle install $BUNDLE_INSTALL_FLAGS - - . scripts/prepare_build.sh - - bundle exec rake db:migrate - -migration pg paths: - <<: *migration-paths - <<: *use-pg - -migration mysql paths: - <<: *migration-paths - <<: *use-mysql - coverage: stage: post-test services: [] @@ -508,8 +511,8 @@ pages: <<: *dedicated-runner dependencies: - coverage - - rake karma - - rake gitlab:assets:compile + - karma + - gitlab:assets:compile - lint:javascript:report script: - mv public/ .public/ diff --git a/.rubocop.yml b/.rubocop.yml index 4e1d456d8d1..3cdafd96456 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -969,6 +969,12 @@ RSpec/DescribeSymbol: RSpec/DescribedClass: Enabled: true +# Checks if an example group does not include any tests. +RSpec/EmptyExampleGroup: + Enabled: true + CustomIncludeMethods: + - run_permission_checks + # Checks for long example. RSpec/ExampleLength: Enabled: false @@ -987,6 +993,10 @@ RSpec/ExampleWording: RSpec/ExpectActual: Enabled: true +# Checks for opportunities to use `expect { … }.to output`. +RSpec/ExpectOutput: + Enabled: true + # Checks the file and folder naming of the spec file. RSpec/FilePath: Enabled: true diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 7582f761bcb..cf30f5728c0 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -10,11 +10,6 @@ RSpec/BeforeAfterAll: Enabled: false -# Offense count: 15 -# Configuration parameters: CustomIncludeMethods. -RSpec/EmptyExampleGroup: - Enabled: false - # Offense count: 233 RSpec/EmptyLineAfterFinalLet: Enabled: false @@ -23,10 +18,6 @@ RSpec/EmptyLineAfterFinalLet: RSpec/EmptyLineAfterSubject: Enabled: false -# Offense count: 3 -RSpec/ExpectOutput: - Enabled: false - # Offense count: 72 # Configuration parameters: EnforcedStyle, SupportedStyles. # SupportedStyles: implicit, each, example diff --git a/CHANGELOG.md b/CHANGELOG.md index 38de411ebb7..4e6d8d398a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,11 +2,213 @@ documentation](doc/development/changelog.md) for instructions on adding your own entry. +## 9.2.1 (2017-05-23) + +- Fix placement of note emoji on hover. +- Fix migration for older PostgreSQL versions. + +## 9.2.0 (2017-05-22) + +- API: Filter merge requests by milestone and labels. (10924) +- Reset New branch button when issue state changes. !5962 (winniehell) +- Frontend prevent authored votes. !6260 (Barthc) +- Change issues list in MR to natural sorting. !7110 (Jeff Stubler) +- Add animations to all the dropdowns. !8419 +- Add update time to project lists. !8514 (Jeff Stubler) +- Remove view fragment caching for project READMEs. !8838 +- API: Add parameters to allow filtering project pipelines. !9367 (dosuken123) +- Database SSL support for backup script. !9715 (Guillaume Simon) +- Fix UI inconsistency different files view (find file button missing). !9847 (TM Lee) +- Display slash commands outcome when previewing Markdown. !10054 (Rares Sfirlogea) +- Resolve "Add more tests for spec/controllers/projects/builds_controller_spec.rb". !10244 (dosuken123) +- Add keyboard edit shotcut for wiki. !10245 (George Andrinopoulos) +- Redirect old links after renaming a user/group/project. !10370 +- Add system note on description change of issue/merge request. !10392 (blackst0ne) +- Improve validation of namespace & project paths. !10413 +- Add board_move slash command. !10433 (Alex Sanford) +- Update all instances of the old loading icon. !10490 (Andrew Torres) +- Implement protected manual actions. !10494 +- Implement search by extern_uid in Users API. !10509 (Robin Bobbitt) +- add support for .vue templates. !10517 +- Only add newlines between multiple uploads. !10545 +- Added balsamiq file viewer. !10564 +- Remove unnecessary test helpers includes. !10567 (Jacopo Beschi @jacopo-beschi) +- Add tooltip to header of Done board. !10574 (Andy Brown) +- Fix redundant cache expiration in Repository. !10575 (blackst0ne) +- Add hashie-forbidden_attributes gem. !10579 (Andy Brown) +- Add spec for schema.rb. !10580 (blackst0ne) +- Keep webpack-dev-server process functional across branch changes. !10581 +- Turns true value and false value database methods from instance to class methods. !10583 +- Improve text on todo list when the todo action comes from yourself. !10594 (Jacopo Beschi @jacopo-beschi) +- Replace rake cache:clear:db with an automatic mechanism. !10597 +- Remove heading and trailing spaces from label's color and title. !10603 (blackst0ne) +- Add webpack_bundle_tag helper to improve non-localhost GDK configurations. !10604 +- Added quick-update (fade-in) animation to newly rendered notes. !10623 +- Fix rendering emoji inside a string. !10647 (blackst0ne) +- Dockerfiles templates are imported from gitlab.com/gitlab-org/Dockerfile. !10663 +- Add support for i18n on Cycle Analytics page. !10669 +- Allow OAuth clients to push code. !10677 +- Add configurable timeout for git fetch and clone operations. !10697 +- Move labels of search results from bottom to title. !10705 (dr) +- Added build failures summary page for pipelines. !10719 +- Expand/collapse button -> Change to make it look like a toggle. !10720 (Jacopo Beschi @jacopo-beschi) +- Decrease ABC threshold to 57.08. !10724 (Rydkin Maxim) +- Removed target blank from the metrics action inside the environments list. !10726 +- Remove Repository#version method and tests. !10734 +- Refactor Admin::GroupsController#members_update method and add some specs. !10735 +- Refactor code that creates project/group members. !10735 +- Add Slack slash command api to services documentation and rearrange order and cases. !10757 (TM Lee) +- Disable test settings on chat notification services when repository is empty. !10759 +- Add support for instantly updating comments. !10760 +- Show checkmark on current assignee in assignee dropdown. !10767 +- Remove pipeline controls for last deployment from Environment monitoring page. !10769 +- Pipeline view updates in near real time. !10777 +- Fetch pipeline status in batch from redis. !10785 +- Add username to activity atom feed. !10802 (winniehell) +- Support Markdown previews for personal snippets. !10810 +- Implement ability to edit hooks. !10816 (Alexander Randa) +- Allow admins to sudo to blocked users via the API. !10842 +- Don't display the is_admin flag in most API responses. !10846 +- Refactor add_users method for project and group. !10850 +- Pipeline schedules got a new and improved UI. !10853 +- Fix updating merge_when_build_succeeds via merge API endpoint. !10873 +- Add index on ci_builds.user_id. !10874 (blackst0ne) +- Improves test settings for chat notification services for empty projects. !10886 +- Change Git commit command in Existing folder to git commit -m. !10900 (TM Lee) +- Show group name on flash container when group is created from Admin area. !10905 +- Make markdown tables thinner. !10909 (blackst0ne) +- Ensure namespace owner is Master of project upon creation. !10910 +- Updated CI status favicons to include the tanuki. !10923 +- Decrease Cyclomatic Complexity threshold to 16. !10928 (Rydkin Maxim) +- Replace header merge request icon. !10932 (blackst0ne) +- Fix error on CI/CD Settings page related to invalid pipeline trigger. !10948 (dosuken123) +- rickettm Add repo parameter to gitaly:install and workhorse:install rake tasks. !10979 (M. Ricketts) +- Generate and handle a gl_repository param to pass around components. !10992 +- Prevent 500 errors caused by testing the Prometheus service. !10994 +- Disable navigation to Project-level pages configuration when Pages disabled. !11008 +- Fix caching large snippet HTML content on MySQL databases. !11024 +- Hide external environment URL button on terminal page if URL is not defined. !11029 +- Always show the latest pipeline information in the commit box. !11038 +- Fix misaligned buttons in wiki pages. !11043 +- Colorize labels in search field. !11047 +- Sort the network graph both by commit date and topographically. !11057 +- Remove carriage returns from commit messages. !11077 +- Add tooltips to user contribution graph key. !11138 +- Add German translation for Cycle Analytics. !11161 +- Fix skipped manual actions problem when processing the pipeline. !11164 +- Fix cross referencing for private and internal projects. !11243 +- Add state to MR widget that prevent merges when branch changes after page load. !11316 +- Fixes the 500 when accessing customized appearance logos. !11479 (Alexis Reigel) +- Implement Users::BuildService. !30349 (George Andrinopoulos) +- Display comments for personal snippets. +- Support comments for personal snippets. +- Support uploaders for personal snippets comments. +- Handle incoming emails from aliases correctly. +- Re-rewrites pipeline graph in vue to support realtime data updates. +- Add issues/:iid/closed_by api endpoint. (mhasbini) +- Disallow merge requests from fork when source project have disabled merge requests. (mhasbini) +- Improved UX on project members settings view. +- Clear emoji search in awards menu after picking emoji. +- Cleanup markdown spacing. +- Separate CE params on Grape API. +- Allow to create new branch and empty WIP merge request from issue page. +- Prevent people from creating branches if they don't have persmission to push. +- Redesign auth 422 page. +- 29595 Update callout design. +- Detect already enabled DeployKeys in EnableDeployKeyService. +- Add transparent top-border to the hover state of done todos. +- Refactor all CI vue badges to use the same vue component. +- Update note edits in real-time. +- Add button to delete filters from filtered search bar. +- Added profile name to user dropdown. +- Display GitLab Pages status in Admin Dashboard. +- Fix label creation from issuable for subgroup projects. +- Vertically align mini pipeline stage container. +- prevent nav tabs from wrapping to new line. +- Fix environments vue architecture to match documentation. +- Enforce project features when searching blobs and wikis. +- fix inline diff copy in firefox. +- Note Ghost user and refer to user deletion documentation. +- Expose project statistics on single requests via the API. +- Job dropdown of pipeline mini graph updates in realtime when its opened. +- Add default margin-top to user request table on project members page. +- Add tooltips to note action buttons. +- Remove `#` being added on commit sha in MR widget. +- Remove spinner from loading comment. +- Fixes an issue preventing screen readers from reading some icons. +- Load milestone tabs asynchronously to increase initial load performance. +- [BB Importer] Save the error trace and the whole raw document to debug problems easier. +- Fixed branches dropdown rendering branch names as HTML. +- Make Asciidoc & other markup go through pipeline to prevent XSS. +- Validate URLs in markdown using URI to detect the host correctly. +- Side-by-side view in commits correcly expands full window width. +- Deploy keys load are loaded async. +- Fixed spacing of discussion submit buttons. +- Add hostname to usage ping. +- Allow usage ping to be disabled completely in gitlab.yml. +- Add artifact file page that uses the blob viewer. +- Add breadcrumb, build header and pipelines submenu to artifacts browser. +- Show Raw button as Download for binary files. +- Add Source/Rendered switch to blobs for SVG, Markdown, Asciidoc and other text files that can be rendered. +- Catch all URI errors in ExternalLinkFilter. +- Allow commenting on older versions of the diff and comparisons between diff versions. +- Paste a copied MR source branch name as code when pasted into a GFM form. +- Fix commenting on an existing discussion on an unchanged line that is no longer in the diff. +- Link to outdated diff in older MR version from outdated diff discussion. +- Bump Sidekiq to 5.0.0. +- Use blob viewers for snippets. +- Add download button to project snippets. +- Display video blobs in-line like images. +- Gracefully handle failures for incoming emails which do not match on the To header, and have no References header. +- Added title to award emoji buttons. +- Fixed alignment of empty task list items. +- Removed the target=_blank from the monitoring component to prevent opening a new tab. +- Fix new admin integrations not taking effect on existing projects. +- Prevent further repository corruption when resolving conflicts from a fork where both the fork and upstream projects require housekeeping. +- Add missing project attributes to Import/Export. +- Remove N+1 queries in processing MR references. +- Fixed wrong method call on notify_post_receive. (Luigi Leoni) +- Fixed search terms not correctly highlighting. +- Refactored the anchor tag to remove the trailing space in the target branch. +- Prevent user profile tabs to display raw json when going back and forward in browser history. +- Add index to webhooks type column. +- Change line-height on build-header so elements don't overlap. (Dino Maric) +- Fix dead link to GDK on the README page. (Dino Maric) +- Fixued preview shortcut focusing wrong preview tab. +- Issue assignees are now removed without loading unnecessary data into memory. +- Refactor backup/restore docs. +- Fixed group issues assignee dropdown loading all users. +- Fix for XSS in project import view caused by Hamlit filter usage. +- Fixed avatar not display on issue boards when Gravatar is disabled. +- Fixed create new label form in issue boards sidebar. +- Add realtime descriptions to issue show pages. +- Issue API change: assignee_id parameter and assignee object in a response have been deprecated. +- Fixed bug where merge request JSON would be displayed. +- Fixed Prometheus monitoring graphs not showing empty states in certain scenarios. +- Removed the milestone references from the milestone views. +- Show sizes correctly in merge requests when diffs overflow. +- Fix notify_only_default_branch check for Slack service. +- Make the `gitlab:gitlab_shell:check` task check that the repositories storage path are owned by the `root` group. +- Optimise pipelines.json endpoint. +- Pass docsUrl to pipeline schedules callout component. +- Fixed alignment of CI icon in issues related branches. +- Set the issuable sidebar to remain closed for mobile devices. +- Sanitize submodule URLs before linking to them in the file tree view. +- Upgrade Sidekiq to 4.2.10. +- Cache Routable#full_path in RequestStore to reduce duplicate route loads. +- Refactor snippets finder & dont return internal snippets for external users. +- Fix snippets visibility for show action - external users can not see internal snippets. +- Store retried in database for CI Builds. +- repository browser: handle submodule urls that don't end with .git. (David Turner) +- Fixed tags sort from defaulting to empty. +- Do not show private groups on subgroups page if user doesn't have access to. +- Make MR link in build sidebar bold. +- Unassign all Issues and Merge Requests when member leaves a team. +- Fix preemptive scroll bar on user activity calendar. +- Pipeline chat notifications convert seconds to minutes and hours. + ## 9.1.4 (2017-05-12) -- No changes. -- No changes. -- No changes. - Fix error on CI/CD Settings page related to invalid pipeline trigger. !10948 (dosuken123) - Sort the network graph both by commit date and topographically. !11057 - Fix cross referencing for private and internal projects. !11243 @@ -56,6 +258,7 @@ entry. ## 9.1.0 (2017-04-22) +- Add Jupyter notebook rendering !10017 - Added merge requests empty state. !7342 - Add option to start a new resolvable discussion in an MR. !7527 - Hide form inputs for group member without editing rights. !7816 diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION index 50e2274e6d3..2d6c0bcf19c 100644 --- a/GITLAB_SHELL_VERSION +++ b/GITLAB_SHELL_VERSION @@ -1 +1 @@ -5.0.3 +5.0.4 @@ -1 +1 @@ -9.2.0-pre +9.3.0-pre diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js index e5f36c84987..6680834a8d1 100644 --- a/app/assets/javascripts/api.js +++ b/app/assets/javascripts/api.js @@ -1,148 +1,175 @@ -/* eslint-disable func-names, space-before-function-paren, quotes, object-shorthand, camelcase, no-var, comma-dangle, prefer-arrow-callback, quote-props, no-param-reassign, max-len */ - -var Api = { - groupsPath: "/api/:version/groups.json", - groupPath: "/api/:version/groups/:id.json", - namespacesPath: "/api/:version/namespaces.json", - groupProjectsPath: "/api/:version/groups/:id/projects.json", - projectsPath: "/api/:version/projects.json?simple=true", - labelsPath: "/:namespace_path/:project_path/labels", - licensePath: "/api/:version/templates/licenses/:key", - gitignorePath: "/api/:version/templates/gitignores/:key", - gitlabCiYmlPath: "/api/:version/templates/gitlab_ci_ymls/:key", - dockerfilePath: "/api/:version/templates/dockerfiles/:key", - issuableTemplatePath: "/:namespace_path/:project_path/templates/:type/:key", - group: function(group_id, callback) { - var url = Api.buildUrl(Api.groupPath) - .replace(':id', group_id); +import $ from 'jquery'; + +const Api = { + groupsPath: '/api/:version/groups.json', + groupPath: '/api/:version/groups/:id.json', + namespacesPath: '/api/:version/namespaces.json', + groupProjectsPath: '/api/:version/groups/:id/projects.json', + projectsPath: '/api/:version/projects.json?simple=true', + labelsPath: '/:namespace_path/:project_path/labels', + licensePath: '/api/:version/templates/licenses/:key', + gitignorePath: '/api/:version/templates/gitignores/:key', + gitlabCiYmlPath: '/api/:version/templates/gitlab_ci_ymls/:key', + dockerfilePath: '/api/:version/templates/dockerfiles/:key', + issuableTemplatePath: '/:namespace_path/:project_path/templates/:type/:key', + usersPath: '/api/:version/users.json', + + group(groupId, callback) { + const url = Api.buildUrl(Api.groupPath) + .replace(':id', groupId); return $.ajax({ - url: url, - dataType: "json" - }).done(function(group) { - return callback(group); - }); + url, + dataType: 'json', + }) + .done(group => callback(group)); }, + // Return groups list. Filtered by query - groups: function(query, options, callback) { - var url = Api.buildUrl(Api.groupsPath); + groups(query, options, callback) { + const url = Api.buildUrl(Api.groupsPath); return $.ajax({ - url: url, - data: $.extend({ + url, + data: Object.assign({ search: query, - per_page: 20 + per_page: 20, }, options), - dataType: "json" - }).done(function(groups) { - return callback(groups); - }); + dataType: 'json', + }) + .done(groups => callback(groups)); }, + // Return namespaces list. Filtered by query - namespaces: function(query, callback) { - var url = Api.buildUrl(Api.namespacesPath); + namespaces(query, callback) { + const url = Api.buildUrl(Api.namespacesPath); return $.ajax({ - url: url, + url, data: { search: query, - per_page: 20 + per_page: 20, }, - dataType: "json" - }).done(function(namespaces) { - return callback(namespaces); - }); + dataType: 'json', + }).done(namespaces => callback(namespaces)); }, + // Return projects list. Filtered by query - projects: function(query, options, callback) { - var url = Api.buildUrl(Api.projectsPath); + projects(query, options, callback) { + const url = Api.buildUrl(Api.projectsPath); return $.ajax({ - url: url, - data: $.extend({ + url, + data: Object.assign({ search: query, per_page: 20, - membership: true + membership: true, }, options), - dataType: "json" - }).done(function(projects) { - return callback(projects); - }); + dataType: 'json', + }) + .done(projects => callback(projects)); }, - newLabel: function(namespace_path, project_path, data, callback) { - var url = Api.buildUrl(Api.labelsPath) - .replace(':namespace_path', namespace_path) - .replace(':project_path', project_path); + + newLabel(namespacePath, projectPath, data, callback) { + const url = Api.buildUrl(Api.labelsPath) + .replace(':namespace_path', namespacePath) + .replace(':project_path', projectPath); return $.ajax({ - url: url, - type: "POST", - data: { 'label': data }, - dataType: "json" - }).done(function(label) { - return callback(label); - }).error(function(message) { - return callback(message.responseJSON); - }); + url, + type: 'POST', + data: { label: data }, + dataType: 'json', + }) + .done(label => callback(label)) + .error(message => callback(message.responseJSON)); }, + // Return group projects list. Filtered by query - groupProjects: function(group_id, query, callback) { - var url = Api.buildUrl(Api.groupProjectsPath) - .replace(':id', group_id); + groupProjects(groupId, query, callback) { + const url = Api.buildUrl(Api.groupProjectsPath) + .replace(':id', groupId); return $.ajax({ - url: url, + url, data: { search: query, - per_page: 20 + per_page: 20, }, - dataType: "json" - }).done(function(projects) { - return callback(projects); - }); + dataType: 'json', + }) + .done(projects => callback(projects)); }, + // Return text for a specific license - licenseText: function(key, data, callback) { - var url = Api.buildUrl(Api.licensePath) + licenseText(key, data, callback) { + const url = Api.buildUrl(Api.licensePath) .replace(':key', key); return $.ajax({ - url: url, - data: data - }).done(function(license) { - return callback(license); - }); + url, + data, + }) + .done(license => callback(license)); }, - gitignoreText: function(key, callback) { - var url = Api.buildUrl(Api.gitignorePath) + + gitignoreText(key, callback) { + const url = Api.buildUrl(Api.gitignorePath) .replace(':key', key); - return $.get(url, function(gitignore) { - return callback(gitignore); - }); + return $.get(url, gitignore => callback(gitignore)); }, - gitlabCiYml: function(key, callback) { - var url = Api.buildUrl(Api.gitlabCiYmlPath) + + gitlabCiYml(key, callback) { + const url = Api.buildUrl(Api.gitlabCiYmlPath) .replace(':key', key); - return $.get(url, function(file) { - return callback(file); - }); + return $.get(url, file => callback(file)); }, - dockerfileYml: function(key, callback) { - var url = Api.buildUrl(Api.dockerfilePath).replace(':key', key); + + dockerfileYml(key, callback) { + const url = Api.buildUrl(Api.dockerfilePath).replace(':key', key); $.get(url, callback); }, - issueTemplate: function(namespacePath, projectPath, key, type, callback) { - var url = Api.buildUrl(Api.issuableTemplatePath) + + issueTemplate(namespacePath, projectPath, key, type, callback) { + const url = Api.buildUrl(Api.issuableTemplatePath) .replace(':key', key) .replace(':type', type) .replace(':project_path', projectPath) .replace(':namespace_path', namespacePath); $.ajax({ - url: url, - dataType: 'json' - }).done(function(file) { - callback(null, file); - }).error(callback); + url, + dataType: 'json', + }) + .done(file => callback(null, file)) + .error(callback); }, - buildUrl: function(url) { + + users(query, options) { + const url = Api.buildUrl(this.usersPath); + return Api.wrapAjaxCall({ + url, + data: Object.assign({ + search: query, + per_page: 20, + }, options), + dataType: 'json', + }); + }, + + buildUrl(url) { + let urlRoot = ''; if (gon.relative_url_root != null) { - url = gon.relative_url_root + url; + urlRoot = gon.relative_url_root; } - return url.replace(':version', gon.api_version); - } + return urlRoot + url.replace(':version', gon.api_version); + }, + + wrapAjaxCall(options) { + return new Promise((resolve, reject) => { + // jQuery 2 is not Promises/A+ compatible (missing catch) + $.ajax(options) // eslint-disable-line promise/catch-or-return + .then(data => resolve(data), + (jqXHR, textStatus, errorThrown) => { + const error = new Error(`${options.url}: ${errorThrown}`); + error.textStatus = textStatus; + reject(error); + }, + ); + }); + }, }; -window.Api = Api; +export default Api; diff --git a/app/assets/javascripts/blob/file_template_selector.js b/app/assets/javascripts/blob/file_template_selector.js index ab5b3751c4e..5ae30990aea 100644 --- a/app/assets/javascripts/blob/file_template_selector.js +++ b/app/assets/javascripts/blob/file_template_selector.js @@ -1,5 +1,3 @@ -/* global Api */ - export default class FileTemplateSelector { constructor(mediator) { this.mediator = mediator; @@ -65,4 +63,3 @@ export default class FileTemplateSelector { this.reportSelection(opts); } } - diff --git a/app/assets/javascripts/blob/template_selectors/ci_yaml_selector.js b/app/assets/javascripts/blob/template_selectors/ci_yaml_selector.js index f2f81af137b..9c41e429c8d 100644 --- a/app/assets/javascripts/blob/template_selectors/ci_yaml_selector.js +++ b/app/assets/javascripts/blob/template_selectors/ci_yaml_selector.js @@ -1,4 +1,4 @@ -/* global Api */ +import Api from '../../api'; import FileTemplateSelector from '../file_template_selector'; diff --git a/app/assets/javascripts/blob/template_selectors/dockerfile_selector.js b/app/assets/javascripts/blob/template_selectors/dockerfile_selector.js index 3cb7b960aaa..45fb614fe00 100644 --- a/app/assets/javascripts/blob/template_selectors/dockerfile_selector.js +++ b/app/assets/javascripts/blob/template_selectors/dockerfile_selector.js @@ -1,4 +1,4 @@ -/* global Api */ +import Api from '../../api'; import FileTemplateSelector from '../file_template_selector'; diff --git a/app/assets/javascripts/blob/template_selectors/gitignore_selector.js b/app/assets/javascripts/blob/template_selectors/gitignore_selector.js index 7efda8e7f50..a894953cc86 100644 --- a/app/assets/javascripts/blob/template_selectors/gitignore_selector.js +++ b/app/assets/javascripts/blob/template_selectors/gitignore_selector.js @@ -1,4 +1,4 @@ -/* global Api */ +import Api from '../../api'; import FileTemplateSelector from '../file_template_selector'; diff --git a/app/assets/javascripts/blob/template_selectors/license_selector.js b/app/assets/javascripts/blob/template_selectors/license_selector.js index 1d757332f6c..b7c4da0f62e 100644 --- a/app/assets/javascripts/blob/template_selectors/license_selector.js +++ b/app/assets/javascripts/blob/template_selectors/license_selector.js @@ -1,4 +1,4 @@ -/* global Api */ +import Api from '../../api'; import FileTemplateSelector from '../file_template_selector'; diff --git a/app/assets/javascripts/blob/viewer/index.js b/app/assets/javascripts/blob/viewer/index.js index 849da633c89..d7c62889dde 100644 --- a/app/assets/javascripts/blob/viewer/index.js +++ b/app/assets/javascripts/blob/viewer/index.js @@ -50,9 +50,9 @@ export default class BlobViewer { if (this.copySourceBtn) { this.copySourceBtn.addEventListener('click', () => { - if (this.copySourceBtn.classList.contains('disabled')) return; + if (this.copySourceBtn.classList.contains('disabled')) return this.copySourceBtn.blur(); - this.switchToViewer('simple'); + return this.switchToViewer('simple'); }); } } @@ -114,6 +114,7 @@ export default class BlobViewer { $(viewer).syntaxHighlight(); this.$fileHolder.trigger('highlight:line'); + gl.utils.handleLocationHash(); this.toggleCopyButtonState(); }) diff --git a/app/assets/javascripts/boards/boards_bundle.js b/app/assets/javascripts/boards/boards_bundle.js index 88eb4251339..e0a6f64dd42 100644 --- a/app/assets/javascripts/boards/boards_bundle.js +++ b/app/assets/javascripts/boards/boards_bundle.js @@ -6,23 +6,22 @@ import Vue from 'vue'; import VueResource from 'vue-resource'; import FilteredSearchBoards from './filtered_search_boards'; import eventHub from './eventhub'; - -require('./models/issue'); -require('./models/label'); -require('./models/list'); -require('./models/milestone'); -require('./models/assignee'); -require('./stores/boards_store'); -require('./stores/modal_store'); -require('./services/board_service'); -require('./mixins/modal_mixins'); -require('./mixins/sortable_default_options'); -require('./filters/due_date_filters'); -require('./components/board'); -require('./components/board_sidebar'); -require('./components/new_list_dropdown'); -require('./components/modal/index'); -require('../vue_shared/vue_resource_interceptor'); +import './models/issue'; +import './models/label'; +import './models/list'; +import './models/milestone'; +import './models/assignee'; +import './stores/boards_store'; +import './stores/modal_store'; +import './services/board_service'; +import './mixins/modal_mixins'; +import './mixins/sortable_default_options'; +import './filters/due_date_filters'; +import './components/board'; +import './components/board_sidebar'; +import './components/new_list_dropdown'; +import './components/modal/index'; +import '../vue_shared/vue_resource_interceptor'; Vue.use(VueResource); diff --git a/app/assets/javascripts/boards/components/board.js b/app/assets/javascripts/boards/components/board.js index 0d23bdeeb99..9ba84489910 100644 --- a/app/assets/javascripts/boards/components/board.js +++ b/app/assets/javascripts/boards/components/board.js @@ -3,9 +3,7 @@ import Vue from 'vue'; import boardList from './board_list'; import boardBlankState from './board_blank_state'; - -require('./board_delete'); -require('./board_list'); +import './board_delete'; const Store = gl.issueBoards.BoardsStore; diff --git a/app/assets/javascripts/boards/components/board_card.js b/app/assets/javascripts/boards/components/board_card.js index f591134c548..079fb6438b9 100644 --- a/app/assets/javascripts/boards/components/board_card.js +++ b/app/assets/javascripts/boards/components/board_card.js @@ -1,4 +1,4 @@ -require('./issue_card_inner'); +import './issue_card_inner'; const Store = gl.issueBoards.BoardsStore; diff --git a/app/assets/javascripts/boards/components/board_sidebar.js b/app/assets/javascripts/boards/components/board_sidebar.js index 9bcea302da2..386102032cb 100644 --- a/app/assets/javascripts/boards/components/board_sidebar.js +++ b/app/assets/javascripts/boards/components/board_sidebar.js @@ -7,11 +7,9 @@ import Vue from 'vue'; import eventHub from '../../sidebar/event_hub'; - import AssigneeTitle from '../../sidebar/components/assignees/assignee_title'; import Assignees from '../../sidebar/components/assignees/assignees'; - -require('./sidebar/remove_issue'); +import './sidebar/remove_issue'; const Store = gl.issueBoards.BoardsStore; @@ -45,6 +43,12 @@ gl.issueBoards.BoardSidebar = Vue.extend({ detail: { handler () { if (this.issue.id !== this.detail.issue.id) { + $('.block.assignee') + .find('input:not(.js-vue)[name="issue[assignee_ids][]"]') + .each((i, el) => { + $(el).remove(); + }); + $('.js-issue-board-sidebar', this.$el).each((i, el) => { $(el).data('glDropdown').clearMenu(); }); @@ -59,18 +63,6 @@ gl.issueBoards.BoardSidebar = Vue.extend({ }, deep: true }, - issue () { - if (this.showSidebar) { - this.$nextTick(() => { - $('.right-sidebar').getNiceScroll(0).doScrollTop(0, 0); - $('.right-sidebar').getNiceScroll().resize(); - }); - } - - this.issue = this.detail.issue; - this.list = this.detail.list; - }, - deep: true }, methods: { closeSidebar () { diff --git a/app/assets/javascripts/boards/components/issue_card_inner.js b/app/assets/javascripts/boards/components/issue_card_inner.js index 710207db0c7..4699ef5a51c 100644 --- a/app/assets/javascripts/boards/components/issue_card_inner.js +++ b/app/assets/javascripts/boards/components/issue_card_inner.js @@ -1,4 +1,5 @@ import Vue from 'vue'; +import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; import eventHub from '../eventhub'; const Store = gl.issueBoards.BoardsStore; @@ -38,6 +39,9 @@ gl.issueBoards.IssueCardInner = Vue.extend({ maxCounter: 99, }; }, + components: { + userAvatarLink, + }, computed: { numberOverLimit() { return this.issue.assignees.length - this.limitBeforeCounter; @@ -146,23 +150,16 @@ gl.issueBoards.IssueCardInner = Vue.extend({ </span> </h4> <div class="card-assignee"> - <a - class="has-tooltip js-no-trigger" - :href="assigneeUrl(assignee)" - :title="assigneeUrlTitle(assignee)" + <user-avatar-link v-for="(assignee, index) in issue.assignees" v-if="shouldRenderAssignee(index)" - data-container="body" - data-placement="bottom" - > - <img - class="avatar avatar-inline s20" - :src="assignee.avatar" - width="20" - height="20" - :alt="avatarUrlTitle(assignee)" - /> - </a> + class="js-no-trigger" + :link-href="assigneeUrl(assignee)" + :img-alt="avatarUrlTitle(assignee)" + :img-src="assignee.avatar" + :tooltip-text="assigneeUrlTitle(assignee)" + tooltip-placement="bottom" + /> <span class="avatar-counter has-tooltip" :title="assigneeCounterTooltip" diff --git a/app/assets/javascripts/boards/components/modal/footer.js b/app/assets/javascripts/boards/components/modal/footer.js index ccd270b27da..fe7ab2db85d 100644 --- a/app/assets/javascripts/boards/components/modal/footer.js +++ b/app/assets/javascripts/boards/components/modal/footer.js @@ -2,8 +2,7 @@ /* global Flash */ import Vue from 'vue'; - -require('./lists_dropdown'); +import './lists_dropdown'; const ModalStore = gl.issueBoards.ModalStore; diff --git a/app/assets/javascripts/boards/components/modal/header.js b/app/assets/javascripts/boards/components/modal/header.js index e2b3f9ae7e2..31f59d295bf 100644 --- a/app/assets/javascripts/boards/components/modal/header.js +++ b/app/assets/javascripts/boards/components/modal/header.js @@ -1,7 +1,6 @@ import Vue from 'vue'; import modalFilters from './filters'; - -require('./tabs'); +import './tabs'; const ModalStore = gl.issueBoards.ModalStore; diff --git a/app/assets/javascripts/boards/components/modal/index.js b/app/assets/javascripts/boards/components/modal/index.js index 507f16f3f06..6356c266ee2 100644 --- a/app/assets/javascripts/boards/components/modal/index.js +++ b/app/assets/javascripts/boards/components/modal/index.js @@ -3,11 +3,10 @@ import Vue from 'vue'; import queryData from '../../utils/query_data'; import loadingIcon from '../../../vue_shared/components/loading_icon.vue'; - -require('./header'); -require('./list'); -require('./footer'); -require('./empty_state'); +import './header'; +import './list'; +import './footer'; +import './empty_state'; const ModalStore = gl.issueBoards.ModalStore; diff --git a/app/assets/javascripts/commit/pipelines/pipelines_table.js b/app/assets/javascripts/commit/pipelines/pipelines_table.js index b8be0d8a301..98698143d22 100644 --- a/app/assets/javascripts/commit/pipelines/pipelines_table.js +++ b/app/assets/javascripts/commit/pipelines/pipelines_table.js @@ -1,11 +1,11 @@ import Vue from 'vue'; import Visibility from 'visibilityjs'; -import PipelinesTableComponent from '../../vue_shared/components/pipelines_table'; +import pipelinesTableComponent from '../../vue_shared/components/pipelines_table'; import PipelinesService from '../../pipelines/services/pipelines_service'; import PipelineStore from '../../pipelines/stores/pipelines_store'; import eventHub from '../../pipelines/event_hub'; -import EmptyState from '../../pipelines/components/empty_state.vue'; -import ErrorState from '../../pipelines/components/error_state.vue'; +import emptyState from '../../pipelines/components/empty_state.vue'; +import errorState from '../../pipelines/components/error_state.vue'; import loadingIcon from '../../vue_shared/components/loading_icon.vue'; import '../../lib/utils/common_utils'; import '../../vue_shared/vue_resource_interceptor'; @@ -23,9 +23,9 @@ import Poll from '../../lib/utils/poll'; export default Vue.component('pipelines-table', { components: { - 'pipelines-table-component': PipelinesTableComponent, - 'error-state': ErrorState, - 'empty-state': EmptyState, + pipelinesTableComponent, + errorState, + emptyState, loadingIcon, }, @@ -47,6 +47,7 @@ export default Vue.component('pipelines-table', { hasError: false, isMakingRequest: false, updateGraphDropdown: false, + hasMadeRequest: false, }; }, @@ -55,9 +56,15 @@ export default Vue.component('pipelines-table', { return this.hasError && !this.isLoading; }, + /** + * Empty state is only rendered if after the first request we receive no pipelines. + * + * @return {Boolean} + */ shouldRenderEmptyState() { return !this.state.pipelines.length && !this.isLoading && + this.hasMadeRequest && !this.hasError; }, @@ -94,6 +101,10 @@ export default Vue.component('pipelines-table', { if (!Visibility.hidden()) { this.isLoading = true; this.poll.makeRequest(); + } else { + // If tab is not visible we need to make the first request so we don't show the empty + // state without knowing if there are any pipelines + this.fetchPipelines(); } Visibility.change(() => { @@ -127,6 +138,8 @@ export default Vue.component('pipelines-table', { successCallback(resp) { const response = resp.json(); + this.hasMadeRequest = true; + // depending of the endpoint the response can either bring a `pipelines` key or not. const pipelines = response.pipelines || response; this.store.storePipelines(pipelines); diff --git a/app/assets/javascripts/copy_as_gfm.js b/app/assets/javascripts/copy_as_gfm.js index 570799c030e..ba9d9a3e1f7 100644 --- a/app/assets/javascripts/copy_as_gfm.js +++ b/app/assets/javascripts/copy_as_gfm.js @@ -1,6 +1,6 @@ /* eslint-disable class-methods-use-this, object-shorthand, no-unused-vars, no-use-before-define, no-new, max-len, no-restricted-syntax, guard-for-in, no-continue */ -require('./lib/utils/common_utils'); +import './lib/utils/common_utils'; const gfmRules = { // The filters referenced in lib/banzai/pipeline/gfm_pipeline.rb convert @@ -18,12 +18,12 @@ const gfmRules = { }, }, TaskListFilter: { - 'input[type=checkbox].task-list-item-checkbox'(el, text) { + 'input[type=checkbox].task-list-item-checkbox'(el) { return `[${el.checked ? 'x' : ' '}]`; }, }, ReferenceFilter: { - '.tooltip'(el, text) { + '.tooltip'(el) { return ''; }, 'a.gfm:not([data-link=true])'(el, text) { @@ -39,15 +39,15 @@ const gfmRules = { }, }, TableOfContentsFilter: { - 'ul.section-nav'(el, text) { + 'ul.section-nav'(el) { return '[[_TOC_]]'; }, }, EmojiFilter: { - 'img.emoji'(el, text) { + 'img.emoji'(el) { return el.getAttribute('alt'); }, - 'gl-emoji'(el, text) { + 'gl-emoji'(el) { return `:${el.getAttribute('data-name')}:`; }, }, @@ -57,13 +57,13 @@ const gfmRules = { }, }, VideoLinkFilter: { - '.video-container'(el, text) { + '.video-container'(el) { const videoEl = el.querySelector('video'); if (!videoEl) return false; return CopyAsGFM.nodeToGFM(videoEl); }, - 'video'(el, text) { + 'video'(el) { return `![${el.dataset.title}](${el.getAttribute('src')})`; }, }, @@ -74,19 +74,19 @@ const gfmRules = { 'code.code.math[data-math-style=inline]'(el, text) { return `$\`${text}\`$`; }, - 'span.katex-display span.katex-mathml'(el, 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, text) { + '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, text) { + 'span.katex-html'(el) { // We don't want to include the content of this element in the copied text. return ''; }, @@ -95,7 +95,7 @@ const gfmRules = { }, }, SanitizationFilter: { - 'a[name]:not([href]):empty'(el, text) { + 'a[name]:not([href]):empty'(el) { return el.outerHTML; }, 'dl'(el, text) { @@ -143,7 +143,7 @@ const gfmRules = { }, }, MarkdownFilter: { - 'br'(el, text) { + 'br'(el) { // Two spaces at the end of a line are turned into a BR return ' '; }, @@ -162,7 +162,7 @@ const gfmRules = { 'blockquote'(el, text) { return text.trim().split('\n').map(s => `> ${s}`.trim()).join('\n'); }, - 'img'(el, text) { + 'img'(el) { return `![${el.getAttribute('alt')}](${el.getAttribute('src')})`; }, 'a.anchor'(el, text) { @@ -222,10 +222,10 @@ const gfmRules = { 'sup'(el, text) { return `^${text}`; }, - 'hr'(el, text) { + 'hr'(el) { return '-----'; }, - 'table'(el, text) { + 'table'(el) { const theadEl = el.querySelector('thead'); const tbodyEl = el.querySelector('tbody'); if (!theadEl || !tbodyEl) return false; @@ -233,11 +233,11 @@ const gfmRules = { const theadText = CopyAsGFM.nodeToGFM(theadEl); const tbodyText = CopyAsGFM.nodeToGFM(tbodyEl); - return theadText + tbodyText; + return [theadText, tbodyText].join('\n'); }, 'thead'(el, text) { const cells = _.map(el.querySelectorAll('th'), (cell) => { - let chars = CopyAsGFM.nodeToGFM(cell).trim().length + 2; + let chars = CopyAsGFM.nodeToGFM(cell).length + 2; let before = ''; let after = ''; @@ -262,10 +262,15 @@ const gfmRules = { return before + middle + after; }); - return `${text}|${cells.join('|')}|`; + const separatorRow = `|${cells.join('|')}|`; + + return [text, separatorRow].join('\n'); }, - 'tr'(el, text) { - const cells = _.map(el.querySelectorAll('td, th'), cell => CopyAsGFM.nodeToGFM(cell).trim()); + '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(' | ')} |`; }, }, @@ -273,12 +278,12 @@ const gfmRules = { class CopyAsGFM { constructor() { - $(document).on('copy', '.md, .wiki', (e) => { this.copyAsGFM(e, CopyAsGFM.transformGFMSelection); }); - $(document).on('copy', 'pre.code.highlight, .diff-content .line_content', (e) => { this.copyAsGFM(e, CopyAsGFM.transformCodeSelection); }); - $(document).on('paste', '.js-gfm-input', this.pasteGFM.bind(this)); + $(document).on('copy', '.md, .wiki', (e) => { CopyAsGFM.copyAsGFM(e, CopyAsGFM.transformGFMSelection); }); + $(document).on('copy', 'pre.code.highlight, .diff-content .line_content', (e) => { CopyAsGFM.copyAsGFM(e, CopyAsGFM.transformCodeSelection); }); + $(document).on('paste', '.js-gfm-input', CopyAsGFM.pasteGFM); } - copyAsGFM(e, transformer) { + static copyAsGFM(e, transformer) { const clipboardData = e.originalEvent.clipboardData; if (!clipboardData) return; @@ -292,26 +297,59 @@ class CopyAsGFM { e.stopPropagation(); clipboardData.setData('text/plain', el.textContent); - clipboardData.setData('text/x-gfm', CopyAsGFM.nodeToGFM(el)); + clipboardData.setData('text/x-gfm', this.nodeToGFM(el)); } - pasteGFM(e) { + static pasteGFM(e) { const clipboardData = e.originalEvent.clipboardData; if (!clipboardData) return; + const text = clipboardData.getData('text/plain'); const gfm = clipboardData.getData('text/x-gfm'); if (!gfm) return; e.preventDefault(); - window.gl.utils.insertText(e.target, gfm); + window.gl.utils.insertText(e.target, (textBefore, textAfter) => { + // 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. + // This logic still holds when there are one or more _closed_ code spans + // or blocks that will have 2 or 6 backticks. + // This will break down when the actual code block contains an uneven + // number of backticks, but this is a rare edge case. + const backtickMatch = textBefore.match(/`/g); + const insideCodeBlock = backtickMatch && (backtickMatch.length % 2) === 1; + + if (insideCodeBlock) { + return text; + } + + return gfm; + }); } static transformGFMSelection(documentFragment) { - // If the documentFragment contains more than just Markdown, don't copy as GFM. - if (documentFragment.querySelector('.md, .wiki')) return null; + const gfmEls = documentFragment.querySelectorAll('.md, .wiki'); + switch (gfmEls.length) { + case 0: { + return documentFragment; + } + case 1: { + return gfmEls[0]; + } + default: { + const allGfmEl = document.createElement('div'); + + for (let i = 0; i < gfmEls.length; i += 1) { + const lineEl = gfmEls[i]; + allGfmEl.appendChild(lineEl); + allGfmEl.appendChild(document.createTextNode('\n\n')); + } - return documentFragment; + return allGfmEl; + } + } } static transformCodeSelection(documentFragment) { @@ -343,7 +381,7 @@ class CopyAsGFM { return codeEl; } - static nodeToGFM(node) { + static nodeToGFM(node, respectWhitespaceParam = false) { if (node.nodeType === Node.COMMENT_NODE) { return ''; } @@ -352,7 +390,9 @@ class CopyAsGFM { return node.textContent; } - const text = this.innerGFM(node); + const respectWhitespace = respectWhitespaceParam || (node.nodeName === 'PRE' || node.nodeName === 'CODE'); + + const text = this.innerGFM(node, respectWhitespace); if (node.nodeType === Node.DOCUMENT_FRAGMENT_NODE) { return text; @@ -366,7 +406,17 @@ class CopyAsGFM { if (!window.gl.utils.nodeMatchesSelector(node, selector)) continue; - const result = func(node, text); + 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; @@ -376,7 +426,7 @@ class CopyAsGFM { return text; } - static innerGFM(parentNode) { + static innerGFM(parentNode, respectWhitespace = false) { const nodes = parentNode.childNodes; const clonedParentNode = parentNode.cloneNode(true); @@ -386,13 +436,19 @@ class CopyAsGFM { const node = nodes[i]; const clonedNode = clonedNodes[i]; - const text = this.nodeToGFM(node); + const text = this.nodeToGFM(node, respectWhitespace); // `clonedNode.replaceWith(text)` is not yet widely supported clonedNode.parentNode.replaceChild(document.createTextNode(text), clonedNode); } - return clonedParentNode.innerText || clonedParentNode.textContent; + let nodeText = clonedParentNode.innerText || clonedParentNode.textContent; + + if (!respectWhitespace) { + nodeText = nodeText.trim(); + } + + return nodeText; } } diff --git a/app/assets/javascripts/create_label.js b/app/assets/javascripts/create_label.js index 121d64db789..907b468e576 100644 --- a/app/assets/javascripts/create_label.js +++ b/app/assets/javascripts/create_label.js @@ -1,5 +1,5 @@ /* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, comma-dangle, prefer-template, quotes, no-param-reassign, wrap-iife, max-len */ -/* global Api */ +import Api from './api'; class CreateLabelDropdown { constructor ($el, namespacePath, projectPath) { diff --git a/app/assets/javascripts/cycle_analytics/components/stage_code_component.js b/app/assets/javascripts/cycle_analytics/components/stage_code_component.js index 0d9ad197abf..7c32a38fbe7 100644 --- a/app/assets/javascripts/cycle_analytics/components/stage_code_component.js +++ b/app/assets/javascripts/cycle_analytics/components/stage_code_component.js @@ -1,6 +1,7 @@ /* eslint-disable no-param-reassign */ import Vue from 'vue'; +import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue'; const global = window.gl || (window.gl = {}); global.cycleAnalytics = global.cycleAnalytics || {}; @@ -10,6 +11,9 @@ global.cycleAnalytics.StageCodeComponent = Vue.extend({ items: Array, stage: Object, }, + components: { + userAvatarImage, + }, template: ` <div> <div class="events-description"> @@ -19,7 +23,8 @@ global.cycleAnalytics.StageCodeComponent = Vue.extend({ <ul class="stage-event-list"> <li v-for="mergeRequest in items" class="stage-event-item"> <div class="item-details"> - <img class="avatar" :src="mergeRequest.author.avatarUrl"> + <!-- FIXME: Pass an alt attribute here for accessibility --> + <user-avatar-image :img-src="mergeRequest.author.avatarUrl"/> <h5 class="item-title merge-merquest-title"> <a :href="mergeRequest.url"> {{ mergeRequest.title }} @@ -28,11 +33,11 @@ global.cycleAnalytics.StageCodeComponent = Vue.extend({ <a :href="mergeRequest.url" class="issue-link">!{{ mergeRequest.iid }}</a> · <span> - {{ __('OpenedNDaysAgo|Opened') }} + {{ s__('OpenedNDaysAgo|Opened') }} <a :href="mergeRequest.url" class="issue-date">{{ mergeRequest.createdAt }}</a> </span> <span> - {{ __('ByAuthor|by') }} + {{ s__('ByAuthor|by') }} <a :href="mergeRequest.author.webUrl" class="issue-author-link">{{ mergeRequest.author.name }}</a> </span> </div> diff --git a/app/assets/javascripts/cycle_analytics/components/stage_issue_component.js b/app/assets/javascripts/cycle_analytics/components/stage_issue_component.js index ad285874643..5f4a0ac8590 100644 --- a/app/assets/javascripts/cycle_analytics/components/stage_issue_component.js +++ b/app/assets/javascripts/cycle_analytics/components/stage_issue_component.js @@ -1,6 +1,6 @@ /* eslint-disable no-param-reassign */ - import Vue from 'vue'; +import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue'; const global = window.gl || (window.gl = {}); global.cycleAnalytics = global.cycleAnalytics || {}; @@ -10,6 +10,9 @@ global.cycleAnalytics.StageIssueComponent = Vue.extend({ items: Array, stage: Object, }, + components: { + userAvatarImage, + }, template: ` <div> <div class="events-description"> @@ -19,7 +22,8 @@ global.cycleAnalytics.StageIssueComponent = Vue.extend({ <ul class="stage-event-list"> <li v-for="issue in items" class="stage-event-item"> <div class="item-details"> - <img class="avatar" :src="issue.author.avatarUrl"> + <!-- FIXME: Pass an alt attribute here for accessibility --> + <user-avatar-image :img-src="issue.author.avatarUrl"/> <h5 class="item-title issue-title"> <a class="issue-title" :href="issue.url"> {{ issue.title }} @@ -28,11 +32,11 @@ global.cycleAnalytics.StageIssueComponent = Vue.extend({ <a :href="issue.url" class="issue-link">#{{ issue.iid }}</a> · <span> - {{ __('OpenedNDaysAgo|Opened') }} + {{ s__('OpenedNDaysAgo|Opened') }} <a :href="issue.url" class="issue-date">{{ issue.createdAt }}</a> </span> <span> - {{ __('ByAuthor|by') }} + {{ s__('ByAuthor|by') }} <a :href="issue.author.webUrl" class="issue-author-link"> {{ issue.author.name }} </a> diff --git a/app/assets/javascripts/cycle_analytics/components/stage_plan_component.js b/app/assets/javascripts/cycle_analytics/components/stage_plan_component.js index dec1704395e..11fee5410d9 100644 --- a/app/assets/javascripts/cycle_analytics/components/stage_plan_component.js +++ b/app/assets/javascripts/cycle_analytics/components/stage_plan_component.js @@ -1,5 +1,6 @@ /* eslint-disable no-param-reassign */ import Vue from 'vue'; +import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue'; import iconCommit from '../svg/icon_commit.svg'; const global = window.gl || (window.gl = {}); @@ -10,11 +11,12 @@ global.cycleAnalytics.StagePlanComponent = Vue.extend({ items: Array, stage: Object, }, - + components: { + userAvatarImage, + }, data() { return { iconCommit }; }, - template: ` <div> <div class="events-description"> @@ -24,17 +26,18 @@ global.cycleAnalytics.StagePlanComponent = Vue.extend({ <ul class="stage-event-list"> <li v-for="commit in items" class="stage-event-item"> <div class="item-details item-conmmit-component"> - <img class="avatar" :src="commit.author.avatarUrl"> + <!-- FIXME: Pass an alt attribute here for accessibility --> + <user-avatar-image :img-src="commit.author.avatarUrl"/> <h5 class="item-title commit-title"> <a :href="commit.commitUrl"> {{ commit.title }} </a> </h5> <span> - {{ __('FirstPushedBy|First') }} + {{ s__('FirstPushedBy|First') }} <span class="commit-icon">${iconCommit}</span> <a :href="commit.commitUrl" class="commit-hash-link commit-sha">{{ commit.shortSha }}</a> - {{ __('FirstPushedBy|pushed by') }} + {{ s__('FirstPushedBy|pushed by') }} <a :href="commit.author.webUrl" class="commit-author-link"> {{ commit.author.name }} </a> diff --git a/app/assets/javascripts/cycle_analytics/components/stage_production_component.js b/app/assets/javascripts/cycle_analytics/components/stage_production_component.js index a14ebc3ece9..b7ba9360f70 100644 --- a/app/assets/javascripts/cycle_analytics/components/stage_production_component.js +++ b/app/assets/javascripts/cycle_analytics/components/stage_production_component.js @@ -1,6 +1,6 @@ /* eslint-disable no-param-reassign */ - import Vue from 'vue'; +import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue'; const global = window.gl || (window.gl = {}); global.cycleAnalytics = global.cycleAnalytics || {}; @@ -10,6 +10,9 @@ global.cycleAnalytics.StageProductionComponent = Vue.extend({ items: Array, stage: Object, }, + components: { + userAvatarImage, + }, template: ` <div> <div class="events-description"> @@ -19,7 +22,8 @@ global.cycleAnalytics.StageProductionComponent = Vue.extend({ <ul class="stage-event-list"> <li v-for="issue in items" class="stage-event-item"> <div class="item-details"> - <img class="avatar" :src="issue.author.avatarUrl"> + <!-- FIXME: Pass an alt attribute here for accessibility --> + <user-avatar-image :img-src="issue.author.avatarUrl"/> <h5 class="item-title issue-title"> <a class="issue-title" :href="issue.url"> {{ issue.title }} @@ -28,11 +32,11 @@ global.cycleAnalytics.StageProductionComponent = Vue.extend({ <a :href="issue.url" class="issue-link">#{{ issue.iid }}</a> · <span> - {{ __('OpenedNDaysAgo|Opened') }} + {{ s__('OpenedNDaysAgo|Opened') }} <a :href="issue.url" class="issue-date">{{ issue.createdAt }}</a> </span> <span> - {{ __('ByAuthor|by') }} + {{ s__('ByAuthor|by') }} <a :href="issue.author.webUrl" class="issue-author-link"> {{ issue.author.name }} </a> diff --git a/app/assets/javascripts/cycle_analytics/components/stage_review_component.js b/app/assets/javascripts/cycle_analytics/components/stage_review_component.js index 1a5bf9bc0b5..f41a0d0e4ff 100644 --- a/app/assets/javascripts/cycle_analytics/components/stage_review_component.js +++ b/app/assets/javascripts/cycle_analytics/components/stage_review_component.js @@ -1,6 +1,6 @@ /* eslint-disable no-param-reassign */ - import Vue from 'vue'; +import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue'; const global = window.gl || (window.gl = {}); global.cycleAnalytics = global.cycleAnalytics || {}; @@ -10,6 +10,9 @@ global.cycleAnalytics.StageReviewComponent = Vue.extend({ items: Array, stage: Object, }, + components: { + userAvatarImage, + }, template: ` <div> <div class="events-description"> @@ -19,7 +22,8 @@ global.cycleAnalytics.StageReviewComponent = Vue.extend({ <ul class="stage-event-list"> <li v-for="mergeRequest in items" class="stage-event-item"> <div class="item-details"> - <img class="avatar" :src="mergeRequest.author.avatarUrl"> + <!-- FIXME: Pass an alt attribute here for accessibility --> + <user-avatar-image :img-src="mergeRequest.author.avatarUrl"/> <h5 class="item-title merge-merquest-title"> <a :href="mergeRequest.url"> {{ mergeRequest.title }} @@ -28,11 +32,11 @@ global.cycleAnalytics.StageReviewComponent = Vue.extend({ <a :href="mergeRequest.url" class="issue-link">!{{ mergeRequest.iid }}</a> · <span> - {{ __('OpenedNDaysAgo|Opened') }} + {{ s__('OpenedNDaysAgo|Opened') }} <a :href="mergeRequest.url" class="issue-date">{{ mergeRequest.createdAt }}</a> </span> <span> - {{ __('ByAuthor|by') }} + {{ s__('ByAuthor|by') }} <a :href="mergeRequest.author.webUrl" class="issue-author-link">{{ mergeRequest.author.name }}</a> </span> <template v-if="mergeRequest.state === 'closed'"> diff --git a/app/assets/javascripts/cycle_analytics/components/stage_staging_component.js b/app/assets/javascripts/cycle_analytics/components/stage_staging_component.js index 1f7c673b1d4..d7c906c9d39 100644 --- a/app/assets/javascripts/cycle_analytics/components/stage_staging_component.js +++ b/app/assets/javascripts/cycle_analytics/components/stage_staging_component.js @@ -1,5 +1,6 @@ /* eslint-disable no-param-reassign */ import Vue from 'vue'; +import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue'; import iconBranch from '../svg/icon_branch.svg'; const global = window.gl || (window.gl = {}); @@ -13,6 +14,9 @@ global.cycleAnalytics.StageStagingComponent = Vue.extend({ data() { return { iconBranch }; }, + components: { + userAvatarImage, + }, template: ` <div> <div class="events-description"> @@ -22,7 +26,8 @@ global.cycleAnalytics.StageStagingComponent = Vue.extend({ <ul class="stage-event-list"> <li v-for="build in items" class="stage-event-item item-build-component"> <div class="item-details"> - <img class="avatar" :src="build.author.avatarUrl"> + <!-- FIXME: Pass an alt attribute here for accessibility --> + <user-avatar-image :img-src="build.author.avatarUrl"/> <h5 class="item-title"> <a :href="build.url" class="pipeline-id">#{{ build.id }}</a> <i class="fa fa-code-fork"></i> @@ -32,7 +37,7 @@ global.cycleAnalytics.StageStagingComponent = Vue.extend({ </h5> <span> <a :href="build.url" class="build-date">{{ build.date }}</a> - {{ __('ByAuthor|by') }} + {{ s__('ByAuthor|by') }} <a :href="build.author.webUrl" class="issue-author-link"> {{ build.author.name }} </a> diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js index c8e53cb554e..44791a93936 100644 --- a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js +++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js @@ -4,18 +4,16 @@ import Vue from 'vue'; import Cookies from 'js-cookie'; import Translate from '../vue_shared/translate'; import LimitWarningComponent from './components/limit_warning_component'; - -require('./components/stage_code_component'); -require('./components/stage_issue_component'); -require('./components/stage_plan_component'); -require('./components/stage_production_component'); -require('./components/stage_review_component'); -require('./components/stage_staging_component'); -require('./components/stage_test_component'); -require('./components/total_time_component'); -require('./cycle_analytics_service'); -require('./cycle_analytics_store'); -require('./default_event_objects'); +import './components/stage_code_component'; +import './components/stage_issue_component'; +import './components/stage_plan_component'; +import './components/stage_production_component'; +import './components/stage_review_component'; +import './components/stage_staging_component'; +import './components/stage_test_component'; +import './components/total_time_component'; +import './cycle_analytics_service'; +import './cycle_analytics_store'; Vue.use(Translate); diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js b/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js index 50bd394e90e..991f8c1f6fd 100644 --- a/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js +++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js @@ -1,8 +1,8 @@ /* eslint-disable no-param-reassign */ -import { __ } from '../locale'; -require('../lib/utils/text_utility'); -const DEFAULT_EVENT_OBJECTS = require('./default_event_objects'); +import { __ } from '../locale'; +import '../lib/utils/text_utility'; +import DEFAULT_EVENT_OBJECTS from './default_event_objects'; const global = window.gl || (window.gl = {}); global.cycleAnalytics = global.cycleAnalytics || {}; diff --git a/app/assets/javascripts/cycle_analytics/default_event_objects.js b/app/assets/javascripts/cycle_analytics/default_event_objects.js index cfaf9835bf8..57f9019d2f8 100644 --- a/app/assets/javascripts/cycle_analytics/default_event_objects.js +++ b/app/assets/javascripts/cycle_analytics/default_event_objects.js @@ -1,4 +1,4 @@ -module.exports = { +export default { issue: { created_at: '', url: '', diff --git a/app/assets/javascripts/diff.js b/app/assets/javascripts/diff.js index 5aa3eb46a69..725ec7b9c70 100644 --- a/app/assets/javascripts/diff.js +++ b/app/assets/javascripts/diff.js @@ -1,6 +1,6 @@ /* eslint-disable class-methods-use-this */ -require('./lib/utils/url_utility'); +import './lib/utils/url_utility'; const UNFOLD_COUNT = 20; let isBound = false; diff --git a/app/assets/javascripts/diff_notes/components/diff_note_avatars.js b/app/assets/javascripts/diff_notes/components/diff_note_avatars.js index f3a688fbf2f..517bdb6be09 100644 --- a/app/assets/javascripts/diff_notes/components/diff_note_avatars.js +++ b/app/assets/javascripts/diff_notes/components/diff_note_avatars.js @@ -3,6 +3,7 @@ import Vue from 'vue'; import collapseIcon from '../icons/collapse_icon.svg'; +import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue'; const DiffNoteAvatars = Vue.extend({ props: ['discussionId'], @@ -15,22 +16,24 @@ const DiffNoteAvatars = Vue.extend({ collapseIcon, }; }, + components: { + userAvatarImage, + }, template: ` <div class="diff-comment-avatar-holders" v-show="notesCount !== 0"> <div v-if="!isVisible"> - <img v-for="note in notesSubset" - class="avatar diff-comment-avatar has-tooltip js-diff-comment-avatar" - width="19" - height="19" - role="button" - data-container="body" - data-placement="top" - data-html="true" + <!-- FIXME: Pass an alt attribute here for accessibility --> + <user-avatar-image + v-for="note in notesSubset" + class="diff-comment-avatar js-diff-comment-avatar" + @click.native="clickedAvatar($event)" + :img-src="note.authorAvatar" + :tooltip-text="getTooltipText(note)" :data-line-type="lineType" - :title="note.authorName + ': ' + note.noteTruncated" - :src="note.authorAvatar" - @click="clickedAvatar($event)" /> + :size="19" + data-html="true" + /> <span v-if="notesCount > shownAvatars" class="diff-comments-more-count has-tooltip js-diff-comment-avatar" data-container="body" @@ -120,7 +123,7 @@ const DiffNoteAvatars = Vue.extend({ }, methods: { clickedAvatar(e) { - notes.addDiffNote(e); + notes.onAddDiffNote(e); // Toggle the active state of the toggle all button this.toggleDiscussionsToggleState(); @@ -150,6 +153,9 @@ const DiffNoteAvatars = Vue.extend({ setDiscussionVisible() { this.isVisible = $(`.diffs .notes[data-discussion-id="${this.discussion.id}"]`).is(':visible'); }, + getTooltipText(note) { + return `${note.authorName}: ${note.noteTruncated}`; + }, }, }); diff --git a/app/assets/javascripts/diff_notes/diff_notes_bundle.js b/app/assets/javascripts/diff_notes/diff_notes_bundle.js index b6b47e2da6f..a2d33b0936e 100644 --- a/app/assets/javascripts/diff_notes/diff_notes_bundle.js +++ b/app/assets/javascripts/diff_notes/diff_notes_bundle.js @@ -2,19 +2,18 @@ /* global ResolveCount */ import Vue from 'vue'; - -require('./models/discussion'); -require('./models/note'); -require('./stores/comments'); -require('./services/resolve'); -require('./mixins/discussion'); -require('./components/comment_resolve_btn'); -require('./components/jump_to_discussion'); -require('./components/resolve_btn'); -require('./components/resolve_count'); -require('./components/resolve_discussion_btn'); -require('./components/diff_note_avatars'); -require('./components/new_issue_for_discussion'); +import './models/discussion'; +import './models/note'; +import './stores/comments'; +import './services/resolve'; +import './mixins/discussion'; +import './components/comment_resolve_btn'; +import './components/jump_to_discussion'; +import './components/resolve_btn'; +import './components/resolve_count'; +import './components/resolve_discussion_btn'; +import './components/diff_note_avatars'; +import './components/new_issue_for_discussion'; $(() => { const projectPath = document.querySelector('.merge-request').dataset.projectPath; @@ -65,4 +64,6 @@ $(() => { 'resolve-count': ResolveCount } }); + + $(window).trigger('resize.nav'); }); diff --git a/app/assets/javascripts/diff_notes/services/resolve.js b/app/assets/javascripts/diff_notes/services/resolve.js index ba4f6d36fcb..807ab11d292 100644 --- a/app/assets/javascripts/diff_notes/services/resolve.js +++ b/app/assets/javascripts/diff_notes/services/resolve.js @@ -3,11 +3,7 @@ /* global CommentsStore */ import Vue from 'vue'; -import VueResource from 'vue-resource'; - -require('../../vue_shared/vue_resource_interceptor'); - -Vue.use(VueResource); +import '../../vue_shared/vue_resource_interceptor'; window.gl = window.gl || {}; diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index 1a791395d6f..2090a7e12d6 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -34,13 +34,13 @@ /* global ShortcutsWiki */ import Issue from './issue'; - import BindInOut from './behaviors/bind_in_out'; import DeleteModal from './branches/branches_delete_modal'; import Group from './group'; import GroupName from './group_name'; import GroupsList from './groups_list'; import ProjectsList from './projects_list'; +import setupProjectEdit from './project_edit'; import MiniPipelineGraph from './mini_pipeline_graph_dropdown'; import BlobLinePermalinkUpdater from './blob/blob_line_permalink_updater'; import Landing from './landing'; @@ -52,8 +52,9 @@ import Pipelines from './pipelines'; import BlobViewer from './blob/viewer/index'; import AutoWidthDropdownSelect from './issuable/auto_width_dropdown_select'; import UsersSelect from './users_select'; - -const ShortcutsBlob = require('./shortcuts_blob'); +import RefSelectDropdown from './ref_select_dropdown'; +import GfmAutoComplete from './gfm_auto_complete'; +import ShortcutsBlob from './shortcuts_blob'; (function() { var Dispatcher; @@ -78,6 +79,8 @@ const ShortcutsBlob = require('./shortcuts_blob'); path = page.split(':'); shortcut_handler = null; + new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources).setup(); + function initBlob() { new LineHighlighter(); @@ -212,6 +215,7 @@ const ShortcutsBlob = require('./shortcuts_blob'); case 'projects:tags:new': new ZenMode(); new gl.GLForm($('.tag-form')); + new RefSelectDropdown($('.js-branch-select'), window.gl.availableRefs); break; case 'projects:releases:edit': new ZenMode(); @@ -256,9 +260,14 @@ const ShortcutsBlob = require('./shortcuts_blob'); new NotificationsForm(); if ($('#tree-slider').length) { new TreeView(); + } + if ($('.blob-viewer').length) { new BlobViewer(); } break; + case 'projects:edit': + setupProjectEdit(); + break; case 'projects:pipelines:builds': case 'projects:pipelines:failures': case 'projects:pipelines:show': diff --git a/app/assets/javascripts/droplab/plugins/ajax.js b/app/assets/javascripts/droplab/plugins/ajax.js index 12afe53ed76..c0da5866139 100644 --- a/app/assets/javascripts/droplab/plugins/ajax.js +++ b/app/assets/javascripts/droplab/plugins/ajax.js @@ -1,25 +1,8 @@ /* eslint-disable */ +import AjaxCache from '~/lib/utils/ajax_cache'; + const Ajax = { - _loadUrlData: function _loadUrlData(url) { - var self = this; - return new Promise(function(resolve, reject) { - var xhr = new XMLHttpRequest; - xhr.open('GET', url, true); - xhr.onreadystatechange = function () { - if(xhr.readyState === XMLHttpRequest.DONE) { - if (xhr.status === 200) { - var data = JSON.parse(xhr.responseText); - self.cache[url] = data; - return resolve(data); - } else { - return reject([xhr.responseText, xhr.status]); - } - } - }; - xhr.send(); - }); - }, _loadData: function _loadData(data, config, self) { if (config.loadingTemplate) { var dataLoadingTemplate = self.hook.list.list.querySelector('[data-loading-template]'); @@ -31,7 +14,6 @@ const Ajax = { init: function init(hook) { var self = this; self.destroyed = false; - self.cache = self.cache || {}; var config = hook.config.Ajax; this.hook = hook; if (!config || !config.endpoint || !config.method) { @@ -48,14 +30,10 @@ const Ajax = { this.listTemplate = dynamicList.outerHTML; dynamicList.outerHTML = loadingTemplate.outerHTML; } - if (self.cache[config.endpoint]) { - self._loadData(self.cache[config.endpoint], config, self); - } else { - this._loadUrlData(config.endpoint) - .then(function(d) { - self._loadData(d, config, self); - }, config.onError).catch(config.onError); - } + + AjaxCache.retrieve(config.endpoint) + .then((data) => self._loadData(data, config, self)) + .catch(config.onError); }, destroy: function() { this.destroyed = true; diff --git a/app/assets/javascripts/droplab/plugins/ajax_filter.js b/app/assets/javascripts/droplab/plugins/ajax_filter.js index cfd7e2ca189..a5427417031 100644 --- a/app/assets/javascripts/droplab/plugins/ajax_filter.js +++ b/app/assets/javascripts/droplab/plugins/ajax_filter.js @@ -1,4 +1,5 @@ /* eslint-disable */ +import AjaxCache from '../../lib/utils/ajax_cache'; const AjaxFilter = { init: function(hook) { @@ -58,50 +59,24 @@ const AjaxFilter = { this.loading = true; var params = config.params || {}; params[config.searchKey] = searchValue; - var self = this; - self.cache = self.cache || {}; var url = config.endpoint + this.buildParams(params); - var urlCachedData = self.cache[url]; - if (urlCachedData) { - self._loadData(urlCachedData, config, self); - } else { - this._loadUrlData(url) - .then(function(data) { - self._loadData(data, config, self); - }, config.onError).catch(config.onError); - } + return AjaxCache.retrieve(url) + .then((data) => { + this._loadData(data, config); + }) + .catch(config.onError); }, - _loadUrlData: function _loadUrlData(url) { - var self = this; - return new Promise(function(resolve, reject) { - var xhr = new XMLHttpRequest; - xhr.open('GET', url, true); - xhr.onreadystatechange = function () { - if(xhr.readyState === XMLHttpRequest.DONE) { - if (xhr.status === 200) { - var data = JSON.parse(xhr.responseText); - self.cache[url] = data; - return resolve(data); - } else { - return reject([xhr.responseText, xhr.status]); - } - } - }; - xhr.send(); - }); - }, - - _loadData: function _loadData(data, config, self) { - const list = self.hook.list; + _loadData(data, config) { + const list = this.hook.list; if (config.loadingTemplate && list.data === undefined || list.data.length === 0) { const dataLoadingTemplate = list.list.querySelector('[data-loading-template]'); if (dataLoadingTemplate) { - dataLoadingTemplate.outerHTML = self.listTemplate; + dataLoadingTemplate.outerHTML = this.listTemplate; } } - if (!self.destroyed) { + if (!this.destroyed) { var hookListChildren = list.list.children; var onlyDynamicList = hookListChildren.length === 1 && hookListChildren[0].hasAttribute('data-dynamic'); if (onlyDynamicList && data.length === 0) { @@ -109,7 +84,7 @@ const AjaxFilter = { } list.setData.call(list, data); } - self.notLoading(); + this.notLoading(); list.currentIndex = 0; }, diff --git a/app/assets/javascripts/dropzone_input.js b/app/assets/javascripts/dropzone_input.js index 3843539a3b8..111449bb8f7 100644 --- a/app/assets/javascripts/dropzone_input.js +++ b/app/assets/javascripts/dropzone_input.js @@ -1,108 +1,158 @@ /* eslint-disable func-names, space-before-function-paren, wrap-iife, max-len, one-var, no-var, one-var-declaration-per-line, no-unused-vars, camelcase, quotes, no-useless-concat, prefer-template, quote-props, comma-dangle, object-shorthand, consistent-return, prefer-arrow-callback */ /* global Dropzone */ -require('./preview_markdown'); +import './preview_markdown'; window.DropzoneInput = (function() { function DropzoneInput(form) { - var $mdArea, alertAttr, alertClass, appendToTextArea, btnAlert, child, closeAlertMessage, closeSpinner, divAlert, divHover, divSpinner, dropzone, form_dropzone, form_textarea, getFilename, handlePaste, iconPaperclip, iconSpinner, insertToTextArea, isImage, max_file_size, pasteText, uploads_path, showError, showSpinner, uploadFile, uploadProgress; + var updateAttachingMessage, $attachingFileMessage, $mdArea, $attachButton, $cancelButton, $retryLink, $uploadingErrorContainer, $uploadingErrorMessage, $uploadProgress, $uploadingProgressContainer, appendToTextArea, btnAlert, child, closeAlertMessage, closeSpinner, divHover, divSpinner, dropzone, $formDropzone, formTextarea, getFilename, handlePaste, iconPaperclip, iconSpinner, insertToTextArea, isImage, maxFileSize, pasteText, uploadsPath, showError, showSpinner, uploadFile; Dropzone.autoDiscover = false; - alertClass = "alert alert-danger alert-dismissable div-dropzone-alert"; - alertAttr = "class=\"close\" data-dismiss=\"alert\"" + "aria-hidden=\"true\""; - divHover = "<div class=\"div-dropzone-hover\"></div>"; - divSpinner = "<div class=\"div-dropzone-spinner\"></div>"; - divAlert = "<div class=\"" + alertClass + "\"></div>"; - iconPaperclip = "<i class=\"fa fa-paperclip div-dropzone-icon\"></i>"; - iconSpinner = "<i class=\"fa fa-spinner fa-spin div-dropzone-icon\"></i>"; - uploadProgress = $("<div class=\"div-dropzone-progress\"></div>"); - btnAlert = "<button type=\"button\"" + alertAttr + ">×</button>"; - uploads_path = window.uploads_path || null; - max_file_size = gon.max_file_size || 10; - form_textarea = $(form).find(".js-gfm-input"); - form_textarea.wrap("<div class=\"div-dropzone\"></div>"); - form_textarea.on('paste', (function(_this) { + divHover = '<div class="div-dropzone-hover"></div>'; + iconPaperclip = '<i class="fa fa-paperclip div-dropzone-icon"></i>'; + $attachButton = form.find('.button-attach-file'); + $attachingFileMessage = form.find('.attaching-file-message'); + $cancelButton = form.find('.button-cancel-uploading-files'); + $retryLink = form.find('.retry-uploading-link'); + $uploadProgress = form.find('.uploading-progress'); + $uploadingErrorContainer = form.find('.uploading-error-container'); + $uploadingErrorMessage = form.find('.uploading-error-message'); + $uploadingProgressContainer = form.find('.uploading-progress-container'); + uploadsPath = window.uploads_path || null; + maxFileSize = gon.max_file_size || 10; + formTextarea = form.find('.js-gfm-input'); + formTextarea.wrap('<div class="div-dropzone"></div>'); + formTextarea.on('paste', (function(_this) { return function(event) { return handlePaste(event); }; })(this)); - $mdArea = $(form_textarea).closest('.md-area'); - $(form).setupMarkdownPreview(); - form_dropzone = $(form).find('.div-dropzone'); - form_dropzone.parent().addClass("div-dropzone-wrapper"); - form_dropzone.append(divHover); - form_dropzone.find(".div-dropzone-hover").append(iconPaperclip); - form_dropzone.append(divSpinner); - form_dropzone.find(".div-dropzone-spinner").append(iconSpinner); - form_dropzone.find(".div-dropzone-spinner").append(uploadProgress); - form_dropzone.find(".div-dropzone-spinner").css({ - "opacity": 0, - "display": "none" - }); - if (!uploads_path) return; + // Add dropzone area to the form. + $mdArea = formTextarea.closest('.md-area'); + form.setupMarkdownPreview(); + $formDropzone = form.find('.div-dropzone'); + $formDropzone.parent().addClass('div-dropzone-wrapper'); + $formDropzone.append(divHover); + $formDropzone.find('.div-dropzone-hover').append(iconPaperclip); + + if (!uploadsPath) return; - dropzone = form_dropzone.dropzone({ - url: uploads_path, - dictDefaultMessage: "", + dropzone = $formDropzone.dropzone({ + url: uploadsPath, + dictDefaultMessage: '', clickable: true, - paramName: "file", - maxFilesize: max_file_size, + paramName: 'file', + maxFilesize: maxFileSize, uploadMultiple: false, headers: { - "X-CSRF-Token": $("meta[name=\"csrf-token\"]").attr("content") + 'X-CSRF-Token': $('meta[name="csrf-token"]').attr('content') }, previewContainer: false, processing: function() { - return $(".div-dropzone-alert").alert("close"); + return $('.div-dropzone-alert').alert('close'); }, dragover: function() { $mdArea.addClass('is-dropzone-hover'); - form.find(".div-dropzone-hover").css("opacity", 0.7); + form.find('.div-dropzone-hover').css('opacity', 0.7); }, dragleave: function() { $mdArea.removeClass('is-dropzone-hover'); - form.find(".div-dropzone-hover").css("opacity", 0); + form.find('.div-dropzone-hover').css('opacity', 0); }, drop: function() { $mdArea.removeClass('is-dropzone-hover'); - form.find(".div-dropzone-hover").css("opacity", 0); - form_textarea.focus(); + form.find('.div-dropzone-hover').css('opacity', 0); + formTextarea.focus(); }, success: function(header, response) { const processingFileCount = this.getQueuedFiles().length + this.getUploadingFiles().length; const shouldPad = processingFileCount >= 1; pasteText(response.link.markdown, shouldPad); + // Show 'Attach a file' link only when all files have been uploaded. + if (!processingFileCount) $attachButton.removeClass('hide'); }, - error: function(temp) { - var checkIfMsgExists, errorAlert; - errorAlert = $(form).find('.error-alert'); - checkIfMsgExists = errorAlert.children().length; - if (checkIfMsgExists === 0) { - errorAlert.append(divAlert); - $(".div-dropzone-alert").append(btnAlert + "Attaching the file failed."); - } + error: function(file, errorMessage = 'Attaching the file failed.', xhr) { + // If 'error' event is fired by dropzone, the second parameter is error message. + // If the 'errorMessage' parameter is empty, the default error message is set. + // If the 'error' event is fired by backend (xhr) error response, the third parameter is + // xhr object (xhr.responseText is error message). + // On error we hide the 'Attach' and 'Cancel' buttons + // and show an error. + + // If there's xhr error message, let's show it instead of dropzone's one. + const message = xhr ? xhr.responseText : errorMessage; + + $uploadingErrorContainer.removeClass('hide'); + $uploadingErrorMessage.html(message); + $attachButton.addClass('hide'); + $cancelButton.addClass('hide'); }, totaluploadprogress: function(totalUploadProgress) { - uploadProgress.text(Math.round(totalUploadProgress) + "%"); + updateAttachingMessage(this.files, $attachingFileMessage); + $uploadProgress.text(Math.round(totalUploadProgress) + '%'); + }, + sending: function(file) { + // DOM elements already exist. + // Instead of dynamically generating them, + // we just either hide or show them. + $attachButton.addClass('hide'); + $uploadingErrorContainer.addClass('hide'); + $uploadingProgressContainer.removeClass('hide'); + $cancelButton.removeClass('hide'); }, - sending: function() { - form_dropzone.find(".div-dropzone-spinner").css({ - "opacity": 0.7, - "display": "inherit" - }); + removedfile: function() { + $attachButton.removeClass('hide'); + $cancelButton.addClass('hide'); + $uploadingProgressContainer.addClass('hide'); + $uploadingErrorContainer.addClass('hide'); }, queuecomplete: function() { - uploadProgress.text(""); - $(".dz-preview").remove(); - $(".markdown-area").trigger("input"); - $(".div-dropzone-spinner").css({ - "opacity": 0, - "display": "none" - }); + $('.dz-preview').remove(); + $('.markdown-area').trigger('input'); + + $uploadingProgressContainer.addClass('hide'); + $cancelButton.addClass('hide'); } }); - child = $(dropzone[0]).children("textarea"); + + child = $(dropzone[0]).children('textarea'); + + // removeAllFiles(true) stops uploading files (if any) + // and remove them from dropzone files queue. + $cancelButton.on('click', (e) => { + const target = e.target.closest('form').querySelector('.div-dropzone'); + + e.preventDefault(); + e.stopPropagation(); + Dropzone.forElement(target).removeAllFiles(true); + }); + + // If 'error' event is fired, we store a failed files, + // clear dropzone files queue, change status of failed files to undefined, + // and add that files to the dropzone files queue again. + // addFile() adds file to dropzone files queue and upload it. + $retryLink.on('click', (e) => { + const dropzoneInstance = Dropzone.forElement(e.target.closest('form').querySelector('.div-dropzone')); + const failedFiles = dropzoneInstance.files; + + e.preventDefault(); + + // 'true' parameter of removeAllFiles() cancels uploading of files that are being uploaded at the moment. + dropzoneInstance.removeAllFiles(true); + + failedFiles.map((failedFile, i) => { + const file = failedFile; + + if (file.status === Dropzone.ERROR) { + file.status = undefined; + file.accepted = undefined; + } + + return dropzoneInstance.addFile(file); + }); + }); + handlePaste = function(event) { var filename, image, pasteEvent, text; pasteEvent = event.originalEvent; @@ -110,25 +160,27 @@ window.DropzoneInput = (function() { image = isImage(pasteEvent); if (image) { event.preventDefault(); - filename = getFilename(pasteEvent) || "image.png"; - text = "{{" + filename + "}}"; + filename = getFilename(pasteEvent) || 'image.png'; + text = `{{${filename}}}`; pasteText(text); return uploadFile(image.getAsFile(), filename); } } }; + isImage = function(data) { var i, item; i = 0; while (i < data.clipboardData.items.length) { item = data.clipboardData.items[i]; - if (item.type.indexOf("image") !== -1) { + if (item.type.indexOf('image') !== -1) { return item; } i += 1; } return false; }; + pasteText = function(text, shouldPad) { var afterSelection, beforeSelection, caretEnd, caretStart, textEnd; var formattedText = text; @@ -142,32 +194,34 @@ window.DropzoneInput = (function() { $(child).val(beforeSelection + formattedText + afterSelection); textarea.setSelectionRange(caretStart + formattedText.length, caretEnd + formattedText.length); textarea.style.height = `${textarea.scrollHeight}px`; - form_textarea.trigger("input"); - form_textarea.get(0).dispatchEvent(new Event('input')); + formTextarea.get(0).dispatchEvent(new Event('input')); + return formTextarea.trigger('input'); }; + getFilename = function(e) { var value; if (window.clipboardData && window.clipboardData.getData) { - value = window.clipboardData.getData("Text"); + value = window.clipboardData.getData('Text'); } else if (e.clipboardData && e.clipboardData.getData) { - value = e.clipboardData.getData("text/plain"); + value = e.clipboardData.getData('text/plain'); } value = value.split("\r"); return value.first(); }; + uploadFile = function(item, filename) { var formData; formData = new FormData(); - formData.append("file", item, filename); + formData.append('file', item, filename); return $.ajax({ - url: uploads_path, - type: "POST", + url: uploadsPath, + type: 'POST', data: formData, - dataType: "json", + dataType: 'json', processData: false, contentType: false, headers: { - "X-CSRF-Token": $("meta[name=\"csrf-token\"]").attr("content") + 'X-CSRF-Token': $('meta[name="csrf-token"]').attr('content') }, beforeSend: function() { showSpinner(); @@ -184,44 +238,54 @@ window.DropzoneInput = (function() { } }); }; + + updateAttachingMessage = (files, messageContainer) => { + let attachingMessage; + const filesCount = files.filter(function(file) { + return file.status === 'uploading' || + file.status === 'queued'; + }).length; + + // Dinamycally change uploading files text depending on files number in + // dropzone files queue. + if (filesCount > 1) { + attachingMessage = 'Attaching ' + filesCount + ' files -'; + } else { + attachingMessage = 'Attaching a file -'; + } + + messageContainer.text(attachingMessage); + }; + insertToTextArea = function(filename, url) { return $(child).val(function(index, val) { - return val.replace("{{" + filename + "}}", url); + return val.replace(`{{${filename}}}`, url); }); }; + appendToTextArea = function(url) { return $(child).val(function(index, val) { return val + url + "\n"; }); }; + showSpinner = function(e) { - return form.find(".div-dropzone-spinner").css({ - "opacity": 0.7, - "display": "inherit" - }); + return $uploadingProgressContainer.removeClass('hide'); }; + closeSpinner = function() { - return form.find(".div-dropzone-spinner").css({ - "opacity": 0, - "display": "none" - }); + return $uploadingProgressContainer.addClass('hide'); }; + showError = function(message) { - var checkIfMsgExists, errorAlert; - errorAlert = $(form).find('.error-alert'); - checkIfMsgExists = errorAlert.children().length; - if (checkIfMsgExists === 0) { - errorAlert.append(divAlert); - return $(".div-dropzone-alert").append(btnAlert + message); - } + $uploadingErrorContainer.removeClass('hide'); + $uploadingErrorMessage.html(message); }; - closeAlertMessage = function() { - return form.find(".div-dropzone-alert").alert("close"); - }; - form.find(".markdown-selector").click(function(e) { + + form.find('.markdown-selector').click(function(e) { e.preventDefault(); $(this).closest('.gfm-form').find('.div-dropzone').click(); - form_textarea.focus(); + formTextarea.focus(); }); } diff --git a/app/assets/javascripts/environments/components/environment_item.vue b/app/assets/javascripts/environments/components/environment_item.vue index 1f01629aa1b..012ff1f975b 100644 --- a/app/assets/javascripts/environments/components/environment_item.vue +++ b/app/assets/javascripts/environments/components/environment_item.vue @@ -1,6 +1,7 @@ <script> import Timeago from 'timeago.js'; import _ from 'underscore'; +import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; import '../../lib/utils/text_utility'; import ActionsComponent from './environment_actions.vue'; import ExternalUrlComponent from './environment_external_url.vue'; @@ -20,6 +21,7 @@ const timeagoInstance = new Timeago(); export default { components: { + userAvatarLink, 'commit-component': CommitComponent, 'actions-component': ActionsComponent, 'external-url-component': ExternalUrlComponent, @@ -468,15 +470,13 @@ export default { <span v-if="!model.isFolder && deploymentHasUser"> by - <a - :href="deploymentUser.web_url" - class="js-deploy-user-container"> - <img - class="avatar has-tooltip s20" - :src="deploymentUser.avatar_url" - :alt="userImageAltDescription" - :title="deploymentUser.username" /> - </a> + <user-avatar-link + class="js-deploy-user-container" + :link-href="deploymentUser.web_url" + :img-src="deploymentUser.avatar_url" + :img-alt="userImageAltDescription" + :tooltip-text="deploymentUser.username" + /> </span> </td> diff --git a/app/assets/javascripts/environments/components/environment_monitoring.vue b/app/assets/javascripts/environments/components/environment_monitoring.vue index 4b030a27900..79c019b3491 100644 --- a/app/assets/javascripts/environments/components/environment_monitoring.vue +++ b/app/assets/javascripts/environments/components/environment_monitoring.vue @@ -21,7 +21,6 @@ export default { <a class="btn monitoring-url has-tooltip" data-container="body" - target="_blank" rel="noopener noreferrer nofollow" :href="monitoringUrl" :title="title" diff --git a/app/assets/javascripts/filtered_search/components/recent_searches_dropdown_content.js b/app/assets/javascripts/filtered_search/components/recent_searches_dropdown_content.js index 15052dbd362..c51d4b056af 100644 --- a/app/assets/javascripts/filtered_search/components/recent_searches_dropdown_content.js +++ b/app/assets/javascripts/filtered_search/components/recent_searches_dropdown_content.js @@ -13,13 +13,17 @@ export default { required: false, default: true, }, + allowedKeys: { + type: Array, + required: true, + }, }, computed: { processedItems() { return this.items.map((item) => { const { tokens, searchToken } - = gl.FilteredSearchTokenizer.processTokens(item); + = gl.FilteredSearchTokenizer.processTokens(item, this.allowedKeys); const resultantTokens = tokens.map(token => ({ prefix: `${token.key}:`, diff --git a/app/assets/javascripts/filtered_search/dropdown_hint.js b/app/assets/javascripts/filtered_search/dropdown_hint.js index 5e9434fd48f..2af242a69df 100644 --- a/app/assets/javascripts/filtered_search/dropdown_hint.js +++ b/app/assets/javascripts/filtered_search/dropdown_hint.js @@ -1,16 +1,19 @@ import Filter from '~/droplab/plugins/filter'; - -require('./filtered_search_dropdown'); +import './filtered_search_dropdown'; class DropdownHint extends gl.FilteredSearchDropdown { - constructor(droplab, dropdown, input, filter) { + constructor(droplab, dropdown, input, tokenKeys, filter) { super(droplab, dropdown, input, filter); this.config = { Filter: { template: 'hint', - filterFunction: gl.DropdownUtils.filterHint.bind(null, input), + filterFunction: gl.DropdownUtils.filterHint.bind(null, { + input, + allowedKeys: tokenKeys.getKeys(), + }), }, }; + this.tokenKeys = tokenKeys; } itemClicked(e) { @@ -53,20 +56,13 @@ class DropdownHint extends gl.FilteredSearchDropdown { } renderContent() { - const dropdownData = []; - - [].forEach.call(this.input.closest('.filtered-search-box-input-container').querySelectorAll('.dropdown-menu'), (dropdownMenu) => { - const { icon, hint, tag, type } = dropdownMenu.dataset; - if (icon && hint && tag) { - dropdownData.push( - Object.assign({ - icon: `fa-${icon}`, - hint, - tag: `<${tag}>`, - }, type && { type }), - ); - } - }); + const dropdownData = gl.FilteredSearchTokenKeys.get() + .map(tokenKey => ({ + icon: `fa-${tokenKey.icon}`, + hint: tokenKey.key, + tag: `<${tokenKey.symbol}${tokenKey.key}>`, + type: tokenKey.type, + })); this.droplab.changeHookList(this.hookId, this.dropdown, [Filter], this.config); this.droplab.setData(this.hookId, dropdownData); diff --git a/app/assets/javascripts/filtered_search/dropdown_non_user.js b/app/assets/javascripts/filtered_search/dropdown_non_user.js index 982dc4b61be..34a9e34070c 100644 --- a/app/assets/javascripts/filtered_search/dropdown_non_user.js +++ b/app/assets/javascripts/filtered_search/dropdown_non_user.js @@ -2,11 +2,10 @@ import Ajax from '~/droplab/plugins/ajax'; import Filter from '~/droplab/plugins/filter'; - -require('./filtered_search_dropdown'); +import './filtered_search_dropdown'; class DropdownNonUser extends gl.FilteredSearchDropdown { - constructor(droplab, dropdown, input, filter, endpoint, symbol) { + constructor(droplab, dropdown, input, tokenKeys, filter, endpoint, symbol) { super(droplab, dropdown, input, filter); this.symbol = symbol; this.config = { diff --git a/app/assets/javascripts/filtered_search/dropdown_user.js b/app/assets/javascripts/filtered_search/dropdown_user.js index 74cec3d75fe..6b4338ca1d6 100644 --- a/app/assets/javascripts/filtered_search/dropdown_user.js +++ b/app/assets/javascripts/filtered_search/dropdown_user.js @@ -1,11 +1,10 @@ /* global Flash */ import AjaxFilter from '~/droplab/plugins/ajax_filter'; - -require('./filtered_search_dropdown'); +import './filtered_search_dropdown'; class DropdownUser extends gl.FilteredSearchDropdown { - constructor(droplab, dropdown, input, filter) { + constructor(droplab, dropdown, input, tokenKeys, filter) { super(droplab, dropdown, input, filter); this.config = { AjaxFilter: { @@ -26,6 +25,7 @@ class DropdownUser extends gl.FilteredSearchDropdown { }, }, }; + this.tokenKeys = tokenKeys; } itemClicked(e) { @@ -44,7 +44,7 @@ class DropdownUser extends gl.FilteredSearchDropdown { getSearchInput() { const query = gl.DropdownUtils.getSearchInput(this.input); - const { lastToken } = gl.FilteredSearchTokenizer.processTokens(query); + const { lastToken } = gl.FilteredSearchTokenizer.processTokens(query, this.tokenKeys.get()); let value = lastToken || ''; diff --git a/app/assets/javascripts/filtered_search/dropdown_utils.js b/app/assets/javascripts/filtered_search/dropdown_utils.js index bc7c1dffece..5c02a7a53d3 100644 --- a/app/assets/javascripts/filtered_search/dropdown_utils.js +++ b/app/assets/javascripts/filtered_search/dropdown_utils.js @@ -50,10 +50,12 @@ class DropdownUtils { return updatedItem; } - static filterHint(input, item) { + static filterHint(config, item) { + const { input, allowedKeys } = config; const updatedItem = item; const searchInput = gl.DropdownUtils.getSearchQuery(input); - const { lastToken, tokens } = gl.FilteredSearchTokenizer.processTokens(searchInput); + const { lastToken, tokens } = + gl.FilteredSearchTokenizer.processTokens(searchInput, allowedKeys); const lastKey = lastToken.key || lastToken || ''; const allowMultiple = item.type === 'array'; const itemInExistingTokens = tokens.some(t => t.key === item.hint); diff --git a/app/assets/javascripts/filtered_search/filtered_search_bundle.js b/app/assets/javascripts/filtered_search/filtered_search_bundle.js index 856eb6590ee..5d48b8aacb2 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_bundle.js +++ b/app/assets/javascripts/filtered_search/filtered_search_bundle.js @@ -1,10 +1,10 @@ -require('./dropdown_hint'); -require('./dropdown_non_user'); -require('./dropdown_user'); -require('./dropdown_utils'); -require('./filtered_search_dropdown_manager'); -require('./filtered_search_dropdown'); -require('./filtered_search_manager'); -require('./filtered_search_token_keys'); -require('./filtered_search_tokenizer'); -require('./filtered_search_visual_tokens'); +import './dropdown_hint'; +import './dropdown_non_user'; +import './dropdown_user'; +import './dropdown_utils'; +import './filtered_search_dropdown_manager'; +import './filtered_search_dropdown'; +import './filtered_search_manager'; +import './filtered_search_token_keys'; +import './filtered_search_tokenizer'; +import './filtered_search_visual_tokens'; 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 49a6cd1ac77..6bc6bc43f51 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js @@ -2,10 +2,10 @@ import DropLab from '~/droplab/drop_lab'; import FilteredSearchContainer from './container'; class FilteredSearchDropdownManager { - constructor(baseEndpoint = '', page) { + constructor(baseEndpoint = '', tokenizer, page) { this.container = FilteredSearchContainer.container; this.baseEndpoint = baseEndpoint.replace(/\/$/, ''); - this.tokenizer = gl.FilteredSearchTokenizer; + this.tokenizer = tokenizer; this.filteredSearchTokenKeys = gl.FilteredSearchTokenKeys; this.filteredSearchInput = this.container.querySelector('.filtered-search'); this.page = page; @@ -98,7 +98,8 @@ class FilteredSearchDropdownManager { if (!mappingKey.reference) { const dl = this.droplab; - const defaultArguments = [null, dl, element, this.filteredSearchInput, key]; + const defaultArguments = + [null, dl, element, this.filteredSearchInput, this.filteredSearchTokenKeys, key]; const glArguments = defaultArguments.concat(mappingKey.extraArguments || []); // Passing glArguments to `new gl[glClass](<arguments>)` @@ -141,7 +142,8 @@ class FilteredSearchDropdownManager { setDropdown() { const query = gl.DropdownUtils.getSearchQuery(true); - const { lastToken, searchToken } = this.tokenizer.processTokens(query); + const { lastToken, searchToken } = + this.tokenizer.processTokens(query, this.filteredSearchTokenKeys.getKeys()); if (this.currentDropdown) { this.updateCurrentDropdownOffset(); diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js index 57d247e11a9..58f2b75bd50 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js @@ -15,6 +15,7 @@ class FilteredSearchManager { this.recentSearchesStore = new RecentSearchesStore({ isLocalStorageAvailable: RecentSearchesService.isAvailable(), + allowedKeys: this.filteredSearchTokenKeys.getKeys(), }); const searchHistoryDropdownElement = document.querySelector('.js-filtered-search-history-dropdown'); const projectPath = searchHistoryDropdownElement ? @@ -46,7 +47,7 @@ class FilteredSearchManager { if (this.filteredSearchInput) { this.tokenizer = gl.FilteredSearchTokenizer; - this.dropdownManager = new gl.FilteredSearchDropdownManager(this.filteredSearchInput.getAttribute('data-base-endpoint') || '', page); + this.dropdownManager = new gl.FilteredSearchDropdownManager(this.filteredSearchInput.getAttribute('data-base-endpoint') || '', this.tokenizer, page); this.recentSearchesRoot = new RecentSearchesRoot( this.recentSearchesStore, @@ -318,7 +319,7 @@ class FilteredSearchManager { handleInputVisualToken() { const input = this.filteredSearchInput; const { tokens, searchToken } - = gl.FilteredSearchTokenizer.processTokens(input.value); + = this.tokenizer.processTokens(input.value, this.filteredSearchTokenKeys.getKeys()); const { isLastVisualTokenValid } = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput(); @@ -444,7 +445,7 @@ class FilteredSearchManager { this.saveCurrentSearchQuery(); const { tokens, searchToken } - = this.tokenizer.processTokens(searchQuery); + = this.tokenizer.processTokens(searchQuery, this.filteredSearchTokenKeys.getKeys()); const currentState = gl.utils.getParameterByName('state') || 'opened'; paths.push(`state=${currentState}`); diff --git a/app/assets/javascripts/filtered_search/filtered_search_token_keys.js b/app/assets/javascripts/filtered_search/filtered_search_token_keys.js index 1abad9d1b73..025d4d8795b 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_token_keys.js +++ b/app/assets/javascripts/filtered_search/filtered_search_token_keys.js @@ -3,21 +3,25 @@ const tokenKeys = [{ type: 'string', param: 'username', symbol: '@', + icon: 'pencil', }, { key: 'assignee', type: 'string', param: 'username', symbol: '@', + icon: 'user', }, { key: 'milestone', type: 'string', param: 'title', symbol: '%', + icon: 'clock-o', }, { key: 'label', type: 'array', param: 'name[]', symbol: '~', + icon: 'tag', }]; const alternativeTokenKeys = [{ @@ -56,6 +60,10 @@ class FilteredSearchTokenKeys { return tokenKeys; } + static getKeys() { + return tokenKeys.map(i => i.key); + } + static getAlternatives() { return alternativeTokenKeys; } diff --git a/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js index 2808e4b238a..f2e66503e5e 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js +++ b/app/assets/javascripts/filtered_search/filtered_search_tokenizer.js @@ -1,8 +1,7 @@ -require('./filtered_search_token_keys'); +import './filtered_search_token_keys'; class FilteredSearchTokenizer { - static processTokens(input) { - const allowedKeys = gl.FilteredSearchTokenKeys.get().map(i => i.key); + static processTokens(input, allowedKeys) { // Regex extracts `(token):(symbol)(value)` // Values that start with a double quote must end in a double quote (same for single) const tokenRegex = new RegExp(`(${allowedKeys.join('|')}):([~%@]?)(?:('[^']*'{0,1})|("[^"]*"{0,1})|(\\S+))`, 'g'); diff --git a/app/assets/javascripts/filtered_search/recent_searches_root.js b/app/assets/javascripts/filtered_search/recent_searches_root.js index b2e6f63aacf..27e49d4fb96 100644 --- a/app/assets/javascripts/filtered_search/recent_searches_root.js +++ b/app/assets/javascripts/filtered_search/recent_searches_root.js @@ -37,6 +37,7 @@ class RecentSearchesRoot { <recent-searches-dropdown-content :items="recentSearches" :is-local-storage-available="isLocalStorageAvailable" + :allowed-keys="allowedKeys" /> `, components: { diff --git a/app/assets/javascripts/filtered_search/stores/recent_searches_store.js b/app/assets/javascripts/filtered_search/stores/recent_searches_store.js index 35fc15e4c87..aaa0c349d93 100644 --- a/app/assets/javascripts/filtered_search/stores/recent_searches_store.js +++ b/app/assets/javascripts/filtered_search/stores/recent_searches_store.js @@ -1,10 +1,11 @@ import _ from 'underscore'; class RecentSearchesStore { - constructor(initialState = {}) { + constructor(initialState = {}, allowedKeys) { this.state = Object.assign({ isLocalStorageAvailable: true, recentSearches: [], + allowedKeys, }, initialState); } diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js index f1b99023c72..b8a923cf619 100644 --- a/app/assets/javascripts/gfm_auto_complete.js +++ b/app/assets/javascripts/gfm_auto_complete.js @@ -1,119 +1,33 @@ -/* eslint-disable func-names, space-before-function-paren, no-template-curly-in-string, comma-dangle, object-shorthand, quotes, dot-notation, no-else-return, one-var, no-var, no-underscore-dangle, one-var-declaration-per-line, no-param-reassign, no-useless-escape, prefer-template, consistent-return, wrap-iife, prefer-arrow-callback, camelcase, no-unused-vars, no-useless-return, vars-on-top, max-len */ - import emojiMap from 'emojis/digests.json'; import emojiAliases from 'emojis/aliases.json'; import { glEmojiTag } from '~/behaviors/gl_emoji'; import glRegexp from '~/lib/utils/regexp'; -// Creates the variables for setting up GFM auto-completion -window.gl = window.gl || {}; - function sanitize(str) { return str.replace(/<(?:.|\n)*?>/gm, ''); } -window.gl.GfmAutoComplete = { - dataSources: {}, - defaultLoadingData: ['loading'], - cachedData: {}, - isLoadingData: {}, - atTypeMap: { - ':': 'emojis', - '@': 'members', - '#': 'issues', - '!': 'mergeRequests', - '~': 'labels', - '%': 'milestones', - '/': 'commands' - }, - // Emoji - Emoji: { - templateFunction: function(name) { - return `<li> - ${name} ${glEmojiTag(name)} - </li> - `; - } - }, - // Team Members - Members: { - template: '<li>${avatarTag} ${username} <small>${title}</small></li>' - }, - Labels: { - template: '<li><span class="dropdown-label-box" style="background: ${color}"></span> ${title}</li>' - }, - // Issues and MergeRequests - Issues: { - template: '<li><small>${id}</small> ${title}</li>' - }, - // Milestones - Milestones: { - template: '<li>${title}</li>' - }, - Loading: { - template: '<li style="pointer-events: none;"><i class="fa fa-spinner fa-spin"></i> Loading...</li>' - }, - DefaultOptions: { - sorter: function(query, items, searchKey) { - this.setting.highlightFirst = this.setting.alwaysHighlightFirst || query.length > 0; - if (gl.GfmAutoComplete.isLoading(items)) { - this.setting.highlightFirst = false; - return items; - } - return $.fn.atwho["default"].callbacks.sorter(query, items, searchKey); - }, - filter: function(query, data, searchKey) { - if (gl.GfmAutoComplete.isLoading(data)) { - gl.GfmAutoComplete.fetchData(this.$inputor, this.at); - return data; - } else { - return $.fn.atwho["default"].callbacks.filter(query, data, searchKey); - } - }, - beforeInsert: function(value) { - if (value && !this.setting.skipSpecialCharacterTest) { - var withoutAt = value.substring(1); - if (withoutAt && /[^\w\d]/.test(withoutAt)) value = value.charAt() + '"' + withoutAt + '"'; - } - return value; - }, - matcher: function (flag, subtext) { - // The below is taken from At.js source - // Tweaked to commands to start without a space only if char before is a non-word character - // https://github.com/ichord/At.js - var _a, _y, regexp, match, atSymbolsWithBar, atSymbolsWithoutBar; - atSymbolsWithBar = Object.keys(this.app.controllers).join('|'); - atSymbolsWithoutBar = Object.keys(this.app.controllers).join(''); - subtext = subtext.split(/\s+/g).pop(); - flag = flag.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); - - _a = decodeURI("%C3%80"); - _y = decodeURI("%C3%BF"); - - regexp = new RegExp("^(?:\\B|[^a-zA-Z0-9_" + atSymbolsWithoutBar + "]|\\s)" + flag + "(?!" + atSymbolsWithBar + ")((?:[A-Za-z" + _a + "-" + _y + "0-9_\'\.\+\-]|[^\\x00-\\x7a])*)$", 'gi'); - - match = regexp.exec(subtext); +class GfmAutoComplete { + constructor(dataSources) { + this.dataSources = dataSources || {}; + this.cachedData = {}; + this.isLoadingData = {}; + } - if (match) { - return match[1]; - } else { - return null; - } - } - }, - setup: function(input, enableMap = { + setup(input, enableMap = { emojis: true, members: true, issues: true, milestones: true, mergeRequests: true, - labels: true + labels: true, }) { // Add GFM auto-completion to all input fields, that accept GFM input. this.input = input || $('.js-gfm-input'); this.enableMap = enableMap; this.setupLifecycle(); - }, + } + setupLifecycle() { this.input.each((i, input) => { const $input = $(input); @@ -122,9 +36,9 @@ window.gl.GfmAutoComplete = { // Needed for slash commands with suffixes (ex: /label ~) $input.on('inserted-commands.atwho', $input.trigger.bind($input, 'keyup')); }); - }, + } - setupAtWho: function($input) { + setupAtWho($input) { if (this.enableMap.emojis) this.setupEmoji($input); if (this.enableMap.members) this.setupMembers($input); if (this.enableMap.issues) this.setupIssues($input); @@ -138,10 +52,11 @@ window.gl.GfmAutoComplete = { alias: 'commands', searchKey: 'search', skipSpecialCharacterTest: true, - data: this.defaultLoadingData, - displayTpl: function(value) { - if (this.isLoading(value)) return this.Loading.template; - var tpl = '<li>/${name}'; + data: GfmAutoComplete.defaultLoadingData, + displayTpl(value) { + if (GfmAutoComplete.isLoading(value)) return GfmAutoComplete.Loading.template; + // eslint-disable-next-line no-template-curly-in-string + let tpl = '<li>/${name}'; if (value.aliases.length > 0) { tpl += ' <small>(or /<%- aliases.join(", /") %>)</small>'; } @@ -153,105 +68,106 @@ window.gl.GfmAutoComplete = { } tpl += '</li>'; return _.template(tpl)(value); - }.bind(this), - insertTpl: function(value) { - var tpl = "/${name} "; - var reference_prefix = null; + }, + insertTpl(value) { + // eslint-disable-next-line no-template-curly-in-string + let tpl = '/${name} '; + let referencePrefix = null; if (value.params.length > 0) { - reference_prefix = value.params[0][0]; - if (/^[@%~]/.test(reference_prefix)) { - tpl += '<%- reference_prefix %>'; + referencePrefix = value.params[0][0]; + if (/^[@%~]/.test(referencePrefix)) { + tpl += '<%- referencePrefix %>'; } } - return _.template(tpl)({ reference_prefix: reference_prefix }); + return _.template(tpl)({ referencePrefix }); }, suffix: '', callbacks: { - sorter: this.DefaultOptions.sorter, - filter: this.DefaultOptions.filter, - beforeInsert: this.DefaultOptions.beforeInsert, - beforeSave: function(commands) { - if (gl.GfmAutoComplete.isLoading(commands)) return commands; - return $.map(commands, function(c) { - var search = c.name; + ...this.getDefaultCallbacks(), + beforeSave(commands) { + if (GfmAutoComplete.isLoading(commands)) return commands; + return $.map(commands, (c) => { + let search = c.name; if (c.aliases.length > 0) { - search = search + " " + c.aliases.join(" "); + search = `${search} ${c.aliases.join(' ')}`; } return { name: c.name, aliases: c.aliases, params: c.params, description: c.description, - search: search + search, }; }); }, - matcher: function(flag, subtext, should_startWithSpace, acceptSpaceBar) { - var regexp = /(?:^|\n)\/([A-Za-z_]*)$/gi; - var match = regexp.exec(subtext); + matcher(flag, subtext) { + const regexp = /(?:^|\n)\/([A-Za-z_]*)$/gi; + const match = regexp.exec(subtext); if (match) { return match[1]; - } else { - return null; } - } - } + return null; + }, + }, }); - return; - }, + } setupEmoji($input) { // Emoji $input.atwho({ at: ':', - displayTpl: function(value) { - return value && value.name ? this.Emoji.templateFunction(value.name) : this.Loading.template; - }.bind(this), + displayTpl(value) { + let tmpl = GfmAutoComplete.Loading.template; + if (value && value.name) { + tmpl = GfmAutoComplete.Emoji.templateFunction(value.name); + } + return tmpl; + }, + // eslint-disable-next-line no-template-curly-in-string insertTpl: ':${name}:', skipSpecialCharacterTest: true, - data: this.defaultLoadingData, + data: GfmAutoComplete.defaultLoadingData, callbacks: { - sorter: this.DefaultOptions.sorter, - beforeInsert: this.DefaultOptions.beforeInsert, - filter: this.DefaultOptions.filter, - - matcher: (flag, subtext) => { + ...this.getDefaultCallbacks(), + matcher(flag, subtext) { const relevantText = subtext.trim().split(/\s/).pop(); const regexp = new RegExp(`(?:[^${glRegexp.unicodeLetters}0-9:]|\n|^):([^:]*)$`, 'gi'); const match = regexp.exec(relevantText); return match && match.length ? match[1] : null; - } - } + }, + }, }); - }, + } setupMembers($input) { // Team Members $input.atwho({ at: '@', - displayTpl: function(value) { - return value.username != null ? this.Members.template : this.Loading.template; - }.bind(this), + displayTpl(value) { + let tmpl = GfmAutoComplete.Loading.template; + if (value.username != null) { + tmpl = GfmAutoComplete.Members.template; + } + return tmpl; + }, + // eslint-disable-next-line no-template-curly-in-string insertTpl: '${atwho-at}${username}', searchKey: 'search', alwaysHighlightFirst: true, skipSpecialCharacterTest: true, - data: this.defaultLoadingData, + data: GfmAutoComplete.defaultLoadingData, callbacks: { - sorter: this.DefaultOptions.sorter, - filter: this.DefaultOptions.filter, - beforeInsert: this.DefaultOptions.beforeInsert, - matcher: this.DefaultOptions.matcher, - beforeSave: function(members) { - return $.map(members, function(m) { + ...this.getDefaultCallbacks(), + beforeSave(members) { + return $.map(members, (m) => { let title = ''; if (m.username == null) { return m; } title = m.name; if (m.count) { - title += " (" + m.count + ")"; + title += ` (${m.count})`; } const autoCompleteAvatar = m.avatar_url || m.username.charAt(0).toUpperCase(); @@ -262,173 +178,271 @@ window.gl.GfmAutoComplete = { username: m.username, avatarTag: autoCompleteAvatar.length === 1 ? txtAvatar : imgAvatar, title: sanitize(title), - search: sanitize(m.username + " " + m.name) + search: sanitize(`${m.username} ${m.name}`), }; }); - } - } + }, + }, }); - }, + } setupIssues($input) { $input.atwho({ at: '#', alias: 'issues', searchKey: 'search', - displayTpl: function(value) { - return value.title != null ? this.Issues.template : this.Loading.template; - }.bind(this), - data: this.defaultLoadingData, + displayTpl(value) { + let tmpl = GfmAutoComplete.Loading.template; + if (value.title != null) { + tmpl = GfmAutoComplete.Issues.template; + } + return tmpl; + }, + data: GfmAutoComplete.defaultLoadingData, + // eslint-disable-next-line no-template-curly-in-string insertTpl: '${atwho-at}${id}', callbacks: { - sorter: this.DefaultOptions.sorter, - filter: this.DefaultOptions.filter, - beforeInsert: this.DefaultOptions.beforeInsert, - matcher: this.DefaultOptions.matcher, - beforeSave: function(issues) { - return $.map(issues, function(i) { + ...this.getDefaultCallbacks(), + beforeSave(issues) { + return $.map(issues, (i) => { if (i.title == null) { return i; } return { id: i.iid, title: sanitize(i.title), - search: i.iid + " " + i.title + search: `${i.iid} ${i.title}`, }; }); - } - } + }, + }, }); - }, + } setupMilestones($input) { $input.atwho({ at: '%', alias: 'milestones', searchKey: 'search', + // eslint-disable-next-line no-template-curly-in-string insertTpl: '${atwho-at}${title}', - displayTpl: function(value) { - return value.title != null ? this.Milestones.template : this.Loading.template; - }.bind(this), - data: this.defaultLoadingData, + displayTpl(value) { + let tmpl = GfmAutoComplete.Loading.template; + if (value.title != null) { + tmpl = GfmAutoComplete.Milestones.template; + } + return tmpl; + }, + data: GfmAutoComplete.defaultLoadingData, callbacks: { - matcher: this.DefaultOptions.matcher, - sorter: this.DefaultOptions.sorter, - beforeInsert: this.DefaultOptions.beforeInsert, - filter: this.DefaultOptions.filter, - beforeSave: function(milestones) { - return $.map(milestones, function(m) { + ...this.getDefaultCallbacks(), + beforeSave(milestones) { + return $.map(milestones, (m) => { if (m.title == null) { return m; } return { id: m.iid, title: sanitize(m.title), - search: "" + m.title + search: m.title, }; }); - } - } + }, + }, }); - }, + } setupMergeRequests($input) { $input.atwho({ at: '!', alias: 'mergerequests', searchKey: 'search', - displayTpl: function(value) { - return value.title != null ? this.Issues.template : this.Loading.template; - }.bind(this), - data: this.defaultLoadingData, + displayTpl(value) { + let tmpl = GfmAutoComplete.Loading.template; + if (value.title != null) { + tmpl = GfmAutoComplete.Issues.template; + } + return tmpl; + }, + data: GfmAutoComplete.defaultLoadingData, + // eslint-disable-next-line no-template-curly-in-string insertTpl: '${atwho-at}${id}', callbacks: { - sorter: this.DefaultOptions.sorter, - filter: this.DefaultOptions.filter, - beforeInsert: this.DefaultOptions.beforeInsert, - matcher: this.DefaultOptions.matcher, - beforeSave: function(merges) { - return $.map(merges, function(m) { + ...this.getDefaultCallbacks(), + beforeSave(merges) { + return $.map(merges, (m) => { if (m.title == null) { return m; } return { id: m.iid, title: sanitize(m.title), - search: m.iid + " " + m.title + search: `${m.iid} ${m.title}`, }; }); - } - } + }, + }, }); - }, + } setupLabels($input) { $input.atwho({ at: '~', alias: 'labels', searchKey: 'search', - data: this.defaultLoadingData, - displayTpl: function(value) { - return this.isLoading(value) ? this.Loading.template : this.Labels.template; - }.bind(this), + data: GfmAutoComplete.defaultLoadingData, + displayTpl(value) { + let tmpl = GfmAutoComplete.Labels.template; + if (GfmAutoComplete.isLoading(value)) { + tmpl = GfmAutoComplete.Loading.template; + } + return tmpl; + }, + // eslint-disable-next-line no-template-curly-in-string insertTpl: '${atwho-at}${title}', callbacks: { - matcher: this.DefaultOptions.matcher, - beforeInsert: this.DefaultOptions.beforeInsert, - filter: this.DefaultOptions.filter, - sorter: this.DefaultOptions.sorter, - beforeSave: function(merges) { - if (gl.GfmAutoComplete.isLoading(merges)) return merges; - var sanitizeLabelTitle; - sanitizeLabelTitle = function(title) { - if (/[\w\?&]+\s+[\w\?&]+/g.test(title)) { - return "\"" + (sanitize(title)) + "\""; - } else { - return sanitize(title); - } - }; - return $.map(merges, function(m) { - return { - title: sanitize(m.title), - color: m.color, - search: "" + m.title - }; - }); - } - } + ...this.getDefaultCallbacks(), + beforeSave(merges) { + if (GfmAutoComplete.isLoading(merges)) return merges; + return $.map(merges, m => ({ + title: sanitize(m.title), + color: m.color, + search: m.title, + })); + }, + }, }); - }, + } - fetchData: function($input, at) { + getDefaultCallbacks() { + const fetchData = this.fetchData.bind(this); + + return { + sorter(query, items, searchKey) { + this.setting.highlightFirst = this.setting.alwaysHighlightFirst || query.length > 0; + if (GfmAutoComplete.isLoading(items)) { + this.setting.highlightFirst = false; + return items; + } + return $.fn.atwho.default.callbacks.sorter(query, items, searchKey); + }, + filter(query, data, searchKey) { + if (GfmAutoComplete.isLoading(data)) { + fetchData(this.$inputor, this.at); + return data; + } + return $.fn.atwho.default.callbacks.filter(query, data, searchKey); + }, + beforeInsert(value) { + let resultantValue = value; + if (value && !this.setting.skipSpecialCharacterTest) { + const withoutAt = value.substring(1); + if (withoutAt && /[^\w\d]/.test(withoutAt)) { + resultantValue = `${value.charAt()}"${withoutAt}"`; + } + } + return resultantValue; + }, + matcher(flag, subtext) { + // The below is taken from At.js source + // Tweaked to commands to start without a space only if char before is a non-word character + // https://github.com/ichord/At.js + const atSymbolsWithBar = Object.keys(this.app.controllers).join('|'); + const atSymbolsWithoutBar = Object.keys(this.app.controllers).join(''); + const targetSubtext = subtext.split(/\s+/g).pop(); + const resultantFlag = flag.replace(/[-[\]/{}()*+?.\\^$|]/g, '\\$&'); + + const accentAChar = decodeURI('%C3%80'); + const accentYChar = decodeURI('%C3%BF'); + + const regexp = new RegExp(`^(?:\\B|[^a-zA-Z0-9_${atSymbolsWithoutBar}]|\\s)${resultantFlag}(?!${atSymbolsWithBar})((?:[A-Za-z${accentAChar}-${accentYChar}0-9_'.+-]|[^\\x00-\\x7a])*)$`, 'gi'); + + const match = regexp.exec(targetSubtext); + + if (match) { + return match[1]; + } + return null; + }, + }; + } + + fetchData($input, at) { if (this.isLoadingData[at]) return; this.isLoadingData[at] = true; if (this.cachedData[at]) { this.loadData($input, at, this.cachedData[at]); - } else if (this.atTypeMap[at] === 'emojis') { + } else if (GfmAutoComplete.atTypeMap[at] === 'emojis') { this.loadData($input, at, Object.keys(emojiMap).concat(Object.keys(emojiAliases))); } else { - $.getJSON(this.dataSources[this.atTypeMap[at]], (data) => { + $.getJSON(this.dataSources[GfmAutoComplete.atTypeMap[at]], (data) => { this.loadData($input, at, data); }).fail(() => { this.isLoadingData[at] = false; }); } - }, - loadData: function($input, at, data) { + } + loadData($input, at, data) { this.isLoadingData[at] = false; this.cachedData[at] = data; $input.atwho('load', at, data); // This trigger at.js again // otherwise we would be stuck with loading until the user types return $input.trigger('keyup'); - }, - isLoading(data) { - var dataToInspect = data; + } + + static isLoading(data) { + let dataToInspect = data; if (data && data.length > 0) { dataToInspect = data[0]; } - var loadingState = this.defaultLoadingData[0]; + const loadingState = GfmAutoComplete.defaultLoadingData[0]; return dataToInspect && (dataToInspect === loadingState || dataToInspect.name === loadingState); } +} + +GfmAutoComplete.defaultLoadingData = ['loading']; + +GfmAutoComplete.atTypeMap = { + ':': 'emojis', + '@': 'members', + '#': 'issues', + '!': 'mergeRequests', + '~': 'labels', + '%': 'milestones', + '/': 'commands', +}; + +// Emoji +GfmAutoComplete.Emoji = { + templateFunction(name) { + return `<li> + ${name} ${glEmojiTag(name)} + </li> + `; + }, }; +// Team Members +GfmAutoComplete.Members = { + // eslint-disable-next-line no-template-curly-in-string + template: '<li>${avatarTag} ${username} <small>${title}</small></li>', +}; +GfmAutoComplete.Labels = { + // eslint-disable-next-line no-template-curly-in-string + template: '<li><span class="dropdown-label-box" style="background: ${color}"></span> ${title}</li>', +}; +// Issues and MergeRequests +GfmAutoComplete.Issues = { + // eslint-disable-next-line no-template-curly-in-string + template: '<li><small>${id}</small> ${title}</li>', +}; +// Milestones +GfmAutoComplete.Milestones = { + // eslint-disable-next-line no-template-curly-in-string + template: '<li>${title}</li>', +}; +GfmAutoComplete.Loading = { + template: '<li style="pointer-events: none;"><i class="fa fa-spinner fa-spin"></i> Loading...</li>', +}; + +export default GfmAutoComplete; diff --git a/app/assets/javascripts/gl_field_errors.js b/app/assets/javascripts/gl_field_errors.js index ca3cec07a88..4f226ff96ea 100644 --- a/app/assets/javascripts/gl_field_errors.js +++ b/app/assets/javascripts/gl_field_errors.js @@ -1,6 +1,6 @@ /* eslint-disable comma-dangle, class-methods-use-this, max-len, space-before-function-paren, arrow-parens, no-param-reassign */ -require('./gl_field_error'); +import './gl_field_error'; const customValidationFlag = 'gl-field-error-ignore'; diff --git a/app/assets/javascripts/gl_form.js b/app/assets/javascripts/gl_form.js index ff06092e4d6..dc9f114af99 100644 --- a/app/assets/javascripts/gl_form.js +++ b/app/assets/javascripts/gl_form.js @@ -3,11 +3,14 @@ /* global DropzoneInput */ /* global autosize */ +import GfmAutoComplete from './gfm_auto_complete'; + window.gl = window.gl || {}; -function GLForm(form) { +function GLForm(form, enableGFM = false) { this.form = form; this.textarea = this.form.find('textarea.js-gfm-input'); + this.enableGFM = enableGFM; // Before we start, we should clean up any previous data for this form this.destroy(); // Setup the form @@ -30,8 +33,14 @@ GLForm.prototype.setupForm = function() { this.form.addClass('gfm-form'); // remove notify commit author checkbox for non-commit notes gl.utils.disableButtonIfEmptyField(this.form.find('.js-note-text'), this.form.find('.js-comment-button, .js-note-new-discussion')); - - gl.GfmAutoComplete.setup(this.form.find('.js-gfm-input')); + new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources).setup(this.form.find('.js-gfm-input'), { + emojis: true, + members: this.enableGFM, + issues: this.enableGFM, + milestones: this.enableGFM, + mergeRequests: this.enableGFM, + labels: this.enableGFM, + }); new DropzoneInput(this.form); autosize(this.textarea); } diff --git a/app/assets/javascripts/group_name.js b/app/assets/javascripts/group_name.js index 62675d7e67e..462d792b8d5 100644 --- a/app/assets/javascripts/group_name.js +++ b/app/assets/javascripts/group_name.js @@ -44,18 +44,18 @@ export default class GroupName { showToggle() { this.title.classList.add('wrap'); this.toggle.classList.remove('hidden'); - if (this.isHidden) this.groupTitle.classList.add('is-hidden'); + if (this.isHidden) this.groupTitle.classList.add('hidden'); } hideToggle() { this.title.classList.remove('wrap'); this.toggle.classList.add('hidden'); - if (this.isHidden) this.groupTitle.classList.remove('is-hidden'); + if (this.isHidden) this.groupTitle.classList.remove('hidden'); } toggleGroups() { this.isHidden = !this.isHidden; - this.groupTitle.classList.toggle('is-hidden'); + this.groupTitle.classList.toggle('hidden'); } render() { diff --git a/app/assets/javascripts/groups_select.js b/app/assets/javascripts/groups_select.js index acfa4bd4c6b..b5975295329 100644 --- a/app/assets/javascripts/groups_select.js +++ b/app/assets/javascripts/groups_select.js @@ -3,7 +3,7 @@ prefer-arrow-callback, comma-dangle, consistent-return, yoda, prefer-rest-params, prefer-spread, no-unused-vars, prefer-template, promise/catch-or-return */ -/* global Api */ +import Api from './api'; var slice = [].slice; diff --git a/app/assets/javascripts/issuable_context.js b/app/assets/javascripts/issuable_context.js index 4520e990e6f..a4d7bf096ef 100644 --- a/app/assets/javascripts/issuable_context.js +++ b/app/assets/javascripts/issuable_context.js @@ -47,7 +47,6 @@ import UsersSelect from './users_select'; Cookies.set('collapsed_gutter', true); } }); - $(".right-sidebar").niceScroll(); } IssuableContext.prototype.initParticipants = function() { diff --git a/app/assets/javascripts/issuable_form.js b/app/assets/javascripts/issuable_form.js index 4310663e0b6..92f6f0d4117 100644 --- a/app/assets/javascripts/issuable_form.js +++ b/app/assets/javascripts/issuable_form.js @@ -6,6 +6,7 @@ /* global Pikaday */ import UsersSelect from './users_select'; +import GfmAutoComplete from './gfm_auto_complete'; (function() { this.IssuableForm = (function() { @@ -20,7 +21,7 @@ import UsersSelect from './users_select'; this.renderWipExplanation = this.renderWipExplanation.bind(this); this.resetAutosave = this.resetAutosave.bind(this); this.handleSubmit = this.handleSubmit.bind(this); - gl.GfmAutoComplete.setup(); + new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources).setup(); new UsersSelect(); new ZenMode(); this.titleField = this.form.find("input[name*='[title]']"); diff --git a/app/assets/javascripts/issue.js b/app/assets/javascripts/issue.js index 694c6177a07..0860e237ce1 100644 --- a/app/assets/javascripts/issue.js +++ b/app/assets/javascripts/issue.js @@ -1,11 +1,11 @@ /* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, one-var, no-underscore-dangle, one-var-declaration-per-line, object-shorthand, no-unused-vars, no-new, comma-dangle, consistent-return, quotes, dot-notation, quote-props, prefer-arrow-callback, max-len */ - /* global Flash */ -import CreateMergeRequestDropdown from './create_merge_request_dropdown'; +/* global Flash */ -require('./flash'); -require('~/lib/utils/text_utility'); -require('vendor/jquery.waitforimages'); -require('./task_list'); +import 'vendor/jquery.waitforimages'; +import '~/lib/utils/text_utility'; +import './flash'; +import './task_list'; +import CreateMergeRequestDropdown from './create_merge_request_dropdown'; class Issue { constructor() { diff --git a/app/assets/javascripts/lib/utils/ajax_cache.js b/app/assets/javascripts/lib/utils/ajax_cache.js index cf030d613df..f1fe95e12e8 100644 --- a/app/assets/javascripts/lib/utils/ajax_cache.js +++ b/app/assets/javascripts/lib/utils/ajax_cache.js @@ -1,21 +1,11 @@ -class AjaxCache { +import Cache from './cache'; + +class AjaxCache extends Cache { constructor() { - this.internalStorage = { }; + super(); this.pendingRequests = { }; } - get(endpoint) { - return this.internalStorage[endpoint]; - } - - hasData(endpoint) { - return Object.prototype.hasOwnProperty.call(this.internalStorage, endpoint); - } - - remove(endpoint) { - delete this.internalStorage[endpoint]; - } - retrieve(endpoint) { if (this.hasData(endpoint)) { return Promise.resolve(this.get(endpoint)); diff --git a/app/assets/javascripts/lib/utils/cache.js b/app/assets/javascripts/lib/utils/cache.js new file mode 100644 index 00000000000..3141f1eeafc --- /dev/null +++ b/app/assets/javascripts/lib/utils/cache.js @@ -0,0 +1,19 @@ +class Cache { + constructor() { + this.internalStorage = { }; + } + + get(key) { + return this.internalStorage[key]; + } + + hasData(key) { + return Object.prototype.hasOwnProperty.call(this.internalStorage, key); + } + + remove(key) { + delete this.internalStorage[key]; + } +} + +export default Cache; diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js index 2f682fbd2fb..a537267643e 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js +++ b/app/assets/javascripts/lib/utils/common_utils.js @@ -135,7 +135,10 @@ gl.utils.getUrlParamsArray = function () { // We can trust that each param has one & since values containing & will be encoded // Remove the first character of search as it is always ? - return window.location.search.slice(1).split('&'); + return window.location.search.slice(1).split('&').map((param) => { + const split = param.split('='); + return [decodeURI(split[0]), split[1]].join('='); + }); }; gl.utils.isMetaKey = function(e) { @@ -195,10 +198,12 @@ const textBefore = value.substring(0, selectionStart); const textAfter = value.substring(selectionEnd, value.length); - const newText = textBefore + text + textAfter; + + const insertedText = text instanceof Function ? text(textBefore, textAfter) : text; + const newText = textBefore + insertedText + textAfter; target.value = newText; - target.selectionStart = target.selectionEnd = selectionStart + text.length; + target.selectionStart = target.selectionEnd = selectionStart + insertedText.length; // Trigger autosave $(target).trigger('input'); diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js index 82dcbdc26c8..b2f48049bb4 100644 --- a/app/assets/javascripts/lib/utils/datetime_utility.js +++ b/app/assets/javascripts/lib/utils/datetime_utility.js @@ -1,9 +1,10 @@ /* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-param-reassign, no-cond-assign, comma-dangle, no-unused-expressions, prefer-template, max-len */ -/* global timeago */ -/* global dateFormat */ -window.timeago = require('timeago.js'); -window.dateFormat = require('vendor/date.format'); +import timeago from 'timeago.js'; +import dateFormat from 'vendor/date.format'; + +window.timeago = timeago; +window.dateFormat = dateFormat; (function() { (function(w) { @@ -101,8 +102,7 @@ window.dateFormat = require('vendor/date.format'); }; w.gl.utils.updateTimeagoText = function(el) { - const timeago = gl.utils.getTimeago(); - const formattedDate = timeago.format(el.getAttribute('datetime'), 'gl_en'); + const formattedDate = gl.utils.getTimeago().format(el.getAttribute('datetime'), 'gl_en'); if (el.textContent !== formattedDate) { el.textContent = formattedDate; diff --git a/app/assets/javascripts/lib/utils/http_status.js b/app/assets/javascripts/lib/utils/http_status.js index bc109a69c20..415e50f32ae 100644 --- a/app/assets/javascripts/lib/utils/http_status.js +++ b/app/assets/javascripts/lib/utils/http_status.js @@ -2,9 +2,7 @@ * exports HTTP status codes */ -const statusCodes = { +export default { NO_CONTENT: 204, OK: 200, }; - -module.exports = statusCodes; diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js index fecd531328d..b43c1c3aac6 100644 --- a/app/assets/javascripts/lib/utils/text_utility.js +++ b/app/assets/javascripts/lib/utils/text_utility.js @@ -1,5 +1,6 @@ /* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-param-reassign, no-cond-assign, quotes, one-var, one-var-declaration-per-line, operator-assignment, no-else-return, prefer-template, prefer-arrow-callback, no-empty, max-len, consistent-return, no-unused-vars, no-return-assign, max-len, vars-on-top */ -require('vendor/latinise'); + +import 'vendor/latinise'; var base; var w = window; diff --git a/app/assets/javascripts/lib/utils/users_cache.js b/app/assets/javascripts/lib/utils/users_cache.js new file mode 100644 index 00000000000..88f8a622c00 --- /dev/null +++ b/app/assets/javascripts/lib/utils/users_cache.js @@ -0,0 +1,28 @@ +import Api from '../../api'; +import Cache from './cache'; + +class UsersCache extends Cache { + retrieve(username) { + if (this.hasData(username)) { + return Promise.resolve(this.get(username)); + } + + return Api.users('', { username }) + .then((users) => { + if (!users.length) { + throw new Error(`User "${username}" could not be found!`); + } + + if (users.length > 1) { + throw new Error(`Expected username "${username}" to be unique!`); + } + + const user = users[0]; + this.internalStorage[username] = user; + return user; + }); + // missing catch is intentional, error handling depends on use case + } +} + +export default new UsersCache(); diff --git a/app/assets/javascripts/line_highlighter.js b/app/assets/javascripts/line_highlighter.js index 517f03d5aba..7400c22543f 100644 --- a/app/assets/javascripts/line_highlighter.js +++ b/app/assets/javascripts/line_highlighter.js @@ -4,8 +4,6 @@ // // Handles single- and multi-line selection and highlight for blob views. // -require('vendor/jquery.scrollTo'); - // // ### Example Markup // diff --git a/app/assets/javascripts/locale/es/app.js b/app/assets/javascripts/locale/es/app.js index 3dafa21f235..f5f510d7c2b 100644 --- a/app/assets/javascripts/locale/es/app.js +++ b/app/assets/javascripts/locale/es/app.js @@ -1 +1 @@ -var locales = locales || {}; locales['es'] = {"domain":"app","locale_data":{"app":{"":{"Project-Id-Version":"gitlab 1.0.0","Report-Msgid-Bugs-To":"","PO-Revision-Date":"2017-05-04 19:24-0500","Language-Team":"Spanish","Language":"es","MIME-Version":"1.0","Content-Type":"text/plain; charset=UTF-8","Content-Transfer-Encoding":"8bit","Plural-Forms":"nplurals=2; plural=n != 1;","Last-Translator":"","X-Generator":"Poedit 2.0.1","lang":"es","domain":"app","plural_forms":"nplurals=2; plural=n != 1;"},"ByAuthor|by":["por"],"Commit":["Cambio","Cambios"],"Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project.":["Cycle Analytics ofrece una visión general de cuánto tiempo tarda en pasar de idea a producción en su proyecto."],"CycleAnalyticsStage|Code":["Código"],"CycleAnalyticsStage|Issue":["Incidencia"],"CycleAnalyticsStage|Plan":["Planificación"],"CycleAnalyticsStage|Production":["Producción"],"CycleAnalyticsStage|Review":["Revisión"],"CycleAnalyticsStage|Staging":["Puesta en escena"],"CycleAnalyticsStage|Test":["Pruebas"],"Deploy":["Despliegue","Despliegues"],"FirstPushedBy|First":["Primer"],"FirstPushedBy|pushed by":["enviado por"],"From issue creation until deploy to production":["Desde la creación de la incidencia hasta el despliegue a producción"],"From merge request merge until deploy to production":["Desde la integración de la solicitud de fusión hasta el despliegue a producción"],"Introducing Cycle Analytics":["Introducción a Cycle Analytics"],"Last %d day":["Último %d día","Últimos %d días"],"Limited to showing %d event at most":["Limitado a mostrar máximo %d evento","Limitado a mostrar máximo %d eventos"],"Median":["Mediana"],"New Issue":["Nueva incidencia","Nuevas incidencias"],"Not available":["No disponible"],"Not enough data":["No hay suficientes datos"],"OpenedNDaysAgo|Opened":["Abierto"],"Pipeline Health":["Estado del Pipeline"],"ProjectLifecycle|Stage":["Etapa"],"Read more":["Leer más"],"Related Commits":["Cambios Relacionados"],"Related Deployed Jobs":["Trabajos Desplegados Relacionados"],"Related Issues":["Incidencias Relacionadas"],"Related Jobs":["Trabajos Relacionados"],"Related Merge Requests":["Solicitudes de fusión Relacionadas"],"Related Merged Requests":["Solicitudes de fusión Relacionadas"],"Showing %d event":["Mostrando %d evento","Mostrando %d eventos"],"The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request.":["La etapa de codificación muestra el tiempo desde el primer cambio hasta la creación de la solicitud de fusión. Los datos serán automáticamente incorporados aquí una vez creada tu primera solicitud de fusión."],"The collection of events added to the data gathered for that stage.":["La colección de eventos agregados a los datos recopilados para esa etapa."],"The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage.":["La etapa de incidencia muestra el tiempo que toma desde la creación de un tema hasta asignar el tema a un hito, o añadir el tema a una lista en el panel de temas. Empieza a crear temas para ver los datos de esta etapa."],"The phase of the development lifecycle.":["La etapa del ciclo de vida de desarrollo."],"The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit.":["La etapa de planificación muestra el tiempo desde el paso anterior hasta el envío de tu primera confirmación. Este tiempo se añadirá automáticamente una vez que usted envíe el primer cambio."],"The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle.":["La etapa de producción muestra el tiempo total que tarda entre la creación de una incidencia y el despliegue del código a producción. Los datos se añadirán automáticamente una vez haya finalizado por completo el ciclo de idea a producción."],"The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request.":["La etapa de revisión muestra el tiempo desde la creación de la solicitud de fusión hasta que los cambios se fusionaron. Los datos se añadirán automáticamente después de fusionar su primera solicitud de fusión."],"The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time.":["La etapa de puesta en escena muestra el tiempo entre la fusión y el despliegue de código en el entorno de producción. Los datos se añadirán automáticamente una vez que se despliega a producción por primera vez."],"The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running.":["La etapa de pruebas muestra el tiempo que GitLab CI toma para ejecutar cada pipeline para la solicitud de fusión relacionada. Los datos se añadirán automáticamente luego de que el primer pipeline termine de ejecutarse."],"The time taken by each data entry gathered by that stage.":["El tiempo utilizado por cada entrada de datos obtenido por esa etapa."],"The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6.":["El valor en el punto medio de una serie de valores observados. Por ejemplo, entre 3, 5, 9, la mediana es 5. Entre 3, 5, 7, 8, la mediana es (5 + 7) / 2 = 6."],"Time before an issue gets scheduled":["Tiempo antes de que una incidencia sea programada"],"Time before an issue starts implementation":["Tiempo antes de que empieze la implementación de una incidencia"],"Time between merge request creation and merge/close":["Tiempo entre la creación de la solicitud de fusión y la integración o cierre de ésta"],"Time until first merge request":["Tiempo hasta la primera solicitud de fusión"],"Time|hr":["hr","hrs"],"Time|min":["min","mins"],"Time|s":["s"],"Total Time":["Tiempo Total"],"Total test time for all commits/merges":["Tiempo total de pruebas para todos los cambios o integraciones"],"Want to see the data? Please ask an administrator for access.":["¿Quieres ver los datos? Por favor pide acceso al administrador."],"We don't have enough data to show this stage.":["No hay suficientes datos para mostrar en esta etapa."],"You need permission.":["Necesitas permisos."],"day":["día","días"]}}};
\ No newline at end of file +var locales = locales || {}; locales['es'] = {"domain":"app","locale_data":{"app":{"":{"Project-Id-Version":"gitlab 1.0.0","Report-Msgid-Bugs-To":"","PO-Revision-Date":"2017-05-20 22:37-0500","Language-Team":"Spanish","Language":"es","MIME-Version":"1.0","Content-Type":"text/plain; charset=UTF-8","Content-Transfer-Encoding":"8bit","Plural-Forms":"nplurals=2; plural=n != 1;","Last-Translator":"","X-Generator":"Poedit 2.0.1","lang":"es","domain":"app","plural_forms":"nplurals=2; plural=n != 1;"},"ByAuthor|by":["por"],"Commit":["Cambio","Cambios"],"Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project.":["Cycle Analytics ofrece una visión general de cuánto tiempo tarda en pasar de idea a producción en su proyecto."],"CycleAnalyticsStage|Code":["Código"],"CycleAnalyticsStage|Issue":["Incidencia"],"CycleAnalyticsStage|Plan":["Planificación"],"CycleAnalyticsStage|Production":["Producción"],"CycleAnalyticsStage|Review":["Revisión"],"CycleAnalyticsStage|Staging":["Puesta en escena"],"CycleAnalyticsStage|Test":["Pruebas"],"Deploy":["Despliegue","Despliegues"],"FirstPushedBy|First":["Primer"],"FirstPushedBy|pushed by":["enviado por"],"From issue creation until deploy to production":["Desde la creación de la incidencia hasta el despliegue a producción"],"From merge request merge until deploy to production":["Desde la integración de la solicitud de fusión hasta el despliegue a producción"],"Introducing Cycle Analytics":["Introducción a Cycle Analytics"],"Last %d day":["Último %d día","Últimos %d días"],"Limited to showing %d event at most":["Limitado a mostrar máximo %d evento","Limitado a mostrar máximo %d eventos"],"Median":["Mediana"],"New Issue":["Nueva incidencia","Nuevas incidencias"],"Not available":["No disponible"],"Not enough data":["No hay suficientes datos"],"OpenedNDaysAgo|Opened":["Abierto"],"Pipeline Health":["Estado del Pipeline"],"ProjectLifecycle|Stage":["Etapa"],"Read more":["Leer más"],"Related Commits":["Cambios Relacionados"],"Related Deployed Jobs":["Trabajos Desplegados Relacionados"],"Related Issues":["Incidencias Relacionadas"],"Related Jobs":["Trabajos Relacionados"],"Related Merge Requests":["Solicitudes de fusión Relacionadas"],"Related Merged Requests":["Solicitudes de fusión Relacionadas"],"Showing %d event":["Mostrando %d evento","Mostrando %d eventos"],"The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request.":["La etapa de desarrollo muestra el tiempo desde el primer cambio hasta la creación de la solicitud de fusión. Los datos serán automáticamente incorporados aquí una vez creada tu primera solicitud de fusión."],"The collection of events added to the data gathered for that stage.":["La colección de eventos agregados a los datos recopilados para esa etapa."],"The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage.":["La etapa de incidencia muestra el tiempo que toma desde la creación de un tema hasta asignar el tema a un hito, o añadir el tema a una lista en el panel de temas. Empieza a crear temas para ver los datos de esta etapa."],"The phase of the development lifecycle.":["La etapa del ciclo de vida de desarrollo."],"The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit.":["La etapa de planificación muestra el tiempo desde el paso anterior hasta el envío de tu primera confirmación. Este tiempo se añadirá automáticamente una vez que usted envíe el primer cambio."],"The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle.":["La etapa de producción muestra el tiempo total que tarda entre la creación de una incidencia y el despliegue del código a producción. Los datos se añadirán automáticamente una vez haya finalizado por completo el ciclo de idea a producción."],"The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request.":["La etapa de revisión muestra el tiempo desde la creación de la solicitud de fusión hasta que los cambios se fusionaron. Los datos se añadirán automáticamente después de fusionar su primera solicitud de fusión."],"The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time.":["La etapa de puesta en escena muestra el tiempo entre la fusión y el despliegue de código en el entorno de producción. Los datos se añadirán automáticamente una vez que se despliega a producción por primera vez."],"The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running.":["La etapa de pruebas muestra el tiempo que GitLab CI toma para ejecutar cada pipeline para la solicitud de fusión relacionada. Los datos se añadirán automáticamente luego de que el primer pipeline termine de ejecutarse."],"The time taken by each data entry gathered by that stage.":["El tiempo utilizado por cada entrada de datos obtenido por esa etapa."],"The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6.":["El valor en el punto medio de una serie de valores observados. Por ejemplo, entre 3, 5, 9, la mediana es 5. Entre 3, 5, 7, 8, la mediana es (5 + 7) / 2 = 6."],"Time before an issue gets scheduled":["Tiempo antes de que una incidencia sea programada"],"Time before an issue starts implementation":["Tiempo antes de que empieze la implementación de una incidencia"],"Time between merge request creation and merge/close":["Tiempo entre la creación de la solicitud de fusión y la integración o cierre de ésta"],"Time until first merge request":["Tiempo hasta la primera solicitud de fusión"],"Time|hr":["hr","hrs"],"Time|min":["min","mins"],"Time|s":["s"],"Total Time":["Tiempo Total"],"Total test time for all commits/merges":["Tiempo total de pruebas para todos los cambios o integraciones"],"Want to see the data? Please ask an administrator for access.":["¿Quieres ver los datos? Por favor pide acceso al administrador."],"We don't have enough data to show this stage.":["No hay suficientes datos para mostrar en esta etapa."],"You need permission.":["Necesitas permisos."],"day":["día","días"]}}};
\ No newline at end of file diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index 30636f6afec..f0958972130 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -96,7 +96,6 @@ import './dropzone_input'; import './due_date_select'; import './files_comment_button'; import './flash'; -import './gfm_auto_complete'; import './gl_dropdown'; import './gl_field_error'; import './gl_field_errors'; @@ -171,7 +170,7 @@ import './visibility_select'; import './wikis'; import './zen_mode'; -// eslint-disable-next-line global-require +// eslint-disable-next-line global-require, import/no-commonjs if (process.env.NODE_ENV !== 'production') require('./test_utils/'); document.addEventListener('beforeunload', function () { diff --git a/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js index 15992460146..17030c3e4d3 100644 --- a/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js +++ b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js @@ -2,14 +2,13 @@ /* global Flash */ import Vue from 'vue'; - -require('./merge_conflict_store'); -require('./merge_conflict_service'); -require('./mixins/line_conflict_utils'); -require('./mixins/line_conflict_actions'); -require('./components/diff_file_editor'); -require('./components/inline_conflict_lines'); -require('./components/parallel_conflict_lines'); +import './merge_conflict_store'; +import './merge_conflict_service'; +import './mixins/line_conflict_utils'; +import './mixins/line_conflict_actions'; +import './components/diff_file_editor'; +import './components/inline_conflict_lines'; +import './components/parallel_conflict_lines'; $(() => { const INTERACTIVE_RESOLVE_MODE = 'interactive'; diff --git a/app/assets/javascripts/merge_request.js b/app/assets/javascripts/merge_request.js index d1cdcadf87d..f93feeec1c2 100644 --- a/app/assets/javascripts/merge_request.js +++ b/app/assets/javascripts/merge_request.js @@ -1,9 +1,9 @@ /* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, quotes, no-underscore-dangle, one-var, one-var-declaration-per-line, consistent-return, dot-notation, quote-props, comma-dangle, object-shorthand, max-len, prefer-arrow-callback */ /* global MergeRequestTabs */ -require('vendor/jquery.waitforimages'); -require('./task_list'); -require('./merge_request_tabs'); +import 'vendor/jquery.waitforimages'; +import './task_list'; +import './merge_request_tabs'; (function() { this.MergeRequest = (function() { diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js index ebb217ab13a..22032d0f914 100644 --- a/app/assets/javascripts/merge_request_tabs.js +++ b/app/assets/javascripts/merge_request_tabs.js @@ -1,6 +1,7 @@ /* eslint-disable no-new, class-methods-use-this */ /* global Breakpoints */ /* global Flash */ +/* global notes */ import Cookies from 'js-cookie'; import './breakpoints'; @@ -251,7 +252,8 @@ import BlobForkSuggestion from './blob/blob_fork_suggestion'; this.ajaxGet({ url: `${urlPathname}.json${location.search}`, success: (data) => { - $('#diffs').html(data.html); + const $container = $('#diffs'); + $container.html(data.html); if (typeof gl.diffNotesCompileComponents !== 'undefined') { gl.diffNotesCompileComponents(); @@ -278,6 +280,24 @@ import BlobForkSuggestion from './blob/blob_fork_suggestion'; }) .init(); }); + + // Scroll any linked note into view + // Similar to `toggler_behavior` in the discussion tab + const hash = window.gl.utils.getLocationHash(); + const anchor = hash && $container.find(`[id="${hash}"]`); + if (anchor) { + const notesContent = anchor.closest('.notes_content'); + const lineType = notesContent.hasClass('new') ? 'new' : 'old'; + notes.toggleDiffNote({ + target: anchor, + lineType, + forceShow: true, + }); + anchor[0].scrollIntoView(); + // We have multiple elements on the page with `#note_xxx` + // (discussion and diff tabs) and `:target` only applies to the first + anchor.addClass('target'); + } }, }); } diff --git a/app/assets/javascripts/merge_request_widget.js b/app/assets/javascripts/merge_request_widget.js deleted file mode 100644 index 3f976680b9d..00000000000 --- a/app/assets/javascripts/merge_request_widget.js +++ /dev/null @@ -1,303 +0,0 @@ -/* eslint-disable max-len, no-var, func-names, space-before-function-paren, vars-on-top, comma-dangle, no-return-assign, consistent-return, no-param-reassign, one-var, one-var-declaration-per-line, quotes, prefer-template, no-else-return, prefer-arrow-callback, no-unused-vars, no-underscore-dangle, no-shadow, no-mixed-operators, camelcase, default-case, wrap-iife */ -/* global notify */ -/* global notifyPermissions */ -/* global merge_request_widget */ - -import './smart_interval'; -import MiniPipelineGraph from './mini_pipeline_graph_dropdown'; - -((global) => { - const DEPLOYMENT_TEMPLATE = `<div class="mr-widget-heading" id="<%- id %>"> - <div class="ci_widget ci-success"> - <%= ci_success_icon %> - <span> - Deployed to - <a href="<%- url %>" target="_blank" rel="noopener noreferrer" class="environment"> - <%- name %> - </a> - <span class="js-environment-timeago" data-toggle="tooltip" data-placement="top" data-title="<%- deployed_at_formatted %>"> - <%- deployed_at %> - </span> - <a class="js-environment-link" href="<%- external_url %>" target="_blank" rel="noopener noreferrer"> - <i class="fa fa-external-link"></i> - View on <%- external_url_formatted %> - </a> - </span> - <span class="stop-env-container js-stop-env-link"> - <a href="<%- stop_url %>" class="close-evn-link" data-method="post" rel="nofollow" data-confirm="Are you sure you want to stop this environment?"> - <i class="fa fa-stop-circle-o"/> - Stop environment - </a> - </span> - </div> - </div>`; - - global.MergeRequestWidget = (function() { - function MergeRequestWidget(opts) { - // Initialize MergeRequestWidget behavior - // - // check_enable - Boolean, whether to check automerge status - // merge_check_url - String, URL to use to check automerge status - // ci_status_url - String, URL to use to check CI status - // pipeline_status_url - String, URL to use to get CI status for Favicon - // - this.opts = opts; - this.opts.pipeline_status_url = `${this.opts.pipeline_status_url}.json`; - this.$widgetBody = $('.mr-widget-body'); - $('#modal_merge_info').modal({ - show: false - }); - this.clearEventListeners(); - this.addEventListeners(); - this.getCIStatus(false); - this.retrieveSuccessIcon(); - - this.initMiniPipelineGraph(); - - this.ciStatusInterval = new global.SmartInterval({ - callback: this.getCIStatus.bind(this, true), - startingInterval: 10000, - maxInterval: 30000, - hiddenInterval: 120000, - incrementByFactorOf: 5000, - }); - this.ciEnvironmentStatusInterval = new global.SmartInterval({ - callback: this.getCIEnvironmentsStatus.bind(this), - startingInterval: 30000, - maxInterval: 120000, - hiddenInterval: 240000, - incrementByFactorOf: 15000, - immediateExecution: true, - }); - - notifyPermissions(); - } - - MergeRequestWidget.prototype.clearEventListeners = function() { - return $(document).off('DOMContentLoaded'); - }; - - MergeRequestWidget.prototype.addEventListeners = function() { - var allowedPages; - allowedPages = ['show', 'commits', 'pipelines', 'changes']; - $(document).on('DOMContentLoaded', (function(_this) { - return function() { - var page; - page = $('body').data('page').split(':').last(); - if (allowedPages.indexOf(page) === -1) { - return _this.clearEventListeners(); - } - }; - })(this)); - }; - - MergeRequestWidget.prototype.retrieveSuccessIcon = function() { - const $ciSuccessIcon = $('.js-success-icon'); - this.$ciSuccessIcon = $ciSuccessIcon.html(); - $ciSuccessIcon.remove(); - }; - - MergeRequestWidget.prototype.mergeInProgress = function(deleteSourceBranch) { - if (deleteSourceBranch == null) { - deleteSourceBranch = false; - } - return $.ajax({ - type: 'GET', - url: $('.merge-request').data('url'), - success: (function(_this) { - return function(data) { - var callback, urlSuffix; - if (data.state === "merged") { - urlSuffix = deleteSourceBranch ? '?deleted_source_branch=true' : ''; - return window.location.href = window.location.pathname + urlSuffix; - } else if (data.merge_error) { - return $('.mr-widget-body').html("<h4>" + data.merge_error + "</h4>"); - } else { - callback = function() { - return merge_request_widget.mergeInProgress(deleteSourceBranch); - }; - return setTimeout(callback, 2000); - } - }; - })(this), - dataType: 'json' - }); - }; - - MergeRequestWidget.prototype.cancelPolling = function () { - this.ciStatusInterval.cancel(); - this.ciEnvironmentStatusInterval.cancel(); - }; - - MergeRequestWidget.prototype.getMergeStatus = function() { - return $.get(this.opts.merge_check_url, (data) => { - var $html = $(data); - this.updateMergeButton(this.status, this.hasCi, $html); - $('.mr-widget-body').replaceWith($html.find('.mr-widget-body')); - $('.mr-widget-footer').replaceWith($html.find('.mr-widget-footer')); - }); - }; - - MergeRequestWidget.prototype.ciLabelForStatus = function(status) { - switch (status) { - case 'success': - return 'passed'; - case 'success_with_warnings': - return 'passed with warnings'; - default: - return status; - } - }; - - MergeRequestWidget.prototype.getCIStatus = function(showNotification) { - var _this; - _this = this; - $('.ci-widget-fetching').show(); - return $.getJSON(this.opts.ci_status_url, (function(_this) { - return function(data) { - var message, status, title, callback; - _this.status = data.status; - _this.hasCi = data.has_ci; - _this.updateMergeButton(_this.status, _this.hasCi); - gl.utils.setCiStatusFavicon(_this.opts.pipeline_status_url); - if (data.environments && data.environments.length) _this.renderEnvironments(data.environments); - if (data.status !== _this.opts.ci_status || - data.sha !== _this.opts.ci_sha || - data.pipeline !== _this.opts.ci_pipeline) { - _this.opts.ci_status = data.status; - _this.showCIStatus(data.status); - if (data.coverage) { - _this.showCICoverage(data.coverage); - } - if (data.pipeline) { - _this.opts.ci_pipeline = data.pipeline; - _this.updatePipelineUrls(data.pipeline); - } - if (data.sha) { - _this.opts.ci_sha = data.sha; - _this.updateCommitUrls(data.sha); - } - if (data.status === "success" || data.status === "failed") { - callback = function() { - return _this.getMergeStatus(); - }; - return setTimeout(callback, 2000); - } - if (showNotification && data.status) { - status = _this.ciLabelForStatus(data.status); - if (status === "preparing") { - title = _this.opts.ci_title.preparing; - status = status.charAt(0).toUpperCase() + status.slice(1); - message = _this.opts.ci_message.preparing.replace('{{status}}', status); - } else { - title = _this.opts.ci_title.normal; - message = _this.opts.ci_message.normal.replace('{{status}}', status); - } - title = title.replace('{{status}}', status); - message = message.replace('{{sha}}', data.sha); - message = message.replace('{{title}}', data.title); - notify(title, message, _this.opts.gitlab_icon, function() { - this.close(); - }); - } - } - }; - })(this)); - }; - - MergeRequestWidget.prototype.getCIEnvironmentsStatus = function() { - $.getJSON(this.opts.ci_environments_status_url, (environments) => { - if (environments && environments.length) this.renderEnvironments(environments); - }); - }; - - MergeRequestWidget.prototype.renderEnvironments = function(environments) { - for (let i = 0; i < environments.length; i += 1) { - const environment = environments[i]; - if ($(`.mr-state-widget #${environment.id}`).length) return; - const $template = $(DEPLOYMENT_TEMPLATE); - if (!environment.external_url || !environment.external_url_formatted) $('.js-environment-link', $template).remove(); - - if (!environment.stop_url) { - $('.js-stop-env-link', $template).remove(); - } - - if (environment.deployed_at && environment.deployed_at_formatted) { - environment.deployed_at = gl.utils.getTimeago().format(environment.deployed_at, 'gl_en') + '.'; - } else { - $('.js-environment-timeago', $template).remove(); - environment.name += '.'; - } - environment.ci_success_icon = this.$ciSuccessIcon; - const templateString = _.unescape($template[0].outerHTML); - const template = _.template(templateString)(environment); - this.$widgetBody.before(template); - } - }; - - MergeRequestWidget.prototype.showCIStatus = function(state) { - var allowed_states; - if (state == null) { - return; - } - $('.ci_widget').hide(); - $('.ci_widget.ci-' + state).show(); - - this.initMiniPipelineGraph(); - }; - - MergeRequestWidget.prototype.showCICoverage = function(coverage) { - var text = `Coverage ${coverage}%`; - return $('.ci_widget:visible .ci-coverage').text(text); - }; - - MergeRequestWidget.prototype.updateMergeButton = function(state, hasCi, $html) { - const allowed_states = ["failed", "canceled", "running", "pending", "success", "success_with_warnings", "skipped", "not_found"]; - let stateClass = 'btn-danger'; - if (!hasCi) { - stateClass = 'btn-create'; - } else if (allowed_states.indexOf(state) !== -1) { - switch (state) { - case "failed": - case "canceled": - case "not_found": - stateClass = 'btn-danger'; - break; - case "running": - stateClass = 'btn-info'; - break; - case "success": - case "success_with_warnings": - stateClass = 'btn-create'; - } - } else { - $('.ci_widget.ci-error').show(); - stateClass = 'btn-danger'; - } - - this.setMergeButtonClass(stateClass, $html); - }; - - MergeRequestWidget.prototype.setMergeButtonClass = function(css_class, $html = $('.mr-state-widget')) { - return $html.find('.js-merge-button').removeClass('btn-danger btn-info btn-create').addClass(css_class); - }; - - MergeRequestWidget.prototype.updatePipelineUrls = function(id) { - const pipelineUrl = this.opts.pipeline_path; - $('.pipeline').text(`#${id}`).attr('href', [pipelineUrl, id].join('/')); - }; - - MergeRequestWidget.prototype.updateCommitUrls = function(id) { - const commitsUrl = this.opts.commits_path; - $('.js-commit-link').text(id).attr('href', [commitsUrl, id].join('/')); - }; - - MergeRequestWidget.prototype.initMiniPipelineGraph = function() { - new MiniPipelineGraph({ - container: '.js-pipeline-inline-mr-widget-graph:visible', - }).bindEvents(); - }; - - return MergeRequestWidget; - })(); -})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/namespace_select.js b/app/assets/javascripts/namespace_select.js index 426d7f3288e..5da2db063a4 100644 --- a/app/assets/javascripts/namespace_select.js +++ b/app/assets/javascripts/namespace_select.js @@ -1,5 +1,5 @@ /* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, one-var, vars-on-top, one-var-declaration-per-line, comma-dangle, object-shorthand, no-else-return, prefer-template, quotes, prefer-arrow-callback, no-param-reassign, no-cond-assign, max-len */ -/* global Api */ +import Api from './api'; (function() { window.NamespaceSelect = (function() { diff --git a/app/assets/javascripts/new_branch_form.js b/app/assets/javascripts/new_branch_form.js index 9d614cdee3a..39fb302b644 100644 --- a/app/assets/javascripts/new_branch_form.js +++ b/app/assets/javascripts/new_branch_form.js @@ -1,4 +1,6 @@ /* eslint-disable func-names, space-before-function-paren, no-var, one-var, prefer-rest-params, max-len, vars-on-top, wrap-iife, consistent-return, comma-dangle, one-var-declaration-per-line, quotes, no-return-assign, prefer-arrow-callback, prefer-template, no-shadow, no-else-return, max-len, object-shorthand */ +import RefSelectDropdown from '~/ref_select_dropdown'; + (function() { this.NewBranchForm = (function() { function NewBranchForm(form, availableRefs) { @@ -6,7 +8,7 @@ this.branchNameError = form.find('.js-branch-name-error'); this.name = form.find('.js-branch-name'); this.ref = form.find('#ref'); - this.setupAvailableRefs(availableRefs); + new RefSelectDropdown($('.js-branch-select'), availableRefs); // eslint-disable-line no-new this.setupRestrictions(); this.addBinding(); this.init(); @@ -22,49 +24,6 @@ } }; - NewBranchForm.prototype.setupAvailableRefs = function(availableRefs) { - var $branchSelect = $('.js-branch-select'); - - $branchSelect.glDropdown({ - data: availableRefs, - filterable: true, - filterByText: true, - remote: false, - fieldName: $branchSelect.data('field-name'), - filterInput: 'input[type="search"]', - selectable: true, - isSelectable: function(branch, $el) { - return !$el.hasClass('is-active'); - }, - text: function(branch) { - return branch; - }, - id: function(branch) { - return branch; - }, - toggleLabel: function(branch) { - if (branch) { - return branch; - } - } - }); - - const $dropdownContainer = $branchSelect.closest('.dropdown'); - const $fieldInput = $(`input[name="${$branchSelect.data('field-name')}"]`, $dropdownContainer); - const $filterInput = $('input[type="search"]', $dropdownContainer); - - $filterInput.on('keyup', (e) => { - const keyCode = e.keyCode || e.which; - if (keyCode !== 13) return; - - const text = $filterInput.val(); - $fieldInput.val(text); - $('.dropdown-toggle-text', $branchSelect).text(text); - - $dropdownContainer.removeClass('open'); - }); - }; - NewBranchForm.prototype.setupRestrictions = function() { var endsWith, invalid, single, startsWith; startsWith = { diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js index f143bfbfc29..b0b1cfd6c8a 100644 --- a/app/assets/javascripts/notes.js +++ b/app/assets/javascripts/notes.js @@ -6,16 +6,17 @@ import $ from 'jquery'; import Cookies from 'js-cookie'; +import autosize from 'vendor/autosize'; +import Dropzone from 'dropzone'; +import 'vendor/jquery.caret'; // required by jquery.atwho +import 'vendor/jquery.atwho'; import CommentTypeToggle from './comment_type_toggle'; +import './autosave'; +import './dropzone_input'; +import './task_list'; -require('./autosave'); -window.autosize = require('vendor/autosize'); -window.Dropzone = require('dropzone'); -require('./dropzone_input'); -require('./gfm_auto_complete'); -require('vendor/jquery.caret'); // required by jquery.atwho -require('vendor/jquery.atwho'); -require('./task_list'); +window.autosize = autosize; +window.Dropzone = Dropzone; const normalizeNewlines = function(str) { return str.replace(/\r\n/g, '\n'); @@ -28,14 +29,14 @@ const normalizeNewlines = function(str) { Notes.interval = null; - function Notes(notes_url, note_ids, last_fetched_at, view) { + function Notes(notes_url, note_ids, last_fetched_at, view, enableGFM = true) { this.updateTargetButtons = this.updateTargetButtons.bind(this); this.updateComment = this.updateComment.bind(this); this.visibilityChange = this.visibilityChange.bind(this); this.cancelDiscussionForm = this.cancelDiscussionForm.bind(this); - this.addDiffNote = this.addDiffNote.bind(this); + this.onAddDiffNote = this.onAddDiffNote.bind(this); this.setupDiscussionNoteForm = this.setupDiscussionNoteForm.bind(this); - this.replyToDiscussionNote = this.replyToDiscussionNote.bind(this); + this.onReplyToDiscussionNote = this.onReplyToDiscussionNote.bind(this); this.removeNote = this.removeNote.bind(this); this.cancelEdit = this.cancelEdit.bind(this); this.updateNote = this.updateNote.bind(this); @@ -47,9 +48,11 @@ const normalizeNewlines = function(str) { this.keydownNoteText = this.keydownNoteText.bind(this); this.toggleCommitList = this.toggleCommitList.bind(this); this.postComment = this.postComment.bind(this); + this.clearFlashWrapper = this.clearFlash.bind(this); this.notes_url = notes_url; this.note_ids = note_ids; + this.enableGFM = enableGFM; // Used to keep track of updated notes while people are editing things this.updatedNotesTrackingMap = {}; this.last_fetched_at = last_fetched_at; @@ -57,6 +60,7 @@ const normalizeNewlines = function(str) { this.notesCountBadge || (this.notesCountBadge = $(".issuable-details").find(".notes-tab .badge")); this.basePollingInterval = 15000; this.maxPollingSteps = 4; + this.flashErrors = []; this.cleanBinding(); this.addBinding(); @@ -100,9 +104,9 @@ const normalizeNewlines = function(str) { // update the file name when an attachment is selected $(document).on("change", ".js-note-attachment-input", this.updateFormAttachment); // reply to diff/discussion notes - $(document).on("click", ".js-discussion-reply-button", this.replyToDiscussionNote); + $(document).on("click", ".js-discussion-reply-button", this.onReplyToDiscussionNote); // add diff note - $(document).on("click", ".js-add-diff-note-button", this.addDiffNote); + $(document).on("click", ".js-add-diff-note-button", this.onAddDiffNote); // hide diff note form $(document).on("click", ".js-close-discussion-note-form", this.cancelDiscussionForm); // toggle commit list @@ -285,6 +289,13 @@ const normalizeNewlines = function(str) { } }; + Notes.prototype.setupNewNote = function($note) { + // Update datetime format on the recent note + gl.utils.localTimeAgo($note.find('.js-timeago'), false); + this.collapseLongCommitList(); + this.taskList.init(); + }; + /* Render note in main comments area. @@ -298,27 +309,24 @@ const normalizeNewlines = function(str) { if (!noteEntity.valid) { if (noteEntity.errors.commands_only) { - new Flash(noteEntity.errors.commands_only, 'notice', this.parentTimeline); + this.addFlash(noteEntity.errors.commands_only, 'notice', this.parentTimeline); this.refresh(); } return; } const $note = $notesList.find(`#note_${noteEntity.id}`); - if (this.isNewNote(noteEntity)) { + if (Notes.isNewNote(noteEntity, this.note_ids)) { this.note_ids.push(noteEntity.id); const $newNote = Notes.animateAppendNote(noteEntity.html, $notesList); - // Update datetime format on the recent note - gl.utils.localTimeAgo($newNote.find('.js-timeago'), false); - this.collapseLongCommitList(); - this.taskList.init(); + this.setupNewNote($newNote); this.refresh(); return this.updateNotesCount(1); } // The server can send the same update multiple times so we need to make sure to only update once per actual update. - else if (this.isUpdatedNote(noteEntity, $note)) { + else if (Notes.isUpdatedNote(noteEntity, $note)) { const isEditing = $note.hasClass('is-editing'); const initialContent = normalizeNewlines( $note.find('.original-note-content').text().trim() @@ -339,30 +347,11 @@ const normalizeNewlines = function(str) { } else { const $updatedNote = Notes.animateUpdateNote(noteEntity.html, $note); - - // Update datetime format on the recent note - gl.utils.localTimeAgo($updatedNote.find('.js-timeago'), false); + this.setupNewNote($updatedNote); } } }; - /* - Check if note does not exists on page - */ - - Notes.prototype.isNewNote = function(noteEntity) { - return $.inArray(noteEntity.id, this.note_ids) === -1; - }; - - Notes.prototype.isUpdatedNote = function(noteEntity, $note) { - // There can be CRLF vs LF mismatches if we don't sanitize and compare the same way - const sanitizedNoteNote = normalizeNewlines(noteEntity.note); - const currentNoteText = normalizeNewlines( - $note.find('.original-note-content').text().trim() - ); - return sanitizedNoteNote !== currentNoteText; - }; - Notes.prototype.isParallelView = function() { return Cookies.get('diff_view') === 'parallel'; }; @@ -375,7 +364,7 @@ const normalizeNewlines = function(str) { Notes.prototype.renderDiscussionNote = function(noteEntity, $form) { var discussionContainer, form, row, lineType, diffAvatarContainer; - if (!this.isNewNote(noteEntity)) { + if (!Notes.isNewNote(noteEntity, this.note_ids)) { return; } this.note_ids.push(noteEntity.id); @@ -522,7 +511,7 @@ const normalizeNewlines = function(str) { Notes.prototype.setupNoteForm = function(form) { var textarea, key; - new gl.GLForm(form); + new gl.GLForm(form, this.enableGFM); textarea = form.find(".js-note-text"); key = [ "Note", @@ -551,14 +540,14 @@ const normalizeNewlines = function(str) { return this.renderNote(note); }; - Notes.prototype.addNoteError = ($form) => { + Notes.prototype.addNoteError = function($form) { let formParentTimeline; if ($form.hasClass('js-main-target-form')) { formParentTimeline = $form.parents('.timeline'); } else if ($form.hasClass('js-discussion-note-form')) { formParentTimeline = $form.closest('.discussion-notes').find('.notes'); } - return new Flash('Your comment could not be submitted! Please check your network connection and try again.', 'alert', formParentTimeline); + return this.addFlash('Your comment could not be submitted! Please check your network connection and try again.', 'alert', formParentTimeline); }; Notes.prototype.updateNoteError = $parentTimeline => new Flash('Your comment could not be updated! Please check your network connection and try again.'); @@ -593,12 +582,12 @@ const normalizeNewlines = function(str) { Updates the current note field. */ - Notes.prototype.updateNote = function(_xhr, noteEntity, _status) { + Notes.prototype.updateNote = function(noteEntity, $targetNote) { var $noteEntityEl, $note_li; // Convert returned HTML to a jQuery object so we can modify it further $noteEntityEl = $(noteEntity.html); $noteEntityEl.addClass('fade-in-full'); - this.revertNoteEditForm(); + this.revertNoteEditForm($targetNote); gl.utils.localTimeAgo($('.js-timeago', $noteEntityEl)); $noteEntityEl.renderGFM(); $noteEntityEl.find('.js-task-list-container').taskList('enable'); @@ -680,10 +669,8 @@ const normalizeNewlines = function(str) { if (this.updatedNotesTrackingMap[noteId]) { const $newNote = $(this.updatedNotesTrackingMap[noteId].html); $note.replaceWith($newNote); + this.setupNewNote($newNote); this.updatedNotesTrackingMap[noteId] = null; - - // Update datetime format on the recent note - gl.utils.localTimeAgo($newNote.find('.js-timeago'), false); } else { $note.find('.js-finish-edit-warning').hide(); @@ -794,10 +781,14 @@ const normalizeNewlines = function(str) { Shows the note form below the notes. */ - Notes.prototype.replyToDiscussionNote = function(e) { + Notes.prototype.onReplyToDiscussionNote = function(e) { + this.replyToDiscussionNote(e.target); + }; + + Notes.prototype.replyToDiscussionNote = function(target) { var form, replyLink; form = this.cleanForm(this.formClone.clone()); - replyLink = $(e.target).closest(".js-discussion-reply-button"); + replyLink = $(target).closest(".js-discussion-reply-button"); // insert the form after the button replyLink .closest('.discussion-reply-holder') @@ -867,35 +858,53 @@ const normalizeNewlines = function(str) { Sets up the form and shows it. */ - Notes.prototype.addDiffNote = function(e) { - var $link, addForm, hasNotes, lineType, newForm, nextRow, noteForm, notesContent, notesContentSelector, replyButton, row, rowCssToAdd, targetContent, isDiffCommentAvatar; + Notes.prototype.onAddDiffNote = function(e) { e.preventDefault(); - $link = $(e.currentTarget || e.target); + const link = e.currentTarget || e.target; + const $link = $(link); + const showReplyInput = !$link.hasClass('js-diff-comment-avatar'); + this.toggleDiffNote({ + target: $link, + lineType: link.dataset.lineType, + showReplyInput + }); + }; + + Notes.prototype.toggleDiffNote = function({ + target, + lineType, + forceShow, + showReplyInput = false, + }) { + var $link, addForm, hasNotes, newForm, noteForm, replyButton, row, rowCssToAdd, targetContent, isDiffCommentAvatar; + $link = $(target); row = $link.closest("tr"); - nextRow = row.next(); - hasNotes = nextRow.is(".notes_holder"); + const nextRow = row.next(); + let targetRow = row; + if (nextRow.is('.notes_holder')) { + targetRow = nextRow; + } + + hasNotes = targetRow.is(".notes_holder"); addForm = false; - notesContentSelector = ".notes_content"; + let lineTypeSelector = ''; rowCssToAdd = "<tr class=\"notes_holder js-temp-notes-holder\"><td class=\"notes_line\" colspan=\"2\"></td><td class=\"notes_content\"><div class=\"content\"></div></td></tr>"; - isDiffCommentAvatar = $link.hasClass('js-diff-comment-avatar'); // In parallel view, look inside the correct left/right pane if (this.isParallelView()) { - lineType = $link.data("lineType"); - notesContentSelector += "." + lineType; + lineTypeSelector = `.${lineType}`; rowCssToAdd = "<tr class=\"notes_holder js-temp-notes-holder\"><td class=\"notes_line old\"></td><td class=\"notes_content parallel old\"><div class=\"content\"></div></td><td class=\"notes_line new\"></td><td class=\"notes_content parallel new\"><div class=\"content\"></div></td></tr>"; } - notesContentSelector += " .content"; - notesContent = nextRow.find(notesContentSelector); + const notesContentSelector = `.notes_content${lineTypeSelector} .content`; + let notesContent = targetRow.find(notesContentSelector); - if (hasNotes && !isDiffCommentAvatar) { - nextRow.show(); - notesContent = nextRow.find(notesContentSelector); + if (hasNotes && showReplyInput) { + targetRow.show(); + notesContent = targetRow.find(notesContentSelector); if (notesContent.length) { notesContent.show(); replyButton = notesContent.find(".js-discussion-reply-button:visible"); if (replyButton.length) { - e.target = replyButton[0]; - $.proxy(this.replyToDiscussionNote, replyButton[0], e).call(); + this.replyToDiscussionNote(replyButton[0]); } else { // In parallel view, the form may not be present in one of the panes noteForm = notesContent.find(".js-discussion-note-form"); @@ -904,19 +913,19 @@ const normalizeNewlines = function(str) { } } } - } else if (!isDiffCommentAvatar) { + } else if (showReplyInput) { // add a notes row and insert the form row.after(rowCssToAdd); - nextRow = row.next(); - notesContent = nextRow.find(notesContentSelector); + targetRow = row.next(); + notesContent = targetRow.find(notesContentSelector); addForm = true; } else { - nextRow.show(); - notesContent.toggle(!notesContent.is(':visible')); + const isCurrentlyShown = targetRow.find('.content:not(:empty)').is(':visible'); + const isForced = forceShow === true || forceShow === false; + const showNow = forceShow === true || (!isCurrentlyShown && !isForced); - if (!nextRow.find('.content:not(:empty)').is(':visible')) { - nextRow.hide(); - } + targetRow.toggle(showNow); + notesContent.toggle(showNow); } if (addForm) { @@ -1101,6 +1110,15 @@ const normalizeNewlines = function(str) { }); }; + Notes.prototype.addFlash = function(...flashParams) { + this.flashErrors.push(new Flash(...flashParams)); + }; + + Notes.prototype.clearFlash = function() { + this.flashErrors.forEach(flash => flash.flashContainer.remove()); + this.flashErrors = []; + }; + Notes.prototype.cleanForm = function($form) { // Remove JS classes that are not needed here $form @@ -1115,6 +1133,25 @@ const normalizeNewlines = function(str) { return $form; }; + /** + * Check if note does not exists on page + */ + Notes.isNewNote = function(noteEntity, noteIds) { + return $.inArray(noteEntity.id, noteIds) === -1; + }; + + /** + * Check if $note already contains the `noteEntity` content + */ + Notes.isUpdatedNote = function(noteEntity, $note) { + // There can be CRLF vs LF mismatches if we don't sanitize and compare the same way + const sanitizedNoteEntityText = normalizeNewlines(noteEntity.note.trim()); + const currentNoteText = normalizeNewlines( + $note.find('.original-note-content').first().text().trim() + ); + return sanitizedNoteEntityText !== currentNoteText; + }; + Notes.checkMergeRequestStatus = function() { if (gl.utils.getPagePath(1) === 'merge_requests') { gl.mrWidget.checkStatus(); @@ -1279,6 +1316,8 @@ const normalizeNewlines = function(str) { .then((note) => { // Submission successful! remove placeholder $notesContainer.find(`#${uniqueId}`).remove(); + // Clear previous form errors + this.clearFlashWrapper(); // Check if this was discussion comment if (isDiscussionForm) { @@ -1318,7 +1357,7 @@ const normalizeNewlines = function(str) { // Show form again on UI on failure if (isDiscussionForm && $notesContainer.length) { const replyButton = $notesContainer.parent().find('.js-discussion-reply-button'); - $.proxy(this.replyToDiscussionNote, replyButton[0], { target: replyButton[0] }).call(); + this.replyToDiscussionNote(replyButton[0]); $form = $notesContainer.parent().find('form'); } @@ -1368,7 +1407,7 @@ const normalizeNewlines = function(str) { gl.utils.ajaxPost(formAction, formData) .then((note) => { // Submission successful! render final note element - this.updateNote(null, note, null); + this.updateNote(note, $editingNote); }) .fail(() => { // Submission failed, revert back to original note diff --git a/app/assets/javascripts/pager.js b/app/assets/javascripts/pager.js index 5f6bc902cf8..0ef20af9260 100644 --- a/app/assets/javascripts/pager.js +++ b/app/assets/javascripts/pager.js @@ -1,5 +1,5 @@ -require('~/lib/utils/common_utils'); -require('~/lib/utils/url_utility'); +import '~/lib/utils/common_utils'; +import '~/lib/utils/url_utility'; (() => { const ENDLESS_SCROLL_BOTTOM_PX = 400; diff --git a/app/assets/javascripts/pipeline_schedules/components/interval_pattern_input.js b/app/assets/javascripts/pipeline_schedules/components/interval_pattern_input.js index 152e75b747e..4d623763ca7 100644 --- a/app/assets/javascripts/pipeline_schedules/components/interval_pattern_input.js +++ b/app/assets/javascripts/pipeline_schedules/components/interval_pattern_input.js @@ -24,9 +24,6 @@ export default { }; }, computed: { - showUnsetWarning() { - return this.cronInterval === ''; - }, intervalIsPreset() { return _.contains(this.cronIntervalPresets, this.cronInterval); }, @@ -63,67 +60,75 @@ export default { }, template: ` <div class="interval-pattern-form-group"> - <input - id="custom" - class="label-light" - type="radio" - :name="inputNameAttribute" - :value="cronInterval" - :checked="isEditable" - @click="toggleCustomInput(true)" - /> + <div class="cron-preset-radio-input"> + <input + id="custom" + class="label-light" + type="radio" + :name="inputNameAttribute" + :value="cronInterval" + :checked="isEditable" + @click="toggleCustomInput(true)" + /> - <label for="custom"> - Custom - </label> + <label for="custom"> + Custom + </label> - <span class="cron-syntax-link-wrap"> - (<a :href="cronSyntaxUrl" target="_blank">Cron syntax</a>) - </span> + <span class="cron-syntax-link-wrap"> + (<a :href="cronSyntaxUrl" target="_blank">Cron syntax</a>) + </span> + </div> - <input - id="every-day" - class="label-light" - type="radio" - v-model="cronInterval" - :name="inputNameAttribute" - :value="cronIntervalPresets.everyDay" - @click="toggleCustomInput(false)" - /> + <div class="cron-preset-radio-input"> + <input + id="every-day" + class="label-light" + type="radio" + v-model="cronInterval" + :name="inputNameAttribute" + :value="cronIntervalPresets.everyDay" + @click="toggleCustomInput(false)" + /> - <label class="label-light" for="every-day"> - Every day (at 4:00am) - </label> + <label class="label-light" for="every-day"> + Every day (at 4:00am) + </label> + </div> - <input - id="every-week" - class="label-light" - type="radio" - v-model="cronInterval" - :name="inputNameAttribute" - :value="cronIntervalPresets.everyWeek" - @click="toggleCustomInput(false)" - /> + <div class="cron-preset-radio-input"> + <input + id="every-week" + class="label-light" + type="radio" + v-model="cronInterval" + :name="inputNameAttribute" + :value="cronIntervalPresets.everyWeek" + @click="toggleCustomInput(false)" + /> - <label class="label-light" for="every-week"> - Every week (Sundays at 4:00am) - </label> + <label class="label-light" for="every-week"> + Every week (Sundays at 4:00am) + </label> + </div> - <input - id="every-month" - class="label-light" - type="radio" - v-model="cronInterval" - :name="inputNameAttribute" - :value="cronIntervalPresets.everyMonth" - @click="toggleCustomInput(false)" - /> + <div class="cron-preset-radio-input"> + <input + id="every-month" + class="label-light" + type="radio" + v-model="cronInterval" + :name="inputNameAttribute" + :value="cronIntervalPresets.everyMonth" + @click="toggleCustomInput(false)" + /> - <label class="label-light" for="every-month"> - Every month (on the 1st at 4:00am) - </label> + <label class="label-light" for="every-month"> + Every month (on the 1st at 4:00am) + </label> + </div> - <div class="cron-interval-input-wrapper col-md-6"> + <div class="cron-interval-input-wrapper"> <input id="schedule_cron" class="form-control inline cron-interval-input" @@ -135,9 +140,6 @@ export default { :disabled="!isEditable" /> </div> - <span class="cron-unset-status col-md-3" v-if="showUnsetWarning"> - Schedule not yet set - </span> </div> `, }; diff --git a/app/assets/javascripts/pipeline_schedules/components/target_branch_dropdown.js b/app/assets/javascripts/pipeline_schedules/components/target_branch_dropdown.js index 22e746ad2c3..0c3926d76b5 100644 --- a/app/assets/javascripts/pipeline_schedules/components/target_branch_dropdown.js +++ b/app/assets/javascripts/pipeline_schedules/components/target_branch_dropdown.js @@ -3,7 +3,7 @@ export default class TargetBranchDropdown { this.$dropdown = $('.js-target-branch-dropdown'); this.$dropdownToggle = this.$dropdown.find('.dropdown-toggle-text'); this.$input = $('#schedule_ref'); - this.initialValue = this.$input.val(); + this.initDefaultBranch(); this.initDropdown(); } @@ -29,13 +29,23 @@ export default class TargetBranchDropdown { } setDropdownToggle() { - if (this.initialValue) { - this.$dropdownToggle.text(this.initialValue); + const initialValue = this.$input.val(); + + this.$dropdownToggle.text(initialValue); + } + + initDefaultBranch() { + const initialValue = this.$input.val(); + const defaultBranch = this.$dropdown.data('defaultBranch'); + + if (!initialValue) { + this.$input.val(defaultBranch); } } updateInputValue({ selectedObj, e }) { e.preventDefault(); + this.$input.val(selectedObj.name); gl.pipelineScheduleFieldErrors.updateFormValidityState(); } diff --git a/app/assets/javascripts/pipeline_schedules/components/timezone_dropdown.js b/app/assets/javascripts/pipeline_schedules/components/timezone_dropdown.js index c70e0502cf8..95ed9c7dc21 100644 --- a/app/assets/javascripts/pipeline_schedules/components/timezone_dropdown.js +++ b/app/assets/javascripts/pipeline_schedules/components/timezone_dropdown.js @@ -1,12 +1,14 @@ /* eslint-disable class-methods-use-this */ +const defaultTimezone = 'UTC'; + export default class TimezoneDropdown { constructor() { this.$dropdown = $('.js-timezone-dropdown'); this.$dropdownToggle = this.$dropdown.find('.dropdown-toggle-text'); this.$input = $('#schedule_cron_timezone'); this.timezoneData = this.$dropdown.data('data'); - this.initialValue = this.$input.val(); + this.initDefaultTimezone(); this.initDropdown(); } @@ -42,12 +44,20 @@ export default class TimezoneDropdown { return `[UTC ${this.formatUtcOffset(item.offset)}] ${item.name}`; } - setDropdownToggle() { - if (this.initialValue) { - this.$dropdownToggle.text(this.initialValue); + initDefaultTimezone() { + const initialValue = this.$input.val(); + + if (!initialValue) { + this.$input.val(defaultTimezone); } } + setDropdownToggle() { + const initialValue = this.$input.val(); + + this.$dropdownToggle.text(initialValue); + } + updateInputValue({ selectedObj, e }) { e.preventDefault(); this.$input.val(selectedObj.identifier); diff --git a/app/assets/javascripts/pipelines/components/pipeline_url.js b/app/assets/javascripts/pipelines/components/pipeline_url.js index ea8aaca6c9c..7cd2e0f9366 100644 --- a/app/assets/javascripts/pipelines/components/pipeline_url.js +++ b/app/assets/javascripts/pipelines/components/pipeline_url.js @@ -1,3 +1,5 @@ +import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; + export default { props: [ 'pipeline', @@ -7,6 +9,9 @@ export default { return !!this.pipeline.user; }, }, + components: { + userAvatarLink, + }, template: ` <td> <a @@ -15,18 +20,13 @@ export default { <span class="pipeline-id">#{{pipeline.id}}</span> </a> <span>by</span> - <a - class="js-pipeline-url-user" + <user-avatar-link v-if="user" - :href="pipeline.user.web_url"> - <img - v-if="user" - class="avatar has-tooltip s20 " - :title="pipeline.user.name" - data-container="body" - :src="pipeline.user.avatar_url" - > - </a> + class="js-pipeline-url-user" + :link-href="pipeline.user.web_url" + :img-src="pipeline.user.avatar_url" + :tooltip-text="pipeline.user.name" + /> <span v-if="!user" class="js-pipeline-url-api api"> diff --git a/app/assets/javascripts/pipelines/pipelines.js b/app/assets/javascripts/pipelines/pipelines.js index 050551e5075..d6952d1ee5f 100644 --- a/app/assets/javascripts/pipelines/pipelines.js +++ b/app/assets/javascripts/pipelines/pipelines.js @@ -1,12 +1,12 @@ import Visibility from 'visibilityjs'; import PipelinesService from './services/pipelines_service'; import eventHub from './event_hub'; -import PipelinesTableComponent from '../vue_shared/components/pipelines_table'; +import pipelinesTableComponent from '../vue_shared/components/pipelines_table'; import tablePagination from '../vue_shared/components/table_pagination.vue'; -import EmptyState from './components/empty_state.vue'; -import ErrorState from './components/error_state.vue'; -import NavigationTabs from './components/navigation_tabs'; -import NavigationControls from './components/nav_controls'; +import emptyState from './components/empty_state.vue'; +import errorState from './components/error_state.vue'; +import navigationTabs from './components/navigation_tabs'; +import navigationControls from './components/nav_controls'; import loadingIcon from '../vue_shared/components/loading_icon.vue'; import Poll from '../lib/utils/poll'; @@ -20,11 +20,11 @@ export default { components: { tablePagination, - 'pipelines-table-component': PipelinesTableComponent, - 'empty-state': EmptyState, - 'error-state': ErrorState, - 'navigation-tabs': NavigationTabs, - 'navigation-controls': NavigationControls, + pipelinesTableComponent, + emptyState, + errorState, + navigationTabs, + navigationControls, loadingIcon, }, @@ -52,6 +52,7 @@ export default { hasError: false, isMakingRequest: false, updateGraphDropdown: false, + hasMadeRequest: false, }; }, @@ -78,6 +79,7 @@ export default { shouldRenderEmptyState() { return !this.isLoading && !this.hasError && + this.hasMadeRequest && !this.state.pipelines.length && (this.scope === 'all' || this.scope === null); }, @@ -150,6 +152,10 @@ export default { if (!Visibility.hidden()) { this.isLoading = true; poll.makeRequest(); + } else { + // If tab is not visible we need to make the first request so we don't show the empty + // state without knowing if there are any pipelines + this.fetchPipelines(); } Visibility.change(() => { @@ -202,6 +208,7 @@ export default { this.isLoading = false; this.updateGraphDropdown = true; + this.hasMadeRequest = true; }, errorCallback() { diff --git a/app/assets/javascripts/profile/profile_bundle.js b/app/assets/javascripts/profile/profile_bundle.js index 15d32825583..ff35a9bcb83 100644 --- a/app/assets/javascripts/profile/profile_bundle.js +++ b/app/assets/javascripts/profile/profile_bundle.js @@ -1,2 +1,2 @@ -require('./gl_crop'); -require('./profile'); +import './gl_crop'; +import './profile'; diff --git a/app/assets/javascripts/project_edit.js b/app/assets/javascripts/project_edit.js new file mode 100644 index 00000000000..d7d284b6c86 --- /dev/null +++ b/app/assets/javascripts/project_edit.js @@ -0,0 +1,9 @@ +export default function setupProjectEdit() { + const $transferForm = $('.js-project-transfer-form'); + const $selectNamespace = $transferForm.find('.select2'); + + $selectNamespace.on('change', () => { + $transferForm.find(':submit').prop('disabled', !$selectNamespace.val()); + }); + $selectNamespace.trigger('change'); +} diff --git a/app/assets/javascripts/project_select.js b/app/assets/javascripts/project_select.js index 3c1c1e7dceb..0ff0a3b6cc4 100644 --- a/app/assets/javascripts/project_select.js +++ b/app/assets/javascripts/project_select.js @@ -1,5 +1,5 @@ /* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, no-var, comma-dangle, object-shorthand, one-var, one-var-declaration-per-line, no-else-return, quotes, max-len */ -/* global Api */ +import Api from './api'; (function() { this.ProjectSelect = (function() { diff --git a/app/assets/javascripts/protected_branches/protected_branches_bundle.js b/app/assets/javascripts/protected_branches/protected_branches_bundle.js index 849c1e31623..874d70a1431 100644 --- a/app/assets/javascripts/protected_branches/protected_branches_bundle.js +++ b/app/assets/javascripts/protected_branches/protected_branches_bundle.js @@ -1,5 +1,5 @@ -require('./protected_branch_access_dropdown'); -require('./protected_branch_create'); -require('./protected_branch_dropdown'); -require('./protected_branch_edit'); -require('./protected_branch_edit_list'); +import './protected_branch_access_dropdown'; +import './protected_branch_create'; +import './protected_branch_dropdown'; +import './protected_branch_edit'; +import './protected_branch_edit_list'; diff --git a/app/assets/javascripts/raven/index.js b/app/assets/javascripts/raven/index.js index 5325e495815..edc2293915f 100644 --- a/app/assets/javascripts/raven/index.js +++ b/app/assets/javascripts/raven/index.js @@ -6,6 +6,10 @@ const index = function index() { currentUserId: gon.current_user_id, whitelistUrls: [gon.gitlab_url], isProduction: process.env.NODE_ENV, + release: gon.revision, + tags: { + revision: gon.revision, + }, }); return RavenConfig; diff --git a/app/assets/javascripts/raven/raven_config.js b/app/assets/javascripts/raven/raven_config.js index c7fe1cacf49..ae54fa5f1a9 100644 --- a/app/assets/javascripts/raven/raven_config.js +++ b/app/assets/javascripts/raven/raven_config.js @@ -1,4 +1,5 @@ import Raven from 'raven-js'; +import $ from 'jquery'; const IGNORE_ERRORS = [ // Random plugins/extensions @@ -57,6 +58,8 @@ const RavenConfig = { configure() { Raven.config(this.options.sentryDsn, { + release: this.options.release, + tags: this.options.tags, whitelistUrls: this.options.whitelistUrls, environment: this.options.isProduction ? 'production' : 'development', ignoreErrors: this.IGNORE_ERRORS, @@ -72,7 +75,7 @@ const RavenConfig = { }, bindRavenErrors() { - window.$(document).on('ajaxError.raven', this.handleRavenErrors); + $(document).on('ajaxError.raven', this.handleRavenErrors); }, handleRavenErrors(event, req, config, err) { diff --git a/app/assets/javascripts/ref_select_dropdown.js b/app/assets/javascripts/ref_select_dropdown.js new file mode 100644 index 00000000000..215cd6fbdfd --- /dev/null +++ b/app/assets/javascripts/ref_select_dropdown.js @@ -0,0 +1,46 @@ +class RefSelectDropdown { + constructor($dropdownButton, availableRefs) { + $dropdownButton.glDropdown({ + data: availableRefs, + filterable: true, + filterByText: true, + remote: false, + fieldName: $dropdownButton.data('field-name'), + filterInput: 'input[type="search"]', + selectable: true, + isSelectable(branch, $el) { + return !$el.hasClass('is-active'); + }, + text(branch) { + return branch; + }, + id(branch) { + return branch; + }, + toggleLabel(branch) { + return branch; + }, + }); + + const $dropdownContainer = $dropdownButton.closest('.dropdown'); + const $fieldInput = $(`input[name="${$dropdownButton.data('field-name')}"]`, $dropdownContainer); + const $filterInput = $('input[type="search"]', $dropdownContainer); + + $filterInput.on('keyup', (e) => { + const keyCode = e.keyCode || e.which; + if (keyCode !== 13) return; + + const ref = $filterInput.val().trim(); + if (ref === '') { + return; + } + + $fieldInput.val(ref); + $('.dropdown-toggle-text', $dropdownButton).text(ref); + + $dropdownContainer.removeClass('open'); + }); + } +} + +export default RefSelectDropdown; diff --git a/app/assets/javascripts/search.js b/app/assets/javascripts/search.js index 39e4006ac4e..05caf177aec 100644 --- a/app/assets/javascripts/search.js +++ b/app/assets/javascripts/search.js @@ -1,6 +1,6 @@ /* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, one-var-declaration-per-line, object-shorthand, prefer-arrow-callback, comma-dangle, prefer-template, quotes, no-else-return, max-len */ /* global Flash */ -/* global Api */ +import Api from './api'; (function() { this.Search = (function() { diff --git a/app/assets/javascripts/shortcuts_blob.js b/app/assets/javascripts/shortcuts_blob.js index bfe90aef71e..ccbf7c59165 100644 --- a/app/assets/javascripts/shortcuts_blob.js +++ b/app/assets/javascripts/shortcuts_blob.js @@ -1,14 +1,14 @@ /* global Mousetrap */ /* global Shortcuts */ -require('./shortcuts'); +import './shortcuts'; const defaults = { skipResetBindings: false, fileBlobPermalinkUrl: null, }; -class ShortcutsBlob extends Shortcuts { +export default class ShortcutsBlob extends Shortcuts { constructor(opts) { const options = Object.assign({}, defaults, opts); super(options.skipResetBindings); @@ -25,5 +25,3 @@ class ShortcutsBlob extends Shortcuts { } } } - -module.exports = ShortcutsBlob; diff --git a/app/assets/javascripts/shortcuts_find_file.js b/app/assets/javascripts/shortcuts_find_file.js index a27ac264a5c..b18b6139b35 100644 --- a/app/assets/javascripts/shortcuts_find_file.js +++ b/app/assets/javascripts/shortcuts_find_file.js @@ -2,7 +2,7 @@ /* global Mousetrap */ /* global ShortcutsNavigation */ -require('./shortcuts_navigation'); +import './shortcuts_navigation'; (function() { var extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, diff --git a/app/assets/javascripts/shortcuts_issuable.js b/app/assets/javascripts/shortcuts_issuable.js index fe58e98cee5..dace03554e8 100644 --- a/app/assets/javascripts/shortcuts_issuable.js +++ b/app/assets/javascripts/shortcuts_issuable.js @@ -3,8 +3,8 @@ /* global ShortcutsNavigation */ /* global sidebar */ -require('mousetrap'); -require('./shortcuts_navigation'); +import 'mousetrap'; +import './shortcuts_navigation'; (function() { var extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, @@ -38,7 +38,7 @@ require('./shortcuts_navigation'); } ShortcutsIssuable.prototype.replyWithSelectedText = function() { - var quote, documentFragment, selected, separator; + var quote, documentFragment, el, selected, separator; var replyField = $('.js-main-target-form #note_note'); documentFragment = window.gl.utils.getSelectedFragment(); @@ -47,10 +47,8 @@ require('./shortcuts_navigation'); return; } - // If the documentFragment contains more than just Markdown, don't copy as GFM. - if (documentFragment.querySelector('.md, .wiki')) return; - - selected = window.gl.CopyAsGFM.nodeToGFM(documentFragment); + el = window.gl.CopyAsGFM.transformGFMSelection(documentFragment.cloneNode(true)); + selected = window.gl.CopyAsGFM.nodeToGFM(el); if (selected.trim() === "") { return; diff --git a/app/assets/javascripts/shortcuts_navigation.js b/app/assets/javascripts/shortcuts_navigation.js index c74ab0afd0c..55bae0c08a1 100644 --- a/app/assets/javascripts/shortcuts_navigation.js +++ b/app/assets/javascripts/shortcuts_navigation.js @@ -1,9 +1,9 @@ /* eslint-disable func-names, space-before-function-paren, max-len, no-var, one-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, prefer-arrow-callback, consistent-return, no-return-assign */ /* global Mousetrap */ /* global Shortcuts */ -import findAndFollowLink from './shortcuts_dashboard_navigation'; -require('./shortcuts'); +import findAndFollowLink from './shortcuts_dashboard_navigation'; +import './shortcuts'; (function() { var extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, diff --git a/app/assets/javascripts/shortcuts_network.js b/app/assets/javascripts/shortcuts_network.js index 4c2bf8bf001..cc44082efa9 100644 --- a/app/assets/javascripts/shortcuts_network.js +++ b/app/assets/javascripts/shortcuts_network.js @@ -2,7 +2,7 @@ /* global Mousetrap */ /* global ShortcutsNavigation */ -require('./shortcuts_navigation'); +import './shortcuts_navigation'; (function() { var extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, diff --git a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.js b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.js index 1488a66c695..da4abf0b68f 100644 --- a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.js +++ b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.js @@ -69,10 +69,11 @@ export default { <div> <assignee-title :number-of-assignees="store.assignees.length" - :loading="loading" + :loading="loading || store.isFetching.assignees" :editable="store.editable" /> <assignees + v-if="!store.isFetching.assignees" class="value" :root-path="store.rootPath" :users="store.assignees" diff --git a/app/assets/javascripts/sidebar/stores/sidebar_store.js b/app/assets/javascripts/sidebar/stores/sidebar_store.js index 2d44c05bb8d..3356dd0191f 100644 --- a/app/assets/javascripts/sidebar/stores/sidebar_store.js +++ b/app/assets/javascripts/sidebar/stores/sidebar_store.js @@ -10,6 +10,9 @@ export default class SidebarStore { this.humanTimeEstimate = ''; this.humanTimeSpent = ''; this.assignees = []; + this.isFetching = { + assignees: true, + }; SidebarStore.singleton = this; } @@ -18,6 +21,7 @@ export default class SidebarStore { } setAssigneeData(data) { + this.isFetching.assignees = false; if (data.assignees) { this.assignees = data.assignees; } diff --git a/app/assets/javascripts/task_list.js b/app/assets/javascripts/task_list.js index b1402c0a880..3392cb9da29 100644 --- a/app/assets/javascripts/task_list.js +++ b/app/assets/javascripts/task_list.js @@ -1,5 +1,6 @@ /* global Flash */ -require('vendor/task_list'); + +import 'vendor/task_list'; class TaskList { constructor(options = {}) { diff --git a/app/assets/javascripts/templates/issuable_template_selector.js b/app/assets/javascripts/templates/issuable_template_selector.js index e62f429f1ae..9dd14488f22 100644 --- a/app/assets/javascripts/templates/issuable_template_selector.js +++ b/app/assets/javascripts/templates/issuable_template_selector.js @@ -1,5 +1,5 @@ /* eslint-disable comma-dangle, max-len, no-useless-return, no-param-reassign, max-len */ -/* global Api */ +import Api from '../api'; import TemplateSelector from '../blob/template_selector'; diff --git a/app/assets/javascripts/terminal/terminal_bundle.js b/app/assets/javascripts/terminal/terminal_bundle.js index 13cf3a10a38..134522ef961 100644 --- a/app/assets/javascripts/terminal/terminal_bundle.js +++ b/app/assets/javascripts/terminal/terminal_bundle.js @@ -1,7 +1,9 @@ -require('vendor/xterm/encoding-indexes.js'); -require('vendor/xterm/encoding.js'); -window.Terminal = require('vendor/xterm/xterm.js'); -require('vendor/xterm/fit.js'); -require('./terminal.js'); +import 'vendor/xterm/encoding-indexes'; +import 'vendor/xterm/encoding'; +import Terminal from 'vendor/xterm/xterm'; +import 'vendor/xterm/fit'; +import './terminal'; + +window.Terminal = Terminal; $(() => new gl.Terminal({ selector: '#terminal' })); diff --git a/app/assets/javascripts/test.js b/app/assets/javascripts/test.js new file mode 100644 index 00000000000..c4c7918a68f --- /dev/null +++ b/app/assets/javascripts/test.js @@ -0,0 +1 @@ +$.fx.off = true; diff --git a/app/assets/javascripts/users/users_bundle.js b/app/assets/javascripts/users/users_bundle.js index 580e2d84be5..a38ce4eb25e 100644 --- a/app/assets/javascripts/users/users_bundle.js +++ b/app/assets/javascripts/users/users_bundle.js @@ -1 +1 @@ -require('./calendar'); +import './calendar'; diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js index 8119a8cd000..aea3592c6ba 100644 --- a/app/assets/javascripts/users_select.js +++ b/app/assets/javascripts/users_select.js @@ -50,7 +50,11 @@ function UsersSelect(currentUser, els) { $collapsedSidebar = $block.find('.sidebar-collapsed-user'); $loading = $block.find('.block-loading').fadeOut(); selectedIdDefault = (defaultNullUser && showNullUser) ? 0 : null; - selectedId = $dropdown.data('selected') || selectedIdDefault; + selectedId = $dropdown.data('selected'); + + if (selectedId === undefined) { + selectedId = selectedIdDefault; + } const assignYourself = function () { const unassignedSelected = $dropdown.closest('.selectbox') @@ -417,14 +421,24 @@ function UsersSelect(currentUser, els) { selected = $dropdown.closest('.selectbox').find("input[name='" + ($dropdown.data('field-name')) + "']").val(); return assignTo(selected); } + + // Automatically close dropdown after assignee is selected + // since CE has no multiple assignees + // EE does not have a max-select + if ($dropdown.data('max-select') && + getSelected().length === $dropdown.data('max-select')) { + // Close the dropdown + $dropdown.dropdown('toggle'); + } }, id: function (user) { return user.id; }, opened: function(e) { const $el = $(e.currentTarget); - if ($dropdown.hasClass('js-issue-board-sidebar')) { - selectedId = parseInt($dropdown[0].dataset.selected, 10) || selectedIdDefault; + const selected = getSelected(); + if ($dropdown.hasClass('js-issue-board-sidebar') && selected.length === 0) { + this.addInput($dropdown.data('field-name'), 0, {}); } $el.find('.is-active').removeClass('is-active'); @@ -432,8 +446,10 @@ function UsersSelect(currentUser, els) { $el.find(`li[data-user-id="${id}"] .dropdown-menu-user-link`).addClass('is-active'); } - if ($selectbox[0]) { + if (selected.length > 0) { getSelected().forEach(selectedId => highlightSelected(selectedId)); + } else if ($dropdown.hasClass('js-issue-board-sidebar')) { + highlightSelected(0); } else { highlightSelected(selectedId); } @@ -444,15 +460,19 @@ function UsersSelect(currentUser, els) { username = user.username ? "@" + user.username : ""; avatar = user.avatar_url ? user.avatar_url : false; - let selected = user.id === parseInt(selectedId, 10); + let selected = false; if (this.multiSelect) { + selected = getSelected().find(u => user.id === u); + const fieldName = this.fieldName; const field = $dropdown.closest('.selectbox').find("input[name='" + fieldName + "'][value='" + user.id + "']"); if (field.length) { selected = true; } + } else { + selected = user.id === selectedId; } img = ""; diff --git a/app/assets/javascripts/version_check_image.js b/app/assets/javascripts/version_check_image.js index d4f716acb72..88ba991af47 100644 --- a/app/assets/javascripts/version_check_image.js +++ b/app/assets/javascripts/version_check_image.js @@ -1,4 +1,4 @@ -class VersionCheckImage { +export default class VersionCheckImage { static bindErrorEvent(imageElement) { imageElement.off('error').on('error', () => imageElement.hide()); } @@ -6,5 +6,3 @@ class VersionCheckImage { window.gl = window.gl || {}; gl.VersionCheckImage = VersionCheckImage; - -module.exports = VersionCheckImage; diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_deployment.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_deployment.js index 8b59e018836..e8e22ad93a5 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_deployment.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_deployment.js @@ -56,7 +56,7 @@ export default { <div class="ci-widget"> <div class="ci-status-icon ci-status-icon-success"> <span class="js-icon-link icon-link"> - <span + <span class="ci-status-icon" v-html="svg" aria-hidden="true"></span> </span> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.js index 9e7299fcdeb..f8b3fb748ae 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.js @@ -1,4 +1,4 @@ -require('../../lib/utils/text_utility'); +import '../../lib/utils/text_utility'; export default { name: 'MRWidgetHeader', @@ -92,10 +92,7 @@ export default { :class="{'label-truncated has-tooltip': isBranchTitleLong(mr.targetBranch)}" :title="isBranchTitleLong(mr.targetBranch) ? mr.targetBranch : ''" data-placement="bottom"> - <a - :href="mr.targetBranchPath"> - {{mr.targetBranch}} - </a> + <a :href="mr.targetBranchPath">{{mr.targetBranch}}</a> </span> </strong> <span diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.js index 517838f92ac..c02e10128e2 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.js @@ -31,7 +31,7 @@ export default { <div class="mr-widget-heading"> <div class="ci-widget"> <template v-if="hasCIError"> - <div class="ci-status-icon ci-status-icon-failed js-ci-error"> + <div class="ci-status-icon ci-status-icon-failed ci-error js-ci-error"> <span class="js-icon-link icon-link"> <span v-html="svg" diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.js index fcccb17f58d..4063859d5d0 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.js @@ -1,8 +1,23 @@ +import eventHub from '../../event_hub'; + export default { name: 'MRWidgetAutoMergeFailed', props: { mr: { type: Object, required: true }, }, + data() { + return { + isRefreshing: false, + }; + }, + methods: { + refreshWidget() { + this.isRefreshing = true; + eventHub.$emit('MRWidgetUpdateRequested', () => { + this.isRefreshing = false; + }); + }, + }, template: ` <div class="mr-widget-body"> <button @@ -13,8 +28,19 @@ export default { </button> <span class="bold danger"> This merge request failed to be merged automatically. + <button + @click="refreshWidget" + :class="{ disabled: isRefreshing }" + type="button" + class="btn btn-xs btn-default"> + <i + v-if="isRefreshing" + class="fa fa-spinner fa-spin" + aria-hidden="true" /> + Refresh + </button> </span> - <div class="merge-error-text"> + <div class="merge-error-text danger bold"> {{mr.mergeError}} </div> </div> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_nothing_to_merge.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_nothing_to_merge.js index 8c4535f1337..375a382615a 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_nothing_to_merge.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_nothing_to_merge.js @@ -1,17 +1,42 @@ +import emptyStateSVG from 'icons/_mr_widget_empty_state.svg'; + export default { name: 'MRWidgetNothingToMerge', + props: { + mr: { + type: Object, + required: true, + }, + }, + data() { + return { emptyStateSVG }; + }, template: ` - <div class="mr-widget-body"> - <button - type="button" - class="btn btn-success btn-small" - disabled="true"> - Merge - </button> - <span class="bold"> - There is nothing to merge from source branch into target branch. - Please push new commits or use a different branch. - </span> + <div class="mr-widget-body empty-state"> + <div class="row"> + <div class="artwork col-sm-5 col-sm-push-7 col-xs-12 text-center"> + <span v-html="emptyStateSVG"></span> + </div> + <div class="text col-sm-7 col-sm-pull-5 col-xs-12"> + <span> + Merge requests are a place to propose changes you have made to a project + and discuss those changes with others. + </span> + <p> + Interested parties can even contribute by pushing commits if they want to. + </p> + <p> + Currently there are no changes in this merge request's source branch. + Please push new commits or use a different branch. + </p> + <a + v-if="mr.newBlobPath" + :href="mr.newBlobPath" + class="btn btn-inverted btn-save"> + Create file + </a> + </div> + </div> </div> `, }; diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js index ebcc03e531b..d866d4e94b0 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js @@ -33,7 +33,7 @@ export default { return this.useCommitMessageWithDescription ? withoutDesc : withDesc; }, mergeButtonClass() { - const defaultClass = 'btn btn-success accept-merge-request'; + const defaultClass = 'btn btn-small btn-success accept-merge-request'; const failedClass = `${defaultClass} btn-danger`; const inActionClass = `${defaultClass} btn-info`; const { pipeline, isPipelineActive, isPipelineFailed, hasCI, ciStatus } = this.mr; @@ -210,7 +210,7 @@ export default { v-if="shouldShowMergeOptionsDropdown" :disabled="isMergeButtonDisabled" type="button" - class="btn btn-info dropdown-toggle" + class="btn btn-small btn-info dropdown-toggle" data-toggle="dropdown"> <i class="fa fa-caret-down" diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js index 5452e19bd8e..99600b6664e 100644 --- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js +++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js @@ -175,7 +175,6 @@ export default { }); }, handleMounted() { - this.checkStatus(); this.setFavicon(); this.initDeploymentsPolling(); }, diff --git a/app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js b/app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js index 42493be3372..79c3d335679 100644 --- a/app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js +++ b/app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js @@ -6,7 +6,7 @@ Vue.use(VueResource); export default class MRWidgetService { constructor(endpoints) { this.mergeResource = Vue.resource(endpoints.mergePath); - this.mergeCheckResource = Vue.resource(endpoints.mergeCheckPath); + this.mergeCheckResource = Vue.resource(endpoints.statusPath); this.cancelAutoMergeResource = Vue.resource(endpoints.cancelAutoMergePath); this.removeWIPResource = Vue.resource(endpoints.removeWIPPath); this.removeSourceBranchResource = Vue.resource(endpoints.sourceBranchPath); diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js b/app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js index fb78ea92da1..7c15abfff10 100644 --- a/app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js +++ b/app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js @@ -11,10 +11,6 @@ export default function deviseState(data) { return 'conflicts'; } else if (data.work_in_progress) { return 'workInProgress'; - } else if (this.mergeWhenPipelineSucceeds) { - return this.mergeError ? 'autoMergeFailed' : 'mergeWhenPipelineSucceeds'; - } else if (!this.canMerge) { - return 'notAllowedToMerge'; } else if (this.onlyAllowMergeIfPipelineSucceeds && this.isPipelineFailed) { return 'pipelineFailed'; } else if (this.hasMergeableDiscussionsState) { @@ -23,6 +19,10 @@ export default function deviseState(data) { return 'pipelineBlocked'; } else if (this.hasSHAChanged) { return 'shaMismatch'; + } else if (this.mergeWhenPipelineSucceeds) { + return this.mergeError ? 'autoMergeFailed' : 'mergeWhenPipelineSucceeds'; + } else if (!this.canMerge) { + return 'notAllowedToMerge'; } else if (this.canBeMerged) { return 'readyToMerge'; } diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js index 05e67706983..c07bd25e6fd 100644 --- a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js +++ b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js @@ -4,7 +4,7 @@ import { getStateKey } from '../dependencies'; export default class MergeRequestStore { constructor(data) { - this.startingSha = data.diff_head_sha; + this.sha = data.diff_head_sha; this.setData(data); } @@ -16,7 +16,6 @@ export default class MergeRequestStore { this.targetBranch = data.target_branch; this.sourceBranch = data.source_branch; this.mergeStatus = data.merge_status; - this.sha = data.diff_head_sha; this.commitMessage = data.merge_commit_message; this.commitMessageWithDescription = data.merge_commit_message_with_description; this.commitsCount = data.commits_count; @@ -58,6 +57,7 @@ export default class MergeRequestStore { this.statusPath = data.status_path; this.emailPatchesPath = data.email_patches_path; this.plainDiffPath = data.plain_diff_path; + this.newBlobPath = data.new_blob_path; this.createIssueToResolveDiscussionsPath = data.create_issue_to_resolve_discussions_path; this.mergeCheckPath = data.merge_check_path; this.mergeActionsContentPath = data.commit_change_content_path; @@ -68,7 +68,7 @@ export default class MergeRequestStore { this.canMerge = !!data.merge_path; this.canCreateIssue = currentUser.can_create_issue || false; this.canCancelAutomaticMerge = !!data.cancel_merge_when_pipeline_succeeds_path; - this.hasSHAChanged = this.sha !== this.startingSha; + this.hasSHAChanged = this.sha !== data.diff_head_sha; this.canBeMerged = data.can_be_merged || false; // Cherry-pick and Revert actions related diff --git a/app/assets/javascripts/vue_shared/components/commit.js b/app/assets/javascripts/vue_shared/components/commit.js index 9b060a0a35f..23bc5fbc034 100644 --- a/app/assets/javascripts/vue_shared/components/commit.js +++ b/app/assets/javascripts/vue_shared/components/commit.js @@ -1,4 +1,5 @@ import commitIconSvg from 'icons/_icon_commit.svg'; +import userAvatarLink from './user_avatar/user_avatar_link.vue'; export default { props: { @@ -110,6 +111,9 @@ export default { return { commitIconSvg }; }, + components: { + userAvatarLink, + }, template: ` <div class="branch-commit"> @@ -133,16 +137,14 @@ export default { <p class="commit-title"> <span v-if="title"> - <a v-if="hasAuthor" + <user-avatar-link + v-if="hasAuthor" class="avatar-image-container" - :href="author.web_url"> - <img - class="avatar has-tooltip s20" - :src="author.avatar_url" - :alt="userImageAltDescription" - :title="author.username" /> - </a> - + :link-href="author.web_url" + :img-src="author.avatar_url" + :img-alt="userImageAltDescription" + :tooltip-text="author.username" + /> <a class="commit-row-message" :href="commitUrl"> {{title}} diff --git a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue new file mode 100644 index 00000000000..b8db6afda12 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue @@ -0,0 +1,80 @@ +<script> + +/* This is a re-usable vue component for rendering a user avatar that + does not need to link to the user's profile. The image and an optional + tooltip can be configured by props passed to this component. + + Sample configuration: + + <user-avatar-image + :img-src="userAvatarSrc" + :img-alt="tooltipText" + :tooltip-text="tooltipText" + tooltip-placement="top" + /> + +*/ + +import defaultAvatarUrl from 'images/no_avatar.png'; +import TooltipMixin from '../../mixins/tooltip'; + +export default { + name: 'UserAvatarImage', + mixins: [TooltipMixin], + props: { + imgSrc: { + type: String, + required: false, + default: defaultAvatarUrl, + }, + cssClasses: { + type: String, + required: false, + default: '', + }, + imgAlt: { + type: String, + required: false, + default: 'user avatar', + }, + size: { + type: Number, + required: false, + default: 20, + }, + tooltipText: { + type: String, + required: false, + default: '', + }, + tooltipPlacement: { + type: String, + required: false, + default: 'top', + }, + }, + computed: { + tooltipContainer() { + return this.tooltipText ? 'body' : null; + }, + avatarSizeClass() { + return `s${this.size}`; + }, + }, +}; +</script> + +<template> + <img + class="avatar" + :class="[avatarSizeClass, cssClasses]" + :src="imgSrc" + :width="size" + :height="size" + :alt="imgAlt" + :data-container="tooltipContainer" + :data-placement="tooltipPlacement" + :title="tooltipText" + ref="tooltip" + /> +</template> diff --git a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue new file mode 100644 index 00000000000..95898d54cf7 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue @@ -0,0 +1,80 @@ +<script> + +/* This is a re-usable vue component for rendering a user avatar wrapped in + a clickable link (likely to the user's profile). The link, image, and + tooltip can be configured by props passed to this component. + + Sample configuration: + + <user-avatar-link + :link-href="userProfileUrl" + :img-src="userAvatarSrc" + :img-alt="tooltipText" + :img-size="20" + :tooltip-text="tooltipText" + tooltip-placement="top" + /> + +*/ + +import userAvatarImage from './user_avatar_image.vue'; + +export default { + name: 'UserAvatarLink', + components: { + userAvatarImage, + }, + props: { + linkHref: { + type: String, + required: false, + default: '', + }, + imgSrc: { + type: String, + required: false, + default: '', + }, + imgAlt: { + type: String, + required: false, + default: '', + }, + imgCssClasses: { + type: String, + required: false, + default: '', + }, + imgSize: { + type: Number, + required: false, + default: 20, + }, + tooltipText: { + type: String, + required: false, + default: '', + }, + tooltipPlacement: { + type: String, + required: false, + default: 'top', + }, + }, +}; +</script> + +<template> + <a + class="user-avatar-link" + :href="linkHref"> + <user-avatar-image + :img-src="imgSrc" + :img-alt="imgAlt" + :css-classes="imgCssClasses" + :size="imgSize" + :tooltip-text="tooltipText" + :tooltip-placement="tooltipPlacement" + /> + </a> +</template> diff --git a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_svg.vue b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_svg.vue new file mode 100644 index 00000000000..d2ff2ac006e --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_svg.vue @@ -0,0 +1,45 @@ +<script> + +/* This is a re-usable vue component for rendering a user avatar svg (typically + for a blank state). It will receive styles comparable to the user avatar, + but no image is loaded, it isn't wrapped in a link, and tooltips aren't supported. + The svg and avatar size can be configured by props passed to this component. + + Sample configuration: + + <user-avatar-svg + :svg="potentialApproverSvg" + :size="20" + /> + +*/ + +export default { + props: { + svg: { + type: String, + required: true, + }, + size: { + type: Number, + required: false, + default: 20, + }, + }, + computed: { + avatarSizeClass() { + return `s${this.size}`; + }, + }, +}; +</script> + +<template> + <svg + :class="avatarSizeClass" + :height="size" + :width="size" + v-html="svg"> + </svg> +</template> + diff --git a/app/assets/javascripts/vue_shared/vue_resource_interceptor.js b/app/assets/javascripts/vue_shared/vue_resource_interceptor.js index d5f87588c28..740930dce5b 100644 --- a/app/assets/javascripts/vue_shared/vue_resource_interceptor.js +++ b/app/assets/javascripts/vue_shared/vue_resource_interceptor.js @@ -4,7 +4,7 @@ import VueResource from 'vue-resource'; Vue.use(VueResource); // Maintain a global counter for active requests -// see: spec/support/wait_for_vue_resource.rb +// see: spec/support/wait_for_requests.rb Vue.http.interceptors.push((request, next) => { window.activeVueResources = window.activeVueResources || 0; window.activeVueResources += 1; diff --git a/app/assets/javascripts/wikis.js b/app/assets/javascripts/wikis.js index 75fd1394a03..4194c1bc08d 100644 --- a/app/assets/javascripts/wikis.js +++ b/app/assets/javascripts/wikis.js @@ -1,8 +1,8 @@ /* eslint-disable no-param-reassign */ /* global Breakpoints */ -require('./breakpoints'); -require('vendor/jquery.nicescroll'); +import 'vendor/jquery.nicescroll'; +import './breakpoints'; ((global) => { class Wikis { diff --git a/app/assets/javascripts/zen_mode.js b/app/assets/javascripts/zen_mode.js index ce626cf7b46..b7fe552dec2 100644 --- a/app/assets/javascripts/zen_mode.js +++ b/app/assets/javascripts/zen_mode.js @@ -1,5 +1,4 @@ /* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, no-unused-vars, consistent-return, camelcase, comma-dangle, max-len */ -/* global Dropzone */ /* global Mousetrap */ // Zen Mode (full screen) textarea @@ -7,10 +6,12 @@ /*= provides zen_mode:enter */ /*= provides zen_mode:leave */ -require('vendor/jquery.scrollTo'); -window.Dropzone = require('dropzone'); -require('mousetrap'); -require('mousetrap/plugins/pause/mousetrap-pause'); +import 'vendor/jquery.scrollTo'; +import Dropzone from 'dropzone'; +import 'mousetrap'; +import 'mousetrap/plugins/pause/mousetrap-pause'; + +window.Dropzone = Dropzone; // // ### Events diff --git a/app/assets/stylesheets/framework/avatar.scss b/app/assets/stylesheets/framework/avatar.scss index 91c1ebd5a7d..4ae2b164d2e 100644 --- a/app/assets/stylesheets/framework/avatar.scss +++ b/app/assets/stylesheets/framework/avatar.scss @@ -10,6 +10,8 @@ border-radius: $avatar_radius; border: 1px solid $avatar-border; &.s16 { @include avatar-size(16px, 6px); } + &.s18 { @include avatar-size(18px, 6px); } + &.s19 { @include avatar-size(19px, 6px); } &.s20 { @include avatar-size(20px, 7px); } &.s24 { @include avatar-size(24px, 8px); } &.s26 { @include avatar-size(26px, 8px); } diff --git a/app/assets/stylesheets/framework/awards.scss b/app/assets/stylesheets/framework/awards.scss index 9159927ed8b..d64b1237b2c 100644 --- a/app/assets/stylesheets/framework/awards.scss +++ b/app/assets/stylesheets/framework/awards.scss @@ -108,8 +108,9 @@ } .award-control { - margin-right: 5px; + margin: 0 5px 6px 0; outline: 0; + position: relative; &.disabled { cursor: default; @@ -227,8 +228,8 @@ .award-control-icon-positive, .award-control-icon-super-positive { position: absolute; - left: 11px; - bottom: 7px; + left: 10px; + bottom: 6px; opacity: 0; @include transition(opacity, transform); } @@ -237,7 +238,3 @@ vertical-align: middle; } } - -.note-awards .award-control-icon-positive { - left: 6px; -} diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index 5c9b71a452c..5ab48b6c874 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -97,7 +97,7 @@ .fa-chevron-down { font-size: $dropdown-chevron-size; position: relative; - top: -3px; + top: -2px; margin-left: 5px; } diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss index e624d0d951e..f0994e968c8 100644 --- a/app/assets/stylesheets/framework/filters.scss +++ b/app/assets/stylesheets/framework/filters.scss @@ -263,7 +263,9 @@ } .filtered-search-input-dropdown-menu { + max-height: 215px; max-width: 280px; + overflow: auto; @media (max-width: $screen-xs-min) { width: auto; @@ -283,17 +285,10 @@ .filtered-search-history-dropdown-toggle-button { flex: 1; width: auto; - padding-right: 10px; - border-radius: 0; - border-top: 0; - border-left: 0; - border-bottom: 0; + border: 0; border-right: 1px solid $border-color; - color: $gl-text-color-secondary; - line-height: 1; - transition: color 0.1s linear; &:hover, @@ -301,6 +296,17 @@ color: $gl-text-color; border-color: $dropdown-input-focus-border; outline: none; + + svg { + fill: $gl-text-color; + } + } + + svg { + height: 14px; + width: 14px; + fill: $gl-text-color-secondary; + vertical-align: middle; } .dropdown-toggle-text { @@ -312,11 +318,6 @@ color: inherit; } } - - .fa { - position: static; - } - } .filtered-search-history-dropdown { @@ -373,11 +374,6 @@ padding: 0; } -.filter-dropdown { - max-height: 215px; - overflow: auto; -} - @media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) { .issue-bulk-update-dropdown-toggle { width: 100px; diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index 586511fe8d4..d8645afb7da 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -24,7 +24,7 @@ header { &.navbar-gitlab { padding: 0 16px; - z-index: 100; + z-index: 400; margin-bottom: 0; min-height: $header-height; background-color: $gray-light; @@ -40,7 +40,17 @@ header { } &.with-horizontal-nav { - border-color: transparent; + border-bottom: 0; + + .navbar-border { + height: 1px; + position: absolute; + right: 0; + left: 0; + bottom: -1px; + background-color: $border-color; + opacity: 0; + } } .container-fluid { @@ -114,16 +124,6 @@ header { } } - .navbar-border { - height: 1px; - position: absolute; - right: 0; - left: 0; - bottom: 0; - background-color: $border-color; - opacity: 0; - } - .global-dropdown { position: absolute; left: -10px; diff --git a/app/assets/stylesheets/framework/icons.scss b/app/assets/stylesheets/framework/icons.scss index 1b7d4e42258..ef864e8f6a9 100644 --- a/app/assets/stylesheets/framework/icons.scss +++ b/app/assets/stylesheets/framework/icons.scss @@ -65,3 +65,7 @@ text-decoration: none; } } + +.user-avatar-link { + text-decoration: none; +} diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss index d2f6a227128..28b2a7cfacd 100644 --- a/app/assets/stylesheets/framework/nav.scss +++ b/app/assets/stylesheets/framework/nav.scss @@ -351,7 +351,10 @@ .scrolling-tabs-container { position: relative; - overflow: hidden; + + .merge-request-tabs-container & { + overflow: hidden; + } .nav-links { @include scrolling-links(); @@ -440,8 +443,8 @@ } } -.activities { - .nav-block { +.nav-block { + &.activities { border-bottom: 1px solid $border-color; .nav-links { @@ -489,7 +492,6 @@ .inner-page-scroll-tabs { position: relative; - overflow: hidden; .fade-right { @include fade(left, $white-light); diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss index 018f61ca3a8..5b62d7fa3a7 100644 --- a/app/assets/stylesheets/framework/sidebar.scss +++ b/app/assets/stylesheets/framework/sidebar.scss @@ -83,4 +83,8 @@ position: fixed; top: $header-height; } + + &:not(.affix-top) { + min-height: 100%; + } } diff --git a/app/assets/stylesheets/framework/timeline.scss b/app/assets/stylesheets/framework/timeline.scss index d2164a1d333..1fd734d279b 100644 --- a/app/assets/stylesheets/framework/timeline.scss +++ b/app/assets/stylesheets/framework/timeline.scss @@ -3,33 +3,9 @@ margin: 0; padding: 0; - .timeline-entry { - padding: $gl-padding $gl-btn-padding 0; - border-color: $white-normal; - color: $gl-text-color; - border-bottom: 1px solid $border-white-light; - - .timeline-entry-inner { - position: relative; - } - - &:target { - background: $line-target-blue; - } - - .avatar { - margin-right: 15px; - } - - .controls { - padding-top: 10px; - float: right; - } - } - .note-text { p:last-child { - margin-bottom: 0; + margin-bottom: 0 !important; } } @@ -46,20 +22,44 @@ } } +.timeline-entry { + border-color: $white-normal; + color: $gl-text-color; + border-bottom: 1px solid $border-white-light; + + .timeline-entry-inner { + position: relative; + } + + &:target, + &.target { + background: $line-target-blue; + } + + .avatar { + margin-right: 15px; + } + + .controls { + padding-top: 10px; + float: right; + } +} + @media (max-width: $screen-xs-max) { .timeline { &::before { background: none; } + } - .timeline-entry .timeline-entry-inner { - .timeline-icon { - display: none; - } + .timeline-entry .timeline-entry-inner { + .timeline-icon { + display: none; + } - .timeline-content { - margin-left: 0; - } + .timeline-content { + margin-left: 0; } } } diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss index a7c6cbaae21..0c3407f34f8 100644 --- a/app/assets/stylesheets/framework/typography.scss +++ b/app/assets/stylesheets/framework/typography.scss @@ -139,6 +139,15 @@ line-height: 1.6em; overflow-x: auto; border-radius: 2px; + + + &.plain-readme { + background: none; + border: none; + padding: 0; + margin: 0; + font-size: 14px; + } } p > code { @@ -169,14 +178,14 @@ } ul.task-list { - li.task-list-item { + > li.task-list-item { list-style-type: none; position: relative; min-height: 22px; padding-left: 28px; margin-left: 0 !important; - input.task-list-item-checkbox { + > input.task-list-item-checkbox { position: absolute; left: 8px; top: 5px; @@ -279,14 +288,6 @@ h6 { /** CODE **/ pre { font-family: $monospace_font; - - &.plain-readme { - background: none; - border: none; - padding: 0; - margin: 0; - font-size: 14px; - } } code { diff --git a/app/assets/stylesheets/pages/detail_page.scss b/app/assets/stylesheets/pages/detail_page.scss index f3de05aa5f6..3d9eff35583 100644 --- a/app/assets/stylesheets/pages/detail_page.scss +++ b/app/assets/stylesheets/pages/detail_page.scss @@ -4,11 +4,7 @@ color: $gl-text-color; line-height: 34px; - .author { - color: $gl-text-color; - } - - .identifier { + a { color: $gl-text-color; } diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss index a42ae7e55a5..48d3b7b1d07 100644 --- a/app/assets/stylesheets/pages/environments.scss +++ b/app/assets/stylesheets/pages/environments.scss @@ -68,10 +68,6 @@ margin: 0; } - .avatar-image-container { - text-decoration: none; - } - .icon-play { height: 13px; width: 12px; diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 0184208ab82..d79ae47f589 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -10,7 +10,6 @@ .page-content-header, .commit-box, .info-well, - .notes, .commit-ci-menu, .files-changed { @extend .fixed-width-container; @@ -57,6 +56,10 @@ padding: 5px; max-height: calc(100vh - 100px); } + + .emoji-block { + padding: 10px 0 4px; + } } .issuable-filter-count { @@ -195,8 +198,17 @@ right: 0; transition: width .3s; background: $gray-light; - padding: 10px 20px; + padding: 0 20px; z-index: 200; + overflow: hidden; + + .issuable-sidebar { + width: calc(100% + 100px); + height: 100%; + overflow-y: scroll; + overflow-x: hidden; + -webkit-overflow-scrolling: touch; + } &.right-sidebar-expanded { width: $gutter_width; @@ -210,6 +222,10 @@ } } + .issuable-sidebar-header { + padding-top: 10px; + } + .assign-yourself .btn-link { padding-left: 0; } @@ -263,11 +279,10 @@ } width: $gutter_collapsed_width; - padding-top: 0; + padding: 0; .block { width: $gutter_collapsed_width - 2px; - margin-left: -19px; padding: 15px 0 0; border-bottom: none; overflow: hidden; diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index d208e54207e..183be86f650 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -109,6 +109,10 @@ height: 22px; margin-right: 8px; } + + .ci-error { + margin-right: $btn-side-margin; + } } .mr-widget-body, @@ -205,17 +209,30 @@ } } - .mr-widget-heading, - .mr-widget-body { + .mr-widget-heading { .btn-default.btn-xs { margin-left: 5px; } } .mr-widget-body { + .btn { + font-size: 15px; + } + + .btn-group .btn { + padding: 5px 10px; + + &.dropdown-toggle { + padding: 5px 7px; + } + } + } + + .mr-widget-body { h4 { - font-weight: 600; - font-size: 16px; + font-weight: bold; + font-size: 15px; margin: 5px 0; color: $gl-text-color; @@ -242,8 +259,8 @@ } .bold { - margin-left: 5px; font-weight: bold; + font-size: 15px; color: $gl-gray-light; } @@ -267,6 +284,11 @@ margin-bottom: 24px; } + .spacing, + .bold { + vertical-align: middle; + } + .dropdown-menu { li a { padding: 5px; @@ -349,6 +371,22 @@ margin-top: 10px; margin-left: 12px; } + + &.empty-state { + .artwork { + margin-bottom: $gl-padding; + } + + .text { + span { + font-weight: bold; + } + + p { + margin-top: $gl-padding; + } + } + } } .mr-widget-footer { @@ -369,6 +407,12 @@ } } +.mr-state-widget .mr-widget-body { + .approve-btn { + margin-right: 5px; + } +} + .mr_source_commit, .mr_target_commit { margin-bottom: 0; @@ -511,7 +555,7 @@ p { float: left; - padding-left: 20px; + padding-left: 21px; &::before { top: 13px; diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss index 62f654ed343..49e453c7dbc 100644 --- a/app/assets/stylesheets/pages/note_form.scss +++ b/app/assets/stylesheets/pages/note_form.scss @@ -164,10 +164,6 @@ .discussion-body, .diff-file { - .notes .note { - padding: 10px 15px; - } - .discussion-reply-holder { background-color: $white-light; padding: 10px 16px; @@ -277,6 +273,7 @@ .toolbar-text { font-size: 14px; line-height: 16px; + margin-top: 2px; @media (min-width: $screen-md-min) { float: left; @@ -402,3 +399,45 @@ } } } + +.uploading-container { + float: right; + + @media (max-width: $screen-xs-max) { + float: left; + margin-top: 5px; + } +} + +.uploading-error-icon, +.uploading-error-message { + color: $gl-text-red; +} + +.uploading-error-message { + @media (max-width: $screen-xs-max) { + &::after { + content: "\a"; + white-space: pre; + } + } +} + +.uploading-progress { + margin-right: 5px; +} + +.attach-new-file, +.button-attach-file, +.retry-uploading-link { + color: $gl-link-color; + padding: 0; + background: none; + border: 0; + font-size: 14px; + line-height: 16px; +} + +.markdown-selector { + color: $gl-link-color; +} diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index 5b6aa9d74f6..51918917329 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -43,7 +43,11 @@ ul.notes { } .discussion-body { - padding-top: 15px; + padding-top: 8px; + + .panel { + margin-bottom: 0; + } } .discussion { @@ -53,6 +57,7 @@ ul.notes { } .note { + padding: $gl-padding $gl-btn-padding 0; display: block; position: relative; border-bottom: 1px solid $white-normal; @@ -78,11 +83,7 @@ ul.notes { &.note-discussion { &.timeline-entry { - padding: 14px 10px; - } - - .system-note { - padding: 0; + padding: $gl-padding 10px; } } @@ -167,7 +168,7 @@ ul.notes { margin-left: 65px; } - .note-header { + .note-header-info { padding-bottom: 0; } @@ -377,7 +378,11 @@ ul.notes { .note-header-info { min-width: 0; - padding-bottom: 5px; + padding-bottom: 8px; +} + +.system-note .note-header-info { + padding-bottom: 0; } .note-headline-light { @@ -582,6 +587,17 @@ ul.notes { } } +.discussion-body, +.diff-file { + .notes .note { + padding: 10px 15px; + + &.system-note { + padding: 0; + } + } +} + .diff-file { .is-over { .add-diff-note { @@ -669,7 +685,7 @@ ul.notes { .line-resolve-btn { position: relative; - top: 2px; + top: 0; padding: 0; background-color: transparent; border: none; @@ -690,8 +706,8 @@ ul.notes { svg { fill: $gray-darkest; - height: 15px; - width: 15px; + height: 16px; + width: 16px; } .loading { diff --git a/app/assets/stylesheets/pages/pipeline_schedules.scss b/app/assets/stylesheets/pages/pipeline_schedules.scss index 0fee54a0d19..ab417948931 100644 --- a/app/assets/stylesheets/pages/pipeline_schedules.scss +++ b/app/assets/stylesheets/pages/pipeline_schedules.scss @@ -31,14 +31,6 @@ margin-right: 10px; font-size: 12px; } - - .cron-unset-status { - padding-top: 16px; - margin-left: -16px; - color: $gl-text-color-secondary; - font-size: 12px; - font-weight: 600; - } } .pipeline-schedule-table-row { @@ -69,3 +61,16 @@ color: $gl-text-color; } } + +.cron-preset-radio-input { + display: inline-block; + + @media (max-width: $screen-md-max) { + display: block; + margin: 0 0 5px 5px; + } + + input { + margin-right: 3px; + } +} diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index e4f5ab26b4d..292584eba28 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -191,7 +191,6 @@ } .commit-title { - margin-top: 4px; max-width: 225px; overflow: hidden; white-space: nowrap; @@ -224,7 +223,7 @@ .duration, .finished-at { color: $gl-text-color-secondary; - margin: 4px 0; + margin: 0; white-space: nowrap; .fa { diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index ed4a5474034..f0bf3d4c267 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -639,58 +639,6 @@ pre.light-well { } } -.project-last-commit { - background-color: $gray-light; - border: 1px solid $border-color; - border-radius: $border-radius-base; - padding: 12px; - - @media (min-width: $screen-sm-min) { - margin-top: $gl-padding; - } - - .ci-status { - margin-right: $gl-padding; - } - - .commit-row-message { - color: $gl-text-color; - } - - .commit-sha { - margin-right: 5px; - font-weight: 600; - } - - .commit-author-link { - .commit-author-name { - font-weight: 600; - } - } -} - -.project-show-readme { - .row-content-block { - background-color: inherit; - border: none; - } - - .readme-holder { - padding: $gl-padding 0; - border-top: 0; - - .edit-project-readme { - z-index: 2; - position: relative; - } - - .wiki h1 { - border-bottom: none; - padding: 0; - } - } -} - .git-clone-holder { width: 380px; diff --git a/app/assets/stylesheets/test.scss b/app/assets/stylesheets/test.scss new file mode 100644 index 00000000000..7d9f3da79c5 --- /dev/null +++ b/app/assets/stylesheets/test.scss @@ -0,0 +1,17 @@ +* { + -o-transition: none !important; + -moz-transition: none !important; + -ms-transition: none !important; + -webkit-transition: none !important; + transition: none !important; + -o-transform: none !important; + -moz-transform: none !important; + -ms-transform: none !important; + -webkit-transform: none !important; + transform: none !important; + -webkit-animation: none !important; + -moz-animation: none !important; + -o-animation: none !important; + -ms-animation: none !important; + animation: none !important; +} diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 8ce9150e4a9..ab5aed24917 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -11,6 +11,7 @@ class ApplicationController < ActionController::Base include EnforcesTwoFactorAuthentication before_action :authenticate_user_from_private_token! + before_action :authenticate_user_from_rss_token! before_action :authenticate_user! before_action :validate_user_service_ticket! before_action :check_password_expiration @@ -72,13 +73,20 @@ class ApplicationController < ActionController::Base user = User.find_by_authentication_token(token) || User.find_by_personal_access_token(token) - if user && can?(user, :log_in) - # Notice we are passing store false, so the user is not - # actually stored in the session and a token is needed - # for every request. If you want the token to work as a - # sign in token, you can simply remove store: false. - sign_in user, store: false - end + sessionless_sign_in(user) + end + + # This filter handles authentication for atom request with an rss_token + def authenticate_user_from_rss_token! + return unless request.format.atom? + + token = params[:rss_token].presence + + return unless token.present? + + user = User.find_by_rss_token(token) + + sessionless_sign_in(user) end def log_exception(exception) @@ -282,4 +290,14 @@ class ApplicationController < ActionController::Base ensure Gitlab::I18n.reset_locale end + + def sessionless_sign_in(user) + if user && can?(user, :log_in) + # Notice we are passing store false, so the user is not + # actually stored in the session and a token is needed + # for every request. If you want the token to work as a + # sign in token, you can simply remove store: false. + sign_in user, store: false + end + end end diff --git a/app/controllers/concerns/renders_blob.rb b/app/controllers/concerns/renders_blob.rb index 4a6630dfd90..1d37e4cb3bd 100644 --- a/app/controllers/concerns/renders_blob.rb +++ b/app/controllers/concerns/renders_blob.rb @@ -14,7 +14,7 @@ module RendersBlob return render_404 unless viewer render json: { - html: view_to_html_string("projects/blob/_viewer", viewer: viewer, load_asynchronously: false) + html: view_to_html_string("projects/blob/_viewer", viewer: viewer, load_async: false) } end diff --git a/app/controllers/concerns/routable_actions.rb b/app/controllers/concerns/routable_actions.rb index afd110adcad..4199da9cdf5 100644 --- a/app/controllers/concerns/routable_actions.rb +++ b/app/controllers/concerns/routable_actions.rb @@ -24,15 +24,15 @@ module RoutableActions end end - def ensure_canonical_path(routable, requested_path) + def ensure_canonical_path(routable, requested_full_path) return unless request.get? canonical_path = routable.full_path - if canonical_path != requested_path - if canonical_path.casecmp(requested_path) != 0 - flash[:notice] = "#{routable.class.to_s.titleize} '#{requested_path}' was moved to '#{canonical_path}'. Please update any links and bookmarks that may still have the old path." + if canonical_path != requested_full_path + if canonical_path.casecmp(requested_full_path) != 0 + flash[:notice] = "#{routable.class.to_s.titleize} '#{requested_full_path}' was moved to '#{canonical_path}'. Please update any links and bookmarks that may still have the old path." end - redirect_to request.original_url.sub(requested_path, canonical_path) + redirect_to build_canonical_path(routable) end end end diff --git a/app/controllers/groups/application_controller.rb b/app/controllers/groups/application_controller.rb index afffb813b44..c0ac47e363d 100644 --- a/app/controllers/groups/application_controller.rb +++ b/app/controllers/groups/application_controller.rb @@ -31,4 +31,10 @@ class Groups::ApplicationController < ApplicationController return render_403 end end + + def build_canonical_path(group) + params[:group_id] = group.to_param + + url_for(params) + end end diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index 1515173d0ac..965ced4d372 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -169,4 +169,12 @@ class GroupsController < Groups::ApplicationController @notification_setting = current_user.notification_settings_for(group) end end + + def build_canonical_path(group) + return group_path(group) if action_name == 'show' # root group path + + params[:id] = group.to_param + + url_for(params) + end end diff --git a/app/controllers/profiles_controller.rb b/app/controllers/profiles_controller.rb index 57e23cea00e..8cd1c47eb3f 100644 --- a/app/controllers/profiles_controller.rb +++ b/app/controllers/profiles_controller.rb @@ -40,6 +40,14 @@ class ProfilesController < Profiles::ApplicationController redirect_to profile_account_path end + def reset_rss_token + if current_user.reset_rss_token! + flash[:notice] = "RSS token was successfully reset" + end + + redirect_to profile_account_path + end + def audit_log @events = AuditEvent.where(entity_type: "User", entity_id: current_user.id). order("created_at DESC"). diff --git a/app/controllers/projects/application_controller.rb b/app/controllers/projects/application_controller.rb index 12e4a6999ae..cb4bd0ad5f5 100644 --- a/app/controllers/projects/application_controller.rb +++ b/app/controllers/projects/application_controller.rb @@ -29,6 +29,13 @@ class Projects::ApplicationController < ApplicationController @project = find_routable!(Project, path, extra_authorization_proc: auth_proc) end + def build_canonical_path(project) + params[:namespace_id] = project.namespace.to_param + params[:project_id] = project.to_param + + url_for(params) + end + def repository @repository ||= project.repository end diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb index 9489bbddfc4..87721fbe2f5 100644 --- a/app/controllers/projects/blob_controller.rb +++ b/app/controllers/projects/blob_controller.rb @@ -42,6 +42,8 @@ class Projects::BlobController < Projects::ApplicationController environment_params = @repository.branch_exists?(@ref) ? { ref: @ref } : { commit: @commit } @environment = EnvironmentsFinder.new(@project, current_user, environment_params).execute.last + @last_commit = @repository.last_commit_for_path(@commit.id, @blob.path) + render 'show' end diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb index fd57afbd05f..efe83776834 100644 --- a/app/controllers/projects/environments_controller.rb +++ b/app/controllers/projects/environments_controller.rb @@ -31,6 +31,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController def folder folder_environments = project.environments.where(environment_type: params[:id]) @environments = folder_environments.with_state(params[:scope] || :available) + .order(:name) respond_to do |format| format.html diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 9d28a7ed85a..59df1e7b86a 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -274,7 +274,10 @@ class Projects::IssuesController < Projects::ApplicationController notice = "Please sign in to create the new issue." - store_location_for :user, request.fullpath + if request.get? && !request.xhr? + store_location_for :user, request.fullpath + end + redirect_to new_user_session_path, notice: notice end end diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index b99ccd453b8..0352065998b 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -9,14 +9,15 @@ class Projects::MergeRequestsController < Projects::ApplicationController before_action :module_enabled before_action :merge_request, only: [ - :edit, :update, :show, :diffs, :commits, :conflicts, :conflict_for_path, :pipelines, :merge, :merge_check, + :edit, :update, :show, :diffs, :commits, :conflicts, :conflict_for_path, :pipelines, :merge, :pipeline_status, :ci_environments_status, :toggle_subscription, :cancel_merge_when_pipeline_succeeds, :remove_wip, :resolve_conflicts, :assign_related_issues, :commit_change_content ] before_action :validates_merge_request, only: [:show, :diffs, :commits, :pipelines] - before_action :define_show_vars, only: [:show, :diffs, :commits, :conflicts, :conflict_for_path, :builds, :pipelines] + before_action :define_show_vars, only: [:diffs, :commits, :conflicts, :conflict_for_path, :builds, :pipelines] before_action :define_commit_vars, only: [:diffs] before_action :ensure_ref_fetched, only: [:show, :diffs, :commits, :builds, :conflicts, :conflict_for_path, :pipelines] before_action :close_merge_request_without_source_project, only: [:show, :diffs, :commits, :builds, :pipelines] + before_action :check_if_can_be_merged, only: :show before_action :apply_diff_view_cookie!, only: [:new_diffs] before_action :build_merge_request, only: [:new, :new_diffs] @@ -75,9 +76,12 @@ class Projects::MergeRequestsController < Projects::ApplicationController respond_to do |format| format.html do define_discussion_vars + define_show_vars end format.json do + Gitlab::PollingInterval.set_header(response, interval: 10_000) + render json: serializer.represent(@merge_request, basic: params[:basic]) end @@ -309,12 +313,6 @@ class Projects::MergeRequestsController < Projects::ApplicationController render json: serializer.represent(@merge_request) end - def merge_check - @merge_request.check_if_can_be_merged - - render json: serializer.represent(@merge_request) - end - def commit_change_content render partial: 'projects/merge_requests/widget/commit_change_content', layout: false end @@ -640,6 +638,10 @@ class Projects::MergeRequestsController < Projects::ApplicationController private + def check_if_can_be_merged + @merge_request.check_if_can_be_merged + end + def merge! # Disable the CI check if merge_when_pipeline_succeeds is enabled since we have # to wait until CI completes to know diff --git a/app/controllers/projects/pipelines_settings_controller.rb b/app/controllers/projects/pipelines_settings_controller.rb index ff50602831c..38a47651000 100644 --- a/app/controllers/projects/pipelines_settings_controller.rb +++ b/app/controllers/projects/pipelines_settings_controller.rb @@ -7,7 +7,7 @@ class Projects::PipelinesSettingsController < Projects::ApplicationController def update if @project.update_attributes(update_params) - flash[:notice] = "CI/CD Pipelines settings for '#{@project.name}' were successfully updated." + flash[:notice] = "Pipelines settings for '#{@project.name}' were successfully updated." redirect_to namespace_project_settings_ci_cd_path(@project.namespace, @project) else render 'show' diff --git a/app/controllers/projects/tree_controller.rb b/app/controllers/projects/tree_controller.rb index 3ce65b29b3c..f8eb8e00a5d 100644 --- a/app/controllers/projects/tree_controller.rb +++ b/app/controllers/projects/tree_controller.rb @@ -24,6 +24,8 @@ class Projects::TreeController < Projects::ApplicationController end end + @last_commit = @repository.last_commit_for_path(@commit.id, @tree.path) || @commit + respond_to do |format| format.html # Disable cache so browser history works diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 63d018c8cbf..cc62e1fa99b 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -257,7 +257,7 @@ class ProjectsController < Projects::ApplicationController # # pages list order: repository readme, wiki home, issues list, customize workflow def render_landing_page - if @project.feature_available?(:repository, current_user) + if can?(current_user, :download_code, @project) return render 'projects/no_repo' unless @project.repository_exists? render 'projects/empty' if @project.empty_repo? else @@ -365,4 +365,11 @@ class ProjectsController < Projects::ApplicationController def project_view_files_allowed? !project.empty_repo? && can?(current_user, :download_code, project) end + + def build_canonical_path(project) + params[:namespace_id] = project.namespace.to_param + params[:id] = project.to_param + + url_for(params) + end end diff --git a/app/controllers/uploads_controller.rb b/app/controllers/uploads_controller.rb index 21a964fb391..eef53730291 100644 --- a/app/controllers/uploads_controller.rb +++ b/app/controllers/uploads_controller.rb @@ -21,6 +21,8 @@ class UploadsController < ApplicationController can?(current_user, :read_project, model.project) when User true + when Appearance + true else permission = "read_#{model.class.to_s.underscore}".to_sym diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index ba22b2f9d29..19fc1e5de49 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -138,4 +138,8 @@ class UsersController < ApplicationController def projects_for_current_user ProjectsFinder.new(current_user: current_user).execute end + + def build_canonical_path(user) + url_for(params.merge(username: user.to_param)) + end end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 97cf4863ddc..e5e64650708 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -278,4 +278,22 @@ module ApplicationHelper def show_user_callout? cookies[:user_callout_dismissed] == 'true' end + + def linkedin_url(user) + name = user.linkedin + if name =~ %r{\Ahttps?:\/\/(www\.)?linkedin\.com\/in\/} + name + else + "https://www.linkedin.com/in/#{name}" + end + end + + def twitter_url(user) + name = user.twitter + if name =~ %r{\Ahttps?:\/\/(www\.)?twitter\.com\/} + name + else + "https://www.twitter.com/#{name}" + end + end end diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb index eb37f2e0267..622e14e21ff 100644 --- a/app/helpers/blob_helper.rb +++ b/app/helpers/blob_helper.rb @@ -226,7 +226,7 @@ module BlobHelper def open_raw_blob_button(blob) return if blob.empty? - + if blob.raw_binary? || blob.stored_externally? icon = icon('download') title = 'Download' @@ -242,9 +242,9 @@ module BlobHelper case viewer.render_error when :too_large max_size = - if viewer.absolutely_too_large? - viewer.absolute_max_size - elsif viewer.too_large? + if viewer.can_override_max_size? + viewer.overridable_max_size + else viewer.max_size end "it is larger than #{number_to_human_size(max_size)}" @@ -278,4 +278,19 @@ module BlobHelper options end + + def contribution_options(project) + options = [] + + if can?(current_user, :create_issue, project) + options << link_to("submit an issue", new_namespace_project_issue_path(project.namespace, project)) + end + + merge_project = can?(current_user, :create_merge_request, project) ? project : (current_user && current_user.fork_of(project)) + if merge_project + options << link_to("create a merge request", new_namespace_project_merge_request_path(project.namespace, project)) + end + + options + end end diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb index 6d6f1361bf9..d59d51905a6 100644 --- a/app/helpers/commits_helper.rb +++ b/app/helpers/commits_helper.rb @@ -91,7 +91,7 @@ module CommitsHelper end def link_to_browse_code(project, commit) - return unless current_controller?(:projects, :commits) + return unless current_controller?(:commits) if @path.blank? return link_to( diff --git a/app/helpers/icons_helper.rb b/app/helpers/icons_helper.rb index 55fa81e95ef..f29faeca22d 100644 --- a/app/helpers/icons_helper.rb +++ b/app/helpers/icons_helper.rb @@ -7,9 +7,10 @@ module IconsHelper # font-awesome-rails gem, but should we ever use a different icon pack in the # future we won't have to change hundreds of method calls. def icon(names, options = {}) - if (options.keys & %w[aria-hidden aria-label]).empty? - # Add `aria-hidden` if there are no aria's set + if (options.keys & %w[aria-hidden aria-label data-hidden]).empty? + # Add 'aria-hidden' and 'data-hidden' if they are not set in options. options['aria-hidden'] = true + options['data-hidden'] = true end options.include?(:base) ? fa_stacked_icon(names, options) : fa_icon(names, options) @@ -19,6 +20,8 @@ module IconsHelper case names when "standard" names = "key" + when "two-factor" + names = "key" end options.include?(:base) ? fa_stacked_icon(names, options) : fa_icon(names, options) diff --git a/app/helpers/markup_helper.rb b/app/helpers/markup_helper.rb index 0009cad86c4..941cfce8370 100644 --- a/app/helpers/markup_helper.rb +++ b/app/helpers/markup_helper.rb @@ -1,6 +1,9 @@ require 'nokogiri' module MarkupHelper + include ActionView::Helpers::TagHelper + include ActionView::Context + def plain?(filename) Gitlab::MarkupHelper.plain?(filename) end diff --git a/app/helpers/preferences_helper.rb b/app/helpers/preferences_helper.rb index de959f13713..d36bb4ab074 100644 --- a/app/helpers/preferences_helper.rb +++ b/app/helpers/preferences_helper.rb @@ -49,7 +49,7 @@ module PreferencesHelper user_view = current_user.project_view - if @project.feature_available?(:repository, current_user) + if can?(current_user, :download_code, @project) user_view elsif user_view == "activity" "activity" diff --git a/app/helpers/rss_helper.rb b/app/helpers/rss_helper.rb index ea5d2932ef4..9ac4df88dc3 100644 --- a/app/helpers/rss_helper.rb +++ b/app/helpers/rss_helper.rb @@ -1,5 +1,5 @@ module RssHelper def rss_url_options - { format: :atom, private_token: current_user.try(:private_token) } + { format: :atom, rss_token: current_user.try(:rss_token) } end end diff --git a/app/helpers/system_note_helper.rb b/app/helpers/system_note_helper.rb index d889d141101..209bd56b78a 100644 --- a/app/helpers/system_note_helper.rb +++ b/app/helpers/system_note_helper.rb @@ -17,7 +17,8 @@ module SystemNoteHelper 'visible' => 'icon_eye', 'milestone' => 'icon_clock_o', 'discussion' => 'icon_comment_o', - 'moved' => 'icon_arrow_circle_o_right' + 'moved' => 'icon_arrow_circle_o_right', + 'outdated' => 'icon_edit' }.freeze def icon_for_system_note(note) diff --git a/app/models/blob.rb b/app/models/blob.rb index 63a81c0e3bd..e75926241ba 100644 --- a/app/models/blob.rb +++ b/app/models/blob.rb @@ -39,7 +39,23 @@ class Blob < SimpleDelegator AUXILIARY_VIEWERS = [ BlobViewer::GitlabCiYml, BlobViewer::RouteMap, - BlobViewer::License + + BlobViewer::Readme, + BlobViewer::License, + BlobViewer::Contributing, + BlobViewer::Changelog, + + BlobViewer::Cartfile, + BlobViewer::ComposerJson, + BlobViewer::Gemfile, + BlobViewer::Gemspec, + BlobViewer::GodepsJson, + BlobViewer::PackageJson, + BlobViewer::Podfile, + BlobViewer::Podspec, + BlobViewer::PodspecJson, + BlobViewer::RequirementsTxt, + BlobViewer::YarnLock ].freeze attr_reader :project diff --git a/app/models/blob_viewer/auxiliary.rb b/app/models/blob_viewer/auxiliary.rb index db124397b27..07a207730cf 100644 --- a/app/models/blob_viewer/auxiliary.rb +++ b/app/models/blob_viewer/auxiliary.rb @@ -2,11 +2,17 @@ module BlobViewer module Auxiliary extend ActiveSupport::Concern + include Gitlab::Allowable + included do self.loading_partial_name = 'loading_auxiliary' self.type = :auxiliary + self.overridable_max_size = 100.kilobytes self.max_size = 100.kilobytes - self.absolute_max_size = 100.kilobytes + end + + def visible_to?(current_user) + true end end end diff --git a/app/models/blob_viewer/base.rb b/app/models/blob_viewer/base.rb index 4f38c31714b..26a3778c2a3 100644 --- a/app/models/blob_viewer/base.rb +++ b/app/models/blob_viewer/base.rb @@ -2,15 +2,17 @@ module BlobViewer class Base PARTIAL_PATH_PREFIX = 'projects/blob/viewers'.freeze - class_attribute :partial_name, :loading_partial_name, :type, :extensions, :file_type, :client_side, :binary, :switcher_icon, :switcher_title, :max_size, :absolute_max_size + class_attribute :partial_name, :loading_partial_name, :type, :extensions, :file_types, :load_async, :binary, :switcher_icon, :switcher_title, :overridable_max_size, :max_size self.loading_partial_name = 'loading' - delegate :partial_path, :loading_partial_path, :rich?, :simple?, :client_side?, :server_side?, :text?, :binary?, to: :class + delegate :partial_path, :loading_partial_path, :rich?, :simple?, :text?, :binary?, to: :class attr_reader :blob attr_accessor :override_max_size + delegate :project, to: :blob + def initialize(blob) @blob = blob end @@ -35,12 +37,8 @@ module BlobViewer type == :auxiliary end - def self.client_side? - client_side - end - - def self.server_side? - !client_side? + def self.load_async? + load_async end def self.binary? @@ -54,21 +52,33 @@ module BlobViewer def self.can_render?(blob, verify_binary: true) return false if verify_binary && binary? != blob.binary? return true if extensions&.include?(blob.extension) - return true if file_type && Gitlab::FileDetector.type_of(blob.path) == file_type + return true if file_types&.include?(Gitlab::FileDetector.type_of(blob.path)) false end - def too_large? - blob.raw_size > max_size + def load_async? + self.class.load_async? && render_error.nil? end - def absolutely_too_large? - blob.raw_size > absolute_max_size + def exceeds_overridable_max_size? + overridable_max_size && blob.raw_size > overridable_max_size + end + + def exceeds_max_size? + max_size && blob.raw_size > max_size end def can_override_max_size? - too_large? && !absolutely_too_large? + exceeds_overridable_max_size? && !exceeds_max_size? + end + + def too_large? + if override_max_size + exceeds_max_size? + else + exceeds_overridable_max_size? + end end # This method is used on the server side to check whether we can attempt to @@ -83,29 +93,13 @@ module BlobViewer # binary from `blob_raw_url` and does its own format validation and error # rendering, especially for potentially large binary formats. def render_error - return @render_error if defined?(@render_error) - - @render_error = - if server_side_but_stored_externally? - # Files that are not stored in the repository, like LFS files and - # build artifacts, can only be rendered using a client-side viewer, - # since we do not want to read large amounts of data into memory on the - # server side. Client-side viewers use JS and can fetch the file from - # `blob_raw_url` using AJAX. - :server_side_but_stored_externally - elsif override_max_size ? absolutely_too_large? : too_large? - :too_large - end + if too_large? + :too_large + end end def prepare! # To be overridden by subclasses end - - private - - def server_side_but_stored_externally? - server_side? && blob.stored_externally? - end end end diff --git a/app/models/blob_viewer/cartfile.rb b/app/models/blob_viewer/cartfile.rb new file mode 100644 index 00000000000..d8471bc33c0 --- /dev/null +++ b/app/models/blob_viewer/cartfile.rb @@ -0,0 +1,15 @@ +module BlobViewer + class Cartfile < DependencyManager + include Static + + self.file_types = %i(cartfile) + + def manager_name + 'Carthage' + end + + def manager_url + 'https://github.com/Carthage/Carthage' + end + end +end diff --git a/app/models/blob_viewer/changelog.rb b/app/models/blob_viewer/changelog.rb new file mode 100644 index 00000000000..0464ae27f71 --- /dev/null +++ b/app/models/blob_viewer/changelog.rb @@ -0,0 +1,16 @@ +module BlobViewer + class Changelog < Base + include Auxiliary + include Static + + self.partial_name = 'changelog' + self.file_types = %i(changelog) + self.binary = false + + def render_error + return if project.repository.tag_count > 0 + + :no_tags + end + end +end diff --git a/app/models/blob_viewer/client_side.rb b/app/models/blob_viewer/client_side.rb index 42ec68f864b..cc68236f92b 100644 --- a/app/models/blob_viewer/client_side.rb +++ b/app/models/blob_viewer/client_side.rb @@ -3,9 +3,9 @@ module BlobViewer extend ActiveSupport::Concern included do - self.client_side = true - self.max_size = 10.megabytes - self.absolute_max_size = 50.megabytes + self.load_async = false + self.overridable_max_size = 10.megabytes + self.max_size = 50.megabytes end end end diff --git a/app/models/blob_viewer/composer_json.rb b/app/models/blob_viewer/composer_json.rb new file mode 100644 index 00000000000..ef8b4aef8e8 --- /dev/null +++ b/app/models/blob_viewer/composer_json.rb @@ -0,0 +1,23 @@ +module BlobViewer + class ComposerJson < DependencyManager + include ServerSide + + self.file_types = %i(composer_json) + + def manager_name + 'Composer' + end + + def manager_url + 'https://getcomposer.com/' + end + + def package_name + @package_name ||= package_name_from_json('name') + end + + def package_url + "https://packagist.org/packages/#{package_name}" + end + end +end diff --git a/app/models/blob_viewer/contributing.rb b/app/models/blob_viewer/contributing.rb new file mode 100644 index 00000000000..fbd1dd48697 --- /dev/null +++ b/app/models/blob_viewer/contributing.rb @@ -0,0 +1,10 @@ +module BlobViewer + class Contributing < Base + include Auxiliary + include Static + + self.partial_name = 'contributing' + self.file_types = %i(contributing) + self.binary = false + end +end diff --git a/app/models/blob_viewer/dependency_manager.rb b/app/models/blob_viewer/dependency_manager.rb new file mode 100644 index 00000000000..a8d9be945dc --- /dev/null +++ b/app/models/blob_viewer/dependency_manager.rb @@ -0,0 +1,43 @@ +module BlobViewer + class DependencyManager < Base + include Auxiliary + + self.partial_name = 'dependency_manager' + self.binary = false + + def manager_name + raise NotImplementedError + end + + def manager_url + raise NotImplementedError + end + + def package_type + 'package' + end + + def package_name + nil + end + + def package_url + nil + end + + private + + def package_name_from_json(key) + prepare! + + JSON.parse(blob.data)[key] rescue nil + end + + def package_name_from_method_call(name) + prepare! + + match = blob.data.match(/#{name}\s*=\s*["'](?<name>[^"']+)["']/) + match[:name] if match + end + end +end diff --git a/app/models/blob_viewer/download.rb b/app/models/blob_viewer/download.rb index adc06587f69..074e7204814 100644 --- a/app/models/blob_viewer/download.rb +++ b/app/models/blob_viewer/download.rb @@ -1,17 +1,9 @@ module BlobViewer class Download < Base include Simple - # We treat the Download viewer as if it renders the content client-side, - # so that it doesn't attempt to load the entire blob contents and is - # rendered synchronously instead of loaded asynchronously. - include ClientSide + include Static self.partial_name = 'download' self.binary = true - - # We can always render the Download viewer, even if the blob is in LFS or too large. - def render_error - nil - end end end diff --git a/app/models/blob_viewer/gemfile.rb b/app/models/blob_viewer/gemfile.rb new file mode 100644 index 00000000000..fae8c8df23f --- /dev/null +++ b/app/models/blob_viewer/gemfile.rb @@ -0,0 +1,15 @@ +module BlobViewer + class Gemfile < DependencyManager + include Static + + self.file_types = %i(gemfile gemfile_lock) + + def manager_name + 'Bundler' + end + + def manager_url + 'http://bundler.io/' + end + end +end diff --git a/app/models/blob_viewer/gemspec.rb b/app/models/blob_viewer/gemspec.rb new file mode 100644 index 00000000000..7802edeb754 --- /dev/null +++ b/app/models/blob_viewer/gemspec.rb @@ -0,0 +1,27 @@ +module BlobViewer + class Gemspec < DependencyManager + include ServerSide + + self.file_types = %i(gemspec) + + def manager_name + 'RubyGems' + end + + def manager_url + 'https://rubygems.org/' + end + + def package_type + 'gem' + end + + def package_name + @package_name ||= package_name_from_method_call('name') + end + + def package_url + "https://rubygems.org/gems/#{package_name}" + end + end +end diff --git a/app/models/blob_viewer/gitlab_ci_yml.rb b/app/models/blob_viewer/gitlab_ci_yml.rb index 81afab2f49b..7267c3965d3 100644 --- a/app/models/blob_viewer/gitlab_ci_yml.rb +++ b/app/models/blob_viewer/gitlab_ci_yml.rb @@ -5,7 +5,7 @@ module BlobViewer self.partial_name = 'gitlab_ci_yml' self.loading_partial_name = 'gitlab_ci_yml_loading' - self.file_type = :gitlab_ci + self.file_types = %i(gitlab_ci) self.binary = false def validation_message diff --git a/app/models/blob_viewer/godeps_json.rb b/app/models/blob_viewer/godeps_json.rb new file mode 100644 index 00000000000..e19a602603b --- /dev/null +++ b/app/models/blob_viewer/godeps_json.rb @@ -0,0 +1,15 @@ +module BlobViewer + class GodepsJson < DependencyManager + include Static + + self.file_types = %i(godeps_json) + + def manager_name + 'godep' + end + + def manager_url + 'https://github.com/tools/godep' + end + end +end diff --git a/app/models/blob_viewer/license.rb b/app/models/blob_viewer/license.rb index 3ad49570c88..57355f2c3aa 100644 --- a/app/models/blob_viewer/license.rb +++ b/app/models/blob_viewer/license.rb @@ -1,17 +1,14 @@ module BlobViewer class License < Base - # We treat the License viewer as if it renders the content client-side, - # so that it doesn't attempt to load the entire blob contents and is - # rendered synchronously instead of loaded asynchronously. - include ClientSide include Auxiliary + include Static self.partial_name = 'license' - self.file_type = :license + self.file_types = %i(license) self.binary = false def license - blob.project.repository.license + project.repository.license end def render_error diff --git a/app/models/blob_viewer/markup.rb b/app/models/blob_viewer/markup.rb index 8fdbab30dd1..33b59c4f512 100644 --- a/app/models/blob_viewer/markup.rb +++ b/app/models/blob_viewer/markup.rb @@ -5,6 +5,7 @@ module BlobViewer self.partial_name = 'markup' self.extensions = Gitlab::MarkupHelper::EXTENSIONS + self.file_types = %i(readme) self.binary = false end end diff --git a/app/models/blob_viewer/package_json.rb b/app/models/blob_viewer/package_json.rb new file mode 100644 index 00000000000..09221efb56c --- /dev/null +++ b/app/models/blob_viewer/package_json.rb @@ -0,0 +1,23 @@ +module BlobViewer + class PackageJson < DependencyManager + include ServerSide + + self.file_types = %i(package_json) + + def manager_name + 'npm' + end + + def manager_url + 'https://www.npmjs.com/' + end + + def package_name + @package_name ||= package_name_from_json('name') + end + + def package_url + "https://www.npmjs.com/package/#{package_name}" + end + end +end diff --git a/app/models/blob_viewer/podfile.rb b/app/models/blob_viewer/podfile.rb new file mode 100644 index 00000000000..507bc734cb4 --- /dev/null +++ b/app/models/blob_viewer/podfile.rb @@ -0,0 +1,15 @@ +module BlobViewer + class Podfile < DependencyManager + include Static + + self.file_types = %i(podfile) + + def manager_name + 'CocoaPods' + end + + def manager_url + 'https://cocoapods.org/' + end + end +end diff --git a/app/models/blob_viewer/podspec.rb b/app/models/blob_viewer/podspec.rb new file mode 100644 index 00000000000..a4c242db3a9 --- /dev/null +++ b/app/models/blob_viewer/podspec.rb @@ -0,0 +1,27 @@ +module BlobViewer + class Podspec < DependencyManager + include ServerSide + + self.file_types = %i(podspec) + + def manager_name + 'CocoaPods' + end + + def manager_url + 'https://cocoapods.org/' + end + + def package_type + 'pod' + end + + def package_name + @package_name ||= package_name_from_method_call('name') + end + + def package_url + "https://cocoapods.org/pods/#{package_name}" + end + end +end diff --git a/app/models/blob_viewer/podspec_json.rb b/app/models/blob_viewer/podspec_json.rb new file mode 100644 index 00000000000..602f4a51fd9 --- /dev/null +++ b/app/models/blob_viewer/podspec_json.rb @@ -0,0 +1,9 @@ +module BlobViewer + class PodspecJson < Podspec + self.file_types = %i(podspec_json) + + def package_name + @package_name ||= package_name_from_json('name') + end + end +end diff --git a/app/models/blob_viewer/readme.rb b/app/models/blob_viewer/readme.rb new file mode 100644 index 00000000000..75c373a03bb --- /dev/null +++ b/app/models/blob_viewer/readme.rb @@ -0,0 +1,14 @@ +module BlobViewer + class Readme < Base + include Auxiliary + include Static + + self.partial_name = 'readme' + self.file_types = %i(readme) + self.binary = false + + def visible_to?(current_user) + can?(current_user, :read_wiki, project) + end + end +end diff --git a/app/models/blob_viewer/requirements_txt.rb b/app/models/blob_viewer/requirements_txt.rb new file mode 100644 index 00000000000..83ac55f61d0 --- /dev/null +++ b/app/models/blob_viewer/requirements_txt.rb @@ -0,0 +1,15 @@ +module BlobViewer + class RequirementsTxt < DependencyManager + include Static + + self.file_types = %i(requirements_txt) + + def manager_name + 'pip' + end + + def manager_url + 'https://pip.pypa.io/' + end + end +end diff --git a/app/models/blob_viewer/route_map.rb b/app/models/blob_viewer/route_map.rb index 1ca730c1ea0..153b4eeb2c9 100644 --- a/app/models/blob_viewer/route_map.rb +++ b/app/models/blob_viewer/route_map.rb @@ -5,7 +5,7 @@ module BlobViewer self.partial_name = 'route_map' self.loading_partial_name = 'route_map_loading' - self.file_type = :route_map + self.file_types = %i(route_map) self.binary = false def validation_message diff --git a/app/models/blob_viewer/server_side.rb b/app/models/blob_viewer/server_side.rb index e8c5c17b824..87884dcd6bf 100644 --- a/app/models/blob_viewer/server_side.rb +++ b/app/models/blob_viewer/server_side.rb @@ -3,9 +3,9 @@ module BlobViewer extend ActiveSupport::Concern included do - self.client_side = false - self.max_size = 2.megabytes - self.absolute_max_size = 5.megabytes + self.load_async = true + self.overridable_max_size = 2.megabytes + self.max_size = 5.megabytes end def prepare! @@ -13,5 +13,18 @@ module BlobViewer blob.load_all_data!(blob.project.repository) end end + + def render_error + if blob.stored_externally? + # Files that are not stored in the repository, like LFS files and + # build artifacts, can only be rendered using a client-side viewer, + # since we do not want to read large amounts of data into memory on the + # server side. Client-side viewers use JS and can fetch the file from + # `blob_raw_url` using AJAX. + return :server_side_but_stored_externally + end + + super + end end end diff --git a/app/models/blob_viewer/static.rb b/app/models/blob_viewer/static.rb new file mode 100644 index 00000000000..c9e257e5388 --- /dev/null +++ b/app/models/blob_viewer/static.rb @@ -0,0 +1,14 @@ +module BlobViewer + module Static + extend ActiveSupport::Concern + + included do + self.load_async = false + end + + # We can always render a static viewer, even if the blob is too large. + def render_error + nil + end + end +end diff --git a/app/models/blob_viewer/text.rb b/app/models/blob_viewer/text.rb index e27b2c2b493..eddca50b4d4 100644 --- a/app/models/blob_viewer/text.rb +++ b/app/models/blob_viewer/text.rb @@ -5,7 +5,7 @@ module BlobViewer self.partial_name = 'text' self.binary = false - self.max_size = 1.megabyte - self.absolute_max_size = 10.megabytes + self.overridable_max_size = 1.megabyte + self.max_size = 10.megabytes end end diff --git a/app/models/blob_viewer/yarn_lock.rb b/app/models/blob_viewer/yarn_lock.rb new file mode 100644 index 00000000000..31588ddcbab --- /dev/null +++ b/app/models/blob_viewer/yarn_lock.rb @@ -0,0 +1,15 @@ +module BlobViewer + class YarnLock < DependencyManager + include Static + + self.file_types = %i(yarn_lock) + + def manager_name + 'Yarn' + end + + def manager_url + 'https://yarnpkg.com/' + end + end +end diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 3c4a4d93349..760ec8e5919 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -300,8 +300,8 @@ module Ci def execute_hooks return unless project build_data = Gitlab::DataBuilder::Build.build(self) - project.execute_hooks(build_data.dup, :build_hooks) - project.execute_services(build_data.dup, :build_hooks) + project.execute_hooks(build_data.dup, :job_hooks) + project.execute_services(build_data.dup, :job_hooks) PagesService.new(build_data).execute project.running_or_pending_build_count(force: true) end diff --git a/app/models/ci/pipeline_schedule.rb b/app/models/ci/pipeline_schedule.rb index 6d7cc83971e..cf6e53c4ca4 100644 --- a/app/models/ci/pipeline_schedule.rb +++ b/app/models/ci/pipeline_schedule.rb @@ -28,10 +28,18 @@ module Ci !active? end + def deactivate! + update_attribute(:active, false) + end + def importing_or_inactive? importing? || inactive? end + def runnable_by_owner? + Ability.allowed?(owner, :create_pipeline, project) + end + def set_next_run_at self.next_run_at = Gitlab::Ci::CronParser.new(cron, cron_timezone).next_time_from(Time.now) end diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index ffafc678968..fe63728ea23 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -89,6 +89,7 @@ class CommitStatus < ActiveRecord::Base else PipelineUpdateWorker.perform_async(pipeline.id) end + ExpireJobCacheWorker.perform_async(commit_status.id) end end end diff --git a/app/models/concerns/discussion_on_diff.rb b/app/models/concerns/discussion_on_diff.rb index a7bdf5587b2..eee1a36ac6b 100644 --- a/app/models/concerns/discussion_on_diff.rb +++ b/app/models/concerns/discussion_on_diff.rb @@ -47,4 +47,12 @@ module DiscussionOnDiff prev_lines end + + def line_code_in_diffs(diff_refs) + if active?(diff_refs) + line_code + elsif diff_refs && created_at_diff?(diff_refs) + original_line_code + end + end end diff --git a/app/models/concerns/has_status.rb b/app/models/concerns/has_status.rb index dff7b6e3523..3c9c6584e02 100644 --- a/app/models/concerns/has_status.rb +++ b/app/models/concerns/has_status.rb @@ -82,7 +82,7 @@ module HasStatus scope :failed_or_canceled, -> { where(status: [:failed, :canceled]) } scope :cancelable, -> do - where(status: [:running, :pending, :created, :manual]) + where(status: [:running, :pending, :created]) end end diff --git a/app/models/diff_discussion.rb b/app/models/diff_discussion.rb index 14ddd2fcc88..800574d8be0 100644 --- a/app/models/diff_discussion.rb +++ b/app/models/diff_discussion.rb @@ -19,21 +19,9 @@ class DiffDiscussion < Discussion def merge_request_version_params return unless for_merge_request? + return {} if active? - if active? - {} - else - diff_refs = position.diff_refs - - if diff = noteable.merge_request_diff_for(diff_refs) - { diff_id: diff.id } - elsif diff = noteable.merge_request_diff_for(diff_refs.head_sha) - { - diff_id: diff.id, - start_sha: diff_refs.start_sha - } - end - end + noteable.version_params_for(position.diff_refs) end def reply_attributes diff --git a/app/models/diff_note.rb b/app/models/diff_note.rb index 76c59199afd..1764004078e 100644 --- a/app/models/diff_note.rb +++ b/app/models/diff_note.rb @@ -8,6 +8,7 @@ class DiffNote < Note serialize :original_position, Gitlab::Diff::Position serialize :position, Gitlab::Diff::Position + serialize :change_position, Gitlab::Diff::Position validates :original_position, presence: true validates :position, presence: true @@ -25,7 +26,7 @@ class DiffNote < Note DiffDiscussion end - %i(original_position position).each do |meth| + %i(original_position position change_position).each do |meth| define_method "#{meth}=" do |new_position| if new_position.is_a?(String) new_position = JSON.parse(new_position) rescue nil @@ -36,6 +37,8 @@ class DiffNote < Note new_position = Gitlab::Diff::Position.new(new_position) end + return if new_position == read_attribute(meth) + super(new_position) end end @@ -45,7 +48,7 @@ class DiffNote < Note end def diff_line - @diff_line ||= diff_file.line_for_position(self.original_position) if diff_file + @diff_line ||= diff_file&.line_for_position(self.original_position) end def for_line?(line) diff --git a/app/models/hooks/project_hook.rb b/app/models/hooks/project_hook.rb index c631e7a7df5..ee6165fd32d 100644 --- a/app/models/hooks/project_hook.rb +++ b/app/models/hooks/project_hook.rb @@ -5,7 +5,7 @@ class ProjectHook < WebHook scope :confidential_issue_hooks, -> { where(confidential_issues_events: true) } scope :note_hooks, -> { where(note_events: true) } scope :merge_request_hooks, -> { where(merge_requests_events: true) } - scope :build_hooks, -> { where(build_events: true) } + scope :job_hooks, -> { where(job_events: true) } scope :pipeline_hooks, -> { where(pipeline_events: true) } scope :wiki_page_hooks, -> { where(wiki_page_events: true) } end diff --git a/app/models/hooks/web_hook.rb b/app/models/hooks/web_hook.rb index 7cf03aabd6f..a165fdc312f 100644 --- a/app/models/hooks/web_hook.rb +++ b/app/models/hooks/web_hook.rb @@ -8,7 +8,7 @@ class WebHook < ActiveRecord::Base default_value_for :note_events, false default_value_for :merge_requests_events, false default_value_for :tag_push_events, false - default_value_for :build_events, false + default_value_for :job_events, false default_value_for :pipeline_events, false default_value_for :repository_update_events, false default_value_for :enable_ssl_verification, true diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index d7e7ae7a25f..2eec013fa9d 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -293,6 +293,8 @@ class MergeRequest < ActiveRecord::Base attr_writer :target_branch_sha, :source_branch_sha def source_branch_head + return unless source_project + source_branch_ref = @source_branch_sha || source_branch source_project.repository.commit(source_branch_ref) if source_branch_ref end @@ -414,13 +416,24 @@ class MergeRequest < ActiveRecord::Base @merge_request_diffs_by_diff_refs_or_sha[diff_refs_or_sha] end + def version_params_for(diff_refs) + if diff = merge_request_diff_for(diff_refs) + { diff_id: diff.id } + elsif diff = merge_request_diff_for(diff_refs.head_sha) + { + diff_id: diff.id, + start_sha: diff_refs.start_sha + } + end + end + def reload_diff_if_branch_changed if source_branch_changed? || target_branch_changed? reload_diff end end - def reload_diff + def reload_diff(current_user = nil) return unless open? old_diff_refs = self.diff_refs @@ -430,7 +443,8 @@ class MergeRequest < ActiveRecord::Base update_diff_notes_positions( old_diff_refs: old_diff_refs, - new_diff_refs: new_diff_refs + new_diff_refs: new_diff_refs, + current_user: current_user ) end @@ -859,7 +873,7 @@ class MergeRequest < ActiveRecord::Base diff_sha_refs && diff_sha_refs.complete? end - def update_diff_notes_positions(old_diff_refs:, new_diff_refs:) + def update_diff_notes_positions(old_diff_refs:, new_diff_refs:, current_user: nil) return unless has_complete_diff_refs? return if new_diff_refs == old_diff_refs @@ -873,7 +887,7 @@ class MergeRequest < ActiveRecord::Base service = Notes::DiffPositionUpdateService.new( self.project, - nil, + current_user, old_diff_refs: old_diff_refs, new_diff_refs: new_diff_refs, paths: paths diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb index f0a3c30ea74..6e3917a10a3 100644 --- a/app/models/merge_request_diff.rb +++ b/app/models/merge_request_diff.rb @@ -175,12 +175,11 @@ class MergeRequestDiff < ActiveRecord::Base self == merge_request.merge_request_diff end - def compare_with(sha, straight: true) + def compare_with(sha) # When compare merge request versions we want diff A..B instead of A...B # so we handle cases when user does squash and rebase of the commits between versions. # For this reason we set straight to true by default. - CompareService.new(project, head_commit_sha) - .execute(project, sha, straight: straight) + CompareService.new(project, head_commit_sha).execute(project, sha, straight: true) end def commits_count diff --git a/app/models/namespace.rb b/app/models/namespace.rb index a7ede5e3b9e..4d59267f71d 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -46,7 +46,7 @@ class Namespace < ActiveRecord::Base before_destroy(prepend: true) { prepare_for_destroy } after_destroy :rm_dir - scope :root, -> { where('type IS NULL') } + scope :for_user, -> { where('type IS NULL') } scope :with_statistics, -> do joins('LEFT JOIN project_statistics ps ON ps.namespace_id = namespaces.id') diff --git a/app/models/note.rb b/app/models/note.rb index 46d0a4f159f..60257aac93b 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -124,13 +124,12 @@ class Note < ActiveRecord::Base groups = {} diff_notes.fresh.discussions.each do |discussion| - if discussion.active?(diff_refs) - discussions = groups[discussion.line_code] ||= [] - elsif diff_refs && discussion.created_at_diff?(diff_refs) - discussions = groups[discussion.original_line_code] ||= [] - end + line_code = discussion.line_code_in_diffs(diff_refs) - discussions << discussion if discussions + if line_code + discussions = groups[line_code] ||= [] + discussions << discussion + end end groups diff --git a/app/models/project.rb b/app/models/project.rb index 65745fd6d37..cfca0dcd2f2 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -205,7 +205,7 @@ class Project < ActiveRecord::Base presence: true, dynamic_path: true, length: { maximum: 255 }, - format: { with: Gitlab::Regex.project_path_regex, + format: { with: Gitlab::Regex.project_path_format_regex, message: Gitlab::Regex.project_path_regex_message }, uniqueness: { scope: :namespace_id } diff --git a/app/models/project_services/issue_tracker_service.rb b/app/models/project_services/issue_tracker_service.rb index 50435b67eda..eddf308eae3 100644 --- a/app/models/project_services/issue_tracker_service.rb +++ b/app/models/project_services/issue_tracker_service.rb @@ -76,7 +76,7 @@ class IssueTrackerService < Service message = "#{self.type} received response #{response.code} when attempting to connect to #{self.project_url}" result = true end - rescue HTTParty::Error, Timeout::Error, SocketError, Errno::ECONNRESET, Errno::ECONNREFUSED => error + rescue HTTParty::Error, Timeout::Error, SocketError, Errno::ECONNRESET, Errno::ECONNREFUSED, OpenSSL::SSL::SSLError => error message = "#{self.type} had an error when trying to connect to #{self.project_url}: #{error.message}" end Rails.logger.info(message) diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb index f388773efee..a91a986e195 100644 --- a/app/models/project_services/jira_service.rb +++ b/app/models/project_services/jira_service.rb @@ -294,7 +294,7 @@ class JiraService < IssueTrackerService def jira_request yield - rescue Timeout::Error, Errno::EINVAL, Errno::ECONNRESET, Errno::ECONNREFUSED, URI::InvalidURIError, JIRA::HTTPError => e + rescue Timeout::Error, Errno::EINVAL, Errno::ECONNRESET, Errno::ECONNREFUSED, URI::InvalidURIError, JIRA::HTTPError, OpenSSL::SSL::SSLError => e Rails.logger.info "#{self.class.name} Send message ERROR: #{url} - #{e.message}" nil end diff --git a/app/models/repository.rb b/app/models/repository.rb index 9153e5ae5a8..07e0b3bae4f 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -649,22 +649,8 @@ class Repository "#{name}-#{highest_branch_id + 1}" end - # Remove archives older than 2 hours def branches_sorted_by(value) - case value - when 'name' - branches.sort_by(&:name) - when 'updated_desc' - branches.sort do |a, b| - commit(b.dereferenced_target).committed_date <=> commit(a.dereferenced_target).committed_date - end - when 'updated_asc' - branches.sort do |a, b| - commit(a.dereferenced_target).committed_date <=> commit(b.dereferenced_target).committed_date - end - else - branches - end + raw_repository.local_branches(sort_by: value) end def tags_sorted_by(value) diff --git a/app/models/service.rb b/app/models/service.rb index c71a7d169ec..8916f88076e 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -12,7 +12,7 @@ class Service < ActiveRecord::Base default_value_for :merge_requests_events, true default_value_for :tag_push_events, true default_value_for :note_events, true - default_value_for :build_events, true + default_value_for :job_events, true default_value_for :pipeline_events, true default_value_for :wiki_page_events, true @@ -40,7 +40,7 @@ class Service < ActiveRecord::Base scope :confidential_issue_hooks, -> { where(confidential_issues_events: true, active: true) } scope :merge_request_hooks, -> { where(merge_requests_events: true, active: true) } scope :note_hooks, -> { where(note_events: true, active: true) } - scope :build_hooks, -> { where(build_events: true, active: true) } + scope :job_hooks, -> { where(job_events: true, active: true) } scope :pipeline_hooks, -> { where(pipeline_events: true, active: true) } scope :wiki_page_hooks, -> { where(wiki_page_events: true, active: true) } scope :external_issue_trackers, -> { issue_trackers.active.without_defaults } diff --git a/app/models/system_note_metadata.rb b/app/models/system_note_metadata.rb index b44f4fe000c..414c95f7705 100644 --- a/app/models/system_note_metadata.rb +++ b/app/models/system_note_metadata.rb @@ -2,6 +2,7 @@ class SystemNoteMetadata < ActiveRecord::Base ICON_TYPES = %w[ commit description merge confidential visible label assignee cross_reference title time_tracking branch milestone discussion task moved opened closed merged + outdated ].freeze validates :note, presence: true diff --git a/app/models/user.rb b/app/models/user.rb index c7160a6af14..cf3914568a6 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -15,6 +15,7 @@ class User < ActiveRecord::Base add_authentication_token_field :authentication_token add_authentication_token_field :incoming_email_token + add_authentication_token_field :rss_token default_value_for :admin, false default_value_for(:external) { current_application_settings.user_default_external } @@ -166,8 +167,13 @@ class User < ActiveRecord::Base enum dashboard: [:projects, :stars, :project_activity, :starred_project_activity, :groups, :todos] # User's Project preference - # Note: When adding an option, it MUST go on the end of the array. - enum project_view: [:readme, :activity, :files] + # + # Note: When adding an option, it MUST go on the end of the hash with a + # number higher than the current max. We cannot move options and/or change + # their numbers. + # + # We skip 0 because this was used by an option that has since been removed. + enum project_view: { activity: 1, files: 2 } alias_attribute :private_token, :authentication_token @@ -350,7 +356,7 @@ class User < ActiveRecord::Base end def find_by_full_path(path, follow_redirects: false) - namespace = Namespace.find_by_full_path(path, follow_redirects: follow_redirects) + namespace = Namespace.for_user.find_by_full_path(path, follow_redirects: follow_redirects) namespace&.owner end @@ -930,10 +936,18 @@ class User < ActiveRecord::Base end def invalidate_cache_counts - Rails.cache.delete(['users', id, 'assigned_open_merge_requests_count']) + invalidate_issue_cache_counts + invalidate_merge_request_cache_counts + end + + def invalidate_issue_cache_counts Rails.cache.delete(['users', id, 'assigned_open_issues_count']) end + def invalidate_merge_request_cache_counts + Rails.cache.delete(['users', id, 'assigned_open_merge_requests_count']) + end + def todos_done_count(force: false) Rails.cache.fetch(['users', id, 'todos_done_count'], force: force) do TodosFinder.new(self, state: :done).execute.count @@ -991,6 +1005,13 @@ class User < ActiveRecord::Base save end + # each existing user needs to have an `rss_token`. + # we do this on read since migrating all existing users is not a feasible + # solution. + def rss_token + ensure_rss_token! + end + protected # override, from Devise::Validatable diff --git a/app/serializers/merge_request_basic_entity.rb b/app/serializers/merge_request_basic_entity.rb index 8771345c135..8461f158bb5 100644 --- a/app/serializers/merge_request_basic_entity.rb +++ b/app/serializers/merge_request_basic_entity.rb @@ -1,4 +1,5 @@ class MergeRequestBasicEntity < Grape::Entity + expose :assignee_id expose :merge_status expose :merge_error expose :state diff --git a/app/serializers/merge_request_entity.rb b/app/serializers/merge_request_entity.rb index a2542c54f7a..b3247ae36dd 100644 --- a/app/serializers/merge_request_entity.rb +++ b/app/serializers/merge_request_entity.rb @@ -1,7 +1,6 @@ class MergeRequestEntity < IssuableEntity include RequestAwareEntity - expose :assignee_id expose :in_progress_merge_commit_sha expose :locked_at expose :merge_commit_sha @@ -97,6 +96,14 @@ class MergeRequestEntity < IssuableEntity presenter(merge_request).target_branch_commits_path end + expose :new_blob_path do |merge_request| + if can?(current_user, :push_code, merge_request.project) + namespace_project_new_blob_path(merge_request.project.namespace, + merge_request.project, + merge_request.source_branch) + end + end + expose :conflict_resolution_path do |merge_request| presenter(merge_request).conflict_resolution_path end @@ -146,12 +153,6 @@ class MergeRequestEntity < IssuableEntity format: :json) end - expose :merge_check_path do |merge_request| - merge_check_namespace_project_merge_request_path(merge_request.project.namespace, - merge_request.project, - merge_request) - end - expose :ci_environments_status_path do |merge_request| ci_environments_status_namespace_project_merge_request_path(merge_request.project.namespace, merge_request.project, diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb index dc2ab99b982..e94ab3e64db 100644 --- a/app/services/issuable_base_service.rb +++ b/app/services/issuable_base_service.rb @@ -178,7 +178,7 @@ class IssuableBaseService < BaseService after_create(issuable) issuable.create_cross_references!(current_user) execute_hooks(issuable) - issuable.assignees.each(&:invalidate_cache_counts) + invalidate_cache_counts(issuable.assignees, issuable) end issuable @@ -237,7 +237,7 @@ class IssuableBaseService < BaseService if old_assignees != issuable.assignees assignees = old_assignees + issuable.assignees.to_a - assignees.compact.each(&:invalidate_cache_counts) + invalidate_cache_counts(assignees.compact, issuable) end after_update(issuable) @@ -330,4 +330,10 @@ class IssuableBaseService < BaseService create_labels_note(issuable, old_labels) if issuable.labels != old_labels end + + def invalidate_cache_counts(users, issuable) + users.each do |user| + user.public_send("invalidate_#{issuable.model_name.singular}_cache_counts") + end + end end diff --git a/app/services/members/authorized_destroy_service.rb b/app/services/members/authorized_destroy_service.rb index 7912cac65d3..f846d72498f 100644 --- a/app/services/members/authorized_destroy_service.rb +++ b/app/services/members/authorized_destroy_service.rb @@ -10,7 +10,7 @@ module Members return false if member.is_a?(GroupMember) && member.source.last_owner?(member.user) Member.transaction do - unassign_issues_and_merge_requests(member) + unassign_issues_and_merge_requests(member) unless member.invite? member.destroy end @@ -26,10 +26,14 @@ module Members def unassign_issues_and_merge_requests(member) if member.is_a?(GroupMember) - issue_ids = IssuesFinder.new(user, group_id: member.source_id, assignee_id: member.user_id). - execute.pluck(:id) + issues = Issue.unscoped.select(1). + joins(:project). + where('issues.id = issue_assignees.issue_id AND projects.namespace_id = ?', member.source_id) - IssueAssignee.destroy_all(issue_id: issue_ids, user_id: member.user_id) + # DELETE FROM issue_assignees WHERE user_id = X AND EXISTS (...) + IssueAssignee.unscoped. + where('user_id = :user_id AND EXISTS (:sub)', user_id: member.user_id, sub: issues). + delete_all MergeRequestsFinder.new(user, group_id: member.source_id, assignee_id: member.user_id). execute. @@ -37,10 +41,15 @@ module Members else project = member.source - IssueAssignee.destroy_all( - user_id: member.user_id, - issue_id: project.issues.opened.assigned_to(member.user).select(:id) - ) + # SELECT 1 FROM issues WHERE issues.id = issue_assignees.issue_id AND issues.project_id = X + issues = Issue.unscoped.select(1). + where('issues.id = issue_assignees.issue_id'). + where(project_id: project.id) + + # DELETE FROM issue_assignees WHERE user_id = X AND EXISTS (...) + IssueAssignee.unscoped. + where('user_id = :user_id AND EXISTS (:sub)', user_id: member.user_id, sub: issues). + delete_all project.merge_requests.opened.assigned_to(member.user).update_all(assignee_id: nil) end diff --git a/app/services/merge_requests/conflicts/list_service.rb b/app/services/merge_requests/conflicts/list_service.rb index 9bf82518643..9835606812c 100644 --- a/app/services/merge_requests/conflicts/list_service.rb +++ b/app/services/merge_requests/conflicts/list_service.rb @@ -15,6 +15,7 @@ module MergeRequests return @conflicts_can_be_resolved_in_ui = false unless merge_request.cannot_be_merged? return @conflicts_can_be_resolved_in_ui = false unless merge_request.has_complete_diff_refs? + return @conflicts_can_be_resolved_in_ui = false if merge_request.branch_missing? begin # Try to parse each conflict. If the MR's mergeable status hasn't been diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb index 1131d6f4913..81d217929d5 100644 --- a/app/services/merge_requests/refresh_service.rb +++ b/app/services/merge_requests/refresh_service.rb @@ -66,12 +66,12 @@ module MergeRequests filter_merge_requests(merge_requests).each do |merge_request| if merge_request.source_branch == @branch_name || force_push? - merge_request.reload_diff + merge_request.reload_diff(current_user) else mr_commit_ids = merge_request.commits_sha push_commit_ids = @commits.map(&:id) matches = mr_commit_ids & push_commit_ids - merge_request.reload_diff if matches.any? + merge_request.reload_diff(current_user) if matches.any? end merge_request.mark_as_unchecked diff --git a/app/services/merge_requests/reopen_service.rb b/app/services/merge_requests/reopen_service.rb index fadcce5d9b6..54b19e6d651 100644 --- a/app/services/merge_requests/reopen_service.rb +++ b/app/services/merge_requests/reopen_service.rb @@ -8,7 +8,7 @@ module MergeRequests create_note(merge_request) notification_service.reopen_mr(merge_request, current_user) execute_hooks(merge_request, 'reopen') - merge_request.reload_diff + merge_request.reload_diff(current_user) merge_request.mark_as_unchecked end diff --git a/app/services/notes/diff_position_update_service.rb b/app/services/notes/diff_position_update_service.rb index 0cb731f5bc3..eff7b287269 100644 --- a/app/services/notes/diff_position_update_service.rb +++ b/app/services/notes/diff_position_update_service.rb @@ -1,26 +1,29 @@ module Notes class DiffPositionUpdateService < BaseService def execute(note) - new_position = tracer.trace(note.position) + results = tracer.trace(note.position) + return unless results - # Don't update the position if the type doesn't match, since that means - # the diff line commented on was changed, and the comment is now outdated - old_position = note.position - if new_position && - new_position != old_position && - new_position.type == old_position.type + position = results[:position] + outdated = results[:outdated] - note.position = new_position - end + if outdated + note.change_position = position - note + if note.persisted? && current_user + SystemNoteService.diff_discussion_outdated(note.to_discussion, project, current_user, position) + end + else + note.position = position + note.change_position = nil + end end private def tracer @tracer ||= Gitlab::Diff::PositionTracer.new( - repository: project.repository, + project: project, old_diff_refs: params[:old_diff_refs], new_diff_refs: params[:new_diff_refs], paths: params[:paths] diff --git a/app/services/projects/transfer_service.rb b/app/services/projects/transfer_service.rb index da6e6acd4a7..1c24b27a870 100644 --- a/app/services/projects/transfer_service.rb +++ b/app/services/projects/transfer_service.rb @@ -12,12 +12,13 @@ module Projects TransferError = Class.new(StandardError) def execute(new_namespace) - if allowed_transfer?(current_user, project, new_namespace) - transfer(project, new_namespace) - else - project.errors.add(:new_namespace, 'is invalid') - false + if new_namespace.blank? + raise TransferError, 'Please select a new namespace for your project.' end + unless allowed_transfer?(current_user, project, new_namespace) + raise TransferError, 'Transfer failed, please contact an admin.' + end + transfer(project, new_namespace) rescue Projects::TransferService::TransferError => ex project.reload project.errors.add(:new_namespace, ex.message) diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb index 93bf1fb1615..0837c07e6aa 100644 --- a/app/services/system_note_service.rb +++ b/app/services/system_note_service.rb @@ -258,7 +258,7 @@ module SystemNoteService create_note(NoteSummary.new(noteable, project, author, body, action: 'title')) end - def self.resolve_all_discussions(merge_request, project, author) + def resolve_all_discussions(merge_request, project, author) body = "resolved all discussions" create_note(NoteSummary.new(merge_request, project, author, body, action: 'discussion')) @@ -274,6 +274,28 @@ module SystemNoteService note end + def diff_discussion_outdated(discussion, project, author, change_position) + merge_request = discussion.noteable + diff_refs = change_position.diff_refs + version_index = merge_request.merge_request_diffs.viewable.count + + body = "changed this line in" + if version_params = merge_request.version_params_for(diff_refs) + line_code = change_position.line_code(project.repository) + url = url_helpers.diffs_namespace_project_merge_request_url(project.namespace, project, merge_request, version_params.merge(anchor: line_code)) + + body << " [version #{version_index} of the diff](#{url})" + else + body << " version #{version_index} of the diff" + end + + note_attributes = discussion.reply_attributes.merge(project: project, author: author, note: body) + note = Note.create(note_attributes.merge(system: true)) + note.system_note_metadata = SystemNoteMetadata.new(action: 'outdated') + + note + end + # Called when the title of a Noteable is changed # # noteable - Noteable object that responds to `title` diff --git a/app/validators/dynamic_path_validator.rb b/app/validators/dynamic_path_validator.rb index d992b0c3725..8d4d7180baf 100644 --- a/app/validators/dynamic_path_validator.rb +++ b/app/validators/dynamic_path_validator.rb @@ -6,199 +6,26 @@ # Values are checked for formatting and exclusion from a list of reserved path # names. class DynamicPathValidator < ActiveModel::EachValidator - # All routes that appear on the top level must be listed here. - # This will make sure that groups cannot be created with these names - # as these routes would be masked by the paths already in place. - # - # Example: - # /api/api-project - # - # the path `api` shouldn't be allowed because it would be masked by `api/*` - # - TOP_LEVEL_ROUTES = %w[ - - - .well-known - abuse_reports - admin - all - api - assets - autocomplete - ci - dashboard - explore - files - groups - health_check - help - hooks - import - invites - issues - jwt - koding - member - merge_requests - new - notes - notification_settings - oauth - profile - projects - public - repository - robots.txt - s - search - sent_notifications - services - snippets - teams - u - unicorn_test - unsubscribes - uploads - users - ].freeze - - # This list should contain all words following `/*namespace_id/:project_id` in - # routes that contain a second wildcard. - # - # Example: - # /*namespace_id/:project_id/badges/*ref/build - # - # If `badges` was allowed as a project/group name, we would not be able to access the - # `badges` route for those projects: - # - # Consider a namespace with path `foo/bar` and a project called `badges`. - # The route to the build badge would then be `/foo/bar/badges/badges/master/build.svg` - # - # When accessing this path the route would be matched to the `badges` path - # with the following params: - # - namespace_id: `foo` - # - project_id: `bar` - # - ref: `badges/master` - # - # Failing to find the project, this would result in a 404. - # - # By rejecting `badges` the router can _count_ on the fact that `badges` will - # be preceded by the `namespace/project`. - WILDCARD_ROUTES = %w[ - badges - blame - blob - builds - commits - create - create_dir - edit - environments/folders - files - find_file - gitlab-lfs/objects - info/lfs/objects - new - preview - raw - refs - tree - update - wikis - ].freeze - - # These are all the paths that follow `/groups/*id/ or `/groups/*group_id` - # We need to reject these because we have a `/groups/*id` page that is the same - # as the `/*id`. - # - # If we would allow a subgroup to be created with the name `activity` then - # this group would not be accessible through `/groups/parent/activity` since - # this would map to the activity-page of it's parent. - GROUP_ROUTES = %w[ - activity - analytics - audit_events - avatar - edit - group_members - hooks - issues - labels - ldap - ldap_group_links - merge_requests - milestones - notification_setting - pipeline_quota - projects - subgroups - ].freeze - - CHILD_ROUTES = (WILDCARD_ROUTES | GROUP_ROUTES).freeze - - def self.without_reserved_wildcard_paths_regex - @without_reserved_wildcard_paths_regex ||= regex_excluding_child_paths(WILDCARD_ROUTES) - end - - def self.without_reserved_child_paths_regex - @without_reserved_child_paths_regex ||= regex_excluding_child_paths(CHILD_ROUTES) - end - - # This is used to validate a full path. - # It doesn't match paths - # - Starting with one of the top level words - # - Containing one of the child level words in the middle of a path - def self.regex_excluding_child_paths(child_routes) - reserved_top_level_words = Regexp.union(TOP_LEVEL_ROUTES) - not_starting_in_reserved_word = %r{\A/?(?!(#{reserved_top_level_words})(/|\z))} - - reserved_child_level_words = Regexp.union(child_routes) - not_containing_reserved_child = %r{(?!\S+/(#{reserved_child_level_words})(/|\z))} - - %r{#{not_starting_in_reserved_word} - #{not_containing_reserved_child} - #{Gitlab::Regex.full_namespace_regex}}x - end - - def self.valid?(path) - path =~ Gitlab::Regex.full_namespace_regex && !full_path_reserved?(path) - end - - def self.full_path_reserved?(path) - path = path.to_s.downcase - _project_part, namespace_parts = path.reverse.split('/', 2).map(&:reverse) - - wildcard_reserved?(path) || child_reserved?(namespace_parts) - end - - def self.child_reserved?(path) - return false unless path - - path !~ without_reserved_child_paths_regex - end - - def self.wildcard_reserved?(path) - return false unless path + class << self + def valid_namespace_path?(path) + "#{path}/" =~ Gitlab::Regex.full_namespace_path_regex + end - path !~ without_reserved_wildcard_paths_regex + def valid_project_path?(path) + "#{path}/" =~ Gitlab::Regex.full_project_path_regex + end end - delegate :full_path_reserved?, - :child_reserved?, - to: :class - - def path_reserved_for_record?(record, value) + def path_valid_for_record?(record, value) full_path = record.respond_to?(:full_path) ? record.full_path : value - # For group paths the entire path cannot contain a reserved child word - # The path doesn't contain the last `_project_part` so we need to validate - # if the entire path. - # Example: - # A *group* with full path `parent/activity` is reserved. - # A *project* with full path `parent/activity` is allowed. - if record.is_a? Group - child_reserved?(full_path) + return true unless full_path + + case record + when Project + self.class.valid_project_path?(full_path) else - full_path_reserved?(full_path) + self.class.valid_namespace_path?(full_path) end end @@ -208,7 +35,7 @@ class DynamicPathValidator < ActiveModel::EachValidator return end - if path_reserved_for_record?(record, value) + unless path_valid_for_record?(record, value) record.errors.add(attribute, "#{value} is a reserved name") end end diff --git a/app/views/admin/health_check/show.html.haml b/app/views/admin/health_check/show.html.haml index 6a208d76a38..4deccf4aa93 100644 --- a/app/views/admin/health_check/show.html.haml +++ b/app/views/admin/health_check/show.html.haml @@ -16,24 +16,15 @@ = icon('spinner') Reset health check access token %p.light - Health information can be retrieved as plain text, JSON, or XML using: + Health information can be retrieved from the following endpoints. More information is available + = link_to 'here', help_page_path('user/admin_area/monitoring/health_check') %ul %li - %code= health_check_url(token: current_application_settings.health_check_access_token) + %code= readiness_url(token: current_application_settings.health_check_access_token) %li - %code= health_check_url(token: current_application_settings.health_check_access_token, format: :json) + %code= liveness_url(token: current_application_settings.health_check_access_token) %li - %code= health_check_url(token: current_application_settings.health_check_access_token, format: :xml) - - %p.light - You can also ask for the status of specific services: - %ul - %li - %code= health_check_url(token: current_application_settings.health_check_access_token, checks: :cache) - %li - %code= health_check_url(token: current_application_settings.health_check_access_token, checks: :database) - %li - %code= health_check_url(token: current_application_settings.health_check_access_token, checks: :migrations) + %code= metrics_url(token: current_application_settings.health_check_access_token) %hr .panel.panel-default diff --git a/app/views/admin/hooks/index.html.haml b/app/views/admin/hooks/index.html.haml index 3338b677bf5..e92b8bc39f4 100644 --- a/app/views/admin/hooks/index.html.haml +++ b/app/views/admin/hooks/index.html.haml @@ -27,7 +27,7 @@ = link_to 'Remove', admin_hook_path(hook), data: { confirm: 'Are you sure?' }, method: :delete, class: 'btn btn-remove btn-sm' .monospace= hook.url %div - - %w(repository_update_events push_events tag_push_events issues_events note_events merge_requests_events build_events).each do |trigger| + - %w(repository_update_events push_events tag_push_events issues_events note_events merge_requests_events job_events).each do |trigger| - if hook.send(trigger) %span.label.label-gray= trigger.titleize %span.label.label-gray SSL Verification: #{hook.enable_ssl_verification ? 'enabled' : 'disabled'} diff --git a/app/views/admin/requests_profiles/index.html.haml b/app/views/admin/requests_profiles/index.html.haml index ae918086a57..c7b63d9de98 100644 --- a/app/views/admin/requests_profiles/index.html.haml +++ b/app/views/admin/requests_profiles/index.html.haml @@ -20,7 +20,7 @@ %ul.content-list - profiles.each do |profile| %li - = link_to profile.time.to_s(:long), admin_requests_profile_path(profile), data: {no_turbolink: true} + = link_to profile.time.to_s(:long), admin_requests_profile_path(profile) - else %p No profiles found diff --git a/app/views/admin/users/index.html.haml b/app/views/admin/users/index.html.haml index c7cd86527d3..5516134d8a0 100644 --- a/app/views/admin/users/index.html.haml +++ b/app/views/admin/users/index.html.haml @@ -3,41 +3,43 @@ = render "admin/dashboard/head" %div{ class: container_class } - .top-area - .prepend-top-default - = form_tag admin_users_path, method: :get do - - if params[:filter].present? - = hidden_field_tag "filter", h(params[:filter]) - .search-holder - .search-field-holder - = search_field_tag :search_query, params[:search_query], placeholder: 'Search by name, email or username', class: 'form-control search-text-input js-search-input', spellcheck: false - = icon("search", class: "search-icon") - .dropdown - - toggle_text = if @sort.present? then sort_options_hash[@sort] else sort_title_name end - = dropdown_toggle(toggle_text, { toggle: 'dropdown' }) - %ul.dropdown-menu.dropdown-menu-align-right - %li.dropdown-header - Sort by - %li - = link_to admin_users_path(sort: sort_value_name, filter: params[:filter]) do - = sort_title_name - = link_to admin_users_path(sort: sort_value_recently_signin, filter: params[:filter]) do - = sort_title_recently_signin - = link_to admin_users_path(sort: sort_value_oldest_signin, filter: params[:filter]) do - = sort_title_oldest_signin - = link_to admin_users_path(sort: sort_value_recently_created, filter: params[:filter]) do - = sort_title_recently_created - = link_to admin_users_path(sort: sort_value_oldest_created, filter: params[:filter]) do - = sort_title_oldest_created - = link_to admin_users_path(sort: sort_value_recently_updated, filter: params[:filter]) do - = sort_title_recently_updated - = link_to admin_users_path(sort: sort_value_oldest_updated, filter: params[:filter]) do - = sort_title_oldest_updated - = link_to 'New user', new_admin_user_path, class: 'btn btn-new btn-search' + .prepend-top-default + = form_tag admin_users_path, method: :get do + - if params[:filter].present? + = hidden_field_tag "filter", h(params[:filter]) + .search-holder + .search-field-holder + = search_field_tag :search_query, params[:search_query], placeholder: 'Search by name, email or username', class: 'form-control search-text-input js-search-input', spellcheck: false + = icon("search", class: "search-icon") + .dropdown + - toggle_text = if @sort.present? then sort_options_hash[@sort] else sort_title_name end + = dropdown_toggle(toggle_text, { toggle: 'dropdown' }) + %ul.dropdown-menu.dropdown-menu-align-right + %li.dropdown-header + Sort by + %li + = link_to admin_users_path(sort: sort_value_name, filter: params[:filter]) do + = sort_title_name + = link_to admin_users_path(sort: sort_value_recently_signin, filter: params[:filter]) do + = sort_title_recently_signin + = link_to admin_users_path(sort: sort_value_oldest_signin, filter: params[:filter]) do + = sort_title_oldest_signin + = link_to admin_users_path(sort: sort_value_recently_created, filter: params[:filter]) do + = sort_title_recently_created + = link_to admin_users_path(sort: sort_value_oldest_created, filter: params[:filter]) do + = sort_title_oldest_created + = link_to admin_users_path(sort: sort_value_recently_updated, filter: params[:filter]) do + = sort_title_recently_updated + = link_to admin_users_path(sort: sort_value_oldest_updated, filter: params[:filter]) do + = sort_title_oldest_updated + = link_to 'New user', new_admin_user_path, class: 'btn btn-new btn-search' - .nav-block - %ul.nav-links.wide.scrolling-tabs.white.scrolling-tabs - .fade-left + .top-area.scrolling-tabs-container.inner-page-scroll-tabs + .fade-left + = icon('angle-left') + .fade-right + = icon('angle-right') + %ul.nav-links.scrolling-tabs = nav_link(html_options: { class: active_when(params[:filter].nil?) }) do = link_to admin_users_path do Active @@ -66,7 +68,6 @@ = link_to admin_users_path(filter: "wop") do Without projects %small.badge= number_with_delimiter(User.without_projects.count) - .fade-right %ul.flex-list.content-list - if @users.empty? diff --git a/app/views/award_emoji/_awards_block.html.haml b/app/views/award_emoji/_awards_block.html.haml index 9aabfb49a29..5f07d2720c2 100644 --- a/app/views/award_emoji/_awards_block.html.haml +++ b/app/views/award_emoji/_awards_block.html.haml @@ -12,9 +12,9 @@ - if current_user .award-menu-holder.js-award-holder %button.btn.award-control.has-tooltip.js-add-award{ type: 'button', - 'aria-label': 'Add emoji', + 'aria-label': 'Add reaction', class: ("js-user-authored" if user_authored), - data: { title: 'Add emoji', placement: "bottom" } } + data: { title: 'Add reaction', placement: "bottom" } } %span{ class: "award-control-icon award-control-icon-neutral" }= custom_icon('emoji_slightly_smiling_face') %span{ class: "award-control-icon award-control-icon-positive" }= custom_icon('emoji_smiley') %span{ class: "award-control-icon award-control-icon-super-positive" }= custom_icon('emoji_smile') diff --git a/app/views/dashboard/_activities.html.haml b/app/views/dashboard/_activities.html.haml index 89d991abe54..e1b270a08c2 100644 --- a/app/views/dashboard/_activities.html.haml +++ b/app/views/dashboard/_activities.html.haml @@ -1,7 +1,7 @@ .hidden-xs = render "events/event_last_push", event: @last_push -.nav-block +.nav-block.activities .controls = link_to dashboard_projects_path(rss_url_options), class: 'btn rss-btn has-tooltip', title: 'Subscribe' do %i.fa.fa-rss diff --git a/app/views/devise/passwords/edit.html.haml b/app/views/devise/passwords/edit.html.haml index 5e189e6dc54..eb0e6701627 100644 --- a/app/views/devise/passwords/edit.html.haml +++ b/app/views/devise/passwords/edit.html.haml @@ -6,10 +6,10 @@ = devise_error_messages! = f.hidden_field :reset_password_token .form-group - = f.label 'New password', for: :password + = f.label 'New password', for: "user_password" = f.password_field :password, class: "form-control top", required: true, title: 'This field is required' .form-group - = f.label 'Confirm new password', for: :password_confirmation + = f.label 'Confirm new password', for: "user_password_confirmation" = f.password_field :password_confirmation, class: "form-control bottom", title: 'This field is required', required: true .clearfix = f.submit "Change your password", class: "btn btn-primary" diff --git a/app/views/devise/sessions/_new_base.html.haml b/app/views/devise/sessions/_new_base.html.haml index 21c751a23f8..4095f30c369 100644 --- a/app/views/devise/sessions/_new_base.html.haml +++ b/app/views/devise/sessions/_new_base.html.haml @@ -1,6 +1,6 @@ = form_for(resource, as: resource_name, url: session_path(resource_name), html: { class: 'new_user gl-show-field-errors', 'aria-live' => 'assertive'}) do |f| .form-group - = f.label "Username or email", for: :login + = f.label "Username or email", for: "user_login" = f.text_field :login, class: "form-control top", autofocus: "autofocus", autocapitalize: "off", autocorrect: "off", required: true, title: "This field is required." .form-group = f.label :password diff --git a/app/views/discussions/_discussion.html.haml b/app/views/discussions/_discussion.html.haml index 74992e439f3..578e751ab47 100644 --- a/app/views/discussions/_discussion.html.haml +++ b/app/views/discussions/_discussion.html.haml @@ -32,10 +32,9 @@ - elsif discussion.diff_discussion? on = conditional_link_to url.present?, url do - - if discussion.active? - the diff - - else - an outdated diff + - unless discussion.active? + an old version of + the diff = time_ago_with_tooltip(discussion.created_at, placement: "bottom", html_class: "note-created-ago") = render "discussions/headline", discussion: discussion diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml index afcc2b6e4f3..9e354987401 100644 --- a/app/views/layouts/_head.html.haml +++ b/app/views/layouts/_head.html.haml @@ -27,6 +27,7 @@ = stylesheet_link_tag "application", media: "all" = stylesheet_link_tag "print", media: "print" + = stylesheet_link_tag "test", media: "all" if Rails.env.test? = Gon::Base.render_data @@ -34,6 +35,7 @@ = webpack_bundle_tag "common" = webpack_bundle_tag "main" = webpack_bundle_tag "raven" if current_application_settings.clientside_sentry_enabled + = webpack_bundle_tag "test" if Rails.env.test? - if content_for?(:page_specific_javascripts) = yield :page_specific_javascripts diff --git a/app/views/layouts/_init_auto_complete.html.haml b/app/views/layouts/_init_auto_complete.html.haml index 769f6fb0151..6caaba240bb 100644 --- a/app/views/layouts/_init_auto_complete.html.haml +++ b/app/views/layouts/_init_auto_complete.html.haml @@ -3,6 +3,7 @@ - if project :javascript + gl.GfmAutoComplete = gl.GfmAutoComplete || {}; gl.GfmAutoComplete.dataSources = { members: "#{members_namespace_project_autocomplete_sources_path(project.namespace, project, type: noteable_type, type_id: params[:id])}", issues: "#{issues_namespace_project_autocomplete_sources_path(project.namespace, project)}", @@ -11,5 +12,3 @@ milestones: "#{milestones_namespace_project_autocomplete_sources_path(project.namespace, project)}", commands: "#{commands_namespace_project_autocomplete_sources_path(project.namespace, project, type: noteable_type, type_id: params[:id])}" }; - - gl.GfmAutoComplete.setup(); diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index 7e011ac3e75..03688e9ff21 100644 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -2,8 +2,8 @@ %html{ lang: I18n.locale, class: "#{page_class}" } = render "layouts/head" %body{ class: @body_class, data: { page: body_data_page, project: "#{@project.path if @project}", group: "#{@group.path if @group}" } } + = render "layouts/init_auto_complete" if @gfm_form = render "layouts/header/default", title: header_title = render 'layouts/page', sidebar: sidebar, nav: nav = yield :scripts_body - = render "layouts/init_auto_complete" if @gfm_form diff --git a/app/views/layouts/nav/_profile.html.haml b/app/views/layouts/nav/_profile.html.haml index e06301bda14..ae1e1361f0f 100644 --- a/app/views/layouts/nav/_profile.html.haml +++ b/app/views/layouts/nav/_profile.html.haml @@ -48,6 +48,6 @@ %span Preferences = nav_link(path: 'profiles#audit_log') do - = link_to audit_log_profile_path, title: 'Audit Log' do + = link_to audit_log_profile_path, title: 'Authentication log' do %span - Audit Log + Authentication log diff --git a/app/views/profiles/_event_table.html.haml b/app/views/profiles/_event_table.html.haml index 879fc170f92..d0ad90ac6cc 100644 --- a/app/views/profiles/_event_table.html.haml +++ b/app/views/profiles/_event_table.html.haml @@ -9,7 +9,6 @@ Signed in with = event.details[:with] authentication - %span.pull-right - #{time_ago_in_words event.created_at} ago + %span.pull-right= time_ago_with_tooltip(event.created_at) = paginate events, theme: "gitlab" diff --git a/app/views/profiles/accounts/_reset_token.html.haml b/app/views/profiles/accounts/_reset_token.html.haml new file mode 100644 index 00000000000..c31a4a8ecd4 --- /dev/null +++ b/app/views/profiles/accounts/_reset_token.html.haml @@ -0,0 +1,11 @@ +- name = label.parameterize +- attribute = name.underscore + +.reset-action + %p.cgray + = label_tag name, label, class: "label-light" + = text_field_tag name, current_user.send(attribute), class: 'form-control', readonly: true, onclick: 'this.select()' + %p.help-block + = help_text + .prepend-top-default + = link_to button_label, [:reset, attribute, :profile], method: :put, data: { confirm: 'Are you sure?' }, class: 'btn btn-default private-token' diff --git a/app/views/profiles/accounts/show.html.haml b/app/views/profiles/accounts/show.html.haml index 73f33e69d68..a319b18e507 100644 --- a/app/views/profiles/accounts/show.html.haml +++ b/app/views/profiles/accounts/show.html.haml @@ -8,35 +8,17 @@ .row.prepend-top-default .col-lg-3.profile-settings-sidebar %h4.prepend-top-0 - = incoming_email_token_enabled? ? "Private Tokens" : "Private Token" + Private Tokens %p - Keep - = incoming_email_token_enabled? ? "these tokens" : "this token" - secret, anyone with access to them can interact with GitLab as if they were you. + Keep these tokens secret, anyone with access to them can interact with + GitLab as if they were you. .col-lg-9.private-tokens-reset - .reset-action - %p.cgray - - if current_user.private_token - = label_tag "private-token", "Private token", class: "label-light" - = text_field_tag "private-token", current_user.private_token, class: "form-control", readonly: true, onclick: "this.select()" - - else - %span You don't have one yet. Click generate to fix it. - %p.help-block - Your private token is used to access the API and Atom feeds without username/password authentication. - .prepend-top-default - - if current_user.private_token - = link_to 'Reset private token', reset_private_token_profile_path, method: :put, data: { confirm: "Are you sure?" }, class: "btn btn-default private-token" - - else - = f.submit 'Generate', class: "btn btn-default" + = render partial: 'reset_token', locals: { label: 'Private token', button_label: 'Reset private token', help_text: 'Your private token is used to access the API and Atom feeds without username/password authentication.' } + + = render partial: 'reset_token', locals: { label: 'RSS token', button_label: 'Reset RSS token', help_text: 'Your RSS token is used to create urls for personalized RSS feeds.' } + - if incoming_email_token_enabled? - .reset-action - %p.cgray - = label_tag "incoming-email-token", "Incoming Email Token", class: 'label-light' - = text_field_tag "incoming-email-token", current_user.incoming_email_token, class: "form-control", readonly: true, onclick: "this.select()" - %p.help-block - Your incoming email token is used to create new issues by email, and is included in your project-specific email addresses. - .prepend-top-default - = link_to 'Reset incoming email token', reset_incoming_email_token_profile_path, method: :put, data: { confirm: "Are you sure?" }, class: "btn btn-default incoming-email-token" + = render partial: 'reset_token', locals: { label: 'Incoming email token', button_label: 'Reset incoming email token', help_text: 'Your incoming email token is used to create new issues by email, and is included in your project-specific email addresses.' } %hr .row.prepend-top-default diff --git a/app/views/profiles/audit_log.html.haml b/app/views/profiles/audit_log.html.haml index 9fe86e6b291..a24b7fd101d 100644 --- a/app/views/profiles/audit_log.html.haml +++ b/app/views/profiles/audit_log.html.haml @@ -1,4 +1,4 @@ -- page_title "Audit Log" +- page_title "Authentication log" = render 'profiles/head' .row.prepend-top-default diff --git a/app/views/profiles/preferences/show.html.haml b/app/views/profiles/preferences/show.html.haml index 99690e6b98a..0ff19b3eab1 100644 --- a/app/views/profiles/preferences/show.html.haml +++ b/app/views/profiles/preferences/show.html.haml @@ -37,9 +37,9 @@ = f.select :dashboard, dashboard_choices, {}, class: 'form-control' .form-group = f.label :project_view, class: 'label-light' do - Project view + Project home page content = f.select :project_view, project_view_choices, {}, class: 'form-control' .help-block - Choose what content you want to see on a project's home page. + Choose what content you want to see on a project’s home page .form-group = f.submit 'Save changes', class: 'btn btn-save' diff --git a/app/views/projects/_activity.html.haml b/app/views/projects/_activity.html.haml index aa0cb3e1a50..f5bb7364d4a 100644 --- a/app/views/projects/_activity.html.haml +++ b/app/views/projects/_activity.html.haml @@ -1,7 +1,7 @@ - @no_container = true %div{ class: container_class } - .nav-block.activity-filter-block + .nav-block.activity-filter-block.activities .controls = link_to namespace_project_path(@project.namespace, @project, rss_url_options), title: "Subscribe", class: 'btn rss-btn has-tooltip' do = icon('rss') diff --git a/app/views/projects/_files.html.haml b/app/views/projects/_files.html.haml index 96c2fa87f45..426085b3e1c 100644 --- a/app/views/projects/_files.html.haml +++ b/app/views/projects/_files.html.haml @@ -1,6 +1,14 @@ +- commit = local_assigns.fetch(:commit) { @repository.commit } +- ref = local_assigns.fetch(:ref) { current_ref } +- project = local_assigns.fetch(:project) { @project } #tree-holder.tree-holder.clearfix .nav-block = render 'projects/tree/tree_header', tree: @tree - = render 'projects/tree/tree_content', tree: @tree + - if commit + .info-well.hidden-xs.project-last-commit.append-bottom-default + .well-segment + %ul.blob-commit-info + = render 'projects/commits/commit', commit: commit, ref: ref, project: project + = render 'projects/tree/tree_content', tree: @tree diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml index 0fd19780570..9a9fca78df3 100644 --- a/app/views/projects/_home_panel.html.haml +++ b/app/views/projects/_home_panel.html.haml @@ -24,7 +24,7 @@ = render 'projects/buttons/fork' %span.hidden-xs - - if @project.feature_available?(:repository, current_user) + - if can?(current_user, :download_code, @project) .project-clone-holder = render "shared/clone_panel" diff --git a/app/views/projects/_last_commit.html.haml b/app/views/projects/_last_commit.html.haml deleted file mode 100644 index d104cc7c1a3..00000000000 --- a/app/views/projects/_last_commit.html.haml +++ /dev/null @@ -1,12 +0,0 @@ -- ref = local_assigns.fetch(:ref) -- status = commit.status(ref) -- if status - = link_to pipelines_namespace_project_commit_path(commit.project.namespace, commit.project, commit), class: "ci-status ci-#{status}" do - = ci_icon_for_status(status) - = ci_text_for_status(status) - -= link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit-sha" -= link_to_gfm commit.title, namespace_project_commit_path(project.namespace, project, commit), class: "commit-row-message" -· -#{time_ago_with_tooltip(commit.committed_date)} by -= commit_author_link(commit, avatar: true, size: 24) diff --git a/app/views/projects/_readme.html.haml b/app/views/projects/_readme.html.haml deleted file mode 100644 index cf09d9db6b7..00000000000 --- a/app/views/projects/_readme.html.haml +++ /dev/null @@ -1,21 +0,0 @@ -- if readme = @repository.readme - %article.readme-holder - .pull-right - - if can?(current_user, :push_code, @project) - = link_to icon('pencil'), namespace_project_edit_blob_path(@project.namespace, @project, tree_join(@repository.root_ref, readme.path)), class: 'light edit-project-readme' - - = render 'projects/blob/viewer', viewer: readme.rich_viewer, viewer_url: namespace_project_blob_path(@project.namespace, @project, tree_join(@repository.root_ref, readme.path), viewer: :rich, format: :json) -- else - .row-content-block.second-block.center - %h3.page-title - This project does not have a README yet - - if can?(current_user, :push_code, @project) - %p - A - %code README - file contains information about other files in a repository and is commonly - distributed with computer software, forming part of its documentation. - %p - We recommend you to - = link_to "add a README", add_special_file_path(@project, file_name: 'README.md'), class: 'underlined-link' - file to the repository and GitLab will render it here instead of this message. diff --git a/app/views/projects/blob/_auxiliary_viewer.html.haml b/app/views/projects/blob/_auxiliary_viewer.html.haml new file mode 100644 index 00000000000..9749afdc580 --- /dev/null +++ b/app/views/projects/blob/_auxiliary_viewer.html.haml @@ -0,0 +1,5 @@ +- blob = local_assigns.fetch(:blob) +- auxiliary_viewer = blob.auxiliary_viewer +- if auxiliary_viewer && auxiliary_viewer.render_error.nil? && auxiliary_viewer.visible_to?(current_user) + .well-segment.blob-auxiliary-viewer + = render 'projects/blob/viewer', viewer: auxiliary_viewer diff --git a/app/views/projects/blob/_blob.html.haml b/app/views/projects/blob/_blob.html.haml index 8af945ddb2c..8bd336269ff 100644 --- a/app/views/projects/blob/_blob.html.haml +++ b/app/views/projects/blob/_blob.html.haml @@ -3,13 +3,9 @@ .info-well.hidden-xs .well-segment %ul.blob-commit-info - - blob_commit = @repository.last_commit_for_path(@commit.id, blob.path) - = render blob_commit, project: @project, ref: @ref + = render 'projects/commits/commit', commit: @last_commit, project: @project, ref: @ref - - auxiliary_viewer = blob.auxiliary_viewer - - if auxiliary_viewer && !auxiliary_viewer.render_error - .well-segment.blob-auxiliary-viewer - = render 'projects/blob/viewer', viewer: auxiliary_viewer + = render "projects/blob/auxiliary_viewer", blob: blob #blob-content-holder.blob-content-holder %article.file-holder diff --git a/app/views/projects/blob/_viewer.html.haml b/app/views/projects/blob/_viewer.html.haml index 3d9c3a59980..4252f27d007 100644 --- a/app/views/projects/blob/_viewer.html.haml +++ b/app/views/projects/blob/_viewer.html.haml @@ -1,10 +1,10 @@ - hidden = local_assigns.fetch(:hidden, false) - render_error = viewer.render_error -- load_asynchronously = local_assigns.fetch(:load_asynchronously, viewer.server_side?) && render_error.nil? +- load_async = local_assigns.fetch(:load_async, viewer.load_async?) -- viewer_url = local_assigns.fetch(:viewer_url) { url_for(params.merge(viewer: viewer.type, format: :json)) } if load_asynchronously +- viewer_url = local_assigns.fetch(:viewer_url) { url_for(params.merge(viewer: viewer.type, format: :json)) } if load_async .blob-viewer{ data: { type: viewer.type, url: viewer_url }, class: ('hidden' if hidden) } - - if load_asynchronously + - if load_async = render viewer.loading_partial_path, viewer: viewer - elsif render_error = render 'projects/blob/render_error', viewer: viewer diff --git a/app/views/projects/blob/viewers/_changelog.html.haml b/app/views/projects/blob/viewers/_changelog.html.haml new file mode 100644 index 00000000000..53921e63b5f --- /dev/null +++ b/app/views/projects/blob/viewers/_changelog.html.haml @@ -0,0 +1,4 @@ += icon('history fw') += succeed '.' do + To find the state of this project's repository at the time of any of these versions, check out + = link_to "the tags", namespace_project_tags_path(viewer.project.namespace, viewer.project) diff --git a/app/views/projects/blob/viewers/_contributing.html.haml b/app/views/projects/blob/viewers/_contributing.html.haml new file mode 100644 index 00000000000..c78f04c9c7c --- /dev/null +++ b/app/views/projects/blob/viewers/_contributing.html.haml @@ -0,0 +1,9 @@ += icon('book fw') +After you've reviewed these contribution guidelines, you'll be all set to + +- options = contribution_options(viewer.project) +- if options.any? + = succeed '.' do + = options.to_sentence(two_words_connector: ' or ', last_word_connector: ', or ').html_safe +- else + contribute to this project. diff --git a/app/views/projects/blob/viewers/_dependency_manager.html.haml b/app/views/projects/blob/viewers/_dependency_manager.html.haml new file mode 100644 index 00000000000..a0f0215a5ff --- /dev/null +++ b/app/views/projects/blob/viewers/_dependency_manager.html.haml @@ -0,0 +1,11 @@ += icon('cubes fw') += succeed '.' do + This project manages its dependencies using + %strong= viewer.manager_name + + - if viewer.package_name + and defines a #{viewer.package_type} named + %strong< + = link_to viewer.package_name, viewer.package_url, target: '_blank', rel: 'noopener noreferrer' + += link_to 'Learn more', viewer.manager_url, target: '_blank', rel: 'noopener noreferrer' diff --git a/app/views/projects/blob/viewers/_license.html.haml b/app/views/projects/blob/viewers/_license.html.haml index 9a79d164692..fb9d0b99d09 100644 --- a/app/views/projects/blob/viewers/_license.html.haml +++ b/app/views/projects/blob/viewers/_license.html.haml @@ -5,4 +5,4 @@ This project is licensed under the = succeed '.' do %strong= license.name -= link_to 'Learn more about this license', license.url, target: '_blank', rel: 'noopener noreferrer' += link_to 'Learn more', license.url, target: '_blank', rel: 'noopener noreferrer' diff --git a/app/views/projects/blob/viewers/_loading_auxiliary.html.haml b/app/views/projects/blob/viewers/_loading_auxiliary.html.haml index 058c74bce0d..c7dc9e3250a 100644 --- a/app/views/projects/blob/viewers/_loading_auxiliary.html.haml +++ b/app/views/projects/blob/viewers/_loading_auxiliary.html.haml @@ -1,2 +1,2 @@ = icon('spinner spin fw') -Loading… +Analyzing file… diff --git a/app/views/projects/blob/viewers/_readme.html.haml b/app/views/projects/blob/viewers/_readme.html.haml new file mode 100644 index 00000000000..334b33faf48 --- /dev/null +++ b/app/views/projects/blob/viewers/_readme.html.haml @@ -0,0 +1,4 @@ += icon('info-circle fw') += succeed '.' do + To learn more about this project, read + = link_to "the wiki", namespace_project_wikis_path(viewer.project.namespace, viewer.project) diff --git a/app/views/projects/boards/components/sidebar/_assignee.html.haml b/app/views/projects/boards/components/sidebar/_assignee.html.haml index 642da679f97..48f8c656080 100644 --- a/app/views/projects/boards/components/sidebar/_assignee.html.haml +++ b/app/views/projects/boards/components/sidebar/_assignee.html.haml @@ -10,7 +10,7 @@ - if can?(current_user, :admin_issue, @project) .selectbox.hide-collapsed - %input{ type: "hidden", + %input.js-vue{ type: "hidden", name: "issue[assignee_ids][]", ":value" => "assignee.id", "v-if" => "issue.assignees", @@ -18,7 +18,6 @@ .dropdown %button.dropdown-menu-toggle.js-user-search.js-author-search.js-multiselect.js-save-user-data.js-issue-board-sidebar{ type: "button", ref: "assigneeDropdown", data: { toggle: "dropdown", field_name: "issue[assignee_ids][]", first_user: (current_user.username if current_user), current_user: "true", project_id: @project.id, null_user: "true", multi_select: "true", 'max-select' => 1, dropdown: { header: 'Assignee' } }, ":data-issuable-id" => "issue.id", - ":data-selected" => "assigneeId", ":data-issue-update" => "'#{namespace_project_issues_path(@project.namespace, @project)}/' + issue.id + '.json'" } Select assignee = icon("chevron-down") diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml index 304c512e1b5..869633e016d 100644 --- a/app/views/projects/branches/_branch.html.haml +++ b/app/views/projects/branches/_branch.html.haml @@ -61,7 +61,7 @@ = icon("trash-o") - if branch.name != @repository.root_ref - .divergence-graph{ title: "#{number_commits_ahead} commits ahead, #{number_commits_behind} commits behind #{@repository.root_ref}" } + .divergence-graph{ title: "#{number_commits_behind} commits behind #{@repository.root_ref}, #{number_commits_ahead} commits ahead" } .graph-side .bar.bar-behind{ style: "width: #{number_commits_behind * bar_graph_width_factor}%" } %span.count.count-behind= number_commits_behind diff --git a/app/views/projects/builds/index.html.haml b/app/views/projects/builds/index.html.haml index 65162aacda1..a8c8afe2695 100644 --- a/app/views/projects/builds/index.html.haml +++ b/app/views/projects/builds/index.html.haml @@ -14,7 +14,7 @@ data: { confirm: 'Are you sure?' }, class: 'btn btn-danger', method: :post - unless @repository.gitlab_ci_yml - = link_to 'Get started with CI/CD Pipelines', help_page_path('ci/quick_start/README'), class: 'btn btn-info' + = link_to 'Get started with Pipelines', help_page_path('ci/quick_start/README'), class: 'btn btn-info' = link_to ci_lint_path, class: 'btn btn-default' do %span CI lint diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml index e796920ac82..a190a8760ef 100644 --- a/app/views/projects/ci/builds/_build.html.haml +++ b/app/views/projects/ci/builds/_build.html.haml @@ -36,7 +36,7 @@ = icon('warning', class: 'text-warning has-tooltip', title: 'Job is stuck. Check runners.') - if retried - = icon('spinner', class: 'text-warning has-tooltip', title: 'Job was retried') + = icon('refresh', class: 'text-warning has-tooltip', title: 'Job was retried') .label-container - if job.tags.any? diff --git a/app/views/projects/commit/show.html.haml b/app/views/projects/commit/show.html.haml index 6051ea2f1ce..3a1be3fa4b6 100644 --- a/app/views/projects/commit/show.html.haml +++ b/app/views/projects/commit/show.html.haml @@ -13,7 +13,7 @@ .block-connector = render "projects/diffs/diffs", diffs: @diffs, environment: @environment - = render "shared/notes/notes_with_form" + = render "shared/notes/notes_with_form", :autocomplete => true - if can_collaborate_with_project? - %w(revert cherry-pick).each do |type| = render "projects/commit/change", type: type, commit: @commit, title: @commit.title diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index d9643dc7957..f5549d7f4cd 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -65,7 +65,7 @@ .row .col-md-9.project-feature.nested = feature_fields.label :builds_access_level, "Pipelines", class: 'label-light' - %span.help-block Submit, test and deploy your changes before merge + %span.help-block Build, test, and deploy your changes .col-md-3 = project_feature_access_select(:builds_access_level) @@ -246,14 +246,16 @@ .row.prepend-top-default .col-lg-3 %h4.prepend-top-0.danger-title - Transfer project + Transfer project to new group + %p.append-bottom-0 + Please select the group you want to transfer this project to in the dropdown to the right. .col-lg-9 - = form_for([@project.namespace.becomes(Namespace), @project], url: transfer_namespace_project_path(@project.namespace, @project), method: :put, remote: true) do |f| + = form_for([@project.namespace.becomes(Namespace), @project], url: transfer_namespace_project_path(@project.namespace, @project), method: :put, remote: true, html: { class: 'js-project-transfer-form' } ) do |f| .form-group = label_tag :new_namespace_id, nil, class: 'label-light' do - %span Namespace + %span Select a new namespace .form-group - = select_tag :new_namespace_id, namespaces_options(@project.namespace_id), { prompt: 'Choose a project namespace', class: 'select2' } + = select_tag :new_namespace_id, namespaces_options(nil), include_blank: true, class: 'select2' %ul %li Be careful. Changing the project's namespace can have unintended side effects. %li You can only transfer the project to namespaces you manage. diff --git a/app/views/projects/issues/_discussion.html.haml b/app/views/projects/issues/_discussion.html.haml index 4dfda54feb5..8b095f4ca10 100644 --- a/app/views/projects/issues/_discussion.html.haml +++ b/app/views/projects/issues/_discussion.html.haml @@ -1,7 +1,7 @@ - content_for :note_actions do - if can?(current_user, :update_issue, @issue) - = link_to 'Reopen issue', issue_path(@issue, issue: {state_event: :reopen}, format: 'json'), data: {no_turbolink: true, original_text: "Reopen issue", alternative_text: "Comment & reopen issue"}, class: "btn btn-nr btn-reopen btn-comment js-note-target-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue' - = link_to 'Close issue', issue_path(@issue, issue: {state_event: :close}, format: 'json'), data: {no_turbolink: true, original_text: "Close issue", alternative_text: "Comment & close issue"}, class: "btn btn-nr btn-close btn-comment js-note-target-close #{issue_button_visibility(@issue, true)}", title: 'Close issue' + = link_to 'Reopen issue', issue_path(@issue, issue: {state_event: :reopen}, format: 'json'), data: {original_text: "Reopen issue", alternative_text: "Comment & reopen issue"}, class: "btn btn-nr btn-reopen btn-comment js-note-target-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue' + = link_to 'Close issue', issue_path(@issue, issue: {state_event: :close}, format: 'json'), data: {original_text: "Close issue", alternative_text: "Comment & close issue"}, class: "btn btn-nr btn-close btn-comment js-note-target-close #{issue_button_visibility(@issue, true)}", title: 'Close issue' #notes - = render 'shared/notes/notes_with_form' + = render 'shared/notes/notes_with_form', :autocomplete => true diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml index dfed7e1f7dc..7bf271c2fc5 100644 --- a/app/views/projects/issues/show.html.haml +++ b/app/views/projects/issues/show.html.haml @@ -2,6 +2,8 @@ - page_title "#{@issue.title} (#{@issue.to_reference})", "Issues" - page_description @issue.description - page_card_attributes @issue.card_attributes +- can_update_issue = can?(current_user, :update_issue, @issue) +- can_report_spam = @issue.submittable_as_spam_by?(current_user) .clearfix.detail-page-header .issuable-header @@ -27,27 +29,29 @@ = icon('caret-down') .dropdown-menu.dropdown-menu-align-right.hidden-lg %ul - %li - = link_to 'New issue', new_namespace_project_issue_path(@project.namespace, @project), title: 'New issue', id: 'new_issue_link' - - if can?(current_user, :update_issue, @issue) + - if can_update_issue %li - = link_to 'Reopen issue', issue_path(@issue, issue: { state_event: :reopen }, format: 'json'), data: {no_turbolink: true}, class: "btn-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue' + = link_to 'Edit', edit_namespace_project_issue_path(@project.namespace, @project, @issue), class: 'issuable-edit' %li - = link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, format: 'json'), data: {no_turbolink: true}, class: "btn-close #{issue_button_visibility(@issue, true)}", title: 'Close issue' + = link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, format: 'json'), class: "btn-close #{issue_button_visibility(@issue, true)}", title: 'Close issue' %li - = link_to 'Edit', edit_namespace_project_issue_path(@project.namespace, @project, @issue), class: 'issuable-edit' - - if @issue.submittable_as_spam_by?(current_user) + = link_to 'Reopen issue', issue_path(@issue, issue: { state_event: :reopen }, format: 'json'), class: "btn-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue' + - if can_report_spam %li = link_to 'Submit as spam', mark_as_spam_namespace_project_issue_path(@project.namespace, @project, @issue), method: :post, class: 'btn-spam', title: 'Submit as spam' + - if can_update_issue || can_report_spam + %li.divider + %li + = link_to 'New issue', new_namespace_project_issue_path(@project.namespace, @project), title: 'New issue', id: 'new_issue_link' + - if can_update_issue + = link_to 'Edit', edit_namespace_project_issue_path(@project.namespace, @project, @issue), class: 'hidden-xs hidden-sm btn btn-grouped issuable-edit' + = link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, format: 'json'), class: "hidden-xs hidden-sm btn btn-grouped btn-close #{issue_button_visibility(@issue, true)}", title: 'Close issue' + = link_to 'Reopen issue', issue_path(@issue, issue: { state_event: :reopen }, format: 'json'), class: "hidden-xs hidden-sm btn btn-grouped btn-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue' + - if can_report_spam + = link_to 'Submit as spam', mark_as_spam_namespace_project_issue_path(@project.namespace, @project, @issue), method: :post, class: 'hidden-xs hidden-sm btn btn-grouped btn-spam', title: 'Submit as spam' = link_to new_namespace_project_issue_path(@project.namespace, @project), class: 'hidden-xs hidden-sm btn btn-grouped new-issue-link btn-new btn-inverted', title: 'New issue', id: 'new_issue_link' do New issue - - if can?(current_user, :update_issue, @issue) - = link_to 'Reopen issue', issue_path(@issue, issue: { state_event: :reopen }, format: 'json'), data: {no_turbolink: true}, class: "hidden-xs hidden-sm btn btn-grouped btn-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue' - = link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, format: 'json'), data: {no_turbolink: true}, class: "hidden-xs hidden-sm btn btn-grouped btn-close #{issue_button_visibility(@issue, true)}", title: 'Close issue' - - if @issue.submittable_as_spam_by?(current_user) - = link_to 'Submit as spam', mark_as_spam_namespace_project_issue_path(@project.namespace, @project, @issue), method: :post, class: 'hidden-xs hidden-sm btn btn-grouped btn-spam', title: 'Submit as spam' - = link_to 'Edit', edit_namespace_project_issue_path(@project.namespace, @project, @issue), class: 'hidden-xs hidden-sm btn btn-grouped issuable-edit' .issue-details.issuable-details .detail-page-description.content-block @@ -67,11 +71,11 @@ #related-branches{ data: { url: related_branches_namespace_project_issue_url(@project.namespace, @project, @issue) } } // This element is filled in using JavaScript. - .content-block.content-block-small + .content-block.emoji-block .row - .col-sm-6 + .col-sm-8 = render 'award_emoji/awards_block', awardable: @issue, inline: true - .col-sm-6.new-branch-col + .col-sm-4.new-branch-col = render 'new_branch' unless @issue.confidential? %section.issuable-discussion diff --git a/app/views/projects/merge_requests/_discussion.html.haml b/app/views/projects/merge_requests/_discussion.html.haml index 2e6420db212..b787edb3427 100644 --- a/app/views/projects/merge_requests/_discussion.html.haml +++ b/app/views/projects/merge_requests/_discussion.html.haml @@ -8,4 +8,4 @@ %button.btn.btn-nr.btn-default.append-right-10.js-comment-resolve-button{ "v-if" => "showButton", type: "submit", data: { project_path: "#{project_path(@merge_request.project)}" } } {{ buttonText }} -#notes= render "shared/notes/notes_with_form" +#notes= render "shared/notes/notes_with_form", :autocomplete => true diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml index b7515e1d91f..75120409bb3 100644 --- a/app/views/projects/merge_requests/_show.html.haml +++ b/app/views/projects/merge_requests/_show.html.haml @@ -21,7 +21,8 @@ #js-vue-mr-widget.mr-widget - content_for :page_specific_javascripts do - = page_specific_javascript_bundle_tag('vue_merge_request_widget') + = webpack_bundle_tag 'common_vue' + = webpack_bundle_tag 'vue_merge_request_widget' .content-block.content-block-small.emoji-list-container = render 'award_emoji/awards_block', awardable: @merge_request, inline: true diff --git a/app/views/projects/merge_requests/show/_how_to_merge.html.haml b/app/views/projects/merge_requests/show/_how_to_merge.html.haml index f3372c7657f..766cb272bec 100644 --- a/app/views/projects/merge_requests/show/_how_to_merge.html.haml +++ b/app/views/projects/merge_requests/show/_how_to_merge.html.haml @@ -49,7 +49,7 @@ %strong Tip: = succeed '.' do You can also checkout merge requests locally by - = link_to 'following these guidelines', help_page_path('user/project/merge_requests.md', anchor: "checkout-merge-requests-locally"), target: '_blank', rel: 'noopener noreferrer' + = link_to 'following these guidelines', help_page_path('user/project/merge_requests/index.md', anchor: "checkout-merge-requests-locally"), target: '_blank', rel: 'noopener noreferrer' :javascript $(function(){ diff --git a/app/views/projects/merge_requests/show/_mr_title.html.haml b/app/views/projects/merge_requests/show/_mr_title.html.haml index e7c5bca6a37..d9428b8562e 100644 --- a/app/views/projects/merge_requests/show/_mr_title.html.haml +++ b/app/views/projects/merge_requests/show/_mr_title.html.haml @@ -13,7 +13,7 @@ = icon('angle-double-left') .issuable-meta - = issuable_meta(@merge_request, @project, "Merge Request") + = issuable_meta(@merge_request, @project, "Merge request") - if can?(current_user, :update_merge_request, @merge_request) .issuable-actions diff --git a/app/views/projects/merge_requests/show/_versions.html.haml b/app/views/projects/merge_requests/show/_versions.html.haml index 37117bc64a3..0999b95c9c9 100644 --- a/app/views/projects/merge_requests/show/_versions.html.haml +++ b/app/views/projects/merge_requests/show/_versions.html.haml @@ -91,7 +91,7 @@ comparing two versions - else viewing an old version - of this merge request. + of the diff. .pull-right = link_to 'Show latest version', diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: 'btn btn-sm' diff --git a/app/views/projects/notes/_actions.html.haml b/app/views/projects/notes/_actions.html.haml index d70ec8a6062..3e79dbec70c 100644 --- a/app/views/projects/notes/_actions.html.haml +++ b/app/views/projects/notes/_actions.html.haml @@ -31,7 +31,7 @@ - if current_user - if note.emoji_awardable? - user_authored = note.user_authored?(current_user) - = link_to '#', title: 'Award Emoji', class: "note-action-button note-emoji-button js-add-award js-note-emoji #{'js-user-authored' if user_authored} has-tooltip", data: { position: 'right' } do + = link_to '#', title: 'Add reaction', class: "note-action-button note-emoji-button js-add-award js-note-emoji #{'js-user-authored' if user_authored} has-tooltip", data: { position: 'right' } do = icon('spinner spin') %span{ class: 'link-highlight award-control-icon-neutral' }= custom_icon('emoji_slightly_smiling_face') %span{ class: 'link-highlight award-control-icon-positive' }= custom_icon('emoji_smiley') diff --git a/app/views/projects/pipeline_schedules/_form.html.haml b/app/views/projects/pipeline_schedules/_form.html.haml index d6f4f1a206c..bbed10039af 100644 --- a/app/views/projects/pipeline_schedules/_form.html.haml +++ b/app/views/projects/pipeline_schedules/_form.html.haml @@ -5,29 +5,29 @@ = form_for [@project.namespace.becomes(Namespace), @project, @schedule], as: :schedule, html: { id: "new-pipeline-schedule-form", class: "form-horizontal js-pipeline-schedule-form" } do |f| = form_errors(@schedule) .form-group - .col-md-6 + .col-md-9 = f.label :description, 'Description', class: 'label-light' = f.text_field :description, class: 'form-control', required: true, autofocus: true, placeholder: 'Provide a short description for this pipeline' .form-group - .col-md-12 + .col-md-9 = f.label :cron, 'Interval Pattern', class: 'label-light' #interval-pattern-input{ data: { initial_interval: @schedule.cron } } .form-group - .col-md-6 + .col-md-9 = f.label :cron_timezone, 'Cron Timezone', class: 'label-light' = dropdown_tag("Select a timezone", options: { toggle_class: 'btn js-timezone-dropdown', title: "Select a timezone", filter: true, placeholder: "Filter", data: { data: timezone_data } } ) = f.text_field :cron_timezone, value: @schedule.cron_timezone, id: 'schedule_cron_timezone', class: 'hidden', name: 'schedule[cron_timezone]', required: true .form-group - .col-md-6 + .col-md-9 = f.label :ref, 'Target Branch', class: 'label-light' - = dropdown_tag("Select target branch", options: { toggle_class: 'btn js-target-branch-dropdown git-revision-dropdown-toggle', dropdown_class: 'git-revision-dropdown', title: "Select target branch", filter: true, placeholder: "Filter", data: { data: @project.repository.branch_names } } ) + = dropdown_tag("Select target branch", options: { toggle_class: 'btn js-target-branch-dropdown git-revision-dropdown-toggle', dropdown_class: 'git-revision-dropdown', title: "Select target branch", filter: true, placeholder: "Filter", data: { data: @project.repository.branch_names, default_branch: @project.default_branch } } ) = f.text_field :ref, value: @schedule.ref, id: 'schedule_ref', class: 'hidden', name: 'schedule[ref]', required: true .form-group - .col-md-6 + .col-md-9 = f.label :active, 'Activated', class: 'label-light' %div = f.check_box :active, required: false, value: @schedule.active? - active + Active .footer-block.row-content-block = f.submit 'Save pipeline schedule', class: 'btn btn-create', tabindex: 3 = link_to 'Cancel', pipeline_schedules_path(@project), class: 'btn btn-cancel' diff --git a/app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml b/app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml index 2cd82e1b661..082a6bcbb2a 100644 --- a/app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml +++ b/app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml @@ -15,7 +15,7 @@ None %td.next-run-cell - if pipeline_schedule.active? - = time_ago_with_tooltip(pipeline_schedule.next_run_at) + = time_ago_with_tooltip(pipeline_schedule.real_next_run) - else Inactive %td diff --git a/app/views/projects/pipeline_schedules/index.html.haml b/app/views/projects/pipeline_schedules/index.html.haml index 25c52175e3d..6751efaaf2f 100644 --- a/app/views/projects/pipeline_schedules/index.html.haml +++ b/app/views/projects/pipeline_schedules/index.html.haml @@ -7,14 +7,14 @@ = render "projects/pipelines/head" %div{ class: container_class } - #pipeline-schedules-callout{ data: { docs_url: help_page_path('ci/pipeline_schedules') } } + #pipeline-schedules-callout{ data: { docs_url: help_page_path('user/project/pipelines/schedules') } } .top-area - schedule_path_proc = ->(scope) { pipeline_schedules_path(@project, scope: scope) } = render "tabs", schedule_path_proc: schedule_path_proc, all_schedules: @all_schedules, scope: @scope .nav-controls = link_to new_namespace_project_pipeline_schedule_path(@project.namespace, @project), class: 'btn btn-create' do - %span New Schedule + %span New schedule - if @schedules.present? %ul.content-list diff --git a/app/views/projects/pipelines_settings/_show.html.haml b/app/views/projects/pipelines_settings/_show.html.haml index a3f84476dea..1b1910b5c0f 100644 --- a/app/views/projects/pipelines_settings/_show.html.haml +++ b/app/views/projects/pipelines_settings/_show.html.haml @@ -1,14 +1,14 @@ .row.prepend-top-default .col-lg-3.profile-settings-sidebar %h4.prepend-top-0 - CI/CD Pipelines + Pipelines .col-lg-9 = form_for @project, url: namespace_project_pipelines_settings_path(@project.namespace.becomes(Namespace), @project) do |f| %fieldset.builds-feature - unless @repository.gitlab_ci_yml .form-group %p Pipelines need to be configured before you can begin using Continuous Integration. - = link_to 'Get started with CI/CD Pipelines', help_page_path('ci/quick_start/README'), class: 'btn btn-info' + = link_to 'Get started with Pipelines', help_page_path('ci/quick_start/README'), class: 'btn btn-info' %hr .form-group.append-bottom-default = f.label :runners_token, "Runner token", class: 'label-light' diff --git a/app/views/projects/runners/_specific_runners.html.haml b/app/views/projects/runners/_specific_runners.html.haml index 6b8e6bd4fee..f8835454140 100644 --- a/app/views/projects/runners/_specific_runners.html.haml +++ b/app/views/projects/runners/_specific_runners.html.haml @@ -9,7 +9,7 @@ (checkout the #{link_to 'GitLab Runner section', 'https://about.gitlab.com/gitlab-ci/#gitlab-runner', target: '_blank'} for information on how to install it). %li Specify the following URL during the Runner setup: - %code= ci_root_url(only_path: false) + %code= root_url(only_path: false) %li Use the following registration token during setup: %code= @project.runners_token diff --git a/app/views/projects/settings/_head.html.haml b/app/views/projects/settings/_head.html.haml index 8c7f9e0191e..faed65d6588 100644 --- a/app/views/projects/settings/_head.html.haml +++ b/app/views/projects/settings/_head.html.haml @@ -24,9 +24,9 @@ Repository - if @project.feature_available?(:builds, current_user) = nav_link(controller: :ci_cd) do - = link_to namespace_project_settings_ci_cd_path(@project.namespace, @project), title: 'CI/CD Pipelines' do + = link_to namespace_project_settings_ci_cd_path(@project.namespace, @project), title: 'Pipelines' do %span - CI/CD Pipelines + Pipelines - if Gitlab.config.pages.enabled = nav_link(controller: :pages) do = link_to namespace_project_pages_path(@project.namespace, @project), title: 'Pages' do diff --git a/app/views/projects/settings/ci_cd/show.html.haml b/app/views/projects/settings/ci_cd/show.html.haml index e2603096014..e8d2e91bd76 100644 --- a/app/views/projects/settings/ci_cd/show.html.haml +++ b/app/views/projects/settings/ci_cd/show.html.haml @@ -1,4 +1,4 @@ -- page_title "CI/CD Pipelines" +- page_title "Pipelines" = render "projects/settings/head" = render 'projects/runners/index' diff --git a/app/views/projects/settings/integrations/_project_hook.html.haml b/app/views/projects/settings/integrations/_project_hook.html.haml index 8dc276a3bec..a6640592dba 100644 --- a/app/views/projects/settings/integrations/_project_hook.html.haml +++ b/app/views/projects/settings/integrations/_project_hook.html.haml @@ -3,7 +3,7 @@ .col-md-8.col-lg-7 %strong.light-header= hook.url %div - - %w(push_events tag_push_events issues_events confidential_issues_events note_events merge_requests_events build_events pipeline_events wiki_page_events).each do |trigger| + - %w(push_events tag_push_events issues_events confidential_issues_events note_events merge_requests_events job_events pipeline_events wiki_page_events).each do |trigger| - if hook.send(trigger) %span.label.label-gray.deploy-project-label= trigger.titleize .col-md-4.col-lg-5.text-right-lg.prepend-top-5 diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index d6c4195e2d0..1ca464696ed 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -73,11 +73,6 @@ = link_to add_special_file_path(@project, file_name: '.gitlab-ci.yml', commit_message: 'Set up auto deploy', branch_name: 'auto-deploy', context: 'autodeploy') do Set up auto deploy - - if @repository.commit - %div{ class: container_class } - .project-last-commit - = render 'projects/last_commit', commit: @repository.commit, ref: current_ref, project: @project - %div{ class: container_class } - if @project.archived? .text-warning.center.prepend-top-20 diff --git a/app/views/projects/snippets/show.html.haml b/app/views/projects/snippets/show.html.haml index aab1c043e66..847f3c2f348 100644 --- a/app/views/projects/snippets/show.html.haml +++ b/app/views/projects/snippets/show.html.haml @@ -9,4 +9,4 @@ .row-content-block.top-block.content-component-block = render 'award_emoji/awards_block', awardable: @snippet, inline: true - #notes= render "shared/notes/notes_with_form" + #notes= render "shared/notes/notes_with_form", :autocomplete => true diff --git a/app/views/projects/tags/new.html.haml b/app/views/projects/tags/new.html.haml index cbf841762b7..52af295bddd 100644 --- a/app/views/projects/tags/new.html.haml +++ b/app/views/projects/tags/new.html.haml @@ -1,4 +1,5 @@ - page_title "New Tag" +- default_ref = params[:ref] || @project.default_branch - if @error .alert.alert-danger @@ -16,9 +17,13 @@ = text_field_tag :tag_name, params[:tag_name], required: true, tabindex: 1, autofocus: true, class: 'form-control' .form-group = label_tag :ref, 'Create from', class: 'control-label' - .col-sm-10 - = text_field_tag :ref, params[:ref] || @project.default_branch, required: true, tabindex: 2, class: 'form-control' - .help-block Branch name or commit SHA + .col-sm-10.create-from + .dropdown + = hidden_field_tag :ref, default_ref + = button_tag type: 'button', title: default_ref, class: 'dropdown-menu-toggle wide form-control js-branch-select', required: true, data: { toggle: 'dropdown', selected: default_ref, field_name: 'ref' } do + .text-left.dropdown-toggle-text= default_ref + = render 'shared/ref_dropdown', dropdown_class: 'wide' + .help-block Existing branch name, tag, or commit SHA .form-group = label_tag :message, nil, class: 'control-label' .col-sm-10 @@ -37,9 +42,5 @@ = link_to 'Cancel', namespace_project_tags_path(@project.namespace, @project), class: 'btn btn-cancel' :javascript - var availableRefs = #{@project.repository.ref_names.to_json}; - - $("#ref").autocomplete({ - source: availableRefs, - minLength: 1 - }); + window.gl = window.gl || { }; + window.gl.availableRefs = #{@project.repository.ref_names.to_json}; diff --git a/app/views/projects/tree/_readme.html.haml b/app/views/projects/tree/_readme.html.haml index 2c2f64283f5..de57cd4ba00 100644 --- a/app/views/projects/tree/_readme.html.haml +++ b/app/views/projects/tree/_readme.html.haml @@ -1,8 +1,9 @@ -%article.file-holder.readme-holder - .js-file-title.file-title - = blob_icon readme.mode, readme.name - = link_to namespace_project_blob_path(@project.namespace, @project, tree_join(@ref, readme.path)) do - %strong - = readme.name +- if readme.rich_viewer + %article.file-holder.readme-holder + .js-file-title.file-title + = blob_icon readme.mode, readme.name + = link_to namespace_project_blob_path(@project.namespace, @project, tree_join(@ref, readme.path)) do + %strong + = readme.name - = render 'projects/blob/viewer', viewer: readme.rich_viewer, viewer_url: namespace_project_blob_path(@project.namespace, @project, tree_join(@ref, readme.path), viewer: :rich, format: :json) + = render 'projects/blob/viewer', viewer: readme.rich_viewer, viewer_url: namespace_project_blob_path(@project.namespace, @project, tree_join(@ref, readme.path), viewer: :rich, format: :json) diff --git a/app/views/projects/tree/show.html.haml b/app/views/projects/tree/show.html.haml index 42700c237fc..b51955010ce 100644 --- a/app/views/projects/tree/show.html.haml +++ b/app/views/projects/tree/show.html.haml @@ -7,13 +7,4 @@ = render 'projects/last_push' %div{ class: container_class } - #tree-holder.tree-holder.clearfix - .nav-block - = render 'projects/tree/tree_header', tree: @tree - - .info-well.hidden-xs.append-bottom-default - .well-segment - %ul.blob-commit-info - = render 'projects/commits/commit', commit: @commit, project: @project, ref: @ref - - = render 'projects/tree/tree_content', tree: @tree + = render 'projects/files', commit: @last_commit, project: @project, ref: @ref diff --git a/app/views/shared/_field.html.haml b/app/views/shared/_field.html.haml index 8d6e16f74c3..d74b0043949 100644 --- a/app/views/shared/_field.html.haml +++ b/app/views/shared/_field.html.haml @@ -9,7 +9,7 @@ .form-group - if type == "password" && value.present? - = form.label name, "Change #{title}", class: "control-label" + = form.label name, "Enter new #{title.downcase}", class: "control-label" - else = form.label name, title, class: "control-label" .col-sm-10 @@ -22,6 +22,6 @@ - elsif type == 'select' = form.select name, options_for_select(choices, value ? value : default_choice), {}, { class: "form-control" } - elsif type == 'password' - = form.password_field name, autocomplete: "new-password", class: 'form-control' + = form.password_field name, autocomplete: "new-password", class: "form-control" - if help %span.help-block= help diff --git a/app/views/shared/icons/_icon_history.svg b/app/views/shared/icons/_icon_history.svg new file mode 100644 index 00000000000..41096da19c5 --- /dev/null +++ b/app/views/shared/icons/_icon_history.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="1792" height="1792" viewBox="0 0 1792 1792"><path d="M1664 896q0 156-61 298t-164 245-245 164-298 61q-172 0-327-72.5T305 1387q-7-10-6.5-22.5t8.5-20.5l137-138q10-9 25-9 16 2 23 12 73 95 179 147t225 52q104 0 198.5-40.5T1258 1258t109.5-163.5T1408 896t-40.5-198.5T1258 534t-163.5-109.5T896 384q-98 0-188 35.5T548 521l137 138q31 30 14 69-17 40-59 40H192q-26 0-45-19t-19-45V256q0-42 40-59 39-17 69 14l130 129q107-101 244.5-156.5T896 128q156 0 298 61t245 164 164 245 61 298zm-640-288v448q0 14-9 23t-23 9H672q-14 0-23-9t-9-23v-64q0-14 9-23t23-9h224V608q0-14 9-23t23-9h64q14 0 23 9t9 23z"/></svg> diff --git a/app/views/shared/icons/_icon_status_skipped.svg b/app/views/shared/icons/_icon_status_skipped.svg index 1998dfef9ea..a9ba29c922c 100755 --- a/app/views/shared/icons/_icon_status_skipped.svg +++ b/app/views/shared/icons/_icon_status_skipped.svg @@ -1 +1 @@ -<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M7.69 7.7l-.905.905a.7.7 0 0 0 .99.99l1.85-1.85c.411-.412.411-1.078 0-1.49l-1.85-1.85a.7.7 0 0 0-.99.99l.905.905H4.48a.7.7 0 0 0 0 1.4h3.21z"/></g></svg> +<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><path d="M7 14A7 7 0 1 1 7 0a7 7 0 0 1 0 14z"/><path d="M7 13A6 6 0 1 0 7 1a6 6 0 0 0 0 12z" fill="#FFF" fill-rule="nonzero"/><path d="M6.415 7.04L4.579 5.203a.295.295 0 0 1 .004-.416l.349-.349a.29.29 0 0 1 .416-.004l2.214 2.214a.289.289 0 0 1 .019.021l.132.133c.11.11.108.291 0 .398L5.341 9.573a.282.282 0 0 1-.398 0l-.331-.331a.285.285 0 0 1 0-.399L6.415 7.04zm2.54 0L7.119 5.203a.295.295 0 0 1 .004-.416l.349-.349a.29.29 0 0 1 .416-.004l2.214 2.214a.289.289 0 0 1 .019.021l.132.133c.11.11.108.291 0 .398L7.881 9.573a.282.282 0 0 1-.398 0l-.331-.331a.285.285 0 0 1 0-.399L8.955 7.04z"/></svg> diff --git a/app/views/shared/icons/_icon_status_skipped_borderless.svg b/app/views/shared/icons/_icon_status_skipped_borderless.svg index fb3e930b3cb..3c8a26d7f4d 100644 --- a/app/views/shared/icons/_icon_status_skipped_borderless.svg +++ b/app/views/shared/icons/_icon_status_skipped_borderless.svg @@ -1 +1 @@ -<svg width="22px" height="22px" viewBox="0 0 22 22" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M12.0846,12.1 L10.6623,13.5223 C10.2454306,13.9539168 10.2513924,14.6399933 10.6756996,15.0643004 C11.1000067,15.4886076 11.7860832,15.4945694 12.2177,15.0777 L15.1261,12.1693 C15.7708612,11.5230891 15.7708612,10.4769109 15.1261,9.8307 L12.2177,6.9223 C11.7860832,6.50543057 11.1000067,6.51139239 10.6756996,6.93569957 C10.2513924,7.36000675 10.2454306,8.04608322 10.6623,8.4777 L12.0846,9.9 L7.04,9.9 C6.43248678,9.9 5.94,10.3924868 5.94,11 C5.94,11.6075132 6.43248678,12.1 7.04,12.1 L12.0846,12.1 L12.0846,12.1 Z" id="Shape"></path></svg> +<svg width="22" height="22" viewBox="0 0 22 22" xmlns="http://www.w3.org/2000/svg"><path d="M14.072 11.063l-2.82 2.82a.46.46 0 0 0-.001.652l.495.495a.457.457 0 0 0 .653-.001l3.7-3.7a.46.46 0 0 0 .001-.653l-.196-.196a.453.453 0 0 0-.03-.033l-3.479-3.479a.464.464 0 0 0-.654.007l-.548.548a.463.463 0 0 0-.007.654l2.886 2.886z"/><path d="M10.08 11.063l-2.819 2.82a.46.46 0 0 0-.002.652l.496.495a.457.457 0 0 0 .652-.001l3.7-3.7a.46.46 0 0 0 .002-.653l-.196-.196a.453.453 0 0 0-.03-.033l-3.48-3.479a.464.464 0 0 0-.653.007l-.548.548a.463.463 0 0 0-.007.654l2.886 2.886z"/></svg> diff --git a/app/views/shared/icons/_mr_widget_empty_state.svg b/app/views/shared/icons/_mr_widget_empty_state.svg new file mode 100644 index 00000000000..6a811893b2d --- /dev/null +++ b/app/views/shared/icons/_mr_widget_empty_state.svg @@ -0,0 +1 @@ +<svg width="256" height="146" viewBox="0 0 256 146" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><title>illustration</title><defs><rect id="a" width="178.714" height="115.389" rx="10"/><mask id="d" x="0" y="0" width="178.714" height="115.389" fill="#fff"><use xlink:href="#a"/></mask><path d="M8.796 31.515c.395.047.8.072 1.207.072h23.065c5.536 0 10.003-4.475 10.003-9.994v-11.6C43.07 4.476 38.594 0 33.07 0H10.003C4.467 0 0 4.475 0 9.994v11.6c0 1.248.23 2.444.65 3.547H0v7.414c0 4.094 2.394 5.113 5.342 2.28l3.454-3.32z" id="b"/><mask id="e" x="0" y="0" width="43.071" height="36.437" fill="#fff"><use xlink:href="#b"/></mask><path d="M8.796 31.515c.395.047.8.072 1.207.072h23.065c5.536 0 10.003-4.475 10.003-9.994v-11.6C43.07 4.476 38.594 0 33.07 0H10.003C4.467 0 0 4.475 0 9.994v11.6c0 1.248.23 2.444.65 3.547H0v7.414c0 4.094 2.394 5.113 5.342 2.28l3.454-3.32z" id="c"/><mask id="f" x="0" y="0" width="43.071" height="36.437" fill="#fff"><use xlink:href="#c"/></mask></defs><g fill="none" fill-rule="evenodd"><g transform="translate(0 3.868)" fill="#F9F9F9"><rect x="19.286" width="77.143" height="14.182" rx="7.091"/><rect y="28.364" width="84.857" height="14.182" rx="7.091"/><rect x="133.714" y="42.546" width="122.143" height="14.182" rx="7.091"/><rect x="82.929" y="126.992" width="101.571" height="14.182" rx="7.091"/><rect x="42.429" y="99.273" width="101.571" height="14.182" rx="7.091"/><rect x="19.929" y="70.909" width="225" height="14.182" rx="7.091"/><path d="M98.37 14.182H13.488h13.81a7.098 7.098 0 0 1 7.094 7.09 7.09 7.09 0 0 1-7.094 7.092h-13.81 84.88-23.452a7.098 7.098 0 0 1-7.095-7.09 7.09 7.09 0 0 1 7.096-7.092h23.452zm162 42.545h-75.238 23.452a7.098 7.098 0 0 1 7.095 7.09 7.09 7.09 0 0 1-7.096 7.092h-23.452 75.237-23.453a7.098 7.098 0 0 1-7.095-7.09 7.09 7.09 0 0 1 7.095-7.093h23.452zM103.512 85.09H28.275h23.452a7.098 7.098 0 0 1 7.095 7.092 7.09 7.09 0 0 1-7.095 7.09H28.275h75.237H80.06a7.098 7.098 0 0 1-7.095-7.09 7.09 7.09 0 0 1 7.095-7.09h23.452zm48.215 28.365H76.49 90.3a7.098 7.098 0 0 1 7.093 7.09 7.09 7.09 0 0 1-7.094 7.092H76.49h75.237-33.096a7.098 7.098 0 0 1-7.094-7.09 7.09 7.09 0 0 1 7.095-7.092h33.097z"/></g><g transform="translate(38.57 12.248)"><use stroke="#EEE" mask="url(#d)" stroke-width="8" fill="#FFF" xlink:href="#a"/><path fill="#EEE" d="M2.57 18.694h174.215v2.58H2.57z"/><g transform="translate(21.857 38.678)"><rect fill="#B5A7DD" y=".645" width="3.857" height="1.289" rx=".645"/><rect fill="#EEE" x="9.643" width="9.643" height="2.579" rx="1.289"/><rect fill="#EEE" x="46.286" width="9.643" height="2.579" rx="1.289"/><rect fill="#EEE" x="25.071" y="14.182" width="9.643" height="2.579" rx="1.289"/><rect fill="#FC6D26" x="34.071" y="7.091" width="9.643" height="2.579" rx="1.289"/><rect fill="#FC6D26" opacity=".5" x="30.857" width="12.857" height="2.579" rx="1.289"/><rect fill="#EEE" x="9.643" y="14.182" width="12.857" height="2.579" rx="1.289"/><rect fill="#EEE" x="18.643" y="7.091" width="12.857" height="2.579" rx="1.289"/><rect fill="#FC6D26" x="21.857" width="6.429" height="2.579" rx="1.289"/><rect fill="#EEE" x="9.643" y="7.091" width="6.429" height="2.579" rx="1.289"/><rect fill="#B5A7DD" y="7.736" width="3.857" height="1.289" rx=".645"/><rect fill="#B5A7DD" y="14.826" width="3.857" height="1.289" rx=".645"/></g><g transform="translate(21.857 59.95)"><rect fill="#B5A7DD" y=".645" width="3.857" height="1.289" rx=".645"/><rect fill="#FC6D26" x="9.643" width="9.643" height="2.579" rx="1.289"/><rect fill="#EEE" x="46.286" width="9.643" height="2.579" rx="1.289"/><rect fill="#FC6D26" opacity=".5" x="25.071" y="14.182" width="9.643" height="2.579" rx="1.289"/><rect fill="#EEE" x="34.071" y="7.091" width="9.643" height="2.579" rx="1.289"/><rect fill="#FC6D26" x="30.857" width="12.857" height="2.579" rx="1.289"/><rect fill="#FC6D26" x="9.643" y="14.182" width="12.857" height="2.579" rx="1.289"/><rect fill="#EEE" x="18.643" y="7.091" width="12.857" height="2.579" rx="1.289"/><rect fill="#FC6D26" opacity=".5" x="21.857" width="6.429" height="2.579" rx="1.289"/><rect fill="#EEE" x="9.643" y="7.091" width="6.429" height="2.579" rx="1.289"/><rect fill="#B5A7DD" y="7.736" width="3.857" height="1.289" rx=".645"/><rect fill="#B5A7DD" y="14.826" width="3.857" height="1.289" rx=".645"/></g><g transform="translate(21.857 81.223)"><rect fill="#B5A7DD" y=".645" width="3.857" height="1.289" rx=".645"/><rect fill="#EEE" x="9.643" width="9.643" height="2.579" rx="1.289"/><rect fill="#EEE" x="46.286" width="9.643" height="2.579" rx="1.289"/><rect fill="#EEE" x="25.071" y="14.182" width="9.643" height="2.579" rx="1.289"/><rect fill="#EEE" x="34.071" y="7.091" width="9.643" height="2.579" rx="1.289"/><rect fill="#FC6D26" x="30.857" width="12.857" height="2.579" rx="1.289"/><rect fill="#EEE" x="9.643" y="14.182" width="12.857" height="2.579" rx="1.289"/><rect fill="#EEE" x="18.643" y="7.091" width="12.857" height="2.579" rx="1.289"/><rect fill="#FC6D26" opacity=".5" x="21.857" width="6.429" height="2.579" rx="1.289"/><rect fill="#EEE" x="9.643" y="7.091" width="6.429" height="2.579" rx="1.289"/><rect fill="#B5A7DD" y="7.736" width="3.857" height="1.289" rx=".645"/><rect fill="#B5A7DD" y="14.826" width="3.857" height="1.289" rx=".645"/></g><g transform="translate(100.93 38.033)"><rect fill="#FDE5D8" y=".645" width="3.857" height="1.289" rx=".645"/><rect fill="#EEE" x="9.643" width="9.643" height="2.579" rx="1.289"/><rect fill="#EEE" x="46.286" width="9.643" height="2.579" rx="1.289"/><rect fill="#6B4FBB" opacity=".5" x="25.071" y="14.182" width="9.643" height="2.579" rx="1.289"/><rect fill="#6B4FBB" x="34.071" y="7.091" width="9.643" height="2.579" rx="1.289"/><rect fill="#6B4FBB" opacity=".5" x="30.857" width="12.857" height="2.579" rx="1.289"/><rect fill="#6B4FBB" x="9.643" y="14.182" width="12.857" height="2.579" rx="1.289"/><rect fill="#EEE" x="18.643" y="7.091" width="12.857" height="2.579" rx="1.289"/><rect fill="#6B4FBB" x="21.857" width="6.429" height="2.579" rx="1.289"/><rect fill="#EEE" x="9.643" y="7.091" width="6.429" height="2.579" rx="1.289"/><rect fill="#FDE5D8" y="7.736" width="3.857" height="1.289" rx=".645"/><rect fill="#FDE5D8" y="14.826" width="3.857" height="1.289" rx=".645"/><rect fill="#FDE5D8" y="21.917" width="3.857" height="1.289" rx=".645"/><rect fill="#EEE" x="9.643" y="21.273" width="9.643" height="2.579" rx="1.289"/><rect fill="#EEE" x="37.286" y="14.182" width="9.643" height="2.579" rx="1.289"/><rect fill="#6B4FBB" opacity=".5" x="25.071" y="35.455" width="9.643" height="2.579" rx="1.289"/><rect fill="#6B4FBB" x="18.643" y="28.364" width="9.643" height="2.579" rx="1.289"/><rect fill="#6B4FBB" x="30.857" y="21.273" width="12.857" height="2.579" rx="1.289"/><rect fill="#EEE" x="9.643" y="35.455" width="12.857" height="2.579" rx="1.289"/><rect fill="#EEE" x="21.857" y="21.273" width="6.429" height="2.579" rx="1.289"/><rect fill="#EEE" x="9.643" y="28.364" width="6.429" height="2.579" rx="1.289"/><rect fill="#EEE" x="30.857" y="28.364" width="6.429" height="2.579" rx="1.289"/><rect fill="#EEE" x="39.857" y="28.364" width="6.429" height="2.579" rx="1.289"/><rect fill="#EEE" x="49.5" y="14.182" width="6.429" height="2.579" rx="1.289"/><rect fill="#FDE5D8" y="29.008" width="3.857" height="1.289" rx=".645"/><rect fill="#FDE5D8" y="36.099" width="3.857" height="1.289" rx=".645"/><rect fill="#FDE5D8" y="43.19" width="3.857" height="1.289" rx=".645"/><rect fill="#6B4FBB" x="9.643" y="42.546" width="9.643" height="2.579" rx="1.289"/><rect fill="#EEE" x="25.071" y="56.727" width="9.643" height="2.579" rx="1.289"/><rect fill="#6B4FBB" opacity=".5" x="34.071" y="49.636" width="9.643" height="2.579" rx="1.289"/><rect fill="#EEE" x="9.643" y="56.727" width="12.857" height="2.579" rx="1.289"/><rect fill="#6B4FBB" x="18.643" y="49.636" width="12.857" height="2.579" rx="1.289"/><rect fill="#EEE" x="21.857" y="42.546" width="6.429" height="2.579" rx="1.289"/><rect fill="#EEE" x="46.286" y="49.636" width="6.429" height="2.579" rx="1.289"/><rect fill="#EEE" x="9.643" y="49.636" width="6.429" height="2.579" rx="1.289"/><rect fill="#FDE5D8" y="50.281" width="3.857" height="1.289" rx=".645"/><rect fill="#FDE5D8" y="57.372" width="3.857" height="1.289" rx=".645"/></g></g><g transform="translate(196.07)"><use stroke="#FDE5D8" mask="url(#e)" stroke-width="8" fill="#FFF" xlink:href="#b"/><rect fill="#FDB692" x="9" y="9.025" width="18.643" height="1.934" rx=".967"/><rect fill="#FDB692" x="9" y="14.826" width="25.071" height="1.934" rx=".967"/><rect fill="#FDB692" x="9" y="20.628" width="18.643" height="1.934" rx=".967"/></g><g transform="translate(189 41.256)"><ellipse stroke="#FC6D26" stroke-width="3" fill="#FFF7F4" cx="10.286" cy="9.669" rx="9.643" ry="9.669"/><path d="M.023 9.002a8.352 8.352 0 0 0 7.94-4.308M9 .644c0-.21-.008-.416-.023-.62" stroke="#FC6D26" stroke-width="2"/><path d="M5.045 2.008A10.266 10.266 0 0 0 13.5 6.446c2.112 0 4.076-.638 5.71-1.733" stroke="#FC6D26" stroke-width="2"/><ellipse fill="#FC6D26" cx="6.75" cy="11.281" rx=".964" ry=".967"/><ellipse fill="#FC6D26" cx="13.821" cy="11.281" rx=".964" ry=".967"/></g><g transform="translate(46.93 96.05)"><ellipse stroke="#6B4FBB" stroke-width="3" fill="#F4F1FA" cx="9.643" cy="10.314" rx="9.643" ry="9.669"/><path d="M12.86 4.51h-.005L11.25 2.58 9.645 4.51H9.64L8.036 2.58 6.43 4.51h-.002L4.82 2.58 3.215 4.512h-1.75A9.646 9.646 0 0 1 9.642 0c3.447 0 6.47 1.8 8.176 4.508h-1.75l-1.605-1.93L12.86 4.51z" fill="#6B4FBB"/><ellipse fill="#6B4FBB" cx="6.107" cy="11.281" rx=".964" ry=".967"/><ellipse fill="#6B4FBB" cx="13.179" cy="11.281" rx=".964" ry=".967"/></g><g transform="matrix(-1 0 0 1 56.57 54.794)"><use stroke="#E2DCF2" mask="url(#f)" stroke-width="8" fill="#FFF" xlink:href="#c"/><rect fill="#6B4FBB" opacity=".5" x="15.429" y="9.025" width="18.643" height="1.934" rx=".967"/><rect fill="#6B4FBB" opacity=".5" x="21.857" y="14.826" width="12.214" height="1.934" rx=".967"/><rect fill="#6B4FBB" opacity=".5" x="21.857" y="20.628" width="12.214" height="1.934" rx=".967"/></g></g></svg> diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml index 0e535117353..d36707dd042 100644 --- a/app/views/shared/issuable/_search_bar.html.haml +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -13,7 +13,7 @@ .issues-other-filters.filtered-search-wrapper .filtered-search-box - if type != :boards_modal && type != :boards - = dropdown_tag(content_tag(:i, '', class: 'fa fa-history'), + = dropdown_tag(custom_icon('icon_history'), options: { wrapper_class: "filtered-search-history-dropdown-wrapper", toggle_class: "filtered-search-history-dropdown-toggle-button", dropdown_class: "filtered-search-history-dropdown", @@ -45,7 +45,7 @@ {{hint}} %span.js-filter-tag.dropdown-light-content {{tag}} - #js-dropdown-author.filtered-search-input-dropdown-menu.dropdown-menu{ data: { icon: 'pencil', hint: 'author', tag: '@author' } } + #js-dropdown-author.filtered-search-input-dropdown-menu.dropdown-menu %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } } %li.filter-dropdown-item %button.btn.btn-link.dropdown-user @@ -55,7 +55,7 @@ {{name}} %span.dropdown-light-content @{{username}} - #js-dropdown-assignee.filtered-search-input-dropdown-menu.dropdown-menu{ data: { icon: 'user', hint: 'assignee', tag: '@assignee' } } + #js-dropdown-assignee.filtered-search-input-dropdown-menu.dropdown-menu %ul{ data: { dropdown: true } } %li.filter-dropdown-item{ data: { value: 'none' } } %button.btn.btn-link @@ -70,7 +70,7 @@ {{name}} %span.dropdown-light-content @{{username}} - #js-dropdown-milestone.filtered-search-input-dropdown-menu.dropdown-menu{ data: { icon: 'clock-o', hint: 'milestone', tag: '%milestone' } } + #js-dropdown-milestone.filtered-search-input-dropdown-menu.dropdown-menu %ul{ data: { dropdown: true } } %li.filter-dropdown-item{ data: { value: 'none' } } %button.btn.btn-link @@ -86,7 +86,7 @@ %li.filter-dropdown-item %button.btn.btn-link.js-data-value {{title}} - #js-dropdown-label.filtered-search-input-dropdown-menu.dropdown-menu{ data: { icon: 'tag', hint: 'label', tag: '~label', type: 'array' } } + #js-dropdown-label.filtered-search-input-dropdown-menu.dropdown-menu %ul{ data: { dropdown: true } } %li.filter-dropdown-item{ data: { value: 'none' } } %button.btn.btn-link diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index ac84fffe831..e49bd5ebb13 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -140,7 +140,7 @@ :javascript gl.sidebarOptions = { - endpoint: "#{issuable_json_path(issuable)}", + endpoint: "#{issuable_json_path(issuable)}?basic=true", editable: #{can_edit_issuable ? true : false}, currentUser: #{current_user.to_json(only: [:username, :id, :name], methods: :avatar_url)}, rootPath: "#{root_path}" diff --git a/app/views/shared/issuable/_sidebar_assignees.html.haml b/app/views/shared/issuable/_sidebar_assignees.html.haml index e9ce7b7ce9c..26567c08eb6 100644 --- a/app/views/shared/issuable/_sidebar_assignees.html.haml +++ b/app/views/shared/issuable/_sidebar_assignees.html.haml @@ -1,5 +1,8 @@ - if issuable.is_a?(Issue) #js-vue-sidebar-assignees{ data: { field: "#{issuable.to_ability_name}[assignee_ids]" } } + .title.hide-collapsed + Assignee + = icon('spinner spin') - else .sidebar-collapsed-icon.sidebar-collapsed-user{ data: { toggle: "tooltip", placement: "left", container: "body" }, title: (issuable.assignee.name if issuable.assignee) } - if issuable.assignee diff --git a/app/views/shared/notes/_edit.html.haml b/app/views/shared/notes/_edit.html.haml index 4a020865828..f4b3aac29b4 100644 --- a/app/views/shared/notes/_edit.html.haml +++ b/app/views/shared/notes/_edit.html.haml @@ -1,3 +1 @@ -.original-note-content.hidden{ data: { post_url: note_url(note), target_id: note.noteable.id, target_type: note.noteable.class.name.underscore } } - #{note.note} %textarea.hidden.js-task-list-field.original-task-list{ data: {update_url: note_url(note) } }= note.note diff --git a/app/views/shared/notes/_hints.html.haml b/app/views/shared/notes/_hints.html.haml index 81d97eabe65..7ce6130de60 100644 --- a/app/views/shared/notes/_hints.html.haml +++ b/app/views/shared/notes/_hints.html.haml @@ -9,6 +9,27 @@ - else is supported - %button.toolbar-button.markdown-selector{ type: 'button', tabindex: '-1' } - = icon('file-image-o', class: 'toolbar-button-icon') - Attach a file + + %span.uploading-container + %span.uploading-progress-container.hide + = icon('file-image-o', class: 'toolbar-button-icon') + %span.attaching-file-message + -# Populated by app/assets/javascripts/dropzone_input.js + %span.uploading-progress 0% + %span.uploading-spinner + = icon('spinner spin', class: 'toolbar-button-icon') + + %span.uploading-error-container.hide + %span.uploading-error-icon + = icon('file-image-o', class: 'toolbar-button-icon') + %span.uploading-error-message + -# Populated by app/assets/javascripts/dropzone_input.js + %button.retry-uploading-link{ type: 'button' } Try again + or + %button.attach-new-file.markdown-selector{ type: 'button' } attach a new file + + %button.markdown-selector.button-attach-file{ type: 'button', tabindex: '-1' } + = icon('file-image-o', class: 'toolbar-button-icon') + Attach a file + + %button.btn.btn-default.btn-xs.hide.button-cancel-uploading-files{ type: 'button' } Cancel diff --git a/app/views/shared/notes/_note.html.haml b/app/views/shared/notes/_note.html.haml index 87aae793966..a7bf610b9c7 100644 --- a/app/views/shared/notes/_note.html.haml +++ b/app/views/shared/notes/_note.html.haml @@ -29,8 +29,6 @@ - if note.system %span.system-note-message = note.redacted_note_html - .original-note-content.hidden - = note.note %a{ href: "##{dom_id(note)}" } = time_ago_with_tooltip(note.created_at, placement: 'bottom', html_class: 'note-created-ago') - unless note.system? @@ -43,6 +41,8 @@ .note-text.md = note.redacted_note_html = edited_time_ago_with_tooltip(note, placement: 'bottom', html_class: 'note_edited_ago') + .original-note-content.hidden{ data: { post_url: note_url(note), target_id: note.noteable.id, target_type: note.noteable.class.name.underscore } } + #{note.note} - if note_editable = render 'shared/notes/edit', note: note .note-awards diff --git a/app/views/shared/notes/_notes_with_form.html.haml b/app/views/shared/notes/_notes_with_form.html.haml index 9930cbd96d7..785b1b22a49 100644 --- a/app/views/shared/notes/_notes_with_form.html.haml +++ b/app/views/shared/notes/_notes_with_form.html.haml @@ -23,4 +23,4 @@ to post a comment :javascript - var notes = new Notes("#{notes_url}", #{@notes.map(&:id).to_json}, #{Time.now.to_i}, "#{diff_view}") + var notes = new Notes("#{notes_url}", #{@notes.map(&:id).to_json}, #{Time.now.to_i}, "#{diff_view}", #{autocomplete}) diff --git a/app/views/shared/web_hooks/_form.html.haml b/app/views/shared/web_hooks/_form.html.haml index 37c3e61912c..1f0e7629fb4 100644 --- a/app/views/shared/web_hooks/_form.html.haml +++ b/app/views/shared/web_hooks/_form.html.haml @@ -54,10 +54,10 @@ %p.light This URL will be triggered when a merge request is created/updated/merged %li - = form.check_box :build_events, class: 'pull-left' + = form.check_box :job_events, class: 'pull-left' .prepend-left-20 - = form.label :build_events, class: 'list-label' do - %strong Jobs events + = form.label :job_events, class: 'list-label' do + %strong Job events %p.light This URL will be triggered when the job status changes %li diff --git a/app/views/snippets/notes/_actions.html.haml b/app/views/snippets/notes/_actions.html.haml index 679a5e934da..e8119642ab8 100644 --- a/app/views/snippets/notes/_actions.html.haml +++ b/app/views/snippets/notes/_actions.html.haml @@ -1,7 +1,7 @@ - if current_user - if note.emoji_awardable? - user_authored = note.user_authored?(current_user) - = link_to '#', title: 'Award Emoji', class: "note-action-button note-emoji-button js-add-award js-note-emoji #{'js-user-authored' if user_authored} has-tooltip", data: { position: 'right' } do + = link_to '#', title: 'Add reaction', class: "note-action-button note-emoji-button js-add-award js-note-emoji #{'js-user-authored' if user_authored} has-tooltip", data: { position: 'right' } do = icon('spinner spin') %span{ class: 'link-highlight award-control-icon-neutral' }= custom_icon('emoji_slightly_smiling_face') %span{ class: 'link-highlight award-control-icon-positive' }= custom_icon('emoji_smiley') diff --git a/app/views/snippets/show.html.haml b/app/views/snippets/show.html.haml index 51dbbc32cc9..216184eb839 100644 --- a/app/views/snippets/show.html.haml +++ b/app/views/snippets/show.html.haml @@ -9,4 +9,4 @@ .row-content-block.top-block.content-component-block = render 'award_emoji/awards_block', awardable: @snippet, inline: true - #notes= render "shared/notes/notes_with_form" + #notes= render "shared/notes/notes_with_form", :autocomplete => false diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index 03e5dd97405..c587155bc4f 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -10,7 +10,7 @@ = auto_discovery_link_tag(:atom, user_url(@user, format: :atom), title: "#{@user.name} activity") .user-profile - .cover-block.user-cover-block + .cover-block.user-cover-block.layout-nav .cover-controls - if @user == current_user = link_to profile_path, class: 'btn btn-gray has-tooltip', title: 'Edit profile', 'aria-label': 'Edit profile' do @@ -56,11 +56,11 @@ = icon('skype') - unless @user.linkedin.blank? .profile-link-holder.middle-dot-divider - = link_to "https://www.linkedin.com/in/#{@user.linkedin}", title: "LinkedIn" do + = link_to linkedin_url(@user), title: "LinkedIn" do = icon('linkedin-square') - unless @user.twitter.blank? .profile-link-holder.middle-dot-divider - = link_to "https://twitter.com/#{@user.twitter}", title: "Twitter" do + = link_to twitter_url(@user), title: "Twitter" do = icon('twitter-square') - unless @user.website_url.blank? .profile-link-holder.middle-dot-divider @@ -71,7 +71,7 @@ = @user.location - unless @user.organization.blank? .profile-link-holder.middle-dot-divider - = icon('building') + = icon('briefcase') = @user.organization - if @user.bio.present? @@ -82,7 +82,7 @@ .scrolling-tabs-container .fade-left= icon('angle-left') .fade-right= icon('angle-right') - %ul.nav-links.center.user-profile-nav.scrolling-tabs + %ul.nav-links.user-profile-nav.scrolling-tabs %li.js-activity-tab = link_to user_path, data: { target: 'div#activity', action: 'activity', toggle: 'tab' } do Activity diff --git a/app/workers/expire_job_cache_worker.rb b/app/workers/expire_job_cache_worker.rb new file mode 100644 index 00000000000..08e281e7350 --- /dev/null +++ b/app/workers/expire_job_cache_worker.rb @@ -0,0 +1,35 @@ +class ExpireJobCacheWorker + include Sidekiq::Worker + include BuildQueue + + def perform(job_id) + job = CommitStatus.joins(:pipeline, :project).find_by(id: job_id) + return unless job + + pipeline = job.pipeline + project = job.project + + Gitlab::EtagCaching::Store.new.tap do |store| + store.touch(project_pipeline_path(project, pipeline)) + store.touch(project_job_path(project, job)) + end + end + + private + + def project_pipeline_path(project, pipeline) + Gitlab::Routing.url_helpers.namespace_project_pipeline_path( + project.namespace, + project, + pipeline, + format: :json) + end + + def project_job_path(project, job) + Gitlab::Routing.url_helpers.namespace_project_build_path( + project.namespace, + project, + job.id, + format: :json) + end +end diff --git a/app/workers/expire_pipeline_cache_worker.rb b/app/workers/expire_pipeline_cache_worker.rb index 603e2f1aaea..d760f5b140f 100644 --- a/app/workers/expire_pipeline_cache_worker.rb +++ b/app/workers/expire_pipeline_cache_worker.rb @@ -10,6 +10,7 @@ class ExpirePipelineCacheWorker store = Gitlab::EtagCaching::Store.new store.touch(project_pipelines_path(project)) + store.touch(project_pipeline_path(project, pipeline)) store.touch(commit_pipelines_path(project, pipeline.commit)) if pipeline.commit store.touch(new_merge_request_pipelines_path(project)) each_pipelines_merge_request_path(project, pipeline) do |path| @@ -28,6 +29,14 @@ class ExpirePipelineCacheWorker format: :json) end + def project_pipeline_path(project, pipeline) + Gitlab::Routing.url_helpers.namespace_project_pipeline_path( + project.namespace, + project, + pipeline, + format: :json) + end + def commit_pipelines_path(project, commit) Gitlab::Routing.url_helpers.pipelines_namespace_project_commit_path( project.namespace, diff --git a/app/workers/pipeline_schedule_worker.rb b/app/workers/pipeline_schedule_worker.rb index a449a765f7b..7eb0e84acb2 100644 --- a/app/workers/pipeline_schedule_worker.rb +++ b/app/workers/pipeline_schedule_worker.rb @@ -3,8 +3,14 @@ class PipelineScheduleWorker include CronjobQueue def perform - Ci::PipelineSchedule.active.where("next_run_at < ?", Time.now).find_each do |schedule| + Ci::PipelineSchedule.active.where("next_run_at < ?", Time.now) + .preload(:owner, :project).find_each do |schedule| begin + unless schedule.runnable_by_owner? + schedule.deactivate! + next + end + Ci::CreatePipelineService.new(schedule.project, schedule.owner, ref: schedule.ref) diff --git a/changelogs/unreleased/12910-personal-snippet-prep-2.yml b/changelogs/unreleased/12910-personal-snippet-prep-2.yml deleted file mode 100644 index bd9527c30c8..00000000000 --- a/changelogs/unreleased/12910-personal-snippet-prep-2.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Support Markdown previews for personal snippets -merge_request: 10810 -author: diff --git a/changelogs/unreleased/12910-personal-snippets-notes-show.yml b/changelogs/unreleased/12910-personal-snippets-notes-show.yml deleted file mode 100644 index 15c6f3c5e6a..00000000000 --- a/changelogs/unreleased/12910-personal-snippets-notes-show.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Display comments for personal snippets -merge_request: -author: diff --git a/changelogs/unreleased/12910-personal-snippets-notes.yml b/changelogs/unreleased/12910-personal-snippets-notes.yml deleted file mode 100644 index 7f1576c3513..00000000000 --- a/changelogs/unreleased/12910-personal-snippets-notes.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Support comments for personal snippets -merge_request: -author: diff --git a/changelogs/unreleased/12910-uploader-pers-snippet.yml b/changelogs/unreleased/12910-uploader-pers-snippet.yml deleted file mode 100644 index 1c163632fc6..00000000000 --- a/changelogs/unreleased/12910-uploader-pers-snippet.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Support uploaders for personal snippets comments -merge_request: -author: diff --git a/changelogs/unreleased/1440-db-backup-ssl-support.yml b/changelogs/unreleased/1440-db-backup-ssl-support.yml deleted file mode 100644 index c78bb4fd351..00000000000 --- a/changelogs/unreleased/1440-db-backup-ssl-support.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Database SSL support for backup script. -merge_request: 9715 -author: Guillaume Simon diff --git a/changelogs/unreleased/17361-redirect-renamed-paths.yml b/changelogs/unreleased/17361-redirect-renamed-paths.yml deleted file mode 100644 index 7a33c9fb3ec..00000000000 --- a/changelogs/unreleased/17361-redirect-renamed-paths.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Redirect old links after renaming a user/group/project. -merge_request: 10370 -author: diff --git a/changelogs/unreleased/17489-hide-code-from-guests.yml b/changelogs/unreleased/17489-hide-code-from-guests.yml new file mode 100644 index 00000000000..eb6daffedfe --- /dev/null +++ b/changelogs/unreleased/17489-hide-code-from-guests.yml @@ -0,0 +1,4 @@ +--- +title: Hide clone panel and file list when user is only a guest +merge_request: +author: James Clark diff --git a/changelogs/unreleased/18927-reorder-issue-action-buttons.yml b/changelogs/unreleased/18927-reorder-issue-action-buttons.yml new file mode 100644 index 00000000000..793d6582940 --- /dev/null +++ b/changelogs/unreleased/18927-reorder-issue-action-buttons.yml @@ -0,0 +1,4 @@ +--- +title: Reorder Issue action buttons in order of usability +merge_request: 11642 +author: diff --git a/changelogs/unreleased/19364-webhook-edit.yml b/changelogs/unreleased/19364-webhook-edit.yml deleted file mode 100644 index 60e154b8b83..00000000000 --- a/changelogs/unreleased/19364-webhook-edit.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Implement ability to edit hooks -merge_request: 10816 -author: Alexander Randa diff --git a/changelogs/unreleased/20378-natural-sort-issue-numbers.yml b/changelogs/unreleased/20378-natural-sort-issue-numbers.yml deleted file mode 100644 index 2ebc8485ddf..00000000000 --- a/changelogs/unreleased/20378-natural-sort-issue-numbers.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Change issues list in MR to natural sorting -merge_request: 7110 -author: Jeff Stubler diff --git a/changelogs/unreleased/21683-show-created-group-name-flash.yml b/changelogs/unreleased/21683-show-created-group-name-flash.yml deleted file mode 100644 index 06ef5e972fc..00000000000 --- a/changelogs/unreleased/21683-show-created-group-name-flash.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Show group name on flash container when group is created from Admin area. -merge_request: 10905 -author: diff --git a/changelogs/unreleased/22714-update-all-instances-of-fa-refresh.yml b/changelogs/unreleased/22714-update-all-instances-of-fa-refresh.yml deleted file mode 100644 index ad7c011933f..00000000000 --- a/changelogs/unreleased/22714-update-all-instances-of-fa-refresh.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Update all instances of the old loading icon -merge_request: 10490 -author: Andrew Torres diff --git a/changelogs/unreleased/22826-ui-inconsistency-different-files-views-find-file-button-missing.yml b/changelogs/unreleased/22826-ui-inconsistency-different-files-views-find-file-button-missing.yml deleted file mode 100644 index c42fbd4e1f1..00000000000 --- a/changelogs/unreleased/22826-ui-inconsistency-different-files-views-find-file-button-missing.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix UI inconsistency different files view (find file button missing) -merge_request: 9847 -author: TM Lee diff --git a/changelogs/unreleased/23751-add-contribution-graph-key-tooltips.yml b/changelogs/unreleased/23751-add-contribution-graph-key-tooltips.yml deleted file mode 100644 index 7c4c6fb46a0..00000000000 --- a/changelogs/unreleased/23751-add-contribution-graph-key-tooltips.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add tooltips to user contribution graph key -merge_request: 11138 -author: diff --git a/changelogs/unreleased/24373-warning-message-go-away.yml b/changelogs/unreleased/24373-warning-message-go-away.yml new file mode 100644 index 00000000000..c0f2fd260ba --- /dev/null +++ b/changelogs/unreleased/24373-warning-message-go-away.yml @@ -0,0 +1,4 @@ +--- +title: 'Notes: Warning message should go away once resolved' +merge_request: 10823 +author: Jacopo Beschi @jacopo-beschi diff --git a/changelogs/unreleased/24883-build-failure-summary-page.yml b/changelogs/unreleased/24883-build-failure-summary-page.yml deleted file mode 100644 index 214cd3e2bc7..00000000000 --- a/changelogs/unreleased/24883-build-failure-summary-page.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Added build failures summary page for pipelines -merge_request: 10719 -author: diff --git a/changelogs/unreleased/25226-realtime-pipelines-fe.yml b/changelogs/unreleased/25226-realtime-pipelines-fe.yml deleted file mode 100644 index 1149c8f0eac..00000000000 --- a/changelogs/unreleased/25226-realtime-pipelines-fe.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Re-rewrites pipeline graph in vue to support realtime data updates -merge_request: -author: diff --git a/changelogs/unreleased/26208-animate-drodowns.yml b/changelogs/unreleased/26208-animate-drodowns.yml deleted file mode 100644 index 580f6c12f67..00000000000 --- a/changelogs/unreleased/26208-animate-drodowns.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add animations to all the dropdowns -merge_request: 8419 -author: diff --git a/changelogs/unreleased/26437-closed-by.yml b/changelogs/unreleased/26437-closed-by.yml deleted file mode 100644 index 6325d3576bc..00000000000 --- a/changelogs/unreleased/26437-closed-by.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add issues/:iid/closed_by api endpoint -merge_request: -author: mhasbini diff --git a/changelogs/unreleased/26488-target-disabled-mr.yml b/changelogs/unreleased/26488-target-disabled-mr.yml deleted file mode 100644 index 02058481ccf..00000000000 --- a/changelogs/unreleased/26488-target-disabled-mr.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Disallow merge requests from fork when source project have disabled merge requests -merge_request: -author: mhasbini diff --git a/changelogs/unreleased/26509-show-update-time.yml b/changelogs/unreleased/26509-show-update-time.yml deleted file mode 100644 index 012fd00dd87..00000000000 --- a/changelogs/unreleased/26509-show-update-time.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add update time to project lists. -merge_request: 8514 -author: Jeff Stubler diff --git a/changelogs/unreleased/26585-remove-readme-view-caching.yml b/changelogs/unreleased/26585-remove-readme-view-caching.yml deleted file mode 100644 index 6aefae982bf..00000000000 --- a/changelogs/unreleased/26585-remove-readme-view-caching.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: 'Remove view fragment caching for project READMEs' -merge_request: 8838 -author: diff --git a/changelogs/unreleased/26883-members-page-layout-looks-broken.yml b/changelogs/unreleased/26883-members-page-layout-looks-broken.yml deleted file mode 100644 index e0e3a529c3e..00000000000 --- a/changelogs/unreleased/26883-members-page-layout-looks-broken.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Improved UX on project members settings view -merge_request: -author: diff --git a/changelogs/unreleased/27376-bvl-load-pipelinestatus-in-batch.yml b/changelogs/unreleased/27376-bvl-load-pipelinestatus-in-batch.yml deleted file mode 100644 index 3d615f5d8a7..00000000000 --- a/changelogs/unreleased/27376-bvl-load-pipelinestatus-in-batch.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fetch pipeline status in batch from redis -merge_request: 10785 -author: diff --git a/changelogs/unreleased/27614-instant-comments.yml b/changelogs/unreleased/27614-instant-comments.yml deleted file mode 100644 index 7b2592f46ed..00000000000 --- a/changelogs/unreleased/27614-instant-comments.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add support for instantly updating comments -merge_request: 10760 -author: diff --git a/changelogs/unreleased/27655-clear-emoji-search-after-selection.yml b/changelogs/unreleased/27655-clear-emoji-search-after-selection.yml deleted file mode 100644 index 5fd02696323..00000000000 --- a/changelogs/unreleased/27655-clear-emoji-search-after-selection.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Clear emoji search in awards menu after picking emoji -merge_request: -author: diff --git a/changelogs/unreleased/27729-improve-webpack-dev-environment.yml b/changelogs/unreleased/27729-improve-webpack-dev-environment.yml deleted file mode 100644 index d04ea70ab1c..00000000000 --- a/changelogs/unreleased/27729-improve-webpack-dev-environment.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add webpack_bundle_tag helper to improve non-localhost GDK configurations -merge_request: 10604 -author: diff --git a/changelogs/unreleased/27827-cleanup-markdown.yml b/changelogs/unreleased/27827-cleanup-markdown.yml deleted file mode 100644 index a8890b78763..00000000000 --- a/changelogs/unreleased/27827-cleanup-markdown.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Cleanup markdown spacing -merge_request: -author: diff --git a/changelogs/unreleased/28017-separate-ce-params-on-api.yml b/changelogs/unreleased/28017-separate-ce-params-on-api.yml deleted file mode 100644 index 039a8d207b0..00000000000 --- a/changelogs/unreleased/28017-separate-ce-params-on-api.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Separate CE params on Grape API -merge_request: -author: diff --git a/changelogs/unreleased/28020-improve-todo-list-when-comes-from-yourself.yml b/changelogs/unreleased/28020-improve-todo-list-when-comes-from-yourself.yml deleted file mode 100644 index 14aecc35bd2..00000000000 --- a/changelogs/unreleased/28020-improve-todo-list-when-comes-from-yourself.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Improve text on todo list when the todo action comes from yourself -merge_request: 10594 -author: Jacopo Beschi @jacopo-beschi diff --git a/changelogs/unreleased/28202_decrease_abc_threshold_step1.yml b/changelogs/unreleased/28202_decrease_abc_threshold_step1.yml deleted file mode 100644 index 8f1520c8b42..00000000000 --- a/changelogs/unreleased/28202_decrease_abc_threshold_step1.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Decrease ABC threshold to 57.08 -merge_request: 10724 -author: Rydkin Maxim diff --git a/changelogs/unreleased/28408-feature-proposal-include-search-options-to-pipelines-api.yml b/changelogs/unreleased/28408-feature-proposal-include-search-options-to-pipelines-api.yml deleted file mode 100644 index 9b9f0032810..00000000000 --- a/changelogs/unreleased/28408-feature-proposal-include-search-options-to-pipelines-api.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: 'API: Add parameters to allow filtering project pipelines' -merge_request: 9367 -author: dosuken123 diff --git a/changelogs/unreleased/28457-slash-command-board-move.yml b/changelogs/unreleased/28457-slash-command-board-move.yml deleted file mode 100644 index cec0f89ed91..00000000000 --- a/changelogs/unreleased/28457-slash-command-board-move.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add board_move slash command -merge_request: 10433 -author: Alex Sanford diff --git a/changelogs/unreleased/28558-create-new-branch-from-issue-page.yml b/changelogs/unreleased/28558-create-new-branch-from-issue-page.yml deleted file mode 100644 index e43b043d6c5..00000000000 --- a/changelogs/unreleased/28558-create-new-branch-from-issue-page.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Allow to create new branch and empty WIP merge request from issue page -merge_request: -author: diff --git a/changelogs/unreleased/28575-expand-collapse-look.yml b/changelogs/unreleased/28575-expand-collapse-look.yml deleted file mode 100644 index d8943316300..00000000000 --- a/changelogs/unreleased/28575-expand-collapse-look.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Expand/collapse button -> Change to make it look like a toggle -merge_request: 10720 -author: Jacopo Beschi @jacopo-beschi diff --git a/changelogs/unreleased/28968-prevent-people-from-creating-branches-if-they-don-have-permission-to-push.yml b/changelogs/unreleased/28968-prevent-people-from-creating-branches-if-they-don-have-permission-to-push.yml deleted file mode 100644 index 6612cfd8866..00000000000 --- a/changelogs/unreleased/28968-prevent-people-from-creating-branches-if-they-don-have-permission-to-push.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Prevent people from creating branches if they don't have persmission to push -merge_request: -author: diff --git a/changelogs/unreleased/29056-backport-ee-cleanup-database-file.yml b/changelogs/unreleased/29056-backport-ee-cleanup-database-file.yml deleted file mode 100644 index 0ebb9d57611..00000000000 --- a/changelogs/unreleased/29056-backport-ee-cleanup-database-file.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Turns true value and false value database methods from instance to class methods -merge_request: 10583 -author: diff --git a/changelogs/unreleased/29145-oauth-422.yml b/changelogs/unreleased/29145-oauth-422.yml deleted file mode 100644 index 94e4cd84ad1..00000000000 --- a/changelogs/unreleased/29145-oauth-422.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Redesign auth 422 page -merge_request: -author: diff --git a/changelogs/unreleased/29181-add-more-tests-for-spec-controllers-projects-builds-controller-spec-rb.yml b/changelogs/unreleased/29181-add-more-tests-for-spec-controllers-projects-builds-controller-spec-rb.yml deleted file mode 100644 index 7a3d687d73f..00000000000 --- a/changelogs/unreleased/29181-add-more-tests-for-spec-controllers-projects-builds-controller-spec-rb.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Resolve "Add more tests for spec/controllers/projects/builds_controller_spec.rb" -merge_request: 10244 -author: dosuken123 diff --git a/changelogs/unreleased/29505-allow-admins-sudo-to-blocked-users.yml b/changelogs/unreleased/29505-allow-admins-sudo-to-blocked-users.yml deleted file mode 100644 index 42fd71ccd5f..00000000000 --- a/changelogs/unreleased/29505-allow-admins-sudo-to-blocked-users.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Allow admins to sudo to blocked users via the API -merge_request: 10842 -author: diff --git a/changelogs/unreleased/29595-customize-experience-callout.yml b/changelogs/unreleased/29595-customize-experience-callout.yml deleted file mode 100644 index ec8393142c6..00000000000 --- a/changelogs/unreleased/29595-customize-experience-callout.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: 29595 Update callout design -merge_request: -author: diff --git a/changelogs/unreleased/29673-500-internal-server-error-when-enabling-a-deploy-key-more-than-once-through-api.yml b/changelogs/unreleased/29673-500-internal-server-error-when-enabling-a-deploy-key-more-than-once-through-api.yml deleted file mode 100644 index 3e62ede1521..00000000000 --- a/changelogs/unreleased/29673-500-internal-server-error-when-enabling-a-deploy-key-more-than-once-through-api.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Detect already enabled DeployKeys in EnableDeployKeyService -merge_request: -author: diff --git a/changelogs/unreleased/29712-unnecessary-wait-for-ajax.yml b/changelogs/unreleased/29712-unnecessary-wait-for-ajax.yml deleted file mode 100644 index 8dc657a4aba..00000000000 --- a/changelogs/unreleased/29712-unnecessary-wait-for-ajax.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Remove unnecessary test helpers includes -merge_request: 10567 -author: Jacopo Beschi @jacopo-beschi diff --git a/changelogs/unreleased/29734-prometheus-monitoring-page-displays-button-to-control-manual-actions.yml b/changelogs/unreleased/29734-prometheus-monitoring-page-displays-button-to-control-manual-actions.yml deleted file mode 100644 index ca4a8889454..00000000000 --- a/changelogs/unreleased/29734-prometheus-monitoring-page-displays-button-to-control-manual-actions.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Remove pipeline controls for last deployment from Environment monitoring page -merge_request: 10769 -author: diff --git a/changelogs/unreleased/29801-add-slash-slack-commands-to-api-doc.yml b/changelogs/unreleased/29801-add-slash-slack-commands-to-api-doc.yml deleted file mode 100644 index 9c5df690085..00000000000 --- a/changelogs/unreleased/29801-add-slash-slack-commands-to-api-doc.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Add Slack slash command api to services documentation and rearrange order and - cases -merge_request: 10757 -author: TM Lee diff --git a/changelogs/unreleased/29816-create-keyboard-shortcut-for-editing-wiki-page.yml b/changelogs/unreleased/29816-create-keyboard-shortcut-for-editing-wiki-page.yml deleted file mode 100644 index a165c70a6d3..00000000000 --- a/changelogs/unreleased/29816-create-keyboard-shortcut-for-editing-wiki-page.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add keyboard edit shotcut for wiki -merge_request: 10245 -author: George Andrinopoulos diff --git a/changelogs/unreleased/29852-latex-formatting.yml b/changelogs/unreleased/29852-latex-formatting.yml new file mode 100644 index 00000000000..e96cda1d6b3 --- /dev/null +++ b/changelogs/unreleased/29852-latex-formatting.yml @@ -0,0 +1,4 @@ +--- +title: Fix LaTeX formatting for AsciiDoc wiki +merge_request: 11212 +author: diff --git a/changelogs/unreleased/29903-remove-user-is-admin-flag-from-api.yml b/changelogs/unreleased/29903-remove-user-is-admin-flag-from-api.yml deleted file mode 100644 index a0d497ac1e9..00000000000 --- a/changelogs/unreleased/29903-remove-user-is-admin-flag-from-api.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Don't display the is_admin flag in most API responses -merge_request: 10846 -author: diff --git a/changelogs/unreleased/29925-gitlab-shell-hooks-can-no-longer-send-absolute-paths-to-gitlab-ce.yml b/changelogs/unreleased/29925-gitlab-shell-hooks-can-no-longer-send-absolute-paths-to-gitlab-ce.yml deleted file mode 100644 index 1df8f695ef1..00000000000 --- a/changelogs/unreleased/29925-gitlab-shell-hooks-can-no-longer-send-absolute-paths-to-gitlab-ce.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Generate and handle a gl_repository param to pass around components -merge_request: 10992 -author: diff --git a/changelogs/unreleased/29977-style-comments-and-system-notes-real-time-updates.yml b/changelogs/unreleased/29977-style-comments-and-system-notes-real-time-updates.yml deleted file mode 100644 index c1640777e12..00000000000 --- a/changelogs/unreleased/29977-style-comments-and-system-notes-real-time-updates.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Added quick-update (fade-in) animation to newly rendered notes -merge_request: 10623 -author: diff --git a/changelogs/unreleased/30007-done-todo-hover-state.yml b/changelogs/unreleased/30007-done-todo-hover-state.yml deleted file mode 100644 index bfbde7a49c8..00000000000 --- a/changelogs/unreleased/30007-done-todo-hover-state.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add transparent top-border to the hover state of done todos -merge_request: -author: diff --git a/changelogs/unreleased/30272-bvl-reject-more-namespaces.yml b/changelogs/unreleased/30272-bvl-reject-more-namespaces.yml deleted file mode 100644 index 56bce084546..00000000000 --- a/changelogs/unreleased/30272-bvl-reject-more-namespaces.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Improve validation of namespace & project paths -merge_request: 10413 -author: diff --git a/changelogs/unreleased/30286-ci-badge-component.yml b/changelogs/unreleased/30286-ci-badge-component.yml deleted file mode 100644 index 13c2a4598c8..00000000000 --- a/changelogs/unreleased/30286-ci-badge-component.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Refactor all CI vue badges to use the same vue component -merge_request: -author: diff --git a/changelogs/unreleased/30305-oauth-token-push-code.yml b/changelogs/unreleased/30305-oauth-token-push-code.yml deleted file mode 100644 index aadfb5ca419..00000000000 --- a/changelogs/unreleased/30305-oauth-token-push-code.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Allow OAuth clients to push code -merge_request: 10677 -author: diff --git a/changelogs/unreleased/30349-create-users-build-service.yml b/changelogs/unreleased/30349-create-users-build-service.yml deleted file mode 100644 index 49b571f5646..00000000000 --- a/changelogs/unreleased/30349-create-users-build-service.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Implement Users::BuildService -merge_request: 30349 -author: George Andrinopoulos diff --git a/changelogs/unreleased/30458-real-time-note-edits.yml b/changelogs/unreleased/30458-real-time-note-edits.yml deleted file mode 100644 index f67348c5302..00000000000 --- a/changelogs/unreleased/30458-real-time-note-edits.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Update note edits in real-time -merge_request: -author: diff --git a/changelogs/unreleased/30466-click-x-to-remove-filter.yml b/changelogs/unreleased/30466-click-x-to-remove-filter.yml deleted file mode 100644 index 2cf08e84ed1..00000000000 --- a/changelogs/unreleased/30466-click-x-to-remove-filter.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add button to delete filters from filtered search bar -merge_request: -author: diff --git a/changelogs/unreleased/30484-profile-dropdown-account-name.yml b/changelogs/unreleased/30484-profile-dropdown-account-name.yml deleted file mode 100644 index 71aa1ce139b..00000000000 --- a/changelogs/unreleased/30484-profile-dropdown-account-name.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Added profile name to user dropdown -merge_request: -author: diff --git a/changelogs/unreleased/30529-remove-pages-tab-if-pages-isn-t-enabled.yml b/changelogs/unreleased/30529-remove-pages-tab-if-pages-isn-t-enabled.yml deleted file mode 100644 index 16938f05326..00000000000 --- a/changelogs/unreleased/30529-remove-pages-tab-if-pages-isn-t-enabled.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Disable navigation to Project-level pages configuration when Pages disabled -merge_request: 11008 -author: diff --git a/changelogs/unreleased/30535-display-whether-pages-is-enabled-in-the-admin-dashboard.yml b/changelogs/unreleased/30535-display-whether-pages-is-enabled-in-the-admin-dashboard.yml deleted file mode 100644 index 4452b13037b..00000000000 --- a/changelogs/unreleased/30535-display-whether-pages-is-enabled-in-the-admin-dashboard.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Display GitLab Pages status in Admin Dashboard -merge_request: -author: diff --git a/changelogs/unreleased/30667-creating-new-label-on-new-issue-causing-bug.yml b/changelogs/unreleased/30667-creating-new-label-on-new-issue-causing-bug.yml deleted file mode 100644 index ce0ea69211e..00000000000 --- a/changelogs/unreleased/30667-creating-new-label-on-new-issue-causing-bug.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix label creation from issuable for subgroup projects -merge_request: -author: diff --git a/changelogs/unreleased/30672-versioned-markdown-cache.yml b/changelogs/unreleased/30672-versioned-markdown-cache.yml deleted file mode 100644 index d8f977b01de..00000000000 --- a/changelogs/unreleased/30672-versioned-markdown-cache.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Replace rake cache:clear:db with an automatic mechanism -merge_request: 10597 -author: diff --git a/changelogs/unreleased/30678-improve-dev-server-process.yml b/changelogs/unreleased/30678-improve-dev-server-process.yml deleted file mode 100644 index efa2fc210e3..00000000000 --- a/changelogs/unreleased/30678-improve-dev-server-process.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Keep webpack-dev-server process functional across branch changes -merge_request: 10581 -author: diff --git a/changelogs/unreleased/30827-changes-to-audit-log.yml b/changelogs/unreleased/30827-changes-to-audit-log.yml new file mode 100644 index 00000000000..32db3bf8e95 --- /dev/null +++ b/changelogs/unreleased/30827-changes-to-audit-log.yml @@ -0,0 +1,4 @@ +--- +title: Renamed users 'Audit Log'' to 'Authentication Log' +merge_request: 11400 +author: diff --git a/changelogs/unreleased/30903-vertically-align-mini-pipeline.yml b/changelogs/unreleased/30903-vertically-align-mini-pipeline.yml deleted file mode 100644 index af87e5ce39f..00000000000 --- a/changelogs/unreleased/30903-vertically-align-mini-pipeline.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Vertically align mini pipeline stage container -merge_request: -author: diff --git a/changelogs/unreleased/31009-disable-test-settings-on-services-when-repository-is-empty.yml b/changelogs/unreleased/31009-disable-test-settings-on-services-when-repository-is-empty.yml deleted file mode 100644 index 6e43a032f20..00000000000 --- a/changelogs/unreleased/31009-disable-test-settings-on-services-when-repository-is-empty.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Disable test settings on chat notification services when repository is empty -merge_request: 10759 -author: diff --git a/changelogs/unreleased/31057-unnecessary-padding-along-left-side-of-assignees-dropdown.yml b/changelogs/unreleased/31057-unnecessary-padding-along-left-side-of-assignees-dropdown.yml deleted file mode 100644 index 0d82bf878c7..00000000000 --- a/changelogs/unreleased/31057-unnecessary-padding-along-left-side-of-assignees-dropdown.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Show checkmark on current assignee in assignee dropdown -merge_request: 10767 -author: diff --git a/changelogs/unreleased/31106-tabs-alignment.yml b/changelogs/unreleased/31106-tabs-alignment.yml deleted file mode 100644 index 53da08cc32d..00000000000 --- a/changelogs/unreleased/31106-tabs-alignment.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: prevent nav tabs from wrapping to new line -merge_request: -author: diff --git a/changelogs/unreleased/31138-improve-test-settings-for-services-in-empty-projects.yml b/changelogs/unreleased/31138-improve-test-settings-for-services-in-empty-projects.yml deleted file mode 100644 index cb1de425d66..00000000000 --- a/changelogs/unreleased/31138-improve-test-settings-for-services-in-empty-projects.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Improves test settings for chat notification services for empty projects -merge_request: 10886 -author: diff --git a/changelogs/unreleased/31156-environments-vue-service.yml b/changelogs/unreleased/31156-environments-vue-service.yml deleted file mode 100644 index 8b899ed9861..00000000000 --- a/changelogs/unreleased/31156-environments-vue-service.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix environments vue architecture to match documentation -merge_request: -author: diff --git a/changelogs/unreleased/31157-respect-project-features-in-wiki-search.yml b/changelogs/unreleased/31157-respect-project-features-in-wiki-search.yml deleted file mode 100644 index 721bb435a2e..00000000000 --- a/changelogs/unreleased/31157-respect-project-features-in-wiki-search.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Enforce project features when searching blobs and wikis -merge_request: -author: diff --git a/changelogs/unreleased/31193-ff-copy.yml b/changelogs/unreleased/31193-ff-copy.yml deleted file mode 100644 index 4d44d83d458..00000000000 --- a/changelogs/unreleased/31193-ff-copy.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: fix inline diff copy in firefox -merge_request: -author: diff --git a/changelogs/unreleased/31254-change-git-commit-command-in-existing-folder.yml b/changelogs/unreleased/31254-change-git-commit-command-in-existing-folder.yml deleted file mode 100644 index 950336ea932..00000000000 --- a/changelogs/unreleased/31254-change-git-commit-command-in-existing-folder.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Change Git commit command in Existing folder to git commit -m -merge_request: 10900 -author: TM Lee diff --git a/changelogs/unreleased/31362_decrease_cyclomatic_complexity_threshold_step1.yml b/changelogs/unreleased/31362_decrease_cyclomatic_complexity_threshold_step1.yml deleted file mode 100644 index fedf4de04d3..00000000000 --- a/changelogs/unreleased/31362_decrease_cyclomatic_complexity_threshold_step1.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Decrease Cyclomatic Complexity threshold to 16 -merge_request: 10928 -author: Rydkin Maxim diff --git a/changelogs/unreleased/31383-admin-remove-user-text-incorrect.yml b/changelogs/unreleased/31383-admin-remove-user-text-incorrect.yml deleted file mode 100644 index a2a2c0c42bd..00000000000 --- a/changelogs/unreleased/31383-admin-remove-user-text-incorrect.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Note Ghost user and refer to user deletion documentation -merge_request: -author: diff --git a/changelogs/unreleased/31483-ordered-task-list.yml b/changelogs/unreleased/31483-ordered-task-list.yml new file mode 100644 index 00000000000..c43915b3268 --- /dev/null +++ b/changelogs/unreleased/31483-ordered-task-list.yml @@ -0,0 +1,4 @@ +--- +title: Fix Ordered Task List Items +merge_request: 31483 +author: Jared Deckard <jared.deckard@gmail.com> diff --git a/changelogs/unreleased/31510-mask-password-field-edit.yml b/changelogs/unreleased/31510-mask-password-field-edit.yml new file mode 100644 index 00000000000..0ef37be328d --- /dev/null +++ b/changelogs/unreleased/31510-mask-password-field-edit.yml @@ -0,0 +1,4 @@ +--- +title: Update password field label while editing service settings +merge_request: 11431 +author: diff --git a/changelogs/unreleased/31544-size-of-project-from-api.yml b/changelogs/unreleased/31544-size-of-project-from-api.yml deleted file mode 100644 index a707d49aecd..00000000000 --- a/changelogs/unreleased/31544-size-of-project-from-api.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Expose project statistics on single requests via the API -merge_request: -author: diff --git a/changelogs/unreleased/31558-job-dropdown.yml b/changelogs/unreleased/31558-job-dropdown.yml deleted file mode 100644 index acd7b2addb6..00000000000 --- a/changelogs/unreleased/31558-job-dropdown.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Job dropdown of pipeline mini graph updates in realtime when its opened -merge_request: -author: diff --git a/changelogs/unreleased/31560-workhose-gitaly-from-mirror.yml b/changelogs/unreleased/31560-workhose-gitaly-from-mirror.yml deleted file mode 100644 index 02c048cb3b4..00000000000 --- a/changelogs/unreleased/31560-workhose-gitaly-from-mirror.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: rickettm Add repo parameter to gitaly:install and workhorse:install rake tasks -merge_request: 10979 -author: M. Ricketts diff --git a/changelogs/unreleased/31647-fix-snippet-content_html.yml b/changelogs/unreleased/31647-fix-snippet-content_html.yml deleted file mode 100644 index db6d45926fd..00000000000 --- a/changelogs/unreleased/31647-fix-snippet-content_html.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix caching large snippet HTML content on MySQL databases -merge_request: 11024 -author: diff --git a/changelogs/unreleased/31671-merge-request-message-contains-carriage-returns.yml b/changelogs/unreleased/31671-merge-request-message-contains-carriage-returns.yml deleted file mode 100644 index c33fa944a83..00000000000 --- a/changelogs/unreleased/31671-merge-request-message-contains-carriage-returns.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Remove carriage returns from commit messages -merge_request: 11077 -author: diff --git a/changelogs/unreleased/31689-request-access-spacing.yml b/changelogs/unreleased/31689-request-access-spacing.yml deleted file mode 100644 index 66076b44f46..00000000000 --- a/changelogs/unreleased/31689-request-access-spacing.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add default margin-top to user request table on project members page -merge_request: -author: diff --git a/changelogs/unreleased/31704-misaligned-buttons-in-wiki-pages.yml b/changelogs/unreleased/31704-misaligned-buttons-in-wiki-pages.yml deleted file mode 100644 index 46368b4510e..00000000000 --- a/changelogs/unreleased/31704-misaligned-buttons-in-wiki-pages.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix misaligned buttons in wiki pages -merge_request: 11043 -author: diff --git a/changelogs/unreleased/31760-add-tooltips-to-note-actions.yml b/changelogs/unreleased/31760-add-tooltips-to-note-actions.yml deleted file mode 100644 index 9bbf43d652e..00000000000 --- a/changelogs/unreleased/31760-add-tooltips-to-note-actions.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add tooltips to note action buttons -merge_request: -author: diff --git a/changelogs/unreleased/31810-commit-link.yml b/changelogs/unreleased/31810-commit-link.yml deleted file mode 100644 index 857c9cb95c5..00000000000 --- a/changelogs/unreleased/31810-commit-link.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Remove `#` being added on commit sha in MR widget -merge_request: -author: diff --git a/changelogs/unreleased/31886-remover-comment-load-spinner.yml b/changelogs/unreleased/31886-remover-comment-load-spinner.yml deleted file mode 100644 index 4b36538064a..00000000000 --- a/changelogs/unreleased/31886-remover-comment-load-spinner.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Remove spinner from loading comment -merge_request: -author: diff --git a/changelogs/unreleased/31998-pipelines-empty-state.yml b/changelogs/unreleased/31998-pipelines-empty-state.yml new file mode 100644 index 00000000000..78ae222255e --- /dev/null +++ b/changelogs/unreleased/31998-pipelines-empty-state.yml @@ -0,0 +1,4 @@ +--- +title: Fix Pipelines table empty state - only render empty state if we receive 0 pipelines +merge_request: +author: diff --git a/changelogs/unreleased/32086-atwho-is-still-enabled-for-personal-snippet-comments-form.yml b/changelogs/unreleased/32086-atwho-is-still-enabled-for-personal-snippet-comments-form.yml new file mode 100644 index 00000000000..0fd248e0400 --- /dev/null +++ b/changelogs/unreleased/32086-atwho-is-still-enabled-for-personal-snippet-comments-form.yml @@ -0,0 +1,4 @@ +--- +title: Disable reference prefixes in notes for Snippets +merge_request: 11278 +author: diff --git a/changelogs/unreleased/32178-prevent-merge-on-sha-change.yml b/changelogs/unreleased/32178-prevent-merge-on-sha-change.yml deleted file mode 100644 index d3208973de6..00000000000 --- a/changelogs/unreleased/32178-prevent-merge-on-sha-change.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add state to MR widget that prevent merges when branch changes after page load -merge_request: 11316 -author: diff --git a/changelogs/unreleased/32219-speed-up-yarn-install-in-ci-by-utilizing-inter-pipeline-cache.yml b/changelogs/unreleased/32219-speed-up-yarn-install-in-ci-by-utilizing-inter-pipeline-cache.yml new file mode 100644 index 00000000000..7fb3cb3a30b --- /dev/null +++ b/changelogs/unreleased/32219-speed-up-yarn-install-in-ci-by-utilizing-inter-pipeline-cache.yml @@ -0,0 +1,4 @@ +--- +title: Cache npm modules between pipelines with yarn to speed up setup-test-env +merge_request: 11343 +author: diff --git a/changelogs/unreleased/32340-correct-jobs-api-documentation b/changelogs/unreleased/32340-correct-jobs-api-documentation new file mode 100644 index 00000000000..4ada62356eb --- /dev/null +++ b/changelogs/unreleased/32340-correct-jobs-api-documentation @@ -0,0 +1,4 @@ +--- +title: "Correction to documention for manual steps on the Jobs API" +merge_request: 11411 +author: Zac Sturgess
\ No newline at end of file diff --git a/changelogs/unreleased/32395-duplicate-string-in-https-docs-gitlab-com-ce-administration-environment_variables-html.yml b/changelogs/unreleased/32395-duplicate-string-in-https-docs-gitlab-com-ce-administration-environment_variables-html.yml new file mode 100644 index 00000000000..d2be3d6cc4b --- /dev/null +++ b/changelogs/unreleased/32395-duplicate-string-in-https-docs-gitlab-com-ce-administration-environment_variables-html.yml @@ -0,0 +1,4 @@ +--- +title: Removes duplicate environment variable in documentation +merge_request: +author: diff --git a/changelogs/unreleased/32418-make-link-to-self-less-obvious.yml b/changelogs/unreleased/32418-make-link-to-self-less-obvious.yml new file mode 100644 index 00000000000..aabe87dac0f --- /dev/null +++ b/changelogs/unreleased/32418-make-link-to-self-less-obvious.yml @@ -0,0 +1,4 @@ +--- +title: Change links in issuable meta to black +merge_request: +author: diff --git a/changelogs/unreleased/32570-project-activity-tab-border.yml b/changelogs/unreleased/32570-project-activity-tab-border.yml new file mode 100644 index 00000000000..100a3e6a74d --- /dev/null +++ b/changelogs/unreleased/32570-project-activity-tab-border.yml @@ -0,0 +1,4 @@ +--- +title: Fix border-bottom for project activity tab +merge_request: +author: diff --git a/changelogs/unreleased/32598-avoid-resource-intensive-login-checks-if-password-is-not-provided-for-git-http.yml b/changelogs/unreleased/32598-avoid-resource-intensive-login-checks-if-password-is-not-provided-for-git-http.yml new file mode 100644 index 00000000000..6da7491bbda --- /dev/null +++ b/changelogs/unreleased/32598-avoid-resource-intensive-login-checks-if-password-is-not-provided-for-git-http.yml @@ -0,0 +1,4 @@ +--- +title: Avoid resource intensive login checks if password is not provided. +merge_request: 11537 +author: Horatiu Eugen Vlad diff --git a/changelogs/unreleased/32682-skipped-ci-icon.yml b/changelogs/unreleased/32682-skipped-ci-icon.yml new file mode 100644 index 00000000000..ad498b51900 --- /dev/null +++ b/changelogs/unreleased/32682-skipped-ci-icon.yml @@ -0,0 +1,4 @@ +--- +title: Adds new icon for CI skipped status +merge_request: +author: diff --git a/changelogs/unreleased/32715-fix-note-padding.yml b/changelogs/unreleased/32715-fix-note-padding.yml new file mode 100644 index 00000000000..867ed7eb171 --- /dev/null +++ b/changelogs/unreleased/32715-fix-note-padding.yml @@ -0,0 +1,4 @@ +--- +title: Make all notes use equal padding +merge_request: +author: diff --git a/changelogs/unreleased/32799-remove-no_turbolink-attribute-from-haml.yml b/changelogs/unreleased/32799-remove-no_turbolink-attribute-from-haml.yml new file mode 100644 index 00000000000..9c1c1fe77f2 --- /dev/null +++ b/changelogs/unreleased/32799-remove-no_turbolink-attribute-from-haml.yml @@ -0,0 +1,4 @@ +--- +title: Remove redundant data-turbolink attributes from links +merge_request: 11672 +author: blackst0ne diff --git a/changelogs/unreleased/32807-company-icon.yml b/changelogs/unreleased/32807-company-icon.yml new file mode 100644 index 00000000000..718108d3733 --- /dev/null +++ b/changelogs/unreleased/32807-company-icon.yml @@ -0,0 +1,4 @@ +--- +title: Use briefcase icon for company in profile page +merge_request: +author: diff --git a/changelogs/unreleased/6260-frontend-prevent-authored-votes.yml b/changelogs/unreleased/6260-frontend-prevent-authored-votes.yml deleted file mode 100644 index 82e852fa197..00000000000 --- a/changelogs/unreleased/6260-frontend-prevent-authored-votes.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: 'Frontend prevent authored votes' -merge_request: 6260 -author: Barthc diff --git a/changelogs/unreleased/add-aria-to-icon.yml b/changelogs/unreleased/add-aria-to-icon.yml deleted file mode 100644 index fd6a25784c6..00000000000 --- a/changelogs/unreleased/add-aria-to-icon.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fixes an issue preventing screen readers from reading some icons -merge_request: -author: diff --git a/changelogs/unreleased/add-tanuki-ci-status-favicons.yml b/changelogs/unreleased/add-tanuki-ci-status-favicons.yml deleted file mode 100644 index b60ad81947a..00000000000 --- a/changelogs/unreleased/add-tanuki-ci-status-favicons.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Updated CI status favicons to include the tanuki -merge_request: 10923 -author: diff --git a/changelogs/unreleased/add-unicode-trace-feature-test.yml b/changelogs/unreleased/add-unicode-trace-feature-test.yml new file mode 100644 index 00000000000..90c6a9afefc --- /dev/null +++ b/changelogs/unreleased/add-unicode-trace-feature-test.yml @@ -0,0 +1,4 @@ +--- +title: Add a feature test for Unicode trace +merge_request: 10736 +author: dosuken123 diff --git a/changelogs/unreleased/add-username-to-activity-feed.yml b/changelogs/unreleased/add-username-to-activity-feed.yml deleted file mode 100644 index f4c216a3954..00000000000 --- a/changelogs/unreleased/add-username-to-activity-feed.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add username to activity atom feed -merge_request: 10802 -author: winniehell diff --git a/changelogs/unreleased/add-vue-loader.yml b/changelogs/unreleased/add-vue-loader.yml deleted file mode 100644 index 382ef61ff21..00000000000 --- a/changelogs/unreleased/add-vue-loader.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: add support for .vue templates -merge_request: 10517 -author: diff --git a/changelogs/unreleased/add_ability_to_cancel_attaching_file_and_redesign_attaching_files_ui.yml b/changelogs/unreleased/add_ability_to_cancel_attaching_file_and_redesign_attaching_files_ui.yml new file mode 100644 index 00000000000..fcf4efa2846 --- /dev/null +++ b/changelogs/unreleased/add_ability_to_cancel_attaching_file_and_redesign_attaching_files_ui.yml @@ -0,0 +1,4 @@ +--- +title: Add an ability to cancel attaching file and redesign attaching files UI +merge_request: 9431 +author: blackst0ne diff --git a/changelogs/unreleased/add_index_on_ci_builds_user_id.yml b/changelogs/unreleased/add_index_on_ci_builds_user_id.yml deleted file mode 100644 index 655ebdb76fa..00000000000 --- a/changelogs/unreleased/add_index_on_ci_builds_user_id.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add index on ci_builds.user_id -merge_request: 10874 -author: blackst0ne diff --git a/changelogs/unreleased/add_system_note_for_editing_issuable.yml b/changelogs/unreleased/add_system_note_for_editing_issuable.yml deleted file mode 100644 index 3cbc7f91bf0..00000000000 --- a/changelogs/unreleased/add_system_note_for_editing_issuable.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add system note on description change of issue/merge request -merge_request: 10392 -author: blackst0ne diff --git a/changelogs/unreleased/always-show-latest-pipeline-in-commit-box.yml b/changelogs/unreleased/always-show-latest-pipeline-in-commit-box.yml deleted file mode 100644 index 6aa0c89f6f7..00000000000 --- a/changelogs/unreleased/always-show-latest-pipeline-in-commit-box.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Always show the latest pipeline information in the commit box -merge_request: 11038 -author: diff --git a/changelogs/unreleased/async-milestone-tabs.yml b/changelogs/unreleased/async-milestone-tabs.yml deleted file mode 100644 index c199a95610c..00000000000 --- a/changelogs/unreleased/async-milestone-tabs.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Load milestone tabs asynchronously to increase initial load performance -merge_request: -author: diff --git a/changelogs/unreleased/balsalmiq-support.yml b/changelogs/unreleased/balsalmiq-support.yml deleted file mode 100644 index 56a0b4c83fa..00000000000 --- a/changelogs/unreleased/balsalmiq-support.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Added balsamiq file viewer -merge_request: 10564 -author: diff --git a/changelogs/unreleased/bb_save_trace.yml b/changelogs/unreleased/bb_save_trace.yml deleted file mode 100644 index 6ff31f4f111..00000000000 --- a/changelogs/unreleased/bb_save_trace.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: "[BB Importer] Save the error trace and the whole raw document to debug problems - easier" -merge_request: -author: diff --git a/changelogs/unreleased/boards-done-add-tooltip.yml b/changelogs/unreleased/boards-done-add-tooltip.yml deleted file mode 100644 index 139f1efc8ee..00000000000 --- a/changelogs/unreleased/boards-done-add-tooltip.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add tooltip to header of Done board -merge_request: 10574 -author: Andy Brown diff --git a/changelogs/unreleased/branch-name-escape.yml b/changelogs/unreleased/branch-name-escape.yml deleted file mode 100644 index bf46235fd79..00000000000 --- a/changelogs/unreleased/branch-name-escape.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fixed branches dropdown rendering branch names as HTML -merge_request: -author: diff --git a/changelogs/unreleased/bvl-markup-pipeline.yml b/changelogs/unreleased/bvl-markup-pipeline.yml deleted file mode 100644 index d73bad03340..00000000000 --- a/changelogs/unreleased/bvl-markup-pipeline.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Make Asciidoc & other markup go through pipeline to prevent XSS -merge_request: -author: diff --git a/changelogs/unreleased/bvl-rename-build-events-to-job-events.yml b/changelogs/unreleased/bvl-rename-build-events-to-job-events.yml new file mode 100644 index 00000000000..2ce01a71361 --- /dev/null +++ b/changelogs/unreleased/bvl-rename-build-events-to-job-events.yml @@ -0,0 +1,4 @@ +--- +title: Rename build_events to job_events +merge_request: 11287 +author: diff --git a/changelogs/unreleased/bvl-validate-urls-in-markdown-using-uri.yml b/changelogs/unreleased/bvl-validate-urls-in-markdown-using-uri.yml deleted file mode 100644 index 03c4e531d73..00000000000 --- a/changelogs/unreleased/bvl-validate-urls-in-markdown-using-uri.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Validate URLs in markdown using URI to detect the host correctly -merge_request: -author: diff --git a/changelogs/unreleased/ce-32623-browser-tooltip-commits-branch-list.yml b/changelogs/unreleased/ce-32623-browser-tooltip-commits-branch-list.yml new file mode 100644 index 00000000000..93edafed699 --- /dev/null +++ b/changelogs/unreleased/ce-32623-browser-tooltip-commits-branch-list.yml @@ -0,0 +1,5 @@ +--- +title: Change order of commits ahead and behind on divergence graph for branch list + view +merge_request: +author: diff --git a/changelogs/unreleased/commit-limited-container-width.yml b/changelogs/unreleased/commit-limited-container-width.yml deleted file mode 100644 index 253646b13da..00000000000 --- a/changelogs/unreleased/commit-limited-container-width.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Side-by-side view in commits correcly expands full window width -merge_request: -author: diff --git a/changelogs/unreleased/counters_cache_invalidation.yml b/changelogs/unreleased/counters_cache_invalidation.yml new file mode 100644 index 00000000000..1e78765ec10 --- /dev/null +++ b/changelogs/unreleased/counters_cache_invalidation.yml @@ -0,0 +1,4 @@ +--- +title: Invalidate cache for issue and MR counters more granularly +merge_request: +author: diff --git a/changelogs/unreleased/deploy-keys-load-async.yml b/changelogs/unreleased/deploy-keys-load-async.yml deleted file mode 100644 index e90910278e8..00000000000 --- a/changelogs/unreleased/deploy-keys-load-async.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Deploy keys load are loaded async -merge_request: -author: diff --git a/changelogs/unreleased/diff-discussion-buttons-spacing.yml b/changelogs/unreleased/diff-discussion-buttons-spacing.yml deleted file mode 100644 index dc76973e55b..00000000000 --- a/changelogs/unreleased/diff-discussion-buttons-spacing.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fixed spacing of discussion submit buttons -merge_request: -author: diff --git a/changelogs/unreleased/disable-usage-ping-2.yml b/changelogs/unreleased/disable-usage-ping-2.yml deleted file mode 100644 index 4abd325f120..00000000000 --- a/changelogs/unreleased/disable-usage-ping-2.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add hostname to usage ping -merge_request: -author: diff --git a/changelogs/unreleased/disable-usage-ping.yml b/changelogs/unreleased/disable-usage-ping.yml deleted file mode 100644 index 5438eb56dba..00000000000 --- a/changelogs/unreleased/disable-usage-ping.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Allow usage ping to be disabled completely in gitlab.yml -merge_request: -author: diff --git a/changelogs/unreleased/dm-artifact-blob-viewer.yml b/changelogs/unreleased/dm-artifact-blob-viewer.yml deleted file mode 100644 index 38f5cbb73e1..00000000000 --- a/changelogs/unreleased/dm-artifact-blob-viewer.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add artifact file page that uses the blob viewer -merge_request: -author: diff --git a/changelogs/unreleased/dm-artifact-browser-header.yml b/changelogs/unreleased/dm-artifact-browser-header.yml deleted file mode 100644 index b88ab2ac7e5..00000000000 --- a/changelogs/unreleased/dm-artifact-browser-header.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add breadcrumb, build header and pipelines submenu to artifacts browser -merge_request: -author: diff --git a/changelogs/unreleased/dm-blob-download-button.yml b/changelogs/unreleased/dm-blob-download-button.yml deleted file mode 100644 index bd31137b670..00000000000 --- a/changelogs/unreleased/dm-blob-download-button.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Show Raw button as Download for binary files -merge_request: -author: diff --git a/changelogs/unreleased/dm-blob-viewers.yml b/changelogs/unreleased/dm-blob-viewers.yml deleted file mode 100644 index 5e0d41f3f29..00000000000 --- a/changelogs/unreleased/dm-blob-viewers.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Add Source/Rendered switch to blobs for SVG, Markdown, Asciidoc and other text - files that can be rendered -merge_request: -author: diff --git a/changelogs/unreleased/dm-comment-on-diff-versions.yml b/changelogs/unreleased/dm-comment-on-diff-versions.yml deleted file mode 100644 index af299713ad3..00000000000 --- a/changelogs/unreleased/dm-comment-on-diff-versions.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Allow commenting on older versions of the diff and comparisons between diff versions -merge_request: -author: diff --git a/changelogs/unreleased/dm-copy-as-gfm-without-empty-elements.yml b/changelogs/unreleased/dm-copy-as-gfm-without-empty-elements.yml new file mode 100644 index 00000000000..45a61320ff2 --- /dev/null +++ b/changelogs/unreleased/dm-copy-as-gfm-without-empty-elements.yml @@ -0,0 +1,4 @@ +--- +title: Don't copy empty elements that were not selected on purpose as GFM +merge_request: +author: diff --git a/changelogs/unreleased/dm-copy-gfm-when-parts-of-other-elements-are-selected.yml b/changelogs/unreleased/dm-copy-gfm-when-parts-of-other-elements-are-selected.yml new file mode 100644 index 00000000000..ae916c30ff8 --- /dev/null +++ b/changelogs/unreleased/dm-copy-gfm-when-parts-of-other-elements-are-selected.yml @@ -0,0 +1,4 @@ +--- +title: Copy as GFM even when parts of other elements are selected +merge_request: +author: diff --git a/changelogs/unreleased/dm-copy-mr-source-branch-as-gfm.yml b/changelogs/unreleased/dm-copy-mr-source-branch-as-gfm.yml deleted file mode 100644 index 708c82604ad..00000000000 --- a/changelogs/unreleased/dm-copy-mr-source-branch-as-gfm.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Paste a copied MR source branch name as code when pasted into a GFM form -merge_request: -author: diff --git a/changelogs/unreleased/dm-fix-position-tracer-for-hidden-lines.yml b/changelogs/unreleased/dm-fix-position-tracer-for-hidden-lines.yml deleted file mode 100644 index d9ba26a0657..00000000000 --- a/changelogs/unreleased/dm-fix-position-tracer-for-hidden-lines.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix commenting on an existing discussion on an unchanged line that is no longer - in the diff -merge_request: -author: diff --git a/changelogs/unreleased/dm-link-discussion-to-outdated-diff.yml b/changelogs/unreleased/dm-link-discussion-to-outdated-diff.yml deleted file mode 100644 index d489bada7ea..00000000000 --- a/changelogs/unreleased/dm-link-discussion-to-outdated-diff.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Link to outdated diff in older MR version from outdated diff discussion -merge_request: -author: diff --git a/changelogs/unreleased/dm-outdated-system-note.yml b/changelogs/unreleased/dm-outdated-system-note.yml new file mode 100644 index 00000000000..a1038a1051b --- /dev/null +++ b/changelogs/unreleased/dm-outdated-system-note.yml @@ -0,0 +1,4 @@ +--- +title: Add system note with link to diff comparison when MR discussion becomes outdated +merge_request: +author: diff --git a/changelogs/unreleased/dm-paste-code-inside-gfm-code.yml b/changelogs/unreleased/dm-paste-code-inside-gfm-code.yml new file mode 100644 index 00000000000..d078ca449a5 --- /dev/null +++ b/changelogs/unreleased/dm-paste-code-inside-gfm-code.yml @@ -0,0 +1,4 @@ +--- +title: Don't wrap pasted code when it's already inside code tags +merge_request: +author: diff --git a/changelogs/unreleased/dm-sidekiq-5.yml b/changelogs/unreleased/dm-sidekiq-5.yml deleted file mode 100644 index 69c94b18929..00000000000 --- a/changelogs/unreleased/dm-sidekiq-5.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Bump Sidekiq to 5.0.0 -merge_request: -author: diff --git a/changelogs/unreleased/dm-snippet-blob-viewers.yml b/changelogs/unreleased/dm-snippet-blob-viewers.yml deleted file mode 100644 index f218095f401..00000000000 --- a/changelogs/unreleased/dm-snippet-blob-viewers.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Use blob viewers for snippets -merge_request: -author: diff --git a/changelogs/unreleased/dm-snippet-download-button.yml b/changelogs/unreleased/dm-snippet-download-button.yml deleted file mode 100644 index 09ece1e7f98..00000000000 --- a/changelogs/unreleased/dm-snippet-download-button.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add download button to project snippets -merge_request: -author: diff --git a/changelogs/unreleased/dm-tree-last-commit.yml b/changelogs/unreleased/dm-tree-last-commit.yml new file mode 100644 index 00000000000..50619fd6ef2 --- /dev/null +++ b/changelogs/unreleased/dm-tree-last-commit.yml @@ -0,0 +1,4 @@ +--- +title: Show last commit for current tree on tree page +merge_request: +author: diff --git a/changelogs/unreleased/dm-video-viewer.yml b/changelogs/unreleased/dm-video-viewer.yml deleted file mode 100644 index 1c42b16e967..00000000000 --- a/changelogs/unreleased/dm-video-viewer.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Display video blobs in-line like images -merge_request: -author: diff --git a/changelogs/unreleased/dz-cleanup-add-users.yml b/changelogs/unreleased/dz-cleanup-add-users.yml deleted file mode 100644 index ba1e2d609f9..00000000000 --- a/changelogs/unreleased/dz-cleanup-add-users.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Refactor add_users method for project and group -merge_request: 10850 -author: diff --git a/changelogs/unreleased/dz-refactor-admin-group-members.yml b/changelogs/unreleased/dz-refactor-admin-group-members.yml deleted file mode 100644 index 993a6cac0df..00000000000 --- a/changelogs/unreleased/dz-refactor-admin-group-members.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Refactor Admin::GroupsController#members_update method and add some specs -merge_request: 10735 -author: diff --git a/changelogs/unreleased/dz-refactor-create-members.yml b/changelogs/unreleased/dz-refactor-create-members.yml deleted file mode 100644 index 8cff21eabb1..00000000000 --- a/changelogs/unreleased/dz-refactor-create-members.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Refactor code that creates project/group members -merge_request: 10735 -author: diff --git a/changelogs/unreleased/dz-remove-repo-version.yml b/changelogs/unreleased/dz-remove-repo-version.yml deleted file mode 100644 index f9e51a920f9..00000000000 --- a/changelogs/unreleased/dz-remove-repo-version.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Remove Repository#version method and tests -merge_request: 10734 -author: diff --git a/changelogs/unreleased/dz-rename-pipelines-settings-tab.yml b/changelogs/unreleased/dz-rename-pipelines-settings-tab.yml new file mode 100644 index 00000000000..6a1232523bb --- /dev/null +++ b/changelogs/unreleased/dz-rename-pipelines-settings-tab.yml @@ -0,0 +1,4 @@ +--- +title: Rename CI/CD Pipelines to Pipelines in the project settings +merge_request: +author: diff --git a/changelogs/unreleased/emoji-button-titles.yml b/changelogs/unreleased/emoji-button-titles.yml deleted file mode 100644 index c8e1b2c6c6b..00000000000 --- a/changelogs/unreleased/emoji-button-titles.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Added title to award emoji buttons -merge_request: -author: diff --git a/changelogs/unreleased/empty-task-list-alignment.yml b/changelogs/unreleased/empty-task-list-alignment.yml deleted file mode 100644 index ca04e1cab5a..00000000000 --- a/changelogs/unreleased/empty-task-list-alignment.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fixed alignment of empty task list items -merge_request: -author: diff --git a/changelogs/unreleased/feature-gb-manual-actions-protected-branches-permissions.yml b/changelogs/unreleased/feature-gb-manual-actions-protected-branches-permissions.yml deleted file mode 100644 index 6f8e80e7d64..00000000000 --- a/changelogs/unreleased/feature-gb-manual-actions-protected-branches-permissions.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Implement protected manual actions -merge_request: 10494 -author: diff --git a/changelogs/unreleased/feature-rss-scoped-token.yml b/changelogs/unreleased/feature-rss-scoped-token.yml new file mode 100644 index 00000000000..740d8778be2 --- /dev/null +++ b/changelogs/unreleased/feature-rss-scoped-token.yml @@ -0,0 +1,4 @@ +--- +title: Expose atom links with an RSS token instead of using the private token +merge_request: 11647 +author: Alexis Reigel diff --git a/changelogs/unreleased/fix-admin-integrations.yml b/changelogs/unreleased/fix-admin-integrations.yml deleted file mode 100644 index 7689623501f..00000000000 --- a/changelogs/unreleased/fix-admin-integrations.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix new admin integrations not taking effect on existing projects -merge_request: -author: diff --git a/changelogs/unreleased/fix-conflict-resolution-with-corrupt-repos.yml b/changelogs/unreleased/fix-conflict-resolution-with-corrupt-repos.yml deleted file mode 100644 index 19a3c56e478..00000000000 --- a/changelogs/unreleased/fix-conflict-resolution-with-corrupt-repos.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Prevent further repository corruption when resolving conflicts from a fork - where both the fork and upstream projects require housekeeping -merge_request: -author: diff --git a/changelogs/unreleased/fix-gb-exclude-manual-actions-from-cancelable-jobs.yml b/changelogs/unreleased/fix-gb-exclude-manual-actions-from-cancelable-jobs.yml new file mode 100644 index 00000000000..a16fc775b5e --- /dev/null +++ b/changelogs/unreleased/fix-gb-exclude-manual-actions-from-cancelable-jobs.yml @@ -0,0 +1,4 @@ +--- +title: Exclude manual actions when checking if pipeline can be canceled +merge_request: 11562 +author: diff --git a/changelogs/unreleased/fix-gb-fix-skipped-manual-actions.yml b/changelogs/unreleased/fix-gb-fix-skipped-manual-actions.yml deleted file mode 100644 index d8d4c668a44..00000000000 --- a/changelogs/unreleased/fix-gb-fix-skipped-manual-actions.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix skipped manual actions problem when processing the pipeline -merge_request: 11164 -author: diff --git a/changelogs/unreleased/fix-gb-hide-environment-external-url-btn-when-not-provided.yml b/changelogs/unreleased/fix-gb-hide-environment-external-url-btn-when-not-provided.yml deleted file mode 100644 index 66158e337fd..00000000000 --- a/changelogs/unreleased/fix-gb-hide-environment-external-url-btn-when-not-provided.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Hide external environment URL button on terminal page if URL is not defined -merge_request: 11029 -author: diff --git a/changelogs/unreleased/fix-link-prometheus-opening-outside-gitlab.yml b/changelogs/unreleased/fix-link-prometheus-opening-outside-gitlab.yml deleted file mode 100644 index e684a1f6684..00000000000 --- a/changelogs/unreleased/fix-link-prometheus-opening-outside-gitlab.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Removed target blank from the metrics action inside the environments list -merge_request: 10726 -author: diff --git a/changelogs/unreleased/fix-n-plus-one-project-features.yml b/changelogs/unreleased/fix-n-plus-one-project-features.yml deleted file mode 100644 index 1b19bd65224..00000000000 --- a/changelogs/unreleased/fix-n-plus-one-project-features.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Remove N+1 queries in processing MR references -merge_request: -author: diff --git a/changelogs/unreleased/fix-notify-post-receive.yml b/changelogs/unreleased/fix-notify-post-receive.yml deleted file mode 100644 index 6b68396d5c5..00000000000 --- a/changelogs/unreleased/fix-notify-post-receive.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fixed wrong method call on notify_post_receive -merge_request: -author: Luigi Leoni diff --git a/changelogs/unreleased/fix-user-profile-tabs-showing-raw-json-instead.yml b/changelogs/unreleased/fix-user-profile-tabs-showing-raw-json-instead.yml deleted file mode 100644 index 410172864e3..00000000000 --- a/changelogs/unreleased/fix-user-profile-tabs-showing-raw-json-instead.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Prevent user profile tabs to display raw json when going back and forward in - browser history -merge_request: -author: diff --git a/changelogs/unreleased/fix-web_hooks-index.yml b/changelogs/unreleased/fix-web_hooks-index.yml deleted file mode 100644 index 16f233e2e7c..00000000000 --- a/changelogs/unreleased/fix-web_hooks-index.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add index to webhooks type column -merge_request: -author: diff --git a/changelogs/unreleased/fix_build_header_line_height.yml b/changelogs/unreleased/fix_build_header_line_height.yml deleted file mode 100644 index 95b6221f8d2..00000000000 --- a/changelogs/unreleased/fix_build_header_line_height.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Change line-height on build-header so elements don't overlap -merge_request: -author: Dino Maric diff --git a/changelogs/unreleased/fix_cache_expiration_in_repository.yml b/changelogs/unreleased/fix_cache_expiration_in_repository.yml deleted file mode 100644 index 5f34f2bd040..00000000000 --- a/changelogs/unreleased/fix_cache_expiration_in_repository.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix redundant cache expiration in Repository -merge_request: 10575 -author: blackst0ne diff --git a/changelogs/unreleased/fix_emoji_parser.yml b/changelogs/unreleased/fix_emoji_parser.yml deleted file mode 100644 index 2b1fffe2457..00000000000 --- a/changelogs/unreleased/fix_emoji_parser.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix rendering emoji inside a string -merge_request: 10647 -author: blackst0ne diff --git a/changelogs/unreleased/fix_link_in_readme.yml b/changelogs/unreleased/fix_link_in_readme.yml deleted file mode 100644 index be5ceac8656..00000000000 --- a/changelogs/unreleased/fix_link_in_readme.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix dead link to GDK on the README page -merge_request: -author: Dino Maric diff --git a/changelogs/unreleased/fix_spaces_in_label_title.yml b/changelogs/unreleased/fix_spaces_in_label_title.yml deleted file mode 100644 index 51f07438edb..00000000000 --- a/changelogs/unreleased/fix_spaces_in_label_title.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Remove heading and trailing spaces from label's color and title -merge_request: 10603 -author: blackst0ne diff --git a/changelogs/unreleased/form-focus-previous-incorrect-form.yml b/changelogs/unreleased/form-focus-previous-incorrect-form.yml deleted file mode 100644 index efabb78de6b..00000000000 --- a/changelogs/unreleased/form-focus-previous-incorrect-form.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fixued preview shortcut focusing wrong preview tab -merge_request: -author: diff --git a/changelogs/unreleased/gitaly-local-branches.yml b/changelogs/unreleased/gitaly-local-branches.yml new file mode 100644 index 00000000000..adcc0fa6280 --- /dev/null +++ b/changelogs/unreleased/gitaly-local-branches.yml @@ -0,0 +1,4 @@ +--- +title: Add suport for find_local_branches GRPC from Gitaly +merge_request: 10059 +author: diff --git a/changelogs/unreleased/gl-version-backup-file.yml b/changelogs/unreleased/gl-version-backup-file.yml deleted file mode 100644 index 9b5abd58ae7..00000000000 --- a/changelogs/unreleased/gl-version-backup-file.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Refactor backup/restore docs -merge_request: -author: diff --git a/changelogs/unreleased/group-assignee-dropdown-send-group-id.yml b/changelogs/unreleased/group-assignee-dropdown-send-group-id.yml deleted file mode 100644 index 4f153f9817d..00000000000 --- a/changelogs/unreleased/group-assignee-dropdown-send-group-id.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fixed group issues assignee dropdown loading all users -merge_request: -author: diff --git a/changelogs/unreleased/hamlit-xss-fix.yml b/changelogs/unreleased/hamlit-xss-fix.yml deleted file mode 100644 index ba4713846e9..00000000000 --- a/changelogs/unreleased/hamlit-xss-fix.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix for XSS in project import view caused by Hamlit filter usage. -merge_request: -author: diff --git a/changelogs/unreleased/implement-i18n-support.yml b/changelogs/unreleased/implement-i18n-support.yml deleted file mode 100644 index d304fbecf90..00000000000 --- a/changelogs/unreleased/implement-i18n-support.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add support for i18n on Cycle Analytics page -merge_request: 10669 -author: diff --git a/changelogs/unreleased/issue-boards-no-avatar.yml b/changelogs/unreleased/issue-boards-no-avatar.yml deleted file mode 100644 index a2dd53b3f2f..00000000000 --- a/changelogs/unreleased/issue-boards-no-avatar.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fixed avatar not display on issue boards when Gravatar is disabled -merge_request: -author: diff --git a/changelogs/unreleased/issue-boards-sidebar-create-new-label-404-error.yml b/changelogs/unreleased/issue-boards-sidebar-create-new-label-404-error.yml deleted file mode 100644 index b935ef14786..00000000000 --- a/changelogs/unreleased/issue-boards-sidebar-create-new-label-404-error.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fixed create new label form in issue boards sidebar -merge_request: -author: diff --git a/changelogs/unreleased/issue-title-description-realtime.yml b/changelogs/unreleased/issue-title-description-realtime.yml deleted file mode 100644 index 003e1a4ab33..00000000000 --- a/changelogs/unreleased/issue-title-description-realtime.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add realtime descriptions to issue show pages -merge_request: -author: diff --git a/changelogs/unreleased/issue_api_change.yml b/changelogs/unreleased/issue_api_change.yml deleted file mode 100644 index 3ad2d57317c..00000000000 --- a/changelogs/unreleased/issue_api_change.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: 'Issue API change: assignee_id parameter and assignee object in a response - have been deprecated' -merge_request: -author: diff --git a/changelogs/unreleased/make_markdown_tables_thinner.yml b/changelogs/unreleased/make_markdown_tables_thinner.yml deleted file mode 100644 index d03a26bdeb3..00000000000 --- a/changelogs/unreleased/make_markdown_tables_thinner.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Make markdown tables thinner -merge_request: 10909 -author: blackst0ne diff --git a/changelogs/unreleased/metrics-graph-error-fix.yml b/changelogs/unreleased/metrics-graph-error-fix.yml deleted file mode 100644 index 2698b92e1f1..00000000000 --- a/changelogs/unreleased/metrics-graph-error-fix.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fixed Prometheus monitoring graphs not showing empty states in certain scenarios -merge_request: -author: diff --git a/changelogs/unreleased/milestone-not-showing-correctly-title.yml b/changelogs/unreleased/milestone-not-showing-correctly-title.yml deleted file mode 100644 index 7c21094d737..00000000000 --- a/changelogs/unreleased/milestone-not-showing-correctly-title.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Removed the milestone references from the milestone views -merge_request: -author: diff --git a/changelogs/unreleased/more-mr-filters.yml b/changelogs/unreleased/more-mr-filters.yml deleted file mode 100644 index 3c2114f6614..00000000000 --- a/changelogs/unreleased/more-mr-filters.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: 'API: Filter merge requests by milestone and labels' -merge_request: Robert Schilling -author: 10924 diff --git a/changelogs/unreleased/move-search-labels.yml b/changelogs/unreleased/move-search-labels.yml deleted file mode 100644 index 3a1d23d622e..00000000000 --- a/changelogs/unreleased/move-search-labels.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Move labels of search results from bottom to title -merge_request: 10705 -author: dr diff --git a/changelogs/unreleased/mr-diff-size-overflow.yml b/changelogs/unreleased/mr-diff-size-overflow.yml deleted file mode 100644 index 87449930cf2..00000000000 --- a/changelogs/unreleased/mr-diff-size-overflow.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Show sizes correctly in merge requests when diffs overflow -merge_request: -author: diff --git a/changelogs/unreleased/mrchrisw-22740-merge-api.yml b/changelogs/unreleased/mrchrisw-22740-merge-api.yml deleted file mode 100644 index e75160aec70..00000000000 --- a/changelogs/unreleased/mrchrisw-22740-merge-api.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix updating merge_when_build_succeeds via merge API endpoint -merge_request: 10873 -author: diff --git a/changelogs/unreleased/mrchrisw-catch-openssl.yml b/changelogs/unreleased/mrchrisw-catch-openssl.yml new file mode 100644 index 00000000000..a8b433fb0cd --- /dev/null +++ b/changelogs/unreleased/mrchrisw-catch-openssl.yml @@ -0,0 +1,4 @@ +--- +title: Rescue OpenSSL::SSL::SSLError in JiraService & IssueTrackerService +merge_request: +author: diff --git a/changelogs/unreleased/mrchrisw-fix-slack-notify.yml b/changelogs/unreleased/mrchrisw-fix-slack-notify.yml deleted file mode 100644 index bb45a117be6..00000000000 --- a/changelogs/unreleased/mrchrisw-fix-slack-notify.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix notify_only_default_branch check for Slack service -merge_request: -author: diff --git a/changelogs/unreleased/mrchrisw-import-shell-timeout.yml b/changelogs/unreleased/mrchrisw-import-shell-timeout.yml deleted file mode 100644 index e43409109d6..00000000000 --- a/changelogs/unreleased/mrchrisw-import-shell-timeout.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add configurable timeout for git fetch and clone operations -merge_request: 10697 -author: diff --git a/changelogs/unreleased/omnibus-gitlab-1993-check-shell-repositories-path-group-is-root.yml b/changelogs/unreleased/omnibus-gitlab-1993-check-shell-repositories-path-group-is-root.yml deleted file mode 100644 index 3b9284258cb..00000000000 --- a/changelogs/unreleased/omnibus-gitlab-1993-check-shell-repositories-path-group-is-root.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: "Make the `gitlab:gitlab_shell:check` task check that the repositories storage path are owned by the `root` group" -merge_request: -author: diff --git a/changelogs/unreleased/optimise-pipelines-json.yml b/changelogs/unreleased/optimise-pipelines-json.yml deleted file mode 100644 index 948679dcbeb..00000000000 --- a/changelogs/unreleased/optimise-pipelines-json.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Optimise pipelines.json endpoint -merge_request: -author: diff --git a/changelogs/unreleased/pipeline-schedules-callout-docs-url.yml b/changelogs/unreleased/pipeline-schedules-callout-docs-url.yml deleted file mode 100644 index b21bb162380..00000000000 --- a/changelogs/unreleased/pipeline-schedules-callout-docs-url.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Pass docsUrl to pipeline schedules callout component. -merge_request: !1126 -author: diff --git a/changelogs/unreleased/prevent-project-transfer.yml b/changelogs/unreleased/prevent-project-transfer.yml new file mode 100644 index 00000000000..a5c74676aab --- /dev/null +++ b/changelogs/unreleased/prevent-project-transfer.yml @@ -0,0 +1,4 @@ +--- +title: Prevent project transfers if a new group is not selected +merge_request: +author: diff --git a/changelogs/unreleased/preview-separate-slash-commands.yml b/changelogs/unreleased/preview-separate-slash-commands.yml deleted file mode 100644 index 6240ccc957c..00000000000 --- a/changelogs/unreleased/preview-separate-slash-commands.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Display slash commands outcome when previewing Markdown -merge_request: 10054 -author: Rares Sfirlogea diff --git a/changelogs/unreleased/prometheus-integration-test-setting-fix.yml b/changelogs/unreleased/prometheus-integration-test-setting-fix.yml deleted file mode 100644 index 45b7c2263e6..00000000000 --- a/changelogs/unreleased/prometheus-integration-test-setting-fix.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Prevent 500 errors caused by testing the Prometheus service -merge_request: 10994 -author: diff --git a/changelogs/unreleased/query-users-by-extern-uid.yml b/changelogs/unreleased/query-users-by-extern-uid.yml deleted file mode 100644 index 39d1cf8d3f3..00000000000 --- a/changelogs/unreleased/query-users-by-extern-uid.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Implement search by extern_uid in Users API -merge_request: 10509 -author: Robin Bobbitt diff --git a/changelogs/unreleased/related-branch-ci-status-icon-alignment.yml b/changelogs/unreleased/related-branch-ci-status-icon-alignment.yml deleted file mode 100644 index 198b6ce15ae..00000000000 --- a/changelogs/unreleased/related-branch-ci-status-icon-alignment.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fixed alignment of CI icon in issues related branches -merge_request: -author: diff --git a/changelogs/unreleased/remove-double-newline-for-single-attachments.yml b/changelogs/unreleased/remove-double-newline-for-single-attachments.yml deleted file mode 100644 index 98a28e1ede1..00000000000 --- a/changelogs/unreleased/remove-double-newline-for-single-attachments.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Only add newlines between multiple uploads -merge_request: 10545 -author: diff --git a/changelogs/unreleased/replace_header_mr_icon.yml b/changelogs/unreleased/replace_header_mr_icon.yml deleted file mode 100644 index 2ef6500f88a..00000000000 --- a/changelogs/unreleased/replace_header_mr_icon.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Replace header merge request icon -merge_request: 10932 -author: blackst0ne diff --git a/changelogs/unreleased/reset-new-branch-button.yml b/changelogs/unreleased/reset-new-branch-button.yml deleted file mode 100644 index 318ee46298f..00000000000 --- a/changelogs/unreleased/reset-new-branch-button.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Reset New branch button when issue state changes -merge_request: 5962 -author: winniehell diff --git a/changelogs/unreleased/right-sidebar-closed-default-mobile.yml b/changelogs/unreleased/right-sidebar-closed-default-mobile.yml deleted file mode 100644 index cf0ec418f0e..00000000000 --- a/changelogs/unreleased/right-sidebar-closed-default-mobile.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Set the issuable sidebar to remain closed for mobile devices -merge_request: -author: diff --git a/changelogs/unreleased/rs-sanitize-submodule-urls.yml b/changelogs/unreleased/rs-sanitize-submodule-urls.yml deleted file mode 100644 index 463b3695687..00000000000 --- a/changelogs/unreleased/rs-sanitize-submodule-urls.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Sanitize submodule URLs before linking to them in the file tree view -merge_request: -author: diff --git a/changelogs/unreleased/sh-bump-sidekiq-version.yml b/changelogs/unreleased/sh-bump-sidekiq-version.yml deleted file mode 100644 index 5369b78b76a..00000000000 --- a/changelogs/unreleased/sh-bump-sidekiq-version.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Upgrade Sidekiq to 4.2.10 -merge_request: -author: diff --git a/changelogs/unreleased/sh-fix-container-registry-s3-redirects.yml b/changelogs/unreleased/sh-fix-container-registry-s3-redirects.yml new file mode 100644 index 00000000000..1e783811b66 --- /dev/null +++ b/changelogs/unreleased/sh-fix-container-registry-s3-redirects.yml @@ -0,0 +1,4 @@ +--- +title: Properly handle container registry redirects to fix metadata stored on a S3 backend +merge_request: +author: diff --git a/changelogs/unreleased/sh-optimize-duplicate-routable-full-path.yml b/changelogs/unreleased/sh-optimize-duplicate-routable-full-path.yml deleted file mode 100644 index b1ef00f09b2..00000000000 --- a/changelogs/unreleased/sh-optimize-duplicate-routable-full-path.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Cache Routable#full_path in RequestStore to reduce duplicate route loads -merge_request: -author: diff --git a/changelogs/unreleased/snippets-finder-visibility.yml b/changelogs/unreleased/snippets-finder-visibility.yml deleted file mode 100644 index fde2262cc8d..00000000000 --- a/changelogs/unreleased/snippets-finder-visibility.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Refactor snippets finder & dont return internal snippets for external users -merge_request: -author: diff --git a/changelogs/unreleased/snippets_visibility.yml b/changelogs/unreleased/snippets_visibility.yml deleted file mode 100644 index 4c10c6882ab..00000000000 --- a/changelogs/unreleased/snippets_visibility.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix snippets visibility for show action - external users can not see internal snippets -merge_request: -author: diff --git a/changelogs/unreleased/spec_for_schema.yml b/changelogs/unreleased/spec_for_schema.yml deleted file mode 100644 index 7ea0b8672ce..00000000000 --- a/changelogs/unreleased/spec_for_schema.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add spec for schema.rb -merge_request: 10580 -author: blackst0ne diff --git a/changelogs/unreleased/store-retried-in-database-for-ci-builds.yml b/changelogs/unreleased/store-retried-in-database-for-ci-builds.yml deleted file mode 100644 index 9185113f51c..00000000000 --- a/changelogs/unreleased/store-retried-in-database-for-ci-builds.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Store retried in database for CI Builds -merge_request: -author: diff --git a/changelogs/unreleased/submodules-no-dotgit.yml b/changelogs/unreleased/submodules-no-dotgit.yml deleted file mode 100644 index 2ff0ee997fa..00000000000 --- a/changelogs/unreleased/submodules-no-dotgit.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: 'repository browser: handle submodule urls that don''t end with .git' -merge_request: -author: David Turner diff --git a/changelogs/unreleased/tags-sort-default.yml b/changelogs/unreleased/tags-sort-default.yml deleted file mode 100644 index 265b765d540..00000000000 --- a/changelogs/unreleased/tags-sort-default.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fixed tags sort from defaulting to empty -merge_request: -author: diff --git a/changelogs/unreleased/tc-fix-private-subgroups-shown.yml b/changelogs/unreleased/tc-fix-private-subgroups-shown.yml deleted file mode 100644 index 82e03921854..00000000000 --- a/changelogs/unreleased/tc-fix-private-subgroups-shown.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: "Do not show private groups on subgroups page if user doesn't have access to" -merge_request: -author: diff --git a/changelogs/unreleased/tc-job-page-mr-bold.yml b/changelogs/unreleased/tc-job-page-mr-bold.yml deleted file mode 100644 index 0243a259119..00000000000 --- a/changelogs/unreleased/tc-job-page-mr-bold.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Make MR link in build sidebar bold -merge_request: -author: diff --git a/changelogs/unreleased/tc-make-user-master-project-by-admin.yml b/changelogs/unreleased/tc-make-user-master-project-by-admin.yml deleted file mode 100644 index 459d6178bdd..00000000000 --- a/changelogs/unreleased/tc-make-user-master-project-by-admin.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Ensure namespace owner is Master of project upon creation -merge_request: 10910 -author: diff --git a/changelogs/unreleased/uassign_on_member_removing.yml b/changelogs/unreleased/uassign_on_member_removing.yml deleted file mode 100644 index cd60bdf5b3d..00000000000 --- a/changelogs/unreleased/uassign_on_member_removing.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Unassign all Issues and Merge Requests when member leaves a team -merge_request: -author: diff --git a/changelogs/unreleased/update-admin-health-page.yml b/changelogs/unreleased/update-admin-health-page.yml new file mode 100644 index 00000000000..51aa6682b49 --- /dev/null +++ b/changelogs/unreleased/update-admin-health-page.yml @@ -0,0 +1,5 @@ +--- +title: Added application readiness endpoints to the monitoring health check admin + view +merge_request: +author: diff --git a/changelogs/unreleased/use-hashie-forbidden_attributes.yml b/changelogs/unreleased/use-hashie-forbidden_attributes.yml deleted file mode 100644 index 4f429b03a0d..00000000000 --- a/changelogs/unreleased/use-hashie-forbidden_attributes.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add hashie-forbidden_attributes gem -merge_request: 10579 -author: Andy Brown diff --git a/changelogs/unreleased/user-activity-scroll-bar.yml b/changelogs/unreleased/user-activity-scroll-bar.yml deleted file mode 100644 index 97cccee42cb..00000000000 --- a/changelogs/unreleased/user-activity-scroll-bar.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix preemptive scroll bar on user activity calendar. -merge_request: !10636 -author: diff --git a/changelogs/unreleased/wait-for-ajax-handling-all-js-requests.yml b/changelogs/unreleased/wait-for-ajax-handling-all-js-requests.yml new file mode 100644 index 00000000000..14aebe792c2 --- /dev/null +++ b/changelogs/unreleased/wait-for-ajax-handling-all-js-requests.yml @@ -0,0 +1,4 @@ +--- +title: Use wait_for_requests for both ajax and Vue requests +merge_request: +author: diff --git a/changelogs/unreleased/winh-german-cycle-analytics.yml b/changelogs/unreleased/winh-german-cycle-analytics.yml deleted file mode 100644 index 14b2d672bd0..00000000000 --- a/changelogs/unreleased/winh-german-cycle-analytics.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add German translation for Cycle Analytics -merge_request: 11161 -author: diff --git a/changelogs/unreleased/winh-visual-token-labels.yml b/changelogs/unreleased/winh-visual-token-labels.yml deleted file mode 100644 index d4952e910b4..00000000000 --- a/changelogs/unreleased/winh-visual-token-labels.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Colorize labels in search field -merge_request: 11047 -author: diff --git a/changelogs/unreleased/zj-better-view-pipeline-schedule.yml b/changelogs/unreleased/zj-better-view-pipeline-schedule.yml deleted file mode 100644 index 6d6fa0784f2..00000000000 --- a/changelogs/unreleased/zj-better-view-pipeline-schedule.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Pipeline schedules got a new and improved UI -merge_request: 10853 -author: diff --git a/changelogs/unreleased/zj-chat-message-pretty-time.yml b/changelogs/unreleased/zj-chat-message-pretty-time.yml deleted file mode 100644 index 68bc647bab2..00000000000 --- a/changelogs/unreleased/zj-chat-message-pretty-time.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Pipeline chat notifications convert seconds to minutes and hours -merge_request: -author: diff --git a/changelogs/unreleased/zj-dockerfiles.yml b/changelogs/unreleased/zj-dockerfiles.yml deleted file mode 100644 index 40cb7dcfb76..00000000000 --- a/changelogs/unreleased/zj-dockerfiles.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Dockerfiles templates are imported from gitlab.com/gitlab-org/Dockerfile -merge_request: 10663 -author: diff --git a/changelogs/unreleased/zj-fix-pipeline-etag.yml b/changelogs/unreleased/zj-fix-pipeline-etag.yml new file mode 100644 index 00000000000..03ebef8c575 --- /dev/null +++ b/changelogs/unreleased/zj-fix-pipeline-etag.yml @@ -0,0 +1,4 @@ +--- +title: Fix issue where real time pipelines were not cached +merge_request: 11615 +author: diff --git a/changelogs/unreleased/zj-pipeline-schedule-owner.yml b/changelogs/unreleased/zj-pipeline-schedule-owner.yml new file mode 100644 index 00000000000..be704e173ab --- /dev/null +++ b/changelogs/unreleased/zj-pipeline-schedule-owner.yml @@ -0,0 +1,4 @@ +--- +title: Add foreign key for pipeline schedule owner +merge_request: 11233 +author: diff --git a/changelogs/unreleased/zj-real-time-pipelines.yml b/changelogs/unreleased/zj-real-time-pipelines.yml deleted file mode 100644 index eec22e67467..00000000000 --- a/changelogs/unreleased/zj-real-time-pipelines.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Pipeline view updates in near real time -merge_request: 10777 -author: diff --git a/changelogs/unreleased/zj-sort-env-folders.yml b/changelogs/unreleased/zj-sort-env-folders.yml new file mode 100644 index 00000000000..b3ca97aef94 --- /dev/null +++ b/changelogs/unreleased/zj-sort-env-folders.yml @@ -0,0 +1,4 @@ +--- +title: Sort folder for environments +merge_request: +author: diff --git a/config/application.rb b/config/application.rb index bf3fb7a18c1..b0533759252 100644 --- a/config/application.rb +++ b/config/application.rb @@ -65,6 +65,7 @@ module Gitlab hook import_url incoming_email_token + rss_token key otp_attempt password @@ -106,6 +107,7 @@ module Gitlab config.assets.precompile << "xterm/xterm.css" config.assets.precompile << "lib/ace.js" config.assets.precompile << "vendor/assets/fonts/*" + config.assets.precompile << "test.css" # Version of your assets, change this if you want to expire all your assets config.assets.version = '1.0' diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index 14d99c243fc..a727f7e2fa3 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -182,7 +182,7 @@ production: &base cron: "0 * * * *" # Execute scheduled triggers pipeline_schedule_worker: - cron: "0 */12 * * *" + cron: "19 * * * *" # Remove expired build artifacts expire_build_artifacts_worker: cron: "50 * * * *" diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index ea1815f500a..5a90830b5b3 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -325,7 +325,7 @@ Settings.cron_jobs['stuck_ci_jobs_worker'] ||= Settingslogic.new({}) Settings.cron_jobs['stuck_ci_jobs_worker']['cron'] ||= '0 * * * *' Settings.cron_jobs['stuck_ci_jobs_worker']['job_class'] = 'StuckCiJobsWorker' Settings.cron_jobs['pipeline_schedule_worker'] ||= Settingslogic.new({}) -Settings.cron_jobs['pipeline_schedule_worker']['cron'] ||= '0 */12 * * *' +Settings.cron_jobs['pipeline_schedule_worker']['cron'] ||= '19 * * * *' Settings.cron_jobs['pipeline_schedule_worker']['job_class'] = 'PipelineScheduleWorker' Settings.cron_jobs['expire_build_artifacts_worker'] ||= Settingslogic.new({}) Settings.cron_jobs['expire_build_artifacts_worker']['cron'] ||= '50 * * * *' diff --git a/config/initializers/rspec_profiling.rb b/config/initializers/rspec_profiling.rb index a7efd74f09e..16b9d5b15e5 100644 --- a/config/initializers/rspec_profiling.rb +++ b/config/initializers/rspec_profiling.rb @@ -32,7 +32,7 @@ end if Rails.env.test? RspecProfiling.configure do |config| - if ENV['RSPEC_PROFILING_POSTGRES_URL'] + if ENV['RSPEC_PROFILING_POSTGRES_URL'].present? RspecProfiling::Collectors::PSQL.prepend(RspecProfilingExt::PSQL) config.collector = RspecProfiling::Collectors::PSQL end diff --git a/config/routes/admin.rb b/config/routes/admin.rb index 48993420ed9..b1b6ef33a47 100644 --- a/config/routes/admin.rb +++ b/config/routes/admin.rb @@ -68,7 +68,9 @@ namespace :admin do resources :projects, only: [:index] - scope(path: 'projects/*namespace_id', as: :namespace) do + scope(path: 'projects/*namespace_id', + as: :namespace, + constraints: { namespace_id: Gitlab::Regex.namespace_route_regex }) do resources(:projects, path: '/', constraints: { id: Gitlab::Regex.project_route_regex }, diff --git a/config/routes/git_http.rb b/config/routes/git_http.rb index 42d874eeebc..cdf658c3e4a 100644 --- a/config/routes/git_http.rb +++ b/config/routes/git_http.rb @@ -1,4 +1,6 @@ -scope(path: '*namespace_id/:project_id', constraints: { format: nil }) do +scope(path: '*namespace_id/:project_id', + format: nil, + constraints: { namespace_id: Gitlab::Regex.namespace_route_regex }) do scope(constraints: { project_id: Gitlab::Regex.project_git_route_regex }, module: :projects) do # Git HTTP clients ('git clone' etc.) scope(controller: :git_http) do diff --git a/config/routes/profile.rb b/config/routes/profile.rb index 07c341999ea..3dc890e5785 100644 --- a/config/routes/profile.rb +++ b/config/routes/profile.rb @@ -5,6 +5,7 @@ resource :profile, only: [:show, :update] do put :reset_private_token put :reset_incoming_email_token + put :reset_rss_token put :update_username end diff --git a/config/routes/project.rb b/config/routes/project.rb index c786cbdee1e..9fe8372edf9 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -5,7 +5,22 @@ resources :projects, only: [:index, :new, :create] draw :git_http constraints(ProjectUrlConstrainer.new) do - scope(path: '*namespace_id', as: :namespace) do + # If the route has a wildcard segment, the segment has a regex constraint, + # the segment is potentially followed by _another_ wildcard segment, and + # the `format` option is not set to false, we need to specify that + # regex constraint _outside_ of `constraints: {}`. + # + # Otherwise, Rails will overwrite the constraint with `/.+?/`, + # which breaks some of our wildcard routes like `/blob/*id` + # and `/tree/*id` that depend on the negative lookahead inside + # `Gitlab::Regex.namespace_route_regex`, which helps the router + # determine whether a certain path segment is part of `*namespace_id`, + # `:project_id`, or `*id`. + # + # See https://github.com/rails/rails/blob/v4.2.8/actionpack/lib/action_dispatch/routing/mapper.rb#L155 + scope(path: '*namespace_id', + as: :namespace, + namespace_id: Gitlab::Regex.namespace_route_regex) do scope(path: ':project_id', constraints: { project_id: Gitlab::Regex.project_route_regex }, module: :projects, @@ -74,7 +89,6 @@ constraints(ProjectUrlConstrainer.new) do get :conflicts get :conflict_for_path get :pipelines - get :merge_check get :commit_change_content post :merge post :cancel_merge_when_pipeline_succeeds diff --git a/config/routes/user.rb b/config/routes/user.rb index b064a15e802..0f3bec9cf58 100644 --- a/config/routes/user.rb +++ b/config/routes/user.rb @@ -13,17 +13,17 @@ end constraints(UserUrlConstrainer.new) do # Get all keys of user - get ':username.keys' => 'profiles/keys#get_keys', constraints: { username: Gitlab::Regex.namespace_route_regex } + get ':username.keys' => 'profiles/keys#get_keys', constraints: { username: Gitlab::Regex.root_namespace_route_regex } scope(path: ':username', as: :user, - constraints: { username: Gitlab::Regex.namespace_route_regex }, + constraints: { username: Gitlab::Regex.root_namespace_route_regex }, controller: :users) do get '/', action: :show end end -scope(constraints: { username: Gitlab::Regex.namespace_route_regex }) do +scope(constraints: { username: Gitlab::Regex.root_namespace_route_regex }) do scope(path: 'users/:username', as: :user, controller: :users) do diff --git a/config/webpack.config.js b/config/webpack.config.js index 5d5a42512b1..42024739fe9 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -63,6 +63,7 @@ var config = { users: './users/users_bundle.js', raven: './raven/index.js', vue_merge_request_widget: './vue_merge_request_widget/index.js', + test: './test.js', }, output: { @@ -89,9 +90,9 @@ var config = { loader: 'raw-loader', }, { - test: /\.gif$/, + test: /\.(gif|png)$/, loader: 'url-loader', - query: { mimetype: 'image/gif' }, + options: { limit: 2048 }, }, { test: /\.(worker\.js|pdf|bmpr)$/, @@ -125,10 +126,8 @@ var config = { jQuery: 'jquery', }), - // use deterministic module ids in all environments - IS_PRODUCTION ? - new webpack.HashedModuleIdsPlugin() : - new webpack.NamedModulesPlugin(), + // use deterministic module ids + new webpack.NamedModulesPlugin(), // create cacheable common library bundle for all vue chunks new webpack.optimize.CommonsChunkPlugin({ @@ -151,6 +150,7 @@ var config = { 'schedule_form', 'schedules_index', 'sidebar', + 'vue_merge_request_widget', ], minChunks: function(module, count) { return module.resource && (/vue_shared/).test(module.resource); @@ -188,6 +188,7 @@ var config = { 'emojis': path.join(ROOT_PATH, 'fixtures/emojis'), 'empty_states': path.join(ROOT_PATH, 'app/views/shared/empty_states'), 'icons': path.join(ROOT_PATH, 'app/views/shared/icons'), + 'images': path.join(ROOT_PATH, 'app/assets/images'), 'vendor': path.join(ROOT_PATH, 'vendor/assets/javascripts'), 'vue$': 'vue/dist/vue.esm.js', } diff --git a/db/migrate/20170320171632_create_issue_assignees_table.rb b/db/migrate/20170320171632_create_issue_assignees_table.rb deleted file mode 100644 index 23b8da37b6d..00000000000 --- a/db/migrate/20170320171632_create_issue_assignees_table.rb +++ /dev/null @@ -1,40 +0,0 @@ -# See http://doc.gitlab.com/ce/development/migration_style_guide.html -# for more information on how to write migrations for GitLab. - -class CreateIssueAssigneesTable < ActiveRecord::Migration - include Gitlab::Database::MigrationHelpers - - INDEX_NAME = 'index_issue_assignees_on_issue_id_and_user_id' - - # Set this constant to true if this migration requires downtime. - DOWNTIME = false - - # When a migration requires downtime you **must** uncomment the following - # constant and define a short and easy to understand explanation as to why the - # migration requires downtime. - # DOWNTIME_REASON = '' - - # When using the methods "add_concurrent_index" or "add_column_with_default" - # you must disable the use of transactions as these methods can not run in an - # existing transaction. When using "add_concurrent_index" make sure that this - # method is the _only_ method called in the migration, any other changes - # should go in a separate migration. This ensures that upon failure _only_ the - # index creation fails and can be retried or reverted easily. - # - # To disable transactions uncomment the following line and remove these - # comments: - # disable_ddl_transaction! - - def up - create_table :issue_assignees do |t| - t.references :user, foreign_key: { on_delete: :cascade }, index: true, null: false - t.references :issue, foreign_key: { on_delete: :cascade }, null: false - end - - add_index :issue_assignees, [:issue_id, :user_id], unique: true, name: INDEX_NAME - end - - def down - drop_table :issue_assignees - end -end diff --git a/db/migrate/20170320173259_migrate_assignees.rb b/db/migrate/20170320173259_migrate_assignees.rb index ba8edbd7d32..23e7500a32d 100644 --- a/db/migrate/20170320173259_migrate_assignees.rb +++ b/db/migrate/20170320173259_migrate_assignees.rb @@ -37,16 +37,8 @@ class MigrateAssignees < ActiveRecord::Migration users.project("true").where(users[:id].eq(table[:assignee_id])).exists.not )) end - - execute <<-EOF - INSERT INTO issue_assignees(issue_id, user_id) - SELECT id, assignee_id FROM issues WHERE assignee_id IS NOT NULL - EOF end def down - execute <<-EOF - DELETE FROM issue_assignees - EOF end end diff --git a/db/migrate/20170511082759_rename_web_hooks_build_events_to_job_events.rb b/db/migrate/20170511082759_rename_web_hooks_build_events_to_job_events.rb new file mode 100644 index 00000000000..a2320a911b7 --- /dev/null +++ b/db/migrate/20170511082759_rename_web_hooks_build_events_to_job_events.rb @@ -0,0 +1,18 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class RenameWebHooksBuildEventsToJobEvents < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + rename_column_concurrently :web_hooks, :build_events, :job_events + end + + def down + cleanup_concurrent_column_rename :web_hooks, :job_events, :build_events + end +end diff --git a/db/migrate/20170511083824_rename_services_build_events_to_job_events.rb b/db/migrate/20170511083824_rename_services_build_events_to_job_events.rb new file mode 100644 index 00000000000..303d47078e7 --- /dev/null +++ b/db/migrate/20170511083824_rename_services_build_events_to_job_events.rb @@ -0,0 +1,18 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class RenameServicesBuildEventsToJobEvents < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + rename_column_concurrently :services, :build_events, :job_events + end + + def down + cleanup_concurrent_column_rename :services, :job_events, :build_events + end +end diff --git a/db/migrate/20170516153305_migrate_assignee_to_separate_table.rb b/db/migrate/20170516153305_migrate_assignee_to_separate_table.rb new file mode 100644 index 00000000000..eed9f00d8b2 --- /dev/null +++ b/db/migrate/20170516153305_migrate_assignee_to_separate_table.rb @@ -0,0 +1,83 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class MigrateAssigneeToSeparateTable < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + + # When a migration requires downtime you **must** uncomment the following + # constant and define a short and easy to understand explanation as to why the + # migration requires downtime. + # DOWNTIME_REASON = '' + + # When using the methods "add_concurrent_index", "remove_concurrent_index" or + # "add_column_with_default" you must disable the use of transactions + # as these methods can not run in an existing transaction. + # When using "add_concurrent_index" or "remove_concurrent_index" methods make sure + # that either of them is the _only_ method called in the migration, + # any other changes should go in a separate migration. + # This ensures that upon failure _only_ the index creation or removing fails + # and can be retried or reverted easily. + # + # To disable transactions uncomment the following line and remove these + # comments: + # disable_ddl_transaction! + + def up + drop_table(:issue_assignees) if table_exists?(:issue_assignees) + + if Gitlab::Database.mysql? + execute <<-EOF + CREATE TABLE issue_assignees AS + SELECT assignee_id AS user_id, id AS issue_id FROM issues WHERE assignee_id IS NOT NULL + EOF + else + ActiveRecord::Base.transaction do + execute('LOCK TABLE issues IN EXCLUSIVE MODE') + + execute <<-EOF + CREATE TABLE issue_assignees AS + SELECT assignee_id AS user_id, id AS issue_id FROM issues WHERE assignee_id IS NOT NULL + EOF + + execute <<-EOF + CREATE OR REPLACE FUNCTION replicate_assignee_id() + RETURNS trigger AS + $BODY$ + BEGIN + if OLD IS NOT NULL AND OLD.assignee_id IS NOT NULL THEN + DELETE FROM issue_assignees WHERE issue_id = OLD.id; + END IF; + + if NEW.assignee_id IS NOT NULL THEN + INSERT INTO issue_assignees (user_id, issue_id) VALUES (NEW.assignee_id, NEW.id); + END IF; + + RETURN NEW; + END; + $BODY$ + LANGUAGE 'plpgsql' + VOLATILE; + + CREATE TRIGGER replicate_assignee_id + BEFORE INSERT OR UPDATE OF assignee_id + ON issues + FOR EACH ROW EXECUTE PROCEDURE replicate_assignee_id(); + EOF + end + end + end + + def down + drop_table(:issue_assignees) if table_exists?(:issue_assignees) + + if Gitlab::Database.postgresql? + execute <<-EOF + DROP TRIGGER IF EXISTS replicate_assignee_id ON issues; + DROP FUNCTION IF EXISTS replicate_assignee_id(); + EOF + end + end +end diff --git a/db/migrate/20170516183131_add_indices_to_issue_assignees.rb b/db/migrate/20170516183131_add_indices_to_issue_assignees.rb new file mode 100644 index 00000000000..a1f064c6848 --- /dev/null +++ b/db/migrate/20170516183131_add_indices_to_issue_assignees.rb @@ -0,0 +1,41 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddIndicesToIssueAssignees < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + + # When a migration requires downtime you **must** uncomment the following + # constant and define a short and easy to understand explanation as to why the + # migration requires downtime. + # DOWNTIME_REASON = '' + + # When using the methods "add_concurrent_index", "remove_concurrent_index" or + # "add_column_with_default" you must disable the use of transactions + # as these methods can not run in an existing transaction. + # When using "add_concurrent_index" or "remove_concurrent_index" methods make sure + # that either of them is the _only_ method called in the migration, + # any other changes should go in a separate migration. + # This ensures that upon failure _only_ the index creation or removing fails + # and can be retried or reverted easily. + # + # To disable transactions uncomment the following line and remove these + # comments: + disable_ddl_transaction! + + def up + add_concurrent_index :issue_assignees, [:issue_id, :user_id], unique: true, name: 'index_issue_assignees_on_issue_id_and_user_id' + add_concurrent_index :issue_assignees, :user_id, name: 'index_issue_assignees_on_user_id' + add_concurrent_foreign_key :issue_assignees, :users, column: :user_id, on_delete: :cascade + add_concurrent_foreign_key :issue_assignees, :issues, column: :issue_id, on_delete: :cascade + end + + def down + remove_foreign_key :issue_assignees, column: :user_id + remove_foreign_key :issue_assignees, column: :issue_id + remove_concurrent_index :issue_assignees, [:issue_id, :user_id] if index_exists?(:issue_assignees, [:issue_id, :user_id]) + remove_concurrent_index :issue_assignees, :user_id if index_exists?(:issue_assignees, :user_id) + end +end diff --git a/db/migrate/20170521184006_add_change_position_to_notes.rb b/db/migrate/20170521184006_add_change_position_to_notes.rb new file mode 100644 index 00000000000..219ed1ade4c --- /dev/null +++ b/db/migrate/20170521184006_add_change_position_to_notes.rb @@ -0,0 +1,13 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddChangePositionToNotes < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + + def change + add_column :notes, :change_position, :text + end +end diff --git a/db/migrate/20170523091700_add_rss_token_to_users.rb b/db/migrate/20170523091700_add_rss_token_to_users.rb new file mode 100644 index 00000000000..06a85f6ac3d --- /dev/null +++ b/db/migrate/20170523091700_add_rss_token_to_users.rb @@ -0,0 +1,19 @@ +class AddRssTokenToUsers < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + add_column :users, :rss_token, :string + + add_concurrent_index :users, :rss_token + end + + def down + remove_concurrent_index :users, :rss_token if index_exists? :users, :rss_token + + remove_column :users, :rss_token + end +end diff --git a/db/post_migrate/20170425121605_migrate_trigger_schedules_to_pipeline_schedules.rb b/db/post_migrate/20170425121605_migrate_trigger_schedules_to_pipeline_schedules.rb index a44b399c4de..dae9750558f 100644 --- a/db/post_migrate/20170425121605_migrate_trigger_schedules_to_pipeline_schedules.rb +++ b/db/post_migrate/20170425121605_migrate_trigger_schedules_to_pipeline_schedules.rb @@ -4,6 +4,13 @@ class MigrateTriggerSchedulesToPipelineSchedules < ActiveRecord::Migration DOWNTIME = false def up + connection.execute <<~SQL + DELETE FROM ci_trigger_schedules WHERE NOT EXISTS + (SELECT true FROM projects + WHERE ci_trigger_schedules.project_id = projects.id + ) + SQL + connection.execute <<-SQL INSERT INTO ci_pipeline_schedules ( project_id, diff --git a/db/post_migrate/20170503004427_upate_retried_for_ci_build.rb b/db/post_migrate/20170503004427_upate_retried_for_ci_build.rb deleted file mode 100644 index 80215d662e4..00000000000 --- a/db/post_migrate/20170503004427_upate_retried_for_ci_build.rb +++ /dev/null @@ -1,29 +0,0 @@ -class UpateRetriedForCiBuild < ActiveRecord::Migration - include Gitlab::Database::MigrationHelpers - - DOWNTIME = false - - disable_ddl_transaction! - - def up - disable_statement_timeout - - latest_id = <<-SQL.strip_heredoc - SELECT MAX(ci_builds2.id) - FROM ci_builds ci_builds2 - WHERE ci_builds.commit_id=ci_builds2.commit_id - AND ci_builds.name=ci_builds2.name - SQL - - # This is slow update as it does single-row query - # This is designed to be run as idle, or a post deployment migration - is_retried = Arel.sql("((#{latest_id}) != ci_builds.id)") - - update_column_in_batches(:ci_builds, :retried, is_retried) do |table, query| - query.where(table[:retried].eq(nil)) - end - end - - def down - end -end diff --git a/db/post_migrate/20170503004427_update_retried_for_ci_build.rb b/db/post_migrate/20170503004427_update_retried_for_ci_build.rb new file mode 100644 index 00000000000..705e11ed47d --- /dev/null +++ b/db/post_migrate/20170503004427_update_retried_for_ci_build.rb @@ -0,0 +1,68 @@ +class UpdateRetriedForCiBuild < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + disable_statement_timeout + + if Gitlab::Database.mysql? + up_mysql + else + up_postgres + end + end + + def down + end + + private + + def up_mysql + # This is a trick to overcome MySQL limitation: + # Mysql2::Error: Table 'ci_builds' is specified twice, both as a target for 'UPDATE' and as a separate source for data + # However, this leads to create a temporary table from `max(ci_builds.id)` which is slow and do full database update + execute <<-SQL.strip_heredoc + UPDATE ci_builds SET retried= + (id NOT IN ( + SELECT * FROM (SELECT MAX(ci_builds.id) FROM ci_builds GROUP BY commit_id, name) AS latest_jobs + )) + WHERE retried IS NULL + SQL + end + + def up_postgres + with_temporary_partial_index do + latest_id = <<-SQL.strip_heredoc + SELECT MAX(ci_builds2.id) + FROM ci_builds ci_builds2 + WHERE ci_builds.commit_id=ci_builds2.commit_id + AND ci_builds.name=ci_builds2.name + SQL + + # This is slow update as it does single-row query + # This is designed to be run as idle, or a post deployment migration + is_retried = Arel.sql("((#{latest_id}) != ci_builds.id)") + + update_column_in_batches(:ci_builds, :retried, is_retried) do |table, query| + query.where(table[:retried].eq(nil)) + end + end + end + + def with_temporary_partial_index + if Gitlab::Database.postgresql? + unless index_exists?(:ci_builds, name: :index_for_ci_builds_retried_migration) + execute 'CREATE INDEX CONCURRENTLY index_for_ci_builds_retried_migration ON ci_builds (id) WHERE retried IS NULL;' + end + end + + yield + + if Gitlab::Database.postgresql? && index_exists?(:ci_builds, name: :index_for_ci_builds_retried_migration) + execute 'DROP INDEX CONCURRENTLY index_for_ci_builds_retried_migration' + end + end +end diff --git a/db/post_migrate/20170510101043_add_foreign_key_on_pipeline_schedule_owner.rb b/db/post_migrate/20170510101043_add_foreign_key_on_pipeline_schedule_owner.rb new file mode 100644 index 00000000000..6a870f08e89 --- /dev/null +++ b/db/post_migrate/20170510101043_add_foreign_key_on_pipeline_schedule_owner.rb @@ -0,0 +1,35 @@ +class AddForeignKeyOnPipelineScheduleOwner < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + execute <<-SQL + UPDATE ci_pipeline_schedules + SET owner_id = NULL + WHERE NOT EXISTS ( + SELECT true + FROM users + WHERE ci_pipeline_schedules.owner_id = users.id + ) + SQL + + add_concurrent_foreign_key(:ci_pipeline_schedules, :users, column: :owner_id, on_delete: on_delete) + end + + def down + remove_foreign_key(:ci_pipeline_schedules, column: :owner_id) + end + + private + + def on_delete + if Gitlab::Database.mysql? + :nullify + else + 'SET NULL' + end + end +end diff --git a/db/post_migrate/20170511100900_cleanup_rename_web_hooks_build_events_to_job_events.rb b/db/post_migrate/20170511100900_cleanup_rename_web_hooks_build_events_to_job_events.rb new file mode 100644 index 00000000000..281be90163a --- /dev/null +++ b/db/post_migrate/20170511100900_cleanup_rename_web_hooks_build_events_to_job_events.rb @@ -0,0 +1,18 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class CleanupRenameWebHooksBuildEventsToJobEvents < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + cleanup_concurrent_column_rename :web_hooks, :build_events, :job_events + end + + def down + rename_column_concurrently :web_hooks, :job_events, :build_events + end +end diff --git a/db/post_migrate/20170511101000_cleanup_rename_services_build_events_to_job_events.rb b/db/post_migrate/20170511101000_cleanup_rename_services_build_events_to_job_events.rb new file mode 100644 index 00000000000..5d26df5688f --- /dev/null +++ b/db/post_migrate/20170511101000_cleanup_rename_services_build_events_to_job_events.rb @@ -0,0 +1,18 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class CleanupRenameServicesBuildEventsToJobEvents < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + cleanup_concurrent_column_rename :services, :build_events, :job_events + end + + def down + rename_column_concurrently :services, :job_events, :build_events + end +end diff --git a/db/post_migrate/20170516165238_cleanup_trigger_for_issues.rb b/db/post_migrate/20170516165238_cleanup_trigger_for_issues.rb new file mode 100644 index 00000000000..378fe5603c3 --- /dev/null +++ b/db/post_migrate/20170516165238_cleanup_trigger_for_issues.rb @@ -0,0 +1,39 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class CleanupTriggerForIssues < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + + # When a migration requires downtime you **must** uncomment the following + # constant and define a short and easy to understand explanation as to why the + # migration requires downtime. + # DOWNTIME_REASON = '' + + # When using the methods "add_concurrent_index", "remove_concurrent_index" or + # "add_column_with_default" you must disable the use of transactions + # as these methods can not run in an existing transaction. + # When using "add_concurrent_index" or "remove_concurrent_index" methods make sure + # that either of them is the _only_ method called in the migration, + # any other changes should go in a separate migration. + # This ensures that upon failure _only_ the index creation or removing fails + # and can be retried or reverted easily. + # + # To disable transactions uncomment the following line and remove these + # comments: + disable_ddl_transaction! + + def up + if Gitlab::Database.postgresql? + execute <<-EOF + DROP TRIGGER IF EXISTS replicate_assignee_id ON issues; + DROP FUNCTION IF EXISTS replicate_assignee_id(); + EOF + end + end + + def down + end +end diff --git a/db/post_migrate/20170516181025_add_constraints_to_issue_assignees_table.rb b/db/post_migrate/20170516181025_add_constraints_to_issue_assignees_table.rb new file mode 100644 index 00000000000..6fa573c5b49 --- /dev/null +++ b/db/post_migrate/20170516181025_add_constraints_to_issue_assignees_table.rb @@ -0,0 +1,37 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddConstraintsToIssueAssigneesTable < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + + # When a migration requires downtime you **must** uncomment the following + # constant and define a short and easy to understand explanation as to why the + # migration requires downtime. + # DOWNTIME_REASON = '' + + # When using the methods "add_concurrent_index", "remove_concurrent_index" or + # "add_column_with_default" you must disable the use of transactions + # as these methods can not run in an existing transaction. + # When using "add_concurrent_index" or "remove_concurrent_index" methods make sure + # that either of them is the _only_ method called in the migration, + # any other changes should go in a separate migration. + # This ensures that upon failure _only_ the index creation or removing fails + # and can be retried or reverted easily. + # + # To disable transactions uncomment the following line and remove these + # comments: + # disable_ddl_transaction! + + def up + change_column_null :issue_assignees, :issue_id, false + change_column_null :issue_assignees, :user_id, false + end + + def down + change_column_null :issue_assignees, :issue_id, true + change_column_null :issue_assignees, :user_id, true + end +end diff --git a/db/post_migrate/20170518200835_rename_users_with_renamed_namespace.rb b/db/post_migrate/20170518200835_rename_users_with_renamed_namespace.rb new file mode 100644 index 00000000000..da0fcda87a6 --- /dev/null +++ b/db/post_migrate/20170518200835_rename_users_with_renamed_namespace.rb @@ -0,0 +1,50 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class RenameUsersWithRenamedNamespace < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + DISALLOWED_ROOT_PATHS = %w[ + abuse_reports + api + autocomplete + explore + health_check + import + invites + jwt + koding + member + notification_settings + oauth + sent_notifications + unicorn_test + uploads + users + ] + + def up + DISALLOWED_ROOT_PATHS.each do |path| + users = Arel::Table.new(:users) + namespaces = Arel::Table.new(:namespaces) + predicate = namespaces[:owner_id].eq(users[:id]) + .and(namespaces[:type].eq(nil)) + .and(users[:username].matches(path)) + update_sql = if Gitlab::Database.postgresql? + "UPDATE users SET username = namespaces.path "\ + "FROM namespaces WHERE #{predicate.to_sql}" + else + "UPDATE users INNER JOIN namespaces "\ + "ON namespaces.owner_id = users.id "\ + "SET username = namespaces.path "\ + "WHERE #{predicate.to_sql}" + end + + connection.execute(update_sql) + end + end + + def down + end +end diff --git a/db/post_migrate/20170518231126_fix_wrongly_renamed_routes.rb b/db/post_migrate/20170518231126_fix_wrongly_renamed_routes.rb new file mode 100644 index 00000000000..c78beda9d21 --- /dev/null +++ b/db/post_migrate/20170518231126_fix_wrongly_renamed_routes.rb @@ -0,0 +1,104 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class FixWronglyRenamedRoutes < ActiveRecord::Migration + include Gitlab::Database::RenameReservedPathsMigration::V1 + + DOWNTIME = false + + disable_ddl_transaction! + + DISALLOWED_ROOT_PATHS = %w[ + - + abuse_reports + api + autocomplete + explore + health_check + import + invites + jwt + koding + member + notification_settings + oauth + sent_notifications + unicorn_test + uploads + users + ] + + FIXED_PATHS = DISALLOWED_ROOT_PATHS.map { |p| "#{p}0" } + + class Route < Gitlab::Database::RenameReservedPathsMigration::V1::MigrationClasses::Route + self.table_name = 'routes' + end + + def routes + @routes ||= Route.arel_table + end + + def namespaces + @namespaces ||= Arel::Table.new(:namespaces) + end + + def wildcard_collection(collection) + collection.map { |word| "#{word}%" } + end + + # The routes that got incorrectly renamed before, still have a namespace that + # contains the correct path. + # This query fetches all rows from the `routes` table that meet the following + # conditions using `api` as an example: + # - route.path ILIKE `api0%` + # - route.source_type = `Namespace` + # - namespace.parent_id IS NULL + # - namespace.path ILIKE `api%` + # - NOT(namespace.path ILIKE `api0%`) + # This gives us all root-routes, that were renamed, but their namespace was not. + # + def wrongly_renamed + Route.joins("INNER JOIN namespaces ON routes.source_id = namespaces.id") + .where( + routes[:source_type].eq('Namespace') + .and(namespaces[:parent_id].eq(nil)) + ) + .where(namespaces[:path].matches_any(wildcard_collection(DISALLOWED_ROOT_PATHS))) + .where.not(namespaces[:path].matches_any(wildcard_collection(FIXED_PATHS))) + .where(routes[:path].matches_any(wildcard_collection(FIXED_PATHS))) + end + + # Using the query above, we just fetch the `route.path` & the `namespace.path` + # `route.path` is the part of the route that is now incorrect + # `namespace.path` is what it should be + # We can use `route.path` to find all the namespaces that need to be fixed + # And we can use `namespace.path` to apply the correct name. + # + def paths_and_corrections + connection.select_all( + wrongly_renamed.select(routes[:path], namespaces[:path].as('namespace_path')).to_sql + ) + end + + # This can be used to limit the `update_in_batches` call to all routes for a + # single namespace, note the `/` that's what went wrong in the initial migration. + # + def routes_in_namespace_query(namespace) + routes[:path].matches_any([namespace, "#{namespace}/%"]) + end + + def up + paths_and_corrections.each do |root_namespace| + wrong_path = root_namespace['path'] + correct_path = root_namespace['namespace_path'] + replace_statement = replace_sql(Route.arel_table[:path], wrong_path, correct_path) + + update_column_in_batches(:routes, :path, replace_statement) do |table, query| + query.where(routes_in_namespace_query(wrong_path)) + end + end + end + + def down + end +end diff --git a/db/schema.rb b/db/schema.rb index a683521c84b..ca33c8cc2a2 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20170508190732) do +ActiveRecord::Schema.define(version: 20170523091700) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -459,7 +459,7 @@ ActiveRecord::Schema.define(version: 20170508190732) do add_index "identities", ["user_id"], name: "index_identities_on_user_id", using: :btree - create_table "issue_assignees", force: :cascade do |t| + create_table "issue_assignees", id: false, force: :cascade do |t| t.integer "user_id", null: false t.integer "issue_id", null: false end @@ -794,6 +794,7 @@ ActiveRecord::Schema.define(version: 20170508190732) do t.string "discussion_id" t.text "note_html" t.integer "cached_markdown_version" + t.text "change_position" end add_index "notes", ["author_id"], name: "index_notes_on_author_id", using: :btree @@ -1125,13 +1126,13 @@ ActiveRecord::Schema.define(version: 20170508190732) do t.boolean "merge_requests_events", default: true t.boolean "tag_push_events", default: true t.boolean "note_events", default: true, null: false - t.boolean "build_events", default: false, null: false t.string "category", default: "common", null: false t.boolean "default", default: false t.boolean "wiki_page_events", default: true t.boolean "pipeline_events", default: false, null: false t.boolean "confidential_issues_events", default: true, null: false t.boolean "commit_events", default: true, null: false + t.boolean "job_events", default: false, null: false end add_index "services", ["project_id"], name: "index_services_on_project_id", using: :btree @@ -1361,6 +1362,7 @@ ActiveRecord::Schema.define(version: 20170508190732) do t.date "last_activity_on" t.boolean "notified_of_own_activity" t.string "preferred_language" + t.string "rss_token" end add_index "users", ["admin"], name: "index_users_on_admin", using: :btree @@ -1374,6 +1376,7 @@ ActiveRecord::Schema.define(version: 20170508190732) do add_index "users", ["name"], name: "index_users_on_name", using: :btree add_index "users", ["name"], name: "index_users_on_name_trigram", using: :gin, opclasses: {"name"=>"gin_trgm_ops"} add_index "users", ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true, using: :btree + add_index "users", ["rss_token"], name: "index_users_on_rss_token", using: :btree add_index "users", ["state"], name: "index_users_on_state", using: :btree add_index "users", ["username"], name: "index_users_on_username", using: :btree add_index "users", ["username"], name: "index_users_on_username_trigram", using: :gin, opclasses: {"username"=>"gin_trgm_ops"} @@ -1401,11 +1404,11 @@ ActiveRecord::Schema.define(version: 20170508190732) do t.boolean "tag_push_events", default: false t.boolean "note_events", default: false, null: false t.boolean "enable_ssl_verification", default: true - t.boolean "build_events", default: false, null: false t.boolean "wiki_page_events", default: false, null: false t.string "token" t.boolean "pipeline_events", default: false, null: false t.boolean "confidential_issues_events", default: false, null: false + t.boolean "job_events", default: false, null: false t.boolean "repository_update_events", default: false, null: false end @@ -1416,14 +1419,15 @@ ActiveRecord::Schema.define(version: 20170508190732) do add_foreign_key "chat_teams", "namespaces", on_delete: :cascade add_foreign_key "ci_builds", "ci_pipelines", column: "auto_canceled_by_id", name: "fk_a2141b1522", on_delete: :nullify add_foreign_key "ci_pipeline_schedules", "projects", name: "fk_8ead60fcc4", on_delete: :cascade + add_foreign_key "ci_pipeline_schedules", "users", column: "owner_id", name: "fk_9ea99f58d2", on_delete: :nullify add_foreign_key "ci_pipelines", "ci_pipeline_schedules", column: "pipeline_schedule_id", name: "fk_3d34ab2e06", on_delete: :nullify add_foreign_key "ci_pipelines", "ci_pipelines", column: "auto_canceled_by_id", name: "fk_262d4c2d19", on_delete: :nullify add_foreign_key "ci_trigger_requests", "ci_triggers", column: "trigger_id", name: "fk_b8ec8b7245", on_delete: :cascade add_foreign_key "ci_triggers", "users", column: "owner_id", name: "fk_e8e10d1964", on_delete: :cascade add_foreign_key "ci_variables", "projects", name: "fk_ada5eb64b3", on_delete: :cascade add_foreign_key "container_repositories", "projects" - add_foreign_key "issue_assignees", "issues", on_delete: :cascade - add_foreign_key "issue_assignees", "users", on_delete: :cascade + add_foreign_key "issue_assignees", "issues", name: "fk_b7d881734a", on_delete: :cascade + add_foreign_key "issue_assignees", "users", name: "fk_5e0c8d9154", on_delete: :cascade add_foreign_key "issue_metrics", "issues", on_delete: :cascade add_foreign_key "label_priorities", "labels", on_delete: :cascade add_foreign_key "label_priorities", "projects", on_delete: :cascade diff --git a/doc/README.md b/doc/README.md index 7bab42bc135..9f12eed1471 100644 --- a/doc/README.md +++ b/doc/README.md @@ -42,6 +42,9 @@ Shortcuts to GitLab's most visited docs: - [Create a group](gitlab-basics/create-group.md) - [GitLab Subgroups](user/group/subgroups/index.md) - [Search through GitLab](user/search/index.md): Search for issues, merge requests, projects, groups, todos, and issues in Issue Boards. +- [Snippets](user/snippets.md): Snippets allow you to create little bits of code. +- [Wikis](user/project/wiki/index.md): Enhance your repository documentation with built-in wikis. +- [GitLab Pages](user/project/pages/index.md): Build, test, and deploy your static website with GitLab Pages. ### Repository @@ -83,14 +86,6 @@ Manage files and branches from the UI (user interface): - [Importing to GitLab](workflow/importing/README.md): Import your projects from GitHub, Bitbucket, GitLab.com, FogBugz and SVN into GitLab. - [Migrating from SVN](workflow/importing/migrating_from_svn.md): Convert a SVN repository to Git and GitLab. -## GitLab's superpowers - -Take a step ahead and dive into GitLab's advanced features. - -- [GitLab Pages](user/project/pages/index.md): Build, test, and deploy your static website with GitLab Pages. -- [Snippets](user/snippets.md): Snippets allow you to create little bits of code. -- [Wikis](user/project/wiki/index.md): Enhance your repository documentation with built-in wikis. - ### Continuous Integration, Delivery, and Deployment - [GitLab CI](ci/README.md): Explore the features and capabilities of Continuous Integration, Continuous Delivery, and Continuous Deployment with GitLab. diff --git a/doc/administration/environment_variables.md b/doc/administration/environment_variables.md index 76029b30dd8..b6676026d06 100644 --- a/doc/administration/environment_variables.md +++ b/doc/administration/environment_variables.md @@ -20,7 +20,6 @@ Variable | Type | Description `GITLAB_EMAIL_FROM` | string | The e-mail address used in the "From" field in e-mails sent by GitLab `GITLAB_EMAIL_DISPLAY_NAME` | string | The name used in the "From" field in e-mails sent by GitLab `GITLAB_EMAIL_REPLY_TO` | string | The e-mail address used in the "Reply-To" field in e-mails sent by GitLab -`GITLAB_EMAIL_REPLY_TO` | string | The e-mail address used in the "Reply-To" field in e-mails sent by GitLab `GITLAB_EMAIL_SUBJECT_SUFFIX` | string | The e-mail subject suffix used in e-mails sent by GitLab `GITLAB_UNICORN_MEMORY_MIN` | integer | The minimum memory threshold (in bytes) for the Unicorn worker killer `GITLAB_UNICORN_MEMORY_MAX` | integer | The maximum memory threshold (in bytes) for the Unicorn worker killer diff --git a/doc/administration/high_availability/README.md b/doc/administration/high_availability/README.md index d5a5aef7ec0..4d3be0ab8f6 100644 --- a/doc/administration/high_availability/README.md +++ b/doc/administration/high_availability/README.md @@ -5,6 +5,20 @@ The solution you choose will be based on the level of scalability and availability you require. The easiest solutions are scalable, but not necessarily highly available. +GitLab provides a service that is usually essential to most organizations: it +enables people to collaborate on code in a timely fashion. Any downtime should +therefore be short and planned. Luckily, GitLab provides a solid setup even on +a single server without special measures. Due to the distributed nature +of Git, developers can still commit code locally even when GitLab is not +available. However, some GitLab features such as the issue tracker and +Continuous Integration are not available when GitLab is down. + +**Keep in mind that all Highly Available solutions come with a trade-off between +cost/complexity and uptime**. The more uptime you want, the more complex the +solution. And the more complex the solution, the more work is involved in +setting up and maintaining it. High availability is not free and every HA +solution should balance the costs against the benefits. + ## Architecture There are two kinds of setups: @@ -37,6 +51,10 @@ Block Device) to keep all data in sync. DRBD requires a low latency link to remain in sync. It is not advisable to attempt to run DRBD between data centers or in different cloud availability zones. +> **Note:** GitLab recommends against choosing this HA method because of the + complexity of managing DRBD and crafting automatic failover. This is + *compatible* with GitLab, but not officially *supported*. + Components/Servers Required: 2 servers/virtual machines (one active/one passive) ![Active/Passive HA Diagram](../img/high_availability/active-passive-diagram.png) diff --git a/doc/administration/high_availability/nfs.md b/doc/administration/high_availability/nfs.md index c5125dc6d5a..d8e76d6ab94 100644 --- a/doc/administration/high_availability/nfs.md +++ b/doc/administration/high_availability/nfs.md @@ -7,6 +7,25 @@ supported natively in NFS version 4. NFSv3 also supports locking as long as Linux Kernel 2.6.5+ is used. We recommend using version 4 and do not specifically test NFSv3. +## AWS Elastic File System + +GitLab does not recommend using AWS Elastic File System (EFS). + +Customers and users have reported that AWS EFS does not perform well for GitLab's +use-case. There are several issues that can cause problems. For these reasons +GitLab does not recommend using EFS with GitLab. + +- EFS bases allowed IOPS on volume size. The larger the volume, the more IOPS + are allocated. For smaller volumes, users may experience decent performance + for a period of time due to 'Burst Credits'. Over a period of weeks to months + credits may run out and performance will bottom out. +- For larger volumes, allocated IOPS may not be the problem. Workloads where + many small files are written in a serialized manner are not well-suited for EFS. + EBS with an NFS server on top will perform much better. + +For more details on another person's experience with EFS, see +[Amazon's Elastic File System: Burst Credits](https://www.rawkode.io/2017/04/amazons-elastic-file-system-burst-credits/) + ### Recommended options When you define your NFS exports, we recommend you also add the following diff --git a/doc/api/award_emoji.md b/doc/api/award_emoji.md index 5f3adcc397a..d6924741ee4 100644 --- a/doc/api/award_emoji.md +++ b/doc/api/award_emoji.md @@ -1,4 +1,4 @@ -# Award Emoji +# Award Emoji API > [Introduced][ce-4575] in GitLab 8.9, Snippet support in 8.12 diff --git a/doc/api/boards.md b/doc/api/boards.md index 17d2be0ee16..69c47abc806 100644 --- a/doc/api/boards.md +++ b/doc/api/boards.md @@ -1,4 +1,4 @@ -# Boards +# Issue Boards API Every API call to boards must be authenticated. diff --git a/doc/api/branches.md b/doc/api/branches.md index 5717215deb6..325d0ea4ce3 100644 --- a/doc/api/branches.md +++ b/doc/api/branches.md @@ -1,4 +1,4 @@ -# Branches +# Branches API ## List repository branches diff --git a/doc/api/broadcast_messages.md b/doc/api/broadcast_messages.md index ad254e3515e..a8a248a17f4 100644 --- a/doc/api/broadcast_messages.md +++ b/doc/api/broadcast_messages.md @@ -1,4 +1,4 @@ -# Broadcast Messages +# Broadcast Messages API > **Note:** This feature was introduced in GitLab 8.12. diff --git a/doc/api/build_variables.md b/doc/api/build_variables.md index 9218902e84a..2aaf1c93705 100644 --- a/doc/api/build_variables.md +++ b/doc/api/build_variables.md @@ -1,4 +1,4 @@ -# Build Variables +# Build Variables API ## List project variables diff --git a/doc/api/ci/lint.md b/doc/api/ci/lint.md index 74def207816..6a4dca92cfe 100644 --- a/doc/api/ci/lint.md +++ b/doc/api/ci/lint.md @@ -1,4 +1,4 @@ -# Validate the .gitlab-ci.yml +# Validate the .gitlab-ci.yml (API) > [Introduced][ce-5953] in GitLab 8.12. diff --git a/doc/api/ci/runners.md b/doc/api/ci/runners.md index 16028d1f124..342c039dad8 100644 --- a/doc/api/ci/runners.md +++ b/doc/api/ci/runners.md @@ -1,4 +1,4 @@ -# Runners API +# Register and Delete Runners API API used by Runners to register and delete themselves. diff --git a/doc/api/deploy_key_multiple_projects.md b/doc/api/deploy_key_multiple_projects.md index f94dbfa4059..127f9a196de 100644 --- a/doc/api/deploy_key_multiple_projects.md +++ b/doc/api/deploy_key_multiple_projects.md @@ -1,4 +1,4 @@ -# Adding deploy keys to multiple projects +# Adding deploy keys to multiple projects via API If you want to easily add the same deploy key to multiple projects in the same group, this can be achieved quite easily with the API. diff --git a/doc/api/deploy_keys.md b/doc/api/deploy_keys.md index c3fe7f84ef2..4fa800ecb9c 100644 --- a/doc/api/deploy_keys.md +++ b/doc/api/deploy_keys.md @@ -1,4 +1,4 @@ -# Deploy Keys +# Deploy Keys API ## List all deploy keys diff --git a/doc/api/jobs.md b/doc/api/jobs.md index 404da3dc603..297115e94ac 100644 --- a/doc/api/jobs.md +++ b/doc/api/jobs.md @@ -11,7 +11,7 @@ GET /projects/:id/jobs | Attribute | Type | Required | Description | |-----------|---------|----------|---------------------| | `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | -| `scope` | string **or** array of strings | no | The scope of jobs to show, one or array of: `created`, `pending`, `running`, `failed`, `success`, `canceled`, `skipped`; showing all jobs if none provided | +| `scope` | string **or** array of strings | no | The scope of jobs to show, one or array of: `created`, `pending`, `running`, `failed`, `success`, `canceled`, `skipped`, `manual`; showing all jobs if none provided | ``` curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" 'https://gitlab.example.com/api/v4/projects/1/jobs?scope[]=pending&scope[]=running' @@ -125,7 +125,7 @@ GET /projects/:id/pipelines/:pipeline_id/jobs |---------------|--------------------------------|----------|----------------------| | `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | | `pipeline_id` | integer | yes | The ID of a pipeline | -| `scope` | string **or** array of strings | no | The scope of jobs to show, one or array of: `created`, `pending`, `running`, `failed`, `success`, `canceled`, `skipped`; showing all jobs if none provided | +| `scope` | string **or** array of strings | no | The scope of jobs to show, one or array of: `created`, `pending`, `running`, `failed`, `success`, `canceled`, `skipped`, `manual`; showing all jobs if none provided | ``` curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" 'https://gitlab.example.com/api/v4/projects/1/pipelines/6/jobs?scope[]=pending&scope[]=running' diff --git a/doc/api/milestones.md b/doc/api/milestones.md index 7640eeb8d00..a082d548499 100644 --- a/doc/api/milestones.md +++ b/doc/api/milestones.md @@ -1,4 +1,4 @@ -# Milestones +# Milestones API ## List project milestones diff --git a/doc/api/namespaces.md b/doc/api/namespaces.md index eef06d5f324..4ad6071a0ed 100644 --- a/doc/api/namespaces.md +++ b/doc/api/namespaces.md @@ -1,4 +1,4 @@ -# Namespaces +# Namespaces API Usernames and groupnames fall under a special category called namespaces. diff --git a/doc/api/notes.md b/doc/api/notes.md index b71fea5fc9f..388e6989df2 100644 --- a/doc/api/notes.md +++ b/doc/api/notes.md @@ -1,4 +1,4 @@ -# Notes +# Notes API Notes are comments on snippets, issues or merge requests. diff --git a/doc/api/pipeline_triggers.md b/doc/api/pipeline_triggers.md index d639e8a0991..9030ae32d17 100644 --- a/doc/api/pipeline_triggers.md +++ b/doc/api/pipeline_triggers.md @@ -1,4 +1,4 @@ -# Pipeline triggers +# Pipeline triggers API You can read more about [triggering pipelines through the API](../ci/triggers/README.md). diff --git a/doc/api/projects.md b/doc/api/projects.md index 673cf02705d..6b919f71792 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -1,6 +1,5 @@ # Projects API - ### Project visibility level Project in GitLab has be either private, internal or public. @@ -17,8 +16,6 @@ Constants for project visibility levels are next: * `public`: The project can be cloned without any authentication. - - ## List projects Get a list of visible projects for authenticated user. When being accessed without authentication, all public projects are returned. diff --git a/doc/api/repositories.md b/doc/api/repositories.md index 859cbd63831..bccef924375 100644 --- a/doc/api/repositories.md +++ b/doc/api/repositories.md @@ -1,4 +1,4 @@ -# Repositories +# Repositories API ## List repository tree diff --git a/doc/api/repository_files.md b/doc/api/repository_files.md index aec91abd390..0b5782a8cc4 100644 --- a/doc/api/repository_files.md +++ b/doc/api/repository_files.md @@ -1,4 +1,4 @@ -# Repository files +# Repository files API **CRUD for repository files** diff --git a/doc/api/services.md b/doc/api/services.md index 0f42c256099..49b87a4228c 100644 --- a/doc/api/services.md +++ b/doc/api/services.md @@ -1,4 +1,4 @@ -# Services +# Services API ## Asana @@ -516,7 +516,7 @@ Example response: "merge_requests_events": true, "tag_push_events": true, "note_events": true, - "build_events": true, + "job_events": true, "pipeline_events": true, "properties": { "token": "9koXpg98eAheJpvBs5tK" diff --git a/doc/api/session.md b/doc/api/session.md index 056cc32597c..7dd504b67c5 100644 --- a/doc/api/session.md +++ b/doc/api/session.md @@ -1,4 +1,4 @@ -# Session +# Session API ## Deprecation Notice diff --git a/doc/api/sidekiq_metrics.md b/doc/api/sidekiq_metrics.md index ea10a26bcd0..b9500916cf2 100644 --- a/doc/api/sidekiq_metrics.md +++ b/doc/api/sidekiq_metrics.md @@ -1,4 +1,4 @@ -# Sidekiq Metrics +# Sidekiq Metrics API >**Note:** This endpoint is only available on GitLab 8.9 and above. diff --git a/doc/api/system_hooks.md b/doc/api/system_hooks.md index bad380794c1..9750475f0a6 100644 --- a/doc/api/system_hooks.md +++ b/doc/api/system_hooks.md @@ -1,4 +1,4 @@ -# System hooks +# System hooks API All methods require administrator authorization. diff --git a/doc/api/tags.md b/doc/api/tags.md index 0f6c4e6794e..54f092d1d30 100644 --- a/doc/api/tags.md +++ b/doc/api/tags.md @@ -1,4 +1,4 @@ -# Tags +# Tags API ## List project repository tags diff --git a/doc/api/templates/gitignores.md b/doc/api/templates/gitignores.md index 3f2f4ed54e0..d3f5c88ca90 100644 --- a/doc/api/templates/gitignores.md +++ b/doc/api/templates/gitignores.md @@ -1,4 +1,4 @@ -# Gitignores +# Gitignores API ## List gitignore templates diff --git a/doc/api/templates/gitlab_ci_ymls.md b/doc/api/templates/gitlab_ci_ymls.md index 27e8973da58..bdb128fc336 100644 --- a/doc/api/templates/gitlab_ci_ymls.md +++ b/doc/api/templates/gitlab_ci_ymls.md @@ -1,4 +1,4 @@ -# GitLab CI YMLs +# GitLab CI YMLs API ## List GitLab CI YML templates diff --git a/doc/api/templates/licenses.md b/doc/api/templates/licenses.md index 33018f0c53f..8d1006e08c5 100644 --- a/doc/api/templates/licenses.md +++ b/doc/api/templates/licenses.md @@ -1,4 +1,4 @@ -# Licenses +# Licenses API ## List license templates diff --git a/doc/api/v3_to_v4.md b/doc/api/v3_to_v4.md index 8e002fe0022..9db8e0351cf 100644 --- a/doc/api/v3_to_v4.md +++ b/doc/api/v3_to_v4.md @@ -1,8 +1,10 @@ -# V3 to V4 version +# API V3 to API V4 Since GitLab 9.0, API V4 is the preferred version to be used. -V3 will remain working until at least GitLab 9.3. The V3 API documentation is still [available](https://gitlab.com/gitlab-org/gitlab-ce/blob/8-16-stable/doc/api/README.md). +API V3 will be removed in GitLab 9.5, to be released on August 22, 2017. In the +meantime, we advise you to make any necessary changes to applications that use +V3. The V3 API documentation is still [available](https://gitlab.com/gitlab-org/gitlab-ce/blob/8-16-stable/doc/api/README.md). Below are the changes made between V3 and V4. diff --git a/doc/ci/README.md b/doc/ci/README.md index c4f9a3cb573..ca7266ac68f 100644 --- a/doc/ci/README.md +++ b/doc/ci/README.md @@ -1,6 +1,6 @@ # GitLab Continuous Integration (GitLab CI) -![CI/CD pipeline graph](img/cicd_pipeline_infograph.png) +![Pipeline graph](img/cicd_pipeline_infograph.png) The benefits of Continuous Integration are huge when automation plays an integral part of your workflow. GitLab comes with built-in Continuous @@ -66,7 +66,8 @@ learn how to leverage its potential even more. submodules are involved - [Auto deploy](autodeploy/index.md) - [Use SSH keys in your build environment](ssh_keys/README.md) -- [Trigger jobs through the GitLab API](triggers/README.md) +- [Trigger pipelines through the GitLab API](triggers/README.md) +- [Trigger pipelines on a schedule](../user/project/pipelines/schedules.md) ## Review Apps @@ -85,7 +86,7 @@ You can change the default behavior of GitLab CI in your whole GitLab instance as well as in each project. - **Project specific** - - [CI/CD pipelines settings](../user/project/pipelines/settings.md) + - [Pipelines settings](../user/project/pipelines/settings.md) - [Learn how to enable or disable GitLab CI](enable_or_disable_ci.md) - **Affecting the whole GitLab instance** - [Continuous Integration admin settings](../user/admin_area/settings/continuous_integration.md) @@ -108,6 +109,7 @@ Here is an collection of tutorials and guides on setting up your CI pipeline. - [Scala](examples/test-scala-application.md) - [Phoenix](examples/test-phoenix-application.md) - [Run PHP Composer & NPM scripts then deploy them to a staging server](examples/deployment/composer-npm-deploy.md) + - [Analyze code quality with the Code Climate CLI](examples/code_climate.md) - **Blog posts** - [Automated Debian packaging](https://about.gitlab.com/2016/10/12/automated-debian-package-build-with-gitlab-ci/) - [Spring boot application with GitLab CI and Kubernetes](https://about.gitlab.com/2016/12/14/continuous-delivery-of-a-spring-boot-application-with-gitlab-ci-and-kubernetes/) diff --git a/doc/ci/api/README.md b/doc/ci/api/README.md index 4ca8d92d7cc..98f37935427 100644 --- a/doc/ci/api/README.md +++ b/doc/ci/api/README.md @@ -1,3 +1 @@ -# GitLab CI API - This document was moved to a [new location](../../api/ci/README.md). diff --git a/doc/ci/api/builds.md b/doc/ci/api/builds.md index f5bd3181c02..0563a367609 100644 --- a/doc/ci/api/builds.md +++ b/doc/ci/api/builds.md @@ -1,3 +1 @@ -# Builds API - This document was moved to a [new location](../../api/ci/builds.md). diff --git a/doc/ci/api/runners.md b/doc/ci/api/runners.md index b14ea99db76..1027363851c 100644 --- a/doc/ci/api/runners.md +++ b/doc/ci/api/runners.md @@ -1,3 +1 @@ -# Runners API - This document was moved to a [new location](../../api/ci/runners.md). diff --git a/doc/ci/docker/using_docker_build.md b/doc/ci/docker/using_docker_build.md index ffa0831290a..408d46a756c 100644 --- a/doc/ci/docker/using_docker_build.md +++ b/doc/ci/docker/using_docker_build.md @@ -37,7 +37,7 @@ GitLab Runner then executes job scripts as the `gitlab-runner` user. ```bash sudo gitlab-ci-multi-runner register -n \ - --url https://gitlab.com/ci \ + --url https://gitlab.com/ \ --registration-token REGISTRATION_TOKEN \ --executor shell \ --description "My Runner" @@ -94,7 +94,7 @@ In order to do that, follow the steps: ```bash sudo gitlab-ci-multi-runner register -n \ - --url https://gitlab.com/ci \ + --url https://gitlab.com/ \ --registration-token REGISTRATION_TOKEN \ --executor docker \ --description "My Docker Runner" \ @@ -112,7 +112,7 @@ In order to do that, follow the steps: ``` [[runners]] - url = "https://gitlab.com/ci" + url = "https://gitlab.com/" token = TOKEN executor = "docker" [runners.docker] @@ -179,7 +179,7 @@ In order to do that, follow the steps: ```bash sudo gitlab-ci-multi-runner register -n \ - --url https://gitlab.com/ci \ + --url https://gitlab.com/ \ --registration-token REGISTRATION_TOKEN \ --executor docker \ --description "My Docker Runner" \ @@ -197,7 +197,7 @@ In order to do that, follow the steps: ``` [[runners]] - url = "https://gitlab.com/ci" + url = "https://gitlab.com/" token = REGISTRATION_TOKEN executor = "docker" [runners.docker] diff --git a/doc/ci/environments.md b/doc/ci/environments.md index bab765d1e12..169e0fbae3d 100644 --- a/doc/ci/environments.md +++ b/doc/ci/environments.md @@ -591,6 +591,38 @@ exist, you should see something like: ![Environment groups](img/environments_dynamic_groups.png) +## Monitoring environments + +>**Notes:** +> +- For the monitor dashboard to appear, you need to: + - Have enabled the [Kubernetes integration][kube] + - Have your app deployed on Kubernetes + - Have enabled the [Prometheus integration][prom] +- With GitLab 9.2, all deployments to an environment are shown directly on the + monitoring dashboard + +If your application is deployed on Kubernetes and you have enabled Prometheus +collecting metrics, you can monitor the performance behavior of your app +through the environments. + +Once configured, GitLab will attempt to retrieve performance metrics for any +environment which has had a successful deployment. If monitoring data was +successfully retrieved, a Monitoring button will appear on the environment's +detail page. + +![Environment Detail with Metrics](img/prometheus_environment_detail_with_metrics.png) + +Clicking on the Monitoring button will display a new page, showing up to the last +8 hours of performance data. It may take a minute or two for data to appear +after initial deployment. + +All deployments to an environment are shown directly on the monitoring dashboard +which allows easy correlation between any changes in performance and a new +version of the app, all without leaving GitLab. + +![Monitoring dashboard](img/environments_monitoring.png) + ## Checkout deployments locally Since 8.13, a reference in the git repository is saved for each deployment, so @@ -632,3 +664,5 @@ Below are some links you may find interesting: [gitlab-flow]: ../workflow/gitlab_flow.md [gitlab runner]: https://docs.gitlab.com/runner/ [git-strategy]: yaml/README.md#git-strategy +[kube]: ../user/project/integrations/kubernetes.md +[prom]: ../user/project/integrations/prometheus.md diff --git a/doc/ci/examples/README.md b/doc/ci/examples/README.md index 33c27b39a8a..2458cb959ab 100644 --- a/doc/ci/examples/README.md +++ b/doc/ci/examples/README.md @@ -55,6 +55,7 @@ Apart from those, here is an collection of tutorials and guides on setting up yo - [Using `dpl` as deployment tool](deployment/README.md) - [Repositories with examples for various languages](https://gitlab.com/groups/gitlab-examples) - [The .gitlab-ci.yml file for GitLab itself](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/.gitlab-ci.yml) +- [Analyze code quality with the Code Climate CLI](code_climate.md) - **Articles:** - [Continuous Deployment with GitLab: how to build and deploy a Debian Package with GitLab CI](https://about.gitlab.com/2016/10/12/automated-debian-package-build-with-gitlab-ci/) diff --git a/doc/ci/examples/code_climate.md b/doc/ci/examples/code_climate.md new file mode 100644 index 00000000000..bd53f80ce14 --- /dev/null +++ b/doc/ci/examples/code_climate.md @@ -0,0 +1,28 @@ +# Analyze project code quality with Code Climate CLI + +This example shows how to run [Code Climate CLI][cli] on your code by using\ +GitLab CI and Docker. + +First, you need GitLab Runner with [docker-in-docker executor](../docker/using_docker_build.md#use-docker-in-docker-executor). + +Once you setup the Runner add new job to `.gitlab-ci.yml`: + +```yaml +codeclimate: + image: docker:latest + variables: + DOCKER_DRIVER: overlay + services: + - docker:dind + script: + - docker pull codeclimate/codeclimate + - docker run --env CODECLIMATE_CODE="$PWD" --volume "$PWD":/code --volume /var/run/docker.sock:/var/run/docker.sock --volume /tmp/cc:/tmp/cc codeclimate/codeclimate init + - docker run --env CODECLIMATE_CODE="$PWD" --volume "$PWD":/code --volume /var/run/docker.sock:/var/run/docker.sock --volume /tmp/cc:/tmp/cc codeclimate/codeclimate analyze -f json > codeclimate.json + artifacts: + paths: [codeclimate.json] +``` + +This will create a `codeclimate` job in your CI pipeline and will allow you to +download and analyze the report artifact in JSON format. + +[cli]: https://github.com/codeclimate/codeclimate diff --git a/doc/ci/examples/deployment/README.md b/doc/ci/examples/deployment/README.md index 7b0995597c4..e80e246c5dd 100644 --- a/doc/ci/examples/deployment/README.md +++ b/doc/ci/examples/deployment/README.md @@ -111,7 +111,7 @@ We also use two secure variables: ## Storing API keys Secure Variables can added by going to your project's -**Settings ➔ CI/CD Pipelines ➔ Secret variables**. The variables that are defined +**Settings ➔ Pipelines ➔ Secret variables**. The variables that are defined in the project settings are sent along with the build script to the Runner. The secure variables are stored out of the repository. Never store secrets in your project's `.gitlab-ci.yml`. It is also important that the secret's value diff --git a/doc/ci/examples/test-and-deploy-python-application-to-heroku.md b/doc/ci/examples/test-and-deploy-python-application-to-heroku.md index e4d3970deac..73aebaf6d7f 100644 --- a/doc/ci/examples/test-and-deploy-python-application-to-heroku.md +++ b/doc/ci/examples/test-and-deploy-python-application-to-heroku.md @@ -55,11 +55,11 @@ You can do this through the [Dashboard](https://dashboard.heroku.com/). ### Create runner First install [Docker Engine](https://docs.docker.com/installation/). To build this project you also need to have [GitLab Runner](https://about.gitlab.com/gitlab-ci/#gitlab-runner). -You can use public runners available on `gitlab.com/ci`, but you can register your own: +You can use public runners available on `gitlab.com`, but you can register your own: ``` gitlab-ci-multi-runner register \ --non-interactive \ - --url "https://gitlab.com/ci/" \ + --url "https://gitlab.com/" \ --registration-token "PROJECT_REGISTRATION_TOKEN" \ --description "python-3.5" \ --executor "docker" \ diff --git a/doc/ci/examples/test-and-deploy-ruby-application-to-heroku.md b/doc/ci/examples/test-and-deploy-ruby-application-to-heroku.md index 42f15a27f12..6fa64a67e82 100644 --- a/doc/ci/examples/test-and-deploy-ruby-application-to-heroku.md +++ b/doc/ci/examples/test-and-deploy-ruby-application-to-heroku.md @@ -50,11 +50,11 @@ You can do this through the [Dashboard](https://dashboard.heroku.com/). ### Create runner First install [Docker Engine](https://docs.docker.com/installation/). To build this project you also need to have [GitLab Runner](https://about.gitlab.com/gitlab-ci/#gitlab-runner). -You can use public runners available on `gitlab.com/ci`, but you can register your own: +You can use public runners available on `gitlab.com`, but you can register your own: ``` gitlab-ci-multi-runner register \ --non-interactive \ - --url "https://gitlab.com/ci/" \ + --url "https://gitlab.com/" \ --registration-token "PROJECT_REGISTRATION_TOKEN" \ --description "ruby-2.2" \ --executor "docker" \ diff --git a/doc/ci/examples/test-scala-application.md b/doc/ci/examples/test-scala-application.md index 01c13941c21..09d83c33f95 100644 --- a/doc/ci/examples/test-scala-application.md +++ b/doc/ci/examples/test-scala-application.md @@ -54,7 +54,7 @@ You can use other versions of Scala and SBT by defining them in ## Display test coverage in job Add the `Coverage was \[\d+.\d+\%\]` regular expression in the -**Settings ➔ CI/CD Pipelines ➔ Coverage report** project setting to +**Settings ➔ Pipelines ➔ Coverage report** project setting to retrieve the [test coverage] rate from the build trace and have it displayed with your jobs. diff --git a/doc/ci/img/environments_monitoring.png b/doc/ci/img/environments_monitoring.png Binary files differnew file mode 100644 index 00000000000..387b6c54b61 --- /dev/null +++ b/doc/ci/img/environments_monitoring.png diff --git a/doc/ci/img/pipeline_schedules_list.png b/doc/ci/img/pipeline_schedules_list.png Binary files differdeleted file mode 100644 index 9388fac98eb..00000000000 --- a/doc/ci/img/pipeline_schedules_list.png +++ /dev/null diff --git a/doc/user/project/integrations/img/prometheus_environment_detail_with_metrics.png b/doc/ci/img/prometheus_environment_detail_with_metrics.png Binary files differindex 214b10624a9..214b10624a9 100644 --- a/doc/user/project/integrations/img/prometheus_environment_detail_with_metrics.png +++ b/doc/ci/img/prometheus_environment_detail_with_metrics.png diff --git a/doc/ci/pipeline_schedules.md b/doc/ci/pipeline_schedules.md deleted file mode 100644 index 0a9b0e7173f..00000000000 --- a/doc/ci/pipeline_schedules.md +++ /dev/null @@ -1,40 +0,0 @@ -# Pipeline Schedules - -> **Note**: -- This feature was introduced in 9.1 as [Trigger Schedule][ce-105533] -- In 9.2, the feature was [renamed to Pipeline Schedule][ce-10853] - -Pipeline schedules can be used to run pipelines only once, or for example every -month on the 22nd for a certain branch. - -## Using Pipeline Schedules - -In order to schedule pipelines, navigate to your their pages **Pipelines ➔ Schedules** -and click the **New Schedule** button. - -![New Schedule Form](img/pipeline_schedules_new_form.png) - -After entering the form, hit **Save Schedule** for the changes to have effect. -You can check a next execution date of the scheduled trigger, which is automatically calculated by a server. - -## Taking ownership - -![Schedules list](img/pipeline_schedules_list.png) - -Pipelines are executed as a user, which owns a schedule. This influences what -projects and other resources the pipeline has access to. If a user does not own -a pipeline, you can take ownership by clicking the **Take ownership** button. -The next time a pipeline is scheduled, your credentials will be used. - -> **Notes**: -- Those pipelines won't be executed precicely. Because schedules are handled by -Sidekiq, which runs according to its interval. For exmaple, if you set a schedule to -create a pipeline every minute (`* * * * *`) and the Sidekiq worker performs 00:00 -and 12:00 o'clock every day (`0 */12 * * *`), only 2 pipelines will be created per day. -To change the Sidekiq worker's frequency, you have to edit the `trigger_schedule_worker_cron` -value in your `gitlab.rb` and restart GitLab. The Sidekiq worker's configuration -on GiLab.com is able to be looked up at [here](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/config/gitlab.yml.example#L185). -- Cron notation is parsed by [Rufus-Scheduler](https://github.com/jmettraux/rufus-scheduler). - -[ce-10533]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10533 -[ce-10853]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10853 diff --git a/doc/ci/triggers/README.md b/doc/ci/triggers/README.md index 1251313cd14..cb646827fb4 100644 --- a/doc/ci/triggers/README.md +++ b/doc/ci/triggers/README.md @@ -1,6 +1,6 @@ -# Triggering jobs through the API +# Triggering pipelines through the API -> **Note**: +> **Notes**: - [Introduced][ci-229] in GitLab CE 7.14. - GitLab 8.12 has a completely redesigned job permissions system. Read all about the [new model and its implications](../../user/project/new_ci_build_permissions_model.md#job-triggers). @@ -12,7 +12,7 @@ with an API call. ## Add a trigger You can add a new trigger by going to your project's -**Settings ➔ CI/CD Pipelines ➔ Triggers**. The **Add trigger** button will +**Settings ➔ Pipelines ➔ Triggers**. The **Add trigger** button will create a new token which you can then use to trigger a rerun of this particular project's pipeline. @@ -60,7 +60,7 @@ POST /projects/:id/trigger/pipeline The required parameters are the trigger's `token` and the Git `ref` on which the trigger will be performed. Valid refs are the branch and the tag. The `:id` of a project can be found by [querying the API](../../api/projects.md) -or by visiting the **CI/CD Pipelines** settings page which provides +or by visiting the **Pipelines** settings page which provides self-explanatory examples. When a rerun of a pipeline is triggered, the information is exposed in GitLab's @@ -208,7 +208,7 @@ curl --request POST \ https://gitlab.example.com/api/v4/projects/9/trigger/pipeline ``` -### Using webhook to trigger job +### Using a webhook to trigger a pipeline You can add the following webhook to another project in order to trigger a job: @@ -216,4 +216,18 @@ You can add the following webhook to another project in order to trigger a job: https://gitlab.example.com/api/v4/projects/9/ref/master/trigger/pipeline?token=TOKEN&variables[UPLOAD_TO_S3]=true ``` +### Using cron to trigger nightly pipelines + +>**Note:** +The following behavior can also be achieved through GitLab's UI with +[pipeline schedules](../../user/project/pipelines/schedules.md). + +Whether you craft a script or just run cURL directly, you can trigger jobs +in conjunction with cron. The example below triggers a job on the `master` +branch of project with ID `9` every night at `00:30`: + +```bash +30 0 * * * curl --request POST --form token=TOKEN --form ref=master https://gitlab.example.com/api/v4/projects/9/trigger/pipeline +``` + [ci-229]: https://gitlab.com/gitlab-org/gitlab-ci/merge_requests/229 diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md index 9a3bbcf2853..0d4d08106f8 100644 --- a/doc/ci/variables/README.md +++ b/doc/ci/variables/README.md @@ -152,7 +152,7 @@ available in the build environment. It's the recommended method to use for storing things like passwords, secret keys and credentials. Secret variables can be added by going to your project's -**Settings ➔ CI/CD Pipelines**, then finding the section called +**Settings ➔ Pipelines**, then finding the section called **Secret Variables**. Once you set them, they will be available for all subsequent jobs. diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index 8546a99a022..da20076da52 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -166,7 +166,11 @@ which can be set in GitLab's UI. ### cache -> Introduced in GitLab Runner v0.7.0. +> +**Notes:** +- Introduced in GitLab Runner v0.7.0. +- Prior to GitLab 9.2, caches were restored after artifacts. +- From GitLab 9.2, caches are restored before artifacts. `cache` is used to specify a list of files and directories which should be cached between jobs. You can only use paths that are within the project @@ -773,6 +777,8 @@ as Review Apps. You can see a simple example using Review Apps at **Notes:** - Introduced in GitLab Runner v0.7.0 for non-Windows platforms. - Windows support was added in GitLab Runner v.1.0.0. +- Prior to GitLab 9.2, caches were restored after artifacts. +- From GitLab 9.2, caches are restored before artifacts. - Currently not all executors are supported. - Job artifacts are only collected for successful jobs by default. diff --git a/doc/development/README.md b/doc/development/README.md index 63db332b557..934c6849ff9 100644 --- a/doc/development/README.md +++ b/doc/development/README.md @@ -50,6 +50,10 @@ - [Post Deployment Migrations](post_deployment_migrations.md) - [Foreign Keys & Associations](foreign_keys.md) +## i18n + +- [Internationalization for GitLab](i18n_guide.md) + ## Compliance - [Licensing](licensing.md) for ensuring license compliance diff --git a/doc/development/fe_guide/img/testing_triangle.png b/doc/development/fe_guide/img/testing_triangle.png Binary files differnew file mode 100644 index 00000000000..7a9a848c2ee --- /dev/null +++ b/doc/development/fe_guide/img/testing_triangle.png diff --git a/doc/development/fe_guide/index.md b/doc/development/fe_guide/index.md index a08694fb66a..64bcb4a0257 100644 --- a/doc/development/fe_guide/index.md +++ b/doc/development/fe_guide/index.md @@ -45,15 +45,11 @@ should be `new-feature`. branch from `new-feature`, let's call it `new-feature-step-2` and repeat the process done before. ```shell -* master -|\ -| * new-feature -| |\ -| | * new-feature-step-1 -| |\ -| | * new-feature-step-2 -| |\ -| | * new-feature-step-3 +master +└─ new-feature + ├─ new-feature-step-1 + ├─ new-feature-step-2 + └─ new-feature-step-3 ``` **Tips** diff --git a/doc/development/fe_guide/testing.md b/doc/development/fe_guide/testing.md index a8c264bbd3c..0ef9fc61a61 100644 --- a/doc/development/fe_guide/testing.md +++ b/doc/development/fe_guide/testing.md @@ -1,11 +1,20 @@ # Frontend Testing -There are two types of tests you'll encounter while developing frontend code -at GitLab. We use Karma and Jasmine for JavaScript unit testing, and RSpec -feature tests with Capybara for integration testing. +There are two types of test suites you'll encounter while developing frontend code +at GitLab. We use Karma and Jasmine for JavaScript unit and integration testing, and RSpec +feature tests with Capybara for e2e (end-to-end) integration testing. -Feature tests need to be written for all new features. Regression tests ought -to be written for all bug fixes to prevent them from recurring in the future. +Unit and feature tests need to be written for all new features. +Most of the time, you should use rspec for your feature tests. +There are cases where the behaviour you are testing is not worth the time spent running the full application, +for example, if you are testing styling, animation or small actions that don't involve the backend, +you should write an integration test using Jasmine. + +![Testing priority triangle](img/testing_triangle.png) + +_This diagram demonstrates the relative priority of each test type we use_ + +Regression tests should be written for bug fixes to prevent them from recurring in the future. See [the Testing Standards and Style Guidelines](../testing.md) for more information on general testing practices at GitLab. @@ -13,10 +22,12 @@ for more information on general testing practices at GitLab. ## Karma test suite GitLab uses the [Karma][karma] test runner with [Jasmine][jasmine] as its test -framework for our JavaScript unit tests. For tests that rely on DOM -manipulation, we generate HTML files using RSpec suites (see `spec/javascripts/fixtures/*.rb` for examples). +framework for our JavaScript unit and integration tests. For integration tests, +we generate HTML files using RSpec (see `spec/javascripts/fixtures/*.rb` for examples). Some fixtures are still HAML templates that are translated to HTML files using the same mechanism (see `static_fixtures.rb`). -Those will be migrated over time. +Adding these static fixtures should be avoided as they are harder to keep up to date with real views. +The existing static fixtures will be migrated over time. +Please see [gitlab-org/gitlab-ce#24753](https://gitlab.com/gitlab-org/gitlab-ce/issues/24753) to track our progress. Fixtures are served during testing by the [jasmine-jquery][jasmine-jquery] plugin. JavaScript tests live in `spec/javascripts/`, matching the folder structure @@ -28,7 +39,9 @@ browser and you will not have access to certain APIs, such as [`Notification`](https://developer.mozilla.org/en-US/docs/Web/API/notification), which will have to be stubbed. -### Writing tests +### Best practice + +#### Naming unit tests When writing describe test blocks to test specific functions/methods, please use the method name as the describe block name. @@ -55,6 +68,77 @@ describe('.methodName', () => { }); }); ``` +#### Testing Promises + +When testing Promises you should always make sure that the test is asynchronous and rejections are handled. +Your Promise chain should therefore end with a call of the `done` callback and `done.fail` in case an error occurred. + +```javascript +// Good +it('tests a promise', (done) => { + promise + .then((data) => { + expect(data).toBe(asExpected); + }) + .then(done) + .catch(done.fail); +}); + +// Good +it('tests a promise rejection', (done) => { + promise + .then(done.fail) + .catch((error) => { + expect(error).toBe(expectedError); + }) + .then(done) + .catch(done.fail); +}); + +// Bad (missing done callback) +it('tests a promise', () => { + promise + .then((data) => { + expect(data).toBe(asExpected); + }) +}); + +// Bad (missing catch) +it('tests a promise', (done) => { + promise + .then((data) => { + expect(data).toBe(asExpected); + }) + .then(done) +}); + +// Bad (use done.fail in asynchronous tests) +it('tests a promise', (done) => { + promise + .then((data) => { + expect(data).toBe(asExpected); + }) + .then(done) + .catch(fail) +}); + +// Bad (missing catch) +it('tests a promise rejection', (done) => { + promise + .catch((error) => { + expect(error).toBe(expectedError); + }) + .then(done) +}); +``` + +#### Stubbing + +For unit tests, you should stub methods that are unrelated to the current unit you are testing. +If you need to use a prototype method, instantiate an instance of the class and call it there instead of mocking the instance completely. + +For integration tests, you should stub methods that will effect the stability of the test if they +execute their original behaviour. i.e. Network requests. ### Vue.js unit tests See this [section][vue-test]. diff --git a/doc/development/i18n_guide.md b/doc/development/i18n_guide.md new file mode 100644 index 00000000000..44eca68aaca --- /dev/null +++ b/doc/development/i18n_guide.md @@ -0,0 +1,239 @@ +# Internationalization for GitLab + +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10669) in GitLab 9.2. + +For working with internationalization (i18n) we use +[GNU gettext](https://www.gnu.org/software/gettext/) given it's the most used +tool for this task and we have a lot of applications that will help us to work +with it. + +## Tools + +We use a couple of gems: + +1. [`gettext_i18n_rails`](https://github.com/grosser/gettext_i18n_rails): this + gem allow us to translate content from models, views and controllers. Also + it gives us access to the following raketasks: + - `rake gettext:find`: Parses almost all the files from the + Rails application looking for content that has been marked for + translation. Finally, it updates the PO files with the new content that + it has found. + - `rake gettext:pack`: Processes the PO files and generates the + MO files that are binary and are finally used by the application. + +1. [`gettext_i18n_rails_js`](https://github.com/webhippie/gettext_i18n_rails_js): + this gem is useful to make the translations available in JavaScript. It + provides the following raketask: + - `rake gettext:po_to_json`: Reads the contents from the PO files and + generates JSON files containing all the available translations. + +1. PO editor: there are multiple applications that can help us to work with PO + files, a good option is [Poedit](https://poedit.net/download) which is + available for macOS, GNU/Linux and Windows. + +## Preparing a page for translation + +We basically have 4 types of files: + +1. Ruby files: basically Models and Controllers. +1. HAML files: these are the view files. +1. ERB files: used for email templates. +1. JavaScript files: we mostly need to work with VUE JS templates. + +### Ruby files + +If there is a method or variable that works with a raw string, for instance: + +```ruby +def hello + "Hello world!" +end +``` + +Or: + +```ruby +hello = "Hello world!" +``` + +You can easily mark that content for translation with: + +```ruby +def hello + _("Hello world!") +end +``` + +Or: + +```ruby +hello = _("Hello world!") +``` + +### HAML files + +Given the following content in HAML: + +```haml +%h1 Hello world! +``` + +You can mark that content for translation with: + +```haml +%h1= _("Hello world!") +``` + +### ERB files + +Given the following content in ERB: + +```erb +<h1>Hello world!</h1> +``` + +You can mark that content for translation with: + +```erb +<h1><%= _("Hello world!") %></h1> +``` + +### JavaScript files + +In JavaScript we added the `__()` (double underscore parenthesis) function +for translations. + +### Updating the PO files with the new content + +Now that the new content is marked for translation, we need to update the PO +files with the following command: + +```sh +bundle exec rake gettext:find +``` + +This command will update the `locale/**/gitlab.edit.po` file with the +new content that the parser has found. + +New translations will be added with their default content and will be marked +fuzzy. To use the translation, look for the `#, fuzzy` mention in `gitlab.edit.po` +and remove it. + +Translations that aren't used in the source code anymore will be marked with +`~#`; these can be removed to keep our translation files clutter-free. + +## Working with special content + +### Interpolation + +- In Ruby/HAML: + + ```ruby + _("Hello %{name}") % { name: 'Joe' } + ``` + +- In JavaScript: Not supported at this moment. + +### Plurals + +- In Ruby/HAML: + + ```ruby + n_('Apple', 'Apples', 3) => 'Apples' + ``` + + Using interpolation: + ```ruby + n_("There is a mouse.", "There are %d mice.", size) % size + ``` + +- In JavaScript: + + ```js + n__('Apple', 'Apples', 3) => 'Apples' + ``` + + Using interpolation: + + ```js + n__('Last day', 'Last %d days', 30) => 'Last 30 days' + ``` + +### Namespaces + +Sometimes you need to add some context to the text that you want to translate +(if the word occurs in a sentence and/or the word is ambiguous). + +- In Ruby/HAML: + + ```ruby + s_('OpenedNDaysAgo|Opened') + ``` + + In case the translation is not found it will return `Opened`. + +- In JavaScript: + + ```js + s__('OpenedNDaysAgo|Opened') + ``` + +### Just marking content for parsing + +Sometimes there are some dynamic translations that can't be found by the +parser when running `bundle exec rake gettext:find`. For these scenarios you can +use the [`_N` method](https://github.com/grosser/gettext_i18n_rails/blob/c09e38d481e0899ca7d3fc01786834fa8e7aab97/Readme.md#unfound-translations-with-rake-gettextfind). + +There is also and alternative method to [translate messages from validation errors](https://github.com/grosser/gettext_i18n_rails/blob/c09e38d481e0899ca7d3fc01786834fa8e7aab97/Readme.md#option-a). + +## Adding a new language + +Let's suppose you want to add translations for a new language, let's say French. + +1. The first step is to register the new language in `lib/gitlab/i18n.rb`: + + ```ruby + ... + AVAILABLE_LANGUAGES = { + ..., + 'fr' => 'Français' + }.freeze + ... + ``` + +1. Next, you need to add the language: + + ```sh + bundle exec rake gettext:add_language[fr] + ``` + + If you want to add a new language for a specific region, the command is similar, + you just need to separate the region with an underscore (`_`). For example: + + ```sh + bundle exec rake gettext:add_language[en_gb] + ``` + +1. Now that the language is added, a new directory has been created under the + path: `locale/fr/`. You can now start using your PO editor to edit the PO file + located in: `locale/fr/gitlab.edit.po`. + +1. After you're done updating the translations, you need to process the PO files + in order to generate the binary MO files and finally update the JSON files + containing the translations: + + ```sh + bundle exec rake gettext:pack + bundle exec rake gettext:po_to_json + ``` + +1. In order to see the translated content we need to change our preferred language + which can be found under the user's **Settings** (`/profile`). + +1. After checking that the changes are ok, you can proceed to commit the new files. + For example: + + ```sh + git add locale/fr/ app/assets/javascripts/locale/fr/ + git commit -m "Add French translations for Cycle Analytics page" + ``` diff --git a/doc/development/ux_guide/basics.md b/doc/development/ux_guide/basics.md index 259b214bd59..a436e9b1948 100644 --- a/doc/development/ux_guide/basics.md +++ b/doc/development/ux_guide/basics.md @@ -22,7 +22,7 @@ GitLab's main typeface used throughout the UI is **Source Sans Pro**. We support ### Monospace typeface -This is the typeface used for code blocks. GitLab uses the OS default font. +This is the typeface used for code blocks and references to commits, branches, and tags (`.commit-sha` or `.ref-name`). GitLab uses the OS default font. - **Menlo** (Mac) - **Consolas** (Windows) - **Liberation Mono** (Linux) diff --git a/doc/development/what_requires_downtime.md b/doc/development/what_requires_downtime.md index 8da6ad684f5..c4830322fa8 100644 --- a/doc/development/what_requires_downtime.md +++ b/doc/development/what_requires_downtime.md @@ -139,6 +139,8 @@ Adding or removing a NOT NULL clause (or another constraint) can typically be done without requiring downtime. However, this does require that any application changes are deployed _first_. Thus, changing the constraints of a column should happen in a post-deployment migration. +NOTE: Avoid using `change_column` as it produces inefficient query because it re-defines +the whole column type. For example, to add a NOT NULL constraint, prefer `change_column_null ` ## Changing Column Types diff --git a/doc/development/writing_documentation.md b/doc/development/writing_documentation.md index 657a826d7ee..eac9ec2a470 100644 --- a/doc/development/writing_documentation.md +++ b/doc/development/writing_documentation.md @@ -78,14 +78,21 @@ Currently GitLab docs use Redcarpet as [markdown](../user/markdown.md) engine, b We try to treat documentation as code, thus have implemented some testing. Currently, the following tests are in place: -1. `docs:check:links`: Check that all internal (relative) links work correctly -1. `docs:check:apilint`: Check that the API docs follow some conventions +1. `docs lint`: Check that all internal (relative) links work correctly and + that all cURL examples in API docs use the full switches. If your contribution contains **only** documentation changes, you can speed up -the CI process by prepending to the name of your branch: `docs/`. For example, -a valid name would be `docs/update-api-issues` and it will run only the docs -tests. If the name is `docs-update-api-issues`, the whole test suite will run -(including docs). +the CI process by following some branch naming conventions. You have three +choices: + +| Branch name | Valid example | +| ----------- | ------------- | +| Starting with `docs/` | `docs/update-api-issues` | +| Starting with `docs-` | `docs-update-api-issues` | +| Ending in `-docs` | `123-update-api-issues-docs` | + +If your branch name matches any of the above, it will run only the docs +tests. If it doesn't, the whole test suite will run (including docs). --- diff --git a/doc/install/kubernetes/gitlab_chart.md b/doc/install/kubernetes/gitlab_chart.md index 2d7edbe16e4..b4ffd57afbb 100644 --- a/doc/install/kubernetes/gitlab_chart.md +++ b/doc/install/kubernetes/gitlab_chart.md @@ -1,4 +1,7 @@ # GitLab Helm Chart +> Officially supported cloud providers are Google Container Service and Azure Container Service. + +> Officially supported schedulers are Kubernetes and Terraform. The `gitlab` Helm chart deploys GitLab into your Kubernetes cluster. @@ -14,7 +17,7 @@ This chart includes the following: ## Prerequisites -- _At least_ 3 GB of RAM available on your cluster, in chunks of 1 GB +- _At least_ 3 GB of RAM available on your cluster, in chunks of 1 GB. 41GB of storage and 2 CPU are also required. - Kubernetes 1.4+ with Beta APIs enabled - [Persistent Volume](https://kubernetes.io/docs/concepts/storage/persistent-volumes/) provisioner support in the underlying infrastructure - The ability to point a DNS entry or URL at your GitLab install @@ -203,9 +206,43 @@ its class in an annotation. >**Note:** The Ingress alone doesn't expose GitLab externally. You need to have a Ingress controller setup to do that. -Setting up an Ingress controller can be as simple as installing the `nginx-ingress` helm chart. But be sure +Setting up an Ingress controller can be done by installing the `nginx-ingress` helm chart. But be sure to read the [documentation](https://github.com/kubernetes/charts/blob/master/stable/nginx-ingress/README.md) +#### Preserving Source IPs + +If you are using the `LoadBalancer` serviceType you may run into issues where user IP addresses in the GitLab +logs, and used in abuse throttling are not accurate. This is due to how Kubernetes uses source NATing on cluster nodes without endpoints. + +See the [Kubernetes documentation](https://kubernetes.io/docs/tutorials/services/source-ip/#source-ip-for-services-with-typeloadbalancer) for more information. + +To fix this you can add the following service annotation to your `values.yaml` + +```yaml +## For minikube, set this to NodePort, elsewhere use LoadBalancer +## ref: http://kubernetes.io/docs/user-guide/services/#publishing-services---service-types +## +serviceType: LoadBalancer + +## Optional annotations for gitlab service. +serviceAnnotations: + service.beta.kubernetes.io/external-traffic: "OnlyLocal" +``` + +>**Note:** +If you are using the ingress routing, you will likely also need to specify the annotation on the service for the ingress +controller. For `nginx-ingress` you can check the +[configuration documentation](https://github.com/kubernetes/charts/blob/master/stable/nginx-ingress/README.md#configuration) +on how to add the annotation to the `controller.service.annotations` array. + +>**Note:** +When using the `nginx-ingress` controller on Google Container Engine (GKE), and using the `external-traffic` annotation, +you will need to additionally set the `controller.kind` to be DaemonSet. Otherwise only pods running on the same node +as the nginx controller will be able to reach GitLab. This may result in pods within your cluster not being able to reach GitLab. +See the [Kubernetes documentation](https://kubernetes.io/docs/tutorials/services/source-ip/#source-ip-for-services-with-typeloadbalancer) and +[nginx-ingress configuration documentation](https://github.com/kubernetes/charts/blob/master/stable/nginx-ingress/README.md#configuration) +for more information. + ### External database You can configure the GitLab Helm chart to connect to an external PostgreSQL @@ -387,6 +424,7 @@ ingress: ``` ## Installing GitLab using the Helm Chart +> You may see a temporary error message `SchedulerPredicates failed due to PersistentVolumeClaim is not bound` while storage provisions. Once the storage provisions, the pods will automatically restart. This may take a couple minutes depending on your cloud provider. If the error persists, please review the [prerequisites](#prerequisites) to ensure you have enough RAM, CPU, and storage. Once you [have configured](#configuration) GitLab in your `values.yml` file, run the following: diff --git a/doc/install/kubernetes/gitlab_runner_chart.md b/doc/install/kubernetes/gitlab_runner_chart.md index dbd9ae3f70c..305b4593c73 100644 --- a/doc/install/kubernetes/gitlab_runner_chart.md +++ b/doc/install/kubernetes/gitlab_runner_chart.md @@ -1,4 +1,7 @@ # GitLab Runner Helm Chart +> Officially supported cloud providers are Google Container Service and Azure Container Service. + +> Officially supported schedulers are Kubernetes and Terraform. The `gitlab-runner` Helm chart deploys a GitLab Runner instance into your Kubernetes cluster. diff --git a/doc/install/kubernetes/index.md b/doc/install/kubernetes/index.md index db0430fc27b..88c56a1d17c 100644 --- a/doc/install/kubernetes/index.md +++ b/doc/install/kubernetes/index.md @@ -1,4 +1,7 @@ -# Installing GitLab in Kubernetes +# Installing GitLab on Kubernetes +> Officially supported cloud providers are Google Container Service and Azure Container Service. + +> Officially supported schedulers are Kubernetes and Terraform. The easiest method to deploy GitLab in [Kubernetes](https://kubernetes.io/) is to take advantage of the official GitLab Helm charts. [Helm] is a package diff --git a/doc/integration/github.md b/doc/integration/github.md index 4b0d33334bd..b0d67db8b59 100644 --- a/doc/integration/github.md +++ b/doc/integration/github.md @@ -103,12 +103,59 @@ GitHub will generate an application ID and secret key for you to use. 1. Save the configuration file. -1. [Reconfigure][] or [restart GitLab][] for the changes to take effect if you +1. [Reconfigure GitLab][] or [restart GitLab][] for the changes to take effect if you installed GitLab via Omnibus or from source respectively. On the sign in page there should now be a GitHub icon below the regular sign in form. Click the icon to begin the authentication process. GitHub will ask the user to sign in and authorize the GitLab application. If everything goes well the user will be returned to GitLab and will be signed in. -[reconfigure]: ../administration/restart_gitlab.md#omnibus-gitlab-reconfigure +### GitHub Enterprise with Self-Signed Certificate + +If you are attempting to import projects from GitHub Enterprise with a self-signed +certificate and the imports are failing, you will need to disable SSL verification. +It should be disabled by adding `verify_ssl` to `false` in the provider configuration +and changing the global Git `sslVerify` option to `false` in the GitLab server. + +For omnibus package: + +```ruby + gitlab_rails['omniauth_providers'] = [ + { + "name" => "github", + "app_id" => "YOUR_APP_ID", + "app_secret" => "YOUR_APP_SECRET", + "url" => "https://github.com/", + "verify_ssl" => false, + "args" => { "scope" => "user:email" } + } + ] +``` + +You will also need to disable Git SSL verification on the server hosting GitLab. + +```ruby +omnibus_gitconfig['system'] = { "http" => ["sslVerify = false"] } +``` + +For installation from source: + +``` + - { name: 'github', app_id: 'YOUR_APP_ID', + app_secret: 'YOUR_APP_SECRET', + url: "https://github.example.com/", + verify_ssl: false, + args: { scope: 'user:email' } } +``` + +You will also need to disable Git SSL verification on the server hosting GitLab. + +``` +$ git config --global http.sslVerify false +``` + +For the changes to take effect, [reconfigure Gitlab] if you installed +via Omnibus, or [restart GitLab] if you installed from source. + +[reconfigure GitLab]: ../administration/restart_gitlab.md#omnibus-gitlab-reconfigure [restart GitLab]: ../administration/restart_gitlab.md#installations-from-source diff --git a/doc/university/high-availability/aws/README.md b/doc/university/high-availability/aws/README.md index 088f1cd7290..6b8f3cd3d1d 100644 --- a/doc/university/high-availability/aws/README.md +++ b/doc/university/high-availability/aws/README.md @@ -159,19 +159,21 @@ subnet and security group and *** -## Elastic File System +## Network File System -This new AWS offering allows us to create a file system accessible by
-EC2 instances within a VPC. Choose our VPC and the subnets will be -
automatically configured assuming we don't need to set explicit IPs. -The
next section allows us to add tags and choose between General -Purpose or
Max I/O which is a good option when being accessed by a -large number of
EC2 instances. +GitLab requires a shared filesystem such as NFS. The file share(s) will be +mounted on all application servers. There are a variety of ways to build an +NFS server on AWS. -
![Elastic File System](img/elastic-file-system.png) +One option is to use a third-party AMI that offers NFS as a service. A [search +for 'NFS' in the AWS Marketplace](https://aws.amazon.com/marketplace/search/results?x=0&y=0&searchTerms=NFS&page=1&ref_=nav_search_box) +shows options such as NetApp, SoftNAS and others. -To actually mount and install the NFS client we'll use the User Data -section when adding our Launch Configuration. +Another option is to build a simple NFS server using a vanilla Linux server backed +by AWS Elastic Block Storage (EBS). + +> **Note:** GitLab does not recommend using AWS Elastic File System (EFS). See + details in [High Availability NFS documentation](../../../administration/high_availability/nfs.md#aws-elastic-file-system) *** diff --git a/doc/update/9.0-to-9.1.md b/doc/update/9.0-to-9.1.md index 2b582d4eefd..2d597894517 100644 --- a/doc/update/9.0-to-9.1.md +++ b/doc/update/9.0-to-9.1.md @@ -104,7 +104,6 @@ cd /home/git/gitlab-shell sudo -u git -H git fetch --all --tags sudo -u git -H git checkout v$(</home/git/gitlab/GITLAB_SHELL_VERSION) -sudo -u git -H bin/compile ``` ### 7. Update gitlab-workhorse diff --git a/doc/user/admin_area/settings/usage_statistics.md b/doc/user/admin_area/settings/usage_statistics.md index 375e7f08e8b..f3745d0efa7 100644 --- a/doc/user/admin_area/settings/usage_statistics.md +++ b/doc/user/admin_area/settings/usage_statistics.md @@ -9,7 +9,7 @@ All statistics are opt-out, you can disable them from the admin panel. GitLab can inform you when an update is available and the importance of it. -No information other than the GitLab version and the instance's domain name +No information other than the GitLab version and the instance's hostname (through the HTTP referer) are collected. In the **Overview** tab you can see if your GitLab version is up to date. There @@ -38,7 +38,7 @@ You can view the exact JSON payload in the administration panel. ### Deactivate the usage ping -By default, usage ping is opt-out. If you want to deactivate this feature, go to +The usage ping is opt-out. If you want to deactivate this feature, go to the Settings page of your administration panel and uncheck the Usage ping checkbox. diff --git a/doc/user/group/subgroups/index.md b/doc/user/group/subgroups/index.md index ce5da07c61a..151c17f3bf1 100644 --- a/doc/user/group/subgroups/index.md +++ b/doc/user/group/subgroups/index.md @@ -71,8 +71,10 @@ structure. - You need to be an Owner of a group in order to be able to create a subgroup. For more information check the [permissions table][permissions]. - For a list of words that are not allowed to be used as group names see the - [`namespace_validator.rb` file][reserved] under the `RESERVED` and - `WILDCARD_ROUTES` lists. + [`regex.rb` file][reserved] under the `TOP_LEVEL_ROUTES`, `PROJECT_WILDCARD_ROUTES` and `GROUP_ROUTES` lists: + - `TOP_LEVEL_ROUTES`: are names that are reserved as usernames or top level groups + - `PROJECT_WILDCARD_ROUTES`: are names that are reserved for child groups or projects. + - `GROUP_ROUTES`: are names that are reserved for all groups or projects. To create a subgroup: @@ -161,4 +163,4 @@ Here's a list of what you can't do with subgroups: [ce-2772]: https://gitlab.com/gitlab-org/gitlab-ce/issues/2772 [permissions]: ../../permissions.md#group -[reserved]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/app/validators/namespace_validator.rb +[reserved]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/gitlab/regex.rb diff --git a/doc/user/img/gitlab_snippet.png b/doc/user/img/gitlab_snippet.png Binary files differnew file mode 100644 index 00000000000..718347fc2d4 --- /dev/null +++ b/doc/user/img/gitlab_snippet.png diff --git a/doc/user/permissions.md b/doc/user/permissions.md index 637967510f3..b0145b0a759 100644 --- a/doc/user/permissions.md +++ b/doc/user/permissions.md @@ -174,7 +174,7 @@ users: | Push container images to other projects | | | | | [^1]: Guest users can only view the confidential issues they created themselves -[^2]: If **Public pipelines** is enabled in **Project Settings > CI/CD Pipelines** +[^2]: If **Public pipelines** is enabled in **Project Settings > Pipelines** [^3]: Not allowed for Guest, Reporter, Developer, Master, or Owner [^4]: Only if user is not external one. [^5]: Only if user is a member of the project. diff --git a/doc/user/profile/preferences.md b/doc/user/profile/preferences.md index e5038835027..f2ad42f21fd 100644 --- a/doc/user/profile/preferences.md +++ b/doc/user/profile/preferences.md @@ -50,15 +50,15 @@ You have 6 options here that you can use for your default dashboard view: - Your groups - Your [Todos] -### Default project view +### Project home page content -The default project view settings allows you to choose what content you want to -see on a project's landing page. +The project home page content setting allows you to choose what content you want to +see on a project’s home page. You can choose between 2 options: - Show the files and the readme (default) -- Show the project's activity +- Show the project’s activity [rouge]: http://rouge.jneen.net/ "Rouge website" [todos]: ../../workflow/todos.md diff --git a/doc/user/project/integrations/img/merge_request_performance.png b/doc/user/project/integrations/img/merge_request_performance.png Binary files differnew file mode 100644 index 00000000000..93b2626fed7 --- /dev/null +++ b/doc/user/project/integrations/img/merge_request_performance.png diff --git a/doc/user/project/integrations/prometheus.md b/doc/user/project/integrations/prometheus.md index b71d6981d1e..d3fb5916dc6 100644 --- a/doc/user/project/integrations/prometheus.md +++ b/doc/user/project/integrations/prometheus.md @@ -17,6 +17,7 @@ the settings page with a default template. To configure the template, see the Integration with Prometheus requires the following: 1. GitLab 9.0 or higher +1. The [Kubernetes integration must be enabled][kube] on your project 1. Your app must be deployed on [Kubernetes][] 1. Prometheus must be configured to collect Kubernetes metrics 1. Each metric must be have a label to indicate the environment @@ -159,23 +160,28 @@ The queries utilized by GitLab are shown in the following table. ## Monitoring CI/CD Environments Once configured, GitLab will attempt to retrieve performance metrics for any -environment which has had a successful deployment. If monitoring data was -successfully retrieved, a Monitoring button will appear on the environment's -detail page. +environment which has had a successful deployment. -![Environment Detail with Metrics](img/prometheus_environment_detail_with_metrics.png) +[Learn more about monitoring environments.](../../../ci/environments.md#monitoring-environments) -Clicking on the Monitoring button will display a new page, showing up to the last -8 hours of performance data. It may take a minute or two for data to appear -after initial deployment. +## Determining the performance impact of a merge -## Determining performance impact of a merge +> [Introduced][ce-10408] in GitLab 9.2. -> [Introduced][ce-10408] in GitLab 9.1. +Developers can view the performance impact of their changes within the merge +request workflow. When a source branch has been deployed to an environment, a +sparkline will appear showing the average memory consumption of the app. The dot +indicates when the current changes were deployed, with up to 30 minutes of +performance data displayed before and after. The sparkline will be updated after +each commit has been deployed. -After a merge request has been approved, a sparkline will appear on the merge request page displaying the average memory usage of the application. The sparkline includes thirty minutes of data prior to the merge, a dot to indicate the merge itself, and then will begin capturing thirty minutes of data after the merge. +Once merged and the target branch has been redeployed, the sparkline will switch +to show the new environments this revision has been deployed to. -This sparkline serves as a quick indicator of the impact on memory consumption of the recently merged changes. If there is a problem, action can then be taken to troubleshoot or revert the merge. +Performance data will be available for the duration it is persisted on the +Prometheus server. + +![Merge Request with Performance Impact](img/merge_request_performance.png) ## Troubleshooting @@ -189,6 +195,7 @@ If the "Attempting to load performance data" screen continues to appear, it coul [autodeploy]: ../../../ci/autodeploy/index.md [kubernetes]: https://kubernetes.io +[kube]: ./kubernetes.md [prometheus-k8s-sd]: https://prometheus.io/docs/operating/configuration/#<kubernetes_sd_config> [prometheus]: https://prometheus.io [gitlab-prometheus-k8s-monitor]: ../../../administration/monitoring/prometheus/index.md#configuring-prometheus-to-monitor-kubernetes diff --git a/doc/user/project/issues/img/create_new_merge_request.png b/doc/user/project/issues/img/create_new_merge_request.png Binary files differdeleted file mode 100644 index d4bfb6fa463..00000000000 --- a/doc/user/project/issues/img/create_new_merge_request.png +++ /dev/null diff --git a/doc/user/project/issues/img/issues_main_view.png b/doc/user/project/issues/img/issues_main_view.png Binary files differindex e9a94a3aab0..4faa42e40ee 100755..100644 --- a/doc/user/project/issues/img/issues_main_view.png +++ b/doc/user/project/issues/img/issues_main_view.png diff --git a/doc/user/project/issues/img/issues_main_view_numbered.jpg b/doc/user/project/issues/img/issues_main_view_numbered.jpg Binary files differnew file mode 100644 index 00000000000..4b5d7fba459 --- /dev/null +++ b/doc/user/project/issues/img/issues_main_view_numbered.jpg diff --git a/doc/user/project/issues/img/issues_main_view_numbered.png b/doc/user/project/issues/img/issues_main_view_numbered.png Binary files differdeleted file mode 100755 index 9cff61d7041..00000000000 --- a/doc/user/project/issues/img/issues_main_view_numbered.png +++ /dev/null diff --git a/doc/user/project/issues/index.md b/doc/user/project/issues/index.md index c726da17259..9598cb801be 100644 --- a/doc/user/project/issues/index.md +++ b/doc/user/project/issues/index.md @@ -49,6 +49,10 @@ Read through the [documentation on creating issues](create_new_issue.md). Read through the distinct ways to [close issues](closing_issues.md) on GitLab. +## Create a merge request from an issue + +Learn more about it on the [GitLab Issues Functionalities documentation](issues_functionalities.md#18-new-merge-request). + ## Search for an issue Learn how to [find an issue](../../search/index.md) by searching for and filtering them. diff --git a/doc/user/project/issues/issues_functionalities.md b/doc/user/project/issues/issues_functionalities.md index 33fe768a0c6..ba843201e1a 100644 --- a/doc/user/project/issues/issues_functionalities.md +++ b/doc/user/project/issues/issues_functionalities.md @@ -6,7 +6,7 @@ Please read through the [GitLab Issue Documentation](index.md) for an overview o The image bellow illustrates how an issue looks like: -![Issue view](img/issues_main_view_numbered.png) +![Issue view](img/issues_main_view_numbered.jpg) You can find all the information on that issue on one screen. @@ -16,6 +16,9 @@ An issue starts with its status (open or closed), followed by its author, and includes many other functionalities, numbered on the image above to explain what they mean, one by one. +Many of the elements of the issue screen refresh automatically, such as the title and description, when they are changed by another user. +Comments and system notes also appear automatically in response to various actions and content updates. + #### 1. New Issue, close issue, edit - New issue: create a new issue in the same project @@ -38,6 +41,21 @@ it's reassigned to someone else to take it from there. if a user is not member of that project, it can only be assigned to them if they created the issue themselves. +##### 3.1. Multiple Assignees (EES/EEP) + +Issue Weights are only available in [GitLab Enterprise Edition](https://about.gitlab.com/gitlab-ee/). + +Often multiple people likely work on the same issue together, +which can especially be difficult to track in large teams +where there is shared ownership of an issue. + +In GitLab Enterprise Edition, you can also select multiple assignees +to an issue. + +> **Note:** +Multiple Assignees was [introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/1904) +in [GitLab Enterprise Edition 9.2](https://about.gitlab.com/2017/05/22/gitlab-9-2-released/#multiple-assignees-for-issues). + #### 4. Milestone - Select a [milestone](../milestones/index.md) to attribute that issue to. @@ -150,14 +168,9 @@ Once you wrote your comment, you can either: - Click "Start discussion": start a thread within that issue's thread to discuss specific points. - Click "Comment and close issue": post your comment and close that issue in one click. -#### 18. New branch - -- [New branch](../repository/web_editor.md#create-a-new-branch-from-an-issue): -create a new branch, followed by a new merge request which will automatically close that -issue as soon as that merge request is merged. - -#### 19. New merge request - -- Create a new merge request (with source branch) in one action. Optionally just create a new branch, as explained above. +#### 18. New Merge Request -![Create new merge request](img/create_new_merge_request.png) +- Create a new merge request (with a new source branch named after the issue) in one action. +The merge request will automatically close that issue as soon as merged. +- Optionally, you can just create a [new branch](../repository/web_editor.md#create-a-new-branch-from-an-issue) +named after that issue. diff --git a/doc/user/project/milestones/img/progress.png b/doc/user/project/milestones/img/progress.png Binary files differnew file mode 100644 index 00000000000..c85aecca729 --- /dev/null +++ b/doc/user/project/milestones/img/progress.png diff --git a/doc/user/project/milestones/index.md b/doc/user/project/milestones/index.md index a43a42a8fe8..99233ed5ae2 100644 --- a/doc/user/project/milestones/index.md +++ b/doc/user/project/milestones/index.md @@ -44,3 +44,11 @@ special options available when filtering by milestone: * **Started** - show issues or merge requests from any milestone with a start date less than today. Note that this can return results from several milestones in the same project. + +## Milestone progress statistics + +Milestone statistics can be viewed in the milestone sidebar. The milestone percentage statistic +is calculated as; closed and merged merge requests plus all closed issues divided by +total merge requests and issues. + +![Milestone statistics](img/progress.png) diff --git a/doc/user/project/new_ci_build_permissions_model.md b/doc/user/project/new_ci_build_permissions_model.md index 051a28efea6..e9512497d6c 100644 --- a/doc/user/project/new_ci_build_permissions_model.md +++ b/doc/user/project/new_ci_build_permissions_model.md @@ -100,7 +100,7 @@ In versions before GitLab 8.12, all CI jobs would use the CI Runner's token to checkout project sources. The project's Runner's token was a token that you could find under the -project's **Settings > CI/CD Pipelines** and was limited to access only that +project's **Settings > Pipelines** and was limited to access only that project. It could be used for registering new specific Runners assigned to the project and to checkout project sources. diff --git a/doc/user/project/pages/getting_started_part_two.md b/doc/user/project/pages/getting_started_part_two.md index c91e2d8c261..64de0463dad 100644 --- a/doc/user/project/pages/getting_started_part_two.md +++ b/doc/user/project/pages/getting_started_part_two.md @@ -57,7 +57,7 @@ created for the steps below. ![remove fork relashionship](img/remove_fork_relashionship.png) -1. Enable Shared Runners for your fork: navigate to your **Project**'s **Settings** > **CI/CD Pipelines** +1. Enable Shared Runners for your fork: navigate to your **Project**'s **Settings** > **Pipelines** 1. Trigger a build (push a change to any file) 1. As soon as the build passes, your website will have been deployed with GitLab Pages. Your website URL will be available under your **Project**'s **Settings** > **Pages** diff --git a/doc/user/project/pipelines/img/pipeline_schedules_list.png b/doc/user/project/pipelines/img/pipeline_schedules_list.png Binary files differnew file mode 100644 index 00000000000..50d9d184b05 --- /dev/null +++ b/doc/user/project/pipelines/img/pipeline_schedules_list.png diff --git a/doc/ci/img/pipeline_schedules_new_form.png b/doc/user/project/pipelines/img/pipeline_schedules_new_form.png Binary files differindex ea5394fa8a6..ea5394fa8a6 100644 --- a/doc/ci/img/pipeline_schedules_new_form.png +++ b/doc/user/project/pipelines/img/pipeline_schedules_new_form.png diff --git a/doc/user/project/pipelines/img/pipeline_schedules_ownership.png b/doc/user/project/pipelines/img/pipeline_schedules_ownership.png Binary files differnew file mode 100644 index 00000000000..31ed83abb4d --- /dev/null +++ b/doc/user/project/pipelines/img/pipeline_schedules_ownership.png diff --git a/doc/user/project/pipelines/job_artifacts.md b/doc/user/project/pipelines/job_artifacts.md index 5ce99843301..151ee4728ad 100644 --- a/doc/user/project/pipelines/job_artifacts.md +++ b/doc/user/project/pipelines/job_artifacts.md @@ -41,6 +41,10 @@ For more examples on artifacts, follow the artifacts reference in ## Browsing job artifacts +>**Note:** +With GitLab 9.2, PDFs, images, videos and other formats can be previewed directly +in the job artifacts browser without the need to download them. + After a job finishes, if you visit the job's specific page, you can see that there are two buttons. One is for downloading the artifacts archive and the other for browsing its contents. diff --git a/doc/user/project/pipelines/schedules.md b/doc/user/project/pipelines/schedules.md new file mode 100644 index 00000000000..641876f948f --- /dev/null +++ b/doc/user/project/pipelines/schedules.md @@ -0,0 +1,62 @@ +# Pipeline Schedules + +> **Notes**: +- This feature was introduced in 9.1 as [Trigger Schedule][ce-10533]. +- In 9.2, the feature was [renamed to Pipeline Schedule][ce-10853]. +- Cron notation is parsed by [Rufus-Scheduler](https://github.com/jmettraux/rufus-scheduler). + +Pipeline schedules can be used to run pipelines only once, or for example every +month on the 22nd for a certain branch. + +## Using Pipeline schedules + +In order to schedule a pipeline: + +1. Navigate to your project's **Pipelines ➔ Schedules** and click the + **New Schedule** button. +1. Fill in the form +1. Hit **Save pipeline schedule** for the changes to take effect. + +![New Schedule Form](img/pipeline_schedules_new_form.png) + +>**Attention:** +The pipelines won't be executed precisely, because schedules are handled by +Sidekiq, which runs according to its interval. +See [advanced admin configuration](#advanced-admin-configuration) for more +information. + +In the **Schedules** index page you can see a list of the pipelines that are +scheduled to run. The next run is automatically calculated by the server GitLab +is installed on. + +![Schedules list](img/pipeline_schedules_list.png) + +## Taking ownership + +Pipelines are executed as a user, who owns a schedule. This influences what +projects and other resources the pipeline has access to. If a user does not own +a pipeline, you can take ownership by clicking the **Take ownership** button. +The next time a pipeline is scheduled, your credentials will be used. + +![Schedules list](img/pipeline_schedules_ownership.png) + +>**Note:** +When the owner of the schedule doesn't have the ability to create pipelines +anymore, due to e.g., being blocked or removed from the project, the schedule +is deactivated. Another user can take ownership and activate it, so the +schedule can be run again. + +## Advanced admin configuration + +The pipelines won't be executed precisely, because schedules are handled by +Sidekiq, which runs according to its interval. For example, if you set a +schedule to create a pipeline every minute (`* * * * *`) and the Sidekiq worker +runs on 00:00 and 12:00 every day (`0 */12 * * *`), only 2 pipelines will be +created per day. To change the Sidekiq worker's frequency, you have to edit the +`trigger_schedule_worker_cron` value in your `gitlab.rb` and restart GitLab. +For GitLab.com, you can check the [dedicated settings page][settings]. If you +don't have admin access to the server, ask your administrator. + +[ce-10533]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10533 +[ce-10853]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10853 +[settings]: https://about.gitlab.com/gitlab-com/settings/#cron-jobs diff --git a/doc/user/project/pipelines/settings.md b/doc/user/project/pipelines/settings.md index 88246e22391..1b42c43cf8f 100644 --- a/doc/user/project/pipelines/settings.md +++ b/doc/user/project/pipelines/settings.md @@ -1,4 +1,4 @@ -# CI/CD pipelines settings +# Pipelines settings To reach the pipelines settings: @@ -6,7 +6,7 @@ To reach the pipelines settings: ![Project settings menu](../img/project_settings_list.png) -1. Select **CI/CD Pipelines** from the menu. +1. Select **Pipelines** from the menu. The following settings can be configured per project. diff --git a/doc/user/search/img/filter_issues_project.gif b/doc/user/search/img/filter_issues_project.gif Binary files differdeleted file mode 100644 index d547588be5d..00000000000 --- a/doc/user/search/img/filter_issues_project.gif +++ /dev/null diff --git a/doc/user/search/img/issue_search_filter.png b/doc/user/search/img/issue_search_filter.png Binary files differnew file mode 100644 index 00000000000..f357abd6bac --- /dev/null +++ b/doc/user/search/img/issue_search_filter.png diff --git a/doc/user/search/index.md b/doc/user/search/index.md index 45f443819ec..6d59dcc6c75 100644 --- a/doc/user/search/index.md +++ b/doc/user/search/index.md @@ -34,18 +34,22 @@ a project's **Issues** tab, and click on the field **Search or filter results... display a dropdown menu, from which you can add filters per author, assignee, milestone, label, and weight. When done, press **Enter** on your keyboard to filter the issues. -![filter issues in a project](img/filter_issues_project.gif) +![filter issues in a project](img/issue_search_filter.png) The same process is valid for merge requests. Navigate to your project's **Merge Requests** tab, and click **Search or filter results...**. Merge requests can be filtered by author, assignee, milestone, and label. -## Search History +## Search history You can view recent searches by clicking on the little arrow-clock icon, which is to the left of the search input. Click the search entry to run that search again. This feature is available for issues and merge requests. Searches are stored locally in your browser. ![search history](img/search_history.gif) +## Removing search filters + +Individual filters can be removed by clicking on the filter's (x) button or backspacing. The entire search filter can be cleared by clicking on the search box's (x) button. + ### Shortcut You'll also find a shortcut on the search field on the top-right of the project's dashboard to diff --git a/doc/user/snippets.md b/doc/user/snippets.md index 417360e08ac..78861625f8a 100644 --- a/doc/user/snippets.md +++ b/doc/user/snippets.md @@ -2,8 +2,18 @@ Snippets are little bits of code or text. +![GitLab Snippet](img/gitlab_snippet.png) + There are 2 types of snippets - project snippets and personal snippets. +## Comments + +With GitLab Snippets you engage in a conversation about that piece of code, +facilitating the collaboration among users. + +> **Note:** +Comments on snippets was [introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/12910) in [GitLab Community Edition 9.2](https://about.gitlab.com/2017/05/22/gitlab-9-2-released/#comments-for-personal-snippets). + ## Project snippets Project snippets are always related to a specific project - see [Project features](../workflow/project_features.md) for more information. diff --git a/doc/workflow/notifications/settings.png b/doc/workflow/img/notification_global_settings.png Binary files differindex 8a5494d16a8..8a5494d16a8 100644 --- a/doc/workflow/notifications/settings.png +++ b/doc/workflow/img/notification_global_settings.png diff --git a/doc/workflow/img/notification_group_settings.png b/doc/workflow/img/notification_group_settings.png Binary files differnew file mode 100644 index 00000000000..fc096f46901 --- /dev/null +++ b/doc/workflow/img/notification_group_settings.png diff --git a/doc/workflow/img/notification_project_settings.png b/doc/workflow/img/notification_project_settings.png Binary files differnew file mode 100644 index 00000000000..006432f65c9 --- /dev/null +++ b/doc/workflow/img/notification_project_settings.png diff --git a/doc/workflow/lfs/lfs_administration.md b/doc/workflow/lfs/lfs_administration.md index 3a6773909d6..d768b73286d 100644 --- a/doc/workflow/lfs/lfs_administration.md +++ b/doc/workflow/lfs/lfs_administration.md @@ -50,7 +50,7 @@ and [projects APIs](../../api/projects.md). * Currently, storing GitLab Git LFS objects on a non-local storage (like S3 buckets) is not supported -* Currently, removing LFS objects from GitLab Git LFS storage is not supported +* Support for removing unreferenced LFS objects was added in 8.14 onwards. * LFS authentications via SSH was added with GitLab 8.12 * Only compatible with the GitLFS client versions 1.1.0 and up, or 1.0.2. * The storage statistics currently count each LFS object multiple times for diff --git a/doc/workflow/notifications.md b/doc/workflow/notifications.md index e91d36987a9..3e2e7d0f7b6 100644 --- a/doc/workflow/notifications.md +++ b/doc/workflow/notifications.md @@ -6,7 +6,7 @@ GitLab has a notification system in place to notify a user of events that are im You can find notification settings under the user profile. -![notification settings](notifications/settings.png) +![notification settings](img/notification_global_settings.png) Notification settings are divided into three groups: @@ -32,19 +32,23 @@ anything that is set at Global Settings. #### Group Settings +![notification settings](img/notification_group_settings.png) + Group Settings are taking precedence over Global Settings but are on a level below Project Settings. This means that you can set a different level of notifications per group while still being able to have a finer level setting per project. Organization like this is suitable for users that belong to different groups but don't have the same need for being notified for every group they are member of. -These settings can be configured on group page or user profile notifications dropdown. +These settings can be configured on group page under the name of the group. It will be the dropdown with the bell icon. They can also be configured on the user profile notifications dropdown. #### Project Settings +![notification settings](img/notification_project_settings.png) + Project Settings are at the top level and any setting placed at this level will take precedence of any other setting. This is suitable for users that have different needs for notifications per project basis. -These settings can be configured on project page or user profile notifications dropdown. +These settings can be configured on project page under the name of the project. It will be the dropdown with the bell icon. They can also be configured on the user profile notifications dropdown. ## Notification events diff --git a/features/profile/active_tab.feature b/features/profile/active_tab.feature index 788b7895d72..21d7d6c3800 100644 --- a/features/profile/active_tab.feature +++ b/features/profile/active_tab.feature @@ -23,7 +23,7 @@ Feature: Profile Active Tab Then the active main tab should be Preferences And no other main tabs should be active - Scenario: On Profile Audit Log - Given I visit Audit Log page - Then the active main tab should be Audit Log + Scenario: On Profile Authentication log + Given I visit Authentication log page + Then the active main tab should be Authentication log And no other main tabs should be active diff --git a/features/profile/profile.feature b/features/profile/profile.feature index 70f47c97173..3263d3e212b 100644 --- a/features/profile/profile.feature +++ b/features/profile/profile.feature @@ -63,7 +63,7 @@ Feature: Profile Given I logout And I sign in via the UI And I have activity - When I visit Audit Log page + When I visit Authentication log page Then I should see my activity Scenario: I visit my user page diff --git a/features/steps/dashboard/event_filters.rb b/features/steps/dashboard/event_filters.rb index ca3cd0ecc4e..a745254cc31 100644 --- a/features/steps/dashboard/event_filters.rb +++ b/features/steps/dashboard/event_filters.rb @@ -1,5 +1,5 @@ class Spinach::Features::EventFilters < Spinach::FeatureSteps - include WaitForAjax + include WaitForRequests include SharedAuthentication include SharedPaths include SharedProject @@ -73,20 +73,20 @@ class Spinach::Features::EventFilters < Spinach::FeatureSteps end When 'I click "push" event filter' do - wait_for_ajax + wait_for_requests click_link("Push events") - wait_for_ajax + wait_for_requests end When 'I click "team" event filter' do - wait_for_ajax + wait_for_requests click_link("Team") - wait_for_ajax + wait_for_requests end When 'I click "merge" event filter' do - wait_for_ajax + wait_for_requests click_link("Merge events") - wait_for_ajax + wait_for_requests end end diff --git a/features/steps/dashboard/todos.rb b/features/steps/dashboard/todos.rb index 14c13c4818a..4a33babe3bd 100644 --- a/features/steps/dashboard/todos.rb +++ b/features/steps/dashboard/todos.rb @@ -3,7 +3,7 @@ class Spinach::Features::DashboardTodos < Spinach::FeatureSteps include SharedPaths include SharedProject include SharedUser - include WaitForAjax + include WaitForRequests step '"John Doe" is a developer of project "Shop"' do project.team << [john_doe, :developer] @@ -138,9 +138,9 @@ class Spinach::Features::DashboardTodos < Spinach::FeatureSteps end step 'I should be directed to the corresponding page' do - page.should have_css('.identifier', text: 'Merge Request !1') + page.should have_css('.identifier', text: 'Merge request !1') # Merge request page loads and issues a number of Ajax requests - wait_for_ajax + wait_for_requests end def should_see_todo(position, title, body, state: :pending) diff --git a/features/steps/group/members.rb b/features/steps/group/members.rb index b04a7015d4e..0ab1012660c 100644 --- a/features/steps/group/members.rb +++ b/features/steps/group/members.rb @@ -1,5 +1,5 @@ class Spinach::Features::GroupMembers < Spinach::FeatureSteps - include WaitForAjax + include WaitForRequests include SharedAuthentication include SharedPaths include SharedGroup @@ -58,7 +58,7 @@ class Spinach::Features::GroupMembers < Spinach::FeatureSteps click_link 'Developer' end - wait_for_ajax + wait_for_requests end end diff --git a/features/steps/group/milestones.rb b/features/steps/group/milestones.rb index 0b0983f0d06..0542b06c0ab 100644 --- a/features/steps/group/milestones.rb +++ b/features/steps/group/milestones.rb @@ -1,5 +1,5 @@ class Spinach::Features::GroupMilestones < Spinach::FeatureSteps - include WaitForAjax + include WaitForRequests include SharedAuthentication include SharedPaths include SharedGroup @@ -91,7 +91,7 @@ class Spinach::Features::GroupMilestones < Spinach::FeatureSteps end step 'I should see the list of labels' do - wait_for_ajax + wait_for_requests page.within('#tab-labels') do expect(page).to have_content 'bug' diff --git a/features/steps/profile/active_tab.rb b/features/steps/profile/active_tab.rb index 4724a326277..069d4e6a23d 100644 --- a/features/steps/profile/active_tab.rb +++ b/features/steps/profile/active_tab.rb @@ -19,7 +19,7 @@ class Spinach::Features::ProfileActiveTab < Spinach::FeatureSteps ensure_active_main_tab('Preferences') end - step 'the active main tab should be Audit Log' do - ensure_active_main_tab('Audit Log') + step 'the active main tab should be Authentication log' do + ensure_active_main_tab('Authentication log') end end diff --git a/features/steps/project/builds/artifacts.rb b/features/steps/project/builds/artifacts.rb index 89132ff068f..4b72355b125 100644 --- a/features/steps/project/builds/artifacts.rb +++ b/features/steps/project/builds/artifacts.rb @@ -3,7 +3,7 @@ class Spinach::Features::ProjectBuildsArtifacts < Spinach::FeatureSteps include SharedProject include SharedBuilds include RepoHelpers - include WaitForAjax + include WaitForRequests step 'I click artifacts download button' do click_link 'Download' @@ -79,7 +79,7 @@ class Spinach::Features::ProjectBuildsArtifacts < Spinach::FeatureSteps step 'I click a link to file within build artifacts' do page.within('.tree-table') { find_link('ci_artifacts.txt').click } - wait_for_ajax + wait_for_requests end step 'I see a download link' do diff --git a/features/steps/project/forked_merge_requests.rb b/features/steps/project/forked_merge_requests.rb index 29055373a57..25514eb9ef2 100644 --- a/features/steps/project/forked_merge_requests.rb +++ b/features/steps/project/forked_merge_requests.rb @@ -4,8 +4,7 @@ class Spinach::Features::ProjectForkedMergeRequests < Spinach::FeatureSteps include SharedNote include SharedPaths include Select2Helper - include WaitForVueResource - include WaitForAjax + include WaitForRequests step 'I am a member of project "Shop"' do @project = ::Project.find_by(name: "Shop") @@ -34,7 +33,7 @@ class Spinach::Features::ProjectForkedMergeRequests < Spinach::FeatureSteps expect(page).to have_content @merge_request.source_branch expect(page).to have_content @merge_request.target_branch - wait_for_vue_resource + wait_for_requests end step 'I fill out a "Merge Request On Forked Project" merge request' do @@ -48,7 +47,7 @@ class Spinach::Features::ProjectForkedMergeRequests < Spinach::FeatureSteps first('.dropdown-target-project a', text: @project.path_with_namespace) first('.js-source-branch').click - wait_for_ajax + wait_for_requests first('.dropdown-source-branch .dropdown-content a', text: 'fix').click click_button "Compare branches and continue" diff --git a/features/steps/project/merge_requests.rb b/features/steps/project/merge_requests.rb index 8133760e619..54b6352c952 100644 --- a/features/steps/project/merge_requests.rb +++ b/features/steps/project/merge_requests.rb @@ -7,11 +7,10 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps include SharedMarkdown include SharedDiffNote include SharedUser - include WaitForAjax - include WaitForVueResource + include WaitForRequests after do - wait_for_ajax if javascript_test? + wait_for_requests if javascript_test? end step 'I click link "New Merge Request"' do @@ -46,23 +45,23 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps page.within '.merge-request' do expect(page).to have_content "Wiki Feature" end - wait_for_vue_resource + wait_for_requests end step 'I should see closed merge request "Bug NS-04"' do expect(page).to have_content "Bug NS-04" expect(page).to have_content "Closed by" - wait_for_vue_resource + wait_for_requests end step 'I should see merge request "Bug NS-04"' do expect(page).to have_content "Bug NS-04" - wait_for_vue_resource + wait_for_requests end step 'I should see merge request "Feature NS-05"' do expect(page).to have_content "Feature NS-05" - wait_for_vue_resource + wait_for_requests end step 'I should not see "master" branch' do @@ -99,7 +98,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps step 'I click button "Unsubscribe"' do click_on "Unsubscribe" - wait_for_ajax + wait_for_requests end step 'I click link "Close"' do @@ -353,7 +352,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps step 'I should see a discussion by user "John Doe" has started on diff' do # Trigger a refresh of notes execute_script("$(document).trigger('visibilitychange');") - wait_for_ajax + wait_for_requests page.within(".notes .discussion") do page.should have_content "#{user_exists("John Doe").name} #{user_exists("John Doe").to_reference} started a discussion" page.should have_content sample_commit.line_code_path @@ -363,12 +362,12 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps step 'I should see a badge of "1" next to the discussion link' do expect_discussion_badge_to_have_counter("1") - wait_for_vue_resource + wait_for_requests end step 'I should see a badge of "0" next to the discussion link' do expect_discussion_badge_to_have_counter("0") - wait_for_vue_resource + wait_for_requests end step 'I should see a discussion has started on commit diff' do @@ -376,7 +375,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps page.should have_content "#{current_user.name} #{current_user.to_reference} started a discussion on commit" page.should have_content sample_commit.line_code_path page.should have_content "Line is wrong" - wait_for_vue_resource + wait_for_requests end end @@ -384,7 +383,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps page.within(".notes .discussion") do page.should have_content "#{current_user.name} #{current_user.to_reference} started a discussion on commit" page.should have_content "One comment to rule them all" - wait_for_vue_resource + wait_for_requests end end @@ -410,7 +409,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps step 'I should see merged request' do page.within '.status-box' do expect(page).to have_content "Merged" - wait_for_vue_resource + wait_for_requests end end @@ -422,7 +421,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps page.within '.status-box' do expect(page).to have_content "Open" end - wait_for_vue_resource + wait_for_requests end step 'I click link "Hide inline discussion" of the third file' do @@ -446,7 +445,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps step 'I should see a comment like "Line is wrong" in the third file' do page.within '.files>div:nth-child(3) .note-body > .note-text' do expect(page).to have_visible_content "Line is wrong" - wait_for_vue_resource + wait_for_requests end end @@ -470,7 +469,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps click_button "Comment" end - wait_for_ajax + wait_for_requests page.within ".files>div:nth-child(2) .note-body > .note-text" do expect(page).to have_content "Line is correct" @@ -485,7 +484,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps click_button "Comment" end - wait_for_ajax + wait_for_requests end step 'I should still see a comment like "Line is correct" in the second file' do @@ -514,7 +513,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps step 'I should see comments on the side-by-side diff page' do page.within '.files>div:nth-child(2) .parallel .note-body > .note-text' do expect(page).to have_visible_content "Line is correct" - wait_for_vue_resource + wait_for_requests end end @@ -538,7 +537,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps step 'I should see new target branch changes' do expect(page).to have_content 'Request to merge fix into feature' expect(page).to have_content 'changed target branch from merge-test to feature' - wait_for_ajax + wait_for_requests end step 'I click on "Email Patches"' do @@ -556,8 +555,14 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps step '"Bug NS-05" has CI status' do project = merge_request.source_project project.enable_ci - pipeline = create :ci_pipeline, project: project, sha: merge_request.diff_head_sha, ref: merge_request.source_branch - merge_request.update(head_pipeline: pipeline) + + pipeline = + create(:ci_pipeline, + project: project, + sha: merge_request.diff_head_sha, + ref: merge_request.source_branch, + head_pipeline_of: merge_request) + create :ci_build, pipeline: pipeline end @@ -572,7 +577,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps expect(page).to have_content /([0-9]+ commits behind)/ end - wait_for_vue_resource + wait_for_requests end step 'I should not see the diverged commits count' do @@ -580,7 +585,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps expect(page).not_to have_content /([0-9]+ commit[s]? behind)/ end - wait_for_vue_resource + wait_for_requests end def merge_request @@ -597,7 +602,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps click_button "Comment" end - wait_for_ajax + wait_for_requests page.within(".notes_holder", visible: true) do expect(page).to have_content message diff --git a/features/steps/project/merge_requests/acceptance.rb b/features/steps/project/merge_requests/acceptance.rb index 3c976f675a2..023f9bef8e5 100644 --- a/features/steps/project/merge_requests/acceptance.rb +++ b/features/steps/project/merge_requests/acceptance.rb @@ -1,7 +1,7 @@ class Spinach::Features::ProjectMergeRequestsAcceptance < Spinach::FeatureSteps include LoginHelpers include GitlabRoutingHelper - include WaitForVueResource + include WaitForRequests step 'I am on the Merge Request detail page' do visit merge_request_path(@merge_request) @@ -24,7 +24,7 @@ class Spinach::Features::ProjectMergeRequestsAcceptance < Spinach::FeatureSteps # Wait for View Resource requests to complete so they don't blow up if they are # only handled after `DatabaseCleaner` has already run - wait_for_vue_resource + wait_for_requests end step 'I should not see the Remove Source Branch button' do @@ -32,7 +32,7 @@ class Spinach::Features::ProjectMergeRequestsAcceptance < Spinach::FeatureSteps # Wait for View Resource requests to complete so they don't blow up if they are # only handled after `DatabaseCleaner` has already run - wait_for_vue_resource + wait_for_requests end step 'There is an open Merge Request' do diff --git a/features/steps/project/merge_requests/revert.rb b/features/steps/project/merge_requests/revert.rb index aa76d6f8c48..98d990f112f 100644 --- a/features/steps/project/merge_requests/revert.rb +++ b/features/steps/project/merge_requests/revert.rb @@ -1,7 +1,7 @@ class Spinach::Features::RevertMergeRequests < Spinach::FeatureSteps include LoginHelpers include GitlabRoutingHelper - include WaitForVueResource + include WaitForRequests step 'I click on the revert button' do find("a[href='#modal-revert-commit']").click @@ -16,7 +16,7 @@ class Spinach::Features::RevertMergeRequests < Spinach::FeatureSteps step 'I should see the revert merge request notice' do page.should have_content('The merge request has been successfully reverted.') - wait_for_vue_resource + wait_for_requests end step 'I should not see the revert button' do diff --git a/features/steps/project/project.rb b/features/steps/project/project.rb index 9c2196a8ef7..de32c9afcca 100644 --- a/features/steps/project/project.rb +++ b/features/steps/project/project.rb @@ -2,7 +2,7 @@ class Spinach::Features::Project < Spinach::FeatureSteps include SharedAuthentication include SharedProject include SharedPaths - include WaitForAjax + include WaitForRequests step 'change project settings' do fill_in 'project_name_edit', with: 'NewName' @@ -87,7 +87,7 @@ class Spinach::Features::Project < Spinach::FeatureSteps end step 'I should see project "Shop" README' do - wait_for_ajax + wait_for_requests page.within('.readme-holder') do expect(page).to have_content 'testme' end diff --git a/features/steps/project/project_milestone.rb b/features/steps/project/project_milestone.rb index dc1190b7eea..a7d3352b8c4 100644 --- a/features/steps/project/project_milestone.rb +++ b/features/steps/project/project_milestone.rb @@ -2,7 +2,7 @@ class Spinach::Features::ProjectMilestone < Spinach::FeatureSteps include SharedAuthentication include SharedProject include SharedPaths - include WaitForAjax + include WaitForRequests step 'milestone has issue "Bugfix1" with labels: "bug", "feature"' do project = Project.find_by(name: "Shop") @@ -35,7 +35,7 @@ class Spinach::Features::ProjectMilestone < Spinach::FeatureSteps end step 'I should see the labels "bug", "enhancement" and "feature"' do - wait_for_ajax + wait_for_requests page.within('#tab-issues') do expect(page).to have_content 'bug' diff --git a/features/steps/project/services.rb b/features/steps/project/services.rb index 772b07d0ad8..3c0d987e403 100644 --- a/features/steps/project/services.rb +++ b/features/steps/project/services.rb @@ -211,7 +211,7 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps end step 'I should see empty field Change Password' do - expect(find_field('Change Password').value).to be_nil + expect(find_field('Enter new password').value).to be_nil end step 'I click JetBrains TeamCity CI service link' do diff --git a/features/steps/project/snippets.rb b/features/steps/project/snippets.rb index 60febd20104..e3f5e9e3ef3 100644 --- a/features/steps/project/snippets.rb +++ b/features/steps/project/snippets.rb @@ -3,7 +3,7 @@ class Spinach::Features::ProjectSnippets < Spinach::FeatureSteps include SharedProject include SharedNote include SharedPaths - include WaitForAjax + include WaitForRequests step 'project "Shop" have "Snippet one" snippet' do create(:project_snippet, @@ -59,7 +59,7 @@ class Spinach::Features::ProjectSnippets < Spinach::FeatureSteps find('.ace_editor').native.send_keys 'Content of snippet three' end click_button "Create snippet" - wait_for_ajax + wait_for_requests end step 'I should see snippet "Snippet three"' do @@ -81,7 +81,7 @@ class Spinach::Features::ProjectSnippets < Spinach::FeatureSteps fill_in "note_note", with: "Good snippet!" click_button "Comment" end - wait_for_ajax + wait_for_requests end step 'I should see comment "Good snippet!"' do diff --git a/features/steps/project/source/browse_files.rb b/features/steps/project/source/browse_files.rb index ef09bddddd8..6efd4374b32 100644 --- a/features/steps/project/source/browse_files.rb +++ b/features/steps/project/source/browse_files.rb @@ -4,7 +4,7 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps include SharedProject include SharedPaths include RepoHelpers - include WaitForAjax + include WaitForRequests step "I don't have write access" do @project = create(:project, :repository, name: "Other Project", path: "other-project") @@ -37,12 +37,12 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps end step 'I should see its content' do - wait_for_ajax + wait_for_requests expect(page).to have_content old_gitignore_content end step 'I should see its new content' do - wait_for_ajax + wait_for_requests expect(page).to have_content new_gitignore_content end diff --git a/features/steps/project/source/markdown_render.rb b/features/steps/project/source/markdown_render.rb index ada0ff20585..0fee158d590 100644 --- a/features/steps/project/source/markdown_render.rb +++ b/features/steps/project/source/markdown_render.rb @@ -5,7 +5,7 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps include SharedAuthentication include SharedPaths include SharedMarkdown - include WaitForAjax + include WaitForRequests step 'I own project "Delta"' do @project = ::Project.find_by(name: "Delta") @@ -35,7 +35,7 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps step 'I should see correct document rendered' do expect(current_path).to eq namespace_project_blob_path(@project.namespace, @project, "markdown/doc/api/README.md") - wait_for_ajax + wait_for_requests expect(page).to have_content "All API requests require authentication" end @@ -65,7 +65,7 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps step 'I should see correct maintenance file rendered' do expect(current_path).to eq namespace_project_blob_path(@project.namespace, @project, "markdown/doc/raketasks/maintenance.md") - wait_for_ajax + wait_for_requests expect(page).to have_content "bundle exec rake gitlab:env:info RAILS_ENV=production" end @@ -97,7 +97,7 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps step 'I see correct file rendered' do expect(current_path).to eq namespace_project_blob_path(@project.namespace, @project, "markdown/doc/api/README.md") - wait_for_ajax + wait_for_requests expect(page).to have_content "Contents" expect(page).to have_link "Users" expect(page).to have_link "Rake tasks" @@ -120,7 +120,7 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps When 'I visit markdown branch' do visit namespace_project_tree_path(@project.namespace, @project, "markdown") - wait_for_ajax + wait_for_requests end When 'I visit markdown branch "README.md" blob' do @@ -143,7 +143,7 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps step 'I see correct file rendered in markdown branch' do expect(current_path).to eq namespace_project_blob_path(@project.namespace, @project, "markdown/doc/api/README.md") - wait_for_ajax + wait_for_requests expect(page).to have_content "Contents" expect(page).to have_link "Users" expect(page).to have_link "Rake tasks" @@ -151,7 +151,7 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps step 'I should see correct document rendered for markdown branch' do expect(current_path).to eq namespace_project_blob_path(@project.namespace, @project, "markdown/doc/api/README.md") - wait_for_ajax + wait_for_requests expect(page).to have_content "All API requests require authentication" end @@ -169,7 +169,7 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps # Expected link contents step 'The link with text "empty" should have url "tree/markdown"' do - wait_for_ajax + wait_for_requests find('a', text: /^empty$/)['href'] == current_host + namespace_project_tree_path(@project.namespace, @project, "markdown") end @@ -205,7 +205,7 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps end step 'The link with text "ID" should have url "blob/markdown/README.mdID"' do - wait_for_ajax + wait_for_requests find('a', text: /^#id$/)['href'] == current_host + namespace_project_blob_path(@project.namespace, @project, "markdown/README.md") + '#id' end @@ -300,12 +300,12 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps step 'I should see the correct markdown' do expect(current_path).to eq namespace_project_blob_path(@project.namespace, @project, "markdown/doc/api/users.md") - wait_for_ajax + wait_for_requests expect(page).to have_content "List users" end step 'Header "Application details" should have correct id and link' do - wait_for_ajax + wait_for_requests header_should_have_correct_id_and_link(2, 'Application details', 'application-details') end diff --git a/features/steps/shared/active_tab.rb b/features/steps/shared/active_tab.rb index 8bae80a8707..af5db05e9e8 100644 --- a/features/steps/shared/active_tab.rb +++ b/features/steps/shared/active_tab.rb @@ -1,9 +1,9 @@ module SharedActiveTab include Spinach::DSL - include WaitForAjax + include WaitForRequests after do - wait_for_ajax if javascript_test? + wait_for_requests if javascript_test? end def ensure_active_main_tab(content) diff --git a/features/steps/shared/diff_note.rb b/features/steps/shared/diff_note.rb index 071aa2e3eff..36fc315599e 100644 --- a/features/steps/shared/diff_note.rb +++ b/features/steps/shared/diff_note.rb @@ -1,10 +1,10 @@ module SharedDiffNote include Spinach::DSL include RepoHelpers - include WaitForAjax + include WaitForRequests after do - wait_for_ajax if javascript_test? + wait_for_requests if javascript_test? end step 'I cancel the diff comment' do diff --git a/features/steps/shared/note.rb b/features/steps/shared/note.rb index 7d260025052..44eb8f321dd 100644 --- a/features/steps/shared/note.rb +++ b/features/steps/shared/note.rb @@ -1,9 +1,9 @@ module SharedNote include Spinach::DSL - include WaitForAjax + include WaitForRequests after do - wait_for_ajax if javascript_test? + wait_for_requests if javascript_test? end step 'I delete a comment' do @@ -25,7 +25,7 @@ module SharedNote click_button "Comment" end - wait_for_ajax + wait_for_requests end step 'I preview a comment text like "Bug fixed :smile:"' do @@ -40,7 +40,7 @@ module SharedNote click_button "Comment" end - wait_for_ajax + wait_for_requests end step 'I write a comment like ":+1: Nice"' do @@ -127,7 +127,7 @@ module SharedNote click_button "Comment" end - wait_for_ajax + wait_for_requests end step 'The comment with the header should not have an ID' do diff --git a/features/steps/shared/paths.rb b/features/steps/shared/paths.rb index 46b3cb79af2..f0e751b820a 100644 --- a/features/steps/shared/paths.rb +++ b/features/steps/shared/paths.rb @@ -2,7 +2,7 @@ module SharedPaths include Spinach::DSL include RepoHelpers include DashboardHelper - include WaitForVueResource + include WaitForRequests step 'I visit new project page' do visit new_project_path @@ -152,7 +152,7 @@ module SharedPaths visit profile_preferences_path end - step 'I visit Audit Log page' do + step 'I visit Authentication log page' do visit audit_log_profile_path end @@ -378,28 +378,28 @@ module SharedPaths step 'I visit merge request page "Bug NS-04"' do visit merge_request_path("Bug NS-04") - wait_for_vue_resource + wait_for_requests end step 'I visit merge request page "Bug NS-05"' do visit merge_request_path("Bug NS-05") - wait_for_vue_resource + wait_for_requests end step 'I visit merge request page "Bug NS-07"' do visit merge_request_path("Bug NS-07") - wait_for_vue_resource + wait_for_requests end step 'I visit merge request page "Bug NS-08"' do visit merge_request_path("Bug NS-08") - wait_for_vue_resource + wait_for_requests end step 'I visit merge request page "Bug CO-01"' do mr = MergeRequest.find_by(title: "Bug CO-01") visit namespace_project_merge_request_path(mr.target_project.namespace, mr.target_project, mr) - wait_for_vue_resource + wait_for_requests end step 'I visit project "Shop" merge requests page' do diff --git a/features/steps/shared/project.rb b/features/steps/shared/project.rb index 15625e045f5..c4f1c57836f 100644 --- a/features/steps/shared/project.rb +++ b/features/steps/shared/project.rb @@ -256,9 +256,9 @@ module SharedProject end step 'I should see last commit with CI status' do - page.within ".project-last-commit" do + page.within ".blob-commit-info" do expect(page).to have_content(project.commit.sha[0..6]) - expect(page).to have_content("skipped") + expect(page).to have_link("Commit: skipped") end end diff --git a/features/steps/snippets/snippets.rb b/features/steps/snippets/snippets.rb index 0b3e942a4fd..a4fc77746ee 100644 --- a/features/steps/snippets/snippets.rb +++ b/features/steps/snippets/snippets.rb @@ -3,7 +3,7 @@ class Spinach::Features::Snippets < Spinach::FeatureSteps include SharedPaths include SharedProject include SharedSnippet - include WaitForAjax + include WaitForRequests step 'I click link "Personal snippet one"' do click_link "Personal snippet one" @@ -30,7 +30,7 @@ class Spinach::Features::Snippets < Spinach::FeatureSteps find('.ace_editor').native.send_keys 'Content of snippet three' end click_button "Create snippet" - wait_for_ajax + wait_for_requests end step 'I submit new internal snippet' do diff --git a/features/support/env.rb b/features/support/env.rb index 23a1f702068..1690465d9b2 100644 --- a/features/support/env.rb +++ b/features/support/env.rb @@ -10,7 +10,7 @@ if ENV['CI'] Knapsack::Adapters::SpinachAdapter.bind end -%w(select2_helper test_env repo_helpers wait_for_ajax wait_for_requests sidekiq wait_for_vue_resource).each do |f| +%w(select2_helper test_env repo_helpers wait_for_requests sidekiq).each do |f| require Rails.root.join('spec', 'support', f) end @@ -33,7 +33,7 @@ end Spinach.hooks.after_scenario do |scenario_data, step_definitions| if scenario_data.tags.include?('javascript') include WaitForRequests - wait_for_requests_complete + block_and_wait_for_requests_complete end end diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 3fc2b453eb6..01cc8e8e1ca 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -60,7 +60,7 @@ module API class ProjectHook < Hook expose :project_id, :issues_events, :merge_requests_events expose :note_events, :pipeline_events, :wiki_page_events - expose :build_events, as: :job_events + expose :job_events end class BasicProjectDetails < Grape::Entity @@ -470,7 +470,7 @@ module API expose :id, :title, :created_at, :updated_at, :active expose :push_events, :issues_events, :merge_requests_events expose :tag_push_events, :note_events, :pipeline_events - expose :build_events, as: :job_events + expose :job_events # Expose serialized properties expose :properties do |service, options| field_names = service.fields. diff --git a/lib/api/internal.rb b/lib/api/internal.rb index 96aaaf868ea..9ebd4841296 100644 --- a/lib/api/internal.rb +++ b/lib/api/internal.rb @@ -136,14 +136,15 @@ module API post "/notify_post_receive" do status 200 - return unless Gitlab::GitalyClient.enabled? - - begin - repository = wiki? ? project.wiki.repository : project.repository - Gitlab::GitalyClient::Notifications.new(repository.raw_repository).post_receive - rescue GRPC::Unavailable => e - render_api_error!(e, 500) - end + # TODO: Re-enable when Gitaly is processing the post-receive notification + # return unless Gitlab::GitalyClient.enabled? + # + # begin + # repository = wiki? ? project.wiki.repository : project.repository + # Gitlab::GitalyClient::Notifications.new(repository.raw_repository).post_receive + # rescue GRPC::Unavailable => e + # render_api_error!(e, 500) + # end end end end diff --git a/lib/api/project_hooks.rb b/lib/api/project_hooks.rb index 87dfd1573a4..7a345289617 100644 --- a/lib/api/project_hooks.rb +++ b/lib/api/project_hooks.rb @@ -54,7 +54,6 @@ module API end post ":id/hooks" do hook_params = declared_params(include_missing: false) - hook_params[:build_events] = hook_params.delete(:job_events) { false } hook = user_project.hooks.new(hook_params) @@ -78,7 +77,6 @@ module API hook = user_project.hooks.find(params.delete(:hook_id)) update_params = declared_params(include_missing: false) - update_params[:build_events] = update_params.delete(:job_events) if update_params[:job_events] if hook.update_attributes(update_params) present hook, with: Entities::ProjectHook diff --git a/lib/api/v3/entities.rb b/lib/api/v3/entities.rb index 56a9b019f1b..332f233bf5e 100644 --- a/lib/api/v3/entities.rb +++ b/lib/api/v3/entities.rb @@ -238,7 +238,8 @@ module API class ProjectService < Grape::Entity expose :id, :title, :created_at, :updated_at, :active expose :push_events, :issues_events, :merge_requests_events - expose :tag_push_events, :note_events, :build_events, :pipeline_events + expose :tag_push_events, :note_events, :pipeline_events + expose :job_events, as: :build_events # Expose serialized properties expose :properties do |service, options| field_names = service.fields. @@ -250,7 +251,8 @@ module API class ProjectHook < ::API::Entities::Hook expose :project_id, :issues_events, :merge_requests_events - expose :note_events, :build_events, :pipeline_events, :wiki_page_events + expose :note_events, :pipeline_events, :wiki_page_events + expose :job_events, as: :build_events end class Issue < ::API::Entities::Issue diff --git a/lib/backup/manager.rb b/lib/backup/manager.rb index 330cd963626..f755c99ea4a 100644 --- a/lib/backup/manager.rb +++ b/lib/backup/manager.rb @@ -84,7 +84,11 @@ module Backup Dir.chdir(backup_path) do backup_file_list.each do |file| - next unless file =~ /(\d+)(?:_\d{4}_\d{2}_\d{2})?_gitlab_backup\.tar/ + # For backward compatibility, there are 3 names the backups can have: + # - 1495527122_gitlab_backup.tar + # - 1495527068_2017_05_23_gitlab_backup.tar + # - 1495527097_2017_05_23_9.3.0-pre_gitlab_backup.tar + next unless file =~ /(\d+)(?:_\d{4}_\d{2}_\d{2}(_\d+\.\d+\.\d+.*)?)?_gitlab_backup\.tar$/ timestamp = $1.to_i diff --git a/lib/banzai/filter/ascii_doc_post_processing_filter.rb b/lib/banzai/filter/ascii_doc_post_processing_filter.rb new file mode 100644 index 00000000000..c9fcf057c5f --- /dev/null +++ b/lib/banzai/filter/ascii_doc_post_processing_filter.rb @@ -0,0 +1,13 @@ +module Banzai + module Filter + class AsciiDocPostProcessingFilter < HTML::Pipeline::Filter + def call + doc.search('[data-math-style]').each do |node| + node.set_attribute('class', 'code math js-render-math') + end + + doc + end + end + end +end diff --git a/lib/banzai/filter/external_link_filter.rb b/lib/banzai/filter/external_link_filter.rb index 7d15a0f6d44..d6327ef31cb 100644 --- a/lib/banzai/filter/external_link_filter.rb +++ b/lib/banzai/filter/external_link_filter.rb @@ -24,7 +24,7 @@ module Banzai def uri(href) URI.parse(href) - rescue URI::InvalidURIError + rescue URI::Error nil end diff --git a/lib/banzai/filter/sanitization_filter.rb b/lib/banzai/filter/sanitization_filter.rb index 522217deae4..2d6e8ffc90f 100644 --- a/lib/banzai/filter/sanitization_filter.rb +++ b/lib/banzai/filter/sanitization_filter.rb @@ -31,6 +31,10 @@ module Banzai # Allow span elements whitelist[:elements].push('span') + # Allow data-math-style attribute in order to support LaTeX formatting + whitelist[:attributes]['code'] = %w(data-math-style) + whitelist[:attributes]['pre'] = %w(data-math-style) + # Allow html5 details/summary elements whitelist[:elements].push('details') whitelist[:elements].push('summary') diff --git a/lib/banzai/pipeline/ascii_doc_pipeline.rb b/lib/banzai/pipeline/ascii_doc_pipeline.rb new file mode 100644 index 00000000000..1048b927cd3 --- /dev/null +++ b/lib/banzai/pipeline/ascii_doc_pipeline.rb @@ -0,0 +1,14 @@ +module Banzai + module Pipeline + class AsciiDocPipeline < BasePipeline + def self.filters + FilterArray[ + Filter::SanitizationFilter, + Filter::ExternalLinkFilter, + Filter::PlantumlFilter, + Filter::AsciiDocPostProcessingFilter + ] + end + end + end +end diff --git a/lib/constraints/group_url_constrainer.rb b/lib/constraints/group_url_constrainer.rb index 5f379756c11..0ea2f97352d 100644 --- a/lib/constraints/group_url_constrainer.rb +++ b/lib/constraints/group_url_constrainer.rb @@ -1,8 +1,8 @@ class GroupUrlConstrainer def matches?(request) - id = request.params[:id] + id = request.params[:group_id] || request.params[:id] - return false unless DynamicPathValidator.valid?(id) + return false unless DynamicPathValidator.valid_namespace_path?(id) Group.find_by_full_path(id, follow_redirects: request.get?).present? end diff --git a/lib/constraints/project_url_constrainer.rb b/lib/constraints/project_url_constrainer.rb index 6f542f63f98..4444a1abee3 100644 --- a/lib/constraints/project_url_constrainer.rb +++ b/lib/constraints/project_url_constrainer.rb @@ -4,7 +4,7 @@ class ProjectUrlConstrainer project_path = request.params[:project_id] || request.params[:id] full_path = namespace_path + '/' + project_path - return false unless DynamicPathValidator.valid?(full_path) + return false unless DynamicPathValidator.valid_project_path?(full_path) Project.find_by_full_path(full_path, follow_redirects: request.get?).present? end diff --git a/lib/container_registry/client.rb b/lib/container_registry/client.rb index 7f5f6d9ddb6..c7263f302ab 100644 --- a/lib/container_registry/client.rb +++ b/lib/container_registry/client.rb @@ -75,10 +75,7 @@ module ContainerRegistry def redirect_response(location) return unless location - # We explicitly remove authorization token - faraday_blob.get(location) do |req| - req['Authorization'] = '' - end + faraday_redirect.get(location) end def faraday @@ -93,5 +90,14 @@ module ContainerRegistry initialize_connection(conn, @options) end end + + # Create a new request to make sure the Authorization header is not inserted + # via the Faraday middleware + def faraday_redirect + @faraday_redirect ||= Faraday.new(@base_uri) do |conn| + conn.request :json + conn.adapter :net_http + end + end end end diff --git a/lib/gitlab/asciidoc.rb b/lib/gitlab/asciidoc.rb index 96d38f6daa0..3d41ac76406 100644 --- a/lib/gitlab/asciidoc.rb +++ b/lib/gitlab/asciidoc.rb @@ -20,21 +20,20 @@ module Gitlab backend: :gitlab_html5, attributes: DEFAULT_ADOC_ATTRS } - context[:pipeline] = :markup + context[:pipeline] = :ascii_doc plantuml_setup html = ::Asciidoctor.convert(input, asciidoc_opts) html = Banzai.render(html, context) - html.html_safe end def self.plantuml_setup Asciidoctor::PlantUml.configure do |conf| - conf.url = ApplicationSetting.current.plantuml_url - conf.svg_enable = ApplicationSetting.current.plantuml_enabled - conf.png_enable = ApplicationSetting.current.plantuml_enabled + conf.url = current_application_settings.plantuml_url + conf.svg_enable = current_application_settings.plantuml_enabled + conf.png_enable = current_application_settings.plantuml_enabled conf.txt_enable = false end end @@ -47,13 +46,13 @@ module Gitlab def stem(node) return super unless node.style.to_sym == :latexmath - %(<pre#{id_attribute(node)} class="code math js-render-math #{node.role}" data-math-style="display"><code>#{node.content}</code></pre>) + %(<pre#{id_attribute(node)} data-math-style="display"><code>#{node.content}</code></pre>) end def inline_quoted(node) return super unless node.type.to_sym == :latexmath - %(<code#{id_attribute(node)} class="code math js-render-math #{node.role}" data-math-style="inline">#{node.text}</code>) + %(<code#{id_attribute(node)} data-math-style="inline">#{node.text}</code>) end private diff --git a/lib/gitlab/auth.rb b/lib/gitlab/auth.rb index ea918b23a63..099c45dcfb7 100644 --- a/lib/gitlab/auth.rb +++ b/lib/gitlab/auth.rb @@ -37,6 +37,9 @@ module Gitlab end def find_with_user_password(login, password) + # Avoid resource intensive login checks if password is not provided + return unless password.present? + Gitlab::Auth::UniqueIpsLimiter.limit_user! do user = User.by_login(login) @@ -44,7 +47,7 @@ module Gitlab # LDAP users are only authenticated via LDAP if user.nil? || user.ldap_user? # Second chance - try LDAP authentication - return nil unless Gitlab::LDAP::Config.enabled? + return unless Gitlab::LDAP::Config.enabled? Gitlab::LDAP::Authentication.login(login, password) else diff --git a/lib/gitlab/database/rename_reserved_paths_migration/v1/migration_classes.rb b/lib/gitlab/database/rename_reserved_paths_migration/v1/migration_classes.rb index 4fdcb682c2f..5481024db8e 100644 --- a/lib/gitlab/database/rename_reserved_paths_migration/v1/migration_classes.rb +++ b/lib/gitlab/database/rename_reserved_paths_migration/v1/migration_classes.rb @@ -48,6 +48,14 @@ module Gitlab def self.name 'Namespace' end + + def kind + type == 'Group' ? 'group' : 'user' + end + end + + class User < ActiveRecord::Base + self.table_name = 'users' end class Route < ActiveRecord::Base diff --git a/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base.rb b/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base.rb index 5397877b5d5..d60fd4bb551 100644 --- a/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base.rb +++ b/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base.rb @@ -41,7 +41,8 @@ module Gitlab new_full_path) update_column_in_batches(:routes, :path, replace_statement) do |table, query| - query.where(MigrationClasses::Route.arel_table[:path].matches("#{old_full_path}%")) + path_or_children = table[:path].matches_any([old_full_path, "#{old_full_path}/%"]) + query.where(path_or_children) end end diff --git a/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces.rb b/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces.rb index b9f4f3cff3c..2958ad4b8e5 100644 --- a/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces.rb +++ b/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces.rb @@ -29,9 +29,15 @@ module Gitlab move_repositories(namespace, old_full_path, new_full_path) move_uploads(old_full_path, new_full_path) move_pages(old_full_path, new_full_path) + rename_user(old_full_path, new_full_path) if namespace.kind == 'user' remove_cached_html_for_projects(projects_for_namespace(namespace).map(&:id)) end + def rename_user(old_username, new_username) + MigrationClasses::User.where(username: old_username) + .update_all(username: new_username) + end + def move_repositories(namespace, old_full_path, new_full_path) repo_paths_for_namespace(namespace).each do |repository_storage_path| # Ensure old directory exists before moving it diff --git a/lib/gitlab/diff/file_collection/base.rb b/lib/gitlab/diff/file_collection/base.rb index 2b9fc65b985..7c32adc6ce7 100644 --- a/lib/gitlab/diff/file_collection/base.rb +++ b/lib/gitlab/diff/file_collection/base.rb @@ -24,6 +24,14 @@ module Gitlab @diff_files ||= @diffs.decorate! { |diff| decorate_diff!(diff) } end + def diff_file_with_old_path(old_path) + diff_files.find { |diff_file| diff_file.old_path == old_path } + end + + def diff_file_with_new_path(new_path) + diff_files.find { |diff_file| diff_file.new_path == new_path } + end + private def decorate_diff!(diff) diff --git a/lib/gitlab/diff/position.rb b/lib/gitlab/diff/position.rb index fc728123c97..4d96778a2b2 100644 --- a/lib/gitlab/diff/position.rb +++ b/lib/gitlab/diff/position.rb @@ -12,20 +12,26 @@ module Gitlab attr_reader :head_sha def initialize(attrs = {}) + if diff_file = attrs[:diff_file] + attrs[:diff_refs] = diff_file.diff_refs + attrs[:old_path] = diff_file.old_path + attrs[:new_path] = diff_file.new_path + end + + if diff_refs = attrs[:diff_refs] + attrs[:base_sha] = diff_refs.base_sha + attrs[:start_sha] = diff_refs.start_sha + attrs[:head_sha] = diff_refs.head_sha + end + @old_path = attrs[:old_path] @new_path = attrs[:new_path] + @base_sha = attrs[:base_sha] + @start_sha = attrs[:start_sha] + @head_sha = attrs[:head_sha] + @old_line = attrs[:old_line] @new_line = attrs[:new_line] - - if attrs[:diff_refs] - @base_sha = attrs[:diff_refs].base_sha - @start_sha = attrs[:diff_refs].start_sha - @head_sha = attrs[:diff_refs].head_sha - else - @base_sha = attrs[:base_sha] - @start_sha = attrs[:start_sha] - @head_sha = attrs[:head_sha] - end end # `Gitlab::Diff::Position` objects are stored as serialized attributes in @@ -129,11 +135,11 @@ module Gitlab end def diff_line(repository) - @diff_line ||= diff_file(repository).line_for_position(self) + @diff_line ||= diff_file(repository)&.line_for_position(self) end def line_code(repository) - @line_code ||= diff_file(repository).line_code_for_position(self) + @line_code ||= diff_file(repository)&.line_code_for_position(self) end private diff --git a/lib/gitlab/diff/position_tracer.rb b/lib/gitlab/diff/position_tracer.rb index c7542a8fabc..dcabb5f7fe5 100644 --- a/lib/gitlab/diff/position_tracer.rb +++ b/lib/gitlab/diff/position_tracer.rb @@ -3,21 +3,21 @@ module Gitlab module Diff class PositionTracer - attr_accessor :repository + attr_accessor :project attr_accessor :old_diff_refs attr_accessor :new_diff_refs attr_accessor :paths - def initialize(repository:, old_diff_refs:, new_diff_refs:, paths: nil) - @repository = repository + def initialize(project:, old_diff_refs:, new_diff_refs:, paths: nil) + @project = project @old_diff_refs = old_diff_refs @new_diff_refs = new_diff_refs @paths = paths end - def trace(old_position) - return unless old_diff_refs.complete? && new_diff_refs.complete? - return unless old_position.diff_refs == old_diff_refs + def trace(ab_position) + return unless old_diff_refs&.complete? && new_diff_refs&.complete? + return unless ab_position.diff_refs == old_diff_refs # Suppose we have an MR with source branch `feature` and target branch `master`. # When the MR was created, the head of `master` was commit A, and the @@ -44,14 +44,16 @@ module Gitlab # # For diff notes for diff A->B, the position looks like this: # Position - # base_sha - ID of commit A + # start_sha - ID of commit A # head_sha - ID of commit B + # base_sha - ID of base commit of A and B # old_path - path as of A (nil if file was newly created) # new_path - path as of B (nil if file was deleted) # old_line - line number as of A (nil if file was newly created) # new_line - line number as of B (nil if file was deleted) # - # We can easily update `base_sha` and `head_sha` to hold the IDs of commits C and D, + # We can easily update `start_sha` and `head_sha` to hold the IDs of + # commits C and D, and can trivially determine `base_sha` based on those, # but need to find the paths and line numbers as of C and D. # # If the file was unchanged or newly created in A->B, the path as of D can be found @@ -68,107 +70,161 @@ module Gitlab # by generating diff A->C ("base to base"), finding the diff file with # `diff_file.old_path == position.old_path`, and taking `diff_file.new_path`. # The path as of D can be found by taking diff C->D, finding the diff file - # with that same `old_path` and taking `diff_file.new_path`. + # with `old_path` set to that `diff_file.new_path` and taking `diff_file.new_path`. # The line number as of C can be found by using the LineMapper on diff A->C # and providing the line number as of A. # The line number as of D can be found by using the LineMapper on diff C->D # and providing the line number as of C. - results = nil - results ||= trace_added_line(old_position) if old_position.added? || old_position.unchanged? - results ||= trace_removed_line(old_position) if old_position.removed? || old_position.unchanged? - - return unless results - - file_diff, old_line, new_line = results - - new_position = Position.new( - old_path: file_diff.old_path, - new_path: file_diff.new_path, - head_sha: new_diff_refs.head_sha, - start_sha: new_diff_refs.start_sha, - base_sha: new_diff_refs.base_sha, - old_line: old_line, - new_line: new_line - ) - - # If a position is found, but is not actually contained in the diff, for example - # because it was an unchanged line in the context of a change that was undone, - # we cannot return this as a successful trace. - return unless new_position.diff_line(repository) - - new_position + if ab_position.added? + trace_added_line(ab_position) + elsif ab_position.removed? + trace_removed_line(ab_position) + else # unchanged + trace_unchanged_line(ab_position) + end end private - def trace_added_line(old_position) - file_path = old_position.new_path - - return unless diff_head_to_head - - file_head_to_head = diff_head_to_head.find { |diff_file| diff_file.old_path == file_path } - - file_path = file_head_to_head.new_path if file_head_to_head - - new_line = LineMapper.new(file_head_to_head).old_to_new(old_position.new_line) - - return unless new_line - - file_diff = new_diffs.find { |diff_file| diff_file.new_path == file_path } - return unless file_diff - - old_line = LineMapper.new(file_diff).new_to_old(new_line) - - [file_diff, old_line, new_line] + def trace_added_line(ab_position) + b_path = ab_position.new_path + b_line = ab_position.new_line + + bd_diff = bd_diffs.diff_file_with_old_path(b_path) + + d_path = bd_diff&.new_path || b_path + d_line = LineMapper.new(bd_diff).old_to_new(b_line) + + if d_line + cd_diff = cd_diffs.diff_file_with_new_path(d_path) + + c_path = cd_diff&.old_path || d_path + c_line = LineMapper.new(cd_diff).new_to_old(d_line) + + if c_line + # If the line is still in D but also in C, it has turned from an + # added line into an unchanged one. + new_position = position(cd_diff, c_line, d_line) + if valid_position?(new_position) + # If the line is still in the MR, we don't treat this as outdated. + { position: new_position, outdated: false } + else + # If the line is no longer in the MR, we unfortunately cannot show + # the current state on the CD diff, so we treat it as outdated. + ac_diff = ac_diffs.diff_file_with_new_path(c_path) + + { position: position(ac_diff, nil, c_line), outdated: true } + end + else + # If the line is still in D and not in C, it is still added. + { position: position(cd_diff, nil, d_line), outdated: false } + end + else + # If the line is no longer in D, it has been removed from the MR. + { position: position(bd_diff, b_line, nil), outdated: true } + end end - def trace_removed_line(old_position) - file_path = old_position.old_path + def trace_removed_line(ab_position) + a_path = ab_position.old_path + a_line = ab_position.old_line - return unless diff_base_to_base + ac_diff = ac_diffs.diff_file_with_old_path(a_path) - file_base_to_base = diff_base_to_base.find { |diff_file| diff_file.old_path == file_path } + c_path = ac_diff&.new_path || a_path + c_line = LineMapper.new(ac_diff).old_to_new(a_line) - file_path = file_base_to_base.old_path if file_base_to_base + if c_line + cd_diff = cd_diffs.diff_file_with_old_path(c_path) - old_line = LineMapper.new(file_base_to_base).old_to_new(old_position.old_line) + d_path = cd_diff&.new_path || c_path + d_line = LineMapper.new(cd_diff).old_to_new(c_line) - return unless old_line + if d_line + # If the line is still in C but also in D, it has turned from a + # removed line into an unchanged one. + bd_diff = bd_diffs.diff_file_with_new_path(d_path) - file_diff = new_diffs.find { |diff_file| diff_file.old_path == file_path } - return unless file_diff - - new_line = LineMapper.new(file_diff).old_to_new(old_line) + { position: position(bd_diff, nil, d_line), outdated: true } + else + # If the line is still in C and not in D, it is still removed. + { position: position(cd_diff, c_line, nil), outdated: false } + end + else + # If the line is no longer in C, it has been removed outside of the MR. + { position: position(ac_diff, a_line, nil), outdated: true } + end + end - [file_diff, old_line, new_line] + def trace_unchanged_line(ab_position) + a_path = ab_position.old_path + a_line = ab_position.old_line + b_path = ab_position.new_path + b_line = ab_position.new_line + + ac_diff = ac_diffs.diff_file_with_old_path(a_path) + + c_path = ac_diff&.new_path || a_path + c_line = LineMapper.new(ac_diff).old_to_new(a_line) + + bd_diff = bd_diffs.diff_file_with_old_path(b_path) + + d_line = LineMapper.new(bd_diff).old_to_new(b_line) + + cd_diff = cd_diffs.diff_file_with_old_path(c_path) + + if c_line && d_line + # If the line is still in C and D, it is still unchanged. + new_position = position(cd_diff, c_line, d_line) + if valid_position?(new_position) + # If the line is still in the MR, we don't treat this as outdated. + { position: new_position, outdated: false } + else + # If the line is no longer in the MR, we unfortunately cannot show + # the current state on the CD diff or any change on the BD diff, + # so we treat it as outdated. + { position: nil, outdated: true } + end + elsif d_line # && !c_line + # If the line is still in D but no longer in C, it has turned from + # an unchanged line into an added one. + # We don't treat this as outdated since the line is still in the MR. + { position: position(cd_diff, nil, d_line), outdated: false } + else # !d_line && (c_line || !c_line) + # If the line is no longer in D, it has turned from an unchanged line + # into a removed one. + { position: position(bd_diff, b_line, nil), outdated: true } + end end - def diff_base_to_base - @diff_base_to_base ||= diff_files(old_diff_refs.base_sha || old_diff_refs.start_sha, new_diff_refs.base_sha || new_diff_refs.start_sha) + def ac_diffs + @ac_diffs ||= compare( + old_diff_refs.base_sha || old_diff_refs.start_sha, + new_diff_refs.base_sha || new_diff_refs.start_sha, + straight: true + ) end - def diff_head_to_head - @diff_head_to_head ||= diff_files(old_diff_refs.head_sha, new_diff_refs.head_sha) + def bd_diffs + @bd_diffs ||= compare(old_diff_refs.head_sha, new_diff_refs.head_sha, straight: true) end - def new_diffs - @new_diffs ||= diff_files(new_diff_refs.start_sha, new_diff_refs.head_sha, use_base: true) + def cd_diffs + @cd_diffs ||= compare(new_diff_refs.start_sha, new_diff_refs.head_sha) end - def diff_files(start_sha, head_sha, use_base: false) - base_sha = self.repository.merge_base(start_sha, head_sha) || start_sha + def compare(start_sha, head_sha, straight: false) + compare = CompareService.new(project, head_sha).execute(project, start_sha, straight: straight) + compare.diffs(paths: paths) + end - diffs = self.repository.raw_repository.diff( - use_base ? base_sha : start_sha, - head_sha, - {}, - *paths - ) + def position(diff_file, old_line, new_line) + Position.new(diff_file: diff_file, old_line: old_line, new_line: new_line) + end - diffs.decorate! do |diff| - Gitlab::Diff::File.new(diff, repository: self.repository) - end + def valid_position?(position) + !!position.diff_line(project.repository) end end end diff --git a/lib/gitlab/ee_compat_check.rb b/lib/gitlab/ee_compat_check.rb index 496ee0bdcb0..38e27513281 100644 --- a/lib/gitlab/ee_compat_check.rb +++ b/lib/gitlab/ee_compat_check.rb @@ -131,10 +131,12 @@ module Gitlab def check_patch(patch_path) step("Checking out master", %w[git checkout master]) step("Resetting to latest master", %w[git reset --hard origin/master]) + step("Fetching CE/#{ce_branch}", %W[git fetch #{CE_REPO} #{ce_branch}]) step( "Checking if #{patch_path} applies cleanly to EE/master", %W[git apply --check --3way #{patch_path}] ) do |output, status| + puts output unless status.zero? @failed_files = output.lines.reduce([]) do |memo, line| if line.start_with?('error: patch failed:') @@ -310,6 +312,17 @@ module Gitlab Resolve them, stage the changes and commit them. + If the patch couldn't be applied cleanly, use the following command: + + # In the EE repo + $ git apply --reject path/to/#{ce_branch}.patch + + This option makes git apply the parts of the patch that are applicable, + and leave the rejected hunks in corresponding `.rej` files. + You can then resolve the conflicts highlighted in `.rej` by + manually applying the correct diff from the `.rej` file to the file with conflicts. + When finished, you can delete the `.rej` files and commit your changes. + ⚠️ Don't forget to push your branch to gitlab-ee: # In the EE repo diff --git a/lib/gitlab/etag_caching/router.rb b/lib/gitlab/etag_caching/router.rb index ba31041d0c1..2b0e19b338b 100644 --- a/lib/gitlab/etag_caching/router.rb +++ b/lib/gitlab/etag_caching/router.rb @@ -10,7 +10,7 @@ module Gitlab # - Ending in `issues/id`/realtime_changes` for the `issue_title` route USED_IN_ROUTES = %w[noteable issue notes issues realtime_changes commit pipelines merge_requests new].freeze - RESERVED_WORDS = DynamicPathValidator::WILDCARD_ROUTES - USED_IN_ROUTES + RESERVED_WORDS = Gitlab::Regex::ILLEGAL_PROJECT_PATH_WORDS - USED_IN_ROUTES RESERVED_WORDS_REGEX = Regexp.union(*RESERVED_WORDS) ROUTES = [ Gitlab::EtagCaching::Router::Route.new( @@ -38,7 +38,7 @@ module Gitlab 'project_pipelines' ), Gitlab::EtagCaching::Router::Route.new( - %r(^(?!.*(#{RESERVED_WORDS})).*/pipelines/\d+\.json\z), + %r(^(?!.*(#{RESERVED_WORDS_REGEX})).*/pipelines/\d+\.json\z), 'project_pipeline' ) ].freeze diff --git a/lib/gitlab/file_detector.rb b/lib/gitlab/file_detector.rb index c6a89597b23..a8cb7fc3fe7 100644 --- a/lib/gitlab/file_detector.rb +++ b/lib/gitlab/file_detector.rb @@ -5,17 +5,33 @@ module Gitlab # a README or a CONTRIBUTING file. module FileDetector PATTERNS = { + # Project files readme: /\Areadme/i, changelog: /\A(changelog|history|changes|news)/i, license: /\A(licen[sc]e|copying)(\..+|\z)/i, contributing: /\Acontributing/i, version: 'version', + avatar: /\Alogo\.(png|jpg|gif)\z/, + + # Configuration files gitignore: '.gitignore', koding: '.koding.yml', - gemfile: /\A(Gemfile|gems\.rb)\z/, gitlab_ci: '.gitlab-ci.yml', - avatar: /\Alogo\.(png|jpg|gif)\z/, - route_map: 'route-map.yml' + route_map: 'route-map.yml', + + # Dependency files + cartfile: /\ACartfile/, + composer_json: 'composer.json', + gemfile: /\A(Gemfile|gems\.rb)\z/, + gemfile_lock: 'Gemfile.lock', + gemspec: /\.gemspec\z/, + godeps_json: 'Godeps.json', + package_json: 'package.json', + podfile: 'Podfile', + podspec_json: /\.podspec\.json\z/, + podspec: /\.podspec\z/, + requirements_txt: /requirements\.txt\z/, + yarn_lock: 'yarn.lock' }.freeze # Returns an Array of file types based on the given paths. diff --git a/lib/gitlab/git/branch.rb b/lib/gitlab/git/branch.rb index 586380da94a..124526e4b59 100644 --- a/lib/gitlab/git/branch.rb +++ b/lib/gitlab/git/branch.rb @@ -1,6 +1,40 @@ module Gitlab module Git class Branch < Ref + def initialize(repository, name, target) + if target.is_a?(Gitaly::FindLocalBranchResponse) + target = target_from_gitaly_local_branches_response(target) + end + + super(repository, name, target) + end + + def target_from_gitaly_local_branches_response(response) + # Git messages have no encoding enforcements. However, in the UI we only + # handle UTF-8, so basically we cross our fingers that the message force + # encoded to UTF-8 is readable. + message = response.commit_subject.dup.force_encoding('UTF-8') + + # NOTE: For ease of parsing in Gitaly, we have only the subject of + # the commit and not the full message. This is ok, since all the + # code that uses `local_branches` only cares at most about the + # commit message. + # TODO: Once gitaly "takes over" Rugged consider separating the + # subject from the message to make it clearer when there's one + # available but not the other. + hash = { + id: response.commit_id, + message: message, + authored_date: Time.at(response.commit_author.date.seconds), + author_name: response.commit_author.name, + author_email: response.commit_author.email, + committed_date: Time.at(response.commit_committer.date.seconds), + committer_name: response.commit_committer.name, + committer_email: response.commit_committer.email + } + + Gitlab::Git::Commit.decorate(hash) + end end end end diff --git a/lib/gitlab/git/commit.rb b/lib/gitlab/git/commit.rb index f9a9b767ef4..297531db4cc 100644 --- a/lib/gitlab/git/commit.rb +++ b/lib/gitlab/git/commit.rb @@ -19,13 +19,7 @@ module Gitlab def ==(other) return false unless other.is_a?(Gitlab::Git::Commit) - methods = [:message, :parent_ids, :authored_date, :author_name, - :author_email, :committed_date, :committer_name, - :committer_email] - - methods.all? do |method| - send(method) == other.send(method) - end + id && id == other.id end class << self @@ -55,6 +49,7 @@ module Gitlab # Commit.find(repo, 'master') # def find(repo, commit_id = "HEAD") + return commit_id if commit_id.is_a?(Gitlab::Git::Commit) return decorate(commit_id) if commit_id.is_a?(Rugged::Commit) obj = if commit_id.is_a?(String) diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb index d380c5021ee..b9f1ac144b6 100644 --- a/lib/gitlab/git/repository.rb +++ b/lib/gitlab/git/repository.rb @@ -80,14 +80,16 @@ module Gitlab end # Returns an Array of Branches - def branches - rugged.branches.map do |rugged_ref| + def branches(filter: nil, sort_by: nil) + branches = rugged.branches.each(filter).map do |rugged_ref| begin Gitlab::Git::Branch.new(self, rugged_ref.name, rugged_ref.target) rescue Rugged::ReferenceError # Omit invalid branch end - end.compact.sort_by(&:name) + end.compact + + sort_branches(branches, sort_by) end def reload_rugged @@ -108,9 +110,15 @@ module Gitlab Gitlab::Git::Branch.new(self, rugged_ref.name, rugged_ref.target) if rugged_ref end - def local_branches - rugged.branches.each(:local).map do |branch| - Gitlab::Git::Branch.new(self, branch.name, branch.target) + def local_branches(sort_by: nil) + gitaly_migrate(:local_branches) do |is_enabled| + if is_enabled + gitaly_ref_client.local_branches(sort_by: sort_by).map do |gitaly_branch| + Gitlab::Git::Branch.new(self, gitaly_branch.name, gitaly_branch) + end + else + branches(filter: :local, sort_by: sort_by) + end end end @@ -471,19 +479,19 @@ module Gitlab # Returns a RefName for a given SHA def ref_name_for_sha(ref_path, sha) - # NOTE: This feature is intentionally disabled until - # https://gitlab.com/gitlab-org/gitaly/issues/180 is resolved - # gitaly_migrate(:find_ref_name) do |is_enabled| - # if is_enabled - # gitaly_ref_client.find_ref_name(sha, ref_path) - # else - args = %W(#{Gitlab.config.git.bin_path} for-each-ref --count=1 #{ref_path} --contains #{sha}) + raise ArgumentError, "sha can't be empty" unless sha.present? + + gitaly_migrate(:find_ref_name) do |is_enabled| + if is_enabled + gitaly_ref_client.find_ref_name(sha, ref_path) + else + args = %W(#{Gitlab.config.git.bin_path} for-each-ref --count=1 #{ref_path} --contains #{sha}) - # Not found -> ["", 0] - # Found -> ["b8d95eb4969eefacb0a58f6a28f6803f8070e7b9 commit\trefs/environments/production/77\n", 0] - Gitlab::Popen.popen(args, @path).first.split.last - # end - # end + # Not found -> ["", 0] + # Found -> ["b8d95eb4969eefacb0a58f6a28f6803f8070e7b9 commit\trefs/environments/production/77\n", 0] + Gitlab::Popen.popen(args, @path).first.split.last + end + end end # Returns commits collection @@ -1202,6 +1210,23 @@ module Gitlab diff.each_patch end + def sort_branches(branches, sort_by) + case sort_by + when 'name' + branches.sort_by(&:name) + when 'updated_desc' + branches.sort do |a, b| + b.dereferenced_target.committed_date <=> a.dereferenced_target.committed_date + end + when 'updated_asc' + branches.sort do |a, b| + a.dereferenced_target.committed_date <=> b.dereferenced_target.committed_date + end + else + branches + end + end + def gitaly_ref_client @gitaly_ref_client ||= Gitlab::GitalyClient::Ref.new(self) end diff --git a/lib/gitlab/gitaly_client/ref.rb b/lib/gitlab/gitaly_client/ref.rb index bf04e1fa50b..227fe45642e 100644 --- a/lib/gitlab/gitaly_client/ref.rb +++ b/lib/gitlab/gitaly_client/ref.rb @@ -28,7 +28,7 @@ module Gitlab def find_ref_name(commit_id, ref_prefix) request = Gitaly::FindRefNameRequest.new( - repository: @repository, + repository: @gitaly_repo, commit_id: commit_id, prefix: ref_prefix ) @@ -44,6 +44,12 @@ module Gitlab branch_names.count end + def local_branches(sort_by: nil) + request = Gitaly::FindLocalBranchesRequest.new(repository: @gitaly_repo) + request.sort_by = sort_by_param(sort_by) if sort_by + consume_branches_response(stub.find_local_branches(request)) + end + private def consume_refs_response(response, prefix:) @@ -51,6 +57,16 @@ module Gitlab r.names.map { |name| name.sub(/\A#{Regexp.escape(prefix)}/, '') } end end + + def sort_by_param(sort_by) + enum_value = Gitaly::FindLocalBranchesRequest::SortBy.resolve(sort_by.upcase.to_sym) + raise ArgumentError, "Invalid sort_by key `#{sort_by}`" unless enum_value + enum_value + end + + def consume_branches_response(response) + response.flat_map { |r| r.branches } + end end end end diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb index 26473f99bc3..21f2e6b6970 100644 --- a/lib/gitlab/gon_helper.rb +++ b/lib/gitlab/gon_helper.rb @@ -12,11 +12,13 @@ module Gitlab gon.katex_js_url = ActionController::Base.helpers.asset_path('katex.js') gon.sentry_dsn = current_application_settings.clientside_sentry_dsn if current_application_settings.clientside_sentry_enabled gon.gitlab_url = Gitlab.config.gitlab.url + gon.revision = Gitlab::REVISION if current_user gon.current_user_id = current_user.id gon.current_username = current_user.username gon.current_user_fullname = current_user.name + gon.current_user_avatar_url = current_user.avatar_url end end end diff --git a/lib/gitlab/regex.rb b/lib/gitlab/regex.rb index b7fef5dd068..f609850f8fa 100644 --- a/lib/gitlab/regex.rb +++ b/lib/gitlab/regex.rb @@ -2,6 +2,136 @@ module Gitlab module Regex extend self + # All routes that appear on the top level must be listed here. + # This will make sure that groups cannot be created with these names + # as these routes would be masked by the paths already in place. + # + # Example: + # /api/api-project + # + # the path `api` shouldn't be allowed because it would be masked by `api/*` + # + TOP_LEVEL_ROUTES = %w[ + - + .well-known + abuse_reports + admin + all + api + assets + autocomplete + ci + dashboard + explore + files + groups + health_check + help + hooks + import + invites + issues + jwt + koding + member + merge_requests + new + notes + notification_settings + oauth + profile + projects + public + repository + robots.txt + s + search + sent_notifications + services + snippets + teams + u + unicorn_test + unsubscribes + uploads + users + ].freeze + + # This list should contain all words following `/*namespace_id/:project_id` in + # routes that contain a second wildcard. + # + # Example: + # /*namespace_id/:project_id/badges/*ref/build + # + # If `badges` was allowed as a project/group name, we would not be able to access the + # `badges` route for those projects: + # + # Consider a namespace with path `foo/bar` and a project called `badges`. + # The route to the build badge would then be `/foo/bar/badges/badges/master/build.svg` + # + # When accessing this path the route would be matched to the `badges` path + # with the following params: + # - namespace_id: `foo` + # - project_id: `bar` + # - ref: `badges/master` + # + # Failing to find the project, this would result in a 404. + # + # By rejecting `badges` the router can _count_ on the fact that `badges` will + # be preceded by the `namespace/project`. + PROJECT_WILDCARD_ROUTES = %w[ + badges + blame + blob + builds + commits + create + create_dir + edit + environments/folders + files + find_file + gitlab-lfs/objects + info/lfs/objects + new + preview + raw + refs + tree + update + wikis + ].freeze + + # These are all the paths that follow `/groups/*id/ or `/groups/*group_id` + # We need to reject these because we have a `/groups/*id` page that is the same + # as the `/*id`. + # + # If we would allow a subgroup to be created with the name `activity` then + # this group would not be accessible through `/groups/parent/activity` since + # this would map to the activity-page of its parent. + GROUP_ROUTES = %w[ + activity + analytics + audit_events + avatar + edit + group_members + hooks + issues + labels + ldap + ldap_group_links + merge_requests + milestones + notification_setting + pipeline_quota + projects + subgroups + ].freeze + + ILLEGAL_PROJECT_PATH_WORDS = PROJECT_WILDCARD_ROUTES + ILLEGAL_GROUP_PATH_WORDS = (PROJECT_WILDCARD_ROUTES | GROUP_ROUTES).freeze + # The namespace regex is used in Javascript to validate usernames in the "Register" form. However, Javascript # does not support the negative lookbehind assertion (?<!) that disallows usernames ending in `.git` and `.atom`. # Since this is a non-trivial problem to solve in Javascript (heavily complicate the regex, modify view code to @@ -18,6 +148,29 @@ module Gitlab # So `group/subgroup` will match this regex but not NAMESPACE_REGEX_STR FULL_NAMESPACE_REGEX_STR = "(?:#{NAMESPACE_REGEX_STR}/)*#{NAMESPACE_REGEX_STR}".freeze + def root_namespace_route_regex + @root_namespace_route_regex ||= begin + illegal_words = Regexp.new(Regexp.union(TOP_LEVEL_ROUTES).source, Regexp::IGNORECASE) + + single_line_regexp %r{ + (?!(#{illegal_words})/) + #{NAMESPACE_REGEX_STR} + }x + end + end + + def root_namespace_path_regex + @root_namespace_path_regex ||= %r{\A#{root_namespace_route_regex}/\z} + end + + def full_namespace_path_regex + @full_namespace_path_regex ||= %r{\A#{namespace_route_regex}/\z} + end + + def full_project_path_regex + @full_project_path_regex ||= %r{\A#{namespace_route_regex}/#{project_route_regex}/\z} + end + def namespace_regex @namespace_regex ||= /\A#{NAMESPACE_REGEX_STR}\z/.freeze end @@ -27,7 +180,18 @@ module Gitlab end def namespace_route_regex - @namespace_route_regex ||= /#{NAMESPACE_REGEX_STR}/.freeze + @namespace_route_regex ||= begin + illegal_words = Regexp.new(Regexp.union(ILLEGAL_GROUP_PATH_WORDS).source, Regexp::IGNORECASE) + + single_line_regexp %r{ + #{root_namespace_route_regex} + (?: + / + (?!#{illegal_words}/) + #{NAMESPACE_REGEX_STR} + )* + }x + end end def namespace_regex_message @@ -53,15 +217,26 @@ module Gitlab end def project_path_regex - @project_path_regex ||= /\A#{PROJECT_REGEX_STR}\z/.freeze + @project_path_regex ||= %r{\A#{project_route_regex}/\z} end def project_route_regex - @project_route_regex ||= /#{PROJECT_REGEX_STR}/.freeze + @project_route_regex ||= begin + illegal_words = Regexp.new(Regexp.union(ILLEGAL_PROJECT_PATH_WORDS).source, Regexp::IGNORECASE) + + single_line_regexp %r{ + (?!(#{illegal_words})/) + #{PROJECT_REGEX_STR} + }x + end end def project_git_route_regex - @project_route_git_regex ||= /#{PATH_REGEX_STR}\.git/.freeze + @project_git_route_regex ||= /#{project_route_regex}\.git/.freeze + end + + def project_path_format_regex + @project_path_format_regex ||= /\A#{PROJECT_REGEX_STR}\z/.freeze end def project_path_regex_message @@ -86,7 +261,7 @@ module Gitlab # Valid git ref regex, see: # https://www.kernel.org/pub/software/scm/git/docs/git-check-ref-format.html - @git_reference_regex ||= %r{ + @git_reference_regex ||= single_line_regexp %r{ (?! (?# doesn't begins with) \/| (?# rule #6) @@ -102,7 +277,7 @@ module Gitlab (?# doesn't end with) (?<!\.lock) (?# rule #1) (?<![\/.]) (?# rule #6-7) - }x.freeze + }x end def container_registry_reference_regex @@ -140,5 +315,13 @@ module Gitlab "can contain only lowercase letters, digits, and '-'. " \ "Must start with a letter, and cannot end with '-'" end + + private + + def single_line_regexp(regex) + # Turns a multiline extended regexp into a single line one, + # beacuse `rake routes` breaks on multiline regexes. + Regexp.new(regex.source.gsub(/\(\?#.+?\)/, '').gsub(/\s*/, ''), regex.options ^ Regexp::EXTENDED).freeze + end end end diff --git a/lib/gitlab/usage_data.rb b/lib/gitlab/usage_data.rb index 4382cf7b12f..bcba2e3e1b6 100644 --- a/lib/gitlab/usage_data.rb +++ b/lib/gitlab/usage_data.rb @@ -40,7 +40,6 @@ module Gitlab projects_prometheus_active: PrometheusService.active.count, protected_branches: ProtectedBranch.count, releases: Release.count, - services: Service.where(active: true).count, snippets: Snippet.count, todos: Todo.count, uploads: Upload.count, diff --git a/lib/tasks/tokens.rake b/lib/tasks/tokens.rake index 95735f43802..ad1818ff1fa 100644 --- a/lib/tasks/tokens.rake +++ b/lib/tasks/tokens.rake @@ -11,6 +11,11 @@ namespace :tokens do reset_all_users_token(:reset_incoming_email_token!) end + desc "Reset all GitLab RSS tokens" + task reset_all_rss: :environment do + reset_all_users_token(:reset_rss_token!) + end + def reset_all_users_token(reset_token_method) TmpUser.find_in_batches do |batch| puts "Processing batch starting with user ID: #{batch.first.id}" @@ -35,4 +40,9 @@ class TmpUser < ActiveRecord::Base write_new_token(:incoming_email_token) save!(validate: false) end + + def reset_rss_token! + write_new_token(:rss_token) + save!(validate: false) + end end diff --git a/locale/es/gitlab.po b/locale/es/gitlab.po index c14ddd3b94c..b61846b9c7d 100644 --- a/locale/es/gitlab.po +++ b/locale/es/gitlab.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: gitlab 1.0.0\n" "Report-Msgid-Bugs-To: \n" -"PO-Revision-Date: 2017-05-04 19:24-0500\n" +"PO-Revision-Date: 2017-05-20 22:37-0500\n" "Language-Team: Spanish\n" "Language: es\n" "MIME-Version: 1.0\n" @@ -130,7 +130,7 @@ msgstr[0] "Mostrando %d evento" msgstr[1] "Mostrando %d eventos" msgid "The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request." -msgstr "La etapa de codificación muestra el tiempo desde el primer cambio hasta la creación de la solicitud de fusión. Los datos serán automáticamente incorporados aquí una vez creada tu primera solicitud de fusión." +msgstr "La etapa de desarrollo muestra el tiempo desde el primer cambio hasta la creación de la solicitud de fusión. Los datos serán automáticamente incorporados aquí una vez creada tu primera solicitud de fusión." msgid "The collection of events added to the data gathered for that stage." msgstr "La colección de eventos agregados a los datos recopilados para esa etapa." diff --git a/scripts/prepare_build.sh b/scripts/prepare_build.sh index c727a0e2d88..03de59f27ad 100644 --- a/scripts/prepare_build.sh +++ b/scripts/prepare_build.sh @@ -4,9 +4,22 @@ export SETUP_DB=${SETUP_DB:-true} export USE_BUNDLE_INSTALL=${USE_BUNDLE_INSTALL:-true} export BUNDLE_INSTALL_FLAGS="--without production --jobs $(nproc) --path vendor --retry 3 --quiet" +if [ "$USE_BUNDLE_INSTALL" != "false" ]; then + bundle install --clean $BUNDLE_INSTALL_FLAGS && bundle check +fi + +# Only install knapsack after bundle install! Otherwise oddly some native +# gems could not be found under some circumstance. No idea why, hours wasted. +retry gem install knapsack fog-aws mime-types + +cp config/resque.yml.example config/resque.yml +sed -i 's/localhost/redis/g' config/resque.yml + +cp config/gitlab.yml.example config/gitlab.yml + # Determine the database by looking at the job name. -# For example, we'll get pg if the job is `rspec pg 19 20` -export GITLAB_DATABASE=$(echo $CI_JOB_NAME | cut -f2 -d' ') +# For example, we'll get pg if the job is `rspec-pg 19 20` +export GITLAB_DATABASE=$(echo $CI_JOB_NAME | cut -f1 -d' ' | cut -f2 -d-) # This would make the default database postgresql, and we could also use # pg to mean postgresql. @@ -24,19 +37,6 @@ else # Assume it's mysql sed -i 's/# host:.*/host: mysql/g' config/database.yml fi -cp config/resque.yml.example config/resque.yml -sed -i 's/localhost/redis/g' config/resque.yml - -cp config/gitlab.yml.example config/gitlab.yml - -if [ "$USE_BUNDLE_INSTALL" != "false" ]; then - bundle install --clean $BUNDLE_INSTALL_FLAGS && bundle check -fi - -# Only install knapsack after bundle install! Otherwise oddly some native -# gems could not be found under some circumstance. No idea why, hours wasted. -retry gem install knapsack fog-aws mime-types - if [ "$SETUP_DB" != "false" ]; then bundle exec rake db:drop db:create db:schema:load db:migrate diff --git a/spec/bin/changelog_spec.rb b/spec/bin/changelog_spec.rb index 7f4298db59f..91aff0db7cc 100644 --- a/spec/bin/changelog_spec.rb +++ b/spec/bin/changelog_spec.rb @@ -46,9 +46,7 @@ describe 'bin/changelog' do it 'parses -h' do expect do - $stdout = StringIO.new - - described_class.parse(%w[foo -h bar]) + expect { described_class.parse(%w[foo -h bar]) }.to output.to_stdout end.to raise_error(SystemExit) end diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb index d40aae04fc3..3f99e2ff596 100644 --- a/spec/controllers/application_controller_spec.rb +++ b/spec/controllers/application_controller_spec.rb @@ -99,6 +99,42 @@ describe ApplicationController do end end + describe '#authenticate_user_from_rss_token' do + describe "authenticating a user from an RSS token" do + controller(described_class) do + def index + render text: 'authenticated' + end + end + + context "when the 'rss_token' param is populated with the RSS token" do + context 'when the request format is atom' do + it "logs the user in" do + get :index, rss_token: user.rss_token, format: :atom + expect(response).to have_http_status 200 + expect(response.body).to eq 'authenticated' + end + end + + context 'when the request format is not atom' do + it "doesn't log the user in" do + get :index, rss_token: user.rss_token + expect(response.status).not_to have_http_status 200 + expect(response.body).not_to eq 'authenticated' + end + end + end + + context "when the 'rss_token' param is populated with an invalid RSS token" do + it "doesn't log the user" do + get :index, rss_token: "token" + expect(response.status).not_to eq 200 + expect(response.body).not_to eq 'authenticated' + end + end + end + end + describe '#route_not_found' do it 'renders 404 if authenticated' do allow(controller).to receive(:current_user).and_return(user) diff --git a/spec/controllers/groups/milestones_controller_spec.rb b/spec/controllers/groups/milestones_controller_spec.rb index 7cf2996ffd0..f3263bc177d 100644 --- a/spec/controllers/groups/milestones_controller_spec.rb +++ b/spec/controllers/groups/milestones_controller_spec.rb @@ -21,7 +21,6 @@ describe Groups::MilestonesController do sign_in(user) group.add_owner(user) project.team << [user, :master] - controller.instance_variable_set(:@group, group) end it_behaves_like 'milestone tabs' @@ -29,7 +28,7 @@ describe Groups::MilestonesController do describe "#create" do it "creates group milestone with Chinese title" do post :create, - group_id: group.id, + group_id: group.to_param, milestone: { project_ids: [project.id, project2.id], title: title } expect(response).to redirect_to(group_milestone_path(group, title.to_slug.to_s, title: title)) @@ -37,9 +36,139 @@ describe Groups::MilestonesController do end it "redirects to new when there are no project ids" do - post :create, group_id: group.id, milestone: { title: title, project_ids: [""] } + post :create, group_id: group.to_param, milestone: { title: title, project_ids: [""] } expect(response).to render_template :new expect(assigns(:milestone).errors).not_to be_nil end end + + describe '#ensure_canonical_path' do + before do + sign_in(user) + end + + context 'for a GET request' do + context 'when requesting the canonical path' do + context 'non-show path' do + context 'with exactly matching casing' do + it 'does not redirect' do + get :index, group_id: group.to_param + + expect(response).not_to have_http_status(301) + end + end + + context 'with different casing' do + it 'redirects to the correct casing' do + get :index, group_id: group.to_param.upcase + + expect(response).to redirect_to(group_milestones_path(group.to_param)) + expect(controller).not_to set_flash[:notice] + end + end + end + + context 'show path' do + context 'with exactly matching casing' do + it 'does not redirect' do + get :show, group_id: group.to_param, id: title + + expect(response).not_to have_http_status(301) + end + end + + context 'with different casing' do + it 'redirects to the correct casing' do + get :show, group_id: group.to_param.upcase, id: title + + expect(response).to redirect_to(group_milestone_path(group.to_param, title)) + expect(controller).not_to set_flash[:notice] + end + end + end + end + + context 'when requesting a redirected path' do + let(:redirect_route) { group.redirect_routes.create(path: 'old-path') } + + it 'redirects to the canonical path' do + get :merge_requests, group_id: redirect_route.path, id: title + + expect(response).to redirect_to(merge_requests_group_milestone_path(group.to_param, title)) + expect(controller).to set_flash[:notice].to(group_moved_message(redirect_route, group)) + end + + context 'when the old group path is a substring of the scheme or host' do + let(:redirect_route) { group.redirect_routes.create(path: 'http') } + + it 'does not modify the requested host' do + get :merge_requests, group_id: redirect_route.path, id: title + + expect(response).to redirect_to(merge_requests_group_milestone_path(group.to_param, title)) + expect(controller).to set_flash[:notice].to(group_moved_message(redirect_route, group)) + end + end + + context 'when the old group path is substring of groups' do + # I.e. /groups/oups should not become /grfoo/oups + let(:redirect_route) { group.redirect_routes.create(path: 'oups') } + + it 'does not modify the /groups part of the path' do + get :merge_requests, group_id: redirect_route.path, id: title + + expect(response).to redirect_to(merge_requests_group_milestone_path(group.to_param, title)) + expect(controller).to set_flash[:notice].to(group_moved_message(redirect_route, group)) + end + end + + context 'when the old group path is substring of groups plus the new path' do + # I.e. /groups/oups/oup should not become /grfoos + let(:redirect_route) { group.redirect_routes.create(path: 'oups/oup') } + + it 'does not modify the /groups part of the path' do + get :merge_requests, group_id: redirect_route.path, id: title + + expect(response).to redirect_to(merge_requests_group_milestone_path(group.to_param, title)) + expect(controller).to set_flash[:notice].to(group_moved_message(redirect_route, group)) + end + end + end + end + end + + context 'for a non-GET request' do + context 'when requesting the canonical path with different casing' do + it 'does not 404' do + post :create, + group_id: group.to_param, + milestone: { project_ids: [project.id, project2.id], title: title } + + expect(response).not_to have_http_status(404) + end + + it 'does not redirect to the correct casing' do + post :create, + group_id: group.to_param, + milestone: { project_ids: [project.id, project2.id], title: title } + + expect(response).not_to have_http_status(301) + end + end + + context 'when requesting a redirected path' do + let(:redirect_route) { group.redirect_routes.create(path: 'old-path') } + + it 'returns not found' do + post :create, + group_id: redirect_route.path, + milestone: { project_ids: [project.id, project2.id], title: title } + + expect(response).to have_http_status(404) + end + end + end + + def group_moved_message(redirect_route, group) + "Group '#{redirect_route.path}' was moved to '#{group.full_path}'. Please update any links and bookmarks that may still have the old path." + end end diff --git a/spec/controllers/groups_controller_spec.rb b/spec/controllers/groups_controller_spec.rb index 15dae3231ca..4626f1ebc29 100644 --- a/spec/controllers/groups_controller_spec.rb +++ b/spec/controllers/groups_controller_spec.rb @@ -84,26 +84,6 @@ describe GroupsController do expect(assigns(:issues)).to eq [issue_2, issue_1] end end - - context 'when requesting the canonical path with different casing' do - it 'redirects to the correct casing' do - get :issues, id: group.to_param.upcase - - expect(response).to redirect_to(issues_group_path(group.to_param)) - expect(controller).not_to set_flash[:notice] - end - end - - context 'when requesting a redirected path' do - let(:redirect_route) { group.redirect_routes.create(path: 'old-path') } - - it 'redirects to the canonical path' do - get :issues, id: redirect_route.path - - expect(response).to redirect_to(issues_group_path(group.to_param)) - expect(controller).to set_flash[:notice].to(group_moved_message(redirect_route, group)) - end - end end describe 'GET #merge_requests' do @@ -129,26 +109,6 @@ describe GroupsController do expect(assigns(:merge_requests)).to eq [merge_request_2, merge_request_1] end end - - context 'when requesting the canonical path with different casing' do - it 'redirects to the correct casing' do - get :merge_requests, id: group.to_param.upcase - - expect(response).to redirect_to(merge_requests_group_path(group.to_param)) - expect(controller).not_to set_flash[:notice] - end - end - - context 'when requesting a redirected path' do - let(:redirect_route) { group.redirect_routes.create(path: 'old-path') } - - it 'redirects to the canonical path' do - get :merge_requests, id: redirect_route.path - - expect(response).to redirect_to(merge_requests_group_path(group.to_param)) - expect(controller).to set_flash[:notice].to(group_moved_message(redirect_route, group)) - end - end end describe 'DELETE #destroy' do @@ -178,30 +138,6 @@ describe GroupsController do expect(response).to redirect_to(root_path) end - - context 'when requesting the canonical path with different casing' do - it 'does not 404' do - delete :destroy, id: group.to_param.upcase - - expect(response).not_to have_http_status(404) - end - - it 'does not redirect to the correct casing' do - delete :destroy, id: group.to_param.upcase - - expect(response).not_to redirect_to(group_path(group.to_param)) - end - end - - context 'when requesting a redirected path' do - let(:redirect_route) { group.redirect_routes.create(path: 'old-path') } - - it 'returns not found' do - delete :destroy, id: redirect_route.path - - expect(response).to have_http_status(404) - end - end end end @@ -224,28 +160,197 @@ describe GroupsController do expect(assigns(:group).errors).not_to be_empty expect(assigns(:group).path).not_to eq('new_path') end + end + + describe '#ensure_canonical_path' do + before do + sign_in(user) + end + + context 'for a GET request' do + context 'when requesting groups at the root path' do + before do + allow(request).to receive(:original_fullpath).and_return("/#{group_full_path}") + get :show, id: group_full_path + end + + context 'when requesting the canonical path with different casing' do + let(:group_full_path) { group.to_param.upcase } + + it 'redirects to the correct casing' do + expect(response).to redirect_to(group) + expect(controller).not_to set_flash[:notice] + end + end + + context 'when requesting a redirected path' do + let(:redirect_route) { group.redirect_routes.create(path: 'old-path') } + let(:group_full_path) { redirect_route.path } + + it 'redirects to the canonical path' do + expect(response).to redirect_to(group) + expect(controller).to set_flash[:notice].to(group_moved_message(redirect_route, group)) + end + + context 'when the old group path is a substring of the scheme or host' do + let(:redirect_route) { group.redirect_routes.create(path: 'http') } + + it 'does not modify the requested host' do + expect(response).to redirect_to(group) + expect(controller).to set_flash[:notice].to(group_moved_message(redirect_route, group)) + end + end + + context 'when the old group path is substring of groups' do + # I.e. /groups/oups should not become /grfoo/oups + let(:redirect_route) { group.redirect_routes.create(path: 'oups') } + + it 'does not modify the /groups part of the path' do + expect(response).to redirect_to(group) + expect(controller).to set_flash[:notice].to(group_moved_message(redirect_route, group)) + end + end + end + end + + context 'when requesting groups under the /groups path' do + context 'when requesting the canonical path' do + context 'non-show path' do + context 'with exactly matching casing' do + it 'does not redirect' do + get :issues, id: group.to_param + + expect(response).not_to have_http_status(301) + end + end + + context 'with different casing' do + it 'redirects to the correct casing' do + get :issues, id: group.to_param.upcase + + expect(response).to redirect_to(issues_group_path(group.to_param)) + expect(controller).not_to set_flash[:notice] + end + end + end + + context 'show path' do + context 'with exactly matching casing' do + it 'does not redirect' do + get :show, id: group.to_param + + expect(response).not_to have_http_status(301) + end + end + + context 'with different casing' do + it 'redirects to the correct casing at the root path' do + get :show, id: group.to_param.upcase + + expect(response).to redirect_to(group) + expect(controller).not_to set_flash[:notice] + end + end + end + end + + context 'when requesting a redirected path' do + let(:redirect_route) { group.redirect_routes.create(path: 'old-path') } + + it 'redirects to the canonical path' do + get :issues, id: redirect_route.path + + expect(response).to redirect_to(issues_group_path(group.to_param)) + expect(controller).to set_flash[:notice].to(group_moved_message(redirect_route, group)) + end + + context 'when the old group path is a substring of the scheme or host' do + let(:redirect_route) { group.redirect_routes.create(path: 'http') } + + it 'does not modify the requested host' do + get :issues, id: redirect_route.path + + expect(response).to redirect_to(issues_group_path(group.to_param)) + expect(controller).to set_flash[:notice].to(group_moved_message(redirect_route, group)) + end + end + + context 'when the old group path is substring of groups' do + # I.e. /groups/oups should not become /grfoo/oups + let(:redirect_route) { group.redirect_routes.create(path: 'oups') } + + it 'does not modify the /groups part of the path' do + get :issues, id: redirect_route.path + + expect(response).to redirect_to(issues_group_path(group.to_param)) + expect(controller).to set_flash[:notice].to(group_moved_message(redirect_route, group)) + end + end + + context 'when the old group path is substring of groups plus the new path' do + # I.e. /groups/oups/oup should not become /grfoos + let(:redirect_route) { group.redirect_routes.create(path: 'oups/oup') } + + it 'does not modify the /groups part of the path' do + get :issues, id: redirect_route.path + + expect(response).to redirect_to(issues_group_path(group.to_param)) + expect(controller).to set_flash[:notice].to(group_moved_message(redirect_route, group)) + end + end + end + end + end - context 'when requesting the canonical path with different casing' do - it 'does not 404' do - post :update, id: group.to_param.upcase, group: { path: 'new_path' } + context 'for a POST request' do + context 'when requesting the canonical path with different casing' do + it 'does not 404' do + post :update, id: group.to_param.upcase, group: { path: 'new_path' } + + expect(response).not_to have_http_status(404) + end + + it 'does not redirect to the correct casing' do + post :update, id: group.to_param.upcase, group: { path: 'new_path' } - expect(response).not_to have_http_status(404) + expect(response).not_to have_http_status(301) + end end - it 'does not redirect to the correct casing' do - post :update, id: group.to_param.upcase, group: { path: 'new_path' } + context 'when requesting a redirected path' do + let(:redirect_route) { group.redirect_routes.create(path: 'old-path') } - expect(response).not_to redirect_to(group_path(group.to_param)) + it 'returns not found' do + post :update, id: redirect_route.path, group: { path: 'new_path' } + + expect(response).to have_http_status(404) + end end end - context 'when requesting a redirected path' do - let(:redirect_route) { group.redirect_routes.create(path: 'old-path') } + context 'for a DELETE request' do + context 'when requesting the canonical path with different casing' do + it 'does not 404' do + delete :destroy, id: group.to_param.upcase + + expect(response).not_to have_http_status(404) + end + + it 'does not redirect to the correct casing' do + delete :destroy, id: group.to_param.upcase - it 'returns not found' do - post :update, id: redirect_route.path, group: { path: 'new_path' } + expect(response).not_to have_http_status(301) + end + end - expect(response).to have_http_status(404) + context 'when requesting a redirected path' do + let(:redirect_route) { group.redirect_routes.create(path: 'old-path') } + + it 'returns not found' do + delete :destroy, id: redirect_route.path + + expect(response).to have_http_status(404) + end end end end diff --git a/spec/controllers/import/gitlab_controller_spec.rb b/spec/controllers/import/gitlab_controller_spec.rb index 2dbb89219d0..3270ea059fa 100644 --- a/spec/controllers/import/gitlab_controller_spec.rb +++ b/spec/controllers/import/gitlab_controller_spec.rb @@ -174,7 +174,7 @@ describe Import::GitlabController do end end end - + context 'user has chosen an existing nested namespace for the project' do let(:parent_namespace) { create(:namespace, name: 'foo', owner: user) } let(:nested_namespace) { create(:namespace, name: 'bar', parent: parent_namespace, owner: user) } diff --git a/spec/controllers/projects/builds_controller_spec.rb b/spec/controllers/projects/builds_controller_spec.rb index 3ce23c17cdc..f41503fd34e 100644 --- a/spec/controllers/projects/builds_controller_spec.rb +++ b/spec/controllers/projects/builds_controller_spec.rb @@ -144,6 +144,8 @@ describe Projects::BuildsController do it 'returns a trace' do expect(response).to have_http_status(:ok) + expect(json_response['id']).to eq build.id + expect(json_response['status']).to eq build.status expect(json_response['html']).to eq('BUILD TRACE') end end @@ -153,10 +155,23 @@ describe Projects::BuildsController do it 'returns no traces' do expect(response).to have_http_status(:ok) + expect(json_response['id']).to eq build.id + expect(json_response['status']).to eq build.status expect(json_response['html']).to be_nil end end + context 'when build has a trace with ANSI sequence and Unicode' do + let(:build) { create(:ci_build, :unicode_trace, pipeline: pipeline) } + + it 'returns a trace with Unicode' do + expect(response).to have_http_status(:ok) + expect(json_response['id']).to eq build.id + expect(json_response['status']).to eq build.status + expect(json_response['html']).to include("ヾ(´༎ຶД༎ຶ`)ノ") + end + end + def get_trace get :trace, namespace_id: project.namespace, project_id: project, @@ -185,48 +200,6 @@ describe Projects::BuildsController do end end - describe 'GET trace.json' do - let(:pipeline) { create(:ci_pipeline, project: project) } - let(:build) { create(:ci_build, pipeline: pipeline) } - let(:user) { create(:user) } - - context 'when user is logged in as developer' do - before do - project.add_developer(user) - sign_in(user) - - get_trace - end - - it 'traces build log' do - expect(response).to have_http_status(:ok) - expect(json_response['id']).to eq build.id - expect(json_response['status']).to eq build.status - end - end - - context 'when user is logged in as non member' do - before do - sign_in(user) - - get_trace - end - - it 'traces build log' do - expect(response).to have_http_status(:ok) - expect(json_response['id']).to eq build.id - expect(json_response['status']).to eq build.status - end - end - - def get_trace - get :trace, namespace_id: project.namespace, - project_id: project, - id: build.id, - format: :json - end - end - describe 'POST retry' do before do project.add_developer(user) diff --git a/spec/controllers/projects/environments_controller_spec.rb b/spec/controllers/projects/environments_controller_spec.rb index c0f8c36a018..20f99b209eb 100644 --- a/spec/controllers/projects/environments_controller_spec.rb +++ b/spec/controllers/projects/environments_controller_spec.rb @@ -1,25 +1,25 @@ require 'spec_helper' describe Projects::EnvironmentsController do - let(:user) { create(:user) } - let(:project) { create(:empty_project) } + set(:user) { create(:user) } + set(:project) { create(:empty_project) } - let(:environment) do + set(:environment) do create(:environment, name: 'production', project: project) end before do - project.team << [user, :master] + project.add_master(user) sign_in(user) end describe 'GET index' do - context 'when standardrequest has been made' do + context 'when a request for the HTML is made' do it 'responds with status code 200' do get :index, environment_params - expect(response).to be_ok + expect(response).to have_http_status(:ok) end end @@ -84,6 +84,9 @@ describe Projects::EnvironmentsController do create(:environment, project: project, name: 'staging-1.0/review', state: :available) + create(:environment, project: project, + name: 'staging-1.0/zzz', + state: :available) end context 'when using default format' do @@ -98,7 +101,7 @@ describe Projects::EnvironmentsController do end context 'when using JSON format' do - it 'responds with JSON' do + it 'sorts the subfolders lexicographically' do get :folder, namespace_id: project.namespace, project_id: project, id: 'staging-1.0', @@ -108,6 +111,8 @@ describe Projects::EnvironmentsController do expect(response).not_to render_template 'folder' expect(json_response['environments'][0]) .to include('name' => 'staging-1.0/review') + expect(json_response['environments'][1]) + .to include('name' => 'staging-1.0/zzz') end end end diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb index 1f79e72495a..04afd07c59e 100644 --- a/spec/controllers/projects/issues_controller_spec.rb +++ b/spec/controllers/projects/issues_controller_spec.rb @@ -156,6 +156,32 @@ describe Projects::IssuesController do end end + describe 'Redirect after sign in' do + context 'with an AJAX request' do + it 'does not store the visited URL' do + xhr :get, + :show, + format: :json, + namespace_id: project.namespace, + project_id: project, + id: issue.iid + + expect(session['user_return_to']).to be_blank + end + end + + context 'without an AJAX request' do + it 'stores the visited URL' do + get :show, + namespace_id: project.namespace.to_param, + project_id: project, + id: issue.iid + + expect(session['user_return_to']).to eq("/#{project.namespace.to_param}/#{project.to_param}/issues/#{issue.iid}") + end + end + end + describe 'PUT #update' do before do sign_in(user) diff --git a/spec/controllers/projects/labels_controller_spec.rb b/spec/controllers/projects/labels_controller_spec.rb index 05999431d8f..130b0b744b5 100644 --- a/spec/controllers/projects/labels_controller_spec.rb +++ b/spec/controllers/projects/labels_controller_spec.rb @@ -157,4 +157,74 @@ describe Projects::LabelsController do end end end + + describe '#ensure_canonical_path' do + before do + sign_in(user) + end + + context 'for a GET request' do + context 'when requesting the canonical path' do + context 'non-show path' do + context 'with exactly matching casing' do + it 'does not redirect' do + get :index, namespace_id: project.namespace, project_id: project.to_param + + expect(response).not_to have_http_status(301) + end + end + + context 'with different casing' do + it 'redirects to the correct casing' do + get :index, namespace_id: project.namespace, project_id: project.to_param.upcase + + expect(response).to redirect_to(namespace_project_labels_path(project.namespace, project)) + expect(controller).not_to set_flash[:notice] + end + end + end + end + + context 'when requesting a redirected path' do + let!(:redirect_route) { project.redirect_routes.create(path: project.full_path + 'old') } + + it 'redirects to the canonical path' do + get :index, namespace_id: project.namespace, project_id: project.to_param + 'old' + + expect(response).to redirect_to(namespace_project_labels_path(project.namespace, project)) + expect(controller).to set_flash[:notice].to(project_moved_message(redirect_route, project)) + end + end + end + end + + context 'for a non-GET request' do + context 'when requesting the canonical path with different casing' do + it 'does not 404' do + post :generate, namespace_id: project.namespace, project_id: project + + expect(response).not_to have_http_status(404) + end + + it 'does not redirect to the correct casing' do + post :generate, namespace_id: project.namespace, project_id: project + + expect(response).not_to have_http_status(301) + end + end + + context 'when requesting a redirected path' do + let!(:redirect_route) { project.redirect_routes.create(path: project.full_path + 'old') } + + it 'returns not found' do + post :generate, namespace_id: project.namespace, project_id: project.to_param + 'old' + + expect(response).to have_http_status(404) + end + end + end + + def project_moved_message(redirect_route, project) + "Project '#{redirect_route.path}' was moved to '#{project.full_path}'. Please update any links and bookmarks that may still have the old path." + end end diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb index 0b3492a8fed..587a5820c6f 100644 --- a/spec/controllers/projects/merge_requests_controller_spec.rb +++ b/spec/controllers/projects/merge_requests_controller_spec.rb @@ -119,6 +119,18 @@ describe Projects::MergeRequestsController do expect(response).to match_response_schema('entities/merge_request') end end + + context 'number of queries' do + it 'verifies number of queries' do + # pre-create objects + merge_request + + recorded = ActiveRecord::QueryRecorder.new { go(format: :json) } + + expect(recorded.count).to be_within(5).of(50) + expect(recorded.cached_count).to eq(0) + end + end end describe "as diff" do @@ -345,8 +357,7 @@ describe Projects::MergeRequestsController do end before do - pipeline = create(:ci_empty_pipeline, project: project, sha: merge_request.diff_head_sha, ref: merge_request.source_branch) - merge_request.update(head_pipeline: pipeline) + create(:ci_empty_pipeline, project: project, sha: merge_request.diff_head_sha, ref: merge_request.source_branch, head_pipeline_of: merge_request) end it 'returns :merge_when_pipeline_succeeds' do @@ -1161,13 +1172,13 @@ describe Projects::MergeRequestsController do let!(:pipeline) do create(:ci_pipeline, project: merge_request.source_project, ref: merge_request.source_branch, - sha: merge_request.diff_head_sha) + sha: merge_request.diff_head_sha, + head_pipeline_of: merge_request) end let(:status) { pipeline.detailed_status(double('user')) } before do - merge_request.update(head_pipeline: pipeline) get_pipeline_status end diff --git a/spec/controllers/projects/pipelines_controller_spec.rb b/spec/controllers/projects/pipelines_controller_spec.rb index fb4a4721a58..c880da1e36a 100644 --- a/spec/controllers/projects/pipelines_controller_spec.rb +++ b/spec/controllers/projects/pipelines_controller_spec.rb @@ -38,7 +38,7 @@ describe Projects::PipelinesController do end describe 'GET show JSON' do - let!(:pipeline) { create(:ci_pipeline_with_one_job, project: project) } + let(:pipeline) { create(:ci_pipeline_with_one_job, project: project) } it 'returns the pipeline' do get_pipeline_json @@ -49,20 +49,48 @@ describe Projects::PipelinesController do expect(json_response['details']).to have_key 'stages' end - context 'when the pipeline has multiple jobs' do + context 'when the pipeline has multiple stages and groups' do + before do + RequestStore.begin! + + create_build('build', 0, 'build') + create_build('test', 1, 'rspec 0') + create_build('deploy', 2, 'production') + create_build('post deploy', 3, 'pages 0') + end + + after do + RequestStore.end! + RequestStore.clear! + end + + let(:project) { create(:project) } + let(:pipeline) do + create(:ci_empty_pipeline, project: project, user: user, sha: project.commit.id) + end + it 'does not perform N + 1 queries' do control_count = ActiveRecord::QueryRecorder.new { get_pipeline_json }.count - create(:ci_build, pipeline: pipeline) + create_build('test', 1, 'rspec 1') + create_build('test', 1, 'spinach 0') + create_build('test', 1, 'spinach 1') + create_build('test', 1, 'audit') + create_build('post deploy', 3, 'pages 1') + create_build('post deploy', 3, 'pages 2') - # The plus 2 is needed to group and sort - expect { get_pipeline_json }.not_to exceed_query_limit(control_count + 2) + new_count = ActiveRecord::QueryRecorder.new { get_pipeline_json }.count + expect(new_count).to be_within(12).of(control_count) end end def get_pipeline_json get :show, namespace_id: project.namespace, project_id: project, id: pipeline, format: :json end + + def create_build(stage, stage_idx, name) + create(:ci_build, pipeline: pipeline, stage: stage, stage_idx: stage_idx, name: name) + end end describe 'GET stages.json' do diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb index e230944d52e..4f6fc6691be 100644 --- a/spec/controllers/projects_controller_spec.rb +++ b/spec/controllers/projects_controller_spec.rb @@ -169,27 +169,6 @@ describe ProjectsController do end end - context "when requested with case sensitive namespace and project path" do - context "when there is a match with the same casing" do - it "loads the project" do - get :show, namespace_id: public_project.namespace, id: public_project - - expect(assigns(:project)).to eq(public_project) - expect(response).to have_http_status(200) - end - end - - context "when there is a match with different casing" do - it "redirects to the normalized path" do - get :show, namespace_id: public_project.namespace, id: public_project.path.upcase - - expect(assigns(:project)).to eq(public_project) - expect(response).to redirect_to("/#{public_project.full_path}") - expect(controller).not_to set_flash[:notice] - end - end - end - context "when the url contains .atom" do let(:public_project_with_dot_atom) { build(:empty_project, :public, name: 'my.atom', path: 'my.atom') } @@ -219,17 +198,6 @@ describe ProjectsController do expect(response).to redirect_to(namespace_project_path) end end - - context 'when requesting a redirected path' do - let!(:redirect_route) { public_project.redirect_routes.create!(path: "foo/bar") } - - it 'redirects to the canonical path' do - get :show, namespace_id: 'foo', id: 'bar' - - expect(response).to redirect_to(public_project) - expect(controller).to set_flash[:notice].to(project_moved_message(redirect_route, public_project)) - end - end end describe "#update" do @@ -256,32 +224,48 @@ describe ProjectsController do expect(assigns(:repository).path).to eq(project.repository.path) expect(response).to have_http_status(302) end + end - context 'when requesting the canonical path' do - it "is case-insensitive" do - controller.instance_variable_set(:@project, project) + describe '#transfer' do + render_views - put :update, - namespace_id: 'FOo', - id: 'baR', - project: project_params + let(:project) { create(:project) } + let(:admin) { create(:admin) } + let(:new_namespace) { create(:namespace) } - expect(project.repository.path).to include(new_path) - expect(assigns(:repository).path).to eq(project.repository.path) - expect(response).to have_http_status(302) - end + it 'updates namespace' do + sign_in(admin) + + put :transfer, + namespace_id: project.namespace.path, + new_namespace_id: new_namespace.id, + id: project.path, + format: :js + + project.reload + + expect(project.namespace).to eq(new_namespace) + expect(response).to have_http_status(200) end - context 'when requesting a redirected path' do - let!(:redirect_route) { project.redirect_routes.create!(path: "foo/bar") } + context 'when new namespace is empty' do + it 'project namespace is not changed' do + controller.instance_variable_set(:@project, project) + sign_in(admin) + + old_namespace = project.namespace - it 'returns not found' do - put :update, - namespace_id: 'foo', - id: 'bar', - project: project_params + put :transfer, + namespace_id: old_namespace.path, + new_namespace_id: nil, + id: project.path, + format: :js - expect(response).to have_http_status(404) + project.reload + + expect(project.namespace).to eq(old_namespace) + expect(response).to have_http_status(200) + expect(flash[:alert]).to eq 'Please select a new namespace for your project.' end end end @@ -319,31 +303,6 @@ describe ProjectsController do expect(merge_request.reload.state).to eq('closed') end end - - context 'when requesting the canonical path' do - it "is case-insensitive" do - controller.instance_variable_set(:@project, project) - sign_in(admin) - - orig_id = project.id - delete :destroy, namespace_id: project.namespace, id: project.path.upcase - - expect { Project.find(orig_id) }.to raise_error(ActiveRecord::RecordNotFound) - expect(response).to have_http_status(302) - expect(response).to redirect_to(dashboard_projects_path) - end - end - - context 'when requesting a redirected path' do - let!(:redirect_route) { project.redirect_routes.create!(path: "foo/bar") } - - it 'returns not found' do - sign_in(admin) - delete :destroy, namespace_id: 'foo', id: 'bar' - - expect(response).to have_http_status(404) - end - end end describe 'PUT #new_issue_address' do @@ -465,17 +424,6 @@ describe ProjectsController do expect(parsed_body["Tags"]).to include("v1.0.0") expect(parsed_body["Commits"]).to include("123456") end - - context 'when requesting a redirected path' do - let!(:redirect_route) { public_project.redirect_routes.create!(path: "foo/bar") } - - it 'redirects to the canonical path' do - get :refs, namespace_id: 'foo', id: 'bar' - - expect(response).to redirect_to(refs_namespace_project_path(namespace_id: public_project.namespace, id: public_project)) - expect(controller).to set_flash[:notice].to(project_moved_message(redirect_route, public_project)) - end - end end describe 'POST #preview_markdown' do @@ -488,6 +436,109 @@ describe ProjectsController do end end + describe '#ensure_canonical_path' do + before do + sign_in(user) + end + + context 'for a GET request' do + context 'when requesting the canonical path' do + context "with exactly matching casing" do + it "loads the project" do + get :show, namespace_id: public_project.namespace, id: public_project + + expect(assigns(:project)).to eq(public_project) + expect(response).to have_http_status(200) + end + end + + context "with different casing" do + it "redirects to the normalized path" do + get :show, namespace_id: public_project.namespace, id: public_project.path.upcase + + expect(assigns(:project)).to eq(public_project) + expect(response).to redirect_to("/#{public_project.full_path}") + expect(controller).not_to set_flash[:notice] + end + end + end + + context 'when requesting a redirected path' do + let!(:redirect_route) { public_project.redirect_routes.create!(path: "foo/bar") } + + it 'redirects to the canonical path' do + get :show, namespace_id: 'foo', id: 'bar' + + expect(response).to redirect_to(public_project) + expect(controller).to set_flash[:notice].to(project_moved_message(redirect_route, public_project)) + end + + it 'redirects to the canonical path (testing non-show action)' do + get :refs, namespace_id: 'foo', id: 'bar' + + expect(response).to redirect_to(refs_namespace_project_path(namespace_id: public_project.namespace, id: public_project)) + expect(controller).to set_flash[:notice].to(project_moved_message(redirect_route, public_project)) + end + end + end + + context 'for a POST request' do + context 'when requesting the canonical path with different casing' do + it 'does not 404' do + post :toggle_star, namespace_id: public_project.namespace, id: public_project.path.upcase + + expect(response).not_to have_http_status(404) + end + + it 'does not redirect to the correct casing' do + post :toggle_star, namespace_id: public_project.namespace, id: public_project.path.upcase + + expect(response).not_to have_http_status(301) + end + end + + context 'when requesting a redirected path' do + let!(:redirect_route) { public_project.redirect_routes.create!(path: "foo/bar") } + + it 'returns not found' do + post :toggle_star, namespace_id: 'foo', id: 'bar' + + expect(response).to have_http_status(404) + end + end + end + + context 'for a DELETE request' do + before do + sign_in(create(:admin)) + end + + context 'when requesting the canonical path with different casing' do + it 'does not 404' do + delete :destroy, namespace_id: project.namespace, id: project.path.upcase + + expect(response).not_to have_http_status(404) + end + + it 'does not redirect to the correct casing' do + delete :destroy, namespace_id: project.namespace, id: project.path.upcase + + expect(response).not_to have_http_status(301) + end + end + + context 'when requesting a redirected path' do + let!(:redirect_route) { project.redirect_routes.create!(path: "foo/bar") } + + it 'returns not found' do + delete :destroy, namespace_id: 'foo', id: 'bar' + + expect(response).to have_http_status(404) + end + end + end + end + def project_moved_message(redirect_route, project) "Project '#{redirect_route.path}' was moved to '#{project.full_path}'. Please update any links and bookmarks that may still have the old path." end diff --git a/spec/controllers/uploads_controller_spec.rb b/spec/controllers/uploads_controller_spec.rb index 7dedfe160a6..8000c9dec61 100644 --- a/spec/controllers/uploads_controller_spec.rb +++ b/spec/controllers/uploads_controller_spec.rb @@ -473,5 +473,45 @@ describe UploadsController do end end end + + context 'Appearance' do + context 'when viewing a custom header logo' do + let!(:appearance) { create :appearance, header_logo: fixture_file_upload(Rails.root.join('spec/fixtures/dk.png'), 'image/png') } + + context 'when not signed in' do + it 'responds with status 200' do + get :show, model: 'appearance', mounted_as: 'header_logo', id: appearance.id, filename: 'dk.png' + + expect(response).to have_http_status(200) + end + + it_behaves_like 'content not cached without revalidation' do + subject do + get :show, model: 'appearance', mounted_as: 'header_logo', id: appearance.id, filename: 'dk.png' + response + end + end + end + end + + context 'when viewing a custom logo' do + let!(:appearance) { create :appearance, logo: fixture_file_upload(Rails.root.join('spec/fixtures/dk.png'), 'image/png') } + + context 'when not signed in' do + it 'responds with status 200' do + get :show, model: 'appearance', mounted_as: 'logo', id: appearance.id, filename: 'dk.png' + + expect(response).to have_http_status(200) + end + + it_behaves_like 'content not cached without revalidation' do + subject do + get :show, model: 'appearance', mounted_as: 'logo', id: appearance.id, filename: 'dk.png' + response + end + end + end + end + end end end diff --git a/spec/controllers/users_controller_spec.rb b/spec/controllers/users_controller_spec.rb index 1d61719f1d0..d33e2ba1e53 100644 --- a/spec/controllers/users_controller_spec.rb +++ b/spec/controllers/users_controller_spec.rb @@ -53,40 +53,6 @@ describe UsersController do end end - context 'when requesting the canonical path' do - let(:user) { create(:user, username: 'CamelCaseUser') } - - before { sign_in(user) } - - context 'with exactly matching casing' do - it 'responds with success' do - get :show, username: user.username - - expect(response).to be_success - end - end - - context 'with different casing' do - it 'redirects to the correct casing' do - get :show, username: user.username.downcase - - expect(response).to redirect_to(user) - expect(controller).not_to set_flash[:notice] - end - end - end - - context 'when requesting a redirected path' do - let(:redirect_route) { user.namespace.redirect_routes.create(path: 'old-username') } - - it 'redirects to the canonical path' do - get :show, username: redirect_route.path - - expect(response).to redirect_to(user) - expect(controller).to set_flash[:notice].to(user_moved_message(redirect_route, user)) - end - end - context 'when a user by that username does not exist' do context 'when logged out' do it 'redirects to login page' do @@ -131,40 +97,6 @@ describe UsersController do expect(assigns(:contributions_calendar).projects.count).to eq(2) end end - - context 'when requesting the canonical path' do - let(:user) { create(:user, username: 'CamelCaseUser') } - - before { sign_in(user) } - - context 'with exactly matching casing' do - it 'responds with success' do - get :calendar, username: user.username - - expect(response).to be_success - end - end - - context 'with different casing' do - it 'redirects to the correct casing' do - get :calendar, username: user.username.downcase - - expect(response).to redirect_to(user_calendar_path(user)) - expect(controller).not_to set_flash[:notice] - end - end - end - - context 'when requesting a redirected path' do - let(:redirect_route) { user.namespace.redirect_routes.create(path: 'old-username') } - - it 'redirects to the canonical path' do - get :calendar, username: redirect_route.path - - expect(response).to redirect_to(user_calendar_path(user)) - expect(controller).to set_flash[:notice].to(user_moved_message(redirect_route, user)) - end - end end describe 'GET #calendar_activities' do @@ -187,38 +119,6 @@ describe UsersController do get :calendar_activities, username: user.username expect(response).to render_template('calendar_activities') end - - context 'when requesting the canonical path' do - let(:user) { create(:user, username: 'CamelCaseUser') } - - context 'with exactly matching casing' do - it 'responds with success' do - get :calendar_activities, username: user.username - - expect(response).to be_success - end - end - - context 'with different casing' do - it 'redirects to the correct casing' do - get :calendar_activities, username: user.username.downcase - - expect(response).to redirect_to(user_calendar_activities_path(user)) - expect(controller).not_to set_flash[:notice] - end - end - end - - context 'when requesting a redirected path' do - let(:redirect_route) { user.namespace.redirect_routes.create(path: 'old-username') } - - it 'redirects to the canonical path' do - get :calendar_activities, username: redirect_route.path - - expect(response).to redirect_to(user_calendar_activities_path(user)) - expect(controller).to set_flash[:notice].to(user_moved_message(redirect_route, user)) - end - end end describe 'GET #snippets' do @@ -241,38 +141,6 @@ describe UsersController do expect(JSON.parse(response.body)).to have_key('html') end end - - context 'when requesting the canonical path' do - let(:user) { create(:user, username: 'CamelCaseUser') } - - context 'with exactly matching casing' do - it 'responds with success' do - get :snippets, username: user.username - - expect(response).to be_success - end - end - - context 'with different casing' do - it 'redirects to the correct casing' do - get :snippets, username: user.username.downcase - - expect(response).to redirect_to(user_snippets_path(user)) - expect(controller).not_to set_flash[:notice] - end - end - end - - context 'when requesting a redirected path' do - let(:redirect_route) { user.namespace.redirect_routes.create(path: 'old-username') } - - it 'redirects to the canonical path' do - get :snippets, username: redirect_route.path - - expect(response).to redirect_to(user_snippets_path(user)) - expect(controller).to set_flash[:notice].to(user_moved_message(redirect_route, user)) - end - end end describe 'GET #exists' do @@ -321,6 +189,127 @@ describe UsersController do end end + describe '#ensure_canonical_path' do + before do + sign_in(user) + end + + context 'for a GET request' do + context 'when requesting users at the root path' do + context 'when requesting the canonical path' do + let(:user) { create(:user, username: 'CamelCaseUser') } + + context 'with exactly matching casing' do + it 'responds with success' do + get :show, username: user.username + + expect(response).to be_success + end + end + + context 'with different casing' do + it 'redirects to the correct casing' do + get :show, username: user.username.downcase + + expect(response).to redirect_to(user) + expect(controller).not_to set_flash[:notice] + end + end + end + + context 'when requesting a redirected path' do + let(:redirect_route) { user.namespace.redirect_routes.create(path: 'old-path') } + + it 'redirects to the canonical path' do + get :show, username: redirect_route.path + + expect(response).to redirect_to(user) + expect(controller).to set_flash[:notice].to(user_moved_message(redirect_route, user)) + end + + context 'when the old path is a substring of the scheme or host' do + let(:redirect_route) { user.namespace.redirect_routes.create(path: 'http') } + + it 'does not modify the requested host' do + get :show, username: redirect_route.path + + expect(response).to redirect_to(user) + expect(controller).to set_flash[:notice].to(user_moved_message(redirect_route, user)) + end + end + + context 'when the old path is substring of users' do + let(:redirect_route) { user.namespace.redirect_routes.create(path: 'ser') } + + it 'redirects to the canonical path' do + get :show, username: redirect_route.path + + expect(response).to redirect_to(user) + expect(controller).to set_flash[:notice].to(user_moved_message(redirect_route, user)) + end + end + end + end + + context 'when requesting users under the /users path' do + context 'when requesting the canonical path' do + let(:user) { create(:user, username: 'CamelCaseUser') } + + context 'with exactly matching casing' do + it 'responds with success' do + get :projects, username: user.username + + expect(response).to be_success + end + end + + context 'with different casing' do + it 'redirects to the correct casing' do + get :projects, username: user.username.downcase + + expect(response).to redirect_to(user_projects_path(user)) + expect(controller).not_to set_flash[:notice] + end + end + end + + context 'when requesting a redirected path' do + let(:redirect_route) { user.namespace.redirect_routes.create(path: 'old-path') } + + it 'redirects to the canonical path' do + get :projects, username: redirect_route.path + + expect(response).to redirect_to(user_projects_path(user)) + expect(controller).to set_flash[:notice].to(user_moved_message(redirect_route, user)) + end + + context 'when the old path is a substring of the scheme or host' do + let(:redirect_route) { user.namespace.redirect_routes.create(path: 'http') } + + it 'does not modify the requested host' do + get :projects, username: redirect_route.path + + expect(response).to redirect_to(user_projects_path(user)) + expect(controller).to set_flash[:notice].to(user_moved_message(redirect_route, user)) + end + end + + context 'when the old path is substring of users' do + let(:redirect_route) { user.namespace.redirect_routes.create(path: 'ser') } + + # I.e. /users/ser should not become /ufoos/ser + it 'does not modify the /users part of the path' do + get :projects, username: redirect_route.path + + expect(response).to redirect_to(user_projects_path(user)) + expect(controller).to set_flash[:notice].to(user_moved_message(redirect_route, user)) + end + end + end + end + end + end + def user_moved_message(redirect_route, user) "User '#{redirect_route.path}' was moved to '#{user.full_path}'. Please update any links and bookmarks that may still have the old path." end diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb index 78ddd8d5584..f5e99fdf00b 100644 --- a/spec/factories/ci/builds.rb +++ b/spec/factories/ci/builds.rb @@ -128,6 +128,16 @@ FactoryGirl.define do end end + trait :unicode_trace do + after(:create) do |build, evaluator| + trace = File.binread( + File.expand_path( + Rails.root.join('spec/fixtures/trace/ansi-sequence-and-unicode'))) + + build.trace.set(trace) + end + end + trait :erased do erased_at Time.now erased_by factory: :user diff --git a/spec/factories/ci/pipelines.rb b/spec/factories/ci/pipelines.rb index 561fbc8e247..361c5b9a49e 100644 --- a/spec/factories/ci/pipelines.rb +++ b/spec/factories/ci/pipelines.rb @@ -20,6 +20,15 @@ FactoryGirl.define do end end + # Persist merge request head_pipeline_id + # on pipeline factories to avoid circular references + transient { head_pipeline_of nil } + + after(:create) do |pipeline, evaluator| + merge_request = evaluator.head_pipeline_of + merge_request&.update(head_pipeline: pipeline) + end + factory :ci_pipeline do transient { config nil } diff --git a/spec/factories/group_members.rb b/spec/factories/group_members.rb index 080b2e75ea1..32cbfe28a60 100644 --- a/spec/factories/group_members.rb +++ b/spec/factories/group_members.rb @@ -10,5 +10,11 @@ FactoryGirl.define do trait(:master) { access_level GroupMember::MASTER } trait(:owner) { access_level GroupMember::OWNER } trait(:access_request) { requested_at Time.now } + + trait(:invited) do + user_id nil + invite_token 'xxx' + invite_email 'email@email.com' + end end end diff --git a/spec/factories/project_hooks.rb b/spec/factories/project_hooks.rb index 0210e871a63..cd754ea235f 100644 --- a/spec/factories/project_hooks.rb +++ b/spec/factories/project_hooks.rb @@ -14,7 +14,7 @@ FactoryGirl.define do issues_events true confidential_issues_events true note_events true - build_events true + job_events true pipeline_events true wiki_page_events true end diff --git a/spec/factories/project_members.rb b/spec/factories/project_members.rb index d62799a5a47..fe4518caadf 100644 --- a/spec/factories/project_members.rb +++ b/spec/factories/project_members.rb @@ -9,5 +9,11 @@ FactoryGirl.define do trait(:developer) { access_level ProjectMember::DEVELOPER } trait(:master) { access_level ProjectMember::MASTER } trait(:access_request) { requested_at Time.now } + + trait(:invited) do + user_id nil + invite_token 'xxx' + invite_email 'email@email.com' + end end end diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb index 3580752a805..7a76f5f8afc 100644 --- a/spec/factories/projects.rb +++ b/spec/factories/projects.rb @@ -60,7 +60,9 @@ FactoryGirl.define do trait :test_repo do after :create do |project| - TestEnv.copy_repo(project) + TestEnv.copy_repo(project, + bare_repo: TestEnv.factory_repo_path_bare, + refs: TestEnv::BRANCH_SHA) end end @@ -139,7 +141,9 @@ FactoryGirl.define do end after :create do |project, evaluator| - TestEnv.copy_repo(project) + TestEnv.copy_repo(project, + bare_repo: TestEnv.factory_repo_path_bare, + refs: TestEnv::BRANCH_SHA) if evaluator.create_template args = evaluator.create_template @@ -172,7 +176,9 @@ FactoryGirl.define do path { 'forked-gitlabhq' } after :create do |project| - TestEnv.copy_forked_repo_with_submodules(project) + TestEnv.copy_repo(project, + bare_repo: TestEnv.forked_repo_path_bare, + refs: TestEnv::FORKED_BRANCH_SHA) end end diff --git a/spec/factories/users.rb b/spec/factories/users.rb index 33fa80772ff..e60fe713bc3 100644 --- a/spec/factories/users.rb +++ b/spec/factories/users.rb @@ -8,6 +8,10 @@ FactoryGirl.define do confirmation_token { nil } can_create_group true + before(:create) do |user| + user.ensure_rss_token + end + trait :admin do admin true end diff --git a/spec/features/admin/admin_labels_spec.rb b/spec/features/admin/admin_labels_spec.rb index fa3d9ee25c0..a9251db13e5 100644 --- a/spec/features/admin/admin_labels_spec.rb +++ b/spec/features/admin/admin_labels_spec.rb @@ -34,11 +34,11 @@ RSpec.describe 'admin issues labels' do page.within '.labels' do page.all('.btn-remove').each do |remove| remove.click - wait_for_ajax + wait_for_requests end end - wait_for_ajax + wait_for_requests expect(page).to have_content("There are no labels yet") expect(page).not_to have_content('bug') diff --git a/spec/features/admin/admin_users_spec.rb b/spec/features/admin/admin_users_spec.rb index c5b1ef1295c..12cf59f42b0 100644 --- a/spec/features/admin/admin_users_spec.rb +++ b/spec/features/admin/admin_users_spec.rb @@ -277,7 +277,7 @@ describe "Admin::Users", feature: true do page.within(first('.group_member')) do find('.btn-remove').click end - wait_for_ajax + wait_for_requests expect(page).not_to have_selector('.group_member') end diff --git a/spec/features/atom/dashboard_issues_spec.rb b/spec/features/atom/dashboard_issues_spec.rb index 9ea325ab41b..711c8a710f3 100644 --- a/spec/features/atom/dashboard_issues_spec.rb +++ b/spec/features/atom/dashboard_issues_spec.rb @@ -20,13 +20,20 @@ describe "Dashboard Issues Feed", feature: true do expect(body).to have_selector('title', text: "#{user.name} issues") end + it "renders atom feed via RSS token" do + visit issues_dashboard_path(:atom, rss_token: user.rss_token) + + expect(response_headers['Content-Type']).to have_content('application/atom+xml') + expect(body).to have_selector('title', text: "#{user.name} issues") + end + it "renders atom feed with url parameters" do - visit issues_dashboard_path(:atom, private_token: user.private_token, state: 'opened', assignee_id: user.id) + visit issues_dashboard_path(:atom, rss_token: user.rss_token, state: 'opened', assignee_id: user.id) link = find('link[type="application/atom+xml"]') params = CGI.parse(URI.parse(link[:href]).query) - expect(params).to include('private_token' => [user.private_token]) + expect(params).to include('rss_token' => [user.rss_token]) expect(params).to include('state' => ['opened']) expect(params).to include('assignee_id' => [user.id.to_s]) end @@ -35,7 +42,7 @@ describe "Dashboard Issues Feed", feature: true do let!(:issue2) { create(:issue, author: user, assignees: [assignee], project: project2, description: 'test desc') } it "renders issue fields" do - visit issues_dashboard_path(:atom, private_token: user.private_token) + visit issues_dashboard_path(:atom, rss_token: user.rss_token) entry = find(:xpath, "//feed/entry[contains(summary/text(),'#{issue2.title}')]") @@ -58,7 +65,7 @@ describe "Dashboard Issues Feed", feature: true do end it "renders issue label and milestone info" do - visit issues_dashboard_path(:atom, private_token: user.private_token) + visit issues_dashboard_path(:atom, rss_token: user.rss_token) entry = find(:xpath, "//feed/entry[contains(summary/text(),'#{issue1.title}')]") diff --git a/spec/features/atom/dashboard_spec.rb b/spec/features/atom/dashboard_spec.rb index 746df36bb25..1df058b023c 100644 --- a/spec/features/atom/dashboard_spec.rb +++ b/spec/features/atom/dashboard_spec.rb @@ -11,6 +11,13 @@ describe "Dashboard Feed", feature: true do end end + context "projects atom feed via RSS token" do + it "renders projects atom feed" do + visit dashboard_projects_path(:atom, rss_token: user.rss_token) + expect(body).to have_selector('feed title') + end + end + context 'feed content' do let(:project) { create(:project) } let(:issue) { create(:issue, project: project, author: user, description: '') } @@ -20,7 +27,7 @@ describe "Dashboard Feed", feature: true do project.team << [user, :master] issue_event(issue, user) note_event(note, user) - visit dashboard_projects_path(:atom, private_token: user.private_token) + visit dashboard_projects_path(:atom, rss_token: user.rss_token) end it "has issue opened event" do diff --git a/spec/features/atom/issues_spec.rb b/spec/features/atom/issues_spec.rb index 4f6754ad541..a61231ea254 100644 --- a/spec/features/atom/issues_spec.rb +++ b/spec/features/atom/issues_spec.rb @@ -43,25 +43,40 @@ describe 'Issues Feed', feature: true do end end + context 'when authenticated via RSS token' do + it 'renders atom feed' do + visit namespace_project_issues_path(project.namespace, project, :atom, + rss_token: user.rss_token) + + expect(response_headers['Content-Type']). + to have_content('application/atom+xml') + expect(body).to have_selector('title', text: "#{project.name} issues") + expect(body).to have_selector('author email', text: issue.author_public_email) + expect(body).to have_selector('assignees assignee email', text: issue.assignees.first.public_email) + expect(body).to have_selector('assignee email', text: issue.assignees.first.public_email) + expect(body).to have_selector('entry summary', text: issue.title) + end + end + it "renders atom feed with url parameters for project issues" do visit namespace_project_issues_path(project.namespace, project, - :atom, private_token: user.private_token, state: 'opened', assignee_id: user.id) + :atom, rss_token: user.rss_token, state: 'opened', assignee_id: user.id) link = find('link[type="application/atom+xml"]') params = CGI.parse(URI.parse(link[:href]).query) - expect(params).to include('private_token' => [user.private_token]) + expect(params).to include('rss_token' => [user.rss_token]) expect(params).to include('state' => ['opened']) expect(params).to include('assignee_id' => [user.id.to_s]) end it "renders atom feed with url parameters for group issues" do - visit issues_group_path(group, :atom, private_token: user.private_token, state: 'opened', assignee_id: user.id) + visit issues_group_path(group, :atom, rss_token: user.rss_token, state: 'opened', assignee_id: user.id) link = find('link[type="application/atom+xml"]') params = CGI.parse(URI.parse(link[:href]).query) - expect(params).to include('private_token' => [user.private_token]) + expect(params).to include('rss_token' => [user.rss_token]) expect(params).to include('state' => ['opened']) expect(params).to include('assignee_id' => [user.id.to_s]) end diff --git a/spec/features/atom/users_spec.rb b/spec/features/atom/users_spec.rb index 7a2987e815d..fae5aaa52bd 100644 --- a/spec/features/atom/users_spec.rb +++ b/spec/features/atom/users_spec.rb @@ -11,6 +11,13 @@ describe "User Feed", feature: true do end end + context 'user atom feed via RSS token' do + it "renders user atom feed" do + visit user_path(user, :atom, rss_token: user.rss_token) + expect(body).to have_selector('feed title') + end + end + context 'feed content' do let(:project) { create(:project) } let(:issue) do @@ -40,7 +47,7 @@ describe "User Feed", feature: true do issue_event(issue, user) note_event(note, user) merge_request_event(merge_request, user) - visit user_path(user, :atom, private_token: user.private_token) + visit user_path(user, :atom, rss_token: user.rss_token) end it 'has issue opened event' do diff --git a/spec/features/auto_deploy_spec.rb b/spec/features/auto_deploy_spec.rb index 6c7423e4922..1cf7396bbac 100644 --- a/spec/features/auto_deploy_spec.rb +++ b/spec/features/auto_deploy_spec.rb @@ -46,7 +46,7 @@ describe 'Auto deploy' do within '.gitlab-ci-yml-selector' do click_on 'OpenShift' end - wait_for_ajax + wait_for_requests click_button 'Commit changes' expect(page).to have_content('New Merge Request From auto-deploy into master') diff --git a/spec/features/boards/add_issues_modal_spec.rb b/spec/features/boards/add_issues_modal_spec.rb index 505e0b5c355..32ac265814f 100644 --- a/spec/features/boards/add_issues_modal_spec.rb +++ b/spec/features/boards/add_issues_modal_spec.rb @@ -1,8 +1,6 @@ require 'rails_helper' describe 'Issue Boards add issue modal', :feature, :js do - include WaitForVueResource - let(:project) { create(:empty_project, :public) } let(:board) { create(:board, project: project) } let(:user) { create(:user) } @@ -19,13 +17,13 @@ describe 'Issue Boards add issue modal', :feature, :js do login_as(user) visit namespace_project_board_path(project.namespace, project, board) - wait_for_vue_resource + wait_for_requests end it 'resets filtered search state' do visit namespace_project_board_path(project.namespace, project, board, search: 'testing') - wait_for_vue_resource + wait_for_requests click_button('Add issues') @@ -74,7 +72,7 @@ describe 'Issue Boards add issue modal', :feature, :js do before do click_button('Add issues') - wait_for_vue_resource + wait_for_requests end it 'loads issues' do @@ -107,7 +105,7 @@ describe 'Issue Boards add issue modal', :feature, :js do click_button('Add issues') - wait_for_vue_resource + wait_for_requests page.within('.add-issues-modal') do expect(find('.add-issues-footer')).not_to have_button(planning.title) @@ -122,7 +120,7 @@ describe 'Issue Boards add issue modal', :feature, :js do find('.form-control').native.send_keys(issue.title) find('.form-control').native.send_keys(:enter) - wait_for_vue_resource + wait_for_requests expect(page).to have_selector('.card', count: 1) end @@ -133,7 +131,7 @@ describe 'Issue Boards add issue modal', :feature, :js do find('.form-control').native.send_keys('testing search') find('.form-control').native.send_keys(:enter) - wait_for_vue_resource + wait_for_requests expect(page).not_to have_selector('.card') expect(page).not_to have_content("You haven't added any issues to your project yet") diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb index 18585488e26..ba27db23ced 100644 --- a/spec/features/boards/boards_spec.rb +++ b/spec/features/boards/boards_spec.rb @@ -1,7 +1,6 @@ require 'rails_helper' describe 'Issue Boards', feature: true, js: true do - include WaitForVueResource include DragTo let(:project) { create(:empty_project, :public) } @@ -19,7 +18,7 @@ describe 'Issue Boards', feature: true, js: true do context 'no lists' do before do visit namespace_project_board_path(project.namespace, project, board) - wait_for_vue_resource + wait_for_requests expect(page).to have_selector('.board', count: 2) end @@ -46,7 +45,7 @@ describe 'Issue Boards', feature: true, js: true do page.within(find('.board-blank-state')) do click_button('Add default lists') end - wait_for_vue_resource + wait_for_requests expect(page).to have_selector('.board', count: 3) @@ -84,7 +83,7 @@ describe 'Issue Boards', feature: true, js: true do before do visit namespace_project_board_path(project.namespace, project, board) - wait_for_vue_resource + wait_for_requests expect(page).to have_selector('.board', count: 3) expect(find('.board:nth-child(1)')).to have_selector('.card') @@ -117,7 +116,7 @@ describe 'Issue Boards', feature: true, js: true do find('.filtered-search').set(issue8.title) find('.filtered-search').native.send_keys(:enter) - wait_for_vue_resource + wait_for_requests expect(find('.board:nth-child(1)')).to have_selector('.card', count: 0) expect(find('.board:nth-child(2)')).to have_selector('.card', count: 0) @@ -128,7 +127,7 @@ describe 'Issue Boards', feature: true, js: true do find('.filtered-search').set(issue5.title) find('.filtered-search').native.send_keys(:enter) - wait_for_vue_resource + wait_for_requests expect(find('.board:nth-child(1)')).to have_selector('.card', count: 1) expect(find('.board:nth-child(2)')).to have_selector('.card', count: 0) @@ -140,20 +139,20 @@ describe 'Issue Boards', feature: true, js: true do find('.board-delete').click end - wait_for_vue_resource + wait_for_requests expect(page).to have_selector('.board', count: 2) end it 'removes checkmark in new list dropdown after deleting' do click_button 'Add list' - wait_for_ajax + wait_for_requests page.within(find('.board:nth-child(1)')) do find('.board-delete').click end - wait_for_vue_resource + wait_for_requests expect(page).to have_selector('.board', count: 2) end @@ -164,7 +163,7 @@ describe 'Issue Boards', feature: true, js: true do end visit namespace_project_board_path(project.namespace, project, board) - wait_for_vue_resource + wait_for_requests page.within(find('.board', match: :first)) do expect(page.find('.board-header')).to have_content('58') @@ -172,13 +171,13 @@ describe 'Issue Boards', feature: true, js: true do expect(page).to have_content('Showing 20 of 58 issues') evaluate_script("document.querySelectorAll('.board .board-list')[0].scrollTop = document.querySelectorAll('.board .board-list')[0].scrollHeight") - wait_for_vue_resource + wait_for_requests expect(page).to have_selector('.card', count: 40) expect(page).to have_content('Showing 40 of 58 issues') evaluate_script("document.querySelectorAll('.board .board-list')[0].scrollTop = document.querySelectorAll('.board .board-list')[0].scrollHeight") - wait_for_vue_resource + wait_for_requests expect(page).to have_selector('.card', count: 58) expect(page).to have_content('Showing all issues') @@ -188,7 +187,7 @@ describe 'Issue Boards', feature: true, js: true do context 'closed' do it 'shows list of closed issues' do wait_for_board_cards(3, 1) - wait_for_ajax + wait_for_requests end it 'moves issue to closed' do @@ -272,7 +271,7 @@ describe 'Issue Boards', feature: true, js: true do context 'new list' do it 'shows all labels in new list dropdown' do click_button 'Add list' - wait_for_ajax + wait_for_requests page.within('.dropdown-menu-issues-board-new') do expect(page).to have_content(planning.title) @@ -283,52 +282,52 @@ describe 'Issue Boards', feature: true, js: true do it 'creates new list for label' do click_button 'Add list' - wait_for_ajax + wait_for_requests page.within('.dropdown-menu-issues-board-new') do click_link testing.title end - wait_for_vue_resource + wait_for_requests expect(page).to have_selector('.board', count: 4) end it 'creates new list for Backlog label' do click_button 'Add list' - wait_for_ajax + wait_for_requests page.within('.dropdown-menu-issues-board-new') do click_link backlog.title end - wait_for_vue_resource + wait_for_requests expect(page).to have_selector('.board', count: 4) end it 'creates new list for Closed label' do click_button 'Add list' - wait_for_ajax + wait_for_requests page.within('.dropdown-menu-issues-board-new') do click_link closed.title end - wait_for_vue_resource + wait_for_requests expect(page).to have_selector('.board', count: 4) end it 'keeps dropdown open after adding new list' do click_button 'Add list' - wait_for_ajax + wait_for_requests page.within('.dropdown-menu-issues-board-new') do click_link closed.title end - wait_for_vue_resource + wait_for_requests expect(page).to have_css('#js-add-list.open') end @@ -336,7 +335,7 @@ describe 'Issue Boards', feature: true, js: true do it 'creates new list from a new label' do click_button 'Add list' - wait_for_ajax + wait_for_requests click_link 'Create new label' @@ -346,8 +345,8 @@ describe 'Issue Boards', feature: true, js: true do click_button 'Create' - wait_for_ajax - wait_for_vue_resource + wait_for_requests + wait_for_requests expect(page).to have_selector('.board', count: 4) end @@ -360,7 +359,7 @@ describe 'Issue Boards', feature: true, js: true do click_filter_link(user2.username) submit_filter - wait_for_vue_resource + wait_for_requests wait_for_board_cards(1, 1) wait_for_empty_boards((2..3)) end @@ -370,7 +369,7 @@ describe 'Issue Boards', feature: true, js: true do click_filter_link(user.username) submit_filter - wait_for_vue_resource + wait_for_requests wait_for_board_cards(1, 1) wait_for_empty_boards((2..3)) @@ -381,7 +380,7 @@ describe 'Issue Boards', feature: true, js: true do click_filter_link(milestone.title) submit_filter - wait_for_vue_resource + wait_for_requests wait_for_board_cards(1, 1) wait_for_board_cards(2, 0) wait_for_board_cards(3, 0) @@ -392,7 +391,7 @@ describe 'Issue Boards', feature: true, js: true do click_filter_link(testing.title) submit_filter - wait_for_vue_resource + wait_for_requests wait_for_board_cards(1, 1) wait_for_empty_boards((2..3)) end @@ -407,7 +406,7 @@ describe 'Issue Boards', feature: true, js: true do wait_for_board_cards(1, 1) wait_for_empty_boards((2..3)) - wait_for_vue_resource + wait_for_requests page.within(find('.board', match: :first)) do expect(page.find('.board-header')).to have_content('1') @@ -442,7 +441,7 @@ describe 'Issue Boards', feature: true, js: true do click_filter_link(testing.title) submit_filter - wait_for_vue_resource + wait_for_requests page.within(find('.board', match: :first)) do expect(page.find('.board-header')).to have_content('51') @@ -470,7 +469,7 @@ describe 'Issue Boards', feature: true, js: true do submit_filter - wait_for_vue_resource + wait_for_requests wait_for_board_cards(1, 1) wait_for_empty_boards((2..3)) @@ -481,14 +480,14 @@ describe 'Issue Boards', feature: true, js: true do expect(page).to have_selector('.card', count: 8) expect(find('.card', match: :first)).to have_content(bug.title) click_button(bug.title) - wait_for_vue_resource + wait_for_requests end page.within('.tokens-container') do expect(page).to have_content(bug.title) end - wait_for_vue_resource + wait_for_requests wait_for_board_cards(1, 1) wait_for_empty_boards((2..3)) @@ -500,12 +499,12 @@ describe 'Issue Boards', feature: true, js: true do click_button(bug.title) end - wait_for_vue_resource + wait_for_requests expect(page).to have_selector('.card', count: 1) end - wait_for_vue_resource + wait_for_requests end end end @@ -513,7 +512,7 @@ describe 'Issue Boards', feature: true, js: true do context 'keyboard shortcuts' do before do visit namespace_project_board_path(project.namespace, project, board) - wait_for_vue_resource + wait_for_requests end it 'allows user to use keyboard shortcuts' do @@ -526,7 +525,7 @@ describe 'Issue Boards', feature: true, js: true do before do logout visit namespace_project_board_path(project.namespace, project, board) - wait_for_vue_resource + wait_for_requests end it 'displays lists' do @@ -550,7 +549,7 @@ describe 'Issue Boards', feature: true, js: true do logout login_as(user_guest) visit namespace_project_board_path(project.namespace, project, board) - wait_for_vue_resource + wait_for_requests end it 'does not show create new list' do diff --git a/spec/features/boards/issue_ordering_spec.rb b/spec/features/boards/issue_ordering_spec.rb index bfa2a72a256..6c40cb2c9eb 100644 --- a/spec/features/boards/issue_ordering_spec.rb +++ b/spec/features/boards/issue_ordering_spec.rb @@ -1,7 +1,6 @@ require 'rails_helper' describe 'Issue Boards', :feature, :js do - include WaitForVueResource include DragTo let(:project) { create(:empty_project, :public) } @@ -24,7 +23,7 @@ describe 'Issue Boards', :feature, :js do before do visit namespace_project_board_path(project.namespace, project, board) - wait_for_vue_resource + wait_for_requests expect(page).to have_selector('.board', count: 2) end @@ -38,7 +37,7 @@ describe 'Issue Boards', :feature, :js do it 'moves un-ordered issue to top of list' do drag(from_index: 3, to_index: 0) - wait_for_vue_resource + wait_for_requests page.within(first('.board')) do expect(first('.card')).to have_content(issue4.title) @@ -49,7 +48,7 @@ describe 'Issue Boards', :feature, :js do context 'ordering in list' do before do visit namespace_project_board_path(project.namespace, project, board) - wait_for_vue_resource + wait_for_requests expect(page).to have_selector('.board', count: 2) end @@ -57,7 +56,7 @@ describe 'Issue Boards', :feature, :js do it 'moves from middle to top' do drag(from_index: 1, to_index: 0) - wait_for_vue_resource + wait_for_requests expect(first('.card')).to have_content(issue2.title) end @@ -65,7 +64,7 @@ describe 'Issue Boards', :feature, :js do it 'moves from middle to bottom' do drag(from_index: 1, to_index: 2) - wait_for_vue_resource + wait_for_requests expect(all('.card').last).to have_content(issue2.title) end @@ -73,7 +72,7 @@ describe 'Issue Boards', :feature, :js do it 'moves from top to bottom' do drag(from_index: 0, to_index: 2) - wait_for_vue_resource + wait_for_requests expect(all('.card').last).to have_content(issue3.title) end @@ -81,7 +80,7 @@ describe 'Issue Boards', :feature, :js do it 'moves from bottom to top' do drag(from_index: 2, to_index: 0) - wait_for_vue_resource + wait_for_requests expect(first('.card')).to have_content(issue1.title) end @@ -89,7 +88,7 @@ describe 'Issue Boards', :feature, :js do it 'moves from top to middle' do drag(from_index: 0, to_index: 1) - wait_for_vue_resource + wait_for_requests expect(first('.card')).to have_content(issue2.title) end @@ -97,7 +96,7 @@ describe 'Issue Boards', :feature, :js do it 'moves from bottom to middle' do drag(from_index: 2, to_index: 1) - wait_for_vue_resource + wait_for_requests expect(all('.card').last).to have_content(issue2.title) end @@ -112,7 +111,7 @@ describe 'Issue Boards', :feature, :js do before do visit namespace_project_board_path(project.namespace, project, board) - wait_for_vue_resource + wait_for_requests expect(page).to have_selector('.board', count: 3) end @@ -120,7 +119,7 @@ describe 'Issue Boards', :feature, :js do it 'moves to top of another list' do drag(list_from_index: 0, list_to_index: 1) - wait_for_vue_resource + wait_for_requests expect(first('.board')).to have_selector('.card', count: 2) expect(all('.board')[1]).to have_selector('.card', count: 4) @@ -133,7 +132,7 @@ describe 'Issue Boards', :feature, :js do it 'moves to bottom of another list' do drag(list_from_index: 0, list_to_index: 1, to_index: 2) - wait_for_vue_resource + wait_for_requests expect(first('.board')).to have_selector('.card', count: 2) expect(all('.board')[1]).to have_selector('.card', count: 4) @@ -146,7 +145,7 @@ describe 'Issue Boards', :feature, :js do it 'moves to index of another list' do drag(list_from_index: 0, list_to_index: 1, to_index: 1) - wait_for_vue_resource + wait_for_requests expect(first('.board')).to have_selector('.card', count: 2) expect(all('.board')[1]).to have_selector('.card', count: 4) diff --git a/spec/features/boards/keyboard_shortcut_spec.rb b/spec/features/boards/keyboard_shortcut_spec.rb index a9cc6c49f8e..c2167ba12cd 100644 --- a/spec/features/boards/keyboard_shortcut_spec.rb +++ b/spec/features/boards/keyboard_shortcut_spec.rb @@ -1,8 +1,6 @@ require 'rails_helper' describe 'Issue Boards shortcut', feature: true, js: true do - include WaitForVueResource - let(:project) { create(:empty_project) } before do @@ -17,6 +15,6 @@ describe 'Issue Boards shortcut', feature: true, js: true do find('body').native.send_keys('gb') expect(page).to have_selector('.boards-list') - wait_for_vue_resource + wait_for_requests end end diff --git a/spec/features/boards/modal_filter_spec.rb b/spec/features/boards/modal_filter_spec.rb index e1367c675e5..ce132bfd979 100644 --- a/spec/features/boards/modal_filter_spec.rb +++ b/spec/features/boards/modal_filter_spec.rb @@ -1,8 +1,6 @@ require 'rails_helper' describe 'Issue Boards add issue modal filtering', :feature, :js do - include WaitForVueResource - let(:project) { create(:empty_project, :public) } let(:board) { create(:board, project: project) } let(:planning) { create(:label, project: project, name: 'Planning') } @@ -24,7 +22,7 @@ describe 'Issue Boards add issue modal filtering', :feature, :js do find('.form-control').native.send_keys('testing empty state') find('.form-control').native.send_keys(:enter) - wait_for_vue_resource + wait_for_requests expect(page).to have_content('There are no issues to show.') end @@ -38,7 +36,7 @@ describe 'Issue Boards add issue modal filtering', :feature, :js do submit_filter page.within('.add-issues-modal') do - wait_for_vue_resource + wait_for_requests expect(page).to have_selector('.card', count: 0) @@ -48,7 +46,7 @@ describe 'Issue Boards add issue modal filtering', :feature, :js do click_button('Add issues') page.within('.add-issues-modal') do - wait_for_vue_resource + wait_for_requests expect(page).to have_selector('.card', count: 1) end @@ -62,13 +60,13 @@ describe 'Issue Boards add issue modal filtering', :feature, :js do submit_filter page.within('.add-issues-modal') do - wait_for_vue_resource + wait_for_requests expect(page).to have_selector('.card', count: 0) find('.clear-search').click - wait_for_vue_resource + wait_for_requests expect(page).to have_selector('.card', count: 1) end @@ -89,7 +87,7 @@ describe 'Issue Boards add issue modal filtering', :feature, :js do submit_filter page.within('.add-issues-modal') do - wait_for_vue_resource + wait_for_requests expect(page).to have_selector('.js-visual-token', text: user2.username) expect(page).to have_selector('.card', count: 1) @@ -112,7 +110,7 @@ describe 'Issue Boards add issue modal filtering', :feature, :js do submit_filter page.within('.add-issues-modal') do - wait_for_vue_resource + wait_for_requests expect(page).to have_selector('.js-visual-token', text: 'none') expect(page).to have_selector('.card', count: 1) @@ -125,7 +123,7 @@ describe 'Issue Boards add issue modal filtering', :feature, :js do submit_filter page.within('.add-issues-modal') do - wait_for_vue_resource + wait_for_requests expect(page).to have_selector('.js-visual-token', text: user2.username) expect(page).to have_selector('.card', count: 1) @@ -147,7 +145,7 @@ describe 'Issue Boards add issue modal filtering', :feature, :js do submit_filter page.within('.add-issues-modal') do - wait_for_vue_resource + wait_for_requests expect(page).to have_selector('.js-visual-token', text: 'upcoming') expect(page).to have_selector('.card', count: 0) @@ -160,7 +158,7 @@ describe 'Issue Boards add issue modal filtering', :feature, :js do submit_filter page.within('.add-issues-modal') do - wait_for_vue_resource + wait_for_requests expect(page).to have_selector('.js-visual-token', text: milestone.name) expect(page).to have_selector('.card', count: 1) @@ -182,7 +180,7 @@ describe 'Issue Boards add issue modal filtering', :feature, :js do submit_filter page.within('.add-issues-modal') do - wait_for_vue_resource + wait_for_requests expect(page).to have_selector('.js-visual-token', text: 'none') expect(page).to have_selector('.card', count: 1) @@ -195,7 +193,7 @@ describe 'Issue Boards add issue modal filtering', :feature, :js do submit_filter page.within('.add-issues-modal') do - wait_for_vue_resource + wait_for_requests expect(page).to have_selector('.js-visual-token', text: label.title) expect(page).to have_selector('.card', count: 1) @@ -205,7 +203,7 @@ describe 'Issue Boards add issue modal filtering', :feature, :js do def visit_board visit namespace_project_board_path(project.namespace, project, board) - wait_for_vue_resource + wait_for_requests click_button('Add issues') end diff --git a/spec/features/boards/new_issue_spec.rb b/spec/features/boards/new_issue_spec.rb index f04a1a89e96..0e98f994018 100644 --- a/spec/features/boards/new_issue_spec.rb +++ b/spec/features/boards/new_issue_spec.rb @@ -1,8 +1,6 @@ require 'rails_helper' describe 'Issue Boards new issue', feature: true, js: true do - include WaitForVueResource - let(:project) { create(:empty_project, :public) } let(:board) { create(:board, project: project) } let!(:list) { create(:list, board: board, position: 0) } @@ -15,7 +13,7 @@ describe 'Issue Boards new issue', feature: true, js: true do login_as(user) visit namespace_project_board_path(project.namespace, project, board) - wait_for_vue_resource + wait_for_requests expect(page).to have_selector('.board', count: 2) end @@ -60,7 +58,7 @@ describe 'Issue Boards new issue', feature: true, js: true do click_button 'Submit issue' end - wait_for_vue_resource + wait_for_requests page.within(first('.board .board-issue-count')) do expect(page).to have_content('1') @@ -77,7 +75,7 @@ describe 'Issue Boards new issue', feature: true, js: true do click_button 'Submit issue' end - wait_for_vue_resource + wait_for_requests expect(page).to have_selector('.issue-boards-sidebar') end @@ -86,7 +84,7 @@ describe 'Issue Boards new issue', feature: true, js: true do context 'unauthorized user' do before do visit namespace_project_board_path(project.namespace, project, board) - wait_for_vue_resource + wait_for_requests end it 'does not display new issue button' do diff --git a/spec/features/boards/sidebar_spec.rb b/spec/features/boards/sidebar_spec.rb index 1238647d3f3..34f4d765117 100644 --- a/spec/features/boards/sidebar_spec.rb +++ b/spec/features/boards/sidebar_spec.rb @@ -1,8 +1,6 @@ require 'rails_helper' describe 'Issue Boards', feature: true, js: true do - include WaitForVueResource - let(:user) { create(:user) } let(:user2) { create(:user) } let(:project) { create(:empty_project, :public) } @@ -25,7 +23,7 @@ describe 'Issue Boards', feature: true, js: true do login_as(user) visit namespace_project_board_path(project.namespace, project, board) - wait_for_vue_resource + wait_for_requests end after do @@ -74,7 +72,7 @@ describe 'Issue Boards', feature: true, js: true do click_button 'Remove from board' end - wait_for_vue_resource + wait_for_requests page.within(first('.board')) do expect(page).to have_selector('.card', count: 1) @@ -88,12 +86,12 @@ describe 'Issue Boards', feature: true, js: true do page.within('.assignee') do click_link 'Edit' - wait_for_ajax + wait_for_requests page.within('.dropdown-menu-user') do click_link user.name - wait_for_vue_resource + wait_for_requests end expect(page).to have_content(user.name) @@ -109,14 +107,13 @@ describe 'Issue Boards', feature: true, js: true do page.within('.assignee') do click_link 'Edit' - wait_for_ajax + wait_for_requests page.within('.dropdown-menu-user') do click_link 'Unassigned' end - find('.dropdown-menu-toggle').click - wait_for_vue_resource + wait_for_requests expect(page).to have_content('No assignee') end @@ -132,7 +129,7 @@ describe 'Issue Boards', feature: true, js: true do click_button 'assign yourself' - wait_for_vue_resource + wait_for_requests expect(page).to have_content(user.name) end @@ -146,12 +143,12 @@ describe 'Issue Boards', feature: true, js: true do page.within('.assignee') do click_link 'Edit' - wait_for_ajax + wait_for_requests page.within('.dropdown-menu-user') do click_link user.name - wait_for_vue_resource + wait_for_requests end expect(page).to have_content(user.name) @@ -163,7 +160,7 @@ describe 'Issue Boards', feature: true, js: true do page.within('.assignee') do click_link 'Edit' - + expect(find('.dropdown-menu')).to have_selector('.is-active') end end @@ -176,11 +173,11 @@ describe 'Issue Boards', feature: true, js: true do page.within('.milestone') do click_link 'Edit' - wait_for_ajax + wait_for_requests click_link milestone.title - wait_for_vue_resource + wait_for_requests page.within('.value') do expect(page).to have_content(milestone.title) @@ -194,11 +191,11 @@ describe 'Issue Boards', feature: true, js: true do page.within('.milestone') do click_link 'Edit' - wait_for_ajax + wait_for_requests click_link "No Milestone" - wait_for_vue_resource + wait_for_requests page.within('.value') do expect(page).not_to have_content(milestone.title) @@ -216,7 +213,7 @@ describe 'Issue Boards', feature: true, js: true do click_button Date.today.day - wait_for_vue_resource + wait_for_requests expect(page).to have_content(Date.today.to_s(:medium)) end @@ -230,11 +227,11 @@ describe 'Issue Boards', feature: true, js: true do page.within('.labels') do click_link 'Edit' - wait_for_ajax + wait_for_requests click_link bug.title - wait_for_vue_resource + wait_for_requests find('.dropdown-menu-close-icon').click @@ -254,12 +251,12 @@ describe 'Issue Boards', feature: true, js: true do page.within('.labels') do click_link 'Edit' - wait_for_ajax + wait_for_requests click_link bug.title click_link regression.title - wait_for_vue_resource + wait_for_requests find('.dropdown-menu-close-icon').click @@ -281,11 +278,11 @@ describe 'Issue Boards', feature: true, js: true do page.within('.labels') do click_link 'Edit' - wait_for_ajax + wait_for_requests click_link stretch.title - wait_for_vue_resource + wait_for_requests find('.dropdown-menu-close-icon').click @@ -306,7 +303,7 @@ describe 'Issue Boards', feature: true, js: true do page.within('.subscription') do click_button 'Subscribe' - wait_for_ajax + wait_for_requests expect(page).to have_content("Unsubscribe") end end diff --git a/spec/features/boards/sub_group_project_spec.rb b/spec/features/boards/sub_group_project_spec.rb index 6cd7fddd288..4cd05010a93 100644 --- a/spec/features/boards/sub_group_project_spec.rb +++ b/spec/features/boards/sub_group_project_spec.rb @@ -1,8 +1,6 @@ require 'rails_helper' describe 'Sub-group project issue boards', :feature, :js do - include WaitForVueResource - let(:group) { create(:group) } let(:nested_group_1) { create(:group, parent: group) } let(:project) { create(:empty_project, group: nested_group_1) } @@ -18,7 +16,7 @@ describe 'Sub-group project issue boards', :feature, :js do login_as(user) visit namespace_project_board_path(project.namespace, project, board) - wait_for_vue_resource + wait_for_requests end it 'creates new label from sidebar' do @@ -35,7 +33,7 @@ describe 'Sub-group project issue boards', :feature, :js do click_button 'Create' - wait_for_ajax + wait_for_requests end page.within '.labels' do diff --git a/spec/features/calendar_spec.rb b/spec/features/calendar_spec.rb index 496faf87a16..1b6d8439f92 100644 --- a/spec/features/calendar_spec.rb +++ b/spec/features/calendar_spec.rb @@ -74,7 +74,7 @@ feature 'Contributions Calendar', :feature, :js do describe 'calendar day selection' do before do visit user.username - wait_for_ajax + wait_for_requests end it 'displays calendar' do @@ -86,7 +86,7 @@ feature 'Contributions Calendar', :feature, :js do before do cells[0].click - wait_for_ajax + wait_for_requests @first_day_activities = selected_day_activities end @@ -97,7 +97,7 @@ feature 'Contributions Calendar', :feature, :js do describe 'select another calendar day' do before do cells[1].click - wait_for_ajax + wait_for_requests end it 'displays different calendar day activities' do @@ -108,7 +108,7 @@ feature 'Contributions Calendar', :feature, :js do describe 'deselect calendar day' do before do cells[0].click - wait_for_ajax + wait_for_requests end it 'hides calendar day activities' do @@ -122,7 +122,7 @@ feature 'Contributions Calendar', :feature, :js do shared_context 'visit user page' do before do visit user.username - wait_for_ajax + wait_for_requests end end diff --git a/spec/features/copy_as_gfm_spec.rb b/spec/features/copy_as_gfm_spec.rb index be615519a09..740f60c05cc 100644 --- a/spec/features/copy_as_gfm_spec.rb +++ b/spec/features/copy_as_gfm_spec.rb @@ -51,7 +51,6 @@ describe 'Copy as GFM', feature: true, js: true do To see how GitLab looks please see the [features page on our website](https://about.gitlab.com/features/). - - Manage Git repositories with fine grained access controls that keep your code secure - Perform code reviews and enhance collaboration with merge requests @@ -66,6 +65,38 @@ describe 'Copy as GFM', feature: true, js: true do GFM ) + aggregate_failures('an accidentally selected empty element') do + gfm = '# Heading1' + + html = <<-HTML.strip_heredoc + <h1>Heading1</h1> + + <h2></h2> + HTML + + output_gfm = html_to_gfm(html) + expect(output_gfm.strip).to eq(gfm.strip) + end + + aggregate_failures('an accidentally selected other element') do + gfm = 'Test comment with **Markdown!**' + + html = <<-HTML.strip_heredoc + <li class="note"> + <div class="md"> + <p> + Test comment with <strong>Markdown!</strong> + </p> + </div> + </li> + + <li class="note"></li> + HTML + + output_gfm = html_to_gfm(html) + expect(output_gfm.strip).to eq(gfm.strip) + end + verify( 'InlineDiffFilter', @@ -352,7 +383,6 @@ describe 'Copy as GFM', feature: true, js: true do <<-GFM.strip_heredoc, - Nested - - Lists GFM @@ -375,7 +405,6 @@ describe 'Copy as GFM', feature: true, js: true do <<-GFM.strip_heredoc, 1. Nested - 1. Numbered lists GFM @@ -479,7 +508,7 @@ describe 'Copy as GFM', feature: true, js: true do context 'from a blob' do before do visit namespace_project_blob_path(project.namespace, project, File.join('master', 'files/ruby/popen.rb')) - wait_for_ajax + wait_for_requests end context 'selecting one word of text' do @@ -521,7 +550,7 @@ describe 'Copy as GFM', feature: true, js: true do context 'from a GFM code block' do before do visit namespace_project_blob_path(project.namespace, project, File.join('markdown', 'doc/api/users.md')) - wait_for_ajax + wait_for_requests end context 'selecting one word of text' do diff --git a/spec/features/cycle_analytics_spec.rb b/spec/features/cycle_analytics_spec.rb index cbeb73d9cae..b416bbd3c79 100644 --- a/spec/features/cycle_analytics_spec.rb +++ b/spec/features/cycle_analytics_spec.rb @@ -7,7 +7,7 @@ feature 'Cycle Analytics', feature: true, js: true do let(:issue) { create(:issue, project: project, created_at: 2.days.ago) } let(:milestone) { create(:milestone, project: project) } let(:mr) { create_merge_request_closing_issue(issue, commit_message: "References #{issue.to_reference}") } - let(:pipeline) { create(:ci_empty_pipeline, status: 'created', project: project, ref: mr.source_branch, sha: mr.source_branch_sha) } + let(:pipeline) { create(:ci_empty_pipeline, status: 'created', project: project, ref: mr.source_branch, sha: mr.source_branch_sha, head_pipeline_of: mr) } context 'as an allowed user' do context 'when project is new' do @@ -17,7 +17,7 @@ feature 'Cycle Analytics', feature: true, js: true do login_as(user) visit namespace_project_cycle_analytics_path(project.namespace, project) - wait_for_ajax + wait_for_requests end it 'shows introductory message' do @@ -33,7 +33,6 @@ feature 'Cycle Analytics', feature: true, js: true do context "when there's cycle analytics data" do before do allow_any_instance_of(Gitlab::ReferenceExtractor).to receive(:issues).and_return([issue]) - mr.update(head_pipeline: pipeline) project.add_master(user) create_cycle @@ -73,7 +72,7 @@ feature 'Cycle Analytics', feature: true, js: true do project.team << [user, :master] login_as(user) visit namespace_project_cycle_analytics_path(project.namespace, project) - wait_for_ajax + wait_for_requests end it 'shows the content in Spanish' do @@ -96,7 +95,7 @@ feature 'Cycle Analytics', feature: true, js: true do login_as(guest) visit namespace_project_cycle_analytics_path(project.namespace, project) - wait_for_ajax + wait_for_requests end it 'needs permissions to see restricted stages' do @@ -140,6 +139,6 @@ feature 'Cycle Analytics', feature: true, js: true do def click_stage(stage_name) find('.stage-nav li', text: stage_name).click - wait_for_ajax + wait_for_requests end end diff --git a/spec/features/dashboard/activity_spec.rb b/spec/features/dashboard/activity_spec.rb index c977f266296..0764044260e 100644 --- a/spec/features/dashboard/activity_spec.rb +++ b/spec/features/dashboard/activity_spec.rb @@ -5,7 +5,7 @@ RSpec.describe 'Dashboard Activity', feature: true do login_as(create :user) visit activity_dashboard_path end - - it_behaves_like "it has an RSS button with current_user's private token" - it_behaves_like "an autodiscoverable RSS feed with current_user's private token" + + it_behaves_like "it has an RSS button with current_user's RSS token" + it_behaves_like "an autodiscoverable RSS feed with current_user's RSS token" end diff --git a/spec/features/dashboard/datetime_on_tooltips_spec.rb b/spec/features/dashboard/datetime_on_tooltips_spec.rb index 0e9e3f78be2..1793e323588 100644 --- a/spec/features/dashboard/datetime_on_tooltips_spec.rb +++ b/spec/features/dashboard/datetime_on_tooltips_spec.rb @@ -15,7 +15,7 @@ feature 'Tooltips on .timeago dates', feature: true, js: true do login_as user visit user_path(user) - wait_for_ajax() + wait_for_requests() page.find('.js-timeago').hover end @@ -32,7 +32,7 @@ feature 'Tooltips on .timeago dates', feature: true, js: true do login_as user visit user_snippets_path(user) - wait_for_ajax() + wait_for_requests() page.find('.js-timeago.snippet-created-ago').hover end diff --git a/spec/features/dashboard/groups_list_spec.rb b/spec/features/dashboard/groups_list_spec.rb index 52b4d82e856..b0e2953dda2 100644 --- a/spec/features/dashboard/groups_list_spec.rb +++ b/spec/features/dashboard/groups_list_spec.rb @@ -23,7 +23,7 @@ describe 'Dashboard Groups page', js: true, feature: true do it 'filters groups' do fill_in 'filter_groups', with: group.name - wait_for_ajax + wait_for_requests expect(page).to have_content(group.full_name) expect(page).not_to have_content(nested_group.full_name) @@ -32,10 +32,10 @@ describe 'Dashboard Groups page', js: true, feature: true do it 'resets search when user cleans the input' do fill_in 'filter_groups', with: group.name - wait_for_ajax + wait_for_requests fill_in 'filter_groups', with: "" - wait_for_ajax + wait_for_requests expect(page).to have_content(group.full_name) expect(page).to have_content(nested_group.full_name) diff --git a/spec/features/dashboard/issues_spec.rb b/spec/features/dashboard/issues_spec.rb index 86c7954e60c..2346a9ec2ed 100644 --- a/spec/features/dashboard/issues_spec.rb +++ b/spec/features/dashboard/issues_spec.rb @@ -26,9 +26,20 @@ RSpec.describe 'Dashboard Issues', feature: true do expect(page).not_to have_content(other_issue.title) end + it 'shows checkmark when unassigned is selected for assignee', js: true do + find('.js-assignee-search').click + find('li', text: 'Unassigned').click + find('.js-assignee-search').click + + expect(find('li[data-user-id="0"] a.is-active')).to be_visible + end + it 'shows issues when current user is author', js: true do find('#assignee_id', visible: false).set('') find('.js-author-search', match: :first).click + + expect(find('li[data-user-id="null"] a.is-active')).to be_visible + find('.dropdown-menu-author li a', match: :first, text: current_user.to_reference).click find('.js-author-search', match: :first).click @@ -51,6 +62,6 @@ RSpec.describe 'Dashboard Issues', feature: true do expect(page).to have_content(other_issue.title) end - it_behaves_like "it has an RSS button with current_user's private token" - it_behaves_like "an autodiscoverable RSS feed with current_user's private token" + it_behaves_like "it has an RSS button with current_user's RSS token" + it_behaves_like "an autodiscoverable RSS feed with current_user's RSS token" end diff --git a/spec/features/dashboard/milestone_filter_spec.rb b/spec/features/dashboard/milestone_filter_spec.rb index d60a002a8d7..b5b92c36895 100644 --- a/spec/features/dashboard/milestone_filter_spec.rb +++ b/spec/features/dashboard/milestone_filter_spec.rb @@ -1,8 +1,6 @@ require 'spec_helper' describe 'Dashboard > milestone filter', :feature, :js do - include WaitForAjax - let(:user) { create(:user) } let(:project) { create(:project, name: 'test', namespace: user.namespace) } let(:milestone) { create(:milestone, title: "v1.0", project: project) } @@ -28,14 +26,14 @@ describe 'Dashboard > milestone filter', :feature, :js do before do find(milestone_select).click - wait_for_ajax + wait_for_requests page.within('.dropdown-content') do click_link 'v1.0' end find(milestone_select).click - wait_for_ajax + wait_for_requests end it 'shows issues with Milestone v1.0' do @@ -48,9 +46,9 @@ describe 'Dashboard > milestone filter', :feature, :js do # open & close dropdown find('.dropdown-menu-close').click - + expect(find('.milestone-filter')).not_to have_selector('.dropdown.open') - + find(milestone_select).click expect(find('.dropdown-content')).to have_selector('a.is-active', count: 1) diff --git a/spec/features/dashboard/project_member_activity_index_spec.rb b/spec/features/dashboard/project_member_activity_index_spec.rb index 16c214ae060..cdf919af9b5 100644 --- a/spec/features/dashboard/project_member_activity_index_spec.rb +++ b/spec/features/dashboard/project_member_activity_index_spec.rb @@ -11,7 +11,7 @@ feature 'Project member activity', feature: true, js: true do def visit_activities_and_wait_with_event(event_type) Event.create(project: project, author_id: user.id, action: event_type) visit activity_namespace_project_path(project.namespace, project) - wait_for_ajax + wait_for_requests end subject { page.find(".event-title").text } diff --git a/spec/features/dashboard/projects_spec.rb b/spec/features/dashboard/projects_spec.rb index f1789fc9d43..01351548a99 100644 --- a/spec/features/dashboard/projects_spec.rb +++ b/spec/features/dashboard/projects_spec.rb @@ -31,5 +31,5 @@ RSpec.describe 'Dashboard Projects', feature: true do end end - it_behaves_like "an autodiscoverable RSS feed with current_user's private token" + it_behaves_like "an autodiscoverable RSS feed with current_user's RSS token" end diff --git a/spec/features/dashboard_issues_spec.rb b/spec/features/dashboard_issues_spec.rb index ad60fb2c74f..1c53f6dff06 100644 --- a/spec/features/dashboard_issues_spec.rb +++ b/spec/features/dashboard_issues_spec.rb @@ -53,10 +53,10 @@ describe "Dashboard Issues filtering", feature: true, js: true do auto_discovery_link = find('link[type="application/atom+xml"]', visible: false) auto_discovery_params = CGI.parse(URI.parse(auto_discovery_link[:href]).query) - expect(params).to include('private_token' => [user.private_token]) + expect(params).to include('rss_token' => [user.rss_token]) expect(params).to include('milestone_title' => ['']) expect(params).to include('assignee_id' => [user.id.to_s]) - expect(auto_discovery_params).to include('private_token' => [user.private_token]) + expect(auto_discovery_params).to include('rss_token' => [user.rss_token]) expect(auto_discovery_params).to include('milestone_title' => ['']) expect(auto_discovery_params).to include('assignee_id' => [user.id.to_s]) end diff --git a/spec/features/expand_collapse_diffs_spec.rb b/spec/features/expand_collapse_diffs_spec.rb index 76c77e0bc5f..0cb75538311 100644 --- a/spec/features/expand_collapse_diffs_spec.rb +++ b/spec/features/expand_collapse_diffs_spec.rb @@ -36,7 +36,7 @@ feature 'Expand and collapse diffs', js: true, feature: true do visit namespace_project_commit_path(project.namespace, project, project.commit(branch), anchor: "#{large_diff[:id]}_0_1") execute_script('window.location.reload()') - wait_for_ajax + wait_for_requests expect(large_diff).to have_selector('.code') expect(large_diff).not_to have_selector('.nothing-here-block') @@ -50,7 +50,7 @@ feature 'Expand and collapse diffs', js: true, feature: true do visit namespace_project_commit_path(project.namespace, project, project.commit(branch), anchor: large_diff[:id]) execute_script('window.location.reload()') - wait_for_ajax + wait_for_requests expect(large_diff).to have_selector('.code') expect(large_diff).not_to have_selector('.nothing-here-block') @@ -94,7 +94,7 @@ feature 'Expand and collapse diffs', js: true, feature: true do context 'expanding a diff for a renamed file' do before do large_diff_renamed.find('.click-to-expand').click - wait_for_ajax + wait_for_requests end it 'shows the old content' do @@ -116,7 +116,7 @@ feature 'Expand and collapse diffs', js: true, feature: true do find('.js-file-title', match: :first) # Click `large_diff.md` title all('.diff-toggle-caret')[1].click - wait_for_ajax + wait_for_requests end it 'makes a request to get the content' do @@ -139,7 +139,7 @@ feature 'Expand and collapse diffs', js: true, feature: true do large_diff.find('.add-diff-note').click large_diff.find('.note-textarea').send_keys comment_text large_diff.find_button('Comment').click - wait_for_ajax + wait_for_requests end it 'adds the comment' do @@ -160,7 +160,7 @@ feature 'Expand and collapse diffs', js: true, feature: true do find('.js-file-title', match: :first) # Click `large_diff.md` title all('.diff-toggle-caret')[1].click - wait_for_ajax + wait_for_requests end it 'shows the diff content' do @@ -216,7 +216,7 @@ feature 'Expand and collapse diffs', js: true, feature: true do expect(page).to have_no_content('No longer a symlink') find('.click-to-expand').click - wait_for_ajax + wait_for_requests expect(page).to have_content('No longer a symlink') end @@ -273,7 +273,7 @@ feature 'Expand and collapse diffs', js: true, feature: true do expect(page).to have_content('too_large_image.jpg') find('.note-textarea') - wait_for_ajax + wait_for_requests execute_script('window.ajaxUris = []; $(document).ajaxSend(function(event, xhr, settings) { ajaxUris.push(settings.url) });') end diff --git a/spec/features/explore/groups_list_spec.rb b/spec/features/explore/groups_list_spec.rb index 9828cb179a7..d4284ed099b 100644 --- a/spec/features/explore/groups_list_spec.rb +++ b/spec/features/explore/groups_list_spec.rb @@ -23,7 +23,7 @@ describe 'Explore Groups page', :js, :feature do it 'filters groups' do fill_in 'filter_groups', with: group.name - wait_for_ajax + wait_for_requests expect(page).to have_content(group.full_name) expect(page).not_to have_content(public_group.full_name) @@ -32,10 +32,10 @@ describe 'Explore Groups page', :js, :feature do it 'resets search when user cleans the input' do fill_in 'filter_groups', with: group.name - wait_for_ajax + wait_for_requests fill_in 'filter_groups', with: "" - wait_for_ajax + wait_for_requests expect(page).to have_content(group.full_name) expect(page).to have_content(public_group.full_name) diff --git a/spec/features/gitlab_flavored_markdown_spec.rb b/spec/features/gitlab_flavored_markdown_spec.rb index 005a029a393..55092412340 100644 --- a/spec/features/gitlab_flavored_markdown_spec.rb +++ b/spec/features/gitlab_flavored_markdown_spec.rb @@ -49,8 +49,6 @@ describe "GitLab Flavored Markdown", feature: true do end describe "for issues", feature: true, js: true do - include WaitForVueResource - before do @other_issue = create(:issue, author: @user, diff --git a/spec/features/groups/activity_spec.rb b/spec/features/groups/activity_spec.rb index 3b481cba424..81f9c103e95 100644 --- a/spec/features/groups/activity_spec.rb +++ b/spec/features/groups/activity_spec.rb @@ -11,8 +11,8 @@ feature 'Group activity page', feature: true do visit path end - it_behaves_like "it has an RSS button with current_user's private token" - it_behaves_like "an autodiscoverable RSS feed with current_user's private token" + it_behaves_like "it has an RSS button with current_user's RSS token" + it_behaves_like "an autodiscoverable RSS feed with current_user's RSS token" end context 'when signed out' do @@ -20,7 +20,7 @@ feature 'Group activity page', feature: true do visit path end - it_behaves_like "it has an RSS button without a private token" - it_behaves_like "an autodiscoverable RSS feed without a private token" + it_behaves_like "it has an RSS button without an RSS token" + it_behaves_like "an autodiscoverable RSS feed without an RSS token" end end diff --git a/spec/features/groups/issues_spec.rb b/spec/features/groups/issues_spec.rb index 45f57845c74..d6b88542ef7 100644 --- a/spec/features/groups/issues_spec.rb +++ b/spec/features/groups/issues_spec.rb @@ -12,15 +12,15 @@ feature 'Group issues page', feature: true do context 'when signed in' do let(:user) { user_in_group } - it_behaves_like "it has an RSS button with current_user's private token" - it_behaves_like "an autodiscoverable RSS feed with current_user's private token" + it_behaves_like "it has an RSS button with current_user's RSS token" + it_behaves_like "an autodiscoverable RSS feed with current_user's RSS token" end context 'when signed out' do let(:user) { nil } - it_behaves_like "it has an RSS button without a private token" - it_behaves_like "an autodiscoverable RSS feed without a private token" + it_behaves_like "it has an RSS button without an RSS token" + it_behaves_like "an autodiscoverable RSS feed without an RSS token" end end @@ -33,7 +33,7 @@ feature 'Group issues page', feature: true do it 'filters by only group users' do click_button('Assignee') - wait_for_ajax + wait_for_requests expect(find('.dropdown-menu-assignee')).to have_link(user.name) expect(find('.dropdown-menu-assignee')).not_to have_link(user2.name) diff --git a/spec/features/groups/show_spec.rb b/spec/features/groups/show_spec.rb index fb39693e8ca..d3c49c37374 100644 --- a/spec/features/groups/show_spec.rb +++ b/spec/features/groups/show_spec.rb @@ -11,7 +11,7 @@ feature 'Group show page', feature: true do visit path end - it_behaves_like "an autodiscoverable RSS feed with current_user's private token" + it_behaves_like "an autodiscoverable RSS feed with current_user's RSS token" end context 'when signed out' do @@ -19,6 +19,6 @@ feature 'Group show page', feature: true do visit path end - it_behaves_like "an autodiscoverable RSS feed without a private token" + it_behaves_like "an autodiscoverable RSS feed without an RSS token" end end diff --git a/spec/features/issues/award_emoji_spec.rb b/spec/features/issues/award_emoji_spec.rb index 853632614c4..81ae54c7a10 100644 --- a/spec/features/issues/award_emoji_spec.rb +++ b/spec/features/issues/award_emoji_spec.rb @@ -1,8 +1,6 @@ require 'rails_helper' describe 'Awards Emoji', feature: true do - include WaitForVueResource - let!(:project) { create(:project, :public) } let!(:user) { create(:user) } let(:issue) do @@ -22,7 +20,7 @@ describe 'Awards Emoji', feature: true do # The `heart_tip` emoji is not valid anymore so we need to skip validation issue.award_emoji.build(user: user, name: 'heart_tip').save!(validate: false) visit namespace_project_issue_path(project.namespace, project, issue) - wait_for_vue_resource + wait_for_requests end # Regression test: https://gitlab.com/gitlab-org/gitlab-ce/issues/29529 @@ -36,19 +34,19 @@ describe 'Awards Emoji', feature: true do before do visit namespace_project_issue_path(project.namespace, project, issue) - wait_for_vue_resource + wait_for_requests end it 'increments the thumbsdown emoji', js: true do find('[data-name="thumbsdown"]').click - wait_for_ajax + wait_for_requests expect(thumbsdown_emoji).to have_text("1") end context 'click the thumbsup emoji' do it 'increments the thumbsup emoji', js: true do find('[data-name="thumbsup"]').click - wait_for_ajax + wait_for_requests expect(thumbsup_emoji).to have_text("1") end @@ -60,7 +58,7 @@ describe 'Awards Emoji', feature: true do context 'click the thumbsdown emoji' do it 'increments the thumbsdown emoji', js: true do find('[data-name="thumbsdown"]').click - wait_for_ajax + wait_for_requests expect(thumbsdown_emoji).to have_text("1") end @@ -113,7 +111,7 @@ describe 'Awards Emoji', feature: true do click_button 'Comment' end - wait_for_ajax + wait_for_requests end def thumbsup_emoji @@ -143,6 +141,6 @@ describe 'Awards Emoji', feature: true do find('[data-name="smiley"]').click end - wait_for_ajax + wait_for_requests end end diff --git a/spec/features/issues/award_spec.rb b/spec/features/issues/award_spec.rb index 08e3f99e29f..fcf22dd5033 100644 --- a/spec/features/issues/award_spec.rb +++ b/spec/features/issues/award_spec.rb @@ -6,12 +6,10 @@ feature 'Issue awards', js: true, feature: true do let(:issue) { create(:issue, project: project) } describe 'logged in' do - include WaitForVueResource - before do login_as(user) visit namespace_project_issue_path(project.namespace, project, issue) - wait_for_vue_resource + wait_for_requests end it 'adds award to issue' do @@ -41,11 +39,9 @@ feature 'Issue awards', js: true, feature: true do end describe 'logged out' do - include WaitForVueResource - before do visit namespace_project_issue_path(project.namespace, project, issue) - wait_for_vue_resource + wait_for_requests end it 'does not see award menu button' do diff --git a/spec/features/issues/bulk_assignment_labels_spec.rb b/spec/features/issues/bulk_assignment_labels_spec.rb index 1de50d6d77e..0a6f645b27e 100644 --- a/spec/features/issues/bulk_assignment_labels_spec.rb +++ b/spec/features/issues/bulk_assignment_labels_spec.rb @@ -306,7 +306,7 @@ feature 'Issues > Labels bulk assignment', feature: true do page.within('.issues_bulk_update') do click_button 'Labels' - wait_for_ajax + wait_for_requests expect(find('.dropdown-menu-labels li', text: 'bug')).to have_css('.is-active') expect(find('.dropdown-menu-labels li', text: 'feature')).to have_css('.is-indeterminate') @@ -349,7 +349,7 @@ feature 'Issues > Labels bulk assignment', feature: true do def open_milestone_dropdown(items = []) page.within('.issues_bulk_update') do click_button 'Milestone' - wait_for_ajax + wait_for_requests items.map do |item| click_link item end @@ -359,7 +359,7 @@ feature 'Issues > Labels bulk assignment', feature: true do def open_labels_dropdown(items = [], unmark = false) page.within('.issues_bulk_update') do click_button 'Labels' - wait_for_ajax + wait_for_requests items.map do |item| click_link item end @@ -392,6 +392,6 @@ feature 'Issues > Labels bulk assignment', feature: true do def update_issues click_button 'Update issues' - wait_for_ajax + wait_for_requests end end diff --git a/spec/features/issues/create_branch_merge_request_spec.rb b/spec/features/issues/create_branch_merge_request_spec.rb index 44c19275ae5..1d7d8d291b2 100644 --- a/spec/features/issues/create_branch_merge_request_spec.rb +++ b/spec/features/issues/create_branch_merge_request_spec.rb @@ -16,7 +16,7 @@ feature 'Create Branch/Merge Request Dropdown on issue page', feature: true, js: select_dropdown_option('create-mr') - wait_for_ajax + wait_for_requests expect(page).to have_content("created branch 1-cherry-coloured-funk") expect(page).to have_content("mentioned in merge request !1") @@ -32,7 +32,7 @@ feature 'Create Branch/Merge Request Dropdown on issue page', feature: true, js: select_dropdown_option('create-branch') - wait_for_ajax + wait_for_requests expect(page).to have_selector('.dropdown-toggle-text ', text: '1-cherry-coloured-funk') expect(current_path).to eq namespace_project_tree_path(project.namespace, project, '1-cherry-coloured-funk') diff --git a/spec/features/issues/filtered_search/dropdown_author_spec.rb b/spec/features/issues/filtered_search/dropdown_author_spec.rb index 0579d6c80ab..b29177bed06 100644 --- a/spec/features/issues/filtered_search/dropdown_author_spec.rb +++ b/spec/features/issues/filtered_search/dropdown_author_spec.rb @@ -16,7 +16,7 @@ describe 'Dropdown author', js: true, feature: true do end sleep 0.5 - wait_for_ajax + wait_for_requests end def dropdown_author_size diff --git a/spec/features/issues/filtered_search/filter_issues_spec.rb b/spec/features/issues/filtered_search/filter_issues_spec.rb index a8f4e2d7e10..7958ad7e24f 100644 --- a/spec/features/issues/filtered_search/filter_issues_spec.rb +++ b/spec/features/issues/filtered_search/filter_issues_spec.rb @@ -761,7 +761,7 @@ describe 'Filter issues', js: true, feature: true do sort_toggle.click find('.filtered-search-wrapper .dropdown-menu li a', text: 'Oldest updated').click - wait_for_ajax + wait_for_requests expect(find('.issues-list .issue:first-of-type .issue-title-text a')).to have_content(old_issue.title) end @@ -778,17 +778,17 @@ describe 'Filter issues', js: true, feature: true do it 'open state' do find('.issues-state-filters a', text: 'Closed').click - wait_for_ajax + wait_for_requests find('.issues-state-filters a', text: 'Open').click - wait_for_ajax + wait_for_requests expect(page).to have_selector('.issues-list .issue', count: 4) end it 'closed state' do find('.issues-state-filters a', text: 'Closed').click - wait_for_ajax + wait_for_requests expect(page).to have_selector('.issues-list .issue', count: 1) expect(find('.issues-list .issue:first-of-type .issue-title-text a')).to have_content(closed_issue.title) @@ -796,7 +796,7 @@ describe 'Filter issues', js: true, feature: true do it 'all state' do find('.issues-state-filters a', text: 'All').click - wait_for_ajax + wait_for_requests expect(page).to have_selector('.issues-list .issue', count: 5) end @@ -810,10 +810,10 @@ describe 'Filter issues', js: true, feature: true do auto_discovery_link = find('link[type="application/atom+xml"]', visible: false) auto_discovery_params = CGI.parse(URI.parse(auto_discovery_link[:href]).query) - expect(params).to include('private_token' => [user.private_token]) + expect(params).to include('rss_token' => [user.rss_token]) expect(params).to include('milestone_title' => [milestone.title]) expect(params).to include('assignee_id' => [user.id.to_s]) - expect(auto_discovery_params).to include('private_token' => [user.private_token]) + expect(auto_discovery_params).to include('rss_token' => [user.rss_token]) expect(auto_discovery_params).to include('milestone_title' => [milestone.title]) expect(auto_discovery_params).to include('assignee_id' => [user.id.to_s]) end @@ -825,10 +825,10 @@ describe 'Filter issues', js: true, feature: true do auto_discovery_link = find('link[type="application/atom+xml"]', visible: false) auto_discovery_params = CGI.parse(URI.parse(auto_discovery_link[:href]).query) - expect(params).to include('private_token' => [user.private_token]) + expect(params).to include('rss_token' => [user.rss_token]) expect(params).to include('milestone_title' => [milestone.title]) expect(params).to include('assignee_id' => [user.id.to_s]) - expect(auto_discovery_params).to include('private_token' => [user.private_token]) + expect(auto_discovery_params).to include('rss_token' => [user.rss_token]) expect(auto_discovery_params).to include('milestone_title' => [milestone.title]) expect(auto_discovery_params).to include('assignee_id' => [user.id.to_s]) end diff --git a/spec/features/issues/form_spec.rb b/spec/features/issues/form_spec.rb index 095cbb65c16..65d854d0896 100644 --- a/spec/features/issues/form_spec.rb +++ b/spec/features/issues/form_spec.rb @@ -3,7 +3,6 @@ require 'rails_helper' describe 'New/edit issue', :feature, :js do include GitlabRoutingHelper include ActionView::Helpers::JavaScriptHelper - include WaitForAjax let!(:project) { create(:project) } let!(:user) { create(:user)} @@ -24,11 +23,11 @@ describe 'New/edit issue', :feature, :js do visit new_namespace_project_issue_path(project.namespace, project) end - describe 'multiple assignees' do + describe 'single assignee' do before do click_button 'Unassigned' - - wait_for_ajax + + wait_for_requests end it 'unselects other assignees when unassigned is selected' do @@ -36,14 +35,12 @@ describe 'New/edit issue', :feature, :js do click_link user2.name end + click_button user2.name + page.within '.dropdown-menu-user' do click_link 'Unassigned' end - page.within '.js-assignee-search' do - expect(page).to have_content 'Unassigned' - end - expect(find('input[name="issue[assignee_ids][]"]', visible: false).value).to match('0') end @@ -54,11 +51,13 @@ describe 'New/edit issue', :feature, :js do expect(find('a', text: 'Assign to me', visible: false)).not_to be_visible - page.within '.dropdown-menu-user' do + click_button user.name + + page.within('.dropdown-menu-user') do click_link user.name end - expect(find('a', text: 'Assign to me')).to be_visible + expect(page.find('.dropdown-menu-user', visible: false)).not_to be_visible end end @@ -69,7 +68,7 @@ describe 'New/edit issue', :feature, :js do expect(find('a', text: 'Assign to me')).to be_visible click_button 'Unassigned' - wait_for_ajax + wait_for_requests page.within '.dropdown-menu-user' do click_link user2.name @@ -154,25 +153,21 @@ describe 'New/edit issue', :feature, :js do it 'correctly updates the selected user when changing assignee' do click_button 'Unassigned' - - wait_for_ajax + + wait_for_requests page.within '.dropdown-menu-user' do click_link user.name end - expect(find('input[name="issue[assignee_ids][]"]', visible: false).value).to match(user.id.to_s) - expect(find('.dropdown-menu-user a.is-active').first(:xpath, '..')['data-user-id']).to eq(user.id.to_s) - # check the ::before pseudo element to ensure checkmark icon is present - expect(before_for_selector('.dropdown-menu-selectable a.is-active')).not_to eq('') - expect(before_for_selector('.dropdown-menu-selectable a:not(.is-active)')).to eq('') + expect(find('.js-assignee-search')).to have_content(user.name) + click_button user.name page.within '.dropdown-menu-user' do click_link user2.name end - expect(find('input[name="issue[assignee_ids][]"]', visible: false).value).to match(user2.id.to_s) - expect(find('.dropdown-menu-user a.is-active').first(:xpath, '..')['data-user-id']).to eq(user2.id.to_s) + expect(find('.js-assignee-search')).to have_content(user2.name) end end diff --git a/spec/features/issues/gfm_autocomplete_spec.rb b/spec/features/issues/gfm_autocomplete_spec.rb index ad29911248f..350473437a8 100644 --- a/spec/features/issues/gfm_autocomplete_spec.rb +++ b/spec/features/issues/gfm_autocomplete_spec.rb @@ -11,7 +11,7 @@ feature 'GFM autocomplete', feature: true, js: true do login_as(user) visit namespace_project_issue_path(project.namespace, project, issue) - wait_for_ajax + wait_for_requests end it 'opens autocomplete menu when field starts with text' do @@ -40,7 +40,7 @@ feature 'GFM autocomplete', feature: true, js: true do expect(page).to have_selector('.atwho-container') - wait_for_ajax + wait_for_requests expect(find('#at-view-58')).not_to have_selector('.cur:first-of-type') end @@ -80,7 +80,7 @@ feature 'GFM autocomplete', feature: true, js: true do expect(page).to have_selector('.atwho-container') - wait_for_ajax + wait_for_requests expect(find('#at-view-64')).to have_selector('.cur:first-of-type') end @@ -93,7 +93,7 @@ feature 'GFM autocomplete', feature: true, js: true do expect(page).to have_selector('.atwho-container') - wait_for_ajax + wait_for_requests expect(find('#at-view-64')).to have_content(user.name) end @@ -106,7 +106,7 @@ feature 'GFM autocomplete', feature: true, js: true do expect(page).to have_selector('.atwho-container') - wait_for_ajax + wait_for_requests expect(find('#at-view-58')).to have_selector('.cur:first-of-type') end diff --git a/spec/features/issues/issue_sidebar_spec.rb b/spec/features/issues/issue_sidebar_spec.rb index 0de0f93089a..99ad8013023 100644 --- a/spec/features/issues/issue_sidebar_spec.rb +++ b/spec/features/issues/issue_sidebar_spec.rb @@ -23,7 +23,7 @@ feature 'Issue Sidebar', feature: true do find('.block.assignee .edit-link').click - wait_for_ajax + wait_for_requests end it 'shows author in assignee dropdown' do @@ -37,7 +37,7 @@ feature 'Issue Sidebar', feature: true do find('.dropdown-input-field').native.send_keys user2.name sleep 1 # Required to wait for end of input delay - wait_for_ajax + wait_for_requests expect(page).to have_content(user2.name) end @@ -48,7 +48,7 @@ feature 'Issue Sidebar', feature: true do click_button 'assign yourself' - wait_for_ajax + wait_for_requests find('.block.assignee .edit-link').click diff --git a/spec/features/issues/note_polling_spec.rb b/spec/features/issues/note_polling_spec.rb index 58b3215f14c..80f57906506 100644 --- a/spec/features/issues/note_polling_spec.rb +++ b/spec/features/issues/note_polling_spec.rb @@ -18,58 +18,109 @@ feature 'Issue notes polling', :feature, :js do end describe 'updates' do - let(:user) { create(:user) } - let(:note_text) { "Hello World" } - let(:updated_text) { "Bye World" } - let!(:existing_note) { create(:note, noteable: issue, project: project, author: user, note: note_text) } + context 'when from own user' do + let(:user) { create(:user) } + let(:note_text) { "Hello World" } + let(:updated_text) { "Bye World" } + let!(:existing_note) { create(:note, noteable: issue, project: project, author: user, note: note_text) } - before do - login_as(user) - visit namespace_project_issue_path(project.namespace, project, issue) - end + before do + login_as(user) + visit namespace_project_issue_path(project.namespace, project, issue) + end - it 'displays the updated content' do - expect(page).to have_selector("#note_#{existing_note.id}", text: note_text) + it 'has .original-note-content to compare against' do + expect(page).to have_selector("#note_#{existing_note.id}", text: note_text) + expect(page).to have_selector("#note_#{existing_note.id} .original-note-content", count: 1, visible: false) - update_note(existing_note, updated_text) + update_note(existing_note, updated_text) - expect(page).to have_selector("#note_#{existing_note.id}", text: updated_text) - end + expect(page).to have_selector("#note_#{existing_note.id}", text: updated_text) + expect(page).to have_selector("#note_#{existing_note.id} .original-note-content", count: 1, visible: false) + end - it 'when editing but have not changed anything, and an update comes in, show the updated content in the textarea' do - find("#note_#{existing_note.id} .js-note-edit").click + it 'displays the updated content' do + expect(page).to have_selector("#note_#{existing_note.id}", text: note_text) - expect(page).to have_field("note[note]", with: note_text) + update_note(existing_note, updated_text) - update_note(existing_note, updated_text) + expect(page).to have_selector("#note_#{existing_note.id}", text: updated_text) + end - expect(page).to have_field("note[note]", with: updated_text) - end + it 'when editing but have not changed anything, and an update comes in, show the updated content in the textarea' do + find("#note_#{existing_note.id} .js-note-edit").click + + expect(page).to have_field("note[note]", with: note_text) + + update_note(existing_note, updated_text) + + expect(page).to have_field("note[note]", with: updated_text) + end + + it 'when editing but you changed some things, and an update comes in, show a warning' do + find("#note_#{existing_note.id} .js-note-edit").click - it 'when editing but you changed some things, and an update comes in, show a warning' do - find("#note_#{existing_note.id} .js-note-edit").click + expect(page).to have_field("note[note]", with: note_text) - expect(page).to have_field("note[note]", with: note_text) + find("#note_#{existing_note.id} .js-note-text").set('something random') - find("#note_#{existing_note.id} .js-note-text").set('something random') + update_note(existing_note, updated_text) - update_note(existing_note, updated_text) + expect(page).to have_selector(".alert") + end - expect(page).to have_selector(".alert") + it 'when editing but you changed some things, an update comes in, and you press cancel, show the updated content' do + find("#note_#{existing_note.id} .js-note-edit").click + + expect(page).to have_field("note[note]", with: note_text) + + find("#note_#{existing_note.id} .js-note-text").set('something random') + + update_note(existing_note, updated_text) + + find("#note_#{existing_note.id} .note-edit-cancel").click + + expect(page).to have_selector("#note_#{existing_note.id}", text: updated_text) + end end - it 'when editing but you changed some things, an update comes in, and you press cancel, show the updated content' do - find("#note_#{existing_note.id} .js-note-edit").click + context 'when from another user' do + let(:user1) { create(:user) } + let(:user2) { create(:user) } + let(:note_text) { "Hello World" } + let(:updated_text) { "Bye World" } + let!(:existing_note) { create(:note, noteable: issue, project: project, author: user1, note: note_text) } + + before do + login_as(user2) + visit namespace_project_issue_path(project.namespace, project, issue) + end - expect(page).to have_field("note[note]", with: note_text) + it 'has .original-note-content to compare against' do + expect(page).to have_selector("#note_#{existing_note.id}", text: note_text) + expect(page).to have_selector("#note_#{existing_note.id} .original-note-content", count: 1, visible: false) - find("#note_#{existing_note.id} .js-note-text").set('something random') + update_note(existing_note, updated_text) + + expect(page).to have_selector("#note_#{existing_note.id}", text: updated_text) + expect(page).to have_selector("#note_#{existing_note.id} .original-note-content", count: 1, visible: false) + end + end - update_note(existing_note, updated_text) + context 'system notes' do + let(:user) { create(:user) } + let(:note_text) { "Some system note" } + let!(:system_note) { create(:system_note, noteable: issue, project: project, author: user, note: note_text) } - find("#note_#{existing_note.id} .note-edit-cancel").click + before do + login_as(user) + visit namespace_project_issue_path(project.namespace, project, issue) + end - expect(page).to have_selector("#note_#{existing_note.id}", text: updated_text) + it 'has .original-note-content to compare against' do + expect(page).to have_selector("#note_#{system_note.id}", text: note_text) + expect(page).to have_selector("#note_#{system_note.id} .original-note-content", count: 1, visible: false) + end end end diff --git a/spec/features/issues/notes_on_issues_spec.rb b/spec/features/issues/notes_on_issues_spec.rb index a4035324d2b..15c817cabac 100644 --- a/spec/features/issues/notes_on_issues_spec.rb +++ b/spec/features/issues/notes_on_issues_spec.rb @@ -15,7 +15,7 @@ describe 'Create notes on issues', :js, :feature do fill_in 'note[note]', with: note_text click_button 'Comment' - wait_for_ajax + wait_for_requests end it 'creates a note with reference and cross references the issue' do diff --git a/spec/features/issues/update_issues_spec.rb b/spec/features/issues/update_issues_spec.rb index b250fa2ed3c..0911f1db9ba 100644 --- a/spec/features/issues/update_issues_spec.rb +++ b/spec/features/issues/update_issues_spec.rb @@ -108,11 +108,11 @@ feature 'Multiple issue updating from issues#index', feature: true do def click_update_assignee_button find('.js-update-assignee').click - wait_for_ajax + wait_for_requests end def click_update_issues_button find('.update_selected_issues').click - wait_for_ajax + wait_for_requests end end diff --git a/spec/features/issues/user_uses_slash_commands_spec.rb b/spec/features/issues/user_uses_slash_commands_spec.rb index 4cd6c1171ac..d14c319707c 100644 --- a/spec/features/issues/user_uses_slash_commands_spec.rb +++ b/spec/features/issues/user_uses_slash_commands_spec.rb @@ -18,7 +18,7 @@ feature 'Issues > User uses slash commands', feature: true, js: true do end after do - wait_for_ajax + wait_for_requests end describe 'adding a due date from note' do diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb index fdd78600a1d..eecc565d2bd 100644 --- a/spec/features/issues_spec.rb +++ b/spec/features/issues_spec.rb @@ -30,13 +30,6 @@ describe 'Issues', feature: true do it 'opens new issue popup' do expect(page).to have_content("Issue ##{issue.iid}") end - - describe 'fill in' do - before do - fill_in 'issue_title', with: 'bug 345' - fill_in 'issue_description', with: 'bug description' - end - end end describe 'Editing issue assignee' do @@ -384,7 +377,7 @@ describe 'Issues', feature: true do previous_token = find('input#issue_email').value find('.incoming-email-token-reset').trigger('click') - wait_for_ajax + wait_for_requests expect(page).to have_no_field('issue_email', with: previous_token) new_token = project1.new_issue_address(@user.reload) @@ -430,7 +423,7 @@ describe 'Issues', feature: true do expect(page).to have_content 'No assignee' end - # wait_for_ajax does not work with vue-resource at the moment + # wait_for_requests does not work with vue-resource at the moment sleep 1 expect(issue.reload.assignees).to be_empty @@ -465,8 +458,6 @@ describe 'Issues', feature: true do click_link 'Edit' click_link @user.name - find('.dropdown-menu-toggle').click - page.within '.value .author' do expect(page).to have_content @user.name end @@ -474,8 +465,6 @@ describe 'Issues', feature: true do click_link 'Edit' click_link @user.name - find('.dropdown-menu-toggle').click - page.within '.value .assign-yourself' do expect(page).to have_content "No assignee" end @@ -561,15 +550,6 @@ describe 'Issues', feature: true do expect(page).to have_content milestone.title end end - - describe 'removing assignee' do - let(:user2) { create(:user) } - - before do - issue.assignees << user2 - issue.save - end - end end describe 'new issue' do @@ -681,7 +661,7 @@ describe 'Issues', feature: true do click_button date.day end - wait_for_ajax + wait_for_requests expect(find('.value').text).to have_content date.strftime('%b %-d, %Y') end @@ -697,7 +677,7 @@ describe 'Issues', feature: true do click_button date.day end - wait_for_ajax + wait_for_requests expect(page).to have_no_content 'No due date' @@ -709,8 +689,6 @@ describe 'Issues', feature: true do end describe 'title issue#show', js: true do - include WaitForVueResource - it 'updates the title', js: true do issue = create(:issue, author: @user, assignees: [@user], project: project, title: 'new title') @@ -720,7 +698,7 @@ describe 'Issues', feature: true do issue.update(title: "updated title") - wait_for_vue_resource + wait_for_requests expect(page).to have_text("updated title") end end diff --git a/spec/features/merge_requests/closes_issues_spec.rb b/spec/features/merge_requests/closes_issues_spec.rb index ee0880a1e2f..e627618042a 100644 --- a/spec/features/merge_requests/closes_issues_spec.rb +++ b/spec/features/merge_requests/closes_issues_spec.rb @@ -1,8 +1,6 @@ require 'spec_helper' feature 'Merge Request closing issues message', feature: true, js: true do - include WaitForAjax - let(:user) { create(:user) } let(:project) { create(:project, :public) } let(:issue_1) { create(:issue, project: project)} @@ -25,7 +23,7 @@ feature 'Merge Request closing issues message', feature: true, js: true do login_as user visit namespace_project_merge_request_path(project.namespace, project, merge_request) - wait_for_ajax + wait_for_requests end context 'not closing or mentioning any issue' do diff --git a/spec/features/merge_requests/conflicts_spec.rb b/spec/features/merge_requests/conflicts_spec.rb index 04b7593ce68..7f669565085 100644 --- a/spec/features/merge_requests/conflicts_spec.rb +++ b/spec/features/merge_requests/conflicts_spec.rb @@ -23,13 +23,13 @@ feature 'Merge request conflict resolution', js: true, feature: true do end click_button 'Commit conflict resolution' - wait_for_ajax + wait_for_requests expect(page).to have_content('All merge conflicts were resolved') merge_request.reload_diff click_on 'Changes' - wait_for_ajax + wait_for_requests within find('.diff-file', text: 'files/ruby/popen.rb') do expect(page).to have_selector('.line_content.new', text: "vars = { 'PWD' => path }") @@ -53,23 +53,23 @@ feature 'Merge request conflict resolution', js: true, feature: true do within find('.files-wrapper .diff-file', text: 'files/ruby/popen.rb') do click_button 'Edit inline' - wait_for_ajax + wait_for_requests execute_script('ace.edit($(".files-wrapper .diff-file pre")[0]).setValue("One morning");') end within find('.files-wrapper .diff-file', text: 'files/ruby/regex.rb') do click_button 'Edit inline' - wait_for_ajax + wait_for_requests execute_script('ace.edit($(".files-wrapper .diff-file pre")[1]).setValue("Gregor Samsa woke from troubled dreams");') end click_button 'Commit conflict resolution' - wait_for_ajax + wait_for_requests expect(page).to have_content('All merge conflicts were resolved') merge_request.reload_diff click_on 'Changes' - wait_for_ajax + wait_for_requests expect(page).to have_content('One morning') expect(page).to have_content('Gregor Samsa woke from troubled dreams') @@ -126,21 +126,21 @@ feature 'Merge request conflict resolution', js: true, feature: true do it 'conflicts are resolved in Edit inline mode' do within find('.files-wrapper .diff-file', text: 'files/markdown/ruby-style-guide.md') do - wait_for_ajax + wait_for_requests execute_script('ace.edit($(".files-wrapper .diff-file pre")[0]).setValue("Gregor Samsa woke from troubled dreams");') end click_button 'Commit conflict resolution' - wait_for_ajax + wait_for_requests expect(page).to have_content('All merge conflicts were resolved') merge_request.reload_diff click_on 'Changes' - wait_for_ajax + wait_for_requests click_link 'Expand all' - wait_for_ajax + wait_for_requests expect(page).to have_content('Gregor Samsa woke from troubled dreams') end @@ -171,7 +171,7 @@ feature 'Merge request conflict resolution', js: true, feature: true do it 'shows an error if the conflicts page is visited directly' do visit current_url + '/conflicts' - wait_for_ajax + wait_for_requests expect(find('#conflicts')).to have_content('Please try to resolve them locally.') end diff --git a/spec/features/merge_requests/create_new_mr_spec.rb b/spec/features/merge_requests/create_new_mr_spec.rb index f1b3e7f158c..82987c768d1 100644 --- a/spec/features/merge_requests/create_new_mr_spec.rb +++ b/spec/features/merge_requests/create_new_mr_spec.rb @@ -1,8 +1,6 @@ require 'spec_helper' feature 'Create New Merge Request', feature: true, js: true do - include WaitForVueResource - let(:user) { create(:user) } let(:project) { create(:project, :public) } @@ -146,7 +144,7 @@ feature 'Create New Merge Request', feature: true, js: true do page.within('.merge-request') do click_link 'Pipelines' - wait_for_vue_resource + wait_for_requests expect(page).to have_content "##{pipeline.id}" end diff --git a/spec/features/merge_requests/deleted_source_branch_spec.rb b/spec/features/merge_requests/deleted_source_branch_spec.rb index 01e5e4f3a05..1723fb7d365 100644 --- a/spec/features/merge_requests/deleted_source_branch_spec.rb +++ b/spec/features/merge_requests/deleted_source_branch_spec.rb @@ -32,7 +32,7 @@ describe 'Deleted source branch', feature: true, js: true do end click_on 'Changes' - wait_for_ajax + wait_for_requests expect(page).to have_selector('.diffs.tab-pane .nothing-here-block') expect(page).to have_content('Source branch does not exist.') diff --git a/spec/features/merge_requests/diff_notes_avatars_spec.rb b/spec/features/merge_requests/diff_notes_avatars_spec.rb index b2e170513c4..854e2d1758f 100644 --- a/spec/features/merge_requests/diff_notes_avatars_spec.rb +++ b/spec/features/merge_requests/diff_notes_avatars_spec.rb @@ -60,7 +60,7 @@ feature 'Diff note avatars', feature: true, js: true do click_button 'Comment' - wait_for_ajax + wait_for_requests end visit namespace_project_merge_request_path(project.namespace, project, merge_request) @@ -76,7 +76,7 @@ feature 'Diff note avatars', feature: true, js: true do before do visit diffs_namespace_project_merge_request_path(project.namespace, project, merge_request, view: view) - wait_for_ajax + wait_for_requests end it 'shows note avatar' do @@ -91,7 +91,7 @@ feature 'Diff note avatars', feature: true, js: true do page.within find("[id='#{position.line_code(project.repository)}']") do find('.diff-notes-collapse').click - expect(first('img.js-diff-comment-avatar')["title"]).to eq("#{note.author.name}: #{note.note.truncate(17)}") + expect(first('img.js-diff-comment-avatar')["data-original-title"]).to eq("#{note.author.name}: #{note.note.truncate(17)}") end end @@ -114,7 +114,7 @@ feature 'Diff note avatars', feature: true, js: true do find('.js-note-delete').click end - wait_for_ajax + wait_for_requests page.within find("[id='#{position.line_code(project.repository)}']") do expect(page).not_to have_selector('img.js-diff-comment-avatar') @@ -129,7 +129,7 @@ feature 'Diff note avatars', feature: true, js: true do click_button 'Comment' - wait_for_ajax + wait_for_requests end page.within find("[id='#{position.line_code(project.repository)}']") do @@ -148,7 +148,7 @@ feature 'Diff note avatars', feature: true, js: true do find('.js-comment-button').trigger 'click' - wait_for_ajax + wait_for_requests end end @@ -166,7 +166,7 @@ feature 'Diff note avatars', feature: true, js: true do visit diffs_namespace_project_merge_request_path(project.namespace, project, merge_request, view: view) - wait_for_ajax + wait_for_requests end it 'shows extra comment count' do diff --git a/spec/features/merge_requests/diffs_spec.rb b/spec/features/merge_requests/diffs_spec.rb index 7dee3b852ca..4860a2a7498 100644 --- a/spec/features/merge_requests/diffs_spec.rb +++ b/spec/features/merge_requests/diffs_spec.rb @@ -20,6 +20,34 @@ feature 'Diffs URL', js: true, feature: true do end end + context 'when linking to note' do + describe 'with unresolved note' do + let(:note) { create :diff_note_on_merge_request, project: project, noteable: merge_request } + let(:fragment) { "#note_#{note.id}" } + + before do + visit "#{diffs_namespace_project_merge_request_path(project.namespace, project, merge_request)}#{fragment}" + end + + it 'shows expanded note' do + expect(page).to have_selector(fragment, visible: true) + end + end + + describe 'with resolved note' do + let(:note) { create :diff_note_on_merge_request, :resolved, project: project, noteable: merge_request } + let(:fragment) { "#note_#{note.id}" } + + before do + visit "#{diffs_namespace_project_merge_request_path(project.namespace, project, merge_request)}#{fragment}" + end + + it 'shows expanded note' do + expect(page).to have_selector(fragment, visible: true) + end + end + end + context 'when merge request has overflow' do it 'displays warning' do allow(Commit).to receive(:max_diff_options).and_return(max_files: 3) diff --git a/spec/features/merge_requests/discussion_spec.rb b/spec/features/merge_requests/discussion_spec.rb index f59d0faa274..1a09cc54c2e 100644 --- a/spec/features/merge_requests/discussion_spec.rb +++ b/spec/features/merge_requests/discussion_spec.rb @@ -43,7 +43,7 @@ feature 'Merge Request Discussions', feature: true do it 'shows a link to the outdated diff' do within(".discussion[data-discussion-id='#{outdated_discussion.id}']") do path = diffs_namespace_project_merge_request_path(project.namespace, project, merge_request, diff_id: old_merge_request_diff.id, anchor: outdated_discussion.line_code) - expect(page).to have_link('an outdated diff', href: path) + expect(page).to have_link('an old version of the diff', href: path) end end end diff --git a/spec/features/merge_requests/filter_merge_requests_spec.rb b/spec/features/merge_requests/filter_merge_requests_spec.rb index 2da60e9f4ad..1e26b3d601e 100644 --- a/spec/features/merge_requests/filter_merge_requests_spec.rb +++ b/spec/features/merge_requests/filter_merge_requests_spec.rb @@ -289,7 +289,7 @@ describe 'Filter merge requests', feature: true do page.within '.dropdown-menu-sort' do click_link 'Oldest created' end - wait_for_ajax + wait_for_requests page.within '.mr-list' do expect(page).to have_content('Frontend') diff --git a/spec/features/merge_requests/form_spec.rb b/spec/features/merge_requests/form_spec.rb index f8518f450dc..00ef1ffdddc 100644 --- a/spec/features/merge_requests/form_spec.rb +++ b/spec/features/merge_requests/form_spec.rb @@ -90,7 +90,7 @@ describe 'New/edit merge request', feature: true, js: true do page.within '.issuable-meta' do merge_request = MergeRequest.find_by(source_branch: 'fix') - expect(page).to have_text("Merge Request #{merge_request.to_reference}") + expect(page).to have_text("Merge request #{merge_request.to_reference}") # compare paths because the host differ in test expect(find_link(merge_request.to_reference)[:href]) .to end_with(merge_request_path(merge_request)) diff --git a/spec/features/merge_requests/merge_immediately_with_pipeline_spec.rb b/spec/features/merge_requests/merge_immediately_with_pipeline_spec.rb index 5820784f8e7..c1d4d508e57 100644 --- a/spec/features/merge_requests/merge_immediately_with_pipeline_spec.rb +++ b/spec/features/merge_requests/merge_immediately_with_pipeline_spec.rb @@ -34,11 +34,13 @@ feature 'Merge immediately', :feature, :js do page.within '.mr-widget-body' do find('.dropdown-toggle').click - click_link 'Merge immediately' + Sidekiq::Testing.fake! do + click_link 'Merge immediately' - expect(find('.accept-merge-request.btn-info')).to have_content('Merge in progress') + expect(find('.accept-merge-request.btn-info')).to have_content('Merge in progress') - wait_for_ajax + wait_for_requests + end end end end diff --git a/spec/features/merge_requests/merge_when_pipeline_succeeds_spec.rb b/spec/features/merge_requests/merge_when_pipeline_succeeds_spec.rb index 11b6f0c0a64..e08721b4724 100644 --- a/spec/features/merge_requests/merge_when_pipeline_succeeds_spec.rb +++ b/spec/features/merge_requests/merge_when_pipeline_succeeds_spec.rb @@ -13,12 +13,12 @@ feature 'Merge When Pipeline Succeeds', :feature, :js do let(:pipeline) do create(:ci_pipeline, project: project, sha: merge_request.diff_head_sha, - ref: merge_request.source_branch) + ref: merge_request.source_branch, + head_pipeline_of: merge_request) end before do project.add_master(user) - merge_request.update(head_pipeline_id: pipeline.id) end context 'when there is active pipeline for merge request' do diff --git a/spec/features/merge_requests/mini_pipeline_graph_spec.rb b/spec/features/merge_requests/mini_pipeline_graph_spec.rb index 5b2798af32f..51e7467c14c 100644 --- a/spec/features/merge_requests/mini_pipeline_graph_spec.rb +++ b/spec/features/merge_requests/mini_pipeline_graph_spec.rb @@ -56,7 +56,7 @@ feature 'Mini Pipeline Graph', :js, :feature do before do toggle.click - wait_for_ajax + wait_for_requests end it 'should open when toggle is clicked' do diff --git a/spec/features/merge_requests/only_allow_merge_if_build_succeeds_spec.rb b/spec/features/merge_requests/only_allow_merge_if_build_succeeds_spec.rb index cdda0542c51..b1dc81a606a 100644 --- a/spec/features/merge_requests/only_allow_merge_if_build_succeeds_spec.rb +++ b/spec/features/merge_requests/only_allow_merge_if_build_succeeds_spec.rb @@ -1,8 +1,6 @@ require 'spec_helper' feature 'Only allow merge requests to be merged if the pipeline succeeds', feature: true, js: true do - include WaitForVueResource - let(:merge_request) { create(:merge_request_with_diffs) } let(:project) { merge_request.target_project } @@ -16,7 +14,7 @@ feature 'Only allow merge requests to be merged if the pipeline succeeds', featu it 'allows MR to be merged' do visit_merge_request(merge_request) - wait_for_vue_resource + wait_for_requests expect(page).to have_button 'Merge' end @@ -28,11 +26,9 @@ feature 'Only allow merge requests to be merged if the pipeline succeeds', featu project: project, sha: merge_request.diff_head_sha, ref: merge_request.source_branch, - status: status) + status: status, head_pipeline_of: merge_request) end - before { merge_request.update(head_pipeline: pipeline) } - context 'when merge requests can only be merged if the pipeline succeeds' do before do project.update_attribute(:only_allow_merge_if_pipeline_succeeds, true) @@ -44,7 +40,7 @@ feature 'Only allow merge requests to be merged if the pipeline succeeds', featu it 'does not allow to merge immediately' do visit_merge_request(merge_request) - wait_for_vue_resource + wait_for_requests expect(page).to have_button 'Merge when pipeline succeeds' expect(page).not_to have_button 'Select merge moment' @@ -57,7 +53,7 @@ feature 'Only allow merge requests to be merged if the pipeline succeeds', featu it 'does not allow MR to be merged' do visit_merge_request(merge_request) - wait_for_vue_resource + wait_for_requests expect(page).to have_css('button[disabled="disabled"]', text: 'Merge') expect(page).to have_content('Please retry the job or push a new commit to fix the failure.') @@ -70,7 +66,7 @@ feature 'Only allow merge requests to be merged if the pipeline succeeds', featu it 'does not allow MR to be merged' do visit_merge_request(merge_request) - wait_for_vue_resource + wait_for_requests expect(page).not_to have_button 'Merge' expect(page).to have_content('Please retry the job or push a new commit to fix the failure.') @@ -83,7 +79,7 @@ feature 'Only allow merge requests to be merged if the pipeline succeeds', featu it 'allows MR to be merged' do visit_merge_request(merge_request) - wait_for_vue_resource + wait_for_requests expect(page).to have_button 'Merge' end @@ -95,7 +91,7 @@ feature 'Only allow merge requests to be merged if the pipeline succeeds', featu it 'allows MR to be merged' do visit_merge_request(merge_request) - wait_for_vue_resource + wait_for_requests expect(page).to have_button 'Merge' end @@ -113,7 +109,7 @@ feature 'Only allow merge requests to be merged if the pipeline succeeds', featu it 'allows MR to be merged immediately' do visit_merge_request(merge_request) - wait_for_vue_resource + wait_for_requests expect(page).to have_button 'Merge when pipeline succeeds' @@ -128,7 +124,7 @@ feature 'Only allow merge requests to be merged if the pipeline succeeds', featu it 'allows MR to be merged' do visit_merge_request(merge_request) - wait_for_vue_resource + wait_for_requests expect(page).to have_button 'Merge' end @@ -140,7 +136,7 @@ feature 'Only allow merge requests to be merged if the pipeline succeeds', featu it 'allows MR to be merged' do visit_merge_request(merge_request) - wait_for_vue_resource + wait_for_requests expect(page).to have_button 'Merge' end diff --git a/spec/features/merge_requests/pipelines_spec.rb b/spec/features/merge_requests/pipelines_spec.rb index 99e283ac181..4c76004cb93 100644 --- a/spec/features/merge_requests/pipelines_spec.rb +++ b/spec/features/merge_requests/pipelines_spec.rb @@ -26,7 +26,7 @@ feature 'Pipelines for Merge Requests', feature: true, js: true do page.within('.merge-request-tabs') do click_link('Pipelines') end - wait_for_ajax + wait_for_requests expect(page).to have_selector('.pipeline-actions') end diff --git a/spec/features/merge_requests/update_merge_requests_spec.rb b/spec/features/merge_requests/update_merge_requests_spec.rb index 9ecc998785b..4ef59a8aeb8 100644 --- a/spec/features/merge_requests/update_merge_requests_spec.rb +++ b/spec/features/merge_requests/update_merge_requests_spec.rb @@ -107,7 +107,7 @@ feature 'Multiple merge requests updating from merge_requests#index', feature: t def change_assignee(text) find('#check_all_issues').click find('.js-update-assignee').click - wait_for_ajax + wait_for_requests page.within '.dropdown-menu-user' do click_link text @@ -125,6 +125,6 @@ feature 'Multiple merge requests updating from merge_requests#index', feature: t def click_update_merge_requests_button find('.update_selected_issues').click - wait_for_ajax + wait_for_requests end end diff --git a/spec/features/merge_requests/user_posts_diff_notes_spec.rb b/spec/features/merge_requests/user_posts_diff_notes_spec.rb index 7756202e3f5..14bc549c9f9 100644 --- a/spec/features/merge_requests/user_posts_diff_notes_spec.rb +++ b/spec/features/merge_requests/user_posts_diff_notes_spec.rb @@ -73,7 +73,7 @@ feature 'Merge requests > User posts diff notes', :js do context 'with an unfolded line' do before(:each) do find('.js-unfold', match: :first).click - wait_for_ajax + wait_for_requests end # The first `.js-unfold` unfolds upwards, therefore the first @@ -122,7 +122,7 @@ feature 'Merge requests > User posts diff notes', :js do context 'with an unfolded line' do before(:each) do find('.js-unfold', match: :first).click - wait_for_ajax + wait_for_requests end # The first `.js-unfold` unfolds upwards, therefore the first @@ -213,7 +213,7 @@ feature 'Merge requests > User posts diff notes', :js do write_comment_on_line(line_holder, diff_side) click_button 'Comment' - wait_for_ajax + wait_for_requests assert_comment_persistence(line_holder, asset_form_reset: asset_form_reset) end diff --git a/spec/features/merge_requests/user_posts_notes_spec.rb b/spec/features/merge_requests/user_posts_notes_spec.rb index 7fc0e2ce6ec..06de072257a 100644 --- a/spec/features/merge_requests/user_posts_notes_spec.rb +++ b/spec/features/merge_requests/user_posts_notes_spec.rb @@ -98,7 +98,7 @@ describe 'Merge requests > User posts notes', :js do find('.btn-save').click end - wait_for_ajax + wait_for_requests find('.note').hover find('.js-note-edit').click @@ -139,7 +139,7 @@ describe 'Merge requests > User posts notes', :js do find('.js-note-attachment-delete').click is_expected.not_to have_css('.note-attachment') is_expected.not_to have_css('.current-note-edit-form') - wait_for_ajax + wait_for_requests end end end diff --git a/spec/features/merge_requests/user_uses_slash_commands_spec.rb b/spec/features/merge_requests/user_uses_slash_commands_spec.rb index f0ad57eb92f..0e64a3e1a4b 100644 --- a/spec/features/merge_requests/user_uses_slash_commands_spec.rb +++ b/spec/features/merge_requests/user_uses_slash_commands_spec.rb @@ -21,7 +21,7 @@ feature 'Merge Requests > User uses slash commands', feature: true, js: true do end after do - wait_for_ajax + wait_for_requests end describe 'toggling the WIP prefix in the title from note' do @@ -160,7 +160,7 @@ feature 'Merge Requests > User uses slash commands', feature: true, js: true do it 'changes target branch from a note' do write_note("message start \n/target_branch merge-test\n message end.") - wait_for_ajax + wait_for_requests expect(page).not_to have_content('/target_branch') expect(page).to have_content('message start') expect(page).to have_content('message end.') diff --git a/spec/features/merge_requests/versions_spec.rb b/spec/features/merge_requests/versions_spec.rb index 2b5b803946c..aad522ee26e 100644 --- a/spec/features/merge_requests/versions_spec.rb +++ b/spec/features/merge_requests/versions_spec.rb @@ -75,7 +75,7 @@ feature 'Merge Request versions', js: true, feature: true do find(".js-comment-button").click end - wait_for_ajax + wait_for_requests expect(page).to have_content("Typo, please fix") end @@ -124,9 +124,11 @@ feature 'Merge Request versions', js: true, feature: true do diff_refs: merge_request_diff3.compare_with(merge_request_diff1.head_commit_sha).diff_refs ) outdated_diff_note = create(:diff_note_on_merge_request, project: project, noteable: merge_request, position: position) + outdated_diff_note.position = outdated_diff_note.original_position + outdated_diff_note.save! visit current_url - wait_for_ajax + wait_for_requests expect(page).to have_css(".diffs .notes[data-discussion-id='#{outdated_diff_note.discussion_id}']") end @@ -144,7 +146,7 @@ feature 'Merge Request versions', js: true, feature: true do find(".js-comment-button").click end - wait_for_ajax + wait_for_requests expect(page).to have_content("Typo, please fix") end diff --git a/spec/features/merge_requests/widget_deployments_spec.rb b/spec/features/merge_requests/widget_deployments_spec.rb index 8370499f6ed..118ecd9cba5 100644 --- a/spec/features/merge_requests/widget_deployments_spec.rb +++ b/spec/features/merge_requests/widget_deployments_spec.rb @@ -18,7 +18,7 @@ feature 'Widget Deployments Header', feature: true, js: true do end scenario 'displays that the environment is deployed' do - wait_for_ajax + wait_for_requests expect(page).to have_content("Deployed to #{environment.name}") expect(find('.js-deploy-time')['data-title']).to eq(deployment.created_at.to_time.in_time_zone.to_s(:medium)) @@ -34,7 +34,7 @@ feature 'Widget Deployments Header', feature: true, js: true do end background do - wait_for_ajax + wait_for_requests end scenario 'does show stop button' do diff --git a/spec/features/merge_requests/widget_spec.rb b/spec/features/merge_requests/widget_spec.rb index ae799584c0f..be8b1423c20 100644 --- a/spec/features/merge_requests/widget_spec.rb +++ b/spec/features/merge_requests/widget_spec.rb @@ -27,7 +27,7 @@ describe 'Merge request', :feature, :js do it 'shows widget status after creating new merge request' do click_button 'Submit merge request' - wait_for_ajax + wait_for_requests expect(page).to have_selector('.accept-merge-request') expect(find('.accept-merge-request')['disabled']).not_to be(true) @@ -48,7 +48,7 @@ describe 'Merge request', :feature, :js do end it 'shows environments link' do - wait_for_ajax + wait_for_requests page.within('.mr-widget-heading') do expect(page).to have_content("Deployed to #{environment.name}") @@ -58,7 +58,7 @@ describe 'Merge request', :feature, :js do it 'shows green accept merge request button' do # Wait for the `ci_status` and `merge_check` requests - wait_for_ajax + wait_for_requests expect(page).to have_selector('.accept-merge-request') expect(find('.accept-merge-request')['disabled']).not_to be(true) end @@ -76,7 +76,7 @@ describe 'Merge request', :feature, :js do it 'has danger button while waiting for external CI status' do # Wait for the `ci_status` and `merge_check` requests - wait_for_ajax + wait_for_requests expect(page).to have_selector('.accept-merge-request.btn-danger') end end @@ -88,32 +88,29 @@ describe 'Merge request', :feature, :js do sha: merge_request.diff_head_sha, ref: merge_request.source_branch, status: 'failed', - statuses: [commit_status]) + statuses: [commit_status], + head_pipeline_of: merge_request) create(:ci_build, :pending, pipeline: pipeline) - merge_request.update(head_pipeline: pipeline) - visit namespace_project_merge_request_path(project.namespace, project, merge_request) end it 'has danger button when not succeeded' do # Wait for the `ci_status` and `merge_check` requests - wait_for_ajax + wait_for_requests expect(page).to have_selector('.accept-merge-request.btn-danger') end end context 'when merge request is in the blocked pipeline state' do before do - pipeline = create( + create( :ci_pipeline, project: project, sha: merge_request.diff_head_sha, ref: merge_request.source_branch, - status: :manual - ) - - merge_request.update(head_pipeline: pipeline) + status: :manual, + head_pipeline_of: merge_request) visit namespace_project_merge_request_path(project.namespace, project, @@ -135,17 +132,16 @@ describe 'Merge request', :feature, :js do sha: merge_request.diff_head_sha, ref: merge_request.source_branch, status: 'pending', - statuses: [commit_status]) + statuses: [commit_status], + head_pipeline_of: merge_request) create(:ci_build, :pending, pipeline: pipeline) - merge_request.update(head_pipeline: pipeline) - visit namespace_project_merge_request_path(project.namespace, project, merge_request) end it 'has info button when MWBS button' do # Wait for the `ci_status` and `merge_check` requests - wait_for_ajax + wait_for_requests expect(page).to have_selector('.accept-merge-request.btn-info') end end @@ -163,7 +159,7 @@ describe 'Merge request', :feature, :js do it 'shows information about the merge error' do # Wait for the `ci_status` and `merge_check` requests - wait_for_ajax + wait_for_requests page.within('.mr-widget-body') do expect(page).to have_content('Something went wrong') @@ -184,7 +180,7 @@ describe 'Merge request', :feature, :js do it 'shows information about the merge error' do # Wait for the `ci_status` and `merge_check` requests - wait_for_ajax + wait_for_requests page.within('.mr-widget-body') do expect(page).to have_content('Something went wrong') diff --git a/spec/features/milestones/milestones_spec.rb b/spec/features/milestones/milestones_spec.rb index 9eec3d7f270..b3dfd6d0e81 100644 --- a/spec/features/milestones/milestones_spec.rb +++ b/spec/features/milestones/milestones_spec.rb @@ -78,7 +78,7 @@ describe 'Milestone draggable', feature: true, js: true do scroll_into_view('.milestone-content') drag_to(selector: '.issues-sortable-list', list_to_index: 1) - wait_for_ajax + wait_for_requests end def create_and_drag_merge_request(params = {}) @@ -87,12 +87,12 @@ describe 'Milestone draggable', feature: true, js: true do visit namespace_project_milestone_path(project.namespace, project, milestone) page.find("a[href='#tab-merge-requests']").click - wait_for_ajax + wait_for_requests scroll_into_view('.milestone-content') drag_to(selector: '.merge_requests-sortable-list', list_to_index: 1) - wait_for_ajax + wait_for_requests end def scroll_into_view(selector) diff --git a/spec/features/profile_spec.rb b/spec/features/profile_spec.rb index e63feb14b7e..7df628fd7a0 100644 --- a/spec/features/profile_spec.rb +++ b/spec/features/profile_spec.rb @@ -47,6 +47,21 @@ describe 'Profile account page', feature: true do end end + describe 'when I reset RSS token' do + before do + visit profile_account_path + end + + it 'resets RSS token' do + previous_token = find("#rss-token").value + + click_link('Reset RSS token') + + expect(page).to have_content 'RSS token was successfully reset' + expect(find('#rss-token').value).not_to eq(previous_token) + end + end + describe 'when I reset incoming email token' do before do allow(Gitlab.config.incoming_email).to receive(:enabled).and_return(true) diff --git a/spec/features/projects/activity/rss_spec.rb b/spec/features/projects/activity/rss_spec.rb index b47c6d431eb..3c1de5c09b2 100644 --- a/spec/features/projects/activity/rss_spec.rb +++ b/spec/features/projects/activity/rss_spec.rb @@ -16,7 +16,7 @@ feature 'Project Activity RSS' do visit path end - it_behaves_like "it has an RSS button with current_user's private token" + it_behaves_like "it has an RSS button with current_user's RSS token" end context 'when signed out' do @@ -24,6 +24,6 @@ feature 'Project Activity RSS' do visit path end - it_behaves_like "it has an RSS button without a private token" + it_behaves_like "it has an RSS button without an RSS token" end end diff --git a/spec/features/projects/artifacts/file_spec.rb b/spec/features/projects/artifacts/file_spec.rb index 74308a7e8dd..25db908d917 100644 --- a/spec/features/projects/artifacts/file_spec.rb +++ b/spec/features/projects/artifacts/file_spec.rb @@ -13,7 +13,7 @@ feature 'Artifact file', :js, feature: true do before do visit_file('other_artifacts_0.1.2/doc_sample.txt') - wait_for_ajax + wait_for_requests end it 'displays an error' do @@ -37,7 +37,7 @@ feature 'Artifact file', :js, feature: true do before do visit_file('rails_sample.jpg') - wait_for_ajax + wait_for_requests end it 'displays the blob' do diff --git a/spec/features/projects/blobs/blob_show_spec.rb b/spec/features/projects/blobs/blob_show_spec.rb index 9888624a509..82cfbfda157 100644 --- a/spec/features/projects/blobs/blob_show_spec.rb +++ b/spec/features/projects/blobs/blob_show_spec.rb @@ -6,7 +6,7 @@ feature 'File blob', :js, feature: true do def visit_blob(path, fragment = nil) visit namespace_project_blob_path(project.namespace, project, File.join('master', path), anchor: fragment) - wait_for_ajax + wait_for_requests end context 'Ruby file' do @@ -61,7 +61,7 @@ feature 'File blob', :js, feature: true do before do find('.js-blob-viewer-switch-btn[data-viewer=simple]').click - wait_for_ajax + wait_for_requests end it 'displays the blob using the simple viewer' do @@ -82,7 +82,7 @@ feature 'File blob', :js, feature: true do before do find('.js-blob-viewer-switch-btn[data-viewer=rich]').click - wait_for_ajax + wait_for_requests end it 'displays the blob using the rich viewer' do @@ -170,7 +170,7 @@ feature 'File blob', :js, feature: true do before do find('.js-blob-viewer-switcher .js-blob-viewer-switch-btn[data-viewer=simple]').click - wait_for_ajax + wait_for_requests end it 'displays an error' do @@ -423,7 +423,43 @@ feature 'File blob', :js, feature: true do expect(page).to have_content('This project is licensed under the MIT License.') # shows a learn more link - expect(page).to have_link('Learn more about this license', 'http://choosealicense.com/licenses/mit/') + expect(page).to have_link('Learn more', 'http://choosealicense.com/licenses/mit/') + end + end + end + + context '*.gemspec' do + before do + project.add_master(project.creator) + + Files::CreateService.new( + project, + project.creator, + start_branch: 'master', + branch_name: 'master', + commit_message: "Add activerecord.gemspec", + file_path: 'activerecord.gemspec', + file_content: <<-SPEC.strip_heredoc + Gem::Specification.new do |s| + s.platform = Gem::Platform::RUBY + s.name = "activerecord" + end + SPEC + ).execute + + visit_blob('activerecord.gemspec') + end + + it 'displays an auxiliary viewer' do + aggregate_failures do + # shows names of dependency manager and package + expect(page).to have_content('This project manages its dependencies using RubyGems and defines a gem named activerecord.') + + # shows a link to the gem + expect(page).to have_link('activerecord', 'https://rubygems.org/gems/activerecord') + + # shows a learn more link + expect(page).to have_link('Learn more', 'http://choosealicense.com/licenses/mit/') end end end diff --git a/spec/features/projects/blobs/edit_spec.rb b/spec/features/projects/blobs/edit_spec.rb index cc5b1a7e734..1a38997450d 100644 --- a/spec/features/projects/blobs/edit_spec.rb +++ b/spec/features/projects/blobs/edit_spec.rb @@ -18,7 +18,7 @@ feature 'Editing file blob', feature: true, js: true do end def edit_and_commit - wait_for_ajax + wait_for_requests find('.js-edit-blob').click execute_script('ace.edit("editor").setValue("class NextFeature\nend\n")') click_button 'Commit changes' diff --git a/spec/features/projects/blobs/user_create_spec.rb b/spec/features/projects/blobs/user_create_spec.rb index d805450e095..4b6c55f5f44 100644 --- a/spec/features/projects/blobs/user_create_spec.rb +++ b/spec/features/projects/blobs/user_create_spec.rb @@ -15,7 +15,7 @@ feature 'New blob creation', feature: true, js: true do end def edit_file - wait_for_ajax + wait_for_requests fill_in 'file_name', with: 'feature.rb' execute_script("ace.edit('editor').setValue('#{content}')") end diff --git a/spec/features/projects/commit/cherry_pick_spec.rb b/spec/features/projects/commit/cherry_pick_spec.rb index fa67d390c47..bc7ca0ddd38 100644 --- a/spec/features/projects/commit/cherry_pick_spec.rb +++ b/spec/features/projects/commit/cherry_pick_spec.rb @@ -72,11 +72,11 @@ describe 'Cherry-pick Commits' do click_button 'master' end - wait_for_ajax + wait_for_requests page.within('#modal-cherry-pick-commit .dropdown-menu') do find('.dropdown-input input').set('feature') - wait_for_ajax + wait_for_requests click_link "feature" end diff --git a/spec/features/projects/commit/mini_pipeline_graph_spec.rb b/spec/features/projects/commit/mini_pipeline_graph_spec.rb index 98c0f2c63b0..f2de195eb7f 100644 --- a/spec/features/projects/commit/mini_pipeline_graph_spec.rb +++ b/spec/features/projects/commit/mini_pipeline_graph_spec.rb @@ -32,7 +32,7 @@ feature 'Mini Pipeline Graph in Commit View', :js, :feature do it 'should show the builds list when stage is clicked' do first('.mini-pipeline-graph-dropdown-toggle').click - wait_for_ajax + wait_for_requests page.within '.js-builds-dropdown-list' do expect(page).to have_selector('.ci-status-icon-running') diff --git a/spec/features/projects/commit/rss_spec.rb b/spec/features/projects/commit/rss_spec.rb index 6e0e1916f87..03b6d560c96 100644 --- a/spec/features/projects/commit/rss_spec.rb +++ b/spec/features/projects/commit/rss_spec.rb @@ -12,8 +12,8 @@ feature 'Project Commits RSS' do visit path end - it_behaves_like "it has an RSS button with current_user's private token" - it_behaves_like "an autodiscoverable RSS feed with current_user's private token" + it_behaves_like "it has an RSS button with current_user's RSS token" + it_behaves_like "an autodiscoverable RSS feed with current_user's RSS token" end context 'when signed out' do @@ -21,7 +21,7 @@ feature 'Project Commits RSS' do visit path end - it_behaves_like "it has an RSS button without a private token" - it_behaves_like "an autodiscoverable RSS feed without a private token" + it_behaves_like "it has an RSS button without an RSS token" + it_behaves_like "an autodiscoverable RSS feed without an RSS token" end end diff --git a/spec/features/projects/compare_spec.rb b/spec/features/projects/compare_spec.rb index b2a3b111c9e..4162f2579d1 100644 --- a/spec/features/projects/compare_spec.rb +++ b/spec/features/projects/compare_spec.rb @@ -52,8 +52,12 @@ describe "Compare", js: true do def select_using_dropdown(dropdown_type, selection) dropdown = find(".js-compare-#{dropdown_type}-dropdown") dropdown.find(".compare-dropdown-toggle").click + # find input before using to wait for the inputs visiblity + dropdown.find('.dropdown-menu') dropdown.fill_in("Filter by Git revision", with: selection) - wait_for_ajax - dropdown.find_all("a[data-ref=\"#{selection}\"]", visible: true).last.click + wait_for_requests + # find before all to wait for the items visiblity + dropdown.find("a[data-ref=\"#{selection}\"]", match: :first) + dropdown.all("a[data-ref=\"#{selection}\"]").last.click end end diff --git a/spec/features/projects/features_visibility_spec.rb b/spec/features/projects/features_visibility_spec.rb index 4533a6fb144..c49648f54bd 100644 --- a/spec/features/projects/features_visibility_spec.rb +++ b/spec/features/projects/features_visibility_spec.rb @@ -21,17 +21,17 @@ describe 'Edit Project Settings', feature: true do select 'Disabled', from: "project_project_feature_attributes_#{tool_name}_access_level" click_button 'Save changes' - wait_for_ajax + wait_for_requests expect(page).not_to have_selector(".shortcuts-#{shortcut_name}") select 'Everyone with access', from: "project_project_feature_attributes_#{tool_name}_access_level" click_button 'Save changes' - wait_for_ajax + wait_for_requests expect(page).to have_selector(".shortcuts-#{shortcut_name}") select 'Only team members', from: "project_project_feature_attributes_#{tool_name}_access_level" click_button 'Save changes' - wait_for_ajax + wait_for_requests expect(page).to have_selector(".shortcuts-#{shortcut_name}") sleep 0.1 @@ -169,7 +169,7 @@ describe 'Edit Project Settings', feature: true do select "Disabled", from: "project_project_feature_attributes_wiki_access_level" click_button "Save changes" - wait_for_ajax + wait_for_requests visit namespace_project_path(project.namespace, project) @@ -182,7 +182,7 @@ describe 'Edit Project Settings', feature: true do select "Disabled", from: "project_project_feature_attributes_wiki_access_level" click_button "Save changes" - wait_for_ajax + wait_for_requests visit activity_namespace_project_path(project.namespace, project) @@ -223,7 +223,7 @@ describe 'Edit Project Settings', feature: true do def save_changes_and_check_activity_tab click_button "Save changes" - wait_for_ajax + wait_for_requests visit activity_namespace_project_path(project.namespace, project) diff --git a/spec/features/projects/files/browse_files_spec.rb b/spec/features/projects/files/browse_files_spec.rb index 70e96efd557..c0a9327249c 100644 --- a/spec/features/projects/files/browse_files_spec.rb +++ b/spec/features/projects/files/browse_files_spec.rb @@ -24,11 +24,23 @@ feature 'user browses project', feature: true, js: true do click_link 'files' click_link 'lfs' click_link 'lfs_object.iso' - wait_for_ajax + wait_for_requests expect(page).not_to have_content 'Download (1.5 MB)' expect(page).to have_content 'version https://git-lfs.github.com/spec/v1' expect(page).to have_content 'oid sha256:91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897' expect(page).to have_content 'size 1575078' end + + scenario 'can see last commit for current directory' do + last_commit = project.repository.last_commit_for_path(project.default_branch, 'files') + + click_link 'files' + wait_for_requests + + page.within('.blob-commit-info') do + expect(page).to have_content last_commit.short_id + expect(page).to have_content last_commit.author_name + end + end end diff --git a/spec/features/projects/files/dockerfile_dropdown_spec.rb b/spec/features/projects/files/dockerfile_dropdown_spec.rb index 548131c7cd4..93909e91d05 100644 --- a/spec/features/projects/files/dockerfile_dropdown_spec.rb +++ b/spec/features/projects/files/dockerfile_dropdown_spec.rb @@ -19,14 +19,14 @@ feature 'User wants to add a Dockerfile file', feature: true do scenario 'user can pick a Dockerfile file from the dropdown', js: true do find('.js-dockerfile-selector').click - wait_for_ajax + wait_for_requests within '.dockerfile-selector' do find('.dropdown-input-field').set('HTTPd') find('.dropdown-content li', text: 'HTTPd').click end - wait_for_ajax + wait_for_requests expect(page).to have_css('.dockerfile-selector .dropdown-toggle-text', text: 'HTTPd') expect(page).to have_content('COPY ./ /usr/local/apache2/htdocs/') diff --git a/spec/features/projects/files/find_file_keyboard_spec.rb b/spec/features/projects/files/find_file_keyboard_spec.rb index e7a6749d8ac..ee42bcaec4b 100644 --- a/spec/features/projects/files/find_file_keyboard_spec.rb +++ b/spec/features/projects/files/find_file_keyboard_spec.rb @@ -10,7 +10,7 @@ feature 'Find file keyboard shortcuts', feature: true, js: true do visit namespace_project_find_file_path(project.namespace, project, project.repository.root_ref) - wait_for_ajax + wait_for_requests end it 'opens file when pressing enter key' do diff --git a/spec/features/projects/files/gitignore_dropdown_spec.rb b/spec/features/projects/files/gitignore_dropdown_spec.rb index e59428f8b24..e9f49453121 100644 --- a/spec/features/projects/files/gitignore_dropdown_spec.rb +++ b/spec/features/projects/files/gitignore_dropdown_spec.rb @@ -15,12 +15,12 @@ feature 'User wants to add a .gitignore file', feature: true do scenario 'user can pick a .gitignore file from the dropdown', js: true do find('.js-gitignore-selector').click - wait_for_ajax + wait_for_requests within '.gitignore-selector' do find('.dropdown-input-field').set('rails') find('.dropdown-content li', text: 'Rails').click end - wait_for_ajax + wait_for_requests expect(page).to have_css('.gitignore-selector .dropdown-toggle-text', text: 'Rails') expect(page).to have_content('/.bundle') diff --git a/spec/features/projects/files/gitlab_ci_yml_dropdown_spec.rb b/spec/features/projects/files/gitlab_ci_yml_dropdown_spec.rb index 85b66b93fba..031b89d0499 100644 --- a/spec/features/projects/files/gitlab_ci_yml_dropdown_spec.rb +++ b/spec/features/projects/files/gitlab_ci_yml_dropdown_spec.rb @@ -15,12 +15,12 @@ feature 'User wants to add a .gitlab-ci.yml file', feature: true do scenario 'user can pick a template from the dropdown', js: true do find('.js-gitlab-ci-yml-selector').click - wait_for_ajax + wait_for_requests within '.gitlab-ci-yml-selector' do find('.dropdown-input-field').set('Jekyll') find('.dropdown-content li', text: 'Jekyll').click end - wait_for_ajax + wait_for_requests expect(page).to have_css('.gitlab-ci-yml-selector .dropdown-toggle-text', text: 'Jekyll') expect(page).to have_content('This file is a template, and might need editing before it works on your project') diff --git a/spec/features/projects/files/project_owner_creates_license_file_spec.rb b/spec/features/projects/files/project_owner_creates_license_file_spec.rb index 249830921ac..8d410cc3f2e 100644 --- a/spec/features/projects/files/project_owner_creates_license_file_spec.rb +++ b/spec/features/projects/files/project_owner_creates_license_file_spec.rb @@ -63,7 +63,7 @@ feature 'project owner creates a license file', feature: true, js: true do page.within('.js-license-selector-wrap') do click_button 'Apply a license template' click_link template - wait_for_ajax + wait_for_requests end end end diff --git a/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb b/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb index 70a41886985..8e197bccabf 100644 --- a/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb +++ b/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb @@ -40,7 +40,7 @@ feature 'project owner sees a link to create a license file in empty project', f page.within('.js-license-selector-wrap') do click_button 'Apply a license template' click_link template - wait_for_ajax + wait_for_requests end end end diff --git a/spec/features/projects/files/undo_template_spec.rb b/spec/features/projects/files/undo_template_spec.rb index cd3af0b7d29..de10eec0557 100644 --- a/spec/features/projects/files/undo_template_spec.rb +++ b/spec/features/projects/files/undo_template_spec.rb @@ -57,7 +57,7 @@ end def select_file_template(template_selector_selector, template_name) find(template_selector_selector).click find('.dropdown-content li', text: template_name).click - wait_for_ajax + wait_for_requests end def select_file_template_type(template_type) diff --git a/spec/features/projects/gfm_autocomplete_load_spec.rb b/spec/features/projects/gfm_autocomplete_load_spec.rb index dd9622f16a0..67bc9142356 100644 --- a/spec/features/projects/gfm_autocomplete_load_spec.rb +++ b/spec/features/projects/gfm_autocomplete_load_spec.rb @@ -10,7 +10,7 @@ describe 'GFM autocomplete loading', feature: true, js: true do end it 'does not load on project#show' do - expect(evaluate_script('gl.GfmAutoComplete.dataSources')).to eq({}) + expect(evaluate_script('gl.GfmAutoComplete')).to eq(nil) end it 'loads on new issue page' do diff --git a/spec/features/projects/guest_navigation_menu_spec.rb b/spec/features/projects/guest_navigation_menu_spec.rb index 726469daba4..b91c3eff478 100644 --- a/spec/features/projects/guest_navigation_menu_spec.rb +++ b/spec/features/projects/guest_navigation_menu_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe "Guest navigation menu" do +describe 'Guest navigation menu' do let(:project) { create(:empty_project, :private, public_builds: false) } let(:guest) { create(:user) } @@ -10,10 +10,10 @@ describe "Guest navigation menu" do login_as(guest) end - it "shows allowed tabs only" do + it 'shows allowed tabs only' do visit namespace_project_path(project.namespace, project) - within(".nav-links") do + within('.layout-nav') do expect(page).to have_content 'Project' expect(page).to have_content 'Issues' expect(page).to have_content 'Wiki' @@ -23,4 +23,60 @@ describe "Guest navigation menu" do expect(page).not_to have_content 'Merge Requests' end end + + it 'does not show fork button' do + visit namespace_project_path(project.namespace, project) + + within('.count-buttons') do + expect(page).not_to have_link 'Fork' + end + end + + it 'does not show clone path' do + visit namespace_project_path(project.namespace, project) + + within('.project-repo-buttons') do + expect(page).not_to have_selector '.project-clone-holder' + end + end + + describe 'project landing page' do + before do + project.project_feature.update!( + issues_access_level: ProjectFeature::DISABLED, + wiki_access_level: ProjectFeature::DISABLED + ) + end + + it 'does not show the project file list landing page' do + visit namespace_project_path(project.namespace, project) + + expect(page).not_to have_selector '.project-stats' + expect(page).not_to have_selector '.project-last-commit' + expect(page).not_to have_selector '.project-show-files' + expect(page).to have_selector '.project-show-customize_workflow' + end + + it 'shows the customize workflow when issues and wiki are disabled' do + visit namespace_project_path(project.namespace, project) + + expect(page).to have_selector '.project-show-customize_workflow' + end + + it 'shows the wiki when enabled' do + project.project_feature.update!(wiki_access_level: ProjectFeature::PRIVATE) + + visit namespace_project_path(project.namespace, project) + + expect(page).to have_selector '.project-show-wiki' + end + + it 'shows the issues when enabled' do + project.project_feature.update!(issues_access_level: ProjectFeature::PRIVATE) + + visit namespace_project_path(project.namespace, project) + + expect(page).to have_selector '.issues-list' + end + end end diff --git a/spec/features/projects/issuable_templates_spec.rb b/spec/features/projects/issuable_templates_spec.rb index fa5e30075e3..3076c863dcb 100644 --- a/spec/features/projects/issuable_templates_spec.rb +++ b/spec/features/projects/issuable_templates_spec.rb @@ -34,14 +34,14 @@ feature 'issuable templates', feature: true, js: true do scenario 'user selects "bug" template' do select_template 'bug' - wait_for_ajax + wait_for_requests assert_template save_changes end scenario 'user selects "bug" template and then "no template"' do select_template 'bug' - wait_for_ajax + wait_for_requests select_option 'No template' assert_template('') save_changes('') @@ -49,7 +49,7 @@ feature 'issuable templates', feature: true, js: true do scenario 'user selects "bug" template, edits description and then selects "reset template"' do select_template 'bug' - wait_for_ajax + wait_for_requests find_field('issue_description').send_keys(description_addition) assert_template(template_content + description_addition) select_option 'Reset template' @@ -61,7 +61,7 @@ feature 'issuable templates', feature: true, js: true do start_height = page.evaluate_script('$(".markdown-area").outerHeight()') select_template 'test' - wait_for_ajax + wait_for_requests end_height = page.evaluate_script('$(".markdown-area").outerHeight()') @@ -88,7 +88,7 @@ feature 'issuable templates', feature: true, js: true do scenario 'user selects "bug" template' do select_template 'bug' - wait_for_ajax + wait_for_requests assert_template("#{template_content}") save_changes end @@ -111,7 +111,7 @@ feature 'issuable templates', feature: true, js: true do scenario 'user selects "feature-proposal" template' do select_template 'feature-proposal' - wait_for_ajax + wait_for_requests assert_template save_changes end @@ -143,7 +143,7 @@ feature 'issuable templates', feature: true, js: true do context 'template exists in target project' do scenario 'user selects template' do select_template 'feature-proposal' - wait_for_ajax + wait_for_requests assert_template save_changes end diff --git a/spec/features/projects/issues/rss_spec.rb b/spec/features/projects/issues/rss_spec.rb index 71429f00095..f6852192aef 100644 --- a/spec/features/projects/issues/rss_spec.rb +++ b/spec/features/projects/issues/rss_spec.rb @@ -16,8 +16,8 @@ feature 'Project Issues RSS' do visit path end - it_behaves_like "it has an RSS button with current_user's private token" - it_behaves_like "an autodiscoverable RSS feed with current_user's private token" + it_behaves_like "it has an RSS button with current_user's RSS token" + it_behaves_like "an autodiscoverable RSS feed with current_user's RSS token" end context 'when signed out' do @@ -25,7 +25,7 @@ feature 'Project Issues RSS' do visit path end - it_behaves_like "it has an RSS button without a private token" - it_behaves_like "an autodiscoverable RSS feed without a private token" + it_behaves_like "it has an RSS button without an RSS token" + it_behaves_like "an autodiscoverable RSS feed without an RSS token" end end diff --git a/spec/features/projects/labels/update_prioritization_spec.rb b/spec/features/projects/labels/update_prioritization_spec.rb index 836f81fb16d..34fafe072a3 100644 --- a/spec/features/projects/labels/update_prioritization_spec.rb +++ b/spec/features/projects/labels/update_prioritization_spec.rb @@ -24,7 +24,7 @@ feature 'Prioritize labels', feature: true do page.within('.other-labels') do all('.js-toggle-priority')[1].click - wait_for_ajax + wait_for_requests expect(page).not_to have_content('feature') end @@ -43,7 +43,7 @@ feature 'Prioritize labels', feature: true do expect(page).to have_content('feature') first('.js-toggle-priority').click - wait_for_ajax + wait_for_requests expect(page).not_to have_content('bug') end @@ -59,7 +59,7 @@ feature 'Prioritize labels', feature: true do page.within('.other-labels') do first('.js-toggle-priority').click - wait_for_ajax + wait_for_requests expect(page).not_to have_content('bug') end @@ -78,7 +78,7 @@ feature 'Prioritize labels', feature: true do expect(page).to have_content('bug') first('.js-toggle-priority').click - wait_for_ajax + wait_for_requests expect(page).not_to have_content('bug') end @@ -107,7 +107,7 @@ feature 'Prioritize labels', feature: true do end refresh - wait_for_ajax + wait_for_requests page.within('.prioritized-labels') do expect(first('li')).to have_content('feature') diff --git a/spec/features/projects/main/rss_spec.rb b/spec/features/projects/main/rss_spec.rb index b1a3af612a1..53966229a2a 100644 --- a/spec/features/projects/main/rss_spec.rb +++ b/spec/features/projects/main/rss_spec.rb @@ -12,7 +12,7 @@ feature 'Project RSS' do visit path end - it_behaves_like "an autodiscoverable RSS feed with current_user's private token" + it_behaves_like "an autodiscoverable RSS feed with current_user's RSS token" end context 'when signed out' do @@ -20,6 +20,6 @@ feature 'Project RSS' do visit path end - it_behaves_like "an autodiscoverable RSS feed without a private token" + it_behaves_like "an autodiscoverable RSS feed without an RSS token" end end diff --git a/spec/features/projects/members/group_links_spec.rb b/spec/features/projects/members/group_links_spec.rb index ab2b089db2e..3d253f01484 100644 --- a/spec/features/projects/members/group_links_spec.rb +++ b/spec/features/projects/members/group_links_spec.rb @@ -20,7 +20,7 @@ feature 'Projects > Members > Anonymous user sees members', feature: true, js: t click_link 'Guest' end - wait_for_ajax + wait_for_requests visit namespace_project_settings_members_path(project.namespace, project) @@ -31,7 +31,7 @@ feature 'Projects > Members > Anonymous user sees members', feature: true, js: t tomorrow = Date.today + 3 fill_in "member_expires_at_#{group.id}", with: tomorrow.strftime("%F") - wait_for_ajax + wait_for_requests page.within(find('li.group_member')) do expect(page).to have_content('Expires in') @@ -42,7 +42,7 @@ feature 'Projects > Members > Anonymous user sees members', feature: true, js: t page.within(first('.group_member')) do find('.btn-remove').click end - wait_for_ajax + wait_for_requests expect(page).not_to have_selector('.group_member') end diff --git a/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb b/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb index 19d14ad9af4..1e6f15d8258 100644 --- a/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb +++ b/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb @@ -38,7 +38,7 @@ feature 'Projects > Members > Master adds member with expiration date', feature: page.within "#project_member_#{new_member.project_members.first.id}" do find('.js-access-expiration-date').set date.to_s(:medium) - wait_for_ajax + wait_for_requests expect(page).to have_content('Expires in 3 days') end end diff --git a/spec/features/projects/pipeline_schedules_spec.rb b/spec/features/projects/pipeline_schedules_spec.rb index fe9f94db574..f40e1bc4930 100644 --- a/spec/features/projects/pipeline_schedules_spec.rb +++ b/spec/features/projects/pipeline_schedules_spec.rb @@ -2,10 +2,9 @@ require 'spec_helper' feature 'Pipeline Schedules', :feature do include PipelineSchedulesHelper - include WaitForAjax let!(:project) { create(:project) } - let!(:pipeline_schedule) { create(:ci_pipeline_schedule, project: project) } + let!(:pipeline_schedule) { create(:ci_pipeline_schedule, :nightly, project: project ) } let!(:pipeline) { create(:ci_pipeline, pipeline_schedule: pipeline_schedule) } let(:scope) { nil } let!(:user) { create(:user) } @@ -32,13 +31,14 @@ feature 'Pipeline Schedules', :feature do it 'displays the required information description' do page.within('.pipeline-schedule-table-row') do expect(page).to have_content('pipeline schedule') + expect(page).to have_content(pipeline_schedule.real_next_run.strftime('%b %d, %Y')) expect(page).to have_link('master') expect(page).to have_link("##{pipeline.id}") end end it 'creates a new scheduled pipeline' do - click_link 'New Schedule' + click_link 'New schedule' expect(page).to have_content('Schedule a new pipeline') end @@ -70,6 +70,11 @@ feature 'Pipeline Schedules', :feature do describe 'POST /projects/pipeline_schedules/new', js: true do let(:visit_page) { visit_new_pipeline_schedule } + it 'sets defaults for timezone and target branch' do + expect(page).to have_button('master') + expect(page).to have_button('UTC') + end + it 'it creates a new scheduled pipeline' do fill_in_schedule_form save_pipeline_schedule @@ -118,12 +123,12 @@ feature 'Pipeline Schedules', :feature do end def select_timezone - click_button 'Select a timezone' + find('.js-timezone-dropdown').click click_link 'American Samoa' end def select_target_branch - click_button 'Select target branch' + find('.js-target-branch-dropdown').click click_link 'master' end diff --git a/spec/features/projects/pipelines/pipelines_spec.rb b/spec/features/projects/pipelines/pipelines_spec.rb index 5f82cf2f5e5..a97a92aa64f 100644 --- a/spec/features/projects/pipelines/pipelines_spec.rb +++ b/spec/features/projects/pipelines/pipelines_spec.rb @@ -1,8 +1,6 @@ require 'spec_helper' describe 'Pipelines', :feature, :js do - include WaitForVueResource - let(:project) { create(:empty_project) } context 'when user is logged in' do @@ -54,7 +52,7 @@ describe 'Pipelines', :feature, :js do context 'header tabs' do before do visit namespace_project_pipelines_path(project.namespace, project) - wait_for_vue_resource + wait_for_requests end it 'shows a tab for All pipelines and count' do @@ -106,7 +104,7 @@ describe 'Pipelines', :feature, :js do context 'when canceling' do before do find('.js-pipelines-cancel-button').click - wait_for_vue_resource + wait_for_requests end it 'indicated that pipelines was canceled' do @@ -136,7 +134,7 @@ describe 'Pipelines', :feature, :js do context 'when retrying' do before do find('.js-pipelines-retry-button').click - wait_for_vue_resource + wait_for_requests end it 'shows running pipeline that is not retryable' do @@ -356,14 +354,14 @@ describe 'Pipelines', :feature, :js do it 'should render pagination' do visit namespace_project_pipelines_path(project.namespace, project) - wait_for_vue_resource + wait_for_requests expect(page).to have_selector('.gl-pagination') end it 'should render second page of pipelines' do visit namespace_project_pipelines_path(project.namespace, project, page: '2') - wait_for_vue_resource + wait_for_requests expect(page).to have_selector('.gl-pagination .page', count: 2) end @@ -392,7 +390,7 @@ describe 'Pipelines', :feature, :js do create(:generic_commit_status, pipeline: pipeline, stage: 'external', name: 'jenkins', stage_idx: 3) visit namespace_project_pipeline_path(project.namespace, project, pipeline) - wait_for_vue_resource + wait_for_requests end it 'shows a graph with grouped stages' do @@ -507,6 +505,6 @@ describe 'Pipelines', :feature, :js do def visit_project_pipelines(**query) visit namespace_project_pipelines_path(project.namespace, project, query) - wait_for_vue_resource + wait_for_requests end end diff --git a/spec/features/projects/ref_switcher_spec.rb b/spec/features/projects/ref_switcher_spec.rb index 881ad7910dd..04414490571 100644 --- a/spec/features/projects/ref_switcher_spec.rb +++ b/spec/features/projects/ref_switcher_spec.rb @@ -12,12 +12,12 @@ feature 'Ref switcher', feature: true, js: true do it 'allow user to change ref by enter key' do click_button 'master' - wait_for_ajax + wait_for_requests page.within '.project-refs-form' do input = find('input[type="search"]') input.set 'binary' - wait_for_ajax + wait_for_requests expect(find('.dropdown-content ul')).to have_selector('li', count: 6) @@ -31,7 +31,7 @@ feature 'Ref switcher', feature: true, js: true do it "user selects ref with special characters" do click_button 'master' - wait_for_ajax + wait_for_requests page.within '.project-refs-form' do page.fill_in 'Search branches and tags', with: "'test'" diff --git a/spec/features/projects/settings/integration_settings_spec.rb b/spec/features/projects/settings/integration_settings_spec.rb index 7909234556e..d3232f0cc16 100644 --- a/spec/features/projects/settings/integration_settings_spec.rb +++ b/spec/features/projects/settings/integration_settings_spec.rb @@ -52,6 +52,7 @@ feature 'Integration settings', feature: true do fill_in 'hook_url', with: url check 'Tag push events' check 'Enable SSL verification' + check 'Job events' click_button 'Add webhook' @@ -59,6 +60,7 @@ feature 'Integration settings', feature: true do expect(page).to have_content('SSL Verification: enabled') expect(page).to have_content('Push Events') expect(page).to have_content('Tag Push Events') + expect(page).to have_content('Job events') end scenario 'edit existing webhook' do diff --git a/spec/features/projects/snippets/show_spec.rb b/spec/features/projects/snippets/show_spec.rb index cedf3778c7e..b844e60e5d5 100644 --- a/spec/features/projects/snippets/show_spec.rb +++ b/spec/features/projects/snippets/show_spec.rb @@ -17,7 +17,7 @@ feature 'Project snippet', :js, feature: true do before do visit namespace_project_snippet_path(project.namespace, project, snippet) - wait_for_ajax + wait_for_requests end it 'displays the blob' do @@ -48,7 +48,7 @@ feature 'Project snippet', :js, feature: true do before do visit namespace_project_snippet_path(project.namespace, project, snippet) - wait_for_ajax + wait_for_requests end it 'displays the blob using the rich viewer' do @@ -78,7 +78,7 @@ feature 'Project snippet', :js, feature: true do before do find('.js-blob-viewer-switch-btn[data-viewer=simple]').click - wait_for_ajax + wait_for_requests end it 'displays the blob using the simple viewer' do @@ -99,7 +99,7 @@ feature 'Project snippet', :js, feature: true do before do find('.js-blob-viewer-switch-btn[data-viewer=rich]').click - wait_for_ajax + wait_for_requests end it 'displays the blob using the rich viewer' do @@ -120,7 +120,7 @@ feature 'Project snippet', :js, feature: true do before do visit namespace_project_snippet_path(project.namespace, project, snippet, anchor: 'L1') - wait_for_ajax + wait_for_requests end it 'displays the blob using the simple viewer' do diff --git a/spec/features/projects/tree/rss_spec.rb b/spec/features/projects/tree/rss_spec.rb index 9ac51997d65..9bf59c4139c 100644 --- a/spec/features/projects/tree/rss_spec.rb +++ b/spec/features/projects/tree/rss_spec.rb @@ -12,7 +12,7 @@ feature 'Project Tree RSS' do visit path end - it_behaves_like "an autodiscoverable RSS feed with current_user's private token" + it_behaves_like "an autodiscoverable RSS feed with current_user's RSS token" end context 'when signed out' do @@ -20,6 +20,6 @@ feature 'Project Tree RSS' do visit path end - it_behaves_like "an autodiscoverable RSS feed without a private token" + it_behaves_like "an autodiscoverable RSS feed without an RSS token" end end diff --git a/spec/features/projects/view_on_env_spec.rb b/spec/features/projects/view_on_env_spec.rb index b7a41ca54e6..640f1376548 100644 --- a/spec/features/projects/view_on_env_spec.rb +++ b/spec/features/projects/view_on_env_spec.rb @@ -54,7 +54,7 @@ describe 'View on environment', js: true do visit diffs_namespace_project_merge_request_path(project.namespace, project, merge_request) - wait_for_ajax + wait_for_requests end it 'has a "View on env" button' do @@ -70,7 +70,7 @@ describe 'View on environment', js: true do visit namespace_project_compare_path(project.namespace, project, from: 'master', to: branch_name) - wait_for_ajax + wait_for_requests end it 'has a "View on env" button' do @@ -84,7 +84,7 @@ describe 'View on environment', js: true do visit namespace_project_compare_path(project.namespace, project, from: 'master', to: sha) - wait_for_ajax + wait_for_requests end it 'has a "View on env" button' do @@ -98,7 +98,7 @@ describe 'View on environment', js: true do visit namespace_project_blob_path(project.namespace, project, File.join(branch_name, file_path)) - wait_for_ajax + wait_for_requests end it 'has a "View on env" button' do @@ -112,7 +112,7 @@ describe 'View on environment', js: true do visit namespace_project_blob_path(project.namespace, project, File.join(sha, file_path)) - wait_for_ajax + wait_for_requests end it 'has a "View on env" button' do @@ -126,7 +126,7 @@ describe 'View on environment', js: true do visit namespace_project_commit_path(project.namespace, project, sha) - wait_for_ajax + wait_for_requests end it 'has a "View on env" button' do diff --git a/spec/features/projects/wiki/user_creates_wiki_page_spec.rb b/spec/features/projects/wiki/user_creates_wiki_page_spec.rb index 5c502ce4fb5..8912d575878 100644 --- a/spec/features/projects/wiki/user_creates_wiki_page_spec.rb +++ b/spec/features/projects/wiki/user_creates_wiki_page_spec.rb @@ -28,6 +28,40 @@ feature 'Projects > Wiki > User creates wiki page', js: true, feature: true do expect(page).to have_content("Last edited by #{user.name}") expect(page).to have_content('My awesome wiki!') end + + scenario 'creates ASCII wiki with LaTeX blocks' do + stub_application_setting(plantuml_url: 'http://localhost', plantuml_enabled: true) + + ascii_content = <<~MD + :stem: latexmath + + [stem] + ++++ + \sqrt{4} = 2 + ++++ + + another part + + [latexmath] + ++++ + \beta_x \gamma + ++++ + + stem:[2+2] is 4 + MD + + find('#wiki_format option[value=asciidoc]').select_option + fill_in :wiki_content, with: ascii_content + + page.within '.wiki-form' do + click_button 'Create page' + end + + page.within '.wiki' do + expect(page).to have_selector('.katex', count: 3) + expect(page).to have_content('2+2 is 4') + end + end end context 'when wiki is not empty' do diff --git a/spec/features/protected_branches_spec.rb b/spec/features/protected_branches_spec.rb index fc9b293c393..884d1bbb10c 100644 --- a/spec/features/protected_branches_spec.rb +++ b/spec/features/protected_branches_spec.rb @@ -1,5 +1,4 @@ require 'spec_helper' -Dir["./spec/features/protected_branches/*.rb"].sort.each { |f| require f } feature 'Projected Branches', feature: true, js: true do let(:user) { create(:user, :admin) } diff --git a/spec/features/protected_tags_spec.rb b/spec/features/protected_tags_spec.rb index e68448467b0..66236dbc7fc 100644 --- a/spec/features/protected_tags_spec.rb +++ b/spec/features/protected_tags_spec.rb @@ -1,5 +1,4 @@ require 'spec_helper' -Dir["./spec/features/protected_tags/*.rb"].sort.each { |f| require f } feature 'Projected Tags', feature: true, js: true do let(:user) { create(:user, :admin) } diff --git a/spec/features/search_spec.rb b/spec/features/search_spec.rb index 2fda7758407..7834807b1f1 100644 --- a/spec/features/search_spec.rb +++ b/spec/features/search_spec.rb @@ -28,7 +28,7 @@ describe "Search", feature: true do it 'shows group name after filtering' do find('.js-search-group-dropdown').trigger('click') - wait_for_ajax + wait_for_requests page.within '.search-holder' do click_link group.name @@ -39,7 +39,7 @@ describe "Search", feature: true do it 'filters by group projects after filtering by group' do find('.js-search-group-dropdown').trigger('click') - wait_for_ajax + wait_for_requests page.within '.search-holder' do click_link group.name @@ -49,7 +49,7 @@ describe "Search", feature: true do page.within('.project-filter') do find('.js-search-project-dropdown').trigger('click') - wait_for_ajax + wait_for_requests expect(page).to have_link(group_project.name_with_namespace) end @@ -58,7 +58,7 @@ describe "Search", feature: true do it 'shows project name after filtering' do page.within('.project-filter') do find('.js-search-project-dropdown').trigger('click') - wait_for_ajax + wait_for_requests click_link project.name_with_namespace end diff --git a/spec/features/snippets/create_snippet_spec.rb b/spec/features/snippets/create_snippet_spec.rb index 9409c323288..31a2d4ae984 100644 --- a/spec/features/snippets/create_snippet_spec.rb +++ b/spec/features/snippets/create_snippet_spec.rb @@ -13,7 +13,7 @@ feature 'Create Snippet', :js, feature: true do end click_button 'Create snippet' - wait_for_ajax + wait_for_requests expect(page).to have_content('My Snippet Title') expect(page).to have_content('Hello World!') @@ -27,7 +27,7 @@ feature 'Create Snippet', :js, feature: true do end click_button 'Create snippet' - wait_for_ajax + wait_for_requests expect(page).to have_content('My Snippet Title') expect(page).to have_content('snippet+file+name') diff --git a/spec/features/snippets/notes_on_personal_snippets_spec.rb b/spec/features/snippets/notes_on_personal_snippets_spec.rb index 698eb46573f..f7afc174019 100644 --- a/spec/features/snippets/notes_on_personal_snippets_spec.rb +++ b/spec/features/snippets/notes_on_personal_snippets_spec.rb @@ -93,7 +93,7 @@ describe 'Comments on personal snippets', :js, feature: true do click_on 'Remove comment' end - wait_for_ajax + wait_for_requests expect(page).not_to have_selector("#notes-list li#note_#{snippet_notes[0].id}") end diff --git a/spec/features/snippets/public_snippets_spec.rb b/spec/features/snippets/public_snippets_spec.rb index 2df483818c3..afd945a8555 100644 --- a/spec/features/snippets/public_snippets_spec.rb +++ b/spec/features/snippets/public_snippets_spec.rb @@ -5,7 +5,7 @@ feature 'Public Snippets', :js, feature: true do public_snippet = create(:personal_snippet, :public) visit snippet_path(public_snippet) - wait_for_ajax + wait_for_requests expect(page).to have_content(public_snippet.content) end diff --git a/spec/features/snippets/show_spec.rb b/spec/features/snippets/show_spec.rb index e36cf547f80..95fc1d2bb62 100644 --- a/spec/features/snippets/show_spec.rb +++ b/spec/features/snippets/show_spec.rb @@ -11,7 +11,7 @@ feature 'Snippet', :js, feature: true do before do visit snippet_path(snippet) - wait_for_ajax + wait_for_requests end it 'displays the blob' do @@ -42,7 +42,7 @@ feature 'Snippet', :js, feature: true do before do visit snippet_path(snippet) - wait_for_ajax + wait_for_requests end it 'displays the blob using the rich viewer' do @@ -72,7 +72,7 @@ feature 'Snippet', :js, feature: true do before do find('.js-blob-viewer-switch-btn[data-viewer=simple]').click - wait_for_ajax + wait_for_requests end it 'displays the blob using the simple viewer' do @@ -93,7 +93,7 @@ feature 'Snippet', :js, feature: true do before do find('.js-blob-viewer-switch-btn[data-viewer=rich]').click - wait_for_ajax + wait_for_requests end it 'displays the blob using the rich viewer' do @@ -114,7 +114,7 @@ feature 'Snippet', :js, feature: true do before do visit snippet_path(snippet, anchor: 'L1') - wait_for_ajax + wait_for_requests end it 'displays the blob using the simple viewer' do diff --git a/spec/features/tags/master_creates_tag_spec.rb b/spec/features/tags/master_creates_tag_spec.rb index ca25c696f75..af25eebed13 100644 --- a/spec/features/tags/master_creates_tag_spec.rb +++ b/spec/features/tags/master_creates_tag_spec.rb @@ -51,10 +51,24 @@ feature 'Master creates tag', feature: true do end end + scenario 'opens dropdown for ref', js: true do + click_link 'New tag' + ref_row = find('.form-group:nth-of-type(2) .col-sm-10') + page.within ref_row do + ref_input = find('[name="ref"]', visible: false) + expect(ref_input.value).to eq 'master' + expect(find('.dropdown-toggle-text')).to have_content 'master' + + find('.js-branch-select').trigger('click') + + expect(find('.dropdown-menu')).to have_content 'empty-branch' + end + end + def create_tag_in_form(tag:, ref:, message: nil, desc: nil) click_link 'New tag' fill_in 'tag_name', with: tag - fill_in 'ref', with: ref + find('#ref', visible: false).set(ref) fill_in 'message', with: message unless message.nil? fill_in 'release_description', with: desc unless desc.nil? click_button 'Create tag' diff --git a/spec/features/task_lists_spec.rb b/spec/features/task_lists_spec.rb index 8bd13caf2b0..563e65d3cc5 100644 --- a/spec/features/task_lists_spec.rb +++ b/spec/features/task_lists_spec.rb @@ -64,13 +64,11 @@ feature 'Task Lists', feature: true do describe 'for Issues', feature: true do describe 'multiple tasks', js: true do - include WaitForVueResource - let!(:issue) { create(:issue, description: markdown, author: user, project: project) } it 'renders' do visit_issue(project, issue) - wait_for_vue_resource + wait_for_requests expect(page).to have_selector('ul.task-list', count: 1) expect(page).to have_selector('li.task-list-item', count: 6) @@ -79,7 +77,7 @@ feature 'Task Lists', feature: true do it 'contains the required selectors' do visit_issue(project, issue) - wait_for_vue_resource + wait_for_requests expect(page).to have_selector(".wiki .task-list .task-list-item .task-list-item-checkbox") expect(page).to have_selector('a.btn-close') @@ -87,14 +85,14 @@ feature 'Task Lists', feature: true do it 'is only editable by author' do visit_issue(project, issue) - wait_for_vue_resource + wait_for_requests expect(page).to have_selector(".wiki .task-list .task-list-item .task-list-item-checkbox") logout(:user) login_as(user2) visit current_path - wait_for_vue_resource + wait_for_requests expect(page).to have_selector(".wiki .task-list .task-list-item .task-list-item-checkbox") end @@ -106,13 +104,11 @@ feature 'Task Lists', feature: true do end describe 'single incomplete task', js: true do - include WaitForVueResource - let!(:issue) { create(:issue, description: singleIncompleteMarkdown, author: user, project: project) } it 'renders' do visit_issue(project, issue) - wait_for_vue_resource + wait_for_requests expect(page).to have_selector('ul.task-list', count: 1) expect(page).to have_selector('li.task-list-item', count: 1) @@ -127,12 +123,11 @@ feature 'Task Lists', feature: true do end describe 'single complete task', js: true do - include WaitForVueResource let!(:issue) { create(:issue, description: singleCompleteMarkdown, author: user, project: project) } it 'renders' do visit_issue(project, issue) - wait_for_vue_resource + wait_for_requests expect(page).to have_selector('ul.task-list', count: 1) expect(page).to have_selector('li.task-list-item', count: 1) diff --git a/spec/features/todos/todos_filtering_spec.rb b/spec/features/todos/todos_filtering_spec.rb index f32e70c2c3f..bbfa4e08379 100644 --- a/spec/features/todos/todos_filtering_spec.rb +++ b/spec/features/todos/todos_filtering_spec.rb @@ -28,7 +28,7 @@ describe 'Dashboard > User filters todos', feature: true, js: true do click_link project_1.name_with_namespace end - wait_for_ajax + wait_for_requests expect(page).to have_content project_1.name_with_namespace expect(page).not_to have_content project_2.name_with_namespace @@ -43,7 +43,7 @@ describe 'Dashboard > User filters todos', feature: true, js: true do click_link user_1.name end - wait_for_ajax + wait_for_requests expect(find('.todos-list')).to have_content 'merge request' expect(find('.todos-list')).not_to have_content 'issue' @@ -90,7 +90,7 @@ describe 'Dashboard > User filters todos', feature: true, js: true do click_link 'Issue' end - wait_for_ajax + wait_for_requests expect(find('.todos-list')).to have_content issue.to_reference expect(find('.todos-list')).not_to have_content merge_request.to_reference @@ -132,7 +132,7 @@ describe 'Dashboard > User filters todos', feature: true, js: true do click_link name end - wait_for_ajax + wait_for_requests end def expect_to_see_action(action_name) diff --git a/spec/features/todos/todos_spec.rb b/spec/features/todos/todos_spec.rb index 55b3e3d9424..bb4b2aed0e3 100644 --- a/spec/features/todos/todos_spec.rb +++ b/spec/features/todos/todos_spec.rb @@ -64,7 +64,7 @@ describe 'Dashboard Todos', feature: true do before do within first('.todo') do click_link 'Done' - wait_for_ajax + wait_for_requests click_link 'Undo' end end @@ -309,9 +309,9 @@ describe 'Dashboard Todos', feature: true do def mark_all_and_undo find('.js-todos-mark-all').trigger('click') - wait_for_ajax + wait_for_requests find('.js-todos-undo-all').trigger('click') - wait_for_ajax + wait_for_requests end end end diff --git a/spec/features/u2f_spec.rb b/spec/features/u2f_spec.rb index 544d2dcb87f..2fed8067042 100644 --- a/spec/features/u2f_spec.rb +++ b/spec/features/u2f_spec.rb @@ -6,7 +6,7 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', :js do def manage_two_factor_authentication click_on 'Manage two-factor authentication' expect(page).to have_content("Setup new U2F device") - wait_for_ajax + wait_for_requests end def register_u2f_device(u2f_device = nil, name: 'My device') diff --git a/spec/features/uploads/user_uploads_file_to_note_spec.rb b/spec/features/uploads/user_uploads_file_to_note_spec.rb index 0c160dd74b4..9332d3b88d2 100644 --- a/spec/features/uploads/user_uploads_file_to_note_spec.rb +++ b/spec/features/uploads/user_uploads_file_to_note_spec.rb @@ -5,18 +5,78 @@ feature 'User uploads file to note', feature: true do let(:user) { create(:user) } let(:project) { create(:empty_project, creator: user, namespace: user.namespace) } + let(:issue) { create(:issue, project: project, author: user) } - scenario 'they see the attached file', js: true do - issue = create(:issue, project: project, author: user) - + before do login_as(user) visit namespace_project_issue_path(project.namespace, project, issue) + end + + context 'before uploading' do + it 'shows "Attach a file" button', js: true do + expect(page).to have_button('Attach a file') + expect(page).not_to have_selector('.uploading-progress-container', visible: true) + end + end + + context 'uploading is in progress' do + it 'shows "Cancel" button on uploading', js: true do + dropzone_file([Rails.root.join('spec', 'fixtures', 'dk.png')], 0, false) + + expect(page).to have_button('Cancel') + end + + it 'cancels uploading on clicking to "Cancel" button', js: true do + dropzone_file([Rails.root.join('spec', 'fixtures', 'dk.png')], 0, false) + + click_button 'Cancel' + + expect(page).to have_button('Attach a file') + expect(page).not_to have_button('Cancel') + expect(page).not_to have_selector('.uploading-progress-container', visible: true) + end + + it 'shows "Attaching a file" message on uploading 1 file', js: true do + dropzone_file([Rails.root.join('spec', 'fixtures', 'dk.png')], 0, false) + + expect(page).to have_selector('.attaching-file-message', visible: true, text: 'Attaching a file -') + end + + it 'shows "Attaching 2 files" message on uploading 2 file', js: true do + dropzone_file([Rails.root.join('spec', 'fixtures', 'video_sample.mp4'), + Rails.root.join('spec', 'fixtures', 'dk.png')], 0, false) + + expect(page).to have_selector('.attaching-file-message', visible: true, text: 'Attaching 2 files -') + end + + it 'shows error message, "retry" and "attach a new file" link a if file is too big', js: true do + dropzone_file([Rails.root.join('spec', 'fixtures', 'video_sample.mp4')], 0.01) + + error_text = 'File is too big (0.06MiB). Max filesize: 0.01MiB.' + + expect(page).to have_selector('.uploading-error-message', visible: true, text: error_text) + expect(page).to have_selector('.retry-uploading-link', visible: true, text: 'Try again') + expect(page).to have_selector('.attach-new-file', visible: true, text: 'attach a new file') + expect(page).not_to have_button('Attach a file') + end + end + + context 'uploading is complete' do + it 'shows "Attach a file" button on uploading complete', js: true do + dropzone_file([Rails.root.join('spec', 'fixtures', 'dk.png')]) + wait_for_requests + + expect(page).to have_button('Attach a file') + expect(page).not_to have_selector('.uploading-progress-container', visible: true) + end - dropzone_file(Rails.root.join('spec', 'fixtures', 'dk.png')) - click_button 'Comment' - wait_for_ajax + scenario 'they see the attached file', js: true do + dropzone_file([Rails.root.join('spec', 'fixtures', 'dk.png')]) + click_button 'Comment' + wait_for_requests - expect(find('a.no-attachment-icon img[alt="dk"]')['src']) - .to match(%r{/#{project.full_path}/uploads/\h{32}/dk\.png$}) + expect(find('a.no-attachment-icon img[alt="dk"]')['src']) + .to match(%r{/#{project.full_path}/uploads/\h{32}/dk\.png$}) + end end end diff --git a/spec/features/users/projects_spec.rb b/spec/features/users/projects_spec.rb index 373b64808f8..67ce4b44464 100644 --- a/spec/features/users/projects_spec.rb +++ b/spec/features/users/projects_spec.rb @@ -16,7 +16,7 @@ describe 'Projects tab on a user profile', :feature, :js do click_link('Personal projects') end - wait_for_ajax + wait_for_requests end it 'paginates results' do diff --git a/spec/features/users/rss_spec.rb b/spec/features/users/rss_spec.rb index 14564abb16d..dbd5f66b55e 100644 --- a/spec/features/users/rss_spec.rb +++ b/spec/features/users/rss_spec.rb @@ -9,7 +9,7 @@ feature 'User RSS' do visit path end - it_behaves_like "it has an RSS button with current_user's private token" + it_behaves_like "it has an RSS button with current_user's RSS token" end context 'when signed out' do @@ -17,6 +17,6 @@ feature 'User RSS' do visit path end - it_behaves_like "it has an RSS button without a private token" + it_behaves_like "it has an RSS button without an RSS token" end end diff --git a/spec/features/users/snippets_spec.rb b/spec/features/users/snippets_spec.rb index 4efbd672322..2e388115633 100644 --- a/spec/features/users/snippets_spec.rb +++ b/spec/features/users/snippets_spec.rb @@ -11,7 +11,7 @@ describe 'Snippets tab on a user profile', feature: true, js: true do allow(Snippet).to receive(:default_per_page).and_return(1) visit user_path(user) page.within('.user-profile-nav') { click_link 'Snippets' } - wait_for_ajax + wait_for_requests end it_behaves_like 'paginated snippets', remote: true @@ -27,7 +27,7 @@ describe 'Snippets tab on a user profile', feature: true, js: true do login_as(:user) visit user_path(user) page.within('.user-profile-nav') { click_link 'Snippets' } - wait_for_ajax + wait_for_requests expect(page).to have_selector('.snippet-row', count: 2) @@ -38,7 +38,7 @@ describe 'Snippets tab on a user profile', feature: true, js: true do it 'contains only public snippets of a user when a user is not logged in' do visit user_path(user) page.within('.user-profile-nav') { click_link 'Snippets' } - wait_for_ajax + wait_for_requests expect(page).to have_selector('.snippet-row', count: 1) expect(page).to have_content(public_snippet.title) diff --git a/spec/features/users_spec.rb b/spec/features/users_spec.rb index c43feadc808..fbe078bd136 100644 --- a/spec/features/users_spec.rb +++ b/spec/features/users_spec.rb @@ -78,25 +78,25 @@ feature 'Users', feature: true, js: true do scenario 'doesn\'t show an error border if the username is available' do fill_in username_input, with: 'new-user' - wait_for_ajax + wait_for_requests expect(find('.username')).not_to have_css '.gl-field-error-outline' end scenario 'does not show an error border if the username contains dots (.)' do fill_in username_input, with: 'new.user.username' - wait_for_ajax + wait_for_requests expect(find('.username')).not_to have_css '.gl-field-error-outline' end scenario 'shows an error border if the username already exists' do fill_in username_input, with: user.username - wait_for_ajax + wait_for_requests expect(find('.username')).to have_css '.gl-field-error-outline' end scenario 'shows an error border if the username contains special characters' do fill_in username_input, with: 'new$user!username' - wait_for_ajax + wait_for_requests expect(find('.username')).to have_css '.gl-field-error-outline' end end diff --git a/spec/fixtures/api/schemas/entities/merge_request.json b/spec/fixtures/api/schemas/entities/merge_request.json index 0a7e0e2d5f2..4afbb87453e 100644 --- a/spec/fixtures/api/schemas/entities/merge_request.json +++ b/spec/fixtures/api/schemas/entities/merge_request.json @@ -3,7 +3,6 @@ "properties" : { "id": { "type": "integer" }, "iid": { "type": "integer" }, - "assignee_id": { "type": ["integer", "null"] }, "author_id": { "type": "integer" }, "description": { "type": ["string", "null"] }, "lock_version": { "type": ["string", "null"] }, @@ -86,6 +85,7 @@ "email_patches_path": { "type": "string" }, "plain_diff_path": { "type": "string" }, "status_path": { "type": "string" }, + "new_blob_path": { "type": "string" }, "merge_check_path": { "type": "string" }, "ci_environments_status_path": { "type": "string" }, "merge_commit_message_with_description": { "type": "string" }, diff --git a/spec/fixtures/api/schemas/entities/merge_request_basic.json b/spec/fixtures/api/schemas/entities/merge_request_basic.json index ea6364b878c..6b14188582a 100644 --- a/spec/fixtures/api/schemas/entities/merge_request_basic.json +++ b/spec/fixtures/api/schemas/entities/merge_request_basic.json @@ -8,7 +8,8 @@ "total_time_spent": { "type": "integer" }, "human_time_estimate": { "type": ["string", "null"] }, "human_total_time_spent": { "type": ["string", "null"] }, - "merge_error": { "type": ["string", "null"] } + "merge_error": { "type": ["string", "null"] }, + "assignee_id": { "type": ["integer", "null"] } }, "additionalProperties": false } diff --git a/spec/helpers/blob_helper_spec.rb b/spec/helpers/blob_helper_spec.rb index 1b4393e6167..41b5df12522 100644 --- a/spec/helpers/blob_helper_spec.rb +++ b/spec/helpers/blob_helper_spec.rb @@ -116,10 +116,11 @@ describe BlobHelper do let(:viewer_class) do Class.new(BlobViewer::Base) do - self.max_size = 1.megabyte - self.absolute_max_size = 5.megabytes + include BlobViewer::ServerSide + + self.overridable_max_size = 1.megabyte + self.max_size = 5.megabytes self.type = :rich - self.client_side = false end end diff --git a/spec/helpers/notes_helper_spec.rb b/spec/helpers/notes_helper_spec.rb index 099146678ae..355a4845afb 100644 --- a/spec/helpers/notes_helper_spec.rb +++ b/spec/helpers/notes_helper_spec.rb @@ -92,7 +92,13 @@ describe NotesHelper do ) end - let(:discussion) { create(:diff_note_on_merge_request, noteable: merge_request, project: project, position: position).to_discussion } + let(:diff_note) { create(:diff_note_on_merge_request, noteable: merge_request, project: project, position: position) } + let(:discussion) { diff_note.to_discussion } + + before do + diff_note.position = diff_note.original_position + diff_note.save! + end it 'returns the diff version comparison path with the line code' do expect(helper.discussion_path(discussion)).to eq(diffs_namespace_project_merge_request_path(project.namespace, project, merge_request, diff_id: merge_request_diff3, start_sha: merge_request_diff1.head_commit_sha, anchor: discussion.line_code)) diff --git a/spec/helpers/rss_helper_spec.rb b/spec/helpers/rss_helper_spec.rb index f3f174f3d14..269e1057e8d 100644 --- a/spec/helpers/rss_helper_spec.rb +++ b/spec/helpers/rss_helper_spec.rb @@ -3,17 +3,17 @@ require 'spec_helper' describe RssHelper do describe '#rss_url_options' do context 'when signed in' do - it "includes the current_user's private_token" do + it "includes the current_user's rss_token" do current_user = create(:user) allow(helper).to receive(:current_user).and_return(current_user) - expect(helper.rss_url_options).to include private_token: current_user.private_token + expect(helper.rss_url_options).to include rss_token: current_user.rss_token end end context 'when signed out' do - it "does not have a private_token" do + it "does not have an rss_token" do allow(helper).to receive(:current_user).and_return(nil) - expect(helper.rss_url_options[:private_token]).to be_nil + expect(helper.rss_url_options[:rss_token]).to be_nil end end end diff --git a/spec/javascripts/abuse_reports_spec.js b/spec/javascripts/abuse_reports_spec.js index 76b370b345b..069d857eab6 100644 --- a/spec/javascripts/abuse_reports_spec.js +++ b/spec/javascripts/abuse_reports_spec.js @@ -1,5 +1,5 @@ -require('~/lib/utils/text_utility'); -require('~/abuse_reports'); +import '~/lib/utils/text_utility'; +import '~/abuse_reports'; ((global) => { describe('Abuse Reports', () => { diff --git a/spec/javascripts/activities_spec.js b/spec/javascripts/activities_spec.js index e6a6fc36ca1..e8c5f721423 100644 --- a/spec/javascripts/activities_spec.js +++ b/spec/javascripts/activities_spec.js @@ -1,8 +1,8 @@ /* eslint-disable no-unused-expressions, no-prototype-builtins, no-new, no-shadow, max-len */ -require('vendor/jquery.endless-scroll.js'); -require('~/pager'); -require('~/activities'); +import 'vendor/jquery.endless-scroll'; +import '~/pager'; +import '~/activities'; (() => { window.gon || (window.gon = {}); diff --git a/spec/javascripts/ajax_loading_spinner_spec.js b/spec/javascripts/ajax_loading_spinner_spec.js index a68bccb16f4..1518ae68b0d 100644 --- a/spec/javascripts/ajax_loading_spinner_spec.js +++ b/spec/javascripts/ajax_loading_spinner_spec.js @@ -1,7 +1,7 @@ -require('~/extensions/array'); -require('jquery'); -require('jquery-ujs'); -require('~/ajax_loading_spinner'); +import '~/extensions/array'; +import 'jquery'; +import 'jquery-ujs'; +import '~/ajax_loading_spinner'; describe('Ajax Loading Spinner', () => { const fixtureTemplate = 'static/ajax_loading_spinner.html.raw'; diff --git a/spec/javascripts/api_spec.js b/spec/javascripts/api_spec.js new file mode 100644 index 00000000000..867322ce8ae --- /dev/null +++ b/spec/javascripts/api_spec.js @@ -0,0 +1,281 @@ +import Api from '~/api'; + +describe('Api', () => { + const dummyApiVersion = 'v3000'; + const dummyUrlRoot = 'http://host.invalid'; + const dummyGon = { + api_version: dummyApiVersion, + relative_url_root: dummyUrlRoot, + }; + const dummyResponse = 'hello from outer space!'; + const sendDummyResponse = () => { + const deferred = $.Deferred(); + deferred.resolve(dummyResponse); + return deferred.promise(); + }; + let originalGon; + + beforeEach(() => { + originalGon = window.gon; + window.gon = dummyGon; + }); + + afterEach(() => { + window.gon = originalGon; + }); + + describe('buildUrl', () => { + it('adds URL root and fills in API version', () => { + const input = '/api/:version/foo/bar'; + const expectedOutput = `${dummyUrlRoot}/api/${dummyApiVersion}/foo/bar`; + + const builtUrl = Api.buildUrl(input); + + expect(builtUrl).toEqual(expectedOutput); + }); + }); + + describe('group', () => { + it('fetches a group', (done) => { + const groupId = '123456'; + const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/groups/${groupId}.json`; + spyOn(jQuery, 'ajax').and.callFake((request) => { + expect(request.url).toEqual(expectedUrl); + expect(request.dataType).toEqual('json'); + return sendDummyResponse(); + }); + + Api.group(groupId, (response) => { + expect(response).toBe(dummyResponse); + done(); + }); + }); + }); + + describe('groups', () => { + it('fetches groups', (done) => { + const query = 'dummy query'; + const options = { unused: 'option' }; + const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/groups.json`; + const expectedData = Object.assign({ + search: query, + per_page: 20, + }, options); + spyOn(jQuery, 'ajax').and.callFake((request) => { + expect(request.url).toEqual(expectedUrl); + expect(request.dataType).toEqual('json'); + expect(request.data).toEqual(expectedData); + return sendDummyResponse(); + }); + + Api.groups(query, options, (response) => { + expect(response).toBe(dummyResponse); + done(); + }); + }); + }); + + describe('namespaces', () => { + it('fetches namespaces', (done) => { + const query = 'dummy query'; + const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/namespaces.json`; + const expectedData = { + search: query, + per_page: 20, + }; + spyOn(jQuery, 'ajax').and.callFake((request) => { + expect(request.url).toEqual(expectedUrl); + expect(request.dataType).toEqual('json'); + expect(request.data).toEqual(expectedData); + return sendDummyResponse(); + }); + + Api.namespaces(query, (response) => { + expect(response).toBe(dummyResponse); + done(); + }); + }); + }); + + describe('projects', () => { + it('fetches projects', (done) => { + const query = 'dummy query'; + const options = { unused: 'option' }; + const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects.json?simple=true`; + const expectedData = Object.assign({ + search: query, + per_page: 20, + membership: true, + }, options); + spyOn(jQuery, 'ajax').and.callFake((request) => { + expect(request.url).toEqual(expectedUrl); + expect(request.dataType).toEqual('json'); + expect(request.data).toEqual(expectedData); + return sendDummyResponse(); + }); + + Api.projects(query, options, (response) => { + expect(response).toBe(dummyResponse); + done(); + }); + }); + }); + + describe('newLabel', () => { + it('creates a new label', (done) => { + const namespace = 'some namespace'; + const project = 'some project'; + const labelData = { some: 'data' }; + const expectedUrl = `${dummyUrlRoot}/${namespace}/${project}/labels`; + const expectedData = { + label: labelData, + }; + spyOn(jQuery, 'ajax').and.callFake((request) => { + expect(request.url).toEqual(expectedUrl); + expect(request.dataType).toEqual('json'); + expect(request.type).toEqual('POST'); + expect(request.data).toEqual(expectedData); + return sendDummyResponse(); + }); + + Api.newLabel(namespace, project, labelData, (response) => { + expect(response).toBe(dummyResponse); + done(); + }); + }); + }); + + describe('groupProjects', () => { + it('fetches group projects', (done) => { + const groupId = '123456'; + const query = 'dummy query'; + const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/groups/${groupId}/projects.json`; + const expectedData = { + search: query, + per_page: 20, + }; + spyOn(jQuery, 'ajax').and.callFake((request) => { + expect(request.url).toEqual(expectedUrl); + expect(request.dataType).toEqual('json'); + expect(request.data).toEqual(expectedData); + return sendDummyResponse(); + }); + + Api.groupProjects(groupId, query, (response) => { + expect(response).toBe(dummyResponse); + done(); + }); + }); + }); + + describe('licenseText', () => { + it('fetches a license text', (done) => { + const licenseKey = "driver's license"; + const data = { unused: 'option' }; + const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/templates/licenses/${licenseKey}`; + spyOn(jQuery, 'ajax').and.callFake((request) => { + expect(request.url).toEqual(expectedUrl); + expect(request.data).toEqual(data); + return sendDummyResponse(); + }); + + Api.licenseText(licenseKey, data, (response) => { + expect(response).toBe(dummyResponse); + done(); + }); + }); + }); + + describe('gitignoreText', () => { + it('fetches a gitignore text', (done) => { + const gitignoreKey = 'ignore git'; + const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/templates/gitignores/${gitignoreKey}`; + spyOn(jQuery, 'get').and.callFake((url, callback) => { + expect(url).toEqual(expectedUrl); + callback(dummyResponse); + }); + + Api.gitignoreText(gitignoreKey, (response) => { + expect(response).toBe(dummyResponse); + done(); + }); + }); + }); + + describe('gitlabCiYml', () => { + it('fetches a .gitlab-ci.yml', (done) => { + const gitlabCiYmlKey = 'Y CI ML'; + const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/templates/gitlab_ci_ymls/${gitlabCiYmlKey}`; + spyOn(jQuery, 'get').and.callFake((url, callback) => { + expect(url).toEqual(expectedUrl); + callback(dummyResponse); + }); + + Api.gitlabCiYml(gitlabCiYmlKey, (response) => { + expect(response).toBe(dummyResponse); + done(); + }); + }); + }); + + describe('dockerfileYml', () => { + it('fetches a Dockerfile', (done) => { + const dockerfileYmlKey = 'a giant whale'; + const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/templates/dockerfiles/${dockerfileYmlKey}`; + spyOn(jQuery, 'get').and.callFake((url, callback) => { + expect(url).toEqual(expectedUrl); + callback(dummyResponse); + }); + + Api.dockerfileYml(dockerfileYmlKey, (response) => { + expect(response).toBe(dummyResponse); + done(); + }); + }); + }); + + describe('issueTemplate', () => { + it('fetches an issue template', (done) => { + const namespace = 'some namespace'; + const project = 'some project'; + const templateKey = 'template key'; + const templateType = 'template type'; + const expectedUrl = `${dummyUrlRoot}/${namespace}/${project}/templates/${templateType}/${templateKey}`; + spyOn(jQuery, 'ajax').and.callFake((request) => { + expect(request.url).toEqual(expectedUrl); + return sendDummyResponse(); + }); + + Api.issueTemplate(namespace, project, templateKey, templateType, (error, response) => { + expect(error).toBe(null); + expect(response).toBe(dummyResponse); + done(); + }); + }); + }); + + describe('users', () => { + it('fetches users', (done) => { + const query = 'dummy query'; + const options = { unused: 'option' }; + const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/users.json`; + const expectedData = Object.assign({ + search: query, + per_page: 20, + }, options); + spyOn(jQuery, 'ajax').and.callFake((request) => { + expect(request.url).toEqual(expectedUrl); + expect(request.dataType).toEqual('json'); + expect(request.data).toEqual(expectedData); + return sendDummyResponse(); + }); + + Api.users(query, options) + .then((response) => { + expect(response).toBe(dummyResponse); + }) + .then(done) + .catch(done.fail); + }); + }); +}); diff --git a/spec/javascripts/awards_handler_spec.js b/spec/javascripts/awards_handler_spec.js index 68ad5f66676..3fc03324d16 100644 --- a/spec/javascripts/awards_handler_spec.js +++ b/spec/javascripts/awards_handler_spec.js @@ -3,7 +3,7 @@ import Cookies from 'js-cookie'; import AwardsHandler from '~/awards_handler'; -require('~/lib/utils/common_utils'); +import '~/lib/utils/common_utils'; (function() { var awardsHandler, lazyAssert, urlRoot, openAndWaitForEmojiMenu; diff --git a/spec/javascripts/behaviors/autosize_spec.js b/spec/javascripts/behaviors/autosize_spec.js index 3deaf258cae..67afba19190 100644 --- a/spec/javascripts/behaviors/autosize_spec.js +++ b/spec/javascripts/behaviors/autosize_spec.js @@ -1,6 +1,6 @@ /* eslint-disable space-before-function-paren, no-var, comma-dangle, no-return-assign, max-len */ -require('~/behaviors/autosize'); +import '~/behaviors/autosize'; (function() { describe('Autosize behavior', function() { diff --git a/spec/javascripts/behaviors/quick_submit_spec.js b/spec/javascripts/behaviors/quick_submit_spec.js index 4820ce41ade..f56b99f8a16 100644 --- a/spec/javascripts/behaviors/quick_submit_spec.js +++ b/spec/javascripts/behaviors/quick_submit_spec.js @@ -1,6 +1,6 @@ /* eslint-disable space-before-function-paren, no-var, no-return-assign, comma-dangle, jasmine/no-spec-dupes, new-cap, max-len */ -require('~/behaviors/quick_submit'); +import '~/behaviors/quick_submit'; (function() { describe('Quick Submit behavior', function() { diff --git a/spec/javascripts/behaviors/requires_input_spec.js b/spec/javascripts/behaviors/requires_input_spec.js index 3a84013a2ed..f9fa814b801 100644 --- a/spec/javascripts/behaviors/requires_input_spec.js +++ b/spec/javascripts/behaviors/requires_input_spec.js @@ -1,6 +1,6 @@ /* eslint-disable space-before-function-paren, no-var */ -require('~/behaviors/requires_input'); +import '~/behaviors/requires_input'; (function() { describe('requiresInput', function() { diff --git a/spec/javascripts/blob/create_branch_dropdown_spec.js b/spec/javascripts/blob/create_branch_dropdown_spec.js index 9f0d373cb81..6dbaa47c544 100644 --- a/spec/javascripts/blob/create_branch_dropdown_spec.js +++ b/spec/javascripts/blob/create_branch_dropdown_spec.js @@ -1,6 +1,6 @@ -require('~/gl_dropdown'); -require('~/blob/create_branch_dropdown'); -require('~/blob/target_branch_dropdown'); +import '~/gl_dropdown'; +import '~/blob/create_branch_dropdown'; +import '~/blob/target_branch_dropdown'; describe('CreateBranchDropdown', () => { const fixtureTemplate = 'static/target_branch_dropdown.html.raw'; diff --git a/spec/javascripts/blob/target_branch_dropdown_spec.js b/spec/javascripts/blob/target_branch_dropdown_spec.js index 76ed3dc1a2d..99c9537d2ec 100644 --- a/spec/javascripts/blob/target_branch_dropdown_spec.js +++ b/spec/javascripts/blob/target_branch_dropdown_spec.js @@ -1,6 +1,6 @@ -require('~/gl_dropdown'); -require('~/blob/create_branch_dropdown'); -require('~/blob/target_branch_dropdown'); +import '~/gl_dropdown'; +import '~/blob/create_branch_dropdown'; +import '~/blob/target_branch_dropdown'; describe('TargetBranchDropdown', () => { const fixtureTemplate = 'static/target_branch_dropdown.html.raw'; diff --git a/spec/javascripts/blob/viewer/index_spec.js b/spec/javascripts/blob/viewer/index_spec.js index 13f122b68b2..af04e7c1e72 100644 --- a/spec/javascripts/blob/viewer/index_spec.js +++ b/spec/javascripts/blob/viewer/index_spec.js @@ -83,25 +83,48 @@ describe('Blob viewer', () => { }); describe('copy blob button', () => { + let copyButton; + + beforeEach(() => { + copyButton = document.querySelector('.js-copy-blob-source-btn'); + }); + it('disabled on load', () => { expect( - document.querySelector('.js-copy-blob-source-btn').classList.contains('disabled'), + copyButton.classList.contains('disabled'), ).toBeTruthy(); }); it('has tooltip when disabled', () => { expect( - document.querySelector('.js-copy-blob-source-btn').getAttribute('data-original-title'), + copyButton.getAttribute('data-original-title'), ).toBe('Switch to the source to copy it to the clipboard'); }); + it('is blurred when clicked and disabled', () => { + spyOn(copyButton, 'blur'); + + copyButton.click(); + + expect(copyButton.blur).toHaveBeenCalled(); + }); + + it('is not blurred when clicked and not disabled', () => { + spyOn(copyButton, 'blur'); + + copyButton.classList.remove('disabled'); + copyButton.click(); + + expect(copyButton.blur).not.toHaveBeenCalled(); + }); + it('enables after switching to simple view', (done) => { document.querySelector('.js-blob-viewer-switch-btn[data-viewer="simple"]').click(); setTimeout(() => { expect($.ajax).toHaveBeenCalled(); expect( - document.querySelector('.js-copy-blob-source-btn').classList.contains('disabled'), + copyButton.classList.contains('disabled'), ).toBeFalsy(); done(); @@ -115,7 +138,7 @@ describe('Blob viewer', () => { expect($.ajax).toHaveBeenCalled(); expect( - document.querySelector('.js-copy-blob-source-btn').getAttribute('data-original-title'), + copyButton.getAttribute('data-original-title'), ).toBe('Copy source to clipboard'); done(); diff --git a/spec/javascripts/boards/board_card_spec.js b/spec/javascripts/boards/board_card_spec.js index 376e706d1db..447b244c71f 100644 --- a/spec/javascripts/boards/board_card_spec.js +++ b/spec/javascripts/boards/board_card_spec.js @@ -8,11 +8,11 @@ import Vue from 'vue'; import '~/boards/models/assignee'; -require('~/boards/models/list'); -require('~/boards/models/label'); -require('~/boards/stores/boards_store'); -const boardCard = require('~/boards/components/board_card').default; -require('./mock_data'); +import '~/boards/models/list'; +import '~/boards/models/label'; +import '~/boards/stores/boards_store'; +import boardCard from '~/boards/components/board_card'; +import './mock_data'; describe('Issue card', () => { let vm; diff --git a/spec/javascripts/boards/board_new_issue_spec.js b/spec/javascripts/boards/board_new_issue_spec.js index 4999933c0c1..45d12e252c4 100644 --- a/spec/javascripts/boards/board_new_issue_spec.js +++ b/spec/javascripts/boards/board_new_issue_spec.js @@ -6,8 +6,8 @@ import Vue from 'vue'; import boardNewIssue from '~/boards/components/board_new_issue'; -require('~/boards/models/list'); -require('./mock_data'); +import '~/boards/models/list'; +import './mock_data'; describe('Issue boards new issue form', () => { let vm; diff --git a/spec/javascripts/boards/issue_card_spec.js b/spec/javascripts/boards/issue_card_spec.js index fddde799d01..bd9b4fbfdd3 100644 --- a/spec/javascripts/boards/issue_card_spec.js +++ b/spec/javascripts/boards/issue_card_spec.js @@ -129,7 +129,7 @@ describe('Issue card component', () => { it('sets title', () => { expect( - component.$el.querySelector('.card-assignee a').getAttribute('title'), + component.$el.querySelector('.card-assignee img').getAttribute('data-original-title'), ).toContain(`Assigned to ${user.name}`); }); diff --git a/spec/javascripts/commits_spec.js b/spec/javascripts/commits_spec.js index 05260760c43..187db7485a5 100644 --- a/spec/javascripts/commits_spec.js +++ b/spec/javascripts/commits_spec.js @@ -1,8 +1,8 @@ /* global CommitsList */ -require('vendor/jquery.endless-scroll'); -require('~/pager'); -require('~/commits'); +import 'vendor/jquery.endless-scroll'; +import '~/pager'; +import '~/commits'; (() => { // TODO: remove this hack! diff --git a/spec/javascripts/copy_as_gfm_spec.js b/spec/javascripts/copy_as_gfm_spec.js new file mode 100644 index 00000000000..ded450749d3 --- /dev/null +++ b/spec/javascripts/copy_as_gfm_spec.js @@ -0,0 +1,49 @@ +import '~/copy_as_gfm'; + +(() => { + describe('gl.CopyAsGFM', () => { + describe('gl.CopyAsGFM.pasteGFM', () => { + function callPasteGFM() { + const e = { + originalEvent: { + clipboardData: { + getData(mimeType) { + // When GFM code is copied, we put the regular plain text + // on the clipboard as `text/plain`, and the GFM as `text/x-gfm`. + // This emulates the behavior of `getData` with that data. + if (mimeType === 'text/plain') { + return 'code'; + } + if (mimeType === 'text/x-gfm') { + return '`code`'; + } + return null; + }, + }, + }, + preventDefault() {}, + }; + + window.gl.CopyAsGFM.pasteGFM(e); + } + + it('wraps pasted code when not already in code tags', () => { + spyOn(window.gl.utils, 'insertText').and.callFake((el, textFunc) => { + const insertedText = textFunc('This is code: ', ''); + expect(insertedText).toEqual('`code`'); + }); + + callPasteGFM(); + }); + + it('does not wrap pasted code when already in code tags', () => { + spyOn(window.gl.utils, 'insertText').and.callFake((el, textFunc) => { + const insertedText = textFunc('This is code: `', '`'); + expect(insertedText).toEqual('code'); + }); + + callPasteGFM(); + }); + }); + }); +})(); diff --git a/spec/javascripts/datetime_utility_spec.js b/spec/javascripts/datetime_utility_spec.js index d5eec10be42..e347c980c78 100644 --- a/spec/javascripts/datetime_utility_spec.js +++ b/spec/javascripts/datetime_utility_spec.js @@ -1,4 +1,4 @@ -require('~/lib/utils/datetime_utility'); +import '~/lib/utils/datetime_utility'; (() => { describe('Date time utils', () => { diff --git a/spec/javascripts/diff_comments_store_spec.js b/spec/javascripts/diff_comments_store_spec.js index 66ece7e4f41..d6fc6b56b82 100644 --- a/spec/javascripts/diff_comments_store_spec.js +++ b/spec/javascripts/diff_comments_store_spec.js @@ -1,9 +1,9 @@ /* eslint-disable jasmine/no-global-setup, dot-notation, jasmine/no-expect-in-setup-teardown, max-len */ /* global CommentsStore */ -require('~/diff_notes/models/discussion'); -require('~/diff_notes/models/note'); -require('~/diff_notes/stores/comments'); +import '~/diff_notes/models/discussion'; +import '~/diff_notes/models/note'; +import '~/diff_notes/stores/comments'; function createDiscussion(noteId = 1, resolved = true) { CommentsStore.create({ diff --git a/spec/javascripts/extensions/array_spec.js b/spec/javascripts/extensions/array_spec.js index 4b871fe967d..b1b81b4efc2 100644 --- a/spec/javascripts/extensions/array_spec.js +++ b/spec/javascripts/extensions/array_spec.js @@ -1,6 +1,6 @@ /* eslint-disable space-before-function-paren, no-var */ -require('~/extensions/array'); +import '~/extensions/array'; (function() { describe('Array extensions', function() { diff --git a/spec/javascripts/filtered_search/components/recent_searches_dropdown_content_spec.js b/spec/javascripts/filtered_search/components/recent_searches_dropdown_content_spec.js index d0f09a561d5..79447787fc9 100644 --- a/spec/javascripts/filtered_search/components/recent_searches_dropdown_content_spec.js +++ b/spec/javascripts/filtered_search/components/recent_searches_dropdown_content_spec.js @@ -2,6 +2,8 @@ import Vue from 'vue'; import eventHub from '~/filtered_search/event_hub'; import RecentSearchesDropdownContent from '~/filtered_search/components/recent_searches_dropdown_content'; +import '~/filtered_search/filtered_search_token_keys'; + const createComponent = (propsData) => { const Component = Vue.extend(RecentSearchesDropdownContent); @@ -17,12 +19,14 @@ const trimMarkupWhitespace = text => text.replace(/(\n|\s)+/gm, ' ').trim(); describe('RecentSearchesDropdownContent', () => { const propsDataWithoutItems = { items: [], + allowedKeys: gl.FilteredSearchTokenKeys.getKeys(), }; const propsDataWithItems = { items: [ 'foo', 'author:@root label:~foo bar', ], + allowedKeys: gl.FilteredSearchTokenKeys.getKeys(), }; let vm; diff --git a/spec/javascripts/filtered_search/dropdown_user_spec.js b/spec/javascripts/filtered_search/dropdown_user_spec.js index 3f92fe4701e..f7708301b6e 100644 --- a/spec/javascripts/filtered_search/dropdown_user_spec.js +++ b/spec/javascripts/filtered_search/dropdown_user_spec.js @@ -1,7 +1,7 @@ -require('~/filtered_search/dropdown_utils'); -require('~/filtered_search/filtered_search_tokenizer'); -require('~/filtered_search/filtered_search_dropdown'); -require('~/filtered_search/dropdown_user'); +import '~/filtered_search/dropdown_utils'; +import '~/filtered_search/filtered_search_tokenizer'; +import '~/filtered_search/filtered_search_dropdown'; +import '~/filtered_search/dropdown_user'; describe('Dropdown User', () => { describe('getSearchInput', () => { @@ -12,7 +12,7 @@ describe('Dropdown User', () => { spyOn(gl.DropdownUser.prototype, 'getProjectId').and.callFake(() => {}); spyOn(gl.DropdownUtils, 'getSearchInput').and.callFake(() => {}); - dropdownUser = new gl.DropdownUser(); + dropdownUser = new gl.DropdownUser(null, null, null, gl.FilteredSearchTokenKeys); }); it('should not return the double quote found in value', () => { diff --git a/spec/javascripts/filtered_search/dropdown_utils_spec.js b/spec/javascripts/filtered_search/dropdown_utils_spec.js index c820c955172..bb02abdeea2 100644 --- a/spec/javascripts/filtered_search/dropdown_utils_spec.js +++ b/spec/javascripts/filtered_search/dropdown_utils_spec.js @@ -1,7 +1,7 @@ -require('~/extensions/array'); -require('~/filtered_search/dropdown_utils'); -require('~/filtered_search/filtered_search_tokenizer'); -require('~/filtered_search/filtered_search_dropdown_manager'); +import '~/extensions/array'; +import '~/filtered_search/dropdown_utils'; +import '~/filtered_search/filtered_search_tokenizer'; +import '~/filtered_search/filtered_search_dropdown_manager'; describe('Dropdown Utils', () => { describe('getEscapedText', () => { @@ -122,6 +122,7 @@ describe('Dropdown Utils', () => { describe('filterHint', () => { let input; + let allowedKeys; beforeEach(() => { setFixtures(` @@ -133,30 +134,38 @@ describe('Dropdown Utils', () => { `); input = document.getElementById('test'); + allowedKeys = gl.FilteredSearchTokenKeys.getKeys(); }); + function config() { + return { + input, + allowedKeys, + }; + } + it('should filter', () => { input.value = 'l'; - let updatedItem = gl.DropdownUtils.filterHint(input, { + let updatedItem = gl.DropdownUtils.filterHint(config(), { hint: 'label', }); expect(updatedItem.droplab_hidden).toBe(false); input.value = 'o'; - updatedItem = gl.DropdownUtils.filterHint(input, { + updatedItem = gl.DropdownUtils.filterHint(config(), { hint: 'label', }); expect(updatedItem.droplab_hidden).toBe(true); }); it('should return droplab_hidden false when item has no hint', () => { - const updatedItem = gl.DropdownUtils.filterHint(input, {}, ''); + const updatedItem = gl.DropdownUtils.filterHint(config(), {}, ''); expect(updatedItem.droplab_hidden).toBe(false); }); it('should allow multiple if item.type is array', () => { input.value = 'label:~first la'; - const updatedItem = gl.DropdownUtils.filterHint(input, { + const updatedItem = gl.DropdownUtils.filterHint(config(), { hint: 'label', type: 'array', }); @@ -165,12 +174,12 @@ describe('Dropdown Utils', () => { it('should prevent multiple if item.type is not array', () => { input.value = 'milestone:~first mile'; - let updatedItem = gl.DropdownUtils.filterHint(input, { + let updatedItem = gl.DropdownUtils.filterHint(config(), { hint: 'milestone', }); expect(updatedItem.droplab_hidden).toBe(true); - updatedItem = gl.DropdownUtils.filterHint(input, { + updatedItem = gl.DropdownUtils.filterHint(config(), { hint: 'milestone', type: 'string', }); diff --git a/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js b/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js index 17bf8932489..c92a147b937 100644 --- a/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js +++ b/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js @@ -1,7 +1,7 @@ -require('~/extensions/array'); -require('~/filtered_search/filtered_search_visual_tokens'); -require('~/filtered_search/filtered_search_tokenizer'); -require('~/filtered_search/filtered_search_dropdown_manager'); +import '~/extensions/array'; +import '~/filtered_search/filtered_search_visual_tokens'; +import '~/filtered_search/filtered_search_tokenizer'; +import '~/filtered_search/filtered_search_dropdown_manager'; describe('Filtered Search Dropdown Manager', () => { describe('addWordToInput', () => { diff --git a/spec/javascripts/filtered_search/filtered_search_manager_spec.js b/spec/javascripts/filtered_search/filtered_search_manager_spec.js index 063d547d00c..8688332782d 100644 --- a/spec/javascripts/filtered_search/filtered_search_manager_spec.js +++ b/spec/javascripts/filtered_search/filtered_search_manager_spec.js @@ -1,14 +1,13 @@ import * as recentSearchesStoreSrc from '~/filtered_search/stores/recent_searches_store'; import RecentSearchesService from '~/filtered_search/services/recent_searches_service'; import RecentSearchesServiceError from '~/filtered_search/services/recent_searches_service_error'; - -require('~/lib/utils/url_utility'); -require('~/lib/utils/common_utils'); -require('~/filtered_search/filtered_search_token_keys'); -require('~/filtered_search/filtered_search_tokenizer'); -require('~/filtered_search/filtered_search_dropdown_manager'); -require('~/filtered_search/filtered_search_manager'); -const FilteredSearchSpecHelper = require('../helpers/filtered_search_spec_helper'); +import '~/lib/utils/url_utility'; +import '~/lib/utils/common_utils'; +import '~/filtered_search/filtered_search_token_keys'; +import '~/filtered_search/filtered_search_tokenizer'; +import '~/filtered_search/filtered_search_dropdown_manager'; +import '~/filtered_search/filtered_search_manager'; +import FilteredSearchSpecHelper from '../helpers/filtered_search_spec_helper'; describe('Filtered Search Manager', () => { let input; @@ -81,6 +80,7 @@ describe('Filtered Search Manager', () => { expect(RecentSearchesService.isAvailable).toHaveBeenCalled(); expect(recentSearchesStoreSrc.default).toHaveBeenCalledWith({ isLocalStorageAvailable, + allowedKeys: gl.FilteredSearchTokenKeys.getKeys(), }); }); diff --git a/spec/javascripts/filtered_search/filtered_search_token_keys_spec.js b/spec/javascripts/filtered_search/filtered_search_token_keys_spec.js index 6f9fa434c35..1a7631994b4 100644 --- a/spec/javascripts/filtered_search/filtered_search_token_keys_spec.js +++ b/spec/javascripts/filtered_search/filtered_search_token_keys_spec.js @@ -1,5 +1,5 @@ -require('~/extensions/array'); -require('~/filtered_search/filtered_search_token_keys'); +import '~/extensions/array'; +import '~/filtered_search/filtered_search_token_keys'; describe('Filtered Search Token Keys', () => { describe('get', () => { diff --git a/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js b/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js index 3e2e577f115..e4a15c83c23 100644 --- a/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js +++ b/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js @@ -1,11 +1,13 @@ -require('~/extensions/array'); -require('~/filtered_search/filtered_search_token_keys'); -require('~/filtered_search/filtered_search_tokenizer'); +import '~/extensions/array'; +import '~/filtered_search/filtered_search_token_keys'; +import '~/filtered_search/filtered_search_tokenizer'; describe('Filtered Search Tokenizer', () => { + const allowedKeys = gl.FilteredSearchTokenKeys.getKeys(); + describe('processTokens', () => { it('returns for input containing only search value', () => { - const results = gl.FilteredSearchTokenizer.processTokens('searchTerm'); + const results = gl.FilteredSearchTokenizer.processTokens('searchTerm', allowedKeys); expect(results.searchToken).toBe('searchTerm'); expect(results.tokens.length).toBe(0); expect(results.lastToken).toBe(results.searchToken); @@ -13,7 +15,7 @@ describe('Filtered Search Tokenizer', () => { it('returns for input containing only tokens', () => { const results = gl.FilteredSearchTokenizer - .processTokens('author:@root label:~"Very Important" milestone:%v1.0 assignee:none'); + .processTokens('author:@root label:~"Very Important" milestone:%v1.0 assignee:none', allowedKeys); expect(results.searchToken).toBe(''); expect(results.tokens.length).toBe(4); expect(results.tokens[3]).toBe(results.lastToken); @@ -37,7 +39,7 @@ describe('Filtered Search Tokenizer', () => { it('returns for input starting with search value and ending with tokens', () => { const results = gl.FilteredSearchTokenizer - .processTokens('searchTerm anotherSearchTerm milestone:none'); + .processTokens('searchTerm anotherSearchTerm milestone:none', allowedKeys); expect(results.searchToken).toBe('searchTerm anotherSearchTerm'); expect(results.tokens.length).toBe(1); expect(results.tokens[0]).toBe(results.lastToken); @@ -48,7 +50,7 @@ describe('Filtered Search Tokenizer', () => { it('returns for input starting with tokens and ending with search value', () => { const results = gl.FilteredSearchTokenizer - .processTokens('assignee:@user searchTerm'); + .processTokens('assignee:@user searchTerm', allowedKeys); expect(results.searchToken).toBe('searchTerm'); expect(results.tokens.length).toBe(1); @@ -60,7 +62,7 @@ describe('Filtered Search Tokenizer', () => { it('returns for input containing search value wrapped between tokens', () => { const results = gl.FilteredSearchTokenizer - .processTokens('author:@root label:~"Won\'t fix" searchTerm anotherSearchTerm milestone:none'); + .processTokens('author:@root label:~"Won\'t fix" searchTerm anotherSearchTerm milestone:none', allowedKeys); expect(results.searchToken).toBe('searchTerm anotherSearchTerm'); expect(results.tokens.length).toBe(3); @@ -81,7 +83,7 @@ describe('Filtered Search Tokenizer', () => { it('returns for input containing search value in between tokens', () => { const results = gl.FilteredSearchTokenizer - .processTokens('author:@root searchTerm assignee:none anotherSearchTerm label:~Doing'); + .processTokens('author:@root searchTerm assignee:none anotherSearchTerm label:~Doing', allowedKeys); expect(results.searchToken).toBe('searchTerm anotherSearchTerm'); expect(results.tokens.length).toBe(3); expect(results.tokens[2]).toBe(results.lastToken); @@ -100,14 +102,14 @@ describe('Filtered Search Tokenizer', () => { }); it('returns search value for invalid tokens', () => { - const results = gl.FilteredSearchTokenizer.processTokens('fake:token'); + const results = gl.FilteredSearchTokenizer.processTokens('fake:token', allowedKeys); expect(results.lastToken).toBe('fake:token'); expect(results.searchToken).toBe('fake:token'); expect(results.tokens.length).toEqual(0); }); it('returns search value and token for mix of valid and invalid tokens', () => { - const results = gl.FilteredSearchTokenizer.processTokens('label:real fake:token'); + const results = gl.FilteredSearchTokenizer.processTokens('label:real fake:token', allowedKeys); expect(results.tokens.length).toEqual(1); expect(results.tokens[0].key).toBe('label'); expect(results.tokens[0].value).toBe('real'); @@ -117,13 +119,13 @@ describe('Filtered Search Tokenizer', () => { }); it('returns search value for invalid symbols', () => { - const results = gl.FilteredSearchTokenizer.processTokens('std::includes'); + const results = gl.FilteredSearchTokenizer.processTokens('std::includes', allowedKeys); expect(results.lastToken).toBe('std::includes'); expect(results.searchToken).toBe('std::includes'); }); it('removes duplicated values', () => { - const results = gl.FilteredSearchTokenizer.processTokens('label:~foo label:~foo'); + const results = gl.FilteredSearchTokenizer.processTokens('label:~foo label:~foo', allowedKeys); expect(results.tokens.length).toBe(1); expect(results.tokens[0].key).toBe('label'); expect(results.tokens[0].value).toBe('foo'); diff --git a/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js b/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js index 8b750561eb7..c5fa2b17106 100644 --- a/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js +++ b/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js @@ -1,7 +1,7 @@ import AjaxCache from '~/lib/utils/ajax_cache'; -require('~/filtered_search/filtered_search_visual_tokens'); -const FilteredSearchSpecHelper = require('../helpers/filtered_search_spec_helper'); +import '~/filtered_search/filtered_search_visual_tokens'; +import FilteredSearchSpecHelper from '../helpers/filtered_search_spec_helper'; describe('Filtered Search Visual Tokens', () => { let tokensContainer; diff --git a/spec/javascripts/filtered_search/services/recent_searches_service_spec.js b/spec/javascripts/filtered_search/services/recent_searches_service_spec.js index 31fa478804a..c293c0afa97 100644 --- a/spec/javascripts/filtered_search/services/recent_searches_service_spec.js +++ b/spec/javascripts/filtered_search/services/recent_searches_service_spec.js @@ -1,6 +1,5 @@ -/* eslint-disable promise/catch-or-return */ - import RecentSearchesService from '~/filtered_search/services/recent_searches_service'; +import RecentSearchesServiceError from '~/filtered_search/services/recent_searches_service_error'; import AccessorUtilities from '~/lib/utils/accessor'; describe('RecentSearchesService', () => { @@ -22,11 +21,9 @@ describe('RecentSearchesService', () => { fetchItemsPromise .then((items) => { expect(items).toEqual([]); - done(); }) - .catch((err) => { - done.fail('Shouldn\'t reject with empty localStorage key', err); - }); + .then(done) + .catch(done.fail); }); it('should reject when unable to parse', (done) => { @@ -34,19 +31,24 @@ describe('RecentSearchesService', () => { const fetchItemsPromise = service.fetch(); fetchItemsPromise + .then(done.fail) .catch((error) => { expect(error).toEqual(jasmine.any(SyntaxError)); - done(); - }); + }) + .then(done) + .catch(done.fail); }); it('should reject when service is unavailable', (done) => { RecentSearchesService.isAvailable.and.returnValue(false); - service.fetch().catch((error) => { - expect(error).toEqual(jasmine.any(Error)); - done(); - }); + service.fetch() + .then(done.fail) + .catch((error) => { + expect(error).toEqual(jasmine.any(Error)); + }) + .then(done) + .catch(done.fail); }); it('should return items from localStorage', (done) => { @@ -56,8 +58,9 @@ describe('RecentSearchesService', () => { fetchItemsPromise .then((items) => { expect(items).toEqual(['foo', 'bar']); - done(); - }); + }) + .then(done) + .catch(done.fail); }); describe('if .isAvailable returns `false`', () => { @@ -65,12 +68,17 @@ describe('RecentSearchesService', () => { RecentSearchesService.isAvailable.and.returnValue(false); spyOn(window.localStorage, 'getItem'); - - RecentSearchesService.prototype.fetch(); }); - it('should not call .getItem', () => { - expect(window.localStorage.getItem).not.toHaveBeenCalled(); + it('should not call .getItem', (done) => { + RecentSearchesService.prototype.fetch() + .then(done.fail) + .catch((err) => { + expect(err).toEqual(new RecentSearchesServiceError()); + expect(window.localStorage.getItem).not.toHaveBeenCalled(); + }) + .then(done) + .catch(done.fail); }); }); }); @@ -105,11 +113,11 @@ describe('RecentSearchesService', () => { RecentSearchesService.isAvailable.and.returnValue(true); spyOn(JSON, 'stringify').and.returnValue(searchesString); - - RecentSearchesService.prototype.save.call(recentSearchesService); }); it('should call .setItem', () => { + RecentSearchesService.prototype.save.call(recentSearchesService); + expect(window.localStorage.setItem).toHaveBeenCalledWith(localStorageKey, searchesString); }); }); @@ -117,11 +125,11 @@ describe('RecentSearchesService', () => { describe('if .isAvailable returns `false`', () => { beforeEach(() => { RecentSearchesService.isAvailable.and.returnValue(false); - - RecentSearchesService.prototype.save(); }); it('should not call .setItem', () => { + RecentSearchesService.prototype.save(); + expect(window.localStorage.setItem).not.toHaveBeenCalled(); }); }); diff --git a/spec/javascripts/gfm_auto_complete_spec.js b/spec/javascripts/gfm_auto_complete_spec.js index 5dfa4008fbd..ad0c7264616 100644 --- a/spec/javascripts/gfm_auto_complete_spec.js +++ b/spec/javascripts/gfm_auto_complete_spec.js @@ -1,13 +1,15 @@ /* eslint no-param-reassign: "off" */ -require('~/gfm_auto_complete'); -require('vendor/jquery.caret'); -require('vendor/jquery.atwho'); +import GfmAutoComplete from '~/gfm_auto_complete'; -const global = window.gl || (window.gl = {}); -const GfmAutoComplete = global.GfmAutoComplete; +import 'vendor/jquery.caret'; +import 'vendor/jquery.atwho'; describe('GfmAutoComplete', function () { + const gfmAutoCompleteCallbacks = GfmAutoComplete.prototype.getDefaultCallbacks.call({ + fetchData: () => {}, + }); + describe('DefaultOptions.sorter', function () { describe('assets loading', function () { beforeEach(function () { @@ -16,7 +18,7 @@ describe('GfmAutoComplete', function () { this.atwhoInstance = { setting: {} }; this.items = []; - this.sorterValue = GfmAutoComplete.DefaultOptions.sorter + this.sorterValue = gfmAutoCompleteCallbacks.sorter .call(this.atwhoInstance, '', this.items); }); @@ -38,7 +40,7 @@ describe('GfmAutoComplete', function () { it('should enable highlightFirst if alwaysHighlightFirst is set', function () { const atwhoInstance = { setting: { alwaysHighlightFirst: true } }; - GfmAutoComplete.DefaultOptions.sorter.call(atwhoInstance); + gfmAutoCompleteCallbacks.sorter.call(atwhoInstance); expect(atwhoInstance.setting.highlightFirst).toBe(true); }); @@ -46,7 +48,7 @@ describe('GfmAutoComplete', function () { it('should enable highlightFirst if a query is present', function () { const atwhoInstance = { setting: {} }; - GfmAutoComplete.DefaultOptions.sorter.call(atwhoInstance, 'query'); + gfmAutoCompleteCallbacks.sorter.call(atwhoInstance, 'query'); expect(atwhoInstance.setting.highlightFirst).toBe(true); }); @@ -58,7 +60,7 @@ describe('GfmAutoComplete', function () { const items = []; const searchKey = 'searchKey'; - GfmAutoComplete.DefaultOptions.sorter.call(atwhoInstance, query, items, searchKey); + gfmAutoCompleteCallbacks.sorter.call(atwhoInstance, query, items, searchKey); expect($.fn.atwho.default.callbacks.sorter).toHaveBeenCalledWith(query, items, searchKey); }); @@ -67,7 +69,7 @@ describe('GfmAutoComplete', function () { describe('DefaultOptions.matcher', function () { const defaultMatcher = (context, flag, subtext) => ( - GfmAutoComplete.DefaultOptions.matcher.call(context, flag, subtext) + gfmAutoCompleteCallbacks.matcher.call(context, flag, subtext) ); const flagsUseDefaultMatcher = ['@', '#', '!', '~', '%']; diff --git a/spec/javascripts/gl_dropdown_spec.js b/spec/javascripts/gl_dropdown_spec.js index 8f90ed69e64..3292590b9ed 100644 --- a/spec/javascripts/gl_dropdown_spec.js +++ b/spec/javascripts/gl_dropdown_spec.js @@ -1,8 +1,8 @@ /* eslint-disable comma-dangle, no-param-reassign, no-unused-expressions, max-len */ -require('~/gl_dropdown'); -require('~/lib/utils/common_utils'); -require('~/lib/utils/url_utility'); +import '~/gl_dropdown'; +import '~/lib/utils/common_utils'; +import '~/lib/utils/url_utility'; (() => { const NON_SELECTABLE_CLASSES = '.divider, .separator, .dropdown-header, .dropdown-menu-empty-link'; diff --git a/spec/javascripts/gl_field_errors_spec.js b/spec/javascripts/gl_field_errors_spec.js index 733023481f5..fa24aa426b6 100644 --- a/spec/javascripts/gl_field_errors_spec.js +++ b/spec/javascripts/gl_field_errors_spec.js @@ -1,6 +1,6 @@ /* eslint-disable space-before-function-paren, arrow-body-style */ -require('~/gl_field_errors'); +import '~/gl_field_errors'; ((global) => { preloadFixtures('static/gl_field_errors.html.raw'); diff --git a/spec/javascripts/gl_form_spec.js b/spec/javascripts/gl_form_spec.js index 5ed18d0a76b..837feacec1d 100644 --- a/spec/javascripts/gl_form_spec.js +++ b/spec/javascripts/gl_form_spec.js @@ -1,9 +1,9 @@ -/* global autosize */ +import autosize from 'vendor/autosize'; +import '~/gl_form'; +import '~/lib/utils/text_utility'; +import '~/lib/utils/common_utils'; -window.autosize = require('vendor/autosize'); -require('~/gl_form'); -require('~/lib/utils/text_utility'); -require('~/lib/utils/common_utils'); +window.autosize = autosize; describe('GLForm', () => { const global = window.gl || (window.gl = {}); @@ -27,7 +27,7 @@ describe('GLForm', () => { $.prototype.off.calls.reset(); $.prototype.on.calls.reset(); $.prototype.css.calls.reset(); - autosize.calls.reset(); + window.autosize.calls.reset(); done(); }); }); @@ -51,7 +51,7 @@ describe('GLForm', () => { }); it('should autosize the textarea', () => { - expect(autosize).toHaveBeenCalledWith(jasmine.any(Object)); + expect(window.autosize).toHaveBeenCalledWith(jasmine.any(Object)); }); it('should set the resize css property to vertical', () => { @@ -81,7 +81,7 @@ describe('GLForm', () => { spyOn($.prototype, 'data'); spyOn($.prototype, 'outerHeight').and.returnValue(200); spyOn(window, 'outerHeight').and.returnValue(400); - spyOn(autosize, 'destroy'); + spyOn(window.autosize, 'destroy'); this.glForm.destroyAutosize(); }); @@ -95,7 +95,7 @@ describe('GLForm', () => { }); it('should call autosize destroy', () => { - expect(autosize.destroy).toHaveBeenCalledWith(this.textarea); + expect(window.autosize.destroy).toHaveBeenCalledWith(this.textarea); }); it('should set the data-height attribute', () => { @@ -114,9 +114,9 @@ describe('GLForm', () => { it('should return undefined if the data-height equals the outerHeight', () => { spyOn($.prototype, 'outerHeight').and.returnValue(200); spyOn($.prototype, 'data').and.returnValue(200); - spyOn(autosize, 'destroy'); + spyOn(window.autosize, 'destroy'); expect(this.glForm.destroyAutosize()).toBeUndefined(); - expect(autosize.destroy).not.toHaveBeenCalled(); + expect(window.autosize.destroy).not.toHaveBeenCalled(); }); }); }); diff --git a/spec/javascripts/header_spec.js b/spec/javascripts/header_spec.js index b5dde5525e5..0e01934d3a3 100644 --- a/spec/javascripts/header_spec.js +++ b/spec/javascripts/header_spec.js @@ -1,7 +1,7 @@ /* eslint-disable space-before-function-paren, no-var */ -require('~/header'); -require('~/lib/utils/text_utility'); +import '~/header'; +import '~/lib/utils/text_utility'; (function() { describe('Header', function() { diff --git a/spec/javascripts/helpers/class_spec_helper.js b/spec/javascripts/helpers/class_spec_helper.js index 61db27a8fcc..7a60d33b471 100644 --- a/spec/javascripts/helpers/class_spec_helper.js +++ b/spec/javascripts/helpers/class_spec_helper.js @@ -1,4 +1,4 @@ -class ClassSpecHelper { +export default class ClassSpecHelper { static itShouldBeAStaticMethod(base, method) { return it('should be a static method', () => { expect(Object.prototype.hasOwnProperty.call(base, method)).toBeTruthy(); @@ -7,5 +7,3 @@ class ClassSpecHelper { } window.ClassSpecHelper = ClassSpecHelper; - -module.exports = ClassSpecHelper; diff --git a/spec/javascripts/helpers/class_spec_helper_spec.js b/spec/javascripts/helpers/class_spec_helper_spec.js index a1cfc8ba820..686b8eaed31 100644 --- a/spec/javascripts/helpers/class_spec_helper_spec.js +++ b/spec/javascripts/helpers/class_spec_helper_spec.js @@ -1,6 +1,6 @@ /* global ClassSpecHelper */ -require('./class_spec_helper'); +import './class_spec_helper'; describe('ClassSpecHelper', () => { describe('itShouldBeAStaticMethod', function () { diff --git a/spec/javascripts/helpers/filtered_search_spec_helper.js b/spec/javascripts/helpers/filtered_search_spec_helper.js index b8d4a93b1ab..0d7092a2357 100644 --- a/spec/javascripts/helpers/filtered_search_spec_helper.js +++ b/spec/javascripts/helpers/filtered_search_spec_helper.js @@ -1,4 +1,4 @@ -class FilteredSearchSpecHelper { +export default class FilteredSearchSpecHelper { static createFilterVisualTokenHTML(name, value, isSelected) { return FilteredSearchSpecHelper.createFilterVisualToken(name, value, isSelected).outerHTML; } @@ -53,5 +53,3 @@ class FilteredSearchSpecHelper { `; } } - -module.exports = FilteredSearchSpecHelper; diff --git a/spec/javascripts/issuable_spec.js b/spec/javascripts/issuable_spec.js index 26d87cc5931..49fa2cb8367 100644 --- a/spec/javascripts/issuable_spec.js +++ b/spec/javascripts/issuable_spec.js @@ -1,7 +1,7 @@ /* global Issuable */ -require('~/lib/utils/url_utility'); -require('~/issuable'); +import '~/lib/utils/url_utility'; +import '~/issuable'; (() => { const BASE_URL = '/user/project/issues?scope=all&state=closed'; diff --git a/spec/javascripts/issue_spec.js b/spec/javascripts/issue_spec.js index 763f5ee9e50..df97a100b0d 100644 --- a/spec/javascripts/issue_spec.js +++ b/spec/javascripts/issue_spec.js @@ -1,7 +1,7 @@ /* eslint-disable space-before-function-paren, one-var, one-var-declaration-per-line, no-use-before-define, comma-dangle, max-len */ import Issue from '~/issue'; -require('~/lib/utils/text_utility'); +import '~/lib/utils/text_utility'; describe('Issue', function() { let $boxClosed, $boxOpen, $btnClose, $btnReopen; diff --git a/spec/javascripts/labels_issue_sidebar_spec.js b/spec/javascripts/labels_issue_sidebar_spec.js index 53aba191b19..c99f379b871 100644 --- a/spec/javascripts/labels_issue_sidebar_spec.js +++ b/spec/javascripts/labels_issue_sidebar_spec.js @@ -2,14 +2,14 @@ /* global IssuableContext */ /* global LabelsSelect */ -require('~/gl_dropdown'); -require('select2'); -require('vendor/jquery.nicescroll'); -require('~/api'); -require('~/create_label'); -require('~/issuable_context'); -require('~/users_select'); -require('~/labels_select'); +import '~/gl_dropdown'; +import 'select2'; +import 'vendor/jquery.nicescroll'; +import '~/api'; +import '~/create_label'; +import '~/issuable_context'; +import '~/users_select'; +import '~/labels_select'; (() => { let saveLabelCount = 0; diff --git a/spec/javascripts/lib/utils/cache_spec.js b/spec/javascripts/lib/utils/cache_spec.js new file mode 100644 index 00000000000..2fe02a7592c --- /dev/null +++ b/spec/javascripts/lib/utils/cache_spec.js @@ -0,0 +1,65 @@ +import Cache from '~/lib/utils/cache'; + +describe('Cache', () => { + const dummyKey = 'just some key'; + const dummyValue = 'more than a value'; + let cache; + + beforeEach(() => { + cache = new Cache(); + }); + + describe('get', () => { + it('return cached data', () => { + cache.internalStorage[dummyKey] = dummyValue; + + expect(cache.get(dummyKey)).toBe(dummyValue); + }); + + it('returns undefined for missing data', () => { + expect(cache.internalStorage[dummyKey]).toBe(undefined); + expect(cache.get(dummyKey)).toBe(undefined); + }); + }); + + describe('hasData', () => { + it('return true for cached data', () => { + cache.internalStorage[dummyKey] = dummyValue; + + expect(cache.hasData(dummyKey)).toBe(true); + }); + + it('returns false for missing data', () => { + expect(cache.internalStorage[dummyKey]).toBe(undefined); + expect(cache.hasData(dummyKey)).toBe(false); + }); + }); + + describe('remove', () => { + it('removes data from cache', () => { + cache.internalStorage[dummyKey] = dummyValue; + + cache.remove(dummyKey); + + expect(cache.internalStorage[dummyKey]).toBe(undefined); + }); + + it('does nothing for missing data', () => { + expect(cache.internalStorage[dummyKey]).toBe(undefined); + + cache.remove(dummyKey); + + expect(cache.internalStorage[dummyKey]).toBe(undefined); + }); + + it('does not remove wrong data', () => { + cache.internalStorage[dummyKey] = dummyValue; + cache.internalStorage[dummyKey + dummyKey] = dummyValue + dummyValue; + + cache.remove(dummyKey); + + expect(cache.internalStorage[dummyKey]).toBe(undefined); + expect(cache.internalStorage[dummyKey + dummyKey]).toBe(dummyValue + dummyValue); + }); + }); +}); diff --git a/spec/javascripts/lib/utils/common_utils_spec.js b/spec/javascripts/lib/utils/common_utils_spec.js index 5eb147ed888..e9bffd74d90 100644 --- a/spec/javascripts/lib/utils/common_utils_spec.js +++ b/spec/javascripts/lib/utils/common_utils_spec.js @@ -1,6 +1,6 @@ /* eslint-disable promise/catch-or-return */ -require('~/lib/utils/common_utils'); +import '~/lib/utils/common_utils'; (() => { describe('common_utils', () => { @@ -41,6 +41,16 @@ require('~/lib/utils/common_utils'); const paramsArray = gl.utils.getUrlParamsArray(); expect(paramsArray[0][0] !== '?').toBe(true); }); + + it('should decode params', () => { + history.pushState('', '', '?label_name%5B%5D=test'); + + expect( + gl.utils.getUrlParamsArray()[0], + ).toBe('label_name[]=test'); + + history.pushState('', '', '?'); + }); }); describe('gl.utils.handleLocationHash', () => { diff --git a/spec/javascripts/lib/utils/text_utility_spec.js b/spec/javascripts/lib/utils/text_utility_spec.js index daef9b93fa5..ca1b1b7cc3c 100644 --- a/spec/javascripts/lib/utils/text_utility_spec.js +++ b/spec/javascripts/lib/utils/text_utility_spec.js @@ -1,4 +1,4 @@ -require('~/lib/utils/text_utility'); +import '~/lib/utils/text_utility'; describe('text_utility', () => { describe('gl.text.getTextWidth', () => { diff --git a/spec/javascripts/lib/utils/users_cache_spec.js b/spec/javascripts/lib/utils/users_cache_spec.js new file mode 100644 index 00000000000..ec6ea35952b --- /dev/null +++ b/spec/javascripts/lib/utils/users_cache_spec.js @@ -0,0 +1,136 @@ +import Api from '~/api'; +import UsersCache from '~/lib/utils/users_cache'; + +describe('UsersCache', () => { + const dummyUsername = 'win'; + const dummyUser = 'has a farm'; + + beforeEach(() => { + UsersCache.internalStorage = { }; + }); + + describe('get', () => { + it('returns undefined for empty cache', () => { + expect(UsersCache.internalStorage).toEqual({ }); + + const user = UsersCache.get(dummyUsername); + + expect(user).toBe(undefined); + }); + + it('returns undefined for missing user', () => { + UsersCache.internalStorage['no body'] = 'no data'; + + const user = UsersCache.get(dummyUsername); + + expect(user).toBe(undefined); + }); + + it('returns matching user', () => { + UsersCache.internalStorage[dummyUsername] = dummyUser; + + const user = UsersCache.get(dummyUsername); + + expect(user).toBe(dummyUser); + }); + }); + + describe('hasData', () => { + it('returns false for empty cache', () => { + expect(UsersCache.internalStorage).toEqual({ }); + + expect(UsersCache.hasData(dummyUsername)).toBe(false); + }); + + it('returns false for missing user', () => { + UsersCache.internalStorage['no body'] = 'no data'; + + expect(UsersCache.hasData(dummyUsername)).toBe(false); + }); + + it('returns true for matching user', () => { + UsersCache.internalStorage[dummyUsername] = dummyUser; + + expect(UsersCache.hasData(dummyUsername)).toBe(true); + }); + }); + + describe('remove', () => { + it('does nothing if cache is empty', () => { + expect(UsersCache.internalStorage).toEqual({ }); + + UsersCache.remove(dummyUsername); + + expect(UsersCache.internalStorage).toEqual({ }); + }); + + it('does nothing if cache contains no matching data', () => { + UsersCache.internalStorage['no body'] = 'no data'; + + UsersCache.remove(dummyUsername); + + expect(UsersCache.internalStorage['no body']).toBe('no data'); + }); + + it('removes matching data', () => { + UsersCache.internalStorage[dummyUsername] = dummyUser; + + UsersCache.remove(dummyUsername); + + expect(UsersCache.internalStorage).toEqual({ }); + }); + }); + + describe('retrieve', () => { + let apiSpy; + + beforeEach(() => { + spyOn(Api, 'users').and.callFake((query, options) => apiSpy(query, options)); + }); + + it('stores and returns data from API call if cache is empty', (done) => { + apiSpy = (query, options) => { + expect(query).toBe(''); + expect(options).toEqual({ username: dummyUsername }); + return Promise.resolve([dummyUser]); + }; + + UsersCache.retrieve(dummyUsername) + .then((user) => { + expect(user).toBe(dummyUser); + expect(UsersCache.internalStorage[dummyUsername]).toBe(dummyUser); + }) + .then(done) + .catch(done.fail); + }); + + it('returns undefined if Ajax call fails and cache is empty', (done) => { + const dummyError = new Error('server exploded'); + apiSpy = (query, options) => { + expect(query).toBe(''); + expect(options).toEqual({ username: dummyUsername }); + return Promise.reject(dummyError); + }; + + UsersCache.retrieve(dummyUsername) + .then(user => fail(`Received unexpected user: ${JSON.stringify(user)}`)) + .catch((error) => { + expect(error).toBe(dummyError); + }) + .then(done) + .catch(done.fail); + }); + + it('makes no Ajax call if matching data exists', (done) => { + UsersCache.internalStorage[dummyUsername] = dummyUser; + apiSpy = () => fail(new Error('expected no Ajax call!')); + + UsersCache.retrieve(dummyUsername) + .then((user) => { + expect(user).toBe(dummyUser); + }) + .then(done) + .catch(done.fail); + }); + }); +}); diff --git a/spec/javascripts/line_highlighter_spec.js b/spec/javascripts/line_highlighter_spec.js index b1b08989028..aee274641e8 100644 --- a/spec/javascripts/line_highlighter_spec.js +++ b/spec/javascripts/line_highlighter_spec.js @@ -1,7 +1,7 @@ /* eslint-disable space-before-function-paren, no-var, no-param-reassign, quotes, prefer-template, no-else-return, new-cap, dot-notation, no-return-assign, comma-dangle, no-new, one-var, one-var-declaration-per-line, jasmine/no-spec-dupes, no-underscore-dangle, max-len */ /* global LineHighlighter */ -require('~/line_highlighter'); +import '~/line_highlighter'; (function() { describe('LineHighlighter', function() { diff --git a/spec/javascripts/merge_request_spec.js b/spec/javascripts/merge_request_spec.js index fd97dced870..1173fa40947 100644 --- a/spec/javascripts/merge_request_spec.js +++ b/spec/javascripts/merge_request_spec.js @@ -1,7 +1,7 @@ /* eslint-disable space-before-function-paren, no-return-assign */ /* global MergeRequest */ -require('~/merge_request'); +import '~/merge_request'; (function() { describe('MergeRequest', function() { diff --git a/spec/javascripts/merge_request_tabs_spec.js b/spec/javascripts/merge_request_tabs_spec.js index 254a41db160..3d1706aab68 100644 --- a/spec/javascripts/merge_request_tabs_spec.js +++ b/spec/javascripts/merge_request_tabs_spec.js @@ -1,13 +1,13 @@ /* eslint-disable no-var, comma-dangle, object-shorthand */ -require('~/merge_request_tabs'); -require('~/commit/pipelines/pipelines_bundle.js'); -require('~/breakpoints'); -require('~/lib/utils/common_utils'); -require('~/diff'); -require('~/single_file_diff'); -require('~/files_comment_button'); -require('vendor/jquery.scrollTo'); +import '~/merge_request_tabs'; +import '~/commit/pipelines/pipelines_bundle'; +import '~/breakpoints'; +import '~/lib/utils/common_utils'; +import '~/diff'; +import '~/single_file_diff'; +import '~/files_comment_button'; +import 'vendor/jquery.scrollTo'; (function () { // TODO: remove this hack! diff --git a/spec/javascripts/new_branch_spec.js b/spec/javascripts/new_branch_spec.js index 90a429beeca..c57f44dae17 100644 --- a/spec/javascripts/new_branch_spec.js +++ b/spec/javascripts/new_branch_spec.js @@ -1,7 +1,7 @@ /* eslint-disable space-before-function-paren, one-var, no-var, one-var-declaration-per-line, no-return-assign, quotes, max-len */ /* global NewBranchForm */ -require('~/new_branch_form'); +import '~/new_branch_form'; (function() { describe('Branch', function() { diff --git a/spec/javascripts/notes_spec.js b/spec/javascripts/notes_spec.js index 8243a9c991a..025f08ee332 100644 --- a/spec/javascripts/notes_spec.js +++ b/spec/javascripts/notes_spec.js @@ -14,6 +14,7 @@ import '~/notes'; gl.utils = gl.utils || {}; describe('Notes', function() { + const FLASH_TYPE_ALERT = 'alert'; var commentsTemplate = 'issues/issue_with_comment.html.raw'; preloadFixtures(commentsTemplate); @@ -78,6 +79,47 @@ import '~/notes'; }); }); + describe('updateNote', () => { + let sampleComment; + let noteEntity; + let $form; + let $notesContainer; + + beforeEach(() => { + this.notes = new Notes('', []); + window.gon.current_username = 'root'; + window.gon.current_user_fullname = 'Administrator'; + sampleComment = 'foo'; + noteEntity = { + id: 1234, + html: `<li class="note note-row-1234 timeline-entry" id="note_1234"> + <div class="note-text">${sampleComment}</div> + </li>`, + note: sampleComment, + valid: true + }; + $form = $('form.js-main-target-form'); + $notesContainer = $('ul.main-notes-list'); + $form.find('textarea.js-note-text').val(sampleComment); + }); + + it('updates note and resets edit form', () => { + const deferred = $.Deferred(); + spyOn($, 'ajax').and.returnValue(deferred.promise()); + spyOn(this.notes, 'revertNoteEditForm'); + + $('.js-comment-button').click(); + deferred.resolve(noteEntity); + + const $targetNote = $notesContainer.find(`#note_${noteEntity.id}`); + const updatedNote = Object.assign({}, noteEntity); + updatedNote.note = 'bar'; + this.notes.updateNote(updatedNote, $targetNote); + + expect(this.notes.revertNoteEditForm).toHaveBeenCalledWith($targetNote); + }); + }); + describe('renderNote', () => { let notes; let note; @@ -97,9 +139,8 @@ import '~/notes'; ]); notes = jasmine.createSpyObj('notes', [ + 'setupNewNote', 'refresh', - 'isNewNote', - 'isUpdatedNote', 'collapseLongCommitList', 'updateNotesCount', 'putConflictEditWarningInPlace' @@ -109,13 +150,15 @@ import '~/notes'; notes.updatedNotesTrackingMap = {}; spyOn(gl.utils, 'localTimeAgo'); + spyOn(Notes, 'isNewNote').and.callThrough(); + spyOn(Notes, 'isUpdatedNote').and.callThrough(); spyOn(Notes, 'animateAppendNote').and.callThrough(); spyOn(Notes, 'animateUpdateNote').and.callThrough(); }); describe('when adding note', () => { it('should call .animateAppendNote', () => { - notes.isNewNote.and.returnValue(true); + Notes.isNewNote.and.returnValue(true); Notes.prototype.renderNote.call(notes, note, null, $notesList); expect(Notes.animateAppendNote).toHaveBeenCalledWith(note.html, $notesList); @@ -124,7 +167,8 @@ import '~/notes'; describe('when note was edited', () => { it('should call .animateUpdateNote', () => { - notes.isUpdatedNote.and.returnValue(true); + Notes.isNewNote.and.returnValue(false); + Notes.isUpdatedNote.and.returnValue(true); const $note = $('<div>'); $notesList.find.and.returnValue($note); Notes.prototype.renderNote.call(notes, note, null, $notesList); @@ -134,7 +178,8 @@ import '~/notes'; describe('while editing', () => { it('should update textarea if nothing has been touched', () => { - notes.isUpdatedNote.and.returnValue(true); + Notes.isNewNote.and.returnValue(false); + Notes.isUpdatedNote.and.returnValue(true); const $note = $(`<div class="is-editing"> <div class="original-note-content">initial</div> <textarea class="js-note-text">initial</textarea> @@ -146,7 +191,8 @@ import '~/notes'; }); it('should call .putConflictEditWarningInPlace', () => { - notes.isUpdatedNote.and.returnValue(true); + Notes.isNewNote.and.returnValue(false); + Notes.isUpdatedNote.and.returnValue(true); const $note = $(`<div class="is-editing"> <div class="original-note-content">initial</div> <textarea class="js-note-text">different</textarea> @@ -160,6 +206,47 @@ import '~/notes'; }); }); + describe('isUpdatedNote', () => { + it('should consider same note text as the same', () => { + const result = Notes.isUpdatedNote( + { + note: 'initial' + }, + $(`<div> + <div class="original-note-content">initial</div> + </div>`) + ); + + expect(result).toEqual(false); + }); + + it('should consider same note with trailing newline as the same', () => { + const result = Notes.isUpdatedNote( + { + note: 'initial\n' + }, + $(`<div> + <div class="original-note-content">initial\n</div> + </div>`) + ); + + expect(result).toEqual(false); + }); + + it('should consider different notes as different', () => { + const result = Notes.isUpdatedNote( + { + note: 'foo' + }, + $(`<div> + <div class="original-note-content">bar</div> + </div>`) + ); + + expect(result).toEqual(true); + }); + }); + describe('renderDiscussionNote', () => { let discussionContainer; let note; @@ -179,15 +266,15 @@ import '~/notes'; row = jasmine.createSpyObj('row', ['prevAll', 'first', 'find']); notes = jasmine.createSpyObj('notes', [ - 'isNewNote', 'isParallelView', 'updateNotesCount', ]); notes.note_ids = []; spyOn(gl.utils, 'localTimeAgo'); + spyOn(Notes, 'isNewNote'); spyOn(Notes, 'animateAppendNote'); - notes.isNewNote.and.returnValue(true); + Notes.isNewNote.and.returnValue(true); notes.isParallelView.and.returnValue(false); row.prevAll.and.returnValue(row); row.first.and.returnValue(row); @@ -498,5 +585,33 @@ import '~/notes'; expect($tempNote.find('.timeline-content').hasClass('discussion')).toBeTruthy(); }); }); + + describe('appendFlash', () => { + beforeEach(() => { + this.notes = new Notes(); + }); + + it('shows a flash message', () => { + this.notes.addFlash('Error message', FLASH_TYPE_ALERT, this.notes.parentTimeline); + + expect(document.querySelectorAll('.flash-alert').length).toBe(1); + }); + }); + + describe('clearFlash', () => { + beforeEach(() => { + $(document).off('ajax:success'); + this.notes = new Notes(); + }); + + it('removes all the associated flash messages', () => { + this.notes.addFlash('Error message 1', FLASH_TYPE_ALERT, this.notes.parentTimeline); + this.notes.addFlash('Error message 2', FLASH_TYPE_ALERT, this.notes.parentTimeline); + + this.notes.clearFlash(); + + expect(document.querySelectorAll('.flash-alert').length).toBe(0); + }); + }); }); }).call(window); diff --git a/spec/javascripts/pager_spec.js b/spec/javascripts/pager_spec.js index d966226909b..1d3e1263371 100644 --- a/spec/javascripts/pager_spec.js +++ b/spec/javascripts/pager_spec.js @@ -1,6 +1,6 @@ /* global fixture */ -require('~/pager'); +import '~/pager'; describe('pager', () => { const Pager = window.Pager; diff --git a/spec/javascripts/pipeline_schedules/interval_pattern_input_spec.js b/spec/javascripts/pipeline_schedules/interval_pattern_input_spec.js index 08fa6ca9057..845b371d90c 100644 --- a/spec/javascripts/pipeline_schedules/interval_pattern_input_spec.js +++ b/spec/javascripts/pipeline_schedules/interval_pattern_input_spec.js @@ -36,20 +36,6 @@ describe('Interval Pattern Input Component', function () { expect(this.intervalPatternComponent.initialCronInterval).toBe(this.initialCronInterval); }); - it('sets showUnsetWarning to false', function (done) { - Vue.nextTick(() => { - expect(this.intervalPatternComponent.showUnsetWarning).toBe(false); - done(); - }); - }); - - it('does not render showUnsetWarning', function (done) { - Vue.nextTick(() => { - expect(this.intervalPatternComponent.$el.outerHTML).not.toContain('Schedule not yet set'); - done(); - }); - }); - it('sets isEditable to true', function (done) { Vue.nextTick(() => { expect(this.intervalPatternComponent.isEditable).toBe(true); @@ -72,20 +58,6 @@ describe('Interval Pattern Input Component', function () { expect(this.intervalPatternComponent).toBeDefined(); }); - it('sets showUnsetWarning to false', function (done) { - Vue.nextTick(() => { - expect(this.intervalPatternComponent.showUnsetWarning).toBe(false); - done(); - }); - }); - - it('does not render showUnsetWarning', function (done) { - Vue.nextTick(() => { - expect(this.intervalPatternComponent.$el.outerHTML).not.toContain('Schedule not yet set'); - done(); - }); - }); - it('sets isEditable to false', function (done) { Vue.nextTick(() => { expect(this.intervalPatternComponent.isEditable).toBe(false); @@ -113,20 +85,6 @@ describe('Interval Pattern Input Component', function () { expect(this.intervalPatternComponent.initialCronInterval).toBe(defaultInitialCronInterval); }); - it('sets showUnsetWarning to true', function (done) { - Vue.nextTick(() => { - expect(this.intervalPatternComponent.showUnsetWarning).toBe(true); - done(); - }); - }); - - it('renders showUnsetWarning to true', function (done) { - Vue.nextTick(() => { - expect(this.intervalPatternComponent.$el.outerHTML).toContain('Schedule not yet set'); - done(); - }); - }); - it('sets isEditable to true', function (done) { Vue.nextTick(() => { expect(this.intervalPatternComponent.isEditable).toBe(true); diff --git a/spec/javascripts/pipelines/pipeline_url_spec.js b/spec/javascripts/pipelines/pipeline_url_spec.js index 53931d67ad7..0bcc3905702 100644 --- a/spec/javascripts/pipelines/pipeline_url_spec.js +++ b/spec/javascripts/pipelines/pipeline_url_spec.js @@ -60,7 +60,7 @@ describe('Pipeline Url Component', () => { expect( component.$el.querySelector('.js-pipeline-url-user').getAttribute('href'), ).toEqual(mockData.pipeline.user.web_url); - expect(image.getAttribute('title')).toEqual(mockData.pipeline.user.name); + expect(image.getAttribute('data-original-title')).toEqual(mockData.pipeline.user.name); expect(image.getAttribute('src')).toEqual(mockData.pipeline.user.avatar_url); }); diff --git a/spec/javascripts/pretty_time_spec.js b/spec/javascripts/pretty_time_spec.js index a4662cfb557..de99e7e3894 100644 --- a/spec/javascripts/pretty_time_spec.js +++ b/spec/javascripts/pretty_time_spec.js @@ -1,4 +1,4 @@ -require('~/lib/utils/pretty_time'); +import '~/lib/utils/pretty_time'; (() => { const prettyTime = gl.utils.prettyTime; diff --git a/spec/javascripts/project_title_spec.js b/spec/javascripts/project_title_spec.js index 5c51e855401..3dba2e817ff 100644 --- a/spec/javascripts/project_title_spec.js +++ b/spec/javascripts/project_title_spec.js @@ -1,11 +1,11 @@ /* eslint-disable space-before-function-paren, no-unused-expressions, no-return-assign, no-param-reassign, no-var, new-cap, wrap-iife, no-unused-vars, quotes, jasmine/no-expect-in-setup-teardown, max-len */ /* global Project */ -require('select2/select2.js'); -require('~/gl_dropdown'); -require('~/api'); -require('~/project_select'); -require('~/project'); +import 'select2/select2'; +import '~/gl_dropdown'; +import '~/api'; +import '~/project_select'; +import '~/project'; (function() { describe('Project Title', function() { diff --git a/spec/javascripts/raven/index_spec.js b/spec/javascripts/raven/index_spec.js index b5662cd0331..a503a54029f 100644 --- a/spec/javascripts/raven/index_spec.js +++ b/spec/javascripts/raven/index_spec.js @@ -2,25 +2,23 @@ import RavenConfig from '~/raven/raven_config'; import index from '~/raven/index'; describe('RavenConfig options', () => { - let sentryDsn; - let currentUserId; - let gitlabUrl; - let isProduction; + const sentryDsn = 'sentryDsn'; + const currentUserId = 'currentUserId'; + const gitlabUrl = 'gitlabUrl'; + const isProduction = 'isProduction'; + const revision = 'revision'; let indexReturnValue; beforeEach(() => { - sentryDsn = 'sentryDsn'; - currentUserId = 'currentUserId'; - gitlabUrl = 'gitlabUrl'; - isProduction = 'isProduction'; - window.gon = { sentry_dsn: sentryDsn, current_user_id: currentUserId, gitlab_url: gitlabUrl, + revision, }; process.env.NODE_ENV = isProduction; + process.env.HEAD_COMMIT_SHA = revision; spyOn(RavenConfig, 'init'); @@ -33,6 +31,10 @@ describe('RavenConfig options', () => { currentUserId, whitelistUrls: [gitlabUrl], isProduction, + release: revision, + tags: { + revision, + }, }); }); diff --git a/spec/javascripts/raven/raven_config_spec.js b/spec/javascripts/raven/raven_config_spec.js index a2d720760fc..c82658b9262 100644 --- a/spec/javascripts/raven/raven_config_spec.js +++ b/spec/javascripts/raven/raven_config_spec.js @@ -25,17 +25,11 @@ describe('RavenConfig', () => { }); describe('init', () => { - let options; + const options = { + currentUserId: 1, + }; beforeEach(() => { - options = { - sentryDsn: '//sentryDsn', - ravenAssetUrl: '//ravenAssetUrl', - currentUserId: 1, - whitelistUrls: ['//gitlabUrl'], - isProduction: true, - }; - spyOn(RavenConfig, 'configure'); spyOn(RavenConfig, 'bindRavenErrors'); spyOn(RavenConfig, 'setUser'); @@ -62,30 +56,28 @@ describe('RavenConfig', () => { it('should not call setUser if there is no current user ID', () => { RavenConfig.setUser.calls.reset(); - RavenConfig.init({ - sentryDsn: '//sentryDsn', - ravenAssetUrl: '//ravenAssetUrl', - currentUserId: undefined, - whitelistUrls: ['//gitlabUrl'], - isProduction: true, - }); + options.currentUserId = undefined; + + RavenConfig.init(options); expect(RavenConfig.setUser).not.toHaveBeenCalled(); }); }); describe('configure', () => { - let options; let raven; let ravenConfig; + const options = { + sentryDsn: '//sentryDsn', + whitelistUrls: ['//gitlabUrl'], + isProduction: true, + release: 'revision', + tags: { + revision: 'revision', + }, + }; beforeEach(() => { - options = { - sentryDsn: '//sentryDsn', - whitelistUrls: ['//gitlabUrl'], - isProduction: true, - }; - ravenConfig = jasmine.createSpyObj('ravenConfig', ['shouldSendSample']); raven = jasmine.createSpyObj('raven', ['install']); @@ -100,6 +92,8 @@ describe('RavenConfig', () => { it('should call Raven.config', () => { expect(Raven.config).toHaveBeenCalledWith(options.sentryDsn, { + release: options.release, + tags: options.tags, whitelistUrls: options.whitelistUrls, environment: 'production', ignoreErrors: ravenConfig.IGNORE_ERRORS, @@ -118,6 +112,8 @@ describe('RavenConfig', () => { RavenConfig.configure.call(ravenConfig); expect(Raven.config).toHaveBeenCalledWith(options.sentryDsn, { + release: options.release, + tags: options.tags, whitelistUrls: options.whitelistUrls, environment: 'development', ignoreErrors: ravenConfig.IGNORE_ERRORS, @@ -144,24 +140,6 @@ describe('RavenConfig', () => { }); }); - describe('bindRavenErrors', () => { - let $document; - let $; - - beforeEach(() => { - $document = jasmine.createSpyObj('$document', ['on']); - $ = jasmine.createSpy('$').and.returnValue($document); - - window.$ = $; - - RavenConfig.bindRavenErrors(); - }); - - it('should call .on', function () { - expect($document.on).toHaveBeenCalledWith('ajaxError.raven', RavenConfig.handleRavenErrors); - }); - }); - describe('handleRavenErrors', () => { let event; let req; diff --git a/spec/javascripts/search_autocomplete_spec.js b/spec/javascripts/search_autocomplete_spec.js index fa52a8a0dd2..a53f58b5d0d 100644 --- a/spec/javascripts/search_autocomplete_spec.js +++ b/spec/javascripts/search_autocomplete_spec.js @@ -1,9 +1,9 @@ /* eslint-disable space-before-function-paren, max-len, no-var, one-var, one-var-declaration-per-line, no-unused-expressions, consistent-return, no-param-reassign, default-case, no-return-assign, comma-dangle, object-shorthand, prefer-template, quotes, new-parens, vars-on-top, new-cap, max-len */ -require('~/gl_dropdown'); -require('~/search_autocomplete'); -require('~/lib/utils/common_utils'); -require('vendor/fuzzaldrin-plus'); +import '~/gl_dropdown'; +import '~/search_autocomplete'; +import '~/lib/utils/common_utils'; +import 'vendor/fuzzaldrin-plus'; (function() { var addBodyAttributes, assertLinks, dashboardIssuesPath, dashboardMRsPath, groupIssuesPath, groupMRsPath, groupName, mockDashboardOptions, mockGroupOptions, mockProjectOptions, projectIssuesPath, projectMRsPath, projectName, userId, widget; diff --git a/spec/javascripts/shortcuts_issuable_spec.js b/spec/javascripts/shortcuts_issuable_spec.js index 757b8d595a4..3515dfbc60b 100644 --- a/spec/javascripts/shortcuts_issuable_spec.js +++ b/spec/javascripts/shortcuts_issuable_spec.js @@ -1,8 +1,8 @@ /* eslint-disable space-before-function-paren, no-return-assign, no-var, quotes */ /* global ShortcutsIssuable */ -require('~/copy_as_gfm'); -require('~/shortcuts_issuable'); +import '~/copy_as_gfm'; +import '~/shortcuts_issuable'; (function() { describe('ShortcutsIssuable', function() { diff --git a/spec/javascripts/sidebar/sidebar_assignees_spec.js b/spec/javascripts/sidebar/sidebar_assignees_spec.js index 865951b2ad7..929ba75e67d 100644 --- a/spec/javascripts/sidebar/sidebar_assignees_spec.js +++ b/spec/javascripts/sidebar/sidebar_assignees_spec.js @@ -43,4 +43,16 @@ describe('sidebar assignees', () => { expect(SidebarMediator.prototype.assignYourself).toHaveBeenCalled(); expect(this.mediator.store.assignees.length).toEqual(1); }); + + it('hides assignees until fetched', (done) => { + component = new SidebarAssigneeComponent().$mount(this.sidebarAssigneesEl); + const currentAssignee = this.sidebarAssigneesEl.querySelector('.value'); + expect(currentAssignee).toBe(null); + + component.store.isFetching.assignees = false; + Vue.nextTick(() => { + expect(component.$el.querySelector('.value')).toBeVisible(); + done(); + }); + }); }); diff --git a/spec/javascripts/sidebar/sidebar_store_spec.js b/spec/javascripts/sidebar/sidebar_store_spec.js index 29facf483b5..b3fa156eb64 100644 --- a/spec/javascripts/sidebar/sidebar_store_spec.js +++ b/spec/javascripts/sidebar/sidebar_store_spec.js @@ -35,6 +35,10 @@ describe('Sidebar store', () => { SidebarStore.singleton = null; }); + it('has default isFetching values', () => { + expect(this.store.isFetching.assignees).toBe(true); + }); + it('adds a new assignee', () => { this.store.addAssignee(assignee); expect(this.store.assignees.length).toEqual(1); @@ -67,6 +71,7 @@ describe('Sidebar store', () => { }; this.store.setAssigneeData(users); + expect(this.store.isFetching.assignees).toBe(false); expect(this.store.assignees.length).toEqual(3); }); diff --git a/spec/javascripts/signin_tabs_memoizer_spec.js b/spec/javascripts/signin_tabs_memoizer_spec.js index 5b4f5933b34..0a32797c3e2 100644 --- a/spec/javascripts/signin_tabs_memoizer_spec.js +++ b/spec/javascripts/signin_tabs_memoizer_spec.js @@ -1,6 +1,6 @@ import AccessorUtilities from '~/lib/utils/accessor'; -require('~/signin_tabs_memoizer'); +import '~/signin_tabs_memoizer'; ((global) => { describe('SigninTabsMemoizer', () => { diff --git a/spec/javascripts/smart_interval_spec.js b/spec/javascripts/smart_interval_spec.js index 4366ec2a5b8..7833bf3fb04 100644 --- a/spec/javascripts/smart_interval_spec.js +++ b/spec/javascripts/smart_interval_spec.js @@ -1,4 +1,4 @@ -require('~/smart_interval'); +import '~/smart_interval'; (() => { const DEFAULT_MAX_INTERVAL = 100; diff --git a/spec/javascripts/syntax_highlight_spec.js b/spec/javascripts/syntax_highlight_spec.js index cea223bd243..946f98379ce 100644 --- a/spec/javascripts/syntax_highlight_spec.js +++ b/spec/javascripts/syntax_highlight_spec.js @@ -1,6 +1,6 @@ /* eslint-disable space-before-function-paren, no-var, no-return-assign, quotes */ -require('~/syntax_highlight'); +import '~/syntax_highlight'; (function() { describe('Syntax Highlighter', function() { diff --git a/spec/javascripts/test_bundle.js b/spec/javascripts/test_bundle.js index 0464b5d2329..13827a26571 100644 --- a/spec/javascripts/test_bundle.js +++ b/spec/javascripts/test_bundle.js @@ -1,13 +1,15 @@ -// enable test fixtures -require('jasmine-jquery'); +import $ from 'jquery'; +import _ from 'underscore'; +import 'jasmine-jquery'; +import '~/commons'; +// enable test fixtures jasmine.getFixtures().fixturesPath = '/base/spec/javascripts/fixtures'; jasmine.getJSONFixtures().fixturesPath = '/base/spec/javascripts/fixtures'; -// include common libraries -require('~/commons/index.js'); -window.$ = window.jQuery = require('jquery'); -window._ = require('underscore'); +// globalize common libraries +window.$ = window.jQuery = $; +window._ = _; // stub expected globals window.gl = window.gl || {}; diff --git a/spec/javascripts/todos_spec.js b/spec/javascripts/todos_spec.js index 66e4fbd6304..cd74aba4a4e 100644 --- a/spec/javascripts/todos_spec.js +++ b/spec/javascripts/todos_spec.js @@ -1,5 +1,5 @@ -require('~/todos'); -require('~/lib/utils/common_utils'); +import '~/todos'; +import '~/lib/utils/common_utils'; describe('Todos', () => { preloadFixtures('todos/todos.html.raw'); diff --git a/spec/javascripts/u2f/authenticate_spec.js b/spec/javascripts/u2f/authenticate_spec.js index af2d02b6b29..a160c86308d 100644 --- a/spec/javascripts/u2f/authenticate_spec.js +++ b/spec/javascripts/u2f/authenticate_spec.js @@ -2,11 +2,11 @@ /* global MockU2FDevice */ /* global U2FAuthenticate */ -require('~/u2f/authenticate'); -require('~/u2f/util'); -require('~/u2f/error'); -require('vendor/u2f'); -require('./mock_u2f_device'); +import '~/u2f/authenticate'; +import '~/u2f/util'; +import '~/u2f/error'; +import 'vendor/u2f'; +import './mock_u2f_device'; (function() { describe('U2FAuthenticate', function() { diff --git a/spec/javascripts/u2f/register_spec.js b/spec/javascripts/u2f/register_spec.js index 3960759f7cb..a445c80f2af 100644 --- a/spec/javascripts/u2f/register_spec.js +++ b/spec/javascripts/u2f/register_spec.js @@ -2,11 +2,11 @@ /* global MockU2FDevice */ /* global U2FRegister */ -require('~/u2f/register'); -require('~/u2f/util'); -require('~/u2f/error'); -require('vendor/u2f'); -require('./mock_u2f_device'); +import '~/u2f/register'; +import '~/u2f/util'; +import '~/u2f/error'; +import 'vendor/u2f'; +import './mock_u2f_device'; (function() { describe('U2FRegister', function() { diff --git a/spec/javascripts/version_check_image_spec.js b/spec/javascripts/version_check_image_spec.js index 83ffeca253a..9637bd0414a 100644 --- a/spec/javascripts/version_check_image_spec.js +++ b/spec/javascripts/version_check_image_spec.js @@ -1,6 +1,5 @@ -const ClassSpecHelper = require('./helpers/class_spec_helper'); -const VersionCheckImage = require('~/version_check_image'); -require('jquery'); +import VersionCheckImage from '~/version_check_image'; +import ClassSpecHelper from './helpers/class_spec_helper'; describe('VersionCheckImage', function () { describe('bindErrorEvent', function () { diff --git a/spec/javascripts/visibility_select_spec.js b/spec/javascripts/visibility_select_spec.js index b26ed41f27a..c2eaea7c2ed 100644 --- a/spec/javascripts/visibility_select_spec.js +++ b/spec/javascripts/visibility_select_spec.js @@ -1,4 +1,4 @@ -require('~/visibility_select'); +import '~/visibility_select'; (() => { const VisibilitySelect = gl.VisibilitySelect; diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_nothing_to_merge_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_nothing_to_merge_spec.js index d40c67b189d..a8a02fa6b66 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_nothing_to_merge_spec.js +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_nothing_to_merge_spec.js @@ -4,14 +4,26 @@ import nothingToMergeComponent from '~/vue_merge_request_widget/components/state describe('MRWidgetNothingToMerge', () => { describe('template', () => { const Component = Vue.extend(nothingToMergeComponent); + const newBlobPath = '/foo'; const vm = new Component({ el: document.createElement('div'), + propsData: { + mr: { newBlobPath }, + }, }); + it('should have correct elements', () => { expect(vm.$el.classList.contains('mr-widget-body')).toBeTruthy(); - expect(vm.$el.querySelector('button').getAttribute('disabled')).toBeTruthy(); - expect(vm.$el.innerText).toContain('There is nothing to merge from source branch into target branch.'); + expect(vm.$el.querySelector('a').href).toContain(newBlobPath); + expect(vm.$el.innerText).toContain('Currently there are no changes in this merge request\'s source branch'); expect(vm.$el.innerText).toContain('Please push new commits or use a different branch.'); }); + + it('should not show new blob link if there is no link available', () => { + vm.mr.newBlobPath = null; + Vue.nextTick(() => { + expect(vm.$el.querySelector('a')).toEqual(null); + }); + }); }); }); diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js index 74df99415c9..d043ad38b8b 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js @@ -80,7 +80,7 @@ describe('MRWidgetReadyToMerge', () => { }); describe('mergeButtonClass', () => { - const defaultClass = 'btn btn-success accept-merge-request'; + const defaultClass = 'btn btn-small btn-success accept-merge-request'; const failedClass = `${defaultClass} btn-danger`; const inActionClass = `${defaultClass} btn-info`; diff --git a/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js b/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js index 22ee7dcf0e7..bdc18243a15 100644 --- a/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js +++ b/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js @@ -227,13 +227,11 @@ describe('mrWidgetOptions', () => { describe('handleMounted', () => { it('should call required methods to do the initial kick-off', () => { - spyOn(vm, 'checkStatus'); spyOn(vm, 'initDeploymentsPolling'); spyOn(vm, 'setFavicon'); vm.handleMounted(); - expect(vm.checkStatus).toHaveBeenCalled(); expect(vm.setFavicon).toHaveBeenCalled(); expect(vm.initDeploymentsPolling).toHaveBeenCalled(); }); diff --git a/spec/javascripts/vue_mr_widget/stores/get_state_key_spec.js b/spec/javascripts/vue_mr_widget/stores/get_state_key_spec.js index 9a331d99865..179e42a7cc4 100644 --- a/spec/javascripts/vue_mr_widget/stores/get_state_key_spec.js +++ b/spec/javascripts/vue_mr_widget/stores/get_state_key_spec.js @@ -25,6 +25,12 @@ describe('getStateKey', () => { context.canBeMerged = true; expect(bound()).toEqual('readyToMerge'); + context.canMerge = false; + expect(bound()).toEqual('notAllowedToMerge'); + + context.mergeWhenPipelineSucceeds = true; + expect(bound()).toEqual('mergeWhenPipelineSucceeds'); + context.hasSHAChanged = true; expect(bound()).toEqual('shaMismatch'); @@ -38,12 +44,6 @@ describe('getStateKey', () => { context.isPipelineFailed = true; expect(bound()).toEqual('pipelineFailed'); - context.canMerge = false; - expect(bound()).toEqual('notAllowedToMerge'); - - context.mergeWhenPipelineSucceeds = true; - expect(bound()).toEqual('mergeWhenPipelineSucceeds'); - data.work_in_progress = true; expect(bound()).toEqual('workInProgress'); diff --git a/spec/javascripts/vue_shared/components/commit_spec.js b/spec/javascripts/vue_shared/components/commit_spec.js index 242010ba688..0638483e7aa 100644 --- a/spec/javascripts/vue_shared/components/commit_spec.js +++ b/spec/javascripts/vue_shared/components/commit_spec.js @@ -86,7 +86,7 @@ describe('Commit component', () => { it('Should render the author avatar with title and alt attributes', () => { expect( - component.$el.querySelector('.commit-title .avatar-image-container img').getAttribute('title'), + component.$el.querySelector('.commit-title .avatar-image-container img').getAttribute('data-original-title'), ).toContain(props.author.username); expect( component.$el.querySelector('.commit-title .avatar-image-container img').getAttribute('alt'), diff --git a/spec/javascripts/vue_shared/components/pipelines_table_row_spec.js b/spec/javascripts/vue_shared/components/pipelines_table_row_spec.js index 14280751053..286118917e8 100644 --- a/spec/javascripts/vue_shared/components/pipelines_table_row_spec.js +++ b/spec/javascripts/vue_shared/components/pipelines_table_row_spec.js @@ -79,7 +79,7 @@ describe('Pipelines Table Row', () => { ).toEqual(pipeline.user.web_url); expect( - component.$el.querySelector('td:nth-child(2) img').getAttribute('title'), + component.$el.querySelector('td:nth-child(2) img').getAttribute('data-original-title'), ).toEqual(pipeline.user.name); }); }); @@ -102,7 +102,7 @@ describe('Pipelines Table Row', () => { } const commitAuthorLink = commitAuthorElement.getAttribute('href'); - const commitAuthorName = commitAuthorElement.querySelector('img.avatar').getAttribute('title'); + const commitAuthorName = commitAuthorElement.querySelector('img.avatar').getAttribute('data-original-title'); return { commitAuthorElement, commitAuthorLink, commitAuthorName }; }; diff --git a/spec/javascripts/vue_shared/components/user_avatar_image_spec.js b/spec/javascripts/vue_shared/components/user_avatar_image_spec.js new file mode 100644 index 00000000000..8daa7610274 --- /dev/null +++ b/spec/javascripts/vue_shared/components/user_avatar_image_spec.js @@ -0,0 +1,54 @@ +import Vue from 'vue'; +import UserAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue'; + +const UserAvatarImageComponent = Vue.extend(UserAvatarImage); + +describe('User Avatar Image Component', function () { + describe('Initialization', function () { + beforeEach(function () { + this.propsData = { + size: 99, + imgSrc: 'myavatarurl.com', + imgAlt: 'mydisplayname', + cssClasses: 'myextraavatarclass', + tooltipText: 'tooltip text', + tooltipPlacement: 'bottom', + }; + + this.userAvatarImage = new UserAvatarImageComponent({ + propsData: this.propsData, + }).$mount(); + }); + + it('should return a defined Vue component', function () { + expect(this.userAvatarImage).toBeDefined(); + }); + + it('should have <img> as a child element', function () { + expect(this.userAvatarImage.$el.tagName).toBe('IMG'); + }); + + it('should properly compute tooltipContainer', function () { + expect(this.userAvatarImage.tooltipContainer).toBe('body'); + }); + + it('should properly render tooltipContainer', function () { + expect(this.userAvatarImage.$el.getAttribute('data-container')).toBe('body'); + }); + + it('should properly compute avatarSizeClass', function () { + expect(this.userAvatarImage.avatarSizeClass).toBe('s99'); + }); + + it('should properly render img css', function () { + const classList = this.userAvatarImage.$el.classList; + const containsAvatar = classList.contains('avatar'); + const containsSizeClass = classList.contains('s99'); + const containsCustomClass = classList.contains('myextraavatarclass'); + + expect(containsAvatar).toBe(true); + expect(containsSizeClass).toBe(true); + expect(containsCustomClass).toBe(true); + }); + }); +}); diff --git a/spec/javascripts/vue_shared/components/user_avatar_link_spec.js b/spec/javascripts/vue_shared/components/user_avatar_link_spec.js new file mode 100644 index 00000000000..52e450e9ba5 --- /dev/null +++ b/spec/javascripts/vue_shared/components/user_avatar_link_spec.js @@ -0,0 +1,50 @@ +import Vue from 'vue'; +import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue'; + +describe('User Avatar Link Component', function () { + beforeEach(function () { + this.propsData = { + linkHref: 'myavatarurl.com', + imgSize: 99, + imgSrc: 'myavatarurl.com', + imgAlt: 'mydisplayname', + imgCssClasses: 'myextraavatarclass', + tooltipText: 'tooltip text', + tooltipPlacement: 'bottom', + }; + + const UserAvatarLinkComponent = Vue.extend(UserAvatarLink); + + this.userAvatarLink = new UserAvatarLinkComponent({ + propsData: this.propsData, + }).$mount(); + + this.userAvatarImage = this.userAvatarLink.$children[0]; + }); + + it('should return a defined Vue component', function () { + expect(this.userAvatarLink).toBeDefined(); + }); + + it('should have user-avatar-image registered as child component', function () { + expect(this.userAvatarLink.$options.components.userAvatarImage).toBeDefined(); + }); + + it('user-avatar-link should have user-avatar-image as child component', function () { + expect(this.userAvatarImage).toBeDefined(); + }); + + it('should render <a> as a child element', function () { + expect(this.userAvatarLink.$el.tagName).toBe('A'); + }); + + it('should have <img> as a child element', function () { + expect(this.userAvatarLink.$el.querySelector('img')).not.toBeNull(); + }); + + it('should return neccessary props as defined', function () { + _.each(this.propsData, (val, key) => { + expect(this.userAvatarLink[key]).toBeDefined(); + }); + }); +}); diff --git a/spec/javascripts/vue_shared/components/user_avatar_svg_spec.js b/spec/javascripts/vue_shared/components/user_avatar_svg_spec.js new file mode 100644 index 00000000000..b8d639ffbec --- /dev/null +++ b/spec/javascripts/vue_shared/components/user_avatar_svg_spec.js @@ -0,0 +1,29 @@ +import Vue from 'vue'; +import UserAvatarSvg from '~/vue_shared/components/user_avatar/user_avatar_svg.vue'; +import avatarSvg from 'icons/_icon_random.svg'; + +const UserAvatarSvgComponent = Vue.extend(UserAvatarSvg); + +describe('User Avatar Svg Component', function () { + describe('Initialization', function () { + beforeEach(function () { + this.propsData = { + size: 99, + svg: avatarSvg, + }; + + this.userAvatarSvg = new UserAvatarSvgComponent({ + propsData: this.propsData, + }).$mount(); + }); + + it('should return a defined Vue component', function () { + expect(this.userAvatarSvg).toBeDefined(); + }); + + it('should have <svg> as a child element', function () { + expect(this.userAvatarSvg.$el.tagName).toEqual('svg'); + expect(this.userAvatarSvg.$el.innerHTML).toContain('<path'); + }); + }); +}); diff --git a/spec/javascripts/zen_mode_spec.js b/spec/javascripts/zen_mode_spec.js index 99515f2e5f2..4399c8b2025 100644 --- a/spec/javascripts/zen_mode_spec.js +++ b/spec/javascripts/zen_mode_spec.js @@ -3,7 +3,7 @@ /* global Mousetrap */ /* global ZenMode */ -require('~/zen_mode'); +import '~/zen_mode'; (function() { var enterZen, escapeKeydown, exitZen; diff --git a/spec/lib/banzai/filter/ascii_doc_post_processing_filter_spec.rb b/spec/lib/banzai/filter/ascii_doc_post_processing_filter_spec.rb new file mode 100644 index 00000000000..33b812ef425 --- /dev/null +++ b/spec/lib/banzai/filter/ascii_doc_post_processing_filter_spec.rb @@ -0,0 +1,15 @@ +require 'spec_helper' + +describe Banzai::Filter::AsciiDocPostProcessingFilter, lib: true do + include FilterSpecHelper + + it "adds class for elements with data-math-style" do + result = filter('<pre data-math-style="inline">some code</pre><div data-math>and</div>').to_html + expect(result).to eq('<pre data-math-style="inline" class="code math js-render-math">some code</pre><div data-math>and</div>') + end + + it "keeps content when no data-math-style found" do + result = filter('<pre>some code</pre><div data-math>and</div>').to_html + expect(result).to eq('<pre>some code</pre><div data-math>and</div>') + end +end diff --git a/spec/lib/banzai/filter/external_link_filter_spec.rb b/spec/lib/banzai/filter/external_link_filter_spec.rb index 6f6c215be87..0f8ec8de7a0 100644 --- a/spec/lib/banzai/filter/external_link_filter_spec.rb +++ b/spec/lib/banzai/filter/external_link_filter_spec.rb @@ -55,6 +55,13 @@ describe Banzai::Filter::ExternalLinkFilter, lib: true do expect(doc.to_html).to eq(expected) end + + it 'skips improperly formatted mailtos' do + doc = filter %q(<p><a href="mailto://jblogs@example.com">Email</a></p>) + expected = %q(<p><a href="mailto://jblogs@example.com">Email</a></p>) + + expect(doc.to_html).to eq(expected) + end end context 'for links with a username' do diff --git a/spec/lib/banzai/filter/sanitization_filter_spec.rb b/spec/lib/banzai/filter/sanitization_filter_spec.rb index fdbc65b5e00..fb7862f49a2 100644 --- a/spec/lib/banzai/filter/sanitization_filter_spec.rb +++ b/spec/lib/banzai/filter/sanitization_filter_spec.rb @@ -97,6 +97,22 @@ describe Banzai::Filter::SanitizationFilter, lib: true do expect(filter(act).to_html).to eq exp end + it 'allows `data-math-style` attribute on `code` and `pre` elements' do + html = <<-HTML + <pre class="code" data-math-style="inline">something</pre> + <code class="code" data-math-style="inline">something</code> + <div class="code" data-math-style="inline">something</div> + HTML + + output = <<-HTML + <pre data-math-style="inline">something</pre> + <code data-math-style="inline">something</code> + <div>something</div> + HTML + + expect(filter(html).to_html).to eq(output) + end + it 'removes `rel` attribute from `a` elements' do act = %q{<a href="#" rel="nofollow">Link</a>} exp = %q{<a href="#">Link</a>} diff --git a/spec/lib/container_registry/blob_spec.rb b/spec/lib/container_registry/blob_spec.rb index f06e5fd54a2..ab010c6dfeb 100644 --- a/spec/lib/container_registry/blob_spec.rb +++ b/spec/lib/container_registry/blob_spec.rb @@ -98,7 +98,7 @@ describe ContainerRegistry::Blob do context 'for a valid address' do before do stub_request(:get, location). - with(headers: { 'Authorization' => nil }). + with { |request| !request.headers.include?('Authorization') }. to_return( status: 200, headers: { 'Content-Type' => 'application/json' }, diff --git a/spec/lib/container_registry/client_spec.rb b/spec/lib/container_registry/client_spec.rb new file mode 100644 index 00000000000..ec03b533383 --- /dev/null +++ b/spec/lib/container_registry/client_spec.rb @@ -0,0 +1,39 @@ +# coding: utf-8 +require 'spec_helper' + +describe ContainerRegistry::Client do + let(:token) { '12345' } + let(:options) { { token: token } } + let(:client) { described_class.new("http://container-registry", options) } + + describe '#blob' do + it 'GET /v2/:name/blobs/:digest' do + stub_request(:get, "http://container-registry/v2/group/test/blobs/sha256:0123456789012345"). + with(headers: { + 'Accept' => 'application/octet-stream', + 'Authorization' => "bearer #{token}" + }). + to_return(status: 200, body: "Blob") + + expect(client.blob('group/test', 'sha256:0123456789012345')).to eq('Blob') + end + + it 'follows 307 redirect for GET /v2/:name/blobs/:digest' do + stub_request(:get, "http://container-registry/v2/group/test/blobs/sha256:0123456789012345"). + with(headers: { + 'Accept' => 'application/octet-stream', + 'Authorization' => "bearer #{token}" + }). + to_return(status: 307, body: "", headers: { Location: 'http://redirected' }) + # We should probably use hash_excluding here, but that requires an update to WebMock: + # https://github.com/bblimke/webmock/blob/master/lib/webmock/matchers/hash_excluding_matcher.rb + stub_request(:get, "http://redirected/"). + with { |request| !request.headers.include?('Authorization') }. + to_return(status: 200, body: "Successfully redirected") + + response = client.blob('group/test', 'sha256:0123456789012345') + + expect(response).to eq('Successfully redirected') + end + end +end diff --git a/spec/lib/gitlab/asciidoc_spec.rb b/spec/lib/gitlab/asciidoc_spec.rb index 2c7ebb15fd7..43d52b941ab 100644 --- a/spec/lib/gitlab/asciidoc_spec.rb +++ b/spec/lib/gitlab/asciidoc_spec.rb @@ -70,6 +70,31 @@ module Gitlab expect(output).to include('rel="nofollow noreferrer noopener"') end end + + context 'LaTex code' do + it 'adds class js-render-math to the output' do + input = <<~MD + :stem: latexmath + + [stem] + ++++ + \sqrt{4} = 2 + ++++ + + another part + + [latexmath] + ++++ + \beta_x \gamma + ++++ + + stem:[2+2] is 4 + MD + + expect(render(input, context)).to include('<pre data-math-style="display" class="code math js-render-math"><code>eta_x gamma</code></pre>') + expect(render(input, context)).to include('<p><code data-math-style="inline" class="code math js-render-math">2+2</code> is 4</p>') + end + end end def render(*args) diff --git a/spec/lib/gitlab/backup/manager_spec.rb b/spec/lib/gitlab/backup/manager_spec.rb index c59ff7fb290..1c3d2547fec 100644 --- a/spec/lib/gitlab/backup/manager_spec.rb +++ b/spec/lib/gitlab/backup/manager_spec.rb @@ -24,8 +24,9 @@ describe Backup::Manager, lib: true do describe '#remove_old' do let(:files) do [ - '1451606400_2016_01_01_gitlab_backup.tar', - '1451520000_2015_12_31_gitlab_backup.tar', + '1451606400_2016_01_01_1.2.3_gitlab_backup.tar', + '1451520000_2015_12_31_4.5.6_gitlab_backup.tar', + '1451510000_2015_12_30_gitlab_backup.tar', '1450742400_2015_12_22_gitlab_backup.tar', '1449878400_gitlab_backup.tar', '1449014400_gitlab_backup.tar', @@ -58,6 +59,7 @@ describe Backup::Manager, lib: true do context 'when there are no files older than keep_time' do before do + # Set to 30 days allow(Gitlab.config.backup).to receive(:keep_time).and_return(2592000) subject.remove_old @@ -74,19 +76,24 @@ describe Backup::Manager, lib: true do context 'when keep_time is set to remove files' do before do + # Set to 1 second allow(Gitlab.config.backup).to receive(:keep_time).and_return(1) subject.remove_old end - it 'removes matching files with a human-readable timestamp' do + it 'removes matching files with a human-readable versioned timestamp' do expect(FileUtils).to have_received(:rm).with(files[1]) + end + + it 'removes matching files with a human-readable non-versioned timestamp' do expect(FileUtils).to have_received(:rm).with(files[2]) + expect(FileUtils).to have_received(:rm).with(files[3]) end it 'removes matching files without a human-readable timestamp' do - expect(FileUtils).to have_received(:rm).with(files[3]) expect(FileUtils).to have_received(:rm).with(files[4]) + expect(FileUtils).to have_received(:rm).with(files[5]) end it 'does not remove files that are not old enough' do @@ -94,11 +101,11 @@ describe Backup::Manager, lib: true do end it 'does not remove non-matching files' do - expect(FileUtils).not_to have_received(:rm).with(files[5]) + expect(FileUtils).not_to have_received(:rm).with(files[6]) end it 'prints a done message' do - expect(progress).to have_received(:puts).with('done. (4 removed)') + expect(progress).to have_received(:puts).with('done. (5 removed)') end end @@ -117,10 +124,11 @@ describe Backup::Manager, lib: true do expect(FileUtils).to have_received(:rm).with(files[2]) expect(FileUtils).to have_received(:rm).with(files[3]) expect(FileUtils).to have_received(:rm).with(files[4]) + expect(FileUtils).to have_received(:rm).with(files[5]) end it 'sets the correct removed count' do - expect(progress).to have_received(:puts).with('done. (3 removed)') + expect(progress).to have_received(:puts).with('done. (4 removed)') end it 'prints the error from file that could not be removed' do @@ -150,7 +158,7 @@ describe Backup::Manager, lib: true do before do allow(Dir).to receive(:glob).and_return( [ - '1451606400_2016_01_01_gitlab_backup.tar', + '1451606400_2016_01_01_1.2.3_gitlab_backup.tar', '1451520000_2015_12_31_gitlab_backup.tar' ] ) @@ -187,21 +195,21 @@ describe Backup::Manager, lib: true do before do allow(Dir).to receive(:glob).and_return( [ - '1451606400_2016_01_01_gitlab_backup.tar' + '1451606400_2016_01_01_1.2.3_gitlab_backup.tar' ] ) allow(File).to receive(:exist?).and_return(true) allow(Kernel).to receive(:system).and_return(true) allow(YAML).to receive(:load_file).and_return(gitlab_version: Gitlab::VERSION) - stub_env('BACKUP', '1451606400_2016_01_01') + stub_env('BACKUP', '1451606400_2016_01_01_1.2.3') end it 'unpacks the file' do subject.unpack expect(Kernel).to have_received(:system) - .with("tar", "-xf", "1451606400_2016_01_01_gitlab_backup.tar") + .with("tar", "-xf", "1451606400_2016_01_01_1.2.3_gitlab_backup.tar") expect(progress).to have_received(:puts).with(a_string_matching('done')) end end diff --git a/spec/lib/gitlab/cycle_analytics/events_spec.rb b/spec/lib/gitlab/cycle_analytics/events_spec.rb index 3610a0354e8..a1b3fe8509e 100644 --- a/spec/lib/gitlab/cycle_analytics/events_spec.rb +++ b/spec/lib/gitlab/cycle_analytics/events_spec.rb @@ -126,12 +126,11 @@ describe 'cycle analytics events' do create(:ci_pipeline, ref: merge_request.source_branch, sha: merge_request.diff_head_sha, - project: context.project) + project: context.project, + head_pipeline_of: merge_request) end before do - merge_request.update(head_pipeline: pipeline) - create(:ci_build, pipeline: pipeline, status: :success, author: user) create(:ci_build, pipeline: pipeline, status: :success, author: user) @@ -224,12 +223,11 @@ describe 'cycle analytics events' do create(:ci_pipeline, ref: merge_request.source_branch, sha: merge_request.diff_head_sha, - project: context.project) + project: context.project, + head_pipeline_of: merge_request) end before do - merge_request.update(head_pipeline: pipeline) - create(:ci_build, pipeline: pipeline, status: :success, author: user) create(:ci_build, pipeline: pipeline, status: :success, author: user) diff --git a/spec/lib/gitlab/database/migration_helpers_spec.rb b/spec/lib/gitlab/database/migration_helpers_spec.rb index dfa3ae9142e..bd5ac6142be 100644 --- a/spec/lib/gitlab/database/migration_helpers_spec.rb +++ b/spec/lib/gitlab/database/migration_helpers_spec.rb @@ -247,6 +247,14 @@ describe Gitlab::Database::MigrationHelpers, lib: true do expect(Project.where(archived: true).count).to eq(1) end end + + context 'when the value is Arel.sql (Arel::Nodes::SqlLiteral)' do + it 'updates the value as a SQL expression' do + model.update_column_in_batches(:projects, :star_count, Arel.sql('1+1')) + + expect(Project.sum(:star_count)).to eq(2 * Project.count) + end + end end describe '#add_column_with_default' do diff --git a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base_spec.rb b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base_spec.rb index 64bc5fc0429..a3ab4e3dd9e 100644 --- a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base_spec.rb +++ b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base_spec.rb @@ -107,6 +107,15 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameBase do expect(new_path).to eq('the-path0') end + it "doesn't rename routes that start with a similar name" do + other_namespace = create(:namespace, path: 'the-path-but-not-really') + project = create(:empty_project, path: 'the-project', namespace: other_namespace) + + subject.rename_path_for_routable(migration_namespace(namespace)) + + expect(project.route.reload.path).to eq('the-path-but-not-really/the-project') + end + context "the-path namespace -> subgroup -> the-path0 project" do it "updates the route of the project correctly" do subgroup = create(:group, path: "subgroup", parent: namespace) diff --git a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces_spec.rb b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces_spec.rb index ec444942804..c56fded7516 100644 --- a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces_spec.rb +++ b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces_spec.rb @@ -137,7 +137,7 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameNamespaces do end describe "#rename_namespace" do - let(:namespace) { create(:namespace, path: 'the-path') } + let(:namespace) { create(:group, name: 'the-path') } it 'renames paths & routes for the namespace' do expect(subject).to receive(:rename_path_for_routable). @@ -177,6 +177,31 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameNamespaces do subject.rename_namespace(namespace) end + + it "doesn't rename users for other namespaces" do + expect(subject).not_to receive(:rename_user) + + subject.rename_namespace(namespace) + end + + it 'renames the username of a namespace for a user' do + user = create(:user, username: 'the-path') + + expect(subject).to receive(:rename_user).with('the-path', 'the-path0') + + subject.rename_namespace(user.namespace) + end + end + + describe '#rename_user' do + it 'renames a username' do + subject = described_class.new([], migration) + user = create(:user, username: 'broken') + + subject.rename_user('broken', 'broken0') + + expect(user.reload.username).to eq('broken0') + end end describe '#rename_namespaces' do diff --git a/spec/lib/gitlab/diff/position_tracer_spec.rb b/spec/lib/gitlab/diff/position_tracer_spec.rb index 4d202a76e1b..93d30b90937 100644 --- a/spec/lib/gitlab/diff/position_tracer_spec.rb +++ b/spec/lib/gitlab/diff/position_tracer_spec.rb @@ -61,9 +61,10 @@ describe Gitlab::Diff::PositionTracer, lib: true do let(:old_diff_refs) { raise NotImplementedError } let(:new_diff_refs) { raise NotImplementedError } + let(:change_diff_refs) { raise NotImplementedError } let(:old_position) { raise NotImplementedError } - let(:position_tracer) { described_class.new(repository: project.repository, old_diff_refs: old_diff_refs, new_diff_refs: new_diff_refs) } + let(:position_tracer) { described_class.new(project: project, old_diff_refs: old_diff_refs, new_diff_refs: new_diff_refs) } subject { position_tracer.trace(old_position) } def diff_refs(base_commit, head_commit) @@ -77,16 +78,40 @@ describe Gitlab::Diff::PositionTracer, lib: true do Gitlab::Diff::Position.new(attrs) end - def expect_new_position(attrs, new_position = subject) - if attrs.nil? - expect(new_position).to be_nil - else - expect(new_position).not_to be_nil + def expect_new_position(attrs, result = subject) + aggregate_failures("expect new position #{attrs.inspect}") do + if attrs.nil? + expect(result[:outdated]).to be_truthy + else + expect(result[:outdated]).to be_falsey - expect(new_position.diff_refs).to eq(new_diff_refs) + new_position = result[:position] + expect(new_position).not_to be_nil - attrs.each do |attr, value| - expect(new_position.send(attr)).to eq(value) + expect(new_position.diff_refs).to eq(new_diff_refs) + + attrs.each do |attr, value| + expect(new_position.send(attr)).to eq(value) + end + end + end + end + + def expect_change_position(attrs, result = subject) + aggregate_failures("expect change position #{attrs.inspect}") do + expect(result[:outdated]).to be_truthy + + change_position = result[:position] + if attrs.nil? || attrs.empty? + expect(change_position).to be_nil + else + expect(change_position).not_to be_nil + + expect(change_position.diff_refs).to eq(change_diff_refs) + + attrs.each do |attr, value| + expect(change_position.send(attr)).to eq(value) + end end end end @@ -395,6 +420,7 @@ describe Gitlab::Diff::PositionTracer, lib: true do context "when that line was changed between the old and the new diff" do let(:old_diff_refs) { diff_refs(initial_commit, create_file_commit) } let(:new_diff_refs) { diff_refs(initial_commit, update_line_commit) } + let(:change_diff_refs) { diff_refs(create_file_commit, update_line_commit) } let(:old_position) { position(new_path: file_name, new_line: 2) } # old diff: @@ -407,14 +433,20 @@ describe Gitlab::Diff::PositionTracer, lib: true do # 2 + BB # 3 + C - it "returns nil" do - expect(subject).to be_nil + it "returns the position of the change" do + expect_change_position( + old_path: file_name, + new_path: file_name, + old_line: 2, + new_line: nil + ) end end context "when that line was deleted between the old and the new diff" do let(:old_diff_refs) { diff_refs(initial_commit, update_line_commit) } let(:new_diff_refs) { diff_refs(initial_commit, delete_line_commit) } + let(:change_diff_refs) { diff_refs(update_line_commit, delete_line_commit) } let(:old_position) { position(new_path: file_name, new_line: 3) } # old diff: @@ -426,8 +458,13 @@ describe Gitlab::Diff::PositionTracer, lib: true do # 1 + A # 2 + BB - it "returns nil" do - expect(subject).to be_nil + it "returns the position of the change" do + expect_change_position( + old_path: file_name, + new_path: file_name, + old_line: 3, + new_line: nil + ) end end end @@ -512,6 +549,7 @@ describe Gitlab::Diff::PositionTracer, lib: true do context "when that line was changed between the old and the new diff" do let(:old_diff_refs) { diff_refs(initial_commit, create_file_commit) } let(:new_diff_refs) { diff_refs(create_file_commit, update_line_commit) } + let(:change_diff_refs) { diff_refs(create_file_commit, update_line_commit) } let(:old_position) { position(new_path: file_name, new_line: 2) } # old diff: @@ -525,14 +563,20 @@ describe Gitlab::Diff::PositionTracer, lib: true do # 2 + BB # 3 3 C - it "returns nil" do - expect(subject).to be_nil + it "returns the position of the change" do + expect_change_position( + old_path: file_name, + new_path: file_name, + old_line: 2, + new_line: nil + ) end end context "when that line was deleted between the old and the new diff" do let(:old_diff_refs) { diff_refs(initial_commit, move_line_commit) } let(:new_diff_refs) { diff_refs(move_line_commit, delete_line_commit) } + let(:change_diff_refs) { diff_refs(move_line_commit, delete_line_commit) } let(:old_position) { position(new_path: file_name, new_line: 3) } # old diff: @@ -545,8 +589,13 @@ describe Gitlab::Diff::PositionTracer, lib: true do # 2 2 A # 3 - C - it "returns nil" do - expect(subject).to be_nil + it "returns the position of the change" do + expect_change_position( + old_path: file_name, + new_path: file_name, + old_line: 3, + new_line: nil + ) end end end @@ -558,6 +607,7 @@ describe Gitlab::Diff::PositionTracer, lib: true do context "when the file's content was unchanged between the old and the new diff" do let(:old_diff_refs) { diff_refs(initial_commit, delete_line_commit) } let(:new_diff_refs) { diff_refs(delete_line_commit, rename_file_commit) } + let(:change_diff_refs) { diff_refs(initial_commit, delete_line_commit) } let(:old_position) { position(new_path: file_name, new_line: 2) } # old diff: @@ -569,8 +619,13 @@ describe Gitlab::Diff::PositionTracer, lib: true do # 1 1 BB # 2 2 A - it "returns nil since the line doesn't exist in the new diffs anymore" do - expect(subject).to be_nil + it "returns the position of the change" do + expect_change_position( + old_path: file_name, + new_path: file_name, + old_line: nil, + new_line: 2 + ) end end @@ -628,6 +683,7 @@ describe Gitlab::Diff::PositionTracer, lib: true do context "when that line was changed between the old and the new diff" do let(:old_diff_refs) { diff_refs(initial_commit, delete_line_commit) } let(:new_diff_refs) { diff_refs(delete_line_commit, update_line_again_commit) } + let(:change_diff_refs) { diff_refs(delete_line_commit, update_line_again_commit) } let(:old_position) { position(new_path: file_name, new_line: 2) } # old diff: @@ -640,28 +696,13 @@ describe Gitlab::Diff::PositionTracer, lib: true do # 2 - A # 2 + AA - it "returns nil" do - expect(subject).to be_nil - end - end - - context "when that line was deleted between the old and the new diff" do - let(:old_diff_refs) { diff_refs(initial_commit, delete_line_commit) } - let(:new_diff_refs) { diff_refs(delete_line_commit, delete_line_again_commit) } - let(:old_position) { position(new_path: file_name, new_line: 1) } - - # old diff: - # 1 + BB - # 2 + A - # - # new diff: - # file_name -> new_file_name - # 1 - BB - # 2 - A - # 1 + AA - - it "returns nil" do - expect(subject).to be_nil + it "returns the position of the change" do + expect_change_position( + old_path: file_name, + new_path: new_file_name, + old_line: 2, + new_line: nil + ) end end end @@ -673,6 +714,7 @@ describe Gitlab::Diff::PositionTracer, lib: true do context "when the file's content was unchanged between the old and the new diff" do let(:old_diff_refs) { diff_refs(initial_commit, delete_line_commit) } let(:new_diff_refs) { diff_refs(delete_line_commit, delete_file_commit) } + let(:change_diff_refs) { diff_refs(delete_line_commit, delete_file_commit) } let(:old_position) { position(new_path: file_name, new_line: 2) } # old diff: @@ -683,8 +725,13 @@ describe Gitlab::Diff::PositionTracer, lib: true do # 1 - BB # 2 - A - it "returns nil" do - expect(subject).to be_nil + it "returns the position of the change" do + expect_change_position( + old_path: file_name, + new_path: file_name, + old_line: 2, + new_line: nil + ) end end @@ -692,6 +739,7 @@ describe Gitlab::Diff::PositionTracer, lib: true do context "when that line was unchanged between the old and the new diff" do let(:old_diff_refs) { diff_refs(initial_commit, move_line_commit) } let(:new_diff_refs) { diff_refs(delete_line_commit, delete_file_commit) } + let(:change_diff_refs) { diff_refs(move_line_commit, delete_file_commit) } let(:old_position) { position(new_path: file_name, new_line: 2) } # old diff: @@ -703,14 +751,20 @@ describe Gitlab::Diff::PositionTracer, lib: true do # 1 - BB # 2 - A - it "returns nil" do - expect(subject).to be_nil + it "returns the position of the change" do + expect_change_position( + old_path: file_name, + new_path: file_name, + old_line: 2, + new_line: nil + ) end end context "when that line was moved between the old and the new diff" do let(:old_diff_refs) { diff_refs(initial_commit, update_line_commit) } let(:new_diff_refs) { diff_refs(move_line_commit, delete_file_commit) } + let(:change_diff_refs) { diff_refs(update_line_commit, delete_file_commit) } let(:old_position) { position(new_path: file_name, new_line: 2) } # old diff: @@ -723,14 +777,20 @@ describe Gitlab::Diff::PositionTracer, lib: true do # 2 - A # 3 - C - it "returns nil" do - expect(subject).to be_nil + it "returns the position of the change" do + expect_change_position( + old_path: file_name, + new_path: file_name, + old_line: 2, + new_line: nil + ) end end context "when that line was changed between the old and the new diff" do let(:old_diff_refs) { diff_refs(initial_commit, create_file_commit) } let(:new_diff_refs) { diff_refs(update_line_commit, delete_file_commit) } + let(:change_diff_refs) { diff_refs(create_file_commit, delete_file_commit) } let(:old_position) { position(new_path: file_name, new_line: 2) } # old diff: @@ -743,14 +803,20 @@ describe Gitlab::Diff::PositionTracer, lib: true do # 2 - BB # 3 - C - it "returns nil" do - expect(subject).to be_nil + it "returns the position of the change" do + expect_change_position( + old_path: file_name, + new_path: file_name, + old_line: 2, + new_line: nil + ) end end context "when that line was deleted between the old and the new diff" do let(:old_diff_refs) { diff_refs(initial_commit, move_line_commit) } let(:new_diff_refs) { diff_refs(delete_line_commit, delete_file_commit) } + let(:change_diff_refs) { diff_refs(move_line_commit, delete_file_commit) } let(:old_position) { position(new_path: file_name, new_line: 3) } # old diff: @@ -762,8 +828,13 @@ describe Gitlab::Diff::PositionTracer, lib: true do # 1 - BB # 2 - A - it "returns nil" do - expect(subject).to be_nil + it "returns the position of the change" do + expect_change_position( + old_path: file_name, + new_path: file_name, + old_line: 3, + new_line: nil + ) end end end @@ -775,6 +846,7 @@ describe Gitlab::Diff::PositionTracer, lib: true do context "when the file's content was unchanged between the old and the new diff" do let(:old_diff_refs) { diff_refs(initial_commit, create_file_commit) } let(:new_diff_refs) { diff_refs(create_file_commit, create_second_file_commit) } + let(:change_diff_refs) { diff_refs(initial_commit, create_file_commit) } let(:old_position) { position(new_path: file_name, new_line: 2) } # old diff: @@ -787,8 +859,13 @@ describe Gitlab::Diff::PositionTracer, lib: true do # 2 2 B # 3 3 C - it "returns nil" do - expect(subject).to be_nil + it "returns the position of the change" do + expect_change_position( + old_path: file_name, + new_path: file_name, + old_line: nil, + new_line: 2 + ) end end @@ -796,6 +873,7 @@ describe Gitlab::Diff::PositionTracer, lib: true do context "when that line was unchanged between the old and the new diff" do let(:old_diff_refs) { diff_refs(initial_commit, create_file_commit) } let(:new_diff_refs) { diff_refs(update_line_commit, update_second_file_line_commit) } + let(:change_diff_refs) { diff_refs(initial_commit, update_line_commit) } let(:old_position) { position(new_path: file_name, new_line: 1) } # old diff: @@ -808,14 +886,20 @@ describe Gitlab::Diff::PositionTracer, lib: true do # 2 2 BB # 3 3 C - it "returns nil" do - expect(subject).to be_nil + it "returns the position of the change" do + expect_change_position( + old_path: file_name, + new_path: file_name, + old_line: nil, + new_line: 1 + ) end end context "when that line was moved between the old and the new diff" do let(:old_diff_refs) { diff_refs(initial_commit, update_line_commit) } let(:new_diff_refs) { diff_refs(move_line_commit, move_second_file_line_commit) } + let(:change_diff_refs) { diff_refs(initial_commit, move_line_commit) } let(:old_position) { position(new_path: file_name, new_line: 2) } # old diff: @@ -828,14 +912,20 @@ describe Gitlab::Diff::PositionTracer, lib: true do # 2 2 A # 3 3 C - it "returns nil" do - expect(subject).to be_nil + it "returns the position of the change" do + expect_change_position( + old_path: file_name, + new_path: file_name, + old_line: nil, + new_line: 1 + ) end end context "when that line was changed between the old and the new diff" do let(:old_diff_refs) { diff_refs(initial_commit, create_file_commit) } let(:new_diff_refs) { diff_refs(update_line_commit, update_second_file_line_commit) } + let(:change_diff_refs) { diff_refs(create_file_commit, update_second_file_line_commit) } let(:old_position) { position(new_path: file_name, new_line: 2) } # old diff: @@ -848,14 +938,20 @@ describe Gitlab::Diff::PositionTracer, lib: true do # 2 2 BB # 3 3 C - it "returns nil" do - expect(subject).to be_nil + it "returns the position of the change" do + expect_change_position( + old_path: file_name, + new_path: file_name, + old_line: 2, + new_line: nil + ) end end context "when that line was deleted between the old and the new diff" do let(:old_diff_refs) { diff_refs(initial_commit, move_line_commit) } let(:new_diff_refs) { diff_refs(delete_line_commit, delete_second_file_line_commit) } + let(:change_diff_refs) { diff_refs(move_line_commit, delete_second_file_line_commit) } let(:old_position) { position(new_path: file_name, new_line: 3) } # old diff: @@ -867,8 +963,13 @@ describe Gitlab::Diff::PositionTracer, lib: true do # 1 1 BB # 2 2 A - it "returns nil" do - expect(subject).to be_nil + it "returns the position of the change" do + expect_change_position( + old_path: file_name, + new_path: file_name, + old_line: 3, + new_line: nil + ) end end end @@ -957,6 +1058,7 @@ describe Gitlab::Diff::PositionTracer, lib: true do context "when that line was changed or deleted between the old and the new diff" do let(:old_diff_refs) { diff_refs(create_file_commit, move_line_commit) } let(:new_diff_refs) { diff_refs(initial_commit, create_file_commit) } + let(:change_diff_refs) { diff_refs(move_line_commit, create_file_commit) } let(:old_position) { position(old_path: file_name, new_path: file_name, new_line: 1) } # old diff: @@ -970,8 +1072,13 @@ describe Gitlab::Diff::PositionTracer, lib: true do # 2 + B # 3 + C - it "returns nil" do - expect(subject).to be_nil + it "returns the position of the change" do + expect_change_position( + old_path: file_name, + new_path: file_name, + old_line: 1, + new_line: nil + ) end end end @@ -980,6 +1087,7 @@ describe Gitlab::Diff::PositionTracer, lib: true do context "when the position pointed at a deleted line in the old diff" do let(:old_diff_refs) { diff_refs(create_file_commit, update_line_commit) } let(:new_diff_refs) { diff_refs(initial_commit, update_line_commit) } + let(:change_diff_refs) { diff_refs(create_file_commit, initial_commit) } let(:old_position) { position(old_path: file_name, new_path: file_name, old_line: 2) } # old diff: @@ -993,8 +1101,13 @@ describe Gitlab::Diff::PositionTracer, lib: true do # 2 + BB # 3 + C - it "returns nil" do - expect(subject).to be_nil + it "returns the position of the change" do + expect_change_position( + old_path: file_name, + new_path: file_name, + old_line: 2, + new_line: nil + ) end end @@ -1076,6 +1189,7 @@ describe Gitlab::Diff::PositionTracer, lib: true do context "when that line was changed or deleted between the old and the new diff" do let(:old_diff_refs) { diff_refs(create_file_commit, move_line_commit) } let(:new_diff_refs) { diff_refs(initial_commit, delete_line_commit) } + let(:change_diff_refs) { diff_refs(move_line_commit, delete_line_commit) } let(:old_position) { position(old_path: file_name, new_path: file_name, old_line: 3, new_line: 3) } # old diff: @@ -1088,8 +1202,13 @@ describe Gitlab::Diff::PositionTracer, lib: true do # 1 + A # 2 + B - it "returns nil" do - expect(subject).to be_nil + it "returns the position of the change" do + expect_change_position( + old_path: file_name, + new_path: file_name, + old_line: 3, + new_line: nil + ) end end end @@ -1182,6 +1301,7 @@ describe Gitlab::Diff::PositionTracer, lib: true do context "when that line was changed or deleted between the old and the new diff" do let(:old_diff_refs) { diff_refs(create_file_commit, move_line_commit) } let(:new_diff_refs) { diff_refs(create_file_commit, update_line_commit) } + let(:change_diff_refs) { diff_refs(move_line_commit, update_line_commit) } let(:old_position) { position(old_path: file_name, new_path: file_name, new_line: 1) } # old diff: @@ -1196,8 +1316,13 @@ describe Gitlab::Diff::PositionTracer, lib: true do # 2 + BB # 3 3 C - it "returns nil" do - expect(subject).to be_nil + it "returns the position of the change" do + expect_change_position( + old_path: file_name, + new_path: file_name, + old_line: 1, + new_line: nil + ) end end end @@ -1239,7 +1364,7 @@ describe Gitlab::Diff::PositionTracer, lib: true do describe "typical use scenarios" do let(:second_branch_name) { "#{branch_name}-2" } - def expect_positions(old_attrs, new_attrs) + def expect_new_positions(old_attrs, new_attrs) old_positions = old_attrs.map do |old_attrs| position(old_attrs) end @@ -1248,8 +1373,14 @@ describe Gitlab::Diff::PositionTracer, lib: true do position_tracer.trace(old_position) end - new_positions.zip(new_attrs).each do |new_position, new_attrs| - expect_new_position(new_attrs, new_position) + aggregate_failures do + new_positions.zip(new_attrs).each do |new_position, new_attrs| + if new_attrs&.delete(:change) + expect_change_position(new_attrs, new_position) + else + expect_new_position(new_attrs, new_position) + end + end end end @@ -1330,6 +1461,7 @@ describe Gitlab::Diff::PositionTracer, lib: true do describe "simple push of new commit" do let(:old_diff_refs) { diff_refs(create_file_commit, update_file_commit) } let(:new_diff_refs) { diff_refs(create_file_commit, update_file_again_commit) } + let(:change_diff_refs) { diff_refs(update_file_commit, update_file_again_commit) } # old diff: # 1 1 A @@ -1368,14 +1500,14 @@ describe Gitlab::Diff::PositionTracer, lib: true do { old_path: file_name, new_path: file_name, old_line: 1, new_line: 1 }, { old_path: file_name, old_line: 2 }, { old_path: file_name, new_path: file_name, old_line: 3, new_line: 3 }, - { old_path: file_name, old_line: 4, new_line: 4 }, - nil, + { new_path: file_name, new_line: 4, change: true }, + { new_path: file_name, old_line: 3, change: true }, { old_path: file_name, new_path: file_name, old_line: 5, new_line: 5 }, - { old_path: file_name, old_line: 6 }, + { new_path: file_name, old_line: 5, change: true }, { new_path: file_name, new_line: 7 } ] - expect_positions(old_position_attrs, new_position_attrs) + expect_new_positions(old_position_attrs, new_position_attrs) end end @@ -1402,6 +1534,7 @@ describe Gitlab::Diff::PositionTracer, lib: true do let(:old_diff_refs) { diff_refs(create_file_commit, update_file_commit) } let(:new_diff_refs) { diff_refs(create_file_commit, second_create_file_commit) } + let(:change_diff_refs) { diff_refs(update_file_commit, second_create_file_commit) } # old diff: # 1 1 A @@ -1440,20 +1573,21 @@ describe Gitlab::Diff::PositionTracer, lib: true do { old_path: file_name, new_path: file_name, old_line: 1, new_line: 1 }, { old_path: file_name, old_line: 2 }, { old_path: file_name, new_path: file_name, old_line: 3, new_line: 3 }, - { old_path: file_name, old_line: 4, new_line: 4 }, - nil, + { new_path: file_name, new_line: 4, change: true }, + { old_path: file_name, old_line: 3, change: true }, { old_path: file_name, new_path: file_name, old_line: 5, new_line: 5 }, - { old_path: file_name, old_line: 6 }, + { old_path: file_name, old_line: 5, change: true }, { new_path: file_name, new_line: 7 } ] - expect_positions(old_position_attrs, new_position_attrs) + expect_new_positions(old_position_attrs, new_position_attrs) end end describe "force push to delete last commit" do let(:old_diff_refs) { diff_refs(create_file_commit, update_file_again_commit) } let(:new_diff_refs) { diff_refs(create_file_commit, update_file_commit) } + let(:change_diff_refs) { diff_refs(update_file_again_commit, update_file_commit) } # old diff: # 1 1 A @@ -1492,16 +1626,16 @@ describe Gitlab::Diff::PositionTracer, lib: true do new_position_attrs = [ { old_path: file_name, new_path: file_name, old_line: 1, new_line: 1 }, { old_path: file_name, old_line: 2 }, - nil, + { old_path: file_name, old_line: 2, change: true }, { old_path: file_name, new_path: file_name, old_line: 3, new_line: 2 }, - { old_path: file_name, old_line: 4 }, + { old_path: file_name, old_line: 4, change: true }, { old_path: file_name, new_path: file_name, old_line: 5, new_line: 4 }, - { old_path: file_name, new_path: file_name, old_line: 6, new_line: 5 }, - nil, + { new_path: file_name, new_line: 5, change: true }, + { old_path: file_name, old_line: 6, change: true }, { new_path: file_name, new_line: 6 } ] - expect_positions(old_position_attrs, new_position_attrs) + expect_new_positions(old_position_attrs, new_position_attrs) end end @@ -1567,6 +1701,7 @@ describe Gitlab::Diff::PositionTracer, lib: true do let(:old_diff_refs) { diff_refs(create_file_commit, update_file_again_commit) } let(:new_diff_refs) { diff_refs(create_file_commit, overwrite_update_file_again_commit) } + let(:change_diff_refs) { diff_refs(update_file_again_commit, overwrite_update_file_again_commit) } # old diff: # 1 1 A @@ -1618,7 +1753,7 @@ describe Gitlab::Diff::PositionTracer, lib: true do { new_path: file_name, new_line: 10 }, # + G ] - expect_positions(old_position_attrs, new_position_attrs) + expect_new_positions(old_position_attrs, new_position_attrs) end end @@ -1643,6 +1778,7 @@ describe Gitlab::Diff::PositionTracer, lib: true do let(:old_diff_refs) { diff_refs(create_file_commit, update_file_again_commit) } let(:new_diff_refs) { diff_refs(create_file_commit, merge_commit) } + let(:change_diff_refs) { diff_refs(update_file_again_commit, merge_commit) } # old diff: # 1 1 A @@ -1694,13 +1830,14 @@ describe Gitlab::Diff::PositionTracer, lib: true do { new_path: file_name, new_line: 10 }, # + G ] - expect_positions(old_position_attrs, new_position_attrs) + expect_new_positions(old_position_attrs, new_position_attrs) end end describe "changing target branch" do let(:old_diff_refs) { diff_refs(create_file_commit, update_file_again_commit) } let(:new_diff_refs) { diff_refs(update_file_commit, update_file_again_commit) } + let(:change_diff_refs) { diff_refs(create_file_commit, update_file_commit) } # old diff: # 1 1 A @@ -1739,7 +1876,7 @@ describe Gitlab::Diff::PositionTracer, lib: true do new_position_attrs = [ { old_path: file_name, new_path: file_name, old_line: 1, new_line: 1 }, - nil, + { old_path: file_name, old_line: 2, change: true }, { new_path: file_name, new_line: 2 }, { old_path: file_name, new_path: file_name, old_line: 2, new_line: 3 }, { new_path: file_name, new_line: 4 }, @@ -1749,7 +1886,7 @@ describe Gitlab::Diff::PositionTracer, lib: true do { new_path: file_name, new_line: 7 } ] - expect_positions(old_position_attrs, new_position_attrs) + expect_new_positions(old_position_attrs, new_position_attrs) end end end diff --git a/spec/lib/gitlab/etag_caching/router_spec.rb b/spec/lib/gitlab/etag_caching/router_spec.rb index 5ae4a19263c..46a238b17f4 100644 --- a/spec/lib/gitlab/etag_caching/router_spec.rb +++ b/spec/lib/gitlab/etag_caching/router_spec.rb @@ -77,6 +77,17 @@ describe Gitlab::EtagCaching::Router do expect(result).to be_blank end + it 'matches pipeline#show endpoint' do + env = build_env( + '/my-group/my-project/pipelines/2.json' + ) + + result = described_class.match(env) + + expect(result).to be_present + expect(result.name).to eq 'project_pipeline' + end + def build_env(path) { 'PATH_INFO' => path } end diff --git a/spec/lib/gitlab/git/branch_spec.rb b/spec/lib/gitlab/git/branch_spec.rb index cdf1b8beee3..9eac7660cd1 100644 --- a/spec/lib/gitlab/git/branch_spec.rb +++ b/spec/lib/gitlab/git/branch_spec.rb @@ -7,6 +7,51 @@ describe Gitlab::Git::Branch, seed_helper: true do it { is_expected.to be_kind_of Array } + describe 'initialize' do + let(:commit_id) { 'f00' } + let(:commit_subject) { "My commit".force_encoding('ASCII-8BIT') } + let(:committer) do + Gitaly::FindLocalBranchCommitAuthor.new( + name: generate(:name), + email: generate(:email), + date: Google::Protobuf::Timestamp.new(seconds: 123) + ) + end + let(:author) do + Gitaly::FindLocalBranchCommitAuthor.new( + name: generate(:name), + email: generate(:email), + date: Google::Protobuf::Timestamp.new(seconds: 456) + ) + end + let(:gitaly_branch) do + Gitaly::FindLocalBranchResponse.new( + name: 'foo', commit_id: commit_id, commit_subject: commit_subject, + commit_author: author, commit_committer: committer + ) + end + let(:attributes) do + { + id: commit_id, + message: commit_subject, + authored_date: Time.at(author.date.seconds), + author_name: author.name, + author_email: author.email, + committed_date: Time.at(committer.date.seconds), + committer_name: committer.name, + committer_email: committer.email + } + end + let(:branch) { described_class.new(repository, 'foo', gitaly_branch) } + + it 'parses Gitaly::FindLocalBranchResponse correctly' do + expect(Gitlab::Git::Commit).to receive(:decorate). + with(hash_including(attributes)).and_call_original + + expect(branch.dereferenced_target.message.encoding).to be(Encoding::UTF_8) + end + end + describe '#size' do subject { super().size } it { is_expected.to eq(SeedRepo::Repo::BRANCHES.size) } diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index fea186fd4f4..cb107c6d1f9 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -26,6 +26,7 @@ describe Gitlab::Git::Repository, seed_helper: true do context 'with gitaly enabled' do before { stub_gitaly } + after { Gitlab::GitalyClient.clear_stubs! } it 'gets the branch name from GitalyClient' do expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:default_branch_name) @@ -120,6 +121,7 @@ describe Gitlab::Git::Repository, seed_helper: true do context 'with gitaly enabled' do before { stub_gitaly } + after { Gitlab::GitalyClient.clear_stubs! } it 'gets the branch names from GitalyClient' do expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:branch_names) @@ -157,6 +159,7 @@ describe Gitlab::Git::Repository, seed_helper: true do context 'with gitaly enabled' do before { stub_gitaly } + after { Gitlab::GitalyClient.clear_stubs! } it 'gets the tag names from GitalyClient' do expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:tag_names) @@ -1046,6 +1049,28 @@ describe Gitlab::Git::Repository, seed_helper: true do end end + describe '#ref_name_for_sha' do + let(:ref_path) { 'refs/heads' } + let(:sha) { repository.find_branch('master').dereferenced_target.id } + let(:ref_name) { 'refs/heads/master' } + + it 'returns the ref name for the given sha' do + expect(repository.ref_name_for_sha(ref_path, sha)).to eq(ref_name) + end + + it "returns an empty name if the ref doesn't exist" do + expect(repository.ref_name_for_sha(ref_path, "000000")).to eq("") + end + + it "raise an exception if the ref is empty" do + expect { repository.ref_name_for_sha(ref_path, "") }.to raise_error(ArgumentError) + end + + it "raise an exception if the ref is nil" do + expect { repository.ref_name_for_sha(ref_path, nil) }.to raise_error(ArgumentError) + end + end + describe '#find_commits' do it 'should return a return a collection of commits' do commits = repository.find_commits @@ -1080,7 +1105,9 @@ describe Gitlab::Git::Repository, seed_helper: true do ref = double() allow(ref).to receive(:name) { 'bad-branch' } allow(ref).to receive(:target) { raise Rugged::ReferenceError } - allow(repository.rugged).to receive(:branches) { [ref] } + branches = double() + allow(branches).to receive(:each) { [ref].each } + allow(repository.rugged).to receive(:branches) { branches } end it 'should return empty branches' do @@ -1264,7 +1291,7 @@ describe Gitlab::Git::Repository, seed_helper: true do describe '#local_branches' do before(:all) do - @repo = Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH) + @repo = Gitlab::Git::Repository.new('default', File.join(TEST_MUTABLE_REPO_PATH, '.git')) end after(:all) do @@ -1279,6 +1306,29 @@ describe Gitlab::Git::Repository, seed_helper: true do expect(@repo.local_branches.any? { |branch| branch.name == 'remote_branch' }).to eq(false) expect(@repo.local_branches.any? { |branch| branch.name == 'local_branch' }).to eq(true) end + + context 'with gitaly enabled' do + before { stub_gitaly } + after { Gitlab::GitalyClient.clear_stubs! } + + it 'gets the branches from GitalyClient' do + expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:local_branches). + and_return([]) + @repo.local_branches + end + + it 'wraps GRPC not found' do + expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:local_branches). + and_raise(GRPC::NotFound) + expect { @repo.local_branches }.to raise_error(Gitlab::Git::Repository::NoRepository) + end + + it 'wraps GRPC exceptions' do + expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:local_branches). + and_raise(GRPC::Unknown) + expect { @repo.local_branches }.to raise_error(Gitlab::Git::CommandError) + end + end end def create_remote_branch(remote_name, branch_name, source_branch_name) diff --git a/spec/lib/gitlab/gitaly_client/ref_spec.rb b/spec/lib/gitlab/gitaly_client/ref_spec.rb index 255f23e6270..d8cd2dcbd2a 100644 --- a/spec/lib/gitlab/gitaly_client/ref_spec.rb +++ b/spec/lib/gitlab/gitaly_client/ref_spec.rb @@ -9,6 +9,13 @@ describe Gitlab::GitalyClient::Ref do allow(Gitlab.config.gitaly).to receive(:enabled).and_return(true) end + after do + # When we say `expect_any_instance_of(Gitaly::Ref::Stub)` a double is created, + # and because GitalyClient shares stubs these will get passed from example to + # example, which will cause an error, so we clean the stubs after each example. + Gitlab::GitalyClient.clear_stubs! + end + describe '#branch_names' do it 'sends a find_all_branch_names message' do expect_any_instance_of(Gitaly::Ref::Stub). @@ -38,4 +45,27 @@ describe Gitlab::GitalyClient::Ref do client.default_branch_name end end + + describe '#local_branches' do + it 'sends a find_local_branches message' do + expect_any_instance_of(Gitaly::Ref::Stub). + to receive(:find_local_branches).with(gitaly_request_with_repo_path(repo_path)). + and_return([]) + + client.local_branches + end + + it 'parses and sends the sort parameter' do + expect_any_instance_of(Gitaly::Ref::Stub). + to receive(:find_local_branches). + with(gitaly_request_with_params(sort_by: :UPDATED_DESC)). + and_return([]) + + client.local_branches(sort_by: 'updated_desc') + end + + it 'raises an argument error if an invalid sort_by parameter is passed' do + expect { client.local_branches(sort_by: 'invalid_sort') }.to raise_error(ArgumentError) + end + end end diff --git a/spec/lib/gitlab/import_export/project.json b/spec/lib/gitlab/import_export/project.json index fdbb6a0556d..e3599d6fe59 100644 --- a/spec/lib/gitlab/import_export/project.json +++ b/spec/lib/gitlab/import_export/project.json @@ -6997,7 +6997,7 @@ "merge_requests_events": true, "tag_push_events": true, "note_events": true, - "build_events": true, + "job_events": true, "type": "TeamcityService", "category": "ci", "default": false, @@ -7041,7 +7041,7 @@ "merge_requests_events": true, "tag_push_events": true, "note_events": true, - "build_events": true, + "job_events": true, "type": "RedmineService", "category": "issue_tracker", "default": false, @@ -7063,7 +7063,7 @@ "merge_requests_events": true, "tag_push_events": true, "note_events": true, - "build_events": true, + "job_events": true, "type": "PushoverService", "category": "common", "default": false, @@ -7085,7 +7085,7 @@ "merge_requests_events": true, "tag_push_events": true, "note_events": true, - "build_events": true, + "job_events": true, "type": "PivotalTrackerService", "category": "common", "default": false, @@ -7108,7 +7108,7 @@ "merge_requests_events": true, "tag_push_events": true, "note_events": true, - "build_events": true, + "job_events": true, "type": "JiraService", "category": "issue_tracker", "default": false, @@ -7130,7 +7130,7 @@ "merge_requests_events": true, "tag_push_events": true, "note_events": true, - "build_events": true, + "job_events": true, "type": "IrkerService", "category": "common", "default": false, @@ -7174,7 +7174,7 @@ "merge_requests_events": true, "tag_push_events": true, "note_events": true, - "build_events": true, + "job_events": true, "type": "GemnasiumService", "category": "common", "default": false, @@ -7196,7 +7196,7 @@ "merge_requests_events": true, "tag_push_events": true, "note_events": true, - "build_events": true, + "job_events": true, "type": "FlowdockService", "category": "common", "default": false, @@ -7218,7 +7218,7 @@ "merge_requests_events": true, "tag_push_events": true, "note_events": true, - "build_events": true, + "job_events": true, "type": "ExternalWikiService", "category": "common", "default": false, @@ -7240,7 +7240,7 @@ "merge_requests_events": true, "tag_push_events": true, "note_events": true, - "build_events": true, + "job_events": true, "type": "EmailsOnPushService", "category": "common", "default": false, @@ -7262,7 +7262,7 @@ "merge_requests_events": true, "tag_push_events": true, "note_events": true, - "build_events": true, + "job_events": true, "type": "DroneCiService", "category": "ci", "default": false, @@ -7284,7 +7284,7 @@ "merge_requests_events": true, "tag_push_events": true, "note_events": true, - "build_events": true, + "job_events": true, "type": "CustomIssueTrackerService", "category": "issue_tracker", "default": false, @@ -7306,7 +7306,7 @@ "merge_requests_events": true, "tag_push_events": true, "note_events": true, - "build_events": true, + "job_events": true, "type": "CampfireService", "category": "common", "default": false, @@ -7328,7 +7328,7 @@ "merge_requests_events": true, "tag_push_events": true, "note_events": true, - "build_events": true, + "job_events": true, "type": "BuildkiteService", "category": "ci", "default": false, @@ -7350,7 +7350,7 @@ "merge_requests_events": true, "tag_push_events": true, "note_events": true, - "build_events": true, + "job_events": true, "type": "BambooService", "category": "ci", "default": false, @@ -7372,7 +7372,7 @@ "merge_requests_events": true, "tag_push_events": true, "note_events": true, - "build_events": true, + "job_events": true, "type": "AssemblaService", "category": "common", "default": false, @@ -7394,7 +7394,7 @@ "merge_requests_events": true, "tag_push_events": true, "note_events": true, - "build_events": true, + "job_events": true, "type": "AssemblaService", "category": "common", "default": false, @@ -7416,7 +7416,7 @@ "merge_requests_events": true, "tag_push_events": true, "note_events": true, - "build_events": true, + "job_events": true, "category": "common", "default": false, "wiki_page_events": true, diff --git a/spec/lib/gitlab/import_export/relation_factory_spec.rb b/spec/lib/gitlab/import_export/relation_factory_spec.rb index 744fed44925..5417c7534ea 100644 --- a/spec/lib/gitlab/import_export/relation_factory_spec.rb +++ b/spec/lib/gitlab/import_export/relation_factory_spec.rb @@ -33,7 +33,7 @@ describe Gitlab::ImportExport::RelationFactory, lib: true do 'tag_push_events' => false, 'note_events' => true, 'enable_ssl_verification' => true, - 'build_events' => false, + 'job_events' => false, 'wiki_page_events' => true, 'token' => token } diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml index 2b95f76e045..96054c996fd 100644 --- a/spec/lib/gitlab/import_export/safe_model_attributes.yml +++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml @@ -54,6 +54,7 @@ Note: - type - position - original_position +- change_position - resolved_at - resolved_by_id - discussion_id @@ -292,7 +293,7 @@ Service: - tag_push_events - note_events - pipeline_events -- build_events +- job_events - category - default - wiki_page_events @@ -312,7 +313,7 @@ ProjectHook: - note_events - pipeline_events - enable_ssl_verification -- build_events +- job_events - wiki_page_events - token - group_id diff --git a/spec/lib/gitlab/prometheus/queries/deployment_query_spec.rb b/spec/lib/gitlab/prometheus/queries/deployment_query_spec.rb index cc7d7e57f06..d957dd932c4 100644 --- a/spec/lib/gitlab/prometheus/queries/deployment_query_spec.rb +++ b/spec/lib/gitlab/prometheus/queries/deployment_query_spec.rb @@ -8,27 +8,28 @@ describe Gitlab::Prometheus::Queries::DeploymentQuery, lib: true do subject { described_class.new(client) } around do |example| - Timecop.freeze { example.run } + time_without_subsecond_values = Time.local(2008, 9, 1, 12, 0, 0) + Timecop.freeze(time_without_subsecond_values) { example.run } end it 'sends appropriate queries to prometheus' do - start_time_matcher = be_within(0.5).of((deployment.created_at - 30.minutes).to_f) - stop_time_matcher = be_within(0.5).of((deployment.created_at + 30.minutes).to_f) - created_at_matcher = be_within(0.5).of(deployment.created_at.to_f) + start_time = (deployment.created_at - 30.minutes).to_f + stop_time = (deployment.created_at + 30.minutes).to_f + created_at = deployment.created_at.to_f expect(client).to receive(:query_range).with('avg(container_memory_usage_bytes{container_name!="POD",environment="environment-slug"}) / 2^20', - start: start_time_matcher, stop: stop_time_matcher) + start: start_time, stop: stop_time) expect(client).to receive(:query).with('avg(avg_over_time(container_memory_usage_bytes{container_name!="POD",environment="environment-slug"}[30m]))', - time: created_at_matcher) + time: created_at) expect(client).to receive(:query).with('avg(avg_over_time(container_memory_usage_bytes{container_name!="POD",environment="environment-slug"}[30m]))', - time: stop_time_matcher) + time: stop_time) expect(client).to receive(:query_range).with('avg(rate(container_cpu_usage_seconds_total{container_name!="POD",environment="environment-slug"}[2m])) * 100', - start: start_time_matcher, stop: stop_time_matcher) + start: start_time, stop: stop_time) expect(client).to receive(:query).with('avg(rate(container_cpu_usage_seconds_total{container_name!="POD",environment="environment-slug"}[30m])) * 100', - time: created_at_matcher) + time: created_at) expect(client).to receive(:query).with('avg(rate(container_cpu_usage_seconds_total{container_name!="POD",environment="environment-slug"}[30m])) * 100', - time: stop_time_matcher) + time: stop_time) expect(subject.query(deployment.id)).to eq(memory_values: nil, memory_before: nil, memory_after: nil, cpu_values: nil, cpu_before: nil, cpu_after: nil) diff --git a/spec/lib/gitlab/regex_spec.rb b/spec/lib/gitlab/regex_spec.rb index 72e947f2cc2..a7d1283acb8 100644 --- a/spec/lib/gitlab/regex_spec.rb +++ b/spec/lib/gitlab/regex_spec.rb @@ -2,9 +2,377 @@ require 'spec_helper' describe Gitlab::Regex, lib: true do + # Pass in a full path to remove the format segment: + # `/ci/lint(.:format)` -> `/ci/lint` + def without_format(path) + path.split('(', 2)[0] + end + + # Pass in a full path and get the last segment before a wildcard + # That's not a parameter + # `/*namespace_id/:project_id/builds/artifacts/*ref_name_and_path` + # -> 'builds/artifacts' + def path_before_wildcard(path) + path = path.gsub(STARTING_WITH_NAMESPACE, "") + path_segments = path.split('/').reject(&:empty?) + wildcard_index = path_segments.index { |segment| parameter?(segment) } + + segments_before_wildcard = path_segments[0..wildcard_index - 1] + + segments_before_wildcard.join('/') + end + + def parameter?(segment) + segment =~ /[*:]/ + end + + # If the path is reserved. Then no conflicting paths can# be created for any + # route using this reserved word. + # + # Both `builds/artifacts` & `build` are covered by reserving the word + # `build` + def wildcards_include?(path) + described_class::PROJECT_WILDCARD_ROUTES.include?(path) || + described_class::PROJECT_WILDCARD_ROUTES.include?(path.split('/').first) + end + + def failure_message(missing_words, constant_name, migration_helper) + missing_words = Array(missing_words) + <<-MSG + Found new routes that could cause conflicts with existing namespaced routes + for groups or projects. + + Add <#{missing_words.join(', ')}> to `Gitlab::Regex::#{constant_name} + to make sure no projects or namespaces can be created with those paths. + + To rename any existing records with those paths you can use the + `Gitlab::Database::RenameReservedpathsMigration::<VERSION>.#{migration_helper}` + migration helper. + + Make sure to make a note of the renamed records in the release blog post. + + MSG + end + + let(:all_routes) do + route_set = Rails.application.routes + routes_collection = route_set.routes + routes_array = routes_collection.routes + routes_array.map { |route| route.path.spec.to_s } + end + + let(:routes_without_format) { all_routes.map { |path| without_format(path) } } + + # Routes not starting with `/:` or `/*` + # all routes not starting with a param + let(:routes_not_starting_in_wildcard) { routes_without_format.select { |p| p !~ %r{^/[:*]} } } + + let(:top_level_words) do + routes_not_starting_in_wildcard.map do |route| + route.split('/')[1] + end.compact.uniq + end + + # All routes that start with a namespaced path, that have 1 or more + # path-segments before having another wildcard parameter. + # - Starting with paths: + # - `/*namespace_id/:project_id/` + # - `/*namespace_id/:id/` + # - Followed by one or more path-parts not starting with `:` or `*` + # - Followed by a path-part that includes a wildcard parameter `*` + # At the time of writing these routes match: http://rubular.com/r/Rv2pDE5Dvw + STARTING_WITH_NAMESPACE = %r{^/\*namespace_id/:(project_)?id} + NON_PARAM_PARTS = %r{[^:*][a-z\-_/]*} + ANY_OTHER_PATH_PART = %r{[a-z\-_/:]*} + WILDCARD_SEGMENT = %r{\*} + let(:namespaced_wildcard_routes) do + routes_without_format.select do |p| + p =~ %r{#{STARTING_WITH_NAMESPACE}/#{NON_PARAM_PARTS}/#{ANY_OTHER_PATH_PART}#{WILDCARD_SEGMENT}} + end + end + + # This will return all paths that are used in a namespaced route + # before another wildcard path: + # + # /*namespace_id/:project_id/builds/artifacts/*ref_name_and_path + # /*namespace_id/:project_id/info/lfs/objects/*oid + # /*namespace_id/:project_id/commits/*id + # /*namespace_id/:project_id/builds/:build_id/artifacts/file/*path + # -> ['builds/artifacts', 'info/lfs/objects', 'commits', 'artifacts/file'] + let(:all_wildcard_paths) do + namespaced_wildcard_routes.map do |route| + path_before_wildcard(route) + end.uniq + end + + STARTING_WITH_GROUP = %r{^/groups/\*(group_)?id/} + let(:group_routes) do + routes_without_format.select do |path| + path =~ STARTING_WITH_GROUP + end + end + + let(:paths_after_group_id) do + group_routes.map do |route| + route.gsub(STARTING_WITH_GROUP, '').split('/').first + end.uniq + end + + describe 'TOP_LEVEL_ROUTES' do + it 'includes all the top level namespaces' do + failure_block = lambda do + missing_words = top_level_words - described_class::TOP_LEVEL_ROUTES + failure_message(missing_words, 'TOP_LEVEL_ROUTES', 'rename_root_paths') + end + + expect(described_class::TOP_LEVEL_ROUTES) + .to include(*top_level_words), failure_block + end + end + + describe 'GROUP_ROUTES' do + it "don't contain a second wildcard" do + failure_block = lambda do + missing_words = paths_after_group_id - described_class::GROUP_ROUTES + failure_message(missing_words, 'GROUP_ROUTES', 'rename_child_paths') + end + + expect(described_class::GROUP_ROUTES) + .to include(*paths_after_group_id), failure_block + end + end + + describe 'PROJECT_WILDCARD_ROUTES' do + it 'includes all paths that can be used after a namespace/project path' do + aggregate_failures do + all_wildcard_paths.each do |path| + expect(wildcards_include?(path)) + .to be(true), failure_message(path, 'PROJECT_WILDCARD_ROUTES', 'rename_wildcard_paths') + end + end + end + end + + describe '.root_namespace_path_regex' do + subject { described_class.root_namespace_path_regex } + + it 'rejects top level routes' do + expect(subject).not_to match('admin/') + expect(subject).not_to match('api/') + expect(subject).not_to match('.well-known/') + end + + it 'accepts project wildcard routes' do + expect(subject).to match('blob/') + expect(subject).to match('edit/') + expect(subject).to match('wikis/') + end + + it 'accepts group routes' do + expect(subject).to match('activity/') + expect(subject).to match('group_members/') + expect(subject).to match('subgroups/') + end + + it 'is not case sensitive' do + expect(subject).not_to match('Users/') + end + + it 'does not allow extra slashes' do + expect(subject).not_to match('/blob/') + expect(subject).not_to match('blob//') + end + end + + describe '.full_namespace_path_regex' do + subject { described_class.full_namespace_path_regex } + + context 'at the top level' do + context 'when the final level' do + it 'rejects top level routes' do + expect(subject).not_to match('admin/') + expect(subject).not_to match('api/') + expect(subject).not_to match('.well-known/') + end + + it 'accepts project wildcard routes' do + expect(subject).to match('blob/') + expect(subject).to match('edit/') + expect(subject).to match('wikis/') + end + + it 'accepts group routes' do + expect(subject).to match('activity/') + expect(subject).to match('group_members/') + expect(subject).to match('subgroups/') + end + end + + context 'when more levels follow' do + it 'rejects top level routes' do + expect(subject).not_to match('admin/more/') + expect(subject).not_to match('api/more/') + expect(subject).not_to match('.well-known/more/') + end + + it 'accepts project wildcard routes' do + expect(subject).to match('blob/more/') + expect(subject).to match('edit/more/') + expect(subject).to match('wikis/more/') + expect(subject).to match('environments/folders/') + expect(subject).to match('info/lfs/objects/') + end + + it 'accepts group routes' do + expect(subject).to match('activity/more/') + expect(subject).to match('group_members/more/') + expect(subject).to match('subgroups/more/') + end + end + end + + context 'at the second level' do + context 'when the final level' do + it 'accepts top level routes' do + expect(subject).to match('root/admin/') + expect(subject).to match('root/api/') + expect(subject).to match('root/.well-known/') + end + + it 'rejects project wildcard routes' do + expect(subject).not_to match('root/blob/') + expect(subject).not_to match('root/edit/') + expect(subject).not_to match('root/wikis/') + expect(subject).not_to match('root/environments/folders/') + expect(subject).not_to match('root/info/lfs/objects/') + end + + it 'rejects group routes' do + expect(subject).not_to match('root/activity/') + expect(subject).not_to match('root/group_members/') + expect(subject).not_to match('root/subgroups/') + end + end + + context 'when more levels follow' do + it 'accepts top level routes' do + expect(subject).to match('root/admin/more/') + expect(subject).to match('root/api/more/') + expect(subject).to match('root/.well-known/more/') + end + + it 'rejects project wildcard routes' do + expect(subject).not_to match('root/blob/more/') + expect(subject).not_to match('root/edit/more/') + expect(subject).not_to match('root/wikis/more/') + expect(subject).not_to match('root/environments/folders/more/') + expect(subject).not_to match('root/info/lfs/objects/more/') + end + + it 'rejects group routes' do + expect(subject).not_to match('root/activity/more/') + expect(subject).not_to match('root/group_members/more/') + expect(subject).not_to match('root/subgroups/more/') + end + end + end + + it 'is not case sensitive' do + expect(subject).not_to match('root/Blob/') + end + + it 'does not allow extra slashes' do + expect(subject).not_to match('/root/admin/') + expect(subject).not_to match('root/admin//') + end + end + describe '.project_path_regex' do subject { described_class.project_path_regex } + it 'accepts top level routes' do + expect(subject).to match('admin/') + expect(subject).to match('api/') + expect(subject).to match('.well-known/') + end + + it 'rejects project wildcard routes' do + expect(subject).not_to match('blob/') + expect(subject).not_to match('edit/') + expect(subject).not_to match('wikis/') + expect(subject).not_to match('environments/folders/') + expect(subject).not_to match('info/lfs/objects/') + end + + it 'accepts group routes' do + expect(subject).to match('activity/') + expect(subject).to match('group_members/') + expect(subject).to match('subgroups/') + end + + it 'is not case sensitive' do + expect(subject).not_to match('Blob/') + end + + it 'does not allow extra slashes' do + expect(subject).not_to match('/admin/') + expect(subject).not_to match('admin//') + end + end + + describe '.full_project_path_regex' do + subject { described_class.full_project_path_regex } + + it 'accepts top level routes' do + expect(subject).to match('root/admin/') + expect(subject).to match('root/api/') + expect(subject).to match('root/.well-known/') + end + + it 'rejects project wildcard routes' do + expect(subject).not_to match('root/blob/') + expect(subject).not_to match('root/edit/') + expect(subject).not_to match('root/wikis/') + expect(subject).not_to match('root/environments/folders/') + expect(subject).not_to match('root/info/lfs/objects/') + end + + it 'accepts group routes' do + expect(subject).to match('root/activity/') + expect(subject).to match('root/group_members/') + expect(subject).to match('root/subgroups/') + end + + it 'is not case sensitive' do + expect(subject).not_to match('root/Blob/') + end + + it 'does not allow extra slashes' do + expect(subject).not_to match('/root/admin/') + expect(subject).not_to match('root/admin//') + end + end + + describe '.namespace_regex' do + subject { described_class.namespace_regex } + + it { is_expected.to match('gitlab-ce') } + it { is_expected.to match('gitlab_git') } + it { is_expected.to match('_underscore.js') } + it { is_expected.to match('100px.com') } + it { is_expected.to match('gitlab.org') } + it { is_expected.not_to match('?gitlab') } + it { is_expected.not_to match('git lab') } + it { is_expected.not_to match('gitlab.git') } + it { is_expected.not_to match('gitlab.org.') } + it { is_expected.not_to match('gitlab.org/') } + it { is_expected.not_to match('/gitlab.org') } + it { is_expected.not_to match('gitlab git') } + end + + describe '.project_path_format_regex' do + subject { described_class.project_path_format_regex } + it { is_expected.to match('gitlab-ce') } it { is_expected.to match('gitlab_git') } it { is_expected.to match('_underscore.js') } diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb index 2c46920456b..b47e1b56fa9 100644 --- a/spec/lib/gitlab/usage_data_spec.rb +++ b/spec/lib/gitlab/usage_data_spec.rb @@ -50,7 +50,6 @@ describe Gitlab::UsageData do pages_domains protected_branches releases - services snippets todos uploads diff --git a/spec/migrations/fix_wrongly_renamed_routes_spec.rb b/spec/migrations/fix_wrongly_renamed_routes_spec.rb new file mode 100644 index 00000000000..148290b0e7d --- /dev/null +++ b/spec/migrations/fix_wrongly_renamed_routes_spec.rb @@ -0,0 +1,73 @@ +require 'spec_helper' +require Rails.root.join('db', 'post_migrate', '20170518231126_fix_wrongly_renamed_routes.rb') + +describe FixWronglyRenamedRoutes, truncate: true do + let(:subject) { described_class.new } + let(:broken_namespace) do + namespace = create(:group, name: 'apiis') + namespace.route.update_attribute(:path, 'api0is') + namespace + end + + describe '#wrongly_renamed' do + it "includes routes that have names that don't match their namespace" do + broken_namespace + _other_namespace = create(:group, name: 'api0') + + expect(subject.wrongly_renamed.map(&:id)) + .to contain_exactly(broken_namespace.route.id) + end + end + + describe "#paths_and_corrections" do + it 'finds the wrong path and gets the correction from the namespace' do + broken_namespace + namespace = create(:group, name: 'uploads-test') + namespace.route.update_attribute(:path, 'uploads0-test') + + expected_result = [ + { 'namespace_path' => 'apiis', 'path' => 'api0is' }, + { 'namespace_path' => 'uploads-test', 'path' => 'uploads0-test' } + ] + + expect(subject.paths_and_corrections).to include(*expected_result) + end + end + + describe '#routes_in_namespace_query' do + it 'includes only the required routes' do + namespace = create(:group, path: 'hello') + project = create(:empty_project, namespace: namespace) + _other_namespace = create(:group, path: 'hello0') + + result = Route.where(subject.routes_in_namespace_query('hello')) + + expect(result).to contain_exactly(namespace.route, project.route) + end + end + + describe '#up' do + let(:broken_project) do + project = create(:empty_project, namespace: broken_namespace, path: 'broken-project') + project.route.update_attribute(:path, 'api0is/broken-project') + project + end + + it 'renames incorrectly named routes' do + broken_project + + subject.up + + expect(broken_project.route.reload.path).to eq('apiis/broken-project') + expect(broken_namespace.route.reload.path).to eq('apiis') + end + + it "doesn't touch namespaces that look like something that should be renamed" do + namespace = create(:group, path: 'api0') + + subject.up + + expect(namespace.route.reload.path).to eq('api0') + end + end +end diff --git a/spec/migrations/migrate_build_events_to_pipeline_events_spec.rb b/spec/migrations/migrate_build_events_to_pipeline_events_spec.rb deleted file mode 100644 index 57eb03e3c80..00000000000 --- a/spec/migrations/migrate_build_events_to_pipeline_events_spec.rb +++ /dev/null @@ -1,74 +0,0 @@ -require 'spec_helper' -require Rails.root.join('db', 'post_migrate', '20170301205640_migrate_build_events_to_pipeline_events.rb') - -# This migration uses multiple threads, and thus different transactions. This -# means data created in this spec may not be visible to some threads. To work -# around this we use the TRUNCATE cleaning strategy. -describe MigrateBuildEventsToPipelineEvents, truncate: true do - let(:migration) { described_class.new } - let(:project_with_pipeline_service) { create(:empty_project) } - let(:project_with_build_service) { create(:empty_project) } - - before do - ActiveRecord::Base.connection.execute <<-SQL - INSERT INTO services (properties, build_events, pipeline_events, type) - VALUES - ('{"notify_only_broken_builds":true}', true, false, 'SlackService') - , ('{"notify_only_broken_builds":true}', true, false, 'MattermostService') - , ('{"notify_only_broken_builds":true}', true, false, 'HipchatService') - ; - SQL - - ActiveRecord::Base.connection.execute <<-SQL - INSERT INTO services - (properties, build_events, pipeline_events, type, project_id) - VALUES - ('{"notify_only_broken_builds":true}', true, false, - 'BuildsEmailService', #{project_with_pipeline_service.id}) - , ('{"notify_only_broken_pipelines":true}', false, true, - 'PipelinesEmailService', #{project_with_pipeline_service.id}) - , ('{"notify_only_broken_builds":true}', true, false, - 'BuildsEmailService', #{project_with_build_service.id}) - ; - SQL - end - - describe '#up' do - before do - silence_migration = Module.new do - # rubocop:disable Rails/Delegate - def execute(query) - connection.execute(query) - end - end - - migration.extend(silence_migration) - migration.up - end - - it 'migrates chat service properly' do - [SlackService, MattermostService, HipchatService].each do |service| - expect(service.count).to eq(1) - - verify_service_record(service.first) - end - end - - it 'migrates pipelines email service only if it has none before' do - Project.find_each do |project| - pipeline_service_count = - project.services.where(type: 'PipelinesEmailService').count - - expect(pipeline_service_count).to eq(1) - - verify_service_record(project.pipelines_email_service) - end - end - - def verify_service_record(service) - expect(service.notify_only_broken_pipelines).to be(true) - expect(service.build_events).to be(false) - expect(service.pipeline_events).to be(true) - end - end -end diff --git a/spec/migrations/migrate_user_project_view_spec.rb b/spec/migrations/migrate_user_project_view_spec.rb index dacaa834aa9..70f8e0d6082 100644 --- a/spec/migrations/migrate_user_project_view_spec.rb +++ b/spec/migrations/migrate_user_project_view_spec.rb @@ -5,7 +5,12 @@ require Rails.root.join('db', 'post_migrate', '20170406142253_migrate_user_proje describe MigrateUserProjectView do let(:migration) { described_class.new } - let!(:user) { create(:user, project_view: 'readme') } + let!(:user) { create(:user) } + + before do + # 0 is the numeric value for the old 'readme' option + user.update_column(:project_view, 0) + end describe '#up' do it 'updates project view setting with new value' do diff --git a/spec/migrations/rename_users_with_renamed_namespace_spec.rb b/spec/migrations/rename_users_with_renamed_namespace_spec.rb new file mode 100644 index 00000000000..1e9aab3d9a1 --- /dev/null +++ b/spec/migrations/rename_users_with_renamed_namespace_spec.rb @@ -0,0 +1,22 @@ +require 'spec_helper' +require Rails.root.join('db', 'post_migrate', '20170518200835_rename_users_with_renamed_namespace.rb') + +describe RenameUsersWithRenamedNamespace, truncate: true do + it 'renames a user that had their namespace renamed to the namespace path' do + other_user = create(:user, username: 'kodingu') + other_user1 = create(:user, username: 'api0') + + user = create(:user, username: "Users0") + user.update_attribute(:username, 'Users') + user1 = create(:user, username: "import0") + user1.update_attribute(:username, 'import') + + described_class.new.up + + expect(user.reload.username).to eq('Users0') + expect(user1.reload.username).to eq('import0') + + expect(other_user.reload.username).to eq('kodingu') + expect(other_user1.reload.username).to eq('api0') + end +end diff --git a/spec/migrations/update_retried_for_ci_builds_spec.rb b/spec/migrations/update_retried_for_ci_builds_spec.rb new file mode 100644 index 00000000000..3742b4dafe5 --- /dev/null +++ b/spec/migrations/update_retried_for_ci_builds_spec.rb @@ -0,0 +1,17 @@ +require 'spec_helper' +require Rails.root.join('db', 'post_migrate', '20170503004427_update_retried_for_ci_build.rb') + +describe UpdateRetriedForCiBuild, truncate: true do + let(:pipeline) { create(:ci_pipeline) } + let!(:build_old) { create(:ci_build, pipeline: pipeline, name: 'test') } + let!(:build_new) { create(:ci_build, pipeline: pipeline, name: 'test') } + + before do + described_class.new.up + end + + it 'updates ci_builds.is_retried' do + expect(build_old.reload).to be_retried + expect(build_new.reload).not_to be_retried + end +end diff --git a/spec/models/blob_viewer/base_spec.rb b/spec/models/blob_viewer/base_spec.rb index a6641970e1b..92fbf64a6b7 100644 --- a/spec/models/blob_viewer/base_spec.rb +++ b/spec/models/blob_viewer/base_spec.rb @@ -7,11 +7,12 @@ describe BlobViewer::Base, model: true do let(:viewer_class) do Class.new(described_class) do + include BlobViewer::ServerSide + self.extensions = %w(pdf) self.binary = true - self.max_size = 1.megabyte - self.absolute_max_size = 5.megabytes - self.client_side = false + self.overridable_max_size = 1.megabyte + self.max_size = 5.megabytes end end @@ -38,10 +39,10 @@ describe BlobViewer::Base, model: true do context 'when the file type is supported' do before do - viewer_class.file_type = :license + viewer_class.file_types = %i(license) viewer_class.binary = false end - + context 'when the binaryness matches' do let(:blob) { fake_blob(path: 'LICENSE', binary: false) } @@ -68,45 +69,45 @@ describe BlobViewer::Base, model: true do end end - describe '#too_large?' do - context 'when the blob size is larger than the max size' do + describe '#exceeds_overridable_max_size?' do + context 'when the blob size is larger than the overridable max size' do let(:blob) { fake_blob(path: 'file.pdf', size: 2.megabytes) } it 'returns true' do - expect(viewer.too_large?).to be_truthy + expect(viewer.exceeds_overridable_max_size?).to be_truthy end end - context 'when the blob size is smaller than the max size' do + context 'when the blob size is smaller than the overridable max size' do let(:blob) { fake_blob(path: 'file.pdf', size: 10.kilobytes) } it 'returns false' do - expect(viewer.too_large?).to be_falsey + expect(viewer.exceeds_overridable_max_size?).to be_falsey end end end - describe '#absolutely_too_large?' do - context 'when the blob size is larger than the absolute max size' do + describe '#exceeds_max_size?' do + context 'when the blob size is larger than the max size' do let(:blob) { fake_blob(path: 'file.pdf', size: 10.megabytes) } it 'returns true' do - expect(viewer.absolutely_too_large?).to be_truthy + expect(viewer.exceeds_max_size?).to be_truthy end end - context 'when the blob size is smaller than the absolute max size' do + context 'when the blob size is smaller than the max size' do let(:blob) { fake_blob(path: 'file.pdf', size: 2.megabytes) } it 'returns false' do - expect(viewer.absolutely_too_large?).to be_falsey + expect(viewer.exceeds_max_size?).to be_falsey end end end describe '#can_override_max_size?' do - context 'when the blob size is larger than the max size' do - context 'when the blob size is larger than the absolute max size' do + context 'when the blob size is larger than the overridable max size' do + context 'when the blob size is larger than the max size' do let(:blob) { fake_blob(path: 'file.pdf', size: 10.megabytes) } it 'returns false' do @@ -114,7 +115,7 @@ describe BlobViewer::Base, model: true do end end - context 'when the blob size is smaller than the absolute max size' do + context 'when the blob size is smaller than the max size' do let(:blob) { fake_blob(path: 'file.pdf', size: 2.megabytes) } it 'returns true' do @@ -123,7 +124,7 @@ describe BlobViewer::Base, model: true do end end - context 'when the blob size is smaller than the max size' do + context 'when the blob size is smaller than the overridable max size' do let(:blob) { fake_blob(path: 'file.pdf', size: 10.kilobytes) } it 'returns false' do @@ -138,7 +139,7 @@ describe BlobViewer::Base, model: true do viewer.override_max_size = true end - context 'when the blob size is larger than the absolute max size' do + context 'when the blob size is larger than the max size' do let(:blob) { fake_blob(path: 'file.pdf', size: 10.megabytes) } it 'returns :too_large' do @@ -146,7 +147,7 @@ describe BlobViewer::Base, model: true do end end - context 'when the blob size is smaller than the absolute max size' do + context 'when the blob size is smaller than the max size' do let(:blob) { fake_blob(path: 'file.pdf', size: 2.megabytes) } it 'returns nil' do @@ -156,7 +157,7 @@ describe BlobViewer::Base, model: true do end context 'when the max size is not overridden' do - context 'when the blob size is larger than the max size' do + context 'when the blob size is larger than the overridable max size' do let(:blob) { fake_blob(path: 'file.pdf', size: 2.megabytes) } it 'returns :too_large' do @@ -164,7 +165,7 @@ describe BlobViewer::Base, model: true do end end - context 'when the blob size is smaller than the max size' do + context 'when the blob size is smaller than the overridable max size' do let(:blob) { fake_blob(path: 'file.pdf', size: 10.kilobytes) } it 'returns nil' do @@ -172,19 +173,5 @@ describe BlobViewer::Base, model: true do end end end - - context 'when the viewer is server side but the blob is stored externally' do - let(:project) { build(:empty_project, lfs_enabled: true) } - - let(:blob) { fake_blob(path: 'file.pdf', lfs: true) } - - before do - allow(Gitlab.config.lfs).to receive(:enabled).and_return(true) - end - - it 'return :server_side_but_stored_externally' do - expect(viewer.render_error).to eq(:server_side_but_stored_externally) - end - end end end diff --git a/spec/models/blob_viewer/changelog_spec.rb b/spec/models/blob_viewer/changelog_spec.rb new file mode 100644 index 00000000000..9066c5a05ac --- /dev/null +++ b/spec/models/blob_viewer/changelog_spec.rb @@ -0,0 +1,27 @@ +require 'spec_helper' + +describe BlobViewer::Changelog, model: true do + include FakeBlobHelpers + + let(:project) { create(:project, :repository) } + let(:blob) { fake_blob(path: 'CHANGELOG') } + subject { described_class.new(blob) } + + describe '#render_error' do + context 'when there are no tags' do + before do + allow(project.repository).to receive(:tag_count).and_return(0) + end + + it 'returns :no_tags' do + expect(subject.render_error).to eq(:no_tags) + end + end + + context 'when there are tags' do + it 'returns nil' do + expect(subject.render_error).to be_nil + end + end + end +end diff --git a/spec/models/blob_viewer/composer_json_spec.rb b/spec/models/blob_viewer/composer_json_spec.rb new file mode 100644 index 00000000000..df4f1f4815c --- /dev/null +++ b/spec/models/blob_viewer/composer_json_spec.rb @@ -0,0 +1,25 @@ +require 'spec_helper' + +describe BlobViewer::ComposerJson, model: true do + include FakeBlobHelpers + + let(:project) { build(:project) } + let(:data) do + <<-SPEC.strip_heredoc + { + "name": "laravel/laravel", + "homepage": "https://laravel.com/" + } + SPEC + end + let(:blob) { fake_blob(path: 'composer.json', data: data) } + subject { described_class.new(blob) } + + describe '#package_name' do + it 'returns the package name' do + expect(subject).to receive(:prepare!) + + expect(subject.package_name).to eq('laravel/laravel') + end + end +end diff --git a/spec/models/blob_viewer/gemspec_spec.rb b/spec/models/blob_viewer/gemspec_spec.rb new file mode 100644 index 00000000000..81e932de290 --- /dev/null +++ b/spec/models/blob_viewer/gemspec_spec.rb @@ -0,0 +1,25 @@ +require 'spec_helper' + +describe BlobViewer::Gemspec, model: true do + include FakeBlobHelpers + + let(:project) { build(:project) } + let(:data) do + <<-SPEC.strip_heredoc + Gem::Specification.new do |s| + s.platform = Gem::Platform::RUBY + s.name = "activerecord" + end + SPEC + end + let(:blob) { fake_blob(path: 'activerecord.gemspec', data: data) } + subject { described_class.new(blob) } + + describe '#package_name' do + it 'returns the package name' do + expect(subject).to receive(:prepare!) + + expect(subject.package_name).to eq('activerecord') + end + end +end diff --git a/spec/models/blob_viewer/package_json_spec.rb b/spec/models/blob_viewer/package_json_spec.rb new file mode 100644 index 00000000000..5c9a9c81963 --- /dev/null +++ b/spec/models/blob_viewer/package_json_spec.rb @@ -0,0 +1,25 @@ +require 'spec_helper' + +describe BlobViewer::PackageJson, model: true do + include FakeBlobHelpers + + let(:project) { build(:project) } + let(:data) do + <<-SPEC.strip_heredoc + { + "name": "module-name", + "version": "10.3.1" + } + SPEC + end + let(:blob) { fake_blob(path: 'package.json', data: data) } + subject { described_class.new(blob) } + + describe '#package_name' do + it 'returns the package name' do + expect(subject).to receive(:prepare!) + + expect(subject.package_name).to eq('module-name') + end + end +end diff --git a/spec/models/blob_viewer/podspec_json_spec.rb b/spec/models/blob_viewer/podspec_json_spec.rb new file mode 100644 index 00000000000..42a00940bc5 --- /dev/null +++ b/spec/models/blob_viewer/podspec_json_spec.rb @@ -0,0 +1,25 @@ +require 'spec_helper' + +describe BlobViewer::PodspecJson, model: true do + include FakeBlobHelpers + + let(:project) { build(:project) } + let(:data) do + <<-SPEC.strip_heredoc + { + "name": "AFNetworking", + "version": "2.0.0" + } + SPEC + end + let(:blob) { fake_blob(path: 'AFNetworking.podspec.json', data: data) } + subject { described_class.new(blob) } + + describe '#package_name' do + it 'returns the package name' do + expect(subject).to receive(:prepare!) + + expect(subject.package_name).to eq('AFNetworking') + end + end +end diff --git a/spec/models/blob_viewer/podspec_spec.rb b/spec/models/blob_viewer/podspec_spec.rb new file mode 100644 index 00000000000..6c9f0f42d53 --- /dev/null +++ b/spec/models/blob_viewer/podspec_spec.rb @@ -0,0 +1,25 @@ +require 'spec_helper' + +describe BlobViewer::Podspec, model: true do + include FakeBlobHelpers + + let(:project) { build(:project) } + let(:data) do + <<-SPEC.strip_heredoc + Pod::Spec.new do |spec| + spec.name = 'Reachability' + spec.version = '3.1.0' + end + SPEC + end + let(:blob) { fake_blob(path: 'Reachability.podspec', data: data) } + subject { described_class.new(blob) } + + describe '#package_name' do + it 'returns the package name' do + expect(subject).to receive(:prepare!) + + expect(subject.package_name).to eq('Reachability') + end + end +end diff --git a/spec/models/blob_viewer/server_side_spec.rb b/spec/models/blob_viewer/server_side_spec.rb index ddca9b79390..f047953d540 100644 --- a/spec/models/blob_viewer/server_side_spec.rb +++ b/spec/models/blob_viewer/server_side_spec.rb @@ -22,4 +22,20 @@ describe BlobViewer::ServerSide, model: true do subject.prepare! end end + + describe '#render_error' do + context 'when the blob is stored externally' do + let(:project) { build(:empty_project, lfs_enabled: true) } + + let(:blob) { fake_blob(path: 'file.pdf', lfs: true) } + + before do + allow(Gitlab.config.lfs).to receive(:enabled).and_return(true) + end + + it 'return :server_side_but_stored_externally' do + expect(subject.render_error).to eq(:server_side_but_stored_externally) + end + end + end end diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 157d17fbb68..56b24ce62f3 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -854,6 +854,16 @@ describe Ci::Pipeline, models: true do end end end + + context 'when there is a manual action present in the pipeline' do + before do + create(:ci_build, :manual, pipeline: pipeline) + end + + it 'is not cancelable' do + expect(pipeline).not_to be_cancelable + end + end end describe '#cancel_running' do diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb index 6947affcc1e..c50b8bf7b13 100644 --- a/spec/models/commit_status_spec.rb +++ b/spec/models/commit_status_spec.rb @@ -36,6 +36,16 @@ describe CommitStatus, :models do it { is_expected.to eq(commit_status.user) } end + describe 'status state machine' do + let!(:commit_status) { create(:commit_status, :running, project: project) } + + it 'invalidates the cache after a transition' do + expect(ExpireJobCacheWorker).to receive(:perform_async).with(commit_status.id) + + commit_status.success! + end + end + describe '#started?' do subject { commit_status.started? } diff --git a/spec/models/concerns/discussion_on_diff_spec.rb b/spec/models/concerns/discussion_on_diff_spec.rb index 8571e85627c..f3e148f95f0 100644 --- a/spec/models/concerns/discussion_on_diff_spec.rb +++ b/spec/models/concerns/discussion_on_diff_spec.rb @@ -21,4 +21,30 @@ describe DiscussionOnDiff, model: true do end end end + + describe '#line_code_in_diffs' do + context 'when the discussion is active in the diff' do + let(:diff_refs) { subject.position.diff_refs } + + it 'returns the current line code' do + expect(subject.line_code_in_diffs(diff_refs)).to eq(subject.line_code) + end + end + + context 'when the discussion was created in the diff' do + let(:diff_refs) { subject.original_position.diff_refs } + + it 'returns the original line code' do + expect(subject.line_code_in_diffs(diff_refs)).to eq(subject.original_line_code) + end + end + + context 'when the discussion is unrelated to the diff' do + let(:diff_refs) { subject.project.commit(RepoHelpers.sample_commit.id).diff_refs } + + it 'returns nil' do + expect(subject.line_code_in_diffs(diff_refs)).to be_nil + end + end + end end diff --git a/spec/models/cycle_analytics/test_spec.rb b/spec/models/cycle_analytics/test_spec.rb index d0b919efcf9..fd58bd1d6ad 100644 --- a/spec/models/cycle_analytics/test_spec.rb +++ b/spec/models/cycle_analytics/test_spec.rb @@ -13,8 +13,7 @@ describe 'CycleAnalytics#test', feature: true do data_fn: lambda do |context| issue = context.create(:issue, project: context.project) merge_request = context.create_merge_request_closing_issue(issue) - pipeline = context.create(:ci_pipeline, ref: merge_request.source_branch, sha: merge_request.diff_head_sha, project: context.project) - merge_request.update(head_pipeline: pipeline) + pipeline = context.create(:ci_pipeline, ref: merge_request.source_branch, sha: merge_request.diff_head_sha, project: context.project, head_pipeline_of: merge_request) { pipeline: pipeline, issue: issue } end, start_time_conditions: [["pipeline is started", -> (context, data) { data[:pipeline].run! }]], diff --git a/spec/models/diff_discussion_spec.rb b/spec/models/diff_discussion_spec.rb index 81f338745b1..45b2f6e4beb 100644 --- a/spec/models/diff_discussion_spec.rb +++ b/spec/models/diff_discussion_spec.rb @@ -48,7 +48,7 @@ describe DiffDiscussion, model: true do end it 'returns the diff ID for the version to show' do - expect(diff_id: merge_request_diff1.id) + expect(subject.merge_request_version_params).to eq(diff_id: merge_request_diff1.id) end end @@ -65,6 +65,11 @@ describe DiffDiscussion, model: true do let(:diff_note) { create(:diff_note_on_merge_request, noteable: merge_request, project: project, position: position) } + before do + diff_note.position = diff_note.original_position + diff_note.save! + end + it 'returns the diff ID and start sha of the versions to compare' do expect(subject.merge_request_version_params).to eq(diff_id: merge_request_diff3.id, start_sha: merge_request_diff1.head_commit_sha) end diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index ce870fcc1d3..da915c49d3c 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -718,8 +718,7 @@ describe MergeRequest, models: true do describe '#head_pipeline' do describe 'when the source project exists' do it 'returns the latest pipeline' do - pipeline = create(:ci_empty_pipeline, project: subject.source_project, ref: 'master', status: 'running', sha: "123abc") - subject.update(head_pipeline: pipeline) + pipeline = create(:ci_empty_pipeline, project: subject.source_project, ref: 'master', status: 'running', sha: "123abc", head_pipeline_of: subject) expect(subject.head_pipeline).to eq(pipeline) end @@ -1214,7 +1213,7 @@ describe MergeRequest, models: true do expect(Notes::DiffPositionUpdateService).to receive(:new).with( subject.project, - nil, + subject.author, old_diff_refs: old_diff_refs, new_diff_refs: commit.diff_refs, paths: note.position.paths @@ -1223,7 +1222,7 @@ describe MergeRequest, models: true do expect_any_instance_of(Notes::DiffPositionUpdateService).to receive(:execute).with(note) expect_any_instance_of(DiffNote).to receive(:save).once - subject.reload_diff + subject.reload_diff(subject.author) end end @@ -1396,9 +1395,8 @@ describe MergeRequest, models: true do project: project, ref: merge_request.source_branch, sha: merge_request.diff_head_sha, - status: status) - - merge_request.update(head_pipeline: pipeline) + status: status, + head_pipeline_of: merge_request) pipeline end @@ -1536,4 +1534,36 @@ describe MergeRequest, models: true do end end end + + describe '#version_params_for' do + subject { create(:merge_request, importing: true) } + let(:project) { subject.project } + let!(:merge_request_diff1) { subject.merge_request_diffs.create(head_commit_sha: '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9') } + let!(:merge_request_diff2) { subject.merge_request_diffs.create(head_commit_sha: nil) } + let!(:merge_request_diff3) { subject.merge_request_diffs.create(head_commit_sha: '5937ac0a7beb003549fc5fd26fc247adbce4a52e') } + + context 'when the diff refs are for an older merge request version' do + let(:diff_refs) { merge_request_diff1.diff_refs } + + it 'returns the diff ID for the version to show' do + expect(subject.version_params_for(diff_refs)).to eq(diff_id: merge_request_diff1.id) + end + end + + context 'when the diff refs are for a comparison between merge request versions' do + let(:diff_refs) { merge_request_diff3.compare_with(merge_request_diff1.head_commit_sha).diff_refs } + + it 'returns the diff ID and start sha of the versions to compare' do + expect(subject.version_params_for(diff_refs)).to eq(diff_id: merge_request_diff3.id, start_sha: merge_request_diff1.head_commit_sha) + end + end + + context 'when the diff refs are not for a merge request version' do + let(:diff_refs) { project.commit(sample_commit.id).diff_refs } + + it 'returns nil' do + expect(subject.version_params_for(diff_refs)).to be_nil + end + end + end end diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb index 8624616316c..312302afdbb 100644 --- a/spec/models/namespace_spec.rb +++ b/spec/models/namespace_spec.rb @@ -37,7 +37,7 @@ describe Namespace, models: true do it 'rejects nested paths' do parent = create(:group, :nested, path: 'environments') - namespace = build(:project, path: 'folders', namespace: parent) + namespace = build(:group, path: 'folders', parent: parent) expect(namespace).not_to be_valid end diff --git a/spec/models/project_snippet_spec.rb b/spec/models/project_snippet_spec.rb index d9d7c0b0aaa..5fe4885eeb4 100644 --- a/spec/models/project_snippet_spec.rb +++ b/spec/models/project_snippet_spec.rb @@ -5,9 +5,6 @@ describe ProjectSnippet, models: true do it { is_expected.to belong_to(:project) } end - describe "Mass assignment" do - end - describe "Validation" do it { is_expected.to validate_presence_of(:project) } end diff --git a/spec/models/protected_branch_spec.rb b/spec/models/protected_branch_spec.rb index 179a443c43d..ca347cf92c9 100644 --- a/spec/models/protected_branch_spec.rb +++ b/spec/models/protected_branch_spec.rb @@ -7,9 +7,6 @@ describe ProtectedBranch, models: true do it { is_expected.to belong_to(:project) } end - describe "Mass assignment" do - end - describe 'Validation' do it { is_expected.to validate_presence_of(:project) } it { is_expected.to validate_presence_of(:name) } diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 61b748429d7..718b7d5e86b 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -110,22 +110,11 @@ describe Repository, models: true do end describe '#ref_name_for_sha' do - context 'ref found' do - it 'returns the ref' do - allow_any_instance_of(Gitlab::Popen).to receive(:popen). - and_return(["b8d95eb4969eefacb0a58f6a28f6803f8070e7b9 commit\trefs/environments/production/77\n", 0]) + it 'returns the ref' do + allow(repository.raw_repository).to receive(:ref_name_for_sha). + and_return('refs/environments/production/77') - expect(repository.ref_name_for_sha('bla', '0' * 40)).to eq 'refs/environments/production/77' - end - end - - context 'ref not found' do - it 'returns nil' do - allow_any_instance_of(Gitlab::Popen).to receive(:popen). - and_return(["", 0]) - - expect(repository.ref_name_for_sha('bla', '0' * 40)).to eq nil - end + expect(repository.ref_name_for_sha('bla', '0' * 40)).to eq 'refs/environments/production/77' end end @@ -1917,12 +1906,18 @@ describe Repository, models: true do describe '#is_ancestor?' do context 'Gitaly is_ancestor feature enabled' do - it "asks Gitaly server if it's an ancestor" do - commit = repository.commit - expect(repository.raw_repository).to receive(:is_ancestor?).and_call_original + let(:commit) { repository.commit } + let(:ancestor) { commit.parents.first } + + before do + allow(Gitlab::GitalyClient).to receive(:enabled?).and_return(true) allow(Gitlab::GitalyClient).to receive(:feature_enabled?).with(:is_ancestor).and_return(true) + end + + it "asks Gitaly server if it's an ancestor" do + expect_any_instance_of(Gitlab::GitalyClient::Commit).to receive(:is_ancestor).with(ancestor.id, commit.id) - expect(repository.is_ancestor?(commit.id, commit.id)).to be true + repository.is_ancestor?(ancestor.id, commit.id) end end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index f2c059010f4..aabdac4bb75 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -440,6 +440,22 @@ describe User, models: true do end end + describe 'ensure incoming email token' do + it 'has incoming email token' do + user = create(:user) + expect(user.incoming_email_token).not_to be_blank + end + end + + describe 'rss token' do + it 'ensures an rss token on read' do + user = create(:user, rss_token: nil) + rss_token = user.rss_token + expect(rss_token).not_to be_blank + expect(user.reload.rss_token).to eq rss_token + end + end + describe '#recently_sent_password_reset?' do it 'is false when reset_password_sent_at is nil' do user = build_stubbed(:user, reset_password_sent_at: nil) @@ -929,10 +945,20 @@ describe User, models: true do end context 'with a group route matching the given path' do - let!(:group) { create(:group, path: 'group_path') } + context 'when the group namespace has an owner_id (legacy data)' do + let!(:group) { create(:group, path: 'group_path', owner: user) } - it 'returns nil' do - expect(User.find_by_full_path('group_path')).to eq(nil) + it 'returns nil' do + expect(User.find_by_full_path('group_path')).to eq(nil) + end + end + + context 'when the group namespace does not have an owner_id' do + let!(:group) { create(:group, path: 'group_path') } + + it 'returns nil' do + expect(User.find_by_full_path('group_path')).to eq(nil) + end end end end @@ -1777,4 +1803,32 @@ describe User, models: true do expect(user.preferred_language).to eq('en') end end + + context '#invalidate_issue_cache_counts' do + let(:user) { build_stubbed(:user) } + + it 'invalidates cache for issue counter' do + cache_mock = double + + expect(cache_mock).to receive(:delete).with(['users', user.id, 'assigned_open_issues_count']) + + allow(Rails).to receive(:cache).and_return(cache_mock) + + user.invalidate_issue_cache_counts + end + end + + context '#invalidate_merge_request_cache_counts' do + let(:user) { build_stubbed(:user) } + + it 'invalidates cache for Merge Request counter' do + cache_mock = double + + expect(cache_mock).to receive(:delete).with(['users', user.id, 'assigned_open_merge_requests_count']) + + allow(Rails).to receive(:cache).and_return(cache_mock) + + user.invalidate_merge_request_cache_counts + end + end end diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb index 2ceb4648ece..cf232e7ff69 100644 --- a/spec/requests/api/internal_spec.rb +++ b/spec/requests/api/internal_spec.rb @@ -466,86 +466,87 @@ describe API::Internal do end end - describe 'POST /notify_post_receive' do - let(:valid_params) do - { project: project.repository.path, secret_token: secret_token } - end - - let(:valid_wiki_params) do - { project: project.wiki.repository.path, secret_token: secret_token } - end - - before do - allow(Gitlab.config.gitaly).to receive(:enabled).and_return(true) - end - - it "calls the Gitaly client with the project's repository" do - expect(Gitlab::GitalyClient::Notifications). - to receive(:new).with(gitlab_git_repository_with(path: project.repository.path)). - and_call_original - expect_any_instance_of(Gitlab::GitalyClient::Notifications). - to receive(:post_receive) - - post api("/internal/notify_post_receive"), valid_params - - expect(response).to have_http_status(200) - end - - it "calls the Gitaly client with the wiki's repository if it's a wiki" do - expect(Gitlab::GitalyClient::Notifications). - to receive(:new).with(gitlab_git_repository_with(path: project.wiki.repository.path)). - and_call_original - expect_any_instance_of(Gitlab::GitalyClient::Notifications). - to receive(:post_receive) - - post api("/internal/notify_post_receive"), valid_wiki_params - - expect(response).to have_http_status(200) - end - - it "returns 500 if the gitaly call fails" do - expect_any_instance_of(Gitlab::GitalyClient::Notifications). - to receive(:post_receive).and_raise(GRPC::Unavailable) - - post api("/internal/notify_post_receive"), valid_params - - expect(response).to have_http_status(500) - end - - context 'with a gl_repository parameter' do - let(:valid_params) do - { gl_repository: "project-#{project.id}", secret_token: secret_token } - end - - let(:valid_wiki_params) do - { gl_repository: "wiki-#{project.id}", secret_token: secret_token } - end - - it "calls the Gitaly client with the project's repository" do - expect(Gitlab::GitalyClient::Notifications). - to receive(:new).with(gitlab_git_repository_with(path: project.repository.path)). - and_call_original - expect_any_instance_of(Gitlab::GitalyClient::Notifications). - to receive(:post_receive) - - post api("/internal/notify_post_receive"), valid_params - - expect(response).to have_http_status(200) - end - - it "calls the Gitaly client with the wiki's repository if it's a wiki" do - expect(Gitlab::GitalyClient::Notifications). - to receive(:new).with(gitlab_git_repository_with(path: project.wiki.repository.path)). - and_call_original - expect_any_instance_of(Gitlab::GitalyClient::Notifications). - to receive(:post_receive) - - post api("/internal/notify_post_receive"), valid_wiki_params - - expect(response).to have_http_status(200) - end - end - end + # TODO: Uncomment when the end-point is reenabled + # describe 'POST /notify_post_receive' do + # let(:valid_params) do + # { project: project.repository.path, secret_token: secret_token } + # end + # + # let(:valid_wiki_params) do + # { project: project.wiki.repository.path, secret_token: secret_token } + # end + # + # before do + # allow(Gitlab.config.gitaly).to receive(:enabled).and_return(true) + # end + # + # it "calls the Gitaly client with the project's repository" do + # expect(Gitlab::GitalyClient::Notifications). + # to receive(:new).with(gitlab_git_repository_with(path: project.repository.path)). + # and_call_original + # expect_any_instance_of(Gitlab::GitalyClient::Notifications). + # to receive(:post_receive) + # + # post api("/internal/notify_post_receive"), valid_params + # + # expect(response).to have_http_status(200) + # end + # + # it "calls the Gitaly client with the wiki's repository if it's a wiki" do + # expect(Gitlab::GitalyClient::Notifications). + # to receive(:new).with(gitlab_git_repository_with(path: project.wiki.repository.path)). + # and_call_original + # expect_any_instance_of(Gitlab::GitalyClient::Notifications). + # to receive(:post_receive) + # + # post api("/internal/notify_post_receive"), valid_wiki_params + # + # expect(response).to have_http_status(200) + # end + # + # it "returns 500 if the gitaly call fails" do + # expect_any_instance_of(Gitlab::GitalyClient::Notifications). + # to receive(:post_receive).and_raise(GRPC::Unavailable) + # + # post api("/internal/notify_post_receive"), valid_params + # + # expect(response).to have_http_status(500) + # end + # + # context 'with a gl_repository parameter' do + # let(:valid_params) do + # { gl_repository: "project-#{project.id}", secret_token: secret_token } + # end + # + # let(:valid_wiki_params) do + # { gl_repository: "wiki-#{project.id}", secret_token: secret_token } + # end + # + # it "calls the Gitaly client with the project's repository" do + # expect(Gitlab::GitalyClient::Notifications). + # to receive(:new).with(gitlab_git_repository_with(path: project.repository.path)). + # and_call_original + # expect_any_instance_of(Gitlab::GitalyClient::Notifications). + # to receive(:post_receive) + # + # post api("/internal/notify_post_receive"), valid_params + # + # expect(response).to have_http_status(200) + # end + # + # it "calls the Gitaly client with the wiki's repository if it's a wiki" do + # expect(Gitlab::GitalyClient::Notifications). + # to receive(:new).with(gitlab_git_repository_with(path: project.wiki.repository.path)). + # and_call_original + # expect_any_instance_of(Gitlab::GitalyClient::Notifications). + # to receive(:post_receive) + # + # post api("/internal/notify_post_receive"), valid_wiki_params + # + # expect(response).to have_http_status(200) + # end + # end + # end def project_with_repo_path(path) double().tap do |fake_project| diff --git a/spec/requests/api/project_hooks_spec.rb b/spec/requests/api/project_hooks_spec.rb index aee0e17a153..0f9330b062d 100644 --- a/spec/requests/api/project_hooks_spec.rb +++ b/spec/requests/api/project_hooks_spec.rb @@ -60,7 +60,7 @@ describe API::ProjectHooks, 'ProjectHooks' do expect(json_response['merge_requests_events']).to eq(hook.merge_requests_events) expect(json_response['tag_push_events']).to eq(hook.tag_push_events) expect(json_response['note_events']).to eq(hook.note_events) - expect(json_response['job_events']).to eq(hook.build_events) + expect(json_response['job_events']).to eq(hook.job_events) expect(json_response['pipeline_events']).to eq(hook.pipeline_events) expect(json_response['wiki_page_events']).to eq(hook.wiki_page_events) expect(json_response['enable_ssl_verification']).to eq(hook.enable_ssl_verification) @@ -148,7 +148,7 @@ describe API::ProjectHooks, 'ProjectHooks' do expect(json_response['merge_requests_events']).to eq(hook.merge_requests_events) expect(json_response['tag_push_events']).to eq(hook.tag_push_events) expect(json_response['note_events']).to eq(hook.note_events) - expect(json_response['job_events']).to eq(hook.build_events) + expect(json_response['job_events']).to eq(hook.job_events) expect(json_response['pipeline_events']).to eq(hook.pipeline_events) expect(json_response['wiki_page_events']).to eq(hook.wiki_page_events) expect(json_response['enable_ssl_verification']).to eq(hook.enable_ssl_verification) diff --git a/spec/requests/api/v3/project_hooks_spec.rb b/spec/requests/api/v3/project_hooks_spec.rb index a3a4c77d09d..1969d1c7f2b 100644 --- a/spec/requests/api/v3/project_hooks_spec.rb +++ b/spec/requests/api/v3/project_hooks_spec.rb @@ -58,7 +58,7 @@ describe API::ProjectHooks, 'ProjectHooks' do expect(json_response['merge_requests_events']).to eq(hook.merge_requests_events) expect(json_response['tag_push_events']).to eq(hook.tag_push_events) expect(json_response['note_events']).to eq(hook.note_events) - expect(json_response['build_events']).to eq(hook.build_events) + expect(json_response['build_events']).to eq(hook.job_events) expect(json_response['pipeline_events']).to eq(hook.pipeline_events) expect(json_response['wiki_page_events']).to eq(hook.wiki_page_events) expect(json_response['enable_ssl_verification']).to eq(hook.enable_ssl_verification) @@ -143,7 +143,7 @@ describe API::ProjectHooks, 'ProjectHooks' do expect(json_response['merge_requests_events']).to eq(hook.merge_requests_events) expect(json_response['tag_push_events']).to eq(hook.tag_push_events) expect(json_response['note_events']).to eq(hook.note_events) - expect(json_response['build_events']).to eq(hook.build_events) + expect(json_response['build_events']).to eq(hook.job_events) expect(json_response['pipeline_events']).to eq(hook.pipeline_events) expect(json_response['wiki_page_events']).to eq(hook.wiki_page_events) expect(json_response['enable_ssl_verification']).to eq(hook.enable_ssl_verification) diff --git a/spec/requests/projects/cycle_analytics_events_spec.rb b/spec/requests/projects/cycle_analytics_events_spec.rb index d92daa345b3..d4d3c9478a0 100644 --- a/spec/requests/projects/cycle_analytics_events_spec.rb +++ b/spec/requests/projects/cycle_analytics_events_spec.rb @@ -121,8 +121,7 @@ describe 'cycle analytics events', api: true do issue.update(milestone: milestone) mr = create_merge_request_closing_issue(issue, commit_message: "References #{issue.to_reference}") - pipeline = create(:ci_empty_pipeline, status: 'created', project: project, ref: mr.source_branch, sha: mr.source_branch_sha) - mr.update(head_pipeline_id: pipeline.id) + pipeline = create(:ci_empty_pipeline, status: 'created', project: project, ref: mr.source_branch, sha: mr.source_branch_sha, head_pipeline_of: mr) pipeline.run create(:ci_build, pipeline: pipeline, status: :success, author: user) diff --git a/spec/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb index 50e96d56191..a391c046f92 100644 --- a/spec/routing/project_routing_spec.rb +++ b/spec/routing/project_routing_spec.rb @@ -243,7 +243,6 @@ describe 'project routing' do # diffs_namespace_project_merge_request GET /:namespace_id/:project_id/merge_requests/:id/diffs(.:format) projects/merge_requests#diffs # commits_namespace_project_merge_request GET /:namespace_id/:project_id/merge_requests/:id/commits(.:format) projects/merge_requests#commits # merge_namespace_project_merge_request POST /:namespace_id/:project_id/merge_requests/:id/merge(.:format) projects/merge_requests#merge - # merge_check_namespace_project_merge_request GET /:namespace_id/:project_id/merge_requests/:id/merge_check(.:format) projects/merge_requests#merge_check # ci_status_namespace_project_merge_request GET /:namespace_id/:project_id/merge_requests/:id/ci_status(.:format) projects/merge_requests#ci_status # toggle_subscription_namespace_project_merge_request POST /:namespace_id/:project_id/merge_requests/:id/toggle_subscription(.:format) projects/merge_requests#toggle_subscription # branch_from_namespace_project_merge_requests GET /:namespace_id/:project_id/merge_requests/branch_from(.:format) projects/merge_requests#branch_from @@ -272,10 +271,6 @@ describe 'project routing' do ) end - it 'to #merge_check' do - expect(get('/gitlab/gitlabhq/merge_requests/1/merge_check')).to route_to('projects/merge_requests#merge_check', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') - end - it 'to #branch_from' do expect(get('/gitlab/gitlabhq/merge_requests/branch_from')).to route_to('projects/merge_requests#branch_from', namespace_id: 'gitlab', project_id: 'gitlabhq') end @@ -467,6 +462,8 @@ describe 'project routing' do expect(get('/gitlab/gitlabhq/blob/master/app/models/compare.rb')).to route_to('projects/blob#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master/app/models/compare.rb') expect(get('/gitlab/gitlabhq/blob/master/app/models/diff.js')).to route_to('projects/blob#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master/app/models/diff.js') expect(get('/gitlab/gitlabhq/blob/master/files.scss')).to route_to('projects/blob#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master/files.scss') + expect(get('/gitlab/gitlabhq/blob/master/blob/index.js')).to route_to('projects/blob#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master/blob/index.js') + expect(get('/gitlab/gitlabhq/blob/blob/master/blob/index.js')).to route_to('projects/blob#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'blob/master/blob/index.js') end end @@ -475,6 +472,8 @@ describe 'project routing' do it 'to #show' do expect(get('/gitlab/gitlabhq/tree/master/app/models/project.rb')).to route_to('projects/tree#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master/app/models/project.rb') expect(get('/gitlab/gitlabhq/tree/master/files.scss')).to route_to('projects/tree#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master/files.scss') + expect(get('/gitlab/gitlabhq/tree/master/tree/files')).to route_to('projects/tree#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master/tree/files') + expect(get('/gitlab/gitlabhq/tree/tree/master/tree/files')).to route_to('projects/tree#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'tree/master/tree/files') end end diff --git a/spec/routing/routing_spec.rb b/spec/routing/routing_spec.rb index 9f6defe1450..a62af13cf0c 100644 --- a/spec/routing/routing_spec.rb +++ b/spec/routing/routing_spec.rb @@ -151,6 +151,10 @@ describe ProfilesController, "routing" do expect(put("/profile/reset_private_token")).to route_to('profiles#reset_private_token') end + it "to #reset_rss_token" do + expect(put("/profile/reset_rss_token")).to route_to('profiles#reset_rss_token') + end + it "to #show" do expect(get("/profile")).to route_to('profiles#show') end @@ -249,17 +253,34 @@ describe RootController, 'routing' do end end -# new_user_session GET /users/sign_in(.:format) devise/sessions#new -# user_session POST /users/sign_in(.:format) devise/sessions#create -# destroy_user_session DELETE /users/sign_out(.:format) devise/sessions#destroy -# user_omniauth_authorize /users/auth/:provider(.:format) omniauth_callbacks#passthru -# user_omniauth_callback /users/auth/:action/callback(.:format) omniauth_callbacks#(?-mix:(?!)) -# user_password POST /users/password(.:format) devise/passwords#create -# new_user_password GET /users/password/new(.:format) devise/passwords#new -# edit_user_password GET /users/password/edit(.:format) devise/passwords#edit -# PUT /users/password(.:format) devise/passwords#update describe "Authentication", "routing" do - # pending + it "GET /users/sign_in" do + expect(get("/users/sign_in")).to route_to('sessions#new') + end + + it "POST /users/sign_in" do + expect(post("/users/sign_in")).to route_to('sessions#create') + end + + it "DELETE /users/sign_out" do + expect(delete("/users/sign_out")).to route_to('sessions#destroy') + end + + it "POST /users/password" do + expect(post("/users/password")).to route_to('passwords#create') + end + + it "GET /users/password/new" do + expect(get("/users/password/new")).to route_to('passwords#new') + end + + it "GET /users/password/edit" do + expect(get("/users/password/edit")).to route_to('passwords#edit') + end + + it "PUT /users/password" do + expect(put("/users/password")).to route_to('passwords#update') + end end describe "Groups", "routing" do diff --git a/spec/serializers/merge_request_entity_spec.rb b/spec/serializers/merge_request_entity_spec.rb index bb6e83ae4bd..b75c73e78c2 100644 --- a/spec/serializers/merge_request_entity_spec.rb +++ b/spec/serializers/merge_request_entity_spec.rb @@ -65,6 +65,23 @@ describe MergeRequestEntity do .to eq(resource.merge_commit_message(include_description: true)) end + describe 'new_blob_path' do + context 'when user can push to project' do + it 'returns path' do + project.add_developer(user) + + expect(subject[:new_blob_path]) + .to eq("/#{resource.project.full_path}/new/#{resource.source_branch}") + end + end + + context 'when user cannot push to project' do + it 'returns nil' do + expect(subject[:new_blob_path]).to be_nil + end + end + end + describe 'diff_head_sha' do before do allow(resource).to receive(:diff_head_sha) { 'sha' } diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb index 1ff1438ba06..b536103ed65 100644 --- a/spec/services/ci/create_pipeline_service_spec.rb +++ b/spec/services/ci/create_pipeline_service_spec.rb @@ -27,12 +27,14 @@ describe Ci::CreatePipelineService, services: true do ) end - it { expect(pipeline).to be_kind_of(Ci::Pipeline) } - it { expect(pipeline).to be_valid } - it { expect(pipeline).to eq(project.pipelines.last) } - it { expect(pipeline).to have_attributes(user: user) } - it { expect(pipeline).to have_attributes(status: 'pending') } - it { expect(pipeline.builds.first).to be_kind_of(Ci::Build) } + it 'creates a pipeline' do + expect(pipeline).to be_kind_of(Ci::Pipeline) + expect(pipeline).to be_valid + expect(pipeline).to eq(project.pipelines.last) + expect(pipeline).to have_attributes(user: user) + expect(pipeline).to have_attributes(status: 'pending') + expect(pipeline.builds.first).to be_kind_of(Ci::Build) + end context '#update_merge_requests_head_pipeline' do it 'updates head pipeline of each merge request' do diff --git a/spec/services/issues/close_service_spec.rb b/spec/services/issues/close_service_spec.rb index 51840531711..0a1f41719f7 100644 --- a/spec/services/issues/close_service_spec.rb +++ b/spec/services/issues/close_service_spec.rb @@ -51,8 +51,10 @@ describe Issues::CloseService, services: true do end end - it { expect(issue).to be_valid } - it { expect(issue).to be_closed } + it 'closes the issue' do + expect(issue).to be_valid + expect(issue).to be_closed + end it 'sends email to user2 about assign of new issue' do email = ActionMailer::Base.deliveries.last @@ -96,9 +98,11 @@ describe Issues::CloseService, services: true do described_class.new(project, user).close_issue(issue) end - it { expect(issue).to be_valid } - it { expect(issue).to be_opened } - it { expect(todo.reload).to be_pending } + it 'closes the issue' do + expect(issue).to be_valid + expect(issue).to be_opened + expect(todo.reload).to be_pending + end end end end diff --git a/spec/services/members/authorized_destroy_service_spec.rb b/spec/services/members/authorized_destroy_service_spec.rb index ab440d18e9f..8a6732faa19 100644 --- a/spec/services/members/authorized_destroy_service_spec.rb +++ b/spec/services/members/authorized_destroy_service_spec.rb @@ -10,6 +10,27 @@ describe Members::AuthorizedDestroyService, services: true do Issue.assigned_to(user).count + MergeRequest.assigned_to(user).count end + context 'Invited users' do + # Regression spec for issue: https://gitlab.com/gitlab-org/gitlab-ce/issues/32504 + it 'destroys invited project member' do + project.team << [member_user, :developer] + + member = create :project_member, :invited, project: project + + expect { described_class.new(member, member_user).execute } + .to change { Member.count }.from(2).to(1) + end + + it 'destroys invited group member' do + group.add_developer(member_user) + + member = create :group_member, :invited, group: group + + expect { described_class.new(member, member_user).execute } + .to change { Member.count }.from(2).to(1) + end + end + context 'Group member' do it "unassigns issues and merge requests" do group.add_developer(member_user) diff --git a/spec/services/merge_requests/conflicts/list_service_spec.rb b/spec/services/merge_requests/conflicts/list_service_spec.rb index e8a305d6130..23982b9e6e1 100644 --- a/spec/services/merge_requests/conflicts/list_service_spec.rb +++ b/spec/services/merge_requests/conflicts/list_service_spec.rb @@ -25,6 +25,13 @@ describe MergeRequests::Conflicts::ListService do expect(conflicts_service(merge_request).can_be_resolved_in_ui?).to be_falsey end + it 'returns a falsey value when one of the MR branches is missing' do + merge_request = create_merge_request('conflict-resolvable') + merge_request.project.repository.rm_branch(merge_request.author, 'conflict-resolvable') + + expect(conflicts_service(merge_request).can_be_resolved_in_ui?).to be_falsey + end + it 'returns a falsey value when the MR has a missing ref after a force push' do merge_request = create_merge_request('conflict-resolvable') service = conflicts_service(merge_request) diff --git a/spec/services/merge_requests/create_service_spec.rb b/spec/services/merge_requests/create_service_spec.rb index 41752f1a01a..b70e9d534a4 100644 --- a/spec/services/merge_requests/create_service_spec.rb +++ b/spec/services/merge_requests/create_service_spec.rb @@ -27,10 +27,12 @@ describe MergeRequests::CreateService, services: true do @merge_request = service.execute end - it { expect(@merge_request).to be_valid } - it { expect(@merge_request.title).to eq('Awesome merge_request') } - it { expect(@merge_request.assignee).to be_nil } - it { expect(@merge_request.merge_params['force_remove_source_branch']).to eq('1') } + it 'creates an MR' do + expect(@merge_request).to be_valid + expect(@merge_request.title).to eq('Awesome merge_request') + expect(@merge_request.assignee).to be_nil + expect(@merge_request.merge_params['force_remove_source_branch']).to eq('1') + end it 'executes hooks with default action' do expect(service).to have_received(:execute_hooks).with(@merge_request) diff --git a/spec/services/merge_requests/merge_when_pipeline_succeeds_service_spec.rb b/spec/services/merge_requests/merge_when_pipeline_succeeds_service_spec.rb index 3ef5135e6a3..f17db70faf6 100644 --- a/spec/services/merge_requests/merge_when_pipeline_succeeds_service_spec.rb +++ b/spec/services/merge_requests/merge_when_pipeline_succeeds_service_spec.rb @@ -79,11 +79,8 @@ describe MergeRequests::MergeWhenPipelineSucceedsService do context 'when triggered by pipeline with valid ref and sha' do let(:triggering_pipeline) do create(:ci_pipeline, project: project, ref: merge_request_ref, - sha: merge_request_head, status: 'success') - end - - before do - mr_merge_if_green_enabled.update(head_pipeline: triggering_pipeline) + sha: merge_request_head, status: 'success', + head_pipeline_of: mr_merge_if_green_enabled) end it "merges all merge requests with merge when the pipeline succeeds enabled" do @@ -125,11 +122,10 @@ describe MergeRequests::MergeWhenPipelineSucceedsService do let(:conflict_pipeline) do create(:ci_pipeline, project: project, ref: mr_conflict.source_branch, - sha: mr_conflict.diff_head_sha, status: 'success') + sha: mr_conflict.diff_head_sha, status: 'success', + head_pipeline_of: mr_conflict) end - before { mr_conflict.update(head_pipeline: conflict_pipeline) } - it 'does not merge the merge request' do expect(MergeWorker).not_to receive(:perform_async) diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb index 2c8fbb46e75..d371fc68312 100644 --- a/spec/services/merge_requests/update_service_spec.rb +++ b/spec/services/merge_requests/update_service_spec.rb @@ -59,14 +59,16 @@ describe MergeRequests::UpdateService, services: true do end end - it { expect(@merge_request).to be_valid } - it { expect(@merge_request.title).to eq('New title') } - it { expect(@merge_request.assignee).to eq(user2) } - it { expect(@merge_request).to be_closed } - it { expect(@merge_request.labels.count).to eq(1) } - it { expect(@merge_request.labels.first.title).to eq(label.name) } - it { expect(@merge_request.target_branch).to eq('target') } - it { expect(@merge_request.merge_params['force_remove_source_branch']).to eq('1') } + it 'mathces base expectations' do + expect(@merge_request).to be_valid + expect(@merge_request.title).to eq('New title') + expect(@merge_request.assignee).to eq(user2) + expect(@merge_request).to be_closed + expect(@merge_request.labels.count).to eq(1) + expect(@merge_request.labels.first.title).to eq(label.name) + expect(@merge_request.target_branch).to eq('target') + expect(@merge_request.merge_params['force_remove_source_branch']).to eq('1') + end it 'executes hooks with update action' do expect(service).to have_received(:execute_hooks). @@ -148,9 +150,11 @@ describe MergeRequests::UpdateService, services: true do end end - it { expect(@merge_request).to be_valid } - it { expect(@merge_request.state).to eq('merged') } - it { expect(@merge_request.merge_error).to be_nil } + it 'merges the MR' do + expect(@merge_request).to be_valid + expect(@merge_request.state).to eq('merged') + expect(@merge_request.merge_error).to be_nil + end end context 'with finished pipeline' do @@ -167,19 +171,22 @@ describe MergeRequests::UpdateService, services: true do end end - it { expect(@merge_request).to be_valid } - it { expect(@merge_request.state).to eq('merged') } + it 'merges the MR' do + expect(@merge_request).to be_valid + expect(@merge_request.state).to eq('merged') + end end context 'with active pipeline' do before do service_mock = double - pipeline = create(:ci_pipeline_with_one_job, + create( + :ci_pipeline_with_one_job, project: project, - ref: merge_request.source_branch, - sha: merge_request.diff_head_sha) - - merge_request.update(head_pipeline: pipeline) + ref: merge_request.source_branch, + sha: merge_request.diff_head_sha, + head_pipeline_of: merge_request + ) expect(MergeRequests::MergeWhenPipelineSucceedsService).to receive(:new).with(project, user). and_return(service_mock) @@ -202,8 +209,10 @@ describe MergeRequests::UpdateService, services: true do end end - it { expect(@merge_request.state).to eq('opened') } - it { expect(@merge_request.merge_error).not_to be_nil } + it 'does not merge the MR' do + expect(@merge_request.state).to eq('opened') + expect(@merge_request.merge_error).not_to be_nil + end end context 'MR can not be merged when note sha != MR sha' do diff --git a/spec/services/notes/diff_position_update_service_spec.rb b/spec/services/notes/diff_position_update_service_spec.rb index d73ae51fbc3..380c296fd3a 100644 --- a/spec/services/notes/diff_position_update_service_spec.rb +++ b/spec/services/notes/diff_position_update_service_spec.rb @@ -2,6 +2,7 @@ require 'spec_helper' describe Notes::DiffPositionUpdateService, services: true do let(:project) { create(:project, :repository) } + let(:current_user) { project.owner } let(:create_commit) { project.commit("913c66a37b4a45b9769037c55c2d238bd0942d2e") } let(:modify_commit) { project.commit("874797c3a73b60d2187ed6e2fcabd289ff75171e") } let(:edit_commit) { project.commit("570e7b2abdd848b95f2f578043fc23bd6f6fd24d") } @@ -25,7 +26,7 @@ describe Notes::DiffPositionUpdateService, services: true do subject do described_class.new( project, - nil, + current_user, old_diff_refs: old_diff_refs, new_diff_refs: new_diff_refs, paths: [path] @@ -170,6 +171,23 @@ describe Notes::DiffPositionUpdateService, services: true do expect(note.original_position).to eq(old_position) expect(note.position).to eq(old_position) end + + it 'sets the change position' do + subject.execute(note) + + change_position = note.change_position + expect(change_position.start_sha).to eq(old_diff_refs.head_sha) + expect(change_position.head_sha).to eq(new_diff_refs.head_sha) + expect(change_position.old_line).to eq(9) + expect(change_position.new_line).to be_nil + end + + it 'creates a system note' do + expect(SystemNoteService).to receive(:diff_discussion_outdated).with( + note.to_discussion, project, current_user, instance_of(Gitlab::Diff::Position)) + + subject.execute(note) + end end end end diff --git a/spec/services/projects/propagate_service_template_spec.rb b/spec/services/projects/propagate_service_template_spec.rb index 90eff3bbc1e..8a6a9f09f74 100644 --- a/spec/services/projects/propagate_service_template_spec.rb +++ b/spec/services/projects/propagate_service_template_spec.rb @@ -71,14 +71,18 @@ describe Projects::PropagateServiceTemplate, services: true do end describe 'bulk update' do - it 'creates services for all projects' do - project_total = 5 + let(:project_total) { 5 } + + before do stub_const 'Projects::PropagateServiceTemplate::BATCH_SIZE', 3 project_total.times { create(:empty_project) } - expect { described_class.propagate(service_template) }. - to change { Service.count }.by(project_total + 1) + described_class.propagate(service_template) + end + + it 'creates services for all projects' do + expect(Service.all.reload.count).to eq(project_total + 2) end end diff --git a/spec/services/projects/transfer_service_spec.rb b/spec/services/projects/transfer_service_spec.rb index 29ccce59c53..b957517c715 100644 --- a/spec/services/projects/transfer_service_spec.rb +++ b/spec/services/projects/transfer_service_spec.rb @@ -26,6 +26,7 @@ describe Projects::TransferService, services: true do it { expect(@result).to eq false } it { expect(project.namespace).to eq(user.namespace) } + it { expect(project.errors.messages[:new_namespace].first).to eq 'Please select a new namespace for your project.' } end context 'disallow transfering of project with tags' do diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb index 7a9cd7553b1..5a7cfaff7fb 100644 --- a/spec/services/system_note_service_spec.rb +++ b/spec/services/system_note_service_spec.rb @@ -1034,4 +1034,35 @@ describe SystemNoteService, services: true do expect(subject.note).to eq 'resolved all discussions' end end + + describe '.diff_discussion_outdated' do + let(:discussion) { create(:diff_note_on_merge_request).to_discussion } + let(:merge_request) { discussion.noteable } + let(:project) { merge_request.source_project } + let(:change_position) { discussion.position } + + def reloaded_merge_request + MergeRequest.find(merge_request.id) + end + + subject { described_class.diff_discussion_outdated(discussion, project, author, change_position) } + + it_behaves_like 'a system note' do + let(:expected_noteable) { discussion.first_note.noteable } + let(:action) { 'outdated' } + end + + it 'creates a new note in the discussion' do + # we need to completely rebuild the merge request object, or the `@discussions` on the merge request are not reloaded. + expect { subject }.to change { reloaded_merge_request.discussions.first.notes.size }.by(1) + end + + it 'links to the diff in the system note' do + expect(subject.note).to include('version 1') + + diff_id = merge_request.merge_request_diff.id + line_code = change_position.line_code(project.repository) + expect(subject.note).to include(diffs_namespace_project_merge_request_url(project.namespace, project, merge_request, diff_id: diff_id, anchor: line_code)) + end + end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index e2d5928e5b2..51571ddebe9 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -10,7 +10,7 @@ require 'shoulda/matchers' require 'rspec/retry' rspec_profiling_is_configured = - ENV['RSPEC_PROFILING_POSTGRES_URL'] || + ENV['RSPEC_PROFILING_POSTGRES_URL'].present? || ENV['RSPEC_PROFILING'] branch_can_be_profiled = ENV['GITLAB_DATABASE'] == 'postgresql' && @@ -44,7 +44,6 @@ RSpec.configure do |config| config.include LoginHelpers, type: :feature config.include SearchHelpers, type: :feature config.include WaitForRequests, :js - config.include WaitForAjax, :js config.include StubConfiguration config.include EmailHelpers, type: :mailer config.include TestEnv diff --git a/spec/support/dropzone_helper.rb b/spec/support/dropzone_helper.rb index 984ec7d2741..02fdeb08afe 100644 --- a/spec/support/dropzone_helper.rb +++ b/spec/support/dropzone_helper.rb @@ -6,32 +6,52 @@ module DropzoneHelper # Dropzone events to perform the actual upload. # # This method waits for the upload to complete before returning. - def dropzone_file(file_path) + # max_file_size is an optional parameter. + # If it's not 0, then it used in dropzone.maxFilesize parameter. + # wait_for_queuecomplete is an optional parameter. + # If it's 'false', then the helper will NOT wait for backend response + # It lets to test behaviors while AJAX is processing. + def dropzone_file(files, max_file_size = 0, wait_for_queuecomplete = true) # Generate a fake file input that Capybara can attach to page.execute_script <<-JS.strip_heredoc + $('#fakeFileInput').remove(); var fakeFileInput = window.$('<input/>').attr( - {id: 'fakeFileInput', type: 'file'} + {id: 'fakeFileInput', type: 'file', multiple: true} ).appendTo('body'); window._dropzoneComplete = false; JS - # Attach the file to the fake input selector with Capybara - attach_file('fakeFileInput', file_path) + # Attach files to the fake input selector with Capybara + attach_file('fakeFileInput', files) # Manually trigger a Dropzone "drop" event with the fake input's file list page.execute_script <<-JS.strip_heredoc - var fileList = [$('#fakeFileInput')[0].files[0]]; - var e = jQuery.Event('drop', { dataTransfer : { files : fileList } }); - var dropzone = $('.div-dropzone')[0].dropzone; + dropzone.options.autoProcessQueue = false; + + if (#{max_file_size} > 0) { + dropzone.options.maxFilesize = #{max_file_size}; + } + dropzone.on('queuecomplete', function() { window._dropzoneComplete = true; }); - dropzone.listeners[0].events.drop(e); + + var fileList = [$('#fakeFileInput')[0].files]; + + $.map(fileList, function(file){ + var e = jQuery.Event('drop', { dataTransfer : { files : file } }); + + dropzone.listeners[0].events.drop(e); + }); + + dropzone.processQueue(); JS - # Wait until Dropzone's fired `queuecomplete` - loop until page.evaluate_script('window._dropzoneComplete === true') + if wait_for_queuecomplete + # Wait until Dropzone's fired `queuecomplete` + loop until page.evaluate_script('window._dropzoneComplete === true') + end end end diff --git a/spec/support/features/issuable_slash_commands_shared_examples.rb b/spec/support/features/issuable_slash_commands_shared_examples.rb index ad46b163cd6..fa82dc5e9f9 100644 --- a/spec/support/features/issuable_slash_commands_shared_examples.rb +++ b/spec/support/features/issuable_slash_commands_shared_examples.rb @@ -22,7 +22,7 @@ shared_examples 'issuable record that supports slash commands in its description after do # Ensure all outstanding Ajax requests are complete to avoid database deadlocks - wait_for_ajax + wait_for_requests end describe "new #{issuable_type}", js: true do @@ -58,7 +58,7 @@ shared_examples 'issuable record that supports slash commands in its description expect(page).not_to have_content '/label ~bug' expect(page).not_to have_content '/milestone %"ASAP"' - wait_for_ajax + wait_for_requests issuable.reload note = issuable.notes.user.first diff --git a/spec/support/features/rss_shared_examples.rb b/spec/support/features/rss_shared_examples.rb index 9a3b0a731ad..1cbb4134995 100644 --- a/spec/support/features/rss_shared_examples.rb +++ b/spec/support/features/rss_shared_examples.rb @@ -1,23 +1,23 @@ -shared_examples "an autodiscoverable RSS feed with current_user's private token" do - it "has an RSS autodiscovery link tag with current_user's private token" do - expect(page).to have_css("link[type*='atom+xml'][href*='private_token=#{Thread.current[:current_user].private_token}']", visible: false) +shared_examples "an autodiscoverable RSS feed with current_user's RSS token" do + it "has an RSS autodiscovery link tag with current_user's RSS token" do + expect(page).to have_css("link[type*='atom+xml'][href*='rss_token=#{Thread.current[:current_user].rss_token}']", visible: false) end end -shared_examples "it has an RSS button with current_user's private token" do - it "shows the RSS button with current_user's private token" do - expect(page).to have_css("a:has(.fa-rss)[href*='private_token=#{Thread.current[:current_user].private_token}']") +shared_examples "it has an RSS button with current_user's RSS token" do + it "shows the RSS button with current_user's RSS token" do + expect(page).to have_css("a:has(.fa-rss)[href*='rss_token=#{Thread.current[:current_user].rss_token}']") end end -shared_examples "an autodiscoverable RSS feed without a private token" do - it "has an RSS autodiscovery link tag without a private token" do - expect(page).to have_css("link[type*='atom+xml']:not([href*='private_token'])", visible: false) +shared_examples "an autodiscoverable RSS feed without an RSS token" do + it "has an RSS autodiscovery link tag without an RSS token" do + expect(page).to have_css("link[type*='atom+xml']:not([href*='rss_token'])", visible: false) end end -shared_examples "it has an RSS button without a private token" do - it "shows the RSS button without a private token" do - expect(page).to have_css("a:has(.fa-rss):not([href*='private_token'])") +shared_examples "it has an RSS button without an RSS token" do + it "shows the RSS button without an RSS token" do + expect(page).to have_css("a:has(.fa-rss):not([href*='rss_token'])") end end diff --git a/spec/support/matchers/gitaly_matchers.rb b/spec/support/matchers/gitaly_matchers.rb index 65dbc01f6e4..ed14bcec9f2 100644 --- a/spec/support/matchers/gitaly_matchers.rb +++ b/spec/support/matchers/gitaly_matchers.rb @@ -1,3 +1,9 @@ RSpec::Matchers.define :gitaly_request_with_repo_path do |path| match { |actual| actual.repository.path == path } end + +RSpec::Matchers.define :gitaly_request_with_params do |params| + match do |actual| + params.reduce(true) { |r, (key, val)| r && actual.send(key) == val } + end +end diff --git a/spec/support/milestone_tabs_examples.rb b/spec/support/milestone_tabs_examples.rb index c69f8e11008..4ad8b0a16e1 100644 --- a/spec/support/milestone_tabs_examples.rb +++ b/spec/support/milestone_tabs_examples.rb @@ -1,7 +1,7 @@ shared_examples 'milestone tabs' do def go(path, extra_params = {}) params = if milestone.is_a?(GlobalMilestone) - { group_id: group.id, id: milestone.safe_title, title: milestone.title } + { group_id: group.to_param, id: milestone.safe_title, title: milestone.title } else { namespace_id: project.namespace.to_param, project_id: project, id: milestone.iid } end diff --git a/spec/features/protected_branches/access_control_ce_spec.rb b/spec/support/protected_branches/access_control_ce_shared_examples.rb index 7fda4ade665..287d6bb13c3 100644 --- a/spec/features/protected_branches/access_control_ce_spec.rb +++ b/spec/support/protected_branches/access_control_ce_shared_examples.rb @@ -38,7 +38,7 @@ RSpec.shared_examples "protected branches > access control > CE" do end end - wait_for_ajax + wait_for_requests expect(ProtectedBranch.last.push_access_levels.map(&:access_level)).to include(access_type_id) end @@ -83,7 +83,7 @@ RSpec.shared_examples "protected branches > access control > CE" do end end - wait_for_ajax + wait_for_requests expect(ProtectedBranch.last.merge_access_levels.map(&:access_level)).to include(access_type_id) end diff --git a/spec/features/protected_tags/access_control_ce_spec.rb b/spec/support/protected_tags/access_control_ce_shared_examples.rb index 12622cd548a..1d11512ef82 100644 --- a/spec/features/protected_tags/access_control_ce_spec.rb +++ b/spec/support/protected_tags/access_control_ce_shared_examples.rb @@ -39,7 +39,7 @@ RSpec.shared_examples "protected tags > access control > CE" do end end - wait_for_ajax + wait_for_requests expect(ProtectedTag.last.create_access_levels.map(&:access_level)).to include(access_type_id) end diff --git a/spec/support/snippets_shared_examples.rb b/spec/support/snippets_shared_examples.rb index 57dfff3471f..85f0facd5c3 100644 --- a/spec/support/snippets_shared_examples.rb +++ b/spec/support/snippets_shared_examples.rb @@ -7,7 +7,7 @@ RSpec.shared_examples 'paginated snippets' do |remote: false| context 'clicking on the link to the second page' do before do click_link('2') - wait_for_ajax if remote + wait_for_requests if remote end it 'shows the remaining snippets' do diff --git a/spec/support/target_branch_helpers.rb b/spec/support/target_branch_helpers.rb index 3ee8f0f657e..01d1c53fe6c 100644 --- a/spec/support/target_branch_helpers.rb +++ b/spec/support/target_branch_helpers.rb @@ -1,7 +1,7 @@ module TargetBranchHelpers def select_branch(name) first('button.js-target-branch').click - wait_for_ajax + wait_for_requests all('a[data-group="Branches"]').find do |el| el.text == name end.click diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb index 9bf9dc5d4b2..b168098edea 100644 --- a/spec/support/test_env.rb +++ b/spec/support/test_env.rb @@ -40,8 +40,8 @@ module TestEnv 'wip' => 'b9238ee', 'csv' => '3dd0896', 'v1.1.0' => 'b83d6e3', - 'add-ipython-files' => '6d85bb69', - 'add-pdf-file' => 'e774ebd3' + 'add-ipython-files' => '6d85bb6', + 'add-pdf-file' => 'e774ebd' }.freeze # gitlab-test-fork is a fork of gitlab-fork, but we don't necessarily @@ -155,14 +155,14 @@ module TestEnv FORKED_BRANCH_SHA) end - def setup_repo(repo_path, repo_path_bare, repo_name, branch_sha) + def setup_repo(repo_path, repo_path_bare, repo_name, refs) clone_url = "https://gitlab.com/gitlab-org/#{repo_name}.git" unless File.directory?(repo_path) system(*%W(#{Gitlab.config.git.bin_path} clone -q #{clone_url} #{repo_path})) end - set_repo_refs(repo_path, branch_sha) + set_repo_refs(repo_path, refs) unless File.directory?(repo_path_bare) # We must copy bare repositories because we will push to them. @@ -170,13 +170,12 @@ module TestEnv end end - def copy_repo(project) - base_repo_path = File.expand_path(factory_repo_path_bare) + def copy_repo(project, bare_repo:, refs:) target_repo_path = File.expand_path(project.repository_storage_path + "/#{project.full_path}.git") FileUtils.mkdir_p(target_repo_path) - FileUtils.cp_r("#{base_repo_path}/.", target_repo_path) + FileUtils.cp_r("#{File.expand_path(bare_repo)}/.", target_repo_path) FileUtils.chmod_R 0755, target_repo_path - set_repo_refs(target_repo_path, BRANCH_SHA) + set_repo_refs(target_repo_path, refs) end def repos_path @@ -191,15 +190,6 @@ module TestEnv Gitlab.config.pages.path end - def copy_forked_repo_with_submodules(project) - base_repo_path = File.expand_path(forked_repo_path_bare) - target_repo_path = File.expand_path(project.repository_storage_path + "/#{project.full_path}.git") - FileUtils.mkdir_p(target_repo_path) - FileUtils.cp_r("#{base_repo_path}/.", target_repo_path) - FileUtils.chmod_R 0755, target_repo_path - set_repo_refs(target_repo_path, FORKED_BRANCH_SHA) - end - # When no cached assets exist, manually hit the root path to create them # # Otherwise they'd be created by the first test, often timing out and @@ -211,16 +201,20 @@ module TestEnv Capybara.current_session.visit '/' end + def factory_repo_path_bare + "#{factory_repo_path}_bare" + end + + def forked_repo_path_bare + "#{forked_repo_path}_bare" + end + private def factory_repo_path @factory_repo_path ||= Rails.root.join('tmp', 'tests', factory_repo_name) end - def factory_repo_path_bare - "#{factory_repo_path}_bare" - end - def factory_repo_name 'gitlab-test' end @@ -229,10 +223,6 @@ module TestEnv @forked_repo_path ||= Rails.root.join('tmp', 'tests', forked_repo_name) end - def forked_repo_path_bare - "#{forked_repo_path}_bare" - end - def forked_repo_name 'gitlab-test-fork' end @@ -244,19 +234,22 @@ module TestEnv end def set_repo_refs(repo_path, branch_sha) - instructions = branch_sha.map {|branch, sha| "update refs/heads/#{branch}\x00#{sha}\x00" }.join("\x00") << "\x00" + instructions = branch_sha.map { |branch, sha| "update refs/heads/#{branch}\x00#{sha}\x00" }.join("\x00") << "\x00" update_refs = %W(#{Gitlab.config.git.bin_path} update-ref --stdin -z) reset = proc do - IO.popen(update_refs, "w") {|io| io.write(instructions) } - $?.success? + Dir.chdir(repo_path) do + IO.popen(update_refs, "w") { |io| io.write(instructions) } + $?.success? + end end - Dir.chdir(repo_path) do - # Try to reset without fetching to avoid using the network. - unless reset.call - raise 'Could not fetch test seed repository.' unless system(*%W(#{Gitlab.config.git.bin_path} fetch origin)) - raise 'The fetched test seed does not contain the required revision.' unless reset.call - end + # Try to reset without fetching to avoid using the network. + unless reset.call + raise 'Could not fetch test seed repository.' unless system(*%W(#{Gitlab.config.git.bin_path} -C #{repo_path} fetch origin)) + + # Before we used Git clone's --mirror option, bare repos could end up + # with missing refs, clearing them and retrying should fix the issue. + cleanup && init unless reset.call end end end diff --git a/spec/support/time_tracking_shared_examples.rb b/spec/support/time_tracking_shared_examples.rb index 84ef46ffa27..b407b8097d2 100644 --- a/spec/support/time_tracking_shared_examples.rb +++ b/spec/support/time_tracking_shared_examples.rb @@ -8,7 +8,7 @@ shared_examples 'issuable time tracker' do it 'updates the sidebar component when estimate is added' do submit_time('/estimate 3w 1d 1h') - wait_for_ajax + wait_for_requests page.within '.time-tracking-estimate-only-pane' do expect(page).to have_content '3w 1d 1h' end @@ -17,7 +17,7 @@ shared_examples 'issuable time tracker' do it 'updates the sidebar component when spent is added' do submit_time('/spend 3w 1d 1h') - wait_for_ajax + wait_for_requests page.within '.time-tracking-spend-only-pane' do expect(page).to have_content '3w 1d 1h' end @@ -27,7 +27,7 @@ shared_examples 'issuable time tracker' do submit_time('/estimate 3w 1d 1h') submit_time('/spend 3w 1d 1h') - wait_for_ajax + wait_for_requests page.within '.time-tracking-comparison-pane' do expect(page).to have_content '3w 1d 1h' end @@ -81,5 +81,5 @@ end def submit_time(slash_command) fill_in 'note[note]', with: slash_command find('.js-comment-submit-button').trigger('click') - wait_for_ajax + wait_for_requests end diff --git a/spec/support/wait_for_ajax.rb b/spec/support/wait_for_ajax.rb deleted file mode 100644 index 508de2ee8e1..00000000000 --- a/spec/support/wait_for_ajax.rb +++ /dev/null @@ -1,18 +0,0 @@ -module WaitForAjax - def wait_for_ajax - Timeout.timeout(Capybara.default_max_wait_time) do - loop until finished_all_ajax_requests? - end - end - - def finished_all_ajax_requests? - return true unless javascript_test? - return true if page.evaluate_script('typeof jQuery === "undefined"') - - page.evaluate_script('jQuery.active').zero? - end - - def javascript_test? - Capybara.current_driver == Capybara.javascript_driver - end -end diff --git a/spec/support/wait_for_requests.rb b/spec/support/wait_for_requests.rb index d41e83ae128..05ec9026141 100644 --- a/spec/support/wait_for_requests.rb +++ b/spec/support/wait_for_requests.rb @@ -1,21 +1,31 @@ -require_relative './wait_for_ajax' -require_relative './wait_for_vue_resource' +require_relative './wait_for_requests' module WaitForRequests extend self - include WaitForAjax - include WaitForVueResource # This is inspired by http://www.salsify.com/blog/engineering/tearing-capybara-ajax-tests - def wait_for_requests_complete + def block_and_wait_for_requests_complete Gitlab::Testing::RequestBlockerMiddleware.block_requests! - wait_for('pending AJAX requests complete') do + wait_for('pending requests complete') do Gitlab::Testing::RequestBlockerMiddleware.num_active_requests.zero? end ensure Gitlab::Testing::RequestBlockerMiddleware.allow_requests! end + def wait_for_requests + wait_for('JS requests') { finished_all_requests? } + end + + private + + def finished_all_requests? + return true unless javascript_test? + + finished_all_ajax_requests? && + finished_all_vue_resource_requests? + end + # Waits until the passed block returns true def wait_for(condition_name, max_wait_time: Capybara.default_max_wait_time, polling_interval: 0.01) wait_until = Time.now + max_wait_time.seconds @@ -28,10 +38,24 @@ module WaitForRequests end end end + + def finished_all_vue_resource_requests? + page.evaluate_script('window.activeVueResources || 0').zero? + end + + def finished_all_ajax_requests? + return true if page.evaluate_script('typeof jQuery === "undefined"') + + page.evaluate_script('jQuery.active').zero? + end + + def javascript_test? + Capybara.current_driver == Capybara.javascript_driver + end end RSpec.configure do |config| config.after(:each, :js) do - wait_for_requests_complete + block_and_wait_for_requests_complete end end diff --git a/spec/support/wait_for_vue_resource.rb b/spec/support/wait_for_vue_resource.rb deleted file mode 100644 index 3bb3d9c2e51..00000000000 --- a/spec/support/wait_for_vue_resource.rb +++ /dev/null @@ -1,19 +0,0 @@ -module WaitForVueResource - def wait_for_vue_resource(spinner: true) - Timeout.timeout(Capybara.default_max_wait_time) do - loop until finished_all_vue_resource_requests? - end - end - - private - - def finished_all_vue_resource_requests? - return true unless javascript_test? - - page.evaluate_script('window.activeVueResources || 0').zero? - end - - def javascript_test? - Capybara.current_driver == Capybara.javascript_driver - end -end diff --git a/spec/tasks/gitlab/gitaly_rake_spec.rb b/spec/tasks/gitlab/gitaly_rake_spec.rb index f035504320b..4a636decafd 100644 --- a/spec/tasks/gitlab/gitaly_rake_spec.rb +++ b/spec/tasks/gitlab/gitaly_rake_spec.rb @@ -84,24 +84,24 @@ describe 'gitlab:gitaly namespace rake task' do } allow(Gitlab.config.repositories).to receive(:storages).and_return(config) - orig_stdout = $stdout - $stdout = StringIO.new - - header = '' + expected_output = '' Timecop.freeze do - header = <<~TOML + expected_output = <<~TOML # Gitaly storage configuration generated from #{Gitlab.config.source} on #{Time.current.to_s(:long)} # This is in TOML format suitable for use in Gitaly's config.toml file. + [[storage]] + name = "default" + path = "/path/to/default" + [[storage]] + name = "nfs_01" + path = "/path/to/nfs_01" TOML - run_rake_task('gitlab:gitaly:storage_config') end - output = $stdout.string - $stdout = orig_stdout - - expect(output).to include(header) + expect { run_rake_task('gitlab:gitaly:storage_config')}. + to output(expected_output).to_stdout - parsed_output = TOML.parse(output) + parsed_output = TOML.parse(expected_output) config.each do |name, params| expect(parsed_output['storage']).to include({ 'name' => name, 'path' => params['path'] }) end diff --git a/spec/tasks/tokens_spec.rb b/spec/tasks/tokens_spec.rb index 19036c7677c..b84137eb365 100644 --- a/spec/tasks/tokens_spec.rb +++ b/spec/tasks/tokens_spec.rb @@ -18,4 +18,10 @@ describe 'tokens rake tasks' do expect { run_rake_task('tokens:reset_all_email') }.to change { user.reload.incoming_email_token } end end + + describe 'reset_all_rss task' do + it 'invokes create_hooks task' do + expect { run_rake_task('tokens:reset_all_rss') }.to change { user.reload.rss_token } + end + end end diff --git a/spec/validators/dynamic_path_validator_spec.rb b/spec/validators/dynamic_path_validator_spec.rb index b114bfc1bca..03e23781d1b 100644 --- a/spec/validators/dynamic_path_validator_spec.rb +++ b/spec/validators/dynamic_path_validator_spec.rb @@ -3,246 +3,46 @@ require 'spec_helper' describe DynamicPathValidator do let(:validator) { described_class.new(attributes: [:path]) } - # Pass in a full path to remove the format segment: - # `/ci/lint(.:format)` -> `/ci/lint` - def without_format(path) - path.split('(', 2)[0] - end - - # Pass in a full path and get the last segment before a wildcard - # That's not a parameter - # `/*namespace_id/:project_id/builds/artifacts/*ref_name_and_path` - # -> 'builds/artifacts' - def path_before_wildcard(path) - path = path.gsub(STARTING_WITH_NAMESPACE, "") - path_segments = path.split('/').reject(&:empty?) - wildcard_index = path_segments.index { |segment| parameter?(segment) } - - segments_before_wildcard = path_segments[0..wildcard_index - 1] - - segments_before_wildcard.join('/') - end - - def parameter?(segment) - segment =~ /[*:]/ - end - - # If the path is reserved. Then no conflicting paths can# be created for any - # route using this reserved word. - # - # Both `builds/artifacts` & `build` are covered by reserving the word - # `build` - def wildcards_include?(path) - described_class::WILDCARD_ROUTES.include?(path) || - described_class::WILDCARD_ROUTES.include?(path.split('/').first) - end - - def failure_message(missing_words, constant_name, migration_helper) - missing_words = Array(missing_words) - <<-MSG - Found new routes that could cause conflicts with existing namespaced routes - for groups or projects. + describe '#path_valid_for_record?' do + context 'for project' do + it 'calls valid_project_path?' do + project = build(:project, path: 'activity') - Add <#{missing_words.join(', ')}> to `DynamicPathValidator::#{constant_name} - to make sure no projects or namespaces can be created with those paths. + expect(described_class).to receive(:valid_project_path?).with(project.full_path).and_call_original - To rename any existing records with those paths you can use the - `Gitlab::Database::RenameReservedpathsMigration::<VERSION>.#{migration_helper}` - migration helper. - - Make sure to make a note of the renamed records in the release blog post. - - MSG - end - - let(:all_routes) do - Rails.application.routes.routes.routes. - map { |r| r.path.spec.to_s } - end - - let(:routes_without_format) { all_routes.map { |path| without_format(path) } } - - # Routes not starting with `/:` or `/*` - # all routes not starting with a param - let(:routes_not_starting_in_wildcard) { routes_without_format.select { |p| p !~ %r{^/[:*]} } } - - let(:top_level_words) do - routes_not_starting_in_wildcard.map do |route| - route.split('/')[1] - end.compact.uniq - end - - # All routes that start with a namespaced path, that have 1 or more - # path-segments before having another wildcard parameter. - # - Starting with paths: - # - `/*namespace_id/:project_id/` - # - `/*namespace_id/:id/` - # - Followed by one or more path-parts not starting with `:` or `*` - # - Followed by a path-part that includes a wildcard parameter `*` - # At the time of writing these routes match: http://rubular.com/r/Rv2pDE5Dvw - STARTING_WITH_NAMESPACE = %r{^/\*namespace_id/:(project_)?id} - NON_PARAM_PARTS = %r{[^:*][a-z\-_/]*} - ANY_OTHER_PATH_PART = %r{[a-z\-_/:]*} - WILDCARD_SEGMENT = %r{\*} - let(:namespaced_wildcard_routes) do - routes_without_format.select do |p| - p =~ %r{#{STARTING_WITH_NAMESPACE}/#{NON_PARAM_PARTS}/#{ANY_OTHER_PATH_PART}#{WILDCARD_SEGMENT}} - end - end - - # This will return all paths that are used in a namespaced route - # before another wildcard path: - # - # /*namespace_id/:project_id/builds/artifacts/*ref_name_and_path - # /*namespace_id/:project_id/info/lfs/objects/*oid - # /*namespace_id/:project_id/commits/*id - # /*namespace_id/:project_id/builds/:build_id/artifacts/file/*path - # -> ['builds/artifacts', 'info/lfs/objects', 'commits', 'artifacts/file'] - let(:all_wildcard_paths) do - namespaced_wildcard_routes.map do |route| - path_before_wildcard(route) - end.uniq - end - - STARTING_WITH_GROUP = %r{^/groups/\*(group_)?id/} - let(:group_routes) do - routes_without_format.select do |path| - path =~ STARTING_WITH_GROUP - end - end - - let(:paths_after_group_id) do - group_routes.map do |route| - route.gsub(STARTING_WITH_GROUP, '').split('/').first - end.uniq - end - - describe 'TOP_LEVEL_ROUTES' do - it 'includes all the top level namespaces' do - failure_block = lambda do - missing_words = top_level_words - described_class::TOP_LEVEL_ROUTES - failure_message(missing_words, 'TOP_LEVEL_ROUTES', 'rename_root_paths') + expect(validator.path_valid_for_record?(project, 'activity')).to be_truthy end - - expect(described_class::TOP_LEVEL_ROUTES) - .to include(*top_level_words), failure_block end - end - describe 'GROUP_ROUTES' do - it "don't contain a second wildcard" do - failure_block = lambda do - missing_words = paths_after_group_id - described_class::GROUP_ROUTES - failure_message(missing_words, 'GROUP_ROUTES', 'rename_child_paths') - end + context 'for group' do + it 'calls valid_namespace_path?' do + group = build(:group, :nested, path: 'activity') - expect(described_class::GROUP_ROUTES) - .to include(*paths_after_group_id), failure_block - end - end + expect(described_class).to receive(:valid_namespace_path?).with(group.full_path).and_call_original - describe 'WILDCARD_ROUTES' do - it 'includes all paths that can be used after a namespace/project path' do - aggregate_failures do - all_wildcard_paths.each do |path| - expect(wildcards_include?(path)) - .to be(true), failure_message(path, 'WILDCARD_ROUTES', 'rename_wildcard_paths') - end + expect(validator.path_valid_for_record?(group, 'activity')).to be_falsey end end - end - describe '.without_reserved_wildcard_paths_regex' do - subject { described_class.without_reserved_wildcard_paths_regex } + context 'for user' do + it 'calls valid_namespace_path?' do + user = build(:user, username: 'activity') - it 'rejects paths starting with a reserved top level' do - expect(subject).not_to match('dashboard/hello/world') - expect(subject).not_to match('dashboard') - end + expect(described_class).to receive(:valid_namespace_path?).with(user.full_path).and_call_original - it 'matches valid paths with a toplevel word in a different place' do - expect(subject).to match('parent/dashboard/project-path') - end - - it 'rejects paths containing a wildcard reserved word' do - expect(subject).not_to match('hello/edit') - expect(subject).not_to match('hello/edit/in-the-middle') - expect(subject).not_to match('foo/bar1/refs/master/logs_tree') - end - - it 'matches valid paths' do - expect(subject).to match('parent/child/project-path') - end - end - - describe '.regex_excluding_child_paths' do - let(:subject) { described_class.without_reserved_child_paths_regex } - - it 'rejects paths containing a child reserved word' do - expect(subject).not_to match('hello/group_members') - expect(subject).not_to match('hello/activity/in-the-middle') - expect(subject).not_to match('foo/bar1/refs/master/logs_tree') - end - - it 'allows a child path on the top level' do - expect(subject).to match('activity/foo') - expect(subject).to match('avatar') - end - end - - describe ".valid?" do - it 'is not case sensitive' do - expect(described_class.valid?("Users")).to be_falsey - end - - it "isn't valid when the top level is reserved" do - test_path = 'u/should-be-a/reserved-word' - - expect(described_class.valid?(test_path)).to be_falsey - end - - it "isn't valid if any of the path segments is reserved" do - test_path = 'the-wildcard/wikis/is-not-allowed' - - expect(described_class.valid?(test_path)).to be_falsey - end - - it "is valid if the path doesn't contain reserved words" do - test_path = 'there-are/no-wildcards/in-this-path' - - expect(described_class.valid?(test_path)).to be_truthy - end - - it 'allows allows a child path on the last spot' do - test_path = 'there/can-be-a/project-called/labels' - - expect(described_class.valid?(test_path)).to be_truthy - end - - it 'rejects a child path somewhere else' do - test_path = 'there/can-be-no/labels/group' - - expect(described_class.valid?(test_path)).to be_falsey + expect(validator.path_valid_for_record?(user, 'activity')).to be_truthy + end end - it 'rejects paths that are in an incorrect format' do - test_path = 'incorrect/format.git' - - expect(described_class.valid?(test_path)).to be_falsey - end - end + context 'for user namespace' do + it 'calls valid_namespace_path?' do + user = create(:user, username: 'activity') + namespace = user.namespace - describe '#path_reserved_for_record?' do - it 'reserves a sub-group named activity' do - group = build(:group, :nested, path: 'activity') + expect(described_class).to receive(:valid_namespace_path?).with(namespace.full_path).and_call_original - expect(validator.path_reserved_for_record?(group, 'activity')).to be_truthy - end - - it "doesn't reserve a project called activity" do - project = build(:project, path: 'activity') - - expect(validator.path_reserved_for_record?(project, 'activity')).to be_falsey + expect(validator.path_valid_for_record?(namespace, 'activity')).to be_truthy + end end end diff --git a/spec/views/projects/_last_commit.html.haml_spec.rb b/spec/views/projects/_last_commit.html.haml_spec.rb deleted file mode 100644 index eea1695b171..00000000000 --- a/spec/views/projects/_last_commit.html.haml_spec.rb +++ /dev/null @@ -1,22 +0,0 @@ -require 'spec_helper' - -describe 'projects/_last_commit', :view do - let(:project) { create(:project, :repository) } - - context 'when there is a pipeline present for the commit' do - context 'when pipeline is blocked' do - let!(:pipeline) do - create(:ci_pipeline, :blocked, project: project, - sha: project.commit.id) - end - - it 'shows correct pipeline badge' do - render 'projects/last_commit', commit: project.commit, - project: project, - ref: :master - - expect(rendered).to have_text "blocked #{project.commit.short_id}" - end - end - end -end diff --git a/spec/views/projects/blob/_viewer.html.haml_spec.rb b/spec/views/projects/blob/_viewer.html.haml_spec.rb index 08018767624..c6b0ed8da3c 100644 --- a/spec/views/projects/blob/_viewer.html.haml_spec.rb +++ b/spec/views/projects/blob/_viewer.html.haml_spec.rb @@ -10,9 +10,9 @@ describe 'projects/blob/_viewer.html.haml', :view do include BlobViewer::Rich self.partial_name = 'text' - self.max_size = 1.megabyte - self.absolute_max_size = 5.megabytes - self.client_side = false + self.overridable_max_size = 1.megabyte + self.max_size = 5.megabytes + self.load_async = true end end @@ -35,9 +35,9 @@ describe 'projects/blob/_viewer.html.haml', :view do render partial: 'projects/blob/viewer', locals: { viewer: viewer } end - context 'when the viewer is server side' do + context 'when the viewer is loaded asynchronously' do before do - viewer_class.client_side = false + viewer_class.load_async = true end context 'when there is no render error' do @@ -65,9 +65,9 @@ describe 'projects/blob/_viewer.html.haml', :view do end end - context 'when the viewer is client side' do + context 'when the viewer is loaded synchronously' do before do - viewer_class.client_side = true + viewer_class.load_async = false end context 'when there is no render error' do diff --git a/spec/views/projects/tree/show.html.haml_spec.rb b/spec/views/projects/tree/show.html.haml_spec.rb index 835a93e620e..33eba3e6d3d 100644 --- a/spec/views/projects/tree/show.html.haml_spec.rb +++ b/spec/views/projects/tree/show.html.haml_spec.rb @@ -21,11 +21,11 @@ describe 'projects/tree/show' do let(:tree) { repository.tree(commit.id, path) } before do + assign(:id, File.join(ref, path)) assign(:ref, ref) - assign(:commit, commit) - assign(:id, commit.id) - assign(:tree, tree) assign(:path, path) + assign(:last_commit, commit) + assign(:tree, tree) end it 'displays correctly' do diff --git a/spec/workers/expire_job_cache_worker_spec.rb b/spec/workers/expire_job_cache_worker_spec.rb new file mode 100644 index 00000000000..1b614342a18 --- /dev/null +++ b/spec/workers/expire_job_cache_worker_spec.rb @@ -0,0 +1,31 @@ +require 'spec_helper' + +describe ExpireJobCacheWorker do + set(:pipeline) { create(:ci_empty_pipeline) } + let(:project) { pipeline.project } + subject { described_class.new } + + describe '#perform' do + context 'with a job in the pipeline' do + let(:job) { create(:ci_build, pipeline: pipeline) } + + it 'invalidates Etag caching for the job path' do + pipeline_path = "/#{project.full_path}/pipelines/#{pipeline.id}.json" + job_path = "/#{project.full_path}/builds/#{job.id}.json" + + expect_any_instance_of(Gitlab::EtagCaching::Store).to receive(:touch).with(pipeline_path) + expect_any_instance_of(Gitlab::EtagCaching::Store).to receive(:touch).with(job_path) + + subject.perform(job.id) + end + end + + context 'when there is no job in the pipeline' do + it 'does not change the etag store' do + expect(Gitlab::EtagCaching::Store).not_to receive(:new) + + subject.perform(9999) + end + end + end +end diff --git a/spec/workers/expire_pipeline_cache_worker_spec.rb b/spec/workers/expire_pipeline_cache_worker_spec.rb index ceba604dea2..28e5b706803 100644 --- a/spec/workers/expire_pipeline_cache_worker_spec.rb +++ b/spec/workers/expire_pipeline_cache_worker_spec.rb @@ -10,9 +10,11 @@ describe ExpirePipelineCacheWorker do it 'invalidates Etag caching for project pipelines path' do pipelines_path = "/#{project.full_path}/pipelines.json" new_mr_pipelines_path = "/#{project.full_path}/merge_requests/new.json" + pipeline_path = "/#{project.full_path}/pipelines/#{pipeline.id}.json" expect_any_instance_of(Gitlab::EtagCaching::Store).to receive(:touch).with(pipelines_path) expect_any_instance_of(Gitlab::EtagCaching::Store).to receive(:touch).with(new_mr_pipelines_path) + expect_any_instance_of(Gitlab::EtagCaching::Store).to receive(:touch).with(pipeline_path) subject.perform(pipeline.id) end diff --git a/spec/workers/pipeline_schedule_worker_spec.rb b/spec/workers/pipeline_schedule_worker_spec.rb index 91d5a16993f..9c650354d72 100644 --- a/spec/workers/pipeline_schedule_worker_spec.rb +++ b/spec/workers/pipeline_schedule_worker_spec.rb @@ -11,40 +11,53 @@ describe PipelineScheduleWorker do end before do - project.add_master(user) - stub_ci_pipeline_to_return_yaml_file - end - context 'when there is a scheduled pipeline within next_run_at' do - let(:next_run_at) { 2.days.ago } + pipeline_schedule.update_column(:next_run_at, 1.day.ago) + end + context 'when the schedule is runnable by the user' do before do - pipeline_schedule.update_column(:next_run_at, next_run_at) + project.add_master(user) end - it 'creates a new pipeline' do - expect { subject }.to change { project.pipelines.count }.by(1) - end + context 'when there is a scheduled pipeline within next_run_at' do + it 'creates a new pipeline' do + expect { subject }.to change { project.pipelines.count }.by(1) + end - it 'updates the next_run_at field' do - subject + it 'updates the next_run_at field' do + subject + + expect(pipeline_schedule.reload.next_run_at).to be > Time.now + end - expect(pipeline_schedule.reload.next_run_at).to be > Time.now + it 'sets the schedule on the pipeline' do + subject + + expect(project.pipelines.last.pipeline_schedule).to eq(pipeline_schedule) + end end - it 'sets the schedule on the pipeline' do - subject - expect(project.pipelines.last.pipeline_schedule).to eq(pipeline_schedule) + context 'inactive schedule' do + before do + pipeline_schedule.deactivate! + end + + it 'does not creates a new pipeline' do + expect { subject }.not_to change { project.pipelines.count } + end end end - context 'inactive schedule' do - before do - pipeline_schedule.update(active: false) + context 'when the schedule is not runnable by the user' do + it 'deactivates the schedule' do + subject + + expect(pipeline_schedule.reload.active).to be_falsy end - it 'does not creates a new pipeline' do + it 'does not schedule a pipeline' do expect { subject }.not_to change { project.pipelines.count } end end diff --git a/spec/workers/project_cache_worker_spec.rb b/spec/workers/project_cache_worker_spec.rb index c23ffdf99c0..a4ba5f7c943 100644 --- a/spec/workers/project_cache_worker_spec.rb +++ b/spec/workers/project_cache_worker_spec.rb @@ -45,6 +45,18 @@ describe ProjectCacheWorker do worker.perform(project.id, %w(readme)) end + + context 'with plain readme' do + it 'refreshes the method caches' do + allow(MarkupHelper).to receive(:gitlab_markdown?).and_return(false) + allow(MarkupHelper).to receive(:plain?).and_return(true) + + expect_any_instance_of(Repository).to receive(:refresh_method_caches). + with(%i(readme)). + and_call_original + worker.perform(project.id, %w(readme)) + end + end end end |